From dd21b1407d04e112a631320d784557af4fb47977 Mon Sep 17 00:00:00 2001 From: Nathan Brown Date: Wed, 28 May 2025 23:09:42 -0700 Subject: [PATCH 1/2] Use existing value +/- correction to calculate new values for clip rectangle --- src/Greenshot.Editor/Drawing/Surface.cs | 5522 ++++++++++++----------- 1 file changed, 2763 insertions(+), 2759 deletions(-) diff --git a/src/Greenshot.Editor/Drawing/Surface.cs b/src/Greenshot.Editor/Drawing/Surface.cs index 6d47a67a6..cbdfc2733 100644 --- a/src/Greenshot.Editor/Drawing/Surface.cs +++ b/src/Greenshot.Editor/Drawing/Surface.cs @@ -1,2765 +1,2769 @@ -/* - * 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.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Formatters.Binary; +/* + * 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.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; using System.ServiceModel.Security; -using System.Windows.Forms; -using Dapplo.Windows.Common.Extensions; -using Dapplo.Windows.Common.Structs; -using Greenshot.Base.Controls; -using Greenshot.Base.Core; -using Greenshot.Base.Effects; -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.Fields; +using System.Windows.Forms; +using Dapplo.Windows.Common.Extensions; +using Dapplo.Windows.Common.Structs; +using Greenshot.Base.Controls; +using Greenshot.Base.Core; +using Greenshot.Base.Effects; +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.Fields; using Greenshot.Editor.Helpers; -using Greenshot.Editor.Memento; -using log4net; - -namespace Greenshot.Editor.Drawing -{ - /// - /// Description of Surface. - /// - public sealed class Surface : Control, ISurface, INotifyPropertyChanged - { - private static readonly ILog LOG = LogManager.GetLogger(typeof(Surface)); - private static readonly CoreConfiguration conf = IniConfig.GetIniSection(); - - // Property to identify the Surface ID - private Guid _uniqueId = Guid.NewGuid(); - - /// - /// This value is used to start counting the step labels - /// - private int _counterStart = 1; - - /// - /// The GUID of the surface - /// - public Guid ID - { - get => _uniqueId; - set => _uniqueId = value; - } - - /// - /// Event handlers (do not serialize!) - /// - [NonSerialized] private PropertyChangedEventHandler _propertyChanged; - - public event PropertyChangedEventHandler PropertyChanged - { - add => _propertyChanged += value; - remove => _propertyChanged -= value; - } - - [NonSerialized] private SurfaceElementEventHandler _movingElementChanged; - - public event SurfaceElementEventHandler MovingElementChanged - { - add => _movingElementChanged += value; - remove => _movingElementChanged -= value; - } - - [NonSerialized] private SurfaceDrawingModeEventHandler _drawingModeChanged; - - public event SurfaceDrawingModeEventHandler DrawingModeChanged - { - add => _drawingModeChanged += value; - remove => _drawingModeChanged -= value; - } - - [NonSerialized] private SurfaceSizeChangeEventHandler _surfaceSizeChanged; - - public event SurfaceSizeChangeEventHandler SurfaceSizeChanged - { - add => _surfaceSizeChanged += value; - remove => _surfaceSizeChanged -= value; - } - - [NonSerialized] private SurfaceMessageEventHandler _surfaceMessage; - - public event SurfaceMessageEventHandler SurfaceMessage - { - add => _surfaceMessage += value; - remove => _surfaceMessage -= value; - } - - [NonSerialized] private SurfaceForegroundColorEventHandler _foregroundColorChanged; - - public event SurfaceForegroundColorEventHandler ForegroundColorChanged - { - add => _foregroundColorChanged += value; - remove => _foregroundColorChanged -= value; - } - - [NonSerialized] private SurfaceBackgroundColorEventHandler _backgroundColorChanged; - - public event SurfaceBackgroundColorEventHandler BackgroundColorChanged - { - add => _backgroundColorChanged += value; - remove => _backgroundColorChanged -= value; - } - - [NonSerialized] private SurfaceLineThicknessEventHandler _lineThicknessChanged; - - public event SurfaceLineThicknessEventHandler LineThicknessChanged - { - add => _lineThicknessChanged += value; - remove => _lineThicknessChanged -= value; - } - - [NonSerialized] private SurfaceShadowEventHandler _shadowChanged; - - public event SurfaceShadowEventHandler ShadowChanged - { - add => _shadowChanged += value; - remove => _shadowChanged -= value; - } - - - [NonSerialized] private int _currentDpi = 96; - /// - /// The most recent DPI value that was used - /// - public int CurrentDpi - { - get => _currentDpi; - set => _currentDpi = value; - } - - /// - /// inUndoRedo makes sure we don't undo/redo while in a undo/redo action - /// - [NonSerialized] private bool _inUndoRedo; - - /// - /// Make only one surface move cycle undoable, see SurfaceMouseMove - /// - [NonSerialized] private bool _isSurfaceMoveMadeUndoable; - - /// - /// Undo/Redo stacks, should not be serialized as the file would be way to big - /// - [NonSerialized] private readonly Stack _undoStack = new Stack(); - - [NonSerialized] private readonly Stack _redoStack = new Stack(); - - /// - /// Last save location, do not serialize! - /// - [NonSerialized] private string _lastSaveFullPath; - - /// - /// current drawing mode, do not serialize! - /// - [NonSerialized] private DrawingModes _drawingMode = DrawingModes.None; - - /// - /// the keys-locked flag helps with focus issues - /// - [NonSerialized] private bool _keysLocked; - - /// - /// Location of the mouse-down (it "starts" here), do not serialize - /// - [NonSerialized] private Point _mouseStart = Point.Empty; - - /// - /// are we in a mouse down, do not serialize - /// - [NonSerialized] private bool _mouseDown; - - /// - /// The selected element for the mouse down, do not serialize - /// - [NonSerialized] private IDrawableContainer _mouseDownElement; - - /// - /// all selected elements, do not serialize - /// - [NonSerialized] private readonly IDrawableContainerList selectedElements; - - /// - /// the element we are drawing with, do not serialize - /// - [NonSerialized] private IDrawableContainer _drawingElement; - - /// - /// the element we want to draw with (not yet drawn), do not serialize - /// - [NonSerialized] private IDrawableContainer _undrawnElement; - - /// - /// the crop container, when cropping this is set, do not serialize - /// - [NonSerialized] private IDrawableContainer _cropContainer; - - /// - /// the brush which is used for transparent backgrounds, set by the editor, do not serialize - /// - [NonSerialized] private Brush _transparencyBackgroundBrush; - - /// - /// The buffer is only for drawing on it when using filters (to supply access) - /// This saves a lot of "create new bitmap" commands - /// Should not be serialized, as it's generated. - /// The actual bitmap is in the paintbox... - /// TODO: Check if this buffer is still needed! - /// - [NonSerialized] private Bitmap _buffer; - - /// - /// all stepLabels for the surface, needed with serialization - /// - private readonly List _stepLabels = new List(); - - public void AddStepLabel(StepLabelContainer stepLabel) - { - if (!_stepLabels.Contains(stepLabel)) - { - _stepLabels.Add(stepLabel); - } - } - - public void RemoveStepLabel(StepLabelContainer stepLabel) - { - _stepLabels.Remove(stepLabel); - } - - /// - /// The start value of the counter objects - /// - public int CounterStart - { - get => _counterStart; - set - { - if (_counterStart == value) - { - return; - } - - _counterStart = value; - Invalidate(); - _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CounterStart))); - } - } - - /// - /// Count all the VISIBLE steplabels in the surface, up to the supplied one - /// - /// can be null, if not the counting stops here - /// number of steplabels before the supplied container - public int CountStepLabels(IDrawableContainer stopAtContainer) - { - int number = CounterStart; - foreach (var possibleThis in _stepLabels) - { - if (possibleThis.Equals(stopAtContainer)) - { - break; - } - - if (IsOnSurface(possibleThis)) - { - number++; - } - } - - return number; - } - - /// - /// all elements on the surface, needed with serialization - /// - private readonly IDrawableContainerList _elements; - - /// - /// all elements on the surface, needed with serialization - /// - private IFieldAggregator _fieldAggregator; - - /// - /// the cursor container, needed with serialization as we need a direct acces to it. - /// - private IDrawableContainer _cursorContainer; - - /// - /// the modified flag specifies if the surface has had modifications after the last export. - /// Initial state is modified, as "it's not saved" - /// After serialization this should actually be "false" (the surface came from a stream) - /// For now we just serialize it... - /// - private bool _modified = true; - - /// - /// The image is the actual captured image, needed with serialization - /// - private Image _image; - - public Image Image - { - get => _image; - set - { - _image = value; - UpdateSize(); - } - } - - [NonSerialized] private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); - [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); - [NonSerialized] private Fraction _zoomFactor = Fraction.Identity; - - public Fraction ZoomFactor - { - get => _zoomFactor; - set - { - _zoomFactor = value; - var inverse = _zoomFactor.Inverse(); - _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); - _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); - UpdateSize(); - } - } - - - /// - /// Sets the surface size as zoomed image size. - /// - private void UpdateSize() - { - var size = _image.Size; - Size = new Size((int) (size.Width * _zoomFactor), (int) (size.Height * _zoomFactor)); - } - - /// - /// The field aggregator is that which is used to have access to all the fields inside the currently selected elements. - /// e.g. used to decided if and which line thickness is shown when multiple elements are selected. - /// - public IFieldAggregator FieldAggregator - { - get => _fieldAggregator; - set => _fieldAggregator = value; - } - - /// - /// The cursor container has it's own accessor so we can find and remove this (when needed) - /// - public IDrawableContainer CursorContainer => _cursorContainer; - - /// - /// A simple getter to ask if this surface has a cursor - /// - public bool HasCursor => _cursorContainer != null; - - /// - /// A simple helper method to remove the cursor from the surface - /// - public void RemoveCursor() - { - RemoveElement(_cursorContainer); - _cursorContainer = null; - } - - /// - /// The brush which is used to draw the transparent background - /// - public Brush TransparencyBackgroundBrush - { - get => _transparencyBackgroundBrush; - set => _transparencyBackgroundBrush = value; - } - - /// - /// Are the keys on this surface locked? - /// - public bool KeysLocked - { - get => _keysLocked; - set => _keysLocked = value; - } - - /// - /// Is this surface modified? This is only true if the surface has not been exported. - /// - public bool Modified - { - get => _modified; - set => _modified = value; - } - - /// - /// The DrawingMode property specifies the mode for drawing, more or less the element type. - /// - public DrawingModes DrawingMode - { - get => _drawingMode; - set - { - _drawingMode = value; - if (_drawingModeChanged != null) - { - SurfaceDrawingModeEventArgs eventArgs = new SurfaceDrawingModeEventArgs - { - DrawingMode = _drawingMode - }; - _drawingModeChanged.Invoke(this, eventArgs); - } - - DeselectAllElements(); - CreateUndrawnElement(); - } - } - - /// - /// Property for accessing the last save "full" path - /// - public string LastSaveFullPath - { - get => _lastSaveFullPath; - set => _lastSaveFullPath = value; - } - - /// - /// Property for accessing the URL to which the surface was recently uploaded - /// - public string UploadUrl { get; set; } - - /// - /// Property for accessing the capture details - /// - public ICaptureDetails CaptureDetails { get; set; } - - /// - /// Adjust UI elements to the supplied DPI settings - /// - /// - public void AdjustToDpi(int dpi) - { - CurrentDpi = dpi; - foreach (var element in this._elements) - { - element.AdjustToDpi(dpi); - } - } - - /// - /// Base Surface constructor - /// - public Surface() - { - _fieldAggregator = new FieldAggregator(this); - _elements = new DrawableContainerList(_uniqueId); - selectedElements = new DrawableContainerList(_uniqueId); - LOG.Debug("Creating surface!"); - MouseDown += SurfaceMouseDown; - MouseUp += SurfaceMouseUp; - MouseMove += SurfaceMouseMove; - MouseDoubleClick += SurfaceDoubleClick; - Paint += SurfacePaint; - AllowDrop = true; - DragDrop += OnDragDrop; - DragEnter += OnDragEnter; - // bind selected & elements to this, otherwise they can't inform of modifications - selectedElements.Parent = this; - _elements.Parent = this; - // Make sure we are visible - Visible = true; - TabStop = false; - // Enable double buffering - DoubleBuffered = true; - SetStyle( - ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.ContainerControl | ControlStyles.OptimizedDoubleBuffer | - ControlStyles.SupportsTransparentBackColor, true); - } - - /// - /// Private method, the current image is disposed the new one will stay. - /// - /// The new image - /// true if the old image needs to be disposed, when using undo this should not be true!! - private void SetImage(Image newImage, bool dispose) - { - // Dispose - if (_image != null && dispose) - { - _image.Dispose(); - } - - // Set new values - Image = newImage; - - _modified = true; - } - - /// - /// Surface constructor with an image - /// - /// - public Surface(Image newImage) : this() - { - LOG.DebugFormat("Got image with dimensions {0} and format {1}", newImage.Size, newImage.PixelFormat); - SetImage(newImage, true); - } - - /// - /// Surface contructor with a capture - /// - /// - public Surface(ICapture capture) : this(capture.Image) - { - // check if cursor is captured, and visible - if (capture.Cursor != null && capture.CursorVisible) - { - var cursorRect = new NativeRect(capture.CursorLocation, capture.Cursor.Size); - var captureRect = new NativeRect(NativePoint.Empty, capture.Image.Size); - // check if cursor is on the capture, otherwise we leave it out. - if (cursorRect.IntersectsWith(captureRect)) - { - _cursorContainer = AddIconContainer(capture.Cursor, capture.CursorLocation.X, capture.CursorLocation.Y); - SelectElement(_cursorContainer); - } - } - - // Make sure the image is NOT disposed, we took the reference directly into ourselves - ((Capture) capture).NullImage(); - - CaptureDetails = capture.CaptureDetails; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - LOG.Debug("Disposing surface!"); - if (_buffer != null) - { - _buffer.Dispose(); - _buffer = null; - } - - if (_transparencyBackgroundBrush != null) - { - _transparencyBackgroundBrush.Dispose(); - _transparencyBackgroundBrush = null; - } - - // Cleanup undo/redo stacks - while (_undoStack != null && _undoStack.Count > 0) - { - _undoStack.Pop().Dispose(); - } - - while (_redoStack != null && _redoStack.Count > 0) - { - _redoStack.Pop().Dispose(); - } - - foreach (IDrawableContainer container in _elements) - { - container.Dispose(); - } - - if (_undrawnElement != null) - { - _undrawnElement.Dispose(); - _undrawnElement = null; - } - - if (_cropContainer != null) - { - _cropContainer.Dispose(); - _cropContainer = null; - } - } - - base.Dispose(disposing); - } - - /// - /// Undo the last action - /// - public void Undo() - { - if (_undoStack.Count > 0) - { - _inUndoRedo = true; - IMemento top = _undoStack.Pop(); - _redoStack.Push(top.Restore()); - _inUndoRedo = false; - } - } - - /// - /// Undo an undo (=redo) - /// - public void Redo() - { - if (_redoStack.Count > 0) - { - _inUndoRedo = true; - IMemento top = _redoStack.Pop(); - _undoStack.Push(top.Restore()); - _inUndoRedo = false; - } - } - - /// - /// Returns if the surface can do a undo - /// - public bool CanUndo => _undoStack.Count > 0; - - /// - /// Returns if the surface can do a redo - /// - public bool CanRedo => _redoStack.Count > 0; - - /// - /// Get the language key for the undo action - /// - public LangKey UndoActionLanguageKey => LangKey.none; - - /// - /// Get the language key for redo action - /// - public LangKey RedoActionLanguageKey => LangKey.none; - - /// - /// Make an action undo-able - /// - /// The memento implementing the undo - /// Allow changes to be merged - public void MakeUndoable(IMemento memento, bool allowMerge) - { - if (_inUndoRedo) - { - throw new InvalidOperationException("Invoking do within an undo/redo action."); - } - - if (memento != null) - { - bool allowPush = true; - if (_undoStack.Count > 0 && allowMerge) - { - // Check if merge is possible - allowPush = !_undoStack.Peek().Merge(memento); - } - - if (allowPush) - { - // Clear the redo-stack and dispose - while (_redoStack.Count > 0) - { - _redoStack.Pop().Dispose(); - } - - _undoStack.Push(memento); - } - } - } - - /// - /// This saves the elements of this surface to a stream. - /// Is used to save a template of the complete surface - /// - /// - /// - public long SaveElementsToStream(Stream streamWrite) - { - long bytesWritten = 0; - try - { - long lengtBefore = streamWrite.Length; - BinaryFormatter binaryWrite = new BinaryFormatter(); - binaryWrite.Serialize(streamWrite, _elements); - bytesWritten = streamWrite.Length - lengtBefore; - } - catch (Exception e) - { - LOG.Error("Error serializing elements to stream.", e); - } - - return bytesWritten; - } - - /// - /// This loads elements from a stream, among others this is used to load a surface. - /// - /// - public void LoadElementsFromStream(Stream streamRead) - { - try - { - BinaryFormatter binaryRead = new BinaryFormatter(); - binaryRead.Binder = new BinaryFormatterHelper(); - IDrawableContainerList loadedElements = (IDrawableContainerList) binaryRead.Deserialize(streamRead); - loadedElements.Parent = this; - // Make sure the steplabels are sorted according to their number - _stepLabels.Sort((p1, p2) => p1.Number.CompareTo(p2.Number)); - DeselectAllElements(); - AddElements(loadedElements); - SelectElements(loadedElements); - FieldAggregator.BindElements(loadedElements); - } +using Greenshot.Editor.Memento; +using log4net; + +namespace Greenshot.Editor.Drawing +{ + /// + /// Description of Surface. + /// + public sealed class Surface : Control, ISurface, INotifyPropertyChanged + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(Surface)); + private static readonly CoreConfiguration conf = IniConfig.GetIniSection(); + + // Property to identify the Surface ID + private Guid _uniqueId = Guid.NewGuid(); + + /// + /// This value is used to start counting the step labels + /// + private int _counterStart = 1; + + /// + /// The GUID of the surface + /// + public Guid ID + { + get => _uniqueId; + set => _uniqueId = value; + } + + /// + /// Event handlers (do not serialize!) + /// + [NonSerialized] private PropertyChangedEventHandler _propertyChanged; + + public event PropertyChangedEventHandler PropertyChanged + { + add => _propertyChanged += value; + remove => _propertyChanged -= value; + } + + [NonSerialized] private SurfaceElementEventHandler _movingElementChanged; + + public event SurfaceElementEventHandler MovingElementChanged + { + add => _movingElementChanged += value; + remove => _movingElementChanged -= value; + } + + [NonSerialized] private SurfaceDrawingModeEventHandler _drawingModeChanged; + + public event SurfaceDrawingModeEventHandler DrawingModeChanged + { + add => _drawingModeChanged += value; + remove => _drawingModeChanged -= value; + } + + [NonSerialized] private SurfaceSizeChangeEventHandler _surfaceSizeChanged; + + public event SurfaceSizeChangeEventHandler SurfaceSizeChanged + { + add => _surfaceSizeChanged += value; + remove => _surfaceSizeChanged -= value; + } + + [NonSerialized] private SurfaceMessageEventHandler _surfaceMessage; + + public event SurfaceMessageEventHandler SurfaceMessage + { + add => _surfaceMessage += value; + remove => _surfaceMessage -= value; + } + + [NonSerialized] private SurfaceForegroundColorEventHandler _foregroundColorChanged; + + public event SurfaceForegroundColorEventHandler ForegroundColorChanged + { + add => _foregroundColorChanged += value; + remove => _foregroundColorChanged -= value; + } + + [NonSerialized] private SurfaceBackgroundColorEventHandler _backgroundColorChanged; + + public event SurfaceBackgroundColorEventHandler BackgroundColorChanged + { + add => _backgroundColorChanged += value; + remove => _backgroundColorChanged -= value; + } + + [NonSerialized] private SurfaceLineThicknessEventHandler _lineThicknessChanged; + + public event SurfaceLineThicknessEventHandler LineThicknessChanged + { + add => _lineThicknessChanged += value; + remove => _lineThicknessChanged -= value; + } + + [NonSerialized] private SurfaceShadowEventHandler _shadowChanged; + + public event SurfaceShadowEventHandler ShadowChanged + { + add => _shadowChanged += value; + remove => _shadowChanged -= value; + } + + + [NonSerialized] private int _currentDpi = 96; + /// + /// The most recent DPI value that was used + /// + public int CurrentDpi + { + get => _currentDpi; + set => _currentDpi = value; + } + + /// + /// inUndoRedo makes sure we don't undo/redo while in a undo/redo action + /// + [NonSerialized] private bool _inUndoRedo; + + /// + /// Make only one surface move cycle undoable, see SurfaceMouseMove + /// + [NonSerialized] private bool _isSurfaceMoveMadeUndoable; + + /// + /// Undo/Redo stacks, should not be serialized as the file would be way to big + /// + [NonSerialized] private readonly Stack _undoStack = new Stack(); + + [NonSerialized] private readonly Stack _redoStack = new Stack(); + + /// + /// Last save location, do not serialize! + /// + [NonSerialized] private string _lastSaveFullPath; + + /// + /// current drawing mode, do not serialize! + /// + [NonSerialized] private DrawingModes _drawingMode = DrawingModes.None; + + /// + /// the keys-locked flag helps with focus issues + /// + [NonSerialized] private bool _keysLocked; + + /// + /// Location of the mouse-down (it "starts" here), do not serialize + /// + [NonSerialized] private Point _mouseStart = Point.Empty; + + /// + /// are we in a mouse down, do not serialize + /// + [NonSerialized] private bool _mouseDown; + + /// + /// The selected element for the mouse down, do not serialize + /// + [NonSerialized] private IDrawableContainer _mouseDownElement; + + /// + /// all selected elements, do not serialize + /// + [NonSerialized] private readonly IDrawableContainerList selectedElements; + + /// + /// the element we are drawing with, do not serialize + /// + [NonSerialized] private IDrawableContainer _drawingElement; + + /// + /// the element we want to draw with (not yet drawn), do not serialize + /// + [NonSerialized] private IDrawableContainer _undrawnElement; + + /// + /// the crop container, when cropping this is set, do not serialize + /// + [NonSerialized] private IDrawableContainer _cropContainer; + + /// + /// the brush which is used for transparent backgrounds, set by the editor, do not serialize + /// + [NonSerialized] private Brush _transparencyBackgroundBrush; + + /// + /// The buffer is only for drawing on it when using filters (to supply access) + /// This saves a lot of "create new bitmap" commands + /// Should not be serialized, as it's generated. + /// The actual bitmap is in the paintbox... + /// TODO: Check if this buffer is still needed! + /// + [NonSerialized] private Bitmap _buffer; + + /// + /// all stepLabels for the surface, needed with serialization + /// + private readonly List _stepLabels = new List(); + + public void AddStepLabel(StepLabelContainer stepLabel) + { + if (!_stepLabels.Contains(stepLabel)) + { + _stepLabels.Add(stepLabel); + } + } + + public void RemoveStepLabel(StepLabelContainer stepLabel) + { + _stepLabels.Remove(stepLabel); + } + + /// + /// The start value of the counter objects + /// + public int CounterStart + { + get => _counterStart; + set + { + if (_counterStart == value) + { + return; + } + + _counterStart = value; + Invalidate(); + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CounterStart))); + } + } + + /// + /// Count all the VISIBLE steplabels in the surface, up to the supplied one + /// + /// can be null, if not the counting stops here + /// number of steplabels before the supplied container + public int CountStepLabels(IDrawableContainer stopAtContainer) + { + int number = CounterStart; + foreach (var possibleThis in _stepLabels) + { + if (possibleThis.Equals(stopAtContainer)) + { + break; + } + + if (IsOnSurface(possibleThis)) + { + number++; + } + } + + return number; + } + + /// + /// all elements on the surface, needed with serialization + /// + private readonly IDrawableContainerList _elements; + + /// + /// all elements on the surface, needed with serialization + /// + private IFieldAggregator _fieldAggregator; + + /// + /// the cursor container, needed with serialization as we need a direct acces to it. + /// + private IDrawableContainer _cursorContainer; + + /// + /// the modified flag specifies if the surface has had modifications after the last export. + /// Initial state is modified, as "it's not saved" + /// After serialization this should actually be "false" (the surface came from a stream) + /// For now we just serialize it... + /// + private bool _modified = true; + + /// + /// The image is the actual captured image, needed with serialization + /// + private Image _image; + + public Image Image + { + get => _image; + set + { + _image = value; + UpdateSize(); + } + } + + [NonSerialized] private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); + [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); + [NonSerialized] private Fraction _zoomFactor = Fraction.Identity; + + public Fraction ZoomFactor + { + get => _zoomFactor; + set + { + _zoomFactor = value; + var inverse = _zoomFactor.Inverse(); + _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); + _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); + UpdateSize(); + } + } + + + /// + /// Sets the surface size as zoomed image size. + /// + private void UpdateSize() + { + var size = _image.Size; + Size = new Size((int) (size.Width * _zoomFactor), (int) (size.Height * _zoomFactor)); + } + + /// + /// The field aggregator is that which is used to have access to all the fields inside the currently selected elements. + /// e.g. used to decided if and which line thickness is shown when multiple elements are selected. + /// + public IFieldAggregator FieldAggregator + { + get => _fieldAggregator; + set => _fieldAggregator = value; + } + + /// + /// The cursor container has it's own accessor so we can find and remove this (when needed) + /// + public IDrawableContainer CursorContainer => _cursorContainer; + + /// + /// A simple getter to ask if this surface has a cursor + /// + public bool HasCursor => _cursorContainer != null; + + /// + /// A simple helper method to remove the cursor from the surface + /// + public void RemoveCursor() + { + RemoveElement(_cursorContainer); + _cursorContainer = null; + } + + /// + /// The brush which is used to draw the transparent background + /// + public Brush TransparencyBackgroundBrush + { + get => _transparencyBackgroundBrush; + set => _transparencyBackgroundBrush = value; + } + + /// + /// Are the keys on this surface locked? + /// + public bool KeysLocked + { + get => _keysLocked; + set => _keysLocked = value; + } + + /// + /// Is this surface modified? This is only true if the surface has not been exported. + /// + public bool Modified + { + get => _modified; + set => _modified = value; + } + + /// + /// The DrawingMode property specifies the mode for drawing, more or less the element type. + /// + public DrawingModes DrawingMode + { + get => _drawingMode; + set + { + _drawingMode = value; + if (_drawingModeChanged != null) + { + SurfaceDrawingModeEventArgs eventArgs = new SurfaceDrawingModeEventArgs + { + DrawingMode = _drawingMode + }; + _drawingModeChanged.Invoke(this, eventArgs); + } + + DeselectAllElements(); + CreateUndrawnElement(); + } + } + + /// + /// Property for accessing the last save "full" path + /// + public string LastSaveFullPath + { + get => _lastSaveFullPath; + set => _lastSaveFullPath = value; + } + + /// + /// Property for accessing the URL to which the surface was recently uploaded + /// + public string UploadUrl { get; set; } + + /// + /// Property for accessing the capture details + /// + public ICaptureDetails CaptureDetails { get; set; } + + /// + /// Adjust UI elements to the supplied DPI settings + /// + /// + public void AdjustToDpi(int dpi) + { + CurrentDpi = dpi; + foreach (var element in this._elements) + { + element.AdjustToDpi(dpi); + } + } + + /// + /// Base Surface constructor + /// + public Surface() + { + _fieldAggregator = new FieldAggregator(this); + _elements = new DrawableContainerList(_uniqueId); + selectedElements = new DrawableContainerList(_uniqueId); + LOG.Debug("Creating surface!"); + MouseDown += SurfaceMouseDown; + MouseUp += SurfaceMouseUp; + MouseMove += SurfaceMouseMove; + MouseDoubleClick += SurfaceDoubleClick; + Paint += SurfacePaint; + AllowDrop = true; + DragDrop += OnDragDrop; + DragEnter += OnDragEnter; + // bind selected & elements to this, otherwise they can't inform of modifications + selectedElements.Parent = this; + _elements.Parent = this; + // Make sure we are visible + Visible = true; + TabStop = false; + // Enable double buffering + DoubleBuffered = true; + SetStyle( + ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.ContainerControl | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.SupportsTransparentBackColor, true); + } + + /// + /// Private method, the current image is disposed the new one will stay. + /// + /// The new image + /// true if the old image needs to be disposed, when using undo this should not be true!! + private void SetImage(Image newImage, bool dispose) + { + // Dispose + if (_image != null && dispose) + { + _image.Dispose(); + } + + // Set new values + Image = newImage; + + _modified = true; + } + + /// + /// Surface constructor with an image + /// + /// + public Surface(Image newImage) : this() + { + LOG.DebugFormat("Got image with dimensions {0} and format {1}", newImage.Size, newImage.PixelFormat); + SetImage(newImage, true); + } + + /// + /// Surface contructor with a capture + /// + /// + public Surface(ICapture capture) : this(capture.Image) + { + // check if cursor is captured, and visible + if (capture.Cursor != null && capture.CursorVisible) + { + var cursorRect = new NativeRect(capture.CursorLocation, capture.Cursor.Size); + var captureRect = new NativeRect(NativePoint.Empty, capture.Image.Size); + // check if cursor is on the capture, otherwise we leave it out. + if (cursorRect.IntersectsWith(captureRect)) + { + _cursorContainer = AddIconContainer(capture.Cursor, capture.CursorLocation.X, capture.CursorLocation.Y); + SelectElement(_cursorContainer); + } + } + + // Make sure the image is NOT disposed, we took the reference directly into ourselves + ((Capture) capture).NullImage(); + + CaptureDetails = capture.CaptureDetails; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + LOG.Debug("Disposing surface!"); + if (_buffer != null) + { + _buffer.Dispose(); + _buffer = null; + } + + if (_transparencyBackgroundBrush != null) + { + _transparencyBackgroundBrush.Dispose(); + _transparencyBackgroundBrush = null; + } + + // Cleanup undo/redo stacks + while (_undoStack != null && _undoStack.Count > 0) + { + _undoStack.Pop().Dispose(); + } + + while (_redoStack != null && _redoStack.Count > 0) + { + _redoStack.Pop().Dispose(); + } + + foreach (IDrawableContainer container in _elements) + { + container.Dispose(); + } + + if (_undrawnElement != null) + { + _undrawnElement.Dispose(); + _undrawnElement = null; + } + + if (_cropContainer != null) + { + _cropContainer.Dispose(); + _cropContainer = null; + } + } + + base.Dispose(disposing); + } + + /// + /// Undo the last action + /// + public void Undo() + { + if (_undoStack.Count > 0) + { + _inUndoRedo = true; + IMemento top = _undoStack.Pop(); + _redoStack.Push(top.Restore()); + _inUndoRedo = false; + } + } + + /// + /// Undo an undo (=redo) + /// + public void Redo() + { + if (_redoStack.Count > 0) + { + _inUndoRedo = true; + IMemento top = _redoStack.Pop(); + _undoStack.Push(top.Restore()); + _inUndoRedo = false; + } + } + + /// + /// Returns if the surface can do a undo + /// + public bool CanUndo => _undoStack.Count > 0; + + /// + /// Returns if the surface can do a redo + /// + public bool CanRedo => _redoStack.Count > 0; + + /// + /// Get the language key for the undo action + /// + public LangKey UndoActionLanguageKey => LangKey.none; + + /// + /// Get the language key for redo action + /// + public LangKey RedoActionLanguageKey => LangKey.none; + + /// + /// Make an action undo-able + /// + /// The memento implementing the undo + /// Allow changes to be merged + public void MakeUndoable(IMemento memento, bool allowMerge) + { + if (_inUndoRedo) + { + throw new InvalidOperationException("Invoking do within an undo/redo action."); + } + + if (memento != null) + { + bool allowPush = true; + if (_undoStack.Count > 0 && allowMerge) + { + // Check if merge is possible + allowPush = !_undoStack.Peek().Merge(memento); + } + + if (allowPush) + { + // Clear the redo-stack and dispose + while (_redoStack.Count > 0) + { + _redoStack.Pop().Dispose(); + } + + _undoStack.Push(memento); + } + } + } + + /// + /// This saves the elements of this surface to a stream. + /// Is used to save a template of the complete surface + /// + /// + /// + public long SaveElementsToStream(Stream streamWrite) + { + long bytesWritten = 0; + try + { + long lengtBefore = streamWrite.Length; + BinaryFormatter binaryWrite = new BinaryFormatter(); + binaryWrite.Serialize(streamWrite, _elements); + bytesWritten = streamWrite.Length - lengtBefore; + } + catch (Exception e) + { + LOG.Error("Error serializing elements to stream.", e); + } + + return bytesWritten; + } + + /// + /// This loads elements from a stream, among others this is used to load a surface. + /// + /// + public void LoadElementsFromStream(Stream streamRead) + { + try + { + BinaryFormatter binaryRead = new BinaryFormatter(); + binaryRead.Binder = new BinaryFormatterHelper(); + IDrawableContainerList loadedElements = (IDrawableContainerList) binaryRead.Deserialize(streamRead); + loadedElements.Parent = this; + // Make sure the steplabels are sorted according to their number + _stepLabels.Sort((p1, p2) => p1.Number.CompareTo(p2.Number)); + DeselectAllElements(); + AddElements(loadedElements); + SelectElements(loadedElements); + FieldAggregator.BindElements(loadedElements); + } catch (SecurityAccessDeniedException) { throw; - } - catch (Exception e) - { - LOG.Error("Error serializing elements from stream.", e); - } - } - - /// - /// This is called from the DrawingMode setter, which is not very correct... - /// But here an element is created which is not yet draw, thus "undrawnElement". - /// The element is than used while drawing on the surface. - /// - private void CreateUndrawnElement() - { - if (_undrawnElement != null) - { - FieldAggregator.UnbindElement(_undrawnElement); - } - - switch (DrawingMode) - { - case DrawingModes.Rect: - _undrawnElement = new RectangleContainer(this); - break; - case DrawingModes.Ellipse: - _undrawnElement = new EllipseContainer(this); - break; - case DrawingModes.Text: - _undrawnElement = new TextContainer(this); - break; - case DrawingModes.SpeechBubble: - _undrawnElement = new SpeechbubbleContainer(this); - break; - case DrawingModes.StepLabel: - _undrawnElement = new StepLabelContainer(this); - break; - case DrawingModes.Line: - _undrawnElement = new LineContainer(this); - break; - case DrawingModes.Arrow: - _undrawnElement = new ArrowContainer(this); - break; - case DrawingModes.Highlight: - _undrawnElement = new HighlightContainer(this); - break; - case DrawingModes.Obfuscate: - _undrawnElement = new ObfuscateContainer(this); - break; - case DrawingModes.Crop: - _cropContainer = new CropContainer(this); - _undrawnElement = _cropContainer; - break; - case DrawingModes.Bitmap: - _undrawnElement = new ImageContainer(this); - break; - case DrawingModes.Path: - _undrawnElement = new FreehandContainer(this); - break; - case DrawingModes.None: - _undrawnElement = null; - break; - } - - if (_undrawnElement != null) - { - FieldAggregator.BindElement(_undrawnElement); - } - } - - #region Plugin interface implementations - - public IImageContainer AddImageContainer(Image image, int x, int y) - { - ImageContainer bitmapContainer = new ImageContainer(this) - { - Image = image, - Left = x, - Top = y - }; - AddElement(bitmapContainer); - return bitmapContainer; - } - - public IImageContainer AddImageContainer(string filename, int x, int y) - { - ImageContainer bitmapContainer = new ImageContainer(this); - bitmapContainer.Load(filename); - bitmapContainer.Left = x; - bitmapContainer.Top = y; - AddElement(bitmapContainer); - return bitmapContainer; - } - - public IIconContainer AddIconContainer(Icon icon, int x, int y) - { - IconContainer iconContainer = new IconContainer(this) - { - Icon = icon, - Left = x, - Top = y - }; - AddElement(iconContainer); - return iconContainer; - } - - public IIconContainer AddIconContainer(string filename, int x, int y) - { - IconContainer iconContainer = new IconContainer(this); - iconContainer.Load(filename); - iconContainer.Left = x; - iconContainer.Top = y; - AddElement(iconContainer); - return iconContainer; - } - - public ICursorContainer AddCursorContainer(Cursor cursor, int x, int y) - { - CursorContainer cursorContainer = new CursorContainer(this) - { - Cursor = cursor, - Left = x, - Top = y - }; - AddElement(cursorContainer); - return cursorContainer; - } - - public ICursorContainer AddCursorContainer(string filename, int x, int y) - { - CursorContainer cursorContainer = new CursorContainer(this); - cursorContainer.Load(filename); - cursorContainer.Left = x; - cursorContainer.Top = y; - AddElement(cursorContainer); - return cursorContainer; - } - - public ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, - Color fillColor) - { - TextContainer textContainer = new TextContainer(this) - { - Text = text, - Left = x, - Top = y - }; - textContainer.SetFieldValue(FieldType.FONT_FAMILY, family.Name); - textContainer.SetFieldValue(FieldType.FONT_BOLD, bold); - textContainer.SetFieldValue(FieldType.FONT_ITALIC, italic); - textContainer.SetFieldValue(FieldType.FONT_SIZE, size); - textContainer.SetFieldValue(FieldType.FILL_COLOR, fillColor); - textContainer.SetFieldValue(FieldType.LINE_COLOR, color); - textContainer.SetFieldValue(FieldType.LINE_THICKNESS, borderSize); - textContainer.SetFieldValue(FieldType.SHADOW, shadow); - // Make sure the Text fits - textContainer.FitToText(); - - //AggregatedProperties.UpdateElement(textContainer); - AddElement(textContainer); - return textContainer; - } - - #endregion - - #region DragDrop - - private void OnDragEnter(object sender, DragEventArgs e) - { - if (LOG.IsDebugEnabled) - { - LOG.Debug("DragEnter got following formats: "); - foreach (string format in ClipboardHelper.GetFormats(e.Data)) - { - LOG.Debug(format); - } - } - - if ((e.AllowedEffect & DragDropEffects.Copy) != DragDropEffects.Copy) - { - e.Effect = DragDropEffects.None; - } - else - { - if (ClipboardHelper.ContainsImage(e.Data) || ClipboardHelper.ContainsFormat(e.Data, "DragImageBits")) - { - e.Effect = DragDropEffects.Copy; - } - else - { - e.Effect = DragDropEffects.None; - } - } - } - - /// - /// This will help to fit the container to the surface - /// - /// IDrawableContainer - private void FitContainer(IDrawableContainer drawableContainer) - { - double factor = 1; - if (drawableContainer.Width > this.Width) - { - factor = drawableContainer.Width / (double)Width; - } - if (drawableContainer.Height > this.Height) - { - var otherFactor = drawableContainer.Height / (double)Height; - factor = Math.Max(factor, otherFactor); - } - - drawableContainer.Width = (int)(drawableContainer.Width / factor); - drawableContainer.Height = (int)(drawableContainer.Height / factor); - } - - /// - /// Handle the drag/drop - /// - /// - /// - private void OnDragDrop(object sender, DragEventArgs e) - { - NativePoint mouse = PointToClient(new NativePoint(e.X, e.Y)); - if (e.Data.GetDataPresent("Text")) - { - string possibleUrl = ClipboardHelper.GetText(e.Data); - // Test if it's an url and try to download the image so we have it in the original form - if (possibleUrl != null && possibleUrl.StartsWith("http")) - { - var drawableContainer = NetworkHelper.DownloadImageAsDrawableContainer(possibleUrl); - if (drawableContainer != null) - { - drawableContainer.Left = Location.X; - drawableContainer.Top = Location.Y; - FitContainer(drawableContainer); - AddElement(drawableContainer); - return; - } - } - } - - foreach (var drawableContainer in ClipboardHelper.GetDrawables(e.Data)) - { - drawableContainer.Left = mouse.X; - drawableContainer.Top = mouse.Y; - FitContainer(drawableContainer); - AddElement(drawableContainer); - mouse = mouse.Offset(10, 10); - } - } - - #endregion - - /// - /// Auto crop the image - /// - /// NativeRect with optional area to find a crop region - /// true if cropped - public bool AutoCrop(NativeRect? cropArea = null) - { - NativeRect cropRectangle; - using (Image tmpImage = GetImageForExport()) - { - cropRectangle = ImageHelper.FindAutoCropRectangle(tmpImage, conf.AutoCropDifference, cropArea); - } - - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.AutoCrop)) - { - return false; - } - - DeselectAllElements(); - // Maybe a bit obscure, but the following line creates a drop container - // It's available as "undrawnElement" - DrawingMode = DrawingModes.Crop; - _undrawnElement.Left = cropRectangle.X; - _undrawnElement.Top = cropRectangle.Y; - _undrawnElement.Width = cropRectangle.Width; - _undrawnElement.Height = cropRectangle.Height; - _undrawnElement.Status = EditStatus.UNDRAWN; - AddElement(_undrawnElement); - SelectElement(_undrawnElement); - _drawingElement = null; - _undrawnElement = null; - return true; - } - - /// - /// A simple clear - /// - /// The color for the background - public void Clear(Color newColor) - { - //create a blank bitmap the same size as original - Bitmap newBitmap = ImageHelper.CreateEmptyLike(Image, newColor); - if (newBitmap == null) return; - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, null), false); - SetImage(newBitmap, false); - Invalidate(); - } - - /// - /// Apply a bitmap effect to the surface - /// - /// - public void ApplyBitmapEffect(IEffect effect) - { - BackgroundForm backgroundForm = new BackgroundForm("Effect", "Please wait"); - backgroundForm.Show(); - Application.DoEvents(); - try - { - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - Matrix matrix = new Matrix(); - Image newImage = ImageHelper.ApplyEffect(Image, effect, matrix); - if (newImage != null) - { - // Make sure the elements move according to the offset the effect made the bitmap move - _elements.Transform(matrix); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - SetImage(newImage, false); - Invalidate(); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - } - else - { - // clean up matrix, as it hasn't been used in the undo stack. - matrix.Dispose(); - } - } - finally - { - // Always close the background form - backgroundForm.CloseDialog(); - } - } - - /// - /// check if a crop is possible - /// - /// Rectangle adapted to the dimensions of the image - /// CropModes - /// true if this is possible - public bool IsCropPossible(ref NativeRect cropRectangle, CropContainer.CropModes cropMode) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, cropRectangle.Height).Normalize(); - //Fitting the rectangle to the dimensions of the image - if (cropRectangle.Left < 0) - { - cropRectangle = new NativeRect(0, cropRectangle.Top, cropRectangle.Width + cropRectangle.Left, cropRectangle.Height); - } - - if (cropRectangle.Top < 0) - { - cropRectangle = new NativeRect(cropRectangle.Left, 0, cropRectangle.Width, cropRectangle.Height + cropRectangle.Top); - } - - if (cropRectangle.Left + cropRectangle.Width > Image.Width) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, Image.Width - cropRectangle.Left, cropRectangle.Height); - } - - if (cropRectangle.Top + cropRectangle.Height > Image.Height) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Image.Height - cropRectangle.Top); - } - - // special condition for vertical - if(cropMode == CropContainer.CropModes.Vertical && cropRectangle.Width == Image.Width) - { - //crop out the hole image is not allowed - return false; - } - - // special condition for vertical - if (cropMode == CropContainer.CropModes.Horizontal && cropRectangle.Height == Image.Height) - { - //crop out the hole image is not allowed - return false; - } - - //condition for all other crop modes - if (cropRectangle.Height > 0 && cropRectangle.Width > 0) - { - return true; - } - - return false; - } - - /// - /// Use to send any registered SurfaceMessageEventHandler a message, e.g. used for the notification area - /// - /// Who send - /// Type of message - /// Message itself - public void SendMessageEvent(object source, SurfaceMessageTyp messageType, string message) - { - if (_surfaceMessage == null) return; - - var eventArgs = new SurfaceMessageEventArgs - { - Message = message, - MessageType = messageType, - Surface = this - }; - _surfaceMessage(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the foreground color - /// - /// Who send - /// new color - private void UpdateForegroundColorEvent(object source, Color color) - { - if (_foregroundColorChanged == null) return; - - var eventArgs = new SurfaceForegroundColorEventArgs - { - Color = color, - }; - _foregroundColorChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the background color - /// - /// Who send - /// new color - private void UpdateBackgroundColorEvent(object source, Color color) - { - if (_lineThicknessChanged == null) return; - - var eventArgs = new SurfaceBackgroundColorEventArgs - { - Color = color, - }; - _backgroundColorChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the line thickness - /// - /// Who send - /// new thickness - private void UpdateLineThicknessEvent(object source, int thickness) - { - if (_lineThicknessChanged == null) return; - - var eventArgs = new SurfaceLineThicknessEventArgs - { - Thickness = thickness, - }; - _lineThicknessChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing the key to show/hide the shadow - /// - /// Who send - /// has shadow - private void UpdateShadowEvent(object source, bool hasShadow) - { - if (_shadowChanged == null) return; - - var eventArgs = new SurfaceShadowEventArgs - { - HasShadow = hasShadow, - }; - _shadowChanged(source, eventArgs); - } - - /// - /// Crop the surface - /// - /// NativeRect that remains - /// bool - public bool ApplyCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Default)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - Bitmap tmpImage; - // Make sure we have information, this this fails - try - { - tmpImage = ImageHelper.CloneArea(Image, cropRectangle, PixelFormat.DontCare); - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - - var matrix = new Matrix(); - matrix.Translate(-cropRectangle.Left, -cropRectangle.Top, MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(tmpImage, false); - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, tmpImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// Crop out the surface - /// Splits the image in 3 parts(top, middle, bottom). Crop out the middle and joins top and bottom. - /// - /// NativeRect of the middle part - /// bool - private bool ApplyHorizontalCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Horizontal)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - var topRectangle = new NativeRect(0, 0, Image.Size.Width, cropRectangle.Top); - var bottomRectangle = new NativeRect(0, cropRectangle.Top + cropRectangle.Height, Image.Size.Width, Image.Size.Height - cropRectangle.Top - cropRectangle.Height); - - Bitmap newImage; - try - { - newImage = new Bitmap(Image.Size.Width, Image.Size.Height - cropRectangle.Height); - - using var graphics = Graphics.FromImage(newImage); - - var insertPositionTop = 0; - if (topRectangle.Height > 0) - { - graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, topRectangle.Width, topRectangle.Height), topRectangle, GraphicsUnit.Pixel); - insertPositionTop += topRectangle.Height; - } - if (bottomRectangle.Height > 0) - { - graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, bottomRectangle.Width, bottomRectangle.Height), bottomRectangle, GraphicsUnit.Pixel); - } - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - var matrix = new Matrix(); - matrix.Translate(0, -(cropRectangle.Top + cropRectangle.Height), MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(newImage, false); - - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// Crop out the surface - /// Splits the image in 3 parts(left, middle, right). Crop out the middle and joins top and bottom. - /// - /// NativeRect of the middle part - /// bool - private bool ApplyVerticalCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Vertical)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - var leftRectangle = new NativeRect(0, 0, cropRectangle.Left, Image.Size.Height); - var rightRectangle = new NativeRect(cropRectangle.Left + cropRectangle.Width, 0, Image.Size.Width - cropRectangle.Width - cropRectangle.Left, Image.Size.Height); - Bitmap newImage; - try - { - newImage = new Bitmap(Image.Size.Width - cropRectangle.Width, Image.Size.Height); - - using var graphics = Graphics.FromImage(newImage); - - var insertPositionLeft = 0; - if (leftRectangle.Width > 0) - { - graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, leftRectangle.Width, leftRectangle.Height), leftRectangle , GraphicsUnit.Pixel); - insertPositionLeft += leftRectangle.Width; - } - - if (rightRectangle.Width > 0) - { - graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, rightRectangle.Width, rightRectangle.Height), rightRectangle, GraphicsUnit.Pixel); - } - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - var matrix = new Matrix(); - matrix.Translate(-cropRectangle.Left - cropRectangle.Width, 0, MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(newImage, false); - - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// The background here is the captured image. - /// This is called from the SurfaceBackgroundChangeMemento. - /// - /// - /// - public void UndoBackgroundChange(Image previous, Matrix matrix) - { - SetImage(previous, false); - if (matrix != null) - { - _elements.Transform(matrix); - } - - _surfaceSizeChanged?.Invoke(this, null); - Invalidate(); - } - - /// - /// Check if an adorner was "hit", and change the cursor if so - /// - /// MouseEventArgs - /// IAdorner - private IAdorner FindActiveAdorner(MouseEventArgs mouseEventArgs) - { - foreach (IDrawableContainer drawableContainer in selectedElements) - { - foreach (IAdorner adorner in drawableContainer.Adorners) - { - if (!adorner.IsActive && !adorner.HitTest(mouseEventArgs.Location)) continue; - - if (adorner.Cursor != null) - { - Cursor = adorner.Cursor; - } - - return adorner; - } - } - - return null; - } - - /// - /// Translate mouse coordinates as if they were applied directly to unscaled image. - /// - private MouseEventArgs InverseZoomMouseCoordinates(MouseEventArgs e) - => new MouseEventArgs(e.Button, e.Clicks, (int) (e.X / _zoomFactor), (int) (e.Y / _zoomFactor), e.Delta); - - /// - /// This event handler is called when someone presses the mouse on a surface. - /// - /// - /// - private void SurfaceMouseDown(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseDown(sender, e); - return; - } - - _mouseStart = e.Location; - - // check contextmenu - if (e.Button == MouseButtons.Right) - { - IDrawableContainerList selectedList = null; - if (selectedElements != null && selectedElements.Count > 0) - { - selectedList = selectedElements; - } - else - { - // Single element - IDrawableContainer rightClickedContainer = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); - if (rightClickedContainer != null) - { - selectedList = new DrawableContainerList(ID) - { - rightClickedContainer - }; - } - } - - if (selectedList != null && selectedList.Count > 0) - { - selectedList.ShowContextMenu(e, this); - } - - return; - } - - _mouseDown = true; - _isSurfaceMoveMadeUndoable = false; - - if (_cropContainer != null && ((_undrawnElement == null) || (_undrawnElement != null && DrawingMode != DrawingModes.Crop))) - { - RemoveElement(_cropContainer, false); - _cropContainer = null; - _drawingElement = null; - } - - if (_drawingElement == null && DrawingMode != DrawingModes.None) - { - if (_undrawnElement == null) - { - DeselectAllElements(); - if (_undrawnElement == null) - { - CreateUndrawnElement(); - } - } - - _drawingElement = _undrawnElement; - // if a new element has been drawn, set location and register it - if (_drawingElement != null) - { - if (_undrawnElement != null) - { - _drawingElement.Status = _undrawnElement.DefaultEditMode; - } - - if (!_drawingElement.HandleMouseDown(_mouseStart.X, _mouseStart.Y)) - { - _drawingElement.Left = _mouseStart.X; - _drawingElement.Top = _mouseStart.Y; - } - - AddElement(_drawingElement); - _drawingElement.Selected = true; - } - - _undrawnElement = null; - } - else - { - // check whether an existing element was clicked - // we save mouse down element separately from selectedElements (checked on mouse up), - // since it could be moved around before it is actually selected - _mouseDownElement = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); - - if (_mouseDownElement != null) - { - _mouseDownElement.Status = EditStatus.MOVING; - } - } - } - - /// - /// This event handle is called when the mouse button is unpressed - /// - /// - /// - private void SurfaceMouseUp(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseUp(sender, e); - return; - } - - Point currentMouse = new Point(e.X, e.Y); - - _elements.Status = EditStatus.IDLE; - if (_mouseDownElement != null) - { - _mouseDownElement.Status = EditStatus.IDLE; - } - - _mouseDown = false; - _mouseDownElement = null; - if (DrawingMode == DrawingModes.None) - { - // check whether an existing element was clicked - IDrawableContainer element = _elements.ClickableElementAt(currentMouse.X, currentMouse.Y); - bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; - if (element != null) - { - element.Invalidate(); - bool alreadySelected = selectedElements.Contains(element); - if (shiftModifier) - { - if (alreadySelected) - { - DeselectElement(element); - } - else - { - SelectElement(element); - } - } - else - { - if (!alreadySelected) - { - DeselectAllElements(); - SelectElement(element); - } - } - } - else if (!shiftModifier) - { - DeselectAllElements(); - } - } - - if (selectedElements.Count > 0) - { - selectedElements.Invalidate(); - selectedElements.Selected = true; - } - - if (_drawingElement != null) - { - if (!_drawingElement.InitContent()) - { - _elements.Remove(_drawingElement); - _drawingElement.Invalidate(); - } - else - { - _drawingElement.HandleMouseUp(currentMouse.X, currentMouse.Y); - _drawingElement.Invalidate(); - if (Math.Abs(_drawingElement.Width) < 5 && Math.Abs(_drawingElement.Height) < 5) - { - _drawingElement.Width = 25; - _drawingElement.Height = 25; - } - - SelectElement(_drawingElement); - _drawingElement.Selected = true; - } - - _drawingElement = null; - } - } - - /// - /// This event handler is called when the mouse moves over the surface - /// - /// - /// - private void SurfaceMouseMove(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseMove(sender, e); - return; - } - - Point currentMouse = e.Location; - - Cursor = DrawingMode != DrawingModes.None ? Cursors.Cross : Cursors.Default; - - if (!_mouseDown) return; - - if (_mouseDownElement != null) - { - // an element is currently dragged - _mouseDownElement.Invalidate(); - selectedElements.Invalidate(); - // Move the element - if (_mouseDownElement.Selected) - { - if (!_isSurfaceMoveMadeUndoable) - { - // Only allow one undoable per mouse-down/move/up "cycle" - _isSurfaceMoveMadeUndoable = true; - selectedElements.MakeBoundsChangeUndoable(false); - } - - // dragged element has been selected before -> move all - selectedElements.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); - } - else - { - if (!_isSurfaceMoveMadeUndoable) - { - // Only allow one undoable per mouse-down/move/up "cycle" - _isSurfaceMoveMadeUndoable = true; - _mouseDownElement.MakeBoundsChangeUndoable(false); - } - - // dragged element is not among selected elements -> just move dragged one - _mouseDownElement.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); - } - - _mouseStart = currentMouse; - _mouseDownElement.Invalidate(); - _modified = true; - } - else if (_drawingElement != null) - { - _drawingElement.HandleMouseMove(currentMouse.X, currentMouse.Y); - _modified = true; - } - } - - /// - /// This event handler is called when the surface is double clicked. - /// - /// - /// - private void SurfaceDoubleClick(object sender, MouseEventArgs e) - { - selectedElements.OnDoubleClick(); - selectedElements.Invalidate(); - } - - /// - /// Privately used to get the rendered image with all the elements on it. - /// - /// - /// - private Image GetImage(RenderMode renderMode) - { - // Generate a copy of the original image with a dpi equal to the default... - Bitmap clone = ImageHelper.Clone(_image, PixelFormat.DontCare); - // otherwise we would have a problem drawing the image to the surface... :( - using (Graphics graphics = Graphics.FromImage(clone)) - { - // Do not set the following, the containers need to decide themselves - //graphics.SmoothingMode = SmoothingMode.HighQuality; - //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - //graphics.CompositingQuality = CompositingQuality.HighQuality; - //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - _elements.Draw(graphics, clone, renderMode, new Rectangle(Point.Empty, clone.Size)); - } - - return clone; - } - - /// - /// This returns the image "result" of this surface, with all the elements rendered on it. - /// - /// - public Image GetImageForExport() - { - return GetImage(RenderMode.EXPORT); - } - - private static NativeRect ZoomClipRectangle(NativeRect rc, double scale, int inflateAmount = 0) - { - rc = new NativeRect( - (int) (rc.X * scale), - (int) (rc.Y * scale), - (int) (rc.Width * scale) + 1, - (int) (rc.Height * scale) + 1 - ); - if (scale > 1) - { - inflateAmount = (int) (inflateAmount * scale); - } - - return rc.Inflate(inflateAmount, inflateAmount); - } - - public void InvalidateElements(NativeRect rc) - => Invalidate(ZoomClipRectangle(rc, _zoomFactor, 1)); - - /// - /// This is the event handler for the Paint Event, try to draw as little as possible! - /// - /// - /// PaintEventArgs - private void SurfacePaint(object sender, PaintEventArgs paintEventArgs) - { - Graphics targetGraphics = paintEventArgs.Graphics; - NativeRect targetClipRectangle = paintEventArgs.ClipRectangle; - if (targetClipRectangle.IsEmpty) - { - LOG.Debug("Empty cliprectangle??"); - return; - } - - // Correction to prevent rounding errors at certain zoom levels. - // When zooming to N/M, clip rectangle top and left coordinates should be multiples of N. - if (_zoomFactor.Numerator > 1 && _zoomFactor.Denominator > 1) - { - int horizontalCorrection = targetClipRectangle.Left % (int) _zoomFactor.Numerator; - int verticalCorrection = targetClipRectangle.Top % (int) _zoomFactor.Numerator; - if (horizontalCorrection != 0) - { - targetClipRectangle = targetClipRectangle.ChangeX(-horizontalCorrection).ChangeWidth(horizontalCorrection); - } - - if (verticalCorrection != 0) - { - targetClipRectangle = targetClipRectangle.ChangeY(-verticalCorrection).ChangeHeight(verticalCorrection); - } - } - - NativeRect imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); - - if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) - { - if (_buffer != null) - { - if (_buffer.Width != Image.Width || _buffer.Height != Image.Height || _buffer.PixelFormat != Image.PixelFormat) - { - _buffer.Dispose(); - _buffer = null; - } - } - - if (_buffer == null) - { - _buffer = ImageHelper.CreateEmpty(Image.Width, Image.Height, Image.PixelFormat, Color.Empty, Image.HorizontalResolution, Image.VerticalResolution); - LOG.DebugFormat("Created buffer with size: {0}x{1}", Image.Width, Image.Height); - } - - // Elements might need the bitmap, so we copy the part we need - using (Graphics graphics = Graphics.FromImage(_buffer)) - { - // do not set the following, the containers need to decide this themselves! - //graphics.SmoothingMode = SmoothingMode.HighQuality; - //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - //graphics.CompositingQuality = CompositingQuality.HighQuality; - //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - DrawBackground(graphics, imageClipRectangle); - graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); - _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); - } - - if (_zoomFactor == Fraction.Identity) - { - targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - } - else - { - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - if (_zoomFactor > Fraction.Identity) - { - DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); - } - else - { - DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); - } - - targetGraphics.ResetTransform(); - } - } - else - { - DrawBackground(targetGraphics, targetClipRectangle); - if (_zoomFactor == Fraction.Identity) - { - targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); - } - else - { - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - DrawSmoothImage(targetGraphics, Image, imageClipRectangle); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); - targetGraphics.ResetTransform(); - } - } - - // No clipping for the adorners - targetGraphics.ResetClip(); - // Draw adorners last - foreach (var drawableContainer in selectedElements) - { - foreach (var adorner in drawableContainer.Adorners) - { - adorner.Paint(paintEventArgs); - } - } - } - - private void DrawSmoothImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) - { - var state = targetGraphics.Save(); - targetGraphics.SmoothingMode = SmoothingMode.HighQuality; - targetGraphics.InterpolationMode = InterpolationMode.HighQualityBilinear; - targetGraphics.CompositingQuality = CompositingQuality.HighQuality; - targetGraphics.PixelOffsetMode = PixelOffsetMode.None; - - targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - - targetGraphics.Restore(state); - } - - private void DrawSharpImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) - { - var state = targetGraphics.Save(); - targetGraphics.SmoothingMode = SmoothingMode.None; - targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; - targetGraphics.CompositingQuality = CompositingQuality.HighQuality; - targetGraphics.PixelOffsetMode = PixelOffsetMode.None; - - targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - - targetGraphics.Restore(state); - } - - private void DrawBackground(Graphics targetGraphics, NativeRect clipRectangle) - { - // check if we need to draw the checkerboard - if (Image.IsAlphaPixelFormat(Image.PixelFormat) && _transparencyBackgroundBrush != null) - { - targetGraphics.FillRectangle(_transparencyBackgroundBrush, clipRectangle); - } - else - { - targetGraphics.Clear(BackColor); - } - } - - /// - /// Draw a checkboard when capturing with transparency - /// - /// PaintEventArgs - protected override void OnPaintBackground(PaintEventArgs e) - { - } - - /// - /// Add a new element to the surface - /// - /// the new element - /// true if the adding should be undoable - /// true if invalidate needs to be called - public void AddElement(IDrawableContainer element, bool makeUndoable = true, bool invalidate = true) - { - _elements.Add(element); - if (element is DrawableContainer container) - { - container.FieldChanged += Element_FieldChanged; - } - - element.Parent = this; - if (element.Status == EditStatus.UNDRAWN) - { - element.Status = EditStatus.IDLE; - } - - if (element.Selected) - { - // Use false, as the element is invalidated when invalidate == true anyway - SelectElement(element, false); - } - - if (invalidate) - { - element.Invalidate(); - } - - if (makeUndoable && element.IsUndoable) - { - MakeUndoable(new AddElementMemento(this, element), false); - } - - _modified = true; - } - - /// - /// Remove the list of elements - /// - /// IDrawableContainerList - /// flag specifying if the remove needs to be undoable - public void RemoveElements(IDrawableContainerList elementsToRemove, bool makeUndoable = true) - { - // fix potential issues with iterating a changing list - DrawableContainerList cloned = new DrawableContainerList(elementsToRemove); - - if (makeUndoable) - { - // Take all containers to make undoable - var undoableContainers = elementsToRemove.Where(c => c.IsUndoable).ToList(); - if (undoableContainers.Any()) - { - var undoableContainerList = new DrawableContainerList(undoableContainers); - MakeUndoable(new DeleteElementsMemento(this, undoableContainerList), false); - } - } - - SuspendLayout(); - foreach (var drawableContainer in cloned) - { - RemoveElement(drawableContainer, false, false, false); - } - - ResumeLayout(); - Invalidate(); - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = cloned - }; - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Remove an element of the elements list - /// - /// Element to remove - /// flag specifying if the remove needs to be undoable - /// flag specifying if an surface invalidate needs to be called - /// false to skip event generation - public void RemoveElement(IDrawableContainer elementToRemove, bool makeUndoable = true, bool invalidate = true, bool generateEvents = true) - { - DeselectElement(elementToRemove, generateEvents); - _elements.Remove(elementToRemove); - if (elementToRemove is DrawableContainer element) - { - element.FieldChanged -= Element_FieldChanged; - } - - if (elementToRemove != null) - { - elementToRemove.Parent = null; - } - - // Do not dispose, the memento should!! element.Dispose(); - if (invalidate) - { - Invalidate(); - } - - if (makeUndoable && elementToRemove is { IsUndoable: true }) - { - MakeUndoable(new DeleteElementMemento(this, elementToRemove), false); - } - - _modified = true; - } - - /// - /// Add the supplied elements to the surface - /// - /// DrawableContainerList - /// true if the adding should be undoable - public void AddElements(IDrawableContainerList elementsToAdd, bool makeUndoable = true) - { - // fix potential issues with iterating a changing list - DrawableContainerList cloned = new DrawableContainerList(elementsToAdd); - if (makeUndoable) - { - // Take all containers to make undoable - var undoableContainers = elementsToAdd.Where(c => c.IsUndoable).ToList(); - if (undoableContainers.Any()) - { - var undoableContainerList = new DrawableContainerList(undoableContainers); - MakeUndoable(new AddElementsMemento(this, undoableContainerList), false); - } - } - - SuspendLayout(); - foreach (var element in cloned) - { - element.Selected = true; - AddElement(element, false, false); - } - - ResumeLayout(); - Invalidate(); - } - - /// - /// Returns if this surface has selected elements - /// - /// bool - public bool HasSelectedElements => selectedElements is { Count: > 0 }; - - /// - /// Provides the selected elements - /// - public IDrawableContainerList SelectedElements => selectedElements; - - /// - /// Remove all the selected elements - /// - public void RemoveSelectedElements() - { - if (!HasSelectedElements) return; - - // As RemoveElement will remove the element from the selectedElements list we need to copy the element to another list. - RemoveElements(selectedElements); - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs(); - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Cut the selected elements from the surface to the clipboard - /// - public void CutSelectedElements() - { - if (!HasSelectedElements) return; - ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); - RemoveSelectedElements(); - } - - /// - /// Copy the selected elements to the clipboard - /// - public void CopySelectedElements() - { - if (!HasSelectedElements) return; - ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); - } - - /// - /// This method is called to confirm/cancel. - /// Called when pressing enter or using the "check" in the editor. - /// redirects to the specialized confirm/cancel method - /// - /// bool - public void Confirm(bool confirm) - { - if (DrawingMode == DrawingModes.Crop) - { - ConfirmCrop(confirm); - } - else - { - ConfirmSelectedConfirmableElements(confirm); - } - } - - /// - /// This method is called to confirm/cancel "confirmable" elements - /// Called when pressing enter or using the "check" in the editor. - ///
- /// For crop-container there is a dedicated method . - ///
- /// bool - public void ConfirmSelectedConfirmableElements(bool confirm) - { - // create new collection so that we can iterate safely (selectedElements might change due with confirm/cancel) - List selectedDCs = new List(selectedElements); - foreach (IDrawableContainer dc in selectedDCs.Where(c => c.IsConfirmable)) - { - throw new NotImplementedException($"No confirm/cancel defined for Container type {dc.GetType()}"); - } - - // maybe the undo button has to be enabled - _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); - } - - /// - /// This method is called to confirm/cancel the crop-container. - /// Called when pressing enter or using the "check" in the editor. - /// - /// bool - public void ConfirmCrop(bool confirm) - { - if (_cropContainer is not CropContainer e) return; - - if (confirm && selectedElements.Count > 0) - { - // No undo memento for the cropcontainer itself, only for the effect - RemoveElement(_cropContainer, false); - - _ = e.GetFieldValue(FieldType.CROPMODE) switch - { - CropContainer.CropModes.Horizontal => ApplyHorizontalCrop(_cropContainer.Bounds), - CropContainer.CropModes.Vertical => ApplyVerticalCrop(_cropContainer.Bounds), - _ => ApplyCrop(_cropContainer.Bounds) - }; - - _cropContainer.Dispose(); - _cropContainer = null; - } - else - { - RemoveCropContainer(); - } - - DrawingMode = DrawingModes.None; - - // maybe the undo button has to be enabled - _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); - } - - public void RemoveCropContainer() - { - if (_cropContainer == null) return; - - RemoveElement(_cropContainer, false); - _cropContainer.Dispose(); - _cropContainer = null; - } - - /// - /// Paste all the elements that are on the clipboard - /// - public void PasteElementFromClipboard() - { - IDataObject clipboard = ClipboardHelper.GetDataObject(); - - var formats = ClipboardHelper.GetFormats(clipboard); - if (formats == null || formats.Count == 0) - { - return; - } - - if (LOG.IsDebugEnabled) - { - LOG.Debug("List of clipboard formats available for pasting:"); - foreach (string format in formats) - { - LOG.Debug("\tgot format: " + format); - } - } - - if (formats.Contains(typeof(IDrawableContainerList).FullName)) - { - IDrawableContainerList dcs = (IDrawableContainerList) ClipboardHelper.GetFromDataObject(clipboard, typeof(IDrawableContainerList)); - if (dcs != null) - { - // Make element(s) only move 10,10 if the surface is the same - bool isSameSurface = (dcs.ParentID == _uniqueId); - dcs.Parent = this; - var moveOffset = isSameSurface ? new NativePoint(10, 10) : NativePoint.Empty; - // Here a fix for bug #1475, first calculate the bounds of the complete IDrawableContainerList - NativeRect drawableContainerListBounds = NativeRect.Empty; - foreach (var element in dcs) - { - drawableContainerListBounds = drawableContainerListBounds == NativeRect.Empty - ? element.DrawingBounds - : drawableContainerListBounds.Union(element.DrawingBounds); - } - - // And find a location inside the target surface to paste to - bool containersCanFit = drawableContainerListBounds.Width < Bounds.Width && drawableContainerListBounds.Height < Bounds.Height; - if (!containersCanFit) - { - NativePoint containersLocation = drawableContainerListBounds.Location; - containersLocation.Offset(moveOffset); - if (!Bounds.Contains(containersLocation)) - { - // Easy fix for same surface - moveOffset = isSameSurface - ? new NativePoint(-10, -10) - : new NativePoint(-drawableContainerListBounds.Location.X + 10, -drawableContainerListBounds.Location.Y + 10); - } - } - else - { - NativeRect moveContainerListBounds = drawableContainerListBounds.Offset(moveOffset); - // check if the element is inside - if (!Bounds.Contains(moveContainerListBounds)) - { - // Easy fix for same surface - if (isSameSurface) - { - moveOffset = new Point(-10, -10); - } - else - { - // For different surface, which is most likely smaller - int offsetX = 0; - int offsetY = 0; - if (drawableContainerListBounds.Right > Bounds.Right) - { - offsetX = Bounds.Right - drawableContainerListBounds.Right; - // Correction for the correction - if (drawableContainerListBounds.Left + offsetX < 0) - { - offsetX += Math.Abs(drawableContainerListBounds.Left + offsetX); - } - } - - if (drawableContainerListBounds.Bottom > Bounds.Bottom) - { - offsetY = Bounds.Bottom - drawableContainerListBounds.Bottom; - // Correction for the correction - if (drawableContainerListBounds.Top + offsetY < 0) - { - offsetY += Math.Abs(drawableContainerListBounds.Top + offsetY); - } - } - - moveOffset = new Point(offsetX, offsetY); - } - } - } - - dcs.MoveBy(moveOffset.X, moveOffset.Y); - AddElements(dcs); - FieldAggregator.BindElements(dcs); - DeselectAllElements(); - SelectElements(dcs); - } - } - else if (ClipboardHelper.ContainsImage(clipboard)) - { - NativePoint pasteLocation = GetPasteLocation(0.1f, 0.1f); - - foreach (var drawableContainer in ClipboardHelper.GetDrawables(clipboard)) - { - if (drawableContainer == null) continue; - DeselectAllElements(); - drawableContainer.Left = pasteLocation.X; - drawableContainer.Top = pasteLocation.Y; - AddElement(drawableContainer); - SelectElement(drawableContainer); - pasteLocation = pasteLocation.Offset(10, 10); - } - } - else if (ClipboardHelper.ContainsText(clipboard)) - { - NativePoint pasteLocation = GetPasteLocation(0.4f, 0.4f); - - string text = ClipboardHelper.GetText(clipboard); - if (text != null) - { - DeselectAllElements(); - ITextContainer textContainer = AddTextContainer(text, pasteLocation.X, pasteLocation.Y, - FontFamily.GenericSansSerif, 12f, false, false, false, 2, Color.Black, Color.Transparent); - SelectElement(textContainer); - } - } - } - - /// - /// Find a location to paste elements. - /// If mouse is over the surface - use it's position, otherwise use the visible area. - /// Return a point in image coordinate space. - /// - /// 0.0f for the left edge of visible area, 1.0f for the right edge of visible area. - /// 0.0f for the top edge of visible area, 1.0f for the bottom edge of visible area. - private NativePoint GetPasteLocation(float horizontalRatio = 0.5f, float verticalRatio = 0.5f) - { - var point = PointToClient(MousePosition); - var rc = GetVisibleRectangle(); - if (!rc.Contains(point)) - { - point = new NativePoint( - rc.Left + (int) (rc.Width * horizontalRatio), - rc.Top + (int) (rc.Height * verticalRatio) - ); - } - - return ToImageCoordinates(point); - } - - /// - /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). - /// - public NativeRect GetVisibleRectangle() - { - var bounds = Bounds; - var clientArea = Parent.ClientRectangle; - return new NativeRect( - Math.Max(0, -bounds.Left), - Math.Max(0, -bounds.Top), - clientArea.Width, - clientArea.Height - ); - } - - /// - /// Get the rectangle bounding all selected elements (in surface coordinates space), - /// or empty rectangle if nothing is selected. - /// - public NativeRect GetSelectionRectangle() - => ToSurfaceCoordinates(selectedElements.DrawingBounds); - - /// - /// Duplicate all the selected elements - /// - public void DuplicateSelectedElements() - { - LOG.DebugFormat("Duplicating {0} selected elements", selectedElements.Count); - IDrawableContainerList dcs = selectedElements.Clone(); - dcs.Parent = this; - dcs.MoveBy(10, 10); - AddElements(dcs); - DeselectAllElements(); - SelectElements(dcs); - } - - /// - /// Deselect the specified element - /// - /// IDrawableContainerList - /// false to skip event generation - public void DeselectElement(IDrawableContainer container, bool generateEvents = true) - { - container.Selected = false; - selectedElements.Remove(container); - FieldAggregator.UnbindElement(container); - if (generateEvents && _movingElementChanged != null) - { - var eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Deselect the specified elements - /// - /// IDrawableContainerList - public void DeselectElements(IDrawableContainerList elements) - { - if (elements.Count == 0) - { - return; - } - - while (elements.Count > 0) - { - var element = elements[0]; - DeselectElement(element, false); - } - - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - Invalidate(); - } - - /// - /// Deselect all the selected elements - /// - public void DeselectAllElements() - { - DeselectElements(selectedElements); - } - - /// - /// Select the supplied element - /// - /// - /// false to skip invalidation - /// false to skip event generation - public void SelectElement(IDrawableContainer container, bool invalidate = true, bool generateEvents = true) - { - if (selectedElements.Contains(container)) return; - - selectedElements.Add(container); - container.Selected = true; - FieldAggregator.BindElement(container); - if (generateEvents && _movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - if (invalidate) - { - container.Invalidate(); - } - } - - /// - /// Select all elements, this is called when Ctrl+A is pressed - /// - public void SelectAllElements() - { - SelectElements(_elements); - } - - /// - /// Select the supplied elements - /// - /// - public void SelectElements(IDrawableContainerList elements) - { - SuspendLayout(); - foreach (var drawableContainer in elements) - { - var element = (DrawableContainer) drawableContainer; - SelectElement(element, false, false); - } - - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - ResumeLayout(); - Invalidate(); - } - - /// - /// Process key presses on the surface, this is called from the editor (and NOT an override from the Control) - /// - /// Keys - /// false if no keys were processed - public bool ProcessCmdKey(Keys k) - { - if (selectedElements.Count <= 0 && k != Keys.Escape) return false; - - bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; - int px = shiftModifier ? 10 : 1; - NativePoint moveBy = NativePoint.Empty; - switch (k) - { - case Keys.Left: - case Keys.Left | Keys.Shift: - moveBy = new NativePoint(-px, 0); - break; - case Keys.Up: - case Keys.Up | Keys.Shift: - moveBy = new NativePoint(0, -px); - break; - case Keys.Right: - case Keys.Right | Keys.Shift: - moveBy = new NativePoint(px, 0); - break; - case Keys.Down: - case Keys.Down | Keys.Shift: - moveBy = new NativePoint(0, px); - break; - case Keys.PageUp: - PullElementsUp(); - break; - case Keys.PageDown: - PushElementsDown(); - break; - case Keys.Home: - PullElementsToTop(); - break; - case Keys.End: - PushElementsToBottom(); - break; - case Keys.Enter: - Confirm(true); - break; - case Keys.Escape: - Confirm(false); - break; - case Keys.D0 | Keys.Control: - case Keys.D0 | Keys.Shift | Keys.Control: - SetSelectedElementColor(shiftModifier ? Color.Orange : Color.Transparent, false, shiftModifier); - break; - case Keys.D1 | Keys.Control: - case Keys.D1 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Red, false, shiftModifier); - break; - case Keys.D2 | Keys.Control: - case Keys.D2 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Green, false, shiftModifier); - break; - case Keys.D3 | Keys.Control: - case Keys.D3 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Blue, false, shiftModifier); - break; - case Keys.D4 | Keys.Control: - case Keys.D4 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Cyan, false, shiftModifier); - break; - case Keys.D5 | Keys.Control: - case Keys.D5 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Magenta, false, shiftModifier); - break; - case Keys.D6 | Keys.Control: - case Keys.D6 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Yellow, false, shiftModifier); - break; - case Keys.D7 | Keys.Control: - case Keys.D7 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Black, false, shiftModifier); - break; - case Keys.D8 | Keys.Control: - case Keys.D8 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Gray, false, shiftModifier); - break; - case Keys.D9 | Keys.Control: - case Keys.D9 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.White, false, shiftModifier); - break; - case Keys.Add | Keys.Control: - case Keys.Add | Keys.Shift | Keys.Control: - ChangeLineThickness(shiftModifier ? 5 : 1); - break; - case Keys.Subtract | Keys.Control: - case Keys.Subtract | Keys.Shift | Keys.Control: - ChangeLineThickness(shiftModifier ? -5 : -1); - break; - case Keys.Divide | Keys.Control: - FlipShadow(); - break; - /*case Keys.Delete: - RemoveSelectedElements(); - break;*/ - default: - return false; - } - - if (moveBy != NativePoint.Empty) - { - selectedElements.MakeBoundsChangeUndoable(true); - selectedElements.MoveBy(moveBy.X, moveBy.Y); - } - - return true; - - } - - // for laptops without numPads, also allow shift modifier - private void SetSelectedElementColor(Color color, bool numPad, bool shift) - { - if (numPad || shift) - { - selectedElements.SetForegroundColor(color); - UpdateForegroundColorEvent(this, color); - } - else - { - selectedElements.SetBackgroundColor(color); - UpdateBackgroundColorEvent(this, color); - } - selectedElements.Invalidate(); - } - - private void ChangeLineThickness(int increaseBy) - { - var newThickness = selectedElements.IncreaseLineThickness(increaseBy); - UpdateLineThicknessEvent(this, newThickness); - selectedElements.Invalidate(); - } - - private void FlipShadow() - { - var shadow = selectedElements.FlipShadow(); - UpdateShadowEvent(this, shadow); - selectedElements.Invalidate(); - } - - /// - /// Property for accessing the elements on the surface - /// - public IDrawableContainerList Elements => _elements; - - /// - /// pulls selected elements up one level in hierarchy - /// - public void PullElementsUp() - { - _elements.PullElementsUp(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements up to top in hierarchy - /// - public void PullElementsToTop() - { - _elements.PullElementsToTop(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements down one level in hierarchy - /// - public void PushElementsDown() - { - _elements.PushElementsDown(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements down to bottom in hierarchy - /// - public void PushElementsToBottom() - { - _elements.PushElementsToBottom(selectedElements); - _elements.Invalidate(); - } - - /// - /// indicates whether the selected elements could be pulled up in hierarchy - /// - /// true if selected elements could be pulled up, false otherwise - public bool CanPullSelectionUp() - { - return _elements.CanPullUp(selectedElements); - } - - /// - /// indicates whether the selected elements could be pushed down in hierarchy - /// - /// true if selected elements could be pushed down, false otherwise - public bool CanPushSelectionDown() - { - return _elements.CanPushDown(selectedElements); - } - - private void Element_FieldChanged(object sender, FieldChangedEventArgs e) - { - selectedElements.HandleFieldChangedEvent(sender, e); - } - - public bool IsOnSurface(IDrawableContainer container) - { - return _elements.Contains(container); - } - - public NativePoint ToSurfaceCoordinates(NativePoint point) - { - Point[] points = - { - point - }; - _zoomMatrix.TransformPoints(points); - return points[0]; - } - - public NativeRect ToSurfaceCoordinates(NativeRect rc) - { - if (_zoomMatrix.IsIdentity) - { - return rc; - } - - Point[] points = - { - rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) - }; - _zoomMatrix.TransformPoints(points); - return new NativeRect( - points[0].X, - points[0].Y, - points[1].X - points[0].X, - points[1].Y - points[0].Y - ); - } - - public NativePoint ToImageCoordinates(NativePoint point) - { - Point[] points = - { - point - }; - _inverseZoomMatrix.TransformPoints(points); - return points[0]; - } - - public NativeRect ToImageCoordinates(NativeRect rc) - { - if (_inverseZoomMatrix.IsIdentity) - { - return rc; - } - - Point[] points = - { - rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) - }; - _inverseZoomMatrix.TransformPoints(points); - return new NativeRect( - points[0].X, - points[0].Y, - points[1].X - points[0].X, - points[1].Y - points[0].Y - ); - } - } + } + catch (Exception e) + { + LOG.Error("Error serializing elements from stream.", e); + } + } + + /// + /// This is called from the DrawingMode setter, which is not very correct... + /// But here an element is created which is not yet draw, thus "undrawnElement". + /// The element is than used while drawing on the surface. + /// + private void CreateUndrawnElement() + { + if (_undrawnElement != null) + { + FieldAggregator.UnbindElement(_undrawnElement); + } + + switch (DrawingMode) + { + case DrawingModes.Rect: + _undrawnElement = new RectangleContainer(this); + break; + case DrawingModes.Ellipse: + _undrawnElement = new EllipseContainer(this); + break; + case DrawingModes.Text: + _undrawnElement = new TextContainer(this); + break; + case DrawingModes.SpeechBubble: + _undrawnElement = new SpeechbubbleContainer(this); + break; + case DrawingModes.StepLabel: + _undrawnElement = new StepLabelContainer(this); + break; + case DrawingModes.Line: + _undrawnElement = new LineContainer(this); + break; + case DrawingModes.Arrow: + _undrawnElement = new ArrowContainer(this); + break; + case DrawingModes.Highlight: + _undrawnElement = new HighlightContainer(this); + break; + case DrawingModes.Obfuscate: + _undrawnElement = new ObfuscateContainer(this); + break; + case DrawingModes.Crop: + _cropContainer = new CropContainer(this); + _undrawnElement = _cropContainer; + break; + case DrawingModes.Bitmap: + _undrawnElement = new ImageContainer(this); + break; + case DrawingModes.Path: + _undrawnElement = new FreehandContainer(this); + break; + case DrawingModes.None: + _undrawnElement = null; + break; + } + + if (_undrawnElement != null) + { + FieldAggregator.BindElement(_undrawnElement); + } + } + + #region Plugin interface implementations + + public IImageContainer AddImageContainer(Image image, int x, int y) + { + ImageContainer bitmapContainer = new ImageContainer(this) + { + Image = image, + Left = x, + Top = y + }; + AddElement(bitmapContainer); + return bitmapContainer; + } + + public IImageContainer AddImageContainer(string filename, int x, int y) + { + ImageContainer bitmapContainer = new ImageContainer(this); + bitmapContainer.Load(filename); + bitmapContainer.Left = x; + bitmapContainer.Top = y; + AddElement(bitmapContainer); + return bitmapContainer; + } + + public IIconContainer AddIconContainer(Icon icon, int x, int y) + { + IconContainer iconContainer = new IconContainer(this) + { + Icon = icon, + Left = x, + Top = y + }; + AddElement(iconContainer); + return iconContainer; + } + + public IIconContainer AddIconContainer(string filename, int x, int y) + { + IconContainer iconContainer = new IconContainer(this); + iconContainer.Load(filename); + iconContainer.Left = x; + iconContainer.Top = y; + AddElement(iconContainer); + return iconContainer; + } + + public ICursorContainer AddCursorContainer(Cursor cursor, int x, int y) + { + CursorContainer cursorContainer = new CursorContainer(this) + { + Cursor = cursor, + Left = x, + Top = y + }; + AddElement(cursorContainer); + return cursorContainer; + } + + public ICursorContainer AddCursorContainer(string filename, int x, int y) + { + CursorContainer cursorContainer = new CursorContainer(this); + cursorContainer.Load(filename); + cursorContainer.Left = x; + cursorContainer.Top = y; + AddElement(cursorContainer); + return cursorContainer; + } + + public ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, + Color fillColor) + { + TextContainer textContainer = new TextContainer(this) + { + Text = text, + Left = x, + Top = y + }; + textContainer.SetFieldValue(FieldType.FONT_FAMILY, family.Name); + textContainer.SetFieldValue(FieldType.FONT_BOLD, bold); + textContainer.SetFieldValue(FieldType.FONT_ITALIC, italic); + textContainer.SetFieldValue(FieldType.FONT_SIZE, size); + textContainer.SetFieldValue(FieldType.FILL_COLOR, fillColor); + textContainer.SetFieldValue(FieldType.LINE_COLOR, color); + textContainer.SetFieldValue(FieldType.LINE_THICKNESS, borderSize); + textContainer.SetFieldValue(FieldType.SHADOW, shadow); + // Make sure the Text fits + textContainer.FitToText(); + + //AggregatedProperties.UpdateElement(textContainer); + AddElement(textContainer); + return textContainer; + } + + #endregion + + #region DragDrop + + private void OnDragEnter(object sender, DragEventArgs e) + { + if (LOG.IsDebugEnabled) + { + LOG.Debug("DragEnter got following formats: "); + foreach (string format in ClipboardHelper.GetFormats(e.Data)) + { + LOG.Debug(format); + } + } + + if ((e.AllowedEffect & DragDropEffects.Copy) != DragDropEffects.Copy) + { + e.Effect = DragDropEffects.None; + } + else + { + if (ClipboardHelper.ContainsImage(e.Data) || ClipboardHelper.ContainsFormat(e.Data, "DragImageBits")) + { + e.Effect = DragDropEffects.Copy; + } + else + { + e.Effect = DragDropEffects.None; + } + } + } + + /// + /// This will help to fit the container to the surface + /// + /// IDrawableContainer + private void FitContainer(IDrawableContainer drawableContainer) + { + double factor = 1; + if (drawableContainer.Width > this.Width) + { + factor = drawableContainer.Width / (double)Width; + } + if (drawableContainer.Height > this.Height) + { + var otherFactor = drawableContainer.Height / (double)Height; + factor = Math.Max(factor, otherFactor); + } + + drawableContainer.Width = (int)(drawableContainer.Width / factor); + drawableContainer.Height = (int)(drawableContainer.Height / factor); + } + + /// + /// Handle the drag/drop + /// + /// + /// + private void OnDragDrop(object sender, DragEventArgs e) + { + NativePoint mouse = PointToClient(new NativePoint(e.X, e.Y)); + if (e.Data.GetDataPresent("Text")) + { + string possibleUrl = ClipboardHelper.GetText(e.Data); + // Test if it's an url and try to download the image so we have it in the original form + if (possibleUrl != null && possibleUrl.StartsWith("http")) + { + var drawableContainer = NetworkHelper.DownloadImageAsDrawableContainer(possibleUrl); + if (drawableContainer != null) + { + drawableContainer.Left = Location.X; + drawableContainer.Top = Location.Y; + FitContainer(drawableContainer); + AddElement(drawableContainer); + return; + } + } + } + + foreach (var drawableContainer in ClipboardHelper.GetDrawables(e.Data)) + { + drawableContainer.Left = mouse.X; + drawableContainer.Top = mouse.Y; + FitContainer(drawableContainer); + AddElement(drawableContainer); + mouse = mouse.Offset(10, 10); + } + } + + #endregion + + /// + /// Auto crop the image + /// + /// NativeRect with optional area to find a crop region + /// true if cropped + public bool AutoCrop(NativeRect? cropArea = null) + { + NativeRect cropRectangle; + using (Image tmpImage = GetImageForExport()) + { + cropRectangle = ImageHelper.FindAutoCropRectangle(tmpImage, conf.AutoCropDifference, cropArea); + } + + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.AutoCrop)) + { + return false; + } + + DeselectAllElements(); + // Maybe a bit obscure, but the following line creates a drop container + // It's available as "undrawnElement" + DrawingMode = DrawingModes.Crop; + _undrawnElement.Left = cropRectangle.X; + _undrawnElement.Top = cropRectangle.Y; + _undrawnElement.Width = cropRectangle.Width; + _undrawnElement.Height = cropRectangle.Height; + _undrawnElement.Status = EditStatus.UNDRAWN; + AddElement(_undrawnElement); + SelectElement(_undrawnElement); + _drawingElement = null; + _undrawnElement = null; + return true; + } + + /// + /// A simple clear + /// + /// The color for the background + public void Clear(Color newColor) + { + //create a blank bitmap the same size as original + Bitmap newBitmap = ImageHelper.CreateEmptyLike(Image, newColor); + if (newBitmap == null) return; + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, null), false); + SetImage(newBitmap, false); + Invalidate(); + } + + /// + /// Apply a bitmap effect to the surface + /// + /// + public void ApplyBitmapEffect(IEffect effect) + { + BackgroundForm backgroundForm = new BackgroundForm("Effect", "Please wait"); + backgroundForm.Show(); + Application.DoEvents(); + try + { + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + Matrix matrix = new Matrix(); + Image newImage = ImageHelper.ApplyEffect(Image, effect, matrix); + if (newImage != null) + { + // Make sure the elements move according to the offset the effect made the bitmap move + _elements.Transform(matrix); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + SetImage(newImage, false); + Invalidate(); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + } + else + { + // clean up matrix, as it hasn't been used in the undo stack. + matrix.Dispose(); + } + } + finally + { + // Always close the background form + backgroundForm.CloseDialog(); + } + } + + /// + /// check if a crop is possible + /// + /// Rectangle adapted to the dimensions of the image + /// CropModes + /// true if this is possible + public bool IsCropPossible(ref NativeRect cropRectangle, CropContainer.CropModes cropMode) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, cropRectangle.Height).Normalize(); + //Fitting the rectangle to the dimensions of the image + if (cropRectangle.Left < 0) + { + cropRectangle = new NativeRect(0, cropRectangle.Top, cropRectangle.Width + cropRectangle.Left, cropRectangle.Height); + } + + if (cropRectangle.Top < 0) + { + cropRectangle = new NativeRect(cropRectangle.Left, 0, cropRectangle.Width, cropRectangle.Height + cropRectangle.Top); + } + + if (cropRectangle.Left + cropRectangle.Width > Image.Width) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, Image.Width - cropRectangle.Left, cropRectangle.Height); + } + + if (cropRectangle.Top + cropRectangle.Height > Image.Height) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Image.Height - cropRectangle.Top); + } + + // special condition for vertical + if(cropMode == CropContainer.CropModes.Vertical && cropRectangle.Width == Image.Width) + { + //crop out the hole image is not allowed + return false; + } + + // special condition for vertical + if (cropMode == CropContainer.CropModes.Horizontal && cropRectangle.Height == Image.Height) + { + //crop out the hole image is not allowed + return false; + } + + //condition for all other crop modes + if (cropRectangle.Height > 0 && cropRectangle.Width > 0) + { + return true; + } + + return false; + } + + /// + /// Use to send any registered SurfaceMessageEventHandler a message, e.g. used for the notification area + /// + /// Who send + /// Type of message + /// Message itself + public void SendMessageEvent(object source, SurfaceMessageTyp messageType, string message) + { + if (_surfaceMessage == null) return; + + var eventArgs = new SurfaceMessageEventArgs + { + Message = message, + MessageType = messageType, + Surface = this + }; + _surfaceMessage(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the foreground color + /// + /// Who send + /// new color + private void UpdateForegroundColorEvent(object source, Color color) + { + if (_foregroundColorChanged == null) return; + + var eventArgs = new SurfaceForegroundColorEventArgs + { + Color = color, + }; + _foregroundColorChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the background color + /// + /// Who send + /// new color + private void UpdateBackgroundColorEvent(object source, Color color) + { + if (_lineThicknessChanged == null) return; + + var eventArgs = new SurfaceBackgroundColorEventArgs + { + Color = color, + }; + _backgroundColorChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the line thickness + /// + /// Who send + /// new thickness + private void UpdateLineThicknessEvent(object source, int thickness) + { + if (_lineThicknessChanged == null) return; + + var eventArgs = new SurfaceLineThicknessEventArgs + { + Thickness = thickness, + }; + _lineThicknessChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing the key to show/hide the shadow + /// + /// Who send + /// has shadow + private void UpdateShadowEvent(object source, bool hasShadow) + { + if (_shadowChanged == null) return; + + var eventArgs = new SurfaceShadowEventArgs + { + HasShadow = hasShadow, + }; + _shadowChanged(source, eventArgs); + } + + /// + /// Crop the surface + /// + /// NativeRect that remains + /// bool + public bool ApplyCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Default)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + Bitmap tmpImage; + // Make sure we have information, this this fails + try + { + tmpImage = ImageHelper.CloneArea(Image, cropRectangle, PixelFormat.DontCare); + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + + var matrix = new Matrix(); + matrix.Translate(-cropRectangle.Left, -cropRectangle.Top, MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(tmpImage, false); + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, tmpImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// Crop out the surface + /// Splits the image in 3 parts(top, middle, bottom). Crop out the middle and joins top and bottom. + /// + /// NativeRect of the middle part + /// bool + private bool ApplyHorizontalCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Horizontal)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + var topRectangle = new NativeRect(0, 0, Image.Size.Width, cropRectangle.Top); + var bottomRectangle = new NativeRect(0, cropRectangle.Top + cropRectangle.Height, Image.Size.Width, Image.Size.Height - cropRectangle.Top - cropRectangle.Height); + + Bitmap newImage; + try + { + newImage = new Bitmap(Image.Size.Width, Image.Size.Height - cropRectangle.Height); + + using var graphics = Graphics.FromImage(newImage); + + var insertPositionTop = 0; + if (topRectangle.Height > 0) + { + graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, topRectangle.Width, topRectangle.Height), topRectangle, GraphicsUnit.Pixel); + insertPositionTop += topRectangle.Height; + } + if (bottomRectangle.Height > 0) + { + graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, bottomRectangle.Width, bottomRectangle.Height), bottomRectangle, GraphicsUnit.Pixel); + } + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + var matrix = new Matrix(); + matrix.Translate(0, -(cropRectangle.Top + cropRectangle.Height), MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(newImage, false); + + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// Crop out the surface + /// Splits the image in 3 parts(left, middle, right). Crop out the middle and joins top and bottom. + /// + /// NativeRect of the middle part + /// bool + private bool ApplyVerticalCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Vertical)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + var leftRectangle = new NativeRect(0, 0, cropRectangle.Left, Image.Size.Height); + var rightRectangle = new NativeRect(cropRectangle.Left + cropRectangle.Width, 0, Image.Size.Width - cropRectangle.Width - cropRectangle.Left, Image.Size.Height); + Bitmap newImage; + try + { + newImage = new Bitmap(Image.Size.Width - cropRectangle.Width, Image.Size.Height); + + using var graphics = Graphics.FromImage(newImage); + + var insertPositionLeft = 0; + if (leftRectangle.Width > 0) + { + graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, leftRectangle.Width, leftRectangle.Height), leftRectangle , GraphicsUnit.Pixel); + insertPositionLeft += leftRectangle.Width; + } + + if (rightRectangle.Width > 0) + { + graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, rightRectangle.Width, rightRectangle.Height), rightRectangle, GraphicsUnit.Pixel); + } + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + var matrix = new Matrix(); + matrix.Translate(-cropRectangle.Left - cropRectangle.Width, 0, MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(newImage, false); + + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// The background here is the captured image. + /// This is called from the SurfaceBackgroundChangeMemento. + /// + /// + /// + public void UndoBackgroundChange(Image previous, Matrix matrix) + { + SetImage(previous, false); + if (matrix != null) + { + _elements.Transform(matrix); + } + + _surfaceSizeChanged?.Invoke(this, null); + Invalidate(); + } + + /// + /// Check if an adorner was "hit", and change the cursor if so + /// + /// MouseEventArgs + /// IAdorner + private IAdorner FindActiveAdorner(MouseEventArgs mouseEventArgs) + { + foreach (IDrawableContainer drawableContainer in selectedElements) + { + foreach (IAdorner adorner in drawableContainer.Adorners) + { + if (!adorner.IsActive && !adorner.HitTest(mouseEventArgs.Location)) continue; + + if (adorner.Cursor != null) + { + Cursor = adorner.Cursor; + } + + return adorner; + } + } + + return null; + } + + /// + /// Translate mouse coordinates as if they were applied directly to unscaled image. + /// + private MouseEventArgs InverseZoomMouseCoordinates(MouseEventArgs e) + => new MouseEventArgs(e.Button, e.Clicks, (int) (e.X / _zoomFactor), (int) (e.Y / _zoomFactor), e.Delta); + + /// + /// This event handler is called when someone presses the mouse on a surface. + /// + /// + /// + private void SurfaceMouseDown(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseDown(sender, e); + return; + } + + _mouseStart = e.Location; + + // check contextmenu + if (e.Button == MouseButtons.Right) + { + IDrawableContainerList selectedList = null; + if (selectedElements != null && selectedElements.Count > 0) + { + selectedList = selectedElements; + } + else + { + // Single element + IDrawableContainer rightClickedContainer = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); + if (rightClickedContainer != null) + { + selectedList = new DrawableContainerList(ID) + { + rightClickedContainer + }; + } + } + + if (selectedList != null && selectedList.Count > 0) + { + selectedList.ShowContextMenu(e, this); + } + + return; + } + + _mouseDown = true; + _isSurfaceMoveMadeUndoable = false; + + if (_cropContainer != null && ((_undrawnElement == null) || (_undrawnElement != null && DrawingMode != DrawingModes.Crop))) + { + RemoveElement(_cropContainer, false); + _cropContainer = null; + _drawingElement = null; + } + + if (_drawingElement == null && DrawingMode != DrawingModes.None) + { + if (_undrawnElement == null) + { + DeselectAllElements(); + if (_undrawnElement == null) + { + CreateUndrawnElement(); + } + } + + _drawingElement = _undrawnElement; + // if a new element has been drawn, set location and register it + if (_drawingElement != null) + { + if (_undrawnElement != null) + { + _drawingElement.Status = _undrawnElement.DefaultEditMode; + } + + if (!_drawingElement.HandleMouseDown(_mouseStart.X, _mouseStart.Y)) + { + _drawingElement.Left = _mouseStart.X; + _drawingElement.Top = _mouseStart.Y; + } + + AddElement(_drawingElement); + _drawingElement.Selected = true; + } + + _undrawnElement = null; + } + else + { + // check whether an existing element was clicked + // we save mouse down element separately from selectedElements (checked on mouse up), + // since it could be moved around before it is actually selected + _mouseDownElement = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); + + if (_mouseDownElement != null) + { + _mouseDownElement.Status = EditStatus.MOVING; + } + } + } + + /// + /// This event handle is called when the mouse button is unpressed + /// + /// + /// + private void SurfaceMouseUp(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseUp(sender, e); + return; + } + + Point currentMouse = new Point(e.X, e.Y); + + _elements.Status = EditStatus.IDLE; + if (_mouseDownElement != null) + { + _mouseDownElement.Status = EditStatus.IDLE; + } + + _mouseDown = false; + _mouseDownElement = null; + if (DrawingMode == DrawingModes.None) + { + // check whether an existing element was clicked + IDrawableContainer element = _elements.ClickableElementAt(currentMouse.X, currentMouse.Y); + bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; + if (element != null) + { + element.Invalidate(); + bool alreadySelected = selectedElements.Contains(element); + if (shiftModifier) + { + if (alreadySelected) + { + DeselectElement(element); + } + else + { + SelectElement(element); + } + } + else + { + if (!alreadySelected) + { + DeselectAllElements(); + SelectElement(element); + } + } + } + else if (!shiftModifier) + { + DeselectAllElements(); + } + } + + if (selectedElements.Count > 0) + { + selectedElements.Invalidate(); + selectedElements.Selected = true; + } + + if (_drawingElement != null) + { + if (!_drawingElement.InitContent()) + { + _elements.Remove(_drawingElement); + _drawingElement.Invalidate(); + } + else + { + _drawingElement.HandleMouseUp(currentMouse.X, currentMouse.Y); + _drawingElement.Invalidate(); + if (Math.Abs(_drawingElement.Width) < 5 && Math.Abs(_drawingElement.Height) < 5) + { + _drawingElement.Width = 25; + _drawingElement.Height = 25; + } + + SelectElement(_drawingElement); + _drawingElement.Selected = true; + } + + _drawingElement = null; + } + } + + /// + /// This event handler is called when the mouse moves over the surface + /// + /// + /// + private void SurfaceMouseMove(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseMove(sender, e); + return; + } + + Point currentMouse = e.Location; + + Cursor = DrawingMode != DrawingModes.None ? Cursors.Cross : Cursors.Default; + + if (!_mouseDown) return; + + if (_mouseDownElement != null) + { + // an element is currently dragged + _mouseDownElement.Invalidate(); + selectedElements.Invalidate(); + // Move the element + if (_mouseDownElement.Selected) + { + if (!_isSurfaceMoveMadeUndoable) + { + // Only allow one undoable per mouse-down/move/up "cycle" + _isSurfaceMoveMadeUndoable = true; + selectedElements.MakeBoundsChangeUndoable(false); + } + + // dragged element has been selected before -> move all + selectedElements.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); + } + else + { + if (!_isSurfaceMoveMadeUndoable) + { + // Only allow one undoable per mouse-down/move/up "cycle" + _isSurfaceMoveMadeUndoable = true; + _mouseDownElement.MakeBoundsChangeUndoable(false); + } + + // dragged element is not among selected elements -> just move dragged one + _mouseDownElement.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); + } + + _mouseStart = currentMouse; + _mouseDownElement.Invalidate(); + _modified = true; + } + else if (_drawingElement != null) + { + _drawingElement.HandleMouseMove(currentMouse.X, currentMouse.Y); + _modified = true; + } + } + + /// + /// This event handler is called when the surface is double clicked. + /// + /// + /// + private void SurfaceDoubleClick(object sender, MouseEventArgs e) + { + selectedElements.OnDoubleClick(); + selectedElements.Invalidate(); + } + + /// + /// Privately used to get the rendered image with all the elements on it. + /// + /// + /// + private Image GetImage(RenderMode renderMode) + { + // Generate a copy of the original image with a dpi equal to the default... + Bitmap clone = ImageHelper.Clone(_image, PixelFormat.DontCare); + // otherwise we would have a problem drawing the image to the surface... :( + using (Graphics graphics = Graphics.FromImage(clone)) + { + // Do not set the following, the containers need to decide themselves + //graphics.SmoothingMode = SmoothingMode.HighQuality; + //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + //graphics.CompositingQuality = CompositingQuality.HighQuality; + //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + _elements.Draw(graphics, clone, renderMode, new Rectangle(Point.Empty, clone.Size)); + } + + return clone; + } + + /// + /// This returns the image "result" of this surface, with all the elements rendered on it. + /// + /// + public Image GetImageForExport() + { + return GetImage(RenderMode.EXPORT); + } + + private static NativeRect ZoomClipRectangle(NativeRect rc, double scale, int inflateAmount = 0) + { + rc = new NativeRect( + (int) (rc.X * scale), + (int) (rc.Y * scale), + (int) (rc.Width * scale) + 1, + (int) (rc.Height * scale) + 1 + ); + if (scale > 1) + { + inflateAmount = (int) (inflateAmount * scale); + } + + return rc.Inflate(inflateAmount, inflateAmount); + } + + public void InvalidateElements(NativeRect rc) + => Invalidate(ZoomClipRectangle(rc, _zoomFactor, 1)); + + /// + /// This is the event handler for the Paint Event, try to draw as little as possible! + /// + /// + /// PaintEventArgs + private void SurfacePaint(object sender, PaintEventArgs paintEventArgs) + { + Graphics targetGraphics = paintEventArgs.Graphics; + NativeRect targetClipRectangle = paintEventArgs.ClipRectangle; + if (targetClipRectangle.IsEmpty) + { + LOG.Debug("Empty cliprectangle??"); + return; + } + + // Correction to prevent rounding errors at certain zoom levels. + // When zooming to N/M, clip rectangle top and left coordinates should be multiples of N. + if (_zoomFactor.Numerator > 1 && _zoomFactor.Denominator > 1) + { + int horizontalCorrection = targetClipRectangle.Left % (int) _zoomFactor.Numerator; + int verticalCorrection = targetClipRectangle.Top % (int) _zoomFactor.Numerator; + if (horizontalCorrection != 0) + { + targetClipRectangle = targetClipRectangle + .ChangeX(targetClipRectangle.X - horizontalCorrection) + .ChangeWidth(targetClipRectangle.Width + horizontalCorrection); + } + + if (verticalCorrection != 0) + { + targetClipRectangle = targetClipRectangle + .ChangeY(targetClipRectangle.Y - verticalCorrection) + .ChangeHeight(targetClipRectangle.Height + verticalCorrection); + } + } + + NativeRect imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); + + if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) + { + if (_buffer != null) + { + if (_buffer.Width != Image.Width || _buffer.Height != Image.Height || _buffer.PixelFormat != Image.PixelFormat) + { + _buffer.Dispose(); + _buffer = null; + } + } + + if (_buffer == null) + { + _buffer = ImageHelper.CreateEmpty(Image.Width, Image.Height, Image.PixelFormat, Color.Empty, Image.HorizontalResolution, Image.VerticalResolution); + LOG.DebugFormat("Created buffer with size: {0}x{1}", Image.Width, Image.Height); + } + + // Elements might need the bitmap, so we copy the part we need + using (Graphics graphics = Graphics.FromImage(_buffer)) + { + // do not set the following, the containers need to decide this themselves! + //graphics.SmoothingMode = SmoothingMode.HighQuality; + //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + //graphics.CompositingQuality = CompositingQuality.HighQuality; + //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + DrawBackground(graphics, imageClipRectangle); + graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); + _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); + } + + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + else + { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + if (_zoomFactor > Fraction.Identity) + { + DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); + } + else + { + DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); + } + + targetGraphics.ResetTransform(); + } + } + else + { + DrawBackground(targetGraphics, targetClipRectangle); + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + } + else + { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + DrawSmoothImage(targetGraphics, Image, imageClipRectangle); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + targetGraphics.ResetTransform(); + } + } + + // No clipping for the adorners + targetGraphics.ResetClip(); + // Draw adorners last + foreach (var drawableContainer in selectedElements) + { + foreach (var adorner in drawableContainer.Adorners) + { + adorner.Paint(paintEventArgs); + } + } + } + + private void DrawSmoothImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.HighQuality; + targetGraphics.InterpolationMode = InterpolationMode.HighQualityBilinear; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + + private void DrawSharpImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.None; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + + private void DrawBackground(Graphics targetGraphics, NativeRect clipRectangle) + { + // check if we need to draw the checkerboard + if (Image.IsAlphaPixelFormat(Image.PixelFormat) && _transparencyBackgroundBrush != null) + { + targetGraphics.FillRectangle(_transparencyBackgroundBrush, clipRectangle); + } + else + { + targetGraphics.Clear(BackColor); + } + } + + /// + /// Draw a checkboard when capturing with transparency + /// + /// PaintEventArgs + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + /// + /// Add a new element to the surface + /// + /// the new element + /// true if the adding should be undoable + /// true if invalidate needs to be called + public void AddElement(IDrawableContainer element, bool makeUndoable = true, bool invalidate = true) + { + _elements.Add(element); + if (element is DrawableContainer container) + { + container.FieldChanged += Element_FieldChanged; + } + + element.Parent = this; + if (element.Status == EditStatus.UNDRAWN) + { + element.Status = EditStatus.IDLE; + } + + if (element.Selected) + { + // Use false, as the element is invalidated when invalidate == true anyway + SelectElement(element, false); + } + + if (invalidate) + { + element.Invalidate(); + } + + if (makeUndoable && element.IsUndoable) + { + MakeUndoable(new AddElementMemento(this, element), false); + } + + _modified = true; + } + + /// + /// Remove the list of elements + /// + /// IDrawableContainerList + /// flag specifying if the remove needs to be undoable + public void RemoveElements(IDrawableContainerList elementsToRemove, bool makeUndoable = true) + { + // fix potential issues with iterating a changing list + DrawableContainerList cloned = new DrawableContainerList(elementsToRemove); + + if (makeUndoable) + { + // Take all containers to make undoable + var undoableContainers = elementsToRemove.Where(c => c.IsUndoable).ToList(); + if (undoableContainers.Any()) + { + var undoableContainerList = new DrawableContainerList(undoableContainers); + MakeUndoable(new DeleteElementsMemento(this, undoableContainerList), false); + } + } + + SuspendLayout(); + foreach (var drawableContainer in cloned) + { + RemoveElement(drawableContainer, false, false, false); + } + + ResumeLayout(); + Invalidate(); + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = cloned + }; + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Remove an element of the elements list + /// + /// Element to remove + /// flag specifying if the remove needs to be undoable + /// flag specifying if an surface invalidate needs to be called + /// false to skip event generation + public void RemoveElement(IDrawableContainer elementToRemove, bool makeUndoable = true, bool invalidate = true, bool generateEvents = true) + { + DeselectElement(elementToRemove, generateEvents); + _elements.Remove(elementToRemove); + if (elementToRemove is DrawableContainer element) + { + element.FieldChanged -= Element_FieldChanged; + } + + if (elementToRemove != null) + { + elementToRemove.Parent = null; + } + + // Do not dispose, the memento should!! element.Dispose(); + if (invalidate) + { + Invalidate(); + } + + if (makeUndoable && elementToRemove is { IsUndoable: true }) + { + MakeUndoable(new DeleteElementMemento(this, elementToRemove), false); + } + + _modified = true; + } + + /// + /// Add the supplied elements to the surface + /// + /// DrawableContainerList + /// true if the adding should be undoable + public void AddElements(IDrawableContainerList elementsToAdd, bool makeUndoable = true) + { + // fix potential issues with iterating a changing list + DrawableContainerList cloned = new DrawableContainerList(elementsToAdd); + if (makeUndoable) + { + // Take all containers to make undoable + var undoableContainers = elementsToAdd.Where(c => c.IsUndoable).ToList(); + if (undoableContainers.Any()) + { + var undoableContainerList = new DrawableContainerList(undoableContainers); + MakeUndoable(new AddElementsMemento(this, undoableContainerList), false); + } + } + + SuspendLayout(); + foreach (var element in cloned) + { + element.Selected = true; + AddElement(element, false, false); + } + + ResumeLayout(); + Invalidate(); + } + + /// + /// Returns if this surface has selected elements + /// + /// bool + public bool HasSelectedElements => selectedElements is { Count: > 0 }; + + /// + /// Provides the selected elements + /// + public IDrawableContainerList SelectedElements => selectedElements; + + /// + /// Remove all the selected elements + /// + public void RemoveSelectedElements() + { + if (!HasSelectedElements) return; + + // As RemoveElement will remove the element from the selectedElements list we need to copy the element to another list. + RemoveElements(selectedElements); + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs(); + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Cut the selected elements from the surface to the clipboard + /// + public void CutSelectedElements() + { + if (!HasSelectedElements) return; + ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); + RemoveSelectedElements(); + } + + /// + /// Copy the selected elements to the clipboard + /// + public void CopySelectedElements() + { + if (!HasSelectedElements) return; + ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); + } + + /// + /// This method is called to confirm/cancel. + /// Called when pressing enter or using the "check" in the editor. + /// redirects to the specialized confirm/cancel method + /// + /// bool + public void Confirm(bool confirm) + { + if (DrawingMode == DrawingModes.Crop) + { + ConfirmCrop(confirm); + } + else + { + ConfirmSelectedConfirmableElements(confirm); + } + } + + /// + /// This method is called to confirm/cancel "confirmable" elements + /// Called when pressing enter or using the "check" in the editor. + ///
+ /// For crop-container there is a dedicated method . + ///
+ /// bool + public void ConfirmSelectedConfirmableElements(bool confirm) + { + // create new collection so that we can iterate safely (selectedElements might change due with confirm/cancel) + List selectedDCs = new List(selectedElements); + foreach (IDrawableContainer dc in selectedDCs.Where(c => c.IsConfirmable)) + { + throw new NotImplementedException($"No confirm/cancel defined for Container type {dc.GetType()}"); + } + + // maybe the undo button has to be enabled + _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); + } + + /// + /// This method is called to confirm/cancel the crop-container. + /// Called when pressing enter or using the "check" in the editor. + /// + /// bool + public void ConfirmCrop(bool confirm) + { + if (_cropContainer is not CropContainer e) return; + + if (confirm && selectedElements.Count > 0) + { + // No undo memento for the cropcontainer itself, only for the effect + RemoveElement(_cropContainer, false); + + _ = e.GetFieldValue(FieldType.CROPMODE) switch + { + CropContainer.CropModes.Horizontal => ApplyHorizontalCrop(_cropContainer.Bounds), + CropContainer.CropModes.Vertical => ApplyVerticalCrop(_cropContainer.Bounds), + _ => ApplyCrop(_cropContainer.Bounds) + }; + + _cropContainer.Dispose(); + _cropContainer = null; + } + else + { + RemoveCropContainer(); + } + + DrawingMode = DrawingModes.None; + + // maybe the undo button has to be enabled + _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); + } + + public void RemoveCropContainer() + { + if (_cropContainer == null) return; + + RemoveElement(_cropContainer, false); + _cropContainer.Dispose(); + _cropContainer = null; + } + + /// + /// Paste all the elements that are on the clipboard + /// + public void PasteElementFromClipboard() + { + IDataObject clipboard = ClipboardHelper.GetDataObject(); + + var formats = ClipboardHelper.GetFormats(clipboard); + if (formats == null || formats.Count == 0) + { + return; + } + + if (LOG.IsDebugEnabled) + { + LOG.Debug("List of clipboard formats available for pasting:"); + foreach (string format in formats) + { + LOG.Debug("\tgot format: " + format); + } + } + + if (formats.Contains(typeof(IDrawableContainerList).FullName)) + { + IDrawableContainerList dcs = (IDrawableContainerList) ClipboardHelper.GetFromDataObject(clipboard, typeof(IDrawableContainerList)); + if (dcs != null) + { + // Make element(s) only move 10,10 if the surface is the same + bool isSameSurface = (dcs.ParentID == _uniqueId); + dcs.Parent = this; + var moveOffset = isSameSurface ? new NativePoint(10, 10) : NativePoint.Empty; + // Here a fix for bug #1475, first calculate the bounds of the complete IDrawableContainerList + NativeRect drawableContainerListBounds = NativeRect.Empty; + foreach (var element in dcs) + { + drawableContainerListBounds = drawableContainerListBounds == NativeRect.Empty + ? element.DrawingBounds + : drawableContainerListBounds.Union(element.DrawingBounds); + } + + // And find a location inside the target surface to paste to + bool containersCanFit = drawableContainerListBounds.Width < Bounds.Width && drawableContainerListBounds.Height < Bounds.Height; + if (!containersCanFit) + { + NativePoint containersLocation = drawableContainerListBounds.Location; + containersLocation.Offset(moveOffset); + if (!Bounds.Contains(containersLocation)) + { + // Easy fix for same surface + moveOffset = isSameSurface + ? new NativePoint(-10, -10) + : new NativePoint(-drawableContainerListBounds.Location.X + 10, -drawableContainerListBounds.Location.Y + 10); + } + } + else + { + NativeRect moveContainerListBounds = drawableContainerListBounds.Offset(moveOffset); + // check if the element is inside + if (!Bounds.Contains(moveContainerListBounds)) + { + // Easy fix for same surface + if (isSameSurface) + { + moveOffset = new Point(-10, -10); + } + else + { + // For different surface, which is most likely smaller + int offsetX = 0; + int offsetY = 0; + if (drawableContainerListBounds.Right > Bounds.Right) + { + offsetX = Bounds.Right - drawableContainerListBounds.Right; + // Correction for the correction + if (drawableContainerListBounds.Left + offsetX < 0) + { + offsetX += Math.Abs(drawableContainerListBounds.Left + offsetX); + } + } + + if (drawableContainerListBounds.Bottom > Bounds.Bottom) + { + offsetY = Bounds.Bottom - drawableContainerListBounds.Bottom; + // Correction for the correction + if (drawableContainerListBounds.Top + offsetY < 0) + { + offsetY += Math.Abs(drawableContainerListBounds.Top + offsetY); + } + } + + moveOffset = new Point(offsetX, offsetY); + } + } + } + + dcs.MoveBy(moveOffset.X, moveOffset.Y); + AddElements(dcs); + FieldAggregator.BindElements(dcs); + DeselectAllElements(); + SelectElements(dcs); + } + } + else if (ClipboardHelper.ContainsImage(clipboard)) + { + NativePoint pasteLocation = GetPasteLocation(0.1f, 0.1f); + + foreach (var drawableContainer in ClipboardHelper.GetDrawables(clipboard)) + { + if (drawableContainer == null) continue; + DeselectAllElements(); + drawableContainer.Left = pasteLocation.X; + drawableContainer.Top = pasteLocation.Y; + AddElement(drawableContainer); + SelectElement(drawableContainer); + pasteLocation = pasteLocation.Offset(10, 10); + } + } + else if (ClipboardHelper.ContainsText(clipboard)) + { + NativePoint pasteLocation = GetPasteLocation(0.4f, 0.4f); + + string text = ClipboardHelper.GetText(clipboard); + if (text != null) + { + DeselectAllElements(); + ITextContainer textContainer = AddTextContainer(text, pasteLocation.X, pasteLocation.Y, + FontFamily.GenericSansSerif, 12f, false, false, false, 2, Color.Black, Color.Transparent); + SelectElement(textContainer); + } + } + } + + /// + /// Find a location to paste elements. + /// If mouse is over the surface - use it's position, otherwise use the visible area. + /// Return a point in image coordinate space. + /// + /// 0.0f for the left edge of visible area, 1.0f for the right edge of visible area. + /// 0.0f for the top edge of visible area, 1.0f for the bottom edge of visible area. + private NativePoint GetPasteLocation(float horizontalRatio = 0.5f, float verticalRatio = 0.5f) + { + var point = PointToClient(MousePosition); + var rc = GetVisibleRectangle(); + if (!rc.Contains(point)) + { + point = new NativePoint( + rc.Left + (int) (rc.Width * horizontalRatio), + rc.Top + (int) (rc.Height * verticalRatio) + ); + } + + return ToImageCoordinates(point); + } + + /// + /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). + /// + public NativeRect GetVisibleRectangle() + { + var bounds = Bounds; + var clientArea = Parent.ClientRectangle; + return new NativeRect( + Math.Max(0, -bounds.Left), + Math.Max(0, -bounds.Top), + clientArea.Width, + clientArea.Height + ); + } + + /// + /// Get the rectangle bounding all selected elements (in surface coordinates space), + /// or empty rectangle if nothing is selected. + /// + public NativeRect GetSelectionRectangle() + => ToSurfaceCoordinates(selectedElements.DrawingBounds); + + /// + /// Duplicate all the selected elements + /// + public void DuplicateSelectedElements() + { + LOG.DebugFormat("Duplicating {0} selected elements", selectedElements.Count); + IDrawableContainerList dcs = selectedElements.Clone(); + dcs.Parent = this; + dcs.MoveBy(10, 10); + AddElements(dcs); + DeselectAllElements(); + SelectElements(dcs); + } + + /// + /// Deselect the specified element + /// + /// IDrawableContainerList + /// false to skip event generation + public void DeselectElement(IDrawableContainer container, bool generateEvents = true) + { + container.Selected = false; + selectedElements.Remove(container); + FieldAggregator.UnbindElement(container); + if (generateEvents && _movingElementChanged != null) + { + var eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Deselect the specified elements + /// + /// IDrawableContainerList + public void DeselectElements(IDrawableContainerList elements) + { + if (elements.Count == 0) + { + return; + } + + while (elements.Count > 0) + { + var element = elements[0]; + DeselectElement(element, false); + } + + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + Invalidate(); + } + + /// + /// Deselect all the selected elements + /// + public void DeselectAllElements() + { + DeselectElements(selectedElements); + } + + /// + /// Select the supplied element + /// + /// + /// false to skip invalidation + /// false to skip event generation + public void SelectElement(IDrawableContainer container, bool invalidate = true, bool generateEvents = true) + { + if (selectedElements.Contains(container)) return; + + selectedElements.Add(container); + container.Selected = true; + FieldAggregator.BindElement(container); + if (generateEvents && _movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + if (invalidate) + { + container.Invalidate(); + } + } + + /// + /// Select all elements, this is called when Ctrl+A is pressed + /// + public void SelectAllElements() + { + SelectElements(_elements); + } + + /// + /// Select the supplied elements + /// + /// + public void SelectElements(IDrawableContainerList elements) + { + SuspendLayout(); + foreach (var drawableContainer in elements) + { + var element = (DrawableContainer) drawableContainer; + SelectElement(element, false, false); + } + + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + ResumeLayout(); + Invalidate(); + } + + /// + /// Process key presses on the surface, this is called from the editor (and NOT an override from the Control) + /// + /// Keys + /// false if no keys were processed + public bool ProcessCmdKey(Keys k) + { + if (selectedElements.Count <= 0 && k != Keys.Escape) return false; + + bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; + int px = shiftModifier ? 10 : 1; + NativePoint moveBy = NativePoint.Empty; + switch (k) + { + case Keys.Left: + case Keys.Left | Keys.Shift: + moveBy = new NativePoint(-px, 0); + break; + case Keys.Up: + case Keys.Up | Keys.Shift: + moveBy = new NativePoint(0, -px); + break; + case Keys.Right: + case Keys.Right | Keys.Shift: + moveBy = new NativePoint(px, 0); + break; + case Keys.Down: + case Keys.Down | Keys.Shift: + moveBy = new NativePoint(0, px); + break; + case Keys.PageUp: + PullElementsUp(); + break; + case Keys.PageDown: + PushElementsDown(); + break; + case Keys.Home: + PullElementsToTop(); + break; + case Keys.End: + PushElementsToBottom(); + break; + case Keys.Enter: + Confirm(true); + break; + case Keys.Escape: + Confirm(false); + break; + case Keys.D0 | Keys.Control: + case Keys.D0 | Keys.Shift | Keys.Control: + SetSelectedElementColor(shiftModifier ? Color.Orange : Color.Transparent, false, shiftModifier); + break; + case Keys.D1 | Keys.Control: + case Keys.D1 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Red, false, shiftModifier); + break; + case Keys.D2 | Keys.Control: + case Keys.D2 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Green, false, shiftModifier); + break; + case Keys.D3 | Keys.Control: + case Keys.D3 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Blue, false, shiftModifier); + break; + case Keys.D4 | Keys.Control: + case Keys.D4 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Cyan, false, shiftModifier); + break; + case Keys.D5 | Keys.Control: + case Keys.D5 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Magenta, false, shiftModifier); + break; + case Keys.D6 | Keys.Control: + case Keys.D6 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Yellow, false, shiftModifier); + break; + case Keys.D7 | Keys.Control: + case Keys.D7 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Black, false, shiftModifier); + break; + case Keys.D8 | Keys.Control: + case Keys.D8 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Gray, false, shiftModifier); + break; + case Keys.D9 | Keys.Control: + case Keys.D9 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.White, false, shiftModifier); + break; + case Keys.Add | Keys.Control: + case Keys.Add | Keys.Shift | Keys.Control: + ChangeLineThickness(shiftModifier ? 5 : 1); + break; + case Keys.Subtract | Keys.Control: + case Keys.Subtract | Keys.Shift | Keys.Control: + ChangeLineThickness(shiftModifier ? -5 : -1); + break; + case Keys.Divide | Keys.Control: + FlipShadow(); + break; + /*case Keys.Delete: + RemoveSelectedElements(); + break;*/ + default: + return false; + } + + if (moveBy != NativePoint.Empty) + { + selectedElements.MakeBoundsChangeUndoable(true); + selectedElements.MoveBy(moveBy.X, moveBy.Y); + } + + return true; + + } + + // for laptops without numPads, also allow shift modifier + private void SetSelectedElementColor(Color color, bool numPad, bool shift) + { + if (numPad || shift) + { + selectedElements.SetForegroundColor(color); + UpdateForegroundColorEvent(this, color); + } + else + { + selectedElements.SetBackgroundColor(color); + UpdateBackgroundColorEvent(this, color); + } + selectedElements.Invalidate(); + } + + private void ChangeLineThickness(int increaseBy) + { + var newThickness = selectedElements.IncreaseLineThickness(increaseBy); + UpdateLineThicknessEvent(this, newThickness); + selectedElements.Invalidate(); + } + + private void FlipShadow() + { + var shadow = selectedElements.FlipShadow(); + UpdateShadowEvent(this, shadow); + selectedElements.Invalidate(); + } + + /// + /// Property for accessing the elements on the surface + /// + public IDrawableContainerList Elements => _elements; + + /// + /// pulls selected elements up one level in hierarchy + /// + public void PullElementsUp() + { + _elements.PullElementsUp(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements up to top in hierarchy + /// + public void PullElementsToTop() + { + _elements.PullElementsToTop(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements down one level in hierarchy + /// + public void PushElementsDown() + { + _elements.PushElementsDown(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements down to bottom in hierarchy + /// + public void PushElementsToBottom() + { + _elements.PushElementsToBottom(selectedElements); + _elements.Invalidate(); + } + + /// + /// indicates whether the selected elements could be pulled up in hierarchy + /// + /// true if selected elements could be pulled up, false otherwise + public bool CanPullSelectionUp() + { + return _elements.CanPullUp(selectedElements); + } + + /// + /// indicates whether the selected elements could be pushed down in hierarchy + /// + /// true if selected elements could be pushed down, false otherwise + public bool CanPushSelectionDown() + { + return _elements.CanPushDown(selectedElements); + } + + private void Element_FieldChanged(object sender, FieldChangedEventArgs e) + { + selectedElements.HandleFieldChangedEvent(sender, e); + } + + public bool IsOnSurface(IDrawableContainer container) + { + return _elements.Contains(container); + } + + public NativePoint ToSurfaceCoordinates(NativePoint point) + { + Point[] points = + { + point + }; + _zoomMatrix.TransformPoints(points); + return points[0]; + } + + public NativeRect ToSurfaceCoordinates(NativeRect rc) + { + if (_zoomMatrix.IsIdentity) + { + return rc; + } + + Point[] points = + { + rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) + }; + _zoomMatrix.TransformPoints(points); + return new NativeRect( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); + } + + public NativePoint ToImageCoordinates(NativePoint point) + { + Point[] points = + { + point + }; + _inverseZoomMatrix.TransformPoints(points); + return points[0]; + } + + public NativeRect ToImageCoordinates(NativeRect rc) + { + if (_inverseZoomMatrix.IsIdentity) + { + return rc; + } + + Point[] points = + { + rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) + }; + _inverseZoomMatrix.TransformPoints(points); + return new NativeRect( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); + } + } } \ No newline at end of file From 56092c75e7266cff23bdd4fd0a08cd409864904c Mon Sep 17 00:00:00 2001 From: Nathan Brown Date: Wed, 28 May 2025 23:17:03 -0700 Subject: [PATCH 2/2] Remove the extra whitespace changes --- src/Greenshot.Editor/Drawing/Surface.cs | 5526 +++++++++++------------ 1 file changed, 2763 insertions(+), 2763 deletions(-) diff --git a/src/Greenshot.Editor/Drawing/Surface.cs b/src/Greenshot.Editor/Drawing/Surface.cs index cbdfc2733..fdb3771ec 100644 --- a/src/Greenshot.Editor/Drawing/Surface.cs +++ b/src/Greenshot.Editor/Drawing/Surface.cs @@ -1,2769 +1,2769 @@ -/* - * 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.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Formatters.Binary; +/* + * 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.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; using System.ServiceModel.Security; -using System.Windows.Forms; -using Dapplo.Windows.Common.Extensions; -using Dapplo.Windows.Common.Structs; -using Greenshot.Base.Controls; -using Greenshot.Base.Core; -using Greenshot.Base.Effects; -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.Fields; +using System.Windows.Forms; +using Dapplo.Windows.Common.Extensions; +using Dapplo.Windows.Common.Structs; +using Greenshot.Base.Controls; +using Greenshot.Base.Core; +using Greenshot.Base.Effects; +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.Fields; using Greenshot.Editor.Helpers; -using Greenshot.Editor.Memento; -using log4net; - -namespace Greenshot.Editor.Drawing -{ - /// - /// Description of Surface. - /// - public sealed class Surface : Control, ISurface, INotifyPropertyChanged - { - private static readonly ILog LOG = LogManager.GetLogger(typeof(Surface)); - private static readonly CoreConfiguration conf = IniConfig.GetIniSection(); - - // Property to identify the Surface ID - private Guid _uniqueId = Guid.NewGuid(); - - /// - /// This value is used to start counting the step labels - /// - private int _counterStart = 1; - - /// - /// The GUID of the surface - /// - public Guid ID - { - get => _uniqueId; - set => _uniqueId = value; - } - - /// - /// Event handlers (do not serialize!) - /// - [NonSerialized] private PropertyChangedEventHandler _propertyChanged; - - public event PropertyChangedEventHandler PropertyChanged - { - add => _propertyChanged += value; - remove => _propertyChanged -= value; - } - - [NonSerialized] private SurfaceElementEventHandler _movingElementChanged; - - public event SurfaceElementEventHandler MovingElementChanged - { - add => _movingElementChanged += value; - remove => _movingElementChanged -= value; - } - - [NonSerialized] private SurfaceDrawingModeEventHandler _drawingModeChanged; - - public event SurfaceDrawingModeEventHandler DrawingModeChanged - { - add => _drawingModeChanged += value; - remove => _drawingModeChanged -= value; - } - - [NonSerialized] private SurfaceSizeChangeEventHandler _surfaceSizeChanged; - - public event SurfaceSizeChangeEventHandler SurfaceSizeChanged - { - add => _surfaceSizeChanged += value; - remove => _surfaceSizeChanged -= value; - } - - [NonSerialized] private SurfaceMessageEventHandler _surfaceMessage; - - public event SurfaceMessageEventHandler SurfaceMessage - { - add => _surfaceMessage += value; - remove => _surfaceMessage -= value; - } - - [NonSerialized] private SurfaceForegroundColorEventHandler _foregroundColorChanged; - - public event SurfaceForegroundColorEventHandler ForegroundColorChanged - { - add => _foregroundColorChanged += value; - remove => _foregroundColorChanged -= value; - } - - [NonSerialized] private SurfaceBackgroundColorEventHandler _backgroundColorChanged; - - public event SurfaceBackgroundColorEventHandler BackgroundColorChanged - { - add => _backgroundColorChanged += value; - remove => _backgroundColorChanged -= value; - } - - [NonSerialized] private SurfaceLineThicknessEventHandler _lineThicknessChanged; - - public event SurfaceLineThicknessEventHandler LineThicknessChanged - { - add => _lineThicknessChanged += value; - remove => _lineThicknessChanged -= value; - } - - [NonSerialized] private SurfaceShadowEventHandler _shadowChanged; - - public event SurfaceShadowEventHandler ShadowChanged - { - add => _shadowChanged += value; - remove => _shadowChanged -= value; - } - - - [NonSerialized] private int _currentDpi = 96; - /// - /// The most recent DPI value that was used - /// - public int CurrentDpi - { - get => _currentDpi; - set => _currentDpi = value; - } - - /// - /// inUndoRedo makes sure we don't undo/redo while in a undo/redo action - /// - [NonSerialized] private bool _inUndoRedo; - - /// - /// Make only one surface move cycle undoable, see SurfaceMouseMove - /// - [NonSerialized] private bool _isSurfaceMoveMadeUndoable; - - /// - /// Undo/Redo stacks, should not be serialized as the file would be way to big - /// - [NonSerialized] private readonly Stack _undoStack = new Stack(); - - [NonSerialized] private readonly Stack _redoStack = new Stack(); - - /// - /// Last save location, do not serialize! - /// - [NonSerialized] private string _lastSaveFullPath; - - /// - /// current drawing mode, do not serialize! - /// - [NonSerialized] private DrawingModes _drawingMode = DrawingModes.None; - - /// - /// the keys-locked flag helps with focus issues - /// - [NonSerialized] private bool _keysLocked; - - /// - /// Location of the mouse-down (it "starts" here), do not serialize - /// - [NonSerialized] private Point _mouseStart = Point.Empty; - - /// - /// are we in a mouse down, do not serialize - /// - [NonSerialized] private bool _mouseDown; - - /// - /// The selected element for the mouse down, do not serialize - /// - [NonSerialized] private IDrawableContainer _mouseDownElement; - - /// - /// all selected elements, do not serialize - /// - [NonSerialized] private readonly IDrawableContainerList selectedElements; - - /// - /// the element we are drawing with, do not serialize - /// - [NonSerialized] private IDrawableContainer _drawingElement; - - /// - /// the element we want to draw with (not yet drawn), do not serialize - /// - [NonSerialized] private IDrawableContainer _undrawnElement; - - /// - /// the crop container, when cropping this is set, do not serialize - /// - [NonSerialized] private IDrawableContainer _cropContainer; - - /// - /// the brush which is used for transparent backgrounds, set by the editor, do not serialize - /// - [NonSerialized] private Brush _transparencyBackgroundBrush; - - /// - /// The buffer is only for drawing on it when using filters (to supply access) - /// This saves a lot of "create new bitmap" commands - /// Should not be serialized, as it's generated. - /// The actual bitmap is in the paintbox... - /// TODO: Check if this buffer is still needed! - /// - [NonSerialized] private Bitmap _buffer; - - /// - /// all stepLabels for the surface, needed with serialization - /// - private readonly List _stepLabels = new List(); - - public void AddStepLabel(StepLabelContainer stepLabel) - { - if (!_stepLabels.Contains(stepLabel)) - { - _stepLabels.Add(stepLabel); - } - } - - public void RemoveStepLabel(StepLabelContainer stepLabel) - { - _stepLabels.Remove(stepLabel); - } - - /// - /// The start value of the counter objects - /// - public int CounterStart - { - get => _counterStart; - set - { - if (_counterStart == value) - { - return; - } - - _counterStart = value; - Invalidate(); - _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CounterStart))); - } - } - - /// - /// Count all the VISIBLE steplabels in the surface, up to the supplied one - /// - /// can be null, if not the counting stops here - /// number of steplabels before the supplied container - public int CountStepLabels(IDrawableContainer stopAtContainer) - { - int number = CounterStart; - foreach (var possibleThis in _stepLabels) - { - if (possibleThis.Equals(stopAtContainer)) - { - break; - } - - if (IsOnSurface(possibleThis)) - { - number++; - } - } - - return number; - } - - /// - /// all elements on the surface, needed with serialization - /// - private readonly IDrawableContainerList _elements; - - /// - /// all elements on the surface, needed with serialization - /// - private IFieldAggregator _fieldAggregator; - - /// - /// the cursor container, needed with serialization as we need a direct acces to it. - /// - private IDrawableContainer _cursorContainer; - - /// - /// the modified flag specifies if the surface has had modifications after the last export. - /// Initial state is modified, as "it's not saved" - /// After serialization this should actually be "false" (the surface came from a stream) - /// For now we just serialize it... - /// - private bool _modified = true; - - /// - /// The image is the actual captured image, needed with serialization - /// - private Image _image; - - public Image Image - { - get => _image; - set - { - _image = value; - UpdateSize(); - } - } - - [NonSerialized] private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); - [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); - [NonSerialized] private Fraction _zoomFactor = Fraction.Identity; - - public Fraction ZoomFactor - { - get => _zoomFactor; - set - { - _zoomFactor = value; - var inverse = _zoomFactor.Inverse(); - _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); - _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); - UpdateSize(); - } - } - - - /// - /// Sets the surface size as zoomed image size. - /// - private void UpdateSize() - { - var size = _image.Size; - Size = new Size((int) (size.Width * _zoomFactor), (int) (size.Height * _zoomFactor)); - } - - /// - /// The field aggregator is that which is used to have access to all the fields inside the currently selected elements. - /// e.g. used to decided if and which line thickness is shown when multiple elements are selected. - /// - public IFieldAggregator FieldAggregator - { - get => _fieldAggregator; - set => _fieldAggregator = value; - } - - /// - /// The cursor container has it's own accessor so we can find and remove this (when needed) - /// - public IDrawableContainer CursorContainer => _cursorContainer; - - /// - /// A simple getter to ask if this surface has a cursor - /// - public bool HasCursor => _cursorContainer != null; - - /// - /// A simple helper method to remove the cursor from the surface - /// - public void RemoveCursor() - { - RemoveElement(_cursorContainer); - _cursorContainer = null; - } - - /// - /// The brush which is used to draw the transparent background - /// - public Brush TransparencyBackgroundBrush - { - get => _transparencyBackgroundBrush; - set => _transparencyBackgroundBrush = value; - } - - /// - /// Are the keys on this surface locked? - /// - public bool KeysLocked - { - get => _keysLocked; - set => _keysLocked = value; - } - - /// - /// Is this surface modified? This is only true if the surface has not been exported. - /// - public bool Modified - { - get => _modified; - set => _modified = value; - } - - /// - /// The DrawingMode property specifies the mode for drawing, more or less the element type. - /// - public DrawingModes DrawingMode - { - get => _drawingMode; - set - { - _drawingMode = value; - if (_drawingModeChanged != null) - { - SurfaceDrawingModeEventArgs eventArgs = new SurfaceDrawingModeEventArgs - { - DrawingMode = _drawingMode - }; - _drawingModeChanged.Invoke(this, eventArgs); - } - - DeselectAllElements(); - CreateUndrawnElement(); - } - } - - /// - /// Property for accessing the last save "full" path - /// - public string LastSaveFullPath - { - get => _lastSaveFullPath; - set => _lastSaveFullPath = value; - } - - /// - /// Property for accessing the URL to which the surface was recently uploaded - /// - public string UploadUrl { get; set; } - - /// - /// Property for accessing the capture details - /// - public ICaptureDetails CaptureDetails { get; set; } - - /// - /// Adjust UI elements to the supplied DPI settings - /// - /// - public void AdjustToDpi(int dpi) - { - CurrentDpi = dpi; - foreach (var element in this._elements) - { - element.AdjustToDpi(dpi); - } - } - - /// - /// Base Surface constructor - /// - public Surface() - { - _fieldAggregator = new FieldAggregator(this); - _elements = new DrawableContainerList(_uniqueId); - selectedElements = new DrawableContainerList(_uniqueId); - LOG.Debug("Creating surface!"); - MouseDown += SurfaceMouseDown; - MouseUp += SurfaceMouseUp; - MouseMove += SurfaceMouseMove; - MouseDoubleClick += SurfaceDoubleClick; - Paint += SurfacePaint; - AllowDrop = true; - DragDrop += OnDragDrop; - DragEnter += OnDragEnter; - // bind selected & elements to this, otherwise they can't inform of modifications - selectedElements.Parent = this; - _elements.Parent = this; - // Make sure we are visible - Visible = true; - TabStop = false; - // Enable double buffering - DoubleBuffered = true; - SetStyle( - ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.ContainerControl | ControlStyles.OptimizedDoubleBuffer | - ControlStyles.SupportsTransparentBackColor, true); - } - - /// - /// Private method, the current image is disposed the new one will stay. - /// - /// The new image - /// true if the old image needs to be disposed, when using undo this should not be true!! - private void SetImage(Image newImage, bool dispose) - { - // Dispose - if (_image != null && dispose) - { - _image.Dispose(); - } - - // Set new values - Image = newImage; - - _modified = true; - } - - /// - /// Surface constructor with an image - /// - /// - public Surface(Image newImage) : this() - { - LOG.DebugFormat("Got image with dimensions {0} and format {1}", newImage.Size, newImage.PixelFormat); - SetImage(newImage, true); - } - - /// - /// Surface contructor with a capture - /// - /// - public Surface(ICapture capture) : this(capture.Image) - { - // check if cursor is captured, and visible - if (capture.Cursor != null && capture.CursorVisible) - { - var cursorRect = new NativeRect(capture.CursorLocation, capture.Cursor.Size); - var captureRect = new NativeRect(NativePoint.Empty, capture.Image.Size); - // check if cursor is on the capture, otherwise we leave it out. - if (cursorRect.IntersectsWith(captureRect)) - { - _cursorContainer = AddIconContainer(capture.Cursor, capture.CursorLocation.X, capture.CursorLocation.Y); - SelectElement(_cursorContainer); - } - } - - // Make sure the image is NOT disposed, we took the reference directly into ourselves - ((Capture) capture).NullImage(); - - CaptureDetails = capture.CaptureDetails; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - LOG.Debug("Disposing surface!"); - if (_buffer != null) - { - _buffer.Dispose(); - _buffer = null; - } - - if (_transparencyBackgroundBrush != null) - { - _transparencyBackgroundBrush.Dispose(); - _transparencyBackgroundBrush = null; - } - - // Cleanup undo/redo stacks - while (_undoStack != null && _undoStack.Count > 0) - { - _undoStack.Pop().Dispose(); - } - - while (_redoStack != null && _redoStack.Count > 0) - { - _redoStack.Pop().Dispose(); - } - - foreach (IDrawableContainer container in _elements) - { - container.Dispose(); - } - - if (_undrawnElement != null) - { - _undrawnElement.Dispose(); - _undrawnElement = null; - } - - if (_cropContainer != null) - { - _cropContainer.Dispose(); - _cropContainer = null; - } - } - - base.Dispose(disposing); - } - - /// - /// Undo the last action - /// - public void Undo() - { - if (_undoStack.Count > 0) - { - _inUndoRedo = true; - IMemento top = _undoStack.Pop(); - _redoStack.Push(top.Restore()); - _inUndoRedo = false; - } - } - - /// - /// Undo an undo (=redo) - /// - public void Redo() - { - if (_redoStack.Count > 0) - { - _inUndoRedo = true; - IMemento top = _redoStack.Pop(); - _undoStack.Push(top.Restore()); - _inUndoRedo = false; - } - } - - /// - /// Returns if the surface can do a undo - /// - public bool CanUndo => _undoStack.Count > 0; - - /// - /// Returns if the surface can do a redo - /// - public bool CanRedo => _redoStack.Count > 0; - - /// - /// Get the language key for the undo action - /// - public LangKey UndoActionLanguageKey => LangKey.none; - - /// - /// Get the language key for redo action - /// - public LangKey RedoActionLanguageKey => LangKey.none; - - /// - /// Make an action undo-able - /// - /// The memento implementing the undo - /// Allow changes to be merged - public void MakeUndoable(IMemento memento, bool allowMerge) - { - if (_inUndoRedo) - { - throw new InvalidOperationException("Invoking do within an undo/redo action."); - } - - if (memento != null) - { - bool allowPush = true; - if (_undoStack.Count > 0 && allowMerge) - { - // Check if merge is possible - allowPush = !_undoStack.Peek().Merge(memento); - } - - if (allowPush) - { - // Clear the redo-stack and dispose - while (_redoStack.Count > 0) - { - _redoStack.Pop().Dispose(); - } - - _undoStack.Push(memento); - } - } - } - - /// - /// This saves the elements of this surface to a stream. - /// Is used to save a template of the complete surface - /// - /// - /// - public long SaveElementsToStream(Stream streamWrite) - { - long bytesWritten = 0; - try - { - long lengtBefore = streamWrite.Length; - BinaryFormatter binaryWrite = new BinaryFormatter(); - binaryWrite.Serialize(streamWrite, _elements); - bytesWritten = streamWrite.Length - lengtBefore; - } - catch (Exception e) - { - LOG.Error("Error serializing elements to stream.", e); - } - - return bytesWritten; - } - - /// - /// This loads elements from a stream, among others this is used to load a surface. - /// - /// - public void LoadElementsFromStream(Stream streamRead) - { - try - { - BinaryFormatter binaryRead = new BinaryFormatter(); - binaryRead.Binder = new BinaryFormatterHelper(); - IDrawableContainerList loadedElements = (IDrawableContainerList) binaryRead.Deserialize(streamRead); - loadedElements.Parent = this; - // Make sure the steplabels are sorted according to their number - _stepLabels.Sort((p1, p2) => p1.Number.CompareTo(p2.Number)); - DeselectAllElements(); - AddElements(loadedElements); - SelectElements(loadedElements); - FieldAggregator.BindElements(loadedElements); - } +using Greenshot.Editor.Memento; +using log4net; + +namespace Greenshot.Editor.Drawing +{ + /// + /// Description of Surface. + /// + public sealed class Surface : Control, ISurface, INotifyPropertyChanged + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(Surface)); + private static readonly CoreConfiguration conf = IniConfig.GetIniSection(); + + // Property to identify the Surface ID + private Guid _uniqueId = Guid.NewGuid(); + + /// + /// This value is used to start counting the step labels + /// + private int _counterStart = 1; + + /// + /// The GUID of the surface + /// + public Guid ID + { + get => _uniqueId; + set => _uniqueId = value; + } + + /// + /// Event handlers (do not serialize!) + /// + [NonSerialized] private PropertyChangedEventHandler _propertyChanged; + + public event PropertyChangedEventHandler PropertyChanged + { + add => _propertyChanged += value; + remove => _propertyChanged -= value; + } + + [NonSerialized] private SurfaceElementEventHandler _movingElementChanged; + + public event SurfaceElementEventHandler MovingElementChanged + { + add => _movingElementChanged += value; + remove => _movingElementChanged -= value; + } + + [NonSerialized] private SurfaceDrawingModeEventHandler _drawingModeChanged; + + public event SurfaceDrawingModeEventHandler DrawingModeChanged + { + add => _drawingModeChanged += value; + remove => _drawingModeChanged -= value; + } + + [NonSerialized] private SurfaceSizeChangeEventHandler _surfaceSizeChanged; + + public event SurfaceSizeChangeEventHandler SurfaceSizeChanged + { + add => _surfaceSizeChanged += value; + remove => _surfaceSizeChanged -= value; + } + + [NonSerialized] private SurfaceMessageEventHandler _surfaceMessage; + + public event SurfaceMessageEventHandler SurfaceMessage + { + add => _surfaceMessage += value; + remove => _surfaceMessage -= value; + } + + [NonSerialized] private SurfaceForegroundColorEventHandler _foregroundColorChanged; + + public event SurfaceForegroundColorEventHandler ForegroundColorChanged + { + add => _foregroundColorChanged += value; + remove => _foregroundColorChanged -= value; + } + + [NonSerialized] private SurfaceBackgroundColorEventHandler _backgroundColorChanged; + + public event SurfaceBackgroundColorEventHandler BackgroundColorChanged + { + add => _backgroundColorChanged += value; + remove => _backgroundColorChanged -= value; + } + + [NonSerialized] private SurfaceLineThicknessEventHandler _lineThicknessChanged; + + public event SurfaceLineThicknessEventHandler LineThicknessChanged + { + add => _lineThicknessChanged += value; + remove => _lineThicknessChanged -= value; + } + + [NonSerialized] private SurfaceShadowEventHandler _shadowChanged; + + public event SurfaceShadowEventHandler ShadowChanged + { + add => _shadowChanged += value; + remove => _shadowChanged -= value; + } + + + [NonSerialized] private int _currentDpi = 96; + /// + /// The most recent DPI value that was used + /// + public int CurrentDpi + { + get => _currentDpi; + set => _currentDpi = value; + } + + /// + /// inUndoRedo makes sure we don't undo/redo while in a undo/redo action + /// + [NonSerialized] private bool _inUndoRedo; + + /// + /// Make only one surface move cycle undoable, see SurfaceMouseMove + /// + [NonSerialized] private bool _isSurfaceMoveMadeUndoable; + + /// + /// Undo/Redo stacks, should not be serialized as the file would be way to big + /// + [NonSerialized] private readonly Stack _undoStack = new Stack(); + + [NonSerialized] private readonly Stack _redoStack = new Stack(); + + /// + /// Last save location, do not serialize! + /// + [NonSerialized] private string _lastSaveFullPath; + + /// + /// current drawing mode, do not serialize! + /// + [NonSerialized] private DrawingModes _drawingMode = DrawingModes.None; + + /// + /// the keys-locked flag helps with focus issues + /// + [NonSerialized] private bool _keysLocked; + + /// + /// Location of the mouse-down (it "starts" here), do not serialize + /// + [NonSerialized] private Point _mouseStart = Point.Empty; + + /// + /// are we in a mouse down, do not serialize + /// + [NonSerialized] private bool _mouseDown; + + /// + /// The selected element for the mouse down, do not serialize + /// + [NonSerialized] private IDrawableContainer _mouseDownElement; + + /// + /// all selected elements, do not serialize + /// + [NonSerialized] private readonly IDrawableContainerList selectedElements; + + /// + /// the element we are drawing with, do not serialize + /// + [NonSerialized] private IDrawableContainer _drawingElement; + + /// + /// the element we want to draw with (not yet drawn), do not serialize + /// + [NonSerialized] private IDrawableContainer _undrawnElement; + + /// + /// the crop container, when cropping this is set, do not serialize + /// + [NonSerialized] private IDrawableContainer _cropContainer; + + /// + /// the brush which is used for transparent backgrounds, set by the editor, do not serialize + /// + [NonSerialized] private Brush _transparencyBackgroundBrush; + + /// + /// The buffer is only for drawing on it when using filters (to supply access) + /// This saves a lot of "create new bitmap" commands + /// Should not be serialized, as it's generated. + /// The actual bitmap is in the paintbox... + /// TODO: Check if this buffer is still needed! + /// + [NonSerialized] private Bitmap _buffer; + + /// + /// all stepLabels for the surface, needed with serialization + /// + private readonly List _stepLabels = new List(); + + public void AddStepLabel(StepLabelContainer stepLabel) + { + if (!_stepLabels.Contains(stepLabel)) + { + _stepLabels.Add(stepLabel); + } + } + + public void RemoveStepLabel(StepLabelContainer stepLabel) + { + _stepLabels.Remove(stepLabel); + } + + /// + /// The start value of the counter objects + /// + public int CounterStart + { + get => _counterStart; + set + { + if (_counterStart == value) + { + return; + } + + _counterStart = value; + Invalidate(); + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CounterStart))); + } + } + + /// + /// Count all the VISIBLE steplabels in the surface, up to the supplied one + /// + /// can be null, if not the counting stops here + /// number of steplabels before the supplied container + public int CountStepLabels(IDrawableContainer stopAtContainer) + { + int number = CounterStart; + foreach (var possibleThis in _stepLabels) + { + if (possibleThis.Equals(stopAtContainer)) + { + break; + } + + if (IsOnSurface(possibleThis)) + { + number++; + } + } + + return number; + } + + /// + /// all elements on the surface, needed with serialization + /// + private readonly IDrawableContainerList _elements; + + /// + /// all elements on the surface, needed with serialization + /// + private IFieldAggregator _fieldAggregator; + + /// + /// the cursor container, needed with serialization as we need a direct acces to it. + /// + private IDrawableContainer _cursorContainer; + + /// + /// the modified flag specifies if the surface has had modifications after the last export. + /// Initial state is modified, as "it's not saved" + /// After serialization this should actually be "false" (the surface came from a stream) + /// For now we just serialize it... + /// + private bool _modified = true; + + /// + /// The image is the actual captured image, needed with serialization + /// + private Image _image; + + public Image Image + { + get => _image; + set + { + _image = value; + UpdateSize(); + } + } + + [NonSerialized] private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); + [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); + [NonSerialized] private Fraction _zoomFactor = Fraction.Identity; + + public Fraction ZoomFactor + { + get => _zoomFactor; + set + { + _zoomFactor = value; + var inverse = _zoomFactor.Inverse(); + _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); + _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); + UpdateSize(); + } + } + + + /// + /// Sets the surface size as zoomed image size. + /// + private void UpdateSize() + { + var size = _image.Size; + Size = new Size((int) (size.Width * _zoomFactor), (int) (size.Height * _zoomFactor)); + } + + /// + /// The field aggregator is that which is used to have access to all the fields inside the currently selected elements. + /// e.g. used to decided if and which line thickness is shown when multiple elements are selected. + /// + public IFieldAggregator FieldAggregator + { + get => _fieldAggregator; + set => _fieldAggregator = value; + } + + /// + /// The cursor container has it's own accessor so we can find and remove this (when needed) + /// + public IDrawableContainer CursorContainer => _cursorContainer; + + /// + /// A simple getter to ask if this surface has a cursor + /// + public bool HasCursor => _cursorContainer != null; + + /// + /// A simple helper method to remove the cursor from the surface + /// + public void RemoveCursor() + { + RemoveElement(_cursorContainer); + _cursorContainer = null; + } + + /// + /// The brush which is used to draw the transparent background + /// + public Brush TransparencyBackgroundBrush + { + get => _transparencyBackgroundBrush; + set => _transparencyBackgroundBrush = value; + } + + /// + /// Are the keys on this surface locked? + /// + public bool KeysLocked + { + get => _keysLocked; + set => _keysLocked = value; + } + + /// + /// Is this surface modified? This is only true if the surface has not been exported. + /// + public bool Modified + { + get => _modified; + set => _modified = value; + } + + /// + /// The DrawingMode property specifies the mode for drawing, more or less the element type. + /// + public DrawingModes DrawingMode + { + get => _drawingMode; + set + { + _drawingMode = value; + if (_drawingModeChanged != null) + { + SurfaceDrawingModeEventArgs eventArgs = new SurfaceDrawingModeEventArgs + { + DrawingMode = _drawingMode + }; + _drawingModeChanged.Invoke(this, eventArgs); + } + + DeselectAllElements(); + CreateUndrawnElement(); + } + } + + /// + /// Property for accessing the last save "full" path + /// + public string LastSaveFullPath + { + get => _lastSaveFullPath; + set => _lastSaveFullPath = value; + } + + /// + /// Property for accessing the URL to which the surface was recently uploaded + /// + public string UploadUrl { get; set; } + + /// + /// Property for accessing the capture details + /// + public ICaptureDetails CaptureDetails { get; set; } + + /// + /// Adjust UI elements to the supplied DPI settings + /// + /// + public void AdjustToDpi(int dpi) + { + CurrentDpi = dpi; + foreach (var element in this._elements) + { + element.AdjustToDpi(dpi); + } + } + + /// + /// Base Surface constructor + /// + public Surface() + { + _fieldAggregator = new FieldAggregator(this); + _elements = new DrawableContainerList(_uniqueId); + selectedElements = new DrawableContainerList(_uniqueId); + LOG.Debug("Creating surface!"); + MouseDown += SurfaceMouseDown; + MouseUp += SurfaceMouseUp; + MouseMove += SurfaceMouseMove; + MouseDoubleClick += SurfaceDoubleClick; + Paint += SurfacePaint; + AllowDrop = true; + DragDrop += OnDragDrop; + DragEnter += OnDragEnter; + // bind selected & elements to this, otherwise they can't inform of modifications + selectedElements.Parent = this; + _elements.Parent = this; + // Make sure we are visible + Visible = true; + TabStop = false; + // Enable double buffering + DoubleBuffered = true; + SetStyle( + ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.ContainerControl | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.SupportsTransparentBackColor, true); + } + + /// + /// Private method, the current image is disposed the new one will stay. + /// + /// The new image + /// true if the old image needs to be disposed, when using undo this should not be true!! + private void SetImage(Image newImage, bool dispose) + { + // Dispose + if (_image != null && dispose) + { + _image.Dispose(); + } + + // Set new values + Image = newImage; + + _modified = true; + } + + /// + /// Surface constructor with an image + /// + /// + public Surface(Image newImage) : this() + { + LOG.DebugFormat("Got image with dimensions {0} and format {1}", newImage.Size, newImage.PixelFormat); + SetImage(newImage, true); + } + + /// + /// Surface contructor with a capture + /// + /// + public Surface(ICapture capture) : this(capture.Image) + { + // check if cursor is captured, and visible + if (capture.Cursor != null && capture.CursorVisible) + { + var cursorRect = new NativeRect(capture.CursorLocation, capture.Cursor.Size); + var captureRect = new NativeRect(NativePoint.Empty, capture.Image.Size); + // check if cursor is on the capture, otherwise we leave it out. + if (cursorRect.IntersectsWith(captureRect)) + { + _cursorContainer = AddIconContainer(capture.Cursor, capture.CursorLocation.X, capture.CursorLocation.Y); + SelectElement(_cursorContainer); + } + } + + // Make sure the image is NOT disposed, we took the reference directly into ourselves + ((Capture) capture).NullImage(); + + CaptureDetails = capture.CaptureDetails; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + LOG.Debug("Disposing surface!"); + if (_buffer != null) + { + _buffer.Dispose(); + _buffer = null; + } + + if (_transparencyBackgroundBrush != null) + { + _transparencyBackgroundBrush.Dispose(); + _transparencyBackgroundBrush = null; + } + + // Cleanup undo/redo stacks + while (_undoStack != null && _undoStack.Count > 0) + { + _undoStack.Pop().Dispose(); + } + + while (_redoStack != null && _redoStack.Count > 0) + { + _redoStack.Pop().Dispose(); + } + + foreach (IDrawableContainer container in _elements) + { + container.Dispose(); + } + + if (_undrawnElement != null) + { + _undrawnElement.Dispose(); + _undrawnElement = null; + } + + if (_cropContainer != null) + { + _cropContainer.Dispose(); + _cropContainer = null; + } + } + + base.Dispose(disposing); + } + + /// + /// Undo the last action + /// + public void Undo() + { + if (_undoStack.Count > 0) + { + _inUndoRedo = true; + IMemento top = _undoStack.Pop(); + _redoStack.Push(top.Restore()); + _inUndoRedo = false; + } + } + + /// + /// Undo an undo (=redo) + /// + public void Redo() + { + if (_redoStack.Count > 0) + { + _inUndoRedo = true; + IMemento top = _redoStack.Pop(); + _undoStack.Push(top.Restore()); + _inUndoRedo = false; + } + } + + /// + /// Returns if the surface can do a undo + /// + public bool CanUndo => _undoStack.Count > 0; + + /// + /// Returns if the surface can do a redo + /// + public bool CanRedo => _redoStack.Count > 0; + + /// + /// Get the language key for the undo action + /// + public LangKey UndoActionLanguageKey => LangKey.none; + + /// + /// Get the language key for redo action + /// + public LangKey RedoActionLanguageKey => LangKey.none; + + /// + /// Make an action undo-able + /// + /// The memento implementing the undo + /// Allow changes to be merged + public void MakeUndoable(IMemento memento, bool allowMerge) + { + if (_inUndoRedo) + { + throw new InvalidOperationException("Invoking do within an undo/redo action."); + } + + if (memento != null) + { + bool allowPush = true; + if (_undoStack.Count > 0 && allowMerge) + { + // Check if merge is possible + allowPush = !_undoStack.Peek().Merge(memento); + } + + if (allowPush) + { + // Clear the redo-stack and dispose + while (_redoStack.Count > 0) + { + _redoStack.Pop().Dispose(); + } + + _undoStack.Push(memento); + } + } + } + + /// + /// This saves the elements of this surface to a stream. + /// Is used to save a template of the complete surface + /// + /// + /// + public long SaveElementsToStream(Stream streamWrite) + { + long bytesWritten = 0; + try + { + long lengtBefore = streamWrite.Length; + BinaryFormatter binaryWrite = new BinaryFormatter(); + binaryWrite.Serialize(streamWrite, _elements); + bytesWritten = streamWrite.Length - lengtBefore; + } + catch (Exception e) + { + LOG.Error("Error serializing elements to stream.", e); + } + + return bytesWritten; + } + + /// + /// This loads elements from a stream, among others this is used to load a surface. + /// + /// + public void LoadElementsFromStream(Stream streamRead) + { + try + { + BinaryFormatter binaryRead = new BinaryFormatter(); + binaryRead.Binder = new BinaryFormatterHelper(); + IDrawableContainerList loadedElements = (IDrawableContainerList) binaryRead.Deserialize(streamRead); + loadedElements.Parent = this; + // Make sure the steplabels are sorted according to their number + _stepLabels.Sort((p1, p2) => p1.Number.CompareTo(p2.Number)); + DeselectAllElements(); + AddElements(loadedElements); + SelectElements(loadedElements); + FieldAggregator.BindElements(loadedElements); + } catch (SecurityAccessDeniedException) { throw; - } - catch (Exception e) - { - LOG.Error("Error serializing elements from stream.", e); - } - } - - /// - /// This is called from the DrawingMode setter, which is not very correct... - /// But here an element is created which is not yet draw, thus "undrawnElement". - /// The element is than used while drawing on the surface. - /// - private void CreateUndrawnElement() - { - if (_undrawnElement != null) - { - FieldAggregator.UnbindElement(_undrawnElement); - } - - switch (DrawingMode) - { - case DrawingModes.Rect: - _undrawnElement = new RectangleContainer(this); - break; - case DrawingModes.Ellipse: - _undrawnElement = new EllipseContainer(this); - break; - case DrawingModes.Text: - _undrawnElement = new TextContainer(this); - break; - case DrawingModes.SpeechBubble: - _undrawnElement = new SpeechbubbleContainer(this); - break; - case DrawingModes.StepLabel: - _undrawnElement = new StepLabelContainer(this); - break; - case DrawingModes.Line: - _undrawnElement = new LineContainer(this); - break; - case DrawingModes.Arrow: - _undrawnElement = new ArrowContainer(this); - break; - case DrawingModes.Highlight: - _undrawnElement = new HighlightContainer(this); - break; - case DrawingModes.Obfuscate: - _undrawnElement = new ObfuscateContainer(this); - break; - case DrawingModes.Crop: - _cropContainer = new CropContainer(this); - _undrawnElement = _cropContainer; - break; - case DrawingModes.Bitmap: - _undrawnElement = new ImageContainer(this); - break; - case DrawingModes.Path: - _undrawnElement = new FreehandContainer(this); - break; - case DrawingModes.None: - _undrawnElement = null; - break; - } - - if (_undrawnElement != null) - { - FieldAggregator.BindElement(_undrawnElement); - } - } - - #region Plugin interface implementations - - public IImageContainer AddImageContainer(Image image, int x, int y) - { - ImageContainer bitmapContainer = new ImageContainer(this) - { - Image = image, - Left = x, - Top = y - }; - AddElement(bitmapContainer); - return bitmapContainer; - } - - public IImageContainer AddImageContainer(string filename, int x, int y) - { - ImageContainer bitmapContainer = new ImageContainer(this); - bitmapContainer.Load(filename); - bitmapContainer.Left = x; - bitmapContainer.Top = y; - AddElement(bitmapContainer); - return bitmapContainer; - } - - public IIconContainer AddIconContainer(Icon icon, int x, int y) - { - IconContainer iconContainer = new IconContainer(this) - { - Icon = icon, - Left = x, - Top = y - }; - AddElement(iconContainer); - return iconContainer; - } - - public IIconContainer AddIconContainer(string filename, int x, int y) - { - IconContainer iconContainer = new IconContainer(this); - iconContainer.Load(filename); - iconContainer.Left = x; - iconContainer.Top = y; - AddElement(iconContainer); - return iconContainer; - } - - public ICursorContainer AddCursorContainer(Cursor cursor, int x, int y) - { - CursorContainer cursorContainer = new CursorContainer(this) - { - Cursor = cursor, - Left = x, - Top = y - }; - AddElement(cursorContainer); - return cursorContainer; - } - - public ICursorContainer AddCursorContainer(string filename, int x, int y) - { - CursorContainer cursorContainer = new CursorContainer(this); - cursorContainer.Load(filename); - cursorContainer.Left = x; - cursorContainer.Top = y; - AddElement(cursorContainer); - return cursorContainer; - } - - public ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, - Color fillColor) - { - TextContainer textContainer = new TextContainer(this) - { - Text = text, - Left = x, - Top = y - }; - textContainer.SetFieldValue(FieldType.FONT_FAMILY, family.Name); - textContainer.SetFieldValue(FieldType.FONT_BOLD, bold); - textContainer.SetFieldValue(FieldType.FONT_ITALIC, italic); - textContainer.SetFieldValue(FieldType.FONT_SIZE, size); - textContainer.SetFieldValue(FieldType.FILL_COLOR, fillColor); - textContainer.SetFieldValue(FieldType.LINE_COLOR, color); - textContainer.SetFieldValue(FieldType.LINE_THICKNESS, borderSize); - textContainer.SetFieldValue(FieldType.SHADOW, shadow); - // Make sure the Text fits - textContainer.FitToText(); - - //AggregatedProperties.UpdateElement(textContainer); - AddElement(textContainer); - return textContainer; - } - - #endregion - - #region DragDrop - - private void OnDragEnter(object sender, DragEventArgs e) - { - if (LOG.IsDebugEnabled) - { - LOG.Debug("DragEnter got following formats: "); - foreach (string format in ClipboardHelper.GetFormats(e.Data)) - { - LOG.Debug(format); - } - } - - if ((e.AllowedEffect & DragDropEffects.Copy) != DragDropEffects.Copy) - { - e.Effect = DragDropEffects.None; - } - else - { - if (ClipboardHelper.ContainsImage(e.Data) || ClipboardHelper.ContainsFormat(e.Data, "DragImageBits")) - { - e.Effect = DragDropEffects.Copy; - } - else - { - e.Effect = DragDropEffects.None; - } - } - } - - /// - /// This will help to fit the container to the surface - /// - /// IDrawableContainer - private void FitContainer(IDrawableContainer drawableContainer) - { - double factor = 1; - if (drawableContainer.Width > this.Width) - { - factor = drawableContainer.Width / (double)Width; - } - if (drawableContainer.Height > this.Height) - { - var otherFactor = drawableContainer.Height / (double)Height; - factor = Math.Max(factor, otherFactor); - } - - drawableContainer.Width = (int)(drawableContainer.Width / factor); - drawableContainer.Height = (int)(drawableContainer.Height / factor); - } - - /// - /// Handle the drag/drop - /// - /// - /// - private void OnDragDrop(object sender, DragEventArgs e) - { - NativePoint mouse = PointToClient(new NativePoint(e.X, e.Y)); - if (e.Data.GetDataPresent("Text")) - { - string possibleUrl = ClipboardHelper.GetText(e.Data); - // Test if it's an url and try to download the image so we have it in the original form - if (possibleUrl != null && possibleUrl.StartsWith("http")) - { - var drawableContainer = NetworkHelper.DownloadImageAsDrawableContainer(possibleUrl); - if (drawableContainer != null) - { - drawableContainer.Left = Location.X; - drawableContainer.Top = Location.Y; - FitContainer(drawableContainer); - AddElement(drawableContainer); - return; - } - } - } - - foreach (var drawableContainer in ClipboardHelper.GetDrawables(e.Data)) - { - drawableContainer.Left = mouse.X; - drawableContainer.Top = mouse.Y; - FitContainer(drawableContainer); - AddElement(drawableContainer); - mouse = mouse.Offset(10, 10); - } - } - - #endregion - - /// - /// Auto crop the image - /// - /// NativeRect with optional area to find a crop region - /// true if cropped - public bool AutoCrop(NativeRect? cropArea = null) - { - NativeRect cropRectangle; - using (Image tmpImage = GetImageForExport()) - { - cropRectangle = ImageHelper.FindAutoCropRectangle(tmpImage, conf.AutoCropDifference, cropArea); - } - - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.AutoCrop)) - { - return false; - } - - DeselectAllElements(); - // Maybe a bit obscure, but the following line creates a drop container - // It's available as "undrawnElement" - DrawingMode = DrawingModes.Crop; - _undrawnElement.Left = cropRectangle.X; - _undrawnElement.Top = cropRectangle.Y; - _undrawnElement.Width = cropRectangle.Width; - _undrawnElement.Height = cropRectangle.Height; - _undrawnElement.Status = EditStatus.UNDRAWN; - AddElement(_undrawnElement); - SelectElement(_undrawnElement); - _drawingElement = null; - _undrawnElement = null; - return true; - } - - /// - /// A simple clear - /// - /// The color for the background - public void Clear(Color newColor) - { - //create a blank bitmap the same size as original - Bitmap newBitmap = ImageHelper.CreateEmptyLike(Image, newColor); - if (newBitmap == null) return; - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, null), false); - SetImage(newBitmap, false); - Invalidate(); - } - - /// - /// Apply a bitmap effect to the surface - /// - /// - public void ApplyBitmapEffect(IEffect effect) - { - BackgroundForm backgroundForm = new BackgroundForm("Effect", "Please wait"); - backgroundForm.Show(); - Application.DoEvents(); - try - { - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - Matrix matrix = new Matrix(); - Image newImage = ImageHelper.ApplyEffect(Image, effect, matrix); - if (newImage != null) - { - // Make sure the elements move according to the offset the effect made the bitmap move - _elements.Transform(matrix); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - SetImage(newImage, false); - Invalidate(); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - } - else - { - // clean up matrix, as it hasn't been used in the undo stack. - matrix.Dispose(); - } - } - finally - { - // Always close the background form - backgroundForm.CloseDialog(); - } - } - - /// - /// check if a crop is possible - /// - /// Rectangle adapted to the dimensions of the image - /// CropModes - /// true if this is possible - public bool IsCropPossible(ref NativeRect cropRectangle, CropContainer.CropModes cropMode) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, cropRectangle.Height).Normalize(); - //Fitting the rectangle to the dimensions of the image - if (cropRectangle.Left < 0) - { - cropRectangle = new NativeRect(0, cropRectangle.Top, cropRectangle.Width + cropRectangle.Left, cropRectangle.Height); - } - - if (cropRectangle.Top < 0) - { - cropRectangle = new NativeRect(cropRectangle.Left, 0, cropRectangle.Width, cropRectangle.Height + cropRectangle.Top); - } - - if (cropRectangle.Left + cropRectangle.Width > Image.Width) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, Image.Width - cropRectangle.Left, cropRectangle.Height); - } - - if (cropRectangle.Top + cropRectangle.Height > Image.Height) - { - cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Image.Height - cropRectangle.Top); - } - - // special condition for vertical - if(cropMode == CropContainer.CropModes.Vertical && cropRectangle.Width == Image.Width) - { - //crop out the hole image is not allowed - return false; - } - - // special condition for vertical - if (cropMode == CropContainer.CropModes.Horizontal && cropRectangle.Height == Image.Height) - { - //crop out the hole image is not allowed - return false; - } - - //condition for all other crop modes - if (cropRectangle.Height > 0 && cropRectangle.Width > 0) - { - return true; - } - - return false; - } - - /// - /// Use to send any registered SurfaceMessageEventHandler a message, e.g. used for the notification area - /// - /// Who send - /// Type of message - /// Message itself - public void SendMessageEvent(object source, SurfaceMessageTyp messageType, string message) - { - if (_surfaceMessage == null) return; - - var eventArgs = new SurfaceMessageEventArgs - { - Message = message, - MessageType = messageType, - Surface = this - }; - _surfaceMessage(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the foreground color - /// - /// Who send - /// new color - private void UpdateForegroundColorEvent(object source, Color color) - { - if (_foregroundColorChanged == null) return; - - var eventArgs = new SurfaceForegroundColorEventArgs - { - Color = color, - }; - _foregroundColorChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the background color - /// - /// Who send - /// new color - private void UpdateBackgroundColorEvent(object source, Color color) - { - if (_lineThicknessChanged == null) return; - - var eventArgs = new SurfaceBackgroundColorEventArgs - { - Color = color, - }; - _backgroundColorChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing a key to change the line thickness - /// - /// Who send - /// new thickness - private void UpdateLineThicknessEvent(object source, int thickness) - { - if (_lineThicknessChanged == null) return; - - var eventArgs = new SurfaceLineThicknessEventArgs - { - Thickness = thickness, - }; - _lineThicknessChanged(source, eventArgs); - } - - /// - /// Use to update UI when pressing the key to show/hide the shadow - /// - /// Who send - /// has shadow - private void UpdateShadowEvent(object source, bool hasShadow) - { - if (_shadowChanged == null) return; - - var eventArgs = new SurfaceShadowEventArgs - { - HasShadow = hasShadow, - }; - _shadowChanged(source, eventArgs); - } - - /// - /// Crop the surface - /// - /// NativeRect that remains - /// bool - public bool ApplyCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Default)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - Bitmap tmpImage; - // Make sure we have information, this this fails - try - { - tmpImage = ImageHelper.CloneArea(Image, cropRectangle, PixelFormat.DontCare); - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - - var matrix = new Matrix(); - matrix.Translate(-cropRectangle.Left, -cropRectangle.Top, MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(tmpImage, false); - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, tmpImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// Crop out the surface - /// Splits the image in 3 parts(top, middle, bottom). Crop out the middle and joins top and bottom. - /// - /// NativeRect of the middle part - /// bool - private bool ApplyHorizontalCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Horizontal)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - var topRectangle = new NativeRect(0, 0, Image.Size.Width, cropRectangle.Top); - var bottomRectangle = new NativeRect(0, cropRectangle.Top + cropRectangle.Height, Image.Size.Width, Image.Size.Height - cropRectangle.Top - cropRectangle.Height); - - Bitmap newImage; - try - { - newImage = new Bitmap(Image.Size.Width, Image.Size.Height - cropRectangle.Height); - - using var graphics = Graphics.FromImage(newImage); - - var insertPositionTop = 0; - if (topRectangle.Height > 0) - { - graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, topRectangle.Width, topRectangle.Height), topRectangle, GraphicsUnit.Pixel); - insertPositionTop += topRectangle.Height; - } - if (bottomRectangle.Height > 0) - { - graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, bottomRectangle.Width, bottomRectangle.Height), bottomRectangle, GraphicsUnit.Pixel); - } - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - var matrix = new Matrix(); - matrix.Translate(0, -(cropRectangle.Top + cropRectangle.Height), MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(newImage, false); - - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// Crop out the surface - /// Splits the image in 3 parts(left, middle, right). Crop out the middle and joins top and bottom. - /// - /// NativeRect of the middle part - /// bool - private bool ApplyVerticalCrop(NativeRect cropRectangle) - { - if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Vertical)) return false; - - var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); - var leftRectangle = new NativeRect(0, 0, cropRectangle.Left, Image.Size.Height); - var rightRectangle = new NativeRect(cropRectangle.Left + cropRectangle.Width, 0, Image.Size.Width - cropRectangle.Width - cropRectangle.Left, Image.Size.Height); - Bitmap newImage; - try - { - newImage = new Bitmap(Image.Size.Width - cropRectangle.Width, Image.Size.Height); - - using var graphics = Graphics.FromImage(newImage); - - var insertPositionLeft = 0; - if (leftRectangle.Width > 0) - { - graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, leftRectangle.Width, leftRectangle.Height), leftRectangle , GraphicsUnit.Pixel); - insertPositionLeft += leftRectangle.Width; - } - - if (rightRectangle.Width > 0) - { - graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, rightRectangle.Width, rightRectangle.Height), rightRectangle, GraphicsUnit.Pixel); - } - } - catch (Exception ex) - { - ex.Data.Add("CropRectangle", cropRectangle); - ex.Data.Add("Width", Image.Width); - ex.Data.Add("Height", Image.Height); - ex.Data.Add("Pixelformat", Image.PixelFormat); - throw; - } - var matrix = new Matrix(); - matrix.Translate(-cropRectangle.Left - cropRectangle.Width, 0, MatrixOrder.Append); - // Make undoable - MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); - - // Do not dispose otherwise we can't undo the image! - SetImage(newImage, false); - - _elements.Transform(matrix); - if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) - { - _surfaceSizeChanged(this, null); - } - - Invalidate(); - return true; - } - - /// - /// The background here is the captured image. - /// This is called from the SurfaceBackgroundChangeMemento. - /// - /// - /// - public void UndoBackgroundChange(Image previous, Matrix matrix) - { - SetImage(previous, false); - if (matrix != null) - { - _elements.Transform(matrix); - } - - _surfaceSizeChanged?.Invoke(this, null); - Invalidate(); - } - - /// - /// Check if an adorner was "hit", and change the cursor if so - /// - /// MouseEventArgs - /// IAdorner - private IAdorner FindActiveAdorner(MouseEventArgs mouseEventArgs) - { - foreach (IDrawableContainer drawableContainer in selectedElements) - { - foreach (IAdorner adorner in drawableContainer.Adorners) - { - if (!adorner.IsActive && !adorner.HitTest(mouseEventArgs.Location)) continue; - - if (adorner.Cursor != null) - { - Cursor = adorner.Cursor; - } - - return adorner; - } - } - - return null; - } - - /// - /// Translate mouse coordinates as if they were applied directly to unscaled image. - /// - private MouseEventArgs InverseZoomMouseCoordinates(MouseEventArgs e) - => new MouseEventArgs(e.Button, e.Clicks, (int) (e.X / _zoomFactor), (int) (e.Y / _zoomFactor), e.Delta); - - /// - /// This event handler is called when someone presses the mouse on a surface. - /// - /// - /// - private void SurfaceMouseDown(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseDown(sender, e); - return; - } - - _mouseStart = e.Location; - - // check contextmenu - if (e.Button == MouseButtons.Right) - { - IDrawableContainerList selectedList = null; - if (selectedElements != null && selectedElements.Count > 0) - { - selectedList = selectedElements; - } - else - { - // Single element - IDrawableContainer rightClickedContainer = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); - if (rightClickedContainer != null) - { - selectedList = new DrawableContainerList(ID) - { - rightClickedContainer - }; - } - } - - if (selectedList != null && selectedList.Count > 0) - { - selectedList.ShowContextMenu(e, this); - } - - return; - } - - _mouseDown = true; - _isSurfaceMoveMadeUndoable = false; - - if (_cropContainer != null && ((_undrawnElement == null) || (_undrawnElement != null && DrawingMode != DrawingModes.Crop))) - { - RemoveElement(_cropContainer, false); - _cropContainer = null; - _drawingElement = null; - } - - if (_drawingElement == null && DrawingMode != DrawingModes.None) - { - if (_undrawnElement == null) - { - DeselectAllElements(); - if (_undrawnElement == null) - { - CreateUndrawnElement(); - } - } - - _drawingElement = _undrawnElement; - // if a new element has been drawn, set location and register it - if (_drawingElement != null) - { - if (_undrawnElement != null) - { - _drawingElement.Status = _undrawnElement.DefaultEditMode; - } - - if (!_drawingElement.HandleMouseDown(_mouseStart.X, _mouseStart.Y)) - { - _drawingElement.Left = _mouseStart.X; - _drawingElement.Top = _mouseStart.Y; - } - - AddElement(_drawingElement); - _drawingElement.Selected = true; - } - - _undrawnElement = null; - } - else - { - // check whether an existing element was clicked - // we save mouse down element separately from selectedElements (checked on mouse up), - // since it could be moved around before it is actually selected - _mouseDownElement = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); - - if (_mouseDownElement != null) - { - _mouseDownElement.Status = EditStatus.MOVING; - } - } - } - - /// - /// This event handle is called when the mouse button is unpressed - /// - /// - /// - private void SurfaceMouseUp(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseUp(sender, e); - return; - } - - Point currentMouse = new Point(e.X, e.Y); - - _elements.Status = EditStatus.IDLE; - if (_mouseDownElement != null) - { - _mouseDownElement.Status = EditStatus.IDLE; - } - - _mouseDown = false; - _mouseDownElement = null; - if (DrawingMode == DrawingModes.None) - { - // check whether an existing element was clicked - IDrawableContainer element = _elements.ClickableElementAt(currentMouse.X, currentMouse.Y); - bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; - if (element != null) - { - element.Invalidate(); - bool alreadySelected = selectedElements.Contains(element); - if (shiftModifier) - { - if (alreadySelected) - { - DeselectElement(element); - } - else - { - SelectElement(element); - } - } - else - { - if (!alreadySelected) - { - DeselectAllElements(); - SelectElement(element); - } - } - } - else if (!shiftModifier) - { - DeselectAllElements(); - } - } - - if (selectedElements.Count > 0) - { - selectedElements.Invalidate(); - selectedElements.Selected = true; - } - - if (_drawingElement != null) - { - if (!_drawingElement.InitContent()) - { - _elements.Remove(_drawingElement); - _drawingElement.Invalidate(); - } - else - { - _drawingElement.HandleMouseUp(currentMouse.X, currentMouse.Y); - _drawingElement.Invalidate(); - if (Math.Abs(_drawingElement.Width) < 5 && Math.Abs(_drawingElement.Height) < 5) - { - _drawingElement.Width = 25; - _drawingElement.Height = 25; - } - - SelectElement(_drawingElement); - _drawingElement.Selected = true; - } - - _drawingElement = null; - } - } - - /// - /// This event handler is called when the mouse moves over the surface - /// - /// - /// - private void SurfaceMouseMove(object sender, MouseEventArgs e) - { - e = InverseZoomMouseCoordinates(e); - - // Handle Adorners - var adorner = FindActiveAdorner(e); - if (adorner != null) - { - adorner.MouseMove(sender, e); - return; - } - - Point currentMouse = e.Location; - - Cursor = DrawingMode != DrawingModes.None ? Cursors.Cross : Cursors.Default; - - if (!_mouseDown) return; - - if (_mouseDownElement != null) - { - // an element is currently dragged - _mouseDownElement.Invalidate(); - selectedElements.Invalidate(); - // Move the element - if (_mouseDownElement.Selected) - { - if (!_isSurfaceMoveMadeUndoable) - { - // Only allow one undoable per mouse-down/move/up "cycle" - _isSurfaceMoveMadeUndoable = true; - selectedElements.MakeBoundsChangeUndoable(false); - } - - // dragged element has been selected before -> move all - selectedElements.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); - } - else - { - if (!_isSurfaceMoveMadeUndoable) - { - // Only allow one undoable per mouse-down/move/up "cycle" - _isSurfaceMoveMadeUndoable = true; - _mouseDownElement.MakeBoundsChangeUndoable(false); - } - - // dragged element is not among selected elements -> just move dragged one - _mouseDownElement.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); - } - - _mouseStart = currentMouse; - _mouseDownElement.Invalidate(); - _modified = true; - } - else if (_drawingElement != null) - { - _drawingElement.HandleMouseMove(currentMouse.X, currentMouse.Y); - _modified = true; - } - } - - /// - /// This event handler is called when the surface is double clicked. - /// - /// - /// - private void SurfaceDoubleClick(object sender, MouseEventArgs e) - { - selectedElements.OnDoubleClick(); - selectedElements.Invalidate(); - } - - /// - /// Privately used to get the rendered image with all the elements on it. - /// - /// - /// - private Image GetImage(RenderMode renderMode) - { - // Generate a copy of the original image with a dpi equal to the default... - Bitmap clone = ImageHelper.Clone(_image, PixelFormat.DontCare); - // otherwise we would have a problem drawing the image to the surface... :( - using (Graphics graphics = Graphics.FromImage(clone)) - { - // Do not set the following, the containers need to decide themselves - //graphics.SmoothingMode = SmoothingMode.HighQuality; - //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - //graphics.CompositingQuality = CompositingQuality.HighQuality; - //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - _elements.Draw(graphics, clone, renderMode, new Rectangle(Point.Empty, clone.Size)); - } - - return clone; - } - - /// - /// This returns the image "result" of this surface, with all the elements rendered on it. - /// - /// - public Image GetImageForExport() - { - return GetImage(RenderMode.EXPORT); - } - - private static NativeRect ZoomClipRectangle(NativeRect rc, double scale, int inflateAmount = 0) - { - rc = new NativeRect( - (int) (rc.X * scale), - (int) (rc.Y * scale), - (int) (rc.Width * scale) + 1, - (int) (rc.Height * scale) + 1 - ); - if (scale > 1) - { - inflateAmount = (int) (inflateAmount * scale); - } - - return rc.Inflate(inflateAmount, inflateAmount); - } - - public void InvalidateElements(NativeRect rc) - => Invalidate(ZoomClipRectangle(rc, _zoomFactor, 1)); - - /// - /// This is the event handler for the Paint Event, try to draw as little as possible! - /// - /// - /// PaintEventArgs - private void SurfacePaint(object sender, PaintEventArgs paintEventArgs) - { - Graphics targetGraphics = paintEventArgs.Graphics; - NativeRect targetClipRectangle = paintEventArgs.ClipRectangle; - if (targetClipRectangle.IsEmpty) - { - LOG.Debug("Empty cliprectangle??"); - return; - } - - // Correction to prevent rounding errors at certain zoom levels. - // When zooming to N/M, clip rectangle top and left coordinates should be multiples of N. - if (_zoomFactor.Numerator > 1 && _zoomFactor.Denominator > 1) - { - int horizontalCorrection = targetClipRectangle.Left % (int) _zoomFactor.Numerator; - int verticalCorrection = targetClipRectangle.Top % (int) _zoomFactor.Numerator; - if (horizontalCorrection != 0) - { - targetClipRectangle = targetClipRectangle - .ChangeX(targetClipRectangle.X - horizontalCorrection) - .ChangeWidth(targetClipRectangle.Width + horizontalCorrection); - } - - if (verticalCorrection != 0) - { - targetClipRectangle = targetClipRectangle - .ChangeY(targetClipRectangle.Y - verticalCorrection) - .ChangeHeight(targetClipRectangle.Height + verticalCorrection); - } - } - - NativeRect imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); - - if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) - { - if (_buffer != null) - { - if (_buffer.Width != Image.Width || _buffer.Height != Image.Height || _buffer.PixelFormat != Image.PixelFormat) - { - _buffer.Dispose(); - _buffer = null; - } - } - - if (_buffer == null) - { - _buffer = ImageHelper.CreateEmpty(Image.Width, Image.Height, Image.PixelFormat, Color.Empty, Image.HorizontalResolution, Image.VerticalResolution); - LOG.DebugFormat("Created buffer with size: {0}x{1}", Image.Width, Image.Height); - } - - // Elements might need the bitmap, so we copy the part we need - using (Graphics graphics = Graphics.FromImage(_buffer)) - { - // do not set the following, the containers need to decide this themselves! - //graphics.SmoothingMode = SmoothingMode.HighQuality; - //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - //graphics.CompositingQuality = CompositingQuality.HighQuality; - //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - DrawBackground(graphics, imageClipRectangle); - graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); - _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); - } - - if (_zoomFactor == Fraction.Identity) - { - targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - } - else - { - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - if (_zoomFactor > Fraction.Identity) - { - DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); - } - else - { - DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); - } - - targetGraphics.ResetTransform(); - } - } - else - { - DrawBackground(targetGraphics, targetClipRectangle); - if (_zoomFactor == Fraction.Identity) - { - targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); - } - else - { - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - DrawSmoothImage(targetGraphics, Image, imageClipRectangle); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); - targetGraphics.ResetTransform(); - } - } - - // No clipping for the adorners - targetGraphics.ResetClip(); - // Draw adorners last - foreach (var drawableContainer in selectedElements) - { - foreach (var adorner in drawableContainer.Adorners) - { - adorner.Paint(paintEventArgs); - } - } - } - - private void DrawSmoothImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) - { - var state = targetGraphics.Save(); - targetGraphics.SmoothingMode = SmoothingMode.HighQuality; - targetGraphics.InterpolationMode = InterpolationMode.HighQualityBilinear; - targetGraphics.CompositingQuality = CompositingQuality.HighQuality; - targetGraphics.PixelOffsetMode = PixelOffsetMode.None; - - targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - - targetGraphics.Restore(state); - } - - private void DrawSharpImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) - { - var state = targetGraphics.Save(); - targetGraphics.SmoothingMode = SmoothingMode.None; - targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; - targetGraphics.CompositingQuality = CompositingQuality.HighQuality; - targetGraphics.PixelOffsetMode = PixelOffsetMode.None; - - targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - - targetGraphics.Restore(state); - } - - private void DrawBackground(Graphics targetGraphics, NativeRect clipRectangle) - { - // check if we need to draw the checkerboard - if (Image.IsAlphaPixelFormat(Image.PixelFormat) && _transparencyBackgroundBrush != null) - { - targetGraphics.FillRectangle(_transparencyBackgroundBrush, clipRectangle); - } - else - { - targetGraphics.Clear(BackColor); - } - } - - /// - /// Draw a checkboard when capturing with transparency - /// - /// PaintEventArgs - protected override void OnPaintBackground(PaintEventArgs e) - { - } - - /// - /// Add a new element to the surface - /// - /// the new element - /// true if the adding should be undoable - /// true if invalidate needs to be called - public void AddElement(IDrawableContainer element, bool makeUndoable = true, bool invalidate = true) - { - _elements.Add(element); - if (element is DrawableContainer container) - { - container.FieldChanged += Element_FieldChanged; - } - - element.Parent = this; - if (element.Status == EditStatus.UNDRAWN) - { - element.Status = EditStatus.IDLE; - } - - if (element.Selected) - { - // Use false, as the element is invalidated when invalidate == true anyway - SelectElement(element, false); - } - - if (invalidate) - { - element.Invalidate(); - } - - if (makeUndoable && element.IsUndoable) - { - MakeUndoable(new AddElementMemento(this, element), false); - } - - _modified = true; - } - - /// - /// Remove the list of elements - /// - /// IDrawableContainerList - /// flag specifying if the remove needs to be undoable - public void RemoveElements(IDrawableContainerList elementsToRemove, bool makeUndoable = true) - { - // fix potential issues with iterating a changing list - DrawableContainerList cloned = new DrawableContainerList(elementsToRemove); - - if (makeUndoable) - { - // Take all containers to make undoable - var undoableContainers = elementsToRemove.Where(c => c.IsUndoable).ToList(); - if (undoableContainers.Any()) - { - var undoableContainerList = new DrawableContainerList(undoableContainers); - MakeUndoable(new DeleteElementsMemento(this, undoableContainerList), false); - } - } - - SuspendLayout(); - foreach (var drawableContainer in cloned) - { - RemoveElement(drawableContainer, false, false, false); - } - - ResumeLayout(); - Invalidate(); - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = cloned - }; - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Remove an element of the elements list - /// - /// Element to remove - /// flag specifying if the remove needs to be undoable - /// flag specifying if an surface invalidate needs to be called - /// false to skip event generation - public void RemoveElement(IDrawableContainer elementToRemove, bool makeUndoable = true, bool invalidate = true, bool generateEvents = true) - { - DeselectElement(elementToRemove, generateEvents); - _elements.Remove(elementToRemove); - if (elementToRemove is DrawableContainer element) - { - element.FieldChanged -= Element_FieldChanged; - } - - if (elementToRemove != null) - { - elementToRemove.Parent = null; - } - - // Do not dispose, the memento should!! element.Dispose(); - if (invalidate) - { - Invalidate(); - } - - if (makeUndoable && elementToRemove is { IsUndoable: true }) - { - MakeUndoable(new DeleteElementMemento(this, elementToRemove), false); - } - - _modified = true; - } - - /// - /// Add the supplied elements to the surface - /// - /// DrawableContainerList - /// true if the adding should be undoable - public void AddElements(IDrawableContainerList elementsToAdd, bool makeUndoable = true) - { - // fix potential issues with iterating a changing list - DrawableContainerList cloned = new DrawableContainerList(elementsToAdd); - if (makeUndoable) - { - // Take all containers to make undoable - var undoableContainers = elementsToAdd.Where(c => c.IsUndoable).ToList(); - if (undoableContainers.Any()) - { - var undoableContainerList = new DrawableContainerList(undoableContainers); - MakeUndoable(new AddElementsMemento(this, undoableContainerList), false); - } - } - - SuspendLayout(); - foreach (var element in cloned) - { - element.Selected = true; - AddElement(element, false, false); - } - - ResumeLayout(); - Invalidate(); - } - - /// - /// Returns if this surface has selected elements - /// - /// bool - public bool HasSelectedElements => selectedElements is { Count: > 0 }; - - /// - /// Provides the selected elements - /// - public IDrawableContainerList SelectedElements => selectedElements; - - /// - /// Remove all the selected elements - /// - public void RemoveSelectedElements() - { - if (!HasSelectedElements) return; - - // As RemoveElement will remove the element from the selectedElements list we need to copy the element to another list. - RemoveElements(selectedElements); - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs(); - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Cut the selected elements from the surface to the clipboard - /// - public void CutSelectedElements() - { - if (!HasSelectedElements) return; - ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); - RemoveSelectedElements(); - } - - /// - /// Copy the selected elements to the clipboard - /// - public void CopySelectedElements() - { - if (!HasSelectedElements) return; - ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); - } - - /// - /// This method is called to confirm/cancel. - /// Called when pressing enter or using the "check" in the editor. - /// redirects to the specialized confirm/cancel method - /// - /// bool - public void Confirm(bool confirm) - { - if (DrawingMode == DrawingModes.Crop) - { - ConfirmCrop(confirm); - } - else - { - ConfirmSelectedConfirmableElements(confirm); - } - } - - /// - /// This method is called to confirm/cancel "confirmable" elements - /// Called when pressing enter or using the "check" in the editor. - ///
- /// For crop-container there is a dedicated method . - ///
- /// bool - public void ConfirmSelectedConfirmableElements(bool confirm) - { - // create new collection so that we can iterate safely (selectedElements might change due with confirm/cancel) - List selectedDCs = new List(selectedElements); - foreach (IDrawableContainer dc in selectedDCs.Where(c => c.IsConfirmable)) - { - throw new NotImplementedException($"No confirm/cancel defined for Container type {dc.GetType()}"); - } - - // maybe the undo button has to be enabled - _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); - } - - /// - /// This method is called to confirm/cancel the crop-container. - /// Called when pressing enter or using the "check" in the editor. - /// - /// bool - public void ConfirmCrop(bool confirm) - { - if (_cropContainer is not CropContainer e) return; - - if (confirm && selectedElements.Count > 0) - { - // No undo memento for the cropcontainer itself, only for the effect - RemoveElement(_cropContainer, false); - - _ = e.GetFieldValue(FieldType.CROPMODE) switch - { - CropContainer.CropModes.Horizontal => ApplyHorizontalCrop(_cropContainer.Bounds), - CropContainer.CropModes.Vertical => ApplyVerticalCrop(_cropContainer.Bounds), - _ => ApplyCrop(_cropContainer.Bounds) - }; - - _cropContainer.Dispose(); - _cropContainer = null; - } - else - { - RemoveCropContainer(); - } - - DrawingMode = DrawingModes.None; - - // maybe the undo button has to be enabled - _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); - } - - public void RemoveCropContainer() - { - if (_cropContainer == null) return; - - RemoveElement(_cropContainer, false); - _cropContainer.Dispose(); - _cropContainer = null; - } - - /// - /// Paste all the elements that are on the clipboard - /// - public void PasteElementFromClipboard() - { - IDataObject clipboard = ClipboardHelper.GetDataObject(); - - var formats = ClipboardHelper.GetFormats(clipboard); - if (formats == null || formats.Count == 0) - { - return; - } - - if (LOG.IsDebugEnabled) - { - LOG.Debug("List of clipboard formats available for pasting:"); - foreach (string format in formats) - { - LOG.Debug("\tgot format: " + format); - } - } - - if (formats.Contains(typeof(IDrawableContainerList).FullName)) - { - IDrawableContainerList dcs = (IDrawableContainerList) ClipboardHelper.GetFromDataObject(clipboard, typeof(IDrawableContainerList)); - if (dcs != null) - { - // Make element(s) only move 10,10 if the surface is the same - bool isSameSurface = (dcs.ParentID == _uniqueId); - dcs.Parent = this; - var moveOffset = isSameSurface ? new NativePoint(10, 10) : NativePoint.Empty; - // Here a fix for bug #1475, first calculate the bounds of the complete IDrawableContainerList - NativeRect drawableContainerListBounds = NativeRect.Empty; - foreach (var element in dcs) - { - drawableContainerListBounds = drawableContainerListBounds == NativeRect.Empty - ? element.DrawingBounds - : drawableContainerListBounds.Union(element.DrawingBounds); - } - - // And find a location inside the target surface to paste to - bool containersCanFit = drawableContainerListBounds.Width < Bounds.Width && drawableContainerListBounds.Height < Bounds.Height; - if (!containersCanFit) - { - NativePoint containersLocation = drawableContainerListBounds.Location; - containersLocation.Offset(moveOffset); - if (!Bounds.Contains(containersLocation)) - { - // Easy fix for same surface - moveOffset = isSameSurface - ? new NativePoint(-10, -10) - : new NativePoint(-drawableContainerListBounds.Location.X + 10, -drawableContainerListBounds.Location.Y + 10); - } - } - else - { - NativeRect moveContainerListBounds = drawableContainerListBounds.Offset(moveOffset); - // check if the element is inside - if (!Bounds.Contains(moveContainerListBounds)) - { - // Easy fix for same surface - if (isSameSurface) - { - moveOffset = new Point(-10, -10); - } - else - { - // For different surface, which is most likely smaller - int offsetX = 0; - int offsetY = 0; - if (drawableContainerListBounds.Right > Bounds.Right) - { - offsetX = Bounds.Right - drawableContainerListBounds.Right; - // Correction for the correction - if (drawableContainerListBounds.Left + offsetX < 0) - { - offsetX += Math.Abs(drawableContainerListBounds.Left + offsetX); - } - } - - if (drawableContainerListBounds.Bottom > Bounds.Bottom) - { - offsetY = Bounds.Bottom - drawableContainerListBounds.Bottom; - // Correction for the correction - if (drawableContainerListBounds.Top + offsetY < 0) - { - offsetY += Math.Abs(drawableContainerListBounds.Top + offsetY); - } - } - - moveOffset = new Point(offsetX, offsetY); - } - } - } - - dcs.MoveBy(moveOffset.X, moveOffset.Y); - AddElements(dcs); - FieldAggregator.BindElements(dcs); - DeselectAllElements(); - SelectElements(dcs); - } - } - else if (ClipboardHelper.ContainsImage(clipboard)) - { - NativePoint pasteLocation = GetPasteLocation(0.1f, 0.1f); - - foreach (var drawableContainer in ClipboardHelper.GetDrawables(clipboard)) - { - if (drawableContainer == null) continue; - DeselectAllElements(); - drawableContainer.Left = pasteLocation.X; - drawableContainer.Top = pasteLocation.Y; - AddElement(drawableContainer); - SelectElement(drawableContainer); - pasteLocation = pasteLocation.Offset(10, 10); - } - } - else if (ClipboardHelper.ContainsText(clipboard)) - { - NativePoint pasteLocation = GetPasteLocation(0.4f, 0.4f); - - string text = ClipboardHelper.GetText(clipboard); - if (text != null) - { - DeselectAllElements(); - ITextContainer textContainer = AddTextContainer(text, pasteLocation.X, pasteLocation.Y, - FontFamily.GenericSansSerif, 12f, false, false, false, 2, Color.Black, Color.Transparent); - SelectElement(textContainer); - } - } - } - - /// - /// Find a location to paste elements. - /// If mouse is over the surface - use it's position, otherwise use the visible area. - /// Return a point in image coordinate space. - /// - /// 0.0f for the left edge of visible area, 1.0f for the right edge of visible area. - /// 0.0f for the top edge of visible area, 1.0f for the bottom edge of visible area. - private NativePoint GetPasteLocation(float horizontalRatio = 0.5f, float verticalRatio = 0.5f) - { - var point = PointToClient(MousePosition); - var rc = GetVisibleRectangle(); - if (!rc.Contains(point)) - { - point = new NativePoint( - rc.Left + (int) (rc.Width * horizontalRatio), - rc.Top + (int) (rc.Height * verticalRatio) - ); - } - - return ToImageCoordinates(point); - } - - /// - /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). - /// - public NativeRect GetVisibleRectangle() - { - var bounds = Bounds; - var clientArea = Parent.ClientRectangle; - return new NativeRect( - Math.Max(0, -bounds.Left), - Math.Max(0, -bounds.Top), - clientArea.Width, - clientArea.Height - ); - } - - /// - /// Get the rectangle bounding all selected elements (in surface coordinates space), - /// or empty rectangle if nothing is selected. - /// - public NativeRect GetSelectionRectangle() - => ToSurfaceCoordinates(selectedElements.DrawingBounds); - - /// - /// Duplicate all the selected elements - /// - public void DuplicateSelectedElements() - { - LOG.DebugFormat("Duplicating {0} selected elements", selectedElements.Count); - IDrawableContainerList dcs = selectedElements.Clone(); - dcs.Parent = this; - dcs.MoveBy(10, 10); - AddElements(dcs); - DeselectAllElements(); - SelectElements(dcs); - } - - /// - /// Deselect the specified element - /// - /// IDrawableContainerList - /// false to skip event generation - public void DeselectElement(IDrawableContainer container, bool generateEvents = true) - { - container.Selected = false; - selectedElements.Remove(container); - FieldAggregator.UnbindElement(container); - if (generateEvents && _movingElementChanged != null) - { - var eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - } - - /// - /// Deselect the specified elements - /// - /// IDrawableContainerList - public void DeselectElements(IDrawableContainerList elements) - { - if (elements.Count == 0) - { - return; - } - - while (elements.Count > 0) - { - var element = elements[0]; - DeselectElement(element, false); - } - - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - Invalidate(); - } - - /// - /// Deselect all the selected elements - /// - public void DeselectAllElements() - { - DeselectElements(selectedElements); - } - - /// - /// Select the supplied element - /// - /// - /// false to skip invalidation - /// false to skip event generation - public void SelectElement(IDrawableContainer container, bool invalidate = true, bool generateEvents = true) - { - if (selectedElements.Contains(container)) return; - - selectedElements.Add(container); - container.Selected = true; - FieldAggregator.BindElement(container); - if (generateEvents && _movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - if (invalidate) - { - container.Invalidate(); - } - } - - /// - /// Select all elements, this is called when Ctrl+A is pressed - /// - public void SelectAllElements() - { - SelectElements(_elements); - } - - /// - /// Select the supplied elements - /// - /// - public void SelectElements(IDrawableContainerList elements) - { - SuspendLayout(); - foreach (var drawableContainer in elements) - { - var element = (DrawableContainer) drawableContainer; - SelectElement(element, false, false); - } - - if (_movingElementChanged != null) - { - SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs - { - Elements = selectedElements - }; - _movingElementChanged(this, eventArgs); - } - - ResumeLayout(); - Invalidate(); - } - - /// - /// Process key presses on the surface, this is called from the editor (and NOT an override from the Control) - /// - /// Keys - /// false if no keys were processed - public bool ProcessCmdKey(Keys k) - { - if (selectedElements.Count <= 0 && k != Keys.Escape) return false; - - bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; - int px = shiftModifier ? 10 : 1; - NativePoint moveBy = NativePoint.Empty; - switch (k) - { - case Keys.Left: - case Keys.Left | Keys.Shift: - moveBy = new NativePoint(-px, 0); - break; - case Keys.Up: - case Keys.Up | Keys.Shift: - moveBy = new NativePoint(0, -px); - break; - case Keys.Right: - case Keys.Right | Keys.Shift: - moveBy = new NativePoint(px, 0); - break; - case Keys.Down: - case Keys.Down | Keys.Shift: - moveBy = new NativePoint(0, px); - break; - case Keys.PageUp: - PullElementsUp(); - break; - case Keys.PageDown: - PushElementsDown(); - break; - case Keys.Home: - PullElementsToTop(); - break; - case Keys.End: - PushElementsToBottom(); - break; - case Keys.Enter: - Confirm(true); - break; - case Keys.Escape: - Confirm(false); - break; - case Keys.D0 | Keys.Control: - case Keys.D0 | Keys.Shift | Keys.Control: - SetSelectedElementColor(shiftModifier ? Color.Orange : Color.Transparent, false, shiftModifier); - break; - case Keys.D1 | Keys.Control: - case Keys.D1 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Red, false, shiftModifier); - break; - case Keys.D2 | Keys.Control: - case Keys.D2 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Green, false, shiftModifier); - break; - case Keys.D3 | Keys.Control: - case Keys.D3 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Blue, false, shiftModifier); - break; - case Keys.D4 | Keys.Control: - case Keys.D4 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Cyan, false, shiftModifier); - break; - case Keys.D5 | Keys.Control: - case Keys.D5 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Magenta, false, shiftModifier); - break; - case Keys.D6 | Keys.Control: - case Keys.D6 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Yellow, false, shiftModifier); - break; - case Keys.D7 | Keys.Control: - case Keys.D7 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Black, false, shiftModifier); - break; - case Keys.D8 | Keys.Control: - case Keys.D8 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.Gray, false, shiftModifier); - break; - case Keys.D9 | Keys.Control: - case Keys.D9 | Keys.Shift | Keys.Control: - SetSelectedElementColor(Color.White, false, shiftModifier); - break; - case Keys.Add | Keys.Control: - case Keys.Add | Keys.Shift | Keys.Control: - ChangeLineThickness(shiftModifier ? 5 : 1); - break; - case Keys.Subtract | Keys.Control: - case Keys.Subtract | Keys.Shift | Keys.Control: - ChangeLineThickness(shiftModifier ? -5 : -1); - break; - case Keys.Divide | Keys.Control: - FlipShadow(); - break; - /*case Keys.Delete: - RemoveSelectedElements(); - break;*/ - default: - return false; - } - - if (moveBy != NativePoint.Empty) - { - selectedElements.MakeBoundsChangeUndoable(true); - selectedElements.MoveBy(moveBy.X, moveBy.Y); - } - - return true; - - } - - // for laptops without numPads, also allow shift modifier - private void SetSelectedElementColor(Color color, bool numPad, bool shift) - { - if (numPad || shift) - { - selectedElements.SetForegroundColor(color); - UpdateForegroundColorEvent(this, color); - } - else - { - selectedElements.SetBackgroundColor(color); - UpdateBackgroundColorEvent(this, color); - } - selectedElements.Invalidate(); - } - - private void ChangeLineThickness(int increaseBy) - { - var newThickness = selectedElements.IncreaseLineThickness(increaseBy); - UpdateLineThicknessEvent(this, newThickness); - selectedElements.Invalidate(); - } - - private void FlipShadow() - { - var shadow = selectedElements.FlipShadow(); - UpdateShadowEvent(this, shadow); - selectedElements.Invalidate(); - } - - /// - /// Property for accessing the elements on the surface - /// - public IDrawableContainerList Elements => _elements; - - /// - /// pulls selected elements up one level in hierarchy - /// - public void PullElementsUp() - { - _elements.PullElementsUp(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements up to top in hierarchy - /// - public void PullElementsToTop() - { - _elements.PullElementsToTop(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements down one level in hierarchy - /// - public void PushElementsDown() - { - _elements.PushElementsDown(selectedElements); - _elements.Invalidate(); - } - - /// - /// pushes selected elements down to bottom in hierarchy - /// - public void PushElementsToBottom() - { - _elements.PushElementsToBottom(selectedElements); - _elements.Invalidate(); - } - - /// - /// indicates whether the selected elements could be pulled up in hierarchy - /// - /// true if selected elements could be pulled up, false otherwise - public bool CanPullSelectionUp() - { - return _elements.CanPullUp(selectedElements); - } - - /// - /// indicates whether the selected elements could be pushed down in hierarchy - /// - /// true if selected elements could be pushed down, false otherwise - public bool CanPushSelectionDown() - { - return _elements.CanPushDown(selectedElements); - } - - private void Element_FieldChanged(object sender, FieldChangedEventArgs e) - { - selectedElements.HandleFieldChangedEvent(sender, e); - } - - public bool IsOnSurface(IDrawableContainer container) - { - return _elements.Contains(container); - } - - public NativePoint ToSurfaceCoordinates(NativePoint point) - { - Point[] points = - { - point - }; - _zoomMatrix.TransformPoints(points); - return points[0]; - } - - public NativeRect ToSurfaceCoordinates(NativeRect rc) - { - if (_zoomMatrix.IsIdentity) - { - return rc; - } - - Point[] points = - { - rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) - }; - _zoomMatrix.TransformPoints(points); - return new NativeRect( - points[0].X, - points[0].Y, - points[1].X - points[0].X, - points[1].Y - points[0].Y - ); - } - - public NativePoint ToImageCoordinates(NativePoint point) - { - Point[] points = - { - point - }; - _inverseZoomMatrix.TransformPoints(points); - return points[0]; - } - - public NativeRect ToImageCoordinates(NativeRect rc) - { - if (_inverseZoomMatrix.IsIdentity) - { - return rc; - } - - Point[] points = - { - rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) - }; - _inverseZoomMatrix.TransformPoints(points); - return new NativeRect( - points[0].X, - points[0].Y, - points[1].X - points[0].X, - points[1].Y - points[0].Y - ); - } - } + } + catch (Exception e) + { + LOG.Error("Error serializing elements from stream.", e); + } + } + + /// + /// This is called from the DrawingMode setter, which is not very correct... + /// But here an element is created which is not yet draw, thus "undrawnElement". + /// The element is than used while drawing on the surface. + /// + private void CreateUndrawnElement() + { + if (_undrawnElement != null) + { + FieldAggregator.UnbindElement(_undrawnElement); + } + + switch (DrawingMode) + { + case DrawingModes.Rect: + _undrawnElement = new RectangleContainer(this); + break; + case DrawingModes.Ellipse: + _undrawnElement = new EllipseContainer(this); + break; + case DrawingModes.Text: + _undrawnElement = new TextContainer(this); + break; + case DrawingModes.SpeechBubble: + _undrawnElement = new SpeechbubbleContainer(this); + break; + case DrawingModes.StepLabel: + _undrawnElement = new StepLabelContainer(this); + break; + case DrawingModes.Line: + _undrawnElement = new LineContainer(this); + break; + case DrawingModes.Arrow: + _undrawnElement = new ArrowContainer(this); + break; + case DrawingModes.Highlight: + _undrawnElement = new HighlightContainer(this); + break; + case DrawingModes.Obfuscate: + _undrawnElement = new ObfuscateContainer(this); + break; + case DrawingModes.Crop: + _cropContainer = new CropContainer(this); + _undrawnElement = _cropContainer; + break; + case DrawingModes.Bitmap: + _undrawnElement = new ImageContainer(this); + break; + case DrawingModes.Path: + _undrawnElement = new FreehandContainer(this); + break; + case DrawingModes.None: + _undrawnElement = null; + break; + } + + if (_undrawnElement != null) + { + FieldAggregator.BindElement(_undrawnElement); + } + } + + #region Plugin interface implementations + + public IImageContainer AddImageContainer(Image image, int x, int y) + { + ImageContainer bitmapContainer = new ImageContainer(this) + { + Image = image, + Left = x, + Top = y + }; + AddElement(bitmapContainer); + return bitmapContainer; + } + + public IImageContainer AddImageContainer(string filename, int x, int y) + { + ImageContainer bitmapContainer = new ImageContainer(this); + bitmapContainer.Load(filename); + bitmapContainer.Left = x; + bitmapContainer.Top = y; + AddElement(bitmapContainer); + return bitmapContainer; + } + + public IIconContainer AddIconContainer(Icon icon, int x, int y) + { + IconContainer iconContainer = new IconContainer(this) + { + Icon = icon, + Left = x, + Top = y + }; + AddElement(iconContainer); + return iconContainer; + } + + public IIconContainer AddIconContainer(string filename, int x, int y) + { + IconContainer iconContainer = new IconContainer(this); + iconContainer.Load(filename); + iconContainer.Left = x; + iconContainer.Top = y; + AddElement(iconContainer); + return iconContainer; + } + + public ICursorContainer AddCursorContainer(Cursor cursor, int x, int y) + { + CursorContainer cursorContainer = new CursorContainer(this) + { + Cursor = cursor, + Left = x, + Top = y + }; + AddElement(cursorContainer); + return cursorContainer; + } + + public ICursorContainer AddCursorContainer(string filename, int x, int y) + { + CursorContainer cursorContainer = new CursorContainer(this); + cursorContainer.Load(filename); + cursorContainer.Left = x; + cursorContainer.Top = y; + AddElement(cursorContainer); + return cursorContainer; + } + + public ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, + Color fillColor) + { + TextContainer textContainer = new TextContainer(this) + { + Text = text, + Left = x, + Top = y + }; + textContainer.SetFieldValue(FieldType.FONT_FAMILY, family.Name); + textContainer.SetFieldValue(FieldType.FONT_BOLD, bold); + textContainer.SetFieldValue(FieldType.FONT_ITALIC, italic); + textContainer.SetFieldValue(FieldType.FONT_SIZE, size); + textContainer.SetFieldValue(FieldType.FILL_COLOR, fillColor); + textContainer.SetFieldValue(FieldType.LINE_COLOR, color); + textContainer.SetFieldValue(FieldType.LINE_THICKNESS, borderSize); + textContainer.SetFieldValue(FieldType.SHADOW, shadow); + // Make sure the Text fits + textContainer.FitToText(); + + //AggregatedProperties.UpdateElement(textContainer); + AddElement(textContainer); + return textContainer; + } + + #endregion + + #region DragDrop + + private void OnDragEnter(object sender, DragEventArgs e) + { + if (LOG.IsDebugEnabled) + { + LOG.Debug("DragEnter got following formats: "); + foreach (string format in ClipboardHelper.GetFormats(e.Data)) + { + LOG.Debug(format); + } + } + + if ((e.AllowedEffect & DragDropEffects.Copy) != DragDropEffects.Copy) + { + e.Effect = DragDropEffects.None; + } + else + { + if (ClipboardHelper.ContainsImage(e.Data) || ClipboardHelper.ContainsFormat(e.Data, "DragImageBits")) + { + e.Effect = DragDropEffects.Copy; + } + else + { + e.Effect = DragDropEffects.None; + } + } + } + + /// + /// This will help to fit the container to the surface + /// + /// IDrawableContainer + private void FitContainer(IDrawableContainer drawableContainer) + { + double factor = 1; + if (drawableContainer.Width > this.Width) + { + factor = drawableContainer.Width / (double)Width; + } + if (drawableContainer.Height > this.Height) + { + var otherFactor = drawableContainer.Height / (double)Height; + factor = Math.Max(factor, otherFactor); + } + + drawableContainer.Width = (int)(drawableContainer.Width / factor); + drawableContainer.Height = (int)(drawableContainer.Height / factor); + } + + /// + /// Handle the drag/drop + /// + /// + /// + private void OnDragDrop(object sender, DragEventArgs e) + { + NativePoint mouse = PointToClient(new NativePoint(e.X, e.Y)); + if (e.Data.GetDataPresent("Text")) + { + string possibleUrl = ClipboardHelper.GetText(e.Data); + // Test if it's an url and try to download the image so we have it in the original form + if (possibleUrl != null && possibleUrl.StartsWith("http")) + { + var drawableContainer = NetworkHelper.DownloadImageAsDrawableContainer(possibleUrl); + if (drawableContainer != null) + { + drawableContainer.Left = Location.X; + drawableContainer.Top = Location.Y; + FitContainer(drawableContainer); + AddElement(drawableContainer); + return; + } + } + } + + foreach (var drawableContainer in ClipboardHelper.GetDrawables(e.Data)) + { + drawableContainer.Left = mouse.X; + drawableContainer.Top = mouse.Y; + FitContainer(drawableContainer); + AddElement(drawableContainer); + mouse = mouse.Offset(10, 10); + } + } + + #endregion + + /// + /// Auto crop the image + /// + /// NativeRect with optional area to find a crop region + /// true if cropped + public bool AutoCrop(NativeRect? cropArea = null) + { + NativeRect cropRectangle; + using (Image tmpImage = GetImageForExport()) + { + cropRectangle = ImageHelper.FindAutoCropRectangle(tmpImage, conf.AutoCropDifference, cropArea); + } + + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.AutoCrop)) + { + return false; + } + + DeselectAllElements(); + // Maybe a bit obscure, but the following line creates a drop container + // It's available as "undrawnElement" + DrawingMode = DrawingModes.Crop; + _undrawnElement.Left = cropRectangle.X; + _undrawnElement.Top = cropRectangle.Y; + _undrawnElement.Width = cropRectangle.Width; + _undrawnElement.Height = cropRectangle.Height; + _undrawnElement.Status = EditStatus.UNDRAWN; + AddElement(_undrawnElement); + SelectElement(_undrawnElement); + _drawingElement = null; + _undrawnElement = null; + return true; + } + + /// + /// A simple clear + /// + /// The color for the background + public void Clear(Color newColor) + { + //create a blank bitmap the same size as original + Bitmap newBitmap = ImageHelper.CreateEmptyLike(Image, newColor); + if (newBitmap == null) return; + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, null), false); + SetImage(newBitmap, false); + Invalidate(); + } + + /// + /// Apply a bitmap effect to the surface + /// + /// + public void ApplyBitmapEffect(IEffect effect) + { + BackgroundForm backgroundForm = new BackgroundForm("Effect", "Please wait"); + backgroundForm.Show(); + Application.DoEvents(); + try + { + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + Matrix matrix = new Matrix(); + Image newImage = ImageHelper.ApplyEffect(Image, effect, matrix); + if (newImage != null) + { + // Make sure the elements move according to the offset the effect made the bitmap move + _elements.Transform(matrix); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + SetImage(newImage, false); + Invalidate(); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + } + else + { + // clean up matrix, as it hasn't been used in the undo stack. + matrix.Dispose(); + } + } + finally + { + // Always close the background form + backgroundForm.CloseDialog(); + } + } + + /// + /// check if a crop is possible + /// + /// Rectangle adapted to the dimensions of the image + /// CropModes + /// true if this is possible + public bool IsCropPossible(ref NativeRect cropRectangle, CropContainer.CropModes cropMode) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, cropRectangle.Height).Normalize(); + //Fitting the rectangle to the dimensions of the image + if (cropRectangle.Left < 0) + { + cropRectangle = new NativeRect(0, cropRectangle.Top, cropRectangle.Width + cropRectangle.Left, cropRectangle.Height); + } + + if (cropRectangle.Top < 0) + { + cropRectangle = new NativeRect(cropRectangle.Left, 0, cropRectangle.Width, cropRectangle.Height + cropRectangle.Top); + } + + if (cropRectangle.Left + cropRectangle.Width > Image.Width) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, Image.Width - cropRectangle.Left, cropRectangle.Height); + } + + if (cropRectangle.Top + cropRectangle.Height > Image.Height) + { + cropRectangle = new NativeRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Image.Height - cropRectangle.Top); + } + + // special condition for vertical + if(cropMode == CropContainer.CropModes.Vertical && cropRectangle.Width == Image.Width) + { + //crop out the hole image is not allowed + return false; + } + + // special condition for vertical + if (cropMode == CropContainer.CropModes.Horizontal && cropRectangle.Height == Image.Height) + { + //crop out the hole image is not allowed + return false; + } + + //condition for all other crop modes + if (cropRectangle.Height > 0 && cropRectangle.Width > 0) + { + return true; + } + + return false; + } + + /// + /// Use to send any registered SurfaceMessageEventHandler a message, e.g. used for the notification area + /// + /// Who send + /// Type of message + /// Message itself + public void SendMessageEvent(object source, SurfaceMessageTyp messageType, string message) + { + if (_surfaceMessage == null) return; + + var eventArgs = new SurfaceMessageEventArgs + { + Message = message, + MessageType = messageType, + Surface = this + }; + _surfaceMessage(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the foreground color + /// + /// Who send + /// new color + private void UpdateForegroundColorEvent(object source, Color color) + { + if (_foregroundColorChanged == null) return; + + var eventArgs = new SurfaceForegroundColorEventArgs + { + Color = color, + }; + _foregroundColorChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the background color + /// + /// Who send + /// new color + private void UpdateBackgroundColorEvent(object source, Color color) + { + if (_lineThicknessChanged == null) return; + + var eventArgs = new SurfaceBackgroundColorEventArgs + { + Color = color, + }; + _backgroundColorChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing a key to change the line thickness + /// + /// Who send + /// new thickness + private void UpdateLineThicknessEvent(object source, int thickness) + { + if (_lineThicknessChanged == null) return; + + var eventArgs = new SurfaceLineThicknessEventArgs + { + Thickness = thickness, + }; + _lineThicknessChanged(source, eventArgs); + } + + /// + /// Use to update UI when pressing the key to show/hide the shadow + /// + /// Who send + /// has shadow + private void UpdateShadowEvent(object source, bool hasShadow) + { + if (_shadowChanged == null) return; + + var eventArgs = new SurfaceShadowEventArgs + { + HasShadow = hasShadow, + }; + _shadowChanged(source, eventArgs); + } + + /// + /// Crop the surface + /// + /// NativeRect that remains + /// bool + public bool ApplyCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Default)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + Bitmap tmpImage; + // Make sure we have information, this this fails + try + { + tmpImage = ImageHelper.CloneArea(Image, cropRectangle, PixelFormat.DontCare); + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + + var matrix = new Matrix(); + matrix.Translate(-cropRectangle.Left, -cropRectangle.Top, MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(tmpImage, false); + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, tmpImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// Crop out the surface + /// Splits the image in 3 parts(top, middle, bottom). Crop out the middle and joins top and bottom. + /// + /// NativeRect of the middle part + /// bool + private bool ApplyHorizontalCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Horizontal)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + var topRectangle = new NativeRect(0, 0, Image.Size.Width, cropRectangle.Top); + var bottomRectangle = new NativeRect(0, cropRectangle.Top + cropRectangle.Height, Image.Size.Width, Image.Size.Height - cropRectangle.Top - cropRectangle.Height); + + Bitmap newImage; + try + { + newImage = new Bitmap(Image.Size.Width, Image.Size.Height - cropRectangle.Height); + + using var graphics = Graphics.FromImage(newImage); + + var insertPositionTop = 0; + if (topRectangle.Height > 0) + { + graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, topRectangle.Width, topRectangle.Height), topRectangle, GraphicsUnit.Pixel); + insertPositionTop += topRectangle.Height; + } + if (bottomRectangle.Height > 0) + { + graphics.DrawImage(Image, new NativeRect(0, insertPositionTop, bottomRectangle.Width, bottomRectangle.Height), bottomRectangle, GraphicsUnit.Pixel); + } + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + var matrix = new Matrix(); + matrix.Translate(0, -(cropRectangle.Top + cropRectangle.Height), MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(newImage, false); + + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// Crop out the surface + /// Splits the image in 3 parts(left, middle, right). Crop out the middle and joins top and bottom. + /// + /// NativeRect of the middle part + /// bool + private bool ApplyVerticalCrop(NativeRect cropRectangle) + { + if (!IsCropPossible(ref cropRectangle, CropContainer.CropModes.Vertical)) return false; + + var imageRectangle = new NativeRect(NativePoint.Empty, Image.Size); + var leftRectangle = new NativeRect(0, 0, cropRectangle.Left, Image.Size.Height); + var rightRectangle = new NativeRect(cropRectangle.Left + cropRectangle.Width, 0, Image.Size.Width - cropRectangle.Width - cropRectangle.Left, Image.Size.Height); + Bitmap newImage; + try + { + newImage = new Bitmap(Image.Size.Width - cropRectangle.Width, Image.Size.Height); + + using var graphics = Graphics.FromImage(newImage); + + var insertPositionLeft = 0; + if (leftRectangle.Width > 0) + { + graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, leftRectangle.Width, leftRectangle.Height), leftRectangle , GraphicsUnit.Pixel); + insertPositionLeft += leftRectangle.Width; + } + + if (rightRectangle.Width > 0) + { + graphics.DrawImage(Image, new NativeRect(insertPositionLeft, 0, rightRectangle.Width, rightRectangle.Height), rightRectangle, GraphicsUnit.Pixel); + } + } + catch (Exception ex) + { + ex.Data.Add("CropRectangle", cropRectangle); + ex.Data.Add("Width", Image.Width); + ex.Data.Add("Height", Image.Height); + ex.Data.Add("Pixelformat", Image.PixelFormat); + throw; + } + var matrix = new Matrix(); + matrix.Translate(-cropRectangle.Left - cropRectangle.Width, 0, MatrixOrder.Append); + // Make undoable + MakeUndoable(new SurfaceBackgroundChangeMemento(this, matrix), false); + + // Do not dispose otherwise we can't undo the image! + SetImage(newImage, false); + + _elements.Transform(matrix); + if (_surfaceSizeChanged != null && !imageRectangle.Equals(new NativeRect(NativePoint.Empty, newImage.Size))) + { + _surfaceSizeChanged(this, null); + } + + Invalidate(); + return true; + } + + /// + /// The background here is the captured image. + /// This is called from the SurfaceBackgroundChangeMemento. + /// + /// + /// + public void UndoBackgroundChange(Image previous, Matrix matrix) + { + SetImage(previous, false); + if (matrix != null) + { + _elements.Transform(matrix); + } + + _surfaceSizeChanged?.Invoke(this, null); + Invalidate(); + } + + /// + /// Check if an adorner was "hit", and change the cursor if so + /// + /// MouseEventArgs + /// IAdorner + private IAdorner FindActiveAdorner(MouseEventArgs mouseEventArgs) + { + foreach (IDrawableContainer drawableContainer in selectedElements) + { + foreach (IAdorner adorner in drawableContainer.Adorners) + { + if (!adorner.IsActive && !adorner.HitTest(mouseEventArgs.Location)) continue; + + if (adorner.Cursor != null) + { + Cursor = adorner.Cursor; + } + + return adorner; + } + } + + return null; + } + + /// + /// Translate mouse coordinates as if they were applied directly to unscaled image. + /// + private MouseEventArgs InverseZoomMouseCoordinates(MouseEventArgs e) + => new MouseEventArgs(e.Button, e.Clicks, (int) (e.X / _zoomFactor), (int) (e.Y / _zoomFactor), e.Delta); + + /// + /// This event handler is called when someone presses the mouse on a surface. + /// + /// + /// + private void SurfaceMouseDown(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseDown(sender, e); + return; + } + + _mouseStart = e.Location; + + // check contextmenu + if (e.Button == MouseButtons.Right) + { + IDrawableContainerList selectedList = null; + if (selectedElements != null && selectedElements.Count > 0) + { + selectedList = selectedElements; + } + else + { + // Single element + IDrawableContainer rightClickedContainer = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); + if (rightClickedContainer != null) + { + selectedList = new DrawableContainerList(ID) + { + rightClickedContainer + }; + } + } + + if (selectedList != null && selectedList.Count > 0) + { + selectedList.ShowContextMenu(e, this); + } + + return; + } + + _mouseDown = true; + _isSurfaceMoveMadeUndoable = false; + + if (_cropContainer != null && ((_undrawnElement == null) || (_undrawnElement != null && DrawingMode != DrawingModes.Crop))) + { + RemoveElement(_cropContainer, false); + _cropContainer = null; + _drawingElement = null; + } + + if (_drawingElement == null && DrawingMode != DrawingModes.None) + { + if (_undrawnElement == null) + { + DeselectAllElements(); + if (_undrawnElement == null) + { + CreateUndrawnElement(); + } + } + + _drawingElement = _undrawnElement; + // if a new element has been drawn, set location and register it + if (_drawingElement != null) + { + if (_undrawnElement != null) + { + _drawingElement.Status = _undrawnElement.DefaultEditMode; + } + + if (!_drawingElement.HandleMouseDown(_mouseStart.X, _mouseStart.Y)) + { + _drawingElement.Left = _mouseStart.X; + _drawingElement.Top = _mouseStart.Y; + } + + AddElement(_drawingElement); + _drawingElement.Selected = true; + } + + _undrawnElement = null; + } + else + { + // check whether an existing element was clicked + // we save mouse down element separately from selectedElements (checked on mouse up), + // since it could be moved around before it is actually selected + _mouseDownElement = _elements.ClickableElementAt(_mouseStart.X, _mouseStart.Y); + + if (_mouseDownElement != null) + { + _mouseDownElement.Status = EditStatus.MOVING; + } + } + } + + /// + /// This event handle is called when the mouse button is unpressed + /// + /// + /// + private void SurfaceMouseUp(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseUp(sender, e); + return; + } + + Point currentMouse = new Point(e.X, e.Y); + + _elements.Status = EditStatus.IDLE; + if (_mouseDownElement != null) + { + _mouseDownElement.Status = EditStatus.IDLE; + } + + _mouseDown = false; + _mouseDownElement = null; + if (DrawingMode == DrawingModes.None) + { + // check whether an existing element was clicked + IDrawableContainer element = _elements.ClickableElementAt(currentMouse.X, currentMouse.Y); + bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; + if (element != null) + { + element.Invalidate(); + bool alreadySelected = selectedElements.Contains(element); + if (shiftModifier) + { + if (alreadySelected) + { + DeselectElement(element); + } + else + { + SelectElement(element); + } + } + else + { + if (!alreadySelected) + { + DeselectAllElements(); + SelectElement(element); + } + } + } + else if (!shiftModifier) + { + DeselectAllElements(); + } + } + + if (selectedElements.Count > 0) + { + selectedElements.Invalidate(); + selectedElements.Selected = true; + } + + if (_drawingElement != null) + { + if (!_drawingElement.InitContent()) + { + _elements.Remove(_drawingElement); + _drawingElement.Invalidate(); + } + else + { + _drawingElement.HandleMouseUp(currentMouse.X, currentMouse.Y); + _drawingElement.Invalidate(); + if (Math.Abs(_drawingElement.Width) < 5 && Math.Abs(_drawingElement.Height) < 5) + { + _drawingElement.Width = 25; + _drawingElement.Height = 25; + } + + SelectElement(_drawingElement); + _drawingElement.Selected = true; + } + + _drawingElement = null; + } + } + + /// + /// This event handler is called when the mouse moves over the surface + /// + /// + /// + private void SurfaceMouseMove(object sender, MouseEventArgs e) + { + e = InverseZoomMouseCoordinates(e); + + // Handle Adorners + var adorner = FindActiveAdorner(e); + if (adorner != null) + { + adorner.MouseMove(sender, e); + return; + } + + Point currentMouse = e.Location; + + Cursor = DrawingMode != DrawingModes.None ? Cursors.Cross : Cursors.Default; + + if (!_mouseDown) return; + + if (_mouseDownElement != null) + { + // an element is currently dragged + _mouseDownElement.Invalidate(); + selectedElements.Invalidate(); + // Move the element + if (_mouseDownElement.Selected) + { + if (!_isSurfaceMoveMadeUndoable) + { + // Only allow one undoable per mouse-down/move/up "cycle" + _isSurfaceMoveMadeUndoable = true; + selectedElements.MakeBoundsChangeUndoable(false); + } + + // dragged element has been selected before -> move all + selectedElements.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); + } + else + { + if (!_isSurfaceMoveMadeUndoable) + { + // Only allow one undoable per mouse-down/move/up "cycle" + _isSurfaceMoveMadeUndoable = true; + _mouseDownElement.MakeBoundsChangeUndoable(false); + } + + // dragged element is not among selected elements -> just move dragged one + _mouseDownElement.MoveBy(currentMouse.X - _mouseStart.X, currentMouse.Y - _mouseStart.Y); + } + + _mouseStart = currentMouse; + _mouseDownElement.Invalidate(); + _modified = true; + } + else if (_drawingElement != null) + { + _drawingElement.HandleMouseMove(currentMouse.X, currentMouse.Y); + _modified = true; + } + } + + /// + /// This event handler is called when the surface is double clicked. + /// + /// + /// + private void SurfaceDoubleClick(object sender, MouseEventArgs e) + { + selectedElements.OnDoubleClick(); + selectedElements.Invalidate(); + } + + /// + /// Privately used to get the rendered image with all the elements on it. + /// + /// + /// + private Image GetImage(RenderMode renderMode) + { + // Generate a copy of the original image with a dpi equal to the default... + Bitmap clone = ImageHelper.Clone(_image, PixelFormat.DontCare); + // otherwise we would have a problem drawing the image to the surface... :( + using (Graphics graphics = Graphics.FromImage(clone)) + { + // Do not set the following, the containers need to decide themselves + //graphics.SmoothingMode = SmoothingMode.HighQuality; + //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + //graphics.CompositingQuality = CompositingQuality.HighQuality; + //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + _elements.Draw(graphics, clone, renderMode, new Rectangle(Point.Empty, clone.Size)); + } + + return clone; + } + + /// + /// This returns the image "result" of this surface, with all the elements rendered on it. + /// + /// + public Image GetImageForExport() + { + return GetImage(RenderMode.EXPORT); + } + + private static NativeRect ZoomClipRectangle(NativeRect rc, double scale, int inflateAmount = 0) + { + rc = new NativeRect( + (int) (rc.X * scale), + (int) (rc.Y * scale), + (int) (rc.Width * scale) + 1, + (int) (rc.Height * scale) + 1 + ); + if (scale > 1) + { + inflateAmount = (int) (inflateAmount * scale); + } + + return rc.Inflate(inflateAmount, inflateAmount); + } + + public void InvalidateElements(NativeRect rc) + => Invalidate(ZoomClipRectangle(rc, _zoomFactor, 1)); + + /// + /// This is the event handler for the Paint Event, try to draw as little as possible! + /// + /// + /// PaintEventArgs + private void SurfacePaint(object sender, PaintEventArgs paintEventArgs) + { + Graphics targetGraphics = paintEventArgs.Graphics; + NativeRect targetClipRectangle = paintEventArgs.ClipRectangle; + if (targetClipRectangle.IsEmpty) + { + LOG.Debug("Empty cliprectangle??"); + return; + } + + // Correction to prevent rounding errors at certain zoom levels. + // When zooming to N/M, clip rectangle top and left coordinates should be multiples of N. + if (_zoomFactor.Numerator > 1 && _zoomFactor.Denominator > 1) + { + int horizontalCorrection = targetClipRectangle.Left % (int) _zoomFactor.Numerator; + int verticalCorrection = targetClipRectangle.Top % (int) _zoomFactor.Numerator; + if (horizontalCorrection != 0) + { + targetClipRectangle = targetClipRectangle + .ChangeX(targetClipRectangle.X - horizontalCorrection) + .ChangeWidth(targetClipRectangle.X + horizontalCorrection); + } + + if (verticalCorrection != 0) + { + targetClipRectangle = targetClipRectangle + .ChangeY(targetClipRectangle.Y - verticalCorrection) + .ChangeHeight(targetClipRectangle.Y + verticalCorrection); + } + } + + NativeRect imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); + + if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) + { + if (_buffer != null) + { + if (_buffer.Width != Image.Width || _buffer.Height != Image.Height || _buffer.PixelFormat != Image.PixelFormat) + { + _buffer.Dispose(); + _buffer = null; + } + } + + if (_buffer == null) + { + _buffer = ImageHelper.CreateEmpty(Image.Width, Image.Height, Image.PixelFormat, Color.Empty, Image.HorizontalResolution, Image.VerticalResolution); + LOG.DebugFormat("Created buffer with size: {0}x{1}", Image.Width, Image.Height); + } + + // Elements might need the bitmap, so we copy the part we need + using (Graphics graphics = Graphics.FromImage(_buffer)) + { + // do not set the following, the containers need to decide this themselves! + //graphics.SmoothingMode = SmoothingMode.HighQuality; + //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + //graphics.CompositingQuality = CompositingQuality.HighQuality; + //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + DrawBackground(graphics, imageClipRectangle); + graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); + _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); + } + + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + else + { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + if (_zoomFactor > Fraction.Identity) + { + DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); + } + else + { + DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); + } + + targetGraphics.ResetTransform(); + } + } + else + { + DrawBackground(targetGraphics, targetClipRectangle); + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + } + else + { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + DrawSmoothImage(targetGraphics, Image, imageClipRectangle); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + targetGraphics.ResetTransform(); + } + } + + // No clipping for the adorners + targetGraphics.ResetClip(); + // Draw adorners last + foreach (var drawableContainer in selectedElements) + { + foreach (var adorner in drawableContainer.Adorners) + { + adorner.Paint(paintEventArgs); + } + } + } + + private void DrawSmoothImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.HighQuality; + targetGraphics.InterpolationMode = InterpolationMode.HighQualityBilinear; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + + private void DrawSharpImage(Graphics targetGraphics, Image image, NativeRect imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.None; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + + private void DrawBackground(Graphics targetGraphics, NativeRect clipRectangle) + { + // check if we need to draw the checkerboard + if (Image.IsAlphaPixelFormat(Image.PixelFormat) && _transparencyBackgroundBrush != null) + { + targetGraphics.FillRectangle(_transparencyBackgroundBrush, clipRectangle); + } + else + { + targetGraphics.Clear(BackColor); + } + } + + /// + /// Draw a checkboard when capturing with transparency + /// + /// PaintEventArgs + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + /// + /// Add a new element to the surface + /// + /// the new element + /// true if the adding should be undoable + /// true if invalidate needs to be called + public void AddElement(IDrawableContainer element, bool makeUndoable = true, bool invalidate = true) + { + _elements.Add(element); + if (element is DrawableContainer container) + { + container.FieldChanged += Element_FieldChanged; + } + + element.Parent = this; + if (element.Status == EditStatus.UNDRAWN) + { + element.Status = EditStatus.IDLE; + } + + if (element.Selected) + { + // Use false, as the element is invalidated when invalidate == true anyway + SelectElement(element, false); + } + + if (invalidate) + { + element.Invalidate(); + } + + if (makeUndoable && element.IsUndoable) + { + MakeUndoable(new AddElementMemento(this, element), false); + } + + _modified = true; + } + + /// + /// Remove the list of elements + /// + /// IDrawableContainerList + /// flag specifying if the remove needs to be undoable + public void RemoveElements(IDrawableContainerList elementsToRemove, bool makeUndoable = true) + { + // fix potential issues with iterating a changing list + DrawableContainerList cloned = new DrawableContainerList(elementsToRemove); + + if (makeUndoable) + { + // Take all containers to make undoable + var undoableContainers = elementsToRemove.Where(c => c.IsUndoable).ToList(); + if (undoableContainers.Any()) + { + var undoableContainerList = new DrawableContainerList(undoableContainers); + MakeUndoable(new DeleteElementsMemento(this, undoableContainerList), false); + } + } + + SuspendLayout(); + foreach (var drawableContainer in cloned) + { + RemoveElement(drawableContainer, false, false, false); + } + + ResumeLayout(); + Invalidate(); + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = cloned + }; + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Remove an element of the elements list + /// + /// Element to remove + /// flag specifying if the remove needs to be undoable + /// flag specifying if an surface invalidate needs to be called + /// false to skip event generation + public void RemoveElement(IDrawableContainer elementToRemove, bool makeUndoable = true, bool invalidate = true, bool generateEvents = true) + { + DeselectElement(elementToRemove, generateEvents); + _elements.Remove(elementToRemove); + if (elementToRemove is DrawableContainer element) + { + element.FieldChanged -= Element_FieldChanged; + } + + if (elementToRemove != null) + { + elementToRemove.Parent = null; + } + + // Do not dispose, the memento should!! element.Dispose(); + if (invalidate) + { + Invalidate(); + } + + if (makeUndoable && elementToRemove is { IsUndoable: true }) + { + MakeUndoable(new DeleteElementMemento(this, elementToRemove), false); + } + + _modified = true; + } + + /// + /// Add the supplied elements to the surface + /// + /// DrawableContainerList + /// true if the adding should be undoable + public void AddElements(IDrawableContainerList elementsToAdd, bool makeUndoable = true) + { + // fix potential issues with iterating a changing list + DrawableContainerList cloned = new DrawableContainerList(elementsToAdd); + if (makeUndoable) + { + // Take all containers to make undoable + var undoableContainers = elementsToAdd.Where(c => c.IsUndoable).ToList(); + if (undoableContainers.Any()) + { + var undoableContainerList = new DrawableContainerList(undoableContainers); + MakeUndoable(new AddElementsMemento(this, undoableContainerList), false); + } + } + + SuspendLayout(); + foreach (var element in cloned) + { + element.Selected = true; + AddElement(element, false, false); + } + + ResumeLayout(); + Invalidate(); + } + + /// + /// Returns if this surface has selected elements + /// + /// bool + public bool HasSelectedElements => selectedElements is { Count: > 0 }; + + /// + /// Provides the selected elements + /// + public IDrawableContainerList SelectedElements => selectedElements; + + /// + /// Remove all the selected elements + /// + public void RemoveSelectedElements() + { + if (!HasSelectedElements) return; + + // As RemoveElement will remove the element from the selectedElements list we need to copy the element to another list. + RemoveElements(selectedElements); + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs(); + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Cut the selected elements from the surface to the clipboard + /// + public void CutSelectedElements() + { + if (!HasSelectedElements) return; + ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); + RemoveSelectedElements(); + } + + /// + /// Copy the selected elements to the clipboard + /// + public void CopySelectedElements() + { + if (!HasSelectedElements) return; + ClipboardHelper.SetClipboardData(typeof(IDrawableContainerList), selectedElements); + } + + /// + /// This method is called to confirm/cancel. + /// Called when pressing enter or using the "check" in the editor. + /// redirects to the specialized confirm/cancel method + /// + /// bool + public void Confirm(bool confirm) + { + if (DrawingMode == DrawingModes.Crop) + { + ConfirmCrop(confirm); + } + else + { + ConfirmSelectedConfirmableElements(confirm); + } + } + + /// + /// This method is called to confirm/cancel "confirmable" elements + /// Called when pressing enter or using the "check" in the editor. + ///
+ /// For crop-container there is a dedicated method . + ///
+ /// bool + public void ConfirmSelectedConfirmableElements(bool confirm) + { + // create new collection so that we can iterate safely (selectedElements might change due with confirm/cancel) + List selectedDCs = new List(selectedElements); + foreach (IDrawableContainer dc in selectedDCs.Where(c => c.IsConfirmable)) + { + throw new NotImplementedException($"No confirm/cancel defined for Container type {dc.GetType()}"); + } + + // maybe the undo button has to be enabled + _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); + } + + /// + /// This method is called to confirm/cancel the crop-container. + /// Called when pressing enter or using the "check" in the editor. + /// + /// bool + public void ConfirmCrop(bool confirm) + { + if (_cropContainer is not CropContainer e) return; + + if (confirm && selectedElements.Count > 0) + { + // No undo memento for the cropcontainer itself, only for the effect + RemoveElement(_cropContainer, false); + + _ = e.GetFieldValue(FieldType.CROPMODE) switch + { + CropContainer.CropModes.Horizontal => ApplyHorizontalCrop(_cropContainer.Bounds), + CropContainer.CropModes.Vertical => ApplyVerticalCrop(_cropContainer.Bounds), + _ => ApplyCrop(_cropContainer.Bounds) + }; + + _cropContainer.Dispose(); + _cropContainer = null; + } + else + { + RemoveCropContainer(); + } + + DrawingMode = DrawingModes.None; + + // maybe the undo button has to be enabled + _movingElementChanged?.Invoke(this, new SurfaceElementEventArgs()); + } + + public void RemoveCropContainer() + { + if (_cropContainer == null) return; + + RemoveElement(_cropContainer, false); + _cropContainer.Dispose(); + _cropContainer = null; + } + + /// + /// Paste all the elements that are on the clipboard + /// + public void PasteElementFromClipboard() + { + IDataObject clipboard = ClipboardHelper.GetDataObject(); + + var formats = ClipboardHelper.GetFormats(clipboard); + if (formats == null || formats.Count == 0) + { + return; + } + + if (LOG.IsDebugEnabled) + { + LOG.Debug("List of clipboard formats available for pasting:"); + foreach (string format in formats) + { + LOG.Debug("\tgot format: " + format); + } + } + + if (formats.Contains(typeof(IDrawableContainerList).FullName)) + { + IDrawableContainerList dcs = (IDrawableContainerList) ClipboardHelper.GetFromDataObject(clipboard, typeof(IDrawableContainerList)); + if (dcs != null) + { + // Make element(s) only move 10,10 if the surface is the same + bool isSameSurface = (dcs.ParentID == _uniqueId); + dcs.Parent = this; + var moveOffset = isSameSurface ? new NativePoint(10, 10) : NativePoint.Empty; + // Here a fix for bug #1475, first calculate the bounds of the complete IDrawableContainerList + NativeRect drawableContainerListBounds = NativeRect.Empty; + foreach (var element in dcs) + { + drawableContainerListBounds = drawableContainerListBounds == NativeRect.Empty + ? element.DrawingBounds + : drawableContainerListBounds.Union(element.DrawingBounds); + } + + // And find a location inside the target surface to paste to + bool containersCanFit = drawableContainerListBounds.Width < Bounds.Width && drawableContainerListBounds.Height < Bounds.Height; + if (!containersCanFit) + { + NativePoint containersLocation = drawableContainerListBounds.Location; + containersLocation.Offset(moveOffset); + if (!Bounds.Contains(containersLocation)) + { + // Easy fix for same surface + moveOffset = isSameSurface + ? new NativePoint(-10, -10) + : new NativePoint(-drawableContainerListBounds.Location.X + 10, -drawableContainerListBounds.Location.Y + 10); + } + } + else + { + NativeRect moveContainerListBounds = drawableContainerListBounds.Offset(moveOffset); + // check if the element is inside + if (!Bounds.Contains(moveContainerListBounds)) + { + // Easy fix for same surface + if (isSameSurface) + { + moveOffset = new Point(-10, -10); + } + else + { + // For different surface, which is most likely smaller + int offsetX = 0; + int offsetY = 0; + if (drawableContainerListBounds.Right > Bounds.Right) + { + offsetX = Bounds.Right - drawableContainerListBounds.Right; + // Correction for the correction + if (drawableContainerListBounds.Left + offsetX < 0) + { + offsetX += Math.Abs(drawableContainerListBounds.Left + offsetX); + } + } + + if (drawableContainerListBounds.Bottom > Bounds.Bottom) + { + offsetY = Bounds.Bottom - drawableContainerListBounds.Bottom; + // Correction for the correction + if (drawableContainerListBounds.Top + offsetY < 0) + { + offsetY += Math.Abs(drawableContainerListBounds.Top + offsetY); + } + } + + moveOffset = new Point(offsetX, offsetY); + } + } + } + + dcs.MoveBy(moveOffset.X, moveOffset.Y); + AddElements(dcs); + FieldAggregator.BindElements(dcs); + DeselectAllElements(); + SelectElements(dcs); + } + } + else if (ClipboardHelper.ContainsImage(clipboard)) + { + NativePoint pasteLocation = GetPasteLocation(0.1f, 0.1f); + + foreach (var drawableContainer in ClipboardHelper.GetDrawables(clipboard)) + { + if (drawableContainer == null) continue; + DeselectAllElements(); + drawableContainer.Left = pasteLocation.X; + drawableContainer.Top = pasteLocation.Y; + AddElement(drawableContainer); + SelectElement(drawableContainer); + pasteLocation = pasteLocation.Offset(10, 10); + } + } + else if (ClipboardHelper.ContainsText(clipboard)) + { + NativePoint pasteLocation = GetPasteLocation(0.4f, 0.4f); + + string text = ClipboardHelper.GetText(clipboard); + if (text != null) + { + DeselectAllElements(); + ITextContainer textContainer = AddTextContainer(text, pasteLocation.X, pasteLocation.Y, + FontFamily.GenericSansSerif, 12f, false, false, false, 2, Color.Black, Color.Transparent); + SelectElement(textContainer); + } + } + } + + /// + /// Find a location to paste elements. + /// If mouse is over the surface - use it's position, otherwise use the visible area. + /// Return a point in image coordinate space. + /// + /// 0.0f for the left edge of visible area, 1.0f for the right edge of visible area. + /// 0.0f for the top edge of visible area, 1.0f for the bottom edge of visible area. + private NativePoint GetPasteLocation(float horizontalRatio = 0.5f, float verticalRatio = 0.5f) + { + var point = PointToClient(MousePosition); + var rc = GetVisibleRectangle(); + if (!rc.Contains(point)) + { + point = new NativePoint( + rc.Left + (int) (rc.Width * horizontalRatio), + rc.Top + (int) (rc.Height * verticalRatio) + ); + } + + return ToImageCoordinates(point); + } + + /// + /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). + /// + public NativeRect GetVisibleRectangle() + { + var bounds = Bounds; + var clientArea = Parent.ClientRectangle; + return new NativeRect( + Math.Max(0, -bounds.Left), + Math.Max(0, -bounds.Top), + clientArea.Width, + clientArea.Height + ); + } + + /// + /// Get the rectangle bounding all selected elements (in surface coordinates space), + /// or empty rectangle if nothing is selected. + /// + public NativeRect GetSelectionRectangle() + => ToSurfaceCoordinates(selectedElements.DrawingBounds); + + /// + /// Duplicate all the selected elements + /// + public void DuplicateSelectedElements() + { + LOG.DebugFormat("Duplicating {0} selected elements", selectedElements.Count); + IDrawableContainerList dcs = selectedElements.Clone(); + dcs.Parent = this; + dcs.MoveBy(10, 10); + AddElements(dcs); + DeselectAllElements(); + SelectElements(dcs); + } + + /// + /// Deselect the specified element + /// + /// IDrawableContainerList + /// false to skip event generation + public void DeselectElement(IDrawableContainer container, bool generateEvents = true) + { + container.Selected = false; + selectedElements.Remove(container); + FieldAggregator.UnbindElement(container); + if (generateEvents && _movingElementChanged != null) + { + var eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + } + + /// + /// Deselect the specified elements + /// + /// IDrawableContainerList + public void DeselectElements(IDrawableContainerList elements) + { + if (elements.Count == 0) + { + return; + } + + while (elements.Count > 0) + { + var element = elements[0]; + DeselectElement(element, false); + } + + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + Invalidate(); + } + + /// + /// Deselect all the selected elements + /// + public void DeselectAllElements() + { + DeselectElements(selectedElements); + } + + /// + /// Select the supplied element + /// + /// + /// false to skip invalidation + /// false to skip event generation + public void SelectElement(IDrawableContainer container, bool invalidate = true, bool generateEvents = true) + { + if (selectedElements.Contains(container)) return; + + selectedElements.Add(container); + container.Selected = true; + FieldAggregator.BindElement(container); + if (generateEvents && _movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + if (invalidate) + { + container.Invalidate(); + } + } + + /// + /// Select all elements, this is called when Ctrl+A is pressed + /// + public void SelectAllElements() + { + SelectElements(_elements); + } + + /// + /// Select the supplied elements + /// + /// + public void SelectElements(IDrawableContainerList elements) + { + SuspendLayout(); + foreach (var drawableContainer in elements) + { + var element = (DrawableContainer) drawableContainer; + SelectElement(element, false, false); + } + + if (_movingElementChanged != null) + { + SurfaceElementEventArgs eventArgs = new SurfaceElementEventArgs + { + Elements = selectedElements + }; + _movingElementChanged(this, eventArgs); + } + + ResumeLayout(); + Invalidate(); + } + + /// + /// Process key presses on the surface, this is called from the editor (and NOT an override from the Control) + /// + /// Keys + /// false if no keys were processed + public bool ProcessCmdKey(Keys k) + { + if (selectedElements.Count <= 0 && k != Keys.Escape) return false; + + bool shiftModifier = (ModifierKeys & Keys.Shift) == Keys.Shift; + int px = shiftModifier ? 10 : 1; + NativePoint moveBy = NativePoint.Empty; + switch (k) + { + case Keys.Left: + case Keys.Left | Keys.Shift: + moveBy = new NativePoint(-px, 0); + break; + case Keys.Up: + case Keys.Up | Keys.Shift: + moveBy = new NativePoint(0, -px); + break; + case Keys.Right: + case Keys.Right | Keys.Shift: + moveBy = new NativePoint(px, 0); + break; + case Keys.Down: + case Keys.Down | Keys.Shift: + moveBy = new NativePoint(0, px); + break; + case Keys.PageUp: + PullElementsUp(); + break; + case Keys.PageDown: + PushElementsDown(); + break; + case Keys.Home: + PullElementsToTop(); + break; + case Keys.End: + PushElementsToBottom(); + break; + case Keys.Enter: + Confirm(true); + break; + case Keys.Escape: + Confirm(false); + break; + case Keys.D0 | Keys.Control: + case Keys.D0 | Keys.Shift | Keys.Control: + SetSelectedElementColor(shiftModifier ? Color.Orange : Color.Transparent, false, shiftModifier); + break; + case Keys.D1 | Keys.Control: + case Keys.D1 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Red, false, shiftModifier); + break; + case Keys.D2 | Keys.Control: + case Keys.D2 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Green, false, shiftModifier); + break; + case Keys.D3 | Keys.Control: + case Keys.D3 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Blue, false, shiftModifier); + break; + case Keys.D4 | Keys.Control: + case Keys.D4 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Cyan, false, shiftModifier); + break; + case Keys.D5 | Keys.Control: + case Keys.D5 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Magenta, false, shiftModifier); + break; + case Keys.D6 | Keys.Control: + case Keys.D6 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Yellow, false, shiftModifier); + break; + case Keys.D7 | Keys.Control: + case Keys.D7 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Black, false, shiftModifier); + break; + case Keys.D8 | Keys.Control: + case Keys.D8 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.Gray, false, shiftModifier); + break; + case Keys.D9 | Keys.Control: + case Keys.D9 | Keys.Shift | Keys.Control: + SetSelectedElementColor(Color.White, false, shiftModifier); + break; + case Keys.Add | Keys.Control: + case Keys.Add | Keys.Shift | Keys.Control: + ChangeLineThickness(shiftModifier ? 5 : 1); + break; + case Keys.Subtract | Keys.Control: + case Keys.Subtract | Keys.Shift | Keys.Control: + ChangeLineThickness(shiftModifier ? -5 : -1); + break; + case Keys.Divide | Keys.Control: + FlipShadow(); + break; + /*case Keys.Delete: + RemoveSelectedElements(); + break;*/ + default: + return false; + } + + if (moveBy != NativePoint.Empty) + { + selectedElements.MakeBoundsChangeUndoable(true); + selectedElements.MoveBy(moveBy.X, moveBy.Y); + } + + return true; + + } + + // for laptops without numPads, also allow shift modifier + private void SetSelectedElementColor(Color color, bool numPad, bool shift) + { + if (numPad || shift) + { + selectedElements.SetForegroundColor(color); + UpdateForegroundColorEvent(this, color); + } + else + { + selectedElements.SetBackgroundColor(color); + UpdateBackgroundColorEvent(this, color); + } + selectedElements.Invalidate(); + } + + private void ChangeLineThickness(int increaseBy) + { + var newThickness = selectedElements.IncreaseLineThickness(increaseBy); + UpdateLineThicknessEvent(this, newThickness); + selectedElements.Invalidate(); + } + + private void FlipShadow() + { + var shadow = selectedElements.FlipShadow(); + UpdateShadowEvent(this, shadow); + selectedElements.Invalidate(); + } + + /// + /// Property for accessing the elements on the surface + /// + public IDrawableContainerList Elements => _elements; + + /// + /// pulls selected elements up one level in hierarchy + /// + public void PullElementsUp() + { + _elements.PullElementsUp(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements up to top in hierarchy + /// + public void PullElementsToTop() + { + _elements.PullElementsToTop(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements down one level in hierarchy + /// + public void PushElementsDown() + { + _elements.PushElementsDown(selectedElements); + _elements.Invalidate(); + } + + /// + /// pushes selected elements down to bottom in hierarchy + /// + public void PushElementsToBottom() + { + _elements.PushElementsToBottom(selectedElements); + _elements.Invalidate(); + } + + /// + /// indicates whether the selected elements could be pulled up in hierarchy + /// + /// true if selected elements could be pulled up, false otherwise + public bool CanPullSelectionUp() + { + return _elements.CanPullUp(selectedElements); + } + + /// + /// indicates whether the selected elements could be pushed down in hierarchy + /// + /// true if selected elements could be pushed down, false otherwise + public bool CanPushSelectionDown() + { + return _elements.CanPushDown(selectedElements); + } + + private void Element_FieldChanged(object sender, FieldChangedEventArgs e) + { + selectedElements.HandleFieldChangedEvent(sender, e); + } + + public bool IsOnSurface(IDrawableContainer container) + { + return _elements.Contains(container); + } + + public NativePoint ToSurfaceCoordinates(NativePoint point) + { + Point[] points = + { + point + }; + _zoomMatrix.TransformPoints(points); + return points[0]; + } + + public NativeRect ToSurfaceCoordinates(NativeRect rc) + { + if (_zoomMatrix.IsIdentity) + { + return rc; + } + + Point[] points = + { + rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) + }; + _zoomMatrix.TransformPoints(points); + return new NativeRect( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); + } + + public NativePoint ToImageCoordinates(NativePoint point) + { + Point[] points = + { + point + }; + _inverseZoomMatrix.TransformPoints(points); + return points[0]; + } + + public NativeRect ToImageCoordinates(NativeRect rc) + { + if (_inverseZoomMatrix.IsIdentity) + { + return rc; + } + + Point[] points = + { + rc.Location, rc.Location.Offset(rc.Size.Width, rc.Size.Height) + }; + _inverseZoomMatrix.TransformPoints(points); + return new NativeRect( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); + } + } } \ No newline at end of file