Merge lp:~kartiksinghal/polly/patch-for-undo-redo-for-compose-box into lp:polly

Proposed by Kartik Singhal
Status: Superseded
Proposed branch: lp:~kartiksinghal/polly/patch-for-undo-redo-for-compose-box
Merge into: lp:polly
Diff against target: 286 lines (+251/-2)
2 files modified
src/polly/external/undobuffer.py (+234/-0)
src/polly/gui/header/entry.py (+17/-2)
To merge this branch: bzr merge lp:~kartiksinghal/polly/patch-for-undo-redo-for-compose-box
Reviewer Review Type Date Requested Status
Conscious User Needs Fixing
Review via email: mp+109091@code.launchpad.net

This proposal has been superseded by a proposal from 2012-06-10.

Description of the change

Undo and redo functionality for the Compose box using tiax's python port of gtksourceview's undo mechanism (http://bitbucket.org/tiax/gtk-textbuffer-with-undo/).

To post a comment you must log in.
Revision history for this message
Conscious User (conscioususer) wrote :

Hi Kartik, thanks for the patch.

Those are more "guideline fixes" than actual fixes, so it should be easy:

1) Please move the undobuffer module to the "external" folder and import it with "from polly.external import undobuffer". I do that for all modules copied from other sources.

2) It seems that you are associating undo with z and redo with y. Shouldn't it be ctrl+z and ctrl+y.

3) The GNOME HIG guidelines, which I tend to follow, actually associate redo with shift+ctrl+z.

It is also needed to update the copyright file to reflect the new module, but you can leave that to me.

review: Needs Fixing
Revision history for this message
Kartik Singhal (kartiksinghal) wrote :

On Sat, Jun 9, 2012 at 9:59 PM, Conscious User <email address hidden>wrote:

> Review: Needs Fixing
>
> Hi Kartik, thanks for the patch.
>

Hi, thanks for the review. :)

> Those are more "guideline fixes" than actual fixes, so it should be easy:
>
> 1) Please move the undobuffer module to the "external" folder and import
> it with "from polly.external import undobuffer". I do that for all modules
> copied from other sources.
>

Okay. Updating this.

> 2) It seems that you are associating undo with z and redo with y.
> Shouldn't it be ctrl+z and ctrl+y.
>

No, they are correctly associated to Ctrl+z and y respectively.
Gtk.keysyms.z maps to Ctrl+z itself.

> 3) The GNOME HIG guidelines, which I tend to follow, actually associate
> redo with shift+ctrl+z.
>

I had this doubt - what shortcut to use for redo while associating Ctrl+y.
I wasn't aware about the GNOME HIG, fixing.

> It is also needed to update the copyright file to reflect the new module,
> but you can leave that to me.
>

Sure.

--
Kartik
http://k4rtik.wordpress.com/

402. By Kartik Singhal

added undo/redo feature in compose box

Unmerged revisions

402. By Kartik Singhal

