Align favorite icon hover colors

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-10-11 10:34:47 +08:00
parent 55a7aa7adc
commit 613bb8a83f
6 changed files with 177 additions and 2 deletions
+3 -1
View File
@@ -2397,11 +2397,13 @@ class AppQSpinBox(QSpinBox):
class AppQTableView(QTableView):
"""Represent app Qt table view."""
SelectionDelegateType = _AppQTableSelectionDelegate
def __init__(self, *args, **kwargs):
"""Initialize the AppQTableView."""
super().__init__(*args, **kwargs)
_installRoundedSelectionDelegate(self, _AppQTableSelectionDelegate)
_installRoundedSelectionDelegate(self, self.SelectionDelegateType)
self.setWordWrap(False)
self.setAlternatingRowColors(True)
+4
View File
@@ -63,6 +63,10 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
color: {palette['text_strong']};
}}
ServerTableView {{
qproperty-hoverTextColor: {palette['text_strong']};
}}
QTableView::item:selected,
QTableWidget::item:selected,
QTreeView::item:selected,
+3
View File
@@ -62,6 +62,9 @@ source tree's root.
Favorite SVG decoration follows the remark's foreground and selected-text roles, including privilege-dependent
connection colors. Use the shared color authority and view palette rather than a dark/light test; keep selected
icon and text behavior aligned. The selected-rendering cases in `tests/test_qt_interactions.py` are the pixel anchor.
The server table's view-owned selection delegate also applies the stylesheet's hover text token to favorite
decoration while leaving selected painting to Qt. The color is a Qt stylesheet property, not a separate theme
decision or hover-state registry. Mouse-hover rendering cases cover color restoration and delegate destruction.
- Endpoint lookup belongs to `EndpointInfoService`; the map renders validated results and has a no-WebEngine
fallback. Optional WebEngine import failure must not prevent importing the widget/package, and hidden presentation
must not retarget a queued lookup.
+32
View File
@@ -31,6 +31,7 @@ from Furious.Plugins import (
)
from Furious.Qt import *
from Furious.Qt.Signals import connectWeakly, singleShotWeakly
from Furious.Qt.QtWidgets import _AppQTableSelectionDelegate
from Furious.Qt import gettext as _
from Furious.Service import (
ProfileTestField,
@@ -495,6 +496,24 @@ def _favoriteIconWithColor(colorName, selectedColorName):
return icon
class _ServerTableSelectionDelegate(_AppQTableSelectionDelegate):
"""Keep favorite decorations aligned with native hover text painting."""
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if (
option.state & QStyle.StateFlag.State_MouseOver
and not option.state & QStyle.StateFlag.State_Selected
and not option.icon.isNull()
):
color = self.parent().hoverTextColor
if color.isValid():
name = color.name(QColor.NameFormat.HexArgb)
option.icon = _favoriteIconWithColor(name, name)
class UserServersTableModel(QtCore.QAbstractTableModel):
"""Expose user servers table data through a Qt item model."""
@@ -928,6 +947,8 @@ class ServerTableView(
activeServerChanged = QtCore.Signal()
SelectionDelegateType = _ServerTableSelectionDelegate
RowHeight = 42
Headers = [
@@ -942,8 +963,19 @@ class ServerTableView(
ServerTableColumn('Speed'),
]
@QtCore.Property(QColor)
def hoverTextColor(self):
"""Read the hover text token supplied by the application stylesheet."""
return self._hoverTextColor
@hoverTextColor.setter
def hoverTextColor(self, color):
self._hoverTextColor = QColor(color)
def __init__(self, *args, **kwargs):
"""Initialize the server table view."""
self._hoverTextColor = QColor()
configurationEditorFactory, self.qrCodeWindowFactory, importActionsFactory = (
kwargs.pop('configurationEditorFactory'),
kwargs.pop('qrCodeWindowFactory'),
+1 -1
View File
@@ -141,7 +141,7 @@ retains the worker/script until helper completion, before cleanup admits another
| [test_dialog_geometry.py](test_dialog_geometry.py) | Dialog show/open/exec preparation, sizing/centering, failure cleanup, specialized message-box geometry and transient destruction. |
| [test_layout_matrix.py](test_layout_matrix.py) | Navigation and message-box layouts in fresh processes at scale factors 1, 1.25, 1.5, and 2, under both themes. |
| [test_ui_behavior.py](test_ui_behavior.py) | Translation and editor mappings, settings construction visibility/organization/search/platform gates, stable server/routing moves, real QR rendering/decoding, incremental log filtering/tail behavior, connected-routing change notices, message boxes and connection-error copy/log navigation, shared Home/tray state, preserved message payloads and managed-versus-plain button retranslation. |
| [test_qt_interactions.py](test_qt_interactions.py) | Real keyboard/mouse/focus and proxy mapping, scoped shortcuts, sorting/selection, stable subscription deletion confirmations, debounced Home search, favorite actions and intersecting filters, Tests-menu selection color, shared settings, batched import/delete/duplication, cancellation, preparation failure, and owner destruction. |
| [test_qt_interactions.py](test_qt_interactions.py) | Real keyboard/mouse/focus and proxy mapping, scoped shortcuts, sorting/selection, stable subscription deletion confirmations, debounced Home search, favorite actions and intersecting filters, selected/hovered favorite SVG and remark colors across themes and privilege states, Tests-menu selection color, shared settings, batched import/delete/duplication, cancellation, preparation failure, and owner destruction. |
| [test_qr_export_scalability.py](test_qr_export_scalability.py) | Production capture cap, immediate single export, incremental yielding, failure/cancel/close paths, immutable snapshots, window-owned state destruction. |
| [test_stylesheet_states.py](test_stylesheet_states.py) | Targeted rendering/alpha/geometry assertions for table/list insets, popup corners, clear buttons, focus/disabled states, and stylesheet composition. |
| [test_theme_transition.py](test_theme_transition.py) | Real cross-fades, immediate theme activation, interruption, per-window resize/destruction, coordinator teardown, animation policy, native resize/deletion probes, deferred completion delivery, replacement/stop flushing and owner-first cancellation. |
+134
View File
@@ -2231,6 +2231,7 @@ class ProfileMutationBatchTest(unittest.TestCase):
index = table.proxyIndexFromSourceIndex(remark)
option = QStyleOptionViewItem()
option.initFrom(table)
option.state &= ~QStyle.StateFlag.State_MouseOver
table.itemDelegate().initStyleOption(option, index)
self.assertEqual(option.text, profiles[0].itemRemark)
@@ -2310,6 +2311,7 @@ class ProfileMutationBatchTest(unittest.TestCase):
option = QStyleOptionViewItem()
option.initFrom(table)
option.state &= ~QStyle.StateFlag.State_MouseOver
table.itemDelegate().initStyleOption(option, index)
self.assertEqual(
@@ -2367,6 +2369,125 @@ class ProfileMutationBatchTest(unittest.TestCase):
QtGui.QColor(AppHue.currentColor()),
)
def testHoveredFavoriteIconMatchesRenderedRemarkAndRestoresProfileColor(self):
"""Real mouse hover keeps the SVG and text aligned in either theme."""
with self.table(2) as (table, controller):
for profile in Storage.UserServers():
profile.metadata.favorite = True
table.resize(900, 220)
table.show()
delegate = table.itemDelegate()
self.assertIs(delegate, table.selectionDelegate)
self.assertIs(delegate.parent(), table)
with (
mock.patch(
'Furious.Qt.DynamicTheme.AppConnectionController',
return_value=controller,
),
mock.patch('Furious.Qt.DynamicTheme.SystemRuntime.isAdmin') as isAdmin,
):
for theme in (AppStyleSheet.Light, AppStyleSheet.Dark):
table.setStyleSheet(AppStyleSheet.forTheme(theme))
for connected, admin in (
(False, False),
(True, False),
(True, True),
):
controller.isConnected.return_value = connected
isAdmin.return_value = admin
table.activateItemByIndex(0, True)
table.sourceModel.emitAllChanged()
table.clearSelection()
for row in (0, 1):
index = table.proxyIndexFromSourceRow(row)
cellRect = table.visualRect(index)
for hovered in (True, False):
with self.subTest(
theme=theme,
connected=connected,
admin=admin,
row=row,
hovered=hovered,
):
QTest.mouseMove(
table.viewport(),
(
cellRect.center()
if hovered
else QtCore.QPoint(
5, table.viewport().height() - 5
)
),
)
processQtEvents()
image = table.viewport().grab(cellRect).toImage()
color = QtGui.QColor(
AppStyleSheet.Palettes[theme]['text_strong']
if hovered
else (
AppHue.currentColor()
if row == 0
else table.palette().color(
QtGui.QPalette.ColorRole.Text
)
)
)
option = QStyleOptionViewItem()
option.initFrom(table)
option.widget = table
option.rect = cellRect
option.decorationSize = table.iconSize()
table.itemDelegate().initStyleOption(option, index)
for element in (
QStyle.SubElement.SE_ItemViewItemDecoration,
QStyle.SubElement.SE_ItemViewItemText,
):
region = (
table.style()
.subElementRect(element, option, table)
.translated(-cellRect.topLeft())
)
ratio = image.devicePixelRatio()
pixels = [
image.pixelColor(x, y)
for x in range(
round(region.left() * ratio),
round((region.right() + 1) * ratio),
)
for y in range(
round(region.top() * ratio),
round((region.bottom() + 1) * ratio),
)
]
self.assertTrue(pixels)
closest = min(
max(
abs(pixel.red() - color.red()),
abs(pixel.green() - color.green()),
abs(pixel.blue() - color.blue()),
)
for pixel in pixels
)
self.assertLessEqual(closest, 12, element.name)
self.assertFalse(isValid(delegate))
def testSelectedFavoriteIconMatchesRenderedRemarkAndRestoresProfileColor(self):
with self.table(2) as (table, controller):
for profile in Storage.UserServers():
@@ -2431,6 +2552,19 @@ class ProfileMutationBatchTest(unittest.TestCase):
if selected:
table.selectAll()
QTest.mouseMove(
table.viewport(),
(
table.visualRect(
table.proxyIndexFromSourceRow(0)
).center()
if selected
else QtCore.QPoint(
5, table.viewport().height() - 5
)
),
)
processQtEvents()
backgrounds = {}