Merge lp:~elementary-apps/slingshot/synapse-desktop-only into lp:~elementary-pantheon/slingshot/trunk

Proposed by Tom Beckmann on 2014-06-12
Status: Merged
Approved by: Cody Garver on 2014-06-28
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
Reviewer Review Type Date Requested Status
Daniel Fore Approve on 2014-06-28
Sergey "Shnatsel" Davidoff (community) ux opinion 2014-06-12 Disapprove on 2014-06-13
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.

To post a comment you must log in.
438. By Tom Beckmann on 2014-06-12

more code style fixes

Cody Garver (codygarver) wrote :

/lib/synapse-core/desktop-file-service.vala:333.5-333.39: warning: method `Synapse.DesktopFileService.get_cache_file_name' never used
    private string? get_cache_file_name (string dir_name)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Cody Garver (codygarver) wrote :

lib/synapse-plugins/desktop-file-plugin.vala:280.35-280.61: warning: Gdk.AppLaunchContext.new has been deprecated since 3.0

lib/synapse-plugins/desktop-file-plugin.vala:115.9-115.30: warning: implicit .begin is deprecated

lib/synapse-plugins/desktop-file-plugin.vala:118.7-118.28: warning: implicit .begin is deprecated

Cody Garver (codygarver) wrote :

lib/synapse-core/CMakeLists.txt should say

add_definitions(${CORE_DEPS_CFLAGS} -include config.h -w)

lib/synapse-plugins/CMakeLists.txt should say

add_definitions(${PLUGINS_DEPS_CFLAGS} -include config.h -w)

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.

review: Disapprove (ux opinion)
439. By Tom Beckmann on 2014-06-13

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 on 2014-06-16

actually enable zeitgeist relevancy

Daniel Fore (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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 @@
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches