diff --git a/Makefile b/Makefile index c935d71..46dee0c 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,24 @@ else detected_OS := $(shell uname) endif -VERSION := $(shell grep VERSION: .github/workflows/workflow.yml | cut -d ':' -f 2 | cut -d ' ' -f 2 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') -BUNDLEID := $(shell grep BUNDLE_ID: .github/workflows/workflow.yml | cut -d ':' -f 2 | sed '1p;d' | cut -d ' ' -f 2 ) +# Extract VERSION - prefer VERSION file, fallback to workflow.yml +ifeq ($(detected_OS),Windows) + # Windows: read from VERSION file using PowerShell (handles newlines properly) + VERSION := $(shell powershell -NoProfile -Command "(Get-Content VERSION -Raw -ErrorAction SilentlyContinue).Trim()" 2>NUL || echo 1.1.0) + # Windows: default BUNDLE_ID (can be overridden via environment variable or Makefile.variables) + BUNDLEID := com.mach1.notepad +else + # Unix/Linux/macOS: use VERSION file if available, otherwise parse workflow.yml + VERSION := $(shell if [ -f VERSION ]; then cat VERSION | tr -d '\r\n '; else grep VERSION: .github/workflows/workflow.yml 2>/dev/null | cut -d ':' -f 2 | cut -d ' ' -f 2 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo 1.1.0; fi) + BUNDLEID := $(shell grep BUNDLE_ID: .github/workflows/workflow.yml 2>/dev/null | head -n 1 | cut -d ':' -f 2 | cut -d ' ' -f 2 || echo com.mach1.notepad) +endif clean: +ifeq ($(detected_OS),Windows) + @powershell -NoProfile -Command "if (Test-Path build) { Remove-Item -Recurse -Force build }" +else rm -rf build +endif setup-codesigning: ifeq ($(detected_OS),Darwin) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 522b536..cb65532 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -13,7 +13,7 @@ NotePadAudioProcessorEditor::NotePadAudioProcessorEditor (NotePadAudioProcessor& p) : AudioProcessorEditor (&p), audioProcessor (p) { - m1TextEditor.reset(new juce::TextEditor("new text editor")); + m1TextEditor.reset(new StrikethroughTextEditor("new text editor")); addAndMakeVisible(m1TextEditor.get()); m1TextEditor->addListener(this); m1TextEditor->setMultiLine(true); @@ -25,6 +25,21 @@ NotePadAudioProcessorEditor::NotePadAudioProcessorEditor (NotePadAudioProcessor& m1TextEditor->setTabKeyUsedAsCharacter(true); m1TextEditor->setTextToShowWhenEmpty("Keep session notes here...", juce::Colours::white); m1TextEditor->setText(audioProcessor.treeState.state.getProperty("SessionText")); // Grabs the string within property labeled "SessionText" + + // Load strikethrough ranges if they exist + auto strikethroughData = audioProcessor.treeState.state.getProperty("StrikethroughRanges").toString(); + if (strikethroughData.isNotEmpty()) + { + m1TextEditor->deserializeStrikethroughRanges(strikethroughData); + } + + // Set up callback to save strikethrough ranges when they change + m1TextEditor->onStrikethroughChanged = [this]() + { + auto strikethroughData = m1TextEditor->serializeStrikethroughRanges(); + audioProcessor.treeState.state.setProperty("StrikethroughRanges", strikethroughData, nullptr); + }; + m1TextEditor->setBounds(0, 0, 800, 512 - 20); m1TextEditor->setColour(juce::TextEditor::backgroundColourId, juce::Colour::fromFloatRGBA(40.0f, 40.0f, 40.0f, 0.10f)); m1TextEditor->setColour(juce::TextEditor::textColourId, juce::Colour::fromFloatRGBA(251.0f, 251.0f, 251.0f, 1.0f)); @@ -37,6 +52,13 @@ NotePadAudioProcessorEditor::NotePadAudioProcessorEditor (NotePadAudioProcessor& todoCheckbox->setToggleState(audioProcessor.isTodoMode(), juce::dontSendNotification); todoCheckbox->setColour(juce::ToggleButton::textColourId, juce::Colours::white); + // Pass through button setup + passThroughButton.reset(new juce::ToggleButton("Audio Pass-Through")); + addAndMakeVisible(passThroughButton.get()); + passThroughButton->addListener(this); + passThroughButton->setToggleState(audioProcessor.isAudioPassThrough(), juce::dontSendNotification); + passThroughButton->setColour(juce::ToggleButton::textColourId, juce::Colours::white); + // Todo input field setup todoInputField.reset(new juce::TextEditor("todo input")); addAndMakeVisible(todoInputField.get()); @@ -64,6 +86,7 @@ NotePadAudioProcessorEditor::~NotePadAudioProcessorEditor() { m1TextEditor = nullptr; todoCheckbox = nullptr; + passThroughButton = nullptr; todoInputField = nullptr; todoItems.clear(); todoLabels.clear(); @@ -99,6 +122,9 @@ void NotePadAudioProcessorEditor::resized() // Position the todo mode controls todoCheckbox->setBounds(0, getHeight() - 50, 100, 24); + // Position the pass-through button next to the todo checkbox + passThroughButton->setBounds(110, getHeight() - 50, 150, 24); + // Position the todo input field and items if in todo mode if (audioProcessor.isTodoMode()) { @@ -130,6 +156,16 @@ void NotePadAudioProcessorEditor::textEditorTextChanged (juce::TextEditor &edito { // On key changes will save editor's string to property labeled/tagged "SessionText" audioProcessor.treeState.state.setProperty("SessionText", editor.getText(), nullptr); + + // Save strikethrough ranges if this is the main text editor + if (&editor == m1TextEditor.get()) + { + // Adjust strikethrough ranges when text changes + m1TextEditor->adjustStrikethroughRanges(); + + auto strikethroughData = m1TextEditor->serializeStrikethroughRanges(); + audioProcessor.treeState.state.setProperty("StrikethroughRanges", strikethroughData, nullptr); + } } void NotePadAudioProcessorEditor::textEditorReturnKeyPressed(juce::TextEditor& editor) @@ -322,6 +358,12 @@ void NotePadAudioProcessorEditor::buttonClicked(juce::Button* button) resized(); // Update layout } + else if (button == passThroughButton.get()) + { + // Toggle audio pass-through mode + bool passThrough = passThroughButton->getToggleState(); + audioProcessor.setAudioPassThrough(passThrough); + } else { // Check if it's one of the todo checkboxes diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index da53943..d3b2a2b 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -11,6 +11,287 @@ #include #include "PluginProcessor.h" +//============================================================================== +/** + * Custom TextEditor with strikethrough support + */ +class StrikethroughTextEditor : public juce::TextEditor +{ +public: + std::function onStrikethroughChanged; + + StrikethroughTextEditor(const juce::String& name = juce::String()) : juce::TextEditor(name) + { + setPopupMenuEnabled(true); // Enable popup menu so right-click works + } + + void addStrikethrough(juce::Range range) + { + if (range.getLength() <= 0) + return; + + // Merge with overlapping ranges + bool merged = false; + for (int i = 0; i < strikethroughRanges.size(); ++i) + { + if (strikethroughRanges[i].intersects(range) || + strikethroughRanges[i].getEnd() == range.getStart() || + strikethroughRanges[i].getStart() == range.getEnd()) + { + strikethroughRanges.set(i, strikethroughRanges[i].getUnionWith(range)); + merged = true; + break; + } + } + + if (!merged) + strikethroughRanges.add(range); + + repaint(); + if (onStrikethroughChanged) + onStrikethroughChanged(); + } + + void removeStrikethrough(juce::Range range) + { + bool changed = false; + for (int i = strikethroughRanges.size() - 1; i >= 0; --i) + { + if (strikethroughRanges[i].intersects(range)) + { + auto existing = strikethroughRanges[i]; + strikethroughRanges.remove(i); + changed = true; + + // Add back the parts that don't overlap + if (existing.getStart() < range.getStart()) + strikethroughRanges.add(juce::Range(existing.getStart(), range.getStart())); + if (existing.getEnd() > range.getEnd()) + strikethroughRanges.add(juce::Range(range.getEnd(), existing.getEnd())); + } + } + if (changed) + { + repaint(); + if (onStrikethroughChanged) + onStrikethroughChanged(); + } + } + + void toggleStrikethrough(juce::Range range) + { + if (range.getLength() <= 0) + return; + + // Check if the entire range is strikethrough + bool allStrikethrough = true; + for (int i = range.getStart(); i < range.getEnd(); ++i) + { + bool hasStrike = false; + for (auto& r : strikethroughRanges) + { + if (r.contains(i)) + { + hasStrike = true; + break; + } + } + if (!hasStrike) + { + allStrikethrough = false; + break; + } + } + + if (allStrikethrough) + removeStrikethrough(range); + else + addStrikethrough(range); + } + + bool hasStrikethrough(int position) const + { + for (auto& range : strikethroughRanges) + { + if (range.contains(position)) + return true; + } + return false; + } + + juce::Array> getStrikethroughRanges() const { return strikethroughRanges; } + + void setStrikethroughRanges(const juce::Array>& ranges) + { + strikethroughRanges = ranges; + repaint(); + } + + // Serialize strikethrough ranges to a string + juce::String serializeStrikethroughRanges() const + { + juce::String result; + for (auto& range : strikethroughRanges) + { + if (result.isNotEmpty()) + result += ";"; + result += juce::String(range.getStart()) + "," + juce::String(range.getEnd()); + } + return result; + } + + // Deserialize strikethrough ranges from a string + void deserializeStrikethroughRanges(const juce::String& data) + { + strikethroughRanges.clear(); + if (data.isEmpty()) + return; + + juce::StringArray ranges; + ranges.addTokens(data, ";", ""); + for (auto& rangeStr : ranges) + { + juce::StringArray parts; + parts.addTokens(rangeStr, ",", ""); + if (parts.size() == 2) + { + int start = parts[0].getIntValue(); + int end = parts[1].getIntValue(); + if (start >= 0 && end > start) + strikethroughRanges.add(juce::Range(start, end)); + } + } + repaint(); + } + + void clearStrikethrough() + { + if (!strikethroughRanges.isEmpty()) + { + strikethroughRanges.clear(); + repaint(); + if (onStrikethroughChanged) + onStrikethroughChanged(); + } + } + + // Adjust strikethrough ranges when text is inserted/deleted + void adjustStrikethroughRanges() + { + // Validate and adjust ranges to ensure they're within bounds + // Note: This doesn't track exact insertion/deletion points, but ensures + // ranges remain valid after text modifications + const int totalChars = getTotalNumChars(); + + for (int i = strikethroughRanges.size() - 1; i >= 0; --i) + { + auto& range = strikethroughRanges.getReference(i); + + // Remove ranges that are completely out of bounds + if (range.getStart() >= totalChars) + { + strikethroughRanges.remove(i); + continue; + } + + // Clamp ranges that extend beyond the text + if (range.getEnd() > totalChars) + { + range = juce::Range(range.getStart(), totalChars); + } + + // Remove empty or invalid ranges + if (range.getLength() <= 0 || range.getStart() < 0) + { + strikethroughRanges.remove(i); + } + } + + // Notify that strikethrough may have changed + if (onStrikethroughChanged) + onStrikethroughChanged(); + + repaint(); + } + + void addPopupMenuItems(juce::PopupMenu& menuToAddTo, + const juce::MouseEvent* mouseClickEvent) override + { + juce::TextEditor::addPopupMenuItems(menuToAddTo, mouseClickEvent); + + auto selRange = getHighlightedRegion(); + bool hasSelection = selRange.getLength() > 0; + + menuToAddTo.addSeparator(); + menuToAddTo.addItem(1000, "Strikethrough", hasSelection && !isReadOnly()); + } + + void performPopupMenuAction(int menuItemID) override + { + if (menuItemID == 1000) + { + auto selRange = getHighlightedRegion(); + if (selRange.getLength() > 0) + toggleStrikethrough(selRange); + } + else + { + juce::TextEditor::performPopupMenuAction(menuItemID); + } + } + +protected: + void paintOverChildren(juce::Graphics& g) override + { + juce::TextEditor::paintOverChildren(g); + + // Draw strikethrough lines over the text content + if (strikethroughRanges.isEmpty()) + return; + + drawStrikethrough(g); + } + +private: + void drawStrikethrough(juce::Graphics& g) + { + if (strikethroughRanges.isEmpty()) + return; + + auto textColour = findColour(textColourId); + g.setColour(textColour); + + // Use TextEditor's getTextBounds to get accurate text positions + for (auto& range : strikethroughRanges) + { + if (range.getStart() >= getTotalNumChars() || range.getLength() <= 0) + continue; + + // Clamp range to valid text range + auto clampedRange = range.getIntersectionWith(juce::Range(0, getTotalNumChars())); + if (clampedRange.isEmpty()) + continue; + + // Get the text bounds for this range (handles multiline and word wrap) + auto textBounds = getTextBounds(clampedRange); + + // Draw strikethrough line for each rectangle in the bounds + for (const auto& rect : textBounds) + { + auto font = getFont(); + float strikethroughY = static_cast(rect.getCentreY()); + float x1 = static_cast(rect.getX()); + float x2 = static_cast(rect.getRight()); + + // Draw the strikethrough line + g.drawLine(x1, strikethroughY, x2, strikethroughY, 1.5f); + } + } + } + + juce::Array> strikethroughRanges; +}; + //============================================================================== /** */ @@ -57,8 +338,9 @@ class NotePadAudioProcessorEditor : public juce::AudioProcessorEditor, void exportTodoList(); void importTodoList(); - std::unique_ptr m1TextEditor; + std::unique_ptr m1TextEditor; std::unique_ptr todoCheckbox; + std::unique_ptr passThroughButton; std::unique_ptr todoInputField; std::unique_ptr priorityCombo; std::unique_ptr searchField; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 5e41ee2..f3bf36b 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -31,6 +31,9 @@ treeState (*this, nullptr /* undomanager */, "TreeState", {std::make_unique& buffer, juce auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); - // In case we have more outputs than inputs, this code clears any output - // channels that didn't contain input data, (because these aren't - // guaranteed to be empty - they may contain garbage). - // This is here to avoid people getting screaming feedback - // when they first compile a plugin, but obviously you don't need to keep - // this code if your algorithm always overwrites all the output channels. - for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) - buffer.clear (i, 0, buffer.getNumSamples()); + bool audioPassThrough = isAudioPassThrough(); + + if (!audioPassThrough) + { + // Mute: clear all output channels + buffer.clear(); + } + // If audioPassThrough is true, the audio already passes through (input is already in output channels) - //TODO: bypass processing + // In case we have more outputs than inputs, clear any output + // channels that didn't contain input data (only needed when pass-through is enabled) + if (audioPassThrough) + { + for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) + { + buffer.clear (i, 0, buffer.getNumSamples()); + } + } } //============================================================================== diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index a85c8df..498742c 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -61,6 +61,10 @@ class NotePadAudioProcessor : public juce::AudioProcessor bool isTodoMode() const { return treeState.state.getProperty("TodoMode", false); } void setTodoMode(bool todoMode) { treeState.state.setProperty("TodoMode", todoMode, nullptr); } + // Pass through mode getter setter + bool isAudioPassThrough() const { return treeState.state.getProperty("PassThrough", true); } + void setAudioPassThrough(bool passThrough) { treeState.state.setProperty("PassThrough", passThrough, nullptr); } + private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NotePadAudioProcessor)