mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-07 14:28:15 +03:00
Modernize Fluent item views
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
# 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 widgets for Xray asset viewer Qt list widget."""
|
||||
"""Provide widgets for the Xray asset viewer list."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -59,17 +59,21 @@ class MBoxAssetExists(AppQMessageBox):
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
"""Provide the Xray asset viewer Qt list widget."""
|
||||
class XrayAssetListWidget(Mixins.ThemeAware, AppQListView):
|
||||
"""Provide the model-based Xray asset viewer list."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the Xray asset list widget."""
|
||||
"""Initialize the Xray asset list view."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.assetModel = QStandardItemModel(parent=self)
|
||||
self.setModel(self.assetModel)
|
||||
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.setSelectionBehavior(AppQListWidget.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListWidget.SelectionMode.ExtendedSelection)
|
||||
self.setSelectionBehavior(AppQListView.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListView.SelectionMode.ExtendedSelection)
|
||||
self.setEditTriggers(AppQListView.EditTrigger.NoEditTriggers)
|
||||
self.setIconSize(QtCore.QSize(64, 64))
|
||||
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
@@ -96,6 +100,12 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Recalculate item geometry after the view receives its display style."""
|
||||
super().showEvent(event)
|
||||
|
||||
self.scheduleDelayedItemsLayout()
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
"""Handle custom context menu requested."""
|
||||
@@ -103,7 +113,7 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
|
||||
def flushItemByTheme(self, theme: str):
|
||||
"""Refresh item by theme."""
|
||||
self.clear()
|
||||
self.assetModel.clear()
|
||||
|
||||
maxlen = max(
|
||||
len(filename)
|
||||
@@ -125,7 +135,8 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
item = QListWidgetItem(f'{filename:{maxlen + 6}}{mdate}')
|
||||
item = QStandardItem(f'{filename:{maxlen + 6}}{mdate}')
|
||||
item.setData(filename, QtCore.Qt.ItemDataRole.UserRole)
|
||||
item.setFont(QFont(AppFontName()))
|
||||
|
||||
if APP().usesForcedDarkTheme():
|
||||
@@ -149,7 +160,16 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
else:
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
|
||||
self.addItem(item)
|
||||
self.assetModel.appendRow(item)
|
||||
|
||||
def filenameAt(self, row: int) -> str:
|
||||
"""Return the filesystem name represented by one model row."""
|
||||
item = self.assetModel.item(row)
|
||||
|
||||
if item is None:
|
||||
return ''
|
||||
|
||||
return str(item.data(QtCore.Qt.ItemDataRole.UserRole) or '')
|
||||
|
||||
def flushItem(self):
|
||||
"""Refresh item."""
|
||||
@@ -224,13 +244,19 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
# Nothing selected
|
||||
return
|
||||
|
||||
def handleResultCode(_indexes, code):
|
||||
filenames = [self.filenameAt(index) for index in indexes]
|
||||
filenames = [filename for filename in filenames if filename]
|
||||
|
||||
if not filenames:
|
||||
return
|
||||
|
||||
def handleResultCode(_filenames, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
for index in _indexes:
|
||||
os.remove(XRAY_ASSET_DIR / self.item(index).text())
|
||||
for filename in _filenames:
|
||||
os.remove(XRAY_ASSET_DIR / filename)
|
||||
|
||||
self.flushItem()
|
||||
else:
|
||||
@@ -247,23 +273,23 @@ class XrayAssetListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
mbox.isMulti = bool(len(indexes) > 1)
|
||||
mbox.possibleRemark = f'{self.item(indexes[0]).text()}'
|
||||
mbox.isMulti = bool(len(filenames) > 1)
|
||||
mbox.possibleRemark = filenames[0]
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
mbox.finished.connect(functools.partial(handleResultCode, filenames))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Handle a key press for the Xray asset viewer Qt list widget."""
|
||||
"""Handle a key press for the Xray asset viewer list."""
|
||||
if event.key() == QtCore.Qt.Key.Key_Delete:
|
||||
self.deleteSelectedItem()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def themeChangedCallback(self, theme):
|
||||
"""Update the Xray asset viewer Qt list widget for a theme change."""
|
||||
"""Update the Xray asset viewer list for a theme change."""
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
# Ubuntu 20.04 system dark theme does not
|
||||
# change menu color. Do nothing
|
||||
|
||||
@@ -667,31 +667,34 @@ class RoutingProfileEditDialog(AppQTransientDialog):
|
||||
}
|
||||
|
||||
|
||||
class RoutingRulesQListWidget(AppQListWidget):
|
||||
"""Provide the routing rules Qt list widget."""
|
||||
class RoutingRulesListView(AppQListView):
|
||||
"""Provide the model-based routing rules list."""
|
||||
|
||||
editRequested = QtCore.Signal()
|
||||
|
||||
def __init__(self, routing: dict, parent=None):
|
||||
"""Initialize the RoutingRulesQListWidget."""
|
||||
"""Initialize the routing rules list view."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.routing = routing
|
||||
self.rulesModel = QtCore.QStringListModel(parent=self)
|
||||
self.setModel(self.rulesModel)
|
||||
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(AppQListWidget.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListWidget.SelectionMode.ExtendedSelection)
|
||||
self.itemDoubleClicked.connect(self._requestEdit)
|
||||
self.setSelectionBehavior(AppQListView.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListView.SelectionMode.ExtendedSelection)
|
||||
self.setEditTriggers(AppQListView.EditTrigger.NoEditTriggers)
|
||||
self.doubleClicked.connect(self._requestEdit)
|
||||
|
||||
self.flushAll()
|
||||
|
||||
@QtCore.Slot(QListWidgetItem)
|
||||
def _requestEdit(self, _item):
|
||||
@QtCore.Slot(QtCore.QModelIndex)
|
||||
def _requestEdit(self, _index):
|
||||
"""Forward a row double-click without retaining a Python closure."""
|
||||
self.editRequested.emit()
|
||||
|
||||
def rules(self):
|
||||
"""Return the rules value used by the routing rules Qt list widget."""
|
||||
"""Return the rules represented by the routing list."""
|
||||
rules = self.routing.setdefault('rules', list())
|
||||
|
||||
if not isinstance(rules, list):
|
||||
@@ -700,11 +703,11 @@ class RoutingRulesQListWidget(AppQListWidget):
|
||||
return rules
|
||||
|
||||
def ruleAt(self, index: int):
|
||||
"""Return the rule at value used by the routing rules Qt list widget."""
|
||||
"""Return the rule represented by one list row."""
|
||||
return self.rules()[index]
|
||||
|
||||
def ruleText(self, rule: dict) -> str:
|
||||
"""Return the rule text value used by the routing rules Qt list widget."""
|
||||
"""Return the display text for one routing rule."""
|
||||
name, outbound, domains, ips = (
|
||||
rule.get('ruleTag', '') or 'Untitled Rule',
|
||||
rule.get('outboundTag', 'proxy'),
|
||||
@@ -742,10 +745,7 @@ class RoutingRulesQListWidget(AppQListWidget):
|
||||
|
||||
def flushAll(self):
|
||||
"""Refresh all."""
|
||||
self.clear()
|
||||
|
||||
for rule in self.rules():
|
||||
self.addItem(self.ruleText(rule))
|
||||
self.rulesModel.setStringList([self.ruleText(rule) for rule in self.rules()])
|
||||
|
||||
|
||||
class RoutingRulesDialog(AppQTransientDialog):
|
||||
@@ -759,8 +759,8 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
self.setWindowTitle(_('Routing Rules'))
|
||||
self.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
self.listWidget = RoutingRulesQListWidget(self.routing, parent=self)
|
||||
self.listWidget.editRequested.connect(self.editRule)
|
||||
self.listView = RoutingRulesListView(self.routing, parent=self)
|
||||
self.listView.editRequested.connect(self.editRule)
|
||||
|
||||
self.addButton = AppQPushButton(
|
||||
_('Add'),
|
||||
@@ -800,7 +800,7 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
layout.setContentsMargins(20, 18, 20, 20)
|
||||
layout.setSpacing(14)
|
||||
layout.addLayout(actionLayout)
|
||||
layout.addWidget(self.listWidget)
|
||||
layout.addWidget(self.listView)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -819,32 +819,32 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
def handleResultCode(code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
|
||||
self.listWidget.appendRule(dialog.routingRule())
|
||||
self.listView.appendRule(dialog.routingRule())
|
||||
|
||||
dialog.finished.connect(handleResultCode)
|
||||
dialog.open()
|
||||
|
||||
def editRule(self):
|
||||
"""Handle edit rule for the routing rules dialog."""
|
||||
indexes = self.listWidget.selectedIndex
|
||||
indexes = self.listView.selectedIndex
|
||||
|
||||
if len(indexes) != 1:
|
||||
return
|
||||
|
||||
index = indexes[0]
|
||||
dialog = RoutingRuleEditDialog(self.listWidget.ruleAt(index), parent=self)
|
||||
dialog = RoutingRuleEditDialog(self.listView.ruleAt(index), parent=self)
|
||||
|
||||
def handleResultCode(_index, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
|
||||
self.listWidget.setRule(_index, dialog.routingRule())
|
||||
self.listView.setRule(_index, dialog.routingRule())
|
||||
|
||||
dialog.finished.connect(functools.partial(handleResultCode, index))
|
||||
dialog.open()
|
||||
|
||||
def deleteRule(self):
|
||||
"""Delete rule."""
|
||||
indexes = self.listWidget.selectedIndex
|
||||
indexes = self.listView.selectedIndex
|
||||
|
||||
if len(indexes) == 0:
|
||||
return
|
||||
@@ -854,7 +854,7 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
self.listWidget.deleteRules(_indexes)
|
||||
self.listView.deleteRules(_indexes)
|
||||
else:
|
||||
# Do not delete
|
||||
pass
|
||||
@@ -871,7 +871,7 @@ class RoutingRulesDialog(AppQTransientDialog):
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
mbox.isMulti = bool(len(indexes) > 1)
|
||||
mbox.possibleRemark = self.listWidget.selectedRuleText()
|
||||
mbox.possibleRemark = self.listView.selectedRuleText()
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
|
||||
|
||||
+92
-4
@@ -46,7 +46,7 @@ __all__ = [
|
||||
'AppQHeaderView',
|
||||
'AppQLabel',
|
||||
'AppQLineEdit',
|
||||
'AppQListWidget',
|
||||
'AppQListView',
|
||||
'AppQMainWindow',
|
||||
'AppQMenu',
|
||||
'AppQMenuBar',
|
||||
@@ -559,13 +559,99 @@ class AppQLineEdit(Mixins.QTranslatable, QLineEdit):
|
||||
self.setPlaceholderText(_(self.placeholderText()))
|
||||
|
||||
|
||||
class AppQListWidget(QListWidget):
|
||||
"""Provide the app Qt list widget."""
|
||||
class _AppQItemViewSelectionDelegate(QStyledItemDelegate):
|
||||
"""Paint a Fluent-style rounded item selection."""
|
||||
|
||||
HorizontalInset = 4
|
||||
VerticalInset = 2
|
||||
Radius = 5
|
||||
|
||||
def _selectionRect(self, option):
|
||||
"""Return the rounded selection rectangle for one list item."""
|
||||
return QtCore.QRectF(option.rect).adjusted(
|
||||
self.HorizontalInset,
|
||||
self.VerticalInset,
|
||||
-self.HorizontalInset,
|
||||
-self.VerticalInset,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _selectionColor(option):
|
||||
"""Return the active or inactive theme-aware selection color."""
|
||||
colorName = (
|
||||
'selection' if option.state & QStyle.StateFlag.State_Active else 'raised'
|
||||
)
|
||||
|
||||
try:
|
||||
return QColor(AppStyleSheet.paletteForTheme(APP().theme())[colorName])
|
||||
except (AttributeError, RuntimeError):
|
||||
return option.palette.color(QPalette.ColorRole.Highlight)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
"""Paint the rounded selection before native item contents."""
|
||||
if option.state & QStyle.StateFlag.State_Selected:
|
||||
selectionRect = self._selectionRect(option)
|
||||
|
||||
if not selectionRect.isEmpty():
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.setClipRect(option.rect)
|
||||
painter.setPen(QtCore.Qt.PenStyle.NoPen)
|
||||
painter.setBrush(self._selectionColor(option))
|
||||
painter.drawRoundedRect(selectionRect, self.Radius, self.Radius)
|
||||
painter.restore()
|
||||
|
||||
super().paint(painter, option, index)
|
||||
|
||||
|
||||
class _AppQTableSelectionDelegate(_AppQItemViewSelectionDelegate):
|
||||
"""Paint one rounded background across a selected table row."""
|
||||
|
||||
def _selectionRect(self, option):
|
||||
"""Return the full visible-column span for the option's row."""
|
||||
table = self.parent()
|
||||
header = table.horizontalHeader()
|
||||
sections = [
|
||||
logical
|
||||
for logical in range(header.count())
|
||||
if not header.isSectionHidden(logical)
|
||||
]
|
||||
|
||||
if not sections:
|
||||
return QtCore.QRectF()
|
||||
|
||||
left, right = (
|
||||
min(header.sectionViewportPosition(logical) for logical in sections),
|
||||
max(
|
||||
header.sectionViewportPosition(logical) + header.sectionSize(logical)
|
||||
for logical in sections
|
||||
),
|
||||
)
|
||||
|
||||
return QtCore.QRectF(
|
||||
left + self.HorizontalInset,
|
||||
option.rect.top() + self.VerticalInset,
|
||||
max(0, right - left - (2 * self.HorizontalInset)),
|
||||
max(0, option.rect.height() - (2 * self.VerticalInset)),
|
||||
)
|
||||
|
||||
|
||||
def _installRoundedSelectionDelegate(view, delegateType):
|
||||
"""Install one persistent, view-owned rounded selection delegate."""
|
||||
view.setProperty('selectionShape', 'rounded')
|
||||
view.selectionDelegate = delegateType(parent=view)
|
||||
view.setItemDelegate(view.selectionDelegate)
|
||||
|
||||
|
||||
class AppQListView(QListView):
|
||||
"""Provide a model-based app Qt list view."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the AppQListWidget."""
|
||||
"""Initialize the AppQListView."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
_installRoundedSelectionDelegate(self, _AppQItemViewSelectionDelegate)
|
||||
|
||||
@property
|
||||
def selectedIndex(self):
|
||||
"""Return the selected index value."""
|
||||
@@ -1941,6 +2027,8 @@ class AppQTableView(QTableView):
|
||||
"""Initialize the AppQTableView."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
_installRoundedSelectionDelegate(self, _AppQTableSelectionDelegate)
|
||||
|
||||
self.setWordWrap(False)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
|
||||
QTableView,
|
||||
QTableWidget,
|
||||
QTreeView,
|
||||
QListView,
|
||||
QListWidget {{
|
||||
QListView {{
|
||||
border: 1px solid {palette['border']};
|
||||
border-radius: 8px;
|
||||
background-color: {palette['panel']};
|
||||
@@ -41,8 +40,7 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
|
||||
QTableView::item,
|
||||
QTableWidget::item,
|
||||
QTreeView::item,
|
||||
QListView::item,
|
||||
QListWidget::item {{
|
||||
QListView::item {{
|
||||
min-height: 24px;
|
||||
padding: 4px 7px;
|
||||
border: none;
|
||||
@@ -51,8 +49,7 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
|
||||
QTableView::item:hover,
|
||||
QTableWidget::item:hover,
|
||||
QTreeView::item:hover,
|
||||
QListView::item:hover,
|
||||
QListWidget::item:hover {{
|
||||
QListView::item:hover {{
|
||||
background-color: {palette['hover']};
|
||||
color: {palette['text_strong']};
|
||||
}}
|
||||
@@ -60,8 +57,7 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
|
||||
QTableView::item:selected,
|
||||
QTableWidget::item:selected,
|
||||
QTreeView::item:selected,
|
||||
QListView::item:selected,
|
||||
QListWidget::item:selected {{
|
||||
QListView::item:selected {{
|
||||
background-color: {palette['selection']};
|
||||
color: {palette['selection_text']};
|
||||
}}
|
||||
@@ -69,12 +65,23 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon):
|
||||
QTableView::item:selected:!active,
|
||||
QTableWidget::item:selected:!active,
|
||||
QTreeView::item:selected:!active,
|
||||
QListView::item:selected:!active,
|
||||
QListWidget::item:selected:!active {{
|
||||
QListView::item:selected:!active {{
|
||||
background-color: {palette['raised']};
|
||||
color: {palette['text']};
|
||||
}}
|
||||
|
||||
QTableView[selectionShape="rounded"],
|
||||
QListView[selectionShape="rounded"] {{
|
||||
selection-background-color: transparent;
|
||||
}}
|
||||
|
||||
QTableView[selectionShape="rounded"]::item:selected,
|
||||
QTableView[selectionShape="rounded"]::item:selected:!active,
|
||||
QListView[selectionShape="rounded"]::item:selected,
|
||||
QListView[selectionShape="rounded"]::item:selected:!active {{
|
||||
background-color: transparent;
|
||||
}}
|
||||
|
||||
QHeaderView {{
|
||||
border: none;
|
||||
background-color: {palette['panel_alt']};
|
||||
|
||||
@@ -61,7 +61,7 @@ from .QtWidgets import (
|
||||
AppQIconTextPushButton,
|
||||
AppQLabel,
|
||||
AppQLineEdit,
|
||||
AppQListWidget,
|
||||
AppQListView,
|
||||
AppQMainWindow,
|
||||
AppQMenu,
|
||||
AppQMenuBar,
|
||||
@@ -114,7 +114,7 @@ __all__ = [
|
||||
'AppQIcon',
|
||||
'AppQLabel',
|
||||
'AppQLineEdit',
|
||||
'AppQListWidget',
|
||||
'AppQListView',
|
||||
'AppQMainWindow',
|
||||
'AppQMenu',
|
||||
'AppQMenuBar',
|
||||
|
||||
Reference in New Issue
Block a user