[Sugar-devel] [PATCH] Add clone to view-source submenu
Walter Bender
walter at sugarlabs.org
Wed Aug 24 11:26:24 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 and more robust handling of icon badge insertion.
Rebased to master 24 Aug 2011
src/jarabe/view/Makefile.am | 1 +
src/jarabe/view/customizebundle.py | 209 ++++++++++++++++++++++++++++++++++++
src/jarabe/view/viewsource.py | 46 +++++++-
3 files changed, 250 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..b6fd440
--- /dev/null
+++ b/src/jarabe/view/customizebundle.py
@@ -0,0 +1,209 @@
+# 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'
+ICON_TRANSFORM = ' <g transform="matrix(1.0,0,0,1.0,0,0)">\n'
+XML_HEADER = '<?xml version="1.0" ?> \
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \
+"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\n\
+<!ENTITY stroke_color "#010101">\n\
+<!ENTITY fill_color "#FFFFFF">\n]>\n'
+SVG_START = '<svg enable-background="new 0 0 55 55" height="55px" \
+version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" \
+xmlns="http://www.w3.org/2000/svg" \
+xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">\n'
+SVG_END = '</svg>\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_fd = open(os.path.join(badge_path, 'sugar', 'scalable',
+ BADGE_SUBPATH), 'r')
+ badge_payload = _extract_svg_payload(badge_fd)
+ badge_fd.close()
+
+ badge_svg = BADGE_TRANSFORM + badge_payload + '\n</g>'
+
+ icon_path = os.path.join(user_activities_path, new_basename, 'activity',
+ icon_name + '.svg')
+ icon_fd = open(icon_path, 'r')
+ icon_payload = _extract_svg_payload(icon_fd)
+ icon_fd.close()
+
+ icon_svg = ICON_TRANSFORM + icon_payload + '\n</g>'
+
+ tmp_path = os.path.join(user_activities_path, new_basename, 'activity',
+ 'tmp.svg')
+ tmp_icon_fd = open(tmp_path, 'w')
+ tmp_icon_fd.write(XML_HEADER)
+ tmp_icon_fd.write(SVG_START)
+ tmp_icon_fd.write(icon_svg)
+ tmp_icon_fd.write(badge_svg)
+ tmp_icon_fd.write(SVG_END)
+ tmp_icon_fd.close()
+
+ os.remove(icon_path)
+ os.rename(tmp_path, icon_path)
+
+
+def _extract_svg_payload(fd):
+ """ Returns everything between <svg ...> and </svg> """
+ payload = ''
+ looking_for_start_svg_token = True
+ looking_for_close_token = True
+ looking_for_end_svg_token = True
+ for line in fd:
+ if looking_for_start_svg_token:
+ if line.find('<svg') < 0:
+ continue
+ looking_for_start_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:
+ payload += line
+ continue
+ payload += line.split('</svg>')[0]
+ break
+ return payload
diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py
index 1edf061..2c6337c 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 sys
import logging
from gettext import gettext as _
@@ -36,7 +37,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', '~',
@@ -250,7 +254,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
@@ -267,15 +271,44 @@ 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':
@@ -334,7 +367,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