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

Walter Bender walter at sugarlabs.org
Mon Aug 22 23:47:34 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 version of the patch has a number of improvements suggested by
silbe and erikos.

 src/jarabe/view/Makefile.am        |    1 +
 src/jarabe/view/customizebundle.py |  198 ++++++++++++++++++++++++++++++++++++
 src/jarabe/view/viewsource.py      |   45 +++++++-
 3 files changed, 238 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..c547ac0
--- /dev/null
+++ b/src/jarabe/view/customizebundle.py
@@ -0,0 +1,198 @@
+# 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 glob
+import shutil
+
+import gtk
+
+import hashlib
+
+import sugar.profile
+from sugar.activity import bundlebuilder
+from sugar.datastore import datastore
+
+import logging
+_logger = logging.getLogger('ViewSource')
+
+
+BADGE_SUBPATH = 'emblems/emblem-view-source.svg'
+BADGE_TRANSFORM = '  <g transform="matrix(0.45,0,0,0.45,32,32)">\n'
+
+
+def generate_unique_id():
+    """ Generate an id based on the user's nick name and their public key
+    (Based on schema used by IRC activity). """
+
+    nick = sugar.profile.get_nick_name()
+    pubkey = sugar.profile.get_pubkey()
+    m = hashlib.sha1()
+    m.update(pubkey)
+    hexhash = m.hexdigest()
+
+    nick_letters = "".join([x for x in nick if x.isalpha()])
+
+    if not nick_letters:
+        nick_letters = 'XO'
+
+    return nick_letters + '_' + hexhash[:4]
+
+
+def generate_bundle(new_activity_name, user_activities_path, new_basename):
+    """ Generate a new .xo bundle for the activity and copy it into the
+    Journal. """
+
+    if os.path.exists(os.path.join(user_activities_path, new_basename,
+                                   'dist')):
+        for path in glob.glob(os.path.join(user_activities_path, new_basename,
+                                           'dist', '*')):
+            os.remove(path)
+
+    config = bundlebuilder.Config(source_dir=os.path.join(
+            user_activities_path, new_basename),
+            dist_name='%s-1.xo' % (new_activity_name))
+    bundlebuilder.cmd_fix_manifest(config, None)
+    bundlebuilder.cmd_dist_xo(config, None)
+
+    dsobject = datastore.create()
+    dsobject.metadata['title'] = '%s-1.xo' % (new_activity_name)
+    dsobject.metadata['mime_type'] = 'application/vnd.olpc-sugar'
+    dsobject.set_file_path(os.path.join(
+            user_activities_path, new_basename, 'dist',
+            '%s-1.xo' % (new_activity_name)))
+    datastore.write(dsobject)
+    dsobject.destroy()
+
+
+def customize_activity_info(nick, user_activities_path, new_basename):
+    """ 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;
+    (4) modify the activity icon by applying a customize overlay.
+    """
+    new_activity_name = ''
+
+    info_old = open(os.path.join(user_activities_path, new_basename,
+                                 'activity', 'activity.info'), 'r')
+    info_new = open(os.path.join(user_activities_path, new_basename,
+                                 'activity', 'new_activity.info'), 'w')
+
+    for line in info_old:
+        if line.find('=') < 0:
+            info_new.write(line)
+            continue
+        name, value = [token.strip() for token in line.split('=', 1)]
+        if name == 'bundle_id':
+            new_value = '%s_%s' % (value, nick)
+        elif name == 'activity_version':
+            new_value = '1'
+        elif name == 'icon':
+            new_value = value
+            icon_name = value
+        elif name == 'name':
+            new_value = '%s_copy_of_%s' % (nick, value)
+            new_activity_name = new_value
+        else:
+            info_new.write(line)
+            continue
+
+        info_new.write('%s = %s\n' % (name, new_value))
+
+    info_old.close()
+    info_new.close()
+
+    os.rename(os.path.join(user_activities_path, new_basename,
+                           'activity', 'new_activity.info'),
+              os.path.join(user_activities_path, new_basename,
+                           'activity', 'activity.info'))
+
+    _create_custom_icon(user_activities_path, new_basename, icon_name)
+
+    return new_activity_name
+
+
+def _create_custom_icon(user_activities_path, new_basename, icon_name):
+    """ Modify activity icon by overlaying a badge:
+    (1) Extract the payload from the badge icon;
+    (2) Add a transform to resize it and position it;
+    (3) Insert it into the activity icon. """
+
+    badge_path = None
+    for path in gtk.icon_theme_get_default().get_search_path():
+        if os.path.exists(os.path.join(path, 'sugar', 'scalable',
+                                       BADGE_SUBPATH)):
+            badge_path = path
+            break
+
+    if badge_path is None:
+        _logger.debug('%s not found', BADGE_SUBPATH)
+        return
+
+    # badge_payload is everything between <svg ...> and </svg>
+    badge_fd = open(os.path.join(badge_path, 'sugar', 'scalable',
+                                BADGE_SUBPATH), 'r')
+    badge_payload = ''
+    looking_for_svg_token = True
+    looking_for_close_token = True
+    looking_for_end_svg_token = True
+    for line in badge_fd:
+        if looking_for_svg_token:
+            if line.find('<svg') < 0:
+                continue
+            looking_for_svg_token = False
+            line = line.split('<svg', 1)[1]
+        if looking_for_close_token:
+            if line.find('>') < 0:
+                continue
+            looking_for_close_token = False
+            line = line.split('>', 1)[1]
+        if looking_for_end_svg_token:
+            if line.find('</svg>') < 0:
+                badge_payload += line
+                continue
+            badge_payload += line.split('</svg>')[0]
+            break
+    badge_fd.close()
+
+    badge_svg = BADGE_TRANSFORM + badge_payload + '\n</g>'
+
+    # Insert badge_svg just before </svg>
+    icon_path = os.path.join(user_activities_path, new_basename, 'activity',
+                             icon_name + '.svg')
+    icon_fd = open(icon_path, 'r')
+
+    icon_svg = ''
+    looking_for_end_svg_token = True
+    for line in icon_fd:
+        if looking_for_end_svg_token:
+            if line.find('</svg>') < 0:
+                icon_svg += line
+                continue
+            icon_svg += line.split('</svg>')[0] + badge_svg + '\n</svg>\n'
+            break
+
+    icon_fd.close()
+
+    tmp_path = os.path.join(user_activities_path, new_basename, 'activity',
+                            'tmp.svg')
+    tmp_icon_fd = open(tmp_path, 'w')
+    tmp_icon_fd.write(icon_svg)
+    tmp_icon_fd.close()
+
+    os.remove(icon_path)
+    os.rename(tmp_path, icon_path)
diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py
index 648e740..6c6eed8 100644
--- a/src/jarabe/view/viewsource.py
+++ b/src/jarabe/view/viewsource.py
@@ -17,6 +17,7 @@
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
 import os
