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: |
300 lines (+257/-2) 3 files modified
src/polly/external/__init__.py (+1/-1) src/polly/external/undobuffer.py (+234/-0) src/polly/gui/header/entry.py (+22/-1) |
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+109503@code.launchpad.net |
This proposal supersedes a proposal from 2012-06-07.
This proposal has been superseded by a proposal from 2012-08-03.
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://
Conscious User (conscioususer) wrote : Posted in a previous version of this proposal | # |
Kartik Singhal (kartiksinghal) wrote : Posted in a previous version of this proposal | # |
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://
Conscious User (conscioususer) wrote : | # |
Thanks again. :) Hopefully we are almost there, here's what needs fixing this time:
1) In the external __init__ file, replace "import oauth2, wraplabel" with "import oauth2, wraplabel, undobuffer"
2) For consistency, please add the "from polly.external" line as the second polly import, right above "from polly.shortener"
3) As I warned, the undo is working when you just type "z", without the Ctrl. Use Gdk.CONTROL_MASK to ensure Ctrl is pressed.
4) Also use Gdk.SHIFT_MASK to ensure shift is pressed. It's more robust than using capital "Z".
Oh, and do not make new commits just for fixing. Revert the previous commits and push the branch with --overwrite.
- 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 | === modified file 'src/polly/external/__init__.py' |
2 | --- src/polly/external/__init__.py 2011-08-24 12:29:48 +0000 |
3 | +++ src/polly/external/__init__.py 2012-08-03 05:51:19 +0000 |
4 | @@ -19,4 +19,4 @@ |
5 | |
6 | |
7 | |
8 | -import oauth2, wraplabel |
9 | +import oauth2, wraplabel, undobuffer |
10 | |
11 | === added file 'src/polly/external/undobuffer.py' |
12 | --- src/polly/external/undobuffer.py 1970-01-01 00:00:00 +0000 |
13 | +++ src/polly/external/undobuffer.py 2012-08-03 05:51:19 +0000 |
14 | @@ -0,0 +1,234 @@ |
15 | +#!/usr/bin/env python |
16 | +# -*- coding:utf-8 -*- |
17 | + |
18 | +""" gtk textbuffer with undo functionality """ |
19 | + |
20 | +#Copyright (C) 2009 Florian Heinle |
21 | +# |
22 | +# This library is free software; you can redistribute it and/or |
23 | +# modify it under the terms of the GNU Lesser General Public |
24 | +# License as published by the Free Software Foundation; either |
25 | +# version 2.1 of the License, or (at your option) any later version. |
26 | +# |
27 | +# This library is distributed in the hope that it will be useful, |
28 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
29 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
30 | +# Lesser General Public License for more details. |
31 | +# |
32 | +# You should have received a copy of the GNU Lesser General Public |
33 | +# License along with this library; if not, write to the Free Software |
34 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
35 | + |
36 | +import gtk |
37 | + |
38 | +class UndoableInsert(object): |
39 | + """something that has been inserted into our textbuffer""" |
40 | + def __init__(self, text_iter, text, length): |
41 | + self.offset = text_iter.get_offset() |
42 | + self.text = text |
43 | + self.length = length |
44 | + if self.length > 1 or self.text in ("\r", "\n", " "): |
45 | + self.mergeable = False |
46 | + else: |
47 | + self.mergeable = True |
48 | + |
49 | +class UndoableDelete(object): |
50 | + """something that has ben deleted from our textbuffer""" |
51 | + def __init__(self, text_buffer, start_iter, end_iter): |
52 | + self.text = text_buffer.get_text(start_iter, end_iter) |
53 | + self.start = start_iter.get_offset() |
54 | + self.end = end_iter.get_offset() |
55 | + # need to find out if backspace or delete key has been used |
56 | + # so we don't mess up during redo |
57 | + insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert()) |
58 | + if insert_iter.get_offset() <= self.start: |
59 | + self.delete_key_used = True |
60 | + else: |
61 | + self.delete_key_used = False |
62 | + if self.end - self.start > 1 or self.text in ("\r", "\n", " "): |
63 | + self.mergeable = False |
64 | + else: |
65 | + self.mergeable = True |
66 | + |
67 | +class UndoableBuffer(gtk.TextBuffer): |
68 | + """text buffer with added undo capabilities |
69 | + |
70 | + designed as a drop-in replacement for gtksourceview, |
71 | + at least as far as undo is concerned""" |
72 | + |
73 | + def __init__(self): |
74 | + """ |
75 | + we'll need empty stacks for undo/redo and some state keeping |
76 | + """ |
77 | + gtk.TextBuffer.__init__(self) |
78 | + self.undo_stack = [] |
79 | + self.redo_stack = [] |
80 | + self.not_undoable_action = False |
81 | + self.undo_in_progress = False |
82 | + self.connect('insert-text', self.on_insert_text) |
83 | + self.connect('delete-range', self.on_delete_range) |
84 | + |
85 | + @property |
86 | + def can_undo(self): |
87 | + return bool(self.undo_stack) |
88 | + |
89 | + @property |
90 | + def can_redo(self): |
91 | + return bool(self.redo_stack) |
92 | + |
93 | + def on_insert_text(self, textbuffer, text_iter, text, length): |
94 | + def can_be_merged(prev, cur): |
95 | + """see if we can merge multiple inserts here |
96 | + |
97 | + will try to merge words or whitespace |
98 | + can't merge if prev and cur are not mergeable in the first place |
99 | + can't merge when user set the input bar somewhere else |
100 | + can't merge across word boundaries""" |
101 | + WHITESPACE = (' ', '\t') |
102 | + if not cur.mergeable or not prev.mergeable: |
103 | + return False |
104 | + elif cur.offset != (prev.offset + prev.length): |
105 | + return False |
106 | + elif cur.text in WHITESPACE and not prev.text in WHITESPACE: |
107 | + return False |
108 | + elif prev.text in WHITESPACE and not cur.text in WHITESPACE: |
109 | + return False |
110 | + return True |
111 | + |
112 | + if not self.undo_in_progress: |
113 | + self.redo_stack = [] |
114 | + if self.not_undoable_action: |
115 | + return |
116 | + undo_action = UndoableInsert(text_iter, text, length) |
117 | + try: |
118 | + prev_insert = self.undo_stack.pop() |
119 | + except IndexError: |
120 | + self.undo_stack.append(undo_action) |
121 | + return |
122 | + if not isinstance(prev_insert, UndoableInsert): |
123 | + self.undo_stack.append(prev_insert) |
124 | + self.undo_stack.append(undo_action) |
125 | + return |
126 | + if can_be_merged(prev_insert, undo_action): |
127 | + prev_insert.length += undo_action.length |
128 | + prev_insert.text += undo_action.text |
129 | + self.undo_stack.append(prev_insert) |
130 | + else: |
131 | + self.undo_stack.append(prev_insert) |
132 | + self.undo_stack.append(undo_action) |
133 | + |
134 | + def on_delete_range(self, text_buffer, start_iter, end_iter): |
135 | + def can_be_merged(prev, cur): |
136 | + """see if we can merge multiple deletions here |
137 | + |
138 | + will try to merge words or whitespace |
139 | + can't merge if prev and cur are not mergeable in the first place |
140 | + can't merge if delete and backspace key were both used |
141 | + can't merge across word boundaries""" |
142 | + |
143 | + WHITESPACE = (' ', '\t') |
144 | + if not cur.mergeable or not prev.mergeable: |
145 | + return False |
146 | + elif prev.delete_key_used != cur.delete_key_used: |
147 | + return False |
148 | + elif prev.start != cur.start and prev.start != cur.end: |
149 | + return False |
150 | + elif cur.text not in WHITESPACE and \ |
151 | + prev.text in WHITESPACE: |
152 | + return False |
153 | + elif cur.text in WHITESPACE and \ |
154 | + prev.text not in WHITESPACE: |
155 | + return False |
156 | + return True |
157 | + |
158 | + if not self.undo_in_progress: |
159 | + self.redo_stack = [] |
160 | + if self.not_undoable_action: |
161 | + return |
162 | + undo_action = UndoableDelete(text_buffer, start_iter, end_iter) |
163 | + try: |
164 | + prev_delete = self.undo_stack.pop() |
165 | + except IndexError: |
166 | + self.undo_stack.append(undo_action) |
167 | + return |
168 | + if not isinstance(prev_delete, UndoableDelete): |
169 | + self.undo_stack.append(prev_delete) |
170 | + self.undo_stack.append(undo_action) |
171 | + return |
172 | + if can_be_merged(prev_delete, undo_action): |
173 | + if prev_delete.start == undo_action.start: # delete key used |
174 | + prev_delete.text += undo_action.text |
175 | + prev_delete.end += (undo_action.end - undo_action.start) |
176 | + else: # Backspace used |
177 | + prev_delete.text = "%s%s" % (undo_action.text, |
178 | + prev_delete.text) |
179 | + prev_delete.start = undo_action.start |
180 | + self.undo_stack.append(prev_delete) |
181 | + else: |
182 | + self.undo_stack.append(prev_delete) |
183 | + self.undo_stack.append(undo_action) |
184 | + |
185 | + def begin_not_undoable_action(self): |
186 | + """don't record the next actions |
187 | + |
188 | + toggles self.not_undoable_action""" |
189 | + self.not_undoable_action = True |
190 | + |
191 | + def end_not_undoable_action(self): |
192 | + """record next actions |
193 | + |
194 | + toggles self.not_undoable_action""" |
195 | + self.not_undoable_action = False |
196 | + |
197 | + def undo(self): |
198 | + """undo inserts or deletions |
199 | + |
200 | + undone actions are being moved to redo stack""" |
201 | + if not self.undo_stack: |
202 | + return |
203 | + self.begin_not_undoable_action() |
204 | + self.undo_in_progress = True |
205 | + undo_action = self.undo_stack.pop() |
206 | + self.redo_stack.append(undo_action) |
207 | + if isinstance(undo_action, UndoableInsert): |
208 | + start = self.get_iter_at_offset(undo_action.offset) |
209 | + stop = self.get_iter_at_offset( |
210 | + undo_action.offset + undo_action.length |
211 | + ) |
212 | + self.delete(start, stop) |
213 | + self.place_cursor(start) |
214 | + else: |
215 | + start = self.get_iter_at_offset(undo_action.start) |
216 | + self.insert(start, undo_action.text) |
217 | + stop = self.get_iter_at_offset(undo_action.end) |
218 | + if undo_action.delete_key_used: |
219 | + self.place_cursor(start) |
220 | + else: |
221 | + self.place_cursor(stop) |
222 | + self.end_not_undoable_action() |
223 | + self.undo_in_progress = False |
224 | + |
225 | + def redo(self): |
226 | + """redo inserts or deletions |
227 | + |
228 | + redone actions are moved to undo stack""" |
229 | + if not self.redo_stack: |
230 | + return |
231 | + self.begin_not_undoable_action() |
232 | + self.undo_in_progress = True |
233 | + redo_action = self.redo_stack.pop() |
234 | + self.undo_stack.append(redo_action) |
235 | + if isinstance(redo_action, UndoableInsert): |
236 | + start = self.get_iter_at_offset(redo_action.offset) |
237 | + self.insert(start, redo_action.text) |
238 | + new_cursor_pos = self.get_iter_at_offset( |
239 | + redo_action.offset + redo_action.length |
240 | + ) |
241 | + self.place_cursor(new_cursor_pos) |
242 | + else: |
243 | + start = self.get_iter_at_offset(redo_action.start) |
244 | + stop = self.get_iter_at_offset(redo_action.end) |
245 | + self.delete(start, stop) |
246 | + self.place_cursor(start) |
247 | + self.end_not_undoable_action() |
248 | + self.undo_in_progress = False |
249 | |
250 | === modified file 'src/polly/gui/header/entry.py' |
251 | --- src/polly/gui/header/entry.py 2011-10-30 01:15:58 +0000 |
252 | +++ src/polly/gui/header/entry.py 2012-08-03 05:51:19 +0000 |
253 | @@ -39,6 +39,8 @@ |
254 | |
255 | from polly import ENCODING, dispatch |
256 | |
257 | +from polly.external import undobuffer |
258 | + |
259 | from polly.shortener import Exit, Error |
260 | |
261 | from polly.gui import NICK_DIMENSION, run_generic_error_dialog |
262 | @@ -461,6 +463,14 @@ |
263 | self.completer.end() |
264 | |
265 | return True |
266 | + elif event.keyval in (Gtk.keysyms.z, Gtk.keysyms.Z) and \ |
267 | + (event.state & Gtk.gdk.CONTROL_MASK) != 0 and \ |
268 | + (event.state & Gtk.gdk.SHIFT_MASK) == 0: |
269 | + self.undo() |
270 | + elif event.keyval in (Gtk.keysyms.z, Gtk.keysyms.Z) and \ |
271 | + (event.state & Gtk.gdk.CONTROL_MASK) != 0 and \ |
272 | + (event.state & Gtk.gdk.SHIFT_MASK) != 0: |
273 | + self.redo() |
274 | elif (event.keyval == Gtk.keysyms.KP_Left or event.keyval == Gtk.keysyms.Left or |
275 | event.keyval == Gtk.keysyms.KP_Right or event.keyval == Gtk.keysyms.Right or |
276 | event.keyval == Gtk.keysyms.Escape): |
277 | @@ -728,7 +738,7 @@ |
278 | self.texttag = Gtk.TextTag(None) |
279 | self.texttag.set_property(u'underline', Pango.UNDERLINE_SINGLE) |
280 | |
281 | - self.textview = Gtk.TextView(None) |
282 | + self.textview = Gtk.TextView(undobuffer.UndoableBuffer()) # now we use Undo-able buffer |
283 | self.textview.set_wrap_mode(Gtk.WRAP_WORD_CHAR) |
284 | self.textview.connect(u'button-press-event', self._on_textview_button_press_event) |
285 | self.textview.connect(u'focus-in-event', self._on_textview_focus_in_event) |
286 | @@ -919,3 +929,14 @@ |
287 | def move_completer(self): |
288 | if self.nicks: |
289 | self._move_completer() |
290 | + |
291 | + |
292 | + def undo(self): |
293 | + if self.textbuffer.can_undo: |
294 | + self.textbuffer.undo() |
295 | + |
296 | + |
297 | + def redo(self): |
298 | + if self.textbuffer.can_redo: |
299 | + self.textbuffer.redo() |
300 | + |
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.