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

master
Gonzalo Odiard 9 years ago
commit 5818721818

@ -21,7 +21,7 @@ treeview.show()
col = Gtk.TreeViewColumn() col = Gtk.TreeViewColumn()
treeview.append_column(col) treeview.append_column(col)
cell_icon = CellRendererIcon(treeview) cell_icon = CellRendererIcon()
cell_icon.props.width = style.GRID_CELL_SIZE cell_icon.props.width = style.GRID_CELL_SIZE
cell_icon.props.height = style.GRID_CELL_SIZE cell_icon.props.height = style.GRID_CELL_SIZE
cell_icon.props.size = style.SMALL_ICON_SIZE cell_icon.props.size = style.SMALL_ICON_SIZE

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

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

@ -825,9 +825,11 @@ class CellRendererIcon(Gtk.CellRenderer):
'clicked': (GObject.SignalFlags.RUN_FIRST, None, [object]), 'clicked': (GObject.SignalFlags.RUN_FIRST, None, [object]),
} }
def __init__(self, tree_view): def __init__(self, treeview=None):
from sugar3.graphics.palette import CellRendererInvoker # 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 = _IconBuffer()
self._buffer.cache = True self._buffer.cache = True
self._xo_color = None self._xo_color = None
@ -836,36 +838,28 @@ class CellRendererIcon(Gtk.CellRenderer):
self._prelit_fill_color = None self._prelit_fill_color = None
self._prelit_stroke_color = None self._prelit_stroke_color = None
self._active_state = False self._active_state = False
self._palette_invoker = CellRendererInvoker()
self._cached_offsets = None self._cached_offsets = None
Gtk.CellRenderer.__init__(self) Gtk.CellRenderer.__init__(self)
tree_view.connect('button-press-event', self._is_scrolling = False
self.__button_press_event_cb)
tree_view.connect('button-release-event',
self.__button_release_event_cb)
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): def _scroll_start_cb(self, event):
self._palette_invoker.detach() self._is_scrolling = True
def __button_press_event_cb(self, widget, event): def _scroll_end_cb(self, event):
if self._point_in_cell_renderer(widget, event.x, event.y): self._is_scrolling = False
self._active_state = True
def __button_release_event_cb(self, widget, event): def is_scrolling(self):
self._active_state = False return self._is_scrolling
def create_palette(self): def create_palette(self):
return None 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): def set_file_name(self, value):
if self._buffer.file_name != value: if self._buffer.file_name != value:
self._buffer.file_name = value self._buffer.file_name = value
@ -964,81 +958,66 @@ class CellRendererIcon(Gtk.CellRenderer):
flags): flags):
pass 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): def do_render(self, cr, widget, background_area, cell_area, flags):
context = widget.get_style_context() if not self._is_scrolling:
context.save()
context.add_class("sugar-icon-cell") context = widget.get_style_context()
context.save()
def is_pointer_inside(): context.add_class("sugar-icon-cell")
# widget is the treeview
x, y = widget.get_pointer() def is_pointer_inside():
x, y = widget.convert_widget_to_bin_window_coords(x, y) # widget is the treeview
return ((cell_area.x <= x <= cell_area.x + cell_area.width) x, y = widget.get_pointer()
and (cell_area.y <= y <= cell_area.y + cell_area.height)) x, y = widget.convert_widget_to_bin_window_coords(x, y)
return ((cell_area.x <= x <= cell_area.x + cell_area.width)
pointer_inside = is_pointer_inside() 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 pointer_inside = is_pointer_inside()
# in this cell only:
if flags & Gtk.CellRendererState.PRELIT: # The context will have prelight state if the mouse pointer is
if pointer_inside: # in the entire row, but we want that state if the pointer is
if self._active_state: # in this cell only:
context.set_state(Gtk.StateFlags.ACTIVE) if flags & Gtk.CellRendererState.PRELIT:
else: if pointer_inside:
context.set_state(Gtk.StateFlags.NORMAL) if self._active_state:
context.set_state(Gtk.StateFlags.ACTIVE)
else:
context.set_state(Gtk.StateFlags.NORMAL)
Gtk.render_background( Gtk.render_background(
context, cr, background_area.x, background_area.y, context, cr, background_area.x, background_area.y,
background_area.width, background_area.height) background_area.width, background_area.height)
if self._xo_color is not None: if self._xo_color is not None:
stroke_color = self._xo_color.get_stroke_color() stroke_color = self._xo_color.get_stroke_color()
fill_color = self._xo_color.get_fill_color() fill_color = self._xo_color.get_fill_color()
prelit_fill_color = None prelit_fill_color = None
prelit_stroke_color = None prelit_stroke_color = None
else: else:
stroke_color = self._stroke_color stroke_color = self._stroke_color
fill_color = self._fill_color fill_color = self._fill_color
prelit_fill_color = self._prelit_fill_color prelit_fill_color = self._prelit_fill_color
prelit_stroke_color = self._prelit_stroke_color prelit_stroke_color = self._prelit_stroke_color
has_prelit_colors = None not in [prelit_fill_color, has_prelit_colors = None not in [prelit_fill_color,
prelit_stroke_color] prelit_stroke_color]
if flags & Gtk.CellRendererState.PRELIT and has_prelit_colors and \ if flags & Gtk.CellRendererState.PRELIT and has_prelit_colors and \
pointer_inside: pointer_inside:
self._buffer.fill_color = prelit_fill_color self._buffer.fill_color = prelit_fill_color
self._buffer.stroke_color = prelit_stroke_color self._buffer.stroke_color = prelit_stroke_color
else:
self._buffer.fill_color = fill_color
self._buffer.stroke_color = stroke_color
else: else:
self._buffer.fill_color = fill_color if self._xo_color is not None:
self._buffer.stroke_color = stroke_color 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() surface = self._buffer.get_surface()
if surface is None: if surface is None:

@ -38,13 +38,14 @@ from sugar3.graphics.palettewindow import PaletteWindow, \
from sugar3.graphics.palettemenu import PaletteMenuItem from sugar3.graphics.palettemenu import PaletteMenuItem
from sugar3.graphics.palettewindow import MouseSpeedDetector, Invoker, \ from sugar3.graphics.palettewindow import MouseSpeedDetector, Invoker, \
WidgetInvoker, CursorInvoker, ToolInvoker, CellRendererInvoker WidgetInvoker, CursorInvoker, ToolInvoker, TreeViewInvoker
assert MouseSpeedDetector assert MouseSpeedDetector
assert Invoker assert Invoker
assert WidgetInvoker assert WidgetInvoker
assert CursorInvoker assert CursorInvoker
assert ToolInvoker assert ToolInvoker
assert CellRendererInvoker assert TreeViewInvoker
class _HeaderItem(Gtk.MenuItem): class _HeaderItem(Gtk.MenuItem):
@ -418,7 +419,8 @@ class Palette(PaletteWindow):
if self._palette_state == self.PRIMARY: if self._palette_state == self.PRIMARY:
self._secondary_box.show() 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: if self._palette_state == self.PRIMARY:
self._secondary_box.hide() self._secondary_box.hide()

@ -35,6 +35,7 @@ from gi.repository import SugarGestures
from sugar3.graphics import palettegroup from sugar3.graphics import palettegroup
from sugar3.graphics import animator from sugar3.graphics import animator
from sugar3.graphics import style from sugar3.graphics import style
from sugar3.graphics.icon import CellRendererIcon
def _calculate_gap(a, b): def _calculate_gap(a, b):
@ -223,12 +224,9 @@ class _PaletteMenuWidget(Gtk.Menu):
x = event.x_root x = event.x_root
y = event.y_root y = event.y_root
if type(self._invoker) is CellRendererInvoker: rect = self._invoker.get_rect()
in_invoker = self._invoker.point_in_cell_renderer(x, y) in_invoker = x >= rect.x and x < (rect.x + rect.width) \
else: 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: if in_invoker != self._mouse_in_invoker:
self._mouse_in_invoker = in_invoker self._mouse_in_invoker = in_invoker
@ -238,12 +236,9 @@ class _PaletteMenuWidget(Gtk.Menu):
x = event.x_root x = event.x_root
y = event.y_root y = event.y_root
if type(self._invoker) is CellRendererInvoker: rect = self._invoker.get_rect()
in_invoker = self._invoker.point_in_cell_renderer(x, y) in_invoker = x >= rect.x and x < (rect.x + rect.width) \
else: 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: if in_invoker:
return True return True
@ -399,6 +394,7 @@ class _PaletteWindowWidget(Gtk.Window):
def popup(self, invoker): def popup(self, invoker):
if self.get_visible(): if self.get_visible():
logging.error('PaletteWindowWidget popup get_visible True')
return return
self.connect('enter-notify-event', self.__enter_notify_event_cb) self.connect('enter-notify-event', self.__enter_notify_event_cb)
self.connect('leave-notify-event', self.__leave_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.') logging.error('Cannot update the palette position.')
return return
if self._widget is None:
return
req = self._widget.size_request() req = self._widget.size_request()
# on Gtk 3.10, menu at the bottom of the screen are resized # on Gtk 3.10, menu at the bottom of the screen are resized
# to not fall out, and report a wrong size. # to not fall out, and report a wrong size.
@ -642,6 +641,8 @@ class PaletteWindow(GObject.GObject):
return self._widget.size_request() return self._widget.size_request()
def popup(self, immediate=False): def popup(self, immediate=False):
if self._widget is None:
return
if self._invoker is not None: if self._invoker is not None:
full_size_request = self.get_full_size_request() full_size_request = self.get_full_size_request()
self._alignment = self._invoker.get_alignment(full_size_request) self._alignment = self._invoker.get_alignment(full_size_request)
@ -1372,28 +1373,34 @@ class ToolInvoker(WidgetInvoker):
self._widget.emit('clicked') self._widget.emit('clicked')
class CellRendererInvoker(Invoker): class TreeViewInvoker(Invoker):
def __init__(self): def __init__(self):
Invoker.__init__(self) Invoker.__init__(self)
self._position_hint = self.AT_CURSOR
self._tree_view = None self._tree_view = None
self._cell_renderer = None
self._motion_hid = None self._motion_hid = None
self._leave_hid = None self._leave_hid = None
self._release_hid = None self._release_hid = None
self._long_pressed_hid = None self._long_pressed_hid = None
self.path = None self._position_hint = self.AT_CURSOR
self._long_pressed_controller = SugarGestures.LongPressController() 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._tree_view = tree_view
self._cell_renderer = cell_renderer
self._motion_hid = tree_view.connect('motion-notify-event', self._motion_hid = tree_view.connect('motion-notify-event',
self.__motion_notify_event_cb) 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_hid = tree_view.connect('leave-notify-event',
self.__leave_notify_event_cb) self.__leave_notify_event_cb)
self._release_hid = tree_view.connect('button-release-event', self._release_hid = tree_view.connect('button-release-event',
@ -1403,129 +1410,104 @@ class CellRendererInvoker(Invoker):
self._long_pressed_controller.attach( self._long_pressed_controller.attach(
tree_view, tree_view,
SugarGestures.EventControllerFlags.NONE) 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): def detach(self):
Invoker.detach(self) Invoker.detach(self)
self._tree_view.disconnect(self._motion_hid) 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._leave_hid)
self._tree_view.disconnect(self._release_hid) self._tree_view.disconnect(self._release_hid)
self._long_pressed_controller.detach(self._tree_view) self._long_pressed_controller.detach(self._tree_view)
self._long_pressed_controller.disconnect(self._long_pressed_hid) self._long_pressed_controller.disconnect(self._long_pressed_hid)
self._mouse_detector.disconnect_by_func(self.__mouse_slow_cb)
def get_rect(self): def get_rect(self):
allocation = self._tree_view.get_allocation() return self._tree_view.get_background_area(self._path, self._column)
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
rect = Gdk.Rectangle() def get_toplevel(self):
rect.x = x + allocation.x return self._tree_view.get_toplevel()
rect.y = y + allocation.y
rect.width = allocation.width def __motion_notify_event_cb(self, widget, event):
rect.height = allocation.height 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: if self.palette is not None:
self.palette.popdown(immediate=True) self.palette.popdown(immediate=True)
self.palette = None self.palette = None
self.path = path
if event.get_source_device().get_source() == \ self._mouse_detector.start()
Gdk.InputSource.TOUCHSCREEN: except TypeError:
return False # tree_view.get_path_at_pos() fail if x,y poition is over
self.notify_mouse_enter() # a empty area
else: pass
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()
def _redraw_path(self, path): def _redraw_cell(self, path, column):
column = None
for column in self._tree_view.get_columns():
if self._cell_renderer in column.get_cells():
break
assert column is not None
area = self._tree_view.get_background_area(path, column) area = self._tree_view.get_background_area(path, column)
x, y = \ x, y = \
self._tree_view.convert_bin_window_to_widget_coords(area.x, area.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) 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): def __leave_notify_event_cb(self, widget, event):
if event.mode == Gdk.CrossingMode.NORMAL: self._mouse_detector.stop()
self.notify_mouse_leave()
return False
def __button_release_event_cb(self, widget, event): def __button_release_event_cb(self, widget, event):
if event.button == 1 and self.point_in_cell_renderer(event.x, x, y = int(event.x), int(event.y)
event.y): path, column, cell_x, cell_y = self._tree_view.get_path_at_pos(x, y)
tree_view = self._tree_view self._path = path
path, column_, x_, y_ = tree_view.get_path_at_pos(int(event.x), self._column = column
int(event.y)) if event.button == 1:
self._cell_renderer.emit('clicked', path) # 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 # So the treeview receives it and knows a drag isn't going on
return False return False
if event.button == 3 and self.point_in_cell_renderer(event.x, if event.button == 3:
event.y): # right mouse button
self._mouse_detector.stop()
self._change_palette()
self.notify_right_click() self.notify_right_click()
return True return True
else: else:
return False return False
def __long_pressed_event_cb(self, controller, x, y, widget): def __long_pressed_event_cb(self, controller, x, y, widget):
if self.point_in_cell_renderer(x, y): path, column, x_, y_ = self._tree_view.get_path_at_pos(x, y)
self.notify_right_click() self._path = path
self._column = column
def point_in_cell_renderer(self, event_x, event_y): self._change_palette()
pos = self._tree_view.get_path_at_pos(int(event_x), int(event_y)) self.notify_right_click()
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
def get_toplevel(self): def __mouse_slow_cb(self, widget):
return self._tree_view.get_toplevel() self._mouse_detector.stop()
self._change_palette()
self.emit('mouse-enter')
def notify_popup(self): def _change_palette(self):
Invoker.notify_popup(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): def notify_popdown(self):
Invoker.notify_popdown(self) Invoker.notify_popdown(self)
self.palette = None self.palette = None
def get_default_position(self):
return self.AT_CURSOR

@ -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)
Loading…
Cancel
Save