Add QElidingLabel (#16)

* wip

* single class implementation

* fix init

* improve implementation

* improve sizeHint

* wrap

* update docs

* rename

* remove overloads

* review changes

* docs and reformat

* remove width from _elided text

* add tests
This commit is contained in:
Talley Lambert
2021-08-17 11:03:57 -04:00
committed by GitHub
parent 939c5222af
commit ba20665d57
4 changed files with 192 additions and 0 deletions

12
examples/eliding_label.py Normal file
View File

@@ -0,0 +1,12 @@
from superqt import QElidingLabel
from superqt.qtcompat.QtWidgets import QApplication
app = QApplication([])
widget = QElidingLabel(
"a skj skjfskfj sdlf sdfl sdlfk jsdf sdlkf jdsf dslfksdl sdlfk sdf sdl "
"fjsdlf kjsdlfk laskdfsal as lsdfjdsl kfjdslf asfd dslkjfldskf sdlkfj"
)
widget.setWordWrap(True)
widget.show()
app.exec_()

View File

@@ -5,6 +5,7 @@ except ImportError:
__version__ = "unknown"
from ._eliding_label import QElidingLabel
from .combobox import QEnumComboBox
from .sliders import (
QDoubleRangeSlider,
@@ -20,6 +21,7 @@ from .spinbox import QLargeIntSpinBox
__all__ = [
"QDoubleRangeSlider",
"QDoubleSlider",
"QElidingLabel",
"QLabeledDoubleRangeSlider",
"QLabeledDoubleSlider",
"QLabeledRangeSlider",

110
superqt/_eliding_label.py Normal file
View File

@@ -0,0 +1,110 @@
from typing import List
from superqt.qtcompat.QtCore import QPoint, QRect, QSize, Qt
from superqt.qtcompat.QtGui import QFont, QFontMetrics, QResizeEvent, QTextLayout
from superqt.qtcompat.QtWidgets import QLabel
class QElidingLabel(QLabel):
"""A QLabel variant that will elide text (add '') to fit width.
QElidingLabel()
QElidingLabel(parent: Optional[QWidget], f: Qt.WindowFlags = ...)
QElidingLabel(text: str, parent: Optional[QWidget] = None, f: Qt.WindowFlags = ...)
For a multiline eliding label, use `setWordWrap(True)`. In this case, text
will wrap to fit the width, and only the last line will be elided.
When `wordWrap()` is True, `sizeHint()` will return the size required to fit
the full text.
"""
def __init__(self, *args, **kwargs) -> None:
self._elide_mode = Qt.TextElideMode.ElideRight
super().__init__(*args, **kwargs)
self.setText(args[0] if args and isinstance(args[0], str) else "")
# New Public methods
def elideMode(self) -> Qt.TextElideMode:
"""The current Qt.TextElideMode."""
return self._elide_mode
def setElideMode(self, mode: Qt.TextElideMode):
"""Set the elide mode to a Qt.TextElideMode."""
self._elide_mode = Qt.TextElideMode(mode)
super().setText(self._elidedText())
@staticmethod
def wrapText(text, width, font=None) -> List[str]:
"""Returns `text`, split as it would be wrapped for `width`, given `font`.
Static method.
"""
tl = QTextLayout(text, font or QFont())
tl.beginLayout()
lines = []
while True:
ln = tl.createLine()
if not ln.isValid():
break
ln.setLineWidth(width)
start = ln.textStart()
lines.append(text[start : start + ln.textLength()])
tl.endLayout()
return lines
# Reimplemented QT methods
def text(self) -> str:
"""This property holds the label's text.
If no text has been set this will return an empty string.
"""
return self._text
def setText(self, txt: str):
"""Set the label's text.
Setting the text clears any previous content.
NOTE: we set the QLabel private text to the elided version
"""
self._text = txt
super().setText(self._elidedText())
def resizeEvent(self, ev: QResizeEvent) -> None:
ev.accept()
super().setText(self._elidedText())
def setWordWrap(self, wrap: bool) -> None:
super().setWordWrap(wrap)
super().setText(self._elidedText())
def sizeHint(self) -> QSize:
if not self.wordWrap():
return super().sizeHint()
fm = QFontMetrics(self.font())
flags = self.alignment() | Qt.TextFlag.TextWordWrap
r = fm.boundingRect(QRect(QPoint(0, 0), self.size()), flags, self._text)
return QSize(self.width(), r.height())
# private implementation methods
def _elidedText(self) -> str:
"""Return `self._text` elided to `width`"""
fm = QFontMetrics(self.font())
# the 2 is a magic number that prevents the ellipses from going missing
# in certain cases (?)
width = self.width() - 2
if not self.wordWrap():
return fm.elidedText(self._text, self._elide_mode, width)
# get number of lines we can fit without eliding
nlines = self.height() // fm.height() - 1
# get the last line (elided)
text = self._wrappedText()
last_line = fm.elidedText("".join(text[nlines:]), self._elide_mode, width)
# join them
return "".join(text[:nlines] + [last_line])
def _wrappedText(self) -> List[str]:
return QElidingLabel.wrapText(self._text, self.width(), self.font())

View File

@@ -0,0 +1,68 @@
from superqt import QElidingLabel
from superqt.qtcompat.QtCore import QSize, Qt
from superqt.qtcompat.QtGui import QResizeEvent
TEXT = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
"minim ven iam, quis nostrud exercitation ullamco laborisnisi ut aliquip "
"ex ea commodo consequat. Duis aute irure dolor inreprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nullapariatur."
)
ELLIPSIS = ""
def test_eliding_label(qtbot):
wdg = QElidingLabel(TEXT)
qtbot.addWidget(wdg)
assert wdg._elidedText().endswith(ELLIPSIS)
oldsize = wdg.size()
newsize = QSize(200, 20)
wdg.resize(newsize)
wdg.resizeEvent(QResizeEvent(oldsize, newsize)) # for test coverage
assert wdg.text() == TEXT
def test_wrapped_eliding_label(qtbot):
wdg = QElidingLabel(TEXT)
qtbot.addWidget(wdg)
assert not wdg.wordWrap()
assert wdg.sizeHint() == QSize(633, 16)
assert wdg._elidedText() == (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
"eiusmod tempor incididunt ut labore et d…"
)
wdg.resize(QSize(200, 100))
assert wdg.text() == TEXT
assert wdg._elidedText() == "Lorem ipsum dolor sit amet, co…"
wdg.setWordWrap(True)
assert wdg.wordWrap()
assert wdg.text() == TEXT
assert wdg._elidedText() == (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
"eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim ven iam, quis nostrud exercitation ullamco la…"
)
assert wdg.sizeHint() == QSize(200, 176)
wdg.resize(wdg.sizeHint())
assert wdg._elidedText() == TEXT
def test_shorter_eliding_label(qtbot):
short = "asd a ads sd flksdf dsf lksfj sd lsdjf sd lsdfk sdlkfj s"
wdg = QElidingLabel()
qtbot.addWidget(wdg)
wdg.setText(short)
assert not wdg._elidedText().endswith(ELLIPSIS)
wdg.resize(100, 20)
assert wdg._elidedText().endswith(ELLIPSIS)
wdg.setElideMode(Qt.TextElideMode.ElideLeft)
assert wdg._elidedText().startswith(ELLIPSIS)
assert wdg.elideMode() == Qt.TextElideMode.ElideLeft
def test_wrap_text():
wrap = QElidingLabel.wrapText(TEXT, 200)
assert isinstance(wrap, list)
assert all(isinstance(x, str) for x in wrap)
assert len(wrap) == 11