
In Gtk+ 3.20, you need to use the css name to select elements, rather than the gtype name. Therefore, these must be added. The css name must be set before the class instances are created, as it effects the class rather than the instance. This is why it must be places after the class definition.
514 lines
17 KiB
Python
514 lines
17 KiB
Python
# Copyright (C) 2007, One Laptop Per Child
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
STABLE.
|
|
"""
|
|
|
|
from gi.repository import GObject
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
|
|
from sugar3.graphics import style
|
|
from sugar3.graphics.palette import ToolInvoker
|
|
from sugar3.graphics.toolbutton import ToolButton
|
|
from sugar3.graphics.icon import Icon
|
|
|
|
|
|
_PREVIOUS_PAGE = 0
|
|
_NEXT_PAGE = 1
|
|
|
|
|
|
class _TrayViewport(Gtk.Viewport):
|
|
|
|
__gproperties__ = {
|
|
'scrollable': (bool, None, None, False, GObject.PARAM_READABLE),
|
|
'can-scroll-prev': (bool, None, None, False, GObject.PARAM_READABLE),
|
|
'can-scroll-next': (bool, None, None, False, GObject.PARAM_READABLE),
|
|
}
|
|
|
|
def __init__(self, orientation):
|
|
self.orientation = orientation
|
|
self._scrollable = False
|
|
self._can_scroll_next = False
|
|
self._can_scroll_prev = False
|
|
|
|
Gtk.Viewport.__init__(self)
|
|
|
|
self.set_shadow_type(Gtk.ShadowType.NONE)
|
|
|
|
self.traybar = Gtk.Toolbar()
|
|
self.traybar.set_orientation(orientation)
|
|
self.traybar.set_show_arrow(False)
|
|
self.add(self.traybar)
|
|
self.traybar.show()
|
|
|
|
self.connect('size_allocate', self._size_allocate_cb)
|
|
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
adj = self.get_hadjustment()
|
|
else:
|
|
adj = self.get_vadjustment()
|
|
adj.connect('changed', self._adjustment_changed_cb)
|
|
adj.connect('value-changed', self._adjustment_changed_cb)
|
|
|
|
def scroll(self, direction):
|
|
if direction == _PREVIOUS_PAGE:
|
|
self._scroll_previous()
|
|
elif direction == _NEXT_PAGE:
|
|
self._scroll_next()
|
|
|
|
def scroll_to_item(self, item):
|
|
"""This function scrolls the viewport so that item will be visible."""
|
|
assert item in self.traybar.get_children()
|
|
|
|
# Get the allocation, and make sure that it is visible
|
|
allocation = item.get_allocation()
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
adj = self.get_hadjustment()
|
|
start = allocation.x
|
|
stop = allocation.x + allocation.width
|
|
else:
|
|
adj = self.get_vadjustment()
|
|
start = allocation.y
|
|
stop = allocation.y + allocation.height
|
|
|
|
if start < adj.get_value():
|
|
adj.set_value(start)
|
|
elif stop > adj.get_value() + adj.get_page_size():
|
|
adj.set_value(stop - adj.get_page_size())
|
|
|
|
def _scroll_next(self):
|
|
allocation = self.get_allocation()
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
adj = self.get_hadjustment()
|
|
new_value = adj.get_value() + allocation.width
|
|
adj.set_value(min(new_value, adj.get_upper() - allocation.width))
|
|
else:
|
|
adj = self.get_vadjustment()
|
|
new_value = adj.get_value() + allocation.height
|
|
adj.set_value(min(new_value, adj.get_upper() - allocation.height))
|
|
|
|
def _scroll_previous(self):
|
|
allocation = self.get_allocation()
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
adj = self.get_hadjustment()
|
|
new_value = adj.get_value() - allocation.width
|
|
adj.set_value(max(adj.get_lower(), new_value))
|
|
else:
|
|
adj = self.get_vadjustment()
|
|
new_value = adj.get_value() - allocation.height
|
|
adj.set_value(max(adj.get_lower(), new_value))
|
|
|
|
def do_get_preferred_width(self):
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
min_width, nat_width = Gtk.Viewport.do_get_preferred_width(self)
|
|
return 0, nat_width
|
|
child_minimum, child_natural = self.get_child().get_preferred_size()
|
|
return child_minimum.width, child_natural.width
|
|
|
|
def do_get_preferred_height(self):
|
|
if self.orientation != Gtk.Orientation.HORIZONTAL:
|
|
min_height, nat_height = Gtk.Viewport.do_get_preferred_height(self)
|
|
return 0, nat_height
|
|
child_minimum, child_natural = self.get_child().get_preferred_size()
|
|
return child_minimum.height, child_natural.height
|
|
|
|
def do_get_property(self, pspec):
|
|
if pspec.name == 'scrollable':
|
|
return self._scrollable
|
|
elif pspec.name == 'can-scroll-next':
|
|
return self._can_scroll_next
|
|
elif pspec.name == 'can-scroll-prev':
|
|
return self._can_scroll_prev
|
|
|
|
def _size_allocate_cb(self, viewport, allocation):
|
|
if allocation.width == 1 and allocation.height == 1:
|
|
# HACK: the first time this callback is called 'width' and
|
|
# 'height' are 1 so we mark the Viewport as scrollable and
|
|
# we show the Prev / Next buttons
|
|
return
|
|
|
|
bar_minimum, bar_natural = self.traybar.get_preferred_size()
|
|
|
|
if self.orientation == Gtk.Orientation.HORIZONTAL:
|
|
scrollable = bar_minimum.width > allocation.width
|
|
else:
|
|
scrollable = bar_minimum.height > allocation.height
|
|
|
|
if scrollable != self._scrollable:
|
|
self._scrollable = scrollable
|
|
self.notify('scrollable')
|
|
|
|
def _adjustment_changed_cb(self, adjustment):
|
|
if adjustment.get_value() <= adjustment.get_lower():
|
|
can_scroll_prev = False
|
|
else:
|
|
can_scroll_prev = True
|
|
|
|
if adjustment.get_value() + adjustment.get_page_size() >= \
|
|
adjustment.get_upper():
|
|
can_scroll_next = False
|
|
else:
|
|
can_scroll_next = True
|
|
|
|
if can_scroll_prev != self._can_scroll_prev:
|
|
self._can_scroll_prev = can_scroll_prev
|
|
self.notify('can-scroll-prev')
|
|
|
|
if can_scroll_next != self._can_scroll_next:
|
|
self._can_scroll_next = can_scroll_next
|
|
self.notify('can-scroll-next')
|
|
|
|
|
|
class _TrayScrollButton(ToolButton):
|
|
|
|
__gtype_name__ = 'SugarTrayScrollButton'
|
|
|
|
def __init__(self, icon_name, scroll_direction):
|
|
ToolButton.__init__(self)
|
|
self._viewport = None
|
|
|
|
self._scroll_direction = scroll_direction
|
|
|
|
self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
|
|
|
|
self.icon = Icon(icon_name=icon_name,
|
|
pixel_size=style.SMALL_ICON_SIZE)
|
|
# The alignment is a hack to work around Gtk.ToolButton code
|
|
# that sets the icon_size when the icon_widget is a Gtk.Image
|
|
alignment = Gtk.Alignment(xalign=0.5, yalign=0.5)
|
|
alignment.add(self.icon)
|
|
self.set_icon_widget(alignment)
|
|
alignment.show_all()
|
|
|
|
self.connect('clicked', self._clicked_cb)
|
|
|
|
def set_viewport(self, viewport):
|
|
self._viewport = viewport
|
|
self._viewport.connect('notify::scrollable',
|
|
self._viewport_scrollable_changed_cb)
|
|
|
|
if self._scroll_direction == _PREVIOUS_PAGE:
|
|
self._viewport.connect('notify::can-scroll-prev',
|
|
self._viewport_can_scroll_dir_changed_cb)
|
|
self.set_sensitive(self._viewport.props.can_scroll_prev)
|
|
else:
|
|
self._viewport.connect('notify::can-scroll-next',
|
|
self._viewport_can_scroll_dir_changed_cb)
|
|
self.set_sensitive(self._viewport.props.can_scroll_next)
|
|
|
|
def _viewport_scrollable_changed_cb(self, viewport, pspec):
|
|
self.props.visible = self._viewport.props.scrollable
|
|
|
|
def _viewport_can_scroll_dir_changed_cb(self, viewport, pspec):
|
|
if self._scroll_direction == _PREVIOUS_PAGE:
|
|
sensitive = self._viewport.props.can_scroll_prev
|
|
else:
|
|
sensitive = self._viewport.props.can_scroll_next
|
|
|
|
self.set_sensitive(sensitive)
|
|
|
|
def _clicked_cb(self, button):
|
|
self._viewport.scroll(self._scroll_direction)
|
|
|
|
viewport = property(fset=set_viewport)
|
|
|
|
|
|
ALIGN_TO_START = 0
|
|
ALIGN_TO_END = 1
|
|
|
|
|
|
class HTray(Gtk.EventBox):
|
|
|
|
__gtype_name__ = 'SugarHTray'
|
|
|
|
__gproperties__ = {
|
|
'align': (int, None, None, 0, 1, ALIGN_TO_START,
|
|
GObject.PARAM_READWRITE |
|
|
GObject.PARAM_CONSTRUCT_ONLY),
|
|
'drag-active': (bool, None, None, False, GObject.PARAM_READWRITE),
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
self._drag_active = False
|
|
self.align = ALIGN_TO_START
|
|
|
|
Gtk.EventBox.__init__(self, **kwargs)
|
|
|
|
self._box = Gtk.HBox()
|
|
self.add(self._box)
|
|
self._box.show()
|
|
|
|
scroll_left = _TrayScrollButton('go-left', _PREVIOUS_PAGE)
|
|
self._box.pack_start(scroll_left, False, False, 0)
|
|
|
|
self._viewport = _TrayViewport(Gtk.Orientation.HORIZONTAL)
|
|
self._box.pack_start(self._viewport, True, True, 0)
|
|
self._viewport.show()
|
|
|
|
scroll_right = _TrayScrollButton('go-right', _NEXT_PAGE)
|
|
self._box.pack_start(scroll_right, False, False, 0)
|
|
|
|
scroll_left.viewport = self._viewport
|
|
scroll_right.viewport = self._viewport
|
|
|
|
if self.align == ALIGN_TO_END:
|
|
spacer = Gtk.SeparatorToolItem()
|
|
spacer.set_size_request(0, 0)
|
|
spacer.props.draw = False
|
|
spacer.set_expand(True)
|
|
self._viewport.traybar.insert(spacer, 0)
|
|
spacer.show()
|
|
|
|
def do_set_property(self, pspec, value):
|
|
if pspec.name == 'align':
|
|
self.align = value
|
|
elif pspec.name == 'drag-active':
|
|
self._set_drag_active(value)
|
|
else:
|
|
raise AssertionError
|
|
|
|
def do_get_property(self, pspec):
|
|
if pspec.name == 'align':
|
|
return self.align
|
|
elif pspec.name == 'drag-active':
|
|
return self._drag_active
|
|
else:
|
|
raise AssertionError
|
|
|
|
def _set_drag_active(self, active):
|
|
if self._drag_active != active:
|
|
self._drag_active = active
|
|
if self._drag_active:
|
|
self._viewport.traybar.modify_bg(
|
|
Gtk.StateType.NORMAL,
|
|
style.COLOR_BLACK.get_gdk_color())
|
|
else:
|
|
self._viewport.traybar.modify_bg(Gtk.StateType.NORMAL, None)
|
|
|
|
def get_children(self):
|
|
children = self._viewport.traybar.get_children()[:]
|
|
if self.align == ALIGN_TO_END:
|
|
children = children[1:]
|
|
return children
|
|
|
|
def add_item(self, item, index=-1):
|
|
if self.align == ALIGN_TO_END and index > -1:
|
|
index += 1
|
|
self._viewport.traybar.insert(item, index)
|
|
|
|
def remove_item(self, item):
|
|
self._viewport.traybar.remove(item)
|
|
|
|
def get_item_index(self, item):
|
|
index = self._viewport.traybar.get_item_index(item)
|
|
if self.align == ALIGN_TO_END:
|
|
index -= 1
|
|
return index
|
|
|
|
def scroll_to_item(self, item):
|
|
self._viewport.scroll_to_item(item)
|
|
if hasattr(HTray, 'set_css_name'):
|
|
HTray.set_css_name('htray')
|
|
|
|
|
|
class VTray(Gtk.EventBox):
|
|
|
|
__gtype_name__ = 'SugarVTray'
|
|
|
|
__gproperties__ = {
|
|
'align': (int, None, None, 0, 1, ALIGN_TO_START,
|
|
GObject.PARAM_READWRITE | GObject.PARAM_CONSTRUCT_ONLY),
|
|
'drag-active': (bool, None, None, False, GObject.PARAM_READWRITE),
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
self._drag_active = False
|
|
self.align = ALIGN_TO_START
|
|
|
|
Gtk.EventBox.__init__(self, **kwargs)
|
|
|
|
self._box = Gtk.VBox()
|
|
self.add(self._box)
|
|
self._box.show()
|
|
|
|
scroll_up = _TrayScrollButton('go-up', _PREVIOUS_PAGE)
|
|
self._box.pack_start(scroll_up, False, False, 0)
|
|
|
|
self._viewport = _TrayViewport(Gtk.Orientation.VERTICAL)
|
|
self._box.pack_start(self._viewport, True, True, 0)
|
|
self._viewport.show()
|
|
|
|
scroll_down = _TrayScrollButton('go-down', _NEXT_PAGE)
|
|
self._box.pack_start(scroll_down, False, False, 0)
|
|
|
|
scroll_up.viewport = self._viewport
|
|
scroll_down.viewport = self._viewport
|
|
|
|
if self.align == ALIGN_TO_END:
|
|
spacer = Gtk.SeparatorToolItem()
|
|
spacer.set_size_request(0, 0)
|
|
spacer.props.draw = False
|
|
spacer.set_expand(True)
|
|
self._viewport.traybar.insert(spacer, 0)
|
|
spacer.show()
|
|
|
|
def do_set_property(self, pspec, value):
|
|
if pspec.name == 'align':
|
|
self.align = value
|
|
elif pspec.name == 'drag-active':
|
|
self._set_drag_active(value)
|
|
else:
|
|
raise AssertionError
|
|
|
|
def do_get_property(self, pspec):
|
|
if pspec.name == 'align':
|
|
return self.align
|
|
elif pspec.name == 'drag-active':
|
|
return self._drag_active
|
|
else:
|
|
raise AssertionError
|
|
|
|
def _set_drag_active(self, active):
|
|
if self._drag_active != active:
|
|
self._drag_active = active
|
|
if self._drag_active:
|
|
self._viewport.traybar.modify_bg(
|
|
Gtk.StateType.NORMAL,
|
|
style.COLOR_BLACK.get_gdk_color())
|
|
else:
|
|
self._viewport.traybar.modify_bg(Gtk.StateType.NORMAL, None)
|
|
|
|
def get_children(self):
|
|
children = self._viewport.traybar.get_children()[:]
|
|
if self.align == ALIGN_TO_END:
|
|
children = children[1:]
|
|
return children
|
|
|
|
def add_item(self, item, index=-1):
|
|
if self.align == ALIGN_TO_END and index > -1:
|
|
index += 1
|
|
self._viewport.traybar.insert(item, index)
|
|
|
|
def remove_item(self, item):
|
|
self._viewport.traybar.remove(item)
|
|
|
|
def get_item_index(self, item):
|
|
index = self._viewport.traybar.get_item_index(item)
|
|
if self.align == ALIGN_TO_END:
|
|
index -= 1
|
|
return index
|
|
|
|
def scroll_to_item(self, item):
|
|
self._viewport.scroll_to_item(item)
|
|
if hasattr(VTray, 'set_css_name'):
|
|
VTray.set_css_name('VTray')
|
|
|
|
|
|
class TrayButton(ToolButton):
|
|
|
|
def __init__(self, **kwargs):
|
|
ToolButton.__init__(self, **kwargs)
|
|
|
|
|
|
class _IconWidget(Gtk.EventBox):
|
|
|
|
__gtype_name__ = 'SugarTrayIconWidget'
|
|
|
|
def __init__(self, icon_name=None, xo_color=None):
|
|
Gtk.EventBox.__init__(self)
|
|
|
|
self.set_app_paintable(True)
|
|
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
|
|
Gdk.EventMask.TOUCH_MASK |
|
|
Gdk.EventMask.BUTTON_RELEASE_MASK)
|
|
|
|
self._icon = Icon(icon_name=icon_name, xo_color=xo_color,
|
|
pixel_size=style.STANDARD_ICON_SIZE)
|
|
self.add(self._icon)
|
|
self._icon.show()
|
|
|
|
def do_draw(self, cr):
|
|
palette = self.get_parent().palette
|
|
|
|
if palette and palette.is_up():
|
|
allocation = self.get_allocation()
|
|
# draw a black background, has been done by the engine before
|
|
cr.set_source_rgb(0, 0, 0)
|
|
cr.rectangle(0, 0, allocation.width, allocation.height)
|
|
cr.paint()
|
|
|
|
Gtk.EventBox.do_draw(self, cr)
|
|
|
|
if palette and palette.is_up():
|
|
invoker = palette.props.invoker
|
|
invoker.draw_rectangle(cr, palette)
|
|
|
|
return False
|
|
|
|
def get_icon(self):
|
|
return self._icon
|
|
|
|
|
|
class TrayIcon(Gtk.ToolItem):
|
|
|
|
__gtype_name__ = 'SugarTrayIcon'
|
|
|
|
def __init__(self, icon_name=None, xo_color=None):
|
|
Gtk.ToolItem.__init__(self)
|
|
|
|
self._icon_widget = _IconWidget(icon_name, xo_color)
|
|
self.add(self._icon_widget)
|
|
self._icon_widget.show()
|
|
|
|
self._palette_invoker = ToolInvoker(self)
|
|
|
|
self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
|
|
|
|
self.connect('destroy', self.__destroy_cb)
|
|
|
|
def __destroy_cb(self, icon):
|
|
if self._palette_invoker is not None:
|
|
self._palette_invoker.detach()
|
|
|
|
def create_palette(self):
|
|
return None
|
|
|
|
def get_palette(self):
|
|
return self._palette_invoker.palette
|
|
|
|
def set_palette(self, palette):
|
|
self._palette_invoker.palette = palette
|
|
|
|
palette = GObject.property(
|
|
type=object, setter=set_palette, getter=get_palette)
|
|
|
|
def get_palette_invoker(self):
|
|
return self._palette_invoker
|
|
|
|
def set_palette_invoker(self, palette_invoker):
|
|
self._palette_invoker.detach()
|
|
self._palette_invoker = palette_invoker
|
|
|
|
palette_invoker = GObject.property(
|
|
type=object, setter=set_palette_invoker, getter=get_palette_invoker)
|
|
|
|
def get_icon(self):
|
|
return self._icon_widget.get_icon()
|
|
icon = property(get_icon, None)
|