File indexing completed on 2024-04-28 15:31:58

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 &note : 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("&#8206;&#") + 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(" &amp;#") + 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("&#8206;&#") + QString::number(unicode) + QLatin1String(";&nbsp;");
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"