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