You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
452 lines
13 KiB
452 lines
13 KiB
# coding:utf-8
|
|
from typing import Union, List, Iterable
|
|
|
|
from PySide6.QtCore import Qt, Signal, QRectF, QPoint, QObject, QEvent
|
|
from PySide6.QtGui import QPainter, QAction, QCursor, QIcon
|
|
from PySide6.QtWidgets import QPushButton, QStyledItemDelegate, QStyle
|
|
|
|
from .menu import RoundMenu, MenuItemDelegate
|
|
from .line_edit import LineEdit, LineEditButton
|
|
from ...common.animation import TranslateYAnimation
|
|
from ...common.icon import FluentIconBase, isDarkTheme
|
|
from ...common.icon import FluentIcon as FIF
|
|
from ...common.style_sheet import FluentStyleSheet, themeColor
|
|
|
|
|
|
class ComboItem:
|
|
""" Combo box item """
|
|
|
|
def __init__(self, text: str, icon: Union[str, QIcon, FluentIconBase] = None, userData=None):
|
|
""" add item
|
|
|
|
Parameters
|
|
----------
|
|
text: str
|
|
the text of item
|
|
|
|
icon: str | QIcon | FluentIconBase
|
|
the icon of item
|
|
|
|
userData: Any
|
|
user data
|
|
"""
|
|
self.text = text
|
|
self.userData = userData
|
|
self.icon = icon
|
|
|
|
@property
|
|
def icon(self):
|
|
if isinstance(self._icon, QIcon):
|
|
return self._icon
|
|
|
|
return self._icon.icon()
|
|
|
|
@icon.setter
|
|
def icon(self, ico: Union[str, QIcon, FluentIconBase]):
|
|
if ico:
|
|
self._icon = QIcon(ico) if isinstance(ico, str) else ico
|
|
else:
|
|
self._icon = QIcon()
|
|
|
|
|
|
class ComboBoxBase:
|
|
""" Combo box base """
|
|
|
|
def __init__(self, parent=None, **kwargs):
|
|
pass
|
|
|
|
def _setUpUi(self):
|
|
self.isHover = False
|
|
self.isPressed = False
|
|
self.items = [] # type: List[ComboItem]
|
|
self._currentIndex = -1
|
|
self.dropMenu = None
|
|
|
|
FluentStyleSheet.COMBO_BOX.apply(self)
|
|
self.installEventFilter(self)
|
|
|
|
def addItem(self, text, icon: Union[str, QIcon, FluentIconBase] = None, userData=None):
|
|
""" add item
|
|
|
|
Parameters
|
|
----------
|
|
text: str
|
|
the text of item
|
|
|
|
icon: str | QIcon | FluentIconBase
|
|
"""
|
|
item = ComboItem(text, icon, userData)
|
|
self.items.append(item)
|
|
|
|
def addItems(self, texts: Iterable[str]):
|
|
""" add items
|
|
|
|
Parameters
|
|
----------
|
|
text: Iterable[str]
|
|
the text of item
|
|
"""
|
|
for text in texts:
|
|
self.addItem(text)
|
|
|
|
def removeItem(self, index: int):
|
|
""" Removes the item at the given index from the combobox.
|
|
This will update the current index if the index is removed.
|
|
"""
|
|
if not 0 <= index < len(self.items):
|
|
return
|
|
|
|
self.items.pop(index)
|
|
|
|
if index < self.currentIndex():
|
|
self._onItemClicked(self._currentIndex - 1)
|
|
elif index == self.currentIndex():
|
|
if index > 0:
|
|
self._onItemClicked(self._currentIndex - 1)
|
|
else:
|
|
self.setCurrentIndex(0)
|
|
self.currentTextChanged.emit(self.currentText())
|
|
self.currentIndexChanged.emit(0)
|
|
|
|
def currentIndex(self):
|
|
return self._currentIndex
|
|
|
|
def setCurrentIndex(self, index: int):
|
|
""" set current index
|
|
|
|
Parameters
|
|
----------
|
|
index: int
|
|
current index
|
|
"""
|
|
if not 0 <= index < len(self.items):
|
|
return
|
|
|
|
self._currentIndex = index
|
|
self.setText(self.items[index].text)
|
|
|
|
def setText(self, text: str):
|
|
super().setText(text)
|
|
self.adjustSize()
|
|
|
|
def currentText(self):
|
|
if not 0 <= self.currentIndex() < len(self.items):
|
|
return ''
|
|
|
|
return self.items[self.currentIndex()].text
|
|
|
|
def currentData(self):
|
|
if not 0 <= self.currentIndex() < len(self.items):
|
|
return None
|
|
|
|
return self.items[self.currentIndex()].userData
|
|
|
|
def setCurrentText(self, text):
|
|
""" set the current text displayed in combo box,
|
|
text should be in the item list
|
|
|
|
Parameters
|
|
----------
|
|
text: str
|
|
text displayed in combo box
|
|
"""
|
|
if text == self.currentText():
|
|
return
|
|
|
|
index = self.findText(text)
|
|
if index >= 0:
|
|
self.setCurrentIndex(index)
|
|
|
|
def setItemText(self, index: int, text: str):
|
|
""" set the text of item
|
|
|
|
Parameters
|
|
----------
|
|
index: int
|
|
the index of item
|
|
|
|
text: str
|
|
new text of item
|
|
"""
|
|
if not 0 <= index < len(self.items):
|
|
return
|
|
|
|
self.items[index].text = text
|
|
if self.currentIndex() == index:
|
|
self.setText(text)
|
|
|
|
def itemData(self, index: int):
|
|
""" Returns the data in the given index """
|
|
if not 0 <= index < len(self.items):
|
|
return None
|
|
|
|
return self.items[index].userData
|
|
|
|
def itemText(self, index: int):
|
|
""" Returns the text in the given index """
|
|
if not 0 <= index < len(self.items):
|
|
return ''
|
|
|
|
return self.items[index].text
|
|
|
|
def itemIcon(self, index: int):
|
|
""" Returns the icon in the given index """
|
|
if not 0 <= index < len(self.items):
|
|
return QIcon()
|
|
|
|
return self.items[index].icon
|
|
|
|
def setItemData(self, index: int, value):
|
|
""" Sets the data role for the item on the given index """
|
|
if 0 <= index < len(self.items):
|
|
self.items[index].userData = value
|
|
|
|
def setItemIcon(self, index: int, icon: Union[str, QIcon, FluentIconBase]):
|
|
""" Sets the data role for the item on the given index """
|
|
if 0 <= index < len(self.items):
|
|
self.items[index].icon = icon
|
|
|
|
def findData(self, data):
|
|
""" Returns the index of the item containing the given data, otherwise returns -1 """
|
|
for i, item in enumerate(self.items):
|
|
if item.userData == data:
|
|
return i
|
|
|
|
return -1
|
|
|
|
def findText(self, text: str):
|
|
""" Returns the index of the item containing the given text; otherwise returns -1. """
|
|
for i, item in enumerate(self.items):
|
|
if item.text == text:
|
|
return i
|
|
|
|
return -1
|
|
|
|
def clear(self):
|
|
""" Clears the combobox, removing all items. """
|
|
if self.currentIndex() >= 0:
|
|
self.setText('')
|
|
|
|
self.items.clear()
|
|
self._currentIndex = -1
|
|
|
|
def count(self):
|
|
""" Returns the number of items in the combobox """
|
|
return len(self.items)
|
|
|
|
def insertItem(self, index: int, text: str, icon: Union[str, QIcon, FluentIconBase] = None, userData=None):
|
|
""" Inserts item into the combobox at the given index. """
|
|
item = ComboItem(text, icon, userData)
|
|
self.items.insert(index, item)
|
|
|
|
if index <= self.currentIndex():
|
|
self._onItemClicked(self.currentIndex() + 1)
|
|
|
|
def insertItems(self, index: int, texts: Iterable[str]):
|
|
""" Inserts items into the combobox, starting at the index specified. """
|
|
pos = index
|
|
for text in texts:
|
|
item = ComboItem(text)
|
|
self.items.insert(pos, item)
|
|
pos += 1
|
|
|
|
if index <= self.currentIndex():
|
|
self._onItemClicked(self.currentIndex() + pos - index)
|
|
|
|
def _closeComboMenu(self):
|
|
if not self.dropMenu:
|
|
return
|
|
|
|
self.dropMenu.close()
|
|
self.dropMenu = None
|
|
|
|
def _onDropMenuClosed(self):
|
|
pos = self.mapFromGlobal(QCursor.pos())
|
|
if not self.rect().contains(pos):
|
|
self.dropMenu = None
|
|
|
|
def _showComboMenu(self):
|
|
if not self.items:
|
|
return
|
|
|
|
menu = ComboBoxMenu(self)
|
|
for i, item in enumerate(self.items):
|
|
menu.addAction(
|
|
QAction(item.icon, item.text, triggered=lambda x=i: self._onItemClicked(x)))
|
|
|
|
if menu.view.width() < self.width():
|
|
menu.view.setMinimumWidth(self.width())
|
|
menu.adjustSize()
|
|
|
|
menu.closedSignal.connect(self._onDropMenuClosed)
|
|
self.dropMenu = menu
|
|
|
|
# set the selected item
|
|
if self.currentIndex() >= 0 and self.items:
|
|
menu.setDefaultAction(menu.menuActions()[self.currentIndex()])
|
|
|
|
# show menu
|
|
x = -menu.width()//2 + menu.layout().contentsMargins().left() + self.width()//2
|
|
y = self.height()
|
|
menu.exec(self.mapToGlobal(QPoint(x, y)))
|
|
|
|
def _toggleComboMenu(self):
|
|
if self.dropMenu:
|
|
self._closeComboMenu()
|
|
else:
|
|
self._showComboMenu()
|
|
|
|
def _onItemClicked(self, index):
|
|
if index == self.currentIndex():
|
|
return
|
|
|
|
self.setCurrentIndex(index)
|
|
self.currentTextChanged.emit(self.currentText())
|
|
self.currentIndexChanged.emit(index)
|
|
|
|
|
|
class ComboBox(QPushButton, ComboBoxBase):
|
|
""" Combo box """
|
|
|
|
currentIndexChanged = Signal(int)
|
|
currentTextChanged = Signal(str)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
self.arrowAni = TranslateYAnimation(self)
|
|
self._setUpUi()
|
|
|
|
def eventFilter(self, obj, e: QEvent):
|
|
if obj is self:
|
|
if e.type() == QEvent.MouseButtonPress:
|
|
self.isPressed = True
|
|
elif e.type() == QEvent.MouseButtonRelease:
|
|
self.isPressed = False
|
|
elif e.type() == QEvent.Enter:
|
|
self.isHover = True
|
|
elif e.type() == QEvent.Leave:
|
|
self.isHover = False
|
|
|
|
return super().eventFilter(obj, e)
|
|
|
|
def setPlaceholderText(self, text: str):
|
|
self.setText(text)
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
super().mouseReleaseEvent(e)
|
|
self._toggleComboMenu()
|
|
|
|
def paintEvent(self, e):
|
|
QPushButton.paintEvent(self, e)
|
|
painter = QPainter(self)
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
|
if self.isHover:
|
|
painter.setOpacity(0.8)
|
|
elif self.isPressed:
|
|
painter.setOpacity(0.7)
|
|
|
|
rect = QRectF(self.width()-22, self.height()/2-5+self.arrowAni.y, 10, 10)
|
|
if isDarkTheme():
|
|
FIF.ARROW_DOWN.render(painter, rect)
|
|
else:
|
|
FIF.ARROW_DOWN.render(painter, rect, fill="#646464")
|
|
|
|
|
|
class EditableComboBox(LineEdit, ComboBoxBase):
|
|
""" Editable combo box """
|
|
|
|
currentIndexChanged = Signal(int)
|
|
currentTextChanged = Signal(str)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
self._setUpUi()
|
|
|
|
self.dropButton = LineEditButton(FIF.ARROW_DOWN, self)
|
|
|
|
self.setTextMargins(0, 0, 29, 0)
|
|
self.dropButton.setFixedSize(30, 25)
|
|
self.hBoxLayout.addWidget(self.dropButton, 0, Qt.AlignRight)
|
|
|
|
self.dropButton.clicked.connect(self._toggleComboMenu)
|
|
self.textEdited.connect(self._onTextEdited)
|
|
self.returnPressed.connect(self._onReturnPressed)
|
|
|
|
FluentStyleSheet.LINE_EDIT.apply(self)
|
|
|
|
def currentText(self):
|
|
return self.text()
|
|
|
|
def clear(self):
|
|
ComboBoxBase.clear(self)
|
|
|
|
def _onReturnPressed(self):
|
|
if not self.text():
|
|
return
|
|
|
|
index = self.findText(self.text())
|
|
if index >= 0 and index != self.currentIndex():
|
|
self._currentIndex = index
|
|
self.currentIndexChanged.emit(index)
|
|
elif index == -1:
|
|
self.addItem(self.text())
|
|
self.setCurrentIndex(self.count() - 1)
|
|
|
|
def eventFilter(self, obj, e: QEvent):
|
|
if obj is self:
|
|
if e.type() == QEvent.MouseButtonPress:
|
|
self.isPressed = True
|
|
elif e.type() == QEvent.MouseButtonRelease:
|
|
self.isPressed = False
|
|
elif e.type() == QEvent.Enter:
|
|
self.isHover = True
|
|
elif e.type() == QEvent.Leave:
|
|
self.isHover = False
|
|
|
|
return super().eventFilter(obj, e)
|
|
|
|
def _onTextEdited(self, text: str):
|
|
self._currentIndex = -1
|
|
self.currentTextChanged.emit(text)
|
|
|
|
for i, item in enumerate(self.items):
|
|
if item.text == text:
|
|
self._currentIndex = i
|
|
self.currentIndexChanged.emit(i)
|
|
return
|
|
|
|
def _onDropMenuClosed(self):
|
|
self.dropMenu = None
|
|
|
|
|
|
class ComboMenuItemDelegate(MenuItemDelegate):
|
|
""" Combo box drop menu item delegate """
|
|
|
|
def paint(self, painter: QPainter, option, index):
|
|
super().paint(painter, option, index)
|
|
if not option.state & QStyle.State_Selected:
|
|
return
|
|
|
|
painter.save()
|
|
painter.setRenderHints(
|
|
QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing)
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setBrush(themeColor())
|
|
painter.drawRoundedRect(0, 11+option.rect.y(), 3, 15, 1.5, 1.5)
|
|
|
|
painter.restore()
|
|
|
|
|
|
class ComboBoxMenu(RoundMenu):
|
|
""" Combo box menu """
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(title="", parent=parent)
|
|
|
|
self.view.setViewportMargins(5, 2, 5, 6)
|
|
self.view.setItemDelegate(ComboMenuItemDelegate())
|
|
|
|
FluentStyleSheet.COMBO_BOX.apply(self)
|
|
self.setItemHeight(33)
|