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

Subscribers

People subscribed via source and target branches