mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-29 18:38:02 +03:00
Improve traffic status presentation
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -967,7 +967,7 @@ class AppStyleSheet:
|
||||
color: {palette['danger']};
|
||||
}}
|
||||
|
||||
QWidget#TrafficSpeedBadge {{
|
||||
QWidget#TrafficStatsBadge {{
|
||||
min-height: 24px;
|
||||
padding: 0 2px;
|
||||
border: 1px solid {palette['border']};
|
||||
@@ -976,13 +976,17 @@ class AppStyleSheet:
|
||||
color: {palette['text_strong']};
|
||||
}}
|
||||
|
||||
QWidget#TrafficSpeedBadge QLabel {{
|
||||
QWidget#TrafficStatsBadge QLabel {{
|
||||
border: none;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
color: {palette['text_strong']};
|
||||
}}
|
||||
|
||||
QWidget#TrafficStatsBadge QLabel#TrafficUsageLabel {{
|
||||
color: {palette['muted']};
|
||||
}}
|
||||
|
||||
QSplitter::handle {{
|
||||
background-color: {palette['border']};
|
||||
}}
|
||||
|
||||
@@ -29,7 +29,11 @@ import multiprocessing
|
||||
import queue
|
||||
import time
|
||||
|
||||
__all__ = ['TrafficStatsManager', 'formatTrafficSpeed']
|
||||
__all__ = [
|
||||
'TrafficStatsManager',
|
||||
'formatTrafficSpeed',
|
||||
'formatTrafficUsage',
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,6 +65,30 @@ def formatTrafficSpeed(bytesPerSecond: float) -> str:
|
||||
return f'{formatted} {unit}'
|
||||
|
||||
|
||||
def formatTrafficUsage(bytesUsed: int) -> str:
|
||||
"""Format cumulative traffic using compact binary units."""
|
||||
try:
|
||||
value = max(int(bytesUsed), 0)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
value = 0
|
||||
|
||||
if value < KIBIBYTE:
|
||||
return f'{value} B'
|
||||
|
||||
amount = float(value)
|
||||
units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB')
|
||||
|
||||
for unit in units:
|
||||
amount /= KIBIBYTE
|
||||
|
||||
if amount < KIBIBYTE or unit == units[-1]:
|
||||
formatted = f'{amount:.2f}'.rstrip('0').rstrip('.')
|
||||
|
||||
return f'{formatted} {unit}'
|
||||
|
||||
return f'{value} B'
|
||||
|
||||
|
||||
def _queryTrafficStats(requestQueue, resultQueue):
|
||||
"""Run potentially GIL-blocking statistics calls in an isolated process."""
|
||||
while True:
|
||||
@@ -87,9 +115,10 @@ class TrafficStatsManager(
|
||||
Mixins.CleanupOnExit,
|
||||
QtCore.QObject,
|
||||
):
|
||||
"""Periodically convert cumulative plugin counters into traffic speeds."""
|
||||
"""Publish plugin traffic usage and rates while its parent is visible."""
|
||||
|
||||
speedChanged = QtCore.Signal(float, float)
|
||||
usageChanged = QtCore.Signal(object, object)
|
||||
statisticsUnavailable = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -114,6 +143,71 @@ class TrafficStatsManager(
|
||||
self._resultTimer.setInterval(TRAFFIC_STATS_RESULT_INTERVAL)
|
||||
self._resultTimer.timeout.connect(self._consumeResults)
|
||||
|
||||
if parent is not None:
|
||||
parent.installEventFilter(self)
|
||||
|
||||
def _parentIsVisible(self) -> bool:
|
||||
"""Return whether the parent widget can currently display updates."""
|
||||
parent = self.parent()
|
||||
|
||||
if parent is None or not hasattr(parent, 'isVisible'):
|
||||
return True
|
||||
|
||||
return parent.isVisible() and not (
|
||||
hasattr(parent, 'isMinimized') and parent.isMinimized()
|
||||
)
|
||||
|
||||
def _suspendSampling(self):
|
||||
"""Pause polling and discard the current rate baseline."""
|
||||
wasActive = (
|
||||
self._sampleTimer.isActive()
|
||||
or self._resultTimer.isActive()
|
||||
or self._queryInFlight
|
||||
)
|
||||
self._sampleTimer.stop()
|
||||
self._resultTimer.stop()
|
||||
|
||||
if wasActive:
|
||||
self._generation += 1
|
||||
self._resetSamples()
|
||||
|
||||
def _resumeSampling(self):
|
||||
"""Resume polling when a monitor and visible parent are available."""
|
||||
if self._monitor is None or not self._parentIsVisible():
|
||||
return
|
||||
|
||||
if not self._ensureWorker():
|
||||
self.statisticsUnavailable.emit()
|
||||
|
||||
return
|
||||
|
||||
self._resultTimer.start()
|
||||
self._sampleTimer.start()
|
||||
self._requestSample()
|
||||
|
||||
@QtCore.Slot()
|
||||
def _syncSamplingVisibility(self):
|
||||
"""Synchronize polling with the parent widget's visibility."""
|
||||
if self._parentIsVisible():
|
||||
self._resumeSampling()
|
||||
else:
|
||||
self._suspendSampling()
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
"""Pause traffic queries while the parent widget is not visible."""
|
||||
if watched is self.parent():
|
||||
eventType = event.type()
|
||||
|
||||
if eventType == QtCore.QEvent.Type.Hide:
|
||||
self._suspendSampling()
|
||||
elif eventType in (
|
||||
QtCore.QEvent.Type.Show,
|
||||
QtCore.QEvent.Type.WindowStateChange,
|
||||
):
|
||||
QtCore.QTimer.singleShot(0, self._syncSamplingVisibility)
|
||||
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
@staticmethod
|
||||
def _activeProcesses():
|
||||
"""Return the processes owned by the active tray connection."""
|
||||
@@ -184,6 +278,8 @@ class TrafficStatsManager(
|
||||
|
||||
def _activateMonitor(self, monitor):
|
||||
"""Begin sampling one plugin-provided monitor."""
|
||||
self._sampleTimer.stop()
|
||||
self._resultTimer.stop()
|
||||
self._generation += 1
|
||||
self._monitor = monitor
|
||||
self._resetSamples()
|
||||
@@ -194,19 +290,12 @@ class TrafficStatsManager(
|
||||
|
||||
return
|
||||
|
||||
if not self._ensureWorker():
|
||||
self.statisticsUnavailable.emit()
|
||||
|
||||
return
|
||||
|
||||
self._resultTimer.start()
|
||||
self._sampleTimer.start()
|
||||
self._requestSample()
|
||||
self._resumeSampling()
|
||||
|
||||
@QtCore.Slot()
|
||||
def _requestSample(self):
|
||||
"""Queue one statistics query unless another query is still running."""
|
||||
if self._monitor is None or self._queryInFlight:
|
||||
if self._monitor is None or self._queryInFlight or not self._parentIsVisible():
|
||||
return
|
||||
|
||||
if not self._ensureWorker():
|
||||
@@ -227,6 +316,8 @@ class TrafficStatsManager(
|
||||
|
||||
def _updateSpeeds(self, counters: TrafficCounters, sampledAt: float):
|
||||
"""Calculate and emit rates from one cumulative counter sample."""
|
||||
self.usageChanged.emit(counters.uplink, counters.downlink)
|
||||
|
||||
previousCounters = self._previousCounters
|
||||
previousSampleTime = self._previousSampleTime
|
||||
self._previousCounters = counters
|
||||
@@ -252,6 +343,11 @@ class TrafficStatsManager(
|
||||
@QtCore.Slot()
|
||||
def _consumeResults(self):
|
||||
"""Drain worker results and ignore samples from older connections."""
|
||||
if not self._parentIsVisible():
|
||||
self._suspendSampling()
|
||||
|
||||
return
|
||||
|
||||
resultQueue = self._resultQueue
|
||||
|
||||
if resultQueue is None:
|
||||
|
||||
@@ -27,7 +27,11 @@ from .SubscriptionImporter import (
|
||||
SubscriptionImportService,
|
||||
SubscriptionSource,
|
||||
)
|
||||
from .TrafficStatsManager import TrafficStatsManager, formatTrafficSpeed
|
||||
from .TrafficStatsManager import (
|
||||
TrafficStatsManager,
|
||||
formatTrafficSpeed,
|
||||
formatTrafficUsage,
|
||||
)
|
||||
from .UpdateManager import UpdateManager
|
||||
|
||||
__all__ = [
|
||||
@@ -40,4 +44,5 @@ __all__ = [
|
||||
'TrafficStatsManager',
|
||||
'UpdateManager',
|
||||
'formatTrafficSpeed',
|
||||
'formatTrafficUsage',
|
||||
]
|
||||
|
||||
@@ -31,6 +31,7 @@ from Furious.Service import (
|
||||
TrafficStatsManager,
|
||||
UpdateManager,
|
||||
formatTrafficSpeed,
|
||||
formatTrafficUsage,
|
||||
)
|
||||
from Furious.Actions.Import import (
|
||||
ImportFromFileAction,
|
||||
@@ -258,33 +259,40 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
self.updateStatusText()
|
||||
|
||||
|
||||
class TrafficSpeedBadge(Mixins.ThemeAware, QWidget):
|
||||
"""Display independently updated upload and download speeds."""
|
||||
class TrafficStatsBadge(Mixins.ThemeAware, QWidget):
|
||||
"""Display independently updated traffic speeds and usage."""
|
||||
|
||||
UploadIconFileName = 'cloud-arrow-up.svg'
|
||||
DownloadIconFileName = 'cloud-arrow-down.svg'
|
||||
UploadIconFileName = 'cloud-upload.svg'
|
||||
DownloadIconFileName = 'cloud-download.svg'
|
||||
IconSize = QtCore.QSize(16, 16)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize dedicated traffic-direction icons and labels."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('TrafficSpeedBadge')
|
||||
self.setObjectName('TrafficStatsBadge')
|
||||
self.setVisible(False)
|
||||
|
||||
self.uploadIconLabel = QLabel(parent=self)
|
||||
self.uploadTextLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.uploadUsageLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.downloadIconLabel = QLabel(parent=self)
|
||||
self.downloadTextLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.downloadUsageLabel = AppQLabel(translatable=False, parent=self)
|
||||
|
||||
self.uploadUsageLabel.setObjectName('TrafficUsageLabel')
|
||||
self.downloadUsageLabel.setObjectName('TrafficUsageLabel')
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(8, 3, 8, 3)
|
||||
self._layout.setSpacing(6)
|
||||
self._layout.addWidget(self.uploadIconLabel)
|
||||
self._layout.addWidget(self.uploadTextLabel)
|
||||
self._layout.addWidget(self.uploadUsageLabel)
|
||||
self._layout.addSpacing(4)
|
||||
self._layout.addWidget(self.downloadIconLabel)
|
||||
self._layout.addWidget(self.downloadTextLabel)
|
||||
self._layout.addWidget(self.downloadUsageLabel)
|
||||
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
@@ -307,11 +315,20 @@ class TrafficSpeedBadge(Mixins.ThemeAware, QWidget):
|
||||
self.downloadTextLabel.setText(formatTrafficSpeed(download))
|
||||
self.setVisible(True)
|
||||
|
||||
@QtCore.Slot(object, object)
|
||||
def setUsage(self, upload: int, download: int):
|
||||
"""Display formatted cumulative upload and download usage."""
|
||||
self.uploadUsageLabel.setText(f'({formatTrafficUsage(upload)})')
|
||||
self.downloadUsageLabel.setText(f'({formatTrafficUsage(download)})')
|
||||
self.setVisible(True)
|
||||
|
||||
@QtCore.Slot()
|
||||
def clearSpeeds(self):
|
||||
"""Hide stale speeds when statistics are unavailable."""
|
||||
def clearStatistics(self):
|
||||
"""Hide stale speed and usage values when statistics are unavailable."""
|
||||
self.uploadTextLabel.clear()
|
||||
self.uploadUsageLabel.clear()
|
||||
self.downloadTextLabel.clear()
|
||||
self.downloadUsageLabel.clear()
|
||||
self.setVisible(False)
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
@@ -328,13 +345,13 @@ class ConnectionStatusWidget(QWidget):
|
||||
|
||||
self.setObjectName('ConnectionStatusWidget')
|
||||
self.networkState = NetworkStateBadge(parent=self)
|
||||
self.trafficSpeed = TrafficSpeedBadge(parent=self)
|
||||
self.trafficStats = TrafficStatsBadge(parent=self)
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSpacing(6)
|
||||
self._layout.addWidget(self.trafficStats)
|
||||
self._layout.addWidget(self.networkState)
|
||||
self._layout.addWidget(self.trafficSpeed)
|
||||
|
||||
|
||||
class SearchButton(AppQPushButton):
|
||||
@@ -812,11 +829,12 @@ class MainWindow(AppQMainWindow):
|
||||
|
||||
self.connectionStatus = ConnectionStatusWidget(parent=self)
|
||||
self.networkState = self.connectionStatus.networkState
|
||||
self.trafficSpeed = self.connectionStatus.trafficSpeed
|
||||
self.trafficStats = self.connectionStatus.trafficStats
|
||||
self.trafficStatsManager = TrafficStatsManager(parent=self)
|
||||
self.trafficStatsManager.speedChanged.connect(self.trafficSpeed.setSpeeds)
|
||||
self.trafficStatsManager.speedChanged.connect(self.trafficStats.setSpeeds)
|
||||
self.trafficStatsManager.usageChanged.connect(self.trafficStats.setUsage)
|
||||
self.trafficStatsManager.statisticsUnavailable.connect(
|
||||
self.trafficSpeed.clearSpeeds
|
||||
self.trafficStats.clearStatistics
|
||||
)
|
||||
|
||||
self.statusBar().addPermanentWidget(self.connectionStatus)
|
||||
|
||||
Reference in New Issue
Block a user