1 //
2 // DocumentWorkspace.cs
3 //
4 // Author:
5 //       Jonathan Pobst <monkey@jpobst.com>
6 //
7 // Copyright (c) 2010 Jonathan Pobst
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 //
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 //
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26 
27 using System;
28 using Gdk;
29 using Mono.Unix;
30 
31 namespace Pinta.Core
32 {
33 	public class DocumentWorkspace
34 	{
35 		private Document document;
36 		private Size canvas_size;
37 		private enum ZoomType
38 		{
39 			ZoomIn,
40 			ZoomOut,
41 			ZoomManually
42 		}
43 
DocumentWorkspace(Document document)44 		internal DocumentWorkspace (Document document)
45 		{
46 			this.document = document;
47 			History = new DocumentWorkspaceHistory (document);
48 		}
49 
50         #region Public Events
51         public event EventHandler<CanvasInvalidatedEventArgs> CanvasInvalidated;
52         public event EventHandler CanvasSizeChanged;
53         #endregion
54 
55 		#region Public Properties
56         public Gtk.DrawingArea Canvas { get; set; }
57 
58 		public bool CanvasFitsInWindow {
59 			get {
60 				Gtk.Viewport view = (Gtk.Viewport)Canvas.Parent;
61 
62 				int window_x = view.Allocation.Width;
63 				int window_y = view.Children[0].Allocation.Height;
64 
65 				if (CanvasSize.Width <= window_x && CanvasSize.Height <= window_y)
66 					return true;
67 
68 				return false;
69 			}
70 		}
71 
72 		public Size CanvasSize {
73 			get { return canvas_size; }
74 			set {
75 				if (canvas_size.Width != value.Width || canvas_size.Height != value.Height) {
76 					canvas_size = value;
77 					OnCanvasSizeChanged ();
78 				}
79 			}
80 		}
81 
82 		public DocumentWorkspaceHistory History { get; private set; }
83 
84 		public bool ImageFitsInWindow {
85 			get {
86 				Gtk.Viewport view = (Gtk.Viewport)Canvas.Parent;
87 
88 				int window_x = view.Allocation.Width;
89 				int window_y = view.Children[0].Allocation.Height;
90 
91 				if (document.ImageSize.Width <= window_x && document.ImageSize.Height <= window_y)
92 					return true;
93 
94 				return false;
95 			}
96 		}
97 
98 		public Cairo.PointD Offset {
99 			get { return new Cairo.PointD ((Canvas.Allocation.Width - canvas_size.Width) / 2, (Canvas.Allocation.Height - canvas_size.Height) / 2); }
100 		}
101 
102 		public double Scale {
103 			get { return (double)CanvasSize.Width / (double)document.ImageSize.Width; }
104 			set {
105 				if (value != (double)CanvasSize.Width / (double)document.ImageSize.Width || value != (double)CanvasSize.Height / (double)document.ImageSize.Height) {
106 					if (document.ImageSize.Width == 0)
107 					{
108 						document.ImageSize = new Size(1, document.ImageSize.Height);
109 					}
110 
111 					if (document.ImageSize.Height == 0)
112 					{
113 						document.ImageSize = new Size(document.ImageSize.Width, 1);
114 					}
115 
116 					int new_x = Math.Max ((int)(document.ImageSize.Width * value), 1);
117 					int new_y = Math.Max ((int)(((long)new_x * document.ImageSize.Height) / document.ImageSize.Width), 1);
118 
119 					CanvasSize = new Gdk.Size (new_x, new_y);
120 					Invalidate ();
121 
122 					if (PintaCore.Tools.CurrentTool.CursorChangesOnZoom)
123 					{
124 						//The current tool's cursor changes when the zoom changes.
125 						PintaCore.Tools.CurrentTool.SetCursor(PintaCore.Tools.CurrentTool.DefaultCursor);
126 					}
127 				}
128 			}
129 		}
130 
131 		#endregion
132 
133 		#region Public Methods
Invalidate()134 		public void Invalidate ()
135 		{
136 			OnCanvasInvalidated (new CanvasInvalidatedEventArgs ());
137 		}
138 
139 		/// <summary>
140 		/// Repaints a rectangle region on the canvas.
141 		/// </summary>
142 		/// <param name='canvasRect'>
143 		/// The rectangle region of the canvas requiring repainting
144 		/// </param>
Invalidate(Gdk.Rectangle canvasRect)145 		public void Invalidate (Gdk.Rectangle canvasRect)
146 		{
147 			Cairo.PointD canvasTopLeft = new Cairo.PointD(canvasRect.Left, canvasRect.Top);
148 			Cairo.PointD canvasBtmRight = new Cairo.PointD(canvasRect.Right + 1, canvasRect.Bottom + 1);
149 
150 			Cairo.PointD winTopLeft = CanvasPointToWindow(canvasTopLeft.X, canvasTopLeft.Y);
151 			Cairo.PointD winBtmRight = CanvasPointToWindow(canvasBtmRight.X, canvasBtmRight.Y);
152 
153 			Gdk.Rectangle winRect = Utility.PointsToRectangle(winTopLeft, winBtmRight, false).ToGdkRectangle();
154 
155 			OnCanvasInvalidated (new CanvasInvalidatedEventArgs (winRect));
156 		}
157 
158 		/// <summary>
159 		/// Determines whether the rectangle lies (at least partially) outside the canvas area.
160 		/// </summary>
IsPartiallyOffscreen(Gdk.Rectangle rect)161 		public bool IsPartiallyOffscreen (Gdk.Rectangle rect)
162 		{
163 			return (rect.IsEmpty || rect.Left < 0 || rect.Top < 0);
164 		}
165 
PointInCanvas(Cairo.PointD point)166 		public bool PointInCanvas (Cairo.PointD point)
167 		{
168 			if (point.X < 0 || point.Y < 0)
169 				return false;
170 
171 			if (point.X >= document.ImageSize.Width || point.Y >= document.ImageSize.Height)
172 				return false;
173 
174 			return true;
175 		}
176 
RecenterView(double x, double y)177 		public void RecenterView (double x, double y)
178 		{
179 			Gtk.Viewport view = (Gtk.Viewport)Canvas.Parent;
180 
181 			view.Hadjustment.Value = Utility.Clamp (x * Scale - view.Hadjustment.PageSize / 2, view.Hadjustment.Lower, view.Hadjustment.Upper);
182 			view.Vadjustment.Value = Utility.Clamp (y * Scale - view.Vadjustment.PageSize / 2, view.Vadjustment.Lower, view.Vadjustment.Upper);
183 		}
184 
ScrollCanvas(int dx, int dy)185 		public void ScrollCanvas (int dx, int dy)
186 		{
187 			Gtk.Viewport view = (Gtk.Viewport)Canvas.Parent;
188 
189 			view.Hadjustment.Value = Utility.Clamp (dx + view.Hadjustment.Value, view.Hadjustment.Lower, view.Hadjustment.Upper - view.Hadjustment.PageSize);
190 			view.Vadjustment.Value = Utility.Clamp (dy + view.Vadjustment.Value, view.Vadjustment.Lower, view.Vadjustment.Upper - view.Vadjustment.PageSize);
191 		}
192 
193 		/// <summary>
194 		/// Converts a point from window coordinates to canvas coordinates
195 		/// </summary>
196 		/// <param name='x'>
197 		/// The X coordinate of the window point
198 		/// </param>
199 		/// <param name='y'>
200 		/// The Y coordinate of the window point
201 		/// </param>
WindowPointToCanvas(double x, double y)202 		public Cairo.PointD WindowPointToCanvas (double x, double y)
203 		{
204 			ScaleFactor sf = new ScaleFactor (PintaCore.Workspace.ImageSize.Width,
205 			                                  PintaCore.Workspace.CanvasSize.Width);
206 			Cairo.PointD pt = sf.ScalePoint (new Cairo.PointD (x - Offset.X, y - Offset.Y));
207 			return new Cairo.PointD(pt.X, pt.Y);
208 		}
209 
210 		/// <summary>
211 		/// Converts a point from canvas coordinates to window coordinates
212 		/// </summary>
213 		/// <param name='x'>
214 		/// The X coordinate of the canvas point
215 		/// </param>
216 		/// <param name='y'>
217 		/// The Y coordinate of the canvas point
218 		/// </param>
CanvasPointToWindow(double x, double y)219 		public Cairo.PointD CanvasPointToWindow (double x, double y)
220 		{
221 			ScaleFactor sf = new ScaleFactor (PintaCore.Workspace.ImageSize.Width,
222 			                                  PintaCore.Workspace.CanvasSize.Width);
223 			Cairo.PointD pt = sf.UnscalePoint (new Cairo.PointD (x, y));
224 			return new Cairo.PointD(pt.X + Offset.X, pt.Y + Offset.Y);
225 		}
226 
ZoomIn()227 		public void ZoomIn ()
228 		{
229 			ZoomAndRecenterView (ZoomType.ZoomIn, new Cairo.PointD (-1, -1)); // Zoom in relative to the center of the viewport.
230 		}
231 
ZoomOut()232 		public void ZoomOut ()
233 		{
234 			ZoomAndRecenterView (ZoomType.ZoomOut, new Cairo.PointD (-1, -1)); // Zoom out relative to the center of the viewport.
235 		}
236 
ZoomInFromMouseScroll(Cairo.PointD point)237 		public void ZoomInFromMouseScroll (Cairo.PointD point)
238 		{
239 			ZoomAndRecenterView (ZoomType.ZoomIn, point); // Zoom in relative to mouse position.
240 		}
241 
ZoomOutFromMouseScroll(Cairo.PointD point)242 		public void ZoomOutFromMouseScroll (Cairo.PointD point)
243 		{
244 			ZoomAndRecenterView (ZoomType.ZoomOut, point); // Zoom out relative to mouse position.
245 		}
246 
ZoomManually()247 		public void ZoomManually ()
248 		{
249 			ZoomAndRecenterView (ZoomType.ZoomManually, new Cairo.PointD (-1, -1));
250 		}
251 
ZoomToRectangle(Cairo.Rectangle rect)252 		public void ZoomToRectangle (Cairo.Rectangle rect)
253 		{
254 			double ratio;
255 
256 			if (document.ImageSize.Width / rect.Width <= document.ImageSize.Height / rect.Height)
257 				ratio = document.ImageSize.Width / rect.Width;
258 			else
259 				ratio = document.ImageSize.Height / rect.Height;
260 
261 			(PintaCore.Actions.View.ZoomComboBox.ComboBox as Gtk.ComboBoxEntry).Entry.Text = ViewActions.ToPercent (ratio);
262 			Gtk.Main.Iteration (); //Force update of scrollbar upper before recenter
263 			RecenterView (rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
264 		}
265 		#endregion
266 
267 		#region Private Methods
OnCanvasInvalidated(CanvasInvalidatedEventArgs e)268         protected internal void OnCanvasInvalidated (CanvasInvalidatedEventArgs e)
269         {
270             if (CanvasInvalidated != null)
271                 CanvasInvalidated (this, e);
272         }
273 
OnCanvasSizeChanged()274         public void OnCanvasSizeChanged ()
275         {
276             if (CanvasSizeChanged != null)
277                 CanvasSizeChanged (this, EventArgs.Empty);
278         }
279 
ZoomAndRecenterView(ZoomType zoomType, Cairo.PointD point)280         private void ZoomAndRecenterView (ZoomType zoomType, Cairo.PointD point)
281 		{
282 			if (zoomType == ZoomType.ZoomOut && (CanvasSize.Width == 1 || CanvasSize.Height ==1))
283 				return; //Can't zoom in past a 1x1 px canvas
284 
285 			double zoom;
286 
287 			if (!ViewActions.TryParsePercent (PintaCore.Actions.View.ZoomComboBox.ComboBox.ActiveText, out zoom))
288 				zoom = Scale * 100;
289 
290 			zoom = Math.Min (zoom, 3600);
291 
292             if (Canvas.GdkWindow != null)
293 			    Canvas.GdkWindow.FreezeUpdates ();
294 
295 			PintaCore.Actions.View.SuspendZoomUpdate ();
296 
297 			Gtk.Viewport view = (Gtk.Viewport)Canvas.Parent;
298 
299 			bool adjustOnMousePosition = point.X >= 0.0 && point.Y >= 0.0;
300 
301 			double center_x = adjustOnMousePosition ?
302 				point.X : view.Hadjustment.Value + (view.Hadjustment.PageSize / 2.0);
303 			double center_y = adjustOnMousePosition ?
304 				point.Y : view.Vadjustment.Value + (view.Vadjustment.PageSize / 2.0);
305 
306 			center_x = (center_x - Offset.X) / Scale;
307 			center_y = (center_y - Offset.Y) / Scale;
308 
309 			if (zoomType == ZoomType.ZoomIn || zoomType == ZoomType.ZoomOut) {
310 				int i = 0;
311 
312 				Predicate<string> UpdateZoomLevel = zoomInList =>
313 				{
314 					double zoom_level;
315 					if (!ViewActions.TryParsePercent (zoomInList, out zoom_level))
316 						return false;
317 
318 					switch (zoomType) {
319 					case ZoomType.ZoomIn:
320 						if (zoomInList == Catalog.GetString ("Window") || zoom_level <= zoom) {
321 							PintaCore.Actions.View.ZoomComboBox.ComboBox.Active = i - 1;
322 							return true;
323 						}
324 
325 						break;
326 
327 					case ZoomType.ZoomOut:
328 						if (zoomInList == Catalog.GetString ("Window"))
329 							return true;
330 
331 						if (zoom_level < zoom) {
332 							PintaCore.Actions.View.ZoomComboBox.ComboBox.Active = i;
333 							return true;
334 						}
335 
336 						break;
337 					}
338 
339 					return false;
340 				};
341 
342 				foreach (string item in PintaCore.Actions.View.ZoomCollection) {
343 					if (UpdateZoomLevel (item))
344 						break;
345 
346 					i++;
347 				}
348 			}
349 
350 			PintaCore.Actions.View.UpdateCanvasScale ();
351 
352 			// Quick fix : need to manually update Upper limit because the value is not changing after updating the canvas scale.
353 			// TODO : I think there is an event need to be fired so that those values updated automatically.
354 			view.Hadjustment.Upper = CanvasSize.Width < view.Hadjustment.PageSize ? view.Hadjustment.PageSize : CanvasSize.Width;
355 			view.Vadjustment.Upper = CanvasSize.Height < view.Vadjustment.PageSize ? view.Vadjustment.PageSize : CanvasSize.Height;
356 
357 			RecenterView (center_x, center_y);
358 
359 			PintaCore.Actions.View.ResumeZoomUpdate ();
360             if (Canvas.GdkWindow != null)
361                 Canvas.GdkWindow.ThawUpdates ();
362 		}
363 		#endregion
364 	}
365 }
366