/* WWWLite, a lightweight web browser.
Copyright (C) 2019 defanor
This program 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.
This program 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 this program. If not, see .
*/
#include
#include "documentbox.h"
#include "inlinebox.h"
G_DEFINE_TYPE (DocumentBox, document_box, GTK_TYPE_SCROLLED_WINDOW);
static void document_box_dispose (GObject *object) {
DocumentBox *db = DOCUMENT_BOX(object);
if (db->links != NULL) {
/* The same links are also referenced from InlineBox, and freed on
its disposal, so only the list needs to be freed here. */
g_list_free(db->links);
db->links = NULL;
}
G_OBJECT_CLASS (document_box_parent_class)->dispose (object);
}
enum {
FOLLOW,
SELECT,
HOVER
};
static guint signals[3];
static GtkSizeRequestMode document_box_get_request_mode (GtkWidget *widget)
{
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}
static void
document_box_class_init (DocumentBoxClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
/* GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass); */
signals[FOLLOW] =
g_signal_new("follow",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
0, /* class_offset */
NULL, /* accumulator */
NULL, /* accu_data */
NULL, /* c_marshaller */
G_TYPE_NONE, /* return_type */
2, /* n_params */
G_TYPE_STRING,
G_TYPE_BOOLEAN);
signals[SELECT] =
g_signal_new("select",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
0, /* class_offset */
NULL, /* accumulator */
NULL, /* accu_data */
NULL, /* c_marshaller */
G_TYPE_NONE, /* return_type */
1, /* n_params */
G_TYPE_STRING);
signals[HOVER] =
g_signal_new("hover",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
0, /* class_offset */
NULL, /* accumulator */
NULL, /* accu_data */
NULL, /* c_marshaller */
G_TYPE_NONE, /* return_type */
1, /* n_params */
G_TYPE_STRING);
widget_class->get_request_mode = document_box_get_request_mode;
gobject_class->dispose = document_box_dispose;
return;
}
static void
document_box_init (DocumentBox *db)
{
db->links = NULL;
db->search.ib = NULL;
db->search.start = 0;
db->search.end = -1;
db->search.str = NULL;
db->search.forward = TRUE;
db->search.state = START;
}
typedef struct _SearchState SearchState;
struct _SearchState
{
gint x;
gint y;
guint index;
IBText *ibt;
InlineBox *ib;
guint ib_index;
};
static void
text_at_position(GtkWidget *widget, SearchState *st)
{
GtkAllocation alloc;
gtk_widget_get_allocation(widget, &alloc);
if (st->x >= alloc.x &&
st->x <= alloc.x + alloc.width &&
st->y >= alloc.y &&
st->y <= alloc.y + alloc.height) {
if (IS_INLINE_BOX(widget)) {
st->ib = INLINE_BOX(widget);
GList *ti;
guint text_position = 0;
for (ti = INLINE_BOX(widget)->children; ti; ti = ti->next) {
if (IS_IB_TEXT(ti->data)) {
IBText *ibt = IB_TEXT(ti->data);
if (st->x >= ibt->alloc.x &&
st->x <= ibt->alloc.x + ibt->alloc.width &&
st->y >= ibt->alloc.y &&
st->y <= ibt->alloc.y + ibt->alloc.height) {
gint position;
pango_layout_xy_to_index(ibt->layout,
(st->x - ibt->alloc.x) * PANGO_SCALE,
(st->y - ibt->alloc.y) * PANGO_SCALE,
&position,
NULL);
st->index = position;
st->ibt = ibt;
st->ib_index = text_position + position;
return;
}
text_position += strlen(pango_layout_get_text(ibt->layout));
}
}
return;
} else if (GTK_IS_CONTAINER(widget)) {
gtk_container_foreach(GTK_CONTAINER(widget),
(GtkCallback)text_at_position, st);
}
}
}
static gboolean widget_is_affected(GtkAllocation *wa, GtkAllocation *ta1,
GtkAllocation *ta2)
{
if (wa == NULL || ta1 == NULL || ta2 == NULL) {
return FALSE;
}
return
gdk_rectangle_intersect(wa, ta1, NULL) ||
gdk_rectangle_intersect(wa, ta2, NULL) ||
((ta2->y >= ta1->y &&
((wa->y >= ta1->y) && (wa->y <= ta2->y))) ||
((ta2->y <= ta1->y) &&
((wa->y >= ta2->y) && (wa->y <= ta1->y))));
}
/* Assuming (Y, X) is strictly increasing */
static gint compare_positions(GtkAllocation *a1, guint i1,
GtkAllocation *a2, guint i2)
{
if ((a1->y < a2->y) ||
(a1->y == a2->y && a1->x < a2->x) ||
(a1->y == a2->y && a1->x == a2->x && i1 < i2)) {
return -1;
} else if (a1->y == a2->y && a1->x == a2->x && i1 == i2) {
return 0;
} else {
return 1;
}
}
/* Updates InlineBox widgets to render selection appropriately. */
static void
selection_update (GtkWidget *widget, SelectionState *st)
{
if (st->selection_start == NULL ||
st->selection_end == NULL ||
st->selection_prev == NULL) {
return;
}
GtkAllocation alloc, alloc_start, alloc_end, alloc_prev;
gtk_widget_get_allocation(widget, &alloc);
gtk_widget_get_allocation(GTK_WIDGET(st->selection_start), &alloc_start);
gtk_widget_get_allocation(GTK_WIDGET(st->selection_end), &alloc_end);
gtk_widget_get_allocation(GTK_WIDGET(st->selection_prev), &alloc_prev);
if (widget_is_affected(&alloc, &alloc_start, &alloc_end) ||
widget_is_affected(&alloc, &alloc_start, &alloc_prev) ||
widget_is_affected(&alloc, &alloc_end, &alloc_prev)) {
if (IS_INLINE_BOX(widget)) {
InlineBox *ib = INLINE_BOX(widget);
ib->selection_end = 0;
ib->selection_start = 0;
gint direction = compare_positions(&alloc_start,
st->selection_start_index,
&alloc_end,
st->selection_end_index);
if (direction == -1) {
if (st->selection_start == ib) {
ib->selection_start = st->selection_start_index;
st->selecting = TRUE;
}
if (st->selecting && st->selection_end == ib) {
ib->selection_end = st->selection_end_index;
st->selecting = FALSE;
}
} else if (direction == 1) {
if (st->selection_end == ib) {
ib->selection_start = st->selection_end_index;
st->selecting = TRUE;
}
if (st->selecting && st->selection_start == ib) {
ib->selection_end = st->selection_start_index;
st->selecting = FALSE;
}
}
if (st->selecting) {
ib->selection_end = inline_box_get_text_length(ib);
}
gtk_widget_queue_draw (widget);
} else if (GTK_IS_CONTAINER(widget)) {
gtk_container_foreach(GTK_CONTAINER(widget),
(GtkCallback)selection_update, st);
}
}
}
static void
selection_read (GtkWidget *widget, gchar **str)
{
if (IS_INLINE_BOX(widget)) {
InlineBox *ib = INLINE_BOX(widget);
if (ib->selection_end == 0) {
return;
}
GList *ti;
guint text_position = 0;
gboolean affected = FALSE, breaks = FALSE;
for (ti = ib->children; ti; ti = ti->next) {
if (IS_IB_TEXT(ti->data)) {
IBText *ibt = IB_TEXT(ti->data);
const gchar *word = pango_layout_get_text(ibt->layout);
guint word_len = strlen(word);
if (ib->selection_start <= text_position + word_len &&
ib->selection_end > text_position) {
guint start_offset = 0, end_offset = 0;
if (ib->selection_start > text_position) {
start_offset = ib->selection_start - text_position;
}
if (ib->selection_end < text_position + word_len) {
end_offset = text_position + word_len - ib->selection_end;
}
guint len = word_len - start_offset - end_offset;
*str = realloc(*str, strlen(*str) + len + 1);
g_strlcpy(*str + strlen(*str), word + start_offset, len + 1);
affected = TRUE;
breaks = TRUE;
} else {
breaks = FALSE;
}
text_position += word_len;
} else if (breaks && IS_IB_BREAK(ti->data)) {
*str = realloc(*str, strlen(*str) + 2);
(*str)[strlen(*str) + 1] = 0;
(*str)[strlen(*str)] = '\n';
breaks = FALSE;
}
}
if (affected) {
/* Add one more newline in the end, so that there are newlines
between paragraphs. */
*str = realloc(*str, strlen(*str) + 2);
(*str)[strlen(*str) + 1] = 0;
(*str)[strlen(*str)] = '\n';
}
} else if (GTK_IS_CONTAINER(widget)) {
gtk_container_foreach(GTK_CONTAINER(widget),
(GtkCallback)selection_read, str);
}
}
static IBLink *find_link (SearchState *ss, gint x, gint y)
{
if (ss->ib && ss->ib->links != NULL) {
GList *li;
for (li = ss->ib->links; li; li = li->next) {
IBLink *link = IB_LINK(li->data);
if (ss->ibt) {
if (link->start <= ss->ib_index && link->end > ss->ib_index) {
return link;
}
}
GList *oi;
GtkAllocation alloc;
for (oi = link->objects; oi; oi = oi->next) {
gtk_widget_get_allocation(oi->data, &alloc);
if (alloc.x <= x && alloc.x + alloc.width >= x &&
alloc.y <= y && alloc.y + alloc.height >= y) {
return link;
}
}
}
}
return NULL;
}
static gboolean
button_press_event_cb (GtkWidget *widget,
GdkEventButton *event,
DocumentBox *db)
{
if (event->button != 1) {
return FALSE;
}
SearchState ss;
gint orig_x, orig_y, ev_orig_x, ev_orig_y;
gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
&orig_x, &orig_y);
gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
ss.ibt = NULL;
ss.x = ev_orig_x - orig_x + event->x;
ss.y = ev_orig_y - orig_y + event->y;
text_at_position(widget, &ss);
if (db->sel.selection_end) {
/* Remove existing selection */
db->sel.selection_prev = db->sel.selection_end;
db->sel.selection_prev_index = db->sel.selection_end_index + 1;
db->sel.selection_end = db->sel.selection_start;
db->sel.selection_end_index = db->sel.selection_start_index;
selection_update(widget, &db->sel);
}
if (ss.ib) {
db->sel.selection_active = TRUE;
db->sel.selection_start = ss.ib;
db->sel.selection_start_index = ss.ib_index;
db->sel.selection_end = ss.ib;
db->sel.selection_end_index = ss.ib_index;
/* todo: grab focus when any non-widget space is clicked, not
just texts */
gtk_widget_grab_focus(GTK_WIDGET(db));
}
return FALSE;
}
static gboolean
motion_notify_event_cb (GtkWidget *widget,
GdkEventButton *event,
DocumentBox *db)
{
SearchState ss;
gint orig_x, orig_y, ev_orig_x, ev_orig_y;
gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
&orig_x, &orig_y);
gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
ss.ib = NULL;
ss.ibt = NULL;
ss.x = ev_orig_x - orig_x + event->x;
ss.y = ev_orig_y - orig_y + event->y;
text_at_position(widget, &ss);
if (ss.ib && db->sel.selection_active) {
db->sel.selection_prev = db->sel.selection_end;
db->sel.selection_prev_index = db->sel.selection_end_index;
db->sel.selection_end = ss.ib;
db->sel.selection_end_index = ss.ib_index;
db->sel.selecting = FALSE;
selection_update(widget, &db->sel);
}
IBLink *link = find_link(&ss, event->x, event->y);
if (link != NULL) {
g_signal_emit(db, signals[HOVER], 0, link->url);
}
return FALSE;
}
static gboolean
button_release_event_cb (GtkWidget *widget,
GdkEventButton *event,
DocumentBox *db)
{
if (event->button != 1 && event->button != 2) {
return FALSE;
}
gchar *str = malloc(1);
gboolean got_selection = FALSE;
str[0] = 0;
selection_read(widget, &str);
if (strlen(str) > 0) {
/* Strip the last newline */
str[strlen(str) - 1] = 0;
got_selection = TRUE;
}
g_signal_emit(db, signals[SELECT], 0, str);
g_free(str);
db->sel.selection_active = FALSE;
if (got_selection) {
return FALSE;
}
SearchState ss;
gint orig_x, orig_y, ev_orig_x, ev_orig_y;
gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
&orig_x, &orig_y);
gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
ss.ib = NULL;
ss.ibt = NULL;
ss.x = ev_orig_x - orig_x + event->x;
ss.y = ev_orig_y - orig_y + event->y;
text_at_position(widget, &ss);
IBLink *link = find_link(&ss, event->x, event->y);
if (link != NULL) {
g_signal_emit(db, signals[FOLLOW], 0, link->url, event->button == 2);
return TRUE;
}
return FALSE;
}
static void
find_focused (GtkWidget *widget, GObject **focused) {
if (IS_INLINE_BOX(widget)) {
if (INLINE_BOX(widget)->focused_object != NULL) {
*focused = INLINE_BOX(widget)->focused_object;
return;
}
} else if (GTK_IS_CONTAINER(widget)) {
gtk_container_foreach(GTK_CONTAINER(widget),
(GtkCallback)find_focused, focused);
}
}
static gboolean
key_press_event_cb (GtkWidget *widget, GdkEventKey *event, DocumentBox *db)
{
if (event->keyval == GDK_KEY_Return) {
GObject *focused;
/* This is inefficient and can be optimised, but there's no
perceivable delay, so perhaps it doesn't worth adding more
code. */
find_focused(widget, &focused);
if (focused != NULL && IS_IB_LINK(focused)) {
g_signal_emit(db, signals[FOLLOW], 0, IB_LINK(focused)->url,
event->state & GDK_CONTROL_MASK);
return TRUE;
}
}
return FALSE;
}
DocumentBox *document_box_new ()
{
DocumentBox *db = DOCUMENT_BOX(g_object_new(document_box_get_type(),
"hadjustment", NULL,
"vadjustment", NULL,
NULL));
db->evbox = GTK_EVENT_BOX(gtk_event_box_new());
gtk_widget_add_events(GTK_WIDGET(db->evbox), GDK_POINTER_MOTION_MASK);
gtk_container_add (GTK_CONTAINER (db), GTK_WIDGET (db->evbox));
g_signal_connect (db->evbox, "button-press-event",
G_CALLBACK (button_press_event_cb), db);
g_signal_connect (db->evbox, "button-release-event",
G_CALLBACK (button_release_event_cb), db);
g_signal_connect (db->evbox, "motion-notify-event",
G_CALLBACK (motion_notify_event_cb), db);
g_signal_connect (db->evbox, "key-press-event",
G_CALLBACK (key_press_event_cb), db);
db->links = NULL;
db->sel.selection_active = FALSE;
return db;
}
static void
document_box_search (GtkWidget *widget, DocumentBox *db) {
/* todo: backwards search */
if (db->search.state == FOUND) {
return;
}
if (db->search.state == START &&
(db->search.ib == NULL || GTK_WIDGET(db->search.ib) == widget)) {
/* No previous position or found the widget */
db->search.state = LOOKING;
if (db->search.ib != NULL) {
db->sel.selection_prev = db->search.ib;
}
}
if (db->search.state == LOOKING &&
(db->search.ib == NULL || GTK_WIDGET(db->search.ib) != widget)) {
db->search.start = 0;
db->search.end = -1;
}
if (db->search.state == LOOKING && IS_INLINE_BOX(widget)) {
InlineBox *ib = INLINE_BOX(widget);
gint pos = inline_box_search(ib, db->search.start, db->search.end, db->search.str);
if (pos != -1) {
db->search.state = FOUND;
db->search.ib = ib;
db->search.start = pos;
db->search.end = pos + strlen(db->search.str);
db->sel.selection_start = db->search.ib;
db->sel.selection_start_index = db->search.start;
db->sel.selection_end = db->search.ib;
db->sel.selection_end_index = db->search.end;
selection_update(widget, &db->sel);
gtk_widget_queue_draw(widget);
}
} else if (db->search.state != FOUND && GTK_IS_CONTAINER(widget)) {
gtk_container_foreach(GTK_CONTAINER(widget),
(GtkCallback)document_box_search, db);
}
}
gboolean
document_box_find (DocumentBox *db, const gchar *str)
{
/* Cleanup selection */
db->sel.selection_prev = db->sel.selection_end;
db->sel.selection_prev_index = db->sel.selection_end_index + 1;
db->sel.selection_end = db->sel.selection_start;
db->sel.selection_end_index = db->sel.selection_start_index;
selection_update(GTK_WIDGET(db), &db->sel);
/* todo: backwards search */
db->search.str = str;
db->search.state = START;
db->search.end = -1;
document_box_search(GTK_WIDGET(db), db);
if (db->search.state == FOUND) {
gtk_widget_grab_focus(GTK_WIDGET(db->search.ib));
return TRUE;
} else {
db->search.ib = NULL;
db->search.start = 0;
db->search.end = -1;
return FALSE;
}
}