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:
parent
f812fae922
commit
9d7bc2a839
|
@ -157,7 +157,9 @@ target_link_libraries (${PROJECT_NAME} ${project_common_libraries})
|
|||
pkg_check_modules (gtk gtk+-3.0)
|
||||
if (gtk_FOUND)
|
||||
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_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries})
|
||||
endif ()
|
||||
|
|
144
src/sdgtk.c
144
src/sdgtk.c
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* 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
|
||||
* purpose with or without fee is hereby granted.
|
||||
|
@ -24,6 +24,7 @@
|
|||
#include "config.h"
|
||||
#include "stardict.h"
|
||||
#include "utils.h"
|
||||
#include "stardict-view.h"
|
||||
|
||||
typedef struct dictionary Dictionary;
|
||||
|
||||
|
@ -40,7 +41,7 @@ static struct
|
|||
GtkWidget *window; ///< Top-level window
|
||||
GtkWidget *notebook; ///< Notebook with tabs
|
||||
GtkWidget *entry; ///< Search entry widget
|
||||
GtkWidget *grid; ///< Entries container
|
||||
GtkWidget *view; ///< Entries view
|
||||
|
||||
gint dictionary; ///< Index of the current dictionary
|
||||
Dictionary *dictionaries; ///< All open dictionaries
|
||||
|
@ -85,99 +86,6 @@ init (gchar **filenames, GError **e)
|
|||
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
|
||||
search (Dictionary *dict)
|
||||
{
|
||||
|
@ -188,13 +96,16 @@ search (Dictionary *dict)
|
|||
stardict_dict_search (dict->dict, input_utf8, NULL);
|
||||
dict->position = stardict_iterator_get_offset (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
|
||||
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
|
||||
{
|
||||
search (&g.dictionaries[g.dictionary]);
|
||||
reload (g.grid);
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -231,7 +142,6 @@ on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
|
|||
{
|
||||
g.dictionary = page_num;
|
||||
search (&g.dictionaries[g.dictionary]);
|
||||
reload (g.grid);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
|
@ -317,12 +227,8 @@ main (int argc, char *argv[])
|
|||
if (!init (filenames, &error))
|
||||
die_with_dialog (error->message);
|
||||
|
||||
// Some Adwaita stupidity and our own additions
|
||||
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); }";
|
||||
// Some Adwaita stupidity
|
||||
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }";
|
||||
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
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_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_signal_connect (g.notebook, "switch-page",
|
||||
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,
|
||||
// though ^U is working already! Note that bindings can be done in CSS
|
||||
// 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);
|
||||
|
||||
// TODO: supposedly attach to "key-press-event" here and react to
|
||||
// PageUp/PageDown and up/down arrow keys... either here or in the Entry
|
||||
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_CALLBACK (on_destroy), NULL);
|
||||
g_signal_connect (g.window, "key-press-event",
|
||||
G_CALLBACK (on_key_press), NULL);
|
||||
|
||||
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
|
||||
gtk_container_add (GTK_CONTAINER (g.window), superbox);
|
||||
gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
|
||||
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++)
|
||||
{
|
||||
|
@ -403,13 +302,6 @@ main (int argc, char *argv[])
|
|||
g_signal_connect (clipboard, "owner-change",
|
||||
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_show_all (g.window);
|
||||
gtk_main ();
|
||||
|
|
|
@ -230,7 +230,7 @@ struct application
|
|||
guint32 top_position; ///< Index of the topmost dict. entry
|
||||
guint top_offset; ///< Offset into the top entry
|
||||
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
|
||||
GArray * input; ///< The current search input
|
||||
|
@ -386,6 +386,7 @@ view_entry_new (StardictIterator *iterator)
|
|||
found_anything_displayable = TRUE;
|
||||
break;
|
||||
case STARDICT_FIELD_PHONETIC:
|
||||
// FIXME this makes it highlightable
|
||||
g_string_append_printf (word, " /%s/", (const gchar *) field->data);
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -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 ("<%s>", 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);
|
||||
}
|
|
@ -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
|
|
@ -198,7 +198,7 @@ struct stardict_entry_field
|
|||
struct stardict_entry
|
||||
{
|
||||
GObject parent_instance;
|
||||
GList * fields; ///< List of StardictEntryField's
|
||||
GList * fields; ///< List of StardictEntryField-s
|
||||
};
|
||||
|
||||
struct stardict_entry_class
|
||||
|
@ -209,4 +209,4 @@ struct stardict_entry_class
|
|||
GType stardict_entry_get_type (void);
|
||||
const GList *stardict_entry_get_fields (StardictEntry *sde) G_GNUC_PURE;
|
||||
|
||||
#endif // ! STARDICT_H
|
||||
#endif // ! STARDICT_H
|
||||
|
|
Loading…
Reference in New Issue