1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Ronny Standtke <Ronny.Standtke@gmx.de>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "ksqueezedtextlabel.h"
9#include <QAction>
10#include <QApplication>
11#include <QClipboard>
12#include <QContextMenuEvent>
13#include <QMenu>
14#include <QRegularExpression>
15#include <QScreen>
16#include <QTextDocument>
17
18class KSqueezedTextLabelPrivate
19{
20public:
21 void copyFullText()
22 {
23 QApplication::clipboard()->setText(fullText);
24 }
25
26 QString fullText;
27 Qt::TextElideMode elideMode;
28};
29
30KSqueezedTextLabel::KSqueezedTextLabel(const QString &text, QWidget *parent)
31 : QLabel(parent)
32 , d(new KSqueezedTextLabelPrivate)
33{
34 setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
35 d->fullText = text;
36 d->elideMode = Qt::ElideMiddle;
37 squeezeTextToLabel();
38}
39
40KSqueezedTextLabel::KSqueezedTextLabel(QWidget *parent)
41 : QLabel(parent)
42 , d(new KSqueezedTextLabelPrivate)
43{
44 setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
45 d->elideMode = Qt::ElideMiddle;
46}
47
48KSqueezedTextLabel::~KSqueezedTextLabel() = default;
49
50void KSqueezedTextLabel::resizeEvent(QResizeEvent *)
51{
52 squeezeTextToLabel();
53}
54
55QSize KSqueezedTextLabel::minimumSizeHint() const
56{
57 QSize sh = QLabel::minimumSizeHint();
58 sh.setWidth(-1);
59 return sh;
60}
61
62QSize KSqueezedTextLabel::sizeHint() const
63{
64 if (!isSqueezed()) {
65 return QLabel::sizeHint();
66 }
67 int maxWidth = screen()->geometry().width() * 3 / 4;
68 QFontMetrics fm(fontMetrics());
69 // Do exactly like qlabel.cpp to avoid slight differences in results
70 // (see https://invent.kde.org/frameworks/kwidgetsaddons/-/merge_requests/100)
71 int textWidth = fm.boundingRect(x: 0, y: 0, w: 2000, h: 2000, flags: Qt::AlignAbsolute | Qt::TextExpandTabs | Qt::AlignLeft, text: d->fullText).width();
72 if (textWidth > maxWidth) {
73 textWidth = maxWidth;
74 }
75 const int chromeWidth = width() - contentsRect().width();
76 return QSize(textWidth + chromeWidth, QLabel::sizeHint().height());
77}
78
79void KSqueezedTextLabel::setIndent(int indent)
80{
81 QLabel::setIndent(indent);
82 squeezeTextToLabel();
83}
84
85void KSqueezedTextLabel::setMargin(int margin)
86{
87 QLabel::setMargin(margin);
88 squeezeTextToLabel();
89}
90
91void KSqueezedTextLabel::setText(const QString &text)
92{
93 d->fullText = text;
94 squeezeTextToLabel();
95}
96
97void KSqueezedTextLabel::clear()
98{
99 d->fullText.clear();
100 QLabel::clear();
101}
102
103void KSqueezedTextLabel::squeezeTextToLabel()
104{
105 QFontMetrics fm(fontMetrics());
106 const int labelWidth = contentsRect().width();
107 QStringList squeezedLines;
108 bool squeezed = false;
109 const auto textLines = d->fullText.split(sep: QLatin1Char('\n'));
110 squeezedLines.reserve(asize: textLines.size());
111 for (const QString &line : textLines) {
112 int lineWidth = fm.boundingRect(text: line).width();
113 if (lineWidth > labelWidth) {
114 squeezed = true;
115 squeezedLines << fm.elidedText(text: line, mode: d->elideMode, width: labelWidth);
116 } else {
117 squeezedLines << line;
118 }
119 }
120
121 if (squeezed) {
122 QLabel::setText(squeezedLines.join(sep: QLatin1Char('\n')));
123 setToolTip(d->fullText);
124 } else {
125 QLabel::setText(d->fullText);
126 setToolTip(QString());
127 }
128}
129
130QRect KSqueezedTextLabel::contentsRect() const
131{
132 // calculation according to API docs for QLabel::indent
133 const int margin = this->margin();
134 int indent = this->indent();
135 if (indent < 0) {
136 if (frameWidth() == 0) {
137 indent = 0;
138 } else {
139 indent = fontMetrics().horizontalAdvance(QLatin1Char('x')) / 2 - margin;
140 }
141 }
142
143 QRect result = QLabel::contentsRect();
144 if (indent > 0) {
145 const int alignment = this->alignment();
146 if (alignment & Qt::AlignLeft) {
147 result.setLeft(result.left() + indent);
148 }
149 if (alignment & Qt::AlignTop) {
150 result.setTop(result.top() + indent);
151 }
152 if (alignment & Qt::AlignRight) {
153 result.setRight(result.right() - indent);
154 }
155 if (alignment & Qt::AlignBottom) {
156 result.setBottom(result.bottom() - indent);
157 }
158 }
159
160 result.adjust(dx1: margin, dy1: margin, dx2: -margin, dy2: -margin);
161 return result;
162}
163
164void KSqueezedTextLabel::setAlignment(Qt::Alignment alignment)
165{
166 // save fullText and restore it
167 QString tmpFull(d->fullText);
168 QLabel::setAlignment(alignment);
169 d->fullText = tmpFull;
170}
171
172Qt::TextElideMode KSqueezedTextLabel::textElideMode() const
173{
174 return d->elideMode;
175}
176
177void KSqueezedTextLabel::setTextElideMode(Qt::TextElideMode mode)
178{
179 d->elideMode = mode;
180 squeezeTextToLabel();
181}
182
183QString KSqueezedTextLabel::fullText() const
184{
185 return d->fullText;
186}
187
188bool KSqueezedTextLabel::isSqueezed() const
189{
190 return d->fullText != text();
191}
192
193void KSqueezedTextLabel::contextMenuEvent(QContextMenuEvent *ev)
194{
195 // We want to reimplement "Copy" to include the elided text.
196 // But this means reimplementing the full popup menu, so no more
197 // copy-link-address or copy-selection support anymore, since we
198 // have no access to the QTextDocument.
199 // Maybe we should have a boolean flag in KSqueezedTextLabel itself for
200 // whether to show the "Copy Full Text" custom popup?
201 // For now I chose to show it when the text is squeezed; when it's not, the
202 // standard popup menu can do the job (select all, copy).
203
204 if (isSqueezed()) {
205 QMenu menu(this);
206
207 QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr(s: "&Copy Full Text", c: "@action:inmenu"), &menu);
208 connect(sender: act, signal: &QAction::triggered, context: this, slot: [this]() {
209 d->copyFullText();
210 });
211 menu.addAction(action: act);
212
213 ev->accept();
214 menu.exec(pos: ev->globalPos());
215 } else {
216 QLabel::contextMenuEvent(ev);
217 }
218}
219
220void KSqueezedTextLabel::mouseReleaseEvent(QMouseEvent *ev)
221{
222 if (QApplication::clipboard()->supportsSelection() //
223 && textInteractionFlags() != Qt::NoTextInteraction //
224 && ev->button() == Qt::LeftButton //
225 && !d->fullText.isEmpty() //
226 && hasSelectedText()) {
227 // Expand "..." when selecting with the mouse
228 QString txt = selectedText();
229 const QChar ellipsisChar(0x2026); // from qtextengine.cpp
230 const int dotsPos = txt.indexOf(c: ellipsisChar);
231 if (dotsPos > -1) {
232 // Ex: abcde...yz, selecting de...y (selectionStart=3)
233 // charsBeforeSelection = selectionStart = 2 (ab)
234 // charsAfterSelection = 1 (z)
235 // final selection length= 26 - 2 - 1 = 23
236 const int start = selectionStart();
237 int charsAfterSelection = text().length() - start - selectedText().length();
238 txt = d->fullText;
239 // Strip markup tags
240 if (textFormat() == Qt::RichText //
241 || (textFormat() == Qt::AutoText && Qt::mightBeRichText(txt))) {
242 txt.remove(re: QRegularExpression(QStringLiteral("<[^>]*>")));
243 // account for stripped characters
244 charsAfterSelection -= d->fullText.length() - txt.length();
245 }
246 txt = txt.mid(position: selectionStart(), n: txt.length() - start - charsAfterSelection);
247 }
248 QApplication::clipboard()->setText(txt, mode: QClipboard::Selection);
249 } else {
250 QLabel::mouseReleaseEvent(ev);
251 }
252}
253
254#include "moc_ksqueezedtextlabel.cpp"
255

source code of kwidgetsaddons/src/ksqueezedtextlabel.cpp