Merge lp:~garyvdm/qbzr/newwtlist into lp:~qbzr-dev/qbzr/trunk

Proposed by Gary van der Merwe
Status: Merged
Merged at revision: not available
Proposed branch: lp:~garyvdm/qbzr/newwtlist
Merge into: lp:~qbzr-dev/qbzr/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~garyvdm/qbzr/newwtlist

This proposal supersedes a proposal from 2009-07-12.

Commit message

qcommit/qadd/qrevert: Replace old wtlist with the new TreeWidget from qbrowse.

To post a comment you must log in.
Revision history for this message
Gary van der Merwe (garyvdm) wrote : Posted in a previous version of this proposal

This replaces wtlist with treewidget in qcommit, qadd, and qrevert.

To do this I have added a mode to the treewidget called changes_mode. This only show changes, and not the whole tree. Like the old wtlist, changes from subdirs are shown in the root, e.g.

In normal mode, you would see:
Dir1
  File1 Modified

but in changes_mode, you will see:
Dir1/File1 Modified

Unless a parent directory is shown, which may happen if you have a unversioned, added, or renamed directory.

e.g. in changes_mode, you will get:

dir1 => dir2 Renamed
  File3 Modified

dir4 Added
  file5 Added

Drive by improvement: qcommit now adjusts the auto complete word list if you change which files are selected in the file list.

Revision history for this message
Gary van der Merwe (garyvdm) wrote : Posted in a previous version of this proposal
Revision history for this message
Gary van der Merwe (garyvdm) wrote : Posted in a previous version of this proposal

I just realised that this does not honer the selected list passed from the command line. I need to fix that.

Revision history for this message
Gary van der Merwe (garyvdm) wrote : Posted in a previous version of this proposal

The above mentioned problem has now been fixed.

Revision history for this message
Gary van der Merwe (garyvdm) wrote :

This replaces wtlist with treewidget in qcommit, qadd, and qrevert.

To do this I have added a mode to the treewidget called changes_mode. This only
show changes, and not the whole tree. Like the old wtlist, changes from subdirs
are shown in the root, e.g.

In normal mode, you would see:
Dir1
  File1 Modified

but in changes_mode, you will see:
Dir1/File1 Modified

Unless a parent directory is shown, which may happen if you have a unversioned,
added, or renamed directory.

e.g. in changes_mode, you will get:

dir1 => dir2 Renamed
  File3 Modified

dir4 Added
  file5 Added

Drive by improvement: qcommit now adjusts the auto complete word list if you
change which files are selected in the file list.

Some screen shots:

http://garyvdm.googlepages.com/qcommit_new_widget.png
http://garyvdm.googlepages.com/qadd_new_widget.png

lp:~garyvdm/qbzr/newwtlist updated
816. By Gary van der Merwe

Merge Javier's qsend dialog.

817. By Gary van der Merwe

qbrowse: Don't check for is_ignored on the tree root.

818. By Gary van der Merwe

qcommit: Show spelling suggestions in the message box context menu.

819. By Gary van der Merwe

Ext diff - Don't pass old revspec if none was provided for working trees.

820. By Gary van der Merwe

