// // IconView.cs: // // Authors: // Ettore Perazzoli // Larry Ewing // // (C) 2003 Novell, Inc. // using Gtk; using Gdk; using Gnome; using GtkSharp; using System; using System.Reflection; using System.Collections; using System.IO; using FSpot; public class IconView : Gtk.Layout { // Public properties. FSpot.PixbufCache cache; /* preserve the scroll postion when possible */ private bool scroll; private double scroll_value; /* suppress it sometimes */ bool suppress_scroll = false; // Zooming factor. protected const double ZOOM_FACTOR = 1.2; /* Width of the thumbnails. */ protected int thumbnail_width = 128; protected const int MAX_THUMBNAIL_WIDTH = 256; protected const int MIN_THUMBNAIL_WIDTH = 64; public int ThumbnailWidth { get { return thumbnail_width; } set { value = Math.Min(value, MAX_THUMBNAIL_WIDTH); value = Math.Max(value, MIN_THUMBNAIL_WIDTH); if (thumbnail_width != value) { thumbnail_width = value; QueueResize (); if (ZoomChanged != null) ZoomChanged (this, System.EventArgs.Empty); } } } public double Zoom { get { return ((double)(ThumbnailWidth - MIN_THUMBNAIL_WIDTH) / (double)(MAX_THUMBNAIL_WIDTH - MIN_THUMBNAIL_WIDTH)); } set { ThumbnailWidth = (int) ((value) * (MAX_THUMBNAIL_WIDTH - MIN_THUMBNAIL_WIDTH)) + MIN_THUMBNAIL_WIDTH; } } protected double thumbnail_ratio = 4.0 / 3.0; public double ThumbnailRatio { get { return thumbnail_ratio; } set { thumbnail_ratio = value; QueueResize (); } } public int ThumbnailHeight { get { return (int) Math.Round ((double) thumbnail_width / ThumbnailRatio); } } public FSpot.PixbufCache Cache { get { return cache; } } private bool display_tags = true; public bool DisplayTags { get { return display_tags; } set { display_tags = value; QueueResize (); } } private bool display_dates = true; public bool DisplayDates { get { if (cell_width > 100) return display_dates; else return false; } set { display_dates = value; QueueResize (); } } private bool display_filenames = false; public bool DisplayFilenames { get { return display_filenames; } set { if (value != display_filenames) { display_filenames = value; QueueResize (); } } } // Size of the frame around the thumbnail. protected int cell_border_width = 10; // Border around the scrolled area. protected const int BORDER_SIZE = 6; // Thickness of the outline used to indicate selected items. private const int SELECTION_THICKNESS = 5; // Size of the tag icon in the view. protected int tag_icon_size = 16; // Horizontal spacing between the tag icons protected int tag_icon_hspacing = 2; // Vertical spacing between the thumbnail and additional infos (tags, dates, ...). protected int tag_icon_vspacing = 3; // Various other layout values. protected int cells_per_row; protected int cell_width; protected int cell_height; // The first pixel line that is currently on the screen (i.e. in the current // scroll region). Used to compute the area that went offscreen in the "changed" // signal handler for the vertical GtkAdjustment. private int y_offset; private int x_offset; // Focus Handling private int real_focus_cell; public int FocusCell { set { if (value != real_focus_cell) { value = Math.Max (value, 0); value = Math.Min (value, collection.Count - 1); InvalidateCell (value); InvalidateCell (real_focus_cell); real_focus_cell = value; } } get { return real_focus_cell; } } // Number of consecutive GDK_BUTTON_PRESS on the same cell, to // distinguish the GDK_2BUTTON_PRESS events that we actually care // about. private int click_count; // Public events. public delegate void DoubleClickedHandler (IconView view, int clicked_item); public event DoubleClickedHandler DoubleClicked; public delegate void ZoomChangedHandler (object sender, System.EventArgs args); public event ZoomChangedHandler ZoomChanged; // Public API. public IconView (IntPtr raw) : base (raw) {} protected IconView () : base (null, null) { cache = new FSpot.PixbufCache (); cache.OnPixbufLoaded += HandlePixbufLoaded; ScrollAdjustmentsSet += new ScrollAdjustmentsSetHandler (HandleScrollAdjustmentsSet); ButtonPressEvent += new ButtonPressEventHandler (HandleButtonPressEvent); ButtonReleaseEvent += new ButtonReleaseEventHandler (HandleButtonReleaseEvent); KeyPressEvent += new KeyPressEventHandler (HandleKeyPressEvent); ScrollEvent += new ScrollEventHandler(HandleScrollEvent); Destroyed += HandleDestroyed; AddEvents ((int) EventMask.KeyPressMask | (int) EventMask.KeyReleaseMask | (int) EventMask.PointerMotionMask); CanFocus = true; //FSpot.Global.ModifyColors (this); } public IconView (FSpot.IBrowsableCollection collection) : this () { this.collection = collection; this.selection = new SelectionCollection (collection); Name = "ImageContainer"; collection.Changed += HandleChanged; collection.ItemsChanged += HandleItemsChanged; selection.DetailedChanged += HandleSelectionChanged; } private void HandleSelectionChanged (FSpot.IBrowsableCollection collection, int [] ids) { if (ids == null) QueueDraw (); else foreach (int id in ids) InvalidateCell (id); } private void HandleChanged (FSpot.IBrowsableCollection sender) { // FIXME we should probably try to merge the selection forward // but it needs some thought to be efficient. suppress_scroll = true; QueueResize (); } private void HandleItemsChanged (FSpot.IBrowsableCollection sender, BrowsableArgs args) { foreach (int item in args.Items) { UpdateThumbnail (item); InvalidateCell (item); } } // // IPhotoSelection // protected FSpot.IBrowsableCollection collection; public FSpot.IBrowsableCollection Collection { get { return collection; } } protected SelectionCollection selection; public SelectionCollection Selection { get { return selection; } } // FIXME right now a selection change triggers a complete view redraw // This should be optimized away by directly notifyiing the view of changed // indexes rather than having the view connect to the collection.Changed event. public class SelectionCollection : IBrowsableCollection { IBrowsableCollection parent; Hashtable selected_cells; int [] selection; IBrowsableItem [] items; IBrowsableItem [] old; public SelectionCollection (IBrowsableCollection collection) { this.selected_cells = new Hashtable (); this.parent = collection; this.parent.Changed += HandleParentChanged; this.parent.ItemsChanged += HandleParentItemsChanged; } private void HandleParentChanged (IBrowsableCollection collection) { IBrowsableItem [] local = old; selected_cells.Clear (); ClearCached (); if (old != null) { int i = 0; for (i = 0; i < local.Length; i++) { int parent_index = parent.IndexOf (local [i]); if (parent_index >= 0) this.Add (parent_index, false); } } // Call the directly so that we don't reset old immediately this way the old selection // set isn't actually lost until we change it. if (this.Changed != null) Changed (this); if (this.DetailedChanged != null) DetailedChanged (this, null); } public void MarkChanged (int item) { // Forward the change event up to our parent // we'll fire the event when the parent calls us back parent.MarkChanged ((int) selected_cells [item]); } private void HandleParentItemsChanged (IBrowsableCollection collection, BrowsableArgs args) { if (this.ItemsChanged == null) return; ArrayList local_ids = new ArrayList (); foreach (int parent_index in args.Items) { // If the item isn't part of the selection ignore it if (!this.Contains (collection [parent_index])) return; int local_index = this.IndexOf (parent_index); if (local_index >= 0) local_ids.Add (local_index); } if (local_ids.Count == 0) return; ItemsChanged (this, new BrowsableArgs ((int [])local_ids.ToArray (typeof (int)))); } public int [] Ids { get { if (selection != null) return selection; selection = new int [selected_cells.Count]; int i = 0; foreach (int cell in selected_cells.Values) selection [i ++] = cell; Array.Sort (selection); return selection; } } public IBrowsableItem this [int index] { get { int [] ids = this.Ids; return parent [ids[index]]; } } public IBrowsableItem [] Items { get { if (items != null) return items; int [] ids = this.Ids; items = new IBrowsableItem [ids.Length]; for (int i = 0; i < items.Length; i++) { items [i] = parent [ids[i]]; } return items; } } public void Clear () { Clear (true); } public void Clear (bool update) { int [] ids = Ids; selected_cells.Clear (); SignalChange (ids); } public void Add (IBrowsableItem item) { if (this.Contains (item)) return; int index = parent.IndexOf (item); this.Add (index); } public int Count { get { return selected_cells.Count; } } public bool Contains (IBrowsableItem item) { return selected_cells.ContainsKey (item); } public bool Contains (int num) { if (num < 0 || num > parent.Count) return false; return this.Contains (parent [num]); } public void Add (int num) { this.Add (num, true); } private void Add (int num, bool notify) { if (num == -1) return; if (this.Contains (num)) return; IBrowsableItem item = parent [num]; selected_cells [item] = num; if (notify) SignalChange (new int [] {num}); } public void Add (int start, int end) { if (start == -1 || end == -1) return; int current = Math.Min (start, end); int final = Math.Max (start, end); int count = final - current + 1; int [] ids = new int [count]; for (int i = 0; i < count; i++) { this.Add (current, false); ids [i] = current; current++; } SignalChange (ids); } // Remove a range, except the start entry public void Remove (int start, int end) { if (start == -1 || end == -1) return; int current = Math.Min (start + 1, end); int final = Math.Max (start - 1, end); int count = final - current + 1; int [] ids = new int [count]; for (int i = 0; i < count; i++) { this.Remove (current, false); ids [i] = current; current++; } SignalChange (ids); } public int IndexOf (int parent_index) { return System.Array.IndexOf (this.Ids, parent_index); } public int IndexOf (IBrowsableItem item) { if (!this.Contains (item)) return -1; int parent_index = (int) selected_cells [item]; return System.Array.IndexOf (Ids, parent_index); } public void Remove (int cell) { Remove (cell, true); } private void Remove (int cell, bool notify) { IBrowsableItem item = parent [cell]; if (item != null) this.Remove (item, notify); } public void Remove (IBrowsableItem item) { Remove (item, true); } private void Remove (IBrowsableItem item, bool notify) { if (item == null) return; int parent_index = (int) selected_cells [item]; selected_cells.Remove (item); if (notify) SignalChange (new int [] {parent_index}); } public event IBrowsableCollectionChangedHandler Changed; public event IBrowsableCollectionItemsChangedHandler ItemsChanged; public delegate void DetailedCollectionChanged (IBrowsableCollection collection, int [] ids); public event DetailedCollectionChanged DetailedChanged; private void ClearCached () { selection = null; items = null; } private void SignalChange (int [] ids) { ClearCached (); old = this.Items; if (Changed != null) Changed (this); if (DetailedChanged!= null) DetailedChanged (this, ids); } } // Updating. public void UpdateThumbnail (int thumbnail_num) { FSpot.IBrowsableItem photo = collection [thumbnail_num]; string thumbnail_path = FSpot.ThumbnailGenerator.ThumbnailPath (photo.DefaultVersionUri); cache.Remove (thumbnail_path); InvalidateCell (thumbnail_num); } // Cell Geometry public int CellAtPosition (int x, int y) { return CellAtPosition (x, y, true); } public int CellAtPosition (int x, int y, bool crop_visible) { if (collection == null) return -1; if (crop_visible && ((y < (int)Vadjustment.Value || y > (int)Vadjustment.Value + Allocation.Height) || (x < (int)Hadjustment.Value || x > (int)Hadjustment.Value + Allocation.Width))) return -1; if (x < BORDER_SIZE || x >= BORDER_SIZE + cells_per_row * cell_width) return -1; if (y < BORDER_SIZE || y >= BORDER_SIZE + (collection.Count / cells_per_row + 1) * cell_height) return -1; int column = (int) ((x - BORDER_SIZE) / cell_width); int row = (int) ((y - BORDER_SIZE) / cell_height); int cell_num = column + row * cells_per_row; if (cell_num < collection.Count) return (int) cell_num; else return -1; } public int TopLeftVisibleCell () { //return CellAtPosition(BORDER_SIZE, (int)Vadjustment.Value + BORDER_SIZE + 8); return CellAtPosition(BORDER_SIZE, (int) (Vadjustment.Value + Allocation.Height * (Vadjustment.Value / Vadjustment.Upper)) + BORDER_SIZE + 8); } public void GetCellCenter (int cell_num, out int x, out int y) { if (cell_num == -1) { x = -1; y = -1; } x = BORDER_SIZE + (cell_num % cells_per_row) * cell_width + cell_width / 2; y = BORDER_SIZE + (cell_num / cells_per_row) * cell_height + cell_height / 2; } public void GetCellSize (int cell_num, out int w, out int h) { // Trivial for now. w = cell_width; h = cell_height; } // Private utility methods. public void SelectAllCells () { selection.Add (0, collection.Count - 1); } private void ToggleCell (int cell_num) { if (selection.Contains (cell_num)) selection.Remove (cell_num); else selection.Add (cell_num); } // Layout and drawing. // FIXME I can't find a c# wrapper for the C PANGO_PIXELS () macro // So this Function is for that. protected static int PangoPixels (int val) { return val >= 0 ? (val + 1024 / 2) / 1024 : (val - 1024 / 2) / 1024; } protected virtual void UpdateLayout () { UpdateLayout (Allocation); } protected virtual void UpdateLayout (Gdk.Rectangle Allocation) { int available_width = Allocation.Width - 2 * BORDER_SIZE; cell_width = ThumbnailWidth + 2 * cell_border_width; cell_height = ThumbnailHeight + 2 * cell_border_width; cells_per_row = Math.Max ((int) (available_width / cell_width), 1); cell_width += (available_width - cells_per_row * cell_width) / cells_per_row; if (DisplayTags || DisplayDates || DisplayFilenames) cell_height += tag_icon_vspacing; if (DisplayTags) cell_height += tag_icon_size; if (DisplayDates && this.Style != null) { Pango.FontMetrics metrics = this.PangoContext.GetMetrics (this.Style.FontDescription, Pango.Language.FromString ("en_US")); cell_height += PangoPixels (metrics.Ascent + metrics.Descent); } if (DisplayFilenames && this.Style != null) { Pango.FontMetrics metrics = this.PangoContext.GetMetrics (this.Style.FontDescription, Pango.Language.FromString ("en_US")); cell_height += PangoPixels (metrics.Ascent + metrics.Descent); } int num_thumbnails; if (collection != null) num_thumbnails = collection.Count; else num_thumbnails = 0; int num_rows = num_thumbnails / cells_per_row; if (num_thumbnails % cells_per_row != 0) num_rows ++; int height = num_rows * cell_height + 2 * BORDER_SIZE; Vadjustment.StepIncrement = cell_height; int x = (int)(Hadjustment.Value); int y = (int)(height * scroll_value); SetSize (x, y, (int) Allocation.Width, (int) height); } void SetSize (int x, int y, int width, int height) { bool xchange = false; bool ychange = false; Hadjustment.Upper = System.Math.Max (Allocation.Width, width); Vadjustment.Upper = System.Math.Max (Allocation.Height, height); if (scroll) { xchange = (int)(Hadjustment.Value) != x; ychange = (int)(Vadjustment.Value) != y; scroll = false; } if (IsRealized) BinWindow.FreezeUpdates (); if (xchange || ychange) { if (IsRealized) BinWindow.MoveResize (-x, -y, (int)(Hadjustment.Upper), (int)(Vadjustment.Upper)); Vadjustment.Value = y; Hadjustment.Value = x; } if (scroll) scroll = false; if (this.Width != Allocation.Width || this.Height != Allocation.Height) SetSize ((uint)Allocation.Width, (uint)height); if (xchange || ychange) { Vadjustment.ChangeValue (); Hadjustment.ChangeValue (); } if (IsRealized) { BinWindow.ThawUpdates (); BinWindow.ProcessUpdates (true); } } int ThrobExpansion (int cell, bool selected) { int expansion = 0; if (cell == throb_cell) { double t = throb_state / (double) (throb_state_max - 1); double s; if (selected) s = Math.Cos (-2 * Math.PI * t); else s = 1 - Math.Cos (-2 * Math.PI * t); expansion = (int) (SELECTION_THICKNESS * s); } else if (selected) { expansion = SELECTION_THICKNESS; } return expansion; } System.Collections.Hashtable date_layouts = new Hashtable (); // FIXME Cache the GCs? private void DrawCell (int thumbnail_num, Gdk.Rectangle area) { Gdk.Rectangle bounds = CellBounds (thumbnail_num); if (!bounds.Intersect (area, out area)) return; FSpot.IBrowsableItem photo = collection [thumbnail_num]; string thumbnail_path = FSpot.ThumbnailGenerator.ThumbnailPath (photo.DefaultVersionUri); FSpot.PixbufCache.CacheEntry entry = cache.Lookup (thumbnail_path); if (entry == null) cache.Request (thumbnail_path, thumbnail_num, ThumbnailWidth, ThumbnailHeight); else entry.Data = thumbnail_num; bool selected = selection.Contains (thumbnail_num); StateType cell_state = selected ? (HasFocus ? StateType.Selected : StateType.Active) : State; if (cell_state != State) Style.PaintBox (Style, BinWindow, cell_state, ShadowType.Out, area, this, "IconView", bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); Gdk.Rectangle focus = Gdk.Rectangle.Inflate (bounds, -3, -3); if (HasFocus && thumbnail_num == FocusCell) { Style.PaintFocus(Style, BinWindow, cell_state, area, this, null, focus.X, focus.Y, focus.Width, focus.Height); } Gdk.Rectangle region = Gdk.Rectangle.Zero; Gdk.Rectangle image_bounds = Gdk.Rectangle.Inflate (bounds, -cell_border_width, -cell_border_width); int expansion = ThrobExpansion (thumbnail_num, selected); Gdk.Pixbuf thumbnail = null; if (entry != null) thumbnail = entry.ShallowCopyPixbuf (); if (Gdk.Rectangle.Inflate (image_bounds, expansion + 1, expansion + 1).Intersect (area, out image_bounds) && thumbnail != null) { PixbufUtils.Fit (thumbnail, ThumbnailWidth, ThumbnailHeight, true, out region.Width, out region.Height); region.X = (int) (bounds.X + (bounds.Width - region.Width) / 2); region.Y = (int) bounds.Y + ThumbnailHeight - region.Height + cell_border_width; if (region.Width != thumbnail.Width && region.Height != thumbnail.Height) cache.Reload (entry, thumbnail_num, thumbnail.Width, thumbnail.Height); region = Gdk.Rectangle.Inflate (region, expansion, expansion); Pixbuf temp_thumbnail; region.Width = System.Math.Max (1, region.Width); region.Height = System.Math.Max (1, region.Height); if (region.Width != thumbnail.Width && region.Height != thumbnail.Height) { if (region.Width < thumbnail.Width && region.Height < thumbnail.Height) { /* temp_thumbnail = PixbufUtils.ScaleDown (thumbnail, region.Width, region.Height); */ temp_thumbnail = thumbnail.ScaleSimple (region.Width, region.Height, InterpType.Bilinear); lock (entry) { if (entry.Reload && expansion == 0 && !entry.IsDisposed) { entry.SetPixbufExtended (PixbufUtils.ShallowCopy (temp_thumbnail), false); entry.Reload = true; } } } else { temp_thumbnail = thumbnail.ScaleSimple (region.Width, region.Height, InterpType.Bilinear); } PixbufUtils.CopyThumbnailOptions (thumbnail, temp_thumbnail); } else temp_thumbnail = thumbnail; // FIXME There seems to be a rounding issue between the // scaled thumbnail sizes, we avoid this for now by using // the actual thumnail sizes here. region.Width = temp_thumbnail.Width; region.Height = temp_thumbnail.Height; Gdk.Rectangle draw = Gdk.Rectangle.Inflate (region, 1, 1); if (!temp_thumbnail.HasAlpha) Style.PaintShadow (Style, BinWindow, cell_state, ShadowType.Out, area, this, "IconView", draw.X, draw.Y, draw.Width, draw.Height); if (region.Intersect (area, out draw)) { temp_thumbnail.RenderToDrawable (BinWindow, Style.WhiteGC, draw.X - region.X, draw.Y - region.Y, draw.X, draw.Y, draw.Width, draw.Height, RgbDither.None, draw.X, draw.Y); } if (temp_thumbnail != thumbnail) { temp_thumbnail.Dispose (); } } if (thumbnail != null) { thumbnail.Dispose (); } Gdk.Rectangle layout_bounds = Gdk.Rectangle.Zero; if (DisplayDates) { string date; if (cell_width > 200) { date = photo.Time.ToLocalTime ().ToString (); } else { date = photo.Time.ToLocalTime ().ToShortDateString (); } Pango.Layout layout = (Pango.Layout)date_layouts [date]; if (layout == null) { layout = new Pango.Layout (this.PangoContext); layout.SetText (date); date_layouts [date] = layout; } layout.GetPixelSize (out layout_bounds.Width, out layout_bounds.Height); layout_bounds.Y = bounds.Y + bounds.Height - cell_border_width - layout_bounds.Height + tag_icon_vspacing; layout_bounds.X = bounds.X + (bounds.Width - layout_bounds.Width) / 2; if (DisplayTags) layout_bounds.Y -= tag_icon_size; if (DisplayFilenames) { Pango.FontMetrics metrics = this.PangoContext.GetMetrics (this.Style.FontDescription, Pango.Language.FromString ("en_US")); layout_bounds.Y -= PangoPixels (metrics.Ascent + metrics.Descent); } if (layout_bounds.Intersect (area, out region)) { Style.PaintLayout (Style, BinWindow, cell_state, true, area, this, "IconView", layout_bounds.X, layout_bounds.Y, layout); } } if (DisplayFilenames) { string filename = System.IO.Path.GetFileName (photo.DefaultVersionUri.LocalPath); Pango.Layout layout = new Pango.Layout (this.PangoContext); layout.SetText (filename); layout.GetPixelSize (out layout_bounds.Width, out layout_bounds.Height); layout_bounds.Y = bounds.Y + bounds.Height - cell_border_width - layout_bounds.Height + tag_icon_vspacing; layout_bounds.X = bounds.X + (bounds.Width - layout_bounds.Width) / 2; if (DisplayTags) layout_bounds.Y -= tag_icon_size; if (layout_bounds.Intersect (area, out region)) { Style.PaintLayout (Style, BinWindow, cell_state, true, area, this, "IconView", layout_bounds.X, layout_bounds.Y, layout); } } if (DisplayTags) { Tag [] tags = photo.Tags; Gdk.Rectangle tag_bounds; tag_bounds.X = bounds.X + (bounds.Width + tag_icon_hspacing - tags.Length * (tag_icon_size + tag_icon_hspacing)) / 2; tag_bounds.Y = bounds.Y + bounds.Height - cell_border_width - tag_icon_size + tag_icon_vspacing; tag_bounds.Width = tag_icon_size; tag_bounds.Height = tag_icon_size; foreach (Tag t in tags) { if (t == null) continue; Pixbuf icon = t.Icon; Tag tag_iter = t.Category; while (icon == null && tag_iter != Core.Database.Tags.RootCategory && tag_iter != null) { icon = tag_iter.Icon; tag_iter = tag_iter.Category; } if (icon == null) continue; if (tag_bounds.Intersect (area, out region)) { Pixbuf scaled_icon; if (icon.Width == tag_bounds.Width) { scaled_icon = icon; } else { scaled_icon = icon.ScaleSimple (tag_bounds.Width, tag_bounds.Height, InterpType.Bilinear); } scaled_icon.RenderToDrawable (BinWindow, Style.WhiteGC, region.X - tag_bounds.X, region.Y - tag_bounds.Y, region.X, region.Y, region.Width, region.Height, RgbDither.None, region.X, region.Y); if (scaled_icon != icon) { scaled_icon.Dispose (); } } tag_bounds.X += tag_bounds.Width + tag_icon_hspacing; } } } private void DrawAllCells (Gdk.Rectangle area) { if (cell_width == 0 || cell_height == 0) return; int start_cell_column = Math.Max ((area.X - BORDER_SIZE) / cell_width, 0); int start_cell_row = Math.Max ((area.Y - BORDER_SIZE) / cell_height, 0); int start_cell_num = start_cell_column + start_cell_row * cells_per_row; int start_cell_x, cell_y; GetCellPosition (start_cell_num, out start_cell_x, out cell_y); int end_cell_column = Math.Max ((area.X + area.Width - BORDER_SIZE) / cell_width, 0); int end_cell_row = Math.Max ((area.Y + area.Height - BORDER_SIZE) / cell_height, 0); int num_rows = end_cell_row - start_cell_row + 1; int num_cols = Math.Min (end_cell_column - start_cell_column + 1, cells_per_row - start_cell_column); int i, cell_num; //Preload (area, false); for (i = 0, cell_num = start_cell_num; i < num_rows && cell_num < collection.Count; i ++) { int cell_x = start_cell_x; //Console.WriteLine ("Drawing row {0}", start_cell_row + i); for (int j = 0; j < num_cols && cell_num + j < collection.Count; j ++) { DrawCell (cell_num + j, area); cell_x += cell_width; } cell_y += cell_height; cell_num += cells_per_row; } } private void GetCellPosition (int cell_num, out int x, out int y) { if (cells_per_row == 0) { x = 0; y = 0; return; } int row = cell_num / cells_per_row; int col = cell_num % cells_per_row; x = col * cell_width + BORDER_SIZE; y = row * cell_height + BORDER_SIZE; } // Scrolling. We do this in an idle loop so we can catch up if the user scrolls quickly. private void Scroll () { int ystep = (int)(Vadjustment.Value - y_offset); int xstep = (int)(Hadjustment.Value - x_offset); if (xstep > 0) xstep = Math.Max (xstep, Allocation.Width); else xstep = Math.Min (xstep, -Allocation.Width); if (ystep > 0) ystep = Math.Max (ystep, Allocation.Height); else ystep = Math.Min (ystep, -Allocation.Height); Gdk.Rectangle area; Gdk.Region offscreen = new Gdk.Region (); /* System.Console.WriteLine ("step ({0}, {1}) allocation ({2},{3},{4},{5})", xstep, ystep, Hadjustment.Value, Vadjustment.Value, Allocation.Width, Allocation.Height); */ /* area = new Gdk.Rectangle (Math.Max ((int) (Hadjustment.Value + 4 * xstep), 0), Math.Max ((int) (Vadjustment.Value + 4 * ystep), 0), Allocation.Width, Allocation.Height); offscreen.UnionWithRect (area); area = new Gdk.Rectangle (Math.Max ((int) (Hadjustment.Value + 3 * xstep), 0), Math.Max ((int) (Vadjustment.Value + 3 * ystep), 0), Allocation.Width, Allocation.Height); offscreen.UnionWithRect (area); */ area = new Gdk.Rectangle (Math.Max ((int) (Hadjustment.Value + 2 * xstep), 0), Math.Max ((int) (Vadjustment.Value + 2 * ystep), 0), Allocation.Width, Allocation.Height); offscreen.UnionWithRect (area); area = new Gdk.Rectangle (Math.Max ((int) (Hadjustment.Value + xstep), 0), Math.Max ((int) (Vadjustment.Value + ystep), 0), Allocation.Width, Allocation.Height); offscreen.UnionWithRect (area); area = new Gdk.Rectangle ((int) Hadjustment.Value, (int) Vadjustment.Value, Allocation.Width, Allocation.Height); // always load the onscreen area last to make sure it // is first in the loading Gdk.Region onscreen = Gdk.Region.Rectangle (area); offscreen.Subtract (onscreen); PreloadRegion (offscreen, ystep); Preload (area, false); y_offset = (int) Vadjustment.Value; x_offset = (int) Hadjustment.Value; } private void PreloadRegion (Gdk.Region region, int step) { Gdk.Rectangle [] rects = region.GetRectangles (); if (step < 0) System.Array.Reverse (rects); foreach (Gdk.Rectangle preload in rects) { Preload (preload, false); } } private void Preload (Gdk.Rectangle area, bool back) { if (cells_per_row ==0) return; int start_cell_column = Math.Max ((area.X - BORDER_SIZE) / cell_width, 0); int start_cell_row = Math.Max ((area.Y - BORDER_SIZE) / cell_height, 0); int start_cell_num = start_cell_column + start_cell_row * cells_per_row; int end_cell_column = Math.Max ((area.X + area.Width - BORDER_SIZE) / cell_width, 0); int end_cell_row = Math.Max ((area.Y + area.Height - BORDER_SIZE) / cell_height, 0); int i; FSpot.IBrowsableItem photo; FSpot.PixbufCache.CacheEntry entry; string thumbnail_path; // Preload the cache with images aroud the expose area // FIXME the preload need to be tuned to the Cache size but this is a resonable start int cols = end_cell_column - start_cell_column + 1; int rows = end_cell_row - start_cell_row + 1; int len = rows * cols; int scell = start_cell_num; int ecell = scell + len; if (scell > collection.Count - len) { ecell = collection.Count; scell = System.Math.Max (0, scell - len); } else ecell = scell + len; int mid = (ecell - scell) / 2; for (i = 0; i < mid; i++) { int cell = back ? ecell - i - 1 : scell + mid + i; photo = collection [cell]; thumbnail_path = FSpot.ThumbnailGenerator.ThumbnailPath (photo.DefaultVersionUri); entry = cache.Lookup (thumbnail_path); if (entry == null) cache.Request (thumbnail_path, cell, ThumbnailWidth, ThumbnailHeight); cell = back ? scell + i : scell + mid - i - 1; photo = collection [cell]; thumbnail_path = FSpot.ThumbnailGenerator.ThumbnailPath (photo.DefaultVersionUri); entry = cache.Lookup (thumbnail_path); if (entry == null) cache.Request (thumbnail_path, cell, ThumbnailWidth, ThumbnailHeight); } } // // The throb interface // private uint throb_timer_id; private int throb_cell = -1; private int throb_state; private const int throb_state_max = 40; public void Throb (int cell_num) { throb_state = 0; throb_cell = cell_num; if (throb_timer_id == 0) throb_timer_id = GLib.Timeout.Add ((39000/throb_state_max)/100, new GLib.TimeoutHandler (HandleThrobTimer)); InvalidateCell (cell_num); } private void CancelThrob () { if (throb_timer_id != 0) GLib.Source.Remove (throb_timer_id); } private bool HandleThrobTimer () { InvalidateCell (throb_cell); if (throb_state++ < throb_state_max) { return true; } else { throb_cell = -1; throb_timer_id = 0; return false; } } public void ScrollTo (int cell_num) { ScrollTo (cell_num, true); } public void ScrollTo (int cell_num, bool center) { if (!IsRealized) return; Adjustment adjustment = Vadjustment; int x; int y; GetCellPosition (cell_num, out x, out y); if (y + cell_height > adjustment.Upper) UpdateLayout (); if (center) adjustment.Value = y + cell_height / 2 - adjustment.PageSize / 2; else adjustment.Value = y; adjustment.ChangeValue (); } public void ZoomIn () { ThumbnailWidth = (int) (ThumbnailWidth * ZOOM_FACTOR); } public void ZoomOut () { ThumbnailWidth = (int) (ThumbnailWidth / ZOOM_FACTOR); } // Event handlers. [GLib.ConnectBefore] private void HandleAdjustmentValueChanged (object sender, EventArgs args) { Scroll (); } private void HandlePixbufLoaded (FSpot.PixbufCache cache, FSpot.PixbufCache.CacheEntry entry) { Gdk.Pixbuf result = entry.ShallowCopyPixbuf (); int order = (int) entry.Data; if (order >= 0 && order < collection.Count) { System.Uri uri = collection [order].DefaultVersionUri; if (result == null && !System.IO.File.Exists (FSpot.ThumbnailGenerator.ThumbnailPath (uri))) FSpot.ThumbnailGenerator.Default.Request (uri, 0, 256, 256); if (result == null) return; if (!FSpot.PhotoLoader.ThumbnailIsValid (uri, result)) FSpot.ThumbnailGenerator.Default.Request (uri, 0, 256, 256); } if (result == null) return; // We have to do the scaling here rather than on load because we need to preserve the // Pixbuf option iformation to verify the thumbnail validity later int width, height; PixbufUtils.Fit (result, ThumbnailWidth, ThumbnailHeight, false, out width, out height); if (result.Width > width && result.Height > height) { // System.Console.WriteLine ("scaling"); Gdk.Pixbuf temp = PixbufUtils.ScaleDown (result, width, height); result.Dispose (); result = temp; } else if (result.Width < ThumbnailWidth && result.Height < ThumbnailHeight) { // FIXME this is a workaround to handle images whose actual size is smaller than // the thumbnail size, it needs to be fixed at a different level. Gdk.Pixbuf temp = new Gdk.Pixbuf (Gdk.Colorspace.Rgb, true, 8, ThumbnailWidth, ThumbnailHeight); temp.Fill (0x00000000); result.CopyArea (0, 0, result.Width, result.Height, temp, (temp.Width - result.Width)/ 2, temp.Height - result.Height); result.Dispose (); result = temp; } cache.Update (entry, result); InvalidateCell (order); } public Gdk.Rectangle CellBounds (int cell) { Rectangle bounds; GetCellPosition (cell, out bounds.X, out bounds.Y); bounds.Width = cell_width; bounds.Height = cell_height; return bounds; } public void InvalidateCell (int order) { Rectangle cell_area = CellBounds (order); // FIXME where are we computing the bounds incorrectly cell_area.Width -= 1; cell_area.Height -= 1; Gdk.Rectangle visible = new Gdk.Rectangle ((int)Hadjustment.Value, (int)Vadjustment.Value, Allocation.Width, Allocation.Height); if (BinWindow != null && cell_area.Intersect (visible, out cell_area)) BinWindow.InvalidateRect (cell_area, false); } private void HandleScrollAdjustmentsSet (object sender, ScrollAdjustmentsSetArgs args) { if (args.Vadjustment != null) args.Vadjustment.ValueChanged += new EventHandler (HandleAdjustmentValueChanged); } private void HandleScrollEvent(object sender, ScrollEventArgs args) { // Activated only by Control + ScrollWheelUp/ScrollWheelDown if (ModifierType.ControlMask != (args.Event.State & ModifierType.ControlMask)) return; if (args.Event.Direction == ScrollDirection.Up) { ZoomIn (); // stop event from propagating. args.RetVal = true; } else if (args.Event.Direction == ScrollDirection.Down ) { ZoomOut (); args.RetVal = true; } } private void SetColors () { if (IsRealized) { BinWindow.Background = Style.BaseColors [(int)State]; GdkWindow.Background = Style.BaseColors [(int)State]; } } protected override void OnRealized () { base.OnRealized (); SetColors (); } protected override void OnStateChanged (StateType previous) { base.OnStateChanged (previous); SetColors (); } protected override void OnStyleSet (Style previous) { base.OnStyleSet (previous); SetColors (); } protected override void OnSizeAllocated (Gdk.Rectangle allocation) { scroll_value = (Vadjustment.Value)/ (Vadjustment.Upper); scroll = !suppress_scroll; suppress_scroll = false; UpdateLayout (allocation); base.OnSizeAllocated (allocation); } protected override bool OnExposeEvent (Gdk.EventExpose args) { foreach (Rectangle area in args.Region.GetRectangles ()) { DrawAllCells (area); } return base.OnExposeEvent (args); } private void HandleButtonPressEvent (object obj, ButtonPressEventArgs args) { int cell_num = CellAtPosition ((int) args.Event.X, (int) args.Event.Y, false); args.RetVal = true; if (cell_num < 0) { args.RetVal = false; selection.Clear (); return; } switch (args.Event.Type) { case EventType.TwoButtonPress: if (args.Event.Button != 1 || (args.Event.State & (ModifierType.ControlMask | ModifierType.ShiftMask)) != 0) return; if (DoubleClicked != null) DoubleClicked (this, cell_num); return; case EventType.ButtonPress: GrabFocus (); if ((args.Event.State & ModifierType.ControlMask) != 0) { ToggleCell (cell_num); } else if ((args.Event.State & ModifierType.ShiftMask) != 0) { selection.Add (FocusCell, cell_num); } else if (!selection.Contains (cell_num)) { selection.Clear (); selection.Add (cell_num); } if (args.Event.Button == 3){ ContextMenu (args, cell_num); return; } if (args.Event.Button != 1) return; FocusCell = cell_num; return; default: args.RetVal = false; return; } } protected virtual void ContextMenu (ButtonPressEventArgs args, int cell_num) { } private void HandleButtonReleaseEvent (object sender, ButtonReleaseEventArgs args) { int cell_num = CellAtPosition ((int) args.Event.X, (int) args.Event.Y); args.RetVal = true; if (cell_num < 0) { args.RetVal = false; return; } switch (args.Event.Type) { case EventType.ButtonRelease: if ((args.Event.State & ModifierType.ControlMask) == 0 && (args.Event.State & ModifierType.ShiftMask ) == 0 && (selection.Count > 1)) { selection.Clear (); selection.Add (FocusCell); } break; default: args.RetVal = false; break; } } private void HandleKeyPressEvent (object sender, KeyPressEventArgs args) { int focus_old; args.RetVal = true; bool shift = ModifierType.ShiftMask == (args.Event.State & ModifierType.ShiftMask); bool control = ModifierType.ControlMask == (args.Event.State & ModifierType.ControlMask); focus_old = FocusCell; switch (args.Event.Key) { case Gdk.Key.Down: case Gdk.Key.J: case Gdk.Key.j: FocusCell += cells_per_row; break; case Gdk.Key.Left: case Gdk.Key.H: case Gdk.Key.h: if (control && shift) FocusCell -= FocusCell % cells_per_row; else FocusCell--; break; case Gdk.Key.Right: case Gdk.Key.L: case Gdk.Key.l: if (control && shift) FocusCell += cells_per_row - (FocusCell % cells_per_row) - 1; else FocusCell++; break; case Gdk.Key.Up: case Gdk.Key.K: case Gdk.Key.k: FocusCell -= cells_per_row; break; case Gdk.Key.Home: FocusCell = 0; break; case Gdk.Key.End: FocusCell = collection.Count - 1; break; case Gdk.Key.space: ToggleCell (FocusCell); break; case Gdk.Key.Return: if (DoubleClicked == null) break; DoubleClicked (this, FocusCell); break; default: args.RetVal = false; return; } if (FocusCell == focus_old) { args.RetVal = false; return; } if (shift) { if (focus_old != FocusCell && selection.Contains (focus_old) && selection.Contains (FocusCell)) selection.Remove (FocusCell, focus_old); else selection.Add (focus_old, FocusCell); } else if (!control) { selection.Clear (); selection.Add (FocusCell); } ScrollTo (FocusCell); } private void HandleDestroyed (object sender, System.EventArgs args) { cache.OnPixbufLoaded -= HandlePixbufLoaded; CancelThrob (); } }