Some day I will finally master git send-mail. (It ignored the --cover-letter option.)
The patch in the previous email is a resubmission of my previous patch series on sugar/src/jarabe/view/viewsource.py (See http://wiki.sugarlabs.org/go/Design_Team/Proposals/Toolbars/View-Source_Enhancements for details.) In regard to comments made at the Sugar Design meeting on 2011-06-12, I made the following changes to the patch: # remove jarabe content from Sugar view source # reset the version number on copy # change the bundle name to bundle_name_NICK_copy # change the activity name to NICK_activity_name # change the activity icon name to NICK-activity-icon.svg # make a .xo bundle from the cloned activity and copy it into the Journal # rename Copy Button hint to "Clone" Many thanks to Sascha, Gary, and Manu for their feedback and suggestions. regards. -walter On Mon, Jun 13, 2011 at 3:20 PM, Walter Bender <wal...@sugarlabs.org> wrote: > From: Walter Bender <walter.ben...@gmail.com> > > --- > src/jarabe/view/Makefile.am | 1 + > src/jarabe/view/customizebundle.py | 216 > ++++++++++++++++++++++++++++++++++++ > src/jarabe/view/viewsource.py | 168 ++++++++++++++++++++++------ > 3 files changed, 350 insertions(+), 35 deletions(-) > create mode 100644 src/jarabe/view/customizebundle.py > > diff --git a/src/jarabe/view/Makefile.am b/src/jarabe/view/Makefile.am > index 1abea6d..630f184 100644 > --- a/src/jarabe/view/Makefile.am > +++ b/src/jarabe/view/Makefile.am > @@ -3,6 +3,7 @@ sugar_PYTHON = \ > __init__.py \ > buddyicon.py \ > buddymenu.py \ > + customizebundle.py \ > keyhandler.py \ > launcher.py \ > palettes.py \ > diff --git a/src/jarabe/view/customizebundle.py > b/src/jarabe/view/customizebundle.py > new file mode 100644 > index 0000000..bc0cf9c > --- /dev/null > +++ b/src/jarabe/view/customizebundle.py > @@ -0,0 +1,216 @@ > +# Copyright (C) 2011 Walter Bender > +# > +# 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 subprocess > +import gtk > + > +import logging > +_logger = logging.getLogger('ViewSource') > + > +from sugar.activity import bundlebuilder > +from sugar.datastore import datastore > + > +CUSTOMICON = 'customize.svg' > +RESCALE = ' <g transform="matrix(0.73,0,0,0.73,7.5,7.5)">\n' > + > + > +def generate_bundle(nick, activity_name, home_activities, new_bundle_name): > + ''' Generate a new .xo bundle for the activity and copy it into the > + Journal. ''' > + > + # First remove any existing bundles in dist/ > + if os.path.exists(os.path.join(home_activities, new_bundle_name, > 'dist')): > + command_line = ['rm', os.path.join(home_activities, new_bundle_name, > + 'dist', '*.xo')] > + _logger.debug(subprocess.call(command_line)) > + command_line = ['rm', os.path.join(home_activities, new_bundle_name, > + 'dist', '*.bz2')] > + _logger.debug(subprocess.call(command_line)) > + > + # Then create a new bundle. > + config = bundlebuilder.Config(source_dir=os.path.join( > + home_activities, new_bundle_name), > + dist_name='%s_%s-1.xo' % (nick, activity_name)) > + bundlebuilder.cmd_fix_manifest(config, None) > + bundlebuilder.cmd_dist_xo(config, None) > + > + # Finally copy the new bundle to the Journal. > + dsobject = datastore.create() > + dsobject.metadata['title'] = '%s_%s-1.xo' % (nick, activity_name) > + dsobject.metadata['mime_type'] = 'application/vnd.olpc-sugar' > + dsobject.set_file_path(os.path.join( > + home_activities, new_bundle_name, 'dist', > + '%s_%s-1.xo' % (nick, activity_name))) > + datastore.write(dsobject) > + dsobject.destroy() > + > + > +def customize_activity_info(nick, home_activities, new_bundle_name): > + ''' Modify bundle_id in new activity.info file: (1) change the > + bundle_id to bundle_id_[NICKNAME]; (2) change the activity_icon > + [NICKNAME]-activity-icon.svg; (3) set activity_version to 1. > + Also, modify the activity icon by applying a customize overlay. > + ''' > + activity_name = '' > + > + fd_old = open(os.path.join(home_activities, new_bundle_name, > + 'activity', 'activity.info'), 'r') > + fd_new = open(os.path.join(home_activities, new_bundle_name, > + 'activity', 'new_activity.info'), 'w') > + for line in fd_old: > + tokens = line.split('=') > + if tokens[0].rstrip() == 'bundle_id': > + new_bundle_id = '%s_%s_clone' % (tokens[1].strip(), nick) > + fd_new.write('%s = %s\n' % (tokens[0].rstrip(), new_bundle_id)) > + elif tokens[0].rstrip() == 'activity_version': > + fd_new.write('%s = 1\n' % (tokens[0].rstrip())) > + elif tokens[0].rstrip() == 'icon': > + old_icon_name = tokens[1].strip() > + new_icon_name = '%s-%s' % (nick, old_icon_name) > + fd_new.write('%s = %s\n' % (tokens[0].rstrip(), new_icon_name)) > + elif tokens[0].rstrip() == 'name': > + fd_new.write('%s = %s_%s\n' % (tokens[0].rstrip(), nick, > + tokens[1].strip())) > + activity_name = tokens[1].strip() > + else: > + fd_new.write(line) > + fd_old.close > + fd_new.close > + command_line = ['mv', os.path.join(home_activities, new_bundle_name, > + 'activity', 'new_activity.info'), > + os.path.join(home_activities, new_bundle_name, > + 'activity', 'activity.info')] > + _logger.debug(subprocess.call(command_line)) > + > + _custom_icon(home_activities, new_bundle_name, old_icon_name, > + new_icon_name) > + > + return activity_name > + > + > +def _custom_icon(home_activities, new_bundle_name, old_icon_name, > + new_icon_name): > + ''' Modify new activity icon by overlaying custom icon. ''' > + > + # First, find customize.svg, which will be used as an overlay. > + path = None > + for path in gtk.icon_theme_get_default().get_search_path(): > + if os.path.exists(os.path.join(path, 'sugar', 'scalable', 'actions', > + CUSTOMICON)): > + break > + > + if path is None: > + _logger.debug('customize.svg not found') > + command_line = ['mv', os.path.join(home_activities, new_bundle_name, > + 'activity', old_icon_name + > '.svg'), > + os.path.join(home_activities, new_bundle_name, > + 'activity', new_icon_name + '.svg')] > + _logger.debug(subprocess.call(command_line)) > + return > + > + # Extract the 'payload' from customize.svg. > + fd_custom = open(os.path.join(path, 'sugar', 'scalable', 'actions', > + CUSTOMICON), 'r') > + > + custom_svg = '' > + found_begin_svg_tag = False > + found_close_tag = False > + found_end_svg_tag = False > + > + for line in fd_custom: > + if not found_begin_svg_tag: > + if line.count('<svg') > 0: > + found_begin_svg_tag = True > + partials = line.split('<svg') > + found_close_tag, temp_string = _find_and_split( > + partials[1], '>', '', None) > + else: > + pass > + elif not found_close_tag: > + found_close_tag, temp_string = _find_and_split( > + line, '>', '', None) > + temp_string = '' > + elif not found_end_svg_tag: > + custom_svg += temp_string > + found_end_svg_tag, temp_string = _find_and_split( > + line, '</svg>', '', None) > + else: > + custom_svg += line > + fd_custom.close > + > + # Next, modify the old icon by shrinking it and applying the overlay. > + fd_old = open(os.path.join(home_activities, new_bundle_name, 'activity', > + old_icon_name + '.svg'), 'r') > + fd_new = open(os.path.join(home_activities, new_bundle_name, 'activity', > + new_icon_name + '.svg'), 'w') > + > + found_begin_svg_tag = False > + found_close_tag = False > + found_end_svg_tag = False > + > + for line in fd_old: > + if not found_begin_svg_tag: > + if line.count('<svg') > 0: > + found_begin_svg_tag = True > + partials = line.split('<svg') > + fd_new.write(partials[0] + '<svg\n') > + found_close_tag = _find_and_split(partials[1], '>', > + RESCALE, fd_new) > + else: > + fd_new.write(line) > + elif not found_close_tag: > + found_close_tag = _find_and_split(line, '>', RESCALE, fd_new) > + elif not found_end_svg_tag: > + found_end_svg_tag = _find_and_split( > + line, '</svg>', ' </g>\n' + custom_svg, fd_new, > + insert_before=True) > + else: > + fd_new.write(line) > + fd_old.close > + fd_new.close > + > + > +def _find_and_split(line, token, insert, fd, insert_before=False): > + ''' If token is found in line, split line, add insert, and write; > + else just write. ''' > + > + tmp_string = '' > + > + if line.count(token) > 0: > + partials = line.split(token) > + if insert_before: > + tmp_string += insert > + tmp_string += partials[0] + token + '\n' > + else: > + tmp_string += partials[0] + token + '\n' > + tmp_string += insert > + tmp_string += partials[1] > + if len(partials) > 2: > + for i, part in enumerate(partials): > + if i > 1: > + tmp_string += part + token > + if fd is None: > + return True, tmp_string > + else: > + fd.write(tmp_string) > + return True > + else: > + if fd is None: > + return False, line > + else: > + fd.write(line) > + return False > diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py > index a1c0be3..f624d78 100644 > --- a/src/jarabe/view/viewsource.py > +++ b/src/jarabe/view/viewsource.py > @@ -1,5 +1,6 @@ > # Copyright (C) 2008 One Laptop Per Child > # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer > +# Copyright (C) 2011 Walter Bender > # > # 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 > @@ -16,6 +17,8 @@ > # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA > > import os > +import sys > +import subprocess > import logging > from gettext import gettext as _ > > @@ -35,7 +38,10 @@ from sugar.graphics.radiotoolbutton import RadioToolButton > from sugar.bundle.activitybundle import ActivityBundle > from sugar.datastore import datastore > from sugar import mime > +from sugar import profile > > +from jarabe.view.customizebundle import customize_activity_info, \ > + generate_bundle > > _SOURCE_FONT = pango.FontDescription('Monospace %d' % style.FONT_SIZE) > > @@ -48,7 +54,6 @@ def setup_view_source(activity): > if service is not None: > try: > service.HandleViewSource() > - return > except dbus.DBusException, e: > expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', > 'org.freedesktop.DBus.Python.NotImplementedError'] > @@ -82,11 +87,17 @@ def setup_view_source(activity): > logging.exception('Exception occured in GetDocumentPath():') > > if bundle_path is None and document_path is None: > - _logger.debug('Activity without bundle_path nor document_path') > + _logger.debug('Activity has neither a bundle_path nor a > document_path') > return > > + sugar_source_paths = [None] > + for path in sys.path: > + if path.endswith('site-packages'): > + sugar_source_paths = [os.path.join(path, 'sugar')] > + break > + > view_source = ViewSource(window_xid, bundle_path, document_path, > - activity.get_title()) > + sugar_source_paths, activity.get_title()) > map_activity_to_window[window_xid] = view_source > view_source.show() > > @@ -94,10 +105,11 @@ def setup_view_source(activity): > class ViewSource(gtk.Window): > __gtype_name__ = 'SugarViewSource' > > - def __init__(self, window_xid, bundle_path, document_path, title): > + def __init__(self, window_xid, bundle_path, document_path, > + sugar_source_paths, title): > gtk.Window.__init__(self) > > - logging.debug('ViewSource paths: %r %r', bundle_path, document_path) > + _logger.debug('ViewSource paths: %r %r', bundle_path, document_path) > > self.set_decorated(False) > self.set_position(gtk.WIN_POS_CENTER_ALWAYS) > @@ -117,7 +129,8 @@ class ViewSource(gtk.Window): > self.add(vbox) > vbox.show() > > - toolbar = Toolbar(title, bundle_path, document_path) > + toolbar = Toolbar(title, bundle_path, document_path, > + sugar_source_paths) > vbox.pack_start(toolbar, expand=False) > toolbar.connect('stop-clicked', self.__stop_clicked_cb) > toolbar.connect('source-selected', self.__source_selected_cb) > @@ -127,26 +140,42 @@ class ViewSource(gtk.Window): > vbox.pack_start(pane) > pane.show() > > - self._selected_file = None > + self._selected_bundle_file = None > + self._selected_sugar_file = None > file_name = '' > > activity_bundle = ActivityBundle(bundle_path) > command = activity_bundle.get_command() > if len(command.split(' ')) > 1: > - name = command.split(' ')[1].split('.')[0] > - file_name = name + '.py' > + name = command.split(' ')[1].split('.')[-1] > + tmppath = command.split(' ')[1].replace('.', '/') > + file_name = tmppath[0:-(len(name) + 1)] + '.py' > path = os.path.join(activity_bundle.get_path(), file_name) > - self._selected_file = path > + self._selected_bundle_file = path > + > + # Split the tree pane into two vertical panes, one of which > + # will be hidden > + tree_panes = gtk.VPaned() > + tree_panes.show() > + > + self._bundle_source_viewer = FileViewer(bundle_path, file_name) > + self._bundle_source_viewer.connect('file-selected', > + self.__file_selected_cb) > + tree_panes.add1(self._bundle_source_viewer) > + self._bundle_source_viewer.show() > > - self._file_viewer = FileViewer(bundle_path, file_name) > - self._file_viewer.connect('file-selected', self.__file_selected_cb) > - pane.add1(self._file_viewer) > - self._file_viewer.show() > + self._sugar_source_viewer = FileViewer(sugar_source_paths, None) > + self._sugar_source_viewer.connect('file-selected', > + self.__file_selected_cb) > + tree_panes.add2(self._sugar_source_viewer) > + self._sugar_source_viewer.hide() > + > + pane.add1(tree_panes) > > self._source_display = SourceDisplay() > pane.add2(self._source_display) > self._source_display.show() > - self._source_display.file_path = self._selected_file > + self._source_display.file_path = self._selected_bundle_file > > if document_path is not None: > self._select_source(document_path) > @@ -173,12 +202,21 @@ class ViewSource(gtk.Window): > > def _select_source(self, path): > if os.path.isfile(path): > + _logger.debug('_select_source called with file: %r', path) > self._source_display.file_path = path > - self._file_viewer.hide() > - else: > - self._file_viewer.set_path(path) > - self._source_display.file_path = self._selected_file > - self._file_viewer.show() > + self._bundle_source_viewer.hide() > + self._sugar_source_viewer.hide() > + elif os.path.isdir(path): > + _logger.debug('_select_source called with path: %r', path) > + self._bundle_source_viewer.set_path(path) > + self._source_display.file_path = self._selected_bundle_file > + self._bundle_source_viewer.show() > + self._sugar_source_viewer.hide() > + else: # Sugar source paths > + _logger.debug('_select_source called with sugar source paths') > + self._source_display.file_path = self._selected_sugar_file > + self._bundle_source_viewer.hide() > + self._sugar_source_viewer.show() > > def __destroy_cb(self, window, document_path): > del map_activity_to_window[self._parent_window_xid] > @@ -193,7 +231,10 @@ class ViewSource(gtk.Window): > def __file_selected_cb(self, file_viewer, file_path): > if file_path is not None and os.path.isfile(file_path): > self._source_display.file_path = file_path > - self._selected_file = file_path > + if file_viewer == self._bundle_source_viewer: > + self._selected_bundle_file = file_path > + else: > + self._selected_sugar_file = file_path > else: > self._source_display.file_path = None > > @@ -201,7 +242,7 @@ class ViewSource(gtk.Window): > class DocumentButton(RadioToolButton): > __gtype_name__ = 'SugarDocumentButton' > > - def __init__(self, file_name, document_path, title): > + def __init__(self, file_name, document_path, title, bundle=False): > RadioToolButton.__init__(self) > > self._document_path = document_path > @@ -218,15 +259,40 @@ class DocumentButton(RadioToolButton): > self.set_icon_widget(icon) > icon.show() > > - menu_item = MenuItem(_('Keep')) > - icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU, > - xo_color=XoColor(self._color)) > + if bundle: > + menu_item = MenuItem(_('Clone')) > + icon = Icon(icon_name='edit-copy', icon_size=gtk.ICON_SIZE_MENU, > + xo_color=XoColor(self._color)) > + menu_item.connect('activate', self.__copy_to_home_cb) > + else: > + menu_item = MenuItem(_('Keep')) > + icon = Icon(icon_name='document-save', > + icon_size=gtk.ICON_SIZE_MENU, > + xo_color=XoColor(self._color)) > + menu_item.connect('activate', self.__keep_in_journal_cb) > + > menu_item.set_image(icon) > > - menu_item.connect('activate', self.__keep_in_journal_cb) > self.props.palette.menu.append(menu_item) > menu_item.show() > > + def __copy_to_home_cb(self, menu_item): > + ''' Make a local copy of the activity bundle in > + $HOME/Activities as MyActivity ''' > + > + # TODO: Check to see if a copy of activity already exisits > + # If so, alert the user before overwriting. > + home_activities = os.path.join(os.environ['HOME'], 'Activities') > + nick = profile.get_nick_name().replace(' ', '_') > + new_bundle_name = '%s_%s' % (nick, > self._document_path.split('/')[-1]) > + command_line = ['cp', '-r', self._document_path, > + os.path.join(home_activities, new_bundle_name)] > + _logger.debug(subprocess.call(command_line)) > + > + activity_name = customize_activity_info(nick, home_activities, > + new_bundle_name) > + generate_bundle(nick, activity_name, home_activities, > new_bundle_name) > + > def __keep_in_journal_cb(self, menu_item): > mime_type = mime.get_from_file_name(self._document_path) > if mime_type == 'application/octet-stream': > @@ -264,7 +330,7 @@ class Toolbar(gtk.Toolbar): > ([str])), > } > > - def __init__(self, title, bundle_path, document_path): > + def __init__(self, title, bundle_path, document_path, > sugar_source_paths): > gtk.Toolbar.__init__(self) > > document_button = None > @@ -283,7 +349,8 @@ class Toolbar(gtk.Toolbar): > self._add_separator() > > if bundle_path is not None and os.path.exists(bundle_path): > - activity_button = RadioToolButton() > + activity_button = DocumentButton(file_name, bundle_path, title, > + bundle=True) > icon = Icon(file=file_name, > icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, > fill_color=style.COLOR_TRANSPARENT.get_svg(), > @@ -299,6 +366,26 @@ class Toolbar(gtk.Toolbar): > activity_button.show() > self._add_separator() > > + if sugar_source_paths[0] is not None and \ > + os.path.exists(sugar_source_paths[0]): > + sugar_button = RadioToolButton() > + icon = Icon(icon_name='computer-xo', > + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, > + fill_color=style.COLOR_TRANSPARENT.get_svg(), > + stroke_color=style.COLOR_WHITE.get_svg()) > + sugar_button.set_icon_widget(icon) > + icon.show() > + if document_button is not None: > + sugar_button.props.group = document_button > + else: > + sugar_button.props.group = activity_button > + sugar_button.props.tooltip = _('Sugar Source') > + sugar_button.connect('toggled', self.__button_toggled_cb, > + sugar_source_paths) > + self.insert(sugar_button, -1) > + sugar_button.show() > + self._add_separator() > + > text = _('View source: %r') % title > label = gtk.Label() > label.set_markup('<b>%s</b>' % text) > @@ -381,20 +468,31 @@ class FileViewer(gtk.ScrolledWindow): > self.emit('file-selected', None) > if self._path == path: > return > - self._path = path > + > self._tree_view.set_model(gtk.TreeStore(str, str)) > + > + if type(path) == list: > + self._path = path[0] > + else: > + self._path = path > + > + self._model = self._tree_view.get_model() > self._add_dir_to_model(path) > > def _add_dir_to_model(self, dir_path, parent=None): > - model = self._tree_view.get_model() > + if type(dir_path) == list: > + for path in dir_path: > + self._add_dir_to_model(path) > + return > + > for f in os.listdir(dir_path): > - if not f.endswith('.pyc'): > + if not f.endswith(('.pyc', '.pyo', '.so', '.mo', '~')): > full_path = os.path.join(dir_path, f) > if os.path.isdir(full_path): > - new_iter = model.append(parent, [f, full_path]) > + new_iter = self._model.append(parent, [f, full_path]) > self._add_dir_to_model(full_path, new_iter) > else: > - current_iter = model.append(parent, [f, full_path]) > + current_iter = self._model.append(parent, [f, full_path]) > if f == self._initial_filename: > selection = self._tree_view.get_selection() > selection.select_iter(current_iter) > @@ -434,8 +532,8 @@ class SourceDisplay(gtk.ScrolledWindow): > self._file_path = None > > def _set_file_path(self, file_path): > - if file_path == self._file_path: > - return > + # if file_path == self._file_path: > + # return > self._file_path = file_path > > if self._file_path is None: > -- > 1.7.4.4 > > -- Walter Bender Sugar Labs http://www.sugarlabs.org _______________________________________________ Sugar-devel mailing list Sugar-devel@lists.sugarlabs.org http://lists.sugarlabs.org/listinfo/sugar-devel