/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once #include "../UI/jucer_PaintRoutineEditor.h" #include "../UI/jucer_ComponentLayoutEditor.h" //============================================================================== /** Base class for a property that edits the x, y, w, or h of a PositionedRectangle. */ class PositionPropertyBase : public PropertyComponent, protected ChangeListener { public: enum ComponentPositionDimension { componentX = 0, componentY = 1, componentWidth = 2, componentHeight = 3 }; PositionPropertyBase (Component* comp, const String& name, ComponentPositionDimension dimension_, const bool includeAnchorOptions_, const bool allowRelativeOptions_, ComponentLayout* layout_) : PropertyComponent (name), layout (layout_), button ("mode"), component (comp), dimension (dimension_), includeAnchorOptions (includeAnchorOptions_), allowRelativeOptions (allowRelativeOptions_) { addAndMakeVisible (button); button.setTriggeredOnMouseDown (true); button.setConnectedEdges (TextButton::ConnectedOnLeft | TextButton::ConnectedOnRight); button.onClick = [this] { SafePointer safeThis { this }; showMenu (layout, [safeThis] (bool shouldRefresh) { if (safeThis == nullptr) return; if (shouldRefresh) safeThis->refresh(); // (to clear the text editor if it's got focus) }); }; textEditor.reset (new PositionPropLabel (*this)); addAndMakeVisible (textEditor.get()); } String getText() const { RelativePositionedRectangle rpr (getPosition()); PositionedRectangle& p = rpr.rect; String s; switch (dimension) { case componentX: if (p.getPositionModeX() == PositionedRectangle::proportionOfParentSize) s << valueToString (p.getX() * 100.0) << '%'; else s << valueToString (p.getX()); break; case componentY: if (p.getPositionModeY() == PositionedRectangle::proportionOfParentSize) s << valueToString (p.getY() * 100.0) << '%'; else s << valueToString (p.getY()); break; case componentWidth: if (p.getWidthMode() == PositionedRectangle::proportionalSize) s << valueToString (p.getWidth() * 100.0) << '%'; else s << valueToString (p.getWidth()); break; case componentHeight: if (p.getHeightMode() == PositionedRectangle::proportionalSize) s << valueToString (p.getHeight() * 100.0) << '%'; else s << valueToString (p.getHeight()); break; default: jassertfalse; break; }; return s; } static String valueToString (const double n) { return String (roundToInt (n * 1000.0) / 1000.0); } void setText (const String& newText) { RelativePositionedRectangle rpr (getPosition()); PositionedRectangle p (rpr.rect); const double value = newText.getDoubleValue(); switch (dimension) { case componentX: if (p.getPositionModeX() == PositionedRectangle::proportionOfParentSize) p.setX (value / 100.0); else p.setX (value); break; case componentY: if (p.getPositionModeY() == PositionedRectangle::proportionOfParentSize) p.setY (value / 100.0); else p.setY (value); break; case componentWidth: if (p.getWidthMode() == PositionedRectangle::proportionalSize) p.setWidth (value / 100.0); else p.setWidth (value); break; case componentHeight: if (p.getHeightMode() == PositionedRectangle::proportionalSize) p.setHeight (value / 100.0); else p.setHeight (value); break; default: jassertfalse; break; }; if (p != rpr.rect) { rpr.rect = p; setPosition (rpr); } } void changeListenerCallback (ChangeBroadcaster*) { refresh(); } void showMenu (ComponentLayout* compLayout, std::function callback) { RelativePositionedRectangle rpr (getPosition()); PositionedRectangle p (rpr.rect); PositionedRectangle::AnchorPoint xAnchor = p.getAnchorPointX(); PositionedRectangle::AnchorPoint yAnchor = p.getAnchorPointY(); PositionedRectangle::PositionMode xMode = p.getPositionModeX(); PositionedRectangle::PositionMode yMode = p.getPositionModeY(); PositionedRectangle::SizeMode sizeW = p.getWidthMode(); PositionedRectangle::SizeMode sizeH = p.getHeightMode(); String relCompName ("parent"); if (Component* const relComp = compLayout != nullptr ? compLayout->getComponentRelativePosTarget (component, (int) dimension) : nullptr) relCompName = compLayout->getComponentMemberVariableName (relComp); jassert (relCompName.isNotEmpty()); PopupMenu m; if (dimension == componentX || dimension == componentY) { const PositionedRectangle::PositionMode posMode = (dimension == componentX) ? xMode : yMode; m.addItem (10, ((dimension == componentX) ? "Absolute distance from left of " : "Absolute distance from top of ") + relCompName, true, posMode == PositionedRectangle::absoluteFromParentTopLeft); m.addItem (11, ((dimension == componentX) ? "Absolute distance from right of " : "Absolute distance from bottom of ") + relCompName, true, posMode == PositionedRectangle::absoluteFromParentBottomRight); m.addItem (12, "Absolute distance from centre of " + relCompName, true, posMode == PositionedRectangle::absoluteFromParentCentre); m.addItem (13, ((dimension == componentX) ? "Percentage of width of " : "Percentage of height of ") + relCompName, true, posMode == PositionedRectangle::proportionOfParentSize); m.addSeparator(); if (includeAnchorOptions) { const PositionedRectangle::AnchorPoint anchor = (dimension == componentX) ? xAnchor : yAnchor; m.addItem (14, (dimension == componentX) ? "Anchored at left of component" : "Anchored at top of component", true, anchor == PositionedRectangle::anchorAtLeftOrTop); m.addItem (15, "Anchored at centre of component", true, anchor == PositionedRectangle::anchorAtCentre); m.addItem (16, (dimension == componentX) ? "Anchored at right of component" : "Anchored at bottom of component", true, anchor == PositionedRectangle::anchorAtRightOrBottom); } } else { const PositionedRectangle::SizeMode sizeMode = (dimension == componentWidth) ? sizeW : sizeH; m.addItem (20, (dimension == componentWidth) ? "Absolute width" : "Absolute height", true, sizeMode == PositionedRectangle::absoluteSize); m.addItem (21, ((dimension == componentWidth) ? "Percentage of width of " : "Percentage of height of ") + relCompName, true, sizeMode == PositionedRectangle::proportionalSize); m.addItem (22, ((dimension == componentWidth) ? "Subtracted from width of " : "Subtracted from height of ") + relCompName, true, sizeMode == PositionedRectangle::parentSizeMinusAbsolute); } if (allowRelativeOptions && compLayout != nullptr) { m.addSeparator(); m.addSubMenu ("Relative to", compLayout->getRelativeTargetMenu (component, (int) dimension)); } m.showMenuAsync (PopupMenu::Options().withTargetComponent (&button), [compLayout, callback, xAnchor, yAnchor, xMode, yMode, sizeW, sizeH, p, rpr, ref = SafePointer { this }] (int menuResult) mutable { if (menuResult == 0 || ref == nullptr) { callback (false); return; } switch (menuResult) { case 10: if (ref->dimension == componentX) xMode = PositionedRectangle::absoluteFromParentTopLeft; else yMode = PositionedRectangle::absoluteFromParentTopLeft; break; case 11: if (ref->dimension == componentX) xMode = PositionedRectangle::absoluteFromParentBottomRight; else yMode = PositionedRectangle::absoluteFromParentBottomRight; break; case 12: if (ref->dimension == componentX) xMode = PositionedRectangle::absoluteFromParentCentre; else yMode = PositionedRectangle::absoluteFromParentCentre; break; case 13: if (ref->dimension == componentX) xMode = PositionedRectangle::proportionOfParentSize; else yMode = PositionedRectangle::proportionOfParentSize; break; case 14: if (ref->dimension == componentX) xAnchor = PositionedRectangle::anchorAtLeftOrTop; else yAnchor = PositionedRectangle::anchorAtLeftOrTop; break; case 15: if (ref->dimension == componentX) xAnchor = PositionedRectangle::anchorAtCentre; else yAnchor = PositionedRectangle::anchorAtCentre; break; case 16: if (ref->dimension == componentX) xAnchor = PositionedRectangle::anchorAtRightOrBottom; else yAnchor = PositionedRectangle::anchorAtRightOrBottom; break; case 20: if (ref->dimension == componentWidth) sizeW = PositionedRectangle::absoluteSize; else sizeH = PositionedRectangle::absoluteSize; break; case 21: if (ref->dimension == componentWidth) sizeW = PositionedRectangle::proportionalSize; else sizeH = PositionedRectangle::proportionalSize; break; case 22: if (ref->dimension == componentWidth) sizeW = PositionedRectangle::parentSizeMinusAbsolute; else sizeH = PositionedRectangle::parentSizeMinusAbsolute; break; default: if (ref->allowRelativeOptions && compLayout != nullptr) compLayout->processRelativeTargetMenuResult (ref->component, (int) ref->dimension, menuResult); break; } const auto parentArea = [&]() -> Rectangle { if (ref->component->findParentComponentOfClass() != nullptr) return { ref->component->getParentWidth(), ref->component->getParentHeight() }; if (auto pre = dynamic_cast (ref->component->getParentComponent())) return pre->getComponentArea(); jassertfalse; return {}; }(); int x, xw, y, yh, w, h; rpr.getRelativeTargetBounds (parentArea, compLayout, x, xw, y, yh, w, h); PositionedRectangle xyRect (p); PositionedRectangle whRect (p); xyRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, Rectangle (x, y, xw, yh)); whRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, Rectangle (x, y, w, h)); p.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, Rectangle (x, y, xw, yh)); p.setX (xyRect.getX()); p.setY (xyRect.getY()); p.setWidth (whRect.getWidth()); p.setHeight (whRect.getHeight()); if (p != rpr.rect) { rpr.rect = p; ref->setPosition (rpr); } callback (true); }); } void resized() { const Rectangle r (getLookAndFeel().getPropertyComponentContentPosition (*this)); button.changeWidthToFitText (r.getHeight()); button.setTopRightPosition (r.getRight(), r.getY()); textEditor->setBounds (r.getX(), r.getY(), button.getX() - r.getX(), r.getHeight()); } void refresh() { textEditor->setText (getText(), dontSendNotification); } void textWasEdited() { const String newText (textEditor->getText()); if (getText() != newText) setText (newText); } //============================================================================== virtual void setPosition (const RelativePositionedRectangle& newPos) = 0; virtual RelativePositionedRectangle getPosition() const = 0; protected: class PositionPropLabel : public Label { public: PositionPropLabel (PositionPropertyBase& owner_) : Label (String(), String()), owner (owner_) { setEditable (true, true, false); updateLookAndFeel(); } TextEditor* createEditorComponent() override { TextEditor* ed = Label::createEditorComponent(); ed->setInputRestrictions (14, "0123456789.-%"); return ed; } void textWasEdited() override { owner.textWasEdited(); } void lookAndFeelChanged() override { updateLookAndFeel(); } private: void updateLookAndFeel() { setColour (backgroundColourId, findColour (widgetBackgroundColourId)); setColour (textColourId, findColour (widgetTextColourId)); } PositionPropertyBase& owner; }; ComponentLayout* layout; std::unique_ptr textEditor; TextButton button; Component* component; ComponentPositionDimension dimension; const bool includeAnchorOptions, allowRelativeOptions; };