diff --git a/examples/cellrenderericon.py b/examples/cellrenderericon.py index 1f875520..e2802045 100644 --- a/examples/cellrenderericon.py +++ b/examples/cellrenderericon.py @@ -21,7 +21,7 @@ treeview.show() col = Gtk.TreeViewColumn() treeview.append_column(col) -cell_icon = CellRendererIcon(treeview) +cell_icon = CellRendererIcon() cell_icon.props.width = style.GRID_CELL_SIZE cell_icon.props.height = style.GRID_CELL_SIZE cell_icon.props.size = style.SMALL_ICON_SIZE diff --git a/examples/scrollingdetector.py b/examples/scrollingdetector.py new file mode 100644 index 00000000..8f7243a0 --- /dev/null +++ b/examples/scrollingdetector.py @@ -0,0 +1,71 @@ +import os +import time + +from gi.repository import Gtk + +from sugar3.graphics import style +from sugar3.graphics.icon import CellRendererIcon +from sugar3.graphics.xocolor import XoColor +from sugar3.graphics.scrollingdetector import ScrollingDetector +from sugar3.graphics.palettewindow import TreeViewInvoker +import common + + +def _scroll_start_cb(event, treeview, invoker): + print "Scroll starts" + invoker.detach() + +def _scroll_end_cb(event, treeview, invoker): + print "Scroll ends" + invoker.attach_treeview(treeview) + +test = common.Test() +test.show() + +model = Gtk.ListStore(str) + +data_dir = os.getenv('GTK_DATA_PREFIX', '/usr/') + +iconlist = os.listdir(os.path.join(data_dir, + 'share/icons/sugar/scalable/actions/')) +print "Displaying %s icons" % len(iconlist) +for icon in iconlist: + icon = os.path.basename(icon) + icon = icon[:icon.find('.')] + model.append([icon]) + +scrolled = Gtk.ScrolledWindow() +scrolled.set_size_request(800, 800) +treeview = Gtk.TreeView() + +treeview.set_model(model) +scrolled.add(treeview) +test.pack_start(scrolled, True, True, 0) +test.show_all() + +col = Gtk.TreeViewColumn() +treeview.append_column(col) + +xo_color = XoColor('#FF0000,#00FF00') +cell_icon = CellRendererIcon() +cell_icon.props.width = style.GRID_CELL_SIZE +cell_icon.props.height = style.GRID_CELL_SIZE +cell_icon.props.size = style.STANDARD_ICON_SIZE +cell_icon.props.xo_color = xo_color + +col.pack_start(cell_icon, expand=False) +col.add_attribute(cell_icon, 'icon-name', 0) +cell_text = Gtk.CellRendererText() +col.pack_start(cell_text, expand=True) +col.add_attribute(cell_text, 'text', 0) + +invoker = TreeViewInvoker() +invoker.attach_treeview(treeview) + +detector = ScrollingDetector(scrolled) +detector.connect('scroll-start', _scroll_start_cb, treeview, invoker) +detector.connect('scroll-end', _scroll_end_cb, treeview, invoker) + +if __name__ == '__main__': + time_ini = time.time() + common.main(test) diff --git a/src/sugar3/graphics/Makefile.am b/src/sugar3/graphics/Makefile.am index 344cc82d..0621750e 100644 --- a/src/sugar3/graphics/Makefile.am +++ b/src/sugar3/graphics/Makefile.am @@ -18,6 +18,7 @@ sugar_PYTHON = \ panel.py \ radiopalette.py \ radiotoolbutton.py \ + scrollingdetector.py \ style.py \ toggletoolbutton.py \ toolbarbox.py \ diff --git a/src/sugar3/graphics/icon.py b/src/sugar3/graphics/icon.py index fc798ee7..43b78a75 100644 --- a/src/sugar3/graphics/icon.py +++ b/src/sugar3/graphics/icon.py @@ -825,9 +825,11 @@ class CellRendererIcon(Gtk.CellRenderer): 'clicked': (GObject.SignalFlags.RUN_FIRST, None, [object]), } - def __init__(self, tree_view): - from sugar3.graphics.palette import CellRendererInvoker - + def __init__(self, treeview=None): + # treeview is not used anymore, is here just to not break the API + if treeview is not None: + logging.warning('CellRendererIcon: treeview parameter in ' + 'constructor is deprecated') self._buffer = _IconBuffer() self._buffer.cache = True self._xo_color = None @@ -836,36 +838,28 @@ class CellRendererIcon(Gtk.CellRenderer): self._prelit_fill_color = None self._prelit_stroke_color = None self._active_state = False - self._palette_invoker = CellRendererInvoker() self._cached_offsets = None Gtk.CellRenderer.__init__(self) - tree_view.connect('button-press-event', - self.__button_press_event_cb) - tree_view.connect('button-release-event', - self.__button_release_event_cb) + self._is_scrolling = False - self._palette_invoker.attach_cell_renderer(tree_view, self) + def connect_to_scroller(self, scrolled): + scrolled.connect('scroll-start', self._scroll_start_cb) + scrolled.connect('scroll-end', self._scroll_end_cb) - def __del__(self): - self._palette_invoker.detach() + def _scroll_start_cb(self, event): + self._is_scrolling = True - def __button_press_event_cb(self, widget, event): - if self._point_in_cell_renderer(widget, event.x, event.y): - self._active_state = True + def _scroll_end_cb(self, event): + self._is_scrolling = False - def __button_release_event_cb(self, widget, event): - self._active_state = False + def is_scrolling(self): + return self._is_scrolling def create_palette(self): return None - def get_palette_invoker(self): - return self._palette_invoker - - palette_invoker = GObject.property(type=object, getter=get_palette_invoker) - def set_file_name(self, value): if self._buffer.file_name != value: self._buffer.file_name = value @@ -964,81 +958,66 @@ class CellRendererIcon(Gtk.CellRenderer): flags): pass - def _point_in_cell_renderer(self, tree_view, x=None, y=None): - """Check if the point with coordinates x, y is inside this icon. - - If the x, y coordinates are not given, they are taken from the - pointer current position. - - """ - if x is None and y is None: - x, y = tree_view.get_pointer() - x, y = tree_view.convert_widget_to_bin_window_coords(x, y) - pos = tree_view.get_path_at_pos(int(x), int(y)) - if pos is None: - return False - - path_, column, x, y_ = pos - - for cell_renderer in column.get_cells(): - if cell_renderer == self: - cell_x, cell_width = column.cell_get_position(cell_renderer) - if x > cell_x and x < (cell_x + cell_width): - return True - return False - - return False - def do_render(self, cr, widget, background_area, cell_area, flags): - context = widget.get_style_context() - context.save() - context.add_class("sugar-icon-cell") - - def is_pointer_inside(): - # widget is the treeview - x, y = widget.get_pointer() - x, y = widget.convert_widget_to_bin_window_coords(x, y) - return ((cell_area.x <= x <= cell_area.x + cell_area.width) - and (cell_area.y <= y <= cell_area.y + cell_area.height)) - - pointer_inside = is_pointer_inside() - - # The context will have prelight state if the mouse pointer is - # in the entire row, but we want that state if the pointer is - # in this cell only: - if flags & Gtk.CellRendererState.PRELIT: - if pointer_inside: - if self._active_state: - context.set_state(Gtk.StateFlags.ACTIVE) - else: - context.set_state(Gtk.StateFlags.NORMAL) + if not self._is_scrolling: + + context = widget.get_style_context() + context.save() + context.add_class("sugar-icon-cell") + + def is_pointer_inside(): + # widget is the treeview + x, y = widget.get_pointer() + x, y = widget.convert_widget_to_bin_window_coords(x, y) + return ((cell_area.x <= x <= cell_area.x + cell_area.width) + and + (cell_area.y <= y <= cell_area.y + cell_area.height)) + + pointer_inside = is_pointer_inside() + + # The context will have prelight state if the mouse pointer is + # in the entire row, but we want that state if the pointer is + # in this cell only: + if flags & Gtk.CellRendererState.PRELIT: + if pointer_inside: + if self._active_state: + context.set_state(Gtk.StateFlags.ACTIVE) + else: + context.set_state(Gtk.StateFlags.NORMAL) - Gtk.render_background( - context, cr, background_area.x, background_area.y, - background_area.width, background_area.height) + Gtk.render_background( + context, cr, background_area.x, background_area.y, + background_area.width, background_area.height) - if self._xo_color is not None: - stroke_color = self._xo_color.get_stroke_color() - fill_color = self._xo_color.get_fill_color() - prelit_fill_color = None - prelit_stroke_color = None - else: - stroke_color = self._stroke_color - fill_color = self._fill_color - prelit_fill_color = self._prelit_fill_color - prelit_stroke_color = self._prelit_stroke_color + if self._xo_color is not None: + stroke_color = self._xo_color.get_stroke_color() + fill_color = self._xo_color.get_fill_color() + prelit_fill_color = None + prelit_stroke_color = None + else: + stroke_color = self._stroke_color + fill_color = self._fill_color + prelit_fill_color = self._prelit_fill_color + prelit_stroke_color = self._prelit_stroke_color - has_prelit_colors = None not in [prelit_fill_color, - prelit_stroke_color] + has_prelit_colors = None not in [prelit_fill_color, + prelit_stroke_color] - if flags & Gtk.CellRendererState.PRELIT and has_prelit_colors and \ - pointer_inside: + if flags & Gtk.CellRendererState.PRELIT and has_prelit_colors and \ + pointer_inside: - self._buffer.fill_color = prelit_fill_color - self._buffer.stroke_color = prelit_stroke_color + self._buffer.fill_color = prelit_fill_color + self._buffer.stroke_color = prelit_stroke_color + else: + self._buffer.fill_color = fill_color + self._buffer.stroke_color = stroke_color else: - self._buffer.fill_color = fill_color - self._buffer.stroke_color = stroke_color + if self._xo_color is not None: + self._buffer.fill_color = self._xo_color.get_fill_color() + self._buffer.stroke_color = self._xo_color.get_stroke_color() + else: + self._buffer.fill_color = self._fill_color + self._buffer.stroke_color = self._stroke_color surface = self._buffer.get_surface() if surface is None: diff --git a/src/sugar3/graphics/palette.py b/src/sugar3/graphics/palette.py index e50ac392..c7092c23 100644 --- a/src/sugar3/graphics/palette.py +++ b/src/sugar3/graphics/palette.py @@ -38,13 +38,14 @@ from sugar3.graphics.palettewindow import PaletteWindow, \ from sugar3.graphics.palettemenu import PaletteMenuItem from sugar3.graphics.palettewindow import MouseSpeedDetector, Invoker, \ - WidgetInvoker, CursorInvoker, ToolInvoker, CellRendererInvoker + WidgetInvoker, CursorInvoker, ToolInvoker, TreeViewInvoker + assert MouseSpeedDetector assert Invoker assert WidgetInvoker assert CursorInvoker assert ToolInvoker -assert CellRendererInvoker +assert TreeViewInvoker class _HeaderItem(Gtk.MenuItem): @@ -418,7 +419,8 @@ class Palette(PaletteWindow): if self._palette_state == self.PRIMARY: self._secondary_box.show() - self._full_request = self._widget.size_request() + if self._widget is not None: + self._full_request = self._widget.size_request() if self._palette_state == self.PRIMARY: self._secondary_box.hide() diff --git a/src/sugar3/graphics/palettewindow.py b/src/sugar3/graphics/palettewindow.py index 1b43b65c..ed96070c 100644 --- a/src/sugar3/graphics/palettewindow.py +++ b/src/sugar3/graphics/palettewindow.py @@ -35,6 +35,7 @@ from gi.repository import SugarGestures from sugar3.graphics import palettegroup from sugar3.graphics import animator from sugar3.graphics import style +from sugar3.graphics.icon import CellRendererIcon def _calculate_gap(a, b): @@ -223,12 +224,9 @@ class _PaletteMenuWidget(Gtk.Menu): x = event.x_root y = event.y_root - if type(self._invoker) is CellRendererInvoker: - in_invoker = self._invoker.point_in_cell_renderer(x, y) - else: - rect = self._invoker.get_rect() - in_invoker = x >= rect.x and x < (rect.x + rect.width) \ - and y >= rect.y and y < (rect.y + rect.height) + rect = self._invoker.get_rect() + 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 @@ -238,12 +236,9 @@ class _PaletteMenuWidget(Gtk.Menu): x = event.x_root y = event.y_root - if type(self._invoker) is CellRendererInvoker: - in_invoker = self._invoker.point_in_cell_renderer(x, y) - else: - rect = self._invoker.get_rect() - in_invoker = x >= rect.x and x < (rect.x + rect.width) \ - and y >= rect.y and y < (rect.y + rect.height) + rect = self._invoker.get_rect() + in_invoker = x >= rect.x and x < (rect.x + rect.width) \ + and y >= rect.y and y < (rect.y + rect.height) if in_invoker: return True @@ -399,6 +394,7 @@ class _PaletteWindowWidget(Gtk.Window): def popup(self, invoker): if self.get_visible(): + logging.error('PaletteWindowWidget popup get_visible True') return self.connect('enter-notify-event', self.__enter_notify_event_cb) self.connect('leave-notify-event', self.__leave_notify_event_cb) @@ -618,6 +614,9 @@ class PaletteWindow(GObject.GObject): logging.error('Cannot update the palette position.') return + if self._widget is None: + return + req = self._widget.size_request() # on Gtk 3.10, menu at the bottom of the screen are resized # to not fall out, and report a wrong size. @@ -642,6 +641,8 @@ class PaletteWindow(GObject.GObject): return self._widget.size_request() def popup(self, immediate=False): + if self._widget is None: + return if self._invoker is not None: full_size_request = self.get_full_size_request() self._alignment = self._invoker.get_alignment(full_size_request) @@ -1372,28 +1373,34 @@ class ToolInvoker(WidgetInvoker): self._widget.emit('clicked') -class CellRendererInvoker(Invoker): - +class TreeViewInvoker(Invoker): def __init__(self): Invoker.__init__(self) - self._position_hint = self.AT_CURSOR self._tree_view = None - self._cell_renderer = None self._motion_hid = None self._leave_hid = None self._release_hid = None self._long_pressed_hid = None - self.path = None + self._position_hint = self.AT_CURSOR self._long_pressed_controller = SugarGestures.LongPressController() - def attach_cell_renderer(self, tree_view, cell_renderer): + self._mouse_detector = MouseSpeedDetector(200, 5) + + self._tree_view = None + self._path = None + self._column = None + + self.palette = None + + def attach_treeview(self, tree_view): self._tree_view = tree_view - self._cell_renderer = cell_renderer self._motion_hid = tree_view.connect('motion-notify-event', self.__motion_notify_event_cb) + self._enter_hid = tree_view.connect('enter-notify-event', + self.__enter_notify_event_cb) self._leave_hid = tree_view.connect('leave-notify-event', self.__leave_notify_event_cb) self._release_hid = tree_view.connect('button-release-event', @@ -1403,129 +1410,104 @@ class CellRendererInvoker(Invoker): self._long_pressed_controller.attach( tree_view, SugarGestures.EventControllerFlags.NONE) - Invoker.attach(self, cell_renderer) + + self._mouse_detector.connect('motion-slow', self.__mouse_slow_cb) + self._mouse_detector.parent = tree_view + Invoker.attach(self, tree_view) def detach(self): Invoker.detach(self) self._tree_view.disconnect(self._motion_hid) + self._tree_view.disconnect(self._enter_hid) self._tree_view.disconnect(self._leave_hid) self._tree_view.disconnect(self._release_hid) self._long_pressed_controller.detach(self._tree_view) self._long_pressed_controller.disconnect(self._long_pressed_hid) + self._mouse_detector.disconnect_by_func(self.__mouse_slow_cb) def get_rect(self): - allocation = self._tree_view.get_allocation() - window = self._tree_view.get_window() - if window is not None: - res, x, y = window.get_origin() - else: - logging.warning( - "Trying to position palette with invoker that's not realized.") - x = 0 - y = 0 + return self._tree_view.get_background_area(self._path, self._column) - rect = Gdk.Rectangle() - rect.x = x + allocation.x - rect.y = y + allocation.y + def get_toplevel(self): + return self._tree_view.get_toplevel() - rect.width = allocation.width - rect.height = allocation.height + def __motion_notify_event_cb(self, widget, event): + try: + path, column, x_, y_ = self._tree_view.get_path_at_pos( + int(event.x), int(event.y)) + if path != self._path or column != self._column: + self._redraw_cell(self._path, self._column) + self._redraw_cell(path, column) - return rect + self._path = path + self._column = column - def __motion_notify_event_cb(self, widget, event): - if event.window != widget.get_bin_window(): - return - if self.point_in_cell_renderer(event.x, event.y): - - tree_view = self._tree_view - path, column_, x_, y_ = tree_view.get_path_at_pos(int(event.x), - int(event.y)) - if path != self.path: - if self.path is not None: - self._redraw_path(self.path) - if path is not None: - self._redraw_path(path) if self.palette is not None: self.palette.popdown(immediate=True) self.palette = None - self.path = path - if event.get_source_device().get_source() == \ - Gdk.InputSource.TOUCHSCREEN: - return False - self.notify_mouse_enter() - else: - if self.path is not None: - self._redraw_path(self.path) - self.path = None - - if event.get_source_device().get_source() == \ - Gdk.InputSource.TOUCHSCREEN: - return False - self.notify_mouse_leave() + self._mouse_detector.start() + except TypeError: + # tree_view.get_path_at_pos() fail if x,y poition is over + # a empty area + pass - def _redraw_path(self, path): - column = None - for column in self._tree_view.get_columns(): - if self._cell_renderer in column.get_cells(): - break - assert column is not None + def _redraw_cell(self, path, column): area = self._tree_view.get_background_area(path, column) x, y = \ self._tree_view.convert_bin_window_to_widget_coords(area.x, area.y) self._tree_view.queue_draw_area(x, y, area.width, area.height) + def __enter_notify_event_cb(self, widget, event): + self._mouse_detector.start() + def __leave_notify_event_cb(self, widget, event): - if event.mode == Gdk.CrossingMode.NORMAL: - self.notify_mouse_leave() - return False + self._mouse_detector.stop() def __button_release_event_cb(self, widget, event): - if event.button == 1 and self.point_in_cell_renderer(event.x, - event.y): - tree_view = self._tree_view - path, column_, x_, y_ = tree_view.get_path_at_pos(int(event.x), - int(event.y)) - self._cell_renderer.emit('clicked', path) + x, y = int(event.x), int(event.y) + path, column, cell_x, cell_y = self._tree_view.get_path_at_pos(x, y) + self._path = path + self._column = column + if event.button == 1: + # left mouse button + if self.palette is not None: + self.palette.popdown(immediate=True) + # NOTE: we don't use columns with more than one cell + cellrenderer = column.get_cells()[0] + if cellrenderer is not None and \ + isinstance(cellrenderer, CellRendererIcon): + cellrenderer.emit('clicked', path) # So the treeview receives it and knows a drag isn't going on return False - if event.button == 3 and self.point_in_cell_renderer(event.x, - event.y): + if event.button == 3: + # right mouse button + self._mouse_detector.stop() + self._change_palette() self.notify_right_click() return True else: return False def __long_pressed_event_cb(self, controller, x, y, widget): - if self.point_in_cell_renderer(x, y): - self.notify_right_click() - - def point_in_cell_renderer(self, event_x, event_y): - pos = self._tree_view.get_path_at_pos(int(event_x), int(event_y)) - if pos is None: - return False - - path_, column, x, y_ = pos - - for cell_renderer in column.get_cells(): - if cell_renderer == self._cell_renderer: - cell_x, cell_width = column.cell_get_position(cell_renderer) - if x > cell_x and x < (cell_x + cell_width): - return True - return False - - return False + path, column, x_, y_ = self._tree_view.get_path_at_pos(x, y) + self._path = path + self._column = column + self._change_palette() + self.notify_right_click() - def get_toplevel(self): - return self._tree_view.get_toplevel() + def __mouse_slow_cb(self, widget): + self._mouse_detector.stop() + self._change_palette() + self.emit('mouse-enter') - def notify_popup(self): - Invoker.notify_popup(self) + def _change_palette(self): + if hasattr(self._tree_view, 'create_palette'): + self.palette = self._tree_view.create_palette( + self._path, self._column) + else: + self.palette = None def notify_popdown(self): Invoker.notify_popdown(self) self.palette = None - - def get_default_position(self): - return self.AT_CURSOR diff --git a/src/sugar3/graphics/scrollingdetector.py b/src/sugar3/graphics/scrollingdetector.py new file mode 100644 index 00000000..e56dda75 --- /dev/null +++ b/src/sugar3/graphics/scrollingdetector.py @@ -0,0 +1,61 @@ +# Copyright (C) 2014, Sugarlabs +# +# 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. + +from gi.repository import GObject +from gi.repository import GLib + + +class ScrollingDetector(GObject.GObject): + """ + ScollingDetector emit signals when a ScrolledWindow starts and + finish scrolling. Other widets can use that information to + avoid do performance expensive operations. + """ + + scroll_start_signal = GObject.Signal('scroll-start') + scroll_end_signal = GObject.Signal('scroll-end') + + def __init__(self, scrolled_window, timeout=100): + self._scrolled_window = scrolled_window + self._timeout = timeout + self.is_scrolling = False + self._prev_value = 0 + + self.connect_scrolled_window() + GObject.GObject.__init__(self) + + def connect_scrolled_window(self): + adj = self._scrolled_window.get_vadjustment() + adj.connect('value-changed', self._value_changed_cb) + + def _check_scroll_cb(self, adj): + if (adj.props.value == self._prev_value): + self.is_scrolling = False + self.scroll_end_signal.emit() + return False + + self._prev_value = adj.props.value + return True + + def _value_changed_cb(self, adj): + if (self.is_scrolling): + return + + self.is_scrolling = True + self.scroll_start_signal.emit() + self._prev_value = adj.props.value + GLib.timeout_add(self._timeout, self._check_scroll_cb, adj)