qtutils: add text_width() and text_size() helper functions
[git-cola.git] / cola / widgets / browse.py
blob4c54d659a44e4a0898e0f9e711ab0b3f580ff7fb
1 import shlex
3 from qtpy.QtCore import Qt
4 from qtpy.QtCore import Signal
5 from qtpy import QtCore
6 from qtpy import QtGui
7 from qtpy import QtWidgets
9 from ..models.browse import GitRepoModel
10 from ..models.browse import GitRepoNameItem
11 from ..models.selection import State
12 from ..i18n import N_
13 from ..interaction import Interaction
14 from .. import cmds
15 from .. import core
16 from .. import difftool
17 from .. import gitcmds
18 from .. import hotkeys
19 from .. import icons
20 from .. import utils
21 from .. import qtutils
22 from .selectcommits import select_commits
23 from . import common
24 from . import defs
25 from . import standard
28 def worktree_browser(context, parent=None, update=True, show=True):
29 """Create a new worktree browser"""
30 view = Browser(context, parent, update=update)
31 if parent is None:
32 context.browser_windows.append(view)
33 view.closed.connect(context.browser_windows.remove)
34 model = GitRepoModel(context, view.tree)
35 view.set_model(model)
36 if update:
37 view.refresh()
38 if show:
39 view.show()
40 return view
43 def save_path(context, path, model):
44 """Choose an output filename based on the selected path"""
45 filename = qtutils.save_as(path)
46 if filename:
47 model.filename = filename
48 cmds.do(SaveBlob, context, model)
49 result = True
50 else:
51 result = False
52 return result
55 class Browser(standard.Widget):
56 """A repository branch file browser. Browses files provided by GitRepoModel"""
58 # Read-only mode property
59 mode = property(lambda self: self.model.mode)
61 def __init__(self, context, parent, update=True):
62 standard.Widget.__init__(self, parent)
63 self.tree = RepoTreeView(context, self)
64 self.mainlayout = qtutils.hbox(defs.no_margin, defs.spacing, self.tree)
65 self.setLayout(self.mainlayout)
67 self.model = context.model
68 self.model.updated.connect(self._updated_callback, type=Qt.QueuedConnection)
69 if parent is None:
70 qtutils.add_close_action(self)
71 if update:
72 self._updated_callback()
74 self.init_state(context.settings, self.resize, 720, 420)
76 def set_model(self, model):
77 """Set the model"""
78 self.tree.set_model(model)
80 def refresh(self):
81 """Refresh the model triggering view updates"""
82 self.tree.refresh()
84 def _updated_callback(self):
85 branch = self.model.currentbranch
86 curdir = core.getcwd()
87 msg = N_('Repository: %s') % curdir
88 msg += '\n'
89 msg += N_('Branch: %s') % branch
90 self.setToolTip(msg)
92 scope = {
93 'project': self.model.project,
94 'branch': branch,
96 title = N_('%(project)s: %(branch)s - Browse') % scope
97 if self.mode == self.model.mode_amend:
98 title += ' %s' % N_('(Amending)')
99 self.setWindowTitle(title)
102 # pylint: disable=too-many-ancestors
103 class RepoTreeView(standard.TreeView):
104 """Provides a filesystem-like view of a git repository."""
106 def __init__(self, context, parent):
107 standard.TreeView.__init__(self, parent)
109 self.context = context
110 self.selection = context.selection
111 self.saved_selection = []
112 self.saved_current_path = None
113 self.saved_open_folders = set()
114 self.restoring_selection = False
115 self._columns_sized = False
117 self.setDragEnabled(True)
118 self.setRootIsDecorated(False)
119 self.setSortingEnabled(False)
120 self.setSelectionMode(self.ExtendedSelection)
122 # Observe model updates
123 model = context.model
124 model.about_to_update.connect(self.save_selection, type=Qt.QueuedConnection)
125 model.updated.connect(self.update_actions, type=Qt.QueuedConnection)
126 self.expanded.connect(self.index_expanded)
128 self.collapsed.connect(lambda idx: self.size_columns())
129 self.collapsed.connect(self.index_collapsed)
131 # Sync selection before the key press event changes the model index
132 queued = Qt.QueuedConnection
133 self.index_about_to_change.connect(self.sync_selection, type=queued)
135 self.action_history = qtutils.add_action_with_tooltip(
136 self,
137 N_('View History...'),
138 N_('View history for selected paths'),
139 self.view_history,
140 hotkeys.HISTORY,
143 self.action_stage = qtutils.add_action_with_tooltip(
144 self,
145 cmds.StageOrUnstage.name(),
146 N_('Stage/unstage selected paths for commit'),
147 cmds.run(cmds.StageOrUnstage, context),
148 hotkeys.STAGE_SELECTION,
151 self.action_untrack = qtutils.add_action_with_tooltip(
152 self,
153 N_('Untrack Selected'),
154 N_('Stop tracking paths'),
155 self.untrack_selected,
158 self.action_rename = qtutils.add_action_with_tooltip(
159 self, N_('Rename'), N_('Rename selected paths'), self.rename_selected
162 self.action_difftool = qtutils.add_action_with_tooltip(
163 self,
164 difftool.LaunchDifftool.name(),
165 N_('Launch git-difftool on the current path'),
166 cmds.run(difftool.LaunchDifftool, context),
167 hotkeys.DIFF,
170 self.action_difftool_predecessor = qtutils.add_action_with_tooltip(
171 self,
172 N_('Diff Against Predecessor...'),
173 N_('Launch git-difftool against previous versions'),
174 self.diff_predecessor,
175 hotkeys.DIFF_SECONDARY,
178 self.action_revert_unstaged = qtutils.add_action_with_tooltip(
179 self,
180 cmds.RevertUnstagedEdits.name(),
181 N_('Revert unstaged changes to selected paths'),
182 cmds.run(cmds.RevertUnstagedEdits, context),
183 hotkeys.REVERT,
184 hotkeys.REVERT_ALT,
187 self.action_revert_uncommitted = qtutils.add_action_with_tooltip(
188 self,
189 cmds.RevertUncommittedEdits.name(),
190 N_('Revert uncommitted changes to selected paths'),
191 cmds.run(cmds.RevertUncommittedEdits, context),
192 hotkeys.UNDO,
195 self.action_editor = qtutils.add_action_with_tooltip(
196 self,
197 cmds.LaunchEditor.name(),
198 N_('Edit selected paths'),
199 cmds.run(cmds.LaunchEditor, context),
200 hotkeys.EDIT,
203 self.action_blame = qtutils.add_action_with_tooltip(
204 self,
205 cmds.BlamePaths.name(),
206 N_('Blame selected paths'),
207 cmds.run(cmds.BlamePaths, context),
210 self.action_refresh = common.refresh_action(context, self)
212 self.action_default_app = common.default_app_action(
213 context, self, self.selected_paths
216 self.action_parent_dir = common.parent_dir_action(
217 context, self, self.selected_paths
220 self.action_terminal = common.terminal_action(
221 context, self, func=self.selected_paths
224 self.x_width = qtutils.text_width(self.font(), 'x')
225 self.size_columns(force=True)
227 def index_expanded(self, index):
228 """Update information about a directory as it is expanded."""
229 # Remember open folders so that we can restore them when refreshing
230 item = self.name_item_from_index(index)
231 self.saved_open_folders.add(item.path)
232 self.size_columns()
234 # update information about a directory as it is expanded
235 if item.cached:
236 return
237 path = item.path
239 model = self.model()
240 model.populate(item)
241 model.update_entry(path)
243 for row in range(item.rowCount()):
244 path = item.child(row, 0).path
245 model.update_entry(path)
247 item.cached = True
249 def index_collapsed(self, index):
250 item = self.name_item_from_index(index)
251 self.saved_open_folders.remove(item.path)
253 def refresh(self):
254 self.model().refresh()
256 def size_columns(self, force=False):
257 """Set the column widths."""
258 cfg = self.context.cfg
259 should_resize = cfg.get('cola.resizebrowsercolumns', default=False)
260 if not force and not should_resize:
261 return
262 self.resizeColumnToContents(0)
263 self.resizeColumnToContents(1)
264 self.resizeColumnToContents(2)
265 self.resizeColumnToContents(3)
266 self.resizeColumnToContents(4)
268 def sizeHintForColumn(self, column):
269 x_width = self.x_width
271 if column == 1:
272 # Status
273 size = x_width * 11
274 elif column == 2:
275 # Summary
276 size = x_width * 64
277 elif column == 3:
278 # Author
279 size = x_width * 18
280 elif column == 4:
281 # Age
282 size = x_width * 16
283 else:
284 # Filename and others use the actual content
285 size = super().sizeHintForColumn(column)
286 return size
288 def save_selection(self):
289 selection = self.selected_paths()
290 if selection:
291 self.saved_selection = selection
293 current = self.current_item()
294 if current:
295 self.saved_current_path = current.path
297 def restore(self):
298 selection = self.selectionModel()
299 flags = selection.Select | selection.Rows
301 self.restoring_selection = True
303 # Restore opened folders
304 model = self.model()
305 for path in sorted(self.saved_open_folders):
306 row = model.get(path)
307 if not row:
308 continue
309 index = row[0].index()
310 if index.isValid():
311 self.setExpanded(index, True)
313 # Restore the current item. We do this first, otherwise
314 # setCurrentIndex() can mess with the selection we set below
315 current_index = None
316 current_path = self.saved_current_path
317 if current_path:
318 row = model.get(current_path)
319 if row:
320 current_index = row[0].index()
322 if current_index and current_index.isValid():
323 self.setCurrentIndex(current_index)
325 # Restore selected items
326 for path in self.saved_selection:
327 row = model.get(path)
328 if not row:
329 continue
330 index = row[0].index()
331 if index.isValid():
332 self.scrollTo(index)
333 selection.select(index, flags)
335 self.restoring_selection = False
337 # Resize the columns once when cola.resizebrowsercolumns is False.
338 # This provides a good initial size since we will not be resizing
339 # the columns during expand/collapse.
340 if not self._columns_sized:
341 self._columns_sized = True
342 self.size_columns(force=True)
344 self.update_diff()
346 def update_actions(self):
347 """Enable/disable actions."""
348 selection = self.selected_paths()
349 selected = bool(selection)
350 staged = bool(self.selected_staged_paths(selection=selection))
351 modified = bool(self.selected_modified_paths(selection=selection))
352 unstaged = bool(self.selected_unstaged_paths(selection=selection))
353 tracked = bool(self.selected_tracked_paths(selection=selection))
354 revertable = staged or modified
356 self.action_editor.setEnabled(selected)
357 self.action_history.setEnabled(selected)
358 self.action_default_app.setEnabled(selected)
359 self.action_parent_dir.setEnabled(selected)
361 if self.action_terminal is not None:
362 self.action_terminal.setEnabled(selected)
364 self.action_stage.setEnabled(staged or unstaged)
365 self.action_untrack.setEnabled(tracked)
366 self.action_rename.setEnabled(tracked)
367 self.action_difftool.setEnabled(staged or modified)
368 self.action_difftool_predecessor.setEnabled(tracked)
369 self.action_revert_unstaged.setEnabled(revertable)
370 self.action_revert_uncommitted.setEnabled(revertable)
372 def contextMenuEvent(self, event):
373 """Create a context menu."""
374 self.update_actions()
375 menu = qtutils.create_menu(N_('Actions'), self)
376 menu.addAction(self.action_editor)
377 menu.addAction(self.action_stage)
378 menu.addSeparator()
379 menu.addAction(self.action_history)
380 menu.addAction(self.action_difftool)
381 menu.addAction(self.action_difftool_predecessor)
382 menu.addAction(self.action_blame)
383 menu.addSeparator()
384 menu.addAction(self.action_revert_unstaged)
385 menu.addAction(self.action_revert_uncommitted)
386 menu.addAction(self.action_untrack)
387 menu.addAction(self.action_rename)
388 menu.addSeparator()
389 menu.addAction(self.action_default_app)
390 menu.addAction(self.action_parent_dir)
392 if self.action_terminal is not None:
393 menu.addAction(self.action_terminal)
394 menu.exec_(self.mapToGlobal(event.pos()))
396 def mousePressEvent(self, event):
397 """Synchronize the selection on mouse-press."""
398 result = QtWidgets.QTreeView.mousePressEvent(self, event)
399 self.sync_selection()
400 return result
402 def sync_selection(self):
403 """Push selection into the selection model."""
404 staged = []
405 unmerged = []
406 modified = []
407 untracked = []
408 state = State(staged, unmerged, modified, untracked)
410 paths = self.selected_paths()
411 model = self.context.model
412 model_staged = utils.add_parents(model.staged)
413 model_modified = utils.add_parents(model.modified)
414 model_unmerged = utils.add_parents(model.unmerged)
415 model_untracked = utils.add_parents(model.untracked)
417 for path in paths:
418 if path in model_unmerged:
419 unmerged.append(path)
420 elif path in model_untracked:
421 untracked.append(path)
422 elif path in model_staged:
423 staged.append(path)
424 elif path in model_modified:
425 modified.append(path)
426 else:
427 staged.append(path)
428 # Push the new selection into the model.
429 self.selection.set_selection(state)
430 return paths
432 def selectionChanged(self, old, new):
433 """Override selectionChanged to update available actions."""
434 result = QtWidgets.QTreeView.selectionChanged(self, old, new)
435 if not self.restoring_selection:
436 self.update_actions()
437 self.update_diff()
438 return result
440 def update_diff(self):
441 context = self.context
442 model = context.model
443 paths = self.sync_selection()
444 if paths and self.model().path_is_interesting(paths[0]):
445 cached = paths[0] in model.staged
446 cmds.do(cmds.Diff, context, paths[0], cached)
448 def set_model(self, model):
449 """Set the concrete QAbstractItemModel instance."""
450 self.setModel(model)
451 model.restore.connect(self.restore, type=Qt.QueuedConnection)
453 def name_item_from_index(self, model_index):
454 """Return the name item corresponding to the model index."""
455 index = model_index.sibling(model_index.row(), 0)
456 return self.model().itemFromIndex(index)
458 def paths_from_indexes(self, indexes):
459 return qtutils.paths_from_indexes(
460 self.model(), indexes, item_type=GitRepoNameItem.TYPE
463 def selected_paths(self):
464 """Return the selected paths."""
465 return self.paths_from_indexes(self.selectedIndexes())
467 def selected_staged_paths(self, selection=None):
468 """Return selected staged paths."""
469 if selection is None:
470 selection = self.selected_paths()
471 model = self.context.model
472 staged = utils.add_parents(model.staged)
473 return [p for p in selection if p in staged]
475 def selected_modified_paths(self, selection=None):
476 """Return selected modified paths."""
477 if selection is None:
478 selection = self.selected_paths()
479 model = self.context.model
480 modified = utils.add_parents(model.modified)
481 return [p for p in selection if p in modified]
483 def selected_unstaged_paths(self, selection=None):
484 """Return selected unstaged paths."""
485 if selection is None:
486 selection = self.selected_paths()
487 model = self.context.model
488 modified = utils.add_parents(model.modified)
489 untracked = utils.add_parents(model.untracked)
490 unstaged = modified.union(untracked)
491 return [p for p in selection if p in unstaged]
493 def selected_tracked_paths(self, selection=None):
494 """Return selected tracked paths."""
495 if selection is None:
496 selection = self.selected_paths()
497 model = self.context.model
498 staged = set(self.selected_staged_paths(selection=selection))
499 modified = set(self.selected_modified_paths(selection=selection))
500 untracked = utils.add_parents(model.untracked)
501 tracked = staged.union(modified)
502 return [p for p in selection if p not in untracked or p in tracked]
504 def view_history(self):
505 """Launch the configured history browser path-limited to entries."""
506 paths = self.selected_paths()
507 cmds.do(cmds.VisualizePaths, self.context, paths)
509 def untrack_selected(self):
510 """untrack selected paths."""
511 context = self.context
512 cmds.do(cmds.Untrack, context, self.selected_tracked_paths())
514 def rename_selected(self):
515 """untrack selected paths."""
516 context = self.context
517 cmds.do(cmds.Rename, context, self.selected_tracked_paths())
519 def diff_predecessor(self):
520 """Diff paths against previous versions."""
521 context = self.context
522 paths = self.selected_tracked_paths()
523 args = ['--'] + paths
524 revs, summaries = gitcmds.log_helper(context, all=False, extra_args=args)
525 commits = select_commits(
526 context, N_('Select Previous Version'), revs, summaries, multiselect=False
528 if not commits:
529 return
530 commit = commits[0]
531 difftool.difftool_launch(context, left=commit, paths=paths)
533 def current_path(self):
534 """Return the path for the current item."""
535 index = self.currentIndex()
536 if not index.isValid():
537 return None
538 return self.name_item_from_index(index).path
541 class BrowseModel:
542 """Context data used for browsing branches via git-ls-tree"""
544 def __init__(self, ref, filename=None):
545 self.ref = ref
546 self.relpath = filename
547 self.filename = filename
550 class SaveBlob(cmds.ContextCommand):
551 def __init__(self, context, model):
552 super().__init__(context)
553 self.browse_model = model
555 def do(self):
556 git = self.context.git
557 model = self.browse_model
558 ref = f'{model.ref}:{model.relpath}'
559 with core.xopen(model.filename, 'wb') as fp:
560 status, output, err = git.show(ref, _stdout=fp)
562 out = '# git show {} >{}\n{}'.format(
563 shlex.quote(ref),
564 shlex.quote(model.filename),
565 output,
567 Interaction.command(N_('Error Saving File'), 'git show', status, out, err)
568 if status != 0:
569 return
571 msg = N_('Saved "%(filename)s" from "%(ref)s" to "%(destination)s"') % {
572 'filename': model.relpath,
573 'ref': model.ref,
574 'destination': model.filename,
576 Interaction.log_status(status, msg, '')
578 Interaction.information(
579 N_('File Saved'), N_('File saved to "%s"') % model.filename
583 class BrowseBranch(standard.Dialog):
584 @classmethod
585 def browse(cls, context, ref):
586 model = BrowseModel(ref)
587 dlg = cls(context, model, parent=qtutils.active_window())
588 dlg_model = GitTreeModel(context, ref, dlg)
589 dlg.setModel(dlg_model)
590 dlg.setWindowTitle(N_('Browsing %s') % model.ref)
591 dlg.show()
592 dlg.raise_()
593 if dlg.exec_() != dlg.Accepted:
594 return None
595 return dlg
597 def __init__(self, context, model, parent=None):
598 standard.Dialog.__init__(self, parent=parent)
599 if parent is not None:
600 self.setWindowModality(Qt.WindowModal)
602 # updated for use by commands
603 self.context = context
604 self.model = model
606 # widgets
607 self.tree = GitTreeWidget(parent=self)
608 self.close_button = qtutils.close_button()
610 text = N_('Save')
611 self.save = qtutils.create_button(text=text, enabled=False, default=True)
613 # layouts
614 self.btnlayt = qtutils.hbox(
615 defs.margin, defs.spacing, self.close_button, qtutils.STRETCH, self.save
618 self.layt = qtutils.vbox(defs.margin, defs.spacing, self.tree, self.btnlayt)
619 self.setLayout(self.layt)
621 # connections
622 self.tree.path_chosen.connect(self.save_path)
624 self.tree.selection_changed.connect(
625 self.selection_changed, type=Qt.QueuedConnection
628 qtutils.connect_button(self.close_button, self.close)
629 qtutils.connect_button(self.save, self.save_blob)
630 self.init_size(parent=parent)
632 def expandAll(self):
633 self.tree.expandAll()
635 def setModel(self, model):
636 self.tree.setModel(model)
638 def path_chosen(self, path, close=True):
639 """Update the model from the view"""
640 model = self.model
641 model.relpath = path
642 model.filename = path
643 if close:
644 self.accept()
646 def save_path(self, path):
647 """Choose an output filename based on the selected path"""
648 self.path_chosen(path, close=False)
649 if save_path(self.context, path, self.model):
650 self.accept()
652 def save_blob(self):
653 """Save the currently selected file"""
654 filenames = self.tree.selected_files()
655 if not filenames:
656 return
657 self.save_path(filenames[0])
659 def selection_changed(self):
660 """Update actions based on the current selection"""
661 filenames = self.tree.selected_files()
662 self.save.setEnabled(bool(filenames))
665 # pylint: disable=too-many-ancestors
666 class GitTreeWidget(standard.TreeView):
667 selection_changed = Signal()
668 path_chosen = Signal(object)
670 def __init__(self, parent=None):
671 standard.TreeView.__init__(self, parent)
672 self.setHeaderHidden(True)
673 # pylint: disable=no-member
674 self.doubleClicked.connect(self.double_clicked)
676 def double_clicked(self, index):
677 item = self.model().itemFromIndex(index)
678 if item is None:
679 return
680 if item.is_dir:
681 return
682 self.path_chosen.emit(item.path)
684 def selected_files(self):
685 items = self.selected_items()
686 return [i.path for i in items if not i.is_dir]
688 def selectionChanged(self, old_selection, new_selection):
689 QtWidgets.QTreeView.selectionChanged(self, old_selection, new_selection)
690 self.selection_changed.emit()
692 def select_first_file(self):
693 """Select the first filename in the tree"""
694 model = self.model()
695 idx = self.indexAt(QtCore.QPoint(0, 0))
696 item = model.itemFromIndex(idx)
697 while idx and idx.isValid() and item and item.is_dir:
698 idx = self.indexBelow(idx)
699 item = model.itemFromIndex(idx)
701 if idx and idx.isValid() and item:
702 self.setCurrentIndex(idx)
705 class GitFileTreeModel(QtGui.QStandardItemModel):
706 """Presents a list of file paths as a hierarchical tree."""
708 def __init__(self, parent):
709 QtGui.QStandardItemModel.__init__(self, parent)
710 self.dir_entries = {'': self.invisibleRootItem()}
711 self.dir_rows = {}
713 def clear(self):
714 QtGui.QStandardItemModel.clear(self)
715 self.dir_rows = {}
716 self.dir_entries = {'': self.invisibleRootItem()}
718 def add_files(self, files):
719 """Add a list of files"""
720 add_file = self.add_file
721 for f in files:
722 add_file(f)
724 def add_file(self, path):
725 """Add a file to the model."""
726 dirname = utils.dirname(path)
727 dir_entries = self.dir_entries
728 try:
729 parent = dir_entries[dirname]
730 except KeyError:
731 parent = dir_entries[dirname] = self.create_dir_entry(dirname)
733 row_items = create_row(path, False)
734 parent.appendRow(row_items)
736 def add_directory(self, parent, path):
737 """Add a directory entry to the model."""
738 # Create model items
739 row_items = create_row(path, True)
741 try:
742 parent_path = parent.path
743 except AttributeError: # root QStandardItem
744 parent_path = ''
746 # Insert directories before file paths
747 try:
748 row = self.dir_rows[parent_path]
749 except KeyError:
750 row = self.dir_rows[parent_path] = 0
752 parent.insertRow(row, row_items)
753 self.dir_rows[parent_path] += 1
754 self.dir_entries[path] = row_items[0]
756 return row_items[0]
758 def create_dir_entry(self, dirname):
760 Create a directory entry for the model.
762 This ensures that directories are always listed before files.
765 entries = dirname.split('/')
766 curdir = []
767 parent = self.invisibleRootItem()
768 curdir_append = curdir.append
769 self_add_directory = self.add_directory
770 dir_entries = self.dir_entries
771 for entry in entries:
772 curdir_append(entry)
773 path = '/'.join(curdir)
774 try:
775 parent = dir_entries[path]
776 except KeyError:
777 grandparent = parent
778 parent = self_add_directory(grandparent, path)
779 dir_entries[path] = parent
780 return parent
783 def create_row(path, is_dir):
784 """Return a list of items representing a row."""
785 return [GitTreeItem(path, is_dir)]
788 class GitTreeModel(GitFileTreeModel):
789 def __init__(self, context, ref, parent):
790 GitFileTreeModel.__init__(self, parent)
791 self.context = context
792 self.ref = ref
793 self._initialize()
795 def _initialize(self):
796 """Iterate over git-ls-tree and create GitTreeItems."""
797 git = self.context.git
798 status, out, err = git.ls_tree('--full-tree', '-r', '-t', '-z', self.ref)
799 if status != 0:
800 Interaction.log_status(status, out, err)
801 return
803 if not out:
804 return
806 for line in out[:-1].split('\0'):
807 # .....6 ...4 ......................................40
808 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
809 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
810 objtype = line[7]
811 relpath = line[6 + 1 + 4 + 1 + 40 + 1 :]
812 if objtype == 't':
813 parent = self.dir_entries[utils.dirname(relpath)]
814 self.add_directory(parent, relpath)
815 elif objtype == 'b':
816 self.add_file(relpath)
819 class GitTreeItem(QtGui.QStandardItem):
821 Represents a cell in a treeview.
823 Many GitRepoItems could map to a single repository path,
824 but this tree only has a single column.
825 Each GitRepoItem manages a different cell in the tree view.
829 def __init__(self, path, is_dir):
830 QtGui.QStandardItem.__init__(self)
831 self.is_dir = is_dir
832 self.path = path
833 self.setEditable(False)
834 self.setDragEnabled(False)
835 self.setText(utils.basename(path))
836 if is_dir:
837 icon = icons.directory()
838 else:
839 icon = icons.file_text()
840 self.setIcon(icon)