/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: https://getgreenshot.org/ * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot * * This program 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 1 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see . */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Runtime.Serialization; using Greenshot.Base.IniFile; using Greenshot.Base.Interfaces; using Greenshot.Base.Interfaces.Drawing; using Greenshot.Base.Interfaces.Drawing.Adorners; using Greenshot.Editor.Configuration; using Greenshot.Editor.Drawing.Adorners; using Greenshot.Editor.Drawing.Fields; using Greenshot.Editor.Drawing.Filters; using Greenshot.Editor.Helpers; using Greenshot.Editor.Memento; using log4net; namespace Greenshot.Editor.Drawing { /// /// represents a rectangle, ellipse, label or whatever. Can contain filters, too. /// serializable for clipboard support /// Subclasses should fulfill INotifyPropertyChanged contract, i.e. call /// OnPropertyChanged whenever a public property has been changed. /// [Serializable] public abstract class DrawableContainer : AbstractFieldHolderWithChildren, IDrawableContainer { private static readonly ILog LOG = LogManager.GetLogger(typeof(DrawableContainer)); protected static readonly EditorConfiguration EditorConfig = IniConfig.GetIniSection(); private const int M11 = 0; private const int M22 = 3; [OnDeserialized] private void OnDeserializedInit(StreamingContext context) { _adorners = new List(); OnDeserialized(context); } /// /// Override to implement your own deserialization logic, like initializing properties which are not serialized /// /// protected virtual void OnDeserialized(StreamingContext streamingContext) { } protected EditStatus _defaultEditMode = EditStatus.DRAWING; public EditStatus DefaultEditMode { get { return _defaultEditMode; } } /// /// The public accessible Dispose /// Will call the GarbageCollector to SuppressFinalize, preventing being cleaned twice /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!disposing) { return; } _parent?.FieldAggregator?.UnbindElement(this); } ~DrawableContainer() { Dispose(false); } [NonSerialized] private PropertyChangedEventHandler _propertyChanged; public event PropertyChangedEventHandler PropertyChanged { add => _propertyChanged += value; remove => _propertyChanged -= value; } public IList Filters { get { List ret = new List(); foreach (IFieldHolder c in Children) { if (c is IFilter) { ret.Add(c as IFilter); } } return ret; } } [NonSerialized] internal ISurface _parent; public ISurface Parent { get => _parent; set => SwitchParent(value); } protected Surface InternalParent { get => (Surface)_parent; } [NonSerialized] private TargetAdorner _targetAdorner; public TargetAdorner TargetAdorner => _targetAdorner; [NonSerialized] private bool _selected; public bool Selected { get => _selected; set { _selected = value; OnPropertyChanged("Selected"); } } [NonSerialized] private EditStatus _status = EditStatus.UNDRAWN; public EditStatus Status { get => _status; set => _status = value; } private int left; public int Left { get => left; set { if (value == left) { return; } left = value; } } private int top; public int Top { get => top; set { if (value == top) { return; } top = value; } } private int width; public int Width { get => width; set { if (value == width) { return; } width = value; } } private int height; public int Height { get => height; set { if (value == height) { return; } height = value; } } public Point Location { get => new Point(left, top); set { left = value.X; top = value.Y; } } public Size Size { get => new Size(width, height); set { width = value.Width; height = value.Height; } } /// /// List of available Adorners /// [NonSerialized] private IList _adorners = new List(); public IList Adorners => _adorners; [NonSerialized] // will store current bounds of this DrawableContainer before starting a resize protected Rectangle _boundsBeforeResize = Rectangle.Empty; [NonSerialized] // "workbench" rectangle - used for calculating bounds during resizing (to be applied to this DrawableContainer afterwards) protected RectangleF _boundsAfterResize = RectangleF.Empty; public Rectangle Bounds { get => GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); set { Left = Round(value.Left); Top = Round(value.Top); Width = Round(value.Width); Height = Round(value.Height); } } public virtual void ApplyBounds(RectangleF newBounds) { Left = Round(newBounds.Left); Top = Round(newBounds.Top); Width = Round(newBounds.Width); Height = Round(newBounds.Height); } public DrawableContainer(ISurface parent) { InitializeFields(); _parent = parent; } public void Add(IFilter filter) { AddChild(filter); } public void Remove(IFilter filter) { RemoveChild(filter); } private static int Round(float f) { if (float.IsPositiveInfinity(f) || f > int.MaxValue / 2) return int.MaxValue / 2; if (float.IsNegativeInfinity(f) || f < int.MinValue / 2) return int.MinValue / 2; return (int) Math.Round(f); } private bool accountForShadowChange; public virtual Rectangle DrawingBounds { get { foreach (IFilter filter in Filters) { if (filter.Invert) { return new Rectangle(Point.Empty, _parent.Image.Size); } } // Take a base safety margin int lineThickness = 5; // add adorner size lineThickness += Adorners.Max(adorner => Math.Max(adorner.Bounds.Width, adorner.Bounds.Height)); if (HasField(FieldType.LINE_THICKNESS)) { lineThickness += GetFieldValueAsInt(FieldType.LINE_THICKNESS); } int offset = lineThickness / 2; int shadow = 0; if (accountForShadowChange || (HasField(FieldType.SHADOW) && GetFieldValueAsBool(FieldType.SHADOW))) { accountForShadowChange = false; shadow += 10; } return new Rectangle(Bounds.Left - offset, Bounds.Top - offset, Bounds.Width + lineThickness + shadow, Bounds.Height + lineThickness + shadow); } } public virtual void Invalidate() { if (Status != EditStatus.UNDRAWN) { _parent?.InvalidateElements(DrawingBounds); } } public virtual bool InitContent() { return true; } public virtual void OnDoubleClick() { } /// /// Initialize a target gripper /// protected void InitAdorner(Color gripperColor, Point location) { _targetAdorner = new TargetAdorner(this, location); Adorners.Add(_targetAdorner); } /// /// Create the default adorners for a rectangle based container /// protected void CreateDefaultAdorners() { if (Adorners.Count > 0) { LOG.Warn("Adorners are already defined!"); } // Create the GripperAdorners Adorners.Add(new ResizeAdorner(this, Positions.TopLeft)); Adorners.Add(new ResizeAdorner(this, Positions.TopCenter)); Adorners.Add(new ResizeAdorner(this, Positions.TopRight)); Adorners.Add(new ResizeAdorner(this, Positions.BottomLeft)); Adorners.Add(new ResizeAdorner(this, Positions.BottomCenter)); Adorners.Add(new ResizeAdorner(this, Positions.BottomRight)); Adorners.Add(new ResizeAdorner(this, Positions.MiddleLeft)); Adorners.Add(new ResizeAdorner(this, Positions.MiddleRight)); } public bool HasFilters => Filters.Count > 0; public abstract void Draw(Graphics graphics, RenderMode renderMode); public virtual void DrawContent(Graphics graphics, Bitmap bmp, RenderMode renderMode, Rectangle clipRectangle) { if (Children.Count > 0) { if (Status != EditStatus.IDLE) { DrawSelectionBorder(graphics, Bounds); } else { if (clipRectangle.Width != 0 && clipRectangle.Height != 0) { foreach (IFilter filter in Filters) { if (filter.Invert) { filter.Apply(graphics, bmp, Bounds, renderMode); } else { Rectangle drawingRect = new Rectangle(Bounds.Location, Bounds.Size); drawingRect.Intersect(clipRectangle); if (filter is MagnifierFilter) { // quick&dirty bugfix, because MagnifierFilter behaves differently when drawn only partially // what we should actually do to resolve this is add a better magnifier which is not that special filter.Apply(graphics, bmp, Bounds, renderMode); } else { filter.Apply(graphics, bmp, drawingRect, renderMode); } } } } } } Draw(graphics, renderMode); } /// /// Adjust UI elements to the supplied DPI settings /// /// uint with dpi value public void AdjustToDpi(uint dpi) { foreach (var adorner in Adorners) { adorner.AdjustToDpi(dpi); } } public virtual bool Contains(int x, int y) { return Bounds.Contains(x, y); } public virtual bool ClickableAt(int x, int y) { Rectangle r = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); r.Inflate(5, 5); return r.Contains(x, y); } protected void DrawSelectionBorder(Graphics g, Rectangle rect) { using Pen pen = new Pen(Color.MediumSeaGreen) { DashPattern = new float[] { 1, 2 }, Width = 1 }; g.DrawRectangle(pen, rect); } /// public virtual bool IsUndoable => true; /// /// Make a following bounds change on this drawablecontainer undoable! /// /// true means allow the moves to be merged public virtual void MakeBoundsChangeUndoable(bool allowMerge) { if (!IsUndoable) { return; } _parent?.MakeUndoable(new DrawableContainerBoundsChangeMemento(this), allowMerge); } public void MoveBy(int dx, int dy) { Left += dx; Top += dy; } /// /// A handler for the MouseDown, used if you don't want the surface to handle this for you /// /// current mouse x /// current mouse y /// true if the event is handled, false if the surface needs to handle it public virtual bool HandleMouseDown(int x, int y) { Left = _boundsBeforeResize.X = x; Top = _boundsBeforeResize.Y = y; return true; } /// /// A handler for the MouseMove, used if you don't want the surface to handle this for you /// /// current mouse x /// current mouse y /// true if the event is handled, false if the surface needs to handle it public virtual bool HandleMouseMove(int x, int y) { Invalidate(); // reset "workbench" rectangle to current bounds _boundsAfterResize.X = _boundsBeforeResize.Left; _boundsAfterResize.Y = _boundsBeforeResize.Top; _boundsAfterResize.Width = x - _boundsAfterResize.Left; _boundsAfterResize.Height = y - _boundsAfterResize.Top; ScaleHelper.Scale(_boundsBeforeResize, x, y, ref _boundsAfterResize, GetAngleRoundProcessor()); // apply scaled bounds to this DrawableContainer ApplyBounds(_boundsAfterResize); Invalidate(); return true; } /// /// A handler for the MouseUp /// /// current mouse x /// current mouse y public virtual void HandleMouseUp(int x, int y) { } protected virtual void SwitchParent(ISurface newParent) { if (newParent == Parent) { return; } _parent?.FieldAggregator?.UnbindElement(this); _parent = newParent; foreach (IFilter filter in Filters) { filter.Parent = this; } } protected void OnPropertyChanged(string propertyName) { if (_propertyChanged == null) return; _propertyChanged(this, new PropertyChangedEventArgs(propertyName)); Invalidate(); } /// /// This method will be called before a field is changes. /// Using this makes it possible to invalidate the object as is before changing. /// /// The field to be changed /// The new value public virtual void BeforeFieldChange(IField fieldToBeChanged, object newValue) { if (IsUndoable) { _parent?.MakeUndoable(new ChangeFieldHolderMemento(this, fieldToBeChanged), true); } Invalidate(); } /// /// Handle the field changed event, this should invalidate the correct bounds (e.g. when shadow comes or goes more pixels!) /// /// /// public void HandleFieldChanged(object sender, FieldChangedEventArgs e) { LOG.DebugFormat("Field {0} changed", e.Field.FieldType); if (Equals(e.Field.FieldType, FieldType.SHADOW)) { accountForShadowChange = true; } } /// /// Retrieve the Y scale from the matrix /// /// /// public static float CalculateScaleY(Matrix matrix) { return matrix.Elements[M22]; } /// /// Retrieve the X scale from the matrix /// /// /// public static float CalculateScaleX(Matrix matrix) { return matrix.Elements[M11]; } /// /// Retrieve the rotation angle from the matrix /// /// /// public static int CalculateAngle(Matrix matrix) { const int M11 = 0; const int M21 = 2; var radians = Math.Atan2(matrix.Elements[M21], matrix.Elements[M11]); return (int) -Math.Round(radians * 180 / Math.PI); } /// /// This method is called on a DrawableContainers when: /// 1) The capture on the surface is modified in such a way, that the elements would not be placed correctly. /// 2) Currently not implemented: an element needs to be moved, scaled or rotated. /// This basis implementation makes sure the coordinates of the element, including the TargetGripper, is correctly rotated/scaled/translated. /// But this implementation doesn't take care of any changes to the content!! /// /// public virtual void Transform(Matrix matrix) { if (matrix == null) { return; } Point topLeft = new Point(Left, Top); Point bottomRight = new Point(Left + Width, Top + Height); Point[] points = new[] { topLeft, bottomRight }; matrix.TransformPoints(points); Left = points[0].X; Top = points[0].Y; Width = points[1].X - points[0].X; Height = points[1].Y - points[0].Y; } protected virtual ScaleHelper.IDoubleProcessor GetAngleRoundProcessor() { return ScaleHelper.ShapeAngleRoundBehavior.Instance; } public virtual bool HasContextMenu => true; public virtual bool HasDefaultSize => false; public virtual Size DefaultSize => throw new NotSupportedException("Object doesn't have a default size"); /// /// Allows to override the initializing of the fields, so we can actually have our own defaults /// protected virtual void InitializeFields() { } } }