/* 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"