using PaintDotNet.Measurement.HistoryMementos; using PaintDotNet.SystemLayer; using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Threading; using System.Windows.Forms; namespace PaintDotNet.Measurement.Tools { public sealed class GradientTool : Tool { public static string StaticName { get { return PdnResources.GetString("GradientTool.Name"); } } public static ImageResource StaticImage { get { return PdnResources.GetImageResource("Icons.GradientToolIcon.png"); } } private Cursor toolCursor; private Cursor toolMouseDownCursor; private ImageResource toolIcon; private MoveNubRenderer[] moveNubs; private MoveNubRenderer startNub; private MoveNubRenderer endNub; private MoveNubRenderer mouseNub; // which nub the mouse is manipulating, null for neither private MouseButtons mouseButton = MouseButtons.None; private PointF startPoint; private PointF endPoint; private string helpTextInitial = PdnResources.GetString("GradientTool.HelpText"); private string helpTextWhileAdjustingFormat = PdnResources.GetString("GradientTool.HelpText.WhileAdjusting.Format"); private string helpTextAdjustable = PdnResources.GetString("GradientTool.HelpText.Adjustable"); private bool shouldMoveBothNubs = false; private bool shouldConstrain = false; private bool shouldSwapColors = false; private bool gradientActive = false; // we are drawing or adjusting a gradient private CompoundHistoryMemento historyMemento = null; private void ConstrainPoints(PointF a, ref PointF b) { PointF dir = new PointF(b.X - a.X, b.Y - a.Y); double theta = Math.Atan2(dir.Y, dir.X); double len = Math.Sqrt(dir.X * dir.X + dir.Y * dir.Y); theta = Math.Round(12 * theta / Math.PI) * Math.PI / 12; b = new PointF((float)(a.X + len * Math.Cos(theta)), (float)(a.Y + len * Math.Sin(theta))); } protected override void OnPulse() { if (this.gradientActive && this.moveNubs != null) { for (int i = 0; i < this.moveNubs.Length; ++i) { if (!this.moveNubs[i].Visible) { continue; } // Oscillate between 25% and 100% alpha over a period of 2 seconds // Alpha value of 100% is sustained for a large duration of this period const int period = 10000 * 2000; // 10000 ticks per ms, 2000ms per second long tick = (DateTime.Now.Ticks % period) + (i * (period / this.moveNubs.Length)); ; double sin = Math.Sin(((double)tick / (double)period) * (2.0 * Math.PI)); // sin is [-1, +1] sin = Math.Min(0.5, sin); // sin is [-1, +0.5] sin += 1.0; // sin is [0, 1.5] sin /= 2.0; // sin is [0, 0.75] sin += 0.25; // sin is [0.25, 1] int newAlpha = (int)(sin * 255.0); int clampedAlpha = Utility.Clamp(newAlpha, 0, 255); this.moveNubs[i].Alpha = clampedAlpha; } } base.OnPulse(); } private bool controlKeyDown = false; private DateTime controlKeyDownTime = DateTime.MinValue; private readonly TimeSpan controlKeyDownThreshold = new TimeSpan(0, 0, 0, 0, 400); protected override void OnKeyDown(KeyEventArgs e) { switch (e.KeyCode) { case Keys.ControlKey: if (!this.controlKeyDown) { this.controlKeyDown = true; this.controlKeyDownTime = DateTime.Now; } break; case Keys.ShiftKey: bool oldShouldConstrain = this.shouldConstrain; this.shouldConstrain = true; if (this.gradientActive && this.mouseButton != MouseButtons.None && !oldShouldConstrain) { RenderGradient(); } break; } base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { switch (e.KeyCode) { case Keys.ControlKey: TimeSpan heldDuration = (DateTime.Now - this.controlKeyDownTime); // If the user taps Ctrl, then we should toggle the visiblity of the moveNubs if (heldDuration < this.controlKeyDownThreshold) { for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Visible = this.gradientActive && !this.moveNubs[i].Visible; } } this.controlKeyDown = false; break; case Keys.ShiftKey: this.shouldConstrain = false; if (this.gradientActive && this.mouseButton != MouseButtons.None) { RenderGradient(); } break; } base.OnKeyUp(e); } protected override void OnKeyPress(KeyPressEventArgs e) { if (this.gradientActive) { switch (e.KeyChar) { case '\r': // Enter e.Handled = true; CommitGradient(); break; case (char)27: // Escape e.Handled = true; CommitGradient(); //HistoryStack.StepBackward(); break; } } base.OnKeyPress(e); } private sealed class RenderContext { public Surface surface; public Rectangle[] rois; public GradientRenderer renderer; public void Render(object cpuIndexObj) { int cpuIndex = (int)cpuIndexObj; int start = (this.rois.Length * cpuIndex) / Processor.LogicalCpuCount; int end = (this.rois.Length * (cpuIndex + 1)) / Processor.LogicalCpuCount; renderer.Render(this.surface, this.rois, start, end - start); } } private void RenderGradient(Surface surface, PdnRegion clipRegion, CompositingMode compositingMode, PointF startPointF, ColorBgra startColor, PointF endPointF, ColorBgra endColor) { GradientRenderer gr = AppEnvironment.GradientInfo().CreateGradientRenderer(); gr.StartColor = startColor; gr.EndColor = endColor; gr.StartPoint = startPointF; gr.EndPoint = endPointF; gr.AlphaBlending = (compositingMode == CompositingMode.SourceOver); gr.BeforeRender(); Rectangle[] oldRois = clipRegion.GetRegionScansReadOnlyInt(); Rectangle[] newRois; if (oldRois.Length == 1) { newRois = new Rectangle[Processor.LogicalCpuCount]; Utility.SplitRectangle(oldRois[0], newRois); } else { newRois = oldRois; } RenderContext rc = new RenderContext(); rc.surface = surface; rc.rois = newRois; rc.renderer = gr; WaitCallback wc = new WaitCallback(rc.Render); for (int i = 0; i < Processor.LogicalCpuCount; ++i) { if (i == Processor.LogicalCpuCount - 1) { wc(BoxedConstants.GetInt32(i)); } else { PaintDotNet.Threading.ThreadPool.Global.QueueUserWorkItem(wc, BoxedConstants.GetInt32(i)); } } PaintDotNet.Threading.ThreadPool.Global.Drain(); } private void RenderGradient() { ColorBgra startColor = AppEnvironment.PrimaryColor(); ColorBgra endColor = AppEnvironment.SecondaryColor(); if (this.shouldSwapColors) { if (AppEnvironment.GradientInfo().AlphaOnly) { // In transparency mode, the color values don't matter. We just need to reverse // and invert the alpha values. byte startAlpha = startColor.A; startColor.A = (byte)(255 - endColor.A); endColor.A = (byte)(255 - startAlpha); } else { Utility.Swap(ref startColor, ref endColor); } } PointF startPointF = this.startPoint; PointF endPointF = this.endPoint; if (this.shouldConstrain) { if (this.mouseNub == this.startNub) { ConstrainPoints(endPointF, ref startPointF); } else { ConstrainPoints(startPointF, ref endPointF); } } RestoreSavedRegion(); Surface surface = ((BitmapLayer)DocumentWorkspace.GetActiveLayer()).Surface; PdnRegion clipRegion = DocumentWorkspace.GetSelection().CreateRegion(); SaveRegion(clipRegion, clipRegion.GetBoundsInt()); RenderGradient(surface, clipRegion, AppEnvironment.GetCompositingMode(), startPointF, startColor, endPointF, endColor); using (PdnRegion simplified = Utility.SimplifyAndInflateRegion(clipRegion, Utility.DefaultSimplificationFactor, 0)) { DocumentWorkspace.GetActiveLayer().Invalidate(simplified); } clipRegion.Dispose(); // Set up status bar text double angle = -180.0 * Math.Atan2(endPointF.Y - startPointF.Y, endPointF.X - startPointF.X) / Math.PI; MeasurementUnit units = AppWorkspace.GetUnits(); double offsetXPhysical = Document.PixelToPhysicalX(endPointF.X - startPointF.X, units); double offsetYPhysical = Document.PixelToPhysicalY(endPointF.Y - startPointF.Y, units); double offsetLengthPhysical = Math.Sqrt(offsetXPhysical * offsetXPhysical + offsetYPhysical * offsetYPhysical); string numberFormat; string unitsAbbreviation; if (units != MeasurementUnit.Pixel) { string unitsAbbreviationName = "MeasurementUnit." + units.ToString() + ".Abbreviation"; unitsAbbreviation = PdnResources.GetString(unitsAbbreviationName); numberFormat = "F2"; } else { unitsAbbreviation = string.Empty; numberFormat = "F0"; } string unitsString = PdnResources.GetString("MeasurementUnit." + units.ToString() + ".Plural"); string statusText = string.Format( this.helpTextWhileAdjustingFormat, offsetXPhysical.ToString(numberFormat), unitsAbbreviation, offsetYPhysical.ToString(numberFormat), unitsAbbreviation, offsetLengthPhysical.ToString("F2"), unitsString, angle.ToString("F2")); SetStatus(this.toolIcon, statusText); // Make sure everything is on screen. Update(); } protected override void OnMouseDown(MouseEventArgs e) { PointF mousePt = new PointF(e.X, e.Y); MoveNubRenderer mouseCursorNub = PointToNub(mousePt); if (this.mouseButton != MouseButtons.None) { this.shouldMoveBothNubs = !this.shouldMoveBothNubs; } else { bool startNewGradient = true; this.mouseButton = e.Button; if (!this.gradientActive) { this.shouldSwapColors = (this.mouseButton == MouseButtons.Right); } else { this.shouldMoveBothNubs = false; // We are already in the process of drawing or adjusting a gradient. // Determine if they clicked to drag one of the nubs for adjusting. if (mouseCursorNub == null) { // No. Commit the old gradient and begin a new one. CommitGradient(); startNewGradient = true; this.shouldSwapColors = (this.mouseButton == MouseButtons.Right); } else { // Yes. Continue adjusting the old gradient. Cursor = this.handCursorMouseDown; this.mouseNub = mouseCursorNub; this.mouseNub.Location = mousePt; if (this.mouseNub == this.startNub) { this.startPoint = mousePt; } else { this.endPoint = mousePt; } if (this.mouseButton == MouseButtons.Right) { this.shouldSwapColors = !this.shouldSwapColors; } RenderGradient(); startNewGradient = false; } } if (startNewGradient) { // Brand new gradient. Set everything up. this.startPoint = mousePt; this.startNub.Location = mousePt; this.startNub.Visible = true; this.endNub.Location = mousePt; this.endNub.Visible = true; this.endPoint = mousePt; this.mouseNub = mouseCursorNub; Cursor = this.toolMouseDownCursor; this.gradientActive = true; ClearSavedRegion(); RenderGradient(); this.historyMemento = new CompoundHistoryMemento(StaticName, StaticImage); //HistoryStack.PushNewMemento(this.historyMemento); // this makes it so they can push Esc to undo } } base.OnMouseDown(e); } private MoveNubRenderer PointToNub(PointF mousePtF) { float startDistance = Utility.Distance(mousePtF, this.startNub.Location); float endDistance = Utility.Distance(mousePtF, this.endNub.Location); if (this.startNub.Visible && startDistance < endDistance && this.startNub.IsPointTouching(mousePtF, true)) { return this.startNub; } else if (this.endNub.Visible && this.endNub.IsPointTouching(mousePtF, true)) { return this.endNub; } else { return null; } } protected override void OnMouseMove(MouseEventArgs e) { PointF mousePtF = new PointF(e.X, e.Y); MoveNubRenderer mouseCursorNub = PointToNub(mousePtF); if (this.mouseButton == MouseButtons.None) { // No mouse button dragging is being tracked. this.mouseNub = mouseCursorNub; if (this.mouseNub == this.startNub || this.mouseNub == this.endNub) { Cursor = this.handCursor; } else { Cursor = this.toolCursor; } } else { if (this.mouseNub == this.startNub) { // Dragging the start nub if (this.shouldConstrain && !this.shouldMoveBothNubs) { ConstrainPoints(this.endPoint, ref mousePtF); } this.startNub.Location = mousePtF; SizeF delta = new SizeF( this.startNub.Location.X - this.startPoint.X, this.startNub.Location.Y - this.startPoint.Y); this.startPoint = mousePtF; if (this.shouldMoveBothNubs) { this.endNub.Location += delta; this.endPoint += delta; } } else if (this.mouseNub == this.endNub) { // Dragging the ending nub if (this.shouldConstrain && !this.shouldMoveBothNubs) { ConstrainPoints(this.startPoint, ref mousePtF); } this.endNub.Location = mousePtF; SizeF delta = new SizeF( this.endNub.Location.X - this.endPoint.X, this.endNub.Location.Y - this.endPoint.Y); this.endPoint = mousePtF; if (this.shouldMoveBothNubs) { this.startNub.Location += delta; this.startPoint += delta; } } else { // Initial drawing if (this.shouldMoveBothNubs) { SizeF delta = new SizeF( this.endNub.Location.X - mousePtF.X, this.endNub.Location.Y - mousePtF.Y); this.startNub.Location -= delta; this.startPoint -= delta; } else if (this.shouldConstrain) { ConstrainPoints(this.startPoint, ref mousePtF); } this.endNub.Location = mousePtF; this.endPoint = mousePtF; } RenderGradient(); } base.OnMouseMove(e); } private void CommitGradient() { if (!this.gradientActive) { throw new InvalidOperationException("CommitGradient() called when a gradient was not active"); } RenderGradient(); using (PdnRegion clipRegion = DocumentWorkspace.GetSelection().CreateRegion()) { BitmapHistoryMemento bhm = new BitmapHistoryMemento( StaticName, StaticImage, DocumentWorkspace, DocumentWorkspace.GetActiveLayerIndex(), clipRegion, this.ScratchSurface); this.historyMemento.PushNewAction(bhm); // We assume this.historyMemento has already been pushed on to the HistoryStack this.historyMemento = null; } this.startNub.Visible = false; this.endNub.Visible = false; ClearSavedRegion(); ClearSavedMemory(); this.gradientActive = false; SetStatus(this.toolIcon, this.helpTextInitial); } protected override void OnMouseUp(MouseEventArgs e) { PointF mousePt = new PointF(e.X, e.Y); if (!this.gradientActive) { // do nothing } else if (e.Button != this.mouseButton) { this.shouldMoveBothNubs = !this.shouldMoveBothNubs; } else { if (this.mouseNub == this.startNub) { // We were adjusting the start nub. if (this.shouldConstrain) { ConstrainPoints(this.endPoint, ref mousePt); } this.startNub.Location = mousePt; this.startPoint = mousePt; } else if (this.mouseNub == this.endNub) { // We were adjusting the ending nub. if (this.shouldConstrain) { ConstrainPoints(this.startPoint, ref mousePt); } this.endNub.Location = mousePt; this.endPoint = mousePt; } else { // We were drawing a brand new gradient. if (this.shouldConstrain) { ConstrainPoints(this.startPoint, ref mousePt); } this.endNub.Location = mousePt; this.endPoint = mousePt; } // In any event, make sure the nubs are visible and other state adjusted accordingly. this.startNub.Visible = true; this.endNub.Visible = true; this.mouseButton = MouseButtons.None; this.gradientActive = true; SetStatus(this.toolIcon, this.helpTextAdjustable); } base.OnMouseUp(e); } private void RenderBecauseOfEvent(object sender, EventArgs e) { if (this.gradientActive) { RenderGradient(); } } protected override void OnActivate() { this.toolCursor = new Cursor(PdnResources.GetResourceStream("Cursors.GenericToolCursor.cur")); this.toolMouseDownCursor = new Cursor(PdnResources.GetResourceStream("Cursors.GenericToolCursorMouseDown.cur")); this.Cursor = this.toolCursor; this.toolIcon = this.Image; this.startNub = new MoveNubRenderer(RendererList); this.startNub.Visible = false; this.startNub.Shape = MoveNubShape.Circle; RendererList.Add(this.startNub, false); this.endNub = new MoveNubRenderer(RendererList); this.endNub.Visible = false; this.endNub.Shape = MoveNubShape.Circle; RendererList.Add(this.endNub, false); this.moveNubs = new MoveNubRenderer[] { this.startNub, this.endNub }; AppEnvironment.PrimaryColorChanged += new EventHandler(RenderBecauseOfEvent); AppEnvironment.SecondaryColorChanged += new EventHandler(RenderBecauseOfEvent); AppEnvironment.GradientInfoChanged += new EventHandler(RenderBecauseOfEvent); AppEnvironment.AlphaBlendingChanged += new EventHandler(RenderBecauseOfEvent); AppWorkspace.UnitsChanged += new EventHandler(RenderBecauseOfEvent); base.OnActivate(); } protected override void OnDeactivate() { AppEnvironment.PrimaryColorChanged -= new EventHandler(RenderBecauseOfEvent); AppEnvironment.SecondaryColorChanged -= new EventHandler(RenderBecauseOfEvent); AppEnvironment.GradientInfoChanged -= new EventHandler(RenderBecauseOfEvent); AppEnvironment.AlphaBlendingChanged -= new EventHandler(RenderBecauseOfEvent); AppWorkspace.UnitsChanged -= new EventHandler(RenderBecauseOfEvent); if (this.gradientActive) { CommitGradient(); this.mouseButton = MouseButtons.None; } if (this.startNub != null) { RendererList.Remove(this.startNub); this.startNub.Dispose(); this.startNub = null; } if (this.endNub != null) { RendererList.Remove(this.endNub); this.endNub.Dispose(); this.endNub = null; } this.moveNubs = null; if (this.toolCursor != null) { this.toolCursor.Dispose(); this.toolCursor = null; } if (this.toolMouseDownCursor != null) { this.toolMouseDownCursor.Dispose(); this.toolMouseDownCursor = null; } base.OnDeactivate(); } public GradientTool(IDocumentWorkspace documentWorkspace) : base(documentWorkspace, StaticImage, StaticName, PdnResources.GetString("GradientTool.HelpText"), 'g', false, ToolBarConfigItems.Gradient | ToolBarConfigItems.AlphaBlending) { } } }