diff --git a/src/kcharselect.cpp b/src/kcharselect.cpp index 4847e15..3680a1f 100644 --- a/src/kcharselect.cpp +++ b/src/kcharselect.cpp @@ -1,1137 +1,1136 @@ /* This file is part of the KDE libraries Copyright (C) 1999 Reginald Stadlbauer Copyright (C) 2017 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcharselect.h" #include "kcharselect_p.h" #include "loggingcategory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_GLOBAL_STATIC(KCharSelectData, s_data) class KCharSelectTablePrivate { public: KCharSelectTablePrivate(KCharSelectTable *q): q(q), model(nullptr) {} KCharSelectTable *q; QFont font; KCharSelectItemModel *model; QVector chars; uint chr; void _k_resizeCells(); void _k_doubleClicked(const QModelIndex &index); void _k_slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); }; class Q_DECL_HIDDEN KCharSelect::KCharSelectPrivate { public: struct HistoryItem { uint c; bool fromSearch; QString searchString; }; enum { MaxHistoryItems = 100 }; KCharSelectPrivate(KCharSelect *q) : q(q) , searchLine(nullptr) , searchMode(false) , historyEnabled(false) , allPlanesEnabled(false) , inHistory(0) , actionParent(nullptr) { } QString tr(const char *str) { return KCharSelect::tr(str); } KCharSelect *q; QToolButton *backButton = nullptr; QToolButton *forwardButton = nullptr; QLineEdit *searchLine = nullptr; QFontComboBox *fontCombo = nullptr; QSpinBox *fontSizeSpinBox = nullptr; QComboBox *sectionCombo = nullptr; QComboBox *blockCombo = nullptr; KCharSelectTable *charTable = nullptr; QTextBrowser *detailBrowser = nullptr; bool searchMode; //a search is active bool historyEnabled; bool allPlanesEnabled; int inHistory; //index of current char in history QList history; QObject *actionParent; QString createLinks(QString s); void historyAdd(uint c, bool fromSearch, const QString &searchString); void showFromHistory(int index); void updateBackForwardButtons(); void _k_activateSearchLine(); void _k_back(); void _k_forward(); void _k_fontSelected(); void _k_charSelected(uint c); void _k_updateCurrentChar(uint c); void _k_slotUpdateUnicode(uint c); void _k_sectionSelected(int index); void _k_blockSelected(int index); void _k_searchEditChanged(); void _k_search(); void _k_linkClicked(QUrl url); }; /******************************************************************/ /* Class: KCharSelectTable */ /******************************************************************/ KCharSelectTable::KCharSelectTable(QWidget *parent, const QFont &_font) : QTableView(parent), d(new KCharSelectTablePrivate(this)) { d->font = _font; setTabKeyNavigation(false); setSelectionBehavior(QAbstractItemView::SelectItems); setSelectionMode(QAbstractItemView::SingleSelection); QPalette _palette; _palette.setColor(backgroundRole(), palette().color(QPalette::Base)); setPalette(_palette); verticalHeader()->setVisible(false); verticalHeader()->setSectionResizeMode(QHeaderView::Custom); horizontalHeader()->setVisible(false); horizontalHeader()->setSectionResizeMode(QHeaderView::Custom); setFocusPolicy(Qt::StrongFocus); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(false); setDragDropMode(QAbstractItemView::DragDrop); setTextElideMode(Qt::ElideNone); connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(_k_doubleClicked(QModelIndex))); d->_k_resizeCells(); } KCharSelectTable::~KCharSelectTable() { delete d; } void KCharSelectTable::setFont(const QFont &_font) { QTableView::setFont(_font); d->font = _font; if (d->model) { d->model->setFont(_font); } d->_k_resizeCells(); } uint KCharSelectTable::chr() { return d->chr; } QFont KCharSelectTable::font() const { return d->font; } QVector KCharSelectTable::displayedChars() const { return d->chars; } void KCharSelectTable::setChar(uint c) { int pos = d->chars.indexOf(c); if (pos != -1) { setCurrentIndex(model()->index(pos / model()->columnCount(), pos % model()->columnCount())); } } void KCharSelectTable::setContents(const QVector &chars) { d->chars = chars; auto oldModel = d->model; d->model = new KCharSelectItemModel(chars, d->font, this); setModel(d->model); d->_k_resizeCells(); // Setting a model changes the selectionModel. Make sure to always reconnect. connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(_k_slotSelectionChanged(QItemSelection,QItemSelection))); connect(d->model, &KCharSelectItemModel::showCharRequested, this, &KCharSelectTable::showCharRequested); delete oldModel; // The selection model is thrown away when the model gets destroyed(). } void KCharSelectTable::scrollTo(const QModelIndex &index, ScrollHint hint) { // this prevents horizontal scrolling when selecting a character in the last column if (index.isValid() && index.column() != 0) { QTableView::scrollTo(d->model->index(index.row(), 0), hint); } else { QTableView::scrollTo(index, hint); } } void KCharSelectTablePrivate::_k_slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); if (!model || selected.indexes().isEmpty()) { return; } QVariant temp = model->data(selected.indexes().at(0), KCharSelectItemModel::CharacterRole); if (temp.type() != QVariant::UInt) { return; } uint c = temp.toUInt(); chr = c; emit q->focusItemChanged(c); } void KCharSelectTable::resizeEvent(QResizeEvent *e) { QTableView::resizeEvent(e); if (e->size().width() != e->oldSize().width()) { // Resize our cells. But do so asynchronously through the event loop. // Otherwise we can end up with an infinite loop as resizing the cells in turn results in // a layout change which results in a resize event. More importantly doing this blockingly // crashes QAccessible as the resize we potentially cause will discard objects which are // still being used in the call chain leading to this event. // https://bugs.kde.org/show_bug.cgi?id=374933 // https://bugreports.qt.io/browse/QTBUG-58153 // This can be removed once a fixed Qt version is the lowest requirement for Frameworks. auto timer = new QTimer(this); timer->setSingleShot(true); connect(timer, &QTimer::timeout, [&,timer]() { d->_k_resizeCells(); timer->deleteLater(); }); timer->start(0); } } void KCharSelectTablePrivate::_k_resizeCells() { KCharSelectItemModel *model = static_cast(q->model()); if (!model) return; const int viewportWidth = q->viewport()->size().width(); QFontMetrics fontMetrics(font); // Determine the max width of the displayed characters // fontMetrics.maxWidth() doesn't help because of font fallbacks // (testcase: Malayalam characters) int maxCharWidth = 0; const QVector chars = model->chars(); for (int i = 0; i < chars.size(); ++i) { uint thisChar = chars.at(i); if(s_data()->isPrint(thisChar)) { maxCharWidth = qMax(maxCharWidth, fontMetrics.boundingRect(QString::fromUcs4(&thisChar, 1)).width()); } } // Avoid too narrow cells maxCharWidth = qMax(maxCharWidth, 2 * fontMetrics.xHeight()); maxCharWidth = qMax(maxCharWidth, fontMetrics.height()); // Add the necessary padding, trying to match the delegate const int textMargin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1; maxCharWidth += 2 * textMargin; const int columns = qMax(1, viewportWidth / maxCharWidth); model->setColumnCount(columns); const uint oldChar = q->chr(); const int new_w = viewportWidth / columns; const int rows = model->rowCount(); q->setUpdatesEnabled(false); QHeaderView *hHeader = q->horizontalHeader(); hHeader->setMinimumSectionSize(new_w); const int spaceLeft = viewportWidth - new_w * columns; for (int i = 0; i <= columns; ++i) { if (i < spaceLeft) { hHeader->resizeSection(i, new_w + 1); } else { hHeader->resizeSection(i, new_w); } } QHeaderView *vHeader = q->verticalHeader(); #ifdef Q_OS_WIN int new_h = fontMetrics.lineSpacing() + 1; #else int new_h = fontMetrics.xHeight() * 3; #endif const int fontHeight = fontMetrics.height(); if (new_h < 5 || new_h < 4 + fontHeight) { new_h = qMax(5, 4 + fontHeight); } vHeader->setMinimumSectionSize(new_h); for (int i = 0; i < rows; ++i) { vHeader->resizeSection(i, new_h); } q->setUpdatesEnabled(true); q->setChar(oldChar); } void KCharSelectTablePrivate::_k_doubleClicked(const QModelIndex &index) { uint c = model->data(index, KCharSelectItemModel::CharacterRole).toUInt(); if (s_data()->isPrint(c)) { emit q->activated(c); } } void KCharSelectTable::keyPressEvent(QKeyEvent *e) { if (d->model) { switch (e->key()) { case Qt::Key_Space: emit activated(QChar::Space); return; case Qt::Key_Enter: case Qt::Key_Return: { if (!currentIndex().isValid()) { return; } uint c = d->model->data(currentIndex(), KCharSelectItemModel::CharacterRole).toUInt(); if (s_data()->isPrint(c)) { emit activated(c); } return; } default: break; } } QTableView::keyPressEvent(e); } /******************************************************************/ /* Class: KCharSelect */ /******************************************************************/ KCharSelect::KCharSelect(QWidget *parent, const Controls controls) : QWidget(parent), d(new KCharSelectPrivate(this)) { initWidget(controls, nullptr); } KCharSelect::KCharSelect( QWidget *parent , QObject *actionParent , const Controls controls) : QWidget(parent), d(new KCharSelectPrivate(this)) { initWidget(controls, actionParent); } void attachToActionParent(QAction *action, QObject *actionParent, const QList &shortcuts) { if (!action || !actionParent) { return; } action->setParent(actionParent); if (actionParent->inherits("KActionCollection")) { QMetaObject::invokeMethod(actionParent, "addAction", Q_ARG(QString, action->objectName()), Q_ARG(QAction*, action)); QMetaObject::invokeMethod(actionParent, "setDefaultShortcuts", Q_ARG(QAction*, action), Q_ARG(QList, shortcuts)); } else { action->setShortcuts(shortcuts); } } void KCharSelect::initWidget(const Controls controls, QObject *actionParent) { d->actionParent = actionParent; QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); if (SearchLine & controls) { QHBoxLayout *searchLayout = new QHBoxLayout(); mainLayout->addLayout(searchLayout); d->searchLine = new QLineEdit(this); searchLayout->addWidget(d->searchLine); d->searchLine->setPlaceholderText(tr("Enter a search term or character here")); d->searchLine->setClearButtonEnabled(true); d->searchLine->setToolTip(tr("Enter a search term or character here")); QAction *findAction = new QAction(this); connect(findAction, &QAction::triggered, this, [this]() { d->_k_activateSearchLine(); }); findAction->setObjectName(QStringLiteral("edit_find")); findAction->setText(tr("&Find...")); findAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); attachToActionParent(findAction, actionParent, QKeySequence::keyBindings(QKeySequence::Find)); connect(d->searchLine, SIGNAL(textChanged(QString)), this, SLOT(_k_searchEditChanged())); connect(d->searchLine, SIGNAL(returnPressed()), this, SLOT(_k_search())); } if ((SearchLine & controls) && ((FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls))) { QFrame *line = new QFrame(this); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); mainLayout->addWidget(line); } QHBoxLayout *comboLayout = new QHBoxLayout(); d->backButton = new QToolButton(this); comboLayout->addWidget(d->backButton); d->backButton->setEnabled(false); d->backButton->setText(tr("Previous in History", "Goes to previous character")); d->backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); d->backButton->setToolTip(tr("Previous Character in History")); d->forwardButton = new QToolButton(this); comboLayout->addWidget(d->forwardButton); d->forwardButton->setEnabled(false); d->forwardButton->setText(tr("Next in History", "Goes to next character")); d->forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); d->forwardButton->setToolTip(tr("Next Character in History")); QAction *backAction = new QAction(this); connect(backAction, &QAction::triggered, d->backButton, &QAbstractButton::animateClick); backAction->setObjectName(QStringLiteral("go_back")); backAction->setText(tr("&Back", "go back")); backAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); attachToActionParent(backAction, actionParent, QKeySequence::keyBindings(QKeySequence::Back)); QAction *forwardAction = new QAction(this); connect(forwardAction, &QAction::triggered, d->forwardButton, &QAbstractButton::animateClick); forwardAction->setObjectName(QStringLiteral("go_forward")); forwardAction->setText(tr("&Forward", "go forward")); forwardAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); attachToActionParent(forwardAction, actionParent, QKeySequence::keyBindings(QKeySequence::Forward)); if (QApplication::isRightToLeft()) { // swap the back/forward icons QIcon tmp = backAction->icon(); backAction->setIcon(forwardAction->icon()); forwardAction->setIcon(tmp); } connect(d->backButton, &QToolButton::clicked, this, [this]() { d->_k_back(); }); connect(d->forwardButton, &QToolButton::clicked, this, [this]() { d->_k_forward(); }); d->sectionCombo = new QComboBox(this); d->sectionCombo->setObjectName(QStringLiteral("sectionCombo")); d->sectionCombo->setToolTip(tr("Select a category")); comboLayout->addWidget(d->sectionCombo); d->blockCombo = new QComboBox(this); d->blockCombo->setObjectName(QStringLiteral("blockCombo")); d->blockCombo->setToolTip(tr("Select a block to be displayed")); d->blockCombo->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); comboLayout->addWidget(d->blockCombo, 1); d->sectionCombo->addItems(s_data()->sectionList()); d->blockCombo->setMinimumWidth(QFontMetrics(QWidget::font()).averageCharWidth() * 25); connect(d->sectionCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(_k_sectionSelected(int))); connect(d->blockCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(_k_blockSelected(int))); d->fontCombo = new QFontComboBox(this); comboLayout->addWidget(d->fontCombo); d->fontCombo->setEditable(true); d->fontCombo->resize(d->fontCombo->sizeHint()); d->fontCombo->setToolTip(tr("Set font")); d->fontSizeSpinBox = new QSpinBox(this); comboLayout->addWidget(d->fontSizeSpinBox); d->fontSizeSpinBox->setValue(QWidget::font().pointSize()); d->fontSizeSpinBox->setRange(1, 400); d->fontSizeSpinBox->setSingleStep(1); d->fontSizeSpinBox->setToolTip(tr("Set font size")); connect(d->fontCombo, SIGNAL(currentIndexChanged(QString)), this, SLOT(_k_fontSelected())); connect(d->fontSizeSpinBox, SIGNAL(valueChanged(int)), this, SLOT(_k_fontSelected())); if ((HistoryButtons & controls) || (FontCombo & controls) || (FontSize & controls) || (BlockCombos & controls)) { mainLayout->addLayout(comboLayout); } if (!(HistoryButtons & controls)) { d->backButton->hide(); d->forwardButton->hide(); } if (!(FontCombo & controls)) { d->fontCombo->hide(); } if (!(FontSize & controls)) { d->fontSizeSpinBox->hide(); } if (!(BlockCombos & controls)) { d->sectionCombo->hide(); d->blockCombo->hide(); } QSplitter *splitter = new QSplitter(this); if ((CharacterTable & controls) || (DetailBrowser & controls)) { mainLayout->addWidget(splitter); } else { splitter->hide(); } d->charTable = new KCharSelectTable(this, QFont()); if (CharacterTable & controls) { splitter->addWidget(d->charTable); d->charTable->setFocus(Qt::OtherFocusReason); } else { d->charTable->hide(); } const QSize sz(200, 200); d->charTable->resize(sz); d->charTable->setMinimumSize(sz); d->charTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setCurrentFont(QFont()); connect(d->charTable, SIGNAL(focusItemChanged(uint)), this, SLOT(_k_updateCurrentChar(uint))); connect(d->charTable, SIGNAL(activated(uint)), this, SLOT(_k_charSelected(uint))); connect(d->charTable, SIGNAL(showCharRequested(uint)), this, SLOT(setCurrentCodePoint(uint))); d->detailBrowser = new QTextBrowser(this); if (DetailBrowser & controls) { splitter->addWidget(d->detailBrowser); } else { d->detailBrowser->hide(); } d->detailBrowser->setOpenLinks(false); connect(d->detailBrowser, SIGNAL(anchorClicked(QUrl)), this, SLOT(_k_linkClicked(QUrl))); setFocusPolicy(Qt::StrongFocus); setFocusProxy(d->charTable); d->_k_sectionSelected(0); // this will also call _k_blockSelected(0) setCurrentCodePoint(QChar::Null); d->historyEnabled = true; } KCharSelect::~KCharSelect() { delete d; } QSize KCharSelect::sizeHint() const { return QWidget::sizeHint(); } void KCharSelect::setCurrentFont(const QFont &_font) { d->fontCombo->setCurrentFont(_font); d->fontSizeSpinBox->setValue(_font.pointSize()); d->_k_fontSelected(); } void KCharSelect::setAllPlanesEnabled(bool all) { d->allPlanesEnabled = all; } bool KCharSelect::allPlanesEnabled() const { return d->allPlanesEnabled; } QChar KCharSelect::currentChar() const { if (d->allPlanesEnabled) { qFatal("You must use KCharSelect::currentCodePoint instead of KCharSelect::currentChar"); } return QChar(d->charTable->chr()); } uint KCharSelect::currentCodePoint() const { return d->charTable->chr(); } QFont KCharSelect::currentFont() const { return d->charTable->font(); } QList KCharSelect::displayedChars() const { if (d->allPlanesEnabled) { qFatal("You must use KCharSelect::displayedCodePoints instead of KCharSelect::displayedChars"); } QList result; const auto displayedChars = d->charTable->displayedChars(); result.reserve(displayedChars.size()); for (uint c : displayedChars) { result.append(QChar(c)); } return result; } QVector KCharSelect::displayedCodePoints() const { return d->charTable->displayedChars(); } void KCharSelect::setCurrentChar(const QChar &c) { if (d->allPlanesEnabled) { qCritical("You should use KCharSelect::setCurrentCodePoint instead of KCharSelect::setCurrentChar"); } setCurrentCodePoint(c.unicode()); } void KCharSelect::setCurrentCodePoint(uint c) { if (!d->allPlanesEnabled && QChar::requiresSurrogates(c)) { qCritical("You must setAllPlanesEnabled(true) to use non-BMP characters"); c = QChar::ReplacementCharacter; } if (c > QChar::LastValidCodePoint) { qCWarning(KWidgetsAddonsLog, "Code point outside Unicode range"); c = QChar::LastValidCodePoint; } bool oldHistoryEnabled = d->historyEnabled; d->historyEnabled = false; int block = s_data()->blockIndex(c); int section = s_data()->sectionIndex(block); d->sectionCombo->setCurrentIndex(section); int index = d->blockCombo->findData(block); if (index != -1) { d->blockCombo->setCurrentIndex(index); } d->historyEnabled = oldHistoryEnabled; d->charTable->setChar(c); } void KCharSelect::KCharSelectPrivate::historyAdd(uint c, bool fromSearch, const QString &searchString) { //qCDebug(KWidgetsAddonsLog) << "about to add char" << c << "fromSearch" << fromSearch << "searchString" << searchString; if (!historyEnabled) { return; } if (!history.isEmpty() && c == history.last().c) { //avoid duplicates return; } //behave like a web browser, i.e. if user goes back from B to A then clicks C, B is forgotten while (!history.isEmpty() && inHistory != history.count() - 1) { history.removeLast(); } while (history.size() >= MaxHistoryItems) { history.removeFirst(); } HistoryItem item; item.c = c; item.fromSearch = fromSearch; item.searchString = searchString; history.append(item); inHistory = history.count() - 1; updateBackForwardButtons(); } void KCharSelect::KCharSelectPrivate::showFromHistory(int index) { Q_ASSERT(index >= 0 && index < history.count()); Q_ASSERT(index != inHistory); inHistory = index; updateBackForwardButtons(); const HistoryItem &item = history[index]; //qCDebug(KWidgetsAddonsLog) << "index" << index << "char" << item.c << "fromSearch" << item.fromSearch // << "searchString" << item.searchString; //avoid adding an item from history into history again bool oldHistoryEnabled = historyEnabled; historyEnabled = false; if (item.fromSearch) { if (searchLine->text() != item.searchString) { searchLine->setText(item.searchString); _k_search(); } charTable->setChar(item.c); } else { searchLine->clear(); q->setCurrentCodePoint(item.c); } historyEnabled = oldHistoryEnabled; } void KCharSelect::KCharSelectPrivate::updateBackForwardButtons() { backButton->setEnabled(inHistory > 0); forwardButton->setEnabled(inHistory < history.count() - 1); } void KCharSelect::KCharSelectPrivate::_k_activateSearchLine() { searchLine->setFocus(); searchLine->selectAll(); } void KCharSelect::KCharSelectPrivate::_k_back() { Q_ASSERT(inHistory > 0); showFromHistory(inHistory - 1); } void KCharSelect::KCharSelectPrivate::_k_forward() { Q_ASSERT(inHistory + 1 < history.count()); showFromHistory(inHistory + 1); } void KCharSelect::KCharSelectPrivate::_k_fontSelected() { QFont font = fontCombo->currentFont(); font.setPointSize(fontSizeSpinBox->value()); charTable->setFont(font); emit q->currentFontChanged(font); } void KCharSelect::KCharSelectPrivate::_k_charSelected(uint c) { if (!allPlanesEnabled) { emit q->charSelected(QChar(c)); } emit q->codePointSelected(c); } void KCharSelect::KCharSelectPrivate::_k_updateCurrentChar(uint c) { if (!allPlanesEnabled) { emit q->currentCharChanged(QChar(c)); } emit q->currentCodePointChanged(c); if (searchMode) { //we are in search mode. make the two comboboxes show the section & block for this character. //(when we are not in search mode the current character always belongs to the current section & block.) int block = s_data()->blockIndex(c); int section = s_data()->sectionIndex(block); sectionCombo->setCurrentIndex(section); int index = blockCombo->findData(block); if (index != -1) { blockCombo->setCurrentIndex(index); } } if (searchLine) { historyAdd(c, searchMode, searchLine->text()); } _k_slotUpdateUnicode(c); } void KCharSelect::KCharSelectPrivate::_k_slotUpdateUnicode(uint c) { QString html = QLatin1String("

") + tr("Character:") + QLatin1Char(' ') + s_data()->display(c, charTable->font()) + QLatin1Char(' ') + s_data()->formatCode(c) + QLatin1String("
"); QString name = s_data()->name(c); if (!name.isEmpty()) { //is name ever empty?

should always be there... html += tr("Name: ") + name.toHtmlEscaped() + QLatin1String("

"); } const QStringList aliases = s_data()->aliases(c); const QStringList notes = s_data()->notes(c); const QVector seeAlso = s_data()->seeAlso(c); const QStringList equivalents = s_data()->equivalents(c); const QStringList approxEquivalents = s_data()->approximateEquivalents(c); const QVector decomposition = s_data()->decomposition(c); if (!(aliases.isEmpty() && notes.isEmpty() && seeAlso.isEmpty() && equivalents.isEmpty() && approxEquivalents.isEmpty() && decomposition.isEmpty())) { html += QLatin1String("

") + tr("Annotations and Cross References") + QLatin1String("

"); } if (!aliases.isEmpty()) { html += QLatin1String("

") + tr("Alias names:") + QLatin1String("

    "); for (const QString &alias : aliases) { html += QLatin1String("
  • ") + alias.toHtmlEscaped() + QLatin1String("
  • "); } html += QLatin1String("
"); } if (!notes.isEmpty()) { html += QLatin1String("

") + tr("Notes:") + QLatin1String("

    "); for (const QString ¬e : notes) { html += QLatin1String("
  • ") + createLinks(note.toHtmlEscaped()) + QLatin1String("
  • "); } html += QLatin1String("
"); } if (!seeAlso.isEmpty()) { html += QLatin1String("

") + tr("See also:") + QLatin1String("

"); } if (!equivalents.isEmpty()) { html += QLatin1String("

") + tr("Equivalents:") + QLatin1String("

    "); for (const QString &equivalent : equivalents) { html += QLatin1String("
  • ") + createLinks(equivalent.toHtmlEscaped()) + QLatin1String("
  • "); } html += QLatin1String("
"); } if (!approxEquivalents.isEmpty()) { html += QLatin1String("

") + tr("Approximate equivalents:") + QLatin1String("

    "); for (const QString &approxEquivalent : approxEquivalents) { html += QLatin1String("
  • ") + createLinks(approxEquivalent.toHtmlEscaped()) + QLatin1String("
  • "); } html += QLatin1String("
"); } if (!decomposition.isEmpty()) { html += QLatin1String("

") + tr("Decomposition:") + QLatin1String("

    "); for (uint c2 : decomposition) { if (!allPlanesEnabled && QChar::requiresSurrogates(c2)) { continue; } html += QLatin1String("
  • ") + createLinks(s_data()->formatCode(c2, 4, QString())) + QLatin1String("
  • "); } html += QLatin1String("
"); } QStringList unihan = s_data()->unihanInfo(c); if (unihan.count() == 7) { html += QLatin1String("

") + tr("CJK Ideograph Information") + QLatin1String("

"); bool newline = true; if (!unihan[0].isEmpty()) { html += tr("Definition in English: ") + unihan[0]; newline = false; } if (!unihan[2].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Mandarin Pronunciation: ") + unihan[2]; newline = false; } if (!unihan[1].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Cantonese Pronunciation: ") + unihan[1]; newline = false; } if (!unihan[6].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Japanese On Pronunciation: ") + unihan[6]; newline = false; } if (!unihan[5].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Japanese Kun Pronunciation: ") + unihan[5]; newline = false; } if (!unihan[3].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Tang Pronunciation: ") + unihan[3]; newline = false; } if (!unihan[4].isEmpty()) { if (!newline) { html += QLatin1String("
"); } html += tr("Korean Pronunciation: ") + unihan[4]; newline = false; } html += QLatin1String("

"); } html += QLatin1String("

") + tr("General Character Properties") + QLatin1String("
"); html += tr("Block: ") + s_data()->block(c) + QLatin1String("
"); html += tr("Unicode category: ") + s_data()->categoryText(s_data()->category(c)) + QLatin1String("

"); const QByteArray utf8 = QString::fromUcs4(&c, 1).toUtf8(); html += QLatin1String("

") + tr("Various Useful Representations") + QLatin1String("
"); html += tr("UTF-8:"); for (unsigned char c : utf8) { html += QLatin1Char(' ') + s_data()->formatCode(c, 2, QStringLiteral("0x")); } html += QLatin1String("
") + tr("UTF-16: "); if (QChar::requiresSurrogates(c)) { html += s_data()->formatCode(QChar::highSurrogate(c), 4, QStringLiteral("0x")); html += QLatin1Char(' ') + s_data->formatCode(QChar::lowSurrogate(c), 4, QStringLiteral("0x")); } else { html += s_data()->formatCode(c, 4, QStringLiteral("0x")); } html += QLatin1String("
") + tr("C octal escaped UTF-8: "); for (unsigned char c : utf8) { html += s_data()->formatCode(c, 3, QStringLiteral("\\"), 8); } html += QLatin1String("
") + tr("XML decimal entity:") + QLatin1String(" &#") + QString::number(c) + QLatin1String(";

"); detailBrowser->setHtml(html); } QString KCharSelect::KCharSelectPrivate::createLinks(QString s) { const QRegularExpression rx(QStringLiteral("\\b([\\dABCDEF]{4,5})\\b")); QRegularExpressionMatchIterator iter = rx.globalMatch(s); QRegularExpressionMatch match; - QStringList chars; + QSet chars; while (iter.hasNext()) { match = iter.next(); - chars << match.captured(1); + chars.insert(match.captured(1)); } - const QSet chars2 = QSet::fromList(chars); - for (const QString &c : chars2) { + for (const QString &c : qAsConst(chars)) { int unicode = c.toInt(nullptr, 16); if (!allPlanesEnabled && QChar::requiresSurrogates(unicode)) { continue; } QString link = QLatin1String(""); if (s_data()->isPrint(unicode)) { link += QLatin1String("‎&#") + QString::number(unicode) + QLatin1String("; "); } link += QLatin1String("U+") + c + QLatin1Char(' '); link += s_data()->name(unicode).toHtmlEscaped() + QLatin1String(""); s.replace(c, link); } return s; } void KCharSelect::KCharSelectPrivate::_k_sectionSelected(int index) { blockCombo->clear(); const QVector blocks = s_data()->sectionContents(index); for (int block : blocks) { if (!allPlanesEnabled) { const QVector contents = s_data()->blockContents(block); if (!contents.isEmpty() && QChar::requiresSurrogates(contents.at(0))) { continue; } } blockCombo->addItem(s_data()->blockName(block), QVariant(block)); } blockCombo->setCurrentIndex(0); } void KCharSelect::KCharSelectPrivate::_k_blockSelected(int index) { if (index == -1) { //the combo box has been cleared and is about to be filled again (because the section has changed) return; } if (searchMode) { //we are in search mode, so don't fill the table with this block. return; } int block = blockCombo->itemData(index).toInt(); const QVector contents = s_data()->blockContents(block); charTable->setContents(contents); emit q->displayedCharsChanged(); charTable->setChar(contents[0]); } void KCharSelect::KCharSelectPrivate::_k_searchEditChanged() { if (searchLine->text().isEmpty()) { sectionCombo->setEnabled(true); blockCombo->setEnabled(true); //upon leaving search mode, keep the same character selected searchMode = false; uint c = charTable->chr(); bool oldHistoryEnabled = historyEnabled; historyEnabled = false; _k_blockSelected(blockCombo->currentIndex()); historyEnabled = oldHistoryEnabled; q->setCurrentCodePoint(c); } else { sectionCombo->setEnabled(false); blockCombo->setEnabled(false); int length = searchLine->text().length(); if (length >= 3) { _k_search(); } } } void KCharSelect::KCharSelectPrivate::_k_search() { if (searchLine->text().isEmpty()) { return; } searchMode = true; QVector contents = s_data()->find(searchLine->text()); if (!allPlanesEnabled) { QVector::iterator it = contents.begin(); while (it != contents.end()) { if (QChar::requiresSurrogates(*it)) { it = contents.erase(it); } else { ++it; } } } charTable->setContents(contents); emit q->displayedCharsChanged(); if (!contents.isEmpty()) { charTable->setChar(contents[0]); } } void KCharSelect::KCharSelectPrivate::_k_linkClicked(QUrl url) { QString hex = url.toString(); if (hex.size() > 6) { return; } int unicode = hex.toInt(nullptr, 16); if (unicode > QChar::LastValidCodePoint) { return; } searchLine->clear(); q->setCurrentCodePoint(unicode); } //// QVariant KCharSelectItemModel::data(const QModelIndex &index, int role) const { int pos = m_columns * (index.row()) + index.column(); if (!index.isValid() || pos < 0 || pos >= m_chars.size() || index.row() < 0 || index.column() < 0) { if (role == Qt::BackgroundRole) { return QVariant(qApp->palette().color(QPalette::Button)); } return QVariant(); } uint c = m_chars[pos]; if (role == Qt::ToolTipRole) { QString result = s_data()->display(c, m_font) + QLatin1String("
") + s_data()->name(c).toHtmlEscaped() + QLatin1String("
") + tr("Unicode code point:") + QLatin1Char(' ') + s_data()->formatCode(c) + QLatin1String("
") + tr("In decimal", "Character") + QLatin1Char(' ') + QString::number(c); return QVariant(result); } else if (role == Qt::TextAlignmentRole) { return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } else if (role == Qt::DisplayRole) { if (s_data()->isPrint(c)) { return QVariant(QString::fromUcs4(&c, 1)); } return QVariant(); } else if (role == Qt::BackgroundRole) { QFontMetrics fm = QFontMetrics(m_font); if (fm.inFontUcs4(c) && s_data()->isPrint(c)) { return QVariant(qApp->palette().color(QPalette::Base)); } else { return QVariant(qApp->palette().color(QPalette::Button)); } } else if (role == Qt::FontRole) { return QVariant(m_font); } else if (role == CharacterRole) { return QVariant(c); } return QVariant(); } bool KCharSelectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row) Q_UNUSED(parent) if (action == Qt::IgnoreAction) { return true; } if (!data->hasText()) { return false; } if (column > 0) { return false; } QString text = data->text(); if (text.isEmpty()) { return false; } emit showCharRequested(text.toUcs4().at(0)); return true; } void KCharSelectItemModel::setColumnCount(int columns) { if (columns == m_columns) { return; } emit layoutAboutToBeChanged(); m_columns = columns; emit layoutChanged(); } #include "moc_kcharselect.cpp" #include "moc_kcharselect_p.cpp" diff --git a/src/kviewstateserializer.cpp b/src/kviewstateserializer.cpp index a5ffbb6..25df639 100644 --- a/src/kviewstateserializer.cpp +++ b/src/kviewstateserializer.cpp @@ -1,375 +1,385 @@ /* Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net, author Stephen Kelly This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kviewstateserializer.h" #include #include #include #include #include class KViewStateSerializerPrivate { public: KViewStateSerializerPrivate(KViewStateSerializer *qq) : q_ptr(qq), m_treeView(nullptr), m_view(nullptr), m_selectionModel(nullptr), m_scrollArea(nullptr), m_horizontalScrollBarValue(-1), m_verticalScrollBarValue(-1) { } Q_DECLARE_PUBLIC(KViewStateSerializer) KViewStateSerializer *const q_ptr; QStringList getExpandedItems(const QModelIndex &index) const; void listenToPendingChanges(); void processPendingChanges(); inline void restoreScrollBarState() { if (!m_scrollArea) { return; } if (m_horizontalScrollBarValue >= 0 && m_horizontalScrollBarValue <= m_scrollArea->horizontalScrollBar()->maximum()) { m_scrollArea->horizontalScrollBar()->setValue(m_horizontalScrollBarValue); m_horizontalScrollBarValue = -1; } if (m_verticalScrollBarValue >= 0 && m_verticalScrollBarValue <= m_scrollArea->verticalScrollBar()->maximum()) { m_scrollArea->verticalScrollBar()->setValue(m_verticalScrollBarValue); m_verticalScrollBarValue = -1; } } void restoreSelection(); void restoreCurrentItem(); void restoreExpanded(); inline bool hasPendingChanges() const { return !m_pendingCurrent.isEmpty() || !m_pendingExpansions.isEmpty() || !m_pendingSelections.isEmpty(); } const QAbstractItemModel *getModel() { if (m_selectionModel && m_selectionModel->model()) { return m_selectionModel->model(); } else if (m_view && m_view->model()) { return m_view->model(); } return nullptr; } void rowsInserted(const QModelIndex &/*index*/, int /*start*/, int /*end*/) { Q_Q(KViewStateSerializer); processPendingChanges(); if (!hasPendingChanges()) { q->disconnect(getModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), q, SLOT(rowsInserted(QModelIndex,int,int))); q->deleteLater(); } } QTreeView *m_treeView; QAbstractItemView *m_view; QItemSelectionModel *m_selectionModel; QPointer m_scrollArea; int m_horizontalScrollBarValue; int m_verticalScrollBarValue; QSet m_pendingSelections; QSet m_pendingExpansions; QString m_pendingCurrent; }; KViewStateSerializer::KViewStateSerializer(QObject *parent) : QObject(nullptr), d_ptr(new KViewStateSerializerPrivate(this)) { Q_UNUSED(parent); qRegisterMetaType("QModelIndex"); } KViewStateSerializer::~KViewStateSerializer() { delete d_ptr; } void KViewStateSerializer::setView(QAbstractItemView *view) { Q_D(KViewStateSerializer); d->m_scrollArea = view; if (view) { d->m_selectionModel = view->selectionModel(); d->m_treeView = qobject_cast(view); } else { d->m_selectionModel = nullptr; d->m_treeView = nullptr; } d->m_view = view; } QAbstractItemView *KViewStateSerializer::view() const { Q_D(const KViewStateSerializer); return d->m_view; } QItemSelectionModel *KViewStateSerializer::selectionModel() const { Q_D(const KViewStateSerializer); return d->m_selectionModel; } void KViewStateSerializer::setSelectionModel(QItemSelectionModel *selectionModel) { Q_D(KViewStateSerializer); d->m_selectionModel = selectionModel; } void KViewStateSerializerPrivate::listenToPendingChanges() { Q_Q(KViewStateSerializer); // watch the model for stuff coming in delayed if (hasPendingChanges()) { const QAbstractItemModel *model = getModel(); if (model) { q->disconnect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), q, SLOT(rowsInserted(QModelIndex,int,int))); q->connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); return; } else { q->deleteLater(); } } else { q->deleteLater(); } } void KViewStateSerializerPrivate::processPendingChanges() { Q_Q(KViewStateSerializer); q->restoreCurrentItem(m_pendingCurrent); q->restoreSelection(m_pendingSelections.values()); q->restoreExpanded(m_pendingExpansions.values()); q->restoreScrollState(m_verticalScrollBarValue, m_horizontalScrollBarValue); } QStringList KViewStateSerializerPrivate::getExpandedItems(const QModelIndex &index) const { Q_Q(const KViewStateSerializer); QStringList expansion; for (int i = 0; i < m_treeView->model()->rowCount(index); ++i) { const QModelIndex child = m_treeView->model()->index(i, 0, index); // http://bugreports.qt.nokia.com/browse/QTBUG-18039 if (m_treeView->model()->hasChildren(child)) { if (m_treeView->isExpanded(child)) { expansion << q->indexToConfigString(child); } expansion << getExpandedItems(child); } } return expansion; } void KViewStateSerializerPrivate::restoreCurrentItem() { Q_Q(KViewStateSerializer); QModelIndex currentIndex = q->indexFromConfigString(m_selectionModel->model(), m_pendingCurrent); if (currentIndex.isValid()) { if (m_treeView) { m_treeView->setCurrentIndex(currentIndex); } else { m_selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); } m_pendingCurrent.clear(); } } void KViewStateSerializer::restoreCurrentItem(const QString &indexString) { Q_D(KViewStateSerializer); if (!d->m_selectionModel || !d->m_selectionModel->model()) { return; } if (indexString.isEmpty()) { return; } d->m_pendingCurrent = indexString; d->restoreCurrentItem(); if (d->hasPendingChanges()) { d->listenToPendingChanges(); } } void KViewStateSerializerPrivate::restoreExpanded() { Q_Q(KViewStateSerializer); QSet::iterator it = m_pendingExpansions.begin(); for (; it != m_pendingExpansions.end();) { QModelIndex idx = q->indexFromConfigString(m_treeView->model(), *it); if (idx.isValid()) { m_treeView->expand(idx); it = m_pendingExpansions.erase(it); } else { ++it; } } } void KViewStateSerializer::restoreExpanded(const QStringList &indexStrings) { Q_D(KViewStateSerializer); if (!d->m_treeView || !d->m_treeView->model()) { return; } if (indexStrings.isEmpty()) { return; } +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) d->m_pendingExpansions.unite(indexStrings.toSet()); +#else + d->m_pendingExpansions.unite(QSet(indexStrings.begin(), indexStrings.end())); +#endif + d->restoreExpanded(); if (d->hasPendingChanges()) { d->listenToPendingChanges(); } } void KViewStateSerializer::restoreScrollState(int verticalScoll, int horizontalScroll) { Q_D(KViewStateSerializer); if (!d->m_scrollArea) { return; } d->m_verticalScrollBarValue = verticalScoll; d->m_horizontalScrollBarValue = horizontalScroll; QTimer::singleShot(0, this, SLOT(restoreScrollBarState())); } void KViewStateSerializerPrivate::restoreSelection() { Q_Q(KViewStateSerializer); QSet::iterator it = m_pendingSelections.begin(); for (; it != m_pendingSelections.end();) { QModelIndex idx = q->indexFromConfigString(m_selectionModel->model(), *it); if (idx.isValid()) { m_selectionModel->select(idx, QItemSelectionModel::Select); it = m_pendingSelections.erase(it); } else { ++it; } } } void KViewStateSerializer::restoreSelection(const QStringList &indexStrings) { Q_D(KViewStateSerializer); if (!d->m_selectionModel || !d->m_selectionModel->model()) { return; } if (indexStrings.isEmpty()) { return; } +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) d->m_pendingSelections.unite(indexStrings.toSet()); +#else + d->m_pendingSelections.unite(QSet(indexStrings.begin(), indexStrings.end())); +#endif + d->restoreSelection(); if (d->hasPendingChanges()) { d->listenToPendingChanges(); } } QString KViewStateSerializer::currentIndexKey() const { Q_D(const KViewStateSerializer); if (!d->m_selectionModel) { return QString(); } return indexToConfigString(d->m_selectionModel->currentIndex()); } QStringList KViewStateSerializer::expansionKeys() const { Q_D(const KViewStateSerializer); if (!d->m_treeView || !d->m_treeView->model()) { return QStringList(); } return d->getExpandedItems(QModelIndex()); } QStringList KViewStateSerializer::selectionKeys() const { Q_D(const KViewStateSerializer); if (!d->m_selectionModel) { return QStringList(); } const QModelIndexList selectedIndexes = d->m_selectionModel->selectedRows(); QStringList selection; selection.reserve(selectedIndexes.count()); for (const QModelIndex &index : selectedIndexes) { selection << indexToConfigString(index); } return selection; } QPair KViewStateSerializer::scrollState() const { Q_D(const KViewStateSerializer); return qMakePair(d->m_scrollArea->verticalScrollBar()->value(), d->m_scrollArea->horizontalScrollBar()->value()); } void KViewStateSerializer::restoreState() { Q_D(KViewStateSerializer); // Delete myself if not finished after 60 seconds QTimer::singleShot(60000, this, &KViewStateSerializer::deleteLater); d->processPendingChanges(); if (d->hasPendingChanges()) { d->listenToPendingChanges(); } } #include "moc_kviewstateserializer.cpp"