1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-22 06:37:17 +00:00

Format files with black

- Format all files with black
- Update tox.ini flake8 arguments to be compatible
- Add black to requirements-extra.txt
- Reduce ignored flake8 rules and fix a few violations
This commit is contained in:
2019-12-31 20:16:27 -06:00
parent 359d6498f7
commit 7ba8aa3514
141 changed files with 5241 additions and 3648 deletions

View File

@@ -8,16 +8,29 @@
from PyQt5.QtCore import Qt, QCoreApplication
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout,
QLabel, QApplication)
from PyQt5.QtWidgets import (
QDialog,
QDialogButtonBox,
QSizePolicy,
QHBoxLayout,
QVBoxLayout,
QLabel,
QApplication,
)
from hscommon.trans import trget
tr = trget('qtlib')
tr = trget("qtlib")
class AboutBox(QDialog):
def __init__(self, parent, app, **kwargs):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
flags = (
Qt.CustomizeWindowHint
| Qt.WindowTitleHint
| Qt.WindowSystemMenuHint
| Qt.MSWindowsFixedSizeDialogHint
)
super().__init__(parent, flags, **kwargs)
self.app = app
self._setupUi()
@@ -26,7 +39,9 @@ class AboutBox(QDialog):
self.buttonBox.rejected.connect(self.reject)
def _setupUi(self):
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
self.setWindowTitle(
tr("About {}").format(QCoreApplication.instance().applicationName())
)
self.resize(400, 190)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -35,7 +50,7 @@ class AboutBox(QDialog):
self.setSizePolicy(sizePolicy)
self.horizontalLayout = QHBoxLayout(self)
self.logoLabel = QLabel(self)
self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME))
self.logoLabel.setPixmap(QPixmap(":/%s_big" % self.app.LOGO_NAME))
self.horizontalLayout.addWidget(self.logoLabel)
self.verticalLayout = QVBoxLayout()
self.nameLabel = QLabel(self)
@@ -46,7 +61,9 @@ class AboutBox(QDialog):
self.nameLabel.setText(QCoreApplication.instance().applicationName())
self.verticalLayout.addWidget(self.nameLabel)
self.versionLabel = QLabel(self)
self.versionLabel.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
self.versionLabel.setText(
tr("Version {}").format(QCoreApplication.instance().applicationVersion())
)
self.verticalLayout.addWidget(self.versionLabel)
self.label_3 = QLabel(self)
self.verticalLayout.addWidget(self.label_3)
@@ -64,13 +81,14 @@ class AboutBox(QDialog):
self.horizontalLayout.addLayout(self.verticalLayout)
if __name__ == '__main__':
if __name__ == "__main__":
import sys
app = QApplication([])
QCoreApplication.setOrganizationName('Hardcoded Software')
QCoreApplication.setApplicationName('FooApp')
QCoreApplication.setApplicationVersion('1.2.3')
app.LOGO_NAME = ''
QCoreApplication.setOrganizationName("Hardcoded Software")
QCoreApplication.setApplicationName("FooApp")
QCoreApplication.setApplicationVersion("1.2.3")
app.LOGO_NAME = ""
dialog = AboutBox(None, app)
dialog.show()
sys.exit(app.exec_())

View File

@@ -9,6 +9,7 @@
from PyQt5.QtCore import pyqtSignal, QTimer, QObject
class Application(QObject):
finishedLaunching = pyqtSignal()
@@ -18,4 +19,3 @@ class Application(QObject):
def __launchTimerTimedOut(self):
self.finishedLaunching.emit()

View File

