Merge lp:~hid-iwata/qbzr/textedit-guidebar into lp:qbzr

Proposed by IWATA Hidetaka
Status: Merged
Merged at revision: 1454
Proposed branch: lp:~hid-iwata/qbzr/textedit-guidebar
Merge into: lp:qbzr
Diff against target: 846 lines (+455/-22)
8 files modified
NEWS.txt (+4/-0)
lib/annotate.py (+46/-5)
lib/diffview.py (+79/-8)
lib/diffwindow.py (+5/-1)
lib/widgets/shelve.py (+15/-2)
lib/widgets/shelvelist.py (+1/-1)
lib/widgets/texteditaccessory.py (+294/-0)
lib/widgets/toolbars.py (+11/-5)
To merge this branch: bzr merge lp:~hid-iwata/qbzr/textedit-guidebar
Reviewer Review Type Date Requested Status
QBzr Developers Pending
Review via email: mp+91126@code.launchpad.net

Description of the change

Add change-marker bar to qdiff, qannotate, qshelve and qunshelve.

[qdiff] Show positions of added, removed or modified lines.
http://dl.dropbox.com/u/16802579/guidebar-qdiff.png

[qannotate] Show positions of lines changed by selected revisions.
http://dl.dropbox.com/u/16802579/guidebar-qannotate.png

[ALL] Additionally, when searching words, show positions of matched words.
http://dl.dropbox.com/u/16802579/guidebar-qdiff-search.png

To post a comment you must log in.
Revision history for this message
Alexander Belchenko (bialix) wrote :

IWATA Hidetaka пишет:
> IWATA Hidetaka has proposed merging lp:~hid-iwata/qbzr/textedit-guidebar into lp:qbzr.
>
> Requested reviews:
> QBzr Developers (qbzr-dev)
> Related bugs:
> Bug #827251 in QBzr: ""Complete" diff display could show change markers in the scroll bar"
> https://bugs.launchpad.net/qbzr/+bug/827251
>
> For more details, see:
> https://code.launchpad.net/~hid-iwata/qbzr/textedit-guidebar/+merge/91126
>
> Add change-marker bar to qdiff, qannotate, qshelve and qunshelve.
>
> [qdiff] Show positions of added, removed or modified lines.
> http://dl.dropbox.com/u/16802579/guidebar-qdiff.png
>
> [qannotate] Show positions of lines changed by selected revisions.
> http://dl.dropbox.com/u/16802579/guidebar-qannotate.png
>
> [ALL] Additionally, when searching words, show positions of matched words.
> http://dl.dropbox.com/u/16802579/guidebar-qdiff-search.png

\o/

that's so cool. thank you!

--
All the dude wanted was his rug back

Revision history for this message
Martin Packman (gz) wrote :

This looks really neat. I wonder if you could find a way to add some test coverage here? I've found it's not actually as hard to do in pyqt as it sometimes seems.

Revision history for this message
Alexander Belchenko (bialix) wrote :

Martin Packman пишет:
> This looks really neat. I wonder if you could find a way to add some test coverage here? I've found it's not actually as hard to do in pyqt as it sometimes seems.

I agree with Martin. Hidetaka, if you can, please add some tests for new
features. In the mean time I'll merge it in the current state to start
testing it now.

Thanks again!

--
All the dude wanted was his rug back

lp:~hid-iwata/qbzr/textedit-guidebar updated
1458. By IWATA Hidetaka

Render small marks more exactly.

1459. By IWATA Hidetaka

Fix : Guide bar of qunshelve does not show matched word positions

Revision history for this message
IWATA Hidetaka (hid-iwata) wrote :

OK, I'll write some test.

Now, I've pushed some revisions, please remerge them.
 * Fix: guide bar of qunshelve does not show search highlight positions.
 * Improve accuracy of 1-dot-height mark positions.

Revision history for this message
Alexander Belchenko (bialix) wrote :

IWATA Hidetaka пишет:
> OK, I'll write some test.
>
> Now, I've pushed some revisions, please remerge them.
> * Fix: guide bar of qunshelve does not show search highlight positions.
> * Improve accuracy of 1-dot-height mark positions.

Hidetaka, I'm sure you have write permissions not only because you're in
~qbzr-dev group, but also because you're so good contributor. Please,
land it if you think it has important improvements.
Thanks!

