File indexing completed on 2024-05-05 05:51:20
0001 /* 0002 SPDX-FileCopyrightText: 2018 Sven Brauch <mail@svenbrauch.de> 0003 SPDX-FileCopyrightText: 2018 Michal Srb <michalsrb@gmail.com> 0004 SPDX-FileCopyrightText: 2020 Jan Paul Batrina <jpmbatrina01@gmail.com> 0005 SPDX-FileCopyrightText: 2021 Dominik Haumann <dhaumann@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "katecolorpickerplugin.h" 0011 0012 #include <KConfigGroup> 0013 #include <KLocalizedString> 0014 #include <KPluginFactory> 0015 #include <KSharedConfig> 0016 #include <KTextEditor/Document> 0017 #include <KTextEditor/View> 0018 0019 #include <QColor> 0020 #include <QColorDialog> 0021 #include <QFontMetricsF> 0022 #include <QPainter> 0023 #include <QPointer> 0024 #include <QVariant> 0025 0026 ColorPickerInlineNoteProvider::ColorPickerInlineNoteProvider(KTextEditor::Document *doc) 0027 : m_doc(doc) 0028 { 0029 // initialize the color regex 0030 m_colorRegex.setPatternOptions(QRegularExpression::DontCaptureOption | QRegularExpression::CaseInsensitiveOption); 0031 updateColorMatchingCriteria(); 0032 0033 const auto views = m_doc->views(); 0034 for (auto view : views) { 0035 view->registerInlineNoteProvider(this); 0036 } 0037 0038 connect(m_doc, &KTextEditor::Document::viewCreated, this, [this](KTextEditor::Document *, KTextEditor::View *view) { 0039 view->registerInlineNoteProvider(this); 0040 }); 0041 0042 auto lineChanged = [this](const int line) { 0043 if (m_startChangedLines == -1 || m_endChangedLines == -1) { 0044 m_startChangedLines = line; 0045 // changed line is directly above/below the previous changed line, so we just update them 0046 } else if (line == m_endChangedLines) { // handled below. Condition added here to avoid fallthrough 0047 } else if (line == m_startChangedLines - 1) { 0048 m_startChangedLines = line; 0049 } else if (line < m_startChangedLines || line > m_endChangedLines) { 0050 // changed line is outside the range of previous changes. Change proably skipped lines 0051 updateNotes(m_startChangedLines, m_endChangedLines); 0052 m_startChangedLines = line; 0053 m_endChangedLines = -1; 0054 } 0055 0056 m_endChangedLines = line >= m_endChangedLines ? line + 1 : m_endChangedLines; 0057 }; 0058 0059 // textInserted and textRemoved are emitted per line, then the last line is followed by a textChanged signal 0060 connect(m_doc, &KTextEditor::Document::textInserted, this, [lineChanged](KTextEditor::Document *, const KTextEditor::Cursor &cur, const QString &) { 0061 lineChanged(cur.line()); 0062 }); 0063 connect(m_doc, &KTextEditor::Document::textRemoved, this, [lineChanged](KTextEditor::Document *, const KTextEditor::Range &range, const QString &) { 0064 lineChanged(range.start().line()); 0065 }); 0066 connect(m_doc, &KTextEditor::Document::textChanged, this, [this](KTextEditor::Document *) { 0067 int newNumLines = m_doc->lines(); 0068 if (m_startChangedLines == -1) { 0069 // textChanged not preceded by textInserted or textRemoved. This probably means that either: 0070 // *empty line(s) were inserted/removed (TODO: Update only the lines directly below the removed/inserted empty line(s)) 0071 // *the document is newly opened so we update all lines 0072 updateNotes(); 0073 } else { 0074 if (m_previousNumLines != newNumLines) { 0075 // either whole line(s) were removed or inserted. We update all lines (even those that are now non-existent) below m_startChangedLines 0076 m_endChangedLines = newNumLines > m_previousNumLines ? newNumLines : m_previousNumLines; 0077 } 0078 updateNotes(m_startChangedLines, m_endChangedLines); 0079 } 0080 0081 m_startChangedLines = -1; 0082 m_endChangedLines = -1; 0083 m_previousNumLines = newNumLines; 0084 }); 0085 0086 updateNotes(); 0087 } 0088 0089 ColorPickerInlineNoteProvider::~ColorPickerInlineNoteProvider() 0090 { 0091 QPointer<KTextEditor::Document> doc = m_doc; 0092 if (doc) { 0093 const auto views = m_doc->views(); 0094 for (auto view : views) { 0095 view->unregisterInlineNoteProvider(this); 0096 } 0097 } 0098 } 0099 0100 void ColorPickerInlineNoteProvider::updateColorMatchingCriteria() 0101 { 0102 KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ColorPicker")); 0103 m_matchHexLengths = config.readEntry("HexLengths", QList<int>{12, 9, 6, 3}).toVector(); 0104 m_putPreviewAfterColor = config.readEntry("PreviewAfterColor", true); 0105 m_matchNamedColors = config.readEntry("NamedColors", false); 0106 0107 QString colorRegex; 0108 if (m_matchHexLengths.size() > 0) { 0109 colorRegex += QLatin1String("(#[[:xdigit:]]{3,12})"); 0110 } 0111 0112 if (m_matchNamedColors) { 0113 if (!colorRegex.isEmpty()) { 0114 colorRegex += QLatin1Char('|'); 0115 } 0116 // shortest and longest colors have 3 (e.g. red) and 20 (lightgoldenrodyellow) characters respectively 0117 colorRegex += QLatin1String("((?<![\\w])[a-z]{3,20})"); 0118 } 0119 0120 if (!colorRegex.isEmpty()) { 0121 colorRegex = QStringLiteral("(?<![-])(%1)(?![-\\w])").arg(colorRegex); 0122 } else { 0123 // No matching criteria enabled. Set regex to negative lookahead to match nothing. 0124 colorRegex = QLatin1String("(?!)"); 0125 } 0126 0127 m_colorRegex.setPattern(colorRegex); 0128 } 0129 0130 void ColorPickerInlineNoteProvider::updateNotes(int startLine, int endLine) 0131 { 0132 if (m_colorNoteIndices.isEmpty()) { 0133 return; 0134 } 0135 0136 startLine = startLine < -1 ? -1 : startLine; 0137 if (startLine == -1) { 0138 startLine = 0; 0139 // we use whichever of newNumLines and m_previousNumLines are longer so that note indices for non-existent lines are also removed 0140 const int lastLine = m_doc->lines(); 0141 endLine = lastLine > m_previousNumLines ? lastLine : m_previousNumLines; 0142 } 0143 0144 if (endLine == -1) { 0145 endLine = startLine; 0146 } 0147 0148 for (int line = startLine; line < endLine; ++line) { 0149 int removed = m_colorNoteIndices.remove(line); 0150 if (removed != 0) { 0151 Q_EMIT inlineNotesChanged(line); 0152 } 0153 } 0154 } 0155 0156 QList<int> ColorPickerInlineNoteProvider::inlineNotes(int line) const 0157 { 0158 if (!m_colorNoteIndices.contains(line)) { 0159 const QString lineText = m_doc->line(line); 0160 auto matchIter = m_colorRegex.globalMatch(lineText); 0161 while (matchIter.hasNext()) { 0162 const auto match = matchIter.next(); 0163 if (!QColor(match.captured()).isValid()) { 0164 continue; 0165 } 0166 0167 if (lineText.at(match.capturedStart()) == QLatin1Char('#') && !m_matchHexLengths.contains(match.capturedLength() - 1)) { 0168 // matching for this hex color format is disabled 0169 continue; 0170 } 0171 0172 int start = match.capturedStart(); 0173 int end = start + match.capturedLength(); 0174 if (m_putPreviewAfterColor) { 0175 start = end; 0176 end = match.capturedStart(); 0177 } 0178 0179 auto &colorIndices = m_colorNoteIndices[line]; 0180 colorIndices.colorNoteIndices.append(start); 0181 colorIndices.otherColorIndices.append(end); 0182 } 0183 } 0184 0185 return m_colorNoteIndices[line].colorNoteIndices; 0186 } 0187 0188 QSize ColorPickerInlineNoteProvider::inlineNoteSize(const KTextEditor::InlineNote ¬e) const 0189 { 0190 return QSize(note.lineHeight() - 1, note.lineHeight() - 1); 0191 } 0192 0193 void ColorPickerInlineNoteProvider::paintInlineNote(const KTextEditor::InlineNote ¬e, QPainter &painter, Qt::LayoutDirection) const 0194 { 0195 const auto line = note.position().line(); 0196 auto colorEnd = note.position().column(); 0197 0198 const QList<int> &colorNoteIndices = m_colorNoteIndices[line].colorNoteIndices; 0199 // 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 0200 // number) for the line 0201 const int colorNoteNumber = std::lower_bound(colorNoteIndices.cbegin(), colorNoteIndices.cend(), colorEnd) - colorNoteIndices.cbegin(); 0202 auto colorStart = m_colorNoteIndices[line].otherColorIndices[colorNoteNumber]; 0203 0204 if (colorStart > colorEnd) { 0205 colorEnd = colorStart; 0206 colorStart = note.position().column(); 0207 } 0208 0209 const auto color = QColor(m_doc->text({line, colorStart, line, colorEnd})); 0210 // ensure that the border color is always visible 0211 QColor penColor = color; 0212 penColor.setAlpha(255); 0213 painter.setPen(penColor.value() < 128 ? penColor.lighter(150) : penColor.darker(150)); 0214 0215 painter.setBrush(color); 0216 painter.setRenderHint(QPainter::Antialiasing, false); 0217 const QFontMetricsF fm(note.font()); 0218 const int inc = note.underMouse() ? 1 : 0; 0219 const int ascent = fm.ascent(); 0220 const int margin = (note.lineHeight() - ascent) / 2; 0221 painter.drawRect(margin - inc, margin - inc, ascent - 1 + 2 * inc, ascent - 1 + 2 * inc); 0222 } 0223 0224 void ColorPickerInlineNoteProvider::inlineNoteActivated(const KTextEditor::InlineNote ¬e, Qt::MouseButtons, const QPoint &) 0225 { 0226 const auto line = note.position().line(); 0227 auto colorEnd = note.position().column(); 0228 0229 const QList<int> &colorNoteIndices = m_colorNoteIndices[line].colorNoteIndices; 0230 // 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 0231 // number) for the line 0232 const int colorNoteNumber = std::lower_bound(colorNoteIndices.cbegin(), colorNoteIndices.cend(), colorEnd) - colorNoteIndices.cbegin(); 0233 auto colorStart = m_colorNoteIndices[line].otherColorIndices[colorNoteNumber]; 0234 if (colorStart > colorEnd) { 0235 colorEnd = colorStart; 0236 colorStart = note.position().column(); 0237 } 0238 0239 const auto oldColor = QColor(m_doc->text({line, colorStart, line, colorEnd})); 0240 QColorDialog::ColorDialogOptions dialogOptions = QColorDialog::ShowAlphaChannel; 0241 QString title = i18n("Select Color (Hex output)"); 0242 if (!m_doc->isReadWrite()) { 0243 dialogOptions |= QColorDialog::NoButtons; 0244 title = i18n("View Color [Read only]"); 0245 } 0246 const QColor newColor = QColorDialog::getColor(oldColor, const_cast<KTextEditor::View *>(note.view()), title, dialogOptions); 0247 if (!newColor.isValid()) { 0248 return; 0249 } 0250 0251 // include alpha channel if the new color has transparency or the old color included transparency (#AARRGGBB, 9 hex digits) 0252 auto colorNameFormat = (newColor.alpha() != 255 || colorEnd - colorStart == 9) ? QColor::HexArgb : QColor::HexRgb; 0253 m_doc->replaceText({line, colorStart, line, colorEnd}, newColor.name(colorNameFormat)); 0254 } 0255 0256 K_PLUGIN_FACTORY_WITH_JSON(KateColorPickerPluginFactory, "katecolorpickerplugin.json", registerPlugin<KateColorPickerPlugin>();) 0257 0258 KateColorPickerPlugin::KateColorPickerPlugin(QObject *parent, const QVariantList &) 0259 : KTextEditor::Plugin(parent) 0260 { 0261 } 0262 0263 KateColorPickerPlugin::~KateColorPickerPlugin() = default; 0264 0265 QObject *KateColorPickerPlugin::createView(KTextEditor::MainWindow *mainWindow) 0266 { 0267 m_mainWindow = mainWindow; 0268 const auto views = m_mainWindow->views(); 0269 for (auto view : views) { 0270 addDocument(view->document()); 0271 } 0272 0273 connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, [this](KTextEditor::View *view) { 0274 addDocument(view->document()); 0275 }); 0276 0277 return nullptr; 0278 } 0279 0280 void KateColorPickerPlugin::addDocument(KTextEditor::Document *doc) 0281 { 0282 if (m_inlineColorNoteProviders.find(doc) == m_inlineColorNoteProviders.end()) { 0283 m_inlineColorNoteProviders.emplace(doc, new ColorPickerInlineNoteProvider(doc)); 0284 } 0285 0286 connect(doc, &KTextEditor::Document::aboutToClose, this, [this, doc]() { 0287 m_inlineColorNoteProviders.erase(doc); 0288 }); 0289 } 0290 0291 void KateColorPickerPlugin::readConfig() 0292 { 0293 for (const auto &[doc, colorNoteProvider] : m_inlineColorNoteProviders) { 0294 Q_UNUSED(doc) 0295 colorNoteProvider->updateColorMatchingCriteria(); 0296 colorNoteProvider->updateNotes(); 0297 } 0298 } 0299 0300 #include "katecolorpickerplugin.moc" 0301 #include "moc_katecolorpickerplugin.cpp"