Merge branch 'treeview_perf2-try2' of https://github.com/godiard/sugar-toolkit-gtk3 into godiard-treeview_perf2-try2

This commit is contained in:
Gonzalo Odiard 2015-05-15 18:12:58 -03:00
commit 5818721818
7 changed files with 292 additions and 196 deletions

View File

@ -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

View File

@ -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)

View File

@ -18,6 +18,7 @@ sugar_PYTHON = \
panel.py \
radiopalette.py \
radiotoolbutton.py \
scrollingdetector.py \
style.py \
toggletoolbutton.py \
toolbarbox.py \

View File

@ -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")
if not self._is_scrolling:
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))
context = widget.get_style_context()
context.save()
context.add_class("sugar-icon-cell")
pointer_inside = is_pointer_inside()
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))
# 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)
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)
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:
context.set_state(Gtk.StateFlags.NORMAL)
stroke_color = self._stroke_color
fill_color = self._fill_color
prelit_fill_color = self._prelit_fill_color
prelit_stroke_color = self._prelit_stroke_color
Gtk.render_background(
context, cr, background_area.x, background_area.y,
background_area.width, background_area.height)
has_prelit_colors = None not in [prelit_fill_color,
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
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
else:
self._buffer.fill_color = fill_color
self._buffer.stroke_color = stroke_color
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]
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
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:

View File

@ -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()

View File

@ -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
rect.width = allocation.width
rect.height = allocation.height
return rect
def get_toplevel(self):
return self._tree_view.get_toplevel()
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):
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)
self._path = path
self._column = column
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
self._mouse_detector.start()
except TypeError:
# tree_view.get_path_at_pos() fail if x,y poition is over
# a empty area
pass
if event.get_source_device().get_source() == \
Gdk.InputSource.TOUCHSCREEN:
return False
self.notify_mouse_leave()
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()
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 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
def __mouse_slow_cb(self, widget):
self._mouse_detector.stop()
self._change_palette()
self.emit('mouse-enter')
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
def get_toplevel(self):
return self._tree_view.get_toplevel()
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

View File

@ -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)