From ae5ce06ccb1f604fa1e4eaeb16d9ba8122b4923d Mon Sep 17 00:00:00 2001 From: Marco Pesenti Gritti Date: Mon, 04 Feb 2008 22:36:12 +0000 Subject: Refactor directory structure a bit, preliminary to the library split-out. --- (limited to 'src/view/frame') diff --git a/src/view/frame/Makefile.am b/src/view/frame/Makefile.am new file mode 100644 index 0000000..02951b9 --- /dev/null +++ b/src/view/frame/Makefile.am @@ -0,0 +1,14 @@ +sugardir = $(pkgdatadir)/shell/view/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + activitybutton.py \ + clipboardbox.py \ + clipboardpanelwindow.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + overlaybox.py \ + framewindow.py \ + zoomtoolbar.py diff --git a/src/view/frame/__init__.py b/src/view/frame/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/frame/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/view/frame/activitiestray.py b/src/view/frame/activitiestray.py new file mode 100644 index 0000000..3dbf955 --- /dev/null +++ b/src/view/frame/activitiestray.py @@ -0,0 +1,156 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo +import logging + +from sugar.graphics.tray import TrayButton +from sugar.graphics.tray import HTray +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar import profile +from sugar import activity +from sugar import env + +from activitybutton import ActivityButton + +class InviteButton(TrayButton): + def __init__(self, activity_model, invite): + TrayButton.__init__(self) + + icon = Icon(file=activity_model.get_icon_name(), + xo_color=activity_model.get_color()) + self.set_icon_widget(icon) + icon.show() + + self._invite = invite + + def get_activity_id(self): + return self._invite.get_activity_id() + + def get_bundle_id(self): + return self._invite.get_bundle_id() + + def get_invite(self): + return self._invite + +class ActivitiesTray(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_HORIZONTAL) + + self._shell = shell + self._shell_model = self._shell.get_model() + self._invite_to_item = {} + self._invites = self._shell_model.get_invites() + self._config = self._load_config() + + self._tray = HTray() + self.append(hippo.CanvasWidget(widget=self._tray), hippo.PACK_EXPAND) + self._tray.show() + + registry = activity.get_registry() + registry.get_activities_async(reply_handler=self._get_activities_cb) + + registry.connect('activity-added', self._activity_added_cb) + registry.connect('activity-removed', self._activity_removed_cb) + + for invite in self._invites: + self.add_invite(invite) + self._invites.connect('invite-added', self._invite_added_cb) + self._invites.connect('invite-removed', self._invite_removed_cb) + + def _load_config(self): + config = [] + + f = open(env.get_data_path('activities.defaults'), 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + config.append(line) + f.close() + + return config + + def _get_activities_cb(self, activity_list): + known_activities = [] + unknown_activities = [] + name_to_activity = {} + + while activity_list: + info = activity_list.pop() + name_to_activity[info.bundle_id] = info + + if info.bundle_id in self._config: + known_activities.append(info) + else: + unknown_activities.append(info) + + sorted_activities = [] + for name in self._config: + if name in name_to_activity: + sorted_activities.append(name_to_activity[name]) + + for info in sorted_activities + unknown_activities: + if info.show_launcher: + self.add_activity(info) + + def _activity_clicked_cb(self, icon): + self._shell.start_activity(icon.get_bundle_id()) + + def _invite_clicked_cb(self, icon): + self._invites.remove_invite(icon.get_invite()) + self._shell.join_activity(icon.get_bundle_id(), + icon.get_activity_id()) + + def _invite_added_cb(self, invites, invite): + self.add_invite(invite) + + def _invite_removed_cb(self, invites, invite): + self.remove_invite(invite) + + def _remove_activity_cb(self, item): + self._tray.remove_item(item) + + def _activity_added_cb(self, activity_registry, activity_info): + self.add_activity(activity_info) + + def _activity_removed_cb(self, activity_registry, activity_info): + for item in self._tray.get_children(): + if item.get_bundle_id() == activity_info.bundle_id: + self._tray.remove_item(item) + return + + def add_activity(self, activity_info): + item = ActivityButton(activity_info) + item.connect('clicked', self._activity_clicked_cb) + item.connect('remove_activity', self._remove_activity_cb) + self._tray.add_item(item, -1) + item.show() + + def add_invite(self, invite): + mesh = self._shell_model.get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + if activity: + item = InviteButton(activity_model, invite) + item.connect('clicked', self._invite_clicked_cb) + self._tray.add_item(item, 0) + item.show() + + self._invite_to_item[invite] = item + + def remove_invite(self, invite): + self._tray.remove_item(self._invite_to_item[invite]) + del self._invite_to_item[invite] diff --git a/src/view/frame/activitybutton.py b/src/view/frame/activitybutton.py new file mode 100644 index 0000000..0c7c7fb --- /dev/null +++ b/src/view/frame/activitybutton.py @@ -0,0 +1,65 @@ +# 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. + +import gtk +import os +import gobject +from gettext import gettext as _ + +from sugar.graphics.palette import Palette +from sugar.graphics.tray import TrayButton +from sugar.graphics.icon import Icon +from sugar.graphics import style + +from view.frame.frameinvoker import FrameWidgetInvoker + +class ActivityButton(TrayButton, gobject.GObject): + __gtype_name__ = 'SugarActivityButton' + __gsignals__ = { + 'remove_activity': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, activity_info): + TrayButton.__init__(self) + + icon = Icon(file=activity_info.icon, + stroke_color=style.COLOR_WHITE.get_svg(), + fill_color=style.COLOR_TRANSPARENT.get_svg()) + self.set_icon_widget(icon) + icon.show() + + self._activity_info = activity_info + self.setup_rollover_options() + + def get_bundle_id(self): + return self._activity_info.bundle_id + + def setup_rollover_options(self): + palette = Palette(self._activity_info.name) + self.set_palette(palette) + palette.props.invoker = FrameWidgetInvoker(self) + +#TODO: Disabled this until later, see #4967 +# if os.path.dirname(self._activity_info.path) == os.path.expanduser('~/Activities'): +# menu_item = gtk.MenuItem(_('Remove')) +# menu_item.connect('activate', self.item_remove_cb) +# palette.menu.append(menu_item) +# menu_item.show() + + def item_remove_cb(self, widget): + self.emit('remove_activity') diff --git a/src/view/frame/clipboardbox.py b/src/view/frame/clipboardbox.py new file mode 100644 index 0000000..7702759 --- /dev/null +++ b/src/view/frame/clipboardbox.py @@ -0,0 +1,193 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import tempfile + +import hippo +import gtk + +from sugar import util +from sugar.clipboard import clipboardservice +from sugar.graphics.tray import VTray +from sugar.graphics import style + +from view.clipboardicon import ClipboardIcon + +class _ContextMap: + """Maps a drag context to the clipboard object involved in the dragging.""" + def __init__(self): + self._context_map = {} + + def add_context(self, context, object_id, data_types): + """Establishes the mapping. data_types will serve us for reference- + counting this mapping. + """ + self._context_map[context] = [object_id, data_types] + + def get_object_id(self, context): + """Retrieves the object_id associated with context. + Will release the association when this function was called as many times + as the number of data_types that this clipboard object contains. + """ + [object_id, data_types_left] = self._context_map[context] + + data_types_left = data_types_left - 1 + if data_types_left == 0: + del self._context_map[context] + else: + self._context_map[context] = [object_id, data_types_left] + + return object_id + + def has_context(self, context): + return context in self._context_map + +class ClipboardBox(hippo.CanvasBox): + + MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2 + + def __init__(self): + hippo.CanvasBox.__init__(self) + self._icons = {} + self._context_map = _ContextMap() + + self._tray = VTray() + self.append(hippo.CanvasWidget(widget=self._tray), hippo.PACK_EXPAND) + self._tray.show() + + cb_service = clipboardservice.get_instance() + cb_service.connect('object-added', self._object_added_cb) + cb_service.connect('object-deleted', self._object_deleted_cb) + + def owns_clipboard(self): + for icon in self._icons.values(): + if icon.owns_clipboard: + return True + return False + + def _add_selection(self, object_id, selection): + if not selection.data: + return + + logging.debug('ClipboardBox: adding type ' + selection.type) + + cb_service = clipboardservice.get_instance() + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list still not supported.') + + cb_service.add_object_format(object_id, + selection.type, + uris[0], + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) + + def _object_added_cb(self, cb_service, object_id, name): + if self._icons: + group = self._icons.values()[0] + else: + group = None + + icon = ClipboardIcon(object_id, name, group) + self._tray.add_item(icon, 0) + icon.show() + self._icons[object_id] = icon + + objects_to_delete = self._tray.get_children()[ClipboardBox.MAX_ITEMS:] + for icon in objects_to_delete: + logging.debug('ClipboardBox: deleting surplus object') + cb_service = clipboardservice.get_instance() + cb_service.delete_object(icon.get_object_id()) + + logging.debug('ClipboardBox: ' + object_id + ' was added.') + + def _object_deleted_cb(self, cb_service, object_id): + icon = self._icons[object_id] + self._tray.remove_item(icon) + del self._icons[object_id] + logging.debug('ClipboardBox: ' + object_id + ' was deleted.') + + def drag_motion_cb(self, widget, context, x, y, time): + logging.debug('ClipboardBox._drag_motion_cb') + context.drag_status(gtk.gdk.ACTION_COPY, time) + return True; + + def drag_drop_cb(self, widget, context, x, y, time): + logging.debug('ClipboardBox._drag_drop_cb') + cb_service = clipboardservice.get_instance() + object_id = cb_service.add_object(name="") + + self._context_map.add_context(context, object_id, len(context.targets)) + + if 'XdndDirectSave0' in context.targets: + window = context.source_window + prop_type, format, filename = \ + window.property_get('XdndDirectSave0','text/plain') + + # FIXME query the clipboard service for a filename? + base_dir = tempfile.gettempdir() + dest_filename = util.unique_id() + + name, dot, extension = filename.rpartition('.') + dest_filename += dot + extension + + dest_uri = 'file://' + os.path.join(base_dir, dest_filename) + + window.property_change('XdndDirectSave0', prop_type, format, + gtk.gdk.PROP_MODE_REPLACE, dest_uri) + + widget.drag_get_data(context, 'XdndDirectSave0', time) + else: + for target in context.targets: + if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): + widget.drag_get_data(context, target, time) + + cb_service.set_object_percent(object_id, percent=100) + + return True + + def drag_data_received_cb(self, widget, context, x, y, selection, targetType, time): + logging.debug('ClipboardBox: got data for target %r' % selection.target) + + object_id = self._context_map.get_object_id(context) + try: + if selection is None: + logging.warn('ClipboardBox: empty selection for target ' + selection.target) + elif selection.target == 'XdndDirectSave0': + if selection.data == 'S': + window = context.source_window + + prop_type, format, dest = \ + window.property_get('XdndDirectSave0','text/plain') + + clipboard = clipboardservice.get_instance() + clipboard.add_object_format( + object_id, 'XdndDirectSave0', dest, on_disk=True) + else: + self._add_selection(object_id, selection) + + finally: + # If it's the last target to be processed, finish the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) + diff --git a/src/view/frame/clipboardpanelwindow.py b/src/view/frame/clipboardpanelwindow.py new file mode 100644 index 0000000..e579b8c --- /dev/null +++ b/src/view/frame/clipboardpanelwindow.py @@ -0,0 +1,99 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import urlparse + +import gtk +import hippo + +from view.frame.framewindow import FrameWindow +from view.frame.clipboardbox import ClipboardBox +from sugar.clipboard import clipboardservice +from sugar import util + +class ClipboardPanelWindow(FrameWindow): + def __init__(self, frame, orientation): + FrameWindow.__init__(self, orientation) + + self._frame = frame + + # Listening for new clipboard objects + # NOTE: we need to keep a reference to gtk.Clipboard in order to keep + # listening to it. + self._clipboard = gtk.Clipboard() + self._clipboard.connect("owner-change", self._owner_change_cb) + + self._clipboard_box = ClipboardBox() + self.append(self._clipboard_box, hippo.PACK_EXPAND) + + # Receiving dnd drops + self.drag_dest_set(0, [], 0) + self.connect("drag_motion", self._clipboard_box.drag_motion_cb) + self.connect("drag_drop", self._clipboard_box.drag_drop_cb) + self.connect("drag_data_received", + self._clipboard_box.drag_data_received_cb) + + def _owner_change_cb(self, clipboard, event): + logging.debug("owner_change_cb") + + if self._clipboard_box.owns_clipboard(): + return + + cb_service = clipboardservice.get_instance() + key = cb_service.add_object(name="") + cb_service.set_object_percent(key, percent=0) + + targets = clipboard.wait_for_targets() + for target in targets: + if target not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE', 'SAVE_TARGETS'): + logging.debug('Asking for target %s.' % target) + selection = clipboard.wait_for_contents(target) + if not selection: + logging.warning('no data for selection target %s.' % target) + continue + self._add_selection(key, selection) + + cb_service.set_object_percent(key, percent=100) + + def _add_selection(self, key, selection): + if not selection.data: + logging.warning('no data for selection target %s.' % selection.type) + return + + logging.debug('adding type ' + selection.type + '.') + + cb_service = clipboardservice.get_instance() + if selection.type == 'text/uri-list': + uris = selection.get_uris() + + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list still not supported.') + uri = uris[0] + + scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(uri) + on_disk = (scheme == 'file') + + cb_service.add_object_format(key, + selection.type, + uri, + on_disk) + else: + cb_service.add_object_format(key, + selection.type, + selection.data, + on_disk=False) + diff --git a/src/view/frame/eventarea.py b/src/view/frame/eventarea.py new file mode 100644 index 0000000..69bb759 --- /dev/null +++ b/src/view/frame/eventarea.py @@ -0,0 +1,106 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import wnck + +class EventArea(gobject.GObject): + __gsignals__ = { + 'enter': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._windows = [] + self._hover = False + + right = gtk.gdk.screen_width() - 1 + bottom = gtk.gdk.screen_height() -1 + + invisible = self._create_invisible(0, 0, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 0, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(0, bottom, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(right, bottom, 1, 1) + self._windows.append(invisible) + + screen = wnck.screen_get_default() + screen.connect('window-stacking-changed', + self._window_stacking_changed_cb) + + def _create_invisible(self, x, y, width, height): + invisible = gtk.Invisible() + invisible.connect('enter-notify-event', self._enter_notify_cb) + invisible.connect('leave-notify-event', self._leave_notify_cb) + + invisible.drag_dest_set(0, [], 0) + invisible.connect('drag_motion', self._drag_motion_cb) + invisible.connect('drag_leave', self._drag_leave_cb) + + invisible.realize() + invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + invisible.window.move_resize(x, y, width, height) + + return invisible + + def _notify_enter(self): + if not self._hover: + self._hover = True + self.emit('enter') + + def _notify_leave(self): + if self._hover: + self._hover = False + self.emit('leave') + + def _enter_notify_cb(self, widget, event): + self._notify_enter() + + def _leave_notify_cb(self, widget, event): + self._notify_leave() + + def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): + drag_context.drag_status(0, timestamp); + self._notify_enter() + return True + + def _drag_leave_cb(self, widget, drag_context, timestamp): + self._notify_leave() + return True + + def show(self): + for window in self._windows: + window.show() + + def hide(self): + for window in self._windows: + window.hide() + + def _window_stacking_changed_cb(self, screen): + for window in self._windows: + window.window.raise_() diff --git a/src/view/frame/frame.py b/src/view/frame/frame.py new file mode 100644 index 0000000..e8f8fa4 --- /dev/null +++ b/src/view/frame/frame.py @@ -0,0 +1,272 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gtk +import gobject +import hippo + +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics import palettegroup +from sugar.clipboard import clipboardservice + +from view.frame.eventarea import EventArea +from view.frame.activitiestray import ActivitiesTray +from view.frame.zoomtoolbar import ZoomToolbar +from view.frame.friendstray import FriendsTray +from view.frame.framewindow import FrameWindow +from view.frame.clipboardpanelwindow import ClipboardPanelWindow +from model.shellmodel import ShellModel + +_FRAME_HIDING_DELAY = 500 + +class _Animation(animator.Animation): + def __init__(self, frame, end): + start = frame.current_position + animator.Animation.__init__(self, start, end) + self._frame = frame + + def next_frame(self, current): + self._frame.move(current) + +class _MouseListener(object): + def __init__(self, frame): + self._frame = frame + self._hide_sid = 0 + + def mouse_enter(self): + self._show_frame() + + def mouse_leave(self): + if self._frame.mode == Frame.MODE_MOUSE: + self._hide_frame() + + def _show_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._frame.show(Frame.MODE_MOUSE) + + def _hide_frame_timeout_cb(self): + self._frame.hide() + return False + + def _hide_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._hide_sid = gobject.timeout_add( + _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + +class _KeyListener(object): + def __init__(self, frame): + self._frame = frame + + def key_press(self): + if self._frame.visible: + if self._frame.mode == Frame.MODE_KEYBOARD: + self._frame.hide() + else: + self._frame.show(Frame.MODE_KEYBOARD) + +class Frame(object): + MODE_MOUSE = 0 + MODE_KEYBOARD = 1 + MODE_NON_INTERACTIVE = 2 + + def __init__(self, shell): + self.mode = None + + self._palette_group = palettegroup.get_group('frame') + self._palette_group.connect('popdown', self._palette_group_popdown_cb) + + self._left_panel = None + self._right_panel = None + self._top_panel = None + self._bottom_panel = None + + self._shell = shell + self.current_position = 0.0 + self._animator = None + + self._event_area = EventArea() + self._event_area.connect('enter', self._enter_corner_cb) + self._event_area.show() + + self._top_panel = self._create_top_panel() + self._bottom_panel = self._create_bottom_panel() + self._left_panel = self._create_left_panel() + self._right_panel = self._create_right_panel() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + cb_service = clipboardservice.get_instance() + cb_service.connect_after('object-added', self._clipboard_object_added_cb) + + self._key_listener = _KeyListener(self) + self._mouse_listener = _MouseListener(self) + + self.move(1.0) + + def is_visible(self): + return self.current_position != 0.0 + + def hide(self): + if self._animator: + self._animator.stop() + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 0.0)) + self._animator.start() + + self._event_area.show() + + self.mode = None + + def show(self, mode): + if self.visible: + return + if self._animator: + self._animator.stop() + + self._shell.take_activity_screenshot() + + self.mode = mode + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 1.0)) + self._animator.start() + + self._event_area.hide() + + def move(self, pos): + self.current_position = pos + self._update_position() + + def _is_hover(self): + return (self._top_panel.hover or \ + self._bottom_panel.hover or \ + self._left_panel.hover or \ + self._right_panel.hover) + + def _create_top_panel(self): + panel = self._create_panel(gtk.POS_TOP) + + toolbar = ZoomToolbar(self._shell) + panel.append(hippo.CanvasWidget(widget=toolbar)) + toolbar.show() + + return panel + + def _create_bottom_panel(self): + panel = self._create_panel(gtk.POS_BOTTOM) + + box = ActivitiesTray(self._shell) + panel.append(box, hippo.PACK_EXPAND) + + return panel + + def _create_right_panel(self): + panel = self._create_panel(gtk.POS_RIGHT) + + tray = FriendsTray(self._shell) + panel.append(hippo.CanvasWidget(widget=tray), hippo.PACK_EXPAND) + tray.show() + + return panel + + def _create_left_panel(self): + panel = ClipboardPanelWindow(self, gtk.POS_LEFT) + + self._connect_to_panel(panel) + panel.connect('drag-motion', self._drag_motion_cb) + panel.connect('drag-leave', self._drag_leave_cb) + + return panel + + def _create_panel(self, orientation): + panel = FrameWindow(orientation) + self._connect_to_panel(panel) + + return panel + + def _move_panel(self, panel, pos, x1, y1, x2, y2): + x = (x2 - x1) * pos + x1 + y = (y2 - y1) * pos + y1 + + panel.move(int(x), int(y)) + + # FIXME we should hide and show as necessary to free memory + if not panel.props.visible: + panel.show() + + def _connect_to_panel(self, panel): + panel.connect('enter-notify-event', self._enter_notify_cb) + panel.connect('leave-notify-event', self._leave_notify_cb) + + def _update_position(self): + screen_h = gtk.gdk.screen_height() + screen_w = gtk.gdk.screen_width() + + self._move_panel(self._top_panel, self.current_position, + 0, - self._top_panel.size, 0, 0) + + self._move_panel(self._bottom_panel, self.current_position, + 0, screen_h, 0, screen_h - self._bottom_panel.size) + + self._move_panel(self._left_panel, self.current_position, + - self._left_panel.size, 0, 0, 0) + + self._move_panel(self._right_panel, self.current_position, + screen_w, 0, screen_w - self._right_panel.size, 0) + + def _size_changed_cb(self, screen): + self._update_position() + + def _clipboard_object_added_cb(self, cb_service, object_id, name): + if not self.visible: + self.show(self.MODE_NON_INTERACTIVE) + gobject.timeout_add(2000, lambda: self.hide()) + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._mouse_listener.mouse_enter() + + def _leave_notify_cb(self, window, event): + if event.detail == gtk.gdk.NOTIFY_INFERIOR: + return + + if not self._is_hover() and not self._palette_group.is_up(): + self._mouse_listener.mouse_leave() + + def _palette_group_popdown_cb(self, group): + if not self._is_hover(): + self._mouse_listener.mouse_leave() + + def _drag_motion_cb(self, window, context, x, y, time): + self._mouse_listener.mouse_enter() + + def _drag_leave_cb(self, window, drag_context, timestamp): + self._mouse_listener.mouse_leave() + + def _enter_corner_cb(self, event_area): + self._mouse_listener.mouse_enter() + + def notify_key_press(self): + self._key_listener.key_press() + + visible = property(is_visible, None) diff --git a/src/view/frame/frameinvoker.py b/src/view/frame/frameinvoker.py new file mode 100644 index 0000000..07dc9d8 --- /dev/null +++ b/src/view/frame/frameinvoker.py @@ -0,0 +1,39 @@ +# Copyright (C) 2007, Eduardo Silva +# +# 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. + +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.palette import CanvasInvoker +from sugar.graphics.palette import WidgetInvoker + +def _get_screen_area(): + frame_thickness = style.GRID_CELL_SIZE + + x = y = frame_thickness + width = gtk.gdk.screen_width() - frame_thickness + height = gtk.gdk.screen_height() - frame_thickness + + return gtk.gdk.Rectangle(x, y, width, height) + +class FrameWidgetInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget.child) + + self._position_hint = self.ANCHORED + self._screen_area = _get_screen_area() diff --git a/src/view/frame/framewindow.py b/src/view/frame/framewindow.py new file mode 100644 index 0000000..623d162 --- /dev/null +++ b/src/view/frame/framewindow.py @@ -0,0 +1,104 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import hippo + +from sugar.graphics import style + +class FrameWindow(gtk.Window): + __gtype_name__ = 'SugarFrameWindow' + + def __init__(self, position): + gtk.Window.__init__(self) + self.hover = False + self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH + + self._position = position + + self.set_decorated(False) + self.connect('realize', self._realize_cb) + self.connect('enter-notify-event', self._enter_notify_cb) + self.connect('leave-notify-event', self._leave_notify_cb) + + self._canvas = hippo.Canvas() + self.add(self._canvas) + self._canvas.show() + + box = hippo.CanvasBox() + self._canvas.set_root(box) + + padding = style.GRID_CELL_SIZE + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + box.props.orientation = hippo.ORIENTATION_HORIZONTAL + box.props.padding_left = padding + box.props.padding_right = padding + box.props.padding_top = 0 + box.props.padding_bottom = 0 + else: + box.props.orientation = hippo.ORIENTATION_VERTICAL + box.props.padding_left = 0 + box.props.padding_right = 0 + box.props.padding_top = padding + box.props.padding_bottom = padding + + self._bg = hippo.CanvasBox( + border_color=style.COLOR_BUTTON_GREY.get_int()) + + border = style.LINE_WIDTH + if position == gtk.POS_TOP: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_bottom = border + elif position == gtk.POS_BOTTOM: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_top = border + elif position == gtk.POS_LEFT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_right = border + elif position == gtk.POS_RIGHT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_left = border + + box.append(self._bg, hippo.PACK_EXPAND) + + self._update_size() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + def append(self, child, flags=0): + self._bg.append(child, flags) + + def _update_size(self): + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + self.resize(gtk.gdk.screen_width(), self.size) + else: + self.resize(self.size, gtk.gdk.screen_height()) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = True + + def _leave_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = False + + def _size_changed_cb(self, screen): + self._update_size() diff --git a/src/view/frame/friendstray.py b/src/view/frame/friendstray.py new file mode 100644 index 0000000..b34f357 --- /dev/null +++ b/src/view/frame/friendstray.py @@ -0,0 +1,142 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo + +from sugar.presence import presenceservice +from sugar.graphics.tray import VTray, TrayIcon + +from view.BuddyMenu import BuddyMenu +from view.frame.frameinvoker import FrameWidgetInvoker +from model.BuddyModel import BuddyModel + +class FriendIcon(TrayIcon): + def __init__(self, shell, buddy): + TrayIcon.__init__(self, icon_name='computer-xo', + xo_color=buddy.get_color()) + + palette = BuddyMenu(shell, buddy) + self.set_palette(palette) + palette.set_group_id('frame') + palette.props.invoker = FrameWidgetInvoker(self) + +class FriendsTray(VTray): + def __init__(self, shell): + VTray.__init__(self) + + self._shell = shell + self._activity_ps = None + self._joined_hid = -1 + self._left_hid = -1 + self._buddies = {} + + self._pservice = presenceservice.get_instance() + self._pservice.connect('activity-appeared', + self.__activity_appeared_cb) + + self._owner = self._pservice.get_owner() + + # Add initial activities the PS knows about + self._pservice.get_activities_async(reply_handler=self._get_activities_cb) + + home_model = shell.get_model().get_home() + home_model.connect('pending-activity-changed', + self._pending_activity_changed_cb) + + def _get_activities_cb(self, list): + for activity in list: + self.__activity_appeared_cb(self._pservice, activity) + + def add_buddy(self, buddy): + if self._buddies.has_key(buddy.props.key): + return + + model = BuddyModel(buddy=buddy) + + icon = FriendIcon(self._shell, model) + self.add_item(icon) + icon.show() + + self._buddies[buddy.props.key] = icon + + def remove_buddy(self, buddy): + if not self._buddies.has_key(buddy.props.key): + return + + self.remove_item(self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def clear(self): + for item in self.get_children(): + self.remove_item(item) + self._buddies = {} + + def __activity_appeared_cb(self, pservice, activity_ps): + activity = self._shell.get_current_activity() + if activity and activity_ps.props.id == activity.get_id(): + self._set_activity_ps(activity_ps, True) + + def _set_activity_ps(self, activity_ps, shared_activity): + if self._activity_ps == activity_ps: + return + + if self._joined_hid > 0: + self._activity_ps.disconnect(self._joined_hid) + self._joined_hid = -1 + if self._left_hid > 0: + self._activity_ps.disconnect(self._left_hid) + self._left_hid = -1 + + self._activity_ps = activity_ps + + self.clear() + + if shared_activity is True: + for buddy in activity_ps.get_joined_buddies(): + self.add_buddy(buddy) + + self._joined_hid = activity_ps.connect( + 'buddy-joined', self.__buddy_joined_cb) + self._left_hid = activity_ps.connect( + 'buddy-left', self.__buddy_left_cb) + else: + # only display myself if not shared + self.add_buddy(self._owner) + + def _pending_activity_changed_cb(self, home_model, home_activity): + if home_activity is None: + return + + activity_id = home_activity.get_activity_id() + if activity_id is None: + return + + # check if activity is shared + activity = None + for act in self._pservice.get_activities(): + if activity_id == act.props.id: + activity = act + break + if activity: + self._set_activity_ps(activity, True) + else: + self._set_activity_ps(home_activity, False) + + def __buddy_joined_cb(self, activity, buddy): + self.add_buddy(buddy) + + def __buddy_left_cb(self, activity, buddy): + self.remove_buddy(buddy) diff --git a/src/view/frame/overlaybox.py b/src/view/frame/overlaybox.py new file mode 100644 index 0000000..bb74f18 --- /dev/null +++ b/src/view/frame/overlaybox.py @@ -0,0 +1,32 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo + +from sugar.graphics.iconbutton import IconButton + +class OverlayBox(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_HORIZONTAL) + + self._shell = shell + + icon = IconButton(icon_name='stock-chat') + icon.connect('activated', self._overlay_clicked_cb) + self.append(icon) + + def _overlay_clicked_cb(self, item): + self._shell.toggle_chat_visibility() diff --git a/src/view/frame/zoomtoolbar.py b/src/view/frame/zoomtoolbar.py new file mode 100644 index 0000000..48e63de --- /dev/null +++ b/src/view/frame/zoomtoolbar.py @@ -0,0 +1,84 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.toolbutton import ToolButton + +from view.frame.frameinvoker import FrameWidgetInvoker +from model.shellmodel import ShellModel + +class ZoomToolbar(gtk.Toolbar): + def __init__(self, shell): + gtk.Toolbar.__init__(self) + + self._shell = shell + + self.set_show_arrow(False) + + button = ToolButton(icon_name='zoom-neighborhood') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_MESH) + self.insert(button, -1) + button.show() + + palette = Palette(_('Neighborhood')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-groups') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_FRIENDS) + self.insert(button, -1) + button.show() + + palette = Palette(_('Group')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-home') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_HOME) + self.insert(button, -1) + button.show() + + palette = Palette(_('Home')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-activity') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_ACTIVITY) + self.insert(button, -1) + button.show() + + palette = Palette(_('Activity')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + def _level_clicked_cb(self, button, level): + self._shell.set_zoom_level(level) -- cgit v0.9.1