[gedit-plugins] Added new plugin: Multi Edit



commit bdb2ccb701d51fb9c02194dc3ec1c9a1f4d900e1
Author: Jesse van den Kieboom <jesse vandenkieboom epfl ch>
Date:   Wed Nov 11 18:47:30 2009 +0100

    Added new plugin: Multi Edit
    
    The multi edit plugin allows you to edit a document in
    multiple places at once. When in multi-edit mode, additional
    insertion points can be inserted in the document. Additionally,
    in this mode, you can convert a text selection to a column
    selection and start multi editing in column mode.

 configure.ac                                       |   16 +-
 plugins/multiedit/Makefile.am                      |   20 +
 .../multiedit/multiedit.gedit-plugin.desktop.in.in |   11 +
 plugins/multiedit/multiedit/Makefile.am            |   12 +
 plugins/multiedit/multiedit/__init__.py            |   42 +
 plugins/multiedit/multiedit/constants.py           |   24 +
 plugins/multiedit/multiedit/documenthelper.py      |  877 ++++++++++++++++++++
 plugins/multiedit/multiedit/signals.py             |   92 ++
 plugins/multiedit/multiedit/windowhelper.py        |  120 +++
 po/POTFILES.in                                     |    3 +
 po/POTFILES.skip                                   |    1 +
 11 files changed, 1212 insertions(+), 6 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 2e07952..5694c1f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -89,9 +89,9 @@ ALL_PLUGINS="bookmarks showtabbar charmap drawspaces wordcompletion"
 USEFUL_PLUGINS="bookmarks showtabbar charmap drawspaces wordcompletion"
 DEFAULT_PLUGINS="bookmarks showtabbar charmap drawspaces wordcompletion"
 
-PYTHON_ALL_PLUGINS="bracketcompletion codecomment colorpicker joinlines sessionsaver smartspaces terminal"
-PYTHON_USEFUL_PLUGINS="bracketcompletion codecomment colorpicker joinlines sessionsaver smartspaces terminal"
-PYTHON_DEFAULT_PLUGINS="bracketcompletion codecomment colorpicker joinlines sessionsaver smartspaces terminal"
+PYTHON_ALL_PLUGINS="bracketcompletion codecomment colorpicker joinlines multiedit sessionsaver smartspaces terminal"
+PYTHON_USEFUL_PLUGINS="bracketcompletion codecomment colorpicker joinlines multiedit sessionsaver smartspaces terminal"
+PYTHON_DEFAULT_PLUGINS="bracketcompletion codecomment colorpicker joinlines multiedit sessionsaver smartspaces terminal"
 
 DIST_PLUGINS="$ALL_PLUGINS $PYTHON_ALL_PLUGINS"
 
@@ -112,9 +112,10 @@ AC_ARG_WITH([plugins],
 [  --with-plugins=plugin1,plugin2,...
 			  build the specified plugins. Available:
 			  bracketcompletion, charmap, codecomment,
-			  colorpicker, drawspaces, joinlines, showtabbar,
-			  sessionsaver, smartspaces,  terminal, wordcompletion,
-			  as well as the aliases default, all, and really-all],
+			  colorpicker, drawspaces, joinlines, multiedit,
+			  showtabbar, sessionsaver, smartspaces, terminal,
+			  wordcompletion, as well as the aliases
+			  default, all, and really-all],
 			  [plugins=$with_plugins],
 			  [plugins="default"])
 
@@ -426,6 +427,9 @@ plugins/drawspaces/Makefile
 plugins/drawspaces/drawspaces.gedit-plugin.desktop.in
 plugins/joinlines/Makefile
 plugins/joinlines/joinlines.gedit-plugin.desktop.in
+plugins/multiedit/Makefile
+plugins/multiedit/multiedit/Makefile
+plugins/multiedit/multiedit.gedit-plugin.desktop.in
 plugins/sessionsaver/Makefile
 plugins/sessionsaver/sessionsaver.gedit-plugin.desktop.in
 plugins/showtabbar/Makefile
