mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-05 13:28:03 +03:00
Completed the repository-wide Qt/PySide6 lifetime audit
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -486,6 +486,9 @@ class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
|
||||
super().__init__(
|
||||
_('Scan QR Code On The Screen'),
|
||||
icon=bootstrapIcon('qr-code-scan.svg'),
|
||||
# Tray and page actions each own a distinct native screen-capture
|
||||
# handle, so every instance must close its own handle at shutdown.
|
||||
uniqueCleanup=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ import sys
|
||||
import logging
|
||||
import platform
|
||||
import threading
|
||||
import functools
|
||||
import traceback
|
||||
import darkdetect
|
||||
|
||||
@@ -199,19 +198,33 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
self._userTUNSettings = None
|
||||
|
||||
# ThreadPool
|
||||
self.threadPool = QtCore.QThreadPool()
|
||||
self.threadPool = QtCore.QThreadPool(self)
|
||||
self.threadPool.setMaxThreadCount(max(OS_CPU_COUNT // 2, 1))
|
||||
|
||||
@callRateLimited(maxCallPerSecond=2)
|
||||
@QtCore.Slot()
|
||||
def handleNewConnection(self):
|
||||
"""Handle new connection."""
|
||||
socket = self.server.nextPendingConnection()
|
||||
socket.readyRead.connect(functools.partial(self.handleNewData, socket))
|
||||
while self.server.hasPendingConnections():
|
||||
socket = self.server.nextPendingConnection()
|
||||
|
||||
@QtCore.Slot(QLocalSocket)
|
||||
def handleNewData(self, socket: QLocalSocket):
|
||||
if socket is None:
|
||||
continue
|
||||
|
||||
# QLocalServer owns pending sockets until they are explicitly
|
||||
# released. Use sender() instead of a partial that retains each
|
||||
# socket and dispose it after the one-command protocol completes.
|
||||
socket.readyRead.connect(self.handleNewData)
|
||||
socket.disconnected.connect(socket.deleteLater)
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleNewData(self):
|
||||
"""Handle new data."""
|
||||
socket = self.sender()
|
||||
|
||||
if not isinstance(socket, QLocalSocket):
|
||||
return
|
||||
|
||||
data = socket.readAll().data()
|
||||
|
||||
if isinstance(data, bytes):
|
||||
@@ -235,6 +248,14 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
# TODO: Not implemented
|
||||
pass
|
||||
|
||||
# The singleton IPC channel carries exactly one command. Closing it
|
||||
# here both wakes RunAs clients and prevents completed QLocalSocket
|
||||
# children from accumulating under the application-wide server.
|
||||
socket.disconnectFromServer()
|
||||
|
||||
if socket.state() == QLocalSocket.LocalSocketState.UnconnectedState:
|
||||
socket.deleteLater()
|
||||
|
||||
def configureLogging(self):
|
||||
"""Configure logging."""
|
||||
self.logManager = LogManager(parent=self)
|
||||
@@ -528,7 +549,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
self.handleSystemThemeChanged(currentTheme)
|
||||
|
||||
self.currentTheme = self.systemTheme()
|
||||
self.themeDetectTimer = QtCore.QTimer()
|
||||
self.themeDetectTimer = QtCore.QTimer(self)
|
||||
self.themeDetectTimer.timeout.connect(handleTimeout)
|
||||
self.themeDetectTimer.start(1000)
|
||||
else:
|
||||
@@ -545,7 +566,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
'darkdetect listener is not implemented on this platform'
|
||||
)
|
||||
|
||||
self.themeDetector = ApplicationThemeDetector()
|
||||
self.themeDetector = ApplicationThemeDetector(self)
|
||||
self.themeDetector.themeChanged.connect(self.handleSystemThemeChanged)
|
||||
|
||||
self.themeListenerThread = threading.Thread(
|
||||
|
||||
@@ -696,17 +696,26 @@ class RoutingRulesQListWidget(AppQListWidget):
|
||||
self.setSelectionMode(AppQListWidget.SelectionMode.ExtendedSelection)
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
|
||||
self.itemDoubleClicked.connect(lambda _item: self.editRequested.emit())
|
||||
self.itemDoubleClicked.connect(self._requestEdit)
|
||||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||||
|
||||
self.contextDeleteAction = AppQAction(
|
||||
_('Delete'),
|
||||
callback=lambda: self.deleteRequested.emit(),
|
||||
)
|
||||
self.contextDeleteAction = AppQAction(_('Delete'))
|
||||
self.contextDeleteAction.triggered.connect(self._requestDelete)
|
||||
|
||||
self.contextMenu = AppQMenu(self.contextDeleteAction, parent=self)
|
||||
|
||||
self.flushAll()
|
||||
|
||||
@QtCore.Slot(QListWidgetItem)
|
||||
def _requestEdit(self, _item):
|
||||
"""Forward a row double-click without retaining a Python closure."""
|
||||
self.editRequested.emit()
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def _requestDelete(self, _checked=False):
|
||||
"""Request deletion without retaining a callback closure."""
|
||||
self.deleteRequested.emit()
|
||||
|
||||
def rules(self):
|
||||
"""Return the rules value used by the routing rules Qt list widget."""
|
||||
rules = self.routing.setdefault('rules', list())
|
||||
@@ -824,7 +833,10 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
def addRule(self):
|
||||
"""Add rule."""
|
||||
rule = {'type': 'field', 'outboundTag': 'proxy', 'ruleTag': 'New Rule'}
|
||||
dialog = RoutingRuleEditDialog(rule, parent=None)
|
||||
# This editor is subordinate to the transient rules dialog. Parenting
|
||||
# it prevents an asynchronous child from outliving its owner and later
|
||||
# invoking a callback on a deleted list widget.
|
||||
dialog = RoutingRuleEditDialog(rule, parent=self)
|
||||
|
||||
def handleResultCode(code):
|
||||
"""Handle result code."""
|
||||
@@ -842,7 +854,7 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
return
|
||||
|
||||
index = indexes[0]
|
||||
dialog = RoutingRuleEditDialog(self.listWidget.ruleAt(index), parent=None)
|
||||
dialog = RoutingRuleEditDialog(self.listWidget.ruleAt(index), parent=self)
|
||||
|
||||
def handleResultCode(_index, code):
|
||||
"""Handle result code."""
|
||||
@@ -1149,9 +1161,14 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
|
||||
routing = self.sourceModel.routingByRow(indexes[0])
|
||||
|
||||
dialog = RoutingRulesDialog(routing, parent=self)
|
||||
dialog.finished.connect(lambda _code: self.flushAll())
|
||||
dialog.finished.connect(self._rulesDialogFinished)
|
||||
dialog.open()
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def _rulesDialogFinished(self, _code):
|
||||
"""Refresh routing presentation after the rules dialog finishes."""
|
||||
self.flushAll()
|
||||
|
||||
|
||||
class XrayRoutingWindow(AppQMainWindow):
|
||||
"""Present the user routing window."""
|
||||
|
||||
+12
-8
@@ -174,17 +174,21 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
|
||||
if shortcut is not None:
|
||||
self.setShortcut(shortcut)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def triggerSignal(paramChecked):
|
||||
"""Handle trigger signal for the app q action."""
|
||||
logger.info(f'action is \'{self.textEnglish}\'. Checked is {paramChecked}')
|
||||
# Connect to a real QObject method rather than a nested closure that
|
||||
# captures this action. PySide stores Python callables connected to a
|
||||
# signal outside the normal Python object graph; a closure here leaves
|
||||
# an otherwise unowned QAction alive indefinitely.
|
||||
self.triggered.connect(self._handleTriggered)
|
||||
|
||||
if callable(self.callback):
|
||||
self.callback()
|
||||
@QtCore.Slot(bool)
|
||||
def _handleTriggered(self, paramChecked):
|
||||
"""Dispatch activation without creating a signal/self reference cycle."""
|
||||
logger.info(f'action is \'{self.textEnglish}\'. Checked is {paramChecked}')
|
||||
|
||||
self.triggeredCallback(paramChecked)
|
||||
if callable(self.callback):
|
||||
self.callback()
|
||||
|
||||
self.triggered.connect(triggerSignal)
|
||||
self.triggeredCallback(paramChecked)
|
||||
|
||||
def addAction(self, action):
|
||||
"""Add action."""
|
||||
|
||||
+31
-8
@@ -187,7 +187,11 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
|
||||
"""Initialize the AppQDialog."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._lifetimeKey = id(self)
|
||||
# Use a per-instance token rather than id(self). A finished transient
|
||||
# can release its Python wrapper before Qt processes deleteLater(); an
|
||||
# ID reused by a newer dialog must not let the older destroyed signal
|
||||
# evict that newer dialog from the asynchronous lifetime registry.
|
||||
self._lifetimeKey = object()
|
||||
self._firstShowPending = True
|
||||
|
||||
# Do not store a nested closure that captures this dialog on the dialog
|
||||
@@ -460,7 +464,7 @@ class AppQMainWindow(
|
||||
"""Initialize the AppQMainWindow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._lifetimeKey = id(self)
|
||||
self._lifetimeKey = object()
|
||||
self._firstShowPending = True
|
||||
|
||||
release = functools.partial(
|
||||
@@ -614,6 +618,19 @@ class _AppMessageBoxMask(QFrame):
|
||||
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def dispose(self):
|
||||
"""Detach from the long-lived owner before scheduling destruction."""
|
||||
owner = self.parentWidget()
|
||||
|
||||
if owner is not None:
|
||||
try:
|
||||
owner.removeEventFilter(self)
|
||||
except RuntimeError:
|
||||
# The owner can be destroyed as part of the same close path.
|
||||
pass
|
||||
|
||||
self.deleteLater()
|
||||
|
||||
|
||||
class AppQMessageBox(AppQTransientDialog):
|
||||
"""Present a responsive Fluent dialog with QMessageBox-compatible APIs."""
|
||||
@@ -682,7 +699,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._lifetimeKey = id(self)
|
||||
self._lifetimeKey = object()
|
||||
self._windowMask = None
|
||||
self._icon = self.Icon.NoIcon
|
||||
self._text = ''
|
||||
@@ -814,7 +831,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
button = QPushButton(str(text), self.buttonFrame)
|
||||
button.setMinimumHeight(34)
|
||||
button.setAttribute(QtCore.Qt.WidgetAttribute.WA_LayoutUsesWidgetRect)
|
||||
button.clicked.connect(functools.partial(self._buttonWasClicked, button))
|
||||
button.clicked.connect(self._handleButtonClicked)
|
||||
|
||||
self._buttonRoles[button] = role
|
||||
|
||||
@@ -1058,9 +1075,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
customButton = button
|
||||
customButton.setParent(self.buttonFrame)
|
||||
customButton.setAttribute(QtCore.Qt.WidgetAttribute.WA_LayoutUsesWidgetRect)
|
||||
customButton.clicked.connect(
|
||||
functools.partial(self._buttonWasClicked, customButton)
|
||||
)
|
||||
customButton.clicked.connect(self._handleButtonClicked)
|
||||
|
||||
self._buttonRoles[customButton] = role
|
||||
self._rebuildButtonLayout()
|
||||
@@ -1072,6 +1087,14 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
|
||||
return self._createButton(button, role)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def _handleButtonClicked(self, _checked=False):
|
||||
"""Dispatch a child button through sender-based QObject ownership."""
|
||||
button = self.sender()
|
||||
|
||||
if isinstance(button, QAbstractButton):
|
||||
self._buttonWasClicked(button)
|
||||
|
||||
def removeButton(self, button):
|
||||
"""Remove one custom or standard button."""
|
||||
self._buttonRoles.pop(button, None)
|
||||
@@ -1399,7 +1422,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self._windowMask = None
|
||||
|
||||
if mask is not None:
|
||||
mask.deleteLater()
|
||||
mask.dispose()
|
||||
|
||||
def moveToCenter(self):
|
||||
"""Move to center."""
|
||||
|
||||
+18
-15
@@ -274,7 +274,7 @@ class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
|
||||
|
||||
if delta > 0:
|
||||
self.zoomIn()
|
||||
if delta < 0:
|
||||
elif delta < 0:
|
||||
self.zoomOut()
|
||||
else:
|
||||
super().wheelEvent(event)
|
||||
@@ -306,23 +306,26 @@ class DraculaTextEditor(Mixins.ThemeAware, AppQPlainTextEdit):
|
||||
)
|
||||
)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def handleModificationChanged(changed):
|
||||
"""Handle modification changed."""
|
||||
if changed:
|
||||
self.document().setModified(False)
|
||||
# QObject-bound slots are important here. Nested closures connected to
|
||||
# the editor's own signals retain the complete editor widget tree in
|
||||
# PySide even after its window has been destroyed.
|
||||
self.modificationChanged.connect(self._handleModificationChanged)
|
||||
self.cursorPositionChanged.connect(self._handleCursorPositionChanged)
|
||||
|
||||
if callable(self._modificationChangedCb):
|
||||
self._modificationChangedCb()
|
||||
@QtCore.Slot(bool)
|
||||
def _handleModificationChanged(self, changed):
|
||||
"""Notify the registered modification callback for a real edit."""
|
||||
if changed:
|
||||
self.document().setModified(False)
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleCursorPositionChanged():
|
||||
"""Handle cursor position changed."""
|
||||
if callable(self._cursorPositionChangedCb):
|
||||
self._cursorPositionChangedCb(self.textCursor())
|
||||
if callable(self._modificationChangedCb):
|
||||
self._modificationChangedCb()
|
||||
|
||||
self.modificationChanged.connect(handleModificationChanged)
|
||||
self.cursorPositionChanged.connect(handleCursorPositionChanged)
|
||||
@QtCore.Slot()
|
||||
def _handleCursorPositionChanged(self):
|
||||
"""Notify the registered cursor-position callback."""
|
||||
if callable(self._cursorPositionChangedCb):
|
||||
self._cursorPositionChangedCb(self.textCursor())
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Refresh editor chrome after an application theme change."""
|
||||
|
||||
@@ -1891,29 +1891,24 @@ class ServerTableView(
|
||||
|
||||
logger.error(f'error while converting factory to input: {ex}')
|
||||
|
||||
guiEditor.accepted.connect(
|
||||
functools.partial(
|
||||
self.handleGuiEditorAccepted,
|
||||
guiEditor,
|
||||
index,
|
||||
factory,
|
||||
)
|
||||
)
|
||||
guiEditor.rejected.connect(
|
||||
functools.partial(
|
||||
self.handleGuiEditorRejected,
|
||||
guiEditor,
|
||||
)
|
||||
)
|
||||
# Keep operation metadata on the transient editor and use QObject-bound
|
||||
# slots. Partials stored by PySide otherwise retain both the complete
|
||||
# editor tree and this application-lifetime table until disconnection.
|
||||
guiEditor._modContext = (index, factory)
|
||||
guiEditor.accepted.connect(self.handleGuiEditorAccepted)
|
||||
guiEditor.rejected.connect(self.handleGuiEditorRejected)
|
||||
guiEditor.open()
|
||||
|
||||
def handleGuiEditorAccepted(
|
||||
self,
|
||||
editor: GuiEditorWidgetQDialog,
|
||||
index: int,
|
||||
factory: ServerProfile,
|
||||
):
|
||||
@QtCore.Slot()
|
||||
def handleGuiEditorAccepted(self):
|
||||
"""Handle GUI editor accepted."""
|
||||
editor = self.sender()
|
||||
|
||||
if not isinstance(editor, GuiEditorWidgetQDialog):
|
||||
return
|
||||
|
||||
index, factory = editor._modContext
|
||||
|
||||
logger.debug(f'guiEditor accepted with index {index}')
|
||||
|
||||
modified = editor.inputToFactory(factory)
|
||||
@@ -1927,12 +1922,22 @@ class ServerTableView(
|
||||
editor.accepted.disconnect()
|
||||
editor.rejected.disconnect()
|
||||
|
||||
@staticmethod
|
||||
def handleGuiEditorRejected(editor: GuiEditorWidgetQDialog):
|
||||
del editor._modContext
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleGuiEditorRejected(self):
|
||||
"""Handle GUI editor rejected."""
|
||||
editor = self.sender()
|
||||
|
||||
if not isinstance(editor, GuiEditorWidgetQDialog):
|
||||
return
|
||||
|
||||
editor.accepted.disconnect()
|
||||
editor.rejected.disconnect()
|
||||
|
||||
if hasattr(editor, '_modContext'):
|
||||
del editor._modContext
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
"""Handle custom context menu requested."""
|
||||
@@ -2052,27 +2057,21 @@ class ServerTableView(
|
||||
|
||||
logger.error(f'error while converting factory to input: {ex}')
|
||||
|
||||
guiEditor.accepted.connect(
|
||||
functools.partial(
|
||||
self.handleAddServerViaGuiAccepted,
|
||||
guiEditor,
|
||||
factory,
|
||||
)
|
||||
)
|
||||
guiEditor.rejected.connect(
|
||||
functools.partial(
|
||||
self.handleAddServerViaGuiRejected,
|
||||
guiEditor,
|
||||
)
|
||||
)
|
||||
guiEditor._addContext = factory
|
||||
guiEditor.accepted.connect(self.handleAddServerViaGuiAccepted)
|
||||
guiEditor.rejected.connect(self.handleAddServerViaGuiRejected)
|
||||
guiEditor.open()
|
||||
|
||||
def handleAddServerViaGuiAccepted(
|
||||
self,
|
||||
editor: GuiEditorWidgetQDialog,
|
||||
factory: ServerProfile,
|
||||
):
|
||||
@QtCore.Slot()
|
||||
def handleAddServerViaGuiAccepted(self):
|
||||
"""Handle add server via GUI accepted."""
|
||||
editor = self.sender()
|
||||
|
||||
if not isinstance(editor, GuiEditorWidgetQDialog):
|
||||
return
|
||||
|
||||
factory = editor._addContext
|
||||
|
||||
editor.inputToFactory(factory)
|
||||
|
||||
self.appendNewItemByFactory(factory)
|
||||
@@ -2080,11 +2079,22 @@ class ServerTableView(
|
||||
editor.accepted.disconnect()
|
||||
editor.rejected.disconnect()
|
||||
|
||||
def handleAddServerViaGuiRejected(self, editor: GuiEditorWidgetQDialog):
|
||||
del editor._addContext
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleAddServerViaGuiRejected(self):
|
||||
"""Handle add server via GUI rejected."""
|
||||
editor = self.sender()
|
||||
|
||||
if not isinstance(editor, GuiEditorWidgetQDialog):
|
||||
return
|
||||
|
||||
editor.accepted.disconnect()
|
||||
editor.rejected.disconnect()
|
||||
|
||||
if hasattr(editor, '_addContext'):
|
||||
del editor._addContext
|
||||
|
||||
def flushRow(self, row: int, item: ServerProfile):
|
||||
"""Refresh row."""
|
||||
itemIndex = item.index
|
||||
|
||||
@@ -729,10 +729,8 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
"""Handle the home-page request to restart with elevated privileges."""
|
||||
if not SystemRuntime.isScriptMode():
|
||||
if not SystemRuntime.isAdmin():
|
||||
process = QtCore.QProcess()
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
process.startDetached(
|
||||
QtCore.QProcess.startDetached(
|
||||
'powershell',
|
||||
arguments=[
|
||||
'-Command',
|
||||
@@ -741,7 +739,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
],
|
||||
)
|
||||
elif PLATFORM == 'Darwin':
|
||||
process.startDetached(
|
||||
QtCore.QProcess.startDetached(
|
||||
'osascript',
|
||||
arguments=[
|
||||
'-e',
|
||||
|
||||
@@ -27,7 +27,6 @@ from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['NetworkTestDialog']
|
||||
|
||||
@@ -76,13 +75,9 @@ class NetworkTestDialog(AppQDialog):
|
||||
self.dialogBtns.rejected.connect(self.reject)
|
||||
|
||||
self.speedTestURLResetBtn = AppQPushButton(_('Reset'))
|
||||
self.speedTestURLResetBtn.clicked.connect(
|
||||
lambda: self.speedTestURLEdit.setText(NETWORK_SPEED_TEST_URL)
|
||||
)
|
||||
self.speedTestURLResetBtn.clicked.connect(self._resetSpeedTestURL)
|
||||
self.connectivityResetBtn = AppQPushButton(_('Reset'))
|
||||
self.connectivityResetBtn.clicked.connect(
|
||||
lambda: self.connectivityEdit.setText(NETWORK_CONNECTIVITY_TEST_URL)
|
||||
)
|
||||
self.connectivityResetBtn.clicked.connect(self._resetConnectivityURL)
|
||||
|
||||
self.speedTestURLHboxLayout = QHBoxLayout()
|
||||
self.speedTestURLHboxLayout.addWidget(self.speedTestURLEdit)
|
||||
@@ -102,7 +97,17 @@ class NetworkTestDialog(AppQDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.finished.connect(functools.partial(self.handleResultCode))
|
||||
self.finished.connect(self.handleResultCode)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _resetSpeedTestURL(self):
|
||||
"""Restore the default speed-test URL."""
|
||||
self.speedTestURLEdit.setText(NETWORK_SPEED_TEST_URL)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _resetConnectivityURL(self):
|
||||
"""Restore the default connectivity-test URL."""
|
||||
self.connectivityEdit.setText(NETWORK_CONNECTIVITY_TEST_URL)
|
||||
|
||||
def setWidthAndHeight(self):
|
||||
"""Apply the default size for the GUI customize network test dialog."""
|
||||
|
||||
@@ -27,7 +27,6 @@ from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['ProxyBypassDialog']
|
||||
|
||||
@@ -65,7 +64,7 @@ class ProxyBypassDialog(AppQDialog):
|
||||
self.dialogBtns.rejected.connect(self.reject)
|
||||
|
||||
self.resetBtn = AppQPushButton(_('Reset'))
|
||||
self.resetBtn.clicked.connect(functools.partial(self.handleResetButtonClicked))
|
||||
self.resetBtn.clicked.connect(self.handleResetButtonClicked)
|
||||
|
||||
self.hboxLayout = QHBoxLayout()
|
||||
self.hboxLayout.addWidget(self.resetBtn)
|
||||
@@ -79,7 +78,7 @@ class ProxyBypassDialog(AppQDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.finished.connect(functools.partial(self.handleResultCode))
|
||||
self.finished.connect(self.handleResultCode)
|
||||
|
||||
def setWidthAndHeight(self):
|
||||
"""Apply the default size for the GUI customize proxy bypass dialog."""
|
||||
|
||||
@@ -229,7 +229,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow):
|
||||
deleteUniqueCallback=self._deleteProfilesForSubscription,
|
||||
parent=self,
|
||||
)
|
||||
self.table.doubleClicked.connect(lambda _index: self.editSelected())
|
||||
self.table.doubleClicked.connect(self._editDoubleClicked)
|
||||
self.table.groupsChanged.connect(
|
||||
self.serverTable.subsManager.subscriptionsChanged.emit
|
||||
)
|
||||
@@ -282,6 +282,11 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow):
|
||||
self.setIconsByTheme(APP().theme())
|
||||
self.retranslate()
|
||||
|
||||
@QtCore.Slot(QtCore.QModelIndex)
|
||||
def _editDoubleClicked(self, _index):
|
||||
"""Edit the selected subscription after a table double-click."""
|
||||
self.editSelected()
|
||||
|
||||
def _deleteProfilesForSubscription(self, unique: str):
|
||||
"""Remove profiles belonging to a deleted subscription group."""
|
||||
indexes = [
|
||||
|
||||
@@ -373,43 +373,43 @@ class TextEditorWindow(AppQMainWindow):
|
||||
|
||||
def setIndent(self):
|
||||
"""Set indent."""
|
||||
|
||||
def handleResultCode(_indentSpinBox, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
|
||||
plain = self.jsonEditor.toPlainText()
|
||||
|
||||
try:
|
||||
jsonObject = JSONEncoder.decode(plain)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
mbox = MBoxJSONDecodeError(
|
||||
icon=AppQMessageBox.Icon.Critical, parent=self
|
||||
)
|
||||
mbox.error = str(ex)
|
||||
mbox.setWindowTitle(_('Error setting indent'))
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setText(mbox.customText())
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
text = JSONEncoder.encode(jsonObject, indent=_indentSpinBox.value())
|
||||
|
||||
self.setPlainText(text, False)
|
||||
else:
|
||||
# Do nothing
|
||||
pass
|
||||
|
||||
indentSpinBox = IndentDialog(parent=self)
|
||||
indentSpinBox.finished.connect(
|
||||
functools.partial(handleResultCode, indentSpinBox)
|
||||
)
|
||||
indentSpinBox.finished.connect(self._indentDialogFinished)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
indentSpinBox.open()
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def _indentDialogFinished(self, code):
|
||||
"""Apply the selected indentation without retaining the dialog."""
|
||||
indentSpinBox = self.sender()
|
||||
|
||||
if not isinstance(indentSpinBox, IndentDialog):
|
||||
return
|
||||
|
||||
if code != PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
|
||||
return
|
||||
|
||||
plain = self.jsonEditor.toPlainText()
|
||||
|
||||
try:
|
||||
jsonObject = JSONEncoder.decode(plain)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
mbox = MBoxJSONDecodeError(icon=AppQMessageBox.Icon.Critical, parent=self)
|
||||
mbox.error = str(ex)
|
||||
mbox.setWindowTitle(_('Error setting indent'))
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setText(mbox.customText())
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
text = JSONEncoder.encode(jsonObject, indent=indentSpinBox.value())
|
||||
|
||||
self.setPlainText(text, False)
|
||||
|
||||
def showTabAndSpaces(self):
|
||||
"""Show tab and spaces."""
|
||||
textOption = QTextOption()
|
||||
|
||||
@@ -30,7 +30,6 @@ from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['TunSettingsDialog']
|
||||
|
||||
@@ -307,21 +306,22 @@ class TunSettingsDialog(GuiEditorWidgetQDialog):
|
||||
self.setFixedSize(int(620 * GOLDEN_RATIO), int(620))
|
||||
|
||||
# Shallow copy
|
||||
config = Storage.UserTUNSettings()
|
||||
self._config = Storage.UserTUNSettings()
|
||||
|
||||
try:
|
||||
self.factoryToInput(config)
|
||||
self.factoryToInput(self._config)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'error while converting factory to input: {ex}')
|
||||
|
||||
self.accepted.connect(functools.partial(self.handleAccepted, config))
|
||||
self.rejected.connect(functools.partial(self.handleRejected))
|
||||
self.accepted.connect(self.handleAccepted)
|
||||
self.rejected.connect(self.handleRejected)
|
||||
|
||||
def handleAccepted(self, config: dict):
|
||||
@QtCore.Slot()
|
||||
def handleAccepted(self):
|
||||
"""Handle accepted."""
|
||||
modified = self.inputToFactory(config)
|
||||
modified = self.inputToFactory(self._config)
|
||||
|
||||
if modified and SystemRuntime.isTUNMode():
|
||||
showMBoxNewChangesNextTime()
|
||||
@@ -329,6 +329,7 @@ class TunSettingsDialog(GuiEditorWidgetQDialog):
|
||||
self.accepted.disconnect()
|
||||
self.rejected.disconnect()
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleRejected(self):
|
||||
"""Handle rejected."""
|
||||
self.accepted.disconnect()
|
||||
|
||||
Reference in New Issue
Block a user