| 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 | |