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

Proposed by Kartik Singhal
Status: Needs review
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
Reviewer Review Type Date Requested Status
Conscious User Needs Fixing
Review via email: mp+118043@code.launchpad.net

This proposal supersedes 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 : Posted in a previous version of this proposal

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 : 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://k4rtik.wordpress.com/

Revision history for this message
Conscious User (conscioususer) wrote : Posted in a previous version of this proposal

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.

review: Needs Fixing
Revision history for this message
Conscious User (conscioususer) wrote :

Sorry it took this long. Only very small things now:

1) I don't think considering both z and Z is necessary.

2) Please use multiple "=="s instead of using "in". Yes, I'm aware this is less pythonic, but it's still better than inconsistency in a single point.

3) There is an extra linebreak at the end, after the redo method.

I promise the next version will not take more than one day!

review: Needs Fixing
Revision history for this message
Conscious User (conscioususer) wrote :

By the way, branch the latest version, as there was a small change since your last proposal.

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
=== modified file 'src/polly/external/__init__.py'
--- src/polly/external/__init__.py 2011-08-24 12:29:48 +0000
+++ src/polly/external/__init__.py 2012-08-03 05:52:20 +0000
@@ -19,4 +19,4 @@
1919
2020
2121
22import oauth2, wraplabel22import oauth2, wraplabel, undobuffer
2323
=== 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-08-03 05:52:20 +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-08-03 05:52:20 +0000
@@ -39,6 +39,8 @@
3939
40from polly import ENCODING, dispatch40from polly import ENCODING, dispatch
4141
42from polly.external import undobuffer
43
42from polly.shortener import Exit, Error44from polly.shortener import Exit, Error
4345
44from polly.gui import NICK_DIMENSION, run_generic_error_dialog46from polly.gui import NICK_DIMENSION, run_generic_error_dialog
@@ -461,6 +463,14 @@
461 self.completer.end()463 self.completer.end()
462464
463 return True465 return True
466 elif event.keyval in (Gtk.keysyms.z, Gtk.keysyms.Z) and \
467 (event.state & Gtk.gdk.CONTROL_MASK) != 0 and \
468 (event.state & Gtk.gdk.SHIFT_MASK) == 0:
469 self.undo()
470 elif event.keyval in (Gtk.keysyms.z, Gtk.keysyms.Z) and \
471 (event.state & Gtk.gdk.CONTROL_MASK) != 0 and \
472 (event.state & Gtk.gdk.SHIFT_MASK) != 0:
473 self.redo()
464 elif (event.keyval == Gtk.keysyms.KP_Left or event.keyval == Gtk.keysyms.Left or474 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 or475 event.keyval == Gtk.keysyms.KP_Right or event.keyval == Gtk.keysyms.Right or
466 event.keyval == Gtk.keysyms.Escape):476 event.keyval == Gtk.keysyms.Escape):
@@ -728,7 +738,7 @@
728 self.texttag = Gtk.TextTag(None)738 self.texttag = Gtk.TextTag(None)
729 self.texttag.set_property(u'underline', Pango.UNDERLINE_SINGLE)739 self.texttag.set_property(u'underline', Pango.UNDERLINE_SINGLE)
730740
731 self.textview = Gtk.TextView(None)741 self.textview = Gtk.TextView(undobuffer.UndoableBuffer()) # now we use Undo-able buffer
732 self.textview.set_wrap_mode(Gtk.WRAP_WORD_CHAR)742 self.textview.set_wrap_mode(Gtk.WRAP_WORD_CHAR)
733 self.textview.connect(u'button-press-event', self._on_textview_button_press_event)743 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)744 self.textview.connect(u'focus-in-event', self._on_textview_focus_in_event)
@@ -919,3 +929,14 @@
919 def move_completer(self):929 def move_completer(self):
920 if self.nicks:930 if self.nicks:
921 self._move_completer()931 self._move_completer()
932
933
934 def undo(self):
935 if self.textbuffer.can_undo:
936 self.textbuffer.undo()
937
938
939 def redo(self):
940 if self.textbuffer.can_redo:
941 self.textbuffer.redo()
942

Subscribers

People subscribed via source and target branches