# 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 # 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 shutil import sys import logging from gettext import gettext as _ from gi.repository import GObject from gi.repository import Pango from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkX11 from gi.repository import GtkSource import dbus from gi.repository import GConf from sugar3.graphics import style from sugar3.graphics.icon import Icon from sugar3.graphics.xocolor import XoColor from sugar3.graphics.menuitem import MenuItem from sugar3.graphics.toolbutton import ToolButton from sugar3.graphics.radiotoolbutton import RadioToolButton from sugar3.bundle.activitybundle import ActivityBundle from sugar3.datastore import datastore from sugar3.env import get_user_activities_path from sugar3 import mime from jarabe.view import customizebundle _EXCLUDE_EXTENSIONS = ('.pyc', '.pyo', '.so', '.o', '.a', '.la', '.mo', '~', '.xo', '.tar', '.bz2', '.zip', '.gz') _EXCLUDE_NAMES = ['.deps', '.libs'] _SOURCE_FONT = Pango.FontDescription('Monospace %d' % style.FONT_SIZE) _logger = logging.getLogger('ViewSource') map_activity_to_window = {} def setup_view_source(activity): service = activity.get_service() 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'] if e.get_dbus_name() not in expected_exceptions: logging.exception('Exception occured in HandleViewSource():') except Exception: logging.exception('Exception occured in HandleViewSource():') window_xid = activity.get_xid() if window_xid is None: _logger.error('Activity without a window xid') return bundle_path = activity.get_bundle_path() if window_xid in map_activity_to_window: _logger.debug('Viewsource window already open for %s %s', window_xid, bundle_path) return document_path = None if service is not None: try: document_path = service.GetDocumentPath() except dbus.DBusException, e: expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', 'org.freedesktop.DBus.Python.NotImplementedError'] if e.get_dbus_name() not in expected_exceptions: logging.exception('Exception occured in GetDocumentPath():') except Exception: 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') return sugar_toolkit_path = None for path in sys.path: if path.endswith('site-packages'): if os.path.exists(os.path.join(path, 'sugar')): sugar_toolkit_path = os.path.join(path, 'sugar') break view_source = ViewSource(window_xid, bundle_path, document_path, sugar_toolkit_path, activity.get_title()) map_activity_to_window[window_xid] = view_source view_source.show() class ViewSource(Gtk.Window): __gtype_name__ = 'SugarViewSource' def __init__(self, window_xid, bundle_path, document_path, sugar_toolkit_path, title): Gtk.Window.__init__(self) _logger.debug('ViewSource paths: %r %r %r', bundle_path, document_path, sugar_toolkit_path) self.set_decorated(False) self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) self.set_border_width(style.LINE_WIDTH) width = Gdk.Screen.width() - style.GRID_CELL_SIZE * 2 height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 2 self.set_size_request(width, height) self._parent_window_xid = window_xid self._sugar_toolkit_path = sugar_toolkit_path self.connect('realize', self.__realize_cb) self.connect('destroy', self.__destroy_cb, document_path) self.connect('key-press-event', self.__key_press_event_cb) vbox = Gtk.VBox() self.add(vbox) vbox.show() toolbar = Toolbar(title, bundle_path, document_path, sugar_toolkit_path) vbox.pack_start(toolbar, False, True, 0) toolbar.connect('stop-clicked', self.__stop_clicked_cb) toolbar.connect('source-selected', self.__source_selected_cb) toolbar.show() pane = Gtk.HPaned() vbox.pack_start(pane, True, True, 0) pane.show() 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('.')[-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_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() file_name = 'env.py' self._selected_sugar_file = os.path.join(sugar_toolkit_path, file_name) self._sugar_source_viewer = FileViewer(sugar_toolkit_path, file_name) 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_bundle_file if document_path is not None: self._select_source(document_path) def _calculate_char_width(self, char_count): widget = Gtk.Label(label='') context = widget.get_pango_context() pango_font = context.load_font(_SOURCE_FONT) metrics = pango_font.get_metrics() return Pango.PIXELS(metrics.get_approximate_char_width()) * char_count def __realize_cb(self, widget): self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.get_window().set_accept_focus(True) display = Gdk.Display.get_default() parent = GdkX11.X11Window.foreign_new_for_display( \ display, self._parent_window_xid) self.set_transient_for(parent) def __stop_clicked_cb(self, widget): self.destroy() def __source_selected_cb(self, widget, path): self._select_source(path) 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._bundle_source_viewer.hide() self._sugar_source_viewer.hide() elif path == self._sugar_toolkit_path: _logger.debug('_select_source called with sugar toolkit path: %r', path) self._sugar_source_viewer.set_path(path) self._source_display.file_path = self._selected_sugar_file self._sugar_source_viewer.show() self._bundle_source_viewer.hide() else: _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() def __destroy_cb(self, window, document_path): del map_activity_to_window[self._parent_window_xid] if document_path is not None and os.path.exists(document_path): os.unlink(document_path) def __key_press_event_cb(self, window, event): keyname = Gdk.keyval_name(event.keyval) if keyname == 'Escape': self.destroy() 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 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 class DocumentButton(RadioToolButton): __gtype_name__ = 'SugarDocumentButton' def __init__(self, file_name, document_path, title, bundle=False): RadioToolButton.__init__(self) self._document_path = document_path self._title = title self._jobject = None self.props.tooltip = _('Instance Source') client = GConf.Client.get_default() self._color = client.get_string('/desktop/sugar/user/color') icon = Icon(file=file_name, icon_size=Gtk.IconSize.LARGE_TOOLBAR, xo_color=XoColor(self._color)) self.set_icon_widget(icon) icon.show() if bundle: menu_item = MenuItem(_('Duplicate')) icon = Icon(icon_name='edit-duplicate', icon_size=Gtk.IconSize.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.IconSize.MENU, xo_color=XoColor(self._color)) menu_item.connect('activate', self.__keep_in_journal_cb) menu_item.set_image(icon) 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 = customizebundle.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) customizebundle.generate_bundle(nick, 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': mime_type = mime.get_for_file(self._document_path) self._jobject = datastore.create() title = _('Source') + ': ' + self._title self._jobject.metadata['title'] = title self._jobject.metadata['keep'] = '0' self._jobject.metadata['buddies'] = '' self._jobject.metadata['preview'] = '' self._jobject.metadata['icon-color'] = self._color self._jobject.metadata['mime_type'] = mime_type self._jobject.metadata['source'] = '1' self._jobject.file_path = self._document_path datastore.write(self._jobject, transfer_ownership=True, reply_handler=self.__internal_save_cb, error_handler=self.__internal_save_error_cb) def __internal_save_cb(self): _logger.debug('Saved Source object to datastore.') self._jobject.destroy() def __internal_save_error_cb(self, err): _logger.debug('Error saving Source object to datastore: %s', err) self._jobject.destroy() class Toolbar(Gtk.Toolbar): __gtype_name__ = 'SugarViewSourceToolbar' __gsignals__ = { 'stop-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), 'source-selected': (GObject.SignalFlags.RUN_FIRST, None, ([str])), } def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): Gtk.Toolbar.__init__(self) document_button = None self.bundle_path = bundle_path self.sugar_toolkit_path = sugar_toolkit_path self._add_separator() activity_bundle = ActivityBundle(bundle_path) file_name = activity_bundle.get_icon() if document_path is not None and os.path.exists(document_path): document_button = DocumentButton(file_name, document_path, title) document_button.connect('toggled', self.__button_toggled_cb, document_path) self.insert(document_button, -1) document_button.show() self._add_separator() if bundle_path is not None and os.path.exists(bundle_path): activity_button = DocumentButton(file_name, bundle_path, title, bundle=True) icon = Icon(file=file_name, icon_size=Gtk.IconSize.LARGE_TOOLBAR, fill_color=style.COLOR_TRANSPARENT.get_svg(), stroke_color=style.COLOR_WHITE.get_svg()) activity_button.set_icon_widget(icon) icon.show() if document_button is not None: activity_button.props.group = document_button activity_button.props.tooltip = _('Activity Bundle Source') activity_button.connect('toggled', self.__button_toggled_cb, bundle_path) self.insert(activity_button, -1) activity_button.show() self._add_separator() if sugar_toolkit_path is not None: sugar_button = RadioToolButton() icon = Icon(icon_name='computer-xo', icon_size=Gtk.IconSize.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 Toolkit Source') sugar_button.connect('toggled', self.__button_toggled_cb, sugar_toolkit_path) self.insert(sugar_button, -1) sugar_button.show() self._add_separator() self.activity_title_text = _('View source: %s') % title self.sugar_toolkit_title_text = _('View source: %r') % 'Sugar Toolkit' self.label = Gtk.Label() self.label.set_markup('%s' % self.activity_title_text) self.label.set_alignment(0, 0.5) self._add_widget(self.label) self._add_separator(True) stop = ToolButton(icon_name='dialog-cancel') stop.set_tooltip(_('Close')) stop.connect('clicked', self.__stop_clicked_cb) self.insert(stop, -1) stop.show() def _add_separator(self, expand=False): separator = Gtk.SeparatorToolItem() separator.props.draw = False if expand: separator.set_expand(True) else: separator.set_size_request(style.DEFAULT_SPACING, -1) self.insert(separator, -1) separator.show() def _add_widget(self, widget, expand=False): tool_item = Gtk.ToolItem() tool_item.set_expand(expand) tool_item.add(widget) widget.show() self.insert(tool_item, -1) tool_item.show() def __stop_clicked_cb(self, button): self.emit('stop-clicked') def __button_toggled_cb(self, button, path): if button.props.active: self.emit('source-selected', path) if path == self.sugar_toolkit_path: self.label.set_markup('%s' % self.sugar_toolkit_title_text) else: # Use activity title for either bundle path or document path self.label.set_markup('%s' % self.activity_title_text) class FileViewer(Gtk.ScrolledWindow): __gtype_name__ = 'SugarFileViewer' __gsignals__ = { 'file-selected': (GObject.SignalFlags.RUN_FIRST, None, ([str])), } def __init__(self, path, initial_filename): Gtk.ScrolledWindow.__init__(self) self.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC self.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC self.set_size_request(style.GRID_CELL_SIZE * 3, -1) self._path = None self._initial_filename = initial_filename self._tree_view = Gtk.TreeView() self.add(self._tree_view) self._tree_view.show() self._tree_view.props.headers_visible = False selection = self._tree_view.get_selection() selection.connect('changed', self.__selection_changed_cb) cell = Gtk.CellRendererText() column = Gtk.TreeViewColumn() column.pack_start(cell, True) column.add_attribute(cell, 'text', 0) self._tree_view.append_column(column) self._tree_view.set_search_column(0) self.set_path(path) def set_path(self, path): self.emit('file-selected', None) if self._path == path: return self._path = path self._tree_view.set_model(Gtk.TreeStore(str, str)) self._model = self._tree_view.get_model() self._add_dir_to_model(path) def _add_dir_to_model(self, dir_path, parent=None): for f in os.listdir(dir_path): if f.endswith(_EXCLUDE_EXTENSIONS) or f in _EXCLUDE_NAMES: continue full_path = os.path.join(dir_path, f) if os.path.isdir(full_path): new_iter = self._model.append(parent, [f, full_path]) self._add_dir_to_model(full_path, new_iter) else: 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) def __selection_changed_cb(self, selection): model, tree_iter = selection.get_selected() if tree_iter is None: file_path = None else: file_path = model.get_value(tree_iter, 1) self.emit('file-selected', file_path) class SourceDisplay(Gtk.ScrolledWindow): __gtype_name__ = 'SugarSourceDisplay' def __init__(self): Gtk.ScrolledWindow.__init__(self) self.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC self.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC self._buffer = GtkSource.Buffer() self._buffer.set_highlight_syntax(True) self._source_view = GtkSource.View(buffer=self._buffer) self._source_view.set_editable(False) self._source_view.set_cursor_visible(True) self._source_view.set_show_line_numbers(True) self._source_view.set_show_right_margin(True) self._source_view.set_right_margin_position(80) #self._source_view.set_highlight_current_line(True) #FIXME: Ugly color self._source_view.modify_font(_SOURCE_FONT) self.add(self._source_view) self._source_view.show() self._file_path = None def _set_file_path(self, file_path): self._file_path = file_path if self._file_path is None: self._buffer.set_text('') return mime_type = mime.get_for_file(self._file_path) _logger.debug('Detected mime type: %r', mime_type) language_manager = GtkSource.LanguageManager.get_default() detected_language = None for language_id in language_manager.get_language_ids(): language = language_manager.get_language(language_id) if mime_type in language.get_mime_types(): detected_language = language break if detected_language is not None: _logger.debug('Detected language: %r', detected_language.get_name()) self._buffer.set_language(detected_language) self._buffer.set_text(open(self._file_path, 'r').read()) def _get_file_path(self): return self._file_path file_path = property(_get_file_path, _set_file_path)