--
All the dude wanted was his rug back

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS.txt'
2--- NEWS.txt 2012-01-30 15:46:28 +0000
3+++ NEWS.txt 2012-02-01 16:39:02 +0000
4@@ -8,6 +8,9 @@
5 * qannotate:
6 * Vertically center the target line when using "Goto Line" in qannotate.
7 (Benoît Pierre)
8+ * Show change markers side of annotate view.
9+ It represents where lines changed by selected revisions are.
10+ (IWATA Hidetaka)
11 * qbrowse:
12 * Does not crash anymore when called for shared repository.
13 (André Bachmann, Alexander Belchenko, Bug #578935)
14@@ -28,6 +31,7 @@
15 (Alexander Belchenko, Bug #814117)
16 * Implement search highlight. This change affects to qdiff, qshelve,
17 qunshelve, qannotate. (IWATA Hidetaka, Bug #785565)
18+ * Show change markers side of diff views. (IWATA Hidetaka Bug #827251)
19 * qgetnew
20 * Base directories for the source branch and the destination checkout folder
21 can now be configured in qconfig, tab 'User Interface'. (André Bachmann)
22
23=== modified file 'lib/annotate.py'
24--- lib/annotate.py 2012-01-24 20:34:19 +0000
25+++ lib/annotate.py 2012-02-01 16:39:02 +0000
26@@ -23,6 +23,7 @@
27 # - better annotate algorithm on packs
28
29 import sys, time
30+from itertools import groupby
31 from PyQt4 import QtCore, QtGui
32
33 from bzrlib.revision import CURRENT_REVISION
34@@ -41,6 +42,9 @@
35 runs_in_loading_queue,
36 )
37 from bzrlib.plugins.qbzr.lib.widgets.toolbars import FindToolbar
38+from bzrlib.plugins.qbzr.lib.widgets.texteditaccessory import (
39+ GuideBarPanel, setup_guidebar_for_find
40+)
41 from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
42 from bzrlib.plugins.qbzr.lib.trace import reports_exception
43 from bzrlib.plugins.qbzr.lib.logwidget import LogList
44@@ -70,7 +74,8 @@
45 self.get_revno = get_revno
46 self.annotate = None
47 self.rev_colors = {}
48- self.highlight_revids = set()
49+ self._highlight_revids = set()
50+ self.highlight_lines = []
51
52 self.splitter = None
53 self.adjustWidth(1, 999)
54@@ -80,6 +85,31 @@
55 self.edit_cursorPositionChanged)
56 self.show_current_line = False
57
58+ def get_highlight_revids(self):
59+ return self._highlight_revids
60+
61+ def set_highlight_revids(self, value):
62+ if self._highlight_revids == value:
63+ return
64+
65+ self._highlight_revids = value
66+ self.update_highlight_lines()
67+
68+ def update_highlight_lines(self):
69+ self.highlight_lines = []
70+ if not self.annotate:
71+ return
72+ lines = [i for i, (revno, istop) in enumerate(self.annotate)
73+ if revno in self._highlight_revids]
74+
75+ # Convert [0,1,2,5,6,9,14,15,16,17] to [(0,3),(5,2),(9,1),(14,4)]
76+ def summarize(lines):
77+ for k, g in groupby(enumerate(lines), key=lambda x:x[1]-x[0]):
78+ yield [line for i, line in g]
79+ self.highlight_lines = [(x[0], len(x)) for x in summarize(lines)]
80+
81+ highlight_revids = property(get_highlight_revids, set_highlight_revids)
82+
83 def edit_cursorPositionChanged(self):
84 self.show_current_line = True
85
86@@ -138,7 +168,7 @@
87 if self.annotate and line_number-1 < len(self.annotate):
88 revid, is_top = self.annotate[line_number - 1]
89 if is_top:
90- if revid in self.highlight_revids:
91+ if revid in self._highlight_revids:
92 font = painter.font()
93 font.setBold(True)
94 painter.setFont(font)
95@@ -271,11 +301,13 @@
96 self.text_edit.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap)
97
98 self.text_edit.document().setDefaultFont(get_monospace_font())
99-
100+
101+ self.guidebar_panel = GuideBarPanel(self.text_edit, parent=self)
102+ self.guidebar_panel.add_entry('annotate', QtGui.QColor(255, 160, 180))
103 self.annotate_bar = AnnotateBar(self.text_edit, self, self.get_revno)
104 annotate_spliter = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
105 annotate_spliter.addWidget(self.annotate_bar)
106- annotate_spliter.addWidget(self.text_edit)
107+ annotate_spliter.addWidget(self.guidebar_panel)
108 self.annotate_bar.splitter = annotate_spliter
109 self.text_edit_frame.hbox.addWidget(annotate_spliter)
110
111@@ -284,7 +316,10 @@
112 self.edit_cursorPositionChanged)
113 self.connect(self.annotate_bar,
114 QtCore.SIGNAL("cursorPositionChanged()"),
115- self.edit_cursorPositionChanged)
116+ self.edit_cursorPositionChanged)
117+ self.connect(self.text_edit,
118+ QtCore.SIGNAL("documentChangeFinished()"),
119+ self.edit_documentChangeFinished)
120
121 self.log_list = AnnotateLogList(self.processEvents, self.throbber, self)
122 self.log_list.header().hideSection(logmodel.COL_DATE)
123@@ -372,6 +407,7 @@
124 self.connect(self.show_find,
125 QtCore.SIGNAL("toggled (bool)"),
126 self.show_find_toggle)
127+ setup_guidebar_for_find(self.guidebar_panel, self.find_toolbar, index=1)
128
129 self.goto_line_toolbar = GotoLineToolbar(self, self.show_goto_line)
130 self.goto_line_toolbar.hide()
131@@ -584,6 +620,10 @@
132 if self.text_edit.annotate:
133 rev_id, is_top = self.text_edit.annotate[current_line]
134 self.log_list.select_revid(rev_id)
135+
136+ def edit_documentChangeFinished(self):
137+ self.annotate_bar.update_highlight_lines()
138+ self.guidebar_panel.update_data(annotate=self.annotate_bar.highlight_lines)
139
140 @runs_in_loading_queue
141 def set_annotate_revision(self):
142@@ -623,6 +663,7 @@
143 def log_list_selectionChanged(self, selected, deselected):
144 revids = self.log_list.get_selection_and_merged_revids()
145 self.annotate_bar.highlight_revids = revids
146+ self.guidebar_panel.update_data(annotate=self.annotate_bar.highlight_lines)
147 self.annotate_bar.update()
148
149 def show_find_toggle(self, state):
150
151=== modified file 'lib/diffview.py'
152--- lib/diffview.py 2011-09-20 18:33:50 +0000
153+++ lib/diffview.py 2012-02-01 16:39:02 +0000
154@@ -35,6 +35,9 @@
155 CachedTTypeFormater,
156 split_tokens_at_lines,
157 )
158+from bzrlib.plugins.qbzr.lib.widgets.texteditaccessory import (
159+ GuideBarPanel, GBAR_LEFT, GBAR_RIGHT
160+)
161
162 have_pygments = True
163 try:
164@@ -378,6 +381,12 @@
165 self.adjust_range()
166
167
168+
169+def setup_guidebar_entries(gb):
170+ gb.add_entry('title', QtGui.QColor(80, 80, 80), -1)
171+ for tag in ('delete', 'insert', 'replace'):
172+ gb.add_entry(tag, colors[tag][0], 0)
173+
174 class _SidebySideDiffView(QtGui.QSplitter):
175 """Widget to show differences in side-by-side format."""
176
177@@ -413,19 +422,30 @@
178 QtGui.QTextDocument())
179 self.browsers = (DiffSourceView(self),
180 DiffSourceView(self))
181+
182+ self.guidebar_panels = [
183+ GuideBarPanel(b, align=a)
184+ for (b, a) in zip(self.browsers, (GBAR_LEFT, GBAR_RIGHT))
185+ ]
186+ for g in self.guidebar_panels:
187+ setup_guidebar_entries(g.bar)
188+
189+ self.reset_guidebar_data()
190+
191 for b in self.browsers:
192 b.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
193
194 self.cursors = [QtGui.QTextCursor(doc) for doc in self.docs]
195
196- for i, (browser, doc, cursor) in enumerate(zip(self.browsers, self.docs, self.cursors)):
197+ for i, (panel, doc, cursor) in enumerate(zip(self.guidebar_panels,
198+ self.docs, self.cursors)):
199 doc.setUndoRedoEnabled(False)
200 doc.setDefaultFont(self.monospacedFont)
201
202+ panel.edit.setDocument(doc)
203+ self.addWidget(panel)
204 self.setCollapsible(i, False)
205- browser.setDocument(doc)
206- self.addWidget(browser)
207-
208+
209 format = QtGui.QTextCharFormat()
210 format.setAnchorNames(["top"])
211 cursor.insertText("", format)
212@@ -452,7 +472,13 @@
213 def setTabStopWidths(self, pixels):
214 for (pixel_width, browser) in zip(pixels, self.browsers):
215 browser.setTabStopWidth(pixel_width)
216-
217+
218+ def reset_guidebar_data(self):
219+ self.guidebar_data = [
220+ dict(title=[], delete=[], insert=[], replace=[]), # for left view
221+ dict(title=[], delete=[], insert=[], replace=[]), # for right view
222+ ]
223+
224 def clear(self):
225 self.browsers[0].clear()
226 self.browsers[1].clear()
227@@ -460,6 +486,7 @@
228 self.scrollbar.clear()
229 for doc in self.docs:
230 doc.clear()
231+ self.reset_guidebar_data()
232 self.update()
233
234 def set_complete(self, complete):
235@@ -471,8 +498,12 @@
236 def append_diff(self, paths, file_id, kind, status, dates,
237 present, binary, lines, groups, data, properties_changed):
238 cursors = self.cursors
239+
240+ guidebar_data = self.guidebar_data
241+
242 for i in range(2):
243 cursor = cursors[i]
244+ guidebar_data[i]['title'].append((cursor.block().blockNumber(), 2))
245 cursor.beginEditBlock()
246 cursor.insertText(paths[i] or " ", self.titleFormat) # None or " " => " "
247 cursor.insertBlock()
248@@ -633,6 +664,7 @@
249 insertIxs(ixs)
250 else:
251 y_top = [cursor.block().layout() for cursor in self.cursors]
252+ g_top = [cursor.block().blockNumber() for cursor in self.cursors]
253 if tag == "replace":
254 insertIxsWithChangesHighlighted(ixs)
255 else:
256@@ -640,7 +672,11 @@
257 linediff += n[0] - n[1]
258 y_bot = [cursor.block().layout() for cursor in self.cursors]
259 changes.append((y_top[0], y_bot[0], y_top[1], y_bot[1], tag))
260-
261+
262+ g_bot = [cursor.block().blockNumber() for cursor in self.cursors]
263+ for data, top, bot in zip(guidebar_data, g_top, g_bot):
264+ data[tag].append((top, bot - top))
265+
266 if linediff == 0:
267 continue
268 if not self.complete:
269@@ -728,6 +764,8 @@
270 or self.browsers[1].horizontalScrollBar().isVisible()):
271 self.browsers[0].setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
272 self.browsers[1].setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
273+
274+ self.update_guidebar()
275 self.update()
276
277 def rewind(self):
278@@ -761,8 +799,26 @@
279 def createHandle(self):
280 return DiffViewHandle(self)
281
282-
283-class SimpleDiffView(QtGui.QTextBrowser):
284+ def update_guidebar(self):
285+ for gb, data in zip(self.guidebar_panels, self.guidebar_data):
286+ gb.bar.update_data(**data)
287+
288+class SimpleDiffView(GuideBarPanel):
289+ def __init__(self, parent):
290+ self.view = _SimpleDiffView(parent)
291+ GuideBarPanel.__init__(self, self.view, parent=parent)
292+ setup_guidebar_entries(self)
293+
294+ def append_diff(self, *args, **kwargs):
295+ self.view.append_diff(*args, **kwargs)
296+ self.update_data(**self.view.guidebar_data)
297+
298+ def __getattr__(self, name):
299+ """Delegate unknown methods to internal diffview."""
300+ return getattr(self.view, name)
301+
302+
303+class _SimpleDiffView(QtGui.QTextBrowser):
304 """Widget to show differences in unidiff format."""
305
306 def __init__(self, parent=None):
307@@ -807,6 +863,8 @@
308 self.monospacedHunkFormat.setFont(monospacedItalicFont)
309 self.monospacedHunkFormat.setForeground(QtGui.QColor(153, 30, 199))
310
311+ self.reset_guidebar_data()
312+
313 def rewind(self):
314 if not self.rewinded:
315 self.rewinded = True
316@@ -817,6 +875,8 @@
317
318 def append_diff(self, paths, file_id, kind, status, dates,
319 present, binary, lines, groups, data, properties_changed):
320+ guidebar_data = self.guidebar_data
321+ guidebar_data['title'].append((self.cursor.block().blockNumber(), 2))
322 self.cursor.beginEditBlock()
323 path_info = paths[1] or paths[0]
324 if status in ('renamed', 'renamed and modified'):
325@@ -878,13 +938,24 @@
326 text = "".join(" " + l for l in a[i0:i1])
327 self.cursor.insertText(text, self.monospacedFormat)
328 else:
329+ start = self.cursor.block().blockNumber()
330 text = "".join("-" + l for l in a[i0:i1])
331 self.cursor.insertText(text, self.monospacedDeleteFormat)
332 text = "".join("+" + l for l in b[j0:j1])
333 self.cursor.insertText(text, self.monospacedInsertFormat)
334+ end = self.cursor.block().blockNumber()
335+ guidebar_data[tag].append((start, end - start))
336 else:
337 self.cursor.insertText("Binary files %s %s and %s %s differ\n" % \
338 (paths[0], dates[0], paths[1], dates[1]))
339 self.cursor.insertText("\n")
340 self.cursor.endEditBlock()
341 self.update()
342+
343+ def clear(self):
344+ QtGui.QTextBrowser.clear(self)
345+ self.reset_guidebar_data()
346+
347+ def reset_guidebar_data(self):
348+ self.guidebar_data = dict(title=[], delete=[], insert=[], replace=[])
349+
350
351=== modified file 'lib/diffwindow.py'
352--- lib/diffwindow.py 2012-01-24 20:34:19 +0000
353+++ lib/diffwindow.py 2012-02-01 16:39:02 +0000
354@@ -66,6 +66,7 @@
355 from bzrlib.plugins.qbzr.lib.trace import reports_exception
356 from bzrlib.plugins.qbzr.lib.encoding_selector import EncodingMenuSelector
357 from bzrlib.plugins.qbzr.lib.widgets.tab_width_selector import TabWidthMenuSelector
358+from bzrlib.plugins.qbzr.lib.widgets.texteditaccessory import setup_guidebar_for_find
359
360
361
362@@ -158,6 +159,9 @@
363 self.show_find)
364 self.find_toolbar.hide()
365 self.addToolBar(self.find_toolbar)
366+ setup_guidebar_for_find(self.sdiffview, self.find_toolbar, 1)
367+ for gb in self.diffview.guidebar_panels:
368+ setup_guidebar_for_find(gb, self.find_toolbar, 1)
369
370 def connect_later(self, *args, **kwargs):
371 """Schedules a signal to be connected after loading CLI arguments.
372@@ -487,7 +491,7 @@
373 def click_toggle_view_mode(self, checked):
374 if checked:
375 view = self.sdiffview
376- self.find_toolbar.set_text_edits([view])
377+ self.find_toolbar.set_text_edits([view.view])
378 self.tab_width_selector_left.menuAction().setVisible(False)
379 self.tab_width_selector_right.menuAction().setVisible(False)
380 self.tab_width_selector_unidiff.menuAction().setVisible(True)
381
382=== modified file 'lib/widgets/shelve.py'
383--- lib/widgets/shelve.py 2011-12-28 06:35:12 +0000
384+++ lib/widgets/shelve.py 2012-02-01 16:39:02 +0000
385@@ -37,6 +37,9 @@
386 FindToolbar, ToolbarPanel, LayoutSelector
387 )
388 from bzrlib.plugins.qbzr.lib.widgets.tab_width_selector import TabWidthMenuSelector
389+from bzrlib.plugins.qbzr.lib.widgets.texteditaccessory import (
390+ GuideBar, setup_guidebar_for_find
391+ )
392 from bzrlib.plugins.qbzr.lib.decorators import lazy_call
393 from bzrlib import errors
394 from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
395@@ -329,6 +332,8 @@
396 hunk_panel.add_widget(self.hunk_view)
397 find_toolbar.hide()
398
399+ setup_guidebar_for_find(self.hunk_view.guidebar, find_toolbar, index=1)
400+
401 layout = QtGui.QVBoxLayout()
402 layout.setMargin(10)
403 layout.addWidget(self.splitter)
404@@ -778,10 +783,13 @@
405 layout.setSpacing(0)
406 layout.setMargin(0)
407 self.browser = HunkTextBrowser(complete, self)
408+ self.guidebar = GuideBar(self.browser, parent=self)
409+ self.guidebar.add_entry('hunk', self.browser.focus_color)
410 self.selector = HunkSelector(self.browser, self)
411 layout.addWidget(self.selector)
412 layout.addWidget(self.browser)
413- self.connect(self.browser, QtCore.SIGNAL("focusedHunkChanged()"),
414+ layout.addWidget(self.guidebar)
415+ self.connect(self.browser, QtCore.SIGNAL("focusedHunkChanged()"),
416 self.update)
417
418 def selected_hunk_changed():
419@@ -814,6 +822,7 @@
420 self.change = change
421 self.encoding = encoding
422 self.browser.set_parsed_patch(change, encoding)
423+ self.guidebar.update_data(hunk=self.browser.guidebar_deta)
424 self.update()
425
426 def update(self):
427@@ -930,6 +939,7 @@
428
429 self.complete = complete
430 self._focused_index = -1
431+ self.guidebar_deta = []
432
433 def rewind(self):
434 if not self.rewinded:
435@@ -972,9 +982,11 @@
436 cursor.insertText(lines, self.monospacedInactiveFormat)
437 start = hunk.mod_pos + hunk.mod_range - 1
438 y1 = cursor.block().layout().position().y()
439+ l1 = cursor.block().blockNumber()
440 print_hunk(hunk, hunk_texts)
441 y2 = cursor.block().layout().position().y()
442-
443+ l2 = cursor.block().blockNumber()
444+ self.guidebar_deta.append((l1, l2 - l1))
445 else:
446 y1 = cursor.block().layout().position().y()
447 cursor.insertText(str(hunk.get_header()), self.monospacedHunkFormat)
448@@ -1003,6 +1015,7 @@
449 QtGui.QTextBrowser.clear(self)
450 del(self.hunk_list[:])
451 self._set_focused_hunk(-1)
452+ self.guidebar_deta = []
453 self.emit(QtCore.SIGNAL("documentChangeFinished()"))
454
455 def paintEvent(self, event):
456
457=== modified file 'lib/widgets/shelvelist.py'
458--- lib/widgets/shelvelist.py 2012-01-30 00:25:46 +0000
459+++ lib/widgets/shelvelist.py 2012-02-01 16:39:02 +0000
460@@ -456,7 +456,7 @@
461 if index == 0:
462 self.find_toolbar.set_text_edits(self.diffviews[0].browsers)
463 else:
464- self.find_toolbar.set_text_edits([self.diffviews[1]])
465+ self.find_toolbar.set_text_edits([self.diffviews[1].view])
466 self.stack.setCurrentIndex(index)
467
468 def complete_toggled(self, state):
469
470=== added file 'lib/widgets/texteditaccessory.py'
471--- lib/widgets/texteditaccessory.py 1970-01-01 00:00:00 +0000
472+++ lib/widgets/texteditaccessory.py 2012-02-01 16:39:02 +0000
473@@ -0,0 +1,294 @@
474+# -*- coding: utf-8 -*-
475+#
476+# QBzr - Qt frontend to Bazaar commands
477+# Copyright (C) 2011 QBzr Developers
478+#
479+# This program is free software; you can redistribute it and/or
480+# modify it under the terms of the GNU General Public License
481+# as published by the Free Software Foundation; either version 2
482+# of the License, or (at your option) any later version.
483+#
484+# This program is distributed in the hope that it will be useful,
485+# but WITHOUT ANY WARRANTY; without even the implied warranty of
486+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
487+# GNU General Public License for more details.
488+#
489+# You should have received a copy of the GNU General Public License
490+# along with this program; if not, write to the Free Software
491+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
492+
493+from PyQt4 import QtCore, QtGui
494+
495+GBAR_LEFT = 1
496+GBAR_RIGHT = 2
497+
498+class _Entry(object):
499+ """
500+ Represent each group of guide bar.
501+
502+ :key: string key to identify this group
503+ :color: color or marker
504+ :data: marker positions, list of tuple (block index, num of blocks)
505+ :index: index of the column to render this entry.
506+ * Two or more groups can be rendered on same columns.
507+ * If index == -1, the group is renderd on all columns.
508+ """
509+ __slots__ = ['key', 'color', 'data', 'index']
510+ def __init__(self, key, color, index=0):
511+ self.key = key
512+ self.color = color
513+ self.data = []
514+ self.index = index
515+
516+class PlainTextEditHelper(QtCore.QObject):
517+ """
518+ Helper class to encapsulate gap between QPlainTextEdit and QTextEdit
519+ """
520+ def __init__(self, edit):
521+ QtCore.QObject.__init__(self)
522+ if not isinstance(edit, QtGui.QPlainTextEdit):
523+ raise ValueError('edit must be QPlainTextEdit')
524+ self.edit = edit
525+
526+ self.connect(edit, QtCore.SIGNAL("updateRequest(const QRect&, int)"),
527+ self.onUpdateRequest)
528+
529+ def onUpdateRequest(self, rect, dy):
530+ self.emit(QtCore.SIGNAL("updateRequest()"))
531+
532+ def center_block(self, block):
533+ """
534+ scroll textarea as specified block locates to center
535+
536+ NOTE: This code is based on Qt source code (qplaintextedit.cpp)
537+ """
538+ edit = self.edit
539+ height = edit.viewport().rect().height() / 2
540+ h = self.edit.blockBoundingRect(block).center().y()
541+ def iter_visible_block_backward(b):
542+ while True:
543+ b = b.previous()
544+ if not b.isValid(): return
545+ if b.isVisible(): yield b
546+ for block in iter_visible_block_backward(block):
547+ h += edit.blockBoundingRect(block).height()
548+ if height < h:
549+ break
550+ edit.verticalScrollBar().setValue(block.firstLineNumber())
551+
552+class TextEditHelper(QtCore.QObject):
553+ """
554+ Helper class to encapsulate gap between QPlainTextEdit and QTextEdit
555+ """
556+ def __init__(self, edit):
557+ QtCore.QObject.__init__(self)
558+ if not isinstance(edit, QtGui.QTextEdit):
559+ raise ValueError('edit must be QTextEdit')
560+ self.edit = edit
561+
562+ self.connect(edit.verticalScrollBar(), QtCore.SIGNAL("valueChanged(int)"),
563+ self.onVerticalScroll)
564+
565+ def onVerticalScroll(self, value):
566+ self.emit(QtCore.SIGNAL("updateRequest()"))
567+
568+ def center_block(self, block):
569+ """
570+ scroll textarea as specified block locates to center
571+ """
572+ y = block.layout().position().y()
573+ vscroll = self.edit.verticalScrollBar()
574+ vscroll.setValue(y - vscroll.pageStep() / 2)
575+
576+def get_edit_helper(edit):
577+ if isinstance(edit, QtGui.QPlainTextEdit):
578+ return PlainTextEditHelper(edit)
579+ if isinstance(edit, QtGui.QTextEdit):
580+ return TextEditHelper(edit)
581+ raise ValueError("edit is unsupported type.")
582+
583+class GuideBar(QtGui.QWidget):
584+ """
585+ Vertical bar attached to TextEdit.
586+ This shows that where changed or highlighted lines are.
587+
588+ Guide bar can have multiple columns.
589+ """
590+ def __init__(self, edit, base_width=10, parent=None):
591+ """
592+ :edit: target widget, must be QPlainTextEdit or QTextEdit
593+ :base_width: width of each column.
594+ """
595+ QtGui.QWidget.__init__(self, parent)
596+ self.base_width = base_width
597+ self.edit = edit
598+ self._helper = get_edit_helper(edit)
599+ self.block_count = 0
600+
601+ self.connect(edit, QtCore.SIGNAL("documentChangeFinished()"),
602+ self.reset_gui)
603+ self.connect(edit.verticalScrollBar(), QtCore.SIGNAL("rangeChanged(int, int)"),
604+ self.vscroll_rangeChanged)
605+
606+ self.connect(self._helper, QtCore.SIGNAL("updateRequest()"), self.update)
607+
608+ self.entries = {}
609+ self.vscroll_visible = None
610+
611+ def add_entry(self, key, color, index=0):
612+ """
613+ Add marker group
614+ """
615+ entry = _Entry(key, color, index)
616+ self.entries[key] = entry
617+
618+ def vscroll_rangeChanged(self, min, max):
619+ vscroll_visible = (min < max)
620+ if self.vscroll_visible != vscroll_visible:
621+ self.vscroll_visible = vscroll_visible
622+ self.reset_gui()
623+ self.update()
624+
625+ def reset_gui(self):
626+ """
627+ Determine show or hide, and num of columns.
628+ """
629+ # Hide when vertical scrollbar is not shown.
630+ if not self.vscroll_visible:
631+ self.setVisible(False)
632+ return
633+ valid_entries = [e for e in self.entries.itervalues() if e.data]
634+ if not valid_entries:
635+ self.setVisible(False)
636+ return
637+
638+ self.setVisible(True)
639+ self.repeats = len(set([e.index for e in valid_entries if e.index >= 0]))
640+ if self.repeats == 0:
641+ self.repeats = 1
642+ self.setFixedWidth(self.repeats * self.base_width + 4)
643+
644+ self.block_count = self.edit.document().blockCount()
645+ self.update()
646+
647+ def update_data(self, **data):
648+ """
649+ Update each marker positions.
650+
651+ :arg_name: marker key
652+ :value: list of marker positions.
653+ Each position is tuple of (block index, num of blocks).
654+ """
655+ for key, value in data.iteritems():
656+ self.entries[key].data[:] = value
657+ self.reset_gui()
658+
659+ def get_visible_block_range(self):
660+ """
661+ Return tuple of (index of first visible block, num of visible block)
662+ """
663+ pos = QtCore.QPoint(0, 1)
664+ block = self.edit.cursorForPosition(pos).block()
665+ first_visible_block = block.blockNumber()
666+
667+ y = self.edit.viewport().height()
668+
669+ pos = QtCore.QPoint(0, y)
670+ block = self.edit.cursorForPosition(pos).block()
671+ if block.isValid():
672+ visible_blocks = block.blockNumber() - first_visible_block + 1
673+ else:
674+ visible_blocks = self.block_count - first_visible_block + 1
675+
676+ return first_visible_block, visible_blocks
677+
678+ def paintEvent(self, event):
679+ QtGui.QWidget.paintEvent(self, event)
680+ painter = QtGui.QPainter(self)
681+ painter.fillRect(event.rect(), QtCore.Qt.white)
682+ if self.block_count == 0:
683+ return
684+ painter.setRenderHints(QtGui.QPainter.Antialiasing, True)
685+ block_height = float(self.height()) / self.block_count
686+
687+ # Draw entries
688+ x_origin = 2
689+ index = -1
690+ prev_index = -1
691+ for e in sorted(self.entries.itervalues(),
692+ key=lambda x:x.index if x.index >= 0 else 999):
693+ if not e.data:
694+ continue
695+ if e.index < 0:
696+ x, width = 0, self.width()
697+ else:
698+ if e.index != prev_index:
699+ index += 1
700+ prev_index = e.index
701+ x, width = x_origin + index * self.base_width, self.base_width
702+ for block_index, block_num in e.data:
703+ y = block_index * block_height
704+ height = max(1, block_num * block_height)
705+ painter.fillRect(x, y, width, height, e.color)
706+
707+ # Draw scroll indicator.
708+ x, width = 0, self.width()
709+ first_block, visible_blocks = self.get_visible_block_range()
710+ y, height = first_block * block_height, max(1, visible_blocks * block_height)
711+ painter.fillRect(x, y, width, height, QtGui.QColor(0, 0, 0, 24))
712+
713+ def mousePressEvent(self, event):
714+ QtGui.QWidget.mousePressEvent(self, event)
715+ if event.button() == QtCore.Qt.LeftButton:
716+ self.scroll_to_pos(event.y())
717+
718+ def mouseMoveEvent(self, event):
719+ QtGui.QWidget.mouseMoveEvent(self, event)
720+ self.scroll_to_pos(event.y())
721+
722+ def scroll_to_pos(self, y):
723+ block_no = int(float(y) / self.height() * self.block_count)
724+ block = self.edit.document().findBlockByNumber(block_no)
725+ if not block.isValid():
726+ return
727+ self._helper.center_block(block)
728+
729+class GuideBarPanel(QtGui.QWidget):
730+ """
731+ Composite widget of TextEdit and GuideBar
732+ """
733+ def __init__(self, edit, base_width=10, align=GBAR_RIGHT, parent=None):
734+ QtGui.QWidget.__init__(self, parent)
735+ hbox = QtGui.QHBoxLayout(self)
736+ hbox.setSpacing(0)
737+ hbox.setMargin(0)
738+ self.bar = GuideBar(edit, base_width=base_width, parent=parent)
739+ self.edit = edit
740+ if align == GBAR_RIGHT:
741+ hbox.addWidget(self.edit)
742+ hbox.addWidget(self.bar)
743+ else:
744+ hbox.addWidget(self.bar)
745+ hbox.addWidget(self.edit)
746+
747+ def add_entry(self, key, color, index=0):
748+ self.bar.add_entry(key, color, index)
749+
750+ def reset_gui(self):
751+ self.bar.reset_gui()
752+
753+ def update_data(self, **data):
754+ return self.bar.update_data(**data)
755+
756+def setup_guidebar_for_find(guidebar, find_toolbar, index=0):
757+ """
758+ Make guidebar enable to show positions that highlighted by FindToolBar
759+ """
760+ def on_highlight_changed():
761+ if guidebar.edit in find_toolbar.text_edits:
762+ guidebar.update_data(
763+ find=[(n, 1) for n in guidebar.edit.highlight_lines]
764+ )
765+ guidebar.add_entry('find', QtGui.QColor(255, 196, 0), index) # Gold
766+ guidebar.connect(find_toolbar, QtCore.SIGNAL("highlightChanged()"),
767+ on_highlight_changed)
768
769=== modified file 'lib/widgets/toolbars.py'
770--- lib/widgets/toolbars.py 2011-12-28 12:06:42 +0000
771+++ lib/widgets/toolbars.py 2012-02-01 16:39:02 +0000
772@@ -48,7 +48,7 @@
773 parent.connect(button, QtCore.SIGNAL(signal), onclick)
774 return button
775
776-def add_toolbar_button(toolbar, text, parent, icon_name=None, icon_size=22,
777+def add_toolbar_button(toolbar, text, parent, icon_name=None, icon_size=22,
778 enabled=True, checkable=False, checked=False,
779 shortcut=None, onclick=None):
780 button = create_toolbar_button(text, parent, icon_name, icon_size,
781@@ -56,7 +56,7 @@
782 toolbar.addAction(button)
783 return button
784
785-
786+
787 class FindToolbar(QtGui.QToolBar):
788
789 def __init__(self, window, text_edit, show_action):
790@@ -131,7 +131,7 @@
791 self.connect(self.find_text,
792 QtCore.SIGNAL("returnPressed()"),
793 self.find_next)
794-
795+
796 def show_action_toggle(self, state):
797 self.setVisible(state)
798 if state:
799@@ -197,6 +197,7 @@
800 t.setExtraSelections([])
801
802 for t in text_edits:
803+ t.highlight_lines = []
804 self.connect(t, QtCore.SIGNAL("documentChangeFinished()"),
805 self.highlight)
806
807@@ -217,6 +218,7 @@
808 flags = self.find_get_flags()
809 for text_edit in self.text_edits:
810 selections = []
811+ highlight_lines = []
812 if text:
813 find = text_edit.document().find
814 pos = 0
815@@ -230,9 +232,13 @@
816 sel = QtGui.QTextEdit.ExtraSelection()
817 sel.cursor, sel.format = cursor, fmt
818 selections.append(sel)
819+ highlight_lines.append(cursor.blockNumber())
820 pos = cursor.selectionEnd()
821
822 text_edit.setExtraSelections(selections)
823+ text_edit.highlight_lines = highlight_lines
824+
825+ self.emit(QtCore.SIGNAL("highlightChanged()"))
826
827 class ToolbarPanel(QtGui.QWidget):
828 def __init__(self, slender=True, icon_size=16, parent=None):
829@@ -255,7 +261,7 @@
830
831 def add_toolbar_button(self, text, icon_name=None, icon_size=0, enabled=True,
832 checkable=False, checked=False, shortcut=None, onclick=None, menu=None):
833- button = create_toolbar_button(text, self, icon_name=icon_name,
834+ button = create_toolbar_button(text, self, icon_name=icon_name,
835 icon_size=icon_size or self.icon_size, enabled=enabled,
836 checkable=checkable, checked=checked, shortcut=shortcut, onclick=onclick)
837 if menu is not None:
838@@ -273,7 +279,7 @@
839 show_shortcut_hint(widget)
840 return button
841
842- def create_button(self, text, icon_name=None, icon_size=0, enabled=True,
843+ def create_button(self, text, icon_name=None, icon_size=0, enabled=True,
844 checkable=False, checked=False, shortcut=None, onclick=None):
845 return create_toolbar_button(text, self, icon_name=icon_name,
846 icon_size=icon_size or self.icon_size, enabled=enabled,

Subscribers

People subscribed via source and target branches