# -*- coding: utf-8 -*- # Xedit External Tools plugin # Copyright (C) 2005-2006 Steve Frécinaux # # 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 __all__ = ('Manager', ) import xedit import gtk import gtksourceview2 as gsv import os.path from library import * from functions import * import hashlib from xml.sax import saxutils import gobject class LanguagesPopup(gtk.Window): COLUMN_NAME = 0 COLUMN_ID = 1 COLUMN_ENABLED = 2 def __init__(self, languages): gtk.Window.__init__(self, gtk.WINDOW_POPUP) self.set_default_size(200, 200) self.props.can_focus = True self.build() self.init_languages(languages) self.show() self.map() self.grab_add() gtk.gdk.keyboard_grab(self.window, False, 0L) gtk.gdk.pointer_grab(self.window, False, gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.PROXIMITY_IN_MASK | gtk.gdk.PROXIMITY_OUT_MASK, None, None, 0L) self.view.get_selection().select_path((0,)) def build(self): self.model = gtk.ListStore(str, str, bool) self.sw = gtk.ScrolledWindow() self.sw.show() self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) self.view = gtk.TreeView(self.model) self.view.show() self.view.set_headers_visible(False) column = gtk.TreeViewColumn() renderer = gtk.CellRendererToggle() column.pack_start(renderer, False) column.set_attributes(renderer, active=self.COLUMN_ENABLED) renderer.connect('toggled', self.on_language_toggled) renderer = gtk.CellRendererText() column.pack_start(renderer, True) column.set_attributes(renderer, text=self.COLUMN_NAME) self.view.append_column(column) self.view.set_row_separator_func(self.on_separator) self.sw.add(self.view) self.add(self.sw) def enabled_languages(self, model, path, piter, ret): enabled = model.get_value(piter, self.COLUMN_ENABLED) if path == (0,) and enabled: return True if enabled: ret.append(model.get_value(piter, self.COLUMN_ID)) return False def languages(self): ret = [] self.model.foreach(self.enabled_languages, ret) return ret def on_separator(self, model, piter): val = model.get_value(piter, self.COLUMN_NAME) return val == '-' def init_languages(self, languages): manager = gsv.LanguageManager() langs = xedit.language_manager_list_languages_sorted(manager, True) self.model.append([_('All languages'), None, not languages]) self.model.append(['-', None, False]) self.model.append([_('Plain Text'), 'plain', 'plain' in languages]) self.model.append(['-', None, False]) for lang in langs: self.model.append([lang.get_name(), lang.get_id(), lang.get_id() in languages]) def correct_all(self, model, path, piter, enabled): if path == (0,): return False model.set_value(piter, self.COLUMN_ENABLED, enabled) def on_language_toggled(self, renderer, path): piter = self.model.get_iter(path) enabled = self.model.get_value(piter, self.COLUMN_ENABLED) self.model.set_value(piter, self.COLUMN_ENABLED, not enabled) if path == '0': self.model.foreach(self.correct_all, False) else: self.model.set_value(self.model.get_iter_first(), self.COLUMN_ENABLED, False) def do_key_press_event(self, event): if event.keyval == gtk.keysyms.Escape: self.destroy() return True else: event.window = self.view.get_bin_window() return self.view.event(event) def do_key_release_event(self, event): event.window = self.view.get_bin_window() return self.view.event(event) def in_window(self, event, window=None): if not window: window = self.window geometry = window.get_geometry() origin = window.get_origin() return event.x_root >= origin[0] and \ event.x_root <= origin[0] + geometry[2] and \ event.y_root >= origin[1] and \ event.y_root <= origin[1] + geometry[3] def do_destroy(self): gtk.gdk.keyboard_ungrab(0L) gtk.gdk.pointer_ungrab(0L) return gtk.Window.do_destroy(self) def setup_event(self, event, window): fr = event.window.get_origin() to = window.get_origin() event.window = window event.x += fr[0] - to[0] event.y += fr[1] - to[1] def resolve_widgets(self, root): res = [root] if isinstance(root, gtk.Container): root.forall(lambda x, y: res.extend(self.resolve_widgets(x)), None) return res def resolve_windows(self, window): if not window: return [] res = [window] res.extend(window.get_children()) return res def propagate_mouse_event(self, event): allwidgets = self.resolve_widgets(self.get_child()) allwidgets.reverse() orig = [event.x, event.y] for widget in allwidgets: windows = self.resolve_windows(widget.window) windows.reverse() for window in windows: if not (window.get_events() & event.type): continue if self.in_window(event, window): self.setup_event(event, window) if widget.event(event): return True return False def do_button_press_event(self, event): if not self.in_window(event): self.destroy() else: return self.propagate_mouse_event(event) def do_button_release_event(self, event): if not self.in_window(event): self.destroy() else: return self.propagate_mouse_event(event) def do_scroll_event(self, event): return self.propagate_mouse_event(event) def do_motion_notify_event(self, event): return self.propagate_mouse_event(event) def do_enter_notify_event(self, event): return self.propagate_mouse_event(event) def do_leave_notify_event(self, event): return self.propagate_mouse_event(event) def do_proximity_in_event(self, event): return self.propagate_mouse_event(event) def do_proximity_out_event(self, event): return self.propagate_mouse_event(event) gobject.type_register(LanguagesPopup) class Manager: TOOL_COLUMN = 0 # For Tree NAME_COLUMN = 1 # For Combo def __init__(self, datadir): self.datadir = datadir self.dialog = None self._languages = {} self._tool_rows = {} self.build() def build(self): callbacks = { 'on_new_tool_button_clicked' : self.on_new_tool_button_clicked, 'on_remove_tool_button_clicked' : self.on_remove_tool_button_clicked, 'on_tool_manager_dialog_response' : self.on_tool_manager_dialog_response, 'on_tool_manager_dialog_focus_out': self.on_tool_manager_dialog_focus_out, 'on_accelerator_key_press' : self.on_accelerator_key_press, 'on_accelerator_focus_in' : self.on_accelerator_focus_in, 'on_accelerator_focus_out' : self.on_accelerator_focus_out, 'on_languages_button_clicked' : self.on_languages_button_clicked } # Load the "main-window" widget from the ui file. self.ui = gtk.Builder() self.ui.add_from_file(os.path.join(self.datadir, 'ui', 'tools.ui')) self.ui.connect_signals(callbacks) self.dialog = self.ui.get_object('tool-manager-dialog') self.view = self.ui.get_object('view') self.__init_tools_model() self.__init_tools_view() for name in ['input', 'output', 'applicability', 'save-files']: self.__init_combobox(name) self.do_update() def expand_from_doc(self, doc): row = None if doc: if doc.get_language(): lid = doc.get_language().get_id() if lid in self._languages: row = self._languages[lid] elif 'plain' in self._languages: row = self._languages['plain'] if not row and None in self._languages: row = self._languages[None] if not row: return self.view.expand_row(row.get_path(), False) self.view.get_selection().select_path(row.get_path()) def run(self, window): if self.dialog == None: self.build() # Open up language self.expand_from_doc(window.get_active_document()) self.dialog.set_transient_for(window) window.get_group().add_window(self.dialog) self.dialog.present() def add_accelerator(self, item): if not item.shortcut: return if item.shortcut in self.accelerators: if not item in self.accelerators[item.shortcut]: self.accelerators[item.shortcut].append(item) else: self.accelerators[item.shortcut] = [item] def remove_accelerator(self, item, shortcut=None): if not shortcut: shortcut = item.shortcut if not shortcut in self.accelerators: return self.accelerators[shortcut].remove(item) if not self.accelerators[shortcut]: del self.accelerators[shortcut] def add_tool_to_language(self, tool, language): if isinstance(language, gsv.Language): lid = language.get_id() else: lid = language if not lid in self._languages: piter = self.model.append(None, [language]) parent = gtk.TreeRowReference(self.model, self.model.get_path(piter)) self._languages[lid] = parent else: parent = self._languages[lid] piter = self.model.get_iter(parent.get_path()) child = self.model.append(piter, [tool]) if not tool in self._tool_rows: self._tool_rows[tool] = [] self._tool_rows[tool].append(gtk.TreeRowReference(self.model, self.model.get_path(child))) return child def add_tool(self, tool): manager = gsv.LanguageManager() ret = None for lang in tool.languages: l = manager.get_language(lang) if l: ret = self.add_tool_to_language(tool, l) elif lang == 'plain': ret = self.add_tool_to_language(tool, 'plain') if not ret: ret = self.add_tool_to_language(tool, None) self.add_accelerator(tool) return ret def __init_tools_model(self): self.tools = ToolLibrary() self.current_node = None self.script_hash = None self.accelerators = dict() self.model = gtk.TreeStore(object) self.view.set_model(self.model) for tool in self.tools.tree.tools: self.add_tool(tool) self.model.set_default_sort_func(self.sort_tools) self.model.set_sort_column_id(-1, gtk.SORT_ASCENDING) def sort_tools(self, model, iter1, iter2): # For languages, sort All before everything else, otherwise alphabetical t1 = model.get_value(iter1, self.TOOL_COLUMN) t2 = model.get_value(iter2, self.TOOL_COLUMN) if model.iter_parent(iter1) == None: if t1 == None: return -1 if t2 == None: return 1 def lang_name(lang): if isinstance(lang, gsv.Language): return lang.get_name() else: return _('Plain Text') n1 = lang_name(t1) n2 = lang_name(t2) else: n1 = t1.name n2 = t2.name return cmp(n1.lower(), n2.lower()) def __init_tools_view(self): # Tools column column = gtk.TreeViewColumn('Tools') renderer = gtk.CellRendererText() column.pack_start(renderer, False) renderer.set_property('editable', True) self.view.append_column(column) column.set_cell_data_func(renderer, self.get_cell_data_cb) renderer.connect('edited', self.on_view_label_cell_edited) renderer.connect('editing-started', self.on_view_label_cell_editing_started) self.selection_changed_id = self.view.get_selection().connect('changed', self.on_view_selection_changed, None) def __init_combobox(self, name): combo = self[name] combo.set_active(0) # Convenience function to get an object from its name def __getitem__(self, key): return self.ui.get_object(key) def set_active_by_name(self, combo_name, option_name): combo = self[combo_name] model = combo.get_model() piter = model.get_iter_first() while piter is not None: if model.get_value(piter, self.NAME_COLUMN) == option_name: combo.set_active_iter(piter) return True piter = model.iter_next(piter) return False def get_selected_tool(self): model, piter = self.view.get_selection().get_selected() if piter is not None: tool = model.get_value(piter, self.TOOL_COLUMN) if not isinstance(tool, Tool): tool = None return piter, tool else: return None, None def compute_hash(self, string): return hashlib.md5(string).hexdigest() def save_current_tool(self): if self.current_node is None: return if self.current_node.filename is None: self.current_node.autoset_filename() def combo_value(o, name): combo = o[name] return combo.get_model().get_value(combo.get_active_iter(), self.NAME_COLUMN) self.current_node.input = combo_value(self, 'input') self.current_node.output = combo_value(self, 'output') self.current_node.applicability = combo_value(self, 'applicability') self.current_node.save_files = combo_value(self, 'save-files') buf = self['commands'].get_buffer() script = buf.get_text(*buf.get_bounds()) h = self.compute_hash(script) if h != self.script_hash: # script has changed -> save it self.current_node.save_with_script([line + "\n" for line in script.splitlines()]) self.script_hash = h else: self.current_node.save() self.update_remove_revert() def clear_fields(self): self['accelerator'].set_text('') buf = self['commands'].get_buffer() buf.begin_not_undoable_action() buf.set_text('') buf.end_not_undoable_action() for nm in ('input', 'output', 'applicability', 'save-files'): self[nm].set_active(0) self['languages_label'].set_text(_('All Languages')) def fill_languages_button(self): if not self.current_node or not self.current_node.languages: self['languages_label'].set_text(_('All Languages')) else: manager = gsv.LanguageManager() langs = [] for lang in self.current_node.languages: if lang == 'plain': langs.append(_('Plain Text')) else: l = manager.get_language(lang) if l: langs.append(l.get_name()) self['languages_label'].set_text(', '.join(langs)) def fill_fields(self): node = self.current_node self['accelerator'].set_text(default(node.shortcut, '')) buf = self['commands'].get_buffer() script = default(''.join(node.get_script()), '') buf.begin_not_undoable_action() buf.set_text(script) buf.end_not_undoable_action() self.script_hash = self.compute_hash(script) contenttype = gio.content_type_guess(data=script) lmanager = xedit.get_language_manager() language = lmanager.guess_language(content_type=contenttype) if language is not None: buf.set_language(language) buf.set_highlight_syntax(True) else: buf.set_highlight_syntax(False) for nm in ('input', 'output', 'applicability', 'save-files'): model = self[nm].get_model() piter = model.get_iter_first() self.set_active_by_name(nm, default(node.__getattribute__(nm.replace('-', '_')), model.get_value(piter, self.NAME_COLUMN))) self.fill_languages_button() def update_remove_revert(self): piter, node = self.get_selected_tool() removable = node is not None and node.is_local() self['remove-tool-button'].set_sensitive(removable) self['revert-tool-button'].set_sensitive(removable) if node is not None and node.is_global(): self['remove-tool-button'].hide() self['revert-tool-button'].show() else: self['remove-tool-button'].show() self['revert-tool-button'].hide() def do_update(self): self.update_remove_revert() piter, node = self.get_selected_tool() self.current_node = node if node is not None: self.fill_fields() self['tool-table'].set_sensitive(True) else: self.clear_fields() self['tool-table'].set_sensitive(False) def language_id_from_iter(self, piter): if not piter: return None tool = self.model.get_value(piter, self.TOOL_COLUMN) if isinstance(tool, Tool): piter = self.model.iter_parent(piter) tool = self.model.get_value(piter, self.TOOL_COLUMN) if isinstance(tool, gsv.Language): return tool.get_id() elif tool: return 'plain' return None def selected_language_id(self): # Find current language if there is any model, piter = self.view.get_selection().get_selected() return self.language_id_from_iter(piter) def on_new_tool_button_clicked(self, button): self.save_current_tool() # block handlers while inserting a new item self.view.get_selection().handler_block(self.selection_changed_id) self.current_node = Tool(self.tools.tree); self.current_node.name = _('New tool') self.tools.tree.tools.append(self.current_node) lang = self.selected_language_id() if lang: self.current_node.languages = [lang] piter = self.add_tool(self.current_node) self.view.set_cursor(self.model.get_path(piter), self.view.get_column(self.TOOL_COLUMN), True) self.fill_fields() self['tool-table'].set_sensitive(True) self.view.get_selection().handler_unblock(self.selection_changed_id) def tool_changed(self, tool, refresh=False): for row in self._tool_rows[tool]: self.model.row_changed(row.get_path(), self.model.get_iter(row.get_path())) if refresh and tool == self.current_node: self.fill_fields() self.update_remove_revert() def on_remove_tool_button_clicked(self, button): piter, node = self.get_selected_tool() if not node: return if node.is_global(): shortcut = node.shortcut if node.parent.revert_tool(node): self.remove_accelerator(node, shortcut) self.add_accelerator(node) self['revert-tool-button'].set_sensitive(False) self.fill_fields() self.tool_changed(node) else: parent = self.model.iter_parent(piter) language = self.language_id_from_iter(parent) self.model.remove(piter) if language in node.languages: node.languages.remove(language) self._tool_rows[node] = filter(lambda x: x.valid(), self._tool_rows[node]) if not self._tool_rows[node]: del self._tool_rows[node] if node.parent.delete_tool(node): self.remove_accelerator(node) self.current_node = None self.script_hash = None if self.model.iter_is_valid(piter): self.view.set_cursor(self.model.get_path(piter), self.view.get_column(self.TOOL_COLUMN), False) self.view.grab_focus() path = self._languages[language].get_path() parent = self.model.get_iter(path) if not self.model.iter_has_child(parent): self.model.remove(parent) del self._languages[language] def on_view_label_cell_edited(self, cell, path, new_text): if new_text != '': piter = self.model.get_iter(path) tool = self.model.get_value(piter, self.TOOL_COLUMN) tool.name = new_text self.save_current_tool() self.tool_changed(tool) def on_view_label_cell_editing_started(self, renderer, editable, path): piter = self.model.get_iter(path) tool = self.model.get_value(piter, self.TOOL_COLUMN) if isinstance(editable, gtk.Entry): editable.set_text(tool.name) editable.grab_focus() def on_view_selection_changed(self, selection, userdata): self.save_current_tool() self.do_update() def accelerator_collision(self, name, node): if not name in self.accelerators: return [] ret = [] for other in self.accelerators[name]: if not other.languages or not node.languages: ret.append(other) continue for lang in other.languages: if lang in node.languages: ret.append(other) continue return ret def set_accelerator(self, keyval, mod): # Check whether accelerator already exists self.remove_accelerator(self.current_node) name = gtk.accelerator_name(keyval, mod) if name == '': self.current_node.shorcut = None self.save_current_tool() return True col = self.accelerator_collision(name, self.current_node) if col: dialog = gtk.MessageDialog(self.dialog, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _('This accelerator is already bound to %s') % (', '.join(map(lambda x: x.name, col)),)) dialog.run() dialog.destroy() self.add_accelerator(self.current_node) return False self.current_node.shortcut = name self.add_accelerator(self.current_node) self.save_current_tool() return True def on_accelerator_key_press(self, entry, event): mask = event.state & gtk.accelerator_get_default_mod_mask() if event.keyval == gtk.keysyms.Escape: entry.set_text(default(self.current_node.shortcut, '')) self['commands'].grab_focus() return True elif event.keyval == gtk.keysyms.Delete \ or event.keyval == gtk.keysyms.BackSpace: entry.set_text('') self.remove_accelerator(self.current_node) self.current_node.shortcut = None self['commands'].grab_focus() return True elif event.keyval in range(gtk.keysyms.F1, gtk.keysyms.F12 + 1): # New accelerator if self.set_accelerator(event.keyval, mask): entry.set_text(default(self.current_node.shortcut, '')) self['commands'].grab_focus() # Capture all `normal characters` return True elif gtk.gdk.keyval_to_unicode(event.keyval): if mask: # New accelerator if self.set_accelerator(event.keyval, mask): entry.set_text(default(self.current_node.shortcut, '')) self['commands'].grab_focus() # Capture all `normal characters` return True else: return False def on_accelerator_focus_in(self, entry, event): if self.current_node is None: return if self.current_node.shortcut: entry.set_text(_('Type a new accelerator, or press Backspace to clear')) else: entry.set_text(_('Type a new accelerator')) def on_accelerator_focus_out(self, entry, event): if self.current_node is not None: entry.set_text(default(self.current_node.shortcut, '')) self.tool_changed(self.current_node) def on_tool_manager_dialog_response(self, dialog, response): if response == gtk.RESPONSE_HELP: xedit.help_display(self.dialog, 'xedit', 'xedit-external-tools-plugin') return self.on_tool_manager_dialog_focus_out(dialog, None) self.dialog.destroy() self.dialog = None self.tools = None def on_tool_manager_dialog_focus_out(self, dialog, event): self.save_current_tool() for window in xedit.app_get_default().get_windows(): helper = window.get_data("ExternalToolsPluginWindowData") helper.menu.update() def get_cell_data_cb(self, column, cell, model, piter): tool = model.get_value(piter, self.TOOL_COLUMN) if tool == None or not isinstance(tool, Tool): if tool == None: label = _('All Languages') elif not isinstance(tool, gsv.Language): label = _('Plain Text') else: label = tool.get_name() markup = saxutils.escape(label) editable = False else: escaped = saxutils.escape(tool.name) if tool.shortcut: markup = '%s (%s)' % (escaped, saxutils.escape(tool.shortcut)) else: markup = escaped editable = True cell.set_properties(markup=markup, editable=editable) def tool_in_language(self, tool, lang): if not lang in self._languages: return False ref = self._languages[lang] parent = ref.get_path() for row in self._tool_rows[tool]: path = row.get_path() if path[0] == parent[0]: return True return False def update_languages(self, popup): self.current_node.languages = popup.languages() self.fill_languages_button() piter, node = self.get_selected_tool() ret = None if node: ref = gtk.TreeRowReference(self.model, self.model.get_path(piter)) # Update languages, make sure to inhibit selection change stuff self.view.get_selection().handler_block(self.selection_changed_id) # Remove all rows that are no longer for row in list(self._tool_rows[self.current_node]): piter = self.model.get_iter(row.get_path()) language = self.language_id_from_iter(piter) if (not language and not self.current_node.languages) or \ (language in self.current_node.languages): continue # Remove from language self.model.remove(piter) self._tool_rows[self.current_node].remove(row) # If language is empty, remove it parent = self.model.get_iter(self._languages[language].get_path()) if not self.model.iter_has_child(parent): self.model.remove(parent) del self._languages[language] # Now, add for any that are new manager = gsv.LanguageManager() for lang in self.current_node.languages: if not self.tool_in_language(self.current_node, lang): l = manager.get_language(lang) if not l: l = 'plain' self.add_tool_to_language(self.current_node, l) if not self.current_node.languages and not self.tool_in_language(self.current_node, None): self.add_tool_to_language(self.current_node, None) # Check if we can still keep the current if not ref or not ref.valid(): # Change selection to first language path = self._tool_rows[self.current_node][0].get_path() piter = self.model.get_iter(path) parent = self.model.iter_parent(piter) # Expand parent, select child and scroll to it self.view.expand_row(self.model.get_path(parent), False) self.view.get_selection().select_path(path) self.view.set_cursor(path, self.view.get_column(self.TOOL_COLUMN), False) self.view.get_selection().handler_unblock(self.selection_changed_id) def on_languages_button_clicked(self, button): popup = LanguagesPopup(self.current_node.languages) popup.set_transient_for(self.dialog) origin = button.window.get_origin() popup.move(origin[0], origin[1] - popup.allocation.height) popup.connect('destroy', self.update_languages) # ex:et:ts=4: