[Sugar-devel] [PATCH] Add 'clone' to view source submenu

Walter Bender walter at sugarlabs.org
Mon Aug 15 14:29:26 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.

---

Note: This is a modification of an earlier version of the patch. In this
an emblem is overlaid onto the cloned activity icon. Also a logic error has
been fixed where cloning would fail if the overlay icon was not found.

Note: This version of the patch uses 'duplicate' rather than 'clone' as
per the discussion in the 2011-08-15 Design Team meeting and has a dependency
on the newly committed edit-duplicate icon.

 src/jarabe/view/Makefile.am        |    1 +
 src/jarabe/view/customizebundle.py |  206 ++++++++++++++++++++++++++++++++++++
 src/jarabe/view/viewsource.py      |   42 ++++++-
 3 files changed, 243 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..57434e9
--- /dev/null
+++ b/src/jarabe/view/customizebundle.py
@@ -0,0 +1,206 @@
+# 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 = 'emblem-view-source.svg'
+TRANSFORM = '  <g transform="matrix(0.4,0,0,0.4,32,32)">\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 = ''
+
+    info_old = open(os.path.join(home_activities, new_bundle_name,
+                                 'activity', 'activity.info'), 'r')
+    info_new = open(os.path.join(home_activities, new_bundle_name,
+                                 'activity', 'new_activity.info'), 'w')
+    for line in info_old:
+        tokens = line.split('=')
+        if tokens[0].rstrip() == 'bundle_id':
+            new_bundle_id = '%s_%s_clone' % (tokens[1].strip(), nick)
+            info_new.write('%s = %s\n' % (tokens[0].rstrip(), new_bundle_id))
+        elif tokens[0].rstrip() == 'activity_version':
+            info_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)
+            info_new.write('%s = %s\n' % (tokens[0].rstrip(), new_icon_name))
+        elif tokens[0].rstrip() == 'name':
+            info_new.write('%s = %s_%s\n' % (tokens[0].rstrip(), nick,
+                                           tokens[1].strip()))
+            activity_name = tokens[1].strip()
+        else:
+            info_new.write(line)
+    info_old.close
+    info_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 CUSTOMICON. '''
+
+    # First, find CUSTOMICON, which will be used as an overlay.
+    icon_path = None
+    for path in gtk.icon_theme_get_default().get_search_path():
+        if os.path.exists(os.path.join(path, 'sugar', 'scalable', 'emblems',
+                                       CUSTOMICON)):
+            icon_path = path
+            break
+
+    if icon_path is None:
+        # If we cannot find the overlay, just copy the old icon
+        _logger.debug('%s not found', CUSTOMICON)
+        command_line = ['cp', 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 CUSTOMICON.
+    fd_custom = open(os.path.join(icon_path, 'sugar', 'scalable', 'actions',
+                                CUSTOMICON), 'r')
+
+    temp_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:
+            temp_custom_svg += temp_string
+            found_end_svg_tag, temp_string = _find_and_split(
+                line, '</svg>', '', None)
+        else:
+            temp_custom_svg += line
+    fd_custom.close
+
+    # Next, modify CUSTOMICON by applying TRANSFORM
+    custom_svg = TRANSFORM + temp_custom_svg + '\n</g>\n'
+
+    # Finally, modify the old icon by applying the overlay.
+    icon_old = open(os.path.join(home_activities, new_bundle_name, 'activity',
+                             old_icon_name + '.svg'), 'r')
+    icon_new = open(os.path.join(home_activities, new_bundle_name, 'activity',
+                             new_icon_name + '.svg'), 'w')
+
+    found_end_svg_tag = False
+    for line in icon_old:
+        if not found_end_svg_tag:
+            found_end_svg_tag = _find_and_split(line, '</svg>', custom_svg,
+                                                icon_new, insert_before=True)
+        else:
+            icon_new.write(line)
+    icon_old.close
+    icon_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 9bc67ef..cafcdfc 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_SUFFIX = ('.pyc', '.pyo', '.so', '.o', '.a', '.la', '.mo', '~', '.xo',
@@ -251,7 +255,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
@@ -268,15 +272,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(_('Duplicate'))
+            icon = Icon(icon_name='edit-duplicate',
+                        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 [NICK]- '''
+
+        # 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':
@@ -335,7 +364,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