using PaintDotNet.Measurement.HistoryFunctions; using PaintDotNet.SystemLayer; using System; using System.Collections; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; namespace PaintDotNet.Measurement { /// /// Encapsulates the functionality for a tool that goes in the main window's toolbar /// and that affects the Document. /// A Tool should only emit a HistoryMemento when it actually modifies the canvas. /// So, for instance, if the user draws a line but that line doesn't fall within /// the canvas (like if the seleciton region excludes it), then since the user /// hasn't really done anything there should be no HistoryMemento emitted. /// /// /// A bit about the eventing model: /// * Perform[Event]() methods are ALWAYS used to trigger the events. This can be called by /// either instance methods or outside (client) callers. /// * [Event]() methods are called first by Perform[Event](). This gives the base Tool class a /// first chance at handling the event. These methods are private and non-overridable. /// * On[Event]() methods are then called by [Event]() if necessary, and should be overrided /// as necessary by derived classes. Always call the base implementation unless the /// documentation says otherwise. The base implementation gives the Tool a chance to provide /// default, overridable behavior for an event. /// public class Tool : IDisposable, IHotKeyTarget { public static readonly Type DefaultToolType = typeof(Tools.PaintBrushTool); private ImageResource toolBarImage; private Cursor cursor; private ToolInfo toolInfo; private int mouseDown = 0; // incremented for every MouseDown, decremented for every MouseUp private int ignoreMouseMove = 0; // when >0, MouseMove is ignored and then this is decremented protected Cursor handCursor; protected Cursor handCursorMouseDown; protected Cursor handCursorInvalid; private Cursor panOldCursor; private Point lastMouseXY; private Point lastPanMouseXY; private bool panMode = false; // 'true' when the user is holding down the spacebar private bool panTracking = false; // 'true' when panMode is true, and when the mouse is down (which is when MouseMove should do panning) private MoveNubRenderer trackingNub = null; // when we are in pan-tracking mode, we draw this in the center of the screen private IDocumentWorkspace documentWorkspace; private bool active = false; protected bool autoScroll = true; private Hashtable keysThatAreDown = new Hashtable(); private MouseButtons lastButton = MouseButtons.None; private Surface scratchSurface; private PdnRegion saveRegion; private int mouseEnter; // increments on MouseEnter, decrements on MouseLeave. The MouseLeave event is ONLY raised when this value decrements to 0, and MouseEnter is ONLY raised when this value increments to 1 protected Surface ScratchSurface { get { return scratchSurface; } } protected Document Document { get { return DocumentWorkspace.GetDocument(); } } public IDocumentWorkspace DocumentWorkspace { get { return this.documentWorkspace; } } public IAppWorkspace AppWorkspace { get { return DocumentWorkspace.GetAppWorkspace(); } } protected IAppEnvironment AppEnvironment { get { return AppWorkspace.GetAppEnvironment(); } } protected Selection Selection { get { return DocumentWorkspace.GetSelection(); } } protected Layer ActiveLayer { get { return DocumentWorkspace.GetActiveLayer(); } } protected int ActiveLayerIndex { get { return DocumentWorkspace.GetActiveLayerIndex(); } set { DocumentWorkspace.SetActiveLayerIndex(value); } } public void ClearSavedMemory() { this.savedTiles = null; } public void ClearSavedRegion() { if (this.saveRegion != null) { this.saveRegion.Dispose(); this.saveRegion = null; } } public void RestoreRegion(PdnRegion region) { if (region != null) { BitmapLayer activeLayer = (BitmapLayer)ActiveLayer; activeLayer.Surface.CopySurface(this.ScratchSurface, region); activeLayer.Invalidate(region); } } public void RestoreSavedRegion() { if (this.saveRegion != null) { BitmapLayer activeLayer = (BitmapLayer)ActiveLayer; activeLayer.Surface.CopySurface(this.ScratchSurface, this.saveRegion); activeLayer.Invalidate(this.saveRegion); this.saveRegion.Dispose(); this.saveRegion = null; } } private const int saveTileGranularity = 32; private BitVector2D savedTiles; public void SaveRegion(PdnRegion saveMeRegion, Rectangle saveMeBounds) { BitmapLayer activeLayer = (BitmapLayer)ActiveLayer; if (savedTiles == null) { savedTiles = new BitVector2D( (activeLayer.Width + saveTileGranularity - 1) / saveTileGranularity, (activeLayer.Height + saveTileGranularity - 1) / saveTileGranularity); savedTiles.Clear(false); } Rectangle regionBounds; if (saveMeRegion == null) { regionBounds = saveMeBounds; } else { regionBounds = saveMeRegion.GetBoundsInt(); } Rectangle bounds = Rectangle.Union(regionBounds, saveMeBounds); bounds.Intersect(activeLayer.Bounds); int leftTile = bounds.Left / saveTileGranularity; int topTile = bounds.Top / saveTileGranularity; int rightTile = (bounds.Right - 1) / saveTileGranularity; int bottomTile = (bounds.Bottom - 1) / saveTileGranularity; for (int tileY = topTile; tileY <= bottomTile; ++tileY) { Rectangle rowAccumBounds = Rectangle.Empty; for (int tileX = leftTile; tileX <= rightTile; ++tileX) { if (!savedTiles.Get(tileX, tileY)) { Rectangle tileBounds = new Rectangle(tileX * saveTileGranularity, tileY * saveTileGranularity, saveTileGranularity, saveTileGranularity); tileBounds.Intersect(activeLayer.Bounds); if (rowAccumBounds == Rectangle.Empty) { rowAccumBounds = tileBounds; } else { rowAccumBounds = Rectangle.Union(rowAccumBounds, tileBounds); } savedTiles.Set(tileX, tileY, true); } else { if (rowAccumBounds != Rectangle.Empty) { using (Surface dst = ScratchSurface.CreateWindow(rowAccumBounds), src = activeLayer.Surface.CreateWindow(rowAccumBounds)) { dst.CopySurface(src); } rowAccumBounds = Rectangle.Empty; } } } if (rowAccumBounds != Rectangle.Empty) { using (Surface dst = ScratchSurface.CreateWindow(rowAccumBounds), src = activeLayer.Surface.CreateWindow(rowAccumBounds)) { dst.CopySurface(src); } rowAccumBounds = Rectangle.Empty; } } if (this.saveRegion != null) { this.saveRegion.Dispose(); this.saveRegion = null; } if (saveMeRegion != null) { this.saveRegion = saveMeRegion.Clone(); } } private sealed class KeyTimeInfo { public DateTime KeyDownTime; public DateTime LastKeyPressPulse; private int repeats = 0; public int Repeats { get { return repeats; } set { repeats = value; } } public KeyTimeInfo() { KeyDownTime = DateTime.Now; LastKeyPressPulse = KeyDownTime; } } /// /// Tells you whether the tool is "active" or not. If the tool is not active /// it is not safe to call any other method besides PerformActivate. All /// properties are safe to get values from. /// public bool Active { get { return this.active; } } /// /// Returns true if the Tool has the input focus, or false if it does not. /// /// /// This is used, for instanced, by the Text Tool so that it doesn't blink the /// cursor unless it's actually going to do something in response to your /// keyboard input! /// public bool Focused { get { return DocumentWorkspace.Focused(); } } public bool IsMouseDown { get { return this.mouseDown > 0; } } /// /// Gets a flag that determines whether the Tool is deactivated while the current /// layer is changing, and then reactivated afterwards. /// /// /// This property is queried every time the ActiveLayer property of DocumentWorkspace /// is changed. If false is returned, then the tool is not deactivated during the /// layer change and must manually maintain coherency. /// public virtual bool DeactivateOnLayerChange { get { return true; } } /// /// Tells you which keys are pressed /// public Keys ModifierKeys { get { return Control.ModifierKeys; } } /// /// Represents the Image that is displayed in the toolbar. /// public ImageResource Image { get { return this.toolBarImage; } } public event EventHandler CursorChanging; protected virtual void OnCursorChanging() { if (CursorChanging != null) { CursorChanging(this, EventArgs.Empty); } } public event EventHandler CursorChanged; protected virtual void OnCursorChanged() { if (CursorChanged != null) { CursorChanged(this, EventArgs.Empty); } } /// /// The Cursor that is displayed when this Tool is active and the /// mouse cursor is inside the document view. /// public Cursor Cursor { get { return this.cursor; } set { OnCursorChanging(); this.cursor = value; OnCursorChanged(); } } /// /// The name of the Tool. For instance, "Pencil". This name should *not* end in "Tool", e.g. "Pencil Tool" /// public string Name { get { return this.toolInfo.Name; } } /// /// A short description of how to use the tool. /// public string HelpText { get { return this.toolInfo.HelpText; } } public ToolInfo Info { get { return this.toolInfo; } } public ToolBarConfigItems ToolBarConfigItems { get { return this.toolInfo.ToolBarConfigItems; } } /// /// Specifies whether or not an inherited tool should take Ink commands /// protected virtual bool SupportsInk { get { return false; } } public char HotKey { get { return this.toolInfo.HotKey; } } // Methods to send messages to this class public void PerformActivate() { Activate(); } public void PerformDeactivate() { Deactivate(); } private bool IsOverflow(MouseEventArgs e) { PointF clientPt = DocumentWorkspace.DocumentToClient(new PointF(e.X, e.Y)); return clientPt.X < -16384 || clientPt.Y < -16384; } public bool IsMouseEntered { get { return this.mouseEnter > 0; } } public void PerformMouseEnter() { MouseEnter(); } private void MouseEnter() { ++this.mouseEnter; if (this.mouseEnter == 1) { OnMouseEnter(); } } protected virtual void OnMouseEnter() { } public void PerformMouseLeave() { MouseLeave(); } private void MouseLeave() { if (this.mouseEnter == 1) { this.mouseEnter = 0; OnMouseLeave(); } else { this.mouseEnter = Math.Max(0, this.mouseEnter - 1); } } protected virtual void OnMouseLeave() { } public void PerformMouseMove(MouseEventArgs e) { if (IsOverflow(e)) { return; } if (e is StylusEventArgs) { if (this.SupportsInk) { StylusMove(e as StylusEventArgs); } // if the tool does not claim ink support, discard } else { MouseMove(e); } } public void PerformMouseDown(MouseEventArgs e) { if (IsOverflow(e)) { return; } if (e is StylusEventArgs) { if (this.SupportsInk) { StylusDown(e as StylusEventArgs); } // if the tool does not claim ink support, discard } else { if (this.SupportsInk) { DocumentWorkspace.Focus(); } MouseDown(e); } } public void PerformMouseUp(MouseEventArgs e) { if (IsOverflow(e)) { return; } if (e is StylusEventArgs) { if (this.SupportsInk) { StylusUp(e as StylusEventArgs); } // if the tool does not claim ink support, discard } else { MouseUp(e); } } public void PerformKeyPress(KeyPressEventArgs e) { KeyPress(e); } public void PerformKeyPress(Keys key) { KeyPress(key); } public void PerformKeyUp(KeyEventArgs e) { KeyUp(e); } public void PerformKeyDown(KeyEventArgs e) { KeyDown(e); } public void PerformClick() { Click(); } public void PerformPulse() { Pulse(); } public void PerformPaste(IDataObject data, out bool handled) { Paste(data, out handled); } public void PerformPasteQuery(IDataObject data, out bool canHandle) { PasteQuery(data, out canHandle); } private void Activate() { Debug.Assert(this.active != true, "already active!"); this.active = true; this.handCursor = new Cursor(PdnResources.GetResourceStream("Cursors.PanToolCursor.cur")); this.handCursorMouseDown = new Cursor(PdnResources.GetResourceStream("Cursors.PanToolCursorMouseDown.cur")); this.handCursorInvalid = new Cursor(PdnResources.GetResourceStream("Cursors.PanToolCursorInvalid.cur")); this.panTracking = false; this.panMode = false; this.mouseDown = 0; this.savedTiles = null; this.saveRegion = null; this.scratchSurface = DocumentWorkspace.BorrowScratchSurface(this.GetType().Name + ": Tool.Activate()"); Selection.Changing += new EventHandler(SelectionChangingHandler); Selection.Changed += new EventHandler(SelectionChangedHandler); this.trackingNub = new MoveNubRenderer(this.RendererList); this.trackingNub.Visible = false; this.trackingNub.Size = new SizeF(10, 10); this.trackingNub.Shape = MoveNubShape.Compass; this.RendererList.Add(this.trackingNub, false); OnActivate(); } void FinishedHistoryStepGroup(object sender, EventArgs e) { OnFinishedHistoryStepGroup(); } protected virtual void OnFinishedHistoryStepGroup() { } /// /// This method is called when the tool is being activated; that is, when the /// user has chosen to use this tool by clicking on it on a toolbar. /// protected virtual void OnActivate() { } private void Deactivate() { Debug.Assert(this.active != false, "not active!"); this.active = false; Selection.Changing -= new EventHandler(SelectionChangingHandler); Selection.Changed -= new EventHandler(SelectionChangedHandler); OnDeactivate(); this.RendererList.Remove(this.trackingNub); this.trackingNub.Dispose(); this.trackingNub = null; DocumentWorkspace.ReturnScratchSurface(this.scratchSurface); this.scratchSurface = null; if (this.saveRegion != null) { this.saveRegion.Dispose(); this.saveRegion = null; } this.savedTiles = null; if (this.handCursor != null) { this.handCursor.Dispose(); this.handCursor = null; } if (this.handCursorMouseDown != null) { this.handCursorMouseDown.Dispose(); this.handCursorMouseDown = null; } if (this.handCursorInvalid != null) { this.handCursorInvalid.Dispose(); this.handCursorInvalid = null; } } /// /// This method is called when the tool is being deactivated; that is, when the /// user has chosen to use another tool by clicking on another tool on a /// toolbar. /// protected virtual void OnDeactivate() { } private void StylusDown(StylusEventArgs e) { if (!this.panMode) { OnStylusDown(e); } } protected virtual void OnStylusDown(StylusEventArgs e) { } private void StylusMove(StylusEventArgs e) { if (!this.panMode) { OnStylusMove(e); } } protected virtual void OnStylusMove(StylusEventArgs e) { if (this.mouseDown > 0) { ScrollIfNecessary(new PointF(e.X, e.Y)); } } private void StylusUp(StylusEventArgs e) { if (this.panTracking) { this.trackingNub.Visible = false; this.panTracking = false; this.Cursor = this.handCursor; } else { OnStylusUp(e); } } protected virtual void OnStylusUp(StylusEventArgs e) { } private void MouseMove(MouseEventArgs e) { if (this.ignoreMouseMove > 0) { --this.ignoreMouseMove; } else if (this.panTracking && e.Button == MouseButtons.Left) { // Pan the document, using Stylus coordinates. This is done in // MouseMove instead of StylusMove because StylusMove is // asynchronous, and would not 'feel' right (pan motions would // stack up) Point position = new Point(e.X, e.Y); RectangleF visibleRect = DocumentWorkspace.GetVisibleDocumentRectangleF(); PointF visibleCenterPt = Utility.GetRectangleCenter(visibleRect); PointF delta = new PointF(e.X - lastPanMouseXY.X, e.Y - lastPanMouseXY.Y); PointF newScroll = DocumentWorkspace.GetDocumentScrollPositionF(); if (delta.X != 0 || delta.Y != 0) { newScroll.X -= delta.X; newScroll.Y -= delta.Y; lastPanMouseXY = new Point(e.X, e.Y); lastPanMouseXY.X -= (int)Math.Truncate(delta.X); lastPanMouseXY.Y -= (int)Math.Truncate(delta.Y); ++this.ignoreMouseMove; // setting DocumentScrollPosition incurs a MouseMove event. ignore it prevents 'jittering' at non-integral zoom levels (like, say, 743%) DocumentWorkspace.SetDocumentScrollPositionF(newScroll); Update(); } } else if (!this.panMode) { OnMouseMove(e); } this.lastMouseXY = new Point(e.X, e.Y); this.lastButton = e.Button; } /// /// This method is called when the Tool is active and the mouse is moving within /// the document canvas area. /// /// Contains information about where the mouse cursor is, in document coordinates. protected virtual void OnMouseMove(MouseEventArgs e) { if (this.panMode || this.mouseDown > 0) { ScrollIfNecessary(new PointF(e.X, e.Y)); } } private void MouseDown(MouseEventArgs e) { ++this.mouseDown; if (this.panMode) { this.panTracking = true; this.lastPanMouseXY = new Point(e.X, e.Y); if (this.CanPan()) { this.Cursor = this.handCursorMouseDown; } } else { OnMouseDown(e); } this.lastMouseXY = new Point(e.X, e.Y); } /// /// This method is called when the Tool is active and a mouse button has been /// pressed within the document area. /// /// Contains information about where the mouse cursor is, in document coordinates, and which mouse buttons were pressed. protected virtual void OnMouseDown(MouseEventArgs e) { this.lastButton = e.Button; } private void MouseUp(MouseEventArgs e) { --this.mouseDown; if (!this.panMode) { OnMouseUp(e); } this.lastMouseXY = new Point(e.X, e.Y); } /// /// This method is called when the Tool is active and a mouse button has been /// released within the document area. /// /// Contains information about where the mouse cursor is, in document coordinates, and which mouse buttons were released. protected virtual void OnMouseUp(MouseEventArgs e) { this.lastButton = e.Button; } private void Click() { OnClick(); } /// /// This method is called when the Tool is active and a mouse button has been /// clicked within the document area. If you need more specific information, /// such as where the mouse was clicked and which button was used, respond to /// the MouseDown/MouseUp events. /// protected virtual void OnClick() { } private void KeyPress(KeyPressEventArgs e) { OnKeyPress(e); } private static DateTime lastToolSwitch = DateTime.MinValue; // if we are pressing 'S' to switch to the selection tools, then consecutive // presses of 'S' should switch to the next selection tol in the list. however, // if we wait awhile then pressing 'S' should go to the *first* selection // tool. 'awhile' is defined by this variable. private static readonly TimeSpan toolSwitchReset = new TimeSpan(0, 0, 0, 2, 0); private const char decPenSizeShortcut = '['; private const char decPenSizeBy5Shortcut = (char)27; // Ctrl [ but must also test that Ctrl is down private const char incPenSizeShortcut = ']'; private const char incPenSizeBy5Shortcut = (char)29; // Ctrl ] but must also test that Ctrl is down private const char swapColorsShortcut = 'x'; private const char swapPrimarySecondaryChoice = 'c'; private char[] wildShortcuts = new char[] { ',', '.', '/' }; // Return true if the key is handled, false if not. protected virtual bool OnWildShortcutKey(int ordinal) { return false; } /// /// This method is called when the tool is active and a keyboard key is pressed /// and released. If you respond to the keyboard key, set e.Handled to true. /// protected virtual void OnKeyPress(KeyPressEventArgs e) { if (!e.Handled && DocumentWorkspace.Focused()) { int wsIndex = Array.IndexOf(wildShortcuts, e.KeyChar); if (wsIndex != -1) { e.Handled = OnWildShortcutKey(wsIndex); } else if (e.KeyChar == swapColorsShortcut) { AppWorkspace.SwapUserColors();//Widgets.ColorsForm. e.Handled = true; } else if (e.KeyChar == swapPrimarySecondaryChoice) { AppWorkspace.ToggleWhichUserColor();//Widgets.ColorsForm. e.Handled = true; } else if (e.KeyChar == decPenSizeShortcut) { AppWorkspace.AddToPenSize(-1.0f);//Widgets.ToolConfigStrip. e.Handled = true; } else if (e.KeyChar == decPenSizeBy5Shortcut && (ModifierKeys & Keys.Control) != 0) { AppWorkspace.AddToPenSize(-5.0f); e.Handled = true; } else if (e.KeyChar == incPenSizeShortcut) { AppWorkspace.AddToPenSize(+1.0f); e.Handled = true; } else if (e.KeyChar == incPenSizeBy5Shortcut && (ModifierKeys & Keys.Control) != 0) { AppWorkspace.AddToPenSize(+5.0f); e.Handled = true; } else { ToolInfo[] toolInfos = DocumentWorkspace.GetToolInfos(); Type currentToolType = DocumentWorkspace.GetToolType(); int currentTool = 0; if (0 != (ModifierKeys & Keys.Shift)) { Array.Reverse(toolInfos); } if (char.ToLower(this.HotKey) != char.ToLower(e.KeyChar) || (DateTime.Now - lastToolSwitch) > toolSwitchReset) { // If it's been a short time since they pressed a tool switching hotkey, // we will start from the beginning of this list. This helps to enable two things: // 1) If multiple tools have the same hotkey, the user may press that hotkey // to cycle through them // 2) After a period of time, pressing the hotkey will revert to always // choosing the first tool in that list of tools which have the same hotkey. currentTool = -1; } else { for (int t = 0; t < toolInfos.Length; ++t) { if (toolInfos[t].ToolType == currentToolType) { currentTool = t; break; } } } for (int t = 0; t < toolInfos.Length; ++t) { int newTool = (t + currentTool + 1) % toolInfos.Length; ToolInfo localToolInfo = toolInfos[newTool]; if (localToolInfo.ToolType == DocumentWorkspace.GetToolType() && localToolInfo.SkipIfActiveOnHotKey) { continue; } if (char.ToLower(localToolInfo.HotKey) == char.ToLower(e.KeyChar)) { if (!this.IsMouseDown) { AppWorkspace.SelectTool(localToolInfo.ToolType);//Widgets.ToolsControl. } e.Handled = true; lastToolSwitch = DateTime.Now; break; } } // If the keypress is still not handled ... if (!e.Handled) { switch (e.KeyChar) { // By default, Esc/Enter clear the current selection if there is any case (char)13: // Enter case (char)27: // Escape if (this.mouseDown == 0 && !Selection.IsEmpty) { e.Handled = true; DocumentWorkspace.ExecuteFunction(new DeselectFunction()); } break; } } } } } private DateTime lastKeyboardMove = DateTime.MinValue; private Keys lastKey; private int keyboardMoveSpeed = 1; private int keyboardMoveRepeats = 0; private void KeyPress(Keys key) { OnKeyPress(key); } /// /// This method is called when the tool is active and a keyboard key is pressed /// and released that is not representable with a regular Unicode chararacter. /// An example would be the arrow keys. /// protected virtual void OnKeyPress(Keys key) { Point dir = Point.Empty; if (key != lastKey) { lastKeyboardMove = DateTime.MinValue; } lastKey = key; switch (key) { case Keys.Left: --dir.X; break; case Keys.Right: ++dir.X; break; case Keys.Up: --dir.Y; break; case Keys.Down: ++dir.Y; break; } if (!dir.Equals(Point.Empty)) { long span = DateTime.Now.Ticks - lastKeyboardMove.Ticks; if ((span * 4) > TimeSpan.TicksPerSecond) { keyboardMoveRepeats = 0; keyboardMoveSpeed = 1; } else { keyboardMoveRepeats++; if (keyboardMoveRepeats > 15 && (keyboardMoveRepeats % 4) == 0) { keyboardMoveSpeed++; } } lastKeyboardMove = DateTime.Now; int offset = (int)(Math.Ceiling(DocumentWorkspace.GetRatio()) * (double)keyboardMoveSpeed);//ScaleFactor. Cursor.Position = new Point(Cursor.Position.X + offset * dir.X, Cursor.Position.Y + offset * dir.Y); Point location = DocumentWorkspace.PointToScreenFromTool(Point.Truncate(DocumentWorkspace.DocumentToClient(PointF.Empty))); PointF stylusLocF = new PointF((float)Cursor.Position.X - (float)location.X, (float)Cursor.Position.Y - (float)location.Y); Point stylusLoc = new Point(Cursor.Position.X - location.X, Cursor.Position.Y - location.Y); stylusLoc = DocumentWorkspace.UnscalePoint(stylusLoc); stylusLocF = DocumentWorkspace.UnscalePoint(stylusLocF); DocumentWorkspace.PerformDocumentMouseMove(new StylusEventArgs(lastButton, 1, stylusLocF.X, stylusLocF.Y, 0, 1.0f)); DocumentWorkspace.PerformDocumentMouseMove(new MouseEventArgs(lastButton, 1, stylusLoc.X, stylusLoc.Y, 0)); } } private bool CanPan() { Rectangle vis = Utility.RoundRectangle(DocumentWorkspace.GetVisibleDocumentRectangleF()); vis.Intersect(Document.Bounds); if (vis == Document.Bounds) { return false; } else { return true; } } private void KeyUp(KeyEventArgs e) { if (this.panMode) { this.panMode = false; this.panTracking = false; this.trackingNub.Visible = false; this.Cursor = this.panOldCursor; this.panOldCursor = null; e.Handled = true; } OnKeyUp(e); } /// /// This method is called when the tool is active and a keyboard key is pressed. /// If you respond to the keyboard key, set e.Handled to true. /// protected virtual void OnKeyUp(KeyEventArgs e) { keysThatAreDown.Clear(); } private void KeyDown(KeyEventArgs e) { OnKeyDown(e); } /// /// This method is called when the tool is active and a keyboard key is released /// Before responding, check that e.Handled is false, and if you then respond to /// the keyboard key, set e.Handled to true. /// protected virtual void OnKeyDown(KeyEventArgs e) { if (!e.Handled) { if (!keysThatAreDown.Contains(e.KeyData)) { keysThatAreDown.Add(e.KeyData, new KeyTimeInfo()); } if (!this.IsMouseDown && !this.panMode && e.KeyCode == Keys.Space) { this.panMode = true; this.panOldCursor = this.Cursor; if (CanPan()) { this.Cursor = this.handCursor; } else { this.Cursor = this.handCursorInvalid; } } // arrow keys are processed in another way // we get their KeyDown but no KeyUp, so they can not be handled // by our normal methods OnKeyPress(e.KeyData); } } private void SelectionChanging() { OnSelectionChanging(); } /// /// This method is called when the Tool is active and the selection area is /// about to be changed. /// protected virtual void OnSelectionChanging() { } private void SelectionChanged() { OnSelectionChanged(); } /// /// This method is called when the Tool is active and the selection area has /// been changed. /// protected virtual void OnSelectionChanged() { } private void ExecutingHistoryMemento(object sender, ExecutingHistoryMementoEventArgs e) { OnExecutingHistoryMemento(e); } protected virtual void OnExecutingHistoryMemento(ExecutingHistoryMementoEventArgs e) { } private void ExecutedHistoryMemento(object sender, ExecutedHistoryMementoEventArgs e) { OnExecutedHistoryMemento(e); } protected virtual void OnExecutedHistoryMemento(ExecutedHistoryMementoEventArgs e) { } private void PasteQuery(IDataObject data, out bool canHandle) { OnPasteQuery(data, out canHandle); } /// /// This method is called when the system is querying a tool as to whether /// it can handle a pasted object. /// /// /// The clipboard data that was pasted by the user that should be inspected. /// /// /// true if the data can be handled by the tool, false if not. /// /// /// If you do not set canHandle to true then the tool will not be /// able to respond to the Edit menu's Paste item. /// protected virtual void OnPasteQuery(IDataObject data, out bool canHandle) { canHandle = false; } private void Paste(IDataObject data, out bool handled) { OnPaste(data, out handled); } /// /// This method is called when the user invokes a paste operation. Tools get /// the first chance to handle this data. /// /// /// The data that was pasted by the user. /// /// /// true if the data was handled and pasted, false if not. /// /// /// If you do not set handled to true the event will be passed to the /// global paste handler. /// protected virtual void OnPaste(IDataObject data, out bool handled) { handled = false; } private void Pulse() { OnPulse(); } protected bool IsFormActive { get { return (object.ReferenceEquals(Form.ActiveForm, DocumentWorkspace.FindForm())); } } /// /// This method is called many times per second, called by the DocumentWorkspace. /// protected virtual void OnPulse() { if (this.panTracking && this.lastButton == MouseButtons.Right) { Point position = this.lastMouseXY; RectangleF visibleRect = DocumentWorkspace.GetVisibleDocumentRectangleF(); PointF visibleCenterPt = Utility.GetRectangleCenter(visibleRect); PointF delta = new PointF(position.X - visibleCenterPt.X, position.Y - visibleCenterPt.Y); PointF newScroll = DocumentWorkspace.GetDocumentScrollPositionF(); this.trackingNub.Visible = true; if (delta.X != 0 || delta.Y != 0) { newScroll.X += delta.X; newScroll.Y += delta.Y; ++this.ignoreMouseMove; // setting DocumentScrollPosition incurs a MouseMove event. ignore it prevents 'jittering' at non-integral zoom levels (like, say, 743%) UI.SuspendControlPainting(DocumentWorkspace.GetThis()); DocumentWorkspace.SetDocumentScrollPositionF(newScroll); this.trackingNub.Visible = true; this.trackingNub.Location = Utility.GetRectangleCenter(DocumentWorkspace.GetVisibleDocumentRectangleF()); UI.ResumeControlPainting(DocumentWorkspace.GetThis()); DocumentWorkspace.InvalidateFromTool(true); Update(); } } } protected bool ScrollIfNecessary(PointF position) { if (!autoScroll || !CanPan()) { return false; } RectangleF visible = DocumentWorkspace.GetVisibleDocumentRectangleF(); PointF lastScrollPosition = DocumentWorkspace.GetDocumentScrollPositionF(); PointF delta = PointF.Empty; PointF zoomedPoint = PointF.Empty; zoomedPoint.X = Utility.Lerp((visible.Left + visible.Right) / 2.0f, position.X, 1.02f); zoomedPoint.Y = Utility.Lerp((visible.Top + visible.Bottom) / 2.0f, position.Y, 1.02f); if (zoomedPoint.X < visible.Left) { delta.X = zoomedPoint.X - visible.Left; } else if (zoomedPoint.X > visible.Right) { delta.X = zoomedPoint.X - visible.Right; } if (zoomedPoint.Y < visible.Top) { delta.Y = zoomedPoint.Y - visible.Top; } else if (zoomedPoint.Y > visible.Bottom) { delta.Y = zoomedPoint.Y - visible.Bottom; } if (!delta.IsEmpty) { PointF newScrollPosition = new PointF(lastScrollPosition.X + delta.X, lastScrollPosition.Y + delta.Y); DocumentWorkspace.SetDocumentScrollPositionF(newScrollPosition); Update(); return true; } else { return false; } } private void SelectionChangingHandler(object sender, EventArgs e) { OnSelectionChanging(); } private void SelectionChangedHandler(object sender, EventArgs e) { OnSelectionChanged(); } protected void SetStatus(ImageResource statusIcon, string statusText) { if (statusIcon == null && statusText != null) { statusIcon = PdnResources.GetImageResource("Icons.MenuHelpHelpTopicsIcon.png"); } DocumentWorkspace.SetStatus(statusText, statusIcon); } protected SurfaceBoxRendererList RendererList { get { return this.DocumentWorkspace.GetRendererList(); } } protected void Update() { DocumentWorkspace.Update(); } protected object GetStaticData() { return DocumentWorkspace.GetStaticToolData(this.GetType()); } protected void SetStaticData(object data) { DocumentWorkspace.SetStaticToolData(this.GetType(), data); } // NOTE: Your constructor must be able to run successfully with a documentWorkspace // of null. This is sent in while the DocumentControl static constructor // class is building a list of ToolInfo instances, so that it may construct // the list without having to also construct a DocumentControl. public Tool(IDocumentWorkspace documentWorkspace, ImageResource toolBarImage, string name, string helpText, char hotKey, bool skipIfActiveOnHotKey, ToolBarConfigItems toolBarConfigItems) { this.documentWorkspace = documentWorkspace; this.toolBarImage = toolBarImage; this.toolInfo = new ToolInfo(name, helpText, toolBarImage, hotKey, skipIfActiveOnHotKey, toolBarConfigItems, this.GetType()); if (this.documentWorkspace != null) { this.documentWorkspace.UpdateStatusBarToToolHelpText(this); } } ~Tool() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { Debug.Assert(!this.active, "Tool is still active!"); if (disposing) { if (this.saveRegion != null) { this.saveRegion.Dispose(); this.saveRegion = null; } OnDisposed(); } } public event EventHandler Disposed; private void OnDisposed() { if (Disposed != null) { Disposed(this, EventArgs.Empty); } } public Form AssociatedForm { get { return AppWorkspace.FindForm(); } } } }