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