提交 0f48ed69 编写于 作者: 之一Yo's avatar 之一Yo

添加编辑框的自动补全菜单

上级 ae9e18e4
# coding:utf-8
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWidgets import QApplication, QWidget, QCompleter
from qfluentwidgets import ComboBox, setTheme, Theme, setThemeColor, EditableComboBox
class Demo(QWidget):
......@@ -10,11 +10,16 @@ class Demo(QWidget):
super().__init__()
self.comboBox = ComboBox(self)
self.comboBox.addItems(['shoko 🥰', '西宫硝子', 'aiko', '柳井爱子'])
items = ['shoko 🥰', '西宫硝子', 'aiko', '柳井爱子']
self.comboBox.addItems(items)
self.comboBox.setCurrentIndex(0)
self.comboBox.currentTextChanged.connect(print)
self.comboBox.move(200, 200)
# NOTE: Completer is only applicable to EditableComboBox
# self.completer = QCompleter(items, self)
# self.comboBox.setCompleter(self.completer)
self.resize(500, 500)
self.setStyleSheet('Demo{background:white}')
......
# coding:utf-8
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QCompleter
from qfluentwidgets import LineEdit, PushButton, SearchLineEdit
......@@ -14,6 +14,31 @@ class Demo(QWidget):
self.lineEdit = SearchLineEdit(self)
self.button = PushButton('Search', self)
# add completer
stands = [
"Star Platinum", "Hierophant Green",
"Made in Haven", "King Crimson",
"Silver Chariot", "Crazy diamond",
"Metallica", "Another One Bites The Dust",
"Heaven's Door", "Killer Queen",
"The Grateful Dead", "Stone Free",
"The World", "Sticky Fingers",
"Ozone Baby", "Love Love Deluxe",
"Hermit Purple", "Gold Experience",
"King Nothing", "Paper Moon King",
"Scary Monster", "Mandom",
"20th Century Boy", "Tusk Act 4",
"Ball Breaker", "Sex Pistols",
"D4C • Love Train", "Born This Way",
"SOFT & WET", "Paisley Park",
"Wonder of U", "Walking Heart",
"Cream Starter", "November Rain",
"Smooth Operators", "The Matte Kudasai"
]
self.completer = QCompleter(stands, self.lineEdit)
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.lineEdit.setCompleter(self.completer)
self.resize(400, 400)
self.hBoxLayout.setAlignment(Qt.AlignCenter)
self.hBoxLayout.addWidget(self.lineEdit, 0, Qt.AlignCenter)
......@@ -21,7 +46,7 @@ class Demo(QWidget):
self.lineEdit.setFixedSize(200, 33)
self.lineEdit.setClearButtonEnabled(True)
self.lineEdit.setPlaceholderText('Search icon')
self.lineEdit.setPlaceholderText('Search stand')
if __name__ == '__main__':
......
......@@ -74,4 +74,22 @@ MenuActionListWidget::item:selected {
MenuActionListWidget::item:selected:active {
background-color: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.7);
}
#completerListWidget[dropDown=true] {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
#completerListWidget[dropDown=false] {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
#completerListWidget::item {
margin-top: 4px;
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
border: none;
}
\ No newline at end of file
......@@ -70,4 +70,22 @@ MenuActionListWidget::item:selected {
MenuActionListWidget::item:selected:active {
background-color: rgba(0, 0, 0, 0.06);
color: rgba(0, 0, 0, 0.7);
}
#completerListWidget[dropDown=true] {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
#completerListWidget[dropDown=false] {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
#completerListWidget::item {
margin-top: 4px;
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
border: none;
}
\ No newline at end of file
此差异已折叠。
......@@ -212,10 +212,6 @@ class NavigationInterface(QWidget):
"""
self.panel.setCurrentItem(name)
def setDefaultRouteKey(self, key: str):
""" set the routing key to use when the navigation history is empty """
self.panel.setDefaultRouteKey(key)
def setExpandWidth(self, width: int):
""" set the maximum width """
self.panel.setExpandWidth(width)
......
......@@ -531,11 +531,6 @@ class NavigationPanel(QFrame):
spacing += self.bottomLayout.count() * self.bottomLayout.spacing()
return 36 + th + bh + sh + spacing
@deprecated('0.9.0', alternative='qrouter.setDefaultRouteKey')
def setDefaultRouteKey(self, key: str):
""" set the routing key to use when the navigation history is empty """
pass
class NavigationItemLayout(QVBoxLayout):
""" Navigation layout """
......
......@@ -5,7 +5,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, QRectF, QPoint, QObject, QEvent
from PyQt5.QtGui import QPainter, QCursor, QIcon
from PyQt5.QtWidgets import QAction, QPushButton, QStyledItemDelegate, QStyle
from .menu import RoundMenu, MenuItemDelegate
from .menu import RoundMenu, MenuItemDelegate, MenuAnimationType
from .line_edit import LineEdit, LineEditButton
from ...common.animation import TranslateYAnimation
from ...common.icon import FluentIconBase, isDarkTheme
......@@ -302,7 +302,17 @@ class ComboBoxBase(QObject):
# show menu
x = -menu.width()//2 + menu.layout().contentsMargins().left() + self.width()//2
y = self.height()
menu.exec(self.mapToGlobal(QPoint(x, y)))
pos = self.mapToGlobal(QPoint(x, y))
aniType = MenuAnimationType.DROP_DOWN
menu.view.adjustSize(pos, aniType)
if menu.view.height() < 120 and menu.view.itemsHeight() > menu.view.height():
aniType = MenuAnimationType.PULL_UP
pos = self.mapToGlobal(QPoint(x, 0))
menu.view.adjustSize(pos, aniType)
menu.exec(pos, aniType=aniType)
def _toggleComboMenu(self):
if self.dropMenu:
......@@ -428,7 +438,13 @@ class ComboBoxMenu(RoundMenu):
super().__init__(title="", parent=parent)
self.view.setViewportMargins(5, 2, 5, 6)
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.view.setItemDelegate(ComboMenuItemDelegate())
FluentStyleSheet.COMBO_BOX.apply(self)
self.setItemHeight(33)
def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
self.view.adjustSize(pos, aniType)
self.adjustSize()
return super().exec(pos, ani, aniType)
\ No newline at end of file
# coding: utf-8
from typing import Union
from PyQt5.QtCore import QSize, Qt, QRectF, pyqtSignal
from PyQt5.QtGui import QPainter, QPainterPath, QIcon
from PyQt5.QtWidgets import QHBoxLayout, QLineEdit, QToolButton, QTextEdit, QPlainTextEdit
from typing import List, Union
from PyQt5.QtCore import QSize, Qt, QRectF, pyqtSignal, QPoint, QTimer, QEvent, QAbstractItemModel
from PyQt5.QtGui import QPainter, QPainterPath, QIcon, QCursor
from PyQt5.QtWidgets import (QApplication, QAction, QHBoxLayout, QLineEdit, QToolButton, QTextEdit,
QPlainTextEdit, QCompleter)
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 .menu import LineEditMenu, TextEditMenu
from .menu import LineEditMenu, TextEditMenu, RoundMenu, MenuAnimationType, MenuActionListWidget
from .scroll_bar import SmoothScrollDelegate
......@@ -58,6 +60,8 @@ class LineEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._isClearButtonEnabled = False
self._completer = None # type: QCompleter
self._completerMenu = None # type: CompleterMenu
self.setProperty("transparent", True)
FluentStyleSheet.LINE_EDIT.apply(self)
......@@ -78,6 +82,7 @@ class LineEdit(QLineEdit):
self.clearButton.clicked.connect(self.clear)
self.textChanged.connect(self.__onTextChanged)
self.textEdited.connect(self.__onTextEdited)
def setClearButtonEnabled(self, enable: bool):
self._isClearButtonEnabled = enable
......@@ -86,6 +91,12 @@ class LineEdit(QLineEdit):
def isClearButtonEnabled(self) -> bool:
return self._isClearButtonEnabled
def setCompleter(self, completer: QCompleter):
self._completer = completer
def completer(self):
return self._completer
def focusOutEvent(self, e):
super().focusOutEvent(e)
self.clearButton.hide()
......@@ -100,6 +111,31 @@ class LineEdit(QLineEdit):
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 _showCompleterMenu(self):
if not self.completer() or not self.text():
return
# create menu
if not self._completerMenu:
self._completerMenu = CompleterMenu(self)
# add menu items
self.completer().setCompletionPrefix(self.text())
changed = self._completerMenu.setCompletion(self.completer().completionModel())
# show menu
if changed:
self._completerMenu.popup()
def contextMenuEvent(self, e):
menu = LineEditMenu(self)
menu.exec_(e.globalPos())
......@@ -125,6 +161,85 @@ class LineEdit(QLineEdit):
painter.fillPath(path, themeColor())
class CompleterMenu(RoundMenu):
""" Completer menu """
def __init__(self, lineEdit: LineEditMenu):
super().__init__()
self.items = []
self.lineEdit = lineEdit
self.installEventFilter(self)
self.view.setViewportMargins(0, 2, 0, 6)
self.setItemHeight(33)
self.view.setObjectName('completerListWidget')
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
def setCompletion(self, model: QAbstractItemModel):
""" set the completion model """
items = []
for i in range(model.rowCount()):
for j in range(model.columnCount()):
items.append(model.data(model.index(i, j)))
if self.items == items and self.isVisible():
return False
self.clear()
self.items = items
# add items
for i in items:
self.addAction(QAction(i, triggered=lambda c, x=i: self.lineEdit.setText(x)))
return True
def eventFilter(self, obj, e: QEvent):
if e.type() == QEvent.KeyPress:
self.lineEdit.event(e)
if e.key() == Qt.Key_Escape:
self.close()
return super().eventFilter(obj, e)
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()
# show menu
x = -self.width()//2 + self.layout().contentsMargins().left() + p.width()//2
y = p.height() - self.layout().contentsMargins().top() + 2
pos = p.mapToGlobal(QPoint(x, y))
aniType = MenuAnimationType.FADE_IN_DROP_DOWN
self.view.adjustSize(pos, aniType)
if self.view.height() < 100 and self.view.itemsHeight() > self.view.height():
aniType = MenuAnimationType.FADE_IN_PULL_UP
pos = p.mapToGlobal(QPoint(x, 7))
self.view.adjustSize(pos, aniType)
# update border style
self.view.setProperty('dropDown', aniType == MenuAnimationType.FADE_IN_DROP_DOWN)
self.view.setStyle(QApplication.style())
self.view.update()
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 """
......
......@@ -4,17 +4,17 @@ from typing import List, Union
from qframelesswindow import WindowEffect
from PyQt5.QtCore import (QEasingCurve, QEvent, QPropertyAnimation, QObject,
Qt, QSize, QRectF, pyqtSignal, QPoint, QTimer, QModelIndex)
Qt, QSize, QRectF, pyqtSignal, QPoint, QTimer, QParallelAnimationGroup)
from PyQt5.QtGui import QIcon, QColor, QPainter, QPen, QPixmap, QRegion, QCursor, QTextCursor, QHoverEvent
from PyQt5.QtWidgets import (QAction, QApplication, QMenu, QProxyStyle, QStyle,
QGraphicsDropShadowEffect, QListWidget, QWidget, QHBoxLayout,
QListWidgetItem, QLineEdit, QTextEdit, QStyledItemDelegate, QStyleOptionViewItem)
from ...common.smooth_scroll import SmoothScroll
from ...common.icon import FluentIcon as FIF
from ...common.icon import MenuIconEngine, Action, FluentIconBase, Icon
from ...common.style_sheet import FluentStyleSheet
from ...common.config import isDarkTheme
from .scroll_bar import SmoothScrollDelegate
class CustomMenuStyle(QProxyStyle):
......@@ -55,6 +55,16 @@ class DWMMenu(QMenu):
return QMenu.event(self, e)
class MenuAnimationType(Enum):
""" Menu animation type """
NONE = 0
DROP_DOWN = 1
PULL_UP = 2
FADE_IN_DROP_DOWN = 3
FADE_IN_PULL_UP = 4
class SubMenuItemWidget(QWidget):
""" Sub menu item """
......@@ -117,22 +127,18 @@ class MenuActionListWidget(QListWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setViewportMargins(0, 6, 0, 6)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setTextElideMode(Qt.ElideNone)
self.setDragEnabled(False)
self.setMouseTracking(True)
self.setVerticalScrollMode(self.ScrollPerPixel)
self.setIconSize(QSize(14, 14))
self.setItemDelegate(MenuItemDelegate(self))
self.smoothScroll = SmoothScroll(self)
self.scrollDelegate = SmoothScrollDelegate(self)
self.setStyleSheet(
'MenuActionListWidget{font: 14px "Segoe UI", "Microsoft YaHei"}')
def wheelEvent(self, e):
self.smoothScroll.wheelEvent(e)
e.setAccepted(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def insertItem(self, row, item):
""" inserts menu item at the position in the list given by row """
......@@ -150,7 +156,7 @@ class MenuActionListWidget(QListWidget):
self.adjustSize()
return item
def adjustSize(self):
def adjustSize(self, pos=None, aniType=MenuAnimationType.NONE):
size = QSize()
for i in range(self.count()):
s = self.item(i).sizeHint()
......@@ -158,18 +164,14 @@ class MenuActionListWidget(QListWidget):
size.setHeight(size.height() + s.height())
# adjust the height of viewport
ss = QApplication.screenAt(QCursor.pos()).availableSize()
w, h = ss.width() - 100, ss.height() - 100
vsize = QSize(size)
vsize.setHeight(min(h-12, vsize.height()))
vsize.setWidth(min(w-12, vsize.width()))
w, h = MenuAnimationManager.make(self, aniType).availableViewSize(pos)
self.viewport().adjustSize()
# adjust the height of list widget
m = self.viewportMargins()
size += QSize(m.left()+m.right()+2, m.top()+m.bottom())
size.setHeight(min(h, size.height()+3))
size.setWidth(min(w, size.width()))
size.setWidth(max(min(w, size.width()), self.minimumWidth()))
self.setFixedSize(size)
def setItemHeight(self, height):
......@@ -180,13 +182,9 @@ class MenuActionListWidget(QListWidget):
self.adjustSize()
class MenuAnimationType(Enum):
""" Menu animation type """
NONE = 0
DROP_DOWN = 1
PULL_UP = 2
def itemsHeight(self):
""" Return the height of all items """
return sum(self.item(i).sizeHint().height() for i in range(self.count()))
class RoundMenu(QWidget):
......@@ -213,7 +211,6 @@ class RoundMenu(QWidget):
self.view = MenuActionListWidget(self)
self.aniManager = None
self.ani = QPropertyAnimation(self, b'pos', self)
self.timer = QTimer(self)
self.__initWidgets()
......@@ -525,7 +522,8 @@ class RoundMenu(QWidget):
return self._actions
def mousePressEvent(self, e):
if self.childAt(e.pos()) is not self.view:
w = self.childAt(e.pos())
if (w is not self.view) and (not self.view.isAncestorOf(w)):
self._hideMenu(True)
def mouseMoveEvent(self, e):
......@@ -584,8 +582,8 @@ class RoundMenu(QWidget):
aniType: MenuAnimationType
menu animation type
"""
if self.isVisible():
return
#if self.isVisible():
# aniType = MenuAnimationType.NONE
self.aniManager = MenuAnimationManager.make(self, aniType)
self.aniManager.exec(pos)
......@@ -628,7 +626,13 @@ class MenuAnimationManager(QObject):
self.ani.valueChanged.connect(self._updateMenuViewport)
def _onValueChanged(self):
raise NotImplementedError
pass
def availableViewSize(self, pos: QPoint):
""" Return the available size of view """
ss = QApplication.screenAt(QCursor.pos()).availableGeometry()
w, h = ss.width() - 100, ss.height() - 100
return w, h
def _updateMenuViewport(self):
self.menu.view.viewport().update()
......@@ -639,9 +643,10 @@ class MenuAnimationManager(QObject):
def _endPosition(self, pos):
m = self.menu
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
w, h = m.width() + 5, m.height() + 5
w, h = m.width() + 5, m.sizeHint().height()
x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
y = min(pos.y() - 4, rect.bottom() - h)
return QPoint(x, y)
def _menuSize(self):
......@@ -698,6 +703,10 @@ class DropDownMenuAnimationManager(MenuAnimationManager):
self.ani.setEndValue(pos)
self.ani.start()
def availableViewSize(self, pos: QPoint):
ss = QApplication.screenAt(QCursor.pos()).availableGeometry()
return ss.width() - 100, max(ss.bottom() - pos.y() - 28, 1)
def _onValueChanged(self):
w, h = self._menuSize()
y = self.ani.endValue().y() - self.ani.currentValue().y()
......@@ -711,9 +720,9 @@ class PullUpMenuAnimationManager(MenuAnimationManager):
def _endPosition(self, pos):
m = self.menu
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
w, h = m.width() + 5, m.height()
w, h = m.width() + 5, m.sizeHint().height()
x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
y = max(pos.y() - h + 15, 4)
y = max(pos.y() - h + 10, 4)
return QPoint(x, y)
def exec(self, pos):
......@@ -724,10 +733,83 @@ class PullUpMenuAnimationManager(MenuAnimationManager):
self.ani.setEndValue(pos)
self.ani.start()
def availableViewSize(self, pos: QPoint):
ss = QApplication.screenAt(QCursor.pos()).availableGeometry()
return ss.width() - 100, max(pos.y() - 28, 1)
def _onValueChanged(self):
w, h = self._menuSize()
y = self.ani.endValue().y() - self.ani.currentValue().y()
self.menu.setMask(QRegion(0, y, w, h))
self.menu.setMask(QRegion(0, y, w, h - 28))
@MenuAnimationManager.register(MenuAnimationType.FADE_IN_DROP_DOWN)
class FadeInDropDownMenuAnimationManager(MenuAnimationManager):
""" Fade in drop down menu animation manager """
def __init__(self, menu: RoundMenu):
super().__init__(menu)
self.opacityAni = QPropertyAnimation(menu, b'windowOpacity', self)
self.aniGroup = QParallelAnimationGroup(self)
self.aniGroup.addAnimation(self.ani)
self.aniGroup.addAnimation(self.opacityAni)
def exec(self, pos):
pos = self._endPosition(pos)
self.opacityAni.setStartValue(0)
self.opacityAni.setEndValue(1)
self.opacityAni.setDuration(150)
self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)
self.ani.setStartValue(pos-QPoint(0, 8))
self.ani.setEndValue(pos)
self.ani.setDuration(150)
self.ani.setEasingCurve(QEasingCurve.OutQuad)
self.aniGroup.start()
def availableViewSize(self, pos: QPoint):
ss = QApplication.screenAt(QCursor.pos()).availableGeometry()
return ss.width() - 100, max(ss.bottom() - pos.y() - 28, 1)
@MenuAnimationManager.register(MenuAnimationType.FADE_IN_PULL_UP)
class FadeInPullUpMenuAnimationManager(MenuAnimationManager):
""" Fade in pull up menu animation manager """
def __init__(self, menu: RoundMenu):
super().__init__(menu)
self.opacityAni = QPropertyAnimation(menu, b'windowOpacity', self)
self.aniGroup = QParallelAnimationGroup(self)
self.aniGroup.addAnimation(self.ani)
self.aniGroup.addAnimation(self.opacityAni)
def _endPosition(self, pos):
m = self.menu
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
w, h = m.width() + 5, m.height()
x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
y = max(pos.y() - h + 15, 4)
return QPoint(x, y)
def exec(self, pos):
pos = self._endPosition(pos)
self.opacityAni.setStartValue(0)
self.opacityAni.setEndValue(1)
self.opacityAni.setDuration(150)
self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)
self.ani.setStartValue(pos+QPoint(0, 8))
self.ani.setEndValue(pos)
self.ani.setDuration(200)
self.ani.setEasingCurve(QEasingCurve.OutQuad)
self.aniGroup.start()
def availableViewSize(self, pos: QPoint):
ss = QApplication.screenAt(QCursor.pos()).availableGeometry()
return ss.width() - 100, pos.y() - 28
class EditMenu(RoundMenu):
......
......@@ -337,11 +337,11 @@ class ScrollBar(QWidget):
if self.orientation() == Qt.Vertical:
total = self.maximum() - self.minimum() + p.height()
s = int(self._grooveLength() * p.height() / max(total, 1))
self.handle.setFixedHeight(max(40, s))
self.handle.setFixedHeight(max(30, s))
else:
total = self.maximum() - self.minimum() + p.width()
s = int(self._grooveLength() * p.width() / max(total, 1))
self.handle.setFixedWidth(max(40, s))
self.handle.setFixedWidth(max(30, s))
def _adjustHandlePos(self):
total = max(self.maximum() - self.minimum(), 1)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册