1/* 2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program; if not, write to the Free Software 16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 17 * 18 * Authored by Michal Hruby <michal.mhr@gmail.com> 19 * 20 */ 21 22namespace Synapse 23{ 24 public class DesktopFilePlugin : Object, Activatable, ItemProvider, ActionProvider 25 { 26 public bool enabled { get; set; default = true; } 27 28 public void activate () 29 { 30 31 } 32 33 public void deactivate () 34 { 35 36 } 37 38 private class DesktopFileMatch : ApplicationMatch 39 { 40 public DesktopFileInfo desktop_info { get; construct; } 41 public string title_folded { get; construct; } 42 public string title_unaccented { get; construct; } 43 public string desktop_id { get; construct; } 44 public string exec { get; construct; } 45 46 public DesktopFileMatch (DesktopFileInfo info) 47 { 48 Object (desktop_info : info); 49 } 50 51 construct 52 { 53 filename = desktop_info.filename; 54 title = desktop_info.name; 55 description = desktop_info.comment; 56 icon_name = desktop_info.icon_name; 57 exec = desktop_info.exec; 58 needs_terminal = desktop_info.needs_terminal; 59 title_folded = desktop_info.get_name_folded () ?? title.casefold (); 60 title_unaccented = Utils.remove_accents (title_folded); 61 desktop_id = "application://" + desktop_info.desktop_id; 62 } 63 } 64 65 static void register_plugin () 66 { 67 PluginRegistry.get_default ().register_plugin ( 68 typeof (DesktopFilePlugin), 69 "Application Search", 70 _("Search for and run applications on your computer."), 71 "system-run", 72 register_plugin 73 ); 74 } 75 76 static construct 77 { 78 register_plugin (); 79 } 80 81 private Gee.List<DesktopFileMatch> desktop_files; 82 83 construct 84 { 85 desktop_files = new Gee.ArrayList<DesktopFileMatch> (); 86 mimetype_map = new Gee.HashMap<string, Gee.List<OpenWithAction>> (); 87 actions_map = new Gee.HashMap<string, Gee.List<OpenAppAction>> (); 88 89 var dfs = DesktopFileService.get_default (); 90 dfs.reload_started.connect (() => { 91 loading_in_progress = true; 92 }); 93 dfs.reload_done.connect (() => { 94 mimetype_map.clear (); 95 desktop_files.clear (); 96 load_all_desktop_files.begin (); 97 }); 98 99 load_all_desktop_files.begin (); 100 } 101 102 public signal void load_complete (); 103 private bool loading_in_progress = false; 104 105 private async void load_all_desktop_files () 106 { 107 loading_in_progress = true; 108 Idle.add_full (Priority.LOW, load_all_desktop_files.callback); 109 yield; 110 111 var dfs = DesktopFileService.get_default (); 112 113 foreach (DesktopFileInfo dfi in dfs.get_desktop_files ()) 114 { 115 desktop_files.add (new DesktopFileMatch (dfi)); 116 } 117 118 loading_in_progress = false; 119 load_complete (); 120 } 121 122 private int compute_relevancy (DesktopFileMatch dfm, int base_relevancy) 123 { 124 var rs = RelevancyService.get_default (); 125 float popularity = rs.get_application_popularity (dfm.desktop_id); 126 127 int r = RelevancyService.compute_relevancy (base_relevancy, popularity); 128 debug ("relevancy for %s: %d", dfm.desktop_id, r); 129 130 return r; 131 } 132 133 private void full_search (Query q, ResultSet results, 134 MatcherFlags flags = 0) 135 { 136 // try to match against global matchers and if those fail, try also exec 137 var matchers = Query.get_matchers_for_query (q.query_string_folded, 138 flags); 139 140 foreach (var dfm in desktop_files) 141 { 142 unowned string folded_title = dfm.title_folded; 143 unowned string unaccented_title = dfm.title_unaccented; 144 bool matched = false; 145 // FIXME: we need to do much smarter relevancy computation in fuzzy re 146 // "sysmon" matching "System Monitor" is very good as opposed to 147 // "seto" matching "System Monitor" 148 foreach (var matcher in matchers) 149 { 150 if (matcher.key.match (folded_title)) 151 { 152 results.add (dfm, compute_relevancy (dfm, matcher.value)); 153 matched = true; 154 break; 155 } 156 else if (unaccented_title != null && matcher.key.match (unaccented_title)) 157 { 158 results.add (dfm, compute_relevancy (dfm, matcher.value - MatchScore.INCREMENT_SMALL)); 159 matched = true; 160 break; 161 } 162 } 163 if (!matched && dfm.exec.has_prefix (q.query_string)) 164 { 165 results.add (dfm, compute_relevancy (dfm, dfm.exec == q.query_string ? 166 MatchScore.VERY_GOOD : MatchScore.AVERAGE - MatchScore.INCREMENT_SMALL)); 167 } 168 } 169 } 170 171 public bool handles_query (Query q) 172 { 173 // we only search for applications 174 if (!(QueryFlags.APPLICATIONS in q.query_type)) return false; 175 if (q.query_string.strip () == "") return false; 176 177 return true; 178 } 179 180 public async ResultSet? search (Query q) throws SearchError 181 { 182 if (loading_in_progress) 183 { 184 // wait 185 ulong signal_id = this.load_complete.connect (() => { 186 search.callback (); 187 }); 188 yield; 189 SignalHandler.disconnect (this, signal_id); 190 } 191 else 192 { 193 // we'll do this so other plugins can send their DBus requests etc. 194 // and they don't have to wait for our blocking (though fast) search 195 // to finish 196 Idle.add_full (Priority.HIGH_IDLE, search.callback); 197 yield; 198 } 199 200 q.check_cancellable (); 201 202 // FIXME: spawn new thread and do the search there? 203 var result = new ResultSet (); 204 205 if (q.query_string.char_count () == 1) 206 { 207 var flags = MatcherFlags.NO_SUBSTRING | MatcherFlags.NO_PARTIAL | 208 MatcherFlags.NO_FUZZY; 209 full_search (q, result, flags); 210 } 211 else 212 { 213 full_search (q, result); 214 } 215 216 q.check_cancellable (); 217 218 return result; 219 } 220 221 private class OpenWithAction : Action 222 { 223 public DesktopFileInfo desktop_info { get; construct; } 224 225 public OpenWithAction (DesktopFileInfo info) 226 { 227 Object (desktop_info : info); 228 } 229 230 construct 231 { 232 title = _("Open with %s").printf (desktop_info.name); 233 icon_name = desktop_info.icon_name; 234 description = _("Opens current selection using %s").printf (desktop_info.name); 235 } 236 237 public override void do_execute (Match match, Match? target = null) 238 { 239 unowned UriMatch? uri_match = match as UriMatch; 240 return_if_fail (uri_match != null); 241 242 var f = File.new_for_uri (uri_match.uri); 243 try 244 { 245 var app_info = new DesktopAppInfo.from_filename (desktop_info.filename); 246 List<File> files = new List<File> (); 247 files.prepend (f); 248 app_info.launch (files, Gdk.Display.get_default ().get_app_launch_context ()); 249 } 250 catch (Error err) 251 { 252 warning ("%s", err.message); 253 } 254 } 255 256 public override bool valid_for_match (Match match) 257 { 258 return (match is UriMatch); 259 } 260 } 261 262 private class OpenAppAction : Action 263 { 264 public DesktopFileInfo desktop_info { get; construct; } 265 public string action { get; construct; } 266 267 DesktopAppInfo app_info; 268 269 public OpenAppAction (DesktopFileInfo info, string action) 270 { 271 Object (desktop_info : info, action : action); 272 } 273 274 construct 275 { 276 app_info = new DesktopAppInfo.from_filename (desktop_info.filename); 277 var display_action = app_info.get_action_name (action); 278 title = display_action; 279 icon_name = desktop_info.icon_name; 280 description = _("Launch action '%s'").printf (display_action); 281 } 282 283 public override void do_execute (Match match, Match? target = null) 284 { 285 app_info.launch_action (action, Gdk.Display.get_default ().get_app_launch_context ()); 286 RelevancyService.get_default ().application_launched (app_info); 287 } 288 289 public override bool valid_for_match (Match match) 290 { 291 return (match is DesktopFileMatch); 292 } 293 } 294 295 private Gee.Map<string, Gee.List<OpenWithAction>> mimetype_map; 296 private Gee.Map<string, Gee.List<OpenAppAction>> actions_map; 297 298 public ResultSet? find_for_match (ref Query query, Match match) 299 { 300 unowned UriMatch? uri_match = null; 301 unowned DesktopFileMatch? app_match = null; 302 Gee.List<Action>? any_list = null; 303 304 if ((uri_match = match as UriMatch) != null) 305 { 306 var dfs = DesktopFileService.get_default (); 307 var list_for_mimetype = dfs.get_desktop_files_for_type (uri_match.mime_type); 308 /* If there's more than one application, fill the ow list */ 309 if (list_for_mimetype.size > 1) 310 { 311 /* Query DesktopFileService only if is necessary */ 312 Gee.List<OpenWithAction>? ow_list = mimetype_map[uri_match.mime_type]; 313 if (ow_list == null) 314 { 315 ow_list = new Gee.LinkedList<OpenWithAction> (); 316 mimetype_map[uri_match.mime_type] = ow_list; 317 318 foreach (var entry in list_for_mimetype) 319 { 320 ow_list.add (new OpenWithAction (entry)); 321 } 322 } 323 324 any_list = ow_list; 325 } 326 } 327 else if ((app_match = match as DesktopFileMatch) != null) 328 { 329 Gee.List<OpenAppAction>? oa_list = actions_map[app_match.filename]; 330 if (oa_list == null) 331 { 332 oa_list = new Gee.LinkedList<OpenAppAction> (); 333 actions_map[app_match.filename] = oa_list; 334 335 var dfs = DesktopFileService.get_default (); 336 var desktop_file_info = dfs.get_desktop_file_for_id (Path.get_basename (app_match.filename)); 337 338 /* There should at a result here */ 339 if (desktop_file_info != null) 340 { 341 foreach (var action in desktop_file_info.actions) 342 { 343 oa_list.add (new OpenAppAction (desktop_file_info, action)); 344 } 345 } 346 else 347 { 348 warning ("No DesktopInfoFile for %s", app_match.filename); 349 } 350 } 351 352 any_list = oa_list; 353 } 354 355 if (any_list == null || any_list.size == 0) return null; 356 357 var rs = new ResultSet (); 358 359 if (query.query_string == "") 360 { 361 foreach (var action in any_list) 362 { 363 rs.add (action, MatchScore.POOR); 364 } 365 } 366 else 367 { 368 var matchers = Query.get_matchers_for_query (query.query_string, 0, 369 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); 370 foreach (var action in any_list) 371 { 372 foreach (var matcher in matchers) 373 { 374 if (matcher.key.match (action.title)) 375 { 376 rs.add (action, matcher.value); 377 break; 378 } 379 } 380 } 381 } 382 383 return rs; 384 } 385 } 386} 387