# Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2009, Aleksey Lim, Sayamindu Dasgupta # # 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. """ import time import gobject import gtk import logging import warnings from sugar.graphics.icon import Icon from sugar.graphics import palettegroup from sugar.logger import trace _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT = 2 class UnfullscreenButton(gtk.Window): def __init__(self): gtk.Window.__init__(self) self.set_decorated(False) self.set_resizable(False) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.set_border_width(0) self.props.accept_focus = False #Setup estimate of width, height w, h = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR) self._width = w self._height = h self.connect('size-request', self._size_request_cb) screen = self.get_screen() screen.connect('size-changed', self._screen_size_changed_cb) self._button = gtk.Button() self._button.set_relief(gtk.RELIEF_NONE) self._icon = Icon(icon_name='view-return', icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) self._icon.show() self._button.add(self._icon) self._button.show() self.add(self._button) def connect_button_press(self, cb): self._button.connect('button-press-event', cb) def _reposition(self): x = gtk.gdk.screen_width() - self._width self.move(x, 0) def _size_request_cb(self, widget, req): self._width = req.width self._height = req.height self._reposition() def _screen_size_changed_cb(self, screen): self._reposition() class Window(gtk.Window): def __init__(self, **args): self._enable_fullscreen_mode = True gtk.Window.__init__(self, **args) self.set_decorated(False) self.maximize() self.connect('realize', self.__window_realize_cb) self.connect('key-press-event', self.__key_press_cb) self._toolbar_box = None self._alerts = [] self._canvas = None self.tray = None self.__vbox = gtk.VBox() self.__hbox = gtk.HBox() self.__vbox.pack_start(self.__hbox) self.__hbox.show() self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.POINTER_MOTION_MASK) self.connect('motion-notify-event', self.__motion_notify_cb) self.add(self.__vbox) self.__vbox.show() self._is_fullscreen = False self._unfullscreen_button = UnfullscreenButton() self._unfullscreen_button.set_transient_for(self) self._unfullscreen_button.connect_button_press( self.__unfullscreen_button_pressed) self._unfullscreen_button_timeout_id = None def reveal(self): """ Make window active In contrast with present(), brings window to the top even after invoking on response on non-gtk events. See #1423. """ if self.window is None: self.show() return timestamp = gtk.get_current_event_time() if not timestamp: timestamp = gtk.gdk.x11_get_server_time(self.window) self.window.focus(timestamp) def fullscreen(self): palettegroup.popdown_all() if self._toolbar_box is not None: self._toolbar_box.hide() if self.tray is not None: self.tray.hide() self._is_fullscreen = True if self.props.enable_fullscreen_mode: self._unfullscreen_button.show() if self._unfullscreen_button_timeout_id is not None: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None self._unfullscreen_button_timeout_id = \ gobject.timeout_add_seconds( \ _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, \ self.__unfullscreen_button_timeout_cb) def unfullscreen(self): if self._toolbar_box is not None: self._toolbar_box.show() if self.tray is not None: self.tray.show() self._is_fullscreen = False if self.props.enable_fullscreen_mode: self._unfullscreen_button.hide() if self._unfullscreen_button_timeout_id: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None def set_canvas(self, canvas): if self._canvas: self.__hbox.remove(self._canvas) if canvas: self.__hbox.pack_start(canvas) self._canvas = canvas self.__vbox.set_focus_child(self._canvas) def get_canvas(self): return self._canvas canvas = property(get_canvas, set_canvas) def get_toolbar_box(self): return self._toolbar_box def set_toolbar_box(self, toolbar_box): if self._toolbar_box: self.__vbox.remove(self._toolbar_box) if toolbar_box: self.__vbox.pack_start(toolbar_box, False) self.__vbox.reorder_child(toolbar_box, 0) self._toolbar_box = toolbar_box toolbar_box = property(get_toolbar_box, set_toolbar_box) def set_tray(self, tray, position): if self.tray: box = self.tray.get_parent() box.remove(self.tray) if position == gtk.POS_LEFT: self.__hbox.pack_start(tray, False) elif position == gtk.POS_RIGHT: self.__hbox.pack_end(tray, False) elif position == gtk.POS_BOTTOM: self.__vbox.pack_end(tray, False) self.tray = tray def add_alert(self, alert): self._alerts.append(alert) if len(self._alerts) == 1: self.__vbox.pack_start(alert, False) if self._toolbar_box is not None: self.__vbox.reorder_child(alert, 1) else: self.__vbox.reorder_child(alert, 0) def remove_alert(self, alert): if alert in self._alerts: self._alerts.remove(alert) # if the alert is the visible one on top of the queue if alert.get_parent() is not None: self.__vbox.remove(alert) if len(self._alerts) >= 1: self.__vbox.pack_start(self._alerts[0], False) if self._toolbar_box is not None: self.__vbox.reorder_child(self._alerts[0], 1) else: self.__vbox.reorder_child(self._alert[0], 0) def __window_realize_cb(self, window): group = gtk.Window() group.realize() window.window.set_group(group.window) def __key_press_cb(self, widget, event): key = gtk.gdk.keyval_name(event.keyval) if event.state & gtk.gdk.MOD1_MASK: if self.tray is not None and key == 'space': self.tray.props.visible = not self.tray.props.visible return True elif key == 'Escape' and self._is_fullscreen and \ self.props.enable_fullscreen_mode: self.unfullscreen() return True return False def __unfullscreen_button_pressed(self, widget, event): self.unfullscreen() def __motion_notify_cb(self, widget, event): if self._is_fullscreen and self.props.enable_fullscreen_mode: if not self._unfullscreen_button.props.visible: self._unfullscreen_button.show() else: # Reset the timer if self._unfullscreen_button_timeout_id is not None: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None self._unfullscreen_button_timeout_id = \ gobject.timeout_add_seconds( \ _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, \ self.__unfullscreen_button_timeout_cb) return False def __unfullscreen_button_timeout_cb(self): self._unfullscreen_button.hide() self._unfullscreen_button_timeout_id = None return False def set_enable_fullscreen_mode(self, enable_fullscreen_mode): self._enable_fullscreen_mode = enable_fullscreen_mode def get_enable_fullscreen_mode(self): return self._enable_fullscreen_mode enable_fullscreen_mode = gobject.property(type=object, setter=set_enable_fullscreen_mode, getter=get_enable_fullscreen_mode) # DEPRECATED def set_toolbox(self, toolbar_box): warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) self.set_toolbar_box(toolbar_box) def get_toolbox(self): warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) return self._toolbar_box toolbox = property(get_toolbox, set_toolbox) class MultiModeWindow(gtk.Window): """ A window that can switch between several independent views (modes) A transition effect may be provided when switching between modes. """ _SCROLL_INTERVAL_MS = 300 _SCROLL_TIME_MS = 2100 _SCROLL_STEPS = _SCROLL_TIME_MS // _SCROLL_INTERVAL_MS def __init__(self, modes, **args): self._enable_fullscreen_mode = True gtk.Window.__init__(self, **args) self.set_decorated(False) self.maximize() self.connect('realize', self.__window_realize_cb) self.connect('key-press-event', self.__key_press_cb) self._modes = modes self._toolbar_boxes = {} self._alerts = dict([(mode, []) for mode in modes]) self._canvasses = {} self._trays = {} self.__vboxes = {} self.__hboxes = {} self.__event_boxes = {} self._current_mode = modes[0] self._next_mode = None self.__layout = gtk.Layout() self.__layout.show() self.__width = 0 self.__height = 0 self.__scroll_step = 0 self.__scroll_direction = 1 self.__scroll_start_time = None self.__scroll_old_snaphot = None self.__scroll_new_snaphot = None for mode in modes: self.__vboxes[mode] = gtk.VBox() self.__hboxes[mode] = gtk.HBox() self.__vboxes[mode].pack_start(self.__hboxes[mode]) self.__hboxes[mode].show() event_box = gtk.EventBox() self.__event_boxes[mode] = event_box self.__hboxes[mode].pack_start(event_box) event_box.show() event_box.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.POINTER_MOTION_MASK) event_box.connect('motion-notify-event', self.__motion_notify_cb) self.__vboxes[mode].show() self.connect('configure-event', self.__configure_cb) self.add(self.__vboxes[self._current_mode]) self._is_fullscreen = False self._unfullscreen_button = UnfullscreenButton() self._unfullscreen_button.set_transient_for(self) self._unfullscreen_button.connect_button_press( self.__unfullscreen_button_pressed) self._unfullscreen_button_timeout_id = None @trace() def switch_to_mode(self, mode): if mode == self._current_mode: return if False: # TODO: config option for disabling scrolling self.remove(self.__vboxes[self._current_mode]) self._current_mode = mode self.add(self.__vboxes[mode]) return if self.child != self.__layout: # work around a race condition on start-up self.remove(self.__vboxes[self._current_mode]) self.__layout.add(self.__vboxes[self._current_mode]) self.add(self.__layout) if self._next_mode is not None: self._switch_mode_now() self._next_mode = mode self.__scroll_step = 0 self.__scroll_start_time = time.time() if self._modes.index(self._current_mode) < self._modes.index(mode): self.__scroll_direction = 1 else: self.__scroll_direction = -1 old_pixmap = self.__vboxes[self._current_mode].get_snapshot() self.__scroll_old_snapshot = gtk.image_new_from_pixmap(old_pixmap, None) # gtk.OffscreenWindow is only available in PyGtk 2.22+, so we need to # resort to a hack for now self.__vboxes[mode].hide() self.__vboxes[mode].connect('expose-event', self._switch_to_mode_cb) self.__layout.put(self.__vboxes[mode], self.__width * self.__scroll_direction, 0) self.__vboxes[mode].resize_children() self.__vboxes[mode].show() # gobject.idle_add(self._switch_to_mode_cb) @trace() def _switch_to_mode_cb(self, *args): new_pixmap = self.__vboxes[self._next_mode].get_snapshot() self.__layout.remove(self.__vboxes[self._next_mode]) self.__scroll_new_snapshot = gtk.image_new_from_pixmap(new_pixmap, None) self.__layout.remove(self.__vboxes[self._current_mode]) self.__layout.add(self.__scroll_old_snapshot) self.__layout.put(self.__scroll_new_snapshot, self.__width * self.__scroll_direction, 0) self.__layout.show_all() gobject.timeout_add(MultiModeWindow._SCROLL_INTERVAL_MS, self.__scroll_cb) @trace() def reveal(self): """ Make window active. In contrast with present(), brings window to the top even after invoking on response on non-gtk events. See SL#1423. """ if self.window is None: self.show() return timestamp = gtk.get_current_event_time() if not timestamp: timestamp = gtk.gdk.x11_get_server_time(self.window) self.window.focus(timestamp) def fullscreen(self): palettegroup.popdown_all() if self._current_mode in self._toolbar_boxes: self._toolbar_boxes[self._current_mode].hide() if self._current_mode in self._trays: self._trays[self._current_mode].hide() self._is_fullscreen = True if self.props.enable_fullscreen_mode: self._unfullscreen_button.show() if self._unfullscreen_button_timeout_id is not None: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None self._unfullscreen_button_timeout_id = \ gobject.timeout_add_seconds( _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, self.__unfullscreen_button_timeout_cb) def unfullscreen(self): if self._current_mode in self._toolbar_boxes: self._toolbar_boxes[self._current_mode].show() if self._current_mode in self._trays: self._trays[self._current_mode].show() self._is_fullscreen = False if self.props.enable_fullscreen_mode: self._unfullscreen_button.hide() if self._unfullscreen_button_timeout_id: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None @trace() def get_canvas(self, mode): return self._canvas.get(mode) @trace() def set_canvas(self, mode, canvas): if mode in self._canvasses: old_canvas = self._canvasses.pop(self._current_mode) self.__event_boxes[mode].remove(old_canvas) if not canvas: return self.__event_boxes[mode].add(canvas) self._canvasses[mode] = canvas self.__vboxes[mode].set_focus_child(canvas) @trace() def get_toolbar_box(self, mode): return self._toolbar_boxes.get(mode) @trace() def set_toolbar_box(self, mode, toolbar_box): if mode in self._toolbar_boxes: self.__vboxes[mode].remove(self._toolbar_boxes.pop(mode)) if not toolbar_box: return self.__vboxes[mode].pack_start(toolbar_box, False) self.__vboxes[mode].reorder_child(toolbar_box, 0) self._toolbar_boxes[mode] = toolbar_box @trace() def get_tray(self, mode): return self._trays.get(mode) @trace() def set_tray(self, mode, tray, position): if mode in self._trays: old_tray = self._trays.pop(mode) box = old_tray.get_parent() box.remove(old_tray) if position == gtk.POS_LEFT: self.__hboxes[mode].pack_start(tray, False) elif position == gtk.POS_RIGHT: self.__hboxes[mode].pack_end(tray, False) elif position == gtk.POS_BOTTOM: self.__vboxes[mode].pack_end(tray, False) self._trays[mode] = tray def add_alert(self, mode, alert): self._alerts[mode].append(alert) if len(self._alerts) > 1: return self.__vboxes[mode].pack_start(alert, False) if mode in self._toolbar_boxes: self.__vboxes[mode].reorder_child(alert, 1) else: self.__vboxes[mode].reorder_child(alert, 0) def remove_alert(self, mode, alert): if alert not in self._alerts[mode]: return self._alerts[mode].remove(alert) if alert.get_parent() is None: return # the alert is the visible one on top of the queue self.__vboxes[mode].remove(alert) if not self._alerts[mode]: return self.__vboxes[mode].pack_start(self._alerts[mode][0], False) if mode in self._toolbar_boxes: self.__vboxes[mode].reorder_child(self._alerts[mode][0], 1) else: self.__vboxes[mode].reorder_child(self._alerts[mode][0], 0) @trace() def __window_realize_cb(self, window): group = gtk.Window() group.realize() window.window.set_group(group.window) def __key_press_cb(self, widget, event): key = gtk.gdk.keyval_name(event.keyval) if event.state & gtk.gdk.MOD1_MASK: if self._current_mode in self._trays and key == 'space': tray = self._trays[self._current_mode] tray.props.visible = not tray.props.visible return True elif key == 'Escape' and self._is_fullscreen and \ self.props.enable_fullscreen_mode: self.unfullscreen() return True return False def __unfullscreen_button_pressed(self, widget, event): self.unfullscreen() def __motion_notify_cb(self, widget, event): if self._is_fullscreen and self.props.enable_fullscreen_mode: if not self._unfullscreen_button.props.visible: self._unfullscreen_button.show() else: # Reset the timer if self._unfullscreen_button_timeout_id is not None: gobject.source_remove(self._unfullscreen_button_timeout_id) self._unfullscreen_button_timeout_id = None self._unfullscreen_button_timeout_id = \ gobject.timeout_add_seconds( _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, self.__unfullscreen_button_timeout_cb) return False def __unfullscreen_button_timeout_cb(self): self._unfullscreen_button.hide() self._unfullscreen_button_timeout_id = None return False def set_enable_fullscreen_mode(self, enable_fullscreen_mode): self._enable_fullscreen_mode = enable_fullscreen_mode def get_enable_fullscreen_mode(self): return self._enable_fullscreen_mode enable_fullscreen_mode = gobject.property(type=object, setter=set_enable_fullscreen_mode, getter=get_enable_fullscreen_mode) @trace() def __configure_cb(self, widget_, event): if self.child != self.__layout: # work around a race condition on start-up self.remove(self.__vboxes[self._current_mode]) self.__layout.add(self.__vboxes[self._current_mode]) self.add(self.__layout) if self.__width == event.width and self.__height == event.height: return self.__width, self.__height = event.width, event.height for vbox in self.__vboxes.values(): vbox.set_size_request(self.__width, self.__height) @trace() def __scroll_cb(self): self.__scroll_step += 1 # skip frames if we're too slow elapsed_time = time.time() - self.__scroll_start_time acceptable_time_per_step = MultiModeWindow._SCROLL_INTERVAL_MS * 1.1 while elapsed_time > self.__scroll_step * acceptable_time_per_step \ and self.__scroll_step < MultiModeWindow._SCROLL_STEPS: logging.debug('MultiModeWindow: Too slow, skipping frame!') self.__scroll_step += 1 if self.__scroll_step >= MultiModeWindow._SCROLL_STEPS: self._switch_mode_now() return False pixel_per_step = self.__width // MultiModeWindow._SCROLL_STEPS scroll_pixels = self.__scroll_step * pixel_per_step #~ if self.__scroll_direction == 1: #~ self.__layout.move(self.__vboxes[self._current_mode], #~ -scroll_pixels, 0) #~ self.__layout.move(self.__vboxes[self._next_mode], #~ self.__width - scroll_pixels, 0) #~ else: #~ self.__layout.move(self.__vboxes[self._current_mode], #~ scroll_pixels, 0) #~ self.__layout.move(self.__vboxes[self._next_mode], #~ scroll_pixels - self.__width, 0) if self.__scroll_direction == 1: self.__layout.move(self.__scroll_old_snapshot, -scroll_pixels, 0) self.__layout.move(self.__scroll_new_snapshot, self.__width - scroll_pixels, 0) else: self.__layout.move(self.__scroll_old_snapshot, scroll_pixels, 0) self.__layout.move(self.__scroll_new_snapshot, scroll_pixels - self.__width, 0) return True @trace() def _switch_mode_now(self): # self.__layout.remove(self.__vboxes[self._current_mode]) self.__layout.add(self.__vboxes[self._next_mode]) self.__layout.remove(self.__scroll_old_snapshot) self.__layout.remove(self.__scroll_new_snapshot) self._current_mode = self._next_mode self._next_mode = None