1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
4 SPDX-FileCopyrightText: 2017 Harald Sitter <sitter@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "kcharselect.h"
10#include "kcharselect_p.h"
11
12#include "loggingcategory.h"
13
14#include <QAction>
15#include <QActionEvent>
16#include <QApplication>
17#include <QBoxLayout>
18#include <QComboBox>
19#include <QDebug>
20#include <QDoubleSpinBox>
21#include <QFontComboBox>
22#include <QHeaderView>
23#include <QLineEdit>
24#include <QRegularExpression>
25#include <QSplitter>
26#include <QTextBrowser>
27#include <QTimer>
28#include <QToolButton>
29
30Q_GLOBAL_STATIC(KCharSelectData, s_data)
31
32class KCharSelectTablePrivate
33{
34public:
35 KCharSelectTablePrivate(KCharSelectTable *qq)
36 : q(qq)
37 {
38 }
39
40 KCharSelectTable *const q;
41
42 QFont font;
43 KCharSelectItemModel *model = nullptr;
44 QList<uint> chars;
45 uint chr = 0;
46
47 void resizeCells();
48 void doubleClicked(const QModelIndex &index);
49 void slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
50};
51
52class KCharSelectPrivate
53{
54 Q_DECLARE_TR_FUNCTIONS(KCharSelect)
55
56public:
57 struct HistoryItem {
58 uint c;
59 bool fromSearch;
60 QString searchString;
61 };
62
63 enum { MaxHistoryItems = 100 };
64
65 KCharSelectPrivate(KCharSelect *qq)
66 : q(qq)
67 {
68 }
69
70 KCharSelect *const q;
71
72 QToolButton *backButton = nullptr;
73 QToolButton *forwardButton = nullptr;
74 QLineEdit *searchLine = nullptr;
75 QFontComboBox *fontCombo = nullptr;
76 QSpinBox *fontSizeSpinBox = nullptr;
77 QComboBox *sectionCombo = nullptr;
78 QComboBox *blockCombo = nullptr;
79 KCharSelectTable *charTable = nullptr;
80 QTextBrowser *detailBrowser = nullptr;
81
82 bool searchMode = false; // a search is active
83 bool historyEnabled = false;
84 bool allPlanesEnabled = false;
85 int inHistory = 0; // index of current char in history
86 QList<HistoryItem> history;
87 QObject *actionParent = nullptr;
88
89 QString createLinks(QString s);
90 void historyAdd(uint c, bool fromSearch, const QString &searchString);
91 void showFromHistory(int index);
92 void updateBackForwardButtons();
93 void activateSearchLine();
94 void back();
95 void forward();
96 void fontSelected();
97 void charSelected(uint c);
98 void updateCurrentChar(uint c);
99 void slotUpdateUnicode(uint c);
100 void sectionSelected(int index);
101 void blockSelected(int index);
102 void searchEditChanged();
103 void search();
104 void linkClicked(QUrl url);
105};
106
107Q_DECLARE_TYPEINFO(KCharSelectPrivate::HistoryItem, Q_RELOCATABLE_TYPE);
108
109/******************************************************************/
110/* Class: KCharSelectTable */
111/******************************************************************/
112
113KCharSelectTable::KCharSelectTable(QWidget *parent, const QFont &_font)
114 : QTableView(parent)
115 , d(new KCharSelectTablePrivate(this))
116{
117 d->font = _font;
118
119 setTabKeyNavigation(false);
120 setSelectionBehavior(QAbstractItemView::SelectItems);
121 setSelectionMode(QAbstractItemView::SingleSelection);
122
123 QPalette _palette;
124 _palette.setColor(acr: backgroundRole(), acolor: palette().color(cr: QPalette::Base));
125 setPalette(_palette);
126 verticalHeader()->setVisible(false);
127 verticalHeader()->setSectionResizeMode(QHeaderView::Custom);
128 horizontalHeader()->setVisible(false);
129 horizontalHeader()->setSectionResizeMode(QHeaderView::Custom);
130
131 setFocusPolicy(Qt::StrongFocus);
132 setDragEnabled(true);
133 setAcceptDrops(true);
134 setDropIndicatorShown(false);
135 setDragDropMode(QAbstractItemView::DragDrop);
136 setTextElideMode(Qt::ElideNone);
137
138 connect(sender: this, signal: &KCharSelectTable::doubleClicked, context: this, slot: [this](const QModelIndex &index) {
139 d->doubleClicked(index);
140 });
141
142 d->resizeCells();
143}
144
145KCharSelectTable::~KCharSelectTable() = default;
146
147void KCharSelectTable::setFont(const QFont &_font)
148{
149 QTableView::setFont(_font);
150 d->font = _font;
151 if (d->model) {
152 d->model->setFont(_font);
153 }
154 d->resizeCells();
155}
156
157uint KCharSelectTable::chr()
158{
159 return d->chr;
160}
161
162QFont KCharSelectTable::font() const
163{
164 return d->font;
165}
166
167QList<uint> KCharSelectTable::displayedChars() const
168{
169 return d->chars;
170}
171
172void KCharSelectTable::setChar(uint c)
173{
174 int pos = d->chars.indexOf(t: c);
175 if (pos != -1) {
176 setCurrentIndex(model()->index(row: pos / model()->columnCount(), column: pos % model()->columnCount()));
177 }
178}
179
180void KCharSelectTable::setContents(const QList<uint> &chars)
181{
182 d->chars = chars;
183
184 auto oldModel = d->model;
185 d->model = new KCharSelectItemModel(chars, d->font, this);
186 setModel(d->model);
187 d->resizeCells();
188
189 // Setting a model changes the selectionModel. Make sure to always reconnect.
190 connect(sender: selectionModel(), signal: &QItemSelectionModel::selectionChanged, context: this, slot: [this](const QItemSelection &selected, const QItemSelection &deselected) {
191 d->slotSelectionChanged(selected, deselected);
192 });
193
194 connect(sender: d->model, signal: &KCharSelectItemModel::showCharRequested, context: this, slot: &KCharSelectTable::showCharRequested);
195
196 delete oldModel; // The selection model is thrown away when the model gets destroyed().
197}
198
199void KCharSelectTable::scrollTo(const QModelIndex &index, ScrollHint hint)
200{
201 // this prevents horizontal scrolling when selecting a character in the last column
202 if (index.isValid() && index.column() != 0) {
203 QTableView::scrollTo(index: d->model->index(row: index.row(), column: 0), hint);
204 } else {
205 QTableView::scrollTo(index, hint);
206 }
207}
208
209void KCharSelectTablePrivate::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
210{
211 Q_UNUSED(deselected);
212 if (!model || selected.indexes().isEmpty()) {
213 return;
214 }
215 QVariant temp = model->data(index: selected.indexes().at(i: 0), role: KCharSelectItemModel::CharacterRole);
216 if (temp.userType() != QMetaType::UInt) {
217 return;
218 }
219 uint c = temp.toUInt();
220 chr = c;
221 Q_EMIT q->focusItemChanged(c);
222}
223
224void KCharSelectTable::resizeEvent(QResizeEvent *e)
225{
226 QTableView::resizeEvent(event: e);
227 if (e->size().width() != e->oldSize().width()) {
228 // Resize our cells. But do so asynchronously through the event loop.
229 // Otherwise we can end up with an infinite loop as resizing the cells in turn results in
230 // a layout change which results in a resize event. More importantly doing this blockingly
231 // crashes QAccessible as the resize we potentially cause will discard objects which are
232 // still being used in the call chain leading to this event.
233 // https://bugs.kde.org/show_bug.cgi?id=374933
234 // https://bugreports.qt.io/browse/QTBUG-58153
235 // This can be removed once a fixed Qt version is the lowest requirement for Frameworks.
236 auto timer = new QTimer(this);
237 timer->setSingleShot(true);
238 connect(sender: timer, signal: &QTimer::timeout, slot: [&, timer]() {
239 d->resizeCells();
240 timer->deleteLater();
241 });
242 timer->start(msec: 0);
243 }
244}
245
246void KCharSelectTablePrivate::resizeCells()
247{
248 KCharSelectItemModel *model = static_cast<KCharSelectItemModel *>(q->model());
249 if (!model) {
250 return;
251 }
252
253 const int viewportWidth = q->viewport()->size().width();
254
255 QFontMetrics fontMetrics(font);
256
257 // Determine the max width of the displayed characters
258 // fontMetrics.maxWidth() doesn't help because of font fallbacks
259 // (testcase: Malayalam characters)
260 int maxCharWidth = 0;
261 const QList<uint> chars = model->chars();
262 for (int i = 0; i < chars.size(); ++i) {
263 char32_t thisChar = chars.at(i);
264 if (s_data()->isPrint(c: thisChar)) {
265 maxCharWidth = qMax(a: maxCharWidth, b: fontMetrics.boundingRect(text: QString::fromUcs4(&thisChar, size: 1)).width());
266 }
267 }
268 // Avoid too narrow cells
269 maxCharWidth = qMax(a: maxCharWidth, b: 2 * fontMetrics.xHeight());
270 maxCharWidth = qMax(a: maxCharWidth, b: fontMetrics.height());
271 // Add the necessary padding, trying to match the delegate
272 const int textMargin = q->style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: nullptr, widget: q) + 1;
273 maxCharWidth += 2 * textMargin;
274
275 const int columns = qMax(a: 1, b: viewportWidth / maxCharWidth);
276 model->setColumnCount(columns);
277
278 const uint oldChar = q->chr();
279
280 const int new_w = viewportWidth / columns;
281 const int rows = model->rowCount();
282 q->setUpdatesEnabled(false);
283 QHeaderView *hHeader = q->horizontalHeader();
284 hHeader->setMinimumSectionSize(new_w);
285 const int spaceLeft = viewportWidth - new_w * columns;
286 for (int i = 0; i <= columns; ++i) {
287 if (i < spaceLeft) {
288 hHeader->resizeSection(logicalIndex: i, size: new_w + 1);
289 } else {
290 hHeader->resizeSection(logicalIndex: i, size: new_w);
291 }
292 }
293
294 QHeaderView *vHeader = q->verticalHeader();
295#ifdef Q_OS_WIN
296 int new_h = fontMetrics.lineSpacing() + 1;
297#else
298 int new_h = fontMetrics.xHeight() * 3;
299#endif
300 const int fontHeight = fontMetrics.height();
301 if (new_h < 5 || new_h < 4 + fontHeight) {
302 new_h = qMax(a: 5, b: 4 + fontHeight);
303 }
304 vHeader->setMinimumSectionSize(new_h);
305 for (int i = 0; i < rows; ++i) {
306 vHeader->resizeSection(logicalIndex: i, size: new_h);
307 }
308
309 q->setUpdatesEnabled(true);
310 q->setChar(oldChar);
311}
312
313void KCharSelectTablePrivate::doubleClicked(const QModelIndex &index)
314{
315 uint c = model->data(index, role: KCharSelectItemModel::CharacterRole).toUInt();
316 if (s_data()->isPrint(c)) {
317 Q_EMIT q->activated(c);
318 }
319}
320
321void KCharSelectTable::keyPressEvent(QKeyEvent *e)
322{
323 if (d->model) {
324 switch (e->key()) {
325 case Qt::Key_Space:
326 Q_EMIT activated(c: QChar::Space);
327 return;
328 case Qt::Key_Enter:
329 case Qt::Key_Return: {
330 if (!currentIndex().isValid()) {
331 return;
332 }
333 uint c = d->model->data(index: currentIndex(), role: KCharSelectItemModel::CharacterRole).toUInt();
334 if (s_data()->isPrint(c)) {
335 Q_EMIT activated(c);
336 }
337 return;
338 }
339 default:
340 break;
341 }
342 }
343 QTableView::keyPressEvent(event: e);
344}
345
346/******************************************************************/
347/* Class: KCharSelect */
348/******************************************************************/
349
350KCharSelect::KCharSelect(QWidget *parent, const Controls controls)
351 : QWidget(parent)
352 , d(new KCharSelectPrivate(this))
353{
354 initWidget(controls, nullptr);
355}
356
357KCharSelect::KCharSelect(QWidget *parent, QObject *actionParent, const Controls controls)
358 : QWidget(parent)
359 , d(new KCharSelectPrivate(this))
360{
361 initWidget(controls, actionParent);
362}
363
364void attachToActionParent(QAction *action, QObject *actionParent, const QList<QKeySequence> &shortcuts)
365{
366 if (!action || !actionParent) {
367 return;
368 }
369
370 action->setParent(actionParent);
371
372 if (actionParent->inherits(classname: "KActionCollection")) {
373 QMetaObject::invokeMethod(obj: actionParent, member: "addAction", Q_ARG(QString, action->objectName()), Q_ARG(QAction *, action));
374 QMetaObject::invokeMethod(obj: actionParent, member: "setDefaultShortcuts", Q_ARG(QAction *, action), Q_ARG(QList<QKeySequence>, shortcuts));
375 } else {
376 action->setShortcuts(shortcuts);
377 }
378}
379
380void KCharSelect::initWidget(const Controls controls, QObject *actionParent)
381{
382 d->actionParent = actionParent;
383
384 QVBoxLayout *mainLayout = new QVBoxLayout(this);
385 mainLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
386 if (SearchLine & controls) {
387 QHBoxLayout *searchLayout = new QHBoxLayout();
388 mainLayout->addLayout(layout: searchLayout);
389 d->searchLine = new QLineEdit(this);
390 searchLayout->addWidget(d->searchLine);
391 d->searchLine->setPlaceholderText(tr(s: "Enter a search term or character...", c: "@info:placeholder"));
392 d->searchLine->setClearButtonEnabled(true);
393 d->searchLine->setToolTip(tr(s: "Enter a search term or character here", c: "@info:tooltip"));
394
395 QAction *findAction = new QAction(this);
396 connect(sender: findAction, signal: &QAction::triggered, context: this, slot: [this]() {
397 d->activateSearchLine();
398 });
399 findAction->setObjectName(QStringLiteral("edit_find"));
400 findAction->setText(tr(s: "&Find...", c: "@action"));
401 findAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
402 attachToActionParent(action: findAction, actionParent, shortcuts: QKeySequence::keyBindings(key: QKeySequence::Find));
403
404 connect(sender: d->searchLine, signal: &QLineEdit::textChanged, context: this, slot: [this]() {
405 d->searchEditChanged();
406 });
407 connect(sender: d->searchLine, signal: &QLineEdit::returnPressed, context: this, slot: [this]() {
408 d->search();
409 });
410 }
411
412 if ((SearchLine & controls) && ((FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls))) {
413 QFrame *line = new QFrame(this);
414 line->setFrameShape(QFrame::HLine);
415 line->setFrameShadow(QFrame::Sunken);
416 mainLayout->addWidget(line);
417 }
418
419 QHBoxLayout *comboLayout = new QHBoxLayout();
420
421 d->backButton = new QToolButton(this);
422 comboLayout->addWidget(d->backButton);
423 d->backButton->setEnabled(false);
424 d->backButton->setText(tr(s: "Previous in History", c: "@action:button Goes to previous character"));
425 d->backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
426 d->backButton->setToolTip(tr(s: "Go to previous character in history", c: "@info:tooltip"));
427
428 d->forwardButton = new QToolButton(this);
429 comboLayout->addWidget(d->forwardButton);
430 d->forwardButton->setEnabled(false);
431 d->forwardButton->setText(tr(s: "Next in History", c: "@action:button Goes to next character"));
432 d->forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
433 d->forwardButton->setToolTip(tr(s: "Go to next character in history", c: "info:tooltip"));
434
435 QAction *backAction = new QAction(this);
436 connect(sender: backAction, signal: &QAction::triggered, context: d->backButton, slot: &QAbstractButton::animateClick);
437 backAction->setObjectName(QStringLiteral("go_back"));
438 backAction->setText(tr(s: "&Back", c: "@action go back"));
439 backAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
440 attachToActionParent(action: backAction, actionParent, shortcuts: QKeySequence::keyBindings(key: QKeySequence::Back));
441
442 QAction *forwardAction = new QAction(this);
443 connect(sender: forwardAction, signal: &QAction::triggered, context: d->forwardButton, slot: &QAbstractButton::animateClick);
444 forwardAction->setObjectName(QStringLiteral("go_forward"));
445 forwardAction->setText(tr(s: "&Forward", c: "@action go forward"));
446 forwardAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
447 attachToActionParent(action: forwardAction, actionParent, shortcuts: QKeySequence::keyBindings(key: QKeySequence::Forward));
448
449 if (QApplication::isRightToLeft()) { // swap the back/forward icons
450 QIcon tmp = backAction->icon();
451 backAction->setIcon(forwardAction->icon());
452 forwardAction->setIcon(tmp);
453 }
454
455 connect(sender: d->backButton, signal: &QToolButton::clicked, context: this, slot: [this]() {
456 d->back();
457 });
458 connect(sender: d->forwardButton, signal: &QToolButton::clicked, context: this, slot: [this]() {
459 d->forward();
460 });
461
462 d->sectionCombo = new QComboBox(this);
463 d->sectionCombo->setObjectName(QStringLiteral("sectionCombo"));
464 d->sectionCombo->setToolTip(tr(s: "Select a category", c: "@info:tooltip"));
465 comboLayout->addWidget(d->sectionCombo);
466 d->blockCombo = new QComboBox(this);
467 d->blockCombo->setObjectName(QStringLiteral("blockCombo"));
468 d->blockCombo->setToolTip(tr(s: "Select a block to be displayed", c: "@info:tooltip"));
469 d->blockCombo->setSizePolicy(hor: QSizePolicy::MinimumExpanding, ver: QSizePolicy::Fixed);
470 comboLayout->addWidget(d->blockCombo, stretch: 1);
471 QStringList sectionList = s_data()->sectionList();
472 d->sectionCombo->addItems(texts: sectionList);
473 d->blockCombo->setMinimumWidth(QFontMetrics(QWidget::font()).averageCharWidth() * 25);
474
475 connect(sender: d->sectionCombo, signal: &QComboBox::currentIndexChanged, context: this, slot: [this](int index) {
476 d->sectionSelected(index);
477 });
478
479 connect(sender: d->blockCombo, signal: &QComboBox::currentIndexChanged, context: this, slot: [this](int index) {
480 d->blockSelected(index);
481 });
482
483 d->fontCombo = new QFontComboBox(this);
484 comboLayout->addWidget(d->fontCombo);
485 d->fontCombo->setEditable(true);
486 d->fontCombo->resize(d->fontCombo->sizeHint());
487 d->fontCombo->setToolTip(tr(s: "Set font", c: "@info:tooltip"));
488
489 d->fontSizeSpinBox = new QSpinBox(this);
490 comboLayout->addWidget(d->fontSizeSpinBox);
491 d->fontSizeSpinBox->setValue(QWidget::font().pointSize());
492 d->fontSizeSpinBox->setRange(min: 1, max: 400);
493 d->fontSizeSpinBox->setSingleStep(1);
494 d->fontSizeSpinBox->setToolTip(tr(s: "Set font size", c: "@info:tooltip"));
495
496 connect(sender: d->fontCombo, signal: &QFontComboBox::currentFontChanged, context: this, slot: [this]() {
497 d->fontSelected();
498 });
499 connect(sender: d->fontSizeSpinBox, signal: &QSpinBox::valueChanged, context: this, slot: [this]() {
500 d->fontSelected();
501 });
502
503 if ((HistoryButtons & controls) || (FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls)) {
504 mainLayout->addLayout(layout: comboLayout);
505 }
506 if (!(HistoryButtons & controls)) {
507 d->backButton->hide();
508 d->forwardButton->hide();
509 }
510 if (!(FontCombo & controls)) {
511 d->fontCombo->hide();
512 }
513 if (!(FontSize & controls)) {
514 d->fontSizeSpinBox->hide();
515 }
516 if (!(BlockCombos & controls)) {
517 d->sectionCombo->hide();
518 d->blockCombo->hide();
519 }
520
521 QSplitter *splitter = new QSplitter(this);
522 if ((CharacterTable & controls) || (DetailBrowser & controls)) {
523 mainLayout->addWidget(splitter);
524 } else {
525 splitter->hide();
526 }
527 d->charTable = new KCharSelectTable(this, QFont());
528 if (CharacterTable & controls) {
529 splitter->addWidget(widget: d->charTable);
530 } else {
531 d->charTable->hide();
532 }
533
534 const QSize sz(200, 200);
535 d->charTable->resize(sz);
536 d->charTable->setMinimumSize(sz);
537
538 d->charTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
539
540 setCurrentFont(QFont());
541
542 connect(sender: d->charTable, signal: &KCharSelectTable::focusItemChanged, context: this, slot: [this](uint c) {
543 d->updateCurrentChar(c);
544 });
545 connect(sender: d->charTable, signal: &KCharSelectTable::activated, context: this, slot: [this](uint c) {
546 d->charSelected(c);
547 });
548 connect(sender: d->charTable, signal: &KCharSelectTable::showCharRequested, context: this, slot: &KCharSelect::setCurrentCodePoint);
549
550 d->detailBrowser = new QTextBrowser(this);
551 if (DetailBrowser & controls) {
552 splitter->addWidget(widget: d->detailBrowser);
553 } else {
554 d->detailBrowser->hide();
555 }
556 d->detailBrowser->setOpenLinks(false);
557 connect(sender: d->detailBrowser, signal: &QTextBrowser::anchorClicked, context: this, slot: [this](const QUrl &url) {
558 d->linkClicked(url);
559 });
560
561 setFocusPolicy(Qt::StrongFocus);
562 if (SearchLine & controls) {
563 setFocusProxy(d->searchLine);
564 } else {
565 setFocusProxy(d->charTable);
566 }
567
568 d->sectionSelected(index: 1); // this will also call blockSelected(0)
569 setCurrentCodePoint(QChar::Null);
570
571 d->historyEnabled = true;
572}
573
574KCharSelect::~KCharSelect() = default;
575
576QSize KCharSelect::sizeHint() const
577{
578 return QWidget::sizeHint();
579}
580
581void KCharSelect::setCurrentFont(const QFont &_font)
582{
583 d->fontCombo->setCurrentFont(_font);
584 d->fontSizeSpinBox->setValue(_font.pointSize());
585 d->fontSelected();
586}
587
588void KCharSelect::setAllPlanesEnabled(bool all)
589{
590 d->allPlanesEnabled = all;
591}
592
593bool KCharSelect::allPlanesEnabled() const
594{
595 return d->allPlanesEnabled;
596}
597
598QChar KCharSelect::currentChar() const
599{
600 if (d->allPlanesEnabled) {
601 qFatal(msg: "You must use KCharSelect::currentCodePoint instead of KCharSelect::currentChar");
602 }
603 return QChar(d->charTable->chr());
604}
605
606uint KCharSelect::currentCodePoint() const
607{
608 return d->charTable->chr();
609}
610
611QFont KCharSelect::currentFont() const
612{
613 return d->charTable->font();
614}
615
616QList<QChar> KCharSelect::displayedChars() const
617{
618 if (d->allPlanesEnabled) {
619 qFatal(msg: "You must use KCharSelect::displayedCodePoints instead of KCharSelect::displayedChars");
620 }
621 QList<QChar> result;
622 const auto displayedChars = d->charTable->displayedChars();
623 result.reserve(asize: displayedChars.size());
624 for (uint c : displayedChars) {
625 result.append(t: QChar(c));
626 }
627 return result;
628}
629
630QList<uint> KCharSelect::displayedCodePoints() const
631{
632 return d->charTable->displayedChars();
633}
634
635void KCharSelect::setCurrentChar(const QChar &c)
636{
637 if (d->allPlanesEnabled) {
638 qCritical(msg: "You should use KCharSelect::setCurrentCodePoint instead of KCharSelect::setCurrentChar");
639 }
640 setCurrentCodePoint(c.unicode());
641}
642
643void KCharSelect::setCurrentCodePoint(uint c)
644{
645 if (!d->allPlanesEnabled && QChar::requiresSurrogates(ucs4: c)) {
646 qCritical(msg: "You must setAllPlanesEnabled(true) to use non-BMP characters");
647 c = QChar::ReplacementCharacter;
648 }
649 if (c > QChar::LastValidCodePoint) {
650 qCWarning(KWidgetsAddonsLog, "Code point outside Unicode range");
651 c = QChar::LastValidCodePoint;
652 }
653 bool oldHistoryEnabled = d->historyEnabled;
654 d->historyEnabled = false;
655 int block = s_data()->blockIndex(c);
656 int section = s_data()->sectionIndex(block);
657 d->sectionCombo->setCurrentIndex(section);
658 int index = d->blockCombo->findData(data: block);
659 if (index != -1) {
660 d->blockCombo->setCurrentIndex(index);
661 }
662 d->historyEnabled = oldHistoryEnabled;
663 d->charTable->setChar(c);
664}
665
666void KCharSelectPrivate::historyAdd(uint c, bool fromSearch, const QString &searchString)
667{
668 // qCDebug(KWidgetsAddonsLog) << "about to add char" << c << "fromSearch" << fromSearch << "searchString" << searchString;
669
670 if (!historyEnabled) {
671 return;
672 }
673
674 if (!history.isEmpty() && c == history.last().c) {
675 // avoid duplicates
676 return;
677 }
678
679 // behave like a web browser, i.e. if user goes back from B to A then clicks C, B is forgotten
680 while (!history.isEmpty() && inHistory != history.count() - 1) {
681 history.removeLast();
682 }
683
684 while (history.size() >= MaxHistoryItems) {
685 history.removeFirst();
686 }
687
688 HistoryItem item;
689 item.c = c;
690 item.fromSearch = fromSearch;
691 item.searchString = searchString;
692 history.append(t: item);
693
694 inHistory = history.count() - 1;
695 updateBackForwardButtons();
696}
697
698void KCharSelectPrivate::showFromHistory(int index)
699{
700 Q_ASSERT(index >= 0 && index < history.count());
701 Q_ASSERT(index != inHistory);
702
703 inHistory = index;
704 updateBackForwardButtons();
705
706 const HistoryItem &item = history[index];
707 // qCDebug(KWidgetsAddonsLog) << "index" << index << "char" << item.c << "fromSearch" << item.fromSearch
708 // << "searchString" << item.searchString;
709
710 // avoid adding an item from history into history again
711 bool oldHistoryEnabled = historyEnabled;
712 historyEnabled = false;
713 if (item.fromSearch) {
714 if (searchLine->text() != item.searchString) {
715 searchLine->setText(item.searchString);
716 search();
717 }
718 charTable->setChar(item.c);
719 } else {
720 searchLine->clear();
721 q->setCurrentCodePoint(item.c);
722 }
723 historyEnabled = oldHistoryEnabled;
724}
725
726void KCharSelectPrivate::updateBackForwardButtons()
727{
728 backButton->setEnabled(inHistory > 0);
729 forwardButton->setEnabled(inHistory < history.count() - 1);
730}
731
732void KCharSelectPrivate::activateSearchLine()
733{
734 searchLine->setFocus();
735 searchLine->selectAll();
736}
737
738void KCharSelectPrivate::back()
739{
740 Q_ASSERT(inHistory > 0);
741 showFromHistory(index: inHistory - 1);
742}
743
744void KCharSelectPrivate::forward()
745{
746 Q_ASSERT(inHistory + 1 < history.count());
747 showFromHistory(index: inHistory + 1);
748}
749
750void KCharSelectPrivate::fontSelected()
751{
752 QFont font = fontCombo->currentFont();
753 font.setPointSize(fontSizeSpinBox->value());
754 charTable->setFont(font);
755 Q_EMIT q->currentFontChanged(font);
756}
757
758void KCharSelectPrivate::charSelected(uint c)
759{
760 if (!allPlanesEnabled) {
761 Q_EMIT q->charSelected(c: QChar(c));
762 }
763 Q_EMIT q->codePointSelected(codePoint: c);
764}
765
766void KCharSelectPrivate::updateCurrentChar(uint c)
767{
768 if (!allPlanesEnabled) {
769 Q_EMIT q->currentCharChanged(c: QChar(c));
770 }
771 Q_EMIT q->currentCodePointChanged(codePoint: c);
772 if (searchMode || sectionCombo->currentIndex() == 0) {
773 // we are in search mode or all characters are shown. make the two comboboxes show the section & block for this character (only the blockCombo for the
774 // all characters mode).
775 //(when we are not in search mode nor in the all characters mode the current character always belongs to the current section & block.)
776 int block = s_data()->blockIndex(c);
777 if (searchMode) {
778 int section = s_data()->sectionIndex(block);
779 sectionCombo->setCurrentIndex(section);
780 }
781 int index = blockCombo->findData(data: block);
782 if (index != -1) {
783 blockCombo->setCurrentIndex(index);
784 }
785 }
786
787 if (searchLine) {
788 historyAdd(c, fromSearch: searchMode, searchString: searchLine->text());
789 }
790
791 slotUpdateUnicode(c);
792}
793
794void KCharSelectPrivate::slotUpdateUnicode(uint c)
795{
796 QString html = QLatin1String("<p>") + tr(sourceText: "Character:") + QLatin1Char(' ') + s_data()->display(c, font: charTable->font()) + QLatin1Char(' ')
797 + s_data()->formatCode(code: c) + QLatin1String("<br />");
798
799 QString name = s_data()->name(c);
800 if (!name.isEmpty()) {
801 // is name ever empty? </p> should always be there...
802 html += tr(sourceText: "Name: ") + name.toHtmlEscaped() + QLatin1String("</p>");
803 }
804 const QStringList aliases = s_data()->aliases(c);
805 const QStringList notes = s_data()->notes(c);
806 const QList<uint> seeAlso = s_data()->seeAlso(c);
807 const QStringList equivalents = s_data()->equivalents(c);
808 const QStringList approxEquivalents = s_data()->approximateEquivalents(c);
809 const QList<uint> decomposition = s_data()->decomposition(c);
810 if (!(aliases.isEmpty() && notes.isEmpty() && seeAlso.isEmpty() && equivalents.isEmpty() && approxEquivalents.isEmpty() && decomposition.isEmpty())) {
811 html += QLatin1String("<p><b>") + tr(sourceText: "Annotations and Cross References") + QLatin1String("</b></p>");
812 }
813
814 if (!aliases.isEmpty()) {
815 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "Alias names:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
816 for (const QString &alias : aliases) {
817 html += QLatin1String("<li>") + alias.toHtmlEscaped() + QLatin1String("</li>");
818 }
819 html += QLatin1String("</ul>");
820 }
821
822 if (!notes.isEmpty()) {
823 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "Notes:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
824 for (const QString &note : notes) {
825 html += QLatin1String("<li>") + createLinks(s: note.toHtmlEscaped()) + QLatin1String("</li>");
826 }
827 html += QLatin1String("</ul>");
828 }
829
830 if (!seeAlso.isEmpty()) {
831 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "See also:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
832 for (uint c2 : seeAlso) {
833 if (!allPlanesEnabled && QChar::requiresSurrogates(ucs4: c2)) {
834 continue;
835 }
836 html += QLatin1String("<li><a href=\"") + QString::number(c2, base: 16) + QLatin1String("\">");
837 if (s_data()->isPrint(c: c2)) {
838 html += QLatin1String("&#8206;&#") + QString::number(c2) + QLatin1String("; ");
839 }
840 html += s_data()->formatCode(code: c2) + QLatin1Char(' ') + s_data()->name(c: c2).toHtmlEscaped() + QLatin1String("</a></li>");
841 }
842 html += QLatin1String("</ul>");
843 }
844
845 if (!equivalents.isEmpty()) {
846 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "Equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
847 for (const QString &equivalent : equivalents) {
848 html += QLatin1String("<li>") + createLinks(s: equivalent.toHtmlEscaped()) + QLatin1String("</li>");
849 }
850 html += QLatin1String("</ul>");
851 }
852
853 if (!approxEquivalents.isEmpty()) {
854 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "Approximate equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
855 for (const QString &approxEquivalent : approxEquivalents) {
856 html += QLatin1String("<li>") + createLinks(s: approxEquivalent.toHtmlEscaped()) + QLatin1String("</li>");
857 }
858 html += QLatin1String("</ul>");
859 }
860
861 if (!decomposition.isEmpty()) {
862 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr(sourceText: "Decomposition:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
863 for (uint c2 : decomposition) {
864 if (!allPlanesEnabled && QChar::requiresSurrogates(ucs4: c2)) {
865 continue;
866 }
867 html += QLatin1String("<li>") + createLinks(s: s_data()->formatCode(code: c2, length: 4, prefix: QString())) + QLatin1String("</li>");
868 }
869 html += QLatin1String("</ul>");
870 }
871
872 QStringList unihan = s_data()->unihanInfo(c);
873 if (unihan.count() == 7) {
874 html += QLatin1String("<p><b>") + tr(sourceText: "CJK Ideograph Information") + QLatin1String("</b></p><p>");
875 bool newline = true;
876 if (!unihan[0].isEmpty()) {
877 html += tr(sourceText: "Definition in English: ") + unihan[0];
878 newline = false;
879 }
880 if (!unihan[2].isEmpty()) {
881 if (!newline) {
882 html += QLatin1String("<br>");
883 }
884 html += tr(sourceText: "Mandarin Pronunciation: ") + unihan[2];
885 newline = false;
886 }
887 if (!unihan[1].isEmpty()) {
888 if (!newline) {
889 html += QLatin1String("<br>");
890 }
891 html += tr(sourceText: "Cantonese Pronunciation: ") + unihan[1];
892 newline = false;
893 }
894 if (!unihan[6].isEmpty()) {
895 if (!newline) {
896 html += QLatin1String("<br>");
897 }
898 html += tr(sourceText: "Japanese On Pronunciation: ") + unihan[6];
899 newline = false;
900 }
901 if (!unihan[5].isEmpty()) {
902 if (!newline) {
903 html += QLatin1String("<br>");
904 }
905 html += tr(sourceText: "Japanese Kun Pronunciation: ") + unihan[5];
906 newline = false;
907 }
908 if (!unihan[3].isEmpty()) {
909 if (!newline) {
910 html += QLatin1String("<br>");
911 }
912 html += tr(sourceText: "Tang Pronunciation: ") + unihan[3];
913 newline = false;
914 }
915 if (!unihan[4].isEmpty()) {
916 if (!newline) {
917 html += QLatin1String("<br>");
918 }
919 html += tr(sourceText: "Korean Pronunciation: ") + unihan[4];
920 newline = false;
921 }
922 html += QLatin1String("</p>");
923 }
924
925 html += QLatin1String("<p><b>") + tr(sourceText: "General Character Properties") + QLatin1String("</b><br>");
926 html += tr(sourceText: "Block: ") + s_data()->block(c) + QLatin1String("<br>");
927 html += tr(sourceText: "Unicode category: ") + s_data()->categoryText(category: s_data()->category(c)) + QLatin1String("</p>");
928
929 const QByteArray utf8 = QString::fromUcs4(reinterpret_cast<char32_t *>(&c), size: 1).toUtf8();
930
931 html += QLatin1String("<p><b>") + tr(sourceText: "Various Useful Representations") + QLatin1String("</b><br>");
932 html += tr(sourceText: "UTF-8:");
933 for (unsigned char c : utf8) {
934 html += QLatin1Char(' ') + s_data()->formatCode(code: c, length: 2, QStringLiteral("0x"));
935 }
936 html += QLatin1String("<br>") + tr(sourceText: "UTF-16: ");
937 if (QChar::requiresSurrogates(ucs4: c)) {
938 html += s_data()->formatCode(code: QChar::highSurrogate(ucs4: c), length: 4, QStringLiteral("0x"));
939 html += QLatin1Char(' ') + s_data->formatCode(code: QChar::lowSurrogate(ucs4: c), length: 4, QStringLiteral("0x"));
940 } else {
941 html += s_data()->formatCode(code: c, length: 4, QStringLiteral("0x"));
942 }
943 html += QLatin1String("<br>") + tr(sourceText: "C octal escaped UTF-8: ");
944 for (unsigned char c : utf8) {
945 html += s_data()->formatCode(code: c, length: 3, QStringLiteral("\\"), base: 8);
946 }
947 html += QLatin1String("<br>") + tr(sourceText: "XML decimal entity:") + QLatin1String(" &amp;#") + QString::number(c) + QLatin1String(";</p>");
948
949 detailBrowser->setHtml(html);
950}
951
952QString KCharSelectPrivate::createLinks(QString s)
953{
954 static const QRegularExpression rx(QStringLiteral("\\b([\\dABCDEF]{4,5})\\b"), QRegularExpression::UseUnicodePropertiesOption);
955 QRegularExpressionMatchIterator iter = rx.globalMatch(subject: s);
956 QRegularExpressionMatch match;
957 QSet<QString> chars;
958 while (iter.hasNext()) {
959 match = iter.next();
960 chars.insert(value: match.captured(nth: 1));
961 }
962
963 for (const QString &c : std::as_const(t&: chars)) {
964 int unicode = c.toInt(ok: nullptr, base: 16);
965 if (!allPlanesEnabled && QChar::requiresSurrogates(ucs4: unicode)) {
966 continue;
967 }
968 QString link = QLatin1String("<a href=\"") + c + QLatin1String("\">");
969 if (s_data()->isPrint(c: unicode)) {
970 link += QLatin1String("&#8206;&#") + QString::number(unicode) + QLatin1String(";&nbsp;");
971 }
972 link += QLatin1String("U+") + c + QLatin1Char(' ');
973 link += s_data()->name(c: unicode).toHtmlEscaped() + QLatin1String("</a>");
974 s.replace(before: c, after: link);
975 }
976 return s;
977}
978
979void KCharSelectPrivate::sectionSelected(int index)
980{
981 blockCombo->clear();
982 QList<uint> chars;
983 const QList<int> blocks = s_data()->sectionContents(section: index);
984 for (int block : blocks) {
985 if (!allPlanesEnabled) {
986 const QList<uint> contents = s_data()->blockContents(block);
987 if (!contents.isEmpty() && QChar::requiresSurrogates(ucs4: contents.at(i: 0))) {
988 continue;
989 }
990 }
991 blockCombo->addItem(atext: s_data()->blockName(index: block), auserData: QVariant(block));
992 if (index == 0) {
993 chars << s_data()->blockContents(block);
994 }
995 }
996 if (index == 0) {
997 charTable->setContents(chars);
998 updateCurrentChar(c: charTable->chr());
999 } else {
1000 blockCombo->setCurrentIndex(0);
1001 }
1002}
1003
1004void KCharSelectPrivate::blockSelected(int index)
1005{
1006 if (index == -1) {
1007 // the combo box has been cleared and is about to be filled again (because the section has changed)
1008 return;
1009 }
1010 if (searchMode) {
1011 // we are in search mode, so don't fill the table with this block.
1012 return;
1013 }
1014 int block = blockCombo->itemData(index).toInt();
1015 if (sectionCombo->currentIndex() == 0 && block == s_data()->blockIndex(c: charTable->chr())) {
1016 // the selected block already contains the selected character
1017 return;
1018 }
1019 const QList<uint> contents = s_data()->blockContents(block);
1020 if (sectionCombo->currentIndex() > 0) {
1021 charTable->setContents(contents);
1022 }
1023 Q_EMIT q->displayedCharsChanged();
1024 charTable->setChar(contents[0]);
1025}
1026
1027void KCharSelectPrivate::searchEditChanged()
1028{
1029 if (searchLine->text().isEmpty()) {
1030 sectionCombo->setEnabled(true);
1031 blockCombo->setEnabled(true);
1032
1033 // upon leaving search mode, keep the same character selected
1034 searchMode = false;
1035 uint c = charTable->chr();
1036 bool oldHistoryEnabled = historyEnabled;
1037 historyEnabled = false;
1038 blockSelected(index: blockCombo->currentIndex());
1039 historyEnabled = oldHistoryEnabled;
1040 q->setCurrentCodePoint(c);
1041 } else {
1042 sectionCombo->setEnabled(false);
1043 blockCombo->setEnabled(false);
1044
1045 int length = searchLine->text().length();
1046 if (length >= 3) {
1047 search();
1048 }
1049 }
1050}
1051
1052void KCharSelectPrivate::search()
1053{
1054 if (searchLine->text().isEmpty()) {
1055 return;
1056 }
1057 searchMode = true;
1058 QList<uint> contents = s_data()->find(s: searchLine->text());
1059 if (!allPlanesEnabled) {
1060 contents.erase(abegin: std::remove_if(first: contents.begin(), last: contents.end(), pred: QChar::requiresSurrogates), aend: contents.end());
1061 }
1062
1063 charTable->setContents(contents);
1064 Q_EMIT q->displayedCharsChanged();
1065 if (!contents.isEmpty()) {
1066 charTable->setChar(contents[0]);
1067 }
1068}
1069
1070void KCharSelectPrivate::linkClicked(QUrl url)
1071{
1072 QString hex = url.toString();
1073 if (hex.size() > 6) {
1074 return;
1075 }
1076 int unicode = hex.toInt(ok: nullptr, base: 16);
1077 if (unicode > QChar::LastValidCodePoint) {
1078 return;
1079 }
1080 searchLine->clear();
1081 q->setCurrentCodePoint(unicode);
1082}
1083
1084////
1085
1086QVariant KCharSelectItemModel::data(const QModelIndex &index, int role) const
1087{
1088 int pos = m_columns * (index.row()) + index.column();
1089 if (!index.isValid() || pos < 0 || pos >= m_chars.size() || index.row() < 0 || index.column() < 0) {
1090 if (role == Qt::BackgroundRole) {
1091 return QVariant(qApp->palette().color(cr: QPalette::Button));
1092 }
1093 return QVariant();
1094 }
1095
1096 char32_t c = m_chars[pos];
1097 if (role == Qt::ToolTipRole) {
1098 QString result = s_data()->display(c, font: m_font) + QLatin1String("<br />") + s_data()->name(c).toHtmlEscaped() + QLatin1String("<br />")
1099 + tr(s: "Unicode code point:") + QLatin1Char(' ') + s_data()->formatCode(code: c) + QLatin1String("<br />") + tr(s: "In decimal", c: "Character")
1100 + QLatin1Char(' ') + QString::number(c);
1101 return QVariant(result);
1102 } else if (role == Qt::TextAlignmentRole) {
1103 return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
1104 } else if (role == Qt::DisplayRole) {
1105 if (s_data()->isPrint(c)) {
1106 return QVariant(QString::fromUcs4(&c, size: 1));
1107 }
1108 return QVariant();
1109 } else if (role == Qt::BackgroundRole) {
1110 QFontMetrics fm = QFontMetrics(m_font);
1111 if (fm.inFontUcs4(ucs4: c) && s_data()->isPrint(c)) {
1112 return QVariant(qApp->palette().color(cr: QPalette::Base));
1113 } else {
1114 return QVariant(qApp->palette().color(cr: QPalette::Button));
1115 }
1116 } else if (role == Qt::FontRole) {
1117 return QVariant(m_font);
1118 } else if (role == CharacterRole) {
1119 return QVariant(c);
1120 }
1121 return QVariant();
1122}
1123
1124bool KCharSelectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1125{
1126 Q_UNUSED(row)
1127 Q_UNUSED(parent)
1128 if (action == Qt::IgnoreAction) {
1129 return true;
1130 }
1131
1132 if (!data->hasText()) {
1133 return false;
1134 }
1135
1136 if (column > 0) {
1137 return false;
1138 }
1139 QString text = data->text();
1140 if (text.isEmpty()) {
1141 return false;
1142 }
1143 Q_EMIT showCharRequested(c: text.toUcs4().at(i: 0));
1144 return true;
1145}
1146
1147void KCharSelectItemModel::setColumnCount(int columns)
1148{
1149 if (columns == m_columns) {
1150 return;
1151 }
1152 Q_EMIT layoutAboutToBeChanged();
1153 m_columns = columns;
1154 Q_EMIT layoutChanged();
1155}
1156
1157#include "moc_kcharselect.cpp"
1158#include "moc_kcharselect_p.cpp"
1159

source code of kwidgetsaddons/src/kcharselect.cpp