initial fluent-widgets ui

This commit is contained in:
2025-08-14 18:45:16 +08:00
parent 746e83ab23
commit 4c66886257
1198 changed files with 805339 additions and 0 deletions

View File

@ -0,0 +1,551 @@
# coding: utf-8
from typing import List, Union
from PySide6.QtCore import QSize, Qt, QRectF, Signal, QPoint, QTimer, QEvent, QAbstractItemModel, Property, QModelIndex
from PySide6.QtGui import QPainter, QPainterPath, QIcon, QColor, QAction
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLineEdit, QToolButton, QTextEdit,
QPlainTextEdit, QCompleter, QStyle, QWidget, QTextBrowser)
from ...common.style_sheet import FluentStyleSheet, themeColor
from ...common.icon import isDarkTheme, FluentIconBase, drawIcon
from ...common.icon import FluentIcon as FIF
from ...common.font import setFont
from ...common.color import FluentSystemColor, autoFallbackThemeColor
from .tool_tip import ToolTipFilter
from .menu import LineEditMenu, TextEditMenu, RoundMenu, MenuAnimationType, IndicatorMenuItemDelegate
from .scroll_bar import SmoothScrollDelegate
class LineEditButton(QToolButton):
""" Line edit button """
def __init__(self, icon: Union[str, QIcon, FluentIconBase], parent=None):
super().__init__(parent=parent)
self._icon = icon
self._action = None
self.isPressed = False
self.setFixedSize(31, 23)
self.setIconSize(QSize(10, 10))
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setObjectName('lineEditButton')
FluentStyleSheet.LINE_EDIT.apply(self)
def setAction(self, action: QAction):
self._action = action
self._onActionChanged()
self.clicked.connect(action.trigger)
action.toggled.connect(self.setChecked)
action.changed.connect(self._onActionChanged)
self.installEventFilter(ToolTipFilter(self, 700))
def _onActionChanged(self):
action = self.action()
self.setIcon(action.icon())
self.setToolTip(action.toolTip())
self.setEnabled(action.isEnabled())
self.setCheckable(action.isCheckable())
self.setChecked(action.isChecked())
def action(self):
return self._action
def setIcon(self, icon: Union[str, FluentIconBase, QIcon]):
self._icon = icon
self.update()
def mousePressEvent(self, e):
self.isPressed = True
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):
self.isPressed = False
super().mouseReleaseEvent(e)
def paintEvent(self, e):
super().paintEvent(e)
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.SmoothPixmapTransform)
iw, ih = self.iconSize().width(), self.iconSize().height()
w, h = self.width(), self.height()
rect = QRectF((w - iw)/2, (h - ih)/2, iw, ih)
if self.isPressed:
painter.setOpacity(0.7)
if isDarkTheme():
drawIcon(self._icon, painter, rect)
else:
drawIcon(self._icon, painter, rect, fill='#656565')
class LineEdit(QLineEdit):
""" Line edit """
def __init__(self, parent=None):
super().__init__(parent=parent)
self._isClearButtonEnabled = False
self._completer = None # type: QCompleter
self._completerMenu = None # type: CompleterMenu
self._isError = False
self.lightFocusedBorderColor = QColor()
self.darkFocusedBorderColor = QColor()
self.leftButtons = [] # type: List[LineEditButton]
self.rightButtons = [] # type: List[LineEditButton]
self.setProperty("transparent", True)
FluentStyleSheet.LINE_EDIT.apply(self)
self.setFixedHeight(33)
self.setAttribute(Qt.WA_MacShowFocusRect, False)
setFont(self)
self.hBoxLayout = QHBoxLayout(self)
self.clearButton = LineEditButton(FIF.CLOSE, self)
self.clearButton.setFixedSize(29, 25)
self.clearButton.hide()
self.hBoxLayout.setSpacing(3)
self.hBoxLayout.setContentsMargins(4, 4, 4, 4)
self.hBoxLayout.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.hBoxLayout.addWidget(self.clearButton, 0, Qt.AlignRight)
self.clearButton.clicked.connect(self.clear)
self.textChanged.connect(self.__onTextChanged)
self.textEdited.connect(self.__onTextEdited)
def isError(self):
return self._isError
def setError(self, isError: bool):
""" set the error status """
if isError == self.isError():
return
self._isError = isError
self.update()
def setCustomFocusedBorderColor(self, light, dark):
""" set the border color in focused status
Parameters
----------
light, dark: str | QColor | Qt.GlobalColor
border color in light/dark theme mode
"""
self.lightFocusedBorderColor = QColor(light)
self.darkFocusedBorderColor = QColor(dark)
self.update()
def focusedBorderColor(self):
if self.isError():
return FluentSystemColor.CRITICAL_FOREGROUND.color()
return autoFallbackThemeColor(self.lightFocusedBorderColor, self.darkFocusedBorderColor)
def setClearButtonEnabled(self, enable: bool):
self._isClearButtonEnabled = enable
self._adjustTextMargins()
def isClearButtonEnabled(self) -> bool:
return self._isClearButtonEnabled
def setCompleter(self, completer: QCompleter):
self._completer = completer
def completer(self):
return self._completer
def addAction(self, action: QAction, position=QLineEdit.ActionPosition.TrailingPosition):
QWidget.addAction(self, action)
button = LineEditButton(action.icon())
button.setAction(action)
button.setFixedWidth(29)
if position == QLineEdit.ActionPosition.LeadingPosition:
self.hBoxLayout.insertWidget(len(self.leftButtons), button, 0, Qt.AlignLeading)
if not self.leftButtons:
self.hBoxLayout.insertStretch(1, 1)
self.leftButtons.append(button)
else:
self.rightButtons.append(button)
self.hBoxLayout.addWidget(button, 0, Qt.AlignRight)
self._adjustTextMargins()
def addActions(self, actions, position=QLineEdit.ActionPosition.TrailingPosition):
for action in actions:
self.addAction(action, position)
def _adjustTextMargins(self):
left = len(self.leftButtons) * 30
right = len(self.rightButtons) * 30 + 28 * self.isClearButtonEnabled()
m = self.textMargins()
self.setTextMargins(left, m.top(), right, m.bottom())
def focusOutEvent(self, e):
super().focusOutEvent(e)
self.clearButton.hide()
def focusInEvent(self, e):
super().focusInEvent(e)
if self.isClearButtonEnabled():
self.clearButton.setVisible(bool(self.text()))
def __onTextChanged(self, text):
""" text changed slot """
if self.isClearButtonEnabled():
self.clearButton.setVisible(bool(text) and self.hasFocus())
def __onTextEdited(self, text):
if not self.completer():
return
if self.text():
QTimer.singleShot(50, self._showCompleterMenu)
elif self._completerMenu:
self._completerMenu.close()
def setCompleterMenu(self, menu):
""" set completer menu
Parameters
----------
menu: CompleterMenu
completer menu
"""
menu.activated.connect(self._completer.activated)
menu.indexActivated.connect(lambda idx: self._completer.activated[QModelIndex].emit(idx))
self._completerMenu = menu
def _showCompleterMenu(self):
if not self.completer() or not self.text():
return
# create menu
if not self._completerMenu:
self.setCompleterMenu(CompleterMenu(self))
# add menu items
self.completer().setCompletionPrefix(self.text())
changed = self._completerMenu.setCompletion(self.completer().completionModel(), self.completer().completionColumn())
self._completerMenu.setMaxVisibleItems(self.completer().maxVisibleItems())
# show menu
if changed:
self._completerMenu.popup()
def contextMenuEvent(self, e):
menu = LineEditMenu(self)
menu.exec(e.globalPos(), ani=True)
def paintEvent(self, e):
super().paintEvent(e)
if not self.hasFocus():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
m = self.contentsMargins()
path = QPainterPath()
w, h = self.width()-m.left()-m.right(), self.height()
path.addRoundedRect(QRectF(m.left(), h-10, w, 10), 5, 5)
rectPath = QPainterPath()
rectPath.addRect(m.left(), h-10, w, 8)
path = path.subtracted(rectPath)
painter.fillPath(path, self.focusedBorderColor())
class CompleterMenu(RoundMenu):
""" Completer menu """
activated = Signal(str)
indexActivated = Signal(QModelIndex)
def __init__(self, lineEdit: LineEdit):
super().__init__()
self.items = []
self.indexes = []
self.lineEdit = lineEdit
self.view.setViewportMargins(0, 2, 0, 6)
self.view.setObjectName('completerListWidget')
self.view.setItemDelegate(IndicatorMenuItemDelegate())
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.installEventFilter(self)
self.setItemHeight(33)
def setCompletion(self, model: QAbstractItemModel, column=0):
""" set the completion model """
items = []
self.indexes.clear()
for i in range(model.rowCount()):
items.append(model.data(model.index(i, column)))
self.indexes.append(model.index(i, column))
if self.items == items and self.isVisible():
return False
self.setItems(items)
return True
def setItems(self, items: List[str]):
""" set completion items """
self.view.clear()
self.items = items
self.view.addItems(items)
for i in range(self.view.count()):
item = self.view.item(i)
item.setSizeHint(QSize(1, self.itemHeight))
def _onItemClicked(self, item):
self._hideMenu(False)
self._onCompletionItemSelected(item.text(), self.view.row(item))
def eventFilter(self, obj, e: QEvent):
if e.type() != QEvent.KeyPress:
return super().eventFilter(obj, e)
# redirect input to line edit
self.lineEdit.event(e)
self.view.event(e)
if e.key() == Qt.Key_Escape:
self.close()
if e.key() in [Qt.Key_Enter, Qt.Key_Return] and self.view.currentRow() >= 0:
self._onCompletionItemSelected(self.view.currentItem().text(), self.view.currentRow())
self.close()
return super().eventFilter(obj, e)
def _onCompletionItemSelected(self, text, row):
self.lineEdit.setText(text)
self.activated.emit(text)
if 0 <= row < len(self.indexes):
self.indexActivated.emit(self.indexes[row])
def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
return super().exec(pos, ani, aniType)
def popup(self):
""" show menu """
if not self.items:
return self.close()
# adjust menu size
p = self.lineEdit
if self.view.width() < p.width():
self.view.setMinimumWidth(p.width())
self.adjustSize()
# determine the animation type by choosing the maximum height of view
x = -self.width()//2 + self.layout().contentsMargins().left() + p.width()//2
y = p.height() - self.layout().contentsMargins().top() + 2
pd = p.mapToGlobal(QPoint(x, y))
hd = self.view.heightForAnimation(pd, MenuAnimationType.FADE_IN_DROP_DOWN)
pu = p.mapToGlobal(QPoint(x, 7))
hu = self.view.heightForAnimation(pu, MenuAnimationType.FADE_IN_PULL_UP)
if hd >= hu:
pos = pd
aniType = MenuAnimationType.FADE_IN_DROP_DOWN
else:
pos = pu
aniType = MenuAnimationType.FADE_IN_PULL_UP
self.view.adjustSize(pos, aniType)
# update border style
self.view.setProperty('dropDown', aniType == MenuAnimationType.FADE_IN_DROP_DOWN)
self.view.setStyle(QApplication.style())
self.adjustSize()
self.exec(pos, aniType=aniType)
# remove the focus of menu
self.view.setFocusPolicy(Qt.NoFocus)
self.setFocusPolicy(Qt.NoFocus)
p.setFocus()
class SearchLineEdit(LineEdit):
""" Search line edit """
searchSignal = Signal(str)
clearSignal = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.searchButton = LineEditButton(FIF.SEARCH, self)
self.hBoxLayout.addWidget(self.searchButton, 0, Qt.AlignRight)
self.setClearButtonEnabled(True)
self.setTextMargins(0, 0, 59, 0)
self.searchButton.clicked.connect(self.search)
self.clearButton.clicked.connect(self.clearSignal)
def search(self):
""" emit search signal """
text = self.text().strip()
if text:
self.searchSignal.emit(text)
else:
self.clearSignal.emit()
def setClearButtonEnabled(self, enable: bool):
self._isClearButtonEnabled = enable
self.setTextMargins(0, 0, 28*enable+30, 0)
class EditLayer(QWidget):
""" Edit layer """
def __init__(self, parent):
super().__init__(parent=parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
parent.installEventFilter(self)
def eventFilter(self, obj, e):
if obj is self.parent() and e.type() == QEvent.Resize:
self.resize(e.size())
return super().eventFilter(obj, e)
def paintEvent(self, e):
if not self.parent().hasFocus():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
m = self.contentsMargins()
path = QPainterPath()
w, h = self.width()-m.left()-m.right(), self.height()
path.addRoundedRect(QRectF(m.left(), h-10, w, 10), 5, 5)
rectPath = QPainterPath()
rectPath.addRect(m.left(), h-10, w, 7.5)
path = path.subtracted(rectPath)
painter.fillPath(path, themeColor())
class TextEdit(QTextEdit):
""" Text edit """
def __init__(self, parent=None):
super().__init__(parent=parent)
self.layer = EditLayer(self)
self.scrollDelegate = SmoothScrollDelegate(self)
FluentStyleSheet.LINE_EDIT.apply(self)
setFont(self)
def contextMenuEvent(self, e):
menu = TextEditMenu(self)
menu.exec(e.globalPos(), ani=True)
class PlainTextEdit(QPlainTextEdit):
""" Plain text edit """
def __init__(self, parent=None):
super().__init__(parent=parent)
self.layer = EditLayer(self)
self.scrollDelegate = SmoothScrollDelegate(self)
FluentStyleSheet.LINE_EDIT.apply(self)
setFont(self)
def contextMenuEvent(self, e):
menu = TextEditMenu(self)
menu.exec(e.globalPos())
class TextBrowser(QTextBrowser):
""" Text browser """
def __init__(self, parent=None):
super().__init__(parent)
self.layer = EditLayer(self)
self.scrollDelegate = SmoothScrollDelegate(self)
FluentStyleSheet.LINE_EDIT.apply(self)
setFont(self)
def contextMenuEvent(self, e):
menu = TextEditMenu(self)
menu.exec(e.globalPos())
class PasswordLineEdit(LineEdit):
""" Password line edit """
def __init__(self, parent=None):
super().__init__(parent)
self.viewButton = LineEditButton(FIF.VIEW, self)
self.setEchoMode(QLineEdit.Password)
self.setContextMenuPolicy(Qt.NoContextMenu)
self.hBoxLayout.addWidget(self.viewButton, 0, Qt.AlignRight)
self.setClearButtonEnabled(False)
self.viewButton.installEventFilter(self)
self.viewButton.setIconSize(QSize(13, 13))
self.viewButton.setFixedSize(29, 25)
def setPasswordVisible(self, isVisible: bool):
""" set the visibility of password """
if isVisible:
self.setEchoMode(QLineEdit.Normal)
else:
self.setEchoMode(QLineEdit.Password)
def isPasswordVisible(self):
return self.echoMode() == QLineEdit.Normal
def setClearButtonEnabled(self, enable: bool):
self._isClearButtonEnabled = enable
if self.viewButton.isHidden():
self.setTextMargins(0, 0, 28*enable, 0)
else:
self.setTextMargins(0, 0, 28*enable + 30, 0)
def setViewPasswordButtonVisible(self, isVisible: bool):
""" set the visibility of view password button """
self.viewButton.setVisible(isVisible)
def eventFilter(self, obj, e):
if obj is not self.viewButton or not self.isEnabled():
return super().eventFilter(obj, e)
if e.type() == QEvent.MouseButtonPress:
self.setPasswordVisible(True)
elif e.type() == QEvent.MouseButtonRelease:
self.setPasswordVisible(False)
return super().eventFilter(obj, e)
def inputMethodQuery(self, query: Qt.InputMethodQuery):
# Disable IME for PasswordLineEdit
if query == Qt.InputMethodQuery.ImEnabled:
return False
else:
return super().inputMethodQuery(query)
passwordVisible = Property(bool, isPasswordVisible, setPasswordVisible)