GEPS 044: Replace Deprecated Gtk.UIManager
This GEPS (Gramps Enhancement Proposal) is closed and available in the version of Gramps indicated below.
Implemented for the release of Gramps 5.1
Goal: Rewrite UIManager code to avoid using deprecated methods
I take this goal to mean upgrading several of the currently used Gtk techniques that have been deprecated to the more modern methods. After looking around the web, it appears that there are few write-ups on how this might be done. The best advice available I found is summarized below.
- Gtk.UIManager is deprecated, use Gtk.Builder instead. UIManager was used to define many of the menus for Gramps using a specialized syntax. Gtk.Builder utilizes an XML input and syntax to define the menus.
- Gtk.Action is deprecated, use GLib.SimpleAction. Actions are used to separate the Menu items (labels, accelerator, Mnemonics etc.) from what they do.
- Gtk.ActionGroup is deprecated, use GLib.SimpleActionGroup. The original idea of ActionGroup was to group related menu items together; Gramps used this mechanism to enable/disable groups of menu items when they were not applicable, for instance most menu items were disabled (invisible) when no tree was opened. Sometimes the groups were set insensitive (greyed out) as well.
The new SimpleActionGroup can NOT be used in this way.
The new mechanisms seem to require the use of Gtk.Application and Gtk.ApplicationWindow (at least to avoid other warnings). Using these will involve some changes to our startup code and main window code.
Gtk.Application includes the possibly of a multi document type of interface, potentially with several trees open at once. I propose that for initial work, we do NOT support this. I propose we continue to operate Gramps as a single instance application, where attempts to start another instance only bring the GUI to the front.
Gtk.Application also has several types of support for CLI. For this GEPS I intend to continue letting Gramps deal with the CLI before starting the Gtk.Application (when the GUI is needed). The Gtk.Application will not deal with CLI parameters.
Gtk.Application uses a (hopefully unique) application name to manage its features. I propose that we use "org.gramps-project.Gramps" (which is a valid name according to the rules).
The main goal is to utilize the new techniques instead of the deprecated ones. And to do this with minimal (hopefully no) changes to the GUI or functionality of Gramps.
- Code changes to support this proposal will have to be made to both Gramps, and its Addons, and synchronized to allow the Addon Views to work correctly. So there will be a minimum of two PRs, one for Gramps, one for the Addons that have to work in concert. For the user, this will have to be part of a larger upgrade, probably a 5.x (5.1?) with the addon repository having a gramps51 branch, all with the same release timing.
- In order to support some of our accelerators (the ones that don't have a menu entry, like view switching '<ctrl>p' etc.) it seems we should use Gtk.Application.set_accels_for_action. However this requires Gtk 3.12, we are currently specifying Gtk 3.10 as our base requirement. It may be possible to use Gtk.Application.add_accelerator instead, however it has been deprecated at 3.14. See 5.1_Roadmap#Dependency_upgrades
- We have the possibility to support user defined keyboard accelerators via a file ('gramps.accel') in the DATA_DIR. The MAC OSX installation uses this to remap some keys by copying the file to the appropriate place. With the changes to accelerators, the format of this file has significantly changed. I have fixed up the MAC version to work correctly, but any user file will no longer work. Maybe we should change the name so we don't get crashes if a user has defined the file.
- Icons next to some of our Menu items are not supported by the GIO menu items. I note that these have been gone from recent Gtk versions anyway. So only users who use older Gtk versions should see a difference.
- Current code has tooltips on Menu entries; I cannot get these to pop up on my Windows/Linux systems, so I suspect that this support has been deprecated in recent Gtk versions. The GIO menu entries do NOT support Tooltips, so I will be removing these from the code.
- Some of the popup menu entries (Events view) did not have the accelerators shown; Using the unified Gio.Actions with accelerators makes them appear now.
- The main toolbar included text labels in the code. These can show if the main window is made small enough that not all the toolbar items will fit, they then appear in a small drop-down menu at the end of the toolbar. I suspect that it is possible to make these show via some Theme setting, although I was unable to find a way to do this. To enable testing, I added a config setting to show the labels as a separate commit, it has to be enabled by editing gramps.ini, I did not add a 'Preferences' item to match.
A new Gramps UIManager class, singleton, which operates the main menu and toolbar, making appropriate changes for different views, modes etc. A new Gramps ActionGroup class which tracks the items in an action group and its name.
Following are the headers for these proposed Classes/methods. These should be regarded as preliminary, subject to change as coding develops.
class ActionGroup(): """ This class represents a group of actions that con be manipulated together. """ def __init__(self, name, actionlist=None, prefix='win'): """ @param name: the action group name, used to match to the 'groups' attribute in the ui xml. @type name: string @type actionlist: list @param actionlist: the list of actions to add The list contains tuples with the following contents: string: Action Name method: signal callback function. None if just adding an accelerator string: accelerator ex: '<Primary>Enter' or '' for no accelerator. optional for non-stateful actions. state: initial state for stateful actions. 'True' or 'False': the action is interpreted as a checkbox. 'None': non stateful action (optional) 'string': the action is interpreted as a Radio button @type prefix: str @param prefix: the prefix used by this group. If not provided, 'win' is assumed. """ def add_actions(self, actionlist): """ Add a list of actions to the current list @type actionlist: list @param actionlist: the list of actions to add """ class UIManager(): """ This is Gramps UIManager, it is designed to replace the deprecated Gtk UIManager. The replacement is not exact, but performs similar functions, in some case with the same method names and parameters. It is designed to be a singleton. The menu portion of this is responsible only for Gramps main window menus and toolbar. This was implemented to extend Gtk.Builder functions to allow editing (merging) of the original builder XML with additional XML fragments during operations. This allows changing of the menus and toolbar when the tree is loaded, views are changed etc. The ActionGroup portions can also be used by other windows. Other windows needing menus or toolbars can create them via Gtk.Builder. """ def __init__(self, app, initial_xml): """ @param app: Gramps Gtk.Application reference @type app: Gtk.Application @param initial_xml: Initial (primary) XML string for Gramps menus and toolbar @type changexml: string The xml is basically Gtk Builder xml, in particular the various menu and toolbar elements. It is possible to add other elements as well. The xml this supports has been extended in two ways; 1) there is an added "groups=" attribute to elements. This attribute associates the element with one or more named ActionGroups for making the element visible or not. If 'groups' is missing, the element will be shown as long as enclosing elements are shown. The element will be shown if the group is present and considered visible by the uimanager. If more than one group is needed, they should be separated by a space. 2) there is an added <placeholder> tag supported; this is used to mark a place where merged UI XML can be inserted. During the update_menu processing, elements enclosed in this tag pair are promoted to the level of the placeholder tag, and the placeholder tag is removed. Note that any elements can be merged (replaced) by the add_ui_from_string method, not just placeholders. This works by matching the "id=" attribute on the element, and replacing the original element with the one from the add method. Note that when added elements are removed by the remove_ui method, they are replaced by the containing xml (with the 'id=') only, so don't put anything inside of the containing xml to start with as it will be lost during editing. """ def update_menu(self, init=False): """ This updates the menus and toolbar when there is a change in the ui; any addition or removal or set_visible operation needs to call this. It is best to make the call only once, at the end, if multiple changes are to be made. It starts with the ET xml stored in self, cleans it up to meet the Gtk.Builder specifications, and then updates the ui. @param init: When True, this is first call and we set the builder toolbar and menu to the application. When False, we update the menus and toolbar @type init: bool """ def add_ui_from_string(self, changexml): """ This performs a merge operation on the xml elements that have matching 'id's between the current ui xml and change xml strings. The 'changexml' is a list of xml strings used to replace matching elements in the current xml. There MUST one and only one matching id in the orig xml. @param changexml: list of xml fragments to merge into main @type changexml: list @return: changexml """ def remove_ui(self, change_xml): """ This removes the 'change_xml' from the current ui xml. It works on any element with matching 'id', the actual element remains but any children are removed. The 'change_xml' is a list of xml strings originally used to replace matching elements in the current ui xml. @param change_xml: list of xml fragments to remove from main @type change_xml: list """ def get_widget(self, obj): """ Get the object from the builder. @param obj: the widget to get @type obj: string @return: the object """ def insert_action_group(self, group, gio_group=None): """ This inserts (actually overwrites any matching actions) the action group's actions to the app. By default (with no gio_group), the action group is added to the main Gramps window and the group assumes a 'win' prefix. If not using the main window, the window MUST have the 'application' property set for the accels to work. In this case the actiongroups must be created like the following: # create Gramps ActionGroup self.action_group = ActionGroup('name', actions, 'prefix') # create Gio action group act_grp = SimpleActionGroup() # associate window with Gio group and its prefix window.insert_action_group('prefix', act_grp) # make the window 'application' aware window.set_application(uimanager.app) # tell the uimanager about the groups. uimanager.insert_action_group(self.action_group, act_grp) @param group: the action group @type group: ActionGroup @param gio_group: the Gio action group associated with a window. @type gio_group: Gio.SimpleActionGroup """ def remove_action_group(self, group): """ This removes the ActionGroup from the UIManager @param group: the action group @type group: ActionGroup """ def get_action_groups(self): """ This returns a list of action Groups installed into the UIManager. @return: list of groups """ def set_action_group_sensitive(self, group, value): """ This sets an ActionGroup enabled or disabled. A disabled action will be greyed out in the UI. @param group: the action group @type group: ActionGroup @param value: the state of the group @type value: bool """ def get_actions_sensitive(self, group): """ This gets an ActionGroup sensitive setting. A disabled action will be greyed out in the UI. We assume that the first action represents the group. @param group: the action group @type group: ActionGroup @return: the state of the group """ def set_actions_visible(self, group, value): """ This sets an ActionGroup visible and enabled or invisible and disabled. Make sure that the menuitems or sections and toolbar items have the 'groups=' xml attribute matching the group name for this to work correctly. @param group: the action group @type group: ActionGroup @param value: the state of the group @type value: bool """ def get_action(self, group, actionname): """ Return a single action from the group. @param group: the action group @type group: ActionGroup @param actionname: the action name @type actionname: string @return: Gio.Action """ def dump_all_accels(self): ''' A function used diagnostically to see what accels are present. This will only dump the current accel set, if other non-open windows or views have accels, you will need to open them and run this again and manually merge the result files. The results are in a 'gramps.accel' file located in the current working directory.''' def load_accels(self, filename): """ This function loads accels from a file such as created by dump_all_accels. The file contents is basically a Python dict definition. As such it contains a line for each dict element. These elements can be commented out with '#' at the beginning of the line. If used, this file overrides the accels defined in other Gramps code. As such it must be loaded before any insert_action_group calls. """
The StyledTextEditor used Gtk.UIManager etc. to implement its toolbar and menu. This was re-coded to use the new Gtk.Builder methods directly, rather than generalizing the proposed Gramps UIManager class to handle multiple instances on different Windows. The ActionGroup methods of the new Gramps UIManager class were generalized to allow use with different windows (including the StyledTextEditor).
The EditPrimary and EditPerson classes used Gtk.UIManager etc. to implement its popup menu. This was re-coded to use the new Gtk.Builder methods directly, rather than generalizing the proposed Gramps UIManager class to handle multiple instances on different Windows.