Skip to content

Instantly share code, notes, and snippets.

@plu5
Last active December 11, 2022 20:35
Show Gist options
  • Select an option

  • Save plu5/a8e2a9f7ea331158a7f4c6cfe7935908 to your computer and use it in GitHub Desktop.

Select an option

Save plu5/a8e2a9f7ea331158a7f4c6cfe7935908 to your computer and use it in GitHub Desktop.
A widget for PyQt5; QPlainTextEdit subclass which mimics QLineEdit. | Adapted from ssokolow’s OneLineSpellTextEdit: https://gist.github.com/ssokolow/abb20a30415fa4debce912c38060ca6a
"""QPlainTextEdit subclass which mimics QLineEdit. The idea is to make it
hotswappable for QLineEdit with no changes needed, so that you can have a
QLineEdit without its inherent limitations, like not being able to modify the
undo behaviour, not being able to use QCursor, etc.
Adapted from ssokolow’s OneLineSpellTextEdit:
https://gist.github.com/ssokolow/abb20a30415fa4debce912c38060ca6a
NOTE:
* textEdited probably works differently from QLineEdit’s textEdited, because I
am not sure yet how to replicate it exactly. For now it emits when the text
changes by user key press.
* Unimplemented signals: editingFinished, inputRejected"""
import sys
from qt import (QPlainTextEdit, QWidget, QSize, QSizePolicy, QKeySequence,
pyqtSlot, pyqtSignal, QFocusEvent, QShortcut, QTextOption,
QTextCursor, QStyle, Qt, QStyleOptionFrame, QFontMetricsF,
QVBoxLayout,
# for the demo only:
QApplication, QLineEdit)
class LineEdit(QWidget):
returnPressed = pyqtSignal()
textChanged = pyqtSignal(str)
textEdited = pyqtSignal(str)
selectionChanged = pyqtSignal()
cursorPositionChanged = pyqtSignal(int, int)
def __init__(self, parent=None, *args):
super().__init__(parent)
self.edit = _LineEdit(self, *args)
self.edit.textChanged.connect(self._emitTextChanged)
self.edit.returnPressed.connect(self.returnPressed)
self.edit.selectionChanged.connect(self.selectionChanged)
self.edit.cursorPositionChanged.connect(
self._emitCursorPositionChanged)
self._oldPos = self.edit.textCursor().position()
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.edit)
def setAccessibleName(self, name):
self.edit.setAccessibleName(name)
def keyPressEvent(self, e):
self.edit.keyPressEvent(e)
def text(self):
return self.edit.toPlainText()
def setText(self, text):
self.edit.setPlainText(text)
def _emitTextChanged(self):
text = self.edit.toPlainText()
self.textChanged.emit(text)
if self.edit._isUserModified:
self.textEdited.emit(text)
self.edit._isUserModified = False
def _emitCursorPositionChanged(self):
newPos = self.edit.textCursor().position()
self.cursorPositionChanged.emit(self._oldPos, newPos)
self._oldPos = newPos
class _LineEdit(QPlainTextEdit):
returnPressed = pyqtSignal()
def __init__(self, wrapper, *args):
super().__init__(*args)
# Set up expected Insert/Overwrite cursor mode toggle
QShortcut(
QKeySequence(
self.tr("Insert", "Hotkey to toggle Insert/Overwrite")),
self, self.cb_toggle_insert, context=Qt.WidgetShortcut)
# Set up QPlainTextEdit to act like QLineEdit
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setWordWrapMode(QTextOption.NoWrap)
self.setTabChangesFocus(True)
self.textChanged.connect(self.cb_text_changed)
self.wrapper = wrapper
self._isUserModified = False # used for textEdited signal logic
def focusInEvent(self, e: QFocusEvent):
"""Override focusInEvent to mimic QLineEdit behaviour"""
super().focusInEvent(e)
# TODO: Are there any other things I'm supposed to be checking for?
if e.reason() in (Qt.BacktabFocusReason, Qt.ShortcutFocusReason,
Qt.TabFocusReason):
self.selectAll()
def focusOutEvent(self, e: QFocusEvent):
"""Override focusOutEvent to mimic QLineEdit behaviour"""
super().focusOutEvent(e)
# TODO: Are there any other things I'm supposed to be checking for?
if e.reason() in (Qt.BacktabFocusReason, Qt.MouseFocusReason,
Qt.ShortcutFocusReason, Qt.TabFocusReason):
# De-select everything and move the cursor to the end
cur = self.textCursor()
cur.movePosition(QTextCursor.End)
self.setTextCursor(cur)
def minimumSizeHint(self):
"""Redefine minimum size hint to match QLineEdit"""
block_fmt = self.document().firstBlock().blockFormat()
width = super().minimumSizeHint().width()
height = int(
QFontMetricsF(self.font()).lineSpacing() + # noqa
block_fmt.topMargin() + block_fmt.bottomMargin() + # noqa
self.document().documentMargin() + # noqa
2 * self.frameWidth()
)
style_opts = QStyleOptionFrame()
style_opts.initFrom(self)
style_opts.lineWidth = self.frameWidth()
# TODO: Is it correct that I'm achieving the correct content height
# under test conditions by feeding self.frameWidth() to both
# QStyleOptionFrame.lineWidth and the sizeFromContents height
# calculation?
return self.style().sizeFromContents(
QStyle.CT_LineEdit,
style_opts,
QSize(width, height),
self
)
def sizeHint(self):
"""Reuse minimumSizeHint for sizeHint"""
return self.minimumSizeHint()
@pyqtSlot()
def cb_text_changed(self):
"""Handler to enforce one-linedness on typing/pastes/etc.
(Can't use self.setMaximumBlockCount(1) because it disables Undo/Redo)
Feel free to subclass and override this for alternative behaviours
such as converting a newline-separated list into a comma-separated list
"""
if self.document().blockCount() > 1:
self.document().setPlainText(
self.document().firstBlock().text())
@pyqtSlot()
def cb_toggle_insert(self, target: bool = None):
"""Event handler for the Insert key"""
if target is None:
target = not self.overwriteMode()
self.setOverwriteMode(target)
self.overtype_changed.emit(target)
def keyPressEvent(self, e):
if e.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
self.returnPressed.emit()
if e.text():
self._isUserModified = True
super().keyPressEvent(e)
# demo
if __name__ == '__main__':
app = QApplication(sys.argv)
win = QWidget()
layout = QVBoxLayout()
lineEdit = LineEdit()
lineEdit.setPlainText("LineEdit")
le = QLineEdit()
le.setText("QLineEdit")
print(lineEdit.sizeHint(), lineEdit.minimumSizeHint())
print(le.sizeHint(), le.minimumSizeHint())
layout.addWidget(lineEdit)
layout.addWidget(le)
win.setLayout(layout)
win.show()
sys.exit(app.exec_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment