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