Merge branch 'details_dialog_improvements' into dev

This commit is contained in:
glubsy 2020-10-27 16:23:23 +01:00
commit 6623b04403
30 changed files with 1848 additions and 162 deletions

4
.gitignore vendored
View File

@ -22,4 +22,6 @@ cocoa/autogen
*.pyd
*.exe
*.spec
*.spec
.vscode

BIN
images/exchange.icns Normal file

Binary file not shown.

BIN
images/exchange.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
images/exchange.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

BIN
images/exchange_purple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/old_zoom_in.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/old_zoom_out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -7,7 +7,7 @@
import sys
import os.path as op
from PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal
from PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
@ -58,11 +58,11 @@ class DupeGuru(QObject):
def _setup(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
self._setupActions()
self.details_dialog = None
self._update_options()
self.recentResults = Recent(self, "recentResults")
self.recentResults.mustOpenItem.connect(self.model.load_from)
self.resultWindow = None
self.details_dialog = None
if self.use_tabs:
self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self)
parent_window = self.main_window
@ -177,6 +177,9 @@ class DupeGuru(QObject):
self.model.options["match_scaled"] = self.prefs.match_scaled
self.model.options["picture_cache_type"] = self.prefs.picture_cache_type
if self.details_dialog:
self.details_dialog.update_options()
# --- Private
def _get_details_dialog_class(self):
if self.model.app_mode == AppMode.Picture:
@ -212,22 +215,22 @@ class DupeGuru(QObject):
def show_details(self):
if self.details_dialog is not None:
self.details_dialog.show()
if not self.details_dialog.isVisible():
self.details_dialog.show()
else:
self.details_dialog.hide()
def showResultsWindow(self):
if self.resultWindow is not None:
if self.use_tabs:
self.main_window.addTab(
self.resultWindow, "Results", switch=True)
self.main_window.showTab(self.resultWindow)
else:
self.resultWindow.show()
def showDirectoriesWindow(self):
if self.directories_dialog is not None:
if self.use_tabs:
index = self.main_window.indexOfWidget(self.directories_dialog)
self.main_window.setTabVisible(index, True)
self.main_window.setCurrentIndex(index)
self.main_window.showTab(self.directories_dialog)
else:
self.directories_dialog.show()
@ -295,6 +298,9 @@ class DupeGuru(QObject):
preferences_dialog.setParent(None)
def quitTriggered(self):
if self.details_dialog is not None:
self.details_dialog.close()
if self.main_window:
self.main_window.close()
else:
@ -333,14 +339,20 @@ class DupeGuru(QObject):
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``.
"""
if self.details_dialog is not None:
# The object is not deleted entirely, avoid saving its geometry in the future
# self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)
# or simply delete it on close which is probably cleaner:
self.details_dialog.setAttribute(Qt.WA_DeleteOnClose)
self.details_dialog.close()
self.details_dialog.setParent(None)
# self.details_dialog.setParent(None) # seems unnecessary
if self.resultWindow is not None:
self.resultWindow.close()
self.resultWindow.setParent(None)
if self.use_tabs:
self.resultWindow = self.main_window.createPage(
"ResultWindow", parent=self.main_window, app=self)
self.main_window.addTab(
self.resultWindow, "Results", switch=False)
else: # We don't use a tab widget, regular floating QMainWindow
self.resultWindow = ResultWindow(self.directories_dialog, self)
self.directories_dialog._updateActionsState()

View File

@ -7,34 +7,63 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QDockWidget, QWidget
from .details_table import DetailsModel
from hscommon.plat import ISLINUX
class DetailsDialog(QDialog):
class DetailsDialog(QDockWidget):
def __init__(self, parent, app, **kwargs):
super().__init__(parent, Qt.Tool, **kwargs)
self.parent = parent
self.app = app
self.model = app.model.details_panel
self.setAllowedAreas(Qt.AllDockWidgetAreas)
self._setupUi()
# To avoid saving uninitialized geometry on appWillSavePrefs, we track whether our dialog
# has been shown. If it has, we know that our geometry should be saved.
self._shown_once = False
self.app.prefs.restoreGeometry("DetailsWindowRect", self)
self.tableModel = DetailsModel(self.model)
self._wasDocked, area = self.app.prefs.restoreGeometry("DetailsWindowRect", self)
self.tableModel = DetailsModel(self.model, app)
# tableView is defined in subclasses
self.tableView.setModel(self.tableModel)
self.model.view = self
self.app.willSavePrefs.connect(self.appWillSavePrefs)
# self.setAttribute(Qt.WA_DeleteOnClose)
parent.addDockWidget(
area if self._wasDocked else Qt.BottomDockWidgetArea, self)
def _setupUi(self): # Virtual
pass
def show(self):
if not self._shown_once and self._wasDocked:
self.setFloating(False)
self._shown_once = True
super().show()
self.update_options()
def update_options(self):
# This disables the title bar (if we had not set one before already)
# essentially making it a simple floating window, not dockable anymore
if not self.app.prefs.details_dialog_titlebar_enabled:
if not self.titleBarWidget(): # default title bar
self.setTitleBarWidget(QWidget()) # disables title bar
# Windows (and MacOS?) users cannot move a floating window which
# has not native decoration so we force it to dock for now
if not ISLINUX:
self.setFloating(False)
elif self.titleBarWidget() is not None: # title bar is disabled
self.setTitleBarWidget(None) # resets to the default title bar
elif not self.titleBarWidget() and not self.app.prefs.details_dialog_titlebar_enabled:
self.setTitleBarWidget(QWidget())
features = self.features()
if self.app.prefs.details_dialog_vertical_titlebar:
self.setFeatures(features | QDockWidget.DockWidgetVerticalTitleBar)
elif features & QDockWidget.DockWidgetVerticalTitleBar:
self.setFeatures(features ^ QDockWidget.DockWidgetVerticalTitleBar)
# --- Events
def appWillSavePrefs(self):

View File

@ -8,7 +8,7 @@
from PyQt5.QtCore import Qt, QAbstractTableModel
from PyQt5.QtWidgets import QHeaderView, QTableView
from PyQt5.QtGui import QFont, QBrush, QColor
from PyQt5.QtGui import QFont, QBrush
from hscommon.trans import trget
@ -18,9 +18,10 @@ HEADER = [tr("Selected"), tr("Reference")]
class DetailsModel(QAbstractTableModel):
def __init__(self, model, **kwargs):
def __init__(self, model, app, **kwargs):
super().__init__(**kwargs)
self.model = model
self.prefs = app.prefs
def columnCount(self, parent):
return len(HEADER)
@ -43,7 +44,7 @@ class DetailsModel(QAbstractTableModel):
if role == Qt.DisplayRole:
return self.model.row(row)[column]
if role == Qt.ForegroundRole and self.model.row(row)[1] != self.model.row(row)[2]:
return QBrush(QColor(250, 20, 20)) # red
return QBrush(self.prefs.details_table_delta_foreground_color)
if role == Qt.FontRole and self.model.row(row)[1] != self.model.row(row)[2]:
font = QFont(self.model.view.font()) # or simply QFont()
font.setBold(True)
@ -85,8 +86,6 @@ class DetailsTable(QTableView):
# The model needs to be set to set header stuff
hheader = self.horizontalHeader()
hheader.setHighlightSections(False)
hheader.setStretchLastSection(False)
hheader.resizeSection(0, 100)
hheader.setSectionResizeMode(0, QHeaderView.Stretch)
hheader.setSectionResizeMode(1, QHeaderView.Stretch)
vheader = self.verticalHeader()

View File

@ -5,5 +5,10 @@
<file alias="plus">../images/plus_8.png</file>
<file alias="minus">../images/minus_8.png</file>
<file alias="search_clear_13">../qtlib/images/search_clear_13.png</file>
<file alias="exchange">../images/exchange_purple_upscaled.png</file>
<file alias="zoom_in">../images/old_zoom_in.png</file>
<file alias="zoom_out">../images/old_zoom_out.png</file>
<file alias="zoom_original">../images/old_zoom_original.png</file>
<file alias="zoom_best_fit">../images/old_zoom_best_fit.png</file>
</qresource>
</RCC>

View File

@ -5,7 +5,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QAbstractItemView
from PyQt5.QtWidgets import QAbstractItemView
from hscommon.trans import trget
from ..details_dialog import DetailsDialog as DetailsDialogBase
@ -19,11 +19,14 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details"))
self.resize(502, 295)
self.setMinimumSize(QSize(250, 250))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
# self.verticalLayout = QVBoxLayout(self)
# self.verticalLayout.setSpacing(0)
# self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)
# self.verticalLayout.addWidget(self.tableView)
# self.centralWidget = QWidget()
# self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView)

View File

@ -76,7 +76,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.debugModeBox)
self._setupBottomPart()
def _load(self, prefs, setchecked):
def _load(self, prefs, setchecked, section):
setchecked(self.tagTrackBox, prefs.scan_tag_track)
setchecked(self.tagArtistBox, prefs.scan_tag_artist)
setchecked(self.tagAlbumBox, prefs.scan_tag_album)

View File

@ -4,115 +4,147 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QVBoxLayout,
QAbstractItemView,
QHBoxLayout,
QLabel,
QSizePolicy,
)
QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame)
from PyQt5.QtGui import QResizeEvent
from hscommon.trans import trget
from hscommon.plat import ISWINDOWS
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
from .image_viewer import (
ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController)
tr = trget("ui")
class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app):
DetailsDialogBase.__init__(self, parent, app)
self.selectedPixmap = None
self.referencePixmap = None
self.vController = None
self.app = app
super().__init__(parent, app)
def _setupUi(self):
self.setWindowTitle(tr("Details"))
self.resize(502, 295)
self.resize(502, 502)
self.setMinimumSize(QSize(250, 250))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setSpacing(4)
self.selectedImage = QLabel(self)
sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.selectedImage.sizePolicy().hasHeightForWidth()
)
self.selectedImage.setSizePolicy(sizePolicy)
self.selectedImage.setScaledContents(False)
self.selectedImage.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.selectedImage)
self.referenceImage = QLabel(self)
sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.referenceImage.sizePolicy().hasHeightForWidth()
)
self.referenceImage.setSizePolicy(sizePolicy)
self.referenceImage.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.referenceImage)
self.verticalLayout.addLayout(self.horizontalLayout)
self.splitter = QSplitter(Qt.Vertical)
self.topFrame = EmittingFrame()
self.topFrame.setFrameShape(QFrame.StyledPanel)
self.horizontalLayout = QGridLayout()
# Minimum width for the toolbar in the middle:
self.horizontalLayout.setColumnMinimumWidth(1, 10)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setColumnStretch(0, 32)
# Smaller value for the toolbar in the middle to avoid excessive resize
self.horizontalLayout.setColumnStretch(1, 2)
self.horizontalLayout.setColumnStretch(2, 32)
# This avoids toolbar getting incorrectly partially hidden when window resizes
self.horizontalLayout.setRowStretch(0, 1)
self.horizontalLayout.setRowStretch(1, 24)
self.horizontalLayout.setRowStretch(2, 1)
self.horizontalLayout.setSpacing(1) # probably not important
self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage")
self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1)
# Use a specific type of controller depending on the underlying viewer type
self.vController = ScrollAreaController(self)
self.verticalToolBar = ViewerToolBar(self, self.vController)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
self.topFrame.setLayout(self.horizontalLayout)
self.splitter.addWidget(self.topFrame)
self.splitter.setStretchFactor(0, 8)
self.tableView = DetailsTable(self)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
self.tableView.setSizePolicy(sizePolicy)
self.tableView.setMinimumSize(QSize(0, 188))
self.tableView.setMaximumSize(QSize(16777215, 190))
# self.tableView.setMinimumSize(QSize(0, 190))
# self.tableView.setMaximumSize(QSize(16777215, 190))
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)
self.splitter.addWidget(self.tableView)
self.splitter.setStretchFactor(1, 1)
# Late population needed here for connections to the toolbar
self.vController.setupViewers(
self.selectedImageViewer, self.referenceImageViewer)
# self.setCentralWidget(self.splitter) # only as QMainWindow
self.setWidget(self.splitter) # only as QDockWidget
self.topFrame.resized.connect(self.resizeEvent)
def _update(self):
if self.vController is None: # Not yet constructed!
return
if not self.app.model.selected_dupes:
# No item from the model, disable and clear everything.
self.vController.resetViewersState()
return
dupe = self.app.model.selected_dupes[0]
group = self.app.model.results.get_group_of_duplicate(dupe)
ref = group.ref
self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe:
self.referencePixmap = None
else:
self.referencePixmap = QPixmap(str(ref.path))
self._updateImages()
def _updateImages(self):
if self.selectedPixmap is not None:
target_size = self.selectedImage.size()
scaledPixmap = self.selectedPixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.selectedImage.setPixmap(scaledPixmap)
else:
self.selectedImage.setPixmap(QPixmap())
if self.referencePixmap is not None:
target_size = self.referenceImage.size()
scaledPixmap = self.referencePixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.referenceImage.setPixmap(scaledPixmap)
else:
self.referenceImage.setPixmap(QPixmap())
self.vController.updateView(ref, dupe, group)
# --- Override
@pyqtSlot(QResizeEvent)
def resizeEvent(self, event):
self._updateImages()
self.ensure_same_sizes()
if self.vController is None or not self.vController.bestFit:
return
# Only update the scaled down pixmaps
self.vController.updateBothImages()
def show(self):
# Compute the maximum size the table view can reach
# Assuming all rows below headers have the same height
self.tableView.setMaximumHeight(
self.tableView.rowHeight(1)
* self.tableModel.model.row_count()
+ self.tableView.verticalHeader().sectionSize(0)
# Windows seems to add a few pixels more to the table somehow
+ (5 if ISWINDOWS else 0))
DetailsDialogBase.show(self)
self.ensure_same_sizes()
self._update()
def ensure_same_sizes(self):
# HACK This ensures same size while shrinking.
# ReferenceViewer might be 1 pixel shorter in width
# due to the toolbar in the middle keeping the same width,
# so resizing in the GridLayout's engine leads to not enough space
# left for the panel on the right.
# This work as a QMainWindow, but doesn't work as a QDockWidget:
# resize can only grow. Might need some custom sizeHint somewhere...
# self.horizontalLayout.setColumnMinimumWidth(
# 0, self.selectedImageViewer.size().width())
# self.horizontalLayout.setColumnMinimumWidth(
# 2, self.selectedImageViewer.size().width())
# This works when expanding but it's ugly:
if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width():
# print(f"""Before selected size: {self.selectedImageViewer.size()}\n""",
# f"""Before reference size: {self.referenceImageViewer.size()}""")
self.selectedImageViewer.resize(self.referenceImageViewer.size())
# print(f"""After selected size: {self.selectedImageViewer.size()}\n""",
# f"""After reference size: {self.referenceImageViewer.size()}""")
# model --> view
def refresh(self):
DetailsDialogBase.refresh(self)
if self.isVisible():
self._update()
class EmittingFrame(QFrame):
"""Emits a signal whenever is resized"""
resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event):
self.resized.emit(event)

1370
qt/pe/image_viewer.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtCore import Qt
from hscommon.trans import trget
from hscommon.plat import ISLINUX
from qtlib.radio_box import RadioBox
from core.scanner import ScanType
from core.app import AppMode
@ -40,12 +42,35 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
self.widgetsVLayout.addWidget(self.debugModeBox)
self.widgetsVLayout.addWidget(QLabel(tr("Picture cache mode:")))
self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False)
self.widgetsVLayout.addWidget(self.cacheTypeRadio)
cache_form = QFormLayout()
cache_form.setLabelAlignment(Qt.AlignLeft)
cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio)
self.widgetsVLayout.addLayout(cache_form)
self._setupBottomPart()
def _load(self, prefs, setchecked):
def _setupDisplayPage(self):
super()._setupDisplayPage()
self._setupAddCheckbox("details_dialog_override_theme_icons",
tr("Override theme icons in viewer toolbar"))
self.details_dialog_override_theme_icons.setToolTip(
tr("Use our own internal icons instead of those provided by the theme engine"))
# Prevent changing this on platforms where themes are unpredictable
self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)
# Insert this right after the vertical title bar option
index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)
self.details_groupbox_layout.insertWidget(
index + 1, self.details_dialog_override_theme_icons)
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars",
tr("Show scrollbars in image viewers"))
self.details_dialog_viewers_show_scrollbars.setToolTip(
tr("When the image displayed doesn't fit the viewport, \
show scrollbars to span the view around"))
self.details_groupbox_layout.insertWidget(
index + 2, self.details_dialog_viewers_show_scrollbars)
def _load(self, prefs, setchecked, section):
setchecked(self.matchScaledBox, prefs.match_scaled)
self.cacheTypeRadio.selected_index = (
1 if prefs.picture_cache_type == "shelve" else 0
@ -55,9 +80,17 @@ class PreferencesDialog(PreferencesDialogBase):
scan_type = prefs.get_scan_type(AppMode.Picture)
fuzzy_scan = scan_type == ScanType.FuzzyBlock
self.filterHardnessSlider.setEnabled(fuzzy_scan)
setchecked(self.details_dialog_override_theme_icons,
prefs.details_dialog_override_theme_icons)
setchecked(self.details_dialog_viewers_show_scrollbars,
prefs.details_dialog_viewers_show_scrollbars)
def _save(self, prefs, ischecked):
prefs.match_scaled = ischecked(self.matchScaledBox)
prefs.picture_cache_type = (
"shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
)
prefs.details_dialog_override_theme_icons =\
ischecked(self.details_dialog_override_theme_icons)
prefs.details_dialog_viewers_show_scrollbars =\
ischecked(self.details_dialog_viewers_show_scrollbars)

View File

@ -5,8 +5,11 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from hscommon import trans
from hscommon.plat import ISLINUX
from core.app import AppMode
from core.scanner import ScanType
from qtlib.preferences import Preferences as PreferencesBase
@ -30,7 +33,25 @@ class Preferences(PreferencesBase):
self.language = trans.installed_lang
self.tableFontSize = get("TableFontSize", self.tableFontSize)
self.reference_bold_font = get('ReferenceBoldFont', self.reference_bold_font)
self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font)
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled",
self.details_dialog_titlebar_enabled)
self.details_dialog_vertical_titlebar = get("DetailsDialogVerticalTitleBar",
self.details_dialog_vertical_titlebar)
# On Windows and MacOS, use internal icons by default
self.details_dialog_override_theme_icons =\
get("DetailsDialogOverrideThemeIcons",
self.details_dialog_override_theme_icons) if ISLINUX else True
self.details_table_delta_foreground_color =\
get("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
self.details_dialog_viewers_show_scrollbars =\
get("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
self.result_table_ref_foreground_color =\
get("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
self.result_table_delta_foreground_color =\
get("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
self.resultWindowIsMaximized = get(
"ResultWindowIsMaximized", self.resultWindowIsMaximized
)
@ -72,6 +93,14 @@ class Preferences(PreferencesBase):
self.tableFontSize = QApplication.font().pointSize()
self.reference_bold_font = True
self.details_dialog_titlebar_enabled = True
self.details_dialog_vertical_titlebar = True
self.details_table_delta_foreground_color = QColor(250, 20, 20) # red
# By default use internal icons on platforms other than Linux for now
self.details_dialog_override_theme_icons = False if not ISLINUX else True
self.details_dialog_viewers_show_scrollbars = True
self.result_table_ref_foreground_color = QColor(Qt.blue)
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
self.resultWindowIsMaximized = False
self.resultWindowRect = None
self.directoriesWindowRect = None
@ -107,7 +136,14 @@ class Preferences(PreferencesBase):
set_("Language", self.language)
set_("TableFontSize", self.tableFontSize)
set_('ReferenceBoldFont', self.reference_bold_font)
set_("ReferenceBoldFont", self.reference_bold_font)
set_("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled)
set_("DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar)
set_("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons)
set_("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
set_("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
set_("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
set_("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
set_("ResultWindowIsMaximized", self.resultWindowIsMaximized)
set_("MainWindowIsMaximized", self.mainWindowIsMaximized)
self.set_rect("ResultWindowRect", self.resultWindowRect)

View File

@ -4,12 +4,13 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import (
QDialog,
QDialogButtonBox,
QVBoxLayout,
QHBoxLayout,
QGridLayout,
QLabel,
QComboBox,
QSlider,
@ -20,11 +21,20 @@ from PyQt5.QtWidgets import (
QMessageBox,
QSpinBox,
QLayout,
QTabWidget,
QWidget,
QColorDialog,
QPushButton,
QGroupBox,
QFormLayout,
)
from PyQt5.QtGui import QPixmap, QIcon
from hscommon.trans import trget
from hscommon.plat import ISLINUX
from qtlib.util import horizontalWrap
from qtlib.preferences import get_langnames
from enum import Flag, auto
from .preferences import Preferences
@ -50,6 +60,13 @@ SUPPORTED_LANGUAGES = [
]
class Sections(Flag):
"""Filter blocks of preferences when reset or loaded"""
GENERAL = auto()
DISPLAY = auto()
ALL = GENERAL | DISPLAY
class PreferencesDialogBase(QDialog):
def __init__(self, parent, app, **kwargs):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
@ -111,25 +128,6 @@ class PreferencesDialogBase(QDialog):
def _setupBottomPart(self):
# The bottom part of the pref panel is always the same in all editions.
self.fontSizeLabel = QLabel(tr("Font size:"))
self.fontSizeSpinBox = QSpinBox()
self.fontSizeSpinBox.setMinimum(5)
self.widgetsVLayout.addLayout(
horizontalWrap([self.fontSizeLabel, self.fontSizeSpinBox, None])
)
self._setupAddCheckbox("reference_bold_font", tr("Bold font for reference"))
self.widgetsVLayout.addWidget(self.reference_bold_font)
self._setupAddCheckbox("tabs_default_pos", tr("Use default position for tab bar (requires restart)"))
self.tabs_default_pos.setToolTip(tr("Place the tab bar below the main menu instead of next to it"))
self.widgetsVLayout.addWidget(self.tabs_default_pos)
self.languageLabel = QLabel(tr("Language:"), self)
self.languageComboBox = QComboBox(self)
for lang in self.supportedLanguages:
self.languageComboBox.addItem(get_langnames()[lang])
self.widgetsVLayout.addLayout(
horizontalWrap([self.languageLabel, self.languageComboBox, None])
)
self.copyMoveLabel = QLabel(self)
self.copyMoveLabel.setText(tr("Copy and Move:"))
self.widgetsVLayout.addWidget(self.copyMoveLabel)
@ -146,6 +144,74 @@ class PreferencesDialogBase(QDialog):
self.customCommandEdit = QLineEdit(self)
self.widgetsVLayout.addWidget(self.customCommandEdit)
def _setupDisplayPage(self):
self.ui_groupbox = QGroupBox("&General Interface")
layout = QVBoxLayout()
self.languageLabel = QLabel(tr("Language:"), self)
self.languageComboBox = QComboBox(self)
for lang in self.supportedLanguages:
self.languageComboBox.addItem(get_langnames()[lang])
layout.addLayout(horizontalWrap([self.languageLabel, self.languageComboBox, None]))
self._setupAddCheckbox("tabs_default_pos",
tr("Use default position for tab bar (requires restart)"))
self.tabs_default_pos.setToolTip(
tr("Place the tab bar below the main menu instead of next to it\n\
On MacOS, the tab bar will fill up the window's width instead."))
layout.addWidget(self.tabs_default_pos)
self.ui_groupbox.setLayout(layout)
self.displayVLayout.addWidget(self.ui_groupbox)
gridlayout = QFormLayout()
result_groupbox = QGroupBox("&Result Table")
self.fontSizeSpinBox = QSpinBox()
self.fontSizeSpinBox.setMinimum(5)
gridlayout.addRow(tr("Font size:"), self.fontSizeSpinBox)
self._setupAddCheckbox("reference_bold_font",
tr("Use bold font for references"))
gridlayout.addRow(self.reference_bold_font)
self.result_table_ref_foreground_color = ColorPickerButton(self)
gridlayout.addRow(tr("Reference foreground color:"),
self.result_table_ref_foreground_color)
self.result_table_delta_foreground_color = ColorPickerButton(self)
gridlayout.addRow(tr("Delta foreground color:"),
self.result_table_delta_foreground_color)
gridlayout.setLabelAlignment(Qt.AlignLeft)
# Keep same vertical spacing as parent layout for consistency
gridlayout.setVerticalSpacing(self.displayVLayout.spacing())
result_groupbox.setLayout(gridlayout)
self.displayVLayout.addWidget(result_groupbox)
details_groupbox = QGroupBox("&Details Window")
self.details_groupbox_layout = QVBoxLayout()
self._setupAddCheckbox("details_dialog_titlebar_enabled",
tr("Show the title bar and can be docked"))
self.details_dialog_titlebar_enabled.setToolTip(
tr("While the title bar is hidden, \
use the modifier key to drag the floating window around") if ISLINUX else
tr("The title bar can only be disabled while the window is docked"))
self.details_groupbox_layout.addWidget(self.details_dialog_titlebar_enabled)
self._setupAddCheckbox("details_dialog_vertical_titlebar",
tr("Vertical title bar"))
self.details_dialog_vertical_titlebar.setToolTip(
tr("Change the title bar from horizontal on top, to vertical on the left side"))
self.details_groupbox_layout.addWidget(self.details_dialog_vertical_titlebar)
self.details_dialog_vertical_titlebar.setEnabled(
self.details_dialog_titlebar_enabled.isChecked())
self.details_dialog_titlebar_enabled.stateChanged.connect(
self.details_dialog_vertical_titlebar.setEnabled)
gridlayout = QGridLayout()
self.details_table_delta_foreground_color_label = QLabel(tr("Delta foreground color:"))
gridlayout.addWidget(self.details_table_delta_foreground_color_label, 4, 0)
self.details_table_delta_foreground_color = ColorPickerButton(self)
gridlayout.addWidget(self.details_table_delta_foreground_color, 4, 2, 1, 1, Qt.AlignLeft)
gridlayout.setColumnStretch(1, 1)
gridlayout.setColumnStretch(3, 4)
self.details_groupbox_layout.addLayout(gridlayout)
details_groupbox.setLayout(self.details_groupbox_layout)
self.displayVLayout.addWidget(details_groupbox)
def _setupAddCheckbox(self, name, label, parent=None):
if parent is None:
parent = self
@ -162,19 +228,32 @@ class PreferencesDialogBase(QDialog):
self.setSizeGripEnabled(False)
self.setModal(True)
self.mainVLayout = QVBoxLayout(self)
self.tabwidget = QTabWidget()
self.page_general = QWidget()
self.page_display = QWidget()
self.widgetsVLayout = QVBoxLayout()
self.page_general.setLayout(self.widgetsVLayout)
self.displayVLayout = QVBoxLayout()
self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style
self.page_display.setLayout(self.displayVLayout)
self._setupPreferenceWidgets()
self.mainVLayout.addLayout(self.widgetsVLayout)
self._setupDisplayPage()
# self.mainVLayout.addLayout(self.widgetsVLayout)
self.buttonBox = QDialogButtonBox(self)
self.buttonBox.setStandardButtons(
QDialogButtonBox.Cancel
| QDialogButtonBox.Ok
| QDialogButtonBox.RestoreDefaults
)
self.mainVLayout.addWidget(self.tabwidget)
self.mainVLayout.addWidget(self.buttonBox)
self.layout().setSizeConstraint(QLayout.SetFixedSize)
self.tabwidget.addTab(self.page_general, "General")
self.tabwidget.addTab(self.page_display, "Display")
self.displayVLayout.addStretch(0)
self.widgetsVLayout.addStretch(0)
def _load(self, prefs, setchecked):
def _load(self, prefs, setchecked, section):
# Edition-specific
pass
@ -182,28 +261,40 @@ class PreferencesDialogBase(QDialog):
# Edition-specific
pass
def load(self, prefs=None):
def load(self, prefs=None, section=Sections.ALL):
if prefs is None:
prefs = self.app.prefs
self.filterHardnessSlider.setValue(prefs.filter_hardness)
self.filterHardnessLabel.setNum(prefs.filter_hardness)
setchecked = lambda cb, b: cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
setchecked(self.mixFileKindBox, prefs.mix_file_kind)
setchecked(self.useRegexpBox, prefs.use_regexp)
setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders)
setchecked(self.ignoreHardlinkMatches, prefs.ignore_hardlink_matches)
setchecked(self.debugModeBox, prefs.debug_mode)
setchecked(self.reference_bold_font, prefs.reference_bold_font)
setchecked(self.tabs_default_pos, prefs.tabs_default_pos)
self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type)
self.customCommandEdit.setText(prefs.custom_command)
self.fontSizeSpinBox.setValue(prefs.tableFontSize)
try:
langindex = self.supportedLanguages.index(self.app.prefs.language)
except ValueError:
langindex = 0
self.languageComboBox.setCurrentIndex(langindex)
self._load(prefs, setchecked)
if section & Sections.GENERAL:
self.filterHardnessSlider.setValue(prefs.filter_hardness)
self.filterHardnessLabel.setNum(prefs.filter_hardness)
setchecked(self.mixFileKindBox, prefs.mix_file_kind)
setchecked(self.useRegexpBox, prefs.use_regexp)
setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders)
setchecked(self.ignoreHardlinkMatches, prefs.ignore_hardlink_matches)
setchecked(self.debugModeBox, prefs.debug_mode)
self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type)
self.customCommandEdit.setText(prefs.custom_command)
if section & Sections.DISPLAY:
setchecked(self.reference_bold_font, prefs.reference_bold_font)
setchecked(self.tabs_default_pos, prefs.tabs_default_pos)
setchecked(self.details_dialog_titlebar_enabled,
prefs.details_dialog_titlebar_enabled)
setchecked(self.details_dialog_vertical_titlebar,
prefs.details_dialog_vertical_titlebar)
self.fontSizeSpinBox.setValue(prefs.tableFontSize)
self.details_table_delta_foreground_color.setColor(
prefs.details_table_delta_foreground_color)
self.result_table_ref_foreground_color.setColor(
prefs.result_table_ref_foreground_color)
self.result_table_delta_foreground_color.setColor(
prefs.result_table_delta_foreground_color)
try:
langindex = self.supportedLanguages.index(self.app.prefs.language)
except ValueError:
langindex = 0
self.languageComboBox.setCurrentIndex(langindex)
self._load(prefs, setchecked, section)
def save(self):
prefs = self.app.prefs
@ -215,6 +306,11 @@ class PreferencesDialogBase(QDialog):
prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches)
prefs.debug_mode = ischecked(self.debugModeBox)
prefs.reference_bold_font = ischecked(self.reference_bold_font)
prefs.details_dialog_titlebar_enabled = ischecked(self.details_dialog_titlebar_enabled)
prefs.details_dialog_vertical_titlebar = ischecked(self.details_dialog_vertical_titlebar)
prefs.details_table_delta_foreground_color = self.details_table_delta_foreground_color.color
prefs.result_table_ref_foreground_color = self.result_table_ref_foreground_color.color
prefs.result_table_delta_foreground_color = self.result_table_delta_foreground_color.color
prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()
prefs.custom_command = str(self.customCommandEdit.text())
prefs.tableFontSize = self.fontSizeSpinBox.value()
@ -232,11 +328,45 @@ class PreferencesDialogBase(QDialog):
self.app.prefs.language = lang
self._save(prefs, ischecked)
def resetToDefaults(self):
self.load(Preferences())
def resetToDefaults(self, section_to_update):
self.load(Preferences(), section_to_update)
# --- Events
def buttonClicked(self, button):
role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ResetRole:
self.resetToDefaults()
current_tab = self.tabwidget.currentWidget()
section_to_update = Sections.ALL
if current_tab is self.page_general:
section_to_update = Sections.GENERAL
if current_tab is self.page_display:
section_to_update = Sections.DISPLAY
self.resetToDefaults(section_to_update)
class ColorPickerButton(QPushButton):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.color = None
self.clicked.connect(self.onClicked)
@pyqtSlot()
def onClicked(self):
color = QColorDialog.getColor(
self.color if self.color is not None else Qt.white,
self.parent)
self.setColor(color)
def setColor(self, color):
size = QSize(16, 16)
px = QPixmap(size)
if color is None:
size.width = 0
size.height = 0
elif not color.isValid():
return
else:
self.color = color
px.fill(color)
self.setIcon(QIcon(px))

View File

@ -7,7 +7,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex
from PyQt5.QtGui import QBrush, QFont, QFontMetrics, QColor
from PyQt5.QtGui import QBrush, QFont, QFontMetrics
from PyQt5.QtWidgets import QTableView
from qtlib.table import Table
@ -20,7 +20,7 @@ class ResultsModel(Table):
view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)
font = view.font()
font.setPointSize(app.prefs.tableFontSize)
self.view.setFont(font)
view.setFont(font)
fm = QFontMetrics(font)
view.verticalHeader().setDefaultSectionSize(fm.height() + 2)
@ -37,9 +37,9 @@ class ResultsModel(Table):
return data[column.name]
elif role == Qt.ForegroundRole:
if row.isref:
return QBrush(Qt.blue)
return QBrush(self.prefs.result_table_ref_foreground_color)
elif row.is_cell_delta(column.name):
return QBrush(QColor(255, 142, 40)) # orange
return QBrush(self.prefs.result_table_delta_foreground_color)
elif role == Qt.FontRole:
font = QFont(self.view.font())
if self.prefs.reference_bold_font:

View File

@ -5,7 +5,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QAbstractItemView
from PyQt5.QtWidgets import QAbstractItemView
from hscommon.trans import trget
from ..details_dialog import DetailsDialog as DetailsDialogBase
@ -19,11 +19,14 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details"))
self.resize(502, 186)
self.setMinimumSize(QSize(200, 0))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
# self.verticalLayout = QVBoxLayout()
# self.verticalLayout.setSpacing(0)
# self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)
# self.verticalLayout.addWidget(self.tableView)
# self.centralWidget = QWidget()
# self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView)

View File

@ -85,7 +85,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.widget)
self._setupBottomPart()
def _load(self, prefs, setchecked):
def _load(self, prefs, setchecked, section):
setchecked(self.matchSimilarBox, prefs.match_similar)
setchecked(self.wordWeightingBox, prefs.word_weighting)
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)

View File

@ -171,6 +171,11 @@ class TabWindow(QMainWindow):
self.setCurrentIndex(index)
return index
def showTab(self, page):
index = self.indexOfWidget(page)
self.setTabVisible(index, True)
self.setCurrentIndex(index)
def indexOfWidget(self, widget):
return self.tabWidget.indexOf(widget)
@ -302,7 +307,7 @@ class TabBarWindow(TabWindow):
@pyqtSlot(int)
def setTabIndex(self, index):
if not index:
if index is None:
return
self.tabBar.setCurrentIndex(index)
@ -344,6 +349,11 @@ class TabBarWindow(TabWindow):
@pyqtSlot(int)
def onTabCloseRequested(self, index):
current_widget = self.getWidgetAtIndex(index)
if isinstance(current_widget, DirectoriesDialog):
# On MacOS, the tab has a close button even though we explicitely
# set it to None in order to hide it. This should prevent
# the "Directories" tab from closing by mistake.
return
current_widget.close()
self.stackedWidget.removeWidget(current_widget)
# In this case the signal will take care of the tab itself after removing the widget

View File

@ -42,7 +42,7 @@ class AboutBox(QDialog):
self.setWindowTitle(
tr("About {}").format(QCoreApplication.instance().applicationName())
)
self.resize(400, 190)
self.resize(400, 290)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -69,6 +69,21 @@ class AboutBox(QDialog):
self.verticalLayout.addWidget(self.label_3)
self.label_3.setText(tr("Licensed under GPLv3"))
self.label = QLabel(self)
self.label_4 = QLabel(self)
self.label_4.setWordWrap(True)
self.label_4.setTextFormat(Qt.RichText)
self.label_4.setOpenExternalLinks(True)
self.label_4.setText(tr(
"""<img src=":/exchange" alt="Exchange" width="16" height="16"> icon
made by <a href="http://jasoncho.ca/"> Jason Cho</a> (used with permission).
<br>
<img src=":/zoom_in" alt="Zoom In" width="16" height="16">
<img src=":/zoom_out" alt="Zoom Out" width="16" height="16">
<img src=":/zoom_best_fit" alt="Zoomt Best Fit" width="16" height="16">
<img src=":/zoom_original" alt="Zoom Original" width="16" height="16">
icons made by <a href="https://findicons.com/pack/1035/human_o2">schollidesign</a>
(licensed under GPL)."""))
self.verticalLayout.addWidget(self.label_4)
font = QFont()
font.setWeight(75)
font.setBold(True)

View File

@ -7,6 +7,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSettings, QRect, QObject, pyqtSignal
from PyQt5.QtWidgets import QDockWidget
from hscommon.trans import trget
from hscommon.util import tryint
@ -123,16 +124,22 @@ class Preferences(QObject):
# We save geometry under a 5-sized int array: first item is a flag for whether the widget
# is maximized and the other 4 are (x, y, w, h).
m = 1 if widget.isMaximized() else 0
d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0
area = widget.parent.dockWidgetArea(widget) if d else 0
r = widget.geometry()
rectAsList = [r.x(), r.y(), r.width(), r.height()]
self.set_value(name, [m] + rectAsList)
self.set_value(name, [m, d, area] + rectAsList)
def restoreGeometry(self, name, widget):
geometry = self.get_value(name)
if geometry and len(geometry) == 5:
m, x, y, w, h = geometry
if geometry and len(geometry) == 7:
m, d, area, x, y, w, h = geometry
if m:
widget.setWindowState(Qt.WindowMaximized)
else:
r = QRect(x, y, w, h)
widget.setGeometry(r)
if isinstance(widget, QDockWidget):
# Inform of the previous dock state and the area used
return bool(d), area
return False, 0