mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-11 16:58:11 +03:00
Fix shared Qt object lifetimes
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -130,11 +130,9 @@ def importURIs(*uris, failureCallback: Union[Callable[[], None], None] = None):
|
||||
mbox.open()
|
||||
|
||||
|
||||
class ImportURIsProgressDialog(AppQDialog):
|
||||
class ImportURIsProgressDialog(AppQTransientDialog):
|
||||
"""Present progress and cancellation controls for import ur is."""
|
||||
|
||||
ActiveDialogs = list()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uris: Tuple[str, ...],
|
||||
@@ -190,8 +188,6 @@ class ImportURIsProgressDialog(AppQDialog):
|
||||
|
||||
def open(self):
|
||||
"""Open the import ur is progress dialog asynchronously."""
|
||||
ImportURIsProgressDialog.ActiveDialogs.append(self)
|
||||
|
||||
result = super().open()
|
||||
|
||||
self.spinner.start()
|
||||
@@ -271,11 +267,6 @@ class ImportURIsProgressDialog(AppQDialog):
|
||||
self.spinner.stop()
|
||||
self.accept()
|
||||
|
||||
try:
|
||||
ImportURIsProgressDialog.ActiveDialogs.remove(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if self.canceled:
|
||||
return
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ def routingObjectFromProfile(routingProfile: dict):
|
||||
}
|
||||
|
||||
|
||||
class RoutingPreviewDialog(AppQDialog):
|
||||
class RoutingPreviewDialog(AppQTransientDialog):
|
||||
"""Present the routing preview dialog."""
|
||||
|
||||
def __init__(self, routingProfile: dict, parent=None):
|
||||
@@ -168,7 +168,7 @@ class RoutingPreviewDialog(AppQDialog):
|
||||
self.setFixedSize(400, int(400 * GOLDEN_RATIO))
|
||||
|
||||
|
||||
class RoutingTextEditDialog(AppQDialog):
|
||||
class RoutingTextEditDialog(AppQTransientDialog):
|
||||
"""Present the routing text edit dialog."""
|
||||
|
||||
def __init__(self, text='', parent=None):
|
||||
@@ -358,7 +358,7 @@ class RoutingProfilesModel(QtCore.QAbstractTableModel):
|
||||
)
|
||||
|
||||
|
||||
class RoutingRuleEditDialog(AppQDialog):
|
||||
class RoutingRuleEditDialog(AppQTransientDialog):
|
||||
"""Present the routing rule edit dialog."""
|
||||
|
||||
MatchInputHeight = 72
|
||||
@@ -587,7 +587,7 @@ class RoutingRuleEditDialog(AppQDialog):
|
||||
return rule
|
||||
|
||||
|
||||
class RoutingRemarkEditDialog(AppQDialog):
|
||||
class RoutingRemarkEditDialog(AppQTransientDialog):
|
||||
"""Present the routing remark edit dialog."""
|
||||
|
||||
def __init__(self, remark: str, parent=None):
|
||||
@@ -623,7 +623,7 @@ class RoutingRemarkEditDialog(AppQDialog):
|
||||
return self.remarkEdit.text().strip()
|
||||
|
||||
|
||||
class RoutingProfileEditDialog(AppQDialog):
|
||||
class RoutingProfileEditDialog(AppQTransientDialog):
|
||||
"""Present the routing profile edit dialog."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -770,7 +770,7 @@ class RoutingRulesQListWidget(AppQListWidget):
|
||||
self.contextMenu.exec(self.viewport().mapToGlobal(point))
|
||||
|
||||
|
||||
class RoutingRulesDialog(AppQDialog):
|
||||
class RoutingRulesDialog(AppQTransientDialog):
|
||||
"""Present the routing rules dialog."""
|
||||
|
||||
def __init__(self, routing: dict, parent=None):
|
||||
|
||||
@@ -161,6 +161,24 @@ class MsgQueue(multiprocessing.queues.Queue):
|
||||
"""Stop timer."""
|
||||
self.timer.stop()
|
||||
|
||||
def dispose(self):
|
||||
"""Release Qt and multiprocessing resources after final process use."""
|
||||
self.stopTimer()
|
||||
|
||||
try:
|
||||
self.timer.timeout.disconnect(self.processMsg)
|
||||
except (RuntimeError, TypeError):
|
||||
pass
|
||||
|
||||
self.timer.deleteLater()
|
||||
self.callback = None
|
||||
self.backgroundOptimizer = None
|
||||
|
||||
try:
|
||||
self.close()
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
@property
|
||||
def optimizer(self):
|
||||
"""Return the optimizer value."""
|
||||
@@ -293,6 +311,19 @@ class CoreProcessMonitor(CoreProcess, ABC):
|
||||
|
||||
self.process = None
|
||||
|
||||
def dispose(self):
|
||||
"""Release the monitor timer after this kernel leaves its owner pool."""
|
||||
self.daemon.stop()
|
||||
|
||||
try:
|
||||
self.daemon.timeout.disconnect(self.queryIsAlive)
|
||||
except (RuntimeError, TypeError):
|
||||
pass
|
||||
|
||||
self.daemon.deleteLater()
|
||||
self.closeProcess()
|
||||
self._exitCallback = None
|
||||
|
||||
|
||||
class CoreProcessWorker(CoreProcessMonitor, ABC):
|
||||
"""Run and monitor a proxy core in a child process."""
|
||||
@@ -325,6 +356,12 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
|
||||
# Reset internal process
|
||||
self.closeProcess()
|
||||
|
||||
def dispose(self):
|
||||
"""Release final queue, callback, process, and Qt timer ownership."""
|
||||
self.msgQueue.dispose()
|
||||
|
||||
super().dispose()
|
||||
|
||||
def start(self, **kwargs) -> bool:
|
||||
"""Start the core process worker."""
|
||||
return self.startWithSpec(CoreLaunchSpec.fromProcessKwargs(**kwargs))
|
||||
|
||||
@@ -372,7 +372,7 @@ class GuiEditorWidgetQGroupBox(GuiEditorWidget, AppQGroupBox):
|
||||
return layout
|
||||
|
||||
|
||||
class GuiEditorWidgetQDialog(EditorBinding, AppQDialog):
|
||||
class GuiEditorWidgetQDialog(EditorBinding, AppQTransientDialog):
|
||||
"""Present the GUI editor widget Qt dialog."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
+53
-46
@@ -40,6 +40,7 @@ __all__ = [
|
||||
'AppQComboBox',
|
||||
'AppQComboBoxSeparatorDelegate',
|
||||
'AppQDialog',
|
||||
'AppQTransientDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
'AppQHeaderView',
|
||||
@@ -186,21 +187,19 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
|
||||
"""Initialize the AppQDialog."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@callOnceOnly
|
||||
def connect(key):
|
||||
"""Connect the lifetime release signals once."""
|
||||
release = functools.partial(AppQDialog._releaseOpenDialog, key)
|
||||
self._lifetimeKey = id(self)
|
||||
self._firstShowPending = True
|
||||
|
||||
self.finished.connect(release)
|
||||
self.destroyed.connect(release)
|
||||
# Do not store a nested closure that captures this dialog on the dialog
|
||||
# itself. Such a self-cycle delays wrapper collection and is especially
|
||||
# costly for widget trees in compiled builds.
|
||||
release = functools.partial(
|
||||
AppQDialog._releaseOpenDialog,
|
||||
self._lifetimeKey,
|
||||
)
|
||||
|
||||
@callOnceOnly
|
||||
def firstShow():
|
||||
"""Apply the first-show sizing once."""
|
||||
self.setWidthAndHeight()
|
||||
|
||||
self._connectOnce = connect
|
||||
self._firstShow = firstShow
|
||||
self.finished.connect(release)
|
||||
self.destroyed.connect(release)
|
||||
|
||||
if PLATFORM != 'Darwin':
|
||||
self.setWidthAndHeight()
|
||||
@@ -219,11 +218,9 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
|
||||
|
||||
def open(self):
|
||||
"""Open and retain the dialog until it finishes or is destroyed."""
|
||||
key = id(self)
|
||||
key = self._lifetimeKey
|
||||
AppQDialog._openDialogs[key] = self
|
||||
|
||||
self._connectOnce(key)
|
||||
|
||||
try:
|
||||
self.show()
|
||||
|
||||
@@ -239,8 +236,9 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
|
||||
"""Show and position the app Qt dialog."""
|
||||
super().show()
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
self._firstShow()
|
||||
if PLATFORM == 'Darwin' and self._firstShowPending:
|
||||
self._firstShowPending = False
|
||||
self.setWidthAndHeight()
|
||||
|
||||
moveToCenter(self)
|
||||
|
||||
@@ -257,6 +255,19 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
|
||||
self.setWindowIcon(AppHue.connectedWindowIcon())
|
||||
|
||||
|
||||
class AppQTransientDialog(AppQDialog):
|
||||
"""Present a one-shot dialog that destroys its Qt object when closed."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a dialog whose accepted/rejected lifetime is transient."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# QDialog normally hides on accept/reject. A long-lived Qt parent then
|
||||
# keeps each closed dialog in its child tree indefinitely. Transient
|
||||
# dialogs opt into native deletion so both Qt and Python ownership end.
|
||||
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
||||
|
||||
|
||||
class AppQDialogButtonBox(Mixins.QTranslatable, QDialogButtonBox):
|
||||
"""Represent app Qt dialog button box."""
|
||||
|
||||
@@ -449,19 +460,14 @@ class AppQMainWindow(
|
||||
"""Initialize the AppQMainWindow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@callOnceOnly
|
||||
def connect(key):
|
||||
"""Connect the lifetime release signal once."""
|
||||
release = functools.partial(AppQMainWindow._releaseOpenWindow, key)
|
||||
self.destroyed.connect(release)
|
||||
self._lifetimeKey = id(self)
|
||||
self._firstShowPending = True
|
||||
|
||||
@callOnceOnly
|
||||
def firstShow():
|
||||
"""Apply the first-show sizing once."""
|
||||
self.setWidthAndHeight()
|
||||
|
||||
self._connectOnce = connect
|
||||
self._firstShow = firstShow
|
||||
release = functools.partial(
|
||||
AppQMainWindow._releaseOpenWindow,
|
||||
self._lifetimeKey,
|
||||
)
|
||||
self.destroyed.connect(release)
|
||||
|
||||
self.setWindowIcon(AppHue.currentWindowIcon())
|
||||
|
||||
@@ -477,11 +483,9 @@ class AppQMainWindow(
|
||||
|
||||
def show(self):
|
||||
"""Show, position, and retain the window until it closes."""
|
||||
key = id(self)
|
||||
key = self._lifetimeKey
|
||||
AppQMainWindow._openWindows[key] = self
|
||||
|
||||
self._connectOnce(key)
|
||||
|
||||
try:
|
||||
super().show()
|
||||
except Exception:
|
||||
@@ -491,8 +495,9 @@ class AppQMainWindow(
|
||||
|
||||
raise
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
self._firstShow()
|
||||
if PLATFORM == 'Darwin' and self._firstShowPending:
|
||||
self._firstShowPending = False
|
||||
self.setWidthAndHeight()
|
||||
|
||||
moveToCenter(self)
|
||||
|
||||
@@ -508,7 +513,7 @@ class AppQMainWindow(
|
||||
result = super().event(event)
|
||||
|
||||
if closes and event.isAccepted():
|
||||
AppQMainWindow._releaseOpenWindow(id(self))
|
||||
AppQMainWindow._releaseOpenWindow(self._lifetimeKey)
|
||||
|
||||
return result
|
||||
|
||||
@@ -603,15 +608,19 @@ class AppQMessageBox(Mixins.QTranslatable, Mixins.ConnectionAware, QMessageBox):
|
||||
"""Initialize the AppQMessageBox."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@callOnceOnly
|
||||
def connect(key):
|
||||
"""Connect the lifetime release signals once."""
|
||||
release = functools.partial(AppQMessageBox._releaseOpenMessageBox, key)
|
||||
self._lifetimeKey = id(self)
|
||||
|
||||
self.finished.connect(release)
|
||||
self.destroyed.connect(release)
|
||||
release = functools.partial(
|
||||
AppQMessageBox._releaseOpenMessageBox,
|
||||
self._lifetimeKey,
|
||||
)
|
||||
|
||||
self._connectOnce = connect
|
||||
self.finished.connect(release)
|
||||
self.destroyed.connect(release)
|
||||
|
||||
# Message boxes in Furious are one-shot notifications or questions.
|
||||
# Parent ownership must not retain a hidden native box after completion.
|
||||
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
||||
|
||||
self.setWindowIcon(AppHue.currentWindowIcon())
|
||||
|
||||
@@ -634,11 +643,9 @@ class AppQMessageBox(Mixins.QTranslatable, Mixins.ConnectionAware, QMessageBox):
|
||||
|
||||
def open(self):
|
||||
"""Open and retain the message box until it finishes or is destroyed."""
|
||||
key = id(self)
|
||||
key = self._lifetimeKey
|
||||
AppQMessageBox._openMessageBoxes[key] = self
|
||||
|
||||
self._connectOnce(key)
|
||||
|
||||
try:
|
||||
self.show()
|
||||
self.moveToCenter()
|
||||
|
||||
+38
-16
@@ -28,7 +28,6 @@ from PySide6.QtNetwork import *
|
||||
from typing import Union
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['WebGETManager']
|
||||
|
||||
@@ -46,6 +45,7 @@ class WebGETManager(AppQNetworkAccessManager):
|
||||
|
||||
self.mustCallOnce = kwargs.pop('mustCallOnce', True)
|
||||
self.mustCalled = False
|
||||
self._replyContexts = {}
|
||||
|
||||
def successCallback(self, networkReply: QNetworkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
@@ -86,6 +86,29 @@ class WebGETManager(AppQNetworkAccessManager):
|
||||
"""Handle ready read by network reply."""
|
||||
self.hasDataCallback(networkReply, **kwargs)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _handleReadyRead(self):
|
||||
"""Dispatch ready-read data without a closure retaining the reply."""
|
||||
networkReply = self.sender()
|
||||
|
||||
if isinstance(networkReply, QNetworkReply):
|
||||
self.handleReadyReadByNetworkReply(
|
||||
networkReply,
|
||||
**self._replyContexts.get(id(networkReply), {}),
|
||||
)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _handleFinished(self):
|
||||
"""Dispatch and release one completed network reply."""
|
||||
networkReply = self.sender()
|
||||
|
||||
if not isinstance(networkReply, QNetworkReply):
|
||||
return
|
||||
|
||||
kwargs = self._replyContexts.pop(id(networkReply), {})
|
||||
|
||||
self.handleFinishedByNetworkReply(networkReply, **kwargs)
|
||||
|
||||
def handleFinishedByNetworkReply(self, networkReply: QNetworkReply, **kwargs):
|
||||
"""Handle finished by network reply."""
|
||||
try:
|
||||
@@ -113,7 +136,14 @@ class WebGETManager(AppQNetworkAccessManager):
|
||||
|
||||
self.successCallback(networkReply, **kwargs)
|
||||
finally:
|
||||
self.must(**kwargs)
|
||||
try:
|
||||
self.must(**kwargs)
|
||||
finally:
|
||||
# QNetworkAccessManager owns replies by default and does not
|
||||
# remove completed children automatically. All response data
|
||||
# has been consumed by this point. The shared slots above use
|
||||
# sender(), so no per-request closure retains this wrapper.
|
||||
networkReply.deleteLater()
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
"""Configure HTTP proxy."""
|
||||
@@ -133,19 +163,11 @@ class WebGETManager(AppQNetworkAccessManager):
|
||||
else:
|
||||
networkReply = self.get(QNetworkRequest(QtCore.QUrl(request)))
|
||||
|
||||
networkReply.readyRead.connect(
|
||||
functools.partial(
|
||||
self.handleReadyReadByNetworkReply,
|
||||
networkReply,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
networkReply.finished.connect(
|
||||
functools.partial(
|
||||
self.handleFinishedByNetworkReply,
|
||||
networkReply,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
key = id(networkReply)
|
||||
|
||||
self._replyContexts[key] = dict(kwargs)
|
||||
|
||||
networkReply.readyRead.connect(self._handleReadyRead)
|
||||
networkReply.finished.connect(self._handleFinished)
|
||||
|
||||
return networkReply
|
||||
|
||||
@@ -55,6 +55,7 @@ from .QtWidgets import (
|
||||
AppQComboBox,
|
||||
AppQComboBoxSeparatorDelegate,
|
||||
AppQDialog,
|
||||
AppQTransientDialog,
|
||||
AppQDialogButtonBox,
|
||||
AppQGroupBox,
|
||||
AppQHeaderView,
|
||||
@@ -106,6 +107,7 @@ __all__ = [
|
||||
'AppQComboBox',
|
||||
'AppQComboBoxSeparatorDelegate',
|
||||
'AppQDialog',
|
||||
'AppQTransientDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
'AppQHeaderView',
|
||||
|
||||
@@ -547,6 +547,16 @@ class ConnectionManager(Mixins.CleanupOnExit):
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'error stopping core process: {ex}')
|
||||
finally:
|
||||
dispose = getattr(process, 'dispose', None)
|
||||
|
||||
if callable(dispose):
|
||||
try:
|
||||
dispose()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'error disposing core process: {ex}')
|
||||
finally:
|
||||
self.processesPool.clear()
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ from PySide6 import QtCore
|
||||
from PySide6.QtNetwork import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['ConnectivityManager']
|
||||
|
||||
@@ -47,11 +46,22 @@ class ConnectivityManager(Mixins.ConnectionAware, WebGETManager):
|
||||
|
||||
self.jobStatus = False
|
||||
self.jobInterval = ConnectivityManager.MIN_JOB_INTERVAL
|
||||
self._testingEnabled = False
|
||||
self._activeReply = None
|
||||
|
||||
self.jobTimeoutTimer = QtCore.QTimer()
|
||||
self.jobArrangeTimer = QtCore.QTimer()
|
||||
self.jobTimeoutTimer = QtCore.QTimer(self)
|
||||
self.jobTimeoutTimer.setSingleShot(True)
|
||||
self.jobTimeoutTimer.timeout.connect(self._abortActiveReply)
|
||||
|
||||
self.jobArrangeTimer.timeout.connect(lambda: self.startSingleTest())
|
||||
self.jobArrangeTimer = QtCore.QTimer(self)
|
||||
self.jobArrangeTimer.setSingleShot(True)
|
||||
self.jobArrangeTimer.timeout.connect(self.startSingleTest)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _abortActiveReply(self):
|
||||
"""Abort the one currently active connectivity request."""
|
||||
if isinstance(self._activeReply, QNetworkReply):
|
||||
self._activeReply.abort()
|
||||
|
||||
def recalculateJobInterval(self, jobStatus: bool) -> int:
|
||||
"""Return the recalculate job interval value used by the network connectivity manager."""
|
||||
@@ -72,17 +82,30 @@ class ConnectivityManager(Mixins.ConnectionAware, WebGETManager):
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
if self._activeReply is networkReply:
|
||||
self._activeReply = None
|
||||
|
||||
self.jobTimeoutTimer.stop()
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=True))
|
||||
|
||||
if self._testingEnabled:
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=True))
|
||||
|
||||
def failureCallback(self, networkReply, **kwargs):
|
||||
"""Handle a failed network operation."""
|
||||
if self._activeReply is networkReply:
|
||||
self._activeReply = None
|
||||
|
||||
self.jobTimeoutTimer.stop()
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=False))
|
||||
|
||||
if self._testingEnabled:
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=False))
|
||||
|
||||
def startSingleTest(self):
|
||||
# Use custom network connectivity test URL if possible
|
||||
"""Start single test."""
|
||||
if not self._testingEnabled or self._activeReply is not None:
|
||||
return
|
||||
|
||||
settings = AppSettings.get('CustomNetworkConnectivityTestURL')
|
||||
|
||||
if isinstance(settings, str):
|
||||
@@ -90,20 +113,15 @@ class ConnectivityManager(Mixins.ConnectionAware, WebGETManager):
|
||||
else:
|
||||
url = NETWORK_CONNECTIVITY_TEST_URL
|
||||
|
||||
networkReply = self.webGET(url)
|
||||
|
||||
def abort(_networkReply):
|
||||
"""Cancel the active network connectivity manager operation."""
|
||||
if isinstance(_networkReply, QNetworkReply):
|
||||
_networkReply.abort()
|
||||
|
||||
self.jobTimeoutTimer.timeout.connect(functools.partial(abort, networkReply))
|
||||
self._activeReply = self.webGET(url)
|
||||
self.jobTimeoutTimer.start(ConnectivityManager.MIN_JOB_INTERVAL - 500)
|
||||
|
||||
def stopTest(self):
|
||||
"""Stop test."""
|
||||
self._testingEnabled = False
|
||||
self.jobArrangeTimer.stop()
|
||||
self.jobTimeoutTimer.stop()
|
||||
self._abortActiveReply()
|
||||
|
||||
def connectedCallback(self):
|
||||
"""Update the network connectivity manager for a connected state."""
|
||||
@@ -114,6 +132,7 @@ class ConnectivityManager(Mixins.ConnectionAware, WebGETManager):
|
||||
self.stopTest()
|
||||
else:
|
||||
self.jobInterval = ConnectivityManager.MIN_JOB_INTERVAL
|
||||
self._testingEnabled = True
|
||||
|
||||
self.jobArrangeTimer.start(self.jobInterval)
|
||||
|
||||
|
||||
@@ -38,22 +38,23 @@ class ConnectionProgressBar(Mixins.ConnectionAware, QProgressBar):
|
||||
|
||||
self.setRange(0, 100)
|
||||
|
||||
@QtCore.Slot()
|
||||
def update():
|
||||
# Update the progress bar value
|
||||
"""Update the auto update progress bar."""
|
||||
if self.value() < 90:
|
||||
self.setValue(self.value() + 1)
|
||||
|
||||
# Stop the timer when the progress bar reaches 100%
|
||||
if self.value() > 99:
|
||||
self.timer.stop()
|
||||
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(update)
|
||||
# The timer belongs to the progress bar. Parenting it and connecting
|
||||
# to a normal method avoids a parentless timer/closure cycle surviving
|
||||
# after the widget's Qt lifetime ends.
|
||||
self.timer = QtCore.QTimer(self)
|
||||
self.timer.timeout.connect(self._advance)
|
||||
|
||||
self._setConnectionState('disconnected')
|
||||
|
||||
@QtCore.Slot()
|
||||
def _advance(self):
|
||||
"""Advance the connection progress animation."""
|
||||
if self.value() < 90:
|
||||
self.setValue(self.value() + 1)
|
||||
|
||||
if self.value() > 99:
|
||||
self.timer.stop()
|
||||
|
||||
def _setConnectionState(self, state: str):
|
||||
"""Expose semantic state to the application-owned progress style."""
|
||||
if self.property('connectionState') == state:
|
||||
|
||||
@@ -524,11 +524,11 @@ class TestDownloadSpeedWorker(WebGETManager):
|
||||
self.networkReply = None
|
||||
self.elapsedTimer = QtCore.QElapsedTimer()
|
||||
|
||||
self.timeoutTimer = QtCore.QTimer()
|
||||
self.timeoutTimer = QtCore.QTimer(self)
|
||||
self.timeoutTimer.setSingleShot(True)
|
||||
self.timeoutTimer.timeout.connect(self.handleTimeout)
|
||||
|
||||
def mustCall(self):
|
||||
def mustCall(self, **kwargs):
|
||||
"""Perform the required completion hook."""
|
||||
self.timeoutTimer.stop()
|
||||
self.finished.emit(self)
|
||||
@@ -880,10 +880,16 @@ class DownloadSpeedTestScheduler(QtCore.QObject):
|
||||
return
|
||||
|
||||
self.releasePort(port)
|
||||
|
||||
# Completed workers are children of the long-lived scheduler. Merely
|
||||
# removing the Python dictionary entry would leave every worker (and
|
||||
# its network/timer children) in the scheduler's QObject tree.
|
||||
worker.deleteLater()
|
||||
|
||||
self.scheduleDrain()
|
||||
|
||||
|
||||
class DeleteServersProgressDialog(AppQDialog):
|
||||
class DeleteServersProgressDialog(AppQTransientDialog):
|
||||
"""Present progress and cancellation controls for delete servers."""
|
||||
|
||||
def __init__(self, table, indexes, showTrayMessage=True, parent=None):
|
||||
|
||||
@@ -648,6 +648,10 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView):
|
||||
self.timers.pop(deleteIndex)
|
||||
self.timerConnected.pop(deleteIndex)
|
||||
|
||||
# The table is the timer's Qt parent, so dropping the list
|
||||
# reference alone would retain it until application exit.
|
||||
qtimer.deleteLater()
|
||||
|
||||
self.sourceModel.endRemoveRows()
|
||||
|
||||
if callable(self.deleteUniqueCallback):
|
||||
|
||||
@@ -436,6 +436,9 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
importActionsFactory=self.serverImportActions,
|
||||
)
|
||||
pluginRegistry = getPluginRegistry()
|
||||
|
||||
# These two settings dialogs intentionally remain parent-owned and are
|
||||
# refreshed/reused by SettingsPage instead of being recreated.
|
||||
self.customizeProxyBypassDialog = ProxyBypassDialog(parent=self)
|
||||
self.customizeNetworkTestDialog = NetworkTestDialog(parent=self)
|
||||
|
||||
@@ -669,16 +672,10 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.userServersQTableWidget.hideTabAndSpaces()
|
||||
|
||||
def getGuiTUNSettings(self, **kwargs):
|
||||
"""Return GUI TUN settings."""
|
||||
"""Create a transient TUN settings editor for the settings page."""
|
||||
parent = kwargs.pop('parent', self)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def cachedGuiTUNSettings():
|
||||
"""Return the TUN settings editor owned by the home page."""
|
||||
parent = kwargs.pop('parent', self)
|
||||
|
||||
return TunSettingsDialog(parent=parent, **kwargs)
|
||||
|
||||
guiTUNSettings = cachedGuiTUNSettings()
|
||||
guiTUNSettings = TunSettingsDialog(parent=parent, **kwargs)
|
||||
guiTUNSettings.factoryToInput(Storage.UserTUNSettings())
|
||||
|
||||
return guiTUNSettings
|
||||
|
||||
@@ -28,7 +28,7 @@ from PySide6.QtWidgets import *
|
||||
__all__ = ['IndentDialog']
|
||||
|
||||
|
||||
class IndentDialog(AppQDialog):
|
||||
class IndentDialog(AppQTransientDialog):
|
||||
"""Represent indent spin box."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
@@ -54,7 +54,7 @@ def _validSubscriptionURL(value) -> str:
|
||||
return url
|
||||
|
||||
|
||||
class _SubscriptionEditorDialog(AppQDialog):
|
||||
class _SubscriptionEditorDialog(AppQTransientDialog):
|
||||
"""Edit one complete subscription definition with validation."""
|
||||
|
||||
def __init__(self, subscription=None, parent=None):
|
||||
|
||||
@@ -400,6 +400,10 @@ class TextEditorWindow(AppQMainWindow):
|
||||
def closeEvent(self, event: QtCore.QEvent):
|
||||
"""Handle closure of the text editor window."""
|
||||
if self.modified:
|
||||
# A close event arrives accepted by default. Keep the reusable
|
||||
# editor alive unless the user explicitly saves or discards.
|
||||
event.ignore()
|
||||
|
||||
mbox = MBoxQuestionSave(icon=AppQMessageBox.Icon.Question, parent=self)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
@@ -428,8 +432,11 @@ class TextEditorWindow(AppQMainWindow):
|
||||
|
||||
# Show the MessageBox and wait for the user to close it
|
||||
mbox.exec()
|
||||
|
||||
if event.isAccepted():
|
||||
super().closeEvent(event)
|
||||
else:
|
||||
event.accept()
|
||||
super().closeEvent(event)
|
||||
|
||||
def retranslate(self):
|
||||
# Do nothing
|
||||
|
||||
Reference in New Issue
Block a user