mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-22 06:37:17 +00:00
Finish moving all qtlib py files to qt
This commit is contained in:
79
qt/about_box.py
Normal file
79
qt/about_box.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-09
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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, QCoreApplication, QTimer
|
||||
from PyQt5.QtGui import QPixmap, QFont
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel
|
||||
|
||||
from core.util import check_for_update
|
||||
from qt.util import move_to_screen_center
|
||||
from hscommon.trans import trget
|
||||
|
||||
tr = trget("qtlib") # TODO change to ui
|
||||
|
||||
|
||||
class AboutBox(QDialog):
|
||||
def __init__(self, parent, app, **kwargs):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
|
||||
super().__init__(parent, flags, **kwargs)
|
||||
self.app = app
|
||||
self._setupUi()
|
||||
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
|
||||
def _setupUi(self):
|
||||
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
|
||||
size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.setSizePolicy(size_policy)
|
||||
main_layout = QHBoxLayout(self)
|
||||
logo_label = QLabel()
|
||||
logo_label.setPixmap(QPixmap(":/%s_big" % self.app.LOGO_NAME))
|
||||
main_layout.addWidget(logo_label)
|
||||
detail_layout = QVBoxLayout()
|
||||
name_label = QLabel()
|
||||
font = QFont()
|
||||
font.setWeight(75)
|
||||
font.setBold(True)
|
||||
name_label.setFont(font)
|
||||
name_label.setText(QCoreApplication.instance().applicationName())
|
||||
detail_layout.addWidget(name_label)
|
||||
version_label = QLabel()
|
||||
version_label.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
|
||||
detail_layout.addWidget(version_label)
|
||||
self.update_label = QLabel(tr("Checking for updates..."))
|
||||
self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
self.update_label.setOpenExternalLinks(True)
|
||||
detail_layout.addWidget(self.update_label)
|
||||
license_label = QLabel()
|
||||
license_label.setText(tr("Licensed under GPLv3"))
|
||||
detail_layout.addWidget(license_label)
|
||||
spacer_label = QLabel()
|
||||
spacer_label.setFont(font)
|
||||
detail_layout.addWidget(spacer_label)
|
||||
self.button_box = QDialogButtonBox()
|
||||
self.button_box.setOrientation(Qt.Horizontal)
|
||||
self.button_box.setStandardButtons(QDialogButtonBox.Ok)
|
||||
detail_layout.addWidget(self.button_box)
|
||||
main_layout.addLayout(detail_layout)
|
||||
|
||||
def _check_for_update(self):
|
||||
update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False)
|
||||
if update is None:
|
||||
self.update_label.setText(tr("No update available."))
|
||||
else:
|
||||
self.update_label.setText(
|
||||
tr('New version {} available, download <a href="{}">here</a>.').format(update["version"], update["url"])
|
||||
)
|
||||
|
||||
def showEvent(self, event):
|
||||
self.update_label.setText(tr("Checking for updates..."))
|
||||
# have to do this here as the frameGeometry is not correct until shown
|
||||
move_to_screen_center(self)
|
||||
super().showEvent(event)
|
||||
QTimer.singleShot(0, self._check_for_update)
|
||||
@@ -14,10 +14,10 @@ from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QSt
|
||||
from hscommon.trans import trget
|
||||
from hscommon import desktop, plat
|
||||
|
||||
from qtlib.about_box import AboutBox
|
||||
from qtlib.recent import Recent
|
||||
from qtlib.util import create_actions
|
||||
from qtlib.progress_window import ProgressWindow
|
||||
from qt.about_box import AboutBox
|
||||
from qt.recent import Recent
|
||||
from qt.util import create_actions
|
||||
from qt.progress_window import ProgressWindow
|
||||
|
||||
from core.app import AppMode, DupeGuru as DupeGuruModel
|
||||
import core.pe.photo
|
||||
|
||||
110
qt/column.py
Normal file
110
qt/column.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-11-25
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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
|
||||
from PyQt5.QtWidgets import QHeaderView
|
||||
|
||||
|
||||
class Column:
|
||||
def __init__(
|
||||
self,
|
||||
attrname,
|
||||
default_width,
|
||||
editor=None,
|
||||
alignment=Qt.AlignLeft,
|
||||
cant_truncate=False,
|
||||
painter=None,
|
||||
resize_to_fit=False,
|
||||
):
|
||||
self.attrname = attrname
|
||||
self.default_width = default_width
|
||||
self.editor = editor
|
||||
# See moneyguru #15. Painter attribute was added to allow custom painting of amount value and
|
||||
# currency information. Can be used as a pattern for custom painting of any column.
|
||||
self.painter = painter
|
||||
self.alignment = alignment
|
||||
# This is to indicate, during printing, that a column can't have its data truncated.
|
||||
self.cant_truncate = cant_truncate
|
||||
self.resize_to_fit = resize_to_fit
|
||||
|
||||
|
||||
class Columns:
|
||||
def __init__(self, model, columns, header_view):
|
||||
self.model = model
|
||||
self._header_view = header_view
|
||||
self._header_view.setDefaultAlignment(Qt.AlignLeft)
|
||||
|
||||
def setspecs(col, modelcol):
|
||||
modelcol.default_width = col.default_width
|
||||
modelcol.editor = col.editor
|
||||
modelcol.painter = col.painter
|
||||
modelcol.resize_to_fit = col.resize_to_fit
|
||||
modelcol.alignment = col.alignment
|
||||
modelcol.cant_truncate = col.cant_truncate
|
||||
|
||||
if columns:
|
||||
for col in columns:
|
||||
modelcol = self.model.column_by_name(col.attrname)
|
||||
setspecs(col, modelcol)
|
||||
else:
|
||||
col = Column("", 100)
|
||||
for modelcol in self.model.column_list:
|
||||
setspecs(col, modelcol)
|
||||
self.model.view = self
|
||||
self._header_view.sectionMoved.connect(self.header_section_moved)
|
||||
self._header_view.sectionResized.connect(self.header_section_resized)
|
||||
|
||||
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
|
||||
for column in self.model.column_list:
|
||||
if column.resize_to_fit:
|
||||
self._header_view.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
|
||||
|
||||
# --- Public
|
||||
def set_columns_width(self, widths):
|
||||
# `widths` can be None. If it is, then default widths are set.
|
||||
columns = self.model.column_list
|
||||
if not widths:
|
||||
widths = [column.default_width for column in columns]
|
||||
for column, width in zip(columns, widths):
|
||||
if width == 0: # column was hidden before.
|
||||
width = column.default_width
|
||||
self._header_view.resizeSection(column.logical_index, width)
|
||||
|
||||
def set_columns_order(self, column_indexes):
|
||||
if not column_indexes:
|
||||
return
|
||||
for dest_index, column_index in enumerate(column_indexes):
|
||||
# moveSection takes 2 visual index arguments, so we have to get our visual index first
|
||||
visual_index = self._header_view.visualIndex(column_index)
|
||||
self._header_view.moveSection(visual_index, dest_index)
|
||||
|
||||
# --- Events
|
||||
def header_section_moved(self, logical_index, old_visual_index, new_visual_index):
|
||||
attrname = self.model.column_by_index(logical_index).name
|
||||
self.model.move_column(attrname, new_visual_index)
|
||||
|
||||
def header_section_resized(self, logical_index, old_size, new_size):
|
||||
attrname = self.model.column_by_index(logical_index).name
|
||||
self.model.resize_column(attrname, new_size)
|
||||
|
||||
# --- model --> view
|
||||
def restore_columns(self):
|
||||
columns = self.model.ordered_columns
|
||||
indexes = [col.logical_index for col in columns]
|
||||
self.set_columns_order(indexes)
|
||||
widths = [col.width for col in self.model.column_list]
|
||||
if not any(widths):
|
||||
widths = None
|
||||
self.set_columns_width(widths)
|
||||
for column in self.model.column_list:
|
||||
visible = self.model.column_is_visible(column.name)
|
||||
self._header_view.setSectionHidden(column.logical_index, not visible)
|
||||
|
||||
def set_column_visible(self, colname, visible):
|
||||
column = self.model.column_by_name(colname)
|
||||
self._header_view.setSectionHidden(column.logical_index, not visible)
|
||||
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBox
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.radio_box import RadioBox
|
||||
from qt.radio_box import RadioBox
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QDockWidget, QWidget
|
||||
|
||||
from qtlib.util import move_to_screen_center
|
||||
from qt.util import move_to_screen_center
|
||||
from .details_table import DetailsModel
|
||||
from hscommon.plat import ISLINUX
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<file alias="logo_se_big">../images/dgse_logo_128.png</file>
|
||||
<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="search_clear_13">../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>
|
||||
|
||||
@@ -27,9 +27,9 @@ from PyQt5.QtGui import QPixmap, QIcon
|
||||
|
||||
from hscommon.trans import trget
|
||||
from core.app import AppMode
|
||||
from qtlib.radio_box import RadioBox
|
||||
from qtlib.recent import Recent
|
||||
from qtlib.util import move_to_screen_center, create_actions
|
||||
from qt.radio_box import RadioBox
|
||||
from qt.recent import Recent
|
||||
from qt.util import move_to_screen_center, create_actions
|
||||
|
||||
from . import platform
|
||||
from .directories_model import DirectoriesModel, DirectoriesDelegate
|
||||
|
||||
@@ -18,7 +18,7 @@ from PyQt5.QtWidgets import (
|
||||
from PyQt5.QtGui import QBrush
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.tree_model import RefNode, TreeModel
|
||||
from qt.tree_model import RefNode, TreeModel
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
|
||||
96
qt/error_report_dialog.py
Normal file
96
qt/error_report_dialog.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-23
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
|
||||
from PyQt5.QtCore import Qt, QCoreApplication, QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from hscommon.desktop import open_url
|
||||
from qt.util import horizontal_spacer
|
||||
|
||||
tr = trget("qtlib") # TODO change to ui
|
||||
|
||||
|
||||
class ErrorReportDialog(QDialog):
|
||||
def __init__(self, parent, github_url, error, **kwargs):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
super().__init__(parent, flags, **kwargs)
|
||||
self._setupUi()
|
||||
name = QCoreApplication.applicationName()
|
||||
version = QCoreApplication.applicationVersion()
|
||||
error_text = "Application Name: {}\nVersion: {}\nPython: {}\nOperating System: {}\n\n{}".format(
|
||||
name, version, platform.python_version(), platform.platform(), error
|
||||
)
|
||||
# Under windows, we end up with an error report without linesep if we don't mangle it
|
||||
error_text = error_text.replace("\n", os.linesep)
|
||||
self.errorTextEdit.setPlainText(error_text)
|
||||
self.github_url = github_url
|
||||
|
||||
self.sendButton.clicked.connect(self.goToGithub)
|
||||
self.dontSendButton.clicked.connect(self.reject)
|
||||
|
||||
def _setupUi(self):
|
||||
self.setWindowTitle(tr("Error Report"))
|
||||
self.resize(553, 349)
|
||||
self.verticalLayout = QVBoxLayout(self)
|
||||
self.label = QLabel(self)
|
||||
self.label.setText(tr("Something went wrong. How about reporting the error?"))
|
||||
self.label.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.label)
|
||||
self.errorTextEdit = QPlainTextEdit(self)
|
||||
self.errorTextEdit.setReadOnly(True)
|
||||
self.verticalLayout.addWidget(self.errorTextEdit)
|
||||
msg = tr(
|
||||
"Error reports should be reported as Github issues. You can copy the error traceback "
|
||||
"above and paste it in a new issue.\n\nPlease make sure to run a search for any already "
|
||||
"existing issues beforehand. Also make sure to test the very latest version available from the repository, "
|
||||
"since the bug you are experiencing might have already been patched.\n\n"
|
||||
"What usually really helps is if you add a description of how you got the error. Thanks!"
|
||||
"\n\n"
|
||||
"Although the application should continue to run after this error, it may be in an "
|
||||
"unstable state, so it is recommended that you restart the application."
|
||||
)
|
||||
self.label2 = QLabel(msg)
|
||||
self.label2.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.label2)
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.addItem(horizontal_spacer())
|
||||
self.dontSendButton = QPushButton(self)
|
||||
self.dontSendButton.setText(tr("Close"))
|
||||
self.dontSendButton.setMinimumSize(QSize(110, 0))
|
||||
self.horizontalLayout.addWidget(self.dontSendButton)
|
||||
self.sendButton = QPushButton(self)
|
||||
self.sendButton.setText(tr("Go to Github"))
|
||||
self.sendButton.setMinimumSize(QSize(110, 0))
|
||||
self.sendButton.setDefault(True)
|
||||
self.horizontalLayout.addWidget(self.sendButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
|
||||
def goToGithub(self):
|
||||
open_url(self.github_url)
|
||||
|
||||
|
||||
def install_excepthook(github_url):
|
||||
def my_excepthook(exctype, value, tb):
|
||||
s = "".join(traceback.format_exception(exctype, value, tb))
|
||||
dialog = ErrorReportDialog(None, github_url, s)
|
||||
dialog.exec_()
|
||||
|
||||
sys.excepthook = my_excepthook
|
||||
@@ -5,8 +5,8 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor
|
||||
|
||||
from qtlib.column import Column
|
||||
from qtlib.table import Table
|
||||
from qt.column import Column
|
||||
from qt.table import Table
|
||||
from hscommon.trans import trget
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
@@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.util import horizontal_wrap
|
||||
from qt.util import horizontal_wrap
|
||||
from .ignore_list_table import IgnoreListTable
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from qtlib.column import Column
|
||||
from qtlib.table import Table
|
||||
from qt.column import Column
|
||||
from qt.table import Table
|
||||
|
||||
|
||||
class IgnoreListTable(Table):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from qtlib.column import Column
|
||||
from qt.column import Column
|
||||
from ..results_model import ResultsModel as ResultsModelBase
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 qt.radio_box import RadioBox
|
||||
from core.scanner import ScanType
|
||||
from core.app import AppMode
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from qtlib.column import Column
|
||||
from qt.column import Column
|
||||
from ..results_model import ResultsModel as ResultsModelBase
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from hscommon import desktop, plat
|
||||
|
||||
from hscommon.trans import trget
|
||||
from hscommon.plat import ISLINUX
|
||||
from qtlib.util import horizontal_wrap, move_to_screen_center
|
||||
from qt.util import horizontal_wrap, move_to_screen_center
|
||||
from qt.preferences import get_langnames
|
||||
from enum import Flag, auto
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
||||
from qtlib.util import vertical_spacer
|
||||
from qt.selectable_list import ComboboxModel, ListviewModel
|
||||
from qt.util import vertical_spacer
|
||||
from core.gui.prioritize_dialog import PrioritizeDialog as PrioritizeDialogModel
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
@@ -19,7 +19,7 @@ from PyQt5.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
)
|
||||
|
||||
from qtlib.util import move_to_screen_center
|
||||
from qt.util import move_to_screen_center
|
||||
from hscommon.trans import trget
|
||||
from .problem_table import ProblemTable
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from qtlib.column import Column
|
||||
from qtlib.table import Table
|
||||
from qt.column import Column
|
||||
from qt.table import Table
|
||||
|
||||
|
||||
class ProblemTable(Table):
|
||||
|
||||
61
qt/progress_window.py
Normal file
61
qt/progress_window.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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, QTimer
|
||||
from PyQt5.QtWidgets import QProgressDialog
|
||||
|
||||
from hscommon.trans import tr
|
||||
|
||||
|
||||
class ProgressWindow:
|
||||
def __init__(self, parent, model):
|
||||
self._window = None
|
||||
self.parent = parent
|
||||
self.model = model
|
||||
model.view = self
|
||||
# We don't have access to QProgressDialog's labels directly, so we se the model label's view
|
||||
# to self and we'll refresh them together.
|
||||
self.model.jobdesc_textfield.view = self
|
||||
self.model.progressdesc_textfield.view = self
|
||||
|
||||
# --- Callbacks
|
||||
def refresh(self): # Labels
|
||||
if self._window is not None:
|
||||
self._window.setWindowTitle(self.model.jobdesc_textfield.text)
|
||||
self._window.setLabelText(self.model.progressdesc_textfield.text)
|
||||
|
||||
def set_progress(self, last_progress):
|
||||
if self._window is not None:
|
||||
if last_progress < 0:
|
||||
self._window.setRange(0, 0)
|
||||
else:
|
||||
self._window.setRange(0, 100)
|
||||
self._window.setValue(last_progress)
|
||||
|
||||
def show(self):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
self._window = QProgressDialog("", tr("Cancel"), 0, 100, self.parent, flags)
|
||||
self._window.setModal(True)
|
||||
self._window.setAutoReset(False)
|
||||
self._window.setAutoClose(False)
|
||||
self._timer = QTimer(self._window)
|
||||
self._timer.timeout.connect(self.model.pulse)
|
||||
self._window.show()
|
||||
self._window.canceled.connect(self.model.cancel)
|
||||
self._timer.start(500)
|
||||
|
||||
def close(self):
|
||||
# it seems it is possible for close to be called without a corresponding
|
||||
# show, only perform a close if there is a window to close
|
||||
if self._window is not None:
|
||||
self._timer.stop()
|
||||
del self._timer
|
||||
# For some weird reason, canceled() signal is sent upon close, whether the user canceled
|
||||
# or not. If we don't want a false cancellation, we have to disconnect it.
|
||||
self._window.canceled.disconnect()
|
||||
self._window.close()
|
||||
self._window.setParent(None)
|
||||
self._window = None
|
||||
88
qt/radio_box.py
Normal file
88
qt/radio_box.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Created On: 2010-06-02
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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 pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton
|
||||
|
||||
from qt.util import horizontal_spacer
|
||||
|
||||
|
||||
class RadioBox(QWidget):
|
||||
def __init__(self, parent=None, items=None, spread=True, **kwargs):
|
||||
# If spread is False, insert a spacer in the layout so that the items don't use all the
|
||||
# space they're given but rather align left.
|
||||
if items is None:
|
||||
items = []
|
||||
super().__init__(parent, **kwargs)
|
||||
self._buttons = []
|
||||
self._labels = items
|
||||
self._selected_index = 0
|
||||
self._spacer = horizontal_spacer() if not spread else None
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._update_buttons()
|
||||
|
||||
# --- Private
|
||||
def _update_buttons(self):
|
||||
if self._spacer is not None:
|
||||
self._layout.removeItem(self._spacer)
|
||||
to_remove = self._buttons[len(self._labels) :]
|
||||
for button in to_remove:
|
||||
self._layout.removeWidget(button)
|
||||
button.setParent(None)
|
||||
del self._buttons[len(self._labels) :]
|
||||
to_add = self._labels[len(self._buttons) :]
|
||||
for _ in to_add:
|
||||
button = QRadioButton(self)
|
||||
self._buttons.append(button)
|
||||
self._layout.addWidget(button)
|
||||
button.toggled.connect(self.buttonToggled)
|
||||
if self._spacer is not None:
|
||||
self._layout.addItem(self._spacer)
|
||||
if not self._buttons:
|
||||
return
|
||||
for button, label in zip(self._buttons, self._labels):
|
||||
button.setText(label)
|
||||
self._update_selection()
|
||||
|
||||
def _update_selection(self):
|
||||
self._selected_index = max(0, min(self._selected_index, len(self._buttons) - 1))
|
||||
selected = self._buttons[self._selected_index]
|
||||
selected.setChecked(True)
|
||||
|
||||
# --- Event Handlers
|
||||
def buttonToggled(self):
|
||||
for i, button in enumerate(self._buttons):
|
||||
if button.isChecked():
|
||||
self._selected_index = i
|
||||
self.itemSelected.emit(i)
|
||||
break
|
||||
|
||||
# --- Signals
|
||||
itemSelected = pyqtSignal(int)
|
||||
|
||||
# --- Properties
|
||||
@property
|
||||
def buttons(self):
|
||||
return self._buttons[:]
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return self._labels[:]
|
||||
|
||||
@items.setter
|
||||
def items(self, value):
|
||||
self._labels = value
|
||||
self._update_buttons()
|
||||
|
||||
@property
|
||||
def selected_index(self):
|
||||
return self._selected_index
|
||||
|
||||
@selected_index.setter
|
||||
def selected_index(self, value):
|
||||
self._selected_index = value
|
||||
self._update_selection()
|
||||
94
qt/recent.py
Normal file
94
qt/recent.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-11-12
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
from PyQt5.QtWidgets import QAction
|
||||
|
||||
from hscommon.trans import trget
|
||||
from hscommon.util import dedupe
|
||||
|
||||
tr = trget("qtlib") # TODO move to ui
|
||||
|
||||
MenuEntry = namedtuple("MenuEntry", "menu fixedItemCount")
|
||||
|
||||
|
||||
class Recent(QObject):
|
||||
def __init__(self, app, pref_name, max_item_count=10, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._app = app
|
||||
self._menuEntries = []
|
||||
self._prefName = pref_name
|
||||
self._maxItemCount = max_item_count
|
||||
self._items = []
|
||||
self._loadFromPrefs()
|
||||
|
||||
self._app.willSavePrefs.connect(self._saveToPrefs)
|
||||
|
||||
# --- Private
|
||||
def _loadFromPrefs(self):
|
||||
items = getattr(self._app.prefs, self._prefName)
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
self._items = items
|
||||
|
||||
def _insertItem(self, item):
|
||||
self._items = dedupe([item] + self._items)[: self._maxItemCount]
|
||||
|
||||
def _refreshMenu(self, menu_entry):
|
||||
menu, fixed_item_count = menu_entry
|
||||
for action in menu.actions()[fixed_item_count:]:
|
||||
menu.removeAction(action)
|
||||
for item in self._items:
|
||||
action = QAction(item, menu)
|
||||
action.setData(item)
|
||||
action.triggered.connect(self.menuItemWasClicked)
|
||||
menu.addAction(action)
|
||||
menu.addSeparator()
|
||||
action = QAction(tr("Clear List"), menu)
|
||||
action.triggered.connect(self.clear)
|
||||
menu.addAction(action)
|
||||
|
||||
def _refreshAllMenus(self):
|
||||
for menu_entry in self._menuEntries:
|
||||
self._refreshMenu(menu_entry)
|
||||
|
||||
def _saveToPrefs(self):
|
||||
setattr(self._app.prefs, self._prefName, self._items)
|
||||
|
||||
# --- Public
|
||||
def addMenu(self, menu):
|
||||
menu_entry = MenuEntry(menu, len(menu.actions()))
|
||||
self._menuEntries.append(menu_entry)
|
||||
self._refreshMenu(menu_entry)
|
||||
|
||||
def clear(self):
|
||||
self._items = []
|
||||
self._refreshAllMenus()
|
||||
self.itemsChanged.emit()
|
||||
|
||||
def insertItem(self, item):
|
||||
self._insertItem(str(item))
|
||||
self._refreshAllMenus()
|
||||
self.itemsChanged.emit()
|
||||
|
||||
def isEmpty(self):
|
||||
return not bool(self._items)
|
||||
|
||||
# --- Event Handlers
|
||||
def menuItemWasClicked(self):
|
||||
action = self.sender()
|
||||
if action is not None:
|
||||
item = action.data()
|
||||
self.mustOpenItem.emit(item)
|
||||
self._refreshAllMenus()
|
||||
|
||||
# --- Signals
|
||||
mustOpenItem = pyqtSignal(str)
|
||||
itemsChanged = pyqtSignal()
|
||||
@@ -24,8 +24,8 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.util import move_to_screen_center, horizontal_wrap, create_actions
|
||||
from qtlib.search_edit import SearchEdit
|
||||
from qt.util import move_to_screen_center, horizontal_wrap, create_actions
|
||||
from qt.search_edit import SearchEdit
|
||||
|
||||
from core.app import AppMode
|
||||
from .results_model import ResultsView
|
||||
|
||||
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex
|
||||
from PyQt5.QtGui import QBrush, QFont, QFontMetrics
|
||||
from PyQt5.QtWidgets import QTableView
|
||||
|
||||
from qtlib.table import Table
|
||||
from qt.table import Table
|
||||
|
||||
|
||||
class ResultsModel(Table):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from qtlib.column import Column
|
||||
from qt.column import Column
|
||||
from ..results_model import ResultsModel as ResultsModelBase
|
||||
|
||||
|
||||
|
||||
120
qt/search_edit.py
Normal file
120
qt/search_edit.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-12-10
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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 pyqtSignal, Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPalette
|
||||
from PyQt5.QtWidgets import QToolButton, QLineEdit, QStyle, QStyleOptionFrame
|
||||
|
||||
from hscommon.trans import trget
|
||||
|
||||
tr = trget("qtlib") # TODO change to ui
|
||||
|
||||
# IMPORTANT: For this widget to work propertly, you have to add "search_clear_13" from the
|
||||
# "images" folder in your resources.
|
||||
|
||||
|
||||
class LineEditButton(QToolButton):
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
pixmap = QPixmap(":/search_clear_13")
|
||||
self.setIcon(QIcon(pixmap))
|
||||
self.setIconSize(pixmap.size())
|
||||
self.setCursor(Qt.ArrowCursor)
|
||||
self.setPopupMode(QToolButton.InstantPopup)
|
||||
stylesheet = "QToolButton { border: none; padding: 0px; }"
|
||||
self.setStyleSheet(stylesheet)
|
||||
|
||||
|
||||
class ClearableEdit(QLineEdit):
|
||||
def __init__(self, parent=None, is_clearable=True, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
self._is_clearable = is_clearable
|
||||
if is_clearable:
|
||||
self._clearButton = LineEditButton(self)
|
||||
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
|
||||
padding_right = self._clearButton.sizeHint().width() + frame_width + 1
|
||||
stylesheet = f"QLineEdit {{ padding-right:{padding_right}px; }}"
|
||||
self.setStyleSheet(stylesheet)
|
||||
self._updateClearButton()
|
||||
|
||||
self._clearButton.clicked.connect(self._clearSearch)
|
||||
self.textChanged.connect(self._textChanged)
|
||||
|
||||
# --- Private
|
||||
def _clearSearch(self):
|
||||
self.clear()
|
||||
|
||||
def _updateClearButton(self):
|
||||
self._clearButton.setVisible(self._hasClearableContent())
|
||||
|
||||
def _hasClearableContent(self):
|
||||
return bool(self.text())
|
||||
|
||||
# --- QLineEdit overrides
|
||||
def resizeEvent(self, event):
|
||||
if self._is_clearable:
|
||||
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
|
||||
rect = self.rect()
|
||||
right_hint = self._clearButton.sizeHint()
|
||||
right_x = rect.right() - frame_width - right_hint.width()
|
||||
right_y = (rect.bottom() - right_hint.height()) // 2
|
||||
self._clearButton.move(right_x, right_y)
|
||||
|
||||
# --- Event Handlers
|
||||
def _textChanged(self, text):
|
||||
if self._is_clearable:
|
||||
self._updateClearButton()
|
||||
|
||||
|
||||
class SearchEdit(ClearableEdit):
|
||||
def __init__(self, parent=None, immediate=False):
|
||||
# immediate: send searchChanged signals at each keystroke.
|
||||
ClearableEdit.__init__(self, parent, is_clearable=True)
|
||||
self.inactiveText = tr("Search...")
|
||||
self.immediate = immediate
|
||||
|
||||
self.returnPressed.connect(self._returnPressed)
|
||||
|
||||
# --- Overrides
|
||||
def _clearSearch(self):
|
||||
ClearableEdit._clearSearch(self)
|
||||
self.searchChanged.emit()
|
||||
|
||||
def _textChanged(self, text):
|
||||
ClearableEdit._textChanged(self, text)
|
||||
if self.immediate:
|
||||
self.searchChanged.emit()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
if key == Qt.Key_Escape:
|
||||
self._clearSearch()
|
||||
else:
|
||||
ClearableEdit.keyPressEvent(self, event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
ClearableEdit.paintEvent(self, event)
|
||||
if not bool(self.text()) and self.inactiveText and not self.hasFocus():
|
||||
panel = QStyleOptionFrame()
|
||||
self.initStyleOption(panel)
|
||||
text_rect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
|
||||
left_margin = 2
|
||||
right_margin = self._clearButton.iconSize().width()
|
||||
text_rect.adjust(left_margin, 0, -right_margin, 0)
|
||||
painter = QPainter(self)
|
||||
disabled_color = self.palette().brush(QPalette.Disabled, QPalette.Text).color()
|
||||
painter.setPen(disabled_color)
|
||||
painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText)
|
||||
|
||||
# --- Event Handlers
|
||||
def _returnPressed(self):
|
||||
if not self.immediate:
|
||||
self.searchChanged.emit()
|
||||
|
||||
# --- Signals
|
||||
searchChanged = pyqtSignal() # Emitted when return is pressed or when the test is cleared
|
||||
100
qt/selectable_list.py
Normal file
100
qt/selectable_list.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2011-09-06
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# 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, QAbstractListModel, QItemSelection, QItemSelectionModel
|
||||
|
||||
|
||||
class SelectableList(QAbstractListModel):
|
||||
def __init__(self, model, view, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._updating = False
|
||||
self.view = view
|
||||
self.model = model
|
||||
self.view.setModel(self)
|
||||
self.model.view = self
|
||||
|
||||
# --- Override
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
# We need EditRole for QComboBoxes with setEditable(True)
|
||||
if role in {Qt.DisplayRole, Qt.EditRole}:
|
||||
return self.model[index.row()]
|
||||
return None
|
||||
|
||||
def rowCount(self, index):
|
||||
if index.isValid():
|
||||
return 0
|
||||
return len(self.model)
|
||||
|
||||
# --- Virtual
|
||||
def _updateSelection(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _restoreSelection(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
# --- model --> view
|
||||
def refresh(self):
|
||||
self._updating = True
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
self._updating = False
|
||||
self._restoreSelection()
|
||||
|
||||
def update_selection(self):
|
||||
self._restoreSelection()
|
||||
|
||||
|
||||
class ComboboxModel(SelectableList):
|
||||
def __init__(self, model, view, **kwargs):
|
||||
super().__init__(model, view, **kwargs)
|
||||
self.view.currentIndexChanged[int].connect(self.selectionChanged)
|
||||
|
||||
# --- Override
|
||||
def _updateSelection(self):
|
||||
index = self.view.currentIndex()
|
||||
if index != self.model.selected_index:
|
||||
self.model.select(index)
|
||||
|
||||
def _restoreSelection(self):
|
||||
index = self.model.selected_index
|
||||
if index is not None:
|
||||
self.view.setCurrentIndex(index)
|
||||
|
||||
# --- Events
|
||||
def selectionChanged(self, index):
|
||||
if not self._updating:
|
||||
self._updateSelection()
|
||||
|
||||
|
||||
class ListviewModel(SelectableList):
|
||||
def __init__(self, model, view, **kwargs):
|
||||
super().__init__(model, view, **kwargs)
|
||||
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||
|
||||
# --- Override
|
||||
def _updateSelection(self):
|
||||
new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
||||
if new_indexes != self.model.selected_indexes:
|
||||
self.model.select(new_indexes)
|
||||
|
||||
def _restoreSelection(self):
|
||||
new_selection = QItemSelection()
|
||||
for index in self.model.selected_indexes:
|
||||
new_selection.select(self.createIndex(index, 0), self.createIndex(index, 0))
|
||||
self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect)
|
||||
if len(new_selection.indexes()):
|
||||
current_index = new_selection.indexes()[0]
|
||||
self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current)
|
||||
self.view.scrollTo(current_index)
|
||||
|
||||
# --- Events
|
||||
def selectionChanged(self, index):
|
||||
if not self._updating:
|
||||
self._updateSelection()
|
||||
@@ -14,7 +14,7 @@ from PyQt5.QtWidgets import (
|
||||
QStackedWidget,
|
||||
)
|
||||
from hscommon.trans import trget
|
||||
from qtlib.util import move_to_screen_center, create_actions
|
||||
from qt.util import move_to_screen_center, create_actions
|
||||
from .directories_dialog import DirectoriesDialog
|
||||
from .result_window import ResultWindow
|
||||
from .ignore_list_dialog import IgnoreListDialog
|
||||
|
||||
160
qt/table.py
Normal file
160
qt/table.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-11-01
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import typing
|
||||
from PyQt5.QtCore import (
|
||||
Qt,
|
||||
QAbstractTableModel,
|
||||
QModelIndex,
|
||||
QItemSelectionModel,
|
||||
QItemSelection,
|
||||
)
|
||||
|
||||
from .column import Columns, Column
|
||||
|
||||
|
||||
class Table(QAbstractTableModel):
|
||||
# Flags you want when index.isValid() is False. In those cases, _getFlags() is never called.
|
||||
INVALID_INDEX_FLAGS = Qt.ItemFlag.ItemIsEnabled
|
||||
COLUMNS: typing.List[Column] = []
|
||||
|
||||
def __init__(self, model, view, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.model = model
|
||||
self.view = view
|
||||
self.view.setModel(self)
|
||||
self.model.view = self
|
||||
if hasattr(self.model, "_columns"):
|
||||
self._columns = Columns(self.model._columns, self.COLUMNS, view.horizontalHeader())
|
||||
|
||||
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||
|
||||
def _updateModelSelection(self):
|
||||
# Takes the selection on the view's side and update the model with it.
|
||||
# an _updateViewSelection() call will normally result in an _updateModelSelection() call.
|
||||
# to avoid infinite loops, we check that the selection will actually change before calling
|
||||
# model.select()
|
||||
new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
||||
if new_indexes != self.model.selected_indexes:
|
||||
self.model.select(new_indexes)
|
||||
|
||||
def _updateViewSelection(self):
|
||||
# Takes the selection on the model's side and update the view with it.
|
||||
new_selection = QItemSelection()
|
||||
column_count = self.columnCount(QModelIndex())
|
||||
for index in self.model.selected_indexes:
|
||||
new_selection.select(self.createIndex(index, 0), self.createIndex(index, column_count - 1))
|
||||
self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect)
|
||||
if len(new_selection.indexes()):
|
||||
current_index = new_selection.indexes()[0]
|
||||
self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current)
|
||||
self.view.scrollTo(current_index)
|
||||
|
||||
# --- Data Model methods
|
||||
# Virtual
|
||||
def _getData(self, row, column, role):
|
||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||
attrname = column.name
|
||||
return row.get_cell_value(attrname)
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
return column.alignment
|
||||
return None
|
||||
|
||||
# Virtual
|
||||
def _getFlags(self, row, column):
|
||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
if row.can_edit_cell(column.name):
|
||||
flags |= Qt.ItemIsEditable
|
||||
return flags
|
||||
|
||||
# Virtual
|
||||
def _setData(self, row, column, value, role):
|
||||
if role == Qt.EditRole:
|
||||
attrname = column.name
|
||||
if attrname == "from":
|
||||
attrname = "from_"
|
||||
setattr(row, attrname, value)
|
||||
return True
|
||||
return False
|
||||
|
||||
def columnCount(self, index):
|
||||
return self.model._columns.columns_count()
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
row = self.model[index.row()]
|
||||
column = self.model._columns.column_by_index(index.column())
|
||||
return self._getData(row, column, role)
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return self.INVALID_INDEX_FLAGS
|
||||
row = self.model[index.row()]
|
||||
column = self.model._columns.column_by_index(index.column())
|
||||
return self._getFlags(row, column)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation != Qt.Horizontal:
|
||||
return None
|
||||
if section >= self.model._columns.columns_count():
|
||||
return None
|
||||
column = self.model._columns.column_by_index(section)
|
||||
if role == Qt.DisplayRole:
|
||||
return column.display
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
return column.alignment
|
||||
else:
|
||||
return None
|
||||
|
||||
def revert(self):
|
||||
self.model.cancel_edits()
|
||||
|
||||
def rowCount(self, index):
|
||||
if index.isValid():
|
||||
return 0
|
||||
return len(self.model)
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if not index.isValid():
|
||||
return False
|
||||
row = self.model[index.row()]
|
||||
column = self.model._columns.column_by_index(index.column())
|
||||
return self._setData(row, column, value, role)
|
||||
|
||||
def sort(self, section, order):
|
||||
column = self.model._columns.column_by_index(section)
|
||||
attrname = column.name
|
||||
self.model.sort_by(attrname, desc=order == Qt.DescendingOrder)
|
||||
|
||||
def submit(self):
|
||||
self.model.save_edits()
|
||||
return True
|
||||
|
||||
# --- Events
|
||||
def selectionChanged(self, selected, deselected):
|
||||
self._updateModelSelection()
|
||||
|
||||
# --- model --> view
|
||||
def refresh(self):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
self._updateViewSelection()
|
||||
|
||||
def show_selected_row(self):
|
||||
if self.model.selected_index is not None:
|
||||
self.view.showRow(self.model.selected_index)
|
||||
|
||||
def start_editing(self):
|
||||
self.view.editSelected()
|
||||
|
||||
def stop_editing(self):
|
||||
self.view.setFocus() # enough to stop editing
|
||||
|
||||
def update_selection(self):
|
||||
self._updateViewSelection()
|
||||
178
qt/tree_model.py
Normal file
178
qt/tree_model.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-09-14
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import QAbstractItemModel, QModelIndex
|
||||
|
||||
|
||||
class NodeContainer:
|
||||
def __init__(self):
|
||||
self._subnodes = None
|
||||
self._ref2node = {}
|
||||
|
||||
# --- Protected
|
||||
def _create_node(self, ref, row):
|
||||
# This returns a TreeNode instance from ref
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_children(self):
|
||||
# This returns a list of ref instances, not TreeNode instances
|
||||
raise NotImplementedError()
|
||||
|
||||
# --- Public
|
||||
def invalidate(self):
|
||||
# Invalidates cached data and list of subnodes without resetting ref2node.
|
||||
self._subnodes = None
|
||||
|
||||
# --- Properties
|
||||
@property
|
||||
def subnodes(self):
|
||||
if self._subnodes is None:
|
||||
children = self._get_children()
|
||||
self._subnodes = []
|
||||
for index, child in enumerate(children):
|
||||
if child in self._ref2node:
|
||||
node = self._ref2node[child]
|
||||
node.row = index
|
||||
else:
|
||||
node = self._create_node(child, index)
|
||||
self._ref2node[child] = node
|
||||
self._subnodes.append(node)
|
||||
return self._subnodes
|
||||
|
||||
|
||||
class TreeNode(NodeContainer):
|
||||
def __init__(self, model, parent, row):
|
||||
NodeContainer.__init__(self)
|
||||
self.model = model
|
||||
self.parent = parent
|
||||
self.row = row
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self.model.createIndex(self.row, 0, self)
|
||||
|
||||
|
||||
class RefNode(TreeNode):
|
||||
"""Node pointing to a reference node.
|
||||
|
||||
Use this if your Qt model wraps around a tree model that has iterable nodes.
|
||||
"""
|
||||
|
||||
def __init__(self, model, parent, ref, row):
|
||||
TreeNode.__init__(self, model, parent, row)
|
||||
self.ref = ref
|
||||
|
||||
def _create_node(self, ref, row):
|
||||
return RefNode(self.model, self, ref, row)
|
||||
|
||||
def _get_children(self):
|
||||
return list(self.ref)
|
||||
|
||||
|
||||
# We use a specific TreeNode subclass to easily spot dummy nodes, especially in exception tracebacks.
|
||||
class DummyNode(TreeNode):
|
||||
pass
|
||||
|
||||
|
||||
class TreeModel(QAbstractItemModel, NodeContainer):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._dummy_nodes = set() # dummy nodes' reference have to be kept to avoid segfault
|
||||
|
||||
# --- Private
|
||||
def _create_dummy_node(self, parent, row):
|
||||
# In some cases (drag & drop row removal, to be precise), there's a temporary discrepancy
|
||||
# between a node's subnodes and what the model think it has. This leads to invalid indexes
|
||||
# being queried. Rather than going through complicated row removal crap, it's simpler to
|
||||
# just have rows with empty data replacing removed rows for the millisecond that the drag &
|
||||
# drop lasts. Override this to return a node of the correct type.
|
||||
return DummyNode(self, parent, row)
|
||||
|
||||
def _last_index(self):
|
||||
"""Index of the very last item in the tree."""
|
||||
current_index = QModelIndex()
|
||||
row_count = self.rowCount(current_index)
|
||||
while row_count > 0:
|
||||
current_index = self.index(row_count - 1, 0, current_index)
|
||||
row_count = self.rowCount(current_index)
|
||||
return current_index
|
||||
|
||||
# --- Overrides
|
||||
def index(self, row, column, parent):
|
||||
if not self.subnodes:
|
||||
return QModelIndex()
|
||||
node = parent.internalPointer() if parent.isValid() else self
|
||||
try:
|
||||
return self.createIndex(row, column, node.subnodes[row])
|
||||
except IndexError:
|
||||
logging.debug(
|
||||
"Wrong tree index called (%r, %r, %r). Returning DummyNode",
|
||||
row,
|
||||
column,
|
||||
node,
|
||||
)
|
||||
parent_node = parent.internalPointer() if parent.isValid() else None
|
||||
dummy = self._create_dummy_node(parent_node, row)
|
||||
self._dummy_nodes.add(dummy)
|
||||
return self.createIndex(row, column, dummy)
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
node = index.internalPointer()
|
||||
if node.parent is None:
|
||||
return QModelIndex()
|
||||
else:
|
||||
return self.createIndex(node.parent.row, 0, node.parent)
|
||||
|
||||
def reset(self):
|
||||
super().beginResetModel()
|
||||
self.invalidate()
|
||||
self._ref2node = {}
|
||||
self._dummy_nodes = set()
|
||||
super().endResetModel()
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
node = parent.internalPointer() if parent.isValid() else self
|
||||
return len(node.subnodes)
|
||||
|
||||
# --- Public
|
||||
def findIndex(self, row_path):
|
||||
"""Returns the QModelIndex at `row_path`
|
||||
|
||||
`row_path` is a sequence of node rows. For example, [1, 2, 1] is the 2nd child of the
|
||||
3rd child of the 2nd child of the root.
|
||||
"""
|
||||
result = QModelIndex()
|
||||
for row in row_path:
|
||||
result = self.index(row, 0, result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def pathForIndex(index):
|
||||
reversed_path = []
|
||||
while index.isValid():
|
||||
reversed_path.append(index.row())
|
||||
index = index.parent()
|
||||
return list(reversed(reversed_path))
|
||||
|
||||
def refreshData(self):
|
||||
"""Updates the data on all nodes, but without having to perform a full reset.
|
||||
|
||||
A full reset on a tree makes us lose selection and expansion states. When all we ant to do
|
||||
is to refresh the data on the nodes without adding or removing a node, a call on
|
||||
dataChanged() is better. But of course, Qt makes our life complicated by asking us topLeft
|
||||
and bottomRight indexes. This is a convenience method refreshing the whole tree.
|
||||
"""
|
||||
column_count = self.columnCount()
|
||||
top_left = self.index(0, 0, QModelIndex())
|
||||
bottom_left = self._last_index()
|
||||
bottom_right = self.sibling(bottom_left.row(), column_count - 1, bottom_left)
|
||||
self.dataChanged.emit(top_left, bottom_right)
|
||||
139
qt/util.py
Normal file
139
qt/util.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2011-02-01
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import sys
|
||||
import io
|
||||
import os.path as op
|
||||
import os
|
||||
import logging
|
||||
|
||||
from core.util import executable_folder
|
||||
from hscommon.util import first
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QGuiApplication
|
||||
from PyQt5.QtWidgets import (
|
||||
QSpacerItem,
|
||||
QSizePolicy,
|
||||
QAction,
|
||||
QHBoxLayout,
|
||||
)
|
||||
|
||||
|
||||
def move_to_screen_center(widget):
|
||||
frame = widget.frameGeometry()
|
||||
if QGuiApplication.screenAt(frame.center()) is None:
|
||||
# if center not on any screen use default screen
|
||||
screen = QGuiApplication.screens()[0].availableGeometry()
|
||||
else:
|
||||
screen = QGuiApplication.screenAt(frame.center()).availableGeometry()
|
||||
# moves to center of screen if partially off screen
|
||||
if screen.contains(frame) is False:
|
||||
# make sure the frame is not larger than screen
|
||||
# resize does not seem to take frame size into account (move does)
|
||||
widget.resize(frame.size().boundedTo(screen.size() - (frame.size() - widget.size())))
|
||||
frame = widget.frameGeometry()
|
||||
frame.moveCenter(screen.center())
|
||||
widget.move(frame.topLeft())
|
||||
|
||||
|
||||
def vertical_spacer(size=None):
|
||||
if size:
|
||||
return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
else:
|
||||
return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
|
||||
|
||||
|
||||
def horizontal_spacer(size=None):
|
||||
if size:
|
||||
return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
else:
|
||||
return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
||||
|
||||
|
||||
def horizontal_wrap(widgets):
|
||||
"""Wrap all widgets in `widgets` in a horizontal layout.
|
||||
|
||||
If, instead of placing a widget in your list, you place an int or None, an horizontal spacer
|
||||
with the width corresponding to the int will be placed (0 or None means an expanding spacer).
|
||||
"""
|
||||
layout = QHBoxLayout()
|
||||
for widget in widgets:
|
||||
if widget is None or isinstance(widget, int):
|
||||
layout.addItem(horizontal_spacer(size=widget))
|
||||
else:
|
||||
layout.addWidget(widget)
|
||||
return layout
|
||||
|
||||
|
||||
def create_actions(actions, target):
|
||||
# actions are list of (name, shortcut, icon, desc, func)
|
||||
for name, shortcut, icon, desc, func in actions:
|
||||
action = QAction(target)
|
||||
if icon:
|
||||
action.setIcon(QIcon(QPixmap(":/" + icon)))
|
||||
if shortcut:
|
||||
action.setShortcut(shortcut)
|
||||
action.setText(desc)
|
||||
action.triggered.connect(func)
|
||||
setattr(target, name, action)
|
||||
|
||||
|
||||
def set_accel_keys(menu):
|
||||
actions = menu.actions()
|
||||
titles = [a.text() for a in actions]
|
||||
available_characters = {c.lower() for s in titles for c in s if c.isalpha()}
|
||||
for action in actions:
|
||||
text = action.text()
|
||||
c = first(c for c in text if c.lower() in available_characters)
|
||||
if c is None:
|
||||
continue
|
||||
i = text.index(c)
|
||||
newtext = text[:i] + "&" + text[i:]
|
||||
available_characters.remove(c.lower())
|
||||
action.setText(newtext)
|
||||
|
||||
|
||||
def get_appdata(portable=False):
|
||||
if portable:
|
||||
return op.join(executable_folder(), "data")
|
||||
else:
|
||||
return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0]
|
||||
|
||||
|
||||
class SysWrapper(io.IOBase):
|
||||
def write(self, s):
|
||||
if s.strip(): # don't log empty stuff
|
||||
logging.warning(s)
|
||||
|
||||
|
||||
def setup_qt_logging(level=logging.WARNING, log_to_stdout=False):
|
||||
# Under Qt, we log in "debug.log" in appdata. Moreover, when under cx_freeze, we have a
|
||||
# problem because sys.stdout and sys.stderr are None, so we need to replace them with a
|
||||
# wrapper that logs with the logging module.
|
||||
appdata = get_appdata()
|
||||
if not op.exists(appdata):
|
||||
os.makedirs(appdata)
|
||||
# Setup logging
|
||||
# Have to use full configuration over basicConfig as FileHandler encoding was not being set.
|
||||
filename = op.join(appdata, "debug.log") if not log_to_stdout else None
|
||||
log = logging.getLogger()
|
||||
handler = logging.FileHandler(filename, "a", "utf-8")
|
||||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
if sys.stderr is None: # happens under a cx_freeze environment
|
||||
sys.stderr = SysWrapper()
|
||||
if sys.stdout is None:
|
||||
sys.stdout = SysWrapper()
|
||||
|
||||
|
||||
def escape_amp(s):
|
||||
# Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because
|
||||
# that character is used to define "accel keys".
|
||||
return s.replace("&", "&&")
|
||||
Reference in New Issue
Block a user