Fix build on Mono 2.8
[pdfmod:pdfmod.git] / src / PdfMod / Gui / Client.cs
1 // Copyright (C) 2009-2010 Novell, Inc.
2 // Copyright (C) 2009 Robert Dyer
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program; if not, write to the Free Software
16 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17
18 using System;
19 using System.Linq;
20 using System.Collections.Generic;
21 using System.IO;
22
23 using Gtk;
24 using Mono.Unix;
25
26 using Hyena;
27 using Hyena.Gui;
28
29 using PdfSharp;
30 using PdfSharp.Pdf;
31 using PdfSharp.Pdf.IO;
32
33 using PdfMod.Pdf;
34
35 namespace PdfMod.Gui
36 {
37     public class Client : Core.Client
38     {
39         static int app_count = 0;
40         static string accel_map_file = Path.Combine (Path.Combine (
41             XdgBaseDirectorySpec.GetUserDirectory ("XDG_CONFIG_HOME", ".config"), "pdfmod"), "gtk_accel_map");
42
43         Gtk.MenuBar menu_bar;
44         Gtk.Label status_label;
45         QueryBox query_box;
46
47         bool loading;
48         long original_size;
49         string original_size_string = null;
50
51         public ActionManager     ActionManager { get; private set; }
52         public Gtk.Toolbar       HeaderToolbar { get; private set; }
53         public Actions           Actions       { get; private set; }
54         public Gtk.Statusbar     StatusBar     { get; private set; }
55         public Gtk.Window        Window        { get; private set; }
56         public DocumentIconView  IconView      { get; private set; }
57         public MetadataEditorBox EditorBox     { get; private set; }
58         public BookmarkView      BookmarkView  { get; private set; }
59
60         static Client ()
61         {
62             Gtk.Application.Init ();
63             ThreadAssist.InitializeMainThread ();
64             ThreadAssist.ProxyToMainHandler = RunIdle;
65             Hyena.Log.Notify += OnLogNotify;
66             Gtk.Window.DefaultIconName = "pdfmod";
67
68             try {
69                 if (System.IO.File.Exists (accel_map_file)) {
70                     Gtk.AccelMap.Load (accel_map_file);
71                     Hyena.Log.DebugFormat ("Loaded custom AccelMap from {0}", accel_map_file);
72                 }
73             } catch (Exception e) {
74                 Hyena.Log.Exception ("Failed to load custom AccelMap", e);
75             }
76         }
77
78         public Client () : this (false)
79         {
80         }
81
82         internal Client (bool loadFiles)
83         {
84             app_count++;
85
86             Window = new Gtk.Window (Gtk.WindowType.Toplevel) { Title = Catalog.GetString ("PDF Mod") };
87             Window.SetSizeRequest (640, 480);
88             Window.DeleteEvent += delegate (object o, DeleteEventArgs args) {
89                 Quit ();
90                 args.RetVal = true;
91             };
92
93             // PDF Icon View
94             IconView = new DocumentIconView (this);
95             var iconview_sw = new Gtk.ScrolledWindow ();
96             iconview_sw.AddWithViewport (IconView);
97
98             query_box = new QueryBox (this) { NoShowAll = true };
99             query_box.Hide ();
100
101             // ActionManager
102             ActionManager = new Hyena.Gui.ActionManager ();
103             Window.AddAccelGroup (ActionManager.UIManager.AccelGroup);
104             Actions = new Actions (this, ActionManager);
105
106             // Status bar
107             StatusBar = new Gtk.Statusbar () { HasResizeGrip = true };
108             status_label = new Label () { Xalign = 0.0f };
109             StatusBar.PackStart (status_label, true, true, 6);
110             StatusBar.ReorderChild (status_label, 0);
111
112             var zoom_slider = new ZoomSlider (this);
113             StatusBar.PackEnd (zoom_slider, false, false, 0);
114             StatusBar.ReorderChild (zoom_slider, 1);
115
116             // Properties editor box
117             EditorBox = new MetadataEditorBox (this) { NoShowAll = true };
118             EditorBox.Hide ();
119
120             // Menubar
121             menu_bar = ActionManager.UIManager.GetWidget ("/MainMenu") as MenuBar;
122
123             // Toolbar
124             HeaderToolbar = ActionManager.UIManager.GetWidget ("/HeaderToolbar") as Gtk.Toolbar;
125             HeaderToolbar.ShowArrow = false;
126             HeaderToolbar.ToolbarStyle = ToolbarStyle.Icons;
127             HeaderToolbar.Tooltips = true;
128             HeaderToolbar.NoShowAll = true;
129             HeaderToolbar.Visible = Configuration.ShowToolbar;
130
131             // BookmarksView
132             BookmarkView = new BookmarkView (this);
133             BookmarkView.NoShowAll = true;
134             BookmarkView.Visible = false;
135
136             var vbox = new VBox ();
137             vbox.PackStart (menu_bar, false, false, 0);
138             vbox.PackStart (HeaderToolbar, false, false, 0);
139             vbox.PackStart (EditorBox, false, false, 0);
140             vbox.PackStart (query_box, false, false, 0);
141
142             var hbox = new HPaned ();
143             hbox.Add1 (BookmarkView);
144             hbox.Add2 (iconview_sw);
145             vbox.PackStart (hbox, true, true, 0);
146
147             vbox.PackStart (StatusBar, false, true, 0);
148             Window.Add (vbox);
149
150             Window.ShowAll ();
151
152             if (loadFiles) {
153                 RunIdle (LoadFiles);
154                 Application.Run ();
155             }
156         }
157
158         public void ToggleMatchQuery ()
159         {
160             if (query_box.Entry.HasFocus) {
161                 query_box.Hide ();
162             } else {
163                 query_box.Show ();
164                 query_box.Entry.GrabFocus ();
165             }
166         }
167
168         public void Quit ()
169         {
170             if (Window == null) {
171                 return;
172             }
173
174             if (PromptIfUnsavedChanges ()) {
175                 return;
176             }
177
178             if (Document != null) {
179                 Document.Dispose ();
180             }
181
182             if (IconView != null) {
183                 IconView.Dispose ();
184                 IconView = null;
185             }
186
187             Window.Destroy ();
188             Window = null;
189
190             if (--app_count == 0) {
191                 try {
192                     Directory.CreateDirectory (Path.GetDirectoryName (accel_map_file));
193                     Gtk.AccelMap.Save (accel_map_file);
194                 } catch (Exception e) {
195                     Hyena.Log.Exception ("Failed to save custom AccelMap", e);
196                 }
197
198                 Application.Quit ();
199             }
200         }
201
202         bool PromptIfUnsavedChanges ()
203         {
204             if (Document != null && Document.HasUnsavedChanges) {
205                 var dialog = new Hyena.Widgets.HigMessageDialog (
206                     Window, DialogFlags.Modal, MessageType.Warning, ButtonsType.None,
207                     Catalog.GetString ("Save the changes made to this document?"),
208                     String.Empty
209                 );
210                 dialog.AddButton (Catalog.GetString ("Close _Without Saving"), ResponseType.Close, false);
211                 dialog.AddButton (Stock.Cancel, ResponseType.Cancel, false);
212                 dialog.AddButton (Stock.SaveAs, ResponseType.Ok, true);
213
214                 var response = (ResponseType) dialog.Run ();
215                 dialog.Destroy ();
216
217                 switch (response) {
218                     case ResponseType.Ok:
219                         Actions["SaveAs"].Activate ();
220                         return PromptIfUnsavedChanges ();
221                     case ResponseType.Close:
222                         return false;
223                     case ResponseType.Cancel:
224                     case ResponseType.DeleteEvent:
225                         return true;
226                 }
227             }
228             return false;
229         }
230
231         public override void LoadFiles (IList<string> files)
232         {
233             if (files.Count == 1) {
234                 LoadPath (files[0]);
235             } else if (files.Count > 1) {
236                 // Make sure the user wants to open N windows
237                 var dialog = new Hyena.Widgets.HigMessageDialog (
238                     Window, DialogFlags.Modal, MessageType.Question, ButtonsType.None,
239                     String.Format (Catalog.GetPluralString (
240                         "Continue, opening {0} document in separate windows?", "Continue, opening all {0} documents in separate windows?", files.Count),
241                         files.Count),
242                     String.Empty);
243                 dialog.AddButton (Stock.Cancel, ResponseType.Cancel, false);
244                 dialog.AddButton (Catalog.GetString ("Open _First"), ResponseType.Accept, false);
245                 dialog.AddButton (Catalog.GetString ("Open _All"), ResponseType.Ok, true);
246                 var response = dialog.Run ();
247                 dialog.Destroy ();
248
249                 if ((Gtk.ResponseType)response == Gtk.ResponseType.Ok) {
250                     foreach (string file in files) {
251                         LoadPath (file);
252                     }
253                 } else if ((Gtk.ResponseType)response == Gtk.ResponseType.Accept) {
254                     LoadPath (files[0]);
255                 }
256             }
257         }
258
259         public override void LoadPath (string path, string suggestedFilename, System.Action finishedCallback)
260         {
261             lock (this) {
262                 // One document per window
263                 if (loading || Document != null) {
264                     new Client ().LoadPath (path, suggestedFilename);
265                     return;
266                 }
267
268                 loading = true;
269             }
270
271             if (!path.StartsWith ("file://")) {
272                 path = System.IO.Path.GetFullPath (path);
273             }
274
275             Configuration.LastOpenFolder = System.IO.Path.GetDirectoryName (suggestedFilename ?? path);
276             status_label.Text = Catalog.GetString ("Loading document...");
277
278             ThreadAssist.SpawnFromMain (delegate {
279                 try {
280                     Document = new Document ();
281                     Document.Load (path, PasswordProvider, suggestedFilename != null);
282                     if (suggestedFilename != null) {
283                         Document.SuggestedSavePath = suggestedFilename;
284                     }
285
286                     ThreadAssist.BlockingProxyToMain (delegate {
287                         IconView.SetDocument (Document);
288                         BookmarkView.SetDocument (Document);
289                         RecentManager.Default.AddItem (Document.Uri);
290
291                         Document.Changed += UpdateForDocument;
292                         UpdateForDocument ();
293                         OnDocumentLoaded ();
294                     });
295                 } catch (Exception e) {
296                     Document = null;
297                     ThreadAssist.BlockingProxyToMain (delegate {
298                         status_label.Text = "";
299                         if (e is System.IO.FileNotFoundException) {
300                             try {
301                                 RecentManager.Default.RemoveItem (new Uri(path).AbsoluteUri);
302                             } catch {}
303                         }
304                     });
305
306                     Hyena.Log.Exception (e);
307                     Hyena.Log.Error (
308                         Catalog.GetString ("Error Loading Document"),
309                         String.Format (Catalog.GetString ("There was an error loading {0}"), GLib.Markup.EscapeText (path ?? "")), true
310                     );
311                 } finally {
312                     lock (this) {
313                         loading = false;
314                     }
315
316                     if (finishedCallback != null)
317                         finishedCallback ();
318                 }
319             });
320         }
321
322         void UpdateForDocument ()
323         {
324             ThreadAssist.AssertInMainThread ();
325             var current_size = Document.FileSize;
326             string size_str = null;
327             if (original_size_string == null) {
328                 size_str = original_size_string = new Hyena.Query.FileSizeQueryValue (current_size).ToUserQuery ();
329                 original_size = current_size;
330             } else if (current_size == original_size) {
331                 size_str = original_size_string;
332             } else {
333                 string current_size_string = new Hyena.Query.FileSizeQueryValue (current_size).ToUserQuery ();
334                 if (current_size_string == original_size_string) {
335                     size_str = original_size_string;
336                 } else {
337                     // Translators: this string is used to show current/original file size, eg "2 MB (originally 1 MB)"
338                     size_str = String.Format (Catalog.GetString ("{0} (originally {1})"), current_size_string, original_size_string);
339                 }
340             }
341
342             status_label.Text = String.Format ("{0} \u2013 {1}",
343                 String.Format (Catalog.GetPluralString ("{0} page", "{0} pages", Document.Count), Document.Count),
344                 size_str
345             );
346
347             var title = Document.Title;
348             var filename = Document.Filename;
349             if (Document.HasUnsavedChanges) {
350                 filename = "*" + filename;
351             }
352             Window.Title = title == null ? filename : String.Format ("{0} - {1}", filename, title);
353         }
354
355         public void PasswordProvider (PdfPasswordProviderArgs args)
356         {
357             // This method is called from some random thread, but we need
358             // to do the dialog on the GUI thread; use the reset_event
359             // to block this thread until the user is done with the dialog.
360             ThreadAssist.BlockingProxyToMain (delegate {
361                 Log.Debug ("Password requested to open document");
362                 var dialog = new Hyena.Widgets.HigMessageDialog (
363                     Window, DialogFlags.Modal, MessageType.Question, ButtonsType.None,
364                     Catalog.GetString ("Document is Encrypted"),
365                     Catalog.GetString ("Enter the document's password to open it:")
366                 );
367                 dialog.Image = Gtk.IconTheme.Default.LoadIcon ("dialog-password", 48, 0);
368
369                 var password_entry = new Entry () { Visibility = false };
370                 password_entry.Show ();
371                 dialog.LabelVBox.PackStart (password_entry, false, false, 12);
372
373                 dialog.AddButton (Stock.Cancel, ResponseType.Cancel, false);
374                 dialog.AddButton (Stock.Ok, ResponseType.Ok, true);
375
376                 var response = (ResponseType)dialog.Run ();
377                 string password = password_entry.Text;
378                 dialog.Destroy ();
379
380                 if (response == ResponseType.Ok) {
381                     args.Password = Document.Password = password;
382                 } else {
383                     Log.Information ("Password dialog cancelled");
384                     args.Abort = true;
385                 }
386             });
387         }
388
389         public Gtk.FileChooserDialog CreateChooser (string title, FileChooserAction action)
390         {
391             var chooser = new Gtk.FileChooserDialog (title, this.Window, action) {
392                 DefaultResponse = ResponseType.Ok
393             };
394             chooser.AddButton (Stock.Cancel, ResponseType.Cancel);
395             chooser.AddFilter (GtkUtilities.GetFileFilter (Catalog.GetString ("PDF Documents"), new string [] {"pdf"}));
396             chooser.AddFilter (GtkUtilities.GetFileFilter (Catalog.GetString ("All Files"), new string [] {"*"}));
397
398             var dirs = new string [] { "DOWNLOAD", "DOCUMENTS" }.Select (s => GetXdgDir (s))
399                                                                 .Where (d => d != null)
400                                                                 .ToArray ();
401             Hyena.Gui.GtkUtilities.SetChooserShortcuts (chooser, dirs);
402
403             return chooser;
404         }
405
406         private string GetXdgDir (string type)
407         {
408             try {
409                 return XdgBaseDirectorySpec.GetXdgDirectoryUnderHome (String.Format ("XDG_{0}_DIR", type), null);
410             } catch {
411                 return null;
412             }
413         }
414
415         static void OnLogNotify (LogNotifyArgs args)
416         {
417             ThreadAssist.ProxyToMain (delegate {
418                 Gtk.MessageType mtype = Gtk.MessageType.Error;
419                 var entry = args.Entry;
420
421                 switch (entry.Type) {
422                     case LogEntryType.Warning:
423                         mtype = Gtk.MessageType.Warning;
424                         break;
425                     case LogEntryType.Information:
426                         mtype = Gtk.MessageType.Info;
427                         break;
428                     case LogEntryType.Error:
429                     default:
430                         mtype = Gtk.MessageType.Error;
431                         break;
432                 }
433
434                 Hyena.Widgets.HigMessageDialog dialog = new Hyena.Widgets.HigMessageDialog (
435                     null, Gtk.DialogFlags.Modal, mtype, Gtk.ButtonsType.Close, entry.Message, entry.Details);
436
437                 dialog.Title = String.Empty;
438                 dialog.Run ();
439                 dialog.Destroy ();
440             });
441         }
442
443         public static void RunIdle (InvokeHandler handler)
444         {
445             GLib.Idle.Add (delegate { handler (); return false; });
446         }
447     }
448 }