1 /////////////////////////////////////////////////////////////////////////////////
2 // Paint.NET                                                                   //
3 // Copyright (C) dotPDN LLC, Rick Brewster, Tom Jackson, and contributors.     //
4 // Portions Copyright (C) Microsoft Corporation. All Rights Reserved.          //
5 // See license-pdn.txt for full licensing and attribution details.             //
6 //                                                                             //
7 // Ported to Pinta by: Jonathan Pobst <monkey@jpobst.com>                      //
8 /////////////////////////////////////////////////////////////////////////////////
9 
10 using System;
11 using Gdk;
12 using Pinta.Core;
13 
14 namespace Pinta.Gui.Widgets
15 {
16 	[System.ComponentModel.ToolboxItem (true)]
17 	public class AnglePickerGraphic : Gtk.DrawingArea
18 	{
19 		private bool tracking = false;
20 		private Point lastMouseXY;
21 		private double angleValue;
22 
AnglePickerGraphic()23 		public AnglePickerGraphic ()
24 		{
25 			Events = ((Gdk.EventMask)(16134));
26 
27 			ButtonPressEvent += HandleHandleButtonPressEvent;
28 			ButtonReleaseEvent += HandleHandleButtonReleaseEvent;
29 			MotionNotifyEvent += HandleHandleMotionNotifyEvent;
30 		}
31 
32 		#region Public Properties
33 		public int Value {
34 			get { return (int)angleValue; }
35 			set {
36 				double v = value % 360;
37 				if (angleValue != v) {
38 					angleValue = v;
39 					OnValueChanged ();
40 					this.GdkWindow.Invalidate ();
41 				}
42 			}
43 		}
44 
45 		public double ValueDouble {
46 			get { return angleValue; }
47 			set {
48 				//double v = Math.IEEERemainder (value, 360.0);
49 				if (angleValue != value) {
50 					angleValue = value;
51 					OnValueChanged ();
52 
53 					if (GdkWindow != null)
54 						GdkWindow.Invalidate ();
55 				}
56 			}
57 		}
58 		#endregion
59 
60 		#region Mouse Handlers
HandleHandleMotionNotifyEvent(object o, Gtk.MotionNotifyEventArgs args)61 		private void HandleHandleMotionNotifyEvent (object o, Gtk.MotionNotifyEventArgs args)
62 		{
63 			ProcessMouseEvent (new Point ((int)args.Event.X, (int)args.Event.Y), (args.Event.State & ModifierType.ShiftMask) == ModifierType.ShiftMask);
64 		}
65 
HandleHandleButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args)66 		private void HandleHandleButtonReleaseEvent (object o, Gtk.ButtonReleaseEventArgs args)
67 		{
68 			tracking = false;
69 		}
70 
HandleHandleButtonPressEvent(object o, Gtk.ButtonPressEventArgs args)71 		private void HandleHandleButtonPressEvent (object o, Gtk.ButtonPressEventArgs args)
72 		{
73 			tracking = true;
74 
75 			ProcessMouseEvent (new Point ((int)args.Event.X, (int)args.Event.Y), args.Event.IsShiftPressed ());
76 		}
77 
ProcessMouseEvent(Point pt, bool constrainAngle)78 		private void ProcessMouseEvent (Point pt, bool constrainAngle)
79 		{
80 			lastMouseXY = pt;
81 
82 			if (tracking) {
83 				Rectangle ourRect = Rectangle.Inflate (GdkWindow.GetBounds (), -2, -2);
84 				int diameter = Math.Min (ourRect.Width, ourRect.Height);
85 				Point center = new Point (ourRect.X + (diameter / 2), ourRect.Y + (diameter / 2));
86 
87 				int dx = lastMouseXY.X - center.X;
88 				int dy = lastMouseXY.Y - center.Y;
89 				double theta = Math.Atan2 (-dy, dx);
90 
91 				double newAngle = (theta * 360) / (2 * Math.PI);
92 
93 				if (newAngle < 0)
94 					newAngle = newAngle + 360;
95 
96 				if (constrainAngle) {
97 					const double constraintAngle = 15.0;
98 
99 					double multiple = newAngle / constraintAngle;
100 					double top = Math.Floor (multiple);
101 					double topDelta = Math.Abs (top - multiple);
102 					double bottom = Math.Ceiling (multiple);
103 					double bottomDelta = Math.Abs (bottom - multiple);
104 
105 					double bestMultiple;
106 					if (bottomDelta < topDelta) {
107 						bestMultiple = bottom;
108 					} else {
109 						bestMultiple = top;
110 					}
111 
112 					newAngle = bestMultiple * constraintAngle;
113 				}
114 
115 				this.ValueDouble = newAngle;
116 
117 				GdkWindow.Invalidate ();
118 			}
119 		}
120 		#endregion
121 
122 		#region Drawing Code
OnExposeEvent(Gdk.EventExpose ev)123 		protected override bool OnExposeEvent (Gdk.EventExpose ev)
124 		{
125 			base.OnExposeEvent (ev);
126 
127 			using (Cairo.Context g = CairoHelper.Create (GdkWindow)) {
128 				Cairo.Rectangle ourRect = Rectangle.Inflate (GdkWindow.GetBounds (), -1, -1).ToCairoRectangle ();
129 				double diameter = Math.Min (ourRect.Width, ourRect.Height);
130 
131 				double radius = (diameter / 2.0);
132 
133 				Cairo.PointD center = new Cairo.PointD (
134 				    (float)(ourRect.X + radius),
135 				    (float)(ourRect.Y + radius));
136 
137 				double theta = (this.angleValue * 2.0 * Math.PI) / 360.0;
138 
139 				Cairo.Rectangle ellipseRect = new Cairo.Rectangle (ourRect.Location (), diameter, diameter);
140 				Cairo.Rectangle ellipseOutlineRect = ellipseRect;
141 
142 				g.DrawEllipse (ellipseOutlineRect, new Cairo.Color (.1, .1, .1), 1);
143 
144 				double endPointRadius = radius - 2;
145 
146 				Cairo.PointD endPoint = new Cairo.PointD (
147 				    (float)(center.X + (endPointRadius * Math.Cos (theta))),
148 				    (float)(center.Y - (endPointRadius * Math.Sin (theta))));
149 
150 				float gripSize = 2.5f;
151 				Cairo.Rectangle gripEllipseRect = new Cairo.Rectangle (center.X - gripSize, center.Y - gripSize, gripSize * 2, gripSize * 2);
152 
153 				g.FillEllipse (gripEllipseRect, new Cairo.Color (.1, .1, .1));
154 				g.DrawLine (center, endPoint, new Cairo.Color (.1, .1, .1), 1);
155 			}
156 
157 			return true;
158 		}
159 
OnSizeRequested(ref Gtk.Requisition requisition)160 		protected override void OnSizeRequested (ref Gtk.Requisition requisition)
161 		{
162 			// Calculate desired size here.
163 			requisition.Height = 50;
164 			requisition.Width = 50;
165 		}
166 		#endregion
167 
168 		#region Public Events
169 		public event EventHandler ValueChanged;
170 
OnValueChanged()171 		protected virtual void OnValueChanged ()
172 		{
173 			if (ValueChanged != null) {
174 				ValueChanged (this, EventArgs.Empty);
175 			}
176 		}
177 		#endregion
178 	}
179 }
180