Change qcommit, qadd, and qrevert to use new TreeWidget. Remove lib/wtlist.py.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/add.py'
2--- lib/add.py 2009-06-11 21:21:01 +0000
3+++ lib/add.py 2009-07-14 03:00:21 +0000
4@@ -23,11 +23,16 @@
5
6 from bzrlib.plugins.qbzr.lib.i18n import gettext
7 from bzrlib.plugins.qbzr.lib.subprocess import SubProcessDialog
8-from bzrlib.plugins.qbzr.lib.wtlist import (
9- ChangeDesc,
10- WorkingTreeFileList,
11- closure_in_selected_list,
12- )
13+from bzrlib.plugins.qbzr.lib.treewidget import (
14+ TreeWidget,
15+ SelectAllCheckBox,
16+ )
17+from bzrlib.plugins.qbzr.lib.util import (
18+ ThrobberWidget,
19+ runs_in_loading_queue,
20+ )
21+from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
22+from bzrlib.plugins.qbzr.lib.trace import reports_exception
23
24
25 class AddWindow(SubProcessDialog):
26@@ -46,22 +51,36 @@
27 hide_progress=True,
28 )
29
30+ self.throbber = ThrobberWidget(self)
31+
32 # Display the list of unversioned files
33 groupbox = QtGui.QGroupBox(gettext("Unversioned Files"), self)
34 vbox = QtGui.QVBoxLayout(groupbox)
35
36- self.filelist = WorkingTreeFileList(groupbox, self.tree)
37+ self.filelist = TreeWidget(groupbox)
38+ self.filelist.throbber = self.throbber
39+ self.filelist.tree_model.is_item_in_select_all = lambda item: (
40+ item.change is not None and
41+ item.change.is_ignored() is None and
42+ not item.change.is_versioned())
43+
44+ def filter_context_menu():
45+ self.filelist.action_open_file.setEnabled(True)
46+ self.filelist.action_open_file.setVisible(True)
47+ self.filelist.action_show_file.setVisible(False)
48+ self.filelist.action_show_annotate.setVisible(False)
49+ self.filelist.action_show_log.setVisible(False)
50+ self.filelist.action_show_diff.setVisible(False)
51+ self.filelist.action_add.setVisible(False)
52+ self.filelist.action_revert.setVisible(False)
53+ self.filelist.filter_context_menu = filter_context_menu
54+
55 vbox.addWidget(self.filelist)
56- self.filelist.sortItems(0, QtCore.Qt.AscendingOrder)
57- self.filelist.setup_actions()
58
59- selectall_checkbox = QtGui.QCheckBox(
60- gettext(self.filelist.SELECTALL_MESSAGE),
61- groupbox)
62+ selectall_checkbox = SelectAllCheckBox(self.filelist, groupbox)
63 vbox.addWidget(selectall_checkbox)
64 selectall_checkbox.setCheckState(QtCore.Qt.Checked)
65 selectall_checkbox.setEnabled(True)
66- self.filelist.set_selectall_checkbox(selectall_checkbox)
67
68 self.show_ignored_checkbox = QtGui.QCheckBox(
69 gettext("Show ignored files"),
70@@ -69,12 +88,6 @@
71 vbox.addWidget(self.show_ignored_checkbox)
72 self.connect(self.show_ignored_checkbox, QtCore.SIGNAL("toggled(bool)"), self.show_ignored)
73
74- self.tree.lock_read()
75- try:
76- self.filelist.fill(self.iter_changes_and_state())
77- finally:
78- self.tree.unlock()
79-
80 # groupbox gets disabled as we are executing.
81 QtCore.QObject.connect(self,
82 QtCore.SIGNAL("subprocessStarted(bool)"),
83@@ -88,44 +101,39 @@
84 self.restoreSplitterSizes([150, 150])
85
86 layout = QtGui.QVBoxLayout(self)
87+ layout.addWidget(self.throbber)
88 layout.addWidget(self.splitter)
89 layout.addWidget(self.buttonbox)
90-
91- def iter_changes_and_state(self):
92- """An iterator for the WorkingTreeFileList widget"""
93-
94- in_selected_list = closure_in_selected_list(self.initial_selected_list)
95-
96- show_ignored = self.show_ignored_checkbox.isChecked()
97-
98- for desc in self.tree.iter_changes(self.tree.basis_tree(),
99- want_unversioned=True):
100-
101- desc = ChangeDesc(desc)
102- if desc.is_versioned():
103- continue
104-
105- pit = desc.path()
106- visible = show_ignored or not self.tree.is_ignored(pit)
107- check_state = visible and in_selected_list(pit)
108- yield desc, visible, check_state
109+ self.throbber.show()
110+
111+
112+ def show(self):
113+ SubProcessDialog.show(self)
114+ QtCore.QTimer.singleShot(1, self.initial_load)
115+
116+ @runs_in_loading_queue
117+ @ui_current_widget
118+ @reports_exception()
119+ def initial_load(self):
120+ self.filelist.tree_model.checkable = True
121+ fmodel = self.filelist.tree_filter_model
122+ fmodel.setFilter(fmodel.CHANGED, False)
123+ self.filelist.set_tree(self.tree, changes_mode = True,
124+ initial_checked_paths=self.initial_selected_list)
125+ self.throbber.hide()
126
127 def start(self):
128 """Add the files."""
129- files = []
130- for desc in self.filelist.iter_checked():
131- files.append(desc.path())
132+ files = [ref.path for ref in self.filelist.tree_model.iter_checked()]
133
134- self.process_widget.start(self.tree.basedir, "add", *files)
135+ self.process_widget.start(self.tree.basedir, "add", "--no-recurse",
136+ *files)
137
138 def show_ignored(self, state):
139 """Show/hide ignored files."""
140- state = not state
141- for (tree_item, change_desc) in self.filelist.iter_treeitem_and_desc(True):
142- path = change_desc.path()
143- if self.tree.is_ignored(path):
144- self.filelist.set_item_hidden(tree_item, state)
145- self.filelist.update_selectall_state(None, None)
146+ fmodel = self.filelist.tree_filter_model
147+ fmodel.setFilter(fmodel.IGNORED, state)
148+ #self.filelist.update_selectall_state(None, None)
149
150 def saveSize(self):
151 SubProcessDialog.saveSize(self)
152
153=== modified file 'lib/commit.py'
154--- lib/commit.py 2009-07-12 16:17:52 +0000
155+++ lib/commit.py 2009-07-14 03:31:17 +0000
156@@ -36,15 +36,18 @@
157 get_global_config,
158 url_for_display,
159 ThrobberWidget,
160- )
161-from bzrlib.plugins.qbzr.lib.wtlist import (
162- ChangeDesc,
163- WorkingTreeFileList,
164- closure_in_selected_list,
165- )
166+ runs_in_loading_queue,
167+ )
168+from bzrlib.plugins.qbzr.lib.treewidget import TreeWidget
169+
170 from bzrlib.plugins.qbzr.lib.logwidget import LogList
171 from bzrlib.plugins.qbzr.lib.trace import *
172 from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
173+from bzrlib.plugins.qbzr.lib.treewidget import (
174+ TreeWidget,
175+ SelectAllCheckBox,
176+ )
177+from bzrlib.plugins.qbzr.lib.trace import reports_exception
178
179
180 MAX_AUTOCOMPLETE_FILES = 20
181@@ -143,55 +146,6 @@
182 RevisionIdRole = QtCore.Qt.UserRole + 1
183 ParentIdRole = QtCore.Qt.UserRole + 2
184
185- def iter_changes_and_state(self):
186- """An iterator for the WorkingTreeFileList widget"""
187- # a word list for message completer
188- words = set()
189- show_nonversioned = self.show_nonversioned_checkbox.isChecked()
190-
191- in_selected_list = closure_in_selected_list(self.initial_selected_list)
192-
193- num_versioned_files = 0
194- for desc in self.tree.iter_changes(self.tree.basis_tree(),
195- want_unversioned=True):
196- desc = ChangeDesc(desc)
197-
198- if desc.is_tree_root() or desc.is_misadded():
199- # skip uninteresting enties
200- continue
201-
202- is_versioned = desc.is_versioned()
203- path = desc.path()
204-
205- if not is_versioned and self.tree.is_ignored(path):
206- continue
207-
208- visible = is_versioned or show_nonversioned
209- check_state = None
210- if not self.has_pending_merges:
211- check_state = visible and is_versioned and in_selected_list(path)
212- yield desc, visible, check_state
213-
214- if is_versioned:
215- num_versioned_files += 1
216-
217- words.update(os.path.split(path))
218- if desc.is_renamed():
219- words.update(os.path.split(desc.oldpath()))
220- if num_versioned_files < MAX_AUTOCOMPLETE_FILES:
221- ext = file_extension(path)
222- builder = get_wordlist_builder(ext)
223- if builder is not None:
224- try:
225- abspath = os.path.join(self.tree.basedir, path)
226- file = open(abspath, 'rt')
227- words.update(builder.iter_words(file))
228- except EnvironmentError:
229- pass
230- words = list(words)
231- words.sort(lambda a, b: cmp(a.lower(), b.lower()))
232- self.completion_words = words
233-
234 def __init__(self, tree, selected_list, dialog=True, parent=None,
235 local=None, message=None, ui_mode=True):
236 super(CommitWindow, self).__init__(
237@@ -202,13 +156,13 @@
238 dialog = dialog,
239 parent = parent)
240 self.tree = tree
241- tree.lock_read()
242- try:
243- self.basis_tree = self.tree.basis_tree()
244- self.is_bound = bool(tree.branch.get_bound_location())
245- self.has_pending_merges = len(tree.get_parent_ids())>1
246- finally:
247- tree.unlock()
248+ #tree.lock_read()
249+ #try:
250+ # self.basis_tree = self.tree.basis_tree()
251+ self.is_bound = bool(tree.branch.get_bound_location())
252+ self.has_pending_merges = len(tree.get_parent_ids())>1
253+ #finally:
254+ # tree.unlock()
255
256 self.windows = []
257 self.initial_selected_list = selected_list
258@@ -263,16 +217,27 @@
259 self.show_nonversioned_checkbox = QtGui.QCheckBox(
260 gettext("Show non-versioned files"))
261
262- self.filelist = WorkingTreeFileList(message_groupbox, self.tree)
263- selectall_checkbox = QtGui.QCheckBox(
264- gettext(self.filelist.SELECTALL_MESSAGE))
265+ self.filelist = TreeWidget(self)
266+ self.filelist.throbber = self.throbber
267+ self.filelist.tree_model.is_item_in_select_all = lambda item: (
268+ item.change is not None and
269+ item.change.is_ignored() is None and
270+ item.change.is_versioned())
271+
272+ self.file_words = {}
273+ self.connect(self.filelist.tree_model,
274+ QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
275+ self.on_filelist_data_changed)
276+
277+ selectall_checkbox = SelectAllCheckBox(self.filelist, self)
278 selectall_checkbox.setCheckState(QtCore.Qt.Checked)
279- self.filelist.set_selectall_checkbox(selectall_checkbox)
280
281 # Equivalent for 'bzr commit --message'
282 self.message = TextEdit(message_groupbox, main_window=self)
283 self.message.setToolTip(gettext("Enter the commit message"))
284 self.completer = QtGui.QCompleter()
285+ self.completer_model = QtGui.QStringListModel(self.completer)
286+ self.completer.setModel(self.completer_model)
287 self.message.setCompleter(self.completer)
288 self.message.setAcceptRichText(False)
289
290@@ -328,8 +293,6 @@
291
292 vbox.addWidget(selectall_checkbox)
293
294- self.filelist.sortItems(0, QtCore.Qt.AscendingOrder)
295-
296 # Display a list of pending merges
297 if self.has_pending_merges:
298 selectall_checkbox.setCheckState(QtCore.Qt.Checked)
299@@ -392,7 +355,8 @@
300 # we show the bare form as soon as possible.
301 SubProcessDialog.show(self)
302 QtCore.QTimer.singleShot(1, self.load)
303-
304+
305+ @runs_in_loading_queue
306 @ui_current_widget
307 @reports_exception()
308 def load(self):
309@@ -403,14 +367,58 @@
310 None,
311 self.tree)
312 self.pending_merges_list.load()
313- self.filelist.fill(self.iter_changes_and_state())
314+ self.processEvents()
315+
316+ self.filelist.tree_model.checkable = not self.pending_merges_list
317+ fmodel = self.filelist.tree_filter_model
318+ fmodel.setFilter(fmodel.UNVERSIONED, False)
319+ self.filelist.set_tree(self.tree, changes_mode = True,
320+ initial_checked_paths = self.initial_selected_list)
321+ self.processEvents()
322+ self.update_compleater_words()
323 finally:
324 self.tree.unlock()
325
326- self.filelist.setup_actions()
327- self.completer.setModel(QtGui.QStringListModel(self.completion_words,
328- self.completer))
329 self.throbber.hide()
330+
331+ def on_filelist_data_changed(self, start_index, end_index):
332+ self.update_compleater_words()
333+
334+ def update_compleater_words(self):
335+ num_files_loaded = 0
336+
337+ words = set()
338+ for ref in self.filelist.tree_model.iter_checked():
339+ path = ref.path
340+ if path not in self.file_words:
341+ file_words = set()
342+ if num_files_loaded < MAX_AUTOCOMPLETE_FILES:
343+ file_words.add(path)
344+ file_words.add(os.path.split(path)[-1])
345+ change = self.filelist.tree_model.inventory_data_by_path[
346+ ref.path].change
347+ if change.is_renamed():
348+ file_words.add(change.oldpath())
349+ file_words.add(os.path.split(change.oldpath())[-1])
350+ #if num_versioned_files < MAX_AUTOCOMPLETE_FILES:
351+ ext = file_extension(path)
352+ builder = get_wordlist_builder(ext)
353+ if builder is not None:
354+ try:
355+ abspath = os.path.join(self.tree.basedir, path)
356+ file = open(abspath, 'rt')
357+ file_words.update(builder.iter_words(file))
358+ self.processEvents()
359+ except EnvironmentError:
360+ pass
361+ self.file_words[path] = file_words
362+ num_files_loaded += 1
363+ else:
364+ file_words = self.file_words[path]
365+ words.update(file_words)
366+ words = list(words)
367+ words.sort(lambda a, b: cmp(a.lower(), b.lower()))
368+ self.completer_model.setStringList(words)
369
370 def enableBugs(self, state):
371 if state == QtCore.Qt.Checked:
372@@ -460,7 +468,7 @@
373
374 def start(self):
375 args = ["commit"]
376- files_to_add = ["add"]
377+ files_to_add = ["add", "--no-recurse"]
378
379 message = unicode(self.message.toPlainText()).strip()
380 if not message:
381@@ -479,12 +487,11 @@
382 checkedFiles = 1
383 if not self.has_pending_merges:
384 checkedFiles = 0
385- for desc in self.filelist.iter_checked():
386- checkedFiles = checkedFiles+1
387- path = desc.path()
388- if not desc.is_versioned():
389- files_to_add.append(path)
390- args.append(path)
391+ for ref in self.filelist.tree_model.iter_checked():
392+ if ref.file_id is None:
393+ files_to_add.append(ref.path)
394+ args.append(ref.path)
395+ checkedFiles = 1
396
397 if checkedFiles == 0: # BUG: 295116
398 # check for availability of --exclude option for commit
399@@ -540,11 +547,8 @@
400
401 def show_nonversioned(self, state):
402 """Show/hide non-versioned files."""
403- state = not state
404- for (tree_item, change_desc) in self.filelist.iter_treeitem_and_desc(True):
405- if change_desc[3] == (False, False):
406- self.filelist.set_item_hidden(tree_item, state)
407- self.filelist.update_selectall_state(None, None)
408+ fmodel = self.filelist.tree_filter_model
409+ fmodel.setFilter(fmodel.UNVERSIONED, state)
410
411 def closeEvent(self, event):
412 if not self.process_widget.is_running():
413@@ -570,7 +574,7 @@
414 # update GUI
415 self.branch_location.setText(loc)
416 self.commit_type_description.setText(desc)
417-
418+#
419 def show_diff_for_checked(self, ext_diff=None, dialog_action='commit'):
420 """Diff button clicked: show the diff for checked entries.
421
422@@ -580,12 +584,11 @@
423 # XXX make this function universal for both qcommit and qrevert (?)
424 checked = [] # checked versioned
425 unversioned = [] # checked unversioned (supposed to be added)
426- for desc in self.filelist.iter_checked():
427- path = desc.path()
428- if desc.is_versioned():
429- checked.append(path)
430+ for ref in self.filelist.tree_model.iter_checked():
431+ if ref.file_id:
432+ checked.append(ref.path)
433 else:
434- unversioned.append(path)
435+ unversioned.append(ref.path)
436
437 if checked:
438 arg_provider = InternalWTDiffArgProvider(
439
440=== modified file 'lib/diff.py'
441--- lib/diff.py 2009-07-13 00:31:49 +0000
442+++ lib/diff.py 2009-07-14 03:02:00 +0000
443@@ -69,6 +69,7 @@
444 window.process_widget.hide_progress()
445 if parent_window:
446 parent_window.windows.append(window)
447+ window.show()
448
449
450 def has_ext_diff():
451
452=== modified file 'lib/diff_arg.py'
453--- lib/diff_arg.py 2009-06-23 00:38:58 +0000
454+++ lib/diff_arg.py 2009-07-10 05:16:40 +0000
455@@ -93,7 +93,9 @@
456 from bzrlib import urlutils
457
458 args = []
459- args.append(self.get_revspec())
460+ revspec = self.get_revspec()
461+ if revspec:
462+ args.append(revspec)
463
464 if not self.old_branch.base == self.new_branch.base:
465 args.append("--old=%s" % self.old_branch.base)
466@@ -138,7 +140,10 @@
467 self.specific_files)
468
469 def get_revspec(self):
470- return "-r revid:%s" % (self.old_revid,)
471+ if self.old_revid is not None:
472+ return "-r revid:%s" % (self.old_revid,)
473+ else:
474+ return None
475
476 def need_to_load_paths(self):
477 return False
478
479=== modified file 'lib/revert.py'
480--- lib/revert.py 2009-06-11 21:21:01 +0000
481+++ lib/revert.py 2009-07-14 03:00:21 +0000
482@@ -28,11 +28,16 @@
483 )
484 from bzrlib.plugins.qbzr.lib.i18n import gettext
485 from bzrlib.plugins.qbzr.lib.subprocess import SubProcessDialog
486-from bzrlib.plugins.qbzr.lib.wtlist import (
487- ChangeDesc,
488- WorkingTreeFileList,
489- closure_in_selected_list,
490- )
491+from bzrlib.plugins.qbzr.lib.treewidget import (
492+ TreeWidget,
493+ SelectAllCheckBox,
494+ )
495+from bzrlib.plugins.qbzr.lib.util import (
496+ ThrobberWidget,
497+ runs_in_loading_queue,
498+ )
499+from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
500+from bzrlib.plugins.qbzr.lib.trace import reports_exception
501
502
503 class RevertWindow(SubProcessDialog):
504@@ -51,26 +56,27 @@
505 parent = parent,
506 hide_progress=True)
507
508+ self.throbber = ThrobberWidget(self)
509+
510 # Display the list of changed files
511 groupbox = QtGui.QGroupBox(gettext("Changes"), self)
512
513- self.filelist = WorkingTreeFileList(groupbox, self.tree)
514-
515- self.tree.lock_read()
516- try:
517- self.filelist.fill(self.iter_changes_and_state())
518- finally:
519- self.tree.unlock()
520-
521- self.filelist.setup_actions()
522+ self.filelist = TreeWidget(groupbox)
523+ self.filelist.throbber = self.throbber
524+ self.filelist.tree_model.is_item_in_select_all = lambda item: (
525+ item.change is not None and item.change.is_versioned())
526+ self.filelist.setRootIsDecorated(False)
527+ def filter_context_menu():
528+ TreeWidget.filter_context_menu(self.filelist)
529+ self.filelist.action_add.setVisible(False)
530+ self.filelist.action_revert.setVisible(False)
531+ self.filelist.filter_context_menu = filter_context_menu
532
533 vbox = QtGui.QVBoxLayout(groupbox)
534 vbox.addWidget(self.filelist)
535- selectall_checkbox = QtGui.QCheckBox(
536- gettext(self.filelist.SELECTALL_MESSAGE))
537+ selectall_checkbox = SelectAllCheckBox(self.filelist, groupbox)
538 selectall_checkbox.setCheckState(QtCore.Qt.Checked)
539 selectall_checkbox.setEnabled(True)
540- self.filelist.set_selectall_checkbox(selectall_checkbox)
541 vbox.addWidget(selectall_checkbox)
542
543 self.no_backup_checkbox = QtGui.QCheckBox(
544@@ -80,8 +86,6 @@
545 self.no_backup_checkbox.setEnabled(True)
546 vbox.addWidget(self.no_backup_checkbox)
547
548- self.filelist.sortItems(0, QtCore.Qt.AscendingOrder)
549-
550 # groupbox gets disabled as we are executing.
551 QtCore.QObject.connect(self,
552 QtCore.SIGNAL("subprocessStarted(bool)"),
553@@ -95,6 +99,7 @@
554 self.restoreSplitterSizes([150, 150])
555
556 layout = QtGui.QVBoxLayout(self)
557+ layout.addWidget(self.throbber)
558 layout.addWidget(self.splitter)
559
560 # Diff button to view changes in files selected to revert
561@@ -107,27 +112,32 @@
562 hbox.addWidget(self.diffbuttons)
563 hbox.addWidget(self.buttonbox)
564 layout.addLayout(hbox)
565-
566- def iter_changes_and_state(self):
567- """An iterator for the WorkingTreeFileList widget"""
568-
569- in_selected_list = closure_in_selected_list(self.initial_selected_list)
570-
571- for desc in self.tree.iter_changes(self.tree.basis_tree()):
572- desc = ChangeDesc(desc)
573- if desc.is_tree_root():
574- continue
575- path = desc.path()
576- check_state = in_selected_list(path)
577- yield desc, True, check_state
578+ self.throbber.show()
579+
580+
581+ def show(self):
582+ SubProcessDialog.show(self)
583+ QtCore.QTimer.singleShot(1, self.initial_load)
584+
585+ @runs_in_loading_queue
586+ @ui_current_widget
587+ @reports_exception()
588+ def initial_load(self):
589+ self.filelist.tree_model.checkable = True
590+ fmodel = self.filelist.tree_filter_model
591+ #fmodel.setFilter(fmodel.UNVERSIONED, False)
592+ self.filelist.set_tree(self.tree, changes_mode=True,
593+ want_unversioned=False,
594+ initial_checked_paths=self.initial_selected_list)
595+ self.throbber.hide()
596
597 def start(self):
598 """Revert the files."""
599 args = ["revert"]
600 if self.no_backup_checkbox.checkState():
601 args.append("--no-backup")
602- for desc in self.filelist.iter_checked():
603- args.append(desc.path())
604+ args.extend([ref.path
605+ for ref in self.filelist.tree_model.iter_checked()])
606 self.process_widget.start(self.tree.basedir, *args)
607
608 def saveSize(self):
609@@ -141,10 +151,7 @@
610 @param dialog_action: purpose of parent window (main action)
611 """
612 # XXX make this function universal for both qcommit and qrevert (?)
613- checked = []
614- for desc in self.filelist.iter_checked():
615- path = desc.path()
616- checked.append(path)
617+ checked = [ref.path for ref in self.filelist.iter_checked()]
618
619 if checked:
620 arg_provider = InternalWTDiffArgProvider(
621
622=== modified file 'lib/revtreeview.py'
623--- lib/revtreeview.py 2009-07-08 16:20:59 +0000
624+++ lib/revtreeview.py 2009-07-10 01:04:24 +0000
625@@ -102,6 +102,8 @@
626 break
627
628 revids = list(revids)
629+ if len(revids) == 0:
630+ return
631
632 self.load_revisions_call_count += 1
633 current_call_count = self.load_revisions_call_count
634
635=== modified file 'lib/tests/modeltest.py'
636--- lib/tests/modeltest.py 2009-07-09 01:28:49 +0000
637+++ lib/tests/modeltest.py 2009-07-10 01:54:38 +0000
638@@ -431,7 +431,8 @@
639
640 # Check that we can get back our real parent
641 p = self.model.parent( index )
642- assert( self.model.parent( index ) == parent )
643+ assert( p.internalId() == parent.internalId() )
644+ assert( p.row() == parent.row() )
645
646 # recursively go down the children
647 if self.model.hasChildren(index) and depth < 10:
648
649=== modified file 'lib/treewidget.py'
650--- lib/treewidget.py 2009-07-10 00:27:28 +0000
651+++ lib/treewidget.py 2009-07-14 03:46:05 +0000
652@@ -20,7 +20,9 @@
653 import os
654 from time import (strftime, localtime)
655 from PyQt4 import QtCore, QtGui
656+from bzrlib import errors
657 from bzrlib.workingtree import WorkingTree
658+from bzrlib.revisiontree import RevisionTree
659
660 from bzrlib.plugins.qbzr.lib.cat import QBzrCatWindow
661 from bzrlib.plugins.qbzr.lib.annotate import AnnotateWindow
662@@ -37,7 +39,6 @@
663 get_apparent_author_name,
664 )
665 from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
666-from bzrlib.plugins.qbzr.lib.wtlist import ChangeDesc
667 from bzrlib.plugins.qbzr.lib.subprocess import SimpleSubProcessDialog
668 from bzrlib.plugins.qbzr.lib.diff import (
669 show_diff,
670@@ -45,18 +46,167 @@
671 ExtDiffMenu,
672 InternalWTDiffArgProvider,
673 )
674-
675-
676-
677-class UnversionedItem():
678- __slots__ = ["name", "path", "kind"]
679- def __init__(self, name, path, kind):
680+from bzrlib.plugins.qbzr.lib.i18n import gettext, N_
681+
682+
683+class InternalItem():
684+ __slots__ = ["name", "kind", "file_id"]
685+ def __init__(self, name, kind, file_id):
686 self.name = name
687- self.path = path
688 self.kind = kind
689+ self.file_id = file_id
690
691 revision = property(lambda self:None)
692- file_id = property(lambda self:None)
693+
694+class UnversionedItem(InternalItem):
695+ def __init__(self, name, kind):
696+ InternalItem.__init__(self, name, kind, None)
697+
698+class ModelItemData():
699+ __slots__ = ["id", "item", "change", "checked", "children_ids",
700+ "parent_id", "row", "path"]
701+
702+ def __init__(self, item, change, path):
703+ self.item = item
704+ self.change = change
705+ self.path = path
706+ if change is not None and change.is_ignored() is None:
707+ self.checked = QtCore.Qt.Checked
708+ else:
709+ self.checked = QtCore.Qt.Unchecked
710+
711+ self.children_ids = None
712+ self.parent_id = None
713+ self.id = None
714+ self.row = None
715+
716+class PersistantItemReference(object):
717+ """This is use to stores a reference to a item that is persisted when we
718+ refresh the model."""
719+ __slots__ = ["file_id", "path"]
720+
721+ def __init__(self, file_id, path):
722+ self.file_id = file_id
723+ self.path = path
724+
725+class ChangeDesc(tuple):
726+ """Helper class that "knows" about internals of iter_changes' changed entry
727+ description tuple, and provides additional helper methods.
728+
729+ iter_changes return tuple with info about changed entry:
730+ [0]: file_id -> ascii string
731+ [1]: paths -> 2-tuple (old, new) fullpaths unicode/None
732+ [2]: changed_content -> bool
733+ [3]: versioned -> 2-tuple (bool, bool)
734+ [4]: parent -> 2-tuple
735+ [5]: name -> 2-tuple (old_name, new_name) utf-8?/None
736+ [6]: kind -> 2-tuple (string/None, string/None)
737+ [7]: executable -> 2-tuple (bool/None, bool/None)
738+
739+ --optional--
740+ [8]: is_ignored -> If the file is ignored, pattern which caused it to
741+ be ignored, otherwise None.
742+
743+ NOTE: None value used for non-existing entry in corresponding
744+ tree, e.g. for added/deleted/ignored/unversioned
745+ """
746+
747+ # XXX We should may be try get this into bzrlib.
748+ # XXX We should use this in qdiff.
749+
750+ def fileid(desc):
751+ return desc[0]
752+
753+ def path(desc):
754+ """Return a suitable entry for a 'specific_files' param to bzr functions."""
755+ oldpath, newpath = desc[1]
756+ return newpath or oldpath
757+
758+ def oldpath(desc):
759+ """Return oldpath for renames."""
760+ return desc[1][0]
761+
762+ def kind(desc):
763+ oldkind, newkind = desc[6]
764+ return newkind or oldkind
765+
766+ def is_versioned(desc):
767+ return desc[3] != (False, False)
768+
769+ def is_modified(desc):
770+ return (desc[3] != (False, False) and desc[2])
771+
772+ def is_renamed(desc):
773+ return (desc[3] == (True, True)
774+ and (desc[4][0], desc[5][0]) != (desc[4][1], desc[5][1]))
775+
776+ def is_tree_root(desc):
777+ """Check if entry actually tree root."""
778+ if desc[3] != (False, False) and desc[4] == (None, None):
779+ # TREE_ROOT has not parents (desc[4]).
780+ # But because we could want to see unversioned files
781+ # we need to check for versioned flag (desc[3])
782+ return True
783+ return False
784+
785+ def is_missing(desc):
786+ """Check if file was present in previous revision but now it's gone
787+ (i.e. deleted manually, without invoking `bzr remove` command)
788+ """
789+ return (desc[3] == (True, True) and desc[6][1] is None)
790+
791+ def is_misadded(desc):
792+ """Check if file was added to the working tree but then gone
793+ (i.e. deleted manually, without invoking `bzr remove` command)
794+ """
795+ return (desc[3] == (False, True) and desc[6][1] is None)
796+
797+ def is_ignored(desc):
798+ if len(desc) >= 8:
799+ return desc[8]
800+ else:
801+ return None
802+
803+ def status(desc):
804+ if len(desc) == 8:
805+ (file_id, (path_in_source, path_in_target),
806+ changed_content, versioned, parent, name, kind,
807+ executable) = desc
808+ is_ignored = None
809+ elif len(desc) == 9:
810+ (file_id, (path_in_source, path_in_target),
811+ changed_content, versioned, parent, name, kind,
812+ executable, is_ignored) = desc
813+ else:
814+ raise RuntimeError, "Unkown number of items to unpack."
815+
816+ if versioned == (False, False):
817+ if is_ignored:
818+ return gettext("ignored")
819+ else:
820+ return gettext("non-versioned")
821+ elif versioned == (False, True):
822+ return gettext("added")
823+ elif versioned == (True, False):
824+ return gettext("removed")
825+ elif kind[0] is not None and kind[1] is None:
826+ return gettext("missing")
827+ else:
828+ # versioned = True, True - so either renamed or modified
829+ # or properties changed (x-bit).
830+ renamed = (parent[0], name[0]) != (parent[1], name[1])
831+ if renamed:
832+ if changed_content:
833+ return gettext("renamed and modified")
834+ else:
835+ return gettext("renamed")
836+ elif changed_content:
837+ return gettext("modified")
838+ elif executable[0] != executable[1]:
839+ return gettext("modified (x-bit)")
840+ else:
841+ raise RuntimeError, "what status am I missing??"
842+
843
844 class TreeModel(QtCore.QAbstractItemModel):
845
846@@ -68,10 +218,6 @@
847 gettext("Status")]
848 NAME, DATE, REVNO, MESSAGE, AUTHOR, STATUS = range(len(HEADER_LABELS))
849
850- REVID = QtCore.Qt.UserRole + 1
851- FILEID = QtCore.Qt.UserRole + 2
852- PATH = QtCore.Qt.UserRole + 3
853-
854 def __init__(self, file_icon, dir_icon, symlink_icon, parent=None):
855 QtCore.QAbstractTableModel.__init__(self, parent)
856
857@@ -79,39 +225,84 @@
858 self.dir_icon = dir_icon
859 self.symlink_icon = symlink_icon
860 self.tree = None
861- self.inventory_items = []
862- self.dir_children_ids = {}
863+ self.inventory_data = []
864+ self.inventory_data_by_path = {}
865+ self.inventory_data_by_id = {} # Will not contain unversioned items.
866 self.parent_ids = []
867+ self.checkable = False
868
869- def set_tree(self, tree, branch):
870+ def set_tree(self, tree, branch=None,
871+ changes_mode=False, want_unversioned=True,
872+ initial_selected_paths=None):
873 self.tree = tree
874 self.branch = branch
875 self.revno_map = None
876+ self.changes_mode = changes_mode
877
878 self.changes = {}
879 self.unver_by_parent = {}
880-
881+ self.inventory_data_by_path = {}
882+ self.inventory_data_by_id = {}
883+
884 if isinstance(self.tree, WorkingTree):
885 tree.lock_read()
886 try:
887+ root_id = self.tree.get_root_id()
888 for change in self.tree.iter_changes(self.tree.basis_tree(),
889- want_unversioned=True):
890+ want_unversioned=want_unversioned):
891 change = ChangeDesc(change)
892 path = change.path()
893+ fileid = change.fileid()
894 is_ignored = self.tree.is_ignored(path)
895 change = ChangeDesc(change+(is_ignored,))
896
897- if change.fileid() is not None:
898+ if fileid is not None and not changes_mode:
899 self.changes[change.fileid()] = change
900 else:
901- (dir_path, slash, name) = path.rpartition('/')
902- dir_fileid = self.tree.path2id(dir_path)
903+ if changes_mode:
904+ dir_path = path
905+ dir_fileid = None
906+ relpath = ""
907+ while dir_path:
908+ (dir_path, slash, name) = dir_path.rpartition('/')
909+ relpath = slash + name + relpath
910+ if dir_path in self.inventory_data_by_path:
911+ dir_item = self.inventory_data_by_path[
912+ dir_path]
913+ dir_fileid = dir_item.item.file_id
914+ break
915+ if dir_fileid is None:
916+ dir_fileid = root_id
917+ dir_path = ""
918+
919+ name = relpath.lstrip("/")
920+ if change.is_renamed():
921+ oldpath = change.oldpath()
922+ if oldpath.startswith(dir_path):
923+ oldpath = oldpath[len(dir_path):]
924+ else:
925+ # The file was mv from a difirent path.
926+ oldpath = '/' + oldpath
927+ name = "%s => %s" % (oldpath, name)
928+ else:
929+ (dir_path, slash, name) = path.rpartition('/')
930+ dir_fileid = self.tree.path2id(dir_path)
931+
932+ if change.is_versioned():
933+ if changes_mode:
934+ item = InternalItem(name, change.kind(),
935+ change.fileid())
936+ else:
937+ item = self.tree.inventory[change.fileid()]
938+ else:
939+ item = UnversionedItem(name, change.kind())
940+
941+ item_data = ModelItemData(item, change, path)
942
943 if dir_fileid not in self.unver_by_parent:
944 self.unver_by_parent[dir_fileid] = []
945- self.unver_by_parent[dir_fileid].append((
946- UnversionedItem(name, path, change.kind()),
947- change))
948+ self.unver_by_parent[dir_fileid].append(item_data)
949+ self.inventory_data_by_path[path] = item_data
950
951 self.process_inventory(self.working_tree_get_children)
952 finally:
953@@ -119,20 +310,22 @@
954 else:
955 self.process_inventory(self.revision_tree_get_children)
956
957- def revision_tree_get_children(self, item):
958- for child in item.children.itervalues():
959- yield (child, None)
960+ def revision_tree_get_children(self, item_data):
961+ for child in item_data.item.children.itervalues():
962+ path = self.tree.id2path(child.file_id)
963+ yield ModelItemData(child, None, path)
964
965- def working_tree_get_children(self, item):
966+ def working_tree_get_children(self, item_data):
967+ item = item_data.item
968 if isinstance(item, UnversionedItem):
969- abspath = self.tree.abspath(item.path)
970+ abspath = self.tree.abspath(item_data.path)
971
972 for name in os.listdir(abspath):
973- path = item.path+"/"+name
974+ path = item_data.path+"/"+name
975 (kind,
976 executable,
977 stat_value) = self.tree._comparison_data(None, path)
978- child = UnversionedItem(name, path, kind)
979+ child = UnversionedItem(name, kind)
980 is_ignored = self.tree.is_ignored(path)
981 change = ChangeDesc((None,
982 (None, path),
983@@ -143,9 +336,10 @@
984 (None, kind),
985 (None, executable),
986 is_ignored))
987- yield (child, change)
988+ yield ModelItemData(child, change, path)
989
990- elif item.children is not None:
991+ if (not isinstance(item, InternalItem) and
992+ item.children is not None and not self.changes_mode):
993 #Because we create copies, we have to get the real item.
994 item = self.tree.inventory[item.file_id]
995 for child in item.children.itervalues():
996@@ -155,35 +349,36 @@
997 change = self.changes[child.file_id]
998 else:
999 change = None
1000- yield (child, change)
1001+ path = self.tree.id2path(child.file_id)
1002+ yield ModelItemData(child, change, path)
1003 if item.file_id in self.unver_by_parent:
1004- for (child, change) in self.unver_by_parent[item.file_id]:
1005- yield (child, change)
1006+ for item_data in self.unver_by_parent[item.file_id]:
1007+ yield item_data
1008
1009 def load_dir(self, dir_id):
1010+ if dir_id>=len(self.inventory_data):
1011+ return
1012+ dir_item = self.inventory_data[dir_id]
1013+ if dir_item.children_ids is not None:
1014+ return
1015+
1016 if isinstance(self.tree, WorkingTree):
1017 self.tree.lock_read()
1018 try:
1019- if dir_id>=len(self.inventory_items):
1020- return
1021- dir_item, dir_change = self.inventory_items[dir_id]
1022- dir_children_ids = []
1023+ dir_item.children_ids = []
1024 children = sorted(self.get_children(dir_item),
1025 self.inventory_dirs_first_cmp,
1026- lambda x: x[0])
1027+ lambda x: (x.item.name, x.item.kind))
1028
1029 parent_model_index = self._index_from_id(dir_id, 0)
1030 self.beginInsertRows(parent_model_index, 0, len(children)-1)
1031 try:
1032- for (child, change) in children:
1033- child_id = self.append_item(child, change, dir_id)
1034- dir_children_ids.append(child_id)
1035- if child.kind == "directory":
1036- self.dir_children_ids[child_id] = None
1037+ for child in children:
1038+ child_id = self.append_item(child, dir_id)
1039+ dir_item.children_ids.append(child_id)
1040
1041- if len(dir_children_ids) % 100 == 0:
1042+ if len(dir_item.children_ids) % 100 == 0:
1043 QtCore.QCoreApplication.processEvents()
1044- self.dir_children_ids[dir_id] = dir_children_ids
1045 finally:
1046 self.endInsertRows();
1047 finally:
1048@@ -194,37 +389,72 @@
1049 self.get_children = get_children
1050
1051 self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
1052- self.inventory_items = []
1053- self.dir_children_ids = {}
1054+
1055+ is_refresh = len(self.inventory_data)>0
1056+ if is_refresh:
1057+ self.beginRemoveRows(QtCore.QModelIndex(), 0,
1058+ len(self.inventory_data[0].children_ids)-1)
1059+ self.inventory_data = []
1060 self.parent_ids = []
1061+ if is_refresh:
1062+ self.endRemoveRows()
1063+
1064+ root_item = ModelItemData(self.tree.inventory[self.tree.get_root_id()],
1065+ None, '')
1066+ root_id = self.append_item(root_item, None)
1067+ self.load_dir(root_id)
1068 self.emit(QtCore.SIGNAL("layoutChanged()"))
1069-
1070- root_item = self.tree.inventory[self.tree.get_root_id()]
1071- root_id = self.append_item(root_item, None, None)
1072- self.dir_children_ids[root_id] = None
1073- self.load_dir(root_id)
1074
1075- def append_item(self, item, change, parent_id):
1076- id = len(self.inventory_items)
1077- self.inventory_items.append((item, change))
1078- self.parent_ids.append(parent_id)
1079- return id
1080+ def append_item(self, item_data, parent_id):
1081+ item_data.id = len(self.inventory_data)
1082+ if parent_id is not None:
1083+ parent_data = self.inventory_data[parent_id]
1084+ if self.is_item_in_select_all(item_data):
1085+ item_data.checked = parent_data.checked
1086+ else:
1087+ item_data.checked = False
1088+ item_data.row = len(parent_data.children_ids)
1089+ else:
1090+ item_data.checked = QtCore.Qt.Checked
1091+ item_data.row = 0
1092+ item_data.parent_id = parent_id
1093+ self.inventory_data.append(item_data)
1094+ self.inventory_data_by_path[item_data.path] = item_data
1095+ if not isinstance(item_data.item, UnversionedItem):
1096+ self.inventory_data_by_id[item_data.item.file_id] = item_data
1097+ return item_data.id
1098
1099 def inventory_dirs_first_cmp(self, x, y):
1100- x_is_dir = x.kind =="directory"
1101- y_is_dir = y.kind =="directory"
1102+ (x_name, x_kind) = x
1103+ (y_name, y_kind) = y
1104+ x_a = x_name
1105+ y_a = y_name
1106+ x_is_dir = x_kind =="directory"
1107+ y_is_dir = y_kind =="directory"
1108+ while True:
1109+ x_b, sep, x_a_t = x_a.partition("/")
1110+ y_b, sep, y_a_t = y_a.partition("/")
1111+ if x_a_t == "" and y_a_t == "":
1112+ break
1113+ if (x_is_dir or not x_a_t == "") and not (y_is_dir or not y_a_t == ""):
1114+ return -1
1115+ if (y_is_dir or not y_a_t == "") and not (x_is_dir or not x_a_t == ""):
1116+ return 1
1117+ cmp_r = cmp(x_b, y_b)
1118+ if not cmp_r == 0:
1119+ return cmp_r
1120+ x_a = x_a_t
1121+ y_a = y_a_t
1122 if x_is_dir and not y_is_dir:
1123 return -1
1124 if y_is_dir and not x_is_dir:
1125 return 1
1126- return cmp(x.name, y.name)
1127+ return cmp(x_name, y_name)
1128
1129 def set_revno_map(self, revno_map):
1130 self.revno_map = revno_map
1131- for id in xrange(1, len(self.inventory_items)):
1132- parent_id = self.parent_ids[id]
1133- row = self.dir_children_ids[parent_id].index(id)
1134- index = self.createIndex (row, self.REVNO, id)
1135+ for item_data in self.inventory_data[1:0]:
1136+ index = self.createIndex (item_data.row, self.REVNO, item_data.id)
1137 self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
1138 index,index)
1139
1140@@ -232,43 +462,48 @@
1141 return len(self.HEADER_LABELS)
1142
1143 def rowCount(self, parent):
1144- parent_id = parent.internalId()
1145- if parent_id not in self.dir_children_ids:
1146- return 0
1147- dir_children_ids = self.dir_children_ids[parent_id]
1148- if dir_children_ids is None:
1149- return 0
1150- return len(dir_children_ids)
1151+ if parent.internalId()>=len(self.inventory_data):
1152+ return 0
1153+ parent_data = self.inventory_data[parent.internalId()]
1154+ if parent_data.children_ids is None:
1155+ return 0
1156+ return len(parent_data.children_ids)
1157
1158 def canFetchMore(self, parent):
1159- parent_id = parent.internalId()
1160- return parent_id in self.dir_children_ids \
1161- and self.dir_children_ids[parent_id] is None
1162+ if parent.internalId()>=len(self.inventory_data):
1163+ return False
1164+ parent_data = self.inventory_data[parent.internalId()]
1165+ return (parent_data.children_ids is None and
1166+ parent_data.item.kind == "directory")
1167
1168 def fetchMore(self, parent):
1169 self.load_dir(parent.internalId())
1170
1171 def _index(self, row, column, parent_id):
1172- if parent_id not in self.dir_children_ids:
1173- return QtCore.QModelIndex()
1174- dir_children_ids = self.dir_children_ids[parent_id]
1175- if dir_children_ids is None:
1176- return QtCore.QModelIndex()
1177- if row >= len(dir_children_ids):
1178- return QtCore.QModelIndex()
1179- item_id = dir_children_ids[row]
1180+ if parent_id>=len(self.inventory_data):
1181+ return QtCore.QModelIndex()
1182+
1183+ parent_data = self.inventory_data[parent_id]
1184+ if parent_data.children_ids is None:
1185+ return QtCore.QModelIndex()
1186+
1187+ if (row < 0 or
1188+ row >= len(parent_data.children_ids) or
1189+ column < 0 or
1190+ column >= len(self.HEADER_LABELS)):
1191+ return QtCore.QModelIndex()
1192+ item_id = parent_data.children_ids[row]
1193 return self.createIndex(row, column, item_id)
1194
1195 def _index_from_id(self, item_id, column):
1196- parent_id = self.parent_ids[item_id]
1197- if parent_id is None:
1198+ if item_id >= len(self.inventory_data):
1199 return QtCore.QModelIndex()
1200- row = self.dir_children_ids[parent_id].index(item_id)
1201- return self.createIndex(row, column, item_id)
1202+
1203+ item_data = self.inventory_data[item_id]
1204+ return self.createIndex(item_data.row, column, item_id)
1205
1206 def index(self, row, column, parent = QtCore.QModelIndex()):
1207- parent_id = parent.internalId()
1208- return self._index(row, column, parent_id)
1209+ return self._index(row, column, parent.internalId())
1210
1211 def sibling(self, row, column, index):
1212 sibling_id = index.internalId()
1213@@ -281,28 +516,109 @@
1214 child_id = child.internalId()
1215 if child_id == 0:
1216 return QtCore.QModelIndex()
1217- if child_id not in self.parent_ids:
1218- return QtCore.QModelIndex()
1219- item_id = self.parent_ids[child_id]
1220- if item_id == 0 :
1221+ item_data = self.inventory_data[child_id]
1222+ if item_data.parent_id == 0 :
1223 return QtCore.QModelIndex()
1224
1225- return self._index_from_id(item_id, 0)
1226+ return self._index_from_id(item_data.parent_id, 0)
1227
1228 def hasChildren(self, parent):
1229- parent_id = parent.internalId()
1230- return parent_id in self.dir_children_ids
1231+ if self.tree is None:
1232+ return False
1233+ if parent.internalId() == 0:
1234+ return True
1235+ item_data = self.inventory_data[parent.internalId()]
1236+ return item_data.item.kind == "directory"
1237+
1238+ is_item_in_select_all = lambda self, item: True
1239+ """Returns wether an item is changed when select all is clicked."""
1240+
1241+ def setData(self, index, value, role):
1242+
1243+
1244+ def set_checked(item_data, checked):
1245+ old_checked = item_data.checked
1246+ item_data.checked = checked
1247+ if not old_checked == checked:
1248+ index = self.createIndex (item_data.row, self.NAME, item_data.id)
1249+ self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
1250+ index,index)
1251+
1252+ if index.column() == self.NAME and role == QtCore.Qt.CheckStateRole:
1253+ value = value.toInt()[0]
1254+ if index.internalId() >= len(self.inventory_data):
1255+ return False
1256+
1257+ item_data = self.inventory_data[index.internalId()]
1258+ set_checked(item_data, value)
1259+
1260+ # Recursively set all children to checked.
1261+ if item_data.children_ids is not None:
1262+ children_ids = list(item_data.children_ids)
1263+ while children_ids:
1264+ child = self.inventory_data[children_ids.pop(0)]
1265+
1266+ # If unchecking, uncheck everything, but if checking,
1267+ # only check those in "select_all" get checked.
1268+ if (self.is_item_in_select_all(child) or
1269+ value == QtCore.Qt.Unchecked):
1270+
1271+ set_checked(child, value)
1272+ if child.children_ids is not None:
1273+ children_ids.extend(child.children_ids)
1274+
1275+ # Walk up the tree, and update every dir
1276+ parent_data = item_data
1277+ while parent_data.parent_id is not None:
1278+ parent_data = self.inventory_data[parent_data.parent_id]
1279+ has_checked = False
1280+ has_unchecked = False
1281+ for child_id in parent_data.children_ids:
1282+ child = self.inventory_data[child_id]
1283+
1284+ if child.checked == QtCore.Qt.Checked:
1285+ has_checked = True
1286+ elif (child.checked == QtCore.Qt.Unchecked and
1287+ self.is_item_in_select_all(child)):
1288+ has_unchecked = True
1289+ elif child.checked == QtCore.Qt.PartiallyChecked:
1290+ has_checked = True
1291+ if self.is_item_in_select_all(child):
1292+ has_unchecked = True
1293+
1294+ if has_checked and has_unchecked:
1295+ break
1296+
1297+ if has_checked and has_unchecked:
1298+ set_checked(parent_data, QtCore.Qt.PartiallyChecked)
1299+ elif has_checked:
1300+ set_checked(parent_data, QtCore.Qt.Checked)
1301+ else:
1302+ set_checked(parent_data, QtCore.Qt.Unchecked)
1303+
1304+ return True
1305+
1306+ return False
1307+
1308+ REVID = QtCore.Qt.UserRole + 1
1309+ FILEID = QtCore.Qt.UserRole + 2
1310+ PATH = QtCore.Qt.UserRole + 3
1311+ INTERNALID = QtCore.Qt.UserRole + 4
1312
1313 def data(self, index, role):
1314 if not index.isValid():
1315 return QtCore.QVariant()
1316
1317- (item, change) = self.inventory_items[index.internalId()]
1318+ if role == self.INTERNALID:
1319+ return QtCore.QVariant(index.internalId())
1320+
1321+ item_data = self.inventory_data[index.internalId()]
1322+ item = item_data.item
1323
1324 if role == self.FILEID:
1325 return QtCore.QVariant(item.file_id)
1326
1327- revid = item.revision
1328+ revid = item_data.item.revision
1329 if role == self.REVID:
1330 if revid is None:
1331 return QtCore.QVariant()
1332@@ -321,13 +637,18 @@
1333 if item.kind == "symlink":
1334 return QtCore.QVariant(self.symlink_icon)
1335 return QtCore.QVariant()
1336+ if role == QtCore.Qt.CheckStateRole:
1337+ if not self.checkable:
1338+ return QtCore.QVariant()
1339+ else:
1340+ return QtCore.QVariant(item_data.checked)
1341
1342 if column == self.STATUS:
1343 if role == QtCore.Qt.DisplayRole:
1344- if change is not None:
1345- return QtCore.QVariant(change.status())
1346+ if item_data.change is not None:
1347+ return QtCore.QVariant(item_data.change.status())
1348 else:
1349- return QtCore.QVariant()
1350+ return QtCore.QVariant("")
1351
1352 if column == self.REVNO:
1353 if role == QtCore.Qt.DisplayRole:
1354@@ -336,7 +657,7 @@
1355 return QtCore.QVariant(
1356 ".".join(["%d" % (revno) for revno in revno_sequence]))
1357 else:
1358- return QtCore.QVariant()
1359+ return QtCore.QVariant("")
1360
1361 if role == QtCore.Qt.DisplayRole:
1362 if revid in cached_revisions:
1363@@ -353,25 +674,23 @@
1364 localtime(rev.timestamp)))
1365
1366 if role == self.PATH:
1367- if isinstance(item, UnversionedItem):
1368- path = item.path
1369- else:
1370- if isinstance(self.tree, WorkingTree):
1371- self.tree.lock_read()
1372- try:
1373- path = self.tree.id2path(item.file_id)
1374- finally:
1375- if isinstance(self.tree, WorkingTree):
1376- self.tree.unlock()
1377- return QtCore.QVariant(path)
1378+ return QtCore.QVariant(item_data.path)
1379
1380+ if role == QtCore.Qt.DisplayRole:
1381+ return QtCore.QVariant("")
1382 return QtCore.QVariant()
1383
1384 def flags(self, index):
1385- if not index.isValid():
1386- return QtCore.Qt.ItemIsEnabled
1387+ #if not index.isValid():
1388+ # return QtCore.Qt.ItemIsEnabled
1389
1390- return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
1391+ if self.checkable and index.column() == self.NAME:
1392+ return (QtCore.Qt.ItemIsEnabled |
1393+ QtCore.Qt.ItemIsSelectable |
1394+ QtCore.Qt.ItemIsUserCheckable)
1395+ else:
1396+ return (QtCore.Qt.ItemIsEnabled |
1397+ QtCore.Qt.ItemIsSelectable)
1398
1399 def headerData(self, section, orientation, role):
1400 if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
1401@@ -380,19 +699,102 @@
1402
1403 def on_revisions_loaded(self, revisions, last_call):
1404 inventory = self.tree.inventory
1405- for id, (item, change) in enumerate(self.inventory_items):
1406- if id == 0:
1407+ for item_data in self.inventory_data:
1408+ if item_data.id == 0:
1409 continue
1410
1411- if item.revision in revisions:
1412- parent_id = self.parent_ids[id]
1413- row = self.dir_children_ids[parent_id].index(id)
1414+ if item_data.item.revision in revisions:
1415 self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
1416- self.createIndex (row, self.DATE, id),
1417- self.createIndex (row, self.AUTHOR,id))
1418+ self.createIndex (item_data.row, self.DATE, item_data.id),
1419+ self.createIndex (item_data.row, self.AUTHOR, item_data.id))
1420
1421 def get_repo(self):
1422 return self.branch.repository
1423+
1424+ def _item2ref(self, item_data):
1425+ if isinstance(item_data.item, UnversionedItem):
1426+ file_id = None
1427+ else:
1428+ file_id = item_data.item.file_id
1429+ return PersistantItemReference(file_id, item_data.path)
1430+
1431+ def index2ref(self, index):
1432+ item_data = self.inventory_data[index.internalId()]
1433+ return self._item2ref(item_data)
1434+
1435+ def indexes2refs(self, indexes):
1436+ refs = []
1437+ for index in indexes:
1438+ refs.append(self.index2ref(index))
1439+ return refs
1440+
1441+ def ref2index(self, ref):
1442+ if ref.file_id is not None:
1443+ key = ref.file_id
1444+ dict = self.inventory_data_by_id
1445+ def iter_parents():
1446+ parent_id = ref.file_id
1447+ parent_ids = []
1448+ while parent_id is not None:
1449+ parent_id = self.tree.inventory[parent_id].parent_id
1450+ parent_ids.append(parent_id)
1451+ return reversed(parent_ids)
1452+ else:
1453+ key = ref.path
1454+ dict = self.inventory_data_by_path
1455+ def iter_parents():
1456+ path_split = ref.path.split("/")
1457+ parent_dir_path = None
1458+ for parent_name in ref.path.split("/")[:-1]:
1459+ if parent_dir_path is None:\
1460+ parent_dir_path = parent_name
1461+ else:
1462+ parent_dir_path += "/" + parent_name
1463+ yield parent_dir_path
1464+
1465+ if key not in dict:
1466+ # Try loading the parents
1467+ for parent_key in iter_parents():
1468+ if parent_key not in dict:
1469+ break
1470+ self.load_dir(dict[parent_key].id)
1471+
1472+ if key not in dict:
1473+ raise errors.NoSuchFile(ref.path)
1474+
1475+ return self._index_from_id(dict[key].id, self.NAME)
1476+
1477+ def refs2indexes(self, refs, ignore_no_file_error=False):
1478+ indexes = []
1479+ for ref in refs:
1480+ try:
1481+ indexes.append(self.ref2index(ref))
1482+ except (errors.NoSuchId, errors.NoSuchFile):
1483+ if not ignore_no_file_error:
1484+ raise
1485+ return indexes
1486+
1487+ def iter_checked(self):
1488+ return [self._item2ref(item_data)
1489+ for item_data in sorted(
1490+ [item_data for item_data in self.inventory_data[1:]
1491+ if item_data.checked == QtCore.Qt.Checked],
1492+ self.inventory_dirs_first_cmp,
1493+ lambda x: (x.change.path(), x.item.kind))]
1494+
1495+ def set_checked_items(self, refs, ignore_no_file_error=False):
1496+ # set every thing off
1497+ root_index = self._index_from_id(0, self.NAME)
1498+ self.setData(root_index, QtCore.QVariant(QtCore.Qt.Unchecked),
1499+ QtCore.Qt.CheckStateRole)
1500+
1501+ for index in self.refs2indexes(refs, ignore_no_file_error):
1502+ self.setData(index, QtCore.QVariant(QtCore.Qt.Checked),
1503+ QtCore.Qt.CheckStateRole)
1504+
1505+ def set_checked_paths(self, paths):
1506+ return self.set_checked_items([PersistantItemReference(None, path)
1507+ for path in paths])
1508
1509 class TreeFilterProxyModel(QtGui.QSortFilterProxyModel):
1510 source_model = None
1511@@ -430,7 +832,13 @@
1512
1513 model = self.source_model
1514 parent_id = source_parent.internalId()
1515- id = model.dir_children_ids[parent_id][source_row]
1516+ children_ids = model.inventory_data[parent_id].children_ids
1517+ if len(children_ids)<=source_row:
1518+ return False
1519+ id = children_ids[source_row]
1520+ if (model.checkable and
1521+ not model.inventory_data[id].checked == QtCore.Qt.Unchecked):
1522+ return True
1523 return self.filter_id_cached(id)
1524
1525 def filter_id_cached(self, id):
1526@@ -445,27 +853,26 @@
1527 (unchanged, changed, unversioned, ignored) = self.filters
1528
1529 model = self.source_model
1530- item, change = model.inventory_items[id]
1531-
1532- if change is None and unchanged: return True
1533-
1534- is_versioned = not isinstance(item, UnversionedItem)
1535-
1536- if is_versioned and change is not None and changed: return True
1537+ item_data = model.inventory_data[id]
1538+
1539+ if item_data.change is None and unchanged: return True
1540+
1541+ is_versioned = not isinstance(item_data.item, UnversionedItem)
1542+
1543+ if is_versioned and item_data.change is not None and changed:
1544+ return True
1545
1546 if not is_versioned and unversioned:
1547- is_ignored = change.is_ignored()
1548+ is_ignored = item_data.change.is_ignored()
1549 if not is_ignored: return True
1550 if is_ignored and ignored: return True
1551 if is_ignored and not ignored: return False
1552
1553- if id in model.dir_children_ids:
1554- dir_children_ids = model.dir_children_ids[id]
1555- if dir_children_ids is None:
1556+ if item_data.item.kind == "directory":
1557+ if item_data.children_ids is None:
1558 model.load_dir(id)
1559- dir_children_ids = model.dir_children_ids[id]
1560
1561- for child_id in dir_children_ids:
1562+ for child_id in item_data.children_ids:
1563 if self.filter_id_cached(child_id):
1564 return True
1565
1566@@ -540,7 +947,6 @@
1567 self.connect(self,
1568 QtCore.SIGNAL("doubleClicked(QModelIndex)"),
1569 self.do_default_action)
1570- self.default_action = self.show_file_content
1571 self.tree = None
1572 self.branch = None
1573
1574@@ -552,7 +958,7 @@
1575 header.setResizeMode(self.tree_model.REVNO, QtGui.QHeaderView.Interactive)
1576 header.setResizeMode(self.tree_model.MESSAGE, QtGui.QHeaderView.Stretch)
1577 header.setResizeMode(self.tree_model.AUTHOR, QtGui.QHeaderView.Interactive)
1578- header.setResizeMode(self.tree_model.STATUS, QtGui.QHeaderView.ResizeToContents)
1579+ header.setResizeMode(self.tree_model.STATUS, QtGui.QHeaderView.Stretch)
1580 fm = self.fontMetrics()
1581 # XXX Make this dynamic.
1582 col_margin = 6
1583@@ -596,10 +1002,41 @@
1584 gettext("&Revert"),
1585 self.revert)
1586
1587- def set_tree(self, tree, branch):
1588+ def set_tree(self, tree, branch=None,
1589+ changes_mode=False, want_unversioned=True,
1590+ initial_checked_paths=None):
1591+ """Causes a tree to be loaded, and displayed in the widget.
1592+
1593+ @param changes_mode: If in changes mode, a list of changes, and
1594+ unversioned items, rather than a tree, is diplayed.
1595+ e.g., when not in changes mode, one will get:
1596+
1597+ dir1
1598+ file1 changed
1599+ file2 changed
1600+
1601+ but when in changes mode, one will get:
1602+
1603+ dir1/file1 changed
1604+ file2 changed
1605+
1606+ When in changes mode, no unchanged items are shown.
1607+ """
1608 self.tree = tree
1609+ if isinstance(tree, RevisionTree) and branch is None:
1610+ raise AttributeError("A branch must be provided if the tree is a "
1611+ "RevisionTree")
1612 self.branch = branch
1613- self.tree_model.set_tree(self.tree, self.branch)
1614+ self.changes_mode = changes_mode
1615+ self.want_unversioned = want_unversioned
1616+ self.tree_model.set_tree(self.tree, self.branch,
1617+ changes_mode, want_unversioned)
1618+ if initial_checked_paths is not None and not self.tree_model.checkable:
1619+ raise AttributeError("You can't have a initial_selection if "
1620+ "tree_model.checkable is not True.")
1621+ if initial_checked_paths is not None:
1622+ self.tree_model.set_checked_paths(initial_checked_paths)
1623+
1624 self.tree_filter_model.invalidateFilter()
1625
1626 if str(QtCore.QT_VERSION_STR).startswith("4.4"):
1627@@ -623,7 +1060,6 @@
1628 header.showSection(self.tree_model.STATUS)
1629
1630 self.context_menu.setDefaultAction(self.action_open_file)
1631- self.default_action = self.open_file
1632 else:
1633 header.showSection(self.tree_model.DATE)
1634 header.showSection(self.tree_model.REVNO)
1635@@ -632,11 +1068,22 @@
1636 header.hideSection(self.tree_model.STATUS)
1637
1638 self.context_menu.setDefaultAction(self.action_show_file)
1639- self.default_action = self.show_file_content
1640
1641 def refresh(self):
1642- self.tree_model.set_tree(self.tree, self.branch)
1643- self.tree_filter_model.invalidateFilter()
1644+ self.tree.lock_read()
1645+ try:
1646+ if self.tree_model.checkable:
1647+ checked = list(self.tree_model.iter_checked())
1648+
1649+ self.tree_model.set_tree(self.tree, self.branch,
1650+ self.changes_mode, self.want_unversioned)
1651+ self.tree_filter_model.invalidateFilter()
1652+
1653+ if self.tree_model.checkable:
1654+ self.tree_model.set_checked_items(checked,
1655+ ignore_no_file_error=True)
1656+ finally:
1657+ self.tree.unlock()
1658
1659 def contextMenuEvent(self, event):
1660 self.filter_context_menu()
1661@@ -644,35 +1091,38 @@
1662 event.accept()
1663
1664 def get_selection_indexes(self, indexes=None):
1665- if indexes == None:
1666+ if indexes is None or (len(indexes) == 1 and indexes[0] is None):
1667 indexes = self.selectedIndexes()
1668 rows = {}
1669- for index in self.selectedIndexes():
1670- if index.row() not in rows:
1671- rows[index.internalId()] = index
1672+ for index in indexes:
1673+ internal_id = index.data(self.tree_model.INTERNALID).toInt()[0]
1674+ if internal_id not in rows:
1675+ rows[internal_id] = index
1676 return rows.values()
1677
1678 def get_selection_items(self, indexes=None):
1679 items = []
1680- if indexes is None:
1681+ if indexes is None or len(indexes)==1 and indexes[0] is None:
1682 indexes = self.get_selection_indexes(indexes)
1683 for index in indexes:
1684 source_index = self.tree_filter_model.mapToSource(index)
1685- items.append(self.tree_model.inventory_items[
1686+ items.append(self.tree_model.inventory_data[
1687 source_index.internalId()])
1688 return items
1689
1690 def filter_context_menu(self):
1691 is_working_tree = isinstance(self.tree, WorkingTree)
1692 items = self.get_selection_items()
1693- versioned = [not isinstance(item[0], UnversionedItem)
1694+ versioned = [not isinstance(item.item, UnversionedItem)
1695 for item in items]
1696- changed = [item[1] is not None
1697+ changed = [item.change is not None
1698 for item in items]
1699+ versioned_changed = [ver and ch for ver,ch in zip(versioned, changed)]
1700+
1701 selection_len = len(items)
1702
1703 single_versioned_file = (selection_len == 1 and versioned[0] and
1704- items[0][0].kind == "file")
1705+ items[0].item.kind == "file")
1706
1707 self.action_open_file.setEnabled(is_working_tree)
1708 self.action_open_file.setVisible(is_working_tree)
1709@@ -680,24 +1130,37 @@
1710 self.action_show_annotate.setEnabled(single_versioned_file)
1711 self.action_show_log.setEnabled(any(versioned))
1712 self.action_show_diff.setVisible(is_working_tree)
1713- self.action_show_diff.setEnabled(any(changed))
1714+ self.action_show_diff.setEnabled(any(versioned_changed))
1715
1716 self.action_add.setVisible(is_working_tree)
1717 self.action_add.setDisabled(all(versioned))
1718 self.action_revert.setVisible(is_working_tree)
1719- self.action_revert.setEnabled(any(changed) and any(versioned))
1720+ self.action_revert.setEnabled(any(versioned_changed))
1721+
1722+ if is_working_tree:
1723+ if any(versioned_changed):
1724+ self.context_menu.setDefaultAction(self.action_show_diff)
1725+ else:
1726+ self.context_menu.setDefaultAction(self.action_open_file)
1727+
1728
1729- def do_default_action(self, index=None):
1730+ def do_default_action(self, index):
1731 indexes = self.get_selection_indexes([index])
1732 if not len(indexes) == 1:
1733 return
1734
1735- item = self.get_selection_items(indexes)[0]
1736- if item[0].kind == "directory":
1737+ item_data = self.get_selection_items(indexes)[0]
1738+ if item_data.item.kind == "directory":
1739 # Don't do anything, so that the directory can be expanded.
1740 return
1741
1742- self.default_action(index)
1743+ if isinstance(self.tree, WorkingTree):
1744+ if item_data.change is not None and item_data.change.is_versioned():
1745+ self.show_differences(index=index)
1746+ else:
1747+ self.open_file(index)
1748+ else:
1749+ self.show_file_content(index)
1750
1751 @ui_current_widget
1752 def show_file_content(self, index=None):
1753@@ -708,7 +1171,7 @@
1754 return
1755
1756 item = self.get_selection_items(indexes)[0]
1757- if isinstance(item[0], UnversionedItem):
1758+ if isinstance(item.item, UnversionedItem):
1759 return
1760
1761 index = indexes[0]
1762@@ -748,8 +1211,8 @@
1763 """Show qlog for one selected file(s)."""
1764
1765 items = self.get_selection_items()
1766- fileids = [item[0].file_id for item in items
1767- if item[0].file_id is not None]
1768+ fileids = [item.item.file_id for item in items
1769+ if item.item.file_id is not None]
1770
1771 window = LogWindow(None, self.branch, fileids)
1772 window.show()
1773@@ -767,19 +1230,15 @@
1774 self.window().windows.append(window)
1775
1776 @ui_current_widget
1777- def show_differences(self, ext_diff=None):
1778+ def show_differences(self, ext_diff=None, index=None):
1779 """Show differences for selected file(s)."""
1780
1781- items = self.get_selection_items()
1782+ items = self.get_selection_items([index])
1783 if len(items) > 0:
1784- self.tree.lock_read()
1785- try:
1786- # Only paths that have changes.
1787- paths = [self.tree.id2path(item[0].file_id)
1788- for item in items
1789- if item[1] is not None]
1790- finally:
1791- self.tree.unlock()
1792+ # Only paths that have changes.
1793+ paths = [item.path
1794+ for item in items
1795+ if item.change is not None]
1796 else:
1797 # Show all.
1798 paths = None
1799@@ -800,9 +1259,9 @@
1800 items = self.get_selection_items()
1801
1802 # Only paths that are not versioned.
1803- paths = [item[0].path
1804+ paths = [item.item.path
1805 for item in items
1806- if isinstance(item[0], UnversionedItem)]
1807+ if isinstance(item.item, UnversionedItem)]
1808
1809 if len(paths) == 0:
1810 return
1811@@ -829,10 +1288,10 @@
1812 self.tree.lock_read()
1813 try:
1814 # Only paths that have changes.
1815- paths = [self.tree.id2path(item[0].file_id)
1816+ paths = [item.path
1817 for item in items
1818- if item[1] is not None and
1819- not isinstance(item[0], UnversionedItem)]
1820+ if item.change is not None and
1821+ not isinstance(item.item, UnversionedItem)]
1822 finally:
1823 self.tree.unlock()
1824
1825@@ -851,4 +1310,39 @@
1826 res = revert_dialog.exec_()
1827 if res == QtGui.QDialog.Accepted:
1828 self.refresh()
1829-
1830\ No newline at end of file
1831+
1832+class SelectAllCheckBox(QtGui.QCheckBox):
1833+
1834+ def __init__(self, tree_widget, parent=None):
1835+ QtGui.QCheckBox.__init__(self, gettext("Select / deselect all"), parent)
1836+
1837+ self.tree_widget = tree_widget
1838+ #self.setTristate(True)
1839+
1840+ self.connect(tree_widget.tree_model,
1841+ QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
1842+ self.on_data_changed)
1843+
1844+ self.connect(self, QtCore.SIGNAL("clicked(bool)"),
1845+ self.clicked)
1846+
1847+ def on_data_changed(self, start_index, end_index):
1848+ self.update_state()
1849+
1850+ def update_state(self):
1851+ model = self.tree_widget.tree_model
1852+ root_index = model._index_from_id(0, model.NAME)
1853+
1854+ state = model.data(root_index, QtCore.Qt.CheckStateRole)
1855+ self.setCheckState(state.toInt()[0])
1856+
1857+ def clicked(self, state):
1858+ model = self.tree_widget.tree_model
1859+ root_index = model._index_from_id(0, model.NAME)
1860+ if state:
1861+ state = QtCore.QVariant(QtCore.Qt.Checked)
1862+ else:
1863+ state = QtCore.QVariant(QtCore.Qt.Unchecked)
1864+
1865+ model.setData(root_index, QtCore.QVariant(state),
1866+ QtCore.Qt.CheckStateRole)
1867
1868=== removed file 'lib/wtlist.py'
1869--- lib/wtlist.py 2009-06-18 03:44:48 +0000
1870+++ lib/wtlist.py 1970-01-01 00:00:00 +0000
1871@@ -1,449 +0,0 @@
1872-# -*- coding: utf-8 -*-
1873-#
1874-# QBzr - Qt frontend to Bazaar commands
1875-# Copyright (C) 2006-2007 Lukáš Lalinský <lalinsky@gmail.com>
1876-# Copyright (C) 2006 Trolltech ASA
1877-# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
1878-#
1879-# This program is free software; you can redistribute it and/or
1880-# modify it under the terms of the GNU General Public License
1881-# as published by the Free Software Foundation; either version 2
1882-# of the License, or (at your option) any later version.
1883-#
1884-# This program is distributed in the hope that it will be useful,
1885-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1886-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1887-# GNU General Public License for more details.
1888-#
1889-# You should have received a copy of the GNU General Public License
1890-# along with this program; if not, write to the Free Software
1891-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1892-
1893-"""A QTreeWidget that shows the items in a working tree, and includes a common
1894-context menu."""
1895-
1896-from PyQt4 import QtCore, QtGui
1897-
1898-from bzrlib import osutils
1899-
1900-from bzrlib.plugins.qbzr.lib.diff import (
1901- show_diff,
1902- has_ext_diff,
1903- ExtDiffMenu,
1904- InternalWTDiffArgProvider,
1905- )
1906-from bzrlib.plugins.qbzr.lib.i18n import gettext, N_
1907-from bzrlib.plugins.qbzr.lib.subprocess import SimpleSubProcessDialog
1908-from bzrlib.plugins.qbzr.lib.util import (
1909- file_extension,
1910- runs_in_loading_queue,
1911- )
1912-
1913-
1914-class WorkingTreeFileList(QtGui.QTreeWidget):
1915-
1916- SELECTALL_MESSAGE = N_("Select / deselect all")
1917-
1918- def __init__(self, parent, tree):
1919- QtGui.QTreeWidget.__init__(self, parent)
1920- self._ignore_select_all_changes = False
1921- self.selectall_checkbox = None # added by client.
1922- self.tree = tree
1923-
1924- def setup_actions(self):
1925- """Setup double-click and context menu"""
1926- parent = self.parentWidget()
1927- parent.connect(self,
1928- QtCore.SIGNAL("itemDoubleClicked(QTreeWidgetItem *, int)"),
1929- self.itemDoubleClicked)
1930-
1931- self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1932- parent.connect(self,
1933- QtCore.SIGNAL("itemSelectionChanged()"),
1934- self.update_context_menu_actions)
1935- parent.connect(self,
1936- QtCore.SIGNAL("customContextMenuRequested(QPoint)"),
1937- self.show_context_menu)
1938-
1939- self.context_menu = QtGui.QMenu(self)
1940- if has_ext_diff():
1941- self.diff_menu = ExtDiffMenu(self)
1942- self.context_menu.addMenu(self.diff_menu)
1943- self.connect(self.diff_menu, QtCore.SIGNAL("triggered(QString)"),
1944- self.show_differences)
1945- self.show_diff_ui = self.diff_menu
1946- else:
1947- self.show_diff_action = self.context_menu.addAction(
1948- gettext("Show &differences..."), self.show_differences)
1949- self.context_menu.setDefaultAction(self.show_diff_action)
1950- self.show_diff_ui = self.show_diff_action
1951-
1952- self.revert_action = self.context_menu.addAction(
1953- gettext("&Revert..."), self.revert_selected)
1954- # set all actions to disabled so it does the right thing with an empty
1955- # list (our itemSelectionChanged() will fire as soon as we select one)
1956- self.revert_action.setEnabled(False)
1957- self.show_diff_ui.setEnabled(False)
1958-
1959- @runs_in_loading_queue
1960- def fill(self, items_iter):
1961- self.setTextElideMode(QtCore.Qt.ElideMiddle)
1962- self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
1963- self.setSortingEnabled(True)
1964- self.setHeaderLabels([gettext("File"), gettext("Extension"), gettext("Status")])
1965- header = self.header()
1966- header.setStretchLastSection(False)
1967- header.setResizeMode(0, QtGui.QHeaderView.Stretch)
1968- header.setResizeMode(1, QtGui.QHeaderView.ResizeToContents)
1969- header.setResizeMode(2, QtGui.QHeaderView.ResizeToContents)
1970- self.setRootIsDecorated(False)
1971- self._ignore_select_all_changes = True # don't update as we add items!
1972-
1973- # Each items_iter returns a tuple of (changes_tuple, is_checked)
1974- # Where changes_tuple is a single item from iter_changes():
1975- # (file_id, (path_in_source, path_in_target),
1976- # changed_content, versioned, parent, name, kind,
1977- # executable)
1978- # Note that the current filter is used to determine if the items are
1979- # shown or not
1980- self.item_to_data = {}
1981- items = []
1982- for change_desc, visible, checked in items_iter:
1983- (file_id, (path_in_source, path_in_target),
1984- changed_content, versioned, parent, name, kind,
1985- executable) = change_desc
1986-
1987- if versioned == (False, False):
1988- if self.tree.is_ignored(path_in_target):
1989- status = gettext("ignored")
1990- else:
1991- status = gettext("non-versioned")
1992- ext = file_extension(path_in_target)
1993- name = path_in_target
1994- elif versioned == (False, True):
1995- status = gettext("added")
1996- ext = file_extension(path_in_target)
1997- name = path_in_target + osutils.kind_marker(kind[1])
1998- elif versioned == (True, False):
1999- status = gettext("removed")
2000- ext = file_extension(path_in_source)
2001- name = path_in_source + osutils.kind_marker(kind[0])
2002- elif kind[0] is not None and kind[1] is None:
2003- status = gettext("missing")
2004- ext = file_extension(path_in_source)
2005- name = path_in_source + osutils.kind_marker(kind[0])
2006- else:
2007- # versioned = True, True - so either renamed or modified
2008- # or properties changed (x-bit).
2009- renamed = (parent[0], name[0]) != (parent[1], name[1])
2010- if renamed:
2011- if changed_content:
2012- status = gettext("renamed and modified")
2013- else:
2014- status = gettext("renamed")
2015- name = "%s%s => %s%s" % (path_in_source,
2016- osutils.kind_marker(kind[0]),
2017- path_in_target,
2018- osutils.kind_marker(kind[0]))
2019- ext = file_extension(path_in_target)
2020- elif changed_content:
2021- status = gettext("modified")
2022- name = path_in_target + osutils.kind_marker(kind[1])
2023- ext = file_extension(path_in_target)
2024- elif executable[0] != executable[1]:
2025- status = gettext("modified (x-bit)")
2026- name = path_in_target + osutils.kind_marker(kind[1])
2027- ext = file_extension(path_in_target)
2028- else:
2029- raise RuntimeError, "what status am I missing??"
2030-
2031- item = QtGui.QTreeWidgetItem()
2032- item.setText(0, name)
2033- item.setText(1, ext)
2034- item.setText(2, status)
2035- if visible:
2036- items.append(item)
2037-
2038- if checked is None:
2039- item.setCheckState(0, QtCore.Qt.PartiallyChecked)
2040- item.setFlags(item.flags() & ~QtCore.Qt.ItemIsUserCheckable)
2041- elif checked:
2042- item.setCheckState(0, QtCore.Qt.Checked)
2043- else:
2044- item.setCheckState(0, QtCore.Qt.Unchecked)
2045- self.item_to_data[item] = change_desc
2046- # add them all to the tree in one hit.
2047- self.insertTopLevelItems(0, items)
2048- self._ignore_select_all_changes = False
2049- if self.selectall_checkbox is not None:
2050- self.update_selectall_state(None, None)
2051-
2052- def set_item_hidden(self, item, hide):
2053- # Due to what seems a bug in Qt "hiding" an item isn't always
2054- # reliable - so a "hidden" item is simply not added to the tree!
2055- # See https://bugs.launchpad.net/qbzr/+bug/274295
2056- index = self.indexOfTopLevelItem(item)
2057- if index == -1 and not hide:
2058- self.addTopLevelItem(item)
2059- elif index != -1 and hide:
2060- self.takeTopLevelItem(index)
2061-
2062- def is_item_hidden(self, item):
2063- return self.indexOfTopLevelItem(item) == -1
2064-
2065- def iter_treeitem_and_desc(self, include_hidden=False):
2066- """iterators to help work with the selection, checked items, etc"""
2067- for ti, desc in self.item_to_data.iteritems():
2068- if include_hidden or not self.is_item_hidden(ti):
2069- yield ti, desc
2070-
2071- def iter_selection(self):
2072- for i in self.selectedItems():
2073- yield self.item_to_data[i]
2074-
2075- def iter_checked(self):
2076- # XXX - just use self.iter_treeitem_and_desc() - no need to hit the
2077- # XXX tree object at all!?
2078- for i in range(self.topLevelItemCount()):
2079- item = self.topLevelItem(i)
2080- if item.checkState(0) == QtCore.Qt.Checked:
2081- yield self.item_to_data[item]
2082-
2083- def show_context_menu(self, pos):
2084- """Context menu and double-click related functions..."""
2085- self.context_menu.popup(self.viewport().mapToGlobal(pos))
2086-
2087- def update_context_menu_actions(self):
2088- contains_non_versioned = False
2089- for desc in self.iter_selection():
2090- if desc[3] == (False, False):
2091- contains_non_versioned = True
2092- break
2093- self.revert_action.setEnabled(not contains_non_versioned)
2094- self.show_diff_ui.setEnabled(not contains_non_versioned)
2095-
2096- def revert_selected(self):
2097- """Revert the selected file."""
2098- items = self.selectedItems()
2099- if not items:
2100- return
2101-
2102- paths = [self.item_to_data[item][1][1] for item in items]
2103-
2104- args = ["revert"]
2105- args.extend(paths)
2106- desc = (gettext("Revert %s to latest revision.") % ", ".join(paths))
2107- revert_dialog = SimpleSubProcessDialog(gettext("Revert"),
2108- desc=desc,
2109- args=args,
2110- dir=self.tree.basedir,
2111- parent=self,
2112- hide_progress=True,
2113- )
2114- res = revert_dialog.exec_()
2115- if res == QtGui.QDialog.Accepted:
2116- for item in items:
2117- index = self.indexOfTopLevelItem(item)
2118- self.takeTopLevelItem(index)
2119-
2120- def itemDoubleClicked(self, items=None, column=None):
2121- self.show_differences()
2122-
2123- def show_differences(self, ext_diff=None):
2124- """Show differences between the working copy and the last revision."""
2125- if not self.show_diff_ui.isEnabled():
2126- return
2127-
2128- entries = [desc.path() for desc in self.iter_selection()]
2129- if entries:
2130- arg_provider = InternalWTDiffArgProvider(
2131- self.tree.basis_tree().get_revision_id(), self.tree,
2132- self.tree.branch, self.tree.branch,
2133- specific_files=entries)
2134-
2135- show_diff(arg_provider, ext_diff=ext_diff,
2136- parent_window=self.topLevelWidget())
2137-
2138- def set_selectall_checkbox(self, checkbox):
2139- """Helpers for a 'show all' checkbox. Parent widgets must create the
2140- widget and pass it to us.
2141- """
2142- checkbox.setTristate(True)
2143- self.selectall_checkbox = checkbox
2144- parent = self.parentWidget()
2145- parent.connect(self,
2146- QtCore.SIGNAL("itemChanged(QTreeWidgetItem *, int)"),
2147- self.update_selectall_state)
2148-
2149- parent.connect(checkbox, QtCore.SIGNAL("stateChanged(int)"),
2150- self.selectall_changed)
2151-
2152- def update_selectall_state(self, item, column):
2153- """Update the state of the 'select all' checkbox to reflect the state
2154- of the items in the list.
2155- """
2156- if self._ignore_select_all_changes:
2157- return
2158- checked = 0
2159- num_items = 0
2160-
2161- for (tree_item, change_desc) in self.iter_treeitem_and_desc():
2162- if tree_item.checkState(0) == QtCore.Qt.Checked:
2163- checked += 1
2164- num_items += 1
2165- self._ignore_select_all_changes = True
2166- if checked == 0:
2167- self.selectall_checkbox.setCheckState(QtCore.Qt.Unchecked)
2168- elif checked == num_items:
2169- self.selectall_checkbox.setCheckState(QtCore.Qt.Checked)
2170- else:
2171- self.selectall_checkbox.setCheckState(QtCore.Qt.PartiallyChecked)
2172- self._ignore_select_all_changes = False
2173-
2174- def selectall_changed(self, state):
2175- if self._ignore_select_all_changes or not self.selectall_checkbox.isEnabled():
2176- return
2177- if state == QtCore.Qt.PartiallyChecked:
2178- self.selectall_checkbox.setCheckState(QtCore.Qt.Checked)
2179- return
2180-
2181- self._ignore_select_all_changes = True
2182- for (tree_item, change_desc) in self.iter_treeitem_and_desc():
2183- tree_item.setCheckState(0, QtCore.Qt.CheckState(state))
2184- self._ignore_select_all_changes = False
2185-
2186-
2187-class ChangeDesc(tuple):
2188- """Helper class that "knows" about internals of iter_changes' changed entry
2189- description tuple, and provides additional helper methods.
2190-
2191- iter_changes return tuple with info about changed entry:
2192- [0]: file_id -> ascii string
2193- [1]: paths -> 2-tuple (old, new) fullpaths unicode/None
2194- [2]: changed_content -> bool
2195- [3]: versioned -> 2-tuple (bool, bool)
2196- [4]: parent -> 2-tuple
2197- [5]: name -> 2-tuple (old_name, new_name) utf-8?/None
2198- [6]: kind -> 2-tuple (string/None, string/None)
2199- [7]: executable -> 2-tuple (bool/None, bool/None)
2200-
2201- --optional--
2202- [8]: is_ignored -> If the file is ignored, pattern which caused it to
2203- be ignored, otherwise None.
2204-
2205- NOTE: None value used for non-existing entry in corresponding
2206- tree, e.g. for added/deleted/ignored/unversioned
2207- """
2208-
2209- # XXX We should may be try get this into bzrlib.
2210- # XXX We should use this in qdiff.
2211-
2212- def fileid(desc):
2213- return desc[0]
2214-
2215- def path(desc):
2216- """Return a suitable entry for a 'specific_files' param to bzr functions."""
2217- oldpath, newpath = desc[1]
2218- return newpath or oldpath
2219-
2220- def oldpath(desc):
2221- """Return oldpath for renames."""
2222- return desc[1][0]
2223-
2224- def kind(desc):
2225- oldkind, newkind = desc[6]
2226- return newkind or oldkind
2227-
2228- def is_versioned(desc):
2229- return desc[3] != (False, False)
2230-
2231- def is_modified(desc):
2232- return (desc[3] != (False, False) and desc[2])
2233-
2234- def is_renamed(desc):
2235- return (desc[3] == (True, True)
2236- and (desc[4][0], desc[5][0]) != (desc[4][1], desc[5][1]))
2237-
2238- def is_tree_root(desc):
2239- """Check if entry actually tree root."""
2240- if desc[3] != (False, False) and desc[4] == (None, None):
2241- # TREE_ROOT has not parents (desc[4]).
2242- # But because we could want to see unversioned files
2243- # we need to check for versioned flag (desc[3])
2244- return True
2245- return False
2246-
2247- def is_missing(desc):
2248- """Check if file was present in previous revision but now it's gone
2249- (i.e. deleted manually, without invoking `bzr remove` command)
2250- """
2251- return (desc[3] == (True, True) and desc[6][1] is None)
2252-
2253- def is_misadded(desc):
2254- """Check if file was added to the working tree but then gone
2255- (i.e. deleted manually, without invoking `bzr remove` command)
2256- """
2257- return (desc[3] == (False, True) and desc[6][1] is None)
2258-
2259- def is_ignored(desc):
2260- if len(desc) >= 8:
2261- return desc[8]
2262- else:
2263- return None
2264-
2265- def status(desc):
2266- if len(desc) == 8:
2267- (file_id, (path_in_source, path_in_target),
2268- changed_content, versioned, parent, name, kind,
2269- executable) = desc
2270- is_ignored = None
2271- elif len(desc) == 9:
2272- (file_id, (path_in_source, path_in_target),
2273- changed_content, versioned, parent, name, kind,
2274- executable, is_ignored) = desc
2275- else:
2276- raise RuntimeError, "Unkown number of items to unpack."
2277-
2278- if versioned == (False, False):
2279- if is_ignored:
2280- return gettext("ignored")
2281- else:
2282- return gettext("non-versioned")
2283- elif versioned == (False, True):
2284- return gettext("added")
2285- elif versioned == (True, False):
2286- return gettext("removed")
2287- elif kind[0] is not None and kind[1] is None:
2288- return gettext("missing")
2289- else:
2290- # versioned = True, True - so either renamed or modified
2291- # or properties changed (x-bit).
2292- renamed = (parent[0], name[0]) != (parent[1], name[1])
2293- if renamed:
2294- if changed_content:
2295- return gettext("renamed and modified")
2296- else:
2297- return gettext("renamed")
2298- elif changed_content:
2299- return gettext("modified")
2300- elif executable[0] != executable[1]:
2301- return gettext("modified (x-bit)")
2302- else:
2303- raise RuntimeError, "what status am I missing??"
2304-
2305-
2306-def closure_in_selected_list(selected_list):
2307- """Return in_selected_list(path) function for given selected_list."""
2308-
2309- def in_selected_list(path):
2310- """Check: is path belongs to some selected_list."""
2311- if path in selected_list:
2312- return True
2313- for p in selected_list:
2314- if path.startswith(p):
2315- return True
2316- return False
2317-
2318- if not selected_list:
2319- return lambda path: True
2320- return in_selected_list

Subscribers

People subscribed via source and target branches

to status/vote changes: