From a9d1055a632ad960231dddf98045c060a2a660d7 Mon Sep 17 00:00:00 2001 From: C. Scott Ananian Date: Thu, 13 Dec 2007 21:08:27 +0000 Subject: 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. --- (limited to 'activity.py') 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) -- cgit v0.9.1