/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #include "../../jucer_Headers.h" #include "jucer_ColouredElement.h" #include "jucer_GradientPointComponent.h" #include "../properties/jucer_PositionPropertyBase.h" #include "../properties/jucer_ColourPropertyComponent.h" #include "jucer_PaintElementUndoableAction.h" #include "jucer_PaintElementPath.h" #include "jucer_ImageResourceProperty.h" //============================================================================== class ElementFillModeProperty : public ChoicePropertyComponent { public: ElementFillModeProperty (ColouredElement* const e, const bool isForStroke_) : ChoicePropertyComponent ("fill mode"), listener (e), isForStroke (isForStroke_) { listener.setPropertyToRefresh (*this); choices.add ("Solid Colour"); choices.add ("Linear Gradient"); choices.add ("Radial Gradient"); choices.add ("Image Brush"); } void setIndex (int newIndex) { JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill : listener.owner->getFillType()); switch (newIndex) { case 0: fill.mode = JucerFillType::solidColour; break; case 1: fill.mode = JucerFillType::linearGradient; break; case 2: fill.mode = JucerFillType::radialGradient; break; case 3: fill.mode = JucerFillType::imageBrush; break; default: jassertfalse; break; } if (! isForStroke) listener.owner->setFillType (fill, true); else listener.owner->setStrokeFill (fill, true); } int getIndex() const { switch (isForStroke ? listener.owner->getStrokeType().fill.mode : listener.owner->getFillType().mode) { case JucerFillType::solidColour: return 0; case JucerFillType::linearGradient: return 1; case JucerFillType::radialGradient: return 2; case JucerFillType::imageBrush: return 3; default: jassertfalse; break; } return 0; } private: ElementListener listener; const bool isForStroke; }; //============================================================================== class ElementFillColourProperty : public JucerColourPropertyComponent { public: enum ColourType { solidColour, gradientColour1, gradientColour2 }; ElementFillColourProperty (const String& name, ColouredElement* const owner_, const ColourType type_, const bool isForStroke_) : JucerColourPropertyComponent (name, false), listener (owner_), type (type_), isForStroke (isForStroke_) { listener.setPropertyToRefresh (*this); } void setColour (Colour newColour) override { listener.owner->getDocument()->getUndoManager().undoCurrentTransactionOnly(); JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill : listener.owner->getFillType()); switch (type) { case solidColour: fill.colour = newColour; break; case gradientColour1: fill.gradCol1 = newColour; break; case gradientColour2: fill.gradCol2 = newColour; break; default: jassertfalse; break; } if (! isForStroke) listener.owner->setFillType (fill, true); else listener.owner->setStrokeFill (fill, true); } Colour getColour() const override { const JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill : listener.owner->getFillType()); switch (type) { case solidColour: return fill.colour; break; case gradientColour1: return fill.gradCol1; break; case gradientColour2: return fill.gradCol2; break; default: jassertfalse; break; } return Colours::black; } void resetToDefault() override { jassertfalse; // option shouldn't be visible } private: ElementListener listener; const ColourType type; const bool isForStroke; }; //============================================================================== class ElementFillPositionProperty : public PositionPropertyBase { public: ElementFillPositionProperty (ColouredElement* const owner_, const String& name, ComponentPositionDimension dimension_, const bool isStart_, const bool isForStroke_) : PositionPropertyBase (owner_, name, dimension_, false, false, owner_->getDocument()->getComponentLayout()), listener (owner_), isStart (isStart_), isForStroke (isForStroke_) { listener.setPropertyToRefresh (*this); } void setPosition (const RelativePositionedRectangle& newPos) { JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill : listener.owner->getFillType()); if (isStart) fill.gradPos1 = newPos; else fill.gradPos2 = newPos; if (! isForStroke) listener.owner->setFillType (fill, true); else listener.owner->setStrokeFill (fill, true); } RelativePositionedRectangle getPosition() const { const JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill : listener.owner->getFillType()); return isStart ? fill.gradPos1 : fill.gradPos2; } private: ElementListener listener; const bool isStart, isForStroke; }; //============================================================================== class EnableStrokeProperty : public BooleanPropertyComponent { public: EnableStrokeProperty (ColouredElement* const owner_) : BooleanPropertyComponent ("outline", "Outline enabled", "No outline"), listener (owner_) { listener.setPropertyToRefresh (*this); } //============================================================================== void setState (bool newState) { listener.owner->enableStroke (newState, true); } bool getState() const { return listener.owner->isStrokeEnabled(); } ElementListener listener; }; //============================================================================== class StrokeThicknessProperty : public SliderPropertyComponent { public: StrokeThicknessProperty (ColouredElement* const owner_) : SliderPropertyComponent ("outline thickness", 0.1, 200.0, 0.1, 0.3), listener (owner_) { listener.setPropertyToRefresh (*this); } void setValue (double newValue) { listener.owner->getDocument()->getUndoManager().undoCurrentTransactionOnly(); listener.owner->setStrokeType (PathStrokeType ((float) newValue, listener.owner->getStrokeType().stroke.getJointStyle(), listener.owner->getStrokeType().stroke.getEndStyle()), true); } double getValue() const { return listener.owner->getStrokeType().stroke.getStrokeThickness(); } ElementListener listener; }; //============================================================================== class StrokeJointProperty : public ChoicePropertyComponent { public: StrokeJointProperty (ColouredElement* const owner_) : ChoicePropertyComponent ("joint style"), listener (owner_) { listener.setPropertyToRefresh (*this); choices.add ("mitered"); choices.add ("curved"); choices.add ("beveled"); } void setIndex (int newIndex) { const PathStrokeType::JointStyle joints[] = { PathStrokeType::mitered, PathStrokeType::curved, PathStrokeType::beveled }; jassert (newIndex >= 0 && newIndex < 3); listener.owner->setStrokeType (PathStrokeType (listener.owner->getStrokeType().stroke.getStrokeThickness(), joints [newIndex], listener.owner->getStrokeType().stroke.getEndStyle()), true); } int getIndex() const { switch (listener.owner->getStrokeType().stroke.getJointStyle()) { case PathStrokeType::mitered: return 0; case PathStrokeType::curved: return 1; case PathStrokeType::beveled: return 2; default: jassertfalse; break; } return 0; } ElementListener listener; }; //============================================================================== class StrokeEndCapProperty : public ChoicePropertyComponent { public: StrokeEndCapProperty (ColouredElement* const owner_) : ChoicePropertyComponent ("end-cap style"), listener (owner_) { listener.setPropertyToRefresh (*this); choices.add ("butt"); choices.add ("square"); choices.add ("round"); } void setIndex (int newIndex) { const PathStrokeType::EndCapStyle ends[] = { PathStrokeType::butt, PathStrokeType::square, PathStrokeType::rounded }; jassert (newIndex >= 0 && newIndex < 3); listener.owner->setStrokeType (PathStrokeType (listener.owner->getStrokeType().stroke.getStrokeThickness(), listener.owner->getStrokeType().stroke.getJointStyle(), ends [newIndex]), true); } int getIndex() const { switch (listener.owner->getStrokeType().stroke.getEndStyle()) { case PathStrokeType::butt: return 0; case PathStrokeType::square: return 1; case PathStrokeType::rounded: return 2; default: jassertfalse; break; } return 0; } ElementListener listener; }; //============================================================================== class ImageBrushResourceProperty : public ImageResourceProperty { public: ImageBrushResourceProperty (ColouredElement* const e, const bool isForStroke_) : ImageResourceProperty (e, isForStroke_ ? "stroke image" : "fill image"), isForStroke (isForStroke_) { } //============================================================================== void setResource (const String& newName) { if (element != nullptr) { if (isForStroke) { JucerFillType type (element->getStrokeType().fill); type.imageResourceName = newName; element->setStrokeFill (type, true); } else { JucerFillType type (element->getFillType()); type.imageResourceName = newName; element->setFillType (type, true); } } } String getResource() const { if (element == nullptr) return {}; if (isForStroke) return element->getStrokeType().fill.imageResourceName; return element->getFillType().imageResourceName; } private: bool isForStroke; }; //============================================================================== class ImageBrushPositionProperty : public PositionPropertyBase { public: ImageBrushPositionProperty (ColouredElement* const owner_, const String& name, ComponentPositionDimension dimension_, const bool isForStroke_) : PositionPropertyBase (owner_, name, dimension_, false, false, owner_->getDocument()->getComponentLayout()), listener (owner_), isForStroke (isForStroke_) { listener.setPropertyToRefresh (*this); } void setPosition (const RelativePositionedRectangle& newPos) { if (isForStroke) { JucerFillType type (listener.owner->getStrokeType().fill); type.imageAnchor = newPos; listener.owner->setStrokeFill (type, true); } else { JucerFillType type (listener.owner->getFillType()); type.imageAnchor = newPos; listener.owner->setFillType (type, true); } } RelativePositionedRectangle getPosition() const { if (isForStroke) return listener.owner->getStrokeType().fill.imageAnchor; return listener.owner->getFillType().imageAnchor; } private: ElementListener listener; const bool isForStroke; }; //============================================================================== class ImageBrushOpacityProperty : public SliderPropertyComponent { public: ImageBrushOpacityProperty (ColouredElement* const e, const bool isForStroke_) : SliderPropertyComponent ("opacity", 0.0, 1.0, 0.001), listener (e), isForStroke (isForStroke_) { listener.setPropertyToRefresh (*this); } void setValue (double newValue) { if (listener.owner != nullptr) { listener.owner->getDocument()->getUndoManager().undoCurrentTransactionOnly(); if (isForStroke) { JucerFillType type (listener.owner->getStrokeType().fill); type.imageOpacity = newValue; listener.owner->setStrokeFill (type, true); } else { JucerFillType type (listener.owner->getFillType()); type.imageOpacity = newValue; listener.owner->setFillType (type, true); } } } double getValue() const { if (listener.owner == nullptr) return 0; if (isForStroke) return listener.owner->getStrokeType().fill.imageOpacity; return listener.owner->getFillType().imageOpacity; } private: ElementListener listener; bool isForStroke; }; //============================================================================== ColouredElement::ColouredElement (PaintRoutine* owner_, const String& name, const bool showOutline_, const bool showJointAndEnd_) : PaintElement (owner_, name), isStrokePresent (false), showOutline (showOutline_), showJointAndEnd (showJointAndEnd_) { } ColouredElement::~ColouredElement() { } //============================================================================== void ColouredElement::getEditableProperties (Array & props) { PaintElement::getEditableProperties (props); getColourSpecificProperties (props); } void ColouredElement::getColourSpecificProperties (Array & props) { props.add (new ElementFillModeProperty (this, false)); switch (getFillType().mode) { case JucerFillType::solidColour: props.add (new ElementFillColourProperty ("colour", this, ElementFillColourProperty::solidColour, false)); break; case JucerFillType::linearGradient: case JucerFillType::radialGradient: props.add (new ElementFillColourProperty ("colour 1", this, ElementFillColourProperty::gradientColour1, false)); props.add (new ElementFillPositionProperty (this, "x1", PositionPropertyBase::componentX, true, false)); props.add (new ElementFillPositionProperty (this, "y1", PositionPropertyBase::componentY, true, false)); props.add (new ElementFillColourProperty ("colour 2", this, ElementFillColourProperty::gradientColour2, false)); props.add (new ElementFillPositionProperty (this, "x2", PositionPropertyBase::componentX, false, false)); props.add (new ElementFillPositionProperty (this, "y2", PositionPropertyBase::componentY, false, false)); break; case JucerFillType::imageBrush: props.add (new ImageBrushResourceProperty (this, false)); props.add (new ImageBrushPositionProperty (this, "anchor x", PositionPropertyBase::componentX, false)); props.add (new ImageBrushPositionProperty (this, "anchor y", PositionPropertyBase::componentY, false)); props.add (new ImageBrushOpacityProperty (this, false)); break; default: jassertfalse; break; } if (showOutline) { props.add (new EnableStrokeProperty (this)); if (isStrokePresent) { props.add (new StrokeThicknessProperty (this)); if (showJointAndEnd) { props.add (new StrokeJointProperty (this)); props.add (new StrokeEndCapProperty (this)); } props.add (new ElementFillModeProperty (this, true)); switch (getStrokeType().fill.mode) { case JucerFillType::solidColour: props.add (new ElementFillColourProperty ("colour", this, ElementFillColourProperty::solidColour, true)); break; case JucerFillType::linearGradient: case JucerFillType::radialGradient: props.add (new ElementFillColourProperty ("colour 1", this, ElementFillColourProperty::gradientColour1, true)); props.add (new ElementFillPositionProperty (this, "x1", PositionPropertyBase::componentX, true, true)); props.add (new ElementFillPositionProperty (this, "y1", PositionPropertyBase::componentY, true, true)); props.add (new ElementFillColourProperty ("colour 2", this, ElementFillColourProperty::gradientColour2, true)); props.add (new ElementFillPositionProperty (this, "x2", PositionPropertyBase::componentX, false, true)); props.add (new ElementFillPositionProperty (this, "y2", PositionPropertyBase::componentY, false, true)); break; case JucerFillType::imageBrush: props.add (new ImageBrushResourceProperty (this, true)); props.add (new ImageBrushPositionProperty (this, "stroke anchor x", PositionPropertyBase::componentX, true)); props.add (new ImageBrushPositionProperty (this, "stroke anchor y", PositionPropertyBase::componentY, true)); props.add (new ImageBrushOpacityProperty (this, true)); break; default: jassertfalse; break; } } } } //============================================================================== const JucerFillType& ColouredElement::getFillType() noexcept { return fillType; } class FillTypeChangeAction : public PaintElementUndoableAction { public: FillTypeChangeAction (ColouredElement* const element, const JucerFillType& newState_) : PaintElementUndoableAction (element), newState (newState_) { oldState = element->getFillType(); } bool perform() { showCorrectTab(); getElement()->setFillType (newState, false); return true; } bool undo() { showCorrectTab(); getElement()->setFillType (oldState, false); return true; } private: JucerFillType newState, oldState; }; void ColouredElement::setFillType (const JucerFillType& newType, const bool undoable) { if (fillType != newType) { if (undoable) { perform (new FillTypeChangeAction (this, newType), "Change fill type"); } else { repaint(); if (fillType.mode != newType.mode) { owner->getSelectedElements().changed(); siblingComponentsChanged(); } fillType = newType; changed(); } } } //============================================================================== bool ColouredElement::isStrokeEnabled() const noexcept { return isStrokePresent && showOutline; } class StrokeEnableChangeAction : public PaintElementUndoableAction { public: StrokeEnableChangeAction (ColouredElement* const element, const bool newState_) : PaintElementUndoableAction (element), newState (newState_) { oldState = element->isStrokeEnabled(); } bool perform() { showCorrectTab(); getElement()->enableStroke (newState, false); return true; } bool undo() { showCorrectTab(); getElement()->enableStroke (oldState, false); return true; } private: bool newState, oldState; }; void ColouredElement::enableStroke (bool enable, const bool undoable) { enable = enable && showOutline; if (isStrokePresent != enable) { if (undoable) { perform (new StrokeEnableChangeAction (this, enable), "Change stroke mode"); } else { repaint(); isStrokePresent = enable; siblingComponentsChanged(); owner->changed(); owner->getSelectedElements().changed(); } } } //============================================================================== const StrokeType& ColouredElement::getStrokeType() noexcept { return strokeType; } class StrokeTypeChangeAction : public PaintElementUndoableAction { public: StrokeTypeChangeAction (ColouredElement* const element, const PathStrokeType& newState_) : PaintElementUndoableAction (element), newState (newState_), oldState (element->getStrokeType().stroke) { } bool perform() { showCorrectTab(); getElement()->setStrokeType (newState, false); return true; } bool undo() { showCorrectTab(); getElement()->setStrokeType (oldState, false); return true; } private: PathStrokeType newState, oldState; }; void ColouredElement::setStrokeType (const PathStrokeType& newType, const bool undoable) { if (strokeType.stroke != newType) { if (undoable) { perform (new StrokeTypeChangeAction (this, newType), "Change stroke type"); } else { repaint(); strokeType.stroke = newType; changed(); } } } class StrokeFillTypeChangeAction : public PaintElementUndoableAction { public: StrokeFillTypeChangeAction (ColouredElement* const element, const JucerFillType& newState_) : PaintElementUndoableAction (element), newState (newState_) { oldState = element->getStrokeType().fill; } bool perform() { showCorrectTab(); getElement()->setStrokeFill (newState, false); return true; } bool undo() { showCorrectTab(); getElement()->setStrokeFill (oldState, false); return true; } private: JucerFillType newState, oldState; }; void ColouredElement::setStrokeFill (const JucerFillType& newType, const bool undoable) { if (strokeType.fill != newType) { if (undoable) { perform (new StrokeFillTypeChangeAction (this, newType), "Change stroke fill type"); } else { repaint(); if (strokeType.fill.mode != newType.mode) { siblingComponentsChanged(); owner->getSelectedElements().changed(); } strokeType.fill = newType; changed(); } } } //============================================================================== void ColouredElement::createSiblingComponents() { { GradientPointComponent* g1 = new GradientPointComponent (this, false, true); siblingComponents.add (g1); GradientPointComponent* g2 = new GradientPointComponent (this, false, false); siblingComponents.add (g2); getParentComponent()->addAndMakeVisible (g1); getParentComponent()->addAndMakeVisible (g2); g1->updatePosition(); g2->updatePosition(); } if (isStrokePresent && showOutline) { GradientPointComponent* g1 = new GradientPointComponent (this, true, true); siblingComponents.add (g1); GradientPointComponent* g2 = new GradientPointComponent (this, true, false); siblingComponents.add (g2); getParentComponent()->addAndMakeVisible (g1); getParentComponent()->addAndMakeVisible (g2); g1->updatePosition(); g2->updatePosition(); } } Rectangle ColouredElement::getCurrentBounds (const Rectangle& parentArea) const { int borderSize = 0; if (isStrokePresent) borderSize = (int) strokeType.stroke.getStrokeThickness() / 2 + 1; return position.getRectangle (parentArea, getDocument()->getComponentLayout()) .expanded (borderSize); } void ColouredElement::setCurrentBounds (const Rectangle& newBounds, const Rectangle& parentArea, const bool undoable) { Rectangle r (newBounds); if (isStrokePresent) { r = r.expanded (-((int) strokeType.stroke.getStrokeThickness() / 2 + 1)); r.setSize (jmax (1, r.getWidth()), jmax (1, r.getHeight())); } RelativePositionedRectangle pr (position); pr.updateFrom (r.getX() - parentArea.getX(), r.getY() - parentArea.getY(), r.getWidth(), r.getHeight(), Rectangle (0, 0, parentArea.getWidth(), parentArea.getHeight()), getDocument()->getComponentLayout()); setPosition (pr, undoable); updateBounds (parentArea); } //============================================================================== void ColouredElement::addColourAttributes (XmlElement* const e) const { e->setAttribute ("fill", fillType.toString()); e->setAttribute ("hasStroke", isStrokePresent); if (isStrokePresent && showOutline) { e->setAttribute ("stroke", strokeType.toString()); e->setAttribute ("strokeColour", strokeType.fill.toString()); } } bool ColouredElement::loadColourAttributes (const XmlElement& xml) { fillType.restoreFromString (xml.getStringAttribute ("fill", String())); isStrokePresent = showOutline && xml.getBoolAttribute ("hasStroke", false); strokeType.restoreFromString (xml.getStringAttribute ("stroke", String())); strokeType.fill.restoreFromString (xml.getStringAttribute ("strokeColour", String())); return true; } //============================================================================== void ColouredElement::convertToNewPathElement (const Path& path) { if (! path.isEmpty()) { PaintElementPath newElement (getOwner()); newElement.setToPath (path); newElement.setFillType (fillType, false); newElement.enableStroke (isStrokeEnabled(), false); newElement.setStrokeType (getStrokeType().stroke, false); newElement.setStrokeFill (getStrokeType().fill, false); ScopedPointer xml (newElement.createXml()); PaintElement* e = getOwner()->addElementFromXml (*xml, getOwner()->indexOfElement (this), true); getOwner()->getSelectedElements().selectOnly (e); getOwner()->removeElement (this, true); } }