Add copyright headers to all .cs files
[pdfmod:pdfmod.git] / src / PdfMod / Gui / DocumentIconView.cs
1 // Copyright (C) 2009 Novell, Inc.
2 // Copyright (C) 2009 Julien Rebetez
3 // Copyright (C) 2009 Igor Vatavuk
4 //
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
19 using System;
20 using System.Collections.Generic;
21 using System.Linq;
22
23 using Gtk;
24 using Gdk;
25
26 using PdfSharp.Pdf;
27
28 using PdfMod.Pdf;
29 using PdfMod.Pdf.Actions;
30
31 namespace PdfMod.Gui
32 {
33     public enum PageSelectionMode
34     {
35         None,
36         Evens,
37         Odds,
38         Matching,
39         All
40     }
41
42     public class DocumentIconView : Gtk.IconView, IDisposable
43     {
44         public const int MIN_WIDTH = 128;
45         public const int MAX_WIDTH = 2054;
46
47         private enum Target {
48             UriSrc,
49             UriDest,
50             MoveInternal,
51             MoveExternal
52         }
53
54         private static readonly TargetEntry uri_src_target = new TargetEntry ("text/uri-list", 0, (uint)Target.UriSrc);
55         private static readonly TargetEntry uri_dest_target = new TargetEntry ("text/uri-list", TargetFlags.OtherApp, (uint)Target.UriDest);
56         private static readonly TargetEntry move_internal_target = new TargetEntry ("pdfmod/page-list", TargetFlags.Widget, (uint)Target.MoveInternal);
57         private static readonly TargetEntry move_external_target = new TargetEntry ("pdfmod/page-list-external", 0, (uint)Target.MoveExternal);
58
59         private Client app;
60         private Document document;
61         private PageListStore store;
62         private PageCell page_renderer;
63         private PageSelectionMode page_selection_mode = PageSelectionMode.None;
64         private bool highlighted;
65
66         public PageListStore Store { get { return store; } }
67         public bool CanZoomIn { get; private set; }
68         public bool CanZoomOut { get; private set; }
69
70         public IEnumerable<Page> SelectedPages {
71             get {
72                 var pages = new List<Page> ();
73                 foreach (var path in SelectedItems) {
74                     TreeIter iter;
75                     store.GetIter (out iter, path);
76                     pages.Add (store.GetValue (iter, PageListStore.PageColumn) as Page);
77                 }
78                 pages.Sort ((a, b) => { return a.Index < b.Index ? -1 : 1; });
79                 return pages;
80             }
81         }
82
83         public event System.Action ZoomChanged;
84
85         public DocumentIconView (Client app) : base ()
86         {
87             this.app = app;
88
89             TooltipColumn = PageListStore.TooltipColumn;
90             SelectionMode = SelectionMode.Multiple;
91             ColumnSpacing = RowSpacing = Margin;
92             Model = store = new PageListStore ();
93             CanZoomIn = CanZoomOut = true;
94             Reorderable = false;
95             Spacing = 0;
96
97             page_renderer = new PageCell (this);
98             PackStart (page_renderer, true);
99             AddAttribute (page_renderer, "page", PageListStore.PageColumn);
100
101             // TODO enable uri-list as drag source target for drag-out-of-pdfmod-to-extract feature
102             EnableModelDragSource (Gdk.ModifierType.None, new TargetEntry [] { move_internal_target, move_external_target, uri_src_target }, Gdk.DragAction.Default | Gdk.DragAction.Move);
103             EnableModelDragDest (new TargetEntry [] { move_internal_target, move_external_target, uri_dest_target }, Gdk.DragAction.Default | Gdk.DragAction.Move);
104
105             SizeAllocated += HandleSizeAllocated;
106             PopupMenu += HandlePopupMenu;
107             ButtonPressEvent += HandleButtonPressEvent;
108             SelectionChanged += HandleSelectionChanged;
109             DragDataReceived += HandleDragDataReceived;
110             DragDataGet += HandleDragDataGet;
111             DragLeave += HandleDragLeave;
112         }
113
114         public override void Dispose ()
115         {
116             page_renderer.Dispose ();
117             base.Dispose ();
118         }
119
120         #region Gtk.Widget event handlers/overrides
121
122         protected override bool OnScrollEvent (Gdk.EventScroll evnt)
123         {
124             if ((evnt.State & Gdk.ModifierType.ControlMask) != 0) {
125                 Zoom (evnt.Direction == ScrollDirection.Down ? -20 : 20);
126                 return true;
127             } else {
128                 return base.OnScrollEvent (evnt);
129             }
130         }
131
132         private void HandleSizeAllocated (object o, EventArgs args)
133         {
134             if (!zoom_manually_set) {
135                 ZoomFit ();
136             }
137         }
138
139         private void HandleSelectionChanged (object o, EventArgs args)
140         {
141             if (!refreshing_selection) {
142                 page_selection_mode = PageSelectionMode.None;
143             }
144         }
145
146         private void HandleButtonPressEvent (object o, ButtonPressEventArgs args)
147         {
148             if (args.Event.Button == 3) {
149                 var path = GetPathAtPos ((int)args.Event.X, (int)args.Event.Y);
150                 if (path != null) {
151                     if (!PathIsSelected (path)) {
152                         bool ctrl = (args.Event.State & Gdk.ModifierType.ControlMask) != 0;
153                         bool shift = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0;
154                         if (ctrl) {
155                             SelectPath (path);
156                         } else if (shift) {
157                             TreePath cursor;
158                             CellRenderer cell;
159                             if (GetCursor (out cursor, out cell)) {
160                                 TreePath first = cursor.Compare (path) < 0 ? cursor : path;
161                                 do {
162                                     SelectPath (first);
163                                     first.Next ();
164                                 } while (first != path && first != cursor && first != null);
165                             } else {
166                                 SelectPath (path);
167                             }
168                         } else {
169                             UnselectAll ();
170                             SelectPath (path);
171                         }
172                     }
173                     HandlePopupMenu (null, null);
174                     args.RetVal = true;
175                 }
176             }
177         }
178
179         private void HandlePopupMenu (object o, PopupMenuArgs args)
180         {
181             app.Actions["PageContextMenu"].Activate ();
182         }
183
184         #endregion
185
186         #region Drag and Drop event handling
187
188         private void HandleDragLeave (object o, DragLeaveArgs args)
189         {
190             if (highlighted) {
191                 Gtk.Drag.Unhighlight (this);
192                 highlighted = false;
193             }
194             args.RetVal = true;
195         }
196
197         protected override bool OnDragMotion (Gdk.DragContext context, int x, int y, uint time_)
198         {
199             // Scroll if within 20 pixels of the top or bottom
200             var parent = Parent as Gtk.ScrolledWindow;
201             if (y < 20) {
202                 parent.Vadjustment.Value -= 30;
203             } else if ((parent.Allocation.Height - y) < 20) {
204                 parent.Vadjustment.Value = Math.Min (parent.Vadjustment.Upper - parent.Allocation.Height, parent.Vadjustment.Value + 30);
205             }
206
207             var targets = context.Targets.Select (t => (string)t);
208
209             if (targets.Contains (move_internal_target.Target) || targets.Contains (move_external_target.Target)) {
210                 bool ret = base.OnDragMotion (context, x, y, time_);
211                 SetDestInfo (x, y);
212                 return ret;
213             } else if (targets.Contains (uri_dest_target.Target)) {
214                 // TODO could do this (from Gtk+ docs) to make sure the uris are all .pdfs (or mime-sniffed as pdfs):
215                 /* If the decision whether the drop will be accepted or rejected can't be made based solely on the
216                    cursor position and the type of the data, the handler may inspect the dragged data by calling gtk_drag_get_data() and
217                    defer the gdk_drag_status() call to the "drag-data-received" handler. Note that you cannot not pass GTK_DEST_DEFAULT_DROP,
218                    GTK_DEST_DEFAULT_MOTION or GTK_DEST_DEFAULT_ALL to gtk_drag_dest_set() when using the drag-motion signal that way. */
219                 Gdk.Drag.Status (context, DragAction.Copy, time_);
220                 if (!highlighted) {
221                     Gtk.Drag.Highlight (this);
222                     highlighted = true;
223                 }
224
225                 SetDestInfo (x, y);
226
227                 return true;
228             }
229
230             Gdk.Drag.Abort (context, time_);
231             return false;
232         }
233
234         private void SetDestInfo (int x, int y)
235         {
236             TreePath path;
237             IconViewDropPosition pos;
238             GetCorrectedPathAndPosition (x, y, out path, out pos);
239             SetDragDestItem (path, pos);
240         }
241
242         private void HandleDragDataGet(object o, DragDataGetArgs args)
243         {
244             if (args.Info == move_internal_target.Info) {
245                 var pages = new Hyena.Gui.DragDropList<Page> ();
246                 pages.AddRange (SelectedPages);
247                 pages.AssignToSelection (args.SelectionData, Gdk.Atom.Intern (move_internal_target.Target, false));
248                 args.RetVal = true;
249             } else if (args.Info == move_external_target.Info) {
250                 string doc_and_pages = String.Format ("{0}{1}{2}", document.CurrentStateUri, newline[0], String.Join (",", SelectedPages.Select (p => p.Index.ToString ()).ToArray ()));
251                 byte [] data = System.Text.Encoding.UTF8.GetBytes (doc_and_pages);
252                 args.SelectionData.Set (Gdk.Atom.Intern (move_external_target.Target, false), 8, data);
253                 args.RetVal = true;
254             } else if (args.Info == uri_src_target.Info) {
255                 // TODO implement page extraction via DnD?
256                 Console.WriteLine ("HandleDragDataGet, wants a uri list...");
257             }
258         }
259
260         private void GetCorrectedPathAndPosition (int x, int y, out TreePath path, out IconViewDropPosition pos)
261         {
262             GetDestItemAtPos (x, y, out path, out pos);
263
264             // Convert drop above/below/into into DropLeft or DropRight based on the x coordinate
265             if (path != null && (pos == IconViewDropPosition.DropAbove || pos == IconViewDropPosition.DropBelow || pos == IconViewDropPosition.DropInto)) {
266                 if (!path.Equals (GetPathAtPos (x + ItemWidth/2, y))) {
267                     pos = IconViewDropPosition.DropRight;
268                 } else {
269                     pos = IconViewDropPosition.DropLeft;
270                 }
271             }
272         }
273
274         private int GetDropIndex (int x, int y)
275         {
276             TreePath path;
277             TreeIter iter;
278             IconViewDropPosition pos;
279             GetCorrectedPathAndPosition (x, y, out path, out pos);
280             if (path == null) {
281                 return -1;
282             }
283
284             store.GetIter (out iter, path);
285             if (TreeIter.Zero.Equals (iter))
286                 return -1;
287
288             var to_index = (store.GetValue (iter, PageListStore.PageColumn) as Page).Index;
289             if (pos == IconViewDropPosition.DropRight) {
290                 to_index++;
291             }
292
293             return to_index;
294         }
295
296         private static string [] newline = new string [] { "\r\n" };
297         private void HandleDragDataReceived (object o, DragDataReceivedArgs args)
298         {
299             args.RetVal = false;
300             string target = (string)args.SelectionData.Target;
301             if (target == move_internal_target.Target) {
302                 // Move pages within the document
303                 int to_index = GetDropIndex (args.X, args.Y);
304                 if (to_index < 0)
305                     return;
306
307                 var pages = args.SelectionData.Data as Hyena.Gui.DragDropList<Page>;
308                 to_index -= pages.Count (p => p.Index < to_index);
309                 var action = new MoveAction (document, pages, to_index);
310                 action.Do ();
311                 app.Actions.UndoManager.AddUndoAction (action);
312                 args.RetVal = true;
313             } else if (target == move_external_target.Target) {
314                 int to_index = GetDropIndex (args.X, args.Y);
315                 if (to_index < 0)
316                     return;
317
318                 string doc_and_pages = System.Text.Encoding.UTF8.GetString (args.SelectionData.Data);
319                 var pieces = doc_and_pages.Split (newline, StringSplitOptions.RemoveEmptyEntries);
320                 string uri = pieces[0];
321                 int [] pages = pieces[1].Split (',').Select (p => Int32.Parse (p)).ToArray ();
322
323                 document.AddFromUri (new Uri (uri), to_index, pages);
324                 args.RetVal = true;
325             } else if (target == uri_src_target.Target) {
326                 var uris = System.Text.Encoding.UTF8.GetString (args.SelectionData.Data).Split (newline, StringSplitOptions.RemoveEmptyEntries);
327                 if (uris.Length == 1 && app.Document == null) {
328                     app.LoadPath (uris[0]);
329                     args.RetVal = true;
330                 } else {
331                     int to_index = GetDropIndex (args.X, args.Y);
332                     if (to_index < 0)
333                         return;
334                     // TODO somehow ask user for which pages of the docs to insert?
335                     // TODO pwd handling - keyring#?
336                     // TODO make action/undoable
337                     foreach (var uri in uris) {
338                         document.AddFromUri (new Uri (uri), to_index);
339                     }
340                     args.RetVal = true;
341                 }
342             }
343
344             Gtk.Drag.Finish (args.Context, (bool)args.RetVal, false, args.Time);
345         }
346
347         #endregion
348
349         #region Document event handling
350
351         public void SetDocument (Document document)
352         {
353             if (this.document != null) {
354                 this.document.PagesAdded   -= OnPagesAdded;
355                 this.document.PagesChanged -= OnPagesChanged;
356                 this.document.PagesRemoved -= OnPagesRemoved;
357                 this.document.PagesMoved   -= OnPagesMoved;
358             }
359
360             this.document = document;
361             this.document.PagesAdded   += OnPagesAdded;
362             this.document.PagesChanged += OnPagesChanged;
363             this.document.PagesRemoved += OnPagesRemoved;
364             this.document.PagesMoved   += OnPagesMoved;
365
366             store.SetDocument (document);
367             page_selection_mode = PageSelectionMode.None;
368             Refresh ();
369             GrabFocus ();
370         }
371
372         private void OnPagesAdded (int index, Page [] pages)
373         {
374             foreach (var page in pages) {
375                 store.InsertWithValues (index, store.GetValuesForPage (page));
376             }
377
378             UpdateAllPages ();
379             Refresh ();
380         }
381
382         private void OnPagesChanged (Page [] pages)
383         {
384             /*foreach (var page in pages) {
385                 var iter = store.GetIterForPage (page);
386                 if (!TreeIter.Zero.Equals (iter)) {
387                     store.EmitRowChanged (store.GetPath (iter), iter);
388                 }
389             }*/
390
391             Refresh ();
392         }
393
394         private void OnPagesRemoved (Page [] pages)
395         {
396             foreach (var page in pages) {
397                 var iter = store.GetIterForPage (page);
398                 if (!TreeIter.Zero.Equals (iter)) {
399                     store.Remove (ref iter);
400                 }
401             }
402
403             UpdateAllPages ();
404             Refresh ();
405         }
406
407         private void OnPagesMoved ()
408         {
409             UpdateAllPages ();
410             Refresh ();
411         }
412
413         private void Refresh ()
414         {
415             if (!zoom_manually_set) {
416                 ZoomFit ();
417             }
418             RefreshSelection ();
419         }
420
421         private void UpdateAllPages ()
422         {
423             foreach (var page in document.Pages) {
424                 var iter = store.GetIterForPage (page);
425                 if (!TreeIter.Zero.Equals (iter)) {
426                     store.UpdateForPage (iter, page);
427                     store.EmitRowChanged (store.GetPath (iter), iter);
428                 }
429             }
430         }
431
432         #endregion
433
434         private bool zoom_manually_set;
435         public void Zoom (int pixels)
436         {
437             CanZoomIn = CanZoomOut = true;
438
439             if (!zoom_manually_set) {
440                 zoom_manually_set = true;
441                 (app.Actions["ZoomFit"] as ToggleAction).Active = false;
442             }
443
444             int new_width = ItemWidth + pixels;
445             if (new_width <= MIN_WIDTH) {
446                 CanZoomOut = false;
447                 new_width = MIN_WIDTH;
448             } else if (new_width >= MAX_WIDTH) {
449                 CanZoomIn = false;
450                 new_width = MAX_WIDTH;
451             }
452
453             if (ItemWidth == new_width) {
454                 return;
455             }
456
457             ItemWidth = new_width;
458
459             var handler = ZoomChanged;
460             if (handler != null) {
461                 handler ();
462             }
463         }
464
465         private int last_zoom, before_last_zoom;
466         public void ZoomFit ()
467         {
468             if (document == null)
469                 return;
470
471             if ((app.Actions["ZoomFit"] as ToggleAction).Active == false)
472                 return;
473
474             zoom_manually_set = false;
475             // Try to fit all pages into the view, with a minimum size
476             var n = (double)document.Count;
477             var width = (double)Allocation.Width - 2 * Margin - 2*BorderWidth - 4; // HACK this -4 is total hack
478             var height = (double)Allocation.Height - 2 * Margin - 2*BorderWidth - 4; // same
479
480             var n_across = (int)Math.Ceiling (Math.Sqrt (width * n / height));
481             var best_width = (int) Math.Floor ((width - (n_across + 1) * ColumnSpacing - n_across*2*FocusLineWidth) / n_across);
482
483             // restrict to min/max
484             best_width = Math.Min (MAX_WIDTH, Math.Max (MIN_WIDTH, best_width));
485
486             if (best_width == ItemWidth) {
487                 return;
488             }
489
490             // Total hack to avoid infinite SizeAllocate/ZoomFit loop
491             if (best_width == before_last_zoom || best_width == last_zoom) {
492                 return;
493             }
494
495             before_last_zoom = last_zoom;
496             last_zoom = ItemWidth;
497
498             ItemWidth  = best_width;
499             CanZoomOut = ItemWidth > MIN_WIDTH;
500             CanZoomIn  = ItemWidth < MAX_WIDTH;
501
502             var handler = ZoomChanged;
503             if (handler != null) {
504                 handler ();
505             }
506         }
507
508         #region Selection
509
510         private string selection_match_query;
511         public void SetSelectionMatchQuery (string query)
512         {
513             selection_match_query = query;
514             SetPageSelectionMode (PageSelectionMode.Matching);
515         }
516
517         public void SetPageSelectionMode (PageSelectionMode mode)
518         {
519             page_selection_mode = mode;
520             RefreshSelection ();
521         }
522
523         private bool refreshing_selection;
524         private void RefreshSelection ()
525         {
526             refreshing_selection = true;
527             if (page_selection_mode == PageSelectionMode.None) {
528             } else if (page_selection_mode == PageSelectionMode.All) {
529                 SelectAll ();
530             } else {
531                 List<Page> matches = null;
532                 if (page_selection_mode == PageSelectionMode.Matching) {
533                     matches = new List<Page> (app.Document.FindPagesMatching (selection_match_query));
534                 }
535                 int i = 1;
536                 foreach (var iter in store.TreeIters) {
537                     var path = store.GetPath (iter);
538                     bool select = false;
539
540                     switch (page_selection_mode) {
541                     case PageSelectionMode.Evens:
542                         select = (i % 2) == 0;
543                         break;
544                     case PageSelectionMode.Odds:
545                         select = (i % 2) == 1;
546                         break;
547                     case PageSelectionMode.Matching:
548                         select = matches.Contains (store.GetValue (iter, PageListStore.PageColumn) as Page);
549                         break;
550                     }
551
552                     if (select) {
553                         SelectPath (path);
554                     } else {
555                         UnselectPath (path);
556                     }
557                     i++;
558                 }
559             }
560             refreshing_selection = false;
561
562             QueueDraw ();
563         }
564
565         #endregion
566     }
567 }