line_edit.py 9.7 KB
Newer Older
之一Yo's avatar
之一Yo 已提交
1
# coding: utf-8
之一Yo's avatar
之一Yo 已提交
2 3 4 5
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,
之一Yo's avatar
之一Yo 已提交
6
                             QPlainTextEdit, QCompleter, QStyle)
之一Yo's avatar
之一Yo 已提交
7

之一Yo's avatar
之一Yo 已提交
8

9
from ...common.style_sheet import FluentStyleSheet, themeColor
10
from ...common.icon import isDarkTheme, FluentIconBase, drawIcon
之一Yo's avatar
之一Yo 已提交
11
from ...common.icon import FluentIcon as FIF
之一Yo's avatar
之一Yo 已提交
12
from ...common.font import setFont
之一Yo's avatar
之一Yo 已提交
13
from .menu import LineEditMenu, TextEditMenu, RoundMenu, MenuAnimationType, IndicatorMenuItemDelegate
之一Yo's avatar
之一Yo 已提交
14
from .scroll_bar import SmoothScrollDelegate
之一Yo's avatar
之一Yo 已提交
15 16


之一Yo's avatar
之一Yo 已提交
17 18
class LineEditButton(QToolButton):
    """ Line edit button """
之一Yo's avatar
之一Yo 已提交
19

之一Yo's avatar
之一Yo 已提交
20
    def __init__(self, icon: Union[str, QIcon, FluentIconBase], parent=None):
之一Yo's avatar
之一Yo 已提交
21
        super().__init__(parent=parent)
之一Yo's avatar
之一Yo 已提交
22
        self._icon = icon
23
        self.isPressed = False
之一Yo's avatar
之一Yo 已提交
24
        self.setFixedSize(31, 23)
之一Yo's avatar
之一Yo 已提交
25 26
        self.setIconSize(QSize(10, 10))
        self.setCursor(Qt.PointingHandCursor)
之一Yo's avatar
之一Yo 已提交
27
        self.setObjectName('lineEditButton')
28
        FluentStyleSheet.LINE_EDIT.apply(self)
之一Yo's avatar
之一Yo 已提交
29

30 31 32 33 34 35 36 37
    def mousePressEvent(self, e):
        self.isPressed = True
        super().mousePressEvent(e)

    def mouseReleaseEvent(self, e):
        self.isPressed = False
        super().mouseReleaseEvent(e)

之一Yo's avatar
之一Yo 已提交
38 39 40
    def paintEvent(self, e):
        super().paintEvent(e)
        painter = QPainter(self)
41 42
        painter.setRenderHints(QPainter.Antialiasing |
                               QPainter.SmoothPixmapTransform)
之一Yo's avatar
之一Yo 已提交
43 44 45 46 47

        iw, ih = self.iconSize().width(), self.iconSize().height()
        w, h = self.width(), self.height()
        rect = QRectF((w - iw)/2, (h - ih)/2, iw, ih)

48 49 50
        if self.isPressed:
            painter.setOpacity(0.7)

之一Yo's avatar
之一Yo 已提交
51
        if isDarkTheme():
52
            drawIcon(self._icon, painter, rect)
之一Yo's avatar
之一Yo 已提交
53
        else:
54
            drawIcon(self._icon, painter, rect, fill='#656565')
之一Yo's avatar
之一Yo 已提交
55 56


之一Yo's avatar
之一Yo 已提交
57 58 59
class LineEdit(QLineEdit):
    """ Line edit """

60 61
    def __init__(self, parent=None):
        super().__init__(parent=parent)
之一Yo's avatar
之一Yo 已提交
62
        self._isClearButtonEnabled = False
之一Yo's avatar
之一Yo 已提交
63 64
        self._completer = None  # type: QCompleter
        self._completerMenu = None  # type: CompleterMenu
之一Yo's avatar
之一Yo 已提交
65

66
        self.setProperty("transparent", True)
67
        FluentStyleSheet.LINE_EDIT.apply(self)
之一Yo's avatar
之一Yo 已提交
68
        self.setFixedHeight(33)
之一Yo's avatar
之一Yo 已提交
69
        self.setAttribute(Qt.WA_MacShowFocusRect, False)
之一Yo's avatar
之一Yo 已提交
70
        setFont(self)
之一Yo's avatar
之一Yo 已提交
71

之一Yo's avatar
之一Yo 已提交
72 73 74 75
        self.hBoxLayout = QHBoxLayout(self)
        self.clearButton = LineEditButton(FIF.CLOSE, self)

        self.clearButton.setFixedSize(29, 25)
之一Yo's avatar
之一Yo 已提交
76 77
        self.clearButton.hide()

之一Yo's avatar
之一Yo 已提交
78 79 80 81 82
        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)

之一Yo's avatar
之一Yo 已提交
83 84
        self.clearButton.clicked.connect(self.clear)
        self.textChanged.connect(self.__onTextChanged)
之一Yo's avatar
之一Yo 已提交
85
        self.textEdited.connect(self.__onTextEdited)
之一Yo's avatar
之一Yo 已提交
86 87 88

    def setClearButtonEnabled(self, enable: bool):
        self._isClearButtonEnabled = enable
89
        self.setTextMargins(0, 0, 28*enable, 0)
之一Yo's avatar
之一Yo 已提交
90 91 92 93

    def isClearButtonEnabled(self) -> bool:
        return self._isClearButtonEnabled

之一Yo's avatar
之一Yo 已提交
94 95 96 97 98 99
    def setCompleter(self, completer: QCompleter):
        self._completer = completer

    def completer(self):
        return self._completer

之一Yo's avatar
之一Yo 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112 113
    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())

