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

Proposed by Tom Beckmann
Status: Merged
Approved by: Cody Garver
Approved revision: 440
Merged at revision: 422
Proposed branch: lp:~elementary-apps/slingshot/synapse-desktop-only
Merge into: lp:~elementary-pantheon/slingshot/trunk
Diff against target: 5927 lines (+5108/-286)
27 files modified
CMakeLists.txt (+16/-3)
lib/synapse-core/CMakeLists.txt (+54/-0)
lib/synapse-core/common-actions.vala (+419/-0)
lib/synapse-core/config-service.vala (+192/-0)
lib/synapse-core/data-sink.vala (+574/-0)
lib/synapse-core/dbus-service.vala (+166/-0)
lib/synapse-core/desktop-file-service.vala (+635/-0)
lib/synapse-core/match.vala (+144/-0)
lib/synapse-core/plugin.vala (+59/-0)
lib/synapse-core/query.vala (+296/-0)
lib/synapse-core/relevancy-backend-zg.vala (+308/-0)
lib/synapse-core/relevancy-service.vala (+95/-0)
lib/synapse-core/result-set.vala (+121/-0)
lib/synapse-core/utils.vala (+439/-0)
lib/synapse-core/volume-service.vala (+193/-0)
lib/synapse-plugins/CMakeLists.txt (+46/-0)
lib/synapse-plugins/command-plugin.vala (+188/-0)
lib/synapse-plugins/desktop-file-plugin.vala (+350/-0)
src/Backend/App.vala (+81/-11)
src/Backend/AppSystem.vala (+8/-6)
src/Backend/SynapseSearch.vala (+163/-0)
src/Slingshot.vala (+11/-11)
src/SlingshotView.vala (+130/-54)
src/Widgets/CategoryView.vala (+2/-2)
src/Widgets/LargeSearchEntry.vala (+73/-0)
src/Widgets/SearchItem.vala (+51/-50)
src/Widgets/SearchView.vala (+294/-149)
To merge this branch: bzr merge lp:~elementary-apps/slingshot/synapse-desktop-only
Reviewer Review Type Date Requested Status
Danielle Foré Approve
Sergey "Shnatsel" Davidoff (community) ux opinion Disapprove
Review via email: mp+222908@code.launchpad.net

Commit message

Copy the sources of Synapse over and implements a new searchview using libsynapse

Copying is currently necessary as synapse's library is internal. I hope we can change this soon, although I don't consider it very urgent, as synapse is almost unmaintained at the moment, so we most likely won't miss out on important updates.

Description of the change

This branch copies the sources of synapse over to slingshot and implements a new searchview using libsynapse.

Copying is currently necessary as synapse's library is internal. I hope we can change this soon, although I don't consider it very urgent, as synapse is almost unmaintained at the moment, so we most likely won't miss out on important updates.

There is currently some unused code flying around in that branch, that's because Dan requested to only add the desktop-file-plugin and the command-plugin for now, so we can have something to merge. The features that are currently inaccessible are mainly the context view, which would pull up context actions when you press "tab" on a result and helper stuff like grabbing favicons for internet results, which currently don't get as both the bookmarks plugin and the zeitgeist plugin are disabled.
I would prefer keeping this code though, for one removing it would probably require more work than fixing it (admittedly, testing is where the actual work would currently be needed, but still) and also I think it's all backend related, so at least the code could already be checked for this review.

To post a comment you must log in.
438. By Tom Beckmann

more code style fixes

Revision history for this message
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)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Revision history for this message
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

Revision history for this message
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)

Revision history for this message
Sergey "Shnatsel" Davidoff (shnatsel) wrote :

Design-wise, I'd prefer Slingshot to search not yet installed applications when I enter something in the search bar. For example, if I type "GIMP" on a clean system right now, I get nothing and have to go to software center, re-type "GIMP", click install, wait for it to complete, close software center, go to slingshot again, type "GIMP" again, click it. I believe this could be streamlined a lot by searching not yet installed applications and showing them *after* the installed ones with "More info..." and "Install" buttons on it, for people unfamiliar and familiar with the program, respectively.

Such functionality is much more relevant to an application launcher and leaves no room for Synapse, thus I believe that Synapse belongs to its own indicator.

review: Disapprove (ux opinion)
439. By Tom Beckmann

disable c warnings

Revision history for this message
Tom Beckmann (tombeckmann) wrote :

@Cody, added -w now. I'd prefer to keep the files by synapse as they are for now, as the final plan is to not have the sources in our project anyway, but if you want me to fix those warnings I can do that too.

440. By Tom Beckmann

actually enable zeitgeist relevancy

Revision history for this message
Danielle Foré (danrabbit) wrote :

Cody says he's good on the code side.

