KWidgetsAddons

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

KDE's Doxygen guidelines are available online.