1
// Copyright (C) 2009-2011 Novell, Inc.
2
// Copyright (C) 2009 Julien Rebetez
3
// Copyright (C) 2009 Igor Vatavuk
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.
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.
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.
20
using System.Collections.Generic;
29
using PdfMod.Pdf.Actions;
33
public enum PageSelectionMode {
42
public class DocumentIconView : Gtk.IconView, IDisposable
44
public const int MIN_WIDTH = 128;
45
public const int MAX_WIDTH = 2054;
54
static readonly TargetEntry uri_src_target = new TargetEntry ("text/uri-list", 0, (uint)Target.UriSrc);
55
static readonly TargetEntry uri_dest_target = new TargetEntry ("text/uri-list", TargetFlags.OtherApp, (uint)Target.UriDest);
56
static readonly TargetEntry move_internal_target = new TargetEntry ("pdfmod/page-list", TargetFlags.Widget, (uint)Target.MoveInternal);
57
static readonly TargetEntry move_external_target = new TargetEntry ("pdfmod/page-list-external", 0, (uint)Target.MoveExternal);
62
PageCell page_renderer;
63
PageSelectionMode page_selection_mode = PageSelectionMode.None;
66
public PageListStore Store { get { return store; } }
67
public bool CanZoomIn { get; private set; }
68
public bool CanZoomOut { get; private set; }
70
public IEnumerable<Page> SelectedPages {
72
var pages = new List<Page> ();
73
foreach (var path in SelectedItems) {
75
store.GetIter (out iter, path);
76
pages.Add (store.GetValue (iter, PageListStore.PageColumn) as Page);
78
pages.Sort ((a, b) => { return a.Index < b.Index ? -1 : 1; });
83
public event System.Action ZoomChanged;
85
public DocumentIconView (Client app) : base ()
89
TooltipColumn = PageListStore.TooltipColumn;
90
SelectionMode = SelectionMode.Multiple;
91
ColumnSpacing = RowSpacing = Margin;
92
Model = store = new PageListStore ();
95
Orientation = Orientation.Vertical;
97
// Properties not bound in Gtk#
98
SetProperty ("item-padding", new GLib.Value ((int)0));
100
CanZoomIn = CanZoomOut = true;
102
page_renderer = new PageCell (this);
103
PackStart (page_renderer, false);
104
AddAttribute (page_renderer, "page", PageListStore.PageColumn);
106
// TODO enable uri-list as drag source target for drag-out-of-pdfmod-to-extract feature
107
EnableModelDragSource (Gdk.ModifierType.None, new TargetEntry [] { move_internal_target, move_external_target, uri_src_target }, Gdk.DragAction.Default | Gdk.DragAction.Move);
108
EnableModelDragDest (new TargetEntry [] { move_internal_target, move_external_target, uri_dest_target }, Gdk.DragAction.Default | Gdk.DragAction.Move);
110
SizeAllocated += HandleSizeAllocated;
111
PopupMenu += HandlePopupMenu;
112
ButtonPressEvent += HandleButtonPressEvent;
113
SelectionChanged += HandleSelectionChanged;
114
DragDataReceived += HandleDragDataReceived;
115
DragDataGet += HandleDragDataGet;
116
DragBegin += HandleDragBegin;
117
DragLeave += HandleDragLeave;
120
public override void Dispose ()
122
page_renderer.Dispose ();
126
#region Gtk.Widget event handlers/overrides
128
protected override bool OnScrollEvent (Gdk.EventScroll evnt)
130
if ((evnt.State & Gdk.ModifierType.ControlMask) != 0) {
131
Zoom (evnt.Direction == ScrollDirection.Down ? -20 : 20);
134
return base.OnScrollEvent (evnt);
138
void HandleSizeAllocated (object o, EventArgs args)
140
if (!zoom_manually_set) {
145
void HandleSelectionChanged (object o, EventArgs args)
147
if (!refreshing_selection) {
148
page_selection_mode = PageSelectionMode.None;
152
void HandleButtonPressEvent (object o, ButtonPressEventArgs args)
154
if (args.Event.Button == 3) {
155
var path = GetPathAtPos ((int)args.Event.X, (int)args.Event.Y);
157
if (!PathIsSelected (path)) {
158
bool ctrl = (args.Event.State & Gdk.ModifierType.ControlMask) != 0;
159
bool shift = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0;
165
if (GetCursor (out cursor, out cell)) {
166
TreePath first = cursor.Compare (path) < 0 ? cursor : path;
170
} while (first != path && first != cursor && first != null);
179
HandlePopupMenu (null, null);
185
void HandlePopupMenu (object o, PopupMenuArgs args)
187
app.Actions["PageContextMenu"].Activate ();
192
#region Drag and Drop event handling
194
void HandleDragBegin (object o, DragBeginArgs args)
196
// Set the drag icon, otherwise it will be a whole page cell rendering,
197
// which can be quite large and obscure the drop points
198
bool single = SelectedItems.Length == 1;
199
Gtk.Drag.SetIconStock (args.Context, single ? Stock.Dnd : Stock.DndMultiple, 0, 0);
202
void HandleDragLeave (object o, DragLeaveArgs args)
205
Gtk.Drag.Unhighlight (this);
211
protected override bool OnDragMotion (Gdk.DragContext context, int x, int y, uint time_)
213
// Scroll if within 20 pixels of the top or bottom
214
var parent = Parent.Parent as Gtk.ScrolledWindow;
215
double rel_y = y - parent.Vadjustment.Value;
217
parent.Vadjustment.Value -= 30;
218
} else if ((parent.Allocation.Height - rel_y) < 20) {
219
parent.Vadjustment.Value = Math.Min (parent.Vadjustment.Upper - parent.Allocation.Height, parent.Vadjustment.Value + 30);
222
var targets = context.Targets.Select (t => (string)t);
224
if (targets.Contains (move_internal_target.Target) || targets.Contains (move_external_target.Target)) {
225
bool ret = base.OnDragMotion (context, x, y, time_);
228
} else if (targets.Contains (uri_dest_target.Target)) {
229
// TODO could do this (from Gtk+ docs) to make sure the uris are all .pdfs (or mime-sniffed as pdfs):
230
/* If the decision whether the drop will be accepted or rejected can't be made based solely on the
231
cursor position and the type of the data, the handler may inspect the dragged data by calling gtk_drag_get_data() and
232
defer the gdk_drag_status() call to the "drag-data-received" handler. Note that you cannot not pass GTK_DEST_DEFAULT_DROP,
233
GTK_DEST_DEFAULT_MOTION or GTK_DEST_DEFAULT_ALL to gtk_drag_dest_set() when using the drag-motion signal that way. */
234
Gdk.Drag.Status (context, DragAction.Copy, time_);
236
Gtk.Drag.Highlight (this);
245
Gdk.Drag.Abort (context, time_);
249
void SetDestInfo (int x, int y)
252
IconViewDropPosition pos;
253
GetCorrectedPathAndPosition (x, y, out path, out pos);
254
SetDragDestItem (path, pos);
257
void HandleDragDataGet(object o, DragDataGetArgs args)
259
if (args.Info == move_internal_target.Info) {
260
var pages = new Hyena.Gui.DragDropList<Page> ();
261
pages.AddRange (SelectedPages);
262
pages.AssignToSelection (args.SelectionData, Gdk.Atom.Intern (move_internal_target.Target, false));
264
} else if (args.Info == move_external_target.Info) {
265
string doc_and_pages = String.Format ("{0}{1}{2}", document.CurrentStateUri, newline[0], String.Join (",", SelectedPages.Select (p => p.Index.ToString ()).ToArray ()));
266
byte [] data = System.Text.Encoding.UTF8.GetBytes (doc_and_pages);
267
args.SelectionData.Set (Gdk.Atom.Intern (move_external_target.Target, false), 8, data);
269
} else if (args.Info == uri_src_target.Info) {
270
// TODO implement page extraction via DnD?
271
Console.WriteLine ("HandleDragDataGet, wants a uri list...");
275
void GetCorrectedPathAndPosition (int x, int y, out TreePath path, out IconViewDropPosition pos)
277
GetDestItemAtPos (x, y, out path, out pos);
279
// Convert drop above/below/into into DropLeft or DropRight based on the x coordinate
280
if (path != null && (pos == IconViewDropPosition.DropAbove || pos == IconViewDropPosition.DropBelow || pos == IconViewDropPosition.DropInto)) {
281
if (!path.Equals (GetPathAtPos (x + ItemSize/2, y))) {
282
pos = IconViewDropPosition.DropRight;
284
pos = IconViewDropPosition.DropLeft;
289
int GetDropIndex (int x, int y)
293
IconViewDropPosition pos;
294
GetCorrectedPathAndPosition (x, y, out path, out pos);
299
store.GetIter (out iter, path);
300
if (TreeIter.Zero.Equals (iter))
303
var to_index = (store.GetValue (iter, PageListStore.PageColumn) as Page).Index;
304
if (pos == IconViewDropPosition.DropRight) {
311
static string [] newline = new string [] { "\r\n" };
312
void HandleDragDataReceived (object o, DragDataReceivedArgs args)
315
string target = (string)args.SelectionData.Target;
316
if (target == move_internal_target.Target) {
317
// Move pages within the document
318
int to_index = GetDropIndex (args.X, args.Y);
322
var pages = args.SelectionData.Data as Hyena.Gui.DragDropList<Page>;
323
to_index -= pages.Count (p => p.Index < to_index);
324
var action = new MoveAction (document, pages, to_index);
326
app.Actions.UndoManager.AddUndoAction (action);
328
} else if (target == move_external_target.Target) {
329
int to_index = GetDropIndex (args.X, args.Y);
333
string doc_and_pages = System.Text.Encoding.UTF8.GetString (args.SelectionData.Data);
334
var pieces = doc_and_pages.Split (newline, StringSplitOptions.RemoveEmptyEntries);
335
string uri = pieces[0];
336
int [] pages = pieces[1].Split (',').Select (p => Int32.Parse (p)).ToArray ();
338
document.AddFromUri (new Uri (uri), to_index, pages);
340
} else if (target == uri_src_target.Target) {
341
var uris = System.Text.Encoding.UTF8.GetString (args.SelectionData.Data).Split (newline, StringSplitOptions.RemoveEmptyEntries);
342
if (uris.Length == 1 && app.Document == null) {
343
app.LoadPath (uris[0]);
346
int to_index = GetDropIndex (args.X, args.Y);
349
var add_pages = new System.Action (delegate {
350
// TODO somehow ask user for which pages of the docs to insert?
351
// TODO pwd handling - keyring#?
352
// TODO make action/undoable
353
for (; uri_i < uris.Length; uri_i++) {
354
var before_count = document.Count;
355
document.AddFromUri (new Uri (uris[uri_i]), to_index);
356
to_index += document.Count - before_count;
360
if (document == null || to_index < 0) {
361
// Load the first page, then add the other pages to it
362
app.LoadPath (uris[uri_i++], null, delegate {
363
if (document != null) {
364
to_index = document.Count;
376
Gtk.Drag.Finish (args.Context, (bool)args.RetVal, false, args.Time);
381
#region Document event handling
383
public void SetDocument (Document new_doc)
385
if (document != null) {
386
document.PagesAdded -= OnPagesAdded;
387
document.PagesChanged -= OnPagesChanged;
388
document.PagesRemoved -= OnPagesRemoved;
389
document.PagesMoved -= OnPagesMoved;
393
document.PagesAdded += OnPagesAdded;
394
document.PagesChanged += OnPagesChanged;
395
document.PagesRemoved += OnPagesRemoved;
396
document.PagesMoved += OnPagesMoved;
398
store.SetDocument (document);
399
page_selection_mode = PageSelectionMode.None;
404
void OnPagesAdded (int index, Page [] pages)
406
foreach (var page in pages) {
407
store.InsertWithValues (index, store.GetValuesForPage (page));
414
void OnPagesChanged (Page [] pages)
419
void OnPagesRemoved (Page [] pages)
421
foreach (var page in pages) {
422
var iter = store.GetIterForPage (page);
423
if (!TreeIter.Zero.Equals (iter)) {
424
store.Remove (ref iter);
440
if (!zoom_manually_set) {
447
void UpdateAllPages ()
449
foreach (var page in document.Pages) {
450
var iter = store.GetIterForPage (page);
451
if (!TreeIter.Zero.Equals (iter)) {
452
store.UpdateForPage (iter, page);
453
store.EmitRowChanged (store.GetPath (iter), iter);
460
bool zoom_manually_set;
461
public void Zoom (int pixels)
463
Zoom (pixels, false);
466
public void Zoom (int pixels, bool absolute)
468
CanZoomIn = CanZoomOut = true;
470
if (!zoom_manually_set) {
471
zoom_manually_set = true;
472
(app.Actions["ZoomFit"] as ToggleAction).Active = false;
475
int new_width = absolute ? pixels : ItemSize + pixels;
476
if (new_width <= MIN_WIDTH) {
478
new_width = MIN_WIDTH;
479
} else if (new_width >= MAX_WIDTH) {
481
new_width = MAX_WIDTH;
484
if (ItemSize == new_width) {
488
SetItemSize (new_width);
490
var handler = ZoomChanged;
491
if (handler != null) {
496
int last_zoom, before_last_zoom;
497
public void ZoomFit ()
499
if (document == null)
502
if ((app.Actions["ZoomFit"] as ToggleAction).Active == false)
505
zoom_manually_set = false;
506
// Try to fit all pages into the view, with a minimum size
507
var n = (double)document.Count;
508
var width = (double)(Parent.Allocation.Width - 2 * (Margin + BorderWidth + 8)); // HACK this + 8 is total hack
509
var height = (double)(Parent.Allocation.Height - 2 * (Margin + BorderWidth + 8)); // same
511
var n_across = (int)Math.Ceiling (Math.Sqrt (width * n / height));
512
var best_width = (int)Math.Floor ((width - (n_across + 1)*ColumnSpacing - n_across*2*FocusLineWidth) / n_across);
514
// restrict to min/max
515
best_width = Math.Min (MAX_WIDTH, Math.Max (MIN_WIDTH, best_width));
517
if (best_width == ItemSize) {
521
// Total hack to avoid infinite SizeAllocate/ZoomFit loop
522
if (best_width == before_last_zoom || best_width == last_zoom) {
526
before_last_zoom = last_zoom;
527
last_zoom = ItemSize;
529
SetItemSize (best_width);
530
CanZoomOut = ItemSize > MIN_WIDTH;
531
CanZoomIn = ItemSize < MAX_WIDTH;
533
var handler = ZoomChanged;
534
if (handler != null) {
539
public int ItemSize {
540
get { return page_renderer.ItemSize; }
541
set { page_renderer.ItemSize = value; }
544
private void SetItemSize (int w)
548
// HACK trigger gtk_icon_view_invalidate_sizes and queue_layout
549
Orientation = Orientation.Horizontal;
550
Orientation = Orientation.Vertical;
555
string selection_match_query;
556
public void SetSelectionMatchQuery (string query)
558
selection_match_query = query;
559
SetPageSelectionMode (PageSelectionMode.Matching);
562
public void SetPageSelectionMode (PageSelectionMode mode)
564
page_selection_mode = mode;
568
bool refreshing_selection;
569
void RefreshSelection ()
571
refreshing_selection = true;
573
if (page_selection_mode == PageSelectionMode.All) {
575
} else if (page_selection_mode != PageSelectionMode.None) {
576
List<Page> matches = null;
577
if (page_selection_mode == PageSelectionMode.Matching) {
578
matches = new List<Page> (app.Document.FindPagesMatching (selection_match_query));
582
foreach (var iter in store.TreeIters) {
583
var path = store.GetPath (iter);
586
switch (page_selection_mode) {
587
case PageSelectionMode.Evens:
588
select = (i % 2) == 0;
590
case PageSelectionMode.Odds:
591
select = (i % 2) == 1;
593
case PageSelectionMode.Matching:
594
select = matches.Contains (store.GetValue (iter, PageListStore.PageColumn) as Page);
596
case PageSelectionMode.Inverse:
597
select = !PathIsSelected (path);
610
refreshing_selection = false;