From 5995081314995c6aebaa58bb239958c2ea242257 Mon Sep 17 00:00:00 2001 From: Joel Blanco Berg Date: Fri, 13 Dec 2024 19:35:58 +0100 Subject: [PATCH] More code editor search features (go to line, whole word...) (#7919) --- src/surge-xt/gui/overlays/LuaEditors.cpp | 249 ++++++++++++++++++++--- src/surge-xt/gui/overlays/LuaEditors.h | 40 +++- 2 files changed, 250 insertions(+), 39 deletions(-) diff --git a/src/surge-xt/gui/overlays/LuaEditors.cpp b/src/surge-xt/gui/overlays/LuaEditors.cpp index 684d054971a..f5aa60eabc2 100644 --- a/src/surge-xt/gui/overlays/LuaEditors.cpp +++ b/src/surge-xt/gui/overlays/LuaEditors.cpp @@ -85,6 +85,7 @@ void TextfieldButton::paint(juce::Graphics &g) void TextfieldButton::setEnabled(bool v) { enabled = v; + updateGraphicState(); repaint(); } @@ -147,7 +148,7 @@ void Textfield::paint(juce::Graphics &g) if (isEmpty()) { // draw prefix text - g.setColour(colour.withAlpha(0.85f)); + g.setColour(colour.withAlpha(0.5f)); g.setFont(getFont()); auto bounds = getBounds(); @@ -161,6 +162,8 @@ void Textfield::paint(juce::Graphics &g) void Textfield::setHeader(juce::String h) { header = h; } +void Textfield::setHeaderColor(juce::Colour c) { colour = c; } + void Textfield::setColour(int colourID, juce::Colour newColour) { Component::setColour(colourID, newColour); @@ -202,9 +205,11 @@ TextfieldPopup::TextfieldPopup(juce::CodeEditorComponent &editor, Surge::GUI::Sk textfield->setColour(juce::TextEditor::outlineColourId, skin->getColor(Colors::FormulaEditor::Background).brighter(0)); + textfield->setHeaderColor(skin->getColor(Colors::Dialog::Button::Text)); + addAndMakeVisible(*textfield); - textfield->setHeader("Search..."); + // textfield->setHeader("Search..."); textfield->setText(""); // textfield->setAlpha(0.5); textfield->setEscapeAndReturnKeysConsumed(true); @@ -227,12 +232,25 @@ void TextfieldPopup::paint(juce::Graphics &g) auto bounds = getBounds(); auto rect = juce::Rectangle(0, 0, bounds.getWidth(), bounds.getHeight()); - auto c = skin->getColor(Colors::FormulaEditor::Background).darker(1.3); + auto c = currentSkin->getColor(Colors::FormulaEditor::Background).darker(1.3); auto col = juce::Colour(c.getRed(), c.getGreen(), c.getBlue()); g.setFillType(juce::FillType(col)); g.fillRect(rect); + juce::Component::paint(g); } + +void TextfieldPopup::show() +{ + + setVisible(true); + textfield->grabKeyboardFocus(); +} + +void TextfieldPopup::hide() { setVisible(false); } + +void TextfieldPopup::textEditorEscapeKeyPressed(juce::TextEditor &) { hide(); } + void TextfieldPopup::onClick(std::unique_ptr &btn) { std::cout << "click button\n"; @@ -255,16 +273,26 @@ void TextfieldPopup::setTextWidth(int w) { textWidth = w; } void TextfieldPopup::setHeader(juce::String t) { textfield->setHeader(t); } +bool TextfieldPopup::keyPressed(const juce::KeyPress &key, juce::Component *originatingComponent) +{ + return originatingComponent->keyPressed(key); + // return juce::KeyListener::keyPressed(key, originatingComponent); +} + void TextfieldPopup::resize() { - int totalWidth = STYLE_MARGIN + textWidth + STYLE_MARGIN + - STYLE_MARGIN_BETWEEN_TEXT_AND_BUTTONS + STYLE_MARGIN + - (STYLE_BUTTON_SIZE + STYLE_BUTTON_MARGIN) * buttonCount + STYLE_MARGIN; + int marginBetweenTextAndButtons = buttonCount > 0 ? STYLE_MARGIN_BETWEEN_TEXT_AND_BUTTONS : 0; + + int totalWidth = STYLE_MARGIN + textWidth + marginBetweenTextAndButtons + STYLE_MARGIN + + (STYLE_BUTTON_SIZE + STYLE_BUTTON_MARGIN) * buttonCount; + + totalWidth += buttonCount > 0 ? STYLE_MARGIN * 2 : 0; + int totalHeight = STYLE_TEXT_HEIGHT + STYLE_MARGIN * 2; - juce::Rectangle bounds = - juce::Rectangle(ed->getWidth() - totalWidth + 2, 2, totalWidth, totalHeight); + juce::Rectangle bounds = juce::Rectangle( + ed->getWidth() - totalWidth - ed->getScrollbarThickness() + 2, 2, totalWidth, totalHeight); setBounds(bounds); @@ -281,14 +309,82 @@ void TextfieldPopup::resize() for (int i = 0; i < buttonCount; i++) { - auto bounds = juce::Rectangle(STYLE_MARGIN + textWidth + STYLE_MARGIN + - STYLE_MARGIN_BETWEEN_TEXT_AND_BUTTONS + STYLE_MARGIN + - (STYLE_BUTTON_SIZE + STYLE_BUTTON_MARGIN) * i, - buttonY, STYLE_BUTTON_SIZE, STYLE_BUTTON_SIZE); + auto bounds = + juce::Rectangle(STYLE_MARGIN + textWidth + STYLE_MARGIN + marginBetweenTextAndButtons + + STYLE_MARGIN + (STYLE_BUTTON_SIZE + STYLE_BUTTON_MARGIN) * i, + buttonY, STYLE_BUTTON_SIZE, STYLE_BUTTON_SIZE); button[i]->setBounds(bounds); } } +/* +--------------------------------------- +Goto line +--------------------------------------- +*/ + +GotoLine::GotoLine(juce::CodeEditorComponent &editor, Surge::GUI::Skin::ptr_t skin) + : TextfieldPopup(editor, skin) +{ + setHeader("Goto line"); + setTextWidth(80); +} + +bool GotoLine::keyPressed(const juce::KeyPress &key, juce::Component *originatingComponent) +{ + originatingComponent->keyPressed(key); + if (key.getKeyCode() == key.returnKey) + { + hide(); + ed->grabKeyboardFocus(); + } + else if (key.getKeyCode() == key.escapeKey) + { + hide(); + ed->moveCaretTo(startCaretPosition, false); + ed->scrollToLine(startScroll); + ed->grabKeyboardFocus(); + } + + else + { + + int line = std::max(0, textfield->getText().getIntValue() - 1); + line = std::min(ed->getDocument().getNumLines(), line); + + int numLines = ed->getNumLinesOnScreen(); + currentLine = line; + + ed->scrollToLine(std::max(0, line - int(numLines * 0.5))); + + auto textLineLength = ed->getDocument().getLine(line).length(); + auto pos = juce::CodeDocument::Position(ed->getDocument(), line, textLineLength); + ed->moveCaretTo(pos, false); + } + + ed->repaint(); + + return true; +} + +void GotoLine::show() +{ + currentLine = ed->getCaretPos().getLineNumber(); + textfield->setText(""); + TextfieldPopup::show(); + startCaretPosition = ed->getCaretPos(); + startScroll = ed->getFirstLineOnScreen(); +} + +void GotoLine::hide() +{ + TextfieldPopup::hide(); + ed->repaint(); +} + +void GotoLine::focusLost(FocusChangeType) { hide(); } +int GotoLine::getCurrentLine() { return currentLine; } + /* --------------------------------------- Search box @@ -317,7 +413,7 @@ CodeEditorSearch::CodeEditorSearch(juce::CodeEditorComponent &editor, Surge::GUI createButton({R"( )"}); - button[1]->setEnabled(false); + // button[1]->setEnabled(false); setHeader("Search.."); COLOR_MATCH = skin->getColor(Colors::FormulaEditor::Lua::Keyword); @@ -330,22 +426,22 @@ void CodeEditorSearch::onClick(std::unique_ptr &btn) // case sensitive if (btn == button[0]) { - search(); + search(true); } // whole word if (btn == button[1]) { - search(); + search(true); } if (btn == button[2]) { - showResult(1, true); + showResult(-1, true); } if (btn == button[3]) { - showResult(-1, true); + showResult(1, true); } } @@ -414,6 +510,7 @@ void CodeEditorSearch::removeHighlightColors() void CodeEditorSearch::show() { + TextfieldPopup::show(); // set selected text as search query unless it includes a newline character auto sel = ed->getHighlightedRegion(); juce::String txt = ed->getTextInRange(sel); @@ -428,16 +525,17 @@ void CodeEditorSearch::show() textfield->moveCaretToStartOfLine(false); textfield->moveCaretToEndOfLine(true); - setVisible(true); - search(); + // setVisible(true); + search(true); textfield->grabKeyboardFocus(); ed->repaint(); // force update selection color } void CodeEditorSearch::hide() { + TextfieldPopup::hide(); removeHighlightColors(); - setVisible(false); + // setVisible(false); ed->repaint(); } @@ -474,7 +572,7 @@ bool CodeEditorSearch::keyPressed(const juce::KeyPress &key, juce::Component *or return true; } -void CodeEditorSearch::textEditorTextChanged(juce::TextEditor &textEditor) { search(); } +void CodeEditorSearch::textEditorTextChanged(juce::TextEditor &textEditor) { search(true); } void CodeEditorSearch::showResult(int increment, bool moveCaret) { @@ -504,18 +602,23 @@ void CodeEditorSearch::showResult(int increment, bool moveCaret) resultCurrent = (resultCurrent + increment + resultTotal) % resultTotal; saveCaretStartPositionLock = true; - ed->setHighlightedRegion( - juce::Range(result[resultCurrent], result[resultCurrent] + textfield->getTotalNumChars())); + if (moveCaret) + { + ed->setHighlightedRegion(juce::Range( + result[resultCurrent], result[resultCurrent] + textfield->getTotalNumChars())); + } + saveCaretStartPositionLock = false; // std::cout << "show result " << resultTotal << "\n"; } -void CodeEditorSearch::search() +void CodeEditorSearch::search(bool moveCaret) { // move to start pos saveCaretStartPositionLock = true; - ed->moveCaretTo(startCaretPosition, false); + if (moveCaret) + ed->moveCaretTo(startCaretPosition, false); saveCaretStartPositionLock = false; auto caret = ed->getCaretPos(); @@ -525,32 +628,54 @@ void CodeEditorSearch::search() juce::String txt = ed->getDocument().getAllContent(); int pos = 0; int count = 0; + + // case sensitivity int res = !button[0]->isSelected() ? txt.indexOfIgnoreCase(pos, textfield->getText()) : txt.indexOf(pos, textfield->getText()); resultCurrent = 0; bool firstFound = false; + bool skip = false; while (res != -1 && count < 512) { - // whole word + // whole word search if (button[1]->isSelected()) { + auto posBefore = (std::max(0, res - 1)); + auto posAfter = std::min(ed->getDocument().getNumCharacters() - 1, + res + textfield->getTotalNumChars()); + + auto strBefore = posBefore == 0 ? 0 : txt[posBefore]; + auto strAfter = txt[posAfter]; + + if (!((strBefore >= 65 && strBefore <= 90) || !(strBefore >= 97 && strBefore <= 122)) || + !((strAfter >= 65 && strAfter <= 90) || !(strAfter >= 97 && strAfter <= 122))) + skip = true; + + // std::cout << "before: " << txt[posBefore] << " after: " << txt[posAfter] + //<< " skip:" << skip << "\n"; } - result[count] = res; - if (caretPos <= res && !firstFound) + + if (caretPos <= res && !firstFound && !skip) { resultCurrent = count; firstFound = true; } + if (!skip) + { + result[count] = res; + count++; + } + pos = res + 1; res = !button[0]->isSelected() ? txt.indexOfIgnoreCase(pos, textfield->getText()) : txt.indexOf(pos, textfield->getText()); - count++; + skip = false; } resultTotal = count; - showResult(0, true); + showResult(0, moveCaret); } juce::String CodeEditorSearch::getSearchQuery() { return textfield->getText(); } @@ -568,8 +693,23 @@ SurgeCodeEditorComponent::SurgeCodeEditorComponent(juce::CodeDocument &d, juce:: currentSkin = &skin; } -void SurgeCodeEditorComponent::setSearch(CodeEditorSearch &s) { search = &s; } +bool SurgeCodeEditorComponent::keyPressed(const juce::KeyPress &key) +{ + + bool code = CodeEditorComponent::keyPressed(key); + + // update search results + if (search != nullptr) + { + if (search->isVisible()) + search->search(false); + } + return code; +} + +void SurgeCodeEditorComponent::setSearch(CodeEditorSearch &s) { search = &s; } +void SurgeCodeEditorComponent::setGotoLine(GotoLine &s) { gotoLine = &s; } void SurgeCodeEditorComponent::paint(juce::Graphics &g) { // draw background @@ -581,8 +721,25 @@ void SurgeCodeEditorComponent::paint(juce::Graphics &g) g.setFillType(juce::FillType(bgColor)); g.fillRect(bounds); - // Draw search matches + // draw the current line for gotoLine module + if (gotoLine != nullptr && gotoLine->isVisible()) + { + auto currentLine = gotoLine->getCurrentLine(); + auto topLine = getFirstLineOnScreen(); + auto numLines = getNumLinesOnScreen(); + auto lineHeight = getLineHeight(); + + if (currentLine >= topLine && currentLine < topLine + numLines) + { + auto highlightColor = bgColor.interpolatedWith(search->COLOR_MATCH, 0.3); + auto y = (currentLine - topLine) * lineHeight; + g.setFillType(juce::FillType(highlightColor)); + g.fillRect(0, y, getWidth() - getScrollbarThickness(), lineHeight); + } + } + + // Draw search matches if (search != nullptr && search->isVisible() && search->getResultTotal() > 0) { auto result = search->getResult(); @@ -635,6 +792,12 @@ void SurgeCodeEditorComponent::handleEscapeKey() search->hide(); return; } + + if (gotoLine->isVisible()) + { + gotoLine->hide(); + return; + } juce::Component *c = this; while (c) { @@ -763,14 +926,19 @@ CodeEditorContainerWithApply::CodeEditorContainerWithApply(SurgeGUIEditor *ed, S EditorColors::setColorsFromSkin(mainEditor.get(), skin); + // modules + gotoLine = std::make_unique(*mainEditor, skin); search = std::make_unique(*mainEditor, skin); mainEditor->setSearch(*search); + mainEditor->setGotoLine(*gotoLine); + if (addComponents) { addAndMakeVisible(applyButton.get()); addAndMakeVisible(mainEditor.get()); addChildComponent(search.get()); + addChildComponent(gotoLine.get()); } applyButton->setEnabled(false); @@ -889,11 +1057,24 @@ bool CodeEditorContainerWithApply::keyPressed(const juce::KeyPress &key, juce::C // search else if (key.getModifiers().isCommandDown() && keyCode == 70) { + gotoLine->hide(); search->show(); return true; } + // search + else if (key.getModifiers().isCommandDown() && keyCode == 71) + { + + bool isgoto = (gotoLine == nullptr); + + std::cout << "gotoLine " << isgoto << "\n"; + search->hide(); + gotoLine->show(); + return true; + } + // handle string completion else if (key.getTextCharacter() == 34) @@ -1514,6 +1695,7 @@ FormulaModulatorEditor::FormulaModulatorEditor(SurgeGUIEditor *ed, SurgeStorage addAndMakeVisible(*controlArea); addAndMakeVisible(*mainEditor); addChildComponent(*search); + addChildComponent(*gotoLine); addChildComponent(*preludeDisplay); debugPanel = std::make_unique(this); @@ -1612,6 +1794,7 @@ void FormulaModulatorEditor::resized() controlArea->setBounds(0, height - controlHeight, width, controlHeight); search->resize(); + gotoLine->resize(); } void FormulaModulatorEditor::showModulatorCode() @@ -2611,6 +2794,7 @@ WavetableScriptEditor::WavetableScriptEditor(SurgeGUIEditor *ed, SurgeStorage *s addAndMakeVisible(*controlArea); addAndMakeVisible(*mainEditor); addChildComponent(*search); + addChildComponent(*gotoLine); addChildComponent(*preludeDisplay); @@ -2714,6 +2898,7 @@ void WavetableScriptEditor::resized() rendererHeight); search->resize(); + gotoLine->resize(); rerenderFromUIState(); } diff --git a/src/surge-xt/gui/overlays/LuaEditors.h b/src/surge-xt/gui/overlays/LuaEditors.h index 6481c665f5d..17567080c85 100644 --- a/src/surge-xt/gui/overlays/LuaEditors.h +++ b/src/surge-xt/gui/overlays/LuaEditors.h @@ -93,6 +93,7 @@ class Textfield : public juce::TextEditor void paint(juce::Graphics &g) override; void setColour(int colourID, juce::Colour newColour); void setHeader(juce::String h); + void setHeaderColor(juce::Colour c); }; class TextfieldPopup : public juce::Component, @@ -108,12 +109,13 @@ class TextfieldPopup : public juce::Component, static inline int STYLE_MARGIN_BETWEEN_TEXT_AND_BUTTONS = 40; protected: + static TextfieldPopup *lastPopup; juce::CodeEditorComponent *ed; Surge::GUI::Skin::ptr_t currentSkin; std::unique_ptr textfield; std::unique_ptr labelResult; std::unique_ptr button[8]; - // TextfieldButton button[8]; + juce::String header; int buttonCount = 0; @@ -121,13 +123,18 @@ class TextfieldPopup : public juce::Component, int textWidth = 120; public: - void paint(juce::Graphics &g) override; + virtual bool keyPressed(const juce::KeyPress &key, + juce::Component *originatingComponent) override; + virtual void paint(juce::Graphics &g) override; virtual void onClick(std::unique_ptr &btn); TextfieldPopup(juce::CodeEditorComponent &editor, Surge::GUI::Skin::ptr_t); + virtual void textEditorEscapeKeyPressed(juce::TextEditor &) override; void resize(); void setHeader(juce::String); void createButton(juce::String svg); void setTextWidth(int w); + virtual void show(); + virtual void hide(); }; class CodeEditorSearch : public TextfieldPopup @@ -150,15 +157,13 @@ class CodeEditorSearch : public TextfieldPopup juce::CodeDocument::Position startCaretPosition; - virtual void search(); - public: juce::Colour COLOR_MATCH; - + virtual void search(bool moveCaret); virtual juce::String getSearchQuery(); virtual bool isActive(); - virtual void show(); - virtual void hide(); + virtual void show() override; + virtual void hide() override; // virtual void resize(); virtual void onClick(std::unique_ptr &btn) override; @@ -179,21 +184,41 @@ class CodeEditorSearch : public TextfieldPopup virtual int getResultTotal(); }; +class GotoLine : public TextfieldPopup +{ + public: + virtual bool keyPressed(const juce::KeyPress &key, + juce::Component *originatingComponent) override; + GotoLine(juce::CodeEditorComponent &editor, Surge::GUI::Skin::ptr_t); + virtual void show() override; + virtual void hide() override; + void focusLost(FocusChangeType) override; + int currentLine; + int getCurrentLine(); + + private: + int startScroll; + juce::CodeDocument::Position startCaretPosition; +}; + class SurgeCodeEditorComponent : public juce::CodeEditorComponent { public: + bool keyPressed(const juce::KeyPress &key) override; virtual void handleEscapeKey() override; virtual void handleReturnKey() override; virtual void caretPositionMoved() override; virtual void paint(juce::Graphics &) override; virtual void setSearch(CodeEditorSearch &s); + virtual void setGotoLine(GotoLine &s); SurgeCodeEditorComponent(juce::CodeDocument &d, juce::CodeTokeniser *t, Surge::GUI::Skin::ptr_t &skin); private: Surge::GUI::Skin::ptr_t *currentSkin; CodeEditorSearch *search = nullptr; + GotoLine *gotoLine = nullptr; }; /* @@ -216,6 +241,7 @@ class CodeEditorContainerWithApply : public OverlayComponent, std::unique_ptr applyButton; std::unique_ptr tokenizer; std::unique_ptr search; + std::unique_ptr gotoLine; void buttonClicked(juce::Button *button) override; void codeDocumentTextDeleted(int startIndex, int endIndex) override;