Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorC. Scott Ananian <cscott@laptop.org>2007-12-13 21:08:27 (GMT)
committer C. Scott Ananian <cscott@laptop.org>2007-12-13 23:30:56 (GMT)
commita9d1055a632ad960231dddf98045c060a2a660d7 (patch)
tree3a647fbb942b721056f8fe63929f58ea8ed8ec85
parentea0596828aa08e611f81f138d121a3e94415187b (diff)
Pippy eats its tail!
Pippy is now a Pippy application; it can be loaded in Pippy and 'make bundle' on it will return a bundle (almost) identical to the original Pippy application.
-rw-r--r--activity.py571
-rw-r--r--activity/activity-default.svg (renamed from skel/activity/activity-icon.svg)0
-rw-r--r--activity/activity-icon.svg (renamed from activity/activity-pippy.svg)0
-rw-r--r--activity/activity.info4
-rw-r--r--pippy_app.py748
-rw-r--r--skel/NEWS1
-rw-r--r--skel/activity.py43
-rwxr-xr-xskel/setup.py8
8 files changed, 775 insertions, 600 deletions
diff --git a/activity.py b/activity.py
index 00fff21..67da456 100644
--- a/activity.py
+++ b/activity.py
@@ -1,564 +1,43 @@
-# Copyright 2007 Chris Ball, based on Collabora's "hellomesh" demo.
-#
-# 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.activity import activity
-"""Pippy Activity: A simple Python programming activity ."""
-from __future__ import with_statement
-import gtksourceview2
-import gtk
-import logging
-import telepathy
-import telepathy.client
-import pango
-import vte
-import re, os, os.path
-import gobject
+import os, sys
+import gtk, pango, vte
-from signal import SIGTERM
-from gettext import gettext as _
-from dbus.service import method, signal
-from dbus.gobject_service import ExportedGObject
-
-from sugar.activity.activity import Activity, ActivityToolbox, get_bundle_path
-from sugar.presence import presenceservice
-
-from sugar.presence.tubeconn import TubeConnection
-from sugar.graphics.icon import Icon
-from sugar.graphics.menuitem import MenuItem
-from sugar.graphics.toolbutton import ToolButton
-
-
-SERVICE = "org.laptop.Pippy"
-IFACE = SERVICE
-PATH = "/org/laptop/Pippy"
-
-ACTIVITY_INFO_TEMPLATE = """
-[Activity]
-name = %s
-bundle_id = org.laptop.pippy.%s
-class = activity.VteActivity
-icon = activity-icon
-activity_version = %d
-show_launcher = yes
-"""
-
-class PippyActivity(Activity):
- """Pippy Activity as specified in activity.info"""
+class VteActivity(activity.Activity):
def __init__(self, handle):
- """Set up the Pippy activity."""
- Activity.__init__(self, handle)
- self._logger = logging.getLogger('pippy-activity')
-
- # Top toolbar with share and close buttons:
- toolbox = ActivityToolbox(self)
- # add 'make bundle' entry to 'keep' palette.
- palette = toolbox.get_activity_toolbar().keep.get_palette()
- # XXX: should clear out old palette entries?
- menu_item = MenuItem(_('As Pippy Document'))
- menu_item.set_image(Icon(file=('%s/activity/activity-pippy.svg' % get_bundle_path()), icon_size=gtk.ICON_SIZE_MENU))
- menu_item.connect('activate', self.keepbutton_cb)
- palette.menu.append(menu_item)
- menu_item.show()
- menu_item = MenuItem(_('As Activity Bundle'))
- menu_item.set_image(Icon(file=('%s/skel/activity/activity-icon.svg' % get_bundle_path()), icon_size=gtk.ICON_SIZE_MENU))
- menu_item.connect('activate', self.makebutton_cb)
- palette.menu.append(menu_item)
- menu_item.show()
+ activity.Activity.__init__(self, handle)
+ toolbox = activity.ActivityToolbox(self)
self.set_toolbox(toolbox)
toolbox.show()
- # Main layout.
- hbox = gtk.HBox()
- vbox = gtk.VBox()
-
- # The sidebar.
- self.model = gtk.TreeStore(gobject.TYPE_PYOBJECT, gobject.TYPE_STRING)
- treeview = gtk.TreeView(self.model)
- cellrenderer = gtk.CellRendererText()
- treecolumn = gtk.TreeViewColumn(_("Examples"), cellrenderer, text=1)
- treeview.get_selection().connect("changed", self.selection_cb)
- treeview.append_column(treecolumn)
- treeview.set_size_request(220, 900)
-
- # Create scrollbars around the view.
- scrolled = gtk.ScrolledWindow()
- scrolled.add(treeview)
- hbox.pack_start(scrolled)
-
- for root, dirs, files in os.walk(get_bundle_path() + '/data/', topdown=True):
- for i in dirs:
- self._logger.debug("dir %s" % i)
- direntry = { "name": _(i), "path": root + i + "/" }
- olditer = self.model.insert_before(None, None)
- self.model.set_value(olditer, 0, direntry)
- self.model.set_value(olditer, 1, direntry["name"])
-
- for _file in sorted(os.listdir(os.path.join(root, i))):
- self._logger.debug("file %s" % _file)
- entry = { "name": _(_file.capitalize()),
- "path": os.path.join(root, i, _file) }
- _iter = self.model.insert_before(olditer, None)
- self.model.set_value(_iter, 0, entry)
- self.model.set_value(_iter, 1, entry["name"])
-
- treeview.expand_all()
-
- # Source buffer
- self.text_buffer = gtksourceview2.Buffer()
- lang_manager = gtksourceview2.language_manager_get_default()
- langs = lang_manager.list_languages()
- for lang in langs:
- for m in lang.get_mime_types():
- if m == "text/x-python":
- self.text_buffer.set_language(lang)
-
- self.text_buffer.set_highlight(True)
-
- # The GTK source view window
- self.text_view = gtksourceview2.View(self.text_buffer)
- self.text_view.set_size_request(900, 350)
- self.text_view.set_editable(True)
- self.text_view.set_cursor_visible(True)
- self.text_view.set_show_line_numbers(True)
- self.text_view.set_wrap_mode(gtk.WRAP_CHAR)
- self.text_view.modify_font(pango.FontDescription("Monospace 10"))
-
- # We could change the color theme here, if we want to.
- #mgr = gtksourceview2.style_manager_get_default()
- #style_scheme = mgr.get_scheme('kate')
- #self.text_buffer.set_style_scheme(style_scheme)
-
- codesw = gtk.ScrolledWindow()
- codesw.set_policy(gtk.POLICY_AUTOMATIC,
- gtk.POLICY_AUTOMATIC)
- codesw.add(self.text_view)
- vbox.pack_start(codesw)
-
- # An hbox for the buttons
- buttonhbox = gtk.HBox()
-
- # The "go" button
- gobutton = gtk.Button(label=_("Run!"))
- gobutton.connect('clicked', self.gobutton_cb)
- gobutton.set_size_request(800, 2)
- buttonhbox.pack_start(gobutton)
-
- # The "stop" button
- stopbutton = gtk.Button(label=_("Stop!"))
- stopbutton.connect('clicked', self.stopbutton_cb)
- stopbutton.set_size_request(200, 2)
- buttonhbox.pack_end(stopbutton)
-
- vbox.pack_start(buttonhbox)
-
- # An hbox to hold the vte window and its scrollbar.
- outbox = gtk.HBox()
+ # XXX: NEED SHOW SOURCE BUTTON / KEYBINDING
- # The vte python window
+ # creates vte widget
self._vte = vte.Terminal()
- self._vte.set_size(30, 5)
+ self._vte.set_size(30,5)
self._vte.set_size_request(200, 300)
font = 'Monospace 10'
self._vte.set_font(pango.FontDescription(font))
self._vte.set_colors(gtk.gdk.color_parse ('#000000'),
gtk.gdk.color_parse ('#E7E7E7'),
[])
- outbox.pack_start(self._vte)
-
- outsb = gtk.VScrollbar(self._vte.get_adjustment())
- outsb.show()
- outbox.pack_start(outsb, False, False, 0)
- vbox.pack_end(outbox)
- hbox.pack_end(vbox)
- self.set_canvas(hbox)
+ # ...and its scrollbar
+ vtebox = gtk.HBox()
+ vtebox.pack_start(self._vte)
+ vtesb = gtk.VScrollbar(self._vte.get_adjustment())
+ vtesb.show()
+ vtebox.pack_start(vtesb, False, False, 0)
+ self.set_canvas(vtebox)
self.show_all()
-
- self.hellotube = None
-
- # get the Presence Service
- self.pservice = presenceservice.get_instance()
- name, path = self.pservice.get_preferred_connection()
- self.tp_conn_name = name
- self.tp_conn_path = path
- self.conn = telepathy.client.Connection(name, path)
- self.initiating = None
-
- self.connect('shared', self._shared_cb)
-
- # Buddy object for you
- owner = self.pservice.get_owner()
- self.owner = owner
-
- if self._shared_activity:
- # we are joining the activity
- self.connect('joined', self._joined_cb)
- self._shared_activity.connect('buddy-joined',
- self._buddy_joined_cb)
- self._shared_activity.connect('buddy-left',
- self._buddy_left_cb)
- if self.get_shared():
- # we've already joined
- self._joined_cb()
-
- def selection_cb(self, column):
- self.save()
- model, _iter = column.get_selected()
- value = model.get_value(_iter,0)
- self._logger.debug("clicked! %s" % value['path'])
- _file = open(value['path'], 'r')
- lines = _file.readlines()
- self.text_buffer.set_text("".join(lines))
- self.metadata['title'] = value['name']
- self._reset_vte()
- self.text_view.grab_focus()
-
- def _write_text_buffer(self, filename):
- start, end = self.text_buffer.get_bounds()
- text = self.text_buffer.get_text(start, end)
-
- with open(filename, 'w') as f:
- for line in text:
- f.write(line)
- def _reset_vte(self):
+ # now start subprocess.
self._vte.grab_focus()
- self._vte.feed("\x1B[H\x1B[J\x1B[0;39m")
-
- def gobutton_cb(self, button):
- self._reset_vte()
-
- # FIXME: We're losing an odd race here
- # gtk.main_iteration(block=False)
-
- pippy_app_name = '%s/tmp/pippy_app.py' % self.get_activity_root()
- self._write_text_buffer(pippy_app_name)
-
+ bundle_path = activity.get_bundle_path()
+ # the 'sleep 1' works around a bug with the command dying before
+ # the vte widget manages to snarf the last bits of its output
self._pid = self._vte.fork_command \
- (command="/bin/sh",
- argv=["/bin/sh", "-c",
- "python %s; sleep 1" % pippy_app_name],
- envv=["PYTHONPATH=%s/library" % get_bundle_path()],
- directory=get_bundle_path())
-
- def stopbutton_cb(self, button):
- os.kill(self._pid, SIGTERM)
-
- def keepbutton_cb(self, __):
- self.copy()
-
- def makebutton_cb(self, __):
- from shutil import copytree, copy2, rmtree
- from tempfile import mkdtemp
- from sugar import profile
- from sugar.datastore import datastore
- # get the name of this pippy program.
- title = self.metadata['title']
- if title == 'Pippy Activity':
- from sugar.graphics.alert import Alert
- from sugar.graphics.icon import Icon
- alert = Alert()
- alert.props.title =_ ('Save as Activity Error')
- alert.props.msg = _('Please give your activity a meaningful name before attempting to save it as an activity.')
- ok_icon = Icon(icon_name='dialog-ok')
- alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon)
- alert.connect('response', self.dismiss_alert_cb)
- self.add_alert(alert)
- return
- self._reset_vte()
- # turn the activity title into a python identifier.
- pytitle = re.sub(r'[^A-Za-z0-9_]', '', title)
- if re.match(r'[0-9]', pytitle) is not None:
- pytitle = '_' + pytitle # first character cannot be numeric
- # create a new temp dir in which to create the bundle.
- app_temp = mkdtemp('.activity', 'Pippy',
- '%s/tmp/' % self.get_activity_root())
- bundle = get_bundle_path()
- try:
- for f in os.listdir('%s/skel' % bundle):
- if os.path.isdir(f):
- cp = copytree
- else:
- cp = copy2
- cp('%s/skel/%s' % (bundle, f), '%s/%s' % (app_temp, f))
- copytree('%s/library' % bundle, '%s/library' % app_temp)
- # create activity.info file.
- version = 1
- with open('%s/activity/activity.info' % app_temp, 'w') as f:
- f.write(ACTIVITY_INFO_TEMPLATE % (title, pytitle, version))
- # put script into $app_temp/pippy_app.py
- self._write_text_buffer('%s/pippy_app.py' % app_temp)
- # write MANIFEST file.
- with open('%s/MANIFEST' % app_temp, 'w') as f:
- for dirpath, dirnames, filenames in os.walk(app_temp):
- for name in filenames:
- fn = os.path.join(dirpath, name).replace(app_temp+'/', '')
- if fn=='MANIFEST': continue
- f.write('%s\n' % fn)
- # invoke bundle builder
- pid = self._vte.fork_command \
- (command="/bin/sh",
- argv=["/bin/sh", "-c",
- "python %s/setup.py dist || sleep 1" % app_temp],
- directory=app_temp)
- # hand off to journal?
- jobject = datastore.create()
- metadata = {
- 'title': '%s Bundle' % title,
- 'title_set_by_user': '1',
- 'buddies': '',
- 'preview': '',
- 'icon-color': profile.get_color().to_string(),
- 'mime_type': 'application/vnd.olpc-sugar',
- }
- for k, v in metadata.items():
- jobject.metadata[k] = v # the dict.update method is missing =(
- jobject.file_path = '%s/%s-%d.xo' % (app_temp, pytitle, version)
- os.waitpid(pid, 0)
- datastore.write(jobject)
- jobject.destroy()
- self._vte.feed("\r\n")
- self._vte.feed(_("Activity saved to journal."))
- self._vte.feed("\r\n")
- finally:
- rmtree(app_temp, ignore_errors=True)
-
- def dismiss_alert_cb(self, alert, response_id):
- self.remove_alert(alert)
-
- def write_file(self, file_path):
- self.metadata['mime_type'] = 'text/x-python'
- start, end = self.text_buffer.get_bounds()
- text = self.text_buffer.get_text(start, end)
- _file = open(file_path, 'w')
- _file.write(text)
-
- def read_file(self, file_path):
- text = open(file_path).read()
- self.text_buffer.set_text(text)
-
- def _shared_cb(self, activity):
- self._logger.debug('My activity was shared')
- self.initiating = True
- self._setup()
-
- for buddy in self._shared_activity.get_joined_buddies():
- self._logger.debug('Buddy %s is already in the activity' %
- buddy.props.nick)
-
- self._shared_activity.connect('buddy-joined', self._buddy_joined_cb)
- self._shared_activity.connect('buddy-left', self._buddy_left_cb)
-
- self._logger.debug('This is my activity: making a tube...')
- _id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
- SERVICE, {})
-
- # presence service should be tubes-aware and give us more help
- # with this
- def _setup(self):
- if self._shared_activity is None:
- self._logger.error('Failed to share or join activity')
- return
-
- bus_name, conn_path, channel_paths =\
- self._shared_activity.get_channels()
-
- # Work out what our room is called and whether we have Tubes already
- room = None
- tubes_chan = None
- text_chan = None
- for channel_path in channel_paths:
- channel = telepathy.client.Channel(bus_name, channel_path)
- htype, handle = channel.GetHandle()
- if htype == telepathy.HANDLE_TYPE_ROOM:
- self._logger.debug('Found our room: it has handle#%d "%s"',
- handle, self.conn.InspectHandles(htype, [handle])[0])
- room = handle
- ctype = channel.GetChannelType()
- if ctype == telepathy.CHANNEL_TYPE_TUBES:
- self._logger.debug('Found our Tubes channel at %s', channel_path)
- tubes_chan = channel
- elif ctype == telepathy.CHANNEL_TYPE_TEXT:
- self._logger.debug('Found our Text channel at %s', channel_path)
- text_chan = channel
-
- if room is None:
- self._logger.error("Presence service didn't create a room")
- return
- if text_chan is None:
- self._logger.error("Presence service didn't create a text channel")
- return
-
- # Make sure we have a Tubes channel - PS doesn't yet provide one
- if tubes_chan is None:
- self._logger.debug("Didn't find our Tubes channel, requesting one...")
- tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
- telepathy.HANDLE_TYPE_ROOM, room, True)
-
- self.tubes_chan = tubes_chan
- self.text_chan = text_chan
-
- tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
- self._new_tube_cb)
-
- def _list_tubes_reply_cb(self, tubes):
- for tube_info in tubes:
- self._new_tube_cb(*tube_info)
-
- def _list_tubes_error_cb(self, e):
- self._logger.error('ListTubes() failed: %s', e)
-
- def _joined_cb(self, activity):
- if not self._shared_activity:
- return
-
- # Find out who's already in the shared activity:
- for buddy in self._shared_activity.get_joined_buddies():
- self._logger.debug('Buddy %s is already in the activity' % buddy.props.nick)
-
- self._logger.debug('Joined an existing shared activity')
- self.initiating = False
- self._setup()
-
- self._logger.debug('This is not my activity: waiting for a tube...')
- self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
- reply_handler=self._list_tubes_reply_cb,
- error_handler=self._list_tubes_error_cb)
-
- def _new_tube_cb(self, _id, initiator, type, service, params, state):
- self._logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
- 'params=%r state=%d', _id, initiator, type, service,
- params, state)
-
- if (type == telepathy.TUBE_TYPE_DBUS and
- service == SERVICE):
- if state == telepathy.TUBE_STATE_LOCAL_PENDING:
- self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(_id)
-
- tube_conn = TubeConnection(self.conn,
- self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
- _id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
- self.hellotube = HelloTube(tube_conn, self.initiating, self._get_buddy)
-
- def _buddy_joined_cb (self, activity, buddy):
- self._logger.debug('Buddy %s joined' % buddy.props.nick)
-
- def _buddy_left_cb (self, activity, buddy):
- self._logger.debug('Buddy %s left' % buddy.props.nick)
-
- def _get_buddy(self, cs_handle):
- """Get a Buddy from a channel specific handle."""
- self._logger.debug('Trying to find owner of handle %u...', cs_handle)
- group = self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]
- my_csh = group.GetSelfHandle()
- self._logger.debug('My handle in that group is %u', my_csh)
- if my_csh == cs_handle:
- handle = self.conn.GetSelfHandle()
- self._logger.debug('CS handle %u belongs to me, %u', cs_handle, handle)
- elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
- handle = group.GetHandleOwners([cs_handle])[0]
- self._logger.debug('CS handle %u belongs to %u', cs_handle, handle)
- else:
- handle = cs_handle
- self._logger.debug('non-CS handle %u belongs to itself', handle)
-
- # XXX: deal with failure to get the handle owner
- assert handle != 0
-
- # XXX: we're assuming that we have Buddy objects for all contacts -
- # this might break when the server becomes scalable.
- return self.pservice.get_buddy_by_telepathy_handle(self.tp_conn_name,
- self.tp_conn_path, handle)
-
-class HelloTube(ExportedGObject):
- """The bit that talks over the TUBES!!!"""
-
- def __init__(self, tube, is_initiator, get_buddy):
- super(HelloTube, self).__init__(tube, PATH)
- self._logger = logging.getLogger('pippy-activity.HelloTube')
- self.tube = tube
- self.is_initiator = is_initiator
- self.entered = False # Have we set up the tube?
- self.helloworld = False # Have we said Hello and received World?
- self._get_buddy = get_buddy # Converts handle to Buddy object
-
- self.ordered_bus_names = []
-
- self.tube.watch_participants(self.participant_change_cb)
-
- def participant_change_cb(self, added, removed):
- self._logger.debug('Adding participants: %r' % added)
- self._logger.debug('Removing participants: %r' % type(removed))
-
- for handle, bus_name in added:
- buddy = self._get_buddy(handle)
- if buddy is not None:
- self._logger.debug('Buddy %s was added' % buddy.props.nick)
-
- for handle in removed:
- buddy = self._get_buddy(handle)
- if buddy is not None:
- self._logger.debug('Buddy %s was removed' % buddy.props.nick)
- try:
- self.ordered_bus_names.remove(self.tube.participants[handle])
- except ValueError:
- # already absent
- pass
-
- if not self.entered:
- #self.tube.add_signal_receiver(self.insert_cb, 'Insert', IFACE,
- # path=PATH, sender_keyword='sender')
- if self.is_initiator:
- self._logger.debug("I'm initiating the tube, will "
- "watch for hellos.")
- self.add_hello_handler()
- self.ordered_bus_names = [self.tube.get_unique_name()]
- else:
- self._logger.debug('Hello, everyone! What did I miss?')
- self.Hello()
- self.entered = True
-
- @signal(dbus_interface=IFACE, signature='')
- def Hello(self):
- """Say Hello to whoever else is in the tube."""
- self._logger.debug('I said Hello.')
-
- @method(dbus_interface=IFACE, in_signature='as', out_signature='')
- def World(self, bus_names):
- """To be called on the incoming XO after they Hello."""
- if not 1 or self.helloworld: # XXX remove 1
- self._logger.debug('Somebody said World.')
- self.ordered_bus_names = bus_names
- # now I can World others
- self.add_hello_handler()
-
- #buddy = self._get_buddy(self.tube.bus_name_to_handle[bus_names[0]])
- else:
- self._logger.debug("I've already been welcomed, doing nothing")
-
- def add_hello_handler(self):
- self._logger.debug('Adding hello handler.')
- self.tube.add_signal_receiver(self.hello_cb, 'Hello', IFACE,
- path=PATH, sender_keyword='sender')
-
- def hello_cb(self, sender=None):
- """Somebody Helloed me. World them."""
- self._logger.debug('Newcomer %s has joined', sender)
- self.ordered_bus_names.append(sender)
- self._logger.debug('Bus names are now: %r', self.ordered_bus_names)
- self._logger.debug('Welcoming newcomer and sending them the game state')
- self.tube.get_object(sender, PATH).World(self.ordered_bus_names,
- dbus_interface=IFACE)
-
-
+ (command='/bin/sh',
+ argv=['/bin/sh','-c',
+ 'python %s/pippy_app.py; sleep 1' % bundle_path],
+ envv=["PYTHONPATH=%s/library" % bundle_path],
+ directory=bundle_path)
diff --git a/skel/activity/activity-icon.svg b/activity/activity-default.svg
index 41253f0..41253f0 100644
--- a/skel/activity/activity-icon.svg
+++ b/activity/activity-default.svg
diff --git a/activity/activity-pippy.svg b/activity/activity-icon.svg
index 68e120c..68e120c 100644
--- a/activity/activity-pippy.svg
+++ b/activity/activity-icon.svg
diff --git a/activity/activity.info b/activity/activity.info
index 895daa1..66ad3a5 100644
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,7 +1,7 @@
[Activity]
name = Pippy
bundle_id = org.laptop.Pippy
-class = activity.PippyActivity
-icon = activity-pippy
+class = pippy_app.PippyActivity
+icon = activity-icon
activity_version = 14
show_launcher = yes
diff --git a/pippy_app.py b/pippy_app.py
new file mode 100644
index 0000000..e63de51
--- /dev/null
+++ b/pippy_app.py
@@ -0,0 +1,748 @@
+# Copyright 2007 Chris Ball, based on Collabora's "hellomesh" demo.
+#
+# 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
+
+"""Pippy Activity: A simple Python programming activity ."""
+from __future__ import with_statement
+import gtk
+import logging
+import telepathy
+import telepathy.client
+import pango
+import vte
+import re, os, os.path
+import gobject
+
+from signal import SIGTERM
+from gettext import gettext as _
+from dbus.service import method, signal
+from dbus.gobject_service import ExportedGObject
+
+from sugar.activity.activity import Activity, ActivityToolbox, get_bundle_path
+from sugar.presence import presenceservice
+
+
+SERVICE = "org.laptop.Pippy"
+IFACE = SERVICE
+PATH = "/org/laptop/Pippy"
+
+class PippyActivity(Activity):
+ """Pippy Activity as specified in activity.info"""
+ def __init__(self, handle):
+ """Set up the Pippy activity."""
+ Activity.__init__(self, handle)
+ self._logger = logging.getLogger('pippy-activity')
+
+ # Top toolbar with share and close buttons:
+ toolbox = ActivityToolbox(self)
+ # add 'make bundle' entry to 'keep' palette.
+ palette = toolbox.get_activity_toolbar().keep.get_palette()
+ # XXX: should clear out old palette entries?
+ from sugar.graphics.menuitem import MenuItem
+ from sugar.graphics.icon import Icon
+ menu_item = MenuItem(_('As Pippy Document'))
+ menu_item.set_image(Icon(file=('%s/activity/activity-icon.svg' % get_bundle_path()), icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self.keepbutton_cb)
+ palette.menu.append(menu_item)
+ menu_item.show()
+ menu_item = MenuItem(_('As Activity Bundle'))
+ menu_item.set_image(Icon(file=('%s/activity/activity-default.svg' % get_bundle_path()), icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self.makebutton_cb)
+ palette.menu.append(menu_item)
+ menu_item.show()
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ # Main layout.
+ hbox = gtk.HBox()
+ vbox = gtk.VBox()
+
+ # The sidebar.
+ self.model = gtk.TreeStore(gobject.TYPE_PYOBJECT, gobject.TYPE_STRING)
+ treeview = gtk.TreeView(self.model)
+ cellrenderer = gtk.CellRendererText()
+ treecolumn = gtk.TreeViewColumn(_("Examples"), cellrenderer, text=1)
+ treeview.get_selection().connect("changed", self.selection_cb)
+ treeview.append_column(treecolumn)
+ treeview.set_size_request(220, 900)
+
+ # Create scrollbars around the view.
+ scrolled = gtk.ScrolledWindow()
+ scrolled.add(treeview)
+ hbox.pack_start(scrolled)
+
+ for root, dirs, files in os.walk(get_bundle_path() + '/data/', topdown=True):
+ for i in dirs:
+ self._logger.debug("dir %s" % i)
+ direntry = { "name": _(i), "path": root + i + "/" }
+ olditer = self.model.insert_before(None, None)
+ self.model.set_value(olditer, 0, direntry)
+ self.model.set_value(olditer, 1, direntry["name"])
+
+ for _file in sorted(os.listdir(os.path.join(root, i))):
+ self._logger.debug("file %s" % _file)
+ entry = { "name": _(_file.capitalize()),
+ "path": os.path.join(root, i, _file) }
+ _iter = self.model.insert_before(olditer, None)
+ self.model.set_value(_iter, 0, entry)
+ self.model.set_value(_iter, 1, entry["name"])
+
+ treeview.expand_all()
+
+ # Source buffer
+ import gtksourceview2
+ self.text_buffer = gtksourceview2.Buffer()
+ lang_manager = gtksourceview2.language_manager_get_default()
+ langs = lang_manager.list_languages()
+ for lang in langs:
+ for m in lang.get_mime_types():
+ if m == "text/x-python":
+ self.text_buffer.set_language(lang)
+
+ self.text_buffer.set_highlight(True)
+
+ # The GTK source view window
+ self.text_view = gtksourceview2.View(self.text_buffer)
+ self.text_view.set_size_request(900, 350)
+ self.text_view.set_editable(True)
+ self.text_view.set_cursor_visible(True)
+ self.text_view.set_show_line_numbers(True)
+ self.text_view.set_wrap_mode(gtk.WRAP_CHAR)
+ self.text_view.modify_font(pango.FontDescription("Monospace 10"))
+
+ # We could change the color theme here, if we want to.
+ #mgr = gtksourceview2.style_manager_get_default()
+ #style_scheme = mgr.get_scheme('kate')
+ #self.text_buffer.set_style_scheme(style_scheme)
+
+ codesw = gtk.ScrolledWindow()
+ codesw.set_policy(gtk.POLICY_AUTOMATIC,
+ gtk.POLICY_AUTOMATIC)
+ codesw.add(self.text_view)
+ vbox.pack_start(codesw)
+
+ # An hbox for the buttons
+ buttonhbox = gtk.HBox()
+
+ # The "go" button
+ gobutton = gtk.Button(label=_("Run!"))
+ gobutton.connect('clicked', self.gobutton_cb)
+ gobutton.set_size_request(800, 2)
+ buttonhbox.pack_start(gobutton)
+
+ # The "stop" button
+ stopbutton = gtk.Button(label=_("Stop!"))
+ stopbutton.connect('clicked', self.stopbutton_cb)
+ stopbutton.set_size_request(200, 2)
+ buttonhbox.pack_end(stopbutton)
+
+ vbox.pack_start(buttonhbox)
+
+ # An hbox to hold the vte window and its scrollbar.
+ outbox = gtk.HBox()
+
+ # The vte python window
+ self._vte = vte.Terminal()
+ self._vte.set_size(30, 5)
+ self._vte.set_size_request(200, 300)
+ font = 'Monospace 10'
+ self._vte.set_font(pango.FontDescription(font))
+ self._vte.set_colors(gtk.gdk.color_parse ('#000000'),
+ gtk.gdk.color_parse ('#E7E7E7'),
+ [])
+ self._vte.connect('child_exited', self.child_exited_cb)
+ self._child_exited_handler = None
+ outbox.pack_start(self._vte)
+
+ outsb = gtk.VScrollbar(self._vte.get_adjustment())
+ outsb.show()
+ outbox.pack_start(outsb, False, False, 0)
+ vbox.pack_end(outbox)
+ hbox.pack_end(vbox)
+ self.set_canvas(hbox)
+ self.show_all()
+
+
+ self.hellotube = None
+
+ # get the Presence Service
+ self.pservice = presenceservice.get_instance()
+ name, path = self.pservice.get_preferred_connection()
+ self.tp_conn_name = name
+ self.tp_conn_path = path
+ self.conn = telepathy.client.Connection(name, path)
+ self.initiating = None
+
+ self.connect('shared', self._shared_cb)
+
+ # Buddy object for you
+ owner = self.pservice.get_owner()
+ self.owner = owner
+
+ if self._shared_activity:
+ # we are joining the activity
+ self.connect('joined', self._joined_cb)
+ self._shared_activity.connect('buddy-joined',
+ self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left',
+ self._buddy_left_cb)
+ if self.get_shared():
+ # we've already joined
+ self._joined_cb()
+
+ def selection_cb(self, column):
+ self.save()
+ model, _iter = column.get_selected()
+ value = model.get_value(_iter,0)
+ self._logger.debug("clicked! %s" % value['path'])
+ _file = open(value['path'], 'r')
+ lines = _file.readlines()
+ self.text_buffer.set_text("".join(lines))
+ self.metadata['title'] = value['name']
+ self._reset_vte()
+ self.text_view.grab_focus()
+
+ def _write_text_buffer(self, filename):
+ start, end = self.text_buffer.get_bounds()
+ text = self.text_buffer.get_text(start, end)
+
+ with open(filename, 'w') as f:
+ for line in text:
+ f.write(line)
+ def _reset_vte(self):
+ self._vte.grab_focus()
+ self._vte.feed("\x1B[H\x1B[J\x1B[0;39m")
+
+ def gobutton_cb(self, button):
+ self.stopbutton_cb(button) # try stopping old code first.
+ self._reset_vte()
+
+ # FIXME: We're losing an odd race here
+ # gtk.main_iteration(block=False)
+
+ pippy_app_name = '%s/tmp/pippy_app.py' % self.get_activity_root()
+ self._write_text_buffer(pippy_app_name)
+
+ self._pid = self._vte.fork_command \
+ (command="/bin/sh",
+ argv=["/bin/sh", "-c",
+ "python %s; sleep 1" % pippy_app_name],
+ envv=["PYTHONPATH=%s/library" % get_bundle_path()],
+ directory=get_bundle_path())
+
+ def stopbutton_cb(self, button):
+ try:
+ os.kill(self._pid, SIGTERM)
+ except:
+ pass # process must already be dead.
+
+ def keepbutton_cb(self, __):
+ self.copy()
+
+ def makebutton_cb(self, __):
+ from shutil import copytree, copy2, rmtree
+ from tempfile import mkdtemp
+ # get the name of this pippy program.
+ title = self.metadata['title']
+ if title == 'Pippy Activity':
+ from sugar.graphics.alert import Alert
+ from sugar.graphics.icon import Icon
+ alert = Alert()
+ alert.props.title =_ ('Save as Activity Error')
+ alert.props.msg = _('Please give your activity a meaningful name before attempting to save it as an activity.')
+ ok_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon)
+ alert.connect('response', self.dismiss_alert_cb)
+ self.add_alert(alert)
+ return
+ self.stopbutton_cb(None) # try stopping old code first.
+ self._reset_vte()
+ self._vte.feed(_("Creating activity bundle...\r\n"))
+ app_temp = mkdtemp('.activity', 'Pippy',
+ '%s/tmp/' % self.get_activity_root())
+ sourcefile = os.path.join(app_temp, 'xyzzy.py')
+ # invoke ourself to build the activity bundle.
+ try:
+ # write out application code
+ self._write_text_buffer(sourcefile)
+ # hook up a callback for when the bundle builder is done.
+ # we can't use gobject.child_watch_add because vte will reap our
+ # children before we can.
+ self._child_exited_handler = lambda: self.bundle_cb(title, app_temp)
+ # invoke bundle builder
+ self._pid = self._vte.fork_command \
+ (command="/usr/bin/python",
+ argv=["/usr/bin/python",
+ "%s/pippy_app.py" % get_bundle_path(),
+ '-p', '%s/library' % get_bundle_path(),
+ '-d', app_temp,
+ title, sourcefile],
+ directory=app_temp)
+ except:
+ rmtree(app_temp, ignore_errors=True) # clean up!
+ raise
+
+ def child_exited_cb(self, *args):
+ """Called whenever a child exits. If there's a handler, run it."""
+ h, self._child_exited_handler = self._child_exited_handler, None
+ if h is not None: h()
+
+ def bundle_cb(self, title, app_temp):
+ """Called when we're done building a bundle for a source file."""
+ from sugar import profile
+ from shutil import rmtree
+ from sugar.datastore import datastore
+ try:
+ # find the .xo file: were we successful?
+ bundle_file=[ f for f in os.listdir(app_temp) if f.endswith('.xo') ]
+ if len(bundle_file) != 1:
+ self._logger.debug("Couldn't find bundle: %s"%str(bundle_file))
+ return # something went wrong.
+ # hand off to journal
+ jobject = datastore.create()
+ metadata = {
+ 'title': '%s Bundle' % title,
+ 'title_set_by_user': '1',
+ 'buddies': '',
+ 'preview': '',
+ 'icon-color': profile.get_color().to_string(),
+ 'mime_type': 'application/vnd.olpc-sugar',
+ }
+ for k, v in metadata.items():
+ jobject.metadata[k] = v # the dict.update method is missing =(
+ jobject.file_path = os.path.join(app_temp, bundle_file[0])
+ datastore.write(jobject)
+ jobject.destroy()
+ self._vte.feed("\r\n")
+ self._vte.feed(_("Activity saved to journal."))
+ self._vte.feed("\r\n")
+ # XXX SHOULD SWITCH TO JOURNAL.
+ finally:
+ rmtree(app_temp, ignore_errors=True) # clean up!
+
+ def dismiss_alert_cb(self, alert, response_id):
+ self.remove_alert(alert)
+
+ def write_file(self, file_path):
+ self.metadata['mime_type'] = 'text/x-python'
+ start, end = self.text_buffer.get_bounds()
+ text = self.text_buffer.get_text(start, end)
+ _file = open(file_path, 'w')
+ _file.write(text)
+
+ def read_file(self, file_path):
+ text = open(file_path).read()
+ self.text_buffer.set_text(text)
+
+ def _shared_cb(self, activity):
+ self._logger.debug('My activity was shared')
+ self.initiating = True
+ self._setup()
+
+ for buddy in self._shared_activity.get_joined_buddies():
+ self._logger.debug('Buddy %s is already in the activity' %
+ buddy.props.nick)
+
+ self._shared_activity.connect('buddy-joined', self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left', self._buddy_left_cb)
+
+ self._logger.debug('This is my activity: making a tube...')
+ _id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
+ SERVICE, {})
+
+ # presence service should be tubes-aware and give us more help
+ # with this
+ def _setup(self):
+ if self._shared_activity is None:
+ self._logger.error('Failed to share or join activity')
+ return
+
+ bus_name, conn_path, channel_paths =\
+ self._shared_activity.get_channels()
+
+ # Work out what our room is called and whether we have Tubes already
+ room = None
+ tubes_chan = None
+ text_chan = None
+ for channel_path in channel_paths:
+ channel = telepathy.client.Channel(bus_name, channel_path)
+ htype, handle = channel.GetHandle()
+ if htype == telepathy.HANDLE_TYPE_ROOM:
+ self._logger.debug('Found our room: it has handle#%d "%s"',
+ handle, self.conn.InspectHandles(htype, [handle])[0])
+ room = handle
+ ctype = channel.GetChannelType()
+ if ctype == telepathy.CHANNEL_TYPE_TUBES:
+ self._logger.debug('Found our Tubes channel at %s', channel_path)
+ tubes_chan = channel
+ elif ctype == telepathy.CHANNEL_TYPE_TEXT:
+ self._logger.debug('Found our Text channel at %s', channel_path)
+ text_chan = channel
+
+ if room is None:
+ self._logger.error("Presence service didn't create a room")
+ return
+ if text_chan is None:
+ self._logger.error("Presence service didn't create a text channel")
+ return
+
+ # Make sure we have a Tubes channel - PS doesn't yet provide one
+ if tubes_chan is None:
+ self._logger.debug("Didn't find our Tubes channel, requesting one...")
+ tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
+ telepathy.HANDLE_TYPE_ROOM, room, True)
+
+ self.tubes_chan = tubes_chan
+ self.text_chan = text_chan
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
+ self._new_tube_cb)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _list_tubes_error_cb(self, e):
+ self._logger.error('ListTubes() failed: %s', e)
+
+ def _joined_cb(self, activity):
+ if not self._shared_activity:
+ return
+
+ # Find out who's already in the shared activity:
+ for buddy in self._shared_activity.get_joined_buddies():
+ self._logger.debug('Buddy %s is already in the activity' % buddy.props.nick)
+
+ self._logger.debug('Joined an existing shared activity')
+ self.initiating = False
+ self._setup()
+
+ self._logger.debug('This is not my activity: waiting for a tube...')
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+
+ def _new_tube_cb(self, _id, initiator, type, service, params, state):
+ self._logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', _id, initiator, type, service,
+ params, state)
+
+ if (type == telepathy.TUBE_TYPE_DBUS and
+ service == SERVICE):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(_id)
+
+ from sugar.presence.tubeconn import TubeConnection
+ tube_conn = TubeConnection(self.conn,
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ _id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+ self.hellotube = HelloTube(tube_conn, self.initiating, self._get_buddy)
+
+ def _buddy_joined_cb (self, activity, buddy):
+ self._logger.debug('Buddy %s joined' % buddy.props.nick)
+
+ def _buddy_left_cb (self, activity, buddy):
+ self._logger.debug('Buddy %s left' % buddy.props.nick)
+
+ def _get_buddy(self, cs_handle):
+ """Get a Buddy from a channel specific handle."""
+ self._logger.debug('Trying to find owner of handle %u...', cs_handle)
+ group = self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]
+ my_csh = group.GetSelfHandle()
+ self._logger.debug('My handle in that group is %u', my_csh)
+ if my_csh == cs_handle:
+ handle = self.conn.GetSelfHandle()
+ self._logger.debug('CS handle %u belongs to me, %u', cs_handle, handle)
+ elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ handle = group.GetHandleOwners([cs_handle])[0]
+ self._logger.debug('CS handle %u belongs to %u', cs_handle, handle)
+ else:
+ handle = cs_handle
+ self._logger.debug('non-CS handle %u belongs to itself', handle)
+
+ # XXX: deal with failure to get the handle owner
+ assert handle != 0
+
+ # XXX: we're assuming that we have Buddy objects for all contacts -
+ # this might break when the server becomes scalable.
+ return self.pservice.get_buddy_by_telepathy_handle(self.tp_conn_name,
+ self.tp_conn_path, handle)
+
+class HelloTube(ExportedGObject):
+ """The bit that talks over the TUBES!!!"""
+
+ def __init__(self, tube, is_initiator, get_buddy):
+ super(HelloTube, self).__init__(tube, PATH)
+ self._logger = logging.getLogger('pippy-activity.HelloTube')
+ self.tube = tube
+ self.is_initiator = is_initiator
+ self.entered = False # Have we set up the tube?
+ self.helloworld = False # Have we said Hello and received World?
+ self._get_buddy = get_buddy # Converts handle to Buddy object
+
+ self.ordered_bus_names = []
+
+ self.tube.watch_participants(self.participant_change_cb)
+
+ def participant_change_cb(self, added, removed):
+ self._logger.debug('Adding participants: %r' % added)
+ self._logger.debug('Removing participants: %r' % type(removed))
+
+ for handle, bus_name in added:
+ buddy = self._get_buddy(handle)
+ if buddy is not None:
+ self._logger.debug('Buddy %s was added' % buddy.props.nick)
+
+ for handle in removed:
+ buddy = self._get_buddy(handle)
+ if buddy is not None:
+ self._logger.debug('Buddy %s was removed' % buddy.props.nick)
+ try:
+ self.ordered_bus_names.remove(self.tube.participants[handle])
+ except ValueError:
+ # already absent
+ pass
+
+ if not self.entered:
+ #self.tube.add_signal_receiver(self.insert_cb, 'Insert', IFACE,
+ # path=PATH, sender_keyword='sender')
+ if self.is_initiator:
+ self._logger.debug("I'm initiating the tube, will "
+ "watch for hellos.")
+ self.add_hello_handler()
+ self.ordered_bus_names = [self.tube.get_unique_name()]
+ else:
+ self._logger.debug('Hello, everyone! What did I miss?')
+ self.Hello()
+ self.entered = True
+
+ @signal(dbus_interface=IFACE, signature='')
+ def Hello(self):
+ """Say Hello to whoever else is in the tube."""
+ self._logger.debug('I said Hello.')
+
+ @method(dbus_interface=IFACE, in_signature='as', out_signature='')
+ def World(self, bus_names):
+ """To be called on the incoming XO after they Hello."""
+ if not 1 or self.helloworld: # XXX remove 1
+ self._logger.debug('Somebody said World.')
+ self.ordered_bus_names = bus_names
+ # now I can World others
+ self.add_hello_handler()
+
+ #buddy = self._get_buddy(self.tube.bus_name_to_handle[bus_names[0]])
+ else:
+ self._logger.debug("I've already been welcomed, doing nothing")
+
+ def add_hello_handler(self):
+ self._logger.debug('Adding hello handler.')
+ self.tube.add_signal_receiver(self.hello_cb, 'Hello', IFACE,
+ path=PATH, sender_keyword='sender')
+
+ def hello_cb(self, sender=None):
+ """Somebody Helloed me. World them."""
+ self._logger.debug('Newcomer %s has joined', sender)
+ self.ordered_bus_names.append(sender)
+ self._logger.debug('Bus names are now: %r', self.ordered_bus_names)
+ self._logger.debug('Welcoming newcomer and sending them the game state')
+ self.tube.get_object(sender, PATH).World(self.ordered_bus_names,
+ dbus_interface=IFACE)
+
+############# TEMPLATES AND INLINE FILES ##############
+ACTIVITY_INFO_TEMPLATE = """
+[Activity]
+name = %(title)s
+bundle_id = %(id)s
+class = %(class)s
+icon = activity-icon
+activity_version = %(version)d
+show_launcher = yes
+"""
+
+PIPPY_ICON = \
+"""<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-pippy">
+ <path d="M28.497,48.507 c5.988,0,14.88-2.838,14.88-11.185c0-9.285-7.743-10.143-10.954-11.083c-3.549-0.799-5.913-1.914-6.055-3.455 c-0.243-2.642,1.158-3.671,3.946-3.671c0,0,6.632,3.664,12.266,0.74c1.588-0.823,4.432-4.668,4.432-7.32 c0-2.653-9.181-5.719-11.967-5.719c-2.788,0-5.159,3.847-5.159,3.847c-5.574,0-11.149,5.306-11.149,10.612 c0,5.305,5.333,9.455,11.707,10.612c2.963,0.469,5.441,2.22,4.878,5.438c-0.457,2.613-2.995,5.306-8.361,5.306 c-4.252,0-13.3-0.219-14.745-4.079c-0.929-2.486,0.168-5.205,1.562-5.205l-0.027-0.16c-1.42-0.158-5.548,0.16-5.548,5.465 C8.202,45.452,17.347,48.507,28.497,48.507z" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ <path d="M42.579,19.854c-2.623-0.287-6.611-2-7.467-5.022" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-width="3"/>
+ <circle cx="35.805" cy="10.96" fill="&stroke_color;" r="1.676"/>
+</g></svg><!-- " -->
+"""
+
+PIPPY_DEFAULT_ICON = \
+"""<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1"
+ viewBox="0 0 55 55" width="55px" x="0px" y="0px" xml:space="preserve"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+><g display="block" id="activity-icon"><path
+ d="M 28.497,48.507 C 34.485,48.507 43.377,45.669 43.377,37.322 C 43.377,32.6795 41.44125,30.14375 39.104125,28.651125 C 36.767,27.1585 38.482419,26.816027 39.758087,25.662766 C 39.42248,24.275242 37.206195,22.826987 36.262179,21.037968 C 34.005473,20.582994 27.526,19.113 30.314,19.113 C 30.314,19.113 36.946,22.777 42.58,19.853 C 44.168,19.03 47.012,15.185 47.012,12.533 C 47.012,9.88 37.831,6.814 35.045,6.814 C 32.257,6.814 29.886,10.661 29.886,10.661 C 24.312,10.661 12.043878,16.258005 12.043878,21.564005 C 12.043878,24.216505 16.585399,30.069973 19.144694,33.736352 C 22.438716,38.455279 27.257,31.3065 30.444,31.885 C 33.407,32.354 35.885,34.105 35.322,37.323 C 34.865,39.936 32.327,42.629 26.961,42.629 C 22.709,42.629 13.661,42.41 12.216,38.55 C 11.287,36.064 12.384,33.345 13.778,33.345 L 13.751,33.185 C 12.331,33.027 8.203,33.345 8.203,38.65 C 8.202,45.452 17.347,48.507 28.497,48.507 z "
+ fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" />
+ <path d="M42.579,19.854c-2.623-0.287-6.611-2-7.467-5.022" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-width="3"/>
+ <circle cx="35.805" cy="10.96" fill="&stroke_color;" r="1.676"/>
+</g></svg><!-- " -->
+"""
+
+############# ACTIVITY META-INFORMATION ###############
+# this is used by Pippy to generate a bundle for itself.
+
+def pippy_activity_version():
+ """Returns the version number of the generated activity bundle."""
+ return 13
+def pippy_activity_extrafiles():
+ """Returns a map of 'extra' files which should be included in the
+ generated activity bundle."""
+ # Cheat here and generate the map from the fs contents.
+ extra = {}
+ bp = get_bundle_path()
+ for d in [ 'po', 'data' ]: # everybody gets library already
+ for root, dirs, files in os.walk(os.path.join(bp, d)):
+ for name in files:
+ fn = os.path.join(root, name).replace(bp+'/', '')
+ if fn == 'po/pseudo.po': continue # XXX breaks bundlebuilder
+ extra[fn] = open(os.path.join(root, name), 'r').read()
+ extra['activity/activity-default.svg'] = PIPPY_DEFAULT_ICON
+ return extra
+def pippy_activity_news():
+ """Return the NEWS file for this activity."""
+ # Cheat again.
+ return open(os.path.join(get_bundle_path(), 'NEWS')).read()
+def pippy_activity_icon():
+ """Return an SVG document specifying the icon for this activity."""
+ return PIPPY_ICON
+def pippy_activity_class():
+ """Return the class which should be started to run this activity."""
+ return 'pippy_app.PippyActivity'
+def pippy_activity_id():
+ """Return the bundle_id for the generated activity."""
+ return 'org.laptop.Pippy'
+
+################# ACTIVITY BUNDLER ################
+
+def main():
+ """Create a bundle from a pippy-style source file"""
+ from optparse import OptionParser
+ from pyclbr import readmodule_ex
+ from tempfile import mkdtemp
+ from shutil import copytree, copy2, rmtree
+ from sugar import profile
+ from sugar.activity import bundlebuilder
+ import sys
+ parser = OptionParser(usage='%prog [options] [title] [sourcefile]')
+ parser.add_option('-d', '--dir', dest='dir',default='.',metavar='DIR',
+ help='Put generated bundle in the specified directory.')
+ parser.add_option('-p', '--pythonpath', dest='path', action='append',
+ default=[], metavar='DIR',
+ help='Append directory to python search path.')
+ (options, args) = parser.parse_args()
+ if len(args) != 2:
+ parser.error('The title and sourcefile arguments are required.')
+ title = args[0]
+ sourcefile = args[1]
+ pytitle = re.sub(r'[^A-Za-z0-9_]', '', title)
+ if re.match(r'[0-9]', pytitle) is not None:
+ pytitle = '_' + pytitle # first character cannot be numeric
+ # first take a gander at the source file and see if it's got extra info
+ # for us.
+ sourcedir, basename = os.path.split(sourcefile)
+ if not sourcedir: sourcedir = '.'
+ module, ext = os.path.splitext(basename)
+ # things we look for:
+ bundle_info = {
+ 'pippy_activity_version': 1,
+ 'pippy_activity_extrafiles': {},
+ 'pippy_activity_news': 'No news.',
+ 'pippy_activity_icon': PIPPY_DEFAULT_ICON,
+ 'pippy_activity_class': 'activity.VteActivity',
+ 'pippy_activity_id': ('org.laptop.pippy.%s' % pytitle),
+ }
+ # are any of these things in the module?
+ try_import = False
+ info = readmodule_ex(module, [ sourcedir ] + options.path)
+ for func in bundle_info.keys():
+ if func in info: try_import = True
+ if try_import:
+ # yes, let's try to execute them to get better info about our bundle
+ oldpath = list(sys.path)
+ sys.path[0:0] = [ sourcedir ] + options.path
+ modobj = __import__(module)
+ for func in bundle_info.keys():
+ if func in modobj.__dict__:
+ bundle_info[func] = modobj.__dict__[func]()
+ sys.path = oldpath
+
+ # okay! We've done the hard part. Now let's build a bundle.
+ # create a new temp dir in which to create the bundle.
+ app_temp = mkdtemp('.activity', 'Pippy') # hope TMPDIR is set correctly!
+ bundle = get_bundle_path()
+ try:
+ copytree('%s/library' % bundle, '%s/library' % app_temp)
+ copy2('%s/activity.py' % bundle, '%s/activity.py' % app_temp)
+ # create activity.info file.
+ version = bundle_info['pippy_activity_version']
+ # put 'extra' files in place.
+ extrafiles = {
+ 'activity/activity.info': ACTIVITY_INFO_TEMPLATE % {
+ 'title': title, 'pytitle': pytitle, 'version': version,
+ 'class': bundle_info['pippy_activity_class'],
+ 'id': bundle_info['pippy_activity_id'],
+ },
+ 'activity/activity-icon.svg': bundle_info['pippy_activity_icon'],
+ 'NEWS': bundle_info['pippy_activity_news'],
+ }
+ extrafiles.update(bundle_info['pippy_activity_extrafiles'])
+ for path, contents in extrafiles.items():
+ # safety first!
+ assert '..' not in path
+ dirname, filename = os.path.split(path)
+ dirname = os.path.join(app_temp, dirname)
+ if not os.path.exists(dirname): os.makedirs(dirname)
+ with open(os.path.join(dirname, filename), 'w') as f:
+ f.write(contents)
+ # put script into $app_temp/pippy_app.py
+ copy2(sourcefile, '%s/pippy_app.py' % app_temp)
+ # write MANIFEST file.
+ with open('%s/MANIFEST' % app_temp, 'w') as f:
+ for dirpath, dirnames, filenames in os.walk(app_temp):
+ for name in filenames:
+ fn = os.path.join(dirpath, name).replace(app_temp+'/', '')
+ if fn=='MANIFEST': continue
+ f.write('%s\n' % fn)
+ # invoke bundle builder
+ olddir = os.getcwd()
+ oldargv = sys.argv
+ os.chdir(app_temp)
+ sys.argv = [ 'setup.py', 'dist' ]
+ bundlebuilder.start(pytitle)
+ sys.argv = oldargv
+ os.chdir(olddir)
+ # move to destination directory.
+ copy2('%s/%s-%d.xo' % (app_temp, pytitle, version),
+ '%s/%s-%d.xo' % (options.dir, pytitle, version))
+ finally:
+ rmtree(app_temp, ignore_errors=True)
+
+if __name__ == '__main__':
+ from gettext import gettext as _
+ import sys
+ if False: # change this to True to test within Pippy
+ sys.argv = sys.argv + [ '-d','/tmp','Pippy', '/home/olpc/pippy_app.py' ]
+ #print _("Working..."),
+ sys.stdout.flush()
+ main()
+ #print _("done!")
diff --git a/skel/NEWS b/skel/NEWS
deleted file mode 100644
index e2620b0..0000000
--- a/skel/NEWS
+++ /dev/null
@@ -1 +0,0 @@
-No news.
diff --git a/skel/activity.py b/skel/activity.py
deleted file mode 100644
index 67da456..0000000
--- a/skel/activity.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from sugar.activity import activity
-
-import os, sys
-import gtk, pango, vte
-
-class VteActivity(activity.Activity):
- def __init__(self, handle):
- activity.Activity.__init__(self, handle)
- toolbox = activity.ActivityToolbox(self)
- self.set_toolbox(toolbox)
- toolbox.show()
-
- # XXX: NEED SHOW SOURCE BUTTON / KEYBINDING
-
- # creates vte widget
- self._vte = vte.Terminal()
- self._vte.set_size(30,5)
- self._vte.set_size_request(200, 300)
- font = 'Monospace 10'
- self._vte.set_font(pango.FontDescription(font))
- self._vte.set_colors(gtk.gdk.color_parse ('#000000'),
- gtk.gdk.color_parse ('#E7E7E7'),
- [])
- # ...and its scrollbar
- vtebox = gtk.HBox()
- vtebox.pack_start(self._vte)
- vtesb = gtk.VScrollbar(self._vte.get_adjustment())
- vtesb.show()
- vtebox.pack_start(vtesb, False, False, 0)
- self.set_canvas(vtebox)
- self.show_all()
-
- # now start subprocess.
- self._vte.grab_focus()
- bundle_path = activity.get_bundle_path()
- # the 'sleep 1' works around a bug with the command dying before
- # the vte widget manages to snarf the last bits of its output
- self._pid = self._vte.fork_command \
- (command='/bin/sh',
- argv=['/bin/sh','-c',
- 'python %s/pippy_app.py; sleep 1' % bundle_path],
- envv=["PYTHONPATH=%s/library" % bundle_path],
- directory=bundle_path)
diff --git a/skel/setup.py b/skel/setup.py
deleted file mode 100755
index a8896a2..0000000
--- a/skel/setup.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-
-if __name__ == "__main__":
- from sugar.activity import bundlebuilder
- from sugar.bundle.activitybundle import ActivityBundle
- b = ActivityBundle('.')
- __, name = b.get_bundle_id().rsplit('.',1)
- bundlebuilder.start(name)