sdgtk: add and use a custom listview widget

Nothing in GTK+ appears to be suited for what are virtually infinite
lists.  Our workaround with GtkLabel and GtkScrolledWindow has been
heavily suboptimal and needs to be replaced.

Use Pango directly to handle our relatively simple needs.

Upgrades:
 - the widget can be scrolled,
 - keywords are repeated for each definition line,
 - definition lines are now wrapped, and support 'g' and 'x' fields.

Downgrades:
 - text can no longer be selected, so far.
This commit is contained in:
Přemysl Eric Janouch 2021-07-09 20:18:01 +02:00
parent f812fae922
commit 9d7bc2a839
Signed by: p
GPG Key ID: A0420B94F92B9493
6 changed files with 585 additions and 130 deletions

View File

@ -157,7 +157,9 @@ target_link_libraries (${PROJECT_NAME} ${project_common_libraries})
pkg_check_modules (gtk gtk+-3.0) pkg_check_modules (gtk gtk+-3.0)
if (gtk_FOUND) if (gtk_FOUND)
add_executable (sdgtk EXCLUDE_FROM_ALL add_executable (sdgtk EXCLUDE_FROM_ALL
src/sdgtk.c ${project_common_sources}) src/sdgtk.c
src/stardict-view.c
${project_common_sources})
target_include_directories (sdgtk PUBLIC ${gtk_INCLUDE_DIRS}) target_include_directories (sdgtk PUBLIC ${gtk_INCLUDE_DIRS})
target_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries}) target_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries})
endif () endif ()

View File

