mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-06 22:08:11 +03:00
Unify Home and tray connection controls
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -36,17 +36,36 @@ from Furious.Widget.ConnectionProgressWidget import ConnectionProgressWidget
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import queue
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['ConnectAction']
|
||||
__all__ = ['ConnectionState', 'ConnectAction']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
registerAppSettings('Connect', isBinary=True)
|
||||
|
||||
|
||||
class ConnectionState(Enum):
|
||||
"""Describe the shared application connection lifecycle."""
|
||||
|
||||
Disconnected = 'Connect'
|
||||
Connecting = 'Connecting'
|
||||
Connected = 'Disconnect'
|
||||
Disconnecting = 'Disconnecting'
|
||||
|
||||
|
||||
_TRANSLATABLE_CONNECTION_STATES = (
|
||||
_('Connect'),
|
||||
_('Connecting'),
|
||||
_('Disconnect'),
|
||||
_('Disconnecting'),
|
||||
)
|
||||
|
||||
|
||||
def validateProxyServer(server) -> bool:
|
||||
"""Validate proxy server."""
|
||||
try:
|
||||
@@ -63,7 +82,9 @@ def validateProxyServer(server) -> bool:
|
||||
|
||||
|
||||
class ConnectAction(AppQAction):
|
||||
"""Handle the connect action."""
|
||||
"""Own connection operations and expose their state to every UI surface."""
|
||||
|
||||
stateChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the ConnectAction."""
|
||||
@@ -77,23 +98,62 @@ class ConnectAction(AppQAction):
|
||||
self.actionQueue = queue.Queue()
|
||||
self.coreManager = ConnectionManager()
|
||||
self.progressBar = ConnectionProgressWidget()
|
||||
self._state = ConnectionState.Disconnected
|
||||
self._activeConfiguration = None
|
||||
|
||||
self.actionTimer = QtCore.QTimer()
|
||||
self.actionTimer.timeout.connect(lambda: self.callActionFromQueue())
|
||||
|
||||
self.updatesManager = UpdateManager()
|
||||
|
||||
@property
|
||||
def state(self) -> ConnectionState:
|
||||
"""Return the current connection lifecycle state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def activeConfiguration(self):
|
||||
"""Return the configuration owned by the current connection attempt."""
|
||||
return self._activeConfiguration
|
||||
|
||||
def _applyStatePresentation(self):
|
||||
"""Apply the tray action presentation for the shared lifecycle state."""
|
||||
state = self.state
|
||||
self.setText(_(state.value))
|
||||
self.setChecked(
|
||||
state
|
||||
in (
|
||||
ConnectionState.Connecting,
|
||||
ConnectionState.Connected,
|
||||
)
|
||||
)
|
||||
|
||||
if state in (ConnectionState.Connecting, ConnectionState.Connected):
|
||||
self.setIcon(bootstrapIcon('lock-fill.svg'))
|
||||
else:
|
||||
self.setIcon(bootstrapIcon('unlock-fill.svg'))
|
||||
|
||||
self.setDisabledAction(
|
||||
state in (ConnectionState.Connecting, ConnectionState.Disconnecting)
|
||||
)
|
||||
|
||||
def _setState(self, state: ConnectionState):
|
||||
"""Publish one atomic lifecycle transition and its action presentation."""
|
||||
changed = state is not self._state
|
||||
self._state = state
|
||||
self._applyStatePresentation()
|
||||
|
||||
if changed:
|
||||
self.stateChanged.emit(state)
|
||||
|
||||
def reset(self):
|
||||
"""Restore the connect action to its initial state."""
|
||||
self.hideProgressBar(True)
|
||||
self.setText(_('Connect'))
|
||||
self.setIcon(bootstrapIcon('unlock-fill.svg'))
|
||||
self.setChecked(False)
|
||||
self._activeConfiguration = None
|
||||
|
||||
AppSettings.turnOFF('Connect')
|
||||
|
||||
# Accept new action
|
||||
self.setDisabledAction(False)
|
||||
self._setState(ConnectionState.Disconnected)
|
||||
|
||||
def showProgressBar(self):
|
||||
"""Show progress bar."""
|
||||
@@ -119,7 +179,10 @@ class ConnectAction(AppQAction):
|
||||
"""Set disabled action."""
|
||||
self.setDisabled(value)
|
||||
|
||||
APP().systemTray.RoutingAction.setDisabled(value)
|
||||
try:
|
||||
APP().systemTray.RoutingAction.setDisabled(value)
|
||||
except (AttributeError, RuntimeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
APP().mainWindow.settingsPage.setConnectionControlsEnabled(not value)
|
||||
@@ -128,35 +191,38 @@ class ConnectAction(AppQAction):
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
"""Return whether connected."""
|
||||
return self.textCompare('Disconnect')
|
||||
return self.state is ConnectionState.Connected
|
||||
|
||||
def isConnecting(self):
|
||||
"""Return whether connecting."""
|
||||
return self.textCompare('Connecting')
|
||||
return self.state is ConnectionState.Connecting
|
||||
|
||||
def isDisconnecting(self):
|
||||
"""Return whether disconnecting."""
|
||||
return self.state is ConnectionState.Disconnecting
|
||||
|
||||
def doConnecting(self):
|
||||
"""Handle do connecting for the connect action."""
|
||||
self.setText(_('Connecting'))
|
||||
self.setIcon(bootstrapIcon('lock-fill.svg'))
|
||||
# Do not accept new action
|
||||
self.setDisabledAction(True)
|
||||
self._setState(ConnectionState.Connecting)
|
||||
self.showProgressBar()
|
||||
|
||||
def doConnected(self):
|
||||
"""Handle do connected for the connect action."""
|
||||
self.hideProgressBar(True)
|
||||
# Connected
|
||||
self.setText(_('Disconnect'))
|
||||
|
||||
AppSettings.turnON_('Connect')
|
||||
|
||||
Mixins.ConnectionAware.callConnectedCallback()
|
||||
self._setState(ConnectionState.Connected)
|
||||
|
||||
# Accept new action
|
||||
self.setDisabledAction(False)
|
||||
Mixins.ConnectionAware.callConnectedCallback()
|
||||
|
||||
def doDisconnect(self):
|
||||
"""Handle do disconnect for the connect action."""
|
||||
if self.state is ConnectionState.Disconnected:
|
||||
return
|
||||
|
||||
self._setState(ConnectionState.Disconnecting)
|
||||
|
||||
SystemProxy.off()
|
||||
|
||||
self.actionTimer.stop()
|
||||
@@ -188,7 +254,7 @@ class ConnectAction(AppQAction):
|
||||
def doConnect(self):
|
||||
# Connect action
|
||||
"""Return the do connect value used by the connect action."""
|
||||
assert self.textCompare('Connect')
|
||||
assert self.state is ConnectionState.Disconnected
|
||||
|
||||
if not Storage.UserServers():
|
||||
AppSettings.turnOFF('Connect')
|
||||
@@ -261,6 +327,7 @@ class ConnectAction(AppQAction):
|
||||
|
||||
return
|
||||
|
||||
self._activeConfiguration = config
|
||||
self.doConnecting()
|
||||
|
||||
logManager = AppLogManager()
|
||||
@@ -424,10 +491,15 @@ class ConnectAction(AppQAction):
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
"""Handle activation of the action."""
|
||||
if checked:
|
||||
if self.state is ConnectionState.Disconnected:
|
||||
self.doConnect()
|
||||
else:
|
||||
# Disconnect action
|
||||
assert self.textCompare('Disconnect')
|
||||
|
||||
elif self.state is ConnectionState.Connected:
|
||||
self.doDisconnectWithTrayMessage(_('Disconnected'))
|
||||
else:
|
||||
# A disabled transition action should not normally be triggered.
|
||||
# Restore its checked presentation if code triggered it directly.
|
||||
self._applyStatePresentation()
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh the state-derived action text."""
|
||||
self._applyStatePresentation()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Connection import ConnectAction
|
||||
from .Connection import ConnectionState, ConnectAction
|
||||
from .Exit import ExitAction
|
||||
from .Home import ShowHomePageAction
|
||||
from .Import import (
|
||||
@@ -34,6 +34,7 @@ from .Routing import RoutingAction
|
||||
|
||||
__all__ = [
|
||||
'ConnectAction',
|
||||
'ConnectionState',
|
||||
'ExitAction',
|
||||
'ImportAction',
|
||||
'ImportFromFileAction',
|
||||
|
||||
@@ -30,6 +30,7 @@ from Furious.Qt.TextEditorTheme import configureEditorLogMetadata
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Repository import *
|
||||
from Furious.Service import ApplicationLogHandler, LogManager
|
||||
from Furious.Actions.Connection import ConnectAction
|
||||
from Furious.Application.TrayIcon import *
|
||||
from Furious.Window.LogPage import *
|
||||
from Furious.Window.MainWindow import *
|
||||
@@ -179,6 +180,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
|
||||
self.mainWindow = None
|
||||
self.systemTray = None
|
||||
self.connectionAction = None
|
||||
|
||||
# Unified logging service and presentation
|
||||
self.logManager = None
|
||||
@@ -295,10 +297,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
|
||||
def isSystemTrayConnected(self):
|
||||
"""Return whether system tray connected."""
|
||||
if isinstance(self.systemTray, TrayIcon):
|
||||
return self.systemTray.ConnectAction.isConnected()
|
||||
else:
|
||||
return False
|
||||
return self.connectionAction is not None and self.connectionAction.isConnected()
|
||||
|
||||
def isDarkMode(self):
|
||||
"""Return whether dark mode."""
|
||||
@@ -545,6 +544,10 @@ 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.
|
||||
self.connectionAction = ConnectAction(isTrayAction=True)
|
||||
|
||||
self.mainWindow = MainWindow()
|
||||
self.systemTray = TrayIcon()
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class TrayIcon(
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
actions = [
|
||||
ConnectAction(isTrayAction=True),
|
||||
APP().connectionAction,
|
||||
RoutingAction(isTrayAction=True),
|
||||
ImportAction(isTrayAction=True),
|
||||
AppQSeperator(),
|
||||
|
||||
Vendored
+8
@@ -2875,5 +2875,13 @@ TRANSLATION = {
|
||||
"RU": "Неизвестная подписка",
|
||||
"ZH": "未知订阅",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Disconnecting": {
|
||||
"source": [
|
||||
"Furious.Actions.Connection"
|
||||
],
|
||||
"RU": "Отключение",
|
||||
"ZH": "正在断开连接",
|
||||
"isReviewed": "True"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ class AppStyleSheet:
|
||||
QWidget#SettingsPageContent,
|
||||
QScrollArea#SubscriptionScrollArea,
|
||||
QWidget#SubscriptionPageContent,
|
||||
QWidget#HomePageContent,
|
||||
QWidget#LogPageContent {{
|
||||
border: none;
|
||||
background-color: {palette['window']};
|
||||
@@ -335,6 +336,7 @@ class AppStyleSheet:
|
||||
QLabel#MetricsPageTitle,
|
||||
QLabel#LogPageTitle,
|
||||
QLabel#SubscriptionPageTitle,
|
||||
QLabel#HomePageTitle,
|
||||
QLabel#SettingsPageTitle {{
|
||||
color: {palette['text_strong']};
|
||||
font-size: 16pt;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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/>.
|
||||
|
||||
"""Provide connection controls backed by the shared connection action."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Actions.Connection import ConnectionState
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QPushButton, QSizePolicy
|
||||
|
||||
from typing import Callable
|
||||
|
||||
__all__ = ['ConnectionButton']
|
||||
|
||||
|
||||
class ConnectionButton(QPushButton):
|
||||
"""Present a shared connection action with a local selection policy."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connectionAction,
|
||||
activateSelected: Callable[[], bool],
|
||||
parent=None,
|
||||
):
|
||||
"""Bind to one connection action without duplicating its state machine."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.connectionAction = connectionAction
|
||||
self.activateSelected = activateSelected
|
||||
self._selectionCount = 0
|
||||
|
||||
self.setObjectName('ConnectionButton')
|
||||
self.setMinimumWidth(124)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.setIconSize(QtCore.QSize(18, 18))
|
||||
|
||||
self.clicked.connect(self._handleClicked)
|
||||
self.connectionAction.changed.connect(self.syncPresentation)
|
||||
self.connectionAction.stateChanged.connect(self.syncPresentation)
|
||||
|
||||
self.syncPresentation()
|
||||
|
||||
def setSelectionCount(self, count: int):
|
||||
"""Update the disconnected-state selection policy."""
|
||||
self._selectionCount = max(0, count)
|
||||
self.syncPresentation()
|
||||
|
||||
@QtCore.Slot()
|
||||
def syncPresentation(self, *_args):
|
||||
"""Mirror text/icon/state while applying only Home's selection rule."""
|
||||
action = self.connectionAction
|
||||
state = action.state
|
||||
|
||||
self.setText(action.text())
|
||||
self.setIcon(action.icon())
|
||||
|
||||
if state is ConnectionState.Disconnected:
|
||||
# Zero-selection remains intentionally clickable but is a no-op.
|
||||
enabled = self._selectionCount <= 1 and action.isEnabled()
|
||||
elif state is ConnectionState.Connected:
|
||||
# Disconnect always targets the active connection, not selection.
|
||||
enabled = action.isEnabled()
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
self.setEnabled(enabled)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _handleClicked(self):
|
||||
"""Delegate connect/disconnect to the shared action."""
|
||||
state = self.connectionAction.state
|
||||
|
||||
if state is ConnectionState.Disconnected:
|
||||
if self._selectionCount != 1 or not self.activateSelected():
|
||||
return
|
||||
|
||||
if self.connectionAction.state is ConnectionState.Disconnected:
|
||||
self.connectionAction.trigger()
|
||||
elif state is ConnectionState.Connected:
|
||||
self.connectionAction.trigger()
|
||||
+81
-50
@@ -39,6 +39,7 @@ from Furious.Actions.Import import (
|
||||
ImportQRCodeOnTheScreenAction,
|
||||
ImportURIFromClipboardAction,
|
||||
)
|
||||
from Furious.Widget.ConnectionButton import ConnectionButton
|
||||
from Furious.Widget.ServerTableView import *
|
||||
from Furious.Window.NetworkTestDialog import *
|
||||
from Furious.Window.ProxyBypassDialog import *
|
||||
@@ -474,49 +475,14 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
)
|
||||
)
|
||||
|
||||
if hasattr(AppQAction, 'setMenu'):
|
||||
self.toolbar = AppQToolBar(
|
||||
AppQAction(
|
||||
_('Server'),
|
||||
icon=bootstrapIcon('server.svg'),
|
||||
menu=AppQMenu(*serverActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
)
|
||||
self.toolbar.setObjectName('HomePageToolBar')
|
||||
self.toolbar.setMovable(False)
|
||||
self.toolbar.setFloatable(False)
|
||||
self.toolbar.setIconSize(QtCore.QSize(64, 32))
|
||||
self.toolbar.setToolButtonStyle(
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon
|
||||
)
|
||||
self.addToolBar(self.toolbar)
|
||||
else:
|
||||
# Menu actions
|
||||
serverMenu = {
|
||||
'name': 'Server',
|
||||
'actions': [*serverActions],
|
||||
}
|
||||
|
||||
# Corresponds to menus defined above
|
||||
_TRANSLATABLE_MENU_NAME = [
|
||||
_('Server'),
|
||||
]
|
||||
|
||||
# Menus
|
||||
for menuDict in (serverMenu,):
|
||||
menuName = menuDict['name']
|
||||
menuObjName = f'_{menuName}Menu'
|
||||
menu = AppQMenu(
|
||||
*menuDict['actions'], title=_(menuName), parent=self.menuBar()
|
||||
)
|
||||
|
||||
# Set reference
|
||||
setattr(self, menuObjName, menu)
|
||||
|
||||
self.menuBar().addMenu(menu)
|
||||
self.serverMenu = AppQMenu(*serverActions, parent=self)
|
||||
self.serverButton = AppQPushButton(
|
||||
_('Server'),
|
||||
icon=bootstrapIcon('server.svg'),
|
||||
)
|
||||
# Keep the protocol/profile creation menu without presenting this as a
|
||||
# split or drop-down button. The regular button opens the menu itself.
|
||||
self.serverButton.clicked.connect(self.showServerMenu)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu)
|
||||
|
||||
@@ -536,9 +502,19 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.statusBar().addPermanentWidget(self.connectionStatus)
|
||||
|
||||
self._widget = QWidget()
|
||||
self._widget.setObjectName('HomePageContent')
|
||||
self._layout = QVBoxLayout(self._widget)
|
||||
self._layout.setContentsMargins(20, 18, 20, 20)
|
||||
self._layout.setSpacing(12)
|
||||
|
||||
self.searchLayout = QHBoxLayout()
|
||||
self.pageTitleLabel = AppQLabel(_('Server'))
|
||||
self.pageTitleLabel.setObjectName('HomePageTitle')
|
||||
|
||||
self.connectButton = ConnectionButton(
|
||||
APP().connectionAction,
|
||||
self.activateSelectedServerForConnection,
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self.searchLineEdit = AppQLineEdit()
|
||||
self.searchLineEdit.setPlaceholderText(
|
||||
@@ -546,17 +522,41 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
'Search servers with text or regex, e.g. trojan, hk|jp, ^vmess, (us|sg).*tls'
|
||||
)
|
||||
)
|
||||
self.searchLineEdit.setMinimumWidth(280)
|
||||
self.searchLineEdit.setMaximumWidth(700)
|
||||
self.searchLineEdit.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
|
||||
self.searchButton = SearchButton()
|
||||
self.subscriptionFilterComboBox = AppQComboBox()
|
||||
self.subscriptionFilterComboBox.setMinimumWidth(180)
|
||||
self.subscriptionFilterComboBox.setMinimumWidth(190)
|
||||
self.subscriptionFilterComboBox.setMaximumWidth(300)
|
||||
self.subscriptionFilterComboBox.setSizePolicy(
|
||||
QSizePolicy.Policy.Preferred,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
|
||||
self.searchLayout.addWidget(self.searchLineEdit)
|
||||
self.searchLayout.addWidget(self.subscriptionFilterComboBox)
|
||||
self.searchLayout.addWidget(self.searchButton)
|
||||
self.headerLayout = QHBoxLayout()
|
||||
self.headerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.headerLayout.setSpacing(8)
|
||||
self.headerLayout.addWidget(self.pageTitleLabel)
|
||||
self.headerLayout.addStretch(1)
|
||||
self.headerLayout.addWidget(self.searchLineEdit, 4)
|
||||
self.headerLayout.addWidget(self.searchButton)
|
||||
|
||||
self._layout.addLayout(self.searchLayout)
|
||||
self._layout.addWidget(self.userServersQTableWidget)
|
||||
self.actionLayout = QHBoxLayout()
|
||||
self.actionLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.actionLayout.setSpacing(8)
|
||||
self.actionLayout.addWidget(self.connectButton)
|
||||
self.actionLayout.addWidget(self.serverButton)
|
||||
self.actionLayout.addStretch(1)
|
||||
self.actionLayout.addWidget(self.subscriptionFilterComboBox)
|
||||
|
||||
self._layout.addLayout(self.headerLayout)
|
||||
self._layout.addLayout(self.actionLayout)
|
||||
self._layout.addWidget(self.userServersQTableWidget, 1)
|
||||
|
||||
self.searchButton.clicked.connect(
|
||||
lambda: self.userServersQTableWidget.search(self.searchLineEdit.text())
|
||||
@@ -574,11 +574,42 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.userServersQTableWidget.subsManager.subscriptionsChanged.connect(
|
||||
self.refreshSubscriptionFilter
|
||||
)
|
||||
self.userServersQTableWidget.selectionModel().selectionChanged.connect(
|
||||
self.handleServerSelectionChanged
|
||||
)
|
||||
|
||||
self.refreshSubscriptionFilter()
|
||||
self.handleServerSelectionChanged()
|
||||
|
||||
self.setCentralWidget(self._widget)
|
||||
|
||||
@QtCore.Slot()
|
||||
def showServerMenu(self):
|
||||
"""Open server creation actions below the regular Server button."""
|
||||
position = self.serverButton.mapToGlobal(
|
||||
QtCore.QPoint(0, self.serverButton.height() + 2)
|
||||
)
|
||||
|
||||
self.serverMenu.popup(position)
|
||||
|
||||
@QtCore.Slot()
|
||||
def handleServerSelectionChanged(self, *_args):
|
||||
"""Apply Home's selection policy to the shared connection control."""
|
||||
self.connectButton.setSelectionCount(
|
||||
len(self.userServersQTableWidget.selectedIndex)
|
||||
)
|
||||
|
||||
def activateSelectedServerForConnection(self) -> bool:
|
||||
"""Activate exactly one selected server before requesting connection."""
|
||||
indexes = self.userServersQTableWidget.selectedIndex
|
||||
|
||||
if len(indexes) != 1:
|
||||
return False
|
||||
|
||||
self.userServersQTableWidget.activateSelectedServer()
|
||||
|
||||
return Storage.UserActivatedItemIndex() == indexes[0]
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def handleUserServersSearchTextChanged(self, text: str):
|
||||
"""Handle user servers search text changed."""
|
||||
|
||||
Reference in New Issue
Block a user