1 //
2 // WorkspaceManager.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 System.Linq;
29 using Cairo;
30 using Mono.Unix;
31 using System.Collections.Generic;
32 using Gtk;
33 
34 namespace Pinta.Core
35 {
36 	public class WorkspaceManager
37 	{
38 		private int active_document_index = -1;
39 		private int new_file_name = 1;
40 
WorkspaceManager()41 		public WorkspaceManager ()
42 		{
43 			OpenDocuments = new List<Document> ();
44             SelectionHandler = new SelectionModeHandler ();
45 		}
46 
47 		public int ActiveDocumentIndex {
48 			get {
49 				return active_document_index;
50 			}
51 		}
52 
53 		public Document ActiveDocument {
54 			get {
55 				if (HasOpenDocuments)
56 					return OpenDocuments[active_document_index];
57 
58 				throw new InvalidOperationException ("Tried to get WorkspaceManager.ActiveDocument when there are no open Documents.  Check HasOpenDocuments first.");
59 			}
60 		}
61 
62         public SelectionModeHandler SelectionHandler { get; private set; }
63 
64 		public DocumentWorkspace ActiveWorkspace {
65 			get {
66 				if (HasOpenDocuments)
67 					return OpenDocuments[active_document_index].Workspace;
68 
69 				throw new InvalidOperationException ("Tried to get WorkspaceManager.ActiveWorkspace when there are no open Documents.  Check HasOpenDocuments first.");
70 			}
71 		}
72 
73 		public Gdk.Size ImageSize {
74 			get { return ActiveDocument.ImageSize; }
75 			set { ActiveDocument.ImageSize = value; }
76 		}
77 
78 		public Gdk.Size CanvasSize {
79 			get { return ActiveWorkspace.CanvasSize; }
80 			set { ActiveWorkspace.CanvasSize = value; }
81 		}
82 
83 		public PointD Offset {
84 			get { return ActiveWorkspace.Offset; }
85 		}
86 
87 		public double Scale {
88 			get { return ActiveWorkspace.Scale; }
89 			set { ActiveWorkspace.Scale = value; }
90 		}
91 
92 		public List<Document> OpenDocuments { get; private set; }
93 		public bool HasOpenDocuments { get { return OpenDocuments.Count > 0; } }
94 
CreateAndActivateDocument(string filename, Gdk.Size size)95 		public Document CreateAndActivateDocument (string filename, Gdk.Size size)
96 		{
97 			Document doc = new Document (size);
98 
99 			if (string.IsNullOrEmpty (filename))
100 				doc.Filename = string.Format (Catalog.GetString ("Unsaved Image {0}"), new_file_name++);
101 			else
102 				doc.PathAndFileName = filename;
103 
104 			OpenDocuments.Add (doc);
105 			OnDocumentCreated (new DocumentEventArgs (doc));
106 
107 			SetActiveDocument (doc);
108 
109 			return doc;
110 		}
111 
CloseActiveDocument()112 		public void CloseActiveDocument ()
113 		{
114 			CloseDocument (ActiveDocument);
115 		}
116 
CloseDocument(Document document)117 		public void CloseDocument (Document document)
118 		{
119 			int index = OpenDocuments.IndexOf (document);
120 			OpenDocuments.Remove (document);
121 
122 			if (index == active_document_index) {
123 				// If there's other documents open, switch to one of them
124 				if (HasOpenDocuments) {
125 					if (index > 0)
126 						SetActiveDocument (index - 1);
127 					else
128 						SetActiveDocument (index);
129 				} else {
130 					active_document_index = -1;
131 					OnActiveDocumentChanged (EventArgs.Empty);
132 				}
133 			}
134 
135 			document.Close ();
136 
137 			OnDocumentClosed (new DocumentEventArgs (document));
138 		}
139 
Invalidate()140 		public void Invalidate ()
141 		{
142 			if (PintaCore.Workspace.HasOpenDocuments)
143 				ActiveWorkspace.Invalidate ();
144 		}
145 
Invalidate(Gdk.Rectangle rect)146 		public void Invalidate (Gdk.Rectangle rect)
147 		{
148 			ActiveWorkspace.Invalidate (rect);
149 		}
150 
NewDocument(Gdk.Size imageSize, Color backgroundColor)151 		public Document NewDocument (Gdk.Size imageSize, Color backgroundColor)
152 		{
153 			Document doc = CreateAndActivateDocument (null, imageSize);
154 			doc.Workspace.CanvasSize = imageSize;
155 
156 			// Start with an empty white layer
157 			Layer background = doc.AddNewLayer (Catalog.GetString ("Background"));
158 
159 			if (backgroundColor.A != 0) {
160 				using (Cairo.Context g = new Cairo.Context (background.Surface)) {
161 					g.SetSourceColor (backgroundColor);
162 					g.Paint ();
163 				}
164 			}
165 
166 			doc.Workspace.History.PushNewItem (new BaseHistoryItem (Stock.New, Catalog.GetString ("New Image")));
167 			doc.Workspace.History.SetClean();
168 
169 			// This ensures these are called after the window is done being created and sized.
170 			// Without it, we sometimes try to zoom when the window has a size of (0, 0).
171 			Gtk.Application.Invoke (delegate {
172 				PintaCore.Actions.View.ZoomToWindow.Activate ();
173 			});
174 
175 			return doc;
176 		}
177 
178 		// TODO: Standardize add to recent files
OpenFile(string file, Window parent = null)179 		public bool OpenFile (string file, Window parent = null)
180 		{
181 			bool fileOpened = false;
182 
183 			if (parent == null)
184 				parent = PintaCore.Chrome.MainWindow;
185 
186 			try {
187 				// Open the image and add it to the layers
188 				IImageImporter importer = PintaCore.System.ImageFormats.GetImporterByFile (file);
189 				if (importer == null)
190 					throw new FormatException( Catalog.GetString ("Unsupported file format"));
191 
192 				importer.Import (file, parent);
193 
194 				PintaCore.Workspace.ActiveDocument.PathAndFileName = file;
195 				PintaCore.Workspace.ActiveWorkspace.History.PushNewItem (new BaseHistoryItem (Stock.Open, Catalog.GetString ("Open Image")));
196 				PintaCore.Workspace.ActiveDocument.History.SetClean();
197 				PintaCore.Workspace.ActiveDocument.HasFile = true;
198 
199                 // This ensures these are called after the window is done being created and sized.
200                 // Without it, we sometimes try to zoom when the window has a size of (0, 0).
201                 Gtk.Application.Invoke (delegate {
202 				    PintaCore.Actions.View.ZoomToWindow.Activate ();
203 				    PintaCore.Workspace.Invalidate ();
204                 });
205 
206 				fileOpened = true;
207 			} catch (UnauthorizedAccessException) {
208 				ShowFilePermissionErrorDialog (parent, file);
209 			} catch (FormatException e) {
210 				ShowUnsupportedFormatDialog (parent, file, e.Message, e.ToString());
211 			} catch (Exception e) {
212 				ShowOpenFileErrorDialog (parent, file, e.Message, e.ToString ());
213 			}
214 
215 			return fileOpened;
216 		}
217 
ResizeImage(int width, int height)218 		public void ResizeImage (int width, int height)
219 		{
220 			ActiveDocument.ResizeImage (width, height);
221 		}
222 
ResizeCanvas(int width, int height, Anchor anchor, CompoundHistoryItem compoundAction)223 		public void ResizeCanvas (int width, int height, Anchor anchor, CompoundHistoryItem compoundAction)
224 		{
225 			ActiveDocument.ResizeCanvas (width, height, anchor, compoundAction);
226 		}
227 
228 		/// <summary>
229 		/// Converts a point from the active documents
230 		/// window coordinates to canvas coordinates
231 		/// </summary>
232 		/// <param name='x'>
233 		/// The X coordinate of the window point
234 		/// </param>
235 		/// <param name='y'>
236 		/// The Y coordinate of the window point
237 		/// </param>
WindowPointToCanvas(double x, double y)238 		public Cairo.PointD WindowPointToCanvas (double x, double y)
239 		{
240 			return ActiveWorkspace.WindowPointToCanvas (x, y);
241 		}
242 
243 		/// <summary>
244 		/// Converts a point from the active documents
245 		/// canvas coordinates to window coordinates
246 		/// </summary>
247 		/// <param name='x'>
248 		/// The X coordinate of the canvas point
249 		/// </param>
250 		/// <param name='y'>
251 		/// The Y coordinate of the canvas point
252 		/// </param>
CanvasPointToWindow(double x, double y)253 		public Cairo.PointD CanvasPointToWindow (double x, double y)
254 		{
255 			return ActiveWorkspace.CanvasPointToWindow (x, y);
256 		}
257 
ClampToImageSize(Gdk.Rectangle r)258 		public Gdk.Rectangle ClampToImageSize (Gdk.Rectangle r)
259 		{
260 			return ActiveDocument.ClampToImageSize (r);
261 		}
262 
263 		public bool ImageFitsInWindow {
264 			get { return ActiveWorkspace.ImageFitsInWindow; }
265 		}
266 
ResetTitle()267 		internal void ResetTitle ()
268 		{
269 			if (HasOpenDocuments)
270 				PintaCore.Chrome.MainWindow.Title = string.Format ("{0}{1} - Pinta", ActiveDocument.Filename, ActiveDocument.IsDirty ? "*" : "");
271 			else
272 				PintaCore.Chrome.MainWindow.Title = "Pinta";
273 		}
274 
SetActiveDocument(int index)275 		public void SetActiveDocument (int index)
276 		{
277 			if (index >= OpenDocuments.Count)
278 				throw new ArgumentOutOfRangeException ("Tried to WorkspaceManager.SetActiveDocument greater than OpenDocuments.");
279 			if (index < 0)
280 				throw new ArgumentOutOfRangeException ("Tried to WorkspaceManager.SetActiveDocument less that zero.");
281 
282 			SetActiveDocument (OpenDocuments[index]);
283 		}
284 
SetActiveDocument(Document document)285 		public void SetActiveDocument (Document document)
286 		{
287 			RadioAction action = PintaCore.Actions.Window.OpenWindows.Where (p => p.Name == document.Guid.ToString ()).FirstOrDefault ();
288 
289 			if (action == null)
290 				throw new ArgumentOutOfRangeException ("Tried to WorkspaceManager.SetActiveDocument.  Could not find document.");
291 
292 			action.Activate ();
293 		}
294 
SetActiveDocumentInternal(Document document)295 		internal void SetActiveDocumentInternal (Document document)
296 		{
297 			// Work around a case where we closed a document but haven't updated
298 			// the active_document_index yet and it points to the closed document
299 			if (HasOpenDocuments && active_document_index != -1 && OpenDocuments.Count > active_document_index)
300 				PintaCore.Tools.Commit ();
301 
302 			int index = OpenDocuments.IndexOf (document);
303 			active_document_index = index;
304 
305 			OnActiveDocumentChanged (EventArgs.Empty);
306 		}
307 
308 		#region Protected Methods
OnActiveDocumentChanged(EventArgs e)309 		protected void OnActiveDocumentChanged (EventArgs e)
310 		{
311 			if (ActiveDocumentChanged != null)
312 				ActiveDocumentChanged (this, EventArgs.Empty);
313 
314             OnSelectionChanged ();
315 
316 			ResetTitle ();
317 		}
318 
OnDocumentCreated(DocumentEventArgs e)319 		protected internal void OnDocumentCreated (DocumentEventArgs e)
320 		{
321             e.Document.SelectionChanged += (sender, args) => {
322 		        OnSelectionChanged ();
323 		    };
324 
325 			if (DocumentCreated != null)
326 				DocumentCreated (this, e);
327 		}
328 
OnDocumentOpened(DocumentEventArgs e)329 		protected internal void OnDocumentOpened (DocumentEventArgs e)
330 		{
331 			if (DocumentOpened != null)
332 				DocumentOpened (this, e);
333 		}
334 
OnDocumentClosed(DocumentEventArgs e)335 		protected internal void OnDocumentClosed (DocumentEventArgs e)
336 		{
337 			if (DocumentClosed != null)
338 				DocumentClosed (this, e);
339 		}
340 
OnSelectionChanged()341 		private void OnSelectionChanged ()
342 		{
343             if (SelectionChanged != null)
344                 SelectionChanged.Invoke(this, EventArgs.Empty);
345         }
346 #endregion
347 
ShowOpenFileErrorDialog(Window parent, string filename, string primaryText, string details)348         private void ShowOpenFileErrorDialog (Window parent, string filename, string primaryText, string details)
349 		{
350 			string markup = "<span weight=\"bold\" size=\"larger\">{0}</span>\n\n{1}";
351 			string secondaryText = string.Format (Catalog.GetString ("Could not open file: {0}"), filename);
352 			string message = string.Format (markup, primaryText, secondaryText);
353 			PintaCore.Chrome.ShowErrorDialog(parent, message, details);
354 		}
355 
ShowUnsupportedFormatDialog(Window parent, string filename, string primaryText, string details)356 		private void ShowUnsupportedFormatDialog (Window parent, string filename, string primaryText, string details)
357 		{
358 			string markup = "<span weight=\"bold\" size=\"larger\">{0}</span>\n\n{1}";
359 
360 			string secondaryText = string.Format(Catalog.GetString("Could not open file: {0}"), filename);
361 			secondaryText += string.Format("\n\n{0}\n", Catalog.GetString("Pinta supports the following file formats:"));
362 			var extensions = from format in PintaCore.System.ImageFormats.Formats
363 							 where format.Importer != null
364 							 from extension in format.Extensions
365 							 where char.IsLower(extension.FirstOrDefault())
366 							 orderby extension
367 							 select extension;
368 
369 			secondaryText += String.Join(", ", extensions);
370 
371 			string message = string.Format (markup, primaryText, secondaryText);
372 			PintaCore.Chrome.ShowUnsupportedFormatDialog(parent, message, details);
373 		}
374 
ShowFilePermissionErrorDialog(Window parent, string filename)375 		private void ShowFilePermissionErrorDialog (Window parent, string filename)
376 		{
377 			string markup = "<span weight=\"bold\" size=\"larger\">{0}</span>\n\n{1}";
378 			string primary = Catalog.GetString ("Failed to open image");
379 			// Translators: {0} is the name of a file that the user does not have permission to open.
380 			string secondary = string.Format(Catalog.GetString ("You do not have access to '{0}'."), filename);
381 			string message = string.Format (markup, primary, secondary);
382 
383 			var md = new MessageDialog (parent, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, message);
384 			md.Run ();
385 			md.Destroy ();
386 		}
387 
388 		#region Public Events
389 		public event EventHandler ActiveDocumentChanged;
390 		public event EventHandler<DocumentEventArgs> DocumentCreated;
391 		public event EventHandler<DocumentEventArgs> DocumentOpened;
392 		public event EventHandler<DocumentEventArgs> DocumentClosed;
393 		public event EventHandler SelectionChanged;
394 #endregion
395 
396 	}
397 }
398