/*
* Tab
*
* Copyright (C) 2017 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
#include
#include "tab.h"
#include "tab-chrome.h"
#include "tab-body.h"
static void
foreach_tab(MqTab *node, void (*cb)(MqTab *node))
{
for (; node; node = node->next) {
cb(node);
foreach_tab(node->first_child, cb);
}
}
static void
update_tab_image(MqTab *tab, GdkPixbuf *favicon)
{
if (favicon) {
gtk_image_set_from_pixbuf(GTK_IMAGE(tab->tab_image), favicon);
} else {
gtk_image_set_from_icon_name(GTK_IMAGE(tab->tab_image),
"text-x-generic", GTK_ICON_SIZE_BUTTON);
}
}
static void
update_tab_label(MqTab *tab)
{
const gchar *title;
gchar *label;
title = tab->scrolling ? tab->scrolled_title : tab->title;
label = g_strdup_printf("%d. %s", tab->position, title);
gtk_label_set_text(GTK_LABEL(tab->tab_label), label);
gtk_widget_set_tooltip_text(tab->tab, label);
g_free(label);
}
static void
update_positions(MqTab *node, gint step)
{
if (node) {
node->position += step;
update_tab_label(node);
if (node->next) {
update_positions(node->next, step);
} else if (node->parent && node->parent->next) {
update_positions(node->parent->next, step);
}
}
}
static void
update_tree_sizes(MqTab *node, guint step)
{
if (node) {
node->tree_size += step;
update_tree_sizes(node->parent, step);
}
}
static void
append_child(MqTab *new_node, MqTab *parent)
{
new_node->root = parent->root;
new_node->parent = parent;
new_node->next = NULL;
new_node->prev = parent->last_child; /* May be NULL */
new_node->first_child = new_node->last_child = NULL;
new_node->tree_size = 0; /* Will be updated */
if (parent->last_child) {
new_node->position = parent->last_child->position;
parent->last_child->next = new_node;
} else {
new_node->position = parent->position;
parent->first_child = new_node;
}
parent->last_child = new_node;
update_positions(new_node, 1);
update_tree_sizes(new_node, 1);
}
static void
append_sibling(MqTab *new_node, MqTab *prev_sibling)
{
new_node->root = prev_sibling->root;
new_node->parent = prev_sibling->parent;
new_node->prev = prev_sibling;
new_node->next = prev_sibling->next; /* May be NULL */
new_node->first_child = new_node->last_child = NULL;
new_node->position = prev_sibling->position; /* Will be updated */
new_node->tree_size = 0; /* Will be updated */
if (prev_sibling->next) {
prev_sibling->next->prev = new_node;
}
prev_sibling->next = new_node;
update_positions(new_node, 1);
update_tree_sizes(new_node, 1);
}
static void
reload_tab_clicked_cb(GtkWidget __attribute__((unused)) *button, MqTab *tab)
{
webkit_web_view_reload(tab->web_view);
gtk_widget_hide(tab->popover);
}
static void
new_tab_clicked_cb(GtkWidget __attribute__((unused)) *button, MqTab *tab)
{
mq_tab_new(NULL, tab);
gtk_widget_hide(tab->popover);
}
static void
new_window_clicked_cb(GtkWidget __attribute__((unused)) *button, MqTab *tab)
{
mq_application_add_window(tab->application, NULL);
gtk_widget_hide(tab->popover);
}
static void
tab_list_button_toggled_cb(GtkToggleButton *toggle_button, GtkWidget *tab_list)
{
if (gtk_toggle_button_get_active(toggle_button)) {
gtk_widget_show(tab_list);
} else {
gtk_widget_hide(tab_list);
}
}
static void
create_tree_model_recurse(MqTab *node, GtkTreeStore *tree_store,
GtkTreeIter *parent_tree_iter)
{
GtkTreeIter tree_iter;
for (; node; node = node->next) {
gtk_tree_store_append(tree_store, &tree_iter, parent_tree_iter);
gtk_tree_store_set(tree_store, &tree_iter, 0, node->title, -1);
create_tree_model_recurse(node->first_child, tree_store,
&tree_iter);
}
}
static GtkTreeModel *
create_tree_model(MqTab *tab)
{
GtkTreeStore *tree_store;
tree_store = gtk_tree_store_new(1, G_TYPE_STRING);
create_tree_model_recurse(tab->root->first_child, tree_store, NULL);
return GTK_TREE_MODEL(tree_store);
}
static void
row_activated_cb(GtkTreeView __attribute__((unused)) *tree_view,
GtkTreePath *tree_path,
GtkTreeViewColumn __attribute__((unused)) *tree_view_column,
MqTab *tab)
{
gint *indices;
gint depth;
indices = gtk_tree_path_get_indices_with_depth(tree_path, &depth);
g_assert(depth == 1);
mq_window_set_current_tab(tab->window, indices[0] + 1);
gtk_widget_hide(tab->popover);
}
static GtkWidget *
create_tab_list(MqTab *tab)
{
GtkWidget *tree_view;
GtkTreeSelection *tree_selection;
GtkCellRenderer *cell_renderer;
GtkWidget *scrolled_window;
tree_view = gtk_tree_view_new_with_model(create_tree_model(tab));
tree_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
gtk_tree_selection_set_mode(tree_selection, GTK_SELECTION_BROWSE);
gtk_tree_selection_select_path(tree_selection,
gtk_tree_path_new_from_indices(
mq_window_get_current_tab(tab->window) - 1, -1));
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_expand_all(GTK_TREE_VIEW(tree_view));
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(row_activated_cb), tab);
cell_renderer = gtk_cell_renderer_text_new();
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(tree_view),
-1, NULL, cell_renderer, "text", 0, NULL);
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), tree_view);
return scrolled_window;
}
#define BUTTON_ROWS 2
#define BUTTON_COLS 4
#define NEW_BUTTON(Y, X, ICON, TOOLTIP) \
do { \
buttons[Y * BUTTON_COLS + X] = gtk_button_new_from_icon_name(\
ICON, GTK_ICON_SIZE_BUTTON); \
gtk_widget_set_tooltip_text(buttons[Y * BUTTON_COLS + X], \
TOOLTIP); \
gtk_widget_set_can_focus(buttons[Y * BUTTON_COLS + X], FALSE); \
gtk_grid_attach(GTK_GRID(button_grid), \
buttons[Y * BUTTON_COLS + X], X, Y, 1, 1); \
} while (0)
#define NEW_TOGGLE(Y, X, ICON, TOOLTIP) \
do { \
buttons[Y * BUTTON_COLS + X] = gtk_toggle_button_new(); \
gtk_button_set_image(GTK_BUTTON(buttons[Y * BUTTON_COLS + X]), \
gtk_image_new_from_icon_name(ICON, \
GTK_ICON_SIZE_BUTTON)); \
gtk_widget_set_tooltip_text(buttons[Y * BUTTON_COLS + X], \
TOOLTIP); \
gtk_widget_set_can_focus(buttons[Y * BUTTON_COLS + X], FALSE); \
gtk_grid_attach(GTK_GRID(button_grid), \
buttons[Y * BUTTON_COLS + X], X, Y, 1, 1); \
} while (0)
#define CLICKED_CB(Y, X, CB) \
g_signal_connect(buttons[Y * BUTTON_COLS + X], "clicked", CB, tab)
static void
create_tab_popover(GtkWidget *widget, MqTab *tab)
{
GtkWidget *button_grid;
GtkWidget *buttons[BUTTON_ROWS * BUTTON_COLS];
GtkWidget *tab_list;
GtkWidget *tab_list_scrolled_window;
GtkWidget *box;
/* Set up button grid. */
button_grid = gtk_grid_new();
gtk_widget_set_halign(button_grid, GTK_ALIGN_CENTER);
/* Set up buttons. */
NEW_BUTTON(0, 0, "view-refresh", "Reload tab");
NEW_BUTTON(0, 1, "edit-copy", "Duplicate tab");
NEW_BUTTON(0, 2, "window-new", "Move tab to new window");
NEW_BUTTON(0, 3, "window-close", "Close tab");
NEW_BUTTON(1, 0, "tab-new-symbolic", "New tab");
NEW_BUTTON(1, 1, "window-new", "New window");
NEW_BUTTON(1, 2, "edit-undo", "Undo close tab");
NEW_TOGGLE(1, 3, "view-list-symbolic", "Tab list...");
CLICKED_CB(0, 0, G_CALLBACK(reload_tab_clicked_cb));
CLICKED_CB(1, 0, G_CALLBACK(new_tab_clicked_cb));
CLICKED_CB(1, 1, G_CALLBACK(new_window_clicked_cb));
/* Set up the tab list. */
tab_list = create_tab_list(tab);
/* Set up the tab list scrolled window.
*
* The following GtkScrolledWindow widget has a hardcoded minimum size,
* because there seems to be (in GTK+ versions before 3.22) no way to
* set the natural size of GtkScrolledWindow and its GtkViewport.
*
* See tab-chrome.c for more information. */
tab_list_scrolled_window = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_min_content_width(
GTK_SCROLLED_WINDOW(tab_list_scrolled_window), 400);
gtk_scrolled_window_set_min_content_height(
GTK_SCROLLED_WINDOW(tab_list_scrolled_window), 200);
gtk_container_add(GTK_CONTAINER(tab_list_scrolled_window), tab_list);
/* Add tab list toggle button handler. */
g_signal_connect(buttons[1 * BUTTON_COLS + 3], "toggled",
G_CALLBACK(tab_list_button_toggled_cb),
tab_list_scrolled_window);
/* Set up the button rows box. */
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(box), button_grid,
TRUE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), tab_list_scrolled_window,
TRUE, FALSE, 0);
/* Set up the popover. */
tab->popover = gtk_popover_new(widget);
gtk_container_add(GTK_CONTAINER(tab->popover), box);
/* NB: gtk_popover_popup() is new in GTK+ 3.22. */
gtk_widget_show_all(tab->popover);
gtk_widget_hide(tab_list_scrolled_window);
}
#undef BUTTON_ROWS
#undef BUTTON_COLS
#undef NEW_BUTTON
#undef NEW_TOGGLE
#undef CLICKED_CB
static gboolean
tab_label_button_press_cb(GtkWidget *widget, GdkEvent *event, MqTab *tab)
{
/* Make sure this is a mouse button press event. */
if (event->type != GDK_BUTTON_PRESS) {
return FALSE;
}
/* Create a popover menu on right click. */
if (event->button.button == 3) {
create_tab_popover(widget, tab);
}
return FALSE;
}
static void
mq_tab_populate_tab(MqTab *tab)
{
GtkWidget *close_button;
GtkWidget *box;
tab->title = "New tab";
/* Set up tab image. */
tab->tab_image = gtk_image_new_from_icon_name("text-x-generic",
GTK_ICON_SIZE_BUTTON);
/* Set up tab label. */
tab->tab_label = gtk_label_new(NULL);
gtk_label_set_ellipsize(GTK_LABEL(tab->tab_label),
PANGO_ELLIPSIZE_END);
gtk_widget_set_hexpand(tab->tab_label, TRUE);
gtk_widget_set_size_request(tab->tab_label, 50, 1);
/* Set up close button. */
close_button = gtk_button_new_from_icon_name("window-close",
GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(close_button, "Close tab");
/* Pack tab box. */
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(box), tab->tab_image, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), tab->tab_label, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(box), close_button, FALSE, FALSE, 0);
gtk_widget_show_all(box);
/* Set up event box. */
tab->tab = gtk_event_box_new();
g_signal_connect(tab->tab, "button-press-event",
G_CALLBACK(tab_label_button_press_cb), tab);
gtk_event_box_set_visible_window(GTK_EVENT_BOX(tab->tab),
FALSE);
gtk_container_add(GTK_CONTAINER(tab->tab), box);
}
static void
favicon_cb(WebKitWebView __attribute__((unused)) *web_view,
GParamSpec __attribute__((unused)) *paramspec, MqTab *tab)
{
cairo_surface_t *surface;
GdkPixbuf *pixbuf;
GdkPixbuf *scaled_pixbuf;
surface = webkit_web_view_get_favicon(tab->web_view);
scaled_pixbuf = NULL;
if (surface) {
pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0,
cairo_image_surface_get_width(surface),
cairo_image_surface_get_height(surface));
if (pixbuf) {
scaled_pixbuf = gdk_pixbuf_scale_simple(pixbuf, 16, 16,
GDK_INTERP_BILINEAR);
g_object_unref(pixbuf);
}
}
update_tab_image(tab, scaled_pixbuf);
}
static void
title_cb(WebKitWebView __attribute__((unused)) *web_view,
GParamSpec __attribute__((unused)) *paramspec, MqTab *tab)
{
tab->title = webkit_web_view_get_title(tab->web_view);
if (tab->scrolling) {
tab->scrolled_title = g_strdup_printf("%s ", tab->title);
}
update_tab_label(tab);
mq_window_update_tab_title(tab->window, tab->position, tab->title);
}
static MqTab *
init_non_root(const gchar *uri)
{
MqTab *tab;
tab = malloc(sizeof(*tab));
tab->parent = NULL;
tab->prev = NULL;
tab->next = NULL;
tab->first_child = tab->last_child = NULL;
tab->tree_size = 1;
mq_tab_populate_tab(tab);
tab->chrome = mq_tab_chrome_new(uri);
tab->body = mq_tab_body_new(tab, uri);
tab->web_view = mq_tab_body_get_web_view(tab->body);
g_signal_connect(tab->web_view, "notify::favicon",
G_CALLBACK(favicon_cb), tab);
g_signal_connect(tab->web_view, "notify::title",
G_CALLBACK(title_cb), tab);
mq_tab_chrome_set_web_view(tab->chrome, tab->web_view);
tab->container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(tab->container),
mq_tab_chrome_get_container(tab->chrome), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(tab->container),
mq_tab_body_get_container(tab->body), TRUE, TRUE, 0);
return tab;
}
static void
scroll_tab_label(MqTab *tab)
{
gchar c[5]; /* Up to 4 bytes for a UTF-8 character, plus NUL */
guint i;
guint j;
/* Save the first (possibly multibyte) character. */
c[0] = tab->scrolled_title[0];
for (i = 1; tab->scrolled_title[i] & 0x80; ++i) {
c[i] = tab->scrolled_title[i];
}
c[i] = '\0';
/* Shift all characters. */
for (j = 0; tab->scrolled_title[i]; ++i, ++j) {
tab->scrolled_title[j] = tab->scrolled_title[i];
}
/* Set the last (possibly multibyte) character. */
for (--j, i = 0; c[i]; ++i, ++j) {
tab->scrolled_title[j] = c[i];
}
update_tab_label(tab);
}
static void
begin_scrolling_tab_label(MqTab *tab)
{
PangoFontDescription *font_desc;
tab->scrolling = TRUE;
tab->scrolled_title = g_strdup_printf("%s ", tab->title);
font_desc = pango_font_description_new();
pango_font_description_set_family_static(font_desc, "monospace");
gtk_widget_override_font(tab->tab_label, font_desc);
}
static void
end_scrolling_tab_label(MqTab *tab)
{
tab->scrolling = FALSE;
gtk_widget_override_font(tab->tab_label, NULL);
update_tab_label(tab);
}
MqTab *
mq_tab_new(const gchar *uri, MqTab *source)
{
MqTab *tab;
tab = init_non_root(uri);
tab->window = source->window;
tab->application = mq_window_get_application(tab->window);
if (mq_application_marquee_mode_on(tab->application)) {
begin_scrolling_tab_label(tab);
} else {
tab->scrolling = FALSE;
}
append_sibling(tab, source);
mq_window_insert_tab(tab->window, tab->container, tab->tab,
tab->position);
return tab;
}
MqTab *
mq_tab_new_relative(const gchar *uri, MqTab *source)
{
MqTab *tab;
tab = init_non_root(uri);
tab->window = source->window;
tab->application = mq_window_get_application(tab->window);
if (mq_application_marquee_mode_on(tab->application)) {
begin_scrolling_tab_label(tab);
} else {
tab->scrolling = FALSE;
}
append_child(tab, source);
mq_window_insert_tab(tab->window, tab->container, tab->tab,
tab->position);
return tab;
}
MqTab *
mq_tab_new_root(MqWindow *window)
{
MqTab *tab;
tab = malloc(sizeof(*tab));
tab->root = tab;
tab->parent = NULL;
tab->prev = NULL;
tab->next = NULL;
tab->first_child = tab->last_child = NULL;
tab->position = 0;
tab->tree_size = 1;
tab->window = window;
return tab;
}
void
mq_tab_update_position(MqTab *tab, guint position)
{
tab->position = position;
update_tab_label(tab);
}
guint
mq_tab_get_position(MqTab *tab)
{
return tab->position;
}
const gchar *
mq_tab_get_title(MqTab *tab)
{
return tab->title;
}
MqTab *
mq_tab_seek(MqTab *node, guint position)
{
/* Skip forward to the containing subtree. */
while (node && node->position + node->tree_size <= position) {
node = node->next;
}
/* Check whether we've gone past the end of the tree. */
if (!node) {
return NULL;
}
/* Check whether the sibling we've reached is the node we want. */
if (node->position == position) {
return node;
}
/* Recurse down the subtree. */
return mq_tab_seek(node->first_child, position);
}
void
mq_tab_scroll_tab_labels(MqTab *root)
{
foreach_tab(root->first_child, scroll_tab_label);
}
void
mq_tab_begin_scrolling_tab_labels(MqTab *root)
{
foreach_tab(root->first_child, begin_scrolling_tab_label);
}
void
mq_tab_end_scrolling_tab_labels(MqTab *root)
{
foreach_tab(root->first_child, end_scrolling_tab_label);
}