From cab6d924aacfd7ee9052e770897a73e9307c47de Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Fri, 12 Feb 2010 15:39:29 +0100 Subject: [PATCH] Adapted the Qt codebase to the addition of core.gui.result_tree and core.gui.stats_label. --- cocoa/base/DetailsPanel.m | 7 ++ cocoa/base/DirectoryOutline.m | 7 ++ cocoa/base/ResultOutline.m | 2 + cocoa/base/StatsLabel.m | 2 + core/gui/details_panel.py | 5 +- core/gui/directory_tree.py | 4 +- core/gui/result_tree.py | 6 +- core/gui/stats_label.py | 5 +- qt/base/app.py | 51 +--------- qt/base/details_dialog.py | 1 + qt/base/directories_model.py | 1 + qt/base/main_window.py | 54 ++-------- qt/base/results_model.py | 185 ++++++++++++++-------------------- qt/base/stats_label.py | 20 ++++ 14 files changed, 142 insertions(+), 208 deletions(-) create mode 100644 qt/base/stats_label.py diff --git a/cocoa/base/DetailsPanel.m b/cocoa/base/DetailsPanel.m index 7019e65e..25ea545c 100644 --- a/cocoa/base/DetailsPanel.m +++ b/cocoa/base/DetailsPanel.m @@ -14,9 +14,16 @@ http://www.hardcoded.net/licenses/hs_license { self = [super initWithNibName:@"DetailsPanel" pyClassName:@"PyDetailsPanel" pyParent:aPy]; [self window]; //So the detailsTable is initialized. + [self connect]; return self; } +- (void)dealloc +{ + [self disconnect]; + [super dealloc]; +} + - (PyDetailsPanel *)py { return (PyDetailsPanel *)py; diff --git a/cocoa/base/DirectoryOutline.m b/cocoa/base/DirectoryOutline.m index 092f5f1c..516d2e7d 100644 --- a/cocoa/base/DirectoryOutline.m +++ b/cocoa/base/DirectoryOutline.m @@ -13,9 +13,16 @@ http://www.hardcoded.net/licenses/hs_license { self = [super initWithPyClassName:@"PyDirectoryOutline" pyParent:aPyParent view:aOutlineView]; [outlineView registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; + [self connect]; return self; } +- (void)dealloc +{ + [self disconnect]; + [super dealloc]; +} + - (PyDirectoryOutline *)py { return (PyDirectoryOutline *)py; diff --git a/cocoa/base/ResultOutline.m b/cocoa/base/ResultOutline.m index db4db114..be3a287b 100644 --- a/cocoa/base/ResultOutline.m +++ b/cocoa/base/ResultOutline.m @@ -16,11 +16,13 @@ http://www.hardcoded.net/licenses/hs_license { self = [super initWithPyClassName:@"PyResultOutline" pyParent:aPyParent view:aOutlineView]; _rootChildrenCounts = nil; + [self connect]; return self; } - (void)dealloc { + [self disconnect]; [_deltaColumns release]; [super dealloc]; } diff --git a/cocoa/base/StatsLabel.m b/cocoa/base/StatsLabel.m index 87a54015..aebd82a4 100644 --- a/cocoa/base/StatsLabel.m +++ b/cocoa/base/StatsLabel.m @@ -14,11 +14,13 @@ http://www.hardcoded.net/licenses/hs_license { self = [super initWithPyClassName:@"PyStatsLabel" pyParent:aPyParent]; labelView = [aLabelView retain]; + [self connect]; return self; } - (void)dealloc { + [self disconnect]; [labelView release]; [super dealloc]; } diff --git a/core/gui/details_panel.py b/core/gui/details_panel.py index ca9f83b0..3f42324b 100644 --- a/core/gui/details_panel.py +++ b/core/gui/details_panel.py @@ -13,8 +13,11 @@ class DetailsPanel(GUIObject): def __init__(self, view, app): GUIObject.__init__(self, view, app) self._table = [] + + def connect(self): + GUIObject.connect(self) self._refresh() - self.connect() + self.view.refresh() #--- Private def _refresh(self): diff --git a/core/gui/directory_tree.py b/core/gui/directory_tree.py index d0534536..d956adb6 100644 --- a/core/gui/directory_tree.py +++ b/core/gui/directory_tree.py @@ -53,7 +53,9 @@ class DirectoryTree(GUIObject, Tree): def __init__(self, view, app): GUIObject.__init__(self, view, app) Tree.__init__(self) - self.connect() + + def connect(self): + GUIObject.connect(self) self._refresh() self.view.refresh() diff --git a/core/gui/result_tree.py b/core/gui/result_tree.py index e1e60246..b5796ad0 100644 --- a/core/gui/result_tree.py +++ b/core/gui/result_tree.py @@ -54,11 +54,13 @@ class ResultTree(GUIObject, Tree): self._power_marker = False self._delta_values = False self._sort_descriptors = (0, True) - self.connect() + + #--- Override + def connect(self): + GUIObject.connect(self) self._refresh() self.view.refresh() - #--- Override def _select_nodes(self, nodes): Tree._select_nodes(self, nodes) self.app._select_dupes(map(attrgetter('_dupe'), nodes)) diff --git a/core/gui/stats_label.py b/core/gui/stats_label.py index ed1bb232..cabf9dd3 100644 --- a/core/gui/stats_label.py +++ b/core/gui/stats_label.py @@ -10,9 +10,8 @@ from .base import GUIObject class StatsLabel(GUIObject): - def __init__(self, view, app): - GUIObject.__init__(self, view, app) - self.connect() + def connect(self): + GUIObject.connect(self) self.view.refresh() @property diff --git a/qt/base/app.py b/qt/base/app.py index 4219801d..b15006b0 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -12,13 +12,12 @@ import logging import os import os.path as op -from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL -from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox +from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, SIGNAL +from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox from hsutil import job from hsutil.reg import RegistrationRequired -from core import fs from core.app import DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE from qtlib.about_box import AboutBox @@ -148,10 +147,6 @@ class DupeGuru(DupeGuruBase, QObject): if self.main_window._confirm(title, msg): DupeGuruBase.add_selected_to_ignore_list(self) - def apply_filter(self, filter): - DupeGuruBase.apply_filter(self, filter) - self.emit(SIGNAL('resultsChanged()')) - @demo_method def copy_or_move_marked(self, copy): opname = 'copy' if copy else 'move' @@ -165,14 +160,6 @@ class DupeGuru(DupeGuruBase, QObject): delete_marked = demo_method(DupeGuruBase.delete_marked) - def make_selected_reference(self): - DupeGuruBase.make_selected_reference(self) - self.emit(SIGNAL('resultsChanged()')) - - def remove_duplicates(self, duplicates): - DupeGuruBase.remove_duplicates(self, duplicates) - self.emit(SIGNAL('resultsChanged()')) - def remove_selected(self): dupes = self.without_ref(self.selected_dupes) if not dupes: @@ -186,37 +173,10 @@ class DupeGuru(DupeGuruBase, QObject): def askForRegCode(self): self.reg.ask_for_code() - def mark_all(self): - self.results.mark_all() - self.emit(SIGNAL('dupeMarkingChanged()')) - - def mark_invert(self): - self.results.mark_invert() - self.emit(SIGNAL('dupeMarkingChanged()')) - - def mark_none(self): - self.results.mark_none() - self.emit(SIGNAL('dupeMarkingChanged()')) - def openDebugLog(self): debugLogPath = op.join(self.appdata, 'debug.log') self._open_path(debugLogPath) - def remove_marked_duplicates(self): - marked = [d for d in self.results.dupes if self.results.is_marked(d)] - self.remove_duplicates(marked) - - def rename_dupe(self, dupe, newname): - try: - dupe.rename(newname) - return True - except (IndexError, fs.FSError) as e: - logging.warning("dupeGuru Warning: %s" % unicode(e)) - return False - - def select_dupes(self, dupes): - self._select_dupes(dupes) - def show_about_box(self): self.about_box.show() @@ -238,11 +198,6 @@ class DupeGuru(DupeGuruBase, QObject): self.prefs.save() self._update_options() - def toggle_marking_for_dupes(self, dupes): - for dupe in dupes: - self.results.mark_toggle(dupe) - self.emit(SIGNAL('dupeMarkingChanged()')) - #--- Events def application_will_terminate(self): self.save() @@ -253,7 +208,7 @@ class DupeGuru(DupeGuruBase, QObject): self.reg.show_nag() def job_finished(self, jobid): - self.emit(SIGNAL('resultsChanged()')) + self._job_completed(jobid) if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0: msg = "{0} files could not be processed.".format(self.results.mark_count) QMessageBox.warning(self.main_window, 'Warning', msg) diff --git a/qt/base/details_dialog.py b/qt/base/details_dialog.py index 61cba4a8..00c36c68 100644 --- a/qt/base/details_dialog.py +++ b/qt/base/details_dialog.py @@ -23,6 +23,7 @@ class DetailsDialog(QDialog): self.tableModel = DetailsModel(self.model) # tableView is defined in subclasses self.tableView.setModel(self.tableModel) + self.model.connect() def _setupUi(self): # Virtual pass diff --git a/qt/base/directories_model.py b/qt/base/directories_model.py index 1787b9c1..52d90485 100644 --- a/qt/base/directories_model.py +++ b/qt/base/directories_model.py @@ -64,6 +64,7 @@ class DirectoriesModel(TreeModel): def __init__(self, app): TreeModel.__init__(self) self.model = DirectoryTree(self, app) + self.model.connect() def _createNode(self, ref, row): return RefNode(self, None, ref, row) diff --git a/qt/base/main_window.py b/qt/base/main_window.py index 862784ed..2ada1c83 100644 --- a/qt/base/main_window.py +++ b/qt/base/main_window.py @@ -8,7 +8,7 @@ from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL, QUrl from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView, - QMessageBox, QInputDialog, QLineEdit, QItemSelectionModel, QDesktopServices) + QMessageBox, QInputDialog, QLineEdit, QDesktopServices) from hsutil.misc import nonone @@ -16,7 +16,8 @@ from core.app import NoScannableFileError, AllFilesAreRefError import dg_rc from main_window_ui import Ui_MainWindow -from results_model import ResultsDelegate, ResultsModel +from results_model import ResultsModel +from stats_label import StatsLabel class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, app): @@ -24,23 +25,16 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app = app self._last_filter = None self._setupUi() - self.resultsDelegate = ResultsDelegate() - self.resultsModel = ResultsModel(self.app) - self.resultsView.setModel(self.resultsModel) - self.resultsView.setItemDelegate(self.resultsDelegate) + self.resultsModel = ResultsModel(self.app, self.resultsView) + self.stats = StatsLabel(app, self.statusLabel) self._load_columns() self._update_column_actions_status() - self.resultsView.expandAll() - self._update_status_line() - self.connect(self.app, SIGNAL('resultsChanged()'), self.resultsChanged) - self.connect(self.app, SIGNAL('dupeMarkingChanged()'), self.dupeMarkingChanged) self.connect(self.actionQuit, SIGNAL('triggered()'), QCoreApplication.instance().quit) - self.connect(self.resultsView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled) self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) - self.connect(self.resultsModel, SIGNAL('modelReset()'), self.resultsReset) self.connect(self.resultsView, SIGNAL('doubleClicked()'), self.resultsDoubleClicked) + self.connect(self.resultsView, SIGNAL('spacePressed()'), self.resultsSpacePressed) def _setupUi(self): self.setupUi(self) @@ -108,11 +102,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): h.setSectionHidden(index, not visible) h.setResizeMode(0, QHeaderView.Stretch) - def _redraw_results(self): - # HACK. this is the only way I found to update the widget without reseting everything - self.resultsView.scroll(0, 1) - self.resultsView.scroll(0, -1) - def _save_columns(self): h = self.resultsView.header() widths = [] @@ -131,9 +120,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): colid = action.column_index action.setChecked(not h.isSectionHidden(colid)) - def _update_status_line(self): - self.statusLabel.setText(self.app.stat_line) - #--- Actions def aboutTriggered(self): self.app.show_about_box() @@ -185,8 +171,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.delete_marked() def deltaTriggered(self): - self.resultsModel.delta = self.actionDelta.isChecked() - self._redraw_results() + self.resultsModel.delta_values = self.actionDelta.isChecked() def detailsTriggered(self): self.app.show_details() @@ -217,8 +202,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.mark_none() def markSelectedTriggered(self): - dupes = self.resultsView.selectedDupes() - self.app.toggle_marking_for_dupes(dupes) + self.app.toggle_selected_mark_state() def moveTriggered(self): self.app.copy_or_move_marked(False) @@ -245,7 +229,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): title = "Remove duplicates" msg = "You are about to remove {0} files from results. Continue?".format(count) if self._confirm(title, msg): - self.app.remove_marked_duplicates() + self.app.remove_marked() def removeSelectedTriggered(self): self.app.remove_selected() @@ -292,25 +276,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): def contextMenuEvent(self, event): self.actionActions.menu().exec_(event.globalPos()) - def dupeMarkingChanged(self): - self._redraw_results() - self._update_status_line() - - def resultsChanged(self): - self.resultsView.model().reset() - def resultsDoubleClicked(self): self.app.open_selected() - def resultsReset(self): - self.resultsView.expandAll() - if self.app.selected_dupes: - [modelIndex] = self.resultsModel.indexesForDupes(self.app.selected_dupes[:1]) - if modelIndex.isValid(): - flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows - self.resultsView.selectionModel().setCurrentIndex(modelIndex, flags) - self._update_status_line() - - def selectionChanged(self, selected, deselected): - self.app.select_dupes(self.resultsView.selectedDupes()) + def resultsSpacePressed(self): + self.app.toggle_selected_mark_state() diff --git a/qt/base/results_model.py b/qt/base/results_model.py index cf7f83c9..fa2db241 100644 --- a/qt/base/results_model.py +++ b/qt/base/results_model.py @@ -6,74 +6,62 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QModelIndex, QRect -from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import (QBrush, QStyledItemDelegate, QFont, QTreeView, QColor, QItemSelectionModel, + QItemSelection) -from qtlib.tree_model import TreeNode, TreeModel +from qtlib.tree_model import TreeModel, RefNode -class ResultNode(TreeNode): - def __init__(self, model, parent, row, dupe, group): - TreeNode.__init__(self, model, parent, row) - self.dupe = dupe - self.group = group - self._normalData = None - self._deltaData = None - - def _createNode(self, ref, row): - return ResultNode(self.model, self, row, ref, self.group) - - def _getChildren(self): - return self.group.dupes if self.dupe is self.group.ref else [] - - def invalidate(self): - self._normalData = None - self._deltaData = None - TreeNode.invalidate(self) - - @property - def normalData(self): - if self._normalData is None: - self._normalData = self.model._app._get_display_info(self.dupe, self.group, delta=False) - return self._normalData - - @property - def deltaData(self): - if self._deltaData is None: - self._deltaData = self.model._app._get_display_info(self.dupe, self.group, delta=True) - return self._deltaData - +from core.gui.result_tree import ResultTree as ResultTreeModel class ResultsDelegate(QStyledItemDelegate): def initStyleOption(self, option, index): QStyledItemDelegate.initStyleOption(self, option, index) node = index.internalPointer() - if node.group.ref is node.dupe: + ref = node.ref + if ref._group.ref is ref._dupe: newfont = QFont(option.font) newfont.setBold(True) option.font = newfont class ResultsModel(TreeModel): - def __init__(self, app): + def __init__(self, app, view): + TreeModel.__init__(self) + self.view = view self._app = app - self._results = app.results self._data = app.data self._delta_columns = app.DELTA_COLUMNS - self.delta = False - self._power_marker = False - TreeModel.__init__(self) + self.resultsDelegate = ResultsDelegate() + self.model = ResultTreeModel(self, app) + self.view.setItemDelegate(self.resultsDelegate) + self.view.setModel(self) + self.model.connect() + + self.connect(self.view.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) def _createNode(self, ref, row): - if self.power_marker: - # ref is a dupe - group = self._results.get_group_of_duplicate(ref) - return ResultNode(self, None, row, ref, group) - else: - # ref is a group - return ResultNode(self, None, row, ref.ref, ref) + return RefNode(self, None, ref, row) def _getChildren(self): - return self._results.dupes if self.power_marker else self._results.groups + return list(self.model) + + def _updateSelection(self): + selectedIndexes = [] + for path in self.model.selected_paths: + modelIndex = self.findIndex(path) + if modelIndex.isValid(): + selectedIndexes.append(modelIndex) + if selectedIndexes: + selection = QItemSelection() + for modelIndex in selectedIndexes: + selection.select(modelIndex, modelIndex) + flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows + self.view.selectionModel().select(selection, flags) + flags = QItemSelectionModel.Rows + self.view.selectionModel().setCurrentIndex(selectedIndexes[0], flags) + else: + self.view.selectionModel().clear() def columnCount(self, parent): return len(self._data.COLUMNS) @@ -82,51 +70,23 @@ class ResultsModel(TreeModel): if not index.isValid(): return None node = index.internalPointer() + ref = node.ref if role == Qt.DisplayRole: - data = node.deltaData if self.delta else node.normalData + data = ref.data_delta if self.model.delta_values else ref.data return data[index.column()] elif role == Qt.CheckStateRole: - if index.column() == 0 and node.dupe is not node.group.ref: - state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked - return state + if index.column() == 0 and ref.markable: + return Qt.Checked if ref.marked else Qt.Unchecked elif role == Qt.ForegroundRole: - if node.dupe is node.group.ref or node.dupe.is_ref: + if ref._dupe is ref._group.ref or ref._dupe.is_ref: return QBrush(Qt.blue) - elif self.delta and index.column() in self._delta_columns: + elif self.model.delta_values and index.column() in self._delta_columns: return QBrush(QColor(255, 142, 40)) # orange elif role == Qt.EditRole: if index.column() == 0: - return node.normalData[index.column()] + return ref.data[index.column()] return None - def dupesForIndexes(self, indexes): - nodes = [index.internalPointer() for index in indexes] - return [node.dupe for node in nodes] - - def indexesForDupes(self, dupes): - def index(dupe): - try: - if self.power_marker: - row = self._results.dupes.index(dupe) - node = self.subnodes[row] - assert node.dupe is dupe - return self.createIndex(row, 0, node) - else: - group = self._results.get_group_of_duplicate(dupe) - row = self._results.groups.index(group) - node = self.subnodes[row] - if dupe is group.ref: - assert node.dupe is dupe - return self.createIndex(row, 0, node) - subrow = group.dupes.index(dupe) - subnode = node.subnodes[subrow] - assert subnode.dupe is dupe - return self.createIndex(subrow, 0, subnode) - except ValueError: # the dupe is not there anymore - return QModelIndex() - - return map(index, dupes) - def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled @@ -144,48 +104,61 @@ class ResultsModel(TreeModel): if not index.isValid(): return False node = index.internalPointer() + ref = node.ref if role == Qt.CheckStateRole: if index.column() == 0: - self._app.toggle_marking_for_dupes([node.dupe]) + self._app.mark_dupe(ref._dupe, value.toBool()) return True if role == Qt.EditRole: if index.column() == 0: value = unicode(value.toString()) - if self._app.rename_dupe(node.dupe, value): - node.invalidate() - return True + return self.model.rename_selected(value) return False def sort(self, column, order): - if self.power_marker: - self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta) - else: - self._results.sort_groups(column, order == Qt.AscendingOrder) - self.reset() - - def toggleMarked(self, indexes): - assert indexes - dupes = self.dupesForIndexes(indexes) - self._app.toggle_marking_for_dupes(dupes) + self.model.sort(column, order == Qt.AscendingOrder) #--- Properties @property def power_marker(self): - return self._power_marker + return self.model.power_marker @power_marker.setter def power_marker(self, value): - if value == self._power_marker: - return - self._power_marker = value + self.model.power_marker = value + + @property + def delta_values(self): + return self.model.delta_values + + @delta_values.setter + def delta_values(self, value): + self.model.delta_values = value + + #--- Events + def selectionChanged(self, selected, deselected): + indexes = self.view.selectionModel().selectedRows() + nodes = [index.internalPointer() for index in indexes] + self.model.selected_nodes = [node.ref for node in nodes] + + #--- model --> view + def refresh(self): self.reset() + self._updateSelection() + self.view.expandAll() + + def invalidate_markings(self): + # redraw view + # HACK. this is the only way I found to update the widget without reseting everything + self.view.scroll(0, 1) + self.view.scroll(0, -1) class ResultsView(QTreeView): #--- Override def keyPressEvent(self, event): if event.text() == ' ': - self.model().toggleMarked(self.selectionModel().selectedRows()) + self.emit(SIGNAL('spacePressed()')) return QTreeView.keyPressEvent(self, event) @@ -193,11 +166,3 @@ class ResultsView(QTreeView): self.emit(SIGNAL('doubleClicked()')) # We don't call the superclass' method because the default behavior is to rename the cell. - def setModel(self, model): - assert isinstance(model, ResultsModel) - QTreeView.setModel(self, model) - - #--- Public - def selectedDupes(self): - return self.model().dupesForIndexes(self.selectionModel().selectedRows()) - diff --git a/qt/base/stats_label.py b/qt/base/stats_label.py new file mode 100644 index 00000000..05a6c62f --- /dev/null +++ b/qt/base/stats_label.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2010-02-12 +# Copyright 2010 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from core.gui.stats_label import StatsLabel as StatsLabelModel + +class StatsLabel(object): + def __init__(self, app, view): + self.view = view + self.model = StatsLabelModel(self, app) + self.model.connect() + + def refresh(self): + self.view.setText(self.model.display) +