lineedit.py 12.2 KB
Newer Older
Dan Pascu's avatar
Dan Pascu committed
1

2 3
import re

Dan Pascu's avatar
Dan Pascu committed
4 5 6
from PyQt5.QtCore import Qt, QEvent, pyqtSignal
from PyQt5.QtGui import QPainter, QPalette, QPixmap
from PyQt5.QtWidgets import QAbstractButton, QLineEdit, QBoxLayout, QHBoxLayout, QLabel, QLayout, QSizePolicy, QSpacerItem, QStyle, QStyleOptionFrame, QWidget
Dan Pascu's avatar
Dan Pascu committed
7

8
from blink.resources import Resources
Dan Pascu's avatar
Dan Pascu committed
9 10 11
from blink.widgets.util import QtDynamicProperty


12 13 14
__all__ = ['LineEdit', 'ValidatingLineEdit', 'SearchBox', 'LocationBar']


Dan Pascu's avatar
Dan Pascu committed
15
class SideWidget(QWidget):
16 17
    sizeHintChanged = pyqtSignal()

Dan Pascu's avatar
Dan Pascu committed
18
    def __init__(self, parent=None):
Dan Pascu's avatar
Dan Pascu committed
19
        super(SideWidget, self).__init__(parent)
Dan Pascu's avatar
Dan Pascu committed
20 21 22

    def event(self, event):
        if event.type() == QEvent.LayoutRequest:
23
            self.sizeHintChanged.emit()
Dan Pascu's avatar
Dan Pascu committed
24 25 26 27
        return QWidget.event(self, event)


class LineEdit(QLineEdit):
28
    inactiveText  = QtDynamicProperty('inactiveText',  unicode)
Dan Pascu's avatar
Dan Pascu committed
29 30 31
    widgetSpacing = QtDynamicProperty('widgetSpacing', int)

    def __init__(self, parent=None, contents=u""):
Dan Pascu's avatar
Dan Pascu committed
32
        super(LineEdit, self).__init__(contents, parent)
Dan Pascu's avatar
Dan Pascu committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
        box_direction = QBoxLayout.RightToLeft if self.isRightToLeft() else QBoxLayout.LeftToRight
        self.inactiveText = u""
        self.left_widget = SideWidget(self)
        self.left_widget.resize(0, 0)
        self.left_layout = QHBoxLayout(self.left_widget)
        self.left_layout.setContentsMargins(0, 0, 0, 0)
        self.left_layout.setDirection(box_direction)
        self.left_layout.setSizeConstraint(QLayout.SetFixedSize)
        self.right_widget = SideWidget(self)
        self.right_widget.resize(0, 0)
        self.right_layout = QHBoxLayout(self.right_widget)
        self.right_layout.setContentsMargins(0, 0, 0, 0)
        self.right_layout.setDirection(box_direction)
        self.right_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
        self.widgetSpacing = 2
48 49
        self.left_widget.sizeHintChanged.connect(self._update_text_margins)
        self.right_widget.sizeHintChanged.connect(self._update_text_margins)
Dan Pascu's avatar
Dan Pascu committed
50 51 52

    @property
    def left_margin(self):
Dan Pascu's avatar
Dan Pascu committed
53
        return self.left_widget.sizeHint().width() + 2*self.left_layout.spacing()
Dan Pascu's avatar
Dan Pascu committed
54 55 56 57 58 59 60 61 62 63

    @property
    def right_margin(self):
        return self.right_widget.sizeHint().width() + 2*self.right_layout.spacing()

    def _update_text_margins(self):
        self.setTextMargins(self.left_margin, 0, self.right_margin, 0)
        self._update_side_widget_locations()

    def _update_side_widget_locations(self):
Dan Pascu's avatar
Dan Pascu committed
64
        option = QStyleOptionFrame()
Dan Pascu's avatar
Dan Pascu committed
65 66 67 68
        self.initStyleOption(option)
        spacing = self.right_layout.spacing()
        text_rect = self.style().subElementRect(QStyle.SE_LineEditContents, option, self)
        text_rect.adjust(spacing, 0, -spacing, 0)
69
        mid_height = text_rect.center().y() + 1 - (text_rect.height() % 2)  # need -1 correction for odd heights -Dan
Dan Pascu's avatar
Dan Pascu committed
70 71 72 73 74 75 76 77 78 79 80 81
        if self.left_layout.count() > 0:
            left_height = mid_height - self.left_widget.height()/2
            left_width = self.left_widget.width()
            if left_width == 0:
                left_height = mid_height - self.left_widget.sizeHint().height()/2
            self.left_widget.move(text_rect.x(), left_height)
        text_rect.setX(self.left_margin)
        text_rect.setY(mid_height - self.right_widget.sizeHint().height()/2.0)
        text_rect.setHeight(self.right_widget.sizeHint().height())
        self.right_widget.setGeometry(text_rect)

    def event(self, event):
