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 { 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(backgroundRole(), palette().color(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(this, &KCharSelectTable::doubleClicked, this, [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(c);
175 if (pos != -1) {
176 setCurrentIndex(model()->index(pos / model()->columnCount(), 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(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selected, const QItemSelection &deselected) {
191 d->slotSelectionChanged(selected, deselected);
192 });
193
194 connect(d->model, &KCharSelectItemModel::showCharRequested, this, &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(d->model->index(index.row(), 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(selected.indexes().at(0), 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{
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(timer, &QTimer::timeout, [&, timer]() {
239 d->resizeCells();
240 timer->deleteLater();
241 });
242 timer->start(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(thisChar)) {
265 maxCharWidth = qMax(maxCharWidth, fontMetrics.boundingRect(QString::fromUcs4(&thisChar, 1)).width());
266 }
267 }
268 // Avoid too narrow cells
269 maxCharWidth = qMax(maxCharWidth, 2 * fontMetrics.xHeight());
270 maxCharWidth = qMax(maxCharWidth, fontMetrics.height());
271 // Add the necessary padding, trying to match the delegate
272 const int textMargin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1;
273 maxCharWidth += 2 * textMargin;
274
275 const int columns = qMax(1, 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(i, new_w + 1);
289 } else {
290 hHeader->resizeSection(i, 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(5, 4 + fontHeight);
303 }
304 vHeader->setMinimumSectionSize(new_h);
305 for (int i = 0; i < rows; ++i) {
306 vHeader->resizeSection(i, 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, 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(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(currentIndex(), KCharSelectItemModel::CharacterRole).toUInt();
334 if (s_data()->isPrint(c)) {
335 Q_EMIT activated(c);
336 }
337 return;
338 }
339 default:
340 break;
341 }
342 }
344}
345
346/******************************************************************/
347/* Class: KCharSelect */
348/******************************************************************/
349
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("KActionCollection")) {
373 QMetaObject::invokeMethod(actionParent, "addAction", Q_ARG(QString, action->objectName()), Q_ARG(QAction *, action));
374 QMetaObject::invokeMethod(actionParent, "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(0, 0, 0, 0);
386 if (SearchLine & controls) {
387 QHBoxLayout *searchLayout = new QHBoxLayout();
388 mainLayout->addLayout(searchLayout);
389 d->searchLine = new QLineEdit(this);
390 searchLayout->addWidget(d->searchLine);
391 d->searchLine->setPlaceholderText(tr("Enter a search term or character…", "@info:placeholder"));
392 d->searchLine->setClearButtonEnabled(true);
393 d->searchLine->setToolTip(tr("Enter a search term or character here", "@info:tooltip"));
394
395 QAction *findAction = new QAction(this);
396 connect(findAction, &QAction::triggered, this, [this]() {
397 d->activateSearchLine();
398 });
399 findAction->setObjectName(QStringLiteral("edit_find"));
400 findAction->setText(tr("&Find…", "@action"));
401 findAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
402 attachToActionParent(findAction, actionParent, QKeySequence::keyBindings(QKeySequence::Find));
403
404 connect(d->searchLine, &QLineEdit::textChanged, this, [this]() {
405 d->searchEditChanged();
406 });
407 connect(d->searchLine, &QLineEdit::returnPressed, this, [this]() {
408 d->search();
409 });
410 }
411
412 if ((SearchLine & controls) && ((FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls))) {
413 QFrame *line = new QFrame(this);
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("Previous in History", "@action:button Goes to previous character"));
425 d->backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
426 d->backButton->setToolTip(tr("Go to previous character in history", "@info:tooltip"));
427
428 d->forwardButton = new QToolButton(this);
429 comboLayout->addWidget(d->forwardButton);
430 d->forwardButton->setEnabled(false);
431 d->forwardButton->setText(tr("Next in History", "@action:button Goes to next character"));
432 d->forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
433 d->forwardButton->setToolTip(tr("Go to next character in history", "info:tooltip"));
434
435 QAction *backAction = new QAction(this);
436 connect(backAction, &QAction::triggered, d->backButton, &QAbstractButton::animateClick);
437 backAction->setObjectName(QStringLiteral("go_back"));
438 backAction->setText(tr("&Back", "@action go back"));
439 backAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
440 attachToActionParent(backAction, actionParent, QKeySequence::keyBindings(QKeySequence::Back));
441
442 QAction *forwardAction = new QAction(this);
443 connect(forwardAction, &QAction::triggered, d->forwardButton, &QAbstractButton::animateClick);
444 forwardAction->setObjectName(QStringLiteral("go_forward"));
445 forwardAction->setText(tr("&Forward", "@action go forward"));
446 forwardAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
447 attachToActionParent(forwardAction, actionParent, QKeySequence::keyBindings(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(d->backButton, &QToolButton::clicked, this, [this]() {
456 d->back();
457 });
458 connect(d->forwardButton, &QToolButton::clicked, this, [this]() {
459 d->forward();
460 });
461
462 d->sectionCombo = new QComboBox(this);
463 d->sectionCombo->setObjectName(QStringLiteral("sectionCombo"));
464 d->sectionCombo->setToolTip(tr("Select a category", "@info:tooltip"));
465 comboLayout->addWidget(d->sectionCombo);
466 d->blockCombo = new QComboBox(this);
467 d->blockCombo->setObjectName(QStringLiteral("blockCombo"));
468 d->blockCombo->setToolTip(tr("Select a block to be displayed", "@info:tooltip"));
469 d->blockCombo->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
470 comboLayout->addWidget(d->blockCombo, 1);
471 QStringList sectionList = s_data()->sectionList();
472 d->sectionCombo->addItems(sectionList);
473 d->blockCombo->setMinimumWidth(QFontMetrics(QWidget::font()).averageCharWidth() * 25);
474
475 connect(d->sectionCombo, &QComboBox::currentIndexChanged, this, [this](int index) {
476 d->sectionSelected(index);
477 });
478
479 connect(d->blockCombo, &QComboBox::currentIndexChanged, this, [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("Set font", "@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(1, 400);
493 d->fontSizeSpinBox->setSingleStep(1);
494 d->fontSizeSpinBox->setToolTip(tr("Set font size", "@info:tooltip"));
495
496 connect(d->fontCombo, &QFontComboBox::currentFontChanged, this, [this]() {
497 d->fontSelected();
498 });
499 connect(d->fontSizeSpinBox, &QSpinBox::valueChanged, this, [this]() {
500 d->fontSelected();
501 });
502
503 if ((HistoryButtons & controls) || (FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls)) {
504 mainLayout->addLayout(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(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
541
542 connect(d->charTable, &KCharSelectTable::focusItemChanged, this, [this](uint c) {
543 d->updateCurrentChar(c);
544 });
545 connect(d->charTable, &KCharSelectTable::activated, this, [this](uint c) {
546 d->charSelected(c);
547 });
548 connect(d->charTable, &KCharSelectTable::showCharRequested, this, &KCharSelect::setCurrentCodePoint);
549
550 d->detailBrowser = new QTextBrowser(this);
551 if (DetailBrowser & controls) {
552 splitter->addWidget(d->detailBrowser);
553 } else {
554 d->detailBrowser->hide();
555 }
556 d->detailBrowser->setOpenLinks(false);
557 connect(d->detailBrowser, &QTextBrowser::anchorClicked, this, [this](const QUrl &url) {
558 d->linkClicked(url);
559 });
560
562 if (SearchLine & controls) {
563 setFocusProxy(d->searchLine);
564 } else {
565 setFocusProxy(d->charTable);
566 }
567
568 d->sectionSelected(1); // this will also call blockSelected(0)
570
571 d->historyEnabled = true;
572}
573
574KCharSelect::~KCharSelect() = default;
575
577{
578 return QWidget::sizeHint();
579}
580
582{
583 d->fontCombo->setCurrentFont(_font);
584 d->fontSizeSpinBox->setValue(_font.pointSize());
585 d->fontSelected();
586}
587
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("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("You must use KCharSelect::displayedCodePoints instead of KCharSelect::displayedChars");
620 }
621 QList<QChar> result;
622 const auto displayedChars = d->charTable->displayedChars();
623 result.reserve(displayedChars.size());
624 for (uint c : displayedChars) {
625 result.append(QChar(c));
626 }
627 return result;
628}
629
630QList<uint> KCharSelect::displayedCodePoints() const
631{
632 return d->charTable->displayedChars();
633}
634
636{
637 if (d->allPlanesEnabled) {
638 qCritical("You should use KCharSelect::setCurrentCodePoint instead of KCharSelect::setCurrentChar");
639 }
641}
642
644{
645 if (!d->allPlanesEnabled && QChar::requiresSurrogates(c)) {
646 qCritical("You must setAllPlanesEnabled(true) to use non-BMP characters");
648 }
650 qCWarning(KWidgetsAddonsLog, "Code point outside Unicode range");
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(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(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(inHistory - 1);
742}
743
744void KCharSelectPrivate::forward()
745{
746 Q_ASSERT(inHistory + 1 < history.count());
747 showFromHistory(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(QChar(c));
762 }
763 Q_EMIT q->codePointSelected(c);
764}
765
766void KCharSelectPrivate::updateCurrentChar(uint c)
767{
768 if (!allPlanesEnabled) {
769 Q_EMIT q->currentCharChanged(QChar(c));
770 }
771 Q_EMIT q->currentCodePointChanged(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(block);
782 if (index != -1) {
783 blockCombo->setCurrentIndex(index);
784 }
785 }
786
787 if (searchLine) {
788 historyAdd(c, searchMode, searchLine->text());
789 }
790
791 slotUpdateUnicode(c);
792}
793
794void KCharSelectPrivate::slotUpdateUnicode(uint c)
795{
796 QString html = QLatin1String("<p>") + tr("Character:") + QLatin1Char(' ') + s_data()->display(c, charTable->font()) + QLatin1Char(' ')
797 + s_data()->formatCode(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("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("Annotations and Cross References") + QLatin1String("</b></p>");
812 }
813
814 if (!aliases.isEmpty()) {
815 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("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("Notes:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
824 for (const QString &note : notes) {
825 html += QLatin1String("<li>") + createLinks(note.toHtmlEscaped()) + QLatin1String("</li>");
826 }
827 html += QLatin1String("</ul>");
828 }
829
830 if (!seeAlso.isEmpty()) {
831 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("See also:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
832 for (uint c2 : seeAlso) {
833 if (!allPlanesEnabled && QChar::requiresSurrogates(c2)) {
834 continue;
835 }
836 html += QLatin1String("<li><a href=\"") + QString::number(c2, 16) + QLatin1String("\">");
837 if (s_data()->isPrint(c2)) {
838 html += QLatin1String("&#8206;&#") + QString::number(c2) + QLatin1String("; ");
839 }
840 html += s_data()->formatCode(c2) + QLatin1Char(' ') + s_data()->name(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("Equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
847 for (const QString &equivalent : equivalents) {
848 html += QLatin1String("<li>") + createLinks(equivalent.toHtmlEscaped()) + QLatin1String("</li>");
849 }
850 html += QLatin1String("</ul>");
851 }
852
853 if (!approxEquivalents.isEmpty()) {
854 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Approximate equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
855 for (const QString &approxEquivalent : approxEquivalents) {
856 html += QLatin1String("<li>") + createLinks(approxEquivalent.toHtmlEscaped()) + QLatin1String("</li>");
857 }
858 html += QLatin1String("</ul>");
859 }
860
861 if (!decomposition.isEmpty()) {
862 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Decomposition:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">");
863 for (uint c2 : decomposition) {
864 if (!allPlanesEnabled && QChar::requiresSurrogates(c2)) {
865 continue;
866 }
867 html += QLatin1String("<li>") + createLinks(s_data()->formatCode(c2, 4, 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("CJK Ideograph Information") + QLatin1String("</b></p><p>");
875 bool newline = true;
876 if (!unihan[0].isEmpty()) {
877 html += tr("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("Mandarin Pronunciation: ") + unihan[2];
885 newline = false;
886 }
887 if (!unihan[1].isEmpty()) {
888 if (!newline) {
889 html += QLatin1String("<br>");
890 }
891 html += tr("Cantonese Pronunciation: ") + unihan[1];
892 newline = false;
893 }
894 if (!unihan[6].isEmpty()) {
895 if (!newline) {
896 html += QLatin1String("<br>");
897 }
898 html += tr("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("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("Tang Pronunciation: ") + unihan[3];
913 newline = false;
914 }
915 if (!unihan[4].isEmpty()) {
916 if (!newline) {
917 html += QLatin1String("<br>");
918 }
919 html += tr("Korean Pronunciation: ") + unihan[4];
920 newline = false;
921 }
922 html += QLatin1String("</p>");
923 }
924
925 html += QLatin1String("<p><b>") + tr("General Character Properties") + QLatin1String("</b><br>");
926 html += tr("Block: ") + s_data()->block(c) + QLatin1String("<br>");
927 html += tr("Unicode category: ") + s_data()->categoryText(s_data()->category(c)) + QLatin1String("</p>");
928
929 const QByteArray utf8 = QString::fromUcs4(reinterpret_cast<char32_t *>(&c), 1).toUtf8();
930
931 html += QLatin1String("<p><b>") + tr("Various Useful Representations") + QLatin1String("</b><br>");
932 html += tr("UTF-8:");
933 for (unsigned char c : utf8) {
934 html += QLatin1Char(' ') + s_data()->formatCode(c, 2, QStringLiteral("0x"));
935 }
936 html += QLatin1String("<br>") + tr("UTF-16: ");
938 html += s_data()->formatCode(QChar::highSurrogate(c), 4, QStringLiteral("0x"));
939 html += QLatin1Char(' ') + s_data->formatCode(QChar::lowSurrogate(c), 4, QStringLiteral("0x"));
940 } else {
941 html += s_data()->formatCode(c, 4, QStringLiteral("0x"));
942 }
943 html += QLatin1String("<br>") + tr("C octal escaped UTF-8: ");
944 for (unsigned char c : utf8) {
945 html += s_data()->formatCode(c, 3, QStringLiteral("\\"), 8);
946 }
947 html += QLatin1String("<br>") + tr("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(s);
957 QSet<QString> chars;
958 while (iter.hasNext()) {
959 match = iter.next();
960 chars.insert(match.captured(1));
961 }
962
963 for (const QString &c : std::as_const(chars)) {
964 int unicode = c.toInt(nullptr, 16);
965 if (!allPlanesEnabled && QChar::requiresSurrogates(unicode)) {
966 continue;
967 }
968 QString link = QLatin1String("<a href=\"") + c + QLatin1String("\">");
969 if (s_data()->isPrint(unicode)) {
970 link += QLatin1String("&#8206;&#") + QString::number(unicode) + QLatin1String(";&nbsp;");
971 }
972 link += QLatin1String("U+") + c + QLatin1Char(' ');
973 link += s_data()->name(unicode).toHtmlEscaped() + QLatin1String("</a>");
974 s.replace(c, 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(index);
984 for (int block : blocks) {
985 if (!allPlanesEnabled) {
986 const QList<uint> contents = s_data()->blockContents(block);
987 if (!contents.isEmpty() && QChar::requiresSurrogates(contents.at(0))) {
988 continue;
989 }
990 }
991 blockCombo->addItem(s_data()->blockName(block), QVariant(block));
992 if (index == 0) {
993 chars << s_data()->blockContents(block);
994 }
995 }
996 if (index == 0) {
997 charTable->setContents(chars);
998 updateCurrentChar(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(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(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(searchLine->text());
1059 if (!allPlanesEnabled) {
1060 contents.erase(std::remove_if(contents.begin(), contents.end(), QChar::requiresSurrogates), 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(nullptr, 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(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, m_font) + QLatin1String("<br />") + s_data()->name(c).toHtmlEscaped() + QLatin1String("<br />")
1099 + tr("Unicode code point:") + QLatin1Char(' ') + s_data()->formatCode(c) + QLatin1String("<br />") + tr("In decimal", "Character")
1100 + QLatin1Char(' ') + QString::number(c);
1101 return QVariant(result);
1102 } else if (role == Qt::TextAlignmentRole) {
1104 } else if (role == Qt::DisplayRole) {
1105 if (s_data()->isPrint(c)) {
1106 return QVariant(QString::fromUcs4(&c, 1));
1107 }
1108 return QVariant();
1109 } else if (role == Qt::BackgroundRole) {
1110 QFontMetrics fm = QFontMetrics(m_font);
1111 if (fm.inFontUcs4(c) && s_data()->isPrint(c)) {
1112 return QVariant(qApp->palette().color(QPalette::Base));
1113 } else {
1114 return QVariant(qApp->palette().color(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(text.toUcs4().at(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"
Character selection widget.
Definition kcharselect.h:61
void setCurrentCodePoint(uint codePoint)
Highlights the character with the specified codePoint.
void displayedCharsChanged()
The currently displayed characters are changed (search results or block).
void setCurrentChar(const QChar &c)
Highlights the character c.
void currentCharChanged(const QChar &c)
The current character is changed.
void currentFontChanged(const QFont &font)
A new font is selected or the font size changed.
@ 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 codePointSelected(uint codePoint)
A character is selected to be inserted somewhere.
void setCurrentFont(const QFont &font)
Sets the font which is displayed to font.
void setAllPlanesEnabled(bool all)
Sets the allowed Unicode code planes.
void charSelected(const QChar &c)
A character is selected to be inserted somewhere.
void currentCodePointChanged(uint codePoint)
The current character is changed.
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 addItem(const QIcon &icon, const QString &text, const QVariant &userData)
void clear()
void currentIndexChanged(int index)
int findData(const QVariant &data, int role, Qt::MatchFlags flags) const const
QVariant itemData(int index, int role) const const
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 clear()
void returnPressed()
void selectAll()
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
T & last()
void removeFirst()
void removeLast()
void reserve(qsizetype size)
qsizetype size() const const
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool inherits(const char *className) 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)
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)
void setHtml(const QString &text)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QString toString(FormattingOptions options) const const
int toInt(bool *ok) const const
uint toUInt(bool *ok) const const
int userType() const const
void setEnabled(bool)
void setFocusPolicy(Qt::FocusPolicy policy)
void hide()
void setFocus()
void setFocusProxy(QWidget *w)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 19 2024 11:51:03 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.