Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/jarabe
diff options
context:
space:
mode:
authorMarco Pesenti Gritti <mpgritti@gmail.com>2008-09-28 10:27:47 (GMT)
committer Marco Pesenti Gritti <mpgritti@gmail.com>2008-09-28 10:27:47 (GMT)
commit091adf5ee3ae797507328ab72225133039dfdebb (patch)
tree27400fde442e59914b2227fa3dbee54e997591c9 /src/jarabe
parent28c225bcbbc76f666bbdcd6c8f31232fc2720947 (diff)
Move the shell code into site-packages.
Diffstat (limited to 'src/jarabe')
-rw-r--r--src/jarabe/.gitignore1
-rw-r--r--src/jarabe/Makefile.am1
-rw-r--r--src/jarabe/__init__.py26
-rw-r--r--src/jarabe/config.py.in23
-rw-r--r--src/jarabe/controlpanel/Makefile.am12
-rw-r--r--src/jarabe/controlpanel/__init__.py16
-rw-r--r--src/jarabe/controlpanel/aboutme/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/aboutme/__init__.py25
-rw-r--r--src/jarabe/controlpanel/aboutme/model.py116
-rw-r--r--src/jarabe/controlpanel/aboutme/view.py226
-rw-r--r--src/jarabe/controlpanel/aboutxo/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/aboutxo/__init__.py22
-rw-r--r--src/jarabe/controlpanel/aboutxo/model.py101
-rw-r--r--src/jarabe/controlpanel/aboutxo/view.py193
-rw-r--r--src/jarabe/controlpanel/cmd.py148
-rw-r--r--src/jarabe/controlpanel/datetime/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/datetime/__init__.py21
-rw-r--r--src/jarabe/controlpanel/datetime/model.py94
-rw-r--r--src/jarabe/controlpanel/datetime/view.py138
-rw-r--r--src/jarabe/controlpanel/frame/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/frame/__init__.py21
-rw-r--r--src/jarabe/controlpanel/frame/model.py64
-rw-r--r--src/jarabe/controlpanel/frame/view.py232
-rw-r--r--src/jarabe/controlpanel/gui.py414
-rw-r--r--src/jarabe/controlpanel/inlinealert.py83
-rw-r--r--src/jarabe/controlpanel/language/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/language/__init__.py22
-rw-r--r--src/jarabe/controlpanel/language/model.py135
-rw-r--r--src/jarabe/controlpanel/language/view.py142
-rw-r--r--src/jarabe/controlpanel/network/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/network/__init__.py25
-rw-r--r--src/jarabe/controlpanel/network/model.py101
-rw-r--r--src/jarabe/controlpanel/network/view.py231
-rw-r--r--src/jarabe/controlpanel/power/Makefile.am6
-rw-r--r--src/jarabe/controlpanel/power/__init__.py23
-rw-r--r--src/jarabe/controlpanel/power/model.py89
-rw-r--r--src/jarabe/controlpanel/power/view.py177
-rw-r--r--src/jarabe/controlpanel/sectionview.py55
-rw-r--r--src/jarabe/controlpanel/toolbar.py157
-rwxr-xr-xsrc/jarabe/emulator.py134
-rw-r--r--src/jarabe/hardware/Makefile.am13
-rw-r--r--src/jarabe/hardware/NetworkManagerInfo.conf26
-rw-r--r--src/jarabe/hardware/__init__.py16
-rw-r--r--src/jarabe/hardware/hardwaremanager.py123
-rw-r--r--src/jarabe/hardware/keydialog.py354
-rw-r--r--src/jarabe/hardware/nmclient.py759
-rw-r--r--src/jarabe/hardware/nminfo.py557
-rw-r--r--src/jarabe/hardware/schoolserver.py54
-rw-r--r--src/jarabe/intro/Makefile.am9
-rw-r--r--src/jarabe/intro/__init__.py0
-rw-r--r--src/jarabe/intro/colorpicker.py43
-rw-r--r--src/jarabe/intro/default-picture.pngbin0 -> 10442 bytes
-rw-r--r--src/jarabe/intro/window.py278
-rw-r--r--src/jarabe/journal/Makefile.am18
-rw-r--r--src/jarabe/journal/__init__.py0
-rw-r--r--src/jarabe/journal/collapsedentry.py385
-rw-r--r--src/jarabe/journal/detailview.py133
-rw-r--r--src/jarabe/journal/expandedentry.py385
-rw-r--r--src/jarabe/journal/journalactivity.py338
-rw-r--r--src/jarabe/journal/journalentrybundle.py96
-rw-r--r--src/jarabe/journal/journaltoolbox.py418
-rw-r--r--src/jarabe/journal/keepicon.py57
-rw-r--r--src/jarabe/journal/listview.py460
-rw-r--r--src/jarabe/journal/misc.py188
-rw-r--r--src/jarabe/journal/modalalert.py93
-rw-r--r--src/jarabe/journal/objectchooser.py199
-rw-r--r--src/jarabe/journal/palettes.py115
-rw-r--r--src/jarabe/journal/query.py266
-rw-r--r--src/jarabe/journal/volumesmanager.py315
-rw-r--r--src/jarabe/journal/volumestoolbar.py137
-rw-r--r--src/jarabe/logsmanager.py55
-rw-r--r--src/jarabe/main.py158
-rw-r--r--src/jarabe/model/BuddyModel.py171
-rw-r--r--src/jarabe/model/Friends.py114
-rw-r--r--src/jarabe/model/Invites.py123
-rw-r--r--src/jarabe/model/Makefile.am16
-rw-r--r--src/jarabe/model/MeshModel.py237
-rw-r--r--src/jarabe/model/Owner.py104
-rw-r--r--src/jarabe/model/__init__.py16
-rw-r--r--src/jarabe/model/accesspointmodel.py80
-rw-r--r--src/jarabe/model/clipboard.py149
-rw-r--r--src/jarabe/model/clipboardobject.py117
-rw-r--r--src/jarabe/model/devices/Makefile.am10
-rw-r--r--src/jarabe/model/devices/__init__.py16
-rw-r--r--src/jarabe/model/devices/battery.py97
-rw-r--r--src/jarabe/model/devices/device.py45
-rw-r--r--src/jarabe/model/devices/devicesmodel.py144
-rw-r--r--src/jarabe/model/devices/network/Makefile.am6
-rw-r--r--src/jarabe/model/devices/network/__init__.py16
-rw-r--r--src/jarabe/model/devices/network/mesh.py74
-rw-r--r--src/jarabe/model/devices/network/wired.py28
-rw-r--r--src/jarabe/model/devices/network/wireless.py95
-rw-r--r--src/jarabe/model/devices/speaker.py65
-rw-r--r--src/jarabe/model/homeactivity.py245
-rw-r--r--src/jarabe/model/homemodel.py286
-rw-r--r--src/jarabe/model/shellmodel.py104
-rw-r--r--src/jarabe/session.py82
-rw-r--r--src/jarabe/shellservice.py135
-rw-r--r--src/jarabe/uicheck.py148
-rw-r--r--src/jarabe/view/ActivityHost.py63
-rw-r--r--src/jarabe/view/BuddyIcon.py60
-rw-r--r--src/jarabe/view/BuddyMenu.py121
-rw-r--r--src/jarabe/view/Makefile.am17
-rw-r--r--src/jarabe/view/OverlayWindow.py70
-rw-r--r--src/jarabe/view/Shell.py270
-rw-r--r--src/jarabe/view/__init__.py16
-rw-r--r--src/jarabe/view/clipboardicon.py142
-rw-r--r--src/jarabe/view/clipboardmenu.py238
-rw-r--r--src/jarabe/view/devices/Makefile.am9
-rw-r--r--src/jarabe/view/devices/__init__.py16
-rw-r--r--src/jarabe/view/devices/battery.py130
-rw-r--r--src/jarabe/view/devices/deviceview.py26
-rw-r--r--src/jarabe/view/devices/network/Makefile.am5
-rw-r--r--src/jarabe/view/devices/network/__init__.py16
-rw-r--r--src/jarabe/view/devices/network/mesh.py131
-rw-r--r--src/jarabe/view/devices/network/wireless.py144
-rw-r--r--src/jarabe/view/devices/speaker.py160
-rw-r--r--src/jarabe/view/frame/Makefile.am14
-rw-r--r--src/jarabe/view/frame/__init__.py16
-rw-r--r--src/jarabe/view/frame/activitiestray.py395
-rw-r--r--src/jarabe/view/frame/clipboardpanelwindow.py102
-rw-r--r--src/jarabe/view/frame/clipboardtray.py193
-rw-r--r--src/jarabe/view/frame/devicestray.py66
-rw-r--r--src/jarabe/view/frame/eventarea.py148
-rw-r--r--src/jarabe/view/frame/frame.py327
-rw-r--r--src/jarabe/view/frame/frameinvoker.py36
-rw-r--r--src/jarabe/view/frame/framewindow.py104
-rw-r--r--src/jarabe/view/frame/friendstray.py144
-rw-r--r--src/jarabe/view/frame/notification.py100
-rw-r--r--src/jarabe/view/frame/zoomtoolbar.py88
-rw-r--r--src/jarabe/view/home/FriendView.py87
-rw-r--r--src/jarabe/view/home/FriendsBox.py89
-rw-r--r--src/jarabe/view/home/HomeBox.py367
-rw-r--r--src/jarabe/view/home/HomeWindow.py153
-rw-r--r--src/jarabe/view/home/Makefile.am17
-rw-r--r--src/jarabe/view/home/MeshBox.py646
-rw-r--r--src/jarabe/view/home/MyIcon.py24
-rw-r--r--src/jarabe/view/home/__init__.py16
-rw-r--r--src/jarabe/view/home/activitieslist.py302
-rw-r--r--src/jarabe/view/home/favoriteslayout.py485
-rw-r--r--src/jarabe/view/home/favoritesview.py456
-rw-r--r--src/jarabe/view/home/grid.py201
-rwxr-xr-xsrc/jarabe/view/home/proc_smaps.py107
-rw-r--r--src/jarabe/view/home/snowflakelayout.py108
-rw-r--r--src/jarabe/view/home/spreadlayout.py83
-rw-r--r--src/jarabe/view/home/transitionbox.py97
-rw-r--r--src/jarabe/view/keyhandler.py289
-rw-r--r--src/jarabe/view/launchwindow.py113
-rw-r--r--src/jarabe/view/palettes.py217
-rw-r--r--src/jarabe/view/pulsingicon.py223
-rw-r--r--src/jarabe/view/tabbinghandler.py146
151 files changed, 19674 insertions, 0 deletions
diff --git a/src/jarabe/.gitignore b/src/jarabe/.gitignore
new file mode 100644
index 0000000..4acd06b
--- /dev/null
+++ b/src/jarabe/.gitignore
@@ -0,0 +1 @@
+config.py
diff --git a/src/jarabe/Makefile.am b/src/jarabe/Makefile.am
new file mode 100644
index 0000000..83571a4
--- /dev/null
+++ b/src/jarabe/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = jarabe
diff --git a/src/jarabe/__init__.py b/src/jarabe/__init__.py
new file mode 100644
index 0000000..41b4b1c
--- /dev/null
+++ b/src/jarabe/__init__.py
@@ -0,0 +1,26 @@
+"""OLPC Sugar Graphical "Shell" Interface
+
+Provides the shell-level operations for managing
+the OLPC laptop computers. It interacts heavily
+with (and depends upon) the Sugar UI libraries.
+
+This is a "graphical" shell, the name does not
+refer to a command-line "shell" interface.
+"""
+
+# 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/jarabe/config.py.in b/src/jarabe/config.py.in
new file mode 100644
index 0000000..7469b00
--- /dev/null
+++ b/src/jarabe/config.py.in
@@ -0,0 +1,23 @@
+# Copyright (C) 2008 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
+
+# pylint: disable-msg=C0301
+
+prefix = '@prefix@'
+data_path = '@prefix@/share/sugar/data'
+shell_path = '@prefix@/share/sugar/shell'
+version = '@SUCROSE_VERSION@'
+
diff --git a/src/jarabe/controlpanel/Makefile.am b/src/jarabe/controlpanel/Makefile.am
new file mode 100644
index 0000000..7d17b4f
--- /dev/null
+++ b/src/jarabe/controlpanel/Makefile.am
@@ -0,0 +1,12 @@
+SUBDIRS = aboutme aboutxo datetime frame language network power
+
+sugardir = $(pythondir)/jarabe/controlpanel
+sugar_PYTHON = \
+ __init__.py \
+ cmd.py \
+ gui.py \
+ inlinealert.py \
+ sectionview.py \
+ toolbar.py
+
+
diff --git a/src/jarabe/controlpanel/__init__.py b/src/jarabe/controlpanel/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/controlpanel/__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/jarabe/controlpanel/aboutme/Makefile.am b/src/jarabe/controlpanel/aboutme/Makefile.am
new file mode 100644
index 0000000..da77593
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutme/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/aboutme
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/aboutme/__init__.py b/src/jarabe/controlpanel/aboutme/__init__.py
new file mode 100644
index 0000000..b683e28
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutme/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+from sugar import profile
+
+CLASS = 'AboutMe'
+ICON = 'module-about_me'
+TITLE = _('About Me')
+COLOR = profile.get_color()
+
+
diff --git a/src/jarabe/controlpanel/aboutme/model.py b/src/jarabe/controlpanel/aboutme/model.py
new file mode 100644
index 0000000..3818792
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutme/model.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2008 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
+#
+
+from gettext import gettext as _
+
+from sugar import profile
+from sugar.graphics.xocolor import XoColor
+
+_COLORS = {'red': {'dark':'#b20008', 'medium':'#e6000a', 'light':'#ffadce'},
+ 'orange': {'dark':'#9a5200', 'medium':'#c97e00', 'light':'#ffc169'},
+ 'yellow': {'dark':'#807500', 'medium':'#be9e00', 'light':'#fffa00'},
+ 'green': {'dark':'#008009', 'medium':'#00b20d', 'light':'#8bff7a'},
+ 'blue': {'dark':'#00588c', 'medium':'#005fe4', 'light':'#bccdff'},
+ 'purple': {'dark':'#5e008c', 'medium':'#7f00bf', 'light':'#d1a3ff'}
+ }
+
+_MODIFIERS = ('dark', 'medium', 'light')
+
+def get_nick():
+ return profile.get_nick_name()
+
+def print_nick():
+ print get_nick()
+
+def set_nick(nick):
+ """Set the nickname.
+ nick : e.g. 'walter'
+ """
+ if not nick:
+ raise ValueError(_("You must enter a name."))
+ pro = profile.get_profile()
+ if not isinstance(nick, unicode):
+ nick = unicode(nick, 'utf-8')
+ pro.nick_name = nick
+ pro.save()
+ return 1
+
+def get_color():
+ return profile.get_color()
+
+def print_color():
+ color_string = get_color().to_string()
+ tmp = color_string.split(',')
+
+ stroke_tuple = None
+ fill_tuple = None
+ for color in _COLORS:
+ for hue in _COLORS[color]:
+ if _COLORS[color][hue] == tmp[0]:
+ stroke_tuple = (color, hue)
+ if _COLORS[color][hue] == tmp[1]:
+ fill_tuple = (color, hue)
+
+ if stroke_tuple is not None:
+ print _('stroke: color=%s hue=%s') % (stroke_tuple[0],
+ stroke_tuple[1])
+ else:
+ print _('stroke: %s') % (tmp[0])
+ if fill_tuple is not None:
+ print _('fill: color=%s hue=%s') % (fill_tuple[0], fill_tuple[1])
+ else:
+ print _('fill: %s') % (tmp[1])
+
+def set_color(stroke, fill, stroke_modifier='medium', fill_modifier='medium'):
+ """Set the system color by setting a fill and stroke color.
+ fill : [red, orange, yellow, blue, green, purple]
+ stroke : [red, orange, yellow, blue, green, purple]
+ hue stroke : [dark, medium, light] (optional)
+ hue fill : [dark, medium, light] (optional)
+ """
+
+ if stroke_modifier not in _MODIFIERS or fill_modifier not in _MODIFIERS:
+ print (_("Error in specified color modifiers."))
+ return
+ if stroke not in _COLORS or fill not in _COLORS:
+ print (_("Error in specified colors."))
+ return
+
+ if stroke_modifier == fill_modifier:
+ if fill_modifier == 'medium':
+ fill_modifier = 'light'
+ else:
+ fill_modifier = 'medium'
+
+ color = _COLORS[stroke][stroke_modifier] + ',' \
+ + _COLORS[fill][fill_modifier]
+ pro = profile.get_profile()
+ pro.color = XoColor(color)
+ pro.save()
+ return 1
+
+def get_color_xo():
+ return profile.get_color()
+
+def set_color_xo(color):
+ """Set a color with an XoColor
+ This method is used by the graphical user interface
+ """
+ pro = profile.get_profile()
+ pro.color = color
+ pro.save()
+ return 1
diff --git a/src/jarabe/controlpanel/aboutme/view.py b/src/jarabe/controlpanel/aboutme/view.py
new file mode 100644
index 0000000..fc4f351
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutme/view.py
@@ -0,0 +1,226 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+from sugar import profile
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+CLASS = 'AboutMe'
+ICON = 'module-about_me'
+COLOR = profile.get_color()
+TITLE = _('About Me')
+
+class EventIcon(gtk.EventBox):
+ __gtype_name__ = "SugarEventIcon"
+ def __init__(self, **kwargs):
+ gtk.EventBox.__init__(self)
+
+ self.icon = Icon(pixel_size = style.XLARGE_ICON_SIZE, **kwargs)
+
+ self.set_visible_window(False)
+ self.set_app_paintable(True)
+ self.set_events(gtk.gdk.BUTTON_PRESS_MASK)
+
+ self.add(self.icon)
+ self.icon.show()
+
+class ColorPicker(EventIcon):
+ __gsignals__ = {
+ 'color-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+ def __init__(self, xocolor=None):
+ EventIcon.__init__(self)
+ self.icon.props.xo_color = xocolor
+ self.icon.props.icon_name = 'computer-xo'
+ self.icon.props.pixel_size = style.XLARGE_ICON_SIZE
+ self.connect('button_press_event', self.__pressed_cb)
+
+ def __pressed_cb(self, button, event):
+ self._set_random_colors()
+
+ def _set_random_colors(self):
+ xocolor = XoColor()
+ self.icon.props.xo_color = xocolor
+ self.emit('color-changed', xocolor)
+
+class AboutMe(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self.restart_alerts = alerts
+ self._nick_sid = 0
+ self._color_valid = True
+ self._nick_valid = True
+ self._color_change_handler = None
+ self._nick_change_handler = None
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+ self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ self._nick_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._nick_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._nick_entry = None
+ self._nick_alert = None
+ self._setup_nick()
+
+ self._color_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._color_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._color_picker = None
+ self._color_alert = None
+ self._setup_color()
+
+ self.setup()
+
+ def _setup_nick(self):
+ label_entry = gtk.Label(_('Name:'))
+ label_entry.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ self._group.add_widget(label_entry)
+ label_entry.set_alignment(1, 0.5)
+ self._nick_box.pack_start(label_entry, expand=False)
+ label_entry.show()
+
+ self._nick_entry = gtk.Entry()
+ self._nick_entry.modify_bg(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._nick_entry.modify_base(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._nick_entry.set_width_chars(25)
+ self._nick_box.pack_start(self._nick_entry, expand=False)
+ self._nick_entry.show()
+
+ label_entry_error = gtk.Label()
+ self._group.add_widget(label_entry_error)
+ self._nick_alert_box.pack_start(label_entry_error, expand=False)
+ label_entry_error.show()
+
+ self._nick_alert = InlineAlert()
+ self._nick_alert_box.pack_start(self._nick_alert)
+ if 'nick' in self.restart_alerts:
+ self._nick_alert.props.msg = self.restart_msg
+ self._nick_alert.show()
+
+ self.pack_start(self._nick_box, False)
+ self.pack_start(self._nick_alert_box, False)
+ self._nick_box.show()
+ self._nick_alert_box.show()
+
+ def _setup_color(self):
+ label_color = gtk.Label(_('Click to change your color:'))
+ label_color.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ self._group.add_widget(label_color)
+ self._color_box.pack_start(label_color, expand=False)
+ label_color.show()
+
+ self._color_picker = ColorPicker()
+ self._color_box.pack_start(self._color_picker, expand=False)
+ self._color_picker.show()
+
+ label_color_error = gtk.Label()
+ self._group.add_widget(label_color_error)
+ self._color_alert_box.pack_start(label_color_error, expand=False)
+ label_color_error.show()
+
+ self._color_alert = InlineAlert()
+ self._color_alert_box.pack_start(self._color_alert)
+ if 'color' in self.restart_alerts:
+ self._color_alert.props.msg = self.restart_msg
+ self._color_alert.show()
+
+ self.pack_start(self._color_box, False)
+ self.pack_start(self._color_alert_box, False)
+ self._color_box.show()
+ self._color_alert_box.show()
+
+ def setup(self):
+ self._nick_entry.set_text(self._model.get_nick())
+ self._color_picker.icon.props.xo_color = self._model.get_color_xo()
+
+ self._color_valid = True
+ self._nick_valid = True
+ self.needs_restart = False
+ self._nick_change_handler = self._nick_entry.connect( \
+ 'changed', self.__nick_changed_cb)
+ self._color_change_handler = self._color_picker.connect( \
+ 'color-changed', self.__color_changed_cb)
+
+ def undo(self):
+ self._color_picker.disconnect(self._color_change_handler)
+ self._nick_entry.disconnect(self._nick_change_handler)
+ self._model.undo()
+ self._nick_alert.hide()
+ self._color_alert.hide()
+
+ def _validate(self):
+ if self._nick_valid and self._color_valid:
+ self.props.is_valid = True
+ else:
+ self.props.is_valid = False
+
+ def __nick_changed_cb(self, widget, data=None):
+ if self._nick_sid:
+ gobject.source_remove(self._nick_sid)
+ self._nick_sid = gobject.timeout_add(self._APPLY_TIMEOUT,
+ self.__nick_timeout_cb, widget)
+
+ def __nick_timeout_cb(self, widget):
+ self._nick_sid = 0
+
+ if widget.get_text() == self._model.get_nick():
+ return False
+ try:
+ self._model.set_nick(widget.get_text())
+ except ValueError, detail:
+ self._nick_alert.props.msg = detail
+ self._nick_valid = False
+ else:
+ self._nick_alert.props.msg = self.restart_msg
+ self._nick_valid = True
+ self.needs_restart = True
+ self.restart_alerts.append('nick')
+
+ self._validate()
+ self._nick_alert.show()
+ return False
+
+ def __color_changed_cb(self, colorpicker, xocolor):
+ self._model.set_color_xo(xocolor)
+ self.needs_restart = True
+ self._color_alert.props.msg = self.restart_msg
+ self._color_valid = True
+ self.restart_alerts.append('color')
+
+ self._validate()
+ self._color_alert.show()
+
+
+
+
+
+
diff --git a/src/jarabe/controlpanel/aboutxo/Makefile.am b/src/jarabe/controlpanel/aboutxo/Makefile.am
new file mode 100644
index 0000000..77cb5d8
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutxo/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/aboutxo
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/aboutxo/__init__.py b/src/jarabe/controlpanel/aboutxo/__init__.py
new file mode 100644
index 0000000..4dc2cb2
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutxo/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+
+CLASS = 'AboutXO'
+ICON = 'module-about_my_xo'
+TITLE = _('About my XO')
+
diff --git a/src/jarabe/controlpanel/aboutxo/model.py b/src/jarabe/controlpanel/aboutxo/model.py
new file mode 100644
index 0000000..ebbcca7
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutxo/model.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2008 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 re
+from gettext import gettext as _
+
+_logger = logging.getLogger('ControlPanel - AboutXO')
+_not_available = _('Not available')
+
+def get_aboutxo():
+ msg = 'Serial Number: %s \nBuild Number: %s \nFirmware Number: %s \n' \
+ % (get_serial_number(), get_build_number(), get_firmware_number())
+ return msg
+
+def print_aboutxo():
+ print get_aboutxo()
+
+def get_serial_number():
+ serial_no = _read_file('/ofw/serial-number')
+ if serial_no is None:
+ serial_no = _not_available
+ return serial_no
+
+def print_serial_number():
+ print get_serial_number()
+
+def get_build_number():
+ build_no = _read_file('/boot/olpc_build')
+ if build_no is None:
+ build_no = _not_available
+ return build_no
+
+def print_build_number():
+ print get_build_number()
+
+def get_firmware_number():
+ firmware_no = _read_file('/ofw/openprom/model')
+ if firmware_no is None:
+ firmware_no = _not_available
+ else:
+ firmware_no = re.split(" +", firmware_no)
+ if len(firmware_no) == 3:
+ firmware_no = firmware_no[1]
+ return firmware_no
+
+def print_firmware_number():
+ print get_firmware_number()
+
+def _read_file(path):
+ if os.access(path, os.R_OK) == 0:
+ return None
+
+ fd = open(path, 'r')
+ value = fd.read()
+ fd.close()
+ if value:
+ value = value.strip('\n')
+ return value
+ else:
+ _logger.debug('No information in file or directory: %s' % path)
+ return None
+
+def get_license():
+ license_file = "/usr/share/licenses/common-licenses/GPLv2"
+ lang = os.environ['LANG']
+ if lang.endswith("UTF-8"):
+ lang = lang[:-6]
+
+ try_file = license_file + "." + lang
+ if os.path.isfile(try_file):
+ license_file = try_file
+ else:
+ try_file = license_file + "." + lang.split("_")[0]
+ if os.path.isfile(try_file):
+ license_file = try_file
+
+ try:
+ fd = open(license_file)
+ # remove 0x0c page breaks which can't be rendered in text views
+ license_text = fd.read().replace('\x0c', '')
+ fd.close()
+ except IOError:
+ license_text = _not_available
+ return license_text
+
diff --git a/src/jarabe/controlpanel/aboutxo/view.py b/src/jarabe/controlpanel/aboutxo/view.py
new file mode 100644
index 0000000..e18d38d
--- /dev/null
+++ b/src/jarabe/controlpanel/aboutxo/view.py
@@ -0,0 +1,193 @@
+# coding=utf-8
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+import config
+from sugar.graphics import style
+
+from jarabe.controlpanel.sectionview import SectionView
+
+class AboutXO(SectionView):
+ def __init__(self, model, alerts=None):
+ SectionView.__init__(self)
+
+ self._model = model
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+
+ self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ scrollwindow = gtk.ScrolledWindow()
+ scrollwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ self.pack_start(scrollwindow, expand=True)
+ scrollwindow.show()
+
+ self._vbox = gtk.VBox()
+ scrollwindow.add_with_viewport(self._vbox)
+ self._vbox.show()
+
+ self._setup_identity()
+ self._setup_software()
+ self._setup_copyright()
+
+ def _setup_identity(self):
+ separator_identity = gtk.HSeparator()
+ self._vbox.pack_start(separator_identity, expand=False)
+ separator_identity.show()
+
+ label_identity = gtk.Label(_('Identity'))
+ label_identity.set_alignment(0, 0)
+ self._vbox.pack_start(label_identity, expand=False)
+ label_identity.show()
+ vbox_identity = gtk.VBox()
+ vbox_identity.set_border_width(style.DEFAULT_SPACING * 2)
+ vbox_identity.set_spacing(style.DEFAULT_SPACING)
+
+ box_identity = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_serial = gtk.Label(_('Serial Number:'))
+ label_serial.set_alignment(1, 0)
+ label_serial.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_identity.pack_start(label_serial, expand=False)
+ self._group.add_widget(label_serial)
+ label_serial.show()
+ label_serial_no = gtk.Label(self._model.get_serial_number())
+ label_serial_no.set_alignment(0, 0)
+ box_identity.pack_start(label_serial_no, expand=False)
+ label_serial_no.show()
+ vbox_identity.pack_start(box_identity, expand=False)
+ box_identity.show()
+
+ self._vbox.pack_start(vbox_identity, expand=False)
+ vbox_identity.show()
+
+
+ def _setup_software(self):
+ separator_software = gtk.HSeparator()
+ self._vbox.pack_start(separator_software, expand=False)
+ separator_software.show()
+
+ label_software = gtk.Label(_('Software'))
+ label_software.set_alignment(0, 0)
+ self._vbox.pack_start(label_software, expand=False)
+ label_software.show()
+ box_software = gtk.VBox()
+ box_software.set_border_width(style.DEFAULT_SPACING * 2)
+ box_software.set_spacing(style.DEFAULT_SPACING)
+
+ box_build = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_build = gtk.Label(_('Build:'))
+ label_build.set_alignment(1, 0)
+ label_build.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_build.pack_start(label_build, expand=False)
+ self._group.add_widget(label_build)
+ label_build.show()
+ label_build_no = gtk.Label(self._model.get_build_number())
+ label_build_no.set_alignment(0, 0)
+ box_build.pack_start(label_build_no, expand=False)
+ label_build_no.show()
+ box_software.pack_start(box_build, expand=False)
+ box_build.show()
+
+ box_sugar = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_sugar = gtk.Label(_('Sugar:'))
+ label_sugar.set_alignment(1, 0)
+ label_sugar.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_sugar.pack_start(label_sugar, expand=False)
+ self._group.add_widget(label_sugar)
+ label_sugar.show()
+ label_sugar_ver = gtk.Label(config.version)
+ label_sugar_ver.set_alignment(0, 0)
+ box_sugar.pack_start(label_sugar_ver, expand=False)
+ label_sugar_ver.show()
+ box_software.pack_start(box_sugar, expand=False)
+ box_sugar.show()
+
+ box_firmware = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_firmware = gtk.Label(_('Firmware:'))
+ label_firmware.set_alignment(1, 0)
+ label_firmware.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_firmware.pack_start(label_firmware, expand=False)
+ self._group.add_widget(label_firmware)
+ label_firmware.show()
+ label_firmware_no = gtk.Label(self._model.get_firmware_number())
+ label_firmware_no.set_alignment(0, 0)
+ box_firmware.pack_start(label_firmware_no, expand=False)
+ label_firmware_no.show()
+ box_software.pack_start(box_firmware, expand=False)
+ box_firmware.show()
+
+ self._vbox.pack_start(box_software, expand=False)
+ box_software.show()
+
+ def _setup_copyright(self):
+ separator_copyright = gtk.HSeparator()
+ self._vbox.pack_start(separator_copyright, expand=False)
+ separator_copyright.show()
+
+ label_copyright = gtk.Label(_('Copyright and License'))
+ label_copyright.set_alignment(0, 0)
+ self._vbox.pack_start(label_copyright, expand=False)
+ label_copyright.show()
+ vbox_copyright = gtk.VBox()
+ vbox_copyright.set_border_width(style.DEFAULT_SPACING * 2)
+ vbox_copyright.set_spacing(style.DEFAULT_SPACING)
+
+ label_copyright = gtk.Label(_("© 2008 One Laptop per Child "
+ "Association Inc; Red Hat Inc; "
+ "and Contributors."))
+ label_copyright.set_alignment(0, 0)
+ label_copyright.show()
+ vbox_copyright.pack_start(label_copyright, expand=False)
+
+ label_info = gtk.Label(_("Sugar is the graphical user interface that "
+ "you are looking at. Sugar is free software, "
+ "covered by the GNU General Public License, "
+ "and you are welcome to change it and/or "
+ "distribute copies of it under certain "
+ "conditions described therein."))
+ label_info.set_alignment(0, 0)
+ label_info.set_line_wrap(True)
+ label_info.set_size_request(gtk.gdk.screen_width() / 2, -1)
+ label_info.show()
+ vbox_copyright.pack_start(label_info, expand=False)
+
+ expander = gtk.Expander(_("Full license:"))
+ expander.connect("notify::expanded", self.license_expander_cb)
+ expander.show()
+ vbox_copyright.pack_start(expander, expand=True)
+
+ self._vbox.pack_start(vbox_copyright, expand=True)
+ vbox_copyright.show()
+
+ def license_expander_cb(self, expander, param_spec):
+ # load/destroy the license viewer on-demand, to avoid storing the
+ # GPL in memory at all times
+ if expander.get_expanded():
+ view_license = gtk.TextView()
+ view_license.set_editable(False)
+ view_license.get_buffer().set_text(self._model.get_license())
+ view_license.show()
+ expander.add(view_license)
+ else:
+ expander.get_child().destroy()
diff --git a/src/jarabe/controlpanel/cmd.py b/src/jarabe/controlpanel/cmd.py
new file mode 100644
index 0000000..8e41af5
--- /dev/null
+++ b/src/jarabe/controlpanel/cmd.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2007, 2008 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 sys
+import getopt
+import os
+from gettext import gettext as _
+
+import config
+
+_RESTART = 1
+
+_same_option_warning = _("sugar-control-panel: WARNING, found more than"
+ " one option with the same name: %s module: %r")
+_no_option_error = _("sugar-control-panel: key=%s not an available option")
+_general_error = _("sugar-control-panel: %s")
+
+def cmd_help():
+ '''Print the help to the screen'''
+ # TRANS: Translators, there's a empty line at the end of this string,
+ # which must appear in the translated string (msgstr) as well.
+ print _('Usage: sugar-control-panel [ option ] key [ args ... ] \n\
+ Control for the sugar environment. \n\
+ Options: \n\
+ -h show this help message and exit \n\
+ -l list all the available options \n\
+ -h key show information about this key \n\
+ -g key get the current value of the key \n\
+ -s key set the current value for the key \n\
+ -c key clear the current value for the key \n\
+ ')
+
+def note_restart():
+ '''Instructions how to restart sugar'''
+ print _('To apply your changes you have to restart sugar.\n' +
+ 'Hit ctrl+alt+erase on the keyboard to trigger a restart.')
+
+def load_modules():
+ '''Build a list of pointers to available modules and import them.
+ '''
+ modules = []
+
+ path = os.path.join(config.shell_path, 'controlpanel')
+ folder = os.listdir(path)
+
+ for item in folder:
+ if os.path.isdir(os.path.join(path, item)) and \
+ os.path.exists(os.path.join(path, item, 'model.py')):
+ module = __import__('.'.join(('controlpanel', item, 'model')),
+ globals(), locals(), ['model'])
+ modules.append(module)
+
+ return modules
+
+def main():
+ try:
+ options, args = getopt.getopt(sys.argv[1:], "h:s:g:c:l", [])
+ except getopt.GetoptError:
+ cmd_help()
+ sys.exit(2)
+
+ if not options:
+ cmd_help()
+ sys.exit(2)
+
+ modules = load_modules()
+
+ for option, key in options:
+ found = 0
+ if option in ("-h"):
+ for module in modules:
+ method = getattr(module, 'set_' + key, None)
+ if method:
+ found += 1
+ if found == 1:
+ print method.__doc__
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-l"):
+ for module in modules:
+ methods = dir(module)
+ print '%s:' % module.__name__.split('.')[1]
+ for method in methods:
+ if method.startswith('get_'):
+ print ' %s' % method[4:]
+ if option in ("-g"):
+ for module in modules:
+ method = getattr(module, 'print_' + key, None)
+ if method:
+ found += 1
+ if found == 1:
+ try:
+ method()
+ except Exception, detail:
+ print _(_general_error % detail)
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-s"):
+ for module in modules:
+ method = getattr(module, 'set_' + key, None)
+ if method:
+ note = 0
+ found += 1
+ if found == 1:
+ try:
+ note = method(*args)
+ except Exception, detail:
+ print _(_general_error % detail)
+ if note == _RESTART:
+ note_restart()
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-c"):
+ for module in modules:
+ method = getattr(module, 'clear_' + key, None)
+ if method:
+ note = 0
+ found += 1
+ if found == 1:
+ try:
+ note = method(*args)
+ except Exception, detail:
+ print _(_general_error % detail)
+ if note == _RESTART:
+ note_restart()
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
diff --git a/src/jarabe/controlpanel/datetime/Makefile.am b/src/jarabe/controlpanel/datetime/Makefile.am
new file mode 100644
index 0000000..f522683
--- /dev/null
+++ b/src/jarabe/controlpanel/datetime/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/datetime
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/datetime/__init__.py b/src/jarabe/controlpanel/datetime/__init__.py
new file mode 100644
index 0000000..fc9be45
--- /dev/null
+++ b/src/jarabe/controlpanel/datetime/__init__.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+
+CLASS = 'TimeZone'
+ICON = 'module-date_and_time'
+TITLE = _('Date & Time')
diff --git a/src/jarabe/controlpanel/datetime/model.py b/src/jarabe/controlpanel/datetime/model.py
new file mode 100644
index 0000000..4a4c560
--- /dev/null
+++ b/src/jarabe/controlpanel/datetime/model.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2007, 2008 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
+#
+#
+# The timezone config is based on the system-config-date
+# (http://fedoraproject.org/wiki/SystemConfig/date) tool.
+# Parts of the code were reused.
+#
+
+import os
+from gettext import gettext as _
+
+from sugar import profile
+
+_zone_tab = '/usr/share/zoneinfo/zone.tab'
+
+def _initialize():
+ '''Initialize the docstring of the set function'''
+ if set_timezone.__doc__ is None:
+ # when running under 'python -OO', all __doc__ fields are None,
+ # so += would fail -- and this function would be unnecessary anyway.
+ return
+ timezones = read_all_timezones()
+ for timezone in timezones:
+ set_timezone.__doc__ += timezone + '\n'
+
+def read_all_timezones(fn=_zone_tab):
+ fd = open (fn, 'r')
+ lines = fd.readlines()
+ fd.close()
+ timezones = []
+ for line in lines:
+ if line.startswith('#'):
+ continue
+ line = line.split()
+ if len(line) > 1:
+ timezones.append(line[2])
+ timezones.sort()
+
+ for offset in xrange(-12, 13):
+ if offset < 0:
+ tz = 'GMT%d' % offset
+ elif offset > 0:
+ tz = 'GMT+%d' % offset
+ else:
+ tz = 'GMT'
+ timezones.append(tz)
+ for offset in xrange(-12, 13):
+ if offset < 0:
+ tz = 'UTC%d' % offset
+ elif offset > 0:
+ tz = 'UTC+%d' % offset
+ else:
+ tz = 'UTC'
+ timezones.append(tz)
+ return timezones
+
+def get_timezone():
+ pro = profile.get_profile()
+ return pro.timezone
+
+def print_timezone():
+ print get_timezone()
+
+def set_timezone(timezone):
+ """Set the system timezone
+ timezone : e.g. 'America/Los_Angeles'
+ """
+ timezones = read_all_timezones()
+ if timezone in timezones:
+ os.environ['TZ'] = timezone
+ pro = profile.get_profile()
+ pro.timezone = timezone
+ pro.save()
+ else:
+ raise ValueError(_("Error timezone does not exist."))
+ return 1
+
+# inilialize the docstrings for the timezone
+_initialize()
+
diff --git a/src/jarabe/controlpanel/datetime/view.py b/src/jarabe/controlpanel/datetime/view.py
new file mode 100644
index 0000000..58719b4
--- /dev/null
+++ b/src/jarabe/controlpanel/datetime/view.py
@@ -0,0 +1,138 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics import style
+from sugar.graphics import iconentry
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+class TimeZone(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self.restart_alerts = alerts
+ self._zone_sid = 0
+ self._cursor_change_handler = None
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+
+ self.connect("realize", self.__realize_cb)
+
+ self._entry = iconentry.IconEntry()
+ self._entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._entry.add_clear_button()
+ self._entry.modify_bg(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._entry.modify_base(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self.pack_start(self._entry, False)
+ self._entry.show()
+
+ self._scrolled_window = gtk.ScrolledWindow()
+ self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ self._scrolled_window.set_shadow_type(gtk.SHADOW_IN)
+
+ self._store = gtk.ListStore(gobject.TYPE_STRING)
+ zones = model.read_all_timezones()
+ for zone in zones:
+ self._store.append([zone])
+
+ self._treeview = gtk.TreeView(self._store)
+ self._treeview.set_search_entry(self._entry)
+ self._treeview.set_search_equal_func(self._search)
+ self._treeview.set_search_column(0)
+ self._scrolled_window.add(self._treeview)
+ self._treeview.show()
+
+ self._timezone_column = gtk.TreeViewColumn(_('Timezone'))
+ self._cell = gtk.CellRendererText()
+ self._timezone_column.pack_start(self._cell, True)
+ self._timezone_column.add_attribute(self._cell, 'text', 0)
+ self._timezone_column.set_sort_column_id(0)
+ self._treeview.append_column(self._timezone_column)
+
+ self.pack_start(self._scrolled_window)
+ self._scrolled_window.show()
+
+ self._zone_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self.pack_start(self._zone_alert_box, False)
+
+ self._zone_alert = InlineAlert()
+ self._zone_alert_box.pack_start(self._zone_alert)
+ if 'zone' in self.restart_alerts:
+ self._zone_alert.props.msg = self.restart_msg
+ self._zone_alert.show()
+ self._zone_alert_box.show()
+
+ self.setup()
+
+ def setup(self):
+ zone = self._model.get_timezone()
+ for row in self._store:
+ if zone == row[0]:
+ self._treeview.set_cursor(row.path, self._timezone_column,
+ False)
+ self._treeview.scroll_to_cell(row.path, self._timezone_column,
+ True, 0.5, 0.5)
+ break
+
+ self.needs_restart = False
+ self._cursor_change_handler = self._treeview.connect( \
+ "cursor-changed", self.__zone_changed_cd)
+
+ def undo(self):
+ self._treeview.disconnect(self._cursor_change_handler)
+ self._model.undo()
+ self._zone_alert.hide()
+
+ def __realize_cb(self, widget):
+ self._entry.grab_focus()
+
+ def _search(self, model, column, key, iterator, data=None):
+ value = model.get_value(iterator, column)
+ if key.lower() in value.lower():
+ return False
+ return True
+
+ def __zone_changed_cd(self, treeview, data=None):
+ list_, row = treeview.get_selection().get_selected()
+ if not row:
+ return False
+ if self._model.get_timezone() == self._store.get_value(row, 0):
+ return False
+
+ if self._zone_sid:
+ gobject.source_remove(self._zone_sid)
+ self._zone_sid = gobject.timeout_add(self._APPLY_TIMEOUT,
+ self.__zone_timeout_cb, row)
+ return True
+
+ def __zone_timeout_cb(self, row):
+ self._zone_sid = 0
+ self._model.set_timezone(self._store.get_value(row, 0))
+ self.restart_alerts.append('zone')
+ self.needs_restart = True
+ self._zone_alert.props.msg = self.restart_msg
+ self._zone_alert.show()
+ return False
diff --git a/src/jarabe/controlpanel/frame/Makefile.am b/src/jarabe/controlpanel/frame/Makefile.am
new file mode 100644
index 0000000..10e2b40
--- /dev/null
+++ b/src/jarabe/controlpanel/frame/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/frame
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/frame/__init__.py b/src/jarabe/controlpanel/frame/__init__.py
new file mode 100644
index 0000000..a93f9c7
--- /dev/null
+++ b/src/jarabe/controlpanel/frame/__init__.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+
+CLASS = 'Frame'
+ICON = 'module-frame'
+TITLE = _('Frame')
diff --git a/src/jarabe/controlpanel/frame/model.py b/src/jarabe/controlpanel/frame/model.py
new file mode 100644
index 0000000..0e19703
--- /dev/null
+++ b/src/jarabe/controlpanel/frame/model.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2008 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
+#
+
+from gettext import gettext as _
+
+from sugar import profile
+
+def get_corner_delay():
+ pro = profile.get_profile()
+ return pro.hot_corners_delay
+
+def print_corner_delay():
+ print get_corner_delay()
+
+def set_corner_delay(delay):
+ """Set a delay for the activation of the frame using hot corners.
+ instantaneous: 0 (0 milliseconds)
+ delay: 100 (100 milliseconds)
+ never: 1000 (disable activation)
+ """
+ try:
+ int(delay)
+ except ValueError:
+ raise ValueError(_("Value must be an integer."))
+ pro = profile.get_profile()
+ pro.hot_corners_delay = int(delay)
+ pro.save()
+ return 1
+
+def get_edge_delay():
+ pro = profile.get_profile()
+ return pro.warm_edges_delay
+
+def print_edge_delay():
+ print get_edge_delay()
+
+def set_edge_delay(delay):
+ """Set a delay for the activation of the frame using warm edges.
+ instantaneous: 0 (0 milliseconds)
+ delay: 100 (100 milliseconds)
+ never: 1000 (disable activation)
+ """
+ try:
+ int(delay)
+ except ValueError:
+ raise ValueError(_("Value must be an integer."))
+ pro = profile.get_profile()
+ pro.warm_edges_delay = int(delay)
+ pro.save()
+ return 1
diff --git a/src/jarabe/controlpanel/frame/view.py b/src/jarabe/controlpanel/frame/view.py
new file mode 100644
index 0000000..7ab7bd2
--- /dev/null
+++ b/src/jarabe/controlpanel/frame/view.py
@@ -0,0 +1,232 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics import style
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+_never = _('never')
+_instantaneous = _('instantaneous')
+_seconds_label = _('%s seconds')
+_MAX_DELAY = 1000.0
+
+class Frame(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self._corner_delay_sid = 0
+ self._corner_delay_is_valid = True
+ self._corner_delay_change_handler = None
+ self._edge_delay_sid = 0
+ self._edge_delay_is_valid = True
+ self._edge_delay_change_handler = None
+ self.restart_alerts = alerts
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+ self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ separator = gtk.HSeparator()
+ self.pack_start(separator, expand=False)
+ separator.show()
+
+ label_activation = gtk.Label(_('Activation Delay'))
+ label_activation.set_alignment(0, 0)
+ self.pack_start(label_activation, expand=False)
+ label_activation.show()
+
+ self._box_sliders = gtk.VBox()
+ self._box_sliders.set_border_width(style.DEFAULT_SPACING * 2)
+ self._box_sliders.set_spacing(style.DEFAULT_SPACING)
+
+ self._corner_delay_slider = None
+ self._corner_delay_alert = None
+ self._setup_corner()
+
+ self._edge_delay_slider = None
+ self._edge_delay_alert = None
+ self._setup_edge()
+
+ self.pack_start(self._box_sliders, expand=False)
+ self._box_sliders.show()
+
+ self.setup()
+
+ def _setup_corner(self):
+ box_delay = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_delay = gtk.Label(_('Corner'))
+ label_delay.set_alignment(1, 0.75)
+ label_delay.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_delay.pack_start(label_delay, expand=False)
+ self._group.add_widget(label_delay)
+ label_delay.show()
+
+ adj = gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY,
+ step_incr=100, page_incr=100, page_size=0)
+ self._corner_delay_slider = gtk.HScale(adj)
+ self._corner_delay_slider.set_digits(0)
+ self._corner_delay_slider.connect('format-value',
+ self.__corner_delay_format_cb)
+ box_delay.pack_start(self._corner_delay_slider)
+ self._corner_delay_slider.show()
+ self._box_sliders.pack_start(box_delay, expand=False)
+ box_delay.show()
+
+ self._corner_delay_alert = InlineAlert()
+ label_delay_error = gtk.Label()
+ self._group.add_widget(label_delay_error)
+
+ delay_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ delay_alert_box.pack_start(label_delay_error, expand=False)
+ label_delay_error.show()
+ delay_alert_box.pack_start(self._corner_delay_alert, expand=False)
+ self._box_sliders.pack_start(delay_alert_box, expand=False)
+ delay_alert_box.show()
+ if 'corner_delay' in self.restart_alerts:
+ self._corner_delay_alert.props.msg = self.restart_msg
+ self._corner_delay_alert.show()
+
+ def _setup_edge(self):
+ box_delay = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_delay = gtk.Label(_('Edge'))
+ label_delay.set_alignment(1, 0.75)
+ label_delay.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_delay.pack_start(label_delay, expand=False)
+ self._group.add_widget(label_delay)
+ label_delay.show()
+
+ adj = gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY,
+ step_incr=100, page_incr=100, page_size=0)
+ self._edge_delay_slider = gtk.HScale(adj)
+ self._edge_delay_slider.set_digits(0)
+ self._edge_delay_slider.connect('format-value',
+ self.__edge_delay_format_cb)
+ box_delay.pack_start(self._edge_delay_slider)
+ self._edge_delay_slider.show()
+ self._box_sliders.pack_start(box_delay, expand=False)
+ box_delay.show()
+
+ self._edge_delay_alert = InlineAlert()
+ label_delay_error = gtk.Label()
+ self._group.add_widget(label_delay_error)
+
+ delay_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ delay_alert_box.pack_start(label_delay_error, expand=False)
+ label_delay_error.show()
+ delay_alert_box.pack_start(self._edge_delay_alert, expand=False)
+ self._box_sliders.pack_start(delay_alert_box, expand=False)
+ delay_alert_box.show()
+ if 'edge_delay' in self.restart_alerts:
+ self._edge_delay_alert.props.msg = self.restart_msg
+ self._edge_delay_alert.show()
+
+ def setup(self):
+ self._corner_delay_slider.set_value(self._model.get_corner_delay())
+ self._edge_delay_slider.set_value(self._model.get_edge_delay())
+ self._corner_delay_is_valid = True
+ self._edge_delay_is_valid = True
+ self.needs_restart = False
+ self._corner_delay_change_handler = self._corner_delay_slider.connect( \
+ 'value-changed', self.__corner_delay_changed_cb)
+ self._edge_delay_change_handler = self._edge_delay_slider.connect( \
+ 'value-changed', self.__edge_delay_changed_cb)
+
+ def undo(self):
+ self._corner_delay_slider.disconnect(self._corner_delay_change_handler)
+ self._edge_delay_slider.disconnect(self._edge_delay_change_handler)
+ self._model.undo()
+ self._corner_delay_alert.hide()
+ self._edge_delay_alert.hide()
+
+ def _validate(self):
+ if self._edge_delay_is_valid and self._corner_delay_is_valid:
+ self.props.is_valid = True
+ else:
+ self.props.is_valid = False
+
+ def __corner_delay_changed_cb(self, scale, data=None):
+ if self._corner_delay_sid:
+ gobject.source_remove(self._corner_delay_sid)
+ self._corner_delay_sid = gobject.timeout_add( \
+ self._APPLY_TIMEOUT, self.__corner_delay_timeout_cb, scale)
+
+ def __corner_delay_timeout_cb(self, scale):
+ self._corner_delay_sid = 0
+ if scale.get_value() == self._model.get_corner_delay():
+ return
+ try:
+ self._model.set_corner_delay(scale.get_value())
+ except ValueError, detail:
+ self._corner_delay_alert.props.msg = detail
+ self._corner_delay_is_valid = False
+ else:
+ self._corner_delay_alert.props.msg = self.restart_msg
+ self._corner_delay_is_valid = True
+ self.needs_restart = True
+ self.restart_alerts.append('corner_delay')
+
+ self._validate()
+ self._corner_delay_alert.show()
+ return False
+
+ def __corner_delay_format_cb(self, scale, value):
+ if value == _MAX_DELAY:
+ return _never
+ elif value == 0.0:
+ return _instantaneous
+ else:
+ return _seconds_label % (value / _MAX_DELAY)
+
+ def __edge_delay_changed_cb(self, scale, data=None):
+ if self._edge_delay_sid:
+ gobject.source_remove(self._edge_delay_sid)
+ self._edge_delay_sid = gobject.timeout_add( \
+ self._APPLY_TIMEOUT, self.__edge_delay_timeout_cb, scale)
+
+ def __edge_delay_timeout_cb(self, scale):
+ self._edge_delay_sid = 0
+ if scale.get_value() == self._model.get_edge_delay():
+ return
+ try:
+ self._model.set_edge_delay(scale.get_value())
+ except ValueError, detail:
+ self._edge_delay_alert.props.msg = detail
+ self._edge_delay_is_valid = False
+ else:
+ self._edge_delay_alert.props.msg = self.restart_msg
+ self._edge_delay_is_valid = True
+ self.needs_restart = True
+ self.restart_alerts.append('edge_delay')
+
+ self._validate()
+ self._edge_delay_alert.show()
+ return False
+
+ def __edge_delay_format_cb(self, scale, value):
+ if value == _MAX_DELAY:
+ return _never
+ elif value == 0.0:
+ return _instantaneous
+ else:
+ return _seconds_label % (value / _MAX_DELAY)
diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py
new file mode 100644
index 0000000..a0e4498
--- /dev/null
+++ b/src/jarabe/controlpanel/gui.py
@@ -0,0 +1,414 @@
+# Copyright (C) 2008 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 gtk
+import os
+import gobject
+import logging
+from gettext import gettext as _
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.alert import Alert
+import config
+from session import get_session_manager
+
+from jarabe.controlpanel.toolbar import MainToolbar
+from jarabe.controlpanel.toolbar import SectionToolbar
+
+_logger = logging.getLogger('ControlPanel')
+_MAX_COLUMNS = 5
+
+class ControlPanel(gtk.Window):
+ __gtype_name__ = 'SugarControlPanel'
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._toolbar = None
+ self._canvas = None
+ self._table = None
+ self._scrolledwindow = None
+ self._separator = None
+ self._section_view = None
+ self._section_toolbar = None
+ self._main_toolbar = None
+
+ self._vbox = gtk.VBox()
+ self._hbox = gtk.HBox()
+ self._vbox.pack_start(self._hbox)
+ self._hbox.show()
+
+ self._main_view = gtk.EventBox()
+ self._hbox.pack_start(self._main_view)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._main_view.show()
+
+ self.add(self._vbox)
+ self._vbox.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ self._options = self._get_options()
+ self._current_option = None
+ self._setup_main()
+ self._setup_section()
+ self._show_main_view()
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def _set_canvas(self, canvas):
+ if self._canvas:
+ self._main_view.remove(self._canvas)
+ if canvas:
+ self._main_view.add(canvas)
+ self._canvas = canvas
+
+ def _set_toolbar(self, toolbar):
+ if self._toolbar:
+ self._vbox.remove(self._toolbar)
+ self._vbox.pack_start(toolbar, False)
+ self._vbox.reorder_child(toolbar, 0)
+ self._toolbar = toolbar
+ if not self._separator:
+ self._separator = gtk.HSeparator()
+ self._vbox.pack_start(self._separator, False)
+ self._vbox.reorder_child(self._separator, 1)
+ self._separator.show()
+
+ def _setup_main(self):
+ self._main_toolbar = MainToolbar()
+
+ self._table = gtk.Table()
+ self._table.set_col_spacings(style.GRID_CELL_SIZE)
+ self._table.set_border_width(style.GRID_CELL_SIZE)
+
+ self._scrolledwindow = gtk.ScrolledWindow()
+ self._scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC,
+ gtk.POLICY_AUTOMATIC)
+ self._scrolledwindow.add_with_viewport(self._table)
+ child = self._scrolledwindow.get_child()
+ child.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color())
+
+ self._setup_options()
+ self._main_toolbar.connect('stop-clicked',
+ self.__stop_clicked_cb)
+ self._main_toolbar.connect('search-changed',
+ self.__search_changed_cb)
+
+ def _setup_options(self):
+ row = 0
+ column = 2
+ options = self._options.keys()
+ options.sort()
+
+ for option in options:
+ sectionicon = _SectionIcon(icon_name=self._options[option]['icon'],
+ title=self._options[option]['title'],
+ xo_color=self._options[option]['color'],
+ pixel_size=style.GRID_CELL_SIZE)
+ sectionicon.connect('button_press_event',
+ self.__select_option_cb, option)
+ sectionicon.show()
+
+ if option == 'aboutme':
+ self._table.attach(sectionicon, 0, 1, 0, 1)
+ elif option == 'aboutxo':
+ self._table.attach(sectionicon, 1, 2, 0, 1)
+ else:
+ self._table.attach(sectionicon,
+ column, column + 1,
+ row, row + 1)
+ column += 1
+ if column == _MAX_COLUMNS:
+ column = 0
+ row += 1
+
+ self._options[option]['button'] = sectionicon
+
+ def _show_main_view(self):
+ self._set_toolbar(self._main_toolbar)
+ self._main_toolbar.show()
+ self._set_canvas(self._scrolledwindow)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._table.show()
+ self._scrolledwindow.show()
+ entry = self._main_toolbar.get_entry()
+ entry.grab_focus()
+ entry.set_text('')
+
+ def _update(self, query):
+ for option in self._options:
+ found = False
+ for key in self._options[option]['keywords']:
+ if query.lower() in key.lower():
+ self._options[option]['button'].set_sensitive(True)
+ found = True
+ break
+ if not found:
+ self._options[option]['button'].set_sensitive(False)
+
+ def _setup_section(self):
+ self._section_toolbar = SectionToolbar()
+ self._section_toolbar.connect('cancel-clicked',
+ self.__cancel_clicked_cb)
+ self._section_toolbar.connect('accept-clicked',
+ self.__accept_clicked_cb)
+
+ def show_section_view(self, option):
+ self._set_toolbar(self._section_toolbar)
+
+ icon = self._section_toolbar.get_icon()
+ icon.set_from_icon_name(self._options[option]['icon'],
+ gtk.ICON_SIZE_LARGE_TOOLBAR)
+ icon.props.xo_color = self._options[option]['color']
+ title = self._section_toolbar.get_title()
+ title.set_text(self._options[option]['title'])
+ self._section_toolbar.show()
+
+ self._current_option = option
+
+ mod = __import__('.'.join(('controlpanel', option, 'view')),
+ globals(), locals(), ['view'])
+ view_class = getattr(mod, self._options[option]['view'], None)
+
+ mod = __import__('.'.join(('controlpanel', option, 'model')),
+ globals(), locals(), ['model'])
+ model = ModelWrapper(mod)
+
+ self._section_view = view_class(model,
+ self._options[option]['alerts'])
+
+ self._set_canvas(self._section_view)
+ self._section_view.show()
+ self._section_view.connect('notify::is-valid',
+ self.__valid_section_cb)
+ self._section_view.connect('request-close',
+ self.__close_request_cb)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+
+ def set_section_view_auto_close(self):
+ '''Automatically close the control panel if there is "nothing to do"
+ '''
+ self._section_view.auto_close = True
+
+ def _get_options(self):
+ '''Get the available option information from the extensions
+ '''
+ options = {}
+
+ path = os.path.join(config.shell_path, 'controlpanel')
+ folder = os.listdir(path)
+
+ for item in folder:
+ if os.path.isdir(os.path.join(path, item)) and \
+ os.path.exists(os.path.join(path, item, '__init__.py')):
+ mod = __import__('.'.join(('controlpanel', item)),
+ globals(), locals(), [item])
+ view_class = getattr(mod, 'CLASS', None)
+ if view_class is not None:
+ options[item] = {}
+ options[item]['alerts'] = []
+ options[item]['view'] = view_class
+ options[item]['icon'] = getattr(mod, 'ICON', item)
+ options[item]['title'] = getattr(mod, 'TITLE', item)
+ options[item]['color'] = getattr(mod, 'COLOR', None)
+ keywords = getattr(mod, 'KEYWORDS', [])
+ keywords.append(options[item]['title'].lower())
+ if item not in keywords:
+ keywords.append(item)
+ else:
+ _logger.error('There is no CLASS constant specified in ' \
+ 'the view file \'%s\'.' % item)
+ return options
+
+ def __cancel_clicked_cb(self, widget):
+ self._section_view.undo()
+ self._options[self._current_option]['alerts'] = []
+ self._section_toolbar.accept_button.set_sensitive(True)
+ self._show_main_view()
+
+ def __accept_clicked_cb(self, widget):
+ if self._section_view.needs_restart:
+ self._section_toolbar.accept_button.set_sensitive(False)
+ self._section_toolbar.cancel_button.set_sensitive(False)
+ alert = Alert()
+ alert.props.title = _('Warning')
+ alert.props.msg = _('Changes require restart')
+
+ icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel changes'), icon)
+ icon.show()
+
+ if self._current_option != 'aboutme':
+ icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_ACCEPT, _('Later'), icon)
+ icon.show()
+
+ icon = Icon(icon_name='system-restart')
+ alert.add_button(gtk.RESPONSE_APPLY, _('Restart now'), icon)
+ icon.show()
+
+ self._vbox.pack_start(alert, False)
+ self._vbox.reorder_child(alert, 2)
+ alert.connect('response', self.__response_cb)
+ alert.show()
+ else:
+ self._show_main_view()
+
+ def __response_cb(self, alert, response_id):
+ self._vbox.remove(alert)
+ self._section_toolbar.accept_button.set_sensitive(True)
+ self._section_toolbar.cancel_button.set_sensitive(True)
+ if response_id is gtk.RESPONSE_CANCEL:
+ self._section_view.undo()
+ self._section_view.setup()
+ self._options[self._current_option]['alerts'] = []
+ elif response_id is gtk.RESPONSE_ACCEPT:
+ self._options[self._current_option]['alerts'] = \
+ self._section_view.restart_alerts
+ self._show_main_view()
+ elif response_id is gtk.RESPONSE_APPLY:
+ session_manager = get_session_manager()
+ session_manager.logout()
+
+ def __select_option_cb(self, button, event, option):
+ self.show_section_view(option)
+
+ def __search_changed_cb(self, maintoolbar, query):
+ self._update(query)
+
+ def __stop_clicked_cb(self, widget):
+ self.destroy()
+
+ def __close_request_cb(self, widget, event=None):
+ self.destroy()
+
+ def __valid_section_cb(self, section_view, pspec):
+ section_is_valid = section_view.props.is_valid
+ self._section_toolbar.accept_button.set_sensitive(section_is_valid)
+
+class ModelWrapper(object):
+ def __init__(self, module):
+ self._module = module
+ self._options = {}
+ self._setup()
+
+ def _setup(self):
+ methods = dir(self._module)
+ for method in methods:
+ if method.startswith('get_') and method[4:] != 'color':
+ try:
+ self._options[method[4:]] = getattr(self._module, method)()
+ except Exception:
+ self._options[method[4:]] = None
+
+ def __getattr__(self, name):
+ return getattr(self._module, name)
+
+ def undo(self):
+ for key in self._options.keys():
+ method = getattr(self._module, 'set_' + key, None)
+ if method and self._options[key] is not None:
+ try:
+ method(self._options[key])
+ except Exception, detail:
+ _logger.debug('Error undo option: %s' % detail)
+
+class _SectionIcon(gtk.EventBox):
+ __gtype_name__ = "SugarSectionIcon"
+
+ __gproperties__ = {
+ 'icon-name' : (str, None, None, None,
+ gobject.PARAM_READWRITE),
+ 'pixel-size' : (object, None, None,
+ gobject.PARAM_READWRITE),
+ 'xo-color' : (object, None, None,
+ gobject.PARAM_READWRITE),
+ 'title' : (str, None, None, None,
+ gobject.PARAM_READWRITE)
+ }
+
+ def __init__(self, **kwargs):
+ self._icon_name = None
+ self._pixel_size = style.GRID_CELL_SIZE
+ self._xo_color = None
+ self._title = 'No Title'
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self._vbox = gtk.VBox()
+ self._icon = Icon(icon_name=self._icon_name,
+ pixel_size=self._pixel_size,
+ xo_color=self._xo_color)
+ self._vbox.pack_start(self._icon, expand=False, fill=False)
+
+ self._label = gtk.Label(self._title)
+ self._label.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._label, expand=False, fill=False)
+
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self.set_visible_window(False)
+ self.set_app_paintable(True)
+ self.set_events(gtk.gdk.BUTTON_PRESS_MASK)
+
+ self.add(self._vbox)
+ self._vbox.show()
+ self._label.show()
+ self._icon.show()
+
+ def get_icon(self):
+ return self._icon
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'icon-name':
+ if self._icon_name != value:
+ self._icon_name = value
+ elif pspec.name == 'pixel-size':
+ if self._pixel_size != value:
+ self._pixel_size = value
+ elif pspec.name == 'xo-color':
+ if self._xo_color != value:
+ self._xo_color = value
+ elif pspec.name == 'title':
+ if self._title != value:
+ self._title = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'icon-name':
+ return self._icon_name
+ elif pspec.name == 'pixel-size':
+ return self._pixel_size
+ elif pspec.name == 'xo-color':
+ return self._xo_color
+ elif pspec.name == 'title':
+ return self._title
diff --git a/src/jarabe/controlpanel/inlinealert.py b/src/jarabe/controlpanel/inlinealert.py
new file mode 100644
index 0000000..619a379
--- /dev/null
+++ b/src/jarabe/controlpanel/inlinealert.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 pango
+
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+
+class InlineAlert(gtk.HBox):
+ """UI interface for Inline alerts
+
+ Inline alerts are different from the other alerts beause they are
+ no dialogs, they only inform about a current event.
+
+ Properties:
+ 'msg': the message of the alert,
+ 'icon': the icon that appears at the far left
+ See __gproperties__
+ """
+
+ __gtype_name__ = 'SugarInlineAlert'
+
+ __gproperties__ = {
+ 'msg' : (str, None, None, None,
+ gobject.PARAM_READWRITE),
+ 'icon' : (object, None, None,
+ gobject.PARAM_WRITABLE)
+ }
+
+ def __init__(self, **kwargs):
+
+ self._msg = None
+ self._msg_color = None
+ self._icon = Icon(icon_name='emblem-warning',
+ fill_color=style.COLOR_SELECTION_GREY.get_svg(),
+ stroke_color=style.COLOR_WHITE.get_svg())
+
+ self._msg_label = gtk.Label()
+ self._msg_label.set_max_width_chars(50)
+ self._msg_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
+ self._msg_label.set_alignment(0, 0.5)
+ self._msg_label.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.set_spacing(style.DEFAULT_SPACING)
+ self.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+
+ self.pack_start(self._icon, False)
+ self.pack_start(self._msg_label, False)
+ self._msg_label.show()
+ self._icon.show()
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'msg':
+ if self._msg != value:
+ self._msg = value
+ self._msg_label.set_markup(self._msg)
+ elif pspec.name == 'icon':
+ if self._icon != value:
+ self._icon = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'msg':
+ return self._msg
+
diff --git a/src/jarabe/controlpanel/language/Makefile.am b/src/jarabe/controlpanel/language/Makefile.am
new file mode 100644
index 0000000..ed8992c
--- /dev/null
+++ b/src/jarabe/controlpanel/language/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/language
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/language/__init__.py b/src/jarabe/controlpanel/language/__init__.py
new file mode 100644
index 0000000..a8f9f08
--- /dev/null
+++ b/src/jarabe/controlpanel/language/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+
+CLASS = 'Language'
+ICON = 'module-language'
+TITLE = _('Language')
+
diff --git a/src/jarabe/controlpanel/language/model.py b/src/jarabe/controlpanel/language/model.py
new file mode 100644
index 0000000..404d9dd
--- /dev/null
+++ b/src/jarabe/controlpanel/language/model.py
@@ -0,0 +1,135 @@
+# Copyright (C) 2007, 2008 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
+#
+#
+# The language config is based on the system-config-language
+# (http://fedoraproject.org/wiki/SystemConfig/language) tool
+# Parts of the code were reused.
+#
+
+import os
+from gettext import gettext as _
+import subprocess
+
+_default_lang = 'en_US.utf8'
+_standard_msg = _("Could not access ~/.i18n. Create standard settings.")
+
+def read_all_languages():
+ fdp = subprocess.Popen(['locale', '-av'], stdout=subprocess.PIPE)
+ lines = fdp.stdout.read().split('\n')
+ locales = []
+
+ for line in lines:
+ if line.find('locale:') != -1:
+ locale = line.lstrip('locale:')
+ locale = locale.split('archive:')[0].strip()
+ elif line.find('language |') != -1:
+ lang = line.lstrip('language |')
+ elif line.find('territory |') != -1:
+ territory = line.lstrip('territory |')
+ if locale.endswith('utf8') and len(lang):
+ locales.append((lang, territory, locale))
+
+ #FIXME: This is a temporary workaround for locales that are essential to
+ # OLPC, but are not in Glibc yet.
+ locales.append(('Kreyol', 'Haiti', 'ht_HT.utf8'))
+ locales.append(('Dari', 'Afghanistan', 'fa_AF.utf8'))
+ locales.append(('Pashto', 'Afghanistan', 'ps_AF.utf8'))
+
+ locales.sort()
+ return locales
+
+def _initialize():
+ if set_language.__doc__ is None:
+ # when running under 'python -OO', all __doc__ fields are None,
+ # so += would fail -- and this function would be unnecessary anyway.
+ return
+ languages = read_all_languages()
+ set_language.__doc__ += '\n'
+ for lang in languages:
+ set_language.__doc__ += '%s \n' % (lang[0].replace(' ', '_') + '/' +
+ lang[1].replace(' ', '_'))
+
+def _write_i18n(lang):
+ path = os.path.join(os.environ.get("HOME"), '.i18n')
+ if os.access(path, os.W_OK) == 0:
+ print _standard_msg
+ fd = open(path, 'w')
+ fd.write('LANG="%s"\n' % _default_lang)
+ fd.close()
+ else:
+ fd = open(path, 'r')
+ lines = fd.readlines()
+ fd.close()
+ for i in range(len(lines)):
+ if lines[i][:5] == "LANG=":
+ lines[i] = 'LANG="%s"\n' % lang
+ fd = open(path, 'w')
+ fd.writelines(lines)
+ fd.close()
+
+def get_language():
+ path = os.path.join(os.environ.get("HOME"), '.i18n')
+ if os.access(path, os.R_OK) == 0:
+ print _standard_msg
+ fd = open(path, 'w')
+ fd.write('LANG="%s"\n' % _default_lang)
+ fd.close()
+ return _default_lang
+
+ fd = open(path, "r")
+ lines = fd.readlines()
+ fd.close()
+
+ lang = None
+
+ for line in lines:
+ if line[:5] == "LANG=":
+ lang = line[5:].replace('"', '')
+ lang = lang.strip()
+
+ return lang
+
+def print_language():
+ code = get_language()
+
+ languages = read_all_languages()
+ for lang in languages:
+ if lang[2].split('.')[0] == code.split('.')[0]:
+ print lang[0].replace(' ', '_') + '/' + lang[1].replace(' ', '_')
+ return
+ print (_("Language for code=%s could not be determined.") % code)
+
+def set_language(language):
+ """Set the system language.
+ languages :
+ """
+ if language.endswith('utf8'):
+ _write_i18n(language)
+ return 1
+ else:
+ languages = read_all_languages()
+ for lang, territory, locale in languages:
+ code = lang.replace(' ', '_') + '/' \
+ + territory.replace(' ', '_')
+ if code == language:
+ _write_i18n(locale)
+ return 1
+ print (_("Sorry I do not speak \'%s\'.") % language)
+
+# inilialize the docstrings for the language
+_initialize()
+
diff --git a/src/jarabe/controlpanel/language/view.py b/src/jarabe/controlpanel/language/view.py
new file mode 100644
index 0000000..71a185f
--- /dev/null
+++ b/src/jarabe/controlpanel/language/view.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics import style
+from sugar.graphics import iconentry
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+class Language(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self.restart_alerts = alerts
+ self._lang_sid = 0
+ self._cursor_change_handler = None
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+
+ self.connect("realize", self.__realize_cb)
+
+ self._entry = iconentry.IconEntry()
+ self._entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._entry.add_clear_button()
+ self._entry.modify_bg(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._entry.modify_base(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self.pack_start(self._entry, False)
+ self._entry.show()
+
+ self._scrolled_window = gtk.ScrolledWindow()
+ self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ self._scrolled_window.set_shadow_type(gtk.SHADOW_IN)
+
+ self._store = gtk.ListStore(gobject.TYPE_STRING,
+ gobject.TYPE_STRING)
+ locales = model.read_all_languages()
+ for locale in locales:
+ self._store.append([locale[2], '%s (%s)' %
+ (locale[0], locale[1])])
+
+ self._treeview = gtk.TreeView(self._store)
+ self._treeview.set_search_entry(self._entry)
+ self._treeview.set_search_equal_func(self._search)
+ self._treeview.set_search_column(1)
+ self._scrolled_window.add(self._treeview)
+ self._treeview.show()
+
+ self._language_column = gtk.TreeViewColumn(_('Language'))
+ self._cell = gtk.CellRendererText()
+ self._language_column.pack_start(self._cell, True)
+ self._language_column.add_attribute(self._cell, 'text', 1)
+ self._language_column.set_sort_column_id(1)
+ self._treeview.append_column(self._language_column)
+
+ self.pack_start(self._scrolled_window)
+ self._scrolled_window.show()
+
+ self._lang_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self.pack_start(self._lang_alert_box, False)
+
+ self._lang_alert = InlineAlert()
+ self._lang_alert_box.pack_start(self._lang_alert)
+ if 'lang' in self.restart_alerts:
+ self._lang_alert.props.msg = self.restart_msg
+ self._lang_alert.show()
+ self._lang_alert_box.show()
+
+ self.setup()
+
+ def setup(self):
+ lang_code = self._model.get_language()
+ for row in self._store:
+ lang = lang_code.split('.')[0]
+ lang_column = row[0].split('.')[0]
+ if lang in lang_column:
+ self._treeview.set_cursor(row.path, self._language_column,
+ False)
+ self._treeview.scroll_to_cell(row.path, self._language_column,
+ True, 0.5, 0.5)
+ break
+
+ self.needs_restart = False
+ self._cursor_change_handler = self._treeview.connect( \
+ "cursor-changed", self.__lang_changed_cd)
+
+ def undo(self):
+ self._treeview.disconnect(self._cursor_change_handler)
+ self._model.undo()
+ self._lang_alert.hide()
+
+ def __realize_cb(self, widget):
+ self._entry.grab_focus()
+
+ def _search(self, model, column, key, iterator, data=None):
+ value = model.get_value(iterator, column)
+ if key.lower() in value.lower():
+ return False
+ return True
+
+ def __lang_changed_cd(self, treeview, data=None):
+ row = treeview.get_selection().get_selected()
+ if not row[1]:
+ return False
+ if self._model.get_language() == self._store.get_value(row[1], 0):
+ return False
+
+ if self._lang_sid:
+ gobject.source_remove(self._lang_sid)
+ self._lang_sid = gobject.timeout_add(self._APPLY_TIMEOUT,
+ self.__lang_timeout_cb,
+ self._store.get_value(row[1], 0))
+
+ def __lang_timeout_cb(self, code):
+ self._lang_sid = 0
+ self._model.set_language(code)
+ self.restart_alerts.append('lang')
+ self.needs_restart = True
+ self._lang_alert.props.msg = self.restart_msg
+ self._lang_alert.show()
+ return False
diff --git a/src/jarabe/controlpanel/network/Makefile.am b/src/jarabe/controlpanel/network/Makefile.am
new file mode 100644
index 0000000..e480aa3
--- /dev/null
+++ b/src/jarabe/controlpanel/network/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/network
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/network/__init__.py b/src/jarabe/controlpanel/network/__init__.py
new file mode 100644
index 0000000..8fea274
--- /dev/null
+++ b/src/jarabe/controlpanel/network/__init__.py
@@ -0,0 +1,25 @@
+# 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 _
+
+CLASS = 'Network'
+ICON = 'module-network'
+TITLE = _('Network')
+KEYWORDS = ['network', 'jabber', 'radio', 'server']
+
+
+
diff --git a/src/jarabe/controlpanel/network/model.py b/src/jarabe/controlpanel/network/model.py
new file mode 100644
index 0000000..b6f71a0
--- /dev/null
+++ b/src/jarabe/controlpanel/network/model.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2008 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 dbus
+from gettext import gettext as _
+
+from sugar import profile
+from jarabe.hardware import hardwaremanager
+
+NM_SERVICE_NAME = 'org.freedesktop.NetworkManager'
+NM_SERVICE_PATH = '/org/freedesktop/NetworkManager'
+NM_SERVICE_IFACE = 'org.freedesktop.NetworkManager'
+NM_ASLEEP = 1
+
+KEYWORDS = ['network', 'jabber', 'radio', 'server']
+
+class ReadError(Exception):
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+def get_jabber():
+ pro = profile.get_profile()
+ return pro.jabber_server
+
+def print_jabber():
+ print get_jabber()
+
+def set_jabber(server):
+ """Set the jabber server
+ server : e.g. 'olpc.collabora.co.uk'
+ """
+ pro = profile.get_profile()
+ pro.jabber_server = server
+ pro.jabber_registered = False
+ pro.save()
+ return 1
+
+def get_radio():
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH)
+ nm = dbus.Interface(proxy, NM_SERVICE_IFACE)
+ state = nm.getWirelessEnabled()
+ if state in (0, 1):
+ return state
+ else:
+ raise ReadError(_('State is unknown.'))
+
+def print_radio():
+ print ('off', 'on')[get_radio()]
+
+def set_radio(state):
+ """Turn Radio 'on' or 'off'
+ state : 'on/off'
+ """
+ if state == 'on' or state == 1:
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH)
+ nm = dbus.Interface(proxy, NM_SERVICE_IFACE)
+ nm.setWirelessEnabled(True)
+ elif state == 'off' or state == 0:
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH)
+ nm = dbus.Interface(proxy, NM_SERVICE_IFACE)
+ nm.setWirelessEnabled(False)
+ else:
+ raise ValueError(_("Error in specified radio argument use on/off."))
+
+ return 0
+
+def clear_registration():
+ """Clear the registration with the schoolserver
+ """
+ pro = profile.get_profile()
+ pro.backup1 = None
+ pro.save()
+ return 1
+
+def clear_networks():
+ """Clear saved passwords and network configurations.
+ """
+ network_manager = hardwaremanager.get_network_manager()
+ if not network_manager:
+ return
+ network_manager.nminfo.delete_all_networks()
+ return 1
diff --git a/src/jarabe/controlpanel/network/view.py b/src/jarabe/controlpanel/network/view.py
new file mode 100644
index 0000000..4f4ada7
--- /dev/null
+++ b/src/jarabe/controlpanel/network/view.py
@@ -0,0 +1,231 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics import style
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+CLASS = 'Network'
+ICON = 'module-network'
+TITLE = _('Network')
+
+class Network(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self.restart_alerts = alerts
+ self._jabber_sid = 0
+ self._jabber_valid = True
+ self._radio_valid = True
+ self._jabber_change_handler = None
+ self._radio_change_handler = None
+ self._network_configuration_reset_handler = None
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+ group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ self._radio_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._jabber_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+
+ separator_wireless = gtk.HSeparator()
+ self.pack_start(separator_wireless, expand=False)
+ separator_wireless.show()
+
+ label_wireless = gtk.Label(_('Wireless'))
+ label_wireless.set_alignment(0, 0)
+ self.pack_start(label_wireless, expand=False)
+ label_wireless.show()
+ box_wireless = gtk.VBox()
+ box_wireless.set_border_width(style.DEFAULT_SPACING * 2)
+ box_wireless.set_spacing(style.DEFAULT_SPACING)
+
+ radio_info = gtk.Label(_("Turn of the wireless radio to save "
+ "battery life"))
+ radio_info.set_alignment(0, 0)
+ radio_info.set_line_wrap(True)
+ radio_info.show()
+ box_wireless.pack_start(radio_info, expand=False)
+
+ box_radio = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._button = gtk.CheckButton()
+ self._button.set_alignment(0, 0)
+ box_radio.pack_start(self._button, expand=False)
+ self._button.show()
+
+ label_radio = gtk.Label(_('Radio'))
+ label_radio.set_alignment(0, 0.5)
+ box_radio.pack_start(label_radio, expand=False)
+ label_radio.show()
+
+ box_wireless.pack_start(box_radio, expand=False)
+ box_radio.show()
+
+ self._radio_alert = InlineAlert()
+ self._radio_alert_box.pack_start(self._radio_alert, expand=False)
+ box_radio.pack_end(self._radio_alert_box, expand=False)
+ self._radio_alert_box.show()
+ if 'radio' in self.restart_alerts:
+ self._radio_alert.props.msg = self.restart_msg
+ self._radio_alert.show()
+
+ history_info = gtk.Label(_("Discard network history if you "
+ "have trouble connecting to the network"))
+ history_info.set_alignment(0, 0)
+ history_info.set_line_wrap(True)
+ history_info.show()
+ box_wireless.pack_start(history_info, expand=False)
+
+ box_clear_history = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._clear_history_button = gtk.Button()
+ self._clear_history_button.set_label(_('Discard network history'))
+ box_clear_history.pack_start(self._clear_history_button, expand=False)
+ self._clear_history_button.show()
+ box_wireless.pack_start(box_clear_history, expand=False)
+ box_clear_history.show()
+
+ self.pack_start(box_wireless, expand=False)
+ box_wireless.show()
+
+ separator_mesh = gtk.HSeparator()
+ self.pack_start(separator_mesh, False)
+ separator_mesh.show()
+
+ label_mesh = gtk.Label(_('Mesh'))
+ label_mesh.set_alignment(0, 0)
+ self.pack_start(label_mesh, expand=False)
+ label_mesh.show()
+ box_mesh = gtk.VBox()
+ box_mesh.set_border_width(style.DEFAULT_SPACING * 2)
+ box_mesh.set_spacing(style.DEFAULT_SPACING)
+
+ box_server = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_server = gtk.Label(_('Server:'))
+ label_server.set_alignment(1, 0.5)
+ label_server.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box_server.pack_start(label_server, expand=False)
+ group.add_widget(label_server)
+ label_server.show()
+ self._entry = gtk.Entry()
+ self._entry.set_alignment(0)
+ self._entry.modify_bg(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._entry.modify_base(gtk.STATE_INSENSITIVE,
+ style.COLOR_WHITE.get_gdk_color())
+ self._entry.set_size_request(int(gtk.gdk.screen_width() / 3), -1)
+ box_server.pack_start(self._entry, expand=False)
+ self._entry.show()
+ box_mesh.pack_start(box_server, expand=False)
+ box_server.show()
+
+ self._jabber_alert = InlineAlert()
+ label_jabber_error = gtk.Label()
+ group.add_widget(label_jabber_error)
+ self._jabber_alert_box.pack_start(label_jabber_error, expand=False)
+ label_jabber_error.show()
+ self._jabber_alert_box.pack_start(self._jabber_alert, expand=False)
+ box_mesh.pack_end(self._jabber_alert_box, expand=False)
+ self._jabber_alert_box.show()
+ if 'jabber' in self.restart_alerts:
+ self._jabber_alert.props.msg = self.restart_msg
+ self._jabber_alert.show()
+
+ self.pack_start(box_mesh, expand=False)
+ box_mesh.show()
+
+ self.setup()
+
+ def setup(self):
+ self._entry.set_text(self._model.get_jabber())
+ try:
+ radio_state = self._model.get_radio()
+ except Exception, detail:
+ self._radio_alert.props.msg = detail
+ self._radio_alert.show()
+ else:
+ self._button.set_active(radio_state)
+
+ self._jabber_valid = True
+ self._radio_valid = True
+ self.needs_restart = False
+ self._radio_change_handler = self._button.connect( \
+ 'toggled', self.__radio_toggled_cb)
+ self._jabber_change_handler = self._entry.connect( \
+ 'changed', self.__jabber_changed_cb)
+ self._network_configuration_reset_handler = \
+ self._clear_history_button.connect( \
+ 'clicked', self.__network_configuration_reset_cb)
+
+ def undo(self):
+ self._button.disconnect(self._radio_change_handler)
+ self._entry.disconnect(self._jabber_change_handler)
+ self._model.undo()
+ self._jabber_alert.hide()
+ self._radio_alert.hide()
+
+ def _validate(self):
+ if self._jabber_valid and self._radio_valid:
+ self.props.is_valid = True
+ else:
+ self.props.is_valid = False
+
+ def __radio_toggled_cb(self, widget, data=None):
+ radio_state = widget.get_active()
+ try:
+ self._model.set_radio(radio_state)
+ except Exception, detail:
+ self._radio_alert.props.msg = detail
+ self._radio_valid = False
+ else:
+ self._radio_valid = True
+
+ self._validate()
+ return False
+
+ def __jabber_changed_cb(self, widget, data=None):
+ if self._jabber_sid:
+ gobject.source_remove(self._jabber_sid)
+ self._jabber_sid = gobject.timeout_add(self._APPLY_TIMEOUT,
+ self.__jabber_timeout_cb, widget)
+
+ def __jabber_timeout_cb(self, widget):
+ self._jabber_sid = 0
+ if widget.get_text() == self._model.get_jabber:
+ return
+ try:
+ self._model.set_jabber(widget.get_text())
+ except ValueError, detail:
+ self._jabber_alert.props.msg = detail
+ self._jabber_valid = False
+ else:
+ self._jabber_alert.props.msg = self.restart_msg
+ self._jabber_valid = True
+ self.needs_restart = True
+ self.restart_alerts.append('jabber')
+
+ self._validate()
+ self._jabber_alert.show()
+ return False
+
+ def __network_configuration_reset_cb(self, widget):
+ self._model.clear_networks()
diff --git a/src/jarabe/controlpanel/power/Makefile.am b/src/jarabe/controlpanel/power/Makefile.am
new file mode 100644
index 0000000..e989e97
--- /dev/null
+++ b/src/jarabe/controlpanel/power/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/controlpanel/power
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/src/jarabe/controlpanel/power/__init__.py b/src/jarabe/controlpanel/power/__init__.py
new file mode 100644
index 0000000..8b2e85f
--- /dev/null
+++ b/src/jarabe/controlpanel/power/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 _
+
+CLASS = 'Power'
+ICON = 'module-power'
+TITLE = _('Power')
+KEYWORDS = ['automatic', 'extreme', 'power', 'suspend', 'battery']
+
diff --git a/src/jarabe/controlpanel/power/model.py b/src/jarabe/controlpanel/power/model.py
new file mode 100644
index 0000000..47af483
--- /dev/null
+++ b/src/jarabe/controlpanel/power/model.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2008 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
+#
+
+from gettext import gettext as _
+
+from sugar import profile
+import dbus
+
+OHM_SERVICE_NAME = 'org.freedesktop.ohm'
+OHM_SERVICE_PATH = '/org/freedesktop/ohm/Keystore'
+OHM_SERVICE_IFACE = 'org.freedesktop.ohm.Keystore'
+
+class ReadError(Exception):
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+def get_automatic_pm():
+ pro = profile.get_profile()
+ ret = pro.automatic_pm
+ return ret
+
+def print_automatic_pm():
+ print ('off', 'on')[get_automatic_pm()]
+
+def set_automatic_pm(enabled):
+ """Automatic suspends on/off."""
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH)
+ keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE)
+
+ if enabled == 'on' or enabled == 1:
+ keystore.SetKey("suspend.automatic_pm", 1)
+ enabled = True
+ elif enabled == 'off' or enabled == 0:
+ keystore.SetKey("suspend.automatic_pm", 0)
+ enabled = False
+ else:
+ raise ValueError(_("Error in automatic pm argument, use on/off."))
+
+ pro = profile.get_profile()
+ pro.automatic_pm = enabled
+ pro.save()
+ return 0
+
+def get_extreme_pm():
+ pro = profile.get_profile()
+ ret = pro.extreme_pm
+ return ret
+
+def print_extreme_pm():
+ print ('off', 'on')[get_extreme_pm()]
+
+def set_extreme_pm(enabled):
+ """Extreme power management on/off."""
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH)
+ keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE)
+
+ if enabled == 'on' or enabled == 1:
+ keystore.SetKey("suspend.extreme_pm", 1)
+ enabled = True
+ elif enabled == 'off' or enabled == 0:
+ keystore.SetKey("suspend.extreme_pm", 0)
+ enabled = False
+ else:
+ raise ValueError(_("Error in extreme pm argument, use on/off."))
+
+ pro = profile.get_profile()
+ pro.extreme_pm = enabled
+ pro.save()
+ return 0
diff --git a/src/jarabe/controlpanel/power/view.py b/src/jarabe/controlpanel/power/view.py
new file mode 100644
index 0000000..8f1ed56
--- /dev/null
+++ b/src/jarabe/controlpanel/power/view.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2008, OLPC
+#
+# 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
+from gettext import gettext as _
+
+from sugar.graphics import style
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+class Power(SectionView):
+ def __init__(self, model, alerts):
+ SectionView.__init__(self)
+
+ self._model = model
+ self.restart_alerts = alerts
+ self._automatic_pm_valid = True
+ self._extreme_pm_valid = True
+ self._extreme_pm_change_handler = None
+ self._automatic_pm_change_handler = None
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+ group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ self._automatic_pm_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._extreme_pm_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+
+ separator_pm = gtk.HSeparator()
+ self.pack_start(separator_pm, expand=False)
+ separator_pm.show()
+
+ label_pm = gtk.Label(_('Power management'))
+ label_pm.set_alignment(0, 0)
+ self.pack_start(label_pm, expand=False)
+ label_pm.show()
+ box_pm = gtk.VBox()
+ box_pm.set_border_width(style.DEFAULT_SPACING * 2)
+ box_pm.set_spacing(style.DEFAULT_SPACING)
+
+ box_automatic_pm = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_automatic_pm = gtk.Label(
+ _('Automatic power management (increases battery life)'))
+ label_automatic_pm.set_alignment(0, 0.5)
+ self._automatic_button = gtk.CheckButton()
+ self._automatic_button.set_alignment(0, 0)
+ box_automatic_pm.pack_start(self._automatic_button, expand=False)
+ box_automatic_pm.pack_start(label_automatic_pm, expand=False)
+ self._automatic_button.show()
+ label_automatic_pm.show()
+ group.add_widget(label_automatic_pm)
+ box_pm.pack_start(box_automatic_pm, expand=False)
+ box_automatic_pm.show()
+
+ self._automatic_pm_alert = InlineAlert()
+ label_automatic_pm_error = gtk.Label()
+ group.add_widget(label_automatic_pm_error)
+ self._automatic_pm_alert_box.pack_start(label_automatic_pm_error,
+ expand=False)
+ label_automatic_pm_error.show()
+ self._automatic_pm_alert_box.pack_start(self._automatic_pm_alert,
+ expand=False)
+ box_pm.pack_end(self._automatic_pm_alert_box, expand=False)
+ self._automatic_pm_alert_box.show()
+ if 'automatic_pm' in self.restart_alerts:
+ self._automatic_pm_alert.props.msg = self.restart_msg
+ self._automatic_pm_alert.show()
+
+ box_extreme_pm = gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label_extreme_pm = gtk.Label(
+ _('Extreme power management (disables' \
+ 'wireless radio, increases battery life)'))
+ label_extreme_pm.set_alignment(0, 0.5)
+ self._extreme_button = gtk.CheckButton()
+ self._extreme_button.set_alignment(0, 0)
+ box_extreme_pm.pack_start(self._extreme_button, expand=False)
+ self._extreme_button.show()
+ box_extreme_pm.pack_start(label_extreme_pm, expand=False)
+ group.add_widget(label_extreme_pm)
+ label_extreme_pm.show()
+ box_pm.pack_start(box_extreme_pm, expand=False)
+ box_extreme_pm.show()
+
+ self._extreme_pm_alert = InlineAlert()
+ label_extreme_pm_error = gtk.Label()
+ group.add_widget(label_extreme_pm_error)
+ self._extreme_pm_alert_box.pack_start(label_extreme_pm_error,
+ expand=False)
+ label_extreme_pm_error.show()
+ self._extreme_pm_alert_box.pack_start(self._extreme_pm_alert,
+ expand=False)
+ box_pm.pack_end(self._extreme_pm_alert_box, expand=False)
+ self._extreme_pm_alert_box.show()
+ if 'extreme_pm' in self.restart_alerts:
+ self._extreme_pm_alert.props.msg = self.restart_msg
+ self._extreme_pm_alert.show()
+
+ self.pack_start(box_pm, expand=False)
+ box_pm.show()
+
+ self.setup()
+
+ def setup(self):
+ try:
+ automatic_state = self._model.get_automatic_pm()
+ extreme_state = self._model.get_extreme_pm()
+
+ except Exception, detail:
+ self._automatic_pm_alert.props.msg = detail
+ self._automatic_pm_alert.show()
+
+ self._extreme_pm_alert.props.msg = detail
+ self._extreme_pm_alert.show()
+ else:
+ self._automatic_button.set_active(automatic_state)
+ self._extreme_button.set_active(extreme_state)
+
+ self._extreme_pm_valid = True
+ self._automatic_pm_valid = True
+ self.needs_restart = False
+ self._automatic_pm_change_handler = self._automatic_button.connect( \
+ 'toggled', self.__automatic_pm_toggled_cb)
+ self._extreme_pm_change_handler = self._extreme_button.connect( \
+ 'toggled', self.__extreme_pm_toggled_cb)
+
+ def undo(self):
+ self._automatic_button.disconnect(self._automatic_pm_change_handler)
+ self._extreme_button.disconnect(self._extreme_pm_change_handler)
+ self._model.undo()
+ self._extreme_pm_alert.hide()
+ self._automatic_pm_alert.hide()
+
+ def _validate(self):
+ if self._extreme_pm_valid and self._automatic_pm_valid:
+ self.props.is_valid = True
+ else:
+ self.props.is_valid = False
+
+ def __automatic_pm_toggled_cb(self, widget, data=None):
+ state = widget.get_active()
+ try:
+ self._model.set_automatic_pm(state)
+ except Exception, detail:
+ print detail
+ self._automatic_pm_alert.props.msg = detail
+ else:
+ self._automatic_pm_valid = True
+
+ self._validate()
+ return False
+
+ def __extreme_pm_toggled_cb(self, widget, data=None):
+ state = widget.get_active()
+ try:
+ self._model.set_extreme_pm(state)
+ except Exception, detail:
+ print detail
+ self._extreme_pm_alert.props.msg = detail
+ else:
+ self._extreme_pm_valid = True
+
+ self._validate()
+ return False
diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py
new file mode 100644
index 0000000..7b76aa1
--- /dev/null
+++ b/src/jarabe/controlpanel/sectionview.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2008, OLPC
+#
+# 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 gobject
+import gtk
+from gettext import gettext as _
+
+class SectionView(gtk.VBox):
+ __gtype_name__ = 'SugarSectionView'
+
+ __gsignals__ = {
+ 'request-close': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ __gproperties__ = {
+ 'is_valid' : (bool, None, None, True,
+ gobject.PARAM_READWRITE)
+ }
+
+ _APPLY_TIMEOUT = 1000
+
+ def __init__(self):
+ gtk.VBox.__init__(self)
+ self._is_valid = True
+ self.auto_close = False
+ self.needs_restart = False
+ self.restart_alerts = []
+ self.restart_msg = _('Changes require restart')
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'is-valid':
+ if self._is_valid != value:
+ self._is_valid = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'is-valid':
+ return self._is_valid
+
+ def undo(self):
+ '''Undo here the changes that have been made in this section.'''
+ pass
diff --git a/src/jarabe/controlpanel/toolbar.py b/src/jarabe/controlpanel/toolbar.py
new file mode 100644
index 0000000..6bb8328
--- /dev/null
+++ b/src/jarabe/controlpanel/toolbar.py
@@ -0,0 +1,157 @@
+# Copyright (C) 2007, 2008 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 gtk
+import gettext
+import gobject
+
+_ = lambda msg: gettext.dgettext('sugar', msg)
+
+from sugar.graphics.icon import Icon
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics import iconentry
+from sugar.graphics import style
+
+class MainToolbar(gtk.Toolbar):
+ """ Main toolbar of the control panel
+ """
+ __gtype_name__ = 'MainToolbar'
+
+ __gsignals__ = {
+ 'stop-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'search-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+ self._search_entry = iconentry.IconEntry()
+ self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._search_entry.add_clear_button()
+ self._search_entry.set_width_chars(25)
+ self._search_entry.connect('changed', self.__search_entry_changed_cb)
+ tool_item.add(self._search_entry)
+ self._search_entry.show()
+
+ self._add_separator(True)
+
+ self.stop = ToolButton(icon_name='dialog-cancel')
+ self.stop.set_tooltip(_('Done'))
+ self.stop.connect('clicked', self.__stop_clicked_cb)
+ self.stop.show()
+ self.insert(self.stop, -1)
+ self.stop.show()
+
+ def get_entry(self):
+ return self._search_entry
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.DEFAULT_SPACING, -1)
+ self.insert(separator, -1)
+ separator.show()
+
+ def __search_entry_changed_cb(self, search_entry):
+ self.emit('search-changed', search_entry.props.text)
+
+ def __stop_clicked_cb(self, button):
+ self.emit('stop-clicked')
+
+class SectionToolbar(gtk.Toolbar):
+ """ Toolbar of the sections of the control panel
+ """
+ __gtype_name__ = 'SectionToolbar'
+
+ __gsignals__ = {
+ 'cancel-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'accept-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._add_separator()
+
+ self._icon = Icon()
+ self._add_widget(self._icon)
+
+ self._add_separator()
+
+ self._title = gtk.Label()
+ self._add_widget(self._title)
+
+ self._add_separator(True)
+
+ self.cancel_button = ToolButton('dialog-cancel')
+ self.cancel_button.set_tooltip(_('Cancel'))
+ self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb)
+ self.insert(self.cancel_button, -1)
+ self.cancel_button.show()
+
+ self.accept_button = ToolButton('dialog-ok')
+ self.accept_button.set_tooltip(_('Ok'))
+ self.accept_button.connect('clicked', self.__accept_button_clicked_cb)
+ self.insert(self.accept_button, -1)
+ self.accept_button.show()
+
+ def get_icon(self):
+ return self._icon
+
+ def get_title(self):
+ return self._title
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.DEFAULT_SPACING, -1)
+ self.insert(separator, -1)
+ separator.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def __cancel_button_clicked_cb(self, widget, data=None):
+ self.emit('cancel-clicked')
+
+ def __accept_button_clicked_cb(self, widget, data=None):
+ self.emit('accept-clicked')
+
diff --git a/src/jarabe/emulator.py b/src/jarabe/emulator.py
new file mode 100755
index 0000000..ac1f4e2
--- /dev/null
+++ b/src/jarabe/emulator.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2006, 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 os
+import logging
+import subprocess
+import time
+from optparse import OptionParser
+
+log = logging.getLogger( 'sugar-emulator' )
+log.setLevel( logging.DEBUG )
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+
+from sugar import env
+
+def _run_xephyr(display, dpi):
+ log.info('Starting Xephyr on display %s', display)
+
+ cmd = [ 'Xephyr' ]
+ cmd.append(':%d' % display)
+ cmd.append('-ac')
+
+ if gtk.gdk.screen_width() < 1200 or gtk.gdk.screen_height() < 900:
+ cmd.append('-fullscreen')
+ else:
+ cmd.append('-screen')
+ cmd.append('%dx%d' % (1200, 900))
+
+ if not dpi:
+ dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') / 1024
+ if dpi > 0:
+ cmd.append('-dpi')
+ cmd.append('%d' % dpi)
+
+ log.debug('Xephyr command: %s', " ".join( cmd ))
+ result = gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH)
+ pid = result[0]
+
+ os.environ['DISPLAY'] = ":%d" % (display)
+ os.environ['SUGAR_EMULATOR_PID'] = str(pid)
+
+def _check_xephyr(display):
+ result = subprocess.call(['xdpyinfo', '-display', ':%d' % display],
+ stdout=open(os.devnull, "w"),
+ stderr=open(os.devnull, "w"))
+ return result == 0
+
+def _start_xephyr(dpi=None):
+ for display in range(100, 110):
+ if not _check_xephyr(display):
+ _run_xephyr(display, dpi)
+
+ tries = 10
+ while tries > 0:
+ if _check_xephyr(display):
+ return
+ else:
+ tries -= 1
+ time.sleep(0.1)
+
+def _start_matchbox():
+ log.info('Starting the matchbox window manager')
+ cmd = ['matchbox-window-manager']
+
+ cmd.extend(['-use_titlebar', 'no'])
+ cmd.extend(['-theme', 'sugar'])
+
+ log.debug('Matchbox command: %s', " ".join( cmd))
+ gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH)
+
+def _setup_env():
+ os.environ['SUGAR_EMULATOR'] = 'yes'
+ os.environ['GABBLE_LOGFILE'] = os.path.join(
+ env.get_profile_path(), 'logs', 'telepathy-gabble.log')
+ os.environ['SALUT_LOGFILE'] = os.path.join(
+ env.get_profile_path(), 'logs', 'telepathy-salut.log')
+ os.environ['STREAM_ENGINE_LOGFILE'] = os.path.join(
+ env.get_profile_path(), 'logs', 'telepathy-stream-engine.log')
+
+def main():
+ """Script-level operations"""
+
+ parser = OptionParser()
+ parser.add_option('-x', '--xo-style', dest='xo_style',
+ action='store_true', help='use the XO style')
+ (options, args) = parser.parse_args()
+
+ logging.basicConfig()
+
+ _setup_env()
+
+ if options.xo_style:
+ _start_xephyr(dpi=201)
+ else:
+ _start_xephyr()
+
+ if options.xo_style:
+ os.environ['SUGAR_THEME'] = 'sugar-xo'
+ os.environ['SUGAR_XO_STYLE'] = 'yes'
+ else:
+ os.environ['SUGAR_XO_STYLE'] = 'no'
+
+ command = ['dbus-launch', 'dbus-launch', '--exit-with-session']
+
+ if not args:
+ command.append('sugar-shell')
+ else:
+ _start_matchbox()
+
+ if args[0].endswith('.py'):
+ command.append('python')
+
+ command.append(args[0])
+
+ log.info("Attempting to launch sugar to replace this process: %s"
+ % " ".join(command))
+ os.execlp( *command )
diff --git a/src/jarabe/hardware/Makefile.am b/src/jarabe/hardware/Makefile.am
new file mode 100644
index 0000000..6684848
--- /dev/null
+++ b/src/jarabe/hardware/Makefile.am
@@ -0,0 +1,13 @@
+sugardir = $(pythondir)/jarabe/hardware
+sugar_PYTHON = \
+ __init__.py \
+ hardwaremanager.py \
+ keydialog.py \
+ nmclient.py \
+ nminfo.py \
+ schoolserver.py
+
+dbusservicedir = $(sysconfdir)/dbus-1/system.d/
+dbusservice_DATA = NetworkManagerInfo.conf
+
+EXTRA_DIST = $(dbusservice_DATA)
diff --git a/src/jarabe/hardware/NetworkManagerInfo.conf b/src/jarabe/hardware/NetworkManagerInfo.conf
new file mode 100644
index 0000000..4fb8270
--- /dev/null
+++ b/src/jarabe/hardware/NetworkManagerInfo.conf
@@ -0,0 +1,26 @@
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <policy user="root">
+ <allow own="org.freedesktop.NetworkManagerInfo"/>
+
+ <allow send_destination="org.freedesktop.NetworkManagerInfo"/>
+ <allow send_interface="org.freedesktop.NetworkManagerInfo"/>
+ </policy>
+ <policy at_console="true">
+ <allow own="org.freedesktop.NetworkManagerInfo"/>
+
+ <allow send_destination="org.freedesktop.NetworkManagerInfo"/>
+ <allow send_interface="org.freedesktop.NetworkManagerInfo"/>
+ </policy>
+ <policy context="default">
+ <deny own="org.freedesktop.NetworkManagerInfo"/>
+
+ <deny send_destination="org.freedesktop.NetworkManagerInfo"/>
+ <deny send_interface="org.freedesktop.NetworkManagerInfo"/>
+ </policy>
+
+ <limit name="max_replies_per_connection">512</limit>
+</busconfig>
+
diff --git a/src/jarabe/hardware/__init__.py b/src/jarabe/hardware/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/hardware/__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/jarabe/hardware/hardwaremanager.py b/src/jarabe/hardware/hardwaremanager.py
new file mode 100644
index 0000000..22cb1db
--- /dev/null
+++ b/src/jarabe/hardware/hardwaremanager.py
@@ -0,0 +1,123 @@
+# 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 dbus
+import gobject
+
+from jarabe.hardware.nmclient import NMClient
+from sugar.profile import get_profile
+from sugar import env
+from sugar import _sugarext
+
+_HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore'
+_HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm'
+_HARDWARE_MANAGER_OBJECT_PATH = '/org/freedesktop/ohm/Keystore'
+
+COLOR_MODE = 0
+B_AND_W_MODE = 1
+
+VOL_CHANGE_INCREMENT_RECOMMENDATION = 10
+
+class HardwareManager(gobject.GObject):
+ __gsignals__ = {
+ 'muted-changed' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN])),
+ 'volume-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_INT, gobject.TYPE_INT])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE,
+ _HARDWARE_MANAGER_OBJECT_PATH,
+ follow_name_owner_changes=True)
+ self._service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE)
+
+ self._volume = _sugarext.VolumeAlsa()
+
+ def get_muted(self):
+ return self._volume.get_mute()
+
+ def get_volume(self):
+ return self._volume.get_volume()
+
+ def set_volume(self, new_volume):
+ old_volume = self._volume.get_volume()
+ self._volume.set_volume(new_volume)
+
+ self.emit('volume-changed', old_volume, new_volume)
+
+ def set_muted(self, new_state):
+ old_state = self._volume.get_mute()
+ self._volume.set_mute(new_state)
+
+ self.emit('muted-changed', old_state, new_state)
+
+ def startup(self):
+ if env.is_emulator() is False:
+ profile = get_profile()
+ self.set_volume(profile.sound_volume)
+
+ def shutdown(self):
+ if env.is_emulator() is False:
+ profile = get_profile()
+ profile.sound_volume = self.get_volume()
+ profile.save()
+
+ def set_dcon_freeze(self, frozen):
+ try:
+ self._service.SetKey("display.dcon_freeze", frozen)
+ except dbus.DBusException:
+ logging.error('Cannot unfreeze the DCON')
+
+ def set_display_mode(self, mode):
+ try:
+ self._service.SetKey("display.dcon_mode", mode)
+ except dbus.DBusException:
+ logging.error('Cannot change DCON mode')
+
+ def set_display_brightness(self, level):
+ try:
+ self._service.SetKey("backlight.hardware_brightness", level)
+ except dbus.DBusException:
+ logging.error('Cannot set display brightness')
+
+ def get_display_brightness(self):
+ try:
+ return self._service.GetKey("backlight.hardware_brightness")
+ except dbus.DBusException:
+ logging.error('Cannot get display brightness')
+ return 0
+
+def get_manager():
+ return _manager
+
+def get_network_manager():
+ return _network_manager
+
+_manager = HardwareManager()
+
+try:
+ _network_manager = NMClient()
+except dbus.DBusException, e:
+ _network_manager = None
+ logging.info('Network manager service not found.')
diff --git a/src/jarabe/hardware/keydialog.py b/src/jarabe/hardware/keydialog.py
new file mode 100644
index 0000000..e70e12b
--- /dev/null
+++ b/src/jarabe/hardware/keydialog.py
@@ -0,0 +1,354 @@
+# vi: ts=4 ai noet
+#
+# 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 md5
+from gettext import gettext as _
+
+import gtk
+
+IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001
+IW_AUTH_ALG_SHARED_KEY = 0x00000002
+
+IW_AUTH_WPA_VERSION_DISABLED = 0x00000001
+IW_AUTH_WPA_VERSION_WPA = 0x00000002
+IW_AUTH_WPA_VERSION_WPA2 = 0x00000004
+
+NM_802_11_CAP_NONE = 0x00000000
+NM_802_11_CAP_PROTO_NONE = 0x00000001
+NM_802_11_CAP_PROTO_WEP = 0x00000002
+NM_802_11_CAP_PROTO_WPA = 0x00000004
+NM_802_11_CAP_PROTO_WPA2 = 0x00000008
+NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040
+NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080
+NM_802_11_CAP_CIPHER_WEP40 = 0x00001000
+NM_802_11_CAP_CIPHER_WEP104 = 0x00002000
+NM_802_11_CAP_CIPHER_TKIP = 0x00004000
+NM_802_11_CAP_CIPHER_CCMP = 0x00008000
+
+NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000
+IW_AUTH_CIPHER_NONE = 0x00000001
+IW_AUTH_CIPHER_WEP40 = 0x00000002
+IW_AUTH_CIPHER_TKIP = 0x00000004
+IW_AUTH_CIPHER_CCMP = 0x00000008
+IW_AUTH_CIPHER_WEP104 = 0x00000010
+
+IW_AUTH_KEY_MGMT_802_1X = 0x1
+IW_AUTH_KEY_MGMT_PSK = 0x2
+
+def string_is_hex(key):
+ is_hex = True
+ for c in key:
+ if not 'a' <= c.lower() <= 'f' and not '0' <= c <= '9':
+ is_hex = False
+ return is_hex
+
+def string_is_ascii(string):
+ try:
+ string.encode('ascii')
+ return True
+ except UnicodeEncodeError:
+ return False
+
+def string_to_hex(passphrase):
+ key = ''
+ for c in passphrase:
+ key += '%02x' % ord(c)
+ return key
+
+def hash_passphrase(passphrase):
+ # passphrase must have a length of 64
+ if len(passphrase) > 64:
+ passphrase = passphrase[:64]
+ elif len(passphrase) < 64:
+ while len(passphrase) < 64:
+ passphrase += passphrase[:64 - len(passphrase)]
+ passphrase = md5.new(passphrase).digest()
+ return string_to_hex(passphrase)[:26]
+
+class KeyDialog(gtk.Dialog):
+ def __init__(self, net, async_cb, async_err_cb):
+ gtk.Dialog.__init__(self, flags=gtk.DIALOG_MODAL)
+ self.set_title("Wireless Key Required")
+
+ self._net = net
+ self._async_cb = async_cb
+ self._async_err_cb = async_err_cb
+ self._entry = None
+
+ self.set_has_separator(False)
+
+ label = gtk.Label("A wireless encryption key is required for\n" \
+ " the wireless network '%s'." % net.get_ssid())
+ self.vbox.pack_start(label)
+
+ self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OK, gtk.RESPONSE_OK)
+ self.set_default_response(gtk.RESPONSE_OK)
+ self.set_has_separator(True)
+
+ def add_key_entry(self):
+ self._entry = gtk.Entry()
+ #self._entry.props.visibility = False
+ self._entry.connect('changed', self._update_response_sensitivity)
+ self._entry.connect('activate', self._entry_activate_cb)
+ self.vbox.pack_start(self._entry)
+ self.vbox.set_spacing(6)
+ self.vbox.show_all()
+
+ self._update_response_sensitivity()
+ self._entry.grab_focus()
+
+ def _entry_activate_cb(self, entry):
+ self.response(gtk.RESPONSE_OK)
+
+ def create_security(self):
+ raise NotImplementedError
+
+ def get_network(self):
+ return self._net
+
+ def get_callbacks(self):
+ return (self._async_cb, self._async_err_cb)
+
+WEP_PASSPHRASE = 1
+WEP_HEX = 2
+WEP_ASCII = 3
+
+class WEPKeyDialog(KeyDialog):
+ def __init__(self, net, async_cb, async_err_cb):
+ KeyDialog.__init__(self, net, async_cb, async_err_cb)
+
+ # WEP key type
+ self.key_store = gtk.ListStore(str, int)
+ self.key_store.append(["Passphrase (128-bit)", WEP_PASSPHRASE])
+ self.key_store.append(["Hex (40/128-bit)", WEP_HEX])
+ self.key_store.append(["ASCII (40/128-bit)", WEP_ASCII])
+
+ self.key_combo = gtk.ComboBox(self.key_store)
+ cell = gtk.CellRendererText()
+ self.key_combo.pack_start(cell, True)
+ self.key_combo.add_attribute(cell, 'text', 0)
+ self.key_combo.set_active(0)
+ self.key_combo.connect('changed', self._key_combo_changed_cb)
+
+ hbox = gtk.HBox()
+ hbox.pack_start(gtk.Label(_("Key Type:")))
+ hbox.pack_start(self.key_combo)
+ hbox.show_all()
+ self.vbox.pack_start(hbox)
+
+ # Key entry field
+ self.add_key_entry()
+
+ # WEP authentication mode
+ self.auth_store = gtk.ListStore(str, int)
+ self.auth_store.append(["Open System", IW_AUTH_ALG_OPEN_SYSTEM])
+ self.auth_store.append(["Shared Key", IW_AUTH_ALG_SHARED_KEY])
+
+ self.auth_combo = gtk.ComboBox(self.auth_store)
+ cell = gtk.CellRendererText()
+ self.auth_combo.pack_start(cell, True)
+ self.auth_combo.add_attribute(cell, 'text', 0)
+ self.auth_combo.set_active(0)
+
+ hbox = gtk.HBox()
+ hbox.pack_start(gtk.Label(_("Authentication Type:")))
+ hbox.pack_start(self.auth_combo)
+ hbox.show_all()
+
+ self.vbox.pack_start(hbox)
+
+ def _key_combo_changed_cb(self, widget):
+ self._update_response_sensitivity()
+
+ def _get_security(self):
+ key = self._entry.get_text()
+
+ it = self.key_combo.get_active_iter()
+ (key_type, ) = self.key_store.get(it, 1)
+
+ if key_type == WEP_PASSPHRASE:
+ key = hash_passphrase(key)
+ elif key_type == WEP_ASCII:
+ key = string_to_hex(key)
+
+ it = self.auth_combo.get_active_iter()
+ (auth_alg, ) = self.auth_store.get(it, 1)
+
+ we_cipher = None
+ if len(key) == 26:
+ we_cipher = IW_AUTH_CIPHER_WEP104
+ elif len(key) == 10:
+ we_cipher = IW_AUTH_CIPHER_WEP40
+
+ return (we_cipher, key, auth_alg)
+
+ def print_security(self):
+ (we_cipher, key, auth_alg) = self._get_security()
+ print "Cipher: %d" % we_cipher
+ print "Key: %s" % key
+ print "Auth: %d" % auth_alg
+
+ def create_security(self):
+ (we_cipher, key, auth_alg) = self._get_security()
+ from jarabe.hardware.nminfo import Security
+ return Security.new_from_args(we_cipher, (key, auth_alg))
+
+ def _update_response_sensitivity(self, ignored=None):
+ key = self._entry.get_text()
+ it = self.key_combo.get_active_iter()
+ (key_type, ) = self.key_store.get(it, 1)
+
+ valid = False
+ if key_type == WEP_PASSPHRASE:
+ # As the md5 passphrase can be of any length and has no indicator,
+ # we cannot check for the validity of the input.
+ if len(key) > 0:
+ valid = True
+ elif key_type == WEP_ASCII:
+ if len(key) == 5 or len(key) == 13:
+ valid = string_is_ascii(key)
+ elif key_type == WEP_HEX:
+ if len(key) == 10 or len(key) == 26:
+ valid = string_is_hex(key)
+
+ self.set_response_sensitive(gtk.RESPONSE_OK, valid)
+
+class WPAKeyDialog(KeyDialog):
+ def __init__(self, net, async_cb, async_err_cb):
+ KeyDialog.__init__(self, net, async_cb, async_err_cb)
+ self.add_key_entry()
+
+ self.store = gtk.ListStore(str, int)
+ self.store.append(["Automatic", NM_AUTH_TYPE_WPA_PSK_AUTO])
+ if net.get_caps() & NM_802_11_CAP_CIPHER_CCMP:
+ self.store.append(["AES-CCMP", IW_AUTH_CIPHER_CCMP])
+ if net.get_caps() & NM_802_11_CAP_CIPHER_TKIP:
+ self.store.append(["TKIP", IW_AUTH_CIPHER_TKIP])
+
+ self.combo = gtk.ComboBox(self.store)
+ cell = gtk.CellRendererText()
+ self.combo.pack_start(cell, True)
+ self.combo.add_attribute(cell, 'text', 0)
+ self.combo.set_active(0)
+
+ self.hbox = gtk.HBox()
+ self.hbox.pack_start(gtk.Label(_("Encryption Type:")))
+ self.hbox.pack_start(self.combo)
+ self.hbox.show_all()
+
+ self.vbox.pack_start(self.hbox)
+
+ def _get_security(self):
+ ssid = self.get_network().get_ssid()
+ key = self._entry.get_text()
+ is_hex = string_is_hex(key)
+
+ real_key = None
+ if len(key) == 64 and is_hex:
+ # Hex key
+ real_key = key
+ elif len(key) >= 8 and len(key) <= 63:
+ # passphrase
+ from subprocess import Popen, PIPE
+ p = Popen(['/usr/sbin/wpa_passphrase', ssid, key], stdout=PIPE)
+ for line in p.stdout:
+ if line.strip().startswith("psk="):
+ real_key = line.strip()[4:]
+ if p.wait() != 0:
+ raise RuntimeError("Error hashing passphrase")
+ if real_key and len(real_key) != 64:
+ real_key = None
+
+ if not real_key:
+ raise RuntimeError("Invalid key")
+
+ it = self.combo.get_active_iter()
+ (we_cipher, ) = self.store.get(it, 1)
+
+ wpa_ver = IW_AUTH_WPA_VERSION_WPA
+ caps = self.get_network().get_caps()
+ if caps & NM_802_11_CAP_PROTO_WPA2:
+ wpa_ver = IW_AUTH_WPA_VERSION_WPA2
+
+ return (we_cipher, real_key, wpa_ver)
+
+ def print_security(self):
+ (we_cipher, key, wpa_ver) = self._get_security()
+ print "Cipher: %d" % we_cipher
+ print "Key: %s" % key
+ print "WPA Ver: %d" % wpa_ver
+
+ def create_security(self):
+ (we_cipher, key, wpa_ver) = self._get_security()
+ from jarabe.hardware.nminfo import Security
+ return Security.new_from_args(we_cipher,
+ (key, wpa_ver, IW_AUTH_KEY_MGMT_PSK))
+
+ def _update_response_sensitivity(self, ignored=None):
+ key = self._entry.get_text()
+ is_hex = string_is_hex(key)
+
+ valid = False
+ if len(key) == 64 and is_hex:
+ # hex key
+ valid = True
+ elif len(key) >= 8 and len(key) <= 63:
+ # passphrase
+ valid = True
+ self.set_response_sensitive(gtk.RESPONSE_OK, valid)
+ return False
+
+def new_key_dialog(net, async_cb, async_err_cb):
+ caps = net.get_caps()
+ if (caps & NM_802_11_CAP_CIPHER_TKIP or caps & NM_802_11_CAP_CIPHER_CCMP) \
+ and (caps & NM_802_11_CAP_PROTO_WPA or \
+ caps & NM_802_11_CAP_PROTO_WPA2):
+ return WPAKeyDialog(net, async_cb, async_err_cb)
+ elif (caps & NM_802_11_CAP_CIPHER_WEP40 or \
+ caps & NM_802_11_CAP_CIPHER_WEP104) and \
+ (caps & NM_802_11_CAP_PROTO_WEP):
+ return WEPKeyDialog(net, async_cb, async_err_cb)
+ else:
+ raise RuntimeError("Unhandled network capabilities %x" % caps)
+
+
+
+class FakeNet(object):
+ def get_ssid(self):
+ return "olpcwpa"
+
+ def get_caps(self):
+ return NM_802_11_CAP_CIPHER_CCMP | NM_802_11_CAP_CIPHER_TKIP | \
+ NM_802_11_CAP_PROTO_WPA
+
+def response_cb(widget, response_id):
+ if response_id == gtk.RESPONSE_OK:
+ print dialog.print_security()
+ else:
+ print "canceled"
+ widget.hide()
+ widget.destroy()
+
+
+if __name__ == "__main__":
+ fake_net = FakeNet()
+ dialog = new_key_dialog(fake_net, None, None)
+ dialog.connect("response", response_cb)
+ dialog.run()
+
diff --git a/src/jarabe/hardware/nmclient.py b/src/jarabe/hardware/nmclient.py
new file mode 100644
index 0000000..b1921aa
--- /dev/null
+++ b/src/jarabe/hardware/nmclient.py
@@ -0,0 +1,759 @@
+#
+# 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 dbus
+import dbus.glib
+import dbus.decorators
+import gobject
+
+from jarabe.hardware import nminfo
+from sugar.graphics import xocolor
+
+IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001
+IW_AUTH_ALG_SHARED_KEY = 0x00000002
+
+NM_DEVICE_STAGE_STRINGS = ("Unknown",
+ "Prepare",
+ "Config",
+ "Need Users Key",
+ "IP Config",
+ "IP Config Get",
+ "IP Config Commit",
+ "Activated",
+ "Failed",
+ "Canceled"
+ )
+
+NM_SERVICE = 'org.freedesktop.NetworkManager'
+NM_IFACE = 'org.freedesktop.NetworkManager'
+NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices'
+NM_PATH = '/org/freedesktop/NetworkManager'
+
+DEVICE_TYPE_UNKNOWN = 0
+DEVICE_TYPE_802_3_ETHERNET = 1
+DEVICE_TYPE_802_11_WIRELESS = 2
+DEVICE_TYPE_802_11_MESH_OLPC = 3
+
+NM_DEVICE_CAP_NONE = 0x00000000
+NM_DEVICE_CAP_NM_SUPPORTED = 0x00000001
+NM_DEVICE_CAP_CARRIER_DETECT = 0x00000002
+NM_DEVICE_CAP_WIRELESS_SCAN = 0x00000004
+
+sys_bus = dbus.SystemBus()
+
+NM_802_11_CAP_NONE = 0x00000000
+NM_802_11_CAP_PROTO_NONE = 0x00000001
+NM_802_11_CAP_PROTO_WEP = 0x00000002
+NM_802_11_CAP_PROTO_WPA = 0x00000004
+NM_802_11_CAP_PROTO_WPA2 = 0x00000008
+NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040
+NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080
+NM_802_11_CAP_CIPHER_WEP40 = 0x00001000
+NM_802_11_CAP_CIPHER_WEP104 = 0x00002000
+NM_802_11_CAP_CIPHER_TKIP = 0x00004000
+NM_802_11_CAP_CIPHER_CCMP = 0x00008000
+
+NETWORK_STATE_CONNECTING = 0
+NETWORK_STATE_CONNECTED = 1
+NETWORK_STATE_NOTCONNECTED = 2
+
+DEVICE_STATE_ACTIVATING = 0
+DEVICE_STATE_ACTIVATED = 1
+DEVICE_STATE_INACTIVE = 2
+
+IW_MODE_ADHOC = 1
+IW_MODE_INFRA = 2
+
+class Network(gobject.GObject):
+ __gsignals__ = {
+ 'initialized' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_BOOLEAN])),
+ 'strength-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'state-changed' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self, client, op):
+ gobject.GObject.__init__(self)
+ self._client = client
+ self._op = op
+ self._ssid = None
+ self._mode = None
+ self._strength = 0
+ self._caps = 0
+ self._valid = False
+ self._favorite = False
+ self._state = NETWORK_STATE_NOTCONNECTED
+
+ obj = sys_bus.get_object(NM_SERVICE, self._op)
+ net = dbus.Interface(obj, NM_IFACE_DEVICES)
+ net.getProperties(reply_handler=self._update_reply_cb,
+ error_handler=self._update_error_cb)
+
+ def _update_reply_cb(self, *props):
+ self._ssid = props[1]
+ self._strength = props[3]
+ self._mode = props[6]
+ self._caps = props[7]
+ if self._caps & NM_802_11_CAP_PROTO_WPA or \
+ self._caps & NM_802_11_CAP_PROTO_WPA2:
+ if not (self._caps & NM_802_11_CAP_KEY_MGMT_PSK):
+ # 802.1x is not supported at this time
+ logging.debug("Net(%s): ssid '%s' dropping because 802.1x" \
+ "is unsupported" % (self._op, self._ssid))
+ self._valid = False
+ self.emit('initialized', self._valid)
+ return
+ if self._mode != IW_MODE_INFRA:
+ # Don't show Ad-Hoc networks; they usually don't DHCP and therefore
+ # won't work well here. This also works around the bug where
+ # we show our own mesh SSID on the Mesh view when in mesh mode
+ logging.debug("Net(%s): ssid '%s' is adhoc; not showing" %
+ (self._op, self._ssid))
+ self._valid = False
+ self.emit('initialized', self._valid)
+ return
+
+ fav_nets = []
+ if self._client.nminfo:
+ fav_nets = self._client.nminfo.get_networks(
+ nminfo.NETWORK_TYPE_ALLOWED)
+ if self._ssid in fav_nets:
+ self._favorite = True
+
+ self._valid = True
+ logging.debug("Net(%s): caps 0x%X" % (self._ssid, self._caps))
+ self.emit('initialized', self._valid)
+
+ def _update_error_cb(self, err):
+ logging.debug("Net(%s): failed to update. (%s)" % (self._op, err))
+ self._valid = False
+ self.emit('initialized', self._valid)
+
+ def get_colors(self):
+ import sha
+ sh = sha.new()
+ data = self._ssid + hex(self._caps) + hex(self._mode)
+ sh.update(data)
+ h = hash(sh.digest())
+ idx = h % len(xocolor.colors)
+ # stroke, fill
+ return (xocolor.colors[idx][0], xocolor.colors[idx][1])
+
+ def get_ssid(self):
+ return self._ssid
+
+ def get_caps(self):
+ return self._caps
+
+ def get_mode(self):
+ return self._mode
+
+ def get_state(self):
+ return self._state
+
+ def set_state(self, state):
+ if state == self._state:
+ return
+ self._state = state
+ if self._valid:
+ self.emit('state-changed')
+
+ def get_op(self):
+ return self._op
+
+ def get_strength(self):
+ return self._strength
+
+ def set_strength(self, strength):
+ if strength == self._strength:
+ return
+ self._strength = strength
+ if self._valid:
+ self.emit('strength-changed')
+
+ def is_valid(self):
+ return self._valid
+
+ def is_favorite(self):
+ return self._favorite
+
+class Device(gobject.GObject):
+ __gsignals__ = {
+ 'initialized': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'init-failed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'ssid-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'strength-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'state-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'activation-stage-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'network-appeared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'network-disappeared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self, client, op):
+ gobject.GObject.__init__(self)
+ self._client = client
+ self._op = op
+ self._iface = None
+ self._type = DEVICE_TYPE_UNKNOWN
+ self._udi = None
+ self._active = False
+ self._act_stage = 0
+ self._strength = 0
+ self._freq = 0.0
+ self._link = False
+ self._valid = False
+ self._networks = {}
+ self._caps = 0
+ self._state = DEVICE_STATE_INACTIVE
+ self._active_network = None
+ self._active_net_sigid = 0
+
+ obj = sys_bus.get_object(NM_SERVICE, self._op)
+ self.dev = dbus.Interface(obj, NM_IFACE_DEVICES)
+ self.dev.getProperties(reply_handler=self._update_reply_cb,
+ error_handler=self._update_error_cb)
+
+ def _is_activating(self):
+ if self._active and self._act_stage >= 1 and self._act_stage <= 6:
+ return True
+ return False
+
+ def _is_activated(self):
+ if self._active and self._act_stage == 7:
+ return True
+ return False
+
+ def _update_reply_cb(self, *props):
+ self._iface = props[1]
+ self._type = props[2]
+ self._udi = props[3]
+ self._active = props[4]
+ self._act_stage = props[5]
+ self._link = props[15]
+ self._caps = props[17]
+
+ if self._type == DEVICE_TYPE_802_11_WIRELESS:
+ old_strength = self._strength
+ self._strength = props[14]
+ if self._strength != old_strength:
+ if self._valid:
+ self.emit('strength-changed')
+ self._update_networks(props[20], props[19])
+ elif self._type == DEVICE_TYPE_802_11_MESH_OLPC:
+ old_strength = self._strength
+ self._strength = props[14]
+ if self._strength != old_strength:
+ if self._valid:
+ self.emit('strength-changed')
+
+ self._valid = True
+
+ if self._is_activating():
+ self.set_state(DEVICE_STATE_ACTIVATING)
+ elif self._is_activated():
+ self.set_state(DEVICE_STATE_ACTIVATED)
+ else:
+ self.set_state(DEVICE_STATE_INACTIVE)
+
+ self.emit('initialized')
+
+ def _update_networks(self, net_ops, active_op):
+ for op in net_ops:
+ net = Network(self._client, op)
+ self._networks[op] = net
+ net.connect('initialized', lambda *args:
+ self._net_initialized_cb(active_op, *args))
+
+ def _update_error_cb(self, err):
+ logging.debug("Device(%s): failed to update. (%s)" % (self._op, err))
+ self._valid = False
+ self.emit('init-failed')
+
+ def _net_initialized_cb(self, active_op, net, valid):
+ net_op = net.get_op()
+ if not self._networks.has_key(net_op):
+ return
+
+ if not valid:
+ # init failure
+ del self._networks[net_op]
+ return
+
+ # init success
+ if self._valid:
+ self.emit('network-appeared', net)
+ if active_op and net_op == active_op:
+ self.set_active_network(net)
+
+ def get_op(self):
+ return self._op
+
+ def get_networks(self):
+ ret = []
+ for net in self._networks.values():
+ if net.is_valid():
+ ret.append(net)
+ return ret
+
+ def get_network(self, op):
+ if self._networks.has_key(op) and self._networks[op].is_valid():
+ return self._networks[op]
+ return None
+
+ def get_network_ops(self):
+ ret = []
+ for net in self._networks.values():
+ if net.is_valid():
+ ret.append(net.get_op())
+ return ret
+
+ def get_mesh_step(self):
+ if self._type != DEVICE_TYPE_802_11_MESH_OLPC:
+ raise RuntimeError("Only valid for mesh devices")
+ try:
+ step = self.dev.getMeshStep(timeout=3)
+ except dbus.DBusException:
+ step = 0
+ return step
+
+ def get_frequency(self):
+ try:
+ freq = self.dev.getFrequency(timeout=3)
+ except dbus.DBusException:
+ freq = 0.0
+ # Hz -> GHz
+ self._freq = freq / 1000000000.0
+ return self._freq
+
+ def get_strength(self):
+ return self._strength
+
+ def set_strength(self, strength):
+ if strength == self._strength:
+ return False
+
+ if strength >= 0 and strength <= 100:
+ self._strength = strength
+ else:
+ self._strength = 0
+
+ if self._valid:
+ self.emit('strength-changed')
+
+ def network_appeared(self, network):
+ # NM may emit NetworkAppeared messages before the initialization-time
+ # getProperties call completes. This means that we are in danger of
+ # instantiating the "appeared" network here, and then instantiating
+ # the same network later on when getProperties completes
+ # (_update_reply_cb calls _update_networks).
+ # We avoid this race by confirming that getProperties has completed
+ # before listening to any NetworkAppeared messages. We assume that
+ # any networks that get reported as appeared in this race window
+ # will be included in the getProperties response.
+ if not self._valid:
+ return
+
+ if self._networks.has_key(network):
+ return
+ net = Network(self._client, network)
+ self._networks[network] = net
+ net.connect('initialized', lambda *args:
+ self._net_initialized_cb(None, *args))
+
+ def network_disappeared(self, network):
+ if not self._networks.has_key(network):
+ return
+
+ if self._valid:
+ self.emit('network-disappeared', self._networks[network])
+
+ del self._networks[network]
+
+ def set_active_network(self, network):
+ if self._active_network == network:
+ return
+
+ # Make sure the old one doesn't get a stuck state
+ if self._active_network:
+ self._active_network.set_state(NETWORK_STATE_NOTCONNECTED)
+ self._active_network.disconnect(self._active_net_sigid)
+
+ self._active_network = network
+
+ if self._active_network:
+ self._active_net_sigid = self._active_network.connect(
+ "initialized", self._active_net_initialized)
+
+ # don't emit ssid-changed for networks that are not yet valid
+ if self._valid:
+ if self._active_network and self._active_network.is_valid():
+ self.emit('ssid-changed')
+ elif not self._active_network:
+ self.emit('ssid-changed')
+
+ def _active_net_initialized(self, net, user_data=None):
+ if self._active_network and self._active_network.is_valid():
+ self.emit('ssid-changed')
+
+ def _get_active_net_cb(self, state, net_op):
+ if not self._networks.has_key(net_op):
+ self.set_active_network(None)
+ return
+
+ self.set_active_network(self._networks[net_op])
+
+ _device_to_network_state = {
+ DEVICE_STATE_ACTIVATING : NETWORK_STATE_CONNECTING,
+ DEVICE_STATE_ACTIVATED : NETWORK_STATE_CONNECTED,
+ DEVICE_STATE_INACTIVE : NETWORK_STATE_NOTCONNECTED
+ }
+
+ network_state = _device_to_network_state[state]
+ self._active_network.set_state(network_state)
+
+ def _get_active_net_error_cb(self, err):
+ logging.debug("Couldn't get active network: %s" % err)
+ self.set_active_network(None)
+
+ def get_state(self):
+ return self._state
+
+ def set_state(self, state):
+ if state == self._state:
+ return
+
+ if state == DEVICE_STATE_INACTIVE:
+ self._act_stage = 0
+
+ self._state = state
+ if self._valid:
+ self.emit('state-changed')
+
+ if self._type == DEVICE_TYPE_802_11_WIRELESS:
+ if state == DEVICE_STATE_INACTIVE:
+ self.set_active_network(None)
+ else:
+ self.dev.getActiveNetwork(
+ reply_handler=lambda *args:
+ self._get_active_net_cb(state, *args),
+ error_handler=self._get_active_net_error_cb)
+
+ def set_activation_stage(self, stage):
+ if stage == self._act_stage:
+ return
+ self._act_stage = stage
+ if self._valid:
+ self.emit('activation-stage-changed')
+
+ def get_activation_stage(self):
+ return self._act_stage
+
+ def get_ssid(self):
+ if self._active_network and self._active_network.is_valid():
+ return self._active_network.get_ssid()
+ elif not self._active_network:
+ return None
+
+ def get_active_network(self):
+ return self._active_network
+
+ def get_type(self):
+ return self._type
+
+ def is_valid(self):
+ return self._valid
+
+ def set_carrier(self, on):
+ self._link = on
+
+ def get_capabilities(self):
+ return self._caps
+
+class NMClient(gobject.GObject):
+ __gsignals__ = {
+ 'device-added' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'device-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'device-activating': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'device-removed' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self.nminfo = None
+ self._nm_present = False
+ self._nm_proxy = None
+ self._nm_obj = None
+ self._sig_handlers = None
+ self._update_timer = 0
+ self._devices = {}
+
+ self.nminfo = nminfo.NMInfo(self)
+
+ self._setup_dbus()
+ if self._nm_present:
+ self._get_initial_devices()
+
+ def get_devices(self):
+ return self._devices.values()
+
+ def _get_initial_devices_reply_cb(self, ops):
+ for op in ops:
+ self._add_device(op)
+
+ def _dev_initialized_cb(self, dev):
+ self.emit('device-added', dev)
+
+ def _dev_init_failed_cb(self, dev):
+ # Device failed to initialize, likely due to dbus errors or something
+ op = dev.get_op()
+ self._remove_device(op)
+
+ def _get_initial_devices_error_cb(self, err):
+ logging.debug("Error updating devices (%s)" % err)
+
+ def _get_initial_devices(self):
+ self._nm_obj.getDevices(
+ reply_handler=self._get_initial_devices_reply_cb,
+ error_handler=self._get_initial_devices_error_cb)
+
+ def _add_device(self, dev_op):
+ if self._devices.has_key(dev_op):
+ return
+ dev = Device(self, dev_op)
+ self._devices[dev_op] = dev
+ dev.connect('init-failed', self._dev_init_failed_cb)
+ dev.connect('initialized', self._dev_initialized_cb)
+ dev.connect('state-changed', self._dev_state_changed_cb)
+
+ def _remove_device(self, dev_op):
+ if not self._devices.has_key(dev_op):
+ return
+ dev = self._devices[dev_op]
+ if dev.is_valid():
+ self.emit('device-removed', dev)
+ del self._devices[dev_op]
+
+ def _dev_state_changed_cb(self, dev):
+ op = dev.get_op()
+ if not self._devices.has_key(op) or not dev.is_valid():
+ return
+ if dev.get_state() == DEVICE_STATE_ACTIVATING:
+ self.emit('device-activating', dev)
+ elif dev.get_state() == DEVICE_STATE_ACTIVATED:
+ self.emit('device-activated', dev)
+
+ def get_device(self, dev_op):
+ if not self._devices.has_key(dev_op):
+ return None
+ return self._devices[dev_op]
+
+ def _setup_dbus(self):
+ self._sig_handlers = {
+ 'StateChange': self.state_changed_sig_handler,
+ 'DeviceAdded': self.device_added_sig_handler,
+ 'DeviceRemoved': self.device_removed_sig_handler,
+ 'DeviceActivationStage': self.device_activation_stage_sig_handler,
+ 'DeviceActivating': self.device_activating_sig_handler,
+ 'DeviceNowActive': self.device_now_active_sig_handler,
+ 'DeviceNoLongerActive': self.device_no_longer_active_sig_handler,
+ 'DeviceActivationFailed': \
+ self.device_activation_failed_sig_handler,
+ 'DeviceCarrierOn': self.device_carrier_on_sig_handler,
+ 'DeviceCarrierOff': self.device_carrier_off_sig_handler,
+ 'DeviceStrengthChanged': \
+ self.wireless_device_strength_changed_sig_handler,
+ 'WirelessNetworkAppeared': \
+ self.wireless_network_appeared_sig_handler,
+ 'WirelessNetworkDisappeared': \
+ self.wireless_network_disappeared_sig_handler,
+ 'WirelessNetworkStrengthChanged': \
+ self.wireless_network_strength_changed_sig_handler
+ }
+
+ try:
+ self._nm_proxy = sys_bus.get_object(NM_SERVICE, NM_PATH,
+ follow_name_owner_changes=True)
+ self._nm_obj = dbus.Interface(self._nm_proxy, NM_IFACE)
+ except dbus.DBusException, e:
+ logging.debug("Could not connect to NetworkManager: %s" % e)
+ self._nm_present = False
+ return
+
+ sys_bus.add_signal_receiver(self.name_owner_changed_sig_handler,
+ signal_name="NameOwnerChanged",
+ dbus_interface="org.freedesktop.DBus")
+
+ for (signal, handler) in self._sig_handlers.items():
+ sys_bus.add_signal_receiver(handler, signal_name=signal,
+ dbus_interface=NM_IFACE)
+
+ # Find out whether or not NMI is running
+ try:
+ bus_object = sys_bus.get_object('org.freedesktop.DBus',
+ '/org/freedesktop/DBus')
+ name_ = bus_object.GetNameOwner( \
+ "org.freedesktop.NetworkManagerInfo",
+ dbus_interface='org.freedesktop.DBus')
+ self._nm_present = True
+ except dbus.DBusException:
+ self._nm_present = False
+
+ def set_active_device(self, device, network=None,
+ mesh_freq=None, mesh_start=None):
+ ssid = ""
+ if network:
+ ssid = network.get_ssid()
+ if device.get_type() == DEVICE_TYPE_802_11_MESH_OLPC:
+ if mesh_freq or mesh_start:
+ if mesh_freq and not mesh_start:
+ self._nm_obj.setActiveDevice(device.get_op(),
+ dbus.Double(mesh_freq))
+ elif mesh_start and not mesh_freq:
+ self._nm_obj.setActiveDevice(device.get_op(),
+ dbus.Double(0.0),
+ dbus.UInt32(mesh_start))
+ else:
+ self._nm_obj.setActiveDevice(device.get_op(),
+ dbus.Double(mesh_freq),
+ dbus.UInt32(mesh_start))
+ else:
+ self._nm_obj.setActiveDevice(device.get_op())
+ else:
+ self._nm_obj.setActiveDevice(device.get_op(), ssid)
+
+ def state_changed_sig_handler(self, new_state):
+ logging.debug('NM State Changed to %d' % new_state)
+
+ def device_activation_stage_sig_handler(self, device, stage):
+ logging.debug('Device Activation Stage "%s" for device %s'
+ % (NM_DEVICE_STAGE_STRINGS[stage], device))
+ if not self._devices.has_key(device):
+ logging.debug('DeviceActivationStage, device %s does not exist'
+ % (device))
+ return
+ self._devices[device].set_activation_stage(stage)
+
+ def device_activating_sig_handler(self, device):
+ logging.debug('DeviceActivating for %s' % (device))
+ if not self._devices.has_key(device):
+ logging.debug('DeviceActivating, device %s does not exist'
+ % (device))
+ return
+ self._devices[device].set_state(DEVICE_STATE_ACTIVATING)
+
+ def device_now_active_sig_handler(self, device, ssid=None):
+ logging.debug('DeviceNowActive for %s' % (device))
+ if not self._devices.has_key(device):
+ logging.debug('DeviceNowActive, device %s does not exist'
+ % (device))
+ return
+ self._devices[device].set_state(DEVICE_STATE_ACTIVATED)
+
+ def device_no_longer_active_sig_handler(self, device):
+ logging.debug('DeviceNoLongerActive for %s' % (device))
+ if not self._devices.has_key(device):
+ logging.debug('DeviceNoLongerActive, device %s does not exist'
+ % (device))
+ return
+ self._devices[device].set_state(DEVICE_STATE_INACTIVE)
+
+ def device_activation_failed_sig_handler(self, device, ssid=None):
+ logging.debug('DeviceActivationFailed for %s' % (device))
+ if not self._devices.has_key(device):
+ logging.debug('DeviceActivationFailed, device %s does not exist'
+ % (device))
+ return
+ self._devices[device].set_state(DEVICE_STATE_INACTIVE)
+
+ def name_owner_changed_sig_handler(self, name, old, new):
+ if name != NM_SERVICE:
+ return
+ if (old and len(old)) and (not new and not len(new)):
+ # NM went away
+ self._nm_present = False
+ devs = self._devices.keys()
+ for op in devs:
+ self._remove_device(op)
+ self._devices = {}
+ elif (not old and not len(old)) and (new and len(new)):
+ # NM started up
+ self._nm_present = True
+ self._get_initial_devices()
+
+ def device_added_sig_handler(self, device):
+ logging.debug('DeviceAdded for %s' % (device))
+ self._add_device(device)
+
+ def device_removed_sig_handler(self, device):
+ logging.debug('DeviceRemoved for %s' % (device))
+ self._remove_device(device)
+
+ def wireless_network_appeared_sig_handler(self, device, network):
+ if not self._devices.has_key(device):
+ return
+ self._devices[device].network_appeared(network)
+
+ def wireless_network_disappeared_sig_handler(self, device, network):
+ if not self._devices.has_key(device):
+ return
+ self._devices[device].network_disappeared(network)
+
+ def wireless_device_strength_changed_sig_handler(self, device, strength):
+ if not self._devices.has_key(device):
+ return
+ self._devices[device].set_strength(strength)
+
+ def wireless_network_strength_changed_sig_handler(self, device,
+ network, strength):
+ if not self._devices.has_key(device):
+ return
+ net = self._devices[device].get_network(network)
+ if net:
+ net.set_strength(strength)
+
+ def device_carrier_on_sig_handler(self, device):
+ if not self._devices.has_key(device):
+ return
+ self._devices[device].set_carrier(True)
+
+ def device_carrier_off_sig_handler(self, device):
+ if not self._devices.has_key(device):
+ return
+ self._devices[device].set_carrier(False)
diff --git a/src/jarabe/hardware/nminfo.py b/src/jarabe/hardware/nminfo.py
new file mode 100644
index 0000000..64c5ab3
--- /dev/null
+++ b/src/jarabe/hardware/nminfo.py
@@ -0,0 +1,557 @@
+# 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 time
+import os
+import binascii
+import ConfigParser
+import logging
+
+import gtk
+import dbus
+import dbus.service
+
+from sugar import env
+
+from jarabe.hardware import keydialog
+
+IW_AUTH_KEY_MGMT_802_1X = 0x1
+IW_AUTH_KEY_MGMT_PSK = 0x2
+
+IW_AUTH_WPA_VERSION_DISABLED = 0x00000001
+IW_AUTH_WPA_VERSION_WPA = 0x00000002
+IW_AUTH_WPA_VERSION_WPA2 = 0x00000004
+
+NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000
+IW_AUTH_CIPHER_NONE = 0x00000001
+IW_AUTH_CIPHER_WEP40 = 0x00000002
+IW_AUTH_CIPHER_TKIP = 0x00000004
+IW_AUTH_CIPHER_CCMP = 0x00000008
+IW_AUTH_CIPHER_WEP104 = 0x00000010
+
+IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001
+IW_AUTH_ALG_SHARED_KEY = 0x00000002
+
+NM_INFO_IFACE = 'org.freedesktop.NetworkManagerInfo'
+NM_INFO_PATH = '/org/freedesktop/NetworkManagerInfo'
+
+
+class NoNetworks(dbus.DBusException):
+ def __init__(self):
+ dbus.DBusException.__init__(self)
+ self._dbus_error_name = NM_INFO_IFACE + '.NoNetworks'
+
+class CanceledKeyRequestError(dbus.DBusException):
+ def __init__(self):
+ dbus.DBusException.__init__(self)
+ self._dbus_error_name = NM_INFO_IFACE + '.CanceledError'
+
+
+class NetworkInvalidError(Exception):
+ pass
+
+
+class NMConfig(ConfigParser.ConfigParser):
+ def get_bool(self, section, name):
+ opt = self.get(section, name)
+ if type(opt) == str:
+ if opt.lower() == 'yes' or opt.lower() == 'true':
+ return True
+ elif opt.lower() == 'no' or opt.lower() == 'false':
+ return False
+ raise ValueError("Invalid format for %s/%s. Should be one of" \
+ " [yes, no, true, false]." % (section, name))
+
+ def get_list(self, section, name):
+ opt = self.get(section, name)
+ if type(opt) != str or not len(opt):
+ return []
+ try:
+ return opt.split()
+ except Exception:
+ raise ValueError("Invalid format for %s/%s. Should be a" \
+ " space-separate list." % (section, name))
+
+ def get_int(self, section, name):
+ opt = self.get(section, name)
+ try:
+ return int(opt)
+ except ValueError:
+ raise ValueError("Invalid format for %s/%s. Should be a" \
+ " valid integer." % (section, name))
+
+ def get_float(self, section, name):
+ opt = self.get(section, name)
+ try:
+ return float(opt)
+ except ValueError:
+ raise ValueError("Invalid format for %s/%s. Should be a" \
+ " valid float." % (section, name))
+
+
+NETWORK_TYPE_UNKNOWN = 0
+NETWORK_TYPE_ALLOWED = 1
+NETWORK_TYPE_INVALID = 2
+
+
+class Security(object):
+ def __init__(self, we_cipher):
+ self._we_cipher = we_cipher
+ self._key = None
+ self._auth_alg = None
+
+ def read_from_config(self, cfg, name):
+ pass
+
+ def read_from_args(self, args):
+ pass
+
+ def new_from_config(cfg, name):
+ security = None
+ we_cipher = cfg.get_int(name, "we_cipher")
+ if we_cipher == IW_AUTH_CIPHER_NONE:
+ security = Security(we_cipher)
+ elif we_cipher == IW_AUTH_CIPHER_WEP40 or \
+ we_cipher == IW_AUTH_CIPHER_WEP104:
+ security = WEPSecurity(we_cipher)
+ elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or \
+ we_cipher == IW_AUTH_CIPHER_CCMP or \
+ we_cipher == IW_AUTH_CIPHER_TKIP:
+ security = WPASecurity(we_cipher)
+ else:
+ raise ValueError("Unsupported security combo")
+ security.read_from_config(cfg, name)
+ return security
+ new_from_config = staticmethod(new_from_config)
+
+ def new_from_args(we_cipher, args):
+ security = None
+ try:
+ if we_cipher == IW_AUTH_CIPHER_NONE:
+ security = Security(we_cipher)
+ elif we_cipher == IW_AUTH_CIPHER_WEP40 or \
+ we_cipher == IW_AUTH_CIPHER_WEP104:
+ security = WEPSecurity(we_cipher)
+ elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or \
+ we_cipher == IW_AUTH_CIPHER_CCMP or \
+ we_cipher == IW_AUTH_CIPHER_TKIP:
+ security = WPASecurity(we_cipher)
+ else:
+ raise ValueError("Unsupported security combo")
+ security.read_from_args(args)
+ except ValueError, e:
+ logging.debug("Error reading security information: %s" % e)
+ del security
+ return None
+ return security
+ new_from_args = staticmethod(new_from_args)
+
+ def get_properties(self):
+ return [dbus.Int32(self._we_cipher)]
+
+ def write_to_config(self, section, config):
+ config.set(section, "we_cipher", self._we_cipher)
+
+
+class WEPSecurity(Security):
+ def read_from_args(self, args):
+ if len(args) != 2:
+ raise ValueError("not enough arguments")
+ key = args[0]
+ auth_alg = args[1]
+ if isinstance(key, unicode):
+ key = key.encode()
+ if not isinstance(key, str):
+ raise ValueError("wrong argument type for key")
+ if not isinstance(auth_alg, int):
+ raise ValueError("wrong argument type for auth_alg")
+ self._key = key
+ self._auth_alg = auth_alg
+
+ def read_from_config(self, cfg, name):
+ # Key should be a hex encoded string
+ self._key = cfg.get(name, "key")
+ if self._we_cipher == IW_AUTH_CIPHER_WEP40 and len(self._key) != 10:
+ raise ValueError("Key length not right for 40-bit WEP")
+ if self._we_cipher == IW_AUTH_CIPHER_WEP104 and len(self._key) != 26:
+ raise ValueError("Key length not right for 104-bit WEP")
+
+ try:
+ binascii.a2b_hex(self._key)
+ except TypeError:
+ raise ValueError("Key was not a hexadecimal string.")
+
+ self._auth_alg = cfg.get_int(name, "auth_alg")
+ if self._auth_alg != IW_AUTH_ALG_OPEN_SYSTEM and \
+ self._auth_alg != IW_AUTH_ALG_SHARED_KEY:
+ raise ValueError("Invalid authentication algorithm %d"
+ % self._auth_alg)
+
+ def get_properties(self):
+ args = Security.get_properties(self)
+ args.append(dbus.String(self._key))
+ args.append(dbus.Int32(self._auth_alg))
+ return args
+
+ def write_to_config(self, section, config):
+ Security.write_to_config(self, section, config)
+ config.set(section, "key", self._key)
+ config.set(section, "auth_alg", self._auth_alg)
+
+class WPASecurity(Security):
+ def __init__(self, we_cipher):
+ Security.__init__(self, we_cipher)
+ self._wpa_ver = None
+ self._key_mgmt = None
+
+ def read_from_args(self, args):
+ if len(args) != 3:
+ raise ValueError("not enough arguments")
+ key = args[0]
+ if isinstance(key, unicode):
+ key = key.encode()
+ if not isinstance(key, str):
+ raise ValueError("wrong argument type for key")
+
+ wpa_ver = args[1]
+ if not isinstance(wpa_ver, int):
+ raise ValueError("wrong argument type for WPA version")
+
+ key_mgmt = args[2]
+ if not isinstance(key_mgmt, int):
+ raise ValueError("wrong argument type for WPA key management")
+ if not key_mgmt & IW_AUTH_KEY_MGMT_PSK:
+ raise ValueError("Key management types other than" \
+ " PSK are not supported")
+
+ self._key = key
+ self._wpa_ver = wpa_ver
+ self._key_mgmt = key_mgmt
+
+ def read_from_config(self, cfg, name):
+ # Key should be a hex encoded string
+ self._key = cfg.get(name, "key")
+ if len(self._key) != 64:
+ raise ValueError("Key length not right for WPA-PSK")
+
+ try:
+ binascii.a2b_hex(self._key)
+ except TypeError:
+ raise ValueError("Key was not a hexadecimal string.")
+
+ self._wpa_ver = cfg.get_int(name, "wpa_ver")
+ if self._wpa_ver != IW_AUTH_WPA_VERSION_WPA and \
+ self._wpa_ver != IW_AUTH_WPA_VERSION_WPA2:
+ raise ValueError("Invalid WPA version %d" % self._wpa_ver)
+
+ self._key_mgmt = cfg.get_int(name, "key_mgmt")
+ if not self._key_mgmt & IW_AUTH_KEY_MGMT_PSK:
+ raise ValueError("Invalid WPA key management option %d"
+ % self._key_mgmt)
+
+ def get_properties(self):
+ args = Security.get_properties(self)
+ args.append(dbus.String(self._key))
+ args.append(dbus.Int32(self._wpa_ver))
+ args.append(dbus.Int32(self._key_mgmt))
+ return args
+
+ def write_to_config(self, section, config):
+ Security.write_to_config(self, section, config)
+ config.set(section, "key", self._key)
+ config.set(section, "wpa_ver", self._wpa_ver)
+ config.set(section, "key_mgmt", self._key_mgmt)
+
+
+class Network:
+ def __init__(self, ssid):
+ self.ssid = ssid
+ self.timestamp = int(time.time())
+ self.bssids = []
+ self.we_cipher = 0
+ self._security = None
+
+ def get_properties(self):
+ bssid_list = dbus.Array([], signature="s")
+ for item in self.bssids:
+ bssid_list.append(dbus.String(item))
+ args = [dbus.String(self.ssid), dbus.Int32(self.timestamp),
+ dbus.Boolean(True), bssid_list]
+ args += self._security.get_properties()
+ return tuple(args)
+
+ def get_security(self):
+ return self._security.get_properties()
+
+ def set_security(self, security):
+ self._security = security
+
+ def read_from_args(self, auto, bssid, we_cipher, args):
+ if auto == False:
+ self.timestamp = int(time.time())
+ if not bssid in self.bssids:
+ self.bssids.append(bssid)
+
+ self._security = Security.new_from_args(we_cipher, args)
+ if not self._security:
+ raise NetworkInvalidError("Invalid security information")
+
+ def read_from_config(self, config):
+ try:
+ self.timestamp = config.get_int(self.ssid, "timestamp")
+ except (ConfigParser.NoOptionError, ValueError), e:
+ raise NetworkInvalidError(e)
+
+ try:
+ self._security = Security.new_from_config(config, self.ssid)
+ except Exception, e:
+ raise NetworkInvalidError(e)
+
+ # The following don't need to be present
+ try:
+ self.bssids = config.get_list(self.ssid, "bssids")
+ except (ConfigParser.NoOptionError, ValueError), e:
+ logging.debug("Error reading bssids: %s" % e)
+
+ def write_to_config(self, config):
+ try:
+ config.add_section(self.ssid)
+ config.set(self.ssid, "timestamp", self.timestamp)
+ if len(self.bssids) > 0:
+ opt = " "
+ opt = opt.join(self.bssids)
+ config.set(self.ssid, "bssids", opt)
+ self._security.write_to_config(self.ssid, config)
+ except Exception, e:
+ logging.debug("Error writing '%s': %s" % (self.ssid, e))
+
+
+class NotFoundError(dbus.DBusException):
+ pass
+class UnsupportedError(dbus.DBusException):
+ pass
+
+class NMInfoDBusServiceHelper(dbus.service.Object):
+ def __init__(self, parent):
+ self._parent = parent
+ bus = dbus.SystemBus()
+
+ # If NMI is already around, don't grab the NMI service
+ bus_object = bus.get_object('org.freedesktop.DBus',
+ '/org/freedesktop/DBus')
+ name = None
+ try:
+ name = bus_object.GetNameOwner( \
+ "org.freedesktop.NetworkManagerInfo",
+ dbus_interface='org.freedesktop.DBus')
+ except dbus.DBusException:
+ logging.debug("Error getting owner of NMI")
+ if name:
+ logging.info("NMI service already owned by %s, won't claim it."
+ % name)
+
+ bus_name = dbus.service.BusName(NM_INFO_IFACE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, NM_INFO_PATH)
+
+ @dbus.service.method(NM_INFO_IFACE, in_signature='i', out_signature='as')
+ def getNetworks(self, net_type):
+ ssids = self._parent.get_networks(net_type)
+ if len(ssids) > 0:
+ return dbus.Array(ssids)
+
+ raise NoNetworks()
+
+ @dbus.service.method(NM_INFO_IFACE, in_signature='si',
+ async_callbacks=('async_cb', 'async_err_cb'))
+ def getNetworkProperties(self, ssid, net_type, async_cb, async_err_cb):
+ self._parent.get_network_properties(ssid, net_type,
+ async_cb, async_err_cb)
+
+ @dbus.service.method(NM_INFO_IFACE)
+ def updateNetworkInfo(self, ssid, bauto, bssid, cipher, *args):
+ self._parent.update_network_info(ssid, bauto, bssid, cipher, args)
+
+ @dbus.service.method(NM_INFO_IFACE,
+ async_callbacks=('async_cb', 'async_err_cb'))
+ def getKeyForNetwork(self, dev_path, net_path, ssid, attempt,
+ new_key, async_cb, async_err_cb):
+ self._parent.get_key_for_network(dev_path, net_path, ssid,
+ attempt, new_key, async_cb, async_err_cb)
+
+ @dbus.service.method(NM_INFO_IFACE)
+ def cancelGetKeyForNetwork(self):
+ self._parent.cancel_get_key_for_network()
+
+class NMInfo(object):
+ def __init__(self, client):
+ profile_path = env.get_profile_path()
+ self._cfg_file = os.path.join(profile_path, "nm", "networks.cfg")
+ self._nmclient = client
+ self._allowed_networks = self._read_config()
+ self._dbus_helper = NMInfoDBusServiceHelper(self)
+ self._key_dialog = None
+
+ def save_config(self):
+ self._write_config(self._allowed_networks)
+
+ def _read_config(self):
+ if not os.path.exists(os.path.dirname(self._cfg_file)):
+ os.makedirs(os.path.dirname(self._cfg_file), 0755)
+ if not os.path.exists(self._cfg_file):
+ self._write_config({})
+ return {}
+
+ config = NMConfig()
+ config.read(self._cfg_file)
+ networks = {}
+ for name in config.sections():
+ try:
+ net = Network(name)
+ net.read_from_config(config)
+ networks[name] = net
+ except Exception, e:
+ logging.error("Error when processing config for" \
+ " the network %s: %r" % (name, e))
+
+ del config
+ return networks
+
+ def _write_config(self, networks):
+ fp = open(self._cfg_file, 'w')
+ config = NMConfig()
+ for net in networks.values():
+ net.write_to_config(config)
+ config.write(fp)
+ fp.close()
+ del config
+
+ def get_networks(self, net_type):
+ if net_type != NETWORK_TYPE_ALLOWED:
+ raise ValueError("Bad network type")
+ nets = []
+ for net in self._allowed_networks.values():
+ nets.append(net.ssid)
+ logging.debug("Returning networks: %s" % nets)
+ return nets
+
+ def get_network_properties(self, ssid, net_type, async_cb, async_err_cb):
+ if not isinstance(ssid, unicode):
+ async_err_cb(ValueError("Invalid arguments; ssid must be unicode."))
+ if net_type != NETWORK_TYPE_ALLOWED:
+ async_err_cb(ValueError("Bad network type"))
+ if not self._allowed_networks.has_key(ssid):
+ async_err_cb(NotFoundError("Network '%s' not found." % ssid))
+ network = self._allowed_networks[ssid]
+ props = network.get_properties()
+
+ # DBus workaround: the normal method return handler wraps
+ # the returned arguments in a tuple and then converts that to a
+ # struct, but NetworkManager expects a plain list of arguments.
+ # It turns out that the async callback method return code _doesn't_
+ # wrap the returned arguments in a tuple, so as a workaround use
+ # the async callback stuff here even though we're not doing it
+ # asynchronously.
+ async_cb(*props)
+
+ def update_network_info(self, ssid, auto, bssid, we_cipher, args):
+ if not isinstance(ssid, unicode):
+ raise ValueError("Invalid arguments; ssid must be unicode.")
+ if self._allowed_networks.has_key(ssid):
+ del self._allowed_networks[ssid]
+ net = Network(ssid)
+ try:
+ net.read_from_args(auto, bssid, we_cipher, args)
+ logging.debug("Updated network information for '%s'." % ssid)
+ self._allowed_networks[ssid] = net
+ self.save_config()
+ except NetworkInvalidError, e:
+ logging.debug("Error updating network information: %s" % e)
+ del net
+
+ def get_key_for_network(self, dev_op, net_op, ssid, attempt,
+ new_key, async_cb, async_err_cb):
+ from jarabe.hardware import nmclient
+
+ if not isinstance(ssid, unicode):
+ raise ValueError("Invalid arguments; ssid must be unicode.")
+ if self._allowed_networks.has_key(ssid) and not new_key:
+ # We've got the info already
+ net = self._allowed_networks[ssid]
+ async_cb(tuple(net.get_security()))
+ return
+
+ # Otherwise, ask the user for it
+ net = None
+ dev = self._nmclient.get_device(dev_op)
+ if not dev:
+ async_err_cb(NotFoundError("Device was unknown."))
+ return
+
+ if dev.get_type() == nmclient.DEVICE_TYPE_802_3_ETHERNET:
+ # We don't support wired 802.1x yet...
+ async_err_cb(UnsupportedError("Device type is unsupported by NMI."))
+ return
+
+ net = dev.get_network(net_op)
+ if not net:
+ async_err_cb(NotFoundError("Network was unknown."))
+ return
+
+ self._key_dialog = keydialog.new_key_dialog(net, async_cb, async_err_cb)
+ self._key_dialog.connect("response", self._key_dialog_response_cb)
+ self._key_dialog.connect("destroy", self._key_dialog_destroy_cb)
+ self._key_dialog.show_all()
+
+ def _key_dialog_destroy_cb(self, widget, data=None):
+ if widget != self._key_dialog:
+ return
+ self._key_dialog_response_cb(widget, gtk.RESPONSE_CANCEL)
+
+ def _key_dialog_response_cb(self, widget, response_id):
+ if widget != self._key_dialog:
+ return
+
+ (async_cb, async_err_cb) = self._key_dialog.get_callbacks()
+ security = None
+ if response_id == gtk.RESPONSE_OK:
+ security = self._key_dialog.create_security()
+ self._key_dialog = None
+ widget.destroy()
+
+ if response_id in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_NONE]:
+ # key dialog dialog was canceled; send the error back to NM
+ async_err_cb(CanceledKeyRequestError())
+ elif response_id == gtk.RESPONSE_OK:
+ if not security:
+ raise RuntimeError("Invalid security arguments.")
+ props = security.get_properties()
+ a = tuple(props)
+ async_cb(*a)
+ else:
+ raise RuntimeError("Unhandled key dialog response %d" % response_id)
+
+ def cancel_get_key_for_network(self):
+ # Close the wireless key dialog and just have it return
+ # with the 'canceled' argument set to true
+ if not self._key_dialog:
+ return
+ self._key_dialog_destroy_cb(self._key_dialog)
+
+ # this method is invoked directly in-process (not by DBus).
+ def delete_all_networks(self):
+ self._allowed_networks = {}
+ self.save_config()
+
diff --git a/src/jarabe/hardware/schoolserver.py b/src/jarabe/hardware/schoolserver.py
new file mode 100644
index 0000000..a9ed60b
--- /dev/null
+++ b/src/jarabe/hardware/schoolserver.py
@@ -0,0 +1,54 @@
+import logging
+from gettext import gettext as _
+from xmlrpclib import ServerProxy, Error
+import socket
+import os
+
+from sugar.profile import get_profile
+
+REGISTER_URL = 'http://schoolserver:8080/'
+
+class RegisterError(Exception):
+ pass
+
+def register_laptop(url=REGISTER_URL):
+ if not have_ofw_tree():
+ logging.error('Registration: Cannot obtain data needed to register.')
+ raise RegisterError(_('Cannot obtain data needed for registration.'))
+
+ sn = read_ofw('mfg-data/SN')
+ uuid = read_ofw('mfg-data/U#')
+ sn = sn or 'SHF00000000'
+ uuid = uuid or '00000000-0000-0000-0000-000000000000'
+
+ profile = get_profile()
+
+ server = ServerProxy(url)
+ try:
+ data = server.register(sn, profile.nick_name, uuid, profile.pubkey)
+ except (Error, socket.error), e:
+ logging.error('Registration: cannot connect to server: %s' % e)
+ raise RegisterError(_('Cannot connect to the server.'))
+
+ if data['success'] != 'OK':
+ logging.error('Registration: server could not complete request: %s' %
+ data['error'])
+ raise RegisterError(_('The server could not complete the request.'))
+
+ profile.jabber_server = data['jabberserver']
+ profile.backup1 = data['backupurl']
+ profile.save()
+
+ return True
+
+def have_ofw_tree():
+ return os.path.exists('/ofw')
+
+def read_ofw(path):
+ path = os.path.join('/ofw', path)
+ if not os.path.exists(path):
+ return None
+ fh = open(path, 'r')
+ data = fh.read().rstrip('\0\n')
+ fh.close()
+ return data
diff --git a/src/jarabe/intro/Makefile.am b/src/jarabe/intro/Makefile.am
new file mode 100644
index 0000000..b9ddcf4
--- /dev/null
+++ b/src/jarabe/intro/Makefile.am
@@ -0,0 +1,9 @@
+imagedir = $(pkgdatadir)/shell/intro
+image_DATA = default-picture.png
+
+EXTRA_DIST = $(conf_DATA) $(image_DATA)
+sugardir = $(pythondir)/jarabe/intro
+sugar_PYTHON = \
+ __init__.py \
+ colorpicker.py \
+ window.py
diff --git a/src/jarabe/intro/__init__.py b/src/jarabe/intro/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/jarabe/intro/__init__.py
diff --git a/src/jarabe/intro/colorpicker.py b/src/jarabe/intro/colorpicker.py
new file mode 100644
index 0000000..a939857
--- /dev/null
+++ b/src/jarabe/intro/colorpicker.py
@@ -0,0 +1,43 @@
+# 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 hippo
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+class ColorPicker(hippo.CanvasBox, hippo.CanvasItem):
+ def __init__(self, **kwargs):
+ hippo.CanvasBox.__init__(self, **kwargs)
+ self.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ self._xo_color = None
+
+ self._xo = CanvasIcon(size=style.XLARGE_ICON_SIZE,
+ icon_name='computer-xo')
+ self._set_random_colors()
+ self._xo.connect('activated', self._xo_activated_cb)
+ self.append(self._xo)
+
+ def _xo_activated_cb(self, item):
+ self._set_random_colors()
+
+ def get_color(self):
+ return self._xo_color
+
+ def _set_random_colors(self):
+ self._xo_color = XoColor()
+ self._xo.props.xo_color = self._xo_color
diff --git a/src/jarabe/intro/default-picture.png b/src/jarabe/intro/default-picture.png
new file mode 100644
index 0000000..e26b9b0
--- /dev/null
+++ b/src/jarabe/intro/default-picture.png
Binary files differ
diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py
new file mode 100644
index 0000000..be1a963
--- /dev/null
+++ b/src/jarabe/intro/window.py
@@ -0,0 +1,278 @@
+# 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 os
+import logging
+from gettext import gettext as _
+
+import gtk
+import gobject
+import hippo
+
+from sugar import env
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+from sugar.graphics.entry import CanvasEntry
+from sugar.graphics.xocolor import XoColor
+from sugar.profile import get_profile
+
+from jarabe.intro import colorpicker
+
+_BACKGROUND_COLOR = style.COLOR_WHITE
+
+def create_profile(name, color=None, pixbuf=None):
+ if not pixbuf:
+ path = os.path.join(os.path.dirname(__file__), 'default-picture.png')
+ pixbuf = gtk.gdk.pixbuf_new_from_file(path)
+
+ if not color:
+ color = XoColor()
+
+ icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg")
+ pixbuf.save(icon_path, "jpeg", {"quality":"85"})
+
+ profile = get_profile()
+ profile.nick_name = name
+ profile.color = color
+ profile.save()
+
+ # Generate keypair
+ import commands
+ keypath = os.path.join(env.get_profile_path(), "owner.key")
+ if not os.path.isfile(keypath):
+ cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % keypath
+ (s, o) = commands.getstatusoutput(cmd)
+ if s != 0:
+ logging.error("Could not generate key pair: %d %s" % (s, o))
+ else:
+ logging.error("Keypair exists, skip generation.")
+
+class _Page(hippo.CanvasBox):
+ __gproperties__ = {
+ 'valid' : (bool, None, None, False,
+ gobject.PARAM_READABLE)
+ }
+
+ def __init__(self, **kwargs):
+ hippo.CanvasBox.__init__(self, **kwargs)
+ self.valid = False
+
+ def set_valid(self, valid):
+ self.valid = valid
+ self.notify('valid')
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'valid':
+ return self.valid
+
+ def activate(self):
+ pass
+
+class _NamePage(_Page):
+ def __init__(self, intro):
+ _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER,
+ background_color=_BACKGROUND_COLOR.get_int(),
+ spacing=style.DEFAULT_SPACING,
+ orientation=hippo.ORIENTATION_HORIZONTAL,)
+
+ self._intro = intro
+
+ label = hippo.CanvasText(text=_("Name:"))
+ self.append(label)
+
+ self._entry = CanvasEntry(box_width=style.zoom(300))
+ self._entry.set_background(_BACKGROUND_COLOR.get_html())
+ self._entry.connect('notify::text', self._text_changed_cb)
+
+ widget = self._entry.props.widget
+ widget.set_max_length(45)
+
+ self.append(self._entry)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ def _text_changed_cb(self, entry, pspec):
+ valid = len(entry.props.text.strip()) > 0
+ self.set_valid(valid)
+
+ def get_name(self):
+ return self._entry.props.text
+
+ def activate(self):
+ self._entry.props.widget.grab_focus()
+
+class _ColorPage(_Page):
+ def __init__(self, **kwargs):
+ _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER,
+ background_color=_BACKGROUND_COLOR.get_int(),
+ spacing=style.DEFAULT_SPACING,
+ yalign=hippo.ALIGNMENT_CENTER, **kwargs)
+
+ self._label = hippo.CanvasText(text=_("Click to change color:"),
+ xalign=hippo.ALIGNMENT_CENTER)
+ self.append(self._label)
+
+ self._cp = colorpicker.ColorPicker(xalign=hippo.ALIGNMENT_CENTER)
+ self.append(self._cp)
+
+ self._color = self._cp.get_color()
+ self.set_valid(True)
+
+ def get_color(self):
+ return self._cp.get_color()
+
+class _IntroBox(hippo.CanvasBox):
+ __gsignals__ = {
+ 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT]))
+ }
+
+ PAGE_NAME = 0
+ PAGE_COLOR = 1
+
+ PAGE_FIRST = PAGE_NAME
+ PAGE_LAST = PAGE_COLOR
+
+ def __init__(self):
+ hippo.CanvasBox.__init__(self, padding=style.zoom(30),
+ background_color=_BACKGROUND_COLOR.get_int())
+
+ self._page = self.PAGE_NAME
+ self._name_page = _NamePage(self)
+ self._color_page = _ColorPage()
+ self._current_page = None
+ self._next_button = None
+
+ self._setup_page()
+
+ def _setup_page(self):
+ self.remove_all()
+
+ if self._page == self.PAGE_NAME:
+ self._current_page = self._name_page
+ elif self._page == self.PAGE_COLOR:
+ self._current_page = self._color_page
+
+ self.append(self._current_page, hippo.PACK_EXPAND)
+
+ button_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ if self._page != self.PAGE_FIRST:
+ back_button = hippo.CanvasButton(text=_('Back'))
+ image = Icon(icon_name='go-left')
+ back_button.props.widget.set_image(image)
+ back_button.connect('activated', self._back_activated_cb)
+ button_box.append(back_button)
+
+ spacer = hippo.CanvasBox()
+ button_box.append(spacer, hippo.PACK_EXPAND)
+
+ self._next_button = hippo.CanvasButton()
+ image = Icon(icon_name='go-right')
+ self._next_button.props.widget.set_image(image)
+
+ if self._page == self.PAGE_LAST:
+ self._next_button.props.text = _('Done')
+ self._next_button.connect('activated', self._done_activated_cb)
+ else:
+ self._next_button.props.text = _('Next')
+ self._next_button.connect('activated', self._next_activated_cb)
+
+ self._current_page.activate()
+
+ self._update_next_button()
+ button_box.append(self._next_button)
+
+ self._current_page.connect('notify::valid',
+ self._page_valid_changed_cb)
+ self.append(button_box)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ button_box.reverse()
+
+ def _update_next_button(self):
+ widget = self._next_button.props.widget
+ widget.props.sensitive = self._current_page.props.valid
+
+ def _page_valid_changed_cb(self, page, pspec):
+ self._update_next_button()
+
+ def _back_activated_cb(self, item):
+ self.back()
+
+ def back(self):
+ if self._page != self.PAGE_FIRST:
+ self._page -= 1
+ self._setup_page()
+
+ def _next_activated_cb(self, item):
+ self.next()
+
+ def next(self):
+ if self._page == self.PAGE_LAST:
+ self.done()
+ if self._current_page.props.valid:
+ self._page += 1
+ self._setup_page()
+
+ def _done_activated_cb(self, item):
+ self.done()
+
+ def done(self):
+ name = self._name_page.get_name()
+ color = self._color_page.get_color()
+
+ self.emit('done', name, color)
+
+class IntroWindow(gtk.Window):
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self._canvas = hippo.Canvas()
+ self._intro_box = _IntroBox()
+ self._intro_box.connect('done', self._done_cb)
+ self._canvas.set_root(self._intro_box)
+
+ self.add(self._canvas)
+ self._canvas.show()
+ self.connect('key-press-event', self.__key_press_cb)
+
+ def _done_cb(self, box, name, color):
+ self.hide()
+ gobject.idle_add(self._create_profile_cb, name, color)
+
+ def _create_profile_cb(self, name, color):
+ create_profile(name, color)
+ gtk.main_quit()
+
+ return False
+
+ def __key_press_cb(self, widget, event):
+ if gtk.gdk.keyval_name(event.keyval) == "Return":
+ self._intro_box.next()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "Escape":
+ self._intro_box.back()
+ return True
+ return False
+
+
+if __name__ == "__main__":
+ w = IntroWindow()
+ w.show()
+ w.connect('destroy', gtk.main_quit)
+ gtk.main()
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
new file mode 100644
index 0000000..11886d6
--- /dev/null
+++ b/src/jarabe/journal/Makefile.am
@@ -0,0 +1,18 @@
+sugardir = $(pythondir)/jarabe/journal
+sugar_PYTHON = \
+ __init__.py \
+ collapsedentry.py \
+ detailview.py \
+ expandedentry.py \
+ journalactivity.py \
+ journalentrybundle.py \
+ journaltoolbox.py \
+ keepicon.py \
+ listview.py \
+ misc.py \
+ modalalert.py \
+ objectchooser.py \
+ palettes.py \
+ query.py \
+ volumesmanager.py \
+ volumestoolbar.py
diff --git a/src/jarabe/journal/__init__.py b/src/jarabe/journal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/jarabe/journal/__init__.py
diff --git a/src/jarabe/journal/collapsedentry.py b/src/jarabe/journal/collapsedentry.py
new file mode 100644
index 0000000..235ac4c
--- /dev/null
+++ b/src/jarabe/journal/collapsedentry.py
@@ -0,0 +1,385 @@
+# 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
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+import json
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import style
+from sugar.datastore import datastore
+from sugar.graphics.entry import CanvasEntry
+
+from jarabe.journal.keepicon import KeepIcon
+from jarabe.journal.palettes import ObjectPalette, BuddyPalette
+from jarabe.journal import misc
+
+class BuddyIcon(CanvasIcon):
+ def __init__(self, buddy, **kwargs):
+ CanvasIcon.__init__(self, **kwargs)
+ self._buddy = buddy
+
+ def create_palette(self):
+ return BuddyPalette(self._buddy)
+
+class BuddyList(hippo.CanvasBox):
+ def __init__(self, model, width):
+ hippo.CanvasBox.__init__(self,
+ orientation=hippo.ORIENTATION_HORIZONTAL,
+ box_width=width,
+ xalign=hippo.ALIGNMENT_START)
+ self.set_model(model)
+
+ def set_model(self, model):
+ for item in self.get_children():
+ self.remove(item)
+
+ for buddy in model[0:3]:
+ nick_, color = buddy
+ icon = BuddyIcon(buddy,
+ icon_name='computer-xo',
+ xo_color=XoColor(color),
+ cache=True)
+ self.append(icon)
+
+class EntryIcon(CanvasIcon):
+ def __init__(self, **kwargs):
+ CanvasIcon.__init__(self, **kwargs)
+ self._jobject = None
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self.props.file_name = misc.get_icon_name(jobject)
+ self.palette = None
+
+ def create_palette(self):
+ if self.show_palette:
+ return ObjectPalette(self._jobject)
+ else:
+ return None
+
+ show_palette = gobject.property(type=bool, default=False)
+
+class BaseCollapsedEntry(hippo.CanvasBox):
+ __gtype_name__ = 'BaseCollapsedEntry'
+
+ _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 3
+ _BUDDIES_COL_WIDTH = style.GRID_CELL_SIZE * 3
+ _PROGRESS_COL_WIDTH = style.GRID_CELL_SIZE * 5
+
+ def __init__(self):
+ hippo.CanvasBox.__init__(self,
+ spacing=style.DEFAULT_SPACING,
+ padding_top=style.DEFAULT_PADDING,
+ padding_bottom=style.DEFAULT_PADDING,
+ padding_left=style.DEFAULT_PADDING * 2,
+ padding_right=style.DEFAULT_PADDING * 2,
+ box_height=style.GRID_CELL_SIZE,
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ self._jobject = None
+ self._is_selected = False
+
+ self.keep_icon = self._create_keep_icon()
+ self.append(self.keep_icon)
+
+ self.icon = self._create_icon()
+ self.append(self.icon)
+
+ self.title = self._create_title()
+ self.append(self.title, hippo.PACK_EXPAND)
+
+ self.buddies_list = self._create_buddies_list()
+ self.append(self.buddies_list)
+
+ self.date = self._create_date()
+ self.append(self.date)
+
+ # Progress controls
+ self.progress_bar = self._create_progress_bar()
+ self.append(self.progress_bar)
+
+ self.cancel_button = self._create_cancel_button()
+ self.append(self.cancel_button)
+
+ def _create_keep_icon(self):
+ keep_icon = KeepIcon(False)
+ keep_icon.connect('button-release-event',
+ self.__keep_icon_button_release_event_cb)
+ return keep_icon
+
+ def _create_date(self):
+ date = hippo.CanvasText(text='',
+ xalign=hippo.ALIGNMENT_START,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ box_width=self._DATE_COL_WIDTH)
+ return date
+
+ def _create_icon(self):
+ icon = EntryIcon(size=style.STANDARD_ICON_SIZE, cache=True)
+ return icon
+
+ def _create_title(self):
+ # TODO: We'd prefer to ellipsize in the middle
+ title = hippo.CanvasText(text='',
+ xalign=hippo.ALIGNMENT_START,
+ font_desc=style.FONT_BOLD.get_pango_desc(),
+ size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END)
+ return title
+
+ def _create_buddies_list(self):
+ return BuddyList([], self._BUDDIES_COL_WIDTH)
+
+ def _create_progress_bar(self):
+ progress_bar = gtk.ProgressBar()
+ return hippo.CanvasWidget(widget=progress_bar,
+ yalign=hippo.ALIGNMENT_CENTER,
+ box_width=self._PROGRESS_COL_WIDTH)
+
+ def _create_cancel_button(self):
+ button = CanvasIcon(icon_name='activity-stop',
+ size=style.SMALL_ICON_SIZE,
+ box_width=style.GRID_CELL_SIZE)
+ button.connect('button-release-event',
+ self._cancel_button_release_event_cb)
+ return button
+
+ def _decode_buddies(self):
+ if self.jobject.metadata.has_key('buddies') and \
+ self.jobject.metadata['buddies']:
+ # json cannot read unicode strings
+ buddies_str = self.jobject.metadata['buddies'].encode('utf8')
+ buddies = json.read(buddies_str).values()
+ else:
+ buddies = []
+ return buddies
+
+ def update_visibility(self):
+ in_process = self.is_in_progress()
+
+ self.buddies_list.set_visible(not in_process)
+ self.date.set_visible(not in_process)
+
+ self.progress_bar.set_visible(in_process)
+ self.cancel_button.set_visible(in_process)
+
+ # TODO: determine the appearance of in-progress entries
+ def _update_color(self):
+ if self.is_in_progress():
+ self.props.background_color = style.COLOR_WHITE.get_int()
+ else:
+ self.props.background_color = style.COLOR_WHITE.get_int()
+
+ def is_in_progress(self):
+ return self._jobject.metadata.has_key('progress') and \
+ int(self._jobject.metadata['progress']) < 100
+
+ def get_keep(self):
+ keep = int(self._jobject.metadata.get('keep', 0))
+ return keep == 1
+
+ def __keep_icon_button_release_event_cb(self, button, event):
+ logging.debug('__keep_icon_button_release_event_cb')
+ jobject = datastore.get(self._jobject.object_id)
+ try:
+ if self.get_keep():
+ jobject.metadata['keep'] = 0
+ else:
+ jobject.metadata['keep'] = 1
+ datastore.write(jobject, update_mtime=False)
+ finally:
+ jobject.destroy()
+
+ self.keep_icon.props.keep = self.get_keep()
+ self._update_color()
+
+ return True
+
+ def _cancel_button_release_event_cb(self, button, event):
+ logging.debug('_cancel_button_release_event_cb')
+ datastore.delete(self._jobject.object_id)
+ return True
+
+ def set_selected(self, is_selected):
+ self._is_selected = is_selected
+ self._update_color()
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self._is_selected = False
+
+ self.keep_icon.props.keep = self.get_keep()
+
+ self.date.props.text = misc.get_date(jobject)
+
+ self.icon.set_jobject(jobject)
+ if jobject.is_activity_bundle():
+ self.icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ self.icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ else:
+ if jobject.metadata.has_key('icon-color') and \
+ jobject.metadata['icon-color']:
+ self.icon.props.xo_color = XoColor( \
+ jobject.metadata['icon-color'])
+ else:
+ self.icon.props.xo_color = None
+
+ if jobject.metadata.get('title', ''):
+ title_text = jobject.metadata['title']
+ else:
+ title_text = _('Untitled')
+ self.title.props.text = title_text
+
+ self.buddies_list.set_model(self._decode_buddies())
+
+ if jobject.metadata.has_key('progress'):
+ self.progress_bar.props.widget.props.fraction = \
+ int(jobject.metadata['progress']) / 100.0
+
+ self.update_visibility()
+ self._update_color()
+
+ def get_jobject(self):
+ return self._jobject
+
+ jobject = property(get_jobject, set_jobject)
+
+ def update_date(self):
+ self.date.props.text = misc.get_date(self._jobject)
+
+class CollapsedEntry(BaseCollapsedEntry):
+ __gtype_name__ = 'CollapsedEntry'
+
+ __gsignals__ = {
+ 'detail-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self):
+ BaseCollapsedEntry.__init__(self)
+
+ self.icon.props.show_palette = True
+ self.icon.connect('button-release-event',
+ self.__icon_button_release_event_cb)
+
+ self.title.connect('button_release_event',
+ self.__title_button_release_event_cb)
+
+ self._title_entry = self._create_title_entry()
+ self.insert_after(self._title_entry, self.title, hippo.PACK_EXPAND)
+ self._title_entry.set_visible(False)
+
+ self._detail_button = self._create_detail_button()
+ self._detail_button.connect('motion-notify-event',
+ self.__detail_button_motion_notify_event_cb)
+ self.append(self._detail_button)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ def _create_title_entry(self):
+ title_entry = CanvasEntry()
+ title_entry.set_background(style.COLOR_WHITE.get_html())
+ title_entry.props.widget.connect('focus-out-event',
+ self.__title_entry_focus_out_event_cb)
+ title_entry.props.widget.connect('activate',
+ self.__title_entry_activate_cb)
+ title_entry.connect('key-press-event',
+ self.__title_entry_key_press_event_cb)
+ return title_entry
+
+ def _create_detail_button(self):
+ button = CanvasIcon(icon_name='go-right',
+ size=style.SMALL_ICON_SIZE,
+ box_width=style.GRID_CELL_SIZE * 3 / 5,
+ fill_color=style.COLOR_BUTTON_GREY.get_svg())
+ button.connect('button-release-event',
+ self.__detail_button_release_event_cb)
+ return button
+
+ def update_visibility(self):
+ BaseCollapsedEntry.update_visibility(self)
+ self._detail_button.set_visible(not self.is_in_progress())
+
+ def set_jobject(self, jobject):
+ BaseCollapsedEntry.set_jobject(self, jobject)
+ self._title_entry.props.text = self.title.props.text
+
+ jobject = property(BaseCollapsedEntry.get_jobject, set_jobject)
+
+ def __detail_button_release_event_cb(self, button, event):
+ logging.debug('_detail_button_release_event_cb')
+ if not self.is_in_progress():
+ self.emit('detail-clicked')
+ return True
+
+ def __detail_button_motion_notify_event_cb(self, button, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ button.props.fill_color = style.COLOR_TOOLBAR_GREY.get_svg()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ button.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+
+ def __icon_button_release_event_cb(self, button, event):
+ logging.debug('__icon_button_release_event_cb')
+ misc.resume(self.jobject)
+ return True
+
+ def __title_button_release_event_cb(self, button, event):
+ self.title.set_visible(False)
+ self._title_entry.set_visible(True)
+ self._title_entry.props.widget.grab_focus()
+
+ def __title_entry_focus_out_event_cb(self, entry, event):
+ self._apply_title_change(entry.props.text)
+
+ def __title_entry_activate_cb(self, entry):
+ self._apply_title_change(entry.props.text)
+
+ def __title_entry_key_press_event_cb(self, entry, event):
+ if event.key == hippo.KEY_ESCAPE:
+ self._cancel_title_change()
+
+ def _apply_title_change(self, title):
+ self._title_entry.set_visible(False)
+ self.title.set_visible(True)
+
+ if title == '':
+ self._cancel_title_change()
+ elif self.title.props.text != title:
+ self.title.props.text = title
+ self._jobject.metadata['title'] = title
+ self._jobject.metadata['title_set_by_user'] = '1'
+ datastore.write(self._jobject, update_mtime=False,
+ reply_handler=self._datastore_write_cb,
+ error_handler=self._datastore_write_error_cb)
+
+ def _cancel_title_change(self):
+ self._title_entry.props.text = self.title.props.text
+ self._title_entry.set_visible(False)
+ self.title.set_visible(True)
+
+ def _datastore_write_cb(self):
+ pass
+
+ def _datastore_write_error_cb(self, error):
+ logging.error('CollapsedEntry._datastore_write_error_cb: %r' % error)
+
diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py
new file mode 100644
index 0000000..5748d6f
--- /dev/null
+++ b/src/jarabe/journal/detailview.py
@@ -0,0 +1,133 @@
+# 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
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+from sugar.datastore import datastore
+
+from jarabe.journal.expandedentry import ExpandedEntry
+
+class DetailView(gtk.VBox):
+ __gtype_name__ = 'DetailView'
+
+ __gproperties__ = {
+ 'jobject' : (object, None, None,
+ gobject.PARAM_READWRITE)
+ }
+
+ __gsignals__ = {
+ 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self, **kwargs):
+ self._jobject = None
+ self._expanded_entry = None
+
+ canvas = hippo.Canvas()
+
+ self._root = hippo.CanvasBox()
+ self._root.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ canvas.set_root(self._root)
+
+ back_bar = BackBar()
+ back_bar.connect('button-release-event',
+ self.__back_bar_release_event_cb)
+ self._root.append(back_bar)
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.pack_start(canvas)
+ canvas.show()
+
+ def _fav_icon_activated_cb(self, fav_icon):
+ keep = not self._expanded_entry.get_keep()
+ self._expanded_entry.set_keep(keep)
+ fav_icon.props.keep = keep
+
+ def __back_bar_release_event_cb(self, back_bar, event):
+ self.emit('go-back-clicked')
+ return False
+
+ def _update_view(self):
+ if self._expanded_entry:
+ self._root.remove(self._expanded_entry)
+
+ # Work around pygobject bug #479227
+ self._expanded_entry.remove_all()
+ import gc
+ gc.collect()
+ if self._jobject:
+ self._expanded_entry = ExpandedEntry(self._jobject.object_id)
+ self._root.append(self._expanded_entry, hippo.PACK_EXPAND)
+
+ def refresh(self):
+ logging.debug('DetailView.refresh')
+ if self._jobject:
+ self._jobject = datastore.get(self._jobject.object_id)
+ self._update_view()
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'jobject':
+ self._jobject = value
+ self._update_view()
+ else:
+ raise AssertionError
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'jobject':
+ return self._jobject
+ else:
+ raise AssertionError
+
+
+class BackBar(hippo.CanvasBox):
+ def __init__(self):
+ hippo.CanvasBox.__init__(self,
+ orientation=hippo.ORIENTATION_HORIZONTAL,
+ border=style.LINE_WIDTH,
+ background_color=style.COLOR_PANEL_GREY.get_int(),
+ border_color=style.COLOR_SELECTION_GREY.get_int(),
+ padding=style.DEFAULT_PADDING,
+ padding_left=style.DEFAULT_SPACING,
+ spacing=style.DEFAULT_SPACING)
+
+ icon = CanvasIcon(icon_name='go-previous',
+ size=style.SMALL_ICON_SIZE,
+ fill_color=style.COLOR_TOOLBAR_GREY.get_svg())
+ self.append(icon)
+
+ label = hippo.CanvasText(text=_('Back'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ self.append(label)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def __motion_notify_event_cb(self, box, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ box.props.background_color = style.COLOR_SELECTION_GREY.get_int()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ box.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ return False
diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py
new file mode 100644
index 0000000..8957728
--- /dev/null
+++ b/src/jarabe/journal/expandedentry.py
@@ -0,0 +1,385 @@
+# 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
+from gettext import gettext as _
+import StringIO
+
+import hippo
+import cairo
+import gobject
+import gtk
+import json
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.entry import CanvasEntry
+from sugar.datastore import datastore
+
+from jarabe.journal.keepicon import KeepIcon
+from jarabe.journal.palettes import ObjectPalette, BuddyPalette
+from jarabe.journal import misc
+
+class Separator(hippo.CanvasBox, hippo.CanvasItem):
+ def __init__(self, orientation):
+ hippo.CanvasBox.__init__(self,
+ background_color=style.COLOR_PANEL_GREY.get_int())
+
+ if orientation == hippo.ORIENTATION_VERTICAL:
+ self.props.box_width = style.LINE_WIDTH
+ else:
+ self.props.box_height = style.LINE_WIDTH
+
+class CanvasTextView(hippo.CanvasWidget):
+ def __init__(self, text, **kwargs):
+ hippo.CanvasWidget.__init__(self, **kwargs)
+ self.text_view_widget = gtk.TextView()
+ self.text_view_widget.props.buffer.props.text = text
+ self.text_view_widget.props.left_margin = style.DEFAULT_SPACING
+ self.text_view_widget.props.right_margin = style.DEFAULT_SPACING
+ self.text_view_widget.props.wrap_mode = gtk.WRAP_WORD
+ self.text_view_widget.show()
+
+ # TODO: These fields should expand vertically instead of scrolling
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.set_shadow_type(gtk.SHADOW_OUT)
+ scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ scrolled_window.add(self.text_view_widget)
+
+ self.props.widget = scrolled_window
+
+class BuddyList(hippo.CanvasBox):
+ def __init__(self, model):
+ hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START,
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ for buddy in model:
+ nick_, color = buddy
+ hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+ icon = CanvasIcon(icon_name='computer-xo',
+ xo_color=XoColor(color),
+ size=style.STANDARD_ICON_SIZE)
+ icon.set_palette(BuddyPalette(buddy))
+ hbox.append(icon)
+ self.append(hbox)
+
+class ExpandedEntry(hippo.CanvasBox):
+ def __init__(self, object_id):
+ hippo.CanvasBox.__init__(self)
+ self.props.orientation = hippo.ORIENTATION_VERTICAL
+ self.props.background_color = style.COLOR_WHITE.get_int()
+ self.props.padding_top = style.DEFAULT_SPACING * 3
+
+ self._jobject = datastore.get(object_id)
+ self._update_title_sid = None
+
+ # Create header
+ header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ padding=style.DEFAULT_PADDING,
+ padding_right=style.GRID_CELL_SIZE,
+ spacing=style.DEFAULT_SPACING)
+ self.append(header)
+
+ # Create two column body
+
+ body = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ spacing=style.DEFAULT_SPACING * 3,
+ padding_left=style.GRID_CELL_SIZE,
+ padding_right=style.GRID_CELL_SIZE,
+ padding_top=style.DEFAULT_SPACING * 3)
+
+ self.append(body, hippo.PACK_EXPAND)
+
+ first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(first_column)
+
+ second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(second_column, hippo.PACK_EXPAND)
+
+ # Header
+
+ self._keep_icon = self._create_keep_icon()
+ header.append(self._keep_icon)
+
+ self._icon = self._create_icon()
+ header.append(self._icon)
+
+ self._title = self._create_title()
+ header.append(self._title, hippo.PACK_EXPAND)
+
+ # TODO: create a version list popup instead of a date label
+ self._date = self._create_date()
+ header.append(self._date)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ header.reverse()
+
+ # First column
+
+ self._preview = self._create_preview()
+ first_column.append(self._preview)
+
+ # Second column
+
+ description_box, self._description = self._create_description()
+ second_column.append(description_box)
+
+ tags_box, self._tags = self._create_tags()
+ second_column.append(tags_box)
+
+ self._buddy_list = self._create_buddy_list()
+ second_column.append(self._buddy_list)
+
+ def _create_keep_icon(self):
+ keep = int(self._jobject.metadata.get('keep', 0)) == 1
+ keep_icon = KeepIcon(keep)
+ keep_icon.connect('activated', self._keep_icon_activated_cb)
+ return keep_icon
+
+ def _create_icon(self):
+ icon = CanvasIcon(file_name=misc.get_icon_name(self._jobject))
+ icon.connect_after('button-release-event',
+ self._icon_button_release_event_cb)
+
+ if self._jobject.is_activity_bundle():
+ icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ else:
+ if self._jobject.metadata.has_key('icon-color') and \
+ self._jobject.metadata['icon-color']:
+ icon.props.xo_color = XoColor( \
+ self._jobject.metadata['icon-color'])
+
+ icon.set_palette(ObjectPalette(self._jobject))
+
+ return icon
+
+ def _create_title(self):
+ title = CanvasEntry()
+ title.set_background(style.COLOR_WHITE.get_html())
+ title.props.text = self._jobject.metadata.get('title', _('Untitled'))
+ title.props.widget.connect('focus-out-event',
+ self._title_focus_out_event_cb)
+ return title
+
+ def _create_date(self):
+ date = hippo.CanvasText(xalign=hippo.ALIGNMENT_START,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ text = misc.get_date(self._jobject))
+ return date
+
+ def _create_preview(self):
+ width = style.zoom(320)
+ height = style.zoom(240)
+ box = hippo.CanvasBox()
+
+ if self._jobject.metadata.has_key('preview') and \
+ len(self._jobject.metadata['preview']) > 4:
+
+ if self._jobject.metadata['preview'][1:4] == 'PNG':
+ preview_data = self._jobject.metadata['preview']
+ else:
+ # TODO: We are close to be able to drop this.
+ import base64
+ preview_data = base64.b64decode(
+ self._jobject.metadata['preview'])
+
+ png_file = StringIO.StringIO(preview_data)
+ try:
+ surface = cairo.ImageSurface.create_from_png(png_file)
+ has_preview = True
+ except Exception, e:
+ logging.error('Error while loading the preview: %r' % e)
+ has_preview = False
+ else:
+ has_preview = False
+
+ if has_preview:
+ preview_box = hippo.CanvasImage(image=surface,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ scale_width=width,
+ scale_height=height)
+ else:
+ preview_box = hippo.CanvasText(text=_('No preview'),
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ color=style.COLOR_BUTTON_GREY.get_int(),
+ box_width=width,
+ box_height=height)
+ preview_box.connect_after('button-release-event',
+ self._preview_box_button_release_event_cb)
+ box.append(preview_box)
+ return box
+
+ def _create_buddy_list(self):
+
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Participants:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ if self._jobject.metadata.has_key('buddies') and \
+ self._jobject.metadata['buddies']:
+ # json cannot read unicode strings
+ buddies_str = self._jobject.metadata['buddies'].encode('utf8')
+ buddies = json.read(buddies_str).values()
+ vbox.append(BuddyList(buddies))
+ return vbox
+ else:
+ return vbox
+
+ def _create_description(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Description:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ description = self._jobject.metadata.get('description', '')
+ text_view = CanvasTextView(description,
+ box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._description_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _create_tags(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Tags:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ tags = self._jobject.metadata.get('tags', '')
+ text_view = CanvasTextView(tags, box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._tags_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _title_notify_text_cb(self, entry, pspec):
+ if not self._update_title_sid:
+ self._update_title_sid = gobject.timeout_add(1000,
+ self._update_title_cb)
+
+ def _datastore_write_cb(self):
+ pass
+
+ def _datastore_write_error_cb(self, error):
+ logging.error('ExpandedEntry._datastore_write_error_cb: %r' % error)
+
+ def _title_focus_out_event_cb(self, entry, event):
+ self._update_entry()
+
+ def _description_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _tags_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _update_entry(self):
+ needs_update = False
+
+ old_title = self._jobject.metadata.get('title', None)
+ if old_title != self._title.props.text:
+ self._icon.palette.props.primary_text = self._title.props.text
+ self._jobject.metadata['title'] = self._title.props.text
+ self._jobject.metadata['title_set_by_user'] = '1'
+ needs_update = True
+
+ old_tags = self._jobject.metadata.get('tags', None)
+ new_tags = self._tags.text_view_widget.props.buffer.props.text
+ if old_tags != new_tags:
+ self._jobject.metadata['tags'] = new_tags
+ needs_update = True
+
+ old_description = self._jobject.metadata.get('description', None)
+ new_description = \
+ self._description.text_view_widget.props.buffer.props.text
+ if old_description != new_description:
+ self._jobject.metadata['description'] = new_description
+ needs_update = True
+
+ if needs_update:
+ datastore.write(self._jobject, update_mtime=False,
+ reply_handler=self._datastore_write_cb,
+ error_handler=self._datastore_write_error_cb)
+
+ self._update_title_sid = None
+
+ def get_keep(self):
+ return self._jobject.metadata.has_key('keep') and \
+ self._jobject.metadata['keep'] == 1
+
+ def _keep_icon_activated_cb(self, keep_icon):
+ if self.get_keep():
+ self._jobject.metadata['keep'] = 0
+ else:
+ self._jobject.metadata['keep'] = 1
+ datastore.write(self._jobject, update_mtime=False)
+
+ keep_icon.props.keep = self.get_keep()
+
+ def _icon_button_release_event_cb(self, button, event):
+ logging.debug('_icon_button_release_event_cb')
+ misc.resume(self._jobject)
+ return True
+
+ def _preview_box_button_release_event_cb(self, button, event):
+ logging.debug('_preview_box_button_release_event_cb')
+ misc.resume(self._jobject)
+ return True
+
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
new file mode 100644
index 0000000..51f0c61
--- /dev/null
+++ b/src/jarabe/journal/journalactivity.py
@@ -0,0 +1,338 @@
+# Copyright (C) 2006, Red Hat, Inc.
+# 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
+from gettext import gettext as _
+import sys
+import traceback
+import uuid
+
+import gtk
+import dbus
+import statvfs
+import os
+
+from sugar.graphics.window import Window
+from sugar.bundle.bundle import ZipExtractException, RegistrationException
+from sugar.datastore import datastore
+from sugar import env
+from sugar.activity import activityfactory
+from sugar import wm
+
+from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.listview import ListView
+from jarabe.journal.detailview import DetailView
+from jarabe.journal.volumestoolbar import VolumesToolbar
+from jarabe.journal import misc
+from jarabe.journal.journalentrybundle import JournalEntryBundle
+from jarabe.journal.objectchooser import ObjectChooser
+from jarabe.journal.modalalert import ModalAlert
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+J_DBUS_SERVICE = 'org.laptop.Journal'
+J_DBUS_INTERFACE = 'org.laptop.Journal'
+J_DBUS_PATH = '/org/laptop/Journal'
+
+_SPACE_TRESHOLD = 52428800
+_BUNDLE_ID = 'org.laptop.JournalActivity'
+
+class JournalActivityDBusService(dbus.service.Object):
+ def __init__(self, parent):
+ self._parent = parent
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(J_DBUS_SERVICE,
+ bus=session_bus, replace_existing=False, allow_replacement=False)
+ logging.debug('bus_name: %r', bus_name)
+ dbus.service.Object.__init__(self, bus_name, J_DBUS_PATH)
+
+ @dbus.service.method(J_DBUS_INTERFACE,
+ in_signature='', out_signature='')
+ def FocusSearch(self):
+ """Become visible and give focus to the search entry
+ """
+ self._parent.present()
+ self._parent.show_main_view()
+ self._parent.search_grab_focus()
+
+ @dbus.service.method(J_DBUS_INTERFACE,
+ in_signature='s', out_signature='')
+ def ShowObject(self, object_id):
+ """Pop-up journal and show object with object_id"""
+
+ logging.debug('Trying to show object %s', object_id)
+
+ if self._parent.show_object(object_id):
+ self._parent.present()
+
+ def _chooser_response_cb(self, chooser, response_id, chooser_id):
+ logging.debug('JournalActivityDBusService._chooser_response_cb')
+ if response_id == gtk.RESPONSE_ACCEPT:
+ object_id = chooser.get_selected_object_id()
+ self.ObjectChooserResponse(chooser_id, object_id)
+ else:
+ self.ObjectChooserCancelled(chooser_id)
+ chooser.destroy()
+ del chooser
+
+ @dbus.service.method(J_DBUS_INTERFACE, in_signature='i', out_signature='s')
+ def ChooseObject(self, parent_xid):
+ chooser_id = uuid.uuid4().hex
+ if parent_xid > 0:
+ parent = gtk.gdk.window_foreign_new(parent_xid)
+ else:
+ parent = None
+ chooser = ObjectChooser(parent)
+ chooser.connect('response', self._chooser_response_cb, chooser_id)
+ chooser.show()
+
+ return chooser_id
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="ss")
+ def ObjectChooserResponse(self, chooser_id, object_id):
+ pass
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="s")
+ def ObjectChooserCancelled(self, chooser_id):
+ pass
+
+class JournalActivity(Window):
+ def __init__(self):
+ Window.__init__(self)
+
+ self.set_title(_('Journal'))
+
+ self._main_view = None
+ self._secondary_view = None
+ self._list_view = None
+ self._detail_view = None
+ self._main_toolbox = None
+ self._detail_toolbox = None
+
+ self._setup_main_view()
+ self._setup_secondary_view()
+
+ self.add_events(gtk.gdk.ALL_EVENTS_MASK |
+ gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self._realized_sid = self.connect('realize', self.__realize_cb)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('window-state-event', self.__window_state_event_cb)
+ self.connect('key-press-event', self._key_press_event_cb)
+ self.connect('focus-in-event', self._focus_in_event_cb)
+
+ bus = dbus.SessionBus()
+ data_store = dbus.Interface(
+ bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE)
+ data_store.connect_to_signal('Created', self.__data_store_created_cb)
+ data_store.connect_to_signal('Updated', self.__data_store_updated_cb)
+ data_store.connect_to_signal('Deleted', self.__data_store_deleted_cb)
+
+ self._dbus_service = JournalActivityDBusService(self)
+
+ self.iconify()
+
+ self._critical_space_alert = None
+ self._check_available_space()
+
+ def __realize_cb(self, window):
+ wm.set_bundle_id(window.window, _BUNDLE_ID)
+ activity_id = activityfactory.create_activity_id()
+ wm.set_activity_id(window.window, str(activity_id))
+ self.disconnect(self._realized_sid)
+ self._realized_sid = None
+
+ def can_close(self):
+ return False
+
+ def _setup_main_view(self):
+ self._main_toolbox = MainToolbox()
+ self._main_view = gtk.VBox()
+
+ self._list_view = ListView()
+ self._list_view.connect('detail-clicked', self.__detail_clicked_cb)
+ self._main_view.pack_start(self._list_view)
+ self._list_view.show()
+
+ volumes_toolbar = VolumesToolbar()
+ volumes_toolbar.connect('volume-changed', self._volume_changed_cb)
+ self._main_view.pack_start(volumes_toolbar, expand=False)
+
+ search_toolbar = self._main_toolbox.search_toolbar
+ search_toolbar.connect('query-changed', self._query_changed_cb)
+ search_toolbar.set_volume_id(datastore.mounts()[0]['id'])
+
+ def _setup_secondary_view(self):
+ self._secondary_view = gtk.VBox()
+
+ self._detail_toolbox = DetailToolbox()
+ entry_toolbar = self._detail_toolbox.entry_toolbar
+
+ self._detail_view = DetailView()
+ self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb)
+ self._secondary_view.pack_end(self._detail_view)
+ self._detail_view.show()
+
+ def _key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ logging.info(keyname)
+ logging.info(event.state)
+ if keyname == 'Escape':
+ self.show_main_view()
+
+ def __detail_clicked_cb(self, list_view, entry):
+ self._show_secondary_view(entry.jobject)
+
+ def __go_back_clicked_cb(self, detail_view):
+ self.show_main_view()
+
+ def _query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+ self.show_main_view()
+
+ def show_main_view(self):
+ if self.toolbox != self._main_toolbox:
+ self.set_toolbox(self._main_toolbox)
+ self._main_toolbox.show()
+
+ if self.canvas != self._main_view:
+ self.set_canvas(self._main_view)
+ self._main_view.show()
+
+ def _show_secondary_view(self, jobject):
+ try:
+ self._detail_toolbox.entry_toolbar.set_jobject(jobject)
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ self.set_toolbox(self._detail_toolbox)
+ self._detail_toolbox.show()
+
+ try:
+ self._detail_view.props.jobject = jobject
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ self.set_canvas(self._secondary_view)
+ self._secondary_view.show()
+
+ def show_object(self, object_id):
+ jobject = datastore.get(object_id)
+ if jobject is None:
+ return False
+ else:
+ self._show_secondary_view(jobject)
+ return True
+
+ def _volume_changed_cb(self, volume_toolbar, volume_id):
+ logging.debug('Selected volume: %r.' % volume_id)
+ self._main_toolbox.search_toolbar.set_volume_id(volume_id)
+ self._main_toolbox.set_current_toolbar(0)
+
+ def __data_store_created_cb(self, uid):
+ jobject = datastore.get(uid)
+ if jobject is None:
+ return
+ try:
+ self._check_for_bundle(jobject)
+ finally:
+ jobject.destroy()
+ self._main_toolbox.search_toolbar.refresh_filters()
+ self._check_available_space()
+
+ def __data_store_updated_cb(self, uid):
+ jobject = datastore.get(uid)
+ if jobject is None:
+ return
+ try:
+ self._check_for_bundle(jobject)
+ finally:
+ jobject.destroy()
+ self._check_available_space()
+
+ def __data_store_deleted_cb(self, uid):
+ if self.canvas == self._secondary_view and \
+ uid == self._detail_view.props.jobject.object_id:
+ self.show_main_view()
+
+ def _focus_in_event_cb(self, window, event):
+ self.search_grab_focus()
+ self._list_view.update_dates()
+
+ def _check_for_bundle(self, jobject):
+ bundle = misc.get_bundle(jobject)
+ if bundle is None:
+ return
+
+ if bundle.is_installed():
+ return
+ try:
+ bundle.install()
+ except (ZipExtractException, RegistrationException), e:
+ logging.warning('Could not install bundle %s: %r' %
+ (jobject.file_path, e))
+ return
+
+ if jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE:
+ datastore.delete(jobject.object_id)
+
+ def search_grab_focus(self):
+ search_toolbar = self._main_toolbox.search_toolbar
+ search_toolbar.give_entry_focus()
+
+ def __window_state_event_cb(self, window, event):
+ logging.debug('window_state_event_cb %r' % self)
+ if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
+ state = event.new_window_state
+ visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED
+ self._list_view.set_is_visible(visible)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r' % self)
+ visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+ def _check_available_space(self):
+ ''' Check available space on device
+
+ If the available space is below 50MB an alert will be
+ shown which encourages to delete old journal entries.
+ '''
+
+ if self._critical_space_alert:
+ return
+ stat = os.statvfs(env.get_profile_path())
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+ if free_space < _SPACE_TRESHOLD:
+ self._critical_space_alert = ModalAlert()
+ self._critical_space_alert.connect('destroy',
+ self.__alert_closed_cb)
+ self._critical_space_alert.show()
+
+ def __alert_closed_cb(self, data):
+ self.show_main_view()
+ self.present()
+ self._critical_space_alert = None
+
+def start():
+ journal = JournalActivity()
+ journal.show()
+
diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py
new file mode 100644
index 0000000..8862ca3
--- /dev/null
+++ b/src/jarabe/journal/journalentrybundle.py
@@ -0,0 +1,96 @@
+# 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 tempfile
+import shutil
+
+import json
+
+import dbus
+from sugar.datastore import datastore
+from sugar.bundle.bundle import Bundle, MalformedBundleException
+
+class JournalEntryBundle(Bundle):
+ """A Journal entry bundle
+
+ See http://wiki.laptop.org/go/Journal_entry_bundles for details
+ """
+
+ MIME_TYPE = 'application/vnd.olpc-journal-entry'
+
+ _zipped_extension = '.xoj'
+ _unzipped_extension = None
+ _infodir = None
+
+ def __init__(self, path):
+ Bundle.__init__(self, path)
+
+ def install(self):
+ if os.environ.has_key('SUGAR_ACTIVITY_ROOT'):
+ install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],
+ 'data')
+ else:
+ install_dir = tempfile.gettempdir()
+ bundle_dir = os.path.join(install_dir, self._zip_root_dir)
+ uid = self._zip_root_dir
+ self._unzip(install_dir)
+ try:
+ metadata = self._read_metadata(bundle_dir)
+ jobject = datastore.create()
+ try:
+ for key, value in metadata.iteritems():
+ jobject.metadata[key] = value
+
+ preview = self._read_preview(uid, bundle_dir)
+ if preview is not None:
+ jobject.metadata['preview'] = dbus.ByteArray(preview)
+
+ jobject.metadata['uid'] = ''
+ jobject.file_path = os.path.join(bundle_dir, uid)
+ datastore.write(jobject)
+ finally:
+ jobject.destroy()
+ finally:
+ shutil.rmtree(bundle_dir, ignore_errors=True)
+
+ def _read_metadata(self, bundle_dir):
+ metadata_path = os.path.join(bundle_dir, '_metadata.json')
+ if not os.path.exists(metadata_path):
+ raise MalformedBundleException(
+ 'Bundle must contain the file "_metadata.json"')
+ f = open(metadata_path, 'r')
+ try:
+ json_data = f.read()
+ finally:
+ f.close()
+ return json.read(json_data)
+
+ def _read_preview(self, uid, bundle_dir):
+ preview_path = os.path.join(bundle_dir, 'preview', uid)
+ if not os.path.exists(preview_path):
+ return ''
+ f = open(preview_path, 'r')
+ try:
+ preview_data = f.read()
+ finally:
+ f.close()
+ return preview_data
+
+ def is_installed(self):
+ # These bundles can be reinstalled as many times as desired.
+ return False
+
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
new file mode 100644
index 0000000..8d52a06
--- /dev/null
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -0,0 +1,418 @@
+# 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
+
+from gettext import gettext as _
+import logging
+from datetime import datetime, timedelta
+import os
+
+import gobject
+import gtk
+
+from sugar.graphics.toolbox import Toolbox
+from sugar.graphics.toolcombobox import ToolComboBox
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.combobox import ComboBox
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics import iconentry
+from sugar.graphics import style
+from sugar import activity
+from sugar import profile
+from sugar import mime
+from sugar.datastore import datastore
+
+from jarabe.journal import volumesmanager
+from jarabe.journal import misc
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+_ACTION_ANYTIME = 0
+_ACTION_TODAY = 1
+_ACTION_SINCE_YESTERDAY = 2
+_ACTION_PAST_WEEK = 3
+_ACTION_PAST_MONTH = 4
+_ACTION_PAST_YEAR = 5
+
+_ACTION_ANYTHING = 0
+
+_ACTION_EVERYBODY = 0
+_ACTION_MY_FRIENDS = 1
+_ACTION_MY_CLASS = 2
+
+class MainToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.search_toolbar = SearchToolbar()
+ self.search_toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ self.add_toolbar(_('Search'), self.search_toolbar)
+ self.search_toolbar.show()
+
+ #self.manage_toolbar = ManageToolbar()
+ #self.add_toolbar(_('Manage'), self.manage_toolbar)
+ #self.manage_toolbar.show()
+
+class SearchToolbar(gtk.Toolbar):
+ __gtype_name__ = 'SearchToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._volume_id = None
+
+ self._search_entry = iconentry.IconEntry()
+ self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._search_entry.connect('activate', self._search_entry_activated_cb)
+ self._search_entry.connect('changed', self._search_entry_changed_cb)
+ self._search_entry.add_clear_button()
+ self._autosearch_timer = None
+ self._add_widget(self._search_entry, expand=True)
+
+ self._what_search_combo = ComboBox()
+ self._what_combo_changed_sid = self._what_search_combo.connect(
+ 'changed', self._combo_changed_cb)
+ tool_item = ToolComboBox(self._what_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self._when_search_combo = self._get_when_search_combo()
+ tool_item = ToolComboBox(self._when_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ # TODO: enable it when the DS supports saving the buddies.
+ #self._with_search_combo = self._get_with_search_combo()
+ #tool_item = ToolComboBox(self._with_search_combo)
+ #self.insert(tool_item, -1)
+ #tool_item.show()
+
+ self._query = self._build_query()
+
+ self.refresh_filters()
+
+ def give_entry_focus(self):
+ self._search_entry.grab_focus()
+
+ def _get_when_search_combo(self):
+ when_search = ComboBox()
+ when_search.append_item(_ACTION_ANYTIME, _('Anytime'))
+ when_search.append_separator()
+ when_search.append_item(_ACTION_TODAY, _('Today'))
+ when_search.append_item(_ACTION_SINCE_YESTERDAY,
+ _('Since yesterday'))
+ # TRANS: Filter entries modified during the last 7 days.
+ when_search.append_item(_ACTION_PAST_WEEK, _('Past week'))
+ # TRANS: Filter entries modified during the last 30 days.
+ when_search.append_item(_ACTION_PAST_MONTH, _('Past month'))
+ # TRANS: Filter entries modified during the last 356 days.
+ when_search.append_item(_ACTION_PAST_YEAR, _('Past year'))
+ when_search.set_active(0)
+ when_search.connect('changed', self._combo_changed_cb)
+ return when_search
+
+ def _get_with_search_combo(self):
+ with_search = ComboBox()
+ with_search.append_item(_ACTION_EVERYBODY, _('Anyone'))
+ with_search.append_separator()
+ with_search.append_item(_ACTION_MY_FRIENDS, _('My friends'))
+ with_search.append_item(_ACTION_MY_CLASS, _('My class'))
+ with_search.append_separator()
+
+ # TODO: Ask the model for buddies.
+ with_search.append_item(3, 'Dan', 'theme:xo')
+
+ with_search.set_active(0)
+ with_search.connect('changed', self._combo_changed_cb)
+ return with_search
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def _build_query(self):
+ query = {}
+ if self._volume_id:
+ query['mountpoints'] = [self._volume_id]
+ if self._what_search_combo.props.value:
+ value = self._what_search_combo.props.value
+ generic_type = mime.get_generic_type(value)
+ if generic_type:
+ mime_types = generic_type.mime_types
+ query['mime_type'] = mime_types
+ else:
+ query['activity'] = self._what_search_combo.props.value
+ if self._when_search_combo.props.value:
+ date_from, date_to = self._get_date_range()
+ query['mtime'] = {'start': date_from, 'end': date_to}
+ if self._search_entry.props.text:
+ text = self._search_entry.props.text.strip()
+
+ if not text.startswith('"'):
+ query_text = ''
+ words = text.split(' ')
+ for word in words:
+ if word:
+ if query_text:
+ query_text += ' '
+ query_text += word + '*'
+ else:
+ query_text = text
+
+ if query_text:
+ query['query'] = query_text
+
+ return query
+
+ def _get_date_range(self):
+ today_start = datetime.today().replace(hour=0, minute=0, second=0)
+ right_now = datetime.today()
+ if self._when_search_combo.props.value == _ACTION_TODAY:
+ date_range = (today_start, right_now)
+ elif self._when_search_combo.props.value == _ACTION_SINCE_YESTERDAY:
+ date_range = (today_start - timedelta(1), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_WEEK:
+ date_range = (today_start - timedelta(7), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_MONTH:
+ date_range = (today_start - timedelta(30), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_YEAR:
+ date_range = (today_start - timedelta(356), right_now)
+
+ return (date_range[0].isoformat(),
+ date_range[1].isoformat())
+
+ def _combo_changed_cb(self, combo):
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_activated_cb(self, search_entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_changed_cb(self, search_entry):
+ if not search_entry.props.text:
+ search_entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self._autosearch_timer_cb)
+
+ def _autosearch_timer_cb(self):
+ logging.debug('_autosearch_timer_cb')
+ self._autosearch_timer = None
+ self._search_entry.activate()
+ return False
+
+ def set_volume_id(self, volume_id):
+ self._volume_id = volume_id
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def refresh_filters(self):
+ current_value = self._what_search_combo.props.value
+ current_value_index = 0
+
+ self._what_search_combo.handler_block(self._what_combo_changed_sid)
+ try:
+ self._what_search_combo.remove_all()
+ # TRANS: Item in a combo box that filters by entry type.
+ self._what_search_combo.append_item(_ACTION_ANYTHING, _('Anything'))
+
+ registry = activity.get_registry()
+ appended_separator = False
+ for service_name in datastore.get_unique_values('activity'):
+ activity_info = registry.get_activity(service_name)
+ if not activity_info is None:
+ if not appended_separator:
+ self._what_search_combo.append_separator()
+ appended_separator = True
+
+ if os.path.exists(activity_info.icon):
+ self._what_search_combo.append_item(service_name,
+ activity_info.name,
+ file_name=activity_info.icon)
+ else:
+ self._what_search_combo.append_item(service_name,
+ activity_info.name,
+ icon_name='application-octet-stream')
+
+ if service_name == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+
+ self._what_search_combo.append_separator()
+
+ types = mime.get_all_generic_types()
+ for generic_type in types :
+ self._what_search_combo.append_item(
+ generic_type.type_id, generic_type.name, generic_type.icon)
+ if generic_type.type_id == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+
+ self._what_search_combo.set_active(current_value_index)
+ finally:
+ self._what_search_combo.handler_unblock(
+ self._what_combo_changed_sid)
+
+class ManageToolbar(gtk.Toolbar):
+ __gtype_name__ = 'ManageToolbar'
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+class DetailToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.entry_toolbar = EntryToolbar()
+ self.add_toolbar('', self.entry_toolbar)
+ self.entry_toolbar.show()
+
+class EntryToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._jobject = None
+
+ self._resume = ToolButton('activity-start')
+ self._resume.connect('clicked', self._resume_clicked_cb)
+ self.add(self._resume)
+ self._resume.show()
+
+ self._copy = ToolButton()
+
+ icon = Icon(icon_name='edit-copy', xo_color=profile.get_color())
+ self._copy.set_icon_widget(icon)
+ icon.show()
+
+ self._copy.set_tooltip(_('Copy'))
+ self._copy.connect('clicked', self._copy_clicked_cb)
+ self.add(self._copy)
+ self._copy.show()
+
+ separator = gtk.SeparatorToolItem()
+ self.add(separator)
+ separator.show()
+
+ erase_button = ToolButton('list-remove')
+ erase_button.set_tooltip(_('Erase'))
+ erase_button.connect('clicked', self._erase_button_clicked_cb)
+ self.add(erase_button)
+ erase_button.show()
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self._refresh_copy_palette()
+ self._refresh_resume_palette()
+
+ def _resume_clicked_cb(self, button):
+ if self._jobject:
+ misc.resume(self._jobject)
+
+ def _copy_clicked_cb(self, button):
+ clipboard = gtk.Clipboard()
+ clipboard.set_with_data([('text/uri-list', 0, 0)],
+ self._clipboard_get_func_cb,
+ self._clipboard_clear_func_cb)
+
+ def _clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ selection_data.set_uris(['file://' + self._jobject.file_path])
+
+ def _clipboard_clear_func_cb(self, clipboard, data):
+ pass
+
+ def _erase_button_clicked_cb(self, button):
+ if self._jobject:
+ bundle = misc.get_bundle(self._jobject)
+ if bundle is not None and bundle.is_installed():
+ bundle.uninstall()
+ datastore.delete(self._jobject.object_id)
+
+ def _resume_menu_item_activate_cb(self, menu_item, service_name):
+ if self._jobject:
+ misc.resume(self._jobject, service_name)
+
+ def _copy_menu_item_activate_cb(self, menu_item, volume):
+ if self._jobject:
+ datastore.copy(self._jobject, volume.id)
+
+ def _refresh_copy_palette(self):
+ palette = self._copy.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ volumes_manager = volumesmanager.get_volumes_manager()
+ for volume in volumes_manager.get_volumes():
+ if self._jobject.metadata['mountpoint'] == volume.id:
+ continue
+ menu_item = MenuItem(volume.name)
+ menu_item.set_image(Icon(icon_name=volume.icon_name,
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate',
+ self._copy_menu_item_activate_cb,
+ volume)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ def _refresh_resume_palette(self):
+ if self._jobject.metadata.get('activity_id', ''):
+ # TRANS: Action label for resuming an activity.
+ self._resume.set_tooltip(_('Resume'))
+ else:
+ # TRANS: Action label for starting an entry.
+ self._resume.set_tooltip(_('Start'))
+
+ palette = self._resume.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ for activity_info in misc.get_activities(self._jobject):
+ menu_item = MenuItem(activity_info.name)
+ menu_item.set_image(Icon(file=activity_info.icon,
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self._resume_menu_item_activate_cb,
+ activity_info.bundle_id)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py
new file mode 100644
index 0000000..8a86c83
--- /dev/null
+++ b/src/jarabe/journal/keepicon.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2006, 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 gobject
+import hippo
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+from sugar import profile
+
+class KeepIcon(CanvasIcon):
+ def __init__(self, keep):
+ CanvasIcon.__init__(self, icon_name='emblem-favorite',
+ box_width=style.GRID_CELL_SIZE * 3 / 5,
+ size=style.SMALL_ICON_SIZE)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ self._keep = None
+ self.set_keep(keep)
+
+ def set_keep(self, keep):
+ if keep == self._keep:
+ return
+
+ self._keep = keep
+ if keep:
+ self.props.xo_color = profile.get_color()
+ else:
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
+ def get_keep(self):
+ return self._keep
+
+ keep = gobject.property(type=int, default=0, getter=get_keep,
+ setter=set_keep)
+
+ def __motion_notify_event_cb(self, icon, event):
+ if not self._keep:
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
new file mode 100644
index 0000000..befc7f4
--- /dev/null
+++ b/src/jarabe/journal/listview.py
@@ -0,0 +1,460 @@
+# 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 traceback
+import sys
+from gettext import gettext as _
+
+import hippo
+import gobject
+import gtk
+import dbus
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+
+from jarabe.journal.collapsedentry import CollapsedEntry
+from jarabe.journal import query
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+UPDATE_INTERVAL = 300000
+
+EMPTY_JOURNAL = _("Your Journal is empty")
+NO_MATCH = _("No matching entries ")
+
+class BaseListView(gtk.HBox):
+ __gtype_name__ = 'BaseListView'
+
+ def __init__(self):
+ self._query = {}
+ self._result_set = None
+ self._entries = []
+ self._page_size = 0
+ self._last_value = -1
+ self._reflow_sid = 0
+
+ gtk.HBox.__init__(self)
+ self.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS)
+ self.connect('key-press-event', self._key_press_event_cb)
+
+ self._box = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_VERTICAL,
+ background_color=style.COLOR_WHITE.get_int())
+
+ self._canvas = hippo.Canvas()
+ self._canvas.set_root(self._box)
+
+ self.pack_start(self._canvas)
+ self._canvas.show()
+
+ self._vadjustment = gtk.Adjustment(value=0, lower=0, upper=0,
+ step_incr=1, page_incr=0,
+ page_size=0)
+ self._vadjustment.connect('value-changed',
+ self._vadjustment_value_changed_cb)
+ self._vadjustment.connect('changed', self._vadjustment_changed_cb)
+
+ self._vscrollbar = gtk.VScrollbar(self._vadjustment)
+ self.pack_end(self._vscrollbar, expand=False, fill=False)
+ self._vscrollbar.show()
+
+ self.connect('scroll-event', self._scroll_event_cb)
+ self.connect('destroy', self.__destroy_cb)
+
+ # DND stuff
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._last_clicked_entry = None
+ self._canvas.drag_source_set(0, [], 0)
+ self._canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK |
+ gtk.gdk.POINTER_MOTION_HINT_MASK)
+ self._canvas.connect_after("motion_notify_event",
+ self._canvas_motion_notify_event_cb)
+ self._canvas.connect("button_press_event",
+ self._canvas_button_press_event_cb)
+ self._canvas.connect("drag_end", self._drag_end_cb)
+ self._canvas.connect("drag_data_get", self._drag_data_get_cb)
+
+ # Auto-update stuff
+ self._fully_obscured = True
+ self._dirty = False
+ self._refresh_idle_handler = None
+ self._update_dates_timer = None
+
+ bus = dbus.SessionBus()
+ datastore = dbus.Interface(
+ bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE)
+ self._datastore_created_handler = \
+ datastore.connect_to_signal('Created',
+ self.__datastore_created_cb)
+ self._datastore_updated_handler = \
+ datastore.connect_to_signal('Updated',
+ self.__datastore_updated_cb)
+
+ self._datastore_deleted_handler = \
+ datastore.connect_to_signal('Deleted',
+ self.__datastore_deleted_cb)
+
+ def __destroy_cb(self, widget):
+ self._datastore_created_handler.remove()
+ self._datastore_updated_handler.remove()
+ self._datastore_deleted_handler.remove()
+
+ if self._result_set:
+ self._result_set.destroy()
+
+ def _vadjustment_changed_cb(self, vadjustment):
+ if vadjustment.props.upper > self._page_size:
+ self._vscrollbar.show()
+ else:
+ self._vscrollbar.hide()
+
+ def _vadjustment_value_changed_cb(self, vadjustment):
+ gobject.idle_add(self._do_scroll)
+
+ def _do_scroll(self, force=False):
+ import time
+ t = time.time()
+
+ value = int(self._vadjustment.props.value)
+
+ if value == self._last_value and not force:
+ return
+ self._last_value = value
+
+ self._result_set.seek(value)
+ jobjects = self._result_set.read(self._page_size)
+
+ if self._result_set.length != self._vadjustment.props.upper:
+ self._vadjustment.props.upper = self._result_set.length
+ self._vadjustment.changed()
+
+ self._refresh_view(jobjects)
+ self._dirty = False
+
+ logging.debug('_do_scroll %r %r\n' % (value, (time.time() - t)))
+
+ return False
+
+ def _refresh_view(self, jobjects):
+ logging.debug('ListView %r' % self)
+ # Indicate when the Journal is empty
+ if len(jobjects) == 0:
+ self._show_message(EMPTY_JOURNAL)
+ return
+
+ # Refresh view and create the entries if they don't exist yet.
+ for i in range(0, self._page_size):
+ try:
+ if i < len(jobjects):
+ if i >= len(self._entries):
+ entry = self.create_entry()
+ self._box.append(entry)
+ self._entries.append(entry)
+ entry.jobject = jobjects[i]
+ else:
+ entry = self._entries[i]
+ entry.jobject = jobjects[i]
+ entry.set_visible(True)
+ elif i < len(self._entries):
+ entry = self._entries[i]
+ entry.set_visible(False)
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ def create_entry(self):
+ """ Create a descendant of BaseCollapsedEntry
+ """
+ raise NotImplementedError
+
+ def update_with_query(self, query_dict):
+ logging.debug('ListView.update_with_query')
+ self._query = query_dict
+ if self._page_size > 0:
+ self.refresh()
+
+ def refresh(self):
+ if self._result_set:
+ self._result_set.destroy()
+ self._result_set = query.find(self._query)
+ self._vadjustment.props.upper = self._result_set.length
+ self._vadjustment.changed()
+
+ self._vadjustment.props.value = min(self._vadjustment.props.value,
+ self._result_set.length - self._page_size)
+ if self._result_set.length == 0:
+ if self._query.get('query', '') or \
+ self._query.get('mime_type', '') or \
+ self._query.get('mtime', ''):
+ self._show_message(NO_MATCH)
+ else:
+ self._show_message(EMPTY_JOURNAL)
+ else:
+ self._clear_message()
+ self._do_scroll(force=True)
+
+ def _scroll_event_cb(self, hbox, event):
+ if event.direction == gtk.gdk.SCROLL_UP:
+ if self._vadjustment.props.value > self._vadjustment.props.lower:
+ self._vadjustment.props.value -= 1
+ elif event.direction == gtk.gdk.SCROLL_DOWN:
+ max_value = self._result_set.length - self._page_size
+ if self._vadjustment.props.value < max_value:
+ self._vadjustment.props.value += 1
+
+ def do_focus(self, direction):
+ if not self.is_focus():
+ self.grab_focus()
+ return True
+ return False
+
+ def _key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+
+ if keyname == 'Up':
+ if self._vadjustment.props.value > self._vadjustment.props.lower:
+ self._vadjustment.props.value -= 1
+ elif keyname == 'Down':
+ max_value = self._result_set.length - self._page_size
+ if self._vadjustment.props.value < max_value:
+ self._vadjustment.props.value += 1
+ elif keyname == 'Page_Up' or keyname == 'KP_Page_Up':
+ new_position = max(0,
+ self._vadjustment.props.value - self._page_size)
+ if new_position != self._vadjustment.props.value:
+ self._vadjustment.props.value = new_position
+ elif keyname == 'Page_Down' or keyname == 'KP_Page_Down':
+ new_position = min(self._result_set.length - self._page_size,
+ self._vadjustment.props.value + self._page_size)
+ if new_position != self._vadjustment.props.value:
+ self._vadjustment.props.value = new_position
+ elif keyname == 'Home' or keyname == 'KP_Home':
+ new_position = 0
+ if new_position != self._vadjustment.props.value:
+ self._vadjustment.props.value = new_position
+ elif keyname == 'End' or keyname == 'KP_End':
+ new_position = max(0, self._result_set.length - self._page_size)
+ if new_position != self._vadjustment.props.value:
+ self._vadjustment.props.value = new_position
+ else:
+ return False
+
+ return True
+
+ def do_size_allocate(self, allocation):
+ gtk.HBox.do_size_allocate(self, allocation)
+ new_page_size = int(allocation.height / style.GRID_CELL_SIZE)
+
+ logging.debug("do_size_allocate: %r" % new_page_size)
+
+ if new_page_size != self._page_size:
+ self._page_size = new_page_size
+ self._queue_reflow()
+
+ def _queue_reflow(self):
+ if not self._reflow_sid:
+ self._reflow_sid = gobject.idle_add(self._reflow_idle_cb)
+
+ def _reflow_idle_cb(self):
+ self._box.clear()
+ self._entries = []
+
+ self._vadjustment.props.page_size = self._page_size
+ self._vadjustment.props.page_increment = self._page_size
+ self._vadjustment.changed()
+
+ if self._result_set is None:
+ self._result_set = query.find(self._query)
+
+ max_value = max(0, self._result_set.length - self._page_size)
+ if self._vadjustment.props.value > max_value:
+ self._vadjustment.props.value = max_value
+ else:
+ self._do_scroll(force=True)
+
+ self._reflow_sid = 0
+
+ def _show_message(self, message):
+ box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ background_color=style.COLOR_WHITE.get_int(),
+ yalign=hippo.ALIGNMENT_CENTER)
+ icon = CanvasIcon(size=style.LARGE_ICON_SIZE,
+ icon_name='activity-journal',
+ stroke_color = style.COLOR_BUTTON_GREY.get_svg(),
+ fill_color = style.COLOR_TRANSPARENT.get_svg())
+ text = hippo.CanvasText(text=message,
+ xalign=hippo.ALIGNMENT_CENTER,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ color = style.COLOR_BUTTON_GREY.get_int())
+
+ box.append(icon)
+ box.append(text)
+ self._canvas.set_root(box)
+
+ def _clear_message(self):
+ self._canvas.set_root(self._box)
+
+ # TODO: Dnd methods. This should be merged somehow inside hippo-canvas.
+ def _canvas_motion_notify_event_cb(self, widget, event):
+ if not self._pressed_button:
+ return True
+
+ # if the mouse button is not pressed, no drag should occurr
+ if not event.state & gtk.gdk.BUTTON1_MASK:
+ self._pressed_button = None
+ return True
+
+ logging.debug("motion_notify_event_cb")
+
+ if event.is_hint:
+ x, y, state_ = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+
+ if widget.drag_check_threshold(int(self._press_start_x),
+ int(self._press_start_y),
+ int(x),
+ int(y)):
+ context_ = widget.drag_begin([('text/uri-list', 0, 0),
+ ('journal-object-id', 0, 0)],
+ gtk.gdk.ACTION_COPY,
+ 1,
+ event)
+ return True
+
+ def _drag_end_cb(self, widget, drag_context):
+ logging.debug("drag_end_cb")
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._last_clicked_entry = None
+
+ def _drag_data_get_cb(self, widget, context, selection, target_type,
+ event_time):
+ logging.debug("drag_data_get_cb: requested target " + selection.target)
+
+ jobject = self._last_clicked_entry.jobject
+ if selection.target == 'text/uri-list':
+ selection.set(selection.target, 8, jobject.file_path)
+ elif selection.target == 'journal-object-id':
+ selection.set(selection.target, 8, jobject.object_id)
+
+ def _canvas_button_press_event_cb(self, widget, event):
+ logging.debug("button_press_event_cb")
+
+ if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS:
+ self._last_clicked_entry = \
+ self._get_entry_at_coords(event.x, event.y)
+ if self._last_clicked_entry:
+ self._pressed_button = event.button
+ self._press_start_x = event.x
+ self._press_start_y = event.y
+
+ return False
+
+ def _get_entry_at_coords(self, x, y):
+ for entry in self._box.get_children():
+ entry_x, entry_y = entry.get_context().translate_to_widget(entry)
+ entry_width, entry_height = entry.get_allocation()
+
+ if (x >= entry_x ) and (x <= entry_x + entry_width) and \
+ (y >= entry_y ) and (y <= entry_y + entry_height):
+ return entry
+ return None
+
+ def update_dates(self):
+ logging.debug('ListView.update_dates')
+ for entry in self._entries:
+ entry.update_date()
+
+ def __datastore_created_cb(self, uid):
+ self._set_dirty()
+
+ def __datastore_updated_cb(self, uid):
+ self._set_dirty()
+
+ def __datastore_deleted_cb(self, uid):
+ self._set_dirty()
+
+ def _set_dirty(self):
+ if self._fully_obscured:
+ self._dirty = True
+ else:
+ self._schedule_refresh()
+
+ def _schedule_refresh(self):
+ if self._refresh_idle_handler is None:
+ logging.debug('Add refresh idle callback')
+ self._refresh_idle_handler = \
+ gobject.idle_add(self.__refresh_idle_cb)
+
+ def __refresh_idle_cb(self):
+ self.refresh()
+ if self._refresh_idle_handler is not None:
+ logging.debug('Remove refresh idle callback')
+ gobject.source_remove(self._refresh_idle_handler)
+ self._refresh_idle_handler = None
+ return False
+
+ def set_is_visible(self, visible):
+ logging.debug('canvas_visibility_notify_event_cb %r' % visible)
+ if visible:
+ self._fully_obscured = False
+ if self._dirty:
+ self._schedule_refresh()
+ if self._update_dates_timer is None:
+ logging.debug('Adding date updating timer')
+ self._update_dates_timer = \
+ gobject.timeout_add(UPDATE_INTERVAL,
+ self.__update_dates_timer_cb)
+ else:
+ self._fully_obscured = True
+ if self._update_dates_timer is not None:
+ logging.debug('Remove date updating timer')
+ gobject.source_remove(self._update_dates_timer)
+ self._update_dates_timer = None
+
+ def __update_dates_timer_cb(self):
+ self.update_dates()
+ return True
+
+class ListView(BaseListView):
+ __gtype_name__ = 'ListView'
+
+ __gsignals__ = {
+ 'detail-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ BaseListView.__init__(self)
+
+ def create_entry(self):
+ entry = CollapsedEntry()
+ entry.connect('detail-clicked', self.__entry_activated_cb)
+ return entry
+
+ def __entry_activated_cb(self, entry):
+ self.emit('detail-clicked', entry)
+
diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py
new file mode 100644
index 0000000..42d179b
--- /dev/null
+++ b/src/jarabe/journal/misc.py
@@ -0,0 +1,188 @@
+# 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 time
+import traceback
+import sys
+import os
+from gettext import gettext as _
+
+import gtk
+
+from sugar import activity
+from sugar.activity import activityfactory
+from sugar.activity.activityhandle import ActivityHandle
+from sugar import mime
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.bundle.contentbundle import ContentBundle
+from sugar.bundle.bundle import MalformedBundleException
+from sugar import util
+
+from jarabe.journal.journalentrybundle import JournalEntryBundle
+
+def _get_icon_file_name(icon_name):
+ icon_theme = gtk.icon_theme_get_default()
+ info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ if not info:
+ # display standard icon when icon for mime type is not found
+ info = icon_theme.lookup_icon('application-octet-stream',
+ gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ fname = info.get_filename()
+ del info
+ return fname
+
+_icon_cache = util.LRU(50)
+
+def get_icon_name(jobject):
+
+ cache_key = (jobject.object_id, jobject.metadata.get('timestamp', None))
+ if cache_key in _icon_cache:
+ return _icon_cache[cache_key]
+
+ file_name = None
+
+ if jobject.is_activity_bundle() and jobject.file_path:
+ try:
+ bundle = ActivityBundle(jobject.file_path)
+ file_name = bundle.get_icon()
+ except Exception:
+ logging.warning('Could not read bundle:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+ file_name = _get_icon_file_name('application-octet-stream')
+
+ if not file_name and jobject.metadata['activity']:
+ service_name = jobject.metadata['activity']
+ activity_info = activity.get_registry().get_activity(service_name)
+ if activity_info:
+ file_name = activity_info.icon
+
+ mime_type = jobject.metadata['mime_type']
+ if not file_name and mime_type:
+ icon_name = mime.get_mime_icon(mime_type)
+ if icon_name:
+ file_name = _get_icon_file_name(icon_name)
+
+ if not file_name or not os.path.exists(file_name):
+ file_name = _get_icon_file_name('application-octet-stream')
+
+ _icon_cache[cache_key] = file_name
+
+ return file_name
+
+def get_date(jobject):
+ """ Convert from a string in iso format to a more human-like format. """
+ if jobject.metadata.has_key('timestamp'):
+ timestamp = float(jobject.metadata['timestamp'])
+ return util.timestamp_to_elapsed_string(timestamp)
+ elif jobject.metadata.has_key('mtime'):
+ ti = time.strptime(jobject.metadata['mtime'], "%Y-%m-%dT%H:%M:%S")
+ return util.timestamp_to_elapsed_string(time.mktime(ti))
+ else:
+ return _('No date')
+
+def get_bundle(jobject):
+ try:
+ if jobject.is_activity_bundle() and jobject.file_path:
+ return ActivityBundle(jobject.file_path)
+ elif jobject.is_content_bundle() and jobject.file_path:
+ return ContentBundle(jobject.file_path)
+ elif jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE \
+ and jobject.file_path:
+ return JournalEntryBundle(jobject.file_path)
+ else:
+ return None
+ except MalformedBundleException, e:
+ logging.warning('Incorrect bundle: %r' % e)
+ return None
+
+def _get_activities_for_mime(mime_type):
+ registry = activity.get_registry()
+ result = registry.get_activities_for_type(mime_type)
+ if not result:
+ for parent_mime in mime.get_mime_parents(mime_type):
+ result.extend(registry.get_activities_for_type(parent_mime))
+ return result
+
+def get_activities(jobject):
+ activities = []
+
+ bundle_id = jobject.metadata.get('activity', '')
+ if bundle_id:
+ activity_info = activity.get_registry().get_activity(bundle_id)
+ if activity_info:
+ activities.append(activity_info)
+
+ mime_type = jobject.metadata.get('mime_type', '')
+ if mime_type:
+ activities_info = _get_activities_for_mime(mime_type)
+ for activity_info in activities_info:
+ if activity_info.bundle_id != bundle_id:
+ activities.append(activity_info)
+
+ return activities
+
+def resume(jobject, bundle_id=None):
+ if jobject.is_activity_bundle() and not bundle_id:
+
+ logging.debug('Creating activity bundle')
+ bundle = ActivityBundle(jobject.file_path)
+ if not bundle.is_installed():
+ logging.debug('Installing activity bundle')
+ bundle.install()
+ elif bundle.need_upgrade():
+ logging.debug('Upgrading activity bundle')
+ bundle.upgrade()
+
+ logging.debug('activityfactory.creating bundle with id %r',
+ bundle.get_bundle_id())
+ activityfactory.create(bundle.get_bundle_id())
+
+ elif jobject.is_content_bundle() and not bundle_id:
+
+ logging.debug('Creating content bundle')
+ bundle = ContentBundle(jobject.file_path)
+ if not bundle.is_installed():
+ logging.debug('Installing content bundle')
+ bundle.install()
+
+ activities = _get_activities_for_mime('text/html')
+ if len(activities) == 0:
+ logging.warning('No activity can open HTML content bundles')
+ return
+
+ uri = bundle.get_start_uri()
+ logging.debug('activityfactory.creating with uri %s', uri)
+ activityfactory.create_with_uri(activities[0].bundle_id,
+ bundle.get_start_uri())
+ else:
+ if not get_activities(jobject) and bundle_id is None:
+ logging.warning('No activity can open this object, %s.' %
+ jobject.metadata.get('mime_type', None))
+ return
+ if bundle_id is None:
+ bundle_id = get_activities(jobject)[0].bundle_id
+
+ activity_id = jobject.metadata['activity_id']
+ object_id = jobject.object_id
+
+ if activity_id:
+ handle = ActivityHandle(object_id=object_id,
+ activity_id=activity_id)
+ activityfactory.create(bundle_id, handle)
+ else:
+ activityfactory.create_with_object_id(bundle_id, object_id)
+
diff --git a/src/jarabe/journal/modalalert.py b/src/jarabe/journal/modalalert.py
new file mode 100644
index 0000000..6c7bce9
--- /dev/null
+++ b/src/jarabe/journal/modalalert.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2008 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 gtk
+from gettext import gettext as _
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar import profile
+
+class ModalAlert(gtk.Window):
+
+ __gtype_name__ = 'SugarModalAlert'
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._main_view = gtk.EventBox()
+ self._vbox = gtk.VBox()
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self._vbox.set_border_width(style.GRID_CELL_SIZE * 2)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._main_view.add(self._vbox)
+ self._vbox.show()
+
+ icon = Icon(icon_name='activity-journal',
+ pixel_size=style.XLARGE_ICON_SIZE,
+ xo_color=profile.get_color())
+ self._vbox.pack_start(icon, False)
+ icon.show()
+
+ self._title = gtk.Label()
+ self._title.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._title.set_markup('<b>%s</b>' % _('Your Journal is full'))
+ self._vbox.pack_start(self._title, False)
+ self._title.show()
+
+ self._message = gtk.Label(_('Please delete some old Journal'
+ ' entries to make space for new ones.'))
+ self._message.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._message, False)
+ self._message.show()
+
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
+ self._vbox.pack_start(alignment, expand=False)
+ alignment.show()
+
+ self._show_journal = gtk.Button()
+ self._show_journal.set_label(_('Show Journal'))
+ alignment.add(self._show_journal)
+ self._show_journal.show()
+ self._show_journal.connect('clicked', self.__show_journal_cb)
+
+ self.add(self._main_view)
+ self._main_view.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def __show_journal_cb(self, button):
+ '''The opener will listen on the destroy signal
+ '''
+ self.destroy()
+
diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py
new file mode 100644
index 0000000..947141d
--- /dev/null
+++ b/src/jarabe/journal/objectchooser.py
@@ -0,0 +1,199 @@
+# 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
+
+from gettext import gettext as _
+import logging
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar.graphics.toolbutton import ToolButton
+from sugar.datastore import datastore
+
+from jarabe.journal.listview import ListView
+from jarabe.journal.collapsedentry import BaseCollapsedEntry
+from jarabe.journal.journaltoolbox import SearchToolbar
+from jarabe.journal.volumestoolbar import VolumesToolbar
+
+class ObjectChooser(gtk.Window):
+
+ __gtype_name__ = 'ObjectChooser'
+
+ __gsignals__ = {
+ 'response': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([int]))
+ }
+
+ def __init__(self, parent=None):
+ gtk.Window.__init__(self)
+ self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.set_decorated(False)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_border_width(style.LINE_WIDTH)
+
+ self._selected_object_id = None
+
+ self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('delete-event', self.__delete_event_cb)
+ self.connect('key-press-event', self.__key_press_event_cb)
+ if parent is not None:
+ self.connect('realize', self.__realize_cb, parent)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+ vbox.show()
+
+ title_box = TitleBox()
+ title_box.connect('volume-changed', self.__volume_changed_cb)
+ title_box.close_button.connect('clicked',
+ self.__close_button_clicked_cb)
+ title_box.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(title_box, expand=False)
+ title_box.show()
+
+ separator = gtk.HSeparator()
+ vbox.pack_start(separator, expand=False)
+ separator.show()
+
+ self._toolbar = SearchToolbar()
+ self._toolbar.connect('query-changed', self.__query_changed_cb)
+ self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ self._list_view = ChooserListView()
+ self._list_view.connect('entry-activated', self.__entry_activated_cb)
+ vbox.pack_start(self._list_view)
+ self._list_view.show()
+
+ self._toolbar.set_volume_id(datastore.mounts()[0]['id'])
+
+ width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2
+ height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2
+ self.set_size_request(width, height)
+
+ def __realize_cb(self, chooser, parent):
+ self.window.set_transient_for(parent)
+ # TODO: Should we disconnect the signal here?
+
+ def __entry_activated_cb(self, list_view, entry):
+ self._selected_object_id = entry.jobject.object_id
+ self.emit('response', gtk.RESPONSE_ACCEPT)
+
+ def __delete_event_cb(self, chooser, event):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Escape':
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __close_button_clicked_cb(self, button):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def get_selected_object_id(self):
+ return self._selected_object_id
+
+ def __query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+
+ def __volume_changed_cb(self, volume_toolbar, volume_id):
+ logging.debug('Selected volume: %r.' % volume_id)
+ self._toolbar.set_volume_id(volume_id)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r' % self)
+ visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+class TitleBox(VolumesToolbar):
+ __gtype_name__ = 'TitleBox'
+
+ def __init__(self):
+ VolumesToolbar.__init__(self)
+
+ label = gtk.Label()
+ label.set_markup('<b>%s</b>' % _('Choose an object'))
+ label.set_alignment(0, 0.5)
+ self._add_widget(label, expand=True)
+
+ self.close_button = ToolButton(icon_name='dialog-cancel')
+ self.close_button.set_tooltip(_('Close'))
+ self.insert(self.close_button, -1)
+ self.close_button.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+class ChooserCollapsedEntry(BaseCollapsedEntry):
+ __gtype_name__ = 'ChooserCollapsedEntry'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self):
+ BaseCollapsedEntry.__init__(self)
+
+ self.connect_after('button-release-event',
+ self.__button_release_event_cb)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def __button_release_event_cb(self, entry, event):
+ self.emit('entry-activated')
+ return True
+
+ def __motion_notify_event_cb(self, entry, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ self.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ self.props.background_color = style.COLOR_WHITE.get_int()
+ return False
+
+class ChooserListView(ListView):
+ __gtype_name__ = 'ChooserListView'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ ListView.__init__(self)
+
+ def create_entry(self):
+ entry = ChooserCollapsedEntry()
+ entry.connect('entry-activated', self.__entry_activated_cb)
+ return entry
+
+ def __entry_activated_cb(self, entry):
+ self.emit('entry-activated', entry)
+
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
new file mode 100644
index 0000000..dfed3b9
--- /dev/null
+++ b/src/jarabe/journal/palettes.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2008 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
+
+from gettext import gettext as _
+import logging
+
+import gtk
+
+from sugar import profile
+from sugar.graphics import style
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.datastore import datastore
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.journal import misc
+
+class ObjectPalette(Palette):
+ def __init__(self, jobject):
+
+ self._jobject = jobject
+
+ activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+ activity_icon.props.file = misc.get_icon_name(jobject)
+ if jobject.metadata.has_key('icon-color') and \
+ jobject.metadata['icon-color']:
+ activity_icon.props.xo_color = \
+ XoColor(jobject.metadata['icon-color'])
+ else:
+ activity_icon.props.xo_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+
+ if jobject.metadata.has_key('title'):
+ title = jobject.metadata['title']
+ else:
+ title = _('Untitled')
+
+ Palette.__init__(self, primary_text=title,
+ icon=activity_icon)
+
+ if jobject.metadata.get('activity_id', ''):
+ resume_label = _('Resume')
+ else:
+ resume_label = _('Start')
+ menu_item = MenuItem(resume_label, 'activity-start')
+ menu_item.connect('activate', self.__start_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: Add "Start with" menu item
+
+ menu_item = MenuItem(_('Copy'))
+ icon = Icon(icon_name='edit-copy', xo_color=profile.get_color(),
+ icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ menu_item.connect('activate', self.__copy_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Erase'), 'list-remove')
+ menu_item.connect('activate', self.__erase_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __start_activate_cb(self, menu_item):
+ misc.resume(self._jobject)
+
+ def __copy_activate_cb(self, menu_item):
+ clipboard = gtk.Clipboard()
+ clipboard.set_with_data([('text/uri-list', 0, 0)],
+ self.__clipboard_get_func_cb,
+ self.__clipboard_clear_func_cb)
+
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ logging.debug('__clipboard_get_func_cb %r' % self._jobject.file_path)
+ selection_data.set_uris(['file://' + self._jobject.file_path])
+
+ def __clipboard_clear_func_cb(self, clipboard, data):
+ pass
+
+ def __erase_activate_cb(self, menu_item):
+ bundle = misc.get_bundle(self._jobject)
+ if bundle is not None and bundle.is_installed():
+ bundle.uninstall()
+ datastore.delete(self._jobject.object_id)
+
+
+class BuddyPalette(Palette):
+ def __init__(self, buddy):
+ self._buddy = buddy
+
+ nick, colors = buddy
+ buddy_icon = Icon(icon_name='computer-xo',
+ icon_size=style.STANDARD_ICON_SIZE,
+ xo_color=XoColor(colors))
+
+ Palette.__init__(self, primary_text=nick,
+ icon=buddy_icon)
+
+ # TODO: Support actions on buddies, like make friend, invite, etc.
diff --git a/src/jarabe/journal/query.py b/src/jarabe/journal/query.py
new file mode 100644
index 0000000..04d9b16
--- /dev/null
+++ b/src/jarabe/journal/query.py
@@ -0,0 +1,266 @@
+# 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
+
+from sugar.datastore import datastore
+
+# Properties the journal cares about.
+PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies',
+ 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint',
+ 'activity_id']
+
+class _Cache(object):
+
+ __gtype_name__ = 'query_Cache'
+
+ def __init__(self, jobjects=None):
+ self._array = []
+ self._dict = {}
+ if jobjects is not None:
+ self.append_all(jobjects)
+
+ def prepend_all(self, jobjects):
+ for jobject in jobjects[::-1]:
+ self._array.insert(0, jobject)
+ self._dict[jobject.object_id] = jobject
+
+ def append_all(self, jobjects):
+ for jobject in jobjects:
+ self._array.append(jobject)
+ self._dict[jobject.object_id] = jobject
+
+ def remove_all(self, jobjects):
+ jobjects = jobjects[:]
+ for jobject in jobjects:
+ obj = self._dict[jobject.object_id]
+ self._array.remove(obj)
+ del self._dict[obj.object_id]
+ obj.destroy()
+
+ def __len__(self):
+ return len(self._array)
+
+ def __getitem__(self, key):
+ if isinstance(key, basestring):
+ return self._dict[key]
+ else:
+ return self._array[key]
+
+ def destroy(self):
+ self._destroy_jobjects(self._array)
+ self._array = []
+ self._dict = {}
+
+ def _destroy_jobjects(self, jobjects):
+ for jobject in jobjects:
+ jobject.destroy()
+
+class ResultSet(object):
+
+ _CACHE_LIMIT = 80
+
+ def __init__(self, query, sorting):
+ self._total_count = -1
+ self._position = -1
+ self._query = query
+ self._sorting = sorting
+
+ self._offset = 0
+ self._cache = _Cache()
+
+ def destroy(self):
+ self._cache.destroy()
+
+ def get_length(self):
+ if self._total_count == -1:
+ jobjects, self._total_count = datastore.find(self._query,
+ sorting=self._sorting,
+ limit=ResultSet._CACHE_LIMIT,
+ properties=PROPERTIES)
+ self._cache.append_all(jobjects)
+ self._offset = 0
+ return self._total_count
+
+ length = property(get_length)
+
+ def seek(self, position):
+ self._position = position
+
+ def read(self, max_count):
+ logging.debug('ResultSet.read position: %r' % self._position)
+
+ if max_count * 5 > ResultSet._CACHE_LIMIT:
+ raise RuntimeError(
+ 'max_count (%i) too big for ResultSet._CACHE_LIMIT'
+ ' (%i).' % (max_count, ResultSet._CACHE_LIMIT))
+
+ if self._position == -1:
+ self.seek(0)
+
+ if self._position < self._offset:
+ remaining_forward_entries = 0
+ else:
+ remaining_forward_entries = self._offset + len(self._cache) - \
+ self._position
+
+ if self._position > self._offset + len(self._cache):
+ remaining_backwards_entries = 0
+ else:
+ remaining_backwards_entries = self._position - self._offset
+
+ last_cached_entry = self._offset + len(self._cache)
+
+ if (remaining_forward_entries <= 0 and
+ remaining_backwards_entries <= 0) or \
+ max_count > ResultSet._CACHE_LIMIT:
+
+ # Total cache miss: remake it
+ offset = max(0, self._position - max_count)
+ logging.debug('remaking cache, offset: %r limit: %r' % \
+ (offset, max_count * 2))
+ jobjects, self._total_count = datastore.find(self._query,
+ sorting=self._sorting,
+ offset=offset,
+ limit=ResultSet._CACHE_LIMIT,
+ properties=PROPERTIES)
+
+ self._cache.remove_all(self._cache)
+ self._cache.append_all(jobjects)
+ self._offset = offset
+
+ elif remaining_forward_entries < 2 * max_count and \
+ last_cached_entry < self._total_count:
+
+ # Add one page to the end of cache
+ logging.debug('appending one more page, offset: %r' % \
+ last_cached_entry)
+ jobjects, self._total_count = datastore.find(self._query,
+ sorting=self._sorting,
+ offset=last_cached_entry,
+ limit=max_count,
+ properties=PROPERTIES)
+ # update cache
+ self._cache.append_all(jobjects)
+
+ # apply the cache limit
+ objects_excess = len(self._cache) - ResultSet._CACHE_LIMIT
+ if objects_excess > 0:
+ self._offset += objects_excess
+ self._cache.remove_all(self._cache[:objects_excess])
+
+ elif remaining_backwards_entries < 2 * max_count and self._offset > 0:
+
+ # Add one page to the beginning of cache
+ limit = min(self._offset, max_count)
+ self._offset = max(0, self._offset - max_count)
+
+ logging.debug('prepending one more page, offset: %r limit: %r' %
+ (self._offset, limit))
+ jobjects, self._total_count = datastore.find(self._query,
+ sorting=self._sorting,
+ offset=self._offset,
+ limit=limit,
+ properties=PROPERTIES)
+
+ # update cache
+ self._cache.prepend_all(jobjects)
+
+ # apply the cache limit
+ objects_excess = len(self._cache) - ResultSet._CACHE_LIMIT
+ if objects_excess > 0:
+ self._cache.remove_all(self._cache[-objects_excess:])
+ else:
+ logging.debug('cache hit and no need to grow the cache')
+
+ first_pos = self._position - self._offset
+ last_pos = self._position - self._offset + max_count
+ return self._cache[first_pos:last_pos]
+
+def find(query, sorting=None):
+ if sorting is None:
+ sorting = ['-mtime']
+ result_set = ResultSet(query, sorting)
+ return result_set
+
+def test():
+ TOTAL_ITEMS = 1000
+ SCREEN_SIZE = 10
+
+ def mock_debug(string):
+ print "\tDEBUG: %s" % string
+ logging.debug = mock_debug
+
+ def mock_find(query, sorting=None, limit=None, offset=None,
+ properties=None):
+ if properties is None:
+ properties = []
+
+ print "mock_find %r %r" % (offset, (offset + limit))
+
+ if limit is None or offset is None:
+ raise RuntimeError("Unimplemented test.")
+
+ result = []
+ for index in range(offset, offset + limit):
+ obj = datastore.DSObject(index, datastore.DSMetadata({}), '')
+ result.append(obj)
+
+ return result, TOTAL_ITEMS
+ datastore.find = mock_find
+
+ result_set = find({})
+
+ print "Get first page"
+ objects = result_set.read(SCREEN_SIZE)
+ print [obj.object_id for obj in objects]
+ assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects]
+ print ""
+
+ print "Scroll to 5th item"
+ result_set.seek(5)
+ objects = result_set.read(SCREEN_SIZE)
+ print [obj.object_id for obj in objects]
+ assert range(5, SCREEN_SIZE + 5) == [obj.object_id for obj in objects]
+ print ""
+
+ print "Scroll back to beginning"
+ result_set.seek(0)
+ objects = result_set.read(SCREEN_SIZE)
+ print [obj.object_id for obj in objects]
+ assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects]
+ print ""
+
+ print "Hit PgDn five times"
+ for i in range(0, 5):
+ result_set.seek((i + 1) * SCREEN_SIZE)
+ objects = result_set.read(SCREEN_SIZE)
+ print [obj.object_id for obj in objects]
+ assert range((i + 1) * SCREEN_SIZE, (i + 2) * SCREEN_SIZE) == \
+ [obj.object_id for obj in objects]
+ print ""
+
+ print "Hit PgUp five times"
+ for i in range(0, 5)[::-1]:
+ result_set.seek(i * SCREEN_SIZE)
+ objects = result_set.read(SCREEN_SIZE)
+ print [obj.object_id for obj in objects]
+ assert range(i * SCREEN_SIZE, (i + 1) * SCREEN_SIZE) == \
+ [obj.object_id for obj in objects]
+ print ""
+
+if __name__ == "__main__":
+ test()
diff --git a/src/jarabe/journal/volumesmanager.py b/src/jarabe/journal/volumesmanager.py
new file mode 100644
index 0000000..b2ef08d
--- /dev/null
+++ b/src/jarabe/journal/volumesmanager.py
@@ -0,0 +1,315 @@
+# 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
+from gettext import gettext as _
+
+import gobject
+import dbus
+
+from sugar import profile
+from sugar.datastore import datastore
+
+HAL_SERVICE_NAME = 'org.freedesktop.Hal'
+HAL_MANAGER_PATH = '/org/freedesktop/Hal/Manager'
+HAL_MANAGER_IFACE = 'org.freedesktop.Hal.Manager'
+HAL_DEVICE_IFACE = 'org.freedesktop.Hal.Device'
+HAL_VOLUME_IFACE = 'org.freedesktop.Hal.Device.Volume'
+
+MOUNT_OPTION_UID = 500
+MOUNT_OPTION_UMASK = 000
+
+_volumes_manager = None
+
+class VolumesManager(gobject.GObject):
+
+ __gtype_name__ = 'VolumesManager'
+
+ __gsignals__ = {
+ 'volume-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object])),
+ 'volume-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._volumes = []
+
+ # Internal flash is not in HAL
+ internal_fash_id = datastore.mounts()[0]['id']
+ self._volumes.append(Volume(internal_fash_id, _('Journal'),
+ 'activity-journal', profile.get_color(),
+ None, False))
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ self._hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ self._hal_manager.connect_to_signal('DeviceAdded',
+ self._hal_device_added_cb)
+
+ for udi in self._hal_manager.FindDeviceByCapability('volume'):
+ if self._is_device_relevant(udi):
+ try:
+ self._add_hal_device(udi)
+ except Exception, e:
+ logging.error('Exception when mounting device %r: %r' % \
+ (udi, e))
+
+ def get_volumes(self):
+ return self._volumes
+
+ def _get_volume_by_udi(self, udi):
+ for volume in self._volumes:
+ if volume.udi == udi:
+ return volume
+ return None
+
+ def _hal_device_added_cb(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+ if device.QueryCapability('volume'):
+ logging.debug('VolumesManager._hal_device_added_cb: %r', udi)
+ if self._is_device_relevant(udi):
+ self._add_hal_device(udi)
+
+ def _is_device_relevant(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # Ignore volumes without a filesystem.
+ if device.GetProperty('volume.fsusage') != 'filesystem':
+ return False
+ # Ignore root.
+ if device.GetProperty('volume.mount_point') == '/':
+ return False
+
+ storage_udi = device.GetProperty('block.storage_device')
+ obj = bus.get_object(HAL_SERVICE_NAME, storage_udi)
+ storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE)
+
+ # Ignore non-removable storage.
+ if not storage_device.GetProperty('storage.hotpluggable'):
+ return False
+
+ return True
+
+ def _add_hal_device(self, udi):
+ logging.debug('VolumeToolbar._add_hal_device: %r' % udi)
+
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # listen to mount/unmount
+ device.connect_to_signal('PropertyModified',
+ lambda *args: self._hal_device_property_modified_cb(udi, *args))
+
+ bus.add_signal_receiver(self._hal_device_removed_cb,
+ 'DeviceRemoved',
+ HAL_MANAGER_IFACE, HAL_SERVICE_NAME,
+ HAL_MANAGER_PATH, arg0=udi)
+
+ if device.GetProperty('volume.is_mounted'):
+ volume_id = self._mount_in_datastore(udi)
+ return
+
+ label = device.GetProperty('volume.label')
+ fs_type = device.GetProperty('volume.fstype')
+ valid_options = device.GetProperty('volume.mount.valid_options')
+ options = []
+
+ if 'uid=' in valid_options:
+ options.append('uid=%i' % MOUNT_OPTION_UID)
+
+ if 'umask=' in valid_options:
+ options.append('umask=%i' % MOUNT_OPTION_UMASK)
+
+ if 'noatime' in valid_options:
+ options.append('noatime')
+
+ if 'utf8' in valid_options:
+ options.append('utf8')
+
+ if 'iocharset=' in valid_options:
+ options.append('iocharset=utf8')
+
+ mount_point = label
+ if not mount_point:
+ mount_point = device.GetProperty('volume.uuid')
+
+ volume = dbus.Interface(device_object, HAL_VOLUME_IFACE)
+
+ # Try 100 times to get a mount point
+ mounted = False
+ i = 0
+ while not mounted:
+ try:
+ if i > 0:
+ volume.Mount('%s_%d' % (mount_point, i), fs_type, options)
+ else:
+ volume.Mount(mount_point, fs_type, options)
+ mounted = True
+ except dbus.DBusException, e:
+ s = 'org.freedesktop.Hal.Device.Volume.MountPointNotAvailable'
+ if i < 100 and e.get_dbus_name() == s:
+ i += 1
+ else:
+ raise
+
+ def _hal_device_property_modified_cb(self, udi, count, changes):
+ if 'volume.is_mounted' in [change[0] for change in changes]:
+ logging.debug('VolumesManager._hal_device_property_modified: %r' % \
+ (udi))
+ bus = dbus.SystemBus()
+ #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ # TODO: Why this doesn't work?
+ #if not hal_manager.DeviceExists(udi):
+ # return
+
+ proxy = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(proxy, HAL_DEVICE_IFACE)
+ try:
+ is_mounted = device.GetProperty('volume.is_mounted')
+ except dbus.DBusException, e:
+ logging.debug('e: %s' % e)
+ return
+
+ if is_mounted:
+ if self._get_volume_by_udi(udi) is not None:
+ # device already mounted in the datastore
+ return
+ volume_id = self._mount_in_datastore(udi)
+ else:
+ self.unmount_from_datastore(udi)
+ return
+
+ def _mount_in_datastore(self, udi):
+ logging.debug('VolumeToolbar._mount_in_datastore: %r' % udi)
+
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ mount_point = device.GetProperty('volume.mount_point')
+ ds_mounts = datastore.mounts()
+ for ds_mount in ds_mounts:
+ if mount_point == ds_mount['uri']:
+ return ds_mount['id']
+
+ mount_id = datastore.mount('inplace:' + mount_point,
+ dict(title=mount_point))
+ if not mount_id:
+ self._unmount_hal_device(udi)
+ raise RuntimeError('datastore.mount(%r, %r) failed.' % (
+ 'inplace:' + mount_point,
+ dict(title=mount_point)))
+
+ volume_name = device.GetProperty('volume.label')
+ if not volume_name:
+ volume_name = device.GetProperty('volume.uuid')
+ volume = Volume(mount_id,
+ volume_name,
+ self._get_icon_for_volume(udi),
+ profile.get_color(),
+ udi,
+ True)
+ self._volumes.append(volume)
+ self.emit('volume-added', volume)
+
+ logging.debug('mounted volume %s' % mount_point)
+
+ def _hal_device_removed_cb(self, udi):
+ logging.debug('VolumesManager._hal_device_removed_cb: %r', udi)
+ bus = dbus.SystemBus()
+ #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ # TODO: Why this doesn't work?
+ #if not hal_manager.DeviceExists(udi):
+ # self._unmount_from_datastore(udi)
+ # self._remove_button(udi)
+ # return
+
+ proxy = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(proxy, HAL_DEVICE_IFACE)
+ try:
+ is_mounted = device.GetProperty('volume.is_mounted')
+ except dbus.DBusException, e:
+ logging.debug('e: %s' % e)
+ self.unmount_from_datastore(udi)
+ return
+
+ if is_mounted:
+ self._unmount_from_datastore(udi)
+ self._unmount_hal_device(udi)
+
+ def unmount_from_datastore(self, udi):
+ logging.debug('VolumesManager._unmount_from_datastore: %r', udi)
+ volume = self._get_volume_by_udi(udi)
+ if volume is not None:
+ datastore.unmount(volume.id)
+
+ self._volumes.remove(volume)
+ self.emit('volume-removed', volume)
+
+ def unmount_hal_device(self, udi):
+ logging.debug('VolumesManager._unmount_hal_device: %r', udi)
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ volume = dbus.Interface(device_object, HAL_VOLUME_IFACE)
+ volume.Unmount([])
+
+ def _get_icon_for_volume(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ storage_udi = device.GetProperty('block.storage_device')
+ obj = bus.get_object(HAL_SERVICE_NAME, storage_udi)
+ storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE)
+
+ storage_drive_type = storage_device.GetProperty('storage.drive_type')
+ if storage_drive_type == 'sd_mmc':
+ return 'media-flash-sd-mmc'
+ else:
+ return 'media-flash-usb'
+
+class Volume(object):
+ def __init__(self, volume_id, name, icon_name, icon_color, udi,
+ can_unmount):
+ self.id = volume_id
+ self.name = name
+ self.icon_name = icon_name
+ self.icon_color = icon_color
+ self.udi = udi
+ self.can_unmount = can_unmount
+
+ def unmount(self):
+ get_volumes_manager().unmount_from_datastore(self.udi)
+ get_volumes_manager().unmount_hal_device(self.udi)
+
+def get_volumes_manager():
+ global _volumes_manager
+ if _volumes_manager is None:
+ _volumes_manager = VolumesManager()
+ return _volumes_manager
+
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
new file mode 100644
index 0000000..f4f21db
--- /dev/null
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -0,0 +1,137 @@
+# 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
+from gettext import gettext as _
+
+import gobject
+import gtk
+
+from sugar.datastore import datastore
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.palette import Palette
+
+from jarabe.journal import volumesmanager
+
+class VolumesToolbar(gtk.Toolbar):
+ __gtype_name__ = 'VolumesToolbar'
+
+ __gsignals__ = {
+ 'volume-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+ self._volume_buttons = []
+ self._volume_added_hid = None
+ self._volume_removed_hid = None
+
+ self.connect('destroy', self.__destroy_cb)
+
+ gobject.idle_add(self._set_up_volumes)
+
+ def __destroy_cb(self, widget):
+ volumes_manager = volumesmanager.get_volumes_manager()
+ volumes_manager.disconnect(self._volume_added_hid)
+ volumes_manager.disconnect(self._volume_removed_hid)
+
+ def _set_up_volumes(self):
+ volumes_manager = volumesmanager.get_volumes_manager()
+ self._volume_added_hid = \
+ volumes_manager.connect('volume-added', self._volume_added_cb)
+ self._volume_removed_hid = \
+ volumes_manager.connect('volume-removed',
+ self._volume_removed_cb)
+
+ for volume in volumes_manager.get_volumes():
+ self._add_button(volume)
+
+ def _volume_added_cb(self, volumes_manager, volume):
+ self._add_button(volume)
+
+ def _volume_removed_cb(self, volumes_manager, volume):
+ self._remove_button(volume)
+
+ def _add_button(self, volume):
+ logging.debug('VolumeToolbar._add_button: %r' % volume.name)
+
+ if self._volume_buttons:
+ group = self._volume_buttons[0]
+ else:
+ group = None
+
+ palette = Palette(volume.name)
+
+ button = VolumeButton(volume, group)
+ button.set_palette(palette)
+ button.connect('toggled', self._button_toggled_cb, volume)
+ if self._volume_buttons:
+ position = self.get_item_index(self._volume_buttons[-1]) + 1
+ else:
+ position = 0
+ self.insert(button, position)
+ button.show()
+
+ self._volume_buttons.append(button)
+
+ if volume.can_unmount:
+ menu_item = gtk.MenuItem(_('Unmount'))
+ menu_item.connect('activate', self._unmount_activated_cb, volume)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ if len(self.get_children()) > 1:
+ self.show()
+
+ def _button_toggled_cb(self, button, volume):
+ if button.props.active:
+ self.emit('volume-changed', volume.id)
+
+ def _unmount_activated_cb(self, menu_item, volume):
+ logging.debug('VolumesToolbar._unmount_activated_cb: %r', volume.udi)
+ volume.unmount()
+
+ def _remove_button(self, volume):
+ for button in self.get_children():
+ if button.volume.id == volume.id:
+ self._volume_buttons.remove(button)
+ self.remove(button)
+ self.get_children()[0].props.active = True
+
+ if len(self.get_children()) < 2:
+ self.hide()
+ return
+
+class VolumeButton(RadioToolButton):
+ def __init__(self, volume, group):
+ RadioToolButton.__init__(self)
+ self.props.named_icon = volume.icon_name
+ self.props.xo_color = volume.icon_color
+ self.props.group = group
+
+ self.volume = volume
+ self.drag_dest_set(gtk.DEST_DEFAULT_ALL,
+ [('journal-object-id', 0, 0)],
+ gtk.gdk.ACTION_COPY)
+ self.connect('drag-data-received', self._drag_data_received_cb)
+
+ def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data,
+ info, timestamp):
+ jobject = datastore.get(selection_data.data)
+ datastore.copy(jobject, self.volume.id)
+
diff --git a/src/jarabe/logsmanager.py b/src/jarabe/logsmanager.py
new file mode 100644
index 0000000..9360211
--- /dev/null
+++ b/src/jarabe/logsmanager.py
@@ -0,0 +1,55 @@
+# 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 os
+import time
+
+from sugar import env
+
+_MAX_BACKUP_DIRS = 3
+
+def setup():
+ """Clean up the log directory, moving old logs into a numbered backup
+ directory. We only keep `_MAX_BACKUP_DIRS` of these backup directories
+ around; the rest are removed."""
+ logs_dir = env.get_logs_path()
+ if not os.path.isdir(logs_dir):
+ os.makedirs(logs_dir)
+
+ backup_logs = []
+ backup_dirs = []
+ for f in os.listdir(logs_dir):
+ path = os.path.join(logs_dir, f)
+ if os.path.isfile(path):
+ backup_logs.append(f)
+ elif os.path.isdir(path):
+ backup_dirs.append(path)
+
+ if len(backup_dirs) > _MAX_BACKUP_DIRS:
+ backup_dirs.sort()
+ root = backup_dirs[0]
+ for f in os.listdir(root):
+ os.remove(os.path.join(root, f))
+ os.rmdir(root)
+
+ if len(backup_logs) > 0:
+ name = str(int(time.time()))
+ backup_dir = os.path.join(logs_dir, name)
+ os.mkdir(backup_dir)
+ for log in backup_logs:
+ source_path = os.path.join(logs_dir, log)
+ dest_path = os.path.join(backup_dir, log)
+ os.rename(source_path, dest_path)
diff --git a/src/jarabe/main.py b/src/jarabe/main.py
new file mode 100644
index 0000000..b8fab9b
--- /dev/null
+++ b/src/jarabe/main.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2006, 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 os
+import gettext
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import dbus.glib
+
+gtk.gdk.threads_init()
+dbus.glib.threads_init()
+
+from sugar import logger
+from sugar.profile import get_profile
+
+import view.Shell
+from shellservice import ShellService
+from jarabe.hardware import hardwaremanager
+from jarabe.intro.window import IntroWindow
+from jarabe.intro.window import create_profile
+from session import get_session_manager
+import logsmanager
+import config
+
+def _start_matchbox():
+ cmd = ['matchbox-window-manager']
+
+ cmd.extend(['-use_titlebar', 'no'])
+ cmd.extend(['-theme', 'sugar'])
+ cmd.extend(['-kbdconfig', os.path.join(config.data_path, 'kbdconfig')])
+
+ gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH)
+
+def _setup_translations():
+ locale_path = os.path.join(config.prefix, 'share', 'locale')
+ domain = 'sugar'
+
+ gettext.bindtextdomain(domain, locale_path)
+ gettext.textdomain(domain)
+
+def check_cm(bus_name):
+ try:
+ bus = dbus.SessionBus()
+ bus_object = bus.get_object('org.freedesktop.DBus',
+ '/org/freedesktop/DBus')
+ name_ = bus_object.GetNameOwner(bus_name,
+ dbus_interface='org.freedesktop.DBus')
+ except dbus.DBusException:
+ return False
+ return True
+
+def _shell_started_cb():
+ # Unfreeze the display
+ hw_manager = hardwaremanager.get_manager()
+ hw_manager.set_dcon_freeze(0)
+
+def _software_update_cb():
+ '''Ask the homeview to display an alert about available software updates
+ '''
+ shell = view.Shell.get_instance()
+ home_box = shell.home_window.get_home_box()
+ home_box.show_software_updates_alert()
+
+def main():
+ gobject.idle_add(_shell_started_cb)
+
+ try:
+ logsmanager.setup()
+ except Exception, e:
+ # logs setup is not critical; it should not prevent sugar from
+ # starting if (for example) the disk is full or read-only.
+ print 'Log setup failed: %s' % e
+
+ logger.start('shell')
+
+ _start_matchbox()
+ _setup_translations()
+
+ hw_manager = hardwaremanager.get_manager()
+ hw_manager.startup()
+
+ icons_path = os.path.join(config.data_path, 'icons')
+ gtk.icon_theme_get_default().append_search_path(icons_path)
+
+ # Do initial setup if needed
+ if not get_profile().is_valid():
+ if 'SUGAR_PROFILE_NAME' in os.environ:
+ create_profile(os.environ['SUGAR_PROFILE_NAME'])
+ else:
+ win = IntroWindow()
+ win.show_all()
+ gtk.main()
+
+ # set timezone
+ if get_profile().timezone is not None:
+ os.environ['TZ'] = get_profile().timezone
+
+ if os.environ.has_key("SUGAR_TP_DEBUG"):
+ # Allow the user time to start up telepathy connection managers
+ # using the Sugar DBus bus address
+ import time
+ from telepathy.client import ManagerRegistry
+
+ registry = ManagerRegistry()
+ registry.LoadManagers()
+
+ debug_flags = os.environ["SUGAR_TP_DEBUG"].split(',')
+ for cm_name in debug_flags:
+ if cm_name not in ["gabble", "salut"]:
+ continue
+
+ try:
+ cm = registry.services[cm_name]
+ except KeyError:
+ print RuntimeError("%s connection manager not found!" % cm_name)
+
+ while not check_cm(cm['busname']):
+ print "Waiting for %s on: DBUS_SESSION_BUS_ADDRESS=%s" % \
+ (cm_name, os.environ["DBUS_SESSION_BUS_ADDRESS"])
+ try:
+ time.sleep(5)
+ except KeyboardInterrupt:
+ print "Got Ctrl+C, continuing..."
+ break
+
+ # TODO: move initializations from the Shell constructor to a start() method
+ view.Shell.get_instance()
+ ShellService()
+
+ session_manager = get_session_manager()
+ session_manager.start()
+
+ # dlo trac #7495: open 'software update' control panel after an upgrade
+ # to update activities.
+ update_trigger_file = os.path.expanduser('~/.sugar-update')
+ if os.path.isfile(update_trigger_file):
+ gobject.idle_add(_software_update_cb)
+
+ try:
+ gtk.main()
+ except KeyboardInterrupt:
+ print 'Ctrl+C pressed, exiting...'
diff --git a/src/jarabe/model/BuddyModel.py b/src/jarabe/model/BuddyModel.py
new file mode 100644
index 0000000..b51b808
--- /dev/null
+++ b/src/jarabe/model/BuddyModel.py
@@ -0,0 +1,171 @@
+# 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 sugar.presence import presenceservice
+from sugar.graphics.xocolor import XoColor
+import gobject
+
+_NOT_PRESENT_COLOR = "#d5d5d5,#FFFFFF"
+
+class BuddyModel(gobject.GObject):
+ __gsignals__ = {
+ 'appeared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'disappeared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'nick-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'color-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'icon-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'current-activity-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self, key=None, buddy=None, nick=None):
+ if (key and buddy) or (not key and not buddy):
+ raise RuntimeError("Must specify only _one_ of key or buddy.")
+
+ gobject.GObject.__init__(self)
+
+ self._color = None
+ self._ba_handler = None
+ self._pc_handler = None
+ self._dis_handler = None
+ self._bic_handler = None
+ self._cac_handler = None
+
+ self._pservice = presenceservice.get_instance()
+
+ self._buddy = None
+
+ if not buddy:
+ self._key = key
+ # connect to the PS's buddy-appeared signal and
+ # wait for the buddy to appear
+ self._ba_handler = self._pservice.connect('buddy-appeared',
+ self._buddy_appeared_cb)
+ # Set color to 'inactive'/'disconnected'
+ self._set_color_from_string(_NOT_PRESENT_COLOR)
+ self._nick = nick
+
+ self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb)
+ else:
+ self._update_buddy(buddy)
+
+ def _get_buddies_cb(self, buddy_list):
+ buddy = None
+ for iter_buddy in buddy_list:
+ if iter_buddy.props.key == self._key:
+ buddy = iter_buddy
+ break
+
+ if buddy:
+ if self._ba_handler:
+ # Once we have the buddy, we no longer need to
+ # monitor buddy-appeared events
+ self._pservice.disconnect(self._ba_handler)
+ self._ba_handler = None
+
+ self._update_buddy(buddy)
+
+ def _set_color_from_string(self, color_string):
+ self._color = XoColor(color_string)
+
+ def get_key(self):
+ return self._key
+
+ def get_nick(self):
+ return self._nick
+
+ def get_color(self):
+ return self._color
+
+ def get_buddy(self):
+ return self._buddy
+
+ def is_owner(self):
+ if not self._buddy:
+ return False
+ return self._buddy.props.owner
+
+ def is_present(self):
+ if self._buddy:
+ return True
+ return False
+
+ def get_current_activity(self):
+ if self._buddy:
+ return self._buddy.props.current_activity
+ return None
+
+ def _update_buddy(self, buddy):
+ if not buddy:
+ raise ValueError("Buddy cannot be None.")
+
+ self._buddy = buddy
+ self._key = self._buddy.props.key
+ self._nick = self._buddy.props.nick
+ self._set_color_from_string(self._buddy.props.color)
+
+ self._pc_handler = self._buddy.connect('property-changed',
+ self._buddy_property_changed_cb)
+ self._bic_handler = self._buddy.connect('icon-changed',
+ self._buddy_icon_changed_cb)
+
+ def _buddy_appeared_cb(self, pservice, buddy):
+ if self._buddy or buddy.props.key != self._key:
+ return
+
+ if self._ba_handler:
+ # Once we have the buddy, we no longer need to
+ # monitor buddy-appeared events
+ self._pservice.disconnect(self._ba_handler)
+ self._ba_handler = None
+
+ self._update_buddy(buddy)
+ self.emit('appeared')
+
+ def _buddy_property_changed_cb(self, buddy, keys):
+ if not self._buddy:
+ return
+ if 'color' in keys:
+ self._set_color_from_string(self._buddy.props.color)
+ self.emit('color-changed', self.get_color())
+ if 'current-activity' in keys:
+ self.emit('current-activity-changed', buddy.props.current_activity)
+ if 'nick' in keys:
+ self._nick = self._buddy.props.nick
+ self.emit('nick-changed', self.get_nick())
+
+ def _buddy_disappeared_cb(self, buddy):
+ if buddy != self._buddy:
+ return
+ self._buddy.disconnect(self._pc_handler)
+ self._buddy.disconnect(self._dis_handler)
+ self._buddy.disconnect(self._bic_handler)
+ self._buddy.disconnect(self._cac_handler)
+ self._set_color_from_string(_NOT_PRESENT_COLOR)
+ self.emit('disappeared')
+ self._buddy = None
+
+ def _buddy_icon_changed_cb(self, buddy):
+ self.emit('icon-changed')
diff --git a/src/jarabe/model/Friends.py b/src/jarabe/model/Friends.py
new file mode 100644
index 0000000..fc6ff65
--- /dev/null
+++ b/src/jarabe/model/Friends.py
@@ -0,0 +1,114 @@
+# 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 dbus
+import os
+from ConfigParser import ConfigParser
+
+import gobject
+
+from jarabe.model.BuddyModel import BuddyModel
+from sugar import env
+import logging
+
+class Friends(gobject.GObject):
+ __gsignals__ = {
+ 'friend-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object])),
+ 'friend-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._friends = {}
+ self._path = os.path.join(env.get_profile_path(), 'friends')
+
+ self.load()
+
+ def has_buddy(self, buddy):
+ return self._friends.has_key(buddy.get_key())
+
+ def add_friend(self, buddy_info):
+ self._friends[buddy_info.get_key()] = buddy_info
+ self.emit('friend-added', buddy_info)
+
+ def make_friend(self, buddy):
+ if not self.has_buddy(buddy):
+ self.add_friend(buddy)
+ self.save()
+
+ def remove(self, buddy_info):
+ del self._friends[buddy_info.get_key()]
+ self.save()
+ self.emit('friend-removed', buddy_info.get_key())
+
+ def __iter__(self):
+ return self._friends.values().__iter__()
+
+ def load(self):
+ cp = ConfigParser()
+
+ try:
+ success = cp.read([self._path])
+ if success:
+ for key in cp.sections():
+ # HACK: don't screw up on old friends files
+ if len(key) < 20:
+ continue
+ buddy = BuddyModel(key=key, nick=cp.get(key, 'nick'))
+ self.add_friend(buddy)
+ except Exception, exc:
+ logging.error("Error parsing friends file: %s" % exc)
+
+ def save(self):
+ cp = ConfigParser()
+
+ for friend in self:
+ section = friend.get_key()
+ cp.add_section(section)
+ cp.set(section, 'nick', friend.get_nick())
+ cp.set(section, 'color', friend.get_color().to_string())
+
+ fileobject = open(self._path, 'w')
+ cp.write(fileobject)
+ fileobject.close()
+
+ self._sync_friends()
+
+ def _sync_friends(self):
+ # XXX: temporary hack
+ # remove this when the shell service has a D-Bus API for buddies
+
+ def friends_synced():
+ pass
+
+ def friends_synced_error(e):
+ logging.error("Error asking presence service to sync friends: %s"
+ % e)
+
+ keys = []
+ for friend in self:
+ keys.append(friend.get_key())
+
+ bus = dbus.SessionBus()
+ ps = bus.get_object('org.laptop.Sugar.Presence',
+ '/org/laptop/Sugar/Presence')
+ psi = dbus.Interface(ps, 'org.laptop.Sugar.Presence')
+ psi.SyncFriends(keys,
+ reply_handler=friends_synced,
+ error_handler=friends_synced_error)
diff --git a/src/jarabe/model/Invites.py b/src/jarabe/model/Invites.py
new file mode 100644
index 0000000..8bcffd2
--- /dev/null
+++ b/src/jarabe/model/Invites.py
@@ -0,0 +1,123 @@
+# 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 gobject
+from sugar.presence import presenceservice
+
+
+class BaseInvite:
+ """Invitation to shared activity or private 1-1 Telepathy channel"""
+ def __init__(self, bundle_id):
+ """init for BaseInvite.
+
+ bundle_id: string, e.g. 'org.laptop.Chat'
+ """
+ self._bundle_id = bundle_id
+
+ def get_bundle_id(self):
+ return self._bundle_id
+
+
+class ActivityInvite(BaseInvite):
+ """Invitation to a shared activity."""
+ def __init__(self, bundle_id, activity_id):
+ BaseInvite.__init__(self, bundle_id)
+ self._activity_id = activity_id
+
+ def get_activity_id(self):
+ return self._activity_id
+
+
+class PrivateInvite(BaseInvite):
+ """Invitation to a private 1-1 Telepathy channel.
+
+ This includes text chat or streaming media.
+ """
+ def __init__(self, bundle_id, private_channel):
+ """init for PrivateInvite.
+
+ bundle_id: string, e.g. 'org.laptop.Chat'
+ private_channel: string containing simplejson dump of Telepathy
+ bus, connection and channel
+ """
+ BaseInvite.__init__(self, bundle_id)
+ self._private_channel = private_channel
+
+ def get_private_channel(self):
+ """Telepathy channel info from private invitation"""
+ return self._private_channel
+
+
+class Invites(gobject.GObject):
+ __gsignals__ = {
+ 'invite-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object])),
+ 'invite-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._dict = {}
+
+ ps = presenceservice.get_instance()
+ owner = ps.get_owner()
+ owner.connect('joined-activity', self._owner_joined_cb)
+
+ def add_invite(self, bundle_id, activity_id):
+ if activity_id in self._dict:
+ # there is no point to add more than one time
+ # an invite for the same activity
+ return
+
+ invite = ActivityInvite(bundle_id, activity_id)
+ self._dict[activity_id] = invite
+ self.emit('invite-added', invite)
+
+ def add_private_invite(self, private_channel, bundle_id):
+ if private_channel in self._dict:
+ # there is no point to add more than one invite for the
+ # same incoming connection
+ return
+
+ invite = PrivateInvite(bundle_id, private_channel)
+ self._dict[private_channel] = invite
+ self.emit('invite-added', invite)
+
+ def remove_invite(self, invite):
+ del self._dict[invite.get_activity_id()]
+ self.emit('invite-removed', invite)
+
+ def remove_private_invite(self, invite):
+ del self._dict[invite.get_private_channel()]
+ self.emit('invite-removed', invite)
+
+ def remove_activity(self, activity_id):
+ invite = self._dict.get(activity_id)
+ if invite is not None:
+ self.remove_invite(invite)
+
+ def remove_private_channel(self, private_channel):
+ invite = self._dict.get(private_channel)
+ if invite is not None:
+ self.remove_private_invite(invite)
+
+ def _owner_joined_cb(self, owner, activity):
+ self.remove_activity(activity.props.id)
+
+ def __iter__(self):
+ return self._dict.values().__iter__()
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am
new file mode 100644
index 0000000..2c4150b
--- /dev/null
+++ b/src/jarabe/model/Makefile.am
@@ -0,0 +1,16 @@
+SUBDIRS = devices
+
+sugardir = $(pythondir)/jarabe/model
+sugar_PYTHON = \
+ __init__.py \
+ accesspointmodel.py \
+ BuddyModel.py \
+ clipboard.py \
+ clipboardobject.py \
+ Friends.py \
+ Invites.py \
+ Owner.py \
+ MeshModel.py \
+ shellmodel.py \
+ homeactivity.py \
+ homemodel.py
diff --git a/src/jarabe/model/MeshModel.py b/src/jarabe/model/MeshModel.py
new file mode 100644
index 0000000..8e16c21
--- /dev/null
+++ b/src/jarabe/model/MeshModel.py
@@ -0,0 +1,237 @@
+# 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 gobject
+
+from sugar.graphics.xocolor import XoColor
+from sugar.presence import presenceservice
+from sugar import activity
+
+from jarabe.model.BuddyModel import BuddyModel
+from jarabe.model.accesspointmodel import AccessPointModel
+from jarabe.hardware import hardwaremanager
+from jarabe.hardware import nmclient
+
+class ActivityModel:
+ def __init__(self, act, bundle):
+ self.activity = act
+ self.bundle = bundle
+
+ def get_id(self):
+ return self.activity.props.id
+
+ def get_icon_name(self):
+ return self.bundle.icon
+
+ def get_color(self):
+ return XoColor(self.activity.props.color)
+
+ def get_bundle_id(self):
+ return self.bundle.bundle_id
+
+class MeshModel(gobject.GObject):
+ __gsignals__ = {
+ 'activity-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'activity-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'buddy-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'buddy-moved': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT,
+ gobject.TYPE_PYOBJECT])),
+ 'buddy-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'access-point-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'access-point-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'mesh-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])),
+ 'mesh-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._activities = {}
+ self._buddies = {}
+ self._access_points = {}
+ self._mesh = None
+
+ self._pservice = presenceservice.get_instance()
+ self._pservice.connect("activity-appeared",
+ self._activity_appeared_cb)
+ self._pservice.connect('activity-disappeared',
+ self._activity_disappeared_cb)
+ self._pservice.connect("buddy-appeared",
+ self._buddy_appeared_cb)
+ self._pservice.connect("buddy-disappeared",
+ self._buddy_disappeared_cb)
+
+ # Add any buddies the PS knows about already
+ self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb)
+
+ self._pservice.get_activities_async(
+ reply_handler=self._get_activities_cb)
+
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager:
+ for nm_device in network_manager.get_devices():
+ self._add_network_device(nm_device)
+ network_manager.connect('device-added',
+ self._nm_device_added_cb)
+ network_manager.connect('device-removed',
+ self._nm_device_removed_cb)
+
+ def _get_buddies_cb(self, buddy_list):
+ for buddy in buddy_list:
+ self._buddy_appeared_cb(self._pservice, buddy)
+
+ def _get_activities_cb(self, activity_list):
+ for act in activity_list:
+ self._check_activity(act)
+
+ def _nm_device_added_cb(self, manager, nm_device):
+ self._add_network_device(nm_device)
+
+ def _nm_device_removed_cb(self, manager, nm_device):
+ self._remove_network_device(nm_device)
+
+ def _nm_network_appeared_cb(self, nm_device, nm_network):
+ self._add_access_point(nm_device, nm_network)
+
+ def _nm_network_disappeared_cb(self, nm_device, nm_network):
+ if self._access_points.has_key(nm_network.get_op()):
+ ap = self._access_points[nm_network.get_op()]
+ self._remove_access_point(ap)
+
+ def _add_network_device(self, nm_device):
+ dtype = nm_device.get_type()
+ if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS:
+ for nm_network in nm_device.get_networks():
+ self._add_access_point(nm_device, nm_network)
+
+ nm_device.connect('network-appeared',
+ self._nm_network_appeared_cb)
+ nm_device.connect('network-disappeared',
+ self._nm_network_disappeared_cb)
+ elif dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC:
+ self._mesh = nm_device
+ self.emit('mesh-added', self._mesh)
+
+ def _remove_network_device(self, nm_device):
+ if nm_device == self._mesh:
+ self._mesh = None
+ self.emit('mesh-removed')
+ elif nm_device.get_type() == nmclient.DEVICE_TYPE_802_11_WIRELESS:
+ aplist = self._access_points.values()
+ for ap in aplist:
+ if ap.get_nm_device() == nm_device:
+ self._remove_access_point(ap)
+
+ def _add_access_point(self, nm_device, nm_network):
+ model = AccessPointModel(nm_device, nm_network)
+ self._access_points[model.get_id()] = model
+ self.emit('access-point-added', model)
+
+ def _remove_access_point(self, ap):
+ if not self._access_points.has_key(ap.get_id()):
+ return
+ self.emit('access-point-removed', ap)
+ del self._access_points[ap.get_id()]
+
+ def get_mesh(self):
+ return self._mesh
+
+ def get_access_points(self):
+ return self._access_points.values()
+
+ def get_activities(self):
+ return self._activities.values()
+
+ def get_buddies(self):
+ return self._buddies.values()
+
+ def _buddy_activity_changed_cb(self, model, cur_activity):
+ if not self._buddies.has_key(model.get_buddy().object_path()):
+ return
+ if cur_activity and self._activities.has_key(cur_activity.props.id):
+ activity_model = self._activities[cur_activity.props.id]
+ self.emit('buddy-moved', model, activity_model)
+ else:
+ self.emit('buddy-moved', model, None)
+
+ def _buddy_appeared_cb(self, pservice, buddy):
+ if self._buddies.has_key(buddy.object_path()):
+ return
+
+ model = BuddyModel(buddy=buddy)
+ model.connect('current-activity-changed',
+ self._buddy_activity_changed_cb)
+ self._buddies[buddy.object_path()] = model
+ self.emit('buddy-added', model)
+
+ cur_activity = buddy.props.current_activity
+ if cur_activity:
+ self._buddy_activity_changed_cb(model, cur_activity)
+
+ def _buddy_disappeared_cb(self, pservice, buddy):
+ if not self._buddies.has_key(buddy.object_path()):
+ return
+ self.emit('buddy-removed', self._buddies[buddy.object_path()])
+ del self._buddies[buddy.object_path()]
+
+ def _activity_appeared_cb(self, pservice, act):
+ self._check_activity(act)
+
+ def _check_activity(self, presence_activity):
+ registry = activity.get_registry()
+ bundle = registry.get_activity(presence_activity.props.type)
+ if not bundle:
+ return
+ if self.has_activity(presence_activity.props.id):
+ return
+ self.add_activity(bundle, presence_activity)
+
+ def has_activity(self, activity_id):
+ return self._activities.has_key(activity_id)
+
+ def get_activity(self, activity_id):
+ if self.has_activity(activity_id):
+ return self._activities[activity_id]
+ else:
+ return None
+
+ def add_activity(self, bundle, act):
+ model = ActivityModel(act, bundle)
+ self._activities[model.get_id()] = model
+ self.emit('activity-added', model)
+
+ for buddy in self._pservice.get_buddies():
+ cur_activity = buddy.props.current_activity
+ object_path = buddy.object_path()
+ if cur_activity == activity and object_path in self._buddies:
+ buddy_model = self._buddies[object_path]
+ self.emit('buddy-moved', buddy_model, model)
+
+ def _activity_disappeared_cb(self, pservice, act):
+ if self._activities.has_key(act.props.id):
+ activity_model = self._activities[act.props.id]
+ self.emit('activity-removed', activity_model)
+ del self._activities[act.props.id]
diff --git a/src/jarabe/model/Owner.py b/src/jarabe/model/Owner.py
new file mode 100644
index 0000000..91be127
--- /dev/null
+++ b/src/jarabe/model/Owner.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2008 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 gobject
+import os
+import simplejson
+
+from telepathy.interfaces import CHANNEL_TYPE_TEXT
+
+from sugar import env
+from sugar import profile
+from sugar.presence import presenceservice
+from sugar import util
+from jarabe.model.Invites import Invites
+
+class ShellOwner(gobject.GObject):
+ """Class representing the owner of this machine/instance. This class
+ runs in the shell and serves up the buddy icon and other stuff. It's the
+ server portion of the Owner, paired with the client portion in Buddy.py.
+ """
+ __gtype_name__ = "ShellOwner"
+
+ __gsignals__ = {
+ 'nick-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_STRING])),
+ 'color-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'icon-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._nick = profile.get_nick_name()
+
+ self._icon = None
+ self._icon_hash = ""
+ icon = os.path.join(env.get_profile_path(), "buddy-icon.jpg")
+ if not os.path.exists(icon):
+ raise RuntimeError("missing buddy icon")
+
+ fd = open(icon, "r")
+ self._icon = fd.read()
+ fd.close()
+ if not self._icon:
+ raise RuntimeError("invalid buddy icon")
+
+ # Get the icon's hash
+ import md5
+ digest = md5.new(self._icon).digest()
+ self._icon_hash = util.printable_hash(digest)
+
+ self._pservice = presenceservice.get_instance()
+ self._pservice.connect('activity-invitation',
+ self._activity_invitation_cb)
+ self._pservice.connect('private-invitation',
+ self._private_invitation_cb)
+ self._pservice.connect('activity-disappeared',
+ self._activity_disappeared_cb)
+
+ self._invites = Invites()
+
+ def get_invites(self):
+ return self._invites
+
+ def get_nick(self):
+ return self._nick
+
+ def _activity_invitation_cb(self, pservice, activity, buddy, message):
+ self._invites.add_invite(activity.props.type,
+ activity.props.id)
+
+ def _private_invitation_cb(self, pservice, bus_name, connection,
+ channel, channel_type):
+ """Handle a private-invitation from Presence Service.
+
+ This is a connection by a non-Sugar XMPP client, so
+ launch Chat or VideoChat with the Telepathy connection and
+ channel.
+ """
+ if channel_type == CHANNEL_TYPE_TEXT:
+ bundle_id = 'org.laptop.Chat'
+ else:
+ bundle_id = 'org.laptop.VideoChat'
+ tp_channel = simplejson.dumps([bus_name, connection, channel])
+ self._invites.add_private_invite(tp_channel, bundle_id)
+
+ def _activity_disappeared_cb(self, pservice, activity):
+ self._invites.remove_activity(activity.props.id)
diff --git a/src/jarabe/model/__init__.py b/src/jarabe/model/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/model/__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/jarabe/model/accesspointmodel.py b/src/jarabe/model/accesspointmodel.py
new file mode 100644
index 0000000..a4760bc
--- /dev/null
+++ b/src/jarabe/model/accesspointmodel.py
@@ -0,0 +1,80 @@
+# 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 gobject
+
+from jarabe.hardware import nmclient
+
+STATE_CONNECTING = 0
+STATE_CONNECTED = 1
+STATE_NOTCONNECTED = 2
+
+_nm_state_to_state = {
+ nmclient.NETWORK_STATE_CONNECTED : STATE_CONNECTED,
+ nmclient.NETWORK_STATE_CONNECTING : STATE_CONNECTING,
+ nmclient.NETWORK_STATE_NOTCONNECTED : STATE_NOTCONNECTED
+}
+
+class AccessPointModel(gobject.GObject):
+ __gproperties__ = {
+ 'name' : (str, None, None, None,
+ gobject.PARAM_READABLE),
+ 'strength' : (int, None, None, 0, 100, 0,
+ gobject.PARAM_READABLE),
+ 'state' : (int, None, None, STATE_CONNECTING,
+ STATE_NOTCONNECTED, 0, gobject.PARAM_READABLE),
+ 'capabilities' : (int, None, None, 0, 0x7FFFFFFF, 0,
+ gobject.PARAM_READABLE),
+ 'mode' : (int, None, None, 0, 6, 0, gobject.PARAM_READABLE)
+ }
+
+ def __init__(self, nm_device, nm_network):
+ gobject.GObject.__init__(self)
+ self._nm_network = nm_network
+ self._nm_device = nm_device
+
+ self._nm_network.connect('strength-changed',
+ self._strength_changed_cb)
+ self._nm_network.connect('state-changed',
+ self._state_changed_cb)
+
+ def _strength_changed_cb(self, nm_network):
+ self.notify('strength')
+
+ def _state_changed_cb(self, nm_network):
+ self.notify('state')
+
+ def get_id(self):
+ return self._nm_network.get_op()
+
+ def get_nm_device(self):
+ return self._nm_device
+
+ def get_nm_network(self):
+ return self._nm_network
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'strength':
+ return self._nm_network.get_strength()
+ elif pspec.name == 'name':
+ return self._nm_network.get_ssid()
+ elif pspec.name == 'state':
+ nm_state = self._nm_network.get_state()
+ return _nm_state_to_state[nm_state]
+ elif pspec.name == 'capabilities':
+ return self._nm_network.get_caps()
+ elif pspec.name == 'mode':
+ return self._nm_network.get_mode()
diff --git a/src/jarabe/model/clipboard.py b/src/jarabe/model/clipboard.py
new file mode 100644
index 0000000..b79867e
--- /dev/null
+++ b/src/jarabe/model/clipboard.py
@@ -0,0 +1,149 @@
+# Copyright (C) 2006, 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 os
+import shutil
+import urlparse
+import tempfile
+
+import gobject
+
+from sugar import mime
+
+from jarabe.model.clipboardobject import ClipboardObject, Format
+
+class Clipboard(gobject.GObject):
+
+ __gsignals__ = {
+ 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([int])),
+ 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._objects = {}
+ self._next_id = 0
+
+ def _get_next_object_id(self):
+ self._next_id += 1
+ return self._next_id
+
+ def add_object(self, name):
+ logging.debug('Clipboard.add_object')
+ object_id = self._get_next_object_id()
+ self._objects[object_id] = ClipboardObject(object_id, name)
+ self.emit('object-added', self._objects[object_id])
+ return object_id
+
+ def add_object_format(self, object_id, format_type, data, on_disk):
+ logging.debug('Clipboard.add_object_format')
+ cb_object = self._objects[object_id]
+
+ if format_type == 'XdndDirectSave0':
+ format = Format('text/uri-list', data + '\r\n', on_disk)
+ format.owns_disk_data = True
+ cb_object.add_format(format)
+ elif on_disk and cb_object.get_percent() == 100:
+ new_uri = self._copy_file(data)
+ cb_object.add_format(Format(format_type, new_uri, on_disk))
+ logging.debug('Added format of type ' + format_type
+ + ' with path at ' + new_uri)
+ else:
+ cb_object.add_format(Format(format_type, data, on_disk))
+ logging.debug('Added in-memory format of type ' + format_type + '.')
+
+ self.emit('object-state-changed', cb_object)
+
+ def delete_object(self, object_id):
+ cb_object = self._objects.pop(object_id)
+ cb_object.destroy()
+ self.emit('object-deleted', object_id)
+ logging.debug('Deleted object with object_id %r' % object_id)
+
+ def set_object_percent(self, object_id, percent):
+ cb_object = self._objects[object_id]
+ if percent < 0 or percent > 100:
+ raise ValueError("invalid percentage")
+ if cb_object.get_percent() > percent:
+ raise ValueError("invalid percentage; less than current percent")
+ if cb_object.get_percent() == percent:
+ # ignore setting same percentage
+ return
+
+ cb_object.set_percent(percent)
+
+ if percent == 100:
+ self._process_object(cb_object)
+
+ self.emit('object-state-changed', cb_object)
+
+ def _process_object(self, cb_object):
+ formats = cb_object.get_formats()
+ for format_name, format in formats.iteritems():
+ if format.is_on_disk() and not format.owns_disk_data:
+ new_uri = self._copy_file(format.get_data())
+ format.set_data(new_uri)
+
+ # Add a text/plain format to objects that are text but lack it
+ if 'text/plain' not in formats.keys():
+ if 'UTF8_STRING' in formats.keys():
+ self.add_object_format(
+ cb_object.get_id(), 'text/plain',
+ data=formats['UTF8_STRING'].get_data(), on_disk=False)
+ elif 'text/unicode' in formats.keys():
+ self.add_object_format(
+ cb_object.get_id(), 'text/plain',
+ data=formats['UTF8_STRING'].get_data(), on_disk=False)
+
+ def get_object(self, object_id):
+ logging.debug('Clipboard.get_object')
+ return self._objects[object_id]
+
+ def get_object_data(self, object_id, format_type):
+ logging.debug('Clipboard.get_object_data')
+ cb_object = self._objects[object_id]
+ format = cb_object.get_formats()[format_type]
+ return format
+
+ def _copy_file(self, original_uri):
+ uri = urlparse.urlparse(original_uri)
+ path_, file_name = os.path.split(uri.path)
+
+ root, ext = os.path.splitext(file_name)
+ if not ext or ext == '.':
+ mime_type = mime.get_for_file(uri.path)
+ ext = '.' + mime.get_primary_extension(mime_type)
+
+ f_, new_file_path = tempfile.mkstemp(ext, root)
+ del f_
+ shutil.copyfile(uri.path, new_file_path)
+ os.chmod(new_file_path, 0644)
+
+ return 'file://' + new_file_path
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = Clipboard()
+ return _instance
diff --git a/src/jarabe/model/clipboardobject.py b/src/jarabe/model/clipboardobject.py
new file mode 100644
index 0000000..a4cd388
--- /dev/null
+++ b/src/jarabe/model/clipboardobject.py
@@ -0,0 +1,117 @@
+# 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 urlparse
+
+from sugar import mime
+from sugar.bundle.activitybundle import ActivityBundle
+
+class ClipboardObject(object):
+
+ def __init__(self, object_path, name):
+ self._id = object_path
+ self._name = name
+ self._percent = 0
+ self._formats = {}
+
+ def destroy(self):
+ for format in self._formats.itervalues():
+ format.destroy()
+
+ def get_id(self):
+ return self._id
+
+ def get_name(self):
+ name = self._name
+ if not name:
+ name = mime.get_mime_description(self.get_mime_type())
+ if not name:
+ name = ''
+ return name
+
+ def get_icon(self):
+ return mime.get_mime_icon(self.get_mime_type())
+
+ def get_preview(self):
+ # TODO: should previews really be here?
+ #return self._get_type_info().get_preview()
+ return ''
+
+ def is_bundle(self):
+ # A bundle will have only one format.
+ if not self._formats:
+ return False
+ else:
+ return self._formats.keys()[0] in [ActivityBundle.MIME_TYPE,
+ ActivityBundle.DEPRECATED_MIME_TYPE]
+
+ def get_percent(self):
+ return self._percent
+
+ def set_percent(self, percent):
+ self._percent = percent
+
+ def add_format(self, format):
+ self._formats[format.get_type()] = format
+
+ def get_formats(self):
+ return self._formats
+
+ def get_mime_type(self):
+ if not self._formats:
+ return ''
+
+ format = mime.choose_most_significant(self._formats.keys())
+ if format == 'text/uri-list':
+ data = self._formats['text/uri-list'].get_data()
+ uri = urlparse.urlparse(mime.split_uri_list(data)[0], 'file')
+ if uri.scheme == 'file':
+ if os.path.exists(uri.path):
+ format = mime.get_for_file(uri.path)
+ else:
+ format = mime.get_from_file_name(uri.path)
+ logging.debug('Choosed %r!' % format)
+
+ return format
+
+class Format(object):
+
+ def __init__(self, mime_type, data, on_disk):
+ self.owns_disk_data = False
+
+ self._type = mime_type
+ self._data = data
+ self._on_disk = on_disk
+
+ def destroy(self):
+ if self._on_disk:
+ uri = urlparse.urlparse(self._data)
+ if os.path.isfile(uri.path):
+ os.remove(uri.path)
+
+ def get_type(self):
+ return self._type
+
+ def get_data(self):
+ return self._data
+
+ def set_data(self, data):
+ self._data = data
+
+ def is_on_disk(self):
+ return self._on_disk
diff --git a/src/jarabe/model/devices/Makefile.am b/src/jarabe/model/devices/Makefile.am
new file mode 100644
index 0000000..564d609
--- /dev/null
+++ b/src/jarabe/model/devices/Makefile.am
@@ -0,0 +1,10 @@
+SUBDIRS = network
+
+sugardir = $(pythondir)/jarabe/model/devices
+sugar_PYTHON = \
+ __init__.py \
+ battery.py \
+ device.py \
+ devicesmodel.py \
+ speaker.py
+
diff --git a/src/jarabe/model/devices/__init__.py b/src/jarabe/model/devices/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/model/devices/__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/jarabe/model/devices/battery.py b/src/jarabe/model/devices/battery.py
new file mode 100644
index 0000000..d041907
--- /dev/null
+++ b/src/jarabe/model/devices/battery.py
@@ -0,0 +1,97 @@
+# 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 gobject
+import dbus
+
+from jarabe.model.devices import device
+
+_LEVEL_PROP = 'battery.charge_level.percentage'
+_CHARGING_PROP = 'battery.rechargeable.is_charging'
+_DISCHARGING_PROP = 'battery.rechargeable.is_discharging'
+
+class Device(device.Device):
+ __gproperties__ = {
+ 'level' : (int, None, None, 0, 100, 0,
+ gobject.PARAM_READABLE),
+ 'charging' : (bool, None, None, False,
+ gobject.PARAM_READABLE),
+ 'discharging' : (bool, None, None, False,
+ gobject.PARAM_READABLE)
+ }
+
+ def __init__(self, udi):
+ device.Device.__init__(self, udi)
+
+ bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM)
+ proxy = bus.get_object('org.freedesktop.Hal', udi,
+ follow_name_owner_changes=True)
+ self._battery = dbus.Interface(proxy, 'org.freedesktop.Hal.Device')
+ bus.add_signal_receiver(self._battery_changed,
+ 'PropertyModified',
+ 'org.freedesktop.Hal.Device',
+ 'org.freedesktop.Hal',
+ udi)
+
+ self._level = self._get_level()
+ self._charging = self._get_charging()
+ self._discharging = self._get_discharging()
+
+ def _get_level(self):
+ try:
+ return self._battery.GetProperty(_LEVEL_PROP)
+ except dbus.DBusException:
+ logging.error('Cannot access %s' % _LEVEL_PROP)
+ return 0
+
+ def _get_charging(self):
+ try:
+ return self._battery.GetProperty(_CHARGING_PROP)
+ except dbus.DBusException:
+ logging.error('Cannot access %s' % _CHARGING_PROP)
+ return False
+
+ def _get_discharging(self):
+ try:
+ return self._battery.GetProperty(_DISCHARGING_PROP)
+ except dbus.DBusException:
+ logging.error('Cannot access %s' % _DISCHARGING_PROP)
+ return False
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'level':
+ return self._level
+ if pspec.name == 'charging':
+ return self._charging
+ if pspec.name == 'discharging':
+ return self._discharging
+
+ def get_type(self):
+ return 'battery'
+
+ def _battery_changed(self, num_changes, changes_list):
+ for change in changes_list:
+ if change[0] == _LEVEL_PROP:
+ self._level = self._get_level()
+ self.notify('level')
+ elif change[0] == _CHARGING_PROP:
+ self._charging = self._get_charging()
+ self.notify('charging')
+ elif change[0] == _DISCHARGING_PROP:
+ self._discharging = self._get_discharging()
+ self.notify('discharging')
diff --git a/src/jarabe/model/devices/device.py b/src/jarabe/model/devices/device.py
new file mode 100644
index 0000000..8d62415
--- /dev/null
+++ b/src/jarabe/model/devices/device.py
@@ -0,0 +1,45 @@
+#
+# 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 gobject
+from jarabe.hardware import nmclient
+
+from sugar import util
+
+STATE_ACTIVATING = 0
+STATE_ACTIVATED = 1
+STATE_INACTIVE = 2
+
+nm_state_to_state = {
+ nmclient.DEVICE_STATE_ACTIVATING : STATE_ACTIVATING,
+ nmclient.DEVICE_STATE_ACTIVATED : STATE_ACTIVATED,
+ nmclient.DEVICE_STATE_INACTIVE : STATE_INACTIVE
+}
+
+class Device(gobject.GObject):
+ def __init__(self, device_id=None):
+ gobject.GObject.__init__(self)
+ if device_id:
+ self._id = device_id
+ else:
+ self._id = util.unique_id()
+
+ def get_type(self):
+ return 'unknown'
+
+ def get_id(self):
+ return self._id
diff --git a/src/jarabe/model/devices/devicesmodel.py b/src/jarabe/model/devices/devicesmodel.py
new file mode 100644
index 0000000..5d6353d
--- /dev/null
+++ b/src/jarabe/model/devices/devicesmodel.py
@@ -0,0 +1,144 @@
+#
+# 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 logging
+import gobject
+import dbus
+
+from jarabe.model.devices import device
+from jarabe.model.devices.network import wireless
+from jarabe.model.devices.network import mesh
+from jarabe.model.devices import battery
+from jarabe.model.devices import speaker
+from jarabe.hardware import hardwaremanager
+from jarabe.hardware import nmclient
+
+class DevicesModel(gobject.GObject):
+ __gsignals__ = {
+ 'device-appeared' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'device-disappeared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._devices = {}
+ self._sigids = {}
+
+ self._observe_hal_manager()
+ self._observe_network_manager()
+
+ try:
+ self.add_device(speaker.Device())
+ except Exception, speaker_fail_msg:
+ logging.error("could not initialize speaker device: %s" %
+ speaker_fail_msg)
+
+ def _observe_hal_manager(self):
+ bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM)
+ proxy = bus.get_object('org.freedesktop.Hal',
+ '/org/freedesktop/Hal/Manager')
+ hal_manager = dbus.Interface(proxy, 'org.freedesktop.Hal.Manager')
+
+ for udi in hal_manager.FindDeviceByCapability('battery'):
+ self.add_device(battery.Device(udi))
+
+ def _observe_network_manager(self):
+ network_manager = hardwaremanager.get_network_manager()
+ if not network_manager:
+ return
+
+ for dev in network_manager.get_devices():
+ self._check_network_device(dev)
+
+ network_manager.connect('device-added',
+ self._network_device_added_cb)
+ network_manager.connect('device-activating',
+ self._network_device_activating_cb)
+ network_manager.connect('device-activated',
+ self._network_device_activated_cb)
+ network_manager.connect('device-removed',
+ self._network_device_removed_cb)
+
+ def _network_device_added_cb(self, network_manager, nm_device):
+ state = nm_device.get_state()
+ if state == nmclient.DEVICE_STATE_ACTIVATING \
+ or state == nmclient.DEVICE_STATE_ACTIVATED:
+ self._check_network_device(nm_device)
+
+ def _network_device_activating_cb(self, network_manager, nm_device):
+ self._check_network_device(nm_device)
+
+ def _network_device_activated_cb(self, network_manager, nm_device):
+ pass
+
+ def _network_device_removed_cb(self, network_manager, nm_device):
+ if self._devices.has_key(str(nm_device.get_op())):
+ self.remove_device(self._get_network_device(nm_device))
+
+ def _check_network_device(self, nm_device):
+ if not nm_device.is_valid():
+ logging.debug("Device %s not valid" % nm_device.get_op())
+ return
+
+ dtype = nm_device.get_type()
+ if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS \
+ or dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC:
+ self._add_network_device(nm_device)
+
+ def _get_network_device(self, nm_device):
+ return self._devices[str(nm_device.get_op())]
+
+ def _network_device_state_changed_cb(self, dev, param):
+ if dev.props.state == device.STATE_INACTIVE:
+ self.remove_device(dev)
+
+ def _add_network_device(self, nm_device):
+ if self._devices.has_key(str(nm_device.get_op())):
+ logging.debug("Tried to add device %s twice" % nm_device.get_op())
+ return
+
+ dtype = nm_device.get_type()
+ if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS:
+ dev = wireless.Device(nm_device)
+ self.add_device(dev)
+ sigid = dev.connect('notify::state',
+ self._network_device_state_changed_cb)
+ self._sigids[dev] = sigid
+ if dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC:
+ dev = mesh.Device(nm_device)
+ self.add_device(dev)
+ sigid = dev.connect('notify::state',
+ self._network_device_state_changed_cb)
+ self._sigids[dev] = sigid
+
+ def __iter__(self):
+ return iter(self._devices.values())
+
+ def add_device(self, dev):
+ self._devices[dev.get_id()] = dev
+ self.emit('device-appeared', dev)
+
+ def remove_device(self, dev):
+ self.emit('device-disappeared', self._devices[dev.get_id()])
+ dev.disconnect(self._sigids[dev])
+ del self._sigids[dev]
+ del self._devices[dev.get_id()]
diff --git a/src/jarabe/model/devices/network/Makefile.am b/src/jarabe/model/devices/network/Makefile.am
new file mode 100644
index 0000000..736d96c
--- /dev/null
+++ b/src/jarabe/model/devices/network/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/jarabe/model/devices/network
+sugar_PYTHON = \
+ __init__.py \
+ mesh.py \
+ wired.py \
+ wireless.py
diff --git a/src/jarabe/model/devices/network/__init__.py b/src/jarabe/model/devices/network/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/model/devices/network/__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/jarabe/model/devices/network/mesh.py b/src/jarabe/model/devices/network/mesh.py
new file mode 100644
index 0000000..57f375f
--- /dev/null
+++ b/src/jarabe/model/devices/network/mesh.py
@@ -0,0 +1,74 @@
+#
+# 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 gobject
+
+from jarabe.model.devices import device
+
+class Device(device.Device):
+ __gproperties__ = {
+ 'strength' : (int, None, None, 0, 100, 0,
+ gobject.PARAM_READABLE),
+ 'state' : (int, None, None, device.STATE_ACTIVATING,
+ device.STATE_INACTIVE, 0, gobject.PARAM_READABLE),
+ 'activation-stage': (int, None, None, 0, 7, 0, gobject.PARAM_READABLE),
+ 'frequency': (float, None, None, 0, 2.72, 0, gobject.PARAM_READABLE),
+ 'mesh-step': (int, None, None, 0, 4, 0, gobject.PARAM_READABLE),
+ }
+
+ def __init__(self, nm_device):
+ device.Device.__init__(self)
+ self._nm_device = nm_device
+
+ self._nm_device.connect('strength-changed',
+ self._strength_changed_cb)
+ self._nm_device.connect('state-changed',
+ self._state_changed_cb)
+ self._nm_device.connect('activation-stage-changed',
+ self._activation_stage_changed_cb)
+
+ def _strength_changed_cb(self, nm_device):
+ self.notify('strength')
+
+ def _state_changed_cb(self, nm_device):
+ self.notify('state')
+
+ def _activation_stage_changed_cb(self, nm_device):
+ self.notify('activation-stage')
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'strength':
+ return self._nm_device.get_strength()
+ elif pspec.name == 'state':
+ nm_state = self._nm_device.get_state()
+ return device.nm_state_to_state[nm_state]
+ elif pspec.name == 'activation-stage':
+ return self._nm_device.get_activation_stage()
+ elif pspec.name == 'frequency':
+ return self._nm_device.get_frequency()
+ elif pspec.name == 'mesh-step':
+ return self._nm_device.get_mesh_step()
+
+ def get_type(self):
+ return 'network.mesh'
+
+ def get_id(self):
+ return str(self._nm_device.get_op())
+
+ def get_nm_device(self):
+ return self._nm_device
+
diff --git a/src/jarabe/model/devices/network/wired.py b/src/jarabe/model/devices/network/wired.py
new file mode 100644
index 0000000..4178010
--- /dev/null
+++ b/src/jarabe/model/devices/network/wired.py
@@ -0,0 +1,28 @@
+# 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 jarabe.model.devices import device
+
+class Device(device.Device):
+ def __init__(self, nm_device):
+ device.Device.__init__(self)
+ self._nm_device = nm_device
+
+ def get_id(self):
+ return str(self._nm_device.get_op())
+
+ def get_type(self):
+ return 'network.wired'
diff --git a/src/jarabe/model/devices/network/wireless.py b/src/jarabe/model/devices/network/wireless.py
new file mode 100644
index 0000000..c5b2a5c
--- /dev/null
+++ b/src/jarabe/model/devices/network/wireless.py
@@ -0,0 +1,95 @@
+#
+# 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 gobject
+
+from jarabe.model.devices import device
+
+def freq_to_channel(freq):
+ ftoc = { 2.412: 1, 2.417: 2, 2.422: 3, 2.427: 4,
+ 2.432: 5, 2.437: 6, 2.442: 7, 2.447: 8,
+ 2.452: 9, 2.457: 10, 2.462: 11, 2.467: 12,
+ 2.472: 13
+ }
+ return ftoc[freq]
+
+def channel_to_freq(channel):
+ ctof = { 1: 2.412, 2: 2.417, 3: 2.422, 4: 2.427,
+ 5: 2.432, 6: 2.437, 7: 2.442, 8: 2.447,
+ 9: 2.452, 10: 2.457, 11: 2.462, 12: 2.467,
+ 13: 2.472
+ }
+ return ctof[channel]
+
+class Device(device.Device):
+ __gproperties__ = {
+ 'name' : (str, None, None, None,
+ gobject.PARAM_READABLE),
+ 'strength' : (int, None, None, 0, 100, 0,
+ gobject.PARAM_READABLE),
+ 'state' : (int, None, None, device.STATE_ACTIVATING,
+ device.STATE_INACTIVE, 0, gobject.PARAM_READABLE),
+ 'frequency': (float, None, None, 0.0, 9999.99, 0.0,
+ gobject.PARAM_READABLE)
+ }
+
+ def __init__(self, nm_device):
+ device.Device.__init__(self)
+ self._nm_device = nm_device
+
+ self._nm_device.connect('strength-changed',
+ self._strength_changed_cb)
+ self._nm_device.connect('ssid-changed',
+ self._ssid_changed_cb)
+ self._nm_device.connect('state-changed',
+ self._state_changed_cb)
+
+ def _strength_changed_cb(self, nm_device):
+ self.notify('strength')
+
+ def _ssid_changed_cb(self, nm_device):
+ self.notify('name')
+
+ def _state_changed_cb(self, nm_device):
+ self.notify('state')
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'strength':
+ return self._nm_device.get_strength()
+ elif pspec.name == 'name':
+ import logging
+ logging.debug('wireless.Device.props.name: %s' %
+ self._nm_device.get_ssid())
+ return self._nm_device.get_ssid()
+ elif pspec.name == 'state':
+ nm_state = self._nm_device.get_state()
+ return device.nm_state_to_state[nm_state]
+ elif pspec.name == 'frequency':
+ return self._nm_device.get_frequency()
+
+ def get_type(self):
+ return 'network.wireless'
+
+ def get_id(self):
+ return str(self._nm_device.get_op())
+
+ def get_active_network_colors(self):
+ net = self._nm_device.get_active_network()
+ if not net:
+ return (None, None)
+ return net.get_colors()
+
diff --git a/src/jarabe/model/devices/speaker.py b/src/jarabe/model/devices/speaker.py
new file mode 100644
index 0000000..73e4f6e
--- /dev/null
+++ b/src/jarabe/model/devices/speaker.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2008 Martin Dengler
+#
+# 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 gobject
+
+from jarabe.hardware import hardwaremanager
+from jarabe.model.devices import device
+
+class Device(device.Device):
+ __gproperties__ = {
+ 'level' : (int, None, None, 0, 100, 0, gobject.PARAM_READWRITE),
+ 'muted' : (bool, None, None, False, gobject.PARAM_READWRITE),
+ }
+
+ def __init__(self):
+ device.Device.__init__(self)
+ self._manager = hardwaremanager.get_manager()
+ self._manager.connect('muted-changed', self.__muted_changed_cb)
+ self._manager.connect('volume-changed', self.__volume_changed_cb)
+
+ def __muted_changed_cb(self, sender_, old_state_, new_state_):
+ self.notify('muted')
+
+ def __volume_changed_cb(self, sender_, old_volume_, new_volume_):
+ self.notify('level')
+
+ def _get_level(self):
+ return self._manager.get_volume()
+
+ def _set_level(self, new_volume):
+ self._manager.set_volume(new_volume)
+
+ def _get_muted(self):
+ return self._manager.get_muted()
+
+ def _set_muted(self, mute):
+ self._manager.set_muted(mute)
+
+ def get_type(self):
+ return 'speaker'
+
+ def do_get_property(self, pspec):
+ if pspec.name == "level":
+ return self._get_level()
+ elif pspec.name == "muted":
+ return self._get_muted()
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == "level":
+ self._set_level(value)
+ elif pspec.name == "muted":
+ self._set_muted(value)
diff --git a/src/jarabe/model/homeactivity.py b/src/jarabe/model/homeactivity.py
new file mode 100644
index 0000000..ba0979e
--- /dev/null
+++ b/src/jarabe/model/homeactivity.py
@@ -0,0 +1,245 @@
+# Copyright (C) 2006-2007 Owen Williams.
+#
+# 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 time
+import logging
+import os
+
+import gobject
+import dbus
+
+from sugar.graphics.xocolor import XoColor
+from sugar.presence import presenceservice
+from sugar import profile
+from sugar import wm
+
+import config
+
+_SERVICE_NAME = "org.laptop.Activity"
+_SERVICE_PATH = "/org/laptop/Activity"
+_SERVICE_INTERFACE = "org.laptop.Activity"
+
+class HomeActivity(gobject.GObject):
+ """Activity which appears in the "Home View" of the Sugar shell
+
+ This class stores the Sugar Shell's metadata regarding a
+ given activity/application in the system. It interacts with
+ the sugar.activity.* modules extensively in order to
+ accomplish its tasks.
+ """
+
+ __gtype_name__ = 'SugarHomeActivity'
+
+ __gproperties__ = {
+ 'launching' : (bool, None, None, False,
+ gobject.PARAM_READWRITE),
+ }
+
+ def __init__(self, activity_info, activity_id, window=None):
+ """Initialise the HomeActivity
+
+ activity_info -- sugar.activity.registry.ActivityInfo instance,
+ provides the information required to actually
+ create the new instance. This is, in effect,
+ the "type" of activity being created.
+ activity_id -- unique identifier for this instance
+ of the activity type
+ window -- Main WnckWindow of the activity
+ """
+ gobject.GObject.__init__(self)
+
+ self._window = None
+ self._service = None
+ self._activity_id = activity_id
+ self._activity_info = activity_info
+ self._launch_time = time.time()
+ self._launching = False
+
+ if window is not None:
+ self.set_window(window)
+
+ self._retrieve_service()
+
+ self._name_owner_changed_handler = None
+ if not self._service:
+ bus = dbus.SessionBus()
+ self._name_owner_changed_handler = bus.add_signal_receiver(
+ self._name_owner_changed_cb,
+ signal_name="NameOwnerChanged",
+ dbus_interface="org.freedesktop.DBus")
+
+ def set_window(self, window):
+ """Set the window for the activity
+
+ We allow resetting the window for an activity so that we
+ can replace the launcher once we get its real window.
+ """
+ if not window:
+ raise ValueError("window must be valid")
+ self._window = window
+
+ def get_service(self):
+ """Get the activity service
+
+ Note that non-native Sugar applications will not have
+ such a service, so the return value will be None in
+ those cases.
+ """
+
+ return self._service
+
+ def get_title(self):
+ """Retrieve the application's root window's suggested title"""
+ if self._window:
+ return self._window.get_name()
+ else:
+ return ''
+
+ def get_icon_path(self):
+ """Retrieve the activity's icon (file) name"""
+ if self.is_journal():
+ return os.path.join(config.data_path, 'icons/activity-journal.svg')
+ elif self._activity_info:
+ return self._activity_info.icon
+ else:
+ return None
+
+ def get_icon_color(self):
+ """Retrieve the appropriate icon colour for this activity
+
+ Uses activity_id to index into the PresenceService's
+ set of activity colours, if the PresenceService does not
+ have an entry (implying that this is not a Sugar-shared application)
+ uses the local user's profile.get_color() to determine the
+ colour for the icon.
+ """
+ pservice = presenceservice.get_instance()
+
+ # HACK to suppress warning in logs when activity isn't found
+ # (if it's locally launched and not shared yet)
+ activity = None
+ for act in pservice.get_activities():
+ if self._activity_id == act.props.id:
+ activity = act
+ break
+
+ if activity != None:
+ return XoColor(activity.props.color)
+ else:
+ return profile.get_color()
+
+ def get_activity_id(self):
+ """Retrieve the "activity_id" passed in to our constructor
+
+ This is a "globally likely unique" identifier generated by
+ sugar.util.unique_id
+ """
+ return self._activity_id
+
+ def get_xid(self):
+ """Retrieve the X-windows ID of our root window"""
+ return self._window.get_xid()
+
+ def get_window(self):
+ """Retrieve the X-windows root window of this application
+
+ This was stored by the set_window method, which was
+ called by HomeModel._add_activity, which was called
+ via a callback that looks for all 'window-opened'
+ events.
+
+ HomeModel currently uses a dbus service query on the
+ activity to determine to which HomeActivity the newly
+ launched window belongs.
+ """
+ return self._window
+
+ def get_type(self):
+ """Retrieve the activity bundle id for future reference"""
+ if self._window is None:
+ return None
+ else:
+ return wm.get_bundle_id(self._window)
+
+ def is_journal(self):
+ """Returns boolean if the activity is of type JournalActivity"""
+ return self.get_type() == 'org.laptop.JournalActivity'
+
+ def get_launch_time(self):
+ """Return the time at which the activity was first launched
+
+ Format is floating-point time.time() value
+ (seconds since the epoch)
+ """
+ return self._launch_time
+
+ def get_pid(self):
+ """Returns the activity's PID"""
+ return self._window.get_pid()
+
+ def equals(self, activity):
+ if self._activity_id and activity.get_activity_id():
+ return self._activity_id == activity.get_activity_id()
+ if self._window.get_xid() and activity.get_xid():
+ return self._window.get_xid() == activity.get_xid()
+ return False
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'launching':
+ self._launching = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'launching':
+ return self._launching
+
+ def _get_service_name(self):
+ if self._activity_id:
+ return _SERVICE_NAME + self._activity_id
+ else:
+ return None
+
+ def _retrieve_service(self):
+ if not self._activity_id:
+ return
+
+ try:
+ bus = dbus.SessionBus()
+ proxy = bus.get_object(self._get_service_name(),
+ _SERVICE_PATH + "/" + self._activity_id)
+ self._service = dbus.Interface(proxy, _SERVICE_INTERFACE)
+ except dbus.DBusException:
+ self._service = None
+
+ def _name_owner_changed_cb(self, name, old, new):
+ if name == self._get_service_name():
+ self._retrieve_service()
+ self.set_active(True)
+ self._name_owner_changed_handler.remove()
+ self._name_owner_changed_handler = None
+
+ def set_active(self, state):
+ """Propagate the current state to the activity object"""
+ if self._service is not None:
+ self._service.SetActive(state,
+ reply_handler=self._set_active_success,
+ error_handler=self._set_active_error)
+
+ def _set_active_success(self):
+ pass
+
+ def _set_active_error(self, err):
+ logging.error("set_active() failed: %s" % err)
+
diff --git a/src/jarabe/model/homemodel.py b/src/jarabe/model/homemodel.py
new file mode 100644
index 0000000..3f116bf
--- /dev/null
+++ b/src/jarabe/model/homemodel.py
@@ -0,0 +1,286 @@
+# Copyright (C) 2006-2007 Owen Williams.
+#
+# 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 gobject
+import wnck
+import gtk
+
+from sugar import wm
+from sugar.activity import get_registry
+
+from jarabe.model.homeactivity import HomeActivity
+
+def get_sugar_window_type(wnck_window):
+ window = gtk.gdk.window_foreign_new(wnck_window.get_xid())
+ prop_info = window.property_get('_SUGAR_WINDOW_TYPE', 'STRING')
+ if prop_info is None:
+ return None
+ else:
+ return prop_info[2]
+
+class HomeModel(gobject.GObject):
+ """Model of the "Home" view (activity management)
+
+ The HomeModel is basically the point of registration
+ for all running activities within Sugar. It traps
+ events that tell the system there is a new activity
+ being created (generated by the activity factories),
+ or removed, as well as those which tell us that the
+ currently focussed activity has changed.
+
+ The HomeModel tracks a set of HomeActivity instances,
+ which are tracking the window to activity mappings
+ the activity factories have set up.
+ """
+ __gsignals__ = {
+ 'activity-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'activity-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-started': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-completed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-failed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._activities = []
+ self._active_activity = None
+ self._tabbing_activity = None
+
+ screen = wnck.screen_get_default()
+ screen.connect('window-opened', self._window_opened_cb)
+ screen.connect('window-closed', self._window_closed_cb)
+ screen.connect('active-window-changed',
+ self._active_window_changed_cb)
+
+ def _get_activities_with_window(self):
+ ret = []
+ for i in self._activities:
+ if i.get_window() is not None:
+ ret.append(i)
+ return ret
+
+ def get_previous_activity(self, current=None):
+ if not current:
+ current = self._active_activity
+
+ activities = self._get_activities_with_window()
+ i = activities.index(current)
+ if len(activities) == 0:
+ return None
+ elif i - 1 >= 0:
+ return activities[i - 1]
+ else:
+ return activities[len(activities) - 1]
+
+ def get_next_activity(self, current=None):
+ if not current:
+ current = self._active_activity
+
+ activities = self._get_activities_with_window()
+ i = activities.index(current)
+ if len(activities) == 0:
+ return None
+ elif i + 1 < len(activities):
+ return activities[i + 1]
+ else:
+ return activities[0]
+
+ def get_active_activity(self):
+ """Returns the activity that the user is currently working in"""
+ return self._active_activity
+
+ def get_tabbing_activity(self):
+ """Returns the activity that is currently highlighted during tabbing"""
+ return self._tabbing_activity
+
+ def set_tabbing_activity(self, activity):
+ """Sets the activity that is currently highlighted during tabbing"""
+ self._tabbing_activity = activity
+ self.emit("tabbing-activity-changed", self._tabbing_activity)
+
+ def _set_active_activity(self, home_activity):
+ if self._active_activity == home_activity:
+ return
+
+ if home_activity:
+ home_activity.set_active(True)
+
+ if self._active_activity:
+ self._active_activity.set_active(False)
+
+ self._active_activity = home_activity
+ self.emit('active-activity-changed', self._active_activity)
+
+ def __iter__(self):
+ return iter(self._activities)
+
+ def __len__(self):
+ return len(self._activities)
+
+ def __getitem__(self, i):
+ return self._activities[i]
+
+ def index(self, obj):
+ return self._activities.index(obj)
+
+ def _window_opened_cb(self, screen, window):
+ if window.get_window_type() == wnck.WINDOW_NORMAL:
+ home_activity = None
+
+ activity_id = wm.get_activity_id(window)
+
+ service_name = wm.get_bundle_id(window)
+ if service_name:
+ registry = get_registry()
+ activity_info = registry.get_activity(service_name)
+ else:
+ activity_info = None
+
+ if activity_id:
+ home_activity = self._get_activity_by_id(activity_id)
+
+ if not home_activity:
+ home_activity = HomeActivity(activity_info, activity_id, window)
+ self._add_activity(home_activity)
+ else:
+ home_activity.set_window(window)
+
+ if get_sugar_window_type(window) != 'launcher':
+ home_activity.props.launching = False
+ self.emit('launch-completed', home_activity)
+
+ if self._active_activity is None:
+ self._set_active_activity(home_activity)
+
+ def _window_closed_cb(self, screen, window):
+ if window.get_window_type() == wnck.WINDOW_NORMAL:
+ self._remove_activity_by_xid(window.get_xid())
+
+ def _get_activity_by_xid(self, xid):
+ for home_activity in self._activities:
+ if home_activity.get_xid() == xid:
+ return home_activity
+ return None
+
+ def _get_activity_by_id(self, activity_id):
+ for home_activity in self._activities:
+ if home_activity.get_activity_id() == activity_id:
+ return home_activity
+ return None
+
+ def _active_window_changed_cb(self, screen, previous_window=None):
+ window = screen.get_active_window()
+ if window is None:
+ return
+
+ if window.get_window_type() != wnck.WINDOW_DIALOG:
+ while window.get_transient() is not None:
+ window = window.get_transient()
+
+ act = self._get_activity_by_xid(window.get_xid())
+ if act is not None:
+ self._set_active_activity(act)
+
+ def _add_activity(self, home_activity):
+ self._activities.append(home_activity)
+ self.emit('activity-added', home_activity)
+
+ def _remove_activity(self, home_activity):
+ if home_activity == self._active_activity:
+ windows = wnck.screen_get_default().get_windows_stacked()
+ windows.reverse()
+ for window in windows:
+ new_activity = self._get_activity_by_xid(window.get_xid())
+ if new_activity is not None:
+ self._set_active_activity(new_activity)
+ break
+ else:
+ logging.error('No activities are running')
+ self._set_active_activity(None)
+
+ self.emit('activity-removed', home_activity)
+ self._activities.remove(home_activity)
+
+ def _remove_activity_by_xid(self, xid):
+ home_activity = self._get_activity_by_xid(xid)
+ if home_activity:
+ self._remove_activity(home_activity)
+ else:
+ logging.error('Model for window %d does not exist.' % xid)
+
+ def notify_launch(self, activity_id, service_name):
+ registry = get_registry()
+ activity_info = registry.get_activity(service_name)
+ if not activity_info:
+ raise ValueError("Activity service name '%s'" \
+ " was not found in the bundle registry."
+ % service_name)
+ home_activity = HomeActivity(activity_info, activity_id)
+ home_activity.props.launching = True
+ self._add_activity(home_activity)
+
+ self._set_active_activity(home_activity)
+
+ self.emit('launch-started', home_activity)
+
+ # FIXME: better learn about finishing processes by receiving a signal.
+ # Now just check whether an activity has a window after ~90sec
+ gobject.timeout_add(90000, self._check_activity_launched, activity_id)
+
+ def notify_launch_failed(self, activity_id):
+ home_activity = self._get_activity_by_id(activity_id)
+ if home_activity:
+ logging.debug("Activity %s (%s) launch failed" % \
+ (activity_id, home_activity.get_type()))
+ home_activity.props.launching = False
+ self._remove_activity(home_activity)
+ else:
+ logging.error('Model for activity id %s does not exist.'
+ % activity_id)
+
+ self.emit('launch-failed', home_activity)
+
+ def _check_activity_launched(self, activity_id):
+ home_activity = self._get_activity_by_id(activity_id)
+
+ if not home_activity:
+ logging.debug('Activity %s has been closed already.' % activity_id)
+ return False
+
+ if home_activity.props.launching:
+ logging.debug('Activity %s still launching, assuming it failed...'
+ % activity_id)
+ self.notify_launch_failed(activity_id)
+ return False
diff --git a/src/jarabe/model/shellmodel.py b/src/jarabe/model/shellmodel.py
new file mode 100644
index 0000000..0c37f35
--- /dev/null
+++ b/src/jarabe/model/shellmodel.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 wnck
+import gobject
+
+from sugar.presence import presenceservice
+from jarabe.model.Friends import Friends
+from jarabe.model.MeshModel import MeshModel
+from jarabe.model.homemodel import HomeModel
+from jarabe.model.Owner import ShellOwner
+from jarabe.model.devices.devicesmodel import DevicesModel
+
+class ShellModel(gobject.GObject):
+ ZOOM_MESH = 0
+ ZOOM_FRIENDS = 1
+ ZOOM_HOME = 2
+ ZOOM_ACTIVITY = 3
+
+ __gproperties__ = {
+ 'zoom-level' : (int, None, None,
+ 0, 3, ZOOM_HOME,
+ gobject.PARAM_READABLE)
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._current_activity = None
+ self._zoom_level = self.ZOOM_HOME
+ self._showing_desktop = True
+
+ self._pservice = presenceservice.get_instance()
+
+ self._owner = ShellOwner()
+
+ self._friends = Friends()
+ self._mesh = MeshModel()
+ self._home = HomeModel()
+ self._devices = DevicesModel()
+
+ self._screen = wnck.screen_get_default()
+ self._screen.connect('showing-desktop-changed',
+ self._showing_desktop_changed_cb)
+
+ def set_zoom_level(self, level):
+ self._zoom_level = level
+ self.notify('zoom-level')
+
+ def get_zoom_level(self):
+ if self._screen.get_showing_desktop():
+ return self._zoom_level
+ else:
+ return self.ZOOM_ACTIVITY
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'zoom-level':
+ return self.get_zoom_level()
+
+ def get_mesh(self):
+ return self._mesh
+
+ def get_friends(self):
+ return self._friends
+
+ def get_invites(self):
+ return self._owner.get_invites()
+
+ def get_home(self):
+ return self._home
+
+ def get_owner(self):
+ return self._owner
+
+ def get_devices(self):
+ return self._devices
+
+ def _showing_desktop_changed_cb(self, screen):
+ showing_desktop = self._screen.get_showing_desktop()
+ if self._showing_desktop != showing_desktop:
+ self._showing_desktop = showing_desktop
+ self.notify('zoom-level')
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = ShellModel()
+ return _instance
+
diff --git a/src/jarabe/session.py b/src/jarabe/session.py
new file mode 100644
index 0000000..c1f8d0f
--- /dev/null
+++ b/src/jarabe/session.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2008, 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 dbus
+import os
+import signal
+
+from sugar import session
+from sugar import env
+
+from jarabe.hardware import hardwaremanager
+
+_session_manager = None
+
+class SessionManager(session.SessionManager):
+ MODE_LOGOUT = 0
+ MODE_SHUTDOWN = 1
+ MODE_REBOOT = 2
+
+ def __init__(self):
+ session.SessionManager.__init__(self)
+ self._logout_mode = None
+
+ def logout(self):
+ self._logout_mode = self.MODE_LOGOUT
+ self.initiate_shutdown()
+
+ def shutdown(self):
+ self._logout_mode = self.MODE_SHUTDOWN
+ self.initiate_shutdown()
+
+ def reboot(self):
+ self._logout_mode = self.MODE_REBOOT
+ self.initiate_shutdown()
+
+ def shutdown_completed(self):
+ session.SessionManager.shutdown_completed(self)
+
+ hw_manager = hardwaremanager.get_manager()
+ hw_manager.shutdown()
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object('org.freedesktop.Hal',
+ '/org/freedesktop/Hal/devices/computer')
+ pm = dbus.Interface(proxy, \
+ 'org.freedesktop.Hal.Device.SystemPowerManagement')
+
+ if env.is_emulator():
+ self._close_emulator()
+ else:
+ if self._logout_mode == self.MODE_SHUTDOWN:
+ pm.Shutdown()
+ elif self._logout_mode == self.MODE_REBOOT:
+ pm.Reboot()
+
+ gtk.main_quit()
+
+ def _close_emulator(self):
+ if os.environ.has_key('SUGAR_EMULATOR_PID'):
+ pid = int(os.environ['SUGAR_EMULATOR_PID'])
+ os.kill(pid, signal.SIGTERM)
+
+def get_session_manager():
+ global _session_manager
+
+ if _session_manager == None:
+ _session_manager = SessionManager()
+ return _session_manager
diff --git a/src/jarabe/shellservice.py b/src/jarabe/shellservice.py
new file mode 100644
index 0000000..fd8868e
--- /dev/null
+++ b/src/jarabe/shellservice.py
@@ -0,0 +1,135 @@
+# 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
+
+"""D-bus service providing access to the shell's functionality"""
+import dbus
+import os
+
+from jarabe.view import Shell
+from jarabe.model import shellmodel
+
+_DBUS_SERVICE = "org.laptop.Shell"
+_DBUS_SHELL_IFACE = "org.laptop.Shell"
+_DBUS_OWNER_IFACE = "org.laptop.Shell.Owner"
+_DBUS_PATH = "/org/laptop/Shell"
+
+_DBUS_RAINBOW_IFACE = "org.laptop.security.Rainbow"
+
+class ShellService(dbus.service.Object):
+ """Provides d-bus service to script the shell's operations
+
+ Uses a shell_model object to observe events such as changes to:
+
+ * nickname
+ * colour
+ * icon
+ * currently active activity
+
+ and pass the event off to the methods in the dbus signature.
+
+ Key method here at the moment is add_bundle, which is used to
+ do a run-time registration of a bundle using it's application path.
+
+ XXX At the moment the d-bus service methods do not appear to do
+ anything other than add_bundle
+ """
+
+ _rainbow = None
+
+ def __init__(self):
+ self._shell = Shell.get_instance()
+ self._shell_model = shellmodel.get_instance()
+
+ self._owner = self._shell_model.get_owner()
+ self._owner.connect('nick-changed', self._owner_nick_changed_cb)
+ self._owner.connect('icon-changed', self._owner_icon_changed_cb)
+ self._owner.connect('color-changed', self._owner_color_changed_cb)
+
+ self._home_model = self._shell_model.get_home()
+ self._home_model.connect('active-activity-changed',
+ self._cur_activity_changed_cb)
+
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ in_signature="s", out_signature="b")
+ def ActivateActivity(self, activity_id):
+ host = self._shell.get_activity(activity_id)
+ if host:
+ host.present()
+ return True
+
+ return False
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ in_signature="ss", out_signature="")
+ def NotifyLaunch(self, bundle_id, activity_id):
+ home = self._shell.get_model().get_home()
+ home.notify_launch(activity_id, bundle_id)
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ in_signature="s", out_signature="")
+ def NotifyLaunchFailure(self, activity_id):
+ home = self._shell.get_model().get_home()
+ home.notify_launch_failed(activity_id)
+
+ @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s")
+ def ColorChanged(self, color):
+ pass
+
+ def _owner_color_changed_cb(self, new_color):
+ self.ColorChanged(new_color.to_string())
+
+ @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s")
+ def NickChanged(self, nick):
+ pass
+
+ def _owner_nick_changed_cb(self, new_nick):
+ self.NickChanged(new_nick)
+
+ @dbus.service.signal(_DBUS_OWNER_IFACE, signature="ay")
+ def IconChanged(self, icon_data):
+ pass
+
+ def _owner_icon_changed_cb(self, new_icon):
+ self.IconChanged(dbus.ByteArray(new_icon))
+
+ def _get_rainbow_service(self):
+ """Lazily initializes an interface to the Rainbow security daemon."""
+ if self._rainbow is None:
+ system_bus = dbus.SystemBus()
+ obj = system_bus.get_object(_DBUS_RAINBOW_IFACE, '/',
+ follow_name_owner_changes=True)
+ self._rainbow = dbus.Interface(obj,
+ dbus_interface=_DBUS_RAINBOW_IFACE)
+ return self._rainbow
+
+ @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s")
+ def CurrentActivityChanged(self, activity_id):
+ if os.path.exists('/etc/olpc-security'):
+ self._get_rainbow_service().ChangeActivity(
+ activity_id,
+ dbus_interface=_DBUS_RAINBOW_IFACE)
+
+ def _cur_activity_changed_cb(self, owner, new_activity):
+ new_id = ""
+ if new_activity:
+ new_id = new_activity.get_activity_id()
+ if new_id:
+ self.CurrentActivityChanged(new_id)
+
diff --git a/src/jarabe/uicheck.py b/src/jarabe/uicheck.py
new file mode 100644
index 0000000..aebe990
--- /dev/null
+++ b/src/jarabe/uicheck.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2008, 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 os
+import sys
+import subprocess
+import time
+
+import gobject
+import gtk
+import wnck
+
+from sugar import wm
+
+from jarabe.model.homemodel import get_sugar_window_type
+import config
+
+checks_queue = []
+checks_failed = []
+checks_succeeded = []
+
+def get_dbus_version():
+ p = subprocess.Popen(['dbus-daemon', '--version'], stdout=subprocess.PIPE)
+
+ output = p.communicate()[0]
+ first_line = output.split('\n')[0]
+
+ return first_line.split(' ')[-1]
+
+class Check(object):
+ def __init__(self):
+ self.name = None
+ self.succeeded = False
+ self.start_time = None
+ self.max_time = None
+ self.timeout = None
+
+ def start(self):
+ logging.info('Start %s check.' % self.name)
+
+ self.start_time = time.time()
+
+ def get_failed(self):
+ if self.max_time and self.start_time:
+ if time.time() - self.start_time > self.max_time:
+ return True
+ return False
+
+ failed = property(get_failed)
+
+class ShellCheck(Check):
+ def __init__(self):
+ Check.__init__(self)
+
+ self.name = 'Shell'
+ self.max_time = 30
+
+ def start(self):
+ Check.start(self)
+
+ screen = wnck.screen_get_default()
+ screen.connect('window-opened', self._window_opened_cb)
+
+ def _window_opened_cb(self, screen, window):
+ if window.get_window_type() == wnck.WINDOW_DESKTOP:
+ self.succeeded = True
+
+class ActivityCheck(Check):
+ def __init__(self, bundle_id):
+ Check.__init__(self)
+
+ self.name = bundle_id
+ self.max_time = 30
+
+ def start(self):
+ Check.start(self)
+
+ self.launch_activity()
+
+ screen = wnck.screen_get_default()
+ screen.connect('window-opened', self._window_opened_cb)
+
+ def launch_activity(self):
+ from sugar.activity import activityfactory
+
+ activityfactory.create(self.name)
+
+ def _window_opened_cb(self, screen, window):
+ if wm.get_bundle_id(window) == self.name and \
+ get_sugar_window_type(window) != 'launcher':
+ self.succeeded = True
+
+def _timeout_cb():
+ check = checks_queue[0]
+ if check.failed:
+ logging.info('%s check failed.' % (check.name))
+ checks_failed.append(checks_queue.pop(0))
+ elif check.succeeded:
+ logging.info('%s check succeeded.' % (check.name))
+ checks_succeeded.append(checks_queue.pop(0))
+ else:
+ return True
+
+ if len(checks_queue) > 0:
+ checks_queue[0].start()
+ else:
+ gtk.main_quit()
+
+ return True
+
+def main():
+ os.environ['GTK2_RC_FILES'] = os.path.join(config.data_path, 'sugar.gtkrc')
+
+ logging.basicConfig(level=logging.INFO,
+ format='%(asctime)s %(levelname)s %(message)s')
+
+ checks_queue.append(ShellCheck())
+
+ if get_dbus_version() >= '1.2.1':
+ # FIXME needs to get a list of the installed activities
+ checks_queue.append(ActivityCheck('org.laptop.Log'))
+ checks_queue.append(ActivityCheck('org.laptop.Chat'))
+ checks_queue.append(ActivityCheck('org.laptop.WebActivity'))
+ checks_queue.append(ActivityCheck('org.laptop.Pippy'))
+ checks_queue.append(ActivityCheck('org.laptop.Terminal'))
+ checks_queue.append(ActivityCheck('org.laptop.AbiWordActivity'))
+
+ checks_queue[0].start()
+ gobject.timeout_add(500, _timeout_cb)
+
+ gtk.main()
+
+ if len(checks_failed) > 0:
+ sys.exit(1)
diff --git a/src/jarabe/view/ActivityHost.py b/src/jarabe/view/ActivityHost.py
new file mode 100644
index 0000000..e3be3db
--- /dev/null
+++ b/src/jarabe/view/ActivityHost.py
@@ -0,0 +1,63 @@
+# 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 logging
+
+from jarabe.view import OverlayWindow
+
+class ActivityHost:
+ def __init__(self, model):
+ self._model = model
+ self._window = model.get_window()
+ self._gdk_window = gtk.gdk.window_foreign_new(self.get_xid())
+
+ try:
+ self._overlay_window = OverlayWindow.OverlayWindow(self._gdk_window)
+ except RuntimeError:
+ self._overlay_window = None
+
+ def get_id(self):
+ return self._model.get_activity_id()
+
+ def get_xid(self):
+ return self._window.get_xid()
+
+ def get_model(self):
+ return self._model
+
+ def invite(self, buddy_model):
+ service = self._model.get_service()
+ if service:
+ buddy = buddy_model.get_buddy()
+ service.Invite(buddy.props.key)
+ else:
+ logging.error('Invite failed, activity service not ')
+
+ def toggle_fullscreen(self):
+ fullscreen = not self._window.is_fullscreen()
+ self._window.set_fullscreen(fullscreen)
+
+ def present(self):
+ self._window.activate(gtk.get_current_event_time())
+
+ def close(self):
+ # The "1" is a fake timestamp as with present()
+ self._window.close(1)
+
+ def show_dialog(self, dialog):
+ dialog.show()
+ dialog.window.set_transient_for(self._gdk_window)
diff --git a/src/jarabe/view/BuddyIcon.py b/src/jarabe/view/BuddyIcon.py
new file mode 100644
index 0000000..3b1db4c
--- /dev/null
+++ b/src/jarabe/view/BuddyIcon.py
@@ -0,0 +1,60 @@
+# 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 sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+
+from jarabe.view.BuddyMenu import BuddyMenu
+
+class BuddyIcon(CanvasIcon):
+ def __init__(self, buddy, size=style.STANDARD_ICON_SIZE):
+ CanvasIcon.__init__(self, icon_name='computer-xo', size=size)
+
+ self._greyed_out = False
+ self._buddy = buddy
+ self._buddy.connect('appeared', self._buddy_presence_change_cb)
+ self._buddy.connect('disappeared', self._buddy_presence_change_cb)
+ self._buddy.connect('color-changed', self._buddy_presence_change_cb)
+
+ palette = BuddyMenu(buddy)
+ self.set_palette(palette)
+
+ self._update_color()
+
+ def _buddy_presence_change_cb(self, buddy, color=None):
+ # Update the icon's color when the buddy comes and goes
+ self._update_color()
+
+ def _update_color(self):
+
+ # keep the icon in the palette in sync with the view
+ palette = self.get_palette()
+ palette_icon = palette.props.icon
+
+ if self._greyed_out:
+ self.props.stroke_color = '#D5D5D5'
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ palette_icon.props.stroke_color = '#D5D5D5'
+ palette_icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ else:
+ self.props.xo_color = self._buddy.get_color()
+ palette_icon.props.xo_color = self._buddy.get_color()
+
+ def set_filter(self, query):
+ self._greyed_out = (self._buddy.get_nick().lower().find(query) == -1) \
+ and not self._buddy.is_owner()
+ self._update_color()
+
diff --git a/src/jarabe/view/BuddyMenu.py b/src/jarabe/view/BuddyMenu.py
new file mode 100644
index 0000000..2f7a048
--- /dev/null
+++ b/src/jarabe/view/BuddyMenu.py
@@ -0,0 +1,121 @@
+# 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.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+
+from jarabe.model import shellmodel
+import view.Shell
+
+class BuddyMenu(Palette):
+ def __init__(self, buddy):
+ self._buddy = buddy
+
+ buddy_icon = Icon(icon_name='computer-xo',
+ xo_color=buddy.get_color(),
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+ Palette.__init__(self, None, primary_text=buddy.get_nick(),
+ icon=buddy_icon)
+ self._invite_menu = None
+ self._active_activity_changed_hid = None
+ self.connect('destroy', self.__destroy_cb)
+
+ self._buddy.connect('icon-changed', self._buddy_icon_changed_cb)
+ self._buddy.connect('nick-changed', self._buddy_nick_changed_cb)
+
+ if not buddy.is_owner():
+ self._add_items()
+
+ def _get_home_model(self):
+ return shellmodel.get_instance().get_home()
+
+ def __destroy_cb(self, menu):
+ if self._active_activity_changed_hid is not None:
+ home_model = self._get_home_model()
+ home_model.disconnect(self._active_activity_changed_hid)
+ self._buddy.disconnect_by_func(self._buddy_icon_changed_cb)
+ self._buddy.disconnect_by_func(self._buddy_nick_changed_cb)
+
+ def _add_items(self):
+ friends = shellmodel.get_instance().get_friends()
+ if friends.has_buddy(self._buddy):
+ menu_item = MenuItem(_('Remove friend'), 'list-remove')
+ menu_item.connect('activate', self._remove_friend_cb)
+ else:
+ menu_item = MenuItem(_('Make friend'), 'list-add')
+ menu_item.connect('activate', self._make_friend_cb)
+
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ self._invite_menu = MenuItem('')
+ self._invite_menu.connect('activate', self._invite_friend_cb)
+ self.menu.append(self._invite_menu)
+
+ home_model = self._get_home_model()
+ self._active_activity_changed_hid = home_model.connect(
+ 'active-activity-changed', self._cur_activity_changed_cb)
+ activity = home_model.get_active_activity()
+ self._update_invite_menu(activity)
+
+ def _update_invite_menu(self, activity):
+ buddy_activity = self._buddy.get_current_activity()
+ if buddy_activity is not None:
+ buddy_activity_id = buddy_activity.props.id
+ else:
+ buddy_activity_id = None
+
+ if activity is None or activity.is_journal() or \
+ activity.get_activity_id() == buddy_activity_id:
+ self._invite_menu.hide()
+ else:
+ title = activity.get_title()
+ label = self._invite_menu.get_children()[0]
+ label.set_text(_('Invite to %s') % title)
+
+ icon = Icon(file=activity.get_icon_path())
+ icon.props.xo_color = activity.get_icon_color()
+ self._invite_menu.set_image(icon)
+ icon.show()
+
+ self._invite_menu.show()
+
+ def _cur_activity_changed_cb(self, home_model, activity_model):
+ self._update_invite_menu(activity_model)
+
+ def _buddy_icon_changed_cb(self, buddy):
+ pass
+
+ def _buddy_nick_changed_cb(self, buddy, nick):
+ self.set_primary_text(nick)
+
+ def _make_friend_cb(self, menuitem):
+ friends = shellmodel.get_instance().get_friends()
+ friends.make_friend(self._buddy)
+
+ def _remove_friend_cb(self, menuitem):
+ friends = shellmodel.get_instance().get_friends()
+ friends.remove(self._buddy)
+
+ def _invite_friend_cb(self, menuitem):
+ activity = view.Shell.get_instance().get_current_activity()
+ activity.invite(self._buddy)
+
diff --git a/src/jarabe/view/Makefile.am b/src/jarabe/view/Makefile.am
new file mode 100644
index 0000000..040e6e7
--- /dev/null
+++ b/src/jarabe/view/Makefile.am
@@ -0,0 +1,17 @@
+SUBDIRS = devices frame home
+
+sugardir = $(pythondir)/jarabe/view
+sugar_PYTHON = \
+ __init__.py \
+ ActivityHost.py \
+ BuddyIcon.py \
+ BuddyMenu.py \
+ clipboardicon.py \
+ clipboardmenu.py \
+ keyhandler.py \
+ launchwindow.py \
+ pulsingicon.py \
+ tabbinghandler.py \
+ OverlayWindow.py \
+ palettes.py \
+ Shell.py
diff --git a/src/jarabe/view/OverlayWindow.py b/src/jarabe/view/OverlayWindow.py
new file mode 100644
index 0000000..15bcb49
--- /dev/null
+++ b/src/jarabe/view/OverlayWindow.py
@@ -0,0 +1,70 @@
+# 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 cairo
+
+def _grab_pixbuf(window=None):
+ if not window:
+ screen = gtk.gdk.screen_get_default()
+ window = screen.get_root_window()
+ color_map = gtk.gdk.colormap_get_system()
+ (x, y, w, h, bpp_) = window.get_geometry()
+ return gtk.gdk.pixbuf_get_from_drawable(None, window, color_map,
+ x, y, 0, 0, w, h)
+
+class OverlayWindow(gtk.Window):
+ def __init__(self, lower_window):
+ gtk.Window.__init__(self)
+ self._lower_window = lower_window
+
+ self._img = gtk.Image()
+ self.add(self._img)
+
+ self.realize()
+
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(False)
+ self.window.set_transient_for(lower_window)
+
+ self.set_decorated(False)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_default_size(gtk.gdk.screen_width(), gtk.gdk.screen_height())
+ self.set_app_paintable(True)
+
+ # self.connect('expose-event', self._expose_cb)
+
+ def appear(self):
+ pbuf = _grab_pixbuf(self._lower_window)
+ #pbuf.saturate_and_pixelate(pbuf, 0.5, False)
+ w = pbuf.get_width()
+ h = pbuf.get_height()
+ pbuf2 = pbuf.composite_color_simple(w, h, gtk.gdk.INTERP_NEAREST,
+ 100, 1024, 0, 0)
+ self._img.set_from_pixbuf(pbuf2)
+ self.show_all()
+
+ def disappear(self):
+ self._img.set_from_pixbuf(None)
+ self.hide()
+
+ def _expose_cb(self, widget, event):
+ cr = widget.window.cairo_create()
+ cr.set_source_rgba(0.0, 0.0, 0.0, 0.4) # Transparent
+ cr.set_operator(cairo.OPERATOR_SOURCE)
+ cr.paint()
+ return False
+
diff --git a/src/jarabe/view/Shell.py b/src/jarabe/view/Shell.py
new file mode 100644
index 0000000..089bfb5
--- /dev/null
+++ b/src/jarabe/view/Shell.py
@@ -0,0 +1,270 @@
+# 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 logging
+import tempfile
+import os
+import time
+import shutil
+
+import gobject
+import gtk
+import wnck
+import dbus
+
+from sugar.activity.activityhandle import ActivityHandle
+from sugar import activity
+from sugar.activity import activityfactory
+from sugar.datastore import datastore
+from sugar import profile
+from sugar import env
+
+from jarabe.view.ActivityHost import ActivityHost
+from jarabe.view.frame import frame
+from jarabe.view.keyhandler import KeyHandler
+from jarabe.view.home.HomeWindow import HomeWindow
+from jarabe.view.launchwindow import LaunchWindow
+from jarabe.model import shellmodel
+from jarabe.journal import journalactivity
+
+# #3903 - this constant can be removed and assumed to be 1 when dbus-python
+# 0.82.3 is the only version used
+if dbus.version >= (0, 82, 3):
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1
+else:
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000
+
+class Shell(gobject.GObject):
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._model = shellmodel.get_instance()
+ self._hosts = {}
+ self._launchers = {}
+ self._screen = wnck.screen_get_default()
+ self._screen_rotation = 0
+
+ self._key_handler = KeyHandler()
+
+ self._frame = frame.get_instance()
+
+ self.home_window = HomeWindow()
+ self.home_window.show()
+
+ home_model = self._model.get_home()
+ home_model.connect('launch-started', self.__launch_started_cb)
+ home_model.connect('launch-failed', self.__launch_failed_cb)
+ home_model.connect('launch-completed', self.__launch_completed_cb)
+ home_model.connect('activity-removed', self._activity_removed_cb)
+
+ gobject.idle_add(self._start_journal_idle)
+
+ def _start_journal_idle(self):
+ # Mount the datastore in internal flash
+ ds_path = env.get_profile_path('datastore')
+ try:
+ datastore.mount(ds_path, [], timeout=120 * \
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
+ except Exception, e:
+ # Don't explode if there's corruption; move the data out of the way
+ # and attempt to create a store from scratch.
+ logging.error(e)
+ shutil.move(ds_path, os.path.abspath(ds_path) + str(time.time()))
+ datastore.mount(ds_path, [], timeout=120 * \
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
+
+ journalactivity.start()
+
+ def __launch_started_cb(self, home_model, home_activity):
+ if home_activity.is_journal():
+ return
+
+ launch_window = LaunchWindow(home_activity)
+ launch_window.show()
+
+ self._launchers[home_activity.get_activity_id()] = launch_window
+ self._model.set_zoom_level(shellmodel.ShellModel.ZOOM_ACTIVITY)
+
+ def __launch_failed_cb(self, home_model, home_activity):
+ if not home_activity.is_journal():
+ self._destroy_launcher(home_activity)
+
+ def __launch_completed_cb(self, home_model, home_activity):
+ activity_host = ActivityHost(home_activity)
+ self._hosts[activity_host.get_xid()] = activity_host
+
+ if not home_activity.is_journal():
+ self._destroy_launcher(home_activity)
+
+ def _destroy_launcher(self, home_activity):
+ activity_id = home_activity.get_activity_id()
+
+ if activity_id in self._launchers:
+ self._launchers[activity_id].destroy()
+ del self._launchers[activity_id]
+ else:
+ logging.error('Launcher for %s is missing' % activity_id)
+
+ def _activity_removed_cb(self, home_model, home_activity):
+ xid = home_activity.get_xid()
+ if self._hosts.has_key(xid):
+ del self._hosts[xid]
+
+ def _get_host_from_activity_model(self, activity_model):
+ if activity_model is None:
+ raise ValueError('activity_model cannot be None')
+ xid = activity_model.get_xid()
+ if xid:
+ return self._hosts[activity_model.get_xid()]
+ else:
+ logging.debug('Activity %r dont have a window yet' % activity_model)
+ return None
+
+ def get_model(self):
+ return self._model
+
+ def get_frame(self):
+ return self._frame
+
+ def join_activity(self, bundle_id, activity_id):
+ activity_host = self.get_activity(activity_id)
+ if activity_host:
+ activity_host.present()
+ return
+
+ # Get the service name for this activity, if
+ # we have a bundle on the system capable of handling
+ # this activity type
+ registry = activity.get_registry()
+ bundle = registry.get_activity(bundle_id)
+ if not bundle:
+ logging.error("Couldn't find activity for type %s" % bundle_id)
+ return
+
+ handle = ActivityHandle(activity_id)
+ activityfactory.create(bundle_id, handle)
+
+ def start_activity(self, activity_type):
+ activityfactory.create(activity_type)
+
+ def start_activity_with_uri(self, activity_type, uri):
+ activityfactory.create_with_uri(activity_type, uri)
+
+ def take_activity_screenshot(self):
+ if self._model.get_zoom_level() != shellmodel.ShellModel.ZOOM_ACTIVITY:
+ return
+ if self.get_frame().visible:
+ return
+
+ home_model = self._model.get_home()
+ active_activity = home_model.get_active_activity()
+ if active_activity is not None:
+ service = active_activity.get_service()
+ if service is not None:
+ try:
+ service.TakeScreenshot(timeout=2.0)
+ except dbus.DBusException, e:
+ logging.debug('Error raised by TakeScreenshot(): %s', e)
+
+ def set_zoom_level(self, level):
+ if level == self._model.get_zoom_level():
+ logging.debug('Already in the level %r' % level)
+ return
+
+ if level == shellmodel.ShellModel.ZOOM_ACTIVITY:
+ host = self.get_current_activity()
+ if host is None:
+ raise ValueError('No current activity')
+ host.present()
+ else:
+ self._model.set_zoom_level(level)
+ self._screen.toggle_showing_desktop(True)
+
+ def toggle_activity_fullscreen(self):
+ if self._model.get_zoom_level() == shellmodel.ShellModel.ZOOM_ACTIVITY:
+ self.get_current_activity().toggle_fullscreen()
+
+ def activate_previous_activity(self):
+ home_model = self._model.get_home()
+ previous_activity = home_model.get_previous_activity()
+ if previous_activity:
+ previous_activity.get_window().activate(
+ gtk.get_current_event_time())
+
+ def activate_next_activity(self):
+ home_model = self._model.get_home()
+ next_activity = home_model.get_next_activity()
+ if next_activity:
+ next_activity.get_window().activate(gtk.get_current_event_time())
+
+ def close_current_activity(self):
+ if self._model.get_zoom_level() != shellmodel.ShellModel.ZOOM_ACTIVITY:
+ return
+
+ home_model = self._model.get_home()
+ active_activity = home_model.get_active_activity()
+ if active_activity.is_journal():
+ return
+
+ self.get_current_activity().close()
+
+ def get_current_activity(self):
+ home_model = self._model.get_home()
+ active_activity = home_model.get_active_activity()
+ return self._get_host_from_activity_model(active_activity)
+
+ def get_activity(self, activity_id):
+ for host in self._hosts.values():
+ if host.get_id() == activity_id:
+ return host
+ return None
+
+ def take_screenshot(self):
+ file_path = os.path.join(tempfile.gettempdir(), '%i' % time.time())
+
+ window = gtk.gdk.get_default_root_window()
+ width, height = window.get_size()
+ x_orig, y_orig = window.get_origin()
+
+ screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False,
+ bits_per_sample=8, width=width,
+ height=height)
+ screenshot.get_from_drawable(window, window.get_colormap(), x_orig,
+ y_orig, 0, 0, width, height)
+ screenshot.save(file_path, "png")
+ jobject = datastore.create()
+ try:
+ jobject.metadata['title'] = _('Screenshot')
+ jobject.metadata['keep'] = '0'
+ jobject.metadata['buddies'] = ''
+ jobject.metadata['preview'] = ''
+ jobject.metadata['icon-color'] = profile.get_color().to_string()
+ jobject.metadata['mime_type'] = 'image/png'
+ jobject.file_path = file_path
+ datastore.write(jobject, transfer_ownership=True)
+ finally:
+ jobject.destroy()
+ del jobject
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = Shell()
+ return _instance
+
diff --git a/src/jarabe/view/__init__.py b/src/jarabe/view/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/view/__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/jarabe/view/clipboardicon.py b/src/jarabe/view/clipboardicon.py
new file mode 100644
index 0000000..bd5d00e
--- /dev/null
+++ b/src/jarabe/view/clipboardicon.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2007, Red Hat, Inc.
+# 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 gtk
+
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.icon import Icon
+from sugar.graphics.xocolor import XoColor
+from sugar import profile
+
+from jarabe.model import clipboard
+from jarabe.view.clipboardmenu import ClipboardMenu
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.view.frame.notification import NotificationIcon
+import view.frame.frame
+
+class ClipboardIcon(RadioToolButton):
+ __gtype_name__ = 'SugarClipboardIcon'
+
+ def __init__(self, cb_object, group):
+ RadioToolButton.__init__(self, group=group)
+ self._cb_object = cb_object
+ self.owns_clipboard = False
+ self.props.sensitive = False
+ self.props.active = False
+ self._notif_icon = None
+ self._current_percent = None
+
+ self._icon = Icon()
+ self._icon.props.xo_color = profile.get_color()
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ cb_service = clipboard.get_instance()
+ cb_service.connect('object-state-changed',
+ self._object_state_changed_cb)
+
+ self.palette = ClipboardMenu(cb_object)
+ self.palette.props.invoker = FrameWidgetInvoker(self)
+
+ child = self.get_child()
+ child.connect('drag_data_get', self._drag_data_get_cb)
+ self.connect('notify::active', self._notify_active_cb)
+
+ def get_object_id(self):
+ return self._cb_object.get_id()
+
+ def _drag_data_get_cb(self, widget, context, selection, target_type,
+ event_time):
+ logging.debug('_drag_data_get_cb: requested target ' + selection.target)
+ data = self._cb_object.get_formats()[selection.target].get_data()
+ selection.set(selection.target, 8, data)
+
+ def _put_in_clipboard(self):
+ logging.debug('ClipboardIcon._put_in_clipboard')
+
+ if self._cb_object.get_percent() < 100:
+ raise ValueError('Object is not complete,' \
+ ' cannot be put into the clipboard.')
+
+ targets = self._get_targets()
+ if targets:
+ x_clipboard = gtk.Clipboard()
+ if not x_clipboard.set_with_data(targets,
+ self._clipboard_data_get_cb,
+ self._clipboard_clear_cb,
+ targets):
+ logging.error('GtkClipboard.set_with_data failed!')
+ else:
+ self.owns_clipboard = True
+
+ def _clipboard_data_get_cb(self, x_clipboard, selection, info, targets):
+ if not selection.target in [target[0] for target in targets]:
+ logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s' \
+ ' but only have %r.' % (selection.target, targets))
+ return
+ data = self._cb_object.get_formats()[selection.target].get_data()
+ selection.set(selection.target, 8, data)
+
+ def _clipboard_clear_cb(self, x_clipboard, targets):
+ logging.debug('ClipboardIcon._clipboard_clear_cb')
+ self.owns_clipboard = False
+
+ def _object_state_changed_cb(self, cb_service, cb_object):
+ if cb_object != self._cb_object:
+ return
+
+ if cb_object.get_icon():
+ self._icon.props.icon_name = cb_object.get_icon()
+ else:
+ self._icon.props.icon_name = 'application-octet-stream'
+
+ child = self.get_child()
+ child.drag_source_set(gtk.gdk.BUTTON1_MASK,
+ self._get_targets(),
+ gtk.gdk.ACTION_COPY)
+ child.drag_source_set_icon_name(self._icon.props.icon_name)
+
+ if cb_object.get_percent() == 100:
+ self.props.sensitive = True
+
+ # Clipboard object became complete. Make it the active one.
+ if self._current_percent < 100 and cb_object.get_percent() == 100:
+ self.props.active = True
+
+ self._notif_icon = NotificationIcon()
+ self._notif_icon.props.icon_name = self._icon.props.icon_name
+ self._notif_icon.props.xo_color = \
+ XoColor('%s,%s' % (self._icon.props.stroke_color,
+ self._icon.props.fill_color))
+ frame = view.frame.frame.get_instance()
+ frame.add_notification(self._notif_icon,
+ view.frame.frame.BOTTOM_LEFT)
+ self._current_percent = cb_object.get_percent()
+
+ def _notify_active_cb(self, widget, pspec):
+ if self.props.active:
+ self._put_in_clipboard()
+ else:
+ self.owns_clipboard = False
+
+ def _get_targets(self):
+ targets = []
+ for format_type in self._cb_object.get_formats().keys():
+ targets.append((format_type, 0, 0))
+ return targets
+
diff --git a/src/jarabe/view/clipboardmenu.py b/src/jarabe/view/clipboardmenu.py
new file mode 100644
index 0000000..cacdef0
--- /dev/null
+++ b/src/jarabe/view/clipboardmenu.py
@@ -0,0 +1,238 @@
+# 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
+
+from gettext import gettext as _
+import tempfile
+import urlparse
+import os
+import logging
+
+import gtk
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.datastore import datastore
+from sugar import mime
+from sugar import profile
+from sugar import activity
+
+from jarabe.model import clipboard
+import journal.misc
+
+class ClipboardMenu(Palette):
+
+ def __init__(self, cb_object):
+ Palette.__init__(self, cb_object.get_name())
+
+ self._cb_object = cb_object
+
+ self.set_group_id('frame')
+
+ cb_service = clipboard.get_instance()
+ cb_service.connect('object-state-changed',
+ self._object_state_changed_cb)
+
+ self._progress_bar = None
+
+ self._remove_item = MenuItem(_('Remove'), 'list-remove')
+ self._remove_item.connect('activate', self._remove_item_activate_cb)
+ self.menu.append(self._remove_item)
+ self._remove_item.show()
+
+ self._open_item = MenuItem(_('Open'), 'zoom-activity')
+ self._open_item.connect('activate', self._open_item_activate_cb)
+ self.menu.append(self._open_item)
+ self._open_item.show()
+
+ self._journal_item = MenuItem(_('Keep'))
+ icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU,
+ xo_color=profile.get_color())
+ self._journal_item.set_image(icon)
+
+ self._journal_item.connect('activate', self._journal_item_activate_cb)
+ self.menu.append(self._journal_item)
+ self._journal_item.show()
+
+ self._update_items_visibility()
+ self._update_open_submenu()
+
+ def _update_open_submenu(self):
+ activities = self._get_activities()
+ logging.debug('_update_open_submenu: %r' % activities)
+ child = self._open_item.get_child()
+ if activities is None or len(activities) <= 1:
+ child.set_text(_('Open'))
+ if self._open_item.get_submenu() is not None:
+ self._open_item.remove_submenu()
+ return
+
+ child.set_text(_('Open with'))
+ submenu = self._open_item.get_submenu()
+ if submenu is None:
+ submenu = gtk.Menu()
+ self._open_item.set_submenu(submenu)
+ submenu.show()
+ else:
+ for item in submenu.get_children():
+ submenu.remove(item)
+
+ for service_name in activities:
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(service_name)
+
+ if not activity_info:
+ logging.warning('Activity %s is unknown.' % service_name)
+
+ item = gtk.MenuItem(activity_info.name)
+ item.connect('activate', self._open_submenu_item_activate_cb,
+ service_name)
+ submenu.append(item)
+ item.show()
+
+ def _update_items_visibility(self):
+ activities = self._get_activities()
+ installable = self._cb_object.is_bundle()
+ percent = self._cb_object.get_percent()
+
+ if percent == 100 and (activities or installable):
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = True
+ self._journal_item.props.sensitive = True
+ elif percent == 100 and (not activities and not installable):
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = False
+ self._journal_item.props.sensitive = True
+ else:
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = False
+ self._journal_item.props.sensitive = False
+
+ self._update_progress_bar()
+
+ def _get_activities(self):
+ mime_type = self._cb_object.get_mime_type()
+ if not mime_type:
+ return ''
+
+ registry = activity.get_registry()
+ activities = registry.get_activities_for_type(mime_type)
+ if activities:
+ return [activity_info.bundle_id for activity_info in activities]
+ else:
+ return ''
+
+ def _update_progress_bar(self):
+ percent = self._cb_object.get_percent()
+ if percent == 100.0:
+ if self._progress_bar:
+ self._progress_bar = None
+ self.set_content(None)
+ else:
+ if self._progress_bar is None:
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.show()
+ self.set_content(self._progress_bar)
+
+ self._progress_bar.props.fraction = percent / 100.0
+ self._progress_bar.props.text = '%.2f %%' % percent
+
+ def _object_state_changed_cb(self, cb_service, cb_object):
+ if cb_object != self._cb_object:
+ return
+ self.set_primary_text(cb_object.get_name())
+ self._update_progress_bar()
+ self._update_items_visibility()
+ self._update_open_submenu()
+
+ def _open_item_activate_cb(self, menu_item):
+ logging.debug('_open_item_activate_cb')
+ percent = self._cb_object.get_percent()
+ if percent < 100 or menu_item.get_submenu() is not None:
+ return
+ jobject = self._copy_to_journal()
+ journal.misc.resume(jobject, self._activities[0])
+ jobject.destroy()
+
+ def _open_submenu_item_activate_cb(self, menu_item, service_name):
+ logging.debug('_open_submenu_item_activate_cb')
+ percent = self._cb_object.get_percent()
+ if percent < 100:
+ return
+ jobject = self._copy_to_journal()
+ journal.misc.resume(jobject, service_name)
+ jobject.destroy()
+
+ def _remove_item_activate_cb(self, menu_item):
+ cb_service = clipboard.get_instance()
+ cb_service.delete_object(self._cb_object.get_id())
+
+ def _journal_item_activate_cb(self, menu_item):
+ logging.debug('_journal_item_activate_cb')
+ jobject = self._copy_to_journal()
+ jobject.destroy()
+
+ def _write_to_temp_file(self, data):
+ f, file_path = tempfile.mkstemp()
+ try:
+ os.write(f, data)
+ finally:
+ os.close(f)
+ return file_path
+
+ def _copy_to_journal(self):
+ formats = self._cb_object.get_formats().keys()
+ most_significant_mime_type = mime.choose_most_significant(formats)
+ format = self._cb_object.get_formats()[most_significant_mime_type]
+
+ transfer_ownership = False
+ if most_significant_mime_type == 'text/uri-list':
+ uris = mime.split_uri_list(format.get_data())
+ if len(uris) == 1 and uris[0].startswith('file://'):
+ file_path = urlparse.urlparse(uris[0]).path
+ transfer_ownership = False
+ mime_type = mime.get_for_file(file_path)
+ else:
+ file_path = self._write_to_temp_file(format.get_data())
+ transfer_ownership = True
+ mime_type = 'text/uri-list'
+ else:
+ if format.is_on_disk():
+ file_path = urlparse.urlparse(format.get_data()).path
+ transfer_ownership = False
+ mime_type = mime.get_for_file(file_path)
+ else:
+ file_path = self._write_to_temp_file(format.get_data())
+ transfer_ownership = True
+ sniffed_mime_type = mime.get_for_file(file_path)
+ if sniffed_mime_type == 'application/octet-stream':
+ mime_type = most_significant_mime_type
+ else:
+ mime_type = sniffed_mime_type
+
+ name = self._cb_object.get_name()
+ jobject = datastore.create()
+ jobject.metadata['title'] = _('%s clipping') % name
+ jobject.metadata['keep'] = '0'
+ jobject.metadata['buddies'] = ''
+ jobject.metadata['preview'] = ''
+ jobject.metadata['icon-color'] = profile.get_color().to_string()
+ jobject.metadata['mime_type'] = mime_type
+ jobject.file_path = file_path
+ datastore.write(jobject, transfer_ownership=transfer_ownership)
+
+ return jobject
+
diff --git a/src/jarabe/view/devices/Makefile.am b/src/jarabe/view/devices/Makefile.am
new file mode 100644
index 0000000..dd71ce6
--- /dev/null
+++ b/src/jarabe/view/devices/Makefile.am
@@ -0,0 +1,9 @@
+SUBDIRS = network
+
+sugardir = $(pythondir)/jarabe/view/devices
+sugar_PYTHON = \
+ __init__.py \
+ battery.py \
+ deviceview.py \
+ speaker.py
+
diff --git a/src/jarabe/view/devices/__init__.py b/src/jarabe/view/devices/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/view/devices/__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/jarabe/view/devices/battery.py b/src/jarabe/view/devices/battery.py
new file mode 100644
index 0000000..90137ea
--- /dev/null
+++ b/src/jarabe/view/devices/battery.py
@@ -0,0 +1,130 @@
+# 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 import profile
+from sugar.graphics import style
+from sugar.graphics.icon import get_icon_state
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics.palette import Palette
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+
+_ICON_NAME = 'battery'
+
+_STATUS_CHARGING = 0
+_STATUS_DISCHARGING = 1
+_STATUS_FULLY_CHARGED = 2
+
+class DeviceView(TrayIcon):
+
+ FRAME_POSITION_RELATIVE = 1000
+
+ def __init__(self, model):
+ TrayIcon.__init__(self, icon_name=_ICON_NAME,
+ xo_color=profile.get_color())
+
+ self._model = model
+ self.palette = BatteryPalette(_('My Battery'))
+ self.set_palette(self.palette)
+ self.palette.props.invoker = FrameWidgetInvoker(self)
+ self.palette.set_group_id('frame')
+
+ model.connect('notify::level', self._battery_status_changed_cb)
+ model.connect('notify::charging', self._battery_status_changed_cb)
+ model.connect('notify::discharging', self._battery_status_changed_cb)
+ self._update_info()
+
+ def _update_info(self):
+ name = _ICON_NAME
+ current_level = self._model.props.level
+ xo_color = profile.get_color()
+ badge_name = None
+
+ if self._model.props.charging:
+ status = _STATUS_CHARGING
+ name += '-charging'
+ xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(),
+ style.COLOR_WHITE.get_svg()))
+ elif self._model.props.discharging:
+ status = _STATUS_DISCHARGING
+ if current_level <= 15:
+ badge_name = 'emblem-warning'
+ else:
+ status = _STATUS_FULLY_CHARGED
+
+ self.icon.props.icon_name = get_icon_state(name, current_level, step=-5)
+ self.icon.props.xo_color = xo_color
+ self.icon.props.badge_name = badge_name
+
+ self.palette.set_level(current_level)
+ self.palette.set_status(status)
+
+ def _battery_status_changed_cb(self, pspec, param):
+ self._update_info()
+
+class BatteryPalette(Palette):
+
+ def __init__(self, primary_text):
+ Palette.__init__(self, primary_text)
+
+ self._level = 0
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.set_size_request(
+ style.zoom(style.GRID_CELL_SIZE * 4), -1)
+ self._progress_bar.show()
+ self._status_label = gtk.Label()
+ self._status_label.show()
+
+ vbox = gtk.VBox()
+ vbox.pack_start(self._progress_bar)
+ vbox.pack_start(self._status_label)
+ vbox.show()
+
+ self.set_content(vbox)
+
+ def set_level(self, percent):
+ self._level = percent
+ fraction = percent / 100.0
+ self._progress_bar.set_fraction(fraction)
+
+ def set_status(self, status):
+ current_level = self._level
+ secondary_text = ''
+ status_text = '%s%%' % current_level
+
+ if status == _STATUS_CHARGING:
+ secondary_text = _('Charging')
+ elif status == _STATUS_DISCHARGING:
+ if current_level <= 15:
+ secondary_text = _('Very little power remaining')
+ else:
+ #TODO: make this less of an wild/educated guess
+ minutes_remaining = int(current_level / 0.59)
+ remaining_hourpart = minutes_remaining / 60
+ remaining_minpart = minutes_remaining % 60
+ secondary_text = _('%(hour)d:%(min).2d remaining'
+ % { 'hour': remaining_hourpart,
+ 'min': remaining_minpart})
+ else:
+ secondary_text = _('Charged')
+
+ self.props.secondary_text = secondary_text
+ self._status_label.set_text(status_text)
diff --git a/src/jarabe/view/devices/deviceview.py b/src/jarabe/view/devices/deviceview.py
new file mode 100644
index 0000000..90ebbf5
--- /dev/null
+++ b/src/jarabe/view/devices/deviceview.py
@@ -0,0 +1,26 @@
+# 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
+
+def create(model):
+ name = 'view.devices.' + model.get_type()
+
+ mod = __import__(name)
+ components = name.split('.')
+ for comp in components[1:]:
+ mod = getattr(mod, comp)
+
+ return mod.DeviceView(model)
+
diff --git a/src/jarabe/view/devices/network/Makefile.am b/src/jarabe/view/devices/network/Makefile.am
new file mode 100644
index 0000000..433bbfd
--- /dev/null
+++ b/src/jarabe/view/devices/network/Makefile.am
@@ -0,0 +1,5 @@
+sugardir = $(pythondir)/jarabe/view/devices/network
+sugar_PYTHON = \
+ __init__.py \
+ mesh.py \
+ wireless.py
diff --git a/src/jarabe/view/devices/network/__init__.py b/src/jarabe/view/devices/network/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/view/devices/network/__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/jarabe/view/devices/network/mesh.py b/src/jarabe/view/devices/network/mesh.py
new file mode 100644
index 0000000..0a945ea
--- /dev/null
+++ b/src/jarabe/view/devices/network/mesh.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2008 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
+
+from gettext import gettext as _
+
+import gtk
+
+from sugar import profile
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics import style
+from sugar.graphics.palette import Palette
+
+from jarabe.model.devices import device
+from jarabe.model.devices.network import wireless
+from jarabe.hardware import hardwaremanager
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+
+class DeviceView(TrayIcon):
+
+ FRAME_POSITION_RELATIVE = 400
+
+ def __init__(self, model):
+ TrayIcon.__init__(self, icon_name='network-mesh')
+
+ self._model = model
+
+ self.palette = MeshPalette(_("Mesh Network"), model)
+ self.set_palette(self.palette)
+ self.palette.props.invoker = FrameWidgetInvoker(self)
+ self.palette.set_group_id('frame')
+
+ model.connect('notify::state', self._state_changed_cb)
+ model.connect('notify::activation-stage', self._state_changed_cb)
+ self._update_state()
+
+ def _state_changed_cb(self, model, pspec):
+ self._update_state()
+
+ def _update_state(self):
+ # FIXME Change icon colors once we have real icons
+ state = self._model.props.state
+ self.palette.update_state(state)
+
+ if state == device.STATE_ACTIVATING:
+ self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg()
+ self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg()
+ elif state == device.STATE_ACTIVATED:
+ self.icon.props.xo_color = profile.get_color()
+ elif state == device.STATE_INACTIVE:
+ self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg()
+ self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg()
+
+ if state == device.STATE_INACTIVE:
+ self.palette.set_primary_text(_("Mesh Network"))
+ else:
+ chan = wireless.freq_to_channel(self._model.props.frequency)
+ if chan > 0:
+ self.palette.set_primary_text(_("Mesh Network") + " %d" % chan)
+ self.palette.set_mesh_step(self._model.props.mesh_step, state)
+
+class MeshPalette(Palette):
+ def __init__(self, primary_text, model):
+ Palette.__init__(self, primary_text, menu_after_content=True)
+ self._model = model
+
+ self._step_label = gtk.Label()
+ self._step_label.show()
+
+ vbox = gtk.VBox()
+ vbox.pack_start(self._step_label)
+ vbox.show()
+
+ self.set_content(vbox)
+
+ self._disconnect_item = gtk.MenuItem(_('Disconnect...'))
+ self._disconnect_item.connect('activate', self._disconnect_activate_cb)
+ self.menu.append(self._disconnect_item)
+
+ def update_state(self, state):
+ if state == device.STATE_ACTIVATED:
+ self._disconnect_item.show()
+ else:
+ self._disconnect_item.hide()
+
+ def _disconnect_activate_cb(self, menuitem):
+ # Disconnection for an mesh means activating the default mesh device
+ # again without a channel
+ network_manager = hardwaremanager.get_network_manager()
+ nm_device = self._model.get_nm_device()
+ if network_manager and nm_device:
+ network_manager.set_active_device(nm_device)
+
+ def set_mesh_step(self, step, state):
+ label = ""
+ if step == 1:
+ if state == device.STATE_ACTIVATED:
+ label = _("Connected to a School Mesh Portal")
+ elif state == device.STATE_ACTIVATING:
+ label = _("Looking for a School Mesh Portal...")
+ elif step == 3:
+ if state == device.STATE_ACTIVATED:
+ label = _("Connected to an XO Mesh Portal")
+ elif state == device.STATE_ACTIVATING:
+ label = _("Looking for an XO Mesh Portal...")
+ elif step == 4:
+ if state == device.STATE_ACTIVATED:
+ label = _("Connected to a Simple Mesh")
+ elif state == device.STATE_ACTIVATING:
+ label = _("Starting a Simple Mesh")
+
+ if len(label):
+ self._step_label.set_text(label)
+ else:
+ import logging
+ logging.debug("Unhandled mesh step %d" % step)
+ self._step_label.set_text(_("Unknown Mesh"))
+
diff --git a/src/jarabe/view/devices/network/wireless.py b/src/jarabe/view/devices/network/wireless.py
new file mode 100644
index 0000000..5db3ae2
--- /dev/null
+++ b/src/jarabe/view/devices/network/wireless.py
@@ -0,0 +1,144 @@
+#
+# 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.icon import get_icon_state
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics import style
+from sugar.graphics.palette import Palette
+
+from jarabe.model.devices.network import wireless
+from jarabe.model.devices import device
+from jarabe.hardware import hardwaremanager
+from jarabe.hardware import nmclient
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+
+_ICON_NAME = 'network-wireless'
+
+class DeviceView(TrayIcon):
+
+ FRAME_POSITION_RELATIVE = 300
+
+ def __init__(self, model):
+ TrayIcon.__init__(self, icon_name=_ICON_NAME)
+ self._model = model
+
+ meshdev = None
+ network_manager = hardwaremanager.get_network_manager()
+ for dev in network_manager.get_devices():
+ if dev.get_type() == nmclient.DEVICE_TYPE_802_11_MESH_OLPC:
+ meshdev = dev
+ break
+
+ self._counter = 0
+ self.palette = WirelessPalette(self._get_palette_primary_text(),
+ meshdev)
+ self.set_palette(self.palette)
+ self.palette.props.invoker = FrameWidgetInvoker(self)
+ self.palette.set_group_id('frame')
+ self.palette.set_frequency(self._model.props.frequency)
+
+ model.connect('notify::name', self._name_changed_cb)
+ model.connect('notify::strength', self._strength_changed_cb)
+ model.connect('notify::state', self._state_changed_cb)
+
+ self._update_icon()
+ self._update_state()
+
+ def _get_palette_primary_text(self):
+ if self._model.props.state == device.STATE_INACTIVE:
+ return _("Disconnected")
+ return self._model.props.name
+
+ def _strength_changed_cb(self, model, pspec):
+ self._update_icon()
+ # Only update frequency periodically
+ if self._counter % 4 == 0:
+ self.palette.set_frequency(self._model.props.frequency)
+ self._counter += 1
+
+ def _name_changed_cb(self, model, pspec):
+ self.palette.set_primary_text(self._get_palette_primary_text())
+
+ def _state_changed_cb(self, model, pspec):
+ self._update_icon()
+ self._update_state()
+ self.palette.set_primary_text(self._get_palette_primary_text())
+
+ def _update_icon(self):
+ # keep this code in sync with view/home/MeshBox.py
+ strength = self._model.props.strength
+ if self._model.props.state == device.STATE_INACTIVE:
+ strength = 0
+ if self._model.props.state == device.STATE_ACTIVATED:
+ icon_name = '%s-connected' % _ICON_NAME
+ else:
+ icon_name = _ICON_NAME
+ icon_name = get_icon_state(icon_name, strength)
+ if icon_name:
+ self.icon.props.icon_name = icon_name
+
+ def _update_state(self):
+ # FIXME Change icon colors once we have real icons
+ state = self._model.props.state
+ if state == device.STATE_ACTIVATING:
+ self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg()
+ self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg()
+ elif state == device.STATE_ACTIVATED:
+ (stroke, fill) = self._model.get_active_network_colors()
+ self.icon.props.stroke_color = stroke
+ self.icon.props.fill_color = fill
+ elif state == device.STATE_INACTIVE:
+ self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg()
+ self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg()
+
+class WirelessPalette(Palette):
+ def __init__(self, primary_text, meshdev):
+ Palette.__init__(self, primary_text, menu_after_content=True)
+ self._meshdev = meshdev
+
+ self._chan_label = gtk.Label()
+ self._chan_label.show()
+
+ vbox = gtk.VBox()
+ vbox.pack_start(self._chan_label)
+ vbox.show()
+
+ if meshdev:
+ disconnect_item = gtk.MenuItem(_('Disconnect...'))
+ disconnect_item.connect('activate', self._disconnect_activate_cb)
+ self.menu.append(disconnect_item)
+ disconnect_item.show()
+
+ self.set_content(vbox)
+
+ def _disconnect_activate_cb(self, menuitem):
+ # Disconnection for an AP means activating the default mesh device
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager and self._meshdev:
+ network_manager.set_active_device(self._meshdev)
+
+ def set_frequency(self, freq):
+ try:
+ chan = wireless.freq_to_channel(freq)
+ except KeyError:
+ chan = 0
+ self._chan_label.set_text("%s: %d" % (_("Channel"), chan))
+
diff --git a/src/jarabe/view/devices/speaker.py b/src/jarabe/view/devices/speaker.py
new file mode 100644
index 0000000..3c907e8
--- /dev/null
+++ b/src/jarabe/view/devices/speaker.py
@@ -0,0 +1,160 @@
+# Copyright (C) 2008 Martin Dengler
+#
+# 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 jarabe.hardware import hardwaremanager
+from sugar import profile
+from sugar.graphics import style
+from sugar.graphics.icon import get_icon_state, Icon
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics.palette import Palette
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+
+_ICON_NAME = 'speaker'
+
+class DeviceView(TrayIcon):
+
+ FRAME_POSITION_RELATIVE = 800
+
+ def __init__(self, model):
+ TrayIcon.__init__(self,
+ icon_name=_ICON_NAME,
+ xo_color=profile.get_color())
+
+ self._model = model
+ self.palette = SpeakerPalette(_('My Speakers'), model=model)
+ self.palette.props.invoker = FrameWidgetInvoker(self)
+ self.palette.set_group_id('frame')
+
+ model.connect('notify::level', self.__speaker_status_changed_cb)
+ model.connect('notify::muted', self.__speaker_status_changed_cb)
+ self.connect('expose-event', self.__expose_event_cb)
+
+ self._icon_widget.connect('button-press-event',
+ self.__update_mute_status)
+
+ self._update_info()
+
+ def _update_info(self):
+ name = _ICON_NAME
+ current_level = self._model.props.level
+ xo_color = profile.get_color()
+
+ if self._model.props.muted:
+ name += '-muted'
+ xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(),
+ style.COLOR_WHITE.get_svg()))
+
+ self.icon.props.icon_name = get_icon_state(name, current_level, step=-1)
+ self.icon.props.xo_color = xo_color
+
+ def __update_mute_status(self, *args):
+ self._model.props.muted = not self._model.props.muted
+
+ def __expose_event_cb(self, *args):
+ self._update_info()
+
+ def __speaker_status_changed_cb(self, pspec_, param_):
+ self._update_info()
+
+class SpeakerPalette(Palette):
+
+ def __init__(self, primary_text, model):
+ Palette.__init__(self, label=primary_text)
+
+ self._model = model
+
+ self.set_size_request(style.zoom(style.GRID_CELL_SIZE * 4), -1)
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ vol_step = hardwaremanager.VOL_CHANGE_INCREMENT_RECOMMENDATION
+ self._adjustment = gtk.Adjustment(value=self._model.props.level,
+ lower=0,
+ upper=100 + vol_step,
+ step_incr=vol_step,
+ page_incr=vol_step,
+ page_size=vol_step)
+ self._hscale = gtk.HScale(self._adjustment)
+ self._hscale.set_digits(0)
+ self._hscale.set_draw_value(False)
+ vbox.add(self._hscale)
+ self._hscale.show()
+
+ self._mute_item = MenuItem('')
+ self._mute_icon = Icon(icon_size=gtk.ICON_SIZE_MENU)
+ self._mute_item.set_image(self._mute_icon)
+ self.menu.append(self._mute_item)
+ self._mute_item.show()
+
+ self._adjustment_handler_id = \
+ self._adjustment.connect('value_changed',
+ self.__adjustment_changed_cb)
+
+ self._model_notify_level_handler_id = \
+ self._model.connect('notify::level', self.__level_changed_cb)
+ self._model.connect('notify::muted', self.__muted_changed_cb)
+
+ self._mute_item.connect('activate', self.__mute_activate_cb)
+
+ self.connect('popup', self.__popup_cb)
+
+ def _update_muted(self):
+ if self._model.props.muted:
+ mute_item_text = _('Unmute')
+ mute_item_icon_name = 'dialog-ok'
+ else:
+ mute_item_text = _('Mute')
+ mute_item_icon_name = 'dialog-cancel'
+ self._mute_item.get_child().set_text(mute_item_text)
+ self._mute_icon.props.icon_name = mute_item_icon_name
+
+ def _update_level(self):
+ if self._adjustment.value != self._model.props.level:
+ self._adjustment.handler_block(self._adjustment_handler_id)
+ try:
+ self._adjustment.value = self._model.props.level
+ finally:
+ self._adjustment.handler_unblock(self._adjustment_handler_id)
+
+ def __adjustment_changed_cb(self, adj_):
+ self._model.handler_block(self._model_notify_level_handler_id)
+ try:
+ self._model.props.level = self._adjustment.value
+ finally:
+ self._model.handler_unblock(self._model_notify_level_handler_id)
+ self._model.props.muted = self._adjustment.value == 0
+
+ def __level_changed_cb(self, pspec_, param_):
+ self._update_level()
+
+ def __mute_activate_cb(self, menuitem_):
+ self._model.props.muted = not self._model.props.muted
+
+ def __muted_changed_cb(self, pspec_, param_):
+ self._update_muted()
+
+ def __popup_cb(self, palette_):
+ self._update_level()
+ self._update_muted()
diff --git a/src/jarabe/view/frame/Makefile.am b/src/jarabe/view/frame/Makefile.am
new file mode 100644
index 0000000..4e6e237
--- /dev/null
+++ b/src/jarabe/view/frame/Makefile.am
@@ -0,0 +1,14 @@
+sugardir = $(pythondir)/jarabe/view/frame
+sugar_PYTHON = \
+ __init__.py \
+ activitiestray.py \
+ clipboardpanelwindow.py \
+ clipboardtray.py \
+ devicestray.py \
+ frameinvoker.py \
+ friendstray.py \
+ eventarea.py \
+ frame.py \
+ notification.py \
+ framewindow.py \
+ zoomtoolbar.py
diff --git a/src/jarabe/view/frame/__init__.py b/src/jarabe/view/frame/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/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/jarabe/view/frame/activitiestray.py b/src/jarabe/view/frame/activitiestray.py
new file mode 100644
index 0000000..735fa96
--- /dev/null
+++ b/src/jarabe/view/frame/activitiestray.py
@@ -0,0 +1,395 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2008 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
+from gettext import gettext as _
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics.tray import HTray
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.icon import Icon
+from sugar.graphics.palette import Palette, WidgetInvoker
+from sugar.graphics.menuitem import MenuItem
+from sugar import activity
+from sugar import profile
+
+from jarabe.model import shellmodel
+from jarabe.view.palettes import JournalPalette, CurrentActivityPalette
+from jarabe.view.pulsingicon import PulsingIcon
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.view.frame.notification import NotificationIcon
+import view.frame.frame
+
+class ActivityButton(RadioToolButton):
+ def __init__(self, home_activity, group):
+ RadioToolButton.__init__(self, group=group)
+
+ self._home_activity = home_activity
+
+ self._icon = PulsingIcon()
+ self._icon.props.base_color = home_activity.get_icon_color()
+ self._icon.props.pulse_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TOOLBAR_GREY.get_svg()))
+ if home_activity.get_icon_path():
+ self._icon.props.file = home_activity.get_icon_path()
+ else:
+ self._icon.props.icon_name = 'image-missing'
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ if home_activity.props.launching:
+ self._icon.props.pulsing = True
+ self._notify_launching_hid = home_activity.connect( \
+ 'notify::launching', self.__notify_launching_cb)
+ else:
+ self._notify_launching_hid = None
+ self._notif_icon = None
+
+ def create_palette(self):
+ if self._home_activity.is_journal():
+ palette = JournalPalette(self._home_activity)
+ else:
+ palette = CurrentActivityPalette(self._home_activity)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ self.set_palette(palette)
+
+ def __notify_launching_cb(self, home_activity, pspec):
+ if not home_activity.props.launching:
+ self._icon.props.pulsing = False
+ home_activity.disconnect(self._notify_launching_hid)
+
+
+class BaseInviteButton(ToolButton):
+ def __init__(self, invite):
+ ToolButton.__init__(self)
+ self._invite = invite
+ self._icon = Icon()
+ self.connect('clicked', self.__clicked_cb)
+ self.connect('destroy', self.__destroy_cb)
+ self._notif_icon = NotificationIcon()
+ self._notif_icon.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ def __button_release_event_cb(self, icon, event):
+ self.emit('clicked')
+
+ def __clicked_cb(self, button):
+ if self._notif_icon is not None:
+ frame = view.frame.frame.get_instance()
+ frame.remove_notification(self._notif_icon)
+ self._notif_icon = None
+ self._launch()
+
+ def _launch(self):
+ """Launch the target of the invite"""
+ raise NotImplementedError
+
+ def __destroy_cb(self, button):
+ frame = view.frame.frame.get_instance()
+ frame.remove_notification(self._notif_icon)
+
+class ActivityInviteButton(BaseInviteButton):
+ """Invite to shared activity"""
+ def __init__(self, invite):
+ BaseInviteButton.__init__(self, invite)
+ mesh = shellmodel.get_instance().get_mesh()
+ activity_model = mesh.get_activity(invite.get_activity_id())
+ self._activity_model = activity_model
+ self._bundle_id = activity_model.get_bundle_id()
+
+ self._icon.props.xo_color = activity_model.get_color()
+ if activity_model.get_icon_name():
+ self._icon.props.file = activity_model.get_icon_name()
+ else:
+ self._icon.props.icon_name = 'image-missing'
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ palette = ActivityInvitePalette(invite)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ self.set_palette(palette)
+
+ self._notif_icon.props.xo_color = activity_model.get_color()
+ if activity_model.get_icon_name():
+ icon_name = activity_model.get_icon_name()
+ self._notif_icon.props.icon_filename = icon_name
+ else:
+ self._notif_icon.props.icon_name = 'image-missing'
+
+ palette = ActivityInvitePalette(invite)
+ palette.props.invoker = WidgetInvoker(self._notif_icon)
+ palette.set_group_id('frame')
+ self._notif_icon.palette = palette
+
+ frame = view.frame.frame.get_instance()
+ frame.add_notification(self._notif_icon,
+ view.frame.frame.TOP_LEFT)
+
+ def _launch(self):
+ """Join the activity in the invite."""
+ shell = view.Shell.get_instance()
+ shell.join_activity(self._activity_model.get_bundle_id(),
+ self._activity_model.get_id())
+
+
+class PrivateInviteButton(BaseInviteButton):
+ """Invite to a private one to one channel"""
+ def __init__(self, invite):
+ BaseInviteButton.__init__(self, invite)
+ self._private_channel = invite.get_private_channel()
+ self._bundle_id = invite.get_bundle_id()
+
+ self._icon.props.xo_color = profile.get_color()
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(self._bundle_id)
+ if activity_info:
+ self._icon.props.file = activity_info.icon
+ else:
+ self._icon.props.icon_name = 'image-missing'
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ palette = PrivateInvitePalette(invite)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ self.set_palette(palette)
+
+ self._notif_icon.props.xo_color = profile.get_color()
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(self._bundle_id)
+ if activity_info:
+ self._notif_icon.props.icon_filename = activity_info.icon
+ else:
+ self._notif_icon.props.icon_name = 'image-missing'
+
+ palette = PrivateInvitePalette(invite)
+ palette.props.invoker = WidgetInvoker(self._notif_icon)
+ palette.set_group_id('frame')
+ self._notif_icon.palette = palette
+
+ frame = view.frame.frame.get_instance()
+ frame.add_notification(self._notif_icon,
+ view.frame.frame.TOP_LEFT)
+
+ def _launch(self):
+ """Start the activity with private channel."""
+ shell = view.Shell.get_instance()
+ shell.start_activity_with_uri(self._bundle_id,
+ self._private_channel)
+
+
+class BaseInvitePalette(Palette):
+ """Palette for frame or notification icon for invites."""
+ def __init__(self):
+ Palette.__init__(self, '')
+
+ menu_item = MenuItem(_('Join'), icon_name='dialog-ok')
+ menu_item.connect('activate', self.__join_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__decline_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __join_activate_cb(self, menu_item):
+ self._join()
+
+ def __decline_activate_cb(self, menu_item):
+ self._decline()
+
+ def _join(self):
+ raise NotImplementedError
+
+ def _decline(self):
+ raise NotImplementedError
+
+
+class ActivityInvitePalette(BaseInvitePalette):
+ """Palette for shared activity invites."""
+
+ def __init__(self, invite):
+ BaseInvitePalette.__init__(self)
+
+ mesh = shellmodel.get_instance().get_mesh()
+ activity_model = mesh.get_activity(invite.get_activity_id())
+ self._activity_model = activity_model
+ self._bundle_id = activity_model.get_bundle_id()
+
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(self._bundle_id)
+ if activity_info:
+ self.set_primary_text(activity_info.name)
+ else:
+ self.set_primary_text(self._bundle_id)
+
+ def _join(self):
+ shell = view.Shell.get_instance()
+ shell.join_activity(self._activity_model.get_bundle_id(),
+ self._activity_model.get_id())
+
+ def _decline(self):
+ invites = shellmodel.get_instance().get_invites()
+ activity_id = self._activity_model.get_id()
+ invites.remove_activity(activity_id)
+
+
+class PrivateInvitePalette(BaseInvitePalette):
+ """Palette for private channel invites."""
+
+ def __init__(self, invite):
+ BaseInvitePalette.__init__(self)
+
+ self._private_channel = invite.get_private_channel()
+ self._bundle_id = invite.get_bundle_id()
+
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(self._bundle_id)
+ if activity_info:
+ self.set_primary_text(activity_info.name)
+ else:
+ self.set_primary_text(self._bundle_id)
+
+ def _join(self):
+ shell = view.Shell.get_instance()
+ shell.start_activity_with_uri(self._bundle_id,
+ self._private_channel)
+ invites = shellmodel.get_instance().get_invites()
+ invites.remove_private_channel(self._private_channel)
+
+ def _decline(self):
+ invites = shellmodel.get_instance().get_invites()
+ invites.remove_private_channel(self._private_channel)
+
+
+class ActivitiesTray(HTray):
+ def __init__(self):
+ HTray.__init__(self)
+
+ self._buttons = {}
+ self._invite_to_item = {}
+ self._freeze_button_clicks = False
+
+ self._home_model = shellmodel.get_instance().get_home()
+ self._home_model.connect('activity-added', self.__activity_added_cb)
+ self._home_model.connect('activity-removed', self.__activity_removed_cb)
+ self._home_model.connect('active-activity-changed',
+ self.__activity_changed_cb)
+ self._home_model.connect('tabbing-activity-changed',
+ self.__tabbing_activity_changed_cb)
+
+ self._invites = shellmodel.get_instance().get_invites()
+ 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 __activity_added_cb(self, home_model, home_activity):
+ logging.debug('__activity_added_cb: %r' % home_activity)
+ if self.get_children():
+ group = self.get_children()[0]
+ else:
+ group = None
+
+ button = ActivityButton(home_activity, group)
+ self.add_item(button)
+ self._buttons[home_activity.get_activity_id()] = button
+ button.connect('clicked', self.__activity_clicked_cb, home_activity)
+ button.show()
+
+ def __activity_removed_cb(self, home_model, home_activity):
+ logging.debug('__activity_removed_cb: %r' % home_activity)
+ button = self._buttons[home_activity.get_activity_id()]
+ self.remove_item(button)
+ del self._buttons[home_activity.get_activity_id()]
+
+ def _activate_activity(self, home_activity):
+ button = self._buttons[home_activity.get_activity_id()]
+ self._freeze_button_clicks = True
+ button.props.active = True
+ self._freeze_button_clicks = False
+
+ self.scroll_to_item(button)
+ # Redraw immediately.
+ # The widget may not be realized yet, and then there is no window.
+ if self.window:
+ self.window.process_updates(True)
+
+ def __activity_changed_cb(self, home_model, home_activity):
+ logging.debug('__activity_changed_cb: %r' % home_activity)
+
+ # Only select the new activity, if there is no tabbing activity.
+ if home_model.get_tabbing_activity() is None:
+ self._activate_activity(home_activity)
+
+ def __tabbing_activity_changed_cb(self, home_model, home_activity):
+ logging.debug('__tabbing_activity_changed_cb: %r' % home_activity)
+ # If the tabbing_activity is set to None just do nothing.
+ # The active activity will be updated a bit later (and it will
+ # be set to the activity that is currently selected).
+ if home_activity is None:
+ return
+
+ self._activate_activity(home_activity)
+
+ def __activity_clicked_cb(self, button, home_activity):
+ if not self._freeze_button_clicks and button.props.active:
+ logging.debug('ActivitiesTray.__activity_clicked_cb')
+ window = home_activity.get_window()
+ if window:
+ window.activate(gtk.get_current_event_time())
+
+ def __invite_clicked_cb(self, icon, invite):
+ if hasattr(invite, 'get_activity_id'):
+ self._invites.remove_invite(invite)
+ else:
+ self._invites.remove_private_invite(invite)
+
+ def __invite_added_cb(self, invites, invite):
+ self._add_invite(invite)
+
+ def __invite_removed_cb(self, invites, invite):
+ self._remove_invite(invite)
+
+ def _add_invite(self, invite):
+ """Add an invite (SugarInvite or PrivateInvite)"""
+ item = None
+ if hasattr(invite, 'get_activity_id'):
+ mesh = shellmodel.get_instance().get_mesh()
+ activity_model = mesh.get_activity(invite.get_activity_id())
+ if activity_model is not None:
+ item = ActivityInviteButton(invite)
+ else:
+ item = PrivateInviteButton(invite)
+ if item is not None:
+ item.connect('clicked', self.__invite_clicked_cb, invite)
+ self.add_item(item)
+ item.show()
+ self._invite_to_item[invite] = item
+
+ def _remove_invite(self, invite):
+ self.remove_item(self._invite_to_item[invite])
+ self._invite_to_item[invite].destroy()
+ del self._invite_to_item[invite]
+
diff --git a/src/jarabe/view/frame/clipboardpanelwindow.py b/src/jarabe/view/frame/clipboardpanelwindow.py
new file mode 100644
index 0000000..bd1959e
--- /dev/null
+++ b/src/jarabe/view/frame/clipboardpanelwindow.py
@@ -0,0 +1,102 @@
+# 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
+from urlparse import urlparse
+
+import gtk
+import hippo
+
+from jarabe.view.frame.framewindow import FrameWindow
+from jarabe.view.frame.clipboardtray import ClipboardTray
+
+from jarabe.model import clipboard
+
+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_tray = ClipboardTray()
+ canvas_widget = hippo.CanvasWidget(widget=self._clipboard_tray)
+ self.append(canvas_widget, hippo.PACK_EXPAND)
+
+ # Receiving dnd drops
+ self.drag_dest_set(0, [], 0)
+ self.connect("drag_motion", self._clipboard_tray.drag_motion_cb)
+ self.connect("drag_drop", self._clipboard_tray.drag_drop_cb)
+ self.connect("drag_data_received",
+ self._clipboard_tray.drag_data_received_cb)
+
+ def _owner_change_cb(self, x_clipboard, event):
+ logging.debug("owner_change_cb")
+
+ if self._clipboard_tray.owns_clipboard():
+ return
+
+ cb_service = clipboard.get_instance()
+ key = cb_service.add_object(name="")
+ cb_service.set_object_percent(key, percent=0)
+
+ targets = x_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 = x_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 = clipboard.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(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/jarabe/view/frame/clipboardtray.py b/src/jarabe/view/frame/clipboardtray.py
new file mode 100644
index 0000000..ca67b82
--- /dev/null
+++ b/src/jarabe/view/frame/clipboardtray.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 gtk
+
+from sugar import util
+from sugar.graphics import tray
+from sugar.graphics import style
+
+from jarabe.model import clipboard
+from jarabe.view.clipboardicon import ClipboardIcon
+
+class _ContextMap(object):
+ """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 ClipboardTray(tray.VTray):
+
+ MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2
+
+ def __init__(self):
+ tray.VTray.__init__(self, align=tray.ALIGN_TO_END)
+ self._icons = {}
+ self._context_map = _ContextMap()
+
+ cb_service = clipboard.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('ClipboardTray: adding type %r' % selection.type)
+
+ cb_service = clipboard.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, cb_object):
+ if self._icons:
+ group = self._icons.values()[0]
+ else:
+ group = None
+
+ icon = ClipboardIcon(cb_object, group)
+ self.add_item(icon)
+ icon.show()
+ self._icons[cb_object.get_id()] = icon
+
+ objects_to_delete = self.get_children()[:-self.MAX_ITEMS]
+ for icon in objects_to_delete:
+ logging.debug('ClipboardTray: deleting surplus object')
+ cb_service = clipboard.get_instance()
+ cb_service.delete_object(icon.get_object_id())
+
+ logging.debug('ClipboardTray: %r was added' % cb_object.get_id())
+
+ def _object_deleted_cb(self, cb_service, object_id):
+ icon = self._icons[object_id]
+ self.remove_item(icon)
+ del self._icons[object_id]
+ logging.debug('ClipboardTray: %r was deleted' % object_id)
+
+ def drag_motion_cb(self, widget, context, x, y, time):
+ logging.debug('ClipboardTray._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('ClipboardTray._drag_drop_cb')
+ cb_service = clipboard.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('ClipboardTray: got data for target %r'
+ % selection.target)
+
+ object_id = self._context_map.get_object_id(context)
+ try:
+ if selection is None:
+ logging.warn('ClipboardTray: empty selection for target %s'
+ % selection.target)
+ elif selection.target == 'XdndDirectSave0':
+ if selection.data == 'S':
+ window = context.source_window
+
+ prop_type, format_, dest = \
+ window.property_get('XdndDirectSave0', 'text/plain')
+
+ clipboardservice = clipboard.get_instance()
+ clipboardservice.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/jarabe/view/frame/devicestray.py b/src/jarabe/view/frame/devicestray.py
new file mode 100644
index 0000000..eb6707e
--- /dev/null
+++ b/src/jarabe/view/frame/devicestray.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2008 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
+
+from sugar.graphics import tray
+
+from jarabe.view.devices import deviceview
+from jarabe.model import shellmodel
+
+_logger = logging.getLogger('DevicesTray')
+
+class DevicesTray(tray.HTray):
+ def __init__(self):
+ tray.HTray.__init__(self, align=tray.ALIGN_TO_END)
+ self._device_icons = {}
+
+ devices_model = shellmodel.get_instance().get_devices()
+
+ for device in devices_model:
+ self._add_device(device)
+
+ devices_model.connect('device-appeared',
+ self.__device_appeared_cb)
+ devices_model.connect('device-disappeared',
+ self.__device_disappeared_cb)
+
+ def _add_device(self, device):
+ try:
+ view = deviceview.create(device)
+ index = 0
+ for item in self.get_children():
+ index = self.get_item_index(item)
+ view_pos = getattr(view, "FRAME_POSITION_RELATIVE", -1)
+ item_pos = getattr(item, "FRAME_POSITION_RELATIVE", 0)
+ if view_pos < item_pos:
+ break
+ self.add_item(view, index=index)
+ view.show()
+ self._device_icons[device.get_id()] = view
+ except Exception, message:
+ _logger.warn("Not able to add icon for device [%r], because of "
+ "an error (%s). Continuing." % (device, message))
+
+ def _remove_device(self, device):
+ self.remove_item(self._device_icons[device.get_id()])
+ del self._device_icons[device.get_id()]
+
+ def __device_appeared_cb(self, model, device):
+ self._add_device(device)
+
+ def __device_disappeared_cb(self, model, device):
+ self._remove_device(device)
diff --git a/src/jarabe/view/frame/eventarea.py b/src/jarabe/view/frame/eventarea.py
new file mode 100644
index 0000000..0eba150
--- /dev/null
+++ b/src/jarabe/view/frame/eventarea.py
@@ -0,0 +1,148 @@
+# 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
+
+from sugar import profile
+
+_MAX_DELAY = 1000
+
+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
+ self._sids = {}
+ pro = profile.get_profile()
+ self._hot_delay = int(pro.hot_corners_delay)
+ self._warm_delay = int(pro.warm_edges_delay)
+
+ right = gtk.gdk.screen_width() - 1
+ bottom = gtk.gdk.screen_height() -1
+ width = gtk.gdk.screen_width() - 2
+ height = gtk.gdk.screen_height() - 2
+
+ if self._warm_delay != _MAX_DELAY:
+ invisible = self._create_invisible(1, 0, width, 1, self._warm_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(1, bottom, width, 1,
+ self._warm_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(0, 1, 1, height,
+ self._warm_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, 1, 1, height,
+ self._warm_delay)
+ self._windows.append(invisible)
+
+ if self._hot_delay != _MAX_DELAY:
+ invisible = self._create_invisible(0, 0, 1, 1, self._hot_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, 0, 1, 1, self._hot_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(0, bottom, 1, 1, self._hot_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, bottom, 1, 1,
+ self._hot_delay)
+ 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, delay):
+ invisible = gtk.Invisible()
+ if delay >= 0:
+ invisible.connect('enter-notify-event', self._enter_notify_cb,
+ delay)
+ 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, delay):
+ if widget in self._sids:
+ gobject.source_remove(self._sids[widget])
+ self._sids[widget] = gobject.timeout_add(delay,
+ self.__delay_cb,
+ widget)
+
+ def __delay_cb(self, widget):
+ del self._sids[widget]
+ self._notify_enter()
+ return False
+
+ def _leave_notify_cb(self, widget, event):
+ if widget in self._sids:
+ gobject.source_remove(self._sids[widget])
+ del self._sids[widget]
+ 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/jarabe/view/frame/frame.py b/src/jarabe/view/frame/frame.py
new file mode 100644
index 0000000..10dcf26
--- /dev/null
+++ b/src/jarabe/view/frame/frame.py
@@ -0,0 +1,327 @@
+# 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 jarabe.view.frame.eventarea import EventArea
+from jarabe.view.frame.activitiestray import ActivitiesTray
+from jarabe.view.frame.zoomtoolbar import ZoomToolbar
+from jarabe.view.frame.friendstray import FriendsTray
+from jarabe.view.frame.devicestray import DevicesTray
+from jarabe.view.frame.framewindow import FrameWindow
+from jarabe.view.frame.clipboardpanelwindow import ClipboardPanelWindow
+from jarabe.view.frame.notification import NotificationIcon, NotificationWindow
+
+TOP_RIGHT = 0
+TOP_LEFT = 1
+BOTTOM_RIGHT = 2
+BOTTOM_LEFT = 3
+
+_FRAME_HIDING_DELAY = 500
+_NOTIFICATION_DURATION = 5000
+
+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):
+ 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.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)
+
+ self._key_listener = _KeyListener(self)
+ self._mouse_listener = _MouseListener(self)
+
+ self._notif_by_icon = {}
+
+ 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.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)
+
+ # TODO: setting box_width and hippo.PACK_EXPAND looks like a hack to me.
+ # Why hippo isn't respecting the request size of these controls?
+
+ zoom_toolbar = ZoomToolbar()
+ panel.append(hippo.CanvasWidget(widget=zoom_toolbar,
+ box_width=4*style.GRID_CELL_SIZE))
+ zoom_toolbar.show()
+
+ activities_tray = ActivitiesTray()
+ panel.append(hippo.CanvasWidget(widget=activities_tray),
+ hippo.PACK_EXPAND)
+ activities_tray.show()
+
+ return panel
+
+ def _create_bottom_panel(self):
+ panel = self._create_panel(gtk.POS_BOTTOM)
+
+ # TODO: same issue as in _create_top_panel()
+ devices_tray = DevicesTray()
+ panel.append(hippo.CanvasWidget(widget=devices_tray), hippo.PACK_EXPAND)
+ devices_tray.show()
+
+ return panel
+
+ def _create_right_panel(self):
+ panel = self._create_panel(gtk.POS_RIGHT)
+
+ tray = FriendsTray()
+ 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 _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()
+
+ def add_notification(self, icon, corner=TOP_LEFT):
+ if not isinstance(icon, NotificationIcon):
+ raise TypeError('icon must be a NotificationIcon.')
+
+ window = NotificationWindow()
+
+ screen = gtk.gdk.screen_get_default()
+ if corner == TOP_LEFT:
+ window.move(0, 0)
+ elif corner == TOP_RIGHT:
+ window.move(screen.get_width() - style.GRID_CELL_SIZE, 0)
+ elif corner == BOTTOM_LEFT:
+ window.move(0, screen.get_height() - style.GRID_CELL_SIZE)
+ elif corner == BOTTOM_RIGHT:
+ window.move(screen.get_width() - style.GRID_CELL_SIZE,
+ screen.get_height() - style.GRID_CELL_SIZE)
+ else:
+ raise ValueError('Inalid corner: %r' % corner)
+
+ window.add(icon)
+ icon.show()
+ window.show()
+
+ self._notif_by_icon[icon] = window
+
+ gobject.timeout_add(_NOTIFICATION_DURATION,
+ lambda: self.remove_notification(icon))
+
+ def remove_notification(self, icon):
+ if not isinstance(icon, NotificationIcon):
+ raise TypeError('icon must be a NotificationIcon.')
+
+ if icon not in self._notif_by_icon:
+ logging.debug('icon %r not in list of notifications.' % icon)
+ return
+
+ window = self._notif_by_icon[icon]
+ window.destroy()
+ del self._notif_by_icon[icon]
+
+ visible = property(is_visible, None)
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = Frame()
+ return _instance
+
diff --git a/src/jarabe/view/frame/frameinvoker.py b/src/jarabe/view/frame/frameinvoker.py
new file mode 100644
index 0000000..e4a13e1
--- /dev/null
+++ b/src/jarabe/view/frame/frameinvoker.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com>
+#
+# 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
+
+from sugar.graphics import style
+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, widget.child)
+
+ self._position_hint = self.ANCHORED
+ self._screen_area = _get_screen_area()
diff --git a/src/jarabe/view/frame/framewindow.py b/src/jarabe/view/frame/framewindow.py
new file mode 100644
index 0000000..623d162
--- /dev/null
+++ b/src/jarabe/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/jarabe/view/frame/friendstray.py b/src/jarabe/view/frame/friendstray.py
new file mode 100644
index 0000000..93d023e
--- /dev/null
+++ b/src/jarabe/view/frame/friendstray.py
@@ -0,0 +1,144 @@
+# 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 sugar.presence import presenceservice
+from sugar.graphics.tray import VTray, TrayIcon
+
+import view.Shell
+from jarabe.view.BuddyMenu import BuddyMenu
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.model import shellmodel
+from jarabe.model.BuddyModel import BuddyModel
+
+class FriendIcon(TrayIcon):
+ def __init__(self, buddy):
+ TrayIcon.__init__(self, icon_name='computer-xo',
+ xo_color=buddy.get_color())
+
+ palette = BuddyMenu(buddy)
+ palette.props.icon_visible = False
+ self.set_palette(palette)
+ palette.set_group_id('frame')
+ palette.props.invoker = FrameWidgetInvoker(self)
+
+class FriendsTray(VTray):
+ def __init__(self):
+ VTray.__init__(self)
+
+ 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 = shellmodel.get_instance().get_home()
+ home_model.connect('active-activity-changed',
+ self._active_activity_changed_cb)
+
+ def _get_activities_cb(self, activities_list):
+ for act in activities_list:
+ self.__activity_appeared_cb(self._pservice, act)
+
+ def add_buddy(self, buddy):
+ if self._buddies.has_key(buddy.props.key):
+ return
+
+ model = BuddyModel(buddy=buddy)
+
+ icon = FriendIcon(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)
+ item.destroy()
+ self._buddies = {}
+
+ def __activity_appeared_cb(self, pservice, activity_ps):
+ activity = view.Shell.get_instance().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()
+
+ # always display ourselves
+ self.add_buddy(self._owner)
+
+ 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)
+
+ def _active_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/jarabe/view/frame/notification.py b/src/jarabe/view/frame/notification.py
new file mode 100644
index 0000000..68107ba
--- /dev/null
+++ b/src/jarabe/view/frame/notification.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2008 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 gobject
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.view.pulsingicon import PulsingIcon
+
+class NotificationIcon(gtk.EventBox):
+ __gtype_name__ = 'SugarNotificationIcon'
+
+ __gproperties__ = {
+ 'xo-color' : (object, None, None, gobject.PARAM_READWRITE),
+ 'icon-name' : (str, None, None, None, gobject.PARAM_READWRITE),
+ 'icon-filename' : (str, None, None, None, gobject.PARAM_READWRITE)
+ }
+
+ _PULSE_TIMEOUT = 3000
+
+ def __init__(self, **kwargs):
+ self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE)
+ gobject.GObject.__init__(self, **kwargs)
+ self.props.visible_window = False
+
+ self._icon.props.pulse_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ self._icon.props.pulsing = True
+ self.add(self._icon)
+ self._icon.show()
+
+ gobject.timeout_add(self._PULSE_TIMEOUT, self.__stop_pulsing_cb)
+
+ self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
+
+ def __stop_pulsing_cb(self):
+ self._icon.props.pulsing = False
+ return False
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'xo-color':
+ if self._icon.props.base_color != value:
+ self._icon.props.base_color = value
+ elif pspec.name == 'icon-name':
+ if self._icon.props.icon_name != value:
+ self._icon.props.icon_name = value
+ elif pspec.name == 'icon-filename':
+ if self._icon.props.file != value:
+ self._icon.props.file = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'xo-color':
+ return self._icon.props.base_color
+ elif pspec.name == 'icon-name':
+ return self._icon.props.icon_name
+ elif pspec.name == 'icon-filename':
+ return self._icon.props.file
+
+ def _set_palette(self, palette):
+ self._icon.palette = palette
+
+ def _get_palette(self):
+ return self._icon.palette
+
+ palette = property(_get_palette, _set_palette)
+
+class NotificationWindow(gtk.Window):
+ __gtype_name__ = 'SugarNotificationWindow'
+
+ def __init__(self, **kwargs):
+
+ gtk.Window.__init__(self, **kwargs)
+
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.connect('realize', self._realize_cb)
+
+ def _realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(False)
+
+ color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html())
+ self.modify_bg(gtk.STATE_NORMAL, color)
+
diff --git a/src/jarabe/view/frame/zoomtoolbar.py b/src/jarabe/view/frame/zoomtoolbar.py
new file mode 100644
index 0000000..8829790
--- /dev/null
+++ b/src/jarabe/view/frame/zoomtoolbar.py
@@ -0,0 +1,88 @@
+# 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 logging
+
+import gtk
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.radiotoolbutton import RadioToolButton
+
+import view.Shell
+from jarabe.view.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.model import shellmodel
+
+class ZoomToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ # we shouldn't be mirrored in RTL locales
+ self.set_direction(gtk.TEXT_DIR_LTR)
+
+ self._mesh_button = self._add_button('zoom-neighborhood',
+ _('Neighborhood'), shellmodel.ShellModel.ZOOM_MESH)
+ self._groups_button = self._add_button('zoom-groups',
+ _('Group'), shellmodel.ShellModel.ZOOM_FRIENDS)
+ self._home_button = self._add_button('zoom-home',
+ _('Home'), shellmodel.ShellModel.ZOOM_HOME)
+ self._activity_button = self._add_button('zoom-activity',
+ _('Activity'), shellmodel.ShellModel.ZOOM_ACTIVITY)
+
+ shell_model = shellmodel.get_instance()
+ self._set_zoom_level(shell_model.props.zoom_level)
+ shell_model.connect('notify::zoom-level', self.__notify_zoom_level_cb)
+
+ def _add_button(self, icon_name, label, zoom_level):
+ if self.get_children():
+ group = self.get_children()[0]
+ else:
+ group = None
+
+ button = RadioToolButton(named_icon=icon_name, group=group)
+ button.connect('clicked', self.__level_clicked_cb, zoom_level)
+ self.add(button)
+ button.show()
+
+ palette = Palette(label)
+ palette.props.invoker = FrameWidgetInvoker(button)
+ palette.set_group_id('frame')
+ button.set_palette(palette)
+
+ return button
+
+ def __level_clicked_cb(self, button, level):
+ if not button.get_active():
+ return
+ if shellmodel.get_instance().props.zoom_level != level:
+ view.Shell.get_instance().set_zoom_level(level)
+
+ def __notify_zoom_level_cb(self, model, pspec):
+ self._set_zoom_level(model.props.zoom_level)
+
+ def _set_zoom_level(self, new_level):
+ logging.debug('new zoom level: %r' % new_level)
+ if new_level == shellmodel.ShellModel.ZOOM_MESH:
+ self._mesh_button.props.active = True
+ elif new_level == shellmodel.ShellModel.ZOOM_FRIENDS:
+ self._groups_button.props.active = True
+ elif new_level == shellmodel.ShellModel.ZOOM_HOME:
+ self._home_button.props.active = True
+ elif new_level == shellmodel.ShellModel.ZOOM_ACTIVITY:
+ self._activity_button.props.active = True
+ else:
+ raise ValueError('Invalid zoom level: %r' % (new_level))
+
diff --git a/src/jarabe/view/home/FriendView.py b/src/jarabe/view/home/FriendView.py
new file mode 100644
index 0000000..f1e00b1
--- /dev/null
+++ b/src/jarabe/view/home/FriendView.py
@@ -0,0 +1,87 @@
+# 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.icon import CanvasIcon
+from sugar.graphics import style
+from sugar.presence import presenceservice
+from sugar import activity
+
+from jarabe.view.BuddyIcon import BuddyIcon
+
+class FriendView(hippo.CanvasBox):
+ def __init__(self, buddy, **kwargs):
+ hippo.CanvasBox.__init__(self, **kwargs)
+
+ self._pservice = presenceservice.get_instance()
+
+ self._buddy = buddy
+ self._buddy_icon = BuddyIcon(buddy)
+ self._buddy_icon.props.size = style.LARGE_ICON_SIZE
+ self.append(self._buddy_icon)
+
+ self._activity_icon = CanvasIcon(size=style.LARGE_ICON_SIZE)
+ self._activity_icon_visible = False
+
+ if self._buddy.is_present():
+ self._buddy_appeared_cb(buddy)
+
+ self._buddy.connect('current-activity-changed',
+ self._buddy_activity_changed_cb)
+ self._buddy.connect('appeared', self._buddy_appeared_cb)
+ self._buddy.connect('disappeared', self._buddy_disappeared_cb)
+ self._buddy.connect('color-changed', self._buddy_color_changed_cb)
+
+ def _get_new_icon_name(self, ps_activity):
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(ps_activity.props.type)
+ if activity_info:
+ return activity_info.icon
+ return None
+
+ def _remove_activity_icon(self):
+ if self._activity_icon_visible:
+ self.remove(self._activity_icon)
+ self._activity_icon_visible = False
+
+ def _buddy_activity_changed_cb(self, buddy, ps_activity=None):
+ if not ps_activity:
+ self._remove_activity_icon()
+ return
+
+ # FIXME: use some sort of "unknown activity" icon rather
+ # than hiding the icon?
+ name = self._get_new_icon_name(ps_activity)
+ if name:
+ self._activity_icon.props.file_name = name
+ self._activity_icon.props.xo_color = buddy.get_color()
+ if not self._activity_icon_visible:
+ self.append(self._activity_icon, hippo.PACK_EXPAND)
+ self._activity_icon_visible = True
+ else:
+ self._remove_activity_icon()
+
+ def _buddy_appeared_cb(self, buddy):
+ home_activity = self._buddy.get_current_activity()
+ self._buddy_activity_changed_cb(buddy, home_activity)
+
+ def _buddy_disappeared_cb(self, buddy):
+ self._buddy_activity_changed_cb(buddy, None)
+
+ def _buddy_color_changed_cb(self, buddy, color):
+ # TODO: shouldn't this change self._buddy_icon instead?
+ self._activity_icon.props.xo_color = buddy.get_color()
diff --git a/src/jarabe/view/home/FriendsBox.py b/src/jarabe/view/home/FriendsBox.py
new file mode 100644
index 0000000..42e5709
--- /dev/null
+++ b/src/jarabe/view/home/FriendsBox.py
@@ -0,0 +1,89 @@
+# 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 gobject
+import gtk
+import hippo
+
+from sugar import profile
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon, Icon
+from sugar.graphics.palette import Palette
+
+from jarabe.model import shellmodel
+from jarabe.view.home.FriendView import FriendView
+from jarabe.view.home.spreadlayout import SpreadLayout
+
+class FriendsBox(hippo.Canvas):
+ __gtype_name__ = 'SugarFriendsBox'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._friends = {}
+
+ self._layout = SpreadLayout()
+ self._box.set_layout(self._layout)
+
+ self._owner_icon = CanvasIcon(icon_name='computer-xo', cache=True,
+ xo_color=profile.get_color())
+ self._owner_icon.props.size = style.LARGE_ICON_SIZE
+ palette_icon = Icon(icon_name='computer-xo',
+ xo_color=profile.get_color())
+ palette_icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR
+ palette = Palette(None, primary_text=profile.get_nick_name(),
+ icon=palette_icon)
+ self._owner_icon.set_palette(palette)
+ self._layout.add(self._owner_icon)
+
+ friends = shellmodel.get_instance().get_friends()
+
+ for friend in friends:
+ self.add_friend(friend)
+
+ friends.connect('friend-added', self._friend_added_cb)
+ friends.connect('friend-removed', self._friend_removed_cb)
+
+ def add_friend(self, buddy_info):
+ icon = FriendView(buddy_info)
+ self._layout.add(icon)
+
+ self._friends[buddy_info.get_key()] = icon
+
+ def _friend_added_cb(self, data_model, buddy_info):
+ self.add_friend(buddy_info)
+
+ def _friend_removed_cb(self, data_model, key):
+ icon = self._friends[key]
+ self._layout.remove(icon)
+ del self._friends[key]
+ icon.destroy()
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, icon_width = self._owner_icon.get_width_request()
+ min_h_, icon_height = self._owner_icon.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - icon_height) / 2
+ self._layout.move(self._owner_icon, x, y)
+
+ hippo.Canvas.do_size_allocate(self, allocation)
+
diff --git a/src/jarabe/view/home/HomeBox.py b/src/jarabe/view/home/HomeBox.py
new file mode 100644
index 0000000..400ee8f
--- /dev/null
+++ b/src/jarabe/view/home/HomeBox.py
@@ -0,0 +1,367 @@
+# Copyright (C) 2008 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
+
+from gettext import gettext as _
+import logging
+import os
+
+import gobject
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics import iconentry
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.alert import Alert
+from sugar.graphics.icon import Icon
+from sugar import profile
+from sugar import activity
+from sugar.bundle.activitybundle import ActivityBundle
+
+from jarabe.view.home import favoritesview
+from jarabe.view.home.activitieslist import ActivitiesList
+
+_FAVORITES_VIEW = 0
+_LIST_VIEW = 1
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+def _convert_layout_constant(profile_constant):
+ for layoutid, layoutclass in favoritesview.LAYOUT_MAP.items():
+ if profile_constant == layoutclass.profile_key:
+ return layoutid
+ logging.warning('Incorrect favorites_layout value: %r' % \
+ profile_constant)
+ return favoritesview.RING_LAYOUT
+
+class HomeBox(gtk.VBox):
+ __gtype_name__ = 'SugarHomeBox'
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._favorites_view = favoritesview.FavoritesView()
+ self._list_view = ActivitiesList()
+ self._enable_xo_palette = False
+
+ self._favorites_view.connect('erase-activated',
+ self.__erase_activated_cb)
+ self._list_view.connect('erase-activated', self.__erase_activated_cb)
+
+ self._toolbar = HomeToolbar()
+ self._toolbar.connect('query-changed', self.__toolbar_query_changed_cb)
+ self._toolbar.connect('view-changed', self.__toolbar_view_changed_cb)
+ self.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ profile_layout_constant = profile.get_profile().favorites_layout
+ layout = _convert_layout_constant(profile_layout_constant)
+ self._set_view(_FAVORITES_VIEW, layout)
+
+ def __erase_activated_cb(self, view, bundle_id):
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(bundle_id)
+
+ alert = Alert()
+ alert.props.title = _('Confirm erase')
+ alert.props.msg = \
+ _('Confirm erase: Do you want to permanently erase %s?') \
+ % activity_info.name
+
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Keep'), cancel_icon)
+
+ erase_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Erase'), erase_icon)
+
+ if self._list_view in self.get_children():
+ self._list_view.add_alert(alert)
+ else:
+ self._favorites_view.add_alert(alert)
+ # TODO: If the favorite layouts didn't hardcoded the box size, we could
+ # just pack an alert between the toolbar and the canvas.
+ #self.pack_start(alert, False)
+ #self.reorder_child(alert, 1)
+ alert.connect('response', self.__erase_confirmation_dialog_response_cb,
+ bundle_id)
+
+ def __erase_confirmation_dialog_response_cb(self, alert, response_id,
+ bundle_id):
+ if self._list_view in self.get_children():
+ self._list_view.remove_alert()
+ else:
+ self._favorites_view.remove_alert()
+ if response_id == gtk.RESPONSE_OK:
+ registry = activity.get_registry()
+ activity_info = registry.get_activity(bundle_id)
+ ActivityBundle(activity_info.path).uninstall()
+
+ def show_software_updates_alert(self):
+ alert = Alert()
+ updater_icon = Icon(icon_name='module-updater',
+ pixel_size = style.STANDARD_ICON_SIZE)
+ alert.props.icon = updater_icon
+ updater_icon.show()
+ alert.props.title = _('Software Update')
+ alert.props.msg = _('Update your activities to ensure'
+ ' compatibility with your new software')
+
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
+
+ alert.add_button(gtk.RESPONSE_REJECT, _('Later'))
+
+ erase_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Check now'), erase_icon)
+
+ if self._list_view in self.get_children():
+ self._list_view.add_alert(alert)
+ else:
+ self._favorites_view.add_alert(alert)
+ alert.connect('response', self.__software_update_response_cb)
+
+ def __software_update_response_cb(self, alert, response_id):
+ if self._list_view in self.get_children():
+ self._list_view.remove_alert()
+ else:
+ self._favorites_view.remove_alert()
+
+ if response_id != gtk.RESPONSE_REJECT:
+ update_trigger_file = os.path.expanduser('~/.sugar-update')
+ try:
+ os.unlink(update_trigger_file)
+ except OSError:
+ logging.error('Software-update: Can not remove file %s' %
+ update_trigger_file)
+
+ if response_id == gtk.RESPONSE_OK:
+ from jarabe.controlpanel.gui import ControlPanel
+ panel = ControlPanel()
+ panel.set_transient_for(self.get_toplevel())
+ panel.show()
+ panel.show_section_view('updater')
+ panel.set_section_view_auto_close()
+
+ def __toolbar_query_changed_cb(self, toolbar, query):
+ query = query.lower()
+ self._list_view.set_filter(query)
+
+ def __toolbar_view_changed_cb(self, toolbar, view, layout):
+ self._set_view(view, layout)
+ if layout is not None:
+ current_profile = profile.get_profile()
+ profile_key = favoritesview.LAYOUT_MAP[layout].profile_key
+ if profile_key != current_profile.favorites_layout:
+ current_profile.favorites_layout = profile_key
+ current_profile.save()
+ else:
+ logging.warning('Incorrect layout requested: %r' % layout)
+
+ def _set_view(self, view, layout):
+ if view == _FAVORITES_VIEW:
+ if self._list_view in self.get_children():
+ self.remove(self._list_view)
+
+ self._favorites_view.layout = layout
+
+ if self._enable_xo_palette:
+ self._favorites_view.enable_xo_palette()
+
+ if self._favorites_view not in self.get_children():
+ self.add(self._favorites_view)
+ self._favorites_view.show()
+ elif view == _LIST_VIEW:
+ if self._favorites_view in self.get_children():
+ self.remove(self._favorites_view)
+
+ if self._list_view not in self.get_children():
+ self.add(self._list_view)
+ self._list_view.show()
+ else:
+ raise ValueError('Invalid view: %r' % view)
+
+ _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes
+
+ def resume(self):
+ pass
+
+ def suspend(self):
+ pass
+
+ def has_activities(self):
+ # TODO: Do we need this?
+ #return self._donut.has_activities()
+ return False
+
+ def enable_xo_palette(self):
+ self._enable_xo_palette = True
+ if self._favorites_view is not None:
+ self._favorites_view.enable_xo_palette()
+
+ def focus_search_entry(self):
+ self._toolbar.search_entry.grab_focus()
+
+
+class HomeToolbar(gtk.Toolbar):
+ __gtype_name__ = 'SugarHomeToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str])),
+ 'view-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object, object]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._query = None
+ self._autosearch_timer = None
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self.search_entry = iconentry.IconEntry()
+ self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self.search_entry.add_clear_button()
+ self.search_entry.set_width_chars(25)
+ self.search_entry.connect('activate', self.__entry_activated_cb)
+ self.search_entry.connect('changed', self.__entry_changed_cb)
+ tool_item.add(self.search_entry)
+ self.search_entry.set_sensitive(False)
+ self.search_entry.show()
+
+ self._add_separator(expand=True)
+
+ favorites_button = FavoritesButton()
+ favorites_button.connect('toggled', self.__view_button_toggled_cb,
+ _FAVORITES_VIEW)
+ self.insert(favorites_button, -1)
+ favorites_button.show()
+
+ self._list_button = RadioToolButton(named_icon='view-list')
+ self._list_button.props.group = favorites_button
+ self._list_button.props.tooltip = _('List view')
+ self._list_button.props.accelerator = _('<Ctrl>2')
+ self._list_button.connect('toggled', self.__view_button_toggled_cb,
+ _LIST_VIEW)
+ self.insert(self._list_button, -1)
+ self._list_button.show()
+
+ self._add_separator()
+
+ def __view_button_toggled_cb(self, button, view):
+ if button.props.active:
+ if view == _FAVORITES_VIEW:
+ self.search_entry.set_text('')
+ self.search_entry.set_sensitive(False)
+ self.emit('view-changed', view, button.layout)
+ else:
+ self.search_entry.set_sensitive(True)
+ self.search_entry.grab_focus()
+ self.emit('view-changed', view, None)
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.GRID_CELL_SIZE,
+ style.GRID_CELL_SIZE)
+ self.insert(separator, -1)
+ separator.show()
+
+ def __entry_activated_cb(self, entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = entry.props.text
+ if self._query != new_query:
+ self._query = new_query
+
+ if self._query is not '':
+ self._list_button.props.active = True
+ self.emit('query-changed', self._query)
+
+ def __entry_changed_cb(self, entry):
+ if not entry.props.text:
+ entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self.__autosearch_timer_cb)
+
+ def __autosearch_timer_cb(self):
+ self._autosearch_timer = None
+ self.search_entry.activate()
+ return False
+
+class FavoritesButton(RadioToolButton):
+ __gtype_name__ = 'SugarFavoritesButton'
+
+ def __init__(self):
+ RadioToolButton.__init__(self)
+
+ self.props.tooltip = _('Favorites view')
+ self.props.accelerator = _('<Ctrl>1')
+ self.props.group = None
+
+ profile_layout_constant = profile.get_profile().favorites_layout
+ self._layout = _convert_layout_constant(profile_layout_constant)
+ self._update_icon()
+
+ # someday, this will be a gtk.Table()
+ layouts_grid = gtk.HBox()
+ layout_item = None
+ for layoutid, layoutclass in sorted(favoritesview.LAYOUT_MAP.items()):
+ layout_item = RadioToolButton(icon_name=layoutclass.icon_name,
+ group=layout_item, active=False)
+ if layoutid == self._layout:
+ layout_item.set_active(True)
+ layouts_grid.add(layout_item)
+ layout_item.connect('toggled', self.__layout_activate_cb,
+ layoutid)
+ layouts_grid.show_all()
+ self.props.palette.set_content(layouts_grid)
+
+ def __layout_activate_cb(self, menu_item, layout):
+ if not menu_item.get_active():
+ return
+ if self._layout == layout and self.props.active:
+ return
+ elif self._layout != layout:
+ self._layout = layout
+ self._update_icon()
+ if not self.props.active:
+ self.props.active = True
+ else:
+ self.emit('toggled')
+
+ def _update_icon(self):
+ self.props.named_icon = favoritesview.LAYOUT_MAP[self._layout]\
+ .icon_name
+
+ def _get_layout(self):
+ return self._layout
+ layout = property(_get_layout, None)
+
diff --git a/src/jarabe/view/home/HomeWindow.py b/src/jarabe/view/home/HomeWindow.py
new file mode 100644
index 0000000..0bc56a0
--- /dev/null
+++ b/src/jarabe/view/home/HomeWindow.py
@@ -0,0 +1,153 @@
+# 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
+
+from sugar.graphics import style
+from sugar.graphics import palettegroup
+
+from jarabe.view.home.MeshBox import MeshBox
+from jarabe.view.home.HomeBox import HomeBox
+from jarabe.view.home.FriendsBox import FriendsBox
+from jarabe.view.home.transitionbox import TransitionBox
+from jarabe.model.shellmodel import ShellModel
+from jarabe.model import shellmodel
+
+_HOME_PAGE = 0
+_FRIENDS_PAGE = 1
+_MESH_PAGE = 2
+_TRANSITION_PAGE = 3
+
+class HomeWindow(gtk.Window):
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ accel_group = gtk.AccelGroup()
+ self.set_data('sugar-accel-group', accel_group)
+ self.add_accel_group(accel_group)
+
+ self._active = False
+ self._level = ShellModel.ZOOM_HOME
+
+ self.set_default_size(gtk.gdk.screen_width(),
+ gtk.gdk.screen_height())
+
+ self.realize()
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP)
+ self.connect('visibility-notify-event',
+ self._visibility_notify_event_cb)
+
+ self._enter_sid = self.connect('enter-notify-event',
+ self._enter_notify_event_cb)
+ self._leave_sid = self.connect('leave-notify-event',
+ self._leave_notify_event_cb)
+ self._motion_sid = self.connect('motion-notify-event',
+ self._motion_notify_event_cb)
+
+ self._home_box = HomeBox()
+ self._friends_box = FriendsBox()
+ self._mesh_box = MeshBox()
+ self._transition_box = TransitionBox()
+
+ self._activate_view()
+ self.add(self._home_box)
+ self._home_box.show()
+
+ self._transition_box.connect('completed',
+ self._transition_completed_cb)
+
+ model = shellmodel.get_instance()
+ model.connect('notify::zoom-level', self.__zoom_level_changed_cb)
+
+ def _enter_notify_event_cb(self, window, event):
+ if event.x != gtk.gdk.screen_width() / 2 or \
+ event.y != gtk.gdk.screen_height() / 2:
+ self._mouse_moved()
+
+ def _leave_notify_event_cb(self, window, event):
+ self._mouse_moved()
+
+ def _motion_notify_event_cb(self, window, event):
+ self._mouse_moved()
+
+ # We want to enable the XO palette only when the user
+ # moved away from the default mouse position (screen center).
+ def _mouse_moved(self):
+ self._home_box.enable_xo_palette()
+ self.disconnect(self._leave_sid)
+ self.disconnect(self._motion_sid)
+ self.disconnect(self._enter_sid)
+
+ def _deactivate_view(self):
+ group = palettegroup.get_group("default")
+ group.popdown()
+ if self._level == ShellModel.ZOOM_HOME:
+ self._home_box.suspend()
+ elif self._level == ShellModel.ZOOM_MESH:
+ self._mesh_box.suspend()
+
+ def _activate_view(self):
+ if self._level == ShellModel.ZOOM_HOME:
+ self._home_box.resume()
+ elif self._level == ShellModel.ZOOM_MESH:
+ self._mesh_box.resume()
+
+ def _visibility_notify_event_cb(self, window, event):
+ if event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED:
+ self._deactivate_view()
+ else:
+ self._activate_view()
+
+ def __zoom_level_changed_cb(self, model, pspec):
+ level = model.props.zoom_level
+ if level == ShellModel.ZOOM_ACTIVITY:
+ return
+
+ self._deactivate_view()
+ self._level = level
+ self._activate_view()
+
+ self.remove(self.get_child())
+ self.add(self._transition_box)
+ self._transition_box.show()
+
+ if self._level == ShellModel.ZOOM_HOME:
+ size = style.XLARGE_ICON_SIZE
+ elif self._level == ShellModel.ZOOM_FRIENDS:
+ size = style.LARGE_ICON_SIZE
+ elif self._level == ShellModel.ZOOM_MESH:
+ size = style.STANDARD_ICON_SIZE
+
+ self._transition_box.set_size(size)
+
+ def _transition_completed_cb(self, transition_box):
+ current_child = self.get_child()
+ self.remove(current_child)
+
+ if self._level == ShellModel.ZOOM_HOME:
+ self.add(self._home_box)
+ self._home_box.show()
+ self._home_box.focus_search_entry()
+ elif self._level == ShellModel.ZOOM_FRIENDS:
+ self.add(self._friends_box)
+ self._friends_box.show()
+ elif self._level == ShellModel.ZOOM_MESH:
+ self.add(self._mesh_box)
+ self._mesh_box.show()
+ self._mesh_box.focus_search_entry()
+
+ def get_home_box(self):
+ return self._home_box
diff --git a/src/jarabe/view/home/Makefile.am b/src/jarabe/view/home/Makefile.am
new file mode 100644
index 0000000..c7d9a98
--- /dev/null
+++ b/src/jarabe/view/home/Makefile.am
@@ -0,0 +1,17 @@
+sugardir = $(pythondir)/jarabe/view/home
+sugar_PYTHON = \
+ __init__.py \
+ activitieslist.py \
+ favoritesview.py \
+ favoriteslayout.py \
+ grid.py \
+ FriendView.py \
+ FriendsBox.py \
+ HomeBox.py \
+ HomeWindow.py \
+ MeshBox.py \
+ MyIcon.py \
+ proc_smaps.py \
+ snowflakelayout.py \
+ spreadlayout.py \
+ transitionbox.py
diff --git a/src/jarabe/view/home/MeshBox.py b/src/jarabe/view/home/MeshBox.py
new file mode 100644
index 0000000..f94b930
--- /dev/null
+++ b/src/jarabe/view/home/MeshBox.py
@@ -0,0 +1,646 @@
+# 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 logging
+
+import hippo
+import gobject
+import gtk
+
+from sugar.graphics.icon import CanvasIcon, Icon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import style
+from sugar.graphics.icon import get_icon_state
+from sugar.graphics import palette
+from sugar.graphics import iconentry
+from sugar.graphics.menuitem import MenuItem
+from sugar import profile
+
+from jarabe.model import accesspointmodel
+from jarabe.model.devices.network import wireless
+from jarabe.model import shellmodel
+from jarabe.hardware import hardwaremanager
+from jarabe.hardware import nmclient
+from jarabe.view.BuddyIcon import BuddyIcon
+from jarabe.view.pulsingicon import CanvasPulsingIcon
+from jarabe.view.home.snowflakelayout import SnowflakeLayout
+from jarabe.view.home.spreadlayout import SpreadLayout
+import view.Shell
+
+from jarabe.hardware.nmclient import NM_802_11_CAP_PROTO_WEP, \
+ NM_802_11_CAP_PROTO_WPA, NM_802_11_CAP_PROTO_WPA2
+
+
+_ICON_NAME = 'network-wireless'
+
+class AccessPointView(CanvasPulsingIcon):
+ def __init__(self, model, mesh_device=None):
+ CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE,
+ cache=True)
+ self._model = model
+ self._meshdev = mesh_device
+ self._disconnect_item = None
+ self._connect_item = None
+ self._greyed_out = False
+
+ self.connect('activated', self._activate_cb)
+
+ model.connect('notify::strength', self._strength_changed_cb)
+ model.connect('notify::name', self._name_changed_cb)
+ model.connect('notify::state', self._state_changed_cb)
+
+ pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ self.props.pulse_color = pulse_color
+
+ # Update badge
+ caps = model.props.capabilities
+ if model.get_nm_network().is_favorite():
+ self.props.badge_name = "emblem-favorite"
+ elif (caps & NM_802_11_CAP_PROTO_WEP) or \
+ (caps & NM_802_11_CAP_PROTO_WPA) or \
+ (caps & NM_802_11_CAP_PROTO_WPA2):
+ self.props.badge_name = "emblem-locked"
+
+ self._palette = self._create_palette()
+ self.set_palette(self._palette)
+
+ self._update_icon()
+ self._update_name()
+ self._update_state()
+
+ def _create_palette(self):
+ icon_name = get_icon_state(_ICON_NAME, self._model.props.strength)
+ palette_icon = Icon(icon_name=icon_name,
+ icon_size=style.STANDARD_ICON_SIZE,
+ badge_name=self.props.badge_name)
+ ap_color = self._model.get_nm_network().get_colors()
+ palette_icon.props.xo_color = XoColor('%s,%s' % ap_color)
+
+ p = palette.Palette(primary_text=self._model.props.name,
+ icon=palette_icon)
+
+ self._connect_item = MenuItem(_('Connect'), 'dialog-ok')
+ self._connect_item.connect('activate', self._activate_cb)
+ p.menu.append(self._connect_item)
+
+ # Only show disconnect when there's a mesh device, because mesh takes
+ # priority over the normal wireless device. NM doesn't have a
+ # "disconnect" method for a device either (for various reasons)
+ # so this doesn't have a good mapping
+ if self._meshdev:
+ self._disconnect_item = MenuItem(_('Disconnect'), 'media-eject')
+ self._disconnect_item.connect('activate',
+ self._disconnect_activate_cb)
+ p.menu.append(self._disconnect_item)
+
+ return p
+
+ def _disconnect_activate_cb(self, menuitem):
+ # Disconnection for an AP means activating the default mesh device
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager and self._meshdev:
+ network_manager.set_active_device(self._meshdev)
+ self._palette.props.secondary_text = _('Disconnecting...')
+ self.props.pulsing = False
+
+ def _strength_changed_cb(self, model, pspec):
+ self._update_icon()
+
+ def _name_changed_cb(self, model, pspec):
+ self._update_name()
+
+ def _state_changed_cb(self, model, pspec):
+ self._update_icon()
+ self._update_state()
+
+ def _activate_cb(self, icon):
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager:
+ device = self._model.get_nm_device()
+ network = self._model.get_nm_network()
+ network_manager.set_active_device(device, network)
+
+ def _update_name(self):
+ self._palette.props.primary_text = self._model.props.name
+
+ def _update_icon(self):
+ # keep this code in sync with view/devices/network/wireless.py
+ strength = self._model.props.strength
+ if self._model.props.state == accesspointmodel.STATE_CONNECTED:
+ icon_name = '%s-connected' % _ICON_NAME
+ else:
+ icon_name = _ICON_NAME
+ icon_name = get_icon_state(icon_name, strength)
+ if icon_name:
+ self.props.icon_name = icon_name
+ icon = self._palette.props.icon
+ icon.props.icon_name = icon_name
+
+ def _update_state(self):
+ if self._model.props.state == accesspointmodel.STATE_CONNECTING:
+ if self._disconnect_item:
+ self._disconnect_item.show()
+ self._connect_item.hide()
+ self._palette.props.secondary_text = _('Connecting...')
+ self.props.pulsing = True
+ elif self._model.props.state == accesspointmodel.STATE_CONNECTED:
+ if self._disconnect_item:
+ self._disconnect_item.show()
+ self._connect_item.hide()
+ # TODO: show the channel number
+ self._palette.props.secondary_text = _('Connected')
+ self.props.pulsing = False
+ elif self._model.props.state == accesspointmodel.STATE_NOTCONNECTED:
+ if self._disconnect_item:
+ self._disconnect_item.hide()
+ self._connect_item.show()
+ # TODO: show the channel number
+ self._palette.props.secondary_text = None
+ self.props.pulsing = False
+
+ if self._greyed_out:
+ self.props.pulsing = False
+ self.props.base_color = XoColor('#D5D5D5,#D5D5D5')
+ else:
+ self.props.base_color = XoColor('%s,%s' % \
+ self._model.get_nm_network().get_colors())
+
+ def set_filter(self, query):
+ self._greyed_out = self._model.props.name.lower().find(query) == -1
+ self._update_state()
+
+_MESH_ICON_NAME = 'network-mesh'
+
+class MeshDeviceView(CanvasPulsingIcon):
+ def __init__(self, nm_device, channel):
+ if not channel in [1, 6, 11]:
+ raise ValueError("Invalid channel %d" % channel)
+
+ CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE,
+ icon_name=_MESH_ICON_NAME, cache=True)
+
+ self._nm_device = nm_device
+ self.channel = channel
+ self.props.badge_name = "badge-channel-%d" % self.channel
+ self._greyed_out = False
+
+ self._disconnect_item = None
+ self._palette = self._create_palette()
+ self.set_palette(self._palette)
+
+ pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ self.props.pulse_color = pulse_color
+
+ self.connect('activated', self._activate_cb)
+
+ self._nm_device.connect('state-changed', self._state_changed_cb)
+ self._nm_device.connect('activation-stage-changed',
+ self._state_changed_cb)
+ self._update_state()
+
+ def _create_palette(self):
+ p = palette.Palette(_("Mesh Network") + " " + str(self.channel),
+ menu_after_content=True)
+
+ self._disconnect_item = gtk.MenuItem(_('Disconnect...'))
+ self._disconnect_item.connect('activate', self._disconnect_activate_cb)
+ p.menu.append(self._disconnect_item)
+
+ state = self._nm_device.get_state()
+ chan = wireless.freq_to_channel(self._nm_device.get_frequency())
+ if state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel:
+ self._disconnect_item.show()
+ return p
+
+ def _disconnect_activate_cb(self, menuitem):
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager:
+ network_manager.set_active_device(self._nm_device)
+
+ def _activate_cb(self, icon):
+ network_manager = hardwaremanager.get_network_manager()
+ if network_manager:
+ freq = wireless.channel_to_freq(self.channel)
+ network_manager.set_active_device(self._nm_device, mesh_freq=freq)
+
+ def _state_changed_cb(self, model):
+ self._update_state()
+
+ def _update_state(self):
+ state = self._nm_device.get_state()
+ chan = wireless.freq_to_channel(self._nm_device.get_frequency())
+ if state == nmclient.DEVICE_STATE_ACTIVATING and chan == self.channel:
+ self._disconnect_item.hide()
+ self.props.pulsing = True
+ elif state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel:
+ self._disconnect_item.show()
+ self.props.pulsing = False
+ elif state == nmclient.DEVICE_STATE_INACTIVE or chan != self.channel:
+ self._disconnect_item.hide()
+ self.props.pulsing = False
+
+ if self._greyed_out:
+ self.props.pulsing = False
+ self.props.base_color = XoColor('#D5D5D5,#D5D5D5')
+ else:
+ self.props.base_color = profile.get_color()
+
+ def set_filter(self, query):
+ self._greyed_out = (query != '')
+ self._update_state()
+
+class ActivityView(hippo.CanvasBox):
+ def __init__(self, model):
+ hippo.CanvasBox.__init__(self)
+
+ self._model = model
+ self._icons = {}
+ self._palette = None
+
+ self._layout = SnowflakeLayout()
+ self.set_layout(self._layout)
+
+ self._icon = self._create_icon()
+ self._layout.add(self._icon, center=True)
+
+ self._update_palette()
+
+ activity = self._model.activity
+ activity.connect('notify::name', self._name_changed_cb)
+ activity.connect('notify::color', self._color_changed_cb)
+ activity.connect('notify::private', self._private_changed_cb)
+ activity.connect('joined', self._joined_changed_cb)
+ #FIXME: 'joined' signal not working, see #5032
+
+ def _create_icon(self):
+ icon = CanvasIcon(file_name=self._model.get_icon_name(),
+ xo_color=self._model.get_color(), cache=True,
+ size=style.STANDARD_ICON_SIZE)
+ icon.connect('activated', self._clicked_cb)
+ return icon
+
+ def _create_palette(self):
+ p_icon = Icon(file=self._model.get_icon_name(),
+ xo_color=self._model.get_color())
+ p_icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR
+ p = palette.Palette(None, primary_text=self._model.activity.props.name,
+ icon=p_icon)
+
+ private = self._model.activity.props.private
+ joined = self._model.activity.props.joined
+
+ if joined:
+ item = MenuItem(_('Resume'), 'activity-start')
+ item.connect('activate', self._clicked_cb)
+ item.show()
+ p.menu.append(item)
+ elif not private:
+ item = MenuItem(_('Join'), 'activity-start')
+ item.connect('activate', self._clicked_cb)
+ item.show()
+ p.menu.append(item)
+
+ return p
+
+ def _update_palette(self):
+ self._palette = self._create_palette()
+ self._icon.set_palette(self._palette)
+
+ def has_buddy_icon(self, key):
+ return self._icons.has_key(key)
+
+ def add_buddy_icon(self, key, icon):
+ self._icons[key] = icon
+ self._layout.add(icon)
+
+ def remove_buddy_icon(self, key):
+ icon = self._icons[key]
+ del self._icons[key]
+ icon.destroy()
+
+ def _clicked_cb(self, item):
+ bundle_id = self._model.get_bundle_id()
+ view.Shell.get_instance().join_activity(bundle_id, self._model.get_id())
+
+ def set_filter(self, query):
+ text_to_check = self._model.activity.props.name.lower() + \
+ self._model.activity.props.type.lower()
+ if text_to_check.find(query) == -1:
+ self._icon.props.stroke_color = '#D5D5D5'
+ self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ else:
+ self._icon.props.xo_color = self._model.get_color()
+
+ for icon in self._icons.itervalues():
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(query)
+
+ def _name_changed_cb(self, activity, pspec):
+ self._update_palette()
+
+ def _color_changed_cb(self, activity, pspec):
+ self._layout.remove(self._icon)
+ self._icon = self._create_icon()
+ self._layout.add(self._icon, center=True)
+ self._icon.set_palette(self._palette)
+
+ def _private_changed_cb(self, activity, pspec):
+ self._update_palette()
+
+ def _joined_changed_cb(self, widget, event):
+ logging.debug('ActivityView._joined_changed_cb: AAAA!!!!')
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+class MeshToolbar(gtk.Toolbar):
+ __gtype_name__ = 'MeshToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._query = None
+ self._autosearch_timer = None
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self.search_entry = iconentry.IconEntry()
+ self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self.search_entry.add_clear_button()
+ self.search_entry.set_width_chars(25)
+ self.search_entry.connect('activate', self._entry_activated_cb)
+ self.search_entry.connect('changed', self._entry_changed_cb)
+ tool_item.add(self.search_entry)
+ self.search_entry.show()
+
+ self._add_separator(expand=True)
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.GRID_CELL_SIZE,
+ style.GRID_CELL_SIZE)
+ self.insert(separator, -1)
+ separator.show()
+
+ def _entry_activated_cb(self, entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = entry.props.text
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _entry_changed_cb(self, entry):
+ if not entry.props.text:
+ entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self._autosearch_timer_cb)
+
+ def _autosearch_timer_cb(self):
+ logging.debug('_autosearch_timer_cb')
+ self._autosearch_timer = None
+ self.search_entry.activate()
+ return False
+
+class MeshBox(gtk.VBox):
+ __gtype_name__ = 'SugarMeshBox'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._model = shellmodel.get_instance().get_mesh()
+ self._buddies = {}
+ self._activities = {}
+ self._access_points = {}
+ self._mesh = {}
+ self._buddy_to_activity = {}
+ self._suspended = True
+ self._query = ''
+ self._owner_icon = None
+
+ self._toolbar = MeshToolbar()
+ self._toolbar.connect('query-changed', self._toolbar_query_changed_cb)
+ self.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ canvas = hippo.Canvas()
+ self.add(canvas)
+ canvas.show()
+
+ self._layout_box = hippo.CanvasBox( \
+ background_color=style.COLOR_WHITE.get_int())
+ canvas.set_root(self._layout_box)
+
+ self._layout = SpreadLayout()
+ self._layout_box.set_layout(self._layout)
+
+ for buddy_model in self._model.get_buddies():
+ self._add_alone_buddy(buddy_model)
+
+ self._model.connect('buddy-added', self._buddy_added_cb)
+ self._model.connect('buddy-removed', self._buddy_removed_cb)
+ self._model.connect('buddy-moved', self._buddy_moved_cb)
+
+ for activity_model in self._model.get_activities():
+ self._add_activity(activity_model)
+
+ self._model.connect('activity-added', self._activity_added_cb)
+ self._model.connect('activity-removed', self._activity_removed_cb)
+
+ for ap_model in self._model.get_access_points():
+ self._add_access_point(ap_model)
+
+ self._model.connect('access-point-added',
+ self._access_point_added_cb)
+ self._model.connect('access-point-removed',
+ self._access_point_removed_cb)
+
+ if self._model.get_mesh():
+ self.__mesh_added_cb(self._model, self._model.get_mesh())
+
+ self._model.connect('mesh-added', self.__mesh_added_cb)
+ self._model.connect('mesh-removed', self.__mesh_removed_cb)
+
+ def __mesh_added_cb(self, model, meshdev):
+ self._add_mesh_icon(meshdev, 1)
+ self._add_mesh_icon(meshdev, 6)
+ self._add_mesh_icon(meshdev, 11)
+
+ def __mesh_removed_cb(self, model):
+ self._remove_mesh_icon(1)
+ self._remove_mesh_icon(6)
+ self._remove_mesh_icon(11)
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, icon_width = self._owner_icon.get_width_request()
+ min_h_, icon_height = self._owner_icon.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - icon_height) / 2 - style.GRID_CELL_SIZE
+ self._layout.move(self._owner_icon, x, y)
+
+ gtk.VBox.do_size_allocate(self, allocation)
+
+ def _buddy_added_cb(self, model, buddy_model):
+ self._add_alone_buddy(buddy_model)
+
+ def _buddy_removed_cb(self, model, buddy_model):
+ self._remove_buddy(buddy_model)
+
+ def _buddy_moved_cb(self, model, buddy_model, activity_model):
+ # Owner doesn't move from the center
+ if buddy_model.is_owner():
+ return
+ self._move_buddy(buddy_model, activity_model)
+
+ def _activity_added_cb(self, model, activity_model):
+ self._add_activity(activity_model)
+
+ def _activity_removed_cb(self, model, activity_model):
+ self._remove_activity(activity_model)
+
+ def _access_point_added_cb(self, model, ap_model):
+ self._add_access_point(ap_model)
+
+ def _access_point_removed_cb(self, model, ap_model):
+ self._remove_access_point(ap_model)
+
+ def _add_mesh_icon(self, meshdev, channel):
+ if self._mesh.has_key(channel):
+ self._remove_mesh_icon(channel)
+ if not meshdev:
+ return
+ self._mesh[channel] = MeshDeviceView(meshdev, channel)
+ self._layout.add(self._mesh[channel])
+
+ def _remove_mesh_icon(self, channel):
+ if not self._mesh.has_key(channel):
+ return
+ self._layout.remove(self._mesh[channel])
+ del self._mesh[channel]
+
+ def _add_alone_buddy(self, buddy_model):
+ icon = BuddyIcon(buddy_model)
+ if buddy_model.is_owner():
+ self._owner_icon = icon
+ self._layout.add(icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ self._buddies[buddy_model.get_buddy().object_path()] = icon
+
+ def _remove_alone_buddy(self, buddy_model):
+ icon = self._buddies[buddy_model.get_buddy().object_path()]
+ self._layout.remove(icon)
+ del self._buddies[buddy_model.get_buddy().object_path()]
+ icon.destroy()
+
+ def _remove_buddy(self, buddy_model):
+ object_path = buddy_model.get_buddy().object_path()
+ if self._buddies.has_key(object_path):
+ self._remove_alone_buddy(buddy_model)
+ else:
+ for activity in self._activities.values():
+ if activity.has_buddy_icon(object_path):
+ activity.remove_buddy_icon(object_path)
+
+ def _move_buddy(self, buddy_model, activity_model):
+ self._remove_buddy(buddy_model)
+
+ if activity_model == None:
+ self._add_alone_buddy(buddy_model)
+ elif activity_model.get_id() in self._activities:
+ activity = self._activities[activity_model.get_id()]
+
+ icon = BuddyIcon(buddy_model, style.STANDARD_ICON_SIZE)
+ activity.add_buddy_icon(buddy_model.get_buddy().object_path(), icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ def _add_activity(self, activity_model):
+ icon = ActivityView(activity_model)
+ self._layout.add(icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ self._activities[activity_model.get_id()] = icon
+
+ def _remove_activity(self, activity_model):
+ icon = self._activities[activity_model.get_id()]
+ self._layout.remove(icon)
+ del self._activities[activity_model.get_id()]
+ icon.destroy()
+
+ def _add_access_point(self, ap_model):
+ meshdev = self._model.get_mesh()
+ icon = AccessPointView(ap_model, meshdev)
+ self._layout.add(icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ self._access_points[ap_model.get_id()] = icon
+
+ def _remove_access_point(self, ap_model):
+ icon = self._access_points[ap_model.get_id()]
+ self._layout.remove(icon)
+ del self._access_points[ap_model.get_id()]
+
+ def suspend(self):
+ if not self._suspended:
+ self._suspended = True
+ for ap in self._access_points.values():
+ ap.props.paused = True
+
+ def resume(self):
+ if self._suspended:
+ self._suspended = False
+ for ap in self._access_points.values():
+ ap.props.paused = False
+
+ def _toolbar_query_changed_cb(self, toolbar, query):
+ self._query = query.lower()
+ for icon in self._layout_box.get_children():
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ def focus_search_entry(self):
+ self._toolbar.search_entry.grab_focus()
diff --git a/src/jarabe/view/home/MyIcon.py b/src/jarabe/view/home/MyIcon.py
new file mode 100644
index 0000000..af0f6ce
--- /dev/null
+++ b/src/jarabe/view/home/MyIcon.py
@@ -0,0 +1,24 @@
+# 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 sugar.graphics.icon import CanvasIcon
+from sugar import profile
+
+class MyIcon(CanvasIcon):
+ def __init__(self, size):
+ CanvasIcon.__init__(self, size=size,
+ icon_name='computer-xo',
+ xo_color=profile.get_color())
diff --git a/src/jarabe/view/home/__init__.py b/src/jarabe/view/home/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/src/jarabe/view/home/__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/jarabe/view/home/activitieslist.py b/src/jarabe/view/home/activitieslist.py
new file mode 100644
index 0000000..54bd023
--- /dev/null
+++ b/src/jarabe/view/home/activitieslist.py
@@ -0,0 +1,302 @@
+# Copyright (C) 2008 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 gobject
+import gtk
+import hippo
+
+from sugar import profile
+from sugar import activity
+from sugar import util
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+
+import view.Shell
+from jarabe.view.palettes import ActivityPalette
+
+class ActivitiesList(gtk.VBox):
+ __gtype_name__ = 'SugarActivitiesList'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ scrolled_window.set_shadow_type(gtk.SHADOW_NONE)
+ scrolled_window.connect('key-press-event', self.__key_press_event_cb)
+ self.pack_start(scrolled_window)
+ scrolled_window.show()
+
+ canvas = hippo.Canvas()
+ scrolled_window.add_with_viewport(canvas)
+ scrolled_window.child.set_shadow_type(gtk.SHADOW_NONE)
+ canvas.show()
+
+ self._alert = None
+ self._query = ''
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ canvas.set_root(self._box)
+
+ 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)
+
+ def _get_activities_cb(self, activity_list):
+ if activity_list:
+ gobject.idle_add(self._add_activity_list, activity_list)
+
+ def _add_activity_list(self, activity_list):
+ info = activity_list.pop()
+ if info.bundle_id != 'org.laptop.JournalActivity':
+ self._add_activity(info)
+ return len(activity_list) > 0
+
+ def __activity_added_cb(self, activity_registry, activity_info):
+ self._add_activity(activity_info)
+
+ def __activity_removed_cb(self, activity_registry, activity_info):
+ for entry in self._box.get_children():
+ if entry.get_bundle_id() == activity_info.bundle_id and \
+ entry.get_version() == activity_info.version:
+ self._box.remove(entry)
+ return
+
+ def _compare_activities(self, entry_a, entry_b):
+ return entry_b.get_installation_time() - entry_a.get_installation_time()
+
+ def _add_activity(self, activity_info):
+ entry = ActivityEntry(activity_info)
+ entry.icon.connect('erase-activated', self.__erase_activated_cb)
+ self._box.insert_sorted(entry, 0, self._compare_activities)
+ entry.set_visible(entry.matches(self._query))
+
+ def __erase_activated_cb(self, activity_icon, bundle_id):
+ self.emit('erase-activated', bundle_id)
+
+ def set_filter(self, query):
+ self._query = query
+ for entry in self._box.get_children():
+ entry.set_visible(entry.matches(query))
+
+ def __key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+
+ vadjustment = self.props.vadjustment
+ if keyname == 'Up':
+ if vadjustment.props.value > vadjustment.props.lower:
+ vadjustment.props.value -= vadjustment.props.step_increment
+ elif keyname == 'Down':
+ max_value = vadjustment.props.upper - vadjustment.props.page_size
+ if vadjustment.props.value < max_value:
+ vadjustment.props.value = min(
+ vadjustment.props.value + vadjustment.props.step_increment,
+ max_value)
+ else:
+ return False
+
+ return True
+
+ def add_alert(self, alert):
+ if self._alert is not None:
+ self.remove_alert()
+ self._alert = alert
+ self.pack_start(alert, False)
+ self.reorder_child(alert, 0)
+
+ def remove_alert(self):
+ self.remove(self._alert)
+ self._alert = None
+
+class ActivityIcon(CanvasIcon):
+ __gtype_name__ = 'SugarListActivityIcon'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self, activity_info):
+ CanvasIcon.__init__(self, size=style.STANDARD_ICON_SIZE, cache=True,
+ file_name=activity_info.icon)
+ self._activity_info = activity_info
+ self._uncolor()
+ self.connect('hovering-changed', self.__hovering_changed_event_cb)
+ self.connect('button-release-event', self.__button_release_event_cb)
+
+ def create_palette(self):
+ palette = ActivityPalette(self._activity_info)
+ palette.connect('erase-activated', self.__erase_activated_cb)
+ return palette
+
+ def __erase_activated_cb(self, palette):
+ self.emit('erase-activated', self._activity_info.bundle_id)
+
+ def _color(self):
+ self.props.xo_color = profile.get_color()
+
+ def _uncolor(self):
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
+ def __hovering_changed_event_cb(self, icon, hovering):
+ if hovering:
+ self._color()
+ else:
+ self._uncolor()
+
+ def __button_release_event_cb(self, icon, event):
+ self.palette.popdown(immediate=True)
+ self._uncolor()
+
+
+class ActivityEntry(hippo.CanvasBox, hippo.CanvasItem):
+ __gtype_name__ = 'SugarActivityEntry'
+
+ _TITLE_COL_WIDTH = style.GRID_CELL_SIZE * 3
+ _VERSION_COL_WIDTH = style.GRID_CELL_SIZE * 1
+ _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 5
+
+ def __init__(self, activity_info):
+ hippo.CanvasBox.__init__(self, spacing=style.DEFAULT_SPACING,
+ padding_top=style.DEFAULT_PADDING,
+ padding_bottom=style.DEFAULT_PADDING,
+ padding_left=style.DEFAULT_PADDING * 2,
+ padding_right=style.DEFAULT_PADDING * 2,
+ box_height=style.GRID_CELL_SIZE,
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ registry = activity.get_registry()
+ registry.connect('activity-changed', self.__activity_changed_cb)
+
+ self._bundle_id = activity_info.bundle_id
+ self._version = activity_info.version
+ self._favorite = activity_info.favorite
+ self._title = activity_info.name
+ self._installation_time = activity_info.installation_time
+
+ self._favorite_icon = FavoriteIcon(self._favorite)
+ self._favorite_icon.connect('notify::favorite',
+ self.__favorite_changed_cb)
+ self.append(self._favorite_icon)
+
+ self.icon = ActivityIcon(activity_info)
+ self.icon.connect('button-release-event',
+ self.__icon_button_release_event_cb)
+ self.append(self.icon)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ align = hippo.ALIGNMENT_END
+ else:
+ align = hippo.ALIGNMENT_START
+
+ title = hippo.CanvasText(text=activity_info.name,
+ xalign=align,
+ font_desc=style.FONT_BOLD.get_pango_desc(),
+ box_width=ActivityEntry._TITLE_COL_WIDTH)
+ self.append(title)
+
+ version = hippo.CanvasText(text=activity_info.version,
+ xalign=hippo.ALIGNMENT_END,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ box_width=ActivityEntry._VERSION_COL_WIDTH)
+ self.append(version)
+
+ expander = hippo.CanvasBox()
+ self.append(expander, hippo.PACK_EXPAND)
+
+ timestamp = activity_info.installation_time
+ date = hippo.CanvasText(
+ text=util.timestamp_to_elapsed_string(timestamp),
+ xalign=align,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ box_width=ActivityEntry._DATE_COL_WIDTH)
+ self.append(date)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ def __favorite_changed_cb(self, favorite_icon, pspec):
+ registry = activity.get_registry()
+ registry.set_activity_favorite(self._bundle_id, self._version,
+ favorite_icon.props.favorite)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ if self._bundle_id == activity_info.bundle_id and \
+ self._version == activity_info.version:
+ self._title = activity_info.name
+ self._favorite = activity_info.favorite
+ self._favorite_icon.props.favorite = self._favorite
+
+ def __icon_button_release_event_cb(self, icon, event):
+ view.Shell.get_instance().start_activity(self._bundle_id)
+
+ def get_bundle_id(self):
+ return self._bundle_id
+
+ def get_version(self):
+ return self._version
+
+ def get_installation_time(self):
+ return self._installation_time
+
+ def matches(self, query):
+ if not query:
+ return True
+ return self._title.lower().find(query) > -1
+
+class FavoriteIcon(CanvasIcon):
+ def __init__(self, favorite):
+ CanvasIcon.__init__(self, icon_name='emblem-favorite',
+ box_width=style.GRID_CELL_SIZE*3/5,
+ size=style.SMALL_ICON_SIZE)
+ self._favorite = None
+ self.set_favorite(favorite)
+ self.connect('button-release-event', self.__release_event_cb)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def set_favorite(self, favorite):
+ if favorite == self._favorite:
+ return
+
+ self._favorite = favorite
+ if favorite:
+ self.props.xo_color = profile.get_color()
+ else:
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_WHITE.get_svg()
+
+ def get_favorite(self):
+ return self._favorite
+
+ favorite = gobject.property(
+ type=bool, default=False, getter=get_favorite, setter=set_favorite)
+
+ def __release_event_cb(self, icon, event):
+ self.props.favorite = not self.props.favorite
+
+ def __motion_notify_event_cb(self, icon, event):
+ if not self._favorite:
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
diff --git a/src/jarabe/view/home/favoriteslayout.py b/src/jarabe/view/home/favoriteslayout.py
new file mode 100644
index 0000000..e6b5c83
--- /dev/null
+++ b/src/jarabe/view/home/favoriteslayout.py
@@ -0,0 +1,485 @@
+# Copyright (C) 2008 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 math
+import hashlib
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar import activity
+
+from jarabe.view.home.grid import Grid
+
+_logger = logging.getLogger('FavoritesLayout')
+
+_CELL_SIZE = 4
+_BASE_SCALE = 1000
+
+class FavoritesLayout(gobject.GObject, hippo.CanvasLayout):
+ """Base class of the different layout types."""
+
+ __gtype_name__ = 'FavoritesLayout'
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self.box = None
+ self.fixed_positions = {}
+
+ def do_set_box(self, box):
+ self.box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE
+
+ def do_get_width_request(self):
+ return 0, gtk.gdk.screen_width()
+
+ def compare_activities(self, icon_a, icon_b):
+ return 0
+
+ def append(self, icon, locked=False):
+ self.box.insert_sorted(icon, 0, self.compare_activities)
+ if hasattr(icon, 'fixed_position'):
+ relative_x, relative_y = icon.fixed_position
+ if relative_x >= 0 and relative_y >= 0:
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+ self.fixed_positions[icon] = \
+ (int(relative_x * _BASE_SCALE / float(width)),
+ int(relative_y * _BASE_SCALE / float(height)))
+
+ def remove(self, icon):
+ if icon in self.fixed_positions:
+ del self.fixed_positions[icon]
+ self.box.remove(icon)
+
+ def move_icon(self, icon, x, y, locked=False):
+ if icon not in self.box.get_children():
+ raise ValueError('Child not in box.')
+
+ if hasattr(icon, 'get_bundle_id') and hasattr(icon, 'get_version'):
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+ registry = activity.get_registry()
+ registry.set_activity_position(
+ icon.get_bundle_id(), icon.get_version(),
+ x * width / float(_BASE_SCALE),
+ y * height / float(_BASE_SCALE))
+ self.fixed_positions[icon] = (x, y)
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ raise NotImplementedError()
+
+ def allow_dnd(self):
+ return False
+
+class RandomLayout(FavoritesLayout):
+ """Lay out icons randomly; try to nudge them around to resolve overlaps."""
+
+ __gtype_name__ = 'RandomLayout'
+
+ icon_name = 'view-freeform'
+ """Name of icon used in home view dropdown palette."""
+
+ profile_key = 'random-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the freeform layout in the favorites view
+ palette_name = _('Freeform')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ FavoritesLayout.__init__(self)
+
+ min_width_, width = self.do_get_width_request()
+ min_height_, height = self.do_get_height_request(width)
+
+ self._grid = Grid(width / _CELL_SIZE, height / _CELL_SIZE)
+ self._grid.connect('child-changed', self.__grid_child_changed_cb)
+
+ def __grid_child_changed_cb(self, grid, child):
+ child.emit_request_changed()
+
+ def append(self, icon, locked=False):
+ FavoritesLayout.append(self, icon, locked)
+
+ min_width_, child_width = icon.get_width_request()
+ min_height_, child_height = icon.get_height_request(child_width)
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+
+ if icon in self.fixed_positions:
+ x, y = self.fixed_positions[icon]
+ x = min(x, width - child_width)
+ y = min(y, height - child_height)
+ elif hasattr(icon, 'get_bundle_id'):
+ name_hash = hashlib.md5(icon.get_bundle_id())
+ x = int(name_hash.hexdigest()[:5], 16) % (width - child_width)
+ y = int(name_hash.hexdigest()[-5:], 16) % (height - child_height)
+ else:
+ x = None
+ y = None
+
+ if x is None or y is None:
+ self._grid.add(icon,
+ child_width / _CELL_SIZE, child_height / _CELL_SIZE)
+ else:
+ self._grid.add(icon,
+ child_width / _CELL_SIZE, child_height / _CELL_SIZE,
+ x / _CELL_SIZE, y / _CELL_SIZE)
+
+ def remove(self, icon):
+ self._grid.remove(icon)
+ FavoritesLayout.remove(self, icon)
+
+ def move_icon(self, icon, x, y, locked=False):
+ self._grid.move(icon, x / _CELL_SIZE, y / _CELL_SIZE, locked)
+ FavoritesLayout.move_icon(self, icon, x, y, locked)
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ for child in self.box.get_layout_children():
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ rect = self._grid.get_child_rect(child.item)
+ child.allocate(rect.x * _CELL_SIZE,
+ rect.y * _CELL_SIZE,
+ child_width,
+ child_height,
+ origin_changed)
+
+ def allow_dnd(self):
+ return True
+
+_MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING + \
+ style.STANDARD_ICON_SIZE * 2
+_MAXIMUM_RADIUS = (gtk.gdk.screen_height() - style.GRID_CELL_SIZE) / 2 - \
+ style.STANDARD_ICON_SIZE - style.DEFAULT_SPACING
+
+class RingLayout(FavoritesLayout):
+ """Lay out icons in a ring around the XO man."""
+
+ __gtype_name__ = 'RingLayout'
+ icon_name = 'view-radial'
+ """Name of icon used in home view dropdown palette."""
+ profile_key = 'ring-layout'
+ """String used in profile to represent this view."""
+ # TRANS: label for the ring layout in the favorites view
+ palette_name = _('Ring')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ FavoritesLayout.__init__(self)
+ self._locked_children = {}
+
+ def append(self, icon, locked=False):
+ FavoritesLayout.append(self, icon, locked)
+ if locked:
+ child = self.box.find_box_child(icon)
+ self._locked_children[child] = (0, 0)
+
+ def remove(self, icon):
+ child = self.box.find_box_child(icon)
+ if child in self._locked_children:
+ del self._locked_children[child]
+ FavoritesLayout.remove(self, icon)
+
+ def move_icon(self, icon, x, y, locked=False):
+ FavoritesLayout.move_icon(self, icon, x, y, locked)
+ if locked:
+ child = self.box.find_box_child(icon)
+ self._locked_children[child] = (x, y)
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ # what's the radius required without downscaling?
+ distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
+ icon_size = style.STANDARD_ICON_SIZE
+ # circumference is 2*pi*r; we want this to be at least
+ # 'children_count * distance'
+ radius = children_count * distance / (2 * math.pi)
+ # limit computed radius to reasonable bounds.
+ radius = max(radius, _MINIMUM_RADIUS)
+ radius = min(radius, _MAXIMUM_RADIUS)
+ # recompute icon size from limited radius
+ if children_count > 0:
+ icon_size = (2 * math.pi * radius / children_count) \
+ - style.DEFAULT_SPACING
+ # limit adjusted icon size.
+ icon_size = max(icon_size, style.SMALL_ICON_SIZE)
+ icon_size = min(icon_size, style.MEDIUM_ICON_SIZE)
+ return radius, icon_size
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=math.sin, cos=math.cos):
+ width, height = self.box.get_allocation()
+ angle = index * (2 * math.pi / children_count) - math.pi / 2
+ x = radius * cos(angle) + (width - icon_size) / 2
+ y = radius * sin(angle) + (height - icon_size -
+ (style.GRID_CELL_SIZE/2) ) / 2
+ return x, y
+
+ def _get_children_in_ring(self):
+ children_in_ring = [child for child in self.box.get_layout_children() \
+ if child not in self._locked_children]
+ return children_in_ring
+
+ def _update_icon_sizes(self):
+ # XXX: THIS METHOD IS NEVER CALLED
+ children_in_ring = self._get_children_in_ring()
+ radius_, icon_size = \
+ self._calculate_radius_and_icon_size(len(children_in_ring))
+
+ for child in children_in_ring:
+ child.item.props.size = icon_size
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ children_in_ring = self._get_children_in_ring()
+ if children_in_ring:
+ radius, icon_size = \
+ self._calculate_radius_and_icon_size(len(children_in_ring))
+
+ for n in range(len(children_in_ring)):
+ child = children_in_ring[n]
+
+ x, y = self._calculate_position(radius, icon_size, n,
+ len(children_in_ring))
+
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ child.allocate(int(x), int(y), child_width, child_height,
+ origin_changed)
+ child.item.props.size = icon_size
+
+ for child in self._locked_children.keys():
+ x, y = self._locked_children[child]
+
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ child.allocate(int(x), int(y), child_width, child_height,
+ origin_changed)
+
+ def compare_activities(self, icon_a, icon_b):
+ if hasattr(icon_a, 'installation_time') and \
+ hasattr(icon_b, 'installation_time'):
+ return icon_b.installation_time - icon_a.installation_time
+ else:
+ return 0
+
+_SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75
+"""Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced."""
+
+_SUNFLOWER_OFFSET = \
+ math.pow((style.XLARGE_ICON_SIZE / 2 + style.STANDARD_ICON_SIZE) /
+ _SUNFLOWER_CONSTANT, 2)
+"""
+Compute a starting index for the `SunflowerLayout` which leaves space for
+the XO man in the center. Since r = _SUNFLOWER_CONSTANT * sqrt(n),
+solve for n when r is (XLARGE_ICON_SIZE + STANDARD_ICON_SIZE)/2.
+"""
+
+_GOLDEN_RATIO = 1.6180339887498949
+"""
+Golden ratio: http://en.wikipedia.org/wiki/Golden_ratio
+Calculation: (math.sqrt(5) + 1) / 2
+"""
+
+_SUNFLOWER_ANGLE = 2.3999632297286531
+"""
+The sunflower angle is approximately 137.5 degrees.
+This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle
+Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO )
+"""
+
+class SunflowerLayout(RingLayout):
+ """Spiral layout based on Fibonacci ratio in phyllotaxis.
+
+ See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf
+ for details of Vogel's model of florets in a sunflower head."""
+
+ __gtype_name__ = 'SunflowerLayout'
+
+ icon_name = 'view-spiral'
+ """Name of icon used in home view dropdown palette."""
+
+ profile_key = 'spiral-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the spiral layout in the favorites view
+ palette_name = _('Spiral')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+ self.skipped_indices = []
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ """Stub out this method; not used in `SunflowerLayout`."""
+ return None, style.STANDARD_ICON_SIZE
+
+ def adjust_index(self, i):
+ """Skip floret indices which end up outside the desired bounding box."""
+ for idx in self.skipped_indices:
+ if i < idx:
+ break
+ i += 1
+ return i
+
+ def _calculate_position(self, radius, icon_size, oindex, children_count,
+ sin=math.sin, cos=math.cos):
+ """Calculate the position of sunflower floret number 'oindex'.
+ If the result is outside the bounding box, use the next index which
+ is inside the bounding box."""
+
+ width, height = self.box.get_allocation()
+
+ while True:
+
+ index = self.adjust_index(oindex)
+
+ # tweak phi to get a nice gap lined up where the "active activity"
+ # icon is, below the central XO man.
+ phi = index * _SUNFLOWER_ANGLE + math.radians(-130)
+
+ # we offset index when computing r to make space for the XO man.
+ r = _SUNFLOWER_CONSTANT * math.sqrt(index + _SUNFLOWER_OFFSET)
+
+ # x,y are the top-left corner of the icon, so remove icon_size
+ # from width/height to compensate. y has an extra GRID_CELL_SIZE/2
+ # removed to make room for the "active activity" icon.
+ x = r * cos(phi) + (width - icon_size) / 2
+ y = r * sin(phi) + (height - icon_size - \
+ (style.GRID_CELL_SIZE / 2) ) / 2
+
+ # skip allocations outside the allocation box.
+ # give up once we can't fit
+ if r < math.hypot(width / 2, height / 2):
+ if y < 0 or y > (height - icon_size) or \
+ x < 0 or x > (width - icon_size):
+ self.skipped_indices.append(index)
+ continue # try again
+
+ return x, y
+
+class BoxLayout(RingLayout):
+ """Lay out icons in a square around the XO man."""
+
+ __gtype_name__ = 'BoxLayout'
+
+ icon_name = 'view-box'
+ """Name of icon used in home view dropdown palette."""
+
+ profile_key = 'box-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the box layout in the favorites view
+ palette_name = _('Box')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=None, cos=None):
+
+ # use "orthogonal" versions of cos and sin in order to square the
+ # circle and turn the 'ring view' into a 'box view'
+ def cos_d(d):
+ while d < 0:
+ d += 360
+ if d < 45:
+ return 1
+ if d < 135:
+ return (90 - d) / 45.
+ if d < 225:
+ return -1
+ return cos_d(360 - d) # mirror around 180
+
+ cos = lambda r: cos_d(math.degrees(r))
+ sin = lambda r: cos_d(math.degrees(r) - 90)
+
+ return RingLayout._calculate_position\
+ (self, radius, icon_size, index, children_count,
+ sin=sin, cos=cos)
+
+class TriangleLayout(RingLayout):
+ """Lay out icons in a triangle around the XO man."""
+
+ __gtype_name__ = 'TriangleLayout'
+
+ icon_name = 'view-triangle'
+ """Name of icon used in home view dropdown palette."""
+
+ profile_key = 'triangle-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the box layout in the favorites view
+ palette_name = _('Triangle')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ # use slightly larger minimum radius than parent, because sides
+ # of triangle come awful close to the center.
+ radius, icon_size = \
+ RingLayout._calculate_radius_and_icon_size(self, children_count)
+ return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=math.sin, cos=math.cos):
+ # tweak cos and sin in order to make the 'ring' into an equilateral
+ # triangle.
+
+ def cos_d(d):
+ while d < -90:
+ d += 360
+ if d <= 30:
+ return (d + 90) / 120.
+ if d <= 90:
+ return (90 - d) / 60.
+ return -cos_d(180 - d) # mirror around 90
+
+ sqrt_3 = math.sqrt(3)
+
+ def sin_d(d):
+ while d < -90:
+ d += 360
+ if d <= 30:
+ return ((d + 90) / 120.) * sqrt_3 - 1
+ if d <= 90:
+ return sqrt_3 - 1
+ return sin_d(180 - d) # mirror around 90
+
+ cos = lambda r: cos_d(math.degrees(r))
+ sin = lambda r: sin_d(math.degrees(r))
+
+ return RingLayout._calculate_position\
+ (self, radius, icon_size, index, children_count,
+ sin=sin, cos=cos)
diff --git a/src/jarabe/view/home/favoritesview.py b/src/jarabe/view/home/favoritesview.py
new file mode 100644
index 0000000..3dae714
--- /dev/null
+++ b/src/jarabe/view/home/favoritesview.py
@@ -0,0 +1,456 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2008 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
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar.graphics.palette import Palette
+from sugar.graphics.icon import Icon, CanvasIcon
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.alert import Alert
+from sugar.profile import get_profile
+from sugar import activity
+
+import view.Shell
+from jarabe.view.palettes import JournalPalette
+from jarabe.view.palettes import CurrentActivityPalette, ActivityPalette
+from jarabe.view.home.MyIcon import MyIcon
+from jarabe.view.home import favoriteslayout
+from jarabe.model import shellmodel
+from jarabe.hardware import schoolserver
+from jarabe.hardware.schoolserver import RegisterError
+from jarabe.controlpanel.gui import ControlPanel
+from session import get_session_manager
+
+_logger = logging.getLogger('FavoritesView')
+
+_ICON_DND_TARGET = ('activity-icon', gtk.TARGET_SAME_WIDGET, 0)
+
+# enumerate the various layout types we will display in the dropdown palette.
+# add a constant for your layout here, and add it to the LAYOUT_MAP to get
+# it to appear in the palette.
+RING_LAYOUT, BOX_LAYOUT, TRIANGLE_LAYOUT, SUNFLOWER_LAYOUT, RANDOM_LAYOUT = \
+ xrange(5)
+
+LAYOUT_MAP = {RING_LAYOUT: favoriteslayout.RingLayout,
+ #BOX_LAYOUT: favoriteslayout.BoxLayout,
+ #TRIANGLE_LAYOUT: favoriteslayout.TriangleLayout,
+ #SUNFLOWER_LAYOUT: favoriteslayout.SunflowerLayout,
+ RANDOM_LAYOUT: favoriteslayout.RandomLayout}
+"""Map numeric layout identifiers to uninstantiated subclasses of
+`FavoritesLayout` which implement the layouts. Additional information
+about the layout can be accessed with fields of the class."""
+
+class FavoritesView(hippo.Canvas):
+ __gtype_name__ = 'SugarFavoritesView'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self, **kwargs):
+ gobject.GObject.__init__(self, **kwargs)
+
+ # DND stuff
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._last_clicked_icon = None
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._my_icon = None
+ self._current_activity = None
+ self._layout = None
+ self._alert = None
+
+ registry = activity.get_registry()
+ registry.connect('activity-added', self.__activity_added_cb)
+ registry.connect('activity-removed', self.__activity_removed_cb)
+ registry.connect('activity-changed', self.__activity_changed_cb)
+
+ # More DND stuff
+ self.add_events(gtk.gdk.BUTTON_PRESS_MASK |
+ gtk.gdk.POINTER_MOTION_HINT_MASK)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+ self.connect('button-press-event', self.__button_press_event_cb)
+ self.connect('drag-begin', self.__drag_begin_cb)
+ self.connect('drag-motion', self.__drag_motion_cb)
+ self.connect('drag-drop', self.__drag_drop_cb)
+ self.connect('drag-data-received', self.__drag_data_received_cb)
+
+ def _add_activity(self, activity_info):
+ icon = ActivityIcon(activity_info)
+ icon.connect('erase-activated', self.__erase_activated_cb)
+ icon.props.size = style.STANDARD_ICON_SIZE
+ self._layout.append(icon)
+
+ def __erase_activated_cb(self, activity_icon, bundle_id):
+ self.emit('erase-activated', bundle_id)
+
+ def _get_activities_cb(self, activity_list):
+ for info in activity_list:
+ if info.favorite and info.bundle_id != "org.laptop.JournalActivity":
+ self._add_activity(info)
+
+ def __activity_added_cb(self, activity_registry, activity_info):
+ if activity_info.favorite and \
+ activity_info.bundle_id != "org.laptop.JournalActivity":
+ self._add_activity(activity_info)
+
+ def _find_activity_icon(self, bundle_id, version):
+ for icon in self._box.get_children():
+ if isinstance(icon, ActivityIcon) and \
+ icon.bundle_id == bundle_id and icon.version == version:
+ return icon
+ return None
+
+ def __activity_removed_cb(self, activity_registry, activity_info):
+ icon = self._find_activity_icon(activity_info.bundle_id,
+ activity_info.version)
+ if icon is not None:
+ self._layout.remove(icon)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ if activity_info.bundle_id == 'org.laptop.JournalActivity':
+ return
+ icon = self._find_activity_icon(activity_info.bundle_id,
+ activity_info.version)
+ if icon is not None:
+ self._box.remove(icon)
+ if activity_info.favorite:
+ self._add_activity(activity_info)
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, my_icon_width = self._my_icon.get_width_request()
+ min_h_, my_icon_height = self._my_icon.get_height_request(my_icon_width)
+ x = (width - my_icon_width) / 2
+ y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2
+ self._layout.move_icon(self._my_icon, x, y, locked=True)
+
+ min_w_, icon_width = self._current_activity.get_width_request()
+ min_h_, icon_height = \
+ self._current_activity.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + \
+ my_icon_height + style.DEFAULT_PADDING
+ self._layout.move_icon(self._current_activity, x, y, locked=True)
+
+ hippo.Canvas.do_size_allocate(self, allocation)
+
+ def enable_xo_palette(self):
+ self._my_icon.enable_palette()
+ if self._my_icon.register_menu is not None:
+ self._my_icon.register_menu.connect('activate',
+ self.__register_activate_cb)
+
+ # TODO: Dnd methods. This should be merged somehow inside hippo-canvas.
+ def __button_press_event_cb(self, widget, event):
+ if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS:
+ self._last_clicked_icon = self._get_icon_at_coords(event.x, event.y)
+ if self._last_clicked_icon is not None:
+ self._pressed_button = event.button
+ self._press_start_x = event.x
+ self._press_start_y = event.y
+
+ return False
+
+ def _get_icon_at_coords(self, x, y):
+ for icon in self._box.get_children():
+ icon_x, icon_y = icon.get_context().translate_to_widget(icon)
+ icon_width, icon_height = icon.get_allocation()
+
+ if (x >= icon_x ) and (x <= icon_x + icon_width) and \
+ (y >= icon_y ) and (y <= icon_y + icon_height) and \
+ isinstance(icon, ActivityIcon):
+ return icon
+ return None
+
+ def __motion_notify_event_cb(self, widget, event):
+ if not self._pressed_button:
+ return False
+
+ # if the mouse button is not pressed, no drag should occurr
+ if not event.state & gtk.gdk.BUTTON1_MASK:
+ self._pressed_button = None
+ return False
+
+ if event.is_hint:
+ x, y, state_ = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+
+ if widget.drag_check_threshold(int(self._press_start_x),
+ int(self._press_start_y),
+ int(x),
+ int(y)):
+ context_ = widget.drag_begin([_ICON_DND_TARGET],
+ gtk.gdk.ACTION_MOVE,
+ 1,
+ event)
+ return False
+
+ def __drag_begin_cb(self, widget, context):
+ icon_file_name = self._last_clicked_icon.props.file_name
+ # TODO: we should get the pixbuf from the widget, so it has colors, etc
+ pixbuf = gtk.gdk.pixbuf_new_from_file(icon_file_name)
+
+ hot_spot = style.zoom(10)
+ context.set_icon_pixbuf(pixbuf, hot_spot, hot_spot)
+
+ def __drag_motion_cb(self, widget, context, x, y, time):
+ if self._last_clicked_icon is not None:
+ context.drag_status(context.suggested_action, time)
+ return True
+ else:
+ return False
+
+ def __drag_drop_cb(self, widget, context, x, y, time):
+ if self._last_clicked_icon is not None:
+ self.drag_get_data(context, _ICON_DND_TARGET[0])
+
+ self._layout.move_icon(self._last_clicked_icon, x, y)
+
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._last_clicked_icon = None
+
+ return True
+ else:
+ return False
+
+ def __drag_data_received_cb(self, widget, context, x, y, selection_data,
+ info, time):
+ context.drop_finish(success=True, time=time)
+
+ def _set_layout(self, layout):
+ if layout not in LAYOUT_MAP:
+ raise ValueError('Unknown favorites layout: %r' % layout)
+ if type(self._layout) != LAYOUT_MAP[layout]:
+ self._box.clear()
+ self._layout = LAYOUT_MAP[layout]()
+ self._box.set_layout(self._layout)
+
+ self._my_icon = _MyIcon(style.XLARGE_ICON_SIZE)
+ self._layout.append(self._my_icon, locked=True)
+
+ self._current_activity = CurrentActivityIcon()
+ self._layout.append(self._current_activity, locked=True)
+
+ registry = activity.get_registry()
+ registry.get_activities_async(reply_handler=self._get_activities_cb)
+
+ if self._layout.allow_dnd():
+ self.drag_source_set(0, [], 0)
+ self.drag_dest_set(0, [], 0)
+ else:
+ self.drag_source_unset()
+ self.drag_dest_unset()
+
+ layout = property(None, _set_layout)
+
+ def add_alert(self, alert):
+ if self._alert is not None:
+ self.remove_alert()
+ alert.set_size_request(gtk.gdk.screen_width(), -1)
+ self._alert = hippo.CanvasWidget(widget=alert)
+ self._box.append(self._alert, hippo.PACK_FIXED)
+
+ def remove_alert(self):
+ self._box.remove(self._alert)
+ self._alert = None
+
+ def __register_activate_cb(self, menuitem):
+ alert = Alert()
+ try:
+ schoolserver.register_laptop()
+ except RegisterError, e:
+ alert.props.title = _('Registration Failed')
+ alert.props.msg = _('%s') % e
+ else:
+ alert.props.title = _('Registration Successful')
+ alert.props.msg = _('You are now registered ' \
+ 'with your school server.')
+ palette = self._my_icon.get_palette()
+ palette.menu.remove(menuitem)
+
+ ok_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon)
+
+ self.add_alert(alert)
+ alert.connect('response', self.__register_alert_response_cb)
+
+ def __register_alert_response_cb(self, alert, response_id):
+ self.remove_alert()
+
+class ActivityIcon(CanvasIcon):
+ __gtype_name__ = 'SugarFavoriteActivityIcon'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self, activity_info):
+ CanvasIcon.__init__(self, cache=True, file_name=activity_info.icon)
+ self._activity_info = activity_info
+ self._uncolor()
+ self.connect('hovering-changed', self.__hovering_changed_event_cb)
+ self.connect('button-release-event', self.__button_release_event_cb)
+
+ def create_palette(self):
+ palette = ActivityPalette(self._activity_info)
+ palette.connect('erase-activated', self.__erase_activated_cb)
+ return palette
+
+ def __erase_activated_cb(self, palette):
+ self.emit('erase-activated', self._activity_info.bundle_id)
+
+ def _color(self):
+ self.props.xo_color = get_profile().color
+
+ def _uncolor(self):
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
+ def __hovering_changed_event_cb(self, icon, hovering):
+ if hovering:
+ self._color()
+ else:
+ self._uncolor()
+
+ def __button_release_event_cb(self, icon, event):
+ self.palette.popdown(immediate=True)
+ self._uncolor()
+ view.Shell.get_instance().start_activity(self._activity_info.bundle_id)
+
+ def get_bundle_id(self):
+ return self._activity_info.bundle_id
+ bundle_id = property(get_bundle_id, None)
+
+ def get_version(self):
+ return self._activity_info.version
+ version = property(get_version, None)
+
+ def _get_installation_time(self):
+ return self._activity_info.installation_time
+ installation_time = property(_get_installation_time, None)
+
+ def _get_fixed_position(self):
+ return self._activity_info.position
+ fixed_position = property(_get_fixed_position, None)
+
+class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem):
+ def __init__(self):
+ CanvasIcon.__init__(self, cache=True)
+ self._home_model = shellmodel.get_instance().get_home()
+
+ if self._home_model.get_active_activity() is not None:
+ self._update(self._home_model.get_active_activity())
+
+ self._home_model.connect('active-activity-changed',
+ self.__active_activity_changed_cb)
+
+ self.connect('button-release-event', self.__button_release_event_cb)
+
+ def __button_release_event_cb(self, icon, event):
+ self._home_model.get_active_activity().get_window().activate(1)
+
+ def _update(self, home_activity):
+ self.props.file_name = home_activity.get_icon_path()
+ self.props.xo_color = home_activity.get_icon_color()
+ self.props.size = style.STANDARD_ICON_SIZE
+
+ if self.palette is not None:
+ self.palette.destroy()
+ self.palette = None
+
+ if home_activity.is_journal():
+ palette = JournalPalette(home_activity)
+ else:
+ palette = CurrentActivityPalette(home_activity)
+ self.set_palette(palette)
+
+ def __active_activity_changed_cb(self, home_model, home_activity):
+ self._update(home_activity)
+
+class _MyIcon(MyIcon):
+ def __init__(self, scale):
+ MyIcon.__init__(self, scale)
+
+ self._power_manager = None
+ self._profile = get_profile()
+ self.register_menu = None
+
+ def enable_palette(self):
+ palette_icon = Icon(icon_name='computer-xo',
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR,
+ xo_color=self._profile.color)
+ palette = Palette(self._profile.nick_name,
+ #secondary_text='Sample secondary label',
+ icon=palette_icon)
+
+ item = MenuItem(_('Settings'), 'preferences-system')
+ item.connect('activate', self.__controlpanel_activate_cb)
+ palette.menu.append(item)
+ item.show()
+
+ item = MenuItem(_('Restart'), 'system-restart')
+ item.connect('activate', self._reboot_activate_cb)
+ palette.menu.append(item)
+ item.show()
+
+ item = MenuItem(_('Shutdown'), 'system-shutdown')
+ item.connect('activate', self._shutdown_activate_cb)
+ palette.menu.append(item)
+ item.show()
+
+ if not self._profile.is_registered():
+ self.register_menu = MenuItem(_('Register'), 'media-record')
+ palette.menu.append(self.register_menu)
+ self.register_menu.show()
+
+ self.set_palette(palette)
+
+ def _reboot_activate_cb(self, menuitem):
+ session_manager = get_session_manager()
+ session_manager.reboot()
+
+ def _shutdown_activate_cb(self, menuitem):
+ session_manager = get_session_manager()
+ session_manager.shutdown()
+
+ def get_toplevel(self):
+ return hippo.get_canvas_for_item(self).get_toplevel()
+
+ def __controlpanel_activate_cb(self, menuitem):
+ panel = ControlPanel()
+ panel.set_transient_for(self.get_toplevel())
+ panel.show()
diff --git a/src/jarabe/view/home/grid.py b/src/jarabe/view/home/grid.py
new file mode 100644
index 0000000..eb7fabc
--- /dev/null
+++ b/src/jarabe/view/home/grid.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2007 Red Hat, Inc.
+# Copyright (C) 2008 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 random
+
+import gobject
+import gtk
+
+from sugar import _sugarext
+
+_PLACE_TRIALS = 20
+_MAX_WEIGHT = 255
+_REFRESH_RATE = 200
+_MAX_COLLISIONS_PER_REFRESH = 20
+
+class Grid(_sugarext.Grid):
+ __gsignals__ = {
+ 'child-changed' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+ def __init__(self, width, height):
+ gobject.GObject.__init__(self)
+
+ self.width = width
+ self.height = height
+ self._children = []
+ self._child_rects = {}
+ self._locked_children = set()
+ self._collisions = []
+ self._collisions_sid = 0
+
+ self.setup(self.width, self.height)
+
+ def add(self, child, width, height, x=None, y=None, locked=False):
+ if x is not None and y is not None:
+ rect = gtk.gdk.Rectangle(x, y, width, height)
+ weight = self.compute_weight(rect)
+ else:
+ trials = _PLACE_TRIALS
+ weight = _MAX_WEIGHT
+ while trials > 0 and weight:
+ x = int(random.random() * (self.width - width))
+ y = int(random.random() * (self.height - height))
+
+ rect = gtk.gdk.Rectangle(x, y, width, height)
+ new_weight = self.compute_weight(rect)
+ if weight > new_weight:
+ weight = new_weight
+
+ trials -= 1
+
+ self._child_rects[child] = rect
+ self._children.append(child)
+ self.add_weight(self._child_rects[child])
+ if locked:
+ self._locked_children.add(child)
+
+ if weight > 0:
+ self._detect_collisions(child)
+
+ def remove(self, child):
+ self._children.remove(child)
+ self.remove_weight(self._child_rects[child])
+ self._locked_children.discard(child)
+ del self._child_rects[child]
+
+ if child in self._collisions:
+ self._collisions.remove(child)
+
+ def move(self, child, x, y, locked=False):
+ self.remove_weight(self._child_rects[child])
+
+ rect = self._child_rects[child]
+ rect.x = x
+ rect.y = y
+
+ weight = self.compute_weight(rect)
+ self.add_weight(self._child_rects[child])
+
+ if locked:
+ self._locked_children.add(child)
+ else:
+ self._locked_children.discard(child)
+
+ if weight > 0:
+ self._detect_collisions(child)
+
+ def _shift_child(self, child, weight):
+ rect = self._child_rects[child]
+
+ new_rects = []
+
+ # Get rects right, left, bottom and top
+ if (rect.x + rect.width < self.width - 1):
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y,
+ rect.width, rect.height))
+
+ if (rect.x - 1 > 0):
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y,
+ rect.width, rect.height))
+
+ if (rect.y + rect.height < self.height - 1):
+ new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1,
+ rect.width, rect.height))
+
+ if (rect.y - 1 > 0):
+ new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1,
+ rect.width, rect.height))
+
+ # Get diagonal rects
+ if rect.x + rect.width < self.width - 1 and \
+ rect.y + rect.height < self.height - 1:
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y + 1,
+ rect.width, rect.height))
+
+ if rect.x - 1 > 0 and rect.y + rect.height < self.height - 1:
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y + 1,
+ rect.width, rect.height))
+
+ if rect.x + rect.width < self.width - 1 and rect.y - 1 > 0:
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y - 1,
+ rect.width, rect.height))
+
+ if rect.x - 1 > 0 and rect.y - 1 > 0:
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y - 1,
+ rect.width, rect.height))
+
+ random.shuffle(new_rects)
+
+ best_rect = None
+ for new_rect in new_rects:
+ new_weight = self.compute_weight(new_rect)
+ if new_weight < weight:
+ best_rect = new_rect
+ weight = new_weight
+
+ if best_rect:
+ self._child_rects[child] = best_rect
+ weight = self._shift_child(child, weight)
+
+ return weight
+
+ def __solve_collisions_cb(self):
+ for i_ in range(_MAX_COLLISIONS_PER_REFRESH):
+ collision = self._collisions.pop(0)
+
+ old_rect = self._child_rects[collision]
+ self.remove_weight(old_rect)
+ weight = self.compute_weight(old_rect)
+ weight = self._shift_child(collision, weight)
+ self.add_weight(self._child_rects[collision])
+
+ # TODO: we shouldn't give up the first time we failed to find a
+ # better position.
+ if old_rect != self._child_rects[collision]:
+ self._detect_collisions(collision)
+ self.emit('child-changed', collision)
+ if weight > 0:
+ self._collisions.append(collision)
+
+ if not self._collisions:
+ self._collisions_sid = 0
+ return False
+
+ return True
+
+ def _detect_collisions(self, child):
+ collision_found = False
+ child_rect = self._child_rects[child]
+ for c in self._children:
+ intersection = child_rect.intersect(self._child_rects[c])
+ if c != child and intersection.width > 0:
+ if c not in self._locked_children and c not in self._collisions:
+ collision_found = True
+ self._collisions.append(c)
+
+ if collision_found:
+ if child not in self._collisions:
+ self._collisions.append(child)
+
+ if self._collisions and not self._collisions_sid:
+ self._collisions_sid = gobject.timeout_add(_REFRESH_RATE,
+ self.__solve_collisions_cb, priority=gobject.PRIORITY_LOW)
+
+ def get_child_rect(self, child):
+ return self._child_rects[child]
diff --git a/src/jarabe/view/home/proc_smaps.py b/src/jarabe/view/home/proc_smaps.py
new file mode 100755
index 0000000..090a4cf
--- /dev/null
+++ b/src/jarabe/view/home/proc_smaps.py
@@ -0,0 +1,107 @@
+# 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 os
+
+# /proc/PID/maps consists of a number of lines like this:
+# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash
+# 006b1000-006bb000 rw-p 000b1000 fd:00 5767206 /bin/bash
+# 006bb000-006c0000 rw-p 006bb000 00:00 0
+# ...
+# The fields are: address, permissions, offset, device, inode, and
+# (for non-anonymous mappings) pathname.
+#
+# /proc/PID/smaps gives additional information for each mapping:
+# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash
+# Size: 708 kB
+# Rss: 476 kB
+# Shared_Clean: 468 kB
+# Shared_Dirty: 0 kB
+# Private_Clean: 8 kB
+# Private_Dirty: 0 kB
+# Referenced: 0 kb
+#
+# The "Referenced" line only appears in kernel 2.6.22 and later.
+
+def get_shared_mapping_names(pid):
+ """Returns a set of the files for which PID has a shared mapping"""
+
+ mappings = set()
+ infile = open("/proc/%s/maps" % pid, "r")
+ for line in infile:
+ # sharable mappings are non-anonymous and either read-only
+ # (permissions "r-..") or writable but explicitly marked
+ # shared ("rw.s")
+ fields = line.split()
+ if len(fields) < 6 or not fields[5].startswith('/'):
+ continue
+ if fields[1][0] != 'r' or (fields[1][1] == 'w' and fields[1][3] != 's'):
+ continue
+ mappings.add(fields[5])
+ infile.close()
+ return mappings
+
+_smaps_lines_per_entry = None
+
+def get_mappings(pid, ignored_shared_mappings):
+ """Returns a list of (name, private, shared) tuples describing the
+ memory mappings of PID. Shared mappings named in
+ ignored_shared_mappings are ignored
+ """
+
+ global _smaps_lines_per_entry
+ if _smaps_lines_per_entry is None:
+ if os.path.isfile('/proc/%s/clear_refs' % os.getpid()):
+ _smaps_lines_per_entry = 8
+ else:
+ _smaps_lines_per_entry = 7
+
+ mappings = []
+
+ smapfile = "/proc/%s/smaps" % pid
+ infile = open(smapfile, "r")
+ data = infile.read()
+ infile.close()
+ lines = data.splitlines()
+
+ for line_idx in range(0, len(lines), _smaps_lines_per_entry):
+ name_idx = lines[line_idx].find('/')
+ if name_idx == -1:
+ name = None
+ else:
+ name = lines[line_idx][name_idx:]
+
+ private_clean = int(lines[line_idx + 5][14:-3])
+ private_dirty = int(lines[line_idx + 6][14:-3])
+ if name in ignored_shared_mappings:
+ shared_clean = 0
+ shared_dirty = 0
+ else:
+ shared_clean = int(lines[line_idx + 3][14:-3])
+ shared_dirty = int(lines[line_idx + 4][14:-3])
+
+ mapping = Mapping(name, private_clean + private_dirty,
+ shared_clean + shared_dirty)
+ mappings.append (mapping)
+
+ return mappings
+
+class Mapping:
+ def __init__ (self, name, private, shared):
+ self.name = name
+ self.private = private
+ self.shared = shared
diff --git a/src/jarabe/view/home/snowflakelayout.py b/src/jarabe/view/home/snowflakelayout.py
new file mode 100644
index 0000000..5782cff
--- /dev/null
+++ b/src/jarabe/view/home/snowflakelayout.py
@@ -0,0 +1,108 @@
+# 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 math
+
+import gobject
+import hippo
+
+from sugar.graphics import style
+
+_BASE_DISTANCE = style.zoom(25)
+_CHILDREN_FACTOR = style.zoom(3)
+
+class SnowflakeLayout(gobject.GObject, hippo.CanvasLayout):
+ __gtype_name__ = 'SugarSnowflakeLayout'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._nflakes = 0
+ self._box = None
+
+ def add(self, child, center=False):
+ if not center:
+ self._nflakes += 1
+
+ self._box.append(child)
+
+ box_child = self._box.find_box_child(child)
+ box_child.is_center = center
+
+ def remove(self, child):
+ box_child = self._box.find_box_child(child)
+ if not box_child.is_center:
+ self._nflakes -= 1
+
+ self._box.remove(child)
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ size = self._calculate_size()
+ return (size, size)
+
+ def do_get_width_request(self):
+ size = self._calculate_size()
+ return (size, size)
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ r = self._get_radius()
+ index = 0
+
+ for child in self._box.get_layout_children():
+ min_width, child_width = child.get_width_request()
+ min_height, child_height = child.get_height_request(child_width)
+
+ if child.is_center:
+ child.allocate(x + (width - child_width) / 2,
+ y + (height - child_height) / 2,
+ child_width, child_height, origin_changed)
+ else:
+ angle = 2 * math.pi * index / self._nflakes
+
+ if self._nflakes != 2:
+ angle -= math.pi / 2
+
+ dx = math.cos(angle) * r
+ dy = math.sin(angle) * r
+
+ child_x = int(x + (width - child_width) / 2 + dx)
+ child_y = int(y + (height - child_height) / 2 + dy)
+
+ child.allocate(child_x, child_y, child_width,
+ child_height, origin_changed)
+
+ index += 1
+
+ def _get_radius(self):
+ radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes)
+ for child in self._box.get_layout_children():
+ if child.is_center:
+ [min_w, child_w] = child.get_width_request()
+ [min_h, child_h] = child.get_height_request(child_w)
+ radius += max(child_w, child_h) / 2
+
+ return radius
+
+ def _calculate_size(self):
+ thickness = 0
+ for child in self._box.get_layout_children():
+ [min_width, child_width] = child.get_width_request()
+ [min_height, child_height] = child.get_height_request(child_width)
+ thickness = max(thickness, max(child_width, child_height))
+
+ return self._get_radius() * 2 + thickness
diff --git a/src/jarabe/view/home/spreadlayout.py b/src/jarabe/view/home/spreadlayout.py
new file mode 100644
index 0000000..72261e7
--- /dev/null
+++ b/src/jarabe/view/home/spreadlayout.py
@@ -0,0 +1,83 @@
+# 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 hippo
+import gobject
+import gtk
+
+from sugar.graphics import style
+
+from jarabe.view.home.grid import Grid
+
+_CELL_SIZE = 4.0
+
+class SpreadLayout(gobject.GObject, hippo.CanvasLayout):
+ __gtype_name__ = 'SugarSpreadLayout'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._box = None
+
+ min_width, width = self.do_get_width_request()
+ min_height, height = self.do_get_height_request(width)
+
+ self._grid = Grid(int(width / _CELL_SIZE), int(height / _CELL_SIZE))
+ self._grid.connect('child-changed', self._grid_child_changed_cb)
+
+ def add(self, child):
+ self._box.append(child)
+
+ width, height = self._get_child_grid_size(child)
+ self._grid.add(child, width, height)
+
+ def remove(self, child):
+ self._grid.remove(child)
+ self._box.remove(child)
+
+ def move(self, child, x, y):
+ self._grid.move(child, x / _CELL_SIZE, y / _CELL_SIZE, locked=True)
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE
+
+ def do_get_width_request(self):
+ return 0, gtk.gdk.screen_width()
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ for child in self._box.get_layout_children():
+ # We need to always get requests to not confuse hippo
+ min_w, child_width = child.get_width_request()
+ min_h, child_height = child.get_height_request(child_width)
+
+ rect = self._grid.get_child_rect(child.item)
+ child.allocate(int(round(rect.x * _CELL_SIZE)),
+ int(round(rect.y * _CELL_SIZE)),
+ child_width,
+ child_height,
+ origin_changed)
+
+ def _get_child_grid_size(self, child):
+ min_width, width = child.get_width_request()
+ min_height, height = child.get_height_request(width)
+
+ return int(width / _CELL_SIZE), int(height / _CELL_SIZE)
+
+ def _grid_child_changed_cb(self, grid, child):
+ child.emit_request_changed()
+
diff --git a/src/jarabe/view/home/transitionbox.py b/src/jarabe/view/home/transitionbox.py
new file mode 100644
index 0000000..230b887
--- /dev/null
+++ b/src/jarabe/view/home/transitionbox.py
@@ -0,0 +1,97 @@
+# 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 hippo
+import gobject
+
+from sugar.graphics import style
+from sugar.graphics import animator
+
+from jarabe.view.home.MyIcon import MyIcon
+
+class _Animation(animator.Animation):
+ def __init__(self, icon, start_size, end_size):
+ animator.Animation.__init__(self, 0.0, 1.0)
+
+ self._icon = icon
+ self.start_size = start_size
+ self.end_size = end_size
+
+ def next_frame(self, current):
+ d = (self.end_size - self.start_size) * current
+ self._icon.props.size = self.start_size + d
+
+class _Layout(gobject.GObject, hippo.CanvasLayout):
+ __gtype_name__ = 'SugarTransitionBoxLayout'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._box = None
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, 0
+
+ def do_get_width_request(self):
+ return 0, 0
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ for child in self._box.get_layout_children():
+ min_width, child_width = child.get_width_request()
+ min_height, child_height = child.get_height_request(child_width)
+
+ child.allocate(x + (width - child_width) / 2,
+ y + (height - child_height) / 2,
+ child_width, child_height, origin_changed)
+
+class TransitionBox(hippo.Canvas):
+ __gtype_name__ = 'SugarTransitionBox'
+
+ __gsignals__ = {
+ 'completed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._size = style.XLARGE_ICON_SIZE
+
+ self._layout = _Layout()
+ self._box.set_layout(self._layout)
+
+ self._my_icon = MyIcon(self._size)
+ self._box.append(self._my_icon)
+
+ self._animator = animator.Animator(0.3)
+ self._animator.connect('completed', self._animation_completed_cb)
+
+ def _animation_completed_cb(self, anim):
+ self.emit('completed')
+
+ def set_size(self, size):
+ self._animator.remove_all()
+ self._animator.add(_Animation(self._my_icon, self._size, size))
+ self._animator.start()
+
+ self._size = size
+
diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py
new file mode 100644
index 0000000..d6b7565
--- /dev/null
+++ b/src/jarabe/view/keyhandler.py
@@ -0,0 +1,289 @@
+# 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 os
+import signal
+import logging
+import subprocess
+import errno
+
+import dbus
+import gtk
+
+from sugar._sugarext import KeyGrabber
+
+from jarabe.hardware import hardwaremanager
+import view.Shell
+from jarabe.view.tabbinghandler import TabbingHandler
+from jarabe.model.shellmodel import ShellModel
+
+_BRIGHTNESS_STEP = 2
+_VOLUME_STEP = hardwaremanager.VOL_CHANGE_INCREMENT_RECOMMENDATION
+_BRIGHTNESS_MAX = 15
+_VOLUME_MAX = 100
+_TABBING_MODIFIER = gtk.gdk.MOD1_MASK
+
+_actions_table = {
+ 'F1' : 'zoom_mesh',
+ 'F2' : 'zoom_friends',
+ 'F3' : 'zoom_home',
+ 'F4' : 'zoom_activity',
+ 'F9' : 'brightness_down',
+ 'F10' : 'brightness_up',
+ '<alt>F9' : 'brightness_min',
+ '<alt>F10' : 'brightness_max',
+ 'F11' : 'volume_down',
+ 'F12' : 'volume_up',
+ '<alt>F11' : 'volume_min',
+ '<alt>F12' : 'volume_max',
+ '<alt>1' : 'screenshot',
+ '0x93' : 'frame',
+ '0xEB' : 'rotate',
+ '<alt>Tab' : 'next_window',
+ '<alt><shift>Tab': 'previous_window',
+ '<alt>Escape' : 'close_window',
+ '0xDC' : 'open_search',
+# the following are intended for emulator users
+ '<alt><shift>f' : 'frame',
+ '<alt><shift>q' : 'quit_emulator',
+ '<alt><shift>o' : 'open_search',
+ '<alt><shift>r' : 'rotate',
+ '<alt><shift>s' : 'say_text',
+}
+
+J_DBUS_SERVICE = 'org.laptop.Journal'
+J_DBUS_PATH = '/org/laptop/Journal'
+J_DBUS_INTERFACE = 'org.laptop.Journal'
+
+SPEECH_DBUS_SERVICE = 'org.laptop.Speech'
+SPEECH_DBUS_PATH = '/org/laptop/Speech'
+SPEECH_DBUS_INTERFACE = 'org.laptop.Speech'
+
+class KeyHandler(object):
+ def __init__(self):
+ self._screen_rotation = 0
+ self._key_pressed = None
+ self._keycode_pressed = 0
+ self._keystate_pressed = 0
+ self._speech_proxy = None
+
+ self._key_grabber = KeyGrabber()
+ self._key_grabber.connect('key-pressed',
+ self._key_pressed_cb)
+ self._key_grabber.connect('key-released',
+ self._key_released_cb)
+
+ self._tabbing_handler = TabbingHandler(_TABBING_MODIFIER)
+
+ self._key_grabber.grab_keys(_actions_table.keys())
+
+ def _change_volume(self, step=None, value=None):
+ hw_manager = hardwaremanager.get_manager()
+
+ if step is not None:
+ volume = hw_manager.get_volume() + step
+ elif value is not None:
+ volume = value
+
+ volume = min(max(0, volume), _VOLUME_MAX)
+
+ hw_manager.set_volume(volume)
+ hw_manager.set_muted(volume == 0)
+
+ def _change_brightness(self, step=None, value=None):
+ hw_manager = hardwaremanager.get_manager()
+
+ if step is not None:
+ level = hw_manager.get_display_brightness() + step
+ elif value is not None:
+ level = value
+
+ level = min(max(0, level), _BRIGHTNESS_MAX)
+
+ hw_manager.set_display_brightness(level)
+ if level == 0:
+ hw_manager.set_display_mode(hardwaremanager.B_AND_W_MODE)
+ else:
+ hw_manager.set_display_mode(hardwaremanager.COLOR_MODE)
+
+ def _get_speech_proxy(self):
+ if self._speech_proxy is None:
+ bus = dbus.SessionBus()
+ speech_obj = bus.get_object(SPEECH_DBUS_SERVICE, SPEECH_DBUS_PATH,
+ follow_name_owner_changes=True)
+ self._speech_proxy = dbus.Interface(speech_obj,
+ SPEECH_DBUS_INTERFACE)
+ return self._speech_proxy
+
+ def _on_speech_err(self, ex):
+ logging.error("An error occurred with the ESpeak service: %r" % (ex, ))
+
+ def _primary_selection_cb(self, clipboard, text, user_data):
+ logging.debug('KeyHandler._primary_selection_cb: %r' % text)
+ if text:
+ self._get_speech_proxy().SayText(text, reply_handler=lambda: None, \
+ error_handler=self._on_speech_err)
+
+ def handle_say_text(self):
+ clipboard = gtk.clipboard_get(selection="PRIMARY")
+ clipboard.request_text(self._primary_selection_cb)
+
+ def handle_previous_window(self):
+ self._tabbing_handler.previous_activity()
+
+ def handle_next_window(self):
+ self._tabbing_handler.next_activity()
+
+ def handle_close_window(self):
+ view.Shell.get_instance().close_current_activity()
+
+ def handle_zoom_mesh(self):
+ view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_MESH)
+
+ def handle_zoom_friends(self):
+ view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_FRIENDS)
+
+ def handle_zoom_home(self):
+ view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_HOME)
+
+ def handle_zoom_activity(self):
+ view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_ACTIVITY)
+
+ def handle_brightness_max(self):
+ self._change_brightness(value=_BRIGHTNESS_MAX)
+
+ def handle_brightness_min(self):
+ self._change_brightness(value=0)
+
+ def handle_volume_max(self):
+ self._change_volume(value=_VOLUME_MAX)
+
+ def handle_volume_min(self):
+ self._change_volume(value=0)
+
+ def handle_brightness_up(self):
+ self._change_brightness(step=_BRIGHTNESS_STEP)
+
+ def handle_brightness_down(self):
+ self._change_brightness(step=-_BRIGHTNESS_STEP)
+
+ def handle_volume_up(self):
+ self._change_volume(step=_VOLUME_STEP)
+
+ def handle_volume_down(self):
+ self._change_volume(step=-_VOLUME_STEP)
+
+ def handle_screenshot(self):
+ view.Shell.get_instance().take_screenshot()
+
+ def handle_frame(self):
+ view.Shell.get_instance().get_frame().notify_key_press()
+
+
+ def handle_rotate(self):
+ """
+ Handles rotation of the display (using xrandr) and of the d-pad.
+
+ Notes: default mappings for keypad on MP
+ KP_Up 80
+ KP_Right 85
+ KP_Down 88
+ KP_Left 83
+ """
+
+ states = [ 'normal', 'left', 'inverted', 'right']
+ keycodes = (80, 85, 88, 83, 80, 85, 88, 83)
+ keysyms = ("KP_Up", "KP_Right", "KP_Down", "KP_Left")
+
+ self._screen_rotation -= 1
+ self._screen_rotation %= 4
+
+ actual_keycodes = keycodes[self._screen_rotation:self._screen_rotation
+ + 4]
+ # code_pairs now contains a mapping of keycode -> keysym in the current
+ # orientation
+ code_pairs = zip(actual_keycodes, keysyms)
+
+ # Using the mappings in code_pairs, we dynamically build up an xmodmap
+ # command to rotate the dpad keys.
+ argv = ['xmodmap']
+ for arg in [('-e', 'keycode %i = %s' % p) for p in code_pairs]:
+ argv.extend(arg)
+
+ # If either the xmodmap or xrandr command fails, check_call will fail
+ # with CalledProcessError, which we raise.
+ try:
+ subprocess.check_call(argv)
+ subprocess.check_call(['xrandr', '-o',
+ states[self._screen_rotation]])
+ except OSError, e:
+ if e.errno != errno.EINTR:
+ raise
+
+
+ def handle_quit_emulator(self):
+ if os.environ.has_key('SUGAR_EMULATOR_PID'):
+ pid = int(os.environ['SUGAR_EMULATOR_PID'])
+ os.kill(pid, signal.SIGTERM)
+
+ def focus_journal_search(self):
+ bus = dbus.SessionBus()
+ obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH)
+ journal = dbus.Interface(obj, J_DBUS_INTERFACE)
+ journal.FocusSearch()
+
+ def handle_open_search(self):
+ self.focus_journal_search()
+
+ def _key_pressed_cb(self, grabber, keycode, state):
+ key = grabber.get_key(keycode, state)
+ logging.debug('_key_pressed_cb: %i %i %s' % (keycode, state, key))
+ if key:
+ self._key_pressed = key
+ self._keycode_pressed = keycode
+ self._keystate_pressed = state
+
+ action = _actions_table[key]
+ if self._tabbing_handler.is_tabbing():
+ # Only accept window tabbing events, everything else
+ # cancels the tabbing operation.
+ if not action in ["next_window", "previous_window"]:
+ self._tabbing_handler.stop()
+ return True
+
+ method = getattr(self, 'handle_' + action)
+ method()
+
+ return True
+ else:
+ # If this is not a registered key, then cancel tabbing.
+ if self._tabbing_handler.is_tabbing():
+ if not grabber.is_modifier(keycode):
+ self._tabbing_handler.stop()
+ return True
+
+ return False
+
+ def _key_released_cb(self, grabber, keycode, state):
+ if self._tabbing_handler.is_tabbing():
+ # We stop tabbing and switch to the new window as soon as the
+ # modifier key is raised again.
+ if grabber.is_modifier(keycode, mask=_TABBING_MODIFIER):
+ self._tabbing_handler.stop()
+
+ return True
+ return False
+
diff --git a/src/jarabe/view/launchwindow.py b/src/jarabe/view/launchwindow.py
new file mode 100644
index 0000000..384e6ec
--- /dev/null
+++ b/src/jarabe/view/launchwindow.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2008, 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
+import gobject
+
+from sugar import wm
+from sugar.graphics import style
+from sugar.graphics import animator
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.model import shellmodel
+from jarabe.view.pulsingicon import CanvasPulsingIcon
+
+class LaunchWindow(hippo.CanvasWindow):
+ def __init__(self, home_activity):
+ gobject.GObject.__init__(
+ self, type_hint=gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
+
+ self._activity_id = home_activity.get_activity_id()
+ self._box = LaunchBox(home_activity)
+ self.set_root(self._box)
+
+ self.connect('realize', self.__realize_cb)
+
+ screen = gtk.gdk.screen_get_default()
+ screen.connect('size-changed', self.__size_changed_cb)
+
+ self._update_size()
+
+ def show(self):
+ self.present()
+ self._box.zoom_in()
+
+ def _update_size(self):
+ self.resize(gtk.gdk.screen_width(), gtk.gdk.screen_height())
+
+ def __realize_cb(self, widget):
+ wm.set_activity_id(widget.window, str(self._activity_id))
+ widget.window.property_change('_SUGAR_WINDOW_TYPE', 'STRING', 8,
+ gtk.gdk.PROP_MODE_REPLACE, 'launcher')
+
+ def __size_changed_cb(self, screen):
+ self._update_size()
+
+class LaunchBox(hippo.CanvasBox):
+ def __init__(self, home_activity):
+ gobject.GObject.__init__(self, orientation=hippo.ORIENTATION_VERTICAL,
+ background_color=style.COLOR_WHITE.get_int())
+
+ self._home_activity = home_activity
+ self._activity_icon = CanvasPulsingIcon(
+ file_name=home_activity.get_icon_path(),
+ pulse_color=home_activity.get_icon_color())
+ self.append(self._activity_icon, hippo.PACK_EXPAND)
+
+ # FIXME support non-xo colors in CanvasPulsingIcon
+ self._activity_icon.props.base_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+
+ self._animator = animator.Animator(1.0)
+
+ self._home = shellmodel.get_instance().get_home()
+ self._home.connect('active-activity-changed',
+ self.__active_activity_changed_cb)
+
+ self.connect('destroy', self.__destroy_cb)
+
+ def __destroy_cb(self, box):
+ self._home.disconnect_by_func(self.__active_activity_changed_cb)
+
+ def zoom_in(self):
+ self._activity_icon.props.size = style.STANDARD_ICON_SIZE
+
+ self._animator.remove_all()
+ self._animator.add(_Animation(self._activity_icon,
+ style.STANDARD_ICON_SIZE,
+ style.XLARGE_ICON_SIZE))
+ self._animator.start()
+ self._activity_icon.props.pulsing = True
+
+ def __active_activity_changed_cb(self, model, activity):
+ if activity == self._home_activity:
+ self._activity_icon.props.paused = False
+ else:
+ self._activity_icon.props.paused = True
+
+class _Animation(animator.Animation):
+ def __init__(self, icon, start_size, end_size):
+ animator.Animation.__init__(self, 0.0, 1.0)
+
+ self._icon = icon
+ self.start_size = start_size
+ self.end_size = end_size
+
+ def next_frame(self, current):
+ d = (self.end_size - self.start_size) * current
+ self._icon.props.size = self.start_size + d
diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py
new file mode 100644
index 0000000..416ff0f
--- /dev/null
+++ b/src/jarabe/view/palettes.py
@@ -0,0 +1,217 @@
+# Copyright (C) 2008 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 statvfs
+from gettext import gettext as _
+
+import gobject
+import gtk
+
+from sugar import env
+from sugar import profile
+from sugar import activity
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+import view.Shell
+
+class BasePalette(Palette):
+ def __init__(self, home_activity):
+ Palette.__init__(self)
+
+ if home_activity.props.launching:
+ home_activity.connect('notify::launching',
+ self._launching_changed_cb)
+ self.set_primary_text(_('Starting...'))
+ else:
+ self.setup_palette()
+
+ def _launching_changed_cb(self, home_activity, pspec):
+ if not home_activity.props.launching:
+ self.setup_palette()
+
+ def setup_palette(self):
+ raise NotImplementedError
+
+class CurrentActivityPalette(BasePalette):
+ def __init__(self, home_activity):
+ self._home_activity = home_activity
+ BasePalette.__init__(self, home_activity)
+
+ def setup_palette(self):
+ self.set_primary_text(self._home_activity.get_title())
+
+ menu_item = MenuItem(_('Resume'), 'activity-start')
+ menu_item.connect('activate', self.__resume_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: share-with, keep
+
+ separator = gtk.SeparatorMenuItem()
+ self.menu.append(separator)
+ separator.show()
+
+ menu_item = MenuItem(_('Stop'), 'activity-stop')
+ menu_item.connect('activate', self.__stop_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __resume_activate_cb(self, menu_item):
+ self._home_activity.get_window().activate(gtk.get_current_event_time())
+
+ def __stop_activate_cb(self, menu_item):
+ self._home_activity.get_window().close(1)
+
+
+class ActivityPalette(Palette):
+ __gtype_name__ = 'SugarActivityPalette'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ }
+
+ def __init__(self, activity_info):
+ activity_icon = Icon(file=activity_info.icon,
+ xo_color=profile.get_color(),
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+
+ Palette.__init__(self, primary_text=activity_info.name,
+ icon=activity_icon)
+
+ self._bundle_id = activity_info.bundle_id
+ self._version = activity_info.version
+ self._favorite = activity_info.favorite
+
+ menu_item = MenuItem(_('Start'), 'activity-start')
+ menu_item.connect('activate', self.__start_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: start-with
+
+ self._favorite_item = MenuItem('')
+ self._favorite_icon = Icon(icon_name='emblem-favorite',
+ icon_size=gtk.ICON_SIZE_MENU)
+ self._favorite_item.set_image(self._favorite_icon)
+ self._favorite_item.connect('activate',
+ self.__change_favorite_activate_cb)
+ self.menu.append(self._favorite_item)
+ self._favorite_item.show()
+
+ menu_item = MenuItem(_('Erase'), 'list-remove')
+ menu_item.connect('activate', self.__erase_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ registry = activity.get_registry()
+ self._activity_changed_sid = registry.connect('activity_changed',
+ self.__activity_changed_cb)
+ self._update_favorite_item()
+
+ self.connect('destroy', self.__destroy_cb)
+
+ def __destroy_cb(self, palette):
+ self.disconnect(self._activity_changed_sid)
+
+ def _update_favorite_item(self):
+ label = self._favorite_item.child
+ if self._favorite:
+ label.set_text(_('Remove favorite'))
+ xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ label.set_text(_('Make favorite'))
+ xo_color = profile.get_color()
+
+ self._favorite_icon.props.xo_color = xo_color
+
+ def __start_activate_cb(self, menu_item):
+ view.Shell.get_instance().start_activity(self._bundle_id)
+
+ def __change_favorite_activate_cb(self, menu_item):
+ registry = activity.get_registry()
+ registry.set_activity_favorite(self._bundle_id,
+ self._version,
+ not self._favorite)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ if activity_info.bundle_id == self._bundle_id and \
+ activity_info.version == self._version:
+ self._favorite = activity_info.favorite
+ self._update_favorite_item()
+
+ def __erase_activate_cb(self, menu_item):
+ self.emit('erase-activated')
+
+class JournalPalette(BasePalette):
+ def __init__(self, home_activity):
+ self._home_activity = home_activity
+ self._progress_bar = None
+ self._free_space_label = None
+
+ BasePalette.__init__(self, home_activity)
+
+ def setup_palette(self):
+ self.set_primary_text(self._home_activity.get_title())
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self._progress_bar = gtk.ProgressBar()
+ vbox.add(self._progress_bar)
+ self._progress_bar.show()
+
+ self._free_space_label = gtk.Label()
+ self._free_space_label.set_alignment(0.5, 0.5)
+ vbox.add(self._free_space_label)
+ self._free_space_label.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = MenuItem(_('Show contents'))
+
+ icon = Icon(file=self._home_activity.get_icon_path(),
+ icon_size=gtk.ICON_SIZE_MENU,
+ xo_color=self._home_activity.get_icon_color())
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__open_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __open_activate_cb(self, menu_item):
+ self._home_activity.get_window().activate(gtk.get_current_event_time())
+
+ def __popup_cb(self, palette):
+ # TODO: we should be able to ask the datastore this info, as that's the
+ # component that knows about mount points.
+ stat = os.statvfs(env.get_profile_path())
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+ total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS]
+
+ fraction = (total_space - free_space) / float(total_space)
+ self._progress_bar.props.fraction = fraction
+ self._free_space_label.props.label = _('%(free_space)d MB Free') % \
+ {'free_space': free_space / (1024 * 1024)}
+
diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py
new file mode 100644
index 0000000..499ae99
--- /dev/null
+++ b/src/jarabe/view/pulsingicon.py
@@ -0,0 +1,223 @@
+# Copyright (C) 2008 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 math
+
+import gobject
+
+from sugar.graphics.icon import Icon, CanvasIcon
+from sugar.graphics.style import Color
+
+_INTERVAL = 100
+_STEP = math.pi / 10 # must be a fraction of pi, for clean caching
+
+class Pulser(object):
+ def __init__(self, icon):
+ self._pulse_hid = None
+ self._icon = icon
+ self._level = 0
+ self._phase = 0
+
+ def start(self, restart=False):
+ if restart:
+ self._phase = 0
+ if self._pulse_hid is None:
+ self._pulse_hid = gobject.timeout_add(_INTERVAL, self.__pulse_cb)
+
+ def stop(self):
+ if self._pulse_hid is not None:
+ gobject.source_remove(self._pulse_hid)
+ self._pulse_hid = None
+ self._icon.xo_color = self._icon.base_color
+
+ def update(self):
+ if self._icon.pulsing:
+ base_color = self._icon.base_color
+ pulse_color = self._icon.pulse_color
+
+ base_stroke = self._get_as_rgba(base_color.get_stroke_color())
+ pulse_stroke = self._get_as_rgba(pulse_color.get_stroke_color())
+ base_fill = self._get_as_rgba(base_color.get_fill_color())
+ pulse_fill = self._get_as_rgba(pulse_color.get_fill_color())
+
+ self._icon.stroke_color = \
+ self._get_color(base_stroke, pulse_stroke).get_svg()
+ self._icon.fill_color = \
+ self._get_color(base_fill, pulse_fill).get_svg()
+ else:
+ self._icon.xo_color = self._icon.base_color
+
+ def _get_as_rgba(self, html_color):
+ if html_color == 'none':
+ return Color('#FFFFFF', alpha=1.0).get_rgba()
+ else:
+ return Color(html_color).get_rgba()
+
+ def _get_color(self, orig_color, target_color):
+ next_point = (orig_color[0] +
+ self._level * (target_color[0] - orig_color[0]),
+ orig_color[1] +
+ self._level * (target_color[1] - orig_color[1]),
+ orig_color[2] +
+ self._level * (target_color[2] - orig_color[2]))
+
+ return Color('#%02x%02x%02x' % (int(next_point[0] * 255),
+ int(next_point[1] * 255),
+ int(next_point[2] * 255)))
+
+ def __pulse_cb(self):
+ self._phase += _STEP
+ self._level = (math.sin(self._phase) + 1) / 2
+ self.update()
+
+ return True
+
+class PulsingIcon(Icon):
+ __gtype_name__ = 'SugarPulsingIcon'
+
+ def __init__(self, **kwargs):
+ self._pulser = Pulser(self)
+ self._base_color = None
+ self._pulse_color = None
+ self._paused = False
+ self._pulsing = False
+
+ Icon.__init__(self, **kwargs)
+
+ self._palette = None
+ self.connect('destroy', self.__destroy_cb)
+
+ def set_pulse_color(self, pulse_color):
+ self._pulse_color = pulse_color
+ self._pulser.update()
+
+ def get_pulse_color(self):
+ return self._pulse_color
+
+ pulse_color = gobject.property(
+ type=object, getter=get_pulse_color, setter=set_pulse_color)
+
+ def set_base_color(self, base_color):
+ self._base_color = base_color
+ self._pulser.update()
+
+ def get_base_color(self):
+ return self._base_color
+
+ base_color = gobject.property(
+ type=object, getter=get_base_color, setter=set_base_color)
+
+ def set_paused(self, paused):
+ self._paused = paused
+
+ if self._paused:
+ self._pulser.stop()
+ else:
+ self._pulser.start(restart=False)
+
+ def get_paused(self):
+ return self._paused
+
+ paused = gobject.property(
+ type=bool, default=False, getter=get_paused, setter=set_paused)
+
+ def set_pulsing(self, pulsing):
+ self._pulsing = pulsing
+
+ if self._pulsing:
+ self._pulser.start(restart=True)
+ else:
+ self._pulser.stop()
+
+ def get_pulsing(self):
+ return self._pulsing
+
+ pulsing = gobject.property(
+ type=bool, default=False, getter=get_pulsing, setter=set_pulsing)
+
+ def _get_palette(self):
+ return self._palette
+
+ def _set_palette(self, palette):
+ if self._palette is not None:
+ self._palette.props.invoker = None
+ self._palette = palette
+
+ palette = property(_get_palette, _set_palette)
+
+ def __destroy_cb(self, icon):
+ if self._palette is not None:
+ self._palette.destroy()
+
+class CanvasPulsingIcon(CanvasIcon):
+ __gtype_name__ = 'SugarCanvasPulsingIcon'
+
+ def __init__(self, **kwargs):
+ self._pulser = Pulser(self)
+ self._base_color = None
+ self._pulse_color = None
+ self._paused = False
+ self._pulsing = False
+
+ CanvasIcon.__init__(self, **kwargs)
+
+ def set_pulse_color(self, pulse_color):
+ self._pulse_color = pulse_color
+ self._pulser.update()
+
+ def get_pulse_color(self):
+ return self._pulse_color
+
+ pulse_color = gobject.property(
+ type=object, getter=get_pulse_color, setter=set_pulse_color)
+
+ def set_base_color(self, base_color):
+ self._base_color = base_color
+ self._pulser.update()
+
+ def get_base_color(self):
+ return self._base_color
+
+ base_color = gobject.property(
+ type=object, getter=get_base_color, setter=set_base_color)
+
+ def set_paused(self, paused):
+ self._paused = paused
+
+ if self._paused:
+ self._pulser.stop()
+ else:
+ self._pulser.start(restart=False)
+
+ def get_paused(self):
+ return self._paused
+
+ paused = gobject.property(
+ type=bool, default=False, getter=get_paused, setter=set_paused)
+
+ def set_pulsing(self, pulsing):
+ self._pulsing = pulsing
+
+ if self._pulsing:
+ self._pulser.start(restart=True)
+ else:
+ self._pulser.stop()
+
+ def get_pulsing(self):
+ return self._pulsing
+
+ pulsing = gobject.property(
+ type=bool, default=False, getter=get_pulsing, setter=set_pulsing)
diff --git a/src/jarabe/view/tabbinghandler.py b/src/jarabe/view/tabbinghandler.py
new file mode 100644
index 0000000..f1df2e3
--- /dev/null
+++ b/src/jarabe/view/tabbinghandler.py
@@ -0,0 +1,146 @@
+# Copyright (C) 2008, Benjamin Berg <benjamin@sipsolutions.net>
+#
+# 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 view.Shell
+from jarabe.view.frame import frame
+from jarabe.model import shellmodel
+
+_RAISE_DELAY = 250
+
+class TabbingHandler(object):
+ def __init__(self, modifier):
+ self._tabbing = False
+ self._modifier = modifier
+ self._timeout = None
+ self._frame = frame.get_instance()
+
+ def _start_tabbing(self):
+ if not self._tabbing:
+ logging.debug('Grabing the input.')
+
+ screen = gtk.gdk.screen_get_default()
+ window = screen.get_root_window()
+ keyboard_grab_result = gtk.gdk.keyboard_grab(window)
+ pointer_grab_result = gtk.gdk.pointer_grab(window)
+
+ self._tabbing = (keyboard_grab_result == gtk.gdk.GRAB_SUCCESS and
+ pointer_grab_result == gtk.gdk.GRAB_SUCCESS)
+
+ # Now test that the modifier is still active to prevent race
+ # conditions. We also test if one of the grabs failed.
+ mask = window.get_pointer()[2]
+ if not self._tabbing or not (mask & self._modifier):
+ logging.debug('Releasing grabs again.')
+
+ # ungrab keyboard/pointer if the grab was successfull.
+ if keyboard_grab_result == gtk.gdk.GRAB_SUCCESS:
+ gtk.gdk.keyboard_ungrab()
+ if pointer_grab_result == gtk.gdk.GRAB_SUCCESS:
+ gtk.gdk.pointer_ungrab()
+
+ self._tabbing = False
+ else:
+ self._frame.show(self._frame.MODE_NON_INTERACTIVE)
+
+ def __timeout_cb(self):
+ self._activate_current()
+ self._timeout = None
+ return False
+
+ def _start_timeout(self):
+ self._cancel_timeout()
+ self._timeout = gobject.timeout_add(_RAISE_DELAY, self.__timeout_cb)
+
+ def _cancel_timeout(self):
+ if self._timeout:
+ gobject.source_remove(self._timeout)
+ self._timeout = None
+
+ def _activate_current(self):
+ shell_model = shellmodel.get_instance()
+ home_model = shell_model.get_home()
+ activity = home_model.get_tabbing_activity()
+ if activity and activity.get_window():
+ activity.get_window().activate(1)
+
+ def next_activity(self):
+ if not self._tabbing:
+ first_switch = True
+ self._start_tabbing()
+ else:
+ first_switch = False
+
+ if self._tabbing:
+ shell_model = shellmodel.get_instance()
+ home_model = shell_model.get_home()
+ zoom_level = shell_model.get_zoom_level()
+ zoom_activity = (zoom_level == shellmodel.ShellModel.ZOOM_ACTIVITY)
+
+ if not zoom_activity and first_switch:
+ activity = home_model.get_active_activity()
+ else:
+ activity = home_model.get_tabbing_activity()
+ activity = home_model.get_next_activity(current=activity)
+
+ home_model.set_tabbing_activity(activity)
+ self._start_timeout()
+ else:
+ view.Shell.get_instance().activate_next_activity()
+
+ def previous_activity(self):
+ if not self._tabbing:
+ first_switch = True
+ self._start_tabbing()
+ else:
+ first_switch = False
+
+ if self._tabbing:
+ shell_model = shellmodel.get_instance()
+ home_model = shell_model.get_home()
+ zoom_level = shell_model.get_zoom_level()
+ zoom_activity = (zoom_level == shellmodel.ShellModel.ZOOM_ACTIVITY)
+
+ if not zoom_activity and first_switch:
+ activity = home_model.get_active_activity()
+ else:
+ activity = home_model.get_tabbing_activity()
+ activity = home_model.get_previous_activity(current=activity)
+
+ home_model.set_tabbing_activity(activity)
+ self._start_timeout()
+ else:
+ view.Shell.get_instance().activate_next_activity()
+
+ def stop(self):
+ gtk.gdk.keyboard_ungrab()
+ gtk.gdk.pointer_ungrab()
+ self._tabbing = False
+
+ self._frame.hide()
+
+ self._cancel_timeout()
+ self._activate_current()
+
+ home_model = shellmodel.get_instance().get_home()
+ home_model.set_tabbing_activity(None)
+
+ def is_tabbing(self):
+ return self._tabbing
+