[Sugar-devel] [PATCH] view source enhancements: browse Sugar source and copy bundle source
Walter Bender
walter.bender at gmail.com
Mon Jun 13 15:33:34 EDT 2011
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 <walter at sugarlabs.org> wrote:
> From: Walter Bender <walter.bender at 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
More information about the Sugar-devel
mailing list