/*
* 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
update_tab_image(MqTab *tab)
{
gtk_image_set_from_surface(GTK_IMAGE(tab->tab_image), tab->favicon);
}
static void
update_tab_label(MqTab *tab)
{
gchar *label;
label = g_strdup_printf("%d. %s", tab->position, tab->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->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->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(tab->window, 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);
}
}
#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 gboolean
tab_label_button_press_cb(GtkWidget *widget, GdkEvent *event, MqTab *tab)
{
GtkWidget *button_grid;
GtkWidget *buttons[BUTTON_ROWS * BUTTON_COLS];
GtkWidget *tab_list;
GtkWidget *tab_list_scrolled_window;
GtkWidget *box;
/* Make sure this is a right mouse button press event. */
if (event->type != GDK_BUTTON_PRESS || event->button.button != 3) {
return FALSE;
}
/* 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 = gtk_label_new("Tab list");
/* 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);
return FALSE;
}
#undef BUTTON_ROWS
#undef BUTTON_COLS
#undef NEW_BUTTON
#undef NEW_TOGGLE
#undef CLICKED_CB
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)
{
tab->favicon = webkit_web_view_get_favicon(tab->web_view);
update_tab_image(tab);
}
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);
update_tab_label(tab);
}
static MqTab *
init_non_root(MqWindow *window, 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;
tab->window = window;
tab->application = mq_window_get_application(window);
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;
}
MqTab *
mq_tab_new(MqWindow *window, gchar *uri, MqTab *source)
{
MqTab *tab;
tab = init_non_root(window, uri);
append_sibling(tab, source);
mq_window_insert_tab(window, tab->container, tab->tab, tab->position);
return tab;
}
MqTab *
mq_tab_new_relative(MqWindow *window, gchar *uri, MqTab *source)
{
MqTab *tab;
tab = init_non_root(window, uri);
append_child(tab, source);
mq_window_insert_tab(window, tab->container, tab->tab, tab->position);
return tab;
}
MqTab *
mq_tab_new_root(void)
{
MqTab *tab;
tab = malloc(sizeof(*tab));
tab->parent = NULL;
tab->prev = NULL;
tab->next = NULL;
tab->first_child = tab->last_child = NULL;
tab->position = 0;
tab->tree_size = 1;
return tab;
}
void
mq_tab_update_position(MqTab *tab, guint position)
{
tab->position = position;
update_tab_label(tab);
}
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);
}