1 /*
2  *
3  *  Copyright (C) 2010-2011  Colomban Wendling <ban@herbesfolles.org>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #include "config.h"
21 
22 #include <stdlib.h>
23 #include <stdio.h>
24 #include <glib.h>
25 #include <glib/gi18n-lib.h>
26 #include <gtk/gtk.h>
27 #include <gdk/gdkkeysyms.h>
28 
29 #include <geanyplugin.h>
30 #include <geany.h>
31 #include <document.h>
32 
33 #include "gwh-utils.h"
34 #include "gwh-browser.h"
35 #include "gwh-settings.h"
36 #include "gwh-plugin.h"
37 #include "gwh-keybindings.h"
38 #include "gwh-enum-types.h"
39 
40 
41 GeanyPlugin      *geany_plugin;
42 GeanyData        *geany_data;
43 
44 
45 PLUGIN_VERSION_CHECK(224)
46 
47 PLUGIN_SET_TRANSLATABLE_INFO (
48   LOCALEDIR, GETTEXT_PACKAGE,
49   _("Web helper"),
50   _("Display a preview web page that gets updated upon document saving and "
51     "provide web analysis and debugging tools (aka Web Inspector), all using "
52     "WebKit."),
53   GWH_PLUGIN_VERSION,
54   "Colomban Wendling <ban@herbesfolles.org>"
55 )
56 
57 
58 enum {
59   CONTAINER_NOTEBOOK,
60   CONTAINER_WINDOW
61 };
62 
63 static GtkWidget   *G_browser   = NULL;
64 static struct {
65   guint       type;
66   GtkWidget  *widget;
67 
68   /* only valid if type == CONTAINER_WINDOW */
69   gboolean    visible;
70 } G_container;
71 static GwhSettings *G_settings  = NULL;
72 
73 
74 static void
separate_window_set_visible(gboolean visible)75 separate_window_set_visible (gboolean visible)
76 {
77   if (visible != G_container.visible) {
78     gchar *geometry;
79 
80     G_container.visible = visible;
81     if (visible) {
82       gtk_widget_show (G_container.widget);
83       g_object_get (G_settings, "browser-separate-window-geometry", &geometry, NULL);
84       gwh_set_window_geometry (GTK_WINDOW (G_container.widget), geometry, NULL, NULL);
85       g_free (geometry);
86     } else {
87       geometry = gwh_get_window_geometry (GTK_WINDOW (G_container.widget), 0, 0);
88       g_object_set (G_settings, "browser-separate-window-geometry", geometry, NULL);
89       g_free (geometry);
90       gtk_widget_hide (G_container.widget);
91     }
92   }
93 }
94 
95 static gboolean
on_separate_window_delete_event(GtkWidget * widget,GdkEvent * event,gpointer data)96 on_separate_window_delete_event (GtkWidget  *widget,
97                                  GdkEvent   *event,
98                                  gpointer    data)
99 {
100   /* never honor delete-event */
101   return TRUE;
102 }
103 
104 static void
on_separate_window_destroy(GtkWidget * widget,gpointer data)105 on_separate_window_destroy (GtkWidget  *widget,
106                             gpointer    data)
107 {
108   gwh_browser_set_inspector_transient_for (GWH_BROWSER (G_browser), NULL);
109   gtk_container_remove (GTK_CONTAINER (G_container.widget), G_browser);
110 }
111 
112 static gboolean
on_idle_widget_show(gpointer data)113 on_idle_widget_show (gpointer data)
114 {
115   separate_window_set_visible (TRUE);
116   /* present back the Geany's window because it is very unlikely the user
117    * expects the focus on our newly created window at this point, since we
118    * either just loaded the plugin or activated a element from Geany's UI */
119   gtk_window_present (GTK_WINDOW (geany_data->main_widgets->window));
120 
121   return FALSE;
122 }
123 
124 static GtkWidget *
create_separate_window(void)125 create_separate_window (void)
126 {
127   GtkWidget  *window;
128   gboolean    skips_taskbar;
129   gboolean    is_transient;
130   gint        window_type;
131 
132   g_object_get (G_settings,
133                 "wm-secondary-windows-skip-taskbar", &skips_taskbar,
134                 "wm-secondary-windows-are-transient", &is_transient,
135                 "wm-secondary-windows-type", &window_type,
136                 NULL);
137   window = g_object_new (GTK_TYPE_WINDOW,
138                          "type", GTK_WINDOW_TOPLEVEL,
139                          "skip-taskbar-hint", skips_taskbar,
140                          "title", _("Web view"),
141                          "deletable", FALSE,
142                          "type-hint", window_type,
143                          NULL);
144   g_signal_connect (window, "delete-event",
145                     G_CALLBACK (on_separate_window_delete_event), NULL);
146   g_signal_connect (window, "destroy",
147                     G_CALLBACK (on_separate_window_destroy), NULL);
148   gtk_container_add (GTK_CONTAINER (window), G_browser);
149   if (is_transient) {
150     gtk_window_set_transient_for (GTK_WINDOW (window),
151                                   GTK_WINDOW (geany_data->main_widgets->window));
152   } else {
153     GList *icons;
154 
155     icons = gtk_window_get_icon_list (GTK_WINDOW (geany_data->main_widgets->window));
156     gtk_window_set_icon_list (GTK_WINDOW (window), icons);
157     g_list_free (icons);
158   }
159   gwh_browser_set_inspector_transient_for (GWH_BROWSER (G_browser),
160                                            GTK_WINDOW (window));
161 
162   return window;
163 }
164 
165 static void
attach_browser(void)166 attach_browser (void)
167 {
168   GwhBrowserPosition position;
169 
170   g_object_get (G_settings, "browser-position", &position, NULL);
171   if (position == GWH_BROWSER_POSITION_SEPARATE_WINDOW) {
172     G_container.type = CONTAINER_WINDOW;
173     G_container.widget = create_separate_window ();
174     /* seems that if a window is shown before it's transient parent, bad stuff
175      * happend. so, show our window a little later. */
176     g_idle_add (on_idle_widget_show, G_container.widget);
177   } else {
178     G_container.type = CONTAINER_NOTEBOOK;
179     if (position == GWH_BROWSER_POSITION_SIDEBAR) {
180       G_container.widget = geany_data->main_widgets->sidebar_notebook;
181     } else {
182       G_container.widget = geany_data->main_widgets->message_window_notebook;
183     }
184     gtk_notebook_append_page (GTK_NOTEBOOK (G_container.widget),
185                               G_browser, gtk_label_new (_("Web preview")));
186     gwh_browser_set_inspector_transient_for (GWH_BROWSER (G_browser),
187                                              GTK_WINDOW (geany_data->main_widgets->window));
188   }
189 }
190 
191 static void
detach_browser(void)192 detach_browser (void)
193 {
194   if (G_container.type == CONTAINER_WINDOW) {
195     separate_window_set_visible (FALSE); /* saves the geometry */
196     gtk_widget_destroy (G_container.widget);
197   } else {
198     gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (G_browser)),
199                           G_browser);
200   }
201 }
202 
203 static void
on_settings_browser_position_notify(GObject * object,GParamSpec * pspec,gpointer data)204 on_settings_browser_position_notify (GObject     *object,
205                                      GParamSpec  *pspec,
206                                      gpointer     data)
207 {
208   g_object_ref (G_browser);
209   detach_browser ();
210   attach_browser ();
211   g_object_unref (G_browser);
212 }
213 
214 static void
on_settings_windows_attrs_notify(GObject * object,GParamSpec * pspec,gpointer data)215 on_settings_windows_attrs_notify (GObject    *object,
216                                   GParamSpec *pspec,
217                                   gpointer    data)
218 {
219   /* recreate the window to apply the new attributes */
220   if (G_container.type == CONTAINER_WINDOW) {
221     g_object_ref (G_browser);
222     detach_browser ();
223     attach_browser ();
224     g_object_unref (G_browser);
225   }
226 }
227 
228 static void
on_document_save(GObject * obj,GeanyDocument * doc,gpointer user_data)229 on_document_save (GObject        *obj,
230                   GeanyDocument  *doc,
231                   gpointer        user_data)
232 {
233   gboolean auto_reload = FALSE;
234 
235   g_object_get (G_OBJECT (G_settings), "browser-auto-reload", &auto_reload,
236                 NULL);
237   if (auto_reload) {
238     gwh_browser_reload (GWH_BROWSER (G_browser));
239   }
240 }
241 
242 static void
on_item_auto_reload_toggled(GtkCheckMenuItem * item,gpointer dummy)243 on_item_auto_reload_toggled (GtkCheckMenuItem *item,
244                              gpointer          dummy)
245 {
246   g_object_set (G_OBJECT (G_settings), "browser-auto-reload",
247                 gtk_check_menu_item_get_active (item), NULL);
248 }
249 
250 static void
on_browser_populate_popup(GwhBrowser * browser,GtkMenu * menu,gpointer dummy)251 on_browser_populate_popup (GwhBrowser *browser,
252                            GtkMenu    *menu,
253                            gpointer    dummy)
254 {
255   GtkWidget  *item;
256   gboolean    auto_reload = FALSE;
257 
258   item = gtk_separator_menu_item_new ();
259   gtk_widget_show (item);
260   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
261 
262   g_object_get (G_OBJECT (G_settings), "browser-auto-reload", &auto_reload,
263                 NULL);
264   item = gtk_check_menu_item_new_with_mnemonic (_("Reload upon document saving"));
265   gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), auto_reload);
266   gtk_widget_show (item);
267   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
268   g_signal_connect (item, "toggled", G_CALLBACK (on_item_auto_reload_toggled),
269                     NULL);
270 }
271 
272 static void
on_kb_toggle_inspector(guint key_id)273 on_kb_toggle_inspector (guint key_id)
274 {
275   gwh_browser_toggle_inspector (GWH_BROWSER (G_browser));
276 }
277 
278 static void
on_kb_show_hide_separate_window(guint key_id)279 on_kb_show_hide_separate_window (guint key_id)
280 {
281   if (G_container.type == CONTAINER_WINDOW) {
282     separate_window_set_visible (! G_container.visible);
283   }
284 }
285 
286 static void
on_kb_toggle_bookmark(guint key_id)287 on_kb_toggle_bookmark (guint key_id)
288 {
289   const gchar *uri = gwh_browser_get_uri (GWH_BROWSER (G_browser));
290 
291   if (gwh_browser_has_bookmark (GWH_BROWSER (G_browser), uri)) {
292     gwh_browser_remove_bookmark (GWH_BROWSER (G_browser), uri);
293   } else {
294     gwh_browser_add_bookmark (GWH_BROWSER (G_browser), uri);
295   }
296 }
297 
298 
299 static gchar *
get_config_filename(void)300 get_config_filename (void)
301 {
302   return g_build_filename (geany_data->app->configdir, "plugins",
303                            GWH_PLUGIN_TARNAME, GWH_PLUGIN_TARNAME".conf", NULL);
304 }
305 
306 static void
load_config(void)307 load_config (void)
308 {
309   gchar  *path;
310   GError *err = NULL;
311 
312   G_settings = gwh_settings_get_default ();
313 
314   gwh_settings_install_property (G_settings, g_param_spec_boolean (
315     "browser-auto-reload",
316     _("Browser auto reload"),
317     _("Whether the browser reloads itself upon document saving"),
318     TRUE,
319     G_PARAM_READWRITE));
320   gwh_settings_install_property (G_settings, g_param_spec_string (
321     "browser-last-uri",
322     _("Browser last URI"),
323     _("Last URI visited by the browser"),
324     "about:blank",
325     G_PARAM_READWRITE));
326   gwh_settings_install_property (G_settings, g_param_spec_boxed (
327     "browser-bookmarks",
328     _("Bookmarks"),
329     _("List of bookmarks"),
330     G_TYPE_STRV,
331     G_PARAM_READWRITE));
332   gwh_settings_install_property (G_settings, g_param_spec_enum (
333     "browser-orientation",
334     _("Browser orientation"),
335     _("Orientation of the browser widget"),
336     GTK_TYPE_ORIENTATION,
337     GTK_ORIENTATION_VERTICAL,
338     G_PARAM_READWRITE));
339   gwh_settings_install_property (G_settings, g_param_spec_enum (
340     "browser-position",
341     _("Browser position"),
342     _("Position of the browser widget in Geany's UI"),
343     GWH_TYPE_BROWSER_POSITION,
344     GWH_BROWSER_POSITION_MESSAGE_WINDOW,
345     G_PARAM_READWRITE));
346   gwh_settings_install_property (G_settings, g_param_spec_string (
347     "browser-separate-window-geometry",
348     _("Browser separate window geometry"),
349     _("Last geometry of the separated browser's window"),
350     "400x300",
351     G_PARAM_READWRITE));
352   gwh_settings_install_property (G_settings, g_param_spec_string (
353     "inspector-window-geometry",
354     _("Inspector window geometry"),
355     _("Last geometry of the inspector window"),
356     "400x300",
357     G_PARAM_READWRITE));
358   gwh_settings_install_property (G_settings, g_param_spec_boolean (
359     "inspector-detached",
360     _("Inspector detached"),
361     _("Whether the inspector is in a separate window or docked in the browser"),
362     FALSE,
363     G_PARAM_READWRITE));
364   gwh_settings_install_property (G_settings, g_param_spec_boolean (
365     "wm-secondary-windows-skip-taskbar",
366     _("Secondary windows skip task bar"),
367     _("Whether to tell the window manager not to show the secondary windows in the task bar"),
368     TRUE,
369     G_PARAM_READWRITE));
370   gwh_settings_install_property (G_settings, g_param_spec_boolean (
371     "wm-secondary-windows-are-transient",
372     _("Secondary windows are transient"),
373     _("Whether secondary windows are transient children of their parent"),
374     TRUE,
375     G_PARAM_READWRITE));
376   gwh_settings_install_property (G_settings, g_param_spec_enum (
377     "wm-secondary-windows-type",
378     _("Secondary windows type"),
379     _("The type of the secondary windows"),
380     GWH_TYPE_WINDOW_TYPE,
381     GWH_WINDOW_TYPE_NORMAL,
382     G_PARAM_READWRITE));
383 
384   path = get_config_filename ();
385   if (! gwh_settings_load_from_file (G_settings, path, &err)) {
386     g_warning ("Failed to load configuration: %s", err->message);
387     g_error_free (err);
388   }
389   g_free (path);
390 }
391 
392 static void
save_config(void)393 save_config (void)
394 {
395   gchar  *path;
396   gchar  *dirname;
397   GError *err = NULL;
398 
399   path = get_config_filename ();
400   dirname = g_path_get_dirname (path);
401   utils_mkdir (dirname, TRUE);
402   g_free (dirname);
403   if (! gwh_settings_save_to_file (G_settings, path, &err)) {
404     g_warning ("Failed to save configuration: %s", err->message);
405     g_error_free (err);
406   }
407   g_free (path);
408   g_object_unref (G_settings);
409   G_settings = NULL;
410 }
411 
412 void
plugin_init(GeanyData * data)413 plugin_init (GeanyData *data)
414 {
415   /* even though it's not really a good idea to keep all the library we load
416    * into memory, this is needed for webkit. first, without this we creash after
417    * module unloading, and webkitgtk inserts static data into the GLib
418    * (g_quark_from_static_string() for example) so it's not safe to remove it */
419   plugin_module_make_resident (geany_plugin);
420 
421   /* webkit uses threads but don't initialize the thread system */
422   if (! g_thread_supported ()) {
423     g_thread_init (NULL);
424   }
425 
426   load_config ();
427   gwh_keybindings_init ();
428 
429   G_browser = gwh_browser_new ();
430   g_signal_connect (G_browser, "populate-popup",
431                     G_CALLBACK (on_browser_populate_popup), NULL);
432 
433   attach_browser ();
434   gtk_widget_show_all (G_browser);
435 
436   plugin_signal_connect (geany_plugin, G_OBJECT (G_settings),
437                          "notify::browser-position", FALSE,
438                          G_CALLBACK (on_settings_browser_position_notify), NULL);
439   plugin_signal_connect (geany_plugin, G_OBJECT (G_settings),
440                          "notify::wm-secondary-windows-skip-taskbar", FALSE,
441                          G_CALLBACK (on_settings_windows_attrs_notify), NULL);
442   plugin_signal_connect (geany_plugin, G_OBJECT (G_settings),
443                          "notify::wm-secondary-windows-are-transient", FALSE,
444                          G_CALLBACK (on_settings_windows_attrs_notify), NULL);
445   plugin_signal_connect (geany_plugin, G_OBJECT (G_settings),
446                          "notify::wm-secondary-windows-type", FALSE,
447                          G_CALLBACK (on_settings_windows_attrs_notify), NULL);
448 
449   plugin_signal_connect (geany_plugin, NULL, "document-save", TRUE,
450                          G_CALLBACK (on_document_save), NULL);
451 
452   /* add keybindings */
453   keybindings_set_item (gwh_keybindings_get_group (), GWH_KB_TOGGLE_INSPECTOR,
454                         on_kb_toggle_inspector, GDK_F12, 0, "toggle_inspector",
455                         _("Toggle Web Inspector"), NULL);
456   keybindings_set_item (gwh_keybindings_get_group (),
457                         GWH_KB_SHOW_HIDE_SEPARATE_WINDOW,
458                         on_kb_show_hide_separate_window, 0, 0,
459                         "show_hide_separate_window",
460                         _("Show/Hide Web View's Window"), NULL);
461   keybindings_set_item (gwh_keybindings_get_group (), GWH_KB_TOGGLE_BOOKMARK,
462                         on_kb_toggle_bookmark, 0, 0, "toggle_bookmark",
463                         _("Toggle bookmark for the current website"), NULL);
464 }
465 
466 void
plugin_cleanup(void)467 plugin_cleanup (void)
468 {
469   detach_browser ();
470 
471   gwh_keybindings_cleanup ();
472   save_config ();
473 }
474 
475 
476 typedef struct _GwhConfigDialog GwhConfigDialog;
477 struct _GwhConfigDialog
478 {
479   GtkWidget *browser_position;
480   GtkWidget *browser_auto_reload;
481 
482   GtkWidget *secondary_windows_skip_taskbar;
483   GtkWidget *secondary_windows_are_transient;
484   GtkWidget *secondary_windows_type;
485 };
486 
487 static void
on_configure_dialog_response(GtkDialog * dialog,gint response_id,GwhConfigDialog * cdialog)488 on_configure_dialog_response (GtkDialog        *dialog,
489                               gint              response_id,
490                               GwhConfigDialog  *cdialog)
491 {
492   switch (response_id) {
493     case GTK_RESPONSE_ACCEPT:
494     case GTK_RESPONSE_APPLY:
495     case GTK_RESPONSE_OK:
496     case GTK_RESPONSE_YES: {
497       gwh_settings_widget_sync_v (G_settings,
498                                   cdialog->browser_position,
499                                   cdialog->browser_auto_reload,
500                                   cdialog->secondary_windows_skip_taskbar,
501                                   cdialog->secondary_windows_are_transient,
502                                   cdialog->secondary_windows_type,
503                                   NULL);
504       break;
505     }
506 
507     default: break;
508   }
509 
510   if (response_id != GTK_RESPONSE_APPLY) {
511     g_free (cdialog);
512   }
513 }
514 
515 GtkWidget *
plugin_configure(GtkDialog * dialog)516 plugin_configure (GtkDialog *dialog)
517 {
518   GtkWidget        *box1;
519   GtkWidget        *box;
520   GtkWidget        *alignment;
521   GwhConfigDialog  *cdialog;
522 
523   cdialog = g_malloc (sizeof *cdialog);
524 
525   /* Top-level box, containing the different frames */
526   box1 = gtk_vbox_new (FALSE, 12);
527 
528   /* Browser */
529   gtk_box_pack_start (GTK_BOX (box1), ui_frame_new_with_alignment (_("Browser"), &alignment), FALSE, FALSE, 0);
530   box = gtk_vbox_new (FALSE, 0);
531   gtk_container_add (GTK_CONTAINER (alignment), box);
532   /* browser position */
533   cdialog->browser_position = gwh_settings_widget_new (G_settings, "browser-position");
534   gtk_box_pack_start (GTK_BOX (box), cdialog->browser_position, FALSE, TRUE, 0);
535   /* auto-reload */
536   cdialog->browser_auto_reload = gwh_settings_widget_new (G_settings,
537                                                           "browser-auto-reload");
538   gtk_box_pack_start (GTK_BOX (box), cdialog->browser_auto_reload, FALSE, TRUE, 0);
539 
540   /* Windows */
541   gtk_box_pack_start (GTK_BOX (box1), ui_frame_new_with_alignment (_("Windows"), &alignment), FALSE, FALSE, 0);
542   box = gtk_vbox_new (FALSE, 0);
543   gtk_container_add (GTK_CONTAINER (alignment), box);
544   /* skip taskbar */
545   cdialog->secondary_windows_skip_taskbar = gwh_settings_widget_new (G_settings,
546                                                                      "wm-secondary-windows-skip-taskbar");
547   gtk_box_pack_start (GTK_BOX (box), cdialog->secondary_windows_skip_taskbar, FALSE, TRUE, 0);
548   /* tranisent */
549   cdialog->secondary_windows_are_transient = gwh_settings_widget_new (G_settings,
550                                                                       "wm-secondary-windows-are-transient");
551   gtk_box_pack_start (GTK_BOX (box), cdialog->secondary_windows_are_transient, FALSE, TRUE, 0);
552   /* type */
553   cdialog->secondary_windows_type = gwh_settings_widget_new (G_settings,
554                                                              "wm-secondary-windows-type");
555   gtk_box_pack_start (GTK_BOX (box), cdialog->secondary_windows_type, FALSE, TRUE, 0);
556 
557   g_signal_connect (dialog, "response",
558                     G_CALLBACK (on_configure_dialog_response), cdialog);
559 
560   return box1;
561 }
562