mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-12 17:28:35 +03:00
Align favorite icon hover colors
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user