@ -1,7 +1,7 @@
/* /*
* StarDict GTK+ UI * StarDict GTK+ UI
* *
* Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name> * Copyright (c) 2020 - 2021, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@ -24,6 +24,7 @@
#include "config.h" #include "config.h"
#include "stardict.h" #include "stardict.h"
#include "utils.h" #include "utils.h"
#include "stardict-view.h"
typedef struct dictionary Dictionary; typedef struct dictionary Dictionary;
@ -40,7 +41,7 @@ static struct
GtkWidget *window; ///< Top-level window GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *entry; ///< Search entry widget GtkWidget *entry; ///< Search entry widget
GtkWidget *grid; ///< Entries container GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary gint dictionary; ///< Index of the current dictionary
Dictionary *dictionaries; ///< All open dictionaries Dictionary *dictionaries; ///< All open dictionaries
@ -85,99 +86,6 @@ init (gchar **filenames, GError **e)
return TRUE; return TRUE;
} }
static void
add_row (StardictIterator *iterator, gint row, gint *height_acc)
{
Dictionary *dict = &g.dictionaries[g.dictionary];
StardictEntry *entry = stardict_iterator_get_entry (iterator);
g_return_if_fail (entry != NULL);
StardictEntryField *field = entry->fields->data;
g_return_if_fail (g_ascii_islower (field->type));
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
g_return_if_fail (input_utf8 != NULL);
const gchar *word_str = stardict_iterator_get_word (iterator);
gsize common_prefix = stardict_longest_common_collation_prefix
(dict->dict, word_str, input_utf8);
gchar *pre = g_markup_escape_text (word_str, common_prefix),
*post = g_markup_escape_text (word_str + common_prefix, -1),
*marked_up = g_strdup_printf ("<u>%s</u>%s", pre, post);
GtkWidget *word = gtk_label_new (marked_up);
gtk_label_set_use_markup (GTK_LABEL (word), TRUE);
gtk_label_set_ellipsize (GTK_LABEL (word), PANGO_ELLIPSIZE_END);
gtk_label_set_selectable (GTK_LABEL (word), TRUE);
gtk_label_set_xalign (GTK_LABEL (word), 0);
gtk_label_set_yalign (GTK_LABEL (word), 0);
// FIXME: they can't be deselected by just clicking outside of them
gtk_widget_set_can_focus (word, FALSE);
g_free (pre);
g_free (post);
g_free (marked_up);
GtkWidget *desc = gtk_label_new (field->data);
gtk_label_set_ellipsize (GTK_LABEL (desc), PANGO_ELLIPSIZE_END);
gtk_label_set_selectable (GTK_LABEL (desc), TRUE);
gtk_label_set_xalign (GTK_LABEL (desc), 0);
gtk_widget_set_can_focus (desc, FALSE);
g_object_unref (entry);
if (iterator->offset % 2 == 0)
{
GtkStyleContext *ctx;
ctx = gtk_widget_get_style_context (word);
gtk_style_context_add_class (ctx, "odd");
ctx = gtk_widget_get_style_context (desc);
gtk_style_context_add_class (ctx, "odd");
}
gtk_grid_attach (GTK_GRID (g.grid), word, 0, row, 1, 1);
gtk_grid_attach (GTK_GRID (g.grid), desc, 1, row, 1, 1);
gtk_widget_show (word);
gtk_widget_show (desc);
gint minimum_word = 0, minimum_desc = 0;
gtk_widget_get_preferred_height (word, &minimum_word, NULL);
gtk_widget_get_preferred_height (desc, &minimum_desc, NULL);
*height_acc += MAX (minimum_word, minimum_desc);
}
static void
reload (GtkWidget *grid)
{
Dictionary *dict = &g.dictionaries[g.dictionary];
GList *children = gtk_container_get_children (GTK_CONTAINER (grid));
for (GList *iter = children; iter != NULL; iter = g_list_next (iter))
gtk_widget_destroy (GTK_WIDGET (iter->data));
g_list_free (children);
gint window_height = 0;
gtk_window_get_size (GTK_WINDOW (g.window), NULL, &window_height);
if (window_height <= 0)
return;
StardictIterator *iterator =
stardict_iterator_new (dict->dict, dict->position);
gint row = 0, height_acc = 0;
while (stardict_iterator_is_valid (iterator))
{
add_row (iterator, row++, &height_acc);
if (height_acc >= window_height)
break;
stardict_iterator_next (iterator);
}
gtk_widget_show_all (grid);
g_object_unref (iterator);
}
static void static void
search (Dictionary *dict) search (Dictionary *dict)
{ {
@ -188,13 +96,16 @@ search (Dictionary *dict)
stardict_dict_search (dict->dict, input_utf8, NULL); stardict_dict_search (dict->dict, input_utf8, NULL);
dict->position = stardict_iterator_get_offset (iterator); dict->position = stardict_iterator_get_offset (iterator);
g_object_unref (iterator); g_object_unref (iterator);
stardict_view_set_position (STARDICT_VIEW (g.view),
dict->dict, dict->position);
stardict_view_set_matched (STARDICT_VIEW (g.view), input_utf8);
} }
static void static void
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data) on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{ {
search (&g.dictionaries[g.dictionary]); search (&g.dictionaries[g.dictionary]);
reload (g.grid);
} }
static void static void
@ -231,7 +142,6 @@ on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
{ {
g.dictionary = page_num; g.dictionary = page_num;
search (&g.dictionaries[g.dictionary]); search (&g.dictionaries[g.dictionary]);
reload (g.grid);
} }
static gboolean static gboolean
@ -317,12 +227,8 @@ main (int argc, char *argv[])
if (!init (filenames, &error)) if (!init (filenames, &error))
die_with_dialog (error->message); die_with_dialog (error->message);
// Some Adwaita stupidity and our own additions // Some Adwaita stupidity
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }" const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }";
"grid { border-top: 1px solid rgba(0, 0, 0, 0.2); background: white; }"
"grid label { padding: 0 5px; "
"/*border-bottom: 1px solid rgba(0, 0, 0, 0.2);*/ }"
"grid label.odd { background: rgba(0, 0, 0, 0.05); }";
GdkScreen *screen = gdk_screen_get_default (); GdkScreen *screen = gdk_screen_get_default ();
GtkCssProvider *provider = gtk_css_provider_new (); GtkCssProvider *provider = gtk_css_provider_new ();
@ -330,20 +236,6 @@ main (int argc, char *argv[])
gtk_style_context_add_provider_for_screen (screen, gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g.grid = gtk_grid_new ();
gtk_grid_set_column_homogeneous (GTK_GRID (g.grid), TRUE);
// FIXME: we'd rather like to trim the contents, not make it scrollable.
// This just limits the allocation.
// TODO: probably create a whole new custom widget, everything is text
// anyway and mostly handled by Pango, including pango_layout_xy_to_index()
// - I don't know where to get selection colour but inversion works, too
GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL);
gtk_widget_set_can_focus (scrolled_window, FALSE);
gtk_container_add (GTK_CONTAINER (scrolled_window), g.grid);
g.notebook = gtk_notebook_new (); g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page", g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL); G_CALLBACK (on_switch_page), NULL);
@ -375,21 +267,28 @@ main (int argc, char *argv[])
// TODO: attach to the "key-press-event" signal and implement ^W at least, // TODO: attach to the "key-press-event" signal and implement ^W at least,
// though ^U is working already! Note that bindings can be done in CSS // though ^U is working already! Note that bindings can be done in CSS
// as well, if we have any extra specially for the editor // as well, if we have any extra specially for the editor
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.grid); g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view);
// TODO: make the entry have a background colour, rather than transparency
gtk_entry_set_has_frame (GTK_ENTRY (g.entry), FALSE); gtk_entry_set_has_frame (GTK_ENTRY (g.entry), FALSE);
// TODO: supposedly attach to "key-press-event" here and react to // TODO: supposedly attach to "key-press-event" here and react to
// PageUp/PageDown and up/down arrow keys... either here or in the Entry // PageUp/PageDown and up/down arrow keys... either here or in the Entry
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL); g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_default_size (GTK_WINDOW (g.window), 300, 600);
g_signal_connect (g.window, "destroy", g_signal_connect (g.window, "destroy",
G_CALLBACK (on_destroy), NULL); G_CALLBACK (on_destroy), NULL);
g_signal_connect (g.window, "key-press-event", g_signal_connect (g.window, "key-press-event",
G_CALLBACK (on_key_press), NULL); G_CALLBACK (on_key_press), NULL);
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1); GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
gtk_container_add (GTK_CONTAINER (g.window), superbox); gtk_container_add (GTK_CONTAINER (g.window), superbox);
gtk_container_add (GTK_CONTAINER (superbox), g.notebook); gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
gtk_container_add (GTK_CONTAINER (superbox), g.entry); gtk_container_add (GTK_CONTAINER (superbox), g.entry);
gtk_box_pack_end (GTK_BOX (superbox), scrolled_window, TRUE, TRUE, 0); gtk_container_add (GTK_CONTAINER (superbox),
gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
g.view = stardict_view_new ();
gtk_box_pack_end (GTK_BOX (superbox), g.view, TRUE, TRUE, 0);
for (gsize i = 0; i < g.dictionaries_len; i++) for (gsize i = 0; i < g.dictionaries_len; i++)
{ {
@ -403,13 +302,6 @@ main (int argc, char *argv[])
g_signal_connect (clipboard, "owner-change", g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL); G_CALLBACK (on_selection), NULL);
// Make sure to fill up the window with entries once we're resized
// XXX: this is rather inefficient as we rebuild everything each time
g_signal_connect (g.window, "configure-event",
G_CALLBACK (on_changed), NULL);
g_signal_connect (g.window, "map-event",
G_CALLBACK (on_changed), NULL);
gtk_widget_grab_focus (g.entry); gtk_widget_grab_focus (g.entry);
gtk_widget_show_all (g.window); gtk_widget_show_all (g.window);
gtk_main (); gtk_main ();

View File

@ -230,7 +230,7 @@ struct application
guint32 top_position; ///< Index of the topmost dict. entry guint32 top_position; ///< Index of the topmost dict. entry
guint top_offset; ///< Offset into the top entry guint top_offset; ///< Offset into the top entry
guint selected; ///< Offset to the selected definition guint selected; ///< Offset to the selected definition
GPtrArray * entries; ///< ViewEntry's within the view GPtrArray * entries; ///< ViewEntry-s within the view
gchar * search_label; ///< Text of the "Search" label gchar * search_label; ///< Text of the "Search" label
GArray * input; ///< The current search input GArray * input; ///< The current search input
@ -386,6 +386,7 @@ view_entry_new (StardictIterator *iterator)
found_anything_displayable = TRUE; found_anything_displayable = TRUE;
break; break;
case STARDICT_FIELD_PHONETIC: case STARDICT_FIELD_PHONETIC:
// FIXME this makes it highlightable
g_string_append_printf (word, " /%s/", (const gchar *) field->data); g_string_append_printf (word, " /%s/", (const gchar *) field->data);
break; break;
default: default:

526
src/stardict-view.c Normal file
View File

@ -0,0 +1,526 @@
/*
* StarDict GTK+ UI - dictionary view component
*
* Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include "stardict-view.h"
#include "utils.h"
typedef struct view_entry ViewEntry;
struct view_entry
{
gchar *word; ///< The word, in Pango markup
gchar *definition; ///< Definition lines, in Pango markup
PangoLayout *word_layout; ///< Ellipsized one-line layout or NULL
PangoLayout *definition_layout; ///< Multiline layout or NULL
};
static void
view_entry_destroy (ViewEntry *self)
{
g_free (self->word);
g_free (self->definition);
g_clear_object (&self->word_layout);
g_clear_object (&self->definition_layout);
g_slice_free1 (sizeof *self, self);
}
static ViewEntry *
view_entry_new (StardictIterator *iterator, const gchar *matched)
{
g_return_val_if_fail (stardict_iterator_is_valid (iterator), NULL);
StardictEntry *entry = stardict_iterator_get_entry (iterator);
g_return_val_if_fail (entry != NULL, NULL);
// Highlighting may change the rendition, so far it's easiest to recompute
// it on each search field change by rebuilding the list of view entries.
// The phonetics suffix would need to be stored separately.
const gchar *word = stardict_iterator_get_word (iterator);
gsize common_prefix = stardict_longest_common_collation_prefix
(iterator->owner, word, matched);
ViewEntry *ve = g_slice_alloc0 (sizeof *ve);
GString *adjusted_word = g_string_new ("");
gchar *pre = g_markup_escape_text (word, common_prefix);
gchar *post = g_markup_escape_text (word + common_prefix, -1);
g_string_printf (adjusted_word, "<u>%s</u>%s", pre, post);
g_free (pre);
g_free (post);
GPtrArray *definitions = g_ptr_array_new_full (2, g_free);
for (const GList *fields = stardict_entry_get_fields (entry); fields; )
{
const StardictEntryField *field = fields->data;
switch (field->type)
{
case STARDICT_FIELD_MEANING:
g_ptr_array_add (definitions,
g_markup_escape_text (field->data, -1));
break;
case STARDICT_FIELD_PANGO:
g_ptr_array_add (definitions, g_strdup (field->data));
break;
case STARDICT_FIELD_XDXF:
g_ptr_array_add (definitions,
xdxf_to_pango_markup_with_reduced_effort (field->data));
break;
case STARDICT_FIELD_PHONETIC:
{
gchar *escaped = g_markup_escape_text (field->data, -1);
g_string_append_printf (adjusted_word, " /%s/", escaped);
g_free (escaped);
break;
}
default:
// TODO: support more of them
break;
}
fields = fields->next;
}
g_object_unref (entry);
ve->word = g_string_free (adjusted_word, FALSE);
if (!definitions->len)
{
gchar *message = g_markup_escape_text (_("no usable field found"), -1);
g_ptr_array_add (definitions, g_strdup_printf ("&lt;%s&gt;", message));
g_free (message);
}
g_ptr_array_add (definitions, NULL);
ve->definition = g_strjoinv ("\n", (gchar **) definitions->pdata);
g_ptr_array_free (definitions, TRUE);
return ve;
}
static gint
view_entry_height (ViewEntry *ve, gint *word_offset, gint *defn_offset)
{
gint word_w = 0, word_h = 0;
gint defn_w = 0, defn_h = 0;
pango_layout_get_pixel_size (ve->word_layout, &word_w, &word_h);
pango_layout_get_pixel_size (ve->definition_layout, &defn_w, &defn_h);
// Align baselines, without further considerations
gint wb = pango_layout_get_baseline (ve->word_layout) / PANGO_SCALE;
gint db = pango_layout_get_baseline (ve->definition_layout) / PANGO_SCALE;
gint word_y = MAX (0, db - wb);
gint defn_y = MAX (0, wb - db);
if (word_offset)
*word_offset = word_y;
if (defn_offset)
*defn_offset = defn_y;
return MAX (word_y + word_h, defn_y + defn_h);
}
#define PADDING 5
static gint
view_entry_draw (ViewEntry *ve, cairo_t *cr, gint full_width, gboolean even)
{
// TODO: this shouldn't be hardcoded, read it out from somewhere
gdouble g = even ? 1. : .95;
gint word_y = 0, defn_y = 0,
height = view_entry_height (ve, &word_y, &defn_y);
cairo_rectangle (cr, 0, 0, full_width, height);
cairo_set_source_rgb (cr, g, g, g);
cairo_fill (cr);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_move_to (cr, full_width / 2 + PADDING, defn_y);
pango_cairo_show_layout (cr, ve->definition_layout);
PangoLayoutIter *iter = pango_layout_get_iter (ve->definition_layout);
do
{
if (!pango_layout_iter_get_line_readonly (iter)->is_paragraph_start)
continue;
PangoRectangle logical = {};
pango_layout_iter_get_line_extents (iter, NULL, &logical);
cairo_move_to (cr, PADDING, word_y + logical.y / PANGO_SCALE);
pango_cairo_show_layout (cr, ve->word_layout);
}
while (pango_layout_iter_next_line (iter));
pango_layout_iter_free (iter);
return height;
}
static void
view_entry_rebuild_layout (ViewEntry *ve, PangoContext *pc, gint width)
{
g_clear_object (&ve->word_layout);
g_clear_object (&ve->definition_layout);
int left_width = width / 2 - 2 * PADDING;
int right_width = width - left_width - 2 * PADDING;
if (left_width < 1 || right_width < 1)
return;
// TODO: preferably pre-validate the layouts with pango_parse_markup(),
// so that it doesn't warn without indication on the frontend
ve->word_layout = pango_layout_new (pc);
pango_layout_set_markup (ve->word_layout, ve->word, -1);
pango_layout_set_ellipsize (ve->word_layout, PANGO_ELLIPSIZE_END);
pango_layout_set_single_paragraph_mode (ve->word_layout, TRUE);
pango_layout_set_width (ve->word_layout, PANGO_SCALE * left_width);
ve->definition_layout = pango_layout_new (pc);
pango_layout_set_markup (ve->definition_layout, ve->definition, -1);
pango_layout_set_width (ve->definition_layout, PANGO_SCALE * right_width);
}
// --- Widget ------------------------------------------------------------------
struct _StardictView
{
GtkWidget parent_instance;
StardictDict *dict; ///< The displayed dictionary
guint top_position; ///< Index of the topmost dict. entry
gchar *matched; ///< Highlight common word part of this
gint top_offset; ///< Pixel offset into the entry
// TODO: think about making it, e.g., a pair of (ViewEntry *, guint)
// NOTE: this is the index of a Pango paragraph (a virtual entity)
guint selected; ///< Offset to the selected definition
GList *entries; ///< ViewEntry-s within the view
};
static void
adjust_for_offset (StardictView *self)
{
// FIXME: lots of code duplication with reload(), could be refactored
GtkWidget *widget = GTK_WIDGET (self);
PangoContext *pc = gtk_widget_get_pango_context (widget);
const gchar *matched = self->matched ? self->matched : "";
GtkAllocation allocation = {};
gtk_widget_get_allocation (widget, &allocation);
// If scrolled way up, prepend entries so long as it's possible
StardictIterator *iterator =
stardict_iterator_new (self->dict, self->top_position);
while (self->top_offset < 0)
{
stardict_iterator_prev (iterator);
if (!stardict_iterator_is_valid (iterator))
{
self->top_offset = 0;
break;
}
self->top_position = stardict_iterator_get_offset (iterator);
ViewEntry *ve = view_entry_new (iterator, matched);
view_entry_rebuild_layout (ve, pc, allocation.width);
self->top_offset += view_entry_height (ve, NULL, NULL);
self->entries = g_list_prepend (self->entries, ve);
}
g_object_unref (iterator);
// If scrolled way down, drop leading entries so long as it's possible
while (self->entries)
{
gint height = view_entry_height (self->entries->data, NULL, NULL);
if (self->top_offset < height)
break;
self->top_offset -= height;
view_entry_destroy (self->entries->data);
self->entries = g_list_delete_link (self->entries, self->entries);
self->top_position++;
}
if (self->top_offset && !self->entries)
self->top_offset = 0;
// Load replacement trailing entries, or drop those no longer visible
iterator = stardict_iterator_new (self->dict, self->top_position);
gint used = -self->top_offset;
for (GList *iter = self->entries, *next;
next = g_list_next (iter), iter; iter = next)
{
if (used < allocation.height)
used += view_entry_height (iter->data, NULL, NULL);
else
{
view_entry_destroy (iter->data);
self->entries = g_list_delete_link (self->entries, iter);
}
stardict_iterator_next (iterator);
}
while (used < allocation.height && stardict_iterator_is_valid (iterator))
{
ViewEntry *ve = view_entry_new (iterator, matched);
view_entry_rebuild_layout (ve, pc, allocation.width);
used += view_entry_height (ve, NULL, NULL);
self->entries = g_list_append (self->entries, ve);
stardict_iterator_next (iterator);
}
g_object_unref (iterator);
gtk_widget_queue_draw (widget);
}
static void
reload (StardictView *self)
{
g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy);
self->entries = NULL;
GtkWidget *widget = GTK_WIDGET (self);
if (!gtk_widget_get_realized (widget) || !self->dict)
return;
GtkAllocation allocation = {};
gtk_widget_get_allocation (widget, &allocation);
PangoContext *pc = gtk_widget_get_pango_context (widget);
StardictIterator *iterator =
stardict_iterator_new (self->dict, self->top_position);
gint used = 0;
const gchar *matched = self->matched ? self->matched : "";
while (used < allocation.height && stardict_iterator_is_valid (iterator))
{
ViewEntry *ve = view_entry_new (iterator, matched);
view_entry_rebuild_layout (ve, pc, allocation.width);
used += view_entry_height (ve, NULL, NULL);
self->entries = g_list_prepend (self->entries, ve);
stardict_iterator_next (iterator);
}
g_object_unref (iterator);
self->entries = g_list_reverse (self->entries);
// Right now, we're being lazy--this could be integrated here
adjust_for_offset (self);
gtk_widget_queue_draw (widget);
}
// --- Boilerplate -------------------------------------------------------------
G_DEFINE_TYPE (StardictView, stardict_view, GTK_TYPE_WIDGET)
static void
stardict_view_finalize (GObject *gobject)
{
StardictView *self = STARDICT_VIEW (gobject);
g_clear_object (&self->dict);
g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy);
self->entries = NULL;
g_free (self->matched);
self->matched = NULL;
G_OBJECT_CLASS (stardict_view_parent_class)->finalize (gobject);
}
static void
stardict_view_get_preferred_height (GtkWidget *widget,
gint *minimum, gint *natural)
{
PangoLayout *layout = gtk_widget_create_pango_layout (widget, NULL);
gint width = 0, height = 0;
pango_layout_get_pixel_size (layout, &width, &height);
g_object_unref (layout);
// There isn't any value that would make any real sense
if (!STARDICT_VIEW (widget)->dict)
*natural = *minimum = 0;
else
*natural = *minimum = height;
}
static void
stardict_view_get_preferred_width (GtkWidget *widget G_GNUC_UNUSED,
gint *minimum, gint *natural)
{
*natural = *minimum = 4 * PADDING;
}
static void
stardict_view_realize (GtkWidget *widget)
{
GtkAllocation allocation;
gtk_widget_get_allocation (widget, &allocation);
GdkWindowAttr attributes =
{
.window_type = GDK_WINDOW_CHILD,
.x = allocation.x,
.y = allocation.y,
.width = allocation.width,
.height = allocation.height,
// Input-only would presumably also work (as in GtkPathBar, e.g.),
// but it merely seems to involve more work.
.wclass = GDK_INPUT_OUTPUT,
.visual = gtk_widget_get_visual (widget),
.event_mask = gtk_widget_get_events (widget) | GDK_SCROLL_MASK,
};
// We need this window to receive input events at all.
// TODO: see if we don't want GDK_WA_CURSOR for setting a text cursor
GdkWindow *window = gdk_window_new (gtk_widget_get_parent_window (widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
// The default background colour of the GDK window is transparent,
// we'll keep it that way, rather than apply the style context.
gtk_widget_register_window (widget, window);
gtk_widget_set_window (widget, window);
gtk_widget_set_realized (widget, TRUE);
}
static gboolean
stardict_view_draw (GtkWidget *widget, cairo_t *cr)
{
StardictView *self = STARDICT_VIEW (widget);
GtkAllocation allocation;
gtk_widget_get_allocation (widget, &allocation);
gint offset = -self->top_offset;
gint i = self->top_position;
for (GList *iter = self->entries; iter; iter = iter->next)
{
cairo_save (cr);
cairo_translate (cr, 0, offset);
// TODO: later exclude clipped entries, but it's not that important
offset += view_entry_draw (iter->data, cr, allocation.width, i++ & 1);
cairo_restore (cr);
}
return TRUE;
}
static void
stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
GTK_WIDGET_CLASS (stardict_view_parent_class)
->size_allocate (widget, allocation);
reload (STARDICT_VIEW (widget));
}
static void
stardict_view_screen_changed (GtkWidget *widget, G_GNUC_UNUSED GdkScreen *prev)
{
// Update the minimum size
gtk_widget_queue_resize (widget);
// Recreate Pango layouts
reload (STARDICT_VIEW (widget));
}
static gboolean
stardict_view_scroll_event (GtkWidget *widget, GdkEventScroll *event)
{
// TODO: rethink the notes here to rather iterate over /definition lines/
// - iterate over selected lines, maybe one, maybe three
StardictView *self = STARDICT_VIEW (widget);
if (!self->dict)
return FALSE;
switch (event->direction)
{
case GDK_SCROLL_UP:
self->top_offset -= 50;
adjust_for_offset (self);
return TRUE;
case GDK_SCROLL_DOWN:
self->top_offset += 50;
adjust_for_offset (self);
return TRUE;
default:
// GDK_SCROLL_SMOOTH doesn't fit the intended way of usage
return FALSE;
}
}
static void
stardict_view_class_init (StardictViewClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = stardict_view_finalize;
// TODO: handle mouse events for text selection
// See https://wiki.gnome.org/HowDoI/CustomWidgets for some guidelines.
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
widget_class->get_preferred_height = stardict_view_get_preferred_height;
widget_class->get_preferred_width = stardict_view_get_preferred_width;
widget_class->realize = stardict_view_realize;
widget_class->draw = stardict_view_draw;
widget_class->size_allocate = stardict_view_size_allocate;
widget_class->screen_changed = stardict_view_screen_changed;
widget_class->scroll_event = stardict_view_scroll_event;
}
static void
stardict_view_init (G_GNUC_UNUSED StardictView *self)
{
}
// --- Public ------------------------------------------------------------------
GtkWidget *
stardict_view_new (void)
{
return GTK_WIDGET (g_object_new (STARDICT_TYPE_VIEW, NULL));
}
void
stardict_view_set_position (StardictView *self,
StardictDict *dict, guint position)
{
g_return_if_fail (STARDICT_IS_VIEW (self));
g_return_if_fail (STARDICT_IS_DICT (dict));
// Update the minimum size, if appropriate (almost never)
if (!self->dict != !dict)
gtk_widget_queue_resize (GTK_WIDGET (self));
g_clear_object (&self->dict);
self->dict = g_object_ref (dict);
self->top_position = position;
self->top_offset = 0;
reload (self);
}
void
stardict_view_set_matched (StardictView *self, const gchar *matched)
{
g_return_if_fail (STARDICT_IS_VIEW (self));
g_free (self->matched);
self->matched = g_strdup (matched);
reload (self);
}

34
src/stardict-view.h Normal file
View File

@ -0,0 +1,34 @@
/*
* StarDict GTK+ UI - dictionary view component
*
* Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#ifndef STARDICT_VIEW_H
#define STARDICT_VIEW_H
#include <gtk/gtk.h>
#include "stardict.h"
#define STARDICT_TYPE_VIEW (stardict_view_get_type ())
G_DECLARE_FINAL_TYPE (StardictView, stardict_view, STARDICT, VIEW, GtkWidget)
GtkWidget *stardict_view_new (void);
void stardict_view_set_position (StardictView *view,
StardictDict *dict, guint position);
void stardict_view_set_matched (StardictView *view, const gchar *matched);
#endif // ! STARDICT_VIEW_H

View File

@ -198,7 +198,7 @@ struct stardict_entry_field
struct stardict_entry struct stardict_entry
{ {
GObject parent_instance; GObject parent_instance;
GList * fields; ///< List of StardictEntryField's GList * fields; ///< List of StardictEntryField-s
}; };
struct stardict_entry_class struct stardict_entry_class