/*
* StarDict GTK+ UI
*
* Copyright (c) 2020 - 2024, Přemysl Eric Janouch
*
* 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
#include
#include
#include "config.h"
#include "stardict.h"
#include "utils.h"
#include "stardict-view.h"
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *hamburger; ///< Hamburger menu
GtkWidget *entry; ///< Search entry widget
GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary
gint last; ///< The last dictionary index
GPtrArray *dictionaries; ///< All open dictionaries
gboolean loading; ///< Dictionaries are being loaded
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_from_filenames (GPtrArray *out, gchar **filenames)
{
for (gsize i = 0; filenames[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_strdup (filenames[i]);
g_ptr_array_add (out, dict);
}
}
// TODO: try to deduplicate, similar to app_load_config_values()
static gboolean
load_from_key_file (GPtrArray *out, GKeyFile *kf, GError **error)
{
const gchar *dictionaries = "Dictionaries";
gchar **names = g_key_file_get_keys (kf, dictionaries, NULL, NULL);
if (!names)
return TRUE;
for (gsize i = 0; names[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->name = names[i];
g_ptr_array_add (out, dict);
}
g_free (names);
for (gsize i = 0; i < out->len; i++)
{
Dictionary *dict = g_ptr_array_index (out, i);
gchar *path =
g_key_file_get_string (kf, dictionaries, dict->name, error);
if (!path)
return FALSE;
// Try to resolve relative paths and expand tildes
if (!(dict->filename =
resolve_filename (path, resolve_relative_config_filename)))
dict->filename = path;
else
g_free (path);
}
return TRUE;
}
static gboolean
load_from_config (GPtrArray *out, GError **error)
{
GKeyFile *key_file = load_project_config_file (error);
if (!key_file)
return FALSE;
gboolean result = load_from_key_file (out, key_file, error);
g_key_file_free (key_file);
return result;
}
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);
if (!dict->dict)
return;
StardictIterator *iterator =
stardict_dict_search (dict->dict, input_utf8, NULL);
stardict_view_set_position (STARDICT_VIEW (g.view),
dict->dict, stardict_iterator_get_offset (iterator));
g_object_unref (iterator);
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_ptr_array_index (g.dictionaries, g.dictionary));
}
static void
on_send (G_GNUC_UNUSED StardictView *view,
const char *word, G_GNUC_UNUSED gpointer data)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
gtk_entry_buffer_set_text (buf, word, -1);
gtk_editable_select_region (GTK_EDITABLE (g.entry), 0, -1);
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gchar *trimmed = g_strstrip (g_strdup (text));
gtk_entry_set_text (GTK_ENTRY (g.entry), trimmed);
g_free (trimmed);
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.last = g.dictionary;
g.dictionary = page_num;
search (g_ptr_array_index (g.dictionaries, g.dictionary));
// Hack: Make right-clicking notebook arrows also re-focus the entry.
GdkEvent *event = gtk_get_current_event ();
if (event && event->type == GDK_BUTTON_PRESS)
gtk_widget_grab_focus (g.entry);
}
static gboolean
accelerate_hamburger (GdkEvent *event)
{
gchar *accelerator = NULL;
g_object_get (gtk_widget_get_settings (g.window), "gtk-menu-bar-accel",
&accelerator, NULL);
if (!accelerator)
return FALSE;
guint key = 0;
GdkModifierType mods = 0;
gtk_accelerator_parse (accelerator, &key, &mods);
g_free (accelerator);
guint mask = gtk_accelerator_get_default_mod_mask ();
if (!key || event->key.keyval != key || (event->key.state & mask) != mods)
return FALSE;
gtk_button_clicked (GTK_BUTTON (g.hamburger));
return TRUE;
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
// The "activate" signal of the GtkMenuButton cannot be used
// from a real accelerator, due to "no trigger event for menu popup".
if (accelerate_hamburger (event))
return TRUE;
GtkNotebook *notebook = GTK_NOTEBOOK (g.notebook);
guint mods = event->key.state & gtk_accelerator_get_default_mod_mask ();
if (mods == GDK_CONTROL_MASK)
{
// Can't use gtk_widget_add_accelerator() to change-current-page(-1/+1)
// because that signal has arguments, which cannot be passed.
gint current = gtk_notebook_get_current_page (notebook);
if (event->key.keyval == GDK_KEY_Page_Up)
return gtk_notebook_set_current_page (notebook, --current), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return gtk_notebook_set_current_page (notebook,
++current % gtk_notebook_get_n_pages (notebook)), TRUE;
}
if (mods == 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 (notebook, (n ? n : 10) - 1);
return TRUE;
}
if (event->key.keyval == GDK_KEY_Tab)
{
gtk_notebook_set_current_page (notebook, g.last);
return TRUE;
}
}
if (mods == 0)
{
StardictView *view = STARDICT_VIEW (g.view);
if (event->key.keyval == GDK_KEY_Page_Up)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, -0.5), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, +0.5), TRUE;
if (event->key.keyval == GDK_KEY_Up)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, -1), TRUE;
if (event->key.keyval == GDK_KEY_Down)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, +1), TRUE;
}
return FALSE;
}
static gboolean
on_tab_focus (G_GNUC_UNUSED GtkWidget *widget,
G_GNUC_UNUSED GtkDirectionType direction, G_GNUC_UNUSED gpointer user_data)
{
// Hack: Make it so that tab headers don't retain newly gained focus
// when clicked, re-focus the entry instead.
GdkEvent *event = gtk_get_current_event ();
if (!event || event->type != GDK_BUTTON_PRESS
|| event->button.button != GDK_BUTTON_PRIMARY)
return FALSE;
gtk_widget_grab_focus (g.entry);
return TRUE;
}
static void
init_tabs (void)
{
for (gsize i = g.dictionaries->len; i--; )
{
Dictionary *dict = g_ptr_array_index (g.dictionaries, i);
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
g_signal_connect (dummy, "focus", G_CALLBACK (on_tab_focus), NULL);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_insert_page (GTK_NOTEBOOK (g.notebook), dummy, label, 0);
}
gtk_widget_show_all (g.notebook);
gtk_widget_grab_focus (g.entry);
}
static void
show_error_dialog (GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (g.window), 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
g_error_free (error);
}
// --- Loading -----------------------------------------------------------------
static void
on_new_dictionaries_loaded (G_GNUC_UNUSED GObject* source_object,
GAsyncResult* res, G_GNUC_UNUSED gpointer user_data)
{
g.loading = FALSE;
GError *error = NULL;
GPtrArray *new_dictionaries =
g_task_propagate_pointer (G_TASK (res), &error);
if (!new_dictionaries)
{
show_error_dialog (error);
return;
}
while (gtk_notebook_get_n_pages (GTK_NOTEBOOK (g.notebook)))
gtk_notebook_remove_page (GTK_NOTEBOOK (g.notebook), -1);
g.dictionary = -1;
if (g.dictionaries)
g_ptr_array_free (g.dictionaries, TRUE);
stardict_view_set_position (STARDICT_VIEW (g.view), NULL, 0);
g.dictionaries = new_dictionaries;
init_tabs ();
}
static void
on_reload_dictionaries_task (GTask *task, G_GNUC_UNUSED gpointer source_object,
gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
GError *error = NULL;
if (load_dictionaries (task_data, &error))
{
g_task_return_pointer (task,
g_ptr_array_ref (task_data), (GDestroyNotify) g_ptr_array_unref);
}
else
g_task_return_error (task, error);
}
static gboolean
reload_dictionaries (GPtrArray *new_dictionaries, GError **error)
{
// TODO: We could cancel that task.
if (g.loading)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"already loading dictionaries");
return FALSE;
}
// TODO: Some other kind of indication.
// Note that "action widgets" aren't visible without GtkNotebook tabs.
g.loading = TRUE;
GTask *task = g_task_new (NULL, NULL, on_new_dictionaries_loaded, NULL);
g_task_set_name (task, __func__);
g_task_set_task_data (task,
new_dictionaries, (GDestroyNotify) g_ptr_array_unref);
g_task_run_in_thread (task, on_reload_dictionaries_task);
g_object_unref (task);
return TRUE;
}
static GtkWidget *
new_open_dialog (void)
{
// The default is local-only. Paths are returned absolute.
GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open dictionary"),
GTK_WINDOW (g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT, NULL);
GtkFileFilter *filter = gtk_file_filter_new ();
gtk_file_filter_add_pattern (filter, "*.ifo");
gtk_file_filter_set_name (filter, "*.ifo");
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
gtk_file_chooser_add_filter (chooser, filter);
gtk_file_chooser_set_select_multiple (chooser, TRUE);
return dialog;
}
static void
on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
{
GSList *paths = gtk_file_chooser_get_filenames (chooser);
for (GSList *iter = paths; iter; iter = iter->next)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = iter->data;
g_ptr_array_add (new_dictionaries, dict);
}
g_slist_free (paths);
}
gtk_widget_destroy (dialog);
GError *error = NULL;
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
if (error)
show_error_dialog (error);
}
static void
on_drag_data_received (G_GNUC_UNUSED GtkWidget *widget, GdkDragContext *context,
G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, GtkSelectionData *data,
G_GNUC_UNUSED guint info, guint time, G_GNUC_UNUSED gpointer user_data)
{
GError *error = NULL;
gchar **dropped_uris = gtk_selection_data_get_uris (data);
if (!dropped_uris)
return;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
for (gsize i = 0; !error && dropped_uris[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_filename_from_uri (dropped_uris[i], NULL, &error);
g_ptr_array_add (new_dictionaries, dict);
}
g_strfreev (dropped_uris);
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
gtk_drag_finish (context, error == NULL, FALSE, time);
if (error)
show_error_dialog (error);
}
// --- Settings ----------------------------------------------------------------
typedef struct settings_data SettingsData;
enum
{
SETTINGS_COLUMN_NAME,
SETTINGS_COLUMN_PATH,
SETTINGS_COLUMN_COUNT
};
struct settings_data
{
GKeyFile *key_file; ///< Configuration file
GtkTreeModel *model; ///< GtkListStore
};
static void
settings_load (SettingsData *data)
{
// We want to keep original comments, as well as any other data.
GError *error = NULL;
data->key_file = load_project_config_file (&error);
if (!data->key_file)
{
if (error)
show_error_dialog (error);
data->key_file = g_key_file_new ();
}
GtkListStore *list_store = gtk_list_store_new (SETTINGS_COLUMN_COUNT,
G_TYPE_STRING, G_TYPE_STRING);
data->model = GTK_TREE_MODEL (list_store);
const gchar *dictionaries = "Dictionaries";
gchar **names =
g_key_file_get_keys (data->key_file, dictionaries, NULL, NULL);
if (!names)
return;
for (gsize i = 0; names[i]; i++)
{
gchar *path = g_key_file_get_string (data->key_file,
dictionaries, names[i], NULL);
if (!path)
continue;
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, names[i], SETTINGS_COLUMN_PATH, path, -1);
g_free (path);
}
g_strfreev (names);
}
static void
settings_save (SettingsData *data)
{
const gchar *dictionaries = "Dictionaries";
g_key_file_remove_group (data->key_file, dictionaries, NULL);
GtkTreeIter iter = { 0 };
gboolean valid = gtk_tree_model_get_iter_first (data->model, &iter);
while (valid)
{
gchar *name = NULL, *path = NULL;
gtk_tree_model_get (data->model, &iter,
SETTINGS_COLUMN_NAME, &name, SETTINGS_COLUMN_PATH, &path, -1);
if (name && path)
g_key_file_set_string (data->key_file, dictionaries, name, path);
g_free (name);
g_free (path);
valid = gtk_tree_model_iter_next (data->model, &iter);
}
GError *e = NULL;
if (!save_project_config_file (data->key_file, &e))
show_error_dialog (e);
}
static void
on_settings_name_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_NAME, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_path_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_PATH, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_add (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GSList *paths = NULL;
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
paths = gtk_file_chooser_get_filenames (chooser);
gtk_widget_destroy (dialog);
// When the dialog is aborted, we simply add an empty list.
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
const gchar *home = g_get_home_dir ();
for (GSList *iter = paths; iter; iter = iter->next)
{
GError *error = NULL;
StardictInfo *ifo = stardict_info_new (iter->data, &error);
g_free (iter->data);
if (!ifo)
{
show_error_dialog (error);
continue;
}
// We also expand tildes, even on Windows, so no problem there.
const gchar *path = stardict_info_get_path (ifo);
gchar *tildified = g_str_has_prefix (stardict_info_get_path (ifo), home)
? g_strdup_printf ("~%s", path + strlen (home))
: g_strdup (path);
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, stardict_info_get_book_name (ifo),
SETTINGS_COLUMN_PATH, tildified, -1);
g_free (tildified);
stardict_info_free (ifo);
}
g_slist_free (paths);
}
static void
on_settings_remove (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
GtkTreeSelection *selection = gtk_tree_view_get_selection (tree_view);
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
GList *selected = gtk_tree_selection_get_selected_rows (selection, &model);
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = iter->data;
iter->data = gtk_tree_row_reference_new (model, path);
gtk_tree_path_free (path);
}
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = gtk_tree_row_reference_get_path (iter->data);
if (path)
{
GtkTreeIter tree_iter = { 0 };
if (gtk_tree_model_get_iter (model, &tree_iter, path))
gtk_list_store_remove (list_store, &tree_iter);
gtk_tree_path_free (path);
}
}
g_list_free_full (selected, (GDestroyNotify) gtk_tree_row_reference_free);
}
static void
on_settings_selection_changed
(GtkTreeSelection* selection, gpointer user_data)
{
GtkWidget *remove = GTK_WIDGET (user_data);
gtk_widget_set_sensitive (remove,
gtk_tree_selection_count_selected_rows (selection) > 0);
}
static void
on_settings (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
SettingsData sd = {};
settings_load (&sd);
GtkWidget *treeview = gtk_tree_view_new_with_model (sd.model);
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (treeview), TRUE);
g_object_unref (sd.model);
GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_name_edited), sd.model);
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes
(_("Name"), renderer, "text", SETTINGS_COLUMN_NAME, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_path_edited), sd.model);
column = gtk_tree_view_column_new_with_attributes
(_("Path"), renderer, "text", SETTINGS_COLUMN_PATH, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
GtkWidget *scrolled = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type
(GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_ETCHED_IN);
gtk_container_add (GTK_CONTAINER (scrolled), treeview);
GtkWidget *dialog = gtk_dialog_new_with_buttons (_("Settings"),
GTK_WINDOW (g.window),
GTK_DIALOG_MODAL,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Save"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 400);
GtkWidget *remove = gtk_button_new_with_mnemonic (_("_Remove"));
gtk_widget_set_sensitive (remove, FALSE);
g_signal_connect (remove, "clicked",
G_CALLBACK (on_settings_remove), treeview);
GtkWidget *add = gtk_button_new_with_mnemonic (_("_Add..."));
g_signal_connect (add, "clicked",
G_CALLBACK (on_settings_add), treeview);
GtkTreeSelection *selection =
gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
g_signal_connect (selection, "changed",
G_CALLBACK (on_settings_selection_changed), remove);
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start (GTK_BOX (box),
gtk_label_new (_("Here you can configure the default dictionaries.")),
FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), remove, FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), add, FALSE, FALSE, 0);
GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
g_object_set (content_area, "margin", 12, NULL);
gtk_box_pack_start (GTK_BOX (content_area), box, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (content_area), scrolled, TRUE, TRUE, 12);
gtk_widget_show_all (dialog);
switch (gtk_dialog_run (GTK_DIALOG (dialog)))
{
case GTK_RESPONSE_NONE:
break;
case GTK_RESPONSE_ACCEPT:
settings_save (&sd);
// Fall through
default:
gtk_widget_destroy (dialog);
}
g_key_file_free (sd.key_file);
}
// --- Main --------------------------------------------------------------------
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
gui_main (char *argv[])
{
// Just like with GtkApplication, argv has been parsed by the option group.
gtk_init (NULL, NULL);
gtk_window_set_default_icon_name (PROJECT_NAME);
GError *error = NULL;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (argv[0])
load_from_filenames (new_dictionaries, argv);
else if (!load_from_config (new_dictionaries, &error) && error)
die_with_dialog (error->message);
if (!new_dictionaries->len)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
_("No dictionaries found either in "
"the configuration or on the command line"));
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
// This is better than nothing.
// Our GtkNotebook action widget would be invisible without any tabs.
on_settings (NULL, NULL);
exit (EXIT_SUCCESS);
}
// Some Adwaita stupidity, plus defaults for our own widget.
// All the named colours have been there since GNOME 3.4
// (see gnome-extra-themes git history, Adwaita used to live there).
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
// `gsettings set org.gnome.desktop.interface gtk-key-theme "Emacs"`
// isn't quite what I want, and note that ^U works by default
"@binding-set Readline {"
"bind 'H' { 'delete-from-cursor' (chars, -1) };"
"bind 'W' { 'delete-from-cursor' (word-ends, -1) }; }"
"entry { -gtk-key-bindings: Readline; border-radius: 0; }"
"stardict-view { padding: 0 .25em; }"
"stardict-view.odd {"
"background: @theme_base_color; "
"color: @theme_text_color; }"
"stardict-view.odd:backdrop {"
"background: @theme_unfocused_base_color; "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view.even {"
"background: mix(@theme_base_color, @theme_text_color, 0.03); "
"color: @theme_text_color; }"
"stardict-view.even:backdrop {"
"background: mix(@theme_unfocused_base_color, "
"@theme_fg_color, 0.03); "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view:selected {"
"background-color: @theme_selected_bg_color; "
"color: @theme_selected_fg_color; }";
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);
GtkWidget *item_open = gtk_menu_item_new_with_mnemonic (_("_Open..."));
g_signal_connect (item_open, "activate", G_CALLBACK (on_open), NULL);
GtkWidget *item_settings = gtk_menu_item_new_with_mnemonic (_("_Settings"));
g_signal_connect (item_settings, "activate",
G_CALLBACK (on_settings), NULL);
g.watch_selection = TRUE;
GtkWidget *item_selection =
gtk_check_menu_item_new_with_mnemonic (_("_Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item_selection), g.watch_selection);
g_signal_connect (item_selection, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_widget_set_halign (menu, GTK_ALIGN_END);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_open);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_settings);
#ifndef G_OS_WIN32
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_selection);
#endif // ! G_OS_WIN32
gtk_widget_show_all (menu);
g.hamburger = gtk_menu_button_new ();
gtk_button_set_relief (GTK_BUTTON (g.hamburger), GTK_RELIEF_NONE);
gtk_button_set_image (GTK_BUTTON (g.hamburger), gtk_image_new_from_icon_name
("open-menu-symbolic", GTK_ICON_SIZE_BUTTON));
gtk_menu_button_set_popup (GTK_MENU_BUTTON (g.hamburger), menu);
gtk_widget_show (g.hamburger);
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);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), g.hamburger, GTK_PACK_END);
g.entry = gtk_search_entry_new ();
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view);
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (g.window), PROJECT_NAME);
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);
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
gtk_drag_dest_set (g.view,
GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
gtk_drag_dest_add_uri_targets (g.view);
g_signal_connect (g.view, "drag-data-received",
G_CALLBACK (on_drag_data_received), NULL);
g_signal_connect (g.view, "send",
G_CALLBACK (on_send), NULL);
if (!reload_dictionaries (new_dictionaries, &error))
die_with_dialog (error->message);
gtk_widget_show_all (g.window);
gtk_main ();
return 0;
}