Commit 9fd0e218 authored by Cosimo Cecchi's avatar Cosimo Cecchi

list-view: add back a treeview option

Re-add most of the code that handled the tree view in NautilusListModel
and NautilusListView, cleaned up and rebased to master.

The tree mode will be activated by a checkbox in the Preferences dialog.
parent d2c4cad5
......@@ -34,6 +34,7 @@
#include "nautilus-file-dnd.h"
#include "nautilus-file-changes-queue.h"
#include "nautilus-global-preferences.h"
#include "nautilus-link.h"
#include <gtk/gtk.h>
......@@ -45,6 +46,7 @@
#include "nautilus-debug.h"
#define AUTO_SCROLL_MARGIN 20
#define HOVER_EXPAND_TIMEOUT 1
struct _NautilusTreeViewDragDestDetails {
GtkTreeView *tree_view;
......@@ -59,6 +61,7 @@ struct _NautilusTreeViewDragDestDetails {
guint hover_id;
guint highlight_id;
guint scroll_id;
guint expand_id;
char *direct_save_uri;
char *target_uri;
......@@ -147,6 +150,33 @@ remove_scroll_timeout (NautilusTreeViewDragDest *dest)
}
}
static int
expand_timeout (gpointer data)
{
GtkTreeView *tree_view;
GtkTreePath *drop_path;
tree_view = GTK_TREE_VIEW (data);
gtk_tree_view_get_drag_dest_row (tree_view, &drop_path, NULL);
if (drop_path) {
gtk_tree_view_expand_row (tree_view, drop_path, FALSE);
gtk_tree_path_free (drop_path);
}
return FALSE;
}
static void
remove_expand_timer (NautilusTreeViewDragDest *dest)
{
if (dest->details->expand_id) {
g_source_remove (dest->details->expand_id);
dest->details->expand_id = 0;
}
}
static gboolean
highlight_draw (GtkWidget *widget,
cairo_t *cr,
......@@ -281,6 +311,7 @@ free_drag_data (NautilusTreeViewDragDest *dest)
dest->details->target_uri = NULL;
remove_hover_timer (dest);
remove_expand_timer (dest);
}
static gboolean
......@@ -322,6 +353,33 @@ check_hover_timer (NautilusTreeViewDragDest *dest,
}
}
static void
check_expand_timer (NautilusTreeViewDragDest *dest,
GtkTreePath *drop_path,
GtkTreePath *old_drop_path)
{
GtkTreeModel *model;
GtkTreeIter drop_iter;
model = gtk_tree_view_get_model (dest->details->tree_view);
if (drop_path == NULL ||
(old_drop_path != NULL && gtk_tree_path_compare (old_drop_path, drop_path) != 0)) {
remove_expand_timer (dest);
}
if (dest->details->expand_id == 0 &&
drop_path != NULL) {
gtk_tree_model_get_iter (model, &drop_iter, drop_path);
if (gtk_tree_model_iter_has_child (model, &drop_iter)) {
dest->details->expand_id =
g_timeout_add_seconds (HOVER_EXPAND_TIMEOUT,
expand_timeout,
dest->details->tree_view);
}
}
}
static char *
get_root_uri (NautilusTreeViewDragDest *dest)
{
......@@ -354,6 +412,48 @@ file_for_path (NautilusTreeViewDragDest *dest, GtkTreePath *path)
return file;
}
static char *
get_drop_target_uri_for_path (NautilusTreeViewDragDest *dest,
GtkTreePath *path)
{
NautilusFile *file;
char *target = NULL;
gboolean can;
file = file_for_path (dest, path);
if (file == NULL) {
return NULL;
}
can = nautilus_drag_can_accept_info (file,
dest->details->drag_type,
dest->details->drag_list);
if (can) {
target = nautilus_file_get_drop_target_uri (file);
}
nautilus_file_unref (file);
return target;
}
static void
check_hover_expand_timer (NautilusTreeViewDragDest *dest,
GtkTreePath *path,
GtkTreePath *drop_path,
GtkTreePath *old_drop_path)
{
gboolean use_tree = g_settings_get_boolean (nautilus_list_view_preferences,
NAUTILUS_PREFERENCES_LIST_VIEW_USE_TREE);
if (use_tree) {
check_expand_timer (dest, drop_path, old_drop_path);
} else {
char *uri;
uri = get_drop_target_uri_for_path (dest, path);
check_hover_timer (dest, uri);
g_free (uri);
}
}
static GtkTreePath *
get_drop_path (NautilusTreeViewDragDest *dest,
GtkTreePath *path)
......@@ -389,29 +489,6 @@ get_drop_path (NautilusTreeViewDragDest *dest,
return ret;
}
static char *
get_drop_target_uri_for_path (NautilusTreeViewDragDest *dest,
GtkTreePath *path)
{
NautilusFile *file;
char *target = NULL;
gboolean can;
file = file_for_path (dest, path);
if (file == NULL) {
return NULL;
}
can = nautilus_drag_can_accept_info (file,
dest->details->drag_type,
dest->details->drag_list);
if (can) {
target = nautilus_file_get_drop_target_uri (file);
}
nautilus_file_unref (file);
return target;
}
static guint
get_drop_action (NautilusTreeViewDragDest *dest,
GdkDragContext *context,
......@@ -509,14 +586,12 @@ drag_motion_callback (GtkWidget *widget,
NULL);
if (action) {
char *uri;
set_drag_dest_row (dest, drop_path);
uri = get_drop_target_uri_for_path (dest, path);
check_hover_timer (dest, uri);
g_free (uri);
check_hover_expand_timer (dest, path, drop_path, old_drop_path);
} else {
clear_drag_dest_row (dest);
remove_hover_timer (dest);
remove_expand_timer (dest);
}
if (path) {
......
......@@ -397,31 +397,39 @@ nautilus_list_model_iter_children (GtkTreeModel *tree_model, GtkTreeIter *iter,
{
NautilusListModel *model;
GSequence *files;
FileEntry *file_entry;
model = (NautilusListModel *)tree_model;
/* this is a list, nodes have no children */
if (parent != NULL) {
iter->stamp = 0;
return FALSE;
if (parent == NULL) {
files = model->details->files;
} else {
file_entry = g_sequence_get (parent->user_data);
files = file_entry->files;
}
files = model->details->files;
if (g_sequence_get_length (files) > 0) {
iter->stamp = model->details->stamp;
iter->user_data = g_sequence_get_begin_iter (files);
return TRUE;
} else {
iter->stamp = 0;
if (files == NULL || g_sequence_get_length (files) == 0) {
return FALSE;
}
iter->stamp = model->details->stamp;
iter->user_data = g_sequence_get_begin_iter (files);
return TRUE;
}
static gboolean
nautilus_list_model_iter_has_child (GtkTreeModel *tree_model, GtkTreeIter *iter)
{
return FALSE;
FileEntry *file_entry;
if (iter == NULL) {
return !nautilus_list_model_is_empty (NAUTILUS_LIST_MODEL (tree_model));
}
file_entry = g_sequence_get (iter->user_data);
return (file_entry->files != NULL && g_sequence_get_length (file_entry->files) > 0);
}
static int
......@@ -429,13 +437,15 @@ nautilus_list_model_iter_n_children (GtkTreeModel *tree_model, GtkTreeIter *iter
{
NautilusListModel *model;
GSequence *files;
FileEntry *file_entry;
model = (NautilusListModel *)tree_model;
if (iter == NULL) {
files = model->details->files;
} else {
return 0;
file_entry = g_sequence_get (iter->user_data);
files = file_entry->files;
}
return g_sequence_get_length (files);
......@@ -447,16 +457,17 @@ nautilus_list_model_iter_nth_child (GtkTreeModel *tree_model, GtkTreeIter *iter,
NautilusListModel *model;
GSequenceIter *child;
GSequence *files;
FileEntry *file_entry;
model = (NautilusListModel *)tree_model;
iter->stamp = 0;
if (parent != NULL) {
return FALSE;
file_entry = g_sequence_get (parent->user_data);
files = file_entry->files;
} else {
files = model->details->files;
}
files = model->details->files;
child = g_sequence_get_iter_at_pos (files, n);
if (g_sequence_iter_is_end (child)) {
......@@ -472,9 +483,21 @@ nautilus_list_model_iter_nth_child (GtkTreeModel *tree_model, GtkTreeIter *iter,
static gboolean
nautilus_list_model_iter_parent (GtkTreeModel *tree_model, GtkTreeIter *iter, GtkTreeIter *child)
{
iter->stamp = 0;
NautilusListModel *model;
FileEntry *file_entry;
return FALSE;
model = (NautilusListModel *)tree_model;
file_entry = g_sequence_get (child->user_data);
if (file_entry->parent == NULL) {
return FALSE;
}
iter->stamp = model->details->stamp;
iter->user_data = file_entry->parent->ptr;
return TRUE;
}
static GSequenceIter *
......
......@@ -88,6 +88,7 @@ struct NautilusListViewDetails {
int drag_y;
gboolean drag_started;
gboolean ignore_button_release;
gboolean row_selected_on_button_down;
gboolean menus_ready;
gboolean active;
......@@ -118,6 +119,9 @@ struct SelectionForeachData {
*/
#define LIST_VIEW_MINIMUM_ROW_HEIGHT 28
/* We wait two seconds after row is collapsed to unload the subdirectory */
#define COLLAPSE_TO_UNLOAD_DELAY 2
/* Wait for the rename to end when activating a file being renamed */
#define WAIT_FOR_RENAME_ON_ACTIVATE 200
......@@ -634,13 +638,16 @@ button_press_callback (GtkWidget *widget, GdkEventButton *event, gpointer callba
NautilusListView *view;
GtkTreeView *tree_view;
GtkTreePath *path;
gboolean call_parent;
GtkTreeSelection *selection;
GtkWidgetClass *tree_view_class;
gint64 current_time;
static gint64 last_click_time = 0;
static int click_count = 0;
int double_click_time;
int expander_size, horizontal_separator;
gboolean call_parent, on_expander;
gboolean is_simple_click, path_selected;
NautilusFile *file;
view = NAUTILUS_LIST_VIEW (callback_data);
tree_view = GTK_TREE_VIEW (widget);
......@@ -681,141 +688,146 @@ button_press_callback (GtkWidget *widget, GdkEventButton *event, gpointer callba
return TRUE;
}
call_parent = TRUE;
if (gtk_tree_view_get_path_at_pos (tree_view, event->x, event->y,
&path, NULL, NULL, NULL)) {
/* Keep track of path of last click so double clicks only happen
* on the same item */
if ((event->button == 1 || event->button == 2) &&
event->type == GDK_BUTTON_PRESS) {
if (view->details->double_click_path[1]) {
gtk_tree_path_free (view->details->double_click_path[1]);
}
view->details->ignore_button_release = FALSE;
is_simple_click = ((event->button == 1 || event->button == 2) && (event->type == GDK_BUTTON_PRESS));
/* No item at this position */
if (!gtk_tree_view_get_path_at_pos (tree_view, event->x, event->y,
&path, NULL, NULL, NULL)) {
if (is_simple_click) {
g_clear_pointer (&view->details->double_click_path[1], gtk_tree_path_free);
view->details->double_click_path[1] = view->details->double_click_path[0];
view->details->double_click_path[0] = gtk_tree_path_copy (path);
view->details->double_click_path[0] = NULL;
}
if (event->type == GDK_2BUTTON_PRESS) {
/* Double clicking does not trigger a D&D action. */
view->details->drag_button = 0;
if (view->details->double_click_path[1] &&
gtk_tree_path_compare (view->details->double_click_path[0], view->details->double_click_path[1]) == 0) {
/* NOTE: Activation can actually destroy the view if we're switching */
if (!button_event_modifies_selection (event)) {
if ((event->button == 1 || event->button == 3)) {
activate_selected_items (view);
} else if (event->button == 2) {
activate_selected_items_alternate (view, NULL, TRUE);
}
} else if (event->button == 1 &&
(event->state & GDK_SHIFT_MASK) != 0) {
NautilusFile *file;
file = nautilus_list_model_file_for_path (view->details->model, path);
if (file != NULL) {
activate_selected_items_alternate (view, file, TRUE);
nautilus_file_unref (file);
}
/* Deselect if people click outside any row. It's OK to
let default code run; it won't reselect anything. */
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
tree_view_class->button_press_event (widget, event);
if (event->button == 3) {
do_popup_menu (widget, view, event);
}
return TRUE;
}
call_parent = TRUE;
path_selected = gtk_tree_selection_path_is_selected (selection, path);
gtk_widget_style_get (widget,
"expander-size", &expander_size,
"horizontal-separator", &horizontal_separator,
NULL);
/* TODO we should not hardcode this extra padding. It is
* EXPANDER_EXTRA_PADDING from GtkTreeView.
*/
expander_size += 4;
on_expander = (event->x <= horizontal_separator / 2 +
gtk_tree_path_get_depth (path) * expander_size);
/* Keep track of path of last click so double clicks only happen
* on the same item */
if (is_simple_click) {
g_clear_pointer (&view->details->double_click_path[1], gtk_tree_path_free);
view->details->double_click_path[1] = view->details->double_click_path[0];
view->details->double_click_path[0] = gtk_tree_path_copy (path);
}
if (event->type == GDK_2BUTTON_PRESS) {
/* Double clicking does not trigger a D&D action. */
view->details->drag_button = 0;
/* NOTE: Activation can actually destroy the view if we're switching */
if (!on_expander &&
view->details->double_click_path[1] &&
gtk_tree_path_compare (view->details->double_click_path[0], view->details->double_click_path[1]) == 0) {
if ((event->button == 1) && button_event_modifies_selection (event)) {
file = nautilus_list_model_file_for_path (view->details->model, path);
if (file != NULL) {
activate_selected_items_alternate (view, file, TRUE);
nautilus_file_unref (file);
}
} else {
tree_view_class->button_press_event (widget, event);
if ((event->button == 1 || event->button == 3)) {
activate_selected_items (view);
} else if (event->button == 2) {
activate_selected_items_alternate (view, NULL, TRUE);
}
}
} else {
/* We're going to filter out some situations where
* we can't let the default code run because all
* but one row would be would be deselected. We don't
* want that; we want the right click menu or single
* click to apply to everything that's currently selected. */
tree_view_class->button_press_event (widget, event);
}
} else {
/* We're going to filter out some situations where
* we can't let the default code run because all
* but one row would be would be deselected. We don't
* want that; we want the right click menu or single
* click to apply to everything that's currently selected.
*/
if (event->button == 3 && path_selected) {
call_parent = FALSE;
}
if (event->button == 3 && gtk_tree_selection_path_is_selected (selection, path)) {
call_parent = FALSE;
}
if ((event->button == 1 || event->button == 2) &&
((event->state & GDK_CONTROL_MASK) != 0 || (event->state & GDK_SHIFT_MASK) == 0)) {
view->details->row_selected_on_button_down = path_selected;
if (path_selected) {
call_parent = on_expander;
view->details->ignore_button_release = on_expander;
} else if ((event->state & GDK_CONTROL_MASK) != 0) {
GList *selected_rows, *l;
if ((event->button == 1 || event->button == 2) &&
((event->state & GDK_CONTROL_MASK) != 0 ||
(event->state & GDK_SHIFT_MASK) == 0)) {
view->details->row_selected_on_button_down = gtk_tree_selection_path_is_selected (selection, path);
if (view->details->row_selected_on_button_down) {
call_parent = FALSE;
} else if ((event->state & GDK_CONTROL_MASK) != 0) {
GList *selected_rows;
GList *l;
call_parent = FALSE;
if ((event->state & GDK_SHIFT_MASK) != 0) {
GtkTreePath *cursor;
gtk_tree_view_get_cursor (tree_view, &cursor, NULL);
if (cursor != NULL) {
gtk_tree_selection_select_range (selection, cursor, path);
} else {
gtk_tree_selection_select_path (selection, path);
}
call_parent = FALSE;
if ((event->state & GDK_SHIFT_MASK) != 0) {
GtkTreePath *cursor;
gtk_tree_view_get_cursor (tree_view, &cursor, NULL);
if (cursor != NULL) {
gtk_tree_selection_select_range (selection, cursor, path);
} else {
gtk_tree_selection_select_path (selection, path);
}
selected_rows = gtk_tree_selection_get_selected_rows (selection, NULL);
} else {
gtk_tree_selection_select_path (selection, path);
}
selected_rows = gtk_tree_selection_get_selected_rows (selection, NULL);
/* This unselects everything */
gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE);
/* This unselects everything */
gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE);
/* So select it again */
l = selected_rows;
while (l != NULL) {
GtkTreePath *p = l->data;
l = l->next;
gtk_tree_selection_select_path (selection, p);
gtk_tree_path_free (p);
}
g_list_free (selected_rows);
/* So select it again */
for (l = selected_rows; l != NULL; l = l->next) {
gtk_tree_selection_select_path (selection, l->data);
}
g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free);
} else {
view->details->ignore_button_release = on_expander;
}
}
if (call_parent) {
g_signal_handlers_block_by_func (tree_view,
row_activated_callback,
view);
tree_view_class->button_press_event (widget, event);
g_signal_handlers_unblock_by_func (tree_view,
row_activated_callback,
view);
} else if (gtk_tree_selection_path_is_selected (selection, path)) {
gtk_widget_grab_focus (widget);
}
if ((event->button == 1 || event->button == 2) &&
event->type == GDK_BUTTON_PRESS) {
view->details->drag_started = FALSE;
view->details->drag_button = event->button;
view->details->drag_x = event->x;
view->details->drag_y = event->y;
}
if (event->button == 3) {
do_popup_menu (widget, view, event);
}
if (call_parent) {
g_signal_handlers_block_by_func (tree_view, row_activated_callback, view);
tree_view_class->button_press_event (widget, event);
g_signal_handlers_unblock_by_func (tree_view, row_activated_callback, view);
} else if (path_selected) {
gtk_widget_grab_focus (widget);
}
gtk_tree_path_free (path);
} else {
if ((event->button == 1 || event->button == 2) &&
event->type == GDK_BUTTON_PRESS) {
if (view->details->double_click_path[1]) {
gtk_tree_path_free (view->details->double_click_path[1]);
}
view->details->double_click_path[1] = view->details->double_click_path[0];
view->details->double_click_path[0] = NULL;
if (is_simple_click) {
view->details->drag_started = FALSE;
view->details->drag_button = event->button;
view->details->drag_x = event->x;
view->details->drag_y = event->y;
}
/* Deselect if people click outside any row. It's OK to
let default code run; it won't reselect anything. */
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
tree_view_class->button_press_event (widget, event);
if (event->button == 3) {
do_popup_menu (widget, view, event);
}
}
gtk_tree_path_free (path);
/* We chained to the default handler in this method, so never
* let the default handler run */
return TRUE;
......@@ -832,7 +844,8 @@ button_release_callback (GtkWidget *widget,
if (event->button == view->details->drag_button) {
stop_drag_check (view);
if (!view->details->drag_started) {
if (!view->details->drag_started &&
!view->details->ignore_button_release) {
nautilus_list_view_did_not_drag (view, event);
}
}
......@@ -857,11 +870,148 @@ subdirectory_done_loading_callback (NautilusDirectory *directory, NautilusListVi
nautilus_list_model_subdirectory_done_loading (view->details->model, directory);
}
struct UnloadDelayData {
static void
row_expanded_callback (GtkTreeView *treeview,
GtkTreeIter *iter,
GtkTreePath *path,
gpointer callback_data)
{
NautilusListView *view;
NautilusDirectory *directory;
char *uri;
view = NAUTILUS_LIST_VIEW (callback_data);
if (!nautilus_list_model_load_subdirectory (view->details->model, path, &directory)) {
return;
}
uri = nautilus_directory_get_uri (directory);
DEBUG ("Row expaded callback for uri %s", uri);
g_free (uri);
nautilus_view_add_subdirectory (NAUTILUS_VIEW (view), directory);
if (nautilus_directory_are_all_files_seen (directory)) {
nautilus_list_model_subdirectory_done_loading (view->details->model,
directory);
} else {
g_signal_connect_object (directory, "done-loading",
G_CALLBACK (subdirectory_done_loading_callback),
view, 0);
}
nautilus_directory_unref (directory);
}
typedef struct {
NautilusFile *file;
NautilusDirectory *directory;
NautilusListView *view;
};
} UnloadDelayData;
static void
unload_delay_data_free (UnloadDelayData *unload_data)
{
if (unload_data->view != NULL) {
g_object_remove_weak_pointer (G_OBJECT (unload_data->view),
(gpointer *) &unload_data->view);
}
nautilus_directory_unref (unload_data->directory);
nautilus_file_unref (unload_data->file);
g_slice_free (UnloadDelayData, unload_data);
}
static UnloadDelayData *
unload_delay_data_new (NautilusFile *file,
NautilusDirectory *parent_directory,
NautilusListView *view)
{
UnloadDelayData *unload_data;
unload_data = g_slice_new0 (UnloadDelayData);
unload_data->view = view;
unload_data->file = nautilus_file_ref (file);
unload_data->directory = nautilus_directory_ref (parent_directory);
g_object_add_weak_pointer (G_OBJECT (unload_data->view),
(gpointer *) &unload_data->view);
return unload_data;
}
static gboolean
unload_file_timeout (gpointer data)
{
UnloadDelayData *unload_data = data;
GtkTreeIter iter;
NautilusListModel *model;
GtkTreePath *path;
if (unload_data->view == NULL) {
goto out;
}
model = unload_data->view->details->model;
if (nautilus_list_model_get_tree_iter_from_file (model,
unload_data->file,
unload_data->directory,
&iter)) {
path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter);
if (!gtk_tree_view_row_expanded (unload_data->view->details->tree_view,
path)) {
nautilus_list_model_unload_subdirectory (model, &iter);
}
gtk_tree_path_free (path);
}
out:
unload_delay_data_free (unload_data);
return FALSE;
}
static void
row_collapsed_callback (GtkTreeView *treeview,
GtkTreeIter *iter,
GtkTreePath *path,
gpointer callback_data)
{
NautilusListView *view;
NautilusFile *file;
NautilusDirectory *directory;
GtkTreeIter parent;
UnloadDelayData *unload_data;
GtkTreeModel *model;
char *uri;
view = NAUTILUS_LIST_VIEW (callback_data);
model = GTK_TREE_MODEL (view->details->model);