/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software 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. ============================================================================== */ DrawableShape::DrawableShape() : strokeType (0.0f), mainFill (Colours::black), strokeFill (Colours::black) { } DrawableShape::DrawableShape (const DrawableShape& other) : Drawable (other), strokeType (other.strokeType), mainFill (other.mainFill), strokeFill (other.strokeFill) { } DrawableShape::~DrawableShape() { } //============================================================================== class DrawableShape::RelativePositioner : public RelativeCoordinatePositionerBase { public: RelativePositioner (DrawableShape& comp, const DrawableShape::RelativeFillType& f, bool isMain) : RelativeCoordinatePositionerBase (comp), owner (comp), fill (f), isMainFill (isMain) { } bool registerCoordinates() override { bool ok = addPoint (fill.gradientPoint1); ok = addPoint (fill.gradientPoint2) && ok; return addPoint (fill.gradientPoint3) && ok; } void applyToComponentBounds() override { ComponentScope scope (owner); if (isMainFill ? owner.mainFill.recalculateCoords (&scope) : owner.strokeFill.recalculateCoords (&scope)) owner.repaint(); } void applyNewBounds (const Rectangle&) override { jassertfalse; // drawables can't be resized directly! } private: DrawableShape& owner; const DrawableShape::RelativeFillType fill; const bool isMainFill; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RelativePositioner) }; void DrawableShape::setFill (const FillType& newFill) { setFill (RelativeFillType (newFill)); } void DrawableShape::setStrokeFill (const FillType& newFill) { setStrokeFill (RelativeFillType (newFill)); } void DrawableShape::setFillInternal (RelativeFillType& fill, const RelativeFillType& newFill, ScopedPointer& pos) { if (fill != newFill) { fill = newFill; pos = nullptr; if (fill.isDynamic()) { pos = new RelativePositioner (*this, fill, true); pos->apply(); } else { fill.recalculateCoords (nullptr); } repaint(); } } void DrawableShape::setFill (const RelativeFillType& newFill) { setFillInternal (mainFill, newFill, mainFillPositioner); } void DrawableShape::setStrokeFill (const RelativeFillType& newFill) { setFillInternal (strokeFill, newFill, strokeFillPositioner); } void DrawableShape::setStrokeType (const PathStrokeType& newStrokeType) { if (strokeType != newStrokeType) { strokeType = newStrokeType; strokeChanged(); } } void DrawableShape::setStrokeThickness (const float newThickness) { setStrokeType (PathStrokeType (newThickness, strokeType.getJointStyle(), strokeType.getEndStyle())); } bool DrawableShape::isStrokeVisible() const noexcept { return strokeType.getStrokeThickness() > 0.0f && ! strokeFill.fill.isInvisible(); } void DrawableShape::refreshFillTypes (const FillAndStrokeState& newState, ComponentBuilder::ImageProvider* imageProvider) { setFill (newState.getFill (FillAndStrokeState::fill, imageProvider)); setStrokeFill (newState.getFill (FillAndStrokeState::stroke, imageProvider)); } void DrawableShape::writeTo (FillAndStrokeState& state, ComponentBuilder::ImageProvider* imageProvider, UndoManager* undoManager) const { state.setFill (FillAndStrokeState::fill, mainFill, imageProvider, undoManager); state.setFill (FillAndStrokeState::stroke, strokeFill, imageProvider, undoManager); state.setStrokeType (strokeType, undoManager); } //============================================================================== void DrawableShape::paint (Graphics& g) { transformContextToCorrectOrigin (g); g.setFillType (mainFill.fill); g.fillPath (path); if (isStrokeVisible()) { g.setFillType (strokeFill.fill); g.fillPath (strokePath); } } void DrawableShape::pathChanged() { strokeChanged(); } void DrawableShape::strokeChanged() { strokePath.clear(); strokeType.createStrokedPath (strokePath, path, AffineTransform::identity, 4.0f); setBoundsToEnclose (getDrawableBounds()); repaint(); } Rectangle DrawableShape::getDrawableBounds() const { if (isStrokeVisible()) return strokePath.getBounds(); return path.getBounds(); } bool DrawableShape::hitTest (int x, int y) { bool allowsClicksOnThisComponent, allowsClicksOnChildComponents; getInterceptsMouseClicks (allowsClicksOnThisComponent, allowsClicksOnChildComponents); if (! allowsClicksOnThisComponent) return false; const float globalX = (float) (x - originRelativeToComponent.x); const float globalY = (float) (y - originRelativeToComponent.y); return path.contains (globalX, globalY) || (isStrokeVisible() && strokePath.contains (globalX, globalY)); } //============================================================================== DrawableShape::RelativeFillType::RelativeFillType() { } DrawableShape::RelativeFillType::RelativeFillType (const FillType& fill_) : fill (fill_) { if (fill.isGradient()) { const ColourGradient& g = *fill.gradient; gradientPoint1 = g.point1.transformedBy (fill.transform); gradientPoint2 = g.point2.transformedBy (fill.transform); gradientPoint3 = Point (g.point1.x + g.point2.y - g.point1.y, g.point1.y + g.point1.x - g.point2.x) .transformedBy (fill.transform); fill.transform = AffineTransform::identity; } } DrawableShape::RelativeFillType::RelativeFillType (const RelativeFillType& other) : fill (other.fill), gradientPoint1 (other.gradientPoint1), gradientPoint2 (other.gradientPoint2), gradientPoint3 (other.gradientPoint3) { } DrawableShape::RelativeFillType& DrawableShape::RelativeFillType::operator= (const RelativeFillType& other) { fill = other.fill; gradientPoint1 = other.gradientPoint1; gradientPoint2 = other.gradientPoint2; gradientPoint3 = other.gradientPoint3; return *this; } bool DrawableShape::RelativeFillType::operator== (const RelativeFillType& other) const { return fill == other.fill && ((! fill.isGradient()) || (gradientPoint1 == other.gradientPoint1 && gradientPoint2 == other.gradientPoint2 && gradientPoint3 == other.gradientPoint3)); } bool DrawableShape::RelativeFillType::operator!= (const RelativeFillType& other) const { return ! operator== (other); } bool DrawableShape::RelativeFillType::recalculateCoords (Expression::Scope* scope) { if (fill.isGradient()) { const Point g1 (gradientPoint1.resolve (scope)); const Point g2 (gradientPoint2.resolve (scope)); AffineTransform t; ColourGradient& g = *fill.gradient; if (g.isRadial) { const Point g3 (gradientPoint3.resolve (scope)); const Point g3Source (g1.x + g2.y - g1.y, g1.y + g1.x - g2.x); t = AffineTransform::fromTargetPoints (g1.x, g1.y, g1.x, g1.y, g2.x, g2.y, g2.x, g2.y, g3Source.x, g3Source.y, g3.x, g3.y); } if (g.point1 != g1 || g.point2 != g2 || fill.transform != t) { g.point1 = g1; g.point2 = g2; fill.transform = t; return true; } } return false; } bool DrawableShape::RelativeFillType::isDynamic() const { return gradientPoint1.isDynamic() || gradientPoint2.isDynamic() || gradientPoint3.isDynamic(); } void DrawableShape::RelativeFillType::writeTo (ValueTree& v, ComponentBuilder::ImageProvider* imageProvider, UndoManager* undoManager) const { if (fill.isColour()) { v.setProperty (FillAndStrokeState::type, "solid", undoManager); v.setProperty (FillAndStrokeState::colour, String::toHexString ((int) fill.colour.getARGB()), undoManager); } else if (fill.isGradient()) { v.setProperty (FillAndStrokeState::type, "gradient", undoManager); v.setProperty (FillAndStrokeState::gradientPoint1, gradientPoint1.toString(), undoManager); v.setProperty (FillAndStrokeState::gradientPoint2, gradientPoint2.toString(), undoManager); v.setProperty (FillAndStrokeState::gradientPoint3, gradientPoint3.toString(), undoManager); const ColourGradient& cg = *fill.gradient; v.setProperty (FillAndStrokeState::radial, cg.isRadial, undoManager); String s; for (int i = 0; i < cg.getNumColours(); ++i) s << ' ' << cg.getColourPosition (i) << ' ' << String::toHexString ((int) cg.getColour(i).getARGB()); v.setProperty (FillAndStrokeState::colours, s.trimStart(), undoManager); } else if (fill.isTiledImage()) { v.setProperty (FillAndStrokeState::type, "image", undoManager); if (imageProvider != nullptr) v.setProperty (FillAndStrokeState::imageId, imageProvider->getIdentifierForImage (fill.image), undoManager); if (fill.getOpacity() < 1.0f) v.setProperty (FillAndStrokeState::imageOpacity, fill.getOpacity(), undoManager); else v.removeProperty (FillAndStrokeState::imageOpacity, undoManager); } else { jassertfalse; } } bool DrawableShape::RelativeFillType::readFrom (const ValueTree& v, ComponentBuilder::ImageProvider* imageProvider) { const String newType (v [FillAndStrokeState::type].toString()); if (newType == "solid") { const String colourString (v [FillAndStrokeState::colour].toString()); fill.setColour (colourString.isEmpty() ? Colours::black : Colour::fromString (colourString)); return true; } else if (newType == "gradient") { ColourGradient g; g.isRadial = v [FillAndStrokeState::radial]; StringArray colourSteps; colourSteps.addTokens (v [FillAndStrokeState::colours].toString(), false); for (int i = 0; i < colourSteps.size() / 2; ++i) g.addColour (colourSteps[i * 2].getDoubleValue(), Colour::fromString (colourSteps[i * 2 + 1])); fill.setGradient (g); gradientPoint1 = RelativePoint (v [FillAndStrokeState::gradientPoint1]); gradientPoint2 = RelativePoint (v [FillAndStrokeState::gradientPoint2]); gradientPoint3 = RelativePoint (v [FillAndStrokeState::gradientPoint3]); return true; } else if (newType == "image") { Image im; if (imageProvider != nullptr) im = imageProvider->getImageForIdentifier (v [FillAndStrokeState::imageId]); fill.setTiledImage (im, AffineTransform::identity); fill.setOpacity ((float) v.getProperty (FillAndStrokeState::imageOpacity, 1.0f)); return true; } jassertfalse; return false; } //============================================================================== const Identifier DrawableShape::FillAndStrokeState::type ("type"); const Identifier DrawableShape::FillAndStrokeState::colour ("colour"); const Identifier DrawableShape::FillAndStrokeState::colours ("colours"); const Identifier DrawableShape::FillAndStrokeState::fill ("Fill"); const Identifier DrawableShape::FillAndStrokeState::stroke ("Stroke"); const Identifier DrawableShape::FillAndStrokeState::path ("Path"); const Identifier DrawableShape::FillAndStrokeState::jointStyle ("jointStyle"); const Identifier DrawableShape::FillAndStrokeState::capStyle ("capStyle"); const Identifier DrawableShape::FillAndStrokeState::strokeWidth ("strokeWidth"); const Identifier DrawableShape::FillAndStrokeState::gradientPoint1 ("point1"); const Identifier DrawableShape::FillAndStrokeState::gradientPoint2 ("point2"); const Identifier DrawableShape::FillAndStrokeState::gradientPoint3 ("point3"); const Identifier DrawableShape::FillAndStrokeState::radial ("radial"); const Identifier DrawableShape::FillAndStrokeState::imageId ("imageId"); const Identifier DrawableShape::FillAndStrokeState::imageOpacity ("imageOpacity"); DrawableShape::FillAndStrokeState::FillAndStrokeState (const ValueTree& state_) : Drawable::ValueTreeWrapperBase (state_) { } DrawableShape::RelativeFillType DrawableShape::FillAndStrokeState::getFill (const Identifier& fillOrStrokeType, ComponentBuilder::ImageProvider* imageProvider) const { DrawableShape::RelativeFillType f; f.readFrom (state.getChildWithName (fillOrStrokeType), imageProvider); return f; } ValueTree DrawableShape::FillAndStrokeState::getFillState (const Identifier& fillOrStrokeType) { ValueTree v (state.getChildWithName (fillOrStrokeType)); if (v.isValid()) return v; setFill (fillOrStrokeType, FillType (Colours::black), nullptr, nullptr); return getFillState (fillOrStrokeType); } void DrawableShape::FillAndStrokeState::setFill (const Identifier& fillOrStrokeType, const RelativeFillType& newFill, ComponentBuilder::ImageProvider* imageProvider, UndoManager* undoManager) { ValueTree v (state.getOrCreateChildWithName (fillOrStrokeType, undoManager)); newFill.writeTo (v, imageProvider, undoManager); } PathStrokeType DrawableShape::FillAndStrokeState::getStrokeType() const { const String jointStyleString (state [jointStyle].toString()); const String capStyleString (state [capStyle].toString()); return PathStrokeType (state [strokeWidth], jointStyleString == "curved" ? PathStrokeType::curved : (jointStyleString == "bevel" ? PathStrokeType::beveled : PathStrokeType::mitered), capStyleString == "square" ? PathStrokeType::square : (capStyleString == "round" ? PathStrokeType::rounded : PathStrokeType::butt)); } void DrawableShape::FillAndStrokeState::setStrokeType (const PathStrokeType& newStrokeType, UndoManager* undoManager) { state.setProperty (strokeWidth, (double) newStrokeType.getStrokeThickness(), undoManager); state.setProperty (jointStyle, newStrokeType.getJointStyle() == PathStrokeType::mitered ? "miter" : (newStrokeType.getJointStyle() == PathStrokeType::curved ? "curved" : "bevel"), undoManager); state.setProperty (capStyle, newStrokeType.getEndStyle() == PathStrokeType::butt ? "butt" : (newStrokeType.getEndStyle() == PathStrokeType::square ? "square" : "round"), undoManager); } static bool replaceColourInFill (DrawableShape::RelativeFillType& fill, Colour original, Colour replacement) { if (fill.fill.colour == original && fill.fill.isColour()) { fill = FillType (replacement); return true; } return false; } bool DrawableShape::replaceColour (Colour original, Colour replacement) { bool changed1 = replaceColourInFill (mainFill, original, replacement); bool changed2 = replaceColourInFill (strokeFill, original, replacement); return changed1 || changed2; }