82 83
        event_type = event.type()
        if event_type == QEvent.LayoutDirectionChange:
Dan Pascu's avatar
Dan Pascu committed
84 85 86
            box_direction = QBoxLayout.RightToLeft if self.isRightToLeft() else QBoxLayout.LeftToRight
            self.left_layout.setDirection(box_direction)
            self.right_layout.setDirection(box_direction)
87 88 89 90 91 92 93 94
        elif event_type == QEvent.DynamicPropertyChange:
            property_name = event.propertyName()
            if property_name == 'widgetSpacing':
                self.left_layout.setSpacing(self.widgetSpacing)
                self.right_layout.setSpacing(self.widgetSpacing)
                self._update_text_margins()
            elif property_name == 'inactiveText':
                self.update()
Dan Pascu's avatar
Dan Pascu committed
95 96 97 98 99 100 101 102
        return QLineEdit.event(self, event)

    def resizeEvent(self, event):
        self._update_side_widget_locations()
        QLineEdit.resizeEvent(self, event)

    def paintEvent(self, event):
        QLineEdit.paintEvent(self, event)
103
        if not self.hasFocus() and not self.text() and self.inactiveText:
Dan Pascu's avatar
Dan Pascu committed
104
            options = QStyleOptionFrame()
Dan Pascu's avatar
Dan Pascu committed
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
            self.initStyleOption(options)
            text_rect = self.style().subElementRect(QStyle.SE_LineEditContents, options, self)
            text_rect.adjust(self.left_margin+2, 0, -self.right_margin, 0)
            painter = QPainter(self)
            painter.setPen(self.palette().brush(QPalette.Disabled, QPalette.Text).color())
            painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText)

    def addHeadWidget(self, widget):
        if self.isRightToLeft():
            self.right_layout.insertWidget(1, widget)
        else:
            self.left_layout.addWidget(widget)

    def addTailWidget(self, widget):
        if self.isRightToLeft():
            self.left_layout.addWidget(widget)
        else:
            self.right_layout.insertWidget(1, widget)

    def removeWidget(self, widget):
        self.left_layout.removeWidget(widget)
        self.right_layout.removeWidget(widget)
        widget.hide()


130 131 132 133 134 135 136 137 138 139 140 141 142
class ValidatingLineEdit(LineEdit):
    statusChanged = pyqtSignal()

    def __init__(self, parent=None):
        super(ValidatingLineEdit, self).__init__(parent)
        self.invalid_entry_label = QLabel(self)
        self.invalid_entry_label.setFixedSize(18, 16)
        self.invalid_entry_label.setPixmap(QPixmap(Resources.get('icons/invalid16.png')))
        self.invalid_entry_label.setScaledContents(False)
        self.invalid_entry_label.setAlignment(Qt.AlignCenter)
        self.invalid_entry_label.setObjectName('invalid_entry_label')
        self.invalid_entry_label.hide()
        self.addTailWidget(self.invalid_entry_label)
Dan Pascu's avatar
Dan Pascu committed
143
        option = QStyleOptionFrame()
144 145 146
        self.initStyleOption(option)
        frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, self)
        self.setMinimumHeight(self.invalid_entry_label.minimumHeight() + 2 + 2*frame_width)
147
        self.textChanged.connect(self._SH_TextChanged)
148 149 150
        self.text_correct = True
        self.text_allowed = True
        self.exceptions = set()
151 152 153 154 155 156 157
        self.regexp = re.compile(r'.*')

    def _get_regexp(self):
        return self.__dict__['regexp']

    def _set_regexp(self, regexp):
        self.__dict__['regexp'] = regexp
158
        self._validate()
159 160 161 162

    regexp = property(_get_regexp, _set_regexp)
    del _get_regexp, _set_regexp

163 164 165 166
    @property
    def text_valid(self):
        return self.text_correct and self.text_allowed

167 168
    def _SH_TextChanged(self, text):
        self._validate()
169

170
    def _validate(self):
171
        text = self.text()
172 173 174 175 176 177
        text_correct = self.regexp.search(text) is not None
        text_allowed = text not in self.exceptions
        if self.text_correct != text_correct or self.text_allowed != text_allowed:
            self.text_correct = text_correct
            self.text_allowed = text_allowed
            self.invalid_entry_label.setVisible(not self.text_valid)
178 179
            self.statusChanged.emit()

180 181
    def addException(self, exception):
        self.exceptions.add(exception)
182
        self._validate()
183 184 185

    def removeException(self, exception):
        self.exceptions.remove(exception)
186
        self._validate()
187

188

189 190
class SearchIcon(QWidget):
    def __init__(self, parent=None, size=16):
Dan Pascu's avatar
Dan Pascu committed
191
        super(SearchIcon, self).__init__(parent)
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        self.setFocusPolicy(Qt.NoFocus)
        self.setVisible(True)
        self.setMinimumSize(size+2, size+2)
        pixmap = QPixmap()
        if pixmap.load(Resources.get("icons/search.svg")):
            self.icon = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        else:
            self.icon = None

    def paintEvent(self, event):
        painter = QPainter(self)
        if self.icon is not None:
            x = (self.width() - self.icon.width()) / 2
            y = (self.height() - self.icon.height()) / 2
            painter.drawPixmap(x, y, self.icon)


class ClearButton(QAbstractButton):
    def __init__(self, parent=None, size=16):
Dan Pascu's avatar
Dan Pascu committed
211
        super(ClearButton, self).__init__(parent)
212 213 214 215 216 217 218 219 220 221 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
        self.setCursor(Qt.ArrowCursor)
        self.setFocusPolicy(Qt.NoFocus)
        self.setToolTip(u"Clear")
        self.setVisible(False)
        self.setMinimumSize(size+2, size+2)
        pixmap = QPixmap()
        if pixmap.load(Resources.get("icons/delete.svg")):
            self.icon = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            # Use QImage because QPainter using a QPixmap does not support CompositionMode_Multiply -Dan
            image = self.icon.toImage()
            painter = QPainter(image)
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setCompositionMode(QPainter.CompositionMode_Multiply)
            painter.drawPixmap(0, 0, self.icon)
            painter.end()
            self.icon_pressed = QPixmap(image)
        else:
            self.icon = self.icon_pressed = None

    def paintEvent(self, event):
        painter = QPainter(self)
        icon = self.icon_pressed if self.isDown() else self.icon
        if icon is not None:
            x = (self.width() - icon.width()) / 2
            y = (self.height() - icon.height()) / 2
            painter.drawPixmap(x, y, icon)
        else:
            width = self.width()
            height = self.height()

            padding = width / 5
            radius = width - 2*padding

            palette = self.palette()

            # Mid is darker than Dark. Go figure... -Dan
            bg_color = palette.color(QPalette.Mid) if self.isDown() else palette.color(QPalette.Dark)
249
            fg_color = palette.color(QPalette.Window)  # or QPalette.Base for white
250 251 252 253 254 255 256 257 258 259 260 261 262 263

            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setBrush(bg_color)
            painter.setPen(bg_color)
            painter.drawEllipse(padding, padding, radius, radius)

            padding = padding * 2
            painter.setPen(fg_color)
            painter.drawLine(padding, padding, width-padding, height-padding)
            painter.drawLine(padding, height-padding, width-padding, padding)


class SearchBox(LineEdit):
    def __init__(self, parent=None):
Dan Pascu's avatar
Dan Pascu committed
264
        super(SearchBox, self).__init__(parent=parent)
265 266 267 268
        self.search_icon = SearchIcon(self)
        self.clear_button = ClearButton(self)
        self.addHeadWidget(self.search_icon)
        self.addTailWidget(self.clear_button)
Dan Pascu's avatar
Dan Pascu committed
269
        option = QStyleOptionFrame()
270 271 272 273 274 275
        self.initStyleOption(option)
        frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, self)
        widgets_height = max(self.search_icon.minimumHeight(), self.clear_button.minimumHeight())
        self.setMinimumHeight(widgets_height + 2 + 2*frame_width)
        self.clear_button.hide()
        self.clear_button.clicked.connect(self.clear)
276
        self.textChanged.connect(self._SH_TextChanged)
277 278
        self.inactiveText = u"Search"

279 280 281 282 283 284
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.clear()
        else:
            super(SearchBox, self).keyPressEvent(event)

285
    def _SH_TextChanged(self, text):
286
        self.clear_button.setVisible(bool(text))
287 288


Dan Pascu's avatar
Dan Pascu committed
289 290 291 292
class LocationBar(LineEdit):
    locationCleared = pyqtSignal()

    def __init__(self, parent=None):
Dan Pascu's avatar
Dan Pascu committed
293
        super(LocationBar, self).__init__(parent=parent)
Dan Pascu's avatar
Dan Pascu committed
294 295
        self.clear_button = ClearButton(self)
        self.addTailWidget(self.clear_button)
Dan Pascu's avatar
Dan Pascu committed
296
        option = QStyleOptionFrame()
Dan Pascu's avatar
Dan Pascu committed
297 298 299 300 301 302 303 304 305 306 307 308 309
        self.initStyleOption(option)
        frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, self)
        widgets_height = self.clear_button.minimumHeight()
        self.setMinimumHeight(widgets_height + 2 + 2*frame_width)
        self.clear_button.hide()
        self.clear_button.clicked.connect(self._SH_ClearButtonClicked)
        self.textChanged.connect(self._SH_TextChanged)

    def _SH_ClearButtonClicked(self):
        self.clear()
        self.locationCleared.emit()

    def _SH_TextChanged(self, text):
310
        self.clear_button.setVisible(bool(text))
Dan Pascu's avatar
Dan Pascu committed
311