@@ -9,8 +9,18 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QHeaderView
class Column:
def __init__(self, attrname, defaultWidth, editor=None, alignment=Qt.AlignLeft, cantTruncate=False, painter=None, resizeToFit=False):
def __init__(
self,
attrname,
defaultWidth,
editor=None,
alignment=Qt.AlignLeft,
cantTruncate=False,
painter=None,
resizeToFit=False,
):
self.attrname = attrname
self.defaultWidth = defaultWidth
self.editor = editor
@@ -28,6 +38,7 @@ class Columns:
self.model = model
self._headerView = headerView
self._headerView.setDefaultAlignment(Qt.AlignLeft)
def setspecs(col, modelcol):
modelcol.default_width = col.defaultWidth
modelcol.editor = col.editor
@@ -35,12 +46,13 @@ class Columns:
modelcol.resizeToFit = col.resizeToFit
modelcol.alignment = col.alignment
modelcol.cantTruncate = col.cantTruncate
if columns:
for col in columns:
modelcol = self.model.column_by_name(col.attrname)
setspecs(col, modelcol)
else:
col = Column('', 100)
col = Column("", 100)
for modelcol in self.model.column_list:
setspecs(col, modelcol)
self.model.view = self
@@ -50,16 +62,18 @@ class Columns:
# 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.resizeToFit:
self._headerView.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
self._headerView.setSectionResizeMode(
column.logical_index, QHeaderView.ResizeToContents
)
#--- Public
# --- Public
def setColumnsWidth(self, widths):
#`widths` can be None. If it is, then default widths are set.
# `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.
if width == 0: # column was hidden before.
width = column.default_width
self._headerView.resizeSection(column.logical_index, width)
@@ -71,7 +85,7 @@ class Columns:
visualIndex = self._headerView.visualIndex(columnIndex)
self._headerView.moveSection(visualIndex, destIndex)
#--- Events
# --- Events
def headerSectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex):
attrname = self.model.column_by_index(logicalIndex).name
self.model.move_column(attrname, newVisualIndex)
@@ -80,7 +94,7 @@ class Columns:
attrname = self.model.column_by_index(logicalIndex).name
self.model.resize_column(attrname, newSize)
#--- model --> view
# --- model --> view
def restore_columns(self):
columns = self.model.ordered_columns
indexes = [col.logical_index for col in columns]

View File

@@ -1,9 +1,9 @@
# 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
#
# 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
@@ -11,13 +11,21 @@ import sys
import os
from PyQt5.QtCore import Qt, QCoreApplication, QSize
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton
from PyQt5.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
)
from hscommon.trans import trget
from hscommon.desktop import open_url
from .util import horizontalSpacer
tr = trget('qtlib')
tr = trget("qtlib")
class ErrorReportDialog(QDialog):
def __init__(self, parent, github_url, error, **kwargs):
@@ -26,15 +34,17 @@ class ErrorReportDialog(QDialog):
self._setupUi()
name = QCoreApplication.applicationName()
version = QCoreApplication.applicationVersion()
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(name, version, error)
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(
name, version, error
)
# Under windows, we end up with an error report without linesep if we don't mangle it
errorText = errorText.replace('\n', os.linesep)
errorText = errorText.replace("\n", os.linesep)
self.errorTextEdit.setPlainText(errorText)
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)
@@ -70,15 +80,15 @@ class ErrorReportDialog(QDialog):
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))
s = "".join(traceback.format_exception(exctype, value, tb))
dialog = ErrorReportDialog(None, github_url, s)
dialog.exec_()
sys.excepthook = my_excepthook

View File

@@ -11,28 +11,30 @@ from PyQt5.QtCore import Qt, QSettings, QRect, QObject, pyqtSignal
from hscommon.trans import trget
from hscommon.util import tryint
tr = trget('qtlib')
tr = trget("qtlib")
def get_langnames():
return {
'en': tr("English"),
'fr': tr("French"),
'de': tr("German"),
'el': tr("Greek"),
'zh_CN': tr("Chinese (Simplified)"),
'cs': tr("Czech"),
'it': tr("Italian"),
'hy': tr("Armenian"),
'ko': tr("Korean"),
'ru': tr("Russian"),
'uk': tr("Ukrainian"),
'nl': tr('Dutch'),
'pl_PL': tr("Polish"),
'pt_BR': tr("Brazilian"),
'es': tr("Spanish"),
'vi': tr("Vietnamese"),
"en": tr("English"),
"fr": tr("French"),
"de": tr("German"),
"el": tr("Greek"),
"zh_CN": tr("Chinese (Simplified)"),
"cs": tr("Czech"),
"it": tr("Italian"),
"hy": tr("Armenian"),
"ko": tr("Korean"),
"ru": tr("Russian"),
"uk": tr("Ukrainian"),
"nl": tr("Dutch"),
"pl_PL": tr("Polish"),
"pt_BR": tr("Brazilian"),
"es": tr("Spanish"),
"vi": tr("Vietnamese"),
}
def normalize_for_serialization(v):
# QSettings doesn't consider set/tuple as "native" typs for serialization, so if we don't
# change them into a list, we get a weird serialized QVariant value which isn't a very
@@ -43,6 +45,7 @@ def normalize_for_serialization(v):
v = [normalize_for_serialization(item) for item in v]
return v
def adjust_after_deserialization(v):
# In some cases, when reading from prefs, we end up with strings that are supposed to be
# bool or int. Convert these.
@@ -50,18 +53,20 @@ def adjust_after_deserialization(v):
return [adjust_after_deserialization(sub) for sub in v]
if isinstance(v, str):
# might be bool or int, try them
if v == 'true':
if v == "true":
return True
elif v == 'false':
elif v == "false":
return False
else:
return tryint(v, v)
return v
# About QRect conversion:
# I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not
# to rely on it and stay with generic structures.
class Preferences(QObject):
prefsChanged = pyqtSignal()
@@ -123,12 +128,11 @@ class Preferences(QObject):
self.set_value(name, [m] + rectAsList)
def restoreGeometry(self, name, widget):
l = self.get_value(name)
if l and len(l) == 5:
m, x, y, w, h = l
geometry = self.get_value(name)
if geometry and len(geometry) == 5:
m, x, y, w, h = geometry
if m:
widget.setWindowState(Qt.WindowMaximized)
else:
r = QRect(x, y, w, h)
widget.setGeometry(r)

View File

@@ -7,6 +7,7 @@
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QProgressDialog
class ProgressWindow:
def __init__(self, parent, model):
self._window = None
@@ -19,7 +20,7 @@ class ProgressWindow:
self.model.progressdesc_textfield.view = self
# --- Callbacks
def refresh(self): # Labels
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)
@@ -30,7 +31,7 @@ class ProgressWindow:
def show(self):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
self._window = QProgressDialog('', "Cancel", 0, 100, self.parent, flags)
self._window = QProgressDialog("", "Cancel", 0, 100, self.parent, flags)
self._window.setModal(True)
self._window.setAutoReset(False)
self._window.setAutoClose(False)
@@ -52,4 +53,3 @@ class ProgressWindow:
self._window.close()
self._window.setParent(None)
self._window = None

View File

@@ -1,8 +1,8 @@
# 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
#
# 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
@@ -10,6 +10,7 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton
from .util import horizontalSpacer
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
@@ -23,17 +24,17 @@ class RadioBox(QWidget):
self._spacer = horizontalSpacer() if not spread else None
self._layout = QHBoxLayout(self)
self._update_buttons()
#--- Private
# --- Private
def _update_buttons(self):
if self._spacer is not None:
self._layout.removeItem(self._spacer)
to_remove = self._buttons[len(self._labels):]
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):]
del self._buttons[len(self._labels) :]
to_add = self._labels[len(self._buttons) :]
for _ in to_add:
button = QRadioButton(self)
self._buttons.append(button)
@@ -46,43 +47,42 @@ class RadioBox(QWidget):
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))
self._selected_index = max(0, min(self._selected_index, len(self._buttons) - 1))
selected = self._buttons[self._selected_index]
selected.setChecked(True)
#--- Event Handlers
# --- 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
# --- Signals
itemSelected = pyqtSignal(int)
#--- Properties
# --- 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()

