Merge lp:~elementary-apps/slingshot/synapse-desktop-only into lp:~elementary-pantheon/slingshot/trunk
- synapse-desktop-only
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Cody Garver |
Approved revision: | 440 |
Merged at revision: | 422 |
Proposed branch: | lp:~elementary-apps/slingshot/synapse-desktop-only |
Merge into: | lp:~elementary-pantheon/slingshot/trunk |
Diff against target: |
5927 lines (+5108/-286) 27 files modified
CMakeLists.txt (+16/-3) lib/synapse-core/CMakeLists.txt (+54/-0) lib/synapse-core/common-actions.vala (+419/-0) lib/synapse-core/config-service.vala (+192/-0) lib/synapse-core/data-sink.vala (+574/-0) lib/synapse-core/dbus-service.vala (+166/-0) lib/synapse-core/desktop-file-service.vala (+635/-0) lib/synapse-core/match.vala (+144/-0) lib/synapse-core/plugin.vala (+59/-0) lib/synapse-core/query.vala (+296/-0) lib/synapse-core/relevancy-backend-zg.vala (+308/-0) lib/synapse-core/relevancy-service.vala (+95/-0) lib/synapse-core/result-set.vala (+121/-0) lib/synapse-core/utils.vala (+439/-0) lib/synapse-core/volume-service.vala (+193/-0) lib/synapse-plugins/CMakeLists.txt (+46/-0) lib/synapse-plugins/command-plugin.vala (+188/-0) lib/synapse-plugins/desktop-file-plugin.vala (+350/-0) src/Backend/App.vala (+81/-11) src/Backend/AppSystem.vala (+8/-6) src/Backend/SynapseSearch.vala (+163/-0) src/Slingshot.vala (+11/-11) src/SlingshotView.vala (+130/-54) src/Widgets/CategoryView.vala (+2/-2) src/Widgets/LargeSearchEntry.vala (+73/-0) src/Widgets/SearchItem.vala (+51/-50) src/Widgets/SearchView.vala (+294/-149) |
To merge this branch: | bzr merge lp:~elementary-apps/slingshot/synapse-desktop-only |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Danielle Foré | Approve | ||
Sergey "Shnatsel" Davidoff (community) | ux opinion | Disapprove | |
Review via email: mp+222908@code.launchpad.net |
Commit message
Copy the sources of Synapse over and implements a new searchview using libsynapse
Copying is currently necessary as synapse's library is internal. I hope we can change this soon, although I don't consider it very urgent, as synapse is almost unmaintained at the moment, so we most likely won't miss out on important updates.
Description of the change
This branch copies the sources of synapse over to slingshot and implements a new searchview using libsynapse.
Copying is currently necessary as synapse's library is internal. I hope we can change this soon, although I don't consider it very urgent, as synapse is almost unmaintained at the moment, so we most likely won't miss out on important updates.
There is currently some unused code flying around in that branch, that's because Dan requested to only add the desktop-file-plugin and the command-plugin for now, so we can have something to merge. The features that are currently inaccessible are mainly the context view, which would pull up context actions when you press "tab" on a result and helper stuff like grabbing favicons for internet results, which currently don't get as both the bookmarks plugin and the zeitgeist plugin are disabled.
I would prefer keeping this code though, for one removing it would probably require more work than fixing it (admittedly, testing is where the actual work would currently be needed, but still) and also I think it's all backend related, so at least the code could already be checked for this review.
- 438. By Tom Beckmann
-
more code style fixes
Cody Garver (codygarver) wrote : | # |
Cody Garver (codygarver) wrote : | # |
lib/synapse-
lib/synapse-
lib/synapse-
Cody Garver (codygarver) wrote : | # |
lib/synapse-
add_definitions
lib/synapse-
add_definitions
Sergey "Shnatsel" Davidoff (shnatsel) wrote : | # |
Design-wise, I'd prefer Slingshot to search not yet installed applications when I enter something in the search bar. For example, if I type "GIMP" on a clean system right now, I get nothing and have to go to software center, re-type "GIMP", click install, wait for it to complete, close software center, go to slingshot again, type "GIMP" again, click it. I believe this could be streamlined a lot by searching not yet installed applications and showing them *after* the installed ones with "More info..." and "Install" buttons on it, for people unfamiliar and familiar with the program, respectively.
Such functionality is much more relevant to an application launcher and leaves no room for Synapse, thus I believe that Synapse belongs to its own indicator.
- 439. By Tom Beckmann
-
disable c warnings
Tom Beckmann (tombeckmann) wrote : | # |
@Cody, added -w now. I'd prefer to keep the files by synapse as they are for now, as the final plan is to not have the sources in our project anyway, but if you want me to fix those warnings I can do that too.
- 440. By Tom Beckmann
-
actually enable zeitgeist relevancy
Danielle Foré (danrabbit) wrote : | # |
Cody says he's good on the code side.
I'm good design side. This branch is made to lay a foundation, not introduce any new features yet. We can address additional feature requests in later branches.
Preview Diff
1 | === modified file 'CMakeLists.txt' |
2 | --- CMakeLists.txt 2014-05-28 08:48:51 +0000 |
3 | +++ CMakeLists.txt 2014-06-16 07:45:32 +0000 |
4 | @@ -54,7 +54,7 @@ |
5 | message ("-- Zeitgeist integration disabled") |
6 | endif () |
7 | |
8 | -set (CORE_DEPS "gobject-2.0;glib-2.0;gio-2.0;gio-unix-2.0;gee-0.8;libgnome-menu-3.0;${UNITY_DEPS};") |
9 | +set (CORE_DEPS "gobject-2.0;glib-2.0;gio-2.0;gio-unix-2.0;libsoup-2.4;gee-0.8;libgnome-menu-3.0;json-glib-1.0;${UNITY_DEPS};") |
10 | set (UI_DEPS "gtk+-3.0>=3.10.0;granite;${ZEITGEIST_DEPS};") |
11 | |
12 | find_package (PkgConfig) |
13 | @@ -74,8 +74,10 @@ |
14 | src/Backend/DBusService.vala |
15 | src/Backend/App.vala |
16 | src/Backend/RelevancyService.vala |
17 | + src/Backend/SynapseSearch.vala |
18 | src/Widgets/AppEntry.vala |
19 | src/Widgets/Grid.vala |
20 | + src/Widgets/LargeSearchEntry.vala |
21 | src/Widgets/Switcher.vala |
22 | src/Widgets/SearchView.vala |
23 | src/Widgets/SearchItem.vala |
24 | @@ -84,15 +86,25 @@ |
25 | PACKAGES |
26 | ${CORE_DEPS} |
27 | ${UI_DEPS} |
28 | + synapse-core |
29 | + synapse-plugins |
30 | CUSTOM_VAPIS |
31 | vapi/config.vapi |
32 | OPTIONS |
33 | --thread |
34 | + --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core |
35 | + --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-plugins |
36 | -g |
37 | ${UNITY_OPTIONS} |
38 | ${ZEITGEIST_OPTIONS} |
39 | ) |
40 | |
41 | +include_directories(${CMAKE_BINARY_DIR}/lib/synapse-core) |
42 | +include_directories(${CMAKE_BINARY_DIR}/lib/synapse-plugins) |
43 | + |
44 | +add_subdirectory(lib/synapse-core) |
45 | +add_subdirectory(lib/synapse-plugins) |
46 | + |
47 | # Comment this out to enable C compiler warnings |
48 | add_definitions (-w) |
49 | |
50 | @@ -101,7 +113,7 @@ |
51 | link_directories (${DEPS_LIBRARY_DIRS}) |
52 | |
53 | add_executable (${APPNAME} ${VALA_C}) |
54 | -target_link_libraries(${APPNAME} m) |
55 | +target_link_libraries(${APPNAME} m synapse-core synapse-plugins) |
56 | |
57 | # Installation |
58 | install (TARGETS ${APPNAME} RUNTIME DESTINATION bin) |
59 | @@ -114,4 +126,5 @@ |
60 | add_schema ("org.pantheon.desktop.slingshot.gschema.xml") |
61 | |
62 | # Translations |
63 | -add_subdirectory (po) |
64 | \ No newline at end of file |
65 | +add_subdirectory (po) |
66 | + |
67 | |
68 | === added directory 'lib' |
69 | === added directory 'lib/synapse-core' |
70 | === added file 'lib/synapse-core/CMakeLists.txt' |
71 | --- lib/synapse-core/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
72 | +++ lib/synapse-core/CMakeLists.txt 2014-06-16 07:45:32 +0000 |
73 | @@ -0,0 +1,54 @@ |
74 | +set(CORE_LIB_VERSION 0.1) |
75 | +set(CORE_LIB_SOVERSION 0) |
76 | +set(CORE_LIBRARY_NAME synapse-core) |
77 | +set(CORE_PKG |
78 | + glib-2.0 |
79 | + zeitgeist-1.0 |
80 | + gio-unix-2.0 |
81 | + json-glib-1.0 |
82 | + gee-0.8 |
83 | + gtk+-3.0 |
84 | +) |
85 | + |
86 | +pkg_check_modules(CORE_DEPS REQUIRED ${CORE_PKG}) |
87 | + |
88 | +set(CORE_SOURCE |
89 | + common-actions.vala |
90 | + config-service.vala |
91 | + data-sink.vala |
92 | + dbus-service.vala |
93 | + desktop-file-service.vala |
94 | + match.vala |
95 | + plugin.vala |
96 | + query.vala |
97 | + relevancy-backend-zg.vala |
98 | + relevancy-service.vala |
99 | + result-set.vala |
100 | + utils.vala |
101 | + volume-service.vala |
102 | +) |
103 | + |
104 | +set(LINK_MODE STATIC) |
105 | + |
106 | +vala_precompile(CORE_VALA_C ${CORE_LIBRARY_NAME} |
107 | + ${CORE_SOURCE} |
108 | +PACKAGES |
109 | + ${CORE_PKG} |
110 | +GENERATE_VAPI |
111 | + ${CORE_LIBRARY_NAME} |
112 | +GENERATE_HEADER |
113 | + ${CORE_LIBRARY_NAME} |
114 | +) |
115 | + |
116 | +add_definitions(${CORE_DEPS_CFLAGS} -include config.h -w) |
117 | +link_directories(${CORE_DEPS_LIBRARY_DIRS}) |
118 | + |
119 | +add_library(${CORE_LIBRARY_NAME} STATIC ${CORE_VALA_C}) |
120 | +target_link_libraries (${CORE_LIBRARY_NAME} ${CORE_DEPS_LIBRARIES}) |
121 | + |
122 | +set_target_properties(${CORE_LIBRARY_NAME} PROPERTIES |
123 | + OUTPUT_NAME ${CORE_LIBRARY_NAME} |
124 | + VERSION ${CORE_LIB_VERSION} |
125 | + SOVERSION ${CORE_LIB_SOVERSION} |
126 | +) |
127 | + |
128 | |
129 | === added file 'lib/synapse-core/common-actions.vala' |
130 | --- lib/synapse-core/common-actions.vala 1970-01-01 00:00:00 +0000 |
131 | +++ lib/synapse-core/common-actions.vala 2014-06-16 07:45:32 +0000 |
132 | @@ -0,0 +1,419 @@ |
133 | +/* |
134 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
135 | + * |
136 | + * This library is free software; you can redistribute it and/or |
137 | + * modify it under the terms of the GNU Lesser General Public |
138 | + * License as published by the Free Software Foundation; either |
139 | + * version 2.1 of the License, or (at your option) any later version. |
140 | + * |
141 | + * This library is distributed in the hope that it will be useful, |
142 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
143 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
144 | + * Lesser General Public License for more details. |
145 | + * |
146 | + * You should have received a copy of the GNU Lesser General Public License |
147 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
148 | + * |
149 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
150 | + * |
151 | + */ |
152 | + |
153 | +namespace Synapse |
154 | +{ |
155 | + public abstract class BaseAction: Object, Match |
156 | + { |
157 | + // from Match interface |
158 | + public string title { get; construct set; } |
159 | + public string description { get; set; } |
160 | + public string icon_name { get; construct set; } |
161 | + public bool has_thumbnail { get; construct set; } |
162 | + public string thumbnail_path { get; construct set; } |
163 | + public MatchType match_type { get; construct set; } |
164 | + |
165 | + public int default_relevancy { get; set; } |
166 | + public bool notify_match { get; set; default = true; } |
167 | + |
168 | + public abstract bool valid_for_match (Match match); |
169 | + public virtual int get_relevancy_for_match (Match match) |
170 | + { |
171 | + return default_relevancy; |
172 | + } |
173 | + |
174 | + public abstract void do_execute (Match? source, Match? target = null); |
175 | + public void execute_with_target (Match? source, Match? target = null) |
176 | + { |
177 | + do_execute (source, target); |
178 | + if (notify_match) source.executed (); |
179 | + } |
180 | + |
181 | + public virtual bool needs_target () { |
182 | + return false; |
183 | + } |
184 | + |
185 | + public virtual QueryFlags target_flags () |
186 | + { |
187 | + return QueryFlags.ALL; |
188 | + } |
189 | + } |
190 | + |
191 | + public class CommonActions: Object, Activatable, ActionProvider |
192 | + { |
193 | + public bool enabled { get; set; default = true; } |
194 | + |
195 | + public void activate () |
196 | + { |
197 | + |
198 | + } |
199 | + |
200 | + public void deactivate () |
201 | + { |
202 | + |
203 | + } |
204 | + |
205 | + private class Runner: BaseAction |
206 | + { |
207 | + public Runner () |
208 | + { |
209 | + Object (title: _ ("Run"), |
210 | + description: _ ("Run an application, action or script"), |
211 | + icon_name: "system-run", has_thumbnail: false, |
212 | + match_type: MatchType.ACTION, |
213 | + default_relevancy: Match.Score.EXCELLENT); |
214 | + } |
215 | + |
216 | + public override void do_execute (Match? match, Match? target = null) |
217 | + { |
218 | + if (match.match_type == MatchType.APPLICATION) |
219 | + { |
220 | + ApplicationMatch? app_match = match as ApplicationMatch; |
221 | + return_if_fail (app_match != null); |
222 | + |
223 | + AppInfo app = app_match.app_info ?? |
224 | + new DesktopAppInfo.from_filename (app_match.filename); |
225 | + |
226 | + try |
227 | + { |
228 | + var display = Gdk.Display.get_default (); |
229 | + app.launch (null, display.get_app_launch_context ()); |
230 | + |
231 | + RelevancyService.get_default ().application_launched (app); |
232 | + } |
233 | + catch (Error err) |
234 | + { |
235 | + Utils.Logger.warning (this, "%s", err.message); |
236 | + } |
237 | + } |
238 | + else // MatchType.ACTION |
239 | + { |
240 | + match.execute (null); |
241 | + } |
242 | + } |
243 | + |
244 | + public override bool valid_for_match (Match match) |
245 | + { |
246 | + switch (match.match_type) |
247 | + { |
248 | + case MatchType.SEARCH: |
249 | + return true; |
250 | + case MatchType.ACTION: |
251 | + return true; |
252 | + case MatchType.APPLICATION: |
253 | + ApplicationMatch? am = match as ApplicationMatch; |
254 | + return am == null || !am.needs_terminal; |
255 | + default: |
256 | + return false; |
257 | + } |
258 | + } |
259 | + } |
260 | + |
261 | + private class TerminalRunner: BaseAction |
262 | + { |
263 | + public TerminalRunner () |
264 | + { |
265 | + Object (title: _ ("Run in Terminal"), |
266 | + description: _ ("Run application or command in terminal"), |
267 | + icon_name: "terminal", has_thumbnail: false, |
268 | + match_type: MatchType.ACTION, |
269 | + default_relevancy: Match.Score.BELOW_AVERAGE); |
270 | + } |
271 | + |
272 | + public override void do_execute (Match? match, Match? target = null) |
273 | + { |
274 | + if (match.match_type == MatchType.APPLICATION) |
275 | + { |
276 | + ApplicationMatch? app_match = match as ApplicationMatch; |
277 | + return_if_fail (app_match != null); |
278 | + |
279 | + AppInfo original = app_match.app_info ?? |
280 | + new DesktopAppInfo.from_filename (app_match.filename); |
281 | + |
282 | + try |
283 | + { |
284 | + AppInfo app = AppInfo.create_from_commandline ( |
285 | + original.get_commandline (), original.get_name (), |
286 | + AppInfoCreateFlags.NEEDS_TERMINAL); |
287 | + var display = Gdk.Display.get_default (); |
288 | + app.launch (null, display.get_app_launch_context ()); |
289 | + } |
290 | + catch (Error err) |
291 | + { |
292 | + Utils.Logger.warning (this, "%s", err.message); |
293 | + } |
294 | + } |
295 | + } |
296 | + |
297 | + public override bool valid_for_match (Match match) |
298 | + { |
299 | + switch (match.match_type) |
300 | + { |
301 | + case MatchType.APPLICATION: |
302 | + ApplicationMatch? am = match as ApplicationMatch; |
303 | + return am != null; |
304 | + default: |
305 | + return false; |
306 | + } |
307 | + } |
308 | + } |
309 | + |
310 | + private class Opener: BaseAction |
311 | + { |
312 | + public Opener () |
313 | + { |
314 | + Object (title: _ ("Open"), |
315 | + description: _ ("Open using default application"), |
316 | + icon_name: "fileopen", has_thumbnail: false, |
317 | + match_type: MatchType.ACTION, |
318 | + default_relevancy: Match.Score.GOOD); |
319 | + } |
320 | + |
321 | + public override void do_execute (Match? match, Match? target = null) |
322 | + { |
323 | + UriMatch uri_match = match as UriMatch; |
324 | + |
325 | + if (uri_match != null) |
326 | + { |
327 | + CommonActions.open_uri (uri_match.uri); |
328 | + } |
329 | + else if (file_path.match (match.title)) |
330 | + { |
331 | + File f; |
332 | + if (match.title.has_prefix ("~")) |
333 | + { |
334 | + f = File.new_for_path (Path.build_filename (Environment.get_home_dir (), |
335 | + match.title.substring (1), |
336 | + null)); |
337 | + } |
338 | + else |
339 | + { |
340 | + f = File.new_for_path (match.title); |
341 | + } |
342 | + CommonActions.open_uri (f.get_uri ()); |
343 | + } |
344 | + else |
345 | + { |
346 | + CommonActions.open_uri (match.title); |
347 | + } |
348 | + } |
349 | + |
350 | + public override bool valid_for_match (Match match) |
351 | + { |
352 | + switch (match.match_type) |
353 | + { |
354 | + case MatchType.GENERIC_URI: |
355 | + return true; |
356 | + case MatchType.UNKNOWN: |
357 | + return web_uri.match (match.title) || file_path.match (match.title); |
358 | + default: |
359 | + return false; |
360 | + } |
361 | + } |
362 | + |
363 | + private Regex web_uri; |
364 | + private Regex file_path; |
365 | + |
366 | + construct |
367 | + { |
368 | + try |
369 | + { |
370 | + web_uri = new Regex ("^(ftp|http(s)?)://[^.]+\\.[^.]+", RegexCompileFlags.OPTIMIZE); |
371 | + file_path = new Regex ("^(/|~/)[^/]+", RegexCompileFlags.OPTIMIZE); |
372 | + } |
373 | + catch (Error err) |
374 | + { |
375 | + Utils.Logger.warning (this, "%s", err.message); |
376 | + } |
377 | + } |
378 | + } |
379 | + |
380 | + private class OpenFolder: BaseAction |
381 | + { |
382 | + public OpenFolder () |
383 | + { |
384 | + Object (title: _ ("Open folder"), |
385 | + description: _ ("Open folder containing this file"), |
386 | + icon_name: "folder-open", has_thumbnail: false, |
387 | + match_type: MatchType.ACTION, |
388 | + default_relevancy: Match.Score.AVERAGE); |
389 | + } |
390 | + |
391 | + public override void do_execute (Match? match, Match? target = null) |
392 | + { |
393 | + UriMatch uri_match = match as UriMatch; |
394 | + return_if_fail (uri_match != null); |
395 | + var f = File.new_for_uri (uri_match.uri); |
396 | + f = f.get_parent (); |
397 | + try |
398 | + { |
399 | + var app_info = f.query_default_handler (null); |
400 | + List<File> files = new List<File> (); |
401 | + files.prepend (f); |
402 | + var display = Gdk.Display.get_default (); |
403 | + app_info.launch (files, display.get_app_launch_context ()); |
404 | + } |
405 | + catch (Error err) |
406 | + { |
407 | + Utils.Logger.warning (this, "%s", err.message); |
408 | + } |
409 | + } |
410 | + |
411 | + public override bool valid_for_match (Match match) |
412 | + { |
413 | + if (match.match_type != MatchType.GENERIC_URI) return false; |
414 | + UriMatch uri_match = match as UriMatch; |
415 | + var f = File.new_for_uri (uri_match.uri); |
416 | + var parent = f.get_parent (); |
417 | + return parent != null && f.is_native (); |
418 | + } |
419 | + } |
420 | + |
421 | + private class ClipboardCopy: BaseAction |
422 | + { |
423 | + public ClipboardCopy () |
424 | + { |
425 | + Object (title: _ ("Copy to Clipboard"), |
426 | + description: _ ("Copy selection to clipboard"), |
427 | + icon_name: "gtk-copy", has_thumbnail: false, |
428 | + match_type: MatchType.ACTION, |
429 | + default_relevancy: Match.Score.AVERAGE); |
430 | + } |
431 | + |
432 | + public override void do_execute (Match? match, Match? target = null) |
433 | + { |
434 | + var cb = Gtk.Clipboard.get (Gdk.Atom.NONE); |
435 | + if (match.match_type == MatchType.GENERIC_URI) |
436 | + { |
437 | + UriMatch uri_match = match as UriMatch; |
438 | + return_if_fail (uri_match != null); |
439 | + |
440 | + /* |
441 | + // just wow, Gtk and also Vala are trying really hard to make this hard to do... |
442 | + Gtk.TargetEntry[] no_entries = {}; |
443 | + Gtk.TargetList l = new Gtk.TargetList (no_entries); |
444 | + l.add_uri_targets (0); |
445 | + l.add_text_targets (0); |
446 | + Gtk.TargetEntry te = Gtk.target_table_new_from_list (l, 2); |
447 | + cb.set_with_data (); |
448 | + */ |
449 | + cb.set_text (uri_match.uri, -1); |
450 | + } |
451 | + else if (match.match_type == MatchType.TEXT) |
452 | + { |
453 | + TextMatch? text_match = match as TextMatch; |
454 | + string content = text_match != null ? text_match.get_text () : match.title; |
455 | + |
456 | + cb.set_text (content, -1); |
457 | + } |
458 | + } |
459 | + |
460 | + public override bool valid_for_match (Match match) |
461 | + { |
462 | + switch (match.match_type) |
463 | + { |
464 | + case MatchType.GENERIC_URI: |
465 | + return true; |
466 | + case MatchType.TEXT: |
467 | + return true; |
468 | + default: |
469 | + return false; |
470 | + } |
471 | + } |
472 | + |
473 | + public override int get_relevancy_for_match (Match match) |
474 | + { |
475 | + TextMatch? text_match = match as TextMatch; |
476 | + if (text_match != null && text_match.text_origin == TextOrigin.CLIPBOARD) |
477 | + { |
478 | + return 0; |
479 | + } |
480 | + |
481 | + return default_relevancy; |
482 | + } |
483 | + } |
484 | + |
485 | + private Gee.List<BaseAction> actions; |
486 | + |
487 | + construct |
488 | + { |
489 | + actions = new Gee.ArrayList<BaseAction> (); |
490 | + |
491 | + actions.add (new Runner ()); |
492 | + actions.add (new TerminalRunner ()); |
493 | + actions.add (new Opener ()); |
494 | + actions.add (new OpenFolder ()); |
495 | + actions.add (new ClipboardCopy ()); |
496 | + } |
497 | + |
498 | + public ResultSet? find_for_match (ref Query query, Match match) |
499 | + { |
500 | + bool query_empty = query.query_string == ""; |
501 | + var results = new ResultSet (); |
502 | + |
503 | + if (query_empty) |
504 | + { |
505 | + foreach (var action in actions) |
506 | + { |
507 | + if (action.valid_for_match (match)) |
508 | + { |
509 | + results.add (action, action.get_relevancy_for_match (match)); |
510 | + } |
511 | + } |
512 | + } |
513 | + else |
514 | + { |
515 | + var matchers = Query.get_matchers_for_query (query.query_string, 0, |
516 | + RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); |
517 | + foreach (var action in actions) |
518 | + { |
519 | + if (!action.valid_for_match (match)) continue; |
520 | + foreach (var matcher in matchers) |
521 | + { |
522 | + if (matcher.key.match (action.title)) |
523 | + { |
524 | + results.add (action, matcher.value); |
525 | + break; |
526 | + } |
527 | + } |
528 | + } |
529 | + } |
530 | + |
531 | + return results; |
532 | + } |
533 | + |
534 | + public static void open_uri (string uri) |
535 | + { |
536 | + var f = File.new_for_uri (uri); |
537 | + try |
538 | + { |
539 | + var app_info = f.query_default_handler (null); |
540 | + List<File> files = new List<File> (); |
541 | + files.prepend (f); |
542 | + var display = Gdk.Display.get_default (); |
543 | + app_info.launch (files, display.get_app_launch_context ()); |
544 | + } |
545 | + catch (Error err) |
546 | + { |
547 | + Utils.Logger.warning (null, "%s", err.message); |
548 | + } |
549 | + } |
550 | + } |
551 | +} |
552 | |
553 | === added file 'lib/synapse-core/config-service.vala' |
554 | --- lib/synapse-core/config-service.vala 1970-01-01 00:00:00 +0000 |
555 | +++ lib/synapse-core/config-service.vala 2014-06-16 07:45:32 +0000 |
556 | @@ -0,0 +1,192 @@ |
557 | +/* |
558 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
559 | + * |
560 | + * This library is free software; you can redistribute it and/or |
561 | + * modify it under the terms of the GNU Lesser General Public |
562 | + * License as published by the Free Software Foundation; either |
563 | + * version 2 of the License, or (at your option) any later version. |
564 | + * |
565 | + * This library is distributed in the hope that it will be useful, |
566 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
567 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
568 | + * Lesser General Public License for more details. |
569 | + * |
570 | + * You should have received a copy of the GNU Lesser General Public License |
571 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
572 | + * |
573 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
574 | + * |
575 | + */ |
576 | + |
577 | +using Json; |
578 | + |
579 | +namespace Synapse |
580 | +{ |
581 | + public abstract class ConfigObject : GLib.Object |
582 | + { |
583 | + } |
584 | + |
585 | + public class ConfigService : GLib.Object |
586 | + { |
587 | + // singleton that can be easily destroyed |
588 | + private static unowned ConfigService? instance; |
589 | + public static ConfigService get_default () |
590 | + { |
591 | + return instance ?? new ConfigService (); |
592 | + } |
593 | + |
594 | + private ConfigService () |
595 | + { |
596 | + } |
597 | + |
598 | + ~ConfigService () |
599 | + { |
600 | + // useless cause the timer takes a reference on self |
601 | + if (save_timer_id != 0) save (); |
602 | + instance = null; |
603 | + } |
604 | + |
605 | + private Json.Node root_node; |
606 | + private string config_file_name; |
607 | + private uint save_timer_id = 0; |
608 | + |
609 | + construct |
610 | + { |
611 | + instance = this; |
612 | + |
613 | + var parser = new Parser (); |
614 | + config_file_name = |
615 | + GLib.Path.build_filename (Environment.get_user_config_dir (), "synapse", |
616 | + "config.json"); |
617 | + try |
618 | + { |
619 | + parser.load_from_file (config_file_name); |
620 | + root_node = parser.get_root ().copy (); |
621 | + if (root_node.get_node_type () != NodeType.OBJECT) |
622 | + { |
623 | + root_node = new Json.Node (NodeType.OBJECT); |
624 | + root_node.take_object (new Json.Object ()); |
625 | + } |
626 | + } |
627 | + catch (Error err) |
628 | + { |
629 | + root_node = new Json.Node (NodeType.OBJECT); |
630 | + root_node.take_object (new Json.Object ()); |
631 | + } |
632 | + } |
633 | + |
634 | + /** |
635 | + * Creates an instance of an object derived from ConfigObject class, which |
636 | + * will have its public properties set to values stored in config file, or |
637 | + * to the default values if this object wasn't yet stored. |
638 | + * |
639 | + * @param group A group name. |
640 | + * @param key A key name. |
641 | + * @param config_type Type of the object (must be subclass of ConfigObject) |
642 | + * @return An instance of config_type. |
643 | + */ |
644 | + public ConfigObject get_config (string group, string key, Type config_type) |
645 | + { |
646 | + unowned Json.Object obj = root_node.get_object (); |
647 | + unowned Json.Node group_node = obj.get_member (group); |
648 | + if (group_node != null) |
649 | + { |
650 | + if (group_node.get_node_type () == NodeType.OBJECT) |
651 | + { |
652 | + unowned Json.Object group_obj = group_node.get_object (); |
653 | + unowned Json.Node key_node = group_obj.get_member (key); |
654 | + if (key_node != null && key_node.get_node_type () == NodeType.OBJECT) |
655 | + { |
656 | + var result = Json.gobject_deserialize (config_type, key_node); |
657 | + return result as ConfigObject; |
658 | + } |
659 | + } |
660 | + } |
661 | + |
662 | + return GLib.Object.new (config_type) as ConfigObject; |
663 | + } |
664 | + |
665 | + /** |
666 | + * Behaves in a similar way to get_config, but it also watches for changes |
667 | + * in the returned config object and saves them back to the config file |
668 | + * (without the need of calling set_config). |
669 | + * |
670 | + * @param group A group name. |
671 | + * @param key A key name. |
672 | + * @param config_type Type of the object (must be subclass of ConfigObject) |
673 | + */ |
674 | + public ConfigObject bind_config (string group, string key, Type config_type) |
675 | + { |
676 | + ConfigObject config_object = get_config (group, key, config_type); |
677 | + // make sure the lambda doesn't take a ref on the config_object |
678 | + unowned ConfigObject co = config_object; |
679 | + co.notify.connect (() => { this.set_config (group, key, co); }); |
680 | + return config_object; |
681 | + } |
682 | + |
683 | + /** |
684 | + * Stores all public properties of the object to the config file under |
685 | + * specified group and key names. |
686 | + * |
687 | + * @param group A group name. |
688 | + * @param key A key name. |
689 | + * @param cfg_obj ConfigObject instance. |
690 | + */ |
691 | + public void set_config (string group, string key, ConfigObject cfg_obj) |
692 | + { |
693 | + unowned Json.Object obj = root_node.get_object (); |
694 | + if (!obj.has_member (group) || |
695 | + obj.get_member (group).get_node_type () != NodeType.OBJECT) |
696 | + { |
697 | + // why set_object_member works, but set_member doesn't ?! |
698 | + obj.set_object_member (group, new Json.Object ()); |
699 | + } |
700 | + |
701 | + unowned Json.Object group_obj = obj.get_object_member (group); |
702 | + // why the hell is this necessary? |
703 | + if (group_obj.has_member (key)) group_obj.remove_member (key); |
704 | + |
705 | + Json.Node node = Json.gobject_serialize (cfg_obj); |
706 | + group_obj.set_object_member (key, node.get_object ()); |
707 | + |
708 | + if (save_timer_id != 0) Source.remove (save_timer_id); |
709 | + // on crap, this takes a reference on self |
710 | + save_timer_id = Timeout.add (30000, this.save_timeout); |
711 | + } |
712 | + |
713 | + private bool save_timeout () |
714 | + { |
715 | + save_timer_id = 0; |
716 | + save (); |
717 | + |
718 | + return false; |
719 | + } |
720 | + |
721 | + /** |
722 | + * Forces immediate saving of the configuration file to the filesystem. |
723 | + */ |
724 | + public void save () |
725 | + { |
726 | + if (save_timer_id != 0) |
727 | + { |
728 | + Source.remove (save_timer_id); |
729 | + save_timer_id = 0; |
730 | + } |
731 | + |
732 | + var generator = new Generator (); |
733 | + generator.pretty = true; |
734 | + generator.set_root (root_node); |
735 | + |
736 | + DirUtils.create_with_parents (GLib.Path.get_dirname (config_file_name), 0755); |
737 | + try |
738 | + { |
739 | + generator.to_file (config_file_name); |
740 | + } |
741 | + catch (Error err) |
742 | + { |
743 | + warning ("%s", err.message); |
744 | + } |
745 | + } |
746 | + } |
747 | +} |
748 | + |
749 | |
750 | === added file 'lib/synapse-core/data-sink.vala' |
751 | --- lib/synapse-core/data-sink.vala 1970-01-01 00:00:00 +0000 |
752 | +++ lib/synapse-core/data-sink.vala 2014-06-16 07:45:32 +0000 |
753 | @@ -0,0 +1,574 @@ |
754 | +/* |
755 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
756 | + * |
757 | + * This library is free software; you can redistribute it and/or |
758 | + * modify it under the terms of the GNU Lesser General Public |
759 | + * License as published by the Free Software Foundation; either |
760 | + * version 2.1 of the License, or (at your option) any later version. |
761 | + * |
762 | + * This library is distributed in the hope that it will be useful, |
763 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
764 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
765 | + * Lesser General Public License for more details. |
766 | + * |
767 | + * You should have received a copy of the GNU Lesser General Public License |
768 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
769 | + * |
770 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
771 | + * |
772 | + */ |
773 | + |
774 | +namespace Synapse |
775 | +{ |
776 | + public errordomain SearchError |
777 | + { |
778 | + SEARCH_CANCELLED, |
779 | + UNKNOWN_ERROR |
780 | + } |
781 | + |
782 | + public interface SearchProvider : Object |
783 | + { |
784 | + public abstract async Gee.List<Match> search (string query, |
785 | + QueryFlags flags, |
786 | + ResultSet? dest_result_set, |
787 | + Cancellable? cancellable = null) throws SearchError; |
788 | + } |
789 | + |
790 | + // don't move into a class, gir doesn't like it |
791 | + [CCode (has_target = false)] |
792 | + public delegate void PluginRegisterFunc (); |
793 | + |
794 | + public class DataSink : Object, SearchProvider |
795 | + { |
796 | + public class PluginRegistry : Object |
797 | + { |
798 | + public class PluginInfo |
799 | + { |
800 | + public Type plugin_type; |
801 | + public string title; |
802 | + public string description; |
803 | + public string icon_name; |
804 | + public PluginRegisterFunc register_func; |
805 | + public bool runnable; |
806 | + public string runnable_error; |
807 | + public PluginInfo (Type type, string title, string desc, |
808 | + string icon_name, PluginRegisterFunc reg_func, |
809 | + bool runnable, string runnable_error) |
810 | + { |
811 | + this.plugin_type = type; |
812 | + this.title = title; |
813 | + this.description = desc; |
814 | + this.icon_name = icon_name; |
815 | + this.register_func = reg_func; |
816 | + this.runnable = runnable; |
817 | + this.runnable_error = runnable_error; |
818 | + } |
819 | + } |
820 | + |
821 | + public static unowned PluginRegistry instance = null; |
822 | + |
823 | + private Gee.List<PluginInfo> plugins; |
824 | + |
825 | + construct |
826 | + { |
827 | + instance = this; |
828 | + plugins = new Gee.ArrayList<PluginInfo> (); |
829 | + } |
830 | + |
831 | + ~PluginRegistry () |
832 | + { |
833 | + instance = null; |
834 | + } |
835 | + |
836 | + public static PluginRegistry get_default () |
837 | + { |
838 | + return instance ?? new PluginRegistry (); |
839 | + } |
840 | + |
841 | + public void register_plugin (Type plugin_type, |
842 | + string title, |
843 | + string description, |
844 | + string icon_name, |
845 | + PluginRegisterFunc reg_func, |
846 | + bool runnable = true, |
847 | + string runnable_error = "") |
848 | + { |
849 | + // FIXME: how about a frickin Type -> PluginInfo map?! |
850 | + int index = -1; |
851 | + for (int i=0; i < plugins.size; i++) |
852 | + { |
853 | + if (plugins[i].plugin_type == plugin_type) |
854 | + { |
855 | + index = i; |
856 | + break; |
857 | + } |
858 | + } |
859 | + if (index >= 0) plugins.remove_at (index); |
860 | + |
861 | + var p = new PluginInfo (plugin_type, title, description, icon_name, |
862 | + reg_func, runnable, runnable_error); |
863 | + plugins.add (p); |
864 | + } |
865 | + |
866 | + public Gee.List<PluginInfo> get_plugins () |
867 | + { |
868 | + return plugins.read_only_view; |
869 | + } |
870 | + |
871 | + public PluginInfo? get_plugin_info_for_type (Type plugin_type) |
872 | + { |
873 | + foreach (PluginInfo pi in plugins) |
874 | + { |
875 | + if (pi.plugin_type == plugin_type) return pi; |
876 | + } |
877 | + |
878 | + return null; |
879 | + } |
880 | + } |
881 | + |
882 | + private class DataSinkConfiguration : ConfigObject |
883 | + { |
884 | + // vala keeps array lengths, and therefore doesn't support setting arrays |
885 | + // via automatic public properties |
886 | + private string[] _disabled_plugins = null; |
887 | + public string[] disabled_plugins |
888 | + { |
889 | + get |
890 | + { |
891 | + return _disabled_plugins; |
892 | + } |
893 | + set |
894 | + { |
895 | + _disabled_plugins = value; |
896 | + } |
897 | + } |
898 | + |
899 | + public void set_plugin_enabled (Type t, bool enabled) |
900 | + { |
901 | + if (enabled) enable_plugin (t.name ()); |
902 | + else disable_plugin (t.name ()); |
903 | + } |
904 | + |
905 | + public bool is_plugin_enabled (Type t) |
906 | + { |
907 | + if (_disabled_plugins == null) return true; |
908 | + unowned string plugin_name = t.name (); |
909 | + foreach (string s in _disabled_plugins) |
910 | + { |
911 | + if (s == plugin_name) return false; |
912 | + } |
913 | + return true; |
914 | + } |
915 | + |
916 | + private void enable_plugin (string name) |
917 | + { |
918 | + if (_disabled_plugins == null) return; |
919 | + if (!(name in _disabled_plugins)) return; |
920 | + |
921 | + string[] cpy = {}; |
922 | + foreach (string s in _disabled_plugins) |
923 | + { |
924 | + if (s != name) cpy += s; |
925 | + } |
926 | + _disabled_plugins = (owned) cpy; |
927 | + } |
928 | + |
929 | + private void disable_plugin (string name) |
930 | + { |
931 | + if (_disabled_plugins == null || !(name in _disabled_plugins)) |
932 | + { |
933 | + _disabled_plugins += name; |
934 | + } |
935 | + } |
936 | + } |
937 | + |
938 | + public DataSink () |
939 | + { |
940 | + } |
941 | + |
942 | + ~DataSink () |
943 | + { |
944 | + Utils.Logger.debug (this, "DataSink died..."); |
945 | + } |
946 | + |
947 | + private DataSinkConfiguration config; |
948 | + private Gee.Set<ItemProvider> item_plugins; |
949 | + private Gee.Set<ActionProvider> action_plugins; |
950 | + private uint query_id; |
951 | + // data sink will keep reference to the name cache, so others will get this |
952 | + // instance on call to get_default() |
953 | + private DBusService dbus_name_cache; |
954 | + private DesktopFileService desktop_file_service; |
955 | + private PluginRegistry registry; |
956 | + private RelevancyService relevancy_service; |
957 | + private VolumeService volume_service; |
958 | + private Type[] plugin_types; |
959 | + |
960 | + construct |
961 | + { |
962 | + item_plugins = new Gee.HashSet<ItemProvider> (); |
963 | + action_plugins = new Gee.HashSet<ActionProvider> (); |
964 | + plugin_types = {}; |
965 | + query_id = 0; |
966 | + |
967 | + var cfg = ConfigService.get_default (); |
968 | + config = (DataSinkConfiguration) |
969 | + cfg.get_config ("data-sink", "global", typeof (DataSinkConfiguration)); |
970 | + |
971 | + // oh well, yea we need a few singletons |
972 | + registry = PluginRegistry.get_default (); |
973 | + relevancy_service = RelevancyService.get_default (); |
974 | + volume_service = VolumeService.get_default (); |
975 | + |
976 | + initialize_caches.begin (); |
977 | + register_static_plugin (typeof (CommonActions)); |
978 | + } |
979 | + |
980 | + private async void initialize_caches () |
981 | + { |
982 | + Idle.add_full (Priority.LOW, initialize_caches.callback); |
983 | + yield; |
984 | + |
985 | + int initialized_components = 0; |
986 | + int NUM_COMPONENTS = 2; |
987 | + |
988 | + dbus_name_cache = DBusService.get_default (); |
989 | + dbus_name_cache.initialize.begin (() => |
990 | + { |
991 | + initialized_components++; |
992 | + if (initialized_components >= NUM_COMPONENTS) |
993 | + { |
994 | + initialize_caches.callback (); |
995 | + } |
996 | + }); |
997 | + |
998 | + desktop_file_service = DesktopFileService.get_default (); |
999 | + desktop_file_service.reload_done.connect (this.check_plugins); |
1000 | + desktop_file_service.initialize.begin (() => |
1001 | + { |
1002 | + initialized_components++; |
1003 | + if (initialized_components >= NUM_COMPONENTS) |
1004 | + { |
1005 | + initialize_caches.callback (); |
1006 | + } |
1007 | + }); |
1008 | + |
1009 | + yield; |
1010 | + |
1011 | + Idle.add (() => { this.load_plugins (); return false; }); |
1012 | + } |
1013 | + |
1014 | + private void check_plugins () |
1015 | + { |
1016 | + PluginRegisterFunc[] reg_funcs = {}; |
1017 | + foreach (var pi in registry.get_plugins ()) |
1018 | + { |
1019 | + reg_funcs += pi.register_func; |
1020 | + } |
1021 | + |
1022 | + foreach (PluginRegisterFunc func in reg_funcs) |
1023 | + { |
1024 | + func (); |
1025 | + } |
1026 | + } |
1027 | + |
1028 | + public bool has_empty_handlers { get; set; default = false; } |
1029 | + public bool has_unknown_handlers { get; set; default = false; } |
1030 | + |
1031 | + private bool plugins_loaded = false; |
1032 | + |
1033 | + public signal void plugin_registered (Object plugin); |
1034 | + |
1035 | + protected void register_plugin (Object plugin) |
1036 | + { |
1037 | + if (plugin is ActionProvider) |
1038 | + { |
1039 | + ActionProvider action_plugin = plugin as ActionProvider; |
1040 | + action_plugins.add (action_plugin); |
1041 | + has_unknown_handlers |= action_plugin.handles_unknown (); |
1042 | + } |
1043 | + if (plugin is ItemProvider) |
1044 | + { |
1045 | + ItemProvider item_plugin = plugin as ItemProvider; |
1046 | + item_plugins.add (item_plugin); |
1047 | + has_empty_handlers |= item_plugin.handles_empty_query (); |
1048 | + } |
1049 | + |
1050 | + plugin_registered (plugin); |
1051 | + } |
1052 | + |
1053 | + private void update_has_unknown_handlers () |
1054 | + { |
1055 | + bool tmp = false; |
1056 | + foreach (var action in action_plugins) |
1057 | + { |
1058 | + if (action.enabled && action.handles_unknown ()) |
1059 | + { |
1060 | + tmp = true; |
1061 | + break; |
1062 | + } |
1063 | + } |
1064 | + has_unknown_handlers = tmp; |
1065 | + } |
1066 | + |
1067 | + private void update_has_empty_handlers () |
1068 | + { |
1069 | + bool tmp = false; |
1070 | + foreach (var item_plugin in item_plugins) |
1071 | + { |
1072 | + if (item_plugin.enabled && item_plugin.handles_empty_query ()) |
1073 | + { |
1074 | + tmp = true; |
1075 | + break; |
1076 | + } |
1077 | + } |
1078 | + has_empty_handlers = tmp; |
1079 | + } |
1080 | + |
1081 | + private Object? create_plugin (Type t) |
1082 | + { |
1083 | + var obj_class = (ObjectClass) t.class_ref (); |
1084 | + if (obj_class != null && obj_class.find_property ("data-sink") != null) |
1085 | + { |
1086 | + return Object.new (t, "data-sink", this, null); |
1087 | + } |
1088 | + else |
1089 | + { |
1090 | + return Object.new (t, null); |
1091 | + } |
1092 | + } |
1093 | + |
1094 | + private void load_plugins () |
1095 | + { |
1096 | + // FIXME: fetch and load modules |
1097 | + foreach (Type t in plugin_types) |
1098 | + { |
1099 | + t.class_ref (); // makes the plugin register itself into PluginRegistry |
1100 | + PluginRegistry.PluginInfo? info = registry.get_plugin_info_for_type (t); |
1101 | + bool skip = info != null && info.runnable == false; |
1102 | + if (config.is_plugin_enabled (t) && !skip) |
1103 | + { |
1104 | + var plugin = create_plugin (t); |
1105 | + register_plugin (plugin); |
1106 | + (plugin as Activatable).activate (); |
1107 | + } |
1108 | + } |
1109 | + |
1110 | + plugins_loaded = true; |
1111 | + } |
1112 | + |
1113 | + /* This needs to be called right after instantiation, |
1114 | + * if plugins_loaded == true, it won't have any effect. */ |
1115 | + public void register_static_plugin (Type plugin_type) |
1116 | + { |
1117 | + if (plugin_type in plugin_types) return; |
1118 | + plugin_types += plugin_type; |
1119 | + } |
1120 | + |
1121 | + public unowned Object? get_plugin (string name) |
1122 | + { |
1123 | + unowned Object? result = null; |
1124 | + |
1125 | + foreach (var plugin in item_plugins) |
1126 | + { |
1127 | + if (plugin.get_type ().name () == name) |
1128 | + { |
1129 | + result = plugin; |
1130 | + break; |
1131 | + } |
1132 | + } |
1133 | + |
1134 | + return result; |
1135 | + } |
1136 | + |
1137 | + public bool is_plugin_enabled (Type plugin_type) |
1138 | + { |
1139 | + foreach (var plugin in item_plugins) |
1140 | + { |
1141 | + if (plugin.get_type () == plugin_type) return plugin.enabled; |
1142 | + } |
1143 | + |
1144 | + foreach (var action in action_plugins) |
1145 | + { |
1146 | + if (action.get_type () == plugin_type) return action.enabled; |
1147 | + } |
1148 | + |
1149 | + return false; |
1150 | + } |
1151 | + |
1152 | + public void set_plugin_enabled (Type plugin_type, bool enabled) |
1153 | + { |
1154 | + // save it into our config object |
1155 | + config.set_plugin_enabled (plugin_type, enabled); |
1156 | + ConfigService.get_default ().set_config ("data-sink", "global", config); |
1157 | + |
1158 | + foreach (var plugin in item_plugins) |
1159 | + { |
1160 | + if (plugin.get_type () == plugin_type) |
1161 | + { |
1162 | + plugin.enabled = enabled; |
1163 | + if (enabled) plugin.activate (); |
1164 | + else plugin.deactivate (); |
1165 | + update_has_empty_handlers (); |
1166 | + return; |
1167 | + } |
1168 | + } |
1169 | + |
1170 | + foreach (var action in action_plugins) |
1171 | + { |
1172 | + if (action.get_type () == plugin_type) |
1173 | + { |
1174 | + action.enabled = enabled; |
1175 | + if (enabled) action.activate (); |
1176 | + else action.deactivate (); |
1177 | + update_has_unknown_handlers (); |
1178 | + return; |
1179 | + } |
1180 | + } |
1181 | + |
1182 | + // plugin isn't instantiated yet |
1183 | + if (enabled) |
1184 | + { |
1185 | + var new_instance = create_plugin (plugin_type); |
1186 | + register_plugin (new_instance); |
1187 | + (new_instance as Activatable).activate (); |
1188 | + } |
1189 | + } |
1190 | + |
1191 | + [Signal (detailed = true)] |
1192 | + public signal void search_done (ResultSet rs, uint query_id); |
1193 | + |
1194 | + public async Gee.List<Match> search (string query, |
1195 | + QueryFlags flags, |
1196 | + ResultSet? dest_result_set, |
1197 | + Cancellable? cancellable = null) throws SearchError |
1198 | + { |
1199 | + // wait for our initialization |
1200 | + while (!plugins_loaded) |
1201 | + { |
1202 | + Timeout.add (100, search.callback); |
1203 | + yield; |
1204 | + if (cancellable != null && cancellable.is_cancelled ()) |
1205 | + { |
1206 | + throw new SearchError.SEARCH_CANCELLED ("Cancelled"); |
1207 | + } |
1208 | + } |
1209 | + var q = Query (query_id++, query, flags); |
1210 | + string query_stripped = query.strip (); |
1211 | + |
1212 | + var cancellables = new GLib.List<Cancellable> (); |
1213 | + |
1214 | + var current_result_set = dest_result_set ?? new ResultSet (); |
1215 | + int search_size = item_plugins.size; |
1216 | + // FIXME: this is probably useless, if async method finishes immediately, |
1217 | + // it'll call complete_in_idle |
1218 | + bool waiting = false; |
1219 | + |
1220 | + foreach (var data_plugin in item_plugins) |
1221 | + { |
1222 | + bool skip = !data_plugin.enabled || |
1223 | + (query == "" && !data_plugin.handles_empty_query ()) || |
1224 | + !data_plugin.handles_query (q); |
1225 | + if (skip) |
1226 | + { |
1227 | + search_size--; |
1228 | + continue; |
1229 | + } |
1230 | + // we need to pass separate cancellable to each plugin, because we're |
1231 | + // running them in parallel |
1232 | + var c = new Cancellable (); |
1233 | + cancellables.prepend (c); |
1234 | + q.cancellable = c; |
1235 | + // magic comes here |
1236 | + data_plugin.search.begin (q, (src_obj, res) => |
1237 | + { |
1238 | + var plugin = src_obj as ItemProvider; |
1239 | + try |
1240 | + { |
1241 | + var results = plugin.search.end (res); |
1242 | + this.search_done[plugin.get_type ().name ()] (results, q.query_id); |
1243 | + current_result_set.add_all (results); |
1244 | + } |
1245 | + catch (SearchError err) |
1246 | + { |
1247 | + if (!(err is SearchError.SEARCH_CANCELLED)) |
1248 | + { |
1249 | + warning ("%s returned error: %s", |
1250 | + plugin.get_type ().name (), err.message); |
1251 | + } |
1252 | + } |
1253 | + |
1254 | + if (--search_size == 0 && waiting) search.callback (); |
1255 | + }); |
1256 | + } |
1257 | + cancellables.reverse (); |
1258 | + |
1259 | + if (cancellable != null) |
1260 | + { |
1261 | + cancellable.connect (() => |
1262 | + { |
1263 | + foreach (var c in cancellables) c.cancel (); |
1264 | + }); |
1265 | + } |
1266 | + |
1267 | + waiting = true; |
1268 | + if (search_size > 0) yield; |
1269 | + |
1270 | + if (cancellable != null && cancellable.is_cancelled ()) |
1271 | + { |
1272 | + throw new SearchError.SEARCH_CANCELLED ("Cancelled"); |
1273 | + } |
1274 | + |
1275 | + if (has_unknown_handlers && query_stripped != "") |
1276 | + { |
1277 | + var unknown_match = new DefaultMatch (query); |
1278 | + bool add_to_rs = false; |
1279 | + if (QueryFlags.ACTIONS in flags || QueryFlags.TEXT in flags) |
1280 | + { |
1281 | + // FIXME: maybe we should also check here if there are any matches |
1282 | + add_to_rs = true; |
1283 | + } |
1284 | + else |
1285 | + { |
1286 | + // check whether any of the actions support this category |
1287 | + var unknown_match_actions = find_actions_for_unknown_match (unknown_match, flags); |
1288 | + if (unknown_match_actions.size > 0) add_to_rs = true; |
1289 | + } |
1290 | + |
1291 | + if (add_to_rs) current_result_set.add (unknown_match, 0); |
1292 | + } |
1293 | + |
1294 | + return current_result_set.get_sorted_list (); |
1295 | + } |
1296 | + |
1297 | + protected Gee.List<Match> find_actions_for_unknown_match (Match match, |
1298 | + QueryFlags flags) |
1299 | + { |
1300 | + var rs = new ResultSet (); |
1301 | + var q = Query (0, "", flags); |
1302 | + foreach (var action_plugin in action_plugins) |
1303 | + { |
1304 | + if (!action_plugin.enabled) continue; |
1305 | + if (!action_plugin.handles_unknown ()) continue; |
1306 | + rs.add_all (action_plugin.find_for_match (ref q, match)); |
1307 | + } |
1308 | + |
1309 | + return rs.get_sorted_list (); |
1310 | + } |
1311 | + |
1312 | + public Gee.List<Match> find_actions_for_match (Match match, string? query, |
1313 | + QueryFlags flags) |
1314 | + { |
1315 | + var rs = new ResultSet (); |
1316 | + var q = Query (0, query ?? "", flags); |
1317 | + foreach (var action_plugin in action_plugins) |
1318 | + { |
1319 | + if (!action_plugin.enabled) continue; |
1320 | + rs.add_all (action_plugin.find_for_match (ref q, match)); |
1321 | + } |
1322 | + |
1323 | + return rs.get_sorted_list (); |
1324 | + } |
1325 | + } |
1326 | +} |
1327 | + |
1328 | |
1329 | === added file 'lib/synapse-core/dbus-service.vala' |
1330 | --- lib/synapse-core/dbus-service.vala 1970-01-01 00:00:00 +0000 |
1331 | +++ lib/synapse-core/dbus-service.vala 2014-06-16 07:45:32 +0000 |
1332 | @@ -0,0 +1,166 @@ |
1333 | +/* |
1334 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
1335 | + * |
1336 | + * This library is free software; you can redistribute it and/or |
1337 | + * modify it under the terms of the GNU Lesser General Public |
1338 | + * License as published by the Free Software Foundation; either |
1339 | + * version 2.1 of the License, or (at your option) any later version. |
1340 | + * |
1341 | + * This library is distributed in the hope that it will be useful, |
1342 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1343 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1344 | + * Lesser General Public License for more details. |
1345 | + * |
1346 | + * You should have received a copy of the GNU Lesser General Public License |
1347 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1348 | + * |
1349 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
1350 | + * |
1351 | + */ |
1352 | + |
1353 | +namespace Synapse |
1354 | +{ |
1355 | + [DBus (name = "org.freedesktop.DBus")] |
1356 | + public interface FreeDesktopDBus : GLib.Object |
1357 | + { |
1358 | + public const string UNIQUE_NAME = "org.freedesktop.DBus"; |
1359 | + public const string OBJECT_PATH = "/org/freedesktop/DBus"; |
1360 | + |
1361 | + public abstract async string[] list_queued_owners (string name) throws IOError; |
1362 | + public abstract async string[] list_names () throws IOError; |
1363 | + public abstract async string[] list_activatable_names () throws IOError; |
1364 | + public abstract async bool name_has_owner (string name) throws IOError; |
1365 | + public signal void name_owner_changed (string name, |
1366 | + string old_owner, |
1367 | + string new_owner); |
1368 | + public abstract async uint32 start_service_by_name (string name, |
1369 | + uint32 flags) throws IOError; |
1370 | + public abstract async string get_name_owner (string name) throws IOError; |
1371 | + } |
1372 | + |
1373 | + public class DBusService : Object |
1374 | + { |
1375 | + private FreeDesktopDBus proxy; |
1376 | + private Gee.Set<string> owned_names; |
1377 | + private Gee.Set<string> activatable_names; |
1378 | + private Gee.Set<string> system_activatable_names; |
1379 | + |
1380 | + private Utils.AsyncOnce<bool> init_once; |
1381 | + |
1382 | + // singleton that can be easily destroyed |
1383 | + public static DBusService get_default () |
1384 | + { |
1385 | + return instance ?? new DBusService (); |
1386 | + } |
1387 | + |
1388 | + private DBusService () |
1389 | + { |
1390 | + } |
1391 | + |
1392 | + private static unowned DBusService? instance; |
1393 | + construct |
1394 | + { |
1395 | + instance = this; |
1396 | + owned_names = new Gee.HashSet<string> (); |
1397 | + activatable_names = new Gee.HashSet<string> (); |
1398 | + system_activatable_names = new Gee.HashSet<string> (); |
1399 | + init_once = new Utils.AsyncOnce<bool> (); |
1400 | + |
1401 | + initialize.begin (); |
1402 | + } |
1403 | + |
1404 | + ~DBusService () |
1405 | + { |
1406 | + instance = null; |
1407 | + } |
1408 | + |
1409 | + private void name_owner_changed (FreeDesktopDBus sender, |
1410 | + string name, |
1411 | + string old_owner, |
1412 | + string new_owner) |
1413 | + { |
1414 | + if (name.has_prefix (":")) return; |
1415 | + |
1416 | + if (old_owner == "") |
1417 | + { |
1418 | + owned_names.add (name); |
1419 | + owner_changed (name, true); |
1420 | + } |
1421 | + else if (new_owner == "") |
1422 | + { |
1423 | + owned_names.remove (name); |
1424 | + owner_changed (name, false); |
1425 | + } |
1426 | + } |
1427 | + |
1428 | + public signal void owner_changed (string name, bool is_owned); |
1429 | + |
1430 | + public bool name_has_owner (string name) |
1431 | + { |
1432 | + return name in owned_names; |
1433 | + } |
1434 | + |
1435 | + public bool name_is_activatable (string name) |
1436 | + { |
1437 | + return name in activatable_names; |
1438 | + } |
1439 | + |
1440 | + public bool service_is_available (string name) |
1441 | + { |
1442 | + return name in system_activatable_names; |
1443 | + } |
1444 | + |
1445 | + public async void initialize () |
1446 | + { |
1447 | + if (init_once.is_initialized ()) return; |
1448 | + var is_locked = yield init_once.enter (); |
1449 | + if (!is_locked) return; |
1450 | + |
1451 | + string[] names; |
1452 | + try |
1453 | + { |
1454 | + proxy = Bus.get_proxy_sync (BusType.SESSION, |
1455 | + FreeDesktopDBus.UNIQUE_NAME, |
1456 | + FreeDesktopDBus.OBJECT_PATH); |
1457 | + |
1458 | + proxy.name_owner_changed.connect (this.name_owner_changed); |
1459 | + names = yield proxy.list_names (); |
1460 | + foreach (unowned string name in names) |
1461 | + { |
1462 | + if (name.has_prefix (":")) continue; |
1463 | + owned_names.add (name); |
1464 | + } |
1465 | + |
1466 | + names = yield proxy.list_activatable_names (); |
1467 | + foreach (unowned string session_act in names) |
1468 | + { |
1469 | + activatable_names.add (session_act); |
1470 | + } |
1471 | + } |
1472 | + catch (Error err) |
1473 | + { |
1474 | + warning ("%s", err.message); |
1475 | + } |
1476 | + |
1477 | + try |
1478 | + { |
1479 | + FreeDesktopDBus sys_proxy = Bus.get_proxy_sync ( |
1480 | + BusType.SYSTEM, |
1481 | + FreeDesktopDBus.UNIQUE_NAME, |
1482 | + FreeDesktopDBus.OBJECT_PATH); |
1483 | + |
1484 | + names = yield sys_proxy.list_activatable_names (); |
1485 | + foreach (unowned string system_act in names) |
1486 | + { |
1487 | + system_activatable_names.add (system_act); |
1488 | + } |
1489 | + } |
1490 | + catch (Error sys_err) |
1491 | + { |
1492 | + warning ("%s", sys_err.message); |
1493 | + } |
1494 | + init_once.leave (true); |
1495 | + } |
1496 | + } |
1497 | +} |
1498 | + |
1499 | |
1500 | === added file 'lib/synapse-core/desktop-file-service.vala' |
1501 | --- lib/synapse-core/desktop-file-service.vala 1970-01-01 00:00:00 +0000 |
1502 | +++ lib/synapse-core/desktop-file-service.vala 2014-06-16 07:45:32 +0000 |
1503 | @@ -0,0 +1,635 @@ |
1504 | +/* |
1505 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
1506 | + * |
1507 | + * This library is free software; you can redistribute it and/or |
1508 | + * modify it under the terms of the GNU Lesser General Public |
1509 | + * License as published by the Free Software Foundation; either |
1510 | + * version 2.1 of the License, or (at your option) any later version. |
1511 | + * |
1512 | + * This library is distributed in the hope that it will be useful, |
1513 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1514 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1515 | + * Lesser General Public License for more details. |
1516 | + * |
1517 | + * You should have received a copy of the GNU Lesser General Public License |
1518 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1519 | + * |
1520 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
1521 | + * Alberto Aldegheri <albyrock87+dev@gmail.com> |
1522 | + * |
1523 | + */ |
1524 | + |
1525 | +namespace Synapse |
1526 | +{ |
1527 | + errordomain DesktopFileError |
1528 | + { |
1529 | + UNINTERESTING_ENTRY |
1530 | + } |
1531 | + |
1532 | + public class DesktopFileInfo: Object |
1533 | + { |
1534 | + // registered environments from http://standards.freedesktop.org/menu-spec/latest |
1535 | + // (and pantheon) |
1536 | + [Flags] |
1537 | + public enum EnvironmentType |
1538 | + { |
1539 | + GNOME = 1 << 0, |
1540 | + KDE = 1 << 1, |
1541 | + LXDE = 1 << 2, |
1542 | + MATE = 1 << 3, |
1543 | + RAZOR = 1 << 4, |
1544 | + ROX = 1 << 5, |
1545 | + TDE = 1 << 6, |
1546 | + UNITY = 1 << 7, |
1547 | + XFCE = 1 << 8, |
1548 | + PANTHEON = 1 << 9, |
1549 | + OLD = 1 << 10, |
1550 | + |
1551 | + ALL = 0x3FF |
1552 | + } |
1553 | + |
1554 | + public string desktop_id { get; construct set; } |
1555 | + public string name { get; construct set; } |
1556 | + public string comment { get; set; default = ""; } |
1557 | + public string icon_name { get; construct set; default = ""; } |
1558 | + |
1559 | + public bool needs_terminal { get; set; default = false; } |
1560 | + public string filename { get; construct set; } |
1561 | + |
1562 | + public string exec { get; set; } |
1563 | + |
1564 | + public bool is_hidden { get; private set; default = false; } |
1565 | + public bool is_valid { get; private set; default = true; } |
1566 | + |
1567 | + public string[] mime_types = null; |
1568 | + |
1569 | + private string? name_folded = null; |
1570 | + public unowned string get_name_folded () |
1571 | + { |
1572 | + if (name_folded == null) name_folded = name.casefold (); |
1573 | + return name_folded; |
1574 | + } |
1575 | + |
1576 | + public EnvironmentType show_in { get; set; default = EnvironmentType.ALL; } |
1577 | + |
1578 | + private static const string GROUP = "Desktop Entry"; |
1579 | + |
1580 | + public DesktopFileInfo.for_keyfile (string path, KeyFile keyfile, |
1581 | + string desktop_id) |
1582 | + { |
1583 | + Object (filename: path, desktop_id: desktop_id); |
1584 | + |
1585 | + init_from_keyfile (keyfile); |
1586 | + } |
1587 | + |
1588 | + private EnvironmentType parse_environments (string[] environments) |
1589 | + { |
1590 | + EnvironmentType result = 0; |
1591 | + foreach (unowned string env in environments) |
1592 | + { |
1593 | + string env_up = env.up (); |
1594 | + switch (env_up) |
1595 | + { |
1596 | + case "GNOME": result |= EnvironmentType.GNOME; break; |
1597 | + case "PANTHEON": result |= EnvironmentType.PANTHEON; break; |
1598 | + case "KDE": result |= EnvironmentType.KDE; break; |
1599 | + case "LXDE": result |= EnvironmentType.LXDE; break; |
1600 | + case "MATE": result |= EnvironmentType.MATE; break; |
1601 | + case "RAZOR": result |= EnvironmentType.RAZOR; break; |
1602 | + case "ROX": result |= EnvironmentType.ROX; break; |
1603 | + case "TDE": result |= EnvironmentType.TDE; break; |
1604 | + case "UNITY": result |= EnvironmentType.UNITY; break; |
1605 | + case "XFCE": result |= EnvironmentType.XFCE; break; |
1606 | + case "OLD": result |= EnvironmentType.OLD; break; |
1607 | + default: warning ("%s is not understood", env); break; |
1608 | + } |
1609 | + } |
1610 | + return result; |
1611 | + } |
1612 | + |
1613 | + private void init_from_keyfile (KeyFile keyfile) |
1614 | + { |
1615 | + try |
1616 | + { |
1617 | + if (keyfile.get_string (GROUP, "Type") != "Application") |
1618 | + { |
1619 | + throw new DesktopFileError.UNINTERESTING_ENTRY ("Not Application-type desktop entry"); |
1620 | + } |
1621 | + |
1622 | + if (keyfile.has_key (GROUP, "Categories")) |
1623 | + { |
1624 | + string[] categories = keyfile.get_string_list (GROUP, "Categories"); |
1625 | + if ("Screensaver" in categories) |
1626 | + { |
1627 | + throw new DesktopFileError.UNINTERESTING_ENTRY ("Screensaver desktop entry"); |
1628 | + } |
1629 | + } |
1630 | + |
1631 | + DesktopAppInfo app_info; |
1632 | + app_info = new DesktopAppInfo.from_keyfile (keyfile); |
1633 | + |
1634 | + if (app_info == null) |
1635 | + { |
1636 | + throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to create AppInfo"); |
1637 | + } |
1638 | + |
1639 | + name = app_info.get_name (); |
1640 | + exec = app_info.get_commandline (); |
1641 | + if (exec == null) |
1642 | + { |
1643 | + throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to get exec for %s".printf (name)); |
1644 | + } |
1645 | + |
1646 | + // check for hidden desktop files |
1647 | + if (keyfile.has_key (GROUP, "Hidden") && |
1648 | + keyfile.get_boolean (GROUP, "Hidden")) |
1649 | + { |
1650 | + is_hidden = true; |
1651 | + } |
1652 | + if (keyfile.has_key (GROUP, "NoDisplay") && |
1653 | + keyfile.get_boolean (GROUP, "NoDisplay")) |
1654 | + { |
1655 | + is_hidden = true; |
1656 | + } |
1657 | + |
1658 | + comment = app_info.get_description () ?? ""; |
1659 | + |
1660 | + var icon = app_info.get_icon () ?? |
1661 | + new ThemedIcon ("application-default-icon"); |
1662 | + icon_name = icon.to_string (); |
1663 | + |
1664 | + if (keyfile.has_key (GROUP, "MimeType")) |
1665 | + { |
1666 | + mime_types = keyfile.get_string_list (GROUP, "MimeType"); |
1667 | + } |
1668 | + if (keyfile.has_key (GROUP, "Terminal")) |
1669 | + { |
1670 | + needs_terminal = keyfile.get_boolean (GROUP, "Terminal"); |
1671 | + } |
1672 | + if (keyfile.has_key (GROUP, "OnlyShowIn")) |
1673 | + { |
1674 | + show_in = parse_environments (keyfile.get_string_list (GROUP, |
1675 | + "OnlyShowIn")); |
1676 | + } |
1677 | + else if (keyfile.has_key (GROUP, "NotShowIn")) |
1678 | + { |
1679 | + var not_show = parse_environments (keyfile.get_string_list (GROUP, |
1680 | + "NotShowIn")); |
1681 | + show_in = EnvironmentType.ALL ^ not_show; |
1682 | + } |
1683 | + |
1684 | + // special case these, people are using them quite often and wonder |
1685 | + // why they don't appear |
1686 | + if (filename.has_suffix ("gconf-editor.desktop") || |
1687 | + filename.has_suffix ("dconf-editor.desktop")) |
1688 | + { |
1689 | + is_hidden = false; |
1690 | + } |
1691 | + } |
1692 | + catch (Error err) |
1693 | + { |
1694 | + Utils.Logger.warning (this, "%s", err.message); |
1695 | + is_valid = false; |
1696 | + } |
1697 | + } |
1698 | + } |
1699 | + |
1700 | + public class DesktopFileService : Object |
1701 | + { |
1702 | + private static unowned DesktopFileService? instance; |
1703 | + private Utils.AsyncOnce<bool> init_once; |
1704 | + |
1705 | + // singleton that can be easily destroyed |
1706 | + public static DesktopFileService get_default () |
1707 | + { |
1708 | + return instance ?? new DesktopFileService (); |
1709 | + } |
1710 | + |
1711 | + private DesktopFileService () |
1712 | + { |
1713 | + } |
1714 | + |
1715 | + private Gee.List<FileMonitor> directory_monitors; |
1716 | + private Gee.List<DesktopFileInfo> all_desktop_files; |
1717 | + private Gee.List<DesktopFileInfo> non_hidden_desktop_files; |
1718 | + private Gee.Map<unowned string, Gee.List<DesktopFileInfo> > mimetype_map; |
1719 | + private Gee.Map<string, Gee.List<DesktopFileInfo> > exec_map; |
1720 | + private Gee.Map<string, DesktopFileInfo> desktop_id_map; |
1721 | + private Gee.MultiMap<string, string> mimetype_parent_map; |
1722 | + |
1723 | + construct |
1724 | + { |
1725 | + instance = this; |
1726 | + |
1727 | + directory_monitors = new Gee.ArrayList<FileMonitor> (); |
1728 | + all_desktop_files = new Gee.ArrayList<DesktopFileInfo> (); |
1729 | + non_hidden_desktop_files = new Gee.ArrayList<DesktopFileInfo> (); |
1730 | + mimetype_parent_map = new Gee.HashMultiMap<string, string> (); |
1731 | + init_once = new Utils.AsyncOnce<bool> (); |
1732 | + |
1733 | + initialize.begin (); |
1734 | + } |
1735 | + |
1736 | + ~DesktopFileService () |
1737 | + { |
1738 | + instance = null; |
1739 | + } |
1740 | + |
1741 | + public async void initialize () |
1742 | + { |
1743 | + if (init_once.is_initialized ()) return; |
1744 | + var is_locked = yield init_once.enter (); |
1745 | + if (!is_locked) return; |
1746 | + |
1747 | + get_environment_type (); |
1748 | + DesktopAppInfo.set_desktop_env (session_type_str); |
1749 | + |
1750 | + Idle.add_full (Priority.LOW, initialize.callback); |
1751 | + yield; |
1752 | + |
1753 | + yield load_all_desktop_files (); |
1754 | + |
1755 | + init_once.leave (true); |
1756 | + } |
1757 | + |
1758 | + private DesktopFileInfo.EnvironmentType session_type = |
1759 | + DesktopFileInfo.EnvironmentType.GNOME; |
1760 | + private string session_type_str = "GNOME"; |
1761 | + |
1762 | + public DesktopFileInfo.EnvironmentType get_environment () |
1763 | + { |
1764 | + return this.session_type; |
1765 | + } |
1766 | + |
1767 | + private void get_environment_type () |
1768 | + { |
1769 | + unowned string? session_var; |
1770 | + session_var = Environment.get_variable ("XDG_CURRENT_DESKTOP"); |
1771 | + if (session_var == null) |
1772 | + { |
1773 | + session_var = Environment.get_variable ("DESKTOP_SESSION"); |
1774 | + } |
1775 | + |
1776 | + if (session_var == null) return; |
1777 | + |
1778 | + string session = session_var.down (); |
1779 | + |
1780 | + if (session.has_prefix ("unity") || session.has_prefix ("ubuntu")) |
1781 | + { |
1782 | + session_type = DesktopFileInfo.EnvironmentType.UNITY; |
1783 | + session_type_str = "Unity"; |
1784 | + } |
1785 | + else if (session.has_prefix ("kde")) |
1786 | + { |
1787 | + session_type = DesktopFileInfo.EnvironmentType.KDE; |
1788 | + session_type_str = "KDE"; |
1789 | + } |
1790 | + else if (session.has_prefix ("gnome")) |
1791 | + { |
1792 | + session_type = DesktopFileInfo.EnvironmentType.GNOME; |
1793 | + session_type_str = "GNOME"; |
1794 | + } |
1795 | + else if (session.has_prefix ("lx")) |
1796 | + { |
1797 | + session_type = DesktopFileInfo.EnvironmentType.LXDE; |
1798 | + session_type_str = "LXDE"; |
1799 | + } |
1800 | + else if (session.has_prefix ("xfce")) |
1801 | + { |
1802 | + session_type = DesktopFileInfo.EnvironmentType.XFCE; |
1803 | + session_type_str = "XFCE"; |
1804 | + } |
1805 | + else if (session.has_prefix ("mate")) |
1806 | + { |
1807 | + session_type = DesktopFileInfo.EnvironmentType.MATE; |
1808 | + session_type_str = "MATE"; |
1809 | + } |
1810 | + else if (session.has_prefix ("razor")) |
1811 | + { |
1812 | + session_type = DesktopFileInfo.EnvironmentType.RAZOR; |
1813 | + session_type_str = "Razor"; |
1814 | + } |
1815 | + else if (session.has_prefix ("tde")) |
1816 | + { |
1817 | + session_type = DesktopFileInfo.EnvironmentType.TDE; |
1818 | + session_type_str = "TDE"; |
1819 | + } |
1820 | + else if (session.has_prefix ("rox")) |
1821 | + { |
1822 | + session_type = DesktopFileInfo.EnvironmentType.ROX; |
1823 | + session_type_str = "ROX"; |
1824 | + } |
1825 | + else if (session.has_prefix ("pantheon")) |
1826 | + { |
1827 | + session_type = DesktopFileInfo.EnvironmentType.PANTHEON; |
1828 | + session_type_str = "Pantheon"; |
1829 | + } |
1830 | + else |
1831 | + { |
1832 | + warning ("Desktop session type is not recognized, assuming GNOME."); |
1833 | + } |
1834 | + } |
1835 | + |
1836 | + private string? get_cache_file_name (string dir_name) |
1837 | + { |
1838 | + // FIXME: should we use this? it's Ubuntu-specific |
1839 | + string? locale = Intl.setlocale (LocaleCategory.MESSAGES, null); |
1840 | + if (locale == null) return null; |
1841 | + |
1842 | + // even though this is what the patch in gnome-menus does, the name |
1843 | + // of the file is different here (utf is uppercase) |
1844 | + string filename = "desktop.%s.cache".printf ( |
1845 | + locale.replace (".UTF-8", ".utf8")); |
1846 | + |
1847 | + return Path.build_filename (dir_name, filename, null); |
1848 | + } |
1849 | + |
1850 | + private async void process_directory (File directory, |
1851 | + string id_prefix, |
1852 | + Gee.Set<File> monitored_dirs) |
1853 | + { |
1854 | + try |
1855 | + { |
1856 | + string path = directory.get_path (); |
1857 | + // we need to skip menu-xdg directory, see lp:686624 |
1858 | + if (path != null && path.has_suffix ("menu-xdg")) return; |
1859 | + // screensavers don't interest us, skip those |
1860 | + if (path != null && path.has_suffix ("/screensavers")) return; |
1861 | + |
1862 | + Utils.Logger.debug (this, "Searching for desktop files in: %s", path); |
1863 | + bool exists = yield Utils.query_exists_async (directory); |
1864 | + if (!exists) return; |
1865 | + /* Check if we already scanned this directory // lp:686624 */ |
1866 | + foreach (var scanned_dir in monitored_dirs) |
1867 | + { |
1868 | + if (path == scanned_dir.get_path ()) return; |
1869 | + } |
1870 | + monitored_dirs.add (directory); |
1871 | + var enumerator = yield directory.enumerate_children_async ( |
1872 | + FileAttribute.STANDARD_NAME + "," + FileAttribute.STANDARD_TYPE, |
1873 | + 0, 0); |
1874 | + var files = yield enumerator.next_files_async (1024, 0); |
1875 | + foreach (var f in files) |
1876 | + { |
1877 | + unowned string name = f.get_name (); |
1878 | + if (f.get_file_type () == FileType.DIRECTORY) |
1879 | + { |
1880 | + // FIXME: this could cause too many open files error, or? |
1881 | + var subdir = directory.get_child (name); |
1882 | + var new_prefix = "%s%s-".printf (id_prefix, subdir.get_basename ()); |
1883 | + yield process_directory (subdir, new_prefix, monitored_dirs); |
1884 | + } |
1885 | + else |
1886 | + { |
1887 | + // ignore ourselves |
1888 | + if (name.has_suffix ("synapse.desktop")) continue; |
1889 | + if (name.has_suffix (".desktop")) |
1890 | + { |
1891 | + yield load_desktop_file (directory.get_child (name), id_prefix); |
1892 | + } |
1893 | + } |
1894 | + } |
1895 | + } |
1896 | + catch (Error err) |
1897 | + { |
1898 | + warning ("%s", err.message); |
1899 | + } |
1900 | + } |
1901 | + |
1902 | + private async void load_all_desktop_files () |
1903 | + { |
1904 | + string[] data_dirs = Environment.get_system_data_dirs (); |
1905 | + data_dirs += Environment.get_user_data_dir (); |
1906 | + |
1907 | + Gee.Set<File> desktop_file_dirs = new Gee.HashSet<File> (); |
1908 | + |
1909 | + mimetype_parent_map.clear (); |
1910 | + |
1911 | + foreach (unowned string data_dir in data_dirs) |
1912 | + { |
1913 | + string dir_path = Path.build_filename (data_dir, "applications", null); |
1914 | + var directory = File.new_for_path (dir_path); |
1915 | + yield process_directory (directory, "", desktop_file_dirs); |
1916 | + dir_path = Path.build_filename (data_dir, "mime", "subclasses"); |
1917 | + yield load_mime_parents_from_file (dir_path); |
1918 | + } |
1919 | + |
1920 | + create_indices (); |
1921 | + |
1922 | + directory_monitors = new Gee.ArrayList<FileMonitor> (); |
1923 | + foreach (File d in desktop_file_dirs) |
1924 | + { |
1925 | + try |
1926 | + { |
1927 | + FileMonitor monitor = d.monitor_directory (0, null); |
1928 | + monitor.changed.connect (this.desktop_file_directory_changed); |
1929 | + directory_monitors.add (monitor); |
1930 | + } |
1931 | + catch (Error err) |
1932 | + { |
1933 | + warning ("Unable to monitor directory: %s", err.message); |
1934 | + } |
1935 | + } |
1936 | + } |
1937 | + |
1938 | + private uint timer_id = 0; |
1939 | + |
1940 | + public signal void reload_started (); |
1941 | + public signal void reload_done (); |
1942 | + |
1943 | + private void desktop_file_directory_changed () |
1944 | + { |
1945 | + reload_started (); |
1946 | + if (timer_id != 0) |
1947 | + { |
1948 | + Source.remove (timer_id); |
1949 | + } |
1950 | + |
1951 | + timer_id = Timeout.add (5000, () => |
1952 | + { |
1953 | + timer_id = 0; |
1954 | + reload_desktop_files.begin (); |
1955 | + return false; |
1956 | + }); |
1957 | + } |
1958 | + |
1959 | + private async void reload_desktop_files () |
1960 | + { |
1961 | + debug ("Reloading desktop files..."); |
1962 | + all_desktop_files.clear (); |
1963 | + non_hidden_desktop_files.clear (); |
1964 | + yield load_all_desktop_files (); |
1965 | + |
1966 | + reload_done (); |
1967 | + } |
1968 | + |
1969 | + private async void load_desktop_file (File file, string id_prefix) |
1970 | + { |
1971 | + try |
1972 | + { |
1973 | + uint8[] file_contents; |
1974 | + bool success = yield file.load_contents_async (null, out file_contents, |
1975 | + null); |
1976 | + if (success) |
1977 | + { |
1978 | + var keyfile = new KeyFile (); |
1979 | + keyfile.load_from_data ((string) file_contents, |
1980 | + file_contents.length, 0); |
1981 | + |
1982 | + var desktop_id = "%s%s".printf (id_prefix, file.get_basename ()); |
1983 | + var dfi = new DesktopFileInfo.for_keyfile (file.get_path (), |
1984 | + keyfile, |
1985 | + desktop_id); |
1986 | + if (dfi.is_valid) |
1987 | + { |
1988 | + all_desktop_files.add (dfi); |
1989 | + if (!dfi.is_hidden && session_type in dfi.show_in) |
1990 | + { |
1991 | + non_hidden_desktop_files.add (dfi); |
1992 | + } |
1993 | + } |
1994 | + } |
1995 | + } |
1996 | + catch (Error err) |
1997 | + { |
1998 | + warning ("%s", err.message); |
1999 | + } |
2000 | + } |
2001 | + |
2002 | + private void create_indices () |
2003 | + { |
2004 | + // create mimetype maps |
2005 | + mimetype_map = |
2006 | + new Gee.HashMap<unowned string, Gee.List<DesktopFileInfo> > (); |
2007 | + // and exec map |
2008 | + exec_map = |
2009 | + new Gee.HashMap<string, Gee.List<DesktopFileInfo> > (); |
2010 | + // and desktop id map |
2011 | + desktop_id_map = |
2012 | + new Gee.HashMap<string, DesktopFileInfo> (); |
2013 | + |
2014 | + Regex exec_re; |
2015 | + try |
2016 | + { |
2017 | + exec_re = new Regex ("%[fFuU]"); |
2018 | + } |
2019 | + catch (Error err) |
2020 | + { |
2021 | + critical ("%s", err.message); |
2022 | + return; |
2023 | + } |
2024 | + |
2025 | + foreach (var dfi in all_desktop_files) |
2026 | + { |
2027 | + string exec = ""; |
2028 | + try |
2029 | + { |
2030 | + exec = exec_re.replace_literal (dfi.exec, -1, 0, ""); |
2031 | + } |
2032 | + catch (RegexError err) |
2033 | + { |
2034 | + Utils.Logger.error (this, "%s", err.message); |
2035 | + } |
2036 | + exec = exec.strip (); |
2037 | + // update exec map |
2038 | + Gee.List<DesktopFileInfo>? exec_list = exec_map[exec]; |
2039 | + if (exec_list == null) |
2040 | + { |
2041 | + exec_list = new Gee.ArrayList<DesktopFileInfo> (); |
2042 | + exec_map[exec] = exec_list; |
2043 | + } |
2044 | + exec_list.add (dfi); |
2045 | + |
2046 | + // update desktop id map |
2047 | + var desktop_id = dfi.desktop_id ?? Path.get_basename (dfi.filename); |
2048 | + desktop_id_map[desktop_id] = dfi; |
2049 | + |
2050 | + // update mimetype map |
2051 | + if (dfi.is_hidden || dfi.mime_types == null) continue; |
2052 | + |
2053 | + foreach (unowned string mime_type in dfi.mime_types) |
2054 | + { |
2055 | + Gee.List<DesktopFileInfo>? list = mimetype_map[mime_type]; |
2056 | + if (list == null) |
2057 | + { |
2058 | + list = new Gee.ArrayList<DesktopFileInfo> (); |
2059 | + mimetype_map[mime_type] = list; |
2060 | + } |
2061 | + list.add (dfi); |
2062 | + } |
2063 | + } |
2064 | + } |
2065 | + |
2066 | + private async void load_mime_parents_from_file (string fi) |
2067 | + { |
2068 | + var file = File.new_for_path (fi); |
2069 | + bool exists = yield Utils.query_exists_async (file); |
2070 | + if (!exists) return; |
2071 | + try |
2072 | + { |
2073 | + var fis = yield file.read_async (GLib.Priority.DEFAULT); |
2074 | + var dis = new DataInputStream (fis); |
2075 | + string line = null; |
2076 | + string[] mimes = null; |
2077 | + int len = 0; |
2078 | + // Read lines until end of file (null) is reached |
2079 | + do { |
2080 | + line = yield dis.read_line_async (GLib.Priority.DEFAULT); |
2081 | + if (line == null) break; |
2082 | + if (line.has_prefix ("#")) continue; //comment line |
2083 | + mimes = line.split (" "); |
2084 | + len = (int)GLib.strv_length (mimes); |
2085 | + if (len != 2) continue; |
2086 | + // cannot be parent of myself! |
2087 | + if (mimes[0] == mimes[1]) continue; |
2088 | + //debug ("Map %s -> %s", mimes[0], mimes[1]); |
2089 | + mimetype_parent_map.set (mimes[0], mimes[1]); |
2090 | + } while (true); |
2091 | + } catch (GLib.Error err) { /* can't read file */ } |
2092 | + } |
2093 | + |
2094 | + private void add_dfi_for_mime (string mime, Gee.Set<DesktopFileInfo> ret) |
2095 | + { |
2096 | + var dfis = mimetype_map[mime]; |
2097 | + if (dfis != null) ret.add_all (dfis); |
2098 | + |
2099 | + var parents = mimetype_parent_map[mime]; |
2100 | + if (parents == null) return; |
2101 | + foreach (string parent in parents) |
2102 | + add_dfi_for_mime (parent, ret); |
2103 | + } |
2104 | + |
2105 | + // retuns desktop files available on the system (without hidden ones) |
2106 | + public Gee.List<DesktopFileInfo> get_desktop_files () |
2107 | + { |
2108 | + return non_hidden_desktop_files.read_only_view; |
2109 | + } |
2110 | + |
2111 | + // returns all desktop files available on the system (even the ones which |
2112 | + // are hidden by default) |
2113 | + public Gee.List<DesktopFileInfo> get_all_desktop_files () |
2114 | + { |
2115 | + return all_desktop_files.read_only_view; |
2116 | + } |
2117 | + |
2118 | + public Gee.List<DesktopFileInfo> get_desktop_files_for_type (string mime_type) |
2119 | + { |
2120 | + var dfi_set = new Gee.HashSet<DesktopFileInfo> (); |
2121 | + add_dfi_for_mime (mime_type, dfi_set); |
2122 | + var ret = new Gee.ArrayList<DesktopFileInfo> (); |
2123 | + ret.add_all (dfi_set); |
2124 | + return ret; |
2125 | + } |
2126 | + |
2127 | + public Gee.List<DesktopFileInfo> get_desktop_files_for_exec (string exec) |
2128 | + { |
2129 | + return exec_map[exec] ?? new Gee.ArrayList<DesktopFileInfo> (); |
2130 | + } |
2131 | + |
2132 | + public DesktopFileInfo? get_desktop_file_for_id (string desktop_id) |
2133 | + { |
2134 | + return desktop_id_map[desktop_id]; |
2135 | + } |
2136 | + } |
2137 | +} |
2138 | + |
2139 | |
2140 | === added file 'lib/synapse-core/match.vala' |
2141 | --- lib/synapse-core/match.vala 1970-01-01 00:00:00 +0000 |
2142 | +++ lib/synapse-core/match.vala 2014-06-16 07:45:32 +0000 |
2143 | @@ -0,0 +1,144 @@ |
2144 | +/* |
2145 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
2146 | + * |
2147 | + * This library is free software; you can redistribute it and/or |
2148 | + * modify it under the terms of the GNU Lesser General Public |
2149 | + * License as published by the Free Software Foundation; either |
2150 | + * version 2.1 of the License, or (at your option) any later version. |
2151 | + * |
2152 | + * This library is distributed in the hope that it will be useful, |
2153 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2154 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
2155 | + * Lesser General Public License for more details. |
2156 | + * |
2157 | + * You should have received a copy of the GNU Lesser General Public License |
2158 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2159 | + * |
2160 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
2161 | + * Alberto Aldegheri <albyrock87+dev@gmail.com> |
2162 | + */ |
2163 | + |
2164 | +namespace Synapse |
2165 | +{ |
2166 | + public enum MatchType |
2167 | + { |
2168 | + UNKNOWN = 0, |
2169 | + TEXT, |
2170 | + APPLICATION, |
2171 | + GENERIC_URI, |
2172 | + ACTION, |
2173 | + SEARCH, |
2174 | + CONTACT |
2175 | + } |
2176 | + |
2177 | + public interface Match: Object |
2178 | + { |
2179 | + public enum Score |
2180 | + { |
2181 | + INCREMENT_MINOR = 2000, |
2182 | + INCREMENT_SMALL = 5000, |
2183 | + INCREMENT_MEDIUM = 10000, |
2184 | + INCREMENT_LARGE = 20000, |
2185 | + URI_PENALTY = 15000, |
2186 | + |
2187 | + POOR = 50000, |
2188 | + BELOW_AVERAGE = 60000, |
2189 | + AVERAGE = 70000, |
2190 | + ABOVE_AVERAGE = 75000, |
2191 | + GOOD = 80000, |
2192 | + VERY_GOOD = 85000, |
2193 | + EXCELLENT = 90000, |
2194 | + |
2195 | + HIGHEST = 100000 |
2196 | + } |
2197 | + |
2198 | + // properties |
2199 | + public abstract string title { get; construct set; } |
2200 | + public abstract string description { get; set; } |
2201 | + public abstract string icon_name { get; construct set; } |
2202 | + public abstract bool has_thumbnail { get; construct set; } |
2203 | + public abstract string thumbnail_path { get; construct set; } |
2204 | + public abstract MatchType match_type { get; construct set; } |
2205 | + |
2206 | + public virtual void execute (Match? match) |
2207 | + { |
2208 | + Utils.Logger.error (this, "execute () is not implemented"); |
2209 | + } |
2210 | + |
2211 | + public virtual void execute_with_target (Match? source, Match? target = null) |
2212 | + { |
2213 | + if (target == null) execute (source); |
2214 | + else Utils.Logger.error (this, "execute () is not implemented"); |
2215 | + } |
2216 | + |
2217 | + public virtual bool needs_target () { |
2218 | + return false; |
2219 | + } |
2220 | + |
2221 | + public virtual QueryFlags target_flags () |
2222 | + { |
2223 | + return QueryFlags.ALL; |
2224 | + } |
2225 | + |
2226 | + public signal void executed (); |
2227 | + } |
2228 | + |
2229 | + public interface ApplicationMatch: Match |
2230 | + { |
2231 | + public abstract AppInfo? app_info { get; set; } |
2232 | + public abstract bool needs_terminal { get; set; } |
2233 | + public abstract string? filename { get; construct set; } |
2234 | + } |
2235 | + |
2236 | + public interface UriMatch: Match |
2237 | + { |
2238 | + public abstract string uri { get; set; } |
2239 | + public abstract QueryFlags file_type { get; set; } |
2240 | + public abstract string mime_type { get; set; } |
2241 | + } |
2242 | + |
2243 | + public interface ContactMatch: Match |
2244 | + { |
2245 | + public abstract void send_message (string message, bool present); |
2246 | + public abstract void open_chat (); |
2247 | + } |
2248 | + |
2249 | + public interface ExtendedInfo: Match |
2250 | + { |
2251 | + public abstract string? extended_info { get; set; } |
2252 | + } |
2253 | + |
2254 | + public enum TextOrigin |
2255 | + { |
2256 | + UNKNOWN, |
2257 | + CLIPBOARD |
2258 | + } |
2259 | + |
2260 | + public interface TextMatch: Match |
2261 | + { |
2262 | + public abstract TextOrigin text_origin { get; set; } |
2263 | + public abstract string get_text (); |
2264 | + } |
2265 | + |
2266 | + public interface SearchMatch: Match, SearchProvider |
2267 | + { |
2268 | + public abstract Match search_source { get; set; } |
2269 | + } |
2270 | + |
2271 | + public class DefaultMatch: Object, Match |
2272 | + { |
2273 | + public string title { get; construct set; } |
2274 | + public string description { get; set; } |
2275 | + public string icon_name { get; construct set; } |
2276 | + public bool has_thumbnail { get; construct set; } |
2277 | + public string thumbnail_path { get; construct set; } |
2278 | + public MatchType match_type { get; construct set; } |
2279 | + |
2280 | + public DefaultMatch (string query_string) |
2281 | + { |
2282 | + Object (title: query_string, description: "", has_thumbnail: false, |
2283 | + icon_name: "unknown", match_type: MatchType.UNKNOWN); |
2284 | + } |
2285 | + } |
2286 | +} |
2287 | + |
2288 | |
2289 | === added file 'lib/synapse-core/plugin.vala' |
2290 | --- lib/synapse-core/plugin.vala 1970-01-01 00:00:00 +0000 |
2291 | +++ lib/synapse-core/plugin.vala 2014-06-16 07:45:32 +0000 |
2292 | @@ -0,0 +1,59 @@ |
2293 | +/* |
2294 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
2295 | + * |
2296 | + * This library is free software; you can redistribute it and/or |
2297 | + * modify it under the terms of the GNU Lesser General Public |
2298 | + * License as published by the Free Software Foundation; either |
2299 | + * version 2.1 of the License, or (at your option) any later version. |
2300 | + * |
2301 | + * This library is distributed in the hope that it will be useful, |
2302 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2303 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
2304 | + * Lesser General Public License for more details. |
2305 | + * |
2306 | + * You should have received a copy of the GNU Lesser General Public License |
2307 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2308 | + * |
2309 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
2310 | + * |
2311 | + */ |
2312 | + |
2313 | +namespace Synapse |
2314 | +{ |
2315 | + public interface Activatable : Object |
2316 | + { |
2317 | + // this property will eventually go away |
2318 | + public abstract bool enabled { get; set; default = true; } |
2319 | + |
2320 | + public abstract void activate (); |
2321 | + public abstract void deactivate (); |
2322 | + } |
2323 | + |
2324 | + public interface Configurable : Object |
2325 | + { |
2326 | + public abstract Gtk.Widget create_config_widget (); |
2327 | + } |
2328 | + |
2329 | + public interface ItemProvider : Activatable |
2330 | + { |
2331 | + public abstract async ResultSet? search (Query query) throws SearchError; |
2332 | + public virtual bool handles_query (Query query) |
2333 | + { |
2334 | + return true; |
2335 | + } |
2336 | + public virtual bool handles_empty_query () |
2337 | + { |
2338 | + return false; |
2339 | + } |
2340 | + } |
2341 | + |
2342 | + public interface ActionProvider : Activatable |
2343 | + { |
2344 | + public abstract ResultSet? find_for_match (ref Query query, Match match); |
2345 | + public virtual bool handles_unknown () |
2346 | + { |
2347 | + return false; |
2348 | + } |
2349 | + } |
2350 | +} |
2351 | + |
2352 | |
2353 | === added file 'lib/synapse-core/query.vala' |
2354 | --- lib/synapse-core/query.vala 1970-01-01 00:00:00 +0000 |
2355 | +++ lib/synapse-core/query.vala 2014-06-16 07:45:32 +0000 |
2356 | @@ -0,0 +1,296 @@ |
2357 | +/* |
2358 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
2359 | + * |
2360 | + * This library is free software; you can redistribute it and/or |
2361 | + * modify it under the terms of the GNU Lesser General Public |
2362 | + * License as published by the Free Software Foundation; either |
2363 | + * version 2.1 of the License, or (at your option) any later version. |
2364 | + * |
2365 | + * This library is distributed in the hope that it will be useful, |
2366 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2367 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
2368 | + * Lesser General Public License for more details. |
2369 | + * |
2370 | + * You should have received a copy of the GNU Lesser General Public License |
2371 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2372 | + * |
2373 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
2374 | + * |
2375 | + */ |
2376 | + |
2377 | +namespace Synapse |
2378 | +{ |
2379 | + [Flags] |
2380 | + public enum QueryFlags |
2381 | + { |
2382 | + /* HowTo create categories (32bit). |
2383 | + * Authored by Alberto Aldegheri <albyrock87+dev@gmail.com> |
2384 | + * Categories are "stored" in 3 Levels: |
2385 | + * Super-Category |
2386 | + * -> Category |
2387 | + * ----> Sub-Category |
2388 | + * ------------------------------------ |
2389 | + * if (Super-Category does NOT have childs): |
2390 | + * SUPER = 1 << FreeBitPosition |
2391 | + * else: |
2392 | + * if (Category does NOT have childs) |
2393 | + * CATEGORY = 1 << FreeBitPosition |
2394 | + * else |
2395 | + * SUB = 1 << FreeBitPosition |
2396 | + * CATEGORY = OR ([subcategories, ...]); |
2397 | + * |
2398 | + * SUPER = OR ([categories, ...]); |
2399 | + * |
2400 | + * |
2401 | + * Remember: |
2402 | + * if you add or remove a category, |
2403 | + * change labels in UIInterface.CategoryConfig.init_labels |
2404 | + * |
2405 | + */ |
2406 | + INCLUDE_REMOTE = 1 << 0, |
2407 | + UNCATEGORIZED = 1 << 1, |
2408 | + |
2409 | + APPLICATIONS = 1 << 2, |
2410 | + |
2411 | + ACTIONS = 1 << 3, |
2412 | + |
2413 | + AUDIO = 1 << 4, |
2414 | + VIDEO = 1 << 5, |
2415 | + DOCUMENTS = 1 << 6, |
2416 | + IMAGES = 1 << 7, |
2417 | + FILES = AUDIO | VIDEO | DOCUMENTS | IMAGES, |
2418 | + |
2419 | + PLACES = 1 << 8, |
2420 | + |
2421 | + // FIXME: shouldn't this be FILES | INCLUDE_REMOTE? |
2422 | + INTERNET = 1 << 9, |
2423 | + |
2424 | + // FIXME: Text Query flag? kinda weird, why do we have this here? |
2425 | + TEXT = 1 << 10, |
2426 | + |
2427 | + CONTACTS = 1 << 11, |
2428 | + |
2429 | + ALL = 0xFFFFFFFF, |
2430 | + LOCAL_CONTENT = ALL ^ QueryFlags.INCLUDE_REMOTE |
2431 | + } |
2432 | + |
2433 | + [Flags] |
2434 | + public enum MatcherFlags |
2435 | + { |
2436 | + NO_REVERSED = 1 << 0, |
2437 | + NO_SUBSTRING = 1 << 1, |
2438 | + NO_PARTIAL = 1 << 2, |
2439 | + NO_FUZZY = 1 << 3 |
2440 | + } |
2441 | + |
2442 | + public struct Query |
2443 | + { |
2444 | + string query_string; |
2445 | + string query_string_folded; |
2446 | + Cancellable cancellable; |
2447 | + QueryFlags query_type; |
2448 | + uint max_results; |
2449 | + uint query_id; |
2450 | + |
2451 | + public Query (uint query_id, |
2452 | + string query, |
2453 | + QueryFlags flags = QueryFlags.LOCAL_CONTENT, |
2454 | + uint num_results = 96) |
2455 | + { |
2456 | + this.query_id = query_id; |
2457 | + this.query_string = query; |
2458 | + this.query_string_folded = query.casefold (); |
2459 | + this.query_type = flags; |
2460 | + this.max_results = num_results; |
2461 | + } |
2462 | + |
2463 | + public bool is_cancelled () |
2464 | + { |
2465 | + return cancellable.is_cancelled (); |
2466 | + } |
2467 | + |
2468 | + public void check_cancellable () throws SearchError |
2469 | + { |
2470 | + if (cancellable.is_cancelled ()) |
2471 | + { |
2472 | + throw new SearchError.SEARCH_CANCELLED ("Cancelled"); |
2473 | + } |
2474 | + } |
2475 | + |
2476 | + public static Gee.List<Gee.Map.Entry<Regex, int>> |
2477 | + get_matchers_for_query (string query, |
2478 | + MatcherFlags match_flags = 0, |
2479 | + RegexCompileFlags flags = GLib.RegexCompileFlags.OPTIMIZE) |
2480 | + { |
2481 | + /* create a couple of regexes and try to help with matching |
2482 | + * match with these regular expressions (with descending score): |
2483 | + * 1) ^query$ |
2484 | + * 2) ^query |
2485 | + * 3) \bquery |
2486 | + * 4) split to words and seach \bword1.+\bword2 (if there are 2+ words) |
2487 | + * 5) query |
2488 | + * 6) split to characters and search \bq.+\bu.+\be.+\br.+\by |
2489 | + * 7) split to characters and search \bq.*u.*e.*r.*y |
2490 | + * |
2491 | + * The set of returned regular expressions depends on MatcherFlags. |
2492 | + */ |
2493 | + |
2494 | + var results = new Gee.HashMap<Regex, int> (); |
2495 | + Regex re; |
2496 | + |
2497 | + try |
2498 | + { |
2499 | + re = new Regex ("^(%s)$".printf (Regex.escape_string (query)), flags); |
2500 | + results[re] = Match.Score.HIGHEST; |
2501 | + } |
2502 | + catch (RegexError err) |
2503 | + { |
2504 | + } |
2505 | + |
2506 | + try |
2507 | + { |
2508 | + re = new Regex ("^(%s)".printf (Regex.escape_string (query)), flags); |
2509 | + results[re] = Match.Score.EXCELLENT; |
2510 | + } |
2511 | + catch (RegexError err) |
2512 | + { |
2513 | + } |
2514 | + |
2515 | + try |
2516 | + { |
2517 | + re = new Regex ("\\b(%s)".printf (Regex.escape_string (query)), flags); |
2518 | + results[re] = Match.Score.VERY_GOOD; |
2519 | + } |
2520 | + catch (RegexError err) |
2521 | + { |
2522 | + } |
2523 | + |
2524 | + // split to individual chars |
2525 | + string[] individual_words = Regex.split_simple ("\\s+", query.strip ()); |
2526 | + if (individual_words.length >= 2) |
2527 | + { |
2528 | + string[] escaped_words = {}; |
2529 | + foreach (unowned string word in individual_words) |
2530 | + { |
2531 | + escaped_words += Regex.escape_string (word); |
2532 | + } |
2533 | + string pattern = "\\b(%s)".printf (string.joinv (").+\\b(", |
2534 | + escaped_words)); |
2535 | + |
2536 | + try |
2537 | + { |
2538 | + re = new Regex (pattern, flags); |
2539 | + results[re] = Match.Score.GOOD; |
2540 | + } |
2541 | + catch (RegexError err) |
2542 | + { |
2543 | + } |
2544 | + |
2545 | + // FIXME: do something generic here |
2546 | + if (!(MatcherFlags.NO_REVERSED in match_flags)) |
2547 | + { |
2548 | + if (escaped_words.length == 2) |
2549 | + { |
2550 | + var reversed = "\\b(%s)".printf (string.join (").+\\b(", |
2551 | + escaped_words[1], |
2552 | + escaped_words[0], |
2553 | + null)); |
2554 | + try |
2555 | + { |
2556 | + re = new Regex (reversed, flags); |
2557 | + results[re] = Match.Score.GOOD - Match.Score.INCREMENT_MINOR; |
2558 | + } |
2559 | + catch (RegexError err) |
2560 | + { |
2561 | + } |
2562 | + } |
2563 | + else |
2564 | + { |
2565 | + // not too nice, but is quite fast to compute |
2566 | + var orred = "\\b((?:%s))".printf (string.joinv (")|(?:", escaped_words)); |
2567 | + var any_order = ""; |
2568 | + for (int i=0; i<escaped_words.length; i++) |
2569 | + { |
2570 | + bool is_last = i == escaped_words.length - 1; |
2571 | + any_order += orred; |
2572 | + if (!is_last) any_order += ".+"; |
2573 | + } |
2574 | + try |
2575 | + { |
2576 | + re = new Regex (any_order, flags); |
2577 | + results[re] = Match.Score.AVERAGE + Match.Score.INCREMENT_MINOR; |
2578 | + } |
2579 | + catch (RegexError err) |
2580 | + { |
2581 | + } |
2582 | + } |
2583 | + } |
2584 | + } |
2585 | + |
2586 | + if (!(MatcherFlags.NO_SUBSTRING in match_flags)) |
2587 | + { |
2588 | + try |
2589 | + { |
2590 | + re = new Regex ("(%s)".printf (Regex.escape_string (query)), flags); |
2591 | + results[re] = Match.Score.BELOW_AVERAGE; |
2592 | + } |
2593 | + catch (RegexError err) |
2594 | + { |
2595 | + } |
2596 | + } |
2597 | + |
2598 | + // split to individual characters |
2599 | + string[] individual_chars = Regex.split_simple ("\\s*", query); |
2600 | + string[] escaped_chars = {}; |
2601 | + foreach (unowned string word in individual_chars) |
2602 | + { |
2603 | + escaped_chars += Regex.escape_string (word); |
2604 | + } |
2605 | + |
2606 | + // make "aj" match "Activity Journal" |
2607 | + if (!(MatcherFlags.NO_PARTIAL in match_flags) && |
2608 | + individual_words.length == 1 && individual_chars.length <= 5) |
2609 | + { |
2610 | + string pattern = "\\b(%s)".printf (string.joinv (").+\\b(", |
2611 | + escaped_chars)); |
2612 | + try |
2613 | + { |
2614 | + re = new Regex (pattern, flags); |
2615 | + results[re] = Match.Score.ABOVE_AVERAGE; |
2616 | + } |
2617 | + catch (RegexError err) |
2618 | + { |
2619 | + } |
2620 | + } |
2621 | + |
2622 | + if (!(MatcherFlags.NO_FUZZY in match_flags) && escaped_chars.length > 0) |
2623 | + { |
2624 | + string pattern = "\\b(%s)".printf (string.joinv (").*(", |
2625 | + escaped_chars)); |
2626 | + try |
2627 | + { |
2628 | + re = new Regex (pattern, flags); |
2629 | + results[re] = Match.Score.POOR; |
2630 | + } |
2631 | + catch (RegexError err) |
2632 | + { |
2633 | + } |
2634 | + } |
2635 | + |
2636 | + var sorted_results = new Gee.ArrayList<Gee.Map.Entry<Regex, int>> (); |
2637 | + var entries = results.entries; |
2638 | + // FIXME: why it doesn't work without this? |
2639 | + sorted_results.set_data ("entries-ref", entries); |
2640 | + sorted_results.add_all (entries); |
2641 | + sorted_results.sort ((a, b) => |
2642 | + { |
2643 | + unowned Gee.Map.Entry<Regex, int> e1 = (Gee.Map.Entry<Regex, int>) a; |
2644 | + unowned Gee.Map.Entry<Regex, int> e2 = (Gee.Map.Entry<Regex, int>) b; |
2645 | + return e2.value - e1.value; |
2646 | + }); |
2647 | + |
2648 | + return sorted_results; |
2649 | + } |
2650 | + } |
2651 | +} |
2652 | + |
2653 | |
2654 | === added file 'lib/synapse-core/relevancy-backend-zg.vala' |
2655 | --- lib/synapse-core/relevancy-backend-zg.vala 1970-01-01 00:00:00 +0000 |
2656 | +++ lib/synapse-core/relevancy-backend-zg.vala 2014-06-16 07:45:32 +0000 |
2657 | @@ -0,0 +1,308 @@ |
2658 | +/* |
2659 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
2660 | + * |
2661 | + * This library is free software; you can redistribute it and/or |
2662 | + * modify it under the terms of the GNU Lesser General Public |
2663 | + * License as published by the Free Software Foundation; either |
2664 | + * version 2.1 of the License, or (at your option) any later version. |
2665 | + * |
2666 | + * This library is distributed in the hope that it will be useful, |
2667 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2668 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
2669 | + * Lesser General Public License for more details. |
2670 | + * |
2671 | + * You should have received a copy of the GNU Lesser General Public License |
2672 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2673 | + * |
2674 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
2675 | + * |
2676 | + */ |
2677 | + |
2678 | +namespace Synapse |
2679 | +{ |
2680 | + private class ZeitgeistRelevancyBackend: Object, RelevancyBackend |
2681 | + { |
2682 | + private Zeitgeist.Log zg_log; |
2683 | + private Zeitgeist.DataSourceRegistry zg_dsr; |
2684 | + private Gee.Map<string, int> application_popularity; |
2685 | + private Gee.Map<string, int> uri_popularity; |
2686 | + private bool has_datahub_gio_module = false; |
2687 | + |
2688 | + private const float MULTIPLIER = 65535.0f; |
2689 | + |
2690 | + construct |
2691 | + { |
2692 | + zg_log = new Zeitgeist.Log (); |
2693 | + application_popularity = new Gee.HashMap<string, int> (); |
2694 | + uri_popularity = new Gee.HashMap<string, int> (); |
2695 | + |
2696 | + refresh_popularity (); |
2697 | + check_data_sources.begin (); |
2698 | + |
2699 | + Timeout.add_seconds (60*30, refresh_popularity); |
2700 | + } |
2701 | + |
2702 | + private async void check_data_sources () |
2703 | + { |
2704 | + zg_dsr = new Zeitgeist.DataSourceRegistry (); |
2705 | + try |
2706 | + { |
2707 | + var ptr_arr = yield zg_dsr.get_data_sources (null); |
2708 | + |
2709 | + for (uint i=0; i < ptr_arr.len; i++) |
2710 | + { |
2711 | + unowned Zeitgeist.DataSource ds; |
2712 | + ds = (Zeitgeist.DataSource) ptr_arr.index (i); |
2713 | + if (ds.get_unique_id () == "com.zeitgeist-project,datahub,gio-launch-listener" |
2714 | + && ds.is_enabled ()) |
2715 | + { |
2716 | + has_datahub_gio_module = true; |
2717 | + break; |
2718 | + } |
2719 | + } |
2720 | + } |
2721 | + catch (Error err) |
2722 | + { |
2723 | + warning ("Unable to check Zeitgeist data sources: %s", err.message); |
2724 | + } |
2725 | + } |
2726 | + |
2727 | + private bool refresh_popularity () |
2728 | + { |
2729 | + load_application_relevancies.begin (); |
2730 | + load_uri_relevancies.begin (); |
2731 | + return true; |
2732 | + } |
2733 | + |
2734 | + private async void load_application_relevancies () |
2735 | + { |
2736 | + Idle.add (load_application_relevancies.callback, Priority.LOW); |
2737 | + yield; |
2738 | + |
2739 | + int64 end = Zeitgeist.Timestamp.now (); |
2740 | + int64 start = end - Zeitgeist.Timestamp.WEEK * 4; |
2741 | + Zeitgeist.TimeRange tr = new Zeitgeist.TimeRange (start, end); |
2742 | + |
2743 | + var event = new Zeitgeist.Event (); |
2744 | + event.set_interpretation ("!" + Zeitgeist.ZG_LEAVE_EVENT); |
2745 | + var subject = new Zeitgeist.Subject (); |
2746 | + subject.set_interpretation (Zeitgeist.NFO_SOFTWARE); |
2747 | + subject.set_uri ("application://*"); |
2748 | + event.add_subject (subject); |
2749 | + |
2750 | + var ptr_arr = new PtrArray (); |
2751 | + ptr_arr.add (event); |
2752 | + |
2753 | + Zeitgeist.ResultSet rs; |
2754 | + |
2755 | + try |
2756 | + { |
2757 | + rs = yield zg_log.find_events (tr, (owned) ptr_arr, |
2758 | + Zeitgeist.StorageState.ANY, |
2759 | + 256, |
2760 | + Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS, |
2761 | + null); |
2762 | + |
2763 | + application_popularity.clear (); |
2764 | + uint size = rs.size (); |
2765 | + uint index = 0; |
2766 | + |
2767 | + // Zeitgeist (0.6) doesn't have any stats API, so let's approximate |
2768 | + |
2769 | + foreach (Zeitgeist.Event e in rs) |
2770 | + { |
2771 | + if (e.num_subjects () <= 0) continue; |
2772 | + Zeitgeist.Subject s = e.get_subject (0); |
2773 | + |
2774 | + float power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0> |
2775 | + float relevancy = 1.0f / Math.powf (index + 1, power); |
2776 | + application_popularity[s.get_uri ()] = (int)(relevancy * MULTIPLIER); |
2777 | + |
2778 | + index++; |
2779 | + } |
2780 | + } |
2781 | + catch (Error err) |
2782 | + { |
2783 | + warning ("%s", err.message); |
2784 | + return; |
2785 | + } |
2786 | + } |
2787 | + |
2788 | + private async void load_uri_relevancies () |
2789 | + { |
2790 | + Idle.add (load_uri_relevancies.callback, Priority.LOW); |
2791 | + yield; |
2792 | + |
2793 | + int64 end = Zeitgeist.Timestamp.now (); |
2794 | + int64 start = end - Zeitgeist.Timestamp.WEEK * 4; |
2795 | + Zeitgeist.TimeRange tr = new Zeitgeist.TimeRange (start, end); |
2796 | + |
2797 | + var event = new Zeitgeist.Event (); |
2798 | + event.set_interpretation ("!" + Zeitgeist.ZG_LEAVE_EVENT); |
2799 | + var subject = new Zeitgeist.Subject (); |
2800 | + subject.set_interpretation ("!" + Zeitgeist.NFO_SOFTWARE); |
2801 | + subject.set_uri ("file://*"); |
2802 | + event.add_subject (subject); |
2803 | + |
2804 | + var ptr_arr = new PtrArray (); |
2805 | + ptr_arr.add (event); |
2806 | + |
2807 | + Zeitgeist.ResultSet rs; |
2808 | + Gee.Map<string, int> popularity_map = new Gee.HashMap<string, int> (); |
2809 | + |
2810 | + try |
2811 | + { |
2812 | + uint size, index; |
2813 | + float power, relevancy; |
2814 | + /* Get popularity for file uris */ |
2815 | + rs = yield zg_log.find_events (tr, (owned) ptr_arr, |
2816 | + Zeitgeist.StorageState.ANY, |
2817 | + 256, |
2818 | + Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS, |
2819 | + null); |
2820 | + |
2821 | + size = rs.size (); |
2822 | + index = 0; |
2823 | + |
2824 | + // Zeitgeist (0.6) doesn't have any stats API, so let's approximate |
2825 | + |
2826 | + foreach (Zeitgeist.Event e1 in rs) |
2827 | + { |
2828 | + if (e1.num_subjects () <= 0) continue; |
2829 | + Zeitgeist.Subject s1 = e1.get_subject (0); |
2830 | + |
2831 | + power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0> |
2832 | + relevancy = 1.0f / Math.powf (index + 1, power); |
2833 | + popularity_map[s1.get_uri ()] = (int)(relevancy * MULTIPLIER); |
2834 | + |
2835 | + index++; |
2836 | + } |
2837 | + |
2838 | + /* Get popularity for web uris */ |
2839 | + subject.set_interpretation (Zeitgeist.NFO_WEBSITE); |
2840 | + subject.set_uri (""); |
2841 | + ptr_arr = new PtrArray (); |
2842 | + ptr_arr.add (event); |
2843 | + |
2844 | + rs = yield zg_log.find_events (tr, (owned) ptr_arr, |
2845 | + Zeitgeist.StorageState.ANY, |
2846 | + 128, |
2847 | + Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS, |
2848 | + null); |
2849 | + |
2850 | + size = rs.size (); |
2851 | + index = 0; |
2852 | + |
2853 | + // Zeitgeist (0.6) doesn't have any stats API, so let's approximate |
2854 | + |
2855 | + foreach (Zeitgeist.Event e2 in rs) |
2856 | + { |
2857 | + if (e2.num_subjects () <= 0) continue; |
2858 | + Zeitgeist.Subject s2 = e2.get_subject (0); |
2859 | + |
2860 | + power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0> |
2861 | + relevancy = 1.0f / Math.powf (index + 1, power); |
2862 | + popularity_map[s2.get_uri ()] = (int)(relevancy * MULTIPLIER); |
2863 | + |
2864 | + index++; |
2865 | + } |
2866 | + } |
2867 | + catch (Error err) |
2868 | + { |
2869 | + warning ("%s", err.message); |
2870 | + } |
2871 | + |
2872 | + uri_popularity = popularity_map; |
2873 | + } |
2874 | + |
2875 | + public float get_application_popularity (string desktop_id) |
2876 | + { |
2877 | + if (application_popularity.has_key (desktop_id)) |
2878 | + { |
2879 | + return application_popularity[desktop_id] / MULTIPLIER; |
2880 | + } |
2881 | + |
2882 | + return 0.0f; |
2883 | + } |
2884 | + |
2885 | + public float get_uri_popularity (string uri) |
2886 | + { |
2887 | + if (uri_popularity.has_key (uri)) |
2888 | + { |
2889 | + return uri_popularity[uri] / MULTIPLIER; |
2890 | + } |
2891 | + |
2892 | + return 0.0f; |
2893 | + } |
2894 | + |
2895 | + private void reload_relevancies () |
2896 | + { |
2897 | + Idle.add_full (Priority.LOW, () => |
2898 | + { |
2899 | + load_application_relevancies.begin (); |
2900 | + return false; |
2901 | + }); |
2902 | + } |
2903 | + |
2904 | + public void application_launched (AppInfo app_info) |
2905 | + { |
2906 | + // FIXME: get rid of this maverick-specific workaround |
2907 | + // detect if the Zeitgeist GIO module is installed |
2908 | + Type zg_gio_module = Type.from_name ("GAppLaunchHandlerZeitgeist"); |
2909 | + // FIXME: perhaps we should check app_info.should_show? |
2910 | + // but user specifically asked to open this, so probably not |
2911 | + // otoh the gio module won't pick it up if it's not should_show |
2912 | + if (zg_gio_module != 0) |
2913 | + { |
2914 | + Utils.Logger.debug (this, "libzg-gio-module detected, not pushing"); |
2915 | + reload_relevancies (); |
2916 | + return; |
2917 | + } |
2918 | + |
2919 | + if (has_datahub_gio_module) |
2920 | + { |
2921 | + reload_relevancies (); |
2922 | + return; |
2923 | + } |
2924 | + |
2925 | + string app_uri = null; |
2926 | + if (app_info.get_id () != null) |
2927 | + { |
2928 | + app_uri = "application://" + app_info.get_id (); |
2929 | + } |
2930 | + else if (app_info is DesktopAppInfo) |
2931 | + { |
2932 | + string? filename = (app_info as DesktopAppInfo).get_filename (); |
2933 | + if (filename == null) return; |
2934 | + app_uri = "application://" + Path.get_basename (filename); |
2935 | + } |
2936 | + |
2937 | + Utils.Logger.debug (this, "launched \"%s\", pushing to ZG", app_uri); |
2938 | + push_app_launch (app_uri, app_info.get_display_name ()); |
2939 | + |
2940 | + // and refresh |
2941 | + reload_relevancies (); |
2942 | + } |
2943 | + |
2944 | + private void push_app_launch (string app_uri, string? display_name) |
2945 | + { |
2946 | + //debug ("pushing launch event: %s [%s]", app_uri, display_name); |
2947 | + var event = new Zeitgeist.Event (); |
2948 | + var subject = new Zeitgeist.Subject (); |
2949 | + |
2950 | + event.set_actor ("application://synapse.desktop"); |
2951 | + event.set_interpretation (Zeitgeist.ZG_ACCESS_EVENT); |
2952 | + event.set_manifestation (Zeitgeist.ZG_USER_ACTIVITY); |
2953 | + event.add_subject (subject); |
2954 | + |
2955 | + subject.set_uri (app_uri); |
2956 | + subject.set_interpretation (Zeitgeist.NFO_SOFTWARE); |
2957 | + subject.set_manifestation (Zeitgeist.NFO_SOFTWARE_ITEM); |
2958 | + subject.set_mimetype ("application/x-desktop"); |
2959 | + subject.set_text (display_name); |
2960 | + |
2961 | + zg_log.insert_events_no_reply (event, null); |
2962 | + } |
2963 | + } |
2964 | +} |
2965 | + |
2966 | |
2967 | === added file 'lib/synapse-core/relevancy-service.vala' |
2968 | --- lib/synapse-core/relevancy-service.vala 1970-01-01 00:00:00 +0000 |
2969 | +++ lib/synapse-core/relevancy-service.vala 2014-06-16 07:45:32 +0000 |
2970 | @@ -0,0 +1,95 @@ |
2971 | +/* |
2972 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
2973 | + * |
2974 | + * This library is free software; you can redistribute it and/or |
2975 | + * modify it under the terms of the GNU Lesser General Public |
2976 | + * License as published by the Free Software Foundation; either |
2977 | + * version 2.1 of the License, or (at your option) any later version. |
2978 | + * |
2979 | + * This library is distributed in the hope that it will be useful, |
2980 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2981 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
2982 | + * Lesser General Public License for more details. |
2983 | + * |
2984 | + * You should have received a copy of the GNU Lesser General Public License |
2985 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2986 | + * |
2987 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
2988 | + * |
2989 | + */ |
2990 | + |
2991 | +namespace Synapse |
2992 | +{ |
2993 | + public interface RelevancyBackend: Object |
2994 | + { |
2995 | + public abstract float get_application_popularity (string desktop_id); |
2996 | + public abstract float get_uri_popularity (string uri); |
2997 | + |
2998 | + public abstract void application_launched (AppInfo app_info); |
2999 | + } |
3000 | + |
3001 | + public class RelevancyService : GLib.Object |
3002 | + { |
3003 | + // singleton that can be easily destroyed |
3004 | + private static unowned RelevancyService? instance; |
3005 | + public static RelevancyService get_default () |
3006 | + { |
3007 | + return instance ?? new RelevancyService (); |
3008 | + } |
3009 | + |
3010 | + private RelevancyService () |
3011 | + { |
3012 | + } |
3013 | + |
3014 | + ~RelevancyService () |
3015 | + { |
3016 | + } |
3017 | + |
3018 | + construct |
3019 | + { |
3020 | + instance = this; |
3021 | + this.add_weak_pointer (&instance); |
3022 | + |
3023 | + initialize_relevancy_backend (); |
3024 | + } |
3025 | + |
3026 | + private RelevancyBackend backend; |
3027 | + |
3028 | + private void initialize_relevancy_backend () |
3029 | + { |
3030 | + backend = new ZeitgeistRelevancyBackend (); |
3031 | + } |
3032 | + |
3033 | + public float get_application_popularity (string desktop_id) |
3034 | + { |
3035 | + if (backend == null) return 0.0f; |
3036 | + return backend.get_application_popularity (desktop_id); |
3037 | + } |
3038 | + |
3039 | + public float get_uri_popularity (string uri) |
3040 | + { |
3041 | + if (backend == null) return 0.0f; |
3042 | + return backend.get_uri_popularity (uri); |
3043 | + } |
3044 | + |
3045 | + public void application_launched (AppInfo app_info) |
3046 | + { |
3047 | + Utils.Logger.debug (this, "application launched"); |
3048 | + if (backend == null) return; |
3049 | + backend.application_launched (app_info); |
3050 | + } |
3051 | + |
3052 | + public static int compute_relevancy (int base_relevancy, float modifier) |
3053 | + { |
3054 | + // FIXME: let's experiment here |
3055 | + // the other idea is to use base_relevancy * (1.0f + modifier) |
3056 | + int relevancy = (int) (base_relevancy + modifier * Match.Score.INCREMENT_LARGE * 2); |
3057 | + //int relevancy = base_relevancy + (int) (modifier * Match.Score.HIGHEST); |
3058 | + return relevancy; |
3059 | + // FIXME: this clamping should be done, but it screws up the popularity |
3060 | + // for very popular items with high match score |
3061 | + //return int.min (relevancy, Match.Score.HIGHEST); |
3062 | + } |
3063 | + } |
3064 | +} |
3065 | + |
3066 | |
3067 | === added file 'lib/synapse-core/result-set.vala' |
3068 | --- lib/synapse-core/result-set.vala 1970-01-01 00:00:00 +0000 |
3069 | +++ lib/synapse-core/result-set.vala 2014-06-16 07:45:32 +0000 |
3070 | @@ -0,0 +1,121 @@ |
3071 | +/* |
3072 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
3073 | + * |
3074 | + * This library is free software; you can redistribute it and/or |
3075 | + * modify it under the terms of the GNU Lesser General Public |
3076 | + * License as published by the Free Software Foundation; either |
3077 | + * version 2.1 of the License, or (at your option) any later version. |
3078 | + * |
3079 | + * This library is distributed in the hope that it will be useful, |
3080 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3081 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
3082 | + * Lesser General Public License for more details. |
3083 | + * |
3084 | + * You should have received a copy of the GNU Lesser General Public License |
3085 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
3086 | + * |
3087 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
3088 | + * |
3089 | + */ |
3090 | + |
3091 | +namespace Synapse |
3092 | +{ |
3093 | + public class ResultSet : Object, Gee.Traversable<Match>, Gee.Iterable <Gee.Map.Entry <Match, int>> |
3094 | + { |
3095 | + protected Gee.Map<Match, int> matches; |
3096 | + protected Gee.Set<unowned string> uris; |
3097 | + |
3098 | + public ResultSet () |
3099 | + { |
3100 | + Object (); |
3101 | + } |
3102 | + |
3103 | + construct |
3104 | + { |
3105 | + matches = new Gee.HashMap<Match, int> (); |
3106 | + // Match.uri is not owned, so we can optimize here |
3107 | + uris = new Gee.HashSet<unowned string> (); |
3108 | + } |
3109 | + |
3110 | + public Type element_type |
3111 | + { |
3112 | + get { return matches.element_type; } |
3113 | + } |
3114 | + |
3115 | + public int size |
3116 | + { |
3117 | + get { return matches.size; } |
3118 | + } |
3119 | + |
3120 | + public Gee.Set<Match> keys |
3121 | + { |
3122 | + owned get { return matches.keys; } |
3123 | + } |
3124 | + |
3125 | + public Gee.Set<Gee.Map.Entry <Match, int>> entries |
3126 | + { |
3127 | + owned get { return matches.entries; } |
3128 | + } |
3129 | + |
3130 | + public Gee.Iterator<Gee.Map.Entry <Match, int>?> iterator () |
3131 | + { |
3132 | + return matches.iterator (); |
3133 | + } |
3134 | + |
3135 | + public bool foreach (Gee.ForallFunc<Match> func) |
3136 | + { |
3137 | + return matches.keys.foreach (func); |
3138 | + } |
3139 | + |
3140 | + public void add (Match match, int relevancy) |
3141 | + { |
3142 | + matches.set (match, relevancy); |
3143 | + |
3144 | + if (match is UriMatch) |
3145 | + { |
3146 | + unowned string uri = (match as UriMatch).uri; |
3147 | + if (uri != null && uri != "") |
3148 | + { |
3149 | + uris.add (uri); |
3150 | + } |
3151 | + } |
3152 | + } |
3153 | + |
3154 | + public void add_all (ResultSet? rs) |
3155 | + { |
3156 | + if (rs == null) return; |
3157 | + matches.set_all (rs.matches); |
3158 | + uris.add_all (rs.uris); |
3159 | + } |
3160 | + |
3161 | + public bool contains_uri (string uri) |
3162 | + { |
3163 | + return uri in uris; |
3164 | + } |
3165 | + |
3166 | + public Gee.List<Match> get_sorted_list () |
3167 | + { |
3168 | + var l = new Gee.ArrayList<Gee.Map.Entry<Match, int>> (); |
3169 | + l.add_all (matches.entries); |
3170 | + |
3171 | + l.sort ((a, b) => |
3172 | + { |
3173 | + unowned Gee.Map.Entry<Match, int> e1 = (Gee.Map.Entry<Match, int>) a; |
3174 | + unowned Gee.Map.Entry<Match, int> e2 = (Gee.Map.Entry<Match, int>) b; |
3175 | + int relevancy_delta = e2.value - e1.value; |
3176 | + if (relevancy_delta != 0) return relevancy_delta; |
3177 | + // FIXME: utf8 compare! |
3178 | + else return e1.key.title.ascii_casecmp (e2.key.title); |
3179 | + }); |
3180 | + |
3181 | + var sorted_list = new Gee.ArrayList<Match> (); |
3182 | + foreach (Gee.Map.Entry<Match, int> m in l) |
3183 | + { |
3184 | + sorted_list.add (m.key); |
3185 | + } |
3186 | + |
3187 | + return sorted_list; |
3188 | + } |
3189 | + } |
3190 | +} |
3191 | + |
3192 | |
3193 | === added file 'lib/synapse-core/utils.vala' |
3194 | --- lib/synapse-core/utils.vala 1970-01-01 00:00:00 +0000 |
3195 | +++ lib/synapse-core/utils.vala 2014-06-16 07:45:32 +0000 |
3196 | @@ -0,0 +1,439 @@ |
3197 | +/* |
3198 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
3199 | + * |
3200 | + * This library is free software; you can redistribute it and/or |
3201 | + * modify it under the terms of the GNU Lesser General Public |
3202 | + * License as published by the Free Software Foundation; either |
3203 | + * version 2.1 of the License, or (at your option) any later version. |
3204 | + * |
3205 | + * This library is distributed in the hope that it will be useful, |
3206 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3207 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
3208 | + * Lesser General Public License for more details. |
3209 | + * |
3210 | + * You should have received a copy of the GNU Lesser General Public License |
3211 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
3212 | + * |
3213 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
3214 | + * |
3215 | + */ |
3216 | + |
3217 | +namespace Synapse |
3218 | +{ |
3219 | + [CCode (gir_namespace = "SynapseUtils", gir_version = "1.0")] |
3220 | + namespace Utils |
3221 | + { |
3222 | + /* Make sure setlocale was called before calling this function |
3223 | + * (Gtk.init calls it automatically) |
3224 | + */ |
3225 | + public static string? remove_accents (string input) |
3226 | + { |
3227 | + string? result; |
3228 | + unowned string charset; |
3229 | + GLib.get_charset (out charset); |
3230 | + try |
3231 | + { |
3232 | + result = GLib.convert (input, input.length, |
3233 | + "US-ASCII//TRANSLIT", charset); |
3234 | + // no need to waste cpu cycles if the input is the same |
3235 | + if (input == result) return null; |
3236 | + } |
3237 | + catch (ConvertError err) |
3238 | + { |
3239 | + result = null; |
3240 | + } |
3241 | + |
3242 | + return result; |
3243 | + } |
3244 | + |
3245 | + public static string? remove_last_unichar (string input) |
3246 | + { |
3247 | + long char_count = input.char_count (); |
3248 | + |
3249 | + int len = input.index_of_nth_char (char_count - 1); |
3250 | + return input.substring (0, len); |
3251 | + } |
3252 | + |
3253 | + public static async bool query_exists_async (GLib.File f) |
3254 | + { |
3255 | + bool exists; |
3256 | + try |
3257 | + { |
3258 | + yield f.query_info_async (FileAttribute.STANDARD_TYPE, 0, 0, null); |
3259 | + exists = true; |
3260 | + } |
3261 | + catch (Error err) |
3262 | + { |
3263 | + exists = false; |
3264 | + } |
3265 | + |
3266 | + return exists; |
3267 | + } |
3268 | + |
3269 | + public string extract_type_name (Type obj_type) |
3270 | + { |
3271 | + string obj_class = obj_type.name (); |
3272 | + if (obj_class.has_prefix ("Synapse")) return obj_class.substring (7); |
3273 | + |
3274 | + return obj_class; |
3275 | + } |
3276 | + |
3277 | + public class Logger |
3278 | + { |
3279 | + protected const string RED = "\x1b[31m"; |
3280 | + protected const string GREEN = "\x1b[32m"; |
3281 | + protected const string YELLOW = "\x1b[33m"; |
3282 | + protected const string BLUE = "\x1b[34m"; |
3283 | + protected const string MAGENTA = "\x1b[35m"; |
3284 | + protected const string CYAN = "\x1b[36m"; |
3285 | + protected const string RESET = "\x1b[0m"; |
3286 | + |
3287 | + private static bool initialized = false; |
3288 | + private static bool show_debug = false; |
3289 | + |
3290 | + private static void log_internal (Object? obj, LogLevelFlags level, string format, va_list args) |
3291 | + { |
3292 | + if (!initialized) initialize (); |
3293 | + string desc = ""; |
3294 | + if (obj != null) |
3295 | + { |
3296 | + string obj_class = extract_type_name (obj.get_type ()); |
3297 | + desc = "%s[%s]%s ".printf (MAGENTA, obj_class, RESET); |
3298 | + } |
3299 | + logv ("Synapse", level, desc + format, args); |
3300 | + } |
3301 | + |
3302 | + private static void initialize () |
3303 | + { |
3304 | + var levels = LogLevelFlags.LEVEL_DEBUG | LogLevelFlags.LEVEL_INFO | |
3305 | + LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_CRITICAL | |
3306 | + LogLevelFlags.LEVEL_ERROR; |
3307 | + |
3308 | + string[] domains = |
3309 | + { |
3310 | + "Synapse", |
3311 | + "Gtk", |
3312 | + "Gdk", |
3313 | + "GLib", |
3314 | + "GLib-GObject", |
3315 | + "Pango", |
3316 | + "GdkPixbuf", |
3317 | + "GLib-GIO", |
3318 | + "GtkHotkey" |
3319 | + }; |
3320 | + foreach (unowned string domain in domains) |
3321 | + { |
3322 | + Log.set_handler (domain, levels, handler); |
3323 | + } |
3324 | + Log.set_handler (null, levels, handler); |
3325 | + |
3326 | + show_debug = Environment.get_variable ("SYNAPSE_DEBUG") != null; |
3327 | + initialized = true; |
3328 | + } |
3329 | + |
3330 | + public static bool debug_enabled () |
3331 | + { |
3332 | + if (!initialized) initialize (); |
3333 | + return show_debug; |
3334 | + } |
3335 | + |
3336 | + public static void log (Object? obj, string format, ...) |
3337 | + { |
3338 | + var args = va_list (); |
3339 | + log_internal (obj, LogLevelFlags.LEVEL_INFO, format, args); |
3340 | + } |
3341 | + |
3342 | + [Diagnostics] |
3343 | + public static void debug (Object? obj, string format, ...) |
3344 | + { |
3345 | + var args = va_list (); |
3346 | + log_internal (obj, LogLevelFlags.LEVEL_DEBUG, format, args); |
3347 | + } |
3348 | + |
3349 | + public static void warning (Object? obj, string format, ...) |
3350 | + { |
3351 | + var args = va_list (); |
3352 | + log_internal (obj, LogLevelFlags.LEVEL_WARNING, format, args); |
3353 | + } |
3354 | + |
3355 | + public static void error (Object? obj, string format, ...) |
3356 | + { |
3357 | + var args = va_list (); |
3358 | + log_internal (obj, LogLevelFlags.LEVEL_ERROR, format, args); |
3359 | + } |
3360 | + |
3361 | + protected static void handler (string? domain, LogLevelFlags level, string msg) |
3362 | + { |
3363 | + string header; |
3364 | + string domain_str = ""; |
3365 | + if (domain != null && domain != "Synapse") domain_str = domain + "-"; |
3366 | + var time_val = TimeVal (); |
3367 | + long time_str_len = time_val.tv_usec != 0 ? 15 : 8; |
3368 | + string cur_time = time_val.to_iso8601 ().substring (11, time_str_len); |
3369 | + if (level == LogLevelFlags.LEVEL_DEBUG) |
3370 | + { |
3371 | + if (!show_debug && domain_str == "") return; |
3372 | + header = @"$(GREEN)[$(cur_time) $(domain_str)Debug]$(RESET)"; |
3373 | + } |
3374 | + else if (level == LogLevelFlags.LEVEL_INFO) |
3375 | + { |
3376 | + header = @"$(BLUE)[$(cur_time) $(domain_str)Info]$(RESET)"; |
3377 | + } |
3378 | + else if (level == LogLevelFlags.LEVEL_WARNING) |
3379 | + { |
3380 | + header = @"$(RED)[$(cur_time) $(domain_str)Warning]$(RESET)"; |
3381 | + } |
3382 | + else if (level == LogLevelFlags.LEVEL_CRITICAL || level == LogLevelFlags.LEVEL_ERROR) |
3383 | + { |
3384 | + header = @"$(RED)[$(cur_time) $(domain_str)Critical]$(RESET)"; |
3385 | + } |
3386 | + else |
3387 | + { |
3388 | + header = @"$(YELLOW)[$(cur_time)]$(RESET)"; |
3389 | + } |
3390 | + |
3391 | + stdout.printf ("%s %s\n", header, msg); |
3392 | +#if 0 |
3393 | + void* buffer[10]; |
3394 | + int num = Linux.backtrace (&buffer, 10); |
3395 | + string[] symbols = Linux.backtrace_symbols (buffer, num); |
3396 | + if (symbols != null) |
3397 | + { |
3398 | + for (int i = 0; i < num; i++) stdout.printf ("%s\n", symbols[i]); |
3399 | + } |
3400 | +#endif |
3401 | + } |
3402 | + } |
3403 | + |
3404 | + [Compact] |
3405 | + private class DelegateWrapper |
3406 | + { |
3407 | + public SourceFunc callback; |
3408 | + |
3409 | + public DelegateWrapper (owned SourceFunc cb) |
3410 | + { |
3411 | + callback = (owned) cb; |
3412 | + } |
3413 | + } |
3414 | + /* |
3415 | + * Asynchronous Once. |
3416 | + * |
3417 | + * Usage: |
3418 | + * private AsyncOnce<string> once = new AsyncOnce<string> (); |
3419 | + * public async void foo () |
3420 | + * { |
3421 | + * if (!once.is_initialized ()) // not stricly necessary but improves perf |
3422 | + * { |
3423 | + * if (yield once.enter ()) |
3424 | + * { |
3425 | + * // this block will be executed only once, but the method |
3426 | + * // is reentrant; it's also recommended to wrap this block |
3427 | + * // in try { } and call once.leave() in finally { } |
3428 | + * // if any of the operations can throw an error |
3429 | + * var s = yield get_the_string (); |
3430 | + * once.leave (s); |
3431 | + * } |
3432 | + * } |
3433 | + * // if control reaches this point the once was initialized |
3434 | + * yield do_something_for_string (once.get_data ()); |
3435 | + * } |
3436 | + */ |
3437 | + public class AsyncOnce<G> |
3438 | + { |
3439 | + private enum OperationState |
3440 | + { |
3441 | + NOT_STARTED, |
3442 | + IN_PROGRESS, |
3443 | + DONE |
3444 | + } |
3445 | + |
3446 | + private G inner; |
3447 | + |
3448 | + private OperationState state; |
3449 | + private DelegateWrapper[] callbacks = {}; |
3450 | + |
3451 | + public AsyncOnce () |
3452 | + { |
3453 | + state = OperationState.NOT_STARTED; |
3454 | + } |
3455 | + |
3456 | + public unowned G get_data () |
3457 | + { |
3458 | + return inner; |
3459 | + } |
3460 | + |
3461 | + public bool is_initialized () |
3462 | + { |
3463 | + return state == OperationState.DONE; |
3464 | + } |
3465 | + |
3466 | + public async bool enter () |
3467 | + { |
3468 | + if (state == OperationState.NOT_STARTED) |
3469 | + { |
3470 | + state = OperationState.IN_PROGRESS; |
3471 | + return true; |
3472 | + } |
3473 | + else if (state == OperationState.IN_PROGRESS) |
3474 | + { |
3475 | + yield wait_async (); |
3476 | + } |
3477 | + |
3478 | + return false; |
3479 | + } |
3480 | + |
3481 | + public void leave (G result) |
3482 | + { |
3483 | + if (state != OperationState.IN_PROGRESS) |
3484 | + { |
3485 | + warning ("Incorrect usage of AsyncOnce"); |
3486 | + return; |
3487 | + } |
3488 | + state = OperationState.DONE; |
3489 | + inner = result; |
3490 | + notify_all (); |
3491 | + } |
3492 | + |
3493 | + /* Once probably shouldn't have this, but it's useful */ |
3494 | + public void reset () |
3495 | + { |
3496 | + if (state == OperationState.IN_PROGRESS) |
3497 | + { |
3498 | + warning ("AsyncOnce.reset() cannot be called in the middle of initialization."); |
3499 | + } |
3500 | + else |
3501 | + { |
3502 | + state = OperationState.NOT_STARTED; |
3503 | + inner = null; |
3504 | + } |
3505 | + } |
3506 | + |
3507 | + private void notify_all () |
3508 | + { |
3509 | + foreach (unowned DelegateWrapper wrapper in callbacks) |
3510 | + { |
3511 | + wrapper.callback (); |
3512 | + } |
3513 | + callbacks = {}; |
3514 | + } |
3515 | + |
3516 | + private async void wait_async () |
3517 | + { |
3518 | + callbacks += new DelegateWrapper (wait_async.callback); |
3519 | + yield; |
3520 | + } |
3521 | + } |
3522 | + |
3523 | + public class FileInfo |
3524 | + { |
3525 | + private static string interesting_attributes; |
3526 | + static construct |
3527 | + { |
3528 | + interesting_attributes = |
3529 | + string.join (",", FileAttribute.STANDARD_TYPE, |
3530 | + FileAttribute.STANDARD_IS_HIDDEN, |
3531 | + FileAttribute.STANDARD_IS_BACKUP, |
3532 | + FileAttribute.STANDARD_DISPLAY_NAME, |
3533 | + FileAttribute.STANDARD_ICON, |
3534 | + FileAttribute.STANDARD_FAST_CONTENT_TYPE, |
3535 | + FileAttribute.THUMBNAIL_PATH, |
3536 | + null); |
3537 | + } |
3538 | + |
3539 | + public string uri; |
3540 | + public string parse_name; |
3541 | + public QueryFlags file_type; |
3542 | + public UriMatch? match_obj; |
3543 | + private bool initialized; |
3544 | + private Type match_obj_type; |
3545 | + |
3546 | + public FileInfo (string uri, Type obj_type) |
3547 | + { |
3548 | + assert (obj_type.is_a (typeof (UriMatch))); |
3549 | + this.uri = uri; |
3550 | + this.match_obj = null; |
3551 | + this.match_obj_type = obj_type; |
3552 | + this.initialized = false; |
3553 | + this.file_type = QueryFlags.UNCATEGORIZED; |
3554 | + |
3555 | + var f = File.new_for_uri (uri); |
3556 | + this.parse_name = f.get_parse_name (); |
3557 | + } |
3558 | + |
3559 | + public bool is_initialized () |
3560 | + { |
3561 | + return this.initialized; |
3562 | + } |
3563 | + |
3564 | + public async void initialize () |
3565 | + { |
3566 | + initialized = true; |
3567 | + var f = File.new_for_uri (uri); |
3568 | + try |
3569 | + { |
3570 | + var fi = yield f.query_info_async (interesting_attributes, |
3571 | + 0, 0, null); |
3572 | + if (fi.get_file_type () == FileType.REGULAR && |
3573 | + !fi.get_is_hidden () && |
3574 | + !fi.get_is_backup ()) |
3575 | + { |
3576 | + match_obj = (UriMatch) Object.new (match_obj_type, |
3577 | + "thumbnail-path", fi.get_attribute_byte_string (FileAttribute.THUMBNAIL_PATH), |
3578 | + "icon-name", fi.get_icon ().to_string (), |
3579 | + "uri", uri, |
3580 | + "title", fi.get_display_name (), |
3581 | + "description", f.get_parse_name (), |
3582 | + "match-type", MatchType.GENERIC_URI, |
3583 | + null |
3584 | + ); |
3585 | + |
3586 | + // let's determine the file type |
3587 | + unowned string mime_type = |
3588 | + fi.get_attribute_string (FileAttribute.STANDARD_FAST_CONTENT_TYPE); |
3589 | + if (ContentType.is_unknown (mime_type)) |
3590 | + { |
3591 | + file_type = QueryFlags.UNCATEGORIZED; |
3592 | + } |
3593 | + else if (ContentType.is_a (mime_type, "audio/*")) |
3594 | + { |
3595 | + file_type = QueryFlags.AUDIO; |
3596 | + } |
3597 | + else if (ContentType.is_a (mime_type, "video/*")) |
3598 | + { |
3599 | + file_type = QueryFlags.VIDEO; |
3600 | + } |
3601 | + else if (ContentType.is_a (mime_type, "image/*")) |
3602 | + { |
3603 | + file_type = QueryFlags.IMAGES; |
3604 | + } |
3605 | + else if (ContentType.is_a (mime_type, "text/*")) |
3606 | + { |
3607 | + file_type = QueryFlags.DOCUMENTS; |
3608 | + } |
3609 | + // FIXME: this isn't right |
3610 | + else if (ContentType.is_a (mime_type, "application/*")) |
3611 | + { |
3612 | + file_type = QueryFlags.DOCUMENTS; |
3613 | + } |
3614 | + |
3615 | + match_obj.file_type = file_type; |
3616 | + match_obj.mime_type = mime_type; |
3617 | + } |
3618 | + } |
3619 | + catch (Error err) |
3620 | + { |
3621 | + warning ("%s", err.message); |
3622 | + } |
3623 | + } |
3624 | + |
3625 | + public async bool exists () |
3626 | + { |
3627 | + var f = File.new_for_uri (uri); |
3628 | + bool result = yield query_exists_async (f); |
3629 | + |
3630 | + return result; |
3631 | + } |
3632 | + } |
3633 | + } |
3634 | +} |
3635 | + |
3636 | |
3637 | === added file 'lib/synapse-core/volume-service.vala' |
3638 | --- lib/synapse-core/volume-service.vala 1970-01-01 00:00:00 +0000 |
3639 | +++ lib/synapse-core/volume-service.vala 2014-06-16 07:45:32 +0000 |
3640 | @@ -0,0 +1,193 @@ |
3641 | +/* |
3642 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
3643 | + * |
3644 | + * This library is free software; you can redistribute it and/or |
3645 | + * modify it under the terms of the GNU Lesser General Public |
3646 | + * License as published by the Free Software Foundation; either |
3647 | + * version 2 of the License, or (at your option) any later version. |
3648 | + * |
3649 | + * This library is distributed in the hope that it will be useful, |
3650 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3651 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
3652 | + * Lesser General Public License for more details. |
3653 | + * |
3654 | + * You should have received a copy of the GNU Lesser General Public License |
3655 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
3656 | + * |
3657 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
3658 | + * |
3659 | + */ |
3660 | + |
3661 | +namespace Synapse |
3662 | +{ |
3663 | + public class VolumeService : GLib.Object |
3664 | + { |
3665 | + // singleton that can be easily destroyed |
3666 | + private static unowned VolumeService? instance; |
3667 | + public static VolumeService get_default () |
3668 | + { |
3669 | + return instance ?? new VolumeService (); |
3670 | + } |
3671 | + |
3672 | + private VolumeService () |
3673 | + { |
3674 | + } |
3675 | + |
3676 | + ~VolumeService () |
3677 | + { |
3678 | + instance = null; |
3679 | + } |
3680 | + |
3681 | + private VolumeMonitor vm; |
3682 | + private Gee.Map<GLib.Volume, VolumeObject> volumes; |
3683 | + |
3684 | + construct |
3685 | + { |
3686 | + instance = this; |
3687 | + |
3688 | + volumes = new Gee.HashMap<GLib.Volume, VolumeObject> (); |
3689 | + initialize (); |
3690 | + } |
3691 | + |
3692 | + protected void initialize () |
3693 | + { |
3694 | + vm = VolumeMonitor.get (); |
3695 | + |
3696 | + vm.volume_added.connect ((volume) => |
3697 | + { |
3698 | + volumes[volume] = new VolumeObject (volume); |
3699 | + }); |
3700 | + vm.volume_removed.connect ((volume) => |
3701 | + { |
3702 | + volumes.unset (volume); |
3703 | + }); |
3704 | + vm.mount_added.connect ((mount) => |
3705 | + { |
3706 | + var volume = mount.get_volume (); |
3707 | + if (volume == null) return; |
3708 | + |
3709 | + if (volume in volumes.keys) volumes[volume].update_state (); |
3710 | + }); |
3711 | + // FIXME: connect also to other signals? |
3712 | + |
3713 | + var volume_list = vm.get_volumes (); |
3714 | + process_volume_list (volume_list); |
3715 | + } |
3716 | + |
3717 | + private void process_volume_list (GLib.List<GLib.Volume> volume_list) |
3718 | + { |
3719 | + foreach (unowned GLib.Volume volume in volume_list) |
3720 | + { |
3721 | + volumes[volume] = new VolumeObject (volume); |
3722 | + } |
3723 | + } |
3724 | + |
3725 | + public Gee.Collection<VolumeObject> get_volumes () |
3726 | + { |
3727 | + return volumes.values; |
3728 | + } |
3729 | + |
3730 | + public string? uri_to_volume_name (string uri, out string? volume_path) |
3731 | + { |
3732 | + volume_path = null; |
3733 | + var g_volumes = volumes.keys; |
3734 | + |
3735 | + var f = File.new_for_uri (uri); |
3736 | + // FIXME: cache this somehow |
3737 | + foreach (var volume in g_volumes) |
3738 | + { |
3739 | + File? root = volume.get_activation_root (); |
3740 | + if (root == null) |
3741 | + { |
3742 | + var mount = volume.get_mount (); |
3743 | + if (mount == null) continue; |
3744 | + root = mount.get_root (); |
3745 | + } |
3746 | + |
3747 | + if (f.has_prefix (root)) |
3748 | + { |
3749 | + volume_path = root.get_path (); |
3750 | + return volume.get_name (); |
3751 | + } |
3752 | + } |
3753 | + |
3754 | + return null; |
3755 | + } |
3756 | + |
3757 | + public class VolumeObject: Object, Match, UriMatch |
3758 | + { |
3759 | + public string title { get; construct set; } |
3760 | + public string description { get; set; } |
3761 | + public string icon_name { get; construct set; } |
3762 | + public bool has_thumbnail { get; construct set; } |
3763 | + public string thumbnail_path { get; construct set; } |
3764 | + public MatchType match_type { get; construct set; } |
3765 | + |
3766 | + // UriMatch |
3767 | + public string uri { get; set; } |
3768 | + public QueryFlags file_type { get; set; } |
3769 | + public string mime_type { get; set; } |
3770 | + |
3771 | + private ulong changed_signal_id; |
3772 | + |
3773 | + private GLib.Volume _volume; |
3774 | + public GLib.Volume volume |
3775 | + { |
3776 | + get { return _volume; } |
3777 | + set |
3778 | + { |
3779 | + _volume = value; |
3780 | + title = value.get_name (); |
3781 | + description = ""; // FIXME |
3782 | + icon_name = value.get_icon ().to_string (); |
3783 | + has_thumbnail = false; |
3784 | + match_type = value.get_mount () != null ? |
3785 | + MatchType.GENERIC_URI : MatchType.ACTION; |
3786 | + |
3787 | + if (match_type == MatchType.GENERIC_URI) |
3788 | + { |
3789 | + uri = value.get_mount ().get_root ().get_uri (); |
3790 | + file_type = QueryFlags.PLACES; |
3791 | + mime_type = ""; // FIXME: do we need this? |
3792 | + } |
3793 | + else |
3794 | + { |
3795 | + uri = null; |
3796 | + } |
3797 | + |
3798 | + if (changed_signal_id == 0) |
3799 | + { |
3800 | + changed_signal_id = _volume.changed.connect (this.update_state); |
3801 | + } |
3802 | + |
3803 | + Utils.Logger.debug (this, "vo[%p]: %s [%s], has_mount: %d, uri: %s", this, title, icon_name, (value.get_mount () != null ? 1 : 0), uri); |
3804 | + } |
3805 | + } |
3806 | + |
3807 | + public void update_state () |
3808 | + { |
3809 | + this.volume = _volume; // call setter again |
3810 | + } |
3811 | + |
3812 | + public bool is_mounted () |
3813 | + { |
3814 | + return _volume.get_mount () != null; |
3815 | + } |
3816 | + |
3817 | + public VolumeObject (GLib.Volume volume) |
3818 | + { |
3819 | + Object (volume: volume); |
3820 | + } |
3821 | + |
3822 | + ~VolumeObject () |
3823 | + { |
3824 | + if (changed_signal_id != 0) |
3825 | + { |
3826 | + SignalHandler.disconnect (_volume, changed_signal_id); |
3827 | + changed_signal_id = 0; |
3828 | + } |
3829 | + } |
3830 | + } |
3831 | + } |
3832 | +} |
3833 | + |
3834 | |
3835 | === added directory 'lib/synapse-plugins' |
3836 | === added file 'lib/synapse-plugins/CMakeLists.txt' |
3837 | --- lib/synapse-plugins/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
3838 | +++ lib/synapse-plugins/CMakeLists.txt 2014-06-16 07:45:32 +0000 |
3839 | @@ -0,0 +1,46 @@ |
3840 | +set(PLUGINS_LIB_VERSION 0.1) |
3841 | +set(PLUGINS_LIB_SOVERSION 0) |
3842 | +set(PLUGINS_LIBRARY_NAME synapse-plugins) |
3843 | +set(PLUGINS_PKG |
3844 | + glib-2.0 |
3845 | + gio-unix-2.0 |
3846 | + gee-0.8 |
3847 | + gtk+-3.0 |
3848 | +) |
3849 | + |
3850 | +pkg_check_modules(PLUGINS_DEPS REQUIRED ${PLUGINS_PKG}) |
3851 | + |
3852 | +set(PLUGINS_SOURCE |
3853 | + command-plugin.vala |
3854 | + desktop-file-plugin.vala |
3855 | +) |
3856 | + |
3857 | +set(LINK_MODE STATIC) |
3858 | + |
3859 | +vala_precompile(PLUGINS_VALA_C ${PLUGINS_LIBRARY_NAME} |
3860 | + ${PLUGINS_SOURCE} |
3861 | +PACKAGES |
3862 | + ${PLUGINS_PKG} |
3863 | + synapse-core |
3864 | +OPTIONS |
3865 | + --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core |
3866 | +GENERATE_VAPI |
3867 | + ${PLUGINS_LIBRARY_NAME} |
3868 | +GENERATE_HEADER |
3869 | + ${PLUGINS_LIBRARY_NAME} |
3870 | +) |
3871 | + |
3872 | +add_definitions(${PLUGINS_DEPS_CFLAGS} -include config.h -w) |
3873 | +link_directories(${PLUGINS_DEPS_LIBRARY_DIRS}) |
3874 | +include_directories(${CMAKE_BINARY_DIR}/lib/synapse-core) |
3875 | + |
3876 | +add_library(${PLUGINS_LIBRARY_NAME} STATIC ${PLUGINS_VALA_C}) |
3877 | + |
3878 | +set_target_properties(${PLUGINS_LIBRARY_NAME} PROPERTIES |
3879 | + OUTPUT_NAME ${PLUGINS_LIBRARY_NAME} |
3880 | + VERSION ${PLUGINS_LIB_VERSION} |
3881 | + SOVERSION ${PLUGINS_LIB_SOVERSION} |
3882 | +) |
3883 | + |
3884 | +target_link_libraries (${PLUGINS_LIBRARY_NAME} ${PLUGINS_DEPS_LIBRARIES} synapse-core) |
3885 | + |
3886 | |
3887 | === added file 'lib/synapse-plugins/command-plugin.vala' |
3888 | --- lib/synapse-plugins/command-plugin.vala 1970-01-01 00:00:00 +0000 |
3889 | +++ lib/synapse-plugins/command-plugin.vala 2014-06-16 07:45:32 +0000 |
3890 | @@ -0,0 +1,188 @@ |
3891 | +/* |
3892 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
3893 | + * |
3894 | + * This program is free software; you can redistribute it and/or modify |
3895 | + * it under the terms of the GNU General Public License as published by |
3896 | + * the Free Software Foundation; either version 2 of the License, or |
3897 | + * (at your option) any later version. |
3898 | + * |
3899 | + * This program is distributed in the hope that it will be useful, |
3900 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3901 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3902 | + * GNU General Public License for more details. |
3903 | + * |
3904 | + * You should have received a copy of the GNU General Public License |
3905 | + * along with this program; if not, write to the Free Software |
3906 | + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
3907 | + * |
3908 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
3909 | + * |
3910 | + */ |
3911 | + |
3912 | +namespace Synapse |
3913 | +{ |
3914 | + public class CommandPlugin: Object, Activatable, ItemProvider |
3915 | + { |
3916 | + public bool enabled { get; set; default = true; } |
3917 | + |
3918 | + public void activate () |
3919 | + { |
3920 | + |
3921 | + } |
3922 | + |
3923 | + public void deactivate () |
3924 | + { |
3925 | + |
3926 | + } |
3927 | + |
3928 | + private class CommandObject: Object, Match, ApplicationMatch |
3929 | + { |
3930 | + // for Match interface |
3931 | + public string title { get; construct set; } |
3932 | + public string description { get; set; default = ""; } |
3933 | + public string icon_name { get; construct set; default = ""; } |
3934 | + public bool has_thumbnail { get; construct set; default = false; } |
3935 | + public string thumbnail_path { get; construct set; } |
3936 | + public MatchType match_type { get; construct set; } |
3937 | + |
3938 | + // for ApplicationMatch |
3939 | + public AppInfo? app_info { get; set; default = null; } |
3940 | + public bool needs_terminal { get; set; default = false; } |
3941 | + public string? filename { get; construct set; default = null; } |
3942 | + public string command { get; construct set; } |
3943 | + |
3944 | + public CommandObject (string cmd) |
3945 | + { |
3946 | + Object (title: _("Execute '%s'").printf (cmd), description: _ ("Run command"), command: cmd, |
3947 | + icon_name: "application-x-executable", |
3948 | + match_type: MatchType.APPLICATION, |
3949 | + needs_terminal: cmd.has_prefix ("sudo ")); |
3950 | + |
3951 | + try |
3952 | + { |
3953 | + app_info = AppInfo.create_from_commandline (cmd, null, 0); |
3954 | + } |
3955 | + catch (Error err) |
3956 | + { |
3957 | + warning ("%s", err.message); |
3958 | + } |
3959 | + } |
3960 | + } |
3961 | + |
3962 | + static void register_plugin () |
3963 | + { |
3964 | + DataSink.PluginRegistry.get_default ().register_plugin ( |
3965 | + typeof (CommandPlugin), |
3966 | + "Command Search", |
3967 | + _ ("Find and execute arbitrary commands."), |
3968 | + "system-run", |
3969 | + register_plugin |
3970 | + ); |
3971 | + } |
3972 | + |
3973 | + static construct |
3974 | + { |
3975 | + register_plugin (); |
3976 | + } |
3977 | + |
3978 | + private Gee.Set<string> past_commands; |
3979 | + private Regex split_regex; |
3980 | + |
3981 | + construct |
3982 | + { |
3983 | + // TODO: load from configuration |
3984 | + past_commands = new Gee.HashSet<string> (); |
3985 | + try |
3986 | + { |
3987 | + split_regex = new Regex ("\\s+", RegexCompileFlags.OPTIMIZE); |
3988 | + } |
3989 | + catch (RegexError err) |
3990 | + { |
3991 | + critical ("%s", err.message); |
3992 | + } |
3993 | + } |
3994 | + |
3995 | + private CommandObject? create_co (string exec) |
3996 | + { |
3997 | + // ignore results that will be returned by DesktopFilePlugin |
3998 | + // and at the same time look for hidden and no-display desktop files, |
3999 | + // so we can display their info (title, comment, icon) |
4000 | + var dfs = DesktopFileService.get_default (); |
4001 | + var df_list = dfs.get_desktop_files_for_exec (exec); |
4002 | + DesktopFileInfo? dfi = null; |
4003 | + foreach (var df in df_list) |
4004 | + { |
4005 | + if (!df.is_hidden) return null; // will be handled by App plugin |
4006 | + dfi = df; |
4007 | + } |
4008 | + |
4009 | + var co = new CommandObject (exec); |
4010 | + if (dfi != null) |
4011 | + { |
4012 | + co.title = dfi.name; |
4013 | + if (dfi.comment != "") co.description = dfi.comment; |
4014 | + if (dfi.icon_name != null && dfi.icon_name != "") co.icon_name = dfi.icon_name; |
4015 | + } |
4016 | + |
4017 | + return co; |
4018 | + } |
4019 | + |
4020 | + private void command_executed (Match match) |
4021 | + { |
4022 | + CommandObject? co = match as CommandObject; |
4023 | + if (co == null) return; |
4024 | + |
4025 | + past_commands.add (co.command); |
4026 | + } |
4027 | + |
4028 | + public async ResultSet? search (Query q) throws SearchError |
4029 | + { |
4030 | + // we only search for applications |
4031 | + if (!(QueryFlags.APPLICATIONS in q.query_type)) return null; |
4032 | + |
4033 | + Idle.add (search.callback); |
4034 | + yield; |
4035 | + |
4036 | + var result = new ResultSet (); |
4037 | + |
4038 | + string stripped = q.query_string.strip (); |
4039 | + if (stripped == "") return null; |
4040 | + if (stripped.has_prefix ("~/")) |
4041 | + { |
4042 | + stripped = stripped.replace ("~", Environment.get_home_dir ()); |
4043 | + } |
4044 | + |
4045 | + if (!(stripped in past_commands)) |
4046 | + { |
4047 | + foreach (var command in past_commands) |
4048 | + { |
4049 | + if (command.has_prefix (stripped)) |
4050 | + { |
4051 | + result.add (create_co (command), Match.Score.AVERAGE); |
4052 | + } |
4053 | + } |
4054 | + |
4055 | + string[] args = split_regex.split (stripped); |
4056 | + string? valid_cmd = Environment.find_program_in_path (args[0]); |
4057 | + |
4058 | + if (valid_cmd != null) |
4059 | + { |
4060 | + // don't allow dangerous commands |
4061 | + if (args[0] == "rm") return null; |
4062 | + CommandObject? co = create_co (stripped); |
4063 | + if (co == null) return null; |
4064 | + result.add (co, Match.Score.POOR); |
4065 | + co.executed.connect (this.command_executed); |
4066 | + } |
4067 | + } |
4068 | + else |
4069 | + { |
4070 | + result.add (create_co (stripped), Match.Score.VERY_GOOD); |
4071 | + } |
4072 | + |
4073 | + q.check_cancellable (); |
4074 | + |
4075 | + return result; |
4076 | + } |
4077 | + } |
4078 | +} |
4079 | |
4080 | === added file 'lib/synapse-plugins/desktop-file-plugin.vala' |
4081 | --- lib/synapse-plugins/desktop-file-plugin.vala 1970-01-01 00:00:00 +0000 |
4082 | +++ lib/synapse-plugins/desktop-file-plugin.vala 2014-06-16 07:45:32 +0000 |
4083 | @@ -0,0 +1,350 @@ |
4084 | +/* |
4085 | + * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> |
4086 | + * |
4087 | + * This program is free software; you can redistribute it and/or modify |
4088 | + * it under the terms of the GNU General Public License as published by |
4089 | + * the Free Software Foundation; either version 2 of the License, or |
4090 | + * (at your option) any later version. |
4091 | + * |
4092 | + * This program is distributed in the hope that it will be useful, |
4093 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
4094 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4095 | + * GNU General Public License for more details. |
4096 | + * |
4097 | + * You should have received a copy of the GNU General Public License |
4098 | + * along with this program; if not, write to the Free Software |
4099 | + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
4100 | + * |
4101 | + * Authored by Michal Hruby <michal.mhr@gmail.com> |
4102 | + * |
4103 | + */ |
4104 | + |
4105 | +namespace Synapse |
4106 | +{ |
4107 | + public class DesktopFilePlugin: Object, Activatable, ItemProvider, ActionProvider |
4108 | + { |
4109 | + public bool enabled { get; set; default = true; } |
4110 | + |
4111 | + public void activate () |
4112 | + { |
4113 | + |
4114 | + } |
4115 | + |
4116 | + public void deactivate () |
4117 | + { |
4118 | + |
4119 | + } |
4120 | + |
4121 | + private class DesktopFileMatch: Object, Match, ApplicationMatch |
4122 | + { |
4123 | + // for Match interface |
4124 | + public string title { get; construct set; } |
4125 | + public string description { get; set; default = ""; } |
4126 | + public string icon_name { get; construct set; default = ""; } |
4127 | + public bool has_thumbnail { get; construct set; default = false; } |
4128 | + public string thumbnail_path { get; construct set; } |
4129 | + public MatchType match_type { get; construct set; } |
4130 | + |
4131 | + // for ApplicationMatch |
4132 | + public AppInfo? app_info { get; set; default = null; } |
4133 | + public bool needs_terminal { get; set; default = false; } |
4134 | + public string? filename { get; construct set; } |
4135 | + |
4136 | + private string? title_folded = null; |
4137 | + public unowned string get_title_folded () |
4138 | + { |
4139 | + if (title_folded == null) title_folded = title.casefold (); |
4140 | + return title_folded; |
4141 | + } |
4142 | + |
4143 | + public string? title_unaccented { get; set; default = null; } |
4144 | + public string? desktop_id { get; set; default = null; } |
4145 | + |
4146 | + public string exec { get; set; } |
4147 | + |
4148 | + public DesktopFileMatch.for_info (DesktopFileInfo info) |
4149 | + { |
4150 | + Object (filename: info.filename, match_type: MatchType.APPLICATION); |
4151 | + |
4152 | + init_from_info (info); |
4153 | + } |
4154 | + |
4155 | + private void init_from_info (DesktopFileInfo info) |
4156 | + { |
4157 | + this.title = info.name; |
4158 | + this.description = info.comment; |
4159 | + this.icon_name = info.icon_name; |
4160 | + this.exec = info.exec; |
4161 | + this.needs_terminal = info.needs_terminal; |
4162 | + this.title_folded = info.get_name_folded (); |
4163 | + this.title_unaccented = Utils.remove_accents (this.title_folded); |
4164 | + this.desktop_id = "application://" + info.desktop_id; |
4165 | + } |
4166 | + } |
4167 | + |
4168 | + static void register_plugin () |
4169 | + { |
4170 | + DataSink.PluginRegistry.get_default ().register_plugin ( |
4171 | + typeof (DesktopFilePlugin), |
4172 | + "Application Search", |
4173 | + _ ("Search for and run applications on your computer."), |
4174 | + "system-run", |
4175 | + register_plugin |
4176 | + ); |
4177 | + } |
4178 | + |
4179 | + static construct |
4180 | + { |
4181 | + register_plugin (); |
4182 | + } |
4183 | + |
4184 | + private Gee.List<DesktopFileMatch> desktop_files; |
4185 | + |
4186 | + construct |
4187 | + { |
4188 | + desktop_files = new Gee.ArrayList<DesktopFileMatch> (); |
4189 | + mimetype_map = new Gee.HashMap<string, OpenWithAction> (); |
4190 | + |
4191 | + var dfs = DesktopFileService.get_default (); |
4192 | + dfs.reload_started.connect (() => { |
4193 | + loading_in_progress = true; |
4194 | + }); |
4195 | + dfs.reload_done.connect (() => { |
4196 | + mimetype_map.clear (); |
4197 | + desktop_files.clear (); |
4198 | + load_all_desktop_files (); |
4199 | + }); |
4200 | + |
4201 | + load_all_desktop_files (); |
4202 | + } |
4203 | + |
4204 | + public signal void load_complete (); |
4205 | + private bool loading_in_progress = false; |
4206 | + |
4207 | + private async void load_all_desktop_files () |
4208 | + { |
4209 | + loading_in_progress = true; |
4210 | + Idle.add_full (Priority.LOW, load_all_desktop_files.callback); |
4211 | + yield; |
4212 | + |
4213 | + var dfs = DesktopFileService.get_default (); |
4214 | + |
4215 | + foreach (DesktopFileInfo dfi in dfs.get_desktop_files ()) |
4216 | + { |
4217 | + desktop_files.add (new DesktopFileMatch.for_info (dfi)); |
4218 | + } |
4219 | + |
4220 | + loading_in_progress = false; |
4221 | + load_complete (); |
4222 | + } |
4223 | + |
4224 | + private int compute_relevancy (DesktopFileMatch dfm, int base_relevancy) |
4225 | + { |
4226 | + var rs = RelevancyService.get_default (); |
4227 | + float popularity = rs.get_application_popularity (dfm.desktop_id); |
4228 | + |
4229 | + int r = RelevancyService.compute_relevancy (base_relevancy, popularity); |
4230 | + Utils.Logger.debug (this, "relevancy for %s: %d", dfm.desktop_id, r); |
4231 | + |
4232 | + return r; |
4233 | + } |
4234 | + |
4235 | + private void full_search (Query q, ResultSet results, |
4236 | + MatcherFlags flags = 0) |
4237 | + { |
4238 | + // try to match against global matchers and if those fail, try also exec |
4239 | + var matchers = Query.get_matchers_for_query (q.query_string_folded, |
4240 | + flags); |
4241 | + |
4242 | + foreach (var dfm in desktop_files) |
4243 | + { |
4244 | + unowned string folded_title = dfm.get_title_folded (); |
4245 | + unowned string unaccented_title = dfm.title_unaccented; |
4246 | + bool matched = false; |
4247 | + // FIXME: we need to do much smarter relevancy computation in fuzzy re |
4248 | + // "sysmon" matching "System Monitor" is very good as opposed to |
4249 | + // "seto" matching "System Monitor" |
4250 | + foreach (var matcher in matchers) |
4251 | + { |
4252 | + if (matcher.key.match (folded_title)) |
4253 | + { |
4254 | + results.add (dfm, compute_relevancy (dfm, matcher.value)); |
4255 | + matched = true; |
4256 | + break; |
4257 | + } |
4258 | + else if (unaccented_title != null && matcher.key.match (unaccented_title)) |
4259 | + { |
4260 | + results.add (dfm, compute_relevancy (dfm, matcher.value - Match.Score.INCREMENT_SMALL)); |
4261 | + matched = true; |
4262 | + break; |
4263 | + } |
4264 | + } |
4265 | + if (!matched && dfm.exec.has_prefix (q.query_string)) |
4266 | + { |
4267 | + results.add (dfm, compute_relevancy (dfm, dfm.exec == q.query_string ? |
4268 | + Match.Score.VERY_GOOD : Match.Score.AVERAGE - Match.Score.INCREMENT_SMALL)); |
4269 | + } |
4270 | + } |
4271 | + } |
4272 | + |
4273 | + public bool handles_query (Query q) |
4274 | + { |
4275 | + // we only search for applications |
4276 | + if (!(QueryFlags.APPLICATIONS in q.query_type)) return false; |
4277 | + if (q.query_string.strip () == "") return false; |
4278 | + |
4279 | + return true; |
4280 | + } |
4281 | + |
4282 | + public async ResultSet? search (Query q) throws SearchError |
4283 | + { |
4284 | + if (loading_in_progress) |
4285 | + { |
4286 | + // wait |
4287 | + ulong signal_id = this.load_complete.connect (() => |
4288 | + { |
4289 | + search.callback (); |
4290 | + }); |
4291 | + yield; |
4292 | + SignalHandler.disconnect (this, signal_id); |
4293 | + } |
4294 | + else |
4295 | + { |
4296 | + // we'll do this so other plugins can send their DBus requests etc. |
4297 | + // and they don't have to wait for our blocking (though fast) search |
4298 | + // to finish |
4299 | + Idle.add_full (Priority.HIGH_IDLE, search.callback); |
4300 | + yield; |
4301 | + } |
4302 | + |
4303 | + q.check_cancellable (); |
4304 | + |
4305 | + // FIXME: spawn new thread and do the search there? |
4306 | + var result = new ResultSet (); |
4307 | + |
4308 | + // FIXME: make sure this is one unichar, not just byte |
4309 | + if (q.query_string.length == 1) |
4310 | + { |
4311 | + var flags = MatcherFlags.NO_SUBSTRING | MatcherFlags.NO_PARTIAL | |
4312 | + MatcherFlags.NO_FUZZY; |
4313 | + full_search (q, result, flags); |
4314 | + } |
4315 | + else |
4316 | + { |
4317 | + full_search (q, result); |
4318 | + } |
4319 | + |
4320 | + q.check_cancellable (); |
4321 | + |
4322 | + return result; |
4323 | + } |
4324 | + |
4325 | + private class OpenWithAction: Object, Match |
4326 | + { |
4327 | + // for Match interface |
4328 | + public string title { get; construct set; } |
4329 | + public string description { get; set; default = ""; } |
4330 | + public string icon_name { get; construct set; default = ""; } |
4331 | + public bool has_thumbnail { get; construct set; default = false; } |
4332 | + public string thumbnail_path { get; construct set; } |
4333 | + public MatchType match_type { get; construct set; } |
4334 | + |
4335 | + public DesktopFileInfo desktop_info { get; private set; } |
4336 | + |
4337 | + public OpenWithAction (DesktopFileInfo info) |
4338 | + { |
4339 | + Object (); |
4340 | + |
4341 | + init_with_info (info); |
4342 | + } |
4343 | + |
4344 | + private void init_with_info (DesktopFileInfo info) |
4345 | + { |
4346 | + this.title = _ ("Open with %s").printf (info.name); |
4347 | + this.icon_name = info.icon_name; |
4348 | + this.description = _ ("Opens current selection using %s").printf (info.name); |
4349 | + this.desktop_info = info; |
4350 | + } |
4351 | + |
4352 | + protected void execute (Match? match) |
4353 | + { |
4354 | + UriMatch uri_match = match as UriMatch; |
4355 | + return_if_fail (uri_match != null); |
4356 | + |
4357 | + var f = File.new_for_uri (uri_match.uri); |
4358 | + try |
4359 | + { |
4360 | + var app_info = new DesktopAppInfo.from_filename (desktop_info.filename); |
4361 | + List<File> files = new List<File> (); |
4362 | + files.prepend (f); |
4363 | + app_info.launch (files, new Gdk.AppLaunchContext ()); |
4364 | + } |
4365 | + catch (Error err) |
4366 | + { |
4367 | + warning ("%s", err.message); |
4368 | + } |
4369 | + } |
4370 | + } |
4371 | + |
4372 | + private Gee.Map<string, Gee.List<OpenWithAction> > mimetype_map; |
4373 | + |
4374 | + public ResultSet? find_for_match (ref Query query, Match match) |
4375 | + { |
4376 | + if (match.match_type != MatchType.GENERIC_URI) return null; |
4377 | + |
4378 | + var uri_match = match as UriMatch; |
4379 | + return_val_if_fail (uri_match != null, null); |
4380 | + |
4381 | + if (uri_match.mime_type == null) return null; |
4382 | + |
4383 | + Gee.List<OpenWithAction> ow_list = mimetype_map[uri_match.mime_type]; |
4384 | + /* Query DesktopFileService only if is necessary */ |
4385 | + if (ow_list == null) |
4386 | + { |
4387 | + /* Initialize ow_list */ |
4388 | + ow_list = new Gee.LinkedList<OpenWithAction> (); |
4389 | + mimetype_map[uri_match.mime_type] = ow_list; |
4390 | + var dfs = DesktopFileService.get_default (); |
4391 | + var list_for_mimetype = dfs.get_desktop_files_for_type (uri_match.mime_type); |
4392 | + /* If there's more than one application, fill the ow list */ |
4393 | + if (list_for_mimetype.size > 1) |
4394 | + { |
4395 | + foreach (var entry in list_for_mimetype) |
4396 | + { |
4397 | + ow_list.add (new OpenWithAction (entry)); |
4398 | + } |
4399 | + } |
4400 | + else return null; |
4401 | + } |
4402 | + else if (ow_list.size == 0) return null; |
4403 | + |
4404 | + var rs = new ResultSet (); |
4405 | + |
4406 | + if (query.query_string == "") |
4407 | + { |
4408 | + foreach (var action in ow_list) |
4409 | + { |
4410 | + rs.add (action, Match.Score.POOR); |
4411 | + } |
4412 | + } |
4413 | + else |
4414 | + { |
4415 | + var matchers = Query.get_matchers_for_query (query.query_string, 0, |
4416 | + RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); |
4417 | + foreach (var action in ow_list) |
4418 | + { |
4419 | + foreach (var matcher in matchers) |
4420 | + { |
4421 | + if (matcher.key.match (action.title)) |
4422 | + { |
4423 | + rs.add (action, matcher.value); |
4424 | + break; |
4425 | + } |
4426 | + } |
4427 | + } |
4428 | + } |
4429 | + |
4430 | + return rs; |
4431 | + } |
4432 | + } |
4433 | +} |
4434 | |
4435 | === modified file 'src/Backend/App.vala' |
4436 | --- src/Backend/App.vala 2014-04-24 10:43:44 +0000 |
4437 | +++ src/Backend/App.vala 2014-06-16 07:45:32 +0000 |
4438 | @@ -16,8 +16,20 @@ |
4439 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
4440 | // |
4441 | |
4442 | +errordomain IconError { |
4443 | + NOT_FOUND |
4444 | +} |
4445 | + |
4446 | public class Slingshot.Backend.App : Object { |
4447 | |
4448 | + public enum AppType { |
4449 | + APP, |
4450 | + COMMAND, |
4451 | + SYNAPSE |
4452 | + } |
4453 | + |
4454 | + public signal void start_search (Synapse.SearchMatch search_match, Synapse.Match? target); |
4455 | + |
4456 | public string name { get; construct set; } |
4457 | public string description { get; private set; default = ""; } |
4458 | public string desktop_id { get; construct set; } |
4459 | @@ -30,13 +42,17 @@ |
4460 | public string desktop_path { get; private set; } |
4461 | public string categories { get; private set; } |
4462 | public string generic_name { get; private set; default = ""; } |
4463 | + public AppType app_type { get; private set; default = AppType.APP; } |
4464 | |
4465 | - private bool is_command = false; |
4466 | + public Synapse.Match? match { get; private set; default = null; } |
4467 | + public Synapse.Match? target { get; private set; default = null; } |
4468 | |
4469 | public signal void icon_changed (); |
4470 | public signal void launched (App app); |
4471 | |
4472 | public App (GMenu.TreeEntry entry) { |
4473 | + app_type = AppType.APP; |
4474 | + |
4475 | unowned GLib.DesktopAppInfo info = entry.get_app_info (); |
4476 | name = info.get_display_name ().dup (); |
4477 | description = info.get_description ().dup () ?? name; |
4478 | @@ -70,6 +86,7 @@ |
4479 | } |
4480 | |
4481 | public App.from_command (string command) { |
4482 | + app_type = AppType.COMMAND; |
4483 | |
4484 | name = command; |
4485 | description = _("Run this command..."); |
4486 | @@ -77,7 +94,20 @@ |
4487 | desktop_id = command; |
4488 | icon_name = "system-run"; |
4489 | |
4490 | - is_command = true; |
4491 | + update_icon (); |
4492 | + |
4493 | + } |
4494 | + |
4495 | + public App.from_synapse_match (Synapse.Match match, Synapse.Match? target = null) { |
4496 | + |
4497 | + app_type = AppType.SYNAPSE; |
4498 | + |
4499 | + name = match.title; |
4500 | + description = match.description; |
4501 | + icon_name = match.icon_name; |
4502 | + |
4503 | + this.match = match; |
4504 | + this.target = target; |
4505 | |
4506 | update_icon (); |
4507 | |
4508 | @@ -98,7 +128,31 @@ |
4509 | } |
4510 | } |
4511 | |
4512 | - public Gdk.Pixbuf load_icon (int size) { |
4513 | + public Gdk.Pixbuf? load_icon (int size) { |
4514 | + if (app_type == AppType.SYNAPSE) { |
4515 | + try { |
4516 | + // for contacts we can load the thumbnail because we expect it to be |
4517 | + // the avatar. For other types it'd be ridiculously small. |
4518 | + if (match.match_type == Synapse.MatchType.CONTACT && match.has_thumbnail) { |
4519 | + return new Gdk.Pixbuf.from_file_at_scale (match.thumbnail_path, size, size, true); |
4520 | + } |
4521 | + |
4522 | + var icon = Icon.new_for_string (icon_name); |
4523 | + var info = Gtk.IconTheme.get_default ().lookup_by_gicon (icon, |
4524 | + size, Gtk.IconLookupFlags.FORCE_SIZE); |
4525 | + |
4526 | + if (info == null) |
4527 | + throw new IconError.NOT_FOUND ("Not found"); |
4528 | + |
4529 | + return info.load_icon (); |
4530 | + } catch (Error e) { |
4531 | + warning ("Failed to load icon: %s\n", e.message); |
4532 | + } |
4533 | + |
4534 | + return Slingshot.icon_theme.load_icon ("application-default-icon", |
4535 | + size, Gtk.IconLookupFlags.FORCE_SIZE); |
4536 | + } |
4537 | + |
4538 | Gdk.Pixbuf icon = null; |
4539 | var flags = Gtk.IconLookupFlags.FORCE_SIZE; |
4540 | |
4541 | @@ -156,19 +210,35 @@ |
4542 | return icon; |
4543 | } |
4544 | |
4545 | - public void launch () { |
4546 | + public bool launch () { |
4547 | try { |
4548 | - if (is_command) { |
4549 | - debug (@"Launching command: $name"); |
4550 | - Process.spawn_command_line_async (exec); |
4551 | - } else { |
4552 | - launched (this); // Emit launched signal |
4553 | - new DesktopAppInfo (desktop_id).launch (null, null); |
4554 | - debug (@"Launching application: $name"); |
4555 | + switch (app_type) { |
4556 | + case AppType.COMMAND: |
4557 | + debug (@"Launching command: $name"); |
4558 | + Process.spawn_command_line_async (exec); |
4559 | + break; |
4560 | + case AppType.APP: |
4561 | + launched (this); // Emit launched signal |
4562 | + new DesktopAppInfo (desktop_id).launch (null, null); |
4563 | + debug (@"Launching application: $name"); |
4564 | + break; |
4565 | + case AppType.SYNAPSE: |
4566 | + if (match.match_type == Synapse.MatchType.SEARCH) { |
4567 | + start_search (match as Synapse.SearchMatch, target); |
4568 | + return false; |
4569 | + } else { |
4570 | + if (target == null) |
4571 | + Backend.SynapseSearch.find_actions_for_match (match).get (0).execute_with_target (match); |
4572 | + else |
4573 | + match.execute_with_target (target); |
4574 | + } |
4575 | + break; |
4576 | } |
4577 | } catch (Error e) { |
4578 | warning ("Failed to launch %s: %s", name, exec); |
4579 | } |
4580 | + |
4581 | + return true; |
4582 | } |
4583 | |
4584 | } |
4585 | |
4586 | === modified file 'src/Backend/AppSystem.vala' |
4587 | --- src/Backend/AppSystem.vala 2014-04-24 21:11:21 +0000 |
4588 | +++ src/Backend/AppSystem.vala 2014-06-16 07:45:32 +0000 |
4589 | @@ -161,7 +161,9 @@ |
4590 | foreach (Gee.ArrayList<App> category in apps.values) { |
4591 | foreach (App app in category) { |
4592 | |
4593 | - if (GCC_PANEL_CATEGORY in app.categories || SWITCHBOARD_PLUG_CATEGORY in app.categories) |
4594 | + if (app.categories != null |
4595 | + && (GCC_PANEL_CATEGORY in app.categories |
4596 | + || SWITCHBOARD_PLUG_CATEGORY in app.categories)) |
4597 | continue; |
4598 | |
4599 | |
4600 | @@ -196,19 +198,19 @@ |
4601 | foreach (App app in category) { |
4602 | if (!(app.exec in sorted_apps_execs)) { |
4603 | sorted_apps_execs += app.exec; |
4604 | - if (search in app.name.down ()) { |
4605 | + if (app.name != null && search in app.name.down ()) { |
4606 | if (search == app.name.down ()[0:search.length]) |
4607 | app.relevancy = 0.5 - app.popularity; // It must be minor than 1.0 |
4608 | else |
4609 | app.relevancy = app.name.length / search.length - app.popularity; |
4610 | filtered.add (app); |
4611 | - } else if (search in app.exec.down ()) { |
4612 | + } else if (app.exec != null && search in app.exec.down ()) { |
4613 | app.relevancy = app.exec.length / search.length * 10.0 - app.popularity; |
4614 | filtered.add (app); |
4615 | - } else if (search in app.description.down ()) { |
4616 | + } else if (app.description != null && search in app.description.down ()) { |
4617 | app.relevancy = app.description.length / search.length - app.popularity; |
4618 | filtered.add (app); |
4619 | - } else if (search in app.generic_name.down ()) { |
4620 | + } else if (app.generic_name != null && search in app.generic_name.down ()) { |
4621 | app.relevancy = app.generic_name.length / search.length - app.popularity; |
4622 | filtered.add (app); |
4623 | } else if (app.keywords != null) { |
4624 | @@ -236,4 +238,4 @@ |
4625 | |
4626 | } |
4627 | |
4628 | -} |
4629 | \ No newline at end of file |
4630 | +} |
4631 | |
4632 | === added file 'src/Backend/SynapseSearch.vala' |
4633 | --- src/Backend/SynapseSearch.vala 1970-01-01 00:00:00 +0000 |
4634 | +++ src/Backend/SynapseSearch.vala 2014-06-16 07:45:32 +0000 |
4635 | @@ -0,0 +1,163 @@ |
4636 | +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- |
4637 | +// |
4638 | +// Copyright (C) 2011-2012 Giulio Collura |
4639 | +// |
4640 | +// This program is free software: you can redistribute it and/or modify |
4641 | +// it under the terms of the GNU General Public License as published by |
4642 | +// the Free Software Foundation, either version 3 of the License, or |
4643 | +// (at your option) any later version. |
4644 | +// |
4645 | +// This program is distributed in the hope that it will be useful, |
4646 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
4647 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4648 | +// GNU General Public License for more details. |
4649 | +// |
4650 | +// You should have received a copy of the GNU General Public License |
4651 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
4652 | +// |
4653 | + |
4654 | +namespace Slingshot.Backend { |
4655 | + |
4656 | + public class SynapseSearch : Object { |
4657 | + |
4658 | + private static Type[] plugins = { |
4659 | + typeof (Synapse.CommandPlugin), |
4660 | + typeof (Synapse.DesktopFilePlugin) |
4661 | + }; |
4662 | + |
4663 | + private static Synapse.DataSink? sink = null; |
4664 | + private static Gee.HashMap<string,Gdk.Pixbuf> favicon_cache; |
4665 | + |
4666 | + Cancellable? current_search = null; |
4667 | + |
4668 | + public SynapseSearch () { |
4669 | + |
4670 | + if (sink == null) { |
4671 | + sink = new Synapse.DataSink (); |
4672 | + foreach (var plugin in plugins) { |
4673 | + sink.register_static_plugin (plugin); |
4674 | + } |
4675 | + |
4676 | + favicon_cache = new Gee.HashMap<string,Gdk.Pixbuf> (); |
4677 | + } |
4678 | + } |
4679 | + |
4680 | + public async Gee.List<Synapse.Match>? search (string text, Synapse.SearchProvider? provider = null) { |
4681 | + |
4682 | + if (current_search != null) |
4683 | + current_search.cancel (); |
4684 | + |
4685 | + if (provider == null) |
4686 | + provider = sink; |
4687 | + |
4688 | + var results = new Synapse.ResultSet (); |
4689 | + |
4690 | + try { |
4691 | + return yield provider.search (text, Synapse.QueryFlags.ALL, results, current_search); |
4692 | + } catch (Error e) { warning (e.message); } |
4693 | + |
4694 | + return null; |
4695 | + } |
4696 | + |
4697 | + public static Gee.List<Synapse.Match> find_actions_for_match (Synapse.Match match) { |
4698 | + return sink.find_actions_for_match (match, null, Synapse.QueryFlags.ALL); |
4699 | + } |
4700 | + |
4701 | + /** |
4702 | + * Attempts to load a favicon for an UriMatch and caches the icon |
4703 | + * |
4704 | + * @param match The UriMatch |
4705 | + * @param size The icon size at which to load the icon. If the favicon is smaller than |
4706 | + * that size, null will be returned |
4707 | + * @param cancellable Cancellable for the loading operations |
4708 | + * @return The pixbuf or null if loading failed or the icon was too small |
4709 | + */ |
4710 | + public static async Gdk.Pixbuf? get_favicon_for_match (Synapse.UriMatch match, int size, |
4711 | + Cancellable? cancellable = null) { |
4712 | + |
4713 | + var soup_uri = new Soup.URI (match.uri); |
4714 | + if (!soup_uri.scheme.has_prefix ("http")) |
4715 | + return null; |
4716 | + |
4717 | + Gdk.Pixbuf? pixbuf = null; |
4718 | + |
4719 | + if (favicon_cache.has_key (soup_uri.host)) |
4720 | + return favicon_cache.get (soup_uri.host); |
4721 | + |
4722 | + var url = "%s://%s/favicon.ico".printf (soup_uri.scheme, soup_uri.host); |
4723 | + |
4724 | + var msg = new Soup.Message ("GET", url); |
4725 | + var session = new Soup.Session (); |
4726 | + session.use_thread_context = true; |
4727 | + |
4728 | + try { |
4729 | + var stream = yield session.send_async (msg, cancellable); |
4730 | + if (stream != null) { |
4731 | + pixbuf = yield new Gdk.Pixbuf.from_stream_async (stream, cancellable); |
4732 | + // as per design decision, icons that are smaller than requested will not |
4733 | + // be displayed, instead the fallback should be used, so we return null |
4734 | + if (pixbuf.width < size) |
4735 | + pixbuf = null; |
4736 | + } |
4737 | + } catch (Error e) {} |
4738 | + |
4739 | + if (cancellable.is_cancelled ()) |
4740 | + return null; |
4741 | + |
4742 | + // we set the cache in any case, even if things failed. No need to |
4743 | + // try requesting an icon again and again |
4744 | + favicon_cache.set (soup_uri.host, pixbuf); |
4745 | + |
4746 | + return pixbuf; |
4747 | + } |
4748 | + |
4749 | + // copied from synapse-ui with some slight changes |
4750 | + public static string markup_string_with_search (string text, string pattern) { |
4751 | + |
4752 | + string markup = "%s"; |
4753 | + |
4754 | + if (pattern == "") { |
4755 | + return markup.printf (Markup.escape_text (text)); |
4756 | + } |
4757 | + |
4758 | + // if no text found, use pattern |
4759 | + if (text == "") { |
4760 | + return markup.printf (Markup.escape_text (pattern)); |
4761 | + } |
4762 | + |
4763 | + var matchers = Synapse.Query.get_matchers_for_query (pattern, 0, |
4764 | + RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); |
4765 | + |
4766 | + string? highlighted = null; |
4767 | + foreach (var matcher in matchers) { |
4768 | + MatchInfo mi; |
4769 | + if (matcher.key.match (text, 0, out mi)) { |
4770 | + int start_pos; |
4771 | + int end_pos; |
4772 | + int last_pos = 0; |
4773 | + int cnt = mi.get_match_count (); |
4774 | + StringBuilder res = new StringBuilder (); |
4775 | + for (int i = 1; i < cnt; i++) { |
4776 | + mi.fetch_pos (i, out start_pos, out end_pos); |
4777 | + warn_if_fail (start_pos >= 0 && end_pos >= 0); |
4778 | + res.append (Markup.escape_text (text.substring (last_pos, start_pos - last_pos))); |
4779 | + last_pos = end_pos; |
4780 | + res.append (Markup.printf_escaped ("<b>%s</b>", mi.fetch (i))); |
4781 | + if (i == cnt - 1) { |
4782 | + res.append (Markup.escape_text (text.substring (last_pos))); |
4783 | + } |
4784 | + } |
4785 | + highlighted = res.str; |
4786 | + break; |
4787 | + } |
4788 | + } |
4789 | + |
4790 | + if (highlighted != null) { |
4791 | + return markup.printf (highlighted); |
4792 | + } else { |
4793 | + return markup.printf (Markup.escape_text(text)); |
4794 | + } |
4795 | + } |
4796 | + } |
4797 | +} |
4798 | + |
4799 | |
4800 | === modified file 'src/Slingshot.vala' |
4801 | --- src/Slingshot.vala 2013-12-26 00:08:04 +0000 |
4802 | +++ src/Slingshot.vala 2014-06-16 07:45:32 +0000 |
4803 | @@ -36,20 +36,20 @@ |
4804 | build_version_info = Build.VERSION_INFO; |
4805 | |
4806 | program_name = "Slingshot"; |
4807 | - exec_name = "slingshot-launcher"; |
4808 | - app_copyright = "GPLv3"; |
4809 | - app_icon = ""; |
4810 | - app_launcher = ""; |
4811 | + exec_name = "slingshot-launcher"; |
4812 | + app_copyright = "GPLv3"; |
4813 | + app_icon = ""; |
4814 | + app_launcher = ""; |
4815 | app_years = "2011-2012"; |
4816 | application_id = "net.launchpad.slingshot"; |
4817 | - main_url = "https://launchpad.net/slingshot"; |
4818 | - bug_url = "https://bugs.launchpad.net/slingshot"; |
4819 | - help_url = "https://answers.launchpad.net/slingshot"; |
4820 | - translate_url = "https://translations.launchpad.net/slingshot"; |
4821 | + main_url = "https://launchpad.net/slingshot"; |
4822 | + bug_url = "https://bugs.launchpad.net/slingshot"; |
4823 | + help_url = "https://answers.launchpad.net/slingshot"; |
4824 | + translate_url = "https://translations.launchpad.net/slingshot"; |
4825 | |
4826 | - about_authors = {"Giulio Collura <random.cpp@gmail.com>", |
4827 | - "Andrea Basso <andrea@elementaryos.org"}; |
4828 | - about_artists = {"Harvey Cabaguio 'BassUltra' <harveycabaguio@gmail.com>", |
4829 | + about_authors = {"Giulio Collura <random.cpp@gmail.com>", |
4830 | + "Andrea Basso <andrea@elementaryos.org"}; |
4831 | + about_artists = {"Harvey Cabaguio 'BassUltra' <harveycabaguio@gmail.com>", |
4832 | "Daniel Foré <bunny@go-docky.com>"}; |
4833 | about_translators = "Launchpad Translators"; |
4834 | about_license_type = Gtk.License.GPL_3_0; |
4835 | |
4836 | === modified file 'src/SlingshotView.vala' |
4837 | --- src/SlingshotView.vala 2014-05-28 08:48:51 +0000 |
4838 | +++ src/SlingshotView.vala 2014-06-16 07:45:32 +0000 |
4839 | @@ -27,7 +27,8 @@ |
4840 | public class SlingshotView : Granite.Widgets.PopOver { |
4841 | |
4842 | // Widgets |
4843 | - public Gtk.SearchEntry search_entry; |
4844 | + public Gtk.SearchEntry dummy_search_entry; |
4845 | + public Widgets.LargeSearchEntry real_search_entry; |
4846 | public Gtk.Stack stack; |
4847 | public Granite.Widgets.ModeButton view_selector; |
4848 | |
4849 | @@ -39,6 +40,7 @@ |
4850 | public Gtk.Grid top; |
4851 | public Gtk.Grid center; |
4852 | public Gtk.Grid container; |
4853 | + public Gtk.Stack main_stack; |
4854 | public Gtk.Box content_area; |
4855 | private Gtk.EventBox event_box; |
4856 | |
4857 | @@ -46,11 +48,11 @@ |
4858 | private Gee.ArrayList<GMenu.TreeDirectory> categories; |
4859 | public Gee.HashMap<string, Gee.ArrayList<Backend.App>> apps; |
4860 | |
4861 | - private int current_position = 0; |
4862 | - private int search_view_position = 0; |
4863 | private Modality modality; |
4864 | private bool can_trigger_hotcorner = true; |
4865 | |
4866 | + private Backend.SynapseSearch synapse; |
4867 | + |
4868 | // Sizes |
4869 | public int columns { |
4870 | get { |
4871 | @@ -91,6 +93,7 @@ |
4872 | Slingshot.icon_theme = Gtk.IconTheme.get_default (); |
4873 | |
4874 | app_system = new Backend.AppSystem (); |
4875 | + synapse = new Backend.SynapseSearch (); |
4876 | |
4877 | categories = app_system.get_categories (); |
4878 | apps = app_system.get_apps (); |
4879 | @@ -133,6 +136,10 @@ |
4880 | // Create the base container |
4881 | container = new Gtk.Grid (); |
4882 | |
4883 | + main_stack = new Gtk.Stack (); |
4884 | + |
4885 | + main_stack.add_named (container, "apps"); |
4886 | + |
4887 | // Add top bar |
4888 | top = new Gtk.Grid (); |
4889 | |
4890 | @@ -154,16 +161,16 @@ |
4891 | else |
4892 | view_selector.selected = 0; |
4893 | |
4894 | - search_entry = new Gtk.SearchEntry (); |
4895 | - search_entry.placeholder_text = _("Search Apps…"); |
4896 | - search_entry.width_request = 250; |
4897 | - search_entry.button_press_event.connect ((e) => {return e.button == 3;}); |
4898 | + dummy_search_entry = new Gtk.SearchEntry (); |
4899 | + dummy_search_entry.placeholder_text = _("Search Apps…"); |
4900 | + dummy_search_entry.width_request = 250; |
4901 | + dummy_search_entry.button_press_event.connect ((e) => {return e.button == 3;}); |
4902 | |
4903 | if (Slingshot.settings.show_category_filter) { |
4904 | top.attach (view_selector, 0, 0, 1, 1); |
4905 | } |
4906 | top.attach (top_separator, 1, 0, 1, 1); |
4907 | - top.attach (search_entry, 2, 0, 1, 1); |
4908 | + top.attach (dummy_search_entry, 2, 0, 1, 1); |
4909 | |
4910 | center = new Gtk.Grid (); |
4911 | |
4912 | @@ -178,12 +185,21 @@ |
4913 | stack.add_named (scrolled_normal, "normal"); |
4914 | |
4915 | // Create the "SEARCH_VIEW" |
4916 | + var search_view_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); |
4917 | + |
4918 | + real_search_entry = new Widgets.LargeSearchEntry (); |
4919 | + real_search_entry.margin_left = real_search_entry.margin_right = 12; |
4920 | + |
4921 | search_view = new Widgets.SearchView (this); |
4922 | - |
4923 | - foreach (Gee.ArrayList<Backend.App> app_list in apps.values) { |
4924 | - search_view.add_apps (app_list); |
4925 | - } |
4926 | - stack.add_named (search_view, "search"); |
4927 | + search_view.start_search.connect ((match, target) => { |
4928 | + search.begin (real_search_entry.text, match, target); |
4929 | + }); |
4930 | + |
4931 | + search_view_container.pack_start (real_search_entry, false); |
4932 | + search_view_container.pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), false); |
4933 | + search_view_container.pack_start (search_view); |
4934 | + |
4935 | + main_stack.add_named (search_view_container, "search"); |
4936 | |
4937 | // Create the "CATEGORY_VIEW" |
4938 | category_view = new Widgets.CategoryView (this); |
4939 | @@ -193,7 +209,7 @@ |
4940 | container.attach (Utils.set_padding (center, 0, 12, 12, 12), 0, 1, 1, 1); |
4941 | |
4942 | event_box = new Gtk.EventBox (); |
4943 | - event_box.add (container); |
4944 | + event_box.add (main_stack); |
4945 | // Add the container to the dialog's content area |
4946 | content_area = get_content_area () as Gtk.Box; |
4947 | content_area.pack_start (event_box); |
4948 | @@ -265,24 +281,37 @@ |
4949 | private void connect_signals () { |
4950 | |
4951 | this.focus_in_event.connect (() => { |
4952 | - search_entry.grab_focus (); |
4953 | + get_current_search_entry ().grab_focus (); |
4954 | return false; |
4955 | }); |
4956 | |
4957 | event_box.key_press_event.connect (on_key_press); |
4958 | - search_entry.key_press_event.connect (search_entry_key_press); |
4959 | - |
4960 | - search_entry.search_changed.connect (() => this.search.begin (search_entry.text)); |
4961 | - search_entry.grab_focus (); |
4962 | - |
4963 | - search_entry.activate.connect (() => { |
4964 | - if (modality == Modality.SEARCH_VIEW) { |
4965 | - search_view.launch_selected (); |
4966 | - hide (); |
4967 | - } else { |
4968 | - if (get_focus () as Widgets.AppEntry != null) // checking the selected widget is an AppEntry |
4969 | - ((Widgets.AppEntry) get_focus ()).launch_app (); |
4970 | - } |
4971 | + dummy_search_entry.key_press_event.connect (search_entry_key_press); |
4972 | + real_search_entry.widget.key_press_event.connect (search_entry_key_press); |
4973 | + |
4974 | + real_search_entry.search_changed.connect (() => { |
4975 | + search.begin (real_search_entry.text); |
4976 | + }); |
4977 | + dummy_search_entry.search_changed.connect (() => { |
4978 | + if (modality != Modality.SEARCH_VIEW) |
4979 | + set_modality (Modality.SEARCH_VIEW); |
4980 | + }); |
4981 | + dummy_search_entry.grab_focus (); |
4982 | + |
4983 | + dummy_search_entry.activate.connect (search_entry_activated); |
4984 | + real_search_entry.widget.activate.connect (search_entry_activated); |
4985 | + |
4986 | + // the focus-out event is fired as soon as the stack transition is ended |
4987 | + // at which point we're able to focus the real_search_entry |
4988 | + dummy_search_entry.focus_out_event.connect (() => { |
4989 | + real_search_entry.text = dummy_search_entry.text; |
4990 | + real_search_entry.widget.grab_focus (); |
4991 | + var cursor_pos = real_search_entry.text.length; |
4992 | + real_search_entry.widget.select_region (cursor_pos, cursor_pos); |
4993 | + |
4994 | + dummy_search_entry.text = ""; |
4995 | + |
4996 | + return false; |
4997 | }); |
4998 | |
4999 | search_view.app_launched.connect (() => hide ()); |
5000 | @@ -365,9 +394,33 @@ |
/lib/synapse- core/desktop- file-service. vala:333. 5-333.39: warning: method `Synapse. DesktopFileServ ice.get_ cache_file_ name' never used ^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^
private string? get_cache_file_name (string dir_name)
^^^