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