zapret-kvn/xray_fluent/ui/adaptive_buttons.py
loop-uh 774fa0bd21
Some checks failed
Windows project source guards / test (push) Failing after 45s
Кнопки: подпись уходит в подсказку, когда ряду не хватает места
- AdaptiveButtonRow: следит за шириной ряда (с учётом переполнения
  прокручиваемой страницы) и сворачивает кнопки со значком с конца ряда в
  квадратные значки, текст — в подсказку; появится место — возвращает.
- fluent_fixes: у PushButton в режиме «только значок» значок рисуется по
  центру (оригинал сдвигал его под подпись — значок съезжал и обрезался).
- Применено: строка сервера на панели (вместо кнопок-близнецов), панель
  инструментов подписок, логов и конфигов; их кнопкам добавлены значки.
Проверено рендером на win10: на 860 px «Сбросить к шаблону» сворачивается
в значок, остальные ряды помещаются с подписями.
2026-09-26 18:25:16 +03:00

109 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Ряд кнопок, который сворачивает подписи в значки, когда не хватает места.
Кнопка со значком, которой нет места в ряду, показывает только значок, а
её текст уходит в подсказку; появится место — текст вернётся. Сворачиваются
с конца ряда (последние кнопки — наименее важные).
Ширину нельзя брать только у самого ряда: на прокручиваемой странице ряд не
бывает уже своего минимума — вместо этого появляется горизонтальная
прокрутка. Поэтому доступная ширина = ширина ряда минус переполнение
ближайшей области прокрутки.
"""
from __future__ import annotations
from collections.abc import Sequence
from PyQt6.QtCore import QEvent, QObject, QTimer
from PyQt6.QtWidgets import QAbstractScrollArea, QLayout, QPushButton, QScrollArea, QWidget
_MAX_WIDTH = 16777215 # QWIDGETSIZE_MAX
_HYSTERESIS = 8 # запас при разворачивании, чтобы ряд не «дрожал» на границе
def set_icon_only(button: QPushButton, icon_only: bool, full_text: str, tooltip: str, square: int) -> None:
"""Переключить кнопку между «значок + текст» и «только значок»."""
button._zk_icon_only = icon_only
if icon_only:
button.setText("")
button.setToolTip(tooltip or full_text)
button.setFixedWidth(square)
else:
button.setText(full_text)
button.setToolTip(tooltip)
button.setMinimumWidth(0)
button.setMaximumWidth(_MAX_WIDTH)
button.update()
class AdaptiveButtonRow(QObject):
def __init__(self, layout: QLayout, buttons: Sequence[QPushButton], *, square: int = 36):
host = layout.parentWidget()
super().__init__(host)
self._layout = layout
self._buttons = [b for b in buttons if not b.icon().isNull()]
self._square = square
self._full: dict[int, tuple[str, str]] = {}
self._widths: dict[int, int] = {}
self._collapsed: list[QPushButton] = []
self._pending = False
host.installEventFilter(self)
self._scroll = self._find_scroll_area(host)
if self._scroll is not None:
self._scroll.viewport().installEventFilter(self)
self.schedule()
@staticmethod
def _find_scroll_area(widget: QWidget | None) -> QAbstractScrollArea | None:
while widget is not None:
if isinstance(widget, QScrollArea):
return widget
widget = widget.parentWidget()
return None
def collapsed_buttons(self) -> list[QPushButton]:
return list(self._collapsed)
def eventFilter(self, obj, event) -> bool:
if event.type() in (QEvent.Type.Resize, QEvent.Type.Show):
self.schedule()
return False
def schedule(self) -> None:
if not self._pending:
self._pending = True
QTimer.singleShot(0, self.update_now)
def _overflow(self) -> int:
scroll = self._scroll
if scroll is None or scroll.widget() is None:
return 0
return max(0, scroll.widget().width() - scroll.viewport().width())
def update_now(self) -> None:
self._pending = False
available = self._layout.geometry().width() - self._overflow()
if available <= 0:
return
needed = self._layout.minimumSize().width()
for button in reversed(self._buttons):
if needed <= available:
break
if button in self._collapsed or button.isHidden():
continue
key = id(button)
self._full[key] = (button.text(), button.toolTip())
self._widths[key] = button.minimumSizeHint().width()
set_icon_only(button, True, button.text(), button.toolTip(), self._square)
self._collapsed.append(button)
needed -= max(0, self._widths[key] - self._square)
while self._collapsed:
button = self._collapsed[-1]
key = id(button)
gain = max(0, self._widths[key] - self._square)
if needed + gain > available - _HYSTERESIS:
break
text, tooltip = self._full[key]
set_icon_only(button, False, text, tooltip, self._square)
self._collapsed.pop()
needed += gain