/*
* 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;
}