# coding: utf-8 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, QStyle) 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, 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.isPressed = False self.setFixedSize(31, 23) self.setIconSize(QSize(10, 10)) self.setCursor(Qt.PointingHandCursor) self.setObjectName('lineEditButton') FluentStyleSheet.LINE_EDIT.apply(self) 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.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 setClearButtonEnabled(self, enable: bool): self._isClearButtonEnabled = enable self.setTextMargins(0, 0, 28*enable, 0) 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() 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 _showCompleterMenu(self): if not self.completer() or not self.text(): return # create menu if not self._completerMenu: self._completerMenu = CompleterMenu(self) self._completerMenu.activated.connect(self._completer.activated) # 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()) 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, themeColor()) class CompleterMenu(RoundMenu): """ Completer menu """ activated = pyqtSignal(str) def __init__(self, lineEdit: LineEdit): super().__init__() self.items = [] 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): """ 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.__onItemSelected(x))) return True 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.__onItemSelected(self.view.currentItem().text()) self.close() return super().eventFilter(obj, e) def __onItemSelected(self, text): self.lineEdit.setText(text) self.activated.emit(text) 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 """ searchSignal = pyqtSignal(str) clearSignal = pyqtSignal() 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 TextEdit(QTextEdit): """ Text edit """ def __init__(self, parent=None): super().__init__(parent=parent) self.scrollDelegate = SmoothScrollDelegate(self) FluentStyleSheet.LINE_EDIT.apply(self) setFont(self) def contextMenuEvent(self, e): menu = TextEditMenu(self) menu.exec_(e.globalPos()) class PlainTextEdit(QPlainTextEdit): """ Plain text edit """ def __init__(self, parent=None): super().__init__(parent=parent) self.scrollDelegate = SmoothScrollDelegate(self) FluentStyleSheet.LINE_EDIT.apply(self) setFont(self) def contextMenuEvent(self, e): menu = TextEditMenu(self) menu.exec_(e.globalPos())