/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999 Reginald Stadlbauer
SPDX-FileCopyrightText: 2017 Harald Sitter
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#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 *qq)
: q(qq)
{
}
KCharSelectTable *const q;
QFont font;
KCharSelectItemModel *model = nullptr;
QList chars;
uint chr = 0;
void resizeCells();
void doubleClicked(const QModelIndex &index);
void slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
};
class KCharSelectPrivate
{
Q_DECLARE_TR_FUNCTIONS(KCharSelect)
public:
struct HistoryItem {
uint c;
bool fromSearch;
QString searchString;
};
enum { MaxHistoryItems = 100 };
KCharSelectPrivate(KCharSelect *qq)
: q(qq)
{
}
KCharSelect *const 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 = false; // a search is active
bool historyEnabled = false;
bool allPlanesEnabled = false;
int inHistory = 0; // index of current char in history
QList history;
QObject *actionParent = nullptr;
QString createLinks(QString s);
void historyAdd(uint c, bool fromSearch, const QString &searchString);
void showFromHistory(int index);
void updateBackForwardButtons();
void activateSearchLine();
void back();
void forward();
void fontSelected();
void charSelected(uint c);
void updateCurrentChar(uint c);
void slotUpdateUnicode(uint c);
void sectionSelected(int index);
void blockSelected(int index);
void searchEditChanged();
void search();
void linkClicked(QUrl url);
};
Q_DECLARE_TYPEINFO(KCharSelectPrivate::HistoryItem, Q_RELOCATABLE_TYPE);
/******************************************************************/
/* 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, &KCharSelectTable::doubleClicked, this, [this](const QModelIndex &index) {
d->doubleClicked(index);
});
d->resizeCells();
}
KCharSelectTable::~KCharSelectTable() = default;
void KCharSelectTable::setFont(const QFont &_font)
{
QTableView::setFont(_font);
d->font = _font;
if (d->model) {
d->model->setFont(_font);
}
d->resizeCells();
}
uint KCharSelectTable::chr()
{
return d->chr;
}
QFont KCharSelectTable::font() const
{
return d->font;
}
QList 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 QList &chars)
{
d->chars = chars;
auto oldModel = d->model;
d->model = new KCharSelectItemModel(chars, d->font, this);
setModel(d->model);
d->resizeCells();
// Setting a model changes the selectionModel. Make sure to always reconnect.
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selected, const QItemSelection &deselected) {
d->slotSelectionChanged(selected, deselected);
});
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::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.userType() != QMetaType::UInt) {
return;
}
uint c = temp.toUInt();
chr = c;
Q_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->resizeCells();
timer->deleteLater();
});
timer->start(0);
}
}
void KCharSelectTablePrivate::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 QList chars = model->chars();
for (int i = 0; i < chars.size(); ++i) {
char32_t 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::doubleClicked(const QModelIndex &index)
{
uint c = model->data(index, KCharSelectItemModel::CharacterRole).toUInt();
if (s_data()->isPrint(c)) {
Q_EMIT q->activated(c);
}
}
void KCharSelectTable::keyPressEvent(QKeyEvent *e)
{
if (d->model) {
switch (e->key()) {
case Qt::Key_Space:
Q_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)) {
Q_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…", "@info:placeholder"));
d->searchLine->setClearButtonEnabled(true);
d->searchLine->setToolTip(tr("Enter a search term or character here", "@info:tooltip"));
QAction *findAction = new QAction(this);
connect(findAction, &QAction::triggered, this, [this]() {
d->activateSearchLine();
});
findAction->setObjectName(QStringLiteral("edit_find"));
findAction->setText(tr("&Find…", "@action"));
findAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
attachToActionParent(findAction, actionParent, QKeySequence::keyBindings(QKeySequence::Find));
connect(d->searchLine, &QLineEdit::textChanged, this, [this]() {
d->searchEditChanged();
});
connect(d->searchLine, &QLineEdit::returnPressed, this, [this]() {
d->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", "@action:button Goes to previous character"));
d->backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
d->backButton->setToolTip(tr("Go to previous character in history", "@info:tooltip"));
d->forwardButton = new QToolButton(this);
comboLayout->addWidget(d->forwardButton);
d->forwardButton->setEnabled(false);
d->forwardButton->setText(tr("Next in History", "@action:button Goes to next character"));
d->forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
d->forwardButton->setToolTip(tr("Go to next character in history", "info:tooltip"));
QAction *backAction = new QAction(this);
connect(backAction, &QAction::triggered, d->backButton, &QAbstractButton::animateClick);
backAction->setObjectName(QStringLiteral("go_back"));
backAction->setText(tr("&Back", "@action 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", "@action 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->back();
});
connect(d->forwardButton, &QToolButton::clicked, this, [this]() {
d->forward();
});
d->sectionCombo = new QComboBox(this);
d->sectionCombo->setObjectName(QStringLiteral("sectionCombo"));
d->sectionCombo->setToolTip(tr("Select a category", "@info:tooltip"));
comboLayout->addWidget(d->sectionCombo);
d->blockCombo = new QComboBox(this);
d->blockCombo->setObjectName(QStringLiteral("blockCombo"));
d->blockCombo->setToolTip(tr("Select a block to be displayed", "@info:tooltip"));
d->blockCombo->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
comboLayout->addWidget(d->blockCombo, 1);
QStringList sectionList = s_data()->sectionList();
d->sectionCombo->addItems(sectionList);
d->blockCombo->setMinimumWidth(QFontMetrics(QWidget::font()).averageCharWidth() * 25);
connect(d->sectionCombo, &QComboBox::currentIndexChanged, this, [this](int index) {
d->sectionSelected(index);
});
connect(d->blockCombo, &QComboBox::currentIndexChanged, this, [this](int index) {
d->blockSelected(index);
});
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", "@info:tooltip"));
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", "@info:tooltip"));
connect(d->fontCombo, &QFontComboBox::currentFontChanged, this, [this]() {
d->fontSelected();
});
connect(d->fontSizeSpinBox, &QSpinBox::valueChanged, this, [this]() {
d->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);
} 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, &KCharSelectTable::focusItemChanged, this, [this](uint c) {
d->updateCurrentChar(c);
});
connect(d->charTable, &KCharSelectTable::activated, this, [this](uint c) {
d->charSelected(c);
});
connect(d->charTable, &KCharSelectTable::showCharRequested, this, &KCharSelect::setCurrentCodePoint);
d->detailBrowser = new QTextBrowser(this);
if (DetailBrowser & controls) {
splitter->addWidget(d->detailBrowser);
} else {
d->detailBrowser->hide();
}
d->detailBrowser->setOpenLinks(false);
connect(d->detailBrowser, &QTextBrowser::anchorClicked, this, [this](const QUrl &url) {
d->linkClicked(url);
});
setFocusPolicy(Qt::StrongFocus);
if (SearchLine & controls) {
setFocusProxy(d->searchLine);
} else {
setFocusProxy(d->charTable);
}
d->sectionSelected(1); // this will also call blockSelected(0)
setCurrentCodePoint(QChar::Null);
d->historyEnabled = true;
}
KCharSelect::~KCharSelect() = default;
QSize KCharSelect::sizeHint() const
{
return QWidget::sizeHint();
}
void KCharSelect::setCurrentFont(const QFont &_font)
{
d->fontCombo->setCurrentFont(_font);
d->fontSizeSpinBox->setValue(_font.pointSize());
d->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;
}
QList 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 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 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);
search();
}
charTable->setChar(item.c);
} else {
searchLine->clear();
q->setCurrentCodePoint(item.c);
}
historyEnabled = oldHistoryEnabled;
}
void KCharSelectPrivate::updateBackForwardButtons()
{
backButton->setEnabled(inHistory > 0);
forwardButton->setEnabled(inHistory < history.count() - 1);
}
void KCharSelectPrivate::activateSearchLine()
{
searchLine->setFocus();
searchLine->selectAll();
}
void KCharSelectPrivate::back()
{
Q_ASSERT(inHistory > 0);
showFromHistory(inHistory - 1);
}
void KCharSelectPrivate::forward()
{
Q_ASSERT(inHistory + 1 < history.count());
showFromHistory(inHistory + 1);
}
void KCharSelectPrivate::fontSelected()
{
QFont font = fontCombo->currentFont();
font.setPointSize(fontSizeSpinBox->value());
charTable->setFont(font);
Q_EMIT q->currentFontChanged(font);
}
void KCharSelectPrivate::charSelected(uint c)
{
if (!allPlanesEnabled) {
Q_EMIT q->charSelected(QChar(c));
}
Q_EMIT q->codePointSelected(c);
}
void KCharSelectPrivate::updateCurrentChar(uint c)
{
if (!allPlanesEnabled) {
Q_EMIT q->currentCharChanged(QChar(c));
}
Q_EMIT q->currentCodePointChanged(c);
if (searchMode || sectionCombo->currentIndex() == 0) {
// 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
// all characters mode).
//(when we are not in search mode nor in the all characters mode the current character always belongs to the current section & block.)
int block = s_data()->blockIndex(c);
if (searchMode) {
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());
}
slotUpdateUnicode(c);
}
void KCharSelectPrivate::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 QList seeAlso = s_data()->seeAlso(c);
const QStringList equivalents = s_data()->equivalents(c);
const QStringList approxEquivalents = s_data()->approximateEquivalents(c);
const QList 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(reinterpret_cast(&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 KCharSelectPrivate::createLinks(QString s)
{
static const QRegularExpression rx(QStringLiteral("\\b([\\dABCDEF]{4,5})\\b"), QRegularExpression::UseUnicodePropertiesOption);
QRegularExpressionMatchIterator iter = rx.globalMatch(s);
QRegularExpressionMatch match;
QSet chars;
while (iter.hasNext()) {
match = iter.next();
chars.insert(match.captured(1));
}
for (const QString &c : std::as_const(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 KCharSelectPrivate::sectionSelected(int index)
{
blockCombo->clear();
QList chars;
const QList blocks = s_data()->sectionContents(index);
for (int block : blocks) {
if (!allPlanesEnabled) {
const QList contents = s_data()->blockContents(block);
if (!contents.isEmpty() && QChar::requiresSurrogates(contents.at(0))) {
continue;
}
}
blockCombo->addItem(s_data()->blockName(block), QVariant(block));
if (index == 0) {
chars << s_data()->blockContents(block);
}
}
if (index == 0) {
charTable->setContents(chars);
updateCurrentChar(charTable->chr());
} else {
blockCombo->setCurrentIndex(0);
}
}
void KCharSelectPrivate::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();
if (sectionCombo->currentIndex() == 0 && block == s_data()->blockIndex(charTable->chr())) {
// the selected block already contains the selected character
return;
}
const QList contents = s_data()->blockContents(block);
if (sectionCombo->currentIndex() > 0) {
charTable->setContents(contents);
}
Q_EMIT q->displayedCharsChanged();
charTable->setChar(contents[0]);
}
void KCharSelectPrivate::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;
blockSelected(blockCombo->currentIndex());
historyEnabled = oldHistoryEnabled;
q->setCurrentCodePoint(c);
} else {
sectionCombo->setEnabled(false);
blockCombo->setEnabled(false);
int length = searchLine->text().length();
if (length >= 3) {
search();
}
}
}
void KCharSelectPrivate::search()
{
if (searchLine->text().isEmpty()) {
return;
}
searchMode = true;
QList contents = s_data()->find(searchLine->text());
if (!allPlanesEnabled) {
contents.erase(std::remove_if(contents.begin(), contents.end(), QChar::requiresSurrogates), contents.end());
}
charTable->setContents(contents);
Q_EMIT q->displayedCharsChanged();
if (!contents.isEmpty()) {
charTable->setChar(contents[0]);
}
}
void KCharSelectPrivate::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();
}
char32_t 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;
}
Q_EMIT showCharRequested(text.toUcs4().at(0));
return true;
}
void KCharSelectItemModel::setColumnCount(int columns)
{
if (columns == m_columns) {
return;
}
Q_EMIT layoutAboutToBeChanged();
m_columns = columns;
Q_EMIT layoutChanged();
}
#include "moc_kcharselect.cpp"
#include "moc_kcharselect_p.cpp"