I'm good design side. This branch is made to lay a foundation, not introduce any new features yet. We can address additional feature requests in later branches.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2014-05-28 08:48:51 +0000
+++ CMakeLists.txt 2014-06-16 07:45:32 +0000
@@ -54,7 +54,7 @@
54 message ("-- Zeitgeist integration disabled")54 message ("-- Zeitgeist integration disabled")
55endif ()55endif ()
5656
57set (CORE_DEPS "gobject-2.0;glib-2.0;gio-2.0;gio-unix-2.0;gee-0.8;libgnome-menu-3.0;${UNITY_DEPS};")57set (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};")
58set (UI_DEPS "gtk+-3.0>=3.10.0;granite;${ZEITGEIST_DEPS};")58set (UI_DEPS "gtk+-3.0>=3.10.0;granite;${ZEITGEIST_DEPS};")
5959
60find_package (PkgConfig)60find_package (PkgConfig)
@@ -74,8 +74,10 @@
74 src/Backend/DBusService.vala74 src/Backend/DBusService.vala
75 src/Backend/App.vala75 src/Backend/App.vala
76 src/Backend/RelevancyService.vala76 src/Backend/RelevancyService.vala
77 src/Backend/SynapseSearch.vala
77 src/Widgets/AppEntry.vala78 src/Widgets/AppEntry.vala
78 src/Widgets/Grid.vala79 src/Widgets/Grid.vala
80 src/Widgets/LargeSearchEntry.vala
79 src/Widgets/Switcher.vala81 src/Widgets/Switcher.vala
80 src/Widgets/SearchView.vala82 src/Widgets/SearchView.vala
81 src/Widgets/SearchItem.vala83 src/Widgets/SearchItem.vala
@@ -84,15 +86,25 @@
84PACKAGES86PACKAGES
85 ${CORE_DEPS}87 ${CORE_DEPS}
86 ${UI_DEPS}88 ${UI_DEPS}
89 synapse-core
90 synapse-plugins
87CUSTOM_VAPIS91CUSTOM_VAPIS
88 vapi/config.vapi92 vapi/config.vapi
89OPTIONS93OPTIONS
90 --thread94 --thread
95 --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core
96 --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-plugins
91 -g97 -g
92 ${UNITY_OPTIONS}98 ${UNITY_OPTIONS}
93 ${ZEITGEIST_OPTIONS}99 ${ZEITGEIST_OPTIONS}
94)100)
95101
102include_directories(${CMAKE_BINARY_DIR}/lib/synapse-core)
103include_directories(${CMAKE_BINARY_DIR}/lib/synapse-plugins)
104
105add_subdirectory(lib/synapse-core)
106add_subdirectory(lib/synapse-plugins)
107
96# Comment this out to enable C compiler warnings108# Comment this out to enable C compiler warnings
97add_definitions (-w)109add_definitions (-w)
98110
@@ -101,7 +113,7 @@
101link_directories (${DEPS_LIBRARY_DIRS})113link_directories (${DEPS_LIBRARY_DIRS})
102114
103add_executable (${APPNAME} ${VALA_C})115add_executable (${APPNAME} ${VALA_C})
104target_link_libraries(${APPNAME} m)116target_link_libraries(${APPNAME} m synapse-core synapse-plugins)
105117
106# Installation118# Installation
107install (TARGETS ${APPNAME} RUNTIME DESTINATION bin)119install (TARGETS ${APPNAME} RUNTIME DESTINATION bin)
@@ -114,4 +126,5 @@
114add_schema ("org.pantheon.desktop.slingshot.gschema.xml")126add_schema ("org.pantheon.desktop.slingshot.gschema.xml")
115127
116# Translations128# Translations
117add_subdirectory (po)
118\ No newline at end of file129\ No newline at end of file
130add_subdirectory (po)
131
119132
=== added directory 'lib'
=== added directory 'lib/synapse-core'
=== added file 'lib/synapse-core/CMakeLists.txt'
--- lib/synapse-core/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/CMakeLists.txt 2014-06-16 07:45:32 +0000
@@ -0,0 +1,54 @@
1set(CORE_LIB_VERSION 0.1)
2set(CORE_LIB_SOVERSION 0)
3set(CORE_LIBRARY_NAME synapse-core)
4set(CORE_PKG
5 glib-2.0
6 zeitgeist-1.0
7 gio-unix-2.0
8 json-glib-1.0
9 gee-0.8
10 gtk+-3.0
11)
12
13pkg_check_modules(CORE_DEPS REQUIRED ${CORE_PKG})
14
15set(CORE_SOURCE
16 common-actions.vala
17 config-service.vala
18 data-sink.vala
19 dbus-service.vala
20 desktop-file-service.vala
21 match.vala
22 plugin.vala
23 query.vala
24 relevancy-backend-zg.vala
25 relevancy-service.vala
26 result-set.vala
27 utils.vala
28 volume-service.vala
29)
30
31set(LINK_MODE STATIC)
32
33vala_precompile(CORE_VALA_C ${CORE_LIBRARY_NAME}
34 ${CORE_SOURCE}
35PACKAGES
36 ${CORE_PKG}
37GENERATE_VAPI
38 ${CORE_LIBRARY_NAME}
39GENERATE_HEADER
40 ${CORE_LIBRARY_NAME}
41)
42
43add_definitions(${CORE_DEPS_CFLAGS} -include config.h -w)
44link_directories(${CORE_DEPS_LIBRARY_DIRS})
45
46add_library(${CORE_LIBRARY_NAME} STATIC ${CORE_VALA_C})
47target_link_libraries (${CORE_LIBRARY_NAME} ${CORE_DEPS_LIBRARIES})
48
49set_target_properties(${CORE_LIBRARY_NAME} PROPERTIES
50 OUTPUT_NAME ${CORE_LIBRARY_NAME}
51 VERSION ${CORE_LIB_VERSION}
52 SOVERSION ${CORE_LIB_SOVERSION}
53)
54
055
=== added file 'lib/synapse-core/common-actions.vala'
--- lib/synapse-core/common-actions.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/common-actions.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,419 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public abstract class BaseAction: Object, Match
24 {
25 // from Match interface
26 public string title { get; construct set; }
27 public string description { get; set; }
28 public string icon_name { get; construct set; }
29 public bool has_thumbnail { get; construct set; }
30 public string thumbnail_path { get; construct set; }
31 public MatchType match_type { get; construct set; }
32
33 public int default_relevancy { get; set; }
34 public bool notify_match { get; set; default = true; }
35
36 public abstract bool valid_for_match (Match match);
37 public virtual int get_relevancy_for_match (Match match)
38 {
39 return default_relevancy;
40 }
41
42 public abstract void do_execute (Match? source, Match? target = null);
43 public void execute_with_target (Match? source, Match? target = null)
44 {
45 do_execute (source, target);
46 if (notify_match) source.executed ();
47 }
48
49 public virtual bool needs_target () {
50 return false;
51 }
52
53 public virtual QueryFlags target_flags ()
54 {
55 return QueryFlags.ALL;
56 }
57 }
58
59 public class CommonActions: Object, Activatable, ActionProvider
60 {
61 public bool enabled { get; set; default = true; }
62
63 public void activate ()
64 {
65
66 }
67
68 public void deactivate ()
69 {
70
71 }
72
73 private class Runner: BaseAction
74 {
75 public Runner ()
76 {
77 Object (title: _ ("Run"),
78 description: _ ("Run an application, action or script"),
79 icon_name: "system-run", has_thumbnail: false,
80 match_type: MatchType.ACTION,
81 default_relevancy: Match.Score.EXCELLENT);
82 }
83
84 public override void do_execute (Match? match, Match? target = null)
85 {
86 if (match.match_type == MatchType.APPLICATION)
87 {
88 ApplicationMatch? app_match = match as ApplicationMatch;
89 return_if_fail (app_match != null);
90
91 AppInfo app = app_match.app_info ??
92 new DesktopAppInfo.from_filename (app_match.filename);
93
94 try
95 {
96 var display = Gdk.Display.get_default ();
97 app.launch (null, display.get_app_launch_context ());
98
99 RelevancyService.get_default ().application_launched (app);
100 }
101 catch (Error err)
102 {
103 Utils.Logger.warning (this, "%s", err.message);
104 }
105 }
106 else // MatchType.ACTION
107 {
108 match.execute (null);
109 }
110 }
111
112 public override bool valid_for_match (Match match)
113 {
114 switch (match.match_type)
115 {
116 case MatchType.SEARCH:
117 return true;
118 case MatchType.ACTION:
119 return true;
120 case MatchType.APPLICATION:
121 ApplicationMatch? am = match as ApplicationMatch;
122 return am == null || !am.needs_terminal;
123 default:
124 return false;
125 }
126 }
127 }
128
129 private class TerminalRunner: BaseAction
130 {
131 public TerminalRunner ()
132 {
133 Object (title: _ ("Run in Terminal"),
134 description: _ ("Run application or command in terminal"),
135 icon_name: "terminal", has_thumbnail: false,
136 match_type: MatchType.ACTION,
137 default_relevancy: Match.Score.BELOW_AVERAGE);
138 }
139
140 public override void do_execute (Match? match, Match? target = null)
141 {
142 if (match.match_type == MatchType.APPLICATION)
143 {
144 ApplicationMatch? app_match = match as ApplicationMatch;
145 return_if_fail (app_match != null);
146
147 AppInfo original = app_match.app_info ??
148 new DesktopAppInfo.from_filename (app_match.filename);
149
150 try
151 {
152 AppInfo app = AppInfo.create_from_commandline (
153 original.get_commandline (), original.get_name (),
154 AppInfoCreateFlags.NEEDS_TERMINAL);
155 var display = Gdk.Display.get_default ();
156 app.launch (null, display.get_app_launch_context ());
157 }
158 catch (Error err)
159 {
160 Utils.Logger.warning (this, "%s", err.message);
161 }
162 }
163 }
164
165 public override bool valid_for_match (Match match)
166 {
167 switch (match.match_type)
168 {
169 case MatchType.APPLICATION:
170 ApplicationMatch? am = match as ApplicationMatch;
171 return am != null;
172 default:
173 return false;
174 }
175 }
176 }
177
178 private class Opener: BaseAction
179 {
180 public Opener ()
181 {
182 Object (title: _ ("Open"),
183 description: _ ("Open using default application"),
184 icon_name: "fileopen", has_thumbnail: false,
185 match_type: MatchType.ACTION,
186 default_relevancy: Match.Score.GOOD);
187 }
188
189 public override void do_execute (Match? match, Match? target = null)
190 {
191 UriMatch uri_match = match as UriMatch;
192
193 if (uri_match != null)
194 {
195 CommonActions.open_uri (uri_match.uri);
196 }
197 else if (file_path.match (match.title))
198 {
199 File f;
200 if (match.title.has_prefix ("~"))
201 {
202 f = File.new_for_path (Path.build_filename (Environment.get_home_dir (),
203 match.title.substring (1),
204 null));
205 }
206 else
207 {
208 f = File.new_for_path (match.title);
209 }
210 CommonActions.open_uri (f.get_uri ());
211 }
212 else
213 {
214 CommonActions.open_uri (match.title);
215 }
216 }
217
218 public override bool valid_for_match (Match match)
219 {
220 switch (match.match_type)
221 {
222 case MatchType.GENERIC_URI:
223 return true;
224 case MatchType.UNKNOWN:
225 return web_uri.match (match.title) || file_path.match (match.title);
226 default:
227 return false;
228 }
229 }
230
231 private Regex web_uri;
232 private Regex file_path;
233
234 construct
235 {
236 try
237 {
238 web_uri = new Regex ("^(ftp|http(s)?)://[^.]+\\.[^.]+", RegexCompileFlags.OPTIMIZE);
239 file_path = new Regex ("^(/|~/)[^/]+", RegexCompileFlags.OPTIMIZE);
240 }
241 catch (Error err)
242 {
243 Utils.Logger.warning (this, "%s", err.message);
244 }
245 }
246 }
247
248 private class OpenFolder: BaseAction
249 {
250 public OpenFolder ()
251 {
252 Object (title: _ ("Open folder"),
253 description: _ ("Open folder containing this file"),
254 icon_name: "folder-open", has_thumbnail: false,
255 match_type: MatchType.ACTION,
256 default_relevancy: Match.Score.AVERAGE);
257 }
258
259 public override void do_execute (Match? match, Match? target = null)
260 {
261 UriMatch uri_match = match as UriMatch;
262 return_if_fail (uri_match != null);
263 var f = File.new_for_uri (uri_match.uri);
264 f = f.get_parent ();
265 try
266 {
267 var app_info = f.query_default_handler (null);
268 List<File> files = new List<File> ();
269 files.prepend (f);
270 var display = Gdk.Display.get_default ();
271 app_info.launch (files, display.get_app_launch_context ());
272 }
273 catch (Error err)
274 {
275 Utils.Logger.warning (this, "%s", err.message);
276 }
277 }
278
279 public override bool valid_for_match (Match match)
280 {
281 if (match.match_type != MatchType.GENERIC_URI) return false;
282 UriMatch uri_match = match as UriMatch;
283 var f = File.new_for_uri (uri_match.uri);
284 var parent = f.get_parent ();
285 return parent != null && f.is_native ();
286 }
287 }
288
289 private class ClipboardCopy: BaseAction
290 {
291 public ClipboardCopy ()
292 {
293 Object (title: _ ("Copy to Clipboard"),
294 description: _ ("Copy selection to clipboard"),
295 icon_name: "gtk-copy", has_thumbnail: false,
296 match_type: MatchType.ACTION,
297 default_relevancy: Match.Score.AVERAGE);
298 }
299
300 public override void do_execute (Match? match, Match? target = null)
301 {
302 var cb = Gtk.Clipboard.get (Gdk.Atom.NONE);
303 if (match.match_type == MatchType.GENERIC_URI)
304 {
305 UriMatch uri_match = match as UriMatch;
306 return_if_fail (uri_match != null);
307
308 /*
309 // just wow, Gtk and also Vala are trying really hard to make this hard to do...
310 Gtk.TargetEntry[] no_entries = {};
311 Gtk.TargetList l = new Gtk.TargetList (no_entries);
312 l.add_uri_targets (0);
313 l.add_text_targets (0);
314 Gtk.TargetEntry te = Gtk.target_table_new_from_list (l, 2);
315 cb.set_with_data ();
316 */
317 cb.set_text (uri_match.uri, -1);
318 }
319 else if (match.match_type == MatchType.TEXT)
320 {
321 TextMatch? text_match = match as TextMatch;
322 string content = text_match != null ? text_match.get_text () : match.title;
323
324 cb.set_text (content, -1);
325 }
326 }
327
328 public override bool valid_for_match (Match match)
329 {
330 switch (match.match_type)
331 {
332 case MatchType.GENERIC_URI:
333 return true;
334 case MatchType.TEXT:
335 return true;
336 default:
337 return false;
338 }
339 }
340
341 public override int get_relevancy_for_match (Match match)
342 {
343 TextMatch? text_match = match as TextMatch;
344 if (text_match != null && text_match.text_origin == TextOrigin.CLIPBOARD)
345 {
346 return 0;
347 }
348
349 return default_relevancy;
350 }
351 }
352
353 private Gee.List<BaseAction> actions;
354
355 construct
356 {
357 actions = new Gee.ArrayList<BaseAction> ();
358
359 actions.add (new Runner ());
360 actions.add (new TerminalRunner ());
361 actions.add (new Opener ());
362 actions.add (new OpenFolder ());
363 actions.add (new ClipboardCopy ());
364 }
365
366 public ResultSet? find_for_match (ref Query query, Match match)
367 {
368 bool query_empty = query.query_string == "";
369 var results = new ResultSet ();
370
371 if (query_empty)
372 {
373 foreach (var action in actions)
374 {
375 if (action.valid_for_match (match))
376 {
377 results.add (action, action.get_relevancy_for_match (match));
378 }
379 }
380 }
381 else
382 {
383 var matchers = Query.get_matchers_for_query (query.query_string, 0,
384 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
385 foreach (var action in actions)
386 {
387 if (!action.valid_for_match (match)) continue;
388 foreach (var matcher in matchers)
389 {
390 if (matcher.key.match (action.title))
391 {
392 results.add (action, matcher.value);
393 break;
394 }
395 }
396 }
397 }
398
399 return results;
400 }
401
402 public static void open_uri (string uri)
403 {
404 var f = File.new_for_uri (uri);
405 try
406 {
407 var app_info = f.query_default_handler (null);
408 List<File> files = new List<File> ();
409 files.prepend (f);
410 var display = Gdk.Display.get_default ();
411 app_info.launch (files, display.get_app_launch_context ());
412 }
413 catch (Error err)
414 {
415 Utils.Logger.warning (null, "%s", err.message);
416 }
417 }
418 }
419}
0420
=== added file 'lib/synapse-core/config-service.vala'
--- lib/synapse-core/config-service.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/config-service.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,192 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21using Json;
22
23namespace Synapse
24{
25 public abstract class ConfigObject : GLib.Object
26 {
27 }
28
29 public class ConfigService : GLib.Object
30 {
31 // singleton that can be easily destroyed
32 private static unowned ConfigService? instance;
33 public static ConfigService get_default ()
34 {
35 return instance ?? new ConfigService ();
36 }
37
38 private ConfigService ()
39 {
40 }
41
42 ~ConfigService ()
43 {
44 // useless cause the timer takes a reference on self
45 if (save_timer_id != 0) save ();
46 instance = null;
47 }
48
49 private Json.Node root_node;
50 private string config_file_name;
51 private uint save_timer_id = 0;
52
53 construct
54 {
55 instance = this;
56
57 var parser = new Parser ();
58 config_file_name =
59 GLib.Path.build_filename (Environment.get_user_config_dir (), "synapse",
60 "config.json");
61 try
62 {
63 parser.load_from_file (config_file_name);
64 root_node = parser.get_root ().copy ();
65 if (root_node.get_node_type () != NodeType.OBJECT)
66 {
67 root_node = new Json.Node (NodeType.OBJECT);
68 root_node.take_object (new Json.Object ());
69 }
70 }
71 catch (Error err)
72 {
73 root_node = new Json.Node (NodeType.OBJECT);
74 root_node.take_object (new Json.Object ());
75 }
76 }
77
78 /**
79 * Creates an instance of an object derived from ConfigObject class, which
80 * will have its public properties set to values stored in config file, or
81 * to the default values if this object wasn't yet stored.
82 *
83 * @param group A group name.
84 * @param key A key name.
85 * @param config_type Type of the object (must be subclass of ConfigObject)
86 * @return An instance of config_type.
87 */
88 public ConfigObject get_config (string group, string key, Type config_type)
89 {
90 unowned Json.Object obj = root_node.get_object ();
91 unowned Json.Node group_node = obj.get_member (group);
92 if (group_node != null)
93 {
94 if (group_node.get_node_type () == NodeType.OBJECT)
95 {
96 unowned Json.Object group_obj = group_node.get_object ();
97 unowned Json.Node key_node = group_obj.get_member (key);
98 if (key_node != null && key_node.get_node_type () == NodeType.OBJECT)
99 {
100 var result = Json.gobject_deserialize (config_type, key_node);
101 return result as ConfigObject;
102 }
103 }
104 }
105
106 return GLib.Object.new (config_type) as ConfigObject;
107 }
108
109 /**
110 * Behaves in a similar way to get_config, but it also watches for changes
111 * in the returned config object and saves them back to the config file
112 * (without the need of calling set_config).
113 *
114 * @param group A group name.
115 * @param key A key name.
116 * @param config_type Type of the object (must be subclass of ConfigObject)
117 */
118 public ConfigObject bind_config (string group, string key, Type config_type)
119 {
120 ConfigObject config_object = get_config (group, key, config_type);
121 // make sure the lambda doesn't take a ref on the config_object
122 unowned ConfigObject co = config_object;
123 co.notify.connect (() => { this.set_config (group, key, co); });
124 return config_object;
125 }
126
127 /**
128 * Stores all public properties of the object to the config file under
129 * specified group and key names.
130 *
131 * @param group A group name.
132 * @param key A key name.
133 * @param cfg_obj ConfigObject instance.
134 */
135 public void set_config (string group, string key, ConfigObject cfg_obj)
136 {
137 unowned Json.Object obj = root_node.get_object ();
138 if (!obj.has_member (group) ||
139 obj.get_member (group).get_node_type () != NodeType.OBJECT)
140 {
141 // why set_object_member works, but set_member doesn't ?!
142 obj.set_object_member (group, new Json.Object ());
143 }
144
145 unowned Json.Object group_obj = obj.get_object_member (group);
146 // why the hell is this necessary?
147 if (group_obj.has_member (key)) group_obj.remove_member (key);
148
149 Json.Node node = Json.gobject_serialize (cfg_obj);
150 group_obj.set_object_member (key, node.get_object ());
151
152 if (save_timer_id != 0) Source.remove (save_timer_id);
153 // on crap, this takes a reference on self
154 save_timer_id = Timeout.add (30000, this.save_timeout);
155 }
156
157 private bool save_timeout ()
158 {
159 save_timer_id = 0;
160 save ();
161
162 return false;
163 }
164
165 /**
166 * Forces immediate saving of the configuration file to the filesystem.
167 */
168 public void save ()
169 {
170 if (save_timer_id != 0)
171 {
172 Source.remove (save_timer_id);
173 save_timer_id = 0;
174 }
175
176 var generator = new Generator ();
177 generator.pretty = true;
178 generator.set_root (root_node);
179
180 DirUtils.create_with_parents (GLib.Path.get_dirname (config_file_name), 0755);
181 try
182 {
183 generator.to_file (config_file_name);
184 }
185 catch (Error err)
186 {
187 warning ("%s", err.message);
188 }
189 }
190 }
191}
192
0193
=== added file 'lib/synapse-core/data-sink.vala'
--- lib/synapse-core/data-sink.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/data-sink.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,574 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public errordomain SearchError
24 {
25 SEARCH_CANCELLED,
26 UNKNOWN_ERROR
27 }
28
29 public interface SearchProvider : Object
30 {
31 public abstract async Gee.List<Match> search (string query,
32 QueryFlags flags,
33 ResultSet? dest_result_set,
34 Cancellable? cancellable = null) throws SearchError;
35 }
36
37 // don't move into a class, gir doesn't like it
38 [CCode (has_target = false)]
39 public delegate void PluginRegisterFunc ();
40
41 public class DataSink : Object, SearchProvider
42 {
43 public class PluginRegistry : Object
44 {
45 public class PluginInfo
46 {
47 public Type plugin_type;
48 public string title;
49 public string description;
50 public string icon_name;
51 public PluginRegisterFunc register_func;
52 public bool runnable;
53 public string runnable_error;
54 public PluginInfo (Type type, string title, string desc,
55 string icon_name, PluginRegisterFunc reg_func,
56 bool runnable, string runnable_error)
57 {
58 this.plugin_type = type;
59 this.title = title;
60 this.description = desc;
61 this.icon_name = icon_name;
62 this.register_func = reg_func;
63 this.runnable = runnable;
64 this.runnable_error = runnable_error;
65 }
66 }
67
68 public static unowned PluginRegistry instance = null;
69
70 private Gee.List<PluginInfo> plugins;
71
72 construct
73 {
74 instance = this;
75 plugins = new Gee.ArrayList<PluginInfo> ();
76 }
77
78 ~PluginRegistry ()
79 {
80 instance = null;
81 }
82
83 public static PluginRegistry get_default ()
84 {
85 return instance ?? new PluginRegistry ();
86 }
87
88 public void register_plugin (Type plugin_type,
89 string title,
90 string description,
91 string icon_name,
92 PluginRegisterFunc reg_func,
93 bool runnable = true,
94 string runnable_error = "")
95 {
96 // FIXME: how about a frickin Type -> PluginInfo map?!
97 int index = -1;
98 for (int i=0; i < plugins.size; i++)
99 {
100 if (plugins[i].plugin_type == plugin_type)
101 {
102 index = i;
103 break;
104 }
105 }
106 if (index >= 0) plugins.remove_at (index);
107
108 var p = new PluginInfo (plugin_type, title, description, icon_name,
109 reg_func, runnable, runnable_error);
110 plugins.add (p);
111 }
112
113 public Gee.List<PluginInfo> get_plugins ()
114 {
115 return plugins.read_only_view;
116 }
117
118 public PluginInfo? get_plugin_info_for_type (Type plugin_type)
119 {
120 foreach (PluginInfo pi in plugins)
121 {
122 if (pi.plugin_type == plugin_type) return pi;
123 }
124
125 return null;
126 }
127 }
128
129 private class DataSinkConfiguration : ConfigObject
130 {
131 // vala keeps array lengths, and therefore doesn't support setting arrays
132 // via automatic public properties
133 private string[] _disabled_plugins = null;
134 public string[] disabled_plugins
135 {
136 get
137 {
138 return _disabled_plugins;
139 }
140 set
141 {
142 _disabled_plugins = value;
143 }
144 }
145
146 public void set_plugin_enabled (Type t, bool enabled)
147 {
148 if (enabled) enable_plugin (t.name ());
149 else disable_plugin (t.name ());
150 }
151
152 public bool is_plugin_enabled (Type t)
153 {
154 if (_disabled_plugins == null) return true;
155 unowned string plugin_name = t.name ();
156 foreach (string s in _disabled_plugins)
157 {
158 if (s == plugin_name) return false;
159 }
160 return true;
161 }
162
163 private void enable_plugin (string name)
164 {
165 if (_disabled_plugins == null) return;
166 if (!(name in _disabled_plugins)) return;
167
168 string[] cpy = {};
169 foreach (string s in _disabled_plugins)
170 {
171 if (s != name) cpy += s;
172 }
173 _disabled_plugins = (owned) cpy;
174 }
175
176 private void disable_plugin (string name)
177 {
178 if (_disabled_plugins == null || !(name in _disabled_plugins))
179 {
180 _disabled_plugins += name;
181 }
182 }
183 }
184
185 public DataSink ()
186 {
187 }
188
189 ~DataSink ()
190 {
191 Utils.Logger.debug (this, "DataSink died...");
192 }
193
194 private DataSinkConfiguration config;
195 private Gee.Set<ItemProvider> item_plugins;
196 private Gee.Set<ActionProvider> action_plugins;
197 private uint query_id;
198 // data sink will keep reference to the name cache, so others will get this
199 // instance on call to get_default()
200 private DBusService dbus_name_cache;
201 private DesktopFileService desktop_file_service;
202 private PluginRegistry registry;
203 private RelevancyService relevancy_service;
204 private VolumeService volume_service;
205 private Type[] plugin_types;
206
207 construct
208 {
209 item_plugins = new Gee.HashSet<ItemProvider> ();
210 action_plugins = new Gee.HashSet<ActionProvider> ();
211 plugin_types = {};
212 query_id = 0;
213
214 var cfg = ConfigService.get_default ();
215 config = (DataSinkConfiguration)
216 cfg.get_config ("data-sink", "global", typeof (DataSinkConfiguration));
217
218 // oh well, yea we need a few singletons
219 registry = PluginRegistry.get_default ();
220 relevancy_service = RelevancyService.get_default ();
221 volume_service = VolumeService.get_default ();
222
223 initialize_caches.begin ();
224 register_static_plugin (typeof (CommonActions));
225 }
226
227 private async void initialize_caches ()
228 {
229 Idle.add_full (Priority.LOW, initialize_caches.callback);
230 yield;
231
232 int initialized_components = 0;
233 int NUM_COMPONENTS = 2;
234
235 dbus_name_cache = DBusService.get_default ();
236 dbus_name_cache.initialize.begin (() =>
237 {
238 initialized_components++;
239 if (initialized_components >= NUM_COMPONENTS)
240 {
241 initialize_caches.callback ();
242 }
243 });
244
245 desktop_file_service = DesktopFileService.get_default ();
246 desktop_file_service.reload_done.connect (this.check_plugins);
247 desktop_file_service.initialize.begin (() =>
248 {
249 initialized_components++;
250 if (initialized_components >= NUM_COMPONENTS)
251 {
252 initialize_caches.callback ();
253 }
254 });
255
256 yield;
257
258 Idle.add (() => { this.load_plugins (); return false; });
259 }
260
261 private void check_plugins ()
262 {
263 PluginRegisterFunc[] reg_funcs = {};
264 foreach (var pi in registry.get_plugins ())
265 {
266 reg_funcs += pi.register_func;
267 }
268
269 foreach (PluginRegisterFunc func in reg_funcs)
270 {
271 func ();
272 }
273 }
274
275 public bool has_empty_handlers { get; set; default = false; }
276 public bool has_unknown_handlers { get; set; default = false; }
277
278 private bool plugins_loaded = false;
279
280 public signal void plugin_registered (Object plugin);
281
282 protected void register_plugin (Object plugin)
283 {
284 if (plugin is ActionProvider)
285 {
286 ActionProvider action_plugin = plugin as ActionProvider;
287 action_plugins.add (action_plugin);
288 has_unknown_handlers |= action_plugin.handles_unknown ();
289 }
290 if (plugin is ItemProvider)
291 {
292 ItemProvider item_plugin = plugin as ItemProvider;
293 item_plugins.add (item_plugin);
294 has_empty_handlers |= item_plugin.handles_empty_query ();
295 }
296
297 plugin_registered (plugin);
298 }
299
300 private void update_has_unknown_handlers ()
301 {
302 bool tmp = false;
303 foreach (var action in action_plugins)
304 {
305 if (action.enabled && action.handles_unknown ())
306 {
307 tmp = true;
308 break;
309 }
310 }
311 has_unknown_handlers = tmp;
312 }
313
314 private void update_has_empty_handlers ()
315 {
316 bool tmp = false;
317 foreach (var item_plugin in item_plugins)
318 {
319 if (item_plugin.enabled && item_plugin.handles_empty_query ())
320 {
321 tmp = true;
322 break;
323 }
324 }
325 has_empty_handlers = tmp;
326 }
327
328 private Object? create_plugin (Type t)
329 {
330 var obj_class = (ObjectClass) t.class_ref ();
331 if (obj_class != null && obj_class.find_property ("data-sink") != null)
332 {
333 return Object.new (t, "data-sink", this, null);
334 }
335 else
336 {
337 return Object.new (t, null);
338 }
339 }
340
341 private void load_plugins ()
342 {
343 // FIXME: fetch and load modules
344 foreach (Type t in plugin_types)
345 {
346 t.class_ref (); // makes the plugin register itself into PluginRegistry
347 PluginRegistry.PluginInfo? info = registry.get_plugin_info_for_type (t);
348 bool skip = info != null && info.runnable == false;
349 if (config.is_plugin_enabled (t) && !skip)
350 {
351 var plugin = create_plugin (t);
352 register_plugin (plugin);
353 (plugin as Activatable).activate ();
354 }
355 }
356
357 plugins_loaded = true;
358 }
359
360 /* This needs to be called right after instantiation,
361 * if plugins_loaded == true, it won't have any effect. */
362 public void register_static_plugin (Type plugin_type)
363 {
364 if (plugin_type in plugin_types) return;
365 plugin_types += plugin_type;
366 }
367
368 public unowned Object? get_plugin (string name)
369 {
370 unowned Object? result = null;
371
372 foreach (var plugin in item_plugins)
373 {
374 if (plugin.get_type ().name () == name)
375 {
376 result = plugin;
377 break;
378 }
379 }
380
381 return result;
382 }
383
384 public bool is_plugin_enabled (Type plugin_type)
385 {
386 foreach (var plugin in item_plugins)
387 {
388 if (plugin.get_type () == plugin_type) return plugin.enabled;
389 }
390
391 foreach (var action in action_plugins)
392 {
393 if (action.get_type () == plugin_type) return action.enabled;
394 }
395
396 return false;
397 }
398
399 public void set_plugin_enabled (Type plugin_type, bool enabled)
400 {
401 // save it into our config object
402 config.set_plugin_enabled (plugin_type, enabled);
403 ConfigService.get_default ().set_config ("data-sink", "global", config);
404
405 foreach (var plugin in item_plugins)
406 {
407 if (plugin.get_type () == plugin_type)
408 {
409 plugin.enabled = enabled;
410 if (enabled) plugin.activate ();
411 else plugin.deactivate ();
412 update_has_empty_handlers ();
413 return;
414 }
415 }
416
417 foreach (var action in action_plugins)
418 {
419 if (action.get_type () == plugin_type)
420 {
421 action.enabled = enabled;
422 if (enabled) action.activate ();
423 else action.deactivate ();
424 update_has_unknown_handlers ();
425 return;
426 }
427 }
428
429 // plugin isn't instantiated yet
430 if (enabled)
431 {
432 var new_instance = create_plugin (plugin_type);
433 register_plugin (new_instance);
434 (new_instance as Activatable).activate ();
435 }
436 }
437
438 [Signal (detailed = true)]
439 public signal void search_done (ResultSet rs, uint query_id);
440
441 public async Gee.List<Match> search (string query,
442 QueryFlags flags,
443 ResultSet? dest_result_set,
444 Cancellable? cancellable = null) throws SearchError
445 {
446 // wait for our initialization
447 while (!plugins_loaded)
448 {
449 Timeout.add (100, search.callback);
450 yield;
451 if (cancellable != null && cancellable.is_cancelled ())
452 {
453 throw new SearchError.SEARCH_CANCELLED ("Cancelled");
454 }
455 }
456 var q = Query (query_id++, query, flags);
457 string query_stripped = query.strip ();
458
459 var cancellables = new GLib.List<Cancellable> ();
460
461 var current_result_set = dest_result_set ?? new ResultSet ();
462 int search_size = item_plugins.size;
463 // FIXME: this is probably useless, if async method finishes immediately,
464 // it'll call complete_in_idle
465 bool waiting = false;
466
467 foreach (var data_plugin in item_plugins)
468 {
469 bool skip = !data_plugin.enabled ||
470 (query == "" && !data_plugin.handles_empty_query ()) ||
471 !data_plugin.handles_query (q);
472 if (skip)
473 {
474 search_size--;
475 continue;
476 }
477 // we need to pass separate cancellable to each plugin, because we're
478 // running them in parallel
479 var c = new Cancellable ();
480 cancellables.prepend (c);
481 q.cancellable = c;
482 // magic comes here
483 data_plugin.search.begin (q, (src_obj, res) =>
484 {
485 var plugin = src_obj as ItemProvider;
486 try
487 {
488 var results = plugin.search.end (res);
489 this.search_done[plugin.get_type ().name ()] (results, q.query_id);
490 current_result_set.add_all (results);
491 }
492 catch (SearchError err)
493 {
494 if (!(err is SearchError.SEARCH_CANCELLED))
495 {
496 warning ("%s returned error: %s",
497 plugin.get_type ().name (), err.message);
498 }
499 }
500
501 if (--search_size == 0 && waiting) search.callback ();
502 });
503 }
504 cancellables.reverse ();
505
506 if (cancellable != null)
507 {
508 cancellable.connect (() =>
509 {
510 foreach (var c in cancellables) c.cancel ();
511 });
512 }
513
514 waiting = true;
515 if (search_size > 0) yield;
516
517 if (cancellable != null && cancellable.is_cancelled ())
518 {
519 throw new SearchError.SEARCH_CANCELLED ("Cancelled");
520 }
521
522 if (has_unknown_handlers && query_stripped != "")
523 {
524 var unknown_match = new DefaultMatch (query);
525 bool add_to_rs = false;
526 if (QueryFlags.ACTIONS in flags || QueryFlags.TEXT in flags)
527 {
528 // FIXME: maybe we should also check here if there are any matches
529 add_to_rs = true;
530 }
531 else
532 {
533 // check whether any of the actions support this category
534 var unknown_match_actions = find_actions_for_unknown_match (unknown_match, flags);
535 if (unknown_match_actions.size > 0) add_to_rs = true;
536 }
537
538 if (add_to_rs) current_result_set.add (unknown_match, 0);
539 }
540
541 return current_result_set.get_sorted_list ();
542 }
543
544 protected Gee.List<Match> find_actions_for_unknown_match (Match match,
545 QueryFlags flags)
546 {
547 var rs = new ResultSet ();
548 var q = Query (0, "", flags);
549 foreach (var action_plugin in action_plugins)
550 {
551 if (!action_plugin.enabled) continue;
552 if (!action_plugin.handles_unknown ()) continue;
553 rs.add_all (action_plugin.find_for_match (ref q, match));
554 }
555
556 return rs.get_sorted_list ();
557 }
558
559 public Gee.List<Match> find_actions_for_match (Match match, string? query,
560 QueryFlags flags)
561 {
562 var rs = new ResultSet ();
563 var q = Query (0, query ?? "", flags);
564 foreach (var action_plugin in action_plugins)
565 {
566 if (!action_plugin.enabled) continue;
567 rs.add_all (action_plugin.find_for_match (ref q, match));
568 }
569
570 return rs.get_sorted_list ();
571 }
572 }
573}
574
0575
=== added file 'lib/synapse-core/dbus-service.vala'
--- lib/synapse-core/dbus-service.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/dbus-service.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,166 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 [DBus (name = "org.freedesktop.DBus")]
24 public interface FreeDesktopDBus : GLib.Object
25 {
26 public const string UNIQUE_NAME = "org.freedesktop.DBus";
27 public const string OBJECT_PATH = "/org/freedesktop/DBus";
28
29 public abstract async string[] list_queued_owners (string name) throws IOError;
30 public abstract async string[] list_names () throws IOError;
31 public abstract async string[] list_activatable_names () throws IOError;
32 public abstract async bool name_has_owner (string name) throws IOError;
33 public signal void name_owner_changed (string name,
34 string old_owner,
35 string new_owner);
36 public abstract async uint32 start_service_by_name (string name,
37 uint32 flags) throws IOError;
38 public abstract async string get_name_owner (string name) throws IOError;
39 }
40
41 public class DBusService : Object
42 {
43 private FreeDesktopDBus proxy;
44 private Gee.Set<string> owned_names;
45 private Gee.Set<string> activatable_names;
46 private Gee.Set<string> system_activatable_names;
47
48 private Utils.AsyncOnce<bool> init_once;
49
50 // singleton that can be easily destroyed
51 public static DBusService get_default ()
52 {
53 return instance ?? new DBusService ();
54 }
55
56 private DBusService ()
57 {
58 }
59
60 private static unowned DBusService? instance;
61 construct
62 {
63 instance = this;
64 owned_names = new Gee.HashSet<string> ();
65 activatable_names = new Gee.HashSet<string> ();
66 system_activatable_names = new Gee.HashSet<string> ();
67 init_once = new Utils.AsyncOnce<bool> ();
68
69 initialize.begin ();
70 }
71
72 ~DBusService ()
73 {
74 instance = null;
75 }
76
77 private void name_owner_changed (FreeDesktopDBus sender,
78 string name,
79 string old_owner,
80 string new_owner)
81 {
82 if (name.has_prefix (":")) return;
83
84 if (old_owner == "")
85 {
86 owned_names.add (name);
87 owner_changed (name, true);
88 }
89 else if (new_owner == "")
90 {
91 owned_names.remove (name);
92 owner_changed (name, false);
93 }
94 }
95
96 public signal void owner_changed (string name, bool is_owned);
97
98 public bool name_has_owner (string name)
99 {
100 return name in owned_names;
101 }
102
103 public bool name_is_activatable (string name)
104 {
105 return name in activatable_names;
106 }
107
108 public bool service_is_available (string name)
109 {
110 return name in system_activatable_names;
111 }
112
113 public async void initialize ()
114 {
115 if (init_once.is_initialized ()) return;
116 var is_locked = yield init_once.enter ();
117 if (!is_locked) return;
118
119 string[] names;
120 try
121 {
122 proxy = Bus.get_proxy_sync (BusType.SESSION,
123 FreeDesktopDBus.UNIQUE_NAME,
124 FreeDesktopDBus.OBJECT_PATH);
125
126 proxy.name_owner_changed.connect (this.name_owner_changed);
127 names = yield proxy.list_names ();
128 foreach (unowned string name in names)
129 {
130 if (name.has_prefix (":")) continue;
131 owned_names.add (name);
132 }
133
134 names = yield proxy.list_activatable_names ();
135 foreach (unowned string session_act in names)
136 {
137 activatable_names.add (session_act);
138 }
139 }
140 catch (Error err)
141 {
142 warning ("%s", err.message);
143 }
144
145 try
146 {
147 FreeDesktopDBus sys_proxy = Bus.get_proxy_sync (
148 BusType.SYSTEM,
149 FreeDesktopDBus.UNIQUE_NAME,
150 FreeDesktopDBus.OBJECT_PATH);
151
152 names = yield sys_proxy.list_activatable_names ();
153 foreach (unowned string system_act in names)
154 {
155 system_activatable_names.add (system_act);
156 }
157 }
158 catch (Error sys_err)
159 {
160 warning ("%s", sys_err.message);
161 }
162 init_once.leave (true);
163 }
164 }
165}
166
0167
=== added file 'lib/synapse-core/desktop-file-service.vala'
--- lib/synapse-core/desktop-file-service.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/desktop-file-service.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,635 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 * Alberto Aldegheri <albyrock87+dev@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24 errordomain DesktopFileError
25 {
26 UNINTERESTING_ENTRY
27 }
28
29 public class DesktopFileInfo: Object
30 {
31 // registered environments from http://standards.freedesktop.org/menu-spec/latest
32 // (and pantheon)
33 [Flags]
34 public enum EnvironmentType
35 {
36 GNOME = 1 << 0,
37 KDE = 1 << 1,
38 LXDE = 1 << 2,
39 MATE = 1 << 3,
40 RAZOR = 1 << 4,
41 ROX = 1 << 5,
42 TDE = 1 << 6,
43 UNITY = 1 << 7,
44 XFCE = 1 << 8,
45 PANTHEON = 1 << 9,
46 OLD = 1 << 10,
47
48 ALL = 0x3FF
49 }
50
51 public string desktop_id { get; construct set; }
52 public string name { get; construct set; }
53 public string comment { get; set; default = ""; }
54 public string icon_name { get; construct set; default = ""; }
55
56 public bool needs_terminal { get; set; default = false; }
57 public string filename { get; construct set; }
58
59 public string exec { get; set; }
60
61 public bool is_hidden { get; private set; default = false; }
62 public bool is_valid { get; private set; default = true; }
63
64 public string[] mime_types = null;
65
66 private string? name_folded = null;
67 public unowned string get_name_folded ()
68 {
69 if (name_folded == null) name_folded = name.casefold ();
70 return name_folded;
71 }
72
73 public EnvironmentType show_in { get; set; default = EnvironmentType.ALL; }
74
75 private static const string GROUP = "Desktop Entry";
76
77 public DesktopFileInfo.for_keyfile (string path, KeyFile keyfile,
78 string desktop_id)
79 {
80 Object (filename: path, desktop_id: desktop_id);
81
82 init_from_keyfile (keyfile);
83 }
84
85 private EnvironmentType parse_environments (string[] environments)
86 {
87 EnvironmentType result = 0;
88 foreach (unowned string env in environments)
89 {
90 string env_up = env.up ();
91 switch (env_up)
92 {
93 case "GNOME": result |= EnvironmentType.GNOME; break;
94 case "PANTHEON": result |= EnvironmentType.PANTHEON; break;
95 case "KDE": result |= EnvironmentType.KDE; break;
96 case "LXDE": result |= EnvironmentType.LXDE; break;
97 case "MATE": result |= EnvironmentType.MATE; break;
98 case "RAZOR": result |= EnvironmentType.RAZOR; break;
99 case "ROX": result |= EnvironmentType.ROX; break;
100 case "TDE": result |= EnvironmentType.TDE; break;
101 case "UNITY": result |= EnvironmentType.UNITY; break;
102 case "XFCE": result |= EnvironmentType.XFCE; break;
103 case "OLD": result |= EnvironmentType.OLD; break;
104 default: warning ("%s is not understood", env); break;
105 }
106 }
107 return result;
108 }
109
110 private void init_from_keyfile (KeyFile keyfile)
111 {
112 try
113 {
114 if (keyfile.get_string (GROUP, "Type") != "Application")
115 {
116 throw new DesktopFileError.UNINTERESTING_ENTRY ("Not Application-type desktop entry");
117 }
118
119 if (keyfile.has_key (GROUP, "Categories"))
120 {
121 string[] categories = keyfile.get_string_list (GROUP, "Categories");
122 if ("Screensaver" in categories)
123 {
124 throw new DesktopFileError.UNINTERESTING_ENTRY ("Screensaver desktop entry");
125 }
126 }
127
128 DesktopAppInfo app_info;
129 app_info = new DesktopAppInfo.from_keyfile (keyfile);
130
131 if (app_info == null)
132 {
133 throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to create AppInfo");
134 }
135
136 name = app_info.get_name ();
137 exec = app_info.get_commandline ();
138 if (exec == null)
139 {
140 throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to get exec for %s".printf (name));
141 }
142
143 // check for hidden desktop files
144 if (keyfile.has_key (GROUP, "Hidden") &&
145 keyfile.get_boolean (GROUP, "Hidden"))
146 {
147 is_hidden = true;
148 }
149 if (keyfile.has_key (GROUP, "NoDisplay") &&
150 keyfile.get_boolean (GROUP, "NoDisplay"))
151 {
152 is_hidden = true;
153 }
154
155 comment = app_info.get_description () ?? "";
156
157 var icon = app_info.get_icon () ??
158 new ThemedIcon ("application-default-icon");
159 icon_name = icon.to_string ();
160
161 if (keyfile.has_key (GROUP, "MimeType"))
162 {
163 mime_types = keyfile.get_string_list (GROUP, "MimeType");
164 }
165 if (keyfile.has_key (GROUP, "Terminal"))
166 {
167 needs_terminal = keyfile.get_boolean (GROUP, "Terminal");
168 }
169 if (keyfile.has_key (GROUP, "OnlyShowIn"))
170 {
171 show_in = parse_environments (keyfile.get_string_list (GROUP,
172 "OnlyShowIn"));
173 }
174 else if (keyfile.has_key (GROUP, "NotShowIn"))
175 {
176 var not_show = parse_environments (keyfile.get_string_list (GROUP,
177 "NotShowIn"));
178 show_in = EnvironmentType.ALL ^ not_show;
179 }
180
181 // special case these, people are using them quite often and wonder
182 // why they don't appear
183 if (filename.has_suffix ("gconf-editor.desktop") ||
184 filename.has_suffix ("dconf-editor.desktop"))
185 {
186 is_hidden = false;
187 }
188 }
189 catch (Error err)
190 {
191 Utils.Logger.warning (this, "%s", err.message);
192 is_valid = false;
193 }
194 }
195 }
196
197 public class DesktopFileService : Object
198 {
199 private static unowned DesktopFileService? instance;
200 private Utils.AsyncOnce<bool> init_once;
201
202 // singleton that can be easily destroyed
203 public static DesktopFileService get_default ()
204 {
205 return instance ?? new DesktopFileService ();
206 }
207
208 private DesktopFileService ()
209 {
210 }
211
212 private Gee.List<FileMonitor> directory_monitors;
213 private Gee.List<DesktopFileInfo> all_desktop_files;
214 private Gee.List<DesktopFileInfo> non_hidden_desktop_files;
215 private Gee.Map<unowned string, Gee.List<DesktopFileInfo> > mimetype_map;
216 private Gee.Map<string, Gee.List<DesktopFileInfo> > exec_map;
217 private Gee.Map<string, DesktopFileInfo> desktop_id_map;
218 private Gee.MultiMap<string, string> mimetype_parent_map;
219
220 construct
221 {
222 instance = this;
223
224 directory_monitors = new Gee.ArrayList<FileMonitor> ();
225 all_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
226 non_hidden_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
227 mimetype_parent_map = new Gee.HashMultiMap<string, string> ();
228 init_once = new Utils.AsyncOnce<bool> ();
229
230 initialize.begin ();
231 }
232
233 ~DesktopFileService ()
234 {
235 instance = null;
236 }
237
238 public async void initialize ()
239 {
240 if (init_once.is_initialized ()) return;
241 var is_locked = yield init_once.enter ();
242 if (!is_locked) return;
243
244 get_environment_type ();
245 DesktopAppInfo.set_desktop_env (session_type_str);
246
247 Idle.add_full (Priority.LOW, initialize.callback);
248 yield;
249
250 yield load_all_desktop_files ();
251
252 init_once.leave (true);
253 }
254
255 private DesktopFileInfo.EnvironmentType session_type =
256 DesktopFileInfo.EnvironmentType.GNOME;
257 private string session_type_str = "GNOME";
258
259 public DesktopFileInfo.EnvironmentType get_environment ()
260 {
261 return this.session_type;
262 }
263
264 private void get_environment_type ()
265 {
266 unowned string? session_var;
267 session_var = Environment.get_variable ("XDG_CURRENT_DESKTOP");
268 if (session_var == null)
269 {
270 session_var = Environment.get_variable ("DESKTOP_SESSION");
271 }
272
273 if (session_var == null) return;
274
275 string session = session_var.down ();
276
277 if (session.has_prefix ("unity") || session.has_prefix ("ubuntu"))
278 {
279 session_type = DesktopFileInfo.EnvironmentType.UNITY;
280 session_type_str = "Unity";
281 }
282 else if (session.has_prefix ("kde"))
283 {
284 session_type = DesktopFileInfo.EnvironmentType.KDE;
285 session_type_str = "KDE";
286 }
287 else if (session.has_prefix ("gnome"))
288 {
289 session_type = DesktopFileInfo.EnvironmentType.GNOME;
290 session_type_str = "GNOME";
291 }
292 else if (session.has_prefix ("lx"))
293 {
294 session_type = DesktopFileInfo.EnvironmentType.LXDE;
295 session_type_str = "LXDE";
296 }
297 else if (session.has_prefix ("xfce"))
298 {
299 session_type = DesktopFileInfo.EnvironmentType.XFCE;
300 session_type_str = "XFCE";
301 }
302 else if (session.has_prefix ("mate"))
303 {
304 session_type = DesktopFileInfo.EnvironmentType.MATE;
305 session_type_str = "MATE";
306 }
307 else if (session.has_prefix ("razor"))
308 {
309 session_type = DesktopFileInfo.EnvironmentType.RAZOR;
310 session_type_str = "Razor";
311 }
312 else if (session.has_prefix ("tde"))
313 {
314 session_type = DesktopFileInfo.EnvironmentType.TDE;
315 session_type_str = "TDE";
316 }
317 else if (session.has_prefix ("rox"))
318 {
319 session_type = DesktopFileInfo.EnvironmentType.ROX;
320 session_type_str = "ROX";
321 }
322 else if (session.has_prefix ("pantheon"))
323 {
324 session_type = DesktopFileInfo.EnvironmentType.PANTHEON;
325 session_type_str = "Pantheon";
326 }
327 else
328 {
329 warning ("Desktop session type is not recognized, assuming GNOME.");
330 }
331 }
332
333 private string? get_cache_file_name (string dir_name)
334 {
335 // FIXME: should we use this? it's Ubuntu-specific
336 string? locale = Intl.setlocale (LocaleCategory.MESSAGES, null);
337 if (locale == null) return null;
338
339 // even though this is what the patch in gnome-menus does, the name
340 // of the file is different here (utf is uppercase)
341 string filename = "desktop.%s.cache".printf (
342 locale.replace (".UTF-8", ".utf8"));
343
344 return Path.build_filename (dir_name, filename, null);
345 }
346
347 private async void process_directory (File directory,
348 string id_prefix,
349 Gee.Set<File> monitored_dirs)
350 {
351 try
352 {
353 string path = directory.get_path ();
354 // we need to skip menu-xdg directory, see lp:686624
355 if (path != null && path.has_suffix ("menu-xdg")) return;
356 // screensavers don't interest us, skip those
357 if (path != null && path.has_suffix ("/screensavers")) return;
358
359 Utils.Logger.debug (this, "Searching for desktop files in: %s", path);
360 bool exists = yield Utils.query_exists_async (directory);
361 if (!exists) return;
362 /* Check if we already scanned this directory // lp:686624 */
363 foreach (var scanned_dir in monitored_dirs)
364 {
365 if (path == scanned_dir.get_path ()) return;
366 }
367 monitored_dirs.add (directory);
368 var enumerator = yield directory.enumerate_children_async (
369 FileAttribute.STANDARD_NAME + "," + FileAttribute.STANDARD_TYPE,
370 0, 0);
371 var files = yield enumerator.next_files_async (1024, 0);
372 foreach (var f in files)
373 {
374 unowned string name = f.get_name ();
375 if (f.get_file_type () == FileType.DIRECTORY)
376 {
377 // FIXME: this could cause too many open files error, or?
378 var subdir = directory.get_child (name);
379 var new_prefix = "%s%s-".printf (id_prefix, subdir.get_basename ());
380 yield process_directory (subdir, new_prefix, monitored_dirs);
381 }
382 else
383 {
384 // ignore ourselves
385 if (name.has_suffix ("synapse.desktop")) continue;
386 if (name.has_suffix (".desktop"))
387 {
388 yield load_desktop_file (directory.get_child (name), id_prefix);
389 }
390 }
391 }
392 }
393 catch (Error err)
394 {
395 warning ("%s", err.message);
396 }
397 }
398
399 private async void load_all_desktop_files ()
400 {
401 string[] data_dirs = Environment.get_system_data_dirs ();
402 data_dirs += Environment.get_user_data_dir ();
403
404 Gee.Set<File> desktop_file_dirs = new Gee.HashSet<File> ();
405
406 mimetype_parent_map.clear ();
407
408 foreach (unowned string data_dir in data_dirs)
409 {
410 string dir_path = Path.build_filename (data_dir, "applications", null);
411 var directory = File.new_for_path (dir_path);
412 yield process_directory (directory, "", desktop_file_dirs);
413 dir_path = Path.build_filename (data_dir, "mime", "subclasses");
414 yield load_mime_parents_from_file (dir_path);
415 }
416
417 create_indices ();
418
419 directory_monitors = new Gee.ArrayList<FileMonitor> ();
420 foreach (File d in desktop_file_dirs)
421 {
422 try
423 {
424 FileMonitor monitor = d.monitor_directory (0, null);
425 monitor.changed.connect (this.desktop_file_directory_changed);
426 directory_monitors.add (monitor);
427 }
428 catch (Error err)
429 {
430 warning ("Unable to monitor directory: %s", err.message);
431 }
432 }
433 }
434
435 private uint timer_id = 0;
436
437 public signal void reload_started ();
438 public signal void reload_done ();
439
440 private void desktop_file_directory_changed ()
441 {
442 reload_started ();
443 if (timer_id != 0)
444 {
445 Source.remove (timer_id);
446 }
447
448 timer_id = Timeout.add (5000, () =>
449 {
450 timer_id = 0;
451 reload_desktop_files.begin ();
452 return false;
453 });
454 }
455
456 private async void reload_desktop_files ()
457 {
458 debug ("Reloading desktop files...");
459 all_desktop_files.clear ();
460 non_hidden_desktop_files.clear ();
461 yield load_all_desktop_files ();
462
463 reload_done ();
464 }
465
466 private async void load_desktop_file (File file, string id_prefix)
467 {
468 try
469 {
470 uint8[] file_contents;
471 bool success = yield file.load_contents_async (null, out file_contents,
472 null);
473 if (success)
474 {
475 var keyfile = new KeyFile ();
476 keyfile.load_from_data ((string) file_contents,
477 file_contents.length, 0);
478
479 var desktop_id = "%s%s".printf (id_prefix, file.get_basename ());
480 var dfi = new DesktopFileInfo.for_keyfile (file.get_path (),
481 keyfile,
482 desktop_id);
483 if (dfi.is_valid)
484 {
485 all_desktop_files.add (dfi);
486 if (!dfi.is_hidden && session_type in dfi.show_in)
487 {
488 non_hidden_desktop_files.add (dfi);
489 }
490 }
491 }
492 }
493 catch (Error err)
494 {
495 warning ("%s", err.message);
496 }
497 }
498
499 private void create_indices ()
500 {
501 // create mimetype maps
502 mimetype_map =
503 new Gee.HashMap<unowned string, Gee.List<DesktopFileInfo> > ();
504 // and exec map
505 exec_map =
506 new Gee.HashMap<string, Gee.List<DesktopFileInfo> > ();
507 // and desktop id map
508 desktop_id_map =
509 new Gee.HashMap<string, DesktopFileInfo> ();
510
511 Regex exec_re;
512 try
513 {
514 exec_re = new Regex ("%[fFuU]");
515 }
516 catch (Error err)
517 {
518 critical ("%s", err.message);
519 return;
520 }
521
522 foreach (var dfi in all_desktop_files)
523 {
524 string exec = "";
525 try
526 {
527 exec = exec_re.replace_literal (dfi.exec, -1, 0, "");
528 }
529 catch (RegexError err)
530 {
531 Utils.Logger.error (this, "%s", err.message);
532 }
533 exec = exec.strip ();
534 // update exec map
535 Gee.List<DesktopFileInfo>? exec_list = exec_map[exec];
536 if (exec_list == null)
537 {
538 exec_list = new Gee.ArrayList<DesktopFileInfo> ();
539 exec_map[exec] = exec_list;
540 }
541 exec_list.add (dfi);
542
543 // update desktop id map
544 var desktop_id = dfi.desktop_id ?? Path.get_basename (dfi.filename);
545 desktop_id_map[desktop_id] = dfi;
546
547 // update mimetype map
548 if (dfi.is_hidden || dfi.mime_types == null) continue;
549
550 foreach (unowned string mime_type in dfi.mime_types)
551 {
552 Gee.List<DesktopFileInfo>? list = mimetype_map[mime_type];
553 if (list == null)
554 {
555 list = new Gee.ArrayList<DesktopFileInfo> ();
556 mimetype_map[mime_type] = list;
557 }
558 list.add (dfi);
559 }
560 }
561 }
562
563 private async void load_mime_parents_from_file (string fi)
564 {
565 var file = File.new_for_path (fi);
566 bool exists = yield Utils.query_exists_async (file);
567 if (!exists) return;
568 try
569 {
570 var fis = yield file.read_async (GLib.Priority.DEFAULT);
571 var dis = new DataInputStream (fis);
572 string line = null;
573 string[] mimes = null;
574 int len = 0;
575 // Read lines until end of file (null) is reached
576 do {
577 line = yield dis.read_line_async (GLib.Priority.DEFAULT);
578 if (line == null) break;
579 if (line.has_prefix ("#")) continue; //comment line
580 mimes = line.split (" ");
581 len = (int)GLib.strv_length (mimes);
582 if (len != 2) continue;
583 // cannot be parent of myself!
584 if (mimes[0] == mimes[1]) continue;
585 //debug ("Map %s -> %s", mimes[0], mimes[1]);
586 mimetype_parent_map.set (mimes[0], mimes[1]);
587 } while (true);
588 } catch (GLib.Error err) { /* can't read file */ }
589 }
590
591 private void add_dfi_for_mime (string mime, Gee.Set<DesktopFileInfo> ret)
592 {
593 var dfis = mimetype_map[mime];
594 if (dfis != null) ret.add_all (dfis);
595
596 var parents = mimetype_parent_map[mime];
597 if (parents == null) return;
598 foreach (string parent in parents)
599 add_dfi_for_mime (parent, ret);
600 }
601
602 // retuns desktop files available on the system (without hidden ones)
603 public Gee.List<DesktopFileInfo> get_desktop_files ()
604 {
605 return non_hidden_desktop_files.read_only_view;
606 }
607
608 // returns all desktop files available on the system (even the ones which
609 // are hidden by default)
610 public Gee.List<DesktopFileInfo> get_all_desktop_files ()
611 {
612 return all_desktop_files.read_only_view;
613 }
614
615 public Gee.List<DesktopFileInfo> get_desktop_files_for_type (string mime_type)
616 {
617 var dfi_set = new Gee.HashSet<DesktopFileInfo> ();
618 add_dfi_for_mime (mime_type, dfi_set);
619 var ret = new Gee.ArrayList<DesktopFileInfo> ();
620 ret.add_all (dfi_set);
621 return ret;
622 }
623
624 public Gee.List<DesktopFileInfo> get_desktop_files_for_exec (string exec)
625 {
626 return exec_map[exec] ?? new Gee.ArrayList<DesktopFileInfo> ();
627 }
628
629 public DesktopFileInfo? get_desktop_file_for_id (string desktop_id)
630 {
631 return desktop_id_map[desktop_id];
632 }
633 }
634}
635
0636
=== added file 'lib/synapse-core/match.vala'
--- lib/synapse-core/match.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/match.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,144 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 * Alberto Aldegheri <albyrock87+dev@gmail.com>
19 */
20
21namespace Synapse
22{
23 public enum MatchType
24 {
25 UNKNOWN = 0,
26 TEXT,
27 APPLICATION,
28 GENERIC_URI,
29 ACTION,
30 SEARCH,
31 CONTACT
32 }
33
34 public interface Match: Object
35 {
36 public enum Score
37 {
38 INCREMENT_MINOR = 2000,
39 INCREMENT_SMALL = 5000,
40 INCREMENT_MEDIUM = 10000,
41 INCREMENT_LARGE = 20000,
42 URI_PENALTY = 15000,
43
44 POOR = 50000,
45 BELOW_AVERAGE = 60000,
46 AVERAGE = 70000,
47 ABOVE_AVERAGE = 75000,
48 GOOD = 80000,
49 VERY_GOOD = 85000,
50 EXCELLENT = 90000,
51
52 HIGHEST = 100000
53 }
54
55 // properties
56 public abstract string title { get; construct set; }
57 public abstract string description { get; set; }
58 public abstract string icon_name { get; construct set; }
59 public abstract bool has_thumbnail { get; construct set; }
60 public abstract string thumbnail_path { get; construct set; }
61 public abstract MatchType match_type { get; construct set; }
62
63 public virtual void execute (Match? match)
64 {
65 Utils.Logger.error (this, "execute () is not implemented");
66 }
67
68 public virtual void execute_with_target (Match? source, Match? target = null)
69 {
70 if (target == null) execute (source);
71 else Utils.Logger.error (this, "execute () is not implemented");
72 }
73
74 public virtual bool needs_target () {
75 return false;
76 }
77
78 public virtual QueryFlags target_flags ()
79 {
80 return QueryFlags.ALL;
81 }
82
83 public signal void executed ();
84 }
85
86 public interface ApplicationMatch: Match
87 {
88 public abstract AppInfo? app_info { get; set; }
89 public abstract bool needs_terminal { get; set; }
90 public abstract string? filename { get; construct set; }
91 }
92
93 public interface UriMatch: Match
94 {
95 public abstract string uri { get; set; }
96 public abstract QueryFlags file_type { get; set; }
97 public abstract string mime_type { get; set; }
98 }
99
100 public interface ContactMatch: Match
101 {
102 public abstract void send_message (string message, bool present);
103 public abstract void open_chat ();
104 }
105
106 public interface ExtendedInfo: Match
107 {
108 public abstract string? extended_info { get; set; }
109 }
110
111 public enum TextOrigin
112 {
113 UNKNOWN,
114 CLIPBOARD
115 }
116
117 public interface TextMatch: Match
118 {
119 public abstract TextOrigin text_origin { get; set; }
120 public abstract string get_text ();
121 }
122
123 public interface SearchMatch: Match, SearchProvider
124 {
125 public abstract Match search_source { get; set; }
126 }
127
128 public class DefaultMatch: Object, Match
129 {
130 public string title { get; construct set; }
131 public string description { get; set; }
132 public string icon_name { get; construct set; }
133 public bool has_thumbnail { get; construct set; }
134 public string thumbnail_path { get; construct set; }
135 public MatchType match_type { get; construct set; }
136
137 public DefaultMatch (string query_string)
138 {
139 Object (title: query_string, description: "", has_thumbnail: false,
140 icon_name: "unknown", match_type: MatchType.UNKNOWN);
141 }
142 }
143}
144
0145
=== added file 'lib/synapse-core/plugin.vala'
--- lib/synapse-core/plugin.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/plugin.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,59 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public interface Activatable : Object
24 {
25 // this property will eventually go away
26 public abstract bool enabled { get; set; default = true; }
27
28 public abstract void activate ();
29 public abstract void deactivate ();
30 }
31
32 public interface Configurable : Object
33 {
34 public abstract Gtk.Widget create_config_widget ();
35 }
36
37 public interface ItemProvider : Activatable
38 {
39 public abstract async ResultSet? search (Query query) throws SearchError;
40 public virtual bool handles_query (Query query)
41 {
42 return true;
43 }
44 public virtual bool handles_empty_query ()
45 {
46 return false;
47 }
48 }
49
50 public interface ActionProvider : Activatable
51 {
52 public abstract ResultSet? find_for_match (ref Query query, Match match);
53 public virtual bool handles_unknown ()
54 {
55 return false;
56 }
57 }
58}
59
060
=== added file 'lib/synapse-core/query.vala'
--- lib/synapse-core/query.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/query.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,296 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 [Flags]
24 public enum QueryFlags
25 {
26 /* HowTo create categories (32bit).
27 * Authored by Alberto Aldegheri <albyrock87+dev@gmail.com>
28 * Categories are "stored" in 3 Levels:
29 * Super-Category
30 * -> Category
31 * ----> Sub-Category
32 * ------------------------------------
33 * if (Super-Category does NOT have childs):
34 * SUPER = 1 << FreeBitPosition
35 * else:
36 * if (Category does NOT have childs)
37 * CATEGORY = 1 << FreeBitPosition
38 * else
39 * SUB = 1 << FreeBitPosition
40 * CATEGORY = OR ([subcategories, ...]);
41 *
42 * SUPER = OR ([categories, ...]);
43 *
44 *
45 * Remember:
46 * if you add or remove a category,
47 * change labels in UIInterface.CategoryConfig.init_labels
48 *
49 */
50 INCLUDE_REMOTE = 1 << 0,
51 UNCATEGORIZED = 1 << 1,
52
53 APPLICATIONS = 1 << 2,
54
55 ACTIONS = 1 << 3,
56
57 AUDIO = 1 << 4,
58 VIDEO = 1 << 5,
59 DOCUMENTS = 1 << 6,
60 IMAGES = 1 << 7,
61 FILES = AUDIO | VIDEO | DOCUMENTS | IMAGES,
62
63 PLACES = 1 << 8,
64
65 // FIXME: shouldn't this be FILES | INCLUDE_REMOTE?
66 INTERNET = 1 << 9,
67
68 // FIXME: Text Query flag? kinda weird, why do we have this here?
69 TEXT = 1 << 10,
70
71 CONTACTS = 1 << 11,
72
73 ALL = 0xFFFFFFFF,
74 LOCAL_CONTENT = ALL ^ QueryFlags.INCLUDE_REMOTE
75 }
76
77 [Flags]
78 public enum MatcherFlags
79 {
80 NO_REVERSED = 1 << 0,
81 NO_SUBSTRING = 1 << 1,
82 NO_PARTIAL = 1 << 2,
83 NO_FUZZY = 1 << 3
84 }
85
86 public struct Query
87 {
88 string query_string;
89 string query_string_folded;
90 Cancellable cancellable;
91 QueryFlags query_type;
92 uint max_results;
93 uint query_id;
94
95 public Query (uint query_id,
96 string query,
97 QueryFlags flags = QueryFlags.LOCAL_CONTENT,
98 uint num_results = 96)
99 {
100 this.query_id = query_id;
101 this.query_string = query;
102 this.query_string_folded = query.casefold ();
103 this.query_type = flags;
104 this.max_results = num_results;
105 }
106
107 public bool is_cancelled ()
108 {
109 return cancellable.is_cancelled ();
110 }
111
112 public void check_cancellable () throws SearchError
113 {
114 if (cancellable.is_cancelled ())
115 {
116 throw new SearchError.SEARCH_CANCELLED ("Cancelled");
117 }
118 }
119
120 public static Gee.List<Gee.Map.Entry<Regex, int>>
121 get_matchers_for_query (string query,
122 MatcherFlags match_flags = 0,
123 RegexCompileFlags flags = GLib.RegexCompileFlags.OPTIMIZE)
124 {
125 /* create a couple of regexes and try to help with matching
126 * match with these regular expressions (with descending score):
127 * 1) ^query$
128 * 2) ^query
129 * 3) \bquery
130 * 4) split to words and seach \bword1.+\bword2 (if there are 2+ words)
131 * 5) query
132 * 6) split to characters and search \bq.+\bu.+\be.+\br.+\by
133 * 7) split to characters and search \bq.*u.*e.*r.*y
134 *
135 * The set of returned regular expressions depends on MatcherFlags.
136 */
137
138 var results = new Gee.HashMap<Regex, int> ();
139 Regex re;
140
141 try
142 {
143 re = new Regex ("^(%s)$".printf (Regex.escape_string (query)), flags);
144 results[re] = Match.Score.HIGHEST;
145 }
146 catch (RegexError err)
147 {
148 }
149
150 try
151 {
152 re = new Regex ("^(%s)".printf (Regex.escape_string (query)), flags);
153 results[re] = Match.Score.EXCELLENT;
154 }
155 catch (RegexError err)
156 {
157 }
158
159 try
160 {
161 re = new Regex ("\\b(%s)".printf (Regex.escape_string (query)), flags);
162 results[re] = Match.Score.VERY_GOOD;
163 }
164 catch (RegexError err)
165 {
166 }
167
168 // split to individual chars
169 string[] individual_words = Regex.split_simple ("\\s+", query.strip ());
170 if (individual_words.length >= 2)
171 {
172 string[] escaped_words = {};
173 foreach (unowned string word in individual_words)
174 {
175 escaped_words += Regex.escape_string (word);
176 }
177 string pattern = "\\b(%s)".printf (string.joinv (").+\\b(",
178 escaped_words));
179
180 try
181 {
182 re = new Regex (pattern, flags);
183 results[re] = Match.Score.GOOD;
184 }
185 catch (RegexError err)
186 {
187 }
188
189 // FIXME: do something generic here
190 if (!(MatcherFlags.NO_REVERSED in match_flags))
191 {
192 if (escaped_words.length == 2)
193 {
194 var reversed = "\\b(%s)".printf (string.join (").+\\b(",
195 escaped_words[1],
196 escaped_words[0],
197 null));
198 try
199 {
200 re = new Regex (reversed, flags);
201 results[re] = Match.Score.GOOD - Match.Score.INCREMENT_MINOR;
202 }
203 catch (RegexError err)
204 {
205 }
206 }
207 else
208 {
209 // not too nice, but is quite fast to compute
210 var orred = "\\b((?:%s))".printf (string.joinv (")|(?:", escaped_words));
211 var any_order = "";
212 for (int i=0; i<escaped_words.length; i++)
213 {
214 bool is_last = i == escaped_words.length - 1;
215 any_order += orred;
216 if (!is_last) any_order += ".+";
217 }
218 try
219 {
220 re = new Regex (any_order, flags);
221 results[re] = Match.Score.AVERAGE + Match.Score.INCREMENT_MINOR;
222 }
223 catch (RegexError err)
224 {
225 }
226 }
227 }
228 }
229
230 if (!(MatcherFlags.NO_SUBSTRING in match_flags))
231 {
232 try
233 {
234 re = new Regex ("(%s)".printf (Regex.escape_string (query)), flags);
235 results[re] = Match.Score.BELOW_AVERAGE;
236 }
237 catch (RegexError err)
238 {
239 }
240 }
241
242 // split to individual characters
243 string[] individual_chars = Regex.split_simple ("\\s*", query);
244 string[] escaped_chars = {};
245 foreach (unowned string word in individual_chars)
246 {
247 escaped_chars += Regex.escape_string (word);
248 }
249
250 // make "aj" match "Activity Journal"
251 if (!(MatcherFlags.NO_PARTIAL in match_flags) &&
252 individual_words.length == 1 && individual_chars.length <= 5)
253 {
254 string pattern = "\\b(%s)".printf (string.joinv (").+\\b(",
255 escaped_chars));
256 try
257 {
258 re = new Regex (pattern, flags);
259 results[re] = Match.Score.ABOVE_AVERAGE;
260 }
261 catch (RegexError err)
262 {
263 }
264 }
265
266 if (!(MatcherFlags.NO_FUZZY in match_flags) && escaped_chars.length > 0)
267 {
268 string pattern = "\\b(%s)".printf (string.joinv (").*(",
269 escaped_chars));
270 try
271 {
272 re = new Regex (pattern, flags);
273 results[re] = Match.Score.POOR;
274 }
275 catch (RegexError err)
276 {
277 }
278 }
279
280 var sorted_results = new Gee.ArrayList<Gee.Map.Entry<Regex, int>> ();
281 var entries = results.entries;
282 // FIXME: why it doesn't work without this?
283 sorted_results.set_data ("entries-ref", entries);
284 sorted_results.add_all (entries);
285 sorted_results.sort ((a, b) =>
286 {
287 unowned Gee.Map.Entry<Regex, int> e1 = (Gee.Map.Entry<Regex, int>) a;
288 unowned Gee.Map.Entry<Regex, int> e2 = (Gee.Map.Entry<Regex, int>) b;
289 return e2.value - e1.value;
290 });
291
292 return sorted_results;
293 }
294 }
295}
296
0297
=== added file 'lib/synapse-core/relevancy-backend-zg.vala'
--- lib/synapse-core/relevancy-backend-zg.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/relevancy-backend-zg.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,308 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 private class ZeitgeistRelevancyBackend: Object, RelevancyBackend
24 {
25 private Zeitgeist.Log zg_log;
26 private Zeitgeist.DataSourceRegistry zg_dsr;
27 private Gee.Map<string, int> application_popularity;
28 private Gee.Map<string, int> uri_popularity;
29 private bool has_datahub_gio_module = false;
30
31 private const float MULTIPLIER = 65535.0f;
32
33 construct
34 {
35 zg_log = new Zeitgeist.Log ();
36 application_popularity = new Gee.HashMap<string, int> ();
37 uri_popularity = new Gee.HashMap<string, int> ();
38
39 refresh_popularity ();
40 check_data_sources.begin ();
41
42 Timeout.add_seconds (60*30, refresh_popularity);
43 }
44
45 private async void check_data_sources ()
46 {
47 zg_dsr = new Zeitgeist.DataSourceRegistry ();
48 try
49 {
50 var ptr_arr = yield zg_dsr.get_data_sources (null);
51
52 for (uint i=0; i < ptr_arr.len; i++)
53 {
54 unowned Zeitgeist.DataSource ds;
55 ds = (Zeitgeist.DataSource) ptr_arr.index (i);
56 if (ds.get_unique_id () == "com.zeitgeist-project,datahub,gio-launch-listener"
57 && ds.is_enabled ())
58 {
59 has_datahub_gio_module = true;
60 break;
61 }
62 }
63 }
64 catch (Error err)
65 {
66 warning ("Unable to check Zeitgeist data sources: %s", err.message);
67 }
68 }
69
70 private bool refresh_popularity ()
71 {
72 load_application_relevancies.begin ();
73 load_uri_relevancies.begin ();
74 return true;
75 }
76
77 private async void load_application_relevancies ()
78 {
79 Idle.add (load_application_relevancies.callback, Priority.LOW);
80 yield;
81
82 int64 end = Zeitgeist.Timestamp.now ();
83 int64 start = end - Zeitgeist.Timestamp.WEEK * 4;
84 Zeitgeist.TimeRange tr = new Zeitgeist.TimeRange (start, end);
85
86 var event = new Zeitgeist.Event ();
87 event.set_interpretation ("!" + Zeitgeist.ZG_LEAVE_EVENT);
88 var subject = new Zeitgeist.Subject ();
89 subject.set_interpretation (Zeitgeist.NFO_SOFTWARE);
90 subject.set_uri ("application://*");
91 event.add_subject (subject);
92
93 var ptr_arr = new PtrArray ();
94 ptr_arr.add (event);
95
96 Zeitgeist.ResultSet rs;
97
98 try
99 {
100 rs = yield zg_log.find_events (tr, (owned) ptr_arr,
101 Zeitgeist.StorageState.ANY,
102 256,
103 Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS,
104 null);
105
106 application_popularity.clear ();
107 uint size = rs.size ();
108 uint index = 0;
109
110 // Zeitgeist (0.6) doesn't have any stats API, so let's approximate
111
112 foreach (Zeitgeist.Event e in rs)
113 {
114 if (e.num_subjects () <= 0) continue;
115 Zeitgeist.Subject s = e.get_subject (0);
116
117 float power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0>
118 float relevancy = 1.0f / Math.powf (index + 1, power);
119 application_popularity[s.get_uri ()] = (int)(relevancy * MULTIPLIER);
120
121 index++;
122 }
123 }
124 catch (Error err)
125 {
126 warning ("%s", err.message);
127 return;
128 }
129 }
130
131 private async void load_uri_relevancies ()
132 {
133 Idle.add (load_uri_relevancies.callback, Priority.LOW);
134 yield;
135
136 int64 end = Zeitgeist.Timestamp.now ();
137 int64 start = end - Zeitgeist.Timestamp.WEEK * 4;
138 Zeitgeist.TimeRange tr = new Zeitgeist.TimeRange (start, end);
139
140 var event = new Zeitgeist.Event ();
141 event.set_interpretation ("!" + Zeitgeist.ZG_LEAVE_EVENT);
142 var subject = new Zeitgeist.Subject ();
143 subject.set_interpretation ("!" + Zeitgeist.NFO_SOFTWARE);
144 subject.set_uri ("file://*");
145 event.add_subject (subject);
146
147 var ptr_arr = new PtrArray ();
148 ptr_arr.add (event);
149
150 Zeitgeist.ResultSet rs;
151 Gee.Map<string, int> popularity_map = new Gee.HashMap<string, int> ();
152
153 try
154 {
155 uint size, index;
156 float power, relevancy;
157 /* Get popularity for file uris */
158 rs = yield zg_log.find_events (tr, (owned) ptr_arr,
159 Zeitgeist.StorageState.ANY,
160 256,
161 Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS,
162 null);
163
164 size = rs.size ();
165 index = 0;
166
167 // Zeitgeist (0.6) doesn't have any stats API, so let's approximate
168
169 foreach (Zeitgeist.Event e1 in rs)
170 {
171 if (e1.num_subjects () <= 0) continue;
172 Zeitgeist.Subject s1 = e1.get_subject (0);
173
174 power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0>
175 relevancy = 1.0f / Math.powf (index + 1, power);
176 popularity_map[s1.get_uri ()] = (int)(relevancy * MULTIPLIER);
177
178 index++;
179 }
180
181 /* Get popularity for web uris */
182 subject.set_interpretation (Zeitgeist.NFO_WEBSITE);
183 subject.set_uri ("");
184 ptr_arr = new PtrArray ();
185 ptr_arr.add (event);
186
187 rs = yield zg_log.find_events (tr, (owned) ptr_arr,
188 Zeitgeist.StorageState.ANY,
189 128,
190 Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS,
191 null);
192
193 size = rs.size ();
194 index = 0;
195
196 // Zeitgeist (0.6) doesn't have any stats API, so let's approximate
197
198 foreach (Zeitgeist.Event e2 in rs)
199 {
200 if (e2.num_subjects () <= 0) continue;
201 Zeitgeist.Subject s2 = e2.get_subject (0);
202
203 power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0>
204 relevancy = 1.0f / Math.powf (index + 1, power);
205 popularity_map[s2.get_uri ()] = (int)(relevancy * MULTIPLIER);
206
207 index++;
208 }
209 }
210 catch (Error err)
211 {
212 warning ("%s", err.message);
213 }
214
215 uri_popularity = popularity_map;
216 }
217
218 public float get_application_popularity (string desktop_id)
219 {
220 if (application_popularity.has_key (desktop_id))
221 {
222 return application_popularity[desktop_id] / MULTIPLIER;
223 }
224
225 return 0.0f;
226 }
227
228 public float get_uri_popularity (string uri)
229 {
230 if (uri_popularity.has_key (uri))
231 {
232 return uri_popularity[uri] / MULTIPLIER;
233 }
234
235 return 0.0f;
236 }
237
238 private void reload_relevancies ()
239 {
240 Idle.add_full (Priority.LOW, () =>
241 {
242 load_application_relevancies.begin ();
243 return false;
244 });
245 }
246
247 public void application_launched (AppInfo app_info)
248 {
249 // FIXME: get rid of this maverick-specific workaround
250 // detect if the Zeitgeist GIO module is installed
251 Type zg_gio_module = Type.from_name ("GAppLaunchHandlerZeitgeist");
252 // FIXME: perhaps we should check app_info.should_show?
253 // but user specifically asked to open this, so probably not
254 // otoh the gio module won't pick it up if it's not should_show
255 if (zg_gio_module != 0)
256 {
257 Utils.Logger.debug (this, "libzg-gio-module detected, not pushing");
258 reload_relevancies ();
259 return;
260 }
261
262 if (has_datahub_gio_module)
263 {
264 reload_relevancies ();
265 return;
266 }
267
268 string app_uri = null;
269 if (app_info.get_id () != null)
270 {
271 app_uri = "application://" + app_info.get_id ();
272 }
273 else if (app_info is DesktopAppInfo)
274 {
275 string? filename = (app_info as DesktopAppInfo).get_filename ();
276 if (filename == null) return;
277 app_uri = "application://" + Path.get_basename (filename);
278 }
279
280 Utils.Logger.debug (this, "launched \"%s\", pushing to ZG", app_uri);
281 push_app_launch (app_uri, app_info.get_display_name ());
282
283 // and refresh
284 reload_relevancies ();
285 }
286
287 private void push_app_launch (string app_uri, string? display_name)
288 {
289 //debug ("pushing launch event: %s [%s]", app_uri, display_name);
290 var event = new Zeitgeist.Event ();
291 var subject = new Zeitgeist.Subject ();
292
293 event.set_actor ("application://synapse.desktop");
294 event.set_interpretation (Zeitgeist.ZG_ACCESS_EVENT);
295 event.set_manifestation (Zeitgeist.ZG_USER_ACTIVITY);
296 event.add_subject (subject);
297
298 subject.set_uri (app_uri);
299 subject.set_interpretation (Zeitgeist.NFO_SOFTWARE);
300 subject.set_manifestation (Zeitgeist.NFO_SOFTWARE_ITEM);
301 subject.set_mimetype ("application/x-desktop");
302 subject.set_text (display_name);
303
304 zg_log.insert_events_no_reply (event, null);
305 }
306 }
307}
308
0309
=== added file 'lib/synapse-core/relevancy-service.vala'
--- lib/synapse-core/relevancy-service.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/relevancy-service.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,95 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public interface RelevancyBackend: Object
24 {
25 public abstract float get_application_popularity (string desktop_id);
26 public abstract float get_uri_popularity (string uri);
27
28 public abstract void application_launched (AppInfo app_info);
29 }
30
31 public class RelevancyService : GLib.Object
32 {
33 // singleton that can be easily destroyed
34 private static unowned RelevancyService? instance;
35 public static RelevancyService get_default ()
36 {
37 return instance ?? new RelevancyService ();
38 }
39
40 private RelevancyService ()
41 {
42 }
43
44 ~RelevancyService ()
45 {
46 }
47
48 construct
49 {
50 instance = this;
51 this.add_weak_pointer (&instance);
52
53 initialize_relevancy_backend ();
54 }
55
56 private RelevancyBackend backend;
57
58 private void initialize_relevancy_backend ()
59 {
60 backend = new ZeitgeistRelevancyBackend ();
61 }
62
63 public float get_application_popularity (string desktop_id)
64 {
65 if (backend == null) return 0.0f;
66 return backend.get_application_popularity (desktop_id);
67 }
68
69 public float get_uri_popularity (string uri)
70 {
71 if (backend == null) return 0.0f;
72 return backend.get_uri_popularity (uri);
73 }
74
75 public void application_launched (AppInfo app_info)
76 {
77 Utils.Logger.debug (this, "application launched");
78 if (backend == null) return;
79 backend.application_launched (app_info);
80 }
81
82 public static int compute_relevancy (int base_relevancy, float modifier)
83 {
84 // FIXME: let's experiment here
85 // the other idea is to use base_relevancy * (1.0f + modifier)
86 int relevancy = (int) (base_relevancy + modifier * Match.Score.INCREMENT_LARGE * 2);
87 //int relevancy = base_relevancy + (int) (modifier * Match.Score.HIGHEST);
88 return relevancy;
89 // FIXME: this clamping should be done, but it screws up the popularity
90 // for very popular items with high match score
91 //return int.min (relevancy, Match.Score.HIGHEST);
92 }
93 }
94}
95
096
=== added file 'lib/synapse-core/result-set.vala'
--- lib/synapse-core/result-set.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/result-set.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,121 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public class ResultSet : Object, Gee.Traversable<Match>, Gee.Iterable <Gee.Map.Entry <Match, int>>
24 {
25 protected Gee.Map<Match, int> matches;
26 protected Gee.Set<unowned string> uris;
27
28 public ResultSet ()
29 {
30 Object ();
31 }
32
33 construct
34 {
35 matches = new Gee.HashMap<Match, int> ();
36 // Match.uri is not owned, so we can optimize here
37 uris = new Gee.HashSet<unowned string> ();
38 }
39
40 public Type element_type
41 {
42 get { return matches.element_type; }
43 }
44
45 public int size
46 {
47 get { return matches.size; }
48 }
49
50 public Gee.Set<Match> keys
51 {
52 owned get { return matches.keys; }
53 }
54
55 public Gee.Set<Gee.Map.Entry <Match, int>> entries
56 {
57 owned get { return matches.entries; }
58 }
59
60 public Gee.Iterator<Gee.Map.Entry <Match, int>?> iterator ()
61 {
62 return matches.iterator ();
63 }
64
65 public bool foreach (Gee.ForallFunc<Match> func)
66 {
67 return matches.keys.foreach (func);
68 }
69
70 public void add (Match match, int relevancy)
71 {
72 matches.set (match, relevancy);
73
74 if (match is UriMatch)
75 {
76 unowned string uri = (match as UriMatch).uri;
77 if (uri != null && uri != "")
78 {
79 uris.add (uri);
80 }
81 }
82 }
83
84 public void add_all (ResultSet? rs)
85 {
86 if (rs == null) return;
87 matches.set_all (rs.matches);
88 uris.add_all (rs.uris);
89 }
90
91 public bool contains_uri (string uri)
92 {
93 return uri in uris;
94 }
95
96 public Gee.List<Match> get_sorted_list ()
97 {
98 var l = new Gee.ArrayList<Gee.Map.Entry<Match, int>> ();
99 l.add_all (matches.entries);
100
101 l.sort ((a, b) =>
102 {
103 unowned Gee.Map.Entry<Match, int> e1 = (Gee.Map.Entry<Match, int>) a;
104 unowned Gee.Map.Entry<Match, int> e2 = (Gee.Map.Entry<Match, int>) b;
105 int relevancy_delta = e2.value - e1.value;
106 if (relevancy_delta != 0) return relevancy_delta;
107 // FIXME: utf8 compare!
108 else return e1.key.title.ascii_casecmp (e2.key.title);
109 });
110
111 var sorted_list = new Gee.ArrayList<Match> ();
112 foreach (Gee.Map.Entry<Match, int> m in l)
113 {
114 sorted_list.add (m.key);
115 }
116
117 return sorted_list;
118 }
119 }
120}
121
0122
=== added file 'lib/synapse-core/utils.vala'
--- lib/synapse-core/utils.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/utils.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,439 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 [CCode (gir_namespace = "SynapseUtils", gir_version = "1.0")]
24 namespace Utils
25 {
26 /* Make sure setlocale was called before calling this function
27 * (Gtk.init calls it automatically)
28 */
29 public static string? remove_accents (string input)
30 {
31 string? result;
32 unowned string charset;
33 GLib.get_charset (out charset);
34 try
35 {
36 result = GLib.convert (input, input.length,
37 "US-ASCII//TRANSLIT", charset);
38 // no need to waste cpu cycles if the input is the same
39 if (input == result) return null;
40 }
41 catch (ConvertError err)
42 {
43 result = null;
44 }
45
46 return result;
47 }
48
49 public static string? remove_last_unichar (string input)
50 {
51 long char_count = input.char_count ();
52
53 int len = input.index_of_nth_char (char_count - 1);
54 return input.substring (0, len);
55 }
56
57 public static async bool query_exists_async (GLib.File f)
58 {
59 bool exists;
60 try
61 {
62 yield f.query_info_async (FileAttribute.STANDARD_TYPE, 0, 0, null);
63 exists = true;
64 }
65 catch (Error err)
66 {
67 exists = false;
68 }
69
70 return exists;
71 }
72
73 public string extract_type_name (Type obj_type)
74 {
75 string obj_class = obj_type.name ();
76 if (obj_class.has_prefix ("Synapse")) return obj_class.substring (7);
77
78 return obj_class;
79 }
80
81 public class Logger
82 {
83 protected const string RED = "\x1b[31m";
84 protected const string GREEN = "\x1b[32m";
85 protected const string YELLOW = "\x1b[33m";
86 protected const string BLUE = "\x1b[34m";
87 protected const string MAGENTA = "\x1b[35m";
88 protected const string CYAN = "\x1b[36m";
89 protected const string RESET = "\x1b[0m";
90
91 private static bool initialized = false;
92 private static bool show_debug = false;
93
94 private static void log_internal (Object? obj, LogLevelFlags level, string format, va_list args)
95 {
96 if (!initialized) initialize ();
97 string desc = "";
98 if (obj != null)
99 {
100 string obj_class = extract_type_name (obj.get_type ());
101 desc = "%s[%s]%s ".printf (MAGENTA, obj_class, RESET);
102 }
103 logv ("Synapse", level, desc + format, args);
104 }
105
106 private static void initialize ()
107 {
108 var levels = LogLevelFlags.LEVEL_DEBUG | LogLevelFlags.LEVEL_INFO |
109 LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_CRITICAL |
110 LogLevelFlags.LEVEL_ERROR;
111
112 string[] domains =
113 {
114 "Synapse",
115 "Gtk",
116 "Gdk",
117 "GLib",
118 "GLib-GObject",
119 "Pango",
120 "GdkPixbuf",
121 "GLib-GIO",
122 "GtkHotkey"
123 };
124 foreach (unowned string domain in domains)
125 {
126 Log.set_handler (domain, levels, handler);
127 }
128 Log.set_handler (null, levels, handler);
129
130 show_debug = Environment.get_variable ("SYNAPSE_DEBUG") != null;
131 initialized = true;
132 }
133
134 public static bool debug_enabled ()
135 {
136 if (!initialized) initialize ();
137 return show_debug;
138 }
139
140 public static void log (Object? obj, string format, ...)
141 {
142 var args = va_list ();
143 log_internal (obj, LogLevelFlags.LEVEL_INFO, format, args);
144 }
145
146 [Diagnostics]
147 public static void debug (Object? obj, string format, ...)
148 {
149 var args = va_list ();
150 log_internal (obj, LogLevelFlags.LEVEL_DEBUG, format, args);
151 }
152
153 public static void warning (Object? obj, string format, ...)
154 {
155 var args = va_list ();
156 log_internal (obj, LogLevelFlags.LEVEL_WARNING, format, args);
157 }
158
159 public static void error (Object? obj, string format, ...)
160 {
161 var args = va_list ();
162 log_internal (obj, LogLevelFlags.LEVEL_ERROR, format, args);
163 }
164
165 protected static void handler (string? domain, LogLevelFlags level, string msg)
166 {
167 string header;
168 string domain_str = "";
169 if (domain != null && domain != "Synapse") domain_str = domain + "-";
170 var time_val = TimeVal ();
171 long time_str_len = time_val.tv_usec != 0 ? 15 : 8;
172 string cur_time = time_val.to_iso8601 ().substring (11, time_str_len);
173 if (level == LogLevelFlags.LEVEL_DEBUG)
174 {
175 if (!show_debug && domain_str == "") return;
176 header = @"$(GREEN)[$(cur_time) $(domain_str)Debug]$(RESET)";
177 }
178 else if (level == LogLevelFlags.LEVEL_INFO)
179 {
180 header = @"$(BLUE)[$(cur_time) $(domain_str)Info]$(RESET)";
181 }
182 else if (level == LogLevelFlags.LEVEL_WARNING)
183 {
184 header = @"$(RED)[$(cur_time) $(domain_str)Warning]$(RESET)";
185 }
186 else if (level == LogLevelFlags.LEVEL_CRITICAL || level == LogLevelFlags.LEVEL_ERROR)
187 {
188 header = @"$(RED)[$(cur_time) $(domain_str)Critical]$(RESET)";
189 }
190 else
191 {
192 header = @"$(YELLOW)[$(cur_time)]$(RESET)";
193 }
194
195 stdout.printf ("%s %s\n", header, msg);
196#if 0
197 void* buffer[10];
198 int num = Linux.backtrace (&buffer, 10);
199 string[] symbols = Linux.backtrace_symbols (buffer, num);
200 if (symbols != null)
201 {
202 for (int i = 0; i < num; i++) stdout.printf ("%s\n", symbols[i]);
203 }
204#endif
205 }
206 }
207
208 [Compact]
209 private class DelegateWrapper
210 {
211 public SourceFunc callback;
212
213 public DelegateWrapper (owned SourceFunc cb)
214 {
215 callback = (owned) cb;
216 }
217 }
218 /*
219 * Asynchronous Once.
220 *
221 * Usage:
222 * private AsyncOnce<string> once = new AsyncOnce<string> ();
223 * public async void foo ()
224 * {
225 * if (!once.is_initialized ()) // not stricly necessary but improves perf
226 * {
227 * if (yield once.enter ())
228 * {
229 * // this block will be executed only once, but the method
230 * // is reentrant; it's also recommended to wrap this block
231 * // in try { } and call once.leave() in finally { }
232 * // if any of the operations can throw an error
233 * var s = yield get_the_string ();
234 * once.leave (s);
235 * }
236 * }
237 * // if control reaches this point the once was initialized
238 * yield do_something_for_string (once.get_data ());
239 * }
240 */
241 public class AsyncOnce<G>
242 {
243 private enum OperationState
244 {
245 NOT_STARTED,
246 IN_PROGRESS,
247 DONE
248 }
249
250 private G inner;
251
252 private OperationState state;
253 private DelegateWrapper[] callbacks = {};
254
255 public AsyncOnce ()
256 {
257 state = OperationState.NOT_STARTED;
258 }
259
260 public unowned G get_data ()
261 {
262 return inner;
263 }
264
265 public bool is_initialized ()
266 {
267 return state == OperationState.DONE;
268 }
269
270 public async bool enter ()
271 {
272 if (state == OperationState.NOT_STARTED)
273 {
274 state = OperationState.IN_PROGRESS;
275 return true;
276 }
277 else if (state == OperationState.IN_PROGRESS)
278 {
279 yield wait_async ();
280 }
281
282 return false;
283 }
284
285 public void leave (G result)
286 {
287 if (state != OperationState.IN_PROGRESS)
288 {
289 warning ("Incorrect usage of AsyncOnce");
290 return;
291 }
292 state = OperationState.DONE;
293 inner = result;
294 notify_all ();
295 }
296
297 /* Once probably shouldn't have this, but it's useful */
298 public void reset ()
299 {
300 if (state == OperationState.IN_PROGRESS)
301 {
302 warning ("AsyncOnce.reset() cannot be called in the middle of initialization.");
303 }
304 else
305 {
306 state = OperationState.NOT_STARTED;
307 inner = null;
308 }
309 }
310
311 private void notify_all ()
312 {
313 foreach (unowned DelegateWrapper wrapper in callbacks)
314 {
315 wrapper.callback ();
316 }
317 callbacks = {};
318 }
319
320 private async void wait_async ()
321 {
322 callbacks += new DelegateWrapper (wait_async.callback);
323 yield;
324 }
325 }
326
327 public class FileInfo
328 {
329 private static string interesting_attributes;
330 static construct
331 {
332 interesting_attributes =
333 string.join (",", FileAttribute.STANDARD_TYPE,
334 FileAttribute.STANDARD_IS_HIDDEN,
335 FileAttribute.STANDARD_IS_BACKUP,
336 FileAttribute.STANDARD_DISPLAY_NAME,
337 FileAttribute.STANDARD_ICON,
338 FileAttribute.STANDARD_FAST_CONTENT_TYPE,
339 FileAttribute.THUMBNAIL_PATH,
340 null);
341 }
342
343 public string uri;
344 public string parse_name;
345 public QueryFlags file_type;
346 public UriMatch? match_obj;
347 private bool initialized;
348 private Type match_obj_type;
349
350 public FileInfo (string uri, Type obj_type)
351 {
352 assert (obj_type.is_a (typeof (UriMatch)));
353 this.uri = uri;
354 this.match_obj = null;
355 this.match_obj_type = obj_type;
356 this.initialized = false;
357 this.file_type = QueryFlags.UNCATEGORIZED;
358
359 var f = File.new_for_uri (uri);
360 this.parse_name = f.get_parse_name ();
361 }
362
363 public bool is_initialized ()
364 {
365 return this.initialized;
366 }
367
368 public async void initialize ()
369 {
370 initialized = true;
371 var f = File.new_for_uri (uri);
372 try
373 {
374 var fi = yield f.query_info_async (interesting_attributes,
375 0, 0, null);
376 if (fi.get_file_type () == FileType.REGULAR &&
377 !fi.get_is_hidden () &&
378 !fi.get_is_backup ())
379 {
380 match_obj = (UriMatch) Object.new (match_obj_type,
381 "thumbnail-path", fi.get_attribute_byte_string (FileAttribute.THUMBNAIL_PATH),
382 "icon-name", fi.get_icon ().to_string (),
383 "uri", uri,
384 "title", fi.get_display_name (),
385 "description", f.get_parse_name (),
386 "match-type", MatchType.GENERIC_URI,
387 null
388 );
389
390 // let's determine the file type
391 unowned string mime_type =
392 fi.get_attribute_string (FileAttribute.STANDARD_FAST_CONTENT_TYPE);
393 if (ContentType.is_unknown (mime_type))
394 {
395 file_type = QueryFlags.UNCATEGORIZED;
396 }
397 else if (ContentType.is_a (mime_type, "audio/*"))
398 {
399 file_type = QueryFlags.AUDIO;
400 }
401 else if (ContentType.is_a (mime_type, "video/*"))
402 {
403 file_type = QueryFlags.VIDEO;
404 }
405 else if (ContentType.is_a (mime_type, "image/*"))
406 {
407 file_type = QueryFlags.IMAGES;
408 }
409 else if (ContentType.is_a (mime_type, "text/*"))
410 {
411 file_type = QueryFlags.DOCUMENTS;
412 }
413 // FIXME: this isn't right
414 else if (ContentType.is_a (mime_type, "application/*"))
415 {
416 file_type = QueryFlags.DOCUMENTS;
417 }
418
419 match_obj.file_type = file_type;
420 match_obj.mime_type = mime_type;
421 }
422 }
423 catch (Error err)
424 {
425 warning ("%s", err.message);
426 }
427 }
428
429 public async bool exists ()
430 {
431 var f = File.new_for_uri (uri);
432 bool result = yield query_exists_async (f);
433
434 return result;
435 }
436 }
437 }
438}
439
0440
=== added file 'lib/synapse-core/volume-service.vala'
--- lib/synapse-core/volume-service.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-core/volume-service.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,193 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authored by Michal Hruby <michal.mhr@gmail.com>
18 *
19 */
20
21namespace Synapse
22{
23 public class VolumeService : GLib.Object
24 {
25 // singleton that can be easily destroyed
26 private static unowned VolumeService? instance;
27 public static VolumeService get_default ()
28 {
29 return instance ?? new VolumeService ();
30 }
31
32 private VolumeService ()
33 {
34 }
35
36 ~VolumeService ()
37 {
38 instance = null;
39 }
40
41 private VolumeMonitor vm;
42 private Gee.Map<GLib.Volume, VolumeObject> volumes;
43
44 construct
45 {
46 instance = this;
47
48 volumes = new Gee.HashMap<GLib.Volume, VolumeObject> ();
49 initialize ();
50 }
51
52 protected void initialize ()
53 {
54 vm = VolumeMonitor.get ();
55
56 vm.volume_added.connect ((volume) =>
57 {
58 volumes[volume] = new VolumeObject (volume);
59 });
60 vm.volume_removed.connect ((volume) =>
61 {
62 volumes.unset (volume);
63 });
64 vm.mount_added.connect ((mount) =>
65 {
66 var volume = mount.get_volume ();
67 if (volume == null) return;
68
69 if (volume in volumes.keys) volumes[volume].update_state ();
70 });
71 // FIXME: connect also to other signals?
72
73 var volume_list = vm.get_volumes ();
74 process_volume_list (volume_list);
75 }
76
77 private void process_volume_list (GLib.List<GLib.Volume> volume_list)
78 {
79 foreach (unowned GLib.Volume volume in volume_list)
80 {
81 volumes[volume] = new VolumeObject (volume);
82 }
83 }
84
85 public Gee.Collection<VolumeObject> get_volumes ()
86 {
87 return volumes.values;
88 }
89
90 public string? uri_to_volume_name (string uri, out string? volume_path)
91 {
92 volume_path = null;
93 var g_volumes = volumes.keys;
94
95 var f = File.new_for_uri (uri);
96 // FIXME: cache this somehow
97 foreach (var volume in g_volumes)
98 {
99 File? root = volume.get_activation_root ();
100 if (root == null)
101 {
102 var mount = volume.get_mount ();
103 if (mount == null) continue;
104 root = mount.get_root ();
105 }
106
107 if (f.has_prefix (root))
108 {
109 volume_path = root.get_path ();
110 return volume.get_name ();
111 }
112 }
113
114 return null;
115 }
116
117 public class VolumeObject: Object, Match, UriMatch
118 {
119 public string title { get; construct set; }
120 public string description { get; set; }
121 public string icon_name { get; construct set; }
122 public bool has_thumbnail { get; construct set; }
123 public string thumbnail_path { get; construct set; }
124 public MatchType match_type { get; construct set; }
125
126 // UriMatch
127 public string uri { get; set; }
128 public QueryFlags file_type { get; set; }
129 public string mime_type { get; set; }
130
131 private ulong changed_signal_id;
132
133 private GLib.Volume _volume;
134 public GLib.Volume volume
135 {
136 get { return _volume; }
137 set
138 {
139 _volume = value;
140 title = value.get_name ();
141 description = ""; // FIXME
142 icon_name = value.get_icon ().to_string ();
143 has_thumbnail = false;
144 match_type = value.get_mount () != null ?
145 MatchType.GENERIC_URI : MatchType.ACTION;
146
147 if (match_type == MatchType.GENERIC_URI)
148 {
149 uri = value.get_mount ().get_root ().get_uri ();
150 file_type = QueryFlags.PLACES;
151 mime_type = ""; // FIXME: do we need this?
152 }
153 else
154 {
155 uri = null;
156 }
157
158 if (changed_signal_id == 0)
159 {
160 changed_signal_id = _volume.changed.connect (this.update_state);
161 }
162
163 Utils.Logger.debug (this, "vo[%p]: %s [%s], has_mount: %d, uri: %s", this, title, icon_name, (value.get_mount () != null ? 1 : 0), uri);
164 }
165 }
166
167 public void update_state ()
168 {
169 this.volume = _volume; // call setter again
170 }
171
172 public bool is_mounted ()
173 {
174 return _volume.get_mount () != null;
175 }
176
177 public VolumeObject (GLib.Volume volume)
178 {
179 Object (volume: volume);
180 }
181
182 ~VolumeObject ()
183 {
184 if (changed_signal_id != 0)
185 {
186 SignalHandler.disconnect (_volume, changed_signal_id);
187 changed_signal_id = 0;
188 }
189 }
190 }
191 }
192}
193
0194
=== added directory 'lib/synapse-plugins'
=== added file 'lib/synapse-plugins/CMakeLists.txt'
--- lib/synapse-plugins/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ lib/synapse-plugins/CMakeLists.txt 2014-06-16 07:45:32 +0000
@@ -0,0 +1,46 @@
1set(PLUGINS_LIB_VERSION 0.1)
2set(PLUGINS_LIB_SOVERSION 0)
3set(PLUGINS_LIBRARY_NAME synapse-plugins)
4set(PLUGINS_PKG
5 glib-2.0
6 gio-unix-2.0
7 gee-0.8
8 gtk+-3.0
9)
10
11pkg_check_modules(PLUGINS_DEPS REQUIRED ${PLUGINS_PKG})
12
13set(PLUGINS_SOURCE
14 command-plugin.vala
15 desktop-file-plugin.vala
16)
17
18set(LINK_MODE STATIC)
19
20vala_precompile(PLUGINS_VALA_C ${PLUGINS_LIBRARY_NAME}
21 ${PLUGINS_SOURCE}
22PACKAGES
23 ${PLUGINS_PKG}
24 synapse-core
25OPTIONS
26 --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core
27GENERATE_VAPI
28 ${PLUGINS_LIBRARY_NAME}
29GENERATE_HEADER
30 ${PLUGINS_LIBRARY_NAME}
31)
32
33add_definitions(${PLUGINS_DEPS_CFLAGS} -include config.h -w)
34link_directories(${PLUGINS_DEPS_LIBRARY_DIRS})
35include_directories(${CMAKE_BINARY_DIR}/lib/synapse-core)
36
37add_library(${PLUGINS_LIBRARY_NAME} STATIC ${PLUGINS_VALA_C})
38
39set_target_properties(${PLUGINS_LIBRARY_NAME} PROPERTIES
40 OUTPUT_NAME ${PLUGINS_LIBRARY_NAME}
41 VERSION ${PLUGINS_LIB_VERSION}
42 SOVERSION ${PLUGINS_LIB_SOVERSION}
43)
44
45target_link_libraries (${PLUGINS_LIBRARY_NAME} ${PLUGINS_DEPS_LIBRARIES} synapse-core)
46
047
=== added file 'lib/synapse-plugins/command-plugin.vala'
--- lib/synapse-plugins/command-plugin.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-plugins/command-plugin.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,188 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
17 *
18 * Authored by Michal Hruby <michal.mhr@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24 public class CommandPlugin: Object, Activatable, ItemProvider
25 {
26 public bool enabled { get; set; default = true; }
27
28 public void activate ()
29 {
30
31 }
32
33 public void deactivate ()
34 {
35
36 }
37
38 private class CommandObject: Object, Match, ApplicationMatch
39 {
40 // for Match interface
41 public string title { get; construct set; }
42 public string description { get; set; default = ""; }
43 public string icon_name { get; construct set; default = ""; }
44 public bool has_thumbnail { get; construct set; default = false; }
45 public string thumbnail_path { get; construct set; }
46 public MatchType match_type { get; construct set; }
47
48 // for ApplicationMatch
49 public AppInfo? app_info { get; set; default = null; }
50 public bool needs_terminal { get; set; default = false; }
51 public string? filename { get; construct set; default = null; }
52 public string command { get; construct set; }
53
54 public CommandObject (string cmd)
55 {
56 Object (title: _("Execute '%s'").printf (cmd), description: _ ("Run command"), command: cmd,
57 icon_name: "application-x-executable",
58 match_type: MatchType.APPLICATION,
59 needs_terminal: cmd.has_prefix ("sudo "));
60
61 try
62 {
63 app_info = AppInfo.create_from_commandline (cmd, null, 0);
64 }
65 catch (Error err)
66 {
67 warning ("%s", err.message);
68 }
69 }
70 }
71
72 static void register_plugin ()
73 {
74 DataSink.PluginRegistry.get_default ().register_plugin (
75 typeof (CommandPlugin),
76 "Command Search",
77 _ ("Find and execute arbitrary commands."),
78 "system-run",
79 register_plugin
80 );
81 }
82
83 static construct
84 {
85 register_plugin ();
86 }
87
88 private Gee.Set<string> past_commands;
89 private Regex split_regex;
90
91 construct
92 {
93 // TODO: load from configuration
94 past_commands = new Gee.HashSet<string> ();
95 try
96 {
97 split_regex = new Regex ("\\s+", RegexCompileFlags.OPTIMIZE);
98 }
99 catch (RegexError err)
100 {
101 critical ("%s", err.message);
102 }
103 }
104
105 private CommandObject? create_co (string exec)
106 {
107 // ignore results that will be returned by DesktopFilePlugin
108 // and at the same time look for hidden and no-display desktop files,
109 // so we can display their info (title, comment, icon)
110 var dfs = DesktopFileService.get_default ();
111 var df_list = dfs.get_desktop_files_for_exec (exec);
112 DesktopFileInfo? dfi = null;
113 foreach (var df in df_list)
114 {
115 if (!df.is_hidden) return null; // will be handled by App plugin
116 dfi = df;
117 }
118
119 var co = new CommandObject (exec);
120 if (dfi != null)
121 {
122 co.title = dfi.name;
123 if (dfi.comment != "") co.description = dfi.comment;
124 if (dfi.icon_name != null && dfi.icon_name != "") co.icon_name = dfi.icon_name;
125 }
126
127 return co;
128 }
129
130 private void command_executed (Match match)
131 {
132 CommandObject? co = match as CommandObject;
133 if (co == null) return;
134
135 past_commands.add (co.command);
136 }
137
138 public async ResultSet? search (Query q) throws SearchError
139 {
140 // we only search for applications
141 if (!(QueryFlags.APPLICATIONS in q.query_type)) return null;
142
143 Idle.add (search.callback);
144 yield;
145
146 var result = new ResultSet ();
147
148 string stripped = q.query_string.strip ();
149 if (stripped == "") return null;
150 if (stripped.has_prefix ("~/"))
151 {
152 stripped = stripped.replace ("~", Environment.get_home_dir ());
153 }
154
155 if (!(stripped in past_commands))
156 {
157 foreach (var command in past_commands)
158 {
159 if (command.has_prefix (stripped))
160 {
161 result.add (create_co (command), Match.Score.AVERAGE);
162 }
163 }
164
165 string[] args = split_regex.split (stripped);
166 string? valid_cmd = Environment.find_program_in_path (args[0]);
167
168 if (valid_cmd != null)
169 {
170 // don't allow dangerous commands
171 if (args[0] == "rm") return null;
172 CommandObject? co = create_co (stripped);
173 if (co == null) return null;
174 result.add (co, Match.Score.POOR);
175 co.executed.connect (this.command_executed);
176 }
177 }
178 else
179 {
180 result.add (create_co (stripped), Match.Score.VERY_GOOD);
181 }
182
183 q.check_cancellable ();
184
185 return result;
186 }
187 }
188}
0189
=== added file 'lib/synapse-plugins/desktop-file-plugin.vala'
--- lib/synapse-plugins/desktop-file-plugin.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-plugins/desktop-file-plugin.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,350 @@
1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
17 *
18 * Authored by Michal Hruby <michal.mhr@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24 public class DesktopFilePlugin: Object, Activatable, ItemProvider, ActionProvider
25 {
26 public bool enabled { get; set; default = true; }
27
28 public void activate ()
29 {
30
31 }
32
33 public void deactivate ()
34 {
35
36 }
37
38 private class DesktopFileMatch: Object, Match, ApplicationMatch
39 {
40 // for Match interface
41 public string title { get; construct set; }
42 public string description { get; set; default = ""; }
43 public string icon_name { get; construct set; default = ""; }
44 public bool has_thumbnail { get; construct set; default = false; }
45 public string thumbnail_path { get; construct set; }
46 public MatchType match_type { get; construct set; }
47
48 // for ApplicationMatch
49 public AppInfo? app_info { get; set; default = null; }
50 public bool needs_terminal { get; set; default = false; }
51 public string? filename { get; construct set; }
52
53 private string? title_folded = null;
54 public unowned string get_title_folded ()
55 {
56 if (title_folded == null) title_folded = title.casefold ();
57 return title_folded;
58 }
59
60 public string? title_unaccented { get; set; default = null; }
61 public string? desktop_id { get; set; default = null; }
62
63 public string exec { get; set; }
64
65 public DesktopFileMatch.for_info (DesktopFileInfo info)
66 {
67 Object (filename: info.filename, match_type: MatchType.APPLICATION);
68
69 init_from_info (info);
70 }
71
72 private void init_from_info (DesktopFileInfo info)
73 {
74 this.title = info.name;
75 this.description = info.comment;
76 this.icon_name = info.icon_name;
77 this.exec = info.exec;
78 this.needs_terminal = info.needs_terminal;
79 this.title_folded = info.get_name_folded ();
80 this.title_unaccented = Utils.remove_accents (this.title_folded);
81 this.desktop_id = "application://" + info.desktop_id;
82 }
83 }
84
85 static void register_plugin ()
86 {
87 DataSink.PluginRegistry.get_default ().register_plugin (
88 typeof (DesktopFilePlugin),
89 "Application Search",
90 _ ("Search for and run applications on your computer."),
91 "system-run",
92 register_plugin
93 );
94 }
95
96 static construct
97 {
98 register_plugin ();
99 }
100
101 private Gee.List<DesktopFileMatch> desktop_files;
102
103 construct
104 {
105 desktop_files = new Gee.ArrayList<DesktopFileMatch> ();
106 mimetype_map = new Gee.HashMap<string, OpenWithAction> ();
107
108 var dfs = DesktopFileService.get_default ();
109 dfs.reload_started.connect (() => {
110 loading_in_progress = true;
111 });
112 dfs.reload_done.connect (() => {
113 mimetype_map.clear ();
114 desktop_files.clear ();
115 load_all_desktop_files ();
116 });
117
118 load_all_desktop_files ();
119 }
120
121 public signal void load_complete ();
122 private bool loading_in_progress = false;
123
124 private async void load_all_desktop_files ()
125 {
126 loading_in_progress = true;
127 Idle.add_full (Priority.LOW, load_all_desktop_files.callback);
128 yield;
129
130 var dfs = DesktopFileService.get_default ();
131
132 foreach (DesktopFileInfo dfi in dfs.get_desktop_files ())
133 {
134 desktop_files.add (new DesktopFileMatch.for_info (dfi));
135 }
136
137 loading_in_progress = false;
138 load_complete ();
139 }
140
141 private int compute_relevancy (DesktopFileMatch dfm, int base_relevancy)
142 {
143 var rs = RelevancyService.get_default ();
144 float popularity = rs.get_application_popularity (dfm.desktop_id);
145
146 int r = RelevancyService.compute_relevancy (base_relevancy, popularity);
147 Utils.Logger.debug (this, "relevancy for %s: %d", dfm.desktop_id, r);
148
149 return r;
150 }
151
152 private void full_search (Query q, ResultSet results,
153 MatcherFlags flags = 0)
154 {
155 // try to match against global matchers and if those fail, try also exec
156 var matchers = Query.get_matchers_for_query (q.query_string_folded,
157 flags);
158
159 foreach (var dfm in desktop_files)
160 {
161 unowned string folded_title = dfm.get_title_folded ();
162 unowned string unaccented_title = dfm.title_unaccented;
163 bool matched = false;
164 // FIXME: we need to do much smarter relevancy computation in fuzzy re
165 // "sysmon" matching "System Monitor" is very good as opposed to
166 // "seto" matching "System Monitor"
167 foreach (var matcher in matchers)
168 {
169 if (matcher.key.match (folded_title))
170 {
171 results.add (dfm, compute_relevancy (dfm, matcher.value));
172 matched = true;
173 break;
174 }
175 else if (unaccented_title != null && matcher.key.match (unaccented_title))
176 {
177 results.add (dfm, compute_relevancy (dfm, matcher.value - Match.Score.INCREMENT_SMALL));
178 matched = true;
179 break;
180 }
181 }
182 if (!matched && dfm.exec.has_prefix (q.query_string))
183 {
184 results.add (dfm, compute_relevancy (dfm, dfm.exec == q.query_string ?
185 Match.Score.VERY_GOOD : Match.Score.AVERAGE - Match.Score.INCREMENT_SMALL));
186 }
187 }
188 }
189
190 public bool handles_query (Query q)
191 {
192 // we only search for applications
193 if (!(QueryFlags.APPLICATIONS in q.query_type)) return false;
194 if (q.query_string.strip () == "") return false;
195
196 return true;
197 }
198
199 public async ResultSet? search (Query q) throws SearchError
200 {
201 if (loading_in_progress)
202 {
203 // wait
204 ulong signal_id = this.load_complete.connect (() =>
205 {
206 search.callback ();
207 });
208 yield;
209 SignalHandler.disconnect (this, signal_id);
210 }
211 else
212 {
213 // we'll do this so other plugins can send their DBus requests etc.
214 // and they don't have to wait for our blocking (though fast) search
215 // to finish
216 Idle.add_full (Priority.HIGH_IDLE, search.callback);
217 yield;
218 }
219
220 q.check_cancellable ();
221
222 // FIXME: spawn new thread and do the search there?
223 var result = new ResultSet ();
224
225 // FIXME: make sure this is one unichar, not just byte
226 if (q.query_string.length == 1)
227 {
228 var flags = MatcherFlags.NO_SUBSTRING | MatcherFlags.NO_PARTIAL |
229 MatcherFlags.NO_FUZZY;
230 full_search (q, result, flags);
231 }
232 else
233 {
234 full_search (q, result);
235 }
236
237 q.check_cancellable ();
238
239 return result;
240 }
241
242 private class OpenWithAction: Object, Match
243 {
244 // for Match interface
245 public string title { get; construct set; }
246 public string description { get; set; default = ""; }
247 public string icon_name { get; construct set; default = ""; }
248 public bool has_thumbnail { get; construct set; default = false; }
249 public string thumbnail_path { get; construct set; }
250 public MatchType match_type { get; construct set; }
251
252 public DesktopFileInfo desktop_info { get; private set; }
253
254 public OpenWithAction (DesktopFileInfo info)
255 {
256 Object ();
257
258 init_with_info (info);
259 }
260
261 private void init_with_info (DesktopFileInfo info)
262 {
263 this.title = _ ("Open with %s").printf (info.name);
264 this.icon_name = info.icon_name;
265 this.description = _ ("Opens current selection using %s").printf (info.name);
266 this.desktop_info = info;
267 }
268
269 protected void execute (Match? match)
270 {
271 UriMatch uri_match = match as UriMatch;
272 return_if_fail (uri_match != null);
273
274 var f = File.new_for_uri (uri_match.uri);
275 try
276 {
277 var app_info = new DesktopAppInfo.from_filename (desktop_info.filename);
278 List<File> files = new List<File> ();
279 files.prepend (f);
280 app_info.launch (files, new Gdk.AppLaunchContext ());
281 }
282 catch (Error err)
283 {
284 warning ("%s", err.message);
285 }
286 }
287 }
288
289 private Gee.Map<string, Gee.List<OpenWithAction> > mimetype_map;
290
291 public ResultSet? find_for_match (ref Query query, Match match)
292 {
293 if (match.match_type != MatchType.GENERIC_URI) return null;
294
295 var uri_match = match as UriMatch;
296 return_val_if_fail (uri_match != null, null);
297
298 if (uri_match.mime_type == null) return null;
299
300 Gee.List<OpenWithAction> ow_list = mimetype_map[uri_match.mime_type];
301 /* Query DesktopFileService only if is necessary */
302 if (ow_list == null)
303 {
304 /* Initialize ow_list */
305 ow_list = new Gee.LinkedList<OpenWithAction> ();
306 mimetype_map[uri_match.mime_type] = ow_list;
307 var dfs = DesktopFileService.get_default ();
308 var list_for_mimetype = dfs.get_desktop_files_for_type (uri_match.mime_type);
309 /* If there's more than one application, fill the ow list */
310 if (list_for_mimetype.size > 1)
311 {
312 foreach (var entry in list_for_mimetype)
313 {
314 ow_list.add (new OpenWithAction (entry));
315 }
316 }
317 else return null;
318 }
319 else if (ow_list.size == 0) return null;
320
321 var rs = new ResultSet ();
322
323 if (query.query_string == "")
324 {
325 foreach (var action in ow_list)
326 {
327 rs.add (action, Match.Score.POOR);
328 }
329 }
330 else
331 {
332 var matchers = Query.get_matchers_for_query (query.query_string, 0,
333 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
334 foreach (var action in ow_list)
335 {
336 foreach (var matcher in matchers)
337 {
338 if (matcher.key.match (action.title))
339 {
340 rs.add (action, matcher.value);
341 break;
342 }
343 }
344 }
345 }
346
347 return rs;
348 }
349 }
350}
0351
=== modified file 'src/Backend/App.vala'
--- src/Backend/App.vala 2014-04-24 10:43:44 +0000
+++ src/Backend/App.vala 2014-06-16 07:45:32 +0000
@@ -16,8 +16,20 @@
16// along with this program. If not, see <http://www.gnu.org/licenses/>.16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17//17//
1818
19errordomain IconError {
20 NOT_FOUND
21}
22
19public class Slingshot.Backend.App : Object {23public class Slingshot.Backend.App : Object {
2024
25 public enum AppType {
26 APP,
27 COMMAND,
28 SYNAPSE
29 }
30
31 public signal void start_search (Synapse.SearchMatch search_match, Synapse.Match? target);
32
21 public string name { get; construct set; }33 public string name { get; construct set; }
22 public string description { get; private set; default = ""; }34 public string description { get; private set; default = ""; }
23 public string desktop_id { get; construct set; }35 public string desktop_id { get; construct set; }
@@ -30,13 +42,17 @@
30 public string desktop_path { get; private set; }42 public string desktop_path { get; private set; }
31 public string categories { get; private set; }43 public string categories { get; private set; }
32 public string generic_name { get; private set; default = ""; }44 public string generic_name { get; private set; default = ""; }
45 public AppType app_type { get; private set; default = AppType.APP; }
3346
34 private bool is_command = false;47 public Synapse.Match? match { get; private set; default = null; }
48 public Synapse.Match? target { get; private set; default = null; }
3549
36 public signal void icon_changed ();50 public signal void icon_changed ();
37 public signal void launched (App app);51 public signal void launched (App app);
3852
39 public App (GMenu.TreeEntry entry) {53 public App (GMenu.TreeEntry entry) {
54 app_type = AppType.APP;
55
40 unowned GLib.DesktopAppInfo info = entry.get_app_info ();56 unowned GLib.DesktopAppInfo info = entry.get_app_info ();
41 name = info.get_display_name ().dup ();57 name = info.get_display_name ().dup ();
42 description = info.get_description ().dup () ?? name;58 description = info.get_description ().dup () ?? name;
@@ -70,6 +86,7 @@
70 }86 }
7187
72 public App.from_command (string command) {88 public App.from_command (string command) {
89 app_type = AppType.COMMAND;
7390
74 name = command;91 name = command;
75 description = _("Run this command...");92 description = _("Run this command...");
@@ -77,7 +94,20 @@
77 desktop_id = command;94 desktop_id = command;
78 icon_name = "system-run";95 icon_name = "system-run";
7996
80 is_command = true;97 update_icon ();
98
99 }
100
101 public App.from_synapse_match (Synapse.Match match, Synapse.Match? target = null) {
102
103 app_type = AppType.SYNAPSE;
104
105 name = match.title;
106 description = match.description;
107 icon_name = match.icon_name;
108
109 this.match = match;
110 this.target = target;
81111
82 update_icon ();112 update_icon ();
83113
@@ -98,7 +128,31 @@
98 }128 }
99 }129 }
100130
101 public Gdk.Pixbuf load_icon (int size) {131 public Gdk.Pixbuf? load_icon (int size) {
132 if (app_type == AppType.SYNAPSE) {
133 try {
134 // for contacts we can load the thumbnail because we expect it to be
135 // the avatar. For other types it'd be ridiculously small.
136 if (match.match_type == Synapse.MatchType.CONTACT && match.has_thumbnail) {
137 return new Gdk.Pixbuf.from_file_at_scale (match.thumbnail_path, size, size, true);
138 }
139
140 var icon = Icon.new_for_string (icon_name);
141 var info = Gtk.IconTheme.get_default ().lookup_by_gicon (icon,
142 size, Gtk.IconLookupFlags.FORCE_SIZE);
143
144 if (info == null)
145 throw new IconError.NOT_FOUND ("Not found");
146
147 return info.load_icon ();
148 } catch (Error e) {
149 warning ("Failed to load icon: %s\n", e.message);
150 }
151
152 return Slingshot.icon_theme.load_icon ("application-default-icon",
153 size, Gtk.IconLookupFlags.FORCE_SIZE);
154 }
155
102 Gdk.Pixbuf icon = null;156 Gdk.Pixbuf icon = null;
103 var flags = Gtk.IconLookupFlags.FORCE_SIZE;157 var flags = Gtk.IconLookupFlags.FORCE_SIZE;
104158
@@ -156,19 +210,35 @@
156 return icon;210 return icon;
157 }211 }
158212
159 public void launch () {213 public bool launch () {
160 try {214 try {
161 if (is_command) {215 switch (app_type) {
162 debug (@"Launching command: $name");216 case AppType.COMMAND:
163 Process.spawn_command_line_async (exec);217 debug (@"Launching command: $name");
164 } else {218 Process.spawn_command_line_async (exec);
165 launched (this); // Emit launched signal219 break;
166 new DesktopAppInfo (desktop_id).launch (null, null);220 case AppType.APP:
167 debug (@"Launching application: $name");221 launched (this); // Emit launched signal
222 new DesktopAppInfo (desktop_id).launch (null, null);
223 debug (@"Launching application: $name");
224 break;
225 case AppType.SYNAPSE:
226 if (match.match_type == Synapse.MatchType.SEARCH) {
227 start_search (match as Synapse.SearchMatch, target);
228 return false;
229 } else {
230 if (target == null)
231 Backend.SynapseSearch.find_actions_for_match (match).get (0).execute_with_target (match);
232 else
233 match.execute_with_target (target);
234 }
235 break;
168 }236 }
169 } catch (Error e) {237 } catch (Error e) {
170 warning ("Failed to launch %s: %s", name, exec);238 warning ("Failed to launch %s: %s", name, exec);
171 }239 }
240
241 return true;
172 }242 }
173243
174}244}
175245
=== modified file 'src/Backend/AppSystem.vala'
--- src/Backend/AppSystem.vala 2014-04-24 21:11:21 +0000
+++ src/Backend/AppSystem.vala 2014-06-16 07:45:32 +0000
@@ -161,7 +161,9 @@
161 foreach (Gee.ArrayList<App> category in apps.values) {161 foreach (Gee.ArrayList<App> category in apps.values) {
162 foreach (App app in category) {162 foreach (App app in category) {
163163
164 if (GCC_PANEL_CATEGORY in app.categories || SWITCHBOARD_PLUG_CATEGORY in app.categories)164 if (app.categories != null
165 && (GCC_PANEL_CATEGORY in app.categories
166 || SWITCHBOARD_PLUG_CATEGORY in app.categories))
165 continue;167 continue;
166 168
167169
@@ -196,19 +198,19 @@
196 foreach (App app in category) {198 foreach (App app in category) {
197 if (!(app.exec in sorted_apps_execs)) {199 if (!(app.exec in sorted_apps_execs)) {
198 sorted_apps_execs += app.exec;200 sorted_apps_execs += app.exec;
199 if (search in app.name.down ()) {201 if (app.name != null && search in app.name.down ()) {
200 if (search == app.name.down ()[0:search.length])202 if (search == app.name.down ()[0:search.length])
201 app.relevancy = 0.5 - app.popularity; // It must be minor than 1.0203 app.relevancy = 0.5 - app.popularity; // It must be minor than 1.0
202 else204 else
203 app.relevancy = app.name.length / search.length - app.popularity;205 app.relevancy = app.name.length / search.length - app.popularity;
204 filtered.add (app);206 filtered.add (app);
205 } else if (search in app.exec.down ()) {207 } else if (app.exec != null && search in app.exec.down ()) {
206 app.relevancy = app.exec.length / search.length * 10.0 - app.popularity;208 app.relevancy = app.exec.length / search.length * 10.0 - app.popularity;
207 filtered.add (app);209 filtered.add (app);
208 } else if (search in app.description.down ()) {210 } else if (app.description != null && search in app.description.down ()) {
209 app.relevancy = app.description.length / search.length - app.popularity;211 app.relevancy = app.description.length / search.length - app.popularity;
210 filtered.add (app);212 filtered.add (app);
211 } else if (search in app.generic_name.down ()) {213 } else if (app.generic_name != null && search in app.generic_name.down ()) {
212 app.relevancy = app.generic_name.length / search.length - app.popularity;214 app.relevancy = app.generic_name.length / search.length - app.popularity;
213 filtered.add (app);215 filtered.add (app);
214 } else if (app.keywords != null) {216 } else if (app.keywords != null) {
@@ -236,4 +238,4 @@
236238
237 }239 }
238240
239}
240\ No newline at end of file241\ No newline at end of file
242}
241243
=== added file 'src/Backend/SynapseSearch.vala'
--- src/Backend/SynapseSearch.vala 1970-01-01 00:00:00 +0000
+++ src/Backend/SynapseSearch.vala 2014-06-16 07:45:32 +0000
@@ -0,0 +1,163 @@
1// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
2//
3// Copyright (C) 2011-2012 Giulio Collura
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17//
18
19namespace Slingshot.Backend {
20
21 public class SynapseSearch : Object {
22
23 private static Type[] plugins = {
24 typeof (Synapse.CommandPlugin),
25 typeof (Synapse.DesktopFilePlugin)
26 };
27
28 private static Synapse.DataSink? sink = null;
29 private static Gee.HashMap<string,Gdk.Pixbuf> favicon_cache;
30
31 Cancellable? current_search = null;
32
33 public SynapseSearch () {
34
35 if (sink == null) {
36 sink = new Synapse.DataSink ();
37 foreach (var plugin in plugins) {
38 sink.register_static_plugin (plugin);
39 }
40
41 favicon_cache = new Gee.HashMap<string,Gdk.Pixbuf> ();
42 }
43 }
44
45 public async Gee.List<Synapse.Match>? search (string text, Synapse.SearchProvider? provider = null) {
46
47 if (current_search != null)
48 current_search.cancel ();
49
50 if (provider == null)
51 provider = sink;
52
53 var results = new Synapse.ResultSet ();
54
55 try {
56 return yield provider.search (text, Synapse.QueryFlags.ALL, results, current_search);
57 } catch (Error e) { warning (e.message); }
58
59 return null;
60 }
61
62 public static Gee.List<Synapse.Match> find_actions_for_match (Synapse.Match match) {
63 return sink.find_actions_for_match (match, null, Synapse.QueryFlags.ALL);
64 }
65
66 /**
67 * Attempts to load a favicon for an UriMatch and caches the icon
68 *
69 * @param match The UriMatch
70 * @param size The icon size at which to load the icon. If the favicon is smaller than
71 * that size, null will be returned
72 * @param cancellable Cancellable for the loading operations
73 * @return The pixbuf or null if loading failed or the icon was too small
74 */
75 public static async Gdk.Pixbuf? get_favicon_for_match (Synapse.UriMatch match, int size,
76 Cancellable? cancellable = null) {
77
78 var soup_uri = new Soup.URI (match.uri);
79 if (!soup_uri.scheme.has_prefix ("http"))
80 return null;
81
82 Gdk.Pixbuf? pixbuf = null;
83
84 if (favicon_cache.has_key (soup_uri.host))
85 return favicon_cache.get (soup_uri.host);
86
87 var url = "%s://%s/favicon.ico".printf (soup_uri.scheme, soup_uri.host);
88
89 var msg = new Soup.Message ("GET", url);
90 var session = new Soup.Session ();
91 session.use_thread_context = true;
92
93 try {
94 var stream = yield session.send_async (msg, cancellable);
95 if (stream != null) {
96 pixbuf = yield new Gdk.Pixbuf.from_stream_async (stream, cancellable);
97 // as per design decision, icons that are smaller than requested will not
98 // be displayed, instead the fallback should be used, so we return null
99 if (pixbuf.width < size)
100 pixbuf = null;
101 }
102 } catch (Error e) {}
103
104 if (cancellable.is_cancelled ())
105 return null;
106
107 // we set the cache in any case, even if things failed. No need to
108 // try requesting an icon again and again
109 favicon_cache.set (soup_uri.host, pixbuf);
110
111 return pixbuf;
112 }
113
114 // copied from synapse-ui with some slight changes
115 public static string markup_string_with_search (string text, string pattern) {
116
117 string markup = "%s";
118
119 if (pattern == "") {
120 return markup.printf (Markup.escape_text (text));
121 }
122
123 // if no text found, use pattern
124 if (text == "") {
125 return markup.printf (Markup.escape_text (pattern));
126 }
127
128 var matchers = Synapse.Query.get_matchers_for_query (pattern, 0,
129 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
130
131 string? highlighted = null;
132 foreach (var matcher in matchers) {
133 MatchInfo mi;
134 if (matcher.key.match (text, 0, out mi)) {
135 int start_pos;
136 int end_pos;
137 int last_pos = 0;
138 int cnt = mi.get_match_count ();
139 StringBuilder res = new StringBuilder ();
140 for (int i = 1; i < cnt; i++) {
141 mi.fetch_pos (i, out start_pos, out end_pos);
142 warn_if_fail (start_pos >= 0 && end_pos >= 0);
143 res.append (Markup.escape_text (text.substring (last_pos, start_pos - last_pos)));
144 last_pos = end_pos;
145 res.append (Markup.printf_escaped ("<b>%s</b>", mi.fetch (i)));
146 if (i == cnt - 1) {
147 res.append (Markup.escape_text (text.substring (last_pos)));
148 }
149 }
150 highlighted = res.str;
151 break;
152 }
153 }
154
155 if (highlighted != null) {
156 return markup.printf (highlighted);
157 } else {
158 return markup.printf (Markup.escape_text(text));
159 }
160 }
161 }
162}
163
0164
=== modified file 'src/Slingshot.vala'
--- src/Slingshot.vala 2013-12-26 00:08:04 +0000
+++ src/Slingshot.vala 2014-06-16 07:45:32 +0000
@@ -36,20 +36,20 @@
36 build_version_info = Build.VERSION_INFO;36 build_version_info = Build.VERSION_INFO;
3737
38 program_name = "Slingshot";38 program_name = "Slingshot";
39 exec_name = "slingshot-launcher";39 exec_name = "slingshot-launcher";
40 app_copyright = "GPLv3";40 app_copyright = "GPLv3";
41 app_icon = "";41 app_icon = "";
42 app_launcher = "";42 app_launcher = "";
43 app_years = "2011-2012";43 app_years = "2011-2012";
44 application_id = "net.launchpad.slingshot";44 application_id = "net.launchpad.slingshot";
45 main_url = "https://launchpad.net/slingshot";45 main_url = "https://launchpad.net/slingshot";
46 bug_url = "https://bugs.launchpad.net/slingshot";46 bug_url = "https://bugs.launchpad.net/slingshot";
47 help_url = "https://answers.launchpad.net/slingshot";47 help_url = "https://answers.launchpad.net/slingshot";
48 translate_url = "https://translations.launchpad.net/slingshot";48 translate_url = "https://translations.launchpad.net/slingshot";
4949
50 about_authors = {"Giulio Collura <random.cpp@gmail.com>",50 about_authors = {"Giulio Collura <random.cpp@gmail.com>",
51 "Andrea Basso <andrea@elementaryos.org"};51 "Andrea Basso <andrea@elementaryos.org"};
52 about_artists = {"Harvey Cabaguio 'BassUltra' <harveycabaguio@gmail.com>",52 about_artists = {"Harvey Cabaguio 'BassUltra' <harveycabaguio@gmail.com>",
53 "Daniel Foré <bunny@go-docky.com>"};53 "Daniel Foré <bunny@go-docky.com>"};
54 about_translators = "Launchpad Translators";54 about_translators = "Launchpad Translators";
55 about_license_type = Gtk.License.GPL_3_0;55 about_license_type = Gtk.License.GPL_3_0;
5656
=== modified file 'src/SlingshotView.vala'
--- src/SlingshotView.vala 2014-05-28 08:48:51 +0000
+++ src/SlingshotView.vala 2014-06-16 07:45:32 +0000
@@ -27,7 +27,8 @@
27 public class SlingshotView : Granite.Widgets.PopOver {27 public class SlingshotView : Granite.Widgets.PopOver {
2828
29 // Widgets29 // Widgets
30 public Gtk.SearchEntry search_entry;30 public Gtk.SearchEntry dummy_search_entry;
31 public Widgets.LargeSearchEntry real_search_entry;
31 public Gtk.Stack stack;32 public Gtk.Stack stack;
32 public Granite.Widgets.ModeButton view_selector;33 public Granite.Widgets.ModeButton view_selector;
3334
@@ -39,6 +40,7 @@
39 public Gtk.Grid top;40 public Gtk.Grid top;
40 public Gtk.Grid center;41 public Gtk.Grid center;
41 public Gtk.Grid container;42 public Gtk.Grid container;
43 public Gtk.Stack main_stack;
42 public Gtk.Box content_area;44 public Gtk.Box content_area;
43 private Gtk.EventBox event_box;45 private Gtk.EventBox event_box;
4446
@@ -46,11 +48,11 @@
46 private Gee.ArrayList<GMenu.TreeDirectory> categories;48 private Gee.ArrayList<GMenu.TreeDirectory> categories;
47 public Gee.HashMap<string, Gee.ArrayList<Backend.App>> apps;49 public Gee.HashMap<string, Gee.ArrayList<Backend.App>> apps;
4850
49 private int current_position = 0;
50 private int search_view_position = 0;
51 private Modality modality;51 private Modality modality;
52 private bool can_trigger_hotcorner = true;52 private bool can_trigger_hotcorner = true;
5353
54 private Backend.SynapseSearch synapse;
55
54 // Sizes56 // Sizes
55 public int columns {57 public int columns {
56 get {58 get {
@@ -91,6 +93,7 @@
91 Slingshot.icon_theme = Gtk.IconTheme.get_default ();93 Slingshot.icon_theme = Gtk.IconTheme.get_default ();
9294
93 app_system = new Backend.AppSystem ();95 app_system = new Backend.AppSystem ();
96 synapse = new Backend.SynapseSearch ();
9497
95 categories = app_system.get_categories ();98 categories = app_system.get_categories ();
96 apps = app_system.get_apps ();99 apps = app_system.get_apps ();
@@ -133,6 +136,10 @@
133 // Create the base container136 // Create the base container
134 container = new Gtk.Grid ();137 container = new Gtk.Grid ();
135138
139 main_stack = new Gtk.Stack ();
140
141 main_stack.add_named (container, "apps");
142
136 // Add top bar143 // Add top bar
137 top = new Gtk.Grid ();144 top = new Gtk.Grid ();
138145
@@ -154,16 +161,16 @@
154 else161 else
155 view_selector.selected = 0;162 view_selector.selected = 0;
156163
157 search_entry = new Gtk.SearchEntry ();164 dummy_search_entry = new Gtk.SearchEntry ();
158 search_entry.placeholder_text = _("Search Apps…");165 dummy_search_entry.placeholder_text = _("Search Apps…");
159 search_entry.width_request = 250;166 dummy_search_entry.width_request = 250;
160 search_entry.button_press_event.connect ((e) => {return e.button == 3;});167 dummy_search_entry.button_press_event.connect ((e) => {return e.button == 3;});
161168
162 if (Slingshot.settings.show_category_filter) {169 if (Slingshot.settings.show_category_filter) {
163 top.attach (view_selector, 0, 0, 1, 1);170 top.attach (view_selector, 0, 0, 1, 1);
164 }171 }
165 top.attach (top_separator, 1, 0, 1, 1);172 top.attach (top_separator, 1, 0, 1, 1);
166 top.attach (search_entry, 2, 0, 1, 1);173 top.attach (dummy_search_entry, 2, 0, 1, 1);
167174
168 center = new Gtk.Grid ();175 center = new Gtk.Grid ();
169 176
@@ -178,12 +185,21 @@
178 stack.add_named (scrolled_normal, "normal");185 stack.add_named (scrolled_normal, "normal");
179186
180 // Create the "SEARCH_VIEW"187 // Create the "SEARCH_VIEW"
188 var search_view_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
189
190 real_search_entry = new Widgets.LargeSearchEntry ();
191 real_search_entry.margin_left = real_search_entry.margin_right = 12;
192
181 search_view = new Widgets.SearchView (this);193 search_view = new Widgets.SearchView (this);
182194 search_view.start_search.connect ((match, target) => {
183 foreach (Gee.ArrayList<Backend.App> app_list in apps.values) {195 search.begin (real_search_entry.text, match, target);
184 search_view.add_apps (app_list);196 });
185 }197
186 stack.add_named (search_view, "search");198 search_view_container.pack_start (real_search_entry, false);
199 search_view_container.pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), false);
200 search_view_container.pack_start (search_view);
201
202 main_stack.add_named (search_view_container, "search");
187203
188 // Create the "CATEGORY_VIEW"204 // Create the "CATEGORY_VIEW"
189 category_view = new Widgets.CategoryView (this);205 category_view = new Widgets.CategoryView (this);
@@ -193,7 +209,7 @@
193 container.attach (Utils.set_padding (center, 0, 12, 12, 12), 0, 1, 1, 1);209 container.attach (Utils.set_padding (center, 0, 12, 12, 12), 0, 1, 1, 1);
194210
195 event_box = new Gtk.EventBox ();211 event_box = new Gtk.EventBox ();
196 event_box.add (container);212 event_box.add (main_stack);
197 // Add the container to the dialog's content area213 // Add the container to the dialog's content area
198 content_area = get_content_area () as Gtk.Box;214 content_area = get_content_area () as Gtk.Box;
199 content_area.pack_start (event_box);215 content_area.pack_start (event_box);
@@ -265,24 +281,37 @@
265 private void connect_signals () {281 private void connect_signals () {
266282
267 this.focus_in_event.connect (() => {283 this.focus_in_event.connect (() => {
268 search_entry.grab_focus ();284 get_current_search_entry ().grab_focus ();
269 return false;285 return false;
270 });286 });
271287
272 event_box.key_press_event.connect (on_key_press);288 event_box.key_press_event.connect (on_key_press);
273 search_entry.key_press_event.connect (search_entry_key_press);289 dummy_search_entry.key_press_event.connect (search_entry_key_press);
274290 real_search_entry.widget.key_press_event.connect (search_entry_key_press);
275 search_entry.search_changed.connect (() => this.search.begin (search_entry.text));291
276 search_entry.grab_focus ();292 real_search_entry.search_changed.connect (() => {
277293 search.begin (real_search_entry.text);
278 search_entry.activate.connect (() => {294 });
279 if (modality == Modality.SEARCH_VIEW) {295 dummy_search_entry.search_changed.connect (() => {
280 search_view.launch_selected ();296 if (modality != Modality.SEARCH_VIEW)
281 hide ();297 set_modality (Modality.SEARCH_VIEW);
282 } else {298 });
283 if (get_focus () as Widgets.AppEntry != null) // checking the selected widget is an AppEntry299 dummy_search_entry.grab_focus ();
284 ((Widgets.AppEntry) get_focus ()).launch_app ();300
285 }301 dummy_search_entry.activate.connect (search_entry_activated);
302 real_search_entry.widget.activate.connect (search_entry_activated);
303
304 // the focus-out event is fired as soon as the stack transition is ended
305 // at which point we're able to focus the real_search_entry
306 dummy_search_entry.focus_out_event.connect (() => {
307 real_search_entry.text = dummy_search_entry.text;
308 real_search_entry.widget.grab_focus ();
309 var cursor_pos = real_search_entry.text.length;
310 real_search_entry.widget.select_region (cursor_pos, cursor_pos);
311
312 dummy_search_entry.text = "";
313
314 return false;
286 });315 });
287316
288 search_view.app_launched.connect (() => hide ());317 search_view.app_launched.connect (() => hide ());
@@ -365,9 +394,33 @@
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches