Files
LorenEteval_Furious/Furious/Widget/UserSubsQTableWidget.py
T
2025-12-02 15:47:26 +08:00

534 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024–present Loren Eteval & contributors <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.Frozenlib import *
from Furious.Library import *
from Furious.Qt import *
from Furious.Qt import gettext as _
from PySide6 import QtCore
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from typing import Union, Callable
import logging
import functools
logger = logging.getLogger(__name__)
__all__ = ['UserSubsQTableWidget']
# Migrate legacy settings
registerAppSettings('SubscriptionWidgetSectionSizeTable')
registerAppSettings('UserSubsHeaderViewState')
class UserSubsQTableWidgetHorizontalHeader(AppQHeaderView):
def __init__(self, *args, **kwargs):
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
class UserSubsQTableWidgetVerticalHeader(AppQHeaderView):
def __init__(self, *args, **kwargs):
super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs)
class UserSubsQTableWidgetHeaders:
def __init__(self, name: str, func: Callable[[dict], str] = None):
self.name = name
self.func = func
def __call__(self, item: dict) -> str:
if callable(self.func):
return self.func(item)
else:
return ''
def __eq__(self, other):
return str(self) == str(other)
def __str__(self):
return self.name
class UserSubsAppQComboBox(AppQComboBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def retranslate(self):
# Do not emit 'currentTextChanged' when retranslate
with Mixins.QBlockSignalContext(self):
super().retranslate()
# Headers VALUE
_TRANSLATABLE_HEADERS = [
_('Never'),
_('Every 5 mins'),
_('Every 10 mins'),
_('Every 15 mins'),
_('Every 30 mins'),
_('Every 45 mins'),
_('Every 1 hour'),
_('Every 2 hours'),
_('Every 3 hours'),
_('Every 6 hours'),
_('Every 8 hours'),
_('Every 10 hours'),
_('Every 12 hours'),
_('Every 24 hours'),
_('Use current proxy'),
_('Force proxy'),
_('No proxy'),
_('Remark'),
_('Auto Update'),
_('Auto Update Use Proxy'),
]
class UserSubsQTableWidget(Mixins.QTranslatable, AppQTableWidget):
AutoUpdateOptions = {
'': None,
'Never': None,
'Every 5 mins': 5 * 60 * 1000,
'Every 10 mins': 10 * 60 * 1000,
'Every 15 mins': 15 * 60 * 1000,
'Every 30 mins': 30 * 60 * 1000,
'Every 45 mins': 45 * 60 * 1000,
'Every 1 hour': 1 * 60 * 60 * 1000,
'Every 2 hours': 2 * 60 * 60 * 1000,
'Every 3 hours': 3 * 60 * 60 * 1000,
'Every 6 hours': 6 * 60 * 60 * 1000,
'Every 8 hours': 8 * 60 * 60 * 1000,
'Every 10 hours': 10 * 60 * 60 * 1000,
'Every 12 hours': 12 * 60 * 60 * 1000,
'Every 24 hours': 24 * 60 * 60 * 1000,
}
ProxyOptions = {
'': lambda: None,
'Use current proxy': lambda: Storage.Extras.UserHttpProxy(),
'Force proxy': lambda: '127.0.0.1:10809',
'No proxy': lambda: None,
}
Headers = [
UserSubsQTableWidgetHeaders('Remark', lambda item: item.get('remark', '')),
UserSubsQTableWidgetHeaders('URL', lambda item: item.get('webURL', '')),
UserSubsQTableWidgetHeaders(
'Auto Update', lambda item: item.get('autoupdate', '')
),
UserSubsQTableWidgetHeaders(
'Auto Update Use Proxy', lambda item: item.get('proxy', '')
),
]
# Corresponds to 'Headers'
ItemKey = ['remark', 'webURL', 'autoupdate', 'proxy']
def __init__(self, *args, **kwargs):
self.deleteUniqueCallback = kwargs.pop('deleteUniqueCallback', None)
super().__init__(*args, **kwargs)
self.timers = list(QtCore.QTimer() for i in range(len(Storage.UserSubs())))
# Must set before flush all
self.setColumnCount(len(self.Headers))
# Flush all data to table
self.flushAll()
# Install custom header
self.setHorizontalHeader(
UserSubsQTableWidgetHorizontalHeader(
parent=self,
legacySectionSizeSettingsName='SubscriptionWidgetSectionSizeTable',
sectionSizeSettingsName='UserSubsHeaderViewState',
)
)
self.setVerticalHeader(UserSubsQTableWidgetVerticalHeader(self))
self.horizontalHeader().setCustomSectionResizeMode()
self.horizontalHeader().restoreSectionSize()
self.setHorizontalHeaderLabels(list(_(str(header)) for header in self.Headers))
# Selection
self.setSelectionColor(AppHue.disconnectedColor())
self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
# No drag and drop
self.setDragEnabled(False)
self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
self.setDropIndicatorShown(False)
self.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction)
contextMenuActions = [
AppQAction(
_('Delete'),
callback=lambda: self.deleteSelectedItem(),
),
]
self.contextMenu = AppQMenu(*contextMenuActions)
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
# Signals
self.itemChanged.connect(self.handleItemChanged)
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
@QtCore.Slot(QTableWidgetItem)
def handleItemChanged(self, item: QTableWidgetItem):
unique = list(Storage.UserSubs().keys())[item.row()]
mapped = UserSubsQTableWidget.ItemKey[item.column()]
# 'autoupdate', 'proxy' is not triggered here
assert mapped not in ['autoupdate', 'proxy']
item.setToolTip(item.text())
Storage.UserSubs()[unique][mapped] = item.text()
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
self.contextMenu.exec(self.mapToGlobal(point))
def deleteSelectedItem(self):
indexes = self.selectedIndex
if len(indexes) == 0:
# Nothing to do
return
def handleResultCode(_indexes, code):
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
for i in range(len(_indexes)):
deleteIndex = _indexes[i] - i
deleteUnique = list(Storage.UserSubs().keys())[deleteIndex]
self.removeRow(deleteIndex)
Storage.UserSubs().pop(deleteUnique)
# Begin timer cleanup
qtimer = self.timers[deleteIndex]
assert isinstance(qtimer, QtCore.QTimer)
try:
qtimer.timeout.disconnect()
except Exception as ex:
# Any non-exit exceptions
# Disconnect all previous signals if possible
pass
qtimer.stop()
# End timer cleanup
# Remove timer from list
self.timers.pop(deleteIndex)
if callable(self.deleteUniqueCallback):
self.deleteUniqueCallback(deleteUnique)
else:
# Do not delete
pass
if PLATFORM == 'Windows':
# Windows
mbox = MBoxQuestionDelete(icon=AppQMessageBox.Icon.Question)
else:
# macOS & linux
mbox = MBoxQuestionDelete(
icon=AppQMessageBox.Icon.Question, parent=self.parent()
)
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.isMulti = bool(len(indexes) > 1)
mbox.possibleRemark = self.item(indexes[0], 0).text()
mbox.setText(mbox.customText())
mbox.finished.connect(functools.partial(handleResultCode, indexes))
# Show the MessageBox asynchronously
mbox.open()
def handleAutoUpdateComboBoxCurrentTextChanged(self, text: str, row: int):
textEnglish = _(text, 'EN')
unique = list(Storage.UserSubs().keys())[row]
subsob = Storage.UserSubs()[unique]
remark, webURL = subsob['remark'], subsob['webURL']
try:
timems = self.AutoUpdateOptions[textEnglish]
except KeyError:
# Invalid key. Reset
logger.error(f'\'{textEnglish}\' is not in auto update options. Reset')
textEnglish = ''
# Write to subs object
subsob['autoupdate'] = textEnglish
timems = self.AutoUpdateOptions[textEnglish]
qtimer = self.timers[row]
assert isinstance(qtimer, QtCore.QTimer)
if timems is not None:
logger.info(
f'start auto update job for subscription ({remark}, {webURL}). '
f'Interval is {timems // (60 * 1000)} mins'
)
def getHttpProxy(_subsob):
return self.ProxyOptions[_subsob.get('proxy', '')]()
try:
qtimer.timeout.disconnect()
except Exception as ex:
# Any non-exit exceptions
# Disconnect all previous signals if possible
pass
qtimer.timeout.connect(
functools.partial(
self.updateSubsByUnique,
unique=unique,
httpProxy=functools.partial(getHttpProxy, subsob),
)
)
qtimer.start(timems)
else:
logger.info(f'stop auto update job for subscription ({remark}, {webURL})')
try:
qtimer.timeout.disconnect()
except Exception as ex:
# Any non-exit exceptions
# Disconnect all previous signals if possible
pass
qtimer.stop()
# Write to subs object
subsob['autoupdate'] = textEnglish
# Return potentially fixed value
return textEnglish
def handleProxyComboBoxCurrentTextChanged(self, text: str, row: int):
textEnglish = _(text, 'EN')
unique = list(Storage.UserSubs().keys())[row]
subsob = Storage.UserSubs()[unique]
try:
proxyFn = self.ProxyOptions[textEnglish]
except KeyError:
# Invalid key. Reset
logger.error(f'\'{textEnglish}\' is not in proxy options. Reset')
textEnglish = ''
# Write to subs object
subsob['proxy'] = textEnglish
proxyFn = self.ProxyOptions[textEnglish]
assert callable(proxyFn)
# Write to subs object
subsob['proxy'] = textEnglish
# Return potentially fixed value
return textEnglish
def flushItem(self, row, column, item):
if column == self.Headers.index('Auto Update'):
header = self.Headers[column]
oldItem = self.item(row, column)
newItem = UserSubsAppQComboBox()
newItem.addItems(list(_(key) for key in self.AutoUpdateOptions.keys()))
if oldItem is None:
# Item does not exist
newItem.setFont(QFont(AppFontName()))
else:
# Use existing
newItem.setFont(oldItem.font())
index = newItem.findText(
_(self.handleAutoUpdateComboBoxCurrentTextChanged(header(item), row))
)
if index >= 0:
newItem.setCurrentIndex(index)
newItem.currentTextChanged.connect(
functools.partial(
self.handleAutoUpdateComboBoxCurrentTextChanged,
row=row,
)
)
self.setCellWidget(row, column, newItem)
elif column == self.Headers.index('Auto Update Use Proxy'):
header = self.Headers[column]
oldItem = self.item(row, column)
newItem = UserSubsAppQComboBox()
newItem.addItems(list(_(key) for key in self.ProxyOptions.keys()))
if oldItem is None:
# Item does not exist
newItem.setFont(QFont(AppFontName()))
else:
# Use existing
newItem.setFont(oldItem.font())
index = newItem.findText(
_(self.handleProxyComboBoxCurrentTextChanged(header(item), row))
)
if index >= 0:
newItem.setCurrentIndex(index)
newItem.currentTextChanged.connect(
functools.partial(
self.handleProxyComboBoxCurrentTextChanged,
row=row,
)
)
self.setCellWidget(row, column, newItem)
else:
header = self.Headers[column]
text = header(item)
oldItem = self.item(row, column)
newItem = QTableWidgetItem(text)
newItem.setToolTip(text)
if oldItem is None:
# Item does not exist
newItem.setFont(QFont(AppFontName()))
else:
# Use existing
newItem.setFont(oldItem.font())
newItem.setForeground(oldItem.foreground())
if oldItem.textAlignment() != 0:
newItem.setTextAlignment(oldItem.textAlignment())
# Editable
newItem.setFlags(
QtCore.Qt.ItemFlag.ItemIsEnabled
| QtCore.Qt.ItemFlag.ItemIsSelectable
| QtCore.Qt.ItemFlag.ItemIsEditable
)
self.setItem(row, column, newItem)
def flushRow(self, row, item):
for column in list(range(self.columnCount())):
self.flushItem(row, column, item)
def flushAll(self):
if self.rowCount() == 0:
# Should insert row
for index, key in enumerate(Storage.UserSubs()):
self.insertRow(index)
self.flushRow(index, Storage.UserSubs()[key])
else:
for index, key in enumerate(Storage.UserSubs()):
self.flushRow(index, Storage.UserSubs()[key])
def appendNewItem(self, **kwargs):
unique, remark, webURL, autoupdate, proxy = (
kwargs.pop('unique', ''),
kwargs.pop('remark', ''),
kwargs.pop('webURL', ''),
# Below values are not filled when adding item via Gui
kwargs.pop('autoupdate', ''),
kwargs.pop('proxy', ''),
)
# The internal subs object
subsob = {
unique: {
'remark': remark,
'webURL': webURL,
'autoupdate': autoupdate,
'proxy': proxy,
}
}
# 'unique' should be unique in subscription storage,
# but protect it anyway.
if unique not in Storage.UserSubs():
# Add timer
self.timers.append(QtCore.QTimer())
Storage.UserSubs().update(subsob)
row = self.rowCount()
self.insertRow(row)
self.flushRow(row, subsob[unique])
@staticmethod
def updateSubsByUnique(
unique: str, httpProxy: Union[str, Callable, None], **kwargs
):
showMessageBox = kwargs.pop('showMessageBox', False)
if callable(httpProxy):
try:
realHttpProxy = httpProxy()
except Exception as ex:
# Any non-exit exceptions
logger.error(
f'error while configuring http proxy: {ex}. '
f'Update subscription uses no proxy'
)
realHttpProxy = None
else:
realHttpProxy = httpProxy
APP().mainWindow.updateSubsByUnique(
unique, realHttpProxy, showMessageBox=showMessageBox, **kwargs
)
def retranslate(self):
self.setHorizontalHeaderLabels(list(_(str(header)) for header in self.Headers))