From 52c4117482310afbe22b0c4327404ec3cf11819a Mon Sep 17 00:00:00 2001 From: Cristiano Nunes Date: Tue, 3 Nov 2026 20:44:02 -0300 Subject: [PATCH] Add optional Submenu path for grouping contract actions Contracts can declare a localized Submenu key so clients nest matching items. New D-Bus methods return ContractMenuEntry with that path; older methods stay a flat list. --- README.md | 3 +++ src/Contract.vala | 21 ++++++++++++++++++ src/ContractKeyFile.vala | 18 +++++++++++++++ src/DBusService.vala | 47 ++++++++++++++++++++++++++++++++++++++++ src/GenericContract.vala | 12 ++++++++++ 5 files changed, 101 insertions(+) diff --git a/README.md b/README.md index a8624fa..b6f18d4 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Exec=gnome-disk-image-mounter %f - `Name`: Text displayed in the right click context menu. - `MimeType`: Mimetype(s) of files it should be shown for. - `Exec`: Command to execute. [More info](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables). +- `Submenu`: Optional menu path. Contracts that share the same value are grouped under that submenu by the client. `/` separates nested labels (`Archive/Compress`). A missing or empty value keeps the item at the top level. Localized keys work the same way as `Name` (`Submenu[pt_BR]=Arquivo`). The Contractor service returns the translated path; the client draws the submenu and hides a group when none of its contracts match the current files. + +`Submenu` is available from `list_all_contract_entries` and `get_contract_entries_by_mimelist_and_file_size`. The older methods still return a flat list without this field. Tips: - Use `pkexec` to ask for root permissions. Example: `Exec=pkexec chmod +x %U` diff --git a/src/Contract.vala b/src/Contract.vala index a6cba1d..e9954a3 100644 --- a/src/Contract.vala +++ b/src/Contract.vala @@ -31,6 +31,11 @@ namespace Contractor { public string name { get; private set; } public string icon { get; private set; default = ""; } public string description { get; private set; default = ""; } + /** + * optional submenu path displayed by clients. Empty means a top-level + * item. '/' separates nested menu labels. + */ + public string submenu { get; private set; default = ""; } /** * the maximal file size a file or list of files are allowed to have to * be applicable for this contract @@ -101,6 +106,20 @@ namespace Contractor { }; } + /** + * creates and returns a ContractMenuEntry filled with data from this + * Contract, including the optional submenu path + */ + public ContractMenuEntry get_menu_entry () { + return ContractMenuEntry () { + id = id, + name = name, + description = description, + icon = icon, + submenu = submenu + }; + } + /** * loads mandatory fields from the key file */ @@ -132,6 +151,8 @@ namespace Contractor { } catch (Error err) { debug ("Contract '%s' does not provide a max file size (%s)", id, err.message); } + + submenu = keyfile.get_submenu (); } } } diff --git a/src/ContractKeyFile.vala b/src/ContractKeyFile.vala index ab96356..d0d81cc 100644 --- a/src/ContractKeyFile.vala +++ b/src/ContractKeyFile.vala @@ -24,6 +24,7 @@ public class Contractor.ContractKeyFile : Object { private const string ICON_KEY = KeyFileDesktop.KEY_ICON; private const string MIMETYPE_KEY = KeyFileDesktop.KEY_MIME_TYPE; private const string MAX_FILE_SIZE_KEY = "MaxFileSize"; + private const string SUBMENU_KEY = "Submenu"; private const string EXEC_KEY = KeyFileDesktop.KEY_EXEC; private const string TRY_EXEC_KEY = KeyFileDesktop.KEY_TRY_EXEC; @@ -90,6 +91,23 @@ public class Contractor.ContractKeyFile : Object { return get_locale_string (DESCRIPTION_KEY); } + /** + * gets the optional submenu path from the key file + * + * A missing or empty key means the contract stays at the top level. + * '/' separates nested menu labels. The returned string is localized + * the same way as the contract name. + * + * @return the submenu path, or an empty string when unset + */ + public string get_submenu () { + try { + return get_locale_string (SUBMENU_KEY); + } catch (Error err) { + return ""; + } + } + /** * gets the contracts icon from the key file * diff --git a/src/DBusService.vala b/src/DBusService.vala index 3d6f698..2e80516 100644 --- a/src/DBusService.vala +++ b/src/DBusService.vala @@ -152,6 +152,37 @@ namespace Contractor { return convert_to_generic_contracts (contracts); } + /** + * Gets every contract, including the optional submenu path. + * Clients group entries that share the same submenu string. + * An empty submenu means a top-level item. + * + * @return an array of ContractMenuEntry containing all contracts + */ + public ContractMenuEntry[] list_all_contract_entries () { + var contracts = contract_source.get_contracts (); + return convert_to_contract_menu_entries (contracts); + } + + /** + * Gets contracts filtered by MIME types and file size, including the + * optional submenu path. + * + * Pass file_size -1 to skip the file size filter. A submenu is only + * relevant for contracts that pass these filters; clients should group + * the returned entries and omit empty groups. + * + * @param mime_types an array of MIME type strings, e.g. text, image + * @param file_size the file size in bytes, or -1 for any size + * + * @return an array of ContractMenuEntry + */ + public ContractMenuEntry[] get_contract_entries_by_mimelist_and_file_size (string[] mime_types, int64 file_size) throws Error { + var all_contracts = contract_source.get_contracts (); + var contracts = ContractMatcher.get_contracts_for_types_and_file_size (mime_types, file_size, all_contracts); + return convert_to_contract_menu_entries (contracts); + } + /** * Converts a Collection of Contracts into an array of * GenericContracts. @@ -167,5 +198,21 @@ namespace Contractor { return generic_contracts; } + + /** + * Converts a Collection of Contracts into an array of + * ContractMenuEntry values, including each submenu path. + * + * @return an array of ContractMenuEntry + */ + private static ContractMenuEntry[] convert_to_contract_menu_entries (Gee.Collection contracts) { + var entries = new ContractMenuEntry[0]; + + foreach (var contract in contracts) { + entries += contract.get_menu_entry (); + } + + return entries; + } } } diff --git a/src/GenericContract.vala b/src/GenericContract.vala index e5290b9..250f021 100644 --- a/src/GenericContract.vala +++ b/src/GenericContract.vala @@ -21,3 +21,15 @@ public struct Contractor.GenericContract { string description; string icon; } + +/** + * A contract exposed to clients that can group actions into submenus. + * submenu is empty for a top-level item. '/' separates nested labels. + */ +public struct Contractor.ContractMenuEntry { + string id; + string name; + string description; + string icon; + string submenu; +}