+import shutil
 import logging
 from gettext import gettext as _
 
@@ -35,7 +36,10 @@ from sugar.graphics.toolbutton import ToolButton
 from sugar.graphics.radiotoolbutton import RadioToolButton
 from sugar.bundle.activitybundle import ActivityBundle
 from sugar.datastore import datastore
+from sugar.env import get_user_activities_path
 from sugar import mime
+from jarabe.view.customizebundle import customize_activity_info, \
+    generate_bundle, generate_unique_id
 
 
 _EXCLUDE_EXTENSIONS = ('.pyc', '.pyo', '.so', '.o', '.a', '.la', '.mo', '~',
@@ -206,7 +210,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
@@ -223,15 +227,43 @@ 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
+        user_activities_path """
+
+        user_activities_path = get_user_activities_path()
+        nick = generate_unique_id()
+        new_basename = '%s_copy_of_%s' % (
+            nick, os.path.basename(self._document_path))
+        if not os.path.exists(os.path.join(user_activities_path, new_basename)):
+            shutil.copytree(self._document_path,
+                            os.path.join(user_activities_path, new_basename),
+                            symlinks=True)
+            new_activity_name = customize_activity_info(
+                nick, user_activities_path, new_basename)
+            generate_bundle(new_activity_name, user_activities_path,
+                            new_basename)
+        else:
+            _logger.debug('%s already exists', new_basename)
+
     def __keep_in_journal_cb(self, menu_item):
         mime_type = mime.get_from_file_name(self._document_path)
         if mime_type == 'application/octet-stream':
@@ -288,7 +320,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