using PaintDotNet.Measurement.HistoryFunctions; using PaintDotNet.Measurement.HistoryMementos; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace PaintDotNet.Measurement.Tools { public class SelectionTool : Tool { private bool tracking = false; private bool moveOriginMode = false; private Point lastXY; private SelectionHistoryMemento undoAction; private CombineMode combineMode; private List tracePoints = null; private DateTime startTime; private bool hasMoved = false; private bool append = false; private bool wasNotEmpty = false; private Selection newSelection; private SelectionRenderer newSelectionRenderer; private Cursor cursorMouseUp; private Cursor cursorMouseUpMinus; private Cursor cursorMouseUpPlus; private Cursor cursorMouseDown; protected CombineMode SelectionMode { get { return this.combineMode; } } protected void SetCursors( string cursorMouseUpResName, string cursorMouseUpMinusResName, string cursorMouseUpPlusResName, string cursorMouseDownResName) { if (this.cursorMouseUp != null) { this.cursorMouseUp.Dispose(); this.cursorMouseUp = null; } if (cursorMouseUpResName != null) { this.cursorMouseUp = new Cursor(PdnResources.GetResourceStream(cursorMouseUpResName)); } if (this.cursorMouseUpMinus != null) { this.cursorMouseUpMinus.Dispose(); this.cursorMouseUpMinus = null; } if (cursorMouseUpMinusResName != null) { this.cursorMouseUpMinus = new Cursor(PdnResources.GetResourceStream(cursorMouseUpMinusResName)); } if (this.cursorMouseUpPlus != null) { this.cursorMouseUpPlus.Dispose(); this.cursorMouseUpPlus = null; } if (cursorMouseUpPlusResName != null) { this.cursorMouseUpPlus = new Cursor(PdnResources.GetResourceStream(cursorMouseUpPlusResName)); } if (this.cursorMouseDown != null) { this.cursorMouseDown.Dispose(); this.cursorMouseDown = null; } if (cursorMouseDownResName != null) { this.cursorMouseDown = new Cursor(PdnResources.GetResourceStream(cursorMouseDownResName)); } } private Cursor GetCursor(bool mouseDown, bool ctrlDown, bool altDown) { Cursor cursor; if (mouseDown) { cursor = this.cursorMouseDown; } else if (ctrlDown) { cursor = this.cursorMouseUpPlus; } else if (altDown) { cursor = this.cursorMouseUpMinus; } else { cursor = this.cursorMouseUp; } return cursor; } private Cursor GetCursor() { return GetCursor(IsMouseDown, (ModifierKeys & Keys.Control) != 0, (ModifierKeys & Keys.Alt) != 0); } protected override void OnActivate() { // Assume that SetCursors() has been called by now this.Cursor = GetCursor(); DocumentWorkspace.SetEnableSelectionTinting(true); this.newSelection = new Selection(); this.newSelectionRenderer = new SelectionRenderer(this.RendererList, this.newSelection, this.DocumentWorkspace.GetUserControl()); this.newSelectionRenderer.EnableSelectionTinting = false; this.newSelectionRenderer.EnableOutlineAnimation = false; this.newSelectionRenderer.Visible = false; this.RendererList.Add(this.newSelectionRenderer, true); base.OnActivate(); } protected override void OnDeactivate() { DocumentWorkspace.SetEnableSelectionTinting(false); if (this.tracking) { Done(); } base.OnDeactivate(); SetCursors(null, null, null, null); // dispose 'em this.RendererList.Remove(this.newSelectionRenderer); this.newSelectionRenderer.Dispose(); this.newSelectionRenderer = null; this.newSelection = null; } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); this.Cursor = GetCursor(); if (tracking) { moveOriginMode = true; lastXY = new Point(e.X, e.Y); OnMouseMove(e); } else if ((e.Button & MouseButtons.Left) == MouseButtons.Left || (e.Button & MouseButtons.Right) == MouseButtons.Right) { tracking = true; hasMoved = false; startTime = DateTime.Now; tracePoints = new List(); tracePoints.Add(new Point(e.X, e.Y)); undoAction = new SelectionHistoryMemento("sentinel", this.Image, DocumentWorkspace); wasNotEmpty = !Selection.IsEmpty; // Determine this.combineMode if ((ModifierKeys & Keys.Control) != 0 && e.Button == MouseButtons.Left) { this.combineMode = CombineMode.Union; } else if ((ModifierKeys & Keys.Alt) != 0 && e.Button == MouseButtons.Left) { this.combineMode = CombineMode.Exclude; } else if ((ModifierKeys & Keys.Control) != 0 && e.Button == MouseButtons.Right) { this.combineMode = CombineMode.Xor; } else if ((ModifierKeys & Keys.Alt) != 0 && e.Button == MouseButtons.Right) { this.combineMode = CombineMode.Intersect; } else { this.combineMode = AppEnvironment.SelectionCombineMode(); } DocumentWorkspace.SetEnableSelectionOutline(false); this.newSelection.Reset(); PdnGraphicsPath basePath = Selection.CreatePath(); this.newSelection.SetContinuation(basePath, CombineMode.Replace, true); this.newSelection.CommitContinuation(); bool newSelectionRendererVisible = true; // Act on this.combineMode switch (this.combineMode) { case CombineMode.Xor: append = true; Selection.ResetContinuation(); break; case CombineMode.Union: append = true; Selection.ResetContinuation(); break; case CombineMode.Exclude: append = true; Selection.ResetContinuation(); break; case CombineMode.Replace: append = false; Selection.Reset(); break; case CombineMode.Intersect: append = true; Selection.ResetContinuation(); break; default: throw new InvalidEnumArgumentException(); } this.newSelectionRenderer.Visible = newSelectionRendererVisible; } } protected virtual List TrimShapePath(List trimTheseTracePoints) { return trimTheseTracePoints; } protected virtual List CreateShape(List inputTracePoints) { List points = Utility.PointListToPointFList(inputTracePoints); return points; } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (moveOriginMode) { Size delta = new Size(e.X - lastXY.X, e.Y - lastXY.Y); for (int i = 0; i < tracePoints.Count; ++i) { Point pt = (Point)tracePoints[i]; pt.X += delta.Width; pt.Y += delta.Height; tracePoints[i] = pt; } lastXY = new Point(e.X, e.Y); Render(); } else if (tracking) { Point mouseXY = new Point(e.X, e.Y); if (mouseXY != (Point)tracePoints[tracePoints.Count - 1]) { tracePoints.Add(mouseXY); } hasMoved = true; Render(); } } private PointF[] CreateSelectionPolygon() { List trimmedTrace = this.TrimShapePath(tracePoints); List shapePoints = CreateShape(trimmedTrace); List polygon; switch (this.combineMode) { case CombineMode.Xor: case CombineMode.Exclude: polygon = shapePoints; break; default: case CombineMode.Complement: case CombineMode.Intersect: case CombineMode.Replace: case CombineMode.Union: polygon = Utility.SutherlandHodgman(DocumentWorkspace.GetDocument().Bounds, shapePoints); break; } return polygon.ToArray(); } private void Render() { if (tracePoints != null && tracePoints.Count > 2) { PointF[] polygon = CreateSelectionPolygon(); if (polygon.Length > 2) { DocumentWorkspace.ResetOutlineWhiteOpacity(); this.newSelectionRenderer.ResetOutlineWhiteOpacity(); Selection.SetContinuation(polygon, this.combineMode); CombineMode cm; if (SelectionMode == CombineMode.Replace) { cm = CombineMode.Replace; } else { cm = CombineMode.Xor; } this.newSelection.SetContinuation(polygon, cm); Update(); } } } protected override void OnPulse() { if (this.tracking) { DocumentWorkspace.ResetOutlineWhiteOpacity(); this.newSelectionRenderer.ResetOutlineWhiteOpacity(); } base.OnPulse(); } private enum WhatToDo { Clear, Emit, Reset, } private void Done() { if (tracking) { // Truth table for what we should do based on three flags: // append | moved | tooQuick | result | optimized expression to yield true // ---------+-------+----------+----------------------------------------------------------------------- // F | T | T | clear selection | !append && (!moved || tooQuick) // F | T | F | emit new selected area | !append && moved && !tooQuick // F | F | T | clear selection | !append && (!moved || tooQuick) // F | F | F | clear selection | !append && (!moved || tooQuick) // T | T | T | append to selection | append && moved // T | T | F | append to selection | append && moved // T | F | T | reset selection | append && !moved // T | F | F | reset selection | append && !moved // // append --> If the user was holding control, then true. Else false. // moved --> If they never moved the mouse, false. Else true. // tooQuick --> If they held the mouse button down for more than 50ms, false. Else true. // // "Clear selection" means to result in no selected area. If the selection area was previously empty, // then no HistoryMemento is emitted. Otherwise a Deselect HistoryMemento is emitted. // // "Reset selection" means to reset the selected area to how it was before interaction with the tool, // without a HistoryMemento. PointF[] polygon = CreateSelectionPolygon(); this.hasMoved &= (polygon.Length > 1); // They were "too quick" if they weren't doing a selection for more than 50ms // This takes care of the case where someone wants to click to deselect, but accidentally moves // the mouse. This happens VERY frequently. bool tooQuick = Utility.TicksToMs((DateTime.Now - startTime).Ticks) <= 50; // If their selection was completedly out of bounds, it will be clipped bool clipped = (polygon.Length == 0); // What the user drew had no effect on the slection, e.g. subtraction where there was nothing in the first place bool noEffect = false; WhatToDo whatToDo; // If their selection gets completely clipped (i.e. outside the image canvas), // then result in a no-op if (append) { if (!hasMoved || clipped || noEffect) { whatToDo = WhatToDo.Reset; } else { whatToDo = WhatToDo.Emit; } } else { if (hasMoved && !tooQuick && !clipped && !noEffect) { whatToDo = WhatToDo.Emit; } else { whatToDo = WhatToDo.Clear; } } switch (whatToDo) { case WhatToDo.Clear: if (wasNotEmpty) { // emit a deselect history action undoAction.Name = DeselectFunction.StaticName; undoAction.Image = DeselectFunction.StaticImage; //HistoryStack.PushNewMemento(undoAction); } Selection.Reset(); break; case WhatToDo.Emit: // emit newly selected area undoAction.Name = this.Name; //HistoryStack.PushNewMemento(undoAction); Selection.CommitContinuation(); break; case WhatToDo.Reset: // reset selection, no HistoryMemento Selection.ResetContinuation(); break; } DocumentWorkspace.ResetOutlineWhiteOpacity(); this.newSelectionRenderer.ResetOutlineWhiteOpacity(); this.newSelection.Reset(); this.newSelectionRenderer.Visible = false; this.tracking = false; DocumentWorkspace.SetEnableSelectionOutline(true); DocumentWorkspace.InvalidateSurface(Utility.RoundRectangle(DocumentWorkspace.GetVisibleDocumentRectangleF())); } } protected override void OnMouseUp(MouseEventArgs e) { OnMouseMove(e); if (moveOriginMode) { moveOriginMode = false; } else { Done(); } base.OnMouseUp(e); Cursor = GetCursor(); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (tracking) { Render(); } Cursor = GetCursor(); } protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); if (tracking) { Render(); } Cursor = GetCursor(); } protected override void OnClick() { base.OnClick(); if (!moveOriginMode) { Done(); } } public SelectionTool( IDocumentWorkspace documentWorkspace, ImageResource toolBarImage, string name, string helpText, char hotKey, ToolBarConfigItems toolBarConfigItems) : base(documentWorkspace, toolBarImage, name, helpText, hotKey, false, toolBarConfigItems | ToolBarConfigItems.SelectionCombineMode) { this.tracking = false; } } }