added undo/redo feature in compose box

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/polly/external/undobuffer.py'
--- src/polly/external/undobuffer.py 1970-01-01 00:00:00 +0000
+++ src/polly/external/undobuffer.py 2012-06-09 17:25:24 +0000
@@ -0,0 +1,234 @@
1#!/usr/bin/env python
2# -*- coding:utf-8 -*-
3
4""" gtk textbuffer with undo functionality """
5
6#Copyright (C) 2009 Florian Heinle
7#
8# This library is free software; you can redistribute it and/or
9# modify it under the terms of the GNU Lesser General Public
10# License as published by the Free Software Foundation; either
11# version 2.1 of the License, or (at your option) any later version.
12#
13# This library is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# Lesser General Public License for more details.
17#
18# You should have received a copy of the GNU Lesser General Public
19# License along with this library; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22import gtk
23
24class UndoableInsert(object):
25 """something that has been inserted into our textbuffer"""
26 def __init__(self, text_iter, text, length):
27 self.offset = text_iter.get_offset()
28 self.text = text
29 self.length = length
30 if self.length > 1 or self.text in ("\r", "\n", " "):
31 self.mergeable = False
32 else:
33 self.mergeable = True
34
35class UndoableDelete(object):
36 """something that has ben deleted from our textbuffer"""
37 def __init__(self, text_buffer, start_iter, end_iter):
38 self.text = text_buffer.get_text(start_iter, end_iter)
39 self.start = start_iter.get_offset()
40 self.end = end_iter.get_offset()
41 # need to find out if backspace or delete key has been used
42 # so we don't mess up during redo
43 insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
44 if insert_iter.get_offset() <= self.start:
45 self.delete_key_used = True
46 else:
47 self.delete_key_used = False
48 if self.end - self.start > 1 or self.text in ("\r", "\n", " "):
49 self.mergeable = False
50 else:
51 self.mergeable = True
52
53class UndoableBuffer(gtk.TextBuffer):
54 """text buffer with added undo capabilities
55
56 designed as a drop-in replacement for gtksourceview,
57 at least as far as undo is concerned"""
58
59 def __init__(self):
60 """
61 we'll need empty stacks for undo/redo and some state keeping
62 """
63 gtk.TextBuffer.__init__(self)
64 self.undo_stack = []
65 self.redo_stack = []
66 self.not_undoable_action = False
67 self.undo_in_progress = False
68 self.connect('insert-text', self.on_insert_text)
69 self.connect('delete-range', self.on_delete_range)
70
71 @property
72 def can_undo(self):
73 return bool(self.undo_stack)
74
75 @property
76 def can_redo(self):
77 return bool(self.redo_stack)
78
79 def on_insert_text(self, textbuffer, text_iter, text, length):
80 def can_be_merged(prev, cur):
81 """see if we can merge multiple inserts here
82
83 will try to merge words or whitespace
84 can't merge if prev and cur are not mergeable in the first place
85 can't merge when user set the input bar somewhere else
86 can't merge across word boundaries"""
87 WHITESPACE = (' ', '\t')
88 if not cur.mergeable or not prev.mergeable:
89 return False
90 elif cur.offset != (prev.offset + prev.length):
91 return False
92 elif cur.text in WHITESPACE and not prev.text in WHITESPACE:
93 return False
94 elif prev.text in WHITESPACE and not cur.text in WHITESPACE:
95 return False
96 return True
97
98 if not self.undo_in_progress:
99 self.redo_stack = []
100 if self.not_undoable_action:
101 return
102 undo_action = UndoableInsert(text_iter, text, length)
103 try:
104 prev_insert = self.undo_stack.pop()
105 except IndexError:
106 self.undo_stack.append(undo_action)
107 return
108 if not isinstance(prev_insert, UndoableInsert):
109 self.undo_stack.append(prev_insert)
110 self.undo_stack.append(undo_action)
111 return
112 if can_be_merged(prev_insert, undo_action):
113 prev_insert.length += undo_action.length
114 prev_insert.text += undo_action.text
115 self.undo_stack.append(prev_insert)
116 else:
117 self.undo_stack.append(prev_insert)
118 self.undo_stack.append(undo_action)
119
120 def on_delete_range(self, text_buffer, start_iter, end_iter):
121 def can_be_merged(prev, cur):
122 """see if we can merge multiple deletions here
123
124 will try to merge words or whitespace
125 can't merge if prev and cur are not mergeable in the first place
126 can't merge if delete and backspace key were both used
127 can't merge across word boundaries"""
128
129 WHITESPACE = (' ', '\t')
130 if not cur.mergeable or not prev.mergeable:
131 return False
132 elif prev.delete_key_used != cur.delete_key_used:
133 return False
134 elif prev.start != cur.start and prev.start != cur.end:
135 return False
136 elif cur.text not in WHITESPACE and \
137 prev.text in WHITESPACE:
138 return False
139 elif cur.text in WHITESPACE and \
140 prev.text not in WHITESPACE:
141 return False
142 return True
143
144 if not self.undo_in_progress:
145 self.redo_stack = []
146 if self.not_undoable_action:
147 return
148 undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
149 try:
150 prev_delete = self.undo_stack.pop()
151 except IndexError:
152 self.undo_stack.append(undo_action)
153 return
154 if not isinstance(prev_delete, UndoableDelete):
155 self.undo_stack.append(prev_delete)
156 self.undo_stack.append(undo_action)
157 return
158 if can_be_merged(prev_delete, undo_action):
159 if prev_delete.start == undo_action.start: # delete key used
160 prev_delete.text += undo_action.text
161 prev_delete.end += (undo_action.end - undo_action.start)
162 else: # Backspace used
163 prev_delete.text = "%s%s" % (undo_action.text,
164 prev_delete.text)
165 prev_delete.start = undo_action.start
166 self.undo_stack.append(prev_delete)
167 else:
168 self.undo_stack.append(prev_delete)
169 self.undo_stack.append(undo_action)
170
171 def begin_not_undoable_action(self):
172 """don't record the next actions
173
174 toggles self.not_undoable_action"""
175 self.not_undoable_action = True
176
177 def end_not_undoable_action(self):
178 """record next actions
179
180 toggles self.not_undoable_action"""
181 self.not_undoable_action = False
182
183 def undo(self):
184 """undo inserts or deletions
185
186 undone actions are being moved to redo stack"""
187 if not self.undo_stack:
188 return
189 self.begin_not_undoable_action()
190 self.undo_in_progress = True
191 undo_action = self.undo_stack.pop()
192 self.redo_stack.append(undo_action)
193 if isinstance(undo_action, UndoableInsert):
194 start = self.get_iter_at_offset(undo_action.offset)
195 stop = self.get_iter_at_offset(
196 undo_action.offset + undo_action.length
197 )
198 self.delete(start, stop)
199 self.place_cursor(start)
200 else:
201 start = self.get_iter_at_offset(undo_action.start)
202 self.insert(start, undo_action.text)
203 stop = self.get_iter_at_offset(undo_action.end)
204 if undo_action.delete_key_used:
205 self.place_cursor(start)
206 else:
207 self.place_cursor(stop)
208 self.end_not_undoable_action()
209 self.undo_in_progress = False
210
211 def redo(self):
212 """redo inserts or deletions
213
214 redone actions are moved to undo stack"""
215 if not self.redo_stack:
216 return
217 self.begin_not_undoable_action()
218 self.undo_in_progress = True
219 redo_action = self.redo_stack.pop()
220 self.undo_stack.append(redo_action)
221 if isinstance(redo_action, UndoableInsert):
222 start = self.get_iter_at_offset(redo_action.offset)
223 self.insert(start, redo_action.text)
224 new_cursor_pos = self.get_iter_at_offset(
225 redo_action.offset + redo_action.length
226 )
227 self.place_cursor(new_cursor_pos)
228 else:
229 start = self.get_iter_at_offset(redo_action.start)
230 stop = self.get_iter_at_offset(redo_action.end)
231 self.delete(start, stop)
232 self.place_cursor(start)
233 self.end_not_undoable_action()
234 self.undo_in_progress = False
0235
=== modified file 'src/polly/gui/header/entry.py'
--- src/polly/gui/header/entry.py 2011-10-30 01:15:58 +0000
+++ src/polly/gui/header/entry.py 2012-06-09 17:25:24 +0000
@@ -43,6 +43,7 @@
4343
44from polly.gui import NICK_DIMENSION, run_generic_error_dialog44from polly.gui import NICK_DIMENSION, run_generic_error_dialog
4545
46from polly.external import undobuffer
4647
4748
48PAGE_SIZE = 549PAGE_SIZE = 5
@@ -460,7 +461,11 @@
460 if self.nicks:461 if self.nicks:
461 self.completer.end()462 self.completer.end()
462463
463 return True464 return True
465 elif event.keyval == Gtk.keysyms.z: # Ctrl+z
466 self.undo()
467 elif event.keyval == Gtk.keysyms.Z: # Ctrl+Shift+z
468 self.redo()
464 elif (event.keyval == Gtk.keysyms.KP_Left or event.keyval == Gtk.keysyms.Left or469 elif (event.keyval == Gtk.keysyms.KP_Left or event.keyval == Gtk.keysyms.Left or
465 event.keyval == Gtk.keysyms.KP_Right or event.keyval == Gtk.keysyms.Right or470 event.keyval == Gtk.keysyms.KP_Right or event.keyval == Gtk.keysyms.Right or
466 event.keyval == Gtk.keysyms.Escape):471 event.keyval == Gtk.keysyms.Escape):
@@ -728,7 +733,7 @@
728 self.texttag = Gtk.TextTag(None)733 self.texttag = Gtk.TextTag(None)
729 self.texttag.set_property(u'underline', Pango.UNDERLINE_SINGLE)734 self.texttag.set_property(u'underline', Pango.UNDERLINE_SINGLE)
730735
731 self.textview = Gtk.TextView(None)736 self.textview = Gtk.TextView(undobuffer.UndoableBuffer()) # now we use Undo-able buffer
732 self.textview.set_wrap_mode(Gtk.WRAP_WORD_CHAR)737 self.textview.set_wrap_mode(Gtk.WRAP_WORD_CHAR)
733 self.textview.connect(u'button-press-event', self._on_textview_button_press_event)738 self.textview.connect(u'button-press-event', self._on_textview_button_press_event)
734 self.textview.connect(u'focus-in-event', self._on_textview_focus_in_event)739 self.textview.connect(u'focus-in-event', self._on_textview_focus_in_event)
@@ -919,3 +924,13 @@
919 def move_completer(self):924 def move_completer(self):
920 if self.nicks:925 if self.nicks:
921 self._move_completer()926 self._move_completer()
927
928
929 def undo(self):
930 if self.textbuffer.can_undo:
931 self.textbuffer.undo()
932
933
934 def redo(self):
935 if self.textbuffer.can_redo:
936 self.textbuffer.redo()

Subscribers

People subscribed via source and target branches