1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/extras/Projucer/Source/ComponentEditor/PaintElements/jucer_ColouredElement.cpp
2024-01-30 11:37:30 +00:00

936 lines
30 KiB
C++

/*
==============================================================================
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.
==============================================================================
*/
#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) override
{
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 override
{
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<ColouredElement> 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<ColouredElement> listener;
const ColourType type;
const bool isForStroke;
};
//==============================================================================
class ElementFillPositionProperty : public PositionPropertyBase
{
public:
ElementFillPositionProperty (ColouredElement* const owner_,
const String& name,
ComponentPositionDimension dimension_,
bool isStart_,
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) override
{
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 override
{
const JucerFillType fill (isForStroke ? listener.owner->getStrokeType().fill
: listener.owner->getFillType());
return isStart ? fill.gradPos1
: fill.gradPos2;
}
private:
ElementListener<ColouredElement> listener;
const bool isStart, isForStroke;
};
//==============================================================================
class EnableStrokeProperty : public BooleanPropertyComponent
{
public:
explicit EnableStrokeProperty (ColouredElement* const owner_)
: BooleanPropertyComponent ("outline", "Outline enabled", "No outline"),
listener (owner_)
{
listener.setPropertyToRefresh (*this);
}
//==============================================================================
void setState (bool newState) override { listener.owner->enableStroke (newState, true); }
bool getState() const override { return listener.owner->isStrokeEnabled(); }
ElementListener<ColouredElement> listener;
};
//==============================================================================
class StrokeThicknessProperty : public SliderPropertyComponent
{
public:
explicit StrokeThicknessProperty (ColouredElement* const owner_)
: SliderPropertyComponent ("outline thickness", 0.1, 200.0, 0.1, 0.3),
listener (owner_)
{
listener.setPropertyToRefresh (*this);
}
void setValue (double newValue) override
{
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 override { return listener.owner->getStrokeType().stroke.getStrokeThickness(); }
ElementListener<ColouredElement> listener;
};
//==============================================================================
class StrokeJointProperty : public ChoicePropertyComponent
{
public:
explicit 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) override
{
const PathStrokeType::JointStyle joints[] = { PathStrokeType::mitered,
PathStrokeType::curved,
PathStrokeType::beveled };
if (! isPositiveAndBelow (newIndex, numElementsInArray (joints)))
{
jassertfalse;
return;
}
listener.owner->setStrokeType (PathStrokeType (listener.owner->getStrokeType().stroke.getStrokeThickness(),
joints [newIndex],
listener.owner->getStrokeType().stroke.getEndStyle()),
true);
}
int getIndex() const override
{
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<ColouredElement> listener;
};
//==============================================================================
class StrokeEndCapProperty : public ChoicePropertyComponent
{
public:
explicit 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) override
{
const PathStrokeType::EndCapStyle ends[] = { PathStrokeType::butt,
PathStrokeType::square,
PathStrokeType::rounded };
if (! isPositiveAndBelow (newIndex, numElementsInArray (ends)))
{
jassertfalse;
return;
}
listener.owner->setStrokeType (PathStrokeType (listener.owner->getStrokeType().stroke.getStrokeThickness(),
listener.owner->getStrokeType().stroke.getJointStyle(),
ends [newIndex]),
true);
}
int getIndex() const override
{
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<ColouredElement> listener;
};
//==============================================================================
class ImageBrushResourceProperty : public ImageResourceProperty <ColouredElement>
{
public:
ImageBrushResourceProperty (ColouredElement* const e, const bool isForStroke_)
: ImageResourceProperty <ColouredElement> (e, isForStroke_ ? "stroke image"
: "fill image"),
isForStroke (isForStroke_)
{
}
//==============================================================================
void setResource (const String& newName) override
{
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 override
{
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) override
{
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 override
{
if (isForStroke)
return listener.owner->getStrokeType().fill.imageAnchor;
return listener.owner->getFillType().imageAnchor;
}
private:
ElementListener<ColouredElement> 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) override
{
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 override
{
if (listener.owner == nullptr)
return 0;
if (isForStroke)
return listener.owner->getStrokeType().fill.imageOpacity;
return listener.owner->getFillType().imageOpacity;
}
private:
ElementListener<ColouredElement> 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 <PropertyComponent*>& props, bool multipleSelected)
{
PaintElement::getEditableProperties (props, multipleSelected);
if (! multipleSelected)
getColourSpecificProperties (props);
}
void ColouredElement::getColourSpecificProperties (Array <PropertyComponent*>& 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 <ColouredElement>
{
public:
FillTypeChangeAction (ColouredElement* const element, const JucerFillType& newState_)
: PaintElementUndoableAction <ColouredElement> (element),
newState (newState_)
{
oldState = element->getFillType();
}
bool perform() override
{
showCorrectTab();
getElement()->setFillType (newState, false);
return true;
}
bool undo() override
{
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 <ColouredElement>
{
public:
StrokeEnableChangeAction (ColouredElement* const element, const bool newState_)
: PaintElementUndoableAction <ColouredElement> (element),
newState (newState_)
{
oldState = element->isStrokeEnabled();
}
bool perform() override
{
showCorrectTab();
getElement()->enableStroke (newState, false);
return true;
}
bool undo() override
{
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 <ColouredElement>
{
public:
StrokeTypeChangeAction (ColouredElement* const element, const PathStrokeType& newState_)
: PaintElementUndoableAction <ColouredElement> (element),
newState (newState_),
oldState (element->getStrokeType().stroke)
{
}
bool perform() override
{
showCorrectTab();
getElement()->setStrokeType (newState, false);
return true;
}
bool undo() override
{
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 <ColouredElement>
{
public:
StrokeFillTypeChangeAction (ColouredElement* const element, const JucerFillType& newState_)
: PaintElementUndoableAction <ColouredElement> (element),
newState (newState_)
{
oldState = element->getStrokeType().fill;
}
bool perform() override
{
showCorrectTab();
getElement()->setStrokeFill (newState, false);
return true;
}
bool undo() override
{
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<int> ColouredElement::getCurrentBounds (const Rectangle<int>& 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<int>& newBounds,
const Rectangle<int>& parentArea,
const bool undoable)
{
Rectangle<int> 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<int> (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<XmlElement> xml (newElement.createXml());
PaintElement* e = getOwner()->addElementFromXml (*xml, getOwner()->indexOfElement (this), true);
getOwner()->getSelectedElements().selectOnly (e);
getOwner()->removeElement (this, true);
}
}