1//      action.vala
2//
3//      Copyright 2011 Hong Jen Yee (PCMan) <pcman.tw@pcman.tw@gmail.com>
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 2 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, write to the Free Software
17//      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18//      MA 02110-1301, USA.
19//
20//
21
22namespace Fm {
23
24private string? desktop_env; // current desktop environment
25private bool actions_loaded = false; // all actions are loaded?
26private HashTable<string, FileActionObject> all_actions = null; // cache all loaded actions
27
28
29public enum FileActionType {
30	NONE,
31	ACTION,
32	MENU
33}
34
35public class FileActionObject : Object {
36	public FileActionObject() {
37	}
38
39	public FileActionObject.from_key_file(KeyFile kf) {
40		name = Utils.key_file_get_locale_string(kf, "Desktop Entry", "Name");
41		tooltip = Utils.key_file_get_locale_string(kf, "Desktop Entry", "Tooltip");
42		icon = Utils.key_file_get_locale_string(kf, "Desktop Entry", "Icon");
43		desc = Utils.key_file_get_locale_string(kf, "Desktop Entry", "Description");
44		enabled = Utils.key_file_get_bool(kf, "Desktop Entry", "Enabled", true);
45		hidden = Utils.key_file_get_bool(kf, "Desktop Entry", "Hidden", false);
46		suggested_shortcut = Utils.key_file_get_string(kf, "Desktop Entry", "SuggestedShortcut");
47
48		condition = new FileActionCondition(kf, "Desktop Entry");
49	}
50
51	public FileActionType type;
52	public string id;
53	public string? name;
54	public string? tooltip;
55	public string? icon;
56	public string? desc;
57	public bool enabled;
58	public bool hidden;
59	public string? suggested_shortcut;
60	public FileActionCondition condition;
61
62	// values cached during menu generation
63	public bool has_parent;
64}
65
66
67public enum FileActionTarget {
68	NONE,
69	CONTEXT = 1,
70	LOCATION = 1 << 1,
71	TOOLBAR = 1 << 2
72}
73
74public class FileAction : FileActionObject {
75
76	public FileAction(string desktop_id) {
77		var kf = new KeyFile();
78		id = desktop_id;
79		try {
80			kf.load_from_file(desktop_id, 0);
81			this.from_keyfile(kf);
82		}
83		catch(KeyFileError err) {
84		}
85		catch(GLib.FileError err) {
86		}
87	}
88
89	public FileAction.from_keyfile(KeyFile kf) {
90		this.from_key_file(kf); // chain up base constructor
91		type = FileActionType.ACTION;
92
93		if(Utils.key_file_get_bool(kf, "Desktop Entry", "TargetContext", true))
94			target |= FileActionTarget.CONTEXT;
95		if(Utils.key_file_get_bool(kf, "Desktop Entry", "TargetLocation"))
96			target |= FileActionTarget.LOCATION;
97		if(Utils.key_file_get_bool(kf, "Desktop Entry", "TargetToolbar"))
98			target |= FileActionTarget.TOOLBAR;
99		toolbar_label = Utils.key_file_get_locale_string(kf, "Desktop Entry", "ToolbarLabel");
100
101		string[] profile_names = Utils.key_file_get_string_list(kf, "Desktop Entry", "Profiles");
102		if(profile_names != null) {
103			foreach(string profile_name in profile_names) {
104				// stdout.printf("%s", profile);
105				profiles.prepend(new FileActionProfile(kf, profile_name.strip()));
106			}
107			profiles.reverse();
108		}
109	}
110
111	public bool match(List<FileInfo> files, out unowned FileActionProfile matched_profile) {
112		matched_profile = null;
113		// stdout.printf("FileAction.match: %s\n", id);
114		if(hidden || !enabled)
115			return false;
116
117		if(!condition.match(files))
118			return false;
119		foreach(unowned FileActionProfile profile in profiles) {
120			if(profile.match(files)) {
121				matched_profile = profile;
122				// stdout.printf("  profile matched!\n\n");
123				return true;
124			}
125		}
126		// stdout.printf("\n");
127		return false;
128	}
129
130	public FileActionTarget target;
131	public string? toolbar_label;
132
133	// FIXME: currently we don't support dynamic profiles
134	public List<FileActionProfile> profiles;
135}
136
137public class FileActionMenu : FileActionObject {
138
139	public FileActionMenu(string desktop_id) {
140		var kf = new KeyFile();
141		id = desktop_id;
142		try {
143			kf.load_from_file(desktop_id, 0);
144			this.from_keyfile(kf);
145		}
146		catch(KeyFileError err) {
147		}
148		catch(GLib.FileError err) {
149		}
150	}
151
152	public FileActionMenu.from_keyfile(KeyFile kf) {
153		this.from_key_file(kf); // chain up base constructor
154		type = FileActionType.MENU;
155
156		items_list = Utils.key_file_get_string_list(kf, "Desktop Entry", "ItemsList");
157	}
158
159	public bool match(List<FileInfo> files) {
160		// stdout.printf("FileActionMenu.match: %s\n", id);
161		if(hidden || !enabled)
162			return false;
163		if(!condition.match(files))
164			return false;
165		// stdout.printf("menu matched!: %s\n\n", id);
166		return true;
167	}
168
169	// called during menu generation
170	public void cache_children(List<FileInfo> files, string[] items_list) {
171		foreach(unowned string item_id_prefix in items_list) {
172			if(item_id_prefix[0] == '[' && item_id_prefix[item_id_prefix.length - 1] == ']') {
173				// runtime dynamic item list
174				string output;
175				int exit_status;
176				var command = FileActionParameters.expand(item_id_prefix[1:-1], files);
177				if(Process.spawn_command_line_sync(command, out output, null, out exit_status)
178					&& exit_status == 0) {
179					string[] item_ids = output.split(";");
180					cache_children(files, item_ids);
181				}
182			}
183			else if(item_id_prefix == "SEPARATOR") {
184				// separator item
185				cached_children.append(null);
186			}
187			else {
188				string item_id = @"$item_id_prefix.desktop";
189				FileActionObject child_action = all_actions.lookup(item_id);
190				if(child_action != null) {
191					child_action.has_parent = true;
192					cached_children.append(child_action);
193					// stdout.printf("add child: %s to menu: %s\n", item_id, id);
194				}
195			}
196		}
197	}
198
199	public string[]? items_list;
200
201	// values cached during menu generation
202	public List<FileActionObject> cached_children;
203}
204
205public class FileActionItem {
206
207	public static FileActionItem? new_for_action_object(FileActionObject action_obj, List<FileInfo> files) {
208		FileActionItem item = null;
209		if(action_obj.type == FileActionType.MENU) {
210			var menu = (FileActionMenu)action_obj;
211			if(menu.match(files)) {
212				item = new FileActionItem.from_menu(menu, files);
213				// eliminate empty menus
214				if(item.children == null)
215					item = null;
216			}
217		}
218		else {
219			// handle profiles here
220			var action = (FileAction)action_obj;
221			unowned FileActionProfile profile;
222			if(action.match(files, out profile)) {
223				item = new FileActionItem.from_action(action, profile, files);
224			}
225		}
226		return item;
227	}
228
229	public FileActionItem.from_action(FileAction action, FileActionProfile profile, List<FileInfo> files) {
230		this(action, files);
231		this.profile = profile;
232	}
233
234	public FileActionItem.from_menu(FileActionMenu menu, List<FileInfo> files) {
235		this(menu, files);
236		foreach(FileActionObject action_obj in menu.cached_children) {
237			if(action_obj == null) { // separator
238				children.append(null);
239			}
240			else { // action item or menu
241				FileActionItem subitem = new_for_action_object(action_obj, files);
242				if(subitem != null)
243					children.append(subitem);
244			}
245		}
246	}
247
248	private FileActionItem(FileActionObject action, List<FileInfo> files) {
249		this.action = action;
250		name = FileActionParameters.expand(action.name, files, true);
251		desc = FileActionParameters.expand(action.desc, files, true);
252		icon = FileActionParameters.expand(action.icon, files, false);
253	}
254
255	public unowned string? get_name() {
256		return name;
257	}
258
259	public unowned string? get_desc() {
260		return desc;
261	}
262
263	public unowned string? get_icon() {
264		return icon;
265	}
266
267	public unowned string get_id() {
268		return action.id;
269	}
270
271	public FileActionTarget get_target() {
272		if(action.type == FileActionType.ACTION)
273			return ((FileAction)action).target;
274		return FileActionTarget.NONE;
275	}
276
277	public bool is_menu() {
278		return (action.type == FileActionType.MENU);
279	}
280
281	public bool is_action() {
282		return (action.type == FileActionType.ACTION);
283	}
284
285	public bool launch(AppLaunchContext ctx, List<FileInfo> files, out string? output) {
286		if(action.type == FileActionType.ACTION) {
287			if(profile != null) {
288				profile.launch(ctx, files, out output);
289			}
290			return true;
291		}
292		return false;
293	}
294
295	public unowned List<FileActionItem>? get_sub_items() {
296		if(action != null && action.type == FileActionType.MENU)
297			return children;
298		return null;
299	}
300
301	public string? name;
302	public string? desc;
303	public string? icon;
304	public FileActionObject action;
305	public unowned FileActionProfile profile; // only used by action item
306	public List<FileActionItem>? children; // only used by menu
307}
308
309
310private void load_actions_from_dir(string dirname, string? id_prefix) {
311	try {
312		// stdout.printf("loading from: %s\n", dirname);
313		var dir = Dir.open(dirname);
314		if(dir != null) {
315			weak string? name;
316			var kf = new KeyFile();
317			for(;;) {
318				name = dir.read_name();
319				if(name == null)
320					break;
321				// found a file in file-manager/actions dir, get its full path
322				var full_path = GLib.Path.build_filename(dirname, name);
323				// stdout.printf("\nfound %s\n", full_path);
324
325				// see if it's a sub dir
326				if(FileUtils.test(full_path, FileTest.IS_DIR)) {
327					// load sub dirs recursively
328					load_actions_from_dir(full_path, id_prefix != null ? @"$id_prefix-$name" : name);
329				}
330				else if (name.has_suffix(".desktop")) {
331					string id = id_prefix != null ? @"$id_prefix-$name" : name;
332					// ensure that it's not already in the cache
333					if(all_actions.lookup(id) == null) {
334						if(kf.load_from_file(full_path, 0)) {
335							string? type = Utils.key_file_get_string(kf, "Desktop Entry", "Type");
336							FileActionObject action = null;
337							if(type == null || type == "Action") {
338								action = new FileAction.from_keyfile(kf);
339								// stdout.printf("load action: %s\n", id);
340							}
341							else if(type == "Menu") {
342								action = new FileActionMenu.from_keyfile(kf);
343								// stdout.printf("load menu: %s\n", id);
344							}
345							else {
346								continue;
347							}
348							action.id = id;
349							all_actions.insert(id, action); // add the id/action pair to hash table
350							// stdout.printf("add to cache %s\n", id);
351						}
352					}
353					else {
354						// stdout.printf("cache found for action: %s\n", id);
355					}
356				}
357			}
358		}
359	}
360	catch(GLib.FileError err) {
361	}
362}
363
364
365public List<FileActionItem>? get_actions_for_files(List<Fm.FileInfo> files) {
366	if(!actions_loaded)
367		load_all_actions();
368
369	// Iterate over all actions to establish association between parent menu
370	// and children actions, and to find out toplevel ones which are not
371	// attached to any parent menu
372	var action_it = HashTableIter<string, FileActionObject>(all_actions);
373	FileActionObject action_obj = null;
374	while(action_it.next(null, out action_obj)) {
375		// stdout.printf("id = %s\n", action_obj.id);
376		if(action_obj.type == FileActionType.MENU) { // this is a menu
377			FileActionMenu menu = (FileActionMenu)action_obj;
378			// stdout.printf("menu: %s\n", menu.name);
379			// associate child items with menus
380			menu.cache_children(files, menu.items_list);
381		}
382	}
383
384	// Output the menus
385	var items = new List<FileActionItem>();
386
387	action_it = HashTableIter<string, FileActionObject>(all_actions);
388	action_obj = null;
389	while(action_it.next(null, out action_obj)) {
390		// only output toplevel items here
391		if(action_obj.has_parent == false) { // this is a toplevel item
392			FileActionItem item = FileActionItem.new_for_action_object(action_obj, files);
393			if(item != null)
394				items.append(item);
395		}
396	}
397
398	// cleanup temporary data cached during menu generation
399	action_it = HashTableIter<string, FileActionObject>(all_actions);
400	action_obj = null;
401	while(action_it.next(null, out action_obj)) {
402		action_obj.has_parent = false;
403		if(action_obj.type == FileActionType.MENU) {
404			FileActionMenu menu = (FileActionMenu)action_obj;
405			menu.cached_children = null;
406		}
407	}
408	return items;
409}
410
411private void load_all_actions() {
412	all_actions.remove_all();
413	weak string[] dirs = Environment.get_system_data_dirs();
414	foreach(weak string dir in dirs) {
415		load_actions_from_dir(GLib.Path.build_filename(dir, "file-manager/actions"), null);
416	}
417	load_actions_from_dir(GLib.Path.build_filename(Environment.get_user_data_dir(),
418				  "file-manager/actions"), null);
419	actions_loaded = true;
420}
421
422public void file_actions_set_desktop_env(string env) {
423	desktop_env = env;
424}
425
426}
427
428namespace _Fm {
429
430public void file_actions_init() {
431	Fm.all_actions = new HashTable<string, Fm.FileActionObject>(str_hash, str_equal);
432}
433
434public void file_actions_finalize() {
435	Fm.all_actions = null;
436}
437
438}
439