diff options
Diffstat (limited to 'src/nautilus-canvas-container.c')
-rw-r--r-- | src/nautilus-canvas-container.c | 6360 |
1 files changed, 6360 insertions, 0 deletions
diff --git a/src/nautilus-canvas-container.c b/src/nautilus-canvas-container.c new file mode 100644 index 0000000..71bcf19 --- /dev/null +++ b/src/nautilus-canvas-container.c @@ -0,0 +1,6360 @@ +/* nautilus-canvas-container.c - Canvas container widget. + * + * Copyright (C) 1999, 2000 Free Software Foundation + * Copyright (C) 2000, 2001 Eazel, Inc. + * Copyright (C) 2002, 2003 Red Hat, Inc. + * + * The Gnome Library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * The Gnome Library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with the Gnome Library; see the file COPYING.LIB. If not, + * see <http://www.gnu.org/licenses/>. + * + * Authors: Ettore Perazzoli <ettore@gnu.org>, + * Darin Adler <darin@bentspoon.com> + */ + +#include "nautilus-canvas-container.h" + +#include <atk/atkaction.h> +#include <eel/eel-art-extensions.h> +#include <eel/eel-gtk-extensions.h> +#include <eel/eel-vfs-extensions.h> +#include <gdk/gdkkeysyms.h> +#include <gdk/gdk.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <math.h> +#include <stdio.h> +#include <string.h> + +#define DEBUG_FLAG NAUTILUS_DEBUG_CANVAS_CONTAINER +#include "nautilus-debug.h" + +#include "nautilus-canvas-private.h" +#include "nautilus-global-preferences.h" +#include "nautilus-icon-info.h" +#include "nautilus-lib-self-check-functions.h" +#include "nautilus-selection-canvas-item.h" + +/* Interval for updating the rubberband selection, in milliseconds. */ +#define RUBBERBAND_TIMEOUT_INTERVAL 10 + +#define RUBBERBAND_SCROLL_THRESHOLD 5 + +/* Initial unpositioned icon value */ +#define ICON_UNPOSITIONED_VALUE -1 + +/* Timeout for making the icon currently selected for keyboard operation visible. + * If this is 0, you can get into trouble with extra scrolling after holding + * down the arrow key for awhile when there are many items. + */ +#define KEYBOARD_ICON_REVEAL_TIMEOUT 10 + +#define CONTEXT_MENU_TIMEOUT_INTERVAL 500 + +/* Maximum amount of milliseconds the mouse button is allowed to stay down + * and still be considered a click. + */ +#define MAX_CLICK_TIME 1500 + +/* Button assignments. */ +#define DRAG_BUTTON 1 +#define RUBBERBAND_BUTTON 1 +#define MIDDLE_BUTTON 2 +#define CONTEXTUAL_MENU_BUTTON 3 +#define DRAG_MENU_BUTTON 2 + +/* Maximum size (pixels) allowed for icons at the standard zoom level. */ +#define MINIMUM_IMAGE_SIZE 24 +#define MAXIMUM_IMAGE_SIZE 96 + +#define ICON_PAD_LEFT 4 +#define ICON_PAD_RIGHT 4 +#define ICON_PAD_TOP 4 +#define ICON_PAD_BOTTOM 4 + +#define CONTAINER_PAD_LEFT 4 +#define CONTAINER_PAD_RIGHT 4 +#define CONTAINER_PAD_TOP 4 +#define CONTAINER_PAD_BOTTOM 4 + +/* Width of a "grid unit". Canvas items will always take up one or more + * grid units, rounding up their size relative to the unit width. + * So with an 80px grid unit, a 100px canvas item would take two grid units, + * where a 76px canvas item would only take one. + * Canvas items are then centered in the extra available space. + * Keep in sync with MAX_TEXT_WIDTH at nautilus-canvas-item. + */ +#define SMALL_ICON_GRID_WIDTH 124 +#define STANDARD_ICON_GRID_WIDTH 112 +#define LARGE_ICON_GRID_WIDTH 106 +#define LARGER_ICON_GRID_WIDTH 128 + +/* Copied from NautilusCanvasContainer */ +#define NAUTILUS_CANVAS_CONTAINER_SEARCH_DIALOG_TIMEOUT 5 + +/* Copied from NautilusFile */ +#define UNDEFINED_TIME ((time_t) (-1)) + +enum +{ + ACTION_ACTIVATE, + ACTION_MENU, + LAST_ACTION +}; + +typedef struct +{ + GList *selection; + char *action_descriptions[LAST_ACTION]; +} NautilusCanvasContainerAccessiblePrivate; + +static GType nautilus_canvas_container_accessible_get_type (void); +static void preview_selected_items (NautilusCanvasContainer *container); +static void activate_selected_items (NautilusCanvasContainer *container); +static void activate_selected_items_alternate (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon); +static NautilusCanvasIcon *get_first_selected_icon (NautilusCanvasContainer *container); +static NautilusCanvasIcon *get_nth_selected_icon (NautilusCanvasContainer *container, + int index); +static gboolean has_multiple_selection (NautilusCanvasContainer *container); +static gboolean all_selected (NautilusCanvasContainer *container); +static gboolean has_selection (NautilusCanvasContainer *container); +static void icon_destroy (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon); +static gboolean finish_adding_new_icons (NautilusCanvasContainer *container); +static inline void icon_get_bounding_box (NautilusCanvasIcon *icon, + int *x1_return, + int *y1_return, + int *x2_return, + int *y2_return, + NautilusCanvasItemBoundsUsage usage); +static void handle_hadjustment_changed (GtkAdjustment *adjustment, + NautilusCanvasContainer *container); +static void handle_vadjustment_changed (GtkAdjustment *adjustment, + NautilusCanvasContainer *container); +static GList *nautilus_canvas_container_get_selected_icons (NautilusCanvasContainer *container); +static void nautilus_canvas_container_update_visible_icons (NautilusCanvasContainer *container); +static void reveal_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon); + +static void nautilus_canvas_container_set_rtl_positions (NautilusCanvasContainer *container); +static double get_mirror_x_position (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + double x); +static void text_ellipsis_limit_changed_container_callback (gpointer callback_data); + +static int compare_icons_horizontal (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b); + +static int compare_icons_vertical (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b); + +static void schedule_redo_layout (NautilusCanvasContainer *container); + +static const char *nautilus_canvas_container_accessible_action_names[] = +{ + "activate", + "menu", + NULL +}; + +static const char *nautilus_canvas_container_accessible_action_descriptions[] = +{ + "Activate selected items", + "Popup context menu", + NULL +}; + +G_DEFINE_TYPE (NautilusCanvasContainer, nautilus_canvas_container, EEL_TYPE_CANVAS); + +/* The NautilusCanvasContainer signals. */ +enum +{ + ACTIVATE, + ACTIVATE_ALTERNATE, + ACTIVATE_PREVIEWER, + BAND_SELECT_STARTED, + BAND_SELECT_ENDED, + BUTTON_PRESS, + CONTEXT_CLICK_BACKGROUND, + CONTEXT_CLICK_SELECTION, + MIDDLE_CLICK, + GET_CONTAINER_URI, + GET_ICON_URI, + GET_ICON_ACTIVATION_URI, + GET_ICON_DROP_TARGET_URI, + ICON_RENAME_STARTED, + ICON_RENAME_ENDED, + ICON_STRETCH_STARTED, + ICON_STRETCH_ENDED, + MOVE_COPY_ITEMS, + HANDLE_NETSCAPE_URL, + HANDLE_URI_LIST, + HANDLE_TEXT, + HANDLE_RAW, + HANDLE_HOVER, + SELECTION_CHANGED, + ICON_ADDED, + ICON_REMOVED, + CLEARED, + LAST_SIGNAL +}; + +typedef struct +{ + int **icon_grid; + int *grid_memory; + int num_rows; + int num_columns; + gboolean tight; +} PlacementGrid; + +static guint signals[LAST_SIGNAL]; + +/* Functions dealing with NautilusIcons. */ + +static void +icon_free (NautilusCanvasIcon *icon) +{ + /* Destroy this icon item; the parent will unref it. */ + eel_canvas_item_destroy (EEL_CANVAS_ITEM (icon->item)); + g_free (icon); +} + +static gboolean +icon_is_positioned (const NautilusCanvasIcon *icon) +{ + return icon->x != ICON_UNPOSITIONED_VALUE && icon->y != ICON_UNPOSITIONED_VALUE; +} + + +/* x, y are the top-left coordinates of the icon. */ +static void +icon_set_position (NautilusCanvasIcon *icon, + double x, + double y) +{ + if (icon->x == x && icon->y == y) + { + return; + } + + if (icon->x == ICON_UNPOSITIONED_VALUE) + { + icon->x = 0; + } + if (icon->y == ICON_UNPOSITIONED_VALUE) + { + icon->y = 0; + } + + eel_canvas_item_move (EEL_CANVAS_ITEM (icon->item), + x - icon->x, + y - icon->y); + + icon->x = x; + icon->y = y; +} + +static guint +nautilus_canvas_container_get_grid_size_for_zoom_level (NautilusCanvasZoomLevel zoom_level) +{ + switch (zoom_level) + { + case NAUTILUS_CANVAS_ZOOM_LEVEL_SMALL: + { + return SMALL_ICON_GRID_WIDTH; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_STANDARD: + { + return STANDARD_ICON_GRID_WIDTH; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_LARGE: + { + return LARGE_ICON_GRID_WIDTH; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_LARGER: + { + return LARGER_ICON_GRID_WIDTH; + } + break; + + default: + { + g_return_val_if_reached (STANDARD_ICON_GRID_WIDTH); + } + break; + } +} + +guint +nautilus_canvas_container_get_icon_size_for_zoom_level (NautilusCanvasZoomLevel zoom_level) +{ + switch (zoom_level) + { + case NAUTILUS_CANVAS_ZOOM_LEVEL_SMALL: + { + return NAUTILUS_CANVAS_ICON_SIZE_SMALL; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_STANDARD: + { + return NAUTILUS_CANVAS_ICON_SIZE_STANDARD; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_LARGE: + { + return NAUTILUS_CANVAS_ICON_SIZE_LARGE; + } + break; + + case NAUTILUS_CANVAS_ZOOM_LEVEL_LARGER: + { + return NAUTILUS_CANVAS_ICON_SIZE_LARGER; + } + break; + + default: + { + g_return_val_if_reached (NAUTILUS_CANVAS_ICON_SIZE_STANDARD); + } + break; + } +} + +static void +icon_get_size (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + guint *size) +{ + if (size != NULL) + { + *size = MAX (nautilus_canvas_container_get_icon_size_for_zoom_level (container->details->zoom_level), + NAUTILUS_CANVAS_ICON_SIZE_SMALL); + } +} + +static void +icon_raise (NautilusCanvasIcon *icon) +{ + EelCanvasItem *item, *band; + + item = EEL_CANVAS_ITEM (icon->item); + band = NAUTILUS_CANVAS_CONTAINER (item->canvas)->details->rubberband_info.selection_rectangle; + + eel_canvas_item_send_behind (item, band); +} + +static void +icon_toggle_selected (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + icon->is_selected = !icon->is_selected; + if (icon->is_selected) + { + container->details->selection = g_list_prepend (container->details->selection, icon->data); + container->details->selection_needs_resort = TRUE; + } + else + { + container->details->selection = g_list_remove (container->details->selection, icon->data); + } + + eel_canvas_item_set (EEL_CANVAS_ITEM (icon->item), + "highlighted_for_selection", (gboolean) icon->is_selected, + NULL); + + /* Raise each newly-selected icon to the front as it is selected. */ + if (icon->is_selected) + { + icon_raise (icon); + } +} + +/* Select an icon. Return TRUE if selection has changed. */ +static gboolean +icon_set_selected (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + gboolean select) +{ + if (select == icon->is_selected) + { + return FALSE; + } + + icon_toggle_selected (container, icon); + g_assert (select == icon->is_selected); + return TRUE; +} + +static inline void +icon_get_bounding_box (NautilusCanvasIcon *icon, + int *x1_return, + int *y1_return, + int *x2_return, + int *y2_return, + NautilusCanvasItemBoundsUsage usage) +{ + double x1, y1, x2, y2; + + if (usage == BOUNDS_USAGE_FOR_DISPLAY) + { + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (icon->item), + &x1, &y1, &x2, &y2); + } + else if (usage == BOUNDS_USAGE_FOR_LAYOUT) + { + nautilus_canvas_item_get_bounds_for_layout (icon->item, + &x1, &y1, &x2, &y2); + } + else if (usage == BOUNDS_USAGE_FOR_ENTIRE_ITEM) + { + nautilus_canvas_item_get_bounds_for_entire_item (icon->item, + &x1, &y1, &x2, &y2); + } + else + { + g_assert_not_reached (); + } + + if (x1_return != NULL) + { + *x1_return = x1; + } + + if (y1_return != NULL) + { + *y1_return = y1; + } + + if (x2_return != NULL) + { + *x2_return = x2; + } + + if (y2_return != NULL) + { + *y2_return = y2; + } +} + +/* Utility functions for NautilusCanvasContainer. */ + +gboolean +nautilus_canvas_container_scroll (NautilusCanvasContainer *container, + int delta_x, + int delta_y) +{ + GtkAdjustment *hadj, *vadj; + int old_h_value, old_v_value; + + hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container)); + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container)); + + /* Store the old ajustment values so we can tell if we + * ended up actually scrolling. We may not have in a case + * where the resulting value got pinned to the adjustment + * min or max. + */ + old_h_value = gtk_adjustment_get_value (hadj); + old_v_value = gtk_adjustment_get_value (vadj); + + gtk_adjustment_set_value (hadj, gtk_adjustment_get_value (hadj) + delta_x); + gtk_adjustment_set_value (vadj, gtk_adjustment_get_value (vadj) + delta_y); + + /* return TRUE if we did scroll */ + return gtk_adjustment_get_value (hadj) != old_h_value || gtk_adjustment_get_value (vadj) != old_v_value; +} + +static void +pending_icon_to_reveal_destroy_callback (NautilusCanvasItem *item, + NautilusCanvasContainer *container) +{ + g_assert (NAUTILUS_IS_CANVAS_CONTAINER (container)); + g_assert (container->details->pending_icon_to_reveal != NULL); + g_assert (container->details->pending_icon_to_reveal->item == item); + + container->details->pending_icon_to_reveal = NULL; +} + +static NautilusCanvasIcon * +get_pending_icon_to_reveal (NautilusCanvasContainer *container) +{ + return container->details->pending_icon_to_reveal; +} + +static void +set_pending_icon_to_reveal (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + NautilusCanvasIcon *old_icon; + + old_icon = container->details->pending_icon_to_reveal; + + if (icon == old_icon) + { + return; + } + + if (old_icon != NULL) + { + g_signal_handlers_disconnect_by_func + (old_icon->item, + G_CALLBACK (pending_icon_to_reveal_destroy_callback), + container); + } + + if (icon != NULL) + { + g_signal_connect (icon->item, "destroy", + G_CALLBACK (pending_icon_to_reveal_destroy_callback), + container); + } + + container->details->pending_icon_to_reveal = icon; +} + +static void +item_get_canvas_bounds (EelCanvasItem *item, + EelIRect *bounds) +{ + EelDRect world_rect; + + eel_canvas_item_get_bounds (item, + &world_rect.x0, + &world_rect.y0, + &world_rect.x1, + &world_rect.y1); + eel_canvas_item_i2w (item->parent, + &world_rect.x0, + &world_rect.y0); + eel_canvas_item_i2w (item->parent, + &world_rect.x1, + &world_rect.y1); + + world_rect.x0 -= ICON_PAD_LEFT + ICON_PAD_RIGHT; + world_rect.x1 += ICON_PAD_LEFT + ICON_PAD_RIGHT; + + world_rect.y0 -= ICON_PAD_TOP + ICON_PAD_BOTTOM; + world_rect.y1 += ICON_PAD_TOP + ICON_PAD_BOTTOM; + + eel_canvas_w2c (item->canvas, + world_rect.x0, + world_rect.y0, + &bounds->x0, + &bounds->y0); + eel_canvas_w2c (item->canvas, + world_rect.x1, + world_rect.y1, + &bounds->x1, + &bounds->y1); +} + +static void +icon_get_row_and_column_bounds (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + EelIRect *bounds) +{ + GList *p; + NautilusCanvasIcon *one_icon; + EelIRect one_bounds; + + item_get_canvas_bounds (EEL_CANVAS_ITEM (icon->item), bounds); + + for (p = container->details->icons; p != NULL; p = p->next) + { + one_icon = p->data; + + if (icon == one_icon) + { + continue; + } + + if (compare_icons_horizontal (container, icon, one_icon) == 0) + { + item_get_canvas_bounds (EEL_CANVAS_ITEM (one_icon->item), &one_bounds); + bounds->x0 = MIN (bounds->x0, one_bounds.x0); + bounds->x1 = MAX (bounds->x1, one_bounds.x1); + } + + if (compare_icons_vertical (container, icon, one_icon) == 0) + { + item_get_canvas_bounds (EEL_CANVAS_ITEM (one_icon->item), &one_bounds); + bounds->y0 = MIN (bounds->y0, one_bounds.y0); + bounds->y1 = MAX (bounds->y1, one_bounds.y1); + } + } +} + +static void +reveal_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + GtkAllocation allocation; + GtkAdjustment *hadj, *vadj; + EelIRect bounds; + + if (!icon_is_positioned (icon)) + { + set_pending_icon_to_reveal (container, icon); + return; + } + + set_pending_icon_to_reveal (container, NULL); + + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + + hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container)); + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container)); + + /* ensure that we reveal the entire row/column */ + icon_get_row_and_column_bounds (container, icon, &bounds); + + if (bounds.y0 < gtk_adjustment_get_value (vadj)) + { + gtk_adjustment_set_value (vadj, bounds.y0); + } + else if (bounds.y1 > gtk_adjustment_get_value (vadj) + allocation.height) + { + gtk_adjustment_set_value + (vadj, bounds.y1 - allocation.height); + } + + if (bounds.x0 < gtk_adjustment_get_value (hadj)) + { + gtk_adjustment_set_value (hadj, bounds.x0); + } + else if (bounds.x1 > gtk_adjustment_get_value (hadj) + allocation.width) + { + gtk_adjustment_set_value + (hadj, bounds.x1 - allocation.width); + } +} + +static void +process_pending_icon_to_reveal (NautilusCanvasContainer *container) +{ + NautilusCanvasIcon *pending_icon_to_reveal; + + pending_icon_to_reveal = get_pending_icon_to_reveal (container); + + if (pending_icon_to_reveal != NULL) + { + reveal_icon (container, pending_icon_to_reveal); + } +} + +static gboolean +keyboard_icon_reveal_timeout_callback (gpointer data) +{ + NautilusCanvasContainer *container; + NautilusCanvasIcon *icon; + + container = NAUTILUS_CANVAS_CONTAINER (data); + icon = container->details->keyboard_icon_to_reveal; + + g_assert (icon != NULL); + + /* Only reveal the icon if it's still the keyboard focus or if + * it's still selected. Someone originally thought we should + * cancel this reveal if the user manages to sneak a direct + * scroll in before the timeout fires, but we later realized + * this wouldn't actually be an improvement + * (see bugzilla.gnome.org 40612). + */ + if (icon == container->details->focus + || icon->is_selected) + { + reveal_icon (container, icon); + } + container->details->keyboard_icon_reveal_timer_id = 0; + + return FALSE; +} + +static void +unschedule_keyboard_icon_reveal (NautilusCanvasContainer *container) +{ + NautilusCanvasContainerDetails *details; + + details = container->details; + + if (details->keyboard_icon_reveal_timer_id != 0) + { + g_source_remove (details->keyboard_icon_reveal_timer_id); + } +} + +static void +schedule_keyboard_icon_reveal (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + NautilusCanvasContainerDetails *details; + + details = container->details; + + unschedule_keyboard_icon_reveal (container); + + details->keyboard_icon_to_reveal = icon; + details->keyboard_icon_reveal_timer_id + = g_timeout_add (KEYBOARD_ICON_REVEAL_TIMEOUT, + keyboard_icon_reveal_timeout_callback, + container); +} + +static void inline +emit_atk_object_notify_focused (NautilusCanvasIcon *icon, + gboolean focused) +{ + AtkObject *atk_object = atk_gobject_accessible_for_object (G_OBJECT (icon->item)); + atk_object_notify_state_change (atk_object, ATK_STATE_FOCUSED, focused); +} + +static void +clear_focus (NautilusCanvasContainer *container) +{ + if (container->details->focus != NULL) + { + if (container->details->keyboard_focus) + { + eel_canvas_item_set (EEL_CANVAS_ITEM (container->details->focus->item), + "highlighted_as_keyboard_focus", 0, + NULL); + } + else + { + emit_atk_object_notify_focused (container->details->focus, FALSE); + } + } + + container->details->focus = NULL; +} + +/* Set @icon as the icon currently focused for accessibility. */ +static void +set_focus (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + gboolean keyboard_focus) +{ + g_assert (icon != NULL); + + if (icon == container->details->focus) + { + return; + } + + clear_focus (container); + + container->details->focus = icon; + container->details->keyboard_focus = keyboard_focus; + + if (keyboard_focus) + { + eel_canvas_item_set (EEL_CANVAS_ITEM (container->details->focus->item), + "highlighted_as_keyboard_focus", 1, + NULL); + } + else + { + emit_atk_object_notify_focused (container->details->focus, TRUE); + } +} + +static void +set_keyboard_rubberband_start (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + container->details->keyboard_rubberband_start = icon; +} + +static void +clear_keyboard_rubberband_start (NautilusCanvasContainer *container) +{ + container->details->keyboard_rubberband_start = NULL; +} + +/* carbon-copy of eel_canvas_group_bounds(), but + * for NautilusCanvasContainerItems it returns the + * bounds for the “entire item”. + */ +static void +get_icon_bounds_for_canvas_bounds (EelCanvasGroup *group, + double *x1, + double *y1, + double *x2, + double *y2, + NautilusCanvasItemBoundsUsage usage) +{ + EelCanvasItem *child; + GList *list; + double tx1, ty1, tx2, ty2; + double minx, miny, maxx, maxy; + int set; + + /* Get the bounds of the first visible item */ + + child = NULL; /* Unnecessary but eliminates a warning. */ + + set = FALSE; + + for (list = group->item_list; list; list = list->next) + { + child = list->data; + + if (!NAUTILUS_IS_CANVAS_ITEM (child)) + { + continue; + } + + if (child->flags & EEL_CANVAS_ITEM_VISIBLE) + { + set = TRUE; + if (!NAUTILUS_IS_CANVAS_ITEM (child) || + usage == BOUNDS_USAGE_FOR_DISPLAY) + { + eel_canvas_item_get_bounds (child, &minx, &miny, &maxx, &maxy); + } + else if (usage == BOUNDS_USAGE_FOR_LAYOUT) + { + nautilus_canvas_item_get_bounds_for_layout (NAUTILUS_CANVAS_ITEM (child), + &minx, &miny, &maxx, &maxy); + } + else if (usage == BOUNDS_USAGE_FOR_ENTIRE_ITEM) + { + nautilus_canvas_item_get_bounds_for_entire_item (NAUTILUS_CANVAS_ITEM (child), + &minx, &miny, &maxx, &maxy); + } + else + { + g_assert_not_reached (); + } + break; + } + } + + /* If there were no visible items, return an empty bounding box */ + + if (!set) + { + *x1 = *y1 = *x2 = *y2 = 0.0; + return; + } + + /* Now we can grow the bounds using the rest of the items */ + + list = list->next; + + for (; list; list = list->next) + { + child = list->data; + + if (!NAUTILUS_IS_CANVAS_ITEM (child)) + { + continue; + } + + if (!(child->flags & EEL_CANVAS_ITEM_VISIBLE)) + { + continue; + } + + if (!NAUTILUS_IS_CANVAS_ITEM (child) || + usage == BOUNDS_USAGE_FOR_DISPLAY) + { + eel_canvas_item_get_bounds (child, &tx1, &ty1, &tx2, &ty2); + } + else if (usage == BOUNDS_USAGE_FOR_LAYOUT) + { + nautilus_canvas_item_get_bounds_for_layout (NAUTILUS_CANVAS_ITEM (child), + &tx1, &ty1, &tx2, &ty2); + } + else if (usage == BOUNDS_USAGE_FOR_ENTIRE_ITEM) + { + nautilus_canvas_item_get_bounds_for_entire_item (NAUTILUS_CANVAS_ITEM (child), + &tx1, &ty1, &tx2, &ty2); + } + else + { + g_assert_not_reached (); + } + + if (tx1 < minx) + { + minx = tx1; + } + + if (ty1 < miny) + { + miny = ty1; + } + + if (tx2 > maxx) + { + maxx = tx2; + } + + if (ty2 > maxy) + { + maxy = ty2; + } + } + + /* Make the bounds be relative to our parent's coordinate system */ + + if (EEL_CANVAS_ITEM (group)->parent) + { + minx += group->xpos; + miny += group->ypos; + maxx += group->xpos; + maxy += group->ypos; + } + + if (x1 != NULL) + { + *x1 = minx; + } + + if (y1 != NULL) + { + *y1 = miny; + } + + if (x2 != NULL) + { + *x2 = maxx; + } + + if (y2 != NULL) + { + *y2 = maxy; + } +} + +static void +get_all_icon_bounds (NautilusCanvasContainer *container, + double *x1, + double *y1, + double *x2, + double *y2, + NautilusCanvasItemBoundsUsage usage) +{ + /* FIXME bugzilla.gnome.org 42477: Do we have to do something about the rubberband + * here? Any other non-icon items? + */ + get_icon_bounds_for_canvas_bounds (EEL_CANVAS_GROUP (EEL_CANVAS (container)->root), + x1, y1, x2, y2, usage); +} + +void +nautilus_canvas_container_update_scroll_region (NautilusCanvasContainer *container) +{ + double x1, y1, x2, y2; + double pixels_per_unit; + GtkAdjustment *hadj, *vadj; + float step_increment; + GtkAllocation allocation; + + pixels_per_unit = EEL_CANVAS (container)->pixels_per_unit; + + get_all_icon_bounds (container, &x1, &y1, &x2, &y2, BOUNDS_USAGE_FOR_ENTIRE_ITEM); + + /* Add border at the "end"of the layout (i.e. after the icons), to + * ensure we get some space when scrolled to the end. + */ + y2 += ICON_PAD_BOTTOM + CONTAINER_PAD_BOTTOM; + + /* Auto-layout assumes a 0, 0 scroll origin and at least allocation->width. + * Then we lay out to the right or to the left, so + * x can be < 0 and > allocation */ + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + x1 = MIN (x1, 0); + x2 = MAX (x2, allocation.width / pixels_per_unit); + y1 = 0; + + x2 -= 1; + x2 = MAX (x1, x2); + + y2 -= 1; + y2 = MAX (y1, y2); + + eel_canvas_set_scroll_region (EEL_CANVAS (container), x1, y1, x2, y2); + + hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container)); + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container)); + + /* Scroll by 1/4 icon each time you click. */ + step_increment = nautilus_canvas_container_get_icon_size_for_zoom_level + (container->details->zoom_level) / 4; + if (gtk_adjustment_get_step_increment (hadj) != step_increment) + { + gtk_adjustment_set_step_increment (hadj, step_increment); + } + if (gtk_adjustment_get_step_increment (vadj) != step_increment) + { + gtk_adjustment_set_step_increment (vadj, step_increment); + } +} + +static void +cache_icon_positions (NautilusCanvasContainer *container) +{ + GList *l; + gint idx; + NautilusCanvasIcon *icon; + + for (l = container->details->icons, idx = 0; l != NULL; l = l->next) + { + icon = l->data; + icon->position = idx++; + } +} + +static int +compare_icons_data (gconstpointer a, + gconstpointer b, + gpointer canvas_container) +{ + NautilusCanvasContainerClass *klass; + NautilusCanvasIconData *data_a, *data_b; + + data_a = (NautilusCanvasIconData *) a; + data_b = (NautilusCanvasIconData *) b; + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (canvas_container); + + return klass->compare_icons (canvas_container, data_a, data_b); +} + +static int +compare_icons (gconstpointer a, + gconstpointer b, + gpointer canvas_container) +{ + NautilusCanvasContainerClass *klass; + const NautilusCanvasIcon *icon_a, *icon_b; + + icon_a = a; + icon_b = b; + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (canvas_container); + + return klass->compare_icons (canvas_container, icon_a->data, icon_b->data); +} + +static void +sort_selection (NautilusCanvasContainer *container) +{ + container->details->selection = g_list_sort_with_data (container->details->selection, + compare_icons_data, + container); + container->details->selection_needs_resort = FALSE; +} + +static void +sort_icons (NautilusCanvasContainer *container, + GList **icons) +{ + NautilusCanvasContainerClass *klass; + + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (container); + g_assert (klass->compare_icons != NULL); + + *icons = g_list_sort_with_data (*icons, compare_icons, container); +} + +static void +resort (NautilusCanvasContainer *container) +{ + sort_icons (container, &container->details->icons); + sort_selection (container); + cache_icon_positions (container); +} + +typedef struct +{ + double width; + double height; + double x_offset; + double y_offset; +} IconPositions; + +static void +lay_down_one_line (NautilusCanvasContainer *container, + GList *line_start, + GList *line_end, + double y, + double max_height, + GArray *positions, + gboolean whole_text) +{ + GList *p; + NautilusCanvasIcon *icon; + double x, ltr_icon_x, icon_x, y_offset; + IconPositions *position; + int i; + gboolean is_rtl; + + is_rtl = nautilus_canvas_container_is_layout_rtl (container); + + /* Lay out the icons along the baseline. */ + x = ICON_PAD_LEFT; + i = 0; + for (p = line_start; p != line_end; p = p->next) + { + icon = p->data; + + position = &g_array_index (positions, IconPositions, i++); + ltr_icon_x = x + position->x_offset; + icon_x = is_rtl ? get_mirror_x_position (container, icon, ltr_icon_x) : ltr_icon_x; + y_offset = position->y_offset; + + icon_set_position (icon, icon_x, y + y_offset); + nautilus_canvas_item_set_entire_text (icon->item, whole_text); + + icon->saved_ltr_x = is_rtl ? ltr_icon_x : icon->x; + + x += position->width; + } +} + +static void +lay_down_icons_horizontal (NautilusCanvasContainer *container, + GList *icons, + double start_y) +{ + GList *p, *line_start; + NautilusCanvasIcon *icon; + double canvas_width, y; + double available_width; + GArray *positions; + IconPositions *position; + EelDRect bounds; + EelDRect icon_bounds; + double max_height_above, max_height_below; + double height_above, height_below; + double line_width; + double min_grid_width; + double grid_width; + double num_columns; + double icon_width, icon_size; + int i; + GtkAllocation allocation; + + g_assert (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + /* We can't get the right allocation if the size hasn't been allocated yet */ + g_return_if_fail (container->details->has_been_allocated); + + if (icons == NULL) + { + return; + } + + positions = g_array_new (FALSE, FALSE, sizeof (IconPositions)); + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + + /* Lay out icons a line at a time. */ + canvas_width = CANVAS_WIDTH (container, allocation); + min_grid_width = nautilus_canvas_container_get_grid_size_for_zoom_level (container->details->zoom_level); + icon_size = nautilus_canvas_container_get_icon_size_for_zoom_level (container->details->zoom_level); + + /* Subtracting 1.0 adds some room for error to prevent the jitter due to + * the code not being able to decide how many columns should be there, as + * "double" is not perfectly precise and increasing the size of the the + * window by one pixel could well make it so that the space taken by the + * icons and the padding is actually greater than the canvas with by like + * 0.01, causing an entire column to be dropped unnecessarily. This fix is + * adapted from Nemo. + */ + available_width = MAX (1.0, canvas_width - ICON_PAD_LEFT - ICON_PAD_RIGHT - 1.0); + num_columns = MAX (1.0, floor (available_width / min_grid_width)); + + if (g_list_nth (icons, num_columns) != NULL) + { + grid_width = available_width / num_columns; + } + else + { + /* It does not look good when the icons jump around when new columns are + * added or removed to the grid while there is only one line. It does + * not look good either when the icons do not move at all when the + * window is resized. + * + * To do this, we first compute the maximum extra fraction we can add to + * the grid width. Adding this much, however, would simply distribute + * the icons evenly, which looks bad when there's a wide window with + * only a few icons. + * + * To fix this, we need to apply a function to the fraction which never + * makes it larger and instead makes its growth slow down quickly but + * smoothly as the window gets wider and wider. Here's the function used + * by this code: + * + * f(x) = ∜(x + 1) - 1 + * + * The +1 and -1 are there to skip the 0 to 1 part of ∜ where it makes + * the number larger. + */ + + double num_icons = MAX (1.0, g_list_length (icons)); + + double used_width = num_icons * min_grid_width; + double unused_width = available_width - used_width; + + double max_extra_fraction = (unused_width / num_icons) / min_grid_width; + double extra_fraction = pow (max_extra_fraction + 1.0, 1.0 / 4.0) - 1.0; + + grid_width = min_grid_width * (1 + extra_fraction); + } + + grid_width = MAX (min_grid_width, grid_width); + + line_width = 0; + line_start = icons; + y = start_y + CONTAINER_PAD_TOP; + i = 0; + + max_height_above = 0; + max_height_below = 0; + for (p = icons; p != NULL; p = p->next) + { + icon = p->data; + + /* Assume it's only one level hierarchy to avoid costly affine calculations */ + nautilus_canvas_item_get_bounds_for_layout (icon->item, + &bounds.x0, &bounds.y0, + &bounds.x1, &bounds.y1); + + /* Normalize the icon width to the grid unit. + * Use the icon size for this zoom level too in the calculation, since + * the actual bounds might be smaller - e.g. because we have a very + * narrow thumbnail. + */ + icon_width = ceil (MAX ((bounds.x1 - bounds.x0), icon_size) / grid_width) * grid_width; + + /* Calculate size above/below baseline */ + icon_bounds = nautilus_canvas_item_get_icon_rectangle (icon->item); + height_above = icon_bounds.y1 - bounds.y0; + height_below = bounds.y1 - icon_bounds.y1; + + /* If this icon doesn't fit, it's time to lay out the line that's queued up. */ + if (line_start != p && line_width + icon_width >= canvas_width) + { + /* Advance to the baseline. */ + y += ICON_PAD_TOP + max_height_above; + + lay_down_one_line (container, line_start, p, y, max_height_above, positions, FALSE); + + /* Advance to next line. */ + y += max_height_below + ICON_PAD_BOTTOM; + + line_width = 0; + line_start = p; + i = 0; + + max_height_above = height_above; + max_height_below = height_below; + } + else + { + if (height_above > max_height_above) + { + max_height_above = height_above; + } + if (height_below > max_height_below) + { + max_height_below = height_below; + } + } + + g_array_set_size (positions, i + 1); + position = &g_array_index (positions, IconPositions, i++); + position->width = icon_width; + position->height = icon_bounds.y1 - icon_bounds.y0; + + position->x_offset = (icon_width - (icon_bounds.x1 - icon_bounds.x0)) / 2; + position->y_offset = icon_bounds.y0 - icon_bounds.y1; + + /* Add this icon. */ + line_width += icon_width; + } + + /* Lay down that last line of icons. */ + if (line_start != NULL) + { + /* Advance to the baseline. */ + y += ICON_PAD_TOP + max_height_above; + + lay_down_one_line (container, line_start, NULL, y, max_height_above, positions, FALSE); + } + + g_array_free (positions, TRUE); +} + +static double +get_mirror_x_position (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + double x) +{ + EelDRect icon_bounds; + GtkAllocation allocation; + + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + icon_bounds = nautilus_canvas_item_get_icon_rectangle (icon->item); + + return CANVAS_WIDTH (container, allocation) - x - (icon_bounds.x1 - icon_bounds.x0); +} + +static void +nautilus_canvas_container_set_rtl_positions (NautilusCanvasContainer *container) +{ + GList *l; + NautilusCanvasIcon *icon; + double x; + + if (!container->details->icons) + { + return; + } + + for (l = container->details->icons; l != NULL; l = l->next) + { + icon = l->data; + x = get_mirror_x_position (container, icon, icon->saved_ltr_x); + icon_set_position (icon, x, icon->y); + } +} + +static void +lay_down_icons (NautilusCanvasContainer *container, + GList *icons, + double start_y) +{ + lay_down_icons_horizontal (container, icons, start_y); +} + +static void +redo_layout_internal (NautilusCanvasContainer *container) +{ + gboolean layout_possible; + + layout_possible = finish_adding_new_icons (container); + if (!layout_possible) + { + schedule_redo_layout (container); + return; + } + + if (container->details->needs_resort) + { + resort (container); + container->details->needs_resort = FALSE; + } + lay_down_icons (container, container->details->icons, 0); + + if (nautilus_canvas_container_is_layout_rtl (container)) + { + nautilus_canvas_container_set_rtl_positions (container); + } + + nautilus_canvas_container_update_scroll_region (container); + + process_pending_icon_to_reveal (container); + nautilus_canvas_container_update_visible_icons (container); +} + +static gboolean +redo_layout_callback (gpointer callback_data) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (callback_data); + redo_layout_internal (container); + container->details->idle_id = 0; + + return FALSE; +} + +static void +unschedule_redo_layout (NautilusCanvasContainer *container) +{ + if (container->details->idle_id != 0) + { + g_source_remove (container->details->idle_id); + container->details->idle_id = 0; + } +} + +static void +schedule_redo_layout (NautilusCanvasContainer *container) +{ + if (container->details->idle_id == 0 + && container->details->has_been_allocated) + { + container->details->idle_id = g_idle_add + (redo_layout_callback, container); + } +} + +static void +redo_layout (NautilusCanvasContainer *container) +{ + unschedule_redo_layout (container); + /* We can't lay out if the size hasn't been allocated yet; wait for it to + * be and then we will be called again from size_allocate () + */ + if (container->details->has_been_allocated) + { + redo_layout_internal (container); + } +} + +/* Container-level icon handling functions. */ + +static gboolean +button_event_modifies_selection (GdkEventButton *event) +{ + return (event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) != 0; +} + +/* invalidate the cached label sizes for all the icons */ +static void +invalidate_label_sizes (NautilusCanvasContainer *container) +{ + GList *p; + NautilusCanvasIcon *icon; + + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + nautilus_canvas_item_invalidate_label_size (icon->item); + } +} + +static gboolean +select_range (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon1, + NautilusCanvasIcon *icon2, + gboolean unselect_outside_range) +{ + gboolean selection_changed; + GList *p; + NautilusCanvasIcon *icon; + NautilusCanvasIcon *unmatched_icon; + gboolean select; + + selection_changed = FALSE; + + unmatched_icon = NULL; + select = FALSE; + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + if (unmatched_icon == NULL) + { + if (icon == icon1) + { + unmatched_icon = icon2; + select = TRUE; + } + else if (icon == icon2) + { + unmatched_icon = icon1; + select = TRUE; + } + } + + if (select || unselect_outside_range) + { + selection_changed |= icon_set_selected + (container, icon, select); + } + + if (unmatched_icon != NULL && icon == unmatched_icon) + { + select = FALSE; + } + } + return selection_changed; +} + + +static gboolean +select_one_unselect_others (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_to_select) +{ + gboolean selection_changed; + GList *p; + NautilusCanvasIcon *icon; + + selection_changed = FALSE; + + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + selection_changed |= icon_set_selected + (container, icon, icon == icon_to_select); + } + + if (selection_changed && icon_to_select != NULL) + { + reveal_icon (container, icon_to_select); + } + return selection_changed; +} + +static gboolean +unselect_all (NautilusCanvasContainer *container) +{ + return select_one_unselect_others (container, NULL); +} + +/* Implementation of rubberband selection. */ +static void +rubberband_select (NautilusCanvasContainer *container, + const EelDRect *current_rect) +{ + GList *p; + gboolean selection_changed, is_in, canvas_rect_calculated; + NautilusCanvasIcon *icon; + EelIRect canvas_rect; + EelCanvas *canvas; + + selection_changed = FALSE; + canvas_rect_calculated = FALSE; + + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + if (!canvas_rect_calculated) + { + /* Only do this calculation once, since all the canvas items + * we are interating are in the same coordinate space + */ + canvas = EEL_CANVAS_ITEM (icon->item)->canvas; + eel_canvas_w2c (canvas, + current_rect->x0, + current_rect->y0, + &canvas_rect.x0, + &canvas_rect.y0); + eel_canvas_w2c (canvas, + current_rect->x1, + current_rect->y1, + &canvas_rect.x1, + &canvas_rect.y1); + canvas_rect_calculated = TRUE; + } + + is_in = nautilus_canvas_item_hit_test_rectangle (icon->item, canvas_rect); + + selection_changed |= icon_set_selected + (container, icon, + is_in ^ icon->was_selected_before_rubberband); + } + + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +static int +rubberband_timeout_callback (gpointer data) +{ + NautilusCanvasContainer *container; + GtkWidget *widget; + NautilusCanvasRubberbandInfo *band_info; + int x, y; + double x1, y1, x2, y2; + double world_x, world_y; + int x_scroll, y_scroll; + int adj_x, adj_y; + gboolean adj_changed; + GtkAllocation allocation; + + EelDRect selection_rect; + + widget = GTK_WIDGET (data); + container = NAUTILUS_CANVAS_CONTAINER (data); + band_info = &container->details->rubberband_info; + + g_assert (band_info->timer_id != 0); + + adj_changed = FALSE; + gtk_widget_get_allocation (widget, &allocation); + + adj_x = gtk_adjustment_get_value (gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container))); + if (adj_x != band_info->last_adj_x) + { + band_info->last_adj_x = adj_x; + adj_changed = TRUE; + } + + adj_y = gtk_adjustment_get_value (gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container))); + if (adj_y != band_info->last_adj_y) + { + band_info->last_adj_y = adj_y; + adj_changed = TRUE; + } + + gdk_window_get_device_position (gtk_widget_get_window (widget), + band_info->device, + &x, &y, NULL); + + if (x < RUBBERBAND_SCROLL_THRESHOLD) + { + x_scroll = x - RUBBERBAND_SCROLL_THRESHOLD; + x = 0; + } + else if (x >= allocation.width - RUBBERBAND_SCROLL_THRESHOLD) + { + x_scroll = x - allocation.width + RUBBERBAND_SCROLL_THRESHOLD + 1; + x = allocation.width - 1; + } + else + { + x_scroll = 0; + } + + if (y < RUBBERBAND_SCROLL_THRESHOLD) + { + y_scroll = y - RUBBERBAND_SCROLL_THRESHOLD; + y = 0; + } + else if (y >= allocation.height - RUBBERBAND_SCROLL_THRESHOLD) + { + y_scroll = y - allocation.height + RUBBERBAND_SCROLL_THRESHOLD + 1; + y = allocation.height - 1; + } + else + { + y_scroll = 0; + } + + if (y_scroll == 0 && x_scroll == 0 + && (int) band_info->prev_x == x && (int) band_info->prev_y == y && !adj_changed) + { + return TRUE; + } + + nautilus_canvas_container_scroll (container, x_scroll, y_scroll); + + /* Remember to convert from widget to scrolled window coords */ + eel_canvas_window_to_world (EEL_CANVAS (container), + x + gtk_adjustment_get_value (gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container))), + y + gtk_adjustment_get_value (gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container))), + &world_x, &world_y); + + if (world_x < band_info->start_x) + { + x1 = world_x; + x2 = band_info->start_x; + } + else + { + x1 = band_info->start_x; + x2 = world_x; + } + + if (world_y < band_info->start_y) + { + y1 = world_y; + y2 = band_info->start_y; + } + else + { + y1 = band_info->start_y; + y2 = world_y; + } + + /* Don't let the area of the selection rectangle be empty. + * Aside from the fact that it would be funny when the rectangle disappears, + * this also works around a crash in libart that happens sometimes when a + * zero height rectangle is passed. + */ + x2 = MAX (x1 + 1, x2); + y2 = MAX (y1 + 1, y2); + + eel_canvas_item_set + (band_info->selection_rectangle, + "x1", x1, "y1", y1, + "x2", x2, "y2", y2, + NULL); + + selection_rect.x0 = x1; + selection_rect.y0 = y1; + selection_rect.x1 = x2; + selection_rect.y1 = y2; + + rubberband_select (container, + &selection_rect); + + band_info->prev_x = x; + band_info->prev_y = y; + + return TRUE; +} + +static void +stop_rubberbanding (NautilusCanvasContainer *container, + GdkEventButton *event); + +static void +start_rubberbanding (NautilusCanvasContainer *container, + GdkEventButton *event) +{ + AtkObject *accessible; + NautilusCanvasContainerDetails *details; + NautilusCanvasRubberbandInfo *band_info; + GList *p; + NautilusCanvasIcon *icon; + + details = container->details; + band_info = &details->rubberband_info; + + if (band_info->active) + { + g_debug ("Canceling active rubberband by device %s", gdk_device_get_name (band_info->device)); + stop_rubberbanding (container, NULL); + } + + g_signal_emit (container, + signals[BAND_SELECT_STARTED], 0); + + band_info->device = event->device; + + for (p = details->icons; p != NULL; p = p->next) + { + icon = p->data; + icon->was_selected_before_rubberband = icon->is_selected; + } + + eel_canvas_window_to_world + (EEL_CANVAS (container), event->x, event->y, + &band_info->start_x, &band_info->start_y); + + band_info->selection_rectangle = eel_canvas_item_new + (eel_canvas_root + (EEL_CANVAS (container)), + NAUTILUS_TYPE_SELECTION_CANVAS_ITEM, + "x1", band_info->start_x, + "y1", band_info->start_y, + "x2", band_info->start_x, + "y2", band_info->start_y, + NULL); + + accessible = atk_gobject_accessible_for_object + (G_OBJECT (band_info->selection_rectangle)); + atk_object_set_name (accessible, "selection"); + atk_object_set_description (accessible, _("The selection rectangle")); + + band_info->prev_x = event->x - gtk_adjustment_get_value (gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container))); + band_info->prev_y = event->y - gtk_adjustment_get_value (gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container))); + + band_info->active = TRUE; + + if (band_info->timer_id == 0) + { + band_info->timer_id = g_timeout_add + (RUBBERBAND_TIMEOUT_INTERVAL, + rubberband_timeout_callback, + container); + } + + eel_canvas_item_grab (band_info->selection_rectangle, + (GDK_POINTER_MOTION_MASK + | GDK_BUTTON_RELEASE_MASK + | GDK_SCROLL_MASK), + NULL, + (GdkEvent *) event); +} + +static void +stop_rubberbanding (NautilusCanvasContainer *container, + GdkEventButton *event) +{ + NautilusCanvasRubberbandInfo *band_info; + GList *icons; + gboolean enable_animation; + + band_info = &container->details->rubberband_info; + + if (event != NULL && event->device != band_info->device) + { + return; + } + + g_assert (band_info->timer_id != 0); + g_source_remove (band_info->timer_id); + band_info->timer_id = 0; + + band_info->active = FALSE; + + band_info->device = NULL; + + g_object_get (gtk_settings_get_default (), "gtk-enable-animations", &enable_animation, NULL); + + /* Destroy this canvas item; the parent will unref it. */ + eel_canvas_item_ungrab (band_info->selection_rectangle); + eel_canvas_item_lower_to_bottom (band_info->selection_rectangle); + eel_canvas_item_destroy (band_info->selection_rectangle); + band_info->selection_rectangle = NULL; + + /* if only one item has been selected, use it as range + * selection base (cf. handle_icon_button_press) */ + icons = nautilus_canvas_container_get_selected_icons (container); + if (g_list_length (icons) == 1) + { + container->details->range_selection_base_icon = icons->data; + } + g_list_free (icons); + + g_signal_emit (container, + signals[BAND_SELECT_ENDED], 0); +} + +/* Keyboard navigation. */ + +typedef gboolean (*IsBetterCanvasFunction) (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data); + +static NautilusCanvasIcon * +find_best_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + IsBetterCanvasFunction function, + void *data) +{ + GList *p; + NautilusCanvasIcon *best, *candidate; + + best = NULL; + for (p = container->details->icons; p != NULL; p = p->next) + { + candidate = p->data; + + if (candidate != start_icon) + { + if ((*function)(container, start_icon, best, candidate, data)) + { + best = candidate; + } + } + } + return best; +} + +static NautilusCanvasIcon * +find_best_selected_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + IsBetterCanvasFunction function, + void *data) +{ + GList *p; + NautilusCanvasIcon *best, *candidate; + + best = NULL; + for (p = container->details->icons; p != NULL; p = p->next) + { + candidate = p->data; + + if (candidate != start_icon && candidate->is_selected) + { + if ((*function)(container, start_icon, best, candidate, data)) + { + best = candidate; + } + } + } + return best; +} + +static int +compare_icons_by_uri (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b) +{ + char *uri_a, *uri_b; + int result; + + g_assert (NAUTILUS_IS_CANVAS_CONTAINER (container)); + g_assert (icon_a != NULL); + g_assert (icon_b != NULL); + g_assert (icon_a != icon_b); + + uri_a = nautilus_canvas_container_get_icon_uri (container, icon_a); + uri_b = nautilus_canvas_container_get_icon_uri (container, icon_b); + result = strcmp (uri_a, uri_b); + g_assert (result != 0); + g_free (uri_a); + g_free (uri_b); + + return result; +} + +static int +get_cmp_point_x (NautilusCanvasContainer *container, + EelDRect icon_rect) +{ + return (icon_rect.x0 + icon_rect.x1) / 2; +} + +static int +get_cmp_point_y (NautilusCanvasContainer *container, + EelDRect icon_rect) +{ + return icon_rect.y1; +} + + +static int +compare_icons_horizontal (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b) +{ + EelDRect world_rect; + int ax, bx; + + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_a->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &ax, + NULL); + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_b->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &bx, + NULL); + + if (ax < bx) + { + return -1; + } + if (ax > bx) + { + return +1; + } + return 0; +} + +static int +compare_icons_vertical (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b) +{ + EelDRect world_rect; + int ay, by; + + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_a->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + NULL, + &ay); + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_b->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + NULL, + &by); + + if (ay < by) + { + return -1; + } + if (ay > by) + { + return +1; + } + return 0; +} + +static int +compare_icons_horizontal_first (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b) +{ + EelDRect world_rect; + int ax, ay, bx, by; + + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_a->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &ax, + &ay); + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_b->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &bx, + &by); + + if (ax < bx) + { + return -1; + } + if (ax > bx) + { + return +1; + } + if (ay < by) + { + return -1; + } + if (ay > by) + { + return +1; + } + return compare_icons_by_uri (container, icon_a, icon_b); +} + +static int +compare_icons_vertical_first (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon_a, + NautilusCanvasIcon *icon_b) +{ + EelDRect world_rect; + int ax, ay, bx, by; + + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_a->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &ax, + &ay); + world_rect = nautilus_canvas_item_get_icon_rectangle (icon_b->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &bx, + &by); + + if (ay < by) + { + return -1; + } + if (ay > by) + { + return +1; + } + if (ax < bx) + { + return -1; + } + if (ax > bx) + { + return +1; + } + return compare_icons_by_uri (container, icon_a, icon_b); +} + +static gboolean +leftmost_in_top_row (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + if (best_so_far == NULL) + { + return TRUE; + } + return compare_icons_vertical_first (container, best_so_far, candidate) > 0; +} + +static gboolean +rightmost_in_top_row (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + if (best_so_far == NULL) + { + return TRUE; + } + return compare_icons_vertical (container, best_so_far, candidate) > 0; + return compare_icons_horizontal (container, best_so_far, candidate) < 0; +} + +static gboolean +rightmost_in_bottom_row (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + if (best_so_far == NULL) + { + return TRUE; + } + return compare_icons_vertical_first (container, best_so_far, candidate) < 0; +} + +static int +compare_with_start_row (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + EelCanvasItem *item; + + item = EEL_CANVAS_ITEM (icon->item); + + if (container->details->arrow_key_start_y < item->y1) + { + return -1; + } + if (container->details->arrow_key_start_y > item->y2) + { + return +1; + } + return 0; +} + +static int +compare_with_start_column (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + EelCanvasItem *item; + + item = EEL_CANVAS_ITEM (icon->item); + + if (container->details->arrow_key_start_x < item->x1) + { + return -1; + } + if (container->details->arrow_key_start_x > item->x2) + { + return +1; + } + return 0; +} + +static gboolean +same_row_right_side_leftmost (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* Candidates not on the start row do not qualify. */ + if (compare_with_start_row (container, candidate) != 0) + { + return FALSE; + } + + /* Candidates that are farther right lose out. */ + if (best_so_far != NULL) + { + if (compare_icons_horizontal_first (container, + best_so_far, + candidate) < 0) + { + return FALSE; + } + } + + /* Candidate to the left of the start do not qualify. */ + if (compare_icons_horizontal_first (container, + candidate, + start_icon) <= 0) + { + return FALSE; + } + + return TRUE; +} + +static gboolean +same_row_left_side_rightmost (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* Candidates not on the start row do not qualify. */ + if (compare_with_start_row (container, candidate) != 0) + { + return FALSE; + } + + /* Candidates that are farther left lose out. */ + if (best_so_far != NULL) + { + if (compare_icons_horizontal_first (container, + best_so_far, + candidate) > 0) + { + return FALSE; + } + } + + /* Candidate to the right of the start do not qualify. */ + if (compare_icons_horizontal_first (container, + candidate, + start_icon) >= 0) + { + return FALSE; + } + + return TRUE; +} + +static gboolean +next_row_leftmost (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* sort out icons that are not below the current row */ + if (compare_with_start_row (container, candidate) >= 0) + { + return FALSE; + } + + if (best_so_far != NULL) + { + if (compare_icons_vertical_first (container, + best_so_far, + candidate) > 0) + { + /* candidate is above best choice, but below the current row */ + return TRUE; + } + + if (compare_icons_horizontal_first (container, + best_so_far, + candidate) > 0) + { + return TRUE; + } + } + + return best_so_far == NULL; +} + +static gboolean +next_row_rightmost (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* sort out icons that are not below the current row */ + if (compare_with_start_row (container, candidate) >= 0) + { + return FALSE; + } + + if (best_so_far != NULL) + { + if (compare_icons_vertical_first (container, + best_so_far, + candidate) > 0) + { + /* candidate is above best choice, but below the current row */ + return TRUE; + } + + if (compare_icons_horizontal_first (container, + best_so_far, + candidate) < 0) + { + return TRUE; + } + } + + return best_so_far == NULL; +} + +static gboolean +previous_row_rightmost (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* sort out icons that are not above the current row */ + if (compare_with_start_row (container, candidate) <= 0) + { + return FALSE; + } + + if (best_so_far != NULL) + { + if (compare_icons_vertical_first (container, + best_so_far, + candidate) < 0) + { + /* candidate is below the best choice, but above the current row */ + return TRUE; + } + + if (compare_icons_horizontal_first (container, + best_so_far, + candidate) < 0) + { + return TRUE; + } + } + + return best_so_far == NULL; +} + +static gboolean +same_column_above_lowest (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* Candidates not on the start column do not qualify. */ + if (compare_with_start_column (container, candidate) != 0) + { + return FALSE; + } + + /* Candidates that are higher lose out. */ + if (best_so_far != NULL) + { + if (compare_icons_vertical_first (container, + best_so_far, + candidate) > 0) + { + return FALSE; + } + } + + /* Candidates below the start do not qualify. */ + if (compare_icons_vertical_first (container, + candidate, + start_icon) >= 0) + { + return FALSE; + } + + return TRUE; +} + +static gboolean +same_column_below_highest (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + /* Candidates not on the start column do not qualify. */ + if (compare_with_start_column (container, candidate) != 0) + { + return FALSE; + } + + /* Candidates that are lower lose out. */ + if (best_so_far != NULL) + { + if (compare_icons_vertical_first (container, + best_so_far, + candidate) < 0) + { + return FALSE; + } + } + + /* Candidates above the start do not qualify. */ + if (compare_icons_vertical_first (container, + candidate, + start_icon) <= 0) + { + return FALSE; + } + + return TRUE; +} + +static gboolean +closest_in_90_degrees (NautilusCanvasContainer *container, + NautilusCanvasIcon *start_icon, + NautilusCanvasIcon *best_so_far, + NautilusCanvasIcon *candidate, + void *data) +{ + EelDRect world_rect; + int x, y; + int dx, dy; + int dist; + int *best_dist; + + + world_rect = nautilus_canvas_item_get_icon_rectangle (candidate->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &x, + &y); + + dx = x - container->details->arrow_key_start_x; + dy = y - container->details->arrow_key_start_y; + + switch (container->details->arrow_key_direction) + { + case GTK_DIR_UP: + { + if (dy > 0 || + ABS (dx) > ABS (dy)) + { + return FALSE; + } + } + break; + + case GTK_DIR_DOWN: + { + if (dy < 0 || + ABS (dx) > ABS (dy)) + { + return FALSE; + } + } + break; + + case GTK_DIR_LEFT: + { + if (dx > 0 || + ABS (dy) > ABS (dx)) + { + return FALSE; + } + } + break; + + case GTK_DIR_RIGHT: + { + if (dx < 0 || + ABS (dy) > ABS (dx)) + { + return FALSE; + } + } + break; + + default: + { + g_assert_not_reached (); + } + break; + } + + dist = dx * dx + dy * dy; + best_dist = data; + + if (best_so_far == NULL) + { + *best_dist = dist; + return TRUE; + } + + if (dist < *best_dist) + { + *best_dist = dist; + return TRUE; + } + + return FALSE; +} + +static EelDRect +get_rubberband (NautilusCanvasIcon *icon1, + NautilusCanvasIcon *icon2) +{ + EelDRect rect1; + EelDRect rect2; + EelDRect ret; + + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (icon1->item), + &rect1.x0, &rect1.y0, + &rect1.x1, &rect1.y1); + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (icon2->item), + &rect2.x0, &rect2.y0, + &rect2.x1, &rect2.y1); + + eel_drect_union (&ret, &rect1, &rect2); + + return ret; +} + +static void +keyboard_move_to (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + NautilusCanvasIcon *from, + GdkEventKey *event) +{ + if (icon == NULL) + { + return; + } + + set_focus (container, icon, TRUE); + + if (event != NULL && + (event->state & GDK_CONTROL_MASK) != 0 && + (event->state & GDK_SHIFT_MASK) == 0) + { + clear_keyboard_rubberband_start (container); + } + else if (event != NULL && + (event->state & GDK_CONTROL_MASK) != 0 && + (event->state & GDK_SHIFT_MASK) != 0) + { + /* Do rubberband selection */ + EelDRect rect; + + if (from && !container->details->keyboard_rubberband_start) + { + set_keyboard_rubberband_start (container, from); + } + + if (icon && container->details->keyboard_rubberband_start) + { + rect = get_rubberband (container->details->keyboard_rubberband_start, + icon); + rubberband_select (container, &rect); + } + } + else if (event != NULL && + (event->state & GDK_CONTROL_MASK) == 0 && + (event->state & GDK_SHIFT_MASK) != 0) + { + /* Select range */ + NautilusCanvasIcon *start_icon; + + start_icon = container->details->range_selection_base_icon; + if (start_icon == NULL || !start_icon->is_selected) + { + start_icon = icon; + container->details->range_selection_base_icon = icon; + } + + if (select_range (container, start_icon, icon, TRUE)) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + else + { + /* Select icon. */ + clear_keyboard_rubberband_start (container); + + container->details->range_selection_base_icon = icon; + if (select_one_unselect_others (container, icon)) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + schedule_keyboard_icon_reveal (container, icon); +} + +static void +keyboard_home (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + NautilusCanvasIcon *from; + NautilusCanvasIcon *to; + + /* Home selects the first canvas. + * Control-Home sets the keyboard focus to the first canvas. + */ + + from = find_best_selected_icon (container, NULL, + rightmost_in_bottom_row, + NULL); + to = find_best_icon (container, NULL, leftmost_in_top_row, NULL); + + keyboard_move_to (container, to, from, event); +} + +static void +keyboard_end (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + NautilusCanvasIcon *to; + NautilusCanvasIcon *from; + + /* End selects the last canvas. + * Control-End sets the keyboard focus to the last canvas. + */ + from = find_best_selected_icon (container, NULL, + leftmost_in_top_row, + NULL); + to = find_best_icon (container, NULL, rightmost_in_bottom_row, NULL); + + keyboard_move_to (container, to, from, event); +} + +static void +record_arrow_key_start (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + GtkDirectionType direction) +{ + EelDRect world_rect; + + world_rect = nautilus_canvas_item_get_icon_rectangle (icon->item); + eel_canvas_w2c + (EEL_CANVAS (container), + get_cmp_point_x (container, world_rect), + get_cmp_point_y (container, world_rect), + &container->details->arrow_key_start_x, + &container->details->arrow_key_start_y); + container->details->arrow_key_direction = direction; +} + +static void +keyboard_arrow_key (NautilusCanvasContainer *container, + GdkEventKey *event, + GtkDirectionType direction, + IsBetterCanvasFunction better_start, + IsBetterCanvasFunction empty_start, + IsBetterCanvasFunction better_destination, + IsBetterCanvasFunction better_destination_fallback, + IsBetterCanvasFunction better_destination_fallback_fallback, + IsBetterCanvasFunction better_destination_manual) +{ + NautilusCanvasIcon *from; + NautilusCanvasIcon *to; + int data; + + /* Chose the icon to start with. + * If we have a keyboard focus, start with it. + * Otherwise, use the single selected icon. + * If there's multiple selection, use the icon farthest toward the end. + */ + + from = container->details->focus; + + if (from == NULL) + { + if (has_multiple_selection (container)) + { + if (all_selected (container)) + { + from = find_best_selected_icon + (container, NULL, + empty_start, NULL); + } + else + { + from = find_best_selected_icon + (container, NULL, + better_start, NULL); + } + } + else + { + from = get_first_selected_icon (container); + } + } + + /* If there's no icon, select the icon farthest toward the end. + * If there is an icon, select the next icon based on the arrow direction. + */ + if (from == NULL) + { + to = from = find_best_icon + (container, NULL, + empty_start, NULL); + } + else + { + record_arrow_key_start (container, from, direction); + + to = find_best_icon + (container, from, + better_destination, + &data); + + /* Wrap around to next/previous row/column */ + if (to == NULL && + better_destination_fallback != NULL) + { + to = find_best_icon + (container, from, + better_destination_fallback, + &data); + } + + /* With a layout like + * 1 2 3 + * 4 + * (horizontal layout) + * + * or + * + * 1 4 + * 2 + * 3 + * (vertical layout) + * + * * pressing down for any of 1,2,3 (horizontal layout) + * * pressing right for any of 1,2,3 (vertical layout) + * + * Should select 4. + */ + if (to == NULL && + better_destination_fallback_fallback != NULL) + { + to = find_best_icon + (container, from, + better_destination_fallback_fallback, + &data); + } + + if (to == NULL) + { + to = from; + } + } + + keyboard_move_to (container, to, from, event); +} + +static gboolean +is_rectangle_selection_event (GdkEventKey *event) +{ + return event != NULL && + (event->state & GDK_CONTROL_MASK) != 0 && + (event->state & GDK_SHIFT_MASK) != 0; +} + +static void +keyboard_right (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + IsBetterCanvasFunction fallback; + + fallback = NULL; + if (!is_rectangle_selection_event (event)) + { + fallback = next_row_leftmost; + } + + /* Right selects the next icon in the same row. + * Control-Right sets the keyboard focus to the next icon in the same row. + */ + keyboard_arrow_key (container, + event, + GTK_DIR_RIGHT, + rightmost_in_bottom_row, + nautilus_canvas_container_is_layout_rtl (container) ? + rightmost_in_top_row : leftmost_in_top_row, + same_row_right_side_leftmost, + fallback, + NULL, + closest_in_90_degrees); +} + +static void +keyboard_left (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + IsBetterCanvasFunction fallback; + + fallback = NULL; + if (!is_rectangle_selection_event (event)) + { + fallback = previous_row_rightmost; + } + + /* Left selects the next icon in the same row. + * Control-Left sets the keyboard focus to the next icon in the same row. + */ + keyboard_arrow_key (container, + event, + GTK_DIR_LEFT, + rightmost_in_bottom_row, + nautilus_canvas_container_is_layout_rtl (container) ? + rightmost_in_top_row : leftmost_in_top_row, + same_row_left_side_rightmost, + fallback, + NULL, + closest_in_90_degrees); +} + +static void +keyboard_down (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + IsBetterCanvasFunction next_row_fallback; + + next_row_fallback = NULL; + if (gtk_widget_get_direction (GTK_WIDGET (container)) == GTK_TEXT_DIR_RTL) + { + next_row_fallback = next_row_leftmost; + } + else + { + next_row_fallback = next_row_rightmost; + } + + /* Down selects the next icon in the same column. + * Control-Down sets the keyboard focus to the next icon in the same column. + */ + keyboard_arrow_key (container, + event, + GTK_DIR_DOWN, + rightmost_in_bottom_row, + nautilus_canvas_container_is_layout_rtl (container) ? + rightmost_in_top_row : leftmost_in_top_row, + same_column_below_highest, + NULL, + next_row_fallback, + closest_in_90_degrees); +} + +static void +keyboard_up (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + /* Up selects the next icon in the same column. + * Control-Up sets the keyboard focus to the next icon in the same column. + */ + keyboard_arrow_key (container, + event, + GTK_DIR_UP, + rightmost_in_bottom_row, + nautilus_canvas_container_is_layout_rtl (container) ? + rightmost_in_top_row : leftmost_in_top_row, + same_column_above_lowest, + NULL, + NULL, + closest_in_90_degrees); +} + +void +nautilus_canvas_container_preview_selection_event (NautilusCanvasContainer *container, + GtkDirectionType direction) +{ + if (direction == GTK_DIR_UP) + { + keyboard_up (container, NULL); + } + else if (direction == GTK_DIR_DOWN) + { + keyboard_down (container, NULL); + } + else if (direction == GTK_DIR_LEFT) + { + keyboard_left (container, NULL); + } + else if (direction == GTK_DIR_RIGHT) + { + keyboard_right (container, NULL); + } +} + +static void +keyboard_space (NautilusCanvasContainer *container, + GdkEventKey *event) +{ + NautilusCanvasIcon *icon; + + if (!has_selection (container) && + container->details->focus != NULL) + { + keyboard_move_to (container, + container->details->focus, + NULL, NULL); + } + else if ((event->state & GDK_CONTROL_MASK) != 0 && + (event->state & GDK_SHIFT_MASK) == 0) + { + /* Control-space toggles the selection state of the current icon. */ + if (container->details->focus != NULL) + { + icon_toggle_selected (container, container->details->focus); + g_signal_emit (container, signals[SELECTION_CHANGED], 0); + if (container->details->focus->is_selected) + { + container->details->range_selection_base_icon = container->details->focus; + } + } + else + { + icon = find_best_selected_icon (container, + NULL, + leftmost_in_top_row, + NULL); + if (icon == NULL) + { + icon = find_best_icon (container, + NULL, + leftmost_in_top_row, + NULL); + } + if (icon != NULL) + { + set_focus (container, icon, TRUE); + } + } + } + else if ((event->state & GDK_SHIFT_MASK) != 0) + { + activate_selected_items_alternate (container, NULL); + } + else + { + preview_selected_items (container); + } +} + +static void +destroy (GtkWidget *object) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (object); + + nautilus_canvas_container_clear (container); + + if (container->details->rubberband_info.timer_id != 0) + { + g_source_remove (container->details->rubberband_info.timer_id); + container->details->rubberband_info.timer_id = 0; + } + + if (container->details->idle_id != 0) + { + g_source_remove (container->details->idle_id); + container->details->idle_id = 0; + } + + if (container->details->align_idle_id != 0) + { + g_source_remove (container->details->align_idle_id); + container->details->align_idle_id = 0; + } + + if (container->details->selection_changed_id != 0) + { + g_source_remove (container->details->selection_changed_id); + container->details->selection_changed_id = 0; + } + + if (container->details->size_allocation_count_id != 0) + { + g_source_remove (container->details->size_allocation_count_id); + container->details->size_allocation_count_id = 0; + } + + GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->destroy (object); +} + +static void +finalize (GObject *object) +{ + NautilusCanvasContainerDetails *details; + + details = NAUTILUS_CANVAS_CONTAINER (object)->details; + + g_signal_handlers_disconnect_by_func (nautilus_icon_view_preferences, + text_ellipsis_limit_changed_container_callback, + object); + + g_hash_table_destroy (details->icon_set); + details->icon_set = NULL; + + g_free (details->font); + + if (details->a11y_item_action_queue != NULL) + { + while (!g_queue_is_empty (details->a11y_item_action_queue)) + { + g_free (g_queue_pop_head (details->a11y_item_action_queue)); + } + g_queue_free (details->a11y_item_action_queue); + } + if (details->a11y_item_action_idle_handler != 0) + { + g_source_remove (details->a11y_item_action_idle_handler); + } + + g_free (details); + + G_OBJECT_CLASS (nautilus_canvas_container_parent_class)->finalize (object); +} + +/* GtkWidget methods. */ + +static gboolean +clear_size_allocation_count (gpointer data) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (data); + + container->details->size_allocation_count_id = 0; + container->details->size_allocation_count = 0; + + return FALSE; +} + +static void +size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + NautilusCanvasContainer *container; + gboolean need_layout_redone; + GtkAllocation wid_allocation; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + need_layout_redone = !container->details->has_been_allocated; + gtk_widget_get_allocation (widget, &wid_allocation); + + if (allocation->width != wid_allocation.width) + { + need_layout_redone = TRUE; + } + + if (allocation->height != wid_allocation.height) + { + need_layout_redone = TRUE; + } + + /* Under some conditions we can end up in a loop when size allocating. + * This happens when the icons don't fit without a scrollbar, but fits + * when a scrollbar is added (bug #129963 for details). + * We keep track of this looping by increasing a counter in size_allocate + * and clearing it in a high-prio idle (the only way to detect the loop is + * done). + * When we've done at more than two iterations (with/without scrollbar) + * we terminate this looping by not redoing the layout when the width + * is wider than the current one (i.e when removing the scrollbar). + */ + if (container->details->size_allocation_count_id == 0) + { + container->details->size_allocation_count_id = + g_idle_add_full (G_PRIORITY_HIGH, + clear_size_allocation_count, + container, NULL); + } + container->details->size_allocation_count++; + if (container->details->size_allocation_count > 2 && + allocation->width >= wid_allocation.width) + { + need_layout_redone = FALSE; + } + + GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->size_allocate (widget, allocation); + + container->details->has_been_allocated = TRUE; + + if (need_layout_redone) + { + redo_layout (container); + } +} + +static GtkSizeRequestMode +get_request_mode (GtkWidget *widget) +{ + /* Don't trade size at all, since we get whatever we get anyway. */ + return GTK_SIZE_REQUEST_CONSTANT_SIZE; +} + +/* We need to implement these since the GtkScrolledWindow uses them + * to guess whether to show scrollbars or not, and if we don't report + * anything it'll tend to get it wrong causing double calls + * to size_allocate (at different sizes) during its size allocation. */ +static void +get_prefered_width (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + EelCanvasGroup *root; + double x1, x2; + int cx1, cx2; + int width; + + root = eel_canvas_root (EEL_CANVAS (widget)); + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (root), + &x1, NULL, &x2, NULL); + eel_canvas_w2c (EEL_CANVAS (widget), x1, 0, &cx1, NULL); + eel_canvas_w2c (EEL_CANVAS (widget), x2, 0, &cx2, NULL); + + width = cx2 - cx1; + if (natural_size) + { + *natural_size = width; + } + if (minimum_size) + { + *minimum_size = width; + } +} + +static void +get_prefered_height (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + EelCanvasGroup *root; + double y1, y2; + int cy1, cy2; + int height; + + root = eel_canvas_root (EEL_CANVAS (widget)); + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (root), + NULL, &y1, NULL, &y2); + eel_canvas_w2c (EEL_CANVAS (widget), 0, y1, NULL, &cy1); + eel_canvas_w2c (EEL_CANVAS (widget), 0, y2, NULL, &cy2); + + height = cy2 - cy1; + if (natural_size) + { + *natural_size = height; + } + if (minimum_size) + { + *minimum_size = height; + } +} + +static void +realize (GtkWidget *widget) +{ + GtkAdjustment *vadj, *hadj; + NautilusCanvasContainer *container; + + GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->realize (widget); + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + /* Set up DnD. */ + nautilus_canvas_dnd_init (container); + + hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (widget)); + g_signal_connect (hadj, "value-changed", + G_CALLBACK (handle_hadjustment_changed), widget); + + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (widget)); + g_signal_connect (vadj, "value-changed", + G_CALLBACK (handle_vadjustment_changed), widget); +} + +static void +unrealize (GtkWidget *widget) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + nautilus_canvas_dnd_fini (container); + + GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->unrealize (widget); +} + +static void +nautilus_canvas_container_request_update_all_internal (NautilusCanvasContainer *container, + gboolean invalidate_labels) +{ + GList *node; + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + for (node = container->details->icons; node != NULL; node = node->next) + { + icon = node->data; + + if (invalidate_labels) + { + nautilus_canvas_item_invalidate_label (icon->item); + } + + nautilus_canvas_container_update_icon (container, icon); + } + + container->details->needs_resort = TRUE; + redo_layout (container); +} + +static void +style_updated (GtkWidget *widget) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->style_updated (widget); + + if (gtk_widget_get_realized (widget)) + { + nautilus_canvas_container_request_update_all_internal (container, TRUE); + } +} + +static gboolean +button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + NautilusCanvasContainer *container; + gboolean selection_changed; + gboolean return_value; + gboolean clicked_on_icon; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + container->details->button_down_time = event->time; + + /* Forget about the old keyboard selection now that we've started mousing. */ + clear_keyboard_rubberband_start (container); + + if (event->type == GDK_2BUTTON_PRESS || event->type == GDK_3BUTTON_PRESS) + { + /* We use our own double-click detection. */ + return TRUE; + } + + /* Invoke the canvas event handler and see if an item picks up the event. */ + clicked_on_icon = GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->button_press_event (widget, event); + + if (!gtk_widget_has_focus (widget)) + { + gtk_widget_grab_focus (widget); + } + + if (clicked_on_icon) + { + return TRUE; + } + + clear_focus (container); + + if (event->button == DRAG_BUTTON && + event->type == GDK_BUTTON_PRESS) + { + /* Clear the last click icon for double click */ + container->details->double_click_icon[1] = container->details->double_click_icon[0]; + container->details->double_click_icon[0] = NULL; + } + + /* Button 1 does rubber banding. */ + if (event->button == RUBBERBAND_BUTTON) + { + if (!button_event_modifies_selection (event)) + { + selection_changed = unselect_all (container); + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + + start_rubberbanding (container, event); + return TRUE; + } + + /* Prevent multi-button weirdness such as bug 6181 */ + if (container->details->rubberband_info.active) + { + return TRUE; + } + + /* Button 2 may be passed to the window manager. */ + if (event->button == MIDDLE_BUTTON) + { + selection_changed = unselect_all (container); + if (selection_changed) + { + g_signal_emit (container, signals[SELECTION_CHANGED], 0); + } + g_signal_emit (widget, signals[MIDDLE_CLICK], 0, event); + return TRUE; + } + + /* Button 3 does a contextual menu. */ + if (event->button == CONTEXTUAL_MENU_BUTTON) + { + selection_changed = unselect_all (container); + if (selection_changed) + { + g_signal_emit (container, signals[SELECTION_CHANGED], 0); + } + g_signal_emit (widget, signals[CONTEXT_CLICK_BACKGROUND], 0, event); + return TRUE; + } + + /* Otherwise, we emit a button_press message. */ + g_signal_emit (widget, + signals[BUTTON_PRESS], 0, event, + &return_value); + return return_value; +} + +static void +nautilus_canvas_container_did_not_drag (NautilusCanvasContainer *container, + GdkEventButton *event) +{ + NautilusCanvasContainerDetails *details; + gboolean selection_changed; + static gint64 last_click_time = 0; + static gint click_count = 0; + gint double_click_time; + gint64 current_time; + + details = container->details; + + if (details->icon_selected_on_button_down && + ((event->state & GDK_CONTROL_MASK) != 0 || + (event->state & GDK_SHIFT_MASK) == 0)) + { + if (button_event_modifies_selection (event)) + { + details->range_selection_base_icon = NULL; + icon_toggle_selected (container, details->drag_icon); + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + else + { + details->range_selection_base_icon = details->drag_icon; + selection_changed = select_one_unselect_others + (container, details->drag_icon); + + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + } + + if (details->drag_icon != NULL && + (details->single_click_mode || + event->button == MIDDLE_BUTTON)) + { + /* Determine click count */ + g_object_get (G_OBJECT (gtk_widget_get_settings (GTK_WIDGET (container))), + "gtk-double-click-time", &double_click_time, + NULL); + current_time = g_get_monotonic_time (); + if (current_time - last_click_time < double_click_time * 1000) + { + click_count++; + } + else + { + click_count = 0; + } + + /* Stash time for next compare */ + last_click_time = current_time; + + /* If single-click mode, activate the selected icons, unless modifying + * the selection or pressing for a very long time, or double clicking. + */ + + + if (click_count == 0 && + event->time - details->button_down_time < MAX_CLICK_TIME && + !button_event_modifies_selection (event)) + { + /* It's a tricky UI issue whether this should activate + * just the clicked item (as if it were a link), or all + * the selected items (as if you were issuing an "activate + * selection" command). For now, we're trying the activate + * entire selection version to see how it feels. Note that + * NautilusList goes the other way because its "links" seem + * much more link-like. + */ + if (event->button == MIDDLE_BUTTON) + { + activate_selected_items_alternate (container, NULL); + } + else + { + activate_selected_items (container); + } + } + } +} + +static gboolean +clicked_within_double_click_interval (NautilusCanvasContainer *container) +{ + static gint64 last_click_time = 0; + static gint click_count = 0; + gint double_click_time; + gint64 current_time; + + /* Determine click count */ + g_object_get (G_OBJECT (gtk_widget_get_settings (GTK_WIDGET (container))), + "gtk-double-click-time", &double_click_time, + NULL); + current_time = g_get_monotonic_time (); + if (current_time - last_click_time < double_click_time * 1000) + { + click_count++; + } + else + { + click_count = 0; + } + + /* Stash time for next compare */ + last_click_time = current_time; + + /* Only allow double click */ + if (click_count == 1) + { + click_count = 0; + return TRUE; + } + else + { + return FALSE; + } +} + +static void +clear_drag_state (NautilusCanvasContainer *container) +{ + container->details->drag_icon = NULL; + container->details->drag_state = DRAG_STATE_INITIAL; +} + +static gboolean +button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + NautilusCanvasContainer *container; + NautilusCanvasContainerDetails *details; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + details = container->details; + + if (event->button == RUBBERBAND_BUTTON && details->rubberband_info.active) + { + stop_rubberbanding (container, event); + return TRUE; + } + + if (event->button == details->drag_button) + { + details->drag_button = 0; + + switch (details->drag_state) + { + case DRAG_STATE_MOVE_OR_COPY: + { + if (!details->drag_started) + { + nautilus_canvas_container_did_not_drag (container, event); + } + else + { + nautilus_canvas_dnd_end_drag (container); + DEBUG ("Ending drag from canvas container"); + } + } + break; + + default: + { + } + break; + } + + clear_drag_state (container); + return TRUE; + } + + return GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->button_release_event (widget, event); +} + +static int +motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + NautilusCanvasContainer *container; + NautilusCanvasContainerDetails *details; + double world_x, world_y; + int canvas_x, canvas_y; + GdkDragAction actions; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + details = container->details; + + if (details->drag_button != 0) + { + switch (details->drag_state) + { + case DRAG_STATE_MOVE_OR_COPY: + { + if (details->drag_started) + { + break; + } + + eel_canvas_window_to_world + (EEL_CANVAS (container), event->x, event->y, &world_x, &world_y); + + if (gtk_drag_check_threshold (widget, + details->drag_x, + details->drag_y, + world_x, + world_y)) + { + details->drag_started = TRUE; + details->drag_state = DRAG_STATE_MOVE_OR_COPY; + + eel_canvas_w2c (EEL_CANVAS (container), + details->drag_x, + details->drag_y, + &canvas_x, + &canvas_y); + + actions = GDK_ACTION_COPY + | GDK_ACTION_MOVE + | GDK_ACTION_LINK + | GDK_ACTION_ASK; + + nautilus_canvas_dnd_begin_drag (container, + actions, + details->drag_button, + event, + canvas_x, + canvas_y); + DEBUG ("Beginning drag from canvas container"); + } + } + break; + + default: + { + } + break; + } + } + + return GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->motion_notify_event (widget, event); +} + +static void +nautilus_canvas_container_get_icon_text (NautilusCanvasContainer *container, + NautilusCanvasIconData *data, + char **editable_text, + char **additional_text, + gboolean include_invisible) +{ + NautilusCanvasContainerClass *klass; + + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (container); + g_assert (klass->get_icon_text != NULL); + + klass->get_icon_text (container, data, editable_text, additional_text, include_invisible); +} + +static gboolean +handle_popups (NautilusCanvasContainer *container, + GdkEvent *event, + const char *signal) +{ + /* ensure we clear the drag state before showing the menu */ + clear_drag_state (container); + + g_signal_emit_by_name (container, signal, event); + + return TRUE; +} + +static int +key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + NautilusCanvasContainer *container; + gboolean handled; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + handled = FALSE; + + switch (event->keyval) + { + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + { + keyboard_home (container, event); + handled = TRUE; + } + break; + + case GDK_KEY_End: + case GDK_KEY_KP_End: + { + keyboard_end (container, event); + handled = TRUE; + } + break; + + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + { + /* Don't eat Alt-Left, as that is used for history browsing */ + if ((event->state & GDK_MOD1_MASK) == 0) + { + keyboard_left (container, event); + handled = TRUE; + } + } + break; + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + { + /* Don't eat Alt-Up, as that is used for alt-shift-Up */ + if ((event->state & GDK_MOD1_MASK) == 0) + { + keyboard_up (container, event); + handled = TRUE; + } + } + break; + + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + { + /* Don't eat Alt-Right, as that is used for history browsing */ + if ((event->state & GDK_MOD1_MASK) == 0) + { + keyboard_right (container, event); + handled = TRUE; + } + } + break; + + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + { + /* Don't eat Alt-Down, as that is used for Open */ + if ((event->state & GDK_MOD1_MASK) == 0) + { + keyboard_down (container, event); + handled = TRUE; + } + } + break; + + case GDK_KEY_space: + { + keyboard_space (container, event); + handled = TRUE; + } + break; + + case GDK_KEY_F10: + { + /* handle Ctrl+F10 because we want to display the + * background popup even if something is selected. + * The other cases are handled by the "popup-menu" GtkWidget signal. + */ + if (event->state & GDK_CONTROL_MASK) + { + handled = handle_popups (container, (GdkEvent *) event, + "context_click_background"); + } + } + break; + + case GDK_KEY_v: + { + /* Eat Control + v to not enable type ahead */ + if ((event->state & GDK_CONTROL_MASK) != 0) + { + handled = TRUE; + } + } + break; + + default: + { + } + break; + } + + if (!handled) + { + handled = GTK_WIDGET_CLASS (nautilus_canvas_container_parent_class)->key_press_event (widget, event); + } + + return handled; +} + +static void +grab_notify_cb (GtkWidget *widget, + gboolean was_grabbed) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + if (container->details->rubberband_info.active && + !was_grabbed) + { + /* we got a (un)grab-notify during rubberband. + * This happens when a new modal dialog shows + * up (e.g. authentication or an error). Stop + * the rubberbanding so that we can handle the + * dialog. */ + stop_rubberbanding (container, NULL); + } +} + +static void +text_ellipsis_limit_changed_container_callback (gpointer callback_data) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (callback_data); + invalidate_label_sizes (container); + schedule_redo_layout (container); +} + +static GObject * +nautilus_canvas_container_constructor (GType type, + guint n_construct_params, + GObjectConstructParam *construct_params) +{ + NautilusCanvasContainer *container; + GObject *object; + + object = G_OBJECT_CLASS (nautilus_canvas_container_parent_class)->constructor + (type, + n_construct_params, + construct_params); + + container = NAUTILUS_CANVAS_CONTAINER (object); + g_signal_connect_swapped (nautilus_icon_view_preferences, + "changed::" NAUTILUS_PREFERENCES_ICON_VIEW_TEXT_ELLIPSIS_LIMIT, + G_CALLBACK (text_ellipsis_limit_changed_container_callback), + container); + + return object; +} + +/* Initialization. */ + +static void +nautilus_canvas_container_class_init (NautilusCanvasContainerClass *class) +{ + GtkWidgetClass *widget_class; + + G_OBJECT_CLASS (class)->constructor = nautilus_canvas_container_constructor; + G_OBJECT_CLASS (class)->finalize = finalize; + + /* Signals. */ + + signals[SELECTION_CHANGED] + = g_signal_new ("selection-changed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[BUTTON_PRESS] + = g_signal_new ("button-press", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + button_press), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT); + signals[ACTIVATE] + = g_signal_new ("activate", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + activate), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + signals[ACTIVATE_ALTERNATE] + = g_signal_new ("activate-alternate", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + activate_alternate), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + signals[ACTIVATE_PREVIEWER] + = g_signal_new ("activate-previewer", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + activate_previewer), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 2, + G_TYPE_POINTER, G_TYPE_POINTER); + signals[CONTEXT_CLICK_SELECTION] + = g_signal_new ("context-click-selection", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + context_click_selection), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + signals[CONTEXT_CLICK_BACKGROUND] + = g_signal_new ("context-click-background", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + context_click_background), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + signals[MIDDLE_CLICK] + = g_signal_new ("middle-click", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + middle_click), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + signals[GET_ICON_URI] + = g_signal_new ("get-icon-uri", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + get_icon_uri), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_STRING, 1, + G_TYPE_POINTER); + signals[GET_ICON_ACTIVATION_URI] + = g_signal_new ("get-icon-activation-uri", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + get_icon_activation_uri), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_STRING, 1, + G_TYPE_POINTER); + signals[GET_ICON_DROP_TARGET_URI] + = g_signal_new ("get-icon-drop-target-uri", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + get_icon_drop_target_uri), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_STRING, 1, + G_TYPE_POINTER); + signals[MOVE_COPY_ITEMS] + = g_signal_new ("move-copy-items", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + move_copy_items), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 3, + G_TYPE_POINTER, + G_TYPE_POINTER, + GDK_TYPE_DRAG_ACTION); + signals[HANDLE_NETSCAPE_URL] + = g_signal_new ("handle-netscape-url", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + handle_netscape_url), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 3, + G_TYPE_STRING, + G_TYPE_STRING, + GDK_TYPE_DRAG_ACTION); + signals[HANDLE_URI_LIST] + = g_signal_new ("handle-uri-list", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + handle_uri_list), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 3, + G_TYPE_STRING, + G_TYPE_STRING, + GDK_TYPE_DRAG_ACTION); + signals[HANDLE_TEXT] + = g_signal_new ("handle-text", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + handle_text), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 3, + G_TYPE_STRING, + G_TYPE_STRING, + GDK_TYPE_DRAG_ACTION); + signals[HANDLE_RAW] + = g_signal_new ("handle-raw", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + handle_raw), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 5, + G_TYPE_POINTER, + G_TYPE_INT, + G_TYPE_STRING, + G_TYPE_STRING, + GDK_TYPE_DRAG_ACTION); + signals[HANDLE_HOVER] = + g_signal_new ("handle-hover", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + handle_hover), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); + signals[GET_CONTAINER_URI] + = g_signal_new ("get-container-uri", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + get_container_uri), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_STRING, 0); + signals[BAND_SELECT_STARTED] + = g_signal_new ("band-select-started", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + band_select_started), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[BAND_SELECT_ENDED] + = g_signal_new ("band-select-ended", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + band_select_ended), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[ICON_ADDED] + = g_signal_new ("icon-added", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + icon_added), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, G_TYPE_POINTER); + signals[ICON_REMOVED] + = g_signal_new ("icon-removed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + icon_removed), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, G_TYPE_POINTER); + + signals[CLEARED] + = g_signal_new ("cleared", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusCanvasContainerClass, + cleared), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* GtkWidget class. */ + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->destroy = destroy; + widget_class->size_allocate = size_allocate; + widget_class->get_request_mode = get_request_mode; + widget_class->get_preferred_width = get_prefered_width; + widget_class->get_preferred_height = get_prefered_height; + widget_class->realize = realize; + widget_class->unrealize = unrealize; + widget_class->button_press_event = button_press_event; + widget_class->button_release_event = button_release_event; + widget_class->motion_notify_event = motion_notify_event; + widget_class->key_press_event = key_press_event; + widget_class->style_updated = style_updated; + widget_class->grab_notify = grab_notify_cb; + + gtk_widget_class_set_accessible_type (widget_class, nautilus_canvas_container_accessible_get_type ()); +} + +static void +update_selected (NautilusCanvasContainer *container) +{ + GList *node; + NautilusCanvasIcon *icon; + + for (node = container->details->icons; node != NULL; node = node->next) + { + icon = node->data; + if (icon->is_selected) + { + eel_canvas_item_request_update (EEL_CANVAS_ITEM (icon->item)); + } + } +} + +static void +handle_has_focus_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + update_selected (NAUTILUS_CANVAS_CONTAINER (object)); +} + +static void +handle_scale_factor_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + nautilus_canvas_container_request_update_all_internal (NAUTILUS_CANVAS_CONTAINER (object), + TRUE); +} + + + +static int text_ellipsis_limits[NAUTILUS_CANVAS_ZOOM_LEVEL_N_ENTRIES]; + +static gboolean +get_text_ellipsis_limit_for_zoom (char **strs, + const char *zoom_level, + int *limit) +{ + char **p; + char *str; + gboolean success; + + success = FALSE; + + /* default */ + *limit = 3; + + if (zoom_level != NULL) + { + str = g_strdup_printf ("%s:%%d", zoom_level); + } + else + { + str = g_strdup ("%d"); + } + + if (strs != NULL) + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + for (p = strs; *p != NULL; p++) + { + if (sscanf (*p, str, limit)) + { + success = TRUE; + } + } +#pragma GCC diagnostic pop + } + + g_free (str); + + return success; +} + +static const char *zoom_level_names[] = +{ + "small", + "standard", + "large", +}; + +static void +text_ellipsis_limit_changed_callback (gpointer callback_data) +{ + char **pref; + unsigned int i; + int one_limit; + + pref = g_settings_get_strv (nautilus_icon_view_preferences, + NAUTILUS_PREFERENCES_ICON_VIEW_TEXT_ELLIPSIS_LIMIT); + + /* set default */ + get_text_ellipsis_limit_for_zoom (pref, NULL, &one_limit); + for (i = 0; i < NAUTILUS_CANVAS_ZOOM_LEVEL_N_ENTRIES; i++) + { + text_ellipsis_limits[i] = one_limit; + } + + /* override for each zoom level */ + for (i = 0; i < G_N_ELEMENTS (zoom_level_names); i++) + { + if (get_text_ellipsis_limit_for_zoom (pref, + zoom_level_names[i], + &one_limit)) + { + text_ellipsis_limits[i] = one_limit; + } + } + + g_strfreev (pref); +} + +static void +nautilus_canvas_container_init (NautilusCanvasContainer *container) +{ + NautilusCanvasContainerDetails *details; + static gboolean setup_prefs = FALSE; + + details = g_new0 (NautilusCanvasContainerDetails, 1); + + details->icon_set = g_hash_table_new (g_direct_hash, g_direct_equal); + details->zoom_level = NAUTILUS_CANVAS_ZOOM_LEVEL_STANDARD; + + container->details = details; + + g_signal_connect (container, "notify::has-focus", + G_CALLBACK (handle_has_focus_changed), NULL); + g_signal_connect (container, "notify::scale-factor", + G_CALLBACK (handle_scale_factor_changed), NULL); + + if (!setup_prefs) + { + g_signal_connect_swapped (nautilus_icon_view_preferences, + "changed::" NAUTILUS_PREFERENCES_ICON_VIEW_TEXT_ELLIPSIS_LIMIT, + G_CALLBACK (text_ellipsis_limit_changed_callback), + NULL); + text_ellipsis_limit_changed_callback (NULL); + + setup_prefs = TRUE; + } +} + +typedef struct +{ + NautilusCanvasContainer *container; + GdkEventButton *event; +} ContextMenuParameters; + +static gboolean +handle_canvas_double_click (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + GdkEventButton *event) +{ + NautilusCanvasContainerDetails *details; + + if (event->button != DRAG_BUTTON) + { + return FALSE; + } + + details = container->details; + + if (!details->single_click_mode && + clicked_within_double_click_interval (container) && + details->double_click_icon[0] == details->double_click_icon[1] && + details->double_click_button[0] == details->double_click_button[1]) + { + details->double_clicked = TRUE; + return TRUE; + } + + return FALSE; +} + +/* NautilusCanvasIcon event handling. */ + +/* Conceptually, pressing button 1 together with CTRL or SHIFT toggles + * selection of a single icon without affecting the other icons; + * without CTRL or SHIFT, it selects a single icon and un-selects all + * the other icons. But in this latter case, the de-selection should + * only happen when the button is released if the icon is already + * selected, because the user might select multiple icons and drag all + * of them by doing a simple click-drag. + */ + +static gboolean +handle_canvas_button_press (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon, + GdkEventButton *event) +{ + NautilusCanvasContainerDetails *details; + + details = container->details; + + if (event->type == GDK_2BUTTON_PRESS || event->type == GDK_3BUTTON_PRESS) + { + return TRUE; + } + + if (event->button != DRAG_BUTTON + && event->button != CONTEXTUAL_MENU_BUTTON + && event->button != DRAG_MENU_BUTTON) + { + return TRUE; + } + + if ((event->button == DRAG_BUTTON) && + event->type == GDK_BUTTON_PRESS) + { + /* The next double click has to be on this icon */ + details->double_click_icon[1] = details->double_click_icon[0]; + details->double_click_icon[0] = icon; + + details->double_click_button[1] = details->double_click_button[0]; + details->double_click_button[0] = event->button; + } + + if (handle_canvas_double_click (container, icon, event)) + { + /* Double clicking does not trigger a D&D action. */ + details->drag_button = 0; + details->drag_icon = NULL; + return TRUE; + } + + if (event->button == DRAG_BUTTON + || event->button == DRAG_MENU_BUTTON) + { + details->drag_button = event->button; + details->drag_icon = icon; + details->drag_x = event->x; + details->drag_y = event->y; + details->drag_state = DRAG_STATE_MOVE_OR_COPY; + details->drag_started = FALSE; + } + + /* Modify the selection as appropriate. Selection is modified + * the same way for contextual menu as it would be without. + */ + details->icon_selected_on_button_down = icon->is_selected; + + if ((event->button == DRAG_BUTTON || event->button == MIDDLE_BUTTON) && + (event->state & GDK_SHIFT_MASK) != 0) + { + NautilusCanvasIcon *start_icon; + + set_focus (container, icon, FALSE); + + start_icon = details->range_selection_base_icon; + if (start_icon == NULL || !start_icon->is_selected) + { + start_icon = icon; + details->range_selection_base_icon = icon; + } + if (select_range (container, start_icon, icon, + (event->state & GDK_CONTROL_MASK) == 0)) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + else if (!details->icon_selected_on_button_down) + { + set_focus (container, icon, FALSE); + + details->range_selection_base_icon = icon; + if (button_event_modifies_selection (event)) + { + icon_toggle_selected (container, icon); + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + else + { + select_one_unselect_others (container, icon); + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } + } + + if (event->button == CONTEXTUAL_MENU_BUTTON) + { + clear_drag_state (container); + + g_signal_emit (container, + signals[CONTEXT_CLICK_SELECTION], 0, + event); + } + + + return TRUE; +} + +static int +item_event_callback (EelCanvasItem *item, + GdkEvent *event, + gpointer data) +{ + NautilusCanvasContainer *container; + NautilusCanvasIcon *icon; + GdkEventButton *event_button; + + container = NAUTILUS_CANVAS_CONTAINER (data); + + icon = NAUTILUS_CANVAS_ITEM (item)->user_data; + g_assert (icon != NULL); + + event_button = &event->button; + + switch (event->type) + { + case GDK_MOTION_NOTIFY: + { + return FALSE; + } + + case GDK_BUTTON_PRESS: + { + container->details->double_clicked = FALSE; + if (handle_canvas_button_press (container, icon, event_button)) + { + /* Stop the event from being passed along further. Returning + * TRUE ain't enough. + */ + return TRUE; + } + return FALSE; + } + + case GDK_BUTTON_RELEASE: + { + if (event_button->button == DRAG_BUTTON + && container->details->double_clicked) + { + if (!button_event_modifies_selection (event_button)) + { + activate_selected_items (container); + } + else if ((event_button->state & GDK_CONTROL_MASK) == 0 && + (event_button->state & GDK_SHIFT_MASK) != 0) + { + activate_selected_items_alternate (container, icon); + } + } + /* fall through */ + } + + default: + { + container->details->double_clicked = FALSE; + return FALSE; + } + break; + } +} + +GtkWidget * +nautilus_canvas_container_new (void) +{ + return gtk_widget_new (NAUTILUS_TYPE_CANVAS_CONTAINER, NULL); +} + +/* Clear all of the icons in the container. */ +void +nautilus_canvas_container_clear (NautilusCanvasContainer *container) +{ + NautilusCanvasContainerDetails *details; + GList *p; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + details = container->details; + + if (details->icons == NULL) + { + return; + } + + clear_focus (container); + clear_keyboard_rubberband_start (container); + unschedule_keyboard_icon_reveal (container); + set_pending_icon_to_reveal (container, NULL); + details->drop_target = NULL; + + for (p = details->icons; p != NULL; p = p->next) + { + icon_free (p->data); + } + g_list_free (details->icons); + details->icons = NULL; + g_list_free (details->new_icons); + details->new_icons = NULL; + g_list_free (details->selection); + details->selection = NULL; + + g_hash_table_destroy (details->icon_set); + details->icon_set = g_hash_table_new (g_direct_hash, g_direct_equal); + + nautilus_canvas_container_update_scroll_region (container); +} + +gboolean +nautilus_canvas_container_is_empty (NautilusCanvasContainer *container) +{ + return container->details->icons == NULL; +} + +NautilusCanvasIconData * +nautilus_canvas_container_get_first_visible_icon (NautilusCanvasContainer *container) +{ + GList *l; + NautilusCanvasIcon *icon, *best_icon; + double x, y; + double x1, y1, x2, y2; + double *pos, best_pos; + double hadj_v, vadj_v, h_page_size; + gboolean better_icon; + gboolean compare_lt; + + hadj_v = gtk_adjustment_get_value (gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container))); + vadj_v = gtk_adjustment_get_value (gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container))); + h_page_size = gtk_adjustment_get_page_size (gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container))); + + if (nautilus_canvas_container_is_layout_rtl (container)) + { + x = hadj_v + h_page_size - ICON_PAD_LEFT - 1; + y = vadj_v; + } + else + { + x = hadj_v; + y = vadj_v; + } + + eel_canvas_c2w (EEL_CANVAS (container), + x, y, + &x, &y); + + l = container->details->icons; + best_icon = NULL; + best_pos = 0; + while (l != NULL) + { + icon = l->data; + + if (icon_is_positioned (icon)) + { + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (icon->item), + &x1, &y1, &x2, &y2); + + compare_lt = FALSE; + pos = &y1; + better_icon = y2 > y + ICON_PAD_TOP; + if (better_icon) + { + if (best_icon == NULL) + { + better_icon = TRUE; + } + else if (compare_lt) + { + better_icon = best_pos < *pos; + } + else + { + better_icon = best_pos > *pos; + } + + if (better_icon) + { + best_icon = icon; + best_pos = *pos; + } + } + } + + l = l->next; + } + + return best_icon ? best_icon->data : NULL; +} + +NautilusCanvasIconData * +nautilus_canvas_container_get_focused_icon (NautilusCanvasContainer *container) +{ + NautilusCanvasIcon *icon; + + icon = container->details->focus; + + if (icon != NULL) + { + return icon->data; + } + + return NULL; +} + +/* puts the icon at the top of the screen */ +void +nautilus_canvas_container_scroll_to_canvas (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + GList *l; + NautilusCanvasIcon *icon; + GtkAdjustment *vadj; + EelIRect bounds; + GtkAllocation allocation; + + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container)); + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + + /* We need to force a relayout now if there are updates queued + * since we need the final positions */ + nautilus_canvas_container_layout_now (container); + + l = container->details->icons; + while (l != NULL) + { + icon = l->data; + + if (icon->data == data && + icon_is_positioned (icon)) + { + /* ensure that we reveal the entire row/column */ + icon_get_row_and_column_bounds (container, icon, &bounds); + + gtk_adjustment_set_value (vadj, bounds.y0); + } + + l = l->next; + } +} + +/* Call a function for all the icons. */ +typedef struct +{ + NautilusCanvasCallback callback; + gpointer callback_data; +} CallbackAndData; + +static void +call_canvas_callback (gpointer data, + gpointer callback_data) +{ + NautilusCanvasIcon *icon; + CallbackAndData *callback_and_data; + + icon = data; + callback_and_data = callback_data; + (*callback_and_data->callback)(icon->data, callback_and_data->callback_data); +} + +void +nautilus_canvas_container_for_each (NautilusCanvasContainer *container, + NautilusCanvasCallback callback, + gpointer callback_data) +{ + CallbackAndData callback_and_data; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + callback_and_data.callback = callback; + callback_and_data.callback_data = callback_data; + + g_list_foreach (container->details->icons, + call_canvas_callback, &callback_and_data); +} + +static int +selection_changed_at_idle_callback (gpointer data) +{ + NautilusCanvasContainer *container; + + container = NAUTILUS_CANVAS_CONTAINER (data); + + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + + container->details->selection_changed_id = 0; + return FALSE; +} + +/* utility routine to remove a single icon from the container */ + +static void +icon_destroy (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + NautilusCanvasContainerDetails *details; + gboolean was_selected; + NautilusCanvasIcon *icon_to_focus; + GList *item; + + details = container->details; + + item = g_list_find (details->icons, icon); + item = item->next ? item->next : item->prev; + icon_to_focus = (item != NULL) ? item->data : NULL; + + details->icons = g_list_remove (details->icons, icon); + details->new_icons = g_list_remove (details->new_icons, icon); + details->selection = g_list_remove (details->selection, icon->data); + g_hash_table_remove (details->icon_set, icon->data); + + was_selected = icon->is_selected; + + if (details->focus == icon || + details->focus == NULL) + { + if (icon_to_focus != NULL) + { + set_focus (container, icon_to_focus, TRUE); + } + else + { + clear_focus (container); + } + } + + if (details->keyboard_rubberband_start == icon) + { + clear_keyboard_rubberband_start (container); + } + + if (details->keyboard_icon_to_reveal == icon) + { + unschedule_keyboard_icon_reveal (container); + } + if (details->drag_icon == icon) + { + clear_drag_state (container); + } + if (details->drop_target == icon) + { + details->drop_target = NULL; + } + if (details->range_selection_base_icon == icon) + { + details->range_selection_base_icon = NULL; + } + if (details->pending_icon_to_reveal == icon) + { + set_pending_icon_to_reveal (container, NULL); + } + + icon_free (icon); + + if (was_selected) + { + /* Coalesce multiple removals causing multiple selection_changed events */ + details->selection_changed_id = g_idle_add (selection_changed_at_idle_callback, container); + } +} + +/* activate any selected items in the container */ +static void +activate_selected_items (NautilusCanvasContainer *container) +{ + GList *selection; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection = nautilus_canvas_container_get_selection (container); + if (selection != NULL) + { + g_signal_emit (container, + signals[ACTIVATE], 0, + selection); + } + g_list_free (selection); +} + +static void +preview_selected_items (NautilusCanvasContainer *container) +{ + GList *selection; + GArray *locations; + gint idx; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection = nautilus_canvas_container_get_selection (container); + locations = nautilus_canvas_container_get_selected_icon_locations (container); + + for (idx = 0; idx < locations->len; idx++) + { + GdkPoint *point = &(g_array_index (locations, GdkPoint, idx)); + gint scroll_x, scroll_y; + + eel_canvas_get_scroll_offsets (EEL_CANVAS (container), + &scroll_x, &scroll_y); + + point->x -= scroll_x; + point->y -= scroll_y; + } + + if (selection != NULL) + { + g_signal_emit (container, + signals[ACTIVATE_PREVIEWER], 0, + selection, locations); + } + g_list_free (selection); + g_array_unref (locations); +} + +static void +activate_selected_items_alternate (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + GList *selection; + + g_assert (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + if (icon != NULL) + { + selection = g_list_prepend (NULL, icon->data); + } + else + { + selection = nautilus_canvas_container_get_selection (container); + } + if (selection != NULL) + { + g_signal_emit (container, + signals[ACTIVATE_ALTERNATE], 0, + selection); + } + g_list_free (selection); +} + +static NautilusIconInfo * +nautilus_canvas_container_get_icon_images (NautilusCanvasContainer *container, + NautilusCanvasIconData *data, + int size, + gboolean for_drag_accept) +{ + NautilusCanvasContainerClass *klass; + + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (container); + g_assert (klass->get_icon_images != NULL); + + return klass->get_icon_images (container, data, size, for_drag_accept); +} + +static void +nautilus_canvas_container_prioritize_thumbnailing (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + NautilusCanvasContainerClass *klass; + + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (container); + g_assert (klass->prioritize_thumbnailing != NULL); + + klass->prioritize_thumbnailing (container, icon->data); +} + +static void +nautilus_canvas_container_update_visible_icons (NautilusCanvasContainer *container) +{ + GtkAdjustment *vadj, *hadj; + double min_y, max_y; + double min_x, max_x; + double x0, y0, x1, y1; + GList *node; + NautilusCanvasIcon *icon; + gboolean visible; + GtkAllocation allocation; + + hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (container)); + vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (container)); + gtk_widget_get_allocation (GTK_WIDGET (container), &allocation); + + min_x = gtk_adjustment_get_value (hadj); + max_x = min_x + allocation.width; + + min_y = gtk_adjustment_get_value (vadj); + max_y = min_y + allocation.height; + + eel_canvas_c2w (EEL_CANVAS (container), + min_x, min_y, &min_x, &min_y); + eel_canvas_c2w (EEL_CANVAS (container), + max_x, max_y, &max_x, &max_y); + + /* Do the iteration in reverse to get the render-order from top to + * bottom for the prioritized thumbnails. + */ + for (node = g_list_last (container->details->icons); node != NULL; node = node->prev) + { + icon = node->data; + + if (icon_is_positioned (icon)) + { + eel_canvas_item_get_bounds (EEL_CANVAS_ITEM (icon->item), + &x0, + &y0, + &x1, + &y1); + eel_canvas_item_i2w (EEL_CANVAS_ITEM (icon->item)->parent, + &x0, + &y0); + eel_canvas_item_i2w (EEL_CANVAS_ITEM (icon->item)->parent, + &x1, + &y1); + + visible = y1 >= min_y && y0 <= max_y; + + if (visible) + { + nautilus_canvas_item_set_is_visible (icon->item, TRUE); + nautilus_canvas_container_prioritize_thumbnailing (container, + icon); + } + else + { + nautilus_canvas_item_set_is_visible (icon->item, FALSE); + } + } + } +} + +static void +handle_vadjustment_changed (GtkAdjustment *adjustment, + NautilusCanvasContainer *container) +{ + nautilus_canvas_container_update_visible_icons (container); +} + +static void +handle_hadjustment_changed (GtkAdjustment *adjustment, + NautilusCanvasContainer *container) +{ + nautilus_canvas_container_update_visible_icons (container); +} + + +void +nautilus_canvas_container_update_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + NautilusCanvasContainerDetails *details; + guint icon_size; + guint min_image_size, max_image_size; + NautilusIconInfo *icon_info; + GdkPixbuf *pixbuf; + char *editable_text, *additional_text; + + if (icon == NULL) + { + return; + } + + details = container->details; + + /* compute the maximum size based on the scale factor */ + min_image_size = MINIMUM_IMAGE_SIZE * EEL_CANVAS (container)->pixels_per_unit; + max_image_size = MAX (MAXIMUM_IMAGE_SIZE * EEL_CANVAS (container)->pixels_per_unit, NAUTILUS_ICON_MAXIMUM_SIZE); + + /* Get the appropriate images for the file. */ + icon_get_size (container, icon, &icon_size); + + icon_size = MAX (icon_size, min_image_size); + icon_size = MIN (icon_size, max_image_size); + + DEBUG ("Icon size, getting for size %d", icon_size); + + /* Get the icons. */ + icon_info = nautilus_canvas_container_get_icon_images (container, icon->data, icon_size, + icon == details->drop_target); + + pixbuf = nautilus_icon_info_get_pixbuf (icon_info); + g_object_unref (icon_info); + + nautilus_canvas_container_get_icon_text (container, + icon->data, + &editable_text, + &additional_text, + FALSE); + + eel_canvas_item_set (EEL_CANVAS_ITEM (icon->item), + "editable_text", editable_text, + "additional_text", additional_text, + "highlighted_for_drop", icon == details->drop_target, + NULL); + + nautilus_canvas_item_set_image (icon->item, pixbuf); + + /* Let the pixbufs go. */ + g_object_unref (pixbuf); + + g_free (editable_text); + g_free (additional_text); +} + +static void +finish_adding_icon (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + nautilus_canvas_container_update_icon (container, icon); + eel_canvas_item_show (EEL_CANVAS_ITEM (icon->item)); + + g_signal_connect_object (icon->item, "event", + G_CALLBACK (item_event_callback), container, 0); + + g_signal_emit (container, signals[ICON_ADDED], 0, icon->data); +} + +static gboolean +finish_adding_new_icons (NautilusCanvasContainer *container) +{ + GList *p, *new_icons; + + new_icons = container->details->new_icons; + container->details->new_icons = NULL; + container->details->is_populating_container = g_list_length (new_icons) == + g_hash_table_size (container->details->icon_set); + + /* Position most icons (not unpositioned manual-layout icons). */ + new_icons = g_list_reverse (new_icons); + for (p = new_icons; p != NULL; p = p->next) + { + finish_adding_icon (container, p->data); + } + g_list_free (new_icons); + + return TRUE; +} + +/** + * nautilus_canvas_container_add: + * @container: A NautilusCanvasContainer + * @data: Icon data. + * + * Add icon to represent @data to container. + * Returns FALSE if there was already such an icon. + **/ +gboolean +nautilus_canvas_container_add (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasContainerDetails *details; + NautilusCanvasIcon *icon; + EelCanvasItem *band, *item; + + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + details = container->details; + + if (g_hash_table_lookup (details->icon_set, data) != NULL) + { + return FALSE; + } + + /* Create the new icon, including the canvas item. */ + icon = g_new0 (NautilusCanvasIcon, 1); + icon->data = data; + icon->x = ICON_UNPOSITIONED_VALUE; + icon->y = ICON_UNPOSITIONED_VALUE; + + /* Whether the saved icon position should only be used + * if the previous icon position is free. If the position + * is occupied, another position near the last one will + */ + icon->item = NAUTILUS_CANVAS_ITEM + (eel_canvas_item_new (EEL_CANVAS_GROUP (EEL_CANVAS (container)->root), + nautilus_canvas_item_get_type (), + "visible", FALSE, + NULL)); + icon->item->user_data = icon; + + /* Make sure the icon is under the selection_rectangle */ + item = EEL_CANVAS_ITEM (icon->item); + band = NAUTILUS_CANVAS_CONTAINER (item->canvas)->details->rubberband_info.selection_rectangle; + if (band) + { + eel_canvas_item_send_behind (item, band); + } + + /* Put it on both lists. */ + details->icons = g_list_prepend (details->icons, icon); + details->new_icons = g_list_prepend (details->new_icons, icon); + + g_hash_table_insert (details->icon_set, data, icon); + + details->needs_resort = TRUE; + + /* Run an idle function to add the icons. */ + schedule_redo_layout (container); + + return TRUE; +} + +void +nautilus_canvas_container_layout_now (NautilusCanvasContainer *container) +{ + container->details->in_layout_now = TRUE; + if (container->details->idle_id != 0) + { + unschedule_redo_layout (container); + redo_layout_internal (container); + } + + /* Also need to make sure we're properly resized, for instance + * newly added files may trigger a change in the size allocation and + * thus toggle scrollbars on */ + gtk_container_check_resize (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (container)))); + container->details->in_layout_now = FALSE; +} + +/** + * nautilus_canvas_container_remove: + * @container: A NautilusCanvasContainer. + * @data: Icon data. + * + * Remove the icon with this data. + **/ +gboolean +nautilus_canvas_container_remove (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasIcon *icon; + + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + icon = g_hash_table_lookup (container->details->icon_set, data); + + if (icon == NULL) + { + return FALSE; + } + + icon_destroy (container, icon); + schedule_redo_layout (container); + + g_signal_emit (container, signals[ICON_REMOVED], 0, icon); + + return TRUE; +} + +/** + * nautilus_canvas_container_request_update: + * @container: A NautilusCanvasContainer. + * @data: Icon data. + * + * Update the icon with this data. + **/ +void +nautilus_canvas_container_request_update (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + g_return_if_fail (data != NULL); + + icon = g_hash_table_lookup (container->details->icon_set, data); + + if (icon != NULL) + { + nautilus_canvas_container_update_icon (container, icon); + container->details->needs_resort = TRUE; + schedule_redo_layout (container); + } +} + +/* zooming */ + +NautilusCanvasZoomLevel +nautilus_canvas_container_get_zoom_level (NautilusCanvasContainer *container) +{ + return container->details->zoom_level; +} + +void +nautilus_canvas_container_set_zoom_level (NautilusCanvasContainer *container, + int new_level) +{ + NautilusCanvasContainerDetails *details; + int pinned_level; + double pixels_per_unit; + + details = container->details; + + pinned_level = new_level; + if (pinned_level < NAUTILUS_CANVAS_ZOOM_LEVEL_SMALL) + { + pinned_level = NAUTILUS_CANVAS_ZOOM_LEVEL_SMALL; + } + else if (pinned_level > NAUTILUS_CANVAS_ZOOM_LEVEL_LARGER) + { + pinned_level = NAUTILUS_CANVAS_ZOOM_LEVEL_LARGER; + } + + if (pinned_level == details->zoom_level) + { + return; + } + + details->zoom_level = pinned_level; + + pixels_per_unit = (double) nautilus_canvas_container_get_icon_size_for_zoom_level (pinned_level) + / NAUTILUS_CANVAS_ICON_SIZE_STANDARD; + eel_canvas_set_pixels_per_unit (EEL_CANVAS (container), pixels_per_unit); + + nautilus_canvas_container_request_update_all_internal (container, TRUE); +} + +/** + * nautilus_canvas_container_request_update_all: + * For each icon, synchronizes the displayed information (image, text) with the + * information from the model. + * + * @container: An canvas container. + **/ +void +nautilus_canvas_container_request_update_all (NautilusCanvasContainer *container) +{ + nautilus_canvas_container_request_update_all_internal (container, FALSE); +} + +/** + * nautilus_canvas_container_reveal: + * Change scroll position as necessary to reveal the specified item. + */ +void +nautilus_canvas_container_reveal (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + g_return_if_fail (data != NULL); + + icon = g_hash_table_lookup (container->details->icon_set, data); + + if (icon != NULL) + { + reveal_icon (container, icon); + } +} + +/** + * nautilus_canvas_container_get_selection: + * @container: An canvas container. + * + * Get a list of the icons currently selected in @container. + * + * Return value: A GList of the programmer-specified data associated to each + * selected icon, or NULL if no canvas is selected. The caller is expected to + * free the list when it is not needed anymore. + **/ +GList * +nautilus_canvas_container_get_selection (NautilusCanvasContainer *container) +{ + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), NULL); + + if (container->details->selection_needs_resort) + { + sort_selection (container); + } + + return g_list_copy (container->details->selection); +} + +static GList * +nautilus_canvas_container_get_selected_icons (NautilusCanvasContainer *container) +{ + GList *list, *p; + + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), NULL); + + list = NULL; + for (p = container->details->icons; p != NULL; p = p->next) + { + NautilusCanvasIcon *icon; + + icon = p->data; + if (icon->is_selected) + { + list = g_list_prepend (list, icon); + } + } + + return g_list_reverse (list); +} + +/** + * nautilus_canvas_container_invert_selection: + * @container: An canvas container. + * + * Inverts the selection in @container. + * + **/ +void +nautilus_canvas_container_invert_selection (NautilusCanvasContainer *container) +{ + GList *p; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + for (p = container->details->icons; p != NULL; p = p->next) + { + NautilusCanvasIcon *icon; + + icon = p->data; + icon_toggle_selected (container, icon); + } + + g_signal_emit (container, signals[SELECTION_CHANGED], 0); +} + + +/* Returns an array of GdkPoints of locations of the icons. */ +static GArray * +nautilus_canvas_container_get_icon_locations (NautilusCanvasContainer *container, + GList *icons) +{ + GArray *result; + GList *node; + int index; + + result = g_array_new (FALSE, TRUE, sizeof (GdkPoint)); + result = g_array_set_size (result, g_list_length (icons)); + + for (index = 0, node = icons; node != NULL; index++, node = node->next) + { + g_array_index (result, GdkPoint, index).x = + ((NautilusCanvasIcon *) node->data)->x; + g_array_index (result, GdkPoint, index).y = + ((NautilusCanvasIcon *) node->data)->y; + } + + return result; +} + +/* Returns a GdkRectangle of the icon. The bounding box is adjusted with the + * pixels_per_unit already, so they are the final positions on the canvas */ +GdkRectangle * +nautilus_canvas_container_get_icon_bounding_box (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasIcon *icon; + int x1, x2, y1, y2; + GdkRectangle *bounding_box; + + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), NULL); + g_return_val_if_fail (data != NULL, NULL); + + icon = g_hash_table_lookup (container->details->icon_set, data); + icon_get_bounding_box (icon, + &x1, &y1, &x2, &y2, + BOUNDS_USAGE_FOR_DISPLAY); + bounding_box = g_malloc0 (sizeof (GdkRectangle)); + bounding_box->x = x1 * EEL_CANVAS (container)->pixels_per_unit; + bounding_box->width = (x2 - x1) * EEL_CANVAS (container)->pixels_per_unit; + bounding_box->y = y1 * EEL_CANVAS (container)->pixels_per_unit; + bounding_box->height = (y2 - y1) * EEL_CANVAS (container)->pixels_per_unit; + + return bounding_box; +} + +/** + * nautilus_canvas_container_get_selected_icon_locations: + * @container: An canvas container widget. + * + * Returns an array of GdkPoints of locations of the selected icons. + **/ +GArray * +nautilus_canvas_container_get_selected_icon_locations (NautilusCanvasContainer *container) +{ + GArray *result; + GList *icons; + + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), NULL); + + icons = nautilus_canvas_container_get_selected_icons (container); + result = nautilus_canvas_container_get_icon_locations (container, icons); + g_list_free (icons); + + return result; +} + +/** + * nautilus_canvas_container_select_all: + * @container: An canvas container widget. + * + * Select all the icons in @container at once. + **/ +void +nautilus_canvas_container_select_all (NautilusCanvasContainer *container) +{ + gboolean selection_changed; + GList *p; + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection_changed = FALSE; + + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + selection_changed |= icon_set_selected (container, icon, TRUE); + } + + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +/** + * nautilus_canvas_container_select_first: + * @container: An canvas container widget. + * + * Select the first icon in @container. + **/ +void +nautilus_canvas_container_select_first (NautilusCanvasContainer *container) +{ + gboolean selection_changed; + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection_changed = FALSE; + + if (container->details->needs_resort) + { + resort (container); + container->details->needs_resort = FALSE; + } + + icon = g_list_nth_data (container->details->icons, 0); + if (icon) + { + selection_changed |= icon_set_selected (container, icon, TRUE); + } + + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +/** + * nautilus_canvas_container_set_selection: + * @container: An canvas container widget. + * @selection: A list of NautilusCanvasIconData *. + * + * Set the selection to exactly the icons in @container which have + * programmer data matching one of the items in @selection. + **/ +void +nautilus_canvas_container_set_selection (NautilusCanvasContainer *container, + GList *selection) +{ + gboolean selection_changed; + GHashTable *hash; + GList *p; + gboolean res; + NautilusCanvasIcon *icon, *selected_icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection_changed = FALSE; + selected_icon = NULL; + + hash = g_hash_table_new (NULL, NULL); + for (p = selection; p != NULL; p = p->next) + { + g_hash_table_insert (hash, p->data, p->data); + } + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + res = icon_set_selected + (container, icon, + g_hash_table_lookup (hash, icon->data) != NULL); + selection_changed |= res; + + if (res) + { + selected_icon = icon; + } + } + g_hash_table_destroy (hash); + + if (selection_changed) + { + /* if only one item has been selected, use it as range + * selection base (cf. handle_canvas_button_press) */ + if (g_list_length (selection) == 1) + { + container->details->range_selection_base_icon = selected_icon; + } + + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +/** + * nautilus_canvas_container_select_list_unselect_others. + * @container: An canvas container widget. + * @selection: A list of NautilusCanvasIcon *. + * + * Set the selection to exactly the icons in @selection. + **/ +void +nautilus_canvas_container_select_list_unselect_others (NautilusCanvasContainer *container, + GList *selection) +{ + gboolean selection_changed; + GHashTable *hash; + GList *p; + NautilusCanvasIcon *icon; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + selection_changed = FALSE; + + hash = g_hash_table_new (NULL, NULL); + for (p = selection; p != NULL; p = p->next) + { + g_hash_table_insert (hash, p->data, p->data); + } + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + + selection_changed |= icon_set_selected + (container, icon, + g_hash_table_lookup (hash, icon) != NULL); + } + g_hash_table_destroy (hash); + + if (selection_changed) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +/** + * nautilus_canvas_container_unselect_all: + * @container: An canvas container widget. + * + * Deselect all the icons in @container. + **/ +void +nautilus_canvas_container_unselect_all (NautilusCanvasContainer *container) +{ + if (unselect_all (container)) + { + g_signal_emit (container, + signals[SELECTION_CHANGED], 0); + } +} + +/** + * nautilus_canvas_container_get_icon_by_uri: + * @container: An canvas container widget. + * @uri: The uri of an canvas to find. + * + * Locate an icon, given the URI. The URI must match exactly. + * Later we may have to have some way of figuring out if the + * URI specifies the same object that does not require an exact match. + **/ +NautilusCanvasIcon * +nautilus_canvas_container_get_icon_by_uri (NautilusCanvasContainer *container, + const char *uri) +{ + NautilusCanvasContainerDetails *details; + GList *p; + + /* Eventually, we must avoid searching the entire canvas list, + * but it's OK for now. + * A hash table mapping uri to canvas is one possibility. + */ + + details = container->details; + + for (p = details->icons; p != NULL; p = p->next) + { + NautilusCanvasIcon *icon; + char *icon_uri; + gboolean is_match; + + icon = p->data; + + icon_uri = nautilus_canvas_container_get_icon_uri + (container, icon); + is_match = strcmp (uri, icon_uri) == 0; + g_free (icon_uri); + + if (is_match) + { + return icon; + } + } + + return NULL; +} + +static NautilusCanvasIcon * +get_nth_selected_icon (NautilusCanvasContainer *container, + int index) +{ + GList *p; + NautilusCanvasIcon *icon; + int selection_count; + + g_assert (index > 0); + + /* Find the nth selected icon. */ + selection_count = 0; + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + if (icon->is_selected) + { + if (++selection_count == index) + { + return icon; + } + } + } + return NULL; +} + +static NautilusCanvasIcon * +get_first_selected_icon (NautilusCanvasContainer *container) +{ + return get_nth_selected_icon (container, 1); +} + +static gboolean +has_multiple_selection (NautilusCanvasContainer *container) +{ + return get_nth_selected_icon (container, 2) != NULL; +} + +static gboolean +all_selected (NautilusCanvasContainer *container) +{ + GList *p; + NautilusCanvasIcon *icon; + + for (p = container->details->icons; p != NULL; p = p->next) + { + icon = p->data; + if (!icon->is_selected) + { + return FALSE; + } + } + return TRUE; +} + +static gboolean +has_selection (NautilusCanvasContainer *container) +{ + return get_nth_selected_icon (container, 1) != NULL; +} + +char * +nautilus_canvas_container_get_icon_uri (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + char *uri; + + uri = NULL; + g_signal_emit (container, + signals[GET_ICON_URI], 0, + icon->data, + &uri); + return uri; +} + +char * +nautilus_canvas_container_get_icon_activation_uri (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + char *uri; + + uri = NULL; + g_signal_emit (container, + signals[GET_ICON_ACTIVATION_URI], 0, + icon->data, + &uri); + return uri; +} + +char * +nautilus_canvas_container_get_icon_drop_target_uri (NautilusCanvasContainer *container, + NautilusCanvasIcon *icon) +{ + char *uri; + + uri = NULL; + g_signal_emit (container, + signals[GET_ICON_DROP_TARGET_URI], 0, + icon->data, + &uri); + return uri; +} + +/* Re-sort, switching to automatic layout if it was in manual layout. */ +void +nautilus_canvas_container_sort (NautilusCanvasContainer *container) +{ + container->details->needs_resort = TRUE; + redo_layout (container); +} + +void +nautilus_canvas_container_set_single_click_mode (NautilusCanvasContainer *container, + gboolean single_click_mode) +{ + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + container->details->single_click_mode = single_click_mode; +} + +/* handle theme changes */ + +void +nautilus_canvas_container_set_font (NautilusCanvasContainer *container, + const char *font) +{ + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + if (g_strcmp0 (container->details->font, font) == 0) + { + return; + } + + g_free (container->details->font); + container->details->font = g_strdup (font); + + nautilus_canvas_container_request_update_all_internal (container, TRUE); + gtk_widget_queue_draw (GTK_WIDGET (container)); +} + +/** + * nautilus_canvas_container_get_icon_description + * @container: An canvas container widget. + * @data: Icon data + * + * Gets the description for the icon. This function may return NULL. + **/ +char * +nautilus_canvas_container_get_icon_description (NautilusCanvasContainer *container, + NautilusCanvasIconData *data) +{ + NautilusCanvasContainerClass *klass; + + klass = NAUTILUS_CANVAS_CONTAINER_GET_CLASS (container); + + if (klass->get_icon_description) + { + return klass->get_icon_description (container, data); + } + else + { + return NULL; + } +} + +/** + * nautilus_canvas_container_set_highlighted_for_clipboard + * @container: An canvas container widget. + * @data: Canvas Data associated with all icons that should be highlighted. + * Others will be unhighlighted. + **/ +void +nautilus_canvas_container_set_highlighted_for_clipboard (NautilusCanvasContainer *container, + GList *clipboard_canvas_data) +{ + GList *l; + NautilusCanvasIcon *icon; + gboolean highlighted_for_clipboard; + + g_return_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container)); + + for (l = container->details->icons; l != NULL; l = l->next) + { + icon = l->data; + highlighted_for_clipboard = (g_list_find (clipboard_canvas_data, icon->data) != NULL); + + eel_canvas_item_set (EEL_CANVAS_ITEM (icon->item), + "highlighted-for-clipboard", highlighted_for_clipboard, + NULL); + } +} + +/* NautilusCanvasContainerAccessible */ +typedef struct +{ + EelCanvasAccessible parent; + NautilusCanvasContainerAccessiblePrivate *priv; +} NautilusCanvasContainerAccessible; + +typedef EelCanvasAccessibleClass NautilusCanvasContainerAccessibleClass; + +#define GET_ACCESSIBLE_PRIV(o) ((NautilusCanvasContainerAccessible *) o)->priv + +/* AtkAction interface */ +static gboolean +nautilus_canvas_container_accessible_do_action (AtkAction *accessible, + int i) +{ + GtkWidget *widget; + NautilusCanvasContainer *container; + GList *selection; + + g_return_val_if_fail (i < LAST_ACTION, FALSE); + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + switch (i) + { + case ACTION_ACTIVATE: + { + selection = nautilus_canvas_container_get_selection (container); + + if (selection) + { + g_signal_emit_by_name (container, "activate", selection); + g_list_free (selection); + } + } + break; + + case ACTION_MENU: + { + handle_popups (container, NULL, "context_click_background"); + } + break; + + default: + { + g_warning ("Invalid action passed to NautilusCanvasContainerAccessible::do_action"); + return FALSE; + } + break; + } + return TRUE; +} + +static int +nautilus_canvas_container_accessible_get_n_actions (AtkAction *accessible) +{ + return LAST_ACTION; +} + +static const char * +nautilus_canvas_container_accessible_action_get_description (AtkAction *accessible, + int i) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + + g_assert (i < LAST_ACTION); + + priv = GET_ACCESSIBLE_PRIV (accessible); + + if (priv->action_descriptions[i]) + { + return priv->action_descriptions[i]; + } + else + { + return nautilus_canvas_container_accessible_action_descriptions[i]; + } +} + +static const char * +nautilus_canvas_container_accessible_action_get_name (AtkAction *accessible, + int i) +{ + g_assert (i < LAST_ACTION); + + return nautilus_canvas_container_accessible_action_names[i]; +} + +static const char * +nautilus_canvas_container_accessible_action_get_keybinding (AtkAction *accessible, + int i) +{ + g_assert (i < LAST_ACTION); + + return NULL; +} + +static gboolean +nautilus_canvas_container_accessible_action_set_description (AtkAction *accessible, + int i, + const char *description) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + + g_assert (i < LAST_ACTION); + + priv = GET_ACCESSIBLE_PRIV (accessible); + + if (priv->action_descriptions[i]) + { + g_free (priv->action_descriptions[i]); + } + priv->action_descriptions[i] = g_strdup (description); + + return FALSE; +} + +static void +nautilus_canvas_container_accessible_action_interface_init (AtkActionIface *iface) +{ + iface->do_action = nautilus_canvas_container_accessible_do_action; + iface->get_n_actions = nautilus_canvas_container_accessible_get_n_actions; + iface->get_description = nautilus_canvas_container_accessible_action_get_description; + iface->get_name = nautilus_canvas_container_accessible_action_get_name; + iface->get_keybinding = nautilus_canvas_container_accessible_action_get_keybinding; + iface->set_description = nautilus_canvas_container_accessible_action_set_description; +} + +/* AtkSelection interface */ + +static void +nautilus_canvas_container_accessible_update_selection (AtkObject *accessible) +{ + NautilusCanvasContainer *container; + NautilusCanvasContainerAccessiblePrivate *priv; + + container = NAUTILUS_CANVAS_CONTAINER (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible))); + priv = GET_ACCESSIBLE_PRIV (accessible); + + if (priv->selection) + { + g_list_free (priv->selection); + priv->selection = NULL; + } + + priv->selection = nautilus_canvas_container_get_selected_icons (container); +} + +static void +nautilus_canvas_container_accessible_selection_changed_cb (NautilusCanvasContainer *container, + gpointer data) +{ + g_signal_emit_by_name (data, "selection-changed"); +} + +static void +nautilus_canvas_container_accessible_icon_added_cb (NautilusCanvasContainer *container, + NautilusCanvasIconData *icon_data, + gpointer data) +{ + NautilusCanvasIcon *icon; + AtkObject *atk_parent; + AtkObject *atk_child; + + /* We don't want to emit children_changed signals during any type of load. */ + if (!container->details->in_layout_now || container->details->is_populating_container) + { + return; + } + + icon = g_hash_table_lookup (container->details->icon_set, icon_data); + if (icon) + { + atk_parent = ATK_OBJECT (data); + atk_child = atk_gobject_accessible_for_object + (G_OBJECT (icon->item)); + + g_signal_emit_by_name (atk_parent, "children-changed::add", + icon->position, atk_child, NULL); + } +} + +static void +nautilus_canvas_container_accessible_icon_removed_cb (NautilusCanvasContainer *container, + NautilusCanvasIconData *icon_data, + gpointer data) +{ + NautilusCanvasIcon *icon; + AtkObject *atk_parent; + AtkObject *atk_child; + + icon = g_hash_table_lookup (container->details->icon_set, icon_data); + if (icon) + { + atk_parent = ATK_OBJECT (data); + atk_child = atk_gobject_accessible_for_object + (G_OBJECT (icon->item)); + + g_signal_emit_by_name (atk_parent, "children-changed::remove", + icon->position, atk_child, NULL); + } +} + +static void +nautilus_canvas_container_accessible_cleared_cb (NautilusCanvasContainer *container, + gpointer data) +{ + g_signal_emit_by_name (data, "children-changed", 0, NULL, NULL); +} + +static gboolean +nautilus_canvas_container_accessible_add_selection (AtkSelection *accessible, + int i) +{ + GtkWidget *widget; + NautilusCanvasContainer *container; + GList *l; + GList *selection; + NautilusCanvasIcon *icon; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + l = g_list_nth (container->details->icons, i); + if (l) + { + icon = l->data; + + selection = nautilus_canvas_container_get_selection (container); + selection = g_list_prepend (selection, + icon->data); + nautilus_canvas_container_set_selection (container, selection); + + g_list_free (selection); + return TRUE; + } + + return FALSE; +} + +static gboolean +nautilus_canvas_container_accessible_clear_selection (AtkSelection *accessible) +{ + GtkWidget *widget; + NautilusCanvasContainer *container; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + nautilus_canvas_container_unselect_all (container); + + return TRUE; +} + +static AtkObject * +nautilus_canvas_container_accessible_ref_selection (AtkSelection *accessible, + int i) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + AtkObject *atk_object; + GList *item; + NautilusCanvasIcon *icon; + + nautilus_canvas_container_accessible_update_selection (ATK_OBJECT (accessible)); + priv = GET_ACCESSIBLE_PRIV (accessible); + + item = (g_list_nth (priv->selection, i)); + + if (item) + { + icon = item->data; + atk_object = atk_gobject_accessible_for_object (G_OBJECT (icon->item)); + if (atk_object) + { + g_object_ref (atk_object); + } + + return atk_object; + } + else + { + return NULL; + } +} + +static int +nautilus_canvas_container_accessible_get_selection_count (AtkSelection *accessible) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + int count; + + priv = GET_ACCESSIBLE_PRIV (accessible); + nautilus_canvas_container_accessible_update_selection (ATK_OBJECT (accessible)); + count = g_list_length (priv->selection); + + return count; +} + +static gboolean +nautilus_canvas_container_accessible_is_child_selected (AtkSelection *accessible, + int i) +{ + NautilusCanvasContainer *container; + GList *l; + NautilusCanvasIcon *icon; + GtkWidget *widget; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + l = g_list_nth (container->details->icons, i); + if (l) + { + icon = l->data; + return icon->is_selected; + } + return FALSE; +} + +static gboolean +nautilus_canvas_container_accessible_remove_selection (AtkSelection *accessible, + int i) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + NautilusCanvasContainer *container; + GList *l; + GList *selection; + NautilusCanvasIcon *icon; + GtkWidget *widget; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + nautilus_canvas_container_accessible_update_selection (ATK_OBJECT (accessible)); + + priv = GET_ACCESSIBLE_PRIV (accessible); + l = g_list_nth (priv->selection, i); + if (l) + { + icon = l->data; + + selection = nautilus_canvas_container_get_selection (container); + selection = g_list_remove (selection, icon->data); + nautilus_canvas_container_set_selection (container, selection); + + g_list_free (selection); + return TRUE; + } + + return FALSE; +} + +static gboolean +nautilus_canvas_container_accessible_select_all_selection (AtkSelection *accessible) +{ + NautilusCanvasContainer *container; + GtkWidget *widget; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + nautilus_canvas_container_select_all (container); + + return TRUE; +} + +void +nautilus_canvas_container_widget_to_file_operation_position (NautilusCanvasContainer *container, + GdkPoint *position) +{ + double x, y; + + g_return_if_fail (position != NULL); + + x = position->x; + y = position->y; + + eel_canvas_window_to_world (EEL_CANVAS (container), x, y, &x, &y); + + position->x = (int) x; + position->y = (int) y; + + /* ensure that we end up in the middle of the icon */ + position->x -= nautilus_canvas_container_get_icon_size_for_zoom_level (container->details->zoom_level) / 2; + position->y -= nautilus_canvas_container_get_icon_size_for_zoom_level (container->details->zoom_level) / 2; +} + +static void +nautilus_canvas_container_accessible_selection_interface_init (AtkSelectionIface *iface) +{ + iface->add_selection = nautilus_canvas_container_accessible_add_selection; + iface->clear_selection = nautilus_canvas_container_accessible_clear_selection; + iface->ref_selection = nautilus_canvas_container_accessible_ref_selection; + iface->get_selection_count = nautilus_canvas_container_accessible_get_selection_count; + iface->is_child_selected = nautilus_canvas_container_accessible_is_child_selected; + iface->remove_selection = nautilus_canvas_container_accessible_remove_selection; + iface->select_all_selection = nautilus_canvas_container_accessible_select_all_selection; +} + + +static gint +nautilus_canvas_container_accessible_get_n_children (AtkObject *accessible) +{ + NautilusCanvasContainer *container; + GtkWidget *widget; + gint i; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return FALSE; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + i = g_hash_table_size (container->details->icon_set); + + return i; +} + +static AtkObject * +nautilus_canvas_container_accessible_ref_child (AtkObject *accessible, + int i) +{ + AtkObject *atk_object; + NautilusCanvasContainer *container; + GList *item; + NautilusCanvasIcon *icon; + GtkWidget *widget; + + widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)); + if (!widget) + { + return NULL; + } + + container = NAUTILUS_CANVAS_CONTAINER (widget); + + item = (g_list_nth (container->details->icons, i)); + + if (item) + { + icon = item->data; + + atk_object = atk_gobject_accessible_for_object (G_OBJECT (icon->item)); + g_object_ref (atk_object); + + return atk_object; + } + return NULL; +} + +G_DEFINE_TYPE_WITH_CODE (NautilusCanvasContainerAccessible, nautilus_canvas_container_accessible, + eel_canvas_accessible_get_type (), + G_IMPLEMENT_INTERFACE (ATK_TYPE_ACTION, nautilus_canvas_container_accessible_action_interface_init) + G_IMPLEMENT_INTERFACE (ATK_TYPE_SELECTION, nautilus_canvas_container_accessible_selection_interface_init)) + +static void +nautilus_canvas_container_accessible_initialize (AtkObject *accessible, + gpointer data) +{ + NautilusCanvasContainer *container; + + if (ATK_OBJECT_CLASS (nautilus_canvas_container_accessible_parent_class)->initialize) + { + ATK_OBJECT_CLASS (nautilus_canvas_container_accessible_parent_class)->initialize (accessible, data); + } + + if (GTK_IS_ACCESSIBLE (accessible)) + { + nautilus_canvas_container_accessible_update_selection + (ATK_OBJECT (accessible)); + + container = NAUTILUS_CANVAS_CONTAINER (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible))); + g_signal_connect (container, "selection-changed", + G_CALLBACK (nautilus_canvas_container_accessible_selection_changed_cb), + accessible); + g_signal_connect (container, "icon-added", + G_CALLBACK (nautilus_canvas_container_accessible_icon_added_cb), + accessible); + g_signal_connect (container, "icon-removed", + G_CALLBACK (nautilus_canvas_container_accessible_icon_removed_cb), + accessible); + g_signal_connect (container, "cleared", + G_CALLBACK (nautilus_canvas_container_accessible_cleared_cb), + accessible); + } +} + +static void +nautilus_canvas_container_accessible_finalize (GObject *object) +{ + NautilusCanvasContainerAccessiblePrivate *priv; + int i; + + priv = GET_ACCESSIBLE_PRIV (object); + + if (priv->selection) + { + g_list_free (priv->selection); + } + + for (i = 0; i < LAST_ACTION; i++) + { + if (priv->action_descriptions[i]) + { + g_free (priv->action_descriptions[i]); + } + } + + G_OBJECT_CLASS (nautilus_canvas_container_accessible_parent_class)->finalize (object); +} + +static void +nautilus_canvas_container_accessible_init (NautilusCanvasContainerAccessible *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, nautilus_canvas_container_accessible_get_type (), + NautilusCanvasContainerAccessiblePrivate); +} + +static void +nautilus_canvas_container_accessible_class_init (NautilusCanvasContainerAccessibleClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = nautilus_canvas_container_accessible_finalize; + + atk_class->get_n_children = nautilus_canvas_container_accessible_get_n_children; + atk_class->ref_child = nautilus_canvas_container_accessible_ref_child; + atk_class->initialize = nautilus_canvas_container_accessible_initialize; + + g_type_class_add_private (klass, sizeof (NautilusCanvasContainerAccessiblePrivate)); +} + +gboolean +nautilus_canvas_container_is_layout_rtl (NautilusCanvasContainer *container) +{ + g_return_val_if_fail (NAUTILUS_IS_CANVAS_CONTAINER (container), 0); + + return (gtk_widget_get_direction (GTK_WIDGET (container)) == GTK_TEXT_DIR_RTL); +} + +int +nautilus_canvas_container_get_max_layout_lines_for_pango (NautilusCanvasContainer *container) +{ + int limit; + + limit = text_ellipsis_limits[container->details->zoom_level]; + + if (limit <= 0) + { + return G_MININT; + } + + return -limit; +} + +int +nautilus_canvas_container_get_max_layout_lines (NautilusCanvasContainer *container) +{ + int limit; + + limit = text_ellipsis_limits[container->details->zoom_level]; + + if (limit <= 0) + { + return G_MAXINT; + } + + return limit; +} |