View File

@@ -1,9 +1,9 @@
# 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
#
# 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
@@ -14,9 +14,10 @@ from PyQt5.QtWidgets import QAction
from hscommon.trans import trget
from hscommon.util import dedupe
tr = trget('qtlib')
tr = trget("qtlib")
MenuEntry = namedtuple("MenuEntry", "menu fixedItemCount")
MenuEntry = namedtuple('MenuEntry', 'menu fixedItemCount')
class Recent(QObject):
def __init__(self, app, prefName, maxItemCount=10, **kwargs):
@@ -27,19 +28,19 @@ class Recent(QObject):
self._maxItemCount = maxItemCount
self._items = []
self._loadFromPrefs()
self._app.willSavePrefs.connect(self._saveToPrefs)
#--- Private
# --- 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]
self._items = dedupe([item] + self._items)[: self._maxItemCount]
def _refreshMenu(self, menuEntry):
menu, fixedItemCount = menuEntry
for action in menu.actions()[fixedItemCount:]:
@@ -53,43 +54,41 @@ class Recent(QObject):
action = QAction(tr("Clear List"), menu)
action.triggered.connect(self.clear)
menu.addAction(action)
def _refreshAllMenus(self):
for menuEntry in self._menuEntries:
self._refreshMenu(menuEntry)
def _saveToPrefs(self):
setattr(self._app.prefs, self._prefName, self._items)
#--- Public
# --- Public
def addMenu(self, menu):
menuEntry = MenuEntry(menu, len(menu.actions()))
self._menuEntries.append(menuEntry)
self._refreshMenu(menuEntry)
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
# --- Event Handlers
def menuItemWasClicked(self):
action = self.sender()
if action is not None:
item = action.data()
self.mustOpenItem.emit(item)
self._refreshAllMenus()
#--- Signals
# --- Signals
mustOpenItem = pyqtSignal(str)
itemsChanged = pyqtSignal()

View File

@@ -12,15 +12,16 @@ from PyQt5.QtWidgets import QToolButton, QLineEdit, QStyle, QStyleOptionFrame
from hscommon.trans import trget
tr = trget('qtlib')
tr = trget("qtlib")
# 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')
pixmap = QPixmap(":/search_clear_13")
self.setIcon(QIcon(pixmap))
self.setIconSize(pixmap.size())
self.setCursor(Qt.ArrowCursor)
@@ -44,7 +45,7 @@ class ClearableEdit(QLineEdit):
self._clearButton.clicked.connect(self._clearSearch)
self.textChanged.connect(self._textChanged)
#--- Private
# --- Private
def _clearSearch(self):
self.clear()
@@ -54,7 +55,7 @@ class ClearableEdit(QLineEdit):
def _hasClearableContent(self):
return bool(self.text())
#--- QLineEdit overrides
# --- QLineEdit overrides
def resizeEvent(self, event):
if self._is_clearable:
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
@@ -64,7 +65,7 @@ class ClearableEdit(QLineEdit):
rightY = (rect.bottom() - rightHint.height()) // 2
self._clearButton.move(rightX, rightY)
#--- Event Handlers
# --- Event Handlers
def _textChanged(self, text):
if self._is_clearable:
self._updateClearButton()
@@ -79,7 +80,7 @@ class SearchEdit(ClearableEdit):
self.returnPressed.connect(self._returnPressed)
#--- Overrides
# --- Overrides
def _clearSearch(self):
ClearableEdit._clearSearch(self)
self.searchChanged.emit()
@@ -101,20 +102,27 @@ class SearchEdit(ClearableEdit):
if not bool(self.text()) and self.inactiveText and not self.hasFocus():
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
textRect = self.style().subElementRect(
QStyle.SE_LineEditContents, panel, self
)
leftMargin = 2
rightMargin = self._clearButton.iconSize().width()
textRect.adjust(leftMargin, 0, -rightMargin, 0)
painter = QPainter(self)
disabledColor = self.palette().brush(QPalette.Disabled, QPalette.Text).color()
disabledColor = (
self.palette().brush(QPalette.Disabled, QPalette.Text).color()
)
painter.setPen(disabledColor)
painter.drawText(textRect, Qt.AlignLeft|Qt.AlignVCenter, self.inactiveText)
painter.drawText(
textRect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText
)
#--- Event Handlers
# --- 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
# --- Signals
searchChanged = (
pyqtSignal()
) # Emitted when return is pressed or when the test is cleared

View File

@@ -8,6 +8,7 @@
from PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel
class SelectableList(QAbstractListModel):
def __init__(self, model, view, **kwargs):
super().__init__(**kwargs)
@@ -17,7 +18,7 @@ class SelectableList(QAbstractListModel):
self.view.setModel(self)
self.model.view = self
#--- Override
# --- Override
def data(self, index, role):
if not index.isValid():
return None
@@ -31,14 +32,14 @@ class SelectableList(QAbstractListModel):
return 0
return len(self.model)
#--- Virtual
# --- Virtual
def _updateSelection(self):
raise NotImplementedError()
def _restoreSelection(self):
raise NotImplementedError()
#--- model --> view
# --- model --> view
def refresh(self):
self._updating = True
self.beginResetModel()
@@ -49,12 +50,13 @@ class SelectableList(QAbstractListModel):
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
# --- Override
def _updateSelection(self):
index = self.view.currentIndex()
if index != self.model.selected_index:
@@ -65,20 +67,24 @@ class ComboboxModel(SelectableList):
if index is not None:
self.view.setCurrentIndex(index)
#--- Events
# --- 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)
self.view.selectionModel().selectionChanged[
(QItemSelection, QItemSelection)
].connect(self.selectionChanged)
#--- Override
# --- Override
def _updateSelection(self):
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
newIndexes = [
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
]
if newIndexes != self.model.selected_indexes:
self.model.select(newIndexes)
@@ -86,13 +92,17 @@ class ListviewModel(SelectableList):
newSelection = QItemSelection()
for index in self.model.selected_indexes:
newSelection.select(self.createIndex(index, 0), self.createIndex(index, 0))
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
self.view.selectionModel().select(
newSelection, QItemSelectionModel.ClearAndSelect
)
if len(newSelection.indexes()):
currentIndex = newSelection.indexes()[0]
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
self.view.selectionModel().setCurrentIndex(
currentIndex, QItemSelectionModel.Current
)
self.view.scrollTo(currentIndex)
#--- Events
# --- Events
def selectionChanged(self, index):
if not self._updating:
self._updateSelection()

View File

@@ -1,53 +1,72 @@
# 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
#
# 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, QAbstractTableModel, QModelIndex, QItemSelectionModel, QItemSelection
from PyQt5.QtCore import (
Qt,
QAbstractTableModel,
QModelIndex,
QItemSelectionModel,
QItemSelection,
)
from .column import Columns
class Table(QAbstractTableModel):
# Flags you want when index.isValid() is False. In those cases, _getFlags() is never called.
INVALID_INDEX_FLAGS = Qt.ItemIsEnabled
COLUMNS = []
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)
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()
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
newIndexes = [
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
]
if newIndexes != self.model.selected_indexes:
self.model.select(newIndexes)
def _updateViewSelection(self):
# Takes the selection on the model's side and update the view with it.
newSelection = QItemSelection()
columnCount = self.columnCount(QModelIndex())
for index in self.model.selected_indexes:
newSelection.select(self.createIndex(index, 0), self.createIndex(index, columnCount-1))
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
newSelection.select(
self.createIndex(index, 0), self.createIndex(index, columnCount - 1)
)
self.view.selectionModel().select(
newSelection, QItemSelectionModel.ClearAndSelect
)
if len(newSelection.indexes()):
currentIndex = newSelection.indexes()[0]
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
self.view.selectionModel().setCurrentIndex(
currentIndex, QItemSelectionModel.Current
)
self.view.scrollTo(currentIndex)
#--- Data Model methods
# --- Data Model methods
# Virtual
def _getData(self, row, column, role):
if role in (Qt.DisplayRole, Qt.EditRole):
@@ -56,41 +75,41 @@ class Table(QAbstractTableModel):
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_'
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
@@ -103,50 +122,50 @@ class Table(QAbstractTableModel):
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)
self.model.sort_by(attrname, desc=order == Qt.DescendingOrder)
def submit(self):
self.model.save_edits()
return True
#--- Events
# --- Events
def selectionChanged(self, selected, deselected):
self._updateModelSelection()
#--- model --> view
# --- 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
self.view.setFocus() # enough to stop editing
def update_selection(self):
self._updateViewSelection()

View File

@@ -1,23 +1,23 @@
# Created On: 2012/01/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
#
# 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
class TextField:
def __init__(self, model, view):
self.model = model
self.view = view
self.model.view = self
# Make TextField also work for QLabel, which doesn't allow editing
if hasattr(self.view, 'editingFinished'):
if hasattr(self.view, "editingFinished"):
self.view.editingFinished.connect(self.editingFinished)
def editingFinished(self):
self.model.text = self.view.text()
# model --> view
def refresh(self):
self.view.setText(self.model.text)

View File

@@ -1,35 +1,36 @@
# 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
#
# 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
# --- Protected
def _createNode(self, ref, row):
# This returns a TreeNode instance from ref
raise NotImplementedError()
def _getChildren(self):
# This returns a list of ref instances, not TreeNode instances
raise NotImplementedError()
#--- Public
# --- Public
def invalidate(self):
# Invalidates cached data and list of subnodes without resetting ref2node.
self._subnodes = None
#--- Properties
# --- Properties
@property
def subnodes(self):
if self._subnodes is None:
@@ -44,7 +45,7 @@ class NodeContainer:
self._ref2node[child] = node
self._subnodes.append(node)
return self._subnodes
class TreeNode(NodeContainer):
def __init__(self, model, parent, row):
@@ -52,38 +53,42 @@ class TreeNode(NodeContainer):
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 _createNode(self, ref, row):
return RefNode(self.model, self, ref, row)
def _getChildren(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._dummyNodes = set() # dummy nodes' reference have to be kept to avoid segfault
#--- Private
self._dummyNodes = (
set()
) # dummy nodes' reference have to be kept to avoid segfault
# --- Private
def _createDummyNode(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
@@ -91,18 +96,18 @@ class TreeModel(QAbstractItemModel, NodeContainer):
# 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 _lastIndex(self):
"""Index of the very last item in the tree.
"""
currentIndex = QModelIndex()
rowCount = self.rowCount(currentIndex)
while rowCount > 0:
currentIndex = self.index(rowCount-1, 0, currentIndex)
currentIndex = self.index(rowCount - 1, 0, currentIndex)
rowCount = self.rowCount(currentIndex)
return currentIndex
#--- Overrides
# --- Overrides
def index(self, row, column, parent):
if not self.subnodes:
return QModelIndex()
@@ -110,13 +115,17 @@ class TreeModel(QAbstractItemModel, NodeContainer):
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)
logging.debug(
"Wrong tree index called (%r, %r, %r). Returning DummyNode",
row,
column,
node,
)
parentNode = parent.internalPointer() if parent.isValid() else None
dummy = self._createDummyNode(parentNode, row)
self._dummyNodes.add(dummy)
return self.createIndex(row, column, dummy)
def parent(self, index):
if not index.isValid():
return QModelIndex()
@@ -125,22 +134,22 @@ class TreeModel(QAbstractItemModel, NodeContainer):
return QModelIndex()
else:
return self.createIndex(node.parent.row, 0, node.parent)
def reset(self):
super().beginResetModel()
self.invalidate()
self._ref2node = {}
self._dummyNodes = set()
super().endResetModel()
def rowCount(self, parent=QModelIndex()):
node = parent.internalPointer() if parent.isValid() else self
return len(node.subnodes)
#--- Public
# --- Public
def findIndex(self, rowPath):
"""Returns the QModelIndex at `rowPath`
`rowPath` 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.
"""
@@ -148,7 +157,7 @@ class TreeModel(QAbstractItemModel, NodeContainer):
for row in rowPath:
result = self.index(row, 0, result)
return result
@staticmethod
def pathForIndex(index):
reversedPath = []
@@ -156,10 +165,10 @@ class TreeModel(QAbstractItemModel, NodeContainer):
reversedPath.append(index.row())
index = index.parent()
return list(reversed(reversedPath))
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
@@ -168,6 +177,5 @@ class TreeModel(QAbstractItemModel, NodeContainer):
columnCount = self.columnCount()
topLeft = self.index(0, 0, QModelIndex())
bottomLeft = self._lastIndex()
bottomRight = self.sibling(bottomLeft.row(), columnCount-1, bottomLeft)
bottomRight = self.sibling(bottomLeft.row(), columnCount - 1, bottomLeft)
self.dataChanged.emit(topLeft, bottomRight)

View File

@@ -16,25 +16,35 @@ from hscommon.util import first
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import QDesktopWidget, QSpacerItem, QSizePolicy, QAction, QHBoxLayout
from PyQt5.QtWidgets import (
QDesktopWidget,
QSpacerItem,
QSizePolicy,
QAction,
QHBoxLayout,
)
def moveToScreenCenter(widget):
frame = widget.frameGeometry()
frame.moveCenter(QDesktopWidget().availableGeometry().center())
widget.move(frame.topLeft())
def verticalSpacer(size=None):
if size:
return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed)
else:
return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
def horizontalSpacer(size=None):
if size:
return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed)
else:
return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
def horizontalWrap(widgets):
"""Wrap all widgets in `widgets` in a horizontal layout.
@@ -49,18 +59,20 @@ def horizontalWrap(widgets):
layout.addWidget(widget)
return layout
def createActions(actions, target):
# actions = [(name, shortcut, icon, desc, func)]
for name, shortcut, icon, desc, func in actions:
action = QAction(target)
if icon:
action.setIcon(QIcon(QPixmap(':/' + icon)))
action.setIcon(QIcon(QPixmap(":/" + icon)))
if shortcut:
action.setShortcut(shortcut)
action.setText(desc)
action.triggered.connect(func)
setattr(target, name, action)
def setAccelKeys(menu):
actions = menu.actions()
titles = [a.text() for a in actions]
@@ -71,18 +83,21 @@ def setAccelKeys(menu):
if c is None:
continue
i = text.index(c)
newtext = text[:i] + '&' + text[i:]
newtext = text[:i] + "&" + text[i:]
available_characters.remove(c.lower())
action.setText(newtext)
def getAppData():
return QStandardPaths.standardLocations(QStandardPaths.DataLocation)[0]
class SysWrapper(io.IOBase):
def write(self, s):
if s.strip(): # don't log empty stuff
if s.strip(): # don't log empty stuff
logging.warning(s)
def setupQtLogging(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
@@ -90,20 +105,21 @@ def setupQtLogging(level=logging.WARNING, log_to_stdout=False):
appdata = getAppData()
if not op.exists(appdata):
os.makedirs(appdata)
# Setup logging
# 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
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 = 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
if sys.stderr is None: # happens under a cx_freeze environment
sys.stderr = SysWrapper()
if sys.stdout is None:
sys.stdout = SysWrapper()
def escapeamp(s):
# Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because
# that character is used to define "accel keys".
return s.replace('&', '&&')
return s.replace("&", "&&")