mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-28 09:58:10 +03:00
622 lines
17 KiB
Python
622 lines
17 KiB
Python
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
|
#
|
|
# This file is part of Furious.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import annotations
|
|
|
|
from Furious.QtFramework.Ancestors import *
|
|
from Furious.QtFramework.DynamicTheme import AppHue
|
|
from Furious.QtFramework.DynamicTranslate import gettext as _
|
|
from Furious.QtFramework.QtGui import AppQAction, AppQSeperator
|
|
from Furious.PyFramework import *
|
|
from Furious.Utility import *
|
|
from Furious.Library import *
|
|
|
|
from PySide6 import QtCore
|
|
from PySide6.QtGui import *
|
|
from PySide6.QtWidgets import *
|
|
|
|
import functools
|
|
|
|
__all__ = [
|
|
'moveToCenter',
|
|
'AppQCheckBox',
|
|
'AppQDialog',
|
|
'AppQDialogButtonBox',
|
|
'AppQGroupBox',
|
|
'AppQHeaderView',
|
|
'AppQLabel',
|
|
'AppQListWidget',
|
|
'AppQMainWindow',
|
|
'AppQMenu',
|
|
'AppQMenuBar',
|
|
'AppQMessageBox',
|
|
'AppQPushButton',
|
|
'AppQTableWidget',
|
|
'AppQTabWidget',
|
|
'AppQToolBar',
|
|
'QuestionDeleteMBox',
|
|
'NewChangesNextTimeMBox',
|
|
'showNewChangesNextTimeMBox',
|
|
'UnrecognizedConfigMBox',
|
|
'showUnrecognizedConfigMBox',
|
|
]
|
|
|
|
|
|
def moveToCenter(widget, parent=None):
|
|
geometry = widget.geometry()
|
|
|
|
if parent is None:
|
|
center = QApplication.primaryScreen().availableGeometry().center()
|
|
else:
|
|
center = parent.geometry().center()
|
|
|
|
geometry.moveCenter(center)
|
|
|
|
widget.move(geometry.topLeft())
|
|
|
|
|
|
class AppQCheckBox(QTranslatable, QCheckBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
self.setText(_(self.text()))
|
|
|
|
|
|
class AppQDialog(QTranslatable, SupportConnectedCallback, QDialog):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self._firstShowCall = True
|
|
|
|
if PLATFORM != 'Darwin':
|
|
self.setWidthAndHeight()
|
|
|
|
self.setWindowIcon(AppHue.currentWindowIcon())
|
|
|
|
def setWidthAndHeight(self):
|
|
pass
|
|
|
|
def exec(self):
|
|
self.show()
|
|
|
|
return super().exec()
|
|
|
|
def open(self):
|
|
self.show()
|
|
|
|
return super().open()
|
|
|
|
def show(self):
|
|
super().show()
|
|
|
|
if PLATFORM == 'Darwin':
|
|
if self._firstShowCall:
|
|
self.setWidthAndHeight()
|
|
|
|
self._firstShowCall = False
|
|
|
|
moveToCenter(self)
|
|
|
|
def retranslate(self):
|
|
self.setWindowTitle(_(self.windowTitle()))
|
|
|
|
def disconnectedCallback(self):
|
|
self.setWindowIcon(AppHue.disconnectedWindowIcon())
|
|
|
|
def connectedCallback(self):
|
|
self.setWindowIcon(AppHue.connectedWindowIcon())
|
|
|
|
|
|
class AppQDialogButtonBox(QTranslatable, QDialogButtonBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
for button in self.buttons():
|
|
button.setText(_(button.text()))
|
|
|
|
|
|
class AppQGroupBox(QTranslatable, QGroupBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
self.setTitle(_(self.title()))
|
|
|
|
|
|
class AppQHeaderView(SupportExitCleanup, SupportConnectedCallback, QHeaderView):
|
|
def sectionSizeSettingsEmpty(self):
|
|
return self.sectionSizeSettingsName == ''
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.sectionSizeSettingsName = kwargs.pop('sectionSizeSettingsName', '')
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.columnCount = self.parent().columnCount()
|
|
self.sectionSizeTable = {}
|
|
|
|
self.setSectionsClickable(True)
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.currentColor()))
|
|
self.setFont(QFont(APP().customFontName))
|
|
|
|
self.sectionResized.connect(self.handleSectionResized)
|
|
|
|
def restoreSectionSize(self):
|
|
if self.sectionSizeSettingsEmpty():
|
|
return
|
|
|
|
try:
|
|
# https://bugreports.qt.io/browse/QTBUG-119862
|
|
# Affected: PySide6 6.6.1+
|
|
self.setDefaultSectionSize(self.defaultSectionSize())
|
|
|
|
self.sectionSizeTable = UJSONEncoder.decode(
|
|
AppSettings.get(self.sectionSizeSettingsName)
|
|
)
|
|
|
|
# Fill missing value
|
|
for column in range(self.columnCount):
|
|
if self.sectionSizeTable.get(str(column)) is None:
|
|
self.sectionSizeTable[str(column)] = self.defaultSectionSize()
|
|
|
|
with QBlockSignals(self):
|
|
for key, value in self.sectionSizeTable.items():
|
|
self.resizeSection(int(key), value)
|
|
except Exception:
|
|
# Any non-exit exceptions
|
|
|
|
# Leave keys as strings since they will be
|
|
# loaded as string from json
|
|
self.sectionSizeTable = {
|
|
str(column): self.defaultSectionSize()
|
|
for column in range(self.columnCount)
|
|
}
|
|
|
|
def setCustomSectionResizeMode(self):
|
|
# Horizontal header resize mode
|
|
for index in range(self.columnCount):
|
|
if index < self.columnCount - 1:
|
|
self.setSectionResizeMode(index, AppQHeaderView.ResizeMode.Interactive)
|
|
else:
|
|
self.setSectionResizeMode(index, AppQHeaderView.ResizeMode.Stretch)
|
|
|
|
@staticmethod
|
|
def getStyleSheet(color):
|
|
return f'QHeaderView::section:hover {{ background-color: {color}; }}'
|
|
|
|
def disconnectedCallback(self):
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.disconnectedColor()))
|
|
|
|
def connectedCallback(self):
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.connectedColor()))
|
|
|
|
@QtCore.Slot(int, int, int)
|
|
def handleSectionResized(self, index: int, oldSize: int, newSize: int):
|
|
if self.sectionSizeSettingsEmpty():
|
|
return
|
|
|
|
# Keys are string when loaded from json
|
|
self.sectionSizeTable[str(index)] = newSize
|
|
|
|
def cleanup(self):
|
|
if self.sectionSizeSettingsEmpty():
|
|
return
|
|
|
|
AppSettings.set(
|
|
self.sectionSizeSettingsName,
|
|
UJSONEncoder.encode(self.sectionSizeTable),
|
|
)
|
|
|
|
|
|
class AppQLabel(QTranslatable, QLabel):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
self.setText(_(self.text()))
|
|
|
|
|
|
class AppQListWidget(SupportConnectedCallback, QListWidget):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setSelectionColor(AppHue.disconnectedColor())
|
|
|
|
def setSelectionColor(self, color):
|
|
self.setStyleSheet(
|
|
f'QListWidget::item:selected {{'
|
|
f' background: {color};'
|
|
f'}}'
|
|
f''
|
|
f'QListWidget::item:hover {{'
|
|
f' background: {color};'
|
|
f'}}'
|
|
)
|
|
|
|
@property
|
|
def selectedIndex(self):
|
|
return sorted(list(set(index.row() for index in self.selectedIndexes())))
|
|
|
|
def disconnectedCallback(self):
|
|
self.setSelectionColor(AppHue.disconnectedColor())
|
|
|
|
def connectedCallback(self):
|
|
self.setSelectionColor(AppHue.connectedColor())
|
|
|
|
|
|
class AppQMainWindow(
|
|
QTranslatable,
|
|
SupportConnectedCallback,
|
|
SupportExitCleanup,
|
|
QMainWindow,
|
|
):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self._firstShowCall = True
|
|
|
|
self.setWindowIcon(AppHue.currentWindowIcon())
|
|
|
|
self._menuBar = AppQMenuBar(parent=self)
|
|
self.setMenuBar(self._menuBar)
|
|
|
|
if PLATFORM != 'Darwin':
|
|
self.setWidthAndHeight()
|
|
|
|
def setWidthAndHeight(self):
|
|
pass
|
|
|
|
def show(self):
|
|
super().show()
|
|
|
|
if PLATFORM == 'Darwin':
|
|
if self._firstShowCall:
|
|
self.setWidthAndHeight()
|
|
|
|
self._firstShowCall = False
|
|
|
|
moveToCenter(self)
|
|
|
|
if PLATFORM == 'Darwin':
|
|
self.activateWindow()
|
|
self.raise_()
|
|
|
|
def closeEvent(self, event):
|
|
event.ignore()
|
|
|
|
self.hide()
|
|
|
|
def retranslate(self):
|
|
self.setWindowTitle(_(self.windowTitle()))
|
|
|
|
def disconnectedCallback(self):
|
|
self.setWindowIcon(AppHue.disconnectedWindowIcon())
|
|
|
|
def connectedCallback(self):
|
|
self.setWindowIcon(AppHue.connectedWindowIcon())
|
|
|
|
def cleanup(self):
|
|
pass
|
|
|
|
|
|
class AppQMenu(QTranslatable, SupportConnectedCallback, QMenu):
|
|
def __init__(self, *actions, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
# In some old version PySide6, the self.actions() method
|
|
# does not return with seperators. _actions list append
|
|
# them all
|
|
self._actions = []
|
|
|
|
for action in actions:
|
|
if isinstance(action, AppQSeperator):
|
|
self._actions.append(action)
|
|
self.addSeparator()
|
|
elif isinstance(action, AppQAction):
|
|
self._actions.append(action)
|
|
self.addAction(action)
|
|
else:
|
|
# Do nothing
|
|
pass
|
|
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.currentColor()))
|
|
|
|
@staticmethod
|
|
def getStyleSheet(color):
|
|
return (
|
|
f'QMenu::item {{'
|
|
f' background-color: solid;'
|
|
f'}}'
|
|
f''
|
|
f'QMenu::item:selected {{'
|
|
f' background-color: {color};'
|
|
f'}}'
|
|
)
|
|
|
|
def retranslate(self):
|
|
self.setTitle(_(self.title()))
|
|
|
|
def disconnectedCallback(self):
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.disconnectedColor()))
|
|
|
|
def connectedCallback(self):
|
|
self.setStyleSheet(self.getStyleSheet(AppHue.connectedColor()))
|
|
|
|
|
|
class AppQMenuBar(SupportConnectedCallback, QMenuBar):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setSelectionColor(AppHue.currentColor())
|
|
|
|
@staticmethod
|
|
def getStyleSheet(color):
|
|
return (
|
|
f'QMenuBar::item:selected {{'
|
|
f' background: {color};'
|
|
f'}}'
|
|
f''
|
|
f'QMenuBar::item:hover {{'
|
|
f' background: {color};'
|
|
f'}}'
|
|
)
|
|
|
|
def setSelectionColor(self, color):
|
|
self.setStyleSheet(self.getStyleSheet(color))
|
|
|
|
def disconnectedCallback(self):
|
|
self.setSelectionColor(AppHue.disconnectedColor())
|
|
|
|
def connectedCallback(self):
|
|
self.setSelectionColor(AppHue.connectedColor())
|
|
|
|
|
|
class AppQMessageBox(QTranslatable, SupportConnectedCallback, QMessageBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setWindowIcon(AppHue.currentWindowIcon())
|
|
|
|
def moveToCenter(self):
|
|
moveToCenter(self, self.parentWidget())
|
|
|
|
return self
|
|
|
|
def exec(self):
|
|
self.show()
|
|
self.moveToCenter()
|
|
|
|
return super().exec()
|
|
|
|
def open(self):
|
|
self.show()
|
|
self.moveToCenter()
|
|
|
|
return super().open()
|
|
|
|
def retranslate(self):
|
|
self.setWindowTitle(_(self.windowTitle()))
|
|
self.setText(_(self.text()))
|
|
|
|
try:
|
|
self.setInformativeText(_(self.informativeText()))
|
|
except KeyError:
|
|
# Any translatable informative text
|
|
pass
|
|
|
|
for button in self.buttons():
|
|
if button.text().find('OK') != -1:
|
|
# &OK...
|
|
pass
|
|
else:
|
|
button.setText(_(button.text()))
|
|
|
|
self.moveToCenter()
|
|
|
|
def disconnectedCallback(self):
|
|
self.setWindowIcon(AppHue.disconnectedWindowIcon())
|
|
|
|
def connectedCallback(self):
|
|
self.setWindowIcon(AppHue.connectedWindowIcon())
|
|
|
|
|
|
class AppQPushButton(QTranslatable, QPushButton):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
self.setText(_(self.text()))
|
|
|
|
|
|
class AppQTableWidget(SupportConnectedCallback, QTableWidget):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setWordWrap(False)
|
|
|
|
@property
|
|
def selectedIndex(self):
|
|
return sorted(list(set(index.row() for index in self.selectedIndexes())))
|
|
|
|
@staticmethod
|
|
def getStyleSheet(color):
|
|
return f'QTableWidget {{ selection-background-color: {color}; }}'
|
|
|
|
def setSelectionColor(self, color):
|
|
self.setStyleSheet(self.getStyleSheet(color))
|
|
|
|
def activateItemByIndex(self, index, activate):
|
|
if activate:
|
|
for column in range(self.columnCount()):
|
|
item = self.item(int(index), column)
|
|
|
|
if item is None:
|
|
# Do nothing
|
|
continue
|
|
|
|
font = item.font()
|
|
font.setBold(True)
|
|
|
|
item.setFont(font)
|
|
item.setForeground(QColor(AppHue.currentColor()))
|
|
else:
|
|
for column in range(self.columnCount()):
|
|
item = self.item(int(index), column)
|
|
|
|
if item is None:
|
|
# Do nothing
|
|
continue
|
|
|
|
font = item.font()
|
|
font.setBold(False)
|
|
|
|
item.setFont(font)
|
|
item.setForeground(QBrush())
|
|
|
|
def selectMultipleRows(self, indexes: list[int], clearCurrentSelection: bool):
|
|
if clearCurrentSelection:
|
|
self.selectionModel().clearSelection()
|
|
|
|
selection = self.selectionModel().selection()
|
|
|
|
for index in indexes:
|
|
selection.select(
|
|
self.model().index(index, 0),
|
|
self.model().index(index, self.columnCount() - 1),
|
|
)
|
|
|
|
self.selectionModel().select(
|
|
selection, QtCore.QItemSelectionModel.SelectionFlag.Select
|
|
)
|
|
|
|
def disconnectedCallback(self):
|
|
self.setSelectionColor(AppHue.disconnectedColor())
|
|
|
|
def connectedCallback(self):
|
|
self.setSelectionColor(AppHue.connectedColor())
|
|
|
|
|
|
class AppQTabWidget(QTranslatable, QTabWidget):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def retranslate(self):
|
|
for index in range(self.count()):
|
|
self.setTabText(index, _(self.tabText(index)))
|
|
|
|
|
|
class AppQToolBar(QTranslatable, QToolBar):
|
|
def __init__(self, *actions, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
self._actions = []
|
|
|
|
for action in actions:
|
|
if isinstance(action, AppQSeperator):
|
|
self._actions.append(action)
|
|
self.addSeparator()
|
|
elif isinstance(action, AppQAction):
|
|
self._actions.append(action)
|
|
self.addAction(action)
|
|
else:
|
|
# Do nothing
|
|
pass
|
|
|
|
self.setStyleSheet(self.getStyleSheet())
|
|
|
|
@staticmethod
|
|
def getStyleSheet():
|
|
return f'QToolBar {{ spacing: 5px; }}'
|
|
|
|
def retranslate(self):
|
|
self.setWindowTitle(_(self.windowTitle()))
|
|
|
|
|
|
class QuestionDeleteMBox(AppQMessageBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.isMulti = False
|
|
self.possibleRemark = ''
|
|
|
|
self.setWindowTitle(_('Delete'))
|
|
self.setStandardButtons(
|
|
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
|
)
|
|
|
|
def customText(self) -> str:
|
|
if self.isMulti:
|
|
return _('Delete these items?')
|
|
else:
|
|
return _('Delete this item?') + f'\n\n{self.possibleRemark}'
|
|
|
|
def retranslate(self):
|
|
self.setWindowTitle(_(self.windowTitle()))
|
|
self.setText(self.customText())
|
|
|
|
# Ignore informative text, buttons
|
|
|
|
self.moveToCenter()
|
|
|
|
|
|
class NewChangesNextTimeMBox(AppQMessageBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setIcon(AppQMessageBox.Icon.Information)
|
|
self.setText(_('New changes will take effect next time'))
|
|
|
|
|
|
def showNewChangesNextTimeMBox(**kwargs):
|
|
try:
|
|
if APP().isSystemTrayConnected():
|
|
mbox = NewChangesNextTimeMBox(**kwargs)
|
|
|
|
if isinstance(mbox.parent(), QMainWindow):
|
|
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
|
else:
|
|
mbox.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
|
|
# Show the MessageBox asynchronously
|
|
mbox.open()
|
|
except Exception:
|
|
# Any non-exit exceptions
|
|
|
|
pass
|
|
|
|
|
|
class UnrecognizedConfigMBox(AppQMessageBox):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setIcon(AppQMessageBox.Icon.Critical)
|
|
self.setText(_('Unrecognized Configuration. Please modify it in the editor'))
|
|
|
|
|
|
def showUnrecognizedConfigMBox(**kwargs):
|
|
mbox = UnrecognizedConfigMBox(**kwargs)
|
|
|
|
if isinstance(mbox.parent(), QMainWindow):
|
|
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
|
else:
|
|
mbox.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
|
|
# Show the MessageBox asynchronously
|
|
mbox.open()
|