mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-05 13:28:03 +03:00
Unify Home and tray routing controls
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -180,7 +180,7 @@ class ConnectAction(AppQAction):
|
||||
self.setDisabled(value)
|
||||
|
||||
try:
|
||||
APP().systemTray.RoutingAction.setDisabled(value)
|
||||
APP().routingController.setInteractionEnabled(not value)
|
||||
except (AttributeError, RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
+38
-58
@@ -15,19 +15,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Implement tray actions for routing."""
|
||||
"""Present shared routing state through the tray action menu."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Repository import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Controllers import RoutingController
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
__all__ = ['RoutingAction']
|
||||
from PySide6 import QtCore
|
||||
|
||||
registerAppSettings('Routing', default=AppBuiltinRouting.BypassMainlandChina.value)
|
||||
__all__ = ['RoutingAction']
|
||||
|
||||
# ALL BUILTIN ROUTING VALUE
|
||||
_TRANSLATABLE_BUILTIN_ROUTING = [
|
||||
@@ -38,33 +36,32 @@ _TRANSLATABLE_BUILTIN_ROUTING = [
|
||||
|
||||
|
||||
class RoutingChildAction(AppQAction):
|
||||
"""Handle the routing child action."""
|
||||
"""Forward one tray route choice to the shared controller."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
controller: RoutingController,
|
||||
routingValue: str,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the RoutingChildAction."""
|
||||
self.routingValue = kwargs.pop('routingValue', None)
|
||||
self.controller = controller
|
||||
self.routingValue = routingValue
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
"""Handle activation of the action."""
|
||||
textEnglish = self.routingValue or self.textEnglish
|
||||
|
||||
if AppSettings.get('Routing') != textEnglish:
|
||||
AppSettings.set('Routing', textEnglish)
|
||||
|
||||
if APP().isSystemTrayConnected():
|
||||
APP().systemTray.ConnectAction.doReconnect()
|
||||
self.controller.selectRouting(self.routingValue)
|
||||
|
||||
|
||||
class RoutingAction(AppQAction):
|
||||
"""Handle the routing action."""
|
||||
"""Render application routing options as a synchronized tray menu."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, controller: RoutingController, **kwargs):
|
||||
"""Initialize the RoutingAction."""
|
||||
if AppSettings.get('Routing') == 'Bypass':
|
||||
# Update value for backward compatibility
|
||||
AppSettings.set('Routing', AppBuiltinRouting.BypassMainlandChina.value)
|
||||
self.controller = controller
|
||||
|
||||
super().__init__(
|
||||
_('Routing'),
|
||||
@@ -73,17 +70,13 @@ class RoutingAction(AppQAction):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.rebuildMenu()
|
||||
self.controller.stateChanged.connect(self._applyState)
|
||||
self.controller.interactionEnabledChanged.connect(self.setEnabled)
|
||||
|
||||
def routingActions(self):
|
||||
"""Return routing actions supported by the active core plugin."""
|
||||
config = self.activeConfig()
|
||||
if config is None:
|
||||
return list()
|
||||
self._applyState(*self.controller.state())
|
||||
|
||||
pluginRegistry = getPluginRegistry()
|
||||
options = pluginRegistry.routingOptions(config)
|
||||
routing = pluginRegistry.normalizeRouting(config, AppSettings.get('Routing'))
|
||||
def routingActions(self, options, routing: str):
|
||||
"""Build tray actions from one shared controller snapshot."""
|
||||
actions = list()
|
||||
|
||||
for option in options:
|
||||
@@ -97,6 +90,7 @@ class RoutingAction(AppQAction):
|
||||
if option.translatable
|
||||
else option.displayName
|
||||
),
|
||||
controller=self.controller,
|
||||
routingValue=option.id,
|
||||
checkable=True,
|
||||
checked=routing == option.id,
|
||||
@@ -105,27 +99,20 @@ class RoutingAction(AppQAction):
|
||||
|
||||
return actions
|
||||
|
||||
@staticmethod
|
||||
def activeConfig():
|
||||
"""Return the currently selected server configuration, if any."""
|
||||
try:
|
||||
index = Storage.UserActivatedItemIndex()
|
||||
servers = Storage.UserServers()
|
||||
@QtCore.Slot(object, str)
|
||||
def _applyState(self, options, routing: str):
|
||||
"""Rebuild the tray menu from one controller state snapshot."""
|
||||
oldActionGroup = getattr(self, '_actionGroup', None)
|
||||
|
||||
if 0 <= index < len(servers):
|
||||
return servers[index]
|
||||
except Exception:
|
||||
# The tray may be built before persistent storage is available.
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def rebuildMenu(self):
|
||||
"""Handle rebuild menu for the routing action."""
|
||||
self._menu.clear()
|
||||
self._menu._actions.clear()
|
||||
|
||||
if oldActionGroup is not None:
|
||||
oldActionGroup.deleteLater()
|
||||
|
||||
self._actionGroup = AppQActionGroup(self)
|
||||
actions = self.routingActions()
|
||||
|
||||
actions = self.routingActions(options, routing)
|
||||
|
||||
for action in actions:
|
||||
if isinstance(action, AppQSeperator):
|
||||
@@ -137,15 +124,8 @@ class RoutingAction(AppQAction):
|
||||
self._actionGroup.addAction(action)
|
||||
|
||||
self.setVisible(bool(actions))
|
||||
self.setEnabled(self.controller.interactionEnabled)
|
||||
|
||||
def getGlobalAction(self):
|
||||
"""Return the active plugin's global routing action, if supported."""
|
||||
return next(
|
||||
(
|
||||
action
|
||||
for action in self._menu._actions
|
||||
if isinstance(action, RoutingChildAction)
|
||||
and action.routingValue == AppBuiltinRouting.Global.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
def rebuildMenu(self):
|
||||
"""Refresh plugin options immediately before the tray menu opens."""
|
||||
self.controller.refresh(force=True)
|
||||
|
||||
@@ -23,6 +23,7 @@ from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Core import Tun2socks
|
||||
from Furious.Backends import OFFICIAL_PLUGIN_TYPES
|
||||
from Furious.Controllers import RoutingController
|
||||
from Furious.Extensions import BUNDLED_EXTENSION_TYPES
|
||||
from Furious.Plugins import getPluginRegistry, initializePluginRegistry
|
||||
from Furious.Qt import AppStyleSheet
|
||||
@@ -181,6 +182,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
self.mainWindow = None
|
||||
self.systemTray = None
|
||||
self.connectionAction = None
|
||||
self.routingController = None
|
||||
|
||||
# Unified logging service and presentation
|
||||
self.logManager = None
|
||||
@@ -544,8 +546,9 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
SystemProxy.off()
|
||||
SystemProxy.daemonOn_()
|
||||
|
||||
# The application owns the connection operation. Home and tray bind
|
||||
# to this same action, regardless of their construction order.
|
||||
# The application owns connection and routing operations. Home and
|
||||
# tray bind to these same objects regardless of construction order.
|
||||
self.routingController = RoutingController(parent=self)
|
||||
self.connectionAction = ConnectAction(isTrayAction=True)
|
||||
|
||||
self.mainWindow = MainWindow()
|
||||
|
||||
@@ -55,7 +55,7 @@ class TrayIcon(
|
||||
|
||||
actions = [
|
||||
APP().connectionAction,
|
||||
RoutingAction(isTrayAction=True),
|
||||
RoutingAction(APP().routingController, isTrayAction=True),
|
||||
ImportAction(isTrayAction=True),
|
||||
AppQSeperator(),
|
||||
ShowHomePageAction(isTrayAction=True),
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# 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/>.
|
||||
|
||||
"""Own application routing selection independently from its UI controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import (
|
||||
APP,
|
||||
AppBuiltinRouting,
|
||||
AppSettings,
|
||||
registerAppSettings,
|
||||
)
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Repository import Storage
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
__all__ = ['RoutingController']
|
||||
|
||||
registerAppSettings('Routing', default=AppBuiltinRouting.BypassMainlandChina.value)
|
||||
|
||||
|
||||
class RoutingController(QtCore.QObject):
|
||||
"""Expose one routing state shared by every application surface."""
|
||||
|
||||
stateChanged = QtCore.Signal(object, str)
|
||||
optionsChanged = QtCore.Signal(object)
|
||||
routingChanged = QtCore.Signal(str)
|
||||
interactionEnabledChanged = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize persisted routing state and plugin-provided options."""
|
||||
super().__init__(parent)
|
||||
|
||||
if AppSettings.get('Routing') == 'Bypass':
|
||||
# Update the value used by older Furious releases.
|
||||
AppSettings.set('Routing', AppBuiltinRouting.BypassMainlandChina.value)
|
||||
|
||||
self._options = tuple()
|
||||
self._routing = str(AppSettings.get('Routing'))
|
||||
self._interactionEnabled = True
|
||||
|
||||
self.refresh()
|
||||
|
||||
@staticmethod
|
||||
def activeConfiguration():
|
||||
"""Return the active server configuration, if one is available."""
|
||||
try:
|
||||
index = Storage.UserActivatedItemIndex()
|
||||
servers = Storage.UserServers()
|
||||
|
||||
if 0 <= index < len(servers):
|
||||
return servers[index]
|
||||
except Exception:
|
||||
# The controller can exist before persistent storage is ready.
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Return routing options supported by the active configuration."""
|
||||
return self._options
|
||||
|
||||
@property
|
||||
def routing(self) -> str:
|
||||
"""Return the normalized route shown by routing controls."""
|
||||
return self._routing
|
||||
|
||||
@property
|
||||
def interactionEnabled(self) -> bool:
|
||||
"""Return whether users may change routing at this lifecycle state."""
|
||||
return self._interactionEnabled
|
||||
|
||||
def state(self):
|
||||
"""Return the current immutable option and selection snapshot."""
|
||||
return self.options, self.routing
|
||||
|
||||
def refresh(self, *, force=False):
|
||||
"""Refresh routing capabilities from the active proxy-core plugin."""
|
||||
config = self.activeConfiguration()
|
||||
registry = getPluginRegistry()
|
||||
options = registry.routingOptions(config) if config is not None else tuple()
|
||||
routing = (
|
||||
registry.normalizeRouting(config, AppSettings.get('Routing'))
|
||||
if config is not None
|
||||
else str(AppSettings.get('Routing'))
|
||||
)
|
||||
|
||||
optionsChanged = options != self._options
|
||||
routingChanged = routing != self._routing
|
||||
|
||||
self._options = options
|
||||
self._routing = routing
|
||||
|
||||
if optionsChanged:
|
||||
self.optionsChanged.emit(options)
|
||||
|
||||
if routingChanged:
|
||||
self.routingChanged.emit(routing)
|
||||
|
||||
if force or optionsChanged or routingChanged:
|
||||
self.stateChanged.emit(options, routing)
|
||||
|
||||
return self.state()
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def selectRouting(self, routing: str) -> bool:
|
||||
"""Persist a supported route and reconnect the active proxy if needed."""
|
||||
options, _current = self.refresh()
|
||||
|
||||
if routing not in tuple(option.id for option in options):
|
||||
return False
|
||||
|
||||
if AppSettings.get('Routing') == routing:
|
||||
return False
|
||||
|
||||
AppSettings.set('Routing', routing)
|
||||
|
||||
self.refresh(force=True)
|
||||
|
||||
app = APP()
|
||||
|
||||
if app is not None and app.isSystemTrayConnected():
|
||||
app.connectionAction.doReconnect()
|
||||
|
||||
return True
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def setInteractionEnabled(self, enabled: bool):
|
||||
"""Publish whether routing controls may accept user interaction."""
|
||||
enabled = bool(enabled)
|
||||
|
||||
if enabled == self._interactionEnabled:
|
||||
return
|
||||
|
||||
self._interactionEnabled = enabled
|
||||
self.interactionEnabledChanged.emit(enabled)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
"""Expose presentation-independent application controllers."""
|
||||
|
||||
from .RoutingController import RoutingController
|
||||
from .SettingsController import SettingsController
|
||||
|
||||
__all__ = ['SettingsController']
|
||||
__all__ = ['RoutingController', 'SettingsController']
|
||||
|
||||
+76
-4
@@ -38,6 +38,7 @@ __all__ = [
|
||||
'moveToCenter',
|
||||
'AppQCheckBox',
|
||||
'AppQComboBox',
|
||||
'AppQComboBoxSeparatorDelegate',
|
||||
'AppQDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
@@ -96,6 +97,55 @@ class AppQCheckBox(Mixins.QTranslatable, QCheckBox):
|
||||
self.setText(_(self.text()))
|
||||
|
||||
|
||||
class AppQComboBoxSeparatorDelegate(Mixins.ThemeAware, QAbstractItemDelegate):
|
||||
"""Paint real combo-box separators with application theme tokens."""
|
||||
|
||||
SeparatorRoleValue = 'separator'
|
||||
HorizontalMargin = 8
|
||||
|
||||
def __init__(self, wrappedDelegate, comboBox):
|
||||
"""Wrap Qt's native delegate without changing normal item painting."""
|
||||
self.wrappedDelegate = wrappedDelegate
|
||||
self.comboBox = comboBox
|
||||
|
||||
super().__init__(comboBox)
|
||||
|
||||
@staticmethod
|
||||
def isSeparator(index) -> bool:
|
||||
"""Return whether *index* was created by QComboBox.insertSeparator()."""
|
||||
return (
|
||||
index.data(QtCore.Qt.ItemDataRole.AccessibleDescriptionRole)
|
||||
== AppQComboBoxSeparatorDelegate.SeparatorRoleValue
|
||||
)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
"""Paint separators and preserve Qt's native rendering for all items."""
|
||||
if not self.isSeparator(index):
|
||||
self.wrappedDelegate.paint(painter, option, index)
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
color = QColor(
|
||||
AppStyleSheet.paletteForTheme(APP().theme())['border_strong']
|
||||
)
|
||||
except (AttributeError, RuntimeError):
|
||||
color = option.palette.color(QPalette.ColorRole.Mid)
|
||||
|
||||
line = option.rect.adjusted(self.HorizontalMargin, 0, -self.HorizontalMargin, 0)
|
||||
y = line.center().y()
|
||||
|
||||
painter.fillRect(line.left(), y, max(0, line.width()), 1, color)
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
"""Keep the native popup's item and separator geometry unchanged."""
|
||||
return self.wrappedDelegate.sizeHint(option, index)
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Repaint the popup viewport after an application theme change."""
|
||||
self.comboBox.view().viewport().update()
|
||||
|
||||
|
||||
class AppQComboBox(Mixins.QTranslatable, QComboBox):
|
||||
"""Represent app q combo box."""
|
||||
|
||||
@@ -103,6 +153,18 @@ class AppQComboBox(Mixins.QTranslatable, QComboBox):
|
||||
"""Initialize the AppQComboBox."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._themedSeparatorDelegate = None
|
||||
|
||||
def enableThemedSeparators(self):
|
||||
"""Theme real insertSeparator() rows while preserving native items."""
|
||||
if self._themedSeparatorDelegate is not None:
|
||||
return
|
||||
|
||||
self._themedSeparatorDelegate = AppQComboBoxSeparatorDelegate(
|
||||
self.itemDelegate(), self
|
||||
)
|
||||
self.setItemDelegate(self._themedSeparatorDelegate)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the app q combo box."""
|
||||
for index in range(self.count()):
|
||||
@@ -1073,10 +1135,20 @@ def showMBoxDirectRulesNotAllowed(**kwargs):
|
||||
def handleResultCode(code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(AppQMessageBox.StandardButton.Yes):
|
||||
globalAction = APP().systemTray.RoutingAction.getGlobalAction()
|
||||
if globalAction is not None:
|
||||
globalAction.trigger()
|
||||
APP().systemTray.ConnectAction.trigger()
|
||||
app = APP()
|
||||
|
||||
controller, wasConnected = (
|
||||
app.routingController,
|
||||
app.isSystemTrayConnected(),
|
||||
)
|
||||
|
||||
changed = controller.selectRouting(AppBuiltinRouting.Global.value)
|
||||
|
||||
if changed or controller.routing == AppBuiltinRouting.Global.value:
|
||||
# Selecting a route reconnects an established connection itself.
|
||||
# During an initial connection attempt, resume that attempt here.
|
||||
if not wasConnected:
|
||||
app.connectionAction.trigger()
|
||||
else:
|
||||
# Do nothing
|
||||
pass
|
||||
|
||||
@@ -53,6 +53,7 @@ from .QtNetwork import AppQNetworkAccessManager
|
||||
from .QtWidgets import (
|
||||
AppQCheckBox,
|
||||
AppQComboBox,
|
||||
AppQComboBoxSeparatorDelegate,
|
||||
AppQDialog,
|
||||
AppQDialogButtonBox,
|
||||
AppQGroupBox,
|
||||
@@ -102,6 +103,7 @@ __all__ = [
|
||||
'AppQActionGroup',
|
||||
'AppQCheckBox',
|
||||
'AppQComboBox',
|
||||
'AppQComboBoxSeparatorDelegate',
|
||||
'AppQDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# 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/>.
|
||||
|
||||
"""Present the shared routing state as a compact Fluent-style selector."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Controllers import RoutingController
|
||||
from Furious.Frozenlib import Mixins
|
||||
from Furious.Qt import AppQComboBox
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QSizePolicy
|
||||
|
||||
__all__ = ['RoutingSelector']
|
||||
|
||||
|
||||
class RoutingSelector(AppQComboBox):
|
||||
"""Select routes through the application-wide routing controller."""
|
||||
|
||||
def __init__(self, controller: RoutingController, parent=None):
|
||||
"""Bind the selector to one shared routing controller."""
|
||||
self.controller = controller
|
||||
|
||||
# This widget restores its enabled state from the shared controller in
|
||||
# retranslate(), so the generic translation disable/enable wrapper is
|
||||
# neither needed nor safe during connection transitions.
|
||||
super().__init__(parent, useQSetDisabled=False)
|
||||
|
||||
self.setObjectName('HomeRoutingSelector')
|
||||
self.enableThemedSeparators()
|
||||
self.setMinimumWidth(180)
|
||||
self.setMaximumWidth(280)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
self.setAccessibleName(_('Routing'))
|
||||
self.setToolTip(_('Routing'))
|
||||
|
||||
self.currentIndexChanged.connect(self._handleCurrentIndexChanged)
|
||||
self.controller.stateChanged.connect(self._applyState)
|
||||
self.controller.interactionEnabledChanged.connect(self._applyInteractionEnabled)
|
||||
|
||||
self._applyState(*self.controller.state())
|
||||
|
||||
@staticmethod
|
||||
def _optionText(option) -> str:
|
||||
"""Return an option's translated or literal display name."""
|
||||
return _(option.displayName) if option.translatable else option.displayName
|
||||
|
||||
@QtCore.Slot(object, str)
|
||||
def _applyState(self, options, routing: str):
|
||||
"""Replace items atomically from one controller state snapshot."""
|
||||
with Mixins.QBlockSignalContext(self):
|
||||
self.clear()
|
||||
|
||||
for option in options:
|
||||
if option.separatorBefore and self.count():
|
||||
self.insertSeparator(self.count())
|
||||
|
||||
self.addItem(self._optionText(option), option.id)
|
||||
|
||||
index = self.findData(routing)
|
||||
|
||||
self.setCurrentIndex(index if index >= 0 else 0)
|
||||
|
||||
self.setVisible(bool(options))
|
||||
self._applyInteractionEnabled(self.controller.interactionEnabled)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def _applyInteractionEnabled(self, enabled: bool):
|
||||
"""Enable selection only when options and connection state allow it."""
|
||||
self.setEnabled(bool(enabled) and self.count() > 0)
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def _handleCurrentIndexChanged(self, index: int):
|
||||
"""Forward user selection without retaining independent state."""
|
||||
routing = self.itemData(index)
|
||||
|
||||
if isinstance(routing, str):
|
||||
self.controller.selectRouting(routing)
|
||||
|
||||
def showPopup(self):
|
||||
"""Refresh plugin capabilities immediately before showing options."""
|
||||
self.controller.refresh(force=True)
|
||||
|
||||
super().showPopup()
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh option text and routing accessibility metadata."""
|
||||
self._applyState(*self.controller.state())
|
||||
self.setAccessibleName(_('Routing'))
|
||||
self.setToolTip(_('Routing'))
|
||||
@@ -1041,6 +1041,8 @@ class DeleteServersProgressDialog(AppQDialog):
|
||||
# Set invalid first
|
||||
AppSettings.set('ActivatedItemIndex', str(-1))
|
||||
|
||||
self.table.activeServerChanged.emit()
|
||||
|
||||
if APP().isSystemTrayConnected():
|
||||
if self.showTrayMessage:
|
||||
# Trigger disconnect
|
||||
@@ -1462,6 +1464,8 @@ class ServerTableView(
|
||||
):
|
||||
"""Represent user servers Qt table view."""
|
||||
|
||||
activeServerChanged = QtCore.Signal()
|
||||
|
||||
RowHeight = 42
|
||||
|
||||
Headers = [
|
||||
@@ -1953,6 +1957,7 @@ class ServerTableView(
|
||||
def activateItemByIndex(self, index, activate):
|
||||
"""Activate item by index."""
|
||||
oldIndex = Storage.UserActivatedItemIndex()
|
||||
changed = activate and oldIndex != int(index)
|
||||
|
||||
if activate:
|
||||
AppSettings.set('ActivatedItemIndex', str(index))
|
||||
@@ -1960,6 +1965,9 @@ class ServerTableView(
|
||||
self.sourceModel.emitRowChanged(oldIndex)
|
||||
self.sourceModel.emitRowChanged(index)
|
||||
|
||||
if changed:
|
||||
self.activeServerChanged.emit()
|
||||
|
||||
def flushItem(self, row: int, column: int, item: ServerProfile):
|
||||
"""Refresh item."""
|
||||
itemIndex = item.index
|
||||
@@ -2092,6 +2100,9 @@ class ServerTableView(
|
||||
|
||||
self.sourceModel.emitRowChanged(row)
|
||||
|
||||
if row == Storage.UserActivatedItemIndex():
|
||||
self.activeServerChanged.emit()
|
||||
|
||||
def flushAll(self):
|
||||
# Refresh index
|
||||
"""Refresh all."""
|
||||
@@ -2251,6 +2262,8 @@ class ServerTableView(
|
||||
# Set invalid first
|
||||
AppSettings.set('ActivatedItemIndex', str(-1))
|
||||
|
||||
self.activeServerChanged.emit()
|
||||
|
||||
if APP().isSystemTrayConnected():
|
||||
if showTrayMessage:
|
||||
# Trigger disconnect
|
||||
@@ -2574,6 +2587,8 @@ class ServerTableView(
|
||||
|
||||
AppSettings.set('ActivatedItemIndex', str(newActivatedIndex))
|
||||
|
||||
self.activeServerChanged.emit()
|
||||
|
||||
self.sourceModel.refreshIndexes()
|
||||
self.proxyModel.invalidate()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
from .ConnectionProgressWidget import ConnectionProgressWidget
|
||||
from .MetricsGraph import MetricsGraphWidget
|
||||
from .NavigationView import NavigationView
|
||||
from .RoutingSelector import RoutingSelector
|
||||
from .ServerTableView import ServerTableView
|
||||
from .SubscriptionTableView import SubscriptionTableView
|
||||
from .WaitingSpinner import WaitingSpinner
|
||||
@@ -30,6 +31,7 @@ __all__ = [
|
||||
'ConnectionProgressWidget',
|
||||
'MetricsGraphWidget',
|
||||
'NavigationView',
|
||||
'RoutingSelector',
|
||||
'ServerTableView',
|
||||
'SubscriptionTableView',
|
||||
'WaitingSpinner',
|
||||
|
||||
@@ -40,6 +40,7 @@ from Furious.Actions.Import import (
|
||||
ImportURIFromClipboardAction,
|
||||
)
|
||||
from Furious.Widget.ConnectionButton import ConnectionButton
|
||||
from Furious.Widget.RoutingSelector import RoutingSelector
|
||||
from Furious.Widget.ServerTableView import *
|
||||
from Furious.Window.NetworkTestDialog import *
|
||||
from Furious.Window.ProxyBypassDialog import *
|
||||
@@ -515,6 +516,8 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.activateSelectedServerForConnection,
|
||||
parent=self,
|
||||
)
|
||||
self.routingController = APP().routingController
|
||||
self.routingSelector = RoutingSelector(self.routingController, parent=self)
|
||||
|
||||
self.searchLineEdit = AppQLineEdit()
|
||||
self.searchLineEdit.setPlaceholderText(
|
||||
@@ -550,6 +553,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.actionLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.actionLayout.setSpacing(8)
|
||||
self.actionLayout.addWidget(self.connectButton)
|
||||
self.actionLayout.addWidget(self.routingSelector)
|
||||
self.actionLayout.addWidget(self.serverButton)
|
||||
self.actionLayout.addStretch(1)
|
||||
self.actionLayout.addWidget(self.subscriptionFilterComboBox)
|
||||
@@ -577,6 +581,9 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.userServersQTableWidget.selectionModel().selectionChanged.connect(
|
||||
self.handleServerSelectionChanged
|
||||
)
|
||||
self.userServersQTableWidget.activeServerChanged.connect(
|
||||
self.routingController.refresh
|
||||
)
|
||||
|
||||
self.refreshSubscriptionFilter()
|
||||
self.handleServerSelectionChanged()
|
||||
@@ -607,6 +614,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
return False
|
||||
|
||||
self.userServersQTableWidget.activateSelectedServer()
|
||||
self.routingController.refresh()
|
||||
|
||||
return Storage.UserActivatedItemIndex() == indexes[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user