From d8c59c1a13663ed0d09784ca2a37fcf29ceb91ec Mon Sep 17 00:00:00 2001 From: simpoir Date: Sat, 14 Mar 2009 22:59:34 +0000 Subject: Functionnal overlay Conflicts: source/external/source/sugar-toolkit/src/sugar/tutorius/Makefile.am source/external/source/sugar-toolkit/src/sugar/tutorius/actions.py Conflicts: source/activities/Writus.activity/TAbiWordActivity.py --- diff --git a/src/sugar/graphics/window.py b/src/sugar/graphics/window.py index 1ad2bca..a3006a6 100644 --- a/src/sugar/graphics/window.py +++ b/src/sugar/graphics/window.py @@ -23,6 +23,7 @@ import gobject import gtk from sugar.graphics.icon import Icon +from sugar.tutorius.overlayer import Overlayer class UnfullscreenButton(gtk.Window): @@ -97,9 +98,16 @@ class Window(gtk.Window): self._hbox.pack_start(self._event_box) self._event_box.show() - self.add(self._vbox) +## self.add(self._vbox) self._vbox.show() + self._overlayer = Overlayer() +## self._overlayer.inject(self._vbox) + self._overlayer.put(self._vbox, 0, 0) + self._overlayer._Overlayer__overlayed = self._vbox + self.add(self._overlayer) + self._overlayer.show() + self._is_fullscreen = False self._unfullscreen_button = UnfullscreenButton() self._unfullscreen_button.set_transient_for(self) diff --git a/src/sugar/tutorius/Makefile.am b/src/sugar/tutorius/Makefile.am index 43cb622..6fd32c7 100644 --- a/src/sugar/tutorius/Makefile.am +++ b/src/sugar/tutorius/Makefile.am @@ -6,4 +6,5 @@ sugar_PYTHON = \ actions.py \ gtkutils.py \ filters.py \ - services.py + services.py \ + overlayer.py diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py index 34802c3..7ecf86b 100644 --- a/src/sugar/tutorius/actions.py +++ b/src/sugar/tutorius/actions.py @@ -17,7 +17,9 @@ This module defines Actions that can be done and undone on a state """ +from sugar.tutorius import gtkutils from dialog import TutoriusDialog +import overlayer class Action(object): @@ -25,7 +27,7 @@ class Action(object): def __init__(self): object.__init__(self) - def do(self): + def do(self, **kwargs): """ Perform the action """ @@ -58,7 +60,7 @@ class OnceWrapper(object): self._called = True self._action.do() self._need_undo = True - + def undo(self): """ Undo the action if it's been done @@ -97,5 +99,48 @@ class DialogMessage(Action): if self._dialog: self._dialog.destroy() self._dialog = None - + + +class BubbleMessage(Action): + """ + Shows a dialog with a given text, at the given position on the screen. + + @param message A string to display to the user + @param pos A list of the form [x, y] + @param speaker treeish representation of the speaking widget + """ + def __init__(self, message, pos=[0,0], speaker=None, tailpos=None): + Action.__init__(self) + self._message = message + self.position = pos + + self.overlay = None + self._bubble = None + self._speaker = None + self._tailpos = tailpos + + + def do(self): + """ + Show the dialog + """ + # get or inject overlayer + self.overlay = gtkutils.activity()._overlayer #FIXME:handle subwin + + if not self._bubble: + x, y = self.position + self._bubble = overlayer.TextBubble(text=self._message, tailpos=self._tailpos) #TODO: add tail + self._bubble.show() + self.overlay.put(self._bubble, x, y) + self.overlay.queue_draw() + + def undo(self): + """ + Destroy the dialog + """ + if self._bubble: +## self.overlay.remove(self._bubble) +## self.overlay.bin_window.clear() + self._bubble.destroy() + self._bubble = None diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py index 7196469..450872d 100644 --- a/src/sugar/tutorius/gtkutils.py +++ b/src/sugar/tutorius/gtkutils.py @@ -18,6 +18,12 @@ Utility classes and functions that are gtk related """ +def activity(activity=None, singleton=[]): + if activity: + singleton.append(activity) + return singleton[0] + + def find_widget(base, target_fqdn): """Find a widget by digging into a parent widget's children tree @param base the parent widget @@ -39,7 +45,10 @@ def find_widget(base, target_fqdn): path.pop(0) while len(path) > 0: - obj = obj.get_children()[int(path.pop(0))] + try: + obj = obj.get_children()[int(path.pop(0))] + except: + break return obj diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py new file mode 100644 index 0000000..0da1841 --- /dev/null +++ b/src/sugar/tutorius/overlayer.py @@ -0,0 +1,308 @@ +""" +This guy manages drawing of overlayed widgets. The class responsible for drawing +management (Overlayer) and overlayable widgets are defined here. +""" +# Copyright (C) 2009, Tutorius.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk +import cairo +import pangocairo + +class CanvasDrawable(object): + """Defines the CanvasDrawable protocol""" + should_expose = None + def drawWithContext(self, context): pass + + +class Overlayer(gtk.Layout): + """ + This guy manages drawing of overlayed widgets. Those can be standard GTK + widgets or special "cairoDrawable" widgets which support the defined + interface (see the put method). + """ + def __init__(self): + gtk.Layout.__init__(self) + + # no overlayed child yet + self.__overlayed = None + +## win.connect("expose-event", self.__transparent_overlay) + self.__realizer = self.connect("expose-event", self.__init_realized) + self.connect("size-allocate", self.__size_allocate) + self.show() + +## win.show_all() + +## win.connect_after("expose-event", self.__expose_overlay) + + def reparent(self, parent): + """Reparent this widget as child of parent instead.""" + self.__realizer = self.connect("expose-event", self.__init_realized) + gtk.Layout.reparent(self, parent) + + def inject(self, child): + """ + Insert this widget at "child" level so that it can overlay "child". + """ + assert not self.__overlayed, "trying to reinject while already injected" + parent = child.parent + self.__overlayed = child + child.reparent(self) + parent.add(self) + self.__realizer = parent.connect("expose-event", self.__init_realized) + + def put(self, child, x, y): + """ + Adds a child widget to be overlayed. This can be, overlay widgets or + normal GTK widgets (though normal widgets will alwas appear under + cairo widgets due to the rendering chain). + """ + if hasattr(child, "should_expose"): + # if the widget has the CanvasDrawable protocol, use it. + # protocol is defined as + # should_expose property with setter to flag as not drawn using + # expose-events + # drawWithContext(context) method to pass a context to draw with. + child.should_expose = True + gtk.Layout.put(self, child, x, y) + + def eject(self): + """ + Remove this widget from the widget hierarchy without affecting its + children. + """ + parent = self.parent + parent.disconnect(self.__render_handle) + assert hasattr(parent, "remove"), "parent should be a container" + self.parent.remove(self) + self.__overlayed.reparent(parent) + self.__overlayed = None + + def __init_realized(self, widget, event): + """ + Initializer to set once widget is realized. + Since an expose event is signaled only to realized widgets, we set this + callback for the first expose run. It should also be called after + beign reparented to ensure the window used for drawing is set up. + """ + assert hasattr(self.window, "set_composited"), \ + "compositing not supported or widget not realized." + self.disconnect(self.__realizer) + del self.__realizer + + # use RGBA on overlayer so that when we paint we don't cover what's + # under + screen = self.get_screen() + rgba_map = screen.get_rgba_colormap() + self.set_colormap(rgba_map) # will fail if already realized + # app paintable will ensure that what we draw isn't erased by gtk + self.parent.set_app_paintable(True) + + # the parent is composited, so we can access gtk's rendered buffer + # and overlay over. If we don't composite, we won't be able to read + # pixels and background will be black. + self.window.set_composited(True) + self.__render_handle = self.parent.connect_after("expose-event", \ + self.__expose_overlay) + + def __expose_overlay(self, widget, event): + """expose event handler to draw the thing.""" + #get our child (in this case, the event box) + child = widget.get_child() + + #create a cairo context to draw to the window + ctx = widget.window.cairo_create() + + #the source data is the (composited) event box + ctx.set_source_pixmap(child.window, + child.allocation.x, + child.allocation.y) + + #draw no more than our expose event intersects our child +## region = gtk.gdk.region_rectangle(child.allocation) +## rect = gtk.gdk.region_rectangle(event.area) +## region.intersect(rect) +## ctx.region (region) +## ctx.clip() + + ctx.set_operator(cairo.OPERATOR_OVER) + # has to be blended and a 1.0 alpha would not make it blend + ctx.paint_with_alpha(0.99) + + #draw overlay + for drawn_child in self.get_children(): + if hasattr(drawn_child, "drawWithContext"): + drawn_child.drawWithContext(ctx) + + + def __size_allocate(self, widget, allocation): + """set size allocation (actual gtk widget size) and propagate it to overlayed child""" + self.allocation = allocation + # One may wonder why using size_request instead of size_allocate; + # Since widget is laid out in a Layout box, the Layout will honor the + # requested size. Using size_allocate could make a nasty nested loop in + # some cases. + self.__overlayed.set_size_request(allocation.width, allocation.height) + + +class TextBubble(gtk.Widget): + def __init__(self, text, speaker=None, tailpos=None): + """ + Creates a new cairo rendered text bubble. + + @param text the text to render in the bubble + @param speaker the widget to associate with the bubble tail + @param tailpos an optional position for the tail if no speaker + """ + gtk.Widget.__init__(self) + + ##self.set_app_paintable(True) # else may be blank + # FIXME ensure previous call does not interfere with widget stacking + self.label = text + self.speaker = speaker + self.tailpos = tailpos + self.lineWidth = 5 + + self.connect("expose-event", self.__on_expose) + + def drawWithContext(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + context.translate(self.allocation.x, self.allocation.y) + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.lineWidth + height -= self.lineWidth + + # TODO fetch speaker coordinates + + # draw bubble tail + if self.tailpos: +## speaker_alloc = self.speaker.get_allocation() + context.move_to(xradius-40, yradius) + context.line_to(self.tailpos[0], self.tailpos[1]) +## context.line_to(speaker_alloc.x-self.allocation.x, +## speaker_alloc.y - self.allocation.y) + context.line_to(xradius+40, yradius) + context.set_source_rgb(1.0, 1.0, 0.0) + context.fill_preserve() + context.set_line_width(self.lineWidth) + context.set_source_rgb(0.0, 0.0, 0.0) + context.stroke() + + # bubble + context.move_to(self.lineWidth, yradius) + context.curve_to(self.lineWidth, self.lineWidth, + self.lineWidth, self.lineWidth, xradius, self.lineWidth) + context.curve_to(width, self.lineWidth, + width, self.lineWidth, width, yradius) + context.curve_to(width, height, width, height, xradius, height) + context.curve_to(self.lineWidth, height, + self.lineWidth, height, self.lineWidth, yradius) + context.set_source_rgb(1.0, 1.0, 0.0) + context.fill_preserve() + context.set_line_width(self.lineWidth) + context.set_source_rgb(0.0, 0.0, 0.0) + context.stroke() + + # text + # FIXME create layout in realize method + pangoctx = pangocairo.CairoContext(context) + text_layout = pangoctx.create_layout() + text_layout.set_text(self.__label) + pangoctx.move_to(int((self.allocation.width-self.__text_dimentions[0])/2), + int((self.allocation.height-self.__text_dimentions[1])/2)) + pangoctx.show_layout(text_layout) + + # work done. Be kind to next cairo widgets and reset matrix. + context.identity_matrix() + + def do_realize(self): + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + self.window = self.get_parent_window() + + def __on_expose(self, widget, event): + """Redraw event callback.""" + # TODO + ctx = self.window.cairo_create() + + # set drawing region. Useless since this widget has its own window. +## region = gtk.gdk.region_rectangle(self.allocation) +## region.intersect(gtk.gdk.region_rectangle(event.area)) +## ctx.region(region) +## ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) +## ctx.clip() + + ##import pdb; pdb.set_trace() + ##ctx.set_operator(cairo.OPERATOR_CLEAR) + ##ctx.paint() + ##ctx.set_operator(cairo.OPERATOR_OVER) + + self.drawWithContext(ctx) + + return True + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + # yes we use the setter + self.__label = value + # FIXME hack to calculate size. necessary because may not have been + # realized + surf = cairo.SVGSurface("/dev/null", 0, 0) + ctx = cairo.Context(surf) + pangoctx = pangocairo.CairoContext(ctx) + text_layout = pangoctx.create_layout() + text_layout.set_text(value) + self.__text_dimentions = text_layout.get_pixel_size() + del text_layout, pangoctx, ctx, surf + + def do_size_request(self, requisition): + width, height = self.__text_dimentions + + # FIXME bogus values follows + requisition.width = int(width+30) + requisition.height = int(height+40) + + def do_size_allocate(self, allocation): + self.allocation = allocation + + def _get_label(self): + """Getter method for the label property""" + return self.__label + + def _set_will_expose(self, value): + """setter for will_expose property""" + if self.__exposer and not value: + self.disconnect(self.__exposer) + elif not (self.__exposer or value): + self.__exposer = self.connect(self.__on_expose) + + should_expose = property(fset=_set_will_expose) + + label = property(fget=_get_label, fset=_set_label,\ + doc="Text label which is to be painted on the top of the widget") + +gobject.type_register(TextBubble) + + +# vim:set ts=4 sts=4 sw=4 et: -- cgit v0.9.1