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