Gooey

Revision 1 as of 2010-03-02 20:01:28

Clear message

Ubuntu Opportunistic Developers Week March 2010 - Gooey Graphics with GooCanvas - Rick Spencer - Mar 2 2010

(11:00:08 AM) rickspencer3: class time?
(11:00:16 AM) rickspencer3: time to talk about goocanvas?
(11:00:48 AM) rickspencer3: okay, let us rock
(11:01:14 AM) rickspencer3: as usual I have put my class notes on the wiki:
(11:01:14 AM) rickspencer3: https://wiki.ubuntu.com/UbuntuOpportunisticDeveloperWeek/GooCanvas
(11:01:23 AM) rickspencer3: I'll be using python and pygoocanvas for code samples to talk about goocanvas today
(11:01:32 AM) rickspencer3: There is good reference documentaion for goocanvas:
(11:01:40 AM) rickspencer3: http://people.gnome.org/~gianmt/pygoocanvas/
(11:01:46 AM) rickspencer3: llive it, learn it, love it ;)
(11:01:54 AM) rickspencer3: Unfortunately, it can be a bit tough to find code samples and tutorials
(11:01:56 AM) rickspencer3: :(
(11:02:03 AM) rickspencer3: so, let's take a look
(11:02:09 AM) rickspencer3: So what is a goocanvas?
(11:02:22 AM) rickspencer3: A goocanvas is a 2d composing surface
(11:02:34 AM) rickspencer3: You can use it to make pretty much any kind of "image"
(11:02:40 AM) rickspencer3: It's kind of like an api around a drawing program
(11:02:54 AM) rickspencer3: So you can have a ton of fun using a goocanvas, because you are pretty much freed from the constraints of a widget library in creating your UI
(11:03:13 AM) rickspencer3: goocanvas is cairo under the covers
(11:03:24 AM) rickspencer3: and is designed to easily integrate into your gtk app
(11:03:31 AM) rickspencer3: so, before we get into specifics
(11:03:33 AM) rickspencer3: questions?
(11:03:50 AM) rickspencer3: and note, I will be trying Classbot for the first time, so bear with me for a bit
(11:04:09 AM) rickspencer3: no questions?
(11:04:34 AM) ClassBot: eviltwin asked: is goo canvas considered the "approved" replacement to libgnomecanvas?
(11:04:40 AM) rickspencer3: I don't know
(11:04:50 AM) rickspencer3: I like goocanvas for it's capabilities
(11:06:45 AM) ClassBot: setimike asked: Is goocanvas sort of like glade?
(11:06:48 AM) rickspencer3: not at all
(11:07:07 AM) rickspencer3: glade is a tool which creates XML files that describe gtk layouts
(11:07:27 AM) rickspencer3: goocanvas is a widget which you embed in your program and then use as a drawing surface
(11:07:33 AM) rickspencer3: let's take a look
(11:07:44 AM) ClassBot: nadako asked: is it some OO-wrapper for basic cairo drawing things?
(11:07:46 AM) rickspencer3: yes
(11:07:58 AM) ClassBot: luismmontielg asked: How do we get pygoocanvas?
(11:08:02 AM) rickspencer3: from the repos
(11:08:15 AM) ClassBot: damo12 asked: Can glade add a widget area for pygoocanvas?
(11:08:25 AM) rickspencer3: I don't think there is a way to do that
(11:08:30 AM) rickspencer3: but I will show you how to add it with code
(11:08:49 AM) rickspencer3: So let's add a goocanvas to a pygtk app
(11:08:56 AM) rickspencer3: Add it just like a normal pygtk widget
(11:09:00 AM) rickspencer3: here's some code
(11:09:08 AM) rickspencer3: self.__goo_canvas = goocanvas.Canvas()
(11:09:08 AM) rickspencer3: self.__goo_canvas.set_size_request(640, 480)
(11:09:08 AM) rickspencer3: self.__goo_canvas.show()
(11:09:17 AM) rickspencer3: Be sure to set the size, otherwise it defaults to 1000,1000, it does not default to
(11:09:18 AM) rickspencer3: the size alloted to it in your window.
(11:09:43 AM) rickspencer3: this is different than normal gtk.Widgets where size is managed for you
(11:09:49 AM) rickspencer3: Handle window resizing to resize your goocanvas as well
(11:09:55 AM) rickspencer3: For example, if your goocanvas is in a VBox, you can do this:
(11:10:04 AM) rickspencer3: rect = self.builder.get_object("vbox2").get_allocation()
(11:10:05 AM) rickspencer3: self.__goo_canvas.set_bounds(0,0,rect.width,rect.height)
(11:10:37 AM) rickspencer3: store a reference to the root item for your goocanvas, you'll need it later often
(11:10:45 AM) rickspencer3: self.__root = self.__goo_canvas.get_root_item()
(11:10:56 AM) rickspencer3: The "root" is kinda like the root of an item tree in XML
(11:11:19 AM) rickspencer3: so if you do this, you'll have a goocanvas in your app, ready to rock
(11:11:23 AM) rickspencer3: questions?
(11:11:58 AM) rickspencer3: ok
(11:12:06 AM) rickspencer3: let's look at adding things to your goocanvas surface
(11:12:20 AM) rickspencer3: I got into goocanvas while working on photobomb
(11:12:33 AM) rickspencer3: so I have some code that I can use for examples
(11:12:44 AM) rickspencer3: so let's take the example of adding an image to your goocanvas
(11:13:00 AM) rickspencer3: assuming you want to modify the image and combine it with other drawing items
(11:13:11 AM) rickspencer3: Anything that can be added to a goocanvas is an Item. It get's it's capabilities by inheriting from ItemSimple, and by implementing the Item interface.
(11:13:28 AM) rickspencer3: plus each items has some extra capabilities
(11:13:50 AM) rickspencer3: so look at the reference for the specific item class, as well as Item and ItemSimple
(11:13:58 AM) rickspencer3: Let's add an item to the goocanvas to get a look at how it works in general.
(11:14:06 AM) rickspencer3: We'll start by adding an image.
(11:14:12 AM) rickspencer3: First, you need to get a gtk.pixbux for your image:
(11:14:24 AM) rickspencer3: pb = gtk.gdk.pixbuf_new_from_file(path)
(11:14:33 AM) rickspencer3: Then you calculate where you want the image to show on the goocanvas. You'll need a top and a left to place most items on a goo canvas.
(11:14:44 AM) rickspencer3: For example, to center the image, I do this:
(11:14:51 AM) rickspencer3: cont_left, cont_top, cont_right, cont_bottom = self.__goo_canvas.get_bounds()
(11:14:51 AM) rickspencer3: img_w = pb.get_width()
(11:14:51 AM) rickspencer3: img_h = pb.get_height()
(11:14:51 AM) rickspencer3: img_left = (cont_right - img_w)/2
(11:14:51 AM) rickspencer3: img_top = (cont_bottom - img_h)/2
(11:15:03 AM) rickspencer3: I've calculated the top and left of the item
(11:15:10 AM) rickspencer3: now I am ready to create it
(11:15:21 AM) rickspencer3: Note that I create the Item, but there is nothing like goocanvas.add(item)
(11:15:22 AM) rickspencer3: rather, when you create the item, you set it's parent property.
(11:15:46 AM) rickspencer3: it's different than most container relationships, like in PyGtk
(11:15:52 AM) rickspencer3: The parent property is the root of the goocanvas
(11:15:57 AM) rickspencer3: This is why I remember the root
(11:16:09 AM) rickspencer3: because I will be using it whenever I create items for the goocanvas
(11:16:26 AM) rickspencer3: finally, here's the code to create the image on teh goocanvas:
(11:16:26 AM) rickspencer3: goocanvas.Image(pixbuf=pb,parent=self.__root, x=img_left,y=img_top)
(11:16:33 AM) rickspencer3: This basic pattern is how you add all other types of items.
(11:16:41 AM) rickspencer3: decide where to put the item, and set it's parent property to the root of the goocanvas.
(11:16:54 AM) rickspencer3: To remove the item from the goocanvas, you don't tell the goocanvas to remove it
(11:17:00 AM) rickspencer3: rather you tell the item to remove itself
(11:17:12 AM) rickspencer3: item.remove()
(11:17:19 AM) rickspencer3: questions so far?
(11:18:05 AM) rickspencer3: no questions?
(11:18:10 AM) rickspencer3: alright then
(11:18:29 AM) rickspencer3: let's talk a bit about different types of items that you can add
(11:18:37 AM) rickspencer3: In my mind, there are really 3 types of items
(11:19:00 AM) ClassBot: nadako asked: why not standard add/remove functions from parent obj?
(11:19:03 AM) rickspencer3: I have no idea
(11:19:08 AM) rickspencer3: this is just how the API is
(11:19:18 AM) rickspencer3: I presume it's because it is a wrapper around a c-api
(11:19:41 AM) rickspencer3: as I was saying,
(11:19:42 AM) rickspencer3: In my mind, there are really 3 types of items
(11:19:52 AM) rickspencer3: first is normal items that you add to draw the stuff you want
(11:20:01 AM) rickspencer3: this includes:
(11:20:05 AM) rickspencer3: Ellipse, Image, Path, Polyline, Rect, and Text
(11:20:33 AM) rickspencer3: then there are Layout and group items include:
(11:20:39 AM) rickspencer3: Group, Grid, and Table
(11:20:49 AM) rickspencer3: And then there is also Widget. Widget is pretty cool.
(11:20:56 AM) rickspencer3: You can add a gtk widget to your goocanvas, but note that it will live in a world seperate from the goocanvas
(11:21:13 AM) rickspencer3: gtk.Widgets won't be rendered if you create images form our goocanvas and such
(11:21:19 AM) rickspencer3: However, this is a cool way to add in situ editing to your goocanvas
(11:21:34 AM) rickspencer3: We'll just be talking about normal items for the rest of this class though
(11:21:43 AM) rickspencer3: I'll take some questions now, if you've got 'em
(11:21:56 AM) ClassBot: fagan asked: What formats does GooCanvas support? (png,jpeg,svg..etc)
(11:22:07 AM) rickspencer3: ya know, I never really investigated
(11:22:15 AM) rickspencer3: I would presume the first two
(11:22:53 AM) rickspencer3: in terms of svg, perhaps that would be supported a bit differently, as many of the items are described with svg language
(11:22:55 AM) rickspencer3: or can be
(11:23:11 AM) rickspencer3: like an elipse in an svg would be turned into an elipse item
(11:23:17 AM) rickspencer3: rather than rendered as an image
(11:23:30 AM) rickspencer3: I would be interested to see what different things people try and how it works for them
(11:23:40 AM) ClassBot: tm_lv asked: why goocanvas?
(11:23:46 AM) rickspencer3: this is a good question
(11:23:56 AM) rickspencer3: I used goocanvas for photobomb because:
(11:24:00 AM) rickspencer3: 1. it was an easy API
(11:24:08 AM) rickspencer3: 2. it integrate well with pygtk
(11:24:16 AM) rickspencer3: 3. it had good reference docs
(11:24:29 AM) rickspencer3: 4. it had the functionality to do the things I wanted
(11:24:40 AM) rickspencer3: for #4 that was stuff like adding text, paths, etc...
(11:24:49 AM) rickspencer3: and also the ability to render off images easily
(11:25:21 AM) ClassBot: nadako asked: is there any ready-to-use things like connector lines that connects two elements and follow their positions
(11:25:25 AM) rickspencer3: not that I know of
(11:25:34 AM) ClassBot: nadako asked: also, what about interactivity? drag/drop, mouse clicks, etc
(11:25:41 AM) rickspencer3: you handle that manually
(11:25:49 AM) rickspencer3: I'll mention mouse handling briefly
(11:25:51 AM) rickspencer3: late
(11:25:51 AM) rickspencer3: r
(11:26:00 AM) rickspencer3: ok, so moving on
(11:26:07 AM) rickspencer3: So what are some of the things that you do with an item?
(11:26:20 AM) rickspencer3: Well, you compose with it. So you scale it, move it, rotate it, change it's z-order and such
(11:26:29 AM) rickspencer3: For a lot of things that you want to do with an item, you use set_property and get_property
(11:26:57 AM) rickspencer3: this is the same way of interacting with properties in pygtk, but you do lots more of it in goocanvas
(11:27:04 AM) rickspencer3: For example, to set the a might make a Text item like this:
(11:27:10 AM) rickspencer3: txt = goocanvas.Text(parent=self.__root,text="some text", x=100, y=100, fill_color=self.__ink_color)
(11:27:27 AM) rickspencer3: so here I am adding some text and it will say "some text"
(11:27:37 AM) rickspencer3: it will be at x,y 100,100
(11:27:42 AM) rickspencer3: and will use the stored ink color
(11:27:48 AM) rickspencer3: then change the text in it like this:
(11:27:54 AM) rickspencer3: txt.set_property("text","new text")
(11:28:06 AM) rickspencer3: if I want to know what the text is, I can say:
(11:28:18 AM) rickspencer3: my_string = txt.get_property("text")
(11:28:30 AM) rickspencer3: some capabilities are accessed directly
(11:28:33 AM) rickspencer3: such as bounds
(11:29:01 AM) rickspencer3: but most are accessed like this
(11:29:08 AM) rickspencer3: Let's look at colors for a moment.
(11:29:12 AM) rickspencer3: There are generally two color properties to work with, stork-color, and fill-color
(11:29:18 AM) rickspencer3: stork-color?
(11:29:24 AM) rickspencer3: let's call that stoke-color
(11:29:25 AM) rickspencer3: :)
(11:29:36 AM) rickspencer3: If you've ever used a tool ink inkscape, this will make sense you to
(11:29:43 AM) rickspencer3: for something like a rect, stroke-color is the outline of the rectangle, and fill-color is the inside of the rectangle
(11:30:00 AM) rickspencer3: before I discuss a bit more about transforming items ...
(11:30:02 AM) rickspencer3: any questions?
(11:30:14 AM) ClassBot: nadako asked: so it's only about drawing things?
(11:30:22 AM) rickspencer3: sort of
(11:30:38 AM) rickspencer3: I was able to use goocanvas to make a pretty functional editing surface in photobomb
(11:31:09 AM) rickspencer3: it included dragging, drawing lines with a pen too, and saving pngs
(11:31:19 AM) rickspencer3: but essentially, yes, it's a drawing surface
(11:31:29 AM) ClassBot: tm_lv asked: getters and setters seem to be result of crappy bindings. do you know if there is any work done to make them more pythonic?
(11:31:35 AM) rickspencer3: I agree
(11:31:43 AM) rickspencer3: but I don't know of any more work to make it more pythonic
(11:31:57 AM) rickspencer3: while the API is not perfect, it is functional and robust
(11:32:12 AM) rickspencer3: ok
(11:32:16 AM) rickspencer3: moving on a bit
(11:32:30 AM) rickspencer3: You can move, rotate, resize, and skew items
(11:32:42 AM) rickspencer3: The APIs for doing this are intuitive, imho
(11:32:52 AM) rickspencer3: To grow something by 10%
(11:32:58 AM) rickspencer3: :
(11:32:59 AM) rickspencer3: item.scale(1.1,1.1)
(11:33:16 AM) rickspencer3: that says make it 1.1 times as tall and wide as it is now
(11:33:21 AM) rickspencer3: And to shrink it a bit:
(11:33:26 AM) rickspencer3: item.scale(.9,.9)
(11:33:36 AM) rickspencer3: Note that the items always consider themeselves to be their original size and orientation, so doing this will cause an item to grow twice:
(11:33:51 AM) rickspencer3: item.scale(1.1,1.1)
(11:33:51 AM) rickspencer3: item.scale(1.1,1.1)
(11:34:06 AM) rickspencer3: Now, when you start rotating and skewing items, some pretty confusing stuff can start happening
(11:34:16 AM) rickspencer3: Essentially, an item tracks it's own coordinate system, and doesn't much care about the goocanvas's coordinate system
(11:34:26 AM) rickspencer3: So if you rotate an item, for example, the coordinate systems are totally out of whack
(11:34:43 AM) rickspencer3: So if you pass the x/ys to an item based on the canvas's coordinate system, it can get waaaay weird
(11:34:52 AM) rickspencer3: like if you have a an item rotated 180 degrees
(11:35:01 AM) rickspencer3: and you tell it to add 100 to it's x
(11:35:06 AM) rickspencer3: it will move left!
(11:35:20 AM) rickspencer3: because the items doesn't know or care that it is rotated
(11:35:29 AM) rickspencer3: Fortunately, goocanvas has some functions on it that just do these transforms for you
(11:35:36 AM) rickspencer3: let's say I catch a mouse click event on an item
(11:35:41 AM) rickspencer3: and I want to know where on the item the click happened
(11:35:50 AM) rickspencer3: well, the click coordinate are reported in the goocanvas's coordinate system, so I need to do a quick calculation to determine where the click happened on the item:
(11:36:01 AM) rickspencer3: e_x, e_y = self.__goo_canvas.convert_to_item_space(self.selected_item,event.x,event.y)
(11:36:41 AM) rickspencer3: here self.selected_item is the item who's coordinate system I want to convert to
(11:36:58 AM) rickspencer3: so now e_x is x from the item
(11:37:07 AM) rickspencer3: 's point of view, not the goocanvas point of view
(11:37:12 AM) rickspencer3: phew
(11:37:19 AM) rickspencer3: this is weird, but you get used to it
(11:37:32 AM) rickspencer3: so, good time for some questions if there are any
(11:37:42 AM) ClassBot: tm_lv asked: can you nest items in goocanvas?
(11:37:46 AM) rickspencer3: yes, but I haven't done that
(11:38:37 AM) ClassBot: nadako asked: can you give a link to that photobomb app?
(11:38:45 AM) rickspencer3: sure, it's in my junk folder
(11:39:30 AM) rickspencer3: https://code.edge.launchpad.net/~rick-rickspencer3/+junk/photobomb
(11:39:44 AM) ClassBot: luismmontielg asked: What could be easier to implement, for a small pygtk app, using pyclutter or pygoocanvas? or whats the difference between those 2
(11:39:50 AM) rickspencer3: clutter is also very cool
(11:40:02 AM) rickspencer3: but it's a wrapper around OpenGL
(11:40:16 AM) rickspencer3: I suspect that for a small 2d surface, goocanvas will be much better
(11:40:26 AM) rickspencer3: however, for rendering animations, clutter will be much better
(11:40:38 AM) rickspencer3: you can do drag and drop and such on goocanvas with no problem
(11:40:48 AM) rickspencer3: and it has some animation support
(11:41:04 AM) rickspencer3: by clutter is all about animations, especially in a cool 3d way
(11:41:19 AM) ClassBot: tm_lv asked: following on the previous one, i'd like to insert a shameless plug - contender #3 the hamster graphics library, sprite-based and python pure: http://wiki.github.com/tbaugis/hamster_experiments/
(11:41:26 AM) rickspencer3: shameless plug accepted ;)
(11:41:35 AM) rickspencer3: ok
(11:41:38 AM) rickspencer3: moving on
(11:41:44 AM) rickspencer3: Just a quick word on paths
(11:41:50 AM) rickspencer3: A path is essentially a "squiggle"
(11:42:09 AM) rickspencer3: It is defined by a string that gets parsed into x,y coords, and then drawn with a bezier curve formula applied
(11:42:13 AM) rickspencer3: let's take a look
(11:42:29 AM) rickspencer3: the curve stuff is a bit of math that I don't much understand
(11:42:37 AM) rickspencer3: but just to get you started, here is a string that describes a scribble
(11:42:43 AM) rickspencer3: line_data = "M 4.0 4.0C4.0 4.0 5.0 4.0 5.0 4.0 5.0 4.0 6.0 4.0 6.0 3.0 10.0 1.0 13.0 2.0 9.0 15.0 6.0 36.0 28.0 11.0 28.0 11.0 29.0 11.0 33.0 12.0 33.0 15.0 32.0 19.0 27.0 51.0 27.0 53.0 27.0 54.0 27.0 54.0 27.0 54.0 36.0 49.0 37.0 49.0"
(11:42:55 AM) rickspencer3: it just kind of traces some x/y coords
(11:43:03 AM) rickspencer3: then I can make a path out of this:
(11:43:10 AM) rickspencer3: path = goocanvas.Path(data=line_data, parent=self.__root, line_width=self.__ink_width, stroke_color=self.__ink_color)
(11:43:22 AM) rickspencer3: so this will draw the squiggle in the goocanvas
(11:43:42 AM) rickspencer3: Now, a path is also useful because you can use it to clip another object
(11:44:27 AM) rickspencer3: like you can draw an arbitrary path around an item and use it kind of like a pair of scissorrs
(11:44:34 AM) rickspencer3: You don't use a path object for this, just the string
(11:44:34 AM) rickspencer3: item.set_property("clip-path",line_data)
(11:44:46 AM) rickspencer3: pretty cool!
(11:45:01 AM) rickspencer3: any questions about paths and such?
(11:45:30 AM) rickspencer3: ok
(11:45:35 AM) rickspencer3: let's go on then
(11:45:55 AM) rickspencer3: somebody asked about managing mouse movements and such
(11:46:03 AM) rickspencer3: In terms of mousing a goocanvas has the normal gtk mouse tracking capabilities
(11:46:09 AM) rickspencer3: to track mouse clicks, for example:
(11:46:13 AM) rickspencer3: self.__goo_canvas.connect("button_press_event",self.mouse_down)
(11:46:35 AM) rickspencer3: so you can also connect to mousedown, mousemove, mouseup, etc... to handle dragging items around
(11:47:12 AM) rickspencer3: self.__motion_handler = self.__goo_canvas.connect("motion_notify_event",self.item_moved)
(11:47:12 AM) rickspencer3:                 self.__mouse_up_handler = self.__goo_canvas.connect("button_release_event",self.drag_stop)
(11:47:28 AM) rickspencer3: so that's how I handle dragging items in photobomb
(11:47:38 AM) rickspencer3: it's a manual process
(11:47:42 AM) rickspencer3: questions?
(11:48:06 AM) rickspencer3: ok
(11:48:10 AM) rickspencer3: let's go on to the last section
(11:48:12 AM) rickspencer3: rendering
(11:48:21 AM) rickspencer3: a goocanvas can use cairo surfaces to render off snapshots of itself
(11:48:35 AM) rickspencer3: So if I want to make a png, I use an image surface
(11:48:41 AM) rickspencer3: x, y, w, h = self.__goo_canvas.get_bounds()
(11:48:41 AM) rickspencer3: surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(w), int(h))
(11:48:41 AM) rickspencer3: context = cairo.Context(surface)
(11:48:41 AM) rickspencer3: context.rectangle(0, 0, 1000, 1000)
(11:48:41 AM) rickspencer3: context.set_source_rgb(1, 1, 1)
(11:48:42 AM) rickspencer3: context.fill()
(11:48:44 AM) rickspencer3: self.__goo_canvas.render(context)
(11:48:46 AM) rickspencer3: surface.write_to_png(image_path)
(11:48:59 AM) rickspencer3: (I sniped this originally from segphaults grabbersnap code)
(11:49:05 AM) rickspencer3: There are other cairo surfaces as well, including a PDF surface
(11:49:11 AM) rickspencer3: so you can render to a pdf, for example
(11:49:20 AM) rickspencer3: okay, that's the last bit of material I prepared
(11:49:25 AM) rickspencer3: any questions about anything?
(11:50:44 AM) ClassBot: quappa1 asked: Can I try photobomb on Karmic? Seems that in your PPA it's only for Lucid.
(11:50:50 AM) rickspencer3: I suppose
(11:50:55 AM) rickspencer3: I think it should work
(11:51:06 AM) rickspencer3: just pull from trunk and give it a try
(11:51:18 AM) ClassBot: ems asked: any chance you could add some goocanvas snippets to acire?
(11:51:22 AM) rickspencer3: I suppose
(11:51:31 AM) rickspencer3: I don't know when I would get to it
(11:51:41 AM) rickspencer3: I would be glad to help someone else who wants to give it a try
(11:51:50 AM) ClassBot: luismmontielg asked: is there any code examples for this? simple working examples?
(11:51:54 AM) rickspencer3: not too many
(11:51:59 AM) rickspencer3: that's why I wanted to do the class
(11:52:12 AM) rickspencer3: photobomb and grabber snap both use it a bit
(11:52:24 AM) rickspencer3: so lots of samples there, but no snippets yet that I know of
(11:52:32 AM) ClassBot: fagan asked: will photobomb get into the repo any time soon? :)
(11:52:34 AM) rickspencer3: uh
(11:52:36 AM) rickspencer3: well
(11:52:43 AM) rickspencer3: look at the code and let me know what you think
(11:52:54 AM) rickspencer3: there are lots of ideas cram in there, and some pretty bad bugs
(11:52:55 AM) rickspencer3: :)
(11:53:01 AM) rickspencer3: maybe sometime though
(11:53:12 AM) rickspencer3: unless someone wants to take over and finish it off
(11:53:32 AM) rickspencer3: no more questions and 7 minutes early!
(11:53:45 AM) rickspencer3: you folks were a great class, thanks for coming
(11:54:20 AM) ClassBot: quappa1 asked: photobomb requires something called quickly.prompts. so probably no luck on Karmic? :)
(11:54:24 AM) rickspencer3: oh goodness
(11:54:29 AM) rickspencer3: yes, that is not in karmic
(11:54:36 AM) rickspencer3: but if you are dedicated, you could hack it together
(11:54:56 AM) rickspencer3: like pull the quidgets project and stick the code in the photobomb library