# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer # # 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 logging import traceback from gettext import gettext as _ import gobject import pango import gtk import gtksourceview2 import dbus import gconf from sugar.graphics import style from sugar.graphics.icon import Icon from sugar.graphics.xocolor import XoColor from sugar.graphics.menuitem import MenuItem 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 import mime _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.error(traceback.format_exc()) except Exception: logging.error(traceback.format_exc()) 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.error(traceback.format_exc()) except Exception: logging.error(traceback.format_exc()) if bundle_path is None and document_path is None: _logger.debug('Activity without bundle_path nor document_path') return view_source = ViewSource(window_xid, bundle_path, document_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, title): gtk.Window.__init__(self) logging.debug('ViewSource paths: %r %r' % (bundle_path, document_path)) self.set_decorated(False) self.set_position(gtk.WIN_POS_CENTER_ALWAYS) self.set_border_width(style.LINE_WIDTH) width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 self.set_size_request(width, height) self._parent_window_xid = window_xid 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) vbox.pack_start(toolbar, expand=False) 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) pane.show() self._selected_file = None file_name = '' activity_bundle = ActivityBundle(bundle_path) command = activity_bundle.get_command() if len(command.split(' ')) > 1: name = command.split(' ')[1].split('.')[0] file_name = name + '.py' path = os.path.join(activity_bundle.get_path(), file_name) self._selected_file = path self._file_viewer = FileViewer(bundle_path, file_name) self._file_viewer.connect('file-selected', self.__file_selected_cb) pane.add1(self._file_viewer) self._file_viewer.show() self._source_display = SourceDisplay() pane.add2(self._source_display) self._source_display.show() self._source_display.file_path = self._selected_file if document_path is not None: self._select_source(document_path) logging.debug('@@@@@ %s' % document_path) def _calculate_char_width(self, char_count): widget = gtk.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.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.window.set_accept_focus(True) parent = gtk.gdk.window_foreign_new(self._parent_window_xid) self.window.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): self._source_display.file_path = path self._file_viewer.hide() else: self._file_viewer.set_path(path) self._source_display.file_path = self._selected_file self._file_viewer.show() 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 = gtk.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 self._selected_file = file_path else: self._source_display.file_path = None class DocumentButton(RadioToolButton): __gtype_name__ = 'SugarDocumentButton' def __init__(self, file_name, document_path, title): 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.ICON_SIZE_LARGE_TOOLBAR, xo_color=XoColor(self._color)) 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)) 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 __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): logging.debug("Saved Source object to datastore.") self._jobject.destroy() def __internal_save_error_cb(self, err): logging.debug("Error saving Source object to datastore: %s" % err) self._jobject.destroy() class Toolbar(gtk.Toolbar): __gtype_name__ = 'SugarViewSourceToolbar' __gsignals__ = { 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), 'source-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } def __init__(self, title, bundle_path, document_path): gtk.Toolbar.__init__(self) 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 = RadioToolButton() icon = Icon(file=file_name, icon_size=gtk.ICON_SIZE_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_path 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() text = _('View source: %r') % title label = gtk.Label() label.set_markup('%s' % text) label.set_alignment(0, 0.5) self._add_widget(label) self._add_separator(True) stop = ToolButton(icon_name='dialog-cancel') stop.set_tooltip(_('Close')) stop.connect('clicked', self.__stop_clicked_cb) stop.show() 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) class FileViewer(gtk.ScrolledWindow): __gtype_name__ = 'SugarFileViewer' __gsignals__ = { 'file-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } def __init__(self, path, initial_filename): gtk.ScrolledWindow.__init__(self) self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC self.props.vscrollbar_policy = gtk.POLICY_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._add_dir_to_model(path) def _add_dir_to_model(self, dir_path, parent=None): model = self._tree_view.get_model() for f in os.listdir(dir_path): if not f.endswith('.pyc'): full_path = os.path.join(dir_path, f) if os.path.isdir(full_path): new_iter = model.append(parent, [f, full_path]) self._add_dir_to_model(full_path, new_iter) else: current_iter = 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.POLICY_AUTOMATIC self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC self._buffer = gtksourceview2.Buffer() self._buffer.set_highlight_syntax(True) self._source_view = gtksourceview2.View(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): if file_path == self._file_path: return 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) logging.debug('Detected mime type: %r' % mime_type) language_manager = gtksourceview2.language_manager_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: logging.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)