File indexing completed on 2025-02-16 13:11:37
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org> 0004 SPDX-FileCopyrightText: 2017 Harald Sitter <sitter@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "kcharselect.h" 0010 #include "kcharselect_p.h" 0011 0012 #include "loggingcategory.h" 0013 0014 #include <QAction> 0015 #include <QActionEvent> 0016 #include <QApplication> 0017 #include <QBoxLayout> 0018 #include <QComboBox> 0019 #include <QDebug> 0020 #include <QDoubleSpinBox> 0021 #include <QFontComboBox> 0022 #include <QHeaderView> 0023 #include <QLineEdit> 0024 #include <QRegularExpression> 0025 #include <QSplitter> 0026 #include <QTextBrowser> 0027 #include <QTimer> 0028 #include <QToolButton> 0029 0030 Q_GLOBAL_STATIC(KCharSelectData, s_data) 0031 0032 class KCharSelectTablePrivate 0033 { 0034 public: 0035 KCharSelectTablePrivate(KCharSelectTable *qq) 0036 : q(qq) 0037 { 0038 } 0039 0040 KCharSelectTable *const q; 0041 0042 QFont font; 0043 KCharSelectItemModel *model = nullptr; 0044 QVector<uint> chars; 0045 uint chr = 0; 0046 0047 void resizeCells(); 0048 void doubleClicked(const QModelIndex &index); 0049 void slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); 0050 }; 0051 0052 class KCharSelectPrivate 0053 { 0054 Q_DECLARE_TR_FUNCTIONS(KCharSelect) 0055 0056 public: 0057 struct HistoryItem { 0058 uint c; 0059 bool fromSearch; 0060 QString searchString; 0061 }; 0062 0063 enum { MaxHistoryItems = 100 }; 0064 0065 KCharSelectPrivate(KCharSelect *qq) 0066 : q(qq) 0067 { 0068 } 0069 0070 KCharSelect *const q; 0071 0072 QToolButton *backButton = nullptr; 0073 QToolButton *forwardButton = nullptr; 0074 QLineEdit *searchLine = nullptr; 0075 QFontComboBox *fontCombo = nullptr; 0076 QSpinBox *fontSizeSpinBox = nullptr; 0077 QComboBox *sectionCombo = nullptr; 0078 QComboBox *blockCombo = nullptr; 0079 KCharSelectTable *charTable = nullptr; 0080 QTextBrowser *detailBrowser = nullptr; 0081 0082 bool searchMode = false; // a search is active 0083 bool historyEnabled = false; 0084 bool allPlanesEnabled = false; 0085 int inHistory = 0; // index of current char in history 0086 QList<HistoryItem> history; 0087 QObject *actionParent = nullptr; 0088 0089 QString createLinks(QString s); 0090 void historyAdd(uint c, bool fromSearch, const QString &searchString); 0091 void showFromHistory(int index); 0092 void updateBackForwardButtons(); 0093 void activateSearchLine(); 0094 void back(); 0095 void forward(); 0096 void fontSelected(); 0097 void charSelected(uint c); 0098 void updateCurrentChar(uint c); 0099 void slotUpdateUnicode(uint c); 0100 void sectionSelected(int index); 0101 void blockSelected(int index); 0102 void searchEditChanged(); 0103 void search(); 0104 void linkClicked(QUrl url); 0105 }; 0106 0107 Q_DECLARE_TYPEINFO(KCharSelectPrivate::HistoryItem, Q_MOVABLE_TYPE); 0108 0109 /******************************************************************/ 0110 /* Class: KCharSelectTable */ 0111 /******************************************************************/ 0112 0113 KCharSelectTable::KCharSelectTable(QWidget *parent, const QFont &_font) 0114 : QTableView(parent) 0115 , d(new KCharSelectTablePrivate(this)) 0116 { 0117 d->font = _font; 0118 0119 setTabKeyNavigation(false); 0120 setSelectionBehavior(QAbstractItemView::SelectItems); 0121 setSelectionMode(QAbstractItemView::SingleSelection); 0122 0123 QPalette _palette; 0124 _palette.setColor(backgroundRole(), palette().color(QPalette::Base)); 0125 setPalette(_palette); 0126 verticalHeader()->setVisible(false); 0127 verticalHeader()->setSectionResizeMode(QHeaderView::Custom); 0128 horizontalHeader()->setVisible(false); 0129 horizontalHeader()->setSectionResizeMode(QHeaderView::Custom); 0130 0131 setFocusPolicy(Qt::StrongFocus); 0132 setDragEnabled(true); 0133 setAcceptDrops(true); 0134 setDropIndicatorShown(false); 0135 setDragDropMode(QAbstractItemView::DragDrop); 0136 setTextElideMode(Qt::ElideNone); 0137 0138 connect(this, &KCharSelectTable::doubleClicked, this, [this](const QModelIndex &index) { 0139 d->doubleClicked(index); 0140 }); 0141 0142 d->resizeCells(); 0143 } 0144 0145 KCharSelectTable::~KCharSelectTable() = default; 0146 0147 void KCharSelectTable::setFont(const QFont &_font) 0148 { 0149 QTableView::setFont(_font); 0150 d->font = _font; 0151 if (d->model) { 0152 d->model->setFont(_font); 0153 } 0154 d->resizeCells(); 0155 } 0156 0157 uint KCharSelectTable::chr() 0158 { 0159 return d->chr; 0160 } 0161 0162 QFont KCharSelectTable::font() const 0163 { 0164 return d->font; 0165 } 0166 0167 QVector<uint> KCharSelectTable::displayedChars() const 0168 { 0169 return d->chars; 0170 } 0171 0172 void KCharSelectTable::setChar(uint c) 0173 { 0174 int pos = d->chars.indexOf(c); 0175 if (pos != -1) { 0176 setCurrentIndex(model()->index(pos / model()->columnCount(), pos % model()->columnCount())); 0177 } 0178 } 0179 0180 void KCharSelectTable::setContents(const QVector<uint> &chars) 0181 { 0182 d->chars = chars; 0183 0184 auto oldModel = d->model; 0185 d->model = new KCharSelectItemModel(chars, d->font, this); 0186 setModel(d->model); 0187 d->resizeCells(); 0188 0189 // Setting a model changes the selectionModel. Make sure to always reconnect. 0190 connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selected, const QItemSelection &deselected) { 0191 d->slotSelectionChanged(selected, deselected); 0192 }); 0193 0194 connect(d->model, &KCharSelectItemModel::showCharRequested, this, &KCharSelectTable::showCharRequested); 0195 0196 delete oldModel; // The selection model is thrown away when the model gets destroyed(). 0197 } 0198 0199 void KCharSelectTable::scrollTo(const QModelIndex &index, ScrollHint hint) 0200 { 0201 // this prevents horizontal scrolling when selecting a character in the last column 0202 if (index.isValid() && index.column() != 0) { 0203 QTableView::scrollTo(d->model->index(index.row(), 0), hint); 0204 } else { 0205 QTableView::scrollTo(index, hint); 0206 } 0207 } 0208 0209 void KCharSelectTablePrivate::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 0210 { 0211 Q_UNUSED(deselected); 0212 if (!model || selected.indexes().isEmpty()) { 0213 return; 0214 } 0215 QVariant temp = model->data(selected.indexes().at(0), KCharSelectItemModel::CharacterRole); 0216 if (temp.type() != QVariant::UInt) { 0217 return; 0218 } 0219 uint c = temp.toUInt(); 0220 chr = c; 0221 Q_EMIT q->focusItemChanged(c); 0222 } 0223 0224 void KCharSelectTable::resizeEvent(QResizeEvent *e) 0225 { 0226 QTableView::resizeEvent(e); 0227 if (e->size().width() != e->oldSize().width()) { 0228 // Resize our cells. But do so asynchronously through the event loop. 0229 // Otherwise we can end up with an infinite loop as resizing the cells in turn results in 0230 // a layout change which results in a resize event. More importantly doing this blockingly 0231 // crashes QAccessible as the resize we potentially cause will discard objects which are 0232 // still being used in the call chain leading to this event. 0233 // https://bugs.kde.org/show_bug.cgi?id=374933 0234 // https://bugreports.qt.io/browse/QTBUG-58153 0235 // This can be removed once a fixed Qt version is the lowest requirement for Frameworks. 0236 auto timer = new QTimer(this); 0237 timer->setSingleShot(true); 0238 connect(timer, &QTimer::timeout, [&, timer]() { 0239 d->resizeCells(); 0240 timer->deleteLater(); 0241 }); 0242 timer->start(0); 0243 } 0244 } 0245 0246 void KCharSelectTablePrivate::resizeCells() 0247 { 0248 KCharSelectItemModel *model = static_cast<KCharSelectItemModel *>(q->model()); 0249 if (!model) { 0250 return; 0251 } 0252 0253 const int viewportWidth = q->viewport()->size().width(); 0254 0255 QFontMetrics fontMetrics(font); 0256 0257 // Determine the max width of the displayed characters 0258 // fontMetrics.maxWidth() doesn't help because of font fallbacks 0259 // (testcase: Malayalam characters) 0260 int maxCharWidth = 0; 0261 const QVector<uint> chars = model->chars(); 0262 for (int i = 0; i < chars.size(); ++i) { 0263 uint thisChar = chars.at(i); 0264 if (s_data()->isPrint(thisChar)) { 0265 maxCharWidth = qMax(maxCharWidth, fontMetrics.boundingRect(QString::fromUcs4(&thisChar, 1)).width()); 0266 } 0267 } 0268 // Avoid too narrow cells 0269 maxCharWidth = qMax(maxCharWidth, 2 * fontMetrics.xHeight()); 0270 maxCharWidth = qMax(maxCharWidth, fontMetrics.height()); 0271 // Add the necessary padding, trying to match the delegate 0272 const int textMargin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1; 0273 maxCharWidth += 2 * textMargin; 0274 0275 const int columns = qMax(1, viewportWidth / maxCharWidth); 0276 model->setColumnCount(columns); 0277 0278 const uint oldChar = q->chr(); 0279 0280 const int new_w = viewportWidth / columns; 0281 const int rows = model->rowCount(); 0282 q->setUpdatesEnabled(false); 0283 QHeaderView *hHeader = q->horizontalHeader(); 0284 hHeader->setMinimumSectionSize(new_w); 0285 const int spaceLeft = viewportWidth - new_w * columns; 0286 for (int i = 0; i <= columns; ++i) { 0287 if (i < spaceLeft) { 0288 hHeader->resizeSection(i, new_w + 1); 0289 } else { 0290 hHeader->resizeSection(i, new_w); 0291 } 0292 } 0293 0294 QHeaderView *vHeader = q->verticalHeader(); 0295 #ifdef Q_OS_WIN 0296 int new_h = fontMetrics.lineSpacing() + 1; 0297 #else 0298 int new_h = fontMetrics.xHeight() * 3; 0299 #endif 0300 const int fontHeight = fontMetrics.height(); 0301 if (new_h < 5 || new_h < 4 + fontHeight) { 0302 new_h = qMax(5, 4 + fontHeight); 0303 } 0304 vHeader->setMinimumSectionSize(new_h); 0305 for (int i = 0; i < rows; ++i) { 0306 vHeader->resizeSection(i, new_h); 0307 } 0308 0309 q->setUpdatesEnabled(true); 0310 q->setChar(oldChar); 0311 } 0312 0313 void KCharSelectTablePrivate::doubleClicked(const QModelIndex &index) 0314 { 0315 uint c = model->data(index, KCharSelectItemModel::CharacterRole).toUInt(); 0316 if (s_data()->isPrint(c)) { 0317 Q_EMIT q->activated(c); 0318 } 0319 } 0320 0321 void KCharSelectTable::keyPressEvent(QKeyEvent *e) 0322 { 0323 if (d->model) { 0324 switch (e->key()) { 0325 case Qt::Key_Space: 0326 Q_EMIT activated(QChar::Space); 0327 return; 0328 case Qt::Key_Enter: 0329 case Qt::Key_Return: { 0330 if (!currentIndex().isValid()) { 0331 return; 0332 } 0333 uint c = d->model->data(currentIndex(), KCharSelectItemModel::CharacterRole).toUInt(); 0334 if (s_data()->isPrint(c)) { 0335 Q_EMIT activated(c); 0336 } 0337 return; 0338 } 0339 default: 0340 break; 0341 } 0342 } 0343 QTableView::keyPressEvent(e); 0344 } 0345 0346 /******************************************************************/ 0347 /* Class: KCharSelect */ 0348 /******************************************************************/ 0349 0350 KCharSelect::KCharSelect(QWidget *parent, const Controls controls) 0351 : QWidget(parent) 0352 , d(new KCharSelectPrivate(this)) 0353 { 0354 initWidget(controls, nullptr); 0355 } 0356 0357 KCharSelect::KCharSelect(QWidget *parent, QObject *actionParent, const Controls controls) 0358 : QWidget(parent) 0359 , d(new KCharSelectPrivate(this)) 0360 { 0361 initWidget(controls, actionParent); 0362 } 0363 0364 void attachToActionParent(QAction *action, QObject *actionParent, const QList<QKeySequence> &shortcuts) 0365 { 0366 if (!action || !actionParent) { 0367 return; 0368 } 0369 0370 action->setParent(actionParent); 0371 0372 if (actionParent->inherits("KActionCollection")) { 0373 QMetaObject::invokeMethod(actionParent, "addAction", Q_ARG(QString, action->objectName()), Q_ARG(QAction *, action)); 0374 QMetaObject::invokeMethod(actionParent, "setDefaultShortcuts", Q_ARG(QAction *, action), Q_ARG(QList<QKeySequence>, shortcuts)); 0375 } else { 0376 action->setShortcuts(shortcuts); 0377 } 0378 } 0379 0380 void KCharSelect::initWidget(const Controls controls, QObject *actionParent) 0381 { 0382 d->actionParent = actionParent; 0383 0384 QVBoxLayout *mainLayout = new QVBoxLayout(this); 0385 mainLayout->setContentsMargins(0, 0, 0, 0); 0386 if (SearchLine & controls) { 0387 QHBoxLayout *searchLayout = new QHBoxLayout(); 0388 mainLayout->addLayout(searchLayout); 0389 d->searchLine = new QLineEdit(this); 0390 searchLayout->addWidget(d->searchLine); 0391 d->searchLine->setPlaceholderText(tr("Enter a search term or character...", "@info:placeholder")); 0392 d->searchLine->setClearButtonEnabled(true); 0393 d->searchLine->setToolTip(tr("Enter a search term or character here", "@info:tooltip")); 0394 0395 QAction *findAction = new QAction(this); 0396 connect(findAction, &QAction::triggered, this, [this]() { 0397 d->activateSearchLine(); 0398 }); 0399 findAction->setObjectName(QStringLiteral("edit_find")); 0400 findAction->setText(tr("&Find...", "@action")); 0401 findAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); 0402 attachToActionParent(findAction, actionParent, QKeySequence::keyBindings(QKeySequence::Find)); 0403 0404 connect(d->searchLine, &QLineEdit::textChanged, this, [this]() { 0405 d->searchEditChanged(); 0406 }); 0407 connect(d->searchLine, &QLineEdit::returnPressed, this, [this]() { 0408 d->search(); 0409 }); 0410 } 0411 0412 if ((SearchLine & controls) && ((FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls))) { 0413 QFrame *line = new QFrame(this); 0414 line->setFrameShape(QFrame::HLine); 0415 line->setFrameShadow(QFrame::Sunken); 0416 mainLayout->addWidget(line); 0417 } 0418 0419 QHBoxLayout *comboLayout = new QHBoxLayout(); 0420 0421 d->backButton = new QToolButton(this); 0422 comboLayout->addWidget(d->backButton); 0423 d->backButton->setEnabled(false); 0424 d->backButton->setText(tr("Previous in History", "@action:button Goes to previous character")); 0425 d->backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); 0426 d->backButton->setToolTip(tr("Go to previous character in history", "@info:tooltip")); 0427 0428 d->forwardButton = new QToolButton(this); 0429 comboLayout->addWidget(d->forwardButton); 0430 d->forwardButton->setEnabled(false); 0431 d->forwardButton->setText(tr("Next in History", "@action:button Goes to next character")); 0432 d->forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); 0433 d->forwardButton->setToolTip(tr("Go to next character in history", "info:tooltip")); 0434 0435 QAction *backAction = new QAction(this); 0436 connect(backAction, &QAction::triggered, d->backButton, &QAbstractButton::animateClick); 0437 backAction->setObjectName(QStringLiteral("go_back")); 0438 backAction->setText(tr("&Back", "@action go back")); 0439 backAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); 0440 attachToActionParent(backAction, actionParent, QKeySequence::keyBindings(QKeySequence::Back)); 0441 0442 QAction *forwardAction = new QAction(this); 0443 connect(forwardAction, &QAction::triggered, d->forwardButton, &QAbstractButton::animateClick); 0444 forwardAction->setObjectName(QStringLiteral("go_forward")); 0445 forwardAction->setText(tr("&Forward", "@action go forward")); 0446 forwardAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); 0447 attachToActionParent(forwardAction, actionParent, QKeySequence::keyBindings(QKeySequence::Forward)); 0448 0449 if (QApplication::isRightToLeft()) { // swap the back/forward icons 0450 QIcon tmp = backAction->icon(); 0451 backAction->setIcon(forwardAction->icon()); 0452 forwardAction->setIcon(tmp); 0453 } 0454 0455 connect(d->backButton, &QToolButton::clicked, this, [this]() { 0456 d->back(); 0457 }); 0458 connect(d->forwardButton, &QToolButton::clicked, this, [this]() { 0459 d->forward(); 0460 }); 0461 0462 d->sectionCombo = new QComboBox(this); 0463 d->sectionCombo->setObjectName(QStringLiteral("sectionCombo")); 0464 d->sectionCombo->setToolTip(tr("Select a category", "@info:tooltip")); 0465 comboLayout->addWidget(d->sectionCombo); 0466 d->blockCombo = new QComboBox(this); 0467 d->blockCombo->setObjectName(QStringLiteral("blockCombo")); 0468 d->blockCombo->setToolTip(tr("Select a block to be displayed", "@info:tooltip")); 0469 d->blockCombo->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 0470 comboLayout->addWidget(d->blockCombo, 1); 0471 QStringList sectionList = s_data()->sectionList(); 0472 d->sectionCombo->addItems(sectionList); 0473 d->blockCombo->setMinimumWidth(QFontMetrics(QWidget::font()).averageCharWidth() * 25); 0474 0475 connect(d->sectionCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) { 0476 d->sectionSelected(index); 0477 }); 0478 0479 connect(d->blockCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) { 0480 d->blockSelected(index); 0481 }); 0482 0483 d->fontCombo = new QFontComboBox(this); 0484 comboLayout->addWidget(d->fontCombo); 0485 d->fontCombo->setEditable(true); 0486 d->fontCombo->resize(d->fontCombo->sizeHint()); 0487 d->fontCombo->setToolTip(tr("Set font", "@info:tooltip")); 0488 0489 d->fontSizeSpinBox = new QSpinBox(this); 0490 comboLayout->addWidget(d->fontSizeSpinBox); 0491 d->fontSizeSpinBox->setValue(QWidget::font().pointSize()); 0492 d->fontSizeSpinBox->setRange(1, 400); 0493 d->fontSizeSpinBox->setSingleStep(1); 0494 d->fontSizeSpinBox->setToolTip(tr("Set font size", "@info:tooltip")); 0495 0496 connect(d->fontCombo, &QFontComboBox::currentFontChanged, this, [this]() { 0497 d->fontSelected(); 0498 }); 0499 connect(d->fontSizeSpinBox, &QSpinBox::valueChanged, this, [this]() { 0500 d->fontSelected(); 0501 }); 0502 0503 if ((HistoryButtons & controls) || (FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls)) { 0504 mainLayout->addLayout(comboLayout); 0505 } 0506 if (!(HistoryButtons & controls)) { 0507 d->backButton->hide(); 0508 d->forwardButton->hide(); 0509 } 0510 if (!(FontCombo & controls)) { 0511 d->fontCombo->hide(); 0512 } 0513 if (!(FontSize & controls)) { 0514 d->fontSizeSpinBox->hide(); 0515 } 0516 if (!(BlockCombos & controls)) { 0517 d->sectionCombo->hide(); 0518 d->blockCombo->hide(); 0519 } 0520 0521 QSplitter *splitter = new QSplitter(this); 0522 if ((CharacterTable & controls) || (DetailBrowser & controls)) { 0523 mainLayout->addWidget(splitter); 0524 } else { 0525 splitter->hide(); 0526 } 0527 d->charTable = new KCharSelectTable(this, QFont()); 0528 if (CharacterTable & controls) { 0529 splitter->addWidget(d->charTable); 0530 } else { 0531 d->charTable->hide(); 0532 } 0533 0534 const QSize sz(200, 200); 0535 d->charTable->resize(sz); 0536 d->charTable->setMinimumSize(sz); 0537 0538 d->charTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0539 0540 setCurrentFont(QFont()); 0541 0542 connect(d->charTable, &KCharSelectTable::focusItemChanged, this, [this](uint c) { 0543 d->updateCurrentChar(c); 0544 }); 0545 connect(d->charTable, &KCharSelectTable::activated, this, [this](uint c) { 0546 d->charSelected(c); 0547 }); 0548 connect(d->charTable, &KCharSelectTable::showCharRequested, this, &KCharSelect::setCurrentCodePoint); 0549 0550 d->detailBrowser = new QTextBrowser(this); 0551 if (DetailBrowser & controls) { 0552 splitter->addWidget(d->detailBrowser); 0553 } else { 0554 d->detailBrowser->hide(); 0555 } 0556 d->detailBrowser->setOpenLinks(false); 0557 connect(d->detailBrowser, &QTextBrowser::anchorClicked, this, [this](const QUrl &url) { 0558 d->linkClicked(url); 0559 }); 0560 0561 setFocusPolicy(Qt::StrongFocus); 0562 if (SearchLine & controls) { 0563 setFocusProxy(d->searchLine); 0564 } else { 0565 setFocusProxy(d->charTable); 0566 } 0567 0568 d->sectionSelected(1); // this will also call blockSelected(0) 0569 setCurrentCodePoint(QChar::Null); 0570 0571 d->historyEnabled = true; 0572 } 0573 0574 KCharSelect::~KCharSelect() = default; 0575 0576 QSize KCharSelect::sizeHint() const 0577 { 0578 return QWidget::sizeHint(); 0579 } 0580 0581 void KCharSelect::setCurrentFont(const QFont &_font) 0582 { 0583 d->fontCombo->setCurrentFont(_font); 0584 d->fontSizeSpinBox->setValue(_font.pointSize()); 0585 d->fontSelected(); 0586 } 0587 0588 void KCharSelect::setAllPlanesEnabled(bool all) 0589 { 0590 d->allPlanesEnabled = all; 0591 } 0592 0593 bool KCharSelect::allPlanesEnabled() const 0594 { 0595 return d->allPlanesEnabled; 0596 } 0597 0598 QChar KCharSelect::currentChar() const 0599 { 0600 if (d->allPlanesEnabled) { 0601 qFatal("You must use KCharSelect::currentCodePoint instead of KCharSelect::currentChar"); 0602 } 0603 return QChar(d->charTable->chr()); 0604 } 0605 0606 uint KCharSelect::currentCodePoint() const 0607 { 0608 return d->charTable->chr(); 0609 } 0610 0611 QFont KCharSelect::currentFont() const 0612 { 0613 return d->charTable->font(); 0614 } 0615 0616 QList<QChar> KCharSelect::displayedChars() const 0617 { 0618 if (d->allPlanesEnabled) { 0619 qFatal("You must use KCharSelect::displayedCodePoints instead of KCharSelect::displayedChars"); 0620 } 0621 QList<QChar> result; 0622 const auto displayedChars = d->charTable->displayedChars(); 0623 result.reserve(displayedChars.size()); 0624 for (uint c : displayedChars) { 0625 result.append(QChar(c)); 0626 } 0627 return result; 0628 } 0629 0630 QVector<uint> KCharSelect::displayedCodePoints() const 0631 { 0632 return d->charTable->displayedChars(); 0633 } 0634 0635 void KCharSelect::setCurrentChar(const QChar &c) 0636 { 0637 if (d->allPlanesEnabled) { 0638 qCritical("You should use KCharSelect::setCurrentCodePoint instead of KCharSelect::setCurrentChar"); 0639 } 0640 setCurrentCodePoint(c.unicode()); 0641 } 0642 0643 void KCharSelect::setCurrentCodePoint(uint c) 0644 { 0645 if (!d->allPlanesEnabled && QChar::requiresSurrogates(c)) { 0646 qCritical("You must setAllPlanesEnabled(true) to use non-BMP characters"); 0647 c = QChar::ReplacementCharacter; 0648 } 0649 if (c > QChar::LastValidCodePoint) { 0650 qCWarning(KWidgetsAddonsLog, "Code point outside Unicode range"); 0651 c = QChar::LastValidCodePoint; 0652 } 0653 bool oldHistoryEnabled = d->historyEnabled; 0654 d->historyEnabled = false; 0655 int block = s_data()->blockIndex(c); 0656 int section = s_data()->sectionIndex(block); 0657 d->sectionCombo->setCurrentIndex(section); 0658 int index = d->blockCombo->findData(block); 0659 if (index != -1) { 0660 d->blockCombo->setCurrentIndex(index); 0661 } 0662 d->historyEnabled = oldHistoryEnabled; 0663 d->charTable->setChar(c); 0664 } 0665 0666 void KCharSelectPrivate::historyAdd(uint c, bool fromSearch, const QString &searchString) 0667 { 0668 // qCDebug(KWidgetsAddonsLog) << "about to add char" << c << "fromSearch" << fromSearch << "searchString" << searchString; 0669 0670 if (!historyEnabled) { 0671 return; 0672 } 0673 0674 if (!history.isEmpty() && c == history.last().c) { 0675 // avoid duplicates 0676 return; 0677 } 0678 0679 // behave like a web browser, i.e. if user goes back from B to A then clicks C, B is forgotten 0680 while (!history.isEmpty() && inHistory != history.count() - 1) { 0681 history.removeLast(); 0682 } 0683 0684 while (history.size() >= MaxHistoryItems) { 0685 history.removeFirst(); 0686 } 0687 0688 HistoryItem item; 0689 item.c = c; 0690 item.fromSearch = fromSearch; 0691 item.searchString = searchString; 0692 history.append(item); 0693 0694 inHistory = history.count() - 1; 0695 updateBackForwardButtons(); 0696 } 0697 0698 void KCharSelectPrivate::showFromHistory(int index) 0699 { 0700 Q_ASSERT(index >= 0 && index < history.count()); 0701 Q_ASSERT(index != inHistory); 0702 0703 inHistory = index; 0704 updateBackForwardButtons(); 0705 0706 const HistoryItem &item = history[index]; 0707 // qCDebug(KWidgetsAddonsLog) << "index" << index << "char" << item.c << "fromSearch" << item.fromSearch 0708 // << "searchString" << item.searchString; 0709 0710 // avoid adding an item from history into history again 0711 bool oldHistoryEnabled = historyEnabled; 0712 historyEnabled = false; 0713 if (item.fromSearch) { 0714 if (searchLine->text() != item.searchString) { 0715 searchLine->setText(item.searchString); 0716 search(); 0717 } 0718 charTable->setChar(item.c); 0719 } else { 0720 searchLine->clear(); 0721 q->setCurrentCodePoint(item.c); 0722 } 0723 historyEnabled = oldHistoryEnabled; 0724 } 0725 0726 void KCharSelectPrivate::updateBackForwardButtons() 0727 { 0728 backButton->setEnabled(inHistory > 0); 0729 forwardButton->setEnabled(inHistory < history.count() - 1); 0730 } 0731 0732 void KCharSelectPrivate::activateSearchLine() 0733 { 0734 searchLine->setFocus(); 0735 searchLine->selectAll(); 0736 } 0737 0738 void KCharSelectPrivate::back() 0739 { 0740 Q_ASSERT(inHistory > 0); 0741 showFromHistory(inHistory - 1); 0742 } 0743 0744 void KCharSelectPrivate::forward() 0745 { 0746 Q_ASSERT(inHistory + 1 < history.count()); 0747 showFromHistory(inHistory + 1); 0748 } 0749 0750 void KCharSelectPrivate::fontSelected() 0751 { 0752 QFont font = fontCombo->currentFont(); 0753 font.setPointSize(fontSizeSpinBox->value()); 0754 charTable->setFont(font); 0755 Q_EMIT q->currentFontChanged(font); 0756 } 0757 0758 void KCharSelectPrivate::charSelected(uint c) 0759 { 0760 if (!allPlanesEnabled) { 0761 Q_EMIT q->charSelected(QChar(c)); 0762 } 0763 Q_EMIT q->codePointSelected(c); 0764 } 0765 0766 void KCharSelectPrivate::updateCurrentChar(uint c) 0767 { 0768 if (!allPlanesEnabled) { 0769 Q_EMIT q->currentCharChanged(QChar(c)); 0770 } 0771 Q_EMIT q->currentCodePointChanged(c); 0772 if (searchMode || sectionCombo->currentIndex() == 0) { 0773 // 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 0774 // all characters mode). 0775 //(when we are not in search mode nor in the all characters mode the current character always belongs to the current section & block.) 0776 int block = s_data()->blockIndex(c); 0777 if (searchMode) { 0778 int section = s_data()->sectionIndex(block); 0779 sectionCombo->setCurrentIndex(section); 0780 } 0781 int index = blockCombo->findData(block); 0782 if (index != -1) { 0783 blockCombo->setCurrentIndex(index); 0784 } 0785 } 0786 0787 if (searchLine) { 0788 historyAdd(c, searchMode, searchLine->text()); 0789 } 0790 0791 slotUpdateUnicode(c); 0792 } 0793 0794 void KCharSelectPrivate::slotUpdateUnicode(uint c) 0795 { 0796 QString html = QLatin1String("<p>") + tr("Character:") + QLatin1Char(' ') + s_data()->display(c, charTable->font()) + QLatin1Char(' ') 0797 + s_data()->formatCode(c) + QLatin1String("<br />"); 0798 0799 QString name = s_data()->name(c); 0800 if (!name.isEmpty()) { 0801 // is name ever empty? </p> should always be there... 0802 html += tr("Name: ") + name.toHtmlEscaped() + QLatin1String("</p>"); 0803 } 0804 const QStringList aliases = s_data()->aliases(c); 0805 const QStringList notes = s_data()->notes(c); 0806 const QVector<uint> seeAlso = s_data()->seeAlso(c); 0807 const QStringList equivalents = s_data()->equivalents(c); 0808 const QStringList approxEquivalents = s_data()->approximateEquivalents(c); 0809 const QVector<uint> decomposition = s_data()->decomposition(c); 0810 if (!(aliases.isEmpty() && notes.isEmpty() && seeAlso.isEmpty() && equivalents.isEmpty() && approxEquivalents.isEmpty() && decomposition.isEmpty())) { 0811 html += QLatin1String("<p><b>") + tr("Annotations and Cross References") + QLatin1String("</b></p>"); 0812 } 0813 0814 if (!aliases.isEmpty()) { 0815 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Alias names:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0816 for (const QString &alias : aliases) { 0817 html += QLatin1String("<li>") + alias.toHtmlEscaped() + QLatin1String("</li>"); 0818 } 0819 html += QLatin1String("</ul>"); 0820 } 0821 0822 if (!notes.isEmpty()) { 0823 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Notes:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0824 for (const QString ¬e : notes) { 0825 html += QLatin1String("<li>") + createLinks(note.toHtmlEscaped()) + QLatin1String("</li>"); 0826 } 0827 html += QLatin1String("</ul>"); 0828 } 0829 0830 if (!seeAlso.isEmpty()) { 0831 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("See also:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0832 for (uint c2 : seeAlso) { 0833 if (!allPlanesEnabled && QChar::requiresSurrogates(c2)) { 0834 continue; 0835 } 0836 html += QLatin1String("<li><a href=\"") + QString::number(c2, 16) + QLatin1String("\">"); 0837 if (s_data()->isPrint(c2)) { 0838 html += QLatin1String("‎&#") + QString::number(c2) + QLatin1String("; "); 0839 } 0840 html += s_data()->formatCode(c2) + QLatin1Char(' ') + s_data()->name(c2).toHtmlEscaped() + QLatin1String("</a></li>"); 0841 } 0842 html += QLatin1String("</ul>"); 0843 } 0844 0845 if (!equivalents.isEmpty()) { 0846 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0847 for (const QString &equivalent : equivalents) { 0848 html += QLatin1String("<li>") + createLinks(equivalent.toHtmlEscaped()) + QLatin1String("</li>"); 0849 } 0850 html += QLatin1String("</ul>"); 0851 } 0852 0853 if (!approxEquivalents.isEmpty()) { 0854 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Approximate equivalents:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0855 for (const QString &approxEquivalent : approxEquivalents) { 0856 html += QLatin1String("<li>") + createLinks(approxEquivalent.toHtmlEscaped()) + QLatin1String("</li>"); 0857 } 0858 html += QLatin1String("</ul>"); 0859 } 0860 0861 if (!decomposition.isEmpty()) { 0862 html += QLatin1String("<p style=\"margin-bottom: 0px;\">") + tr("Decomposition:") + QLatin1String("</p><ul style=\"margin-top: 0px;\">"); 0863 for (uint c2 : decomposition) { 0864 if (!allPlanesEnabled && QChar::requiresSurrogates(c2)) { 0865 continue; 0866 } 0867 html += QLatin1String("<li>") + createLinks(s_data()->formatCode(c2, 4, QString())) + QLatin1String("</li>"); 0868 } 0869 html += QLatin1String("</ul>"); 0870 } 0871 0872 QStringList unihan = s_data()->unihanInfo(c); 0873 if (unihan.count() == 7) { 0874 html += QLatin1String("<p><b>") + tr("CJK Ideograph Information") + QLatin1String("</b></p><p>"); 0875 bool newline = true; 0876 if (!unihan[0].isEmpty()) { 0877 html += tr("Definition in English: ") + unihan[0]; 0878 newline = false; 0879 } 0880 if (!unihan[2].isEmpty()) { 0881 if (!newline) { 0882 html += QLatin1String("<br>"); 0883 } 0884 html += tr("Mandarin Pronunciation: ") + unihan[2]; 0885 newline = false; 0886 } 0887 if (!unihan[1].isEmpty()) { 0888 if (!newline) { 0889 html += QLatin1String("<br>"); 0890 } 0891 html += tr("Cantonese Pronunciation: ") + unihan[1]; 0892 newline = false; 0893 } 0894 if (!unihan[6].isEmpty()) { 0895 if (!newline) { 0896 html += QLatin1String("<br>"); 0897 } 0898 html += tr("Japanese On Pronunciation: ") + unihan[6]; 0899 newline = false; 0900 } 0901 if (!unihan[5].isEmpty()) { 0902 if (!newline) { 0903 html += QLatin1String("<br>"); 0904 } 0905 html += tr("Japanese Kun Pronunciation: ") + unihan[5]; 0906 newline = false; 0907 } 0908 if (!unihan[3].isEmpty()) { 0909 if (!newline) { 0910 html += QLatin1String("<br>"); 0911 } 0912 html += tr("Tang Pronunciation: ") + unihan[3]; 0913 newline = false; 0914 } 0915 if (!unihan[4].isEmpty()) { 0916 if (!newline) { 0917 html += QLatin1String("<br>"); 0918 } 0919 html += tr("Korean Pronunciation: ") + unihan[4]; 0920 newline = false; 0921 } 0922 html += QLatin1String("</p>"); 0923 } 0924 0925 html += QLatin1String("<p><b>") + tr("General Character Properties") + QLatin1String("</b><br>"); 0926 html += tr("Block: ") + s_data()->block(c) + QLatin1String("<br>"); 0927 html += tr("Unicode category: ") + s_data()->categoryText(s_data()->category(c)) + QLatin1String("</p>"); 0928 0929 const QByteArray utf8 = QString::fromUcs4(&c, 1).toUtf8(); 0930 0931 html += QLatin1String("<p><b>") + tr("Various Useful Representations") + QLatin1String("</b><br>"); 0932 html += tr("UTF-8:"); 0933 for (unsigned char c : utf8) { 0934 html += QLatin1Char(' ') + s_data()->formatCode(c, 2, QStringLiteral("0x")); 0935 } 0936 html += QLatin1String("<br>") + tr("UTF-16: "); 0937 if (QChar::requiresSurrogates(c)) { 0938 html += s_data()->formatCode(QChar::highSurrogate(c), 4, QStringLiteral("0x")); 0939 html += QLatin1Char(' ') + s_data->formatCode(QChar::lowSurrogate(c), 4, QStringLiteral("0x")); 0940 } else { 0941 html += s_data()->formatCode(c, 4, QStringLiteral("0x")); 0942 } 0943 html += QLatin1String("<br>") + tr("C octal escaped UTF-8: "); 0944 for (unsigned char c : utf8) { 0945 html += s_data()->formatCode(c, 3, QStringLiteral("\\"), 8); 0946 } 0947 html += QLatin1String("<br>") + tr("XML decimal entity:") + QLatin1String(" &#") + QString::number(c) + QLatin1String(";</p>"); 0948 0949 detailBrowser->setHtml(html); 0950 } 0951 0952 QString KCharSelectPrivate::createLinks(QString s) 0953 { 0954 static const QRegularExpression rx(QStringLiteral("\\b([\\dABCDEF]{4,5})\\b"), QRegularExpression::UseUnicodePropertiesOption); 0955 QRegularExpressionMatchIterator iter = rx.globalMatch(s); 0956 QRegularExpressionMatch match; 0957 QSet<QString> chars; 0958 while (iter.hasNext()) { 0959 match = iter.next(); 0960 chars.insert(match.captured(1)); 0961 } 0962 0963 for (const QString &c : std::as_const(chars)) { 0964 int unicode = c.toInt(nullptr, 16); 0965 if (!allPlanesEnabled && QChar::requiresSurrogates(unicode)) { 0966 continue; 0967 } 0968 QString link = QLatin1String("<a href=\"") + c + QLatin1String("\">"); 0969 if (s_data()->isPrint(unicode)) { 0970 link += QLatin1String("‎&#") + QString::number(unicode) + QLatin1String("; "); 0971 } 0972 link += QLatin1String("U+") + c + QLatin1Char(' '); 0973 link += s_data()->name(unicode).toHtmlEscaped() + QLatin1String("</a>"); 0974 s.replace(c, link); 0975 } 0976 return s; 0977 } 0978 0979 void KCharSelectPrivate::sectionSelected(int index) 0980 { 0981 blockCombo->clear(); 0982 QVector<uint> chars; 0983 const QVector<int> blocks = s_data()->sectionContents(index); 0984 for (int block : blocks) { 0985 if (!allPlanesEnabled) { 0986 const QVector<uint> contents = s_data()->blockContents(block); 0987 if (!contents.isEmpty() && QChar::requiresSurrogates(contents.at(0))) { 0988 continue; 0989 } 0990 } 0991 blockCombo->addItem(s_data()->blockName(block), QVariant(block)); 0992 if (index == 0) { 0993 chars << s_data()->blockContents(block); 0994 } 0995 } 0996 if (index == 0) { 0997 charTable->setContents(chars); 0998 updateCurrentChar(charTable->chr()); 0999 } 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(charTable->chr())) { 1016 // the selected block already contains the selected character 1017 return; 1018 } 1019 const QVector<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(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 QVector<uint> contents = s_data()->find(searchLine->text()); 1059 if (!allPlanesEnabled) { 1060 contents.erase(std::remove_if(contents.begin(), contents.end(), QChar::requiresSurrogates), 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(nullptr, 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(QPalette::Button)); 1092 } 1093 return QVariant(); 1094 } 1095 1096 uint c = m_chars[pos]; 1097 if (role == Qt::ToolTipRole) { 1098 QString result = s_data()->display(c, m_font) + QLatin1String("<br />") + s_data()->name(c).toHtmlEscaped() + QLatin1String("<br />") 1099 + tr("Unicode code point:") + QLatin1Char(' ') + s_data()->formatCode(c) + QLatin1String("<br />") + tr("In decimal", "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, 1)); 1107 } 1108 return QVariant(); 1109 } else if (role == Qt::BackgroundRole) { 1110 QFontMetrics fm = QFontMetrics(m_font); 1111 if (fm.inFontUcs4(c) && s_data()->isPrint(c)) { 1112 return QVariant(qApp->palette().color(QPalette::Base)); 1113 } else { 1114 return QVariant(qApp->palette().color(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(text.toUcs4().at(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"