/* * Tabbed notebook * * Copyright (C) 2017, 2018 Patrick McDermott * * This file is part of Marquee. * * Marquee is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Marquee 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Marquee. If not, see . */ #include "notebook.h" #include #include #include "i18n.h" #include "tab-label.h" #include "tab-page.h" #include "tree.h" #include "window.h" #define MQ_TAB_TREE(obj) ((MqTabTree *) (obj)) typedef struct { MqTree parent_instance; MqTabLabel *label; MqTabPage *page; } MqTabTree; struct _MqNotebook { GtkNotebook parent_instance; MqWindow *window; MqTabTree *tree; MqTabPage *find_page; MqTabTree *found_node; MqTabPage *current_page; GtkWidget *tab_tree_popover; gboolean (*foreach_label_cb)(MqTabLabel *label, gpointer user_data); gboolean (*foreach_page_cb)(MqTabPage *page, gpointer user_data); gpointer foreach_data; }; enum { PROP_WINDOW = 1, N_PROPERTIES }; static GParamSpec *obj_properties[N_PROPERTIES] = {NULL,}; struct _MqNotebookClass { GtkNotebookClass parent_class; }; G_DEFINE_TYPE(MqNotebook, mq_notebook, GTK_TYPE_NOTEBOOK) static void get_property(GObject *object, guint property_id, GValue *value, GParamSpec *param_spec) { MqNotebook *notebook; notebook = MQ_NOTEBOOK(object); switch (property_id) { case PROP_WINDOW: g_value_set_object(value, notebook->window); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, param_spec); break; } } static void set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *param_spec) { MqNotebook *notebook; notebook = MQ_NOTEBOOK(object); switch (property_id) { case PROP_WINDOW: notebook->window = g_value_get_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, param_spec); break; } } static void mq_notebook_class_init(MqNotebookClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->get_property = get_property; object_class->set_property = set_property; obj_properties[PROP_WINDOW] = g_param_spec_object( "window", P_("MqWindow"), P_("The parent MqWindow instance"), MQ_TYPE_WINDOW, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB); g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties); } static void switch_page_cb(MqNotebook *notebook, MqTabPage *page, guint G_GNUC_UNUSED page_num) { notebook->current_page = page; mq_window_set_title(notebook->window, mq_tab_page_get_title(page)); } static void new_tab_clicked_cb(GtkWidget G_GNUC_UNUSED *button, MqNotebook *notebook) { mq_notebook_insert_sibling(notebook, NULL, notebook->current_page, TRUE); } static void create_tab_tree_model_recurse(MqNotebook *notebook, MqTabTree *node, GtkTreeStore *tree_store, GtkTreeIter *parent_tree_iter, GtkTreeIter **select_tree_iter) { GtkTreeIter tree_iter; for (; node; node = MQ_TAB_TREE(mq_tree_next(node))) { gtk_tree_store_append(tree_store, &tree_iter, parent_tree_iter); gtk_tree_store_set(tree_store, &tree_iter, 0, mq_tree_position(node), 1, mq_tab_page_get_title(node->page), -1); if (node->page == notebook->current_page) { *select_tree_iter = gtk_tree_iter_copy(&tree_iter); } create_tab_tree_model_recurse(notebook, MQ_TAB_TREE(mq_tree_first_child(node)), tree_store, &tree_iter, select_tree_iter); } } static GtkTreeModel * create_tab_tree_model(MqNotebook *notebook, GtkTreeIter **select_tree_iter) { GtkTreeStore *tree_store; tree_store = gtk_tree_store_new(2, G_TYPE_INT, G_TYPE_STRING); create_tab_tree_model_recurse(notebook, MQ_TAB_TREE(mq_tree_first_child(notebook->tree)), tree_store, NULL, select_tree_iter); return GTK_TREE_MODEL(tree_store); } static void tab_tree_row_activated_cb(GtkTreeView *tree_view, GtkTreePath *tree_path, GtkTreeViewColumn G_GNUC_UNUSED *tree_view_column, MqNotebook *notebook) { GtkTreeModel *tree_model; GtkTreeIter tree_iter; gint position; tree_model = gtk_tree_view_get_model(tree_view); if (gtk_tree_model_get_iter(tree_model, &tree_iter, tree_path)) { gtk_tree_model_get(tree_model, &tree_iter, 0, &position, -1); mq_notebook_set_current_page(notebook, position); } else { g_assert_not_reached(); } gtk_widget_hide(notebook->tab_tree_popover); } static GtkWidget * create_tab_tree_view(MqNotebook *notebook) { GtkTreeModel *tree_model; GtkTreeIter *select_tree_iter; GtkWidget *tree_view; GtkTreeSelection *tree_selection; GtkCellRenderer *cell_renderer; tree_model = create_tab_tree_model(notebook, &select_tree_iter); tree_view = gtk_tree_view_new_with_model(tree_model); gtk_tree_view_expand_all(GTK_TREE_VIEW(tree_view)); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), FALSE); gtk_tree_view_set_activate_on_single_click(GTK_TREE_VIEW(tree_view), TRUE); gtk_tree_view_set_reorderable(GTK_TREE_VIEW(tree_view), TRUE); gtk_tree_view_set_enable_tree_lines(GTK_TREE_VIEW(tree_view), TRUE); g_signal_connect(tree_view, "row-activated", G_CALLBACK(tab_tree_row_activated_cb), notebook); tree_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view)); gtk_tree_selection_set_mode(tree_selection, GTK_SELECTION_BROWSE); /* gtk_tree_view_expand_all() must be called before * gtk_tree_selection_select_iter(). */ gtk_tree_selection_select_iter(tree_selection, select_tree_iter); gtk_tree_iter_free(select_tree_iter); cell_renderer = gtk_cell_renderer_text_new(); gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(tree_view), -1, NULL, cell_renderer, "text", 1, NULL); return tree_view; } static void tab_list_clicked_cb(GtkWidget *button, MqNotebook *notebook) { GtkWidget *tab_tree_view; GtkWidget *scrolled_window; tab_tree_view = create_tab_tree_view(notebook); scrolled_window = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_min_content_width( GTK_SCROLLED_WINDOW(scrolled_window), 400); gtk_scrolled_window_set_min_content_height( GTK_SCROLLED_WINDOW(scrolled_window), 200); gtk_container_add(GTK_CONTAINER(scrolled_window), tab_tree_view); notebook->tab_tree_popover = gtk_popover_new(button); gtk_container_add(GTK_CONTAINER(notebook->tab_tree_popover), scrolled_window); /* NB: gtk_popover_popup() is new in GTK+ 3.22. */ gtk_widget_show_all(notebook->tab_tree_popover); } static void mq_notebook_init(MqNotebook *notebook) { GtkWidget *new_tab_button; GtkWidget *tab_list_button; notebook->tree = MQ_TAB_TREE(mq_tree_insert_root_allocated( MQ_TREE(g_new0(MqTabTree, 1)), NULL)); gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE); gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE); gtk_notebook_set_group_name(GTK_NOTEBOOK(notebook), "mq-tabs"); gtk_widget_set_can_focus(GTK_WIDGET(notebook), FALSE); new_tab_button = gtk_button_new_from_icon_name("tab-new-symbolic", GTK_ICON_SIZE_BUTTON); gtk_button_set_relief(GTK_BUTTON(new_tab_button), GTK_RELIEF_NONE); gtk_widget_set_can_focus(new_tab_button, FALSE); gtk_widget_set_tooltip_text(new_tab_button, _("New tab")); gtk_notebook_set_action_widget(GTK_NOTEBOOK(notebook), new_tab_button, GTK_PACK_START); gtk_widget_show_all(new_tab_button); g_signal_connect(new_tab_button, "clicked", G_CALLBACK(new_tab_clicked_cb), notebook); tab_list_button = gtk_button_new_from_icon_name("pan-down-symbolic", GTK_ICON_SIZE_BUTTON); gtk_button_set_relief(GTK_BUTTON(tab_list_button), GTK_RELIEF_NONE); gtk_widget_set_can_focus(tab_list_button, FALSE); gtk_widget_set_tooltip_text(tab_list_button, _("Tab list")); gtk_notebook_set_action_widget(GTK_NOTEBOOK(notebook), tab_list_button, GTK_PACK_END); gtk_widget_show_all(tab_list_button); g_signal_connect(tab_list_button, "clicked", G_CALLBACK(tab_list_clicked_cb), notebook); g_signal_connect(notebook, "switch-page", G_CALLBACK(switch_page_cb), NULL); } GtkWidget * mq_notebook_new(MqWindow *window) { return g_object_new(MQ_TYPE_NOTEBOOK, "window", window, NULL); } static gboolean find_node_compare(MqTree *node, gpointer user_data) { MqNotebook *notebook; notebook = MQ_NOTEBOOK(user_data); if (MQ_TAB_TREE(node)->page == notebook->find_page) { notebook->found_node = MQ_TAB_TREE(node); return MQ_TREE_STOP; } else { return MQ_TREE_CONTINUE; } } static void find_node(MqNotebook *notebook, MqTabPage *page) { notebook->find_page = page; notebook->found_node = NULL; mq_tree_foreach(MQ_TREE(notebook->tree), find_node_compare, notebook); notebook->find_page = NULL; g_assert(notebook->found_node); } static gboolean update_position(MqTree *node, gpointer G_GNUC_UNUSED user_data) { mq_tab_page_set_position (MQ_TAB_TREE(node)->page, node->position); mq_tab_label_set_position(MQ_TAB_TREE(node)->label, node->position); return MQ_TREE_CONTINUE; } static void insert_page(MqNotebook *notebook, MqTabTree *node, const gchar *uri) { node->page = mq_tab_page_new(notebook->window, uri); node->label = mq_tab_page_get_label(node->page); gtk_notebook_insert_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(node->page), GTK_WIDGET(node->label), MQ_TREE(node)->position - 1); gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook), GTK_WIDGET(node->page), TRUE); gtk_notebook_set_tab_detachable(GTK_NOTEBOOK(notebook), GTK_WIDGET(node->page), TRUE); gtk_widget_show_all(GTK_WIDGET(node->page)); gtk_widget_show_all(GTK_WIDGET(node->label)); mq_tree_foreach_from(MQ_TREE(node), update_position, NULL); } MqTabPage * mq_notebook_insert_top(MqNotebook *notebook, const gchar *uri, gboolean foreground) { MqTabTree *node; node = MQ_TAB_TREE(mq_tree_append_child_allocated( MQ_TREE(g_new0(MqTabTree, 1)), MQ_TREE(notebook->tree), NULL)); insert_page(notebook, node, uri); if (foreground) { notebook->current_page = node->page; gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), mq_tree_position(node) - 1); } if (!uri || !uri[0]) { mq_tab_page_focus_uri_entry(node->page); } else { mq_tab_page_focus_web_view(node->page); } return node->page; } MqTabPage * mq_notebook_insert_sibling(MqNotebook *notebook, const gchar *uri, MqTabPage *sibling, gboolean foreground) { MqTabTree *node; g_assert(sibling); find_node(notebook, sibling); node = MQ_TAB_TREE(mq_tree_append_sibling_allocated( MQ_TREE(g_new0(MqTabTree, 1)), MQ_TREE(notebook->found_node), NULL)); insert_page(notebook, node, uri); if (foreground) { notebook->current_page = node->page; gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), mq_tree_position(node) - 1); if (!uri || !uri[0]) { mq_tab_page_focus_uri_entry(node->page); } else { mq_tab_page_focus_web_view(node->page); } } return node->page; } MqTabPage * mq_notebook_insert_child(MqNotebook *notebook, const gchar *uri, MqTabPage *child, gboolean foreground) { MqTabTree *node; g_assert(child); find_node(notebook, child); node = MQ_TAB_TREE(mq_tree_append_child_allocated( MQ_TREE(g_new0(MqTabTree, 1)), MQ_TREE(notebook->found_node), NULL)); insert_page(notebook, node, uri); if (foreground) { notebook->current_page = node->page; gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), mq_tree_position(node) - 1); if (!uri || !uri[0]) { mq_tab_page_focus_uri_entry(node->page); } else { mq_tab_page_focus_web_view(node->page); } } return node->page; } void mq_notebook_remove_page(MqNotebook *notebook, MqTabPage *page) { find_node(notebook, page); mq_tree_remove_allocated(MQ_TREE(notebook->found_node)); mq_tree_foreach_from(MQ_TREE(notebook->found_node), update_position, NULL); gtk_notebook_remove_page(GTK_NOTEBOOK(notebook), mq_tree_position(notebook->found_node)); g_free(notebook->found_node); if (mq_tree_size(MQ_TREE(notebook->tree)) == 1) { mq_notebook_insert_top(notebook, NULL, TRUE); } } void mq_notebook_remove_current_page(MqNotebook *notebook) { mq_notebook_remove_page(notebook, notebook->current_page); } gint mq_notebook_get_n_pages(MqNotebook *notebook) { return mq_tree_size(MQ_TREE(notebook->tree)) - 1; } void mq_notebook_set_current_page(MqNotebook *notebook, gint page_num) { MqTabTree *node; node = MQ_TAB_TREE(mq_tree_seek(MQ_TREE(notebook->tree), page_num)); notebook->current_page = node->page; gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), page_num - 1); } void mq_notebook_update_tab_title(MqNotebook *notebook, MqTabPage *tab_page, const gchar *title) { if (tab_page == notebook->current_page) { mq_window_set_title(notebook->window, title); } } static gboolean foreach_label(MqTree *node, gpointer user_data) { MqNotebook *notebook; notebook = MQ_NOTEBOOK(user_data); return notebook->foreach_label_cb(MQ_TAB_TREE(node)->label, notebook->foreach_data); } void mq_notebook_foreach_label(MqNotebook *notebook, gboolean (*cb)(MqTabLabel *label, gpointer user_data), gpointer user_data) { notebook->foreach_label_cb = cb; notebook->foreach_data = user_data; mq_tree_foreach(MQ_TREE(notebook->tree), foreach_label, notebook); notebook->foreach_label_cb = NULL; notebook->foreach_data = NULL; } static gboolean foreach_page(MqTree *node, gpointer user_data) { MqNotebook *notebook; notebook = MQ_NOTEBOOK(user_data); return notebook->foreach_page_cb(MQ_TAB_TREE(node)->page, notebook->foreach_data); } void mq_notebook_foreach_page(MqNotebook *notebook, gboolean (*cb)(MqTabPage *page, gpointer user_data), gpointer user_data) { notebook->foreach_page_cb = cb; notebook->foreach_data = user_data; mq_tree_foreach(MQ_TREE(notebook->tree), foreach_page, notebook); notebook->foreach_page_cb = NULL; notebook->foreach_data = NULL; }