/midori/midori-speeddial.vala
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