diff --git a/plugins/multiedit/Makefile.am b/plugins/multiedit/Makefile.am
new file mode 100644
index 0000000..0f2b44f
--- /dev/null
+++ b/plugins/multiedit/Makefile.am
@@ -0,0 +1,20 @@
+# Multi Edit
+
+SUBDIRS = multiedit
+
+plugindir = $(GEDIT_PLUGINS_LIBS_DIR)
+
+plugin_in_files = multiedit.gedit-plugin.desktop.in
+
+%.gedit-plugin: %.gedit-plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po)
+	$(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.gedit-plugin.desktop.in=.gedit-plugin)
+
+EXTRA_DIST = $(plugin_in_files)
+
+CLEANFILES = $(plugin_DATA)
+
+DISTCLEANFILES = $(plugin_DATA)
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/multiedit/multiedit.gedit-plugin.desktop.in.in b/plugins/multiedit/multiedit.gedit-plugin.desktop.in.in
new file mode 100644
index 0000000..2834f45
--- /dev/null
+++ b/plugins/multiedit/multiedit.gedit-plugin.desktop.in.in
@@ -0,0 +1,11 @@
+[Gedit Plugin]
+Loader=python
+Module=multiedit
+IAge=2
+_Name=Multi Edit
+_Description=Edit document in multiple places at once
+Icon=gedit-plugin
+Authors=Jesse van den Kieboom <jessevdk gnome org>
+Copyright=Copyright © 2009 Jesse van den Kieboom
+Website=http://www.gedit.org
+Version=2.28.1
diff --git a/plugins/multiedit/multiedit/Makefile.am b/plugins/multiedit/multiedit/Makefile.am
new file mode 100644
index 0000000..3ff7d5c
--- /dev/null
+++ b/plugins/multiedit/multiedit/Makefile.am
@@ -0,0 +1,12 @@
+# Multi Edit
+
+plugindir = $(GEDIT_PLUGINS_LIBS_DIR)/multiedit
+
+plugin_PYTHON = 		\
+	constants.py 		\
+	documenthelper.py 	\
+	__init__.py 		\
+	signals.py 		\
+	windowhelper.py
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/multiedit/multiedit/__init__.py b/plugins/multiedit/multiedit/__init__.py
new file mode 100644
index 0000000..a170127
--- /dev/null
+++ b/plugins/multiedit/multiedit/__init__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+#
+#  multiedit.py - Multi Edit
+#  
+#  Copyright (C) 2009 - Jesse van den Kieboom
+#  
+#  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., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+import gedit
+from windowhelper import WindowHelper
+
+class MultiEditPlugin(gedit.Plugin):
+    def __init__(self):
+        gedit.Plugin.__init__(self)
+        self._instances = {}
+
+    def activate(self, window):
+        self._instances[window] = WindowHelper(self, window)
+        
+    def deactivate(self, window):
+        if window in self._instances:
+            self._instances[window].deactivate()
+            del self._instances[window]
+
+    def update_ui(self, window):
+        if window in self._instances:
+            self._instances[window].update_ui()
+
+# ex:ts=4:et:
diff --git a/plugins/multiedit/multiedit/constants.py b/plugins/multiedit/multiedit/constants.py
new file mode 100644
index 0000000..15647d9
--- /dev/null
+++ b/plugins/multiedit/multiedit/constants.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+#
+#  constants.py - Multi Edit
+#  
+#  Copyright (C) 2009 - Jesse van den Kieboom
+#  
+#  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., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+DOCUMENT_HELPER_KEY = 'GeditMultiEditPluginDocumentHelperKey'
+
+# ex:ts=4:et:
diff --git a/plugins/multiedit/multiedit/documenthelper.py b/plugins/multiedit/multiedit/documenthelper.py
new file mode 100644
index 0000000..44937a8
--- /dev/null
+++ b/plugins/multiedit/multiedit/documenthelper.py
@@ -0,0 +1,877 @@
+# -*- coding: utf-8 -*-
+#
+#  documenthelper.py - Multi Edit
+#
+#  Copyright (C) 2009 - Jesse van den Kieboom
+#
+#  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., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+import gedit
+import gtksourceview2 as gsv
+import gtk
+import glib
+
+from signals import Signals
+import constants
+import time
+import pango
+import xml.sax.saxutils
+
+from gpdefs import *
+
+try:
+    gettext.bindtextdomain(GETTEXT_PACKAGE, GP_LOCALEDIR)
+    _ = lambda s: gettext.dgettext(GETTEXT_PACKAGE, s);
+except:
+    _ = lambda s: s
+
+class DocumentHelper(Signals):
+    def __init__(self, view):
+        Signals.__init__(self)
+
+        view.set_data(constants.DOCUMENT_HELPER_KEY, self)
+
+        self._view = view
+        self._buffer = None
+        self._in_mode = False
+        self._column_mode = None
+
+        self._edit_points = []
+        self._multi_edited = False
+        self._status = None
+        self._status_timeout = 0
+        self._delete_mode_id = 0
+
+        self.connect_signal(self._view, 'notify::buffer', self.on_notify_buffer)
+        self.connect_signal(self._view, 'key-press-event', self.on_key_press_event)
+        self.connect_signal(self._view, 'expose-event', self.on_view_expose_event)
+        self.connect_signal(self._view, 'style-set', self.on_view_style_set)
+        self.connect_signal(self._view, 'undo', self.on_view_undo)
+        self.connect_signal(self._view, 'copy-clipboard', self.on_copy_clipboard)
+        self.connect_signal(self._view, 'cut-clipboard', self.on_cut_clipboard)
+        self.connect_signal(self._view, 'query-tooltip', self.on_query_tooltip)
+        
+        self._view.props.has_tooltip = True
+
+        self.reset_buffer(self._view.get_buffer())
+
+        self.initialize_event_handlers()
+
+    def _update_selection_tag(self):
+        style = self._view.get_style()
+
+        fg = style.text[gtk.STATE_SELECTED]
+        bg = style.base[gtk.STATE_SELECTED]
+
+        self._selection_tag.props.foreground_gdk = fg
+        self._selection_tag.props.background_gdk = bg
+
+    def reset_buffer(self, newbuf):
+        if self._buffer:
+            self.disable_multi_edit()
+
+            self.disconnect_signals(self._buffer)
+            self._buffer.get_tag_table().remove(self._selection_tag)
+            self._buffer.delete_mark(self._last_insert)
+
+        self._buffer = None
+
+        if newbuf == None or not isinstance(newbuf, gedit.Document):
+            return
+
+        if newbuf != None:
+            self.connect_signal_after(newbuf, 'insert-text', self.on_insert_text)
+
+            self.connect_signal(newbuf, 'delete-range', self.on_delete_range_before)
+            self.connect_signal_after(newbuf, 'delete-range', self.on_delete_range)
+
+            self.connect_signal_after(newbuf, 'mark-set', self.on_mark_set)
+            self.connect_signal(newbuf, 'notify::style-scheme', self.on_notify_style_scheme)
+
+            self._selection_tag = newbuf.create_tag(None)
+            self._selection_tag.set_priority(newbuf.get_tag_table().get_size() - 1)
+            self._last_insert = newbuf.create_mark(None, newbuf.get_iter_at_mark(newbuf.get_insert()), True)
+
+            self._update_selection_tag()
+
+        self._buffer = newbuf
+
+    def stop(self):
+        self._cancel_column_mode()
+        self.reset_buffer(None)
+
+        self._view.set_data(constants.DOCUMENT_HELPER_KEY, None)
+
+        self.disconnect_signals(self._view)
+        self._view = None
+        
+        if self._status_timeout != 0:
+            glib.source_remove(self._status_timeout)
+            self._status_timeout = 0
+        
+        if self._delete_mode_id != 0:
+            glib.source_remove(self._delete_mode_id)
+            self._delete_mode_id = 0
+
+    def initialize_event_handlers(self):
+        self._event_handlers = [
+            [('Escape',), 0, self.do_escape_mode, True],
+            [('Return',), 0, self.do_column_edit, True],
+            [('Home',), gtk.gdk.CONTROL_MASK, self.do_mark_start, True],
+            [('End',), gtk.gdk.CONTROL_MASK, self.do_mark_end, True],
+            [('e',), gtk.gdk.CONTROL_MASK, self.do_toggle_edit_point, True]
+        ]
+
+        for handler in self._event_handlers:
+            handler[0] = map(lambda x: gtk.gdk.keyval_from_name(x), handler[0])
+
+    def enable_multi_edit(self):
+        self._view.set_border_window_size(gtk.TEXT_WINDOW_TOP, 20)
+
+        if self._in_mode:
+            return
+
+        self._in_mode = True
+
+    def remove_edit_points(self):
+        buf = self._buffer
+
+        for mark in self._edit_points:
+            buf.delete_mark(mark)
+
+        self._edit_points = []
+        self._multi_edited = False
+        self._view.queue_draw()
+
+    def disable_multi_edit(self):
+        if self._column_mode:
+            self._cancel_column_mode()
+
+        self._in_mode = False
+
+        self._view.set_border_window_size(gtk.TEXT_WINDOW_TOP, 0)
+        self.remove_edit_points()
+
+    def do_escape_mode(self, event):
+        if self._column_mode:
+            self._cancel_column_mode()
+            return True
+
+        if self._edit_points:
+            self.remove_edit_points()
+            return True
+
+        self.disable_multi_edit()
+        return True
+
+    def iter_to_offset(self, piter):
+        tw = self._view.get_tab_width()
+        start = piter.copy()
+        start.set_line_offset(0)
+
+        offset = 0
+
+        while not start.equal(piter):
+            if start.get_char() == "\t":
+                offset += tw - (offset % tw)
+            else:
+                offset += 1
+
+            start.forward_char()
+
+        return offset
+
+    def get_visible_iter(self, line, offset):
+        piter = self._buffer.get_iter_at_line(line)
+        tw = self._view.get_tab_width()
+        visiblepos = 0
+
+        while visiblepos < offset:
+            if piter.get_char() == "\t":
+                visiblepos += (tw - (visiblepos % tw))
+            else:
+                visiblepos += 1
+
+            if not piter.forward_char() or piter.get_line() != line:
+                if piter.get_line() != line:
+                    piter.backward_char()
+                    visiblepos -= 1
+
+                return piter, offset - visiblepos
+
+        return piter, offset - visiblepos
+
+    def _delete_columns(self):
+        # Delete the text currently selected in column mode
+        # If a line ends before the column selection, simply don't do anything.
+        # Convert any tabs in the column selection to spaces
+        # Remove all characters on each line within the column selection
+        mode = self._column_mode
+        self._column_mode = None
+
+        buf = self._buffer
+        start = mode[0]
+        end = mode[1]
+
+        buf.begin_user_action()
+
+        while start <= end:
+            start_iter, start_offset = self.get_visible_iter(start, mode[2])
+            start += 1
+
+            if start_offset > 0:
+                # Only insert spaces
+                buf.insert(start_iter, ' ' * start_offset)
+                continue
+
+            prefix = ''
+
+            if start_offset < 0:
+                # We went one tab over the start, go one back
+                start_iter.backward_char()
+                prefix = ' ' * (-start_offset)
+
+            # Get the end one
+            end_iter, end_offset = self.get_visible_iter(start - 1, mode[3])
+            suffix = ''
+
+            if end_offset > 0:
+                # Delete until end of line
+                end_iter = start_iter.copy()
+
+                if not end_iter.ends_line():
+                    end_iter.forward_to_line_end()
+            elif end_offset < 0:
+                # Within tab
+                suffix = ' ' * (-end_offset)
+
+            buf.delete(start_iter, end_iter)
+            buf.insert(start_iter, prefix + suffix)
+
+        buf.end_user_action()
+
+    def _add_edit_point(self, piter):
+        # Check if there is already an edit point here
+        marks = piter.get_marks()
+        
+        for mark in marks:
+            if mark in self._edit_points:
+                return
+        
+        buf = self._buffer
+        mark = buf.create_mark(None, piter, True)
+        mark.set_visible(True)
+
+        self._edit_points.append(mark)
+        self.status('<i>%s</i>' % (xml.sax.saxutils.escape(_('Added edit point...'),)))
+
+    def _remove_duplicate_edit_points(self):
+        buf = self._buffer
+        
+        for mark in list(self._edit_points):
+            if mark.get_deleted():
+                continue
+
+            piter = buf.get_iter_at_mark(mark)
+
+            others = piter.get_marks()
+            others.remove(mark)
+            
+            for other in others:
+                if other in self._edit_points:
+                    buf.delete_mark(other)
+                    self._edit_points.remove(other)
+        
+        marks = buf.get_iter_at_mark(buf.get_insert()).get_marks()
+        
+        for mark in marks:
+            if mark in self._edit_points:
+                buf.delete_mark(mark)
+                self._edit_points.remove(mark)
+
+    def _invalidate_status(self):
+        if not self._in_mode:
+            return
+
+        window = self._view.get_window(gtk.TEXT_WINDOW_TOP)
+        geom = window.get_geometry()
+        window.invalidate_rect(gtk.gdk.Rectangle(0, 0, geom[2], geom[3]), False)
+
+    def _remove_status(self):
+        self._status = None
+        self._invalidate_status()
+        
+        self._status_timeout = 0
+        return False
+
+    def status(self, text):
+        if not self._in_mode:
+            self._status = None
+            return
+            
+        self._status = text
+        self._invalidate_status()
+        
+        if self._status_timeout != 0:
+            glib.source_remove(self._status_timeout)
+        
+        self._status_timeout = glib.timeout_add(3000, self._remove_status)
+
+    def _apply_column_mode(self):
+        mode = self._column_mode
+
+        # Delete the columns
+        self._delete_columns()
+
+        # Insert insertion marks at the start column
+        start = mode[0]
+        end = mode[1]
+        column = mode[2]
+        buf = self._buffer
+
+        while start <= end:
+            piter, offset = self.get_visible_iter(start, column)
+
+            if offset != 0:
+                sys.stderr.write('Wrong offset in applying column mode, should never happen: %d, %d' % (start, offset))
+            elif start != end:
+                # Add edit point for all lines, except last one
+                self._add_edit_point(piter)
+            else:
+                # For last line, just move the insertion point there
+                buf.move_mark(buf.get_insert(), piter)
+                buf.move_mark(buf.get_selection_bound(), piter)
+
+            start += 1
+
+        self._view.queue_draw()
+
+    def line_column_edit(self, piter, soff, eoff):
+        start, soff = self.get_visible_iter(piter.get_line(), soff)
+        end, eof = self.get_visible_iter(piter.get_line(), eoff)
+
+        if eof < 0:
+            end.backward_char()
+
+        # Apply tag to start -> end
+        if start.compare(end) < 0:
+            self._buffer.apply_tag(self._selection_tag, start, end)
+
+    def do_column_edit(self, event):
+        buf = self._buffer
+
+        bounds = buf.get_selection_bounds()
+
+        if not bounds or bounds[0].get_line() == bounds[1].get_line():
+            return False
+
+        # Determine the column edit range in character positions, for each line
+        # in the selection, determine where to put the edit with respect to tabs
+        # that might be in the selection. Set selection tags on normal text.
+        # If the column starts or stops in a tab, do custom overlay drawing
+        bounds[0].order(bounds[1])
+        start = bounds[0]
+        end = bounds[1]
+
+        tw = self._view.get_tab_width()
+        soff = self.iter_to_offset(start)
+        eoff = self.iter_to_offset(end)
+
+        if eoff < soff:
+            tmp = soff
+            soff = eoff
+            eoff = soff
+
+        # Apply tags where possible
+        start_line = start.get_line()
+        end_line = end.get_line()
+
+        while start.get_line() <= end.get_line():
+            self.line_column_edit(start, soff, eoff)
+
+            if not start.forward_line():
+                break
+
+        # Remove official selection
+        insert = buf.get_iter_at_mark(buf.get_insert())
+        buf.move_mark(buf.get_selection_bound(), insert)
+        
+        # Remove previous marks
+        self.remove_edit_points()
+
+        # Set the column mode
+        self._column_mode = (start_line, end_line, soff, eoff)
+        self.status('<i>%s</i>' % (xml.sax.saxutils.escape(_('Column Mode...')),))
+
+        return True
+
+    def _draw_column_mode(self, event):
+        if not self._column_mode:
+            return False
+
+        start = self._column_mode[0]
+        end = self._column_mode[1]
+        buf = self._buffer
+
+        col = self._view.get_style().base[gtk.STATE_SELECTED]
+        layout = self._view.create_pango_layout('W')
+        width = layout.get_pixel_extents()[1][2]
+
+        ctx = event.window.cairo_create()
+        ctx.rectangle(event.area)
+        ctx.clip()
+
+        ctx.set_source_color(col)
+
+        cstart = self._column_mode[2]
+        cend = self._column_mode[3]
+
+        while start <= end:
+            # Get the line range, convert to window coords, and see if it needs
+            # rendering
+            piter = buf.get_iter_at_line(start)
+            y, height = self._view.get_line_yrange(piter)
+
+            x_, y = self._view.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, y)
+            start += 1
+
+            # Check in visible area
+            if y >= event.area.y + event.area.height or \
+               y + height <= event.area.y:
+                continue
+
+            # Check where to possible draw fake selection
+            start_iter, soff = self.get_visible_iter(start - 1, cstart)
+            end_iter, eoff = self.get_visible_iter(start - 1, cend)
+
+            if soff == 0 and eoff == 0 and not start_iter.equal(end_iter):
+                continue
+
+            rx = cstart * width + self._view.get_left_margin()
+            rw = (cend - cstart) * width
+
+            if rw == 0:
+                rw = 1
+
+            ctx.rectangle(rx, y, rw, height)
+            ctx.fill()
+            
+        return False
+
+    def do_mark_start_end(self, test, move):
+        buf = self._buffer
+        bounds = buf.get_selection_bounds()
+
+        if bounds:
+            start = bounds[0]
+            end = bounds[1]
+        else:
+            start = buf.get_iter_at_mark(buf.get_insert())
+            end = start.copy()
+
+        start.order(end)
+        orig = start.copy()
+
+        while start.get_line() <= end.get_line():
+            if not test or not test(start):
+                move(start)
+
+            self._add_edit_point(start)
+
+            if not start.forward_line():
+                break
+        
+        return orig, end
+
+    def do_mark_start(self, event):
+        start, end = self.do_mark_start_end(None, lambda x: x.set_line_offset(0))
+        
+        start.backward_line()
+        
+        buf = self._buffer
+        buf.move_mark(buf.get_insert(), start)
+        buf.move_mark(buf.get_selection_bound(), start)
+        
+        return True
+
+    def do_mark_end(self, event):
+        start, end = self.do_mark_start_end(lambda x: x.ends_line(), lambda x: x.forward_to_line_end())
+        
+        end.forward_line()
+        
+        if not end.ends_line():
+            end.forward_to_line_end()
+        
+        buf = self._buffer
+        buf.move_mark(buf.get_insert(), end)
+        buf.move_mark(buf.get_selection_bound(), end)
+
+        return True
+        
+    def do_toggle_edit_point(self, event):
+        buf = self._buffer
+        piter = buf.get_iter_at_mark(buf.get_insert())
+        
+        marks = piter.get_marks()
+        
+        for mark in marks:
+            if mark in self._edit_points:
+                buf.delete_mark(mark)
+                self._edit_points.remove(mark)
+                
+                self.status('<i>%s</i>' % (xml.sax.saxutils.escape(_('Removed edit point...'),)))
+                return
+        
+        self._add_edit_point(piter)
+        return True
+        
+    def on_key_press_event(self, view, event):
+        defmod = gtk.accelerator_get_default_mod_mask() & event.state
+
+        for handler in self._event_handlers:
+            if (not handler[3] or self._in_mode) and event.keyval in handler[0] and (defmod == handler[1]):
+                return handler[2](event)
+
+        return False
+
+    def on_notify_style_scheme(self, buf, spec):
+        self._update_selection_tag()
+
+    def on_view_style_set(self, view, prev):
+        self._update_selection_tag()
+
+    def on_notify_buffer(self, view, spec):
+        self.reset_buffer(view.get_buffer())
+
+    def on_insert_text(self, buf, where, text, length):
+        if not self._in_mode:
+            return
+
+        self._remove_duplicate_edit_points()
+
+        self.block_signal(buf, 'insert-text')
+        buf.begin_user_action()
+
+        insert = buf.get_iter_at_mark(buf.get_insert())
+        atinsert = where.equal(insert)
+        wasat = buf.create_mark(None, where, True)
+
+        if self._column_mode:
+            self._apply_column_mode()
+
+        if self._edit_points and atinsert:
+            # Insert the text at all the edit points
+            for mark in self._edit_points:
+                piter = buf.get_iter_at_mark(mark)
+                
+                if not buf.get_iter_at_mark(buf.get_insert()).equal(piter):
+                    self._multi_edited = True
+                    buf.insert(piter, text)
+        else:
+            self.remove_edit_points()
+
+        iterwas = buf.get_iter_at_mark(wasat)
+
+        if hasattr(where, 'assign'):
+            where.assign(iterwas)
+        
+        if atinsert:
+            buf.move_mark(buf.get_insert(), iterwas)
+            buf.move_mark(buf.get_selection_bound(), iterwas)
+            
+        buf.delete_mark(wasat)
+        buf.end_user_action()
+        self.unblock_signal(buf, 'insert-text')
+
+    def on_delete_range_before(self, buf, start, end):
+        if not self._in_mode:
+            return
+
+        self._remove_duplicate_edit_points()
+        self._delete_text = start.get_text(end)
+        self._delete_length = abs(end.get_offset() - start.get_offset())
+
+        start.order(end)
+        self._is_backspace = start.compare(buf.get_iter_at_mark(buf.get_insert())) < 0
+
+    def handle_column_mode_delete(self, mark):
+        buf = self._buffer
+        start = buf.get_iter_at_mark(mark)
+        buf.delete_mark(mark)
+
+        self._view.set_editable(True)
+
+        # Reinsert what was deleted, and apply column mode
+        self.block_signal(buf, 'insert-text')
+
+        buf.begin_user_action()
+        buf.insert(start, self._delete_text)
+        self._apply_column_mode()
+        buf.end_user_action()
+
+        self.unblock_signal(buf, 'insert-text')
+        self._delete_mode_id = 0
+        return False
+
+    def on_delete_range(self, buf, start, end):
+        if self._column_mode:
+            # Ooooh, what a hack to be able to work with the undo manager
+            self._view.set_editable(False)
+            mark = buf.create_mark(None, start, True)
+            self._delete_mode_id = glib.timeout_add(0, self.handle_column_mode_delete, mark)
+        elif self._edit_points:
+            if start.equal(buf.get_iter_at_mark(buf.get_insert())):
+                self.block_signal(buf, 'delete-range')
+                buf.begin_user_action()
+                orig = buf.create_mark(None, start, True)
+
+                for mark in self._edit_points:
+                    piter = buf.get_iter_at_mark(mark)
+                    other = piter.copy()
+                    
+                    if self._is_backspace:
+                        # Remove 'delete_length' chars _before_ piter
+                        if not other.backward_chars(self._delete_length):
+                            continue
+                    else:
+                        # Remove 'delete_text' chars _after_ piter
+                        if not other.forward_chars(self._delete_length):
+                            continue
+
+                    if piter.equal(other):
+                        continue
+
+                    piter.order(other)
+                    buf.delete(piter, other)
+                    self._multi_edited = True
+
+                buf.end_user_action()
+                self.unblock_signal(buf, 'delete-range')
+                
+                piter = buf.get_iter_at_mark(orig)
+                buf.delete_mark(orig)
+                
+                # To be able to have it not crash with old pygtk
+                if hasattr(start, 'assign'):
+                    start.assign(piter)
+                    end.assign(piter)
+            else:
+                self.remove_edit_points()
+
+    def _cancel_column_mode(self):
+        if not self._column_mode:
+            return
+
+        self._column_mode = None
+
+        buf = self._buffer
+        bounds = buf.get_bounds()
+
+        buf.remove_tag(self._selection_tag, bounds[0], bounds[1])
+        
+        self.status('<i>%s</i>' % (xml.sax.saxutils.escape(_('Cancelled column mode...'),)))
+        self._view.queue_draw()
+
+    def _column_text(self):
+        if not self._column_mode:
+            return ''
+        
+        start = self._column_mode[0]
+        end = self._column_mode[1]
+        buf = self._buffer
+
+        cstart = self._column_mode[2]
+        cend = self._column_mode[3]
+        
+        lines = []
+        width = cend - cstart
+
+        while start <= end:
+            start_iter, soff = self.get_visible_iter(start, cstart)
+            end_iter, eoff = self.get_visible_iter(start, cend)
+            
+            if soff == 0 and eoff == 0:
+                # Just text
+                lines.append(start_iter.get_text(end_iter))
+            elif (soff < 0 and eoff < 0) or soff > 0:
+                # Only spaces
+                lines.append(' ' * width)
+            elif soff < 0:
+                # start to end_iter
+                lines.append((' ' * abs(soff)) + start_iter.get_text(end_iter))
+            elif eoff != 0:
+                # Draw from start_iter to end
+                if eoff < 0:
+                    end_iter.backward_char()
+                    eoff = self._view.get_tab_width() + eoff
+
+                lines.append(start_iter.get_text(end_iter) + (' ' * abs(eoff)))
+            else:
+                lines.append('')
+            
+            start += 1
+        
+        return "\n".join(lines)
+
+    def on_copy_clipboard(self, view):
+        if not self._column_mode:
+            return
+        
+        text = self._column_text()
+
+        clipboard = gtk.Clipboard(self._view.get_display())
+        clipboard.set_text(text)
+        
+        view.stop_emission('copy-clipboard')
+    
+    def on_cut_clipboard(self, view):
+        if not self._column_mode:
+            return
+        
+        text = self._column_text()
+        clipboard = gtk.Clipboard(self._view.get_display())
+        clipboard.set_text(text)
+        
+        view.stop_emission('cut-clipboard')
+        
+        self._apply_column_mode()
+
+    def on_mark_set(self, buf, where, mark):
+        if not mark == buf.get_insert():
+            return
+
+        if self._in_mode:
+            if self._column_mode != None:
+                # Cancel column mode when cursor moves
+                self._cancel_column_mode()
+            elif self._edit_points and self._multi_edited:
+                # Detect moving up or down a line
+                
+                diff = where.get_offset() - buf.get_iter_at_mark(self._last_insert).get_offset()
+
+                for point in self._edit_points:
+                    piter = buf.get_iter_at_mark(point)
+                    piter.set_offset(piter.get_offset() + diff)
+                    buf.move_mark(point, piter)
+
+                self._remove_duplicate_edit_points()
+
+        self._buffer.move_mark(self._last_insert, where)
+
+    def on_view_undo(self, view):
+        self._cancel_column_mode()
+        self.remove_edit_points()
+    
+    def make_label(self, text):
+        lbl = gtk.Label(text)
+        lbl.set_alignment(0, 0.5)
+        lbl.show()
+        
+        return lbl
+    
+    def on_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
+        if not self._in_mode:
+            return False
+        
+        geom = view.get_window(gtk.TEXT_WINDOW_TEXT).get_geometry()
+        geom2 = view.get_window(gtk.TEXT_WINDOW_LEFT).get_geometry()
+        
+        if y < geom[3] or x < geom2[2]:
+            return False
+
+        table = gtk.Table(4, 2)
+        table.set_row_spacings(3)
+        table.set_col_spacings(6)
+
+        table.attach(self.make_label('<Enter>:'), 0, 1, 0, 1, gtk.SHRINK | gtk.FILL, gtk.SHRINK | gtk.FILL)
+        table.attach(self.make_label('<Ctrl>+E:'), 0, 1, 1, 2, gtk.SHRINK | gtk.FILL, gtk.SHRINK | gtk.FILL)
+        table.attach(self.make_label('<Ctrl><Home>:'), 0, 1, 2, 3, gtk.SHRINK | gtk.FILL, gtk.SHRINK | gtk.FILL)
+        table.attach(self.make_label('<Ctrl><End>:'), 0, 1, 3, 4, gtk.SHRINK | gtk.FILL, gtk.SHRINK | gtk.FILL)
+        
+        table.attach(self.make_label(_('Enter column edit mode using selection')), 1, 2, 0, 1)
+        table.attach(self.make_label(_('Toggle edit point')), 1, 2, 1, 2)
+        table.attach(self.make_label(_('Add edit point at beginning of line/selection')), 1, 2, 2, 3)
+        table.attach(self.make_label(_('Add edit point at end of line/selection')), 1, 2, 3, 4)
+        
+        table.show_all()
+        tooltip.set_custom(table)
+        return True
+
+    def from_color(self, col):
+        return [col.red / float(0x10000), col.green / float(0x10000), col.blue / float(0x10000)]
+
+    def _background_color(self):
+        col = self.from_color(self._view.get_style().base[self._view.state])
+        if col[2] > 0.8:
+            col[2] -= 0.2
+        else:
+            col[2] += 0.2
+
+        return col
+
+    def on_view_expose_event(self, view, event):
+        if event.window == view.get_window(gtk.TEXT_WINDOW_TEXT):
+            return self._draw_column_mode(event)
+
+        if event.window != view.get_window(gtk.TEXT_WINDOW_TOP):
+            return False
+
+        if not self._in_mode:
+            return False
+
+        ctx = event.window.cairo_create()
+        col = self._background_color()
+
+        ctx.set_source_rgb(col[0], col[1], col[2])
+        ctx.rectangle(event.area)
+        ctx.fill_preserve()
+        ctx.clip()
+
+        layout = view.create_pango_layout(_('Multi Edit Mode'))
+
+        layout.set_font_description(pango.FontDescription('Sans 10'))
+        extents = layout.get_pixel_extents()
+
+        geom = event.window.get_geometry()
+
+        ctx.translate(0.5, 0.5)
+        ctx.set_line_width(1)
+
+        col = self.from_color(view.get_style().text[view.state])
+        
+        ctx.set_source_rgba(col[0], col[1], col[2], 0.6)
+        ctx.move_to(geom[0], geom[1] + geom[3] - 1)
+        ctx.rel_line_to(geom[2], 0)
+        ctx.stroke()
+
+        ctx.set_source_rgb(col[0], col[1], col[2])
+        ctx.move_to(geom[2] - extents[1][2] - 3, (geom[3] - extents[1][3]) / 2)
+        ctx.show_layout(layout)
+        
+        if not self._status:
+            status = ''
+        else:
+            status = str(self._status)
+
+        if status:
+            layout.set_markup(status)
+            
+            ctx.move_to(3, (geom[3] - extents[1][3]) / 2)
+            ctx.show_layout(layout)
+
+        return False
+
+# ex:ts=4:et:
diff --git a/plugins/multiedit/multiedit/signals.py b/plugins/multiedit/multiedit/signals.py
new file mode 100644
index 0000000..654f29b
--- /dev/null
+++ b/plugins/multiedit/multiedit/signals.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+#
+#  signals.py - Multi Edit
+#  
+#  Copyright (C) 2009 - Jesse van den Kieboom
+#  
+#  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., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+class Signals:
+    def __init__(self):
+        self._signals = {}
+
+    def _connect(self, obj, name, handler, connector):
+        ret = self._signals.setdefault(obj, {})
+        
+        hid = connector(name, handler)
+        ret.setdefault(name, []).append(hid)
+
+        return hid
+
+    def connect_signal(self, obj, name, handler):
+        return self._connect(obj, name, handler, obj.connect)
+    
+    def connect_signal_after(self, obj, name, handler):
+        return self._connect(obj, name, handler, obj.connect_after)
+    
+    def disconnect_signals(self, obj):
+        if not obj in self._signals:
+            return False
+
+        for name in self._signals[obj]:
+            for hid in self._signals[obj][name]:
+                obj.disconnect(hid)
+        
+        del self._signals[obj]
+        return True
+    
+    def block_signal(self, obj, name):
+        if not obj in self._signals:
+            return False
+        
+        if not name in self._signals[obj]:
+            return False
+        
+        for hid in self._signals[obj][name]:
+            obj.handler_block(hid)
+        
+        return True
+    
+    def unblock_signal(self, obj, name):
+        if not obj in self._signals:
+            return False
+        
+        if not name in self._signals[obj]:
+            return False
+        
+        for hid in self._signals[obj][name]:
+            obj.handler_unblock(hid)
+        
+        return True
+
+    def disconnect_signal(self, obj, name):
+        if not obj in self._signals:
+            return False
+        
+        if not name in self._signals[obj]:
+            return False
+        
+        for hid in self._signals[obj][name]:
+            obj.disconnect(hid)
+        
+        del self._signals[obj][name]
+        
+        if len(self._signals[obj]) == 0:
+            del self._signals[obj]
+        
+        return True
+
+# ex:ts=4:et:
diff --git a/plugins/multiedit/multiedit/windowhelper.py b/plugins/multiedit/multiedit/windowhelper.py
new file mode 100644
index 0000000..63ac0ef
--- /dev/null
+++ b/plugins/multiedit/multiedit/windowhelper.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+#
+#  windowhelper.py - Multi Edit
+#  
+#  Copyright (C) 2009 - Jesse van den Kieboom
+#  
+#  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., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+import gedit
+from documenthelper import DocumentHelper
+from signals import Signals
+import constants
+import gtk
+
+from gpdefs import *
+
+try:
+    gettext.bindtextdomain(GETTEXT_PACKAGE, GP_LOCALEDIR)
+    _ = lambda s: gettext.dgettext(GETTEXT_PACKAGE, s);
+except:
+    _ = lambda s: s
+
+ui_str = """
+<ui>
+  <menubar name="MenuBar">
+    <menu name="EditMenu" action="Edit">
+      <placeholder name="EditOps_5">
+        <menuitem name="MultiEditMode" action="MultiEditModeAction"/>
+      </placeholder>
+    </menu>
+  </menubar>
+</ui>
+"""
+
+class WindowHelper(Signals):
+    def __init__(self, plugin, window):
+        Signals.__init__(self)
+        
+        self._window = window
+        self._plugin = plugin
+        
+        # Insert document helpers
+        for view in window.get_views():
+            self.add_document_helper(view)
+        
+        self.connect_signal(window, 'tab-added', self.on_tab_added)
+        self.connect_signal(window, 'tab-removed', self.on_tab_removed)
+        
+        self.install_ui()
+
+    def install_ui(self):
+        manager = self._window.get_ui_manager()
+        
+        self._action_group = gtk.ActionGroup("GeditMultiEditPluginActions")
+        self._action_group.add_actions(
+            [('MultiEditModeAction', None, _('Multi Edit Mode'), '<Ctrl><Shift>C', _('Start multi edit mode'), self.on_multi_edit_mode)])
+        
+        manager.insert_action_group(self._action_group, -1)
+        self._merge_id = manager.add_ui_from_string(ui_str)
+
+    def uninstall_ui(self):
+        manager = self._window.get_ui_manager()
+        manager.remove_ui(self._merge_id)
+        manager.remove_action_group(self._action_group)
+        
+        manager.ensure_update()
+
+    def deactivate(self):
+        # Remove document helpers
+        for view in self._window.get_views():
+            self.remove_document_helper(view)
+
+        self.disconnect_signals(self._window)
+        self.uninstall_ui()
+
+        self._window = None
+        self._plugin = None        
+
+    def update_ui(self):
+        pass
+
+    def add_document_helper(self, view):
+        if view.get_data(constants.DOCUMENT_HELPER_KEY) != None:
+            return
+            
+        DocumentHelper(view)
+
+    def remove_document_helper(self, view):
+        helper = view.get_data(constants.DOCUMENT_HELPER_KEY)
+        
+        if helper != None:
+            helper.stop()
+    
+    def on_tab_added(self, window, tab):
+        self.add_document_helper(tab.get_view())
+    
+    def on_tab_removed(self, window, tab):
+        self.remove_document_helper(tab.get_view())
+    
+    def on_multi_edit_mode(self, action):
+        view = self._window.get_active_view()
+        helper = view.get_data(constants.DOCUMENT_HELPER_KEY)
+        
+        if helper != None:
+            helper.enable_multi_edit()
+
+# ex:ts=4:et:
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8dcd030..dd16d8a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,6 +12,9 @@ plugins/drawspaces/drawspaces.gedit-plugin.desktop.in.in
 plugins/drawspaces/gedit-drawspaces-plugin.c
 plugins/joinlines/joinlines.gedit-plugin.desktop.in.in
 plugins/joinlines/joinlines.py
+plugins/multiedit/multiedit.gedit-plugin.desktop.in.in
+plugins/multiedit/multiedit/documenthelper.py
+plugins/multiedit/multiedit/windowhelper.py
 plugins/showtabbar/gedit-show-tabbar-plugin.c
 plugins/showtabbar/gedit-show-tabbar-plugin.schemas.in
 plugins/showtabbar/showtabbar.gedit-plugin.desktop.in.in
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 18d9558..7441a21 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -5,6 +5,7 @@ plugins/codecomment/codecomment.gedit-plugin.desktop.in
 plugins/colorpicker/colorpicker.gedit-plugin.desktop.in
 plugins/drawspaces/drawspaces.gedit-plugin.desktop.in
 plugins/joinlines/joinlines.gedit-plugin.desktop.in
+plugins/multiedit/multiedit.gedit-plugin.desktop.in
 plugins/sessionsaver/sessionsaver.gedit-plugin.desktop.in
 plugins/showtabbar/showtabbar.gedit-plugin.desktop.in
 plugins/smartspaces/smartspaces.gedit-plugin.desktop.in



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]