From b600149a59366d534c75ffd26bbabafa468833e4 Mon Sep 17 00:00:00 2001 From: uvos Date: Sun, 29 Oct 2023 21:41:19 +0100 Subject: [PATCH] inial commit --- CMakeLists.txt | 37 +++++ katecolorpickerplugin.cpp | 302 +++++++++++++++++++++++++++++++++++++ katecolorpickerplugin.h | 81 ++++++++++ katecolorpickerplugin.json | 75 +++++++++ 4 files changed, 495 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 katecolorpickerplugin.cpp create mode 100644 katecolorpickerplugin.h create mode 100644 katecolorpickerplugin.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..dfa6c76 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +project(kateai VERSION 0.1) + +set(KF5_DEP_VERSION "5.111.0") + +find_package(ECM ${KF5_DEP_VERSION} QUIET REQUIRED NO_MODULE) +list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDECompilerSettings NO_POLICY_SCOPE) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEClangFormat) +include(KDEGitCommitHooks) + +include(ECMOptionalAddSubdirectory) +include(ECMAddAppIcon) +include(ECMInstallIcons) +include(ECMDeprecationSettings) + +find_package(Qt5Widgets CONFIG REQUIRED) +find_package(KF5 + REQUIRED COMPONENTS + CoreAddons + GuiAddons + TextEditor +) + +kcoreaddons_add_plugin(${PROJECT_NAME} INSTALL_NAMESPACE "kf5/ktexteditor") +target_link_libraries(${PROJECT_NAME} PRIVATE KF5::TextEditor) +target_sources( + ${PROJECT_NAME} + PRIVATE + katecolorpickerplugin.cpp +) + + diff --git a/katecolorpickerplugin.cpp b/katecolorpickerplugin.cpp new file mode 100644 index 0000000..96d52c3 --- /dev/null +++ b/katecolorpickerplugin.cpp @@ -0,0 +1,302 @@ +/* + SPDX-FileCopyrightText: 2018 Sven Brauch + SPDX-FileCopyrightText: 2018 Michal Srb + SPDX-FileCopyrightText: 2020 Jan Paul Batrina + SPDX-FileCopyrightText: 2021 Dominik Haumann + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "katecolorpickerplugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +ColorPickerInlineNoteProvider::ColorPickerInlineNoteProvider(KTextEditor::Document *doc) + : m_doc(doc) +{ + // initialize the color regex + m_colorRegex.setPatternOptions(QRegularExpression::DontCaptureOption | QRegularExpression::CaseInsensitiveOption); + updateColorMatchingCriteria(); + + const auto views = m_doc->views(); + for (auto view : views) { + qobject_cast(view)->registerInlineNoteProvider(this); + } + + connect(m_doc, &KTextEditor::Document::viewCreated, this, [this](KTextEditor::Document *, KTextEditor::View *view) { + qobject_cast(view)->registerInlineNoteProvider(this); + }); + + auto lineChanged = [this](const int line) { + if (m_startChangedLines == -1 || m_endChangedLines == -1) { + m_startChangedLines = line; + // changed line is directly above/below the previous changed line, so we just update them + } else if (line == m_endChangedLines) { // handled below. Condition added here to avoid fallthrough + } else if (line == m_startChangedLines - 1) { + m_startChangedLines = line; + } else if (line < m_startChangedLines || line > m_endChangedLines) { + // changed line is outside the range of previous changes. Change proably skipped lines + updateNotes(m_startChangedLines, m_endChangedLines); + m_startChangedLines = line; + m_endChangedLines = -1; + } + + m_endChangedLines = line >= m_endChangedLines ? line + 1 : m_endChangedLines; + }; + + // textInserted and textRemoved are emitted per line, then the last line is followed by a textChanged signal + connect(m_doc, &KTextEditor::Document::textInserted, this, [lineChanged](KTextEditor::Document *, const KTextEditor::Cursor &cur, const QString &) { + lineChanged(cur.line()); + }); + connect(m_doc, &KTextEditor::Document::textRemoved, this, [lineChanged](KTextEditor::Document *, const KTextEditor::Range &range, const QString &) { + lineChanged(range.start().line()); + }); + 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 (m_previousNumLines != newNumLines) { + // either whole line(s) were removed or inserted. We update all lines (even those that are now non-existent) below m_startChangedLines + m_endChangedLines = newNumLines > m_previousNumLines ? newNumLines : m_previousNumLines; + } + updateNotes(m_startChangedLines, m_endChangedLines); + } + + m_startChangedLines = -1; + m_endChangedLines = -1; + m_previousNumLines = newNumLines; + }); + + updateNotes(); +} + +ColorPickerInlineNoteProvider::~ColorPickerInlineNoteProvider() +{ + QPointer doc = m_doc; + if (doc) { + const auto views = m_doc->views(); + for (auto view : views) { + qobject_cast(view)->unregisterInlineNoteProvider(this); + } + } +} + +void ColorPickerInlineNoteProvider::updateColorMatchingCriteria() +{ + KConfigGroup config(KSharedConfig::openConfig(), "ColorPicker"); + m_matchHexLengths = config.readEntry("HexLengths", QList{12, 9, 6, 3}).toVector(); + m_putPreviewAfterColor = config.readEntry("PreviewAfterColor", true); + m_matchNamedColors = config.readEntry("NamedColors", false); + + QString colorRegex; + if (m_matchHexLengths.size() > 0) { + colorRegex += QLatin1String("(#[[:xdigit:]]{3,12})"); + } + + if (m_matchNamedColors) { + if (!colorRegex.isEmpty()) { + colorRegex += QLatin1Char('|'); + } + // shortest and longest colors have 3 (e.g. red) and 20 (lightgoldenrodyellow) characters respectively + colorRegex += QLatin1String("((?lines(); + endLine = lastLine > m_previousNumLines ? lastLine : m_previousNumLines; + } + + if (endLine == -1) { + endLine = startLine; + } + + for (int line = startLine; line < endLine; ++line) { + int removed = m_colorNoteIndices.remove(line); + if (removed != 0) { + Q_EMIT inlineNotesChanged(line); + } + } +} + +QVector ColorPickerInlineNoteProvider::inlineNotes(int line) const +{ + if (!m_colorNoteIndices.contains(line)) { + const QString lineText = m_doc->line(line); + auto matchIter = m_colorRegex.globalMatch(lineText); + while (matchIter.hasNext()) { + const auto match = matchIter.next(); + if (!QColor(match.captured()).isValid()) { + continue; + } + + if (lineText.at(match.capturedStart()) == QLatin1Char('#') && !m_matchHexLengths.contains(match.capturedLength() - 1)) { + // matching for this hex color format is disabled + continue; + } + + int start = match.capturedStart(); + int end = start + match.capturedLength(); + if (m_putPreviewAfterColor) { + start = end; + end = match.capturedStart(); + } + + auto &colorIndices = m_colorNoteIndices[line]; + colorIndices.colorNoteIndices.append(start); + colorIndices.otherColorIndices.append(end); + } + } + + return m_colorNoteIndices[line].colorNoteIndices; +} + +QSize ColorPickerInlineNoteProvider::inlineNoteSize(const KTextEditor::InlineNote ¬e) const +{ + return QSize(note.lineHeight() - 1, note.lineHeight() - 1); +} + +void ColorPickerInlineNoteProvider::paintInlineNote(const KTextEditor::InlineNote ¬e, QPainter &painter) const +{ + const auto line = note.position().line(); + auto colorEnd = note.position().column(); + + const QVector &colorNoteIndices = m_colorNoteIndices[line].colorNoteIndices; + // Since the colorNoteIndices are inserted in left-to-right (increasing) order in inlineNotes(), we can use binary search to find the index (or color note + // number) for the line + const int colorNoteNumber = std::lower_bound(colorNoteIndices.cbegin(), colorNoteIndices.cend(), colorEnd) - colorNoteIndices.cbegin(); + auto colorStart = m_colorNoteIndices[line].otherColorIndices[colorNoteNumber]; + + if (colorStart > colorEnd) { + colorEnd = colorStart; + colorStart = note.position().column(); + } + + const auto color = QColor(m_doc->text({line, colorStart, line, colorEnd})); + // ensure that the border color is always visible + QColor penColor = color; + penColor.setAlpha(255); + painter.setPen(penColor.value() < 128 ? penColor.lighter(150) : penColor.darker(150)); + + painter.setBrush(color); + painter.setRenderHint(QPainter::Antialiasing, false); + const QFontMetricsF fm(note.font()); + const int inc = note.underMouse() ? 1 : 0; + const int ascent = fm.ascent(); + const int margin = (note.lineHeight() - ascent) / 2; + painter.drawRect(margin - inc, margin - inc, ascent - 1 + 2 * inc, ascent - 1 + 2 * inc); +} + +void ColorPickerInlineNoteProvider::inlineNoteActivated(const KTextEditor::InlineNote ¬e, Qt::MouseButtons, const QPoint &) +{ + const auto line = note.position().line(); + auto colorEnd = note.position().column(); + + const QVector &colorNoteIndices = m_colorNoteIndices[line].colorNoteIndices; + // Since the colorNoteIndices are inserted in left-to-right (increasing) order in inlineNotes, we can use binary search to find the index (or color note + // number) for the line + const int colorNoteNumber = std::lower_bound(colorNoteIndices.cbegin(), colorNoteIndices.cend(), colorEnd) - colorNoteIndices.cbegin(); + auto colorStart = m_colorNoteIndices[line].otherColorIndices[colorNoteNumber]; + 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 = i18n("Select Color (Hex output)"); + if (!m_doc->isReadWrite()) { + dialogOptions |= QColorDialog::NoButtons; + title = i18n("View Color [Read only]"); + } + const QColor newColor = QColorDialog::getColor(oldColor, const_cast(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(QObject *parent, const QList &) + : KTextEditor::Plugin(parent) +{ +} + +KateColorPickerPlugin::~KateColorPickerPlugin() = default; + +QObject *KateColorPickerPlugin::createView(KTextEditor::MainWindow *mainWindow) +{ + m_mainWindow = mainWindow; + const auto views = m_mainWindow->views(); + for (auto view : 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.find(doc) == m_inlineColorNoteProviders.end()) { + m_inlineColorNoteProviders.emplace(doc, new ColorPickerInlineNoteProvider(doc)); + } + + connect(doc, &KTextEditor::Document::aboutToClose, this, [this, doc]() { + m_inlineColorNoteProviders.erase(doc); + }); +} + +void KateColorPickerPlugin::readConfig() +{ + for (const auto &[doc, colorNoteProvider] : m_inlineColorNoteProviders) { + Q_UNUSED(doc) + colorNoteProvider->updateColorMatchingCriteria(); + colorNoteProvider->updateNotes(); + } +} + +#include "katecolorpickerplugin.moc" +#include "moc_katecolorpickerplugin.cpp" diff --git a/katecolorpickerplugin.h b/katecolorpickerplugin.h new file mode 100644 index 0000000..1462925 --- /dev/null +++ b/katecolorpickerplugin.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2018 Sven Brauch + SPDX-FileCopyrightText: 2018 Michal Srb + SPDX-FileCopyrightText: 2020 Jan Paul Batrina + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +class ColorPickerInlineNoteProvider : public KTextEditor::InlineNoteProvider +{ + Q_OBJECT +public: + explicit ColorPickerInlineNoteProvider(KTextEditor::Document *doc); + ~ColorPickerInlineNoteProvider() override; + + void updateColorMatchingCriteria(); + // if startLine == -1, update all notes. endLine is optional + void updateNotes(int startLine = -1, int endLine = -1); + + QVector inlineNotes(int line) const override; + QSize inlineNoteSize(const KTextEditor::InlineNote ¬e) const override; + void paintInlineNote(const KTextEditor::InlineNote ¬e, QPainter &painter) const override; + void inlineNoteActivated(const KTextEditor::InlineNote ¬e, Qt::MouseButtons buttons, const QPoint &globalPos) override; + +private: + KTextEditor::Document *m_doc; + int m_startChangedLines = -1; + int m_endChangedLines = -1; + int m_previousNumLines = -1; + + struct ColorIndices { + // When m_putPreviewAfterColor is true, otherColorIndices holds the starting color indices while colorNoteIndices holds the end color indices (and vice + // versa) colorNoteIndices[i] corresponds to otherColorIndices[i] + QVector colorNoteIndices; + QVector otherColorIndices; + }; + + // mutable is used here since InlineNoteProvider::inlineNotes() is const only, and we update the notes lazily (only when inlineNotes() is called) + mutable QHash m_colorNoteIndices; + + QRegularExpression m_colorRegex; + QVector m_matchHexLengths; + bool m_putPreviewAfterColor; + bool m_matchNamedColors; +}; + +class KateColorPickerPlugin : public KTextEditor::Plugin +{ + Q_OBJECT +public: + explicit KateColorPickerPlugin(QObject *parent = nullptr, const QList & = QList()); + ~KateColorPickerPlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; + void readConfig(); + +private: + void addDocument(KTextEditor::Document *doc); + + int configPages() const override + { + return 0; + } + + KTextEditor::MainWindow *m_mainWindow; + std::unordered_map> m_inlineColorNoteProviders; +}; diff --git a/katecolorpickerplugin.json b/katecolorpickerplugin.json new file mode 100644 index 0000000..5943697 --- /dev/null +++ b/katecolorpickerplugin.json @@ -0,0 +1,75 @@ +{ + "KPlugin": { + "Description": "Adds an inline Color preview/picker to colors in the text (e.g. #FFFFFF, white)", + "Description[az]": "Mətndəki sətirdaxili rəng önizləməsi əlavə edir/mətndəki rəngləri seçə vasitəsi (məs., #FFFFFF, ağ rəng)", + "Description[bg]": "Добавя възможност за преглед и избор на цвят в текста (напр.. #FFFFFF, бял)", + "Description[ca@valencia]": "Afig una vista prèvia/selector de colors inclòs per als colors del text (p. ex., #FFFFFF, blanc)", + "Description[ca]": "Afegeix una vista prèvia/selector de colors inclòs per als colors del text (p. ex., #FFFFFF, blanc)", + "Description[de]": "Fügt eine Farbvorschau/Farbauswahl für Farben wie #FFFFFF, white am Zeilenende ein", + "Description[el]": "Προσθέτει ένα εμβόλιμο εργαλείο προεπισκόπησης/επιλογής χρώματος στα χρώματα του κειμένου (π.χ. #FFFFFF, λευκό)", + "Description[en_GB]": "Adds an inline Colour preview/picker to colours in the text (e.g. #FFFFFF, white)", + "Description[eo]": "Aldonas enlinian Koloran antaŭrigardon/elektilon al koloroj en la teksto (ekz. #FFFFFF, blanka)", + "Description[es]": "Añade una vista previa/selector de color en línea a los colores del texto (por ejemplo: #FFFFFF, blanco)", + "Description[eu]": "Testuko koloreei lerro barruko Kolore aurreikuspegi/hautatzaile bat gehitzen die (adib. #FFFFFF, zuria)", + "Description[fi]": "Lisää tekstin väreille (esim. #FFFFFF, white) upotetun väriesikatselun/-valitsimen", + "Description[fr]": "Ajoute un aperçu intégré / sélecteur de couleurs pour les couleurs dans le texte (par exemple #FFFFFF, blanc)", + "Description[gl]": "Engade un selector e vista previa de cores in situ no texto (p. ex. #FFFFFF, white).", + "Description[hu]": "Beágyazott színválasztó a szövegben lévő színekhez (például #FFFFFF, fehér)", + "Description[ia]": "Adde un vista preliminar/selectionator de Color a lolores in le texto (p.ex. #FFFFFF, blanco)", + "Description[ie]": "Adjunter un visor e selector de colores in li textu (p.ex. #FFFFFF, white)", + "Description[it]": "Aggiunge nelle righe del testo un'anteprima colore/selettore (ad es. #FFFFFF, bianco)", + "Description[ka]": "ხაზშივე ტექსტის ფერის გადახედვის/ამრჩევის დამატება (მაგ: #FFFFFF, თეთრი)", + "Description[ko]": "문자열 내 인라인 색상 미리 보기/선택기 추가(예: #FFFFFF, 흰색)", + "Description[nl]": "Voegt een inline kleurenvoorbeeld/kiezer toe aan kleuren in de tekst (bijv. #FFFFFF, wit)", + "Description[pl]": "Dodaje wbudowany wybierak barwy dla barw tekstu (np. #FFFFFF, biały)", + "Description[pt]": "Adiciona uma antevisão/selector de cores incorporado para as cores no texto (p.ex. #FFFFFF, white)", + "Description[ru]": "Добавление в текст встроенного средства предварительного просмотра и выбора цвета (например, #FFFFFF, white (белый))", + "Description[sk]": "Pridá vložený náhľad/výber farieb na farby v texte (napr. #FFFFFF,biela)", + "Description[sl]": "Doda besedilu vrstni predogled/izbirnik barv (npr. #FFFFFF, bela)", + "Description[sv]": "Lägger till en färggranskare/färghämtare på plats i texten (t.ex. #FFFFFF, vit)", + "Description[tr]": "Metindeki renklere satır içi renk önizleyici/seçici ekler (örn. #FFFFFF, beyaz)", + "Description[uk]": "Додає вбудовану панель попереднього перегляду і піпетку для кольорів у тексті (наприклад, #FFFFFF, білого)", + "Description[vi]": "Thêm một bộ xem thử / nhặt màu tại chỗ cho các màu trong văn bản (vd. #FFFFFF, trắng)", + "Description[x-test]": "xxAdds an inline Color preview/picker to colors in the text (e.g. #FFFFFF, white)xx", + "Description[zh_CN]": "在文本中添加内联的颜色预览/拾色器 (例如 #FFFFFF, 白色)", + "Name": "Color Picker", + "Name[ar]": "منتق الألوان", + "Name[az]": "Rəng seçici", + "Name[bg]": "Избиране на цвят", + "Name[ca@valencia]": "Selector de color", + "Name[ca]": "Selector de color", + "Name[cs]": "Kapátko", + "Name[de]": "Farbauswahl", + "Name[el]": "Επιλογέας χρωμάτων", + "Name[en_GB]": "Colour Picker", + "Name[eo]": "Kolorelektilo", + "Name[es]": "Selector de color", + "Name[eu]": "Kolore hautatzailea", + "Name[fi]": "Värivalinta", + "Name[fr]": "Sélecteur de couleurs", + "Name[gl]": "Selector de cores", + "Name[hu]": "Színválasztó", + "Name[ia]": "Selectionator de color", + "Name[ie]": "Selector de color", + "Name[it]": "Selettore di colore", + "Name[ka]": "ფერის ამრჩევი", + "Name[ko]": "색상 선택기", + "Name[my]": "အရောင်ရွေးကိရိယာ", + "Name[nl]": "Kleurenkiezer", + "Name[pl]": "Wybierak barwy", + "Name[pt]": "Selector de Cores", + "Name[pt_BR]": "Seletor de cores", + "Name[ru]": "Выбор цвета", + "Name[sk]": "Výber farby", + "Name[sl]": "Izbirnik barv", + "Name[sv]": "Färgväljare", + "Name[tr]": "Renk Seçicisi", + "Name[uk]": "Піпетка", + "Name[vi]": "Trình nhặt màu", + "Name[x-test]": "xxColor Pickerxx", + "Name[zh_CN]": "拾色器", + "ServiceTypes": [ + "KTextEditor/Plugin" + ] + } +}