tdv/src/sdgtk.c

312 lines
9.1 KiB
C

/*
* StarDict GTK+ UI
*
* 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.
*
* 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 <locale.h>
#include "config.h"
#include "stardict.h"
#include "utils.h"
#include "stardict-view.h"
typedef struct dictionary Dictionary;
struct dictionary
{
const gchar *filename; ///< Filename
StardictDict *dict; ///< Stardict dictionary data
gchar *name; ///< Name to show
guint position; ///< Current position
};
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *entry; ///< Search entry widget
GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary
Dictionary *dictionaries; ///< All open dictionaries
gsize dictionaries_len; ///< Total number of dictionaries
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
dictionary_load (Dictionary *self, gchar *filename, GError **e)
{
self->filename = filename;
if (!(self->dict = stardict_dict_new (self->filename, e)))
return FALSE;
if (!self->name)
{
self->name = g_strdup (stardict_info_get_book_name
(stardict_dict_get_info (self->dict)));
}
return TRUE;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
init (gchar **filenames, GError **e)
{
while (filenames[g.dictionaries_len])
g.dictionaries_len++;
g.dictionaries = g_malloc0_n (sizeof *g.dictionaries, g.dictionaries_len);
for (gsize i = 0; i < g.dictionaries_len; i++)
{
Dictionary *dict = &g.dictionaries[i];
if (!dictionary_load (dict, filenames[i], e))
return FALSE;
}
return TRUE;
}
static void
search (Dictionary *dict)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
StardictIterator *iterator =
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]);
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gtk_entry_set_text (GTK_ENTRY (g.entry), text);
g_signal_emit_by_name (g.entry,
"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
}
static void
on_selection (GtkClipboard *clipboard, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (g.watch_selection
&& !gtk_window_has_toplevel_focus (GTK_WINDOW (g.window))
&& event->owner_change.owner != NULL)
gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
}
static void
on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
{
g.watch_selection = gtk_check_menu_item_get_active (item);
}
static void
on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
guint page_num, G_GNUC_UNUSED gpointer data)
{
g.dictionary = page_num;
search (&g.dictionaries[g.dictionary]);
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (event->key.state == GDK_CONTROL_MASK)
{
if (event->key.keyval == GDK_KEY_Page_Up)
{
gtk_notebook_prev_page (GTK_NOTEBOOK (g.notebook));
return TRUE;
}
if (event->key.keyval == GDK_KEY_Page_Down)
{
gtk_notebook_next_page (GTK_NOTEBOOK (g.notebook));
return TRUE;
}
}
if (event->key.state == GDK_MOD1_MASK)
{
if (event->key.keyval >= GDK_KEY_0
&& event->key.keyval <= GDK_KEY_9)
{
gint n = event->key.keyval - GDK_KEY_0;
gtk_notebook_set_current_page
(GTK_NOTEBOOK (g.notebook), n ? (n - 1) : 10);
return TRUE;
}
}
return FALSE;
}
static void
on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
gtk_main_quit ();
}
static void
die_with_dialog (const gchar *message)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
exit (EXIT_FAILURE);
}
int
main (int argc, char *argv[])
{
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
gchar **filenames = NULL;
GOptionEntry option_entries[] =
{
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
NULL, N_("FILE...")},
{},
};
GError *error = NULL;
gtk_init_with_args (&argc, &argv, N_("- StarDict GTK+ UI"),
option_entries, GETTEXT_PACKAGE, &error);
if (error)
{
g_warning ("%s", error->message);
g_error_free (error);
return 1;
}
if (!filenames)
{
// TODO: eventually just load all dictionaries from configuration
die_with_dialog ("No arguments have been passed.");
}
if (!init (filenames, &error))
die_with_dialog (error->message);
// 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 ();
gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL);
gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
g.watch_selection = TRUE;
GtkWidget *item =
gtk_check_menu_item_new_with_label (_("Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item), g.watch_selection);
g_signal_connect (item, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
gtk_widget_show_all (menu);
GtkWidget *hamburger = gtk_menu_button_new ();
gtk_menu_button_set_direction (GTK_MENU_BUTTON (hamburger), GTK_ARROW_NONE);
gtk_menu_button_set_popup (GTK_MENU_BUTTON (hamburger), menu);
gtk_button_set_relief (GTK_BUTTON (hamburger), GTK_RELIEF_NONE);
gtk_widget_show (hamburger);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), hamburger, GTK_PACK_END);
// FIXME: when the clear icon shows, the widget changes in height
g.entry = gtk_search_entry_new ();
// 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.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_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++)
{
Dictionary *dict = &g.dictionaries[i];
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_append_page (GTK_NOTEBOOK (g.notebook), dummy, label);
}
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
gtk_widget_grab_focus (g.entry);
gtk_widget_show_all (g.window);
gtk_main ();
g_strfreev (filenames);
return 0;
}