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
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:52:20 +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:52:20 +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:52:20 +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+

Subscribers

People subscribed via source and target branches