From 211ac6e2c497dccb6ec470f43529e776e14c19b6 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Thu, 15 Oct 2026 22:43:22 +0200 Subject: Use logical scrolling + m misc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- Makefile | 6 +- TODO | 25 +++ char-table.c | 192 +++++++++-------- char-table.h | 2 +- common.h | 7 + gcmap.c | 34 ++- gcmap.desktop | 8 + logical-scroll-pane.c | 579 ++++++++++++++++++++++++++++++++++++++++++++++++++ logical-scroll-pane.h | 59 +++++ 9 files changed, 805 insertions(+), 107 deletions(-) create mode 100644 TODO create mode 100644 gcmap.desktop create mode 100644 logical-scroll-pane.c create mode 100644 logical-scroll-pane.h diff --git a/Makefile b/Makefile index 0725b98..25034c9 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,13 @@ include $(CONFIGFILE) OBJ =\ gcmap.o\ char-table.o\ + logical-scroll-pane.o\ colour.o HDR =\ common.h\ - char-table.h + char-table.h\ + logical-scroll-pane.h all: gcmap $(OBJ): $(HDR) @@ -26,10 +28,12 @@ install: gcmap mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" cp -- gcmap "$(DESTDIR)$(PREFIX)/bin/" cp -- gcmap.1 "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- gcmap.desktop "$(DESTDIR)$(PREFIX)/share/applications/" uninstall: -rm -f -- "$(DESTDIR)$(PREFIX)/bin/gcmap" -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/gcmap.1" + -rm -f -- "$(DESTDIR)$(PREFIX)/share/applications/gcmap.desktop" clean: -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda diff --git a/TODO b/TODO new file mode 100644 index 0000000..07fc25d --- /dev/null +++ b/TODO @@ -0,0 +1,25 @@ +Add bookmarks expander under the tabbed pane + +Mark whenever a fallback font is used for a glyph + +Mark reserved, non-printable, and unassigned characters + +Add handheld-layout (with compile-time opt-in as default): + alternative 1: put group list, as combo box, above table + alternative 2: put group list as tab + also: skip the menubar (but add the accelerators) + +Remember some settings + +Add font blacklist + +Add support for command line operands: select listed fonts by path + +Add editor to font drop-down to make it easier to select a font + +Scrolling inside a GtkScrolledWindow or LogicalScrollPane shall steal focus + +Scrolling with Control held down shall change zoom. The hovered cell shall +remain visible, preferably at the same Y position as the pointer. The pointer +shall jump to the new position of the cell so account for it being impossible +to ensure that cell remains at the same X position as the pointer. diff --git a/char-table.c b/char-table.c index d49ef19..4aec952 100644 --- a/char-table.c +++ b/char-table.c @@ -7,6 +7,12 @@ #define CELL_X_CLEARANCE 2 #define CELL_Y_CLEARANCE 2 +struct cells { + int r0, c0; /* inclusive */ + int r1, c1; /* exclusive */ + int r_off; +}; + G_DEFINE_TYPE(CharTable, gcmap_char_table, GTK_TYPE_DRAWING_AREA) @@ -97,30 +103,47 @@ fit_text(CharTable *this, const char *text, int *text_w_out, int *text_h_out) } +static void +get_exposed_cells(GtkWidget *widget, GdkEventExpose *event, struct cells *cells_out) +{ + CharTable *this = GCMAP_CHAR_TABLE(widget); + GdkRectangle area = event->area; + LogicalScrollPane *scrolls = LOGICAL_SCROLL_PANE(gtk_widget_get_parent(widget)); + GtkAdjustment *vadjustment = logical_scroll_pane_get_vadjustment(scrolls); + + cells_out->r0 = area.y * this->nrows / this->viewport_h; + cells_out->c0 = area.x * this->ncols / this->viewport_w; + cells_out->r1 = (area.y + area.height - 1) * this->nrows / this->viewport_h + 1; + cells_out->c1 = (area.x + area.width - 1) * this->ncols / this->viewport_w + 1; + + cells_out->r0 = MAX(cells_out->r0, 0); + cells_out->c0 = MAX(cells_out->c0, 0); + cells_out->r1 = MIN(cells_out->r1, cells_out->r0 + this->nrows); + cells_out->c1 = MIN(cells_out->c1, cells_out->c0 + this->ncols); + + cells_out->r_off = MAX((int)vadjustment->value, 0); +} + + static gboolean gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) { + struct cells cells; CharTable *this = GCMAP_CHAR_TABLE(widget); GdkRectangle area = event->area; - int r0 = MAX(area.y * this->nrows / this->viewport_h, 0); - int c0 = MAX(area.x * this->ncols / this->viewport_w, 0); - int r1 = MIN((area.y + area.height - 1) * this->nrows / this->viewport_h + 1, r0 + this->nrows); - int c1 = MIN((area.x + area.width - 1) * this->ncols / this->viewport_w + 1, c0 + this->ncols); GtkStyle *style = gtk_widget_get_style(widget); cairo_t *g; char text[16]; - int r, c, x0, x1, y0, y1; - int text_w, text_h; - size_t skip, range, row_skip; + int r, c, x0, x1, y0, y1, text_w, text_h; + size_t skip, range, row_skip, max_jump, jump; uint32_t cp; + get_exposed_cells(widget, event, &cells); + g = gdk_cairo_create(widget->window); if (!g) eprintf("gdk_cairo_create:"); - /* Make sure it still looks good even if starting in the middle of row */ - r1 += 1; - /* Configure drawing area */ gdk_cairo_rectangle(g, &area); cairo_clip(g); @@ -129,44 +152,47 @@ gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) set_source_colour(g, &style->base[GTK_STATE_NORMAL]); cairo_paint(g); + r = cells.r0; + c = cells.c0; + if (this->nranges == 0u) + goto out; + /* Draw grid lines */ - set_source_colour_blend(g, &style->base[GTK_STATE_NORMAL], 0.10, &style->text[GTK_STATE_NORMAL]); + set_source_colour_blend(g, &style->base[GTK_STATE_NORMAL], 0.05, &style->text[GTK_STATE_NORMAL]); cairo_set_line_width(g, 1.0); /* horizontal */ - y0 = this->viewport_h * (r0 + 1) / this->nrows; - x0 = this->viewport_w * c0 / this->ncols; - x1 = this->viewport_w * c1 / this->ncols; - for (r = r0 + (area.y > 0); r < r1; r++, y0 = y1) { - y1 = this->viewport_h * (r + 1) / this->nrows; + y0 = (this->viewport_h + 1) * (cells.r0 + 1) / this->nrows; + x0 = (this->viewport_w + 1) * cells.c0 / this->ncols; + x1 = (this->viewport_w + 1) * cells.c1 / this->ncols; + for (r = cells.r0 + (area.y > 0); r < cells.r1; r++, y0 = y1) { + y1 = (this->viewport_h + 1) * (r + 1) / this->nrows; cairo_move_to(g, x0, y0 + 0.5); cairo_line_to(g, x1, y0 + 0.5); cairo_stroke(g); } /* vertical */ - x0 = this->viewport_w * (c0 + 1) / this->ncols; - y0 = this->viewport_h * r0 / this->nrows; - y1 = this->viewport_h * r1 / this->nrows; - for (c = c0 + (area.x > 0); c < c1; c++, x0 = x1) { - x1 = this->viewport_w * (c + 1) / this->ncols; + x0 = (this->viewport_w + 1) * (cells.c0 + 1) / this->ncols; + y0 = (this->viewport_h + 1) * cells.r0 / this->nrows; + y1 = (this->viewport_h + 1) * cells.r1 / this->nrows; + for (c = cells.c0 + (area.x > 0); c < cells.c1; c++, x0 = x1) { + x1 = (this->viewport_w + 1) * (c + 1) / this->ncols; cairo_move_to(g, x0 + 0.5, y0); cairo_line_to(g, x0 + 0.5, y1); cairo_stroke(g); } /* Draw gryphs */ - if (this->nranges == 0u) - goto out; set_source_colour(g, &style->text[GTK_STATE_NORMAL]); - y0 = this->viewport_h * r0 / this->nrows; + y0 = (this->viewport_h + 1) * cells.r0 / this->nrows; range = 0u; cp = this->ranges[range].first; - skip = (size_t)r0 * (size_t)this->ncols + (size_t)c0; - row_skip = (size_t)this->ncols - (size_t)(c1 - c0); - /* TODO scroll makes the lines jump by a pixel */ - for (r = r0; r < r1; r++, y0 = y1, skip = row_skip) { + skip = (size_t)cells.r0 + (size_t)cells.r_off; + skip = skip * (size_t)this->ncols + (size_t)cells.c0; + row_skip = (size_t)this->ncols - (size_t)(cells.c1 - cells.c0); + for (r = cells.r0; r < cells.r1; r++, y0 = y1, skip = row_skip) { while (skip) { - size_t max_jump = (size_t)(this->ranges[range].last - cp + 1u); - size_t jump = MIN(skip, max_jump); + max_jump = (size_t)(this->ranges[range].last - cp + 1u); + jump = MIN(skip, max_jump); cp += (uint32_t)jump; skip -= jump; if (cp > this->ranges[range].last) { @@ -175,15 +201,15 @@ gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) cp = this->ranges[range].first; } } - x0 = this->viewport_w * c0 / this->ncols; - y1 = this->viewport_h * (r + 1) / this->nrows; - for (c = c0; c < c1; c++, cp++, x0 = x1) { + x0 = (this->viewport_w + 1) * cells.c0 / this->ncols; + y1 = (this->viewport_h + 1) * (r + 1) / this->nrows; + for (c = cells.c0; c < cells.c1; c++, cp++, x0 = x1) { if (cp > this->ranges[range].last) { if (++range == this->nranges) goto out; cp = this->ranges[range].first; } - x1 = this->viewport_w * (c + 1) / this->ncols; + x1 = (this->viewport_w + 1) * (c + 1) / this->ncols; format_character((gunichar)cp, text); fit_text(this, text, &text_w, &text_h); @@ -193,40 +219,29 @@ gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) } out: + /* Grey out unused area */ + set_source_colour_blend(g, &style->base[GTK_STATE_NORMAL], 0.05, &style->text[GTK_STATE_NORMAL]); + y0 = (this->viewport_h + 1) * r / this->nrows; + x0 = (this->viewport_w + 1) * c / this->ncols; + y1 = (this->viewport_h + 1) * (r + 1) / this->nrows; + cairo_rectangle(g, x0, y0, (double)this->viewport_w - x0, y1 - y0); + cairo_fill(g); + cairo_rectangle(g, 0.0, y1, (double)this->viewport_w, (double)this->viewport_h - y1); + cairo_fill(g); + cairo_destroy(g); return FALSE; } static void -update_adjustment(CharTable *this) -{ - uint32_t nrows_total; - nrows_total = this->nchars + (uint32_t)this->ncols - 1u; - nrows_total /= (uint32_t)this->nrows; - /* TODO - if (!this->adjustment) - return; - gtk_adjustment_set_upper(this->adjustment, (double)nrows_total); - gtk_adjustment_set_page_increment(this->adjustment, this->nrows); - gtk_adjustment_set_page_size(this->adjustment, this->nrows); - */ -} - - -static int update_cell_size(GtkWidget *widget, int ranges_changed) { CharTable *this = GCMAP_CHAR_TABLE(widget); - int text_w, text_h; - int cell_w, cell_h; + int text_w, text_h, cell_w, cell_h; uint32_t nchars; - int table_h; size_t i; - int old_ncols = this->ncols; - int old_cell_h = this->cell_h; - pango_layout_set_text(this->layout, REFERENCE_CHARACTER, -1); pango_font_description_set_weight(this->font, PANGO_WEIGHT_NORMAL); @@ -237,43 +252,50 @@ update_cell_size(GtkWidget *widget, int ranges_changed) cell_w = MAX(text_w, 1) * CELL_WIDTH_FACTOR; cell_h = MAX(text_h, 1) * CELL_HEIGHT_FACTOR; - this->ncols = MAX(this->viewport_w / cell_w, 1); - this->nrows = MAX(this->viewport_h / cell_h, 1); - cell_w = MAX(this->viewport_w / this->ncols - 1, 1); - cell_h = MAX(this->viewport_h / this->nrows - 1, 1); + this->ncols = (this->viewport_w + 1) / (cell_w + 1); + this->nrows = (this->viewport_h + 1) / (cell_h + 1); + this->ncols = MAX(this->ncols, 1); + this->nrows = MAX(this->nrows, 1); + + cell_w = (this->viewport_w + 1) / this->ncols - 1; + cell_h = (this->viewport_h + 1) / this->nrows - 1; + cell_w = MAX(cell_w, 1); + cell_h = MAX(cell_h, 1); this->max_w = cell_w - CELL_X_CLEARANCE * 2; this->max_h = cell_h - CELL_Y_CLEARANCE * 2; this->max_w = this->max_w < 1 ? cell_w : this->max_w; this->max_h = this->max_h < 1 ? cell_h : this->max_h; this->cell_h = cell_h; - if (old_ncols == this->ncols && old_cell_h == this->cell_h && !ranges_changed) - return 0; - - nchars = (uint32_t)this->nranges; - for (i = 0u; i < this->nranges; i++) - nchars += this->ranges[i].last - this->ranges[i].first; - if (this->nchars != nchars) { + if (ranges_changed) { + nchars = (uint32_t)this->nranges; + for (i = 0u; i < this->nranges; i++) + nchars += this->ranges[i].last - this->ranges[i].first; this->nchars = nchars; - update_adjustment(this); } - table_h = (int)(nchars / (uint32_t)this->ncols); - if (nchars % (uint32_t)this->ncols) - table_h += 1; - table_h = table_h > INT_MAX / this->cell_h ? INT_MAX : table_h * this->cell_h; - if (this->table_h == table_h) - return 0; - this->table_h = table_h; - return 1; +} + + +static void +update_content_size(CharTable *this) +{ + GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(this)); + int nrows; + if (parent && LOGICAL_IS_SCROLL_PANE(parent)) { + nrows = (int)(this->nchars / (uint32_t)this->ncols); + nrows += this->nchars % (uint32_t)this->ncols > 0u; + logical_scroll_pane_set_content_size(LOGICAL_SCROLL_PANE(parent), this->ncols, nrows); + logical_scroll_pane_set_viewport_size(LOGICAL_SCROLL_PANE(parent), this->ncols, this->nrows); + } } static void gcmap_char_table_size_request(GtkWidget *widget, GtkRequisition *requisition) { - CharTable *this = GCMAP_CHAR_TABLE(widget); + (void) widget; requisition->width = 1; - requisition->height = this->table_h; + requisition->height = 1; } static gboolean @@ -294,7 +316,6 @@ gcmap_char_table_init(CharTable *this) this->max_w = 1; this->max_h = 1; this->cell_h = 1; - this->table_h = 1; this->nchars = 0u; this->ranges = NULL; this->nranges = 0u; @@ -311,8 +332,6 @@ gcmap_char_table_init(CharTable *this) g_signal_connect(widget, "button-press-event", G_CALLBACK(&gcmap_char_table_button_press), NULL); - - /* TODO scrolling with control held should change font size (attempt to keep hover glyph in place) */ } @@ -352,7 +371,8 @@ gcmap_char_table_set_font_family(CharTable *this, const char *family) pango_font_description_set_family(this->font, family); pango_layout_set_font_description(this->layout, this->font); pango_layout_context_changed(this->layout); - gtk_widget_queue_resize(GTK_WIDGET(this)); + update_cell_size(GTK_WIDGET(this), 0); + update_content_size(this); gtk_widget_queue_draw(GTK_WIDGET(this)); } @@ -394,7 +414,8 @@ gcmap_char_table_set_font_size(CharTable *this, int size) pango_layout_set_font_description(this->layout, this->font); pango_layout_context_changed(this->layout); widget = GTK_WIDGET(this); - gtk_widget_queue_resize(widget); + update_cell_size(widget, 0); + update_content_size(this); gtk_widget_queue_draw(widget); } @@ -408,8 +429,8 @@ gcmap_char_table_set_ranges(CharTable *this, const struct libcmap_range *ranges, this->ranges = ranges; this->nranges = nranges; widget = GTK_WIDGET(this); - if (update_cell_size(widget, 1)) - gtk_widget_queue_resize(widget); + update_cell_size(widget, 1); + update_content_size(this); gtk_widget_queue_draw(widget); } @@ -420,6 +441,7 @@ gcmap_char_table_viewport_updated(CharTable *this, int viewport_w, int viewport_ this->viewport_w = viewport_w; this->viewport_h = viewport_h; update_cell_size(GTK_WIDGET(this), 0); + update_content_size(this); } diff --git a/char-table.h b/char-table.h index 33999b2..1663c9c 100644 --- a/char-table.h +++ b/char-table.h @@ -14,7 +14,7 @@ typedef struct _CharTable { GtkDrawingArea parent_instance; int nrows, ncols; int max_w, max_h; - int cell_h, table_h; + int cell_h; uint32_t nchars; int viewport_w, viewport_h; PangoFontDescription *font; diff --git a/common.h b/common.h index a30d4c7..f54e8ed 100644 --- a/common.h +++ b/common.h @@ -25,9 +25,16 @@ # define HAVE_ITEM_TOOLTIPS #endif +#if defined(__GNUC__) +# define PURE __attribute__((__pure__)) +#else +# define PURE +#endif + #define COMMA , #include "char-table.h" +#include "logical-scroll-pane.h" /* colour.c */ double srgb_encode_unsigned(double t); diff --git a/gcmap.c b/gcmap.c index ac82d0d..665229c 100644 --- a/gcmap.c +++ b/gcmap.c @@ -728,17 +728,17 @@ vtabtext_changed(GtkWidget *menu_item, void *user_data) static void -chartable_viewport_size_allocate(GtkWidget *viewport, GtkAllocation *allocation, void *user_data) { +chartable_size_allocate(GtkWidget *widget, GtkAllocation *allocation, void *user_data) +{ (void) user_data; - (void) viewport; - gcmap_char_table_viewport_updated(GCMAP_CHAR_TABLE(chartable), allocation->width, allocation->height); + gcmap_char_table_viewport_updated(GCMAP_CHAR_TABLE(widget), allocation->width, allocation->height); } static void create_window(const char *default_font, PangoWeight default_weight, PangoStyle default_style) { - GtkWidget *vbox, *hbox, *left, *scrolled, *viewport; + GtkWidget *vbox, *hbox, *left, *scrolled; GtkWidget *label, *copybutton, *align, *menubar, *menu; GtkListStore *store; GtkCellRenderer *renderer; @@ -877,21 +877,18 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d gtk_notebook_set_tab_pos(GTK_NOTEBOOK(notebook), GTK_POS_TOP); /* TODO character map tab pane (should have focus: gtk_widget_grab_focus(GTK_WIDGET(...)) */ - scrolled = gtk_scrolled_window_new(NULL, NULL); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); - gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE); + scrolled = logical_scroll_pane_new(); + logical_scroll_pane_set_hscrollbar_visible(LOGICAL_SCROLL_PANE(scrolled), FALSE); chartable = gcmap_char_table_new(default_font, (int)default_font_size, default_weight, default_style); - gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrolled), chartable); - viewport = gtk_bin_get_child(GTK_BIN(scrolled)); - g_signal_connect(viewport, "size-allocate", G_CALLBACK(&chartable_viewport_size_allocate), NULL); - /* TODO scroll shall snap to beginning of a row */ + logical_scroll_pane_set_child(LOGICAL_SCROLL_PANE(scrolled), chartable); + g_signal_connect(chartable, "size-allocate", G_CALLBACK(&chartable_size_allocate), NULL); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Character _table")); #ifdef TODO scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE); - gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); // gtk_scrolled_window_add_with_viewport + gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Character _details")); #endif @@ -899,7 +896,7 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE); - gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); // gtk_scrolled_window_add_with_viewport + gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Glyph v_ariants")); #endif @@ -907,7 +904,7 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE); - gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); // gtk_scrolled_window_add_with_viewport + gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("TODO")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Font c_omparison")); #endif @@ -1009,9 +1006,6 @@ main(int argc, char *argv[]) { GtkIconTheme *theme; const char *icon; - int xargc = 1; - char *xargva[] = {argv[0], NULL}; - char **xargv = xargva; enum listing listing = (enum listing)0; int use_bold = 0; int use_italic = 0; @@ -1022,10 +1016,10 @@ main(int argc, char *argv[]) ARGBEGIN { case 'B': - listing = 1; + listing = BY_BLOCK; break; case 'S': - listing = 0; + listing = BY_SCRIPT; break; case 'b': use_bold = 1; @@ -1077,7 +1071,7 @@ main(int argc, char *argv[]) g_set_prgname(argv0); g_set_application_name("gcmap"); - gtk_init(&xargc, &xargv); + gtk_init(NULL, NULL); theme = gtk_icon_theme_get_default(); if (!gtk_icon_theme_has_icon(theme, icon = "apps/gcmap") && !gtk_icon_theme_has_icon(theme, icon = "gcmap") && diff --git a/gcmap.desktop b/gcmap.desktop new file mode 100644 index 0000000..e11c21d --- /dev/null +++ b/gcmap.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=gcmap +Comment=Find and insert unusual characters +Exec=gcmap %F +Icon=accessories-character-map +Terminal=false +Categories=Accessories;Utility; diff --git a/logical-scroll-pane.c b/logical-scroll-pane.c new file mode 100644 index 0000000..207d34e --- /dev/null +++ b/logical-scroll-pane.c @@ -0,0 +1,579 @@ +#include "common.h" + +G_DEFINE_TYPE(LogicalScrollPane, logical_scroll_pane, GTK_TYPE_BIN) + +static void +logical_scroll_pane_adjustment_value_changed(GtkAdjustment *adjustment, LogicalScrollPane *this) +{ +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + + double value; + if (this->viewport_size_is_logical) { + value = round(adjustment->value); + if (value != adjustment->value) { + gtk_adjustment_set_value(adjustment, value); + return; + } + } + if (this->child) + gtk_widget_queue_draw(this->child); + +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif +} + +static void +logical_scroll_pane_configure_adjustment(LogicalScrollPane *this, GtkAdjustment *adjustment, int content_size, int page_size) +{ + double value, upper, pg; + page_size = MAX(page_size, 1); + pg = (double)page_size; + upper = (double)MAX(content_size - page_size, 0); + value = this->viewport_size_is_logical ? round(adjustment->value) : adjustment->value; + value = MAX(value, 0.0); + value = MIN(value, upper); + upper = (double)MAX(content_size, page_size); + if (this->viewport_size_is_logical) + gtk_adjustment_configure(adjustment, value, 0.0, upper, 1.0, pg, pg); + else + gtk_adjustment_configure(adjustment, value, 0.0, upper, 0.1 * pg, 0.9 * pg, pg); +} + +static void +logical_scroll_pane_configure_hadjustment(LogicalScrollPane *this) +{ + logical_scroll_pane_configure_adjustment(this, this->hadjustment, this->content_width, this->page_width); +} + +static void +logical_scroll_pane_configure_vadjustment(LogicalScrollPane *this) +{ + logical_scroll_pane_configure_adjustment(this, this->vadjustment, this->content_height, this->page_height); +} + +static void +logical_scroll_pane_configure_adjustments(LogicalScrollPane *this) +{ + logical_scroll_pane_configure_hadjustment(this); + logical_scroll_pane_configure_vadjustment(this); +} + +static void +logical_scroll_pane_destroy(GtkObject *this_) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (this->child) { + g_signal_handlers_disconnect_by_data(this->child, this); + gtk_widget_unparent(this->child); + } + if (this->hscrollbar) gtk_widget_unparent(this->hscrollbar); + if (this->vscrollbar) gtk_widget_unparent(this->vscrollbar); + if (this->hadjustment) g_object_unref(this->hadjustment); + if (this->vadjustment) g_object_unref(this->vadjustment); + + this->child = NULL; + this->hscrollbar = NULL; + this->vscrollbar = NULL; + this->hadjustment = NULL; + this->vadjustment = NULL; + + GTK_OBJECT_CLASS(logical_scroll_pane_parent_class)->destroy(this_); +} + +static void +logical_scroll_pane_size_request(GtkWidget *this_, GtkRequisition *requisition) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + GtkRequisition hreq; + GtkRequisition vreq; + + requisition->width = hreq.width = vreq.width = 0; + requisition->height = hreq.height = vreq.height = 0; + + if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq); + if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq); + + if (this->vscrollbar_visible) requisition->width += vreq.width; + if (this->hscrollbar_visible) requisition->height += hreq.height; + + requisition->width = MAX(requisition->width, hreq.width); + requisition->height = MAX(requisition->height, vreq.height); +} + +static void +logical_scroll_pane_size_allocate(GtkWidget *this_, GtkAllocation *allocation) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + GtkRequisition hreq; + GtkRequisition vreq; + + GtkAllocation child_allocation; + GtkAllocation hscrollbar_allocation; + GtkAllocation vscrollbar_allocation; + + int content_width; + int content_height; + double x, y, max; + + this_->allocation = *allocation; + + x = this->hadjustment->value + 0.5 * this->hadjustment->page_size; + y = this->vadjustment->value + 0.5 * this->vadjustment->page_size; + + hreq.width = hreq.height = 0; + vreq.width = vreq.height = 0; + + if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq); + if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq); + + content_width = allocation->width; + if (this->vscrollbar_visible) + content_width -= vreq.width; + content_width = MAX(content_width, 1); + + content_height = allocation->height; + if (this->hscrollbar_visible) + content_height -= hreq.height; + content_height = MAX(content_height, 1); + + if (!this->viewport_size_is_logical) { + this->page_width = content_width; + this->page_height = content_height; + } + logical_scroll_pane_configure_adjustments(this); + + max = this->hadjustment->upper - this->hadjustment->page_size; + x -= 0.5 * this->hadjustment->page_size; + x = MIN(MAX(this->hadjustment->lower, x), max); + gtk_adjustment_set_value(this->hadjustment, x); + + max = this->vadjustment->upper - this->vadjustment->page_size; + y -= 0.5 * this->vadjustment->page_size; + y = MIN(MAX(this->vadjustment->lower, y), max); + gtk_adjustment_set_value(this->vadjustment, y); + + if (this->child && GTK_WIDGET_VISIBLE(this->child)) { + child_allocation.x = allocation->x; + child_allocation.y = allocation->y; + child_allocation.width = content_width; + child_allocation.height = content_height; + gtk_widget_size_allocate(this->child, &child_allocation); + } + + if (this->hscrollbar && this->hscrollbar_visible) { + hscrollbar_allocation.x = allocation->x; + hscrollbar_allocation.y = allocation->y + content_height; + hscrollbar_allocation.width = content_width; + hscrollbar_allocation.height = hreq.height; + gtk_widget_size_allocate(this->hscrollbar, &hscrollbar_allocation); + } + + if (this->vscrollbar && this->vscrollbar_visible) { + vscrollbar_allocation.x = allocation->x + content_width; + vscrollbar_allocation.y = allocation->y; + vscrollbar_allocation.width = vreq.width; + vscrollbar_allocation.height = content_height; + gtk_widget_size_allocate(this->vscrollbar, &vscrollbar_allocation); + } +} + +static void +logical_scroll_pane_scroll_adjustment(GtkAdjustment *adjustment, double amount) +{ + double value = adjustment->value; + double lower = adjustment->lower; + double upper = adjustment->upper - adjustment->page_size; + double new_value = value + amount; + if (amount < 0.0 && value > lower) + gtk_adjustment_set_value(adjustment, MAX(new_value, lower)); + else if (amount > 0.0 && value < upper) + gtk_adjustment_set_value(adjustment, MIN(new_value, upper)); +} + +static gboolean +logical_scroll_pane_child_scroll(GtkWidget *widget, GdkEventScroll *event, LogicalScrollPane *this) +{ + GtkAdjustment *adjustment = this->vadjustment; + + (void) widget; + + switch (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_META_MASK)) { + case 0: + switch (event->direction) { + case GDK_SCROLL_LEFT: + case GDK_SCROLL_RIGHT: + goto horizontal; + case GDK_SCROLL_UP: + case GDK_SCROLL_DOWN: + default: + break; + } + break; + case GDK_SHIFT_MASK: + horizontal: + adjustment = this->hadjustment; + break; + default: + return FALSE; + } + + switch (event->direction) { + case GDK_SCROLL_UP: + case GDK_SCROLL_LEFT: + logical_scroll_pane_scroll_adjustment(adjustment, -adjustment->step_increment); + return TRUE; + case GDK_SCROLL_DOWN: + case GDK_SCROLL_RIGHT: + logical_scroll_pane_scroll_adjustment(adjustment, +adjustment->step_increment); + return TRUE; + default: + return FALSE; + } +} + +static gboolean +logical_scroll_pane_child_key_press(GtkWidget *widget, GdkEventKey *event, LogicalScrollPane *this) +{ + GtkAdjustment *adjustment = this->vadjustment; + + (void) widget; + + switch (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_META_MASK)) { + case 0: + break; + case GDK_SHIFT_MASK: + adjustment = this->hadjustment; + break; + default: + return FALSE; + } + + switch (event->keyval) { + case GDK_Page_Up: + case GDK_KP_Page_Up: + logical_scroll_pane_scroll_adjustment(adjustment, -adjustment->page_increment); + return TRUE; + case GDK_Page_Down: + case GDK_KP_Page_Down: + logical_scroll_pane_scroll_adjustment(adjustment, +adjustment->page_increment); + return TRUE; + default: + return FALSE; + } +} + +static void +logical_scroll_pane_add(GtkContainer *this_, GtkWidget *widget) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + if (this->child) { + g_warning("LogicalScrollPane already has a child"); + return; + } + logical_scroll_pane_set_child(this, widget); +} + +static void +logical_scroll_pane_remove(GtkContainer *this_, GtkWidget *widget) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (widget == this->child) { + g_signal_handlers_disconnect_by_data(widget, this); + this->child = NULL; + GTK_BIN(this)->child = NULL; + } else if (widget == this->hscrollbar) { + this->hscrollbar = NULL; + } else if (widget == this->vscrollbar) { + this->vscrollbar = NULL; + } else { + g_warning("Widget is not a child of LogicalScrollPane"); + return; + } + + gtk_widget_unparent(widget); + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +static void +logical_scroll_pane_forall(GtkContainer *this_, int include_internals, GtkCallback callback, void *callback_data) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (this->child) (*callback)(this->child, callback_data); + + if (!include_internals) + return; + + if (this->hscrollbar) (*callback)(this->hscrollbar, callback_data); + if (this->vscrollbar) (*callback)(this->vscrollbar, callback_data); +} + +static void +set_adjustment(LogicalScrollPane *this, GtkAdjustment *adjustment, GtkAdjustment **slot, GtkWidget *scrollbar) +{ + if (!adjustment) + adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0)); + + if (*slot != adjustment) { + g_object_ref_sink(adjustment); + + if (*slot) { + g_signal_handlers_disconnect_by_data(*slot, this); + g_object_unref(*slot); + } + + *slot = adjustment; + g_signal_connect(*slot, "value-changed", + G_CALLBACK(&logical_scroll_pane_adjustment_value_changed), this); + } + + if (scrollbar) + gtk_range_set_adjustment(GTK_RANGE(scrollbar), adjustment); +} + +static void +logical_scroll_pane_class_init(LogicalScrollPaneClass *class) +{ + GtkObjectClass *object_class = GTK_OBJECT_CLASS(class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS(class); + + object_class->destroy = &logical_scroll_pane_destroy; + + widget_class->size_request = &logical_scroll_pane_size_request; + widget_class->size_allocate = &logical_scroll_pane_size_allocate; + + container_class->add = &logical_scroll_pane_add; + container_class->remove = &logical_scroll_pane_remove; + container_class->forall = &logical_scroll_pane_forall; +} + +static void +logical_scroll_pane_init(LogicalScrollPane *this) +{ + this->child = NULL; + this->content_width = 1; + this->content_height = 1; + this->page_width = 1; + this->page_height = 1; + this->viewport_size_is_logical = 0; + this->hscrollbar_visible = 1; + this->vscrollbar_visible = 1; + + this->hadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, /* value */ + 0.0, /* lower */ + 0.0, /* upper */ + 1.0, /* step increment */ + 1.0, /* page increment */ + 0.0)); /* page size */ + + this->vadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0)); + + g_object_ref_sink(this->hadjustment); + g_object_ref_sink(this->vadjustment); + g_signal_connect(this->hadjustment, "value-changed", + G_CALLBACK(&logical_scroll_pane_adjustment_value_changed), this); + g_signal_connect(this->vadjustment, "value-changed", + G_CALLBACK(&logical_scroll_pane_adjustment_value_changed), this); + + this->hscrollbar = gtk_hscrollbar_new(this->hadjustment); + this->vscrollbar = gtk_vscrollbar_new(this->vadjustment); + + gtk_widget_set_parent(this->hscrollbar, GTK_WIDGET(this)); + gtk_widget_set_parent(this->vscrollbar, GTK_WIDGET(this)); + + gtk_widget_show(this->hscrollbar); + gtk_widget_show(this->vscrollbar); +} + +GtkWidget * +logical_scroll_pane_new(void) +{ + return GTK_WIDGET(g_object_new(LOGICAL_TYPE_SCROLL_PANE, NULL)); +} + +void +logical_scroll_pane_set_child(LogicalScrollPane *this, GtkWidget *child) +{ + if (child == this->child) + return; + + if (this->child) { + g_signal_handlers_disconnect_by_data(this->child, this); + gtk_widget_unparent(this->child); + this->child = NULL; + GTK_BIN(this)->child = NULL; + } + + if (child) { + this->child = child; + GTK_BIN(this)->child = child; + gtk_widget_add_events(child, GDK_SCROLL_MASK | GDK_KEY_PRESS_MASK); + g_signal_connect(child, "scroll-event", G_CALLBACK(&logical_scroll_pane_child_scroll), this); + g_signal_connect(child, "key-press-event", G_CALLBACK(&logical_scroll_pane_child_key_press), this); + gtk_widget_set_parent(child, GTK_WIDGET(this)); + if (GTK_WIDGET_VISIBLE(this)) + gtk_widget_show(child); + } + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +GtkWidget * +logical_scroll_pane_get_child(LogicalScrollPane *this) +{ + return this->child; +} + +void +logical_scroll_pane_set_content_size(LogicalScrollPane *this, int width, int height) +{ + width = MAX(1, width); + height = MAX(1, height); + if (this->content_width == width && this->content_height == height) + return; + + this->content_width = width; + this->content_height = height; + logical_scroll_pane_configure_adjustments(this); + gtk_widget_queue_resize(GTK_WIDGET(this)); + if (this->child) + gtk_widget_queue_draw(this->child); +} + +void +logical_scroll_pane_set_viewport_size(LogicalScrollPane *this, int width, int height) +{ + width = MAX(1, width); + height = MAX(1, height); + this->viewport_size_is_logical = 1; + if (this->page_width == width && this->page_height == height) + return; + + this->page_width = width; + this->page_height = height; + logical_scroll_pane_configure_adjustments(this); + if (this->child) + gtk_widget_queue_draw(this->child); +} + +static void +set_scrollbar(LogicalScrollPane *this, GtkWidget *scrollbar, GtkWidget **slot, int *scrollbar_visible) +{ + if (scrollbar == *slot) + return; + + if (scrollbar) { + if (*slot) + gtk_widget_unparent(*slot); + *slot = scrollbar; + gtk_widget_set_parent(scrollbar, GTK_WIDGET(this)); + + if (*scrollbar_visible) + gtk_widget_show(scrollbar); + } else { + if (*slot) { + gtk_widget_unparent(*slot); + *slot = NULL; + } + } + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +void +logical_scroll_pane_set_hscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar) +{ + set_scrollbar(this, scrollbar, &this->hscrollbar, &this->hscrollbar_visible); +} + +void +logical_scroll_pane_set_vscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar) +{ + set_scrollbar(this, scrollbar, &this->vscrollbar, &this->vscrollbar_visible); +} + +GtkWidget * +logical_scroll_pane_get_hscrollbar(LogicalScrollPane *this) +{ + return this->hscrollbar; +} + +GtkWidget * +logical_scroll_pane_get_vscrollbar(LogicalScrollPane *this) +{ + return this->vscrollbar; +} + +static void +set_scrollbar_visible(LogicalScrollPane *this, int visible, GtkWidget **scrollbar, int *scrollbar_visible) +{ + visible = visible != 0; + + if (*scrollbar_visible == visible) + return; + + *scrollbar_visible = visible; + + if (*scrollbar) + gtk_widget_set_visible(*scrollbar, visible); + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +void +logical_scroll_pane_set_hscrollbar_visible(LogicalScrollPane *this, int visible) +{ + set_scrollbar_visible(this, visible, &this->hscrollbar, &this->hscrollbar_visible); +} + +void +logical_scroll_pane_set_vscrollbar_visible(LogicalScrollPane *this, int visible) +{ + set_scrollbar_visible(this, visible, &this->vscrollbar, &this->vscrollbar_visible); +} + +int +logical_scroll_pane_get_hscrollbar_visible(LogicalScrollPane *this) +{ + return this->hscrollbar_visible; +} + +int +logical_scroll_pane_get_vscrollbar_visible(LogicalScrollPane *this) +{ + return this->vscrollbar_visible; +} + +void +logical_scroll_pane_set_hadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment) +{ + set_adjustment(this, adjustment, &this->hadjustment, this->hscrollbar); + logical_scroll_pane_configure_hadjustment(this); +} + +void +logical_scroll_pane_set_vadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment) +{ + set_adjustment(this, adjustment, &this->vadjustment, this->vscrollbar); + logical_scroll_pane_configure_vadjustment(this); +} + +GtkAdjustment * +logical_scroll_pane_get_hadjustment(LogicalScrollPane *this) +{ + return this->hadjustment; +} + +GtkAdjustment * +logical_scroll_pane_get_vadjustment(LogicalScrollPane *this) +{ + return this->vadjustment; +} diff --git a/logical-scroll-pane.h b/logical-scroll-pane.h new file mode 100644 index 0000000..7401d94 --- /dev/null +++ b/logical-scroll-pane.h @@ -0,0 +1,59 @@ +#include + +#define LOGICAL_TYPE_SCROLL_PANE (logical_scroll_pane_get_type()) +#define LOGICAL_SCROLL_PANE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LOGICAL_TYPE_SCROLL_PANE, LogicalScrollPane)) +#define LOGICAL_IS_SCROLL_PANE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), LOGICAL_TYPE_SCROLL_PANE)) + +typedef struct _LogicalScrollPane LogicalScrollPane; +typedef struct _LogicalScrollPaneClass LogicalScrollPaneClass; + +struct _LogicalScrollPane { + GtkBin parent_instance; + + GtkWidget *child; + GtkWidget *hscrollbar; + GtkWidget *vscrollbar; + + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; + + int content_width; + int content_height; + int page_width; + int page_height; + int viewport_size_is_logical; + int hscrollbar_visible; + int vscrollbar_visible; +}; + +struct _LogicalScrollPaneClass { + GtkBinClass parent_class; +}; + +PURE GType logical_scroll_pane_get_type(void); + +GtkWidget *logical_scroll_pane_new(void); + +void logical_scroll_pane_set_child(LogicalScrollPane *this, GtkWidget *child); +PURE GtkWidget *logical_scroll_pane_get_child(LogicalScrollPane *this); + +void logical_scroll_pane_set_content_size(LogicalScrollPane *this, int width, int height); +void logical_scroll_pane_set_viewport_size(LogicalScrollPane *this, int width, int height); + +void logical_scroll_pane_set_hscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar); +void logical_scroll_pane_set_vscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar); + +PURE GtkWidget *logical_scroll_pane_get_hscrollbar(LogicalScrollPane *this); +PURE GtkWidget *logical_scroll_pane_get_vscrollbar(LogicalScrollPane *this); + +void logical_scroll_pane_set_hscrollbar_visible(LogicalScrollPane *this, int visible); +void logical_scroll_pane_set_vscrollbar_visible(LogicalScrollPane *this, int visible); + +PURE int logical_scroll_pane_get_hscrollbar_visible(LogicalScrollPane *this); +PURE int logical_scroll_pane_get_vscrollbar_visible(LogicalScrollPane *this); + +void logical_scroll_pane_set_hadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment); +void logical_scroll_pane_set_vadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment); + +PURE GtkAdjustment *logical_scroll_pane_get_hadjustment(LogicalScrollPane *this); +PURE GtkAdjustment *logical_scroll_pane_get_vadjustment(LogicalScrollPane *this); -- cgit v1.3.1