diff --git a/src/sugar3/Makefile.am b/src/sugar3/Makefile.am index 1f073df6..3aec8bf0 100644 --- a/src/sugar3/Makefile.am +++ b/src/sugar3/Makefile.am @@ -53,9 +53,7 @@ libsugarext_la_SOURCES = \ sugar-grid.c \ sugar-grid.h \ sugar-key-grabber.c \ - sugar-key-grabber.h \ - sugar-menu.c \ - sugar-menu.h + sugar-key-grabber.h sugar_LTLIBRARIES = _sugarbaseext.la diff --git a/src/sugar3/graphics/palette.py b/src/sugar3/graphics/palette.py index ee76674a..1f95c11a 100644 --- a/src/sugar3/graphics/palette.py +++ b/src/sugar3/graphics/palette.py @@ -1,6 +1,8 @@ # Copyright (C) 2007, Eduardo Silva # Copyright (C) 2008, One Laptop Per Child # Copyright (C) 2009, Tomeu Vizoso +# Copyright (C) 2011, Benjamin Berg +# Copyright (C) 2011, Marco Pesenti Gritti # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -29,8 +31,8 @@ from sugar3.graphics import palettegroup from sugar3.graphics import animator from sugar3.graphics import style from sugar3.graphics.icon import Icon -from sugar3.graphics.palettewindow import PaletteWindow -from gi.repository import SugarExt +from sugar3.graphics.palettewindow import PaletteWindow, \ + _PaletteWindowWidget, _PaletteMenuWidget # DEPRECATED # Import these for backwards compatibility @@ -39,16 +41,37 @@ from sugar3.graphics.palettewindow import MouseSpeedDetector, Invoker, \ class Palette(PaletteWindow): + """ + Floating palette implementation. + + This class dynamically switches between one of two encapsulated child + widget types: a _PaletteWindowWidget or a _PaletteMenuWidget. + + The window widget, created by default, acts as the container for any + type of widget the user may wish to add. It can optionally display primary + text, secondary text, and an icon at the top of the palette. + + If the user attempts to access the 'menu' property, the window widget is + destroyed and the palette is dynamically switched to use a menu widget. + This is a GtkMenu that retains the same look and feel as a normal palette, + allowing submenus and so on. If primary text, secondary text and/or icons + were provided, an initial menu entry is created containing widgets to + display such information. + """ + PRIMARY = 0 SECONDARY = 1 + __gsignals__ = { + 'activate': (GObject.SignalFlags.RUN_FIRST, None, ([])), + } + __gtype_name__ = 'SugarPalette' - def __init__(self, label=None, accel_path=None, menu_after_content=False, + def __init__(self, label=None, accel_path=None, text_maxlen=60, **kwargs): # DEPRECATED: label is passed with the primary-text property, - # accel_path is set via the invoker property, and menu_after_content - # is not used + # accel_path is set via the invoker property self._primary_text = None self._secondary_text = None @@ -56,15 +79,12 @@ class Palette(PaletteWindow): self._icon_visible = True self._palette_state = self.PRIMARY - palette_box = Gtk.VBox() - - primary_box = Gtk.HBox() - palette_box.pack_start(primary_box, False, True, 0) - primary_box.show() + self._primary_box = Gtk.HBox() + self._primary_box.show() self._icon_box = Gtk.HBox() self._icon_box.set_size_request(style.GRID_CELL_SIZE, -1) - primary_box.pack_start(self._icon_box, False, True, 0) + self._primary_box.pack_start(self._icon_box, False, True, 0) labels_box = Gtk.VBox() self._label_alignment = Gtk.Alignment(xalign=0, yalign=0.5, xscale=1, @@ -73,7 +93,7 @@ class Palette(PaletteWindow): style.DEFAULT_SPACING) self._label_alignment.add(labels_box) self._label_alignment.show() - primary_box.pack_start(self._label_alignment, True, True, 0) + self._primary_box.pack_start(self._label_alignment, True, True, 0) labels_box.show() self._label = Gtk.AccelLabel(label='') @@ -94,77 +114,51 @@ class Palette(PaletteWindow): labels_box.pack_start(self._secondary_label, True, True, 0) self._secondary_box = Gtk.VBox() - palette_box.pack_start(self._secondary_box, True, True, 0) self._separator = Gtk.HSeparator() self._secondary_box.pack_start(self._separator, True, True, 0) - self._menu_content_separator = Gtk.HSeparator() - self._secondary_anim = animator.Animator(2.0, 10) self._secondary_anim.add(_SecondaryAnimation(self)) # we init after initializing all of our containers PaletteWindow.__init__(self, **kwargs) - primary_box.set_size_request(-1, style.GRID_CELL_SIZE - - 2 * self.get_border_width()) - self._full_request = [0, 0] - self._menu_box = None self._content = None # we set these for backward compatibility if label is not None: self.props.primary_text = label - self._add_menu() - self._secondary_box.pack_start(self._menu_content_separator, True, True, 0) self._add_content() self.action_bar = PaletteActionBar() self._secondary_box.pack_start(self.action_bar, True, True, 0) self.action_bar.show() - self.add(palette_box) - palette_box.show() + self.connect('notify::invoker', self.__notify_invoker_cb) + self.connect('popdown', self.__popdown_cb) - # The menu is not shown here until an item is added - self.menu = _Menu(self) - self.menu.connect('item-inserted', self.__menu_item_inserted_cb) + # Default to a normal window palette + self._content_widget = None + self.set_content(None) - self.connect('realize', self.__realize_cb) - self.connect('show', self.__show_cb) - self.connect('hide', self.__hide_cb) - self.connect('notify::invoker', self.__notify_invoker_cb) - self.connect('destroy', self.__destroy_cb) + def _setup_widget(self): + PaletteWindow._setup_widget(self) + self._widget.connect('destroy', self.__destroy_cb) def _invoker_right_click_cb(self, invoker): self.popup(immediate=True, state=self.SECONDARY) - def do_style_set(self, previous_style): - # Prevent a warning from pygtk - if previous_style is not None: - Gtk.Window.do_style_set(self, previous_style) - self.set_border_width(self.get_style().xthickness) - - def __menu_item_inserted_cb(self, menu): - self._update_separators() - def __destroy_cb(self, palette): self._secondary_anim.stop() self.popdown(immediate=True) # Break the reference cycle. It looks like the gc is not able to free # it, possibly because Gtk.Menu memory handling is very special. - self.menu.disconnect_by_func(self.__menu_item_inserted_cb) - self.menu = None + self._widget = None - def __show_cb(self, widget): - self.menu.set_active(True) - - def __hide_cb(self, widget): - self.menu.set_active(False) - self.menu.cancel() + def __popdown_cb(self, widget): self._secondary_anim.stop() def __notify_invoker_cb(self, palette, pspec): @@ -198,28 +192,16 @@ class Palette(PaletteWindow): def popdown(self, immediate=False): if immediate: self._secondary_anim.stop() - self._popdown_submenus() # to suppress glitches while later re-opening self.set_palette_state(self.PRIMARY) + if self._widget: + self._widget.size_request() PaletteWindow.popdown(self, immediate) - def _popdown_submenus(self): - # TODO explicit hiding of subitems - # should be removed after fixing #1301 - if self.menu is not None: - for menu_item in self.menu.get_children(): - if menu_item.props.submenu is not None: - menu_item.props.submenu.popdown() - - def on_enter(self, event): - PaletteWindow.on_enter(self, event) + def on_enter(self): + PaletteWindow.on_enter(self) self._secondary_anim.start() - def _add_menu(self): - self._menu_box = Gtk.VBox() - self._secondary_box.pack_start(self._menu_box, True, True, 0) - self._menu_box.show() - def _add_content(self): # The content is not shown until a widget is added self._content = Gtk.VBox() @@ -315,6 +297,22 @@ class Palette(PaletteWindow): setter=set_icon_visible) def set_content(self, widget): + assert self._widget is None \ + or isinstance(self._widget, _PaletteWindowWidget) + + if self._widget is None: + self._widget = _PaletteWindowWidget() + self._setup_widget() + + self._palette_box = Gtk.VBox() + self._palette_box.pack_start(self._primary_box, False, True, 0) + self._palette_box.pack_start(self._secondary_box, True, True, 0) + + self._widget.add(self._palette_box) + self._palette_box.show() + height = style.GRID_CELL_SIZE - 2 * self._widget.get_border_width() + self._primary_box.set_size_request(-1, height) + if self._content.get_children(): self._content.remove(self._content.get_children()[0]) @@ -324,48 +322,37 @@ class Palette(PaletteWindow): else: self._content.hide() + self._content_widget = widget + self._update_accept_focus() self._update_separators() - def do_size_request(self, requisition): - PaletteWindow.do_size_request(self, requisition) + def do_get_preferred_width(self): + minimum, natural = PaletteWindow.do_get_preferred_width(self) # Gtk.AccelLabel request doesn't include the accelerator. label_width = self._label_alignment.size_request()[0] + \ self._label.get_accel_width() + \ 2 * self.get_border_width() - requisition.width = max(requisition.width, - label_width, - self._full_request[0]) + width = max(minimum, label_width, self._full_request[0]) + return width, width def _update_separators(self): - visible = self.menu.get_children() or \ - self._content.get_children() + visible = self._content.get_children() self._separator.props.visible = visible - visible = self.menu.get_children() and \ - self._content.get_children() - self._menu_content_separator.props.visible = visible - def _update_accept_focus(self): accept_focus = len(self._content.get_children()) - window = self.get_window() - if window: - window.set_accept_focus(accept_focus) - - def __realize_cb(self, widget): - self._update_accept_focus() + self._widget.set_accept_focus(accept_focus) def _update_full_request(self): if self._palette_state == self.PRIMARY: - self.menu.embed(self._menu_box) self._secondary_box.show() - self._full_request = self.size_request() + self._full_request = self._widget.size_request() if self._palette_state == self.PRIMARY: - self.menu.unembed() self._secondary_box.hide() def _set_palette_state(self, state): @@ -373,15 +360,46 @@ class Palette(PaletteWindow): return if state == self.PRIMARY: - self.menu.unembed() self._secondary_box.hide() elif state == self.SECONDARY: - self.menu.embed(self._menu_box) self._secondary_box.show() self.update_position() self._palette_state = state + def get_menu(self): + assert self._content_widget is None + + if self._widget is None \ + or not isinstance(self._widget, _PaletteMenuWidget): + if self._widget is not None: + self._palette_box.remove(self._primary_box) + self._palette_box.remove(self._secondary_box) + self._teardown_widget() + self._widget.destroy() + + self._widget = _PaletteMenuWidget() + + self._label_menuitem = Gtk.MenuItem() + child = self._label_menuitem.get_child() + if child is not None: + self._label_menuitem.remove(child) + self._label_menuitem.add(self._primary_box) + + # Mark the menuitem as insensitive so that it appears as an + # informational element, rather than a clickable item in the menu. + # TODO: see if we can do this better in GTK. + self._label_menuitem.set_sensitive(False) + + self._label_menuitem.show() + self._widget.append(self._label_menuitem) + + self._setup_widget() + + return self._widget + + menu = GObject.property(type=object, getter=get_menu) + class PaletteActionBar(Gtk.HButtonBox): @@ -397,44 +415,6 @@ class PaletteActionBar(Gtk.HButtonBox): button.show() -class _Menu(SugarExt.Menu): - - __gtype_name__ = 'SugarPaletteMenu' - - __gsignals__ = { - 'item-inserted': (GObject.SignalFlags.RUN_FIRST, None, ([])), - } - - def __init__(self, palette): - SugarExt.Menu.__init__(self) - self._palette = palette - - def do_insert(self, item, position): - SugarExt.Menu.do_insert(self, item, position) - self.emit('item-inserted') - self.show() - - def attach(self, child, left_attach, right_attach, - top_attach, bottom_attach): - SugarExt.Menu.attach(self, child, left_attach, right_attach, - top_attach, bottom_attach) - self.emit('item-inserted') - self.show() - - def do_expose_event(self, event): - # Ignore the Menu expose, just do the MenuShell expose to prevent any - # border from being drawn here. A border is drawn by the palette object - # around everything. - Gtk.MenuShell.do_expose_event(self, event) - - def do_grab_notify(self, was_grabbed): - # Ignore grab_notify as the menu would close otherwise - pass - - def do_deactivate(self): - self._palette.hide() - - class _SecondaryAnimation(animator.Animation): def __init__(self, palette): diff --git a/src/sugar3/graphics/palettegroup.py b/src/sugar3/graphics/palettegroup.py index cf389277..2d1537d9 100644 --- a/src/sugar3/graphics/palettegroup.py +++ b/src/sugar3/graphics/palettegroup.py @@ -75,6 +75,11 @@ class Group(GObject.GObject): self._sig_ids[palette].append(sid) def remove(self, palette): + if not palette in self._palettes: + # This happens when converting a window based palette to a menu + # based one. + return + sig_ids = self._sig_ids[palette] for sid in sig_ids: palette.disconnect(sid) diff --git a/src/sugar3/graphics/palettewindow.py b/src/sugar3/graphics/palettewindow.py index 562f1d5f..760c99a5 100644 --- a/src/sugar3/graphics/palettewindow.py +++ b/src/sugar3/graphics/palettewindow.py @@ -1,6 +1,8 @@ # Copyright (C) 2007, Eduardo Silva # Copyright (C) 2008, One Laptop Per Child # Copyright (C) 2009, Tomeu Vizoso +# Copyright (C) 2011, Benjamin Berg +# Copyright (C) 2011, Marco Pesenti Gritti # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -65,6 +67,258 @@ def _calculate_gap(a, b): return False +class _PaletteMenuWidget(Gtk.Menu): + + __gtype_name__ = "SugarPaletteMenuWidget" + + __gsignals__ = { + 'enter-notify': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'leave-notify': (GObject.SignalFlags.RUN_FIRST, None, ([])), + } + + def __init__(self): + Gtk.Menu.__init__(self) + + accel_group = Gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.get_toplevel().add_accel_group(accel_group) + + self._popup_position = (0, 0) + self._entered = False + self._mouse_in_palette = False + self._mouse_in_invoker = False + self._up = False + self._invoker = None + self._menus = [] + + def set_accept_focus(self, focus): + pass + + def get_origin(self): + res_, x, y = self.get_toplevel().get_window().get_origin() + return x, y + + def do_size_request(self, requisition): + Gtk.Window.do_size_request(self, requisition) + requisition.width = max(requisition.width, style.GRID_CELL_SIZE * 2) + + def move(self, x, y): + self._popup_position = (x, y) + + def set_transient_for(self, window): + pass + + def _position(self, widget, data): + return self._popup_position[0], self._popup_position[1], False + + def popup(self, invoker): + if self._up: + return + + # We need to track certain mouse events in order to close the palette + # when the mouse leaves the palette and the invoker widget, but + # GtkMenu makes our lives hard here. + # + # GtkMenu takes a grab on the root window, meaning that normal + # enter/leave events are not sent to the relevant widgets. + # However, connecting enter-notify and leave-notify events in this + # GtkMenu subclass mean that we get to see the events being grabbed. + # With certain filtering in place (see _enter_notify_cb and + # _leave_notify_cb) we are able to accurately determine when the + # mouse leaves/enters the palette menu. Some spurious events are + # generated but the important thing is that the last event generated + # in response to a user action is always reliable (i.e. we will + # always get a leave event last if the user left the menu, + # even if we get some strange enter events leading up to it). + # + # This is complicated with submenus; in this case the submenu takes + # the grab, so we must also listen for events on any submenus of + # the palette and apply the same considerations. + # + # The remaining challenge is tracking when the mouse enters or leaves + # the invoker area. While the appropriate GtkMenu grab is active, + # we do get informed of such events, however these events will only + # arrive if the user has entered the menu. If the user hovers over + # the invoker and then leaves the invoker without entering the palette, + # we get no enter/leave event. + # We work around this by tracking mouse motion events. When the mouse + # moves, we compare the mouse coordinates to the region occupied by the + # invoker, and this lets us track enter/leave for the invoker widget. + + self._invoker = invoker + self._find_all_menus(self) + for menu in self._menus: + if self._invoker: + menu.connect('motion-notify-event', self._motion_notify_cb) + menu.connect('enter-notify-event', self._enter_notify_cb) + menu.connect('leave-notify-event', self._leave_notify_cb) + self._entered = False + self._mouse_in_palette = False + self._mouse_in_invoker = False + Gtk.Menu.popup(self, None, None, self._position, None, 0, 0) + self._up = True + + def popdown(self): + if not self._up: + return + Gtk.Menu.popdown(self) + + for menu in self._menus: + menu.disconnect_by_func(self._motion_notify_cb) + menu.disconnect_by_func(self._enter_notify_cb) + menu.disconnect_by_func(self._leave_notify_cb) + + self._up = False + self._menus = [] + self._invoker = None + + def _find_all_menus(self, menu): + """ + Recursively find all submenus of menu, adding them to self._menus. + """ + self._menus.append(menu) + for child in menu.get_children(): + if not isinstance(child, Gtk.MenuItem): + continue + submenu = child.get_submenu() + if submenu and isinstance(submenu, Gtk.Menu): + self._find_all_menus(submenu) + + def _enter_notify_cb(self, widget, event): + if event.mode in (Gdk.CrossingMode.GRAB, Gdk.CrossingMode.GTK_GRAB): + return False + if Gtk.get_event_widget(event) not in self._menus: + return False + + self._mouse_in_palette = True + self._reevaluate_state() + return False + + def _leave_notify_cb(self, widget, event): + if event.mode in (Gdk.CrossingMode.GRAB, Gdk.CrossingMode.GTK_GRAB): + return False + if Gtk.get_event_widget(event) not in self._menus: + return False + + self._mouse_in_palette = False + self._reevaluate_state() + return False + + def _motion_notify_cb(self, widget, event): + rect = self._invoker.get_rect() + x = event.x_root + y = event.y_root + in_invoker = x >= rect.x and x < (rect.x + rect.width) \ + and y >= rect.y and y < (rect.y + rect.height) + if in_invoker != self._mouse_in_invoker: + self._mouse_in_invoker = in_invoker + self._reevaluate_state() + + def _reevaluate_state(self): + if self._entered: + # If we previously advised that the mouse was inside, but now the + # mouse is outside both the invoker and the palette, notify that + # the mouse has left. + if not self._mouse_in_palette and not self._mouse_in_invoker: + self._entered = False + self.emit('leave-notify') + else: + # If we previously advised that the mouse had left, but now the + # mouse is inside either the palette or the invoker, notify that + # the mouse has entered. + if self._mouse_in_palette or self._mouse_in_invoker: + self._entered = True + self.emit('enter-notify') + + +class _PaletteWindowWidget(Gtk.Window): + + __gtype_name__ = 'SugarPaletteWindowWidget' + + __gsignals__ = { + 'enter-notify': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'leave-notify': (GObject.SignalFlags.RUN_FIRST, None, ([])), + } + + def __init__(self): + Gtk.Window.__init__(self) + + self.set_decorated(False) + self.set_resizable(False) + self.set_position(Gtk.WindowPosition.NONE) + + accel_group = Gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self._old_alloc = None + + self._should_accept_focus = True + + def set_accept_focus(self, focus): + self._should_accept_focus = focus + if self.get_window() != None: + self.get_window().set_accept_focus(focus) + + def get_origin(self): + res_, x, y = self.get_window().get_origin() + return x, y + + def do_realize(self): + Gtk.Window.do_realize(self) + + self.get_window().set_accept_focus(self._should_accept_focus) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + + def do_size_request(self, requisition): + Gtk.Window.do_size_request(self, requisition) + requisition.width = max(requisition.width, style.GRID_CELL_SIZE * 2) + + def do_size_allocate(self, allocation): + Gtk.Window.do_size_allocate(self, allocation) + + if self._old_alloc is None or \ + self._old_alloc.x != allocation.x or \ + self._old_alloc.y != allocation.y or \ + self._old_alloc.width != allocation.width or \ + self._old_alloc.height != allocation.height: + self.queue_draw() + + # We need to store old allocation because when size_allocate + # is called widget.allocation is already updated. + # Gtk.Window resizing is different from normal containers: + # the X window is resized, widget.allocation is updated from + # the configure request handler and finally size_allocate is called. + self._old_alloc = allocation + + def __enter_notify_event_cb(self, widget, event): + if event.mode == Gdk.CrossingMode.NORMAL and \ + event.detail != Gdk.NotifyType.INFERIOR: + self.emit('enter-notify') + return False + + def __leave_notify_event_cb(self, widget, event): + if event.mode != Gdk.CrossingMode.NORMAL: + return False + + if event.detail != Gdk.NotifyType.INFERIOR: + self.emit('leave-notify') + + def popup(self, invoker): + if self.get_visible(): + return + self.connect('enter-notify-event', self.__enter_notify_event_cb) + self.connect('leave-notify-event', self.__leave_notify_event_cb) + self.show() + + def popdown(self): + if not self.get_visible(): + return + self.disconnect_by_func(self.__enter_notify_event_cb) + self.disconnect_by_func(self.__leave_notify_event_cb) + self.hide() + + class MouseSpeedDetector(GObject.GObject): __gsignals__ = { @@ -75,15 +329,15 @@ class MouseSpeedDetector(GObject.GObject): _MOTION_SLOW = 1 _MOTION_FAST = 2 - def __init__(self, parent, delay, thresh): + def __init__(self, delay, thresh): """Create MouseSpeedDetector object, delay in msec threshold in pixels (per tick of 'delay' msec)""" GObject.GObject.__init__(self) + self.parent = None self._threshold = thresh - self._parent = parent self._delay = delay self._state = None self._timeout_hid = None @@ -101,8 +355,10 @@ class MouseSpeedDetector(GObject.GObject): self._state = None def _get_mouse_position(self): - display = Gdk.Display.get_default() - screen_, x, y, mask_ = display.get_pointer() + display = self.parent.get_display() + manager = display.get_device_manager() + pointer_device = manager.get_client_pointer() + screen, x, y = pointer_device.get_position() return (x, y) def _detect_motion(self): @@ -128,14 +384,16 @@ class MouseSpeedDetector(GObject.GObject): return True -class PaletteWindow(Gtk.Window): +class PaletteWindow(GObject.GObject): + """ + Base class for _ToolbarPalette and Palette. - __gtype_name__ = 'SugarPaletteWindow' + Provides basic management of child widget, invoker, and animation. + """ __gsignals__ = { 'popup': (GObject.SignalFlags.RUN_FIRST, None, ([])), 'popdown': (GObject.SignalFlags.RUN_FIRST, None, ([])), - 'activate': (GObject.SignalFlags.RUN_FIRST, None, ([])), } def __init__(self, **kwargs): @@ -146,8 +404,8 @@ class PaletteWindow(Gtk.Window): self._cursor_y = 0 self._alignment = None self._up = False - self._old_alloc = None self._palette_state = None + self._widget = None self._popup_anim = animator.Animator(.5, 10) self._popup_anim.add(_PopupAnimation(self)) @@ -157,29 +415,35 @@ class PaletteWindow(Gtk.Window): GObject.GObject.__init__(self, **kwargs) - self.set_decorated(False) - self.set_resizable(False) - # Just assume xthickness and ythickness are the same - self.set_border_width(self.get_style().xthickness) + self.set_group_id('default') - accel_group = Gtk.AccelGroup() - self.set_data('sugar-accel-group', accel_group) - self.add_accel_group(accel_group) + self._mouse_detector = MouseSpeedDetector(200, 5) - self.set_group_id('default') + def _setup_widget(self): + self._widget.connect('show', self.__show_cb) + self._widget.connect('hide', self.__hide_cb) + self._widget.connect('destroy', self.__destroy_cb) + self._widget.connect('enter-notify', self.__enter_notify_cb) + self._widget.connect('leave-notify', self.__leave_notify_cb) - self.connect('show', self.__show_cb) - self.connect('hide', self.__hide_cb) - self.connect('realize', self.__realize_cb) - self.connect('destroy', self.__destroy_cb) - self.connect('enter-notify-event', self.__enter_notify_event_cb) - self.connect('leave-notify-event', self.__leave_notify_event_cb) + self._set_effective_group_id(self._group_id) - self._mouse_detector = MouseSpeedDetector(self, 200, 5) self._mouse_detector.connect('motion-slow', self._mouse_slow_cb) + self._mouse_detector.parent = self._widget + + def _teardown_widget(self): + self._widget.disconnect_by_func(self.__show_cb) + self._widget.disconnect_by_func(self.__hide_cb) + self._widget.disconnect_by_func(self.__destroy_cb) + self._widget.disconnect_by_func(self.__enter_notify_cb) + self._widget.disconnect_by_func(self.__leave_notify_cb) + self._set_effective_group_id(None) + + def destroy(self): + if self._widget is not None: + self._widget.destroy() def __destroy_cb(self, palette): - self.set_group_id(None) self._mouse_detector.disconnect_by_func(self._mouse_slow_cb) def set_invoker(self, invoker): @@ -203,9 +467,6 @@ class PaletteWindow(Gtk.Window): getter=get_invoker, setter=set_invoker) - def __realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - def _mouse_slow_cb(self, widget): self._mouse_detector.stop() self._palette_do_popup() @@ -228,15 +489,18 @@ class PaletteWindow(Gtk.Window): def is_up(self): return self._up - def set_group_id(self, group_id): + def _set_effective_group_id(self, group_id): if self._group_id: group = palettegroup.get_group(self._group_id) group.remove(self) if group_id: - self._group_id = group_id group = palettegroup.get_group(group_id) group.add(self) + def set_group_id(self, group_id): + self._set_effective_group_id(group_id) + self._group_id = group_id + def get_group_id(self): return self._group_id @@ -244,69 +508,21 @@ class PaletteWindow(Gtk.Window): getter=get_group_id, setter=set_group_id) - def do_size_request(self, requisition): - Gtk.Window.do_size_request(self, requisition) - requisition.width = max(requisition.width, style.GRID_CELL_SIZE * 2) - - def do_size_allocate(self, allocation): - Gtk.Window.do_size_allocate(self, allocation) - - if self._old_alloc is None or \ - self._old_alloc.x != allocation.x or \ - self._old_alloc.y != allocation.y or \ - self._old_alloc.width != allocation.width or \ - self._old_alloc.height != allocation.height: - self.queue_draw() - - # We need to store old allocation because when size_allocate - # is called widget.allocation is already updated. - # Gtk.Window resizing is different from normal containers: - # the X window is resized, widget.allocation is updated from - # the configure request handler and finally size_allocate is called. - self._old_alloc = allocation - - def do_expose_event(self, event): - # We want to draw a border with a beautiful gap - if self._invoker is not None and self._invoker.has_rectangle_gap(): - invoker = self._invoker.get_rect() - palette = self.get_rect() - - gap = _calculate_gap(palette, invoker) - else: - gap = False - - allocation = self.get_allocation() - wstyle = self.get_style() - - if gap: - wstyle.paint_box_gap(event.window, Gtk.StateType.PRELIGHT, - Gtk.ShadowType.IN, event.area, self, 'palette', - 0, 0, allocation.width, allocation.height, - gap[0], gap[1], gap[2]) - else: - wstyle.paint_box(event.window, Gtk.StateType.PRELIGHT, - Gtk.ShadowType.IN, event.area, self, 'palette', - 0, 0, allocation.width, allocation.height) - - # Fall trough to the container expose handler. - # (Leaving out the window expose handler which redraws everything) - Gtk.Bin.do_expose_event(self, event) - def update_position(self): invoker = self._invoker if invoker is None or self._alignment is None: logging.error('Cannot update the palette position.') return - rect = self.size_request() + rect = self._widget.size_request() position = invoker.get_position_for_alignment(self._alignment, rect) if position is None: position = invoker.get_position(rect) - self.move(position.x, position.y) + self._widget.move(position.x, position.y) def get_full_size_request(self): - return self.size_request() + return self._widget.size_request() def popup(self, immediate=False): if self._invoker is not None: @@ -314,7 +530,7 @@ class PaletteWindow(Gtk.Window): self._alignment = self._invoker.get_alignment(full_size_request) self.update_position() - self.set_transient_for(self._invoker.get_toplevel()) + self._widget.set_transient_for(self._invoker.get_toplevel()) self._popdown_anim.stop() @@ -322,7 +538,7 @@ class PaletteWindow(Gtk.Window): self._popup_anim.start() else: self._popup_anim.stop() - self.show() + self._widget.popup(self._invoker) # we have to invoke update_position() twice # since WM could ignore first move() request self.update_position() @@ -335,8 +551,8 @@ class PaletteWindow(Gtk.Window): self._popdown_anim.start() else: self._popdown_anim.stop() - self.size_request() - self.hide() + if self._widget is not None: + self._widget.popdown() def on_invoker_enter(self): self._popdown_anim.stop() @@ -346,10 +562,10 @@ class PaletteWindow(Gtk.Window): self._mouse_detector.stop() self.popdown() - def on_enter(self, event): + def on_enter(self): self._popdown_anim.stop() - def on_leave(self, event): + def on_leave(self): self.popdown() def _invoker_mouse_enter_cb(self, invoker): @@ -361,15 +577,11 @@ class PaletteWindow(Gtk.Window): def _invoker_right_click_cb(self, invoker): self.popup(immediate=True) - def __enter_notify_event_cb(self, widget, event): - if event.detail != Gdk.NOTIFY_INFERIOR and \ - event.mode == Gdk.CROSSING_NORMAL: - self.on_enter(event) + def __enter_notify_cb(self, widget): + self.on_enter() - def __leave_notify_event_cb(self, widget, event): - if event.detail != Gdk.NOTIFY_INFERIOR and \ - event.mode == Gdk.CROSSING_NORMAL: - self.on_leave(event) + def __leave_notify_cb(self, widget): + self.on_leave() def __show_cb(self, widget): if self._invoker is not None: @@ -386,14 +598,20 @@ class PaletteWindow(Gtk.Window): self.emit('popdown') def get_rect(self): - win_x, win_y = self.get_window().get_origin() + win_x, win_y = self._widget.get_origin() rectangle = self.get_allocation() x = win_x + rectangle.x y = win_y + rectangle.y - width, height = self.size_request() + requisition = self._widget.size_request() - return (x, y, width, height) + rect = Gdk.Rectangle() + rect.x = x + rect.y = y + rect.width = requisition.width + rect.height = requisition.height + + return rect def get_palette_state(self): return self._palette_state @@ -453,8 +671,10 @@ class Invoker(GObject.GObject): self.parent = None - self._screen_area = (0, 0, Gdk.Screen.width(), - Gdk.Screen.height()) + self._screen_area = Gdk.Rectangle() + self._screen_area.x = self._screen_area.y = 0 + self._screen_area.width = Gdk.Screen.width() + self._screen_area.height = Gdk.Screen.height() self._position_hint = self.ANCHORED self._cursor_x = -1 self._cursor_y = -1 @@ -477,8 +697,10 @@ class Invoker(GObject.GObject): invoker_valign = alignment[3] if self._cursor_x == -1 or self._cursor_y == -1: - display = Gdk.Display.get_default() - screen_, x, y, mask_ = display.get_pointer() + display = self.parent.get_display() + manager = display.get_device_manager() + pointer_device = manager.get_client_pointer() + screen, x, y = pointer_device.get_position() self._cursor_x = x self._cursor_y = y @@ -486,11 +708,12 @@ class Invoker(GObject.GObject): rect = self.get_rect() else: dist = style.PALETTE_CURSOR_DISTANCE - rect = (self._cursor_x - dist, - self._cursor_y - dist, - dist * 2, dist * 2) + rect = Gdk.Rectangle() + rect.x = self._cursor_x - dist + rect.y = self._cursor_y - dist + rect.width = rect.height = dist * 2 - palette_width, palette_height = palette_dim + palette_width, palette_height = palette_dim.width, palette_dim.height x = rect.x + rect.width * invoker_halign + \ palette_width * palette_halign @@ -498,8 +721,12 @@ class Invoker(GObject.GObject): y = rect.y + rect.height * invoker_valign + \ palette_height * palette_valign - return (int(x), int(y), - palette_width, palette_height) + rect = Gdk.Rectangle() + rect.x = int(x) + rect.y = int(y) + rect.width = palette_width + rect.height = palette_height + return rect def _in_screen(self, rect): return rect.x >= self._screen_area.x and \ @@ -582,12 +809,12 @@ class Invoker(GObject.GObject): # Set palette_valign to align to screen on the top if dtop > dbottom: - pv = -float(dtop) / palette_dim[1] + pv = -float(dtop) / palette_dim.height # Set palette_valign to align to screen on the bottom else: - pv = -float(palette_dim[1] - dbottom - rect.height) \ - / palette_dim[1] + pv = -float(palette_dim.height - dbottom - rect.height) \ + / palette_dim.height elif best_alignment in self.TOP or best_alignment in self.BOTTOM: dleft = rect.x - screen_area.x @@ -597,12 +824,12 @@ class Invoker(GObject.GObject): # Set palette_halign to align to screen on left if dleft > dright: - ph = -float(dleft) / palette_dim[0] + ph = -float(dleft) / palette_dim.width # Set palette_halign to align to screen on right else: - ph = -float(palette_dim[0] - dright - rect.width) \ - / palette_dim[0] + ph = -float(palette_dim.width - dright - rect.width) \ + / palette_dim.width return (ph, pv, ih, iv) @@ -717,30 +944,31 @@ class WidgetInvoker(Invoker): allocation = self._widget.get_allocation() window = self._widget.get_window() if window is not None: - x, y = window.get_origin() + res, x, y = window.get_origin() else: logging.warning( "Trying to position palette with invoker that's not realized.") x = 0 y = 0 - if self._widget.flags() & Gtk.NO_WINDOW: - x += allocation.x - y += allocation.y + x += allocation.x + y += allocation.y width = allocation.width height = allocation.height - return (x, y, width, height) + rect = Gdk.Rectangle() + rect.x = x + rect.y = y + rect.width = width + rect.height = height + return rect def has_rectangle_gap(self): return True def draw_rectangle(self, event, palette): - if self._widget.flags() & Gtk.NO_WINDOW: - x, y = self._widget.allocation.x, self._widget.allocation.y - else: - x = y = 0 + x, y = self._widget.allocation.x, self._widget.allocation.y wstyle = self._widget.get_style() gap = _calculate_gap(self.get_rect(), palette.get_rect()) @@ -763,7 +991,8 @@ class WidgetInvoker(Invoker): self.notify_mouse_enter() def __leave_notify_event_cb(self, widget, event): - self.notify_mouse_leave() + if event.mode == Gdk.CrossingMode.NORMAL: + self.notify_mouse_leave() def __button_release_event_cb(self, widget, event): if event.button == 3: @@ -908,16 +1137,15 @@ class CellRendererInvoker(Invoker): allocation = self._tree_view.get_allocation() window = self._tree_view.get_window() if window is not None: - x, y = window.get_origin() + res, x, y = window.get_origin() else: logging.warning( "Trying to position palette with invoker that's not realized.") x = 0 y = 0 - if self._tree_view.flags() & Gtk.NO_WINDOW: - x += allocation.x - y += allocation.y + x += allocation.x + y += allocation.y width = allocation.width height = allocation.height diff --git a/src/sugar3/graphics/toolbarbox.py b/src/sugar3/graphics/toolbarbox.py index b3217156..5ec2db98 100644 --- a/src/sugar3/graphics/toolbarbox.py +++ b/src/sugar3/graphics/toolbarbox.py @@ -15,11 +15,14 @@ # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. +import math + from gi.repository import Gtk from gi.repository import GObject from sugar3.graphics import style -from sugar3.graphics.palette import PaletteWindow, ToolInvoker +from sugar3.graphics.palettewindow import PaletteWindow, ToolInvoker, \ + _PaletteWindowWidget from sugar3.graphics.toolbutton import ToolButton from sugar3.graphics import palettegroup @@ -74,7 +77,7 @@ class ToolbarButton(ToolButton): def is_in_palette(self): return self.page is not None and \ - self.page_widget.get_parent() == self.props.palette + self.page_widget.get_parent() == self.props.palette._widget def is_expanded(self): return self.page is not None and \ @@ -97,10 +100,7 @@ class ToolbarButton(ToolButton): box = self.toolbar_box if box.expanded_button is not None: - button_window = box.expanded_button.get_window() - if button_window is not None: - # need to redraw it to erase arrow - button_window.invalidate_rect(None, True) + box.expanded_button.queue_draw() box.expanded_button.set_expanded(False) box.expanded_button = self @@ -117,7 +117,7 @@ class ToolbarButton(ToolButton): self._unparent() if isinstance(self.props.palette, _ToolbarPalette): - self.props.palette.add(self.page_widget) + self.props.palette._widget.add(self.page_widget) def _unparent(self): page_parent = self.page_widget.get_parent() @@ -125,28 +125,22 @@ class ToolbarButton(ToolButton): return page_parent.remove(self.page_widget) - def do_expose_event(self, event): + def do_draw(self, cr): if not self.is_expanded() or self.props.palette is not None and \ self.props.palette.is_up(): - ToolButton.do_expose_event(self, event) - _paint_arrow(self, event, Gtk.ArrowType.DOWN) + Gtk.ToolButton.do_draw(self, cr) + _paint_arrow(self, cr, math.pi) return - alloc = self.allocation - - self.get_style().paint_box(event.window, - Gtk.StateType.NORMAL, Gtk.ShadowType.IN, event.area, self, - 'palette-invoker', alloc.x, 0, - alloc.width, alloc.height + style.FOCUS_LINE_WIDTH) + alloc = self.get_allocation() - if self.get_child().state != Gtk.StateType.PRELIGHT: - self.get_style().paint_box(event.window, - Gtk.StateType.NORMAL, Gtk.ShadowType.NONE, event.area, self, None, - alloc.x + style.FOCUS_LINE_WIDTH, style.FOCUS_LINE_WIDTH, - alloc.width - style.FOCUS_LINE_WIDTH * 2, alloc.height) + context = self.get_style_context() + context.add_class('toolitem') - Gtk.ToolButton.do_expose_event(self, event) - _paint_arrow(self, event, Gtk.ArrowType.UP) + Gtk.render_frame_gap(context, cr, 0, 0, alloc.width, alloc.height, + Gtk.PositionType.BOTTOM, 0, alloc.width) + Gtk.ToolButton.do_draw(self, cr) + _paint_arrow(self, cr, 0) class ToolbarBox(Gtk.VBox): @@ -214,15 +208,20 @@ class _ToolbarPalette(PaletteWindow): def __init__(self, **kwargs): PaletteWindow.__init__(self, **kwargs) - self.set_border_width(0) self._has_focus = False group = palettegroup.get_group('default') group.connect('popdown', self.__group_popdown_cb) self.set_group_id('toolbarbox') + self._widget = _PaletteWindowWidget() + self._widget.set_border_width(0) + self._setup_widget() + + self._widget.connect('realize', self._realize_cb) + def get_expanded_button(self): - return self.invoker.get_parent() + return self.invoker.parent expanded_button = property(get_expanded_button) @@ -234,12 +233,12 @@ class _ToolbarPalette(PaletteWindow): PaletteWindow.on_invoker_leave(self) self._set_focus(False) - def on_enter(self, event): - PaletteWindow.on_enter(self, event) + def on_enter(self): + PaletteWindow.on_enter(self) self._set_focus(True) - def on_leave(self, event): - PaletteWindow.on_enter(self, event) + def on_leave(self): + PaletteWindow.on_enter(self) self._set_focus(False) def _set_focus(self, new_focus): @@ -249,10 +248,10 @@ class _ToolbarPalette(PaletteWindow): if not group.is_up(): self.popdown() - def do_size_request(self, requisition): - Gtk.Window.do_size_request(self, requisition) - requisition.width = max(requisition.width, - Gdk.Screen.width()) + def _realize_cb(self, widget): + screen = self._widget.get_screen() + width = screen.width() + self._widget.set_size_request(width, -1) def popup(self, immediate=False): button = self.expanded_button @@ -272,10 +271,10 @@ class _Box(Gtk.EventBox): def __init__(self): GObject.GObject.__init__(self) - self.connect('expose-event', self.do_expose_event) self.set_app_paintable(True) def do_expose_event(self, widget, event): + # TODO: reimplement this in the theme expanded_button = self.get_parent().expanded_button if expanded_button is None: return @@ -326,12 +325,14 @@ def _get_embedded_page(page_widget): return page_widget.get_child().get_child() -def _paint_arrow(widget, event, arrow_type): - alloc = widget.allocation - x = alloc.x + alloc.width / 2 - style.TOOLBAR_ARROW_SIZE / 2 - y = alloc.y + alloc.height - int(style.TOOLBAR_ARROW_SIZE * .85) +def _paint_arrow(widget, cr, angle): + alloc = widget.get_allocation() + + arrow_size = style.TOOLBAR_ARROW_SIZE / 2 + y = alloc.height - arrow_size + x = (alloc.width - arrow_size) / 2 + + context = widget.get_style_context() + context.add_class('toolitem') - widget.get_style().paint_arrow(event.window, - Gtk.StateType.NORMAL, Gtk.ShadowType.NONE, event.area, widget, - None, arrow_type, True, - x, y, style.TOOLBAR_ARROW_SIZE, style.TOOLBAR_ARROW_SIZE) + Gtk.render_arrow(context, cr, angle, x, y, arrow_size) diff --git a/src/sugar3/sugar-menu.c b/src/sugar3/sugar-menu.c deleted file mode 100644 index f19dc4b8..00000000 --- a/src/sugar3/sugar-menu.c +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2006-2007, Red Hat, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#include - -#include "sugar-menu.h" - -static void sugar_menu_class_init (SugarMenuClass *menu_class); -static void sugar_menu_init (SugarMenu *menu); - - -G_DEFINE_TYPE(SugarMenu, sugar_menu, GTK_TYPE_MENU) - -void -sugar_menu_set_active(SugarMenu *menu, gboolean active) -{ - GTK_MENU_SHELL(menu)->active = active; -} - -void -sugar_menu_embed(SugarMenu *menu, GtkContainer *parent) -{ - menu->orig_toplevel = GTK_MENU(menu)->toplevel; - - GTK_MENU(menu)->toplevel = gtk_widget_get_toplevel(GTK_WIDGET(parent)); - gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(parent)); -} - -void -sugar_menu_unembed(SugarMenu *menu) -{ - if (menu->orig_toplevel) { - GTK_MENU(menu)->toplevel = menu->orig_toplevel; - gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(menu->orig_toplevel)); - } -} - -static void -sugar_menu_class_init(SugarMenuClass *menu_class) -{ -} - -static void -sugar_menu_init(SugarMenu *menu) -{ - menu->orig_toplevel = NULL; -} diff --git a/src/sugar3/sugar-menu.h b/src/sugar3/sugar-menu.h deleted file mode 100644 index c3bb3d02..00000000 --- a/src/sugar3/sugar-menu.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2006-2007, Red Hat, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#ifndef __SUGAR_MENU_H__ -#define __SUGAR_MENU_H__ - -#include - -G_BEGIN_DECLS - -typedef struct _SugarMenu SugarMenu; -typedef struct _SugarMenuClass SugarMenuClass; - -#define SUGAR_TYPE_MENU (sugar_menu_get_type()) -#define SUGAR_MENU(object) (G_TYPE_CHECK_INSTANCE_CAST((object), SUGAR_TYPE_MENU, SugarMenu)) -#define SUGAR_MENU_CLASS(klass) (G_TYPE_CHACK_CLASS_CAST((klass), SUGAR_TYPE_MENU, SugarMenuClass)) -#define SUGAR_IS_MENU(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), SUGAR_TYPE_MENU)) -#define SUGAR_IS_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SUGAR_TYPE_MENU)) -#define SUGAR_MENU_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_MENU, SugarMenuClass)) - -struct _SugarMenu { - GtkMenu base_instance; - - GtkWidget *orig_toplevel; - int min_width; -}; - -struct _SugarMenuClass { - GtkMenuClass base_class; -}; - -GType sugar_menu_get_type (void); -void sugar_menu_set_active (SugarMenu *menu, - gboolean active); -void sugar_menu_embed (SugarMenu *menu, - GtkContainer *parent); -void sugar_menu_unembed (SugarMenu *menu); - -G_END_DECLS - -#endif /* __SUGAR_MENU_H__ */