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