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 ¬e : 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("‎&#") + 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(" &#") + 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("‎&#") + QString::number(unicode) + QLatin1String("; ");
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