/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * Copyright (C) 2010-2020 Shaun McCance * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, see . * * Author: Shaun McCance */ #ifdef HAVE_CONFIG_H #include #endif #define G_SETTINGS_ENABLE_BACKEND #include #include #include #include #ifdef GDK_WINDOWING_X11 #include #endif #include #include #include "yelp-bookmarks.h" #include "yelp-settings.h" #include "yelp-view.h" #include "yelp-application.h" #include "yelp-window.h" #define DEFAULT_URI "help:gnome-help" static gboolean editor_mode = FALSE; G_GNUC_NORETURN static gboolean option_version_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { g_print ("%s %s\n", PACKAGE, VERSION); exit (0); } static const GOptionEntry entries[] = { {"editor-mode", 0, 0, G_OPTION_ARG_NONE, &editor_mode, N_("Turn on editor mode"), NULL}, { "version", 0, G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, option_version_cb, NULL, NULL }, { NULL } }; typedef struct _YelpApplicationLoad YelpApplicationLoad; struct _YelpApplicationLoad { YelpApplication *app; guint32 timestamp; gboolean new; gboolean fallback_help_list; }; static void yelp_application_iface_init (YelpBookmarksInterface *iface); static void yelp_application_dispose (GObject *object); static void yelp_application_finalize (GObject *object); static gboolean yelp_application_cmdline (GApplication *app, gchar ***arguments, gint *exit_status); static void yelp_application_startup (GApplication *app); static int yelp_application_command_line (GApplication *app, GApplicationCommandLine *cmdline); static void application_uri_resolved (YelpUri *uri, YelpApplicationLoad *data); static gboolean application_window_deleted (YelpWindow *window, GdkEvent *event, YelpApplication *app); GSettings * application_get_doc_settings (YelpApplication *app, const gchar *doc_uri); static void application_adjust_font (GAction *action, GVariant *parameter, YelpApplication *app); static void application_set_font_sensitivity (YelpApplication *app); static void bookmarks_changed (GSettings *settings, const gchar *key, YelpApplication *app); static gboolean window_resized (YelpWindow *window, YelpApplication *app); typedef struct _YelpApplicationPrivate YelpApplicationPrivate; struct _YelpApplicationPrivate { GSList *windows; GHashTable *windows_by_document; GPropertyAction *show_cursor_action; GSimpleAction *larger_text_action; GSimpleAction *smaller_text_action; GSettingsBackend *backend; GSettings *gsettings; GHashTable *docsettings; }; G_DEFINE_TYPE_WITH_CODE (YelpApplication, yelp_application, GTK_TYPE_APPLICATION, G_IMPLEMENT_INTERFACE (YELP_TYPE_BOOKMARKS, yelp_application_iface_init) G_ADD_PRIVATE (YelpApplication) ) static void yelp_application_init (YelpApplication *app) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); priv->docsettings = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_object_unref); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "app.yelp-application-show-cursor", (const gchar*[]) {"F7", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "app.yelp-application-larger-text", (const gchar*[]) {"plus", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "app.yelp-application-smaller-text", (const gchar*[]) {"minus", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-window-find", (const gchar*[]) {"F", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-window-search", (const gchar*[]) {"S", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-window-new", (const gchar*[]) {"N", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-window-close", (const gchar*[]) {"W", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-window-ctrll", (const gchar*[]) {"L", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-view-print", (const gchar*[]) {"P", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-view-go-back", (const gchar*[]) {"Left", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-view-go-forward", (const gchar*[]) {"Right", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-view-go-previous", (const gchar*[]) {"Page_Up", NULL}); gtk_application_set_accels_for_action (GTK_APPLICATION (app), "win.yelp-view-go-next", (const gchar*[]) {"Page_Down", NULL}); } static void yelp_application_class_init (YelpApplicationClass *klass) { GApplicationClass *application_class = G_APPLICATION_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); application_class->local_command_line = yelp_application_cmdline; application_class->startup = yelp_application_startup; application_class->command_line = yelp_application_command_line; object_class->dispose = yelp_application_dispose; object_class->finalize = yelp_application_finalize; } static void yelp_application_iface_init (YelpBookmarksInterface *iface) { iface->add_bookmark = yelp_application_add_bookmark; iface->remove_bookmark = yelp_application_remove_bookmark; iface->is_bookmarked = yelp_application_is_bookmarked; } static void yelp_application_dispose (GObject *object) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (YELP_APPLICATION (object)); if (priv->show_cursor_action) { g_object_unref (priv->show_cursor_action); priv->show_cursor_action = NULL; } if (priv->larger_text_action) { g_object_unref (priv->larger_text_action); priv->larger_text_action = NULL; } if (priv->smaller_text_action) { g_object_unref (priv->smaller_text_action); priv->smaller_text_action = NULL; } if (priv->gsettings) { g_object_unref (priv->gsettings); priv->gsettings = NULL; } G_OBJECT_CLASS (yelp_application_parent_class)->dispose (object); } static void yelp_application_finalize (GObject *object) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (YELP_APPLICATION (object)); g_hash_table_destroy (priv->windows_by_document); g_hash_table_destroy (priv->docsettings); G_OBJECT_CLASS (yelp_application_parent_class)->finalize (object); } static gboolean yelp_application_cmdline (GApplication *app, gchar ***arguments, gint *exit_status) { GOptionContext *context; gint argc = g_strv_length (*arguments); gint i; context = g_option_context_new (NULL); g_option_context_add_group (context, gtk_get_option_group (FALSE)); g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); g_option_context_parse (context, &argc, arguments, NULL); for (i = 1; i < argc; i++) { if (!strchr ((*arguments)[i], ':') && !((*arguments)[i][0] == '/')) { GFile *base, *new; gchar *cur, *newuri; cur = g_get_current_dir (); base = g_file_new_for_path (cur); new = g_file_resolve_relative_path (base, (*arguments)[i]); newuri = g_file_get_uri (new); g_free ((*arguments)[i]); (*arguments)[i] = newuri; g_free (cur); g_object_unref (new); g_object_unref (base); } } return G_APPLICATION_CLASS (yelp_application_parent_class) ->local_command_line (app, arguments, exit_status); } static void yelp_application_startup (GApplication *application) { YelpApplication *app = YELP_APPLICATION (application); YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); gchar *keyfile; YelpSettings *settings; g_set_application_name (_("Help")); /* chain up */ G_APPLICATION_CLASS (yelp_application_parent_class)->startup (application); hdy_init (); settings = yelp_settings_get_default (); if (editor_mode) yelp_settings_set_editor_mode (settings, TRUE); priv->windows_by_document = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); /* Use a config file for per-document settings, because Ryan asked me to. */ keyfile = g_build_filename (g_get_user_config_dir (), "yelp", "yelp.cfg", NULL); priv->backend = g_keyfile_settings_backend_new (keyfile, "/org/gnome/yelp/", "yelp"); g_free (keyfile); /* But the main settings are in dconf */ priv->gsettings = g_settings_new ("org.gnome.yelp"); g_settings_bind (priv->gsettings, "show-cursor", settings, "show-text-cursor", G_SETTINGS_BIND_DEFAULT); priv->show_cursor_action = g_property_action_new ("yelp-application-show-cursor", settings, "show-text-cursor"); g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (priv->show_cursor_action)); g_settings_bind (priv->gsettings, "font-adjustment", settings, "font-adjustment", G_SETTINGS_BIND_DEFAULT); priv->larger_text_action = g_simple_action_new ("yelp-application-larger-text", NULL); g_signal_connect (priv->larger_text_action, "activate", G_CALLBACK (application_adjust_font), app); g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (priv->larger_text_action)); priv->smaller_text_action = g_simple_action_new ("yelp-application-smaller-text", NULL); g_signal_connect (priv->smaller_text_action, "activate", G_CALLBACK (application_adjust_font), app); g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (priv->smaller_text_action)); application_set_font_sensitivity (app); } /******************************************************************************/ static void application_adjust_font (GAction *action, GVariant *parameter, YelpApplication *app) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); gint adjustment = g_settings_get_int (priv->gsettings, "font-adjustment"); gint adjust = g_str_equal (g_action_get_name (action), "yelp-application-larger-text") ? 1 : -1; adjustment += adjust; g_settings_set_int (priv->gsettings, "font-adjustment", adjustment); application_set_font_sensitivity (app); } static void application_set_font_sensitivity (YelpApplication *app) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); YelpSettings *settings = yelp_settings_get_default (); GParamSpec *spec = g_object_class_find_property ((GObjectClass *) YELP_SETTINGS_GET_CLASS (settings), "font-adjustment"); gint adjustment = g_settings_get_int (priv->gsettings, "font-adjustment"); if (!G_PARAM_SPEC_INT (spec)) { g_warning ("Expcected integer param spec for font-adjustment"); return; } g_simple_action_set_enabled (priv->larger_text_action, adjustment < ((GParamSpecInt *) spec)->maximum); g_simple_action_set_enabled (priv->smaller_text_action, adjustment > ((GParamSpecInt *) spec)->minimum); } /******************************************************************************/ YelpApplication * yelp_application_new (void) { YelpApplication *app; char *app_id = NULL; char *yelp = NULL; if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) { GKeyFile *kf = g_key_file_new (); g_key_file_load_from_file (kf, "/.flatpak-info", G_KEY_FILE_NONE, NULL); if (g_key_file_has_group (kf, "Application")) app_id = g_key_file_get_string (kf, "Application", "name", NULL); else app_id = g_key_file_get_string (kf, "Runtime", "name", NULL); yelp = g_strconcat (app_id, ".Help", NULL); g_key_file_unref (kf); } else { yelp = g_strdup ("org.gnome.Yelp"); } app = g_object_new (YELP_TYPE_APPLICATION, "application-id", yelp, "flags", G_APPLICATION_HANDLES_COMMAND_LINE, "inactivity-timeout", 5000, NULL); g_free (app_id); g_free (yelp); return app; } /* consumes the uri */ static void open_uri (YelpApplication *app, YelpUri *uri, gboolean new_window, gboolean fallback_help_list) { YelpApplicationLoad *data; data = g_new (YelpApplicationLoad, 1); data->app = app; data->timestamp = gtk_get_current_event_time (); data->new = new_window; data->fallback_help_list = fallback_help_list; g_signal_connect (uri, "resolved", G_CALLBACK (application_uri_resolved), data); /* hold the app while resolving the uri so we don't exit while * in the middle of the load */ g_application_hold (G_APPLICATION (app)); yelp_uri_resolve (uri); } static int yelp_application_command_line (GApplication *application, GApplicationCommandLine *cmdline) { YelpApplication *app = YELP_APPLICATION (application); gchar **argv; int i; argv = g_application_command_line_get_arguments (cmdline, NULL); if (argv[1] == NULL) open_uri (app, yelp_uri_new (DEFAULT_URI), FALSE, TRUE); for (i = 1; argv[i]; i++) open_uri (app, yelp_uri_new (argv[i]), FALSE, FALSE); g_strfreev (argv); return 0; } void yelp_application_new_window (YelpApplication *app, const gchar *uri) { if (uri) open_uri (app, yelp_uri_new (uri), TRUE, FALSE); else open_uri (app, yelp_uri_new (DEFAULT_URI), TRUE, TRUE); } void yelp_application_new_window_uri (YelpApplication *app, YelpUri *uri) { open_uri (app, g_object_ref (uri), TRUE, FALSE); } static void application_uri_resolved (YelpUri *uri, YelpApplicationLoad *data) { YelpWindow *window; gchar *doc_uri; GdkWindow *gdk_window; YelpApplicationPrivate *priv = yelp_application_get_instance_private (data->app); GFile *gfile; /* We held the application while resolving the URI, so unhold now. */ g_application_release (G_APPLICATION (data->app)); /* Get the GFile associated with the URI, or NULL if not available */ gfile = yelp_uri_get_file (uri); if (gfile == NULL && data->fallback_help_list) { /* There is no file associated to the default uri, so we'll fallback * to help-list: if we're told to do so. */ open_uri (data->app, yelp_uri_new ("help-list:"), data->new, FALSE); g_object_unref (uri); g_free (data); return; } g_clear_object (&gfile); doc_uri = yelp_uri_get_document_uri (uri); if (data->new || !doc_uri) window = NULL; else window = g_hash_table_lookup (priv->windows_by_document, doc_uri); if (window == NULL) { gint width, height; GSettings *settings = application_get_doc_settings (data->app, doc_uri); g_settings_get (settings, "geometry", "(ii)", &width, &height); window = yelp_window_new (data->app); gtk_window_set_default_size (GTK_WINDOW (window), width, height); g_signal_connect (window, "resized", G_CALLBACK (window_resized), data->app); priv->windows = g_slist_prepend (priv->windows, window); if (!data->new) { g_hash_table_insert (priv->windows_by_document, doc_uri, window); g_object_set_data (G_OBJECT (window), "doc_uri", doc_uri); } else { g_free (doc_uri); } g_signal_connect (window, "delete-event", G_CALLBACK (application_window_deleted), data->app); gtk_window_set_application (GTK_WINDOW (window), GTK_APPLICATION (data->app)); } else { g_free (doc_uri); } yelp_window_load_uri (window, uri); gtk_widget_show_all (GTK_WIDGET (window)); /* Metacity no longer does anything useful with gtk_window_present */ gdk_window = gtk_widget_get_window (GTK_WIDGET (window)); #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_WINDOW (gdk_window)){ if (gdk_window) gdk_x11_window_move_to_current_desktop (gdk_window); /* Ensure we actually present the window when invoked from the command * line. This is somewhat evil, but the minor evil of Yelp stealing * focus (after you requested it) is outweighed for me by the major * evil of no help window appearing when you click Help. */ if (data->timestamp == 0) data->timestamp = gdk_x11_get_server_time (gtk_widget_get_window (GTK_WIDGET (window))); gtk_window_present_with_time (GTK_WINDOW (window), data->timestamp); } else #endif gtk_window_present (GTK_WINDOW (window)); g_object_unref (uri); g_free (data); } static gboolean application_window_deleted (YelpWindow *window, GdkEvent *event, YelpApplication *app) { gchar *doc_uri; /* owned by windows_by_document */ YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); priv->windows = g_slist_remove (priv->windows, window); doc_uri = g_object_get_data (G_OBJECT (window), "doc_uri"); if (doc_uri) g_hash_table_remove (priv->windows_by_document, doc_uri); return FALSE; } GSettings * application_get_doc_settings (YelpApplication *app, const gchar *doc_uri) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); GSettings *settings = g_hash_table_lookup (priv->docsettings, doc_uri); if (settings == NULL) { gchar *tmp, *key, *settings_path; tmp = g_uri_escape_string (doc_uri, "", FALSE); settings_path = g_strconcat ("/org/gnome/yelp/documents/", tmp, "/", NULL); g_free (tmp); if (priv->backend) settings = g_settings_new_with_backend_and_path ("org.gnome.yelp.documents", priv->backend, settings_path); else settings = g_settings_new_with_path ("org.gnome.yelp.documents", settings_path); key = g_strdup (doc_uri); g_hash_table_insert (priv->docsettings, key, settings); g_object_set_data ((GObject *) settings, "doc_uri", key); g_signal_connect (settings, "changed::bookmarks", G_CALLBACK (bookmarks_changed), app); g_free (settings_path); } return settings; } /******************************************************************************/ void yelp_application_add_bookmark (YelpBookmarks *bookmarks, const gchar *doc_uri, const gchar *page_id, const gchar *icon, const gchar *title) { GSettings *settings; YelpApplication *app = YELP_APPLICATION (bookmarks); g_return_if_fail (page_id); g_return_if_fail (doc_uri); settings = application_get_doc_settings (app, doc_uri); if (settings) { GVariantBuilder builder; GVariantIter *iter; gchar *this_id, *this_icon, *this_title; gboolean broken = FALSE; g_settings_get (settings, "bookmarks", "a(sss)", &iter); g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sss)")); while (g_variant_iter_loop (iter, "(&s&s&s)", &this_id, &this_icon, &this_title)) { if (g_str_equal (page_id, this_id)) { /* Already have this bookmark */ broken = TRUE; break; } g_variant_builder_add (&builder, "(sss)", this_id, this_icon, this_title); } g_variant_iter_free (iter); if (!broken) { GVariant *value; g_variant_builder_add (&builder, "(sss)", page_id, icon, title); value = g_variant_builder_end (&builder); g_settings_set_value (settings, "bookmarks", value); } } } void yelp_application_remove_bookmark (YelpBookmarks *bookmarks, const gchar *doc_uri, const gchar *page_id) { GSettings *settings; YelpApplication *app = YELP_APPLICATION (bookmarks); g_return_if_fail (page_id); g_return_if_fail (doc_uri); settings = application_get_doc_settings (app, doc_uri); if (settings) { GVariantBuilder builder; GVariantIter *iter; gchar *this_id, *this_icon, *this_title; g_settings_get (settings, "bookmarks", "a(sss)", &iter); g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sss)")); while (g_variant_iter_loop (iter, "(&s&s&s)", &this_id, &this_icon, &this_title)) { if (!g_str_equal (page_id, this_id)) g_variant_builder_add (&builder, "(sss)", this_id, this_icon, this_title); } g_variant_iter_free (iter); g_settings_set_value (settings, "bookmarks", g_variant_builder_end (&builder)); } } gboolean yelp_application_is_bookmarked (YelpBookmarks *bookmarks, const gchar *doc_uri, const gchar *page_id) { GVariant *stored_bookmarks; GVariantIter *iter; gboolean ret = FALSE; gchar *this_id = NULL; GSettings *settings; YelpApplication *app = YELP_APPLICATION (bookmarks); g_return_val_if_fail (page_id, FALSE); g_return_val_if_fail (doc_uri, FALSE); settings = application_get_doc_settings (app, doc_uri); if (settings == NULL) return FALSE; stored_bookmarks = g_settings_get_value (settings, "bookmarks"); g_settings_get (settings, "bookmarks", "a(sss)", &iter); while (g_variant_iter_loop (iter, "(&s&s&s)", &this_id, NULL, NULL)) { if (g_str_equal (page_id, this_id)) { ret = TRUE; break; } } g_variant_iter_free (iter); g_variant_unref (stored_bookmarks); return ret; } void yelp_application_update_bookmarks (YelpApplication *app, const gchar *doc_uri, const gchar *page_id, const gchar *icon, const gchar *title) { GSettings *settings; g_return_if_fail (page_id); g_return_if_fail (doc_uri); settings = application_get_doc_settings (app, doc_uri); if (settings) { GVariantBuilder builder; GVariantIter *iter; gchar *this_id, *this_icon, *this_title; gboolean updated = FALSE; g_settings_get (settings, "bookmarks", "a(sss)", &iter); g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sss)")); while (g_variant_iter_loop (iter, "(&s&s&s)", &this_id, &this_icon, &this_title)) { if (g_str_equal (page_id, this_id)) { if (icon && !g_str_equal (icon, this_icon)) { this_icon = (gchar *) icon; updated = TRUE; } if (title && !g_str_equal (title, this_title)) { this_title = (gchar *) title; updated = TRUE; } if (!updated) break; } g_variant_builder_add (&builder, "(sss)", this_id, this_icon, this_title); } g_variant_iter_free (iter); if (updated) g_settings_set_value (settings, "bookmarks", g_variant_builder_end (&builder)); else g_variant_builder_clear (&builder); } } GVariant * yelp_application_get_bookmarks (YelpApplication *app, const gchar *doc_uri) { GSettings *settings = application_get_doc_settings (app, doc_uri); return g_settings_get_value (settings, "bookmarks"); } static void bookmarks_changed (GSettings *settings, const gchar *key, YelpApplication *app) { const gchar *doc_uri = g_object_get_data ((GObject *) settings, "doc_uri"); if (doc_uri) g_signal_emit_by_name (app, "bookmarks-changed", doc_uri); } static gboolean window_resized (YelpWindow *window, YelpApplication *app) { YelpApplicationPrivate *priv = yelp_application_get_instance_private (app); YelpUri *uri; gchar *doc_uri; GSettings *settings; uri = yelp_window_get_uri (window); if (uri == NULL) return FALSE; doc_uri = yelp_uri_get_document_uri (uri); if (doc_uri == NULL) { g_object_unref (uri); return FALSE; } settings = g_hash_table_lookup (priv->docsettings, doc_uri); if (settings) { gint width, height; yelp_window_get_geometry (window, &width, &height); g_settings_set (settings, "geometry", "(ii)", width, height); } g_free (doc_uri); g_object_unref (uri); return FALSE; }