105
105
QtGui.QDialogButtonBox.__init__(self, *args)
106
106
self._root = None
107
107
self._selected_fileinfo = None
108
self._button_style = QtCore.Qt.ToolButtonTextUnderIcon
109
110
# Build the buttons and add them to a panel
110
browse_button = QtGui.QPushButton()
111
browse_button.setText(self._browse_action.iconText())
112
browse_button.setIcon(self._browse_action.icon())
113
open_button = QtGui.QPushButton()
114
open_button.setText(gettext("&Manage"))
115
open_button.setIcon(icon_for_kind(OPEN_FOLDER_ACTION))
116
edit_button = QtGui.QPushButton()
117
edit_button.setText(gettext("&Edit"))
118
edit_button.setIcon(icon_for_kind(EDIT_ACTION))
119
self.addButton(browse_button, QtGui.QDialogButtonBox.ActionRole)
120
self.addButton(open_button, QtGui.QDialogButtonBox.ActionRole)
121
self.addButton(edit_button, QtGui.QDialogButtonBox.AcceptRole)
122
self.connect(browse_button, QtCore.SIGNAL("clicked(bool)"),
111
self._open_button = self._build_button(
112
kinds.icon_for_kind(kinds.OPEN_ACTION),
114
gettext("Open selected item"),
116
self._edit_button = self._build_button(
117
kinds.icon_for_kind(kinds.EDIT_ACTION),
119
gettext("Edit selected item"),
123
kinds.icon_for_kind(kinds.OPEN_FOLDER_ACTION),
125
gettext("Open file manager on folder"),
126
self._do_open_folder)
128
kinds.icon_for_kind(kinds.NEW_FILE_ACTION),
129
gettext("&New File"),
130
gettext("Create a new file"),
133
kinds.icon_for_kind(kinds.NEW_FOLDER_ACTION),
134
gettext("New &Folder"),
135
gettext("Create a new folder"),
139
self._browse_action.icon(),
140
self._browse_action.iconText(),
141
self._browse_action.statusTip(),
123
142
self._do_browse_items)
124
self.connect(open_button, QtCore.SIGNAL("clicked(bool)"),
125
self._do_open_folder)
126
self.connect(edit_button, QtCore.SIGNAL("clicked(bool)"),
128
self._edit_button = edit_button
130
def _do_browse_items(self):
131
self._action_callback("browse", None)
133
def _do_open_folder(self):
145
kinds.icon_for_kind(kinds.COLLAPSE_ALL_ACTION),
146
gettext("&Collapse"),
147
gettext("Fully collapse tree"),
148
self._do_collapse_all)
150
kinds.icon_for_kind(kinds.EXPAND_ALL_ACTION),
152
gettext("Fully expand tree"),
155
def _build_button(self, icon, text, tooltip, callback, arrow=None):
156
button = QtGui.QToolButton()
159
button.setArrowType(arrow)
162
button.setToolTip(tooltip)
163
#button.setToolButtonStyle(self._button_style)
164
button.setAutoRaise(True)
165
#self.addButton(button, QtGui.QDialogButtonBox.ActionRole)
166
self.addWidget(button)
167
self.connect(button, QtCore.SIGNAL("clicked(bool)"), callback)
170
def _get_folder(self):
134
171
fileinfo = self._selected_fileinfo
135
172
if fileinfo is None:
136
173
folder = self._root
138
175
folder = fileinfo.canonicalFilePath()
140
177
folder = fileinfo.canonicalPath()
141
self._action_callback("open", folder)
178
return unicode(folder)
180
def _do_browse_items(self):
181
self._action_callback("browse", None)
183
def _do_open_folder(self):
184
self._action_callback("open", self._get_folder())
186
def _do_open_file(self):
187
fileinfo = self._selected_fileinfo
188
path = fileinfo.canonicalFilePath()
189
self._action_callback("open", unicode(path))
143
191
def _do_edit_file(self):
144
192
fileinfo = self._selected_fileinfo
145
193
path = fileinfo.canonicalFilePath()
146
194
self._action_callback("edit", unicode(path))
196
def _do_new_file(self):
197
destination = self._get_folder()
198
self._action_callback("new-file", destination)
199
self._tree_viewer.refresh()
201
def _do_new_folder(self):
202
destination = self._get_folder()
203
self._action_callback("new-directory", destination)
204
self._tree_viewer.refresh()
206
def _do_collapse_all(self):
207
self._tree_viewer.collapseAll()
209
def _do_expand_all(self):
210
self._tree_viewer.expandAll()
212
def set_viewer(self, tree_viewer):
213
self._tree_viewer = tree_viewer
148
215
def set_root(self, path):
149
216
self._root = path
151
def set_selection(self, fileinfo):
218
def set_selection(self, fileinfo, model_index=None):
152
219
self._selected_fileinfo = fileinfo
220
self._selected_index = model_index
153
221
edit_is_enabled = True
154
222
if fileinfo is None or fileinfo.isDir():
155
223
edit_is_enabled = False
156
224
self._edit_button.setEnabled(edit_is_enabled)
225
self._open_button.setEnabled(edit_is_enabled)
159
228
class _FilterPanel(QtGui.QWidget):
396
458
"""Refresh the browser."""
397
459
self._tree_viewer.refresh()
461
def set_default_action(self, default_action):
462
"""Set the default action to either 'open' or 'edit'."""
463
self._tree_viewer.set_default_action(default_action)
466
class _ClassicBrowser(_AbstractBrowser):
468
def _apply_filter(self, text):
469
self._tree_viewer.invalidate_filter()
471
# Filtering active. We want to see all matches and
472
# this mostly works. It seems to struggle though when
473
# the filter gets shorter - maybe a Qt 4.4 bug w.r.t.
474
# propagating exactly what's changed to the view?
475
self._tree_viewer.expandAll()
477
# Filtering cleared. Restore the initial view with
478
# everything collapsed. (If the user has selectively
479
# expanded some directories out before filtering,
480
# collapsing all here implies losing that setup. We
481
# could always remember the expanded directories at
482
# the start of filtering and restore them at this
483
# point if it proves a problem in practice.)
484
self._tree_viewer.collapseAll()
485
self._tree_viewer.refresh()
400
488
### QBrowse style browser support ###
402
490
# The map from filter category to filter flags for TreeFilterProxyModels
403
491
_FILTER_FLAGS_MAP = {
404
492
# The flag order is UNCHANGED, CHANGED, UNVERSIONED, IGNORED
405
'versioned': [True, True, True, False],
493
'all': [True, True, True, True],
406
494
'changed': [False, True, False, False],
495
'versioned': [True, True, False, False],
496
'unversioned': [False, False, True, False],
407
497
'ignored': [False, False, True, True],
408
'unversioned': [False, False, True, False],
409
'all': [True, True, True, True],
498
'unignored': [True, True, True, False],
412
501
class _QBrowseStyleFilterPanel(QtGui.QWidget):
418
507
# Build the controls
419
508
self._category_combo = build_combo_with_labels([
509
('all', gettext("All")),
510
('changed', gettext("Changed")),
420
511
('versioned', gettext("Versioned")),
421
('changed', gettext("Changed")),
512
('unversioned', gettext("Unversioned")),
422
513
('ignored', gettext("Ignored")),
423
('unversioned', gettext("Unversioned")),
424
('all', gettext("All")),
514
('unignored', gettext("Unignored")),
516
self._category_combo.setCurrentIndex(5)
517
self._text_filter = _FilterPanel()
427
519
# Connect up the controls to handlers
428
520
QtCore.QObject.connect(self._category_combo,
429
521
QtCore.SIGNAL("currentIndexChanged(int)"),
430
522
self._apply_category)
523
self._text_filter.set_apply_filter(self._apply_text)
432
525
# Put the controls together
433
526
layout = QtGui.QHBoxLayout()
434
527
layout.addWidget(self._category_combo)
528
layout.addWidget(self._text_filter)
436
529
layout.setContentsMargins(0, 0, 0, 0)
437
530
self.setLayout(layout)
532
def _flags_for_index(self, index):
533
category = str(self._category_combo.itemData(index).toString())
534
return _FILTER_FLAGS_MAP[category]
439
536
def _apply_category(self, index):
440
category = str(self._category_combo.itemData(index).toString())
441
filter_flags = _FILTER_FLAGS_MAP[category]
442
#print "category %s mapped to flags %s" % (category, filter_flags)
443
self._tree_viewer.tree_filter_model.setFilters(filter_flags)
537
self._apply_filter_callback(self._flags_for_index(index), True,
538
self._text_filter.get_filter(), False)
540
def _apply_text(self, text):
541
index = self._category_combo.currentIndex()
542
self._apply_filter_callback(self._flags_for_index(index), False,
445
545
def clear_filter(self):
546
self._text_filter.clear_filter()
449
548
def get_filter(self):
450
549
index = self._category_combo.currentIndex()
451
return str(self._category_combo.itemData(index).toString())
453
def set_tree_viewer(self, tree_viewer):
454
self._tree_viewer = tree_viewer
457
class _QBrowseStyleTreeViewer(treewidget.TreeWidget):
459
def __init__(self, action_callback, selection_callback,
460
filter_callback, *args):
461
self._selection_callback = selection_callback
550
return self._flags_for_index(index), self._text_filter.get_filter()
552
def set_apply_filter(self, apply_filter_callback):
553
self._apply_filter_callback = apply_filter_callback
556
class _QBrowseFilterProxyModel(treewidget.TreeFilterProxyModel):
557
"""Filter a working tree by a text string.
559
If the text matches a directory, the directory and all it's contents
560
are shown. If the text matches a file, the parent directories and
563
Note: For efficency, we pre-calculate the interesting entries
564
once each time the text is changed. We then simply look up
565
each path in the set of interesting paths inside the per-row
569
def __init__(self, *args):
570
treewidget.TreeFilterProxyModel.__init__(self, *args)
571
self.text_to_match = None
572
self._interesting = None
573
self._all_data_loaded = False
575
def setTextToMatch(self, text):
576
self.text_to_match = text
577
self._update_interesting()
578
self.invalidateFilter()
580
def invalidateFilter(self):
581
treewidget.TreeFilterProxyModel.invalidateFilter(self)
583
def _update_interesting(self):
584
if not self.text_to_match:
585
self._interesting = None
587
text = self.text_to_match
588
interesting = set([''])
590
for path in self._iter_paths():
591
if path.find(text) >= 0:
592
interesting.add(path)
593
directories.update(parent_directories(path))
594
interesting.update(directories)
595
#print "\ninteresting for %s ...\n%s\n" % (text,
596
# "\n".join(sorted(interesting)))
597
self._interesting = interesting
599
def _iter_paths(self):
600
# The source model loads data lazily. We need everything loaded
601
# to find the complete set of paths though
602
self._ensure_all_data_loaded()
603
return iter(self.source_model.inventory_data_by_path.keys())
605
def _ensure_all_data_loaded(self):
606
if self._all_data_loaded:
608
model = self.source_model
609
for id in range(0, len(model.inventory_data)):
610
self._load_dirs(model, id)
611
self._all_data_loaded = True
613
def _load_dirs(self, model, id):
614
item_data = model.inventory_data[id]
615
if item_data.item.kind == "directory":
616
if item_data.children_ids is None:
617
# This locks and unlocks the tree each time.
618
# I wonder how that impacts performance?
620
for child_id in item_data.children_ids:
621
self._load_dirs(model, child_id)
623
def filterAcceptsRow(self, source_row, source_parent):
624
result = treewidget.TreeFilterProxyModel.filterAcceptsRow(
625
self, source_row, source_parent)
626
if result and self._interesting:
627
# Apply the text filter (matches found earlier)
628
path, kind = self._get_path_info(source_row, source_parent)
629
result = path in self._interesting
632
def _get_path_info(self, source_row, source_parent):
633
"""Return path, kind for a model item."""
634
model = self.source_model
635
parent_id = source_parent.internalId()
636
children_ids = model.inventory_data[parent_id].children_ids
637
# Why is this check required?
638
if len(children_ids) <= source_row:
640
id = children_ids[source_row]
641
data = model.inventory_data[id]
642
return data.path, data.item.kind
645
class _QBrowseTreeWidget(treewidget.TreeWidget):
647
def __init__(self, *args):
462
648
treewidget.TreeWidget.__init__(self, *args)
649
# Patch in a custom FilterProxyModel that handles text filtering
650
self.tree_filter_model = _QBrowseFilterProxyModel()
651
self.tree_filter_model.setSourceModel(self.tree_model)
652
self.setModel(self.tree_filter_model)
655
class _QBrowseStyleTreeViewer(_QBrowseTreeWidget):
657
def __init__(self, action_callback, selection_callback,
658
filter_callback, *args):
659
self._action_callback = action_callback
660
self._selection_callback = selection_callback
661
_QBrowseTreeWidget.__init__(self, *args)
662
self._default_action = "open"
464
664
# Track the selection
465
665
self.connect(self.selectionModel(),
466
666
QtCore.SIGNAL("currentChanged(QModelIndex,QModelIndex)"),
467
667
self._do_current_changed)
669
def do_default_action(self, model_index):
670
"""Override the handler provided by treewidget.TreeWidget."""
671
item_data = self.get_selection_items([model_index])[0]
672
fileinfo = QtCore.QFileInfo(item_data.path)
673
if fileinfo is not None and not fileinfo.isDir():
674
path = fileinfo.canonicalFilePath()
675
self._action_callback(self._default_action, unicode(path))
469
677
def _do_current_changed(self, model_index, prev_model_index):
470
678
item_data = self.get_selection_items([model_index])[0]
471
679
fileinfo = QtCore.QFileInfo(item_data.path)
472
self._selection_callback(fileinfo)
680
self._selection_callback(fileinfo, model_index)
474
682
def invalidate_filter(self):
475
683
self.tree_filter_model.invalidateFilter()
478
class _QBrowseStyleBrowser(_ClassicBrowser):
685
def set_default_action(self, default_action):
686
"""Set the default action to either 'open' or 'edit'."""
687
self._default_action = default_action
690
class _QBrowseStyleBrowser(_AbstractBrowser):
480
692
filter_panel_factory = _QBrowseStyleFilterPanel
481
693
tree_viewer_factory = _QBrowseStyleTreeViewer
695
def _apply_filter(self, flags, flags_changed, text, text_changed):
697
self._tree_viewer.tree_filter_model.setFilters(flags)
699
self._tree_viewer.tree_filter_model.setTextToMatch(text)