PageRenderTime 27ms CodeModel.GetById 13ms app.highlight 6ms RepoModel.GetById 1ms app.codeStats 0ms

/midori/midori-speeddial.vala

https://bitbucket.org/edgimar/midori
Vala | 376 lines | 315 code | 39 blank | 22 comment | 61 complexity | e9eda4773e18d584013baa0411c1a0b3 MD5 | raw file
Possible License(s): LGPL-2.1
  1/*
  2 Copyright (C) 2011-2012 Christian Dywan <christian@twotoats.de>
  3
  4 This library is free software; you can redistribute it and/or
  5 modify it under the terms of the GNU Lesser General Public
  6 License as published by the Free Software Foundation; either
  7 version 2.1 of the License, or (at your option) any later version.
  8
  9 See the file COPYING for the full license text.
 10*/
 11
 12namespace Katze {
 13    extern static string mkdir_with_parents (string pathname, int mode);
 14}
 15
 16namespace Sokoke {
 17    extern static string js_script_eval (void* ctx, string script, void* error);
 18    extern static string build_thumbnail_path (string uri);
 19}
 20
 21namespace Midori {
 22    public class SpeedDial : GLib.Object {
 23        string filename;
 24        public GLib.KeyFile keyfile;
 25        string? html = null;
 26        List<Spec> thumb_queue = null;
 27        WebKit.WebView thumb_view = null;
 28        Spec? spec = null;
 29
 30        public class Spec {
 31            public string dial_id;
 32            public string uri;
 33            public Spec (string dial_id, string uri) {
 34                this.dial_id = dial_id;
 35                this.uri = uri;
 36            }
 37        }
 38
 39        public SpeedDial (string new_filename, string? fallback = null) {
 40            filename = new_filename;
 41            keyfile = new GLib.KeyFile ();
 42            try {
 43                keyfile.load_from_file (filename, GLib.KeyFileFlags.NONE);
 44            }
 45            catch (GLib.Error io_error) {
 46                string json;
 47                size_t len;
 48                try {
 49                    FileUtils.get_contents (fallback ?? (filename + ".json"),
 50                                            out json, out len);
 51                }
 52                catch (GLib.Error fallback_error) {
 53                    json = "'{}'";
 54                    len = 4;
 55                }
 56
 57                var script = new StringBuilder.sized (len);
 58                script.append ("var json = JSON.parse (");
 59                script.append_len (json, (ssize_t)len);
 60                script.append ("""
 61        );
 62        var keyfile = '';
 63        for (var i in json['shortcuts']) {
 64        var tile = json['shortcuts'][i];
 65        keyfile += '[Dial ' + tile['id'].substring (1) + ']\n'
 66                +  'uri=' + tile['href'] + '\n'
 67                +  'img=' + tile['img'] + '\n'
 68                +  'title=' + tile['title'] + '\n\n';
 69        }
 70        var columns = json['width'] ? json['width'] : 3;
 71        var rows = json['shortcuts'] ? json['shortcuts'].length / columns : 0;
 72        keyfile += '[settings]\n'
 73                +  'columns=' + columns + '\n'
 74                +  'rows=' + (rows > 3 ? rows : 3) + '\n\n';
 75        keyfile;
 76                    """);
 77
 78                try {
 79                    keyfile.load_from_data (
 80                        Sokoke.js_script_eval (null, script.str, null),
 81                        -1, 0);
 82                }
 83                catch (GLib.Error eval_error) {
 84                    GLib.critical ("Failed to parse %s as speed dial JSON: %s",
 85                                   fallback ?? (filename + ".json"), eval_error.message);
 86                }
 87                Katze.mkdir_with_parents (
 88                    Path.build_path (Path.DIR_SEPARATOR_S,
 89                                     Environment.get_user_cache_dir (),
 90                                     "midori", "thumbnails"), 0700);
 91
 92                foreach (string tile in keyfile.get_groups ()) {
 93                    try {
 94                        string img = keyfile.get_string (tile, "img");
 95                        string uri = keyfile.get_string (tile, "uri");
 96                        if (img != null && uri[0] != '\0' && uri[0] != '#') {
 97                            uchar[] decoded = Base64.decode (img);
 98                            FileUtils.set_data (Sokoke.build_thumbnail_path (uri), decoded);
 99                        }
100                        keyfile.remove_key (tile, "img");
101                    }
102                    catch (GLib.Error img_error) {
103                        /* img and uri can be missing */
104                    }
105                }
106            }
107        }
108
109        public string get_next_free_slot () {
110            uint slot_count = 0;
111            foreach (string tile in keyfile.get_groups ()) {
112                try {
113                    if (keyfile.has_key (tile, "uri"))
114                        slot_count++;
115                }
116                catch (KeyFileError error) { }
117            }
118
119            uint slot = 1;
120            while (slot <= slot_count) {
121                string tile = "Dial %u".printf (slot);
122                if (!keyfile.has_group (tile))
123                    return "s%u".printf (slot);
124                slot++;
125            }
126            return "s%u".printf (slot_count + 1);
127        }
128
129        public void add (string uri, string title, Gdk.Pixbuf img) {
130            string id = "Dial " + get_next_free_slot ();
131            add_with_id (id, uri, title, img);
132        }
133
134        public void add_with_id (string id, string uri, string title, Gdk.Pixbuf img) {
135            keyfile.set_string (id, "uri", uri);
136            keyfile.set_string (id, "title", title);
137
138            Katze.mkdir_with_parents (Path.build_path (Path.DIR_SEPARATOR_S,
139                Paths.get_cache_dir (), "thumbnails"), 0700);
140            string filename = Sokoke.build_thumbnail_path (uri);
141            try {
142                img.save (filename, "png", null, "compression", "7", null);
143            }
144            catch (Error error) {
145                critical ("Failed to save speed dial thumbnail: %s", error.message);
146            }
147            save ();
148        }
149
150        public unowned string get_html (bool close_buttons_left, GLib.Object view) throws Error {
151            bool load_missing = true;
152
153            if (html != null)
154                return html;
155
156            string? head = null;
157            string filename = Paths.get_res_filename ("speeddial-head.html");
158            if (keyfile != null
159             && FileUtils.get_contents (filename, out head, null)) {
160                string header = head.replace ("{title}", _("Speed Dial")).
161                    replace ("{click_to_add}", _("Click to add a shortcut")).
162                    replace ("{enter_shortcut_address}", _("Enter shortcut address")).
163                    replace ("{enter_shortcut_name}", _("Enter shortcut title")).
164                    replace ("{are_you_sure}", _("Are you sure you want to delete this shortcut?"));
165                var markup = new StringBuilder (header);
166
167                uint slot_count = 1;
168                foreach (string tile in keyfile.get_groups ()) {
169                    try {
170                        if (keyfile.has_key (tile, "uri"))
171                            slot_count++;
172                    }
173                    catch (KeyFileError error) { }
174                }
175
176                /* Try to guess the best X by X grid size */
177                uint grid_index = 3;
178                while ((grid_index * grid_index) < slot_count)
179                    grid_index++;
180
181                /* Percent width size of one slot */
182                uint slot_size = (100 / grid_index);
183
184                /* No editing in private/ app mode or without scripts */
185                markup.append_printf (
186                    "%s<style>.cross { display:none }</style>%s" +
187                    "<style> div.shortcut { height: %d%%; width: %d%%; }</style>\n",
188                    Paths.is_readonly () ? "" : "<noscript>",
189                    Paths.is_readonly () ? "" : "</noscript>",
190                    slot_size + 1, slot_size - 4);
191
192                /* Combined width of slots should always be less than 100%.
193                 * Use half of the remaining percentage as a margin size */
194                uint div_factor;
195                if (slot_size * grid_index >= 100 && grid_index > 4)
196                    div_factor = 8;
197                else
198                    div_factor = 2;
199                uint margin = (100 - ((slot_size - 4) * grid_index)) / div_factor;
200                if (margin > 9)
201                    margin = margin % 10;
202
203                markup.append_printf (
204                    "<style> body { overflow:hidden } #content { margin-left: %u%%; }</style>", margin);
205                if (close_buttons_left)
206                    markup.append_printf (
207                        "<style>.cross { left: -14px }</style>");
208
209                foreach (string tile in keyfile.get_groups ()) {
210                    try {
211                        string uri = keyfile.get_string (tile, "uri");
212                        if (uri != null && uri.str ("://") != null && tile.has_prefix ("Dial ")) {
213                            string title = keyfile.get_string (tile, "title");
214                            string thumb_filename = Sokoke.build_thumbnail_path (uri);
215                            uint slot = tile.substring (5, -1).to_int ();
216                            string encoded;
217                            try {
218                                uint8[] thumb;
219                                FileUtils.get_data (thumb_filename, out thumb);
220                                encoded = Base64.encode (thumb);
221                            }
222                            catch (FileError error) {
223                                encoded = null;
224                                if (load_missing)
225                                    get_thumb (tile, uri);
226                            }
227                            markup.append_printf ("""
228                                <div class="shortcut" id="s%u"><div class="preview">
229                                <a class="cross" href="#" onclick='clearShortcut("s%u");'></a>
230                                <a href="%s"><img src="data:image/png;base64,%s" title='%s'></a>
231                                </div><div class="title" onclick='renameShortcut("s%u");'>%s</div></div>
232                                """,
233                                slot, slot, uri, encoded ?? "", title, slot, title ?? "");
234                        }
235                        else if (tile != "settings")
236                            keyfile.remove_group (tile);
237                    }
238                    catch (KeyFileError error) { }
239                }
240
241                markup.append_printf ("""
242                    <div class="shortcut" id="s%u"><div class="preview new">
243                    <a class="add" href="#" onclick='return getAction("s%u");'></a>
244                    </div><div class="title">%s</div></div>
245                    """,
246                    slot_count + 1, slot_count + 1, _("Click to add a shortcut"));
247                markup.append_printf ("</div>\n</body>\n</html>\n");
248                html = markup.str;
249            }
250            else
251                html = "";
252
253            return html;
254        }
255
256        public void save_message (string message) throws Error {
257            string msg = message.substring (16, -1);
258            string[] parts = msg.split (" ", 4);
259            string action = parts[0];
260
261            if (action == "add" || action == "rename"
262                                || action == "delete" || action == "swap") {
263                uint slot_id = parts[1].to_int () + 1;
264                string dial_id = "Dial %u".printf (slot_id);
265
266                if (action == "delete") {
267                    string uri = keyfile.get_string (dial_id, "uri");
268                    string file_path = Sokoke.build_thumbnail_path (uri);
269                    keyfile.remove_group (dial_id);
270                    FileUtils.unlink (file_path);
271                }
272                else if (action == "add") {
273                    keyfile.set_string (dial_id, "uri", parts[2]);
274                    get_thumb (dial_id, parts[2]);
275                }
276                else if (action == "rename") {
277                    uint offset = parts[0].length + parts[1].length + 2;
278                    string title = msg.substring (offset, -1);
279                    keyfile.set_string (dial_id, "title", title);
280                }
281                else if (action == "swap") {
282                    uint slot2_id = parts[2].to_int () + 1;
283                    string dial2_id = "Dial %u".printf (slot2_id);
284
285                    string uri = keyfile.get_string (dial_id, "uri");
286                    string title = keyfile.get_string (dial_id, "title");
287                    string uri2 = keyfile.get_string (dial2_id, "uri");
288                    string title2 = keyfile.get_string (dial2_id, "title");
289
290                    keyfile.set_string (dial_id, "uri", uri2);
291                    keyfile.set_string (dial2_id, "uri", uri);
292                    keyfile.set_string (dial_id, "title", title2);
293                    keyfile.set_string (dial2_id, "title", title);
294                }
295            }
296            save ();
297        }
298
299        void save () {
300            html = null;
301
302            try {
303                FileUtils.set_contents (filename, keyfile.to_data ());
304            }
305            catch (Error error) {
306                critical ("Failed to update speed dial: %s", error.message);
307            }
308            /* FIXME Refresh all open views */
309        }
310
311        void load_status (GLib.Object thumb_view_, ParamSpec pspec) {
312            if (thumb_view.load_status != WebKit.LoadStatus.FINISHED)
313                return;
314
315            return_if_fail (spec != null);
316            #if HAVE_OFFSCREEN
317            var img = (thumb_view.parent as Gtk.OffscreenWindow).get_pixbuf ();
318            var pixbuf_scaled = img.scale_simple (240, 160, Gdk.InterpType.TILES);
319            img = pixbuf_scaled;
320            #else
321            thumb_view.realize ();
322            var img = midori_view_web_view_get_snapshot (thumb_view, 240, 160);
323            #endif
324            unowned string title = thumb_view.get_title ();
325            add_with_id (spec.dial_id, spec.uri, title ?? spec.uri, img);
326
327            thumb_queue.remove (spec);
328            if (thumb_queue != null && thumb_queue.data != null) {
329                spec = thumb_queue.data;
330                thumb_view.load_uri (spec.uri);
331            }
332            else
333                /* disconnect_by_func (thumb_view, load_status) */;
334        }
335
336        void get_thumb (string dial_id, string uri) {
337            if (thumb_view == null) {
338                thumb_view = new WebKit.WebView ();
339                var settings = new WebKit.WebSettings ();
340                settings. set ("enable-scripts", false,
341                               "enable-plugins", false,
342                               "auto-load-images", true,
343                               "enable-html5-database", false,
344                               "enable-html5-local-storage", false);
345                if (settings.get_class ().find_property ("enable-java-applet") != null)
346                    settings.set ("enable-java-applet", false);
347                thumb_view.settings = settings;
348                #if HAVE_OFFSCREEN
349                var offscreen = new Gtk.OffscreenWindow ();
350                offscreen.add (thumb_view);
351                thumb_view.set_size_request (800, 600);
352                offscreen.show_all ();
353                #else
354                /* What we are doing here is a bit of a hack. In order to render a
355                   thumbnail we need a new view and load the url in it. But it has
356                   to be visible and packed in a container. So we secretly pack it
357                   into the notebook of the parent browser. */
358                notebook.add (thumb_view);
359                thumb_view.destroy.connect (Gtk.widget_destroyed);
360                /* We use an empty label. It's not invisible but hard to spot. */
361                notebook.set_tab_label (thumb_view, new Gtk.EventBox ());
362                thumb_view.show ();
363                #endif
364            }
365
366            thumb_queue.append (new Spec (dial_id, uri));
367            if (thumb_queue.nth_data (1) != null)
368                return;
369
370            spec = thumb_queue.data;
371            thumb_view.notify["load-status"].connect (load_status);
372            thumb_view.load_uri (spec.uri);
373        }
374    }
375}
376