[Sugar-devel] [PATCH] add cloning submenu to viewsource toolbar

Walter Bender walter at sugarlabs.org
Mon Jul 4 13:58:44 EDT 2011


From: Walter Bender <walter.bender at gmail.com>

This patch adds a Clone submenu to the view source toolbar, enabling the
cloning of an activity for end-user modification.

As per the design team meeting, this patch includes the generation of an
.xo bundle to be copied to the Journal of the cloned activity.

One known bug not addressed here is that if the bundle_id is missing, the
cloning will be successful, but the resultant activity will not launch.

---
 src/jarabe/view/Makefile.am        |    1 +
 src/jarabe/view/customizebundle.py |  216 ++++++++++++++++++++++++++++++++++++
 src/jarabe/view/viewsource.py      |   42 ++++++-
 3 files changed, 253 insertions(+), 6 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 e12b546..74d0330 100644
--- a/src/jarabe/view/viewsource.py
+++ b/src/jarabe/view/viewsource.py
@@ -18,6 +18,7 @@
 
 import os
 import sys
+import subprocess
 import logging
 from gettext import gettext as _
 
@@ -37,6 +38,9 @@ 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
 
 
 _EXCLUDE = ('.pyc', '.pyo', '.so', '.o', '.a', '.la', '.mo', '~', '.xo',
@@ -248,7 +252,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
@@ -265,15 +269,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':
@@ -330,7 +359,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(),
-- 
1.7.4.4



More information about the Sugar-devel mailing list