First Commit
This commit is contained in:
commit
ae0fab9400
424 files changed
+71006
No files matched your search
@@ -0,0 +1,406 @@
|
||||
// 26 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
void *uiTableModelStrdup(const char *str)
|
||||
{
|
||||
return g_strdup(str);
|
||||
}
|
||||
|
||||
void *uiTableModelGiveColor(double r, double g, double b, double a)
|
||||
{
|
||||
GdkRGBA rgba;
|
||||
|
||||
rgba.red = r;
|
||||
rgba.green = g;
|
||||
rgba.blue = b;
|
||||
rgba.alpha = a;
|
||||
return gdk_rgba_copy(&rgba);
|
||||
}
|
||||
|
||||
uiTableModel *uiNewTableModel(uiTableModelHandler *mh)
|
||||
{
|
||||
uiTableModel *m;
|
||||
|
||||
m = uiTableModel(g_object_new(uiTableModelType, NULL));
|
||||
m->mh = mh;
|
||||
return m;
|
||||
}
|
||||
|
||||
void uiFreeTableModel(uiTableModel *m)
|
||||
{
|
||||
g_object_unref(m);
|
||||
}
|
||||
|
||||
void uiTableModelRowInserted(uiTableModel *m, int newIndex)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(newIndex, -1);
|
||||
iter.stamp = STAMP_GOOD;
|
||||
iter.user_data = GINT_TO_POINTER(newIndex);
|
||||
gtk_tree_model_row_inserted(GTK_TREE_MODEL(m), path, &iter);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
void uiTableModelRowChanged(uiTableModel *m, int index)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(index, -1);
|
||||
iter.stamp = STAMP_GOOD;
|
||||
iter.user_data = GINT_TO_POINTER(index);
|
||||
gtk_tree_model_row_changed(GTK_TREE_MODEL(m), path, &iter);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
void uiTableModelRowDeleted(uiTableModel *m, int oldIndex)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(oldIndex, -1);
|
||||
gtk_tree_model_row_deleted(GTK_TREE_MODEL(m), path);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
enum {
|
||||
partText,
|
||||
partImage,
|
||||
partButton,
|
||||
partCheckbox,
|
||||
partProgressBar,
|
||||
};
|
||||
|
||||
struct tablePart {
|
||||
int type;
|
||||
int textColumn;
|
||||
int imageColumn;
|
||||
int valueColumn;
|
||||
int colorColumn;
|
||||
GtkCellRenderer *r;
|
||||
uiTable *tv; // for pixbufs and background color
|
||||
};
|
||||
|
||||
struct uiTableColumn {
|
||||
GtkTreeViewColumn *c;
|
||||
uiTable *tv; // for pixbufs and background color
|
||||
GPtrArray *parts;
|
||||
};
|
||||
|
||||
struct uiTable {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *scontainer;
|
||||
GtkScrolledWindow *sw;
|
||||
GtkWidget *treeWidget;
|
||||
GtkTreeView *tv;
|
||||
GPtrArray *columns;
|
||||
uiTableModel *model;
|
||||
int backgroundColumn;
|
||||
};
|
||||
|
||||
// use the same size as GtkFileChooserWidget's treeview
|
||||
// TODO refresh when icon theme changes
|
||||
// TODO doesn't work when scaled
|
||||
// TODO is this even necessary?
|
||||
static void setImageSize(GtkCellRenderer *r)
|
||||
{
|
||||
gint size;
|
||||
gint width, height;
|
||||
gint xpad, ypad;
|
||||
|
||||
size = 16; // fallback used by GtkFileChooserWidget
|
||||
if (gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height) != FALSE)
|
||||
size = MAX(width, height);
|
||||
gtk_cell_renderer_get_padding(r, &xpad, &ypad);
|
||||
gtk_cell_renderer_set_fixed_size(r,
|
||||
2 * xpad + size,
|
||||
2 * ypad + size);
|
||||
}
|
||||
|
||||
static void applyColor(GtkTreeModel *mm, GtkTreeIter *iter, int modelColumn, GtkCellRenderer *r, const char *prop, const char *propSet)
|
||||
{
|
||||
GValue value = G_VALUE_INIT;
|
||||
GdkRGBA *rgba;
|
||||
|
||||
gtk_tree_model_get_value(mm, iter, modelColumn, &value);
|
||||
rgba = (GdkRGBA *) g_value_get_boxed(&value);
|
||||
if (rgba != NULL)
|
||||
g_object_set(r, prop, rgba, NULL);
|
||||
else
|
||||
g_object_set(r, propSet, FALSE, NULL);
|
||||
g_value_unset(&value);
|
||||
}
|
||||
|
||||
static void dataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *mm, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct tablePart *part = (struct tablePart *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
const gchar *str;
|
||||
uiImage *img;
|
||||
int pval;
|
||||
|
||||
switch (part->type) {
|
||||
case partText:
|
||||
gtk_tree_model_get_value(mm, iter, part->textColumn, &value);
|
||||
str = g_value_get_string(&value);
|
||||
g_object_set(r, "text", str, NULL);
|
||||
if (part->colorColumn != -1)
|
||||
applyColor(mm, iter,
|
||||
part->colorColumn,
|
||||
r, "foreground-rgba", "foreground-set");
|
||||
break;
|
||||
case partImage:
|
||||
//TODO setImageSize(r);
|
||||
gtk_tree_model_get_value(mm, iter, part->imageColumn, &value);
|
||||
img = (uiImage *) g_value_get_pointer(&value);
|
||||
g_object_set(r, "surface",
|
||||
uiprivImageAppropriateSurface(img, part->tv->treeWidget),
|
||||
NULL);
|
||||
break;
|
||||
case partButton:
|
||||
gtk_tree_model_get_value(mm, iter, part->textColumn, &value);
|
||||
str = g_value_get_string(&value);
|
||||
g_object_set(r, "text", str, NULL);
|
||||
break;
|
||||
case partCheckbox:
|
||||
gtk_tree_model_get_value(mm, iter, part->valueColumn, &value);
|
||||
g_object_set(r, "active", g_value_get_int(&value) != 0, NULL);
|
||||
break;
|
||||
case partProgressBar:
|
||||
gtk_tree_model_get_value(mm, iter, part->valueColumn, &value);
|
||||
pval = g_value_get_int(&value);
|
||||
if (pval == -1) {
|
||||
// TODO
|
||||
} else
|
||||
g_object_set(r,
|
||||
"pulse", -1,
|
||||
"value", pval,
|
||||
NULL);
|
||||
break;
|
||||
}
|
||||
g_value_unset(&value);
|
||||
|
||||
if (part->tv->backgroundColumn != -1)
|
||||
applyColor(mm, iter,
|
||||
part->tv->backgroundColumn,
|
||||
r, "cell-background-rgba", "cell-background-set");
|
||||
}
|
||||
|
||||
static void onEdited(struct tablePart *part, int column, const char *pathstr, const void *data)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
int row;
|
||||
uiTableModel *m;
|
||||
|
||||
path = gtk_tree_path_new_from_string(pathstr);
|
||||
row = gtk_tree_path_get_indices(path)[0];
|
||||
gtk_tree_path_free(path);
|
||||
m = part->tv->model;
|
||||
(*(m->mh->SetCellValue))(m->mh, m, row, column, data);
|
||||
// and update
|
||||
uiTableModelRowChanged(m, row);
|
||||
}
|
||||
|
||||
static void appendPart(uiTableColumn *c, struct tablePart *part, GtkCellRenderer *r, int expand)
|
||||
{
|
||||
part->r = r;
|
||||
gtk_tree_view_column_pack_start(c->c, part->r, expand != 0);
|
||||
gtk_tree_view_column_set_cell_data_func(c->c, part->r, dataFunc, part, NULL);
|
||||
g_ptr_array_add(c->parts, part);
|
||||
}
|
||||
|
||||
static void textEdited(GtkCellRendererText *renderer, gchar *path, gchar *newText, gpointer data)
|
||||
{
|
||||
struct tablePart *part = (struct tablePart *) data;
|
||||
|
||||
onEdited(part, part->textColumn, path, newText);
|
||||
}
|
||||
|
||||
void uiTableColumnAppendTextPart(uiTableColumn *c, int modelColumn, int expand)
|
||||
{
|
||||
struct tablePart *part;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
part = uiprivNew(struct tablePart);
|
||||
part->type = partText;
|
||||
part->textColumn = modelColumn;
|
||||
part->tv = c->tv;
|
||||
part->colorColumn = -1;
|
||||
|
||||
r = gtk_cell_renderer_text_new();
|
||||
g_object_set(r, "editable", FALSE, NULL);
|
||||
g_signal_connect(r, "edited", G_CALLBACK(textEdited), part);
|
||||
|
||||
appendPart(c, part, r, expand);
|
||||
}
|
||||
|
||||
void uiTableColumnAppendImagePart(uiTableColumn *c, int modelColumn, int expand)
|
||||
{
|
||||
struct tablePart *part;
|
||||
|
||||
part = uiprivNew(struct tablePart);
|
||||
part->type = partImage;
|
||||
part->imageColumn = modelColumn;
|
||||
part->tv = c->tv;
|
||||
appendPart(c, part,
|
||||
gtk_cell_renderer_pixbuf_new(),
|
||||
expand);
|
||||
}
|
||||
|
||||
// TODO wrong type here
|
||||
static void buttonClicked(GtkCellRenderer *r, gchar *pathstr, gpointer data)
|
||||
{
|
||||
struct tablePart *part = (struct tablePart *) data;
|
||||
|
||||
onEdited(part, part->textColumn, pathstr, NULL);
|
||||
}
|
||||
|
||||
void uiTableColumnAppendButtonPart(uiTableColumn *c, int modelColumn, int expand)
|
||||
{
|
||||
struct tablePart *part;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
part = uiprivNew(struct tablePart);
|
||||
part->type = partButton;
|
||||
part->textColumn = modelColumn;
|
||||
part->tv = c->tv;
|
||||
|
||||
r = uiprivNewCellRendererButton();
|
||||
g_object_set(r, "sensitive", TRUE, NULL); // editable by default
|
||||
g_signal_connect(r, "clicked", G_CALLBACK(buttonClicked), part);
|
||||
|
||||
appendPart(c, part, r, expand);
|
||||
}
|
||||
|
||||
// yes, we need to do all this twice :|
|
||||
static void checkboxToggled(GtkCellRendererToggle *r, gchar *pathstr, gpointer data)
|
||||
{
|
||||
struct tablePart *part = (struct tablePart *) data;
|
||||
GtkTreePath *path;
|
||||
int row;
|
||||
uiTableModel *m;
|
||||
void *value;
|
||||
int intval;
|
||||
|
||||
path = gtk_tree_path_new_from_string(pathstr);
|
||||
row = gtk_tree_path_get_indices(path)[0];
|
||||
gtk_tree_path_free(path);
|
||||
m = part->tv->model;
|
||||
value = (*(m->mh->CellValue))(m->mh, m, row, part->valueColumn);
|
||||
intval = !uiTableModelTakeInt(value);
|
||||
onEdited(part, part->valueColumn, pathstr, uiTableModelGiveInt(intval));
|
||||
}
|
||||
|
||||
void uiTableColumnAppendCheckboxPart(uiTableColumn *c, int modelColumn, int expand)
|
||||
{
|
||||
struct tablePart *part;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
part = uiprivNew(struct tablePart);
|
||||
part->type = partCheckbox;
|
||||
part->valueColumn = modelColumn;
|
||||
part->tv = c->tv;
|
||||
|
||||
r = gtk_cell_renderer_toggle_new();
|
||||
g_object_set(r, "sensitive", TRUE, NULL); // editable by default
|
||||
g_signal_connect(r, "toggled", G_CALLBACK(checkboxToggled), part);
|
||||
|
||||
appendPart(c, part, r, expand);
|
||||
}
|
||||
|
||||
void uiTableColumnAppendProgressBarPart(uiTableColumn *c, int modelColumn, int expand)
|
||||
{
|
||||
struct tablePart *part;
|
||||
|
||||
part = uiprivNew(struct tablePart);
|
||||
part->type = partProgressBar;
|
||||
part->valueColumn = modelColumn;
|
||||
part->tv = c->tv;
|
||||
appendPart(c, part,
|
||||
gtk_cell_renderer_progress_new(),
|
||||
expand);
|
||||
}
|
||||
|
||||
void uiTableColumnPartSetEditable(uiTableColumn *c, int part, int editable)
|
||||
{
|
||||
struct tablePart *p;
|
||||
|
||||
p = (struct tablePart *) g_ptr_array_index(c->parts, part);
|
||||
switch (p->type) {
|
||||
case partImage:
|
||||
case partProgressBar:
|
||||
return;
|
||||
case partButton:
|
||||
case partCheckbox:
|
||||
g_object_set(p->r, "sensitive", editable != 0, NULL);
|
||||
return;
|
||||
}
|
||||
g_object_set(p->r, "editable", editable != 0, NULL);
|
||||
}
|
||||
|
||||
void uiTableColumnPartSetTextColor(uiTableColumn *c, int part, int modelColumn)
|
||||
{
|
||||
struct tablePart *p;
|
||||
|
||||
p = (struct tablePart *) g_ptr_array_index(c->parts, part);
|
||||
p->colorColumn = modelColumn;
|
||||
// TODO refresh table
|
||||
}
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiTable)
|
||||
|
||||
static void uiTableDestroy(uiControl *c)
|
||||
{
|
||||
uiTable *t = uiTable(c);
|
||||
|
||||
// TODO
|
||||
g_object_unref(t->widget);
|
||||
uiFreeControl(uiControl(t));
|
||||
}
|
||||
|
||||
uiTableColumn *uiTableAppendColumn(uiTable *t, const char *name)
|
||||
{
|
||||
uiTableColumn *c;
|
||||
|
||||
c = uiprivNew(uiTableColumn);
|
||||
c->c = gtk_tree_view_column_new();
|
||||
gtk_tree_view_column_set_resizable(c->c, TRUE);
|
||||
gtk_tree_view_column_set_title(c->c, name);
|
||||
gtk_tree_view_append_column(t->tv, c->c);
|
||||
c->tv = t; // TODO rename field to t, cascade
|
||||
c->parts = g_ptr_array_new();
|
||||
return c;
|
||||
}
|
||||
|
||||
void uiTableSetRowBackgroundColorModelColumn(uiTable *t, int modelColumn)
|
||||
{
|
||||
t->backgroundColumn = modelColumn;
|
||||
// TODO refresh table
|
||||
}
|
||||
|
||||
uiTable *uiNewTable(uiTableModel *model)
|
||||
{
|
||||
uiTable *t;
|
||||
|
||||
uiUnixNewControl(uiTable, t);
|
||||
|
||||
t->model = model;
|
||||
t->backgroundColumn = -1;
|
||||
|
||||
t->widget = gtk_scrolled_window_new(NULL, NULL);
|
||||
t->scontainer = GTK_CONTAINER(t->widget);
|
||||
t->sw = GTK_SCROLLED_WINDOW(t->widget);
|
||||
gtk_scrolled_window_set_shadow_type(t->sw, GTK_SHADOW_IN);
|
||||
|
||||
t->treeWidget = gtk_tree_view_new_with_model(GTK_TREE_MODEL(t->model));
|
||||
t->tv = GTK_TREE_VIEW(t->treeWidget);
|
||||
// TODO set up t->tv
|
||||
|
||||
gtk_container_add(t->scontainer, t->treeWidget);
|
||||
// and make the tree view visible; only the scrolled window's visibility is controlled by libui
|
||||
gtk_widget_show(t->treeWidget);
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 7 april 2015
|
||||
#include <string.h>
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
static GPtrArray *allocations;
|
||||
|
||||
#define UINT8(p) ((uint8_t *) (p))
|
||||
#define PVOID(p) ((void *) (p))
|
||||
#define EXTRA (sizeof (size_t) + sizeof (const char **))
|
||||
#define DATA(p) PVOID(UINT8(p) + EXTRA)
|
||||
#define BASE(p) PVOID(UINT8(p) - EXTRA)
|
||||
#define SIZE(p) ((size_t *) (p))
|
||||
#define CCHAR(p) ((const char **) (p))
|
||||
#define TYPE(p) CCHAR(UINT8(p) + sizeof (size_t))
|
||||
|
||||
void uiprivInitAlloc(void)
|
||||
{
|
||||
allocations = g_ptr_array_new();
|
||||
}
|
||||
|
||||
static void uninitComplain(gpointer ptr, gpointer data)
|
||||
{
|
||||
char **str = (char **) data;
|
||||
char *str2;
|
||||
|
||||
if (*str == NULL)
|
||||
*str = g_strdup_printf("");
|
||||
str2 = g_strdup_printf("%s%p %s\n", *str, ptr, *TYPE(ptr));
|
||||
g_free(*str);
|
||||
*str = str2;
|
||||
}
|
||||
|
||||
void uiprivUninitAlloc(void)
|
||||
{
|
||||
char *str = NULL;
|
||||
|
||||
if (allocations->len == 0) {
|
||||
g_ptr_array_free(allocations, TRUE);
|
||||
return;
|
||||
}
|
||||
g_ptr_array_foreach(allocations, uninitComplain, &str);
|
||||
uiprivUserBug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", str);
|
||||
g_free(str);
|
||||
}
|
||||
|
||||
void *uiprivAlloc(size_t size, const char *type)
|
||||
{
|
||||
void *out;
|
||||
|
||||
out = g_malloc0(EXTRA + size);
|
||||
*SIZE(out) = size;
|
||||
*TYPE(out) = type;
|
||||
g_ptr_array_add(allocations, out);
|
||||
return DATA(out);
|
||||
}
|
||||
|
||||
void *uiprivRealloc(void *p, size_t new, const char *type)
|
||||
{
|
||||
void *out;
|
||||
size_t *s;
|
||||
|
||||
if (p == NULL)
|
||||
return uiprivAlloc(new, type);
|
||||
p = BASE(p);
|
||||
out = g_realloc(p, EXTRA + new);
|
||||
s = SIZE(out);
|
||||
if (new > *s)
|
||||
memset(((uint8_t *) DATA(out)) + *s, 0, new - *s);
|
||||
*s = new;
|
||||
if (g_ptr_array_remove(allocations, p) == FALSE)
|
||||
uiprivImplBug("%p not found in allocations array in uiprivRealloc()", p);
|
||||
g_ptr_array_add(allocations, out);
|
||||
return DATA(out);
|
||||
}
|
||||
|
||||
void uiprivFree(void *p)
|
||||
{
|
||||
if (p == NULL)
|
||||
uiprivImplBug("attempt to uiprivFree(NULL)");
|
||||
p = BASE(p);
|
||||
g_free(p);
|
||||
if (g_ptr_array_remove(allocations, p) == FALSE)
|
||||
uiprivImplBug("%p not found in allocations array in uiprivFree()", p);
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
// 4 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// notes:
|
||||
// - G_DECLARE_DERIVABLE/FINAL_INTERFACE() requires glib 2.44 and that's starting with debian stretch (testing) (GTK+ 3.18) and ubuntu 15.04 (GTK+ 3.14) - debian jessie has 2.42 (GTK+ 3.14)
|
||||
#define areaWidgetType (areaWidget_get_type())
|
||||
#define areaWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), areaWidgetType, areaWidget))
|
||||
#define isAreaWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), areaWidgetType))
|
||||
#define areaWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), areaWidgetType, areaWidgetClass))
|
||||
#define isAreaWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), areaWidget))
|
||||
#define getAreaWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), areaWidgetType, areaWidgetClass))
|
||||
|
||||
typedef struct areaWidget areaWidget;
|
||||
typedef struct areaWidgetClass areaWidgetClass;
|
||||
|
||||
struct areaWidget {
|
||||
GtkDrawingArea parent_instance;
|
||||
uiArea *a;
|
||||
// construct-only parameters aare not set until after the init() function has returned
|
||||
// we need this particular object available during init(), so put it here instead of in uiArea
|
||||
// keep a pointer in uiArea for convenience, though
|
||||
uiprivClickCounter cc;
|
||||
};
|
||||
|
||||
struct areaWidgetClass {
|
||||
GtkDrawingAreaClass parent_class;
|
||||
};
|
||||
|
||||
struct uiArea {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget; // either swidget or areaWidget depending on whether it is scrolling
|
||||
|
||||
GtkWidget *swidget;
|
||||
GtkContainer *scontainer;
|
||||
GtkScrolledWindow *sw;
|
||||
|
||||
GtkWidget *areaWidget;
|
||||
GtkDrawingArea *drawingArea;
|
||||
areaWidget *area;
|
||||
|
||||
uiAreaHandler *ah;
|
||||
|
||||
gboolean scrolling;
|
||||
int scrollWidth;
|
||||
int scrollHeight;
|
||||
|
||||
// note that this is a pointer; see above
|
||||
uiprivClickCounter *cc;
|
||||
|
||||
// for user window drags
|
||||
GdkEventButton *dragevent;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(areaWidget, areaWidget, GTK_TYPE_DRAWING_AREA)
|
||||
|
||||
static void areaWidget_init(areaWidget *aw)
|
||||
{
|
||||
// for events
|
||||
gtk_widget_add_events(GTK_WIDGET(aw),
|
||||
GDK_POINTER_MOTION_MASK |
|
||||
GDK_BUTTON_MOTION_MASK |
|
||||
GDK_BUTTON_PRESS_MASK |
|
||||
GDK_BUTTON_RELEASE_MASK |
|
||||
GDK_KEY_PRESS_MASK |
|
||||
GDK_KEY_RELEASE_MASK |
|
||||
GDK_ENTER_NOTIFY_MASK |
|
||||
GDK_LEAVE_NOTIFY_MASK);
|
||||
|
||||
gtk_widget_set_can_focus(GTK_WIDGET(aw), TRUE);
|
||||
|
||||
uiprivClickCounterReset(&(aw->cc));
|
||||
}
|
||||
|
||||
static void areaWidget_dispose(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(areaWidget_parent_class)->dispose(obj);
|
||||
}
|
||||
|
||||
static void areaWidget_finalize(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(areaWidget_parent_class)->finalize(obj);
|
||||
}
|
||||
|
||||
static void areaWidget_size_allocate(GtkWidget *w, GtkAllocation *allocation)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
|
||||
// GtkDrawingArea has a size_allocate() implementation; we need to call it
|
||||
// this will call gtk_widget_set_allocation() for us
|
||||
GTK_WIDGET_CLASS(areaWidget_parent_class)->size_allocate(w, allocation);
|
||||
|
||||
if (!a->scrolling)
|
||||
// we must redraw everything on resize because Windows requires it
|
||||
// TODO https://developer.gnome.org/gtk3/3.10/GtkWidget.html#gtk-widget-set-redraw-on-allocate ?
|
||||
// TODO drop this rule; it was stupid and documenting this was stupid — let platforms where it matters do it on their own
|
||||
// TODO or do we not, for parity of performance?
|
||||
gtk_widget_queue_resize(w);
|
||||
}
|
||||
|
||||
static void loadAreaSize(uiArea *a, double *width, double *height)
|
||||
{
|
||||
GtkAllocation allocation;
|
||||
|
||||
*width = 0;
|
||||
*height = 0;
|
||||
// don't provide size information for scrolling areas
|
||||
if (!a->scrolling) {
|
||||
gtk_widget_get_allocation(a->areaWidget, &allocation);
|
||||
// these are already in drawing space coordinates
|
||||
// for drawing, the size of drawing space has the same value as the widget allocation
|
||||
// thanks to tristan in irc.gimp.net/#gtk+
|
||||
*width = allocation.width;
|
||||
*height = allocation.height;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean areaWidget_draw(GtkWidget *w, cairo_t *cr)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
uiAreaDrawParams dp;
|
||||
double clipX0, clipY0, clipX1, clipY1;
|
||||
|
||||
dp.Context = uiprivNewContext(cr,
|
||||
gtk_widget_get_style_context(a->widget));
|
||||
|
||||
loadAreaSize(a, &(dp.AreaWidth), &(dp.AreaHeight));
|
||||
|
||||
cairo_clip_extents(cr, &clipX0, &clipY0, &clipX1, &clipY1);
|
||||
dp.ClipX = clipX0;
|
||||
dp.ClipY = clipY0;
|
||||
dp.ClipWidth = clipX1 - clipX0;
|
||||
dp.ClipHeight = clipY1 - clipY0;
|
||||
|
||||
// no need to save or restore the graphics state to reset transformations; GTK+ does that for us
|
||||
(*(a->ah->Draw))(a->ah, a, &dp);
|
||||
|
||||
uiprivFreeContext(dp.Context);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// to do this properly for scrolling areas, we need to
|
||||
// - return the same value for min and nat
|
||||
// - call gtk_widget_queue_resize() when the size changes
|
||||
// thanks to Company in irc.gimp.net/#gtk+
|
||||
static void areaWidget_get_preferred_height(GtkWidget *w, gint *min, gint *nat)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
|
||||
// always chain up just in case
|
||||
GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_height(w, min, nat);
|
||||
if (a->scrolling) {
|
||||
*min = a->scrollHeight;
|
||||
*nat = a->scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
static void areaWidget_get_preferred_width(GtkWidget *w, gint *min, gint *nat)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
|
||||
// always chain up just in case
|
||||
GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_width(w, min, nat);
|
||||
if (a->scrolling) {
|
||||
*min = a->scrollWidth;
|
||||
*nat = a->scrollWidth;
|
||||
}
|
||||
}
|
||||
|
||||
static guint translateModifiers(guint state, GdkWindow *window)
|
||||
{
|
||||
GdkModifierType statetype;
|
||||
|
||||
// GDK doesn't initialize the modifier flags fully; we have to explicitly tell it to (thanks to Daniel_S and daniels (two different people) in irc.gimp.net/#gtk+)
|
||||
statetype = state;
|
||||
gdk_keymap_add_virtual_modifiers(
|
||||
gdk_keymap_get_for_display(gdk_window_get_display(window)),
|
||||
&statetype);
|
||||
return statetype;
|
||||
}
|
||||
|
||||
static uiModifiers toModifiers(guint state)
|
||||
{
|
||||
uiModifiers m;
|
||||
|
||||
m = 0;
|
||||
if ((state & GDK_CONTROL_MASK) != 0)
|
||||
m |= uiModifierCtrl;
|
||||
if ((state & GDK_META_MASK) != 0)
|
||||
m |= uiModifierAlt;
|
||||
if ((state & GDK_MOD1_MASK) != 0) // GTK+ itself requires this to be Alt (just read through gtkaccelgroup.c)
|
||||
m |= uiModifierAlt;
|
||||
if ((state & GDK_SHIFT_MASK) != 0)
|
||||
m |= uiModifierShift;
|
||||
if ((state & GDK_SUPER_MASK) != 0)
|
||||
m |= uiModifierSuper;
|
||||
return m;
|
||||
}
|
||||
|
||||
// capture on drag is done automatically on GTK+
|
||||
static void finishMouseEvent(uiArea *a, uiAreaMouseEvent *me, guint mb, gdouble x, gdouble y, guint state, GdkWindow *window)
|
||||
{
|
||||
// on GTK+, mouse buttons 4-7 are for scrolling; if we got here, that's a mistake
|
||||
if (mb >= 4 && mb <= 7)
|
||||
return;
|
||||
// if the button ID >= 8, continue counting from 4, as in the MouseEvent spec
|
||||
if (me->Down >= 8)
|
||||
me->Down -= 4;
|
||||
if (me->Up >= 8)
|
||||
me->Up -= 4;
|
||||
|
||||
state = translateModifiers(state, window);
|
||||
me->Modifiers = toModifiers(state);
|
||||
|
||||
// the mb != # checks exclude the Up/Down button from Held
|
||||
me->Held1To64 = 0;
|
||||
if (mb != 1 && (state & GDK_BUTTON1_MASK) != 0)
|
||||
me->Held1To64 |= 1 << 0;
|
||||
if (mb != 2 && (state & GDK_BUTTON2_MASK) != 0)
|
||||
me->Held1To64 |= 1 << 1;
|
||||
if (mb != 3 && (state & GDK_BUTTON3_MASK) != 0)
|
||||
me->Held1To64 |= 1 << 2;
|
||||
// don't check GDK_BUTTON4_MASK or GDK_BUTTON5_MASK because those are for the scrolling buttons mentioned above
|
||||
// GDK expressly does not support any more buttons in the GdkModifierType; see https://git.gnome.org/browse/gtk+/tree/gdk/x11/gdkdevice-xi2.c#n763 (thanks mclasen in irc.gimp.net/#gtk+)
|
||||
|
||||
// these are already in drawing space coordinates
|
||||
// the size of drawing space has the same value as the widget allocation
|
||||
// thanks to tristan in irc.gimp.net/#gtk+
|
||||
me->X = x;
|
||||
me->Y = y;
|
||||
|
||||
loadAreaSize(a, &(me->AreaWidth), &(me->AreaHeight));
|
||||
|
||||
(*(a->ah->MouseEvent))(a->ah, a, me);
|
||||
}
|
||||
|
||||
static gboolean areaWidget_button_press_event(GtkWidget *w, GdkEventButton *e)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
gint maxTime, maxDistance;
|
||||
GtkSettings *settings;
|
||||
uiAreaMouseEvent me;
|
||||
|
||||
// clicking doesn't automatically transfer keyboard focus; we must do so manually (thanks tristan in irc.gimp.net/#gtk+)
|
||||
gtk_widget_grab_focus(w);
|
||||
|
||||
me.Down = e->button;
|
||||
me.Up = 0;
|
||||
|
||||
// we handle multiple clicks ourselves here, in the same way as we do on Windows
|
||||
if (e->type != GDK_BUTTON_PRESS)
|
||||
// ignore GDK's generated double-clicks and beyond
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
settings = gtk_widget_get_settings(w);
|
||||
g_object_get(settings,
|
||||
"gtk-double-click-time", &maxTime,
|
||||
"gtk-double-click-distance", &maxDistance,
|
||||
NULL);
|
||||
// don't unref settings; it's transfer-none (thanks gregier in irc.gimp.net/#gtk+)
|
||||
// e->time is guint32
|
||||
// e->x and e->y are floating-point; just make them 32-bit integers
|
||||
// maxTime and maxDistance... are gint, which *should* fit, hopefully...
|
||||
me.Count = uiprivClickCounterClick(a->cc, me.Down,
|
||||
e->x, e->y,
|
||||
e->time, maxTime,
|
||||
maxDistance, maxDistance);
|
||||
|
||||
// and set things up for window drags
|
||||
a->dragevent = e;
|
||||
finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window);
|
||||
a->dragevent = NULL;
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
static gboolean areaWidget_button_release_event(GtkWidget *w, GdkEventButton *e)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
uiAreaMouseEvent me;
|
||||
|
||||
me.Down = 0;
|
||||
me.Up = e->button;
|
||||
me.Count = 0;
|
||||
finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window);
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
static gboolean areaWidget_motion_notify_event(GtkWidget *w, GdkEventMotion *e)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
uiAreaMouseEvent me;
|
||||
|
||||
me.Down = 0;
|
||||
me.Up = 0;
|
||||
me.Count = 0;
|
||||
finishMouseEvent(a, &me, 0, e->x, e->y, e->state, e->window);
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
// we want switching away from the control to reset the double-click counter, like with WM_ACTIVATE on Windows
|
||||
// according to tristan in irc.gimp.net/#gtk+, doing this on both enter-notify-event and leave-notify-event is correct (and it seems to be true in my own tests; plus the events DO get sent when switching programs with the keyboard (just pointing that out))
|
||||
static gboolean onCrossing(areaWidget *aw, int left)
|
||||
{
|
||||
uiArea *a = aw->a;
|
||||
|
||||
(*(a->ah->MouseCrossed))(a->ah, a, left);
|
||||
uiprivClickCounterReset(a->cc);
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
static gboolean areaWidget_enter_notify_event(GtkWidget *w, GdkEventCrossing *e)
|
||||
{
|
||||
return onCrossing(areaWidget(w), 0);
|
||||
}
|
||||
|
||||
static gboolean areaWidget_leave_notify_event(GtkWidget *w, GdkEventCrossing *e)
|
||||
{
|
||||
return onCrossing(areaWidget(w), 1);
|
||||
}
|
||||
|
||||
// note: there is no equivalent to WM_CAPTURECHANGED on GTK+; there literally is no way to break a grab like that (at least not on X11 and Wayland)
|
||||
// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons
|
||||
// therefore, no DragBroken()
|
||||
|
||||
// we use GDK_KEY_Print as a sentinel because libui will never support the print screen key; that key belongs to the user
|
||||
|
||||
static const struct {
|
||||
guint keyval;
|
||||
uiExtKey extkey;
|
||||
} extKeys[] = {
|
||||
{ GDK_KEY_Escape, uiExtKeyEscape },
|
||||
{ GDK_KEY_Insert, uiExtKeyInsert },
|
||||
{ GDK_KEY_Delete, uiExtKeyDelete },
|
||||
{ GDK_KEY_Home, uiExtKeyHome },
|
||||
{ GDK_KEY_End, uiExtKeyEnd },
|
||||
{ GDK_KEY_Page_Up, uiExtKeyPageUp },
|
||||
{ GDK_KEY_Page_Down, uiExtKeyPageDown },
|
||||
{ GDK_KEY_Up, uiExtKeyUp },
|
||||
{ GDK_KEY_Down, uiExtKeyDown },
|
||||
{ GDK_KEY_Left, uiExtKeyLeft },
|
||||
{ GDK_KEY_Right, uiExtKeyRight },
|
||||
{ GDK_KEY_F1, uiExtKeyF1 },
|
||||
{ GDK_KEY_F2, uiExtKeyF2 },
|
||||
{ GDK_KEY_F3, uiExtKeyF3 },
|
||||
{ GDK_KEY_F4, uiExtKeyF4 },
|
||||
{ GDK_KEY_F5, uiExtKeyF5 },
|
||||
{ GDK_KEY_F6, uiExtKeyF6 },
|
||||
{ GDK_KEY_F7, uiExtKeyF7 },
|
||||
{ GDK_KEY_F8, uiExtKeyF8 },
|
||||
{ GDK_KEY_F9, uiExtKeyF9 },
|
||||
{ GDK_KEY_F10, uiExtKeyF10 },
|
||||
{ GDK_KEY_F11, uiExtKeyF11 },
|
||||
{ GDK_KEY_F12, uiExtKeyF12 },
|
||||
// numpad numeric keys and . are handled in events.c
|
||||
{ GDK_KEY_KP_Enter, uiExtKeyNEnter },
|
||||
{ GDK_KEY_KP_Add, uiExtKeyNAdd },
|
||||
{ GDK_KEY_KP_Subtract, uiExtKeyNSubtract },
|
||||
{ GDK_KEY_KP_Multiply, uiExtKeyNMultiply },
|
||||
{ GDK_KEY_KP_Divide, uiExtKeyNDivide },
|
||||
{ GDK_KEY_Print, 0 },
|
||||
};
|
||||
|
||||
static const struct {
|
||||
guint keyval;
|
||||
uiModifiers mod;
|
||||
} modKeys[] = {
|
||||
{ GDK_KEY_Control_L, uiModifierCtrl },
|
||||
{ GDK_KEY_Control_R, uiModifierCtrl },
|
||||
{ GDK_KEY_Alt_L, uiModifierAlt },
|
||||
{ GDK_KEY_Alt_R, uiModifierAlt },
|
||||
{ GDK_KEY_Meta_L, uiModifierAlt },
|
||||
{ GDK_KEY_Meta_R, uiModifierAlt },
|
||||
{ GDK_KEY_Shift_L, uiModifierShift },
|
||||
{ GDK_KEY_Shift_R, uiModifierShift },
|
||||
{ GDK_KEY_Super_L, uiModifierSuper },
|
||||
{ GDK_KEY_Super_R, uiModifierSuper },
|
||||
{ GDK_KEY_Print, 0 },
|
||||
};
|
||||
|
||||
static int areaKeyEvent(uiArea *a, int up, GdkEventKey *e)
|
||||
{
|
||||
uiAreaKeyEvent ke;
|
||||
guint state;
|
||||
int i;
|
||||
|
||||
ke.Key = 0;
|
||||
ke.ExtKey = 0;
|
||||
ke.Modifier = 0;
|
||||
|
||||
state = translateModifiers(e->state, e->window);
|
||||
ke.Modifiers = toModifiers(state);
|
||||
|
||||
ke.Up = up;
|
||||
|
||||
for (i = 0; extKeys[i].keyval != GDK_KEY_Print; i++)
|
||||
if (extKeys[i].keyval == e->keyval) {
|
||||
ke.ExtKey = extKeys[i].extkey;
|
||||
goto keyFound;
|
||||
}
|
||||
|
||||
for (i = 0; modKeys[i].keyval != GDK_KEY_Print; i++)
|
||||
if (modKeys[i].keyval == e->keyval) {
|
||||
ke.Modifier = modKeys[i].mod;
|
||||
// don't include the modifier in ke.Modifiers
|
||||
ke.Modifiers &= ~ke.Modifier;
|
||||
goto keyFound;
|
||||
}
|
||||
|
||||
if (uiprivFromScancode(e->hardware_keycode - 8, &ke))
|
||||
goto keyFound;
|
||||
|
||||
// no supported key found; treat as unhandled
|
||||
return 0;
|
||||
|
||||
keyFound:
|
||||
return (*(a->ah->KeyEvent))(a->ah, a, &ke);
|
||||
}
|
||||
|
||||
static gboolean areaWidget_key_press_event(GtkWidget *w, GdkEventKey *e)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
|
||||
if (areaKeyEvent(a, 0, e))
|
||||
return GDK_EVENT_STOP;
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
static gboolean areaWidget_key_release_event(GtkWidget *w, GdkEventKey *e)
|
||||
{
|
||||
areaWidget *aw = areaWidget(w);
|
||||
uiArea *a = aw->a;
|
||||
|
||||
if (areaKeyEvent(a, 1, e))
|
||||
return GDK_EVENT_STOP;
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
enum {
|
||||
pArea = 1,
|
||||
nProps,
|
||||
};
|
||||
|
||||
static GParamSpec *pspecArea;
|
||||
|
||||
static void areaWidget_set_property(GObject *obj, guint prop, const GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
areaWidget *aw = areaWidget(obj);
|
||||
|
||||
switch (prop) {
|
||||
case pArea:
|
||||
aw->a = (uiArea *) g_value_get_pointer(value);
|
||||
aw->a->cc = &(aw->cc);
|
||||
return;
|
||||
}
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
|
||||
}
|
||||
|
||||
static void areaWidget_get_property(GObject *obj, guint prop, GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
|
||||
}
|
||||
|
||||
static void areaWidget_class_init(areaWidgetClass *class)
|
||||
{
|
||||
G_OBJECT_CLASS(class)->dispose = areaWidget_dispose;
|
||||
G_OBJECT_CLASS(class)->finalize = areaWidget_finalize;
|
||||
G_OBJECT_CLASS(class)->set_property = areaWidget_set_property;
|
||||
G_OBJECT_CLASS(class)->get_property = areaWidget_get_property;
|
||||
|
||||
GTK_WIDGET_CLASS(class)->size_allocate = areaWidget_size_allocate;
|
||||
GTK_WIDGET_CLASS(class)->draw = areaWidget_draw;
|
||||
GTK_WIDGET_CLASS(class)->get_preferred_height = areaWidget_get_preferred_height;
|
||||
GTK_WIDGET_CLASS(class)->get_preferred_width = areaWidget_get_preferred_width;
|
||||
GTK_WIDGET_CLASS(class)->button_press_event = areaWidget_button_press_event;
|
||||
GTK_WIDGET_CLASS(class)->button_release_event = areaWidget_button_release_event;
|
||||
GTK_WIDGET_CLASS(class)->motion_notify_event = areaWidget_motion_notify_event;
|
||||
GTK_WIDGET_CLASS(class)->enter_notify_event = areaWidget_enter_notify_event;
|
||||
GTK_WIDGET_CLASS(class)->leave_notify_event = areaWidget_leave_notify_event;
|
||||
GTK_WIDGET_CLASS(class)->key_press_event = areaWidget_key_press_event;
|
||||
GTK_WIDGET_CLASS(class)->key_release_event = areaWidget_key_release_event;
|
||||
|
||||
pspecArea = g_param_spec_pointer("libui-area",
|
||||
"libui-area",
|
||||
"uiArea.",
|
||||
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
|
||||
g_object_class_install_property(G_OBJECT_CLASS(class), pArea, pspecArea);
|
||||
}
|
||||
|
||||
// control implementation
|
||||
|
||||
uiUnixControlAllDefaults(uiArea)
|
||||
|
||||
void uiAreaSetSize(uiArea *a, int width, int height)
|
||||
{
|
||||
if (!a->scrolling)
|
||||
uiprivUserBug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a);
|
||||
a->scrollWidth = width;
|
||||
a->scrollHeight = height;
|
||||
gtk_widget_queue_resize(a->areaWidget);
|
||||
}
|
||||
|
||||
void uiAreaQueueRedrawAll(uiArea *a)
|
||||
{
|
||||
gtk_widget_queue_draw(a->areaWidget);
|
||||
}
|
||||
|
||||
void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height)
|
||||
{
|
||||
// TODO
|
||||
// TODO adjust adjustments and find source for that
|
||||
}
|
||||
|
||||
void uiAreaBeginUserWindowMove(uiArea *a)
|
||||
{
|
||||
GtkWidget *toplevel;
|
||||
|
||||
if (a->dragevent == NULL)
|
||||
uiprivUserBug("cannot call uiAreaBeginUserWindowMove() outside of a Mouse() with Down != 0");
|
||||
// TODO don't we have a libui function for this? did I scrap it?
|
||||
// TODO widget or areaWidget?
|
||||
toplevel = gtk_widget_get_toplevel(a->widget);
|
||||
if (toplevel == NULL) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
// the docs say to do this
|
||||
if (!gtk_widget_is_toplevel(toplevel)) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
if (!GTK_IS_WINDOW(toplevel)) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
gtk_window_begin_move_drag(GTK_WINDOW(toplevel),
|
||||
a->dragevent->button,
|
||||
a->dragevent->x_root, // TODO are these correct?
|
||||
a->dragevent->y_root,
|
||||
a->dragevent->time);
|
||||
}
|
||||
|
||||
static const GdkWindowEdge edges[] = {
|
||||
[uiWindowResizeEdgeLeft] = GDK_WINDOW_EDGE_WEST,
|
||||
[uiWindowResizeEdgeTop] = GDK_WINDOW_EDGE_NORTH,
|
||||
[uiWindowResizeEdgeRight] = GDK_WINDOW_EDGE_EAST,
|
||||
[uiWindowResizeEdgeBottom] = GDK_WINDOW_EDGE_SOUTH,
|
||||
[uiWindowResizeEdgeTopLeft] = GDK_WINDOW_EDGE_NORTH_WEST,
|
||||
[uiWindowResizeEdgeTopRight] = GDK_WINDOW_EDGE_NORTH_EAST,
|
||||
[uiWindowResizeEdgeBottomLeft] = GDK_WINDOW_EDGE_SOUTH_WEST,
|
||||
[uiWindowResizeEdgeBottomRight] = GDK_WINDOW_EDGE_SOUTH_EAST,
|
||||
};
|
||||
|
||||
void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge)
|
||||
{
|
||||
GtkWidget *toplevel;
|
||||
|
||||
if (a->dragevent == NULL)
|
||||
uiprivUserBug("cannot call uiAreaBeginUserWindowResize() outside of a Mouse() with Down != 0");
|
||||
// TODO don't we have a libui function for this? did I scrap it?
|
||||
// TODO widget or areaWidget?
|
||||
toplevel = gtk_widget_get_toplevel(a->widget);
|
||||
if (toplevel == NULL) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
// the docs say to do this
|
||||
if (!gtk_widget_is_toplevel(toplevel)) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
if (!GTK_IS_WINDOW(toplevel)) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
gtk_window_begin_resize_drag(GTK_WINDOW(toplevel),
|
||||
edges[edge],
|
||||
a->dragevent->button,
|
||||
a->dragevent->x_root, // TODO are these correct?
|
||||
a->dragevent->y_root,
|
||||
a->dragevent->time);
|
||||
}
|
||||
|
||||
uiArea *uiNewArea(uiAreaHandler *ah)
|
||||
{
|
||||
uiArea *a;
|
||||
|
||||
uiUnixNewControl(uiArea, a);
|
||||
|
||||
a->ah = ah;
|
||||
a->scrolling = FALSE;
|
||||
|
||||
a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType,
|
||||
"libui-area", a,
|
||||
NULL));
|
||||
a->drawingArea = GTK_DRAWING_AREA(a->areaWidget);
|
||||
a->area = areaWidget(a->areaWidget);
|
||||
|
||||
a->widget = a->areaWidget;
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height)
|
||||
{
|
||||
uiArea *a;
|
||||
|
||||
uiUnixNewControl(uiArea, a);
|
||||
|
||||
a->ah = ah;
|
||||
a->scrolling = TRUE;
|
||||
a->scrollWidth = width;
|
||||
a->scrollHeight = height;
|
||||
|
||||
a->swidget = gtk_scrolled_window_new(NULL, NULL);
|
||||
a->scontainer = GTK_CONTAINER(a->swidget);
|
||||
a->sw = GTK_SCROLLED_WINDOW(a->swidget);
|
||||
|
||||
a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType,
|
||||
"libui-area", a,
|
||||
NULL));
|
||||
a->drawingArea = GTK_DRAWING_AREA(a->areaWidget);
|
||||
a->area = areaWidget(a->areaWidget);
|
||||
|
||||
a->widget = a->swidget;
|
||||
|
||||
gtk_container_add(a->scontainer, a->areaWidget);
|
||||
// and make the area visible; only the scrolled window's visibility is controlled by libui
|
||||
gtk_widget_show(a->areaWidget);
|
||||
|
||||
return a;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 12 february 2017
|
||||
#include "uipriv_unix.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
// TODO pango alpha attributes turn 0 into 65535 :|
|
||||
|
||||
// TODO make this name less generic?
|
||||
struct foreachParams {
|
||||
PangoAttrList *attrs;
|
||||
};
|
||||
|
||||
static void addattr(struct foreachParams *p, size_t start, size_t end, PangoAttribute *attr)
|
||||
{
|
||||
if (attr == NULL) // in case of a future attribute
|
||||
return;
|
||||
attr->start_index = start;
|
||||
attr->end_index = end;
|
||||
pango_attr_list_insert(p->attrs, attr);
|
||||
}
|
||||
|
||||
static uiForEach processAttribute(const uiAttributedString *s, const uiAttribute *attr, size_t start, size_t end, void *data)
|
||||
{
|
||||
struct foreachParams *p = (struct foreachParams *) data;
|
||||
double r, g, b, a;
|
||||
PangoUnderline underline;
|
||||
uiUnderlineColor colorType;
|
||||
const uiOpenTypeFeatures *features;
|
||||
GString *featurestr;
|
||||
|
||||
switch (uiAttributeGetType(attr)) {
|
||||
case uiAttributeTypeFamily:
|
||||
addattr(p, start, end,
|
||||
pango_attr_family_new(uiAttributeFamily(attr)));
|
||||
break;
|
||||
case uiAttributeTypeSize:
|
||||
addattr(p, start, end,
|
||||
pango_attr_size_new(cairoToPango(uiAttributeSize(attr))));
|
||||
break;
|
||||
case uiAttributeTypeWeight:
|
||||
// TODO reverse the misalignment from drawtext.c if it is corrected
|
||||
addattr(p, start, end,
|
||||
pango_attr_weight_new(uiprivWeightToPangoWeight(uiAttributeWeight(attr))));
|
||||
break;
|
||||
case uiAttributeTypeItalic:
|
||||
addattr(p, start, end,
|
||||
pango_attr_style_new(uiprivItalicToPangoStyle(uiAttributeItalic(attr))));
|
||||
break;
|
||||
case uiAttributeTypeStretch:
|
||||
addattr(p, start, end,
|
||||
pango_attr_stretch_new(uiprivStretchToPangoStretch(uiAttributeStretch(attr))));
|
||||
break;
|
||||
case uiAttributeTypeColor:
|
||||
uiAttributeColor(attr, &r, &g, &b, &a);
|
||||
addattr(p, start, end,
|
||||
pango_attr_foreground_new(
|
||||
(guint16) (r * 65535.0),
|
||||
(guint16) (g * 65535.0),
|
||||
(guint16) (b * 65535.0)));
|
||||
addattr(p, start, end,
|
||||
uiprivFUTURE_pango_attr_foreground_alpha_new(
|
||||
(guint16) (a * 65535.0)));
|
||||
break;
|
||||
case uiAttributeTypeBackground:
|
||||
// TODO make sure this works properly with line paragraph spacings (after figuring out what that means, of course)
|
||||
uiAttributeColor(attr, &r, &g, &b, &a);
|
||||
addattr(p, start, end,
|
||||
pango_attr_background_new(
|
||||
(guint16) (r * 65535.0),
|
||||
(guint16) (g * 65535.0),
|
||||
(guint16) (b * 65535.0)));
|
||||
addattr(p, start, end,
|
||||
uiprivFUTURE_pango_attr_background_alpha_new(
|
||||
(guint16) (a * 65535.0)));
|
||||
break;
|
||||
case uiAttributeTypeUnderline:
|
||||
switch (uiAttributeUnderline(attr)) {
|
||||
case uiUnderlineNone:
|
||||
underline = PANGO_UNDERLINE_NONE;
|
||||
break;
|
||||
case uiUnderlineSingle:
|
||||
underline = PANGO_UNDERLINE_SINGLE;
|
||||
break;
|
||||
case uiUnderlineDouble:
|
||||
underline = PANGO_UNDERLINE_DOUBLE;
|
||||
break;
|
||||
case uiUnderlineSuggestion:
|
||||
underline = PANGO_UNDERLINE_ERROR;
|
||||
break;
|
||||
}
|
||||
addattr(p, start, end,
|
||||
pango_attr_underline_new(underline));
|
||||
break;
|
||||
case uiAttributeTypeUnderlineColor:
|
||||
uiAttributeUnderlineColor(attr, &colorType, &r, &g, &b, &a);
|
||||
switch (colorType) {
|
||||
case uiUnderlineColorCustom:
|
||||
addattr(p, start, end,
|
||||
pango_attr_underline_color_new(
|
||||
(guint16) (r * 65535.0),
|
||||
(guint16) (g * 65535.0),
|
||||
(guint16) (b * 65535.0)));
|
||||
break;
|
||||
case uiUnderlineColorSpelling:
|
||||
// TODO GtkTextView style property error-underline-color
|
||||
addattr(p, start, end,
|
||||
pango_attr_underline_color_new(65535, 0, 0));
|
||||
break;
|
||||
case uiUnderlineColorGrammar:
|
||||
// TODO find a more appropriate color
|
||||
addattr(p, start, end,
|
||||
pango_attr_underline_color_new(0, 65535, 0));
|
||||
break;
|
||||
case uiUnderlineColorAuxiliary:
|
||||
// TODO find a more appropriate color
|
||||
addattr(p, start, end,
|
||||
pango_attr_underline_color_new(0, 0, 65535));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case uiAttributeTypeFeatures:
|
||||
// only generate an attribute if the features object is not NULL
|
||||
// TODO state that this is allowed
|
||||
features = uiAttributeFeatures(attr);
|
||||
if (features == NULL)
|
||||
break;
|
||||
featurestr = uiprivOpenTypeFeaturesToPangoCSSFeaturesString(features);
|
||||
addattr(p, start, end,
|
||||
uiprivFUTURE_pango_attr_font_features_new(featurestr->str));
|
||||
g_string_free(featurestr, TRUE);
|
||||
break;
|
||||
default:
|
||||
// TODO complain
|
||||
;
|
||||
}
|
||||
return uiForEachContinue;
|
||||
}
|
||||
|
||||
PangoAttrList *uiprivAttributedStringToPangoAttrList(uiDrawTextLayoutParams *p)
|
||||
{
|
||||
struct foreachParams fep;
|
||||
|
||||
fep.attrs = pango_attr_list_new();
|
||||
uiAttributedStringForEachAttribute(p->String, processAttribute, &fep);
|
||||
return fep.attrs;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 11 march 2018
|
||||
#include "../common/attrstr.h"
|
||||
|
||||
// See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description
|
||||
// For the conversion, see https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-to-double and https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-from-double
|
||||
#define pangoToCairo(pango) (pango_units_to_double(pango))
|
||||
#define cairoToPango(cairo) (pango_units_from_double(cairo))
|
||||
|
||||
// opentype.c
|
||||
extern GString *uiprivOpenTypeFeaturesToPangoCSSFeaturesString(const uiOpenTypeFeatures *otf);
|
||||
|
||||
// fontmatch.c
|
||||
extern PangoWeight uiprivWeightToPangoWeight(uiTextWeight w);
|
||||
extern PangoStyle uiprivItalicToPangoStyle(uiTextItalic i);
|
||||
extern PangoStretch uiprivStretchToPangoStretch(uiTextStretch s);
|
||||
extern PangoFontDescription *uiprivFontDescriptorToPangoFontDescription(const uiFontDescriptor *uidesc);
|
||||
extern void uiprivFontDescriptorFromPangoFontDescription(PangoFontDescription *pdesc, uiFontDescriptor *uidesc);
|
||||
|
||||
// attrstr.c
|
||||
extern PangoAttrList *uiprivAttributedStringToPangoAttrList(uiDrawTextLayoutParams *p);
|
||||
@@ -0,0 +1,164 @@
|
||||
// 7 april 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct boxChild {
|
||||
uiControl *c;
|
||||
int stretchy;
|
||||
gboolean oldhexpand;
|
||||
GtkAlign oldhalign;
|
||||
gboolean oldvexpand;
|
||||
GtkAlign oldvalign;
|
||||
};
|
||||
|
||||
struct uiBox {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkBox *box;
|
||||
GArray *controls;
|
||||
int vertical;
|
||||
int padded;
|
||||
GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiBox)
|
||||
|
||||
#define ctrl(b, i) &g_array_index(b->controls, struct boxChild, i)
|
||||
|
||||
static void uiBoxDestroy(uiControl *c)
|
||||
{
|
||||
uiBox *b = uiBox(c);
|
||||
struct boxChild *bc;
|
||||
guint i;
|
||||
|
||||
// kill the size group
|
||||
g_object_unref(b->stretchygroup);
|
||||
// free all controls
|
||||
for (i = 0; i < b->controls->len; i++) {
|
||||
bc = ctrl(b, i);
|
||||
uiControlSetParent(bc->c, NULL);
|
||||
// and make sure the widget itself stays alive
|
||||
uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE);
|
||||
uiControlDestroy(bc->c);
|
||||
}
|
||||
g_array_free(b->controls, TRUE);
|
||||
// and then ourselves
|
||||
g_object_unref(b->widget);
|
||||
uiFreeControl(uiControl(b));
|
||||
}
|
||||
|
||||
void uiBoxAppend(uiBox *b, uiControl *c, int stretchy)
|
||||
{
|
||||
struct boxChild bc;
|
||||
GtkWidget *widget;
|
||||
|
||||
bc.c = c;
|
||||
bc.stretchy = stretchy;
|
||||
widget = GTK_WIDGET(uiControlHandle(bc.c));
|
||||
bc.oldhexpand = gtk_widget_get_hexpand(widget);
|
||||
bc.oldhalign = gtk_widget_get_halign(widget);
|
||||
bc.oldvexpand = gtk_widget_get_vexpand(widget);
|
||||
bc.oldvalign = gtk_widget_get_valign(widget);
|
||||
|
||||
if (bc.stretchy) {
|
||||
if (b->vertical) {
|
||||
gtk_widget_set_vexpand(widget, TRUE);
|
||||
gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
|
||||
} else {
|
||||
gtk_widget_set_hexpand(widget, TRUE);
|
||||
gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
|
||||
}
|
||||
gtk_size_group_add_widget(b->stretchygroup, widget);
|
||||
} else
|
||||
if (b->vertical)
|
||||
gtk_widget_set_vexpand(widget, FALSE);
|
||||
else
|
||||
gtk_widget_set_hexpand(widget, FALSE);
|
||||
// and make them fill the opposite direction
|
||||
if (b->vertical) {
|
||||
gtk_widget_set_hexpand(widget, TRUE);
|
||||
gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
|
||||
} else {
|
||||
gtk_widget_set_vexpand(widget, TRUE);
|
||||
gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
|
||||
}
|
||||
|
||||
uiControlSetParent(bc.c, uiControl(b));
|
||||
uiUnixControlSetContainer(uiUnixControl(bc.c), b->container, FALSE);
|
||||
g_array_append_val(b->controls, bc);
|
||||
}
|
||||
|
||||
void uiBoxDelete(uiBox *b, int index)
|
||||
{
|
||||
struct boxChild *bc;
|
||||
GtkWidget *widget;
|
||||
|
||||
bc = ctrl(b, index);
|
||||
widget = GTK_WIDGET(uiControlHandle(bc->c));
|
||||
|
||||
uiControlSetParent(bc->c, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE);
|
||||
|
||||
if (bc->stretchy)
|
||||
gtk_size_group_remove_widget(b->stretchygroup, widget);
|
||||
gtk_widget_set_hexpand(widget, bc->oldhexpand);
|
||||
gtk_widget_set_halign(widget, bc->oldhalign);
|
||||
gtk_widget_set_vexpand(widget, bc->oldvexpand);
|
||||
gtk_widget_set_valign(widget, bc->oldvalign);
|
||||
|
||||
g_array_remove_index(b->controls, index);
|
||||
}
|
||||
|
||||
int uiBoxNumChildren(uiBox *b)
|
||||
{
|
||||
return (int) b->controls->len;
|
||||
}
|
||||
|
||||
int uiBoxPadded(uiBox *b)
|
||||
{
|
||||
return b->padded;
|
||||
}
|
||||
|
||||
void uiBoxSetPadded(uiBox *b, int padded)
|
||||
{
|
||||
b->padded = padded;
|
||||
if (b->padded)
|
||||
if (b->vertical)
|
||||
gtk_box_set_spacing(b->box, uiprivGTKYPadding);
|
||||
else
|
||||
gtk_box_set_spacing(b->box, uiprivGTKXPadding);
|
||||
else
|
||||
gtk_box_set_spacing(b->box, 0);
|
||||
}
|
||||
|
||||
static uiBox *finishNewBox(GtkOrientation orientation)
|
||||
{
|
||||
uiBox *b;
|
||||
|
||||
uiUnixNewControl(uiBox, b);
|
||||
|
||||
b->widget = gtk_box_new(orientation, 0);
|
||||
b->container = GTK_CONTAINER(b->widget);
|
||||
b->box = GTK_BOX(b->widget);
|
||||
|
||||
b->vertical = orientation == GTK_ORIENTATION_VERTICAL;
|
||||
|
||||
if (b->vertical)
|
||||
b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
|
||||
else
|
||||
b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
|
||||
|
||||
b->controls = g_array_new(FALSE, TRUE, sizeof (struct boxChild));
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
uiBox *uiNewHorizontalBox(void)
|
||||
{
|
||||
return finishNewBox(GTK_ORIENTATION_HORIZONTAL);
|
||||
}
|
||||
|
||||
uiBox *uiNewVerticalBox(void)
|
||||
{
|
||||
return finishNewBox(GTK_ORIENTATION_VERTICAL);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 10 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiButton {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkButton *button;
|
||||
void (*onClicked)(uiButton *, void *);
|
||||
void *onClickedData;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiButton)
|
||||
|
||||
static void onClicked(GtkButton *button, gpointer data)
|
||||
{
|
||||
uiButton *b = uiButton(data);
|
||||
|
||||
(*(b->onClicked))(b, b->onClickedData);
|
||||
}
|
||||
|
||||
static void defaultOnClicked(uiButton *b, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
char *uiButtonText(uiButton *b)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_button_get_label(b->button));
|
||||
}
|
||||
|
||||
void uiButtonSetText(uiButton *b, const char *text)
|
||||
{
|
||||
gtk_button_set_label(b->button, text);
|
||||
}
|
||||
|
||||
void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data)
|
||||
{
|
||||
b->onClicked = f;
|
||||
b->onClickedData = data;
|
||||
}
|
||||
|
||||
uiButton *uiNewButton(const char *text)
|
||||
{
|
||||
uiButton *b;
|
||||
|
||||
uiUnixNewControl(uiButton, b);
|
||||
|
||||
b->widget = gtk_button_new_with_label(text);
|
||||
b->button = GTK_BUTTON(b->widget);
|
||||
|
||||
g_signal_connect(b->widget, "clicked", G_CALLBACK(onClicked), b);
|
||||
uiButtonOnClicked(b, defaultOnClicked, NULL);
|
||||
|
||||
return b;
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// 28 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// TODOs
|
||||
// - it's a rather tight fit
|
||||
// - selected row text color is white (TODO not on 3.22)
|
||||
// - accessibility
|
||||
// - right side too big? (TODO reverify)
|
||||
|
||||
#define cellRendererButtonType (cellRendererButton_get_type())
|
||||
#define cellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), cellRendererButtonType, cellRendererButton))
|
||||
#define isCellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), cellRendererButtonType))
|
||||
#define cellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_CAST((class), cellRendererButtonType, cellRendererButtonClass))
|
||||
#define isCellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), cellRendererButton))
|
||||
#define getCellRendererButtonClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), cellRendererButtonType, cellRendererButtonClass))
|
||||
|
||||
typedef struct cellRendererButton cellRendererButton;
|
||||
typedef struct cellRendererButtonClass cellRendererButtonClass;
|
||||
|
||||
struct cellRendererButton {
|
||||
GtkCellRenderer parent_instance;
|
||||
char *text;
|
||||
};
|
||||
|
||||
struct cellRendererButtonClass {
|
||||
GtkCellRendererClass parent_class;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(cellRendererButton, cellRendererButton, GTK_TYPE_CELL_RENDERER)
|
||||
|
||||
static void cellRendererButton_init(cellRendererButton *c)
|
||||
{
|
||||
g_object_set(c, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
|
||||
// the standard cell renderers all do this
|
||||
gtk_cell_renderer_set_padding(GTK_CELL_RENDERER(c), 2, 2);
|
||||
}
|
||||
|
||||
static void cellRendererButton_dispose(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(cellRendererButton_parent_class)->dispose(obj);
|
||||
}
|
||||
|
||||
static void cellRendererButton_finalize(GObject *obj)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(obj);
|
||||
|
||||
if (c->text != NULL) {
|
||||
g_free(c->text);
|
||||
c->text = NULL;
|
||||
}
|
||||
G_OBJECT_CLASS(cellRendererButton_parent_class)->finalize(obj);
|
||||
}
|
||||
|
||||
static GtkSizeRequestMode cellRendererButton_get_request_mode(GtkCellRenderer *r)
|
||||
{
|
||||
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
|
||||
}
|
||||
|
||||
// this is basically what GtkCellRendererToggle did in 3.10 and does in 3.20, as well as what the Foreign Drawing gtk3-demo demo does
|
||||
// TODO how does this seem to work with highlight on 3.22, and does that work with 3.10 too
|
||||
static GtkStyleContext *setButtonStyle(GtkWidget *widget)
|
||||
{
|
||||
GtkStyleContext *base, *context;
|
||||
GtkWidgetPath *path;
|
||||
|
||||
base = gtk_widget_get_style_context(widget);
|
||||
context = gtk_style_context_new();
|
||||
|
||||
path = gtk_widget_path_copy(gtk_style_context_get_path(base));
|
||||
gtk_widget_path_append_type(path, G_TYPE_NONE);
|
||||
if (!uiprivFUTURE_gtk_widget_path_iter_set_object_name(path, -1, "button"))
|
||||
// not on 3.20; try the type
|
||||
gtk_widget_path_iter_set_object_type(path, -1, GTK_TYPE_BUTTON);
|
||||
|
||||
gtk_style_context_set_path(context, path);
|
||||
gtk_style_context_set_parent(context, base);
|
||||
// the gtk3-demo example (which says we need to do this) uses gtk_widget_path_iter_get_state(path, -1) but that's not available until 3.14
|
||||
// TODO make a future for that too
|
||||
gtk_style_context_set_state(context, gtk_style_context_get_state(base));
|
||||
gtk_widget_path_unref(path);
|
||||
|
||||
// and if the above widget path screwery stil doesn't work, this will
|
||||
gtk_style_context_add_class(context, GTK_STYLE_CLASS_BUTTON);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
void unsetButtonStyle(GtkStyleContext *context)
|
||||
{
|
||||
g_object_unref(context);
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static PangoLayout *cellRendererButtonPangoLayout(cellRendererButton *c, GtkWidget *widget)
|
||||
{
|
||||
PangoLayout *layout;
|
||||
|
||||
layout = gtk_widget_create_pango_layout(widget, c->text);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
|
||||
pango_layout_set_width(layout, -1);
|
||||
pango_layout_set_wrap(layout, PANGO_WRAP_CHAR);
|
||||
pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
|
||||
return layout;
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static void cellRendererButtonSize(cellRendererButton *c, GtkWidget *widget, PangoLayout *layout, const GdkRectangle *cell_area, gint *xoff, gint *yoff, gint *width, gint *height)
|
||||
{
|
||||
PangoRectangle rect;
|
||||
gint xpad, ypad;
|
||||
gfloat xalign, yalign;
|
||||
|
||||
gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
|
||||
pango_layout_get_pixel_extents(layout, NULL, &rect);
|
||||
if (rect.width > cell_area->width - (2 * xpad))
|
||||
rect.width = cell_area->width - (2 * xpad);
|
||||
if (rect.height > cell_area->height - (2 * ypad))
|
||||
rect.height = cell_area->height - (2 * ypad);
|
||||
|
||||
gtk_cell_renderer_get_alignment(GTK_CELL_RENDERER(c), &xalign, &yalign);
|
||||
if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL)
|
||||
xalign = 1.0 - xalign;
|
||||
if (xoff != NULL) {
|
||||
*xoff = cell_area->width - (rect.width + (2 * xpad));
|
||||
*xoff = (gint) ((gfloat) (*xoff) * xalign);
|
||||
}
|
||||
if (yoff != NULL) {
|
||||
*yoff = cell_area->height - (rect.height + (2 * ypad));
|
||||
*yoff = (gint) ((gfloat) (*yoff) * yalign);
|
||||
if (*yoff < 0)
|
||||
*yoff = 0;
|
||||
}
|
||||
if (width != NULL)
|
||||
*width = rect.width - (2 * xpad);
|
||||
if (height != NULL)
|
||||
*height = rect.height - (2 * ypad);
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static void cellRendererButton_get_preferred_width(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(r);
|
||||
gint xpad;
|
||||
PangoLayout *layout;
|
||||
PangoRectangle rect;
|
||||
gint out;
|
||||
|
||||
gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, NULL);
|
||||
|
||||
layout = cellRendererButtonPangoLayout(c, widget);
|
||||
pango_layout_get_extents(layout, NULL, &rect);
|
||||
g_object_unref(layout);
|
||||
|
||||
out = PANGO_PIXELS_CEIL(rect.width) + (2 * xpad);
|
||||
if (rect.x > 0)
|
||||
out += rect.x;
|
||||
if (minimum != NULL)
|
||||
*minimum = out;
|
||||
if (natural != NULL)
|
||||
*natural = out;
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static void cellRendererButton_get_preferred_height_for_width(GtkCellRenderer *r, GtkWidget *widget, gint width, gint *minimum, gint *natural)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(r);
|
||||
gint xpad, ypad;
|
||||
PangoLayout *layout;
|
||||
gint height;
|
||||
gint out;
|
||||
|
||||
gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
|
||||
|
||||
layout = cellRendererButtonPangoLayout(c, widget);
|
||||
pango_layout_set_width(layout, (width + (xpad * 2)) * PANGO_SCALE);
|
||||
pango_layout_get_pixel_size(layout, NULL, &height);
|
||||
g_object_unref(layout);
|
||||
|
||||
out = height + (ypad * 2);
|
||||
if (minimum != NULL)
|
||||
*minimum = out;
|
||||
if (natural != NULL)
|
||||
*natural = out;
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static void cellRendererButton_get_preferred_height(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural)
|
||||
{
|
||||
gint width;
|
||||
|
||||
gtk_cell_renderer_get_preferred_width(r, widget, &width, NULL);
|
||||
gtk_cell_renderer_get_preferred_height_for_width(r, widget, width, minimum, natural);
|
||||
}
|
||||
|
||||
// this is based on what GtkCellRendererText in GTK+ 3.22.30 does
|
||||
// TODO compare to 3.10.9 (https://gitlab.gnome.org/GNOME/gtk/blob/3.10.9/gtk/gtkcellrenderertext.c)
|
||||
static void cellRendererButton_get_aligned_area(GtkCellRenderer *r, GtkWidget *widget, GtkCellRendererState flags, const GdkRectangle *cell_area, GdkRectangle *aligned_area)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(r);
|
||||
PangoLayout *layout;
|
||||
gint xoff, yoff;
|
||||
gint width, height;
|
||||
|
||||
layout = cellRendererButtonPangoLayout(c, widget);
|
||||
cellRendererButtonSize(c, widget, layout, cell_area,
|
||||
&xoff, &yoff, &width, &height);
|
||||
|
||||
aligned_area->x = cell_area->x + xoff;
|
||||
aligned_area->y = cell_area->y + yoff;
|
||||
aligned_area->width = width;
|
||||
aligned_area->height = height;
|
||||
|
||||
g_object_unref(layout);
|
||||
}
|
||||
|
||||
// this is based on both what GtkCellRendererText on 3.22.30 does and what GtkCellRendererToggle does (TODO verify the latter; both on 3.10.9)
|
||||
static void cellRendererButton_render(GtkCellRenderer *r, cairo_t *cr, GtkWidget *widget, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(r);
|
||||
gint xpad, ypad;
|
||||
GdkRectangle alignedArea;
|
||||
gint xoff, yoff;
|
||||
GtkStyleContext *context;
|
||||
PangoLayout *layout;
|
||||
PangoRectangle rect;
|
||||
|
||||
gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
|
||||
layout = cellRendererButtonPangoLayout(c, widget);
|
||||
cellRendererButtonSize(c, widget, layout, cell_area,
|
||||
&xoff, &yoff, NULL, NULL);
|
||||
|
||||
context = setButtonStyle(widget);
|
||||
|
||||
gtk_render_background(context, cr,
|
||||
background_area->x + xpad,
|
||||
background_area->y + ypad,
|
||||
background_area->width - (xpad * 2),
|
||||
background_area->height - (ypad * 2));
|
||||
gtk_render_frame(context, cr,
|
||||
background_area->x + xpad,
|
||||
background_area->y + ypad,
|
||||
background_area->width - (xpad * 2),
|
||||
background_area->height - (ypad * 2));
|
||||
|
||||
pango_layout_get_pixel_extents(layout, NULL, &rect);
|
||||
xoff -= rect.x;
|
||||
gtk_render_layout(context, cr,
|
||||
cell_area->x + xoff + xpad,
|
||||
cell_area->y + yoff + ypad,
|
||||
layout);
|
||||
|
||||
unsetButtonStyle(context);
|
||||
g_object_unref(layout);
|
||||
}
|
||||
|
||||
static guint clickedSignal;
|
||||
|
||||
static gboolean cellRendererButton_activate(GtkCellRenderer *r, GdkEvent *e, GtkWidget *widget, const gchar *path, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags)
|
||||
{
|
||||
g_signal_emit(r, clickedSignal, 0, path);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static GParamSpec *props[2] = { NULL, NULL };
|
||||
|
||||
static void cellRendererButton_set_property(GObject *object, guint prop, const GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(object);
|
||||
|
||||
if (prop != 1) {
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec);
|
||||
return;
|
||||
}
|
||||
if (c->text != NULL)
|
||||
g_free(c->text);
|
||||
c->text = g_value_dup_string(value);
|
||||
// GtkCellRendererText doesn't queue a redraw; we won't either
|
||||
}
|
||||
|
||||
static void cellRendererButton_get_property(GObject *object, guint prop, GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
cellRendererButton *c = cellRendererButton(object);
|
||||
|
||||
if (prop != 1) {
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec);
|
||||
return;
|
||||
}
|
||||
g_value_set_string(value, c->text);
|
||||
}
|
||||
|
||||
static void cellRendererButton_class_init(cellRendererButtonClass *class)
|
||||
{
|
||||
G_OBJECT_CLASS(class)->dispose = cellRendererButton_dispose;
|
||||
G_OBJECT_CLASS(class)->finalize = cellRendererButton_finalize;
|
||||
G_OBJECT_CLASS(class)->set_property = cellRendererButton_set_property;
|
||||
G_OBJECT_CLASS(class)->get_property = cellRendererButton_get_property;
|
||||
GTK_CELL_RENDERER_CLASS(class)->get_request_mode = cellRendererButton_get_request_mode;
|
||||
GTK_CELL_RENDERER_CLASS(class)->get_preferred_width = cellRendererButton_get_preferred_width;
|
||||
GTK_CELL_RENDERER_CLASS(class)->get_preferred_height_for_width = cellRendererButton_get_preferred_height_for_width;
|
||||
GTK_CELL_RENDERER_CLASS(class)->get_preferred_height = cellRendererButton_get_preferred_height;
|
||||
// don't provide a get_preferred_width_for_height()
|
||||
GTK_CELL_RENDERER_CLASS(class)->get_aligned_area = cellRendererButton_get_aligned_area;
|
||||
// don't provide a get_size()
|
||||
GTK_CELL_RENDERER_CLASS(class)->render = cellRendererButton_render;
|
||||
GTK_CELL_RENDERER_CLASS(class)->activate = cellRendererButton_activate;
|
||||
// don't provide a start_editing()
|
||||
|
||||
props[1] = g_param_spec_string("text",
|
||||
"Text",
|
||||
"Button text",
|
||||
"",
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
|
||||
g_object_class_install_properties(G_OBJECT_CLASS(class), 2, props);
|
||||
|
||||
clickedSignal = g_signal_new("clicked",
|
||||
G_TYPE_FROM_CLASS(class),
|
||||
G_SIGNAL_RUN_LAST,
|
||||
0,
|
||||
NULL, NULL, NULL,
|
||||
G_TYPE_NONE,
|
||||
1, G_TYPE_STRING);
|
||||
}
|
||||
|
||||
GtkCellRenderer *uiprivNewCellRendererButton(void)
|
||||
{
|
||||
return GTK_CELL_RENDERER(g_object_new(cellRendererButtonType, NULL));
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 10 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiCheckbox {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkButton *button;
|
||||
GtkToggleButton *toggleButton;
|
||||
GtkCheckButton *checkButton;
|
||||
void (*onToggled)(uiCheckbox *, void *);
|
||||
void *onToggledData;
|
||||
gulong onToggledSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiCheckbox)
|
||||
|
||||
static void onToggled(GtkToggleButton *b, gpointer data)
|
||||
{
|
||||
uiCheckbox *c = uiCheckbox(data);
|
||||
|
||||
(*(c->onToggled))(c, c->onToggledData);
|
||||
}
|
||||
|
||||
static void defaultOnToggled(uiCheckbox *c, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
char *uiCheckboxText(uiCheckbox *c)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_button_get_label(c->button));
|
||||
}
|
||||
|
||||
void uiCheckboxSetText(uiCheckbox *c, const char *text)
|
||||
{
|
||||
gtk_button_set_label(GTK_BUTTON(c->button), text);
|
||||
}
|
||||
|
||||
void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data)
|
||||
{
|
||||
c->onToggled = f;
|
||||
c->onToggledData = data;
|
||||
}
|
||||
|
||||
int uiCheckboxChecked(uiCheckbox *c)
|
||||
{
|
||||
return gtk_toggle_button_get_active(c->toggleButton) != FALSE;
|
||||
}
|
||||
|
||||
void uiCheckboxSetChecked(uiCheckbox *c, int checked)
|
||||
{
|
||||
gboolean active;
|
||||
|
||||
active = FALSE;
|
||||
if (checked)
|
||||
active = TRUE;
|
||||
// we need to inhibit sending of ::toggled because this WILL send a ::toggled otherwise
|
||||
g_signal_handler_block(c->toggleButton, c->onToggledSignal);
|
||||
gtk_toggle_button_set_active(c->toggleButton, active);
|
||||
g_signal_handler_unblock(c->toggleButton, c->onToggledSignal);
|
||||
}
|
||||
|
||||
uiCheckbox *uiNewCheckbox(const char *text)
|
||||
{
|
||||
uiCheckbox *c;
|
||||
|
||||
uiUnixNewControl(uiCheckbox, c);
|
||||
|
||||
c->widget = gtk_check_button_new_with_label(text);
|
||||
c->button = GTK_BUTTON(c->widget);
|
||||
c->toggleButton = GTK_TOGGLE_BUTTON(c->widget);
|
||||
c->checkButton = GTK_CHECK_BUTTON(c->widget);
|
||||
|
||||
c->onToggledSignal = g_signal_connect(c->widget, "toggled", G_CALLBACK(onToggled), c);
|
||||
uiCheckboxOnToggled(c, defaultOnToggled, NULL);
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 28 august 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// This file contains helpers for managing child controls.
|
||||
|
||||
struct uiprivChild {
|
||||
uiControl *c;
|
||||
GtkWidget *widget;
|
||||
|
||||
gboolean oldhexpand;
|
||||
GtkAlign oldhalign;
|
||||
gboolean oldvexpand;
|
||||
GtkAlign oldvalign;
|
||||
|
||||
// Some children can be boxed; that is, they can have an optionally-margined box around them.
|
||||
// uiGroup, uiTab, and uiWindow all do this.
|
||||
GtkWidget *box;
|
||||
|
||||
// If the child is not boxed, this is its parent.
|
||||
// If the child is boxed, this is the box.
|
||||
GtkContainer *parent;
|
||||
|
||||
// This flag is for users of these functions.
|
||||
// For uiBox, this is "spaced".
|
||||
// For uiTab, this is "margined". (uiGroup and uiWindow have to maintain their margined state themselves, since the margined state is independent of whether there is a child for those two.)
|
||||
int flag;
|
||||
};
|
||||
|
||||
uiprivChild *uiprivNewChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer)
|
||||
{
|
||||
uiprivChild *c;
|
||||
|
||||
if (child == NULL)
|
||||
return NULL;
|
||||
|
||||
c = uiprivNew(uiprivChild);
|
||||
c->c = child;
|
||||
c->widget = GTK_WIDGET(uiControlHandle(c->c));
|
||||
|
||||
c->oldhexpand = gtk_widget_get_hexpand(c->widget);
|
||||
c->oldhalign = gtk_widget_get_halign(c->widget);
|
||||
c->oldvexpand = gtk_widget_get_vexpand(c->widget);
|
||||
c->oldvalign = gtk_widget_get_valign(c->widget);
|
||||
|
||||
uiControlSetParent(c->c, parent);
|
||||
uiUnixControlSetContainer(uiUnixControl(c->c), parentContainer, FALSE);
|
||||
c->parent = parentContainer;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
uiprivChild *uiprivNewChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined)
|
||||
{
|
||||
uiprivChild *c;
|
||||
GtkWidget *box;
|
||||
|
||||
if (child == NULL)
|
||||
return NULL;
|
||||
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_show(box);
|
||||
c = uiprivNewChild(child, parent, GTK_CONTAINER(box));
|
||||
gtk_widget_set_hexpand(c->widget, TRUE);
|
||||
gtk_widget_set_halign(c->widget, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_vexpand(c->widget, TRUE);
|
||||
gtk_widget_set_valign(c->widget, GTK_ALIGN_FILL);
|
||||
c->box = box;
|
||||
gtk_container_add(parentContainer, c->box);
|
||||
uiprivChildSetMargined(c, margined);
|
||||
return c;
|
||||
}
|
||||
|
||||
void uiprivChildRemove(uiprivChild *c)
|
||||
{
|
||||
uiControlSetParent(c->c, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(c->c), c->parent, TRUE);
|
||||
|
||||
gtk_widget_set_hexpand(c->widget, c->oldhexpand);
|
||||
gtk_widget_set_halign(c->widget, c->oldhalign);
|
||||
gtk_widget_set_vexpand(c->widget, c->oldvexpand);
|
||||
gtk_widget_set_valign(c->widget, c->oldvalign);
|
||||
|
||||
if (c->box != NULL)
|
||||
gtk_widget_destroy(c->box);
|
||||
|
||||
uiprivFree(c);
|
||||
}
|
||||
|
||||
void uiprivChildDestroy(uiprivChild *c)
|
||||
{
|
||||
uiControl *child;
|
||||
|
||||
child = c->c;
|
||||
uiprivChildRemove(c);
|
||||
uiControlDestroy(child);
|
||||
}
|
||||
|
||||
GtkWidget *uiprivChildWidget(uiprivChild *c)
|
||||
{
|
||||
return c->widget;
|
||||
}
|
||||
|
||||
int uiprivChildFlag(uiprivChild *c)
|
||||
{
|
||||
return c->flag;
|
||||
}
|
||||
|
||||
void uiprivChildSetFlag(uiprivChild *c, int flag)
|
||||
{
|
||||
c->flag = flag;
|
||||
}
|
||||
|
||||
GtkWidget *uiprivChildBox(uiprivChild *c)
|
||||
{
|
||||
return c->box;
|
||||
}
|
||||
|
||||
void uiprivChildSetMargined(uiprivChild *c, int margined)
|
||||
{
|
||||
uiprivSetMargined(GTK_CONTAINER(c->box), margined);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 15 may 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiColorButton {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkButton *button;
|
||||
GtkColorButton *cb;
|
||||
GtkColorChooser *cc;
|
||||
void (*onChanged)(uiColorButton *, void *);
|
||||
void *onChangedData;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiColorButton)
|
||||
|
||||
static void onColorSet(GtkColorButton *button, gpointer data)
|
||||
{
|
||||
uiColorButton *b = uiColorButton(data);
|
||||
|
||||
(*(b->onChanged))(b, b->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiColorButton *b, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a)
|
||||
{
|
||||
GdkRGBA rgba;
|
||||
|
||||
gtk_color_chooser_get_rgba(b->cc, &rgba);
|
||||
*r = rgba.red;
|
||||
*g = rgba.green;
|
||||
*bl = rgba.blue;
|
||||
*a = rgba.alpha;
|
||||
}
|
||||
|
||||
void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a)
|
||||
{
|
||||
GdkRGBA rgba;
|
||||
|
||||
rgba.red = r;
|
||||
rgba.green = g;
|
||||
rgba.blue = bl;
|
||||
rgba.alpha = a;
|
||||
// no need to inhibit the signal; color-set is documented as only being sent when the user changes the color
|
||||
gtk_color_chooser_set_rgba(b->cc, &rgba);
|
||||
}
|
||||
|
||||
void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data)
|
||||
{
|
||||
b->onChanged = f;
|
||||
b->onChangedData = data;
|
||||
}
|
||||
|
||||
uiColorButton *uiNewColorButton(void)
|
||||
{
|
||||
uiColorButton *b;
|
||||
GdkRGBA black;
|
||||
|
||||
uiUnixNewControl(uiColorButton, b);
|
||||
|
||||
// I'm not sure what the initial color is; set up a real one
|
||||
black.red = 0.0;
|
||||
black.green = 0.0;
|
||||
black.blue = 0.0;
|
||||
black.alpha = 1.0;
|
||||
b->widget = gtk_color_button_new_with_rgba(&black);
|
||||
b->button = GTK_BUTTON(b->widget);
|
||||
b->cb = GTK_COLOR_BUTTON(b->widget);
|
||||
b->cc = GTK_COLOR_CHOOSER(b->widget);
|
||||
|
||||
gtk_color_chooser_set_use_alpha(b->cc, TRUE);
|
||||
|
||||
g_signal_connect(b->widget, "color-set", G_CALLBACK(onColorSet), b);
|
||||
uiColorButtonOnChanged(b, defaultOnChanged, NULL);
|
||||
|
||||
return b;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiCombobox {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkComboBox *combobox;
|
||||
GtkComboBoxText *comboboxText;
|
||||
void (*onSelected)(uiCombobox *, void *);
|
||||
void *onSelectedData;
|
||||
gulong onSelectedSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiCombobox)
|
||||
|
||||
static void onChanged(GtkComboBox *cbox, gpointer data)
|
||||
{
|
||||
uiCombobox *c = uiCombobox(data);
|
||||
|
||||
(*(c->onSelected))(c, c->onSelectedData);
|
||||
}
|
||||
|
||||
static void defaultOnSelected(uiCombobox *c, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void uiComboboxAppend(uiCombobox *c, const char *text)
|
||||
{
|
||||
gtk_combo_box_text_append(c->comboboxText, NULL, text);
|
||||
}
|
||||
|
||||
void uiComboboxInsertAt(uiCombobox *c, int n, const char *text)
|
||||
{
|
||||
gtk_combo_box_text_insert(c->comboboxText, n, NULL, text);
|
||||
}
|
||||
|
||||
void uiComboboxDelete(uiCombobox *c, int n)
|
||||
{
|
||||
gtk_combo_box_text_remove(c->comboboxText, n);
|
||||
}
|
||||
|
||||
void uiComboboxClear(uiCombobox *c)
|
||||
{
|
||||
gtk_combo_box_text_remove_all(c->comboboxText);
|
||||
}
|
||||
|
||||
int uiComboboxNumItems(uiCombobox *c)
|
||||
{
|
||||
GtkListStore *store;
|
||||
|
||||
store = GTK_LIST_STORE(gtk_combo_box_get_model(c->combobox));
|
||||
return gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), NULL);
|
||||
}
|
||||
|
||||
int uiComboboxSelected(uiCombobox *c)
|
||||
{
|
||||
return gtk_combo_box_get_active(c->combobox);
|
||||
}
|
||||
|
||||
void uiComboboxSetSelected(uiCombobox *c, int n)
|
||||
{
|
||||
// we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
|
||||
g_signal_handler_block(c->combobox, c->onSelectedSignal);
|
||||
gtk_combo_box_set_active(c->combobox, n);
|
||||
g_signal_handler_unblock(c->combobox, c->onSelectedSignal);
|
||||
}
|
||||
|
||||
void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data)
|
||||
{
|
||||
c->onSelected = f;
|
||||
c->onSelectedData = data;
|
||||
}
|
||||
|
||||
uiCombobox *uiNewCombobox(void)
|
||||
{
|
||||
uiCombobox *c;
|
||||
|
||||
uiUnixNewControl(uiCombobox, c);
|
||||
|
||||
c->widget = gtk_combo_box_text_new();
|
||||
c->combobox = GTK_COMBO_BOX(c->widget);
|
||||
c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget);
|
||||
|
||||
c->onSelectedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c);
|
||||
uiComboboxOnSelected(c, defaultOnSelected, NULL);
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 16 august 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
void uiUnixControlSetContainer(uiUnixControl *c, GtkContainer *container, gboolean remove)
|
||||
{
|
||||
(*(c->SetContainer))(c, container, remove);
|
||||
}
|
||||
|
||||
#define uiUnixControlSignature 0x556E6978
|
||||
|
||||
uiUnixControl *uiUnixAllocControl(size_t n, uint32_t typesig, const char *typenamestr)
|
||||
{
|
||||
return uiUnixControl(uiAllocControl(n, uiUnixControlSignature, typesig, typenamestr));
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
// 4 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// LONGTERM imitate gnome-calendar's day/month/year entries above the calendar
|
||||
// LONGTERM allow entering a 24-hour hour in the hour spinbutton and adjust accordingly
|
||||
|
||||
#define uiprivDateTimePickerWidgetType (uiprivDateTimePickerWidget_get_type())
|
||||
#define uiprivDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidget))
|
||||
#define isDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), uiprivDateTimePickerWidgetType))
|
||||
#define uiprivDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidgetClass))
|
||||
#define isDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), uiprivDateTimePickerWidget))
|
||||
#define getDateTimePickerWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidgetClass))
|
||||
|
||||
typedef struct uiprivDateTimePickerWidget uiprivDateTimePickerWidget;
|
||||
typedef struct uiprivDateTimePickerWidgetClass uiprivDateTimePickerWidgetClass;
|
||||
|
||||
struct uiprivDateTimePickerWidget {
|
||||
GtkToggleButton parent_instance;
|
||||
|
||||
gulong toggledSignal;
|
||||
|
||||
gboolean hasTime;
|
||||
gboolean hasDate;
|
||||
|
||||
GtkWidget *window;
|
||||
GtkWidget *box;
|
||||
GtkWidget *calendar;
|
||||
GtkWidget *timebox;
|
||||
GtkWidget *hours;
|
||||
GtkWidget *minutes;
|
||||
GtkWidget *seconds;
|
||||
GtkWidget *ampm;
|
||||
|
||||
gulong calendarBlock;
|
||||
gulong hoursBlock;
|
||||
gulong minutesBlock;
|
||||
gulong secondsBlock;
|
||||
gulong ampmBlock;
|
||||
|
||||
GdkDevice *keyboard;
|
||||
GdkDevice *mouse;
|
||||
};
|
||||
|
||||
struct uiprivDateTimePickerWidgetClass {
|
||||
GtkToggleButtonClass parent_class;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(uiprivDateTimePickerWidget, uiprivDateTimePickerWidget, GTK_TYPE_TOGGLE_BUTTON)
|
||||
|
||||
static int realSpinValue(GtkSpinButton *spinButton)
|
||||
{
|
||||
GtkAdjustment *adj;
|
||||
|
||||
adj = gtk_spin_button_get_adjustment(spinButton);
|
||||
return (int) gtk_adjustment_get_value(adj);
|
||||
}
|
||||
|
||||
static void setRealSpinValue(GtkSpinButton *spinButton, int value, gulong block)
|
||||
{
|
||||
GtkAdjustment *adj;
|
||||
|
||||
g_signal_handler_block(spinButton, block);
|
||||
adj = gtk_spin_button_get_adjustment(spinButton);
|
||||
gtk_adjustment_set_value(adj, value);
|
||||
g_signal_handler_unblock(spinButton, block);
|
||||
}
|
||||
|
||||
static GDateTime *selected(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
// choose a day for which all times are likely to be valid for the default date in case we're only dealing with time
|
||||
guint year = 1970, month = 1, day = 1;
|
||||
guint hour = 0, minute = 0, second = 0;
|
||||
|
||||
if (d->hasDate) {
|
||||
gtk_calendar_get_date(GTK_CALENDAR(d->calendar), &year, &month, &day);
|
||||
month++; // GtkCalendar/GDateTime differences
|
||||
}
|
||||
if (d->hasTime) {
|
||||
hour = realSpinValue(GTK_SPIN_BUTTON(d->hours));
|
||||
if (realSpinValue(GTK_SPIN_BUTTON(d->ampm)) != 0)
|
||||
hour += 12;
|
||||
minute = realSpinValue(GTK_SPIN_BUTTON(d->minutes));
|
||||
second = realSpinValue(GTK_SPIN_BUTTON(d->seconds));
|
||||
}
|
||||
return g_date_time_new_local(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
static void setLabel(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
GDateTime *dt;
|
||||
char *fmt;
|
||||
char *msg;
|
||||
gboolean free;
|
||||
|
||||
dt = selected(d);
|
||||
free = FALSE;
|
||||
if (d->hasDate && d->hasTime) {
|
||||
// don't use D_T_FMT; that's too verbose
|
||||
fmt = g_strdup_printf("%s %s", nl_langinfo(D_FMT), nl_langinfo(T_FMT));
|
||||
free = TRUE;
|
||||
} else if (d->hasDate)
|
||||
fmt = nl_langinfo(D_FMT);
|
||||
else
|
||||
fmt = nl_langinfo(T_FMT);
|
||||
msg = g_date_time_format(dt, fmt);
|
||||
gtk_button_set_label(GTK_BUTTON(d), msg);
|
||||
g_free(msg);
|
||||
if (free)
|
||||
g_free(fmt);
|
||||
g_date_time_unref(dt);
|
||||
}
|
||||
|
||||
static int changedSignal;
|
||||
|
||||
static void dateTimeChanged(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
g_signal_emit(d, changedSignal, 0);
|
||||
setLabel(d);
|
||||
// TODO fire event here instead?
|
||||
}
|
||||
|
||||
// we don't want ::toggled to be sent again
|
||||
static void setActive(uiprivDateTimePickerWidget *d, gboolean active)
|
||||
{
|
||||
g_signal_handler_block(d, d->toggledSignal);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d), active);
|
||||
g_signal_handler_unblock(d, d->toggledSignal);
|
||||
}
|
||||
|
||||
// like startGrab() below, a lot of this is in the order that GtkComboBox does it
|
||||
static void endGrab(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
if (d->keyboard != NULL)
|
||||
gdk_device_ungrab(d->keyboard, GDK_CURRENT_TIME);
|
||||
gdk_device_ungrab(d->mouse, GDK_CURRENT_TIME);
|
||||
gtk_device_grab_remove(d->window, d->mouse);
|
||||
d->keyboard = NULL;
|
||||
d->mouse = NULL;
|
||||
}
|
||||
|
||||
static void hidePopup(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
endGrab(d);
|
||||
gtk_widget_hide(d->window);
|
||||
setActive(d, FALSE);
|
||||
}
|
||||
|
||||
// this consolidates a good chunk of what GtkComboBox does
|
||||
static gboolean startGrab(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
GdkDevice *dev;
|
||||
guint32 time;
|
||||
GdkWindow *window;
|
||||
GdkDevice *keyboard, *mouse;
|
||||
|
||||
dev = gtk_get_current_event_device();
|
||||
if (dev == NULL) {
|
||||
// this is what GtkComboBox does
|
||||
// since no device was set, just use the first available "master device"
|
||||
GdkDisplay *disp;
|
||||
GdkDeviceManager *dm;
|
||||
GList *list;
|
||||
|
||||
disp = gtk_widget_get_display(GTK_WIDGET(d));
|
||||
dm = gdk_display_get_device_manager(disp);
|
||||
list = gdk_device_manager_list_devices(dm, GDK_DEVICE_TYPE_MASTER);
|
||||
dev = (GdkDevice *) (list->data);
|
||||
g_list_free(list);
|
||||
}
|
||||
|
||||
time = gtk_get_current_event_time();
|
||||
keyboard = dev;
|
||||
mouse = gdk_device_get_associated_device(dev);
|
||||
if (gdk_device_get_source(dev) != GDK_SOURCE_KEYBOARD) {
|
||||
dev = mouse;
|
||||
mouse = keyboard;
|
||||
keyboard = dev;
|
||||
}
|
||||
|
||||
window = gtk_widget_get_window(d->window);
|
||||
if (keyboard != NULL)
|
||||
if (gdk_device_grab(keyboard, window,
|
||||
GDK_OWNERSHIP_WINDOW, TRUE,
|
||||
GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
|
||||
NULL, time) != GDK_GRAB_SUCCESS)
|
||||
return FALSE;
|
||||
if (mouse != NULL)
|
||||
if (gdk_device_grab(mouse, window,
|
||||
GDK_OWNERSHIP_WINDOW, TRUE,
|
||||
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK,
|
||||
NULL, time) != GDK_GRAB_SUCCESS) {
|
||||
if (keyboard != NULL)
|
||||
gdk_device_ungrab(keyboard, time);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gtk_device_grab_add(d->window, mouse, TRUE);
|
||||
d->keyboard = keyboard;
|
||||
d->mouse = mouse;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// based on gtk_combo_box_list_position() in the GTK+ source code
|
||||
static void allocationToScreen(uiprivDateTimePickerWidget *d, gint *x, gint *y)
|
||||
{
|
||||
GdkWindow *window;
|
||||
GtkAllocation a;
|
||||
GtkRequisition aWin;
|
||||
GdkScreen *screen;
|
||||
GdkRectangle workarea;
|
||||
int otherY;
|
||||
|
||||
gtk_widget_get_allocation(GTK_WIDGET(d), &a);
|
||||
gtk_widget_get_preferred_size(d->window, &aWin, NULL);
|
||||
*x = 0;
|
||||
*y = 0;
|
||||
if (!gtk_widget_get_has_window(GTK_WIDGET(d))) {
|
||||
*x = a.x;
|
||||
*y = a.y;
|
||||
}
|
||||
window = gtk_widget_get_window(GTK_WIDGET(d));
|
||||
gdk_window_get_root_coords(window, *x, *y, x, y);
|
||||
if (gtk_widget_get_direction(GTK_WIDGET(d)) == GTK_TEXT_DIR_RTL)
|
||||
*x += a.width - aWin.width;
|
||||
|
||||
// now adjust to prevent the box from going offscreen
|
||||
screen = gtk_widget_get_screen(GTK_WIDGET(d));
|
||||
gdk_screen_get_monitor_workarea(screen,
|
||||
gdk_screen_get_monitor_at_window(screen, window),
|
||||
&workarea);
|
||||
if (*x < workarea.x) // too far to the left?
|
||||
*x = workarea.x;
|
||||
else if (*x + aWin.width > (workarea.x + workarea.width)) // too far to the right?
|
||||
*x = (workarea.x + workarea.width) - aWin.width;
|
||||
// this isn't the same algorithm used by GtkComboBox
|
||||
// first, get our two choices; *y for down and otherY for up
|
||||
otherY = *y - aWin.height;
|
||||
*y += a.height;
|
||||
// and use otherY if we're too low
|
||||
if (*y + aWin.height >= workarea.y + workarea.height)
|
||||
*y = otherY;
|
||||
}
|
||||
|
||||
static void showPopup(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
GtkWidget *toplevel;
|
||||
gint x, y;
|
||||
|
||||
// GtkComboBox does it
|
||||
toplevel = gtk_widget_get_toplevel(GTK_WIDGET(d));
|
||||
if (GTK_IS_WINDOW(toplevel))
|
||||
gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(d->window));
|
||||
|
||||
allocationToScreen(d, &x, &y);
|
||||
gtk_window_move(GTK_WINDOW(d->window), x, y);
|
||||
|
||||
gtk_widget_show(d->window);
|
||||
setActive(d, TRUE);
|
||||
|
||||
if (!startGrab(d))
|
||||
hidePopup(d);
|
||||
}
|
||||
|
||||
static void onToggled(GtkToggleButton *b, gpointer data)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(b);
|
||||
|
||||
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d)))
|
||||
showPopup(d);
|
||||
else
|
||||
hidePopup(d);
|
||||
}
|
||||
|
||||
static gboolean grabBroken(GtkWidget *w, GdkEventGrabBroken *e, gpointer data)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data);
|
||||
|
||||
hidePopup(d);
|
||||
return TRUE; // this is what GtkComboBox does
|
||||
}
|
||||
|
||||
static gboolean buttonReleased(GtkWidget *w, GdkEventButton *e, gpointer data)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data);
|
||||
int winx, winy;
|
||||
GtkAllocation wina;
|
||||
gboolean in;
|
||||
|
||||
gtk_widget_get_allocation(d->window, &wina);
|
||||
winx = 0;
|
||||
winy = 0;
|
||||
if (!gtk_widget_get_has_window(d->window)) {
|
||||
winx = wina.x;
|
||||
winy = wina.y;
|
||||
}
|
||||
gdk_window_get_root_coords(gtk_widget_get_window(d->window), winx, winy, &winx, &winy);
|
||||
in = TRUE;
|
||||
if (e->x_root < winx)
|
||||
in = FALSE;
|
||||
if (e->x_root >= (winx + wina.width))
|
||||
in = FALSE;
|
||||
if (e->y_root < winy)
|
||||
in = FALSE;
|
||||
if (e->y_root >= (winy + wina.height))
|
||||
in = FALSE;
|
||||
if (!in)
|
||||
hidePopup(d);
|
||||
return TRUE; // this is what GtkComboBox does
|
||||
}
|
||||
|
||||
static gint hoursSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data)
|
||||
{
|
||||
double *out = (double *) ptr;
|
||||
const gchar *text;
|
||||
int value;
|
||||
|
||||
text = gtk_entry_get_text(GTK_ENTRY(sb));
|
||||
value = (int) g_strtod(text, NULL);
|
||||
if (value < 0 || value > 12)
|
||||
return GTK_INPUT_ERROR;
|
||||
if (value == 12) // 12 to the user is 0 internally
|
||||
value = 0;
|
||||
*out = (double) value;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean hoursSpinboxOutput(GtkSpinButton *sb, gpointer data)
|
||||
{
|
||||
gchar *text;
|
||||
int value;
|
||||
|
||||
value = realSpinValue(sb);
|
||||
if (value == 0) // 0 internally is 12 to the user
|
||||
value = 12;
|
||||
text = g_strdup_printf("%d", value);
|
||||
gtk_entry_set_text(GTK_ENTRY(sb), text);
|
||||
g_free(text);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean zeroPadSpinbox(GtkSpinButton *sb, gpointer data)
|
||||
{
|
||||
gchar *text;
|
||||
int value;
|
||||
|
||||
value = realSpinValue(sb);
|
||||
text = g_strdup_printf("%02d", value);
|
||||
gtk_entry_set_text(GTK_ENTRY(sb), text);
|
||||
g_free(text);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// this is really hacky but we can't use GtkCombobox here :(
|
||||
static gint ampmSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data)
|
||||
{
|
||||
double *out = (double *) ptr;
|
||||
const gchar *text;
|
||||
char firstAM, firstPM;
|
||||
|
||||
text = gtk_entry_get_text(GTK_ENTRY(sb));
|
||||
// LONGTERM don't use ASCII here for case insensitivity
|
||||
firstAM = g_ascii_tolower(nl_langinfo(AM_STR)[0]);
|
||||
firstPM = g_ascii_tolower(nl_langinfo(PM_STR)[0]);
|
||||
for (; *text != '\0'; text++)
|
||||
if (g_ascii_tolower(*text) == firstAM) {
|
||||
*out = 0;
|
||||
return TRUE;
|
||||
} else if (g_ascii_tolower(*text) == firstPM) {
|
||||
*out = 1;
|
||||
return TRUE;
|
||||
}
|
||||
return GTK_INPUT_ERROR;
|
||||
}
|
||||
|
||||
static gboolean ampmSpinboxOutput(GtkSpinButton *sb, gpointer data)
|
||||
{
|
||||
int value;
|
||||
|
||||
value = gtk_spin_button_get_value_as_int(sb);
|
||||
if (value == 0)
|
||||
gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(AM_STR));
|
||||
else
|
||||
gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(PM_STR));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void spinboxChanged(GtkSpinButton *sb, gpointer data)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data);
|
||||
|
||||
dateTimeChanged(d);
|
||||
}
|
||||
|
||||
static GtkWidget *newSpinbox(uiprivDateTimePickerWidget *d, int min, int max, gint (*input)(GtkSpinButton *, gpointer, gpointer), gboolean (*output)(GtkSpinButton *, gpointer), gulong *block)
|
||||
{
|
||||
GtkWidget *sb;
|
||||
|
||||
sb = gtk_spin_button_new_with_range(min, max, 1);
|
||||
gtk_spin_button_set_digits(GTK_SPIN_BUTTON(sb), 0);
|
||||
gtk_spin_button_set_wrap(GTK_SPIN_BUTTON(sb), TRUE);
|
||||
gtk_orientable_set_orientation(GTK_ORIENTABLE(sb), GTK_ORIENTATION_VERTICAL);
|
||||
*block = g_signal_connect(sb, "value-changed", G_CALLBACK(spinboxChanged), d);
|
||||
if (input != NULL)
|
||||
g_signal_connect(sb, "input", G_CALLBACK(input), NULL);
|
||||
if (output != NULL)
|
||||
g_signal_connect(sb, "output", G_CALLBACK(output), NULL);
|
||||
return sb;
|
||||
}
|
||||
|
||||
static void dateChanged(GtkCalendar *c, gpointer data)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data);
|
||||
|
||||
dateTimeChanged(d);
|
||||
}
|
||||
|
||||
static void setDateOnly(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
d->hasTime = FALSE;
|
||||
gtk_container_remove(GTK_CONTAINER(d->box), d->timebox);
|
||||
}
|
||||
|
||||
static void setTimeOnly(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
d->hasDate = FALSE;
|
||||
gtk_container_remove(GTK_CONTAINER(d->box), d->calendar);
|
||||
}
|
||||
|
||||
static void uiprivDateTimePickerWidget_setTime(uiprivDateTimePickerWidget *d, GDateTime *dt)
|
||||
{
|
||||
gint year, month, day;
|
||||
gint hour;
|
||||
|
||||
// notice how we block signals from firing
|
||||
if (d->hasDate) {
|
||||
g_date_time_get_ymd(dt, &year, &month, &day);
|
||||
month--; // GDateTime/GtkCalendar differences
|
||||
g_signal_handler_block(d->calendar, d->calendarBlock);
|
||||
gtk_calendar_select_month(GTK_CALENDAR(d->calendar), month, year);
|
||||
gtk_calendar_select_day(GTK_CALENDAR(d->calendar), day);
|
||||
g_signal_handler_unblock(d->calendar, d->calendarBlock);
|
||||
}
|
||||
if (d->hasTime) {
|
||||
hour = g_date_time_get_hour(dt);
|
||||
if (hour >= 12) {
|
||||
hour -= 12;
|
||||
setRealSpinValue(GTK_SPIN_BUTTON(d->ampm), 1, d->ampmBlock);
|
||||
}
|
||||
setRealSpinValue(GTK_SPIN_BUTTON(d->hours), hour, d->hoursBlock);
|
||||
setRealSpinValue(GTK_SPIN_BUTTON(d->minutes), g_date_time_get_minute(dt), d->minutesBlock);
|
||||
setRealSpinValue(GTK_SPIN_BUTTON(d->seconds), g_date_time_get_seconds(dt), d->secondsBlock);
|
||||
}
|
||||
g_date_time_unref(dt);
|
||||
}
|
||||
|
||||
static void uiprivDateTimePickerWidget_init(uiprivDateTimePickerWidget *d)
|
||||
{
|
||||
d->window = gtk_window_new(GTK_WINDOW_POPUP);
|
||||
gtk_window_set_resizable(GTK_WINDOW(d->window), FALSE);
|
||||
gtk_window_set_attached_to(GTK_WINDOW(d->window), GTK_WIDGET(d));
|
||||
gtk_window_set_decorated(GTK_WINDOW(d->window), FALSE);
|
||||
gtk_window_set_deletable(GTK_WINDOW(d->window), FALSE);
|
||||
gtk_window_set_type_hint(GTK_WINDOW(d->window), GDK_WINDOW_TYPE_HINT_COMBO);
|
||||
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(d->window), TRUE);
|
||||
gtk_window_set_skip_pager_hint(GTK_WINDOW(d->window), TRUE);
|
||||
gtk_window_set_has_resize_grip(GTK_WINDOW(d->window), FALSE);
|
||||
gtk_container_set_border_width(GTK_CONTAINER(d->window), 12);
|
||||
// and make it stand out a bit
|
||||
gtk_style_context_add_class(gtk_widget_get_style_context(d->window), "frame");
|
||||
|
||||
d->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_container_add(GTK_CONTAINER(d->window), d->box);
|
||||
|
||||
d->calendar = gtk_calendar_new();
|
||||
d->calendarBlock = g_signal_connect(d->calendar, "day-selected", G_CALLBACK(dateChanged), d);
|
||||
gtk_container_add(GTK_CONTAINER(d->box), d->calendar);
|
||||
|
||||
d->timebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_valign(d->timebox, GTK_ALIGN_CENTER);
|
||||
gtk_container_add(GTK_CONTAINER(d->box), d->timebox);
|
||||
|
||||
d->hours = newSpinbox(d, 0, 11, hoursSpinboxInput, hoursSpinboxOutput, &(d->hoursBlock));
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox), d->hours);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox),
|
||||
gtk_label_new(":"));
|
||||
|
||||
d->minutes = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->minutesBlock));
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox), d->minutes);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox),
|
||||
gtk_label_new(":"));
|
||||
|
||||
d->seconds = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->secondsBlock));
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox), d->seconds);
|
||||
|
||||
// LONGTERM this should be the case, but that interferes with grabs
|
||||
// switch to it when we can drop GTK+ 3.10 and use popovers
|
||||
#if 0
|
||||
d->ampm = gtk_combo_box_text_new();
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "AM");
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "PM");
|
||||
#endif
|
||||
d->ampm = newSpinbox(d, 0, 1, ampmSpinboxInput, ampmSpinboxOutput, &(d->ampmBlock));
|
||||
gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(d->ampm), FALSE);
|
||||
gtk_widget_set_valign(d->ampm, GTK_ALIGN_CENTER);
|
||||
gtk_container_add(GTK_CONTAINER(d->timebox), d->ampm);
|
||||
|
||||
gtk_widget_show_all(d->box);
|
||||
|
||||
g_signal_connect(d->window, "grab-broken-event", G_CALLBACK(grabBroken), d);
|
||||
g_signal_connect(d->window, "button-release-event", G_CALLBACK(buttonReleased), d);
|
||||
|
||||
d->toggledSignal = g_signal_connect(d, "toggled", G_CALLBACK(onToggled), NULL);
|
||||
d->keyboard = NULL;
|
||||
d->mouse = NULL;
|
||||
|
||||
d->hasTime = TRUE;
|
||||
d->hasDate = TRUE;
|
||||
|
||||
// set the current date/time
|
||||
uiprivDateTimePickerWidget_setTime(d, g_date_time_new_now_local());
|
||||
}
|
||||
|
||||
static void uiprivDateTimePickerWidget_dispose(GObject *obj)
|
||||
{
|
||||
uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(obj);
|
||||
|
||||
if (d->window != NULL) {
|
||||
gtk_widget_destroy(d->window);
|
||||
d->window = NULL;
|
||||
}
|
||||
G_OBJECT_CLASS(uiprivDateTimePickerWidget_parent_class)->dispose(obj);
|
||||
}
|
||||
|
||||
static void uiprivDateTimePickerWidget_finalize(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(uiprivDateTimePickerWidget_parent_class)->finalize(obj);
|
||||
}
|
||||
|
||||
static void uiprivDateTimePickerWidget_class_init(uiprivDateTimePickerWidgetClass *class)
|
||||
{
|
||||
G_OBJECT_CLASS(class)->dispose = uiprivDateTimePickerWidget_dispose;
|
||||
G_OBJECT_CLASS(class)->finalize = uiprivDateTimePickerWidget_finalize;
|
||||
|
||||
changedSignal = g_signal_new("changed",
|
||||
G_TYPE_FROM_CLASS(class),
|
||||
G_SIGNAL_RUN_LAST,
|
||||
0,
|
||||
NULL, NULL, NULL,
|
||||
G_TYPE_NONE,
|
||||
0);
|
||||
}
|
||||
|
||||
struct uiDateTimePicker {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
uiprivDateTimePickerWidget *d;
|
||||
void (*onChanged)(uiDateTimePicker *, void *);
|
||||
void *onChangedData;
|
||||
gulong setBlock;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiDateTimePicker)
|
||||
|
||||
static void defaultOnChanged(uiDateTimePicker *d, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void uiDateTimePickerTime(uiDateTimePicker *d, struct tm *time)
|
||||
{
|
||||
time_t t;
|
||||
struct tm tmbuf;
|
||||
GDateTime *dt;
|
||||
|
||||
dt = selected(d->d);
|
||||
t = g_date_time_to_unix(dt);
|
||||
g_date_time_unref(dt);
|
||||
|
||||
// Copy time to minimize a race condition
|
||||
// time.h functions use global non-thread-safe data
|
||||
tmbuf = *localtime(&t);
|
||||
memcpy(time, &tmbuf, sizeof (struct tm));
|
||||
}
|
||||
|
||||
void uiDateTimePickerSetTime(uiDateTimePicker *d, const struct tm *time)
|
||||
{
|
||||
time_t t;
|
||||
struct tm tmbuf;
|
||||
|
||||
// TODO find a better way to avoid this; possibly by removing the signal entirely, or the call to dateTimeChanged() (most likely both)
|
||||
g_signal_handler_block(d->d, d->setBlock);
|
||||
|
||||
// Copy time because mktime() modifies its argument
|
||||
memcpy(&tmbuf, time, sizeof (struct tm));
|
||||
t = mktime(&tmbuf);
|
||||
|
||||
uiprivDateTimePickerWidget_setTime(d->d, g_date_time_new_from_unix_local(t));
|
||||
dateTimeChanged(d->d);
|
||||
|
||||
g_signal_handler_unblock(d->d, d->setBlock);
|
||||
}
|
||||
|
||||
void uiDateTimePickerOnChanged(uiDateTimePicker *d, void (*f)(uiDateTimePicker *, void *), void *data)
|
||||
{
|
||||
d->onChanged = f;
|
||||
d->onChangedData = data;
|
||||
}
|
||||
|
||||
static void onChanged(uiprivDateTimePickerWidget *d, gpointer data)
|
||||
{
|
||||
uiDateTimePicker *c;
|
||||
|
||||
c = uiDateTimePicker(data);
|
||||
(*(c->onChanged))(c, c->onChangedData);
|
||||
}
|
||||
|
||||
static GtkWidget *newDTP(void)
|
||||
{
|
||||
GtkWidget *w;
|
||||
|
||||
w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL));
|
||||
setLabel(uiprivDateTimePickerWidget(w));
|
||||
return w;
|
||||
}
|
||||
|
||||
static GtkWidget *newDP(void)
|
||||
{
|
||||
GtkWidget *w;
|
||||
|
||||
w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL));
|
||||
setDateOnly(uiprivDateTimePickerWidget(w));
|
||||
setLabel(uiprivDateTimePickerWidget(w));
|
||||
return w;
|
||||
}
|
||||
|
||||
static GtkWidget *newTP(void)
|
||||
{
|
||||
GtkWidget *w;
|
||||
|
||||
w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL));
|
||||
setTimeOnly(uiprivDateTimePickerWidget(w));
|
||||
setLabel(uiprivDateTimePickerWidget(w));
|
||||
return w;
|
||||
}
|
||||
|
||||
uiDateTimePicker *finishNewDateTimePicker(GtkWidget *(*fn)(void))
|
||||
{
|
||||
uiDateTimePicker *d;
|
||||
|
||||
uiUnixNewControl(uiDateTimePicker, d);
|
||||
|
||||
d->widget = (*fn)();
|
||||
d->d = uiprivDateTimePickerWidget(d->widget);
|
||||
d->setBlock = g_signal_connect(d->widget, "changed", G_CALLBACK(onChanged), d);
|
||||
uiDateTimePickerOnChanged(d, defaultOnChanged, NULL);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
uiDateTimePicker *uiNewDateTimePicker(void)
|
||||
{
|
||||
return finishNewDateTimePicker(newDTP);
|
||||
}
|
||||
|
||||
uiDateTimePicker *uiNewDatePicker(void)
|
||||
{
|
||||
return finishNewDateTimePicker(newDP);
|
||||
}
|
||||
|
||||
uiDateTimePicker *uiNewTimePicker(void)
|
||||
{
|
||||
return finishNewDateTimePicker(newTP);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 13 may 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// LONGTERM don't halt on release builds
|
||||
|
||||
void uiprivRealBug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap)
|
||||
{
|
||||
char *a, *b;
|
||||
|
||||
a = g_strdup_printf("[libui] %s:%s:%s() %s", file, line, func, prefix);
|
||||
b = g_strdup_vprintf(format, ap);
|
||||
g_critical("%s%s", a, b);
|
||||
G_BREAKPOINT();
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// 6 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
|
||||
uiDrawContext *uiprivNewContext(cairo_t *cr, GtkStyleContext *style)
|
||||
{
|
||||
uiDrawContext *c;
|
||||
|
||||
c = uiprivNew(uiDrawContext);
|
||||
c->cr = cr;
|
||||
c->style = style;
|
||||
return c;
|
||||
}
|
||||
|
||||
void uiprivFreeContext(uiDrawContext *c)
|
||||
{
|
||||
// free neither cr nor style; we own neither
|
||||
uiprivFree(c);
|
||||
}
|
||||
|
||||
static cairo_pattern_t *mkbrush(uiDrawBrush *b)
|
||||
{
|
||||
cairo_pattern_t *pat;
|
||||
size_t i;
|
||||
|
||||
switch (b->Type) {
|
||||
case uiDrawBrushTypeSolid:
|
||||
pat = cairo_pattern_create_rgba(b->R, b->G, b->B, b->A);
|
||||
break;
|
||||
case uiDrawBrushTypeLinearGradient:
|
||||
pat = cairo_pattern_create_linear(b->X0, b->Y0, b->X1, b->Y1);
|
||||
break;
|
||||
case uiDrawBrushTypeRadialGradient:
|
||||
// make the start circle radius 0 to make it a point
|
||||
pat = cairo_pattern_create_radial(
|
||||
b->X0, b->Y0, 0,
|
||||
b->X1, b->Y1, b->OuterRadius);
|
||||
break;
|
||||
// case uiDrawBrushTypeImage:
|
||||
}
|
||||
if (cairo_pattern_status(pat) != CAIRO_STATUS_SUCCESS)
|
||||
uiprivImplBug("error creating pattern in mkbrush(): %s",
|
||||
cairo_status_to_string(cairo_pattern_status(pat)));
|
||||
switch (b->Type) {
|
||||
case uiDrawBrushTypeLinearGradient:
|
||||
case uiDrawBrushTypeRadialGradient:
|
||||
for (i = 0; i < b->NumStops; i++)
|
||||
cairo_pattern_add_color_stop_rgba(pat,
|
||||
b->Stops[i].Pos,
|
||||
b->Stops[i].R,
|
||||
b->Stops[i].G,
|
||||
b->Stops[i].B,
|
||||
b->Stops[i].A);
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p)
|
||||
{
|
||||
cairo_pattern_t *pat;
|
||||
|
||||
uiprivRunPath(path, c->cr);
|
||||
pat = mkbrush(b);
|
||||
cairo_set_source(c->cr, pat);
|
||||
switch (p->Cap) {
|
||||
case uiDrawLineCapFlat:
|
||||
cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_BUTT);
|
||||
break;
|
||||
case uiDrawLineCapRound:
|
||||
cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_ROUND);
|
||||
break;
|
||||
case uiDrawLineCapSquare:
|
||||
cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_SQUARE);
|
||||
break;
|
||||
}
|
||||
switch (p->Join) {
|
||||
case uiDrawLineJoinMiter:
|
||||
cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_MITER);
|
||||
cairo_set_miter_limit(c->cr, p->MiterLimit);
|
||||
break;
|
||||
case uiDrawLineJoinRound:
|
||||
cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_ROUND);
|
||||
break;
|
||||
case uiDrawLineJoinBevel:
|
||||
cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_BEVEL);
|
||||
break;
|
||||
}
|
||||
cairo_set_line_width(c->cr, p->Thickness);
|
||||
cairo_set_dash(c->cr, p->Dashes, p->NumDashes, p->DashPhase);
|
||||
cairo_stroke(c->cr);
|
||||
cairo_pattern_destroy(pat);
|
||||
}
|
||||
|
||||
void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b)
|
||||
{
|
||||
cairo_pattern_t *pat;
|
||||
|
||||
uiprivRunPath(path, c->cr);
|
||||
pat = mkbrush(b);
|
||||
cairo_set_source(c->cr, pat);
|
||||
switch (uiprivPathFillMode(path)) {
|
||||
case uiDrawFillModeWinding:
|
||||
cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING);
|
||||
break;
|
||||
case uiDrawFillModeAlternate:
|
||||
cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
break;
|
||||
}
|
||||
cairo_fill(c->cr);
|
||||
cairo_pattern_destroy(pat);
|
||||
}
|
||||
|
||||
void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m)
|
||||
{
|
||||
cairo_matrix_t cm;
|
||||
|
||||
uiprivM2C(m, &cm);
|
||||
cairo_transform(c->cr, &cm);
|
||||
}
|
||||
|
||||
void uiDrawClip(uiDrawContext *c, uiDrawPath *path)
|
||||
{
|
||||
uiprivRunPath(path, c->cr);
|
||||
switch (uiprivPathFillMode(path)) {
|
||||
case uiDrawFillModeWinding:
|
||||
cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING);
|
||||
break;
|
||||
case uiDrawFillModeAlternate:
|
||||
cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
break;
|
||||
}
|
||||
cairo_clip(c->cr);
|
||||
}
|
||||
|
||||
void uiDrawSave(uiDrawContext *c)
|
||||
{
|
||||
cairo_save(c->cr);
|
||||
}
|
||||
|
||||
void uiDrawRestore(uiDrawContext *c)
|
||||
{
|
||||
cairo_restore(c->cr);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 5 may 2016
|
||||
|
||||
// draw.c
|
||||
struct uiDrawContext {
|
||||
cairo_t *cr;
|
||||
GtkStyleContext *style;
|
||||
};
|
||||
|
||||
// drawpath.c
|
||||
extern void uiprivRunPath(uiDrawPath *p, cairo_t *cr);
|
||||
extern uiDrawFillMode uiprivPathFillMode(uiDrawPath *path);
|
||||
|
||||
// drawmatrix.c
|
||||
extern void uiprivM2C(uiDrawMatrix *m, cairo_matrix_t *c);
|
||||
@@ -0,0 +1,115 @@
|
||||
// 6 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
|
||||
static void m2c(uiDrawMatrix *m, cairo_matrix_t *c)
|
||||
{
|
||||
c->xx = m->M11;
|
||||
c->yx = m->M12;
|
||||
c->xy = m->M21;
|
||||
c->yy = m->M22;
|
||||
c->x0 = m->M31;
|
||||
c->y0 = m->M32;
|
||||
}
|
||||
|
||||
// needed by uiDrawTransform()
|
||||
void uiprivM2C(uiDrawMatrix *m, cairo_matrix_t *c)
|
||||
{
|
||||
m2c(m, c);
|
||||
}
|
||||
|
||||
static void c2m(cairo_matrix_t *c, uiDrawMatrix *m)
|
||||
{
|
||||
m->M11 = c->xx;
|
||||
m->M12 = c->yx;
|
||||
m->M21 = c->xy;
|
||||
m->M22 = c->yy;
|
||||
m->M31 = c->x0;
|
||||
m->M32 = c->y0;
|
||||
}
|
||||
|
||||
void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
cairo_matrix_translate(&c, x, y);
|
||||
c2m(&c, m);
|
||||
}
|
||||
|
||||
void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
double xt, yt;
|
||||
|
||||
m2c(m, &c);
|
||||
xt = x;
|
||||
yt = y;
|
||||
uiprivScaleCenter(xCenter, yCenter, &xt, &yt);
|
||||
cairo_matrix_translate(&c, xt, yt);
|
||||
cairo_matrix_scale(&c, x, y);
|
||||
cairo_matrix_translate(&c, -xt, -yt);
|
||||
c2m(&c, m);
|
||||
}
|
||||
|
||||
void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
cairo_matrix_translate(&c, x, y);
|
||||
cairo_matrix_rotate(&c, amount);
|
||||
cairo_matrix_translate(&c, -x, -y);
|
||||
c2m(&c, m);
|
||||
}
|
||||
|
||||
void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount)
|
||||
{
|
||||
uiprivFallbackSkew(m, x, y, xamount, yamount);
|
||||
}
|
||||
|
||||
void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
cairo_matrix_t d;
|
||||
|
||||
m2c(dest, &c);
|
||||
m2c(src, &d);
|
||||
cairo_matrix_multiply(&c, &c, &d);
|
||||
c2m(&c, dest);
|
||||
}
|
||||
|
||||
int uiDrawMatrixInvertible(uiDrawMatrix *m)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
return cairo_matrix_invert(&c) == CAIRO_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
int uiDrawMatrixInvert(uiDrawMatrix *m)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
if (cairo_matrix_invert(&c) != CAIRO_STATUS_SUCCESS)
|
||||
return 0;
|
||||
c2m(&c, m);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
cairo_matrix_transform_point(&c, x, y);
|
||||
}
|
||||
|
||||
void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y)
|
||||
{
|
||||
cairo_matrix_t c;
|
||||
|
||||
m2c(m, &c);
|
||||
cairo_matrix_transform_distance(&c, x, y);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 6 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
|
||||
struct uiDrawPath {
|
||||
GArray *pieces;
|
||||
uiDrawFillMode fillMode;
|
||||
gboolean ended;
|
||||
};
|
||||
|
||||
struct piece {
|
||||
int type;
|
||||
double d[8];
|
||||
int b;
|
||||
};
|
||||
|
||||
enum {
|
||||
newFigure,
|
||||
newFigureArc,
|
||||
lineTo,
|
||||
arcTo,
|
||||
bezierTo,
|
||||
closeFigure,
|
||||
addRect,
|
||||
};
|
||||
|
||||
uiDrawPath *uiDrawNewPath(uiDrawFillMode mode)
|
||||
{
|
||||
uiDrawPath *p;
|
||||
|
||||
p = uiprivNew(uiDrawPath);
|
||||
p->pieces = g_array_new(FALSE, TRUE, sizeof (struct piece));
|
||||
p->fillMode = mode;
|
||||
return p;
|
||||
}
|
||||
|
||||
void uiDrawFreePath(uiDrawPath *p)
|
||||
{
|
||||
g_array_free(p->pieces, TRUE);
|
||||
uiprivFree(p);
|
||||
}
|
||||
|
||||
static void add(uiDrawPath *p, struct piece *piece)
|
||||
{
|
||||
if (p->ended)
|
||||
uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);
|
||||
g_array_append_vals(p->pieces, piece, 1);
|
||||
}
|
||||
|
||||
void uiDrawPathNewFigure(uiDrawPath *p, double x, double y)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
piece.type = newFigure;
|
||||
piece.d[0] = x;
|
||||
piece.d[1] = y;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
if (sweep > 2 * uiPi)
|
||||
sweep = 2 * uiPi;
|
||||
piece.type = newFigureArc;
|
||||
piece.d[0] = xCenter;
|
||||
piece.d[1] = yCenter;
|
||||
piece.d[2] = radius;
|
||||
piece.d[3] = startAngle;
|
||||
piece.d[4] = sweep;
|
||||
piece.b = negative;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathLineTo(uiDrawPath *p, double x, double y)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
piece.type = lineTo;
|
||||
piece.d[0] = x;
|
||||
piece.d[1] = y;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
if (sweep > 2 * uiPi)
|
||||
sweep = 2 * uiPi;
|
||||
piece.type = arcTo;
|
||||
piece.d[0] = xCenter;
|
||||
piece.d[1] = yCenter;
|
||||
piece.d[2] = radius;
|
||||
piece.d[3] = startAngle;
|
||||
piece.d[4] = sweep;
|
||||
piece.b = negative;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
piece.type = bezierTo;
|
||||
piece.d[0] = c1x;
|
||||
piece.d[1] = c1y;
|
||||
piece.d[2] = c2x;
|
||||
piece.d[3] = c2y;
|
||||
piece.d[4] = endX;
|
||||
piece.d[5] = endY;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathCloseFigure(uiDrawPath *p)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
piece.type = closeFigure;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height)
|
||||
{
|
||||
struct piece piece;
|
||||
|
||||
piece.type = addRect;
|
||||
piece.d[0] = x;
|
||||
piece.d[1] = y;
|
||||
piece.d[2] = width;
|
||||
piece.d[3] = height;
|
||||
add(p, &piece);
|
||||
}
|
||||
|
||||
void uiDrawPathEnd(uiDrawPath *p)
|
||||
{
|
||||
p->ended = TRUE;
|
||||
}
|
||||
|
||||
int uiDrawPathEnded(uiDrawPath *p)
|
||||
{
|
||||
return p->ended == TRUE ? 1 : 0;
|
||||
}
|
||||
|
||||
void uiprivRunPath(uiDrawPath *p, cairo_t *cr)
|
||||
{
|
||||
guint i;
|
||||
struct piece *piece;
|
||||
void (*arc)(cairo_t *, double, double, double, double, double);
|
||||
|
||||
if (!p->ended)
|
||||
uiprivUserBug("You cannot draw with a uiDrawPath that has not been ended. (path: %p)", p);
|
||||
cairo_new_path(cr);
|
||||
for (i = 0; i < p->pieces->len; i++) {
|
||||
piece = &g_array_index(p->pieces, struct piece, i);
|
||||
switch (piece->type) {
|
||||
case newFigure:
|
||||
cairo_move_to(cr, piece->d[0], piece->d[1]);
|
||||
break;
|
||||
case newFigureArc:
|
||||
cairo_new_sub_path(cr);
|
||||
// fall through
|
||||
case arcTo:
|
||||
arc = cairo_arc;
|
||||
if (piece->b)
|
||||
arc = cairo_arc_negative;
|
||||
(*arc)(cr,
|
||||
piece->d[0],
|
||||
piece->d[1],
|
||||
piece->d[2],
|
||||
piece->d[3],
|
||||
piece->d[3] + piece->d[4]);
|
||||
break;
|
||||
case lineTo:
|
||||
cairo_line_to(cr, piece->d[0], piece->d[1]);
|
||||
break;
|
||||
case bezierTo:
|
||||
cairo_curve_to(cr,
|
||||
piece->d[0],
|
||||
piece->d[1],
|
||||
piece->d[2],
|
||||
piece->d[3],
|
||||
piece->d[4],
|
||||
piece->d[5]);
|
||||
break;
|
||||
case closeFigure:
|
||||
cairo_close_path(cr);
|
||||
break;
|
||||
case addRect:
|
||||
cairo_rectangle(cr,
|
||||
piece->d[0],
|
||||
piece->d[1],
|
||||
piece->d[2],
|
||||
piece->d[3]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiDrawFillMode uiprivPathFillMode(uiDrawPath *path)
|
||||
{
|
||||
return path->fillMode;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 11 march 2018
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
PangoLayout *layout;
|
||||
};
|
||||
|
||||
// we need a context for a few things
|
||||
// the documentation suggests creating cairo_t-specific, GdkScreen-specific, or even GtkWidget-specific contexts, but we can't really do that because we want our uiDrawTextFonts and uiDrawTextLayouts to be context-independent
|
||||
// we could use pango_font_map_create_context(pango_cairo_font_map_get_default()) but that will ignore GDK-specific settings
|
||||
// so let's use gdk_pango_context_get() instead; even though it's for the default screen only, it's good enough for us
|
||||
#define mkGenericPangoCairoContext() (gdk_pango_context_get())
|
||||
|
||||
static const PangoAlignment pangoAligns[] = {
|
||||
[uiDrawTextAlignLeft] = PANGO_ALIGN_LEFT,
|
||||
[uiDrawTextAlignCenter] = PANGO_ALIGN_CENTER,
|
||||
[uiDrawTextAlignRight] = PANGO_ALIGN_RIGHT,
|
||||
};
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p)
|
||||
{
|
||||
uiDrawTextLayout *tl;
|
||||
PangoContext *context;
|
||||
PangoFontDescription *desc;
|
||||
PangoAttrList *attrs;
|
||||
int pangoWidth;
|
||||
|
||||
tl = uiprivNew(uiDrawTextLayout);
|
||||
|
||||
// in this case, the context is necessary to create the layout
|
||||
// the layout takes a ref on the context so we can unref it afterward
|
||||
context = mkGenericPangoCairoContext();
|
||||
tl->layout = pango_layout_new(context);
|
||||
g_object_unref(context);
|
||||
|
||||
// this is safe; pango_layout_set_text() copies the string
|
||||
pango_layout_set_text(tl->layout, uiAttributedStringString(p->String), -1);
|
||||
|
||||
desc = uiprivFontDescriptorToPangoFontDescription(p->DefaultFont);
|
||||
pango_layout_set_font_description(tl->layout, desc);
|
||||
// this is safe; the description is copied
|
||||
pango_font_description_free(desc);
|
||||
|
||||
pangoWidth = cairoToPango(p->Width);
|
||||
if (p->Width < 0)
|
||||
pangoWidth = -1;
|
||||
pango_layout_set_width(tl->layout, pangoWidth);
|
||||
|
||||
pango_layout_set_alignment(tl->layout, pangoAligns[p->Align]);
|
||||
|
||||
attrs = uiprivAttributedStringToPangoAttrList(p);
|
||||
pango_layout_set_attributes(tl->layout, attrs);
|
||||
pango_attr_list_unref(attrs);
|
||||
|
||||
return tl;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
|
||||
{
|
||||
g_object_unref(tl->layout);
|
||||
uiprivFree(tl);
|
||||
}
|
||||
|
||||
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
|
||||
{
|
||||
// TODO have an implicit save/restore on each drawing functions instead? and is this correct?
|
||||
cairo_set_source_rgb(c->cr, 0.0, 0.0, 0.0);
|
||||
cairo_move_to(c->cr, x, y);
|
||||
pango_cairo_show_layout(c->cr, tl->layout);
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
|
||||
{
|
||||
PangoRectangle logical;
|
||||
|
||||
pango_layout_get_extents(tl->layout, NULL, &logical);
|
||||
*width = pangoToCairo(logical.width);
|
||||
*height = pangoToCairo(logical.height);
|
||||
}
|
||||
|
||||
void uiLoadControlFont(uiFontDescriptor *f)
|
||||
{
|
||||
GtkWidget *widget;
|
||||
GtkStyleContext *style;
|
||||
PangoFontDescription *fontdesc;
|
||||
|
||||
widget = g_object_ref_sink(gtk_label_new(""));
|
||||
style = gtk_widget_get_style_context(widget);
|
||||
gtk_style_context_get(style, GTK_STATE_FLAG_NORMAL,
|
||||
"font", &fontdesc, NULL);
|
||||
uiprivFontDescriptorFromPangoFontDescription(fontdesc, f);
|
||||
// pdesc is transfer-full and thus is a copy
|
||||
pango_font_description_free(fontdesc);
|
||||
g_object_unref(widget);
|
||||
}
|
||||
|
||||
void uiFreeFontDescriptor(uiFontDescriptor *desc)
|
||||
{
|
||||
// TODO ensure this is synchronized with fontmatch.c
|
||||
uiFreeText((char *) (desc->Family));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiEditableCombobox {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkBin *bin;
|
||||
GtkComboBox *combobox;
|
||||
GtkComboBoxText *comboboxText;
|
||||
void (*onChanged)(uiEditableCombobox *, void *);
|
||||
void *onChangedData;
|
||||
gulong onChangedSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiEditableCombobox)
|
||||
|
||||
static void onChanged(GtkComboBox *cbox, gpointer data)
|
||||
{
|
||||
uiEditableCombobox *c = uiEditableCombobox(data);
|
||||
|
||||
(*(c->onChanged))(c, c->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiEditableCombobox *c, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text)
|
||||
{
|
||||
gtk_combo_box_text_append(c->comboboxText, NULL, text);
|
||||
}
|
||||
|
||||
char *uiEditableComboboxText(uiEditableCombobox *c)
|
||||
{
|
||||
return gtk_combo_box_text_get_active_text(c->comboboxText);
|
||||
}
|
||||
|
||||
void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text)
|
||||
{
|
||||
GtkEntry *e;
|
||||
|
||||
// we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
|
||||
g_signal_handler_block(c->combobox, c->onChangedSignal);
|
||||
// since there isn't a gtk_combo_box_text_set_active_text()...
|
||||
e = GTK_ENTRY(gtk_bin_get_child(c->bin));
|
||||
gtk_entry_set_text(e, text);
|
||||
g_signal_handler_unblock(c->combobox, c->onChangedSignal);
|
||||
}
|
||||
|
||||
void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data)
|
||||
{
|
||||
c->onChanged = f;
|
||||
c->onChangedData = data;
|
||||
}
|
||||
|
||||
uiEditableCombobox *uiNewEditableCombobox(void)
|
||||
{
|
||||
uiEditableCombobox *c;
|
||||
|
||||
uiUnixNewControl(uiEditableCombobox, c);
|
||||
|
||||
c->widget = gtk_combo_box_text_new_with_entry();
|
||||
c->bin = GTK_BIN(c->widget);
|
||||
c->combobox = GTK_COMBO_BOX(c->widget);
|
||||
c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget);
|
||||
|
||||
c->onChangedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c);
|
||||
uiEditableComboboxOnChanged(c, defaultOnChanged, NULL);
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiEntry {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkEntry *entry;
|
||||
GtkEditable *editable;
|
||||
void (*onChanged)(uiEntry *, void *);
|
||||
void *onChangedData;
|
||||
gulong onChangedSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiEntry)
|
||||
|
||||
static void onChanged(GtkEditable *editable, gpointer data)
|
||||
{
|
||||
uiEntry *e = uiEntry(data);
|
||||
|
||||
(*(e->onChanged))(e, e->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiEntry *e, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
char *uiEntryText(uiEntry *e)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_entry_get_text(e->entry));
|
||||
}
|
||||
|
||||
void uiEntrySetText(uiEntry *e, const char *text)
|
||||
{
|
||||
// we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
|
||||
g_signal_handler_block(e->editable, e->onChangedSignal);
|
||||
gtk_entry_set_text(e->entry, text);
|
||||
g_signal_handler_unblock(e->editable, e->onChangedSignal);
|
||||
// don't queue the control for resize; entry sizes are independent of their contents
|
||||
}
|
||||
|
||||
void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data)
|
||||
{
|
||||
e->onChanged = f;
|
||||
e->onChangedData = data;
|
||||
}
|
||||
|
||||
int uiEntryReadOnly(uiEntry *e)
|
||||
{
|
||||
return gtk_editable_get_editable(e->editable) == FALSE;
|
||||
}
|
||||
|
||||
void uiEntrySetReadOnly(uiEntry *e, int readonly)
|
||||
{
|
||||
gboolean editable;
|
||||
|
||||
editable = TRUE;
|
||||
if (readonly)
|
||||
editable = FALSE;
|
||||
gtk_editable_set_editable(e->editable, editable);
|
||||
}
|
||||
|
||||
static uiEntry *finishNewEntry(GtkWidget *w, const gchar *signal)
|
||||
{
|
||||
uiEntry *e;
|
||||
|
||||
uiUnixNewControl(uiEntry, e);
|
||||
|
||||
e->widget = w;
|
||||
e->entry = GTK_ENTRY(e->widget);
|
||||
e->editable = GTK_EDITABLE(e->widget);
|
||||
|
||||
e->onChangedSignal = g_signal_connect(e->widget, signal, G_CALLBACK(onChanged), e);
|
||||
uiEntryOnChanged(e, defaultOnChanged, NULL);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
uiEntry *uiNewEntry(void)
|
||||
{
|
||||
return finishNewEntry(gtk_entry_new(), "changed");
|
||||
}
|
||||
|
||||
uiEntry *uiNewPasswordEntry(void)
|
||||
{
|
||||
GtkWidget *e;
|
||||
|
||||
e = gtk_entry_new();
|
||||
gtk_entry_set_visibility(GTK_ENTRY(e), FALSE);
|
||||
return finishNewEntry(e, "changed");
|
||||
}
|
||||
|
||||
// TODO make it use a separate function to be type-safe
|
||||
uiEntry *uiNewSearchEntry(void)
|
||||
{
|
||||
return finishNewEntry(gtk_search_entry_new(), "search-changed");
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 14 april 2016
|
||||
#include "uipriv_unix.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
struct uiFontButton {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkButton *button;
|
||||
GtkFontButton *fb;
|
||||
GtkFontChooser *fc;
|
||||
void (*onChanged)(uiFontButton *, void *);
|
||||
void *onChangedData;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiFontButton)
|
||||
|
||||
// TODO NOTE no need to inhibit the signal; font-set is documented as only being sent when the user changes the font
|
||||
static void onFontSet(GtkFontButton *button, gpointer data)
|
||||
{
|
||||
uiFontButton *b = uiFontButton(data);
|
||||
|
||||
(*(b->onChanged))(b, b->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiFontButton *b, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void uiFontButtonFont(uiFontButton *b, uiFontDescriptor *desc)
|
||||
{
|
||||
PangoFontDescription *pdesc;
|
||||
|
||||
pdesc = gtk_font_chooser_get_font_desc(b->fc);
|
||||
uiprivFontDescriptorFromPangoFontDescription(pdesc, desc);
|
||||
// pdesc is transfer-full and thus is a copy
|
||||
pango_font_description_free(pdesc);
|
||||
}
|
||||
|
||||
void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data)
|
||||
{
|
||||
b->onChanged = f;
|
||||
b->onChangedData = data;
|
||||
}
|
||||
|
||||
uiFontButton *uiNewFontButton(void)
|
||||
{
|
||||
uiFontButton *b;
|
||||
|
||||
uiUnixNewControl(uiFontButton, b);
|
||||
|
||||
b->widget = gtk_font_button_new();
|
||||
b->button = GTK_BUTTON(b->widget);
|
||||
b->fb = GTK_FONT_BUTTON(b->widget);
|
||||
b->fc = GTK_FONT_CHOOSER(b->widget);
|
||||
|
||||
// match behavior on other platforms
|
||||
gtk_font_button_set_show_style(b->fb, TRUE);
|
||||
gtk_font_button_set_show_size(b->fb, TRUE);
|
||||
gtk_font_button_set_use_font(b->fb, FALSE);
|
||||
gtk_font_button_set_use_size(b->fb, FALSE);
|
||||
// other customizations
|
||||
gtk_font_chooser_set_show_preview_entry(b->fc, TRUE);
|
||||
|
||||
g_signal_connect(b->widget, "font-set", G_CALLBACK(onFontSet), b);
|
||||
uiFontButtonOnChanged(b, defaultOnChanged, NULL);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
void uiFreeFontButtonFont(uiFontDescriptor *desc)
|
||||
{
|
||||
uiFreeFontDescriptor(desc);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 11 march 2018
|
||||
#include "uipriv_unix.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
static const PangoStyle pangoItalics[] = {
|
||||
[uiTextItalicNormal] = PANGO_STYLE_NORMAL,
|
||||
[uiTextItalicOblique] = PANGO_STYLE_OBLIQUE,
|
||||
[uiTextItalicItalic] = PANGO_STYLE_ITALIC,
|
||||
};
|
||||
|
||||
static const PangoStretch pangoStretches[] = {
|
||||
[uiTextStretchUltraCondensed] = PANGO_STRETCH_ULTRA_CONDENSED,
|
||||
[uiTextStretchExtraCondensed] = PANGO_STRETCH_EXTRA_CONDENSED,
|
||||
[uiTextStretchCondensed] = PANGO_STRETCH_CONDENSED,
|
||||
[uiTextStretchSemiCondensed] = PANGO_STRETCH_SEMI_CONDENSED,
|
||||
[uiTextStretchNormal] = PANGO_STRETCH_NORMAL,
|
||||
[uiTextStretchSemiExpanded] = PANGO_STRETCH_SEMI_EXPANDED,
|
||||
[uiTextStretchExpanded] = PANGO_STRETCH_EXPANDED,
|
||||
[uiTextStretchExtraExpanded] = PANGO_STRETCH_EXTRA_EXPANDED,
|
||||
[uiTextStretchUltraExpanded] = PANGO_STRETCH_ULTRA_EXPANDED,
|
||||
};
|
||||
|
||||
// for the most part, pango weights correlate to ours
|
||||
// the differences:
|
||||
// - Book — libui: 350, Pango: 380
|
||||
// - Ultra Heavy — libui: 950, Pango: 1000
|
||||
// TODO figure out what to do about this misalignment
|
||||
PangoWeight uiprivWeightToPangoWeight(uiTextWeight w)
|
||||
{
|
||||
return (PangoWeight) w;
|
||||
}
|
||||
|
||||
PangoStyle uiprivItalicToPangoStyle(uiTextItalic i)
|
||||
{
|
||||
return pangoItalics[i];
|
||||
}
|
||||
|
||||
PangoStretch uiprivStretchToPangoStretch(uiTextStretch s)
|
||||
{
|
||||
return pangoStretches[s];
|
||||
}
|
||||
|
||||
PangoFontDescription *uiprivFontDescriptorToPangoFontDescription(const uiFontDescriptor *uidesc)
|
||||
{
|
||||
PangoFontDescription *desc;
|
||||
|
||||
desc = pango_font_description_new();
|
||||
pango_font_description_set_family(desc, uidesc->Family);
|
||||
// see https://developer.gnome.org/pango/1.30/pango-Fonts.html#pango-font-description-set-size and https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-from-double
|
||||
pango_font_description_set_size(desc, pango_units_from_double(uidesc->Size));
|
||||
pango_font_description_set_weight(desc, uiprivWeightToPangoWeight(uidesc->Weight));
|
||||
pango_font_description_set_style(desc, uiprivItalicToPangoStyle(uidesc->Italic));
|
||||
pango_font_description_set_stretch(desc, uiprivStretchToPangoStretch(uidesc->Stretch));
|
||||
return desc;
|
||||
}
|
||||
|
||||
void uiprivFontDescriptorFromPangoFontDescription(PangoFontDescription *pdesc, uiFontDescriptor *uidesc)
|
||||
{
|
||||
PangoStyle pitalic;
|
||||
PangoStretch pstretch;
|
||||
|
||||
uidesc->Family = uiUnixStrdupText(pango_font_description_get_family(pdesc));
|
||||
pitalic = pango_font_description_get_style(pdesc);
|
||||
// TODO reverse the above misalignment if it is corrected
|
||||
uidesc->Weight = pango_font_description_get_weight(pdesc);
|
||||
pstretch = pango_font_description_get_stretch(pdesc);
|
||||
// absolute size does not matter because, as above, 1 device unit == 1 cairo point
|
||||
uidesc->Size = pango_units_to_double(pango_font_description_get_size(pdesc));
|
||||
|
||||
for (uidesc->Italic = uiTextItalicNormal; uidesc->Italic < uiTextItalicItalic; uidesc->Italic++)
|
||||
if (pangoItalics[uidesc->Italic] == pitalic)
|
||||
break;
|
||||
for (uidesc->Stretch = uiTextStretchUltraCondensed; uidesc->Stretch < uiTextStretchUltraExpanded; uidesc->Stretch++)
|
||||
if (pangoStretches[uidesc->Stretch] == pstretch)
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// 8 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct formChild {
|
||||
uiControl *c;
|
||||
int stretchy;
|
||||
GtkWidget *label;
|
||||
gboolean oldhexpand;
|
||||
GtkAlign oldhalign;
|
||||
gboolean oldvexpand;
|
||||
GtkAlign oldvalign;
|
||||
GBinding *labelBinding;
|
||||
};
|
||||
|
||||
struct uiForm {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkGrid *grid;
|
||||
GArray *children;
|
||||
int padded;
|
||||
GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiForm)
|
||||
|
||||
#define ctrl(f, i) &g_array_index(f->children, struct formChild, i)
|
||||
|
||||
static void uiFormDestroy(uiControl *c)
|
||||
{
|
||||
uiForm *f = uiForm(c);
|
||||
struct formChild *fc;
|
||||
guint i;
|
||||
|
||||
// kill the size group
|
||||
g_object_unref(f->stretchygroup);
|
||||
// free all controls
|
||||
for (i = 0; i < f->children->len; i++) {
|
||||
fc = ctrl(f, i);
|
||||
uiControlSetParent(fc->c, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE);
|
||||
uiControlDestroy(fc->c);
|
||||
gtk_widget_destroy(fc->label);
|
||||
}
|
||||
g_array_free(f->children, TRUE);
|
||||
// and then ourselves
|
||||
g_object_unref(f->widget);
|
||||
uiFreeControl(uiControl(f));
|
||||
}
|
||||
|
||||
void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy)
|
||||
{
|
||||
struct formChild fc;
|
||||
GtkWidget *widget;
|
||||
guint row;
|
||||
|
||||
fc.c = c;
|
||||
widget = GTK_WIDGET(uiControlHandle(fc.c));
|
||||
fc.stretchy = stretchy;
|
||||
fc.oldhexpand = gtk_widget_get_hexpand(widget);
|
||||
fc.oldhalign = gtk_widget_get_halign(widget);
|
||||
fc.oldvexpand = gtk_widget_get_vexpand(widget);
|
||||
fc.oldvalign = gtk_widget_get_valign(widget);
|
||||
|
||||
if (stretchy) {
|
||||
gtk_widget_set_vexpand(widget, TRUE);
|
||||
gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
|
||||
gtk_size_group_add_widget(f->stretchygroup, widget);
|
||||
} else
|
||||
gtk_widget_set_vexpand(widget, FALSE);
|
||||
// and make them fill horizontally
|
||||
gtk_widget_set_hexpand(widget, TRUE);
|
||||
gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
|
||||
|
||||
fc.label = gtk_label_new(label);
|
||||
gtk_widget_set_hexpand(fc.label, FALSE);
|
||||
gtk_widget_set_halign(fc.label, GTK_ALIGN_END);
|
||||
gtk_widget_set_vexpand(fc.label, FALSE);
|
||||
if (GTK_IS_SCROLLED_WINDOW(widget))
|
||||
gtk_widget_set_valign(fc.label, GTK_ALIGN_START);
|
||||
else
|
||||
gtk_widget_set_valign(fc.label, GTK_ALIGN_CENTER);
|
||||
gtk_style_context_add_class(gtk_widget_get_style_context(fc.label), "dim-label");
|
||||
row = f->children->len;
|
||||
gtk_grid_attach(f->grid, fc.label,
|
||||
0, row,
|
||||
1, 1);
|
||||
// and make them share visibility so if the control is hidden, so is its label
|
||||
fc.labelBinding = g_object_bind_property(GTK_WIDGET(uiControlHandle(fc.c)), "visible",
|
||||
fc.label, "visible",
|
||||
G_BINDING_SYNC_CREATE);
|
||||
|
||||
uiControlSetParent(fc.c, uiControl(f));
|
||||
uiUnixControlSetContainer(uiUnixControl(fc.c), f->container, FALSE);
|
||||
g_array_append_val(f->children, fc);
|
||||
|
||||
// move the widget to the correct place
|
||||
gtk_container_child_set(f->container, widget,
|
||||
"left-attach", 1,
|
||||
"top-attach", row,
|
||||
NULL);
|
||||
}
|
||||
|
||||
void uiFormDelete(uiForm *f, int index)
|
||||
{
|
||||
struct formChild *fc;
|
||||
GtkWidget *widget;
|
||||
|
||||
fc = ctrl(f, index);
|
||||
widget = GTK_WIDGET(uiControlHandle(fc->c));
|
||||
|
||||
gtk_widget_destroy(fc->label);
|
||||
|
||||
uiControlSetParent(fc->c, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE);
|
||||
|
||||
if (fc->stretchy)
|
||||
gtk_size_group_remove_widget(f->stretchygroup, widget);
|
||||
gtk_widget_set_hexpand(widget, fc->oldhexpand);
|
||||
gtk_widget_set_halign(widget, fc->oldhalign);
|
||||
gtk_widget_set_vexpand(widget, fc->oldvexpand);
|
||||
gtk_widget_set_valign(widget, fc->oldvalign);
|
||||
|
||||
g_array_remove_index(f->children, index);
|
||||
}
|
||||
|
||||
int uiFormNumChildren(uiForm *f)
|
||||
{
|
||||
return (int) f->children->len;
|
||||
}
|
||||
|
||||
int uiFormPadded(uiForm *f)
|
||||
{
|
||||
return f->padded;
|
||||
}
|
||||
|
||||
void uiFormSetPadded(uiForm *f, int padded)
|
||||
{
|
||||
f->padded = padded;
|
||||
if (f->padded) {
|
||||
gtk_grid_set_row_spacing(f->grid, uiprivGTKYPadding);
|
||||
gtk_grid_set_column_spacing(f->grid, uiprivGTKXPadding);
|
||||
} else {
|
||||
gtk_grid_set_row_spacing(f->grid, 0);
|
||||
gtk_grid_set_column_spacing(f->grid, 0);
|
||||
}
|
||||
}
|
||||
|
||||
uiForm *uiNewForm(void)
|
||||
{
|
||||
uiForm *f;
|
||||
|
||||
uiUnixNewControl(uiForm, f);
|
||||
|
||||
f->widget = gtk_grid_new();
|
||||
f->container = GTK_CONTAINER(f->widget);
|
||||
f->grid = GTK_GRID(f->widget);
|
||||
|
||||
f->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
|
||||
|
||||
f->children = g_array_new(FALSE, TRUE, sizeof (struct formChild));
|
||||
|
||||
return f;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 29 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// functions FROM THE FUTURE!
|
||||
// in some cases, because being held back by LTS releases sucks :/
|
||||
// in others, because parts of GTK+ being unstable until recently also sucks :/
|
||||
|
||||
// added in pango 1.38; we need 1.36
|
||||
static PangoAttribute *(*newFeaturesAttr)(const gchar *features) = NULL;
|
||||
static PangoAttribute *(*newFGAlphaAttr)(guint16 alpha) = NULL;
|
||||
static PangoAttribute *(*newBGAlphaAttr)(guint16 alpha) = NULL;
|
||||
|
||||
// added in GTK+ 3.20; we need 3.10
|
||||
static void (*gwpIterSetObjectName)(GtkWidgetPath *path, gint pos, const char *name) = NULL;
|
||||
|
||||
// note that we treat any error as "the symbols aren't there" (and don't care if dlclose() failed)
|
||||
void uiprivLoadFutures(void)
|
||||
{
|
||||
void *handle;
|
||||
|
||||
// dlsym() walks the dependency chain, so opening the current process should be sufficient
|
||||
handle = dlopen(NULL, RTLD_LAZY);
|
||||
if (handle == NULL)
|
||||
return;
|
||||
#define GET(var, fn) *((void **) (&var)) = dlsym(handle, #fn)
|
||||
GET(newFeaturesAttr, pango_attr_font_features_new);
|
||||
GET(newFGAlphaAttr, pango_attr_foreground_alpha_new);
|
||||
GET(newBGAlphaAttr, pango_attr_background_alpha_new);
|
||||
GET(gwpIterSetObjectName, gtk_widget_path_iter_set_object_name);
|
||||
dlclose(handle);
|
||||
}
|
||||
|
||||
PangoAttribute *uiprivFUTURE_pango_attr_font_features_new(const gchar *features)
|
||||
{
|
||||
if (newFeaturesAttr == NULL)
|
||||
return NULL;
|
||||
return (*newFeaturesAttr)(features);
|
||||
}
|
||||
|
||||
PangoAttribute *uiprivFUTURE_pango_attr_foreground_alpha_new(guint16 alpha)
|
||||
{
|
||||
if (newFGAlphaAttr == NULL)
|
||||
return NULL;
|
||||
return (*newFGAlphaAttr)(alpha);
|
||||
}
|
||||
|
||||
PangoAttribute *uiprivFUTURE_pango_attr_background_alpha_new(guint16 alpha)
|
||||
{
|
||||
if (newBGAlphaAttr == NULL)
|
||||
return NULL;
|
||||
return (*newBGAlphaAttr)(alpha);
|
||||
}
|
||||
|
||||
gboolean uiprivFUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name)
|
||||
{
|
||||
if (gwpIterSetObjectName == NULL)
|
||||
return FALSE;
|
||||
(*gwpIterSetObjectName)(path, pos, name);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 25 may 2016
|
||||
#include "uipriv_unix.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
int uiprivGraphemesTakesUTF16(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uiprivGraphemes *uiprivNewGraphemes(void *s, size_t len)
|
||||
{
|
||||
uiprivGraphemes *g;
|
||||
char *text = (char *) s;
|
||||
size_t lenchars;
|
||||
PangoLogAttr *logattrs;
|
||||
size_t i;
|
||||
size_t *op;
|
||||
|
||||
g = uiprivNew(uiprivGraphemes);
|
||||
|
||||
// TODO see if we can use the utf routines
|
||||
lenchars = g_utf8_strlen(text, -1);
|
||||
logattrs = (PangoLogAttr *) uiprivAlloc((lenchars + 1) * sizeof (PangoLogAttr), "PangoLogAttr[] (graphemes)");
|
||||
pango_get_log_attrs(text, len,
|
||||
-1, NULL,
|
||||
logattrs, lenchars + 1);
|
||||
|
||||
// first figure out how many graphemes there are
|
||||
g->len = 0;
|
||||
for (i = 0; i < lenchars; i++)
|
||||
if (logattrs[i].is_cursor_position != 0)
|
||||
g->len++;
|
||||
|
||||
g->pointsToGraphemes = (size_t *) uiprivAlloc((len + 1) * sizeof (size_t), "size_t[] (graphemes)");
|
||||
g->graphemesToPoints = (size_t *) uiprivAlloc((g->len + 1) * sizeof (size_t), "size_t[] (graphemes)");
|
||||
|
||||
// compute the graphemesToPoints array
|
||||
// TODO merge with the next for loop somehow?
|
||||
op = g->graphemesToPoints;
|
||||
for (i = 0; i < lenchars; i++)
|
||||
if (logattrs[i].is_cursor_position != 0)
|
||||
// TODO optimize this
|
||||
*op++ = g_utf8_offset_to_pointer(text, i) - text;
|
||||
// and do the last one
|
||||
*op++ = len;
|
||||
|
||||
// and finally build the pointsToGraphemes array
|
||||
op = g->pointsToGraphemes;
|
||||
for (i = 0; i < g->len; i++) {
|
||||
size_t j;
|
||||
size_t first, last;
|
||||
|
||||
first = g->graphemesToPoints[i];
|
||||
last = g->graphemesToPoints[i + 1];
|
||||
for (j = first; j < last; j++)
|
||||
*op++ = i;
|
||||
}
|
||||
// and do the last one
|
||||
*op++ = i;
|
||||
|
||||
uiprivFree(logattrs);
|
||||
return g;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 9 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct gridChild {
|
||||
uiControl *c;
|
||||
GtkWidget *label;
|
||||
gboolean oldhexpand;
|
||||
GtkAlign oldhalign;
|
||||
gboolean oldvexpand;
|
||||
GtkAlign oldvalign;
|
||||
};
|
||||
|
||||
struct uiGrid {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkGrid *grid;
|
||||
GArray *children;
|
||||
int padded;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiGrid)
|
||||
|
||||
#define ctrl(g, i) &g_array_index(g->children, struct gridChild, i)
|
||||
|
||||
static void uiGridDestroy(uiControl *c)
|
||||
{
|
||||
uiGrid *g = uiGrid(c);
|
||||
struct gridChild *gc;
|
||||
guint i;
|
||||
|
||||
// free all controls
|
||||
for (i = 0; i < g->children->len; i++) {
|
||||
gc = ctrl(g, i);
|
||||
uiControlSetParent(gc->c, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(gc->c), g->container, TRUE);
|
||||
uiControlDestroy(gc->c);
|
||||
}
|
||||
g_array_free(g->children, TRUE);
|
||||
// and then ourselves
|
||||
g_object_unref(g->widget);
|
||||
uiFreeControl(uiControl(g));
|
||||
}
|
||||
|
||||
#define TODO_MASSIVE_HACK(c) \
|
||||
if (!uiUnixControl(c)->addedBefore) { \
|
||||
g_object_ref_sink(GTK_WIDGET(uiControlHandle(uiControl(c)))); \
|
||||
gtk_widget_show(GTK_WIDGET(uiControlHandle(uiControl(c)))); \
|
||||
uiUnixControl(c)->addedBefore = TRUE; \
|
||||
}
|
||||
|
||||
static const GtkAlign gtkAligns[] = {
|
||||
[uiAlignFill] = GTK_ALIGN_FILL,
|
||||
[uiAlignStart] = GTK_ALIGN_START,
|
||||
[uiAlignCenter] = GTK_ALIGN_CENTER,
|
||||
[uiAlignEnd] = GTK_ALIGN_END,
|
||||
};
|
||||
|
||||
static const GtkPositionType gtkPositions[] = {
|
||||
[uiAtLeading] = GTK_POS_LEFT,
|
||||
[uiAtTop] = GTK_POS_TOP,
|
||||
[uiAtTrailing] = GTK_POS_RIGHT,
|
||||
[uiAtBottom] = GTK_POS_BOTTOM,
|
||||
};
|
||||
|
||||
static GtkWidget *prepare(struct gridChild *gc, uiControl *c, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
|
||||
{
|
||||
GtkWidget *widget;
|
||||
|
||||
gc->c = c;
|
||||
widget = GTK_WIDGET(uiControlHandle(gc->c));
|
||||
gc->oldhexpand = gtk_widget_get_hexpand(widget);
|
||||
gc->oldhalign = gtk_widget_get_halign(widget);
|
||||
gc->oldvexpand = gtk_widget_get_vexpand(widget);
|
||||
gc->oldvalign = gtk_widget_get_valign(widget);
|
||||
gtk_widget_set_hexpand(widget, hexpand != 0);
|
||||
gtk_widget_set_halign(widget, gtkAligns[halign]);
|
||||
gtk_widget_set_vexpand(widget, vexpand != 0);
|
||||
gtk_widget_set_valign(widget, gtkAligns[valign]);
|
||||
return widget;
|
||||
}
|
||||
|
||||
void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
|
||||
{
|
||||
struct gridChild gc;
|
||||
GtkWidget *widget;
|
||||
|
||||
widget = prepare(&gc, c, hexpand, halign, vexpand, valign);
|
||||
uiControlSetParent(gc.c, uiControl(g));
|
||||
TODO_MASSIVE_HACK(uiUnixControl(gc.c));
|
||||
gtk_grid_attach(g->grid, widget,
|
||||
left, top,
|
||||
xspan, yspan);
|
||||
g_array_append_val(g->children, gc);
|
||||
}
|
||||
|
||||
void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
|
||||
{
|
||||
struct gridChild gc;
|
||||
GtkWidget *widget;
|
||||
|
||||
widget = prepare(&gc, c, hexpand, halign, vexpand, valign);
|
||||
uiControlSetParent(gc.c, uiControl(g));
|
||||
TODO_MASSIVE_HACK(uiUnixControl(gc.c));
|
||||
gtk_grid_attach_next_to(g->grid, widget,
|
||||
GTK_WIDGET(uiControlHandle(existing)), gtkPositions[at],
|
||||
xspan, yspan);
|
||||
g_array_append_val(g->children, gc);
|
||||
}
|
||||
|
||||
int uiGridPadded(uiGrid *g)
|
||||
{
|
||||
return g->padded;
|
||||
}
|
||||
|
||||
void uiGridSetPadded(uiGrid *g, int padded)
|
||||
{
|
||||
g->padded = padded;
|
||||
if (g->padded) {
|
||||
gtk_grid_set_row_spacing(g->grid, uiprivGTKYPadding);
|
||||
gtk_grid_set_column_spacing(g->grid, uiprivGTKXPadding);
|
||||
} else {
|
||||
gtk_grid_set_row_spacing(g->grid, 0);
|
||||
gtk_grid_set_column_spacing(g->grid, 0);
|
||||
}
|
||||
}
|
||||
|
||||
uiGrid *uiNewGrid(void)
|
||||
{
|
||||
uiGrid *g;
|
||||
|
||||
uiUnixNewControl(uiGrid, g);
|
||||
|
||||
g->widget = gtk_grid_new();
|
||||
g->container = GTK_CONTAINER(g->widget);
|
||||
g->grid = GTK_GRID(g->widget);
|
||||
|
||||
g->children = g_array_new(FALSE, TRUE, sizeof (struct gridChild));
|
||||
|
||||
return g;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiGroup {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkBin *bin;
|
||||
GtkFrame *frame;
|
||||
|
||||
// unfortunately, even though a GtkFrame is a GtkBin, calling gtk_container_set_border_width() on it /includes/ the GtkFrame's label; we don't want that
|
||||
uiprivChild *child;
|
||||
|
||||
int margined;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiGroup)
|
||||
|
||||
static void uiGroupDestroy(uiControl *c)
|
||||
{
|
||||
uiGroup *g = uiGroup(c);
|
||||
|
||||
if (g->child != NULL)
|
||||
uiprivChildDestroy(g->child);
|
||||
g_object_unref(g->widget);
|
||||
uiFreeControl(uiControl(g));
|
||||
}
|
||||
|
||||
char *uiGroupTitle(uiGroup *g)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_frame_get_label(g->frame));
|
||||
}
|
||||
|
||||
void uiGroupSetTitle(uiGroup *g, const char *text)
|
||||
{
|
||||
gtk_frame_set_label(g->frame, text);
|
||||
}
|
||||
|
||||
void uiGroupSetChild(uiGroup *g, uiControl *child)
|
||||
{
|
||||
if (g->child != NULL)
|
||||
uiprivChildRemove(g->child);
|
||||
g->child = uiprivNewChildWithBox(child, uiControl(g), g->container, g->margined);
|
||||
}
|
||||
|
||||
int uiGroupMargined(uiGroup *g)
|
||||
{
|
||||
return g->margined;
|
||||
}
|
||||
|
||||
void uiGroupSetMargined(uiGroup *g, int margined)
|
||||
{
|
||||
g->margined = margined;
|
||||
if (g->child != NULL)
|
||||
uiprivChildSetMargined(g->child, g->margined);
|
||||
}
|
||||
|
||||
uiGroup *uiNewGroup(const char *text)
|
||||
{
|
||||
uiGroup *g;
|
||||
gfloat yalign;
|
||||
GtkLabel *label;
|
||||
PangoAttribute *bold;
|
||||
PangoAttrList *boldlist;
|
||||
|
||||
uiUnixNewControl(uiGroup, g);
|
||||
|
||||
g->widget = gtk_frame_new(text);
|
||||
g->container = GTK_CONTAINER(g->widget);
|
||||
g->bin = GTK_BIN(g->widget);
|
||||
g->frame = GTK_FRAME(g->widget);
|
||||
|
||||
// with GTK+, groupboxes by default have frames and slightly x-offset regular text
|
||||
// they should have no frame and fully left-justified, bold text
|
||||
// preserve default y-alignment
|
||||
gtk_frame_get_label_align(g->frame, NULL, &yalign);
|
||||
gtk_frame_set_label_align(g->frame, 0, yalign);
|
||||
gtk_frame_set_shadow_type(g->frame, GTK_SHADOW_NONE);
|
||||
label = GTK_LABEL(gtk_frame_get_label_widget(g->frame));
|
||||
// this is the boldness level used by GtkPrintUnixDialog
|
||||
// (it technically uses "bold" but see pango's pango-enum-types.c for the name conversion; GType is weird)
|
||||
bold = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
|
||||
boldlist = pango_attr_list_new();
|
||||
pango_attr_list_insert(boldlist, bold);
|
||||
gtk_label_set_attributes(label, boldlist);
|
||||
pango_attr_list_unref(boldlist); // thanks baedert in irc.gimp.net/#gtk+
|
||||
|
||||
return g;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 27 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiImage {
|
||||
double width;
|
||||
double height;
|
||||
GPtrArray *images;
|
||||
};
|
||||
|
||||
static void freeImageRep(gpointer item)
|
||||
{
|
||||
cairo_surface_t *cs = (cairo_surface_t *) item;
|
||||
|
||||
cairo_surface_destroy(cs);
|
||||
}
|
||||
|
||||
uiImage *uiNewImage(double width, double height)
|
||||
{
|
||||
uiImage *i;
|
||||
|
||||
i = uiprivNew(uiImage);
|
||||
i->width = width;
|
||||
i->height = height;
|
||||
i->images = g_ptr_array_new_with_free_func(freeImageRep);
|
||||
return i;
|
||||
}
|
||||
|
||||
void uiFreeImage(uiImage *i)
|
||||
{
|
||||
g_ptr_array_free(i->images, TRUE);
|
||||
uiprivFree(i);
|
||||
}
|
||||
|
||||
void uiImageAppend(uiImage *i, void *pixels, int pixelWidth, int pixelHeight, int byteStride)
|
||||
{
|
||||
cairo_surface_t *cs;
|
||||
uint8_t *data, *pix;
|
||||
int realStride;
|
||||
int x, y;
|
||||
|
||||
// note that this is native-endian
|
||||
cs = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
|
||||
pixelWidth, pixelHeight);
|
||||
if (cairo_surface_status(cs) != CAIRO_STATUS_SUCCESS)
|
||||
/* TODO */;
|
||||
cairo_surface_flush(cs);
|
||||
|
||||
pix = (uint8_t *) pixels;
|
||||
data = (uint8_t *) cairo_image_surface_get_data(cs);
|
||||
realStride = cairo_image_surface_get_stride(cs);
|
||||
for (y = 0; y < pixelHeight; y++) {
|
||||
for (x = 0; x < pixelWidth * 4; x += 4) {
|
||||
union {
|
||||
uint32_t v32;
|
||||
uint8_t v8[4];
|
||||
} v;
|
||||
|
||||
v.v32 = ((uint32_t) (pix[x + 3])) << 24;
|
||||
v.v32 |= ((uint32_t) (pix[x])) << 16;
|
||||
v.v32 |= ((uint32_t) (pix[x + 1])) << 8;
|
||||
v.v32 |= ((uint32_t) (pix[x + 2]));
|
||||
data[x] = v.v8[0];
|
||||
data[x + 1] = v.v8[1];
|
||||
data[x + 2] = v.v8[2];
|
||||
data[x + 3] = v.v8[3];
|
||||
}
|
||||
pix += byteStride;
|
||||
data += realStride;
|
||||
}
|
||||
|
||||
cairo_surface_mark_dirty(cs);
|
||||
g_ptr_array_add(i->images, cs);
|
||||
}
|
||||
|
||||
struct matcher {
|
||||
cairo_surface_t *best;
|
||||
int distX;
|
||||
int distY;
|
||||
int targetX;
|
||||
int targetY;
|
||||
gboolean foundLarger;
|
||||
};
|
||||
|
||||
// TODO is this the right algorithm?
|
||||
static void match(gpointer surface, gpointer data)
|
||||
{
|
||||
cairo_surface_t *cs = (cairo_surface_t *) surface;
|
||||
struct matcher *m = (struct matcher *) data;
|
||||
int x, y;
|
||||
int x2, y2;
|
||||
|
||||
x = cairo_image_surface_get_width(cs);
|
||||
y = cairo_image_surface_get_height(cs);
|
||||
if (m->best == NULL)
|
||||
goto writeMatch;
|
||||
|
||||
if (x < m->targetX && y < m->targetY)
|
||||
if (m->foundLarger)
|
||||
// always prefer larger ones
|
||||
return;
|
||||
if (x >= m->targetX && y >= m->targetY && !m->foundLarger)
|
||||
// we set foundLarger below
|
||||
goto writeMatch;
|
||||
|
||||
x2 = abs(m->targetX - x);
|
||||
y2 = abs(m->targetY - y);
|
||||
if (x2 < m->distX && y2 < m->distY)
|
||||
goto writeMatch;
|
||||
|
||||
// TODO weight one dimension? threshhold?
|
||||
return;
|
||||
|
||||
writeMatch:
|
||||
// must set this here too; otherwise the first image will never have ths set
|
||||
if (x >= m->targetX && y >= m->targetY && !m->foundLarger)
|
||||
m->foundLarger = TRUE;
|
||||
m->best = cs;
|
||||
m->distX = abs(m->targetX - x);
|
||||
m->distY = abs(m->targetY - y);
|
||||
}
|
||||
|
||||
cairo_surface_t *uiprivImageAppropriateSurface(uiImage *i, GtkWidget *w)
|
||||
{
|
||||
struct matcher m;
|
||||
|
||||
m.best = NULL;
|
||||
m.distX = G_MAXINT;
|
||||
m.distY = G_MAXINT;
|
||||
m.targetX = i->width * gtk_widget_get_scale_factor(w);
|
||||
m.targetY = i->height * gtk_widget_get_scale_factor(w);
|
||||
m.foundLarger = FALSE;
|
||||
g_ptr_array_foreach(i->images, match, &m);
|
||||
return m.best;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiLabel {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkMisc *misc;
|
||||
GtkLabel *label;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiLabel)
|
||||
|
||||
char *uiLabelText(uiLabel *l)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_label_get_text(l->label));
|
||||
}
|
||||
|
||||
void uiLabelSetText(uiLabel *l, const char *text)
|
||||
{
|
||||
gtk_label_set_text(l->label, text);
|
||||
}
|
||||
|
||||
uiLabel *uiNewLabel(const char *text)
|
||||
{
|
||||
uiLabel *l;
|
||||
|
||||
uiUnixNewControl(uiLabel, l);
|
||||
|
||||
l->widget = gtk_label_new(text);
|
||||
l->misc = GTK_MISC(l->widget);
|
||||
l->label = GTK_LABEL(l->widget);
|
||||
|
||||
gtk_misc_set_alignment(l->misc, 0, 0);
|
||||
|
||||
return l;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// 6 april 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
uiInitOptions uiprivOptions;
|
||||
|
||||
static GHashTable *timers;
|
||||
|
||||
const char *uiInit(uiInitOptions *o)
|
||||
{
|
||||
GError *err = NULL;
|
||||
const char *msg;
|
||||
|
||||
uiprivOptions = *o;
|
||||
if (gtk_init_with_args(NULL, NULL, NULL, NULL, NULL, &err) == FALSE) {
|
||||
msg = g_strdup(err->message);
|
||||
g_error_free(err);
|
||||
return msg;
|
||||
}
|
||||
uiprivInitAlloc();
|
||||
uiprivLoadFutures();
|
||||
timers = g_hash_table_new(g_direct_hash, g_direct_equal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct timer; // TODO get rid of forward declaration
|
||||
|
||||
static void uninitTimer(gpointer key, gpointer value, gpointer data)
|
||||
{
|
||||
uiprivFree((struct timer *) key);
|
||||
}
|
||||
|
||||
void uiUninit(void)
|
||||
{
|
||||
g_hash_table_foreach(timers, uninitTimer, NULL);
|
||||
g_hash_table_destroy(timers);
|
||||
uiprivUninitMenus();
|
||||
uiprivUninitAlloc();
|
||||
}
|
||||
|
||||
void uiFreeInitError(const char *err)
|
||||
{
|
||||
g_free((gpointer) err);
|
||||
}
|
||||
|
||||
static gboolean (*iteration)(gboolean) = NULL;
|
||||
|
||||
void uiMain(void)
|
||||
{
|
||||
iteration = gtk_main_iteration_do;
|
||||
gtk_main();
|
||||
}
|
||||
|
||||
static gboolean stepsQuit = FALSE;
|
||||
|
||||
// the only difference is we ignore the return value from gtk_main_iteration_do(), since it will always be TRUE if gtk_main() was never called
|
||||
// gtk_main_iteration_do() will still run the main loop regardless
|
||||
static gboolean stepsIteration(gboolean block)
|
||||
{
|
||||
gtk_main_iteration_do(block);
|
||||
return stepsQuit;
|
||||
}
|
||||
|
||||
void uiMainSteps(void)
|
||||
{
|
||||
iteration = stepsIteration;
|
||||
}
|
||||
|
||||
int uiMainStep(int wait)
|
||||
{
|
||||
gboolean block;
|
||||
|
||||
block = FALSE;
|
||||
if (wait)
|
||||
block = TRUE;
|
||||
return (*iteration)(block) == FALSE;
|
||||
}
|
||||
|
||||
// gtk_main_quit() may run immediately, or it may wait for other pending events; "it depends" (thanks mclasen in irc.gimp.net/#gtk+)
|
||||
// PostQuitMessage() on Windows always waits, so we must do so too
|
||||
// we'll do it by using an idle callback
|
||||
static gboolean quit(gpointer data)
|
||||
{
|
||||
if (iteration == stepsIteration)
|
||||
stepsQuit = TRUE;
|
||||
// TODO run a gtk_main() here just to do the cleanup steps of syncing the clipboard and other stuff gtk_main() does before it returns
|
||||
else
|
||||
gtk_main_quit();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void uiQuit(void)
|
||||
{
|
||||
gdk_threads_add_idle(quit, NULL);
|
||||
}
|
||||
|
||||
struct queued {
|
||||
void (*f)(void *);
|
||||
void *data;
|
||||
};
|
||||
|
||||
static gboolean doqueued(gpointer data)
|
||||
{
|
||||
struct queued *q = (struct queued *) data;
|
||||
|
||||
(*(q->f))(q->data);
|
||||
g_free(q);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void uiQueueMain(void (*f)(void *data), void *data)
|
||||
{
|
||||
struct queued *q;
|
||||
|
||||
// we have to use g_new0()/g_free() because uiprivAlloc() is only safe to call on the main thread
|
||||
// for some reason it didn't affect me, but it did affect krakjoe
|
||||
q = g_new0(struct queued, 1);
|
||||
q->f = f;
|
||||
q->data = data;
|
||||
gdk_threads_add_idle(doqueued, q);
|
||||
}
|
||||
|
||||
struct timer {
|
||||
int (*f)(void *);
|
||||
void *data;
|
||||
};
|
||||
|
||||
static gboolean doTimer(gpointer data)
|
||||
{
|
||||
struct timer *t = (struct timer *) data;
|
||||
|
||||
if (!(*(t->f))(t->data)) {
|
||||
g_hash_table_remove(timers, t);
|
||||
uiprivFree(t);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void uiTimer(int milliseconds, int (*f)(void *data), void *data)
|
||||
{
|
||||
struct timer *t;
|
||||
|
||||
t = uiprivNew(struct timer);
|
||||
t->f = f;
|
||||
t->data = data;
|
||||
g_timeout_add(milliseconds, doTimer, t);
|
||||
g_hash_table_add(timers, t);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// 23 april 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
static GArray *menus = NULL;
|
||||
static gboolean menusFinalized = FALSE;
|
||||
static gboolean hasQuit = FALSE;
|
||||
static gboolean hasPreferences = FALSE;
|
||||
static gboolean hasAbout = FALSE;
|
||||
|
||||
struct uiMenu {
|
||||
char *name;
|
||||
GArray *items; // []*uiMenuItem
|
||||
};
|
||||
|
||||
struct uiMenuItem {
|
||||
char *name;
|
||||
int type;
|
||||
void (*onClicked)(uiMenuItem *, uiWindow *, void *);
|
||||
void *onClickedData;
|
||||
GType gtype; // template for new instances; kept in sync with everything else
|
||||
gboolean disabled;
|
||||
gboolean checked;
|
||||
GHashTable *windows; // map[GtkMenuItem]*menuItemWindow
|
||||
};
|
||||
|
||||
struct menuItemWindow {
|
||||
uiWindow *w;
|
||||
gulong signal;
|
||||
};
|
||||
|
||||
enum {
|
||||
typeRegular,
|
||||
typeCheckbox,
|
||||
typeQuit,
|
||||
typePreferences,
|
||||
typeAbout,
|
||||
typeSeparator,
|
||||
};
|
||||
|
||||
// we do NOT want programmatic updates to raise an ::activated signal
|
||||
static void singleSetChecked(GtkCheckMenuItem *menuitem, gboolean checked, gulong signal)
|
||||
{
|
||||
g_signal_handler_block(menuitem, signal);
|
||||
gtk_check_menu_item_set_active(menuitem, checked);
|
||||
g_signal_handler_unblock(menuitem, signal);
|
||||
}
|
||||
|
||||
static void setChecked(uiMenuItem *item, gboolean checked)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
gpointer widget;
|
||||
gpointer ww;
|
||||
struct menuItemWindow *w;
|
||||
|
||||
item->checked = checked;
|
||||
g_hash_table_iter_init(&iter, item->windows);
|
||||
while (g_hash_table_iter_next(&iter, &widget, &ww)) {
|
||||
w = (struct menuItemWindow *) ww;
|
||||
singleSetChecked(GTK_CHECK_MENU_ITEM(widget), item->checked, w->signal);
|
||||
}
|
||||
}
|
||||
|
||||
static void onClicked(GtkMenuItem *menuitem, gpointer data)
|
||||
{
|
||||
uiMenuItem *item = uiMenuItem(data);
|
||||
struct menuItemWindow *w;
|
||||
|
||||
// we need to manually update the checked states of all menu items if one changes
|
||||
// notice that this is getting the checked state of the menu item that this signal is sent from
|
||||
if (item->type == typeCheckbox)
|
||||
setChecked(item, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem)));
|
||||
|
||||
w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, menuitem);
|
||||
(*(item->onClicked))(item, w->w, item->onClickedData);
|
||||
}
|
||||
|
||||
static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static void onQuitClicked(uiMenuItem *item, uiWindow *w, void *data)
|
||||
{
|
||||
if (uiprivShouldQuit())
|
||||
uiQuit();
|
||||
}
|
||||
|
||||
static void menuItemEnableDisable(uiMenuItem *item, gboolean enabled)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
gpointer widget;
|
||||
|
||||
item->disabled = !enabled;
|
||||
g_hash_table_iter_init(&iter, item->windows);
|
||||
while (g_hash_table_iter_next(&iter, &widget, NULL))
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget), enabled);
|
||||
}
|
||||
|
||||
void uiMenuItemEnable(uiMenuItem *item)
|
||||
{
|
||||
menuItemEnableDisable(item, TRUE);
|
||||
}
|
||||
|
||||
void uiMenuItemDisable(uiMenuItem *item)
|
||||
{
|
||||
menuItemEnableDisable(item, FALSE);
|
||||
}
|
||||
|
||||
void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data)
|
||||
{
|
||||
if (item->type == typeQuit)
|
||||
uiprivUserBug("You cannot call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead.");
|
||||
item->onClicked = f;
|
||||
item->onClickedData = data;
|
||||
}
|
||||
|
||||
int uiMenuItemChecked(uiMenuItem *item)
|
||||
{
|
||||
return item->checked != FALSE;
|
||||
}
|
||||
|
||||
void uiMenuItemSetChecked(uiMenuItem *item, int checked)
|
||||
{
|
||||
gboolean c;
|
||||
|
||||
// use explicit values
|
||||
c = FALSE;
|
||||
if (checked)
|
||||
c = TRUE;
|
||||
setChecked(item, c);
|
||||
}
|
||||
|
||||
static uiMenuItem *newItem(uiMenu *m, int type, const char *name)
|
||||
{
|
||||
uiMenuItem *item;
|
||||
|
||||
if (menusFinalized)
|
||||
uiprivUserBug("You cannot create a new menu item after menus have been finalized.");
|
||||
|
||||
item = uiprivNew(uiMenuItem);
|
||||
|
||||
g_array_append_val(m->items, item);
|
||||
|
||||
item->type = type;
|
||||
switch (item->type) {
|
||||
case typeQuit:
|
||||
item->name = g_strdup("Quit");
|
||||
break;
|
||||
case typePreferences:
|
||||
item->name = g_strdup("Preferences...");
|
||||
break;
|
||||
case typeAbout:
|
||||
item->name = g_strdup("About");
|
||||
break;
|
||||
case typeSeparator:
|
||||
break;
|
||||
default:
|
||||
item->name = g_strdup(name);
|
||||
break;
|
||||
}
|
||||
|
||||
if (item->type == typeQuit) {
|
||||
// can't call uiMenuItemOnClicked() here
|
||||
item->onClicked = onQuitClicked;
|
||||
item->onClickedData = NULL;
|
||||
} else
|
||||
uiMenuItemOnClicked(item, defaultOnClicked, NULL);
|
||||
|
||||
switch (item->type) {
|
||||
case typeCheckbox:
|
||||
item->gtype = GTK_TYPE_CHECK_MENU_ITEM;
|
||||
break;
|
||||
case typeSeparator:
|
||||
item->gtype = GTK_TYPE_SEPARATOR_MENU_ITEM;
|
||||
break;
|
||||
default:
|
||||
item->gtype = GTK_TYPE_MENU_ITEM;
|
||||
break;
|
||||
}
|
||||
|
||||
item->windows = g_hash_table_new(g_direct_hash, g_direct_equal);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name)
|
||||
{
|
||||
return newItem(m, typeRegular, name);
|
||||
}
|
||||
|
||||
uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name)
|
||||
{
|
||||
return newItem(m, typeCheckbox, name);
|
||||
}
|
||||
|
||||
uiMenuItem *uiMenuAppendQuitItem(uiMenu *m)
|
||||
{
|
||||
if (hasQuit)
|
||||
uiprivUserBug("You cannot have multiple Quit menu items in the same program.");
|
||||
hasQuit = TRUE;
|
||||
newItem(m, typeSeparator, NULL);
|
||||
return newItem(m, typeQuit, NULL);
|
||||
}
|
||||
|
||||
uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m)
|
||||
{
|
||||
if (hasPreferences)
|
||||
uiprivUserBug("You cannot have multiple Preferences menu items in the same program.");
|
||||
hasPreferences = TRUE;
|
||||
newItem(m, typeSeparator, NULL);
|
||||
return newItem(m, typePreferences, NULL);
|
||||
}
|
||||
|
||||
uiMenuItem *uiMenuAppendAboutItem(uiMenu *m)
|
||||
{
|
||||
if (hasAbout)
|
||||
uiprivUserBug("You cannot have multiple About menu items in the same program.");
|
||||
hasAbout = TRUE;
|
||||
newItem(m, typeSeparator, NULL);
|
||||
return newItem(m, typeAbout, NULL);
|
||||
}
|
||||
|
||||
void uiMenuAppendSeparator(uiMenu *m)
|
||||
{
|
||||
newItem(m, typeSeparator, NULL);
|
||||
}
|
||||
|
||||
uiMenu *uiNewMenu(const char *name)
|
||||
{
|
||||
uiMenu *m;
|
||||
|
||||
if (menusFinalized)
|
||||
uiprivUserBug("You cannot create a new menu after menus have been finalized.");
|
||||
if (menus == NULL)
|
||||
menus = g_array_new(FALSE, TRUE, sizeof (uiMenu *));
|
||||
|
||||
m = uiprivNew(uiMenu);
|
||||
|
||||
g_array_append_val(menus, m);
|
||||
|
||||
m->name = g_strdup(name);
|
||||
m->items = g_array_new(FALSE, TRUE, sizeof (uiMenuItem *));
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
static void appendMenuItem(GtkMenuShell *submenu, uiMenuItem *item, uiWindow *w)
|
||||
{
|
||||
GtkWidget *menuitem;
|
||||
gulong signal;
|
||||
struct menuItemWindow *ww;
|
||||
|
||||
menuitem = g_object_new(item->gtype, NULL);
|
||||
if (item->name != NULL)
|
||||
gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), item->name);
|
||||
if (item->type != typeSeparator) {
|
||||
signal = g_signal_connect(menuitem, "activate", G_CALLBACK(onClicked), item);
|
||||
gtk_widget_set_sensitive(menuitem, !item->disabled);
|
||||
if (item->type == typeCheckbox)
|
||||
singleSetChecked(GTK_CHECK_MENU_ITEM(menuitem), item->checked, signal);
|
||||
}
|
||||
gtk_menu_shell_append(submenu, menuitem);
|
||||
ww = uiprivNew(struct menuItemWindow);
|
||||
ww->w = w;
|
||||
ww->signal = signal;
|
||||
g_hash_table_insert(item->windows, menuitem, ww);
|
||||
}
|
||||
|
||||
GtkWidget *uiprivMakeMenubar(uiWindow *w)
|
||||
{
|
||||
GtkWidget *menubar;
|
||||
guint i, j;
|
||||
uiMenu *m;
|
||||
GtkWidget *menuitem;
|
||||
GtkWidget *submenu;
|
||||
|
||||
menusFinalized = TRUE;
|
||||
|
||||
menubar = gtk_menu_bar_new();
|
||||
|
||||
if (menus != NULL)
|
||||
for (i = 0; i < menus->len; i++) {
|
||||
m = g_array_index(menus, uiMenu *, i);
|
||||
menuitem = gtk_menu_item_new_with_label(m->name);
|
||||
submenu = gtk_menu_new();
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
|
||||
for (j = 0; j < m->items->len; j++)
|
||||
appendMenuItem(GTK_MENU_SHELL(submenu), g_array_index(m->items, uiMenuItem *, j), w);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), menuitem);
|
||||
}
|
||||
|
||||
gtk_widget_set_hexpand(menubar, TRUE);
|
||||
gtk_widget_set_halign(menubar, GTK_ALIGN_FILL);
|
||||
return menubar;
|
||||
}
|
||||
|
||||
struct freeMenuItemData {
|
||||
GArray *items;
|
||||
guint i;
|
||||
};
|
||||
|
||||
static void freeMenuItem(GtkWidget *widget, gpointer data)
|
||||
{
|
||||
struct freeMenuItemData *fmi = (struct freeMenuItemData *) data;
|
||||
uiMenuItem *item;
|
||||
struct menuItemWindow *w;
|
||||
|
||||
item = g_array_index(fmi->items, uiMenuItem *, fmi->i);
|
||||
w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, widget);
|
||||
if (g_hash_table_remove(item->windows, widget) == FALSE)
|
||||
uiprivImplBug("GtkMenuItem %p not in menu item's item/window map", widget);
|
||||
uiprivFree(w);
|
||||
fmi->i++;
|
||||
}
|
||||
|
||||
static void freeMenu(GtkWidget *widget, gpointer data)
|
||||
{
|
||||
guint *i = (guint *) data;
|
||||
uiMenu *m;
|
||||
GtkMenuItem *item;
|
||||
GtkWidget *submenu;
|
||||
struct freeMenuItemData fmi;
|
||||
|
||||
m = g_array_index(menus, uiMenu *, *i);
|
||||
item = GTK_MENU_ITEM(widget);
|
||||
submenu = gtk_menu_item_get_submenu(item);
|
||||
fmi.items = m->items;
|
||||
fmi.i = 0;
|
||||
gtk_container_foreach(GTK_CONTAINER(submenu), freeMenuItem, &fmi);
|
||||
(*i)++;
|
||||
}
|
||||
|
||||
void uiprivFreeMenubar(GtkWidget *mb)
|
||||
{
|
||||
guint i;
|
||||
|
||||
i = 0;
|
||||
gtk_container_foreach(GTK_CONTAINER(mb), freeMenu, &i);
|
||||
// no need to worry about destroying any widgets; destruction of the window they're in will do it for us
|
||||
}
|
||||
|
||||
void uiprivUninitMenus(void)
|
||||
{
|
||||
uiMenu *m;
|
||||
uiMenuItem *item;
|
||||
guint i, j;
|
||||
|
||||
if (menus == NULL)
|
||||
return;
|
||||
for (i = 0; i < menus->len; i++) {
|
||||
m = g_array_index(menus, uiMenu *, i);
|
||||
g_free(m->name);
|
||||
for (j = 0; j < m->items->len; j++) {
|
||||
item = g_array_index(m->items, uiMenuItem *, j);
|
||||
if (g_hash_table_size(item->windows) != 0)
|
||||
// TODO is this really a uiprivUserBug()?
|
||||
uiprivImplBug("menu item %p (%s) still has uiWindows attached; did you forget to destroy some windows?", item, item->name);
|
||||
g_free(item->name);
|
||||
g_hash_table_destroy(item->windows);
|
||||
uiprivFree(item);
|
||||
}
|
||||
g_array_free(m->items, TRUE);
|
||||
uiprivFree(m);
|
||||
}
|
||||
g_array_free(menus, TRUE);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# 23 march 2019
|
||||
|
||||
libui_sources += [
|
||||
'unix/alloc.c',
|
||||
'unix/area.c',
|
||||
'unix/attrstr.c',
|
||||
'unix/box.c',
|
||||
'unix/button.c',
|
||||
'unix/cellrendererbutton.c',
|
||||
'unix/checkbox.c',
|
||||
'unix/child.c',
|
||||
'unix/colorbutton.c',
|
||||
'unix/combobox.c',
|
||||
'unix/control.c',
|
||||
'unix/datetimepicker.c',
|
||||
'unix/debug.c',
|
||||
'unix/draw.c',
|
||||
'unix/drawmatrix.c',
|
||||
'unix/drawpath.c',
|
||||
'unix/drawtext.c',
|
||||
'unix/editablecombo.c',
|
||||
'unix/entry.c',
|
||||
'unix/fontbutton.c',
|
||||
'unix/fontmatch.c',
|
||||
'unix/form.c',
|
||||
'unix/future.c',
|
||||
'unix/graphemes.c',
|
||||
'unix/grid.c',
|
||||
'unix/group.c',
|
||||
'unix/image.c',
|
||||
'unix/label.c',
|
||||
'unix/main.c',
|
||||
'unix/menu.c',
|
||||
'unix/multilineentry.c',
|
||||
'unix/opentype.c',
|
||||
'unix/progressbar.c',
|
||||
'unix/radiobuttons.c',
|
||||
'unix/separator.c',
|
||||
'unix/slider.c',
|
||||
'unix/spinbox.c',
|
||||
'unix/stddialogs.c',
|
||||
'unix/tab.c',
|
||||
'unix/table.c',
|
||||
'unix/tablemodel.c',
|
||||
'unix/text.c',
|
||||
'unix/util.c',
|
||||
'unix/window.c',
|
||||
]
|
||||
|
||||
libui_deps += [
|
||||
dependency('gtk+-3.0',
|
||||
version: '>=3.10.0',
|
||||
method: 'pkg-config',
|
||||
required: true),
|
||||
# We specify these as not required because some Unix systems include them with libc instead of providing them as separate files (thanks textshell and jpakkane in freenode #mesonbuild)
|
||||
meson.get_compiler('c').find_library('m',
|
||||
required: false),
|
||||
meson.get_compiler('c').find_library('dl',
|
||||
required: false),
|
||||
]
|
||||
libui_soversion = '0'
|
||||
libui_rpath = '$ORIGIN'
|
||||
@@ -0,0 +1,122 @@
|
||||
// 6 december 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// TODO GTK_WRAP_WORD_CHAR to avoid spurious resizes?
|
||||
|
||||
struct uiMultilineEntry {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *scontainer;
|
||||
GtkScrolledWindow *sw;
|
||||
GtkWidget *textviewWidget;
|
||||
GtkTextView *textview;
|
||||
GtkTextBuffer *textbuf;
|
||||
void (*onChanged)(uiMultilineEntry *, void *);
|
||||
void *onChangedData;
|
||||
gulong onChangedSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiMultilineEntry)
|
||||
|
||||
static void onChanged(GtkTextBuffer *textbuf, gpointer data)
|
||||
{
|
||||
uiMultilineEntry *e = uiMultilineEntry(data);
|
||||
|
||||
(*(e->onChanged))(e, e->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiMultilineEntry *e, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
char *uiMultilineEntryText(uiMultilineEntry *e)
|
||||
{
|
||||
GtkTextIter start, end;
|
||||
|
||||
gtk_text_buffer_get_start_iter(e->textbuf, &start);
|
||||
gtk_text_buffer_get_end_iter(e->textbuf, &end);
|
||||
|
||||
return gtk_text_buffer_get_text(e->textbuf, &start, &end, TRUE);
|
||||
}
|
||||
|
||||
void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text)
|
||||
{
|
||||
// we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
|
||||
g_signal_handler_block(e->textbuf, e->onChangedSignal);
|
||||
gtk_text_buffer_set_text(e->textbuf, text, -1);
|
||||
g_signal_handler_unblock(e->textbuf, e->onChangedSignal);
|
||||
}
|
||||
|
||||
// TODO scroll to end?
|
||||
void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text)
|
||||
{
|
||||
GtkTextIter end;
|
||||
|
||||
gtk_text_buffer_get_end_iter(e->textbuf, &end);
|
||||
// we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
|
||||
g_signal_handler_block(e->textbuf, e->onChangedSignal);
|
||||
gtk_text_buffer_insert(e->textbuf, &end, text, -1);
|
||||
g_signal_handler_unblock(e->textbuf, e->onChangedSignal);
|
||||
}
|
||||
|
||||
void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data)
|
||||
{
|
||||
e->onChanged = f;
|
||||
e->onChangedData = data;
|
||||
}
|
||||
|
||||
int uiMultilineEntryReadOnly(uiMultilineEntry *e)
|
||||
{
|
||||
return gtk_text_view_get_editable(e->textview) == FALSE;
|
||||
}
|
||||
|
||||
void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly)
|
||||
{
|
||||
gboolean editable;
|
||||
|
||||
editable = TRUE;
|
||||
if (readonly)
|
||||
editable = FALSE;
|
||||
gtk_text_view_set_editable(e->textview, editable);
|
||||
}
|
||||
|
||||
static uiMultilineEntry *finishMultilineEntry(GtkPolicyType hpolicy, GtkWrapMode wrapMode)
|
||||
{
|
||||
uiMultilineEntry *e;
|
||||
|
||||
uiUnixNewControl(uiMultilineEntry, e);
|
||||
|
||||
e->widget = gtk_scrolled_window_new(NULL, NULL);
|
||||
e->scontainer = GTK_CONTAINER(e->widget);
|
||||
e->sw = GTK_SCROLLED_WINDOW(e->widget);
|
||||
gtk_scrolled_window_set_policy(e->sw,
|
||||
hpolicy,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_shadow_type(e->sw, GTK_SHADOW_IN);
|
||||
|
||||
e->textviewWidget = gtk_text_view_new();
|
||||
e->textview = GTK_TEXT_VIEW(e->textviewWidget);
|
||||
gtk_text_view_set_wrap_mode(e->textview, wrapMode);
|
||||
|
||||
gtk_container_add(e->scontainer, e->textviewWidget);
|
||||
// and make the text view visible; only the scrolled window's visibility is controlled by libui
|
||||
gtk_widget_show(e->textviewWidget);
|
||||
|
||||
e->textbuf = gtk_text_view_get_buffer(e->textview);
|
||||
|
||||
e->onChangedSignal = g_signal_connect(e->textbuf, "changed", G_CALLBACK(onChanged), e);
|
||||
uiMultilineEntryOnChanged(e, defaultOnChanged, NULL);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
uiMultilineEntry *uiNewMultilineEntry(void)
|
||||
{
|
||||
return finishMultilineEntry(GTK_POLICY_NEVER, GTK_WRAP_WORD);
|
||||
}
|
||||
|
||||
uiMultilineEntry *uiNewNonWrappingMultilineEntry(void)
|
||||
{
|
||||
return finishMultilineEntry(GTK_POLICY_AUTOMATIC, GTK_WRAP_NONE);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 11 may 2017
|
||||
#include "uipriv_unix.h"
|
||||
#include "attrstr.h"
|
||||
|
||||
// see https://developer.mozilla.org/en/docs/Web/CSS/font-feature-settings
|
||||
static uiForEach toCSS(const uiOpenTypeFeatures *otf, char a, char b, char c, char d, uint32_t value, void *data)
|
||||
{
|
||||
GString *s = (GString *) data;
|
||||
|
||||
// the last trailing comma is removed after foreach is done
|
||||
g_string_append_printf(s, "\"%c%c%c%c\" %" PRIu32 ", ",
|
||||
a, b, c, d, value);
|
||||
return uiForEachContinue;
|
||||
}
|
||||
|
||||
GString *uiprivOpenTypeFeaturesToPangoCSSFeaturesString(const uiOpenTypeFeatures *otf)
|
||||
{
|
||||
GString *s;
|
||||
|
||||
s = g_string_new("");
|
||||
uiOpenTypeFeaturesForEach(otf, toCSS, s);
|
||||
if (s->len != 0)
|
||||
// and remove the last comma
|
||||
g_string_truncate(s, s->len - 2);
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// LONGTERM:
|
||||
// - in GTK+ 3.22 at least, running both a GtkProgressBar and a GtkCellRendererProgress in pulse mode with our code will cause the former to slow down and eventually stop, and I can't tell why at all
|
||||
|
||||
struct uiProgressBar {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkProgressBar *pbar;
|
||||
gboolean indeterminate;
|
||||
guint pulser;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiProgressBar)
|
||||
|
||||
static void uiProgressBarDestroy(uiControl *c)
|
||||
{
|
||||
uiProgressBar *p = uiProgressBar(c);
|
||||
|
||||
// be sure to stop the timeout now
|
||||
if (p->indeterminate)
|
||||
g_source_remove(p->pulser);
|
||||
g_object_unref(p->widget);
|
||||
uiFreeControl(uiControl(p));
|
||||
}
|
||||
|
||||
int uiProgressBarValue(uiProgressBar *p)
|
||||
{
|
||||
if (p->indeterminate)
|
||||
return -1;
|
||||
return (int) (gtk_progress_bar_get_fraction(p->pbar) * 100);
|
||||
}
|
||||
|
||||
static gboolean pulse(void* data)
|
||||
{
|
||||
uiProgressBar *p = uiProgressBar(data);
|
||||
|
||||
gtk_progress_bar_pulse(p->pbar);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void uiProgressBarSetValue(uiProgressBar *p, int value)
|
||||
{
|
||||
if (value == -1) {
|
||||
if (!p->indeterminate) {
|
||||
p->indeterminate = TRUE;
|
||||
// TODO verify the timeout
|
||||
p->pulser = g_timeout_add(100, pulse, p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (p->indeterminate) {
|
||||
p->indeterminate = FALSE;
|
||||
g_source_remove(p->pulser);
|
||||
}
|
||||
|
||||
if (value < 0 || value > 100)
|
||||
uiprivUserBug("Value %d is out of range for a uiProgressBar.", value);
|
||||
|
||||
gtk_progress_bar_set_fraction(p->pbar, ((gdouble) value) / 100);
|
||||
}
|
||||
|
||||
uiProgressBar *uiNewProgressBar(void)
|
||||
{
|
||||
uiProgressBar *p;
|
||||
|
||||
uiUnixNewControl(uiProgressBar, p);
|
||||
|
||||
p->widget = gtk_progress_bar_new();
|
||||
p->pbar = GTK_PROGRESS_BAR(p->widget);
|
||||
|
||||
return p;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// on GTK+ a uiRadioButtons is a GtkBox with each of the GtkRadioButtons as children
|
||||
|
||||
struct uiRadioButtons {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkBox *box;
|
||||
GPtrArray *buttons;
|
||||
void (*onSelected)(uiRadioButtons *, void *);
|
||||
void *onSelectedData;
|
||||
gboolean changing;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiRadioButtons)
|
||||
|
||||
static void defaultOnSelected(uiRadioButtons *r, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static void onToggled(GtkToggleButton *tb, gpointer data)
|
||||
{
|
||||
uiRadioButtons *r = uiRadioButtons(data);
|
||||
|
||||
// only care if a button is selected
|
||||
if (!gtk_toggle_button_get_active(tb))
|
||||
return;
|
||||
// ignore programmatic changes
|
||||
if (r->changing)
|
||||
return;
|
||||
(*(r->onSelected))(r, r->onSelectedData);
|
||||
}
|
||||
|
||||
static void uiRadioButtonsDestroy(uiControl *c)
|
||||
{
|
||||
uiRadioButtons *r = uiRadioButtons(c);
|
||||
GtkWidget *b;
|
||||
|
||||
while (r->buttons->len != 0) {
|
||||
b = GTK_WIDGET(g_ptr_array_remove_index(r->buttons, 0));
|
||||
gtk_widget_destroy(b);
|
||||
}
|
||||
g_ptr_array_free(r->buttons, TRUE);
|
||||
// and free ourselves
|
||||
g_object_unref(r->widget);
|
||||
uiFreeControl(uiControl(r));
|
||||
}
|
||||
|
||||
void uiRadioButtonsAppend(uiRadioButtons *r, const char *text)
|
||||
{
|
||||
GtkWidget *rb;
|
||||
GtkRadioButton *previous;
|
||||
|
||||
previous = NULL;
|
||||
if (r->buttons->len > 0)
|
||||
previous = GTK_RADIO_BUTTON(g_ptr_array_index(r->buttons, 0));
|
||||
rb = gtk_radio_button_new_with_label_from_widget(previous, text);
|
||||
g_signal_connect(rb, "toggled", G_CALLBACK(onToggled), r);
|
||||
gtk_container_add(r->container, rb);
|
||||
g_ptr_array_add(r->buttons, rb);
|
||||
gtk_widget_show(rb);
|
||||
}
|
||||
|
||||
int uiRadioButtonsSelected(uiRadioButtons *r)
|
||||
{
|
||||
GtkToggleButton *tb;
|
||||
guint i;
|
||||
|
||||
for (i = 0; i < r->buttons->len; i++) {
|
||||
tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, i));
|
||||
if (gtk_toggle_button_get_active(tb))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void uiRadioButtonsSetSelected(uiRadioButtons *r, int n)
|
||||
{
|
||||
GtkToggleButton *tb;
|
||||
gboolean active;
|
||||
|
||||
active = TRUE;
|
||||
// TODO this doesn't work
|
||||
if (n == -1) {
|
||||
n = uiRadioButtonsSelected(r);
|
||||
if (n == -1) // no selection; keep it that way
|
||||
return;
|
||||
active = FALSE;
|
||||
}
|
||||
tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, n));
|
||||
// this is easier than remembering all the signals
|
||||
r->changing = TRUE;
|
||||
gtk_toggle_button_set_active(tb, active);
|
||||
r->changing = FALSE;
|
||||
}
|
||||
|
||||
void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data)
|
||||
{
|
||||
r->onSelected = f;
|
||||
r->onSelectedData = data;
|
||||
}
|
||||
|
||||
uiRadioButtons *uiNewRadioButtons(void)
|
||||
{
|
||||
uiRadioButtons *r;
|
||||
|
||||
uiUnixNewControl(uiRadioButtons, r);
|
||||
|
||||
r->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
r->container = GTK_CONTAINER(r->widget);
|
||||
r->box = GTK_BOX(r->widget);
|
||||
|
||||
r->buttons = g_ptr_array_new();
|
||||
|
||||
uiRadioButtonsOnSelected(r, defaultOnSelected, NULL);
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiSeparator {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkSeparator *separator;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiSeparator)
|
||||
|
||||
uiSeparator *uiNewHorizontalSeparator(void)
|
||||
{
|
||||
uiSeparator *s;
|
||||
|
||||
uiUnixNewControl(uiSeparator, s);
|
||||
|
||||
s->widget = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
s->separator = GTK_SEPARATOR(s->widget);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
uiSeparator *uiNewVerticalSeparator(void)
|
||||
{
|
||||
uiSeparator *s;
|
||||
|
||||
uiUnixNewControl(uiSeparator, s);
|
||||
|
||||
s->widget = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
|
||||
s->separator = GTK_SEPARATOR(s->widget);
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
/* Number of digits to represent an n-bit number in decimal: log10(2^n)
|
||||
* Simplify, truncate & add 1 for full digits, add 1 for negative numbers:
|
||||
* -> n log10(2) + 2 <= n * 0.302 + 2
|
||||
*/
|
||||
#define MAX_STRLEN_FOR_NBITS_IN_DECIMAL(n) ((int)((n) * 302 / 1000) + 2)
|
||||
|
||||
struct uiSlider {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkRange *range;
|
||||
GtkScale *scale;
|
||||
void (*onChanged)(uiSlider *, void *);
|
||||
void *onChangedData;
|
||||
void (*onReleased)(uiSlider *, void *);
|
||||
void *onReleasedData;
|
||||
gulong onChangedSignal;
|
||||
gchar tooltip[MAX_STRLEN_FOR_NBITS_IN_DECIMAL(sizeof(int) * CHAR_BIT) + 1];
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiSlider)
|
||||
|
||||
static void _uiSliderUpdateToolTip(uiSlider *s)
|
||||
{
|
||||
g_snprintf(s->tooltip, sizeof(s->tooltip)/sizeof(s->tooltip[0]), "%d", uiSliderValue(s));
|
||||
gtk_widget_set_tooltip_text(s->widget, s->tooltip);
|
||||
}
|
||||
|
||||
static void onChanged(GtkRange *range, gpointer data)
|
||||
{
|
||||
uiSlider *s = uiSlider(data);
|
||||
|
||||
(*(s->onChanged))(s, s->onChangedData);
|
||||
|
||||
if (uiSliderHasToolTip(s))
|
||||
_uiSliderUpdateToolTip(s);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiSlider *s, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static gboolean onReleased(GtkWidget *w, GdkEventButton *event, gpointer data)
|
||||
{
|
||||
uiSlider *s = uiSlider(data);
|
||||
|
||||
(*(s->onReleased))(s, s->onReleasedData);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void defaultOnReleased(uiSlider *s, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
int uiSliderValue(uiSlider *s)
|
||||
{
|
||||
return gtk_range_get_value(s->range);
|
||||
}
|
||||
|
||||
void uiSliderSetValue(uiSlider *s, int value)
|
||||
{
|
||||
// we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise
|
||||
g_signal_handler_block(s->range, s->onChangedSignal);
|
||||
gtk_range_set_value(s->range, value);
|
||||
g_signal_handler_unblock(s->range, s->onChangedSignal);
|
||||
}
|
||||
|
||||
int uiSliderHasToolTip(uiSlider *s)
|
||||
{
|
||||
return gtk_widget_get_has_tooltip(s->widget);
|
||||
}
|
||||
|
||||
void uiSliderSetHasToolTip(uiSlider *s, int hasToolTip)
|
||||
{
|
||||
gtk_widget_set_has_tooltip(s->widget, hasToolTip);
|
||||
|
||||
if (hasToolTip)
|
||||
_uiSliderUpdateToolTip(s);
|
||||
}
|
||||
|
||||
void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data)
|
||||
{
|
||||
s->onChanged = f;
|
||||
s->onChangedData = data;
|
||||
}
|
||||
|
||||
void uiSliderOnReleased(uiSlider *s, void (*f)(uiSlider *, void *), void *data)
|
||||
{
|
||||
s->onReleased = f;
|
||||
s->onReleasedData = data;
|
||||
}
|
||||
|
||||
void uiSliderSetRange(uiSlider *s, int min, int max)
|
||||
{
|
||||
int temp;
|
||||
|
||||
if (min >= max) {
|
||||
temp = min;
|
||||
min = max;
|
||||
max = temp;
|
||||
}
|
||||
|
||||
// we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise
|
||||
g_signal_handler_block(s->range, s->onChangedSignal);
|
||||
gtk_range_set_range(s->range, min, max);
|
||||
g_signal_handler_unblock(s->range, s->onChangedSignal);
|
||||
}
|
||||
|
||||
uiSlider *uiNewSlider(int min, int max)
|
||||
{
|
||||
uiSlider *s;
|
||||
int temp;
|
||||
|
||||
if (min >= max) {
|
||||
temp = min;
|
||||
min = max;
|
||||
max = temp;
|
||||
}
|
||||
|
||||
uiUnixNewControl(uiSlider, s);
|
||||
|
||||
s->widget = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, 1);
|
||||
s->range = GTK_RANGE(s->widget);
|
||||
s->scale = GTK_SCALE(s->widget);
|
||||
|
||||
// do not draw value, show tooltip instead
|
||||
gtk_scale_set_draw_value(s->scale, 0);
|
||||
uiSliderSetHasToolTip(s, 1);
|
||||
|
||||
// ensure integers, just to be safe
|
||||
gtk_scale_set_digits(s->scale, 0);
|
||||
|
||||
s->onChangedSignal = g_signal_connect(s->scale, "value-changed", G_CALLBACK(onChanged), s);
|
||||
g_signal_connect(s->scale, "button-release-event", G_CALLBACK(onReleased), s);
|
||||
uiSliderOnChanged(s, defaultOnChanged, NULL);
|
||||
uiSliderOnReleased(s, defaultOnReleased, NULL);
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiSpinbox {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkEntry *entry;
|
||||
GtkSpinButton *spinButton;
|
||||
void (*onChanged)(uiSpinbox *, void *);
|
||||
void *onChangedData;
|
||||
gulong onChangedSignal;
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaults(uiSpinbox)
|
||||
|
||||
static void onChanged(GtkSpinButton *sb, gpointer data)
|
||||
{
|
||||
uiSpinbox *s = uiSpinbox(data);
|
||||
|
||||
(*(s->onChanged))(s, s->onChangedData);
|
||||
}
|
||||
|
||||
static void defaultOnChanged(uiSpinbox *s, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
int uiSpinboxValue(uiSpinbox *s)
|
||||
{
|
||||
return gtk_spin_button_get_value(s->spinButton);
|
||||
}
|
||||
|
||||
void uiSpinboxSetValue(uiSpinbox *s, int value)
|
||||
{
|
||||
// we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise
|
||||
g_signal_handler_block(s->spinButton, s->onChangedSignal);
|
||||
// this clamps for us
|
||||
gtk_spin_button_set_value(s->spinButton, (gdouble) value);
|
||||
g_signal_handler_unblock(s->spinButton, s->onChangedSignal);
|
||||
}
|
||||
|
||||
void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data)
|
||||
{
|
||||
s->onChanged = f;
|
||||
s->onChangedData = data;
|
||||
}
|
||||
|
||||
uiSpinbox *uiNewSpinbox(int min, int max)
|
||||
{
|
||||
uiSpinbox *s;
|
||||
int temp;
|
||||
|
||||
if (min >= max) {
|
||||
temp = min;
|
||||
min = max;
|
||||
max = temp;
|
||||
}
|
||||
|
||||
uiUnixNewControl(uiSpinbox, s);
|
||||
|
||||
s->widget = gtk_spin_button_new_with_range(min, max, 1);
|
||||
s->entry = GTK_ENTRY(s->widget);
|
||||
s->spinButton = GTK_SPIN_BUTTON(s->widget);
|
||||
|
||||
// ensure integers, just to be safe
|
||||
gtk_spin_button_set_digits(s->spinButton, 0);
|
||||
|
||||
s->onChangedSignal = g_signal_connect(s->spinButton, "value-changed", G_CALLBACK(onChanged), s);
|
||||
uiSpinboxOnChanged(s, defaultOnChanged, NULL);
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 26 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
// LONGTERM figure out why, and describe, that this is the desired behavior
|
||||
// LONGTERM also point out that font and color buttons also work like this
|
||||
|
||||
#define windowWindow(w) (GTK_WINDOW(uiControlHandle(uiControl(w))))
|
||||
|
||||
static char *filedialog(GtkWindow *parent, GtkFileChooserAction mode, const gchar *confirm)
|
||||
{
|
||||
GtkWidget *fcd;
|
||||
GtkFileChooser *fc;
|
||||
gint response;
|
||||
char *filename;
|
||||
|
||||
fcd = gtk_file_chooser_dialog_new(NULL, parent, mode,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
confirm, GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
fc = GTK_FILE_CHOOSER(fcd);
|
||||
gtk_file_chooser_set_local_only(fc, FALSE);
|
||||
gtk_file_chooser_set_select_multiple(fc, FALSE);
|
||||
gtk_file_chooser_set_show_hidden(fc, TRUE);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(fc, TRUE);
|
||||
gtk_file_chooser_set_create_folders(fc, TRUE);
|
||||
response = gtk_dialog_run(GTK_DIALOG(fcd));
|
||||
if (response != GTK_RESPONSE_ACCEPT) {
|
||||
gtk_widget_destroy(fcd);
|
||||
return NULL;
|
||||
}
|
||||
filename = gtk_file_chooser_get_filename(fc);
|
||||
gtk_widget_destroy(fcd);
|
||||
return filename;
|
||||
}
|
||||
|
||||
char *uiOpenFile(uiWindow *parent)
|
||||
{
|
||||
return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open");
|
||||
}
|
||||
|
||||
char *uiOpenFolder(uiWindow *parent)
|
||||
{
|
||||
return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Open");
|
||||
}
|
||||
|
||||
char *uiSaveFile(uiWindow *parent)
|
||||
{
|
||||
return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_SAVE, "_Save");
|
||||
}
|
||||
|
||||
static void msgbox(GtkWindow *parent, const char *title, const char *description, GtkMessageType type, GtkButtonsType buttons)
|
||||
{
|
||||
GtkWidget *md;
|
||||
|
||||
md = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL,
|
||||
type, buttons,
|
||||
"%s", title);
|
||||
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(md), "%s", description);
|
||||
gtk_dialog_run(GTK_DIALOG(md));
|
||||
gtk_widget_destroy(md);
|
||||
}
|
||||
|
||||
void uiMsgBox(uiWindow *parent, const char *title, const char *description)
|
||||
{
|
||||
msgbox(windowWindow(parent), title, description, GTK_MESSAGE_OTHER, GTK_BUTTONS_OK);
|
||||
}
|
||||
|
||||
void uiMsgBoxError(uiWindow *parent, const char *title, const char *description)
|
||||
{
|
||||
msgbox(windowWindow(parent), title, description, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiTab {
|
||||
uiUnixControl c;
|
||||
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkNotebook *notebook;
|
||||
|
||||
GArray *pages; // []*uiprivChild
|
||||
};
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiTab)
|
||||
|
||||
static void uiTabDestroy(uiControl *c)
|
||||
{
|
||||
uiTab *t = uiTab(c);
|
||||
guint i;
|
||||
uiprivChild *page;
|
||||
|
||||
for (i = 0; i < t->pages->len; i++) {
|
||||
page = g_array_index(t->pages, uiprivChild *, i);
|
||||
uiprivChildDestroy(page);
|
||||
}
|
||||
g_array_free(t->pages, TRUE);
|
||||
// and free ourselves
|
||||
g_object_unref(t->widget);
|
||||
uiFreeControl(uiControl(t));
|
||||
}
|
||||
|
||||
void uiTabAppend(uiTab *t, const char *name, uiControl *child)
|
||||
{
|
||||
uiTabInsertAt(t, name, t->pages->len, child);
|
||||
}
|
||||
|
||||
void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child)
|
||||
{
|
||||
uiprivChild *page;
|
||||
|
||||
// this will create a tab, because of gtk_container_add()
|
||||
page = uiprivNewChildWithBox(child, uiControl(t), t->container, 0);
|
||||
|
||||
gtk_notebook_set_tab_label_text(t->notebook, uiprivChildBox(page), name);
|
||||
gtk_notebook_reorder_child(t->notebook, uiprivChildBox(page), n);
|
||||
|
||||
g_array_insert_val(t->pages, n, page);
|
||||
}
|
||||
|
||||
void uiTabDelete(uiTab *t, int n)
|
||||
{
|
||||
uiprivChild *page;
|
||||
|
||||
page = g_array_index(t->pages, uiprivChild *, n);
|
||||
// this will remove the tab, because gtk_widget_destroy() calls gtk_container_remove()
|
||||
uiprivChildRemove(page);
|
||||
g_array_remove_index(t->pages, n);
|
||||
}
|
||||
|
||||
int uiTabNumPages(uiTab *t)
|
||||
{
|
||||
return t->pages->len;
|
||||
}
|
||||
|
||||
int uiTabMargined(uiTab *t, int n)
|
||||
{
|
||||
uiprivChild *page;
|
||||
|
||||
page = g_array_index(t->pages, uiprivChild *, n);
|
||||
return uiprivChildFlag(page);
|
||||
}
|
||||
|
||||
void uiTabSetMargined(uiTab *t, int n, int margined)
|
||||
{
|
||||
uiprivChild *page;
|
||||
|
||||
page = g_array_index(t->pages, uiprivChild *, n);
|
||||
uiprivChildSetFlag(page, margined);
|
||||
uiprivChildSetMargined(page, uiprivChildFlag(page));
|
||||
}
|
||||
|
||||
uiTab *uiNewTab(void)
|
||||
{
|
||||
uiTab *t;
|
||||
|
||||
uiUnixNewControl(uiTab, t);
|
||||
|
||||
t->widget = gtk_notebook_new();
|
||||
t->container = GTK_CONTAINER(t->widget);
|
||||
t->notebook = GTK_NOTEBOOK(t->widget);
|
||||
|
||||
gtk_notebook_set_scrollable(t->notebook, TRUE);
|
||||
|
||||
t->pages = g_array_new(FALSE, TRUE, sizeof (uiprivChild *));
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
// 26 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
#include "table.h"
|
||||
|
||||
// TODO with GDK_SCALE set to 2 the 32x32 images are scaled up to 64x64?
|
||||
|
||||
struct uiTable {
|
||||
uiUnixControl c;
|
||||
GtkWidget *widget;
|
||||
GtkContainer *scontainer;
|
||||
GtkScrolledWindow *sw;
|
||||
GtkWidget *treeWidget;
|
||||
GtkTreeView *tv;
|
||||
uiTableModel *model;
|
||||
GPtrArray *columnParams;
|
||||
int backgroundColumn;
|
||||
// keys are struct rowcol, values are gint
|
||||
// TODO document this properly
|
||||
GHashTable *indeterminatePositions;
|
||||
guint indeterminateTimer;
|
||||
void (*headerOnClicked)(uiTable *, int, void *);
|
||||
void *headerOnClickedData;
|
||||
};
|
||||
|
||||
// use the same size as GtkFileChooserWidget's treeview
|
||||
// TODO refresh when icon theme changes
|
||||
// TODO doesn't work when scaled?
|
||||
// TODO is this even necessary?
|
||||
static void setImageSize(GtkCellRenderer *r)
|
||||
{
|
||||
gint size;
|
||||
gint width, height;
|
||||
gint xpad, ypad;
|
||||
|
||||
size = 16; // fallback used by GtkFileChooserWidget
|
||||
if (gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height) != FALSE)
|
||||
size = MAX(width, height);
|
||||
gtk_cell_renderer_get_padding(r, &xpad, &ypad);
|
||||
gtk_cell_renderer_set_fixed_size(r,
|
||||
2 * xpad + size,
|
||||
2 * ypad + size);
|
||||
}
|
||||
|
||||
static void applyColor(GtkTreeModel *m, GtkTreeIter *iter, int modelColumn, GtkCellRenderer *r, const char *prop, const char *propSet)
|
||||
{
|
||||
GValue value = G_VALUE_INIT;
|
||||
GdkRGBA *rgba;
|
||||
|
||||
gtk_tree_model_get_value(m, iter, modelColumn, &value);
|
||||
rgba = (GdkRGBA *) g_value_get_boxed(&value);
|
||||
if (rgba != NULL)
|
||||
g_object_set(r, prop, rgba, NULL);
|
||||
else
|
||||
g_object_set(r, propSet, FALSE, NULL);
|
||||
g_value_unset(&value);
|
||||
}
|
||||
|
||||
static void setEditable(uiTableModel *m, GtkTreeIter *iter, int modelColumn, GtkCellRenderer *r, const char *prop)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
int row;
|
||||
gboolean editable;
|
||||
|
||||
// TODO avoid the need for this
|
||||
path = gtk_tree_model_get_path(GTK_TREE_MODEL(m), iter);
|
||||
row = gtk_tree_path_get_indices(path)[0];
|
||||
gtk_tree_path_free(path);
|
||||
editable = uiprivTableModelCellEditable(m, row, modelColumn) != 0;
|
||||
g_object_set(r, prop, editable, NULL);
|
||||
}
|
||||
|
||||
static void applyBackgroundColor(uiTable *t, GtkTreeModel *m, GtkTreeIter *iter, GtkCellRenderer *r)
|
||||
{
|
||||
if (t->backgroundColumn != -1)
|
||||
applyColor(m, iter, t->backgroundColumn,
|
||||
r, "cell-background-rgba", "cell-background-set");
|
||||
}
|
||||
|
||||
static void onEdited(uiTableModel *m, int column, const char *pathstr, const uiTableValue *tvalue, GtkTreeIter *iter)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
int row;
|
||||
|
||||
path = gtk_tree_path_new_from_string(pathstr);
|
||||
row = gtk_tree_path_get_indices(path)[0];
|
||||
if (iter != NULL)
|
||||
gtk_tree_model_get_iter(GTK_TREE_MODEL(m), iter, path);
|
||||
gtk_tree_path_free(path);
|
||||
uiprivTableModelSetCellValue(m, row, column, tvalue);
|
||||
}
|
||||
|
||||
struct textColumnParams {
|
||||
uiTable *t;
|
||||
uiTableModel *m;
|
||||
int modelColumn;
|
||||
int editableColumn;
|
||||
uiTableTextColumnOptionalParams params;
|
||||
};
|
||||
|
||||
static void textColumnDataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *m, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct textColumnParams *p = (struct textColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
const gchar *str;
|
||||
|
||||
gtk_tree_model_get_value(m, iter, p->modelColumn, &value);
|
||||
str = g_value_get_string(&value);
|
||||
g_object_set(r, "text", str, NULL);
|
||||
g_value_unset(&value);
|
||||
|
||||
setEditable(p->m, iter, p->editableColumn, r, "editable");
|
||||
|
||||
if (p->params.ColorModelColumn != -1)
|
||||
applyColor(m, iter, p->params.ColorModelColumn,
|
||||
r, "foreground-rgba", "foreground-set");
|
||||
|
||||
applyBackgroundColor(p->t, m, iter, r);
|
||||
}
|
||||
|
||||
static void textColumnEdited(GtkCellRendererText *r, gchar *path, gchar *newText, gpointer data)
|
||||
{
|
||||
struct textColumnParams *p = (struct textColumnParams *) data;
|
||||
uiTableValue *tvalue;
|
||||
GtkTreeIter iter;
|
||||
|
||||
tvalue = uiNewTableValueString(newText);
|
||||
onEdited(p->m, p->modelColumn, path, tvalue, &iter);
|
||||
uiFreeTableValue(tvalue);
|
||||
// and update the column TODO copy comment here
|
||||
textColumnDataFunc(NULL, GTK_CELL_RENDERER(r), GTK_TREE_MODEL(p->m), &iter, data);
|
||||
}
|
||||
|
||||
struct imageColumnParams {
|
||||
uiTable *t;
|
||||
int modelColumn;
|
||||
};
|
||||
|
||||
static void imageColumnDataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *m, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct imageColumnParams *p = (struct imageColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
uiImage *img;
|
||||
|
||||
//TODO setImageSize(r);
|
||||
gtk_tree_model_get_value(m, iter, p->modelColumn, &value);
|
||||
img = (uiImage *) g_value_get_pointer(&value);
|
||||
g_object_set(r, "surface",
|
||||
uiprivImageAppropriateSurface(img, p->t->treeWidget),
|
||||
NULL);
|
||||
g_value_unset(&value);
|
||||
|
||||
applyBackgroundColor(p->t, m, iter, r);
|
||||
}
|
||||
|
||||
struct checkboxColumnParams {
|
||||
uiTable *t;
|
||||
uiTableModel *m;
|
||||
int modelColumn;
|
||||
int editableColumn;
|
||||
};
|
||||
|
||||
static void checkboxColumnDataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *m, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct checkboxColumnParams *p = (struct checkboxColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
gboolean active;
|
||||
|
||||
gtk_tree_model_get_value(m, iter, p->modelColumn, &value);
|
||||
active = g_value_get_int(&value) != 0;
|
||||
g_object_set(r, "active", active, NULL);
|
||||
g_value_unset(&value);
|
||||
|
||||
setEditable(p->m, iter, p->editableColumn, r, "activatable");
|
||||
|
||||
applyBackgroundColor(p->t, m, iter, r);
|
||||
}
|
||||
|
||||
static void checkboxColumnToggled(GtkCellRendererToggle *r, gchar *pathstr, gpointer data)
|
||||
{
|
||||
struct checkboxColumnParams *p = (struct checkboxColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
int v;
|
||||
uiTableValue *tvalue;
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
path = gtk_tree_path_new_from_string(pathstr);
|
||||
gtk_tree_model_get_iter(GTK_TREE_MODEL(p->m), &iter, path);
|
||||
gtk_tree_path_free(path);
|
||||
gtk_tree_model_get_value(GTK_TREE_MODEL(p->m), &iter, p->modelColumn, &value);
|
||||
v = g_value_get_int(&value);
|
||||
g_value_unset(&value);
|
||||
tvalue = uiNewTableValueInt(!v);
|
||||
onEdited(p->m, p->modelColumn, pathstr, tvalue, NULL);
|
||||
uiFreeTableValue(tvalue);
|
||||
// and update the column TODO copy comment here
|
||||
// TODO avoid fetching the model data twice
|
||||
checkboxColumnDataFunc(NULL, GTK_CELL_RENDERER(r), GTK_TREE_MODEL(p->m), &iter, data);
|
||||
}
|
||||
|
||||
struct progressBarColumnParams {
|
||||
uiTable *t;
|
||||
int modelColumn;
|
||||
};
|
||||
|
||||
struct rowcol {
|
||||
int row;
|
||||
int col;
|
||||
};
|
||||
|
||||
static guint rowcolHash(gconstpointer key)
|
||||
{
|
||||
const struct rowcol *rc = (const struct rowcol *) key;
|
||||
guint row, col;
|
||||
|
||||
row = (guint) (rc->row);
|
||||
col = (guint) (rc->col);
|
||||
return row ^ col;
|
||||
}
|
||||
|
||||
static gboolean rowcolEqual(gconstpointer a, gconstpointer b)
|
||||
{
|
||||
const struct rowcol *ra = (const struct rowcol *) a;
|
||||
const struct rowcol *rb = (const struct rowcol *) b;
|
||||
|
||||
return (ra->row == rb->row) && (ra->col == rb->col);
|
||||
}
|
||||
|
||||
static void pulseOne(gpointer key, gpointer value, gpointer data)
|
||||
{
|
||||
uiTable *t = uiTable(data);
|
||||
struct rowcol *rc = (struct rowcol *) key;
|
||||
|
||||
// TODO this is bad: it produces changed handlers for every table because that's how GtkTreeModel works, yet this is per-table because that's how it works
|
||||
// however, a proper fix would require decoupling progress from normal integers, which we could do...
|
||||
uiTableModelRowChanged(t->model, rc->row);
|
||||
}
|
||||
|
||||
static gboolean indeterminatePulse(gpointer data)
|
||||
{
|
||||
uiTable *t = uiTable(data);
|
||||
|
||||
g_hash_table_foreach(t->indeterminatePositions, pulseOne, t);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void progressBarColumnDataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *m, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct progressBarColumnParams *p = (struct progressBarColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
int pval;
|
||||
struct rowcol *rc;
|
||||
gint *val;
|
||||
GtkTreePath *path;
|
||||
|
||||
gtk_tree_model_get_value(m, iter, p->modelColumn, &value);
|
||||
pval = g_value_get_int(&value);
|
||||
rc = uiprivNew(struct rowcol);
|
||||
// TODO avoid the need for this
|
||||
path = gtk_tree_model_get_path(GTK_TREE_MODEL(m), iter);
|
||||
rc->row = gtk_tree_path_get_indices(path)[0];
|
||||
gtk_tree_path_free(path);
|
||||
rc->col = p->modelColumn;
|
||||
val = (gint *) g_hash_table_lookup(p->t->indeterminatePositions, rc);
|
||||
if (pval == -1) {
|
||||
if (val == NULL) {
|
||||
val = uiprivNew(gint);
|
||||
*val = 1;
|
||||
g_hash_table_insert(p->t->indeterminatePositions, rc, val);
|
||||
} else {
|
||||
uiprivFree(rc);
|
||||
(*val)++;
|
||||
if (*val == G_MAXINT)
|
||||
*val = 1;
|
||||
}
|
||||
g_object_set(r,
|
||||
"pulse", *val,
|
||||
NULL);
|
||||
if (p->t->indeterminateTimer == 0)
|
||||
// TODO verify the timeout
|
||||
p->t->indeterminateTimer = g_timeout_add(100, indeterminatePulse, p->t);
|
||||
} else {
|
||||
if (val != NULL) {
|
||||
g_hash_table_remove(p->t->indeterminatePositions, rc);
|
||||
if (g_hash_table_size(p->t->indeterminatePositions) == 0) {
|
||||
g_source_remove(p->t->indeterminateTimer);
|
||||
p->t->indeterminateTimer = 0;
|
||||
}
|
||||
}
|
||||
uiprivFree(rc);
|
||||
g_object_set(r,
|
||||
"pulse", -1,
|
||||
"value", pval,
|
||||
NULL);
|
||||
}
|
||||
g_value_unset(&value);
|
||||
|
||||
applyBackgroundColor(p->t, m, iter, r);
|
||||
}
|
||||
|
||||
struct buttonColumnParams {
|
||||
uiTable *t;
|
||||
uiTableModel *m;
|
||||
int modelColumn;
|
||||
int clickableColumn;
|
||||
};
|
||||
|
||||
static void buttonColumnDataFunc(GtkTreeViewColumn *c, GtkCellRenderer *r, GtkTreeModel *m, GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
struct buttonColumnParams *p = (struct buttonColumnParams *) data;
|
||||
GValue value = G_VALUE_INIT;
|
||||
const gchar *str;
|
||||
|
||||
gtk_tree_model_get_value(m, iter, p->modelColumn, &value);
|
||||
str = g_value_get_string(&value);
|
||||
g_object_set(r, "text", str, NULL);
|
||||
g_value_unset(&value);
|
||||
|
||||
setEditable(p->m, iter, p->clickableColumn, r, "sensitive");
|
||||
|
||||
applyBackgroundColor(p->t, m, iter, r);
|
||||
}
|
||||
|
||||
// TODO wrong type here
|
||||
static void buttonColumnClicked(GtkCellRenderer *r, gchar *pathstr, gpointer data)
|
||||
{
|
||||
struct buttonColumnParams *p = (struct buttonColumnParams *) data;
|
||||
|
||||
onEdited(p->m, p->modelColumn, pathstr, NULL, NULL);
|
||||
}
|
||||
|
||||
uiSortIndicator uiTableHeaderSortIndicator(uiTable *t, int lcol)
|
||||
{
|
||||
GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, lcol);
|
||||
|
||||
if (c == NULL || gtk_tree_view_column_get_sort_indicator(c) == FALSE)
|
||||
return uiSortIndicatorNone;
|
||||
|
||||
if (gtk_tree_view_column_get_sort_order(c) == GTK_SORT_ASCENDING)
|
||||
return uiSortIndicatorAscending;
|
||||
else
|
||||
return uiSortIndicatorDescending;
|
||||
}
|
||||
|
||||
void uiTableHeaderSetSortIndicator(uiTable *t, int lcol, uiSortIndicator indicator)
|
||||
{
|
||||
GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, lcol);
|
||||
|
||||
if (c == NULL)
|
||||
return;
|
||||
|
||||
if (indicator == uiSortIndicatorNone) {
|
||||
gtk_tree_view_column_set_sort_indicator(c, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
gtk_tree_view_column_set_sort_indicator(c, TRUE);
|
||||
if (indicator == uiSortIndicatorAscending)
|
||||
gtk_tree_view_column_set_sort_order(c, GTK_SORT_ASCENDING);
|
||||
else
|
||||
gtk_tree_view_column_set_sort_order(c, GTK_SORT_DESCENDING);
|
||||
}
|
||||
|
||||
void uiTableHeaderOnClicked(uiTable *t, void (*f)(uiTable *, int, void *), void *data)
|
||||
{
|
||||
t->headerOnClicked = f;
|
||||
t->headerOnClickedData = data;
|
||||
}
|
||||
|
||||
static void defaultHeaderOnClicked(uiTable *table, int column, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static void headerOnClicked(GtkTreeViewColumn *c, gpointer data)
|
||||
{
|
||||
guint i;
|
||||
uiTable *t = uiTable(data);
|
||||
|
||||
for (i = 0; i < gtk_tree_view_get_n_columns(t->tv); ++i)
|
||||
if (gtk_tree_view_get_column(t->tv, i) == c)
|
||||
t->headerOnClicked(t, i, t->headerOnClickedData);
|
||||
}
|
||||
|
||||
static GtkTreeViewColumn *addColumn(uiTable *t, const char *name)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = gtk_tree_view_column_new();
|
||||
gtk_tree_view_column_set_resizable(c, TRUE);
|
||||
gtk_tree_view_column_set_title(c, name);
|
||||
gtk_tree_view_column_set_clickable(c, 1);
|
||||
g_signal_connect(c, "clicked", G_CALLBACK(headerOnClicked), t);
|
||||
gtk_tree_view_append_column(t->tv, c);
|
||||
return c;
|
||||
}
|
||||
|
||||
static void addTextColumn(uiTable *t, GtkTreeViewColumn *c, int textModelColumn, int textEditableModelColumn, uiTableTextColumnOptionalParams *textParams)
|
||||
{
|
||||
struct textColumnParams *p;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
p = uiprivNew(struct textColumnParams);
|
||||
p->t = t;
|
||||
// TODO get rid of these fields AND rename t->model in favor of t->m
|
||||
p->m = t->model;
|
||||
p->modelColumn = textModelColumn;
|
||||
p->editableColumn = textEditableModelColumn;
|
||||
if (textParams != NULL)
|
||||
p->params = *textParams;
|
||||
else
|
||||
p->params = uiprivDefaultTextColumnOptionalParams;
|
||||
|
||||
r = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_column_pack_start(c, r, TRUE);
|
||||
gtk_tree_view_column_set_cell_data_func(c, r, textColumnDataFunc, p, NULL);
|
||||
g_signal_connect(r, "edited", G_CALLBACK(textColumnEdited), p);
|
||||
g_ptr_array_add(t->columnParams, p);
|
||||
}
|
||||
|
||||
// TODO rename modelCOlumn and params everywhere
|
||||
void uiTableAppendTextColumn(uiTable *t, const char *name, int textModelColumn, int textEditableModelColumn, uiTableTextColumnOptionalParams *textParams)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = addColumn(t, name);
|
||||
addTextColumn(t, c, textModelColumn, textEditableModelColumn, textParams);
|
||||
}
|
||||
|
||||
static void addImageColumn(uiTable *t, GtkTreeViewColumn *c, int imageModelColumn)
|
||||
{
|
||||
struct imageColumnParams *p;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
p = uiprivNew(struct imageColumnParams);
|
||||
p->t = t;
|
||||
p->modelColumn = imageModelColumn;
|
||||
|
||||
r = gtk_cell_renderer_pixbuf_new();
|
||||
gtk_tree_view_column_pack_start(c, r, FALSE);
|
||||
gtk_tree_view_column_set_cell_data_func(c, r, imageColumnDataFunc, p, NULL);
|
||||
g_ptr_array_add(t->columnParams, p);
|
||||
}
|
||||
|
||||
void uiTableAppendImageColumn(uiTable *t, const char *name, int imageModelColumn)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = addColumn(t, name);
|
||||
addImageColumn(t, c, imageModelColumn);
|
||||
}
|
||||
|
||||
void uiTableAppendImageTextColumn(uiTable *t, const char *name, int imageModelColumn, int textModelColumn, int textEditableModelColumn, uiTableTextColumnOptionalParams *textParams)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = addColumn(t, name);
|
||||
addImageColumn(t, c, imageModelColumn);
|
||||
addTextColumn(t, c, textModelColumn, textEditableModelColumn, textParams);
|
||||
}
|
||||
|
||||
static void addCheckboxColumn(uiTable *t, GtkTreeViewColumn *c, int checkboxModelColumn, int checkboxEditableModelColumn)
|
||||
{
|
||||
struct checkboxColumnParams *p;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
p = uiprivNew(struct checkboxColumnParams);
|
||||
p->t = t;
|
||||
p->m = t->model;
|
||||
p->modelColumn = checkboxModelColumn;
|
||||
p->editableColumn = checkboxEditableModelColumn;
|
||||
|
||||
r = gtk_cell_renderer_toggle_new();
|
||||
gtk_tree_view_column_pack_start(c, r, FALSE);
|
||||
gtk_tree_view_column_set_cell_data_func(c, r, checkboxColumnDataFunc, p, NULL);
|
||||
g_signal_connect(r, "toggled", G_CALLBACK(checkboxColumnToggled), p);
|
||||
g_ptr_array_add(t->columnParams, p);
|
||||
}
|
||||
|
||||
void uiTableAppendCheckboxColumn(uiTable *t, const char *name, int checkboxModelColumn, int checkboxEditableModelColumn)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = addColumn(t, name);
|
||||
addCheckboxColumn(t, c, checkboxModelColumn, checkboxEditableModelColumn);
|
||||
}
|
||||
|
||||
void uiTableAppendCheckboxTextColumn(uiTable *t, const char *name, int checkboxModelColumn, int checkboxEditableModelColumn, int textModelColumn, int textEditableModelColumn, uiTableTextColumnOptionalParams *textParams)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
|
||||
c = addColumn(t, name);
|
||||
addCheckboxColumn(t, c, checkboxModelColumn, checkboxEditableModelColumn);
|
||||
addTextColumn(t, c, textModelColumn, textEditableModelColumn, textParams);
|
||||
}
|
||||
|
||||
void uiTableAppendProgressBarColumn(uiTable *t, const char *name, int progressModelColumn)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
struct progressBarColumnParams *p;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
c = addColumn(t, name);
|
||||
|
||||
p = uiprivNew(struct progressBarColumnParams);
|
||||
p->t = t;
|
||||
// TODO make progress and progressBar consistent everywhere
|
||||
p->modelColumn = progressModelColumn;
|
||||
|
||||
r = gtk_cell_renderer_progress_new();
|
||||
gtk_tree_view_column_pack_start(c, r, TRUE);
|
||||
gtk_tree_view_column_set_cell_data_func(c, r, progressBarColumnDataFunc, p, NULL);
|
||||
g_ptr_array_add(t->columnParams, p);
|
||||
}
|
||||
|
||||
void uiTableAppendButtonColumn(uiTable *t, const char *name, int buttonModelColumn, int buttonClickableModelColumn)
|
||||
{
|
||||
GtkTreeViewColumn *c;
|
||||
struct buttonColumnParams *p;
|
||||
GtkCellRenderer *r;
|
||||
|
||||
c = addColumn(t, name);
|
||||
|
||||
p = uiprivNew(struct buttonColumnParams);
|
||||
p->t = t;
|
||||
p->m = t->model;
|
||||
p->modelColumn = buttonModelColumn;
|
||||
p->clickableColumn = buttonClickableModelColumn;
|
||||
|
||||
r = uiprivNewCellRendererButton();
|
||||
gtk_tree_view_column_pack_start(c, r, TRUE);
|
||||
gtk_tree_view_column_set_cell_data_func(c, r, buttonColumnDataFunc, p, NULL);
|
||||
g_signal_connect(r, "clicked", G_CALLBACK(buttonColumnClicked), p);
|
||||
g_ptr_array_add(t->columnParams, p);
|
||||
}
|
||||
|
||||
int uiTableHeaderVisible(uiTable *t)
|
||||
{
|
||||
return gtk_tree_view_get_headers_visible(t->tv);
|
||||
}
|
||||
|
||||
void uiTableHeaderSetVisible(uiTable *t, int visible)
|
||||
{
|
||||
gtk_tree_view_set_headers_visible(t->tv, visible);
|
||||
}
|
||||
|
||||
uiUnixControlAllDefaultsExceptDestroy(uiTable)
|
||||
|
||||
static void uiTableDestroy(uiControl *c)
|
||||
{
|
||||
uiTable *t = uiTable(c);
|
||||
guint i;
|
||||
|
||||
for (i = 0; i < t->columnParams->len; i++)
|
||||
uiprivFree(g_ptr_array_index(t->columnParams, i));
|
||||
g_ptr_array_free(t->columnParams, TRUE);
|
||||
if (g_hash_table_size(t->indeterminatePositions) != 0)
|
||||
g_source_remove(t->indeterminateTimer);
|
||||
g_hash_table_destroy(t->indeterminatePositions);
|
||||
g_object_unref(t->widget);
|
||||
uiFreeControl(uiControl(t));
|
||||
}
|
||||
|
||||
uiTable *uiNewTable(uiTableParams *p)
|
||||
{
|
||||
uiTable *t;
|
||||
|
||||
uiUnixNewControl(uiTable, t);
|
||||
|
||||
t->model = p->Model;
|
||||
t->columnParams = g_ptr_array_new();
|
||||
t->backgroundColumn = p->RowBackgroundColorModelColumn;
|
||||
|
||||
t->widget = gtk_scrolled_window_new(NULL, NULL);
|
||||
t->scontainer = GTK_CONTAINER(t->widget);
|
||||
t->sw = GTK_SCROLLED_WINDOW(t->widget);
|
||||
gtk_scrolled_window_set_shadow_type(t->sw, GTK_SHADOW_IN);
|
||||
|
||||
t->treeWidget = gtk_tree_view_new_with_model(GTK_TREE_MODEL(t->model));
|
||||
t->tv = GTK_TREE_VIEW(t->treeWidget);
|
||||
// TODO set up t->tv
|
||||
|
||||
gtk_container_add(t->scontainer, t->treeWidget);
|
||||
// and make the tree view visible; only the scrolled window's visibility is controlled by libui
|
||||
gtk_widget_show(t->treeWidget);
|
||||
|
||||
t->indeterminatePositions = g_hash_table_new_full(rowcolHash, rowcolEqual,
|
||||
uiprivFree, uiprivFree);
|
||||
|
||||
uiTableHeaderOnClicked(t, defaultHeaderOnClicked, NULL);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
int uiTableColumnWidth(uiTable *t, int column)
|
||||
{
|
||||
GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, column);
|
||||
return gtk_tree_view_column_get_width(c);
|
||||
}
|
||||
|
||||
void uiTableColumnSetWidth(uiTable *t, int column, int width)
|
||||
{
|
||||
GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, column);
|
||||
gtk_tree_view_column_set_fixed_width(c, width);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 4 june 2018
|
||||
#include "../common/table.h"
|
||||
|
||||
// tablemodel.c
|
||||
#define uiTableModelType (uiTableModel_get_type())
|
||||
#define uiTableModel(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), uiTableModelType, uiTableModel))
|
||||
#define isuiTableModel(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), uiTableModelType))
|
||||
#define uiTableModelClass(class) (G_TYPE_CHECK_CLASS_CAST((class), uiTableModelType, uiTableModelClass))
|
||||
#define isuiTableModelClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), uiTableModel))
|
||||
#define getuiTableModelClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), uiTableModelType, uiTableModelClass))
|
||||
typedef struct uiTableModelClass uiTableModelClass;
|
||||
struct uiTableModel {
|
||||
GObject parent_instance;
|
||||
gint stamp;
|
||||
uiTableModelHandler *mh;
|
||||
};
|
||||
struct uiTableModelClass {
|
||||
GObjectClass parent_class;
|
||||
};
|
||||
extern GType uiTableModel_get_type(void);
|
||||
@@ -0,0 +1,285 @@
|
||||
// 26 june 2016
|
||||
#include "uipriv_unix.h"
|
||||
#include "table.h"
|
||||
|
||||
static void uiTableModel_gtk_tree_model_interface_init(GtkTreeModelIface *iface);
|
||||
|
||||
G_DEFINE_TYPE_WITH_CODE(uiTableModel, uiTableModel, G_TYPE_OBJECT,
|
||||
G_IMPLEMENT_INTERFACE(GTK_TYPE_TREE_MODEL, uiTableModel_gtk_tree_model_interface_init))
|
||||
|
||||
static void uiTableModel_init(uiTableModel *m)
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
static void uiTableModel_dispose(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(uiTableModel_parent_class)->dispose(obj);
|
||||
}
|
||||
|
||||
static void uiTableModel_finalize(GObject *obj)
|
||||
{
|
||||
G_OBJECT_CLASS(uiTableModel_parent_class)->finalize(obj);
|
||||
}
|
||||
|
||||
static GtkTreeModelFlags uiTableModel_get_flags(GtkTreeModel *mm)
|
||||
{
|
||||
return GTK_TREE_MODEL_LIST_ONLY;
|
||||
}
|
||||
|
||||
static gint uiTableModel_get_n_columns(GtkTreeModel *mm)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
|
||||
return uiprivTableModelNumColumns(m);
|
||||
}
|
||||
|
||||
static GType uiTableModel_get_column_type(GtkTreeModel *mm, gint index)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
|
||||
switch (uiprivTableModelColumnType(m, index)) {
|
||||
case uiTableValueTypeString:
|
||||
return G_TYPE_STRING;
|
||||
case uiTableValueTypeImage:
|
||||
return G_TYPE_POINTER;
|
||||
case uiTableValueTypeInt:
|
||||
return G_TYPE_INT;
|
||||
case uiTableValueTypeColor:
|
||||
return GDK_TYPE_RGBA;
|
||||
}
|
||||
// TODO
|
||||
return G_TYPE_INVALID;
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_get_iter(GtkTreeModel *mm, GtkTreeIter *iter, GtkTreePath *path)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
gint row;
|
||||
|
||||
if (gtk_tree_path_get_depth(path) != 1)
|
||||
goto bad;
|
||||
row = gtk_tree_path_get_indices(path)[0];
|
||||
if (row < 0)
|
||||
goto bad;
|
||||
if (row >= uiprivTableModelNumRows(m))
|
||||
goto bad;
|
||||
iter->stamp = m->stamp;
|
||||
iter->user_data = GINT_TO_POINTER(row);
|
||||
return TRUE;
|
||||
bad:
|
||||
iter->stamp = 0;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// GtkListStore returns NULL on error; let's do that too
|
||||
static GtkTreePath *uiTableModel_get_path(GtkTreeModel *mm, GtkTreeIter *iter)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
gint row;
|
||||
|
||||
g_return_val_if_fail(iter->stamp == m->stamp, NULL);
|
||||
|
||||
row = GPOINTER_TO_INT(iter->user_data);
|
||||
return gtk_tree_path_new_from_indices(row, -1);
|
||||
}
|
||||
|
||||
// GtkListStore leaves value empty on failure; let's do the same
|
||||
static void uiTableModel_get_value(GtkTreeModel *mm, GtkTreeIter *iter, gint column, GValue *value)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
gint row;
|
||||
uiTableValue *tvalue;
|
||||
double r, g, b, a;
|
||||
GdkRGBA rgba;
|
||||
|
||||
g_return_if_fail(iter->stamp == m->stamp);
|
||||
|
||||
row = GPOINTER_TO_INT(iter->user_data);
|
||||
tvalue = uiprivTableModelCellValue(m, row, column);
|
||||
switch (uiprivTableModelColumnType(m, column)) {
|
||||
case uiTableValueTypeString:
|
||||
g_value_init(value, G_TYPE_STRING);
|
||||
g_value_set_string(value, uiTableValueString(tvalue));
|
||||
uiFreeTableValue(tvalue);
|
||||
return;
|
||||
case uiTableValueTypeImage:
|
||||
g_value_init(value, G_TYPE_POINTER);
|
||||
g_value_set_pointer(value, uiTableValueImage(tvalue));
|
||||
uiFreeTableValue(tvalue);
|
||||
return;
|
||||
case uiTableValueTypeInt:
|
||||
g_value_init(value, G_TYPE_INT);
|
||||
g_value_set_int(value, uiTableValueInt(tvalue));
|
||||
uiFreeTableValue(tvalue);
|
||||
return;
|
||||
case uiTableValueTypeColor:
|
||||
g_value_init(value, GDK_TYPE_RGBA);
|
||||
if (tvalue == NULL) {
|
||||
g_value_set_boxed(value, NULL);
|
||||
return;
|
||||
}
|
||||
uiTableValueColor(tvalue, &r, &g, &b, &a);
|
||||
uiFreeTableValue(tvalue);
|
||||
rgba.red = r;
|
||||
rgba.green = g;
|
||||
rgba.blue = b;
|
||||
rgba.alpha = a;
|
||||
g_value_set_boxed(value, &rgba);
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_iter_next(GtkTreeModel *mm, GtkTreeIter *iter)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
gint row;
|
||||
|
||||
g_return_val_if_fail(iter->stamp == m->stamp, FALSE);
|
||||
|
||||
row = GPOINTER_TO_INT(iter->user_data);
|
||||
row++;
|
||||
if (row >= uiprivTableModelNumRows(m)) {
|
||||
iter->stamp = 0;
|
||||
return FALSE;
|
||||
}
|
||||
iter->user_data = GINT_TO_POINTER(row);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_iter_previous(GtkTreeModel *mm, GtkTreeIter *iter)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
gint row;
|
||||
|
||||
g_return_val_if_fail(iter->stamp == m->stamp, FALSE);
|
||||
|
||||
row = GPOINTER_TO_INT(iter->user_data);
|
||||
row--;
|
||||
if (row < 0) {
|
||||
iter->stamp = 0;
|
||||
return FALSE;
|
||||
}
|
||||
iter->user_data = GINT_TO_POINTER(row);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_iter_children(GtkTreeModel *mm, GtkTreeIter *iter, GtkTreeIter *parent)
|
||||
{
|
||||
return gtk_tree_model_iter_nth_child(mm, iter, parent, 0);
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_iter_has_child(GtkTreeModel *mm, GtkTreeIter *iter)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gint uiTableModel_iter_n_children(GtkTreeModel *mm, GtkTreeIter *iter)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
|
||||
if (iter != NULL)
|
||||
return 0;
|
||||
return uiprivTableModelNumRows(m);
|
||||
}
|
||||
|
||||
static gboolean uiTableModel_iter_nth_child(GtkTreeModel *mm, GtkTreeIter *iter, GtkTreeIter *parent, gint n)
|
||||
{
|
||||
uiTableModel *m = uiTableModel(mm);
|
||||
|
||||
if (parent != NULL || n < 0 || n >= uiprivTableModelNumRows(m)) {
|
||||
iter->stamp = 0;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
iter->stamp = m->stamp;
|
||||
iter->user_data = GINT_TO_POINTER(n);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean uiTableModel_iter_parent(GtkTreeModel *mm, GtkTreeIter *iter, GtkTreeIter *child)
|
||||
{
|
||||
iter->stamp = 0;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void uiTableModel_class_init(uiTableModelClass *class)
|
||||
{
|
||||
G_OBJECT_CLASS(class)->dispose = uiTableModel_dispose;
|
||||
G_OBJECT_CLASS(class)->finalize = uiTableModel_finalize;
|
||||
}
|
||||
|
||||
static void uiTableModel_gtk_tree_model_interface_init(GtkTreeModelIface *iface)
|
||||
{
|
||||
iface->get_flags = uiTableModel_get_flags;
|
||||
iface->get_n_columns = uiTableModel_get_n_columns;
|
||||
iface->get_column_type = uiTableModel_get_column_type;
|
||||
iface->get_iter = uiTableModel_get_iter;
|
||||
iface->get_path = uiTableModel_get_path;
|
||||
iface->get_value = uiTableModel_get_value;
|
||||
iface->iter_next = uiTableModel_iter_next;
|
||||
iface->iter_previous = uiTableModel_iter_previous;
|
||||
iface->iter_children = uiTableModel_iter_children;
|
||||
iface->iter_has_child = uiTableModel_iter_has_child;
|
||||
iface->iter_n_children = uiTableModel_iter_n_children;
|
||||
iface->iter_nth_child = uiTableModel_iter_nth_child;
|
||||
iface->iter_parent = uiTableModel_iter_parent;
|
||||
// don't specify ref_node() or unref_node()
|
||||
}
|
||||
|
||||
uiTableModel *uiNewTableModel(uiTableModelHandler *mh)
|
||||
{
|
||||
uiTableModel *m;
|
||||
|
||||
m = uiTableModel(g_object_new(uiTableModelType, NULL));
|
||||
while ((m->stamp = g_random_int()) == 0) {
|
||||
//iter of 0 means invalid
|
||||
}
|
||||
m->mh = mh;
|
||||
return m;
|
||||
}
|
||||
|
||||
void uiFreeTableModel(uiTableModel *m)
|
||||
{
|
||||
g_object_unref(m);
|
||||
}
|
||||
|
||||
void uiTableModelRowInserted(uiTableModel *m, int newIndex)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(newIndex, -1);
|
||||
iter.stamp = m->stamp;
|
||||
iter.user_data = GINT_TO_POINTER(newIndex);
|
||||
gtk_tree_model_row_inserted(GTK_TREE_MODEL(m), path, &iter);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
void uiTableModelRowChanged(uiTableModel *m, int index)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(index, -1);
|
||||
iter.stamp = m->stamp;
|
||||
iter.user_data = GINT_TO_POINTER(index);
|
||||
gtk_tree_model_row_changed(GTK_TREE_MODEL(m), path, &iter);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
void uiTableModelRowDeleted(uiTableModel *m, int oldIndex)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(oldIndex, -1);
|
||||
gtk_tree_model_row_deleted(GTK_TREE_MODEL(m), path);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
uiTableModelHandler *uiprivTableModelHandler(uiTableModel *m)
|
||||
{
|
||||
return m->mh;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 9 april 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
char *uiUnixStrdupText(const char *t)
|
||||
{
|
||||
return g_strdup(t);
|
||||
}
|
||||
|
||||
void uiFreeText(char *t)
|
||||
{
|
||||
g_free(t);
|
||||
}
|
||||
|
||||
int uiprivStricmp(const char *a, const char *b)
|
||||
{
|
||||
return strcasecmp(a, b);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 22 april 2015
|
||||
#define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_40
|
||||
#define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_40
|
||||
#define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_10
|
||||
#define GDK_VERSION_MAX_ALLOWED GDK_VERSION_3_10
|
||||
#include <gtk/gtk.h>
|
||||
#include <math.h>
|
||||
#include <dlfcn.h> // see future.c
|
||||
#include <langinfo.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "../ui.h"
|
||||
#include "../ui_unix.h"
|
||||
#include "../common/uipriv.h"
|
||||
|
||||
#define uiprivGTKXMargin 12
|
||||
#define uiprivGTKYMargin 12
|
||||
#define uiprivGTKXPadding 12
|
||||
#define uiprivGTKYPadding 6
|
||||
|
||||
// menu.c
|
||||
extern GtkWidget *uiprivMakeMenubar(uiWindow *);
|
||||
extern void uiprivFreeMenubar(GtkWidget *);
|
||||
extern void uiprivUninitMenus(void);
|
||||
|
||||
// alloc.c
|
||||
extern void uiprivInitAlloc(void);
|
||||
extern void uiprivUninitAlloc(void);
|
||||
|
||||
// util.c
|
||||
extern void uiprivSetMargined(GtkContainer *, int);
|
||||
|
||||
// child.c
|
||||
typedef struct uiprivChild uiprivChild;
|
||||
extern uiprivChild *uiprivNewChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer);
|
||||
extern uiprivChild *uiprivNewChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined);
|
||||
extern void uiprivChildRemove(uiprivChild *c);
|
||||
extern void uiprivChildDestroy(uiprivChild *c);
|
||||
extern GtkWidget *uiprivChildWidget(uiprivChild *c);
|
||||
extern int uiprivChildFlag(uiprivChild *c);
|
||||
extern void uiprivChildSetFlag(uiprivChild *c, int flag);
|
||||
extern GtkWidget *uiprivChildBox(uiprivChild *c);
|
||||
extern void uiprivChildSetMargined(uiprivChild *c, int margined);
|
||||
|
||||
// draw.c
|
||||
extern uiDrawContext *uiprivNewContext(cairo_t *cr, GtkStyleContext *style);
|
||||
extern void uiprivFreeContext(uiDrawContext *);
|
||||
|
||||
// image.c
|
||||
extern cairo_surface_t *uiprivImageAppropriateSurface(uiImage *i, GtkWidget *w);
|
||||
|
||||
// cellrendererbutton.c
|
||||
extern GtkCellRenderer *uiprivNewCellRendererButton(void);
|
||||
|
||||
// future.c
|
||||
extern void uiprivLoadFutures(void);
|
||||
extern PangoAttribute *uiprivFUTURE_pango_attr_font_features_new(const gchar *features);
|
||||
extern PangoAttribute *uiprivFUTURE_pango_attr_foreground_alpha_new(guint16 alpha);
|
||||
extern PangoAttribute *uiprivFUTURE_pango_attr_background_alpha_new(guint16 alpha);
|
||||
extern gboolean uiprivFUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name);
|
||||
@@ -0,0 +1,10 @@
|
||||
// 18 april 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
void uiprivSetMargined(GtkContainer *c, int margined)
|
||||
{
|
||||
if (margined)
|
||||
gtk_container_set_border_width(c, uiprivGTKXMargin);
|
||||
else
|
||||
gtk_container_set_border_width(c, 0);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// 11 june 2015
|
||||
#include "uipriv_unix.h"
|
||||
|
||||
struct uiWindow {
|
||||
uiUnixControl c;
|
||||
|
||||
GtkWidget *widget;
|
||||
GtkContainer *container;
|
||||
GtkWindow *window;
|
||||
|
||||
GtkWidget *vboxWidget;
|
||||
GtkContainer *vboxContainer;
|
||||
GtkBox *vbox;
|
||||
|
||||
GtkWidget *childHolderWidget;
|
||||
GtkContainer *childHolderContainer;
|
||||
|
||||
GtkWidget *menubar;
|
||||
|
||||
uiControl *child;
|
||||
int margined;
|
||||
int resizeable;
|
||||
int focused;
|
||||
|
||||
int (*onClosing)(uiWindow *, void *);
|
||||
void *onClosingData;
|
||||
void (*onContentSizeChanged)(uiWindow *, void *);
|
||||
void *onContentSizeChangedData;
|
||||
void (*onFocusChanged)(uiWindow *, void *);
|
||||
void *onFocusChangedData;
|
||||
gboolean fullscreen;
|
||||
};
|
||||
|
||||
static gboolean onClosing(GtkWidget *win, GdkEvent *e, gpointer data)
|
||||
{
|
||||
uiWindow *w = uiWindow(data);
|
||||
|
||||
// manually destroy the window ourselves; don't let the delete-event handler do it
|
||||
if ((*(w->onClosing))(w, w->onClosingData))
|
||||
uiControlDestroy(uiControl(w));
|
||||
// don't continue to the default delete-event handler; we destroyed the window by now
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void onSizeAllocate(GtkWidget *widget, GdkRectangle *allocation, gpointer data)
|
||||
{
|
||||
uiWindow *w = uiWindow(data);
|
||||
|
||||
// TODO deal with spurious size-allocates
|
||||
(*(w->onContentSizeChanged))(w, w->onContentSizeChangedData);
|
||||
}
|
||||
|
||||
static gboolean onGetFocus(GtkWidget *win, GdkEvent *e, gpointer data)
|
||||
{
|
||||
uiWindow *w = uiWindow(data);
|
||||
w->focused = 1;
|
||||
w->onFocusChanged(w, w->onFocusChangedData);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean onLoseFocus(GtkWidget *win, GdkEvent *e, gpointer data)
|
||||
{
|
||||
uiWindow *w = uiWindow(data);
|
||||
w->focused = 0;
|
||||
w->onFocusChanged(w, w->onFocusChangedData);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static int defaultOnClosing(uiWindow *w, void *data)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static void defaultOnFocusChanged(uiWindow *w, void *data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
static void uiWindowDestroy(uiControl *c)
|
||||
{
|
||||
uiWindow *w = uiWindow(c);
|
||||
|
||||
// first hide ourselves
|
||||
gtk_widget_hide(w->widget);
|
||||
// now destroy the child
|
||||
if (w->child != NULL) {
|
||||
uiControlSetParent(w->child, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE);
|
||||
uiControlDestroy(w->child);
|
||||
}
|
||||
// now destroy the menus, if any
|
||||
if (w->menubar != NULL)
|
||||
uiprivFreeMenubar(w->menubar);
|
||||
gtk_widget_destroy(w->childHolderWidget);
|
||||
gtk_widget_destroy(w->vboxWidget);
|
||||
// and finally free ourselves
|
||||
// use gtk_widget_destroy() instead of g_object_unref() because GTK+ has internal references (see #165)
|
||||
gtk_widget_destroy(w->widget);
|
||||
uiFreeControl(uiControl(w));
|
||||
}
|
||||
|
||||
uiUnixControlDefaultHandle(uiWindow)
|
||||
|
||||
uiControl *uiWindowParent(uiControl *c)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void uiWindowSetParent(uiControl *c, uiControl *parent)
|
||||
{
|
||||
uiUserBugCannotSetParentOnToplevel("uiWindow");
|
||||
}
|
||||
|
||||
static int uiWindowToplevel(uiControl *c)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
uiUnixControlDefaultVisible(uiWindow)
|
||||
|
||||
static void uiWindowShow(uiControl *c)
|
||||
{
|
||||
uiWindow *w = uiWindow(c);
|
||||
|
||||
// don't use gtk_widget_show_all() as that will show all children, regardless of user settings
|
||||
// don't use gtk_widget_show(); that doesn't bring to front or give keyboard focus
|
||||
// (gtk_window_present() does call gtk_widget_show() though)
|
||||
gtk_window_present(w->window);
|
||||
}
|
||||
|
||||
uiUnixControlDefaultHide(uiWindow)
|
||||
uiUnixControlDefaultEnabled(uiWindow)
|
||||
uiUnixControlDefaultEnable(uiWindow)
|
||||
uiUnixControlDefaultDisable(uiWindow)
|
||||
// TODO?
|
||||
uiUnixControlDefaultSetContainer(uiWindow)
|
||||
|
||||
char *uiWindowTitle(uiWindow *w)
|
||||
{
|
||||
return uiUnixStrdupText(gtk_window_get_title(w->window));
|
||||
}
|
||||
|
||||
void uiWindowSetTitle(uiWindow *w, const char *title)
|
||||
{
|
||||
gtk_window_set_title(w->window, title);
|
||||
}
|
||||
|
||||
void uiWindowContentSize(uiWindow *w, int *width, int *height)
|
||||
{
|
||||
GtkAllocation allocation;
|
||||
|
||||
gtk_widget_get_allocation(w->childHolderWidget, &allocation);
|
||||
*width = allocation.width;
|
||||
*height = allocation.height;
|
||||
}
|
||||
|
||||
void uiWindowSetContentSize(uiWindow *w, int width, int height)
|
||||
{
|
||||
GtkAllocation childAlloc;
|
||||
gint winWidth, winHeight;
|
||||
|
||||
// we need to resize the child holder widget to the given size
|
||||
// we can't resize that without running the event loop
|
||||
// but we can do gtk_window_set_size()
|
||||
// so how do we deal with the differences in sizes?
|
||||
// simple arithmetic, of course!
|
||||
|
||||
// from what I can tell, the return from gtk_widget_get_allocation(w->window) and gtk_window_get_size(w->window) will be the same
|
||||
// this is not affected by Wayland and not affected by GTK+ builtin CSD
|
||||
// so we can safely juse use them to get the real window size!
|
||||
// since we're using gtk_window_resize(), use the latter
|
||||
gtk_window_get_size(w->window, &winWidth, &winHeight);
|
||||
|
||||
// now get the child holder widget's current allocation
|
||||
gtk_widget_get_allocation(w->childHolderWidget, &childAlloc);
|
||||
// and punch that out of the window size
|
||||
winWidth -= childAlloc.width;
|
||||
winHeight -= childAlloc.height;
|
||||
|
||||
// now we just need to add the new size back in
|
||||
winWidth += width;
|
||||
winHeight += height;
|
||||
// and set it
|
||||
// this will not move the window in my tests, so we're good
|
||||
gtk_window_resize(w->window, winWidth, winHeight);
|
||||
}
|
||||
|
||||
int uiWindowFullscreen(uiWindow *w)
|
||||
{
|
||||
return w->fullscreen;
|
||||
}
|
||||
|
||||
// TODO use window-state-event to track
|
||||
// TODO does this send an extra size changed?
|
||||
// TODO what behavior do we want?
|
||||
void uiWindowSetFullscreen(uiWindow *w, int fullscreen)
|
||||
{
|
||||
w->fullscreen = fullscreen;
|
||||
if (w->fullscreen)
|
||||
gtk_window_fullscreen(w->window);
|
||||
else
|
||||
gtk_window_unfullscreen(w->window);
|
||||
}
|
||||
|
||||
void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data)
|
||||
{
|
||||
w->onContentSizeChanged = f;
|
||||
w->onContentSizeChangedData = data;
|
||||
}
|
||||
|
||||
void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data)
|
||||
{
|
||||
w->onClosing = f;
|
||||
w->onClosingData = data;
|
||||
}
|
||||
|
||||
int uiWindowFocused(uiWindow *w)
|
||||
{
|
||||
return w->focused;
|
||||
}
|
||||
|
||||
void uiWindowOnFocusChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data)
|
||||
{
|
||||
w->onFocusChanged = f;
|
||||
w->onFocusChangedData = data;
|
||||
}
|
||||
|
||||
int uiWindowBorderless(uiWindow *w)
|
||||
{
|
||||
return gtk_window_get_decorated(w->window) == FALSE;
|
||||
}
|
||||
|
||||
void uiWindowSetBorderless(uiWindow *w, int borderless)
|
||||
{
|
||||
gtk_window_set_decorated(w->window, borderless == 0);
|
||||
}
|
||||
|
||||
// TODO save and restore expands and aligns
|
||||
void uiWindowSetChild(uiWindow *w, uiControl *child)
|
||||
{
|
||||
if (w->child != NULL) {
|
||||
uiControlSetParent(w->child, NULL);
|
||||
uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE);
|
||||
}
|
||||
w->child = child;
|
||||
if (w->child != NULL) {
|
||||
uiControlSetParent(w->child, uiControl(w));
|
||||
uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
int uiWindowMargined(uiWindow *w)
|
||||
{
|
||||
return w->margined;
|
||||
}
|
||||
|
||||
void uiWindowSetMargined(uiWindow *w, int margined)
|
||||
{
|
||||
w->margined = margined;
|
||||
uiprivSetMargined(w->childHolderContainer, w->margined);
|
||||
}
|
||||
|
||||
int uiWindowResizeable(uiWindow *w)
|
||||
{
|
||||
return w->resizeable;
|
||||
}
|
||||
|
||||
void uiWindowSetResizeable(uiWindow *w, int resizeable)
|
||||
{
|
||||
// workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/4945
|
||||
// calling gtk_window_set_resizable(w->window, 0) will cause the window to resize to default size
|
||||
// (default is smallest size here because we're using gtk_window_resize() when creating the window)
|
||||
// to prevent this we call gtk_window_set_default_size() on the current window size so that it doesn't resize
|
||||
if (resizeable == 0) {
|
||||
gint width, height;
|
||||
gtk_window_get_size(w->window, &width, &height);
|
||||
gtk_window_set_default_size(w->window, width, height);
|
||||
}
|
||||
|
||||
w->resizeable = resizeable;
|
||||
gtk_window_set_resizable(w->window, resizeable);
|
||||
}
|
||||
|
||||
uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar)
|
||||
{
|
||||
uiWindow *w;
|
||||
|
||||
uiUnixNewControl(uiWindow, w);
|
||||
|
||||
w->resizeable = TRUE;
|
||||
w->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
w->container = GTK_CONTAINER(w->widget);
|
||||
w->window = GTK_WINDOW(w->widget);
|
||||
|
||||
gtk_window_set_title(w->window, title);
|
||||
gtk_window_resize(w->window, width, height);
|
||||
|
||||
w->vboxWidget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
w->vboxContainer = GTK_CONTAINER(w->vboxWidget);
|
||||
w->vbox = GTK_BOX(w->vboxWidget);
|
||||
|
||||
// set the vbox as the GtkWindow child
|
||||
gtk_container_add(w->container, w->vboxWidget);
|
||||
|
||||
if (hasMenubar) {
|
||||
w->menubar = uiprivMakeMenubar(uiWindow(w));
|
||||
gtk_container_add(w->vboxContainer, w->menubar);
|
||||
}
|
||||
|
||||
w->childHolderWidget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
w->childHolderContainer = GTK_CONTAINER(w->childHolderWidget);
|
||||
gtk_widget_set_hexpand(w->childHolderWidget, TRUE);
|
||||
gtk_widget_set_halign(w->childHolderWidget, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_vexpand(w->childHolderWidget, TRUE);
|
||||
gtk_widget_set_valign(w->childHolderWidget, GTK_ALIGN_FILL);
|
||||
gtk_container_add(w->vboxContainer, w->childHolderWidget);
|
||||
|
||||
// show everything in the vbox, but not the GtkWindow itself
|
||||
gtk_widget_show_all(w->vboxWidget);
|
||||
|
||||
// and connect our events
|
||||
g_signal_connect(w->widget, "delete-event", G_CALLBACK(onClosing), w);
|
||||
g_signal_connect(w->childHolderWidget, "size-allocate", G_CALLBACK(onSizeAllocate), w);
|
||||
g_signal_connect(w->widget, "focus-in-event", G_CALLBACK(onGetFocus), w);
|
||||
g_signal_connect(w->widget, "focus-out-event", G_CALLBACK(onLoseFocus), w);
|
||||
|
||||
uiWindowOnClosing(w, defaultOnClosing, NULL);
|
||||
uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL);
|
||||
|
||||
uiWindowOnFocusChanged(w, defaultOnFocusChanged, NULL);
|
||||
|
||||
// normally it's SetParent() that does this, but we can't call SetParent() on a uiWindow
|
||||
// TODO we really need to clean this up, especially since see uiWindowDestroy() above
|
||||
g_object_ref(w->widget);
|
||||
|
||||
return w;
|
||||
}
|
||||
Reference in new issue
Block a user