First stab at a GTK layout engine. It's missing all sorts of stuff
[u/mdw/putty] / unix / gtkcols.c
diff --git a/unix/gtkcols.c b/unix/gtkcols.c
new file mode 100644 (file)
index 0000000..d4c9367
--- /dev/null
@@ -0,0 +1,685 @@
+/*
+ * gtkcols.c - implementation of the `Columns' GTK layout container.
+ */
+
+#include "gtkcols.h"
+
+static void columns_init(Columns *cols);
+static void columns_class_init(ColumnsClass *klass);
+static void columns_map(GtkWidget *widget);
+static void columns_unmap(GtkWidget *widget);
+static void columns_draw(GtkWidget *widget, GdkRectangle *area);
+static gint columns_expose(GtkWidget *widget, GdkEventExpose *event);
+static void columns_base_add(GtkContainer *container, GtkWidget *widget);
+static void columns_remove(GtkContainer *container, GtkWidget *widget);
+static void columns_forall(GtkContainer *container, gboolean include_internals,
+                           GtkCallback callback, gpointer callback_data);
+static gint columns_focus(GtkContainer *container, GtkDirectionType dir);
+static GtkType columns_child_type(GtkContainer *container);
+static void columns_size_request(GtkWidget *widget, GtkRequisition *req);
+static void columns_size_allocate(GtkWidget *widget, GtkAllocation *alloc);
+
+static GtkContainerClass *parent_class = NULL;
+
+GtkType columns_get_type(void)
+{
+    static GtkType columns_type = 0;
+
+    if (!columns_type) {
+        static const GtkTypeInfo columns_info = {
+            "Columns",
+            sizeof(Columns),
+            sizeof(ColumnsClass),
+            (GtkClassInitFunc) columns_class_init,
+            (GtkObjectInitFunc) columns_init,
+            /* reserved_1 */ NULL,
+            /* reserved_2 */ NULL,
+            (GtkClassInitFunc) NULL,
+        };
+
+        columns_type = gtk_type_unique(GTK_TYPE_CONTAINER, &columns_info);
+    }
+
+    return columns_type;
+}
+
+static gint (*columns_inherited_focus)(GtkContainer *container,
+                                      GtkDirectionType direction);
+
+static void columns_class_init(ColumnsClass *klass)
+{
+    GtkObjectClass *object_class;
+    GtkWidgetClass *widget_class;
+    GtkContainerClass *container_class;
+
+    object_class = (GtkObjectClass *)klass;
+    widget_class = (GtkWidgetClass *)klass;
+    container_class = (GtkContainerClass *)klass;
+
+    parent_class = gtk_type_class(GTK_TYPE_CONTAINER);
+
+    /*
+     * FIXME: do we have to do all this faffing with set_arg,
+     * get_arg and child_arg_type? Ick.
+     */
+
+    widget_class->map = columns_map;
+    widget_class->unmap = columns_unmap;
+    widget_class->draw = columns_draw;
+    widget_class->expose_event = columns_expose;
+    widget_class->size_request = columns_size_request;
+    widget_class->size_allocate = columns_size_allocate;
+
+    container_class->add = columns_base_add;
+    container_class->remove = columns_remove;
+    container_class->forall = columns_forall;
+    container_class->child_type = columns_child_type;
+    /* Save the previous value of this method. */
+    if (!columns_inherited_focus)
+       columns_inherited_focus = container_class->focus;
+    container_class->focus = columns_focus;
+}
+
+static void columns_init(Columns *cols)
+{
+    GTK_WIDGET_SET_FLAGS(cols, GTK_NO_WINDOW);
+
+    cols->children = NULL;
+    cols->spacing = 0;
+}
+
+/*
+ * These appear to be thoroughly tedious functions; the only reason
+ * we have to reimplement them at all is because we defined our own
+ * format for our GList of children...
+ */
+static void columns_map(GtkWidget *widget)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+
+    g_return_if_fail(widget != NULL);
+    g_return_if_fail(IS_COLUMNS(widget));
+
+    cols = COLUMNS(widget);
+    GTK_WIDGET_SET_FLAGS(cols, GTK_MAPPED);
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        if (child->widget &&
+           GTK_WIDGET_VISIBLE(child->widget) &&
+            !GTK_WIDGET_MAPPED(child->widget))
+            gtk_widget_map(child->widget);
+    }
+}
+static void columns_unmap(GtkWidget *widget)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+
+    g_return_if_fail(widget != NULL);
+    g_return_if_fail(IS_COLUMNS(widget));
+
+    cols = COLUMNS(widget);
+    GTK_WIDGET_UNSET_FLAGS(cols, GTK_MAPPED);
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        if (child->widget &&
+           GTK_WIDGET_VISIBLE(child->widget) &&
+            GTK_WIDGET_MAPPED(child->widget))
+            gtk_widget_unmap(child->widget);
+    }
+}
+static void columns_draw(GtkWidget *widget, GdkRectangle *area)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+    GdkRectangle child_area;
+
+    g_return_if_fail(widget != NULL);
+    g_return_if_fail(IS_COLUMNS(widget));
+
+    if (GTK_WIDGET_DRAWABLE(widget)) {
+        cols = COLUMNS(widget);
+
+        for (children = cols->children;
+             children && (child = children->data);
+             children = children->next) {
+            if (child->widget &&
+               GTK_WIDGET_DRAWABLE(child->widget) &&
+                gtk_widget_intersect(child->widget, area, &child_area))
+                gtk_widget_draw(child->widget, &child_area);
+        }
+    }
+}
+static gint columns_expose(GtkWidget *widget, GdkEventExpose *event)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+    GdkEventExpose child_event;
+
+    g_return_val_if_fail(widget != NULL, FALSE);
+    g_return_val_if_fail(IS_COLUMNS(widget), FALSE);
+    g_return_val_if_fail(event != NULL, FALSE);
+
+    if (GTK_WIDGET_DRAWABLE(widget)) {
+        cols = COLUMNS(widget);
+        child_event = *event;
+
+        for (children = cols->children;
+             children && (child = children->data);
+             children = children->next) {
+            if (child->widget &&
+               GTK_WIDGET_DRAWABLE(child->widget) &&
+                GTK_WIDGET_NO_WINDOW(child->widget) &&
+                gtk_widget_intersect(child->widget, &event->area,
+                                     &child_event.area))
+                gtk_widget_event(child->widget, (GdkEvent *)&child_event);
+        }
+    }
+    return FALSE;
+}
+
+static void columns_base_add(GtkContainer *container, GtkWidget *widget)
+{
+    Columns *cols;
+
+    g_return_if_fail(container != NULL);
+    g_return_if_fail(IS_COLUMNS(container));
+    g_return_if_fail(widget != NULL);
+
+    cols = COLUMNS(container);
+
+    /*
+     * Default is to add a new widget spanning all columns.
+     */
+    columns_add(cols, widget, 0, 0);   /* 0 means ncols */
+}
+
+static void columns_remove(GtkContainer *container, GtkWidget *widget)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GtkWidget *childw;
+    GList *children;
+    gboolean was_visible;
+
+    g_return_if_fail(container != NULL);
+    g_return_if_fail(IS_COLUMNS(container));
+    g_return_if_fail(widget != NULL);
+
+    cols = COLUMNS(container);
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        if (child->widget != widget)
+            continue;
+
+        was_visible = GTK_WIDGET_VISIBLE(widget);
+        gtk_widget_unparent(widget);
+        cols->children = g_list_remove_link(cols->children, children);
+        g_list_free(children);
+        g_free(child);
+        if (was_visible)
+            gtk_widget_queue_resize(GTK_WIDGET(container));
+        break;
+    }
+
+    for (children = cols->taborder;
+         children && (childw = children->data);
+         children = children->next) {
+        if (childw != widget)
+            continue;
+
+        cols->taborder = g_list_remove_link(cols->taborder, children);
+        g_list_free(children);
+        break;
+    }
+}
+
+static void columns_forall(GtkContainer *container, gboolean include_internals,
+                           GtkCallback callback, gpointer callback_data)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+
+    g_return_if_fail(container != NULL);
+    g_return_if_fail(IS_COLUMNS(container));
+    g_return_if_fail(callback != NULL);
+
+    cols = COLUMNS(container);
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next)
+       if (child->widget)
+           callback(child->widget, callback_data);
+}
+
+static GtkType columns_child_type(GtkContainer *container)
+{
+    return GTK_TYPE_WIDGET;
+}
+
+GtkWidget *columns_new(gint spacing)
+{
+    Columns *cols;
+
+    cols = gtk_type_new(columns_get_type());
+    cols->spacing = spacing;
+
+    return GTK_WIDGET(cols);
+}
+
+void columns_set_cols(Columns *cols, gint ncols, const gint *percentages)
+{
+    ColumnsChild *childdata;
+    gint i;
+
+    g_return_if_fail(cols != NULL);
+    g_return_if_fail(IS_COLUMNS(cols));
+    g_return_if_fail(ncols > 0);
+    g_return_if_fail(percentages != NULL);
+
+    childdata = g_new(ColumnsChild, 1);
+    childdata->widget = NULL;
+    childdata->ncols = ncols;
+    childdata->percentages = g_new(gint, ncols);
+    childdata->force_left = FALSE;
+    for (i = 0; i < ncols; i++)
+        childdata->percentages[i] = percentages[i];
+
+    cols->children = g_list_append(cols->children, childdata);
+}
+
+void columns_add(Columns *cols, GtkWidget *child,
+                 gint colstart, gint colspan)
+{
+    ColumnsChild *childdata;
+
+    g_return_if_fail(cols != NULL);
+    g_return_if_fail(IS_COLUMNS(cols));
+    g_return_if_fail(child != NULL);
+    g_return_if_fail(child->parent == NULL);
+
+    childdata = g_new(ColumnsChild, 1);
+    childdata->widget = child;
+    childdata->colstart = colstart;
+    childdata->colspan = colspan;
+    childdata->force_left = FALSE;
+
+    cols->children = g_list_append(cols->children, childdata);
+    cols->taborder = g_list_append(cols->taborder, child);
+
+    gtk_widget_set_parent(child, GTK_WIDGET(cols));
+
+    if (GTK_WIDGET_REALIZED(cols))
+        gtk_widget_realize(child);
+
+    if (GTK_WIDGET_VISIBLE(cols) && GTK_WIDGET_VISIBLE(child)) {
+        if (GTK_WIDGET_MAPPED(cols))
+            gtk_widget_map(child);
+        gtk_widget_queue_resize(child);
+    }
+}
+
+void columns_force_left_align(Columns *cols, GtkWidget *widget)
+{
+    ColumnsChild *child;
+    GList *children;
+
+    g_return_if_fail(cols != NULL);
+    g_return_if_fail(IS_COLUMNS(cols));
+    g_return_if_fail(widget != NULL);
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        if (child->widget != widget)
+            continue;
+
+       child->force_left = TRUE;
+        if (GTK_WIDGET_VISIBLE(widget))
+            gtk_widget_queue_resize(GTK_WIDGET(cols));
+        break;
+    }
+}
+
+void columns_taborder_last(Columns *cols, GtkWidget *widget)
+{
+    GtkWidget *childw;
+    GList *children;
+
+    g_return_if_fail(cols != NULL);
+    g_return_if_fail(IS_COLUMNS(cols));
+    g_return_if_fail(widget != NULL);
+
+    for (children = cols->taborder;
+         children && (childw = children->data);
+         children = children->next) {
+        if (childw != widget)
+            continue;
+
+        cols->taborder = g_list_remove_link(cols->taborder, children);
+        g_list_free(children);
+       cols->taborder = g_list_append(cols->taborder, widget);
+        break;
+    }
+}
+
+/*
+ * Override GtkContainer's focus movement so the user can
+ * explicitly specify the tab order.
+ */
+static gint columns_focus(GtkContainer *container, GtkDirectionType dir)
+{
+    Columns *cols;
+    GList *pos;
+    GtkWidget *focuschild;
+
+    g_return_val_if_fail(container != NULL, FALSE);
+    g_return_val_if_fail(IS_COLUMNS(container), FALSE);
+
+    cols = COLUMNS(container);
+
+    if (!GTK_WIDGET_DRAWABLE(cols) ||
+       !GTK_WIDGET_IS_SENSITIVE(cols))
+       return FALSE;
+
+    if (!GTK_WIDGET_CAN_FOCUS(container) &&
+       (dir == GTK_DIR_TAB_FORWARD || dir == GTK_DIR_TAB_BACKWARD)) {
+
+       focuschild = container->focus_child;
+       gtk_container_set_focus_child(container, NULL);
+
+       if (dir == GTK_DIR_TAB_FORWARD)
+           pos = cols->taborder;
+       else
+           pos = g_list_last(cols->taborder);
+
+       while (pos) {
+           GtkWidget *child = pos->data;
+
+           if (focuschild) {
+               if (focuschild == child) {
+                   focuschild = NULL; /* now we can start looking in here */
+                   if (GTK_WIDGET_DRAWABLE(child) &&
+                       GTK_IS_CONTAINER(child) &&
+                       !GTK_WIDGET_HAS_FOCUS(child)) {
+                       if (gtk_container_focus(GTK_CONTAINER(child), dir))
+                           return TRUE;
+                   }
+               }
+           } else if (GTK_WIDGET_DRAWABLE(child)) {
+               if (GTK_IS_CONTAINER(child)) {
+                   if (gtk_container_focus(GTK_CONTAINER(child), dir))
+                       return TRUE;
+               } else if (GTK_WIDGET_CAN_FOCUS(child)) {
+                   gtk_widget_grab_focus(child);
+                   return TRUE;
+               }
+           }
+
+           if (dir == GTK_DIR_TAB_FORWARD)
+               pos = pos->next;
+           else
+               pos = pos->prev;
+       }
+
+       return FALSE;
+    } else
+       return columns_inherited_focus(container, dir);
+}
+
+/*
+ * Now here comes the interesting bit. The actual layout part is
+ * done in the following two functions:
+ * 
+ * columns_size_request() examines the list of widgets held in the
+ * Columns, and returns a requisition stating the absolute minimum
+ * size it can bear to be.
+ * 
+ * columns_size_allocate() is given an allocation telling it what
+ * size the whole container is going to be, and it calls
+ * gtk_widget_size_allocate() on all of its (visible) children to
+ * set their size and position relative to the top left of the
+ * container.
+ */
+
+static void columns_size_request(GtkWidget *widget, GtkRequisition *req)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+    gint i, ncols, colspan, *colypos;
+    const gint *percentages;
+    static const gint onecol[] = { 100 };
+
+    g_return_if_fail(widget != NULL);
+    g_return_if_fail(IS_COLUMNS(widget));
+    g_return_if_fail(req != NULL);
+
+    cols = COLUMNS(widget);
+
+    req->width = 0;
+    req->height = cols->spacing;
+
+    ncols = 1;
+    colypos = g_new(gint, 1);
+    colypos[0] = 0;
+    percentages = onecol;
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        GtkRequisition creq;
+
+       if (!child->widget) {
+           /* Column reconfiguration. */
+           for (i = 1; i < ncols; i++) {
+               if (colypos[0] < colypos[i])
+                   colypos[0] = colypos[i];
+           }
+           ncols = child->ncols;
+           percentages = child->percentages;
+           colypos = g_renew(gint, colypos, ncols);
+           for (i = 1; i < ncols; i++)
+               colypos[i] = colypos[0];
+           continue;
+       }
+
+        /* Only take visible widgets into account. */
+        if (!GTK_WIDGET_VISIBLE(child->widget))
+            continue;
+
+        gtk_widget_size_request(child->widget, &creq);
+       colspan = child->colspan ? child->colspan : ncols-child->colstart;
+
+        /*
+         * To compute width: we know that creq.width plus
+         * cols->spacing needs to equal a certain percentage of the
+         * full width of the container. So we work this value out,
+         * figure out how wide the container will need to be to
+         * make that percentage of it equal to that width, and
+         * ensure our returned width is at least that much. Very
+         * simple really.
+         */
+        {
+            int percent, thiswid, fullwid;
+
+            percent = 0;
+            for (i = 0; i < colspan; i++)
+                percent += percentages[child->colstart+i];
+
+            thiswid = creq.width + cols->spacing;
+            /*
+             * Since creq is the _minimum_ size the child needs, we
+             * must ensure that it gets _at least_ that size.
+             * Hence, when scaling thiswid up to fullwid, we must
+             * round up, which means adding percent-1 before
+             * dividing by percent.
+             */
+            fullwid = (thiswid * 100 + percent - 1) / percent;
+
+            /*
+             * The above calculation assumes every widget gets
+             * cols->spacing on the right. So we subtract
+             * cols->spacing here to account for the extra load of
+             * spacing on the right.
+             */
+            if (req->width < fullwid - cols->spacing)
+                req->width = fullwid - cols->spacing;
+        }
+
+        /*
+         * To compute height: the widget's top will be positioned
+         * at the largest y value so far reached in any of the
+         * columns it crosses. Then it will go down by creq.height
+         * plus padding; and the point it reaches at the bottom is
+         * the new y value in all those columns, and minus the
+         * padding it is also a lower bound on our own size
+         * request.
+         */
+        {
+            int topy, boty;
+
+            topy = 0;
+            for (i = 0; i < colspan; i++) {
+                if (topy < colypos[child->colstart+i])
+                    topy = colypos[child->colstart+i];
+            }
+            boty = topy + creq.height + cols->spacing;
+            for (i = 0; i < colspan; i++) {
+                colypos[child->colstart+i] = boty;
+            }
+
+            if (req->height < boty - cols->spacing)
+                req->height = boty - cols->spacing;
+        }
+    }
+
+    req->width += 2*GTK_CONTAINER(cols)->border_width;
+    req->height += 2*GTK_CONTAINER(cols)->border_width;
+
+    g_free(colypos);
+}
+
+static void columns_size_allocate(GtkWidget *widget, GtkAllocation *alloc)
+{
+    Columns *cols;
+    ColumnsChild *child;
+    GList *children;
+    gint i, ncols, colspan, border, *colxpos, *colypos;
+    const gint *percentages;
+    static const gint onecol[] = { 100 };
+
+    g_return_if_fail(widget != NULL);
+    g_return_if_fail(IS_COLUMNS(widget));
+    g_return_if_fail(alloc != NULL);
+
+    cols = COLUMNS(widget);
+    widget->allocation = *alloc;
+    border = GTK_CONTAINER(cols)->border_width;
+
+    ncols = 1;
+    percentages = onecol;
+    /* colxpos gives the starting x position of each column.
+     * We supply n+1 of them, so that we can find the RH edge easily.
+     * All ending x positions are expected to be adjusted afterwards by
+     * subtracting the spacing. */
+    colxpos = g_new(gint, 2);
+    colxpos[0] = 0;
+    colxpos[1] = alloc->width - 2*border + cols->spacing;
+    /* As in size_request, colypos is the lowest y reached in each column. */
+    colypos = g_new(gint, 1);
+    colypos[0] = 0;
+
+    for (children = cols->children;
+         children && (child = children->data);
+         children = children->next) {
+        GtkRequisition creq;
+        GtkAllocation call;
+
+       if (!child->widget) {
+           gint percent;
+
+           /* Column reconfiguration. */
+           for (i = 1; i < ncols; i++) {
+               if (colypos[0] < colypos[i])
+                   colypos[0] = colypos[i];
+           }
+           ncols = child->ncols;
+           percentages = child->percentages;
+           colypos = g_renew(gint, colypos, ncols);
+           for (i = 1; i < ncols; i++)
+               colypos[i] = colypos[0];
+           colxpos = g_renew(gint, colxpos, ncols + 1);
+           colxpos[0] = 0;
+           percent = 0;
+           for (i = 0; i < ncols; i++) {
+               percent += percentages[i];
+               colxpos[i+1] = (((alloc->width - 2*border) + cols->spacing)
+                               * percent / 100);
+           }
+           continue;
+       }
+
+        /* Only take visible widgets into account. */
+        if (!GTK_WIDGET_VISIBLE(child->widget))
+            continue;
+
+        gtk_widget_get_child_requisition(child->widget, &creq);
+       colspan = child->colspan ? child->colspan : ncols-child->colstart;
+
+        /*
+         * Starting x position is cols[colstart].
+         * Ending x position is cols[colstart+colspan] - spacing.
+        * 
+        * Unless we're forcing left, in which case the width is
+        * exactly the requisition width.
+         */
+        call.x = alloc->x + border + colxpos[child->colstart];
+       if (child->force_left)
+           call.width = creq.width;
+       else
+           call.width = (colxpos[child->colstart+colspan] -
+                         colxpos[child->colstart] - cols->spacing);
+
+        /*
+         * To compute height: the widget's top will be positioned
+         * at the largest y value so far reached in any of the
+         * columns it crosses. Then it will go down by creq.height
+         * plus padding; and the point it reaches at the bottom is
+         * the new y value in all those columns.
+         */
+        {
+            int topy, boty;
+
+            topy = 0;
+            for (i = 0; i < colspan; i++) {
+                if (topy < colypos[child->colstart+i])
+                    topy = colypos[child->colstart+i];
+            }
+            call.y = alloc->y + border + topy;
+            call.height = creq.height;
+            boty = topy + creq.height + cols->spacing;
+            for (i = 0; i < colspan; i++) {
+                colypos[child->colstart+i] = boty;
+            }
+        }
+
+        gtk_widget_size_allocate(child->widget, &call);
+    }
+
+    g_free(colxpos);
+    g_free(colypos);    
+}