From 528c1e5ff51e213936e800fc5a9a25da99c0bdf2 Mon Sep 17 00:00:00 2001 From: Perberos Date: Mon, 7 Nov 2011 16:46:58 -0300 Subject: initial --- plugins/externaltools/tools/Makefile.am | 23 + plugins/externaltools/tools/__init__.py | 281 +++++++++ plugins/externaltools/tools/capture.py | 214 +++++++ plugins/externaltools/tools/filelookup.py | 145 +++++ plugins/externaltools/tools/functions.py | 303 +++++++++ plugins/externaltools/tools/library.py | 493 +++++++++++++++ plugins/externaltools/tools/linkparsing.py | 231 +++++++ plugins/externaltools/tools/manager.py | 948 +++++++++++++++++++++++++++++ plugins/externaltools/tools/outputpanel.py | 224 +++++++ plugins/externaltools/tools/outputpanel.ui | 53 ++ plugins/externaltools/tools/tools.ui | 606 ++++++++++++++++++ 11 files changed, 3521 insertions(+) create mode 100755 plugins/externaltools/tools/Makefile.am create mode 100755 plugins/externaltools/tools/__init__.py create mode 100755 plugins/externaltools/tools/capture.py create mode 100755 plugins/externaltools/tools/filelookup.py create mode 100755 plugins/externaltools/tools/functions.py create mode 100755 plugins/externaltools/tools/library.py create mode 100755 plugins/externaltools/tools/linkparsing.py create mode 100755 plugins/externaltools/tools/manager.py create mode 100755 plugins/externaltools/tools/outputpanel.py create mode 100755 plugins/externaltools/tools/outputpanel.ui create mode 100755 plugins/externaltools/tools/tools.ui (limited to 'plugins/externaltools/tools') diff --git a/plugins/externaltools/tools/Makefile.am b/plugins/externaltools/tools/Makefile.am new file mode 100755 index 00000000..5edcab58 --- /dev/null +++ b/plugins/externaltools/tools/Makefile.am @@ -0,0 +1,23 @@ +# Python snippets plugin + +plugindir = $(GEDIT_PLUGINS_LIBS_DIR)/externaltools +plugin_PYTHON = \ + __init__.py \ + capture.py \ + library.py \ + functions.py \ + manager.py \ + outputpanel.py \ + filelookup.py \ + linkparsing.py + +uidir = $(GEDIT_PLUGINS_DATA_DIR)/externaltools/ui +ui_DATA = tools.ui \ + outputpanel.ui + +EXTRA_DIST = $(ui_DATA) + +CLEANFILES = *.bak *.gladep +DISTCLEANFILES = *.bak *.gladep + +-include $(top_srcdir)/git.mk diff --git a/plugins/externaltools/tools/__init__.py b/plugins/externaltools/tools/__init__.py new file mode 100755 index 00000000..a46aef8f --- /dev/null +++ b/plugins/externaltools/tools/__init__.py @@ -0,0 +1,281 @@ +# -*- coding: UTF-8 -*- +# Gedit 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__ = ('ExternalToolsPlugin', 'ExternalToolsWindowHelper', + 'Manager', 'OutputPanel', 'Capture', 'UniqueById') + +import gedit +import gtk +from manager import Manager +from library import ToolLibrary +from outputpanel import OutputPanel +from capture import Capture +from functions import * + +class ToolMenu(object): + ACTION_HANDLER_DATA_KEY = "ExternalToolActionHandlerData" + ACTION_ITEM_DATA_KEY = "ExternalToolActionItemData" + + def __init__(self, library, window, menupath): + super(ToolMenu, self).__init__() + self._library = library + self._window = window + self._menupath = menupath + + self._merge_id = 0 + self._action_group = gtk.ActionGroup("ExternalToolsPluginToolActions") + self._signals = [] + + self.update() + + def deactivate(self): + self.remove() + + def remove(self): + if self._merge_id != 0: + self._window.get_ui_manager().remove_ui(self._merge_id) + self._window.get_ui_manager().remove_action_group(self._action_group) + self._merge_id = 0 + + for action in self._action_group.list_actions(): + handler = action.get_data(self.ACTION_HANDLER_DATA_KEY) + + if handler is not None: + action.disconnect(handler) + + action.set_data(self.ACTION_ITEM_DATA_KEY, None) + action.set_data(self.ACTION_HANDLER_DATA_KEY, None) + + self._action_group.remove_action(action) + + accelmap = gtk.accel_map_get() + + for s in self._signals: + accelmap.disconnect(s) + + self._signals = [] + + def _insert_directory(self, directory, path): + manager = self._window.get_ui_manager() + + for item in directory.subdirs: + action_name = 'ExternalToolDirectory%X' % id(item) + action = gtk.Action(action_name, item.name.replace('_', '__'), None, None) + self._action_group.add_action(action) + + manager.add_ui(self._merge_id, path, + action_name, action_name, + gtk.UI_MANAGER_MENU, False) + + self._insert_directory(item, path + '/' + action_name) + + for item in directory.tools: + action_name = 'ExternalToolTool%X' % id(item) + action = gtk.Action(action_name, item.name.replace('_', '__'), item.comment, None) + handler = action.connect("activate", capture_menu_action, self._window, item) + + action.set_data(self.ACTION_ITEM_DATA_KEY, item) + action.set_data(self.ACTION_HANDLER_DATA_KEY, handler) + + # Make sure to replace accel + accelpath = '/ExternalToolsPluginToolActions/%s' % (action_name, ) + + if item.shortcut: + key, mod = gtk.accelerator_parse(item.shortcut) + gtk.accel_map_change_entry(accelpath, key, mod, True) + + self._signals.append(gtk.accel_map_get().connect('changed::%s' % (accelpath,), self.on_accelmap_changed, item)) + + self._action_group.add_action_with_accel(action, item.shortcut) + + manager.add_ui(self._merge_id, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + + def on_accelmap_changed(self, accelmap, path, key, mod, tool): + tool.shortcut = gtk.accelerator_name(key, mod) + tool.save() + + self._window.get_data("ExternalToolsPluginWindowData").update_manager(tool) + + def update(self): + self.remove() + self._merge_id = self._window.get_ui_manager().new_merge_id() + self._insert_directory(self._library.tree, self._menupath) + self._window.get_ui_manager().insert_action_group(self._action_group, -1) + self.filter(self._window.get_active_document()) + + def filter_language(self, language, item): + if not item.languages: + return True + + if not language and 'plain' in item.languages: + return True + + if language and (language.get_id() in item.languages): + return True + else: + return False + + def filter(self, document): + if document is None: + return + + titled = document.get_uri() is not None + remote = not document.is_local() + + states = { + 'all' : True, + 'local': titled and not remote, + 'remote': titled and remote, + 'titled': titled, + 'untitled': not titled, + } + + language = document.get_language() + + for action in self._action_group.list_actions(): + item = action.get_data(self.ACTION_ITEM_DATA_KEY) + + if item is not None: + action.set_visible(states[item.applicability] and self.filter_language(language, item)) + +class ExternalToolsWindowHelper(object): + def __init__(self, plugin, window): + super(ExternalToolsWindowHelper, self).__init__() + + self._window = window + self._plugin = plugin + self._library = ToolLibrary() + + manager = window.get_ui_manager() + + self._action_group = gtk.ActionGroup('ExternalToolsPluginActions') + self._action_group.set_translation_domain('gedit') + self._action_group.add_actions([('ExternalToolManager', + None, + _('Manage _External Tools...'), + None, + _("Opens the External Tools Manager"), + lambda action: plugin.open_dialog()), + ('ExternalTools', + None, + _('External _Tools'), + None, + _("External tools"), + None)]) + manager.insert_action_group(self._action_group, -1) + + ui_string = """ + + + + + + + + + + + + + + + + """ + + self._merge_id = manager.add_ui_from_string(ui_string) + + self.menu = ToolMenu(self._library, self._window, + "/MenuBar/ToolsMenu/ToolsOps_4/ExternalToolsMenu/ExternalToolPlaceholder") + manager.ensure_update() + + # Create output console + self._output_buffer = OutputPanel(self._plugin.get_data_dir(), window) + bottom = window.get_bottom_panel() + bottom.add_item(self._output_buffer.panel, + _("Shell Output"), + gtk.STOCK_EXECUTE) + + def update_ui(self): + self.menu.filter(self._window.get_active_document()) + self._window.get_ui_manager().ensure_update() + + def deactivate(self): + manager = self._window.get_ui_manager() + self.menu.deactivate() + manager.remove_ui(self._merge_id) + manager.remove_action_group(self._action_group) + manager.ensure_update() + + bottom = self._window.get_bottom_panel() + bottom.remove_item(self._output_buffer.panel) + + def update_manager(self, tool): + self._plugin.update_manager(tool) + +class ExternalToolsPlugin(gedit.Plugin): + WINDOW_DATA_KEY = "ExternalToolsPluginWindowData" + + def __init__(self): + super(ExternalToolsPlugin, self).__init__() + + self._manager = None + self._manager_default_size = None + + ToolLibrary().set_locations(os.path.join(self.get_data_dir(), 'tools')) + + def activate(self, window): + helper = ExternalToolsWindowHelper(self, window) + window.set_data(self.WINDOW_DATA_KEY, helper) + + def deactivate(self, window): + window.get_data(self.WINDOW_DATA_KEY).deactivate() + window.set_data(self.WINDOW_DATA_KEY, None) + + def update_ui(self, window): + window.get_data(self.WINDOW_DATA_KEY).update_ui() + + def create_configure_dialog(self): + return self.open_dialog() + + def open_dialog(self): + if not self._manager: + self._manager = Manager(self.get_data_dir()) + + if self._manager_default_size: + self._manager.dialog.set_default_size(*self._manager_default_size) + + self._manager.dialog.connect('destroy', self.on_manager_destroy) + + window = gedit.app_get_default().get_active_window() + self._manager.run(window) + + return self._manager.dialog + + def update_manager(self, tool): + if not self._manager: + return + + self._manager.tool_changed(tool, True) + + def on_manager_destroy(self, dialog): + self._manager_default_size = [dialog.allocation.width, dialog.allocation.height] + self._manager = None + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/capture.py b/plugins/externaltools/tools/capture.py new file mode 100755 index 00000000..e47862c8 --- /dev/null +++ b/plugins/externaltools/tools/capture.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Gedit 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__ = ('Capture', ) + +import os, sys, signal +import locale +import subprocess +import gobject +import fcntl +import glib + +class Capture(gobject.GObject): + CAPTURE_STDOUT = 0x01 + CAPTURE_STDERR = 0x02 + CAPTURE_BOTH = 0x03 + CAPTURE_NEEDS_SHELL = 0x04 + + WRITE_BUFFER_SIZE = 0x4000 + + __gsignals__ = { + 'stdout-line' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), + 'stderr-line' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), + 'begin-execute': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, tuple()), + 'end-execute' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)) + } + + def __init__(self, command, cwd = None, env = {}): + gobject.GObject.__init__(self) + self.pipe = None + self.env = env + self.cwd = cwd + self.flags = self.CAPTURE_BOTH | self.CAPTURE_NEEDS_SHELL + self.command = command + self.input_text = None + + def set_env(self, **values): + self.env.update(**values) + + def set_command(self, command): + self.command = command + + def set_flags(self, flags): + self.flags = flags + + def set_input(self, text): + self.input_text = text + + def set_cwd(self, cwd): + self.cwd = cwd + + def execute(self): + if self.command is None: + return + + # Initialize pipe + popen_args = { + 'cwd' : self.cwd, + 'shell': self.flags & self.CAPTURE_NEEDS_SHELL, + 'env' : self.env + } + + if self.input_text is not None: + popen_args['stdin'] = subprocess.PIPE + if self.flags & self.CAPTURE_STDOUT: + popen_args['stdout'] = subprocess.PIPE + if self.flags & self.CAPTURE_STDERR: + popen_args['stderr'] = subprocess.PIPE + + self.tried_killing = False + self.idle_write_id = 0 + self.read_buffer = '' + + try: + self.pipe = subprocess.Popen(self.command, **popen_args) + except OSError, e: + self.pipe = None + self.emit('stderr-line', _('Could not execute command: %s') % (e, )) + return + + # Signal + self.emit('begin-execute') + + if self.flags & self.CAPTURE_STDOUT: + # Set non blocking + flags = fcntl.fcntl(self.pipe.stdout.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(self.pipe.stdout.fileno(), fcntl.F_SETFL, flags) + + gobject.io_add_watch(self.pipe.stdout, + gobject.IO_IN | gobject.IO_HUP, + self.on_output) + + if self.flags & self.CAPTURE_STDERR: + # Set non blocking + flags = fcntl.fcntl(self.pipe.stderr.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(self.pipe.stderr.fileno(), fcntl.F_SETFL, flags) + + gobject.io_add_watch(self.pipe.stderr, + gobject.IO_IN | gobject.IO_HUP, + self.on_output) + + # IO + if self.input_text is not None: + # Write async, in chunks of something + self.write_buffer = str(self.input_text) + + if self.idle_write_chunk(): + self.idle_write_id = gobject.idle_add(self.idle_write_chunk) + + # Wait for the process to complete + gobject.child_watch_add(self.pipe.pid, self.on_child_end) + + def idle_write_chunk(self): + if not self.pipe: + self.idle_write_id = 0 + return False + + try: + l = len(self.write_buffer) + m = min(l, self.WRITE_BUFFER_SIZE) + + self.pipe.stdin.write(self.write_buffer[:m]) + + if m == l: + self.write_buffer = '' + self.pipe.stdin.close() + + self.idle_write_id = 0 + + return False + else: + self.write_buffer = self.write_buffer[m:] + return True + except IOError: + self.pipe.stdin.close() + self.idle_write_id = 0 + + return False + + def on_output(self, source, condition): + if condition & (glib.IO_IN | glib.IO_PRI): + line = source.read() + + if len(line) > 0: + try: + line = unicode(line, 'utf-8') + except: + line = unicode(line, + locale.getdefaultlocale()[1], + 'replace') + + self.read_buffer += line + lines = self.read_buffer.splitlines(True) + + if not lines[-1].endswith("\n"): + self.read_buffer = lines[-1] + lines = lines[0:-1] + else: + self.read_buffer = '' + + for line in lines: + if not self.pipe or source == self.pipe.stdout: + self.emit('stdout-line', line) + else: + self.emit('stderr-line', line) + + if condition & ~(glib.IO_IN | glib.IO_PRI): + if self.read_buffer: + if source == self.pipe.stdout: + self.emit('stdout-line', self.read_buffer) + else: + self.emit('stderr-line', self.read_buffer) + + self.read_buffer = '' + + self.pipe = None + + return False + else: + return True + + def stop(self, error_code = -1): + if self.pipe is not None: + if self.idle_write_id: + gobject.source_remove(self.idle_write_id) + self.idle_write_id = 0 + + if not self.tried_killing: + os.kill(self.pipe.pid, signal.SIGTERM) + self.tried_killing = True + else: + os.kill(self.pipe.pid, signal.SIGKILL) + + def on_child_end(self, pid, error_code): + # In an idle, so it is emitted after all the std*-line signals + # have been intercepted + gobject.idle_add(self.emit, 'end-execute', error_code) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/filelookup.py b/plugins/externaltools/tools/filelookup.py new file mode 100755 index 00000000..229823b7 --- /dev/null +++ b/plugins/externaltools/tools/filelookup.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2010 Per Arneng +# +# 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 gio +import gedit + +class FileLookup: + """ + This class is responsible for looking up files given a part or the whole + path of a real file. The lookup is delegated to providers wich use different + methods of trying to find the real file. + """ + + def __init__(self): + self.providers = [] + self.providers.append(AbsoluteFileLookupProvider()) + self.providers.append(CwdFileLookupProvider()) + self.providers.append(OpenDocumentRelPathFileLookupProvider()) + self.providers.append(OpenDocumentFileLookupProvider()) + + def lookup(self, path): + """ + Tries to find a file specified by the path parameter. It delegates to + different lookup providers and the first match is returned. If no file + was found then None is returned. + + path -- the path to find + """ + found_file = None + for provider in self.providers: + found_file = provider.lookup(path) + if found_file is not None: + break + + return found_file + + +class FileLookupProvider: + """ + The base class of all file lookup providers. + """ + + def lookup(self, path): + """ + This method must be implemented by subclasses. Implementors will be + given a path and will try to find a matching file. If no file is found + then None is returned. + """ + raise NotImplementedError("need to implement a lookup method") + + +class AbsoluteFileLookupProvider(FileLookupProvider): + """ + This file tries to see if the path given is an absolute path and that the + path references a file. + """ + + def lookup(self, path): + if os.path.isabs(path) and os.path.isfile(path): + return gio.File(path) + else: + return None + + +class CwdFileLookupProvider(FileLookupProvider): + """ + This lookup provider tries to find a file specified by the path relative to + the current working directory. + """ + + def lookup(self, path): + try: + cwd = os.getcwd() + except OSError: + cwd = os.getenv('HOME') + + real_path = os.path.join(cwd, path) + + if os.path.isfile(real_path): + return gio.File(real_path) + else: + return None + + +class OpenDocumentRelPathFileLookupProvider(FileLookupProvider): + """ + Tries to see if the path is relative to any directories where the + currently open documents reside in. Example: If you have a document opened + '/tmp/Makefile' and a lookup is made for 'src/test2.c' then this class + will try to find '/tmp/src/test2.c'. + """ + + def lookup(self, path): + if path.startswith('/'): + return None + + for doc in gedit.app_get_default().get_documents(): + if doc.is_local(): + location = doc.get_location() + if location: + rel_path = location.get_parent().get_path() + joined_path = os.path.join(rel_path, path) + if os.path.isfile(joined_path): + return gio.File(joined_path) + + return None + + +class OpenDocumentFileLookupProvider(FileLookupProvider): + """ + Makes a guess that the if the path that was looked for matches the end + of the path of a currently open document then that document is the one + that is looked for. Example: If a document is opened called '/tmp/t.c' + and a lookup is made for 't.c' or 'tmp/t.c' then both will match since + the open document ends with the path that is searched for. + """ + + def lookup(self, path): + if path.startswith('/'): + return None + + for doc in gedit.app_get_default().get_documents(): + if doc.is_local(): + location = doc.get_location() + if location and location.get_uri().endswith(path): + return location + return None + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/functions.py b/plugins/externaltools/tools/functions.py new file mode 100755 index 00000000..0d2bfdbf --- /dev/null +++ b/plugins/externaltools/tools/functions.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +# Gedit 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 + +import os +import gtk +from gtk import gdk +import gio +import gedit +#import gtksourceview +from outputpanel import OutputPanel +from capture import * + +def default(val, d): + if val is not None: + return val + else: + return d + +def current_word(document): + piter = document.get_iter_at_mark(document.get_insert()) + start = piter.copy() + + if not piter.starts_word() and (piter.inside_word() or piter.ends_word()): + start.backward_word_start() + + if not piter.ends_word() and piter.inside_word(): + piter.forward_word_end() + + return (start, piter) + +# ==== Capture related functions ==== +def run_external_tool(window, node): + # Configure capture environment + try: + cwd = os.getcwd() + except OSError: + cwd = os.getenv('HOME'); + + capture = Capture(node.command, cwd) + capture.env = os.environ.copy() + capture.set_env(GEDIT_CWD = cwd) + + view = window.get_active_view() + if view is not None: + # Environment vars relative to current document + document = view.get_buffer() + uri = document.get_uri() + + # Current line number + piter = document.get_iter_at_mark(document.get_insert()) + capture.set_env(GEDIT_CURRENT_LINE_NUMBER=str(piter.get_line() + 1)) + + # Current line text + piter.set_line_offset(0) + end = piter.copy() + + if not end.ends_line(): + end.forward_to_line_end() + + capture.set_env(GEDIT_CURRENT_LINE=piter.get_text(end)) + + # Selected text (only if input is not selection) + if node.input != 'selection' and node.input != 'selection-document': + bounds = document.get_selection_bounds() + + if bounds: + capture.set_env(GEDIT_SELECTED_TEXT=bounds[0].get_text(bounds[1])) + + bounds = current_word(document) + capture.set_env(GEDIT_CURRENT_WORD=bounds[0].get_text(bounds[1])) + + capture.set_env(GEDIT_CURRENT_DOCUMENT_TYPE=document.get_mime_type()) + + if uri is not None: + gfile = gio.File(uri) + scheme = gfile.get_uri_scheme() + name = os.path.basename(uri) + capture.set_env(GEDIT_CURRENT_DOCUMENT_URI = uri, + GEDIT_CURRENT_DOCUMENT_NAME = name, + GEDIT_CURRENT_DOCUMENT_SCHEME = scheme) + if gedit.utils.uri_has_file_scheme(uri): + path = gfile.get_path() + cwd = os.path.dirname(path) + capture.set_cwd(cwd) + capture.set_env(GEDIT_CURRENT_DOCUMENT_PATH = path, + GEDIT_CURRENT_DOCUMENT_DIR = cwd) + + documents_uri = [doc.get_uri() + for doc in window.get_documents() + if doc.get_uri() is not None] + documents_path = [gio.File(uri).get_path() + for uri in documents_uri + if gedit.utils.uri_has_file_scheme(uri)] + capture.set_env(GEDIT_DOCUMENTS_URI = ' '.join(documents_uri), + GEDIT_DOCUMENTS_PATH = ' '.join(documents_path)) + + flags = capture.CAPTURE_BOTH + + if not node.has_hash_bang(): + flags |= capture.CAPTURE_NEEDS_SHELL + + capture.set_flags(flags) + + # Get input text + input_type = node.input + output_type = node.output + + # Get the panel + panel = window.get_data("ExternalToolsPluginWindowData")._output_buffer + panel.clear() + + if output_type == 'output-panel': + panel.show() + + # Assign the error output to the output panel + panel.set_process(capture) + + if input_type != 'nothing' and view is not None: + if input_type == 'document': + start, end = document.get_bounds() + elif input_type == 'selection' or input_type == 'selection-document': + try: + start, end = document.get_selection_bounds() + + print start, end + except ValueError: + if input_type == 'selection-document': + start, end = document.get_bounds() + + if output_type == 'replace-selection': + document.select_range(start, end) + else: + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + + elif input_type == 'line': + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + if not start.starts_line(): + start.set_line_offset(0) + if not end.ends_line(): + end.forward_to_line_end() + elif input_type == 'word': + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + if not start.inside_word(): + panel.write(_('You must be inside a word to run this command'), + panel.command_tag) + return + if not start.starts_word(): + start.backward_word_start() + if not end.ends_word(): + end.forward_word_end() + + input_text = document.get_text(start, end) + capture.set_input(input_text) + + # Assign the standard output to the chosen "file" + if output_type == 'new-document': + tab = window.create_tab(True) + view = tab.get_view() + document = tab.get_document() + pos = document.get_start_iter() + capture.connect('stdout-line', capture_stdout_line_document, document, pos) + document.begin_user_action() + view.set_editable(False) + view.set_cursor_visible(False) + elif output_type != 'output-panel' and output_type != 'nothing' and view is not None: + document.begin_user_action() + view.set_editable(False) + view.set_cursor_visible(False) + + if output_type == 'insert': + pos = document.get_iter_at_mark(document.get_mark('insert')) + elif output_type == 'replace-selection': + document.delete_selection(False, False) + pos = document.get_iter_at_mark(document.get_mark('insert')) + elif output_type == 'replace-document': + document.set_text('') + pos = document.get_end_iter() + else: + pos = document.get_end_iter() + capture.connect('stdout-line', capture_stdout_line_document, document, pos) + elif output_type != 'nothing': + capture.connect('stdout-line', capture_stdout_line_panel, panel) + document.begin_user_action() + + capture.connect('stderr-line', capture_stderr_line_panel, panel) + capture.connect('begin-execute', capture_begin_execute_panel, panel, view, node.name) + capture.connect('end-execute', capture_end_execute_panel, panel, view, output_type) + + # Run the command + capture.execute() + + if output_type != 'nothing': + document.end_user_action() + +class MultipleDocumentsSaver: + def __init__(self, window, docs, node): + self._window = window + self._node = node + self._error = False + + self._counter = len(docs) + self._signal_ids = {} + self._counter = 0 + + signals = {} + + for doc in docs: + signals[doc] = doc.connect('saving', self.on_document_saving) + gedit.commands.save_document(window, doc) + doc.disconnect(signals[doc]) + + def on_document_saving(self, doc, size, total_size): + self._counter += 1 + self._signal_ids[doc] = doc.connect('saved', self.on_document_saved) + + def on_document_saved(self, doc, error): + if error: + self._error = True + + doc.disconnect(self._signal_ids[doc]) + del self._signal_ids[doc] + + self._counter -= 1 + + if self._counter == 0 and not self._error: + run_external_tool(self._window, self._node) + +def capture_menu_action(action, window, node): + if node.save_files == 'document' and window.get_active_document(): + MultipleDocumentsSaver(window, [window.get_active_document()], node) + return + elif node.save_files == 'all': + MultipleDocumentsSaver(window, window.get_documents(), node) + return + + run_external_tool(window, node) + +def capture_stderr_line_panel(capture, line, panel): + if not panel.visible(): + panel.show() + + panel.write(line, panel.error_tag) + +def capture_begin_execute_panel(capture, panel, view, label): + view.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(gdk.Cursor(gdk.WATCH)) + + panel['stop'].set_sensitive(True) + panel.clear() + panel.write(_("Running tool:"), panel.italic_tag); + panel.write(" %s\n\n" % label, panel.bold_tag); + +def capture_end_execute_panel(capture, exit_code, panel, view, output_type): + panel['stop'].set_sensitive(False) + + if output_type in ('new-document','replace-document'): + doc = view.get_buffer() + start = doc.get_start_iter() + end = start.copy() + end.forward_chars(300) + + mtype = gio.content_type_guess(data=doc.get_text(start, end)) + lmanager = gedit.get_language_manager() + + language = lmanager.guess_language(doc.get_uri(), mtype) + + if language is not None: + doc.set_language(language) + + view.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(gdk.Cursor(gdk.XTERM)) + view.set_cursor_visible(True) + view.set_editable(True) + + if exit_code == 0: + panel.write("\n" + _("Done.") + "\n", panel.italic_tag) + else: + panel.write("\n" + _("Exited") + ":", panel.italic_tag) + panel.write(" %d\n" % exit_code, panel.bold_tag) + +def capture_stdout_line_panel(capture, line, panel): + panel.write(line) + +def capture_stdout_line_document(capture, line, document, pos): + document.insert(pos, line) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/library.py b/plugins/externaltools/tools/library.py new file mode 100755 index 00000000..6eb6ff1a --- /dev/null +++ b/plugins/externaltools/tools/library.py @@ -0,0 +1,493 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 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 + +import os +import re +import locale +import platform + +class Singleton(object): + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(Singleton, cls).__new__( + cls, *args, **kwargs) + cls._instance.__init_once__() + + return cls._instance + +class ToolLibrary(Singleton): + def __init_once__(self): + self.locations = [] + + def set_locations(self, datadir): + self.locations = [] + + if platform.platform() != 'Windows': + for d in self.get_xdg_data_dirs(): + self.locations.append(os.path.join(d, 'gedit-2', 'plugins', 'externaltools', 'tools')) + + self.locations.append(datadir) + + # self.locations[0] is where we save the custom scripts + if platform.platform() == 'Windows': + toolsdir = os.path.expanduser('~/gedit/tools') + else: + userdir = os.getenv('MATE22_USER_DIR') + if userdir: + toolsdir = os.path.join(userdir, 'gedit/tools') + else: + toolsdir = os.path.expanduser('~/.mate2/gedit/tools') + + self.locations.insert(0, toolsdir); + + if not os.path.isdir(self.locations[0]): + os.makedirs(self.locations[0]) + self.tree = ToolDirectory(self, '') + self.import_old_xml_store() + else: + self.tree = ToolDirectory(self, '') + + # cf. http://standards.freedesktop.org/basedir-spec/latest/ + def get_xdg_data_dirs(self): + dirs = os.getenv('XDG_DATA_DIRS') + if dirs: + dirs = dirs.split(os.pathsep) + else: + dirs = ('/usr/local/share', '/usr/share') + return dirs + + # This function is meant to be ran only once, when the tools directory is + # created. It imports eventual tools that have been saved in the old XML + # storage file. + def import_old_xml_store(self): + import xml.etree.ElementTree as et + userdir = os.getenv('MATE22_USER_DIR') + if userdir: + filename = os.path.join(userdir, 'gedit/gedit-tools.xml') + else: + filename = os.path.expanduser('~/.mate2/gedit/gedit-tools.xml') + + if not os.path.isfile(filename): + return + + print "External tools: importing old tools into the new store..." + + xtree = et.parse(filename) + xroot = xtree.getroot() + + for xtool in xroot: + for i in self.tree.tools: + if i.name == xtool.get('label'): + tool = i + break + else: + tool = Tool(self.tree) + tool.name = xtool.get('label') + tool.autoset_filename() + self.tree.tools.append(tool) + tool.comment = xtool.get('description') + tool.shortcut = xtool.get('accelerator') + tool.applicability = xtool.get('applicability') + tool.output = xtool.get('output') + tool.input = xtool.get('input') + + tool.save_with_script(xtool.text) + + def get_full_path(self, path, mode='r', system = True, local = True): + assert (system or local) + if path is None: + return None + if mode == 'r': + if system and local: + locations = self.locations + elif local and not system: + locations = [self.locations[0]] + elif system and not local: + locations = self.locations[1:] + else: + raise ValueError("system and local can't be both set to False") + + for i in locations: + p = os.path.join(i, path) + if os.path.lexists(p): + return p + return None + else: + path = os.path.join(self.locations[0], path) + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + os.mkdir(dirname) + return path + +class ToolDirectory(object): + def __init__(self, parent, dirname): + super(ToolDirectory, self).__init__() + self.subdirs = list() + self.tools = list() + if isinstance(parent, ToolDirectory): + self.parent = parent + self.library = parent.library + else: + self.parent = None + self.library = parent + self.dirname = dirname + self._load() + + def listdir(self): + elements = dict() + for l in self.library.locations: + d = os.path.join(l, self.dirname) + if not os.path.isdir(d): + continue + for i in os.listdir(d): + elements[i] = None + keys = elements.keys() + keys.sort() + return keys + + def _load(self): + for p in self.listdir(): + path = os.path.join(self.dirname, p) + full_path = self.library.get_full_path(path) + if os.path.isdir(full_path): + self.subdirs.append(ToolDirectory(self, p)) + elif os.path.isfile(full_path) and os.access(full_path, os.X_OK): + self.tools.append(Tool(self, p)) + + def get_path(self): + if self.parent is None: + return self.dirname + else: + return os.path.join(self.parent.get_path(), self.dirname) + path = property(get_path) + + def get_name(self): + return os.path.basename(self.dirname) + name = property(get_name) + + def delete_tool(self, tool): + # Only remove it if it lays in $HOME + if tool in self.tools: + path = tool.get_path() + if path is not None: + filename = os.path.join(self.library.locations[0], path) + if os.path.isfile(filename): + os.unlink(filename) + self.tools.remove(tool) + return True + else: + return False + + def revert_tool(self, tool): + # Only remove it if it lays in $HOME + filename = os.path.join(self.library.locations[0], tool.get_path()) + if tool in self.tools and os.path.isfile(filename): + os.unlink(filename) + tool._load() + return True + else: + return False + + +class Tool(object): + RE_KEY = re.compile('^([a-zA-Z_][a-zA-Z0-9_.\-]*)(\[([a-zA-Z_@]+)\])?$') + + def __init__(self, parent, filename = None): + super(Tool, self).__init__() + self.parent = parent + self.library = parent.library + self.filename = filename + self.changed = False + self._properties = dict() + self._transform = { + 'Languages': [self._to_list, self._from_list] + } + self._load() + + def _to_list(self, value): + if value.strip() == '': + return [] + else: + return map(lambda x: x.strip(), value.split(',')) + + def _from_list(self, value): + return ','.join(value) + + def _parse_value(self, key, value): + if key in self._transform: + return self._transform[key][0](value) + else: + return value + + def _load(self): + if self.filename is None: + return + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return + + fp = file(filename, 'r', 1) + in_block = False + lang = locale.getlocale(locale.LC_MESSAGES)[0] + + for line in fp: + if not in_block: + in_block = line.startswith('# [Gedit Tool]') + continue + if line.startswith('##') or line.startswith('# #'): continue + if not line.startswith('# '): break + + try: + (key, value) = [i.strip() for i in line[2:].split('=', 1)] + m = self.RE_KEY.match(key) + if m.group(3) is None: + self._properties[m.group(1)] = self._parse_value(m.group(1), value) + elif lang is not None and lang.startswith(m.group(3)): + self._properties[m.group(1)] = self._parse_value(m.group(1), value) + except ValueError: + break + fp.close() + self.changed = False + + def _set_property_if_changed(self, key, value): + if value != self._properties.get(key): + self._properties[key] = value + + self.changed = True + + def is_global(self): + return self.library.get_full_path(self.get_path(), local=False) is not None + + def is_local(self): + return self.library.get_full_path(self.get_path(), system=False) is not None + + def is_global(self): + return self.library.get_full_path(self.get_path(), local=False) is not None + + def get_path(self): + if self.filename is not None: + return os.path.join(self.parent.get_path(), self.filename) + else: + return None + path = property(get_path) + + # This command is the one that is meant to be ran + # (later, could have an Exec key or something) + def get_command(self): + return self.library.get_full_path(self.get_path()) + command = property(get_command) + + def get_applicability(self): + applicability = self._properties.get('Applicability') + if applicability: return applicability + return 'all' + def set_applicability(self, value): + self._set_property_if_changed('Applicability', value) + applicability = property(get_applicability, set_applicability) + + def get_name(self): + name = self._properties.get('Name') + if name: return name + return os.path.basename(self.filename) + def set_name(self, value): + self._set_property_if_changed('Name', value) + name = property(get_name, set_name) + + def get_shortcut(self): + shortcut = self._properties.get('Shortcut') + if shortcut: return shortcut + return None + def set_shortcut(self, value): + self._set_property_if_changed('Shortcut', value) + shortcut = property(get_shortcut, set_shortcut) + + def get_comment(self): + comment = self._properties.get('Comment') + if comment: return comment + return self.filename + def set_comment(self, value): + self._set_property_if_changed('Comment', value) + comment = property(get_comment, set_comment) + + def get_input(self): + input = self._properties.get('Input') + if input: return input + return 'nothing' + def set_input(self, value): + self._set_property_if_changed('Input', value) + input = property(get_input, set_input) + + def get_output(self): + output = self._properties.get('Output') + if output: return output + return 'output-panel' + def set_output(self, value): + self._set_property_if_changed('Output', value) + output = property(get_output, set_output) + + def get_save_files(self): + save_files = self._properties.get('Save-files') + if save_files: return save_files + return 'nothing' + def set_save_files(self, value): + self._set_property_if_changed('Save-files', value) + save_files = property(get_save_files, set_save_files) + + def get_languages(self): + languages = self._properties.get('Languages') + if languages: return languages + return [] + def set_languages(self, value): + self._set_property_if_changed('Languages', value) + languages = property(get_languages, set_languages) + + def has_hash_bang(self): + if self.filename is None: + return True + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return True + + fp = open(filename, 'r', 1) + for line in fp: + if line.strip() == '': + continue + + return line.startswith('#!') + + # There is no property for this one because this function is quite + # expensive to perform + def get_script(self): + if self.filename is None: + return ["#!/bin/sh\n"] + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return ["#!/bin/sh\n"] + + fp = open(filename, 'r', 1) + lines = list() + + # before entering the data block + for line in fp: + if line.startswith('# [Gedit Tool]'): + break + lines.append(line) + # in the block: + for line in fp: + if line.startswith('##'): continue + if not (line.startswith('# ') and '=' in line): + # after the block: strip one emtpy line (if present) + if line.strip() != '': + lines.append(line) + break + # after the block + for line in fp: + lines.append(line) + fp.close() + return lines + + def _dump_properties(self): + lines = ['# [Gedit Tool]'] + for item in self._properties.iteritems(): + if item[0] in self._transform: + lines.append('# %s=%s' % (item[0], self._transform[item[0]][1](item[1]))) + elif item[1] is not None: + lines.append('# %s=%s' % item) + return '\n'.join(lines) + '\n' + + def save_with_script(self, script): + filename = self.library.get_full_path(self.filename, 'w') + + fp = open(filename, 'w', 1) + + # Make sure to first print header (shebang, modeline), then + # properties, and then actual content + header = [] + content = [] + inheader = True + + # Parse + for line in script: + line = line.rstrip("\n") + + if not inheader: + content.append(line) + elif line.startswith('#!'): + # Shebang (should be always present) + header.append(line) + elif line.strip().startswith('#') and ('-*-' in line or 'ex:' in line or 'vi:' in line or 'vim:' in line): + header.append(line) + else: + content.append(line) + inheader = False + + # Write out header + for line in header: + fp.write(line + "\n") + + fp.write(self._dump_properties()) + fp.write("\n") + + for line in content: + fp.write(line + "\n") + + fp.close() + os.chmod(filename, 0750) + self.changed = False + + def save(self): + if self.changed: + self.save_with_script(self.get_script()) + + def autoset_filename(self): + if self.filename is not None: + return + dirname = self.parent.path + if dirname != '': + dirname += os.path.sep + + basename = self.name.lower().replace(' ', '-').replace('/', '-') + + if self.library.get_full_path(dirname + basename): + i = 2 + while self.library.get_full_path(dirname + "%s-%d" % (basename, i)): + i += 1 + basename = "%s-%d" % (basename, i) + self.filename = basename + +if __name__ == '__main__': + library = ToolLibrary() + + def print_tool(t, indent): + print indent * " " + "%s: %s" % (t.filename, t.name) + + def print_dir(d, indent): + print indent * " " + d.dirname + '/' + for i in d.subdirs: + print_dir(i, indent+1) + for i in d.tools: + print_tool(i, indent+1) + + print_dir(library.tree, 0) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/linkparsing.py b/plugins/externaltools/tools/linkparsing.py new file mode 100755 index 00000000..27b9ba89 --- /dev/null +++ b/plugins/externaltools/tools/linkparsing.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2010 Per Arneng +# +# 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 re + +class Link: + """ + This class represents a file link from within a string given by the + output of some software tool. A link contains a reference to a file, the + line number within the file and the boundaries within the given output + string that should be marked as a link. + """ + + def __init__(self, path, line_nr, start, end): + """ + path -- the path of the file (that could be extracted) + line_nr -- the line nr of the specified file + start -- the index within the string that the link starts at + end -- the index within the string where the link ends at + """ + self.path = path + self.line_nr = int(line_nr) + self.start = start + self.end = end + + def __repr__(self): + return "%s[%s](%s:%s)" % (self.path, self.line_nr, + self.start, self.end) + +class LinkParser: + """ + Parses a text using different parsing providers with the goal of finding one + or more file links within the text. A typical example could be the output + from a compiler that specifies an error in a specific file. The path of the + file, the line nr and some more info is then returned so that it can be used + to be able to navigate from the error output in to the specific file. + + The actual work of parsing the text is done by instances of classes that + inherits from AbstractLinkParser or by regular expressions. To add a new + parser just create a class that inherits from AbstractLinkParser and then + register in this class cunstructor using the method add_parser. If you want + to add a regular expression then just call add_regexp in this class + constructor and provide your regexp string as argument. + """ + + def __init__(self): + self._providers = [] + self.add_regexp(REGEXP_STANDARD) + self.add_regexp(REGEXP_PYTHON) + self.add_regexp(REGEXP_VALAC) + self.add_regexp(REGEXP_BASH) + self.add_regexp(REGEXP_RUBY) + self.add_regexp(REGEXP_PERL) + self.add_regexp(REGEXP_MCS) + + def add_parser(self, parser): + self._providers.append(parser) + + def add_regexp(self, regexp): + """ + Adds a regular expression string that should match a link using + re.MULTILINE and re.VERBOSE regexp. The area marked as a link should + be captured by a group named lnk. The path of the link should be + captured by a group named pth. The line number should be captured by + a group named ln. To read more about this look at the documentation + for the RegexpLinkParser constructor. + """ + self.add_parser(RegexpLinkParser(regexp)) + + def parse(self, text): + """ + Parses the given text and returns a list of links that are parsed from + the text. This method delegates to parser providers that can parse + output from different kinds of formats. If no links are found then an + empty list is returned. + + text -- the text to scan for file links. 'text' can not be None. + """ + if text is None: + raise ValueError("text can not be None") + + links = [] + + for provider in self._providers: + links.extend(provider.parse(text)) + + return links + +class AbstractLinkParser(object): + """The "abstract" base class for link parses""" + + def parse(self, text): + """ + This method should be implemented by subclasses. It takes a text as + argument (never None) and then returns a list of Link objects. If no + links are found then an empty list is expected. The Link class is + defined in this module. If you do not override this method then a + NotImplementedError will be thrown. + + text -- the text to parse. This argument is never None. + """ + raise NotImplementedError("need to implement a parse method") + +class RegexpLinkParser(AbstractLinkParser): + """ + A class that represents parsers that only use one single regular expression. + It can be used by subclasses or by itself. See the constructor documentation + for details about the rules surrouning the regexp. + """ + + def __init__(self, regex): + """ + Creates a new RegexpLinkParser based on the given regular expression. + The regular expression is multiline and verbose (se python docs on + compilation flags). The regular expression should contain three named + capturing groups 'lnk', 'pth' and 'ln'. 'lnk' represents the area wich + should be marked as a link in the text. 'pth' is the path that should + be looked for and 'ln' is the line number in that file. + """ + self.re = re.compile(regex, re.MULTILINE | re.VERBOSE) + + def parse(self, text): + links = [] + for m in re.finditer(self.re, text): + path = m.group("pth") + line_nr = m.group("ln") + start = m.start("lnk") + end = m.end("lnk") + link = Link(path, line_nr, start, end) + links.append(link) + + return links + +# gcc 'test.c:13: warning: ...' +# javac 'Test.java:13: ...' +# ruby 'test.rb:5: ...' +# scalac 'Test.scala:5: ...' +# 6g (go) 'test.go:9: ...' +REGEXP_STANDARD = r""" +^ +(?P + (?P .*[a-z0-9] ) + \: + (?P \d+) +) +\:\s""" + +# python ' File "test.py", line 13' +REGEXP_PYTHON = r""" +^\s\sFile\s +(?P + \" + (?P [^\"]+ ) + \",\sline\s + (?P \d+ ) +),""" + +# python 'test.sh: line 5:' +REGEXP_BASH = r""" +^(?P + (?P .* ) + \:\sline\s + (?P \d+ ) +)\:""" + +# valac 'Test.vala:13.1-13.3: ...' +REGEXP_VALAC = r""" +^(?P + (?P + .*vala + ) + \: + (?P + \d+ + ) + \.\d+-\d+\.\d+ + )\: """ + +#ruby +#test.rb:5: ... +# from test.rb:3:in `each' +# fist line parsed by REGEXP_STANDARD +REGEXP_RUBY = r""" +^\s+from\s +(?P + (?P + .* + ) + \: + (?P + \d+ + ) + )""" + +# perl 'syntax error at test.pl line 88, near "$fake_var' +REGEXP_PERL = r""" +\sat\s +(?P + (?P .* ) + \sline\s + (?P \d+ ) +)""" + +# mcs (C#) 'Test.cs(12,7): error CS0103: The name `fakeMethod' +REGEXP_MCS = r""" +^ +(?P + (?P .*\.[cC][sS] ) + \( + (?P \d+ ) + ,\d+\) +) +\:\s +""" + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/manager.py b/plugins/externaltools/tools/manager.py new file mode 100755 index 00000000..e28a088a --- /dev/null +++ b/plugins/externaltools/tools/manager.py @@ -0,0 +1,948 @@ +# -*- coding: utf-8 -*- +# Gedit 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 gedit +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 = gedit.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 = gedit.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: + gedit.help_display(self.dialog, 'gedit', 'gedit-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 gedit.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: diff --git a/plugins/externaltools/tools/outputpanel.py b/plugins/externaltools/tools/outputpanel.py new file mode 100755 index 00000000..a30aad72 --- /dev/null +++ b/plugins/externaltools/tools/outputpanel.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# Copyright (C) 2010 Per Arneng +# +# 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__ = ('OutputPanel', 'UniqueById') + +import gtk, gedit +import pango +import gobject +import os +from weakref import WeakKeyDictionary +from capture import * +from gtk import gdk +import re +import gio +import linkparsing +import filelookup + +class UniqueById: + __shared_state = WeakKeyDictionary() + + def __init__(self, i): + if i in self.__class__.__shared_state: + self.__dict__ = self.__class__.__shared_state[i] + return True + else: + self.__class__.__shared_state[i] = self.__dict__ + return False + + def states(self): + return self.__class__.__shared_state + +class OutputPanel(UniqueById): + def __init__(self, datadir, window): + if UniqueById.__init__(self, window): + return + + callbacks = { + 'on_stop_clicked' : self.on_stop_clicked, + 'on_view_visibility_notify_event': self.on_view_visibility_notify_event, + 'on_view_motion_notify_event': self.on_view_motion_notify_event, + 'on_view_button_press_event': self.on_view_button_press_event + } + + self.window = window + self.ui = gtk.Builder() + self.ui.add_from_file(os.path.join(datadir, 'ui', 'outputpanel.ui')) + self.ui.connect_signals(callbacks) + + self.panel = self["output-panel"] + self['view'].modify_font(pango.FontDescription('Monospace')) + + buffer = self['view'].get_buffer() + + self.normal_tag = buffer.create_tag('normal') + + self.error_tag = buffer.create_tag('error') + self.error_tag.set_property('foreground', 'red') + + self.italic_tag = buffer.create_tag('italic') + self.italic_tag.set_property('style', pango.STYLE_OBLIQUE) + + self.bold_tag = buffer.create_tag('bold') + self.bold_tag.set_property('weight', pango.WEIGHT_BOLD) + + self.invalid_link_tag = buffer.create_tag('invalid_link') + + self.link_tag = buffer.create_tag('link') + self.link_tag.set_property('underline', pango.UNDERLINE_SINGLE) + + self.link_cursor = gdk.Cursor(gdk.HAND2) + self.normal_cursor = gdk.Cursor(gdk.XTERM) + + self.process = None + + self.links = [] + + self.link_parser = linkparsing.LinkParser() + self.file_lookup = filelookup.FileLookup() + + def set_process(self, process): + self.process = process + + def __getitem__(self, key): + # Convenience function to get an object from its name + return self.ui.get_object(key) + + def on_stop_clicked(self, widget, *args): + if self.process is not None: + self.write("\n" + _('Stopped.') + "\n", + self.italic_tag) + self.process.stop(-1) + + def scroll_to_end(self): + iter = self['view'].get_buffer().get_end_iter() + self['view'].scroll_to_iter(iter, 0.0) + return False # don't requeue this handler + + def clear(self): + self['view'].get_buffer().set_text("") + self.links = [] + + def visible(self): + panel = self.window.get_bottom_panel() + return panel.props.visible and panel.item_is_active(self.panel) + + def write(self, text, tag = None): + buffer = self['view'].get_buffer() + + end_iter = buffer.get_end_iter() + insert = buffer.create_mark(None, end_iter, True) + + if tag is None: + buffer.insert(end_iter, text) + else: + buffer.insert_with_tags(end_iter, text, tag) + + # find all links and apply the appropriate tag for them + links = self.link_parser.parse(text) + for lnk in links: + + insert_iter = buffer.get_iter_at_mark(insert) + lnk.start = insert_iter.get_offset() + lnk.start + lnk.end = insert_iter.get_offset() + lnk.end + + start_iter = buffer.get_iter_at_offset(lnk.start) + end_iter = buffer.get_iter_at_offset(lnk.end) + + tag = None + + # if the link points to an existing file then it is a valid link + if self.file_lookup.lookup(lnk.path) is not None: + self.links.append(lnk) + tag = self.link_tag + else: + tag = self.invalid_link_tag + + buffer.apply_tag(tag, start_iter, end_iter) + + buffer.delete_mark(insert) + gobject.idle_add(self.scroll_to_end) + + def show(self): + panel = self.window.get_bottom_panel() + panel.show() + panel.activate_item(self.panel) + + def update_cursor_style(self, view, x, y): + if self.get_link_at_location(view, x, y) is not None: + cursor = self.link_cursor + else: + cursor = self.normal_cursor + + view.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor) + + def on_view_motion_notify_event(self, view, event): + if event.window == view.get_window(gtk.TEXT_WINDOW_TEXT): + self.update_cursor_style(view, int(event.x), int(event.y)) + + return False + + def on_view_visibility_notify_event(self, view, event): + if event.window == view.get_window(gtk.TEXT_WINDOW_TEXT): + x, y, m = event.window.get_pointer() + self.update_cursor_style(view, x, y) + + return False + + def idle_grab_focus(self): + self.window.get_active_view().grab_focus() + return False + + def get_link_at_location(self, view, x, y): + """ + Get the link under a specified x,y coordinate. If no link exists then + None is returned. + """ + + # get the offset within the buffer from the x,y coordinates + buff_x, buff_y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, + x, y) + iter_at_xy = view.get_iter_at_location(buff_x, buff_y) + offset = iter_at_xy.get_offset() + + # find the first link that contains the offset + for lnk in self.links: + if offset >= lnk.start and offset <= lnk.end: + return lnk + + # no link was found at x,y + return None + + def on_view_button_press_event(self, view, event): + if event.button != 1 or event.type != gdk.BUTTON_PRESS or \ + event.window != view.get_window(gtk.TEXT_WINDOW_TEXT): + return False + + link = self.get_link_at_location(view, int(event.x), int(event.y)) + if link is None: + return False + + gfile = self.file_lookup.lookup(link.path) + + if gfile: + gedit.commands.load_uri(self.window, gfile.get_uri(), None, + link.line_nr) + gobject.idle_add(self.idle_grab_focus) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/outputpanel.ui b/plugins/externaltools/tools/outputpanel.ui new file mode 100755 index 00000000..f0281792 --- /dev/null +++ b/plugins/externaltools/tools/outputpanel.ui @@ -0,0 +1,53 @@ + + + + + True + + + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + True + False + GTK_WRAP_WORD + False + False + + + + + + + + + + True + 6 + 6 + GTK_BUTTONBOX_END + + + True + False + gtk-stop + True + + + + + + False + 1 + + + + diff --git a/plugins/externaltools/tools/tools.ui b/plugins/externaltools/tools/tools.ui new file mode 100755 index 00000000..dff7d192 --- /dev/null +++ b/plugins/externaltools/tools/tools.ui @@ -0,0 +1,606 @@ + + + + + + + + + + + + Nothing + nothing + + + Current document + document + + + All documents + all + + + + + + + + + + + Nothing + nothing + + + Current document + document + + + Current selection + selection + + + Current selection (default to document) + selection-document + + + Current line + line + + + Current word + word + + + + + + + + + + + + + Nothing + nothing + + + Display in bottom pane + output-panel + + + Create new document + new-document + + + Append to current document + append-document + + + Replace current document + replace-document + + + Replace current selection + replace-selection + + + Insert at cursor position + insert + + + + + + + + + + + + + All documents + all + + + All documents except untitled ones + titled + + + Local files only + local + + + Remote files only + remote + + + Untitled documents only + untitled + + + + + True + + + External Tools Manager + 750 + 500 + dialog + True + False + + + + + True + + + True + True + 6 + 275 + + + True + 6 + + + True + 0 + _Tools: + True + view + + + False + False + 0 + + + + + True + False + automatic + automatic + in + + + True + True + False + True + + + + + 1 + + + + + True + 6 + + + True + False + True + False + + + + True + gtk-new + 4 + + + + + False + False + 0 + + + + + False + False + + + + True + gtk-revert-to-saved + 4 + + + + + False + False + end + 2 + + + + + True + False + True + False + + + + True + gtk-delete + 4 + + + + + False + False + end + 1 + + + + + False + False + 2 + + + + + False + False + + + + + True + 6 + + + True + 0 + 0.5 + _Edit: + commands + True + + + False + False + 0 + + + + + True + + + True + + + + False + False + 0 + + + + + True + 6 + 2 + 6 + 6 + + + True + True + + + + + + 1 + 2 + 1 + 2 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + + + True + model_applicability + + + + 0 + + + + + 0 + False + True + + + + + True + True + + + True + + + + True + All Languages + 0 + 0.5 + PANGO_ELLIPSIZE_MIDDLE + + + + + + + 1 + True + True + + + + + 1 + 2 + 5 + 6 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + model_output + + + + 0 + + + + + 1 + 2 + 4 + 5 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + model_input + + + + 0 + + + + + 1 + 2 + 3 + 4 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + model_save_files + True + + + + 0 + + + + + 1 + 2 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + 0 + _Applicability: + True + applicability + + + 5 + 6 + GTK_FILL + + + + + + True + 0 + _Output: + True + output + + + 4 + 5 + GTK_FILL + + + + + + True + 0 + _Input: + True + input + + + 3 + 4 + GTK_FILL + + + + + + 0 + _Save: + True + save-files + True + + + 2 + 3 + GTK_FILL + + + + + + True + 0 + _Shortcut Key: + True + accelerator + + + 1 + 2 + GTK_FILL + + + + + + True + True + automatic + automatic + in + + + commands_buffer + True + True + False + GTK_SOURCE_SMART_HOME_END_AFTER + 2 + True + False + True + + + + + 2 + + + + + 1 + + + + + 1 + + + + + True + False + + + + + 1 + + + + + True + end + + + gtk-help + True + True + True + False + True + + + False + False + 0 + + + + + gtk-close + True + True + True + False + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + button1 + button2 + + + -- cgit v1.2.1