/* * This file is part of RawTherapee. * * Copyright (c) 2019 Jean-Christophe FRISCH * * RawTherapee is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * RawTherapee 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. * * You should have received a copy of the GNU General Public License * along with RawTherapee. If not, see . */ #pragma once #ifdef GUIVERSION #include #include #include "editcoordsys.h" #include "rtsurface.h" #include "rtengine/coord.h" #include "rtengine/rt_math.h" class ObjectMOBuffer; /** @file * * The Edit mechanism is designed to let tools (subscribers) communicate with the preview area (provider). * Subscribers will be tools that need to create some graphics in the preview area, to let the user interact * with it in a more user friendly way. * * Do not confuse with _local_ editing, which is another topic implemented in another class. The Edit feature * is also not supported in batch editing from the File Browser. * * Edit tool can be of 2 types: pipette editing and object editing. * * ## Pipette edition * * By using this class, a pipette mechanism can be handled on the preview. * * Each pipette Edit tool must have a unique ID, that will identify them, and which let the ImProcCoordinator * or other mechanism act as appropriated. They are all defined in rtgui/editid.h. A buffer type has to be given * too, to know which kind of buffer to allocate (see EditSubscriber::BufferType). * * Only the first mouse button can be used to manipulate the pipette on the Preview, that's why the developer has * to implement at least the following 4 methods: * - mouseOver * - button1Pressed * - drag1 * - button1Released * * Actually, only curves does use this class, and everything is handled for curve implementer (as much as possible). * See the curve's class documentation to see how to implement the curve's pipette feature. * * ### Event handling * * The mouseOver method is called on each mouse movement, excepted when dragging a point. This method can then access * the pipetteVal array values, which contain the mean of the pixel read in the buffer, or -1 if the cursor is outside * of the image. In this case, EditDataProvider::object is also set to 0 (and 1 if over the image). * * When the user will click on the left mouse button while pressing the CTRL key, button1Pressed will be called. * Setting "dragging" to true (or false) is not required for the pipette type editing. * * The drag1 method will be called on all subsequent mouse move. The pipetteVal[3] array will already be filled with * the mean of the read values under the cursor (actually a fixed square of 8px). If the BufferType is BT_SINGLEPLANE_FLOAT, * only the first array value will be filled. * * Then the button1Released will be called to stop the dragging. * * ## Object edition * * By using this class, objects can be drawn and manipulated on the preview. * * The developer has to handle the buttonPress, buttonRelease, drag and mouseOver methods that he needs. There * are buttonPress, buttonRelease and drag methods dedicated to each mouse button, for better flexibility * (e.g.button2Press, button2Release, drag2 will handle event when mouse button 2 is used first). RT actually * does not handle multiple mouse button event (e.g. button1 + button2), only one at a time. The first button pressed * set the mechanism, all other combined button press are ignored. * * The developer also have to fill 2 display list with object of the Geometry subclass. Each geometric shape * _can_ be used in one or the other, or both list at a time. * * The first list (visibleGeometry) is used to be displayed on the preview. The developer will have to set their state * manually (see Geometry::State), but the display shape, line width and color can be handled automatically, or with * specific values. To be displayed, the F_VISIBLE flag has to be set through the setActive or setVisible methods. * * The second list (mouseOverGeometry) is used in a backbuffer, the color used to draw the shape being the id of the * mouseOverGeometry. As an example, you could create a line to be shown in the preview, but create 2 filled Circle object * to be used as mouseOver detection, one on each end of the line. The association between both shape (visible and mouseOver) * is handled by the developer. To be displayed on this backbuffer, the F_HOVERABLE flag has to be set through the * setActive or setHoverable methods. For overlapping mouse over geometry, the priority is set by the order in the list : * the last item is detected first (think of it like a stack piled up). * * * ### Event handling * * RT will draw in the back buffer all mouseOverGeometry set by the developer once the Edit button is pressed, and handle * the events automatically. * * RT will call the mouseOver method on each mouse movement where no mouse button is pressed. * * On mouse button press over a mouseOverGeometry (that has F_HOVERABLE set), it will call the button press method corresponding * to the button (e.g. button1Pressed for mouse button 1), with the modifier key as parameter. Any other mouse button pressed at * the same time will be ignored. It's up to the developer to decide whether this action is starting a 'drag' or 'pick' action, * by setting the 'action' parameter to the appropriated value. * * If the user sets action to ES_ACTION_DRAGGING, RT will then send drag1 events (to stay with our button 1 pressed example) on each * mouse movement. It's up to the developer of the tool to handle the dragging. The EditProvider class will help you in this by * handling the actual position in various coordinate system and ways. * * When the user will release the mouse button, RT will call the button1Release event (in our example). The developer have * then to set action to ES_ACTION_NONE. * * If the user sets action to ES_ACTION_PICKING, RT will keep in memory the mouseOver object that was selected when pressing the mouse * (e.g. button 1), as well as the modifier keys. * * The element is said to be picked when the mouse button is released over the same mouse over object and with the same active * modifier keys. In this case, the corresponding picked event (e.g. picked1 in our example) and the 'picked' flag will be true. * If any of those condition is false, picked1 will still be be called to terminate the initiated picking action, but 'picked' * will be false. This is necessary because the user may want to update the geometry if the picking is aborted. The developer have * then to set action to ES_ACTION_NONE. * * Picking an on-screen element correspond to single-clicking on it. No double click is supported so far. * * Each of these methods have to returns a boolean value saying that the preview has to be refreshed or not (i.e. the displayed * geometry). * * ## Other general internal implementation notes * * When a tool is being constructed, unique IDs are affected to the EditSubscribers of the Pipette type. * Then the EditorPanel class will ask all ToolPanel to register the 'after' preview ImageArea object as data provider. * The Subscribers have now to provide a toggle button to click on to start the Edit listening. When toggling on, the Subscriber * register itself to the DataProvider, then an event is thrown through the standard ToolPanelListener::panelChanged * method to update the preview with new graphics to be displayed. If a previous Edit button was active, it will be deactivated * (the Edit buttons are mutually exclusive). For the Pipette type, a buffer will be created and has to be populated * by the developer in rtengine's pipeline. The unique pipette ID will be used to know where to fill the buffer, as each pipette * will need different data, corresponding to the state of the image right before the tool that needs pipette values. E.g for * the HSV tool, the Hue and Saturation and Value curves are applied on the current state of the image. That's why the pipette * of the H, S and V curve will share the same data of this "current state", otherwise the read value would be wrong. * * When the Edit process stops, the Subscriber is removed from the DataProvider, so buffers can be freed up. * A new ToolPanelListener::panelChanged event is also thrown to update the preview again, without the tool's * graphical objects. The Edit button is also toggled off (by the user or programmatically). * * It means that each Edit buttons toggled on will start an update of the preview which might or might not create * a new History entry, depending on the ProcEvent used. * */ class RGBColor { double r; double g; double b; public: RGBColor (); explicit RGBColor (double r, double g, double b); explicit RGBColor (char r, char g, char b); void setColor (double r, double g, double b); void setColor (char r, char g, char b); double getR (); double getG (); double getB (); }; class RGBAColor : public RGBColor { double a; public: RGBAColor (); explicit RGBAColor (double r, double g, double b, double a); explicit RGBAColor (char r, char g, char b, char a); void setColor (double r, double g, double b, double a); void setColor (char r, char g, char b, char a); double getA (); }; /// @brief Displayable and MouseOver geometry base class class Geometry { public: /// @brief Graphical state of the element enum State { NORMAL, /// Default state ACTIVE, /// Focused state PRELIGHT, /// Hovered state DRAGGED, /// When being dragged INSENSITIVE /// Displayed but insensitive }; /// @brief Coordinate space and origin of the point enum Datum { IMAGE, /// Image coordinate system with image's top left corner as origin CLICKED_POINT, /// Screen coordinate system with clicked point as origin CURSOR /// Screen coordinate system with actual cursor position as origin }; enum Flags { F_VISIBLE = 1 << 0, /// true if the geometry have to be drawn on the visible layer F_HOVERABLE = 1 << 1, /// true if the geometry have to be drawn on the "mouse over" layer F_AUTO_COLOR = 1 << 2, /// true if the color depend on the state value, not the color field above F_DASHED = 1 << 3, /// true if the geometry have to be drawn as a dash line // (TODO: add a F_LARGE_DASH to have two different dash size ?) }; /// @brief Key point of the image's rectangle that is used to locate the icon copy to the target point: enum DrivenPoint { DP_CENTERCENTER, DP_TOPLEFT, DP_TOPCENTER, DP_TOPRIGHT, DP_CENTERRIGHT, DP_BOTTOMRIGHT, DP_BOTTOMCENTER, DP_BOTTOMLEFT, DP_CENTERLEFT }; protected: static const std::vector dash; RGBColor innerLineColor; RGBColor outerLineColor; short flags; // Set MO Channel color according to Edit Widget id and MO Channel mode static void setMOChannelColor (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, unsigned short id); public: float innerLineWidth; // ...outerLineWidth = innerLineWidth+2 Datum datum; State state; // set by the Subscriber float opacity; // Percentage of opacity Geometry (); virtual ~Geometry() {} void setInnerLineColor (double r, double g, double b); void setInnerLineColor (char r, char g, char b); RGBColor getInnerLineColor (); void setOuterLineColor (double r, double g, double b); void setOuterLineColor (char r, char g, char b); RGBColor getOuterLineColor (); double getOuterLineWidth (); double getMouseOverLineWidth (); void setAutoColor (bool aColor); bool isVisible (); void setVisible (bool visible); bool isHoverable (); void setHoverable (bool visible); bool isDashed (); void setDashed (bool dashed); // setActive will enable/disable the visible and hoverable flags in one shot! void setActive (bool active); virtual void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) = 0; virtual void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) = 0; virtual void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) = 0; }; class Circle : public Geometry { public: rtengine::Coord center; int radius; bool filled; bool radiusInImageSpace; /// If true, the radius depend on the image scale; if false, it is a fixed 'screen' size Circle (); Circle (rtengine::Coord& center, int radius, bool filled = false, bool radiusInImageSpace = false); Circle (int centerX, int centerY, int radius, bool filled = false, bool radiusInImageSpace = false); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class Line : public Geometry { public: rtengine::Coord begin; rtengine::Coord end; Line (); Line (const rtengine::Coord& begin, const rtengine::Coord& end); Line (int beginX, int beginY, int endX, int endY); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class Polyline : public Geometry { public: std::vector points; bool filled; Polyline (); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class EditRectangle : public Geometry // New class name to avoid conflict elsewhere (exiv2), would be nicer to put in namespace? { public: rtengine::Coord topLeft; rtengine::Coord bottomRight; bool filled; EditRectangle (); void setXYWH(int left, int top, int width, int height); void setXYXY(int left, int top, int right, int bottom); void setXYWH(rtengine::Coord topLeft, rtengine::Coord widthHeight); void setXYXY(rtengine::Coord topLeft, rtengine::Coord bottomRight); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class Ellipse : public Geometry { public: rtengine::Coord center; int radYT; // Ellipse half-radius for top y-axis int radY; // Ellipse half-radius for bottom y-axis int radXL; // Ellipse half-radius for left x-axis int radX; // Ellipse half-radius for right x-axis bool filled; bool radiusInImageSpace; /// If true, the radius depend on the image scale; if false, it is a fixed 'screen' size Ellipse (); Ellipse (const rtengine::Coord& center, int radYT, int radY, int radXL, int radX, bool filled = false, bool radiusInImageSpace = false); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class OPIcon : public Geometry // OP stands for "On Preview" { private: std::shared_ptr normalImg; std::shared_ptr prelightImg; std::shared_ptr activeImg; std::shared_ptr draggedImg; std::shared_ptr insensitiveImg; void drawImage (std::shared_ptr &img, Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem); void drawMOImage (std::shared_ptr &img, Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem); void drivenPointToRectangle(const rtengine::Coord &pos, rtengine::Coord &topLeft, rtengine::Coord &bottomRight, int W, int H); public: DrivenPoint drivenPoint; rtengine::Coord position; OPIcon (const std::shared_ptr &normal, const std::shared_ptr &active, const std::shared_ptr &prelight = nullptr, const std::shared_ptr &dragged = nullptr, const std::shared_ptr &insensitive = nullptr, DrivenPoint drivenPoint = DP_CENTERCENTER); OPIcon (Glib::ustring normalImage, Glib::ustring activeImage, Glib::ustring prelightImage = "", Glib::ustring draggedImage = "", Glib::ustring insensitiveImage = "", DrivenPoint drivenPoint = DP_CENTERCENTER); const std::shared_ptr getNormalImg(); const std::shared_ptr getPrelightImg(); const std::shared_ptr getActiveImg(); const std::shared_ptr getDraggedImg(); const std::shared_ptr getInsensitiveImg(); void drawOuterGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawInnerGeometry (Cairo::RefPtr &cr, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; void drawToMOChannel (Cairo::RefPtr &cr, unsigned short id, ObjectMOBuffer *objectBuffer, EditCoordSystem &coordSystem) override; }; class OPAdjuster : public Geometry // OP stands for "On Preview" { }; inline void RGBColor::setColor (double r, double g, double b) { this->r = r; this->g = g; this->b = b; } inline void RGBColor::setColor (char r, char g, char b) { this->r = double (r) / 255.; this->g = double (g) / 255.; this->b = double (b) / 255.; } inline double RGBColor::getR () { return r; } inline double RGBColor::getG () { return g; } inline double RGBColor::getB () { return b; } inline void RGBAColor::setColor (double r, double g, double b, double a) { RGBColor::setColor (r, g, b); this->a = a; } inline void RGBAColor::setColor (char r, char g, char b, char a) { RGBColor::setColor (r, g, b); this->a = double (a) / 255.; } inline double RGBAColor::getA () { return a; } inline void Geometry::setInnerLineColor (double r, double g, double b) { innerLineColor.setColor (r, g, b); flags &= ~F_AUTO_COLOR; } inline void Geometry::setInnerLineColor (char r, char g, char b) { innerLineColor.setColor (r, g, b); flags &= ~F_AUTO_COLOR; } inline void Geometry::setOuterLineColor (double r, double g, double b) { outerLineColor.setColor (r, g, b); flags &= ~F_AUTO_COLOR; } inline double Geometry::getOuterLineWidth () { return double (innerLineWidth) + 2.; } inline void Geometry::setOuterLineColor (char r, char g, char b) { outerLineColor.setColor (r, g, b); flags &= ~F_AUTO_COLOR; } inline double Geometry::getMouseOverLineWidth () { return rtengine::max(double(innerLineWidth), 1.) + 2.; } inline void Geometry::setAutoColor (bool aColor) { if (aColor) { flags |= F_AUTO_COLOR; } else { flags &= ~F_AUTO_COLOR; } } inline bool Geometry::isVisible () { return flags & F_VISIBLE; } inline void Geometry::setVisible (bool visible) { if (visible) { flags |= F_VISIBLE; } else { flags &= ~F_VISIBLE; } } inline bool Geometry::isHoverable () { return flags & F_HOVERABLE; } inline void Geometry::setHoverable (bool hoverable) { if (hoverable) { flags |= F_HOVERABLE; } else { flags &= ~F_HOVERABLE; } } inline bool Geometry::isDashed () { return flags & F_DASHED; } inline void Geometry::setDashed (bool dashed) { if (dashed) { flags |= F_DASHED; } else { flags &= ~F_DASHED; } } inline void Geometry::setActive (bool active) { if (active) { flags |= (F_VISIBLE | F_HOVERABLE); } else { flags &= ~(F_VISIBLE | F_HOVERABLE); } } inline Geometry::Geometry () : innerLineColor (char (255), char (255), char (255)), outerLineColor ( char (0), char (0), char (0)), flags ( F_VISIBLE | F_HOVERABLE | F_AUTO_COLOR), innerLineWidth (1.5f), datum ( IMAGE), state (NORMAL), opacity(100.) { } inline RGBAColor::RGBAColor () : RGBColor (0., 0., 0.), a (0.) { } inline RGBColor::RGBColor () : r (0.), g (0.), b (0.) { } inline RGBColor::RGBColor (double r, double g, double b) : r (r), g (g), b (b) { } inline RGBColor::RGBColor (char r, char g, char b) : r (double (r) / 255.), g (double (g) / 255.), b (double (b) / 255.) { } inline RGBAColor::RGBAColor (double r, double g, double b, double a) : RGBColor (r, g, b), a (a) { } inline RGBAColor::RGBAColor (char r, char g, char b, char a) : RGBColor (r, g, b), a (double (a) / 255.) { } inline Circle::Circle () : center (100, 100), radius (10), filled (false), radiusInImageSpace ( false) { } inline EditRectangle::EditRectangle () : topLeft (0, 0), bottomRight (10, 10), filled (false) { } inline Polyline::Polyline () : filled (false) { } inline Line::Line () : begin (10, 10), end (100, 100) { } inline Ellipse::Ellipse () : center (100, 100), radYT (5), radY (5), radXL (10), radX (10), filled (false), radiusInImageSpace (false) { } inline Circle::Circle (rtengine::Coord& center, int radius, bool filled, bool radiusInImageSpace) : center (center), radius (radius), filled (filled), radiusInImageSpace ( radiusInImageSpace) { } inline Circle::Circle (int centerX, int centerY, int radius, bool filled, bool radiusInImageSpace) : center (centerX, centerY), radius (radius), filled (filled), radiusInImageSpace ( radiusInImageSpace) { } inline Line::Line (const rtengine::Coord& begin, const rtengine::Coord& end) : begin (begin), end (end) { } inline Line::Line (int beginX, int beginY, int endX, int endY) : begin (beginX, beginY), end (endX, endY) { } inline Ellipse::Ellipse (const rtengine::Coord& center, int radYT, int radY, int radXL, int radX, bool filled, bool radiusInImageSpace) : center (center), radYT (radYT), radY (radY), radXL (radXL), radX (radX), filled (filled), radiusInImageSpace (radiusInImageSpace) { } #endif