之一Yo's avatar
之一Yo 已提交
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    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)
C
Cheukfung 已提交
130
            self._completerMenu.activated.connect(self._completer.activated)
之一Yo's avatar
之一Yo 已提交
131 132 133 134 135 136 137 138 139

        # add menu items
        self.completer().setCompletionPrefix(self.text())
        changed = self._completerMenu.setCompletion(self.completer().completionModel())

        # show menu
        if changed:
            self._completerMenu.popup()

之一Yo's avatar
之一Yo 已提交
140 141 142 143
    def contextMenuEvent(self, e):
        menu = LineEditMenu(self)
        menu.exec_(e.globalPos())

144 145 146 147 148 149 150 151 152
    def paintEvent(self, e):
        super().paintEvent(e)
        if not self.hasFocus():
            return

        painter = QPainter(self)
        painter.setRenderHints(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)

之一Yo's avatar
之一Yo 已提交
153
        m = self.contentsMargins()
154
        path = QPainterPath()
之一Yo's avatar
之一Yo 已提交
155 156
        w, h = self.width()-m.left()-m.right(), self.height()
        path.addRoundedRect(QRectF(m.left(), h-10, w, 10), 5, 5)
157 158

        rectPath = QPainterPath()
之一Yo's avatar
之一Yo 已提交
159
        rectPath.addRect(m.left(), h-10, w, 8)
160 161 162 163 164
        path = path.subtracted(rectPath)

        painter.fillPath(path, themeColor())


之一Yo's avatar
之一Yo 已提交
165 166 167
class CompleterMenu(RoundMenu):
    """ Completer menu """

C
Cheukfung 已提交
168 169
    activated = pyqtSignal(str)

之一Yo's avatar
之一Yo 已提交
170
    def __init__(self, lineEdit: LineEdit):
之一Yo's avatar
之一Yo 已提交
171 172 173 174
        super().__init__()
        self.items = []
        self.lineEdit = lineEdit

之一Yo's avatar
之一Yo 已提交
175
        self.view.setViewportMargins(0, 2, 0, 6)
之一Yo's avatar
之一Yo 已提交
176
        self.view.setObjectName('completerListWidget')
之一Yo's avatar
之一Yo 已提交
177
        self.view.setItemDelegate(IndicatorMenuItemDelegate())
之一Yo's avatar
之一Yo 已提交
178 179
        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)

之一Yo's avatar
之一Yo 已提交
180 181 182
        self.installEventFilter(self)
        self.setItemHeight(33)

之一Yo's avatar
之一Yo 已提交
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
    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:
C
Cheukfung 已提交
198
            self.addAction(QAction(i, triggered=lambda c, x=i: self.__onItemSelected(x)))
之一Yo's avatar
之一Yo 已提交
199 200 201 202

        return True

    def eventFilter(self, obj, e: QEvent):
之一Yo's avatar
之一Yo 已提交
203 204 205 206 207 208 209 210 211 212
        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:
C
Cheukfung 已提交
213
            self.__onItemSelected(self.view.currentItem().text())
之一Yo's avatar
之一Yo 已提交
214
            self.close()
之一Yo's avatar
之一Yo 已提交
215 216 217

        return super().eventFilter(obj, e)

C
Cheukfung 已提交
218 219 220 221
    def __onItemSelected(self, text):
        self.lineEdit.setText(text)
        self.activated.emit(text)

之一Yo's avatar
之一Yo 已提交
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    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()


之一Yo's avatar
之一Yo 已提交
260 261 262 263 264 265 266
class SearchLineEdit(LineEdit):
    """ Search line edit """

    searchSignal = pyqtSignal(str)
    clearSignal = pyqtSignal()

    def __init__(self, parent=None):
267
        super().__init__(parent)
之一Yo's avatar
之一Yo 已提交
268 269 270 271 272 273 274 275 276
        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)

之一Yo's avatar
之一Yo 已提交
277
    def search(self):
之一Yo's avatar
之一Yo 已提交
278
        """ emit search signal """
之一Yo's avatar
之一Yo 已提交
279
        text = self.text().strip()
之一Yo's avatar
之一Yo 已提交
280 281 282 283 284
        if text:
            self.searchSignal.emit(text)
        else:
            self.clearSignal.emit()

之一Yo's avatar
之一Yo 已提交
285 286 287 288
    def setClearButtonEnabled(self, enable: bool):
        self._isClearButtonEnabled = enable
        self.setTextMargins(0, 0, 28*enable+30, 0)

之一Yo's avatar
之一Yo 已提交
289

290 291 292 293 294
class TextEdit(QTextEdit):
    """ Text edit """

    def __init__(self, parent=None):
        super().__init__(parent=parent)
295
        self.scrollDelegate = SmoothScrollDelegate(self)
296
        FluentStyleSheet.LINE_EDIT.apply(self)
之一Yo's avatar
之一Yo 已提交
297
        setFont(self)
298 299 300 301 302

    def contextMenuEvent(self, e):
        menu = TextEditMenu(self)
        menu.exec_(e.globalPos())

之一Yo's avatar
之一Yo 已提交
303 304 305 306 307 308

class PlainTextEdit(QPlainTextEdit):
    """ Plain text edit """

    def __init__(self, parent=None):
        super().__init__(parent=parent)
309
        self.scrollDelegate = SmoothScrollDelegate(self)
310
        FluentStyleSheet.LINE_EDIT.apply(self)
之一Yo's avatar
之一Yo 已提交
311
        setFont(self)
之一Yo's avatar
之一Yo 已提交
312 313 314 315 316

    def contextMenuEvent(self, e):
        menu = TextEditMenu(self)
        menu.exec_(e.globalPos())