Commit 2aa203eb authored by Jan Paul Batrina's avatar Jan Paul Batrina
Browse files

Add Inline Color Picker Plugin

Inspired by https://kate-editor.org/2018/08/17/kate-gains-support-for-inline-notes/.

The supported colors are from QColor. See https://doc.qt.io/qt-5/qcolor.html#setNamedColor.
parent 752704b9
......@@ -3,6 +3,7 @@ find_package(KF5TextEditor ${KF5_DEP_VERSION} QUIET REQUIRED)
ecm_optional_add_subdirectory(backtracebrowser)
ecm_optional_add_subdirectory(close-except-like) # Close all documents except this one (or similar).
ecm_optional_add_subdirectory(colorpicker) # Inline color preview/picker
ecm_optional_add_subdirectory(externaltools)
ecm_optional_add_subdirectory(filebrowser)
ecm_optional_add_subdirectory(filetree)
......
add_library(katecolorpickerplugin MODULE "")
target_compile_definitions(katecolorpickerplugin PRIVATE TRANSLATION_DOMAIN="katecolorpickerplugin")
target_link_libraries(katecolorpickerplugin PRIVATE KF5::TextEditor)
target_sources(
katecolorpickerplugin
PRIVATE
katecolorpickerplugin.cpp
colorpickerconfigpage.cpp
)
kcoreaddons_desktop_to_json(katecolorpickerplugin katecolorpickerplugin.desktop)
install(TARGETS katecolorpickerplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor)
#! /bin/sh
$EXTRACTRC *.rc >> rc.cpp
$XGETTEXT *.cpp -o $podir/katecolorpickerplugin.pot
/*
SPDX-FileCopyrightText: 2018 Sven Brauch <mail@svenbrauch.de>
SPDX-FileCopyrightText: 2018 Michal Srb <michalsrb@gmail.com>
SPDX-FileCopyrightText: 2020 Jan Paul Batrina <jpmbatrina01@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "colorpickerconfigpage.h"
#include <KTextEditor/ConfigPage>
#include <KLocalizedString>
#include <KConfigGroup>
#include <KSharedConfig>
#include <QApplication>
#include <QCheckBox>
#include <QGroupBox>
#include <QStyle>
#include <QVBoxLayout>
KTextEditor::ConfigPage *KateColorPickerPlugin::configPage(int number, QWidget *parent)
{
if (number != 0) {
return nullptr;
}
return new KateColorPickerConfigPage(parent, this);
}
KateColorPickerConfigPage::KateColorPickerConfigPage(QWidget *parent, KateColorPickerPlugin *plugin)
: KTextEditor::ConfigPage(parent)
, m_plugin(plugin)
, m_colorConfigChanged(true)
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
chkNamedColors = new QCheckBox(i18n("Show preview for known color names"), this);
chkNamedColors->setToolTip(i18n("Also show the color picker for known color names (e.g. skyblue).\nSee https://www.w3.org/TR/SVG11/types.html#ColorKeywords for the list of colors."));
layout->addWidget(chkNamedColors);
chkPreviewAfterColor = new QCheckBox(i18n("Place preview after text color"), this);
layout->addWidget(chkPreviewAfterColor);
connect(chkNamedColors, &QCheckBox::stateChanged, this, &KateColorPickerConfigPage::changed);
connect(chkPreviewAfterColor, &QCheckBox::stateChanged, this, &KateColorPickerConfigPage::changed);
QGroupBox *hexGroup = new QGroupBox(i18n("Hex color matching"), this);
QVBoxLayout *hexLayout = new QVBoxLayout();
// Hex color formats supported by QColor. See https://doc.qt.io/qt-5/qcolor.html#setNamedColor
chkHexLengths.insert(12, new QCheckBox(i18n("12 digits (#RRRRGGGGBBBB)"), this));
chkHexLengths.insert(9, new QCheckBox(i18n("9 digits (#RRRGGGBBB)"), this));
chkHexLengths.insert(8, new QCheckBox(i18n("8 digits (#AARRGGBB)"), this));
chkHexLengths.insert(6, new QCheckBox(i18n("6 digits (#RRGGBB)"), this));
chkHexLengths.insert(3, new QCheckBox(i18n("3 digits (#RGB)"), this));
for (QCheckBox *chk : chkHexLengths.values()) {
hexLayout->addWidget(chk);
connect(chk, &QCheckBox::stateChanged, this, &KateColorPickerConfigPage::changed);
}
hexGroup->setLayout(hexLayout);
layout->addWidget(hexGroup);
layout->addStretch();
connect(this, &KateColorPickerConfigPage::changed, this, [this]() {
m_colorConfigChanged = true;
});
reset();
}
QString KateColorPickerConfigPage::name() const
{
return i18n("Color Picker");
}
QString KateColorPickerConfigPage::fullName() const
{
return i18n("Color Picker Settings");
}
QIcon KateColorPickerConfigPage::icon() const
{
return QIcon::fromTheme(QStringLiteral("color-picker"));
}
void KateColorPickerConfigPage::apply()
{
if (!m_colorConfigChanged) {
// apply() gets called when the "Apply" or "OK" button is pressed
// this means that if a user presses "Apply" THEN "OK", the config is updated twice
// since the reconstruction of the regex (and the regeneration of color note positions) is expensive,
// we only update on the first call to apply() before changes are made again
return;
}
KConfigGroup config(KSharedConfig::openConfig(), "ColorPicker");
config.writeEntry("NamedColors", chkNamedColors->isChecked());
config.writeEntry("PreviewAfterColor", chkPreviewAfterColor->isChecked());
QList<int> hexLengths;
for (auto it = chkHexLengths.cbegin(); it != chkHexLengths.cend(); ++it) {
if (it.value()->isChecked()) {
hexLengths.append(it.key());
}
}
config.writeEntry("HexLengths", hexLengths);
config.sync();
m_plugin->readConfig();
m_colorConfigChanged = false;
}
void KateColorPickerConfigPage::reset()
{
KConfigGroup config(KSharedConfig::openConfig(), "ColorPicker");
chkNamedColors->setChecked(config.readEntry("NamedColors", true));
chkPreviewAfterColor->setChecked(config.readEntry("PreviewAfterColor", true));
QList<int> enabledHexLengths = config.readEntry("HexLengths", QList<int>{12, 9, 8, 6, 3});
for (const int hexLength : chkHexLengths.keys()) {
chkHexLengths[hexLength]->setChecked(enabledHexLengths.contains(hexLength));
}
}
/*
SPDX-FileCopyrightText: 2018 Sven Brauch <mail@svenbrauch.de>
SPDX-FileCopyrightText: 2018 Michal Srb <michalsrb@gmail.com>
SPDX-FileCopyrightText: 2020 Jan Paul Batrina <jpmbatrina01@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KATE_COLORPICKER_CONFIGPAGE_H
#define KATE_COLORPICKER_CONFIGPAGE_H
#include "katecolorpickerplugin.h"
#include <KTextEditor/ConfigPage>
#include <QCheckBox>
#include <QMap>
class KateColorPickerConfigPage : public KTextEditor::ConfigPage
{
Q_OBJECT
public:
explicit KateColorPickerConfigPage(QWidget *parent = nullptr, KateColorPickerPlugin *plugin = nullptr);
~KateColorPickerConfigPage() override
{
}
QString name() const override;
QString fullName() const override;
QIcon icon() const override;
void apply() override;
void reset() override;
void defaults() override
{
}
private:
QCheckBox *chkNamedColors;
QCheckBox *chkPreviewAfterColor;
QMap <int, QCheckBox*> chkHexLengths;
KateColorPickerPlugin *m_plugin;
bool m_colorConfigChanged;
};
#endif // KATE_COLORPICKER_H
/*
SPDX-FileCopyrightText: 2018 Sven Brauch <mail@svenbrauch.de>
SPDX-FileCopyrightText: 2018 Michal Srb <michalsrb@gmail.com>
SPDX-FileCopyrightText: 2020 Jan Paul Batrina <jpmbatrina01@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "katecolorpickerplugin.h"
#include "colorpickerconfigpage.h"
#include <algorithm>
#include <KTextEditor/Document>
#include <KTextEditor/InlineNoteProvider>
#include <KTextEditor/InlineNoteInterface>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
#include <KConfigGroup>
#include <KPluginFactory>
#include <KSharedConfig>
#include <QColor>
#include <QColorDialog>
#include <QHash>
#include <QPainter>
#include <QRegularExpression>
#include <QVariant>
QRegularExpression ColorPickerInlineNoteProvider::s_colorRegEx = QRegularExpression();
bool ColorPickerInlineNoteProvider::s_putPreviewAfterColor = true;
ColorPickerInlineNoteProvider::ColorPickerInlineNoteProvider(KTextEditor::Document *doc)
: m_doc(doc)
, m_startChangedLines(-1)
, m_previousNumLines(-1)
{
// initialize the color regex
updateColorMatchingCriteria();
s_colorRegEx.setPatternOptions(QRegularExpression::DontCaptureOption);
for (auto view : m_doc->views()) {
qobject_cast<KTextEditor::InlineNoteInterface*>(view)->registerInlineNoteProvider(this);
}
connect(m_doc, &KTextEditor::Document::viewCreated, this, [this](KTextEditor::Document *, KTextEditor::View *view) {
qobject_cast<KTextEditor::InlineNoteInterface*>(view)->registerInlineNoteProvider(this);
});
// textInserted and textRemoved are emitted per line, then the last line is followed by a textChanged signal
connect(m_doc, &KTextEditor::Document::textInserted, this, [this](KTextEditor::Document *, const KTextEditor::Cursor &cur, const QString &) {
int line = cur.line();
if (m_startChangedLines == -1 || m_startChangedLines > line) {
m_startChangedLines = line;
}
});
connect(m_doc, &KTextEditor::Document::textRemoved, this, [this](KTextEditor::Document *, const KTextEditor::Range &range, const QString &) {
int startLine = range.start().line();
if (m_startChangedLines == -1 || m_startChangedLines > startLine) {
m_startChangedLines = startLine;
}
});
connect(m_doc, &KTextEditor::Document::textChanged, this, [this](KTextEditor::Document *) {
int newNumLines = m_doc->lines();
if (m_startChangedLines == -1) {
// textChanged not preceded by textInserted or textRemoved. This probably means that either:
// *empty line(s) were inserted/removed (TODO: Update only the lines directly below the removed/inserted empty line(s))
// *the document is newly opened so we update all lines
updateNotes();
} else {
// if the change involves the insertion/deletion of lines, all lines after it get shifted, so we need to update all of them
int endLine = m_previousNumLines != newNumLines ? newNumLines-1 : -1;
updateNotes(m_startChangedLines, endLine);
}
m_startChangedLines = -1;
m_previousNumLines = newNumLines;
});
updateNotes();
}
ColorPickerInlineNoteProvider::~ColorPickerInlineNoteProvider()
{
for (auto view : m_doc->views()) {
qobject_cast<KTextEditor::InlineNoteInterface*>(view)->unregisterInlineNoteProvider(this);
}
}
void ColorPickerInlineNoteProvider::updateColorMatchingCriteria()
{
QString colorRegex;
KConfigGroup config(KSharedConfig::openConfig(), "ColorPicker");
QList <int> matchHexLengths = config.readEntry("HexLengths", QList<int>{12, 9, 8, 6, 3});
// sort by decreasing number of digits to maximize matched hex
std::sort(matchHexLengths.rbegin(), matchHexLengths.rend());
if (matchHexLengths.size() > 0) {
colorRegex = QLatin1String("#(%1)(?![[:xdigit:]])");
QStringList hexRegex;
for (const int hexLength : matchHexLengths) {
hexRegex.append(QStringLiteral("[[:xdigit:]]{%1}").arg(hexLength));
}
colorRegex = colorRegex.arg(hexRegex.join(QLatin1String("|")));
}
if (config.readEntry("NamedColors", true)) {
if (!colorRegex.isEmpty()) {
colorRegex = QLatin1String("(%1)|").arg(colorRegex);
}
QHash <int, QStringList> colorsByLength;
int numColors = 0;
for (const QString &color : QColor::colorNames()) {
const int colorLength = color.length();
if (!colorsByLength.contains(colorLength)) {
colorsByLength.insert(colorLength, {});
}
colorsByLength[colorLength].append(color);
++numColors;
}
QList<int> colorLengths = colorsByLength.keys();
// sort by descending length so that longer color names are prioritized
std::sort(colorLengths.rbegin(), colorLengths.rend());
QStringList colorNames;
colorNames.reserve(numColors);
for (const int length : colorLengths) {
colorNames.append(colorsByLength[length]);
}
colorRegex.append(QLatin1String("(?<![-\\w])(%1)(?![-\\w])").arg(colorNames.join(QLatin1String("|"))));
}
if (colorRegex.isEmpty()) {
// No matching criteria enabled. Set to regex negative lookahead to match nothing.
colorRegex = QLatin1String("(?!)");
}
s_colorRegEx.setPattern(colorRegex);
s_putPreviewAfterColor = config.readEntry("PreviewAfterColor", true);
}
void ColorPickerInlineNoteProvider::updateNotes(int startLine, int endLine) {
int maxLine = m_doc->lines() - 1;
startLine = startLine < -1 ? -1 : startLine;
endLine = endLine > maxLine ? maxLine : endLine;
if (startLine == -1) {
startLine = 0;
endLine = maxLine;
}
if (endLine == -1) {
endLine = startLine;
}
for (int line = startLine; line <= endLine; ++line) {
m_colorNoteIndices.remove(line);
emit inlineNotesChanged(line);
}
}
QVector<int> ColorPickerInlineNoteProvider::inlineNotes(int line) const
{
if (!m_colorNoteIndices.contains(line)) {
m_colorNoteIndices.insert(line, {});
auto matchIterator = s_colorRegEx.globalMatch(m_doc->line(line).toLower());
while (matchIterator.hasNext()) {
const auto match = matchIterator.next();
int colorOtherIndex = match.capturedStart();
int colorNoteIndex = colorOtherIndex + match.capturedLength();
if (!s_putPreviewAfterColor) {
colorOtherIndex = colorNoteIndex;
colorNoteIndex = match.capturedStart();
}
m_colorNoteIndices[line][colorNoteIndex] = colorOtherIndex;
}
}
return m_colorNoteIndices.value(line).keys().toVector();
}
QSize ColorPickerInlineNoteProvider::inlineNoteSize(const KTextEditor::InlineNote &note) const
{
return QSize(note.lineHeight(), note.lineHeight());
}
void ColorPickerInlineNoteProvider::paintInlineNote(const KTextEditor::InlineNote &note, QPainter& painter) const
{
const auto line = note.position().line();
auto colorEnd = note.position().column();
auto colorStart = m_colorNoteIndices[line][colorEnd];
if (colorStart > colorEnd) {
colorEnd = colorStart;
colorStart = note.position().column();
}
const auto color = QColor(m_doc->text({line, colorStart, line, colorEnd}));
auto penColor = color;
penColor.setAlpha(255);
// ensure that the border color is always visible
painter.setPen( (penColor.value() < 128 ? penColor.lighter(150) : penColor.darker(150)) );
painter.setBrush(color);
painter.drawRoundedRect(1, 1, note.width() - 2, note.lineHeight() - 2, 2, 2);
}
void ColorPickerInlineNoteProvider::inlineNoteActivated(const KTextEditor::InlineNote &note, Qt::MouseButtons, const QPoint &)
{
const auto line = note.position().line();
auto colorEnd = note.position().column();
auto colorStart = m_colorNoteIndices[line][colorEnd];
if (colorStart > colorEnd) {
colorEnd = colorStart;
colorStart = note.position().column();
}
const auto oldColor = QColor(m_doc->text({line, colorStart, line, colorEnd}));
QColorDialog::ColorDialogOptions dialogOptions = QColorDialog::ShowAlphaChannel;
QString title = QLatin1String("Select Color (Hex output)");
if (!m_doc->isReadWrite()) {
dialogOptions |= QColorDialog::NoButtons;
title = QLatin1String("View Color [Read only]");
}
const auto newColor = QColorDialog::getColor(oldColor, const_cast<KTextEditor::View*>(note.view()), title, dialogOptions);
if (!newColor.isValid()) {
return;
}
// include alpha channel if the new color has transparency or the old color included transparency (#AARRGGBB, 9 hex digits)
auto colorNameFormat = (newColor.alpha() != 255 || colorEnd-colorStart == 9) ? QColor::HexArgb : QColor::HexRgb;
m_doc->replaceText({line, colorStart, line, colorEnd}, newColor.name(colorNameFormat));
}
K_PLUGIN_FACTORY_WITH_JSON(KateColorPickerPluginFactory, "katecolorpickerplugin.json", registerPlugin<KateColorPickerPlugin>();)
KateColorPickerPlugin::KateColorPickerPlugin(QObject *parent, const QList<QVariant> &)
: KTextEditor::Plugin(parent)
{
}
KateColorPickerPlugin::~KateColorPickerPlugin()
{
qDeleteAll(m_inlineColorNoteProviders);
}
QObject *KateColorPickerPlugin::createView(KTextEditor::MainWindow *mainWindow)
{
m_mainWindow = mainWindow;
for (auto view : m_mainWindow->views()) {
addDocument(view->document());
}
connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, [this](KTextEditor::View *view) {
addDocument(view->document());
});
return nullptr;
}
void KateColorPickerPlugin::addDocument(KTextEditor::Document *doc) {
if (!m_inlineColorNoteProviders.contains(doc)) {
m_inlineColorNoteProviders.insert(doc, new ColorPickerInlineNoteProvider(doc));
}
connect(doc, &KTextEditor::Document::destroyed, this, [this, doc]() {
m_inlineColorNoteProviders.remove(doc);
});
}
void KateColorPickerPlugin::readConfig() {
ColorPickerInlineNoteProvider::updateColorMatchingCriteria();
for (auto colorNoteProvider : m_inlineColorNoteProviders.values()) {
colorNoteProvider->updateNotes();
}
}
#include "katecolorpickerplugin.moc"
[Desktop Entry]
Type=Service
ServiceTypes=KTextEditor/Plugin
X-KDE-Library=katecolorpickerplugin
Name=Color Picker
Name[x-test]=xxColor Pickerx
Comment=Adds an inline Color preview/picker to colors in the text (e.g. #FFFFFF, white)
Comment[x-test]=xxAdds an inline Color preview/picker to colors in the text (e.g. #FFFFFF, white)xx
/*
SPDX-FileCopyrightText: 2018 Sven Brauch <mail@svenbrauch.de>
SPDX-FileCopyrightText: 2018 Michal Srb <michalsrb@gmail.com>
SPDX-FileCopyrightText: 2020 Jan Paul Batrina <jpmbatrina01@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KATE_COLORPICKER_H
#define KATE_COLORPICKER_H
#include <KTextEditor/ConfigPage>
#include <KTextEditor/InlineNoteProvider>
#include <KTextEditor/MainWindow>
#include <KTextEditor/Plugin>
#include <QHash>
#include <QList>
#include <QRegularExpression>
#include <QVariant>
#include <QVector>
class ColorPickerInlineNoteProvider : public KTextEditor::InlineNoteProvider
{
Q_OBJECT
public:
ColorPickerInlineNoteProvider(KTextEditor::Document *doc);
~ColorPickerInlineNoteProvider();
static void updateColorMatchingCriteria();
// if startLine == -1, update all notes. endLine is inclusive and optional
void updateNotes(int startLine=-1, int endLine=-1);
QVector<int> inlineNotes(int line) const override;
QSize inlineNoteSize(const KTextEditor::InlineNote &note) const override;
void paintInlineNote(const KTextEditor::InlineNote &note, QPainter &painter) const override;
void inlineNoteActivated(const KTextEditor::InlineNote &note, Qt::MouseButtons buttons, const QPoint &globalPos) override;
private:
KTextEditor::Document *m_doc;
int m_startChangedLines;
int m_previousNumLines;
// line, <colorNoteIndex, otherColorIndex>
mutable QHash<int, QHash<int, int>> m_colorNoteIndices;
// config variables shared between all note providers
static QRegularExpression s_colorRegEx;
static bool s_putPreviewAfterColor;
};
class KateColorPickerPlugin : public KTextEditor::Plugin
{
Q_OBJECT
public:
explicit KateColorPickerPlugin(QObject *parent = nullptr, const QList<QVariant> & = QList<QVariant>());
~KateColorPickerPlugin() override;
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
void readConfig();
private:
void addDocument(KTextEditor::Document *doc);
int configPages() const override
{
return 1;
}
KTextEditor::ConfigPage *configPage(int number = 0, QWidget *parent = nullptr) override;
KTextEditor::MainWindow *m_mainWindow;
QHash<KTextEditor::Document*, ColorPickerInlineNoteProvider*> m_inlineColorNoteProviders;
};
#endif // KATE_COLORPICKER_H
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment