Files
LorenEteval_Furious/Furious/Frozenlib/Mixins.py
T
2026-08-12 12:20:14 +08:00

338 lines
11 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) 2024present 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/>.
"""Provide bundled mixins."""
from __future__ import annotations
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
from shiboken6 import isValid as isValidQObject
import logging
import weakref
logger = logging.getLogger(__name__)
__all__ = ['Mixins']
class _WeakObjectsPool:
"""Store objects in registration order without extending their lifetime."""
def __init__(self):
"""Initialize an empty weak object registry."""
self._references = {}
def _removeReference(self, key, reference):
"""Remove a weak reference if it is still registered under its object ID."""
if self._references.get(key) is reference:
self._references.pop(key)
def append(self, ob):
"""Register an object without retaining a strong reference to it."""
key = id(ob)
current = self._references.get(key)
if current is not None:
if current() is ob:
raise ValueError('object is already registered')
self._references.pop(key)
reference = weakref.ref(
ob,
lambda ref, _key=key: self._removeReference(_key, ref),
)
self._references[key] = reference
if isinstance(ob, QtCore.QObject):
ob.destroyed.connect(
lambda *_args, _key=key, _reference=reference: self._removeReference(
_key, _reference
)
)
def remove(self, ob):
"""Remove an object's registration by identity."""
key = id(ob)
reference = self._references.get(key)
if reference is None or reference() is not ob:
raise ValueError('object is not registered')
self._references.pop(key)
def clear(self):
"""Remove every registration from the registry."""
self._references.clear()
def prune(self, predicate=None):
"""Remove dead objects and live objects rejected by a predicate."""
for key, reference in list(self._references.items()):
ob = reference()
if ob is None or (predicate is not None and not predicate(ob)):
self._removeReference(key, reference)
def __iter__(self):
"""Iterate over a stable snapshot of currently live objects."""
for key, reference in list(self._references.items()):
ob = reference()
if ob is None:
self._removeReference(key, reference)
else:
yield ob
def __len__(self):
"""Return the number of currently live registrations."""
self.prune()
return len(self._references)
class Mixins:
"""Group reusable lifecycle, translation, theme, and Qt context mixins."""
@staticmethod
def qObjectIsValid(qobject) -> bool:
"""Return the q object is valid value used by the mixins."""
if not isinstance(qobject, QtCore.QObject):
return True
try:
return isValidQObject(qobject)
except RuntimeError:
return False
class ConnectionAware:
"""Represent connection aware."""
ObjectsPool = _WeakObjectsPool()
def __init__(self, *args, **kwargs):
"""Initialize the ConnectionAware."""
super().__init__(*args, **kwargs)
Mixins.ConnectionAware.ObjectsPool.append(self)
def disconnectedCallback(self):
"""Update the connection aware for a disconnected state."""
raise NotImplementedError
def connectedCallback(self):
"""Update the connection aware for a connected state."""
raise NotImplementedError
@staticmethod
def callConnectedCallback():
"""Call connected callback."""
Mixins.ConnectionAware.ObjectsPool.prune(Mixins.qObjectIsValid)
for ob in list(Mixins.ConnectionAware.ObjectsPool):
assert isinstance(ob, Mixins.ConnectionAware)
ob.connectedCallback()
@staticmethod
def callDisconnectedCallback():
"""Call disconnected callback."""
Mixins.ConnectionAware.ObjectsPool.prune(Mixins.qObjectIsValid)
for ob in list(Mixins.ConnectionAware.ObjectsPool):
assert isinstance(ob, Mixins.ConnectionAware)
ob.disconnectedCallback()
class ThemeAware:
"""Represent theme aware."""
ObjectsPool = _WeakObjectsPool()
def __init__(self, *args, **kwargs):
"""Initialize the ThemeAware."""
super().__init__(*args, **kwargs)
Mixins.ThemeAware.ObjectsPool.append(self)
def themeChangedCallback(self, theme: str):
"""Update the theme aware for a theme change."""
raise NotImplementedError
@staticmethod
def callThemeChangedCallbackUnchecked(theme: str):
"""Call theme changed callback unchecked."""
Mixins.ThemeAware.ObjectsPool.prune(Mixins.qObjectIsValid)
for ob in list(Mixins.ThemeAware.ObjectsPool):
assert isinstance(ob, Mixins.ThemeAware)
ob.themeChangedCallback(theme)
@staticmethod
def callThemeChangedCallback(theme: str):
"""Call theme changed callback."""
try:
app = QApplication.instance()
if app is not None and app.isDarkModeEnabled():
# Ignore application dark detect system
logger.info(f'ignore system theme \'{theme}\' changes in dark mode')
return
except Exception:
# Any non-exit exceptions
pass
logger.info(f'system theme changed to \'{theme}\'')
Mixins.ThemeAware.callThemeChangedCallbackUnchecked(theme)
class CleanupOnExit:
"""Represent cleanup on exit."""
ObjectsPool = _WeakObjectsPool()
VisitedType = dict()
def __init__(self, *args, **kwargs):
"""Initialize the CleanupOnExit."""
self.uniqueCleanup = kwargs.pop('uniqueCleanup', True)
super().__init__(*args, **kwargs)
Mixins.CleanupOnExit.ObjectsPool.append(self)
def cleanup(self):
"""Release resources owned by the cleanup on exit."""
raise NotImplementedError
@staticmethod
def cleanupAll():
"""Handle cleanup all for the cleanup on exit."""
Mixins.CleanupOnExit.ObjectsPool.prune(Mixins.qObjectIsValid)
for ob in list(Mixins.CleanupOnExit.ObjectsPool):
assert isinstance(ob, Mixins.CleanupOnExit)
if ob.uniqueCleanup:
obtype = str(type(ob))
if not Mixins.CleanupOnExit.VisitedType.get(obtype, False):
ob.cleanup()
Mixins.CleanupOnExit.VisitedType[obtype] = True
else:
pass
else:
ob.cleanup()
Mixins.CleanupOnExit.ObjectsPool.clear()
Mixins.CleanupOnExit.VisitedType.clear()
class QSetDisabledContext:
"""Manage the q set disabled context."""
def __init__(self, qobject: QtCore.QObject):
"""Initialize the QSetDisabledContext."""
self.qobject = qobject
def __enter__(self):
"""Enter the q set disabled context context."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'setDisabled'
):
self.qobject.setDisabled(True)
def __exit__(self, exceptionType, exceptionValue, tb):
"""Exit the q set disabled context context and restore state."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'setDisabled'
):
self.qobject.setDisabled(False)
class QBlockSignalContext:
"""Manage the q block signal context."""
def __init__(self, qobject: QtCore.QObject):
"""Initialize the QBlockSignalContext."""
self.qobject = qobject
def __enter__(self):
"""Enter the q block signal context context."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'blockSignals'
):
self.qobject.blockSignals(True)
def __exit__(self, exceptionType, exceptionValue, tb):
"""Exit the q block signal context context and restore state."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'blockSignals'
):
self.qobject.blockSignals(False)
class QTranslatable:
"""Represent q translatable."""
ObjectsPool = _WeakObjectsPool()
def __init__(self, *args, **kwargs):
"""Initialize the QTranslatable."""
self.translatable = kwargs.pop('translatable', True)
self.useQSetDisabled = kwargs.pop('useQSetDisabled', True)
super().__init__(*args, **kwargs)
Mixins.QTranslatable.ObjectsPool.append(self)
def retranslate(self):
"""Refresh translated text for the q translatable."""
raise NotImplementedError
@staticmethod
def unregister(ob):
"""Handle unregister for the q translatable."""
try:
Mixins.QTranslatable.ObjectsPool.remove(ob)
except ValueError:
pass
@staticmethod
def pruneObjectsPool():
"""Handle prune objects pool for the q translatable."""
Mixins.QTranslatable.ObjectsPool.prune(Mixins.qObjectIsValid)
@staticmethod
def retranslateAll():
"""Handle retranslate all for the q translatable."""
Mixins.QTranslatable.pruneObjectsPool()
for ob in list(Mixins.QTranslatable.ObjectsPool):
assert isinstance(ob, Mixins.QTranslatable)
if ob.translatable:
if ob.useQSetDisabled:
assert isinstance(ob, QtCore.QObject)
with Mixins.QSetDisabledContext(ob):
ob.retranslate()
else:
ob.retranslate()