mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-02-05 12:51:39 +00:00
Finish moving all qtlib py files to qt
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
Copyright 2014, Hardcoded Software Inc., http://www.hardcoded.net
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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 qtlib.util import move_to_screen_center
|
||||
from hscommon.trans import trget
|
||||
|
||||
tr = trget("qtlib")
|
||||
|
||||
|
||||
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)
|
||||
110
qtlib/column.py
110
qtlib/column.py
@@ -1,110 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,96 +0,0 @@
|
||||
# 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 .util import horizontal_spacer
|
||||
|
||||
tr = trget("qtlib")
|
||||
|
||||
|
||||
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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 398 B |
@@ -1,61 +0,0 @@
|
||||
# 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
|
||||
@@ -1,88 +0,0 @@
|
||||
# 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 .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()
|
||||
@@ -1,94 +0,0 @@
|
||||
# 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")
|
||||
|
||||
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()
|
||||
@@ -1,120 +0,0 @@
|
||||
# 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")
|
||||
|
||||
# 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
|
||||
@@ -1,100 +0,0 @@
|
||||
# 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()
|
||||
160
qtlib/table.py
160
qtlib/table.py
@@ -1,160 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,178 +0,0 @@
|
||||
# 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
qtlib/util.py
139
qtlib/util.py
@@ -1,139 +0,0 @@
|
||||
# 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