diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b782a4f0f5..d8fa1cfed6 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -252,20 +252,20 @@ jobs: fail-fast: false matrix: include: - - name: Ubuntu-22.04-x64 - os: ubuntu:22.04 - pacman: apt - name: Ubuntu-24.04-x64 os: ubuntu:24.04 pacman: apt + - name: Ubuntu-26.04-x64 + os: ubuntu:26.04 + pacman: apt - name: Debian-x64 os: debian pacman: apt - - name: Fedora-41-x64 - os: fedora:41 + - name: Fedora-43-x64 + os: fedora:43 pacman: dnf - - name: Fedora-42-x64 - os: fedora:42 + - name: Fedora-44-x64 + os: fedora:44 pacman: dnf - name: OpenSUSE-Tumbleweed-x64 os: opensuse/tumbleweed @@ -350,20 +350,20 @@ jobs: fail-fast: false matrix: include: - - name: Ubuntu-22.04-aarch64 - os: ubuntu:22.04 - pacman: apt - name: Ubuntu-24.04-aarch64 os: ubuntu:24.04 pacman: apt + - name: Ubuntu-26.04-aarch64 + os: ubuntu:26.04 + pacman: apt - name: Debian-aarch64 os: debian pacman: apt - - name: Fedora-41-aarch64 - os: fedora:41 + - name: Fedora-43-aarch64 + os: fedora:43 pacman: dnf - - name: Fedora-42-aarch64 - os: fedora:42 + - name: Fedora-44-aarch64 + os: fedora:44 pacman: dnf - name: OpenSUSE-Tumbleweed-aarch64 os: opensuse/tumbleweed diff --git a/Libraries/JUCE b/Libraries/JUCE index c111c46448..fb35862589 160000 --- a/Libraries/JUCE +++ b/Libraries/JUCE @@ -1 +1 @@ -Subproject commit c111c46448175908e49ad04ca21370584ce85aae +Subproject commit fb35862589d4d0a1dc8b58252d67e0d33df6e6ed diff --git a/Libraries/pd-cyclone b/Libraries/pd-cyclone index e35aede30e..bb5d46eb68 160000 --- a/Libraries/pd-cyclone +++ b/Libraries/pd-cyclone @@ -1 +1 @@ -Subproject commit e35aede30e22c6a65a82e25127a2b8ce1bc7da3b +Subproject commit bb5d46eb68abd0c4ed148c1989da79396050ae9a diff --git a/Libraries/pd-else b/Libraries/pd-else index d6ffcffabd..76cfe03a9c 160000 --- a/Libraries/pd-else +++ b/Libraries/pd-else @@ -1 +1 @@ -Subproject commit d6ffcffabd9a7996ecec76bc15a22570891e8f00 +Subproject commit 76cfe03a9c0b202d1317674029ceec85c3b1b4a2 diff --git a/Libraries/pd-lua b/Libraries/pd-lua index ee882d0cc1..955e32342b 160000 --- a/Libraries/pd-lua +++ b/Libraries/pd-lua @@ -1 +1 @@ -Subproject commit ee882d0cc1837536788c33969ef739da05b52c2f +Subproject commit 955e32342b0b0ce6f77efa3acf03c765ef95e332 diff --git a/Libraries/pure-data b/Libraries/pure-data index 477c0ce2cb..bdb30eedee 160000 --- a/Libraries/pure-data +++ b/Libraries/pure-data @@ -1 +1 @@ -Subproject commit 477c0ce2cb5783cae5c72683779b4650f2b29b08 +Subproject commit bdb30eedee9ef0a9eecbeceb66c0203b47ca5a92 diff --git a/README.md b/README.md index e64b74c5e9..0ceffdf64a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ You can also download a recent experimental build from our [website](https://plu git clone --recursive https://github.com/plugdata-team/plugdata.git cd plugdata mkdir build && cd build -cmake .. (the generator can be specified using -G"Unix Makefiles", -G"XCode" or -G"Visual Studio 16 2019" -A x64) +cmake .. (the generator can be specified using -G"Unix Makefiles", -G"Xcode" or -G"Visual Studio 16 2019" -A x64) cmake --build . ``` diff --git a/Resources/Documentation/vanilla/tgl.json b/Resources/Documentation/vanilla/tgl.json index 96d2d29104..249b054e30 100644 --- a/Resources/Documentation/vanilla/tgl.json +++ b/Resources/Documentation/vanilla/tgl.json @@ -41,10 +41,6 @@ "type": "size ", "description": "sets the GUI size" }, - { - "type": "flashtime ", - "description": "set minimum and maximum flash time in ms" - }, { "type": "init ", "description": "non-0 sets to init mode" @@ -83,4 +79,4 @@ } ], "body": "" -} \ No newline at end of file +} diff --git a/Resources/Fonts/IconFont.ttf b/Resources/Fonts/IconFont.ttf index 326124f507..490be3626b 100644 Binary files a/Resources/Fonts/IconFont.ttf and b/Resources/Fonts/IconFont.ttf differ diff --git a/Source/Components/ColourPicker.h b/Source/Components/ColourPicker.h index 275ca81634..bcd1cbfae3 100644 --- a/Source/Components/ColourPicker.h +++ b/Source/Components/ColourPicker.h @@ -530,6 +530,7 @@ class ColourPicker final : public Component void paint(Graphics& g) override { // draw the image + g.setOpacity(1.0f); g.drawImageAt(colourWheelHSV, margin, margin); g.setColour(PlugDataColours::outlineColour); diff --git a/Source/Components/SuggestionComponent.h b/Source/Components/SuggestionComponent.h index 5eec4f3c4a..1d44038e16 100644 --- a/Source/Components/SuggestionComponent.h +++ b/Source/Components/SuggestionComponent.h @@ -29,10 +29,15 @@ struct SuggestionEntry { String displayName; String description; IconType icon = IconType::None; - bool clickable = true; + String displayOverride; String detailLookupName; String completionOverride; + String const& getDisplayText() const + { + return displayOverride.isNotEmpty() ? displayOverride : displayName; + } + String const& getCompletionText() const { return completionOverride.isNotEmpty() ? completionOverride : displayName; @@ -43,8 +48,10 @@ struct SuggestionQueryResult { HeapArray entries; String topAutocompleteText; bool autocompleteSupported : 1 = false; + bool autocompleteRequiresNavigation : 1 = false; bool detailLookupSupported : 1 = false; bool detailPanelFillsPopup : 1 = false; + bool shrinkHeightToFitEntries : 1 = false; String detailLookupTarget; }; @@ -201,7 +208,7 @@ class SuggestionComponent final { current = entry; setButtonText(entry.displayName); - setInterceptsMouseClicks(entry.clickable, false); + setInterceptsMouseClicks(true, false); repaint(); } @@ -235,12 +242,7 @@ class SuggestionComponent final constexpr auto rightIndent = 14; auto textWidth = getWidth() - leftIndent - rightIndent; - // Send/receive rows are stored as e.g. "send foo" so we can reuse - // the displayName as completion text. The visible label drops the prefix. - auto displayed = current.displayName; - if (displayed.startsWith("s ") || displayed.startsWith("send ") - || displayed.startsWith("r ") || displayed.startsWith("receive ")) - displayed = displayed.fromFirstOccurrenceOf(" ", false, false); + auto const& displayed = current.getDisplayText(); if (textWidth > 0) { Fonts::drawStyledText(g, displayed, leftIndent, yIndent, textWidth, @@ -615,7 +617,11 @@ class SuggestionComponent final lastQueriedText = ""; currentResultSupportsAutocomplete = false; + currentResultAutocompleteRequiresNavigation = false; currentResultSupportsDetail = false; + autocompleteActivatedByNavigation = false; + currentResultShouldShrinkHeightToFitEntries = false; + autocompleteNavigationBaseText.clear(); } void updateBounds() @@ -631,10 +637,7 @@ class SuggestionComponent final setTopLeftPosition(objectPos.translated(-getMargin(), 5 - getMargin())); - if (cnv->viewport) { - setVisible(cnv->viewport->getBounds().contains( - cnv->viewport->getLocalArea(currentObject, currentObject->getLocalBounds()))); - } + updatePopupVisibility(); } String getText() const @@ -662,6 +665,8 @@ class SuggestionComponent final } lastQueriedText = currentText; + autocompleteActivatedByNavigation = false; + autocompleteNavigationBaseText = currentText; applyQueryResult(queryActive(currentText)); } @@ -736,7 +741,6 @@ class SuggestionComponent final e.detailLookupName = name; e.description = library->getObjectInfo(name).description; e.icon = iconForName(name); - e.clickable = true; result.entries.add(std::move(e)); if (result.entries.size() >= numRowsAllocated) @@ -771,17 +775,18 @@ class SuggestionComponent final { SuggestionQueryResult result; result.autocompleteSupported = true; + result.autocompleteRequiresNavigation = true; + result.shrinkHeightToFitEntries = true; - auto methods = findNearbyMethods(text); - for (auto const& [objectName, methodName, description] : methods) { + auto methods = findNearbyMessages(text); + for (auto const& [objectName, messageName, description] : methods) { SuggestionEntry e; - e.displayName = methodName; + e.displayName = messageName; e.description = "(" + objectName + ") " + description; e.icon = SuggestionEntry::IconType::None; - e.clickable = false; - auto const nameOnly = methodName.upToFirstOccurrenceOf(" ", false, false); - if (nameOnly != methodName) + auto const nameOnly = messageName.upToFirstOccurrenceOf(" ", false, false); + if (nameOnly != messageName) e.completionOverride = nameOnly; result.entries.add(std::move(e)); @@ -810,6 +815,8 @@ class SuggestionComponent final { SuggestionQueryResult result; result.autocompleteSupported = true; + result.autocompleteRequiresNavigation = true; + result.shrinkHeightToFitEntries = true; auto const prefix = text.upToFirstOccurrenceOf(" ", false, false); auto const searchSymbol = text.fromFirstOccurrenceOf(" ", false, false).upToFirstOccurrenceOf(" ", false, false); @@ -821,9 +828,9 @@ class SuggestionComponent final SuggestionEntry e; auto const symbol = isSend ? sr.receiveSymbol : sr.sendSymbol; e.displayName = prefix + " " + symbol; + e.displayOverride = symbol; e.description = sr.name; e.icon = SuggestionEntry::IconType::None; - e.clickable = false; result.entries.add(std::move(e)); if (result.entries.size() >= numRowsAllocated) @@ -844,8 +851,11 @@ class SuggestionComponent final // Apply a query result to the UI void applyQueryResult(SuggestionQueryResult const& result) { - currentResultSupportsAutocomplete = result.autocompleteSupported; + currentResultAutocompleteRequiresNavigation = result.autocompleteRequiresNavigation; + currentResultSupportsAutocomplete = result.autocompleteSupported + && (!result.autocompleteRequiresNavigation || autocompleteActivatedByNavigation); currentResultSupportsDetail = result.detailLookupSupported; + currentResultShouldShrinkHeightToFitEntries = result.shrinkHeightToFitEntries; // Argument mode: detail panel fills the popup if (result.detailPanelFillsPopup) { @@ -867,8 +877,7 @@ class SuggestionComponent final applyLayoutMode(LayoutMode::DetailOnly); // Only show the popup if we actually found something useful. - bool const editorHasText = openedEditor && openedEditor->getText().isNotEmpty(); - setVisible(editorHasText && detailPanel->hasContent()); + updatePopupVisibility(); currentSelection = -1; return; } @@ -886,18 +895,16 @@ class SuggestionComponent final } bool const editorHasText = openedEditor && openedEditor->getText().isNotEmpty(); - setVisible(editorHasText && numOptions > 0); - if (autoCompleteComponent) { - autoCompleteComponent->setEnabled(result.autocompleteSupported); - if (result.autocompleteSupported && result.topAutocompleteText.isNotEmpty()) { + autoCompleteComponent->setEnabled(currentResultSupportsAutocomplete && editorHasText); + if (currentResultSupportsAutocomplete && editorHasText && result.topAutocompleteText.isNotEmpty()) { autoCompleteComponent->setSuggestion(result.topAutocompleteText); } else { autoCompleteComponent->clear(); } } - if (result.autocompleteSupported && result.topAutocompleteText.isNotEmpty()) { + if (currentResultSupportsAutocomplete && editorHasText && result.topAutocompleteText.isNotEmpty() && numOptions > 0) { currentSelection = 0; rows[0]->setToggleState(true, dontSendNotification); } else { @@ -905,7 +912,36 @@ class SuggestionComponent final } applyLayoutMode(currentResultSupportsDetail ? LayoutMode::ListWithDetail : LayoutMode::ListOnly); + shrinkHeightToFitEntriesIfNeeded(); updateDetailPanel(); + updatePopupVisibility(); + } + + void shrinkHeightToFitEntriesIfNeeded() + { + if (!currentResultShouldShrinkHeightToFitEntries || layoutMode == LayoutMode::DetailOnly || numOptions <= 0) + return; + + auto const margins = getMargin() * 2; + int const numVisibleRows = std::min(numOptions, numRowsAllocated); + int const targetHeight = numVisibleRows * rowHeight + 12 + margins; + if (targetHeight < getHeight()) + setSize(getWidth(), targetHeight); + } + + void updatePopupVisibility() + { + bool const editorHasText = openedEditor && openedEditor->getText().isNotEmpty(); + bool const hasContent = layoutMode == LayoutMode::DetailOnly ? detailPanel->hasContent() : numOptions > 0; + + bool isInViewport = true; + if (currentObject && currentObject->cnv->viewport) { + auto* viewport = currentObject->cnv->viewport.get(); + isInViewport = viewport->getBounds().contains( + viewport->getLocalArea(currentObject, currentObject->getLocalBounds())); + } + + setVisible(editorHasText && hasContent && isInViewport); } // Layout switching @@ -916,7 +952,7 @@ class SuggestionComponent final if (requestedMode == LayoutMode::DetailOnly || requestedMode == LayoutMode::ListOnly) { layoutMode = requestedMode; - setSize(340 + margins, jmax(getHeight(), 200 + margins)); + setSize(340 + margins, 160 + margins); } else { int const targetWidth = wasSmallPanel ? savedListSize.x : getWidth(); int const targetHeight = wasSmallPanel ? savedListSize.y : getHeight(); @@ -975,7 +1011,39 @@ class SuggestionComponent final auto const& fullText = row->getEntry().getCompletionText(); - if (inCompletedState && currentResultSupportsAutocomplete && fullText.isNotEmpty()) { + if (currentResultAutocompleteRequiresNavigation && fullText.isNotEmpty()) { + autocompleteActivatedByNavigation = true; + currentResultSupportsAutocomplete = true; + lastQueriedText = fullText; + openedEditor->setText(fullText, sendNotification); + openedEditor->moveCaretToEnd(); + if (autoCompleteComponent) { + autoCompleteComponent->setEnabled(true); + autoCompleteComponent->clear(); + } + currentObject->updateBounds(); + } else if (!currentResultAutocompleteRequiresNavigation && currentResultSupportsAutocomplete + && autoCompleteComponent && fullText.isNotEmpty()) { + auto const& baseText = autocompleteNavigationBaseText; + if (baseText.isNotEmpty() && fullText.startsWith(baseText)) { + if (openedEditor->getText() != baseText) { + lastQueriedText = baseText; + openedEditor->setText(baseText, sendNotification); + openedEditor->moveCaretToEnd(); + } + + autoCompleteComponent->setEnabled(true); + autoCompleteComponent->setSuggestion(fullText); + } else { + lastQueriedText = fullText; + openedEditor->setText(fullText, sendNotification); + openedEditor->moveCaretToEnd(); + autoCompleteComponent->setEnabled(true); + autoCompleteComponent->clear(); + } + + currentObject->updateBounds(); + } else if (inCompletedState && currentResultSupportsAutocomplete && fullText.isNotEmpty()) { lastQueriedText = fullText; openedEditor->setText(fullText, sendNotification); openedEditor->moveCaretToEnd(); @@ -1247,10 +1315,7 @@ class SuggestionComponent final void mouseUp(MouseEvent const& e) override { - // Persist the popup size when the user releases the resize corner. - // Detail-only mode is transient and uses its own sizing; we don't - // overwrite the user's preferred list-mode size from there. - if (e.eventComponent == &resizer && layoutMode != LayoutMode::DetailOnly) + if (e.eventComponent == &resizer && layoutMode != LayoutMode::DetailOnly && currentResultSupportsDetail) saveCurrentSize(); } @@ -1329,69 +1394,126 @@ class SuggestionComponent final rows[0]->setToggleState(true, dontSendNotification); } - SmallArray> findNearbyMethods(String const& toSearch) const + SmallArray> findNearbyMessages(String const& toSearch) const { - SmallArray, int>> objects; + struct NearbyObject { + Object* object = nullptr; + String objectName; + int distance = 0; + }; + + struct MessageCandidate { + String objectName; + String messageName; + String description; + int distance = 0; + int relevance = 0; + bool directAutocomplete = false; + }; + + SmallArray objects; auto* cnv = currentObject->cnv; + + auto isInsideViewport = [cnv](Object* object) { + if (!cnv->viewport) + return true; + + auto* viewport = cnv->viewport.get(); + return viewport->getBounds().intersects( + viewport->getLocalArea(object, object->getLocalBounds())); + }; + + UnorderedSet connectedObjects; + for (auto* connection : currentObject->getConnections()) { + if (connection && connection->outobj == currentObject && connection->inobj) + connectedObjects.insert(connection->inobj.get()); + } + for (auto* obj : cnv->objects) { int distance = currentObject->getPosition().getDistanceFrom(obj->getPosition()); - if (!obj->getPointer() || obj == currentObject || distance > 300) + if (!obj->getPointer() || obj == currentObject || distance > 200 || !isInsideViewport(obj)) + continue; + + if (!connectedObjects.empty() && !connectedObjects.contains(obj)) continue; auto objectName = obj->getType(); auto alreadyExists = std::ranges::find_if(objects, [objectName](auto const& toCompare) { - return std::get<0>(toCompare) == objectName; + return toCompare.objectName == objectName; }) != objects.end(); if (alreadyExists) continue; - auto const& info = cnv->pd->objectLibrary->getObjectInfo(objectName); - objects.add({ objectName, info.methods, distance }); + objects.add({ obj, objectName, distance }); } objects.sort([](auto const& a, auto const& b) { - return std::get<2>(a) > std::get<2>(b); + return a.distance < b.distance; }); - SmallArray> nearbyMethods; + SmallArray candidates; - for (auto& [objectName, methods, distance] : objects) { - for (auto method : methods) { - if (objectName.contains(toSearch)) { - nearbyMethods.add({ objectName, method.type, method.description }); - } - } - } + auto addCandidate = [&candidates, &toSearch](String const& objectName, + pd::Library::ObjectReferenceTable::ReferenceItem const& item, + String const& descriptionPrefix, int const distance) { + int relevance = -1; + if (objectName.contains(toSearch)) + relevance = 0; + else if (item.type.upToFirstOccurrenceOf(" ", false, false).contains(toSearch)) + relevance = 1; + else if (item.description.contains(toSearch)) + relevance = 2; - for (auto& [objectName, methods, distance] : objects) { - for (auto method : methods) { - if (method.type.contains(toSearch)) { - nearbyMethods.add({ objectName, method.type, method.description }); - } - } - } + if (relevance < 0) + return; - for (auto& [objectName, methods, distance] : objects) { - for (auto method : methods) { - if (method.description.contains(toSearch)) { - nearbyMethods.add({ objectName, method.type, method.description }); + auto const completionText = item.type.upToFirstOccurrenceOf(" ", false, false); + bool const directAutocomplete = toSearch.isNotEmpty() && completionText.startsWith(toSearch); + candidates.add({ objectName, item.type, descriptionPrefix + item.description, distance, relevance, directAutocomplete }); + }; + + for (auto& object : objects) { + auto const& info = cnv->pd->objectLibrary->getObjectInfo(object.objectName); + + for (auto const& method : info.methods) + addCandidate(object.objectName, method, "", object.distance); + + for (int i = 0; i < info.inlets.size(); i++) { + auto const descriptionPrefix = "inlet " + String(i + 1) + ": "; + for (auto const& message : info.inlets[i].messages) { + if(message.type.contains("signal")) continue; + + addCandidate(object.objectName, message, descriptionPrefix, object.distance); } } } - for (int i = nearbyMethods.size() - 1; i >= 0; i--) { - auto& [objectName1, method1, distance1] = nearbyMethods[i]; - for (int j = nearbyMethods.size() - 1; j >= 0; j--) { - auto& [objectName2, method2, distance2] = nearbyMethods[j]; - if (objectName1 == objectName2 && method1 == method2 && i != j) { - nearbyMethods.remove_at(i); + candidates.sort([](auto const& a, auto const& b) { + if (a.distance != b.distance) + return a.distance < b.distance; + if (a.directAutocomplete != b.directAutocomplete) + return a.directAutocomplete; + return a.relevance < b.relevance; + }); + + for (int i = candidates.size() - 1; i >= 0; i--) { + for (int j = candidates.size() - 1; j >= 0; j--) { + if (i != j + && candidates[i].objectName == candidates[j].objectName + && candidates[i].messageName == candidates[j].messageName + && candidates[i].description == candidates[j].description) { + candidates.remove_at(i); break; } } } - return nearbyMethods; + SmallArray> nearbyMessages; + for (auto const& candidate : candidates) + nearbyMessages.add({ candidate.objectName, candidate.messageName, candidate.description }); + + return nearbyMessages; } struct SendReceiveEntry { @@ -1635,7 +1757,7 @@ class SuggestionComponent final std::unique_ptr detailViewport; OwnedArray rows; ResizerLookAndFeel resizerLookAndFeel; - ResizableCornerComponent resizer; + MouseRateReducedComponent resizer; ComponentBoundsConstrainer constrainer; LayoutMode layoutMode = LayoutMode::ListWithDetail; @@ -1644,9 +1766,13 @@ class SuggestionComponent final int numOptions = 0; int currentSelection = -1; bool currentResultSupportsAutocomplete = false; + bool currentResultAutocompleteRequiresNavigation = false; bool currentResultSupportsDetail = false; + bool autocompleteActivatedByNavigation = false; + bool currentResultShouldShrinkHeightToFitEntries = false; String lastQueriedText; + String autocompleteNavigationBaseText; SafePointer openedEditor = nullptr; SafePointer currentObject = nullptr; SmallArray sendReceiveDatabase; diff --git a/Source/Dialogs/ObjectBrowserDialog.h b/Source/Dialogs/ObjectBrowserDialog.h index 351ba579a0..773b72d142 100644 --- a/Source/Dialogs/ObjectBrowserDialog.h +++ b/Source/Dialogs/ObjectBrowserDialog.h @@ -258,6 +258,7 @@ class ObjectsListBox final : public ListBox HeapArray> objects; std::function changeCallback; }; + class ObjectViewerDragArea final : public Component { public: ObjectViewerDragArea(PluginEditor* editor, std::function const& dismissMenu) @@ -402,7 +403,7 @@ class ObjectViewer final : public Component { } { - g.setFont(Fonts::getMonospaceFont().withHeight(18.f)); + g.setFont(Fonts::getSemiBoldFont().withHeight(18.f)); g.setColour(colour); g.drawText(objectName, layout.titleBounds.translated(2, 0), Justification::centredLeft); } @@ -548,12 +549,12 @@ class ObjectViewer final : public Component { for (auto& inlet : objectInfo.inlets) { if (inlet.repeating) unknownInletLayout = true; - inlets.add(inlet.tooltip.contains("(signal)")); + inlets.add(inlet.tooltip.upToFirstOccurrenceOf(":", false, false).contains("signal")); } for (auto& outlet : objectInfo.outlets) { if (outlet.repeating) unknownOutletLayout = true; - outlets.add(outlet.tooltip.contains("(signal)")); + outlets.add(outlet.tooltip.upToFirstOccurrenceOf(":", false, false).contains("signal")); } objectName = name; diff --git a/Source/Dialogs/ObjectReferenceDialog.h b/Source/Dialogs/ObjectReferenceDialog.h index 9505156027..87194da91f 100644 --- a/Source/Dialogs/ObjectReferenceDialog.h +++ b/Source/Dialogs/ObjectReferenceDialog.h @@ -197,7 +197,7 @@ class ObjectReferenceDialog final : public Component { void paint(Graphics& g) override { - g.setFont(Fonts::getMonospaceFont().withHeight(38.0f)); + g.setFont(Fonts::getSemiBoldFont().withHeight(38.0f)); g.setColour(PlugDataColours::panelTextColour); int w = static_cast(Fonts::getStringWidthInt(name, g.getCurrentFont())); @@ -501,7 +501,7 @@ class ObjectReferenceDialog final : public Component { String text = i < row.size() ? row[i] : ""; Font f = columns[i].mono - ? Fonts::getMonospaceFont().withHeight(12.5f) + ? Fonts::getMonospaceFont().withHeight(13.5f) : Fonts::getDefaultFont().withHeight(13.5f); AttributedString s; @@ -655,12 +655,12 @@ class ObjectReferenceDialog final : public Component { for (auto const& il : info.inlets) { if (il.repeating) unknownLayout = true; - inletsSig.add(il.tooltip.contains("(signal)")); + inletsSig.add(il.tooltip.upToFirstOccurrenceOf(":", false, false).contains("signal")); } for (auto const& ol : info.outlets) { if (ol.repeating) unknownLayout = true; - outletsSig.add(ol.tooltip.contains("(signal)")); + outletsSig.add(ol.tooltip.upToFirstOccurrenceOf(":", false, false).contains("signal")); } objectPreview.setData(name, inletsSig, outletsSig, unknownLayout); diff --git a/Source/Dialogs/OnboardingDialog.h b/Source/Dialogs/OnboardingDialog.h index 00fc7d1235..a9c10376e1 100644 --- a/Source/Dialogs/OnboardingDialog.h +++ b/Source/Dialogs/OnboardingDialog.h @@ -1131,9 +1131,11 @@ class OnboardingDialog final : public Component { styleButton(skipButton, "Skip & use defaults"); skipButton.onClick = [this] { close(); }; + addAndMakeVisible(skipButton); styleButton(backButton, "Back"); backButton.onClick = [this] { goTo(currentPage - 1); }; + addAndMakeVisible(backButton); styleButton(nextButton, "Next"); nextButton.onClick = [this] { @@ -1149,6 +1151,7 @@ class OnboardingDialog final : public Component { goTo(currentPage + 1); } }; + addAndMakeVisible(nextButton); goTo(0); } @@ -1200,14 +1203,21 @@ class OnboardingDialog final : public Component { } private: - void styleButton(TextButton& b, String const& text) + + void lookAndFeelChanged() override + { + styleButton(skipButton); + styleButton(backButton); + styleButton(nextButton); + } + + void styleButton(TextButton& b, String const& text = "") { auto const backgroundColour = PlugDataColours::panelBackgroundColour; b.setColour(TextButton::buttonColourId, backgroundColour.contrasting(0.05f)); b.setColour(TextButton::buttonOnColourId, backgroundColour.contrasting(0.10f)); b.setColour(ComboBox::outlineColourId, Colours::transparentBlack); - b.setButtonText(text); - addAndMakeVisible(b); + if(text.isNotEmpty()) b.setButtonText(text); } void goTo(int newPage) diff --git a/Source/Dialogs/TextEditorDialog.h b/Source/Dialogs/TextEditorDialog.h index 3b2ec4f753..0f686364b5 100644 --- a/Source/Dialogs/TextEditorDialog.h +++ b/Source/Dialogs/TextEditorDialog.h @@ -14,6 +14,7 @@ #include #include "Utility/Config.h" +#include "Utility/SettingsFile.h" #include "Constants.h" struct LuaTokeniserFunctions { @@ -1317,6 +1318,7 @@ class PlugDataTextEditor final : public Component { std::pair getCurrentSearchSelection() { return document.getCurrentSearchSelection(); } float getScale() { return viewScaleFactor; } + std::function onScaleChanged = [](float) { }; void setSearchText(String const& searchText); void searchNext(); @@ -2435,6 +2437,7 @@ bool PlugDataTextEditor::scaleView(float const scaleFactor, float const vertical updateViewTransform(); document.setMaximumLineWidth(getWidth(), viewScaleFactor); updateSelections(); + onScaleChanged(viewScaleFactor); return true; } @@ -3077,6 +3080,13 @@ struct TextEditorDialog final : public Component editor.searchNext(); }; + editor.onScaleChanged = [](float const scale) { + SettingsFile::getInstance()->setProperty("text_editor_zoom", scale * 100.0f); + }; + + auto const savedScale = std::clamp(SettingsFile::getInstance()->getProperty("text_editor_zoom") / 100.0f, 0.75f, 1.5f); + editor.scaleView(savedScale, 0.0f, true); + editor.grabKeyboardFocus(); editor.setEnableSyntaxHighlighting(enableSyntaxHighlighting); } diff --git a/Source/Heavy/CompatibleObjects.h b/Source/Heavy/CompatibleObjects.h index 8d1acda5db..4f9be61576 100644 --- a/Source/Heavy/CompatibleObjects.h +++ b/Source/Heavy/CompatibleObjects.h @@ -217,10 +217,47 @@ class HeavyCompatibleObjects { "hv.vline~" }; - static inline StringArray const elseObjects = { - "knob" + static inline StringArray const cycloneObjects = { + "acosh~", + "acos~", + "asinh~", + "asin~", + "atan2~", + "atanh~", + "atan~", + "bitand~", + "bitor~", + "bitnot~", + "bitsafe~", + "bitxor~", + "cartopol~", + "cosh~", + "cosx~", + "equals~", + "==~", + "greaterthaneq~", + ">=~", + "greaterthan~", + ">~", + "lessthaneq~", + "<=~", + "lessthan~", + "<~", + "notequals~", + "!=~", + "poltocar~", + "sinh~", + "sinx~", + "tanh~", + "tanx~", }; + static inline StringArray const otherObjects = { + "pdnam~", + "knob" // from ELSE library + }; + + static inline StringArray const elseAbstractions = { "above", "add", @@ -258,7 +295,8 @@ class HeavyCompatibleObjects { StringArray allObjects; allObjects.addArray(heavyObjects); allObjects.addArray(heavyAbstractions); - allObjects.addArray(elseObjects); + allObjects.addArray(cycloneObjects); + allObjects.addArray(otherObjects); allObjects.addArray(elseAbstractions); allObjects.addArray(pdAbstractions); allObjects.addArray(extra); diff --git a/Source/Heavy/HeavyExportDialog.cpp b/Source/Heavy/HeavyExportDialog.cpp index 148ce45919..f0ac73e6e2 100644 --- a/Source/Heavy/HeavyExportDialog.cpp +++ b/Source/Heavy/HeavyExportDialog.cpp @@ -216,7 +216,7 @@ HeavyExportDialog::HeavyExportDialog(Dialog* dialog) }; }; */ infoButton->onClick = [] { - URL("https://wasted-audio.github.io/hvcc/docs/01.introduction.html#what-is-heavy").launchInDefaultBrowser(); + URL("https://wasted-audio.github.io/hvcc/latest/getting-started/#what-is-heavy").launchInDefaultBrowser(); }; addAndMakeVisible(*infoButton); diff --git a/Source/Object.cpp b/Source/Object.cpp index 3553eaaecc..4eac04ae6d 100644 --- a/Source/Object.cpp +++ b/Source/Object.cpp @@ -1406,7 +1406,7 @@ void Object::openNewObjectEditor() editor->grabKeyboardFocus(); editor->onFocusLost = [this, editor] { - if (cnv->suggestor->hasKeyboardFocus(true) || Component::getCurrentlyFocusedComponent() == editor) { + if (cnv->suggestor->shouldKeepEditorOpen(editor) || Component::getCurrentlyFocusedComponent() == editor) { editor->grabKeyboardFocus(); return; } diff --git a/Source/Objects/ArrayObject.h b/Source/Objects/ArrayObject.h index f65bb8aeab..9b3d6b9c66 100644 --- a/Source/Objects/ArrayObject.h +++ b/Source/Objects/ArrayObject.h @@ -297,11 +297,10 @@ class GraphicalArray final : public Component updateArrayPath(); break; } - case hash("xticks"): { - repaint(); - break; - } - case hash("yticks"): { + case hash("xticks"): + case hash("yticks"): + case hash("xlabel"): + case hash("ylabel"): { repaint(); break; } @@ -1151,8 +1150,6 @@ class ArrayObject final : public ObjectBase { SafePointer propertiesPanel = nullptr; Value sizeProperty = SynchronousValue(); - GraphTicks ticks; - // Array component ArrayObject(pd::WeakReference obj, Object* object) : ObjectBase(obj, object) @@ -1260,7 +1257,7 @@ class ArrayObject final : public ObjectBase { } nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); - ticks.render(nvg, b); + getTicks()->render(nvg, b); } bool isTransparent() override { return true; } @@ -1285,15 +1282,16 @@ class ArrayObject final : public ObjectBase { title += graph->getUnexpandedName() + (graph != graphs.getLast() ? "," : ""); } + ObjectLabel* label; + if (labels.isEmpty()) { + label = labels.add(new ObjectLabel()); + object->cnv->addChildComponent(label); + } else { + label = labels[0]; + } + if (title.isNotEmpty()) { int constexpr fontHeight = 14.0f; - ObjectLabel* label; - if (labels.isEmpty()) { - label = labels.add(new ObjectLabel()); - } else { - label = labels[0]; - } - auto font = Font(FontOptions(fontHeight)); auto bounds = object->getBounds().reduced(Object::margin).removeFromTop(fontHeight + 2).withWidth(Fonts::getStringWidthInt(title, font)); @@ -1303,11 +1301,14 @@ class ArrayObject final : public ObjectBase { label->setBounds(bounds); label->setText(title, dontSendNotification); label->setLabelColour(PlugDataColours::canvasTextColour); + label->setVisible(true); object->cnv->addAndMakeVisible(label); } else { - labels.clear(); + label->setVisible(false); } + + getTicks()->updateLabel(object); } Rectangle getPdBounds() override @@ -1348,8 +1349,9 @@ class ArrayObject final : public ObjectBase { } if (auto glist = ptr.get()) { sizeProperty = VarArray { var(glist->gl_pixwidth), var(glist->gl_pixheight) }; - ticks.update(glist.get()); + getTicks()->update(glist.get()); } + updateLabel(); } void updateSizeProperty() override @@ -1430,6 +1432,10 @@ class ArrayObject final : public ObjectBase { { switch (symbol) { case hash("redraw"): { + if (auto glist = ptr.get()) { + getTicks()->update(glist.get()); + } + updateLabel(); updateGraphs(); if (dialog) { dialog->updateGraphs(); @@ -1437,10 +1443,13 @@ class ArrayObject final : public ObjectBase { break; } case hash("yticks"): - case hash("xticks"): { + case hash("xticks"): + case hash("ylabel"): + case hash("xlabel"): { if (auto glist = ptr.get()) { - ticks.update(glist.get()); + getTicks()->update(glist.get()); } + updateLabel(); repaint(); break; } @@ -1452,6 +1461,24 @@ class ArrayObject final : public ObjectBase { private: OwnedArray graphs; std::unique_ptr dialog = nullptr; + + GraphTicks* getTicks() + { + while (labels.size() < 1) { + auto* label = labels.add(new ObjectLabel()); + object->cnv->addChildComponent(label); + label->setVisible(false); + } + + if (labels.size() < 2) { + auto* ticks = new GraphTicks(); + labels.add(ticks); + object->cnv->addChildComponent(ticks); + return ticks; + } + + return reinterpret_cast(labels[1]); + } }; class ArrayDefineObject final : public TextObjectBase { diff --git a/Source/Objects/GraphOnParent.h b/Source/Objects/GraphOnParent.h index 85c7f18f56..a108f8d367 100644 --- a/Source/Objects/GraphOnParent.h +++ b/Source/Objects/GraphOnParent.h @@ -6,11 +6,14 @@ #pragma once // Also used by garray -class GraphTicks { +class GraphTicks final : public ObjectLabel { int xTicksPerBig = 0, yTicksPerBig = 0; float xTickPoint = 0, yTickPoint = 0; float xTickInc = 0, yTickInc = 0; + float xLabelY = 0, yLabelX = 0; float gl_x1 = 0.f, gl_x2 = 1.f, gl_y1 = 100.f, gl_y2 = 0.f; + Rectangle graphBounds; + StringArray xLabels, yLabels; public: void update(t_glist const* glist) @@ -25,13 +28,85 @@ class GraphTicks { gl_y1 = glist->gl_y1; gl_x2 = glist->gl_x2; gl_y2 = glist->gl_y2; + + xLabelY = glist->gl_xlabely; + yLabelX = glist->gl_ylabelx; + xLabels.clear(); + yLabels.clear(); + + for (int i = 0; i < glist->gl_nxlabels; i++) + xLabels.add(String::fromUTF8(glist->gl_xlabel[i]->s_name)); + + for (int i = 0; i < glist->gl_nylabels; i++) + yLabels.add(String::fromUTF8(glist->gl_ylabel[i]->s_name)); + } + + int getSizeOfTicksAndLabels() const + { + int size = (xTicksPerBig || yTicksPerBig) ? 4 : 0; + + if (xLabels.size() > 0) + size = std::max(size, 14); + + if (yLabels.size() > 0) { + auto const font = Font(FontOptions(10.0f)); + int labelWidth = 0; + for (auto const& label : yLabels) + labelWidth = std::max(labelWidth, Fonts::getStringWidthInt(label, font)); + + size = std::max(size, labelWidth + 4); + } + + return size; + } + + void updateLabel(Object* object) + { + graphBounds = object->getBounds().reduced(Object::margin); + setBounds(graphBounds.expanded(getSizeOfTicksAndLabels())); + setVisible(xLabels.size() > 0 || yLabels.size() > 0); + setLabelColour(PlugDataColours::canvasTextColour); + } + + void renderLabel(NVGcontext* nvg, float const scale) override + { + ignoreUnused(scale); + + if (!isVisible()) + return; + + auto const localGraphBounds = graphBounds.translated(-getX(), -getY()); + auto const y1 = static_cast(localGraphBounds.getY()); + auto const y2 = static_cast(localGraphBounds.getBottom()); + auto const x1 = static_cast(localGraphBounds.getX()); + auto const x2 = static_cast(localGraphBounds.getRight()); + + nvgFontFace(nvg, "Inter-Regular"); + nvgFontSize(nvg, 10.0f); + nvgFillColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); + + for (auto const& text : xLabels) { + auto const xpos = jmap(text.getFloatValue(), gl_x1, gl_x2, x1, x2); + auto const ypos = jmap(xLabelY, gl_y1, gl_y2, y1, y2); + auto const align = xLabelY > 0.5f * (gl_y1 + gl_y2) ? NVG_ALIGN_BOTTOM : NVG_ALIGN_TOP; + nvgTextAlign(nvg, NVG_ALIGN_CENTER | align); + nvgText(nvg, xpos, ypos, text.toRawUTF8(), nullptr); + } + + for (auto const& text : yLabels) { + auto const xpos = jmap(yLabelX, gl_x1, gl_x2, x1, x2); + auto const ypos = jmap(text.getFloatValue(), gl_y1, gl_y2, y1, y2); + auto const align = yLabelX > 0.5f * (gl_x1 + gl_x2) ? NVG_ALIGN_LEFT : NVG_ALIGN_RIGHT; + nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | align); + nvgText(nvg, xpos, ypos, text.toRawUTF8(), nullptr); + } } void render(NVGcontext* nvg, Rectangle const b) const { - if (xTicksPerBig) { - t_float const y1 = b.getY(), y2 = b.getBottom(), x1 = b.getX(), x2 = b.getRight(); + t_float const y1 = b.getY(), y2 = b.getBottom(), x1 = b.getX(), x2 = b.getRight(); + if (xTicksPerBig) { t_float f = xTickPoint; for (int i = 0; f < 0.99f * gl_x2 + 0.01f * gl_x1; i++, f += xTickInc) { auto const xpos = jmap(f, gl_x2, gl_x1, x1, x2); @@ -64,7 +139,6 @@ class GraphTicks { } if (yTicksPerBig) { - t_float const y1 = b.getY(), y2 = b.getBottom(), x1 = b.getX(), x2 = b.getRight(); t_float f = yTickPoint; for (int i = 0; f < 0.99f * gl_y1 + 0.01f * gl_y2; i++, f += yTickInc) { auto const ypos = jmap(f, gl_y2, gl_y1, y1, y2); @@ -117,8 +191,6 @@ class GraphOnParent final : public ObjectBase { bool isLocked : 1 = false; bool isOpenedInSplitView : 1 = false; - GraphTicks ticks; - public: // Graph On Parent GraphOnParent(pd::WeakReference obj, Object* object) @@ -162,20 +234,37 @@ class GraphOnParent final : public ObjectBase { xRange = VarArray { var(glist->gl_x1), var(glist->gl_x2) }; yRange = VarArray { var(glist->gl_y2), var(glist->gl_y1) }; sizeProperty = VarArray { var(glist->gl_pixwidth), var(glist->gl_pixheight) }; - ticks.update(glist.get()); + getTicks()->update(glist.get()); } + getTicks()->updateLabel(object); updateCanvas(); } + GraphTicks* getTicks() + { + if (labels.isEmpty()) { + auto* ticks = new GraphTicks(); + labels.add(ticks); + object->cnv->addChildComponent(ticks); + return ticks; + } + + return reinterpret_cast(labels[0]); + } + void receiveObjectMessage(hash32 const symbol, SmallArray const& atoms) override { switch (symbol) { + case hash("redraw"): case hash("yticks"): - case hash("xticks"): { + case hash("xticks"): + case hash("ylabel"): + case hash("xlabel"): { if (auto glist = ptr.get()) { - ticks.update(glist.get()); + getTicks()->update(glist.get()); } + getTicks()->updateLabel(object); repaint(); break; } @@ -476,7 +565,7 @@ class GraphOnParent final : public ObjectBase { nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); - ticks.render(nvg, b); + getTicks()->render(nvg, b); } std::unique_ptr createConstrainer() override diff --git a/Source/Pd/Instance.cpp b/Source/Pd/Instance.cpp index 43e4986149..8fab4dc13a 100644 --- a/Source/Pd/Instance.cpp +++ b/Source/Pd/Instance.cpp @@ -512,13 +512,15 @@ void Instance::initialisePd(String& pdlua_version) pd_typedmess(reinterpret_cast(ptr), gensym("clear"), 0, nullptr); // remove repeating spaces - text = text.replace("\r ", "\r"); - text = text.replace(";\r", ";"); - text = text.replace("\r;", ";"); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + text = text.replace("\n ", "\n"); + text = text.replace(";\n", ";"); + text = text.replace("\n;", ";"); text = text.replace(" ;", ";"); text = text.replace("; ", ";"); text = text.replace(",", " , "); - text = text.replaceCharacters("\r", " "); + text = text.replaceCharacters("\n", " "); while (text.contains(" ")) { text = text.replace(" ", " "); @@ -598,13 +600,15 @@ void Instance::initialisePd(String& pdlua_version) pd_typedmess(reinterpret_cast(ptr), gensym("clear"), 0, nullptr); // remove repeating spaces - text = text.replace("\r ", "\r"); - text = text.replace(";\r", ";"); - text = text.replace("\r;", ";"); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + text = text.replace("\n ", "\n"); + text = text.replace(";\n", ";"); + text = text.replace("\n;", ";"); text = text.replace(" ;", ";"); text = text.replace("; ", ";"); text = text.replace(",", " , "); - text = text.replaceCharacters("\r", " "); + text = text.replaceCharacters("\n", " "); while (text.contains(" ")) { text = text.replace(" ", " "); diff --git a/Source/Pd/Patch.h b/Source/Pd/Patch.h index 5b838bfaa6..eb8575893b 100644 --- a/Source/Pd/Patch.h +++ b/Source/Pd/Patch.h @@ -29,7 +29,7 @@ class Patch final : public ReferenceCountedObject { // The compare equal operator. bool operator==(Patch const& other) const { - return getRawPointer() == other.getRawPointer(); + return getUncheckedPointer() == other.getUncheckedPointer() && ptr.isDeleted() == other.ptr.isDeleted(); } // Gets the bounds of the patch. diff --git a/Source/Pd/Setup.cpp b/Source/Pd/Setup.cpp index ee3a1d8aea..55cbe053d7 100644 --- a/Source/Pd/Setup.cpp +++ b/Source/Pd/Setup.cpp @@ -870,7 +870,7 @@ void allpass_tilde_setup(); void asin_tilde_setup(); void asinh_tilde_setup(); void atan_tilde_setup(); -void atan2_tilde_setup(); +void cyclone_atan2_tilde_setup(); void atanh_tilde_setup(); void atodb_tilde_setup(); void average_tilde_setup(); @@ -954,6 +954,7 @@ void slide_tilde_setup(); void snapshot_tilde_setup(); void spike_tilde_setup(); void svf_tilde_setup(); +void cyclone_tanh_setup(); void cyclone_tanh_tilde_setup(); void tanx_tilde_setup(); void teeth_tilde_setup(); @@ -970,6 +971,7 @@ void above_tilde_setup(); void add_tilde_setup(); void adsr_tilde_setup(); void asr_tilde_setup(); +void atan2_tilde_setup(); void setup_allpass0x2e2nd_tilde(); void setup_allpass0x2erev_tilde(); void args_setup(); @@ -1200,6 +1202,7 @@ void svfilter_tilde_setup(); void symbol2any_setup(); void smooth_tilde_setup(); void smooth2_tilde_setup(); +void tanh_setup(); void tanh_tilde_setup(); void tabplayer_tilde_setup(); void tabreader_setup(); @@ -2485,7 +2488,7 @@ void Setup::initialiseCyclone() sustain_setup(); switch_setup(); table_setup(); - tanh_setup(); + cyclone_tanh_setup(); thresh_setup(); togedge_setup(); tosymbol_setup(); diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 4392c1de91..7a28af226d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -383,10 +383,12 @@ PluginEditor::PluginEditor(PluginProcessor& p) pd->lnf->setMainComponent(this); #endif - if(!pd->findPatchInPluginMode(editorIndex)) { - if (!settingsFile->getProperty("onboarding_completed") || SystemStats::getEnvironmentVariable("PLUGDATA_ONBOARDING", {}).isNotEmpty()) - Dialogs::showOnboardingDialog(&openedDialog, this); - } + MessageManager::callAsync([_this = SafePointer(this)](){ + if(_this && !_this->pd->findPatchInPluginMode(_this->editorIndex)) { + if (!SettingsFile::getInstance()->getProperty("onboarding_completed") || SystemStats::getEnvironmentVariable("PLUGDATA_ONBOARDING", {}).isNotEmpty()) + Dialogs::showOnboardingDialog(&_this->openedDialog, _this.get()); + } + }); addModifierKeyListener(&pd->keyHandler); startTimerHz(90); @@ -578,8 +580,8 @@ void PluginEditor::showWelcomePanel(bool const shouldShow) addObjectMenuButton.setVisible(!shouldShow); undoButton.setVisible(!shouldShow); redoButton.setVisible(!shouldShow); - leftSidebar->setVisible(!shouldShow); - rightSidebar->setVisible(!shouldShow); + leftSidebar->setVisible(!shouldShow && leftSidebar->hasAnyPanel()); + rightSidebar->setVisible(!shouldShow && rightSidebar->hasAnyPanel()); statusbar->setVisible(!shouldShow); sidebarToggleButton.setVisible(shouldShow); @@ -638,11 +640,14 @@ void PluginEditor::resized() return; } + bool const floatingPanels = usesFloatingPanels(); + #if JUCE_LINUX || JUCE_BSD - nvgSurface.setRoundedBottomCorners(leftSidebar->isHidden(), welcomePanel->isVisible() || rightSidebar->isHidden()); + auto roundedLeft = welcomePanel->isVisible() || (floatingPanels && (leftSidebar->isHidden() || !leftSidebar->hasAnyPanel())); + auto roundedRight = welcomePanel->isVisible() || (floatingPanels && (rightSidebar->isHidden() || !rightSidebar->hasAnyPanel())); + nvgSurface.setRoundedBottomCorners(roundedLeft, roundedRight); #endif - - bool const floatingPanels = usesFloatingPanels(); + bool const touchMode = SettingsFile::getInstance()->isUsingTouchMode(); auto const leftHasSelectors = leftSidebar && leftSidebar->isVisible() && leftSidebar->hasAnyPanel(); auto const rightHasSelectors = rightSidebar && rightSidebar->isVisible() && rightSidebar->hasAnyPanel(); @@ -754,20 +759,6 @@ void PluginEditor::resized() welcomePanelSearchButton.setBounds(sidebarToggleButton.getX() - buttonSize - 2, 0, buttonSize, buttonSize); welcomePanelSearchInput.setBounds(libraryPanelSelector.getRight() + 10, 4, welcomePanelSearchButton.getX() - libraryPanelSelector.getRight() - 20, toolbarHeight - 4); - - for (auto* button : SmallArray { - &mainMenuButton, - &undoButton, - &redoButton, - &addObjectMenuButton, - &welcomePanelSearchButton, - &sidebarToggleButton, - &recentlyOpenedPanelSelector, - &libraryPanelSelector, - &welcomePanelSearchInput }) { - button->toFront(false); - } - repaint(); // Some outlines are dependent on whether or not the sidebars are expanded, or whether or not a patch is opened } @@ -1685,7 +1676,7 @@ bool PluginEditor::perform(InvocationInfo const& info) } case CommandIDs::ToggleRightSidebar: { if (rightSidebar) - rightSidebar->showSidebar(leftSidebar->isHidden()); + rightSidebar->showSidebar(rightSidebar->isHidden()); return true; } case CommandIDs::Search: { diff --git a/Source/Sidebar/CommandInput.h b/Source/Sidebar/CommandInput.h index fe3c735da5..8d89d169eb 100644 --- a/Source/Sidebar/CommandInput.h +++ b/Source/Sidebar/CommandInput.h @@ -820,7 +820,7 @@ class CommandInput final auto const clearBounds = bounds.removeFromRight(30).removeFromBottom(inputHeight); clearButton.setBounds(clearBounds); - commandInput.setBounds(bounds.withTrimmedLeft(consoleTargetLength + 4)); + commandInput.setBounds(bounds.withTrimmedLeft(consoleTargetLength + 4).removeFromBottom(inputHeight)); } void setConsoleTargetName(String const& target) diff --git a/Source/Sidebar/Sidebar.cpp b/Source/Sidebar/Sidebar.cpp index 5c8d381825..5cd3276b1d 100644 --- a/Source/Sidebar/Sidebar.cpp +++ b/Source/Sidebar/Sidebar.cpp @@ -302,7 +302,10 @@ void Sidebar::paint(Graphics& g) if (ProjectInfo::isStandalone && !editor->isActiveWindow()) baseColour = baseColour.brighter(baseColour.getBrightness() / 2.5f); - g.fillAll(baseColour); + g.setColour(baseColour); + g.fillRect(0, 30, getWidth(), getHeight() - 42); + g.fillRoundedRectangle(0.0f, 30.0f, getWidth(), getHeight() - 30.0f, Corners::windowCornerRadius); + g.setColour(PlugDataColours::toolbarOutlineColour); if (side == Side::Right) g.drawLine(0.5f, 30.5f, 0.5f, static_cast(getHeight())); @@ -315,7 +318,7 @@ void Sidebar::paint(Graphics& g) return; g.setColour(PlugDataColours::sidebarBackgroundColour); - g.fillRect(0, 30, getWidth(), getHeight() - 12); + g.fillRect(0, 30, getWidth(), getHeight() - 42); g.fillRoundedRectangle(0.0f, 30.0f, getWidth(), getHeight() - 30.0f, Corners::windowCornerRadius); String panelName = hasCurrentPanel && currentPanel < panelDisplayNames.size() diff --git a/Source/Standalone/PlugDataApp.h b/Source/Standalone/PlugDataApp.h index abe5bc73e1..008ad6d7aa 100644 --- a/Source/Standalone/PlugDataApp.h +++ b/Source/Standalone/PlugDataApp.h @@ -207,35 +207,6 @@ class PlugDataApp final : public JUCEApplication { PlugDataWindow* mainWindow = nullptr; }; -void PlugDataWindow::closeAllPatches() -{ - // Show an ask to save dialog for each patch that is dirty - // Because save dialog uses an asynchronous callback, we can't loop over them (so have to chain them) - if (auto* editor = dynamic_cast(mainComponent->getEditor())) { - auto* processor = ProjectInfo::getStandalonePluginHolder()->processor.get(); - auto const* mainEditor = dynamic_cast(processor->getActiveEditor()); - auto& openedEditors = editor->pd->openedEditors; - - if (editor == mainEditor) { - processor->editorBeingDeleted(editor); - } - - if (openedEditors.size() == 1) { - editor->getTabComponent().closeAllTabs(true, nullptr, [this, editor, &openedEditors] { - editor->nvgSurface.detachContext(); - removeFromDesktop(); - openedEditors.removeObject(editor); - }); - } else { - editor->getTabComponent().closeAllTabs(false, nullptr, [this, editor, &openedEditors] { - editor->nvgSurface.detachContext(); - removeFromDesktop(); - openedEditors.removeObject(editor); - }); - } - } -} - StandalonePluginHolder* StandalonePluginHolder::getInstance() { if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone) { diff --git a/Source/Standalone/PlugDataWindow.h b/Source/Standalone/PlugDataWindow.h index 637c87116f..6886043a4d 100644 --- a/Source/Standalone/PlugDataWindow.h +++ b/Source/Standalone/PlugDataWindow.h @@ -497,12 +497,41 @@ class PlugDataWindow final : public DocumentWindow closeAllPatches(); } - // implemented in PlugDataApp.cpp - void closeAllPatches(); + void closeAllPatches() + { + // Show an ask-to-save dialog for each dirty patch. Because the save + // dialog uses an asynchronous callback, these closes have to be chained. + if (auto* editor = dynamic_cast(mainComponent->getEditor())) { + auto* processor = ProjectInfo::getStandalonePluginHolder()->processor.get(); + auto const* mainEditor = dynamic_cast(processor->getActiveEditor()); + auto& openedEditors = editor->pd->openedEditors; + + if (editor == mainEditor) { + processor->editorBeingDeleted(editor); + } + + if (openedEditors.size() == 1) { + editor->getTabComponent().closeAllTabs(true, nullptr, [this, editor, &openedEditors] { + editor->nvgSurface.detachContext(); + removeFromDesktop(); + openedEditors.removeObject(editor); + }); + } else { + editor->getTabComponent().closeAllTabs(false, nullptr, [this, editor, &openedEditors] { + editor->nvgSurface.detachContext(); + removeFromDesktop(); + openedEditors.removeObject(editor); + }); + } + } + } bool isMaximised() const { -#if JUCE_LINUX +#if JUCE_LINUX || JUCE_BSD + if (hasPendingLinuxMaximisedState) + return pendingLinuxMaximisedState; + if (auto* peer = getPeer()) { return OSUtils::isLinuxWindowMaximised(peer); } else { @@ -523,11 +552,12 @@ class PlugDataWindow final : public DocumentWindow #if JUCE_LINUX || JUCE_BSD if (auto* b = getMaximiseButton()) { if (auto* peer = getPeer()) { - bool shouldBeMaximised = OSUtils::isLinuxWindowMaximised(peer); - b->setToggleState(!shouldBeMaximised, dontSendNotification); + bool shouldBeMaximised = !isMaximised(); + setPendingLinuxMaximisedState(shouldBeMaximised); + b->setToggleState(shouldBeMaximised, dontSendNotification); if (!useNativeTitlebar()) { - OSUtils::maximiseLinuxWindow(peer, !shouldBeMaximised); + OSUtils::maximiseLinuxWindow(peer, shouldBeMaximised); } } else { b->setToggleState(false, dontSendNotification); @@ -619,6 +649,31 @@ class PlugDataWindow final : public DocumentWindow } private: +#if JUCE_LINUX || JUCE_BSD + void setPendingLinuxMaximisedState(bool shouldBeMaximised) + { + hasPendingLinuxMaximisedState = true; + pendingLinuxMaximisedState = shouldBeMaximised; + auto const pendingStateSerial = ++pendingLinuxMaximisedStateSerial; + + Timer::callAfterDelay(250, [_this = SafePointer(this), pendingStateSerial] { + if (!_this) + return; + + if (_this->pendingLinuxMaximisedStateSerial != pendingStateSerial) + return; + + _this->hasPendingLinuxMaximisedState = false; + + if (auto* b = _this->getMaximiseButton()) + b->setToggleState(_this->isMaximised(), dontSendNotification); + + _this->resized(); + _this->repaint(); + }); + } +#endif + class MainContentComponent final : public Component , private ComponentListener , public MenuBarModel { @@ -784,5 +839,13 @@ class PlugDataWindow final : public DocumentWindow public: MainContentComponent* mainComponent = nullptr; +private: +#if JUCE_LINUX || JUCE_BSD + bool hasPendingLinuxMaximisedState = false; + bool pendingLinuxMaximisedState = false; + int pendingLinuxMaximisedStateSerial = 0; +#endif + +public: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PlugDataWindow) }; diff --git a/Source/Statusbar.cpp b/Source/Statusbar.cpp index 6b892821c5..efa5b477a7 100644 --- a/Source/Statusbar.cpp +++ b/Source/Statusbar.cpp @@ -374,13 +374,6 @@ void Statusbar::paint(Graphics& g) g.drawRoundedRectangle(b.toFloat(), Corners::largeCornerRadius, 1.0f); } else { - auto const b = getLocalBounds(); - auto baseColour = PlugDataColours::toolbarBackgroundColour; - if (ProjectInfo::isStandalone && !editor->isActiveWindow()) { - baseColour = baseColour.brighter(baseColour.getBrightness() / 2.5f); - } - g.setColour(baseColour); - g.fillRect(b); g.setColour(PlugDataColours::toolbarOutlineColour); auto outlineLeft = editor->leftSidebar->isHidden() ? editor->leftSidebar->getRight() - 1.0f : 0.0f; auto outlineRight = editor->rightSidebar->isHidden() ? editor->rightSidebar->getX() + 1.0f : getWidth(); diff --git a/Source/Toolbar.cpp b/Source/Toolbar.cpp index f0389528d3..0bebb359ce 100644 --- a/Source/Toolbar.cpp +++ b/Source/Toolbar.cpp @@ -1475,6 +1475,7 @@ class PowerButton final : public Component toggle.setToggleState(pd_getdspstate(), dontSendNotification); toggle.onClick = [this] { toggle.getToggleState() ? pd->startDSP() : pd->releaseDSP(); }; + chevron.setTooltip("DSP options"); chevron.setButtonText(Icons::ThinDown); chevron.onClick = [this] { showCallout(); diff --git a/Source/Utility/NVGGraphicsContext.cpp b/Source/Utility/NVGGraphicsContext.cpp index 6fb8c15ce5..2ee1fcec7c 100644 --- a/Source/Utility/NVGGraphicsContext.cpp +++ b/Source/Utility/NVGGraphicsContext.cpp @@ -374,7 +374,7 @@ Font const& NVGGraphicsContext::getFont() void NVGGraphicsContext::drawGlyphs(Span glyphs, Span const> positions, AffineTransform const& t) { for (auto const [i, glyph] : enumerate(glyphs, size_t { })) { - auto const scale = font.getHeight(); + auto const scale = font.getHeightInPoints(); auto tx = AffineTransform::scale(scale * font.getHorizontalScale(), scale).translated(positions[i]).followedBy(t); nvgSave(nvg); @@ -392,8 +392,7 @@ void NVGGraphicsContext::drawGlyphs(Span glyphs, SpangetOutlineForGlyph(f.getMetricsKind(), glyph, p); + font.getTypefacePtr()->getOutlineForGlyph(glyph, p); setPath(p, AffineTransform()); nvgFill(nvg); diff --git a/Source/Utility/OSUtils.cpp b/Source/Utility/OSUtils.cpp index 714dff2ef4..38b1add22e 100644 --- a/Source/Utility/OSUtils.cpp +++ b/Source/Utility/OSUtils.cpp @@ -45,67 +45,95 @@ namespace fs = ghc::filesystem; bool OSUtils::createJunction(std::string from, std::string to) { + auto const linkPath = std::wstring(juce::String::fromUTF8(from.c_str(), static_cast(from.size())).toWideCharPointer()); + auto const targetPath = std::wstring(juce::String::fromUTF8(to.c_str(), static_cast(to.size())).toWideCharPointer()); + if (linkPath.empty() || targetPath.empty()) + return false; - typedef struct { - DWORD ReparseTag; - DWORD ReparseDataLength; - WORD Reserved; - WORD ReparseTargetLength; - WORD ReparseTargetMaximumLength; - WORD Reserved1; - WCHAR ReparseTarget[1]; - } REPARSE_MOUNTPOINT_DATA_BUFFER, *PREPARSE_MOUNTPOINT_DATA_BUFFER; + auto const requiredLength = GetFullPathNameW(targetPath.c_str(), 0, nullptr, nullptr); + if (requiredLength == 0) + return false; - auto szJunction = (LPCTSTR)from.c_str(); - auto szPath = (LPCTSTR)to.c_str(); + std::wstring absTarget(requiredLength, L'\0'); + auto const length = GetFullPathNameW(targetPath.c_str(), requiredLength, absTarget.data(), nullptr); + if (length == 0 || length >= requiredLength) + return false; - BYTE buf[sizeof(REPARSE_MOUNTPOINT_DATA_BUFFER) + MAX_PATH * sizeof(WCHAR)]; - REPARSE_MOUNTPOINT_DATA_BUFFER& ReparseBuffer = (REPARSE_MOUNTPOINT_DATA_BUFFER&)buf; - char szTarget[MAX_PATH] = "\\??\\"; + absTarget.resize(length); - strcat(szTarget, szPath); - strcat(szTarget, "\\"); + auto substName = std::wstring(); + if (absTarget.compare(0, 8, L"\\\\?\\UNC\\") == 0) { + substName = L"\\??\\UNC\\" + absTarget.substr(8); + } else if (absTarget.compare(0, 4, L"\\\\?\\") == 0) { + substName = L"\\??\\" + absTarget.substr(4); + } else if (absTarget.compare(0, 2, L"\\\\") == 0) { + substName = L"\\??\\UNC\\" + absTarget.substr(2); + } else { + substName = L"\\??\\" + absTarget; + } - if (!::CreateDirectory(szJunction, nullptr)) - return false; + auto const printName = absTarget; - // Obtain SE_RESTORE_NAME privilege (required for opening a directory) - HANDLE hToken = nullptr; - TOKEN_PRIVILEGES tp; - try { - if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) - throw ::GetLastError(); - if (!::LookupPrivilegeValue(nullptr, SE_RESTORE_NAME, &tp.Privileges[0].Luid)) - throw ::GetLastError(); - tp.PrivilegeCount = 1; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - if (!::AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), nullptr, nullptr)) + auto createdDirectory = false; + if (!CreateDirectoryW(linkPath.c_str(), nullptr)) { + if (GetLastError() != ERROR_ALREADY_EXISTS) return false; - } catch (DWORD) { - } // Ignore errors - if (hToken) - ::CloseHandle(hToken); + } else { + createdDirectory = true; + } - HANDLE hDir = ::CreateFile(szJunction, GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (hDir == INVALID_HANDLE_VALUE) + auto* hDir = CreateFileW(linkPath.c_str(), GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (hDir == INVALID_HANDLE_VALUE) { + if (createdDirectory) + RemoveDirectoryW(linkPath.c_str()); return false; + } + + auto const substBytes = substName.size() * sizeof(wchar_t); + auto const printBytes = printName.size() * sizeof(wchar_t); + auto const pathBufferBytes = substBytes + sizeof(wchar_t) + printBytes + sizeof(wchar_t); + auto const reparseDataBytes = 8 + pathBufferBytes; + + if (substBytes > 0xffff || printBytes > 0xffff || pathBufferBytes > 0xffff || reparseDataBytes > 0xffff) { + CloseHandle(hDir); + if (createdDirectory) + RemoveDirectoryW(linkPath.c_str()); + return false; + } - memset(buf, 0, sizeof(buf)); - ReparseBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; - int len = ::MultiByteToWideChar(CP_ACP, 0, szTarget, -1, ReparseBuffer.ReparseTarget, MAX_PATH); - ReparseBuffer.ReparseTargetMaximumLength = (len--) * sizeof(WCHAR); - ReparseBuffer.ReparseTargetLength = len * sizeof(WCHAR); - ReparseBuffer.ReparseDataLength = ReparseBuffer.ReparseTargetLength + 12; - - DWORD dwRet; - if (!::DeviceIoControl(hDir, FSCTL_SET_REPARSE_POINT, &ReparseBuffer, ReparseBuffer.ReparseDataLength + REPARSE_MOUNTPOINT_HEADER_SIZE, nullptr, 0, &dwRet, nullptr)) { - DWORD dr = ::GetLastError(); - ::CloseHandle(hDir); - ::RemoveDirectory(szJunction); + auto const reparseDataLength = static_cast(reparseDataBytes); + auto const totalBytes = static_cast(REPARSE_MOUNTPOINT_HEADER_SIZE + reparseDataLength); + + std::vector buf(totalBytes, 0); + auto* const buffer = buf.data(); + + *reinterpret_cast(buffer + 0) = IO_REPARSE_TAG_MOUNT_POINT; + *reinterpret_cast(buffer + 4) = reparseDataLength; + *reinterpret_cast(buffer + 6) = 0; + *reinterpret_cast(buffer + 8) = 0; + *reinterpret_cast(buffer + 10) = static_cast(substBytes); + *reinterpret_cast(buffer + 12) = static_cast(substBytes + sizeof(wchar_t)); + *reinterpret_cast(buffer + 14) = static_cast(printBytes); + + auto* const pathBuffer = reinterpret_cast(buffer + 16); + memcpy(pathBuffer, substName.data(), substBytes); + pathBuffer[substName.size()] = L'\0'; + memcpy(pathBuffer + substName.size() + 1, printName.data(), printBytes); + pathBuffer[substName.size() + 1 + printName.size()] = L'\0'; + + DWORD dwRet = 0; + BOOL ok = DeviceIoControl(hDir, FSCTL_SET_REPARSE_POINT, + buf.data(), totalBytes, + nullptr, 0, &dwRet, nullptr); + CloseHandle(hDir); + + if (!ok) { + if (createdDirectory) + RemoveDirectoryW(linkPath.c_str()); return false; } - ::CloseHandle(hDir); return true; } @@ -286,7 +314,7 @@ bool OSUtils::isLinuxWindowMaximised(ComponentPeer* peer) XFree(states); } - return state & WINDOW_STATE_MAXIMIZED; + return (state & WINDOW_STATE_MAXIMIZED) != 0; } void OSUtils::maximiseLinuxWindow(ComponentPeer* peer, bool shouldBeMaximised) diff --git a/Source/Utility/SettingsFile.h b/Source/Utility/SettingsFile.h index 5c68bffec9..ef148a2507 100644 --- a/Source/Utility/SettingsFile.h +++ b/Source/Utility/SettingsFile.h @@ -153,6 +153,7 @@ class SettingsFile final { "autoconnect", var(true) }, { "global_scale", var(1.0f) }, { "default_zoom", var(100.0f) }, + { "text_editor_zoom", var(100.0f) }, { "floating_panels", var(true) }, { "cpu_meter_mapping_mode", var(0) }, { "centre_resized_canvas", var(true) },