/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2017 - ROLI Ltd. You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../../Application/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, bool multipleSelected) { PaintElement::getEditableProperties (props, multipleSelected); if (! multipleSelected) 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); std::unique_ptr xml (newElement.createXml()); PaintElement* e = getOwner()->addElementFromXml (*xml, getOwner()->indexOfElement (this), true); getOwner()->getSelectedElements().selectOnly (e); getOwner()->removeElement (this, true); } }