From 9c6b6e780fe76b6854cdc47935eded95c337a239 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Fri, 2 Aug 2024 16:04:40 +0100 Subject: [PATCH 01/42] Remove dead_ends for config-edit and edit. Add in entry point for rose config-edit and an alias for edit. --- metomi/rose/rose.py | 12 +++--------- setup.cfg | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/metomi/rose/rose.py b/metomi/rose/rose.py index 85460443c..a33d017c8 100644 --- a/metomi/rose/rose.py +++ b/metomi/rose/rose.py @@ -95,14 +95,6 @@ def iter_entry_points(name: str): # (ns, sub_cmd): message ('rosa', 'rpmbuild'): 'Rosa RPM Builder has been removed.', - ('rose', 'config-edit'): ( - 'The Rose configuration editor has been removed. The old ' - 'Rose 2019 GUI remains compatible with Rose 2 configurations.' - ), - ('rose', 'edit'): ( - 'The Rose configuration editor has been removed. The old ' - 'Rose 2019 GUI remains compatible with Rose 2 configurations.' - ), ('rose', 'metadata-graph'): 'This command has been removed pending re-implementation', ('rose', 'suite-clean'): @@ -148,7 +140,9 @@ def iter_entry_points(name: str): ('rosie', 'co'): ('rosie', 'checkout'), ('rosie', 'copy'): - ('rosie', 'create') + ('rosie', 'create'), + ('rose', 'edit'): + ('rose', 'config-edit') } # fmt: on diff --git a/setup.cfg b/setup.cfg index 608e65b5f..04c368c33 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,6 +110,7 @@ rose.commands = config = metomi.rose.config_cli:main config-diff = metomi.rose.config_diff:main config-dump = metomi.rose.config_dump:main + config-edit = metomi.rose.config_editor.main:main date = metomi.rose.date_cli:main env-cat = metomi.rose.env_cat:main host-select = metomi.rose.host_select:main From 52ce0701d471bce887e8a751d383cf7005dbbbc2 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Fri, 2 Aug 2024 16:06:29 +0100 Subject: [PATCH 02/42] Add config_editor dir created by running 2to3 and the pygobject conversion script on Rose 2019. --- metomi/rose/config_editor/__init__.py | 770 +++++++ metomi/rose/config_editor/data.py | 1275 +++++++++++ metomi/rose/config_editor/data_helper.py | 495 ++++ metomi/rose/config_editor/keywidget.py | 486 ++++ metomi/rose/config_editor/main.py | 2024 +++++++++++++++++ metomi/rose/config_editor/menu.py | 1026 +++++++++ metomi/rose/config_editor/menuwidget.py | 346 +++ metomi/rose/config_editor/nav_controller.py | 143 ++ metomi/rose/config_editor/nav_panel.py | 612 +++++ metomi/rose/config_editor/nav_panel_menu.py | 532 +++++ metomi/rose/config_editor/ops/__init__.py | 0 metomi/rose/config_editor/ops/group.py | 468 ++++ metomi/rose/config_editor/ops/section.py | 317 +++ metomi/rose/config_editor/ops/variable.py | 434 ++++ metomi/rose/config_editor/page.py | 1192 ++++++++++ .../rose/config_editor/pagewidget/__init__.py | 21 + metomi/rose/config_editor/pagewidget/table.py | 366 +++ .../config_editor/panelwidget/__init__.py | 22 + .../config_editor/panelwidget/filesystem.py | 125 + .../config_editor/panelwidget/summary_data.py | 865 +++++++ metomi/rose/config_editor/plugin/__init__.py | 19 + .../rose/config_editor/plugin/um/__init__.py | 19 + .../plugin/um/widget/__init__.py | 19 + .../config_editor/plugin/um/widget/stash.py | 868 +++++++ .../plugin/um/widget/stash_add.py | 694 ++++++ .../plugin/um/widget/stash_util.py | 31 + metomi/rose/config_editor/stack.py | 203 ++ metomi/rose/config_editor/status.py | 261 +++ metomi/rose/config_editor/updater.py | 760 +++++++ .../rose/config_editor/upgrade_controller.py | 286 +++ metomi/rose/config_editor/util.py | 227 ++ .../config_editor/valuewidget/__init__.py | 136 ++ .../valuewidget/array/__init__.py | 19 + .../config_editor/valuewidget/array/entry.py | 482 ++++ .../valuewidget/array/logical.py | 258 +++ .../config_editor/valuewidget/array/mixed.py | 414 ++++ .../valuewidget/array/python_list.py | 454 ++++ .../config_editor/valuewidget/array/row.py | 469 ++++ .../valuewidget/array/spaced_list.py | 450 ++++ .../config_editor/valuewidget/boolradio.py | 88 + .../config_editor/valuewidget/booltoggle.py | 96 + .../config_editor/valuewidget/character.py | 143 ++ .../rose/config_editor/valuewidget/choice.py | 276 +++ .../config_editor/valuewidget/combobox.py | 77 + .../rose/config_editor/valuewidget/files.py | 130 ++ .../rose/config_editor/valuewidget/format.py | 137 ++ .../rose/config_editor/valuewidget/intspin.py | 118 + metomi/rose/config_editor/valuewidget/meta.py | 91 + .../config_editor/valuewidget/radiobuttons.py | 87 + .../rose/config_editor/valuewidget/source.py | 257 +++ metomi/rose/config_editor/valuewidget/text.py | 141 ++ .../config_editor/valuewidget/valuehints.py | 80 + metomi/rose/config_editor/variable.py | 548 +++++ metomi/rose/config_editor/window.py | 777 +++++++ 54 files changed, 20634 insertions(+) create mode 100644 metomi/rose/config_editor/__init__.py create mode 100644 metomi/rose/config_editor/data.py create mode 100644 metomi/rose/config_editor/data_helper.py create mode 100644 metomi/rose/config_editor/keywidget.py create mode 100644 metomi/rose/config_editor/main.py create mode 100644 metomi/rose/config_editor/menu.py create mode 100644 metomi/rose/config_editor/menuwidget.py create mode 100644 metomi/rose/config_editor/nav_controller.py create mode 100644 metomi/rose/config_editor/nav_panel.py create mode 100644 metomi/rose/config_editor/nav_panel_menu.py create mode 100644 metomi/rose/config_editor/ops/__init__.py create mode 100644 metomi/rose/config_editor/ops/group.py create mode 100644 metomi/rose/config_editor/ops/section.py create mode 100644 metomi/rose/config_editor/ops/variable.py create mode 100644 metomi/rose/config_editor/page.py create mode 100644 metomi/rose/config_editor/pagewidget/__init__.py create mode 100644 metomi/rose/config_editor/pagewidget/table.py create mode 100644 metomi/rose/config_editor/panelwidget/__init__.py create mode 100644 metomi/rose/config_editor/panelwidget/filesystem.py create mode 100644 metomi/rose/config_editor/panelwidget/summary_data.py create mode 100644 metomi/rose/config_editor/plugin/__init__.py create mode 100644 metomi/rose/config_editor/plugin/um/__init__.py create mode 100644 metomi/rose/config_editor/plugin/um/widget/__init__.py create mode 100644 metomi/rose/config_editor/plugin/um/widget/stash.py create mode 100644 metomi/rose/config_editor/plugin/um/widget/stash_add.py create mode 100644 metomi/rose/config_editor/plugin/um/widget/stash_util.py create mode 100644 metomi/rose/config_editor/stack.py create mode 100644 metomi/rose/config_editor/status.py create mode 100644 metomi/rose/config_editor/updater.py create mode 100644 metomi/rose/config_editor/upgrade_controller.py create mode 100644 metomi/rose/config_editor/util.py create mode 100644 metomi/rose/config_editor/valuewidget/__init__.py create mode 100644 metomi/rose/config_editor/valuewidget/array/__init__.py create mode 100644 metomi/rose/config_editor/valuewidget/array/entry.py create mode 100644 metomi/rose/config_editor/valuewidget/array/logical.py create mode 100644 metomi/rose/config_editor/valuewidget/array/mixed.py create mode 100644 metomi/rose/config_editor/valuewidget/array/python_list.py create mode 100644 metomi/rose/config_editor/valuewidget/array/row.py create mode 100644 metomi/rose/config_editor/valuewidget/array/spaced_list.py create mode 100644 metomi/rose/config_editor/valuewidget/boolradio.py create mode 100644 metomi/rose/config_editor/valuewidget/booltoggle.py create mode 100644 metomi/rose/config_editor/valuewidget/character.py create mode 100644 metomi/rose/config_editor/valuewidget/choice.py create mode 100644 metomi/rose/config_editor/valuewidget/combobox.py create mode 100644 metomi/rose/config_editor/valuewidget/files.py create mode 100644 metomi/rose/config_editor/valuewidget/format.py create mode 100644 metomi/rose/config_editor/valuewidget/intspin.py create mode 100644 metomi/rose/config_editor/valuewidget/meta.py create mode 100644 metomi/rose/config_editor/valuewidget/radiobuttons.py create mode 100644 metomi/rose/config_editor/valuewidget/source.py create mode 100644 metomi/rose/config_editor/valuewidget/text.py create mode 100644 metomi/rose/config_editor/valuewidget/valuehints.py create mode 100644 metomi/rose/config_editor/variable.py create mode 100644 metomi/rose/config_editor/window.py diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py new file mode 100644 index 000000000..2c91e44e6 --- /dev/null +++ b/metomi/rose/config_editor/__init__.py @@ -0,0 +1,770 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +r"""This package contains the code for the Rose config editor. + +This module contains constants that are only used in the config editor. + +To override constants at runtime, place a section: + +[rose-config-edit] + +in your site or user configuration file for Rose, convert the name +of the constants to lowercase, and place constant=value lines in the +section. For example, to override the "ACCEL_HELP_GUI" constant, you +could put the following in your site or user configuration: + +[rose-config-edit] +accel_help_gui="H" + +The values you enter will be cast by Python's ast.literal_eval, so: +foo=100 +will be cast to an integer, but: +bar="100" +will be cast to a string. + +Generate the user config options help by extracting the 'User-relevant' +flagged blocks of text, e.g. via: + +sed -n '/^# User-relevant: /,/^$/p' __init__.py | \ + sed -n '/User-relevant/d; /^$/d; N; '\ +'s/^# \(.*\)\n\(^[^#].*\) = \(.*\)/'\ +'\

\2\E=\3\<\/h4\>\\1\<\/p\>\n/p;' | sort + +Use this text to update the doc/etc/rose-rug-config-edit/rose.conf.html +text, remembering to add the [rose-config-edit] section. + +""" + +import ast +import os +import sys + +from rose.resource import ResourceLocator + +# Accelerators +# Keyboard shortcut mappings. +ACCEL_NEW = "N" +ACCEL_OPEN = "O" +ACCEL_SAVE = "S" +ACCEL_QUIT = "Q" +ACCEL_UNDO = "Z" +ACCEL_REDO = "Z" +ACCEL_FIND = "F" +ACCEL_FIND_NEXT = "G" +ACCEL_METADATA_REFRESH = "F5" +ACCEL_BROWSER = "B" +ACCEL_SUITE_RUN = "R" +ACCEL_TERMINAL = "T" +ACCEL_HELP_GUI = "F1" +ACCEL_REMOVE = "Delete" +ACCEL_IGNORE = "I" + +# Menu or panel strings +ADD_MENU_BLANK = "Add blank variable" +ADD_MENU_BLANK_MULTIPLE = "Add blank variable..." +ADD_MENU_META = "Add latent variable" +ICON_PATH_SCHEDULER = None +TAB_MENU_CLOSE = "Close" +TAB_MENU_HELP = "Help" +TAB_MENU_EDIT = "Edit comments" +TAB_MENU_INFO = "Info" +TAB_MENU_OPEN_NEW = "Open in a new window" +TAB_MENU_WEB_HELP = "Web Help" +TOP_MENU_FILE = "_File" +TOP_MENU_FILE_CHECK_AND_SAVE = "_Check And Save" +TOP_MENU_FILE_LOAD_APPS = "_Load All Apps" +TOP_MENU_FILE_NEW = "_New" +TOP_MENU_FILE_OPEN = "_Open..." +TOP_MENU_FILE_SAVE = "_Save" +TOP_MENU_FILE_CLOSE = "_Close" +TOP_MENU_FILE_QUIT = "_Quit" +TOP_MENU_EDIT = "_Edit" +TOP_MENU_EDIT_UNDO = "_Undo" +TOP_MENU_EDIT_REDO = "_Redo" +TOP_MENU_EDIT_STACK = "Undo/Redo _Viewer" +TOP_MENU_EDIT_FIND = "_Find..." +TOP_MENU_EDIT_FIND_NEXT = "_Find Next" +TOP_MENU_EDIT_PREFERENCES = "_Preferences" +TOP_MENU_VIEW = "_View" +TOP_MENU_VIEW_LATENT_VARS = "View _Latent Variables" +TOP_MENU_VIEW_FIXED_VARS = "View _Fixed Variables" +TOP_MENU_VIEW_IGNORED_VARS = "View All _Ignored Variables" +TOP_MENU_VIEW_USER_IGNORED_VARS = "View _User Ignored Variables" +TOP_MENU_VIEW_LATENT_PAGES = "View Latent _Pages" +TOP_MENU_VIEW_IGNORED_PAGES = "View All _Ignored Pages" +TOP_MENU_VIEW_USER_IGNORED_PAGES = "View _User Ignored Pages" +TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS = "Hide Variable Descriptions" +TOP_MENU_VIEW_WITHOUT_HELP = "Hide Variable Help" +TOP_MENU_VIEW_WITHOUT_TITLES = "Hide Variable _Titles" +TOP_MENU_VIEW_CUSTOM_DESCRIPTIONS = "Use Custom _Description Format" +TOP_MENU_VIEW_CUSTOM_HELP = "Use Custom _Help Format" +TOP_MENU_VIEW_CUSTOM_TITLES = "Use Custom _Title Format" +TOP_MENU_VIEW_FLAG_OPT_CONF_VARS = "Flag Opt _Config Variables" +TOP_MENU_VIEW_FLAG_OPTIONAL_VARS = "Flag _Optional Variables" +TOP_MENU_VIEW_FLAG_NO_METADATA_VARS = "Flag _No-metadata Variables" +TOP_MENU_VIEW_STATUS_BAR = "View _Status Bar" +TOP_MENU_PAGE = "_Page" +TOP_MENU_PAGE_ADD = "_Add" +TOP_MENU_PAGE_REVERT = "_Revert to Saved" +TOP_MENU_PAGE_INFO = "_Info" +TOP_MENU_PAGE_HELP = "_Help" +TOP_MENU_PAGE_WEB_HELP = "_Web Help" +TOP_MENU_METADATA = "_Metadata" +TOP_MENU_METADATA_CHECK = "_Check fail-if, warn-if" +TOP_MENU_METADATA_GRAPH = "_Graph Metadata" +TOP_MENU_METADATA_MACRO_ALL_V = "Check All _Validator Macros" +TOP_MENU_METADATA_MACRO_AUTOFIX = "_Auto-fix all configurations" +TOP_MENU_METADATA_MACRO_CONFIG = "{0}" +TOP_MENU_METADATA_PREFERENCES = "Layout _Preferences" +TOP_MENU_METADATA_REFRESH = "_Refresh Metadata" +TOP_MENU_METADATA_LOAD = "Metadata Search Path..." +TOP_MENU_METADATA_SWITCH_OFF = "_Switch off Metadata" +TOP_MENU_METADATA_UPGRADE = "_Upgrade..." +TOP_MENU_TOOLS = "_Tools" +TOP_MENU_TOOLS_BROWSER = "Launch _File Browser" +TOP_MENU_TOOLS_SUITE_RUN = "_Run Suite" +TOP_MENU_TOOLS_SUITE_RUN_CUSTOM = "_Custom ..." +TOP_MENU_TOOLS_SUITE_RUN_DEFAULT = "_Default" +TOP_MENU_TOOLS_TERMINAL = "Launch _Terminal" +TOP_MENU_TOOLS_VIEW_OUTPUT = "View _Output" +TOP_MENU_HELP = "_Help" +TOP_MENU_HELP_GUI = "_Documentation" +TOP_MENU_HELP_ABOUT = "_About" +TOOLBAR_CHECK_AND_SAVE = "Check and save" +TOOLBAR_LOAD_APPS = "Load All Apps" +TOOLBAR_NEW = "New" +TOOLBAR_OPEN = "Open..." +TOOLBAR_SAVE = "Save" +TOOLBAR_BROWSE = "Browse files" +TOOLBAR_UNDO = "Undo" +TOOLBAR_REDO = "Redo" +TOOLBAR_ADD = "Add to page..." +TOOLBAR_REVERT = "Revert page to saved" +TOOLBAR_FIND = "Find expression (regex)" +TOOLBAR_FIND_NEXT = "Find next" +TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL = "Launch Suite Control _GUI" +TOOLBAR_TRANSFORM = "Auto-fix configurations (run built-in transform macros)" +TOOLBAR_VALIDATE = "Check fail-if, warn-if, and run all validator macros" +TOOLBAR_SUITE_GCONTROL = "Launch Suite Control GUI" +TOOLBAR_SUITE_RUN = "Run suite" +TOOLBAR_SUITE_RUN_MENU = "Run suite ..." +TOOLBAR_VIEW_OUTPUT = "View Output" +TREE_PANEL_TITLE = "Index" +TREE_PANEL_ADD_GENERIC = "_Add a new section..." +TREE_PANEL_ADD_SECTION = "_Add {0}" +TREE_PANEL_AUTOFIX_CONFIG = "_Auto-fix configuration" +TREE_PANEL_CLONE_SECTION = "_Clone this section" +TREE_PANEL_EDIT_SECTION = "Edit section comments..." +TREE_PANEL_ENABLE_GENERIC = "_Enable a section..." +TREE_PANEL_ENABLE_SECTION = "_Enable" +TREE_PANEL_GRAPH_SECTION = "_Graph Metadata" +TREE_PANEL_IGNORE_GENERIC = "_Ignore a section..." +TREE_PANEL_IGNORE_SECTION = "_Ignore" +TREE_PANEL_INFO_SECTION = "I_nfo" +TREE_PANEL_HELP_SECTION = "_Help" +TREE_PANEL_NEW_CONFIG = "_Create new configuration..." +TREE_PANEL_REMOVE_GENERIC = "Remove a section..." +TREE_PANEL_REMOVE_SECTION = "_Remove" +TREE_PANEL_RENAME_GENERIC = "Rename a section..." +TREE_PANEL_RENAME_SECTION = "_Rename" +TREE_PANEL_URL_SECTION = "_Web Help" +TREE_PANEL_KBD_TIMEOUT = 600 +MACRO_MENU_ALL_VALIDATORS = "All Validators" +MACRO_MENU_ALL_VALIDATORS_TIP = "Run all available validator macros." +VAR_MENU_ADD = "_Add to configuration" +VAR_MENU_EDIT_COMMENTS = "Edit _comments" +VAR_MENU_FIX_IGNORE = "Auto-Fix Error" +VAR_MENU_ENABLE = "_Enable" +VAR_MENU_HELP = "_Help" +VAR_MENU_IGNORE = "_User-Ignore" +VAR_MENU_INFO = "I_nfo" +VAR_MENU_REMOVE = "_Remove" +VAR_MENU_URL = "_Web Help" +# Button strings +LABEL_EDIT = "edit" +LABEL_PAGE_HELP = "Page help" +LABEL_PAGE_MACRO_BUTTON = "Macros" + +# Loading strings +EVENT_LOAD_CONFIG = "{0} - reading " +EVENT_LOAD_DONE = "{0} - loading GUI" +EVENT_LOAD_ERRORS = "{0} - errors: {1}" +EVENT_LOAD_METADATA = "{0} - configuring" +EVENT_LOAD_STATUSES = "{0} - checking " +LOAD_NUMBER_OF_EVENTS = 2 + +# Other event strings +EVENT_FOUND_ID = "Found {0}" +EVENT_INVALID_TRIGGERS = "{0}: triggers disabled" +EVENT_LOAD_ATTEMPT = "Attempting to load {0}" +EVENT_LOADED = "Loaded {0}" +EVENT_MACRO_CONFIGS = "{0} configurations" +EVENT_MACRO_TRANSFORM = "{1}: {0}: {2} changes" +EVENT_MACRO_TRANSFORM_ALL = "Transforms: {0}: {1} changes" +EVENT_MACRO_TRANSFORM_ALL_OK = "Transforms: {0}: no changes" +EVENT_MACRO_TRANSFORM_OK = "{1}: {0}: no changes" +EVENT_MACRO_VALIDATE = "{1}: {0}: {2} errors" +EVENT_MACRO_VALIDATE_ALL = "Custom Validators: {0}: {1} errors" +EVENT_MACRO_VALIDATE_ALL_OK = "Custom Validators: {0}: all OK" +EVENT_MACRO_VALIDATE_CHECK_ALL = ( + "Custom Validators, FailureRuleChecker: {0} total problems found") +EVENT_MACRO_VALIDATE_CHECK_ALL_OK = ( + "Custom Validators, FailureRuleChecker: No problems found") +EVENT_MACRO_VALIDATE_OK = "{1}: {0} is OK" +EVENT_MACRO_VALIDATE_NO_PROBLEMS = "Custom Validators: No problems found" +EVENT_MACRO_VALIDATE_PROBLEMS_FOUND = "Custom Validators: {0} problems found" +EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS = "FailureRuleChecker: No problems found" +EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND = ( + "FailureRuleChecker: {0} problems found") +EVENT_REDO = "{0}" +EVENT_REVERT = "Reverted {0}" +EVENT_TIME = "%H:%M:%S" +EVENT_TIME_LONG = "%Y-%m-%dT%H:%M:%S" +EVENT_UNDO = "{0}" +EVENT_UNDO_ACTION_ID = "{0} {1}" + +# Widget strings + +CHOICE_LABEL_EMPTY = "(empty)" +CHOICE_MENU_REMOVE = "Remove from list" +CHOICE_TIP_ENTER_CUSTOM = "Enter a custom choice" +CHOICE_TITLE_AVAILABLE = "Available" +CHOICE_TITLE_INCLUDED = "Included" + +# Error and warning strings +ERROR_ADD_FILE = "Could not add file {0}: {1}" +ERROR_BAD_FIND = "Bad search expression" +ERROR_BAD_NAME = "{0}: invalid name" +ERROR_BAD_MACRO_EXCEPTION = "Could not apply macro: error: {0}: {1}" +ERROR_BAD_MACRO_RETURN = "Bad return value for macro: {0}" +ERROR_BAD_TRIGGER = ("{0}\nfor {1}\n" + "from the configuration {2}. " + "\nDisabling triggers for this configuration.") +ERROR_CONFIG_CREATE = ("Error creating application config at {0}:" + + "\n {1}, {2}") +ERROR_CONFIG_CREATE_TITLE = "Error in creating configuration" +ERROR_CONFIG_DELETE = ("Error deleting application config at {0}:" + + "\n {1}, {2}") +ERROR_CONFIG_DELETE_TITLE = "Error in deleting configuration" +ERROR_ID_NOT_FOUND = "Could not find resource: {0}" +ERROR_FILE_DELETE_FAILED = "Delete failed. {0}" +ERROR_IMPORT_CLASS = "Could not retrieve class {0}" +ERROR_IMPORT_WIDGET = "Could not import widget: {0}" +ERROR_IMPORT_WIDGET_TITLE = "Error importing widget." +ERROR_LOAD_OPT_CONFS = "Could not load optional configurations:\n{0}" +ERROR_LOAD_OPT_CONFS_FORMAT = "{0}\n {1}: {2}\n" +ERROR_LOAD_OPT_CONFS_TITLE = "Error loading opt configs" +ERROR_LOAD_SYNTAX = "Could not load path: {0}\n\nSyntax error:\n{0}\n{1}" +ERROR_METADATA_CHECKER_TITLE = "Flawed metadata warning" +ERROR_METADATA_CHECKER_TEXT = ( + "{0} problem(s) found in metadata at {1}.\n" + + "Some functionality has been switched off.\n\n" + + "Run rose metadata-check for more info.") +ERROR_MIN_PYGTK_VERSION = "Requires PyGTK version {0}, found {1}." +ERROR_MIN_PYGTK_VERSION_TITLE = "Need later PyGTK version to run" +ERROR_NO_OUTPUT = "No output found for {0}" +ERROR_NOT_FOUND = "Could not find path: {0}" +ERROR_NOT_REGEX = "Could not compile expression: {0}\nError info: {1}" +ERROR_ORPHAN_SECTION = "Orphaned section: {0} will not be output at runtime." +ERROR_ORPHAN_SECTION_TIP = "Error: orphaned section!" +ERROR_REMOVE_FILE = "Could not remove file {0}: {1}" +ERROR_RUN_MACRO_TITLE = "Error in running {0}" +ERROR_SECTION_ADD = "Could not add section, already exists: {0}" +ERROR_SECTION_ADD_TITLE = "Error in adding section" +ERROR_SAVE_PATH_FAIL = "Could not save to path!\n {0}" +ERROR_SAVE_BLANK = "Cannot save configuration {0}.\nUnnamed variable in {1}" +ERROR_SAVE_TITLE = "Error saving {0}" +ERROR_UPGRADE = "Error: cannot upgrade {0}" +IGNORED_STATUS_CONFIG = "from configuration." +IGNORED_STATUS_DEFAULT = "from default." +IGNORED_STATUS_MANUAL = "from manual intervention." +IGNORED_STATUS_MACRO = "from macro." +PAGE_WARNING = "Error ({0}): {1}" +PAGE_WARNING_IGNORED_SECTION = "Ignored section: {0}" +PAGE_WARNING_IGNORED_SECTION_TIP = "Ignored section" +PAGE_WARNING_LATENT = "Latent page - no data" +PAGE_WARNING_NO_CONTENT = "Blank page - no data" +PAGE_WARNING_NO_CONTENT_TIP = ("No associated configuration or summary data " + + "for this page.") +WARNING_APP_CONFIG_CREATE = "Cannot create another configuration here." +WARNING_APP_CONFIG_CREATE_TITLE = "Warning - application configuration." +WARNING_CONFIG_DELETE = ("Cannot remove a whole configuration:\n{0}\n" + + "This must be done externally.") +WARNING_CONFIG_DELETE_TITLE = "Can't remove configuration" +WARNING_ERRORS_FOUND_ON_SAVE = "Errors found in {0}. Save anyway?" +WARNING_FILE_DELETE = ("Not a configuration file entry!\n" + + "This file must be manually removed" + + " in the filesystem:\n {0}.") +WARNING_FILE_DELETE_TITLE = "Can't remove filesystem file" +WARNING_CANNOT_ENABLE = "Warning - cannot override a trigger setting: {0}" +WARNING_CANNOT_ENABLE_TITLE = "Warning - can't enable" +WARNING_CANNOT_IGNORE = "Warning - cannot override a trigger setting: {0}" +WARNING_CANNOT_IGNORE_TITLE = "Warning - can't ignore" +WARNING_CANNOT_GRAPH = "Warning - graphing not possible" +WARNING_CANNOT_USER_IGNORE = "Warning - cannot override this setting: {0}" +WARNING_NOT_ENABLED = "Should be enabled from " +WARNING_NOT_FOUND = "No results" +WARNING_NOT_FOUND_TITLE = "Couldn't find it" +WARNING_NOT_IGNORED = "Should be ignored " +WARNING_NOT_TRIGGER = "Not part of the trigger mechanism" +WARNING_USER_NOT_TRIGGER_IGNORED = ( + "User-ignored, but should be trigger-ignored") +WARNING_NOT_USER_IGNORABLE = "User-ignored, but is compulsory" +WARNING_TYPE_ENABLED = "enabled" +WARNING_TYPE_TRIGGER_IGNORED = "trigger-ignored" +WARNING_TYPE_USER_IGNORED = "user-ignored" +WARNING_TYPE_NOT_TRIGGER = "trigger" +WARNING_TYPES_IGNORE = [WARNING_TYPE_ENABLED, WARNING_TYPE_TRIGGER_IGNORED, + WARNING_TYPE_USER_IGNORED, WARNING_TYPE_NOT_TRIGGER] +WARNING_INTEGER_OUT_OF_BOUNDS = "Warning: integer out of bounds" + +# Special metadata "type" values +FILE_TYPE_FORMATS = "formats" +FILE_TYPE_INTERNAL = "file_int" +FILE_TYPE_NORMAL = "file" +FILE_TYPE_TOP = "suite" + +META_PROP_INTERNAL = "_internal" + +# Setting visibility modes +SHOW_MODE_CUSTOM_DESCRIPTION = "custom-description" +SHOW_MODE_CUSTOM_HELP = "custom-help" +SHOW_MODE_CUSTOM_TITLE = "custom-title" +SHOW_MODE_FIXED = "fixed" +SHOW_MODE_FLAG_NO_META = "flag:no-meta" +SHOW_MODE_FLAG_OPT_CONF = "flag:optional-conf" +SHOW_MODE_FLAG_OPTIONAL = "flag:optional" +SHOW_MODE_IGNORED = "ignored" +SHOW_MODE_USER_IGNORED = "user-ignored" +SHOW_MODE_LATENT = "latent" +SHOW_MODE_NO_DESCRIPTION = "description" +SHOW_MODE_NO_HELP = "help" +SHOW_MODE_NO_TITLE = "title" + +# User-relevant: Defaults for the view and layout modes. +# Control showing a custom variable description format. +SHOULD_SHOW_CUSTOM_DESCRIPTION = False +# Control showing a custom variable help format. +SHOULD_SHOW_CUSTOM_HELP = False +# Control showing a custom variable title format. +SHOULD_SHOW_CUSTOM_TITLE = False +# Control flagging no-metadata variables. +SHOULD_SHOW_FLAG_NO_META_VARS = False +# Control flagging optional configuration variables. +SHOULD_SHOW_FLAG_OPT_CONF_VARS = True +# Control flagging non-compulsory variables. +SHOULD_SHOW_FLAG_OPTIONAL_VARS = False +# Control showing all comment text. +SHOULD_SHOW_ALL_COMMENTS = False +# Control showing fixed variables on a page. +SHOULD_SHOW_FIXED_VARS = True +# Control showing ! or !! ignored pages. +SHOULD_SHOW_IGNORED_PAGES = False +# Control showing ! or !! ignored variables on a page. +SHOULD_SHOW_IGNORED_VARS = False +# Control showing ! ignored pages. +SHOULD_SHOW_USER_IGNORED_PAGES = True +# Control showing ! ignored variables on a page. +SHOULD_SHOW_USER_IGNORED_VARS = True +# Control showing latent (potential) pages. +SHOULD_SHOW_LATENT_PAGES = False +# Control showing latent (potential) variables on a page. +SHOULD_SHOW_LATENT_VARS = False +# Control hiding the description text for variables on a page. +SHOULD_SHOW_NO_DESCRIPTION = False +# Control hiding the help text for variables on a page. +SHOULD_SHOW_NO_HELP = True +# Control hiding the title text for variables on a page. +SHOULD_SHOW_NO_TITLE = False +# Control showing the status bar. +SHOULD_SHOW_STATUS_BAR = True + +# User-relevant: Custom format strings for variable metadata display. +# Metadata representation strings: +# {name} gets replaced with the data/metadata property name. +# For example, you may want to have the description format as: +# "{name} - {description}" +# Configure the override custom format used for description. +CUSTOM_FORMAT_DESCRIPTION = "{name}: {description}" +# Configure the override custom format used for help. +CUSTOM_FORMAT_HELP = "{title}\n\n{help}" +# Configure the override custom format used for title. +CUSTOM_FORMAT_TITLE = "{title} ({name})" + +# User-relevant: Window sizing +# Configure the width of the LHS page tree in pixels. +WIDTH_TREE_PANEL = 256 +# Configure the width and height of the macro dialog in pixels (as a tuple). +SIZE_MACRO_DIALOG_MAX = (800, 600) +# Configure the width and height of the stack viewer in pixels (as a tuple). +SIZE_STACK = (800, 600) +# Configure the width and height of a detached tab in pixels (as a tuple). +SIZE_PAGE_DETACH = (650, 600) +# Configure the width and height of the config editor in pixels (as a tuple). +SIZE_WINDOW = (900, 600) +# Configure the pixel padding/spacing between config editor major items. +SPACING_PAGE = 10 +# Configure the pixel padding/spacing between config editor minor items. +SPACING_SUB_PAGE = 5 + +# Status bar configuration +STATUS_BAR_CONSOLE_TIP = "View more messages (Console)" +STATUS_BAR_CONSOLE_CATEGORY_ERROR = "Error" +STATUS_BAR_CONSOLE_CATEGORY_INFO = "Info" +STATUS_BAR_MESSAGE_LIMIT = 1000 +STATUS_BAR_VERBOSITY = 0 # Compare with rose.reporter.Reporter. + +# Stack action names and presentation +STACK_GROUP_ADD = "Add" +STACK_GROUP_COPY = "Copy" +STACK_GROUP_IGNORE = "Ignore" +STACK_GROUP_DELETE = "Delete" +STACK_GROUP_RENAME = "Rename" +STACK_GROUP_REORDER = "Reorder" + +STACK_ACTION_ADDED = "Added" +STACK_ACTION_APPLIED = "Applied" +STACK_ACTION_CHANGED = "Changed" +STACK_ACTION_CHANGED_COMMENTS = "Changed #" +STACK_ACTION_ENABLED = "Enabled" +STACK_ACTION_IGNORED = "Ignored" +STACK_ACTION_REMOVED = "Removed" +STACK_ACTION_REVERSED = "Reversed" + +# User-relevant: Undo/Redo Stack Viewer Colours +# Configure the colour for 'added' action. +COLOUR_STACK_ADDED = "green" +# Configure the colour for 'applied a diff' action. +COLOUR_STACK_APPLIED = "green" +# Configure the colour for 'changed' action. +COLOUR_STACK_CHANGED = "blue" +# Configure the colour for 'changed comments' action. +COLOUR_STACK_CHANGED_COMMENTS = "dark blue" +# Configure the colour for 'enabled' action. +COLOUR_STACK_ENABLED = "light green" +# Configure the colour for 'ignore' action. +COLOUR_STACK_IGNORED = "grey" +# Configure the colour for 'remove' action. +COLOUR_STACK_REMOVED = "red" +# Configure the colour for 'revert a diff' action. +COLOUR_STACK_REVERSED = "red" + +# User-relevant: Macro Dialog Colours +# Configure the colour for 'changed' action. +COLOUR_MACRO_CHANGED = "blue" +# Configure the colour for an error. +COLOUR_MACRO_ERROR = "red" +# Configure the colour for a warning. +COLOUR_MACRO_WARNING = "orange" + +STACK_COL_NS = "Namespace" +STACK_COL_ACT = "Action" +STACK_COL_NAME = "Name" +STACK_COL_VALUE = "Value" +STACK_COL_OLD_VALUE = "Old Value" + +# User-relevant: Variable Widget Colours. +# Configure the background colour for the currently-selected variable widget. +COLOUR_VALUEWIDGET_BASE_SELECTED = "GhostWhite" +# Configure the modified-status variable widget colour. +COLOUR_VARIABLE_CHANGED = "blue" +# Configure the error-state variable widget colour. +COLOUR_VARIABLE_TEXT_ERROR = "dark red" +# Configure the irrelevant-value variable widget colour. +COLOUR_VARIABLE_TEXT_IRRELEVANT = "light grey" +# Configure the environment-variable-value variable widget colour. +COLOUR_VARIABLE_TEXT_VAL_ENV = "purple4" + +# Dialog text +DIALOG_BODY_ADD_CONFIG = "Choose configuration to add to" +DIALOG_BODY_ADD_SECTION = "Specify new configuration section name" +DIALOG_BODY_IGNORE_ENABLE_CONFIG = "Choose configuration" +DIALOG_BODY_IGNORE_SECTION = "Choose the section to ignore" +DIALOG_BODY_ENABLE_SECTION = "Choose the section to enable" +DIALOG_BODY_FILE_ADD = "The file {0} will be added at your next save." +DIALOG_BODY_FILE_REMOVE = "The file {0} will be deleted at your next save." +DIALOG_BODY_GRAPH_CONFIG = "Choose the configuration to graph" +DIALOG_BODY_GRAPH_SECTION = "Choose a particular section to graph" +DIALOG_BODY_MACRO_CHANGES = "{0} {1}\n {2}\n" +DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH = 150 # Must > raw CHANGES text above +DIALOG_BODY_MACRO_CHANGES_NUM_HEIGHT = 3 # > Number, needs more height. +DIALOG_BODY_NL_CASE_CHANGE = ("Mixed-case names cause trouble in namelists." + + "\nSuggested: {0}") +DIALOG_BODY_REMOVE_CONFIG = "Choose configuration" +DIALOG_BODY_RENAME_CONFIG = "Choose configuration" +DIALOG_BODY_REMOVE_SECTION = "Choose the section to remove" +DIALOG_BODY_RENAME_SECTION = "Choose the section to rename" +DIALOG_COLUMNS_UPGRADE = ["Name", "Version", "Upgrade Version", "Upgrade?"] +DIALOG_HELP_TITLE = "Help for {0}" +DIALOG_LABEL_AUTOFIX = "Run built-in transform (fixer) macros?" +DIALOG_LABEL_AUTOFIX_ALL = ( + "Run built-in transform (fixer) macros for all configurations?") +DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR = "Choose a section for the new variable:" +DIALOG_LABEL_CHOOSE_SECTION_EDIT = "Choose a section to edit:" +DIALOG_LABEL_CONFIG_CHOOSE_META = "Metadata id:" +DIALOG_LABEL_CONFIG_CHOOSE_NAME = "New config name:" +DIALOG_LABEL_MACRO_TRANSFORM_CHANGES = ("{0}: {1}\n" + + "changes: {2}") +DIALOG_LABEL_MACRO_TRANSFORM_NONE = ( + "No configuration changes from this macro.") +DIALOG_LABEL_MACRO_VALIDATE_ISSUES = ("{0} {1}\n" + + "errors: {2}") +DIALOG_LABEL_MACRO_VALIDATE_NONE = "Configuration OK for this macro." +DIALOG_LABEL_MACRO_WARN_ISSUES = ("warnings: {0}") +DIALOG_LABEL_NULL_SECTION = "None" +DIALOG_LABEL_PREFERENCES = ("Please edit your site and user " + + "configurations to make changes.") +DIALOG_LABEL_UPGRADE = ( + "Click Upgrade Version cells to change target versions.") +DIALOG_LABEL_UPGRADE_ALL = "Populate all possible versions" +DIALOG_TIP_SUITE_RUN_HELP = "Read the help for rose suite-run" +DIALOG_TEXT_MACRO_CHANGED = "changed" +DIALOG_TEXT_MACRO_ERROR = "error" +DIALOG_TEXT_MACRO_WARNING = "warning" +DIALOG_TEXT_SUITE_NOT_RUNNING = ("Cannot launch gcontrol: {0}") +DIALOG_TEXT_UNREGISTERED_SUITE = ("Cannot launch gcontrol: " + + "suite {0} is not registered.") +DIALOG_TITLE_MACRO_TRANSFORM = "{0} - Changes for {1}" +DIALOG_TITLE_MACRO_TRANSFORM_NONE = "{0}" +DIALOG_TITLE_MACRO_VALIDATE = "{0} - Issues for {1}" +DIALOG_TITLE_MACRO_VALIDATE_NONE = "{0}" +DIALOG_TITLE_ADD = "Add section" +DIALOG_TITLE_AUTOFIX = "Automatic fixing" +DIALOG_TITLE_CHOOSE_SECTION = "Choose section" +DIALOG_TITLE_CONFIG_CREATE = "Create configuration" +DIALOG_TITLE_CRITICAL_ERROR = "Error" +DIALOG_TITLE_EDIT_COMMENTS = "Edit comments for {0}" +DIALOG_TITLE_ENABLE = "Enable section" +DIALOG_TITLE_ERROR = "Error" +DIALOG_TITLE_GRAPH = "rose metadata-graph" +DIALOG_TITLE_IGNORE = "Ignore section" +DIALOG_TITLE_INFO = "Information" +DIALOG_TITLE_OPEN = "Open configuration" +DIALOG_TITLE_LOAD_METADATA = "Add search path" +DIALOG_TITLE_MANAGE_METADATA = "Metadata search path" +DIALOG_TITLE_MACRO_CHANGES = "Accept changes made by {0}?" +DIALOG_TITLE_META_LOAD_ERROR = "Error loading metadata." +DIALOG_TITLE_NL_CASE_WARNING = "Mixed-case warning" +DIALOG_TITLE_PREFERENCES = "Configure preferences" +DIALOG_TITLE_REMOVE = "Remove section" +DIALOG_TITLE_RENAME = "Rename section" +DIALOG_TITLE_SAVE_CHANGES = "Save changes?" +DIALOG_TITLE_SUITE_NOT_RUNNING = "Suite not running" +DIALOG_TITLE_UNREGISTERED_SUITE = "Suite not registered" +DIALOG_TITLE_UPGRADE = "Upgrade configurations" +DIALOG_TITLE_WARNING = "Warning" +DIALOG_VARIABLE_ERROR_TITLE = "{0} error for {1}" +DIALOG_VARIABLE_WARNING_TITLE = "{0} warning for {1}" +DIALOG_NODE_INFO_ATTRIBUTE = "{0}" +DIALOG_NODE_INFO_CHANGES = "{0}\n" +DIALOG_NODE_INFO_DATA = "Data\n" +DIALOG_NODE_INFO_DELIMITER = " " +DIALOG_NODE_INFO_METADATA = ("Metadata\n") +DIALOG_NODE_INFO_MAX_LEN = 80 +DIALOG_NODE_INFO_SUB_ATTRIBUTE = "{0}:" +STACK_VIEW_TITLE = "Undo and Redo Stack Viewer" + +# Page names + +TITLE_PAGE_IGNORED_MARKUP = "{0} {1}" +TITLE_PAGE_INFO = "suite info" + +# User-relevant: Latent Page Colour +# Configure the colour used to indicate a latent page in the page tree. +TITLE_PAGE_LATENT_COLOUR = "grey" + +TITLE_PAGE_LATENT_MARKUP = ("{0}" + "") +TITLE_PAGE_PREVIEW_MARKUP = ("{0}" + "") +TITLE_PAGE_ROOT_MARKUP = "{0}" +TITLE_PAGE_SUITE = "suite conf" + +# User-relevant: Page Tree Expansion +# Configure the maximum number of configurations loaded at startup. +TREE_PANEL_MAX_EXPANDED_ROOTS = 5 +# Configure the depth to which the page tree will expand itself. +TREE_PANEL_MAX_EXPANDED_DEPTH = 2 +# Configure a regex for pages whose children should not be expanded. +TREE_PANEL_NO_EXPAND_LEAVES_REGEX = "/file$" + +# File panel names + +FILE_PANEL_EXPAND = 2 +FILE_PANEL_MENU_OPEN = "Open" +TITLE_FILE_PANEL = "Other files" + +# Summary (sub) data panel names + +SUMMARY_DATA_PANEL_ERROR_TIP = "Error ({0}): {1}\n" + +# User-relevant: Summary Data Panel Colours +# Configure the Pango markup used to indicate an error. +SUMMARY_DATA_PANEL_ERROR_MARKUP = "X" +# Configure the Pango markup used to indicate modified state. +SUMMARY_DATA_PANEL_MODIFIED_MARKUP = "*" + +SUMMARY_DATA_PANEL_FILTER_LABEL = "Filter:" +SUMMARY_DATA_PANEL_FILTER_MAX_CHAR = 8 +SUMMARY_DATA_PANEL_GROUP_LABEL = "Group:" +SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP = "^" +SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP = "!!" +SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP = "!" +SUMMARY_DATA_PANEL_MAX_LEN = 15 +SUMMARY_DATA_PANEL_MENU_ADD = "Add new section" +SUMMARY_DATA_PANEL_MENU_COPY = "Clone this section" +SUMMARY_DATA_PANEL_MENU_ENABLE = "Enable this section" +SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI = "Enable these sections" +SUMMARY_DATA_PANEL_MENU_GO_TO = "View {0}" +SUMMARY_DATA_PANEL_MENU_IGNORE = "Ignore this section" +SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI = "Ignore these sections" +SUMMARY_DATA_PANEL_MENU_REMOVE = "Remove this section" +SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI = "Remove these sections" +SUMMARY_DATA_PANEL_SECTION_TITLE = "Section" +SUMMARY_DATA_PANEL_INDEX_TITLE = "Index" +FILE_CONTENT_PANEL_FORMAT_LABEL = "Hide available sections" +FILE_CONTENT_PANEL_MENU_OPTIONAL = "Toggle optional status" +FILE_CONTENT_PANEL_OPT_TIP = "Items available for file source" +FILE_CONTENT_PANEL_TIP = "Items included in file source" +FILE_CONTENT_PANEL_TITLE = "Available sections" + +# Tooltip (hover-over) text + +TREE_PANEL_TIP_ADDED_CONFIG = "Added configuration since the last save" +TREE_PANEL_TIP_ADDED_VARS = "Added variable(s) since the last save" +TREE_PANEL_TIP_CHANGED_CONFIG = "Modified since the last save" +TREE_PANEL_TIP_CHANGED_SECTIONS = "Modified section data since the last save" +TREE_PANEL_TIP_CHANGED_VARS = "Modified variable data since the last save" +TREE_PANEL_TIP_DIFF_SECTIONS = "Added/removed sections since the last save" +TREE_PANEL_TIP_REMOVED_VARS = "Removed variable(s) since the last save" + +KEY_TIP_ADDED = "Added since the last save." +KEY_TIP_CHANGED = "Modified since the last save, old value {0}" +KEY_TIP_CHANGED_COMMENTS = "Modified comments since the last save." +KEY_TIP_ENABLED = "Enabled since the last save." +KEY_TIP_SECTION_IGNORED = "Section ignored since the last save." +KEY_TIP_TRIGGER_IGNORED = "Trigger ignored since the last save." +KEY_TIP_MISSING = "Removed since the last save." +KEY_TIP_USER_IGNORED = "User ignored since the last save." +TIP_CONFIG_CHOOSE_META = "Enter a metadata identifier for the new config" +TIP_CONFIG_CHOOSE_NAME = "Enter a directory name for the new config." +TIP_CONFIG_CHOOSE_NAME_ERROR = "Invalid directory name for the new config." +TIP_ADD_TO_PAGE = "Add to page..." +TIP_LATENT_PAGE = "Latent page" +TIP_MACRO_RUN_PAGE = "Choose a macro to run for this page" +TIP_REVERT_PAGE = "Revert page to last save" +TIP_SUITE_RUN_ARG = "Enter extra suite run arguments" +TIP_VALUE_ADD_URI = "Add a URI - for example a file path, or a web url" +TREE_PANEL_ERROR = " (1 error)" +TREE_PANEL_ERRORS = " ({0} errors)" +TREE_PANEL_MODIFIED = " (modified)" +TERMINAL_TIP_CLOSE = "Close terminal" +VAR_COMMENT_TIP = "# {0}" +VAR_FLAG_MARKUP = "{0}" +VAR_FLAG_TIP_FIXED = "Fixed variable (only one allowed value)" +VAR_FLAG_TIP_NO_META = "Flag: no metadata" +VAR_FLAG_TIP_OPT_CONF = "Optional conf overrides:\n{0}" +# Numbers below mean: 0-opt config name, 1-id state/value. +VAR_FLAG_TIP_OPT_CONF_INFO = " {0}: {1}\n" +# Numbers below mean: 0-sect state, 1-sect, 2-opt state, 3-opt, 4-opt value. +VAR_FLAG_TIP_OPT_CONF_STATE = "{0}{1}={2}{3}={4}" +VAR_FLAG_TIP_OPTIONAL = "Flag: optional" +VAR_MENU_TIP_ERROR = "Error " +VAR_MENU_TIP_LATENT = "This variable could be added to the configuration." +VAR_MENU_TIP_WARNING = "Warning " +VAR_MENU_TIP_FIX_IGNORE = "Auto-fix the variable's ignored state error" +VAR_WIDGET_ENV_INFO = "Set to environment variable" + +# Flags for variable widgets + +FLAG_TYPE_DEFAULT = "Default flag" +FLAG_TYPE_ERROR = "Error flag" +FLAG_TYPE_FIXED = "Fixed flag" +FLAG_TYPE_NO_META = "No metadata flag" +FLAG_TYPE_OPT_CONF = "Opt conf override flag" +FLAG_TYPE_OPTIONAL = "Optional flag" + +# Relevant metadata properties + +META_PROP_WIDGET = "widget[rose-config-edit]" +META_PROP_WIDGET_SUB_NS = "widget[rose-config-edit:sub-ns]" + +# Miscellaneous +COPYRIGHT = ( + "Copyright (C) 2012-2020 British Crown (Met Office) & Contributors.") +HELP_FILE = "rose-rug-config-edit.html" +LAUNCH_COMMAND = "rose config-edit" +LAUNCH_COMMAND_CONFIG = "rose config-edit -C" +LAUNCH_COMMAND_GRAPH = "rose metadata-graph -C" +LAUNCH_SUITE_RUN = "rose suite-run" +LAUNCH_SUITE_RUN_HELP = "rose help suite-run" +MAX_APPS_THRESHOLD = 10 +MIN_PYGTK_VERSION = (2, 12, 0) +PROGRAM_NAME = "rose edit" +PROJECT_URL = "http://github.com/metomi/rose/" +UNTITLED_NAME = "Untitled" +VAR_ID_IN_CONFIG = "Variable id {0} from the configuration {1}" + + +_OVERRIDE_WARNING_PRIVATE = ( + "Cannot override: {0}={1} ({2}): not permitted.\n") +_OVERRIDE_WARNING_TYPE = ( + "Cannot override: {0}={1}={2}: site/user conf: was {3}, supplied {4}\n") + + +def false_function(*args, **kwargs): + """Return False, no matter what the arguments are.""" + return False + + +def load_override_config(sections, my_globals=None): + if my_globals is None: + my_globals = globals() + for section in sections: + conf = ResourceLocator.default().get_conf().get([section]) + if conf is None: + continue + for key, node in list(conf.value.items()): + if node.is_ignored(): + continue + try: + cast_value = ast.literal_eval(node.value) + except Exception: + cast_value = node.value + name = key.replace("-", "_").upper() + orig_value = my_globals[name] + if (not isinstance(orig_value, type(cast_value)) and + orig_value is not None): + sys.stderr.write(_OVERRIDE_WARNING_TYPE.format( + section, key, cast_value, + type(orig_value), type(cast_value)) + ) + continue + if name.startswith("_"): + sys.stderr.write(_OVERRIDE_WARNING_PRIVATE.format( + section, key, name) + ) + continue + my_globals[name] = cast_value + + +load_override_config(["rose-config-edit"]) diff --git a/metomi/rose/config_editor/data.py b/metomi/rose/config_editor/data.py new file mode 100644 index 000000000..89dd49088 --- /dev/null +++ b/metomi/rose/config_editor/data.py @@ -0,0 +1,1275 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This module contains: + +VarData -- class to store rose.variable.Variable instances +SectData -- class to store rose.section.Section instances +ConfigData -- class to store and process a directory into internal +data structures +ConfigDataManager -- class to load and process objects in ConfigData + +""" + +import copy +import itertools +import glob +import os +import re +import sys + +import rose.config +import rose.config_editor.data_helper +import rose.config_tree +import rose.gtk.dialog +import rose.macro +import rose.metadata_check +import rose.resource +import rose.section +import rose.macros.trigger +import rose.variable + + +REC_NS_SECTION = re.compile(r"^(" + rose.META_PROP_NS + rose.CONFIG_DELIMITER + + r")(.*)$") + + +class VarData(object): + + """Stores past, present, and missing variables.""" + + def __init__(self, v_map, latent_v_map, save_v_map, latent_save_v_map): + self.now = v_map + self.latent = latent_v_map + self.save = save_v_map + self.latent_save = latent_save_v_map + + def foreach(self, save=False, skip_latent=False): + """Yield all (section, variables) tuples for real and latent.""" + if save: + real = self.save + latent = self.latent_save + else: + real = self.now + latent = self.latent + for section, variables in list(real.items()): + yield section, variables + if not skip_latent: + for section, variables in list(latent.items()): + yield section, variables + + def get_all(self, save=False, skip_latent=False, skip_real=False): + """Return all real and latent variables.""" + if save: + real = self.save + latent = self.latent_save + else: + real = self.now + latent = self.latent + all_vars = [] + if not skip_real: + all_vars += list(itertools.chain(*list(real.values()))) + if not skip_latent: + all_vars += list(itertools.chain(*list(latent.values()))) + return all_vars + + def get_var(self, section, option, save=False, skip_latent=False): + """Return the variable specified by section, option.""" + var_id = section + rose.CONFIG_DELIMITER + option + if save: + nodes = [self.save, self.latent_save] + else: + nodes = [self.now, self.latent] + if skip_latent: + nodes.pop() + for node in nodes: + for var in node.get(section, []): + if var.metadata['id'] == var_id: + return var + return None + + +class SectData(object): + + """Stores past, present, and missing sections.""" + + def __init__(self, sections, latent_sections, save_sections, + latent_save_sections): + self.now = sections + self.latent = latent_sections + self.save = save_sections + self.latent_save = latent_save_sections + + def get_all(self, save=False, skip_latent=False, skip_real=False): + """Return all sections that match the save/latent criteria.""" + if save: + real = self.save + latent = self.latent_save + else: + real = self.now + latent = self.latent + all_sections = [] + if not skip_real: + all_sections += list(real.values()) + if not skip_latent: + all_sections += list(latent.values()) + return all_sections + + def get_sect(self, section, save=False, skip_latent=False): + """Return the section data specified by section.""" + if save: + nodes = [self.save, self.latent_save] + else: + nodes = [self.now, self.latent] + if skip_latent: + nodes.pop() + for node in nodes: + if section in node: + return node[section] + return None + + +class ConfigData(object): + + """Stores information about a configuration.""" + + def __init__(self, config, s_config, directory, opt_conf_lookup, meta, + meta_id, meta_files, macros, config_type, + var_data=None, sect_data=None, is_preview=False): + self.config = config + self.save_config = s_config + self.directory = directory + self.opt_configs = opt_conf_lookup + self.meta = meta + self.meta_id = meta_id + self.meta_files = meta_files + self.macros = macros + self.config_type = config_type + self.vars = var_data + self.sections = sect_data + self.is_preview = is_preview + + +class ConfigDataManager(object): + + """Loads the information from the various configurations.""" + + def __init__(self, util, reporter, page_ns_show_modes, + reload_ns_tree_func, opt_meta_paths=None, + no_warn=None): + """Load the root configuration and all its sub-configurations.""" + self.util = util + self.helper = rose.config_editor.data_helper.ConfigDataHelper( + self, util) + self.reporter = reporter + self.page_ns_show_modes = page_ns_show_modes + self.reload_ns_tree_func = reload_ns_tree_func + self.config = {} # Stores configuration name: object + self._builtin_value_macro = rose.macros.value.ValueChecker() # value + self.builtin_macros = {} # Stores other Rose built-in macro instances + self._bad_meta_dir_paths = [] # Stores flawed metadata directories. + self.trigger = {} # Stores trigger macro instances per configuration + self.trigger_id_trees = {} # Stores trigger dependencies + self.trigger_id_value_lookup = {} # Stores old values of trigger vars + self.namespace_meta_lookup = {} # Stores titles etc of namespaces + self.namespace_cached_statuses = { + 'latent': {}, 'ignored': {}} # Caches ns statuses + self._config_section_namespace_map = {} # Store section namespaces + self.locator = rose.resource.ResourceLocator(paths=sys.path) + if opt_meta_paths is None: + self.opt_meta_paths = [] + else: + self.opt_meta_paths = opt_meta_paths + self.no_warn = no_warn + self.top_level_directory = None + self.app_count = 0 + self.saved_config_names = None + self.top_level_name = None + + def load(self, top_level_directory, config_obj_dict, + config_obj_type_dict=None, load_all_apps=False, + load_no_apps=False, metadata_off=False): + """Load configurations and their metadata.""" + if config_obj_type_dict is None: + config_obj_type_dict = {} + if top_level_directory is not None: + for filename in os.listdir(top_level_directory): + if filename in [rose.TOP_CONFIG_NAME, rose.SUB_CONFIG_NAME]: + self.load_top_config(top_level_directory, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps, + metadata_off=metadata_off) + break + else: + self.load_top_config(None) + elif not config_obj_dict: + self.load_top_config(None) + else: + self.top_level_name = list(config_obj_dict.keys())[0] + self.top_level_directory = None + for name, obj in list(config_obj_dict.items()): + config_type = config_obj_type_dict.get(name) + self.load_config(config_name=name, config=obj, + config_type=config_type) + self.saved_config_names = set(self.config.keys()) + + def load_top_config(self, top_level_directory, preview=False, + load_all_apps=False, load_no_apps=False, + metadata_off=False): + """Load the config at the top level and any sub configs.""" + self.top_level_directory = top_level_directory + + self.app_count = 0 + if top_level_directory is None: + self.top_level_name = rose.config_editor.UNTITLED_NAME + else: + self.top_level_name = os.path.basename(top_level_directory) + config_container_dir = os.path.join(top_level_directory, + rose.SUB_CONFIGS_DIR) + if os.path.isdir(config_container_dir): + sub_contents = sorted(os.listdir(config_container_dir)) + + if not load_all_apps: + if load_no_apps: + preview = True + else: + for config_dir in sub_contents: + conf_path = os.path.join(config_container_dir, + config_dir) + if (os.path.isdir(conf_path) and + not config_dir.startswith('.')): + self.app_count += 1 + + if (self.app_count > + rose.config_editor.MAX_APPS_THRESHOLD): + preview = True + + for config_dir in sub_contents: + conf_path = os.path.join(config_container_dir, config_dir) + if (os.path.isdir(conf_path) and + not config_dir.startswith('.')): + self.load_config(conf_path, preview=preview, + metadata_off=metadata_off) + self.load_config(top_level_directory) + self.reload_ns_tree_func() + + def load_info_config(self, config_directory): + """Load any information (discovery) config.""" + disc_path = os.path.join(config_directory, + rose.INFO_CONFIG_NAME) + if os.path.isfile(disc_path): + config_obj = self.load_config_file(disc_path)[0] + self.load_config(config_name="/" + self.top_level_name + "-info", + config=config_obj, + config_type=rose.INFO_CONFIG_NAME) + + def load_config(self, config_directory=None, + config_name=None, config=None, + config_type=None, reload_tree_on=False, + skip_load_event=False, preview=False, + metadata_off=False): + """Load the configuration and metadata.""" + if config_directory is None: + name = "/" + config_name.lstrip("/") + config = config + s_config = copy.deepcopy(config) + if not skip_load_event: + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_CONFIG.format( + name.lstrip("/"))) + else: + config_directory = config_directory.rstrip("/") + if config_directory != self.top_level_directory: + # One of the sub configurations + head, tail = os.path.split(config_directory) + name = '' + while tail != rose.SUB_CONFIGS_DIR: + name = "/" + os.path.join(tail, name).rstrip('/') + head, tail = os.path.split(head) + name = "/" + name.lstrip("/") + config_type = rose.SUB_CONFIG_NAME + elif rose.TOP_CONFIG_NAME not in os.listdir(config_directory): + # Just editing a single sub configuration, not a suite + name = "/" + self.top_level_name + config_type = rose.SUB_CONFIG_NAME + else: + # Make sure we also load any discovery (info) configuration + self.load_info_config(config_directory) + # A suite configuration + name = "/" + self.top_level_name + "-conf" + config_type = rose.TOP_CONFIG_NAME + if not skip_load_event: + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_CONFIG.format( + name.lstrip("/"))) + config_path = os.path.join(config_directory, rose.SUB_CONFIG_NAME) + if not os.path.isfile(config_path): + if (os.path.abspath(config_directory) == + os.path.abspath(self.top_level_directory)): + config_path = os.path.join(config_directory, + rose.TOP_CONFIG_NAME) + config_type = rose.TOP_CONFIG_NAME + else: + text = rose.config_editor.ERROR_NOT_FOUND.format( + config_path) + title = rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + sys.exit(2) + + if config_directory != self.top_level_directory and preview: + # Load with empty ConfigNodes for initial app access. + config = rose.config.ConfigNode() + s_config = rose.config.ConfigNode() + else: + config, s_config = self.load_config_file(config_path) + + if config_directory != self.top_level_directory and preview: + meta_config_tree = rose.config_tree.ConfigTree() + elif metadata_off: + meta_config_tree = self.load_meta_config_tree( + config_type=config_type, + opt_meta_paths=self.opt_meta_paths) + else: + try: + meta_config_tree = self.load_meta_config_tree( + config, config_directory, + config_type=config_type, + opt_meta_paths=self.opt_meta_paths + ) + except IOError as exc: + rose.gtk.dialog.run_exception_dialog(exc) + meta_config_tree = rose.config_tree.ConfigTree() + + meta_config = meta_config_tree.node + opt_conf_lookup = self.load_optional_configs(config_directory) + + macro_module_prefix = self.helper.get_macro_module_prefix(name) + meta_files = self.load_meta_files(meta_config_tree) + macros = rose.macro.load_meta_macro_modules( + meta_files, module_prefix=macro_module_prefix) + meta_id = self.helper.get_config_meta_flag( + name, from_this_config_obj=config) + # Initialise configuration data object. + self.config[name] = ConfigData(config, s_config, config_directory, + opt_conf_lookup, meta_config, + meta_id, meta_files, macros, + config_type, is_preview=preview) + + self.load_builtin_macros(name) + self.load_file_metadata(name) + self.filter_meta_config(name) + + # Load section and variable data into the object. + sects, l_sects = self.load_sections_from_config(name) + s_sects, s_l_sects = self.load_sections_from_config(name) + self.config[name].sections = SectData(sects, l_sects, s_sects, + s_l_sects) + var, l_var, s_var, s_l_var = self.load_vars_from_config( + name, return_copies=True) + self.config[name].vars = VarData(var, l_var, s_var, s_l_var) + + if not skip_load_event: + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_METADATA.format( + name.lstrip("/"))) + # Process namespaces and ignored statuses. + self.load_node_namespaces(name) + self.load_node_namespaces(name, from_saved=True) + self.load_ignored_data(name) + self.load_metadata_for_namespaces(name) + if reload_tree_on: + self.reload_ns_tree_func() + + def load_config_file(self, config_path): + """Return two copies of the rose.config.ConfigNode at config_path.""" + try: + config = rose.config.load(config_path) + except rose.config.ConfigSyntaxError as exc: + text = rose.config_editor.ERROR_LOAD_SYNTAX.format( + config_path, exc) + title = rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + sys.exit(2) + else: + master_config = rose.config.load(config_path) + rose.macro.standard_format_config(config) + rose.macro.standard_format_config(master_config) + return config, master_config + + def load_optional_configs(self, config_directory): + """Load any optional configurations.""" + opt_conf_lookup = {} + if config_directory is None: + return opt_conf_lookup + opt_dir = os.path.join(config_directory, rose.config.OPT_CONFIG_DIR) + if not os.path.isdir(opt_dir): + return opt_conf_lookup + opt_exceptions = {} + opt_glob = os.path.join(opt_dir, rose.GLOB_OPT_CONFIG_FILE) + for path in glob.glob(opt_glob): + if os.access(path, os.F_OK | os.R_OK): + filename = os.path.basename(path) + # filename is a null string if path is to a directory. + result = re.match(rose.RE_OPT_CONFIG_FILE, filename) + if not result: + continue + name = result.group(1) + try: + opt_config = rose.config.load(path) + except Exception as exc: + opt_exceptions.update({path: exc}) + continue + opt_conf_lookup.update({name: opt_config}) + if opt_exceptions: + err_text = "" + err_format = rose.config_editor.ERROR_LOAD_OPT_CONFS_FORMAT + for path, exc in sorted(opt_exceptions.items()): + err_text += err_format.format(path, type(exc).__name__, exc) + err_text = err_text.rstrip() + text = rose.config_editor.ERROR_LOAD_OPT_CONFS.format(err_text) + title = rose.config_editor.ERROR_LOAD_OPT_CONFS_TITLE + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title=title, modal=False) + return opt_conf_lookup + + def load_builtin_macros(self, config_name): + """Load Rose builtin macros.""" + self.builtin_macros[config_name] = { + rose.META_PROP_COMPULSORY: + rose.macros.compulsory.CompulsoryChecker(), + rose.META_PROP_TYPE: + self._builtin_value_macro} + + def load_sections_from_config(self, config_name, save=False): + """Return maps of section objects from the configuration.""" + sect_map = {} + latent_sect_map = {} + real_sect_ids = [] + real_sect_basic_ids = [] + if save: + config = self.config[config_name].save_config + else: + config = self.config[config_name].config + meta_config = self.config[config_name].meta + for section, node in list(config.value.items()): + if not isinstance(node.value, dict): + if "" in sect_map: + sect_map[""].options.append(section) + continue + meta_data = self.helper.get_metadata_for_config_id( + "", config_name) + sect_map.update({"": rose.section.Section("", [section], + meta_data)}) + real_sect_ids.append("") + continue + meta_data = self.helper.get_metadata_for_config_id(section, + config_name) + options = list(node.value.keys()) + sect_map.update({section: rose.section.Section(section, options, + meta_data)}) + sect_map[section].comments = list(node.comments) + real_sect_ids.append(section) + real_sect_basic_ids.extend( + [rose.macro.REC_ID_STRIP_DUPL.sub("", section), + rose.macro.REC_ID_STRIP.sub("", section)] + ) + if node.is_ignored(): + reason = {} + if node.state == rose.config.ConfigNode.STATE_SYST_IGNORED: + reason = {rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_CONFIG} + elif (node.state == + rose.config.ConfigNode.STATE_USER_IGNORED): + reason = {rose.variable.IGNORED_BY_USER: + rose.config_editor.IGNORED_STATUS_CONFIG} + sect_map[section].ignored_reason.update(reason) + if "" not in sect_map: + # This always exists for a configuration. + meta_data = self.helper.get_metadata_for_config_id("", + config_name) + sect_map.update({"": rose.section.Section("", [], meta_data)}) + real_sect_ids.append("") + for setting_id, sect_node in list(meta_config.value.items()): + if sect_node.is_ignored() or isinstance(sect_node.value, str): + continue + section, option = self.util.get_section_option_from_id(setting_id) + if (option is not None or section in real_sect_ids or + section in real_sect_basic_ids): + continue + meta_data = {} + for prop_opt, opt_node in list(sect_node.value.items()): + if opt_node.is_ignored(): + continue + meta_data.update({prop_opt: opt_node.value}) + latent_section_name = section + if (meta_data.get(rose.META_PROP_DUPLICATE) == + rose.META_PROP_VALUE_TRUE): + latent_section_name = section + "({0})".format( + rose.CONFIG_SETTING_INDEX_DEFAULT) + meta_data.update({'id': latent_section_name}) + if section not in ['ns', 'file:*']: + latent_sect_map[latent_section_name] = rose.section.Section( + latent_section_name, [], meta_data) + return sect_map, latent_sect_map + + def load_vars_from_config(self, config_name, only_this_section=None, + save=False, update=False, return_copies=False): + """Return maps of variables from the configuration""" + config_data = self.config[config_name] + if save: + config = config_data.save_config + section_map = config_data.sections.save + latent_section_map = config_data.sections.latent_save + else: + config = config_data.config + section_map = config_data.sections.now + latent_section_map = config_data.sections.latent + meta_config = config_data.meta + if update: + if save: + var_map = config_data.vars.save + latent_var_map = config_data.vars.latent_save + else: + var_map = config_data.vars.now + latent_var_map = config_data.vars.latent + else: + var_map = {} + latent_var_map = {} + if return_copies: + var_map_copy = {} + latent_var_map_copy = {} + real_var_ids = [] + basic_dupl_map = {} + if only_this_section is None: + key_nodes = config.walk() + else: + key_nodes = config.walk(keys=[only_this_section]) + self._load_dupl_sect_map(basic_dupl_map, only_this_section) + for keylist, node in key_nodes: + if len(keylist) < 2: + self._load_dupl_sect_map(basic_dupl_map, keylist[0]) + continue + section, option = keylist + flags = self.load_option_flags(config_name, section, option) + ignored_reason = {} + if section_map[section].ignored_reason: + ignored_reason.update({ + rose.variable.IGNORED_BY_SECTION: + rose.config_editor.IGNORED_STATUS_CONFIG}) + if node.state == rose.config.ConfigNode.STATE_SYST_IGNORED: + ignored_reason.update({ + rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_CONFIG}) + elif (node.state == + rose.config.ConfigNode.STATE_USER_IGNORED): + ignored_reason.update({ + rose.variable.IGNORED_BY_USER: + rose.config_editor.IGNORED_STATUS_CONFIG}) + cfg_comments = node.comments + var_id = self.util.get_id_from_section_option(section, option) + real_var_ids.append(var_id) + meta_data = self.helper.get_metadata_for_config_id(var_id, + config_name) + var_map.setdefault(section, []) + if return_copies: + var_map_copy.setdefault(section, []) + if update: + id_list = [v.metadata['id'] for v in var_map[section]] + if var_id in id_list: + for i, var in enumerate(var_map[section]): + if var.metadata['id'] == var_id: + var_map[section].pop(i) + break + var_map[section].append( + rose.variable.Variable( + option, + node.value, + meta_data, + ignored_reason, + error={}, + flags=flags, + comments=cfg_comments + ) + ) + if return_copies: + var_map_copy[section].append( + rose.variable.Variable( + option, + node.value, + meta_data, + ignored_reason, + error={}, + flags=flags, + comments=cfg_comments + ) + ) + id_node_stack = list(meta_config.value.items()) + while id_node_stack: + setting_id, sect_node = id_node_stack.pop(0) + if sect_node.is_ignored() or isinstance(sect_node.value, str): + continue + section, option = self.util.get_section_option_from_id(setting_id) + if section in ['ns', 'file:*']: + continue + if section in basic_dupl_map: + # There is a matching duplicate e.g. foo(3) or foo{bar}(1) + for dupl_section in basic_dupl_map[section]: + dupl_id = self.util.get_id_from_section_option( + dupl_section, option) + id_node_stack.insert(0, (dupl_id, sect_node)) + continue + if only_this_section is not None and section != only_this_section: + continue + if option is None: + # A section, not a variable. + continue + if setting_id in real_var_ids: + # This variable isn't missing, so skip. + continue + if (meta_config.get_value([section, rose.META_PROP_DUPLICATE]) == + rose.META_PROP_VALUE_TRUE and + section not in basic_dupl_map and + config.get([section]) is None): + section = section + "({0})".format( + rose.CONFIG_SETTING_INDEX_DEFAULT) + setting_id = self.util.get_id_from_section_option( + section, option) + flags = self.load_option_flags(config_name, section, option) + ignored_reason = {} + sect_data = section_map.get(section) + if sect_data is None: + sect_data = latent_section_map.get(section) + if sect_data is not None and sect_data.ignored_reason: + ignored_reason = { + rose.variable.IGNORED_BY_SECTION: + rose.config_editor.IGNORED_STATUS_CONFIG} + meta_data = {} + for prop_opt, opt_node in list(sect_node.value.items()): + if opt_node.is_ignored(): + continue + meta_data.update({prop_opt: opt_node.value}) + meta_data.update({'id': setting_id}) + value = rose.variable.get_value_from_metadata(meta_data) + latent_var_map.setdefault(section, []) + if return_copies: + latent_var_map_copy.setdefault(section, []) + if update: + id_list = [v.metadata['id'] for v in latent_var_map[section]] + if setting_id in id_list: + for var in latent_var_map[section]: + if var.metadata['id'] == setting_id: + latent_var_map[section].remove(var) + latent_var_map[section].append( + rose.variable.Variable( + option, + value, + meta_data, + ignored_reason, + error={}, + flags=flags + ) + ) + if return_copies: + latent_var_map_copy[section].append( + rose.variable.Variable( + option, + value, + meta_data, + ignored_reason, + error={}, + flags=flags + ) + ) + if return_copies: + return var_map, latent_var_map, var_map_copy, latent_var_map_copy + return var_map, latent_var_map + + def _load_dupl_sect_map(self, basic_dupl_map, section): + basic_section = rose.macro.REC_ID_STRIP.sub("", section) + if basic_section != section: + basic_dupl_map.setdefault(basic_section, []) + basic_dupl_map[basic_section].append(section) + mod_section = rose.macro.REC_ID_STRIP_DUPL.sub("", section) + if mod_section != basic_section and mod_section != section: + basic_dupl_map.setdefault(mod_section, []) + basic_dupl_map[mod_section].append(section) + + def load_option_flags(self, config_name, section, option): + """Load flags for an option.""" + flags = {} + opt_conf_flags = self._load_opt_conf_flags(config_name, + section, option) + if opt_conf_flags: + flags.update({rose.config_editor.FLAG_TYPE_OPT_CONF: + opt_conf_flags}) + return flags + + def _load_opt_conf_flags(self, config_name, section, option): + opt_config_map = self.config[config_name].opt_configs + opt_conf_diff_format = rose.config_editor.VAR_FLAG_TIP_OPT_CONF_STATE + opt_flags = {} + for opt_name in sorted(opt_config_map): + opt_config = opt_config_map[opt_name] + opt_node = opt_config.get([section, option]) + if opt_node is not None: + opt_sect_node = opt_config.get([section]) + text = opt_conf_diff_format.format(opt_sect_node.state, + section, + opt_node.state, + option, + opt_node.value) + opt_flags[opt_name] = text + return opt_flags + + def add_section_to_config(self, section, config_name): + """Add a blank section to the configuration.""" + self.config[config_name].config.set([section]) + + def dump_to_internal_config(self, config_name, only_this_ns=None): + """Return a rose.config.ConfigNode object from variable info.""" + config = rose.config.ConfigNode() + var_map = self.config[config_name].vars.now + sect_map = self.config[config_name].sections.now + user_ignored_state = rose.config.ConfigNode.STATE_USER_IGNORED + syst_ignored_state = rose.config.ConfigNode.STATE_SYST_IGNORED + enabled_state = rose.config.ConfigNode.STATE_NORMAL + sections_to_be_dumped = [] + if only_this_ns is None: + allowed_sections = set(list(sect_map.keys()) + list(var_map.keys())) + else: + allowed_sections = self.helper.get_sections_from_namespace( + only_this_ns) + for section in sect_map: + if only_this_ns is not None and section not in allowed_sections: + continue + sections_to_be_dumped.append(section) + for section in allowed_sections: + variables = var_map.get(section, []) + for variable in variables: + if only_this_ns is not None: + if variable.metadata['full_ns'] != only_this_ns: + continue + option = variable.name + if not variable.name: + var_id = variable.metadata["id"] + option = self.util.get_section_option_from_id(var_id)[1] + value = variable.value + var_state = enabled_state + if variable.ignored_reason: + if (rose.variable.IGNORED_BY_USER in + variable.ignored_reason): + var_state = user_ignored_state + elif (rose.variable.IGNORED_BY_SYSTEM in + variable.ignored_reason): + var_state = syst_ignored_state + var_comments = variable.comments + config.set([section, option], value, state=var_state, + comments=var_comments) + for section_id in sections_to_be_dumped: + comments = sect_map[section_id].comments + if not section_id: + config.comments = list(comments) + continue + section_state = enabled_state + if sect_map[section_id].ignored_reason: + if (rose.variable.IGNORED_BY_USER in + sect_map[section_id].ignored_reason): + section_state = user_ignored_state + elif (rose.variable.IGNORED_BY_SYSTEM in + sect_map[section_id].ignored_reason): + section_state = syst_ignored_state + node = config.get([section_id]) + if node is None: + config.set([section_id], state=section_state) + node = config.get([section_id]) + else: + node.state = section_state + node.comments = list(comments) + return config + + def load_meta_path(self, config=None, directory=None): + """Retrieve the path to the metadata.""" + return rose.macro.load_meta_path(config, directory) + + def clear_meta_lookups(self, config_name): + for ns in list(self.namespace_meta_lookup.keys()): + if (ns.startswith(config_name) and + self.util.split_full_ns(self, ns)[0] == config_name): + self.namespace_meta_lookup.pop(ns) + if config_name in self._config_section_namespace_map: + self._config_section_namespace_map.pop(config_name) + + def load_meta_config_tree(self, config=None, directory=None, + config_type=None, opt_meta_paths=None): + """Load the main metadata, and any specified in 'config'.""" + if config is None: + config = rose.config.ConfigNode() + error_handler = rose.config_editor.util.launch_error_dialog + return rose.macro.load_meta_config_tree( + config, + directory, + config_type=config_type, + error_handler=error_handler, + opt_meta_paths=opt_meta_paths, + no_warn=self.no_warn + ) + + def load_meta_files(self, config_tree): + """Load the file paths of files within the metadata directory.""" + meta_files = [] + for rel_path, conf_dir in list(config_tree.files.items()): + meta_files.append(os.path.join(conf_dir, rel_path)) + return meta_files + + def filter_meta_config(self, config_name): + """Filter out invalid metadata.""" + config_data = self.config[config_name] + config = config_data.config + meta_config = config_data.meta + directory = config_data.directory + meta_dir_path = self.load_meta_path(config, directory)[0] + reports = rose.metadata_check.metadata_check(meta_config, + directory) + if reports and meta_dir_path not in self._bad_meta_dir_paths: + # There are problems with some metadata. + title = rose.config_editor.ERROR_METADATA_CHECKER_TITLE.format( + meta_dir_path) + text = rose.config_editor.ERROR_METADATA_CHECKER_TEXT.format( + len(reports), meta_dir_path) + self._bad_meta_dir_paths.append(meta_dir_path) + reports_map = {None: reports} + reports_text = rose.macro.get_reports_as_text( + reports_map, "rose.metadata_check.MetadataChecker") + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title, modal=False, + extra_text=reports_text) + for report in reports: + if report.option != rose.META_PROP_TRIGGER: + meta_config.unset([report.section, report.option]) + + def load_ignored_data(self, config_name): + """Deal with ignored variables and sections. + + In particular, this assigns errors based on incorrect ignore + state. + + 'Doc table' in the comments refers to + doc/rose-configuration-metadata.html#appendix-ignored-config-edit + + """ + self.trigger[config_name] = rose.macros.trigger.TriggerMacro() + config = self.config[config_name].config + sect_map = self.config[config_name].sections.now + latent_sect_map = self.config[config_name].sections.latent + var_map = self.config[config_name].vars.now + latent_var_map = self.config[config_name].vars.latent + config_for_macro = rose.config.ConfigNode() + enabled_state = rose.config.ConfigNode.STATE_NORMAL + syst_ignored_state = rose.config.ConfigNode.STATE_SYST_IGNORED + # Deliberately reset state information in the macro config. + for keylist, node in config.walk(): + if len(keylist) == 1 and list(node.value.keys()): + # Setting non-empty section info would overwrite options. + continue + config_for_macro.set(keylist, copy.deepcopy(node.value)) + meta_config = self.config[config_name].meta + bad_list = self.trigger[config_name].validate_dependencies( + config_for_macro, meta_config) + if bad_list: + self.trigger[config_name].trigger_family_lookup.clear() + event = rose.config_editor.EVENT_INVALID_TRIGGERS.format( + config_name.strip("/")) + self.reporter.report(event, self.reporter.KIND_ERR) + return + trig_config = self.trigger[config_name].transform( + config_for_macro, meta_config)[0] + self.trigger_id_value_lookup.setdefault(config_name, {}) + var_id_map = {} + for variables in list(var_map.values()): + for variable in variables: + var_id_map.update({variable.metadata['id']: variable}) + latent_var_id_map = {} + for variables in list(latent_var_map.values()): + for variable in variables: + latent_var_id_map.update({variable.metadata['id']: variable}) + trig_ids = list(self.trigger[config_name].trigger_family_lookup.keys()) + while trig_ids: + var_id = trig_ids.pop() + var = var_id_map.get(var_id) + if var is None: + value = None + else: + value = var.value + self.trigger_id_value_lookup[config_name].update({var_id: value}) + sect, opt = self.util.get_section_option_from_id(var_id) + if sect.endswith(")"): + continue + node = meta_config.get([sect, rose.META_PROP_DUPLICATE]) + if node is not None and node.value == rose.META_PROP_VALUE_TRUE: + search_string = sect + "(" + for section in sect_map: + if section.startswith(search_string): + new_id = self.util.get_id_from_section_option( + section, opt) + trig_ids.append(new_id) + id_node_map = {} + id_node_map.update(sect_map) + id_node_map.update(latent_sect_map) + id_node_map.update(var_id_map) + id_node_map.update(latent_var_id_map) + ignored_dict = self.trigger[config_name].ignored_dict + enabled_dict = self.trigger[config_name].enabled_dict + for setting_id, node_inst in list(id_node_map.items()): + is_latent = False + section, option = self.util.get_section_option_from_id(setting_id) + is_section = (option is None) + if is_section: + if section not in sect_map: + is_latent = True + else: + if setting_id not in var_id_map: + is_latent = True + trig_cfg_node = trig_config.get([section, option]) + if trig_cfg_node is None: + # Latent variable or sections cannot be user-ignored. + if (setting_id in ignored_dict and + setting_id not in enabled_dict): + trig_cfg_state = syst_ignored_state + else: + trig_cfg_state = enabled_state + else: + trig_cfg_state = trig_cfg_node.state + if (trig_cfg_state == enabled_state and + not node_inst.ignored_reason): + # For speed, skip the rest of the checking. + # Doc table: E -> E + continue + comp_val = node_inst.metadata.get(rose.META_PROP_COMPULSORY) + node_is_compulsory = comp_val == rose.META_PROP_VALUE_TRUE + ignored_reasons = list(node_inst.ignored_reason.keys()) + if trig_cfg_state == syst_ignored_state: + # It should be trigger-ignored. + # Doc table: * -> I_t + info = ignored_dict.get(setting_id) + if rose.variable.IGNORED_BY_SYSTEM not in ignored_reasons: + help_str = ", ".join(list(info.values())) + if rose.variable.IGNORED_BY_USER in ignored_reasons: + # It is user-ignored but should be trigger-ignored. + # Doc table: I_u -> I_t + if node_is_compulsory: + # Doc table: I_u -> I_t -> compulsory + key = rose.config_editor.WARNING_TYPE_USER_IGNORED + val = getattr(rose.config_editor, + "WARNING_USER_NOT_TRIGGER_IGNORED") + node_inst.warning.update({key: val}) + else: + # Doc table: I_u -> I_t -> optional + pass + else: + # It is not ignored at all. + # Doc table: E -> I_t + if is_latent: + # Fix this for latent settings. + node_inst.ignored_reason.update({ + rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_CONFIG}) + else: + # Flag an error for real settings. + node_inst.error.update({ + rose.config_editor.WARNING_TYPE_ENABLED: + (rose.config_editor.WARNING_NOT_IGNORED + + help_str)}) + else: + # Otherwise, they both agree about trigger-ignored. + # Doc table: I_t -> I_t + pass + elif rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: + # It should be enabled, but is trigger-ignored. + # Doc table: I_t + if (setting_id in enabled_dict and + setting_id not in ignored_dict): + # It is a valid trigger. + # Doc table: I_t -> E + parents = self.trigger[config_name].enabled_dict.get( + setting_id) + help_str = (rose.config_editor.WARNING_NOT_ENABLED + + ', '.join(parents)) + err_type = rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED + node_inst.error.update({err_type: help_str}) + elif (setting_id not in enabled_dict and + setting_id not in ignored_dict): + # It is not a valid trigger. + # Doc table: I_t -> not trigger + if node_is_compulsory: + # This is an error for compulsory variables. + # Doc table: I_t -> not trigger -> compulsory + help_str = rose.config_editor.WARNING_NOT_TRIGGER + err_type = rose.config_editor.WARNING_TYPE_NOT_TRIGGER + node_inst.error.update({err_type: help_str}) + else: + # Overlook for optional variables. + # Doc table: I_t -> not trigger -> optional + pass + elif rose.variable.IGNORED_BY_USER in ignored_reasons: + # It possibly should be enabled, but is user-ignored. + # Doc table: I_u + # We've already covered I_u -> I_t + if node_is_compulsory: + # Compulsory settings should not be user-ignored. + # Doc table: I_u -> E -> compulsory + # Doc table: I_u -> not trigger -> compulsory + help_str = rose.config_editor.WARNING_NOT_USER_IGNORABLE + err_type = rose.config_editor.WARNING_TYPE_USER_IGNORED + node_inst.error.update({err_type: help_str}) + # Remaining possibilities are not a problem: + # Doc table: E -> E, E -> not trigger + + def load_file_metadata(self, config_name, section_name=None): + """Deal with file section variables.""" + if section_name is not None and not section_name.startswith("file:"): + return False + config = self.config[config_name].config + meta_config = self.config[config_name].meta + file_sections = [] + for section, sect_node in list(config.value.items()): + if not isinstance(sect_node.value, dict): + continue + if not sect_node.is_ignored() and section.startswith("file:"): + file_sections.append(section) + duplicate_file_sections = [] + for meta_id, sect_node in list(meta_config.value.items()): + section, option = self.util.get_section_option_from_id(meta_id) + if option is None: + if not isinstance(sect_node.value, dict): + continue + if not sect_node.is_ignored() and section.startswith("file:"): + file_sections.append(section) + if (sect_node.get_value([rose.META_PROP_DUPLICATE]) == + rose.META_PROP_VALUE_TRUE): + duplicate_file_sections.append(section) + # Remove metadata for individual duplicate sections - no need. + for section in list(file_sections): + if section in duplicate_file_sections: + continue + base_section = rose.macro.REC_ID_STRIP.sub("", section) + if base_section in duplicate_file_sections: + file_sections.remove(section) + file_ids = [] + for setting_id, sect_node in list(meta_config.value.items()): + # The following 'wildcard-esque' id is an exception. + # Wildcards are not supported in Rose metadata. + if not sect_node.is_ignored() and setting_id.startswith("file:*="): + file_ids.append(setting_id) + for section in file_sections: + title = meta_config.get_value([section, rose.META_PROP_TITLE]) + if title is None: + meta_config.set([section, rose.META_PROP_TITLE], + section.replace("file:", "", 1)) + for file_entry in file_ids: + sect_node = meta_config.get([file_entry]) + for meta_prop, opt_node in list(sect_node.value.items()): + if opt_node.is_ignored(): + continue + prop_val = opt_node.value + new_id = section + '=' + file_entry.replace( + 'file:*=', '', 1) + if meta_config.get([new_id, meta_prop]) is None: + meta_config.set([new_id, meta_prop], prop_val) + + def load_node_namespaces(self, config_name, only_this_section=None, + from_saved=False): + """Load namespaces for variables and sections.""" + config_sections = self.config[config_name].sections + config_vars = self.config[config_name].vars + for section, variables in config_vars.foreach(from_saved): + if only_this_section is not None and section != only_this_section: + continue + for variable in variables: + self.load_ns_for_node(variable, config_name) + section_objects = [] + if only_this_section is not None: + if only_this_section in config_sections.now: + section_objects = [config_sections.now[only_this_section]] + elif only_this_section in config_sections.latent: + section_objects = [config_sections.latent[only_this_section]] + else: + section_objects = config_sections.get_all(save=from_saved) + for sect_obj in section_objects: + self.load_ns_for_node(sect_obj, config_name) + + def load_ns_for_node(self, node, config_name): + """Load a namespace for a variable or section.""" + node_id = node.metadata.get('id') + section, option = self.util.get_section_option_from_id(node_id) + subspace = node.metadata.get(rose.META_PROP_NS) + if subspace is None or option is None: + new_namespace = self.helper.get_default_section_namespace( + section, config_name) + else: + new_namespace = config_name + '/' + subspace + if new_namespace == config_name + '/': + new_namespace = config_name + node.metadata['full_ns'] = new_namespace + return new_namespace + + def load_metadata_for_namespaces(self, config_name): + """Load namespace metadata, e.g. namespace titles.""" + config_data = self.config[config_name] + meta_config = config_data.meta + for setting_id, sect_node in list(meta_config.value.items()): + if sect_node.is_ignored(): + continue + section, option = self.util.get_section_option_from_id( + setting_id) + is_ns = (section == "ns") + is_duplicate_section = ( + self.util.get_section_option_from_id(section)[1] is None and + sect_node.get_value([rose.META_PROP_DUPLICATE]) == + rose.META_PROP_VALUE_TRUE + ) + if is_ns or is_duplicate_section: + if is_ns: + subspace = option + if subspace: + namespace = config_name + "/" + subspace + else: + namespace = config_name + else: + subspace = sect_node.get_value([rose.META_PROP_NS]) + if subspace is None: + namespace = ( + self.helper.get_default_section_namespace( + section, config_name)) + else: + if subspace: + namespace = config_name + "/" + subspace + else: + namespace = config_name + self.namespace_meta_lookup.setdefault(namespace, {}) + ns_metadata = self.namespace_meta_lookup[namespace] + for option, opt_node in list(sect_node.value.items()): + if opt_node.is_ignored(): + continue + value = meta_config[setting_id][option].value + if option == rose.META_PROP_MACRO: + if option in ns_metadata: + ns_metadata[option] += ", " + value + else: + ns_metadata[option] = value + else: + ns_metadata.update({option: value}) + ns_sections = {} # Namespace-sections key value pairs. + for variable in config_data.vars.get_all(): + ns = variable.metadata['full_ns'] + var_id = variable.metadata['id'] + sect = self.util.get_section_option_from_id(var_id)[0] + ns_sections.setdefault(ns, []) + if sect not in ns_sections[ns]: + ns_sections[ns].append(sect) + if rose.META_PROP_MACRO in variable.metadata: + macro_info = variable.metadata[rose.META_PROP_MACRO] + self.namespace_meta_lookup.setdefault(ns, {}) + ns_metadata = self.namespace_meta_lookup[ns] + if rose.META_PROP_MACRO in ns_metadata: + ns_metadata[rose.META_PROP_MACRO] += ", " + macro_info + else: + ns_metadata[rose.META_PROP_MACRO] = macro_info + default_ns_sections = {} + for section_data in config_data.sections.get_all(): + # Use the default section namespace. + ns = section_data.metadata["full_ns"] + ns_sections.setdefault(ns, []) + if section_data.name not in ns_sections[ns]: + ns_sections[ns].append(section_data.name) + default_ns_sections.setdefault(ns, []) + if section_data.name not in default_ns_sections[ns]: + default_ns_sections[ns].append(section_data.name) + for ns in ns_sections: + self.namespace_meta_lookup.setdefault(ns, {}) + ns_metadata = self.namespace_meta_lookup[ns] + ns_metadata['sections'] = ns_sections[ns] + for ns_section in ns_sections[ns]: + # Loop over metadata from contributing sections. + # Note: rogue-variable section metadata can be overridden. + metadata = self.helper.get_metadata_for_config_id(ns_section, + config_name) + for key, value in list(metadata.items()): + if (ns_section not in default_ns_sections.get(ns, []) and + key in [rose.META_PROP_TITLE, rose.META_PROP_SORT_KEY, + rose.META_PROP_DESCRIPTION]): + # ns created from variables, not a section - no title. + continue + if key == rose.META_PROP_MACRO: + macro_info = value + if key in ns_metadata: + ns_metadata[rose.META_PROP_MACRO] += ( + ", " + macro_info) + else: + ns_metadata[rose.META_PROP_MACRO] = macro_info + else: + ns_metadata.setdefault(key, value) + self.load_namespace_has_sub_data(config_name) + for config_name in list(self.config.keys()): + icon_path = self.helper.get_icon_path_for_config(config_name) + self.namespace_meta_lookup.setdefault(config_name, {}) + self.namespace_meta_lookup[config_name].setdefault( + "icon", icon_path) + if self.config[config_name].config_type == rose.TOP_CONFIG_NAME: + self.namespace_meta_lookup[config_name].setdefault( + rose.META_PROP_TITLE, + rose.config_editor.TITLE_PAGE_SUITE) + self.namespace_meta_lookup[config_name].setdefault( + rose.META_PROP_SORT_KEY, " 1") + elif self.config[config_name].config_type == rose.INFO_CONFIG_NAME: + self.namespace_meta_lookup[config_name].setdefault( + rose.META_PROP_TITLE, + rose.config_editor.TITLE_PAGE_INFO) + self.namespace_meta_lookup[config_name].setdefault( + rose.META_PROP_SORT_KEY, " 0") + + def load_namespace_has_sub_data(self, config_name=None): + """Load namespace sub-data status.""" + file_ns = "/" + rose.SUB_CONFIG_FILE_DIR + ns_hierarchy = {} + for ns in self.namespace_meta_lookup: + if config_name is None or ns.startswith(config_name): + parent_ns = ns.rsplit("/", 1)[0] + ns_hierarchy.setdefault(parent_ns, []) + ns_hierarchy[parent_ns].append(ns) + if config_name is None: + configs = list(self.config.keys()) + else: + configs = [config_name] + # File root pages have summary data for files. + for alt_config_name in configs: + file_root_ns = alt_config_name + file_ns + self.namespace_meta_lookup.setdefault(file_root_ns, {}) + self.namespace_meta_lookup[file_root_ns].setdefault( + "has_sub_data", True) + # Duplicate root pages have summary data for their members. + for ns, prop_map in list(self.namespace_meta_lookup.items()): + if config_name is not None and not ns.startswith(config_name): + continue + if (rose.META_PROP_DUPLICATE in prop_map and + ns_hierarchy.get(ns, [])): + prop_map.setdefault("has_sub_data", True) diff --git a/metomi/rose/config_editor/data_helper.py b/metomi/rose/config_editor/data_helper.py new file mode 100644 index 000000000..8ebb2fbdb --- /dev/null +++ b/metomi/rose/config_editor/data_helper.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re + +import rose.config + + +REC_ELEMENT_SECTION = re.compile(r"^(.*)\((.+)\)$") + + +class ConfigDataHelper(object): + + def __init__(self, data, util): + self.data = data + self.util = util + + def get_config_has_unsaved_changes(self, config_name): + """Return True if there are unsaved changes for config_name.""" + config_data = self.data.config[config_name] + variables = config_data.vars.get_all(skip_latent=True) + save_vars = config_data.vars.get_all(save=True, skip_latent=True) + sections = config_data.sections.get_all(skip_latent=True) + save_sections = config_data.sections.get_all(save=True, + skip_latent=True) + now_set = set([v.to_hashable() for v in variables]) + save_set = set([v.to_hashable() for v in save_vars]) + now_sect_set = set([s.to_hashable() for s in sections]) + save_sect_set = set([s.to_hashable() for s in save_sections]) + return (config_name not in self.data.saved_config_names or + now_set ^ save_set or + now_sect_set ^ save_sect_set) + + def get_config_meta_flag(self, config_name, from_this_config_obj=None): + """Return the metadata id flag.""" + for section, option in [ + [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_META_TYPE], + [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_PROJECT]]: + if from_this_config_obj is not None: + type_node = from_this_config_obj.get( + [section, option], no_ignore=True) + if type_node is not None and type_node.value: + return type_node.value + continue + id_ = self.util.get_id_from_section_option(section, option) + var = self.get_variable_by_id(id_, config_name) + if var is not None: + return var.value + return None + + def is_ns_sub_data(self, ns): + """Return whether a namespace is mentioned in summary data.""" + ns_meta = self.data.namespace_meta_lookup.get(ns, {}) + return ns_meta.get("has_sub_data", False) + + def is_ns_content(self, ns): + """Return whether a namespace has any existing content.""" + config_name = self.util.split_full_ns(self.data, ns)[0] + for section in self.get_sections_from_namespace(ns): + if section in self.data.config[config_name].sections.now: + return True + return self.is_ns_sub_data(ns) + + def get_metadata_for_config_id(self, node_id, config_name): + """Retrieve the corresponding metadata for a variable.""" + config_data = self.data.config[config_name] + meta_config = config_data.meta + if not node_id: + return {'id': node_id} + return rose.macro.get_metadata_for_config_id(node_id, meta_config) + + def get_variable_by_id(self, var_id, config_name, save=False, + latent=False): + """Return the matching variable or None.""" + sect, opt = self.util.get_section_option_from_id(var_id) + return self.data.config[config_name].vars.get_var( + sect, opt, save, skip_latent=not latent) + +# ----------------- Data model helper functions ------------------------------ + + def get_data_for_namespace(self, ns, from_saved=False): + """Return a list of vars and a list of latent vars for this ns.""" + config_name = self.util.split_full_ns(self.data, ns)[0] + config_data = self.data.config[config_name] + allowed_sections = self.get_sections_from_namespace(ns) + variables = [] + latents = [] + if from_saved: + var_map = config_data.vars.save + latent_var_map = config_data.vars.latent_save + else: + var_map = config_data.vars.now + latent_var_map = config_data.vars.latent + for section in allowed_sections: + variables.extend(var_map.get(section, [])) + latents.extend(latent_var_map.get(section, [])) + ns_vars = [v for v in variables if v.metadata.get('full_ns') == ns] + ns_latents = [v for v in latents if v.metadata.get('full_ns') == ns] + return ns_vars, ns_latents + + def get_macro_info_for_namespace(self, ns): + """Return some information for custom macros for this namespace.""" + config_name = self.util.split_full_ns(self, ns)[0] + config_data = self.data.config[config_name] + ns_macros_text = self.data.namespace_meta_lookup.get(ns, {}).get( + rose.META_PROP_MACRO, "") + if not ns_macros_text: + return {} + ns_macros = rose.variable.array_split(ns_macros_text, + only_this_delim=",") + module_prefix = self.get_macro_module_prefix(config_name) + for i, ns_macro in enumerate(ns_macros): + ns_macros[i] = module_prefix + ns_macro + ns_macro_info = {} + macro_tuples = rose.macro.get_macro_class_methods(config_data.macros) + for module_name, class_name, method_name, docstring in macro_tuples: + this_macro_name = ".".join([module_name, class_name]) + this_macro_method_name = ".".join([this_macro_name, method_name]) + this_info = (method_name, docstring) + if this_macro_name in ns_macros: + key = this_macro_name.replace(module_prefix, "", 1) + ns_macro_info.update({key: this_info}) + elif this_macro_method_name in ns_macros: + key = this_macro_method_name.replace(module_prefix, "", 1) + ns_macro_info.update({key: this_info}) + return ns_macro_info + + def get_section_data_for_namespace(self, ns): + """Return real and latent lists of Section objects for this ns.""" + allowed_sections = ( + self.data.helper.get_sections_from_namespace(ns)) + config_name = self.util.split_full_ns(self.data, ns)[0] + config_data = self.data.config[config_name] + real_sections = [] + for section, sect_data in list(config_data.sections.now.items()): + if section in allowed_sections: + real_sections.append(sect_data) + latent_sections = [] + for section, sect_data in list(config_data.sections.latent.items()): + if section in allowed_sections: + latent_sections.append(sect_data) + return real_sections, latent_sections + + def get_sub_data_for_namespace(self, ns, from_saved=False): + """Return any sections/variables below this namespace.""" + sub_data = {"sections": {}, "variables": {}} + config_name = self.util.split_full_ns(self.data, ns)[0] + config_data = self.data.config[config_name] + for sect, sect_data in list(config_data.sections.now.items()): + sect_ns = sect_data.metadata["full_ns"] + if sect_ns.startswith(ns): + sub_data["sections"].update({sect: sect_data}) + for sect, variables in list(config_data.vars.now.items()): + for variable in variables: + if variable.metadata["full_ns"].startswith(ns): + sub_data["variables"].setdefault(sect, []) + sub_data["variables"][sect].append(variable) + if not sub_data["sections"] and not sub_data["variables"]: + return None + return sub_data + + def get_sub_data_var_id_value_map(self, config_name): + """Return all real (=existing) variable values for sub data.""" + config_data = self.data.config[config_name] + var_id_val_map = {} + for variable in config_data.vars.get_all(): + var_id_val_map.update({variable.metadata["id"]: variable.value}) + return var_id_val_map + + def get_ns_comment_string(self, ns): + """Return a comment string for this namespace.""" + comment = "" + comments = [] + config_name = self.util.split_full_ns(self.data, ns)[0] + config_data = self.data.config[config_name] + sections = self.get_sections_from_namespace(ns) + sections.sort(rose.config.sort_settings) + for section in sections: + sect_data = config_data.sections.now.get(section) + if sect_data is not None and sect_data.comments: + comments.extend(sect_data.comments) + if comments: + comment = "#" + "\n#".join(comments) + return comment + + def get_ns_variable(self, var_id, ns): + """Return a variable with this id in the config specified by ns.""" + config_name = self.util.split_full_ns(self.data, ns)[0] + config_data = self.data.config[config_name] + sect, opt = self.util.get_section_option_from_id(var_id) + var = config_data.vars.get_var(sect, opt) + if var is None: + var = config_data.vars.get_var(sect, opt, save=True) + return var # May be None. + + def get_ns_url_for_variable(self, variable): + """Return the parent (ns or section) URL property, if any.""" + config_name = self.util.split_full_ns( + self.data, variable.metadata["full_ns"])[0] + ns_metadata = self.data.namespace_meta_lookup.get( + variable.metadata["full_ns"], {}) + ns_url = ns_metadata.get(rose.META_PROP_URL) + if ns_url: + return ns_url + section = self.util.get_section_option_from_id( + variable.metadata["id"])[0] + section_object = self.data.config[config_name].sections.get_sect( + section) + section_url = section_object.metadata.get(rose.META_PROP_URL) + return section_url + + def get_sections_from_namespace(self, namespace): + """Return all sections contributing to a namespace.""" + # FIXME: What about files? + ns_metadata = self.data.namespace_meta_lookup.get(namespace, {}) + sections = ns_metadata.get('sections', []) + if sections: + return [s for s in sections] + base, subsp = self.util.split_full_ns(self.data, namespace) + ns_section = subsp.replace('/', ':') + if ns_section in self.data.config[base].sections.now: + sect_data = self.data.config[base].sections.now[ns_section] + if sect_data.metadata["full_ns"] == namespace: + return [ns_section] + if ns_section in self.data.config[base].sections.latent: + sect_data = self.data.config[base].sections.latent[ns_section] + if sect_data.metadata["full_ns"] == namespace: + return [ns_section] + return [] + + def get_ns_is_default(self, namespace): + """Sets if this namespace is the default for a section. Slow!""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + allowed_sections = self.get_sections_from_namespace(namespace) + empty = True + for section in allowed_sections: + for variable in config_data.vars.now.get(section, []): + if variable.metadata['full_ns'] == namespace: + empty = False + if rose.META_PROP_NS not in variable.metadata: + return True + for variable in config_data.vars.latent.get(section, []): + if variable.metadata['full_ns'] == namespace: + empty = False + if rose.META_PROP_NS not in variable.metadata: + return True + if empty: + # An added, non-metadata section with no variables. + return True + return False + + def get_all_namespaces(self, only_this_config=None): + """Return all unique namespaces.""" + nses = list(self.data.namespace_meta_lookup.keys()) + if only_this_config is not None: + nses = [n for n in nses if n.startswith(only_this_config)] + return nses + + def get_missing_sections(self, config_name=None): + """Return full section ids that are missing.""" + full_sections = [] + if config_name is not None: + config_names = [config_name] + else: + config_names = list(self.data.config.keys()) + for config_name in config_names: + section_store = self.data.config[config_name].sections + miss_sections = [] + real_sections = list(section_store.now.keys()) + for section in list(section_store.latent.keys()): + if section not in real_sections: + miss_sections.append(section) + for section in self.data.config[config_name].vars.latent: + if (section not in real_sections and + section not in miss_sections): + miss_sections.append(section) + full_sections += [config_name + ':' + s for s in miss_sections] + sorter = rose.config.sort_settings + full_sections.sort(sorter) + return full_sections + + def get_default_section_namespace(self, section, config_name): + """Return the default namespace for the section.""" + if config_name not in self.data._config_section_namespace_map: + self.data._config_section_namespace_map.setdefault( + config_name, {}) + section_ns = ( + self.data._config_section_namespace_map[config_name].get( + section)) + if section_ns is None: + config_data = self.data.config[config_name] + meta_config = config_data.meta + node = meta_config.get( + [section, rose.META_PROP_NS], no_ignore=True) + if node is not None: + subspace = node.value + else: + match = REC_ELEMENT_SECTION.match(section) + if match: + node = meta_config.get( + [match.groups()[0], rose.META_PROP_NS]) + if node is None or node.is_ignored(): + subspace = section.replace('(', '/') + subspace = subspace.replace(')', '') + subspace = subspace.replace(':', '/') + else: + subspace = node.value + '/' + str(match.groups()[1]) + elif section.startswith(rose.SUB_CONFIG_FILE_DIR + ":"): + subspace = section.rstrip('/').replace('/', ':') + subspace = subspace.replace(':', '/', 1) + else: + subspace = section.rstrip('/').replace(':', '/') + section_ns = config_name + '/' + subspace + if not subspace: + section_ns = config_name + self.data._config_section_namespace_map[config_name].update( + {section: section_ns}) + return section_ns + + def get_format_sections(self, config_name): + """Return all format-like sections in the current data.""" + format_keys = [] + for section in self.data.config[config_name].sections.now: + if (section not in format_keys and + ':' in section and not section.startswith('file:')): + format_keys.append(section) + format_keys.sort(rose.config.sort_settings) + return format_keys + + def get_icon_path_for_config(self, config_name): + """Return the path to the config identifier icon or None.""" + icon_path = None + for filename in self.data.config[config_name].meta_files: + if filename.endswith('/images/icon.png'): + icon_path = filename + break + return icon_path + + def get_macro_module_prefix(self, config_name): + """Return a valid module-like name for macros.""" + return re.sub(r"[^\w]", "_", config_name.strip("/")) + "/" + + def get_ignored_sections(self, namespace, get_enabled=False): + """Return the user-ignored sections for this namespace. + + If namespace is a config_name, return all config ignored + sections. + + Return enabled sections instead if get_enabled is True. + + """ + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + if namespace == config_name: + sections = list(config_data.sections.now.keys()) + else: + sections = self.get_sections_from_namespace(namespace) + return_sections = [] + for section in sections: + sect_data = config_data.sections.get_sect(section) + if get_enabled: + if not sect_data.ignored_reason: + return_sections.append(section) + elif (rose.variable.IGNORED_BY_USER in + sect_data.ignored_reason): + return_sections.append(section) + return_sections.sort(rose.config.sort_settings) + return return_sections + + def get_latent_sections(self, namespace): + """Return the latent sections for this namespace.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + if namespace == config_name: + sections = list(config_data.sections.now.keys()) + else: + sections = self.get_sections_from_namespace(namespace) + return_sections = [] + for section in sections: + if section not in config_data.sections.now: + return_sections.append(section) + return_sections.sort(rose.config.sort_settings) + return return_sections + + def get_ns_ignored_status(self, namespace): + """Return the ignored status for a namespace's data.""" + cache = self.data.namespace_cached_statuses['ignored'] + if namespace in cache: + return cache[namespace] + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + sections = self.get_sections_from_namespace(namespace) + status = rose.config.ConfigNode.STATE_NORMAL + default_section_statuses = {} + variable_statuses = {} + for section in sections: + sect_data = config_data.sections.get_sect(section) + if sect_data is None: + continue + if sect_data.metadata["full_ns"] == namespace: + if not sect_data.ignored_reason: + cache[namespace] = status + return status + for key in sect_data.ignored_reason: + default_section_statuses.setdefault(key, 0) + default_section_statuses[key] += 1 + real_data, latent_data = self.get_data_for_namespace(namespace) + for var in real_data + latent_data: + if not var.ignored_reason: + cache[namespace] = status + return status + for key in var.ignored_reason: + if key == rose.variable.IGNORED_BY_SECTION: + # Section ignored statuses need interpreting. + var_id = var.metadata["id"] + section = self.util.get_section_option_from_id(var_id)[0] + sect_data = config_data.sections.get_sect(section) + for key2 in sect_data.ignored_reason: + variable_statuses.setdefault(key2, 0) + variable_statuses[key2] += 1 + else: + variable_statuses.setdefault(key, 0) + variable_statuses[key] += 1 + if not (variable_statuses or sections): + # No data, so no ignored state. + cache[namespace] = status + return status + # Now return the most 'popular' ignored status. + # Choose section statuses if any are default for this namespace. + if default_section_statuses: + object_statuses = default_section_statuses + else: + object_statuses = variable_statuses + status_counts = list(object_statuses.items()) + status_counts.sort(lambda x, y: cmp(x[1], y[1])) + if not status_counts: + cache[namespace] = status + return rose.config.ConfigNode.STATE_NORMAL + status = status_counts[0][0] + cache[namespace] = status + if status == rose.variable.IGNORED_BY_USER: + return rose.config.ConfigNode.STATE_USER_IGNORED + if status == rose.variable.IGNORED_BY_SYSTEM: + return rose.config.ConfigNode.STATE_SYST_IGNORED + return rose.config.ConfigNode.STATE_NORMAL + + def get_ns_latent_status(self, namespace): + """Return whether a page has no associated content.""" + cache = self.data.namespace_cached_statuses['latent'] + if namespace in cache: + return cache[namespace] + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + sections = self.get_sections_from_namespace(namespace) + for section in sections: + if section in config_data.sections.now: + # It has a current section associated. + section_namespace = ( + config_data.sections.now[section].metadata["full_ns"]) + if section_namespace == namespace: + # This is a default page for an existing section. + cache[namespace] = False + return False + for variable in config_data.vars.now.get(section, []): + if variable.metadata["full_ns"] == namespace: + # This contains an existing variable. + cache[namespace] = False + return False + cache[namespace] = True + return True + + def clear_namespace_cached_statuses(self, namespace): + """Reset cached latent, ignored, modified statuses for namespace.""" + if namespace in self.data.namespace_cached_statuses['ignored']: + self.data.namespace_cached_statuses['ignored'].pop(namespace) + if namespace in self.data.namespace_cached_statuses['latent']: + self.data.namespace_cached_statuses['latent'].pop(namespace) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py new file mode 100644 index 000000000..5f7e3e11d --- /dev/null +++ b/metomi/rose/config_editor/keywidget.py @@ -0,0 +1,486 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re + +from gi.repository import Pango +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +import rose.gtk.dialog +import rose.gtk.util +import rose.variable + + +class KeyWidget(Gtk.VBox): + + """This class generates a label or entry box for a variable name.""" + + FLAG_ICON_MAP = { + rose.config_editor.FLAG_TYPE_DEFAULT: Gtk.STOCK_INFO, + rose.config_editor.FLAG_TYPE_ERROR: Gtk.STOCK_DIALOG_WARNING, + rose.config_editor.FLAG_TYPE_FIXED: Gtk.STOCK_DIALOG_AUTHENTICATION, + rose.config_editor.FLAG_TYPE_OPT_CONF: Gtk.STOCK_INDEX, + rose.config_editor.FLAG_TYPE_OPTIONAL: Gtk.STOCK_ABOUT, + rose.config_editor.FLAG_TYPE_NO_META: Gtk.STOCK_DIALOG_QUESTION, + } + + MODIFIED_COLOUR = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_CHANGED) + + LABEL_X_OFFSET = 0.01 + + def __init__(self, variable, var_ops, launch_help_func, update_func, + show_modes): + super(KeyWidget, self).__init__(homogeneous=False, spacing=0) + self.my_variable = variable + self.hbox = Gtk.HBox() + self.hbox.show() + self.pack_start(self.hbox, expand=False, fill=False) + self.var_ops = var_ops + self.meta = variable.metadata + self.launch_help = launch_help_func + self.update_status = update_func + self.show_modes = show_modes + self.var_flags = [] + self._last_var_comments = None + self.ignored_label = Gtk.Label() + self.ignored_label.show() + self.hbox.pack_start(self.ignored_label, expand=False, fill=False) + self.set_ignored() + if self.my_variable.name != '': + self.entry = Gtk.Label() + self.entry.set_alignment( + self.LABEL_X_OFFSET, + self.entry.get_alignment()[1]) + self.entry.set_text(self.my_variable.name) + else: + self.entry = Gtk.Entry() + self.entry.modify_text(Gtk.StateType.NORMAL, + self.MODIFIED_COLOUR) + self.entry.connect("focus-out-event", + lambda w, e: self._setter(w, variable)) + event_box = Gtk.EventBox() + event_box.add(self.entry) + event_box.connect('enter-notify-event', + lambda b, w: self._handle_enter(b)) + event_box.connect('leave-notify-event', + lambda b, w: self._handle_leave(b)) + self.hbox.pack_start(event_box, expand=True, fill=True, + padding=0) + self.comments_box = Gtk.HBox() + self.hbox.pack_start(self.comments_box, expand=False, fill=False) + self.grab_focus = self.entry.grab_focus + self.set_sensitive(True) + self.set_sensitive = self._set_sensitive + event_box.connect('button-press-event', self.handle_launch_help) + self.update_comment_display() + self.entry.show() + for key, value in list(self.show_modes.items()): + if key not in [rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + rose.config_editor.SHOW_MODE_CUSTOM_HELP, + rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + self.set_show_mode(key, value) + if (rose.META_PROP_VALUES in self.meta and + len(self.meta[rose.META_PROP_VALUES]) == 1): + self.add_flag(rose.config_editor.FLAG_TYPE_FIXED, + rose.config_editor.VAR_FLAG_TIP_FIXED) + event_box.show() + self.show() + + def add_flag(self, flag_type, tooltip_text=None): + """Set the display of a flag denoting a property.""" + if flag_type in self.var_flags: + return + self.var_flags.append(flag_type) + stock_id = self.FLAG_ICON_MAP[flag_type] + event_box = Gtk.EventBox() + event_box._flag_type = flag_type + image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.MENU) + image.set_tooltip_text(tooltip_text) + image.show() + event_box.add(image) + event_box.show() + event_box.connect("button-press-event", self._toggle_flag_label) + self.hbox.pack_end(event_box, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + + def get_centre_height(self): + """Return the vertical displacement of the centre of this widget.""" + return (self.entry.size_request()[1] / 2) + + def handle_launch_help(self, widget, event): + """Handle launching help.""" + if event.type == Gdk.EventType.BUTTON_PRESS and event.button != 3: + url_mode = (rose.META_PROP_HELP not in self.meta) + self.launch_help(url_mode=url_mode) + + def launch_edit_comments(self, *args): + """Launch an edit comments dialog.""" + text = "\n".join(self.my_variable.comments) + title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( + self.my_variable.metadata['id']) + rose.gtk.dialog.run_edit_dialog(text, + finish_hook=self._edit_finish_hook, + title=title) + + def refresh(self, variable=None): + """Reload the contents - however, no need for this at present.""" + self.my_variable = variable + + def remove_flag(self, flag_type): + """Remove the flag from the widget.""" + for widget in self.get_children(): + if (isinstance(widget, Gtk.EventBox) and + getattr(widget, "_flag_type", None) == flag_type): + self.remove(widget) + if flag_type in self.var_flags: + self.var_flags.remove(flag_type) + return True + + def set_ignored(self): + """Update the ignored display.""" + self.ignored_label.set_markup( + rose.variable.get_ignored_markup(self.my_variable)) + hover_string = "" + if not self.my_variable.ignored_reason: + self.ignored_label.set_tooltip_text(None) + for key, value in sorted(self.my_variable.ignored_reason.items()): + hover_string += key + " " + value + "\n" + self.ignored_label.set_tooltip_text(hover_string.strip()) + + def set_modified(self, is_modified): + """Set the display of modified status in the text.""" + if is_modified: + if isinstance(self.entry, Gtk.Label): + att_list = self.entry.get_attributes() + if att_list is None: + att_list = Pango.AttrList() + att_list.insert(Pango.AttrForeground( + self.MODIFIED_COLOUR.red, + self.MODIFIED_COLOUR.green, + self.MODIFIED_COLOUR.blue, + start_index=0, + end_index=-1)) + self.entry.set_attributes(att_list) + else: + if isinstance(self.entry, Gtk.Label): + att_list = self.entry.get_attributes() + if att_list is not None: + att_list = att_list.filter( + lambda a: a.type != Pango.ATTR_FOREGROUND) + + if att_list is None: + att_list = Pango.AttrList() + self.entry.set_attributes(att_list) + + def set_show_mode(self, show_mode, should_show_mode): + """Set the display of a mode on or off.""" + if show_mode in [rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + rose.config_editor.SHOW_MODE_CUSTOM_HELP, + rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + return self._set_show_custom_meta_text(show_mode, should_show_mode) + if show_mode == rose.config_editor.SHOW_MODE_NO_TITLE: + return self._set_show_title(not should_show_mode) + if show_mode == rose.config_editor.SHOW_MODE_NO_DESCRIPTION: + return self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + not should_show_mode) + if show_mode == rose.config_editor.SHOW_MODE_NO_HELP: + return self._set_show_meta_text_mode(rose.META_PROP_HELP, + not should_show_mode) + if show_mode == rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: + if (should_show_mode and + self.meta.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE): + return self.add_flag( + rose.config_editor.FLAG_TYPE_OPTIONAL, + rose.config_editor.VAR_FLAG_TIP_OPTIONAL) + return self.remove_flag(rose.config_editor.FLAG_TYPE_OPTIONAL) + if show_mode == rose.config_editor.SHOW_MODE_FLAG_NO_META: + if should_show_mode and len(self.meta) <= 2: + return self.add_flag(rose.config_editor.FLAG_TYPE_NO_META, + rose.config_editor.VAR_FLAG_TIP_NO_META) + return self.remove_flag(rose.config_editor.FLAG_TYPE_NO_META) + if show_mode == rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: + if (should_show_mode and rose.config_editor.FLAG_TYPE_OPT_CONF in + self.my_variable.flags): + opts_info = self.my_variable.flags[ + rose.config_editor.FLAG_TYPE_OPT_CONF] + info_text = "" + info_format = rose.config_editor.VAR_FLAG_TIP_OPT_CONF_INFO + for opt, diff in sorted(opts_info.items()): + info_text += info_format.format(opt, diff) + info_text = info_text.rstrip() + if info_text: + text = rose.config_editor.VAR_FLAG_TIP_OPT_CONF.format( + info_text) + return self.add_flag( + rose.config_editor.FLAG_TYPE_OPT_CONF, text) + return self.remove_flag(rose.config_editor.FLAG_TYPE_OPT_CONF) + + def update_comment_display(self): + """Update the display of variable comments.""" + if self.my_variable.comments == self._last_var_comments: + return + self._last_var_comments = self.my_variable.comments + if (self.my_variable.comments or + rose.config_editor.SHOULD_SHOW_ALL_COMMENTS): + tip_fmt = rose.config_editor.VAR_COMMENT_TIP + comments = [tip_fmt.format(c) for c in self.my_variable.comments] + tooltip_text = "\n".join(comments) + comment_widgets = self.comments_box.get_children() + if comment_widgets: + comment_widgets[0].set_tooltip_text(tooltip_text) + else: + edit_eb = Gtk.EventBox() + edit_eb.show() + edit_label = Gtk.Label(label="#") + edit_label.show() + edit_eb.add(edit_label) + edit_eb.set_tooltip_text(tooltip_text) + edit_eb.connect("button-press-event", + self._handle_comment_click) + edit_eb.connect("enter-notify-event", + self._handle_comment_enter_leave, True) + edit_eb.connect("leave-notify-event", + self._handle_comment_enter_leave, False) + self.comments_box.pack_start( + edit_eb, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + self.comments_box.show() + else: + self.comments_box.hide() + + def _get_metadata_formatting(self, mode): + """Apply the correct formatting for a metadata property.""" + mode_format = "{" + mode + "}" + if (mode == rose.META_PROP_DESCRIPTION and + self.show_modes[ + rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION]): + mode_format = rose.config_editor.CUSTOM_FORMAT_DESCRIPTION + if (mode == rose.META_PROP_HELP and + self.show_modes[rose.config_editor.SHOW_MODE_CUSTOM_HELP]): + mode_format = rose.config_editor.CUSTOM_FORMAT_HELP + if (mode == rose.META_PROP_TITLE and + self.show_modes[rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + mode_format = rose.config_editor.CUSTOM_FORMAT_TITLE + mode_string = rose.variable.expand_format_string(mode_format, + self.my_variable) + if mode_string is None: + return self.my_variable.metadata[mode] + return mode_string + + def _set_show_custom_meta_text(self, mode, should_show_mode): + """Set the display of a custom format for a metadata property.""" + if mode == rose.config_editor.SHOW_MODE_CUSTOM_TITLE: + return self._set_show_title( + not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]) + if mode == rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: + is_shown = not self.show_modes[ + rose.config_editor.SHOW_MODE_NO_DESCRIPTION] + if is_shown: + self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + False) + self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + True) + if mode == rose.config_editor.SHOW_MODE_CUSTOM_HELP: + is_shown = not self.show_modes[ + rose.config_editor.SHOW_MODE_NO_HELP] + if is_shown: + self._set_show_meta_text_mode(rose.META_PROP_HELP, False) + self._set_show_meta_text_mode(rose.META_PROP_HELP, True) + + def _set_show_meta_text_mode(self, mode, should_show_mode): + """Set the display of description or help below the title/name.""" + if should_show_mode: + search_func = lambda i: self.var_ops.search_for_var( + self.meta["full_ns"], i) + if mode not in self.meta: + return + mode_text = self._get_metadata_formatting(mode) + mode_text = rose.gtk.util.safe_str(mode_text) + mode_text = rose.config_editor.VAR_FLAG_MARKUP.format(mode_text) + label = rose.gtk.util.get_hyperlink_label(mode_text, search_func) + label.show() + hbox = Gtk.HBox() + hbox.show() + hbox.pack_start(label, expand=False, fill=False) + hbox.set_sensitive(self.entry.get_property("sensitive")) + hbox._show_mode = mode + self.pack_start(hbox, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + show_mode_widget_indices = [] + for i, widget in enumerate(self.get_children()): + if hasattr(widget, "_show_mode"): + show_mode_widget_indices.append((widget._show_mode, i)) + show_mode_widget_indices.sort() + for j, (show_mode, i) in enumerate(show_mode_widget_indices): + if show_mode == mode and j < len(show_mode_widget_indices) - 1: + # The new widget goes before the next one alphabetically. + new_index = show_mode_widget_indices[j + 1][1] + self.reorder_child(hbox, new_index) + break + else: + for widget in self.get_children(): + if (isinstance(widget, Gtk.HBox) and + hasattr(widget, "_show_mode") and + widget._show_mode == mode): + self.remove(widget) + + def _set_show_title(self, should_show_title): + """Set the display of a variable title instead of the name.""" + if not self.my_variable.name: + return False + if should_show_title: + if rose.META_PROP_TITLE in self.meta: + title_string = self._get_metadata_formatting( + rose.META_PROP_TITLE) + if title_string != self.entry.get_text(): + return self.entry.set_text(title_string) + if self.entry.get_text() != self.my_variable.name: + self.entry.set_text(self.my_variable.name) + + def _toggle_flag_label(self, event_box, event, text=None): + """Toggle a label describing the flag.""" + flag_type = event_box._flag_type + if text is None: + text = event_box.get_child().get_tooltip_text() + for widget in self.get_children(): + if (hasattr(widget, "_flag_type") and + widget._flag_type == flag_type): + return self.remove(widget) + label = Gtk.Label() + markup = rose.gtk.util.safe_str(text) + markup = rose.config_editor.VAR_FLAG_MARKUP.format(markup) + label.set_markup(markup) + label.show() + hbox = Gtk.HBox() + hbox._flag_type = flag_type + hbox.pack_start(label, expand=False, fill=False) + hbox.set_sensitive(self.entry.get_property("sensitive")) + hbox.show() + self.pack_start(hbox, expand=False, fill=False) + + def _edit_finish_hook(self, text): + self.var_ops.set_var_comments(self.my_variable, text.splitlines()) + self.update_status() + + def _handle_comment_enter_leave(self, widget, event, is_entering=False): + label = widget.get_child() + self._set_underline(label, underline=is_entering) + + def _handle_comment_click(self, widget, event): + if event.button == 1: + self.launch_edit_comments() + + def _handle_enter(self, event_box): + label_text = self.entry.get_text() + tooltip_text = "" + if rose.META_PROP_DESCRIPTION in self.meta: + tooltip_text = self._get_metadata_formatting( + rose.META_PROP_DESCRIPTION) + if rose.META_PROP_TITLE in self.meta: + if self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]: + # Titles are hidden, so show them in the hover-over. + tooltip_text += ("\n (" + + rose.META_PROP_TITLE.capitalize() + + ": '" + + self.meta[rose.META_PROP_TITLE] + "')") + elif (self.my_variable.name not in label_text or + not self.show_modes[ + rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + # No custom title, or a custom title without the name. + tooltip_text += ("\n (" + self.my_variable.name + ")") + if self.my_variable.comments: + tip_fmt = rose.config_editor.VAR_COMMENT_TIP + if tooltip_text: + tooltip_text += "\n" + comments = [tip_fmt.format(c) for c in self.my_variable.comments] + tooltip_text += "\n".join(comments) + changes = self.var_ops.get_var_changes(self.my_variable) + if changes != '' and tooltip_text != '': + tooltip_text += '\n\n' + changes + else: + tooltip_text += changes + tooltip_text.strip() + if tooltip_text == '': + tooltip_text = None + event_box.set_tooltip_text(tooltip_text) + if (rose.META_PROP_URL not in self.meta and + 'http://' in self.my_variable.value): + new_url = re.search('(http://[^ ]+)', + self.my_variable.value).group() + # This is not very nice. + self.meta.update({rose.META_PROP_URL: new_url}) + if rose.META_PROP_HELP in self.meta or rose.META_PROP_URL in self.meta: + if isinstance(self.entry, Gtk.Label): + self._set_underline(self.entry, underline=True) + return False + + def _set_underline(self, label, underline=False): + # Set an underline in a label widget. + att_list = label.get_attributes() + if att_list is None: + att_list = Pango.AttrList() + if underline: + att_list.insert(Pango.AttrUnderline(Pango.Underline.SINGLE, + start_index=0, + end_index=-1)) + else: + att_list = att_list.filter(lambda a: + a.type != Pango.ATTR_UNDERLINE) + if att_list is None: + att_list = Pango.AttrList() + label.set_attributes(att_list) + + def _handle_leave(self, event_box): + event_box.set_tooltip_text(None) + if isinstance(self.entry, Gtk.Label): + self._set_underline(self.entry, underline=False) + return False + + def _set_sensitive(self, is_sensitive): + self.entry.set_sensitive(is_sensitive) + for child in self.get_children(): + if hasattr(child, "_flag_type") or hasattr(child, "_show_mode"): + child.set_sensitive(is_sensitive) + + def _setter(self, widget, variable): + """Re-set the name of the variable in the dictionary object.""" + new_name = widget.get_text() + if variable.name != new_name: + section = variable.metadata['id'].split(rose.CONFIG_DELIMITER)[0] + if section.startswith("namelist:"): + if new_name.lower() != new_name: + text = rose.config_editor.DIALOG_BODY_NL_CASE_CHANGE + text = text.format(new_name.lower()) + title = rose.config_editor.DIALOG_TITLE_NL_CASE_WARNING + new_name = rose.gtk.dialog.run_choices_dialog( + text, [new_name.lower(), new_name], + title) + if new_name is None: + return None + self.var_ops.remove_var(variable) + variable.name = new_name + variable.metadata['id'] = (section + rose.CONFIG_DELIMITER + + variable.name) + self.var_ops.add_var(variable) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py new file mode 100644 index 000000000..19397ee63 --- /dev/null +++ b/metomi/rose/config_editor/main.py @@ -0,0 +1,2024 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +""" +This module contains the core processing of the config editor. + +Classes: + MainController - driver for loading and central coordination. + +""" + +import cProfile +import os +import pstats +import re +import shutil +import sre_constants +import sys +import tempfile +import warnings + +# Ignore add menu related warnings for now, but remove this later. +warnings.filterwarnings('ignore', + 'instance of invalid non-instantiatable type', + Warning) +warnings.filterwarnings('ignore', + 'g_signal_handlers_disconnect_matched', + Warning) +warnings.filterwarnings('ignore', + 'use set_markup', + Warning) +warnings.filterwarnings('ignore', + 'Unable to show', + Warning) +warnings.filterwarnings('ignore', + 'gdk', + Warning) + +import gi +gi.require_version('Gtk', '3.0') +import gtk # Only used to run the main gtk loop. + +import rose.config +import rose.config_editor +import rose.config_editor.data +import rose.config_editor.menu +import rose.config_editor.nav_controller +import rose.config_editor.nav_panel +import rose.config_editor.nav_panel_menu +import rose.config_editor.ops.group +import rose.config_editor.ops.section +import rose.config_editor.ops.variable +import rose.config_editor.page +import rose.config_editor.stack +import rose.config_editor.status +import rose.config_editor.updater +import rose.config_editor.util +import rose.config_editor.variable +import rose.config_editor.window +import rose.gtk.dialog +import rose.gtk.splash +import rose.gtk.util +import rose.macro +import rose.opt_parse +import rose.resource +import rose.macros + + +class MainController(object): + + """The main controller class. + + Call with a configuration directory and/or a dict of + configuration names and objects. + + pluggable is a boolean that if True, returns containers for + plugging into other GTK applications. If pluggable is False, + launch the standalone application. + + load_updater is a rose.gtk.splash.SplashScreenProcess instance or + None, in which case it will be set to a + rose.gtk.splash.NullSplashScreenProcess. + + load_all_apps is a boolean that overrides the load-on-demand + automation to always load all sub configurations at start time. + + load_no_apps is a boolean that overrides the load-on-demand + automation to always skip loading sub configurations at start time. + + metadata_off is a boolean that controls whether the suite or app + should load with metadata on or off. + + """ + + RE_ARRAY_ELEMENT = re.compile(r'\([\d:, ]+\)$') + + def __init__(self, config_directory=None, config_objs=None, + config_obj_types=None, pluggable=False, load_updater=None, + load_all_apps=False, load_no_apps=False, metadata_off=False, + opt_meta_paths=None, no_warn=None): + if config_objs is None: + config_objs = {} + if pluggable: + rose.macro.add_meta_paths() + if load_updater is None: + load_updater = rose.gtk.splash.NullSplashScreenProcess() + self.is_pluggable = pluggable + self.tab_windows = [] # No child windows yet + self.orphan_pages = [] + self.undo_stack = [] # Nothing to undo yet + self.redo_stack = [] # Nothing to redo yet + self.find_hist = {'regex': '', 'ids': []} + self.util = rose.config_editor.util.Lookup() + self.metadata_off = metadata_off + if opt_meta_paths is None: + opt_meta_paths = [] + + # Set page variable 'verbosity' defaults. + self.page_var_show_modes = { + rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: + rose.config_editor.SHOULD_SHOW_CUSTOM_DESCRIPTION, + rose.config_editor.SHOW_MODE_CUSTOM_HELP: + rose.config_editor.SHOULD_SHOW_CUSTOM_HELP, + rose.config_editor.SHOW_MODE_CUSTOM_TITLE: + rose.config_editor.SHOULD_SHOW_CUSTOM_TITLE, + rose.config_editor.SHOW_MODE_FIXED: + rose.config_editor.SHOULD_SHOW_FIXED_VARS, + rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: + rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS, + rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: + rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS, + rose.config_editor.SHOW_MODE_FLAG_NO_META: + rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS, + rose.config_editor.SHOW_MODE_IGNORED: + rose.config_editor.SHOULD_SHOW_IGNORED_VARS, + rose.config_editor.SHOW_MODE_USER_IGNORED: + rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS, + rose.config_editor.SHOW_MODE_LATENT: + rose.config_editor.SHOULD_SHOW_LATENT_VARS, + rose.config_editor.SHOW_MODE_NO_DESCRIPTION: + rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION, + rose.config_editor.SHOW_MODE_NO_HELP: + rose.config_editor.SHOULD_SHOW_NO_HELP, + rose.config_editor.SHOW_MODE_NO_TITLE: + rose.config_editor.SHOULD_SHOW_NO_TITLE + } + + # Set page tree 'verbosity' defaults. + self.page_ns_show_modes = { + rose.config_editor.SHOW_MODE_IGNORED: + rose.config_editor.SHOULD_SHOW_IGNORED_PAGES, + rose.config_editor.SHOW_MODE_USER_IGNORED: + rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES, + rose.config_editor.SHOW_MODE_LATENT: + rose.config_editor.SHOULD_SHOW_LATENT_PAGES, + rose.config_editor.SHOW_MODE_NO_TITLE: + rose.config_editor.SHOULD_SHOW_NO_TITLE + } + + self.reporter = rose.config_editor.status.StatusReporter( + load_updater, + self.update_status_text + ) + + # Load the top configuration directory + self.data = rose.config_editor.data.ConfigDataManager( + self.util, + self.reporter, + self.page_ns_show_modes, + self.reload_namespace_tree, + opt_meta_paths=opt_meta_paths, + no_warn=no_warn + ) + + self.nav_controller = ( + rose.config_editor.nav_controller.NavTreeManager( + self.data, + self.util, + self.reporter, + self.tree_trigger_update + )) + + self.mainwindow = rose.config_editor.window.MainWindow() + + self.section_ops = rose.config_editor.ops.section.SectionOperations( + self.data, self.util, self.reporter, + self.undo_stack, self.redo_stack, + self.check_cannot_enable_setting, + self.update_namespace, + self.update_namespace_sub_data, + self.update_ns_info, + update_tree_func=self.reload_namespace_tree, + view_page_func=self.view_page, + kill_page_func=self.kill_page + ) + + self.variable_ops = ( + rose.config_editor.ops.variable.VariableOperations( + self.data, self.util, self.reporter, + self.undo_stack, self.redo_stack, + self.section_ops.add_section, + self.check_cannot_enable_setting, + self.update_namespace, + search_id_func=self.perform_find_by_id + )) + + self.group_ops = rose.config_editor.ops.group.GroupOperations( + self.data, self.util, self.reporter, + self.undo_stack, self.redo_stack, + self.section_ops, + self.variable_ops, + self.view_page, + self.update_ns_sub_data, + self.reload_namespace_tree + ) + + # Add in the main menu bar and tool bar handler. + self.main_handle = rose.config_editor.menu.MainMenuHandler( + self.data, self.util, self.reporter, + self.mainwindow, + self.undo_stack, self.redo_stack, + self.perform_undo, + self.update_config, + self.apply_macro_transform, + self.apply_macro_validation, + self.group_ops, + self.section_ops, + self.variable_ops, + self.perform_find_by_ns_id + ) + + # Add in the navigation panel menu handler. + self.nav_handle = rose.config_editor.nav_panel_menu.NavPanelHandler( + self.data, self.util, self.reporter, + self.mainwindow, + self.undo_stack, self.redo_stack, + self._add_config, + self.group_ops, + self.section_ops, + self.variable_ops, + self.kill_page, + self.reload_namespace_tree, + self.main_handle.transform_default, + self.main_handle.launch_graph + ) + + self.updater = rose.config_editor.updater.Updater( + self.data, self.util, self.reporter, + self.mainwindow, self.main_handle, + self.nav_controller, + self._get_pagelist, + self.update_bar_widgets, + self._refresh_metadata_if_on, + self.is_pluggable + ) + + self.data.load(config_directory, config_objs, + config_obj_type_dict=config_obj_types, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps) + + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_STATUSES.format( + self.data.top_level_name) + ) + + if not self.is_pluggable: + self.generate_toolbar() + self.generate_menubar() + self.generate_nav_panel() + self.generate_status_bar() + # Create notebook (tabbed container) and connect signals. + self.notebook = rose.gtk.util.Notebook() + + self.updater.nav_panel = getattr(self, "nav_panel", None) + + # Create the main panel with the menu, toolbar, tree panel, notebook. + if not self.is_pluggable: + self.mainwindow.load(name=self.data.top_level_name, + menu=self.top_menu, + accelerators=self.menubar.accelerators, + toolbar=self.toolbar, + nav_panel=self.nav_panel, + status_bar=self.status_bar, + notebook=self.notebook, + page_change_func=self.handle_page_change, + save_func=self.save_to_file) + self.mainwindow.window.connect('destroy', self.main_handle.destroy) + self.mainwindow.window.connect('delete-event', + self.main_handle.destroy) + self.mainwindow.window.connect_after('grab_focus', + self.handle_page_change) + self.mainwindow.window.connect_after('focus-in-event', + self.handle_page_change) + self.updater.update_all(is_loading=True) + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_ERRORS.format( + self.data.top_level_name, + self.updater.load_errors + )) + self.updater.perform_startup_check() + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_DONE.format( + self.data.top_level_name + )) + if (self.data.top_level_directory is None and not self.data.config): + self.load_from_file() + + self.update_bar_widgets() + + self.performing_undo = False + +# ----------------- Setting up main component functions ---------------------- + + def generate_toolbar(self): + """Link in the toolbar functionality.""" + self.toolbar = rose.gtk.util.ToolBar( + widgets=[ + (rose.config_editor.TOOLBAR_OPEN, 'Gtk.STOCK_OPEN'), + (rose.config_editor.TOOLBAR_SAVE, 'Gtk.STOCK_SAVE'), + (rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + 'Gtk.STOCK_SPELL_CHECK'), + (rose.config_editor.TOOLBAR_LOAD_APPS, 'Gtk.STOCK_CDROM'), + (rose.config_editor.TOOLBAR_BROWSE, 'Gtk.STOCK_DIRECTORY'), + (rose.config_editor.TOOLBAR_UNDO, 'Gtk.STOCK_UNDO'), + (rose.config_editor.TOOLBAR_REDO, 'Gtk.STOCK_REDO'), + (rose.config_editor.TOOLBAR_ADD, 'Gtk.STOCK_ADD'), + (rose.config_editor.TOOLBAR_REVERT, + 'Gtk.STOCK_REVERT_TO_SAVED'), + (rose.config_editor.TOOLBAR_FIND, 'Gtk.Entry'), + (rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), + (rose.config_editor.TOOLBAR_VALIDATE, + 'Gtk.STOCK_DIALOG_QUESTION'), + (rose.config_editor.TOOLBAR_TRANSFORM, + 'Gtk.STOCK_CONVERT'), + (rose.config_editor.TOOLBAR_VIEW_OUTPUT, + 'Gtk.STOCK_DIRECTORY'), + (rose.config_editor.TOOLBAR_SUITE_GCONTROL, + 'rose-gtk-scheduler') + ], + sep_on_name=[ + rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + rose.config_editor.TOOLBAR_BROWSE, + rose.config_editor.TOOLBAR_REDO, + rose.config_editor.TOOLBAR_REVERT, + rose.config_editor.TOOLBAR_FIND_NEXT, + rose.config_editor.TOOLBAR_TRANSFORM + ] + ) + assign = self.toolbar.set_widget_function + assign(rose.config_editor.TOOLBAR_OPEN, self.load_from_file) + assign(rose.config_editor.TOOLBAR_SAVE, self.save_to_file) + assign(rose.config_editor.TOOLBAR_CHECK_AND_SAVE, self.save_to_file, + [None, True]) + assign(rose.config_editor.TOOLBAR_LOAD_APPS, self.handle_load_all) + assign(rose.config_editor.TOOLBAR_BROWSE, + self.main_handle.launch_browser) + assign(rose.config_editor.TOOLBAR_UNDO, self.perform_undo) + assign(rose.config_editor.TOOLBAR_REDO, self.perform_undo, [True]) + assign(rose.config_editor.TOOLBAR_REVERT, self.revert_to_saved_data) + assign(rose.config_editor.TOOLBAR_FIND_NEXT, self._launch_find) + assign(rose.config_editor.TOOLBAR_VALIDATE, + self.main_handle.check_all_extra) + assign(rose.config_editor.TOOLBAR_TRANSFORM, + self.main_handle.transform_default) + assign(rose.config_editor.TOOLBAR_VIEW_OUTPUT, + self.main_handle.launch_output_viewer) + assign(rose.config_editor.TOOLBAR_SUITE_GCONTROL, + self.main_handle.launch_scheduler) + self.find_entry = self.toolbar.item_dict.get( + rose.config_editor.TOOLBAR_FIND)['widget'] + self.find_entry.connect("activate", self._launch_find) + self.find_entry.connect("changed", self._clear_find) + add_icon = self.toolbar.item_dict.get( + rose.config_editor.TOOLBAR_ADD)['widget'] + add_icon.connect('button_press_event', self.add_page_variable) + custom_text = rose.config_editor.TOOLBAR_SUITE_RUN_MENU + self._toolbar_run_button = rose.gtk.util.CustomMenuButton( + stock_id=Gtk.STOCK_MEDIA_PLAY, + menu_items=[(custom_text, Gtk.STOCK_MEDIA_PLAY)], + menu_funcs=[self.main_handle.get_run_suite_args], + tip_text=rose.config_editor.TOOLBAR_SUITE_RUN) + self._toolbar_run_button.connect("clicked", self.main_handle.run_suite) + self.toolbar.insert(self._toolbar_run_button, -1) + + self.toolbar.set_widget_sensitive( + rose.config_editor.TOOLBAR_SUITE_GCONTROL, + any(c.config_type == rose.TOP_CONFIG_NAME + for c in list(self.data.config.values()))) + + self.toolbar.set_widget_sensitive( + rose.config_editor.TOOLBAR_VIEW_OUTPUT, + any(c.config_type == rose.TOP_CONFIG_NAME + for c in list(self.data.config.values()))) + + def generate_menubar(self): + """Link in the menu functionality and accelerators.""" + self.menubar = rose.config_editor.menu.MenuBar() + self.menu_widgets = {} + menu_list = [ + ('/TopMenuBar/File/Open...', self.load_from_file), + ('/TopMenuBar/File/Save', lambda m: self.save_to_file()), + ('/TopMenuBar/File/Check and save', + lambda m: self.save_to_file(check_on_save=True)), + ('/TopMenuBar/File/Load All Apps', + lambda m: self.handle_load_all()), + ('/TopMenuBar/File/Quit', self.main_handle.destroy), + ('/TopMenuBar/Edit/Undo', + lambda m: self.perform_undo()), + ('/TopMenuBar/Edit/Redo', + lambda m: self.perform_undo(redo_mode_on=True)), + ('/TopMenuBar/Edit/Find', self._launch_find), + ('/TopMenuBar/Edit/Find Next', + lambda m: self.perform_find(self.find_hist['regex'])), + ('/TopMenuBar/Edit/Preferences', self.main_handle.prefs), + ('/TopMenuBar/Edit/Stack', self.main_handle.view_stack), + ('/TopMenuBar/View/View fixed vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_FIXED, + m.get_active() + )), + ('/TopMenuBar/View/View ignored vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_IGNORED, + m.get_active() + )), + ('/TopMenuBar/View/View user-ignored vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_USER_IGNORED, + m.get_active() + )), + ('/TopMenuBar/View/View latent vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_LATENT, + m.get_active() + )), + ('/TopMenuBar/View/View ignored pages', + lambda m: self._set_page_ns_show_modes( + rose.config_editor.SHOW_MODE_IGNORED, + m.get_active() + )), + ('/TopMenuBar/View/View user-ignored pages', + lambda m: self._set_page_ns_show_modes( + rose.config_editor.SHOW_MODE_USER_IGNORED, + m.get_active() + )), + ('/TopMenuBar/View/View latent pages', + lambda m: self._set_page_ns_show_modes( + rose.config_editor.SHOW_MODE_LATENT, + m.get_active() + )), + ('/TopMenuBar/View/Flag no-metadata vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_FLAG_NO_META, + m.get_active() + )), + ('/TopMenuBar/View/Flag opt config vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_FLAG_OPT_CONF, + m.get_active() + )), + ('/TopMenuBar/View/Flag optional vars', + lambda m: self._set_page_var_show_modes( + rose.config_editor.SHOW_MODE_FLAG_OPTIONAL, + m.get_active() + )), + ('/TopMenuBar/View/View status bar', + lambda m: self._set_show_status_bar(m.get_active())), + ('/TopMenuBar/Metadata/Prefs/View without descriptions', + lambda m: self._set_page_show_modes( + rose.config_editor.SHOW_MODE_NO_DESCRIPTION, + m.get_active() + )), + ('/TopMenuBar/Metadata/Prefs/View without help', + lambda m: self._set_page_show_modes( + rose.config_editor.SHOW_MODE_NO_HELP, + m.get_active() + )), + ('/TopMenuBar/Metadata/Prefs/View without titles', + lambda m: self._set_page_show_modes( + rose.config_editor.SHOW_MODE_NO_TITLE, + m.get_active() + )), + ('/TopMenuBar/Metadata/All V', + lambda m: self.main_handle.handle_run_custom_macro( + method_name=rose.macro.VALIDATE_METHOD + )), + ('/TopMenuBar/Metadata/Autofix', + lambda m: self.main_handle.transform_default()), + ('/TopMenuBar/Metadata/Extra checks', + lambda m: self.main_handle.check_fail_rules()), + ('/TopMenuBar/Metadata/Graph', + lambda m: self.main_handle.handle_graph()), + ('/TopMenuBar/Metadata/Reload metadata', + lambda m: self._refresh_metadata_if_on()), + ('/TopMenuBar/Metadata/Load custom metadata', + lambda m: self.load_custom_metadata()), + ('/TopMenuBar/Metadata/Switch off metadata', + lambda m: self.refresh_metadata(m.get_active())), + ('/TopMenuBar/Metadata/Upgrade', + lambda m: self.main_handle.handle_upgrade()), + ('/TopMenuBar/Tools/Run Suite/Run Suite default', + self.main_handle.run_suite), + ('/TopMenuBar/Tools/Run Suite/Run Suite custom', + self.main_handle.get_run_suite_args), + ('/TopMenuBar/Tools/Browser', + lambda m: self.main_handle.launch_browser()), + ('/TopMenuBar/Tools/Terminal', + lambda m: self.main_handle.launch_terminal()), + ('/TopMenuBar/Tools/View Output', + lambda m: self.main_handle.launch_output_viewer()), + ('/TopMenuBar/Tools/Open Suite GControl', + lambda m: self.main_handle.launch_scheduler()), + ('/TopMenuBar/Page/Revert', + lambda m: self.revert_to_saved_data()), + ('/TopMenuBar/Page/Page Info', + lambda m: self.nav_handle.info_request( + self._get_current_page().namespace + )), + ('/TopMenuBar/Page/Page Help', + lambda m: self._get_current_page().launch_help()), + ('/TopMenuBar/Page/Page Web Help', + lambda m: self._get_current_page().launch_url()), + ('/TopMenuBar/Help/Documentation', self.main_handle.help), + ('/TopMenuBar/Help/About', self.main_handle.about_dialog) + ] + is_toggled = dict( + [('/TopMenuBar/View/View fixed vars', + rose.config_editor.SHOULD_SHOW_FIXED_VARS), + ('/TopMenuBar/View/View ignored vars', + rose.config_editor.SHOULD_SHOW_IGNORED_VARS), + ('/TopMenuBar/View/View user-ignored vars', + rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS), + ('/TopMenuBar/View/View latent vars', + rose.config_editor.SHOULD_SHOW_LATENT_VARS), + ('/TopMenuBar/Metadata/Prefs/View without descriptions', + rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION), + ('/TopMenuBar/Metadata/Prefs/View without help', + rose.config_editor.SHOULD_SHOW_NO_HELP), + ('/TopMenuBar/Metadata/Prefs/View without titles', + rose.config_editor.SHOULD_SHOW_NO_TITLE), + ('/TopMenuBar/View/View ignored pages', + rose.config_editor.SHOULD_SHOW_IGNORED_PAGES), + ('/TopMenuBar/View/View user-ignored pages', + rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES), + ('/TopMenuBar/View/View latent pages', + rose.config_editor.SHOULD_SHOW_LATENT_PAGES), + ('/TopMenuBar/View/Flag opt config vars', + rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS), + ('/TopMenuBar/View/Flag optional vars', + rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS), + ('/TopMenuBar/View/Flag no-metadata vars', + rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS), + ('/TopMenuBar/View/View status bar', + rose.config_editor.SHOULD_SHOW_STATUS_BAR), + ('/TopMenuBar/Metadata/Switch off metadata', + self.metadata_off)] + ) + for (address, action) in menu_list: + widget = self.menubar.uimanager.get_widget(address) + self.menu_widgets.update({address: widget}) + if address in is_toggled: + widget.set_active(is_toggled[address]) + if (address.endswith("View user-ignored pages") and + rose.config_editor.SHOULD_SHOW_IGNORED_PAGES): + widget.set_sensitive(False) + if (address.endswith("View user-ignored vars") and + rose.config_editor.SHOULD_SHOW_IGNORED_VARS): + widget.set_sensitive(False) + if address.endswith("Reload metadata") and self.metadata_off: + widget.set_sensitive(False) + widget.connect('activate', action) + page_menu = self.menubar.uimanager.get_widget("/TopMenuBar/Page") + add_menuitem = self.menubar.uimanager.get_widget( + "/TopMenuBar/Page/Add variable") + page_menu.connect( + "activate", + lambda m: self.main_handle.load_page_menu( + self.menubar, + add_menuitem, + self._get_current_page() + )) + page_menu.get_submenu().connect( + "deactivate", + lambda m: self.main_handle.clear_page_menu( + self.menubar, + add_menuitem + )) + self.main_handle.load_macro_menu(self.menubar) + if not any(c.config_type == rose.TOP_CONFIG_NAME + for c in list(self.data.config.values())): + self.menubar.uimanager.get_widget( + "/TopMenuBar/Tools/Run Suite").set_sensitive(False) + self.update_bar_widgets() + self.top_menu = self.menubar.uimanager.get_widget('/TopMenuBar') + # Load the keyboard accelerators. + accel = { + rose.config_editor.ACCEL_UNDO: + self.perform_undo, + rose.config_editor.ACCEL_REDO: + lambda: self.perform_undo(redo_mode_on=True), + rose.config_editor.ACCEL_FIND: + self.find_entry.grab_focus, + rose.config_editor.ACCEL_FIND_NEXT: + lambda: self.perform_find(self.find_hist['regex']), + rose.config_editor.ACCEL_HELP_GUI: + self.main_handle.help, + rose.config_editor.ACCEL_OPEN: + self.load_from_file, + rose.config_editor.ACCEL_SAVE: + self.save_to_file, + rose.config_editor.ACCEL_QUIT: + self.main_handle.destroy, + rose.config_editor.ACCEL_METADATA_REFRESH: + self._refresh_metadata_if_on, + rose.config_editor.ACCEL_SUITE_RUN: + self.main_handle.run_suite, + rose.config_editor.ACCEL_BROWSER: + self.main_handle.launch_browser, + rose.config_editor.ACCEL_TERMINAL: + self.main_handle.launch_terminal, + } + self.menubar.set_accelerators(accel) + + def generate_nav_panel(self): + """"Create tree panel and link functions.""" + self.nav_panel = rose.config_editor.nav_panel.PageNavigationPanel( + self.nav_controller.namespace_tree, + self.handle_launch_request, + self.nav_handle.get_ns_metadata_and_comments, + self.nav_handle.popup_panel_menu, + self.nav_handle.get_can_show_page, + self.nav_handle.ask_is_preview + ) + + def generate_status_bar(self): + """Create a status bar.""" + self.status_bar = rose.config_editor.status.StatusBar( + verbosity=rose.config_editor.STATUS_BAR_VERBOSITY) + self._set_show_status_bar(rose.config_editor.SHOULD_SHOW_STATUS_BAR) + +# ----------------- Page manipulation functions ------------------------------ + + def handle_load_all(self, *args): + """Handle a request to load all preview configurations.""" + load_these = [] + for item in list(self.data.config.keys()): + if self.data.config[item].is_preview: + load_these.append(item) + load_these.sort() + number_of_events = (len(load_these) * + rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + self.reporter.report_load_event( + "Loading all preview apps", + new_total_events=number_of_events + ) + for namespace_name in load_these: + config_data = self.data.config[namespace_name] + self.data.load_config(config_data.directory, preview=False, + metadata_off=self.metadata_off) + self.reporter.report_load_event( + rose.config_editor.EVENT_LOADED.format(namespace_name[1:]), + no_progress=True + ) + self.reload_namespace_tree() + self.reporter.stop() + self.nav_panel.update_row_tooltips() + if hasattr(self, 'menubar'): + self.main_handle.load_macro_menu(self.menubar) + self.update_bar_widgets() + self.updater.perform_startup_check() + return + + def handle_launch_request(self, namespace_name, as_new=False): + """Handle a request to create a page. + + It normally returns a page containing all variables associated with + the namespace namespace_name, but it won't create a page if it is + already open. It will overwrite the existing current page, if any, + in the internal notebook, unless as_new is True. + + """ + if not namespace_name.startswith('/'): + namespace_name = '/' + namespace_name + + config_name = self.util.split_full_ns(self.data, namespace_name)[0] + config_data = self.data.config[config_name] + + if config_data.is_preview: + self.reporter.report_load_event( + rose.config_editor.EVENT_LOAD_ATTEMPT.format( + namespace_name), + new_total_events=3) + self.data.load_config(config_data.directory, preview=False, + metadata_off=self.metadata_off) + self.reload_namespace_tree() + self.nav_panel.update_row_tooltips() + self.reporter.report_load_event( + rose.config_editor.EVENT_LOADED.format(namespace_name), + no_progress=True) + self.reporter.stop() + if hasattr(self, 'menubar'): + self.main_handle.load_macro_menu(self.menubar) + self.update_bar_widgets() + self.updater.perform_startup_check() + + if namespace_name in self.notebook.get_page_ids(): + index = self.notebook.get_page_ids().index(namespace_name) + self.notebook.set_current_page(index) + return False + for tab_window in self.tab_windows: + if tab_window.get_child().namespace == namespace_name: + tab_window.present() + return False + page = self.make_page(namespace_name) + if page is None: + return False + if as_new: + self.notebook.append_page(page, page.labelwidget) + self.notebook.set_current_page(-1) + else: + index = self.notebook.get_current_page() + self.notebook.insert_page(page, page.labelwidget, index) + self.notebook.set_current_page(index) + if index != -1: + self.notebook.remove_page(index + 1) + self.notebook.set_tab_label_packing(page) + + def make_page(self, namespace_name): + """Look up page data and attributes and call a page constructor.""" + config_name, subspace = self.util.split_full_ns(self.data, + namespace_name) + data, latent_data = self.data.helper.get_data_for_namespace( + namespace_name) + config_data = self.data.config[config_name] + ns_metadata = self.data.namespace_meta_lookup.get(namespace_name, {}) + description = ns_metadata.get(rose.META_PROP_DESCRIPTION, '') + duplicate = ns_metadata.get(rose.META_PROP_DUPLICATE) + help_ = ns_metadata.get(rose.META_PROP_HELP) + url = ns_metadata.get(rose.META_PROP_URL) + custom_widget = ns_metadata.get(rose.config_editor.META_PROP_WIDGET) + custom_sub_widget = ns_metadata.get( + rose.config_editor.META_PROP_WIDGET_SUB_NS) + has_sub_data = self.data.helper.is_ns_sub_data(namespace_name) + label = ns_metadata.get(rose.META_PROP_TITLE) + if label is None: + label = subspace.split('/')[-1] + if duplicate == rose.META_PROP_VALUE_TRUE and not has_sub_data: + # For example, namelist/foo/1 should be shown as foo(1). + label = "(".join(subspace.split('/')[-2:]) + ")" + section_data_objects, latent_section_data_objects = ( + self.data.helper.get_section_data_for_namespace(namespace_name)) + # Related pages + see_also = '' + sections = [s for s in ns_metadata.get('sections', [])] + for section_name in [s for s in sections if s.startswith('namelist')]: + no_num_name = rose.macro.REC_ID_STRIP_DUPL.sub("", section_name) + no_mod_name = rose.macro.REC_ID_STRIP.sub("", section_name) + ok_names = [section_name, no_num_name + "(:)", + no_mod_name + "(:)"] + if no_mod_name != no_num_name: + # There's a modifier in the section name. + ok_names.append(no_num_name) + for section, variables in list(config_data.vars.now.items()): + if not section.startswith(rose.SUB_CONFIG_FILE_DIR): + continue + for variable in variables: + if variable.name != rose.FILE_VAR_SOURCE: + continue + var_values = rose.variable.array_split(variable.value) + for i, val in enumerate(var_values): + if val.startswith("(") and val.endswith(")"): + # It is optional - e.g. "(namelist:baz)". + var_values[i] = val[1:-1] + if set(ok_names) & set(var_values): + var_id = variable.metadata['id'] + see_also += ", " + var_id + see_also = see_also.replace(", ", "", 1) + # Icon + icon_path = self.data.helper.get_icon_path_for_config(config_name) + is_default = self.data.helper.get_ns_is_default(namespace_name) + sub_data = None + sub_ops = None + if has_sub_data: + sub_data = self.data.helper.get_sub_data_for_namespace( + namespace_name) + sub_ops = self.group_ops.get_sub_ops_for_namespace(namespace_name) + macro_info = self.data.helper.get_macro_info_for_namespace( + namespace_name) + page_metadata = { + "namespace": namespace_name, + "ns_is_default": is_default, + "label": label, + "description": description, + "duplicate": duplicate, + "help": help_, + "url": url, + "macro": macro_info, + "widget": custom_widget, + "widget_sub_ns": custom_sub_widget, + "see_also": see_also, + "config_name": config_name, + "show_modes": self.page_var_show_modes, + "icon": icon_path + } + if len(sections) == 1: + page_metadata.update({"id": sections.pop()}) + sect_ops = rose.config_editor.ops.section.SectionOperations( + self.data, self.util, self.reporter, + self.undo_stack, self.redo_stack, + self.check_cannot_enable_setting, + self.updater.update_namespace, + self.updater.update_ns_sub_data, + self.updater.update_ns_info, + update_tree_func=self.reload_namespace_tree, + view_page_func=self.view_page, + kill_page_func=self.kill_page + ) + var_ops = rose.config_editor.ops.variable.VariableOperations( + self.data, self.util, self.reporter, + self.undo_stack, self.redo_stack, + sect_ops.add_section, + self.check_cannot_enable_setting, + self.updater.update_namespace, + search_id_func=self.perform_find_by_id + ) + directory = None + if namespace_name == config_name: + directory = config_data.directory + launch_info = lambda: self.nav_handle.info_request( + namespace_name) + launch_edit = lambda: self.nav_handle.edit_request( + namespace_name) + page = rose.config_editor.page.ConfigPage( + page_metadata, + data, + latent_data, + sect_ops, + var_ops, + section_data_objects, + latent_section_data_objects, + self.data.helper.get_format_sections, + self.reporter, + directory, + sub_data=sub_data, + sub_ops=sub_ops, + launch_info_func=launch_info, + launch_edit_func=launch_edit, + launch_macro_func=self.main_handle.handle_run_custom_macro + ) + # FIXME: These three should go. + page.trigger_tab_detach = lambda b: self._handle_detach_request(page) + var_ops.trigger_ignored_update = lambda v: page.update_ignored() + page.trigger_update_status = lambda: self.updater.update_status(page) + return page + + def get_orphan_page(self, namespace): + """Return a page widget for embedding somewhere else.""" + page = self.make_page(namespace) + orphan_container = self.main_handle.get_orphan_container(page) + self.orphan_pages.append(page) + return orphan_container + + def _handle_detach_request(self, page, old_window=None): + """Open tab (or 'page') in a window and manage close page events.""" + if old_window is None: + tab_window = Gtk.Window() + tab_window.set_icon(self.mainwindow.window.get_icon()) + tab_window.add_accel_group(self.menubar.accelerators) + tab_window.set_default_size(*rose.config_editor.SIZE_PAGE_DETACH) + tab_window.connect('destroy-event', lambda w, e: + self.tab_windows.remove(w) and False) + tab_window.connect('delete-event', lambda w, e: + self.tab_windows.remove(w) and False) + else: + tab_window = old_window + add_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_ADD, + tip_text=rose.config_editor.TIP_ADD_TO_PAGE, + size=Gtk.IconSize.LARGE_TOOLBAR, + as_tool=True + ) + revert_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_REVERT_TO_SAVED, + tip_text=rose.config_editor.TIP_REVERT_PAGE, + size=Gtk.IconSize.LARGE_TOOLBAR, + as_tool=True + ) + add_button.connect('button_press_event', self.add_page_variable) + revert_button.connect('clicked', + lambda b: self.revert_to_saved_data()) + if old_window is None: + parent = self.notebook + else: + parent = old_window + page.reshuffle_for_detached(add_button, revert_button, parent) + tab_window.set_title(' - '.join([page.label, self.data.top_level_name, + rose.config_editor.PROGRAM_NAME])) + tab_window.add(page) + tab_window.connect_after('focus-in-event', self.handle_page_change) + if old_window is None: + self.tab_windows.append(tab_window) + tab_window.show() + tab_window.present() + self.set_current_page_indicator(page.namespace) + return False + + def handle_page_change(self, *args): + """Handle a page change and select the correct tree row.""" + current_page = self._get_current_page() + self.update_page_bar_sensitivity(current_page) + if current_page is None: + self.nav_panel.select_row(None) + return False + self.set_current_page_indicator(current_page.namespace) + return False + + def update_page_bar_sensitivity(self, current_page): + """Update the top 'Page' menu and the toolbar.""" + if not hasattr(self, 'toolbar') or not hasattr(self, 'menubar'): + return False + page_icons = ['Add to page...', 'Revert page to saved'] + get_widget = self.menubar.uimanager.get_widget + page_menu = get_widget('/TopMenuBar/Page') + page_menuitems = page_menu.get_submenu().get_children() + if current_page is None or not self.notebook.get_n_pages(): + for name in page_icons: + self.toolbar.set_widget_sensitive(name, False) + for menuitem in page_menuitems: + menuitem.set_sensitive(False) + else: + for name in page_icons: + self.toolbar.set_widget_sensitive(name, True) + for menuitem in page_menuitems: + menuitem.set_sensitive(True) + ns = current_page.namespace + metadata = self.data.namespace_meta_lookup.get(ns, {}) + get_widget("/TopMenuBar/Page/Page Help").set_sensitive( + rose.META_PROP_HELP in metadata) + get_widget("/TopMenuBar/Page/Page Web Help").set_sensitive( + rose.META_PROP_URL in metadata) + + def set_current_page_indicator(self, namespace): + """Make sure the current page is highlighted in the nav panel.""" + if hasattr(self, 'nav_panel'): + self.nav_panel.select_row(namespace.lstrip('/').split('/')) + + def add_page_variable(self, widget, event): + """Launch an add menu based on page content.""" + page = self._get_current_page() + if page is None: + return False + page.launch_add_menu(event.button, event.time) + + def revert_to_saved_data(self): + """Reload the page data from saved configuration information.""" + page = self._get_current_page() + if page is None: + return + namespace = page.namespace + config_name = self.util.split_full_ns(self.data, namespace)[0] + self.data.load_node_namespaces(config_name, from_saved=True) + config_data, ghost_data = self.data.helper.get_data_for_namespace( + namespace, from_saved=True) + page.reload_from_data(config_data, ghost_data) + self.data.load_node_namespaces(config_name) + self.updater.update_status(page) + self.reporter.report(rose.config_editor.EVENT_REVERT.format( + namespace.lstrip("/"))) + + def _get_pagelist(self): + """Load an attribute self.pagelist with a list of open pages.""" + self.pagelist = [] + if hasattr(self, 'notebook'): + for index in range(self.notebook.get_n_pages()): + if hasattr(self.notebook.get_nth_page(index), 'panel_data'): + self.pagelist.append(self.notebook.get_nth_page(index)) + if hasattr(self, 'tab_windows'): + for window in self.tab_windows: + if hasattr(window.get_child(), 'panel_data'): + self.pagelist.append(window.get_child()) + self.pagelist.extend(self.orphan_pages) + return self.pagelist + + def _get_current_page(self): + """Return the currently focused page.""" + self._get_pagelist() + if not self.pagelist: + return None + for window in self.tab_windows: + if window.has_toplevel_focus(): + return window.get_child() + for page in self.orphan_pages: + if page.get_toplevel().is_active(): + return page + if hasattr(self, "notebook"): + index = self.notebook.get_current_page() + return self.notebook.get_nth_page(index) + return None + + def _get_current_page_and_id(self): + """Return the currently focused page and the variable id (if any).""" + page = self._get_current_page() + if page is None: + return None, None + return page, page.get_main_focus() + + def _set_show_status_bar(self, should_show_status_bar): + """Set whether the status bar is shown or hidden.""" + if hasattr(self, "status_bar") and self.status_bar is not None: + if should_show_status_bar: + self.status_bar.show() + else: + self.status_bar.hide() + + def _set_page_show_modes(self, key, is_key_allowed): + """Set generic variable/namespace view options.""" + self._set_page_var_show_modes(key, is_key_allowed) + self._set_page_ns_show_modes(key, is_key_allowed) + + def _set_page_ns_show_modes(self, key, is_key_allowed): + """Set namespace view options.""" + self.page_ns_show_modes[key] = is_key_allowed + if (hasattr(self, "menubar") and + key == rose.config_editor.SHOW_MODE_IGNORED): + user_ign_item = self.menubar.uimanager.get_widget( + "/TopMenuBar/View/View user-ignored pages") + user_ign_item.set_sensitive(not is_key_allowed) + + def _set_page_var_show_modes(self, key, is_key_allowed): + """Set variable widgets' view options.""" + self.page_var_show_modes[key] = is_key_allowed + self._get_pagelist() + for page in self.pagelist: + page.react_to_show_modes(key, is_key_allowed) + if (hasattr(self, "menubar") and + key == rose.config_editor.SHOW_MODE_IGNORED): + user_ign_item = self.menubar.uimanager.get_widget( + "/TopMenuBar/View/View user-ignored vars") + user_ign_item.set_sensitive(not is_key_allowed) + + def kill_page(self, namespace): + """Destroy a page if it has the same namespace as the argument.""" + self._get_pagelist() + for page in self.pagelist: + if page.namespace == namespace: + if page.namespace in self.notebook.get_page_ids(): + self.notebook.delete_by_id(page.namespace) + else: + tab_pages = [w.get_child() for w in self.tab_windows] + if page in tab_pages: + page_window = self.tab_windows[tab_pages.index(page)] + page_window.destroy() + self.tab_windows.remove(page_window) + else: + self.orphan_pages.remove(page) + +# ----------------- Update functions ----------------------------------------- + + def reload_namespace_tree(self, *args, **kwargs): + """Redraw the navigation namespace tree.""" + self.nav_controller.reload_namespace_tree(*args, **kwargs) + + def tree_trigger_update(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.tree_trigger_update(*args, **kwargs) + + def update_config(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.update_config(*args, **kwargs) + + def update_namespace(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.update_namespace(*args, **kwargs) + + def update_namespace_sub_data(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.update_ns_sub_data(*args, **kwargs) + + def update_ns_info(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.update_ns_info(*args, **kwargs) + + def update_ns_sub_data(self, *args, **kwargs): + """Placeholder for updater function of the same name.""" + self.updater.update_ns_sub_data(*args, **kwargs) + +# ----------------- Page viewer function ------------------------------------- + + def view_page(self, page_id, var_id=None): + """Set focus by namespace (page_id), and optionally by var key.""" + page = None + if page_id is None: + return None + current_page = self._get_current_page() + if current_page is not None and current_page.namespace == page_id: + current_page.set_main_focus(var_id) + self.handle_page_change() # Just to make sure. + return current_page + self._get_pagelist() + if (page_id not in [p.namespace for p in self.pagelist]): + self.handle_launch_request(page_id, as_new=True) + index = self.notebook.get_current_page() + page = self.notebook.get_nth_page(index) + if page_id in self.notebook.get_page_ids(): + index = self.notebook.get_page_ids().index(page_id) + page = self.notebook.get_nth_page(index) + self.notebook.set_current_page(index) + if not self.mainwindow.window.is_active(): + self.mainwindow.window.present() + page.set_main_focus(var_id) + else: + for tab_window in self.tab_windows: + if tab_window.get_child().namespace == page_id: + page = tab_window.get_child() + if not tab_window.is_active(): + tab_window.present() + page.set_main_focus(var_id) + self.set_current_page_indicator(page_id) + return page + +# ----------------- Primary menu functions ----------------------------------- + + def load_from_file(self, somewidget=None): + """Open a standard dialogue and load a config file, if selected.""" + dirname = self.mainwindow.launch_open_dirname_dialog() + if dirname is None or not os.path.isdir(dirname): + return False + if self.data.top_level_directory is None and not self.is_pluggable: + self.data.load_top_config(dirname) + self.data.saved_config_names = set(self.data.config.keys()) + self.mainwindow.window.set_title(self.data.top_level_name + + ' - rose-config-editor') + self.updater.update_all() + self.updater.perform_startup_check() + else: + spawn_subprocess_window(dirname) + + def save_to_file(self, only_config_name=None, check_on_save=False): + """Dump the component configurations in memory to disk.""" + if only_config_name is None: + config_names = [] + for config_name in list(self.data.config.keys()): + if not self.data.config[config_name].is_preview: + config_names.append(config_name) + else: + config_names = [only_config_name] + save_ok = True + if check_on_save: + self.main_handle.check_all_extra() + + for config_name in sorted(config_names): + short_config_name = config_name.lstrip("/") + config = self.data.dump_to_internal_config(config_name) + new_save_config = self.data.dump_to_internal_config(config_name) + config_data = self.data.config[config_name] + vars_ok = True + for var in config_data.vars.get_all(skip_latent=True): + if not var.name: + self.view_page(var.metadata["full_ns"], + var.metadata["id"]) + page_address = var.metadata["full_ns"].lstrip("/") + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_SAVE_BLANK.format( + short_config_name, + page_address + ), + title=rose.config_editor.ERROR_SAVE_TITLE.format( + short_config_name), + modal=False + ) + vars_ok = False + break + if not vars_ok: + save_ok = False + continue + directory = config_data.directory + config_vars = config_data.vars + config_sections = config_data.sections + + # Run check fail-if, warn-if and validator macros if check_on_save + if check_on_save: + errors = self.nav_panel.get_change_error_totals( + config_name=short_config_name)[1] + if errors > 0: + dialog = Gtk.MessageDialog( + None, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.INFO, + Gtk.ButtonsType.YES_NO, + None + ) + dialog.set_markup( + rose.config_editor.WARNING_ERRORS_FOUND_ON_SAVE.format( + short_config_name + )) + res = dialog.run() + dialog.destroy() + if res == Gtk.ResponseType.NO: + continue + + # Dump the configuration. + filename = config_data.config_type + if (directory is None and + config_data.config_type == rose.INFO_CONFIG_NAME): + directory = self.data.top_level_directory + save_path = os.path.join(directory, filename) + rose.macro.pretty_format_config(config, ignore_error=True) + try: + rose.config.dump(config, save_path) + except (OSError, IOError) as exc: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_SAVE_PATH_FAIL.format(exc), + title=rose.config_editor.ERROR_SAVE_TITLE.format( + short_config_name), + modal=False + ) + save_ok = False + continue + # Un-prettify. + config = self.data.dump_to_internal_config(config_name) + # Update the last save data. + config_data.save_config = new_save_config + config_vars.save.clear() + config_vars.latent_save.clear() + for section, variables in list(config_vars.now.items()): + config_vars.save.update({section: []}) + for variable in variables: + config_vars.save[section].append(variable.copy()) + for section, variables in list(config_vars.latent.items()): + config_vars.latent_save.update({section: []}) + for variable in variables: + config_vars.latent_save[section].append(variable.copy()) + config_sections.save.clear() + config_sections.latent_save.clear() + for section, data in list(config_sections.now.items()): + config_sections.save.update({section: data.copy()}) + for section, data in list(config_sections.latent.items()): + config_sections.latent_save.update({section: data.copy()}) + self.data.saved_config_names = set(self.data.config.keys()) + # Update open pages. + self._get_pagelist() + for page in self.pagelist: + page.refresh_widget_status() + # Update everything else. + self.updater.update_all() + return save_ok + + def output_config_objects(self, only_config_name=None): + """Return a dict of config name - object pairs from this session.""" + if only_config_name is None: + config_names = list(self.data.config.keys()) + else: + config_names = [only_config_name] + return_dict = {} + for config_name in config_names: + config = self.data.dump_to_internal_config(config_name) + return_dict.update({config_name: config}) + return return_dict + +# ----------------- Secondary Menu/Dialog handling functions ----------------- + + def apply_macro_transform(self, *args, **kwargs): + """Placeholder for updater module function.""" + self.updater.apply_macro_transform(*args, **kwargs) + + def apply_macro_validation(self, *args, **kwargs): + """Placeholder for updater module function.""" + self.updater.apply_macro_validation(*args, **kwargs) + + def _add_config(self, config_name, meta=None): + """Add a configuration, optionally with META=TYPE=meta.""" + config_short_name = config_name.split("/")[-1] + root = os.path.join(self.data.top_level_directory, + rose.SUB_CONFIGS_DIR) + new_path = os.path.join(root, config_short_name, rose.SUB_CONFIG_NAME) + new_config = rose.config.ConfigNode() + if meta is not None: + new_config.set( + [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_META_TYPE], + meta + ) + try: + os.mkdir(os.path.dirname(new_path)) + rose.config.dump(new_config, new_path) + except (OSError, IOError) as exc: + text = rose.config_editor.ERROR_CONFIG_CREATE.format( + new_path, type(exc), str(exc)) + title = rose.config_editor.ERROR_CONFIG_CREATE_TITLE + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + return False + self.data.load_config(os.path.dirname(new_path), reload_tree_on=True, + skip_load_event=True) + stack_item = rose.config_editor.stack.StackItem( + config_name, + rose.config_editor.STACK_ACTION_ADDED, + rose.variable.Variable('', '', {}), + self._remove_config, + (config_name, meta) + ) + self.undo_stack.append(stack_item) + while self.redo_stack: + self.redo_stack.pop() + self.view_page(config_name) + self.updater.update_namespace(config_name) + + def _remove_config(self, config_name, meta=None): + """Remove a configuration, optionally caching a meta id.""" + config_data = self.data.config[config_name] + dirpath = config_data.directory + nses = self.data.helper.get_all_namespaces(config_name) + nses.remove(config_name) + self._get_pagelist() + for page in self.pagelist: + name = self.util.split_full_ns(self.data, page.namespace)[0] + if name == config_name: + if name in self.notebook.get_page_ids(): + self.notebook.delete_by_id(name) + else: + tab_nses = [w.get_child().namespace + for w in self.tab_windows] + page_window = self.tab_windows[tab_nses.index(name)] + page_window.destroy() + self.group_ops.remove_sections(config_name, + list(config_data.sections.now.keys())) + if dirpath is not None: + try: + shutil.rmtree(dirpath) + except (shutil.Error, OSError, IOError) as exc: + text = rose.config_editor.ERROR_CONFIG_DELETE.format( + dirpath, type(exc), str(exc)) + title = rose.config_editor.ERROR_CONFIG_CREATE_TITLE + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + return False + self.data.config.pop(config_name) + self.reload_namespace_tree() + stack_item = rose.config_editor.stack.StackItem( + config_name, + rose.config_editor.STACK_ACTION_REMOVED, + rose.variable.Variable('', '', {}), + self._add_config, + (config_name, meta) + ) + self.undo_stack.append(stack_item) + while self.redo_stack: + self.redo_stack.pop() + + def _get_menu_widget(self, suffix): + """Return the menu widget whose ui address ends with suffix.""" + for address in self.menu_widgets: + if address.endswith(suffix): + return self.menu_widgets[address] + return None + + def _has_preview_apps(self): + """Return whether any configurations are currently just previews.""" + for item in list(self.data.config.keys()): + if self.data.config[item].is_preview: + return True + else: + return False + + def update_bar_widgets(self): + """Update bar functionality like Undo and Redo.""" + if not hasattr(self, 'toolbar'): + return False + self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_UNDO, + len(self.undo_stack) > 0) + self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_REDO, + len(self.redo_stack) > 0) + self._get_menu_widget('/Undo').set_sensitive(len(self.undo_stack) > 0) + self._get_menu_widget('/Redo').set_sensitive(len(self.redo_stack) > 0) + self._get_menu_widget('/Find Next').set_sensitive( + len(self.find_hist['ids']) > 0) + self._get_menu_widget('/Load All Apps').set_sensitive( + self._has_preview_apps()) + self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_LOAD_APPS, + self._has_preview_apps()) + if not hasattr(self, "nav_panel"): + return False + changes, errors = self.nav_panel.get_change_error_totals() + self.status_bar.set_num_errors(errors) + self._get_menu_widget('/Autofix').set_sensitive(bool(errors)) + self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_TRANSFORM, + bool(errors)) + self._update_changed_sensitivity(is_changed=bool(changes)) + + def update_status_text(self, *args, **kwargs): + """Update the message displayed in the status bar.""" + if hasattr(self, "status_bar"): + self.status_bar.set_message(*args, **kwargs) + + def _update_changed_sensitivity(self, is_changed=False): + """Alter sensitivity of 'unsaved changes' related widgets.""" + self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_SAVE, + is_changed) + self.toolbar.set_widget_sensitive( + rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + is_changed + ) + self._get_menu_widget('/Save').set_sensitive(is_changed) + self._get_menu_widget('/Check and save').set_sensitive(is_changed) + self._get_menu_widget('/Graph').set_sensitive(not is_changed) + self._toolbar_run_button.set_sensitive(not is_changed) + self._get_menu_widget('/Run Suite custom').set_sensitive( + not is_changed) + self._get_menu_widget('/Run Suite default').set_sensitive( + not is_changed) + + def _refresh_metadata_if_on(self, config_name=None): + """Reload any metadata, if present - otherwise do nothing.""" + if not self.metadata_off: + self.refresh_metadata(only_this_config=config_name) + + def refresh_metadata(self, metadata_off=False, only_this_config=None): + """Switch metadata on/off and reloads namespaces.""" + self.metadata_off = metadata_off + if hasattr(self, 'menubar'): + self._get_menu_widget('/Reload metadata').set_sensitive( + not self.metadata_off) + if only_this_config is None: + configs = list(self.data.config.keys()) + else: + configs = [only_this_config] + for config_name in configs: + if self.data.config[config_name].is_preview: + continue + self.data.clear_meta_lookups(config_name) + config = self.data.dump_to_internal_config(config_name) + config_data = self.data.config[config_name] + config_data.config = config + directory = config_data.directory + del config_data.macros + meta_config = config_data.meta + if metadata_off: + meta_config_tree = self.data.load_meta_config_tree( + config_type=config_data.config_type, + opt_meta_paths=self.data.opt_meta_paths) + meta_config = meta_config_tree.node + meta_files = self.data.load_meta_files(meta_config_tree) + macros = [] + else: + meta_config_tree = self.data.load_meta_config_tree( + config, directory, config_type=config_data.config_type, + opt_meta_paths=self.data.opt_meta_paths) + meta_config = meta_config_tree.node + meta_files = self.data.load_meta_files(meta_config_tree) + macro_module_prefix = ( + self.data.helper.get_macro_module_prefix(config_name)) + macros = rose.macro.load_meta_macro_modules( + meta_files, module_prefix=macro_module_prefix) + config_data.meta = meta_config + self.data.load_builtin_macros(config_name) + self.data.load_file_metadata(config_name) + self.data.filter_meta_config(config_name) + # Load section and variable data into the object. + sects, l_sects = self.data.load_sections_from_config(config_name) + s_sects, s_l_sects = self.data.load_sections_from_config( + config_name, save=True) + config_data.sections = rose.config_editor.data.SectData( + sects, l_sects, s_sects, s_l_sects) + var, l_var = self.data.load_vars_from_config(config_name) + s_var, s_l_var = self.data.load_vars_from_config( + config_name, save=True) + config_data.vars = rose.config_editor.data.VarData( + var, l_var, s_var, s_l_var) + config_data.meta_files = meta_files + config_data.macros = macros + self.data.load_node_namespaces(config_name) + self.data.load_node_namespaces(config_name, from_saved=True) + self.data.load_ignored_data(config_name) + self.data.load_metadata_for_namespaces(config_name) + self.reload_namespace_tree() + if self.is_pluggable: + self.updater.update_all() + if hasattr(self, 'menubar'): + self.main_handle.load_macro_menu(self.menubar) + namespaces_updated = [] + for config_name in configs: + config_data = self.data.config[config_name] + for variable in config_data.vars.get_all(skip_latent=True): + ns = variable.metadata.get('full_ns') + if ns not in namespaces_updated: + self.updater.update_tree_status(ns, icon_type='changed') + namespaces_updated.append(ns) + self._get_pagelist() + current_page, current_id = self._get_current_page_and_id() + current_namespace = None + if current_page is not None: + current_namespace = current_page.namespace + + # Generate replacements for existing pages. + for page in self.pagelist: + namespace = page.namespace + config_name = self.util.split_full_ns(self.data, namespace)[0] + if config_name not in configs: + continue + data, missing_data = self.data.helper.get_data_for_namespace( + namespace) + if len(data + missing_data) > 0: + new_page = self.make_page(namespace) + if new_page is None: + continue + if page in [w.get_child() for w in self.tab_windows]: + # Insert a new page into the old window. + tab_pages = [w.get_child() for w in self.tab_windows] + old_window = self.tab_windows[tab_pages.index(page)] + old_window.remove(page) + self._handle_detach_request(new_page, old_window) + elif hasattr(self, 'notebook'): + # Replace a notebook page. + index = self.notebook.get_page_ids().index(namespace) + self.notebook.remove_page(index) + self.notebook.insert_page(new_page, new_page.labelwidget, + index) + else: + # Replace an orphan page + parent = page.get_parent() + if parent is not None: + parent.remove(page) + parent.pack_start(new_page, True, True, 0) + self.orphan_pages.remove(page) + self.orphan_pages.append(new_page) + else: + self.kill_page(page.namespace) + + # Preserve the old current page view, if possible. + if current_namespace is not None: + config_name = self.util.split_full_ns(self.data, + current_namespace)[0] + self._get_pagelist() + page_namespaces = [page.namespace for page in self.pagelist] + if config_name in configs: + if current_namespace in page_namespaces: + self.view_page(current_namespace, current_id) + + def load_custom_metadata(self): + # open metadata dialog, use list() to pass by value + paths = self.mainwindow.launch_metadata_manager( + list(self.data.opt_meta_paths)) + if paths is not None: + # if form submitted + self.data.opt_meta_paths = paths + self.refresh_metadata() + +# ----------------- Data-intensive menu functions / utilities ---------------- + + def _launch_find(self, *args): + """Get the find expression from a dialog.""" + if not self.find_entry.is_focus(): + self.find_entry.grab_focus() + expression = self.find_entry.get_text() + start_page = self._get_current_page() + if expression is not None and expression != '': + page, var_id = self.perform_find(expression, start_page) + if page is None: + text = rose.config_editor.WARNING_NOT_FOUND + try: # Needs PyGTK >= 2.16 + self.find_entry.set_icon_from_stock( + 0, Gtk.STOCK_DIALOG_WARNING) + self.find_entry.set_icon_tooltip_text(0, text) + except AttributeError: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_INFO, + text, + rose.config_editor.WARNING_NOT_FOUND_TITLE + ) + else: + if var_id is not None: + self.reporter.report( + rose.config_editor.EVENT_FOUND_ID.format(var_id)) + self._clear_find() + + def _clear_find(self, *args): + """Clear any warning icons from the find entry.""" + try: # Needs PyGTK >= 2.16 + self.find_entry.set_icon_from_stock(0, None) + except AttributeError: + pass + + def perform_find(self, expression, start_page=None): + """Drive the finding of the regex 'expression' within the data.""" + if expression == '': + return None, None + page_id, var_id = self.get_found_page_and_id(expression, start_page) + return self.view_page(page_id, var_id), var_id + + def perform_find_by_ns_id(self, namespace, setting_id): + """Drive find by id.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + self.perform_find_by_id(config_name, setting_id) + + def perform_find_by_id(self, config_name, setting_id): + """Drive the finding of a setting id within the data.""" + section, option = self.util.get_section_option_from_id(setting_id) + if option is None: + page_id = self.data.helper.get_default_section_namespace( + section, config_name) + self.view_page(page_id) + else: + var = self.data.helper.get_variable_by_id(setting_id, config_name) + if var is None: + var = self.data.helper.get_variable_by_id(setting_id, + config_name, + latent=True) + if var is not None: + page_id = var.metadata["full_ns"] + self.view_page(page_id, setting_id) + + def get_found_page_and_id(self, expression, start_page): + """Using regex expression, return a matching page and variable.""" + try: + reg_find = re.compile(expression).search + except sre_constants.error as exc: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_NOT_REGEX.format( + expression, str(exc)), + rose.config_editor.ERROR_BAD_FIND) + return None, None + if self.find_hist['regex'] != expression: + self.find_hist['ids'] = [] + self.find_hist['regex'] = expression + if start_page is None: + ns_cmp = lambda x, y: 0 + name_cmp = lambda x, y: 0 + else: + current_ns = start_page.namespace + current_name = self.util.split_full_ns(self.data, current_ns)[0] + ns_cmp = lambda x, y: (y == current_ns) - (x == current_ns) + name_cmp = lambda x, y: (y == current_name) - (x == current_name) + id_cmp = lambda v, w: cmp(v.metadata['id'], w.metadata['id']) + config_keys = sorted(list(self.data.config.keys())) + config_keys.sort(name_cmp) + for config_name in config_keys: + config_data = self.data.config[config_name] + search_vars = config_data.vars.get_all( + skip_latent=not self.page_var_show_modes["latent"]) + found_ns_vars = {} + for variable in search_vars: + var_id = variable.metadata.get('id') + ns = variable.metadata.get('full_ns') + if (rose.META_PROP_TITLE in variable.metadata and + reg_find(variable.metadata[rose.META_PROP_TITLE])): + found_ns_vars.setdefault(ns, []) + found_ns_vars[ns].append(variable) + continue + if reg_find(variable.name) or reg_find(variable.value): + found_ns_vars.setdefault(ns, []) + found_ns_vars[ns].append(variable) + ns_list = sorted(list(found_ns_vars.keys())) + ns_list.sort(ns_cmp) + for ns in ns_list: + variables = found_ns_vars[ns] + variables.sort(id_cmp) + for variable in variables: + var_id = variable.metadata['id'] + if (config_name, var_id) not in self.find_hist['ids']: + if (not self.page_var_show_modes['fixed'] and + len(variable.metadata.get('values', [])) == 1): + continue + if (not self.page_var_show_modes['ignored'] and + variable.ignored_reason): + continue + self.find_hist['ids'].append((config_name, var_id)) + return ns, var_id + if self.find_hist['ids']: + config_name, var_id = self.find_hist['ids'][0] + config_data = self.data.config[config_name] + var = self.data.helper.get_variable_by_id(var_id, config_name) + if var is None: + var = self.data.helper.get_variable_by_id(var_id, config_name, + latent=True) + if var is not None: + self.find_hist['ids'] = [self.find_hist['ids'][0]] + return var.metadata['full_ns'], var_id + return None, None + + def check_cannot_enable_setting(self, config_name, setting_id): + """Check if the setting is involved in the trigger mechanism.""" + return setting_id in self.data.trigger[config_name].get_all_ids() + + def perform_undo(self, redo_mode_on=False): + """Change focus to the correct page and call an undo or redo. + + It grabs the relevant page and widget focus and calls the + correct 'undo_func' StackItem attribute function. + It then regenerates the affected container and sets the focus to + the variable that was last affected by the undo. + + """ + if redo_mode_on: + stack = self.redo_stack + else: + stack = self.undo_stack + if not stack: + return False + if self.performing_undo: + # Prevent multiple calls from existing concurrently. + return False + else: + self.performing_undo = True + self._get_pagelist() + do_list = [stack[-1]] + # We should undo/redo all same-grouped items together. + for stack_item in reversed(stack[:-1]): + if (stack_item.group is None or + stack_item.group != do_list[0].group): + break + do_list.append(stack_item) + group = do_list[0].group + is_group = len(do_list) > 1 + stack_info = [] + namespace_id_map = {} + event_text = rose.config_editor.EVENT_UNDO + if redo_mode_on: + event_text = rose.config_editor.EVENT_REDO + for stack_item in do_list: + action = stack_item.action + node = stack_item.node + node_id = None + try: + node_id = node.metadata['id'] + except (AttributeError, KeyError): + pass + # We need to handle namespace and metadata changes + if node_id is None: + # Not a variable or section + namespace = stack_item.page_label + node_is_section = False + else: + # A variable or section + opt = self.util.get_section_option_from_id(node_id)[1] + node_is_section = (opt is None) + namespace = node.metadata.get('full_ns') + if namespace is None: + namespace = stack_item.page_label + config_name = self.util.split_full_ns( + self.data, namespace)[0] + node.process_metadata( + self.data.helper.get_metadata_for_config_id(node_id, + config_name)) + self.data.load_ns_for_node(node, config_name) + namespace = node.metadata.get('full_ns') + if (not is_group and + self.nav_controller.is_ns_in_tree(namespace) and + not node_is_section): + page = self.view_page(namespace, node_id) + redo_items = [x for x in self.redo_stack] + if stack_item.undo_args: + args = list(stack_item.undo_args) + for i, arg_item in enumerate(args): + if arg_item == stack_item.page_label: + # Then it is a namespace argument & should be changed. + args[i] = namespace + stack_item.undo_func(*args) + else: + stack_item.undo_func() + del self.redo_stack[:] + self.redo_stack.extend(redo_items) + just_done_item = self.undo_stack[-1] + just_done_item.group = group + del self.undo_stack[-1] + del stack[-1] + if redo_mode_on: + self.undo_stack.append(just_done_item) + else: + self.redo_stack.append(just_done_item) + if not self.nav_controller.is_ns_in_tree(namespace): + self.reload_namespace_tree() + page = None + if is_group: + # Store namespaces and ids for later updating. + stack_info.extend([namespace, stack_item.page_label]) + namespace_id_map.setdefault(namespace, []) + namespace_id_map[namespace].append(node_id) + if namespace != stack_item.page_label: + namespace_id_map.setdefault(stack_item.page_label, []) + namespace_id_map[stack_item.page_label].append(node_id) + elif self.nav_controller.is_ns_in_tree(namespace): + if not node_is_section: + # Section operations should not require pages. + page = self.view_page(namespace, node_id) + self.updater.sync_page_var_lists(page) + page.sort_data() + page.refresh(node_id) + page.update_ignored() + page.update_info() + page.set_main_focus(node_id) + self.set_current_page_indicator(page.namespace) + if namespace != stack_item.page_label: + # Make sure the right status update is made. + self.updater.update_status(page) + self.update_bar_widgets() + self.updater.update_stack_viewer_if_open() + if not is_group: + if namespace is not None: + self.updater.focus_sub_page_if_open(namespace, node_id) + if node_id is None: + title = stack_item.name + else: + title = node_id + id_text = rose.config_editor.EVENT_UNDO_ACTION_ID.format( + action, title) + self.reporter.report(event_text.format(id_text)) + if is_group: + group_name = do_list[0].group.split("-")[0] + self.reporter.report(event_text.format(group_name)) + namespace = None + for namespace in set(stack_info): + self.reload_namespace_tree(namespace) + # Use the last node_id for a sub page focus (if any). + if namespace: + focus_id = namespace_id_map[namespace][-1] + self.updater.focus_sub_page_if_open(namespace, focus_id) + self.performing_undo = False + return True + +# ----------------------- System functions ----------------------------------- + + +def spawn_window(config_directory_path=None, debug_mode=False, + load_all_apps=False, load_no_apps=False, metadata_off=False, + initial_namespaces=None, opt_meta_paths=None, + no_warn=None): + """Create a window and load the configuration into it. Run Gtk.""" + if opt_meta_paths is None: + opt_meta_paths = [] + if not debug_mode: + warnings.filterwarnings('ignore') + resourcer = rose.resource.ResourceLocator.default() + rose.gtk.util.rc_setup( + resourcer.locate('rose-config-edit/.gtkrc-2.0')) + rose.gtk.util.setup_stock_icons() + logo = resourcer.locate('images/rose-splash-logo.png') + if rose.config_editor.ICON_PATH_SCHEDULER is None: + gcontrol_icon = None + else: + try: + gcontrol_icon = resourcer.locate( + rose.config_editor.ICON_PATH_SCHEDULER) + except rose.resource.ResourceError: + gcontrol_icon = None + rose.gtk.util.setup_scheduler_icon(gcontrol_icon) + number_of_events = (get_number_of_configs(config_directory_path) * + rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + if config_directory_path is None: + title = rose.config_editor.UNTITLED_NAME + else: + title = config_directory_path.split("/")[-1] + splash_screen = rose.gtk.splash.SplashScreenProcess(logo, title, + number_of_events) + try: + ctrl = MainController(config_directory_path, + load_updater=splash_screen, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps, + metadata_off=metadata_off, + opt_meta_paths=opt_meta_paths, + no_warn=no_warn) + except BaseException: + splash_screen.stop() + raise + + # open up any initial_namespaces the user has provided us with + if initial_namespaces: + # if the namespace ends with a / remove it + for i in range(len(initial_namespaces)): + if (len(initial_namespaces[i]) > 1 and + initial_namespaces[i][-1] == '/'): + initial_namespaces[i] = initial_namespaces[i][0:-1] + + # for each partial namespace get the full namespace + full_namespaces = [] + for namespace in initial_namespaces: + exp = re.compile(r'(.*%s?[^\/]+)' % (re.escape(namespace),)) + for ns in sorted(sorted(ctrl.data.namespace_meta_lookup), + key=len): + match = exp.search(ns) + if match: + full_namespaces.append(match.groups()[0]) + break + + # open each namespace in a new tab + for namespace in full_namespaces: + # if the namespace begins with a / remove it + if namespace[0] == '/': + namespace = namespace[1:] + # open namespace + try: + ctrl.view_page(namespace) + except Exception: + print('could not open ' + namespace, file=sys.stderr) + # expand namespace in nav_panel + path = ctrl.nav_panel.get_path_from_names(namespace.split('/')) + if path: + ctrl.nav_panel.tree.expand_to_path(path) + + Gtk.Settings.get_default().set_long_property("gtk-button-images", + True, "main") + Gtk.Settings.get_default().set_long_property("gtk-menu-images", + True, "main") + splash_screen.stop() + Gtk.main() + + +def spawn_subprocess_window(config_directory_path=None): + """Launch a subprocess for a new config editor. Is it safe?""" + if config_directory_path is None: + os.system(rose.config_editor.LAUNCH_COMMAND + ' --new &') + return + elif not os.path.isdir(str(config_directory_path)): + return + os.system(rose.config_editor.LAUNCH_COMMAND_CONFIG + + config_directory_path + " &") + + +def get_number_of_configs(config_directory_path=None): + """Return the number of configurations that will be loaded.""" + number_to_load = 0 + if config_directory_path is not None: + for listing in set(os.listdir(config_directory_path)): + if listing in rose.CONFIG_NAMES: + number_to_load += 1 + app_dir = os.path.join(config_directory_path, rose.SUB_CONFIGS_DIR) + if os.path.exists(app_dir): + for entry in os.listdir(app_dir): + if (os.path.isdir(os.path.join(app_dir, entry)) and + not entry.startswith('.')): + number_to_load += 1 + return number_to_load + + +def main(): + """Launch from the command line.""" + if (Gtk.pygtk_version[0] < rose.config_editor.MIN_PYGTK_VERSION[0] or + Gtk.pygtk_version[1] < rose.config_editor.MIN_PYGTK_VERSION[1]): + this_version = '{0}.{1}.{2}'.format(*Gtk.pygtk_version) + required_version = '{0}.{1}.{2}'.format( + *rose.config_editor.MIN_PYGTK_VERSION) + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( + required_version, this_version), + rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE + ) + sys.exit(1) + sys.path.append(os.getenv('ROSE_HOME')) + opt_parser = rose.opt_parse.RoseOptionParser() + opt_parser.add_my_options("conf_dir", "meta_path", "new_mode", + "load_no_apps", "load_all_apps", "no_metadata", + "no_warn") + opts, args = opt_parser.parse_args() + rose.macro.add_meta_paths() + opt_meta_paths = [] + if opts.meta_path: + for meta_path in opts.meta_path: + for path in meta_path.split(os.pathsep): + opt_meta_paths.append( + os.path.abspath( + os.path.expandvars( + os.path.expanduser(path)))) + if opts.conf_dir: + os.chdir(opts.conf_dir) + path = os.getcwd() + name_set = set([rose.SUB_CONFIG_NAME, rose.TOP_CONFIG_NAME]) + while True: + if set(os.listdir(path)) & name_set: + break + path = os.path.dirname(path) + if path == os.path.dirname(path): + # We don't support suites located at the root! + break + if path != os.getcwd() and path != os.path.dirname(path): + os.chdir(path) + cwd = os.getcwd() + if opts.new_mode: + cwd = None + rose.gtk.dialog.set_exception_hook_dialog(keep_alive=True) + if opts.profile_mode: + handle = tempfile.NamedTemporaryFile() + cProfile.runctx("""spawn_window(cwd, debug_mode=opts.debug_mode, + load_all_apps=opts.load_all_apps, + load_no_apps=opts.load_no_apps, + metadata_off=opts.no_metadata, + initial_namespaces=args, + opt_meta_paths=opt_meta_paths, + no_warn=opts.no_warn) + """, globals(), locals(), handle.name) + pstat = pstats.Stats(handle.name) + pstat.strip_dirs().sort_stats("cumulative").print_stats() + handle.close() + else: + spawn_window(cwd, debug_mode=opts.debug_mode, + load_all_apps=opts.load_all_apps, + load_no_apps=opts.load_no_apps, + metadata_off=opts.no_metadata, + initial_namespaces=args, + opt_meta_paths=opt_meta_paths, + no_warn=opts.no_warn) + + +if __name__ == '__main__': + main() diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py new file mode 100644 index 000000000..77bd824f1 --- /dev/null +++ b/metomi/rose/config_editor/menu.py @@ -0,0 +1,1026 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import ast +import inspect +import os +import shlex +import subprocess +import sys +import traceback + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.config_editor +import rose.config_editor.upgrade_controller +import rose.external +import rose.gtk.dialog +import rose.gtk.run +import rose.macro +import rose.macros +import rose.popen +import rose.suite_control +import rose.suite_engine_proc + + +class MenuBar(object): + + """Generate the menu bar, using the GTK UIManager. + + Parses the settings in 'ui_config_string'. Connection of buttons is done + at a higher level. + + """ + + ui_config_string = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + action_details = [ + ('File', None, + rose.config_editor.TOP_MENU_FILE), + ('Open...', Gtk.STOCK_OPEN, + rose.config_editor.TOP_MENU_FILE_OPEN, + rose.config_editor.ACCEL_OPEN), + ('Save', Gtk.STOCK_SAVE, + rose.config_editor.TOP_MENU_FILE_SAVE, + rose.config_editor.ACCEL_SAVE), + ('Check and save', Gtk.STOCK_SPELL_CHECK, + rose.config_editor.TOP_MENU_FILE_CHECK_AND_SAVE), + ('Load All Apps', Gtk.STOCK_CDROM, + rose.config_editor.TOP_MENU_FILE_LOAD_APPS), + ('Quit', Gtk.STOCK_QUIT, + rose.config_editor.TOP_MENU_FILE_QUIT, + rose.config_editor.ACCEL_QUIT), + ('Edit', None, + rose.config_editor.TOP_MENU_EDIT), + ('Undo', Gtk.STOCK_UNDO, + rose.config_editor.TOP_MENU_EDIT_UNDO, + rose.config_editor.ACCEL_UNDO), + ('Redo', Gtk.STOCK_REDO, + rose.config_editor.TOP_MENU_EDIT_REDO, + rose.config_editor.ACCEL_REDO), + ('Stack', Gtk.STOCK_INFO, + rose.config_editor.TOP_MENU_EDIT_STACK), + ('Find', Gtk.STOCK_FIND, + rose.config_editor.TOP_MENU_EDIT_FIND, + rose.config_editor.ACCEL_FIND), + ('Find Next', Gtk.STOCK_FIND, + rose.config_editor.TOP_MENU_EDIT_FIND_NEXT, + rose.config_editor.ACCEL_FIND_NEXT), + ('Preferences', Gtk.STOCK_PREFERENCES, + rose.config_editor.TOP_MENU_EDIT_PREFERENCES), + ('View', None, + rose.config_editor.TOP_MENU_VIEW), + ('Page', None, + rose.config_editor.TOP_MENU_PAGE), + ('Add variable', Gtk.STOCK_ADD, + rose.config_editor.TOP_MENU_PAGE_ADD), + ('Revert', Gtk.STOCK_REVERT_TO_SAVED, + rose.config_editor.TOP_MENU_PAGE_REVERT), + ('Page Info', Gtk.STOCK_INFO, + rose.config_editor.TOP_MENU_PAGE_INFO), + ('Page Help', Gtk.STOCK_HELP, + rose.config_editor.TOP_MENU_PAGE_HELP), + ('Page Web Help', Gtk.STOCK_HOME, + rose.config_editor.TOP_MENU_PAGE_WEB_HELP), + ('Metadata', None, + rose.config_editor.TOP_MENU_METADATA), + ('Reload metadata', Gtk.STOCK_REFRESH, + rose.config_editor.TOP_MENU_METADATA_REFRESH, + rose.config_editor.ACCEL_METADATA_REFRESH), + ('Load custom metadata', Gtk.STOCK_DIRECTORY, + rose.config_editor.TOP_MENU_METADATA_LOAD), + ('Prefs', Gtk.STOCK_PREFERENCES, + rose.config_editor.TOP_MENU_METADATA_PREFERENCES), + ('Upgrade', Gtk.STOCK_GO_UP, + rose.config_editor.TOP_MENU_METADATA_UPGRADE), + ('All V', Gtk.STOCK_DIALOG_QUESTION, + rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V), + ('Autofix', Gtk.STOCK_CONVERT, + rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX), + ('Extra checks', Gtk.STOCK_DIALOG_QUESTION, + rose.config_editor.TOP_MENU_METADATA_CHECK), + ('Graph', Gtk.STOCK_SORT_ASCENDING, + rose.config_editor.TOP_MENU_METADATA_GRAPH), + ('Tools', None, + rose.config_editor.TOP_MENU_TOOLS), + ('Run Suite', Gtk.STOCK_MEDIA_PLAY, + rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN), + ('Run Suite default', Gtk.STOCK_MEDIA_PLAY, + rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_DEFAULT, + rose.config_editor.ACCEL_SUITE_RUN), + ('Run Suite custom', Gtk.STOCK_EDIT, + rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_CUSTOM), + ('Browser', Gtk.STOCK_DIRECTORY, + rose.config_editor.TOP_MENU_TOOLS_BROWSER, + rose.config_editor.ACCEL_BROWSER), + ('Terminal', Gtk.STOCK_EXECUTE, + rose.config_editor.TOP_MENU_TOOLS_TERMINAL, + rose.config_editor.ACCEL_TERMINAL), + ('View Output', Gtk.STOCK_DIRECTORY, + rose.config_editor.TOP_MENU_TOOLS_VIEW_OUTPUT), + ('Open Suite GControl', "rose-gtk-scheduler", + rose.config_editor.TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL), + ('Help', None, + rose.config_editor.TOP_MENU_HELP), + ('Documentation', Gtk.STOCK_HELP, + rose.config_editor.TOP_MENU_HELP_GUI, + rose.config_editor.ACCEL_HELP_GUI), + ('About', Gtk.STOCK_DIALOG_INFO, + rose.config_editor.TOP_MENU_HELP_ABOUT)] + + toggle_action_details = [ + ('View latent vars', None, + rose.config_editor.TOP_MENU_VIEW_LATENT_VARS), + ('View fixed vars', None, + rose.config_editor.TOP_MENU_VIEW_FIXED_VARS), + ('View ignored vars', None, + rose.config_editor.TOP_MENU_VIEW_IGNORED_VARS), + ('View user-ignored vars', None, + rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_VARS), + ('View without descriptions', None, + rose.config_editor.TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS), + ('View without help', None, + rose.config_editor.TOP_MENU_VIEW_WITHOUT_HELP), + ('View without titles', None, + rose.config_editor.TOP_MENU_VIEW_WITHOUT_TITLES), + ('View ignored pages', None, + rose.config_editor.TOP_MENU_VIEW_IGNORED_PAGES), + ('View user-ignored pages', None, + rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_PAGES), + ('View latent pages', None, + rose.config_editor.TOP_MENU_VIEW_LATENT_PAGES), + ('Flag opt config vars', None, + rose.config_editor.TOP_MENU_VIEW_FLAG_OPT_CONF_VARS), + ('Flag optional vars', None, + rose.config_editor.TOP_MENU_VIEW_FLAG_OPTIONAL_VARS), + ('Flag no-metadata vars', None, + rose.config_editor.TOP_MENU_VIEW_FLAG_NO_METADATA_VARS), + ('View status bar', None, + rose.config_editor.TOP_MENU_VIEW_STATUS_BAR), + ('Switch off metadata', None, + rose.config_editor.TOP_MENU_METADATA_SWITCH_OFF)] + + def __init__(self): + self.uimanager = Gtk.UIManager() + self.actiongroup = Gtk.ActionGroup('MenuBar') + self.actiongroup.add_actions(self.action_details) + self.actiongroup.add_toggle_actions(self.toggle_action_details) + self.uimanager.insert_action_group(self.actiongroup, pos=0) + self.uimanager.add_ui_from_string(self.ui_config_string) + self.macro_ids = [] + + def set_accelerators(self, accel_dict): + """Add the keyboard accelerators.""" + self.accelerators = Gtk.AccelGroup() + self.accelerators.lookup = {} # Unfortunately, this is necessary. + for key_press, accel_func in list(accel_dict.items()): + key, mod = Gtk.accelerator_parse(key_press) + self.accelerators.lookup[str(key) + str(mod)] = accel_func + self.accelerators.connect_group( + key, mod, + Gtk.AccelFlags.VISIBLE, + lambda a, c, k, m: self.accelerators.lookup[str(k) + str(m)]()) + + def clear_macros(self): + """Reset menu to original configuration and clear macros.""" + for merge_id in self.macro_ids: + self.uimanager.remove_ui(merge_id) + self.macro_ids = [] + all_v_item = self.uimanager.get_widget("/TopMenuBar/Metadata/All V") + all_v_item.set_sensitive(False) + + def add_macro(self, config_name, modulename, classname, methodname, + help_, image_path, run_macro): + """Add a macro to the macro menu.""" + macro_address = '/TopMenuBar/Metadata' + self.uimanager.get_widget(macro_address).get_submenu() + if methodname == rose.macro.VALIDATE_METHOD: + all_v_item = self.uimanager.get_widget(macro_address + "/All V") + all_v_item.set_sensitive(True) + config_menu_name = config_name.replace('/', ':').replace('_', '__') + config_label_name = config_name.split('/')[-1].replace('_', '__') + label = rose.config_editor.TOP_MENU_METADATA_MACRO_CONFIG.format( + config_label_name) + config_address = macro_address + '/' + config_menu_name + config_item = self.uimanager.get_widget(config_address) + if config_item is None: + actiongroup = self.uimanager.get_action_groups()[0] + if actiongroup.get_action(config_menu_name) is None: + actiongroup.add_action(Gtk.Action(config_menu_name, + label, + None, None)) + new_ui = """ + + + """.format(config_menu_name) + self.macro_ids.append(self.uimanager.add_ui_from_string(new_ui)) + config_item = self.uimanager.get_widget(config_address) + if image_path is not None: + image = Gtk.image_new_from_file(image_path) + config_item.set_image(image) + if config_item.get_submenu() is None: + config_item.set_submenu(Gtk.Menu()) + macro_fullname = ".".join([modulename, classname, methodname]) + macro_fullname = macro_fullname.replace("_", "__") + if methodname == rose.macro.VALIDATE_METHOD: + stock_id = Gtk.STOCK_DIALOG_QUESTION + else: + stock_id = Gtk.STOCK_CONVERT + macro_item = Gtk.ImageMenuItem(stock_id=stock_id) + macro_item.set_label(macro_fullname) + macro_item.set_tooltip_text(help_) + macro_item.show() + macro_item._run_data = [config_name, modulename, classname, + methodname] + macro_item.connect("activate", + lambda i: run_macro(*i._run_data)) + config_item.get_submenu().append(macro_item) + if (methodname == rose.macro.VALIDATE_METHOD): + for item in config_item.get_submenu().get_children(): + if hasattr(item, "_rose_all_validators"): + return False + all_item = Gtk.ImageMenuItem(Gtk.STOCK_DIALOG_QUESTION) + all_item._rose_all_validators = True + all_item.set_label(rose.config_editor.MACRO_MENU_ALL_VALIDATORS) + all_item.set_tooltip_text( + rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) + all_item.show() + all_item._run_data = [config_name, None, None, methodname] + all_item.connect("activate", + lambda i: run_macro(*i._run_data)) + config_item.get_submenu().prepend(all_item) + + +class MainMenuHandler(object): + + """Handles signals from the main menu and tool bar.""" + + def __init__(self, data, util, reporter, mainwindow, + undo_stack, redo_stack, undo_func, + update_config_func, + apply_macro_transform_func, apply_macro_validation_func, + group_ops_inst, section_ops_inst, variable_ops_inst, + find_ns_id_func): + self.data = data + self.util = util + self.reporter = reporter + self.mainwindow = mainwindow + self.undo_stack = undo_stack + self.redo_stack = redo_stack + self.perform_undo = undo_func + self.update_config = update_config_func + self.apply_macro_transform = apply_macro_transform_func + self.apply_macro_validation = apply_macro_validation_func + self.group_ops = group_ops_inst + self.sect_ops = section_ops_inst + self.var_ops = variable_ops_inst + self.find_ns_id_func = find_ns_id_func + self.bad_colour = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + + def about_dialog(self, args): + self.mainwindow.launch_about_dialog() + + def get_orphan_container(self, page): + """Return a container with the page object inside.""" + box = Gtk.VBox() + box.pack_start(page, expand=True, fill=True) + box.show() + return box + + def view_stack(self, args): + """Handle a View Stack request.""" + self.mainwindow.launch_view_stack(self.undo_stack, self.redo_stack, + self.perform_undo) + + def destroy(self, *args): + """Handle a destroy main program request.""" + for name in self.data.config: + if self.data.helper.get_config_has_unsaved_changes(name): + self.mainwindow.launch_exit_warning_dialog() + return True + try: + Gtk.main_quit() + except RuntimeError: + # This can occur before Gtk.main() is called, during the load. + sys.exit() + + def check_all_extra(self): + """Check fail-if, warn-if, and run all validator macros.""" + for config_name in self.data.config: + if not self.data.config[config_name].is_preview: + self.update_config(config_name) + num_errors = self.check_fail_rules(configs_updated=True) + num_errors += self.run_custom_macro( + method_name=rose.macro.VALIDATE_METHOD, + configs_updated=True) + if num_errors: + text = rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL.format( + num_errors) + kind = self.reporter.KIND_ERR + else: + text = rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL_OK + kind = self.reporter.KIND_OUT + self.reporter.report(text, kind=kind) + + def check_fail_rules(self, configs_updated=False): + """Check the fail-if and warn-if conditions of the configurations.""" + if not configs_updated: + for config_name in self.data.config: + if not self.data.config[config_name].is_preview: + self.update_config(config_name) + macro = rose.macros.rule.FailureRuleChecker() + macro_fullname = "rule.FailureRuleChecker.validate" + error_count = 0 + for config_name in sorted(self.data.config.keys()): + config_data = self.data.config[config_name] + if config_data.is_preview: + continue + config = config_data.config + meta = config_data.meta + try: + return_value = macro.validate(config, meta) + if return_value: + error_count += len(return_value) + except Exception as exc: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + str(exc), + rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + macro_fullname)) + continue + sorter = rose.config.sort_settings + to_id = lambda s: self.util.get_id_from_section_option( + s.section, s.option) + return_value.sort(lambda x, y: sorter(to_id(x), to_id(y))) + self.handle_macro_validation(config_name, macro_fullname, + config, return_value, + no_display=(not return_value)) + if error_count > 0: + msg = rose.config_editor.EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND + info_text = msg.format(error_count) + kind = self.reporter.KIND_ERR + else: + msg = rose.config_editor.EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS + info_text = msg + kind = self.reporter.KIND_OUT + self.reporter.report(info_text, kind=kind) + return error_count + + def clear_page_menu(self, menubar, add_menuitem): + """Clear all page add variable items.""" + add_menuitem.remove_submenu() + + def load_page_menu(self, menubar, add_menuitem, current_page): + """Load the page add variable items, if any.""" + if current_page is None: + return False + add_var_menu = current_page.get_add_menu() + if add_var_menu is None or not add_var_menu.get_children(): + add_menuitem.set_sensitive(False) + return False + add_menuitem.set_sensitive(True) + add_menuitem.set_submenu(add_var_menu) + + def load_macro_menu(self, menubar): + """Refresh the menu dealing with custom macro launches.""" + menubar.clear_macros() + config_keys = sorted(list(self.data.config.keys())) + tuple_sorter = lambda x, y: cmp(x[0], y[0]) + for config_name in config_keys: + image = self.data.helper.get_icon_path_for_config(config_name) + macros = self.data.config[config_name].macros + macro_tuples = rose.macro.get_macro_class_methods(macros) + macro_tuples.sort(tuple_sorter) + for macro_mod, macro_cls, macro_func, help_ in macro_tuples: + menubar.add_macro(config_name, macro_mod, macro_cls, + macro_func, help_, image, + self.handle_run_custom_macro) + + def inspect_custom_macro(self, macro_meth): + """Inspect a custom macro for kwargs and return any""" + arglist = inspect.getargspec(macro_meth).args + defaultlist = inspect.getargspec(macro_meth).defaults + optionals = {} + while defaultlist is not None and len(defaultlist) > 0: + if arglist[-1] not in ["self", "config", "meta_config"]: + optionals[arglist[-1]] = defaultlist[-1] + arglist = arglist[0:-1] + defaultlist = defaultlist[0:-1] + else: + break + return optionals + + def handle_graph(self): + """Handle a graph metadata request.""" + config_sect_dict = {} + for config_name in self.data.config: + config_data = self.data.config[config_name] + config_sect_dict[config_name] = list(config_data.sections.now.keys()) + config_sect_dict[config_name].sort(rose.config.sort_settings) + config_name, section = self.mainwindow.launch_graph_dialog( + config_sect_dict) + if config_name is None: + return False + if section is None: + allowed_sections = None + else: + allowed_sections = [section] + self.launch_graph(config_name, allowed_sections=allowed_sections) + + def check_entry_value(self, entry_widget, dialog, entries, + labels, optionals): + is_valid = True + for k, entry in list(entries.items()): + this_is_valid = True + try: + new_val = ast.literal_eval(entry.get_text()) + entry.modify_text(Gtk.StateType.NORMAL, None) + except (ValueError, EOFError, SyntaxError): + entry.modify_text(Gtk.StateType.NORMAL, self.bad_colour) + is_valid = False + this_is_valid = False + if not this_is_valid or new_val != optionals[k]: + lab = '{0}'.format(str(k) + ":") + labels[k].set_markup(lab) + else: + labels[k].set_text(str(k) + ":") + dialog.set_response_sensitive(Gtk.ResponseType.OK, is_valid) + return + + def handle_macro_entry_activate(self, entry_widget, dialog, entries): + for entry in list(entries.values()): + try: + ast.literal_eval(entry.get_text()) + except (ValueError, EOFError, SyntaxError): + break + else: + dialog.response(Gtk.ResponseType.OK) + + def override_macro_defaults(self, optionals, methname): + """Launch a dialog to handle capture of any override args to macro""" + if not optionals: + return {} + res = {} + # create the text input field + entries = {} + labels = {} + dialog = Gtk.MessageDialog( + None, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.QUESTION, + Gtk.ButtonsType.OK_CANCEL, + None) + dialog.set_markup('Specify overrides for macro arguments:') + dialog.set_title(methname) + table = Gtk.Table(len(list(optionals.items())), 2, False) + dialog.vbox.add(table) + for i in range(len(list(optionals.items()))): + key, value = list(optionals.items())[i] + label = Gtk.Label(label=str(key) + ":") + entry = Gtk.Entry() + if isinstance(value, str): + entry.set_text("'" + value + "'") + else: + entry.set_text(str(value)) + entry.connect("changed", self.check_entry_value, dialog, + entries, labels, optionals) + entry.connect("activate", self.handle_macro_entry_activate, + dialog, entries) + entries[key] = entry + labels[key] = label + table.attach(entry, 1, 2, i, i + 1) + hbox = Gtk.HBox() + hbox.pack_start(label, False, True, 0) + table.attach(hbox, 0, 1, i, i + 1) + dialog.show_all() + response = dialog.run() + if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.CLOSE: + res = optionals + dialog.destroy() + else: + res = {} + for key, box in list(entries.items()): + res[key] = ast.literal_eval(box.get_text()) + dialog.destroy() + return res + + def handle_run_custom_macro(self, *args, **kwargs): + """Wrap the method so that this returns False for GTK callbacks.""" + self.run_custom_macro(*args, **kwargs) + return False + + def run_custom_macro(self, config_name=None, module_name=None, + class_name=None, method_name=None, + configs_updated=False): + """Run the custom macro method and launch a dialog.""" + old_pwd = os.getcwd() + macro_data = [] + if config_name is None: + configs = sorted(self.data.config.keys()) + else: + configs = [config_name] + for name in list(configs): + if self.data.config[name].is_preview: + configs.remove(name) + continue + if not configs_updated: + self.update_config(name) + if method_name is None: + method_names = [rose.macro.VALIDATE_METHOD, + rose.macro.TRANSFORM_METHOD] + else: + method_names = [method_name] + if module_name is not None and config_name is not None: + config_mod_prefix = ( + self.data.helper.get_macro_module_prefix(config_name)) + if not module_name.startswith(config_mod_prefix): + module_name = config_mod_prefix + module_name + for config_name in configs: + config_data = self.data.config[config_name] + if config_data.directory is not None: + os.chdir(config_data.directory) + for module in config_data.macros: + if module_name is not None and module.__name__ != module_name: + continue + for obj_name, obj in inspect.getmembers(module): + for method_name in method_names: + if (not hasattr(obj, method_name) or + obj_name.startswith("_") or + not issubclass(obj, rose.macro.MacroBase)): + continue + if class_name is not None and obj_name != class_name: + continue + macro_fullname = ".".join([module.__name__, + obj_name, + method_name]) + err_text = ( + rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + macro_fullname)) + try: + macro_inst = obj() + except Exception as exc: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + str(exc), err_text) + continue + if hasattr(macro_inst, method_name): + macro_data.append((config_name, macro_inst, + module.__name__, obj_name, + method_name)) + os.chdir(old_pwd) + if not macro_data: + return 0 + sorter = rose.config.sort_settings + to_id = lambda s: self.util.get_id_from_section_option(s.section, + s.option) + config_macro_errors = [] + config_macro_changes = [] + for config_name, macro_inst, modname, objname, methname in macro_data: + macro_fullname = '.'.join([modname, objname, methname]) + macro_config = self.data.dump_to_internal_config(config_name) + config_data = self.data.config[config_name] + meta_config = config_data.meta + macro_method = getattr(macro_inst, methname) + optionals = self.inspect_custom_macro(macro_method) + if optionals: + res = self.override_macro_defaults(optionals, objname) + else: + res = {} + os.chdir(config_data.directory) + try: + return_value = macro_method(macro_config, meta_config, **res) + except Exception: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + 'Error in custom macro:\n\n%s' % ( + traceback.format_exc()), + rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + macro_fullname)) + continue + if methname == rose.macro.TRANSFORM_METHOD: + if (not isinstance(return_value, tuple) or + len(return_value) != 2 or + not isinstance( + return_value[0], rose.config.ConfigNode) or + not isinstance(return_value[1], list)): + self._handle_bad_macro_return(macro_fullname, return_value) + continue + integrity_exception = rose.macro.check_config_integrity( + return_value[0]) + if integrity_exception is not None: + self._handle_bad_macro_return(macro_fullname, + integrity_exception) + continue + macro_config, change_list = return_value + if not change_list: + continue + change_list.sort(lambda x, y: sorter(to_id(x), to_id(y))) + num_changes = len(change_list) + self.handle_macro_transforms(config_name, macro_fullname, + macro_config, change_list) + config_macro_changes.append((config_name, + macro_fullname, + num_changes)) + continue + elif methname == rose.macro.VALIDATE_METHOD: + if not isinstance(return_value, list): + self._handle_bad_macro_return(macro_fullname, + return_value) + continue + if return_value: + return_value.sort(lambda x, y: sorter(to_id(x), to_id(y))) + config_macro_errors.append((config_name, + macro_fullname, + len(return_value))) + self.handle_macro_validation(config_name, macro_fullname, + macro_config, return_value) + os.chdir(old_pwd) + if class_name is None: + # Construct a grouped report. + config_macro_errors.sort() + config_macro_changes.sort() + if rose.macro.VALIDATE_METHOD in method_names: + null_format = rose.config_editor.EVENT_MACRO_VALIDATE_ALL_OK + change_format = rose.config_editor.EVENT_MACRO_VALIDATE_ALL + num_issues = sum([e[2] for e in config_macro_errors]) + issue_confs = [e[0] for e in config_macro_errors if e[2]] + else: + null_format = rose.config_editor.EVENT_MACRO_TRANSFORM_ALL_OK + change_format = rose.config_editor.EVENT_MACRO_TRANSFORM_ALL + num_issues = sum([e[2] for e in config_macro_changes]) + issue_confs = [e[0] for e in config_macro_changes if e[2]] + issue_confs = sorted(set(issue_confs)) + if num_issues: + issue_conf_text = self._format_macro_config_names(issue_confs) + self.reporter.report(change_format.format(issue_conf_text, + num_issues), + kind=self.reporter.KIND_ERR) + else: + all_conf_text = self._format_macro_config_names(configs) + self.reporter.report(null_format.format(all_conf_text), + kind=self.reporter.KIND_OUT) + num_errors = sum([e[2] for e in config_macro_errors]) + num_changes = sum([c[2] for c in config_macro_changes]) + return num_errors + num_changes + + def _format_macro_config_names(self, config_names): + if len(config_names) > 5: + return rose.config_editor.EVENT_MACRO_CONFIGS.format( + len(config_names)) + config_names = [c.lstrip("/") for c in config_names] + return ", ".join(config_names) + + def _handle_bad_macro_return(self, macro_fullname, info): + if isinstance(info, Exception): + text = rose.config_editor.ERROR_BAD_MACRO_EXCEPTION.format( + type(info).__name__, str(info)) + else: + text = rose.config_editor.ERROR_BAD_MACRO_RETURN.format(info) + summary = rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + macro_fullname) + self.reporter.report(summary, + kind=self.reporter.KIND_ERR) + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, summary) + + def handle_macro_transforms(self, config_name, macro_name, + macro_config, change_list, no_display=False, + triggers_ok=False): + """Calculate needed changes and apply them if prompted to. + + At the moment trigger-ignore of variables and sections is + assumed to be the exclusive property of the Rose trigger + macro and is not allowed for any other macro. + + """ + if not change_list: + self._report_macro_transform(config_name, macro_name, 0) + return + macro_type = ".".join(macro_name.split(".")[:-1]) + var_changes = [] + sect_changes = [] + for item in list(change_list): + if item.option is None: + sect_changes.append(item) + else: + var_changes.append(item) + search = lambda i: self.find_ns_id_func(config_name, i) + if not no_display: + proceed_ok = self.mainwindow.launch_macro_changes_dialog( + config_name, macro_type, change_list, search_func=search) + if not proceed_ok: + self._report_macro_transform(config_name, macro_name, 0) + return 0 + config_diff = macro_config - self.data.config[config_name].config + changed_ids = self.group_ops.apply_diff(config_name, config_diff, + origin_name=macro_type, + triggers_ok=triggers_ok) + self.apply_macro_transform( + config_name, changed_ids, skip_update=True) + self._report_macro_transform(config_name, macro_name, len(change_list)) + return len(change_list) + + def _report_macro_transform(self, config_name, macro_name, num_changes): + name = config_name.lstrip("/") + if macro_name.endswith(rose.macro.TRANSFORM_METHOD): + macro = macro_name.split('.')[-2] + else: + macro = macro_name.split('.')[-1] + kind = self.reporter.KIND_OUT + if num_changes: + info_text = rose.config_editor.EVENT_MACRO_TRANSFORM.format( + name, macro, num_changes) + else: + info_text = rose.config_editor.EVENT_MACRO_TRANSFORM_OK.format( + name, macro) + self.reporter.report(info_text, kind=kind) + + def handle_macro_validation(self, config_name, macro_name, + macro_config, problem_list, no_display=False): + """Apply errors and give information to the user.""" + macro_type = ".".join(macro_name.split(".")[:-1]) + self.apply_macro_validation(config_name, macro_type, problem_list) + search = lambda i: self.find_ns_id_func(config_name, i) + self._report_macro_validation(config_name, macro_name, + len(problem_list)) + if not no_display: + self.mainwindow.launch_macro_changes_dialog( + config_name, macro_type, problem_list, + mode="validate", search_func=search) + + def _report_macro_validation(self, config_name, macro_name, num_errors): + name = config_name.lstrip("/") + if macro_name.endswith(rose.macro.VALIDATE_METHOD): + macro = macro_name.split('.')[-2] + else: + macro = macro_name.split('.')[-1] + if num_errors: + info_text = rose.config_editor.EVENT_MACRO_VALIDATE.format( + name, macro, num_errors) + kind = self.reporter.KIND_ERR + else: + info_text = rose.config_editor.EVENT_MACRO_VALIDATE_OK.format( + name, macro) + kind = self.reporter.KIND_OUT + self.reporter.report(info_text, kind=kind) + + def handle_upgrade(self, only_this_config_name=None): + """Run the upgrade manager for this suite.""" + config_dict = {} + for config_name in self.data.config: + config_data = self.data.config[config_name] + if config_data.is_preview: + continue + self.update_config(config_name) + if (only_this_config_name is None or + config_name == only_this_config_name): + config_dict[config_name] = { + "config": config_data.config, + "directory": config_data.directory + } + rose.config_editor.upgrade_controller.UpgradeController( + config_dict, self.handle_macro_transforms, + parent_window=self.mainwindow.window, + upgrade_inspector=self.override_macro_defaults) + + def help(self, *args): + """Handle a GUI help request.""" + self.mainwindow.launch_help_dialog() + + def prefs(self, args): + """Handle a Preferences view request.""" + self.mainwindow.launch_prefs() + + def launch_browser(self): + start_directory = self.data.top_level_directory + if self.data.top_level_directory is None: + start_directory = os.getcwd() + try: + rose.external.launch_fs_browser(start_directory) + except rose.popen.RosePopenError as exc: + rose.gtk.dialog.run_exception_dialog(exc) + + def launch_graph(self, namespace, allowed_sections=None): + try: + import pygraphviz + except ImportError as exc: + title = rose.config_editor.WARNING_CANNOT_GRAPH + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + str(exc), title) + return + else: + del pygraphviz + config_name = self.util.split_full_ns(self.data, namespace)[0] + self.update_config(config_name) + config_data = self.data.config[config_name] + if config_data.directory is None: + return False + if allowed_sections is None: + if config_name == namespace: + allowed_sections = [] + else: + allowed_sections = ( + self.data.helper.get_sections_from_namespace(namespace)) + cmd = (shlex.split(rose.config_editor.LAUNCH_COMMAND_GRAPH) + + [config_data.directory] + allowed_sections) + try: + rose.popen.RosePopener().run_bg( + *cmd, stdout=sys.stdout, stderr=sys.stderr) + except rose.popen.RosePopenError as exc: + rose.gtk.dialog.run_exception_dialog(exc) + + def launch_scheduler(self, *args): + """Run the scheduler for a suite open in config edit.""" + this_id = self.data.top_level_name + scontrol = rose.suite_control.SuiteControl() + if scontrol.suite_engine_proc.is_suite_registered(this_id): + try: + return scontrol.gcontrol(this_id) + except rose.suite_control.SuiteNotRunningError as err: + msg = rose.config_editor.DIALOG_TEXT_SUITE_NOT_RUNNING.format( + str(err)) + return rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + msg, + rose.config_editor.DIALOG_TITLE_SUITE_NOT_RUNNING) + else: + msg = rose.config_editor.DIALOG_TEXT_UNREGISTERED_SUITE.format( + this_id) + return rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + msg, + rose.config_editor.DIALOG_TITLE_UNREGISTERED_SUITE) + + def launch_terminal(self): + # Handle a launch terminal request. + try: + rose.external.launch_terminal() + except rose.popen.RosePopenError as exc: + rose.gtk.dialog.run_exception_dialog(exc) + + def launch_output_viewer(self): + """View a suite's output, if any.""" + seproc = rose.suite_engine_proc.SuiteEngineProcessor.get_processor() + try: + seproc.launch_suite_log_browser(None, self.data.top_level_name) + except rose.suite_engine_proc.NoSuiteLogError: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_NO_OUTPUT.format( + self.data.top_level_name), + rose.config_editor.DIALOG_TITLE_ERROR) + + def get_run_suite_args(self, *args): + """Ask the user for custom arguments to suite run.""" + help_cmds = shlex.split(rose.config_editor.LAUNCH_SUITE_RUN_HELP) + help_text = subprocess.Popen(help_cmds, + stdout=subprocess.PIPE).communicate()[0] + rose.gtk.dialog.run_command_arg_dialog( + rose.config_editor.LAUNCH_SUITE_RUN, + help_text, self.run_suite_check_args) + + def run_suite_check_args(self, args): + if args is None: + return False + self.run_suite(args) + + def run_suite(self, args=None, **kwargs): + """Run the suite, if possible.""" + if not isinstance(args, list): + args = [] + for key, value in list(kwargs.items()): + args.extend([key, value]) + rose.gtk.run.run_suite(*args) + return False + + def transform_default(self, only_this_config=None): + """Run the Rose built-in transformer macros.""" + if (only_this_config is not None and + only_this_config in list(self.data.config.keys())): + config_keys = [only_this_config] + text = rose.config_editor.DIALOG_LABEL_AUTOFIX + else: + config_keys = sorted(self.data.config.keys()) + text = rose.config_editor.DIALOG_LABEL_AUTOFIX_ALL + proceed = rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_WARNING, + text, + rose.config_editor.DIALOG_TITLE_AUTOFIX, + cancel=True) + if not proceed: + return False + sorter = rose.config.sort_settings + to_id = lambda s: self.util.get_id_from_section_option(s.section, + s.option) + for config_name in config_keys: + macro_config = self.data.dump_to_internal_config(config_name) + meta_config = self.data.config[config_name].meta + macro = rose.macros.DefaultTransforms() + change_list = macro.transform(macro_config, meta_config)[1] + change_list.sort(lambda x, y: sorter(to_id(x), to_id(y))) + self.handle_macro_transforms( + config_name, "Autofixer.transform", + macro_config, change_list, triggers_ok=True) diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py new file mode 100644 index 000000000..8f09a3b38 --- /dev/null +++ b/metomi/rose/config_editor/menuwidget.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +import rose.config_editor.util +import rose.gtk.dialog +import rose.gtk.util + + +class MenuWidget(Gtk.HBox): + + """This class generates a button with a menu for variable actions.""" + + MENU_ICON_ERRORS = 'rose-gtk-gnome-package-system-errors' + MENU_ICON_WARNINGS = 'rose-gtk-gnome-package-system-warnings' + MENU_ICON_LATENT = 'rose-gtk-gnome-add' + MENU_ICON_LATENT_ERRORS = 'rose-gtk-gnome-add-errors' + MENU_ICON_LATENT_WARNINGS = 'rose-gtk-gnome-add-warnings' + MENU_ICON_NORMAL = 'rose-gtk-gnome-package-system-normal' + + def __init__(self, variable, var_ops, remove_func, update_func, + launch_help_func): + super(MenuWidget, self).__init__(homogeneous=False, spacing=0) + self.my_variable = variable + self.var_ops = var_ops + self.trigger_remove = remove_func + self.update_status = update_func + self.launch_help = launch_help_func + self.is_ghost = self.var_ops.is_var_ghost(variable) + self.load_contents() + + def load_contents(self): + """Load the GTK, including menu.""" + variable = self.my_variable + option_ui_start = """ + """ + option_ui_middle = """ + """ + option_ui_end = """ + + + + + """ + actions = [('Options', 'rose-gtk-gnome-package-system', ''), + ('Info', Gtk.STOCK_INFO, + rose.config_editor.VAR_MENU_INFO), + ('Help', Gtk.STOCK_HELP, + rose.config_editor.VAR_MENU_HELP), + ('Web Help', Gtk.STOCK_HOME, + rose.config_editor.VAR_MENU_URL), + ('Edit', Gtk.STOCK_EDIT, + rose.config_editor.VAR_MENU_EDIT_COMMENTS), + ('Fix Ignore', Gtk.STOCK_CONVERT, + rose.config_editor.VAR_MENU_FIX_IGNORE), + ('Ignore', Gtk.STOCK_NO, + rose.config_editor.VAR_MENU_IGNORE), + ('Enable', Gtk.STOCK_YES, + rose.config_editor.VAR_MENU_ENABLE), + ('Remove', Gtk.STOCK_DELETE, + rose.config_editor.VAR_MENU_REMOVE), + ('Add', Gtk.STOCK_ADD, + rose.config_editor.VAR_MENU_ADD)] + menu_icon_id = 'rose-gtk-gnome-package-system' + is_comp = (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE) + if self.is_ghost or is_comp: + option_ui_middle = ( + option_ui_middle.replace("", '')) + error_types = rose.config_editor.WARNING_TYPES_IGNORE + if (set(error_types) & set(variable.error.keys()) or + set(error_types) & set(variable.warning.keys()) or + (rose.META_PROP_COMPULSORY in variable.error and + not self.is_ghost)): + option_ui_middle = ("" + + "" + + option_ui_middle) + if variable.warning: + if self.is_ghost: + menu_icon_id = self.MENU_ICON_LATENT_WARNINGS + else: + menu_icon_id = self.MENU_ICON_WARNINGS + old_middle = option_ui_middle + option_ui_middle = '' + for warn in variable.warning: + warn_name = warn.replace("/", "_") + option_ui_middle += ( + "") + w_string = "(" + warn.replace("_", "__") + ")" + actions.append(("Warn_" + warn_name, Gtk.STOCK_DIALOG_INFO, + w_string)) + option_ui_middle += "" + old_middle + if variable.error: + if self.is_ghost: + menu_icon_id = self.MENU_ICON_LATENT_ERRORS + else: + menu_icon_id = self.MENU_ICON_ERRORS + old_middle = option_ui_middle + option_ui_middle = '' + for err in variable.error: + err_name = err.replace("/", "_") + option_ui_middle += ("") + e_string = "(" + err.replace("_", "__") + ")" + actions.append(("Error_" + err_name, Gtk.STOCK_DIALOG_WARNING, + e_string)) + option_ui_middle += "" + old_middle + if self.is_ghost: + if not variable.error and not variable.warning: + menu_icon_id = self.MENU_ICON_LATENT + option_ui_middle = ("" + + "" + + option_ui_middle) + if rose.META_PROP_URL in variable.metadata: + url_ui = "" + option_ui_middle += url_ui + option_ui = option_ui_start + option_ui_middle + option_ui_end + self.button = rose.gtk.util.CustomButton( + stock_id=menu_icon_id, + size=Gtk.IconSize.MENU, + as_tool=True) + self._set_hover_over(variable) + self.option_ui = option_ui + self.actions = actions + self.pack_start(self.button, expand=False, fill=False, padding=0) + self.button.connect( + "button-press-event", + lambda b, e: self._popup_option_menu( + self.option_ui, self.actions, e.button, e.time)) + # FIXME: Try to popup the menu at the button, instead of the cursor. + self.button.connect( + "activate", + lambda b: self._popup_option_menu( + self.option_ui, + self.actions, + 1, + Gdk.Event(Gdk.KEY_PRESS).time)) + self.button.connect( + "enter-notify-event", + lambda b, e: self._set_hover_over(variable)) + self._set_hover_over(variable) + self.button.show() + + def get_centre_height(self): + """Return the vertical displacement of the centre of this widget.""" + return (self.size_request()[1] / 2) + + def refresh(self, variable=None): + """Reload the contents.""" + if variable is not None: + self.my_variable = variable + for widget in self.get_children(): + self.remove(widget) + self.load_contents() + + def _set_hover_over(self, variable): + hover_string = 'Variable options' + if variable.warning: + hover_string = rose.config_editor.VAR_MENU_TIP_WARNING + for warn, warn_info in list(variable.warning.items()): + hover_string += "(" + warn + "): " + warn_info + '\n' + hover_string = hover_string.rstrip('\n') + if variable.error: + hover_string = rose.config_editor.VAR_MENU_TIP_ERROR + for err, err_info in list(variable.error.items()): + hover_string += "(" + err + "): " + err_info + '\n' + hover_string = hover_string.rstrip('\n') + if self.is_ghost: + if not variable.error: + hover_string = rose.config_editor.VAR_MENU_TIP_LATENT + self.hover_text = hover_string + self.button.set_tooltip_text(self.hover_text) + self.button.show() + + def _perform_add(self): + self.var_ops.add_var(self.my_variable) + + def _popup_option_menu(self, option_ui, actions, button, time): + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.set_translation_domain('') + actiongroup.add_actions(actions) + uimanager = Gtk.UIManager() + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(option_ui) + remove_item = uimanager.get_widget('/Options/Remove') + remove_item.connect("activate", + lambda b: self.trigger_remove()) + edit_item = uimanager.get_widget('/Options/Edit') + edit_item.connect("activate", self.launch_edit) + errors = list(self.my_variable.error.keys()) + warnings = list(self.my_variable.warning.keys()) + ns = self.my_variable.metadata["full_ns"] + search_function = lambda i: self.var_ops.search_for_var(ns, i) + dialog_func = rose.gtk.dialog.run_hyperlink_dialog + for error in errors: + err_name = error.replace("/", "_") + action_name = "Error_" + err_name + if "action='" + action_name + "'" not in option_ui: + continue + err_item = uimanager.get_widget('/Options/' + action_name) + title = rose.config_editor.DIALOG_VARIABLE_ERROR_TITLE.format( + error, self.my_variable.metadata["id"]) + err_item.set_tooltip_text(self.my_variable.error[error]) + err_item.connect( + "activate", + lambda e: dialog_func(Gtk.STOCK_DIALOG_WARNING, + self.my_variable.error[error], + title, search_function)) + for warning in warnings: + action_name = "Warn_" + warning.replace("/", "_") + if "action='" + action_name + "'" not in option_ui: + continue + warn_item = uimanager.get_widget('/Options/' + action_name) + title = rose.config_editor.DIALOG_VARIABLE_WARNING_TITLE.format( + warning, self.my_variable.metadata["id"]) + warn_item.set_tooltip_text(self.my_variable.warning[warning]) + warn_item.connect( + "activate", + lambda e: dialog_func(Gtk.STOCK_DIALOG_INFO, + self.my_variable.warning[warning], + title, search_function)) + ignore_item = None + enable_item = None + if "action='Ignore'" in option_ui: + ignore_item = uimanager.get_widget('/Options/Ignore') + if (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE or self.is_ghost): + ignore_item.set_sensitive(False) + # It is a non-trigger, optional, enabled variable. + new_reason = {rose.variable.IGNORED_BY_USER: + rose.config_editor.IGNORED_STATUS_MANUAL} + ignore_item.connect( + "activate", + lambda b: self.var_ops.set_var_ignored( + self.my_variable, new_reason)) + elif "action='Enable'" in option_ui: + enable_item = uimanager.get_widget('/Options/Enable') + enable_item.connect( + "activate", + lambda b: self.var_ops.set_var_ignored(self.my_variable, {})) + if "action='Fix Ignore'" in option_ui: + fix_ignore_item = uimanager.get_widget('/Options/Fix Ignore') + fix_ignore_item.set_tooltip_text( + rose.config_editor.VAR_MENU_TIP_FIX_IGNORE) + fix_ignore_item.connect( + "activate", + lambda e: self.var_ops.fix_var_ignored(self.my_variable)) + if ignore_item is not None: + ignore_item.set_sensitive(False) + if enable_item is not None: + enable_item.set_sensitive(False) + info_item = uimanager.get_widget('/Options/Info') + info_item.connect("activate", self._launch_info_dialog) + if (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE or self.is_ghost): + remove_item.set_sensitive(False) + help_item = uimanager.get_widget('/Options/Help') + help_item.connect("activate", + lambda b: self.launch_help()) + if rose.META_PROP_HELP not in self.my_variable.metadata: + help_item.set_sensitive(False) + url_item = uimanager.get_widget('/Options/Web Help') + if url_item is not None and 'url' in self.my_variable.metadata: + url_item.connect( + "activate", + lambda b: self.launch_help(url_mode=True)) + if self.is_ghost: + add_item = uimanager.get_widget('/Options/Add') + add_item.connect("activate", lambda b: self._perform_add()) + option_menu = uimanager.get_widget('/Options') + option_menu.attach_to_widget(self.button, + lambda m, w: False) + option_menu.show() + option_menu.popup(None, None, None, button, time) + return False + + def _launch_info_dialog(self, *args): + changes = self.var_ops.get_var_changes(self.my_variable) + ns = self.my_variable.metadata["full_ns"] + search_function = lambda i: self.var_ops.search_for_var(ns, i) + rose.config_editor.util.launch_node_info_dialog(self.my_variable, + changes, + search_function) + + def launch_edit(self, *args): + text = "\n".join(self.my_variable.comments) + title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( + self.my_variable.metadata['id']) + rose.gtk.dialog.run_edit_dialog(text, + finish_hook=self._edit_finish_hook, + title=title) + + def _edit_finish_hook(self, text): + self.var_ops.set_var_comments(self.my_variable, text.splitlines()) + self.update_status() + + +class CheckedMenuWidget(MenuWidget): + + """Represent the menu button with a check box instead.""" + + def __init__(self, *args): + super(CheckedMenuWidget, self).__init__(*args) + self.remove(self.button) + for string in ["", + "", + ""]: + self.option_ui = self.option_ui.replace(string, "") + self.checkbutton = Gtk.CheckButton() + self.checkbutton.set_active(not self.is_ghost) + meta = self.my_variable.metadata + if not self.is_ghost and meta.get( + rose.META_PROP_COMPULSORY) == rose.META_PROP_VALUE_TRUE: + self.checkbutton.set_sensitive(False) + self.pack_start(self.checkbutton, expand=False, fill=False, padding=0) + self.pack_start(self.button, expand=False, fill=False, padding=0) + self.checkbutton.connect("toggled", self.on_toggle) + self.checkbutton.show() + + def on_toggle(self, widget): + """Handle a toggle.""" + if self.is_ghost: + self._perform_add() + else: + self.trigger_remove() diff --git a/metomi/rose/config_editor/nav_controller.py b/metomi/rose/config_editor/nav_controller.py new file mode 100644 index 000000000..5eb4eb253 --- /dev/null +++ b/metomi/rose/config_editor/nav_controller.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + + +import rose.config_editor + + +class NavTreeManager(object): + + """This controls the navigation namespace tree structure.""" + + def __init__(self, data, util, reporter, tree_trigger_update): + self.data = data + self.util = util + self.reporter = reporter + self.tree_trigger_update = tree_trigger_update + self.namespace_tree = {} # Stores the namespace hierarchy + + def is_ns_in_tree(self, ns): + """Determine if the namespace is in the tree or not.""" + if ns is None: + return False + spaces = ns.lstrip('/').split('/') + subtree = self.namespace_tree + while spaces: + if spaces[0] not in subtree: + return False + subtree = subtree[spaces[0]][0] + spaces.pop(0) + return True + + def reload_namespace_tree(self, only_this_namespace=None, + only_this_config=None, + skip_update=False): + """Make the tree of namespaces and load to the tree panel.""" + # Clear the old namespace tree information (selectively if necessary). + if only_this_namespace is not None and only_this_config is None: + config_name = self.util.split_full_ns(self.data, + only_this_namespace)[0] + only_this_config = config_name + clear_namespace = only_this_namespace.rsplit("/", 1)[0] + self.clear_namespace_tree(clear_namespace) + elif only_this_config is not None: + self.clear_namespace_tree(only_this_config) + else: + self.clear_namespace_tree() + # Reload the information into the tree. + if only_this_config is None: + configs = list(self.data.config.keys()) + configs.sort(rose.config.sort_settings) + configs.sort( + lambda x, y: cmp( + self.data.config[y].config_type == rose.TOP_CONFIG_NAME, + self.data.config[x].config_type == rose.TOP_CONFIG_NAME + ) + ) + else: + configs = [only_this_config] + for config_name in configs: + config_data = self.data.config[config_name] + if only_this_namespace: + top_spaces = only_this_namespace.lstrip('/').split('/')[:-1] + else: + top_spaces = config_name.lstrip('/').split('/') + self.update_namespace_tree(top_spaces, self.namespace_tree, + prev_spaces=[]) + self.data.load_metadata_for_namespaces(config_name) + # Load tree from sections (usually vast majority of tree nodes) + self.data.load_node_namespaces(config_name) + for section_data in config_data.sections.get_all(): + ns = section_data.metadata["full_ns"] + self.data.namespace_meta_lookup.setdefault(ns, {}) + self.data.namespace_meta_lookup[ns].setdefault( + 'title', ns.split('/')[-1]) + spaces = ns.lstrip('/').split('/') + self.update_namespace_tree(spaces, + self.namespace_tree, + prev_spaces=[]) + # Now load tree from variables + for var in config_data.vars.get_all(): + ns = var.metadata['full_ns'] + self.data.namespace_meta_lookup.setdefault(ns, {}) + self.data.namespace_meta_lookup[ns].setdefault( + 'title', ns.split('/')[-1]) + spaces = ns.lstrip('/').split('/') + self.update_namespace_tree(spaces, + self.namespace_tree, + prev_spaces=[]) + if not skip_update: + # Perform an update. + self.tree_trigger_update(only_this_config=only_this_config, + only_this_namespace=only_this_namespace) + + def clear_namespace_tree(self, namespace=None): + """Clear the namespace tree, or a subtree from namespace.""" + if namespace is None: + spaces = [] + else: + spaces = namespace.lstrip('/').split('/') + tree = self.namespace_tree + for space in spaces: + if space not in tree: + break + tree = tree[space][0] + tree.clear() + + def update_namespace_tree(self, spaces, subtree, prev_spaces): + """Recursively load the namespace tree for a single path (spaces). + + The tree is specified with subtree, and it requires an array of names + to load (spaces). + + """ + if spaces: + this_ns = "/" + "/".join(prev_spaces + [spaces[0]]) + change = "" + meta = self.data.namespace_meta_lookup.get(this_ns, {}) + meta.setdefault('title', spaces[0]) + latent_status = self.data.helper.get_ns_latent_status(this_ns) + ignored_status = self.data.helper.get_ns_ignored_status(this_ns) + statuses = {rose.config_editor.SHOW_MODE_LATENT: latent_status, + rose.config_editor.SHOW_MODE_IGNORED: ignored_status} + subtree.setdefault(spaces[0], [{}, meta, statuses, change]) + prev_spaces += [spaces[0]] + self.update_namespace_tree(spaces[1:], subtree[spaces[0]][0], + prev_spaces) diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py new file mode 100644 index 000000000..723bfdf96 --- /dev/null +++ b/metomi/rose/config_editor/nav_panel.py @@ -0,0 +1,612 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re +import sys + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import GObject + +import rose.config +import rose.config_editor +import rose.config_editor.util +import rose.gtk.util +import rose.resource + + +class PageNavigationPanel(Gtk.ScrolledWindow): + + """Generate the page launcher panel. + + This contains the namespace groupings as child rows. + Icons denoting changes in the attribute internal data are displayed + next to the attributes. + + """ + + COLUMN_ERROR_ICON = 0 + COLUMN_CHANGE_ICON = 1 + COLUMN_TITLE = 2 + COLUMN_NAME = 3 + COLUMN_ERROR_INTERNAL = 4 + COLUMN_ERROR_TOTAL = 5 + COLUMN_CHANGE_INTERNAL = 6 + COLUMN_CHANGE_TOTAL = 7 + COLUMN_LATENT_STATUS = 8 + COLUMN_IGNORED_STATUS = 9 + COLUMN_TOOLTIP_TEXT = 10 + COLUMN_CHANGE_TEXT = 11 + + def __init__(self, namespace_tree, launch_ns_func, + get_metadata_comments_func, + popup_menu_func, ask_can_show_func, ask_is_preview): + super(PageNavigationPanel, self).__init__() + self._launch_ns_func = launch_ns_func + self._get_metadata_comments_func = get_metadata_comments_func + self._popup_menu_func = popup_menu_func + self._ask_can_show_func = ask_can_show_func + self._ask_is_preview = ask_is_preview + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.set_shadow_type(Gtk.ShadowType.OUT) + self._rec_no_expand_leaves = re.compile( + rose.config_editor.TREE_PANEL_NO_EXPAND_LEAVES_REGEX) + self.panel_top = Gtk.TreeViewColumn() + self.panel_top.set_title(rose.config_editor.TREE_PANEL_TITLE) + self.cell_error_icon = Gtk.CellRendererPixbuf() + self.cell_changed_icon = Gtk.CellRendererPixbuf() + self.cell_title = Gtk.CellRendererText() + self.panel_top.pack_start(self.cell_error_icon, False, True, 0) + self.panel_top.pack_start(self.cell_changed_icon, False, True, 0) + self.panel_top.pack_start(self.cell_title, False, True, 0) + self.panel_top.add_attribute(self.cell_error_icon, + attribute='pixbuf', + column=self.COLUMN_ERROR_ICON) + self.panel_top.add_attribute(self.cell_changed_icon, + attribute='pixbuf', + column=self.COLUMN_CHANGE_ICON) + self.panel_top.set_cell_data_func(self.cell_title, + self._set_title_markup, + self.COLUMN_TITLE) + # The columns in self.data_store correspond to: error_icon, + # change_icon, title, name, error and change totals (4), + # latent and ignored statuses, main tip text, and change text. + self.data_store = Gtk.TreeStore(GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, + str, str, int, int, int, int, + bool, str, str, str) + resource_loc = rose.resource.ResourceLocator(paths=sys.path) + image_path = resource_loc.locate('etc/images/rose-config-edit') + self.null_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + + '/null_icon.xpm') + self.changed_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + + '/change_icon.xpm') + self.error_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + + '/error_icon.xpm') + self.tree = rose.gtk.util.TooltipTreeView( + get_tooltip_func=self.get_treeview_tooltip) + self.tree.append_column(self.panel_top) + self.filter_model = self.data_store.filter_new() + self.filter_model.set_visible_func(self._get_should_show) + self.tree.set_model(self.filter_model) + self.tree.show() + self.name_iter_map = {} + self.add(self.tree) + self.load_tree(None, namespace_tree) + self.tree.connect('button-press-event', + self.handle_activation) + self._last_tree_activation_path = None + self.tree.connect('row_activated', + self.handle_activation) + self.tree.connect_after('move-cursor', self._handle_cursor_change) + self.tree.connect('key-press-event', self.add_cursor_extra) + self.panel_top.set_clickable(True) + self.panel_top.connect('clicked', + lambda c: self.collapse_reset()) + self.show() + self.tree.columns_autosize() + self.tree.connect('enter-notify-event', + lambda t, e: self.update_row_tooltips()) + self.visible_iter_map = {} + + def get_treeview_tooltip(self, view, row_iter, col_index, tip): + """Handle creating a tooltip for the treeview.""" + tip.set_text(self.filter_model.get_value(row_iter, + self.COLUMN_TOOLTIP_TEXT)) + return True + + def add_cursor_extra(self, widget, event): + left = (event.keyval == Gdk.KEY_Left) + right = (event.keyval == Gdk.KEY_Right) + if left or right: + path = widget.get_cursor()[0] + if path is not None: + if right: + widget.expand_row(path, open_all=False) + elif left: + widget.collapse_row(path) + return False + + def _handle_cursor_change(self, *args): + current_path = self.tree.get_cursor()[0] + if current_path != self._last_tree_activation_path: + GObject.timeout_add(rose.config_editor.TREE_PANEL_KBD_TIMEOUT, + self._timeout_launch, current_path) + + def _timeout_launch(self, timeout_path): + current_path = self.tree.get_cursor()[0] + if (current_path == timeout_path and + self._last_tree_activation_path != timeout_path): + self._launch_ns_func(self.get_name(timeout_path), + as_new=False) + return False + + def load_tree(self, row, namespace_subtree): + expanded_rows = [] + self.tree.map_expanded_rows(lambda r, d: expanded_rows.append(d)) + self.load_tree_stack(row, namespace_subtree) + self.set_expansion() + for this_row in expanded_rows: + self.tree.expand_to_path(this_row) + + def set_expansion(self): + """Set the default expanded rows.""" + top_rows = self.filter_model.iter_n_children(None) + if top_rows > rose.config_editor.TREE_PANEL_MAX_EXPANDED_ROOTS: + return False + if top_rows == 1: + return self.expand_recursive(no_duplicates=True) + r_iter = self.filter_model.get_iter_first() + while r_iter is not None: + path = self.filter_model.get_path(r_iter) + self.tree.expand_to_path(path) + r_iter = self.filter_model.iter_next(r_iter) + + def load_tree_stack(self, row, namespace_subtree): + """Update the tree store recursively using namespace_subtree.""" + self.name_iter_map = {} + self.visible_iter_map = {} + if row is None: + self.data_store.clear() + initials = list(namespace_subtree.items()) + initials.sort(self.sort_tree_items) + stack = [] + if row is None: + start_keylist = [] + else: + path = self.data_store.get_path(row) + start_keylist = self.get_name(path, unfiltered=True).split("/") + for item in initials: + key, value_meta_tuple = item + stack.append([row] + [list(start_keylist)] + list(item)) + self.name_iter_map.setdefault(True, {}) # True maps to unfiltered. + name_iter_map = self.name_iter_map[True] + while stack: + row, keylist, key, value_meta_tuple = stack[0] + value, meta, statuses, change = value_meta_tuple + title = meta[rose.META_PROP_TITLE] + latent_status = statuses[rose.config_editor.SHOW_MODE_LATENT] + ignored_status = statuses[rose.config_editor.SHOW_MODE_IGNORED] + new_row = self.data_store.append(row, [self.null_icon, + self.null_icon, + title, + key, + 0, 0, 0, 0, + latent_status, + ignored_status, + '', + change]) + new_keylist = keylist + [key] + name_iter_map["/".join(new_keylist)] = new_row + if isinstance(value, dict): + newer_initials = list(value.items()) + newer_initials.sort(self.sort_tree_items) + for vals in newer_initials: + stack.append([new_row] + [list(new_keylist)] + list(vals)) + stack.pop(0) + + def _set_title_markup(self, column, cell, model, r_iter, index): + title = model.get_value(r_iter, index) + title = rose.gtk.util.safe_str(title) + if len(model.get_path(r_iter)) == 1: + title = rose.config_editor.TITLE_PAGE_ROOT_MARKUP.format(title) + latent_status = model.get_value(r_iter, self.COLUMN_LATENT_STATUS) + ignored_status = model.get_value(r_iter, self.COLUMN_IGNORED_STATUS) + name = self.get_name(model.get_path(r_iter)) + preview_status = self._ask_is_preview(name) + if preview_status: + title = rose.config_editor.TITLE_PAGE_PREVIEW_MARKUP.format(title) + if latent_status: + if self._get_is_latent_sub_tree(model, r_iter): + title = rose.config_editor.TITLE_PAGE_LATENT_MARKUP.format( + title) + if ignored_status: + title = rose.config_editor.TITLE_PAGE_IGNORED_MARKUP.format( + ignored_status, title) + cell.set_property("markup", title) + + def sort_tree_items(self, row_item_1, row_item_2): + """Sort tree items according to name and sort key.""" + sort_key_1 = row_item_1[1][1].get(rose.META_PROP_SORT_KEY, '~') + sort_key_2 = row_item_2[1][1].get(rose.META_PROP_SORT_KEY, '~') + var_id_1 = row_item_1[0] + var_id_2 = row_item_2[0] + + x_key = (sort_key_1, var_id_1) + y_key = (sort_key_2, var_id_2) + + return rose.config_editor.util.null_cmp(x_key, y_key) + + def set_row_icon(self, names, ind_count=0, ind_type='changed'): + """Set the icons for row status on or off. Check parent icons. + + After updating the row which is specified by a list of namespace + pieces (names), go up through the tree and update parent row icons + according to the status of their child row icons. + + """ + ind_map = {'changed': {'icon_col': self.COLUMN_CHANGE_ICON, + 'icon': self.changed_icon, + 'int_col': self.COLUMN_CHANGE_INTERNAL, + 'total_col': self.COLUMN_CHANGE_TOTAL}, + 'error': {'icon_col': self.COLUMN_ERROR_ICON, + 'icon': self.error_icon, + 'int_col': self.COLUMN_ERROR_INTERNAL, + 'total_col': self.COLUMN_ERROR_TOTAL}} + int_col = ind_map[ind_type]['int_col'] + total_col = ind_map[ind_type]['total_col'] + row_path = self.get_path_from_names(names, unfiltered=True) + if row_path is None: + return False + row_iter = self.data_store.get_iter(row_path) + old_total = self.data_store.get_value(row_iter, total_col) + old_int = self.data_store.get_value(row_iter, int_col) + diff_int_count = ind_count - old_int + if diff_int_count == 0: + # No change. + return False + new_total = old_total + diff_int_count + self.data_store.set_value(row_iter, int_col, ind_count) + self.data_store.set_value(row_iter, total_col, new_total) + if new_total > 0: + self.data_store.set_value(row_iter, ind_map[ind_type]['icon_col'], + ind_map[ind_type]['icon']) + else: + self.data_store.set_value(row_iter, ind_map[ind_type]['icon_col'], + self.null_icon) + + # Now pass information up the tree + for parent in [row_path[:i] for i in range(len(row_path) - 1, 0, -1)]: + parent_iter = self.data_store.get_iter(parent) + old_parent_total = self.data_store.get_value(parent_iter, + total_col) + new_parent_total = old_parent_total + diff_int_count + self.data_store.set_value(parent_iter, + total_col, + new_parent_total) + if new_parent_total > 0: + self.data_store.set_value(parent_iter, + ind_map[ind_type]['icon_col'], + ind_map[ind_type]['icon']) + else: + self.data_store.set_value(parent_iter, + ind_map[ind_type]['icon_col'], + self.null_icon) + + def update_row_tooltips(self): + """Synchronise the icon information with the hover-over text.""" + my_iter = self.data_store.get_iter_first() + if my_iter is None: + return + paths = [] + iter_stack = [my_iter] + while iter_stack: + my_iter = iter_stack.pop(0) + paths.append(self.data_store.get_path(my_iter)) + next_iter = self.data_store.iter_next(my_iter) + if next_iter is not None: + iter_stack.append(next_iter) + if self.data_store.iter_has_child(my_iter): + iter_stack.append(self.data_store.iter_children(my_iter)) + for path in paths: + path_iter = self.data_store.get_iter(path) + title = self.data_store.get_value(path_iter, self.COLUMN_TITLE) + name = self.data_store.get_value(path_iter, self.COLUMN_NAME) + num_errors = self.data_store.get_value(path_iter, + self.COLUMN_ERROR_INTERNAL) + mods = self.data_store.get_value(path_iter, + self.COLUMN_CHANGE_INTERNAL) + proper_name = self.get_name(path, unfiltered=True) + metadata, comment = self._get_metadata_comments_func(proper_name) + description = metadata.get(rose.META_PROP_DESCRIPTION, "") + change = self.data_store.get_value( + path_iter, self.COLUMN_CHANGE_TEXT) + text = title + if name != title: + text += " (" + name + ")" + if mods > 0: + text += " - " + rose.config_editor.TREE_PANEL_MODIFIED + if description: + text += ":\n" + description + if num_errors > 0: + if num_errors == 1: + text += rose.config_editor.TREE_PANEL_ERROR + else: + text += rose.config_editor.TREE_PANEL_ERRORS.format( + num_errors) + if comment: + text += "\n" + comment + if change: + text += "\n\n" + change + self.data_store.set_value( + path_iter, self.COLUMN_TOOLTIP_TEXT, text) + + def update_change(self, row_names, new_change): + """Update 'changed' text.""" + self._set_row_names_value( + row_names, self.COLUMN_CHANGE_TEXT, new_change) + + def update_statuses(self, row_names, latent_status, ignored_status): + """Update latent and ignored statuses.""" + self._set_row_names_value( + row_names, self.COLUMN_LATENT_STATUS, latent_status) + self._set_row_names_value( + row_names, self.COLUMN_IGNORED_STATUS, ignored_status) + + def _set_row_names_value(self, row_names, index, value): + path = self.get_path_from_names(row_names, unfiltered=True) + if path is not None: + row_iter = self.data_store.get_iter(path) + self.data_store.set_value(row_iter, index, value) + + def select_row(self, row_names): + """Highlight one particular row, but only this one.""" + if row_names is None: + return + path = self.get_path_from_names(row_names, unfiltered=True) + try: + path = self.filter_model.convert_child_path_to_path(path) + except TypeError: + path = None + if path is None: + dest_path = (0,) + else: + i = 1 + while self.tree.row_expanded(path[:i]) and i <= len(path): + i += 1 + dest_path = path[:i] + cursor_path = self.tree.get_cursor()[0] + if cursor_path != dest_path: + self.tree.set_cursor(dest_path) + + def get_path_from_names(self, row_names, unfiltered=False): + """Return a row path corresponding to the list of branch names.""" + if unfiltered: + tree_model = self.data_store + else: + tree_model = self.filter_model + self.name_iter_map.setdefault(unfiltered, {}) + name_iter_map = self.name_iter_map[unfiltered] + key = "/".join(row_names) + if key in name_iter_map: + return tree_model.get_path(name_iter_map[key]) + if unfiltered: + # This would be cached in name_iter_map by load_tree_stack. + return None + my_iter = tree_model.get_iter_first() + these_names = [] + good_paths = [row_names[:i] for i in range(len(row_names) + 1)] + for names in reversed(good_paths): + subkey = "/".join(names) + if subkey in name_iter_map: + my_iter = name_iter_map[subkey] + these_names = names[:-1] + break + while my_iter is not None: + branch_name = tree_model.get_value(my_iter, self.COLUMN_NAME) + my_names = these_names + [branch_name] + subkey = "/".join(my_names) + name_iter_map[subkey] = my_iter + if my_names in good_paths: + if my_names == row_names: + return tree_model.get_path(my_iter) + else: + these_names.append(branch_name) + my_iter = tree_model.iter_children(my_iter) + else: + my_iter = tree_model.iter_next(my_iter) + return None + + def get_change_error_totals(self, config_name=None): + """Return the number of changes and total errors for the root nodes.""" + if config_name: + path = self.get_path_from_names([config_name], unfiltered=True) + iter_ = self.data_store.get_iter(path) + else: + iter_ = self.data_store.get_iter_first() + changes = 0 + errors = 0 + while iter_ is not None: + iter_changes = self.data_store.get_value( + iter_, self.COLUMN_CHANGE_TOTAL) + iter_errors = self.data_store.get_value( + iter_, self.COLUMN_ERROR_TOTAL) + if iter_changes is not None: + changes += iter_changes + if iter_errors is not None: + errors += iter_errors + if config_name: + break + else: + iter_ = self.data_store.iter_next(iter_) + return changes, errors + + def handle_activation(self, treeview=None, event=None, somewidget=None): + """Send a page launch request based on left or middle clicks.""" + if event is not None and treeview is not None: + if hasattr(event, 'button'): + pathinfo = treeview.get_path_at_pos(int(event.x), + int(event.y)) + if pathinfo is not None: + path, col, cell_x, _ = pathinfo + if (treeview.get_expander_column() == col and + cell_x < 1 + 18 * len(path)): # Hardwired, bad. + if event.button != 3: + return False + else: + return self.expand_recursive(start_path=path, + no_duplicates=True) + if event.button == 3: + self.popup_menu(path, event) + else: + treeview.grab_focus() + treeview.set_cursor(pathinfo[0], col, 0) + elif event.button == 3: # Right clicked outside the rows + self.popup_menu(None, event) + else: # Clicked outside the rows + return False + if event.button == 1: # Left click event, replace old tab + self._last_tree_activation_path = path + self._launch_ns_func(self.get_name(path), as_new=False) + elif event.button == 2: # Middle click event, make new tab + self._last_tree_activation_path = path + self._launch_ns_func(self.get_name(path), as_new=True) + else: + path = event + self._launch_ns_func(self.get_name(path), as_new=False) + return False + + def get_name(self, path=None, unfiltered=False): + """Return the row name (text) corresponding to the treeview path.""" + if path is None: + tree_selection = self.tree.get_selection() + (tree_model, tree_iter) = tree_selection.get_selected() + path = tree_model.get_path(tree_iter) + else: + tree_model = self.tree.get_model() + if unfiltered: + tree_model = tree_model.get_model() + tree_iter = tree_model.get_iter(path) + row_name = str(tree_model.get_value(tree_iter, self.COLUMN_NAME)) + full_name = row_name + for parent in [path[:i] for i in range(len(path) - 1, 0, -1)]: + parent_iter = tree_model.get_iter(parent) + full_name = str( + tree_model.get_value(parent_iter, self.COLUMN_NAME) + + "/" + full_name) + return full_name + + def get_subtree_names(self, path=None): + """Return all names that exist in a subtree of path.""" + tree_model = self.tree.get_model() + root_iter = tree_model.get_iter(path) + sub_iters = [] + for i in range(tree_model.iter_n_children(root_iter)): + sub_iters.append(tree_model.iter_nth_child(root_iter, i)) + sub_names = [] + while sub_iters: + if sub_iters[0] is not None: + path = tree_model.get_path(sub_iters[0]) + sub_names.append(self.get_name(path)) + for i in range(tree_model.iter_n_children(sub_iters[0])): + sub_iters.append(tree_model.iter_nth_child(sub_iters[0], + i)) + sub_iters.pop(0) + return sub_names + + def popup_menu(self, path, event): + """Launch a popup menu for add/clone/remove.""" + if path: + path_name = "/" + self.get_name(path) + else: + path_name = None + return self._popup_menu_func(path_name, event) + + def collapse_reset(self): + """Return the tree view to the basic startup state.""" + self.tree.collapse_all() + self.set_expansion() + self.tree.grab_focus() + return False + + def expand_recursive(self, start_path=None, no_duplicates=False): + """Expand the tree starting at start_path.""" + treemodel = self.tree.get_model() + if start_path is None: + start_iter = treemodel.get_iter_first() + start_path = treemodel.get_path(start_iter) + if not no_duplicates: + return self.tree.expand_row(start_path, open_all=True) + max_depth = rose.config_editor.TREE_PANEL_MAX_EXPANDED_DEPTH + stack = [treemodel.get_iter(start_path)] + while stack: + iter_ = stack.pop(0) + if iter_ is None: + continue + path = treemodel.get_path(iter_) + name = self.get_name(path) + child_iter = treemodel.iter_children(iter_) + child_dups = [] + while child_iter is not None: + child_name = self.get_name(treemodel.get_path(child_iter)) + metadata = self._get_metadata_comments_func(child_name)[0] + dupl = metadata.get(rose.META_PROP_DUPLICATE) + child_dups.append(dupl == rose.META_PROP_VALUE_TRUE) + child_iter = treemodel.iter_next(child_iter) + if path != start_path: + stack.append(treemodel.iter_next(iter_)) + if (not all(child_dups) and + len(path) <= max_depth and + not self._rec_no_expand_leaves.search(name)): + self.tree.expand_row(path, open_all=False) + stack.append(treemodel.iter_children(iter_)) + + def _get_is_latent_sub_tree(self, model, iter_): + """Return True if the whole model sub tree is latent.""" + if not model.get_value(iter_, self.COLUMN_LATENT_STATUS): + # This row is not latent. + return False + iter_stack = [model.iter_children(iter_)] + while iter_stack: + iter_ = iter_stack.pop(0) + if iter_ is None: + continue + if not model.get_value(iter_, self.COLUMN_LATENT_STATUS): + # This sub-row is not latent. + return False + iter_stack.append(model.iter_children(iter_)) + iter_stack.append(model.iter_next(iter_)) + return True + + def _get_should_show(self, model, iter_): + # Determine whether to show a row. + latent_status = model.get_value(iter_, self.COLUMN_LATENT_STATUS) + ignored_status = model.get_value(iter_, self.COLUMN_IGNORED_STATUS) + has_error = bool(model.get_value(iter_, self.COLUMN_ERROR_INTERNAL)) + child_iter = model.iter_children(iter_) + is_visible = self._ask_can_show_func(latent_status, ignored_status, + has_error) + if is_visible: + return True + while child_iter is not None: + if self._get_should_show(model, child_iter): + return True + child_iter = model.iter_next(child_iter) + return False diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py new file mode 100644 index 000000000..d0a08229a --- /dev/null +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -0,0 +1,532 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import os +import time +import webbrowser + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.config_editor.util +import rose.gtk.dialog + + +class NavPanelHandler(object): + + """Handles the navigation panel menu.""" + + def __init__(self, data, util, reporter, mainwindow, + undo_stack, redo_stack, add_config_func, + group_ops_inst, section_ops_inst, variable_ops_inst, + kill_page_func, reload_ns_tree_func, transform_default_func, + graph_ns_func): + self.data = data + self.util = util + self.reporter = reporter + self.mainwindow = mainwindow + self.undo_stack = undo_stack + self.redo_stack = redo_stack + self.group_ops = group_ops_inst + self.sect_ops = section_ops_inst + self.var_ops = variable_ops_inst + self._add_config = add_config_func + self.kill_page_func = kill_page_func + self.reload_ns_tree_func = reload_ns_tree_func + self._transform_default_func = transform_default_func + self._graph_ns_func = graph_ns_func + + def add_dialog(self, base_ns): + """Handle an add section dialog and request.""" + if base_ns is not None and '/' in base_ns: + config_name, subsp = self.util.split_full_ns(self.data, base_ns) + config_data = self.data.config[config_name] + if config_name == base_ns: + help_str = '' + else: + sections = self.data.helper.get_sections_from_namespace( + base_ns) + if sections == []: + help_str = subsp.replace('/', ':') + else: + help_str = sections[0] + help_str = help_str.split(':', 1)[0] + for config_section in (list(config_data.sections.now.keys()) + + list(config_data.sections.latent.keys())): + if config_section.startswith(help_str + ":"): + help_str = help_str + ":" + else: + help_str = None + config_name = None + choices_help = self.data.helper.get_missing_sections(config_name) + + config_names = [ + n for n in self.data.config if not self.ask_is_preview(n)] + config_names.sort(lambda x, y: (y == config_name) - (x == config_name)) + config_name, section = self.mainwindow.launch_add_dialog( + config_names, choices_help, help_str) + if config_name in self.data.config and section is not None: + self.sect_ops.add_section(config_name, section, page_launch=True) + + def ask_is_preview(self, base_ns): + namespace = "/" + base_ns.lstrip("/") + try: + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + return config_data.is_preview + except KeyError: + print(config_name) + return False + + def copy_request(self, base_ns, new_section=None, skip_update=False): + """Handle a copy request for a section and its options.""" + namespace = "/" + base_ns.lstrip("/") + sections = self.data.helper.get_sections_from_namespace(namespace) + if len(sections) != 1: + return False + section = sections.pop() + config_name = self.util.split_full_ns(self.data, namespace)[0] + return self.group_ops.copy_section(config_name, section, + skip_update=skip_update) + + def create_request(self): + """Handle a create configuration request.""" + if not any(v.config_type == rose.TOP_CONFIG_NAME + for v in list(self.data.config.values())): + text = rose.config_editor.WARNING_APP_CONFIG_CREATE + title = rose.config_editor.WARNING_APP_CONFIG_CREATE_TITLE + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + return False + # Need an application configuration to be created. + root = os.path.join(self.data.top_level_directory, + rose.SUB_CONFIGS_DIR) + name, meta = self.mainwindow.launch_new_config_dialog(root) + if name is None: + return False + config_name = "/" + name + self._add_config(config_name, meta) + + def ignore_request(self, base_ns, is_ignored): + """Handle an ignore or enable section request.""" + config_names = list(self.data.config.keys()) + if base_ns is not None and '/' in base_ns: + config_name = self.util.split_full_ns(self.data, base_ns)[0] + prefer_name_sections = { + config_name: + self.data.helper.get_sections_from_namespace(base_ns)} + else: + prefer_name_sections = {} + config_sect_dict = {} + for config_name in config_names: + config_data = self.data.config[config_name] + config_sect_dict[config_name] = [] + sect_and_data = list(config_data.sections.now.items()) + for v_sect in config_data.vars.now: + sect_data = config_data.sections.now[v_sect] + sect_and_data.append((v_sect, sect_data)) + for section, sect_data in sect_and_data: + if section not in config_sect_dict[config_name]: + if sect_data.ignored_reason: + if is_ignored: + continue + if not is_ignored: + mode = sect_data.metadata.get( + rose.META_PROP_COMPULSORY) + if (not sect_data.ignored_reason or + mode == rose.META_PROP_VALUE_TRUE): + continue + config_sect_dict[config_name].append(section) + config_sect_dict[config_name].sort(rose.config.sort_settings) + if config_name in prefer_name_sections: + prefer_name_sections[config_name].sort( + rose.config.sort_settings) + config_name, section = self.mainwindow.launch_ignore_dialog( + config_sect_dict, prefer_name_sections, is_ignored) + if config_name in self.data.config and section is not None: + self.sect_ops.ignore_section(config_name, section, is_ignored) + + def edit_request(self, base_ns): + """Handle a request for editing section comments.""" + if base_ns is None: + return False + base_ns = "/" + base_ns.lstrip("/") + config_name = self.util.split_full_ns(self.data, base_ns)[0] + config_data = self.data.config[config_name] + sections = self.data.helper.get_sections_from_namespace(base_ns) + for section in list(sections): + if section not in config_data.sections.now: + sections.remove(section) + if not sections: + return False + if len(sections) > 1: + section = rose.gtk.dialog.run_choices_dialog( + rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_EDIT, + sections, + rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + else: + section = sections[0] + if section is None: + return False + title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format(section) + text = "\n".join(config_data.sections.now[section].comments) + finish = lambda t: self.sect_ops.set_section_comments( + config_name, section, t.splitlines()) + rose.gtk.dialog.run_edit_dialog(text, finish_hook=finish, title=title) + + def fix_request(self, base_ns): + """Handle a request to auto-fix a configuration.""" + if base_ns is None: + return False + base_ns = "/" + base_ns.lstrip("/") + config_name = self.util.split_full_ns(self.data, base_ns)[0] + self._transform_default_func(only_this_config=config_name) + + def get_ns_metadata_and_comments(self, namespace): + """Return metadata dict and comments list.""" + namespace = "/" + namespace.lstrip("/") + metadata = {} + comments = "" + if namespace is None: + return metadata, comments + metadata = self.data.namespace_meta_lookup.get(namespace, {}) + comments = self.data.helper.get_ns_comment_string(namespace) + return metadata, comments + + def info_request(self, namespace): + """Handle a request for namespace info.""" + if namespace is None: + return False + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + sections = self.data.helper.get_sections_from_namespace(namespace) + search_function = lambda i: self.search_request(namespace, i) + for section in sections: + sect_data = config_data.sections.now.get(section) + if sect_data is not None: + rose.config_editor.util.launch_node_info_dialog( + sect_data, "", search_function) + + def graph_request(self, namespace): + """Handle a graph request for namespace info.""" + self._graph_ns_func(namespace) + + def remove_request(self, base_ns): + """Handle a delete section request.""" + config_names = list(self.data.config.keys()) + if base_ns is not None and '/' in base_ns: + config_name = self.util.split_full_ns(self.data, base_ns)[0] + prefer_name_sections = { + config_name: + self.data.helper.get_sections_from_namespace(base_ns)} + else: + prefer_name_sections = {} + config_sect_dict = {} + for config_name in config_names: + config_data = self.data.config[config_name] + config_sect_dict[config_name] = list(config_data.sections.now.keys()) + config_sect_dict[config_name].sort(rose.config.sort_settings) + if config_name in prefer_name_sections: + prefer_name_sections[config_name].sort( + rose.config.sort_settings) + config_name, section = self.mainwindow.launch_remove_dialog( + config_sect_dict, prefer_name_sections) + if config_name in self.data.config and section is not None: + start_stack_index = len(self.undo_stack) + group = ( + rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time())) + config_data = self.data.config[config_name] + variable_sorter = lambda v, w: rose.config.sort_settings( + v.metadata['id'], w.metadata['id']) + variables = list(config_data.vars.now.get(section, [])) + variables.sort(variable_sorter) + variables.reverse() + for variable in variables: + self.var_ops.remove_var(variable) + self.sect_ops.remove_section(config_name, section) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + + def rename_dialog(self, base_ns): + """Handle a rename section dialog and request.""" + if base_ns is not None and '/' in base_ns: + config_name = self.util.split_full_ns(self.data, base_ns)[0] + prefer_name_sections = { + config_name: + self.data.helper.get_sections_from_namespace(base_ns)} + else: + prefer_name_sections = {} + config_sect_dict = {} + for config_name in self.data.config: + config_data = self.data.config[config_name] + config_sect_dict[config_name] = list(config_data.sections.now.keys()) + config_sect_dict[config_name].sort(rose.config.sort_settings) + if config_name in prefer_name_sections: + prefer_name_sections[config_name].sort( + rose.config.sort_settings) + config_name, source_section, target_section = ( + self.mainwindow.launch_rename_dialog( + config_sect_dict, prefer_name_sections) + ) + if (config_name in self.data.config and + source_section is not None and target_section): + self.group_ops.rename_section( + config_name, source_section, target_section) + + def search_request(self, namespace, setting_id): + """Handle a search for an id (hyperlink).""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + self.var_ops.search_for_var(config_name, setting_id) + + def popup_panel_menu(self, base_ns, event): + """Popup a page menu on the navigation panel.""" + if base_ns is None: + namespace = None + else: + namespace = "/" + base_ns.lstrip("/") + + ui_config_string = """ """ + actions = [('New', Gtk.STOCK_NEW, + rose.config_editor.TREE_PANEL_NEW_CONFIG), + ('Add', Gtk.STOCK_ADD, + rose.config_editor.TREE_PANEL_ADD_GENERIC), + ('Autofix', Gtk.STOCK_CONVERT, + rose.config_editor.TREE_PANEL_AUTOFIX_CONFIG), + ('Clone', Gtk.STOCK_COPY, + rose.config_editor.TREE_PANEL_CLONE_SECTION), + ('Edit', Gtk.STOCK_EDIT, + rose.config_editor.TREE_PANEL_EDIT_SECTION), + ('Enable', Gtk.STOCK_YES, + rose.config_editor.TREE_PANEL_ENABLE_GENERIC), + ('Graph', Gtk.STOCK_SORT_ASCENDING, + rose.config_editor.TREE_PANEL_GRAPH_SECTION), + ('Ignore', Gtk.STOCK_NO, + rose.config_editor.TREE_PANEL_IGNORE_GENERIC), + ('Info', Gtk.STOCK_INFO, + rose.config_editor.TREE_PANEL_INFO_SECTION), + ('Help', Gtk.STOCK_HELP, + rose.config_editor.TREE_PANEL_HELP_SECTION), + ('URL', Gtk.STOCK_HOME, + rose.config_editor.TREE_PANEL_URL_SECTION), + ('Remove', Gtk.STOCK_DELETE, + rose.config_editor.TREE_PANEL_REMOVE_GENERIC), + ('Rename', Gtk.STOCK_COPY, + rose.config_editor.TREE_PANEL_RENAME_GENERIC)] + url = None + help_ = None + is_empty = (not self.data.config) + if namespace is not None: + config_name = self.util.split_full_ns(self.data, namespace)[0] + if self.data.config[config_name].is_preview: + return False + cloneable = self.is_ns_duplicate(namespace) + is_top = (namespace in list(self.data.config.keys())) + is_fixable = bool(self.get_ns_errors(namespace)) + has_content = self.data.helper.is_ns_content(namespace) + is_unsaved = self.data.helper.get_config_has_unsaved_changes( + config_name) + is_latent = self.data.helper.get_ns_latent_status(namespace) + latent_sections = self.data.helper.get_latent_sections(namespace) + metadata = self.get_ns_metadata_and_comments(namespace)[0] + if is_latent: + for i, section in enumerate(latent_sections): + action_name = "Add {0}".format(i) + ui_config_string += ''.format( + action_name) + actions.append( + (action_name, Gtk.STOCK_ADD, + rose.config_editor.TREE_PANEL_ADD_SECTION.format( + section.replace("_", "__"))) + ) + ui_config_string += '' + ui_config_string += '' + if cloneable: + ui_config_string += '' + ui_config_string += '' + if not is_empty: + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + if has_content: + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + url = metadata.get(rose.META_PROP_URL) + help_ = metadata.get(rose.META_PROP_HELP) + if url is not None or help_ is not None: + ui_config_string += '' + if url is not None: + ui_config_string += '' + if help_ is not None: + ui_config_string += '' + if not is_empty: + ui_config_string += """""" + ui_config_string += """""" + if is_fixable: + ui_config_string += """ + """ + else: + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + ui_config_string += '' + if namespace is None or (is_top or is_empty): + ui_config_string += """ + """ + ui_config_string += """ """ + uimanager = Gtk.UIManager() + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.add_actions(actions) + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(ui_config_string) + if namespace is None or (is_top or is_empty): + new_item = uimanager.get_widget('/Popup/New') + new_item.connect("activate", lambda b: self.create_request()) + new_item.set_sensitive(not is_empty) + add_item = uimanager.get_widget('/Popup/Add') + add_item.connect("activate", lambda b: self.add_dialog(namespace)) + add_item.set_sensitive(not is_empty) + enable_item = uimanager.get_widget('/Popup/Enable') + enable_item.connect( + "activate", lambda b: self.ignore_request(namespace, False)) + enable_item.set_sensitive(not is_empty) + ignore_item = uimanager.get_widget('/Popup/Ignore') + ignore_item.connect( + "activate", lambda b: self.ignore_request(namespace, True)) + ignore_item.set_sensitive(not is_empty) + if namespace is not None: + if is_latent: + for i, section in enumerate(latent_sections): + action_name = "Add {0}".format(i) + add_item = uimanager.get_widget("/Popup/" + action_name) + add_item._section = section + add_item.connect( + "activate", + lambda b: self.sect_ops.add_section( + config_name, b._section)) + if cloneable: + clone_item = uimanager.get_widget('/Popup/Clone') + clone_item.connect("activate", + lambda b: self.copy_request(namespace)) + if has_content: + edit_item = uimanager.get_widget('/Popup/Edit') + edit_item.connect("activate", + lambda b: self.edit_request(namespace)) + info_item = uimanager.get_widget('/Popup/Info') + info_item.connect("activate", + lambda b: self.info_request(namespace)) + graph_item = uimanager.get_widget("/Popup/Graph") + graph_item.connect("activate", + lambda b: self.graph_request(namespace)) + if is_unsaved: + graph_item.set_sensitive(False) + if help_ is not None: + help_item = uimanager.get_widget('/Popup/Help') + help_title = namespace.split('/')[1:] + help_title = rose.config_editor.DIALOG_HELP_TITLE.format( + help_title) + search_function = lambda i: self.search_request(namespace, i) + help_item.connect( + "activate", + lambda b: rose.gtk.dialog.run_hyperlink_dialog( + Gtk.STOCK_DIALOG_INFO, help_, help_title, + search_function)) + if url is not None: + url_item = uimanager.get_widget('/Popup/URL') + url_item.connect( + "activate", lambda b: webbrowser.open(url)) + if is_fixable: + autofix_item = uimanager.get_widget('/Popup/Autofix') + autofix_item.connect("activate", + lambda b: self.fix_request(namespace)) + remove_section_item = uimanager.get_widget('/Popup/Remove') + remove_section_item.connect( + "activate", lambda b: self.remove_request(namespace)) + rename_section_item = uimanager.get_widget('/Popup/Rename') + rename_section_item.connect( + "activate", lambda b: self.rename_dialog(namespace)) + menu = uimanager.get_widget('/Popup') + menu.popup(None, None, None, event.button, event.time) + return False + + def is_ns_duplicate(self, namespace): + """Lookup whether a page can be cloned, via the metadata.""" + sections = self.data.helper.get_sections_from_namespace(namespace) + if len(sections) != 1: + return False + section = sections.pop() + config_name = self.util.split_full_ns(self.data, namespace)[0] + sect_data = self.data.config[config_name].sections.now.get(section) + if sect_data is None: + return False + return (sect_data.metadata.get(rose.META_PROP_DUPLICATE) == + rose.META_PROP_VALUE_TRUE) + + def get_ns_errors(self, namespace): + """Count the number of errors in a namespace.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + sections = self.data.helper.get_sections_from_namespace(namespace) + errors = 0 + for section in sections: + errors += len(config_data.sections.get_sect(section).error) + real_data, latent_data = self.data.helper.get_data_for_namespace( + namespace) + errors += sum([len(v.error) for v in real_data + latent_data]) + return errors + + def get_ns_ignored(self, base_ns): + """Lookup the ignored status of a namespace's data.""" + namespace = "/" + base_ns.lstrip("/") + return self.data.helper.get_ns_ignored_status(namespace) + + def get_can_show_page(self, latent_status, ignored_status, has_error): + """Lookup whether to display a page based on the data status.""" + if has_error or (not ignored_status and not latent_status): + # Always show this. + return True + show_ignored = self.data.page_ns_show_modes[ + rose.config_editor.SHOW_MODE_IGNORED] + show_user_ignored = self.data.page_ns_show_modes[ + rose.config_editor.SHOW_MODE_USER_IGNORED] + show_latent = self.data.page_ns_show_modes[ + rose.config_editor.SHOW_MODE_LATENT] + if latent_status: + if not show_latent: + # Latent page, no latent pages allowed. + return False + # Latent page, latent pages allowed (but may be ignored...). + if ignored_status: + if ignored_status == rose.config.ConfigNode.STATE_USER_IGNORED: + if show_ignored or show_user_ignored: + # This is an allowed user-ignored page. + return True + # This is a user-ignored page that isn't allowed. + return False + # This is a trigger-ignored page that may be allowed. + return show_ignored + # This is a latent page that isn't ignored, latent pages allowed. + return True diff --git a/metomi/rose/config_editor/ops/__init__.py b/metomi/rose/config_editor/ops/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/metomi/rose/config_editor/ops/group.py b/metomi/rose/config_editor/ops/group.py new file mode 100644 index 000000000..f1c1e4920 --- /dev/null +++ b/metomi/rose/config_editor/ops/group.py @@ -0,0 +1,468 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This module handles grouped operations. + +These are supersets of section and variable operations, such as adding +a section together with some variables, mass removal of sections, +copying sections with their variables, and so on. + +""" + +import copy +import re +import time + +import rose.config +import rose.config_editor + + +class GroupOperations(object): + + """Class to perform actions on groups of sections and/or options.""" + + def __init__(self, data, util, reporter, undo_stack, redo_stack, + section_ops_inst, + variable_ops_inst, + view_page_func, update_ns_sub_data_func, + reload_ns_tree_func): + self.data = data + self.util = util + self.reporter = reporter + self.undo_stack = undo_stack + self.redo_stack = redo_stack + self.sect_ops = section_ops_inst + self.var_ops = variable_ops_inst + self.view_page_func = view_page_func + self.update_ns_sub_data_func = update_ns_sub_data_func + self.reload_ns_tree_func = reload_ns_tree_func + + def apply_diff(self, config_name, config_diff, origin_name=None, + triggers_ok=False, is_reversed=False): + """Apply a rose.config.ConfigNodeDiff object to the config.""" + state_reason_dict = { + rose.config.ConfigNode.STATE_NORMAL: {}, + rose.config.ConfigNode.STATE_USER_IGNORED: { + rose.variable.IGNORED_BY_USER: + rose.config_editor.IGNORED_STATUS_MACRO + }, + rose.config.ConfigNode.STATE_SYST_IGNORED: { + rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_MACRO + } + } + nses = [] + ids = [] + # Handle added sections. + for keys, data in sorted(config_diff.get_added(), + key=lambda _: len(_[0])): + value, state, comments = data + reason = state_reason_dict[state] + if len(keys) == 1: + # Section. + sect = keys[0] + ids.append(sect) + nses.append( + self.sect_ops.add_section(config_name, sect, + comments=comments, + ignored_reason=reason, + skip_update=True, + skip_undo=True) + ) + else: + sect, opt = keys + var_id = self.util.get_id_from_section_option(sect, opt) + ids.append(var_id) + metadata = self.data.helper.get_metadata_for_config_id( + var_id, config_name) + variable = rose.variable.Variable(opt, value, metadata) + variable.comments = copy.deepcopy(comments) + variable.ignored_reason = copy.deepcopy(reason) + self.data.load_ns_for_node(variable, config_name) + nses.append( + self.var_ops.add_var(variable, skip_update=True, + skip_undo=True) + ) + + # Handle modified settings. + sections = self.data.config[config_name].sections + for keys, data in config_diff.get_modified(): + old_value, old_state, old_comments = data[0] + value, state, comments = data[1] + comments = copy.deepcopy(comments) + old_reason = state_reason_dict[old_state] + reason = copy.deepcopy(state_reason_dict[state]) + sect = keys[0] + sect_data = sections.now[sect] + opt = None + var = None + if len(keys) > 1: + opt = keys[1] + var_id = self.util.get_id_from_section_option(sect, opt) + ids.append(var_id) + var = self.data.helper.get_variable_by_id(var_id, config_name) + else: + ids.append(sect) + if comments != old_comments: + # Change the comments. + if opt is None: + # Section. + nses.append( + self.sect_ops.set_section_comments(config_name, sect, + comments, + skip_update=True, + skip_undo=True) + ) + else: + nses.append( + self.var_ops.set_var_comments( + variable, comments, skip_undo=True, + skip_update=True) + ) + + if opt is not None and value != old_value: + # Change the value (has to be a variable). + nses.append( + self.var_ops.set_var_value( + var, value, skip_undo=True, skip_update=True) + ) + + if opt is None: + ignored_changed = True + is_ignored = False + if (rose.variable.IGNORED_BY_USER in old_reason and + rose.variable.IGNORED_BY_USER not in reason): + # Enable from user-ignored. + is_ignored = False + elif (rose.variable.IGNORED_BY_USER not in old_reason and + rose.variable.IGNORED_BY_USER in reason): + # User-ignore from enabled. + is_ignored = True + elif (triggers_ok and + rose.variable.IGNORED_BY_SYSTEM not in old_reason and + rose.variable.IGNORED_BY_SYSTEM in reason): + # Trigger-ignore. + sect_data.error.setdefault( + rose.config_editor.WARNING_TYPE_ENABLED, + rose.config_editor.IGNORED_STATUS_MACRO) + is_ignored = True + elif (triggers_ok and + rose.variable.IGNORED_BY_SYSTEM in old_reason and + rose.variable.IGNORED_BY_SYSTEM not in reason): + # Enabled from trigger-ignore. + sect_data.error.setdefault( + rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED, + rose.config_editor.IGNORED_STATUS_MACRO) + is_ignored = False + else: + ignored_changed = False + if ignored_changed: + ignore_nses, ignore_ids = ( + self.sect_ops.ignore_section(config_name, sect, + is_ignored, + override=True, + skip_update=True, + skip_undo=True) + ) + nses.extend(ignore_nses) + ids.extend(ignore_ids) + elif set(reason) != set(old_reason): + nses.append( + self.var_ops.set_var_ignored(var, new_reason_dict=reason, + override=True, skip_undo=True, + skip_update=True) + ) + + for keys, data in sorted(config_diff.get_removed(), + key=lambda _: -len(_[0])): + # Sort so that variables are removed first. + sect = keys[0] + if len(keys) == 1: + ids.append(sect) + nses.extend( + self.sect_ops.remove_section(config_name, sect, + skip_update=True, + skip_undo=True)) + else: + sect = keys[0] + opt = keys[1] + var_id = self.util.get_id_from_section_option(sect, opt) + ids.append(var_id) + var = self.data.helper.get_variable_by_id(var_id, config_name) + nses.append( + self.var_ops.remove_var( + var, skip_update=True, skip_undo=True) + ) + reverse_diff = config_diff.get_reversed() + if is_reversed: + action = rose.config_editor.STACK_ACTION_REVERSED + else: + action = rose.config_editor.STACK_ACTION_APPLIED + stack_item = rose.config_editor.stack.StackItem( + None, + action, + reverse_diff, + self.apply_diff, + (config_name, reverse_diff, origin_name, triggers_ok, + not is_reversed), + custom_name=origin_name + ) + self.undo_stack.append(stack_item) + del self.redo_stack[:] + self.reload_ns_tree_func() + for ns in set(nses): + if ns is None: + # Invalid or zero-change operation, e.g. by a corrupt macro. + continue + self.sect_ops.trigger_update(ns, skip_sub_data_update=True) + self.sect_ops.trigger_info_update(ns) + self.sect_ops.trigger_update_sub_data() + self.sect_ops.trigger_update(config_name) + return ids + + def add_section_with_options(self, config_name, new_section_name, + opt_map=None): + """Add a section and any compulsory options. + + Any option-value pairs in the opt_map dict will also be added. + + """ + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time()) + self.sect_ops.add_section(config_name, new_section_name, + skip_update=True) + namespace = self.data.helper.get_default_section_namespace( + new_section_name, config_name) + config_data = self.data.config[config_name] + if opt_map is None: + opt_map = {} + for var in list(config_data.vars.latent.get(new_section_name, [])): + if var.name in opt_map: + var.value = opt_map.pop(var.name) + if (var.name in opt_map or + (var.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE)): + self.var_ops.add_var(var, skip_update=True) + for opt_name, value in list(opt_map.items()): + var_id = self.util.get_id_from_section_option( + new_section_name, opt_name) + metadata = self.data.helper.get_metadata_for_config_id( + var_id, config_name) + metadata['full_ns'] = namespace + flags = self.data.load_option_flags(config_name, + new_section_name, opt_name) + ignored_reason = {} # This may not be safe. + var = rose.variable.Variable(opt_name, value, + metadata, ignored_reason, + error={}, + flags=flags) + self.var_ops.add_var(var, skip_update=True) + self.reload_ns_tree_func(namespace) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + return new_section_name + + def copy_section(self, config_name, section, new_section=None, + skip_update=False): + """Copy a section and its options.""" + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_COPY + "-" + str(time.time()) + config_data = self.data.config[config_name] + section_base = re.sub(r'(.*)\(\w+\)$', r"\1", section) + existing_sections = [] + clone_vars = [] + existing_sections = list(config_data.vars.now.keys()) + existing_sections.extend(list(config_data.sections.now.keys())) + for variable in config_data.vars.now.get(section, []): + clone_vars.append(variable.copy()) + if new_section is None: + i = 1 + new_section = section_base + "(" + str(i) + ")" + while new_section in existing_sections: + i += 1 + new_section = section_base + "(" + str(i) + ")" + new_namespace = self.sect_ops.add_section(config_name, new_section, + skip_update=skip_update) + if new_namespace is None: + # Add failed (section already exists). + return + for var in clone_vars: + var_id = self.util.get_id_from_section_option( + new_section, var.name) + metadata = self.data.helper.get_metadata_for_config_id( + var_id, config_name) + var.process_metadata(metadata) + var.metadata['full_ns'] = new_namespace + sorter = rose.config.sort_settings + clone_vars.sort(lambda v, w: sorter(v.name, w.name)) + if skip_update: + for var in clone_vars: + self.var_ops.add_var(var, skip_update=skip_update) + else: + page = self.view_page_func(new_namespace) + for var in clone_vars: + page.add_row(var) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + return new_section + + def ignore_sections(self, config_name, sections, is_ignored, + skip_update=False, skip_sub_data_update=True): + """Implement a mass user-ignore or enable of sections.""" + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_IGNORE + "-" + str(time.time()) + nses = [] + for section in sections: + ns = self.data.helper.get_default_section_namespace( + section, config_name) + if ns not in nses: + nses.append(ns) + skipped_nses = self.sect_ops.ignore_section( + config_name, section, is_ignored, skip_update=True)[0] + for ns in skipped_nses: + if ns not in nses: + nses.append(ns) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + if not skip_update: + for ns in nses: + self.sect_ops.trigger_update( + ns, skip_sub_data_update=skip_sub_data_update) + self.sect_ops.trigger_info_update(ns) + self.sect_ops.trigger_update(config_name) + self.update_ns_sub_data_func(config_name) + + def remove_section(self, config_name, section, skip_update=False): + """Implement a remove of a section and its options.""" + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + config_data = self.data.config[config_name] + variables = config_data.vars.now.get(section, []) + for variable in list(variables): + self.var_ops.remove_var(variable, skip_update=True) + self.sect_ops.remove_section(config_name, section, + skip_update=skip_update) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + + def rename_section(self, config_name, section, target_section, + skip_update=False): + """Implement a rename of a section and its options.""" + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_RENAME + "-" + str(time.time()) + added_section = self.copy_section(config_name, section, + target_section, + skip_update=skip_update) + if added_section is None: + # Couldn't add the target section. + return + self.remove_section(config_name, section, skip_update=skip_update) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + + def remove_sections(self, config_name, sections, skip_update=False): + """Implement a mass removal of sections.""" + start_stack_index = len(self.undo_stack) + group = rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + nses = [] + for section in sections: + ns = self.data.helper.get_default_section_namespace( + section, config_name) + if ns not in nses: + nses.append(ns) + self.remove_section(config_name, section, skip_update=True) + for stack_item in self.undo_stack[start_stack_index:]: + stack_item.group = group + if not skip_update: + self.reload_ns_tree_func(only_this_config=config_name) + + def get_sub_ops_for_namespace(self, namespace): + """Return data functions for summary (sub) data panels.""" + if not namespace.startswith("/"): + namespace = "/" + namespace + config_name = self.util.split_full_ns(self.data, namespace)[0] + return SubDataOperations( + config_name, + self.add_section_with_options, + self.copy_section, + self.sect_ops.ignore_section, + self.ignore_sections, + self.remove_section, + self.remove_sections, + get_var_id_values_func=( + self.data.helper.get_sub_data_var_id_value_map)) + + +class SubDataOperations(object): + + """Class to hold a selected set of functions.""" + + def __init__(self, config_name, + add_section_func, clone_section_func, + ignore_section_func, ignore_sections_func, + remove_section_func, remove_sections_func, + get_var_id_values_func): + self.config_name = config_name + self._add_section_func = add_section_func + self._clone_section_func = clone_section_func + self._ignore_section_func = ignore_section_func + self._ignore_sections_func = ignore_sections_func + self._remove_section_func = remove_section_func + self._remove_sections_func = remove_sections_func + self._get_var_id_values_func = get_var_id_values_func + + def add_section(self, new_section_name, opt_map=None): + """Add a new section, complete with any compulsory variables.""" + return self._add_section_func(self.config_name, new_section_name, + opt_map=opt_map) + + def clone_section(self, clone_section_name): + """Copy a (duplicate) section and all its options.""" + return self._clone_section_func(self.config_name, clone_section_name) + + def ignore_section(self, ignore_section_name, is_ignored): + """User-ignore or enable a section.""" + return self._ignore_section_func( + self.config_name, + ignore_section_name, + is_ignored) + + def ignore_sections(self, ignore_sections_list, is_ignored, + skip_sub_data_update=True): + """User-ignore or enable a list of sections.""" + return self._ignore_sections_func( + self.config_name, + ignore_sections_list, + is_ignored, + skip_sub_data_update=skip_sub_data_update + ) + + def remove_section(self, remove_section_name): + """Remove a section and all its options.""" + return self._remove_section_func(self.config_name, + remove_section_name) + + def remove_sections(self, remove_sections_list): + """Remove a list of sections and all their options.""" + return self._remove_sections_func(self.config_name, + remove_sections_list) + + def get_var_id_values(self): + """Return a map of all var id values.""" + return self._get_var_id_values_func(self.config_name) diff --git a/metomi/rose/config_editor/ops/section.py b/metomi/rose/config_editor/ops/section.py new file mode 100644 index 000000000..864169504 --- /dev/null +++ b/metomi/rose/config_editor/ops/section.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This module deals with section-specific actions. + +The methods of SectionOperations are the only ways that section data +objects should be interacted with. There are also some utility methods. + +""" + +import copy + +import gi +gi.require_version('Gtk', '3.0') + +import rose.config_editor.stack +import rose.gtk.dialog +import rose.gtk.util + + +class SectionOperations(object): + + """A class to hold functions that act on sections and their storage.""" + + def __init__(self, data, util, reporter, undo_stack, redo_stack, + check_cannot_enable_func=rose.config_editor.false_function, + update_ns_func=rose.config_editor.false_function, + update_sub_data_func=rose.config_editor.false_function, + update_info_func=rose.config_editor.false_function, + update_comments_func=rose.config_editor.false_function, + update_tree_func=rose.config_editor.false_function, + search_id_func=rose.config_editor.false_function, + view_page_func=rose.config_editor.false_function, + kill_page_func=rose.config_editor.false_function): + self.__data = data + self.__util = util + self.__reporter = reporter + self.__undo_stack = undo_stack + self.__redo_stack = redo_stack + self.check_cannot_enable_setting = check_cannot_enable_func + self.trigger_update = update_ns_func + self.trigger_update_sub_data = update_sub_data_func + self.trigger_info_update = update_info_func + self.trigger_reload_tree = update_tree_func + self.search_id_func = search_id_func + self.view_page_func = view_page_func + self.kill_page_func = kill_page_func + + def add_section(self, config_name, section, skip_update=False, + page_launch=False, comments=None, ignored_reason=None, + skip_undo=False): + """Add a section to this configuration.""" + config_data = self.__data.config[config_name] + new_section_data = None + if not section or section in config_data.sections.now: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.ERROR_SECTION_ADD.format(section), + title=rose.config_editor.ERROR_SECTION_ADD_TITLE, + modal=False) + return + if section in config_data.sections.latent: + new_section_data = config_data.sections.latent.pop(section) + else: + metadata = self.__data.helper.get_metadata_for_config_id( + section, config_name) + new_section_data = rose.section.Section(section, [], metadata) + if comments is not None: + new_section_data.comments = copy.deepcopy(comments) + if ignored_reason is not None: + new_section_data.ignored_reason = copy.deepcopy(ignored_reason) + config_data.sections.now.update({section: new_section_data}) + self.__data.add_section_to_config(section, config_name) + self.__data.load_ns_for_node(new_section_data, config_name) + self.__data.load_file_metadata(config_name, section) + self.__data.load_vars_from_config(config_name, + only_this_section=section, + update=True) + self.__data.load_node_namespaces(config_name, + only_this_section=section) + metadata = self.__data.helper.get_metadata_for_config_id(section, + config_name) + new_section_data.process_metadata(metadata) + ns = new_section_data.metadata["full_ns"] + if not skip_update: + self.trigger_reload_tree(ns) + if rose.META_PROP_DUPLICATE in metadata: + self.__data.load_namespace_has_sub_data(config_name) + if not skip_undo: + copy_section_data = new_section_data.copy() + stack_item = rose.config_editor.stack.StackItem( + ns, + rose.config_editor.STACK_ACTION_ADDED, + copy_section_data, + self.remove_section, + (config_name, section, skip_update)) + self.__undo_stack.append(stack_item) + del self.__redo_stack[:] + if page_launch and not skip_update: + self.view_page_func(ns) + if not skip_update: + self.trigger_update(ns) + return ns + + def ignore_section(self, config_name, section, is_ignored, + override=False, skip_update=False, skip_undo=False): + """Ignore or enable a section for this configuration. + + Returns a list of namespaces that need further updates. This is + empty if skip_update is False. + + """ + config_data = self.__data.config[config_name] + sect_data = config_data.sections.now[section] + nses_to_do = [sect_data.metadata["full_ns"]] + ids_to_do = [section] + + if is_ignored: + # User-ignore request for this section. + # The section must be enabled and optional. + if (not override and ( + sect_data.ignored_reason or + sect_data.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE)): + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.WARNING_CANNOT_USER_IGNORE.format( + section), + rose.config_editor.WARNING_CANNOT_IGNORE_TITLE) + return [], [] + for error in [rose.config_editor.WARNING_TYPE_USER_IGNORED, + rose.config_editor.WARNING_TYPE_ENABLED]: + if error in sect_data.error: + sect_data.ignored_reason.update({ + rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_MANUAL}) + sect_data.error.pop(error) + break + else: + sect_data.ignored_reason.update({ + rose.variable.IGNORED_BY_USER: + rose.config_editor.IGNORED_STATUS_MANUAL}) + action = rose.config_editor.STACK_ACTION_IGNORED + else: + # Enable request for this section. + # The section must not be justifiably triggered ignored. + ign_errors = [e for e in rose.config_editor.WARNING_TYPES_IGNORE + if e != rose.config_editor.WARNING_TYPE_ENABLED] + my_errors = list(sect_data.error.keys()) + if (not override and + (rose.variable.IGNORED_BY_SYSTEM in + sect_data.ignored_reason) and + all([e not in my_errors for e in ign_errors]) and + self.check_cannot_enable_setting(config_name, section)): + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + rose.config_editor.WARNING_CANNOT_ENABLE.format(section), + rose.config_editor.WARNING_CANNOT_ENABLE_TITLE) + return [], [] + sect_data.ignored_reason.clear() + for error in ign_errors: + if error in my_errors: + sect_data.error.pop(error) + action = rose.config_editor.STACK_ACTION_ENABLED + + ns = sect_data.metadata["full_ns"] + copy_sect_data = sect_data.copy() + if not skip_undo: + stack_item = rose.config_editor.stack.StackItem( + ns, + action, + copy_sect_data, + self.ignore_section, + (config_name, section, not is_ignored, True) + ) + self.__undo_stack.append(stack_item) + del self.__redo_stack[:] + for var in (config_data.vars.now.get(section, []) + + config_data.vars.latent.get(section, [])): + self.trigger_info_update(var) + if var.metadata['full_ns'] not in nses_to_do: + nses_to_do.append(var.metadata['full_ns']) + ids_to_do.append(var.metadata['id']) + if is_ignored: + var.ignored_reason.update( + {rose.variable.IGNORED_BY_SECTION: + rose.config_editor.IGNORED_STATUS_MANUAL}) + elif rose.variable.IGNORED_BY_SECTION in var.ignored_reason: + var.ignored_reason.pop(rose.variable.IGNORED_BY_SECTION) + else: + continue + if skip_update: + return nses_to_do, ids_to_do + for ns in nses_to_do: + self.trigger_update(ns) + self.trigger_info_update(ns) + self.trigger_update(config_name) + return [], [] + + def remove_section(self, config_name, section, skip_update=False, + skip_undo=False): + """Remove a section from this configuration.""" + config_data = self.__data.config[config_name] + old_section_data = config_data.sections.now.pop(section) + config_data.sections.latent.update({section: old_section_data}) + if section in config_data.vars.now: + config_data.vars.now.pop(section) + namespace = old_section_data.metadata["full_ns"] + ns_list = [namespace] + for ns, values in list(self.__data.namespace_meta_lookup.items()): + sections = values.get('sections') + if sections == [section]: + if ns not in ns_list: + ns_list.append(ns) + if not skip_undo: + stack_item = rose.config_editor.stack.StackItem( + namespace, + rose.config_editor.STACK_ACTION_REMOVED, + old_section_data.copy(), + self.add_section, + (config_name, section, skip_update) + ) + for ns in ns_list: + self.kill_page_func(ns) + self.__undo_stack.append(stack_item) + del self.__redo_stack[:] + if not skip_update: + self.trigger_reload_tree(only_this_namespace=namespace) + return ns_list + + def set_section_comments(self, config_name, section, comments, + skip_update=False, skip_undo=False): + """Change the comments field for the section object.""" + config_data = self.__data.config[config_name] + sect_data = config_data.sections.now[section] + old_sect_data = sect_data.copy() + last_comments = old_sect_data.comments + sect_data.comments = comments + if not skip_undo: + ns = sect_data.metadata["full_ns"] + stack_item = rose.config_editor.stack.StackItem( + ns, + rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, + old_sect_data, + self.set_section_comments, + (config_name, section, last_comments) + ) + self.__undo_stack.append(stack_item) + del self.__redo_stack[:] + if not skip_update: + self.trigger_update(ns) + return ns + + def is_section_modified(self, section_object): + """Check against the last saved section object reference.""" + section = section_object.metadata["id"] + namespace = section_object.metadata["full_ns"] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + config_data = self.__data.config[config_name] + this_section = config_data.sections.now.get(section) + save_section = config_data.sections.save.get(section) + if this_section is None: + # Ghost variable, check absence from saved list. + if save_section is not None: + return True + else: + # Real variable, check value and presence in saved list. + if save_section is None: + return True + return this_section.to_hashable() != this_section.to_hashable() + + def get_section_changes(self, section_object): + """Return text describing changes since the last save.""" + section = section_object.metadata["id"] + namespace = section_object.metadata["full_ns"] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + config_data = self.__data.config[config_name] + this_section = config_data.sections.now.get(section) + save_section = config_data.sections.save.get(section) + if this_section is None: + if save_section is not None: + return rose.config_editor.KEY_TIP_MISSING + # Ignore both-missing scenarios (no actual diff in output). + return "" + if save_section is None: + return rose.config_editor.KEY_TIP_ADDED + if this_section.to_hashable() == save_section.to_hashable(): + return "" + if this_section.comments != save_section.comments: + return rose.config_editor.KEY_TIP_CHANGED_COMMENTS + # The difference must now be in the ignored state. + if rose.variable.IGNORED_BY_SYSTEM in this_section.ignored_reason: + return rose.config_editor.KEY_TIP_TRIGGER_IGNORED + if rose.variable.IGNORED_BY_USER in this_section.ignored_reason: + return rose.config_editor.KEY_TIP_USER_IGNORED + return rose.config_editor.KEY_TIP_ENABLED + + def get_ns_metadata_files(self, namespace): + """Retrieve filenames within the metadata for this namespace.""" + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + return self.__data.config[config_name].meta_files diff --git a/metomi/rose/config_editor/ops/variable.py b/metomi/rose/config_editor/ops/variable.py new file mode 100644 index 000000000..6bc7140ea --- /dev/null +++ b/metomi/rose/config_editor/ops/variable.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This module deals with variable actions. + +The methods of VariableOperations are the only ways that variable data +objects should be interacted with (adding, removing, changing value, +etc). There are also some utility methods. + +""" +import copy +import time +import webbrowser + +import rose.variable +import rose.config_editor +import rose.config_editor.stack + + +class VariableOperations(object): + + """A class to hold functions that act on variables and their storage.""" + + def __init__(self, data, util, reporter, undo_stack, redo_stack, + add_section_func, + check_cannot_enable_func=rose.config_editor.false_function, + update_ns_func=rose.config_editor.false_function, + ignore_update_func=rose.config_editor.false_function, + search_id_func=rose.config_editor.false_function): + self.__data = data + self.__util = util + self.__reporter = reporter + self.__undo_stack = undo_stack + self.__redo_stack = redo_stack + self.__add_section_func = add_section_func + self.check_cannot_enable_setting = check_cannot_enable_func + self.trigger_update = update_ns_func + self.trigger_ignored_update = ignore_update_func + self.search_id_func = search_id_func + + def _get_proper_variable(self, possible_copy_variable): + # Some variables are just copies, and changes to them + # won't affect anything. We need to look up the 'real' variable. + namespace = possible_copy_variable.metadata.get('full_ns') + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + var_id = possible_copy_variable.metadata['id'] + return self.__data.helper.get_ns_variable(var_id, config_name) + + def add_var(self, variable, skip_update=False, skip_undo=False): + """Add a variable to the internal list.""" + namespace = variable.metadata.get('full_ns') + var_id = variable.metadata['id'] + sect, opt = self.__util.get_section_option_from_id(var_id) + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + config_data = self.__data.config[config_name] + old_metadata = copy.deepcopy(variable.metadata) + flags = self.__data.load_option_flags(config_name, sect, opt) + variable.flags.update(flags) + metadata = self.__data.helper.get_metadata_for_config_id(var_id, + config_name) + variable.process_metadata(metadata) + variable.metadata.update(old_metadata) + variables = config_data.vars.now.get(sect, []) + copy_var = variable.copy() + v_id = variable.metadata.get('id') + if v_id in [v.metadata.get('id') for v in variables]: + # This is the case of adding a blank variable and + # renaming it to an existing variable's name. + # At the moment, assume this should just be skipped. + pass + else: + group = None + if sect not in config_data.sections.now: + start_stack_index = len(self.__undo_stack) + group = (rose.config_editor.STACK_GROUP_ADD + "-" + + str(time.time())) + self.__add_section_func(config_name, sect) + for item in self.__undo_stack[start_stack_index:]: + item.group = group + latent_variables = config_data.vars.latent.get(sect, []) + for latent_var in list(latent_variables): + if latent_var.metadata["id"] == v_id: + latent_variables.remove(latent_var) + config_data.vars.now.setdefault(sect, []) + config_data.vars.now[sect].append(variable) + if not skip_undo: + self.__undo_stack.append( + rose.config_editor.stack.StackItem( + variable.metadata['full_ns'], + rose.config_editor.STACK_ACTION_ADDED, + copy_var, + self.remove_var, + [copy_var, skip_update], + group=group) + ) + del self.__redo_stack[:] + if not skip_update: + self.trigger_update(variable.metadata['full_ns']) + return variable.metadata['full_ns'] + + def remove_var(self, variable, skip_update=False, skip_undo=False): + """Remove the variable entry from the internal lists.""" + variable = self._get_proper_variable(variable) + variable.error = {} # Kill any metadata errors before removing. + namespace = variable.metadata.get('full_ns') + var_id = variable.metadata['id'] + sect = self.__util.get_section_option_from_id(var_id)[0] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + config_data = self.__data.config[config_name] + variables = config_data.vars.now.get(sect, []) + latent_variables = config_data.vars.latent.get(sect, []) + if variable in latent_variables: + latent_variables.remove(variable) + if not config_data.vars.latent[sect]: + config_data.vars.latent.pop(sect) + return None + if variable in variables: + variables.remove(variable) + if not config_data.vars.now[sect]: + config_data.vars.now.pop(sect) + if variable.name: + config_data.vars.latent.setdefault(sect, []) + config_data.vars.latent[sect].append(variable) + if not skip_undo: + copy_var = variable.copy() + self.__undo_stack.append( + rose.config_editor.stack.StackItem( + variable.metadata['full_ns'], + rose.config_editor.STACK_ACTION_REMOVED, + copy_var, + self.add_var, + [copy_var, skip_update])) + del self.__redo_stack[:] + if not skip_update: + self.trigger_update(variable.metadata['full_ns']) + return variable.metadata['full_ns'] + + def fix_var_ignored(self, variable): + """Fix any variable ignore state errors.""" + ignored_reasons = list(variable.ignored_reason.keys()) + new_reason_dict = {} # Enable, by default. + old_reason = variable.ignored_reason.copy() + if rose.variable.IGNORED_BY_SECTION in old_reason: + # Preserve section-ignored status. + new_reason_dict.setdefault( + rose.variable.IGNORED_BY_SECTION, + old_reason[rose.variable.IGNORED_BY_SECTION]) + if rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: + # Doc table I_t + if rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + # Enable new_reason_dict. + # Doc table I_t -> E + pass + if rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + pass + elif rose.variable.IGNORED_BY_USER in ignored_reasons: + # Doc table I_u + if rose.config_editor.WARNING_TYPE_USER_IGNORED in variable.error: + # Enable new_reason_dict. + # Doc table I_u -> I_t -> *, + # I_u -> E -> compulsory, + # I_u -> not trigger -> compulsory + pass + else: + # Doc table E + if rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + # Doc table E -> I_t -> * + new_reason_dict = {rose.variable.IGNORED_BY_SYSTEM: + rose.config_editor.IGNORED_STATUS_MANUAL} + self.set_var_ignored(variable, new_reason_dict) + + def set_var_ignored(self, variable, new_reason_dict=None, override=False, + skip_update=False, skip_undo=False): + """Set the ignored flag data for the variable. + + new_reason_dict replaces the variable.ignored_reason attribute, + except for the rose.variable.IGNORED_BY_SECTION key. + + """ + if new_reason_dict is None: + new_reason_dict = {} + variable = self._get_proper_variable(variable) + old_reason = variable.ignored_reason.copy() + if rose.variable.IGNORED_BY_SECTION in old_reason: + new_reason_dict.setdefault( + rose.variable.IGNORED_BY_SECTION, + old_reason[rose.variable.IGNORED_BY_SECTION]) + if rose.variable.IGNORED_BY_SECTION not in old_reason: + if rose.variable.IGNORED_BY_SECTION in new_reason_dict: + new_reason_dict.pop(rose.variable.IGNORED_BY_SECTION) + variable.ignored_reason = new_reason_dict.copy() + if not set(old_reason.keys()) ^ set(new_reason_dict.keys()): + # No practical difference, so don't do anything. + return None + # Protect against user-enabling of triggered ignored. + if (not override and + rose.variable.IGNORED_BY_SYSTEM in old_reason and + rose.variable.IGNORED_BY_SYSTEM not in new_reason_dict): + if rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + variable.error.pop( + rose.config_editor.WARNING_TYPE_NOT_TRIGGER) + my_ignored_keys = list(variable.ignored_reason.keys()) + if rose.variable.IGNORED_BY_SECTION in my_ignored_keys: + my_ignored_keys.remove(rose.variable.IGNORED_BY_SECTION) + old_ignored_keys = list(old_reason.keys()) + if rose.variable.IGNORED_BY_SECTION in old_ignored_keys: + old_ignored_keys.remove(rose.variable.IGNORED_BY_SECTION) + if len(my_ignored_keys) > len(old_ignored_keys): + action_text = rose.config_editor.STACK_ACTION_IGNORED + if (not old_ignored_keys and + rose.config_editor.WARNING_TYPE_ENABLED in variable.error): + variable.error.pop(rose.config_editor.WARNING_TYPE_ENABLED) + else: + action_text = rose.config_editor.STACK_ACTION_ENABLED + if not my_ignored_keys: + for err_type in rose.config_editor.WARNING_TYPES_IGNORE: + if err_type in variable.error: + variable.error.pop(err_type) + if not skip_undo: + copy_var = variable.copy() + self.__undo_stack.append( + rose.config_editor.stack.StackItem( + variable.metadata['full_ns'], + action_text, + copy_var, + self.set_var_ignored, + [copy_var, old_reason, True]) + ) + del self.__redo_stack[:] + self.trigger_ignored_update(variable) + if not skip_update: + self.trigger_update(variable.metadata['full_ns']) + return variable.metadata['full_ns'] + + def set_var_value(self, variable, new_value, skip_update=False, + skip_undo=False): + """Set the value of the variable.""" + variable = self._get_proper_variable(variable) + if variable.value == new_value: + # A bad valuewidget setter. + return None + variable.old_value = variable.value + variable.value = new_value + if not skip_undo: + copy_var = variable.copy() + self.__undo_stack.append( + rose.config_editor.stack.StackItem( + variable.metadata['full_ns'], + rose.config_editor.STACK_ACTION_CHANGED, + copy_var, + self.set_var_value, + [copy_var, copy_var.old_value]) + ) + del self.__redo_stack[:] + if not skip_update: + self.trigger_update(variable.metadata['full_ns']) + return variable.metadata['full_ns'] + + def set_var_comments(self, variable, comments, + skip_update=False, skip_undo=False): + """Set the comments field for the variable.""" + variable = self._get_proper_variable(variable) + copy_variable = variable.copy() + old_comments = copy_variable.comments + variable.comments = comments + if not skip_undo: + self.__undo_stack.append( + rose.config_editor.stack.StackItem( + variable.metadata['full_ns'], + rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, + copy_variable, + self.set_var_comments, + [copy_variable, old_comments]) + ) + del self.__redo_stack[:] + if not skip_update: + self.trigger_update(variable.metadata['full_ns']) + return variable.metadata['full_ns'] + + def get_var_original_comments(self, variable): + """Get the original comments, if any.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + save_var = self.__data.helper.get_variable_by_id(var_id, config_name, + save=True) + if save_var is None: + return None + return save_var.comments + + def get_var_original_ignore(self, variable): + """Get the original value, if any.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + save_var = self.__data.helper.get_variable_by_id(var_id, config_name, + save=True) + if save_var is None: + return None + return save_var.ignored_reason + + def get_var_original_value(self, variable): + """Get the original value, if any.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + save_variable = self.__data.helper.get_variable_by_id( + var_id, config_name, save=True) + if save_variable is None: + return None + return save_variable.value + + def is_var_modified(self, variable): + """Check against the last saved variable reference.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + this_variable = self.__data.helper.get_variable_by_id(var_id, + config_name) + save_variable = self.__data.helper.get_variable_by_id(var_id, + config_name, + save=True) + if this_variable is None: + # Ghost variable, check absence from saved list. + if save_variable is not None: + return True + else: + # Real variable, check value and presence in saved list. + if save_variable is None: + return True + return this_variable.to_hashable() != save_variable.to_hashable() + + def is_var_added(self, variable): + """Check if missing from the saved variables list.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + save_variable = self.__data.helper.get_variable_by_id(var_id, + config_name, + save=True) + return save_variable is None + + def is_var_ghost(self, variable): + """Check if the variable is a latent variable.""" + var_id = variable.metadata['id'] + namespace = variable.metadata['full_ns'] + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + this_variable = self.__data.helper.get_variable_by_id(var_id, + config_name) + return (this_variable is None) + + def get_var_changes(self, variable): + """Return a description of any changed status the variable has.""" + if self.is_var_modified(variable): + if self.is_var_added(variable): + return rose.config_editor.KEY_TIP_ADDED + if self.is_var_ghost(variable): + return rose.config_editor.KEY_TIP_MISSING + old_value = self.get_var_original_value(variable) + if variable.value != self.get_var_original_value(variable): + return rose.config_editor.KEY_TIP_CHANGED.format(old_value) + if self.get_var_original_comments(variable) != variable.comments: + return rose.config_editor.KEY_TIP_CHANGED_COMMENTS + if not variable.ignored_reason: + return rose.config_editor.KEY_TIP_ENABLED + old_ignore = self.get_var_original_ignore(variable) + if len(old_ignore) > len(variable.ignored_reason): + return rose.config_editor.KEY_TIP_ENABLED + if (rose.variable.IGNORED_BY_SYSTEM in variable.ignored_reason and + rose.variable.IGNORED_BY_SYSTEM not in old_ignore): + return rose.config_editor.KEY_TIP_TRIGGER_IGNORED + if (rose.variable.IGNORED_BY_USER in variable.ignored_reason and + rose.variable.IGNORED_BY_USER not in old_ignore): + return rose.config_editor.KEY_TIP_USER_IGNORED + if (rose.variable.IGNORED_BY_SECTION in variable.ignored_reason and + rose.variable.IGNORED_BY_SECTION not in old_ignore): + return rose.config_editor.KEY_TIP_SECTION_IGNORED + return rose.config_editor.KEY_TIP_ENABLED + return '' + + def launch_url(self, variable): + """Determine and launch the variable help URL in a web browser.""" + if rose.META_PROP_URL not in variable.metadata: + return + url = variable.metadata[rose.META_PROP_URL] + if rose.variable.REC_FULL_URL.match(url): + # It is a proper URL by itself - launch it. + return self._launch_url(url) + # Must be a partial URL (e.g. '#foo') - try to prefix a parent URL. + ns_url = self.__data.helper.get_ns_url_for_variable(variable) + if ns_url: + return self._launch_url(ns_url + url) + return self._launch_url(url) + + def _launch_url(self, url): + """Actually launch a URL.""" + try: + webbrowser.open(url) + except webbrowser.Error as exc: + rose.gtk.dialog.run_exception_dialog(exc) + + def search_for_var(self, config_name_or_namespace, setting_id): + """Launch a search for a setting or variable id.""" + config_name = self.__util.split_full_ns( + self.__data, config_name_or_namespace)[0] + self.search_id_func(config_name, setting_id) + + def get_ns_metadata_files(self, namespace): + """Retrieve filenames within the metadata for this namespace.""" + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + return self.__data.config[config_name].meta_files + + def get_sections(self, namespace): + """Retrieve all real sections (empty or not) for this ns's config.""" + config_name = self.__util.split_full_ns(self.__data, namespace)[0] + section_objects = self.__data.config[config_name].sections.get_all( + skip_latent=True) + return [_.name for _ in section_objects] diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py new file mode 100644 index 000000000..ce39a9dba --- /dev/null +++ b/metomi/rose/config_editor/page.py @@ -0,0 +1,1192 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re +import time +import webbrowser + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Pango + +import rose.config_editor.panelwidget +import rose.config_editor.pagewidget +import rose.config_editor.stack +import rose.config_editor.util +import rose.config_editor.variable +import rose.formats +import rose.gtk.dialog +import rose.gtk.util +import rose.resource +import rose.variable + + +class ConfigPage(Gtk.VBox): + + """Returns a container for a tab.""" + + def __init__(self, page_metadata, config_data, ghost_data, section_ops, + variable_ops, sections, latent_sections, get_formats_func, + reporter, directory=None, sub_data=None, sub_ops=None, + launch_info_func=None, launch_edit_func=None, + launch_macro_func=None): + super(ConfigPage, self).__init__(homogeneous=False) + self.namespace = page_metadata.get('namespace') + self.ns_is_default = page_metadata.get('ns_is_default') + self.config_name = page_metadata.get('config_name') + self.label = page_metadata.get('label') + self.description = page_metadata.get('description') + self.help = page_metadata.get('help') + self.url = page_metadata.get('url') + self.see_also = page_metadata.get('see_also') + self.custom_macros = page_metadata.get('macro', {}) + self.custom_widget = page_metadata.get('widget') + self.custom_sub_widget = page_metadata.get('widget_sub_ns') + self.show_modes = page_metadata.get('show_modes') + self.is_duplicate = (page_metadata.get('duplicate') == + rose.META_PROP_VALUE_TRUE) + self.section = None + if sections: + self.section = sections[0] + self.sections = sections + self.latent_sections = latent_sections + self.icon_path = page_metadata.get('icon') + self.reporter = reporter + self.directory = directory + self.sub_data = sub_data + self.sub_ops = sub_ops + self.launch_info = launch_info_func + self.launch_edit = launch_edit_func + self._launch_macro_func = launch_macro_func + namespaces = self.namespace.strip('/').split('/') + namespaces.reverse() + self.info = "" + if self.description is None: + self.info = " - ".join(namespaces[:-1]) + else: + if self.description != '': + self.info = self.description + '\n' + self.info += " - ".join(namespaces[:-1]) + if self.see_also != '': + self.info += '\n => ' + self.see_also + self.panel_data = config_data + self.ghost_data = ghost_data + self.section_ops = section_ops + self.variable_ops = variable_ops + self.trigger_ask_for_config_keys = ( + lambda: get_formats_func(self.config_name)) + self.sort_data() + self.sort_data(ghost=True) + self._last_info_labels = None + self.generate_main_container() + self.get_page() + self.update_ignored(no_refresh=True) + + def get_page(self): + """Generate a container of widgets for page content and a label.""" + self.labelwidget = self.get_label_widget() + self.scrolled_main_window = Gtk.ScrolledWindow() + self.scrolled_main_window.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + self.scrolled_vbox = Gtk.VBox() + self.scrolled_vbox.show() + self.scrolled_main_window.add_with_viewport(self.scrolled_vbox) + self.scrolled_main_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) + self.scrolled_main_window.set_border_width( + rose.config_editor.SPACING_SUB_PAGE) + self.scrolled_vbox.pack_start(self.main_container, + expand=False, fill=True) + self.scrolled_main_window.show() + self.main_vpaned = Gtk.VPaned() + self.info_panel = Gtk.VBox(homogeneous=False) + self.info_panel.show() + self.update_info() + second_panel = None + if self.namespace == self.config_name and self.directory is not None: + self.generate_filesystem_panel() + second_panel = self.filesystem_panel + elif self.sub_data is not None: + self.generate_sub_data_panel() + second_panel = self.sub_data_panel + self.vpaned = Gtk.VPaned() + if self.panel_data: + self.vpaned.pack1(self.scrolled_main_window, resize=True, + shrink=True) + if second_panel is not None: + self.vpaned.pack2(second_panel, resize=False, shrink=True) + elif second_panel is not None: + self.vpaned.pack1(self.scrolled_main_window, resize=False, + shrink=True) + self.vpaned.pack2(second_panel, resize=True, shrink=True) + self.vpaned.set_position(rose.config_editor.FILE_PANEL_EXPAND) + else: + self.vpaned.pack1(self.scrolled_main_window, resize=True, + shrink=True) + self.vpaned.show() + self.main_vpaned.pack2(self.vpaned) + self.main_vpaned.show() + self.pack_start(self.main_vpaned, expand=True, fill=True) + self.show() + self.scroll_vadj = self.scrolled_main_window.get_vadjustment() + self.scrolled_main_window.connect( + "button-press-event", + self._handle_click_main_window) + + def _handle_click_main_window(self, widget, event): + if event.button != 3: + return False + self.launch_add_menu(event.button, event.time) + return False + + def get_label_widget(self, is_detached=False): + """Return a container of widgets for the notebook tab label.""" + if is_detached: + location = self.config_name.lstrip('/').split('/') + location.reverse() + label = Gtk.Label(label=' - '.join([self.label] + location)) + self.is_detached = True + else: + label = Gtk.Label(label=self.label) + self.is_detached = False + label.show() + label_event_box = Gtk.EventBox() + label_event_box.add(label) + label_event_box.show() + if self.help or self.url: + label_event_box.connect("enter-notify-event", + self._handle_enter_label) + label_event_box.connect("leave-notify-event", + self._handle_leave_label) + label_box = Gtk.HBox(homogeneous=False) + if self.icon_path is not None: + self.label_icon = Gtk.Image() + self.label_icon.set_from_file(self.icon_path) + self.label_icon.show() + label_box.pack_start(self.label_icon, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + close_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_CLOSE, size=Gtk.IconSize.MENU, as_tool=True) + style = Gtk.RcStyle() + style.xthickness = 0 + style.ythickness = 0 + setattr(style, "inner-border", [0, 0, 0, 0]) + close_button.modify_style(style) + + label_box.pack_start(label_event_box, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + if not is_detached: + label_box.pack_end(close_button, expand=False, fill=False) + label_box.show() + event_box = Gtk.EventBox() + event_box.add(label_box) + close_button.connect('released', lambda b: self.close_self()) + event_box.connect('button_press_event', self._handle_click_tab) + event_box.show() + if self.info is not None: + event_box.connect("enter-notify-event", self._set_tab_tooltip) + return event_box + + def _handle_enter_label(self, label_event_box, event=None): + label = label_event_box.get_child() + att_list = label.get_attributes() + if att_list is None: + att_list = Pango.AttrList() + att_list.insert(Pango.AttrUnderline(Pango.Underline.SINGLE, + start_index=0, + end_index=-1)) + label.set_attributes(att_list) + + def _handle_leave_label(self, label_event_box, event=None): + label = label_event_box.get_child() + att_list = label.get_attributes() + if att_list is None: + att_list = Pango.AttrList() + att_list = att_list.filter(lambda a: + a.type != Pango.ATTR_UNDERLINE) + if att_list is None: + # This is messy but necessary. + att_list = Pango.AttrList() + label.set_attributes(att_list) + + def _set_tab_tooltip(self, event_box, event): + tip_text = "" + if self.info is not None: + tip_text += self.info + if self.section is not None: + comment_format = rose.config_editor.VAR_COMMENT_TIP.format + for comment_line in self.section.comments: + tip_text += "\n" + comment_format(comment_line) + event_box.set_tooltip_text(tip_text) + + def _handle_click_tab(self, event_widget, event): + if event.button == 3: + return self.launch_tab_menu(event) + if self.main_vpaned.get_mapped(): + if self.help: + return self.launch_help() + if self.url: + return self.launch_url() + + def launch_tab_menu(self, event): + """Open a popup menu for the tab, if right clicked.""" + ui_config_string_start = """ """ + ui_config_string_end = """ """ + if not self.is_detached: + ui_config_string_start += """ + """ + close_string = """ + """ + ui_config_string_end = close_string + ui_config_string_end + ui_config_string_start += """ + """ + actions = [ + ('Open', Gtk.STOCK_NEW, rose.config_editor.TAB_MENU_OPEN_NEW), + ('Info', Gtk.STOCK_INFO, rose.config_editor.TAB_MENU_INFO), + ('Edit', Gtk.STOCK_EDIT, rose.config_editor.TAB_MENU_EDIT), + ('Help', Gtk.STOCK_HELP, rose.config_editor.TAB_MENU_HELP), + ('Web_Help', Gtk.STOCK_HOME, + rose.config_editor.TAB_MENU_WEB_HELP), + ('Close', Gtk.STOCK_CLOSE, rose.config_editor.TAB_MENU_CLOSE)] + if self.help is not None: + help_string = """ + """ + ui_config_string_end = help_string + ui_config_string_end + if self.url is not None: + url_string = """ + """ + ui_config_string_end = url_string + ui_config_string_end + + uimanager = Gtk.UIManager() + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.add_actions(actions) + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(ui_config_string_start + + ui_config_string_end) + if not self.is_detached: + window_item = uimanager.get_widget('/Popup/Open') + window_item.connect("activate", self.trigger_tab_detach) + close_item = uimanager.get_widget('/Popup/Close') + close_item.connect("activate", lambda b: self.close_self()) + edit_item = uimanager.get_widget('/Popup/Edit') + edit_item.connect("activate", lambda b: self.launch_edit()) + info_item = uimanager.get_widget('/Popup/Info') + info_item.connect("activate", lambda b: self.launch_info()) + if self.help is not None: + help_item = uimanager.get_widget('/Popup/Help') + help_item.connect("activate", lambda b: self.launch_help()) + if self.url is not None: + url_item = uimanager.get_widget('/Popup/Web_Help') + url_item.connect("activate", lambda b: self.launch_url()) + tab_menu = uimanager.get_widget('/Popup') + tab_menu.popup(None, None, None, event.button, event.time) + return False + + def trigger_tab_detach(self, widget=None): + """Connect this at a higher level to manage tab detachment.""" + pass + + def reshuffle_for_detached(self, add_button, revert_button, parent): + """Reshuffle widgets for detached view.""" + focus_child = getattr(self, 'focus_child') + button_hbox = Gtk.HBox(homogeneous=False, spacing=0) + self.tool_hbox = Gtk.HBox(homogeneous=False, spacing=0) + sep = Gtk.VSeparator() + sep.show() + sep_vbox = Gtk.VBox() + sep_vbox.pack_start(sep, expand=True, fill=True) + sep_vbox.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + sep_vbox.show() + info_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_INFO, + as_tool=True, + tip_text=rose.config_editor.TAB_MENU_INFO) + info_button.connect("clicked", lambda m: self.launch_info()) + help_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_HELP, + as_tool=True, + tip_text=rose.config_editor.TAB_MENU_HELP) + help_button.connect("clicked", self.launch_help) + url_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_HOME, + as_tool=True, + tip_text=rose.config_editor.TAB_MENU_WEB_HELP) + url_button.connect("clicked", self.launch_url) + button_hbox.pack_start(add_button, expand=False, fill=False) + button_hbox.pack_start(revert_button, expand=False, fill=False) + button_hbox.pack_start(sep_vbox, expand=False, fill=False) + button_hbox.pack_start(info_button, expand=False, fill=False) + if self.help is not None: + button_hbox.pack_start(help_button, expand=False, fill=False) + if self.url is not None: + button_hbox.pack_start(url_button, expand=False, fill=False) + button_hbox.show() + button_frame = Gtk.Frame() + button_frame.set_shadow_type(Gtk.ShadowType.NONE) + button_frame.add(button_hbox) + button_frame.show() + self.tool_hbox.pack_start(button_frame, expand=False, fill=False) + label_box = Gtk.HBox(homogeneous=False, + spacing=rose.config_editor.SPACING_PAGE) + label_box.pack_start(self.get_label_widget(is_detached=True, True, True, 0)) + label_box.show() + self.tool_hbox.pack_start( + label_box, expand=True, fill=True, padding=10) + self.tool_hbox.show() + self.pack_start(self.tool_hbox, expand=False, fill=False) + self.reorder_child(self.tool_hbox, 0) + if isinstance(parent, Gtk.Window): + if parent.get_child() is not None: + parent.remove(parent.get_child()) + else: + self.close_self() + if focus_child is not None: + focus_child.grab_focus() + + def close_self(self): + """Delete this instance from a rose.gtk.util.Notebook.""" + parent = self.get_parent() + my_index = parent.get_page_ids().index(self.namespace) + parent.remove_page(my_index) + parent.emit('select-page', False) + + def launch_help(self, *args): + """Launch the page help.""" + title = rose.config_editor.DIALOG_HELP_TITLE.format(self.label) + rose.gtk.dialog.run_hyperlink_dialog( + Gtk.STOCK_DIALOG_INFO, str(self.help), title) + + def launch_url(self, *args): + """Launch the page url help.""" + webbrowser.open(str(self.url)) + + def update_info(self): + """Driver routine to update non-variable information.""" + button_list, label_list, _ = self._get_page_info_widgets() + if [l.get_text() for l in label_list] == self._last_info_labels: + # No change - do not redraw. + return False + self.generate_page_info(button_list, label_list) + has_content = (self.info_panel.get_children() and + self.info_panel.get_children()[0].get_children()) + if self.info_panel in self.main_vpaned.get_children(): + if not has_content: + self.main_vpaned.remove(self.info_panel) + elif has_content: + self.main_vpaned.pack1(self.info_panel) + + def generate_page_info(self, button_list=None, label_list=None, info=None): + """Generate a widget giving information about sections.""" + info_container = Gtk.VBox(homogeneous=False) + info_container.show() + if button_list is None or label_list is None or info is None: + button_list, label_list, info = self._get_page_info_widgets() + self._last_info_labels = [l.get_text() for l in label_list] + for button, label in zip(button_list, label_list): + var_hbox = Gtk.HBox(homogeneous=False) + var_hbox.pack_start(button, expand=False, fill=False) + var_hbox.pack_start(label, expand=False, fill=True, + padding=rose.config_editor.SPACING_SUB_PAGE) + var_hbox.show() + info_container.pack_start(var_hbox, expand=False, fill=True) + # Add page help. + if self.description: + help_label = rose.gtk.util.get_hyperlink_label( + self.description, search_func=self.search_for_id) + help_label_window = Gtk.ScrolledWindow() + help_label_window.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + help_label_hbox = Gtk.HBox() + help_label_hbox.pack_start(help_label, expand=False, fill=False) + help_label_hbox.show() + help_label_vbox = Gtk.VBox() + help_label_vbox.pack_start( + help_label_hbox, expand=False, fill=False) + help_label_vbox.show() + help_label_window.add_with_viewport(help_label_vbox) + help_label_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) + help_label_window.show() + width, height = help_label_window.size_request() + if info == "Blank page - no data": + self.main_vpaned.set_position( + rose.config_editor.SIZE_WINDOW[1] * 100) + else: + height = min([rose.config_editor.SIZE_WINDOW[1] / 3, + help_label.size_request()[1]]) + help_label_window.set_size_request(width, height) + help_hbox = Gtk.HBox() + help_hbox.pack_start(help_label_window, expand=True, fill=True, + padding=rose.config_editor.SPACING_SUB_PAGE) + help_hbox.show() + info_container.pack_start( + help_hbox, expand=True, fill=True, + padding=rose.config_editor.SPACING_SUB_PAGE) + for child in self.info_panel.get_children(): + self.info_panel.remove(child) + self.info_panel.pack_start(info_container, expand=True, fill=True) + + def generate_filesystem_panel(self): + """Generate a widget to view the file hierarchy.""" + self.filesystem_panel = ( + rose.config_editor.panelwidget.filesystem.FileSystemPanel( + self.directory)) + + def generate_sub_data_panel(self, override_custom=False): + """Generate a panel giving a summary of other page data.""" + args = (self.sub_data["sections"], + self.sub_data["variables"], + self.section_ops, + self.variable_ops, + self.search_for_id, + self.sub_ops, + self.is_duplicate) + if self.custom_sub_widget is not None and not override_custom: + widget_name_args = self.custom_sub_widget.split(None, 1) + if len(widget_name_args) > 1: + widget_path, widget_args = widget_name_args + else: + widget_path, widget_args = widget_name_args[0], None + metadata_files = self.section_ops.get_ns_metadata_files( + self.namespace) + widget_dir = rose.META_DIR_WIDGET + metadata_files.sort( + lambda x, y: (widget_dir in y) - (widget_dir in x)) + prefix = re.sub(r"[^\w]", "_", self.config_name.strip("/")) + prefix += "/" + rose.META_DIR_WIDGET + "/" + custom_widget = rose.resource.import_object( + widget_path, + metadata_files, + self.handle_bad_custom_sub_widget, + module_prefix=prefix) + if custom_widget is None: + text = rose.config_editor.ERROR_IMPORT_CLASS.format( + self.custom_sub_widget) + self.handle_bad_custom_sub_widget(text) + return False + try: + self.sub_data_panel = custom_widget(*args, arg_str=widget_args) + except Exception as exc: + self.handle_bad_custom_sub_widget(str(exc)) + else: + panel_module = rose.config_editor.panelwidget.summary_data + self.sub_data_panel = ( + panel_module.StandardSummaryDataPanel(*args)) + + def handle_bad_custom_sub_widget(self, error_info): + text = rose.config_editor.ERROR_IMPORT_WIDGET.format( + error_info) + self.reporter( + rose.config_editor.util.ImportWidgetError(text)) + self.generate_sub_data_panel(override_custom=True) + + def update_sub_data(self): + """Update the sub (summary) data panel.""" + if self.sub_data is None: + if (hasattr(self, "sub_data_panel") and + self.sub_data_panel is not None): + self.vpaned.remove(self.sub_data_panel) + self.sub_data_panel.destroy() + self.sub_data_panel = None + else: + if (hasattr(self, "sub_data_panel") and + self.sub_data_panel is not None): + self.sub_data_panel.update(self.sub_data["sections"], + self.sub_data["variables"]) + + def launch_add_menu(self, button, my_time): + """Pop up a contextual add variable menu.""" + add_menu = self.get_add_menu() + if add_menu is None: + return False + add_menu.popup(None, None, None, button, my_time) + return False + + def get_add_menu(self): + def _add_var_from_item(item): + for variable in self.ghost_data: + if variable.metadata['id'] == item.var_id: + self.add_row(variable) + return + add_ui_start = """ + """ + add_ui_end = """ """ + actions = [('Add meta', Gtk.STOCK_DIRECTORY, + rose.config_editor.ADD_MENU_META)] + section_choices = [] + for sect_data in self.sections: + if not sect_data.ignored_reason: + section_choices.append(sect_data.name) + section_choices.sort(rose.config.sort_settings) + if self.ns_is_default and section_choices: + add_ui_start = add_ui_start.replace( + "'Popup'>", + """'Popup'>""") + text = rose.config_editor.ADD_MENU_BLANK + if len(section_choices) > 1: + text = rose.config_editor.ADD_MENU_BLANK_MULTIPLE + actions.insert(0, ('Add blank', Gtk.STOCK_NEW, text)) + ghost_list = [v for v in self.ghost_data] + sorter = rose.config.sort_settings + ghost_list.sort(lambda v, w: sorter(v.metadata['id'], + w.metadata['id'])) + for variable in ghost_list: + label_text = variable.name + if (not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE] and + rose.META_PROP_TITLE in variable.metadata): + label_text = variable.metadata[rose.META_PROP_TITLE] + label_text = label_text.replace("_", "__") + add_ui_start += ('') + actions.append((variable.metadata['id'], None, + "_" + label_text)) + add_ui = add_ui_start + add_ui_end + uimanager = Gtk.UIManager() + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.add_actions(actions) + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(add_ui) + if 'Add blank' in add_ui: + blank_item = uimanager.get_widget('/Popup/Add blank') + if len(section_choices) > 1: + blank_item.connect( + "activate", + lambda b: self._launch_section_chooser(section_choices)) + else: + blank_item.connect("activate", lambda b: self.add_row()) + for variable in ghost_list: + named_item = uimanager.get_widget( + '/Popup/Add meta/' + variable.metadata['id']) + if not named_item: + return None + named_item.var_id = variable.metadata['id'] + tooltip_text = "" + description = variable.metadata.get(rose.META_PROP_DESCRIPTION) + if description: + tooltip_text += description + "\n" + tooltip_text += "(" + variable.metadata["id"] + ")" + named_item.set_tooltip_text(tooltip_text) + named_item.connect("activate", _add_var_from_item) + if 'Add blank' in add_ui or self.ghost_data: + return uimanager.get_widget('/Popup') + return None + + def _launch_section_chooser(self, section_choices): + """Choose a section to add a blank variable to.""" + section = rose.gtk.dialog.run_choices_dialog( + rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR, + section_choices, + rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + if section is not None: + self.add_row(section=section) + + def add_row(self, variable=None, section=None): + """Append a new variable to the page's main variable list. + + If variable is None, a blank name/value/metadata variable is added. + This is only allowed where there are not multiple config sections + represented in the namespace, as otherwise the location of the + variable in the configuration data is badly defined. + + """ + if variable is None: + if self.section is None and section is None: + return False + creation_time = str(time.time()).replace('.', '_') + if section is None: + sect = self.section.name + else: + sect = section + v_id = sect + '=null' + creation_time + variable = rose.variable.Variable('', '', + {'id': v_id, + 'full_ns': self.namespace}) + if section is None and self.section.ignored_reason: + # Cannot add to an ignored section. + return False + self.variable_ops.add_var(variable) + if hasattr(self.main_container, 'add_variable_widget'): + self.main_container.add_variable_widget(variable) + self.trigger_update_status() + self.update_ignored() + else: + self.refresh() + self.update_ignored(no_refresh=True) + self.set_main_focus(variable.metadata.get('id')) + + def generate_main_container(self, override_custom=False): + """Choose a container to interface with variables in panel_data.""" + if self.custom_widget is not None and not override_custom: + widget_name_args = self.custom_widget.split(None, 1) + if len(widget_name_args) > 1: + widget_path, widget_args = widget_name_args + else: + widget_path, widget_args = widget_name_args[0], None + metadata_files = self.section_ops.get_ns_metadata_files( + self.namespace) + custom_widget = rose.resource.import_object( + widget_path, + metadata_files, + self.handle_bad_custom_main_widget) + if custom_widget is None: + text = rose.config_editor.ERROR_IMPORT_CLASS.format( + widget_path) + self.handle_bad_custom_main_widget(text) + return + try: + self.main_container = custom_widget(self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes, + arg_str=widget_args) + except Exception as exc: + self.handle_bad_custom_main_widget(exc) + else: + return + std_table = rose.config_editor.pagewidget.table.PageTable + disc_table = rose.config_editor.pagewidget.table.PageLatentTable + if self.namespace == "/discovery": + self.main_container = disc_table(self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes) + else: + self.main_container = std_table(self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes) + + def handle_bad_custom_main_widget(self, error_info): + """Handle a bad custom page widget import.""" + text = rose.config_editor.ERROR_IMPORT_WIDGET.format( + error_info) + self.reporter.report( + rose.config_editor.util.ImportWidgetError(text)) + self.generate_main_container(override_custom=True) + + def validate_errors(self, variable_id=None): + """Check if there are there errors in variables on this page.""" + if variable_id is None: + bad_list = [] + for variable in self.panel_data + self.ghost_data: + bad_list += list(variable.error.items()) + return bad_list + else: + for variable in self.panel_data + self.ghost_data: + if variable.metadata.get('id') == variable_id: + if variable.error == {}: + return None + return list(variable.error.items()) + return None + + def choose_focus(self, focus_variable=None): + """Select a widget to have the focus on page generation.""" + if self.custom_widget is not None: + return + if self.show_modes[rose.config_editor.SHOW_MODE_LATENT]: + for widget in self.get_main_variable_widgets(): + if hasattr(widget.get_parent(), 'variable'): + if widget.get_parent().variable.name == '': + widget.get_parent().grab_focus() + return + names = [v.name for v in (self.panel_data + self.ghost_data)] + if focus_variable is None or focus_variable.name not in names: + return + if self.panel_data: + for widget in self.get_main_variable_widgets(): + if hasattr(widget.get_parent(), 'variable'): + var = widget.get_parent().variable + if var.name == focus_variable.name: + if (var.metadata.get('id') == + focus_variable.metadata.get('id')): + widget.get_parent().grab_focus() + return + + def refresh(self, only_this_var_id=None): + """Reload the page or selectively refresh widgets for one variable.""" + if only_this_var_id is None: + self.generate_page_info() + return self.sort_main(remake_forced=True) + variable = None + for variable in self.panel_data + self.ghost_data: + if variable.metadata['id'] == only_this_var_id: + break + else: + return self.sort_main(remake_forced=True) + var_id = variable.metadata['id'] + widget_for_var = {} + for widget in self.get_main_variable_widgets(): + if hasattr(widget, 'variable'): + target_id = widget.variable.metadata['id'] + target_widget = widget + else: + target_id = widget.get_parent().variable.metadata['id'] + target_widget = widget.get_parent() + widget_for_var.update({target_id: target_widget}) + if variable in self.panel_data: + if var_id in widget_for_var: + widget = widget_for_var[var_id] + if widget.is_ghost: + # Then it is an added ghost variable. + return self.handle_add_var_widget(variable) + # Then it has an existing variable widget. + if ((rose.META_PROP_TYPE in widget.errors) != + (rose.META_PROP_TYPE in variable.error) and + hasattr(widget, "needs_type_error_refresh") and + not widget.needs_type_error_refresh()): + return widget.type_error_refresh(variable) + else: + return self.handle_reload_var_widget(variable) + # Then there were no widgets for this variable. Insert it. + return self.handle_add_var_widget(variable) + else: + if var_id in widget_for_var and widget_for_var[var_id].is_ghost: + # It is a latent variable that needs a refresh. + return self.handle_reload_var_widget(variable) + # It is a normal variable that has been removed. + return self.handle_remove_var_widget(variable) + + def handle_add_var_widget(self, variable): + if hasattr(self.main_container, 'add_variable_widget'): + self.main_container.add_variable_widget(variable) + self.update_ignored() + else: + self.refresh() + self.set_main_focus(variable.metadata.get('id')) + + def handle_reload_var_widget(self, variable): + if hasattr(self.main_container, 'reload_variable_widget'): + self.main_container.reload_variable_widget(variable) + self.update_ignored() + else: + self.refresh() + + def handle_remove_var_widget(self, variable): + if hasattr(self.main_container, 'remove_variable_widget'): + self.main_container.remove_variable_widget(variable) + self.update_ignored() + else: + self.refresh() + + def sort_main(self, column_index=0, ascending=True, + remake_forced=False): + """Regenerate a sorted table, according to the arguments. + + column_index maps as {0: index, 1: title, 2: key, 3: value}. + ascending specifies whether to use 'normal' cmp or 'reversed' cmp + arguments. + + """ + if self.sort_data(column_index, ascending) or remake_forced: + focus_var = None + focus_widget = self.get_toplevel().focus_child + if (focus_widget is not None and + hasattr(focus_widget.get_parent(), 'variable')): + focus_var = focus_widget.get_parent().variable + self.main_container.destroy() + self.generate_main_container() + self.scrolled_vbox.pack_start(self.main_container, expand=False, + fill=True) + self.choose_focus(focus_var) + self.update_ignored(no_refresh=True) + self.trigger_update_status() + + def get_main_variable_widgets(self): + """Return the widgets within the main_container.""" + return self.get_widgets_with_attribute('variable') + + def get_widgets_with_attribute(self, att_name, parent_widget=None): + """Return widgets with a certain named attribute.""" + if parent_widget is None: + widget_list = self.main_container.get_children() + else: + widget_list = parent_widget.get_children() + i = 0 + while i < len(widget_list): + widget = widget_list[i] + if not (hasattr(widget.get_parent(), att_name) or + hasattr(widget, att_name)): + widget_list.pop(i) + i -= 1 + if hasattr(widget, 'get_children'): + widget_list.extend(widget.get_children()) + elif hasattr(widget, 'get_child'): + widget_list.append(widget.get_child()) + i += 1 + return widget_list + + def get_main_focus(self): + """Retrieve the focus variable widget id.""" + widget_list = self.get_main_variable_widgets() + focus_child = getattr(self.main_container, "focus_child") + for widget in widget_list: + if focus_child == widget: + if hasattr(widget.get_parent(), 'variable'): + return widget.get_parent().variable.metadata['id'] + elif hasattr(widget, 'variable'): + return widget.variable.metadata['id'] + return None + + def set_main_focus(self, var_id): + """Set the main focus on the key-matched variable widget.""" + widget_list = self.get_main_variable_widgets() + for widget in widget_list: + if (hasattr(widget.get_parent(), 'variable') and + widget.get_parent().variable.metadata['id'] == var_id): + widget.get_parent().grab_focus(self.main_container) + return True + for widget in widget_list: + if (hasattr(widget, 'variable') and + widget.variable.metadata['id'] == var_id): + widget.grab_focus() + return True + return False + + def set_sub_focus(self, node_id): + if (self.sub_data is not None and + hasattr(self, "sub_data_panel") and + hasattr(self.sub_data_panel, "set_focus_node_id")): + self.sub_data_panel.set_focus_node_id(node_id) + + def react_to_show_modes(self, mode_key, is_mode_on): + self.show_modes[mode_key] = is_mode_on + if hasattr(self.main_container, 'show_mode_change'): + self.update_ignored() + react_func = getattr(self.main_container, 'show_mode_change') + react_func(mode_key, is_mode_on) + elif mode_key in [rose.config_editor.SHOW_MODE_IGNORED, + rose.config_editor.SHOW_MODE_USER_IGNORED]: + self.update_ignored() + else: + self.refresh() + + def refresh_widget_status(self): + """Refresh the status of all variable widgets.""" + for widget in self.get_widgets_with_attribute('update_status'): + if hasattr(widget.get_parent(), 'update_status'): + widget.get_parent().update_status() + else: + widget.update_status() + + def update_ignored(self, no_refresh=False): + """Set variable widgets to 'ignored' or 'enabled' status.""" + new_tuples = [] + for variable in self.panel_data + self.ghost_data: + if variable.ignored_reason: + new_tuples.append((variable.metadata['id'], + variable.ignored_reason.copy())) + target_widgets_done = [] + refresh_list = [] + relevant_errs = rose.config_editor.WARNING_TYPES_IGNORE + for widget in self.get_main_variable_widgets(): + if hasattr(widget.get_parent(), 'variable'): + target = widget.get_parent() + elif hasattr(widget, 'variable'): + target = widget + else: + continue + if target in target_widgets_done: + continue + for var_id, help_text in [x for x in new_tuples]: + if target.variable.metadata.get('id') == var_id: + self._set_widget_ignored(target, help_text) + new_tuples.remove((var_id, help_text)) + break + else: + if hasattr(target, 'is_ignored') and target.is_ignored: + self._set_widget_ignored(target, '', enabled=True) + if (any(e in target.errors for e in relevant_errs) or + any(e in target.variable.error for e in relevant_errs)): + if ([e in target.errors for e in relevant_errs] != + [e in target.variable.error for e in relevant_errs]): + refresh_list.append(target.variable.metadata['id']) + target.errors = list(target.variable.error.keys()) + target_widgets_done.append(target) + if hasattr(self.main_container, "update_ignored"): + self.main_container.update_ignored() + elif not no_refresh: + self.refresh() + for variable_id in refresh_list: + self.refresh(variable_id) + + def _check_show_ignored_reason(self, ignored_reason): + """Return whether we should show this state.""" + mode = self.show_modes + if list(ignored_reason.keys()) == [rose.variable.IGNORED_BY_USER]: + return (mode[rose.config_editor.SHOW_MODE_IGNORED] or + mode[rose.config_editor.SHOW_MODE_USER_IGNORED]) + return mode[rose.config_editor.SHOW_MODE_IGNORED] + + def _set_widget_ignored(self, widget, help_text, enabled=False): + if self._check_show_ignored_reason(widget.variable.ignored_reason): + if hasattr(widget, 'set_ignored'): + widget.set_ignored() + elif hasattr(widget, 'set_sensitive'): + widget.set_sensitive(enabled) + else: + if hasattr(widget, 'hide') and hasattr(widget, 'show'): + if hasattr(widget, 'set_ignored'): + widget.set_ignored() + elif hasattr(widget, 'set_sensitive'): + widget.set_sensitive(enabled) + + def reload_from_data(self, new_config_data, new_ghost_data): + """Load the new data into the page as gracefully as possible.""" + for variable in [v for v in self.panel_data]: + # Remove redundant existing variables + var_id = variable.metadata.get('id') + new_id_list = [x.metadata['id'] for x in new_config_data] + if var_id not in new_id_list or var_id is None: + self.variable_ops.remove_var(variable) + for variable in [v for v in self.ghost_data]: + # Remove redundant metadata variables. + var_id = variable.metadata.get('id') + new_id_list = [x.metadata['id'] for x in new_ghost_data] + if var_id not in new_id_list: + self.variable_ops.remove_var(variable) # From the ghost list. + for variable in new_config_data: + # Update or add variables + var_id = variable.metadata['id'] + old_id_list = [x.metadata.get('id') for x in self.panel_data] + if var_id in old_id_list: + old_variable = self.panel_data[old_id_list.index(var_id)] + old_variable.metadata = variable.metadata + if old_variable.value != variable.value: + # Reset the value. + self.variable_ops.set_var_value(old_variable, + variable.value) + if old_variable.comments != variable.comments: + self.variable_ops.set_var_comments(old_variable, + variable.comments) + old_ign_set = set(old_variable.ignored_reason.keys()) + new_ign_set = set(variable.ignored_reason.keys()) + if old_ign_set != new_ign_set: + # Reset the ignored state. + self.variable_ops.set_var_ignored( + old_variable, + variable.ignored_reason.copy(), + override=True) + else: + # The types are the same, but pass on the info. + old_variable.ignored_reason = ( + variable.ignored_reason.copy()) + old_variable.error = variable.error.copy() + old_variable.warning = variable.warning.copy() + else: + self.variable_ops.add_var(variable.copy()) + for variable in new_ghost_data: + # Update or remove variables + var_id = variable.metadata['id'] + old_id_list = [x.metadata.get('id') + for x in self.ghost_data] + if var_id in old_id_list: + index = old_id_list.index(var_id) + old_variable = self.ghost_data[index] + old_variable.metadata = variable.metadata.copy() + old_variable.ignored_reason = variable.ignored_reason.copy() + if old_variable.value != variable.value: + old_variable.value = variable.value + old_variable.error = variable.error.copy() + old_variable.warning = variable.warning.copy() + else: + self.ghost_data.append(variable.copy()) + self.refresh() + self.trigger_update_status() + return False + + def sort_data(self, column_index=0, ascending=True, ghost=False): + """Sort page data by an attribute specified with column_index. + + The column_index maps to attributes like this - + {0: index, 1:title, 2:key, 3:value}, where index is the metadata + index (or null string if there isn't one) plus the key. Sorting + does not affect the undo stack. + + """ + sorted_data = [] + if ghost: + datavars = self.ghost_data + else: + datavars = self.panel_data + for variable in datavars: + title = variable.metadata.get(rose.META_PROP_TITLE, variable.name) + var_id = variable.metadata.get('id', variable.name) + key = ( + variable.metadata.get(rose.META_PROP_SORT_KEY, '~'), + var_id + ) + if variable.name == '': + key = ('~', '') + sorted_data.append((key, title, variable.name, + variable.value, variable)) + ascending_cmp = lambda x, y: rose.config_editor.util.null_cmp( + x[0], y[0]) + descending_cmp = lambda x, y: rose.config_editor.util.null_cmp( + x[0], y[0]) + if ascending: + sorted_data.sort(ascending_cmp) + else: + sorted_data.sort(descending_cmp) + if [x[4] for x in sorted_data] == datavars: + return False + for i, datum in enumerate(sorted_data): + datavars[i] = datum[4] # variable + return True + + def _macro_menu_launch(self, widget, event): + # Create a menu below the widget for macro actions. + menu = Gtk.Menu() + for macro_name, info in sorted(self.custom_macros.items()): + method, description = info + if method == rose.macro.TRANSFORM_METHOD: + stock_id = Gtk.STOCK_CONVERT + else: + stock_id = Gtk.STOCK_DIALOG_QUESTION + macro_menuitem = Gtk.ImageMenuItem(stock_id=stock_id) + macro_menuitem.set_label(macro_name) + macro_menuitem.set_tooltip_text(description) + macro_menuitem.show() + macro_menuitem._macro = macro_name + macro_menuitem.connect( + "button-release-event", + lambda m, e: self.launch_macro(m._macro)) + menu.append(macro_menuitem) + menu.popup(None, None, widget.position_menu, event.button, + event.time, widget) + + def launch_macro(self, macro_name_string): + """Launch a macro, if possible.""" + class_name = None + method_name = None + if "." in macro_name_string: + module_name, class_name = macro_name_string.split(".", 1) + if "." in class_name: + class_name, method_name = class_name.split(".", 1) + else: + module_name = macro_name_string + self._launch_macro_func( + config_name=self.config_name, + module_name=module_name, + class_name=class_name, + method_name=method_name) + + def search_for_id(self, id_): + """Launch a search for variable or section id.""" + return self.variable_ops.search_for_var(self.namespace, id_) + + def trigger_update_status(self): + """Connect this at a higher level to allow changed data signals.""" + pass + + def _get_page_info_widgets(self): + button_list = [] + label_list = [] + info = "" + # No content warning, if applicable. + has_no_content = (self.section is None and + not self.ghost_data and + self.sub_data is None and + not self.latent_sections) + if has_no_content: + info = rose.config_editor.PAGE_WARNING_NO_CONTENT + tip = rose.config_editor.PAGE_WARNING_NO_CONTENT_TIP + error_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_INFO, + as_tool=True, + tip_text=tip) + error_label = Gtk.Label() + error_label.set_text(info) + error_label.show() + button_list.append(error_button) + label_list.append(error_label) + if self.section is not None and self.section.ignored_reason: + # This adds an ignored warning. + info = rose.config_editor.PAGE_WARNING_IGNORED_SECTION.format( + self.section.name) + tip = rose.config_editor.PAGE_WARNING_IGNORED_SECTION_TIP + error_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_NO, + as_tool=True, + tip_text=tip) + error_label = Gtk.Label() + error_label.set_text(info) + error_label.show() + button_list.append(error_button) + label_list.append(error_label) + elif self.see_also == '' or rose.FILE_VAR_SOURCE not in self.see_also: + # This adds an 'orphaned' warning, only if the section is enabled. + if (self.section is not None and + self.section.name.startswith('namelist:')): + error_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_DIALOG_WARNING, + as_tool=True, + tip_text=rose.config_editor.ERROR_ORPHAN_SECTION_TIP) + error_label = Gtk.Label() + info = rose.config_editor.ERROR_ORPHAN_SECTION.format( + self.section.name) + error_label.set_text(info) + error_label.show() + button_list.append(error_button) + label_list.append(error_label) + has_data = (has_no_content or + self.sub_data is not None or + bool(self.panel_data)) + if not has_data: + for section in self.sections: + if section.metadata["full_ns"] == self.namespace: + has_data = True + break + if not has_data: + # This is a latent namespace page. + latent_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_INFO, + as_tool=True, + tip_text=rose.config_editor.TIP_LATENT_PAGE) + latent_label = Gtk.Label() + latent_label.set_text(rose.config_editor.PAGE_WARNING_LATENT) + latent_label.show() + button_list.append(latent_button) + label_list.append(latent_label) + # This adds error notification for sections. + for sect_data in self.sections + self.latent_sections: + for err, info in list(sect_data.error.items()): + error_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_DIALOG_ERROR, + as_tool=True, + tip_text=info) + error_label = Gtk.Label() + error_label.set_text(rose.config_editor.PAGE_WARNING.format( + err, sect_data.name)) + error_label.show() + button_list.append(error_button) + label_list.append(error_label) + if list(self.custom_macros.items()): + macro_button = rose.gtk.util.CustomButton( + label=rose.config_editor.LABEL_PAGE_MACRO_BUTTON, + stock_id=Gtk.STOCK_EXECUTE, + tip_text=rose.config_editor.TIP_MACRO_RUN_PAGE, + as_tool=True, icon_at_start=True, + has_menu=True) + macro_button.connect("button-press-event", + self._macro_menu_launch) + macro_label = Gtk.Label() + macro_label.show() + button_list.append(macro_button) + label_list.append(macro_label) + return button_list, label_list, info diff --git a/metomi/rose/config_editor/pagewidget/__init__.py b/metomi/rose/config_editor/pagewidget/__init__.py new file mode 100644 index 000000000..bbe46a78b --- /dev/null +++ b/metomi/rose/config_editor/pagewidget/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +from . import table diff --git a/metomi/rose/config_editor/pagewidget/table.py b/metomi/rose/config_editor/pagewidget/table.py new file mode 100644 index 000000000..a96dfeb50 --- /dev/null +++ b/metomi/rose/config_editor/pagewidget/table.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import shlex + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.config_editor.util +import rose.config_editor.variable +import rose.formats +import rose.variable + + +class PageTable(Gtk.Table): + + """Return a widget table generated from panel_data. + + It uses the variable information to create instances of + VariableWidget, which are then asked to insert themselves into the + table. + + """ + + MAX_ROWS = 2000 + MAX_COLS = 3 + BORDER_WIDTH = rose.config_editor.SPACING_SUB_PAGE + + def __init__(self, panel_data, ghost_data, var_ops, show_modes, + arg_str=None): + super(PageTable, self).__init__(rows=self.MAX_ROWS, + columns=self.MAX_COLS, + homogeneous=False) + self.panel_data = panel_data + self.ghost_data = ghost_data + self.var_ops = var_ops + self.show_modes = show_modes + variable_is_ghost_list = self._get_sorted_variables() + self.attach_variable_widgets(variable_is_ghost_list, start_index=0) + self._show_and_hide_variable_widgets() + self.show() + + def add_variable_widget(self, variable): + """Add a variable widget that was previously in ghost_data.""" + new_variable_widget = self.get_variable_widget(variable) + widget_coordinate_list = [] + for child in self.get_children(): + top_row = self.child_get(child, 'top_attach')[0] + variable_widget = child.get_parent() + if variable_widget not in [x[0] for x in widget_coordinate_list]: + widget_coordinate_list.append((variable_widget, top_row)) + widget_coordinate_list.sort(lambda x, y: cmp(x[1], y[1])) + old_index = None + for widget, index in widget_coordinate_list: + if widget.variable.metadata["id"] == variable.metadata["id"]: + old_index = index + break + if old_index is None: + variable_is_ghost_list = self._get_sorted_variables() + variables = [x[0] for x in variable_is_ghost_list] + num_vars_above_this = variables.index(variable) + if num_vars_above_this == 0: + row_above_new = -1 + else: + row_above_new = widget_coordinate_list[ + num_vars_above_this - 1][1] + for variable_widget, widget_row in widget_coordinate_list: + if widget_row > row_above_new: + for child in self.get_children(): + if child.get_parent() == variable_widget: + self.remove(child) + new_variable_widget.insert_into( + self, self.MAX_COLS, row_above_new + 1) + self._show_and_hide_variable_widgets(new_variable_widget) + rownum = row_above_new + 2 + for variable_widget, widget_row in widget_coordinate_list: + if (widget_row > row_above_new and + variable_widget.variable.metadata.get('id') != + variable.metadata.get('id')): + variable_widget.insert_into(self, self.MAX_COLS, rownum) + rownum += 1 + else: + self.reload_variable_widget(variable) + + def attach_variable_widgets(self, variable_is_ghost_list, start_index=0): + """Create and attach variable widgets for these inputs.""" + rownum = start_index + for variable, is_ghost in variable_is_ghost_list: + variable_widget = self.get_variable_widget(variable, is_ghost) + variable_widget.insert_into(self, self.MAX_COLS, rownum + 1) + variable_widget.set_sensitive(not is_ghost) + rownum += 1 + + def get_variable_widget(self, variable, is_ghost=False): + """Create a variable widget for this variable.""" + return rose.config_editor.variable.VariableWidget( + variable, + self.var_ops, + is_ghost=is_ghost, + show_modes=self.show_modes) + + def reload_variable_widget(self, variable): + """Reload the widgets for the given variable.""" + is_ghost = variable in self.ghost_data + new_variable_widget = self.get_variable_widget(variable, is_ghost) + new_variable_widget.set_sensitive(not is_ghost) + focus_dict = {"had_focus": False} + variable_row = None + for child in self.get_children(): + variable_widget = child.get_parent() + if (variable_widget.variable.name == variable.name and + variable_widget.variable.metadata.get('id') == + variable.metadata.get('id')): + if "index" not in focus_dict: + focus_dict["index"] = variable_widget.get_focus_index() + if getattr(self, 'focus_child') == child: + focus_dict["had_focus"] = True + top_row = self.child_get(child, 'top_attach')[0] + variable_row = top_row + self.remove(child) + child.destroy() + if variable_row is None: + return False + new_variable_widget.insert_into(self, self.MAX_COLS, variable_row) + self._show_and_hide_variable_widgets(new_variable_widget) + if focus_dict["had_focus"]: + new_variable_widget.grab_focus(index=focus_dict.get("index")) + + def remove_variable_widget(self, variable): + """Remove the selected widget and/or relocate to ghosts.""" + self.reload_variable_widget(variable) + + def _get_sorted_variables(self): + sort_key_vars = [] + for val in self.panel_data + self.ghost_data: + sort_key = ( + (val.metadata.get("sort-key", "~")), val.metadata["id"]) + is_ghost = val in self.ghost_data + sort_key_vars.append((sort_key, val, is_ghost)) + sort_key_vars.sort(rose.config_editor.util.null_cmp) + sort_key_vars.sort(lambda x, y: cmp("=null" in x[1].metadata["id"], + "=null" in y[1].metadata["id"])) + return [(x[1], x[2]) for x in sort_key_vars] + + def _show_and_hide_variable_widgets(self, just_this_widget=None): + """Figure out whether to display a widget or not.""" + modes = self.show_modes + if just_this_widget: + variable_widgets = [just_this_widget] + else: + variable_widgets = [] + for child in self.get_children(): + if child.get_parent() not in variable_widgets: + variable_widgets.append(child.get_parent()) + for variable_widget in variable_widgets: + variable = variable_widget.variable + ign_reason = variable.ignored_reason + if variable.error: + variable_widget.show() + elif (len(variable.metadata.get( + rose.META_PROP_VALUES, [])) == 1 and + not modes[rose.config_editor.SHOW_MODE_FIXED]): + variable_widget.hide() + elif (variable_widget.is_ghost and + not modes[rose.config_editor.SHOW_MODE_LATENT]): + variable_widget.hide() + elif ((rose.variable.IGNORED_BY_SYSTEM in ign_reason or + rose.variable.IGNORED_BY_SECTION in ign_reason) and + not modes[rose.config_editor.SHOW_MODE_IGNORED]): + variable_widget.hide() + elif (rose.variable.IGNORED_BY_USER in ign_reason and + not (modes[rose.config_editor.SHOW_MODE_IGNORED] or + modes[rose.config_editor.SHOW_MODE_USER_IGNORED])): + variable_widget.hide() + else: + variable_widget.show() + + def show_mode_change(self, mode, mode_on=False): + done_variable_widgets = [] + for child in self.get_children(): + parent = child.get_parent() + if parent in done_variable_widgets: + continue + parent.set_show_mode(mode, mode_on) + done_variable_widgets.append(parent) + self._show_and_hide_variable_widgets() + + def update_ignored(self): + self._show_and_hide_variable_widgets() + + +class PageArrayTable(PageTable): + + """Return a widget table that treats array values as row elements.""" + + def __init__(self, *args, **kwargs): + arg_str = kwargs.get("arg_str", "") + if arg_str is None: + arg_str = "" + self.headings = shlex.split(arg_str) + super(PageArrayTable, self).__init__(*args, **kwargs) + self._set_length() + + def attach_variable_widgets(self, variable_is_ghost_list, start_index=0): + """Create and attach variable widgets for these inputs.""" + self._set_length() + rownum = start_index + for variable, is_ghost in variable_is_ghost_list: + variable_widget = self.get_variable_widget(variable, is_ghost) + variable_widget.insert_into(self, self.MAX_COLS, rownum + 1) + variable_widget.set_sensitive(not is_ghost) + rownum += 1 + + def get_variable_widget(self, variable, is_ghost=False): + """Create a variable widget for this variable.""" + if (rose.META_PROP_LENGTH in variable.metadata or + isinstance(variable.metadata.get(rose.META_PROP_TYPE), list)): + return rose.config_editor.variable.RowVariableWidget( + variable, + self.var_ops, + is_ghost=is_ghost, + show_modes=self.show_modes, + length=self.array_length) + return rose.config_editor.variable.VariableWidget( + variable, + self.var_ops, + is_ghost=is_ghost, + show_modes=self.show_modes) + + def _set_length(self): + max_meta_length = 0 + max_values_length = 0 + for variable in self.panel_data + self.ghost_data: + length = variable.metadata.get(rose.META_PROP_LENGTH) + if (length is not None and length.isdigit() and + int(length) > max_meta_length): + max_meta_length = int(length) + types = variable.metadata.get(rose.META_PROP_TYPE) + if isinstance(types, list) and len(types) > max_meta_length: + max_meta_length = len(types) + values_length = len(rose.variable.array_split(variable.value)) + if values_length > max_values_length: + max_values_length = values_length + self.array_length = max([max_meta_length, max_values_length]) + + +class PageLatentTable(Gtk.Table): + + """Return a widget table generated from panel_data. + + It uses the variable information to create instances of + VariableWidget, which are then asked to insert themselves into the + table. + + This particular container always shows latent variables. + + """ + + MAX_ROWS = 2000 + MAX_COLS = 3 + + def __init__(self, panel_data, ghost_data, var_ops, show_modes, + arg_str=None): + super(PageLatentTable, self).__init__( + rows=self.MAX_ROWS, columns=self.MAX_COLS, homogeneous=False) + self.show() + self.num_removes = 0 + self.panel_data = panel_data + self.ghost_data = ghost_data + self.var_ops = var_ops + self.show_modes = show_modes + self.title_on = ( + not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]) + self.alt_menu_class = rose.config_editor.menuwidget.CheckedMenuWidget + rownum = 0 + v_sort_ids = [] + for val in self.panel_data + self.ghost_data: + v_sort_ids.append((val.metadata.get("sort-key", ""), + val.metadata["id"])) + v_sort_ids.sort( + lambda x, y: rose.config.sort_settings( + x[0] + "~" + x[1], y[0] + "~" + y[1])) + v_sort_ids.sort(lambda x, y: cmp("=null" in x[1], "=null" in y[1])) + for _, var_id in v_sort_ids: + is_ghost = False + for variable in self.panel_data: + if variable.metadata['id'] == var_id: + break + else: + for variable in self.ghost_data: + if variable.metadata['id'] == var_id: + is_ghost = True + break + variable_widget = self.get_variable_widget( + variable, is_ghost=is_ghost) + variable_widget.insert_into(self, self.MAX_COLS, rownum + 1) + variable_widget.set_sensitive(not is_ghost) + rownum += 1 + + def get_variable_widget(self, variable, is_ghost=False): + """Create a variable widget for this variable.""" + return rose.config_editor.variable.VariableWidget( + variable, self.var_ops, is_ghost=is_ghost, + show_modes=self.show_modes) + + def reload_variable_widget(self, variable): + """Reload the widgets for the given variable.""" + is_ghost = variable in self.ghost_data + new_variable_widget = self.get_variable_widget(variable, is_ghost) + new_variable_widget.set_sensitive(not is_ghost) + focus_dict = {"had_focus": False} + variable_row = None + for child in self.get_children(): + variable_widget = child.get_parent() + if (variable_widget.variable.name == variable.name and + variable_widget.variable.metadata.get('id') == + variable.metadata.get('id')): + if "index" not in focus_dict: + focus_dict["index"] = variable_widget.get_focus_index() + if getattr(self, 'focus_child') == child: + focus_dict["had_focus"] = True + top_row = self.child_get(child, 'top_attach')[0] + variable_row = top_row + self.remove(child) + child.destroy() + if variable_row is None: + return False + new_variable_widget.insert_into(self, self.MAX_COLS, variable_row) + if focus_dict["had_focus"]: + new_variable_widget.grab_focus(index=focus_dict.get("index")) + + def show_mode_change(self, mode, mode_on=False): + done_variable_widgets = [] + for child in self.get_children(): + parent = child.get_parent() + if parent in done_variable_widgets: + continue + parent.set_show_mode(mode, mode_on) + done_variable_widgets.append(parent) + + def refresh(self, var_id=None): + """Reload the container - don't need this at the moment.""" + pass + + def update_ignored(self): + """Update ignored statuses - no need to do anything extra here.""" + pass diff --git a/metomi/rose/config_editor/panelwidget/__init__.py b/metomi/rose/config_editor/panelwidget/__init__.py new file mode 100644 index 000000000..2792550c0 --- /dev/null +++ b/metomi/rose/config_editor/panelwidget/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +from . import filesystem +from . import summary_data diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py new file mode 100644 index 000000000..18dd0501a --- /dev/null +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import os + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +import rose.external +import rose.gtk.dialog +import rose.gtk.util + + +class FileSystemPanel(Gtk.ScrolledWindow): + + """A class to show underlying files and directories in a Gtk.TreeView.""" + + def __init__(self, directory): + super(FileSystemPanel, self).__init__() + self.directory = directory + view = Gtk.TreeView() + store = Gtk.TreeStore(str, str) + dirpath_iters = {self.directory: None} + for dirpath, dirnames, filenames in os.walk(self.directory): + if dirpath not in dirpath_iters: + known_path = os.path.dirname(dirpath) + new_iter = store.append(dirpath_iters[known_path], + [os.path.basename(dirpath), + os.path.abspath(dirpath)]) + dirpath_iters.update({dirpath: new_iter}) + this_iter = dirpath_iters[dirpath] + filenames.sort() + for name in filenames: + if name in rose.CONFIG_NAMES: + continue + filepath = os.path.join(dirpath, name) + store.append(this_iter, [name, os.path.abspath(filepath)]) + for dirname in list(dirnames): + if (dirname.startswith(".") or dirname in [ + rose.SUB_CONFIGS_DIR, rose.CONFIG_META_DIR]): + dirnames.remove(dirname) + dirnames.sort() + view.set_model(store) + col = Gtk.TreeViewColumn() + col.set_title(rose.config_editor.TITLE_FILE_PANEL) + cell = Gtk.CellRendererText() + col.pack_start(cell, True, True, 0) + col.set_cell_data_func(cell, + self._set_path_markup, store) + view.append_column(col) + view.expand_all() + view.show() + view.connect("row-activated", self._handle_activation) + view.connect("button-press-event", self._handle_click) + self.add(view) + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.show() + + def _set_path_markup(self, column, cell, model, r_iter, treestore): + title = model.get_value(r_iter, 0) + title = rose.gtk.util.safe_str(title) + cell.set_property("markup", title) + + def _handle_activation(self, view=None, path=None, col=None): + target_func = rose.external.launch_fs_browser + if path is None: + target = self.directory + else: + model = view.get_model() + row_iter = model.get_iter(path) + fs_path = model.get_value(row_iter, 1) + target = fs_path + if not os.path.isdir(target): + target_func = rose.external.launch_geditor + try: + target_func(target) + except Exception as exc: + rose.gtk.dialog.run_exception_dialog(exc) + + def _handle_click(self, view, event): + pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) + if (event.button == 1 and event.type == Gdk._2BUTTON_PRESS and + pathinfo is None): + self._handle_activation() + if event.button == 3: + ui_string = """ + + """ + actions = [('Open', Gtk.STOCK_OPEN, + rose.config_editor.FILE_PANEL_MENU_OPEN)] + uimanager = Gtk.UIManager() + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.add_actions(actions) + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(ui_string) + if pathinfo is None: + path = None + col = None + else: + path, col = pathinfo[:2] + open_item = uimanager.get_widget('/Popup/Open') + open_item.connect( + "activate", + lambda m: self._handle_activation(view, path, col)) + this_menu = uimanager.get_widget('/Popup') + this_menu.popup(None, None, None, event.button, event.time) diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py new file mode 100644 index 000000000..3b36cc9f2 --- /dev/null +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -0,0 +1,865 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Pango + +import rose.config +import rose.config_editor +import rose.config_editor.util +import rose.gtk.util + + +class BaseSummaryDataPanel(Gtk.VBox): + + """A base class for summarising data across many namespaces. + + Subclasses should provide the following methods: + - def add_cell_renderer_for_value(self, column, column_title): + - def get_model_data(self): + - def get_section_column_index(self): + - def set_tree_cell_status(self, column, cell, model, row_iter): + - def set_tree_tip(self, treeview, row_iter, col_index, tip): + + Subclasses may provide the following methods: + - def _get_custom_menu_items(self, path, column, event): + + These are described below in their placeholder methods. + + """ + + def __init__(self, sections, variables, sect_ops, var_ops, + search_function, sub_ops, + is_duplicate, arg_str=None): + super(BaseSummaryDataPanel, self).__init__() + self.sections = sections + self.variables = variables + self._section_data_list = None + self._last_column_names = [] + self.column_names = [] + self.sect_ops = sect_ops + self.var_ops = var_ops + self.search_function = search_function + self.sub_ops = sub_ops + self.is_duplicate = is_duplicate + self.group_index = None + self.util = rose.config_editor.util.Lookup() + self.control_widget_hbox = self._get_control_widget_hbox() + self.pack_start(self.control_widget_hbox, expand=False, fill=False) + self._prev_store = None + self._prev_sort_model = None + self._view = rose.gtk.util.TooltipTreeView( + get_tooltip_func=self.set_tree_tip, + multiple_selection=True) + self._view.set_rules_hint(True) + self.sort_util = rose.gtk.util.TreeModelSortUtil( + self._view.get_model, multi_sort_num=2) + self._view.show() + self._view.connect("button-press-event", + self._handle_button_press_event) + self._view.connect("key-press-event", + self._handle_key_press_event) + self._window = Gtk.ScrolledWindow() + self._window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.update() + self._window.add(self._view) + self._window.show() + self.pack_start(self._window, expand=True, fill=True) + self.show() + + def add_cell_renderer_for_value(self, column, column_title): + """Add a cell renderer to represent the model value. + + column is the Gtk.TreeColumn to pack the cell in. + column_title is the title of column. + + You may want to use column.set_cell_data_func. + + """ + raise NotImplementedError() + + def get_model_data(self): + """Return a list of data tuples, plus column names. + + The returned list should contain lists of items for each row. + The column names should be a list of strings for column titles. + + """ + raise NotImplementedError() + + def get_section_column_index(self): + """Return the section name column index from the Gtk.TreeView. + + This may change based on the grouping (self.group_index). + + """ + raise NotImplementedError() + + def set_tree_cell_status(self, column, cell, model, row_iter): + """Add status markup to the cell - e.g. error notification. + + column is the Gtk.TreeColumn where the cell is + cell is the Gtk.CellRendererText to add markup to + model is the Gtk.TreeModel-derived data store + row_iter is the Gtk.TreeIter pointing to the cell's row + + """ + raise NotImplementedError() + + def set_tree_tip(self, treeview, row_iter, col_index, tip): + """Add the hover-over text for a cell to 'tip'. + + treeview is the Gtk.TreeView object + row_iter is the Gtk.TreeIter for the row + col_index is the index of the Gtk.TreeColumn in + e.g. treeview.get_columns() + tip is the Gtk.Tooltip object that the text needs to be set in. + + """ + raise NotImplementedError() + + def _get_custom_menu_items(self, path, column, event): + """Override this method to add to the right click menu. + + This should return a list of Gtk.MenuItem subclass instances. + + """ + return [] + + def _get_control_widget_hbox(self): + filter_label = Gtk.Label(label= + rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + filter_label.show() + self._filter_widget = Gtk.Entry() + self._filter_widget.set_width_chars( + rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + self._filter_widget.connect("changed", self._refilter) + self._filter_widget.show() + group_label = Gtk.Label(label= + rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + group_label.show() + self._group_widget = Gtk.ComboBox() + cell = Gtk.CellRendererText() + self._group_widget.pack_start(cell, True, True, 0) + self._group_widget.add_attribute(cell, 'text', 0) + self._group_widget.connect("changed", self._handle_group_change) + self._group_widget.show() + filter_hbox = Gtk.HBox() + filter_hbox.pack_start(group_label, expand=False, fill=False) + filter_hbox.pack_start(self._group_widget, expand=False, fill=False) + filter_hbox.pack_start(filter_label, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + filter_hbox.pack_start(self._filter_widget, expand=False, fill=False) + filter_hbox.show() + return filter_hbox + + def update_tree_model(self): + """Construct a data model of other page data.""" + self.var_id_map = {} + for variables in list(self.variables.values()): + for variable in variables: + self.var_id_map[variable.metadata["id"]] = variable + data_rows, column_names = self.get_model_data() + data_rows, column_names, rows_are_descendants = self._apply_grouping( + data_rows, column_names, self.group_index) + self.column_names = column_names + should_redraw = self.column_names != self._last_column_names + if data_rows: + col_types = [str] * len(data_rows[0]) + else: + col_types = [] + need_new_store = (should_redraw or self.group_index) + if need_new_store: + # We need to construct a new TreeModel. + if self._prev_sort_model is not None: + prev_sort_id = self._prev_sort_model.get_sort_column_id() + store = Gtk.TreeStore(*col_types) + self._prev_store = store + else: + store = self._prev_store + parent_iter = None + for i, row_data in enumerate(data_rows): + insert_iter = store.iter_nth_child(None, i) + if insert_iter is not None: + for j, value in enumerate(row_data): + store.set_value(insert_iter, j, value) + elif not rows_are_descendants: + store.append(None, row_data) + elif rows_are_descendants[i]: + store.append(parent_iter, row_data) + else: + parent_data = [row_data[0]] + [None] * len(row_data[1:]) + parent_iter = store.append(None, parent_data) + store.append(parent_iter, row_data) + for extra_index in range(i + 1, store.iter_n_children(None)): + remove_iter = store.iter_nth_child(None, extra_index) + if remove_iter is not None: + store.remove(remove_iter) + if need_new_store: + filter_model = store.filter_new() + filter_model.set_visible_func(self._filter_visible) + sort_model = Gtk.TreeModelSort(filter_model) + for i in range(len(self.column_names)): + sort_model.set_sort_func(i, self.sort_util.sort_column, i) + if (self._prev_sort_model is not None and + prev_sort_id[0] is not None): + sort_model.set_sort_column_id(*prev_sort_id) + self._prev_sort_model = sort_model + sort_model.connect("sort-column-changed", + self.sort_util.handle_sort_column_change) + if should_redraw: + self.sort_util.clear_sort_columns() + for column in list(self._view.get_columns()): + self._view.remove_column(column) + self._view.set_model(sort_model) + self._last_column_names = self.column_names + return should_redraw + + def set_focus_node_id(self, node_id): + """Set the focus on a particular node id, if possible.""" + section = self.util.get_section_option_from_id(node_id)[0] + self.scroll_to_section(section) + + def update(self, sections=None, variables=None): + """Update the summary of page data.""" + if sections is not None: + self.sections = sections + if variables is not None: + self.variables = variables + old_cols = set(self.column_names) + + # Generate a set of expanded sections (one level only). + expanded_sections = set() + model = self._view.get_model() + self._view.map_expanded_rows( + lambda r, p: + expanded_sections.add(model.get_value(model.get_iter(p), 0))) + + should_redraw = self.update_tree_model() + if should_redraw: + self.add_new_columns(self._view, self.column_names) + if old_cols != set(self.column_names): + iter_ = self._group_widget.get_active_iter() + if self.group_index is not None: + current_model = self._group_widget.get_model() + current_group = None + if current_model is not None: + current_group = current_model().get_value(iter_, 0) + group_model = Gtk.TreeStore(str) + group_model.append(None, [""]) + start_index = 0 + for i, name in enumerate(self.column_names): + if self.group_index is not None and name == current_group: + start_index = i + group_model.append(None, [name]) + if self.group_index is not None: + group_model.append(None, [""]) + self._group_widget.set_model(group_model) + self._group_widget.set_active(start_index) + model = self._view.get_model() + + # Expand previously expanded sections (one level only). + path = (0,) + while True: + if model.get_value(model.get_iter(path), 0) in expanded_sections: + self._view.expand_to_path(path) + + sibling = model.iter_next(model.get_iter(path)) + if sibling: + path = model.get_path(sibling) + else: + break + + def add_new_columns(self, treeview, column_names): + """Create new columns.""" + for i, column_name in enumerate(column_names): + col = Gtk.TreeViewColumn() + col.set_title(column_name.replace("_", "__")) + cell_for_status = Gtk.CellRendererText() + col.pack_start(cell_for_status, False, True, 0) + col.set_cell_data_func(cell_for_status, + self.set_tree_cell_status) + self.add_cell_renderer_for_value(col, column_name) + if i < len(column_names) - 1: + col.set_resizable(True) + col.set_sort_column_id(i) + treeview.append_column(col) + + def get_status_from_data(self, node_data): + """Return markup corresponding to changes since the last save.""" + text = "" + mod_markup = rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + err_markup = rose.config_editor.SUMMARY_DATA_PANEL_ERROR_MARKUP + if node_data is None: + return None + if rose.variable.IGNORED_BY_SYSTEM in node_data.ignored_reason: + text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP + elif rose.variable.IGNORED_BY_USER in node_data.ignored_reason: + text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP + if rose.variable.IGNORED_BY_SECTION in node_data.ignored_reason: + text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP + if isinstance(node_data, rose.section.Section): + # Modified status + section = node_data.metadata["id"] + if self.sect_ops.is_section_modified(node_data): + text += mod_markup + else: + for var in self.variables.get(section, []): + if self.var_ops.is_var_modified(var): + text += mod_markup + break + # Error status + if node_data.error: + text += err_markup + else: + for var in self.variables.get(section, []): + if var.error: + text += err_markup + break + elif isinstance(node_data, rose.variable.Variable): + if self.var_ops.is_var_modified(node_data): + text += mod_markup + if node_data.error: + text += err_markup + return text + + def _refilter(self, widget=None): + self._view.get_model().get_model().refilter() + + def _filter_visible(self, model, iter_): + filt_text = self._filter_widget.get_text() + if not filt_text: + return True + for i in range(model.get_n_columns()): + col_text = model.get_value(iter_, i) + if isinstance(col_text, str) and filt_text in col_text: + return True + child_iter = model.iter_children(iter_) + while child_iter is not None: + if self._filter_visible(model, child_iter): + return True + child_iter = model.iter_next(child_iter) + return False + + def _handle_activation(self, view, path, column): + if path is None: + return False + model = view.get_model() + row_iter = model.get_iter(path) + col_index = view.get_columns().index(column) + cell_data = model.get_value(row_iter, col_index) + sect_index = self.get_section_column_index() + section = model.get_value(row_iter, sect_index) + option = None + if col_index != sect_index and cell_data is not None: + option = self.column_names[col_index] + id_ = self.util.get_id_from_section_option(section, option) + self.search_function(id_) + + def _handle_button_press_event(self, treeview, event): + pathinfo = treeview.get_path_at_pos(int(event.x), + int(event.y)) + if pathinfo is not None: + path, col = pathinfo[0:2] + if event.button == 3: + rows = self._view.get_selection().get_selected_rows()[1] + if len(rows) > 1: + # Multiple selection. + self._popup_tree_multi_menu(event) + else: + # Single selection. + self._popup_tree_menu(path, col, event) + return True + elif event.button == 2: + self._handle_activation(treeview, path, col) + return False + + def _get_selected_sections(self): + """Return a set of the names of any sections that are currently being + selected by the user.""" + ret = set([]) + col = self.get_section_column_index() + model, rows = self._view.get_selection().get_selected_rows() + for row in rows: + iter_ = model.get_iter(row) + section = model.get_value(iter_, col) + if section: + # This row is a section. + ret.add(section) + else: + # This may be the parent of some sections. + ret.update(self._get_child_row_sections(model, iter_, col)) + return ret + + def _get_child_row_sections(self, model, iter_, col): + """Return a set of any sub-sections contained within the section + represented by the provided iter_. Method is recursive so will iterate + down the tree.""" + ret = set([]) + for child_no in range(model.iter_n_children(iter_)): + child_iter = model.iter_nth_child(iter_, child_no) + if not child_iter: + continue + section = model.get_value(child_iter, col) + if not section: + # Recursively acquire sub-sections if present. + ret.update(self._get_child_rows(child_iter)) + ret.add(section) + return ret + + def _popup_tree_multi_menu(self, event): + """Launch a menu for these main treeview rows (multi-selection).""" + menu = Gtk.Menu() + menu.show() + shortcuts = [] + + # Ignore all. + ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) + ign_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) + ign_menuitem.connect("activate", self._ignore_selected_sections, True) + ign_menuitem.show() + menu.append(ign_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + ign_menuitem)) + # Enable all. + ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) + ign_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) + ign_menuitem.connect("activate", self._ignore_selected_sections, False) + ign_menuitem.show() + menu.append(ign_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + ign_menuitem)) + # Remove all. + rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) + rem_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) + rem_menuitem.connect("activate", self._remove_selected_sections) + rem_menuitem.show() + menu.append(rem_menuitem) + shortcuts.append((rose.config_editor.ACCEL_REMOVE, rem_menuitem)) + + # list shortcut keys + accel = Gtk.AccelGroup() + menu.set_accel_group(accel) + for key_press, menuitem in shortcuts: + key, mod = Gtk.accelerator_parse(key_press) + menuitem.add_accelerator( + 'activate', + accel, + key, + mod, + Gtk.AccelFlags.VISIBLE + ) + + menu.popup(None, None, None, event.button, event.time) + return False + + def _popup_tree_menu(self, path, col, event): + """Launch a menu for this main treeview row (single selection).""" + shortcuts = [] + menu = Gtk.Menu() + menu.show() + model = self._view.get_model() + row_iter = model.get_iter(path) + sect_index = self.get_section_column_index() + this_section = model.get_value(row_iter, sect_index) + if this_section is not None: + # Jump to section. + menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_JUMP_TO) + label = rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( + this_section.replace("_", "__")) + menuitem.set_label(label) + menuitem._section = this_section + menuitem.connect("activate", + lambda i: self.search_function(i._section)) + menuitem.show() + menu.append(menuitem) + sep = Gtk.SeparatorMenuItem() + sep.show() + menu.append(sep) + extra_menuitems = self._get_custom_menu_items(path, col, event) + if extra_menuitems: + for extra_menuitem in extra_menuitems: + menu.append(extra_menuitem) + if this_section is not None: + sep = Gtk.SeparatorMenuItem() + sep.show() + menu.append(sep) + if self.is_duplicate: + # A section is currently selected + if this_section is not None: + # Add section. + add_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) + add_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) + add_menuitem.connect("activate", + lambda i: self.add_section()) + add_menuitem.show() + menu.append(add_menuitem) + # Copy section. + copy_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_COPY) + copy_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) + copy_menuitem.connect( + "activate", lambda i: self.copy_section(this_section)) + copy_menuitem.show() + menu.append(copy_menuitem) + if (rose.variable.IGNORED_BY_USER in + self.sections[this_section].ignored_reason): + # Enable section. + enab_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) + enab_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + enab_menuitem.connect( + "activate", + lambda i: self.sub_ops.ignore_section(this_section, + False)) + enab_menuitem.show() + menu.append(enab_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + enab_menuitem)) + else: + # Ignore section. + ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) + ign_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem.connect( + "activate", + lambda i: self.sub_ops.ignore_section(this_section, + True)) + ign_menuitem.show() + menu.append(ign_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + ign_menuitem)) + # Remove section. + rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) + rem_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem.connect( + "activate", lambda i: self.remove_section(this_section)) + rem_menuitem.show() + menu.append(rem_menuitem) + shortcuts.append((rose.config_editor.ACCEL_REMOVE, + rem_menuitem)) + else: # A group is currently selected. + # Ignore all + ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) + ign_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem.connect("activate", + self._ignore_selected_sections, + True) + ign_menuitem.show() + menu.append(ign_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + ign_menuitem)) + # Enable all + ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) + ign_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + ign_menuitem.connect("activate", + self._ignore_selected_sections, + False) + ign_menuitem.show() + menu.append(ign_menuitem) + shortcuts.append((rose.config_editor.ACCEL_IGNORE, + ign_menuitem)) + # Delete all. + rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) + rem_menuitem.set_label( + rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem.connect( + "activate", self._remove_selected_sections) + rem_menuitem.show() + menu.append(rem_menuitem) + shortcuts.append((rose.config_editor.ACCEL_REMOVE, + rem_menuitem)) + + # list shortcut keys + accel = Gtk.AccelGroup() + menu.set_accel_group(accel) + for key_press, menuitem in shortcuts: + key, mod = Gtk.accelerator_parse(key_press) + menuitem.add_accelerator( + 'activate', + accel, + key, + mod, + Gtk.AccelFlags.VISIBLE + ) + + menu.popup(None, None, None, event.button, event.time) + return False + + def add_section(self, section=None, opt_map=None): + """Add a new section. + + section is the optional name for the new section - otherwise + one will be calculated, if the sub data sections are duplicates + opt_map is a dictionary of option names and values to add with + the section + + """ + if section is None: + if not self.sections or not self.is_duplicate: + return False + section_base = list(self.sections.keys())[0].rsplit("(", 1)[0] + i = 1 + section = section_base + "(" + str(i) + ")" + while section in self.sections: + i += 1 + section = section_base + "(" + str(i) + ")" + self.sub_ops.add_section(section, opt_map=opt_map) + self.scroll_to_section(section) + return section + + def copy_section(self, section): + """Copy a section and its content into a new section name.""" + new_section = self.sub_ops.clone_section(section) + self.scroll_to_section(new_section) + + def _handle_key_press_event(self, treeview, event): + if event.keyval == Gdk.KEY_Delete: + # `Delete` - remove section(s) + self._remove_selected_sections() + # detect key combination + elif 'GDK_CONTROL_MASK' in event.get_state().value_names: + # `Ctrl + ?` + if event.keyval == Gdk.KEY_i: + # `Ctrl + i` - ignore section(s) + self._ignore_selected_sections(None) + + def _remove_selected_sections(self, *args): + """Remove any sections currently selected by the user.""" + sections = self._get_selected_sections() + self.sub_ops.remove_sections(sections) + self._view.get_selection().unselect_all() + + def _ignore_selected_sections(self, _, ignore=None): + """Ignores any sections currently selected by the user. Ignore mode is + set by the 'ignore' kwarg: + True: Ignore all sections (if not already ignored) + False: Enable all sections (if not already enabled) + None: Ignore all sections, if sections are all already ignored + then enable all sections inserted. + """ + sections_ = self._get_selected_sections() + ignored = [rose.variable.IGNORED_BY_USER in + self.sections[section_].ignored_reason for + section_ in sections_] + # If ignore mode is not specified decide whether to ignore or enable. + if ignore is None: + ignore = not all(ignored) + # Filter out sections that are already ignored/enabled. + sections_ = [section_ for section_, ignored in zip(sections_, ignored) + if ignored != ignore] + self.sub_ops.ignore_sections( + sections_, ignore, skip_sub_data_update=False) + + def remove_section(self, section): + """Remove a section.""" + self.sub_ops.remove_section(section) + + def scroll_to_section(self, section): + """Find a particular section in the treeview and scroll to it.""" + iter_ = self.get_section_iter(section) + if iter_ is not None: + path = self._view.get_model().get_path(iter_) + self._view.scroll_to_cell(path) + self._view.set_cursor(path) + + def get_section_iter(self, section): + """Get the Gtk.TreeIter of this section.""" + iters = [] + sect_index = self.get_section_column_index() + self._view.get_model().foreach(self._check_value_iter, + [sect_index, section, iters]) + if iters: + return iters[0] + return None + + def _check_value_iter(self, model, path, iter_, data): + value_index, value, iters = data + if model.get_value(iter_, value_index) == value: + iters.append(iter_) + return True + return False + + def _sort_row_data(self, row1, row2, sort_index, descending=False): + fac = (-1 if descending else 1) + return fac * self.sort_util.cmp_(row1[sort_index], row2[sort_index]) + + def _handle_group_change(self, combobox): + model = combobox.get_model() + col_name = model.get_value(combobox.get_active_iter(), 0) + if col_name: + group_index = self.column_names.index(col_name) + # Any existing grouping changes the order of self.column_names. + if (self.group_index is not None and + group_index <= self.group_index): + group_index -= 1 + else: + group_index = None + if group_index == self.group_index: + return False + self.group_index = group_index + self.update() + return False + + def _apply_grouping(self, data_rows, column_names, group_index=None, + descending=False): + rows_are_descendants = [] + if group_index is None: + return data_rows, column_names, rows_are_descendants + k = group_index + data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] + column_names.insert(0, column_names.pop(k)) + data_rows.sort(lambda x, y: self._sort_row_data(x, y, 0, descending)) + last_entry = None + rows_are_descendants = [] + for i, row in enumerate(data_rows): + if i > 0 and last_entry == row[0]: + rows_are_descendants.append(True) + else: + rows_are_descendants.append(False) + last_entry = row[0] + return data_rows, column_names, rows_are_descendants + + +class StandardSummaryDataPanel(BaseSummaryDataPanel): + + """Class that provides a standard interface to summary data.""" + + def add_cell_renderer_for_value(self, col, col_title): + """Add a CellRendererText for the column.""" + cell_for_value = Gtk.CellRendererText() + col.pack_start(cell_for_value, True, True, 0) + col.set_cell_data_func(cell_for_value, + self._set_tree_cell_value) + + def set_tree_cell_status(self, col, cell, model, row_iter): + """Set the status text for a cell in this column.""" + col_index = self._view.get_columns().index(col) + sect_index = self.get_section_column_index() + section = model.get_value(row_iter, sect_index) + if section is None: + cell.set_property("markup", None) + return False + if col_index == sect_index: + node_data = self.sections.get(section) + else: + option = self.column_names[col_index] + id_ = self.util.get_id_from_section_option(section, option) + node_data = self.var_id_map.get(id_) + cell.set_property("markup", + self.get_status_from_data(node_data)) + + def get_model_data(self): + """Construct a data model of other page data.""" + sub_sect_names = list(self.sections.keys()) + sub_var_names = [] + self.var_id_map = {} + for section, variables in list(self.variables.items()): + for variable in variables: + self.var_id_map[variable.metadata["id"]] = variable + if variable.name not in sub_var_names: + sub_var_names.append(variable.name) + sub_sect_names.sort(rose.config.sort_settings) + sub_var_names.sort(rose.config.sort_settings) + data_rows = [] + for section in sub_sect_names: + row_data = [section] + for opt in sub_var_names: + id_ = self.util.get_id_from_section_option(section, opt) + var = self.var_id_map.get(id_) + if var is None: + row_data.append(None) + else: + row_data.append(rose.gtk.util.safe_str(var.value)) + data_rows.append(row_data) + if self.is_duplicate: + sect_name = rose.config_editor.SUMMARY_DATA_PANEL_INDEX_TITLE + else: + sect_name = rose.config_editor.SUMMARY_DATA_PANEL_SECTION_TITLE + column_names = [sect_name] + column_names += sub_var_names + return data_rows, column_names + + def _set_tree_cell_value(self, column, cell, treemodel, iter_): + cell.set_property("visible", True) + col_index = self._view.get_columns().index(column) + value = self._view.get_model().get_value(iter_, col_index) + max_len = rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN + if value is not None and len(value) > max_len and col_index != 0: + cell.set_property("width-chars", max_len) + cell.set_property("ellipsize", Pango.EllipsizeMode.END) + sect_index = self.get_section_column_index() + if (value is not None and col_index == sect_index and + self.is_duplicate): + value = value.split("(")[-1].rstrip(")") + if col_index == 0 and treemodel.iter_parent(iter_) is not None: + cell.set_property("visible", False) + cell.set_property("markup", value) + + def set_tree_tip(self, view, row_iter, col_index, tip): + """Set the hover-over (Tooltip) text for the TreeView.""" + sect_index = self.get_section_column_index() + section = view.get_model().get_value(row_iter, sect_index) + if section is None: + return False + if col_index == sect_index: + option = None + if section not in self.sections: + return False + id_data = self.sections[section] + tip_text = section + else: + option = self.column_names[col_index] + id_ = self.util.get_id_from_section_option(section, option) + if id_ not in self.var_id_map: + return False + id_data = self.var_id_map[id_] + value = str(view.get_model().get_value(row_iter, col_index)) + tip_text = rose.CONFIG_DELIMITER.join([section, option, value]) + tip_text += id_data.metadata.get(rose.META_PROP_DESCRIPTION, "") + if tip_text: + tip_text += "\n" + for key, value in list(id_data.error.items()): + tip_text += ( + rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( + key, value)) + for key in id_data.ignored_reason: + tip_text += key + "\n" + if option is not None: + change_text = self.var_ops.get_var_changes(id_data) + tip_text += change_text + "\n" + tip.set_text(tip_text.rstrip()) + return True + + def get_section_column_index(self): + """Return the column index for the section name.""" + sect_index = 0 + if self.group_index is not None and self.group_index != 0: + sect_index = 1 + return sect_index diff --git a/metomi/rose/config_editor/plugin/__init__.py b/metomi/rose/config_editor/plugin/__init__.py new file mode 100644 index 000000000..ea1a3150a --- /dev/null +++ b/metomi/rose/config_editor/plugin/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- diff --git a/metomi/rose/config_editor/plugin/um/__init__.py b/metomi/rose/config_editor/plugin/um/__init__.py new file mode 100644 index 000000000..ea1a3150a --- /dev/null +++ b/metomi/rose/config_editor/plugin/um/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- diff --git a/metomi/rose/config_editor/plugin/um/widget/__init__.py b/metomi/rose/config_editor/plugin/um/widget/__init__.py new file mode 100644 index 000000000..ea1a3150a --- /dev/null +++ b/metomi/rose/config_editor/plugin/um/widget/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py new file mode 100644 index 000000000..0ea8a75ab --- /dev/null +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -0,0 +1,868 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import ast +import os + +from gi.repository import Pango +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import rose.config +import rose.config_editor.panelwidget.summary_data +import rose.config_tree +import rose.gtk.dialog +import rose.gtk.util +import rose.reporter +import rose.resource + +import rose.config_editor.plugin.um.widget.stash_add as stash_add +import rose.config_editor.plugin.um.widget.stash_util as stash_util + + +class BaseStashSummaryDataPanelv1( + rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel): + + """This is a base class for displaying and editing STASH requests. + + It adds editing capability for option values, displays metadata + fetched from the STASHmaster file, and can launch a custom dialog + for adding/removing STASH requests. + + Subclasses *must* provide the following method: + def get_stashmaster_lookup_dict(self): + which should return a nested dictionary containing STASHmaster file + information. + + Subclasses *must* override the STASH_PACKAGE_PATH attribute with an + absolute path to a directory containing a rose-app.conf file with + STASH request package information. + + Subclasses should override the STASHMASTER_PATH attribute with an + absolute path to a directory containing e.g. the STASHmaster_A + file. An argument to the widget metadata option can also be used to + provide this information. + + Subclasses should override the STASHMASTER_META_PATH attribute with + an absolute path to a directory containing a + 'STASHmaster-meta.conf' file that provides metadata for STASHmaster + fields and values. + + """ + + # These attributes must/should be overridden: + STASH_PACKAGE_PATH = None + STASHMASTER_PATH = None + STASHMASTER_META_PATH = None + + # This attribute may be overridden, if necessary: + STASHMASTER_META_FILENAME = "STASHmaster-meta.conf" + + # These attributes are generic titles. + ADD_NEW_STASH_LABEL = "New" + ADD_NEW_STASH_TIP = "Launch a window for adding new STASH requests" + ADD_NEW_STASH_WINDOW_TITLE = "Add new STASH requests" + DESCRIPTION_TITLE = "Info" + INCLUDED_TITLE = "Incl?" + PACKAGE_MANAGER_LABEL = "Packages" + PACKAGE_MANAGER_TIP = "Launch a menu for managing groups of requests" + SECTION_INDEX_TITLE = "Index" + VIEW_MANAGER_LABEL = "View" + VIEW_MANAGER_TIP = "Change view options" + + # The title property name for a request (must match the parser's one). + STASH_PARSE_DESC_OPT = "name" + + # These attributes are namelist/UM-input specific. + STREQ_NL_BASE = "namelist:streq" + STREQ_NL_SECT_OPT = "isec" + STREQ_NL_ITEM_OPT = "item" + STREQ_NL_PACKAGE_OPT = "package" + OPTION_NL_MAP = {"dom_name": "namelist:domain", + "tim_name": "namelist:time", + "use_name": "namelist:use"} + + def __init__(self, *args, **kwargs): + self.stashmaster_directory_path = kwargs.get("arg_str", "") + if not self.stashmaster_directory_path: + self.stashmaster_directory_path = self.STASHMASTER_PATH + self.load_stash() + super(BaseStashSummaryDataPanelv1, self).__init__(*args, **kwargs) + self._add_new_diagnostic_launcher() + self._diag_panel = None + + def get_stashmaster_lookup_dict(self): + """Return a nested dictionary with STASHmaster info. + + Record properties are stored under section_number => + item_number => property_name. + + For example, if the nested dictionary is called 'stash_dict': + stash_dict[section_number][item_number]['name'] + would be something like: + "U COMPNT OF WIND AFTER TIMESTEP" + + Subclasses must provide (override) this method. + The attribute self.stashmaster_directory_path may be used. + + """ + raise NotImplementedError() + + def add_cell_renderer_for_value(self, col, col_title): + """(Override) Add a cell renderer type based on the column.""" + self._update_available_profiles() + if col_title in self.OPTION_NL_MAP: + cell_for_value = Gtk.CellRendererCombo() + listmodel = Gtk.ListStore(str) + values = sorted(self._available_profile_map[col_title]) + for possible_value in values: + listmodel.append([possible_value]) + cell_for_value.set_property("has-entry", False) + cell_for_value.set_property("editable", True) + cell_for_value.set_property("model", listmodel) + cell_for_value.set_property("text-column", 0) + try: + cell_for_value.connect("changed", + self._handle_cell_combo_change, + col_title) + except TypeError: + # PyGTK 2.14 - changed signal. + cell_for_value.connect("edited", + self._handle_cell_combo_change, + col_title) + col.pack_start(cell_for_value, True, True, 0) + col.set_cell_data_func(cell_for_value, + self._set_tree_cell_value_combo) + elif col_title == self.INCLUDED_TITLE: + cell_for_value = Gtk.CellRendererToggle() + col.pack_start(cell_for_value, False, True, 0) + cell_for_value.set_property("activatable", True) + cell_for_value.connect("toggled", + self._handle_cell_toggle_change) + col.set_cell_data_func(cell_for_value, + self._set_tree_cell_value_toggle) + else: + cell_for_value = Gtk.CellRendererText() + col.pack_start(cell_for_value, True, True, 0) + if (col_title not in [self.SECTION_INDEX_TITLE, + self.DESCRIPTION_TITLE]): + cell_for_value.set_property("editable", True) + cell_for_value.connect("edited", + self._handle_cell_text_change, + col_title) + col.set_cell_data_func(cell_for_value, + self._set_tree_cell_value) + + def get_model_data(self): + """(Override) Construct a data model of other page data.""" + sub_sect_names = list(self.sections.keys()) + sub_var_names = [] + self.var_id_map = {} + section_sort_keys = {} + # Apply the correct default sorting (section, item) + for section, variables in list(self.variables.items()): + for variable in variables: + self.var_id_map[variable.metadata["id"]] = variable + if variable.name not in sub_var_names: + sub_var_names.append(variable.name) + if variable.name == self.STREQ_NL_SECT_OPT: + section_sort_keys.setdefault(section, []) + try: + value = int(variable.value) + except (TypeError, ValueError): + value = variable.value + if len(section_sort_keys[section]) < 1: + section_sort_keys[section].append(None) + section_sort_keys[section][0] = value + if variable.name == self.STREQ_NL_ITEM_OPT: + section_sort_keys.setdefault(section, []) + try: + value = int(variable.value) + except (TypeError, ValueError): + value = variable.value + while len(section_sort_keys[section]) < 2: + section_sort_keys[section].append(None) + section_sort_keys[section][1] = value + if variable.name == self.STREQ_NL_PACKAGE_OPT: + section_sort_keys.setdefault(section, []) + while len(section_sort_keys[section]) < 3: + section_sort_keys[section].append(None) + section_sort_keys[section][2] = variable.value + for section, sort_list in list(section_sort_keys.items()): + while len(sort_list) < 4: + sort_list.append(None) + sort_list[3] = section + sub_sect_names.sort(lambda x, y: cmp(section_sort_keys.get(x), + section_sort_keys.get(y))) + sub_var_names.sort(rose.config.sort_settings) + sub_var_names.sort(lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) - + (x != self.STREQ_NL_PACKAGE_OPT)) + sub_var_names.sort(lambda x, y: (y == self.STREQ_NL_ITEM_OPT) - + (x == self.STREQ_NL_ITEM_OPT)) + sub_var_names.sort(lambda x, y: (y == self.STREQ_NL_SECT_OPT) - + (x == self.STREQ_NL_SECT_OPT)) + # Load the data. + data_rows = [] + for section in sub_sect_names: + row_data = [] + stash_sect_id = self.util.get_id_from_section_option( + section, self.STREQ_NL_SECT_OPT) + stash_item_id = self.util.get_id_from_section_option( + section, self.STREQ_NL_ITEM_OPT) + sect_var = self.var_id_map.get(stash_sect_id) + item_var = self.var_id_map.get(stash_item_id) + stash_props = None + if sect_var is not None and item_var is not None: + stash_props = self._stash_lookup.get( + sect_var.value, {}).get(item_var.value) + if stash_props is None: + row_data.append(None) + else: + desc = stash_props[self.STASH_PARSE_DESC_OPT].strip() + row_data.append(desc) + is_enabled = (rose.variable.IGNORED_BY_USER not in + self.sections[section].ignored_reason) + row_data.append(str(is_enabled)) + for opt in sub_var_names: + id_ = self.util.get_id_from_section_option(section, opt) + var = self.var_id_map.get(id_) + if var is None: + row_data.append(None) + else: + row_data.append(rose.gtk.util.safe_str(var.value)) + row_data.append(section) + data_rows.append(row_data) + # Set the column names and their ordering. + column_names = [self.DESCRIPTION_TITLE, self.INCLUDED_TITLE] + column_names += sub_var_names + [self.SECTION_INDEX_TITLE] + return data_rows, column_names + + def get_section_column_index(self): + """(Override) Return the column index for the section (Rose section)""" + return self.column_names.index(self.SECTION_INDEX_TITLE) + + def get_stashmaster_meta_map(self): + """Return a nested dictionary with STASHmaster metadata. + + This stores metadata about STASHmaster fields and their values. + Field metadata is stored as field_name => metadata_property => + metadata_value. Field value metadata (for a particular value of + a field) is under (field_name + "=" + value) => + metadata_property => metadata_value. + + For example, if the nested dictionary is called + 'stash_meta_dict': + stash_meta_dict["grid"]["title"] + would be something like: + "Grid code" + and: + stash_meta_dict["grid=2"]["description"] + would be something like: + "A grid code of 2 means...." + + """ + if self.STASHMASTER_META_PATH is None: + return {} + try: + config = rose.config_tree.ConfigTreeLoader().load( + self.STASHMASTER_META_PATH, + self.STASHMASTER_META_FILENAME).node + except (rose.config.ConfigSyntaxError, IOError, OSError) as exc: + rose.reporter.Reporter()( + "Error loading STASHmaster metadata resource: " + + type(exc).__name__ + ": " + str(exc) + "\n", + kind=rose.reporter.Reporter.KIND_ERR, + level=rose.reporter.Reporter.FAIL + ) + return {} + stash_meta_dict = {} + for keys, node in config.walk(no_ignore=True): + if len(keys) == 2: + address = keys[0].replace("stashmaster:", "", 1) + prop = keys[1] + stash_meta_dict.setdefault(address, {}) + stash_meta_dict[address][prop] = node.value + return stash_meta_dict + + def set_tree_cell_status(self, col, cell, model, row_iter): + """(Override) Set the status-related markup for a cell.""" + col_index = self._view.get_columns().index(col) + sect_index = self.get_section_column_index() + section = model.get_value(row_iter, sect_index) + if section is None: + return cell.set_property("markup", None) + if (col_index == sect_index or + self.column_names[col_index] == self.DESCRIPTION_TITLE): + node_data = self.sections.get(section) + else: + option = self.column_names[col_index] + if option is None: + return cell.set_property("markup", None) + id_ = self.util.get_id_from_section_option(section, option) + node_data = self.var_id_map.get(id_) + cell.set_property("markup", + self.get_status_from_data(node_data)) + + def set_tree_tip(self, view, row_iter, col_index, tip): + """(Override) Set the TreeView Tooltip.""" + sect_index = self.get_section_column_index() + model = view.get_model() + section = model.get_value(row_iter, sect_index) + if section is None: + return False + col_name = self.column_names[col_index] + stash_section_index = self.column_names.index( + self.STREQ_NL_SECT_OPT) + stash_item_index = self.column_names.index( + self.STREQ_NL_ITEM_OPT) + stash_section = model.get_value(row_iter, stash_section_index) + stash_item = model.get_value(row_iter, stash_item_index) + if (col_index == sect_index or + col_name in [self.DESCRIPTION_TITLE, self.INCLUDED_TITLE]): + option = None + if section not in self.sections: + return False + id_data = self.sections[section] + if col_index == sect_index: + tip_text = section + elif col_name in [self.DESCRIPTION_TITLE, self.INCLUDED_TITLE]: + tip_text = str(model.get_value(row_iter, col_index)) + tip_text += "\n" + section + if col_name == self.DESCRIPTION_TITLE: + value = str(model.get_value(row_iter, col_index)) + metadata = stash_util.get_stash_section_meta( + self._stashmaster_meta_lookup, stash_section, stash_item, + value + ) + help_ = metadata.get(rose.META_PROP_HELP) + if help_ is not None: + tip_text += "\n\n" + help_ + else: + option = self.column_names[col_index] + id_ = self.util.get_id_from_section_option(section, option) + if (id_ not in self.var_id_map): + tip.set_text(str( + model.get_value(row_iter, col_index))) + return True + id_data = self.var_id_map[id_] + value = str(model.get_value(row_iter, col_index)) + tip_text = rose.CONFIG_DELIMITER.join( + [section, option, value]) + "\n" + if (option in self.OPTION_NL_MAP and + option in list(self._profile_location_map.keys())): + profile_id = self._profile_location_map[option].get(value) + if profile_id is not None: + profile_sect = self.util.get_section_option_from_id( + profile_id)[0] + tip_text += "See " + profile_sect + tip_text += id_data.metadata.get(rose.META_PROP_DESCRIPTION, "") + if tip_text: + tip_text += "\n" + for key, value in list(id_data.error.items()): + tip_text += ( + rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( + key, value)) + for key in id_data.ignored_reason: + tip_text += "({0})\n".format(key) + if option is None: + change_text = self.sect_ops.get_section_changes(id_data) + else: + change_text = self.var_ops.get_var_changes(id_data) + if change_text: + tip_text += change_text + "\n" + tip.set_text(tip_text.rstrip()) + return True + + def _get_custom_menu_items(self, path, col, event): + """(Override) Add some custom menu items to the TreeView menu.""" + menuitems = [] + model = self._view.get_model() + col_index = self._view.get_columns().index(col) + col_title = self.column_names[col_index] + if (col_title not in self.OPTION_NL_MAP and + col_title != self.DESCRIPTION_TITLE): + return [] + iter_ = model.get_iter(path) + value = model.get_value(iter_, col_index) + if col_title == self.DESCRIPTION_TITLE: + meta_key = self.STASH_PARSE_DESC_OPT + "=" + str(value) + metadata = self._stashmaster_meta_lookup.get(meta_key, {}) + help_ = metadata.get(rose.META_PROP_HELP) + if help_ is not None: + menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) + menuitem.set_label(label="Help") + menuitem._help_text = help_ + menuitem._help_title = "Help for %s" % value + menuitem.connect("activate", self._launch_record_help) + menuitem.show() + return [menuitem] + return [] + if value not in self._profile_location_map[col_title]: + return [] + location = self._profile_location_map[col_title][value] + menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ABOUT) + menuitem.set_label(label="View " + value.strip("'")) + menuitem._loc_id = location + menuitem.connect("activate", + lambda i: self.search_function(i._loc_id)) + menuitem.show() + menuitems.append(menuitem) + profiles_menuitems = [] + for profile in self._available_profile_map[col_title]: + label = "View " + profile.strip("'") + menuitem = Gtk.MenuItem(label=label) + menuitem._loc_id = self._profile_location_map[col_title][profile] + menuitem.connect("button-release-event", + lambda i, e: self.search_function(i._loc_id)) + menuitem.show() + profiles_menuitems.append(menuitem) + if profiles_menuitems: + profiles_menu = Gtk.Menu() + profiles_menu.show() + profiles_root_menuitem = Gtk.ImageMenuItem( + stock_id=Gtk.STOCK_ABOUT) + profiles_root_menuitem.set_label("View...") + profiles_root_menuitem.show() + profiles_root_menuitem.set_submenu(profiles_menu) + for profiles_menuitem in profiles_menuitems: + profiles_menu.append(profiles_menuitem) + menuitems.append(profiles_root_menuitem) + return menuitems + + def add_new_stash_request(self, section, item, launch_dialog=False): + """Add a new streq namelist.""" + new_opt_map = {self.STREQ_NL_SECT_OPT: section, + self.STREQ_NL_ITEM_OPT: item} + new_section = self.add_section(None, opt_map=new_opt_map) + if launch_dialog: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_INFO, + "Added request as {0}".format(new_section), + "New Request") + + def generate_package_lookup(self): + """Store a dictionary of package requests and domains.""" + self._package_lookup = {} + self._package_profile_lookup = {} + for sect, node in list(self.package_config.value.items()): + if not isinstance(node.value, dict) or node.is_ignored(): + continue + base_sect = sect.rsplit("(", 1)[0] + if base_sect == self.STREQ_NL_BASE: + package_node = node.get([self.STREQ_NL_PACKAGE_OPT], + no_ignore=True) + if package_node is not None: + package = package_node.value + self._package_lookup.setdefault(package, {}) + self._package_lookup[package].setdefault(base_sect, []) + self._package_lookup[package][base_sect].append(sect) + for profile in self.OPTION_NL_MAP: + profile_node = node.get([profile], no_ignore=True) + if profile_node is not None: + self._package_lookup[package].setdefault( + profile, []) + self._package_lookup[package][profile].append( + profile_node.value) + continue + for profile, profile_nl in list(self.OPTION_NL_MAP.items()): + if base_sect == profile_nl: + name_node = node.get([profile], no_ignore=True) + if name_node is not None: + name = name_node.value + self._package_profile_lookup.setdefault(profile, {}) + self._package_profile_lookup[profile][name] = sect + break + + def load_stash(self): + """Load a STASHmaster file into data structures for later use.""" + self._stash_lookup = self.get_stashmaster_lookup_dict() + package_config_file = os.path.join(self.STASH_PACKAGE_PATH, + rose.SUB_CONFIG_NAME) + self.package_config = rose.config.ConfigNode() + rose.config.ConfigLoader().load_with_opts(package_config_file, + self.package_config) + self.generate_package_lookup() + self._stashmaster_meta_lookup = ( + self.get_stashmaster_meta_map()) + + def _add_new_diagnostic_launcher(self): + # Create a button for launching the "Add new STASH" dialog. + self._add_button = rose.gtk.util.CustomButton( + label=self.ADD_NEW_STASH_LABEL, + stock_id=Gtk.STOCK_ADD, + tip_text=self.ADD_NEW_STASH_TIP) + package_button = rose.gtk.util.CustomButton( + label=self.PACKAGE_MANAGER_LABEL, + tip_text=self.PACKAGE_MANAGER_TIP, + has_menu=True) + self.control_widget_hbox.pack_end(package_button, expand=False, + fill=False) + self.control_widget_hbox.pack_end(self._add_button, + expand=False, fill=False) + self._add_button.connect("clicked", + self._launch_new_diagnostic_window) + package_button.connect("button-press-event", + self._package_menu_launch) + + def _handle_activation(self, view, path, column): + # React to row activation in the TreeView. + if path is None: + return False + model = view.get_model() + row_iter = model.get_iter(path) + col_index = view.get_columns().index(column) + col_title = self.column_names[col_index] + if col_title in self.OPTION_NL_MAP: + return False + cell_data = model.get_value(row_iter, col_index) + sect_index = self.get_section_column_index() + section = model.get_value(row_iter, sect_index) + if section is None: + return False + option = None + if col_index != sect_index and cell_data is not None: + option = self.column_names[col_index] + if option == self.DESCRIPTION_TITLE: + option = None + id_ = self.util.get_id_from_section_option(section, option) + self.search_function(id_) + + def _handle_cell_combo_change(self, combo_cell, path_string, new, + col_title): + # Handle a Gtk.CellRendererCombo (variable) value change. + if isinstance(new, str): + new_value = new + else: + new_value = combo_cell.get_property("model").get_value(new, 0) + row_iter = self._view.get_model().get_iter(path_string) + sect_index = self.get_section_column_index() + section = self._view.get_model().get_value(row_iter, sect_index) + option = col_title + id_ = self.util.get_id_from_section_option(section, option) + var = self.var_id_map[id_] + self.var_ops.set_var_value(var, new_value) + return False + + def _handle_cell_text_change(self, text_cell, path_string, new_text, + col_title): + # Handle a Gtk.CellRendererText (variable) value change. + row_iter = self._view.get_model().get_iter(path_string) + sect_index = self.get_section_column_index() + section = self._view.get_model().get_value(row_iter, sect_index) + option = col_title + id_ = self.util.get_id_from_section_option(section, option) + var = self.var_id_map[id_] + self.var_ops.set_var_value(var, new_text) + return False + + def _handle_cell_toggle_change(self, combo_cell, path_string): + # Handle a Gtk.CellRendererToggle value change. + was_active = combo_cell.get_property("active") + row_iter = self._view.get_model().get_iter(path_string) + sect_index = self.get_section_column_index() + section = self._view.get_model().get_value(row_iter, sect_index) + if section is None: + return False + is_active = not was_active + combo_cell.set_property("active", is_active) + self.sub_ops.ignore_section(section, not is_active) + return False + + def _get_request_lookup(self): + # Return a lookup dictionary of streq info. + request_lookup = {} + for section in self.sections: + stash_sect_id = self.util.get_id_from_section_option( + section, self.STREQ_NL_SECT_OPT) + stash_item_id = self.util.get_id_from_section_option( + section, self.STREQ_NL_ITEM_OPT) + sect_var = self.var_id_map.get(stash_sect_id) + item_var = self.var_id_map.get(stash_item_id) + if sect_var is None or item_var is None: + continue + st_sect = sect_var.value + st_item = item_var.value + request_lookup.setdefault(st_sect, {}) + request_lookup[st_sect].setdefault(st_item, {}) + request_lookup[st_sect][st_item][section] = {} + for variable in self.variables.get(section, []): + request_lookup[st_sect][st_item][section].update( + {variable.name: variable.value}) + return request_lookup + + def _get_request_changes(self): + # Return a list of request indices with changes. + changed_requests = {} + for section, sect_data in list(self.sections.items()): + changes = self.sect_ops.get_section_changes(sect_data) + if changes: + changed_requests.update({section: changes}) + return changed_requests + + def _handle_close_diagnostic_window(self, widget=None): + # Handle a close of the diagnostic window. + self._diag_panel = None + self._add_button.set_sensitive(True) + + def _launch_new_diagnostic_window(self, widget=None): + # Launch the "new STASH request" dialog. + window = Gtk.Window() + window.set_title(self.ADD_NEW_STASH_WINDOW_TITLE) + request_lookup = self._get_request_lookup() + request_changes = self._get_request_changes() + add_new_func = lambda s, i: ( + self.add_new_stash_request(s, i, launch_dialog=True)) + self._diag_panel = stash_add.AddStashDiagnosticsPanelv1( + self._stash_lookup, + request_lookup, + request_changes, + self._stashmaster_meta_lookup, + add_new_func, + self.scroll_to_section, + self._refresh_diagnostic_window + ) + window.add(self._diag_panel) + window.set_default_size(900, 800) + window.connect("destroy", self._handle_close_diagnostic_window) + window.show() + self._add_button.set_sensitive(False) + + def _launch_record_help(self, menuitem): + """Launch the help from a menu.""" + rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, + menuitem._help_title) + + def _refresh_diagnostic_window(self): + # Refresh information in the "new STASH request" dialog. + if self._diag_panel is not None: + request_lookup = self._get_request_lookup() + request_changes = self._get_request_changes() + self._diag_panel.update_request_info(request_lookup, + request_changes) + + def _set_tree_cell_value_combo(self, column, cell, treemodel, iter_): + # Extract a value for a combo box cell renderer. + cell.set_property("visible", True) + cell.set_property("editable", True) + col_index = self._view.get_columns().index(column) + value = self._view.get_model().get_value(iter_, col_index) + if value is None: + cell.set_property("editable", False) + cell.set_property("text", None) + cell.set_property("visible", False) + if col_index == 0 and treemodel.iter_parent(iter_) is not None: + cell.set_property("visible", False) + cell.set_property("text", value) + + def _set_tree_cell_value_toggle(self, column, cell, treemodel, iter_): + # Extract a value for a toggle cell renderer. + cell.set_property("visible", True) + col_index = self._view.get_columns().index(column) + value = self._view.get_model().get_value(iter_, col_index) + if value is None: + cell.set_property("visible", False) + if col_index == 0 and treemodel.iter_parent(iter_) is not None: + cell.set_property("visible", False) + try: + value = ast.literal_eval(value) + except ValueError: + return False + cell.set_property("active", value) + + def _set_tree_cell_value(self, column, cell, treemodel, iter_): + # Extract a value for a conventional text cell renderer. + cell.set_property("visible", True) + col_index = self._view.get_columns().index(column) + value = self._view.get_model().get_value(iter_, col_index) + if value is None: + cell.set_property("markup", None) + cell.set_property("visible", False) + max_len = rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN + if value is not None and len(value) > max_len and col_index != 0: + cell.set_property("width-chars", max_len) + cell.set_property("ellipsize", Pango.EllipsizeMode.END) + sect_index = self.get_section_column_index() + if value is not None and col_index == sect_index and self.is_duplicate: + value = value.split("(")[-1].rstrip(")") + if col_index == 0 and treemodel.iter_parent(iter_) is not None: + cell.set_property("visible", False) + cell.set_property("markup", rose.gtk.util.safe_str(value)) + + def _update_available_profiles(self): + # Retrieve which profiles (namelists like domain) are available. + self._available_profile_map = {} + self._profile_location_map = {} + ok_var_names = list(self.OPTION_NL_MAP.keys()) + ok_sect_names = list(self.OPTION_NL_MAP.values()) + for name in ok_var_names: + self._available_profile_map[name] = [] + for id_, value in list(self.sub_ops.get_var_id_values().items()): + section, option = self.util.get_section_option_from_id(id_) + if (option in ok_var_names and + any(section.startswith(n) for n in ok_sect_names)): + self._profile_location_map.setdefault(option, {}) + self._profile_location_map[option].update({value: id_}) + self._available_profile_map.setdefault(option, []) + self._available_profile_map[option].append(value) + for profile_names in list(self._available_profile_map.values()): + profile_names.sort() + + def _package_add(self, package): + # Add a package of new requests, and profiles if needed. + sections_for_adding = [] + for sect_type, values in list(self._package_lookup[package].items()): + if sect_type == self.STREQ_NL_BASE: + sections_for_adding.extend(values) + else: + for profile_name in values: + sect = self._package_profile_lookup[sect_type].get( + profile_name) + sections_for_adding.append(sect) + sections_for_adding = sorted(set(sections_for_adding)) + for section in sections_for_adding: + opt_name_values = {} + node = self.package_config.get([section], no_ignore=True) + if node is None or not isinstance(node.value, dict): + continue + for opt, node in list(node.value.items()): + opt_name_values.update({opt: node.value}) + if section not in self.sections: + self.sub_ops.add_section(section, opt_map=opt_name_values) + + def _package_menu_launch(self, widget, event): + # Create a menu below the widget for package actions. + menu = Gtk.Menu() + packages = {} + for section, vars_ in list(self.variables.items()): + for var in vars_: + if var.name == self.STREQ_NL_PACKAGE_OPT: + is_ignored = (rose.variable.IGNORED_BY_USER in + self.sections[section].ignored_reason) + packages.setdefault(var.value, []) + packages[var.value].append(is_ignored) + for package in sorted(packages.keys()): + ignored_list = packages[package] + package_title = "Package: " + package + package_menuitem = Gtk.MenuItem(package_title) + package_menuitem.show() + package_menu = Gtk.Menu() + enable_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) + enable_menuitem.set_label(label="Enable all") + enable_menuitem._connect_args = (package, False) + enable_menuitem.connect( + "button-release-event", + lambda m, e: self._packages_enable(*m._connect_args)) + enable_menuitem.show() + enable_menuitem.set_sensitive(any(ignored_list)) + package_menu.append(enable_menuitem) + ignore_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) + ignore_menuitem.set_label(label="Ignore all") + ignore_menuitem._connect_args = (package, True) + ignore_menuitem.connect( + "button-release-event", + lambda m, e: self._packages_enable(*m._connect_args)) + ignore_menuitem.set_sensitive(any(not i for i in ignored_list)) + ignore_menuitem.show() + package_menu.append(ignore_menuitem) + remove_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) + remove_menuitem.set_label(label="Remove all") + remove_menuitem._connect_args = (package,) + remove_menuitem.connect( + "button-release-event", + lambda m, e: self._packages_remove(*m._connect_args)) + remove_menuitem.show() + package_menu.append(remove_menuitem) + package_menuitem.set_submenu(package_menu) + menu.append(package_menuitem) + menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) + menuitem.set_label(label="Import") + import_menu = Gtk.Menu() + new_packages = set(self._package_lookup.keys()) - set(packages.keys()) + for new_package in sorted(new_packages): + new_pack_menuitem = Gtk.MenuItem(label=new_package) + new_pack_menuitem._connect_args = (new_package,) + new_pack_menuitem.connect( + "button-release-event", + lambda m, e: self._package_add(*m._connect_args)) + new_pack_menuitem.show() + import_menu.append(new_pack_menuitem) + if not new_packages: + menuitem.set_sensitive(False) + menuitem.set_submenu(import_menu) + menuitem.show() + menu.append(menuitem) + menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) + menuitem.set_label(label="Disable all packages") + menuitem.connect("activate", + lambda i: self._packages_enable(disable=True)) + menuitem.show() + menu.append(menuitem) + menu.popup(None, None, widget.position_menu, event.button, + event.time, widget) + + def _packages_remove(self, only_this_package=None): + # Remove requests and no-longer-needed profiles for packages. + self._update_available_profiles() + sections_for_removing = [] + profile_streqs = {} + for section, vars_ in list(self.variables.items()): + for var in vars_: + if var.name == self.STREQ_NL_PACKAGE_OPT: + if (only_this_package is None or + var.value == only_this_package): + sect = self.util.get_section_option_from_id( + var.metadata["id"])[0] + if sect not in sections_for_removing: + sections_for_removing.append(sect) + elif var.name in self.OPTION_NL_MAP: + profile_streqs.setdefault(var.name, {}) + profile_streqs[var.name].setdefault(var.value, []) + profile_streqs[var.name][var.value].append(section) + streq_remove_list = list(sections_for_removing) + for profile_type in profile_streqs: + for name, streq_list in list(profile_streqs[profile_type].items()): + if all([s in streq_remove_list for s in streq_list]): + # This is only referenced by sections about to be removed. + profile_id = self._profile_location_map.get( + profile_type, {}).get(name) + if profile_id is None: + continue + profile_section = self.util.get_section_option_from_id( + profile_id)[0] + sections_for_removing.append(profile_section) + self.sub_ops.remove_sections(sections_for_removing) + + def _packages_enable(self, only_this_package=None, disable=False): + """Enable or user-ignore requests matching these packages.""" + sections_for_changing = [] + for vars_ in list(self.variables.values()): + for var in vars_: + if var.name == self.STREQ_NL_PACKAGE_OPT: + if (only_this_package is None or + var.value == only_this_package): + sect = self.util.get_section_option_from_id( + var.metadata["id"])[0] + if sect not in sections_for_changing: + is_ignored = (rose.variable.IGNORED_BY_USER in + self.sections[sect].ignored_reason) + if is_ignored != disable: + sections_for_changing.append(sect) + self.sub_ops.ignore_sections(sections_for_changing, disable) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py new file mode 100644 index 000000000..e1fcdc96a --- /dev/null +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -0,0 +1,694 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +from gi.repository import Pango +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import rose.config +import rose.config_editor +import rose.gtk.util + +import rose.config_editor.plugin.um.widget.stash_util as stash_util + + +class AddStashDiagnosticsPanelv1(Gtk.VBox): + + """Display a grouped set of stash requests to add.""" + + STASH_PARSE_DESC_OPT = "name" + STASH_PARSE_ITEM_OPT = "item" + STASH_PARSE_SECT_OPT = "sectn" + + def __init__(self, stash_lookup, request_lookup, + changed_request_lookup, stash_meta_lookup, + add_stash_request_func, + navigate_to_stash_request_func, + refresh_stash_requests_func): + """Create a widget displaying STASHmaster information. + + stash_lookup is a nested dictionary that uses STASH section + numbers and item numbers as a key chain to get the information + about a specific record - e.g. stash_lookup[1][0]["name"] may + return the 'name' (text description) for stash section 1, item + 0. + + request_lookup is a nested dictionary in the same form as stash + lookup (section numbers, item numbers), but then contains + a dictionary of relevant streq namelists vs option-value pairs + as a sub-level - e.g. request_lookup[1][0].keys() gives all the + relevant streq indices for stash section 1, item 0. + request_lookup[1][0]["0abcd123"]["dom_name"] may give the + domain profile name for the relevant namelist:streq(0abcd123). + + changed_request_lookup is a dictionary of changed streq + namelists (keys) and their change description text (values). + + stash_meta_lookup is a dictionary of STASHmaster property + names (keys) with value-metadata-dict key-value pairs (values). + To extract the metadata dict for a 'grid' value of "2", look + at stash_meta_lookup["grid=2"] which should be a dict of normal + Rose metadata key-value pairs such as: + {"description": "2 means Something something"}. + + add_stash_request_func is a hook function that should take a + STASH section number argument and a STASH item number argument, + and add this request as a new namelist in a configuration. + + navigate_to_stash_request_func is a hook function that should + take a streq namelist section id and search for it. It should + display it if found. + + refresh_stash_requests_func is a hook function that should call + the update_request_info method with updated streq namelist + info. + + """ + super(AddStashDiagnosticsPanelv1, self).__init__(self) + self.set_property("homogeneous", False) + self.stash_lookup = stash_lookup + self.request_lookup = request_lookup + self.changed_request_lookup = changed_request_lookup + self.stash_meta_lookup = stash_meta_lookup + self._add_stash_request = add_stash_request_func + self.navigate_to_stash_request = navigate_to_stash_request_func + self.refresh_stash_requests = refresh_stash_requests_func + self.group_index = 0 + self._visible_metadata_columns = ["Section"] + + # Automatically hide columns which have fixed-value metadata. + self._hidden_column_names = [] + for key, metadata in list(self.stash_meta_lookup.items()): + if "=" in key: + continue + values_string = metadata.get(rose.META_PROP_VALUES, "0, 1") + if len(rose.variable.array_split(values_string)) == 1: + self._hidden_column_names.append(key) + + self._should_show_meta_column_titles = False + self.control_widget_hbox = self._get_control_widget_hbox() + self.pack_start(self.control_widget_hbox, expand=False, fill=False) + self._view = rose.gtk.util.TooltipTreeView( + get_tooltip_func=self.set_tree_tip) + self._view.set_rules_hint(True) + self.sort_util = rose.gtk.util.TreeModelSortUtil( + self._view.get_model, 2) + self._view.show() + self._view.connect("button-press-event", + self._handle_button_press_event) + self._view.connect("cursor-changed", self._update_control_sensitivity) + self._window = Gtk.ScrolledWindow() + self._window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.generate_tree_view(is_startup=True) + self._window.add(self._view) + self._window.show() + self.pack_start(self._window, expand=True, fill=True) + self._update_control_sensitivity() + self.show() + + def add_cell_renderer_for_value(self, column): + """Add a cell renderer to represent the model value.""" + cell_for_value = Gtk.CellRendererText() + column.pack_start(cell_for_value, True, True, 0) + column.set_cell_data_func(cell_for_value, + self._set_tree_cell_value) + + def add_stash_request(self, section, item): + """Handle an add stash request call.""" + self._add_stash_request(section, item) + self.refresh_stash_requests() + + def generate_tree_view(self, is_startup=False): + """Create the summary of page data.""" + for column in self._view.get_columns(): + self._view.remove_column(column) + self._view.set_model(self.get_tree_model()) + for i, column_name in enumerate(self.column_names): + col = Gtk.TreeViewColumn() + if column_name in self._hidden_column_names: + col.set_visible(False) + col_title = column_name.replace("_", "__") + if self._should_show_meta_column_titles: + col_meta = self.stash_meta_lookup.get(column_name, {}) + title = col_meta.get(rose.META_PROP_TITLE) + if title is not None: + col_title = title + col.set_title(col_title) + self.add_cell_renderer_for_value(col) + if i < len(self.column_names) - 1: + col.set_resizable(True) + col.set_sort_column_id(i) + self._view.append_column(col) + if is_startup: + group_model = Gtk.TreeStore(str) + group_model.append(None, [""]) + for i, name in enumerate(self.column_names): + if name not in ["?", "#"]: + group_model.append(None, [name]) + self._group_widget.set_model(group_model) + self._group_widget.set_active(self.group_index + 1) + self._group_widget.connect("changed", self._handle_group_change) + self.update_request_info() + + def get_model_data_and_columns(self): + """Return a list of data tuples and columns""" + data_rows = [] + columns = ["Section", "Item", "Description", "?", "#"] + sections = list(self.stash_lookup.keys()) + sections.sort(self.sort_util.cmp_) + props_excess = [self.STASH_PARSE_DESC_OPT, self.STASH_PARSE_ITEM_OPT, + self.STASH_PARSE_SECT_OPT] + for section in sections: + if section == "-1": + continue + items = list(self.stash_lookup[section].keys()) + items.sort(self.sort_util.cmp_) + for item in items: + data = self.stash_lookup[section][item] + this_row = [section, item, data[self.STASH_PARSE_DESC_OPT]] + this_row += ["", ""] + for prop in sorted(data.keys()): + if prop not in props_excess: + this_row.append(data[prop]) + if prop not in columns: + columns.append(prop) + data_rows.append(this_row) + return data_rows, columns + + def get_tree_model(self): + """Construct a data model of other page data.""" + data_rows, cols = self.get_model_data_and_columns() + data_rows, cols, rows_are_descendants = self._apply_grouping( + data_rows, cols, self.group_index) + self.column_names = cols + if data_rows: + col_types = [str] * len(data_rows[0]) + else: + col_types = [] + self._store = Gtk.TreeStore(*col_types) + parent_iter = None + for i, row_data in enumerate(data_rows): + if rows_are_descendants is None: + self._store.append(None, row_data) + elif rows_are_descendants[i]: + self._store.append(parent_iter, row_data) + else: + parent_data = [row_data[0]] + [None] * len(row_data[1:]) + parent_iter = self._store.append(None, parent_data) + self._store.append(parent_iter, row_data) + filter_model = self._store.filter_new() + filter_model.set_visible_func(self._filter_visible) + sort_model = Gtk.TreeModelSort(filter_model) + for i in range(len(self.column_names)): + sort_model.set_sort_func(i, self.sort_util.sort_column, i) + sort_model.connect("sort-column-changed", + self.sort_util.handle_sort_column_change) + return sort_model + + def set_tree_tip(self, treeview, row_iter, col_index, tip): + """Add the hover-over text for a cell to 'tip'. + + treeview is the Gtk.TreeView object + row_iter is the Gtk.TreeIter for the row + col_index is the index of the Gtk.TreeColumn in + e.g. treeview.get_columns() + tip is the Gtk.Tooltip object that the text needs to be set in. + + """ + model = treeview.get_model() + stash_section_index = self.column_names.index("Section") + stash_item_index = self.column_names.index("Item") + stash_desc_index = self.column_names.index("Description") + stash_request_num_index = self.column_names.index("#") + stash_section = model.get_value(row_iter, stash_section_index) + stash_item = model.get_value(row_iter, stash_item_index) + stash_desc = model.get_value(row_iter, stash_desc_index) + stash_request_num = model.get_value(row_iter, stash_request_num_index) + if not stash_request_num or stash_request_num == "0": + stash_request_num = "None" + name = self.column_names[col_index] + value = model.get_value(row_iter, col_index) + help_ = None + if value is None: + return False + if name == "?": + name = "Requests Status" + if value == rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP: + value = "changed" + else: + value = "no changes" + elif name == "#": + name = "Requests" + if stash_request_num != "None": + sect_streqs = self.request_lookup.get(stash_section, {}) + streqs = list(sect_streqs.get(stash_item, {}).keys()) + streqs.sort(rose.config.sort_settings) + if streqs: + value = "\n " + "\n ".join(streqs) + else: + value = stash_request_num + " total" + if name == "Section": + meta_key = self.STASH_PARSE_SECT_OPT + "=" + value + elif name == "Description": + metadata = stash_util.get_stash_section_meta( + self.stash_meta_lookup, stash_section, stash_item, value + ) + help_ = metadata.get(rose.META_PROP_HELP) + meta_key = self.STASH_PARSE_DESC_OPT + "=" + value + else: + meta_key = name + "=" + value + value_meta = self.stash_meta_lookup.get(meta_key, {}) + title = value_meta.get(rose.META_PROP_TITLE, "") + if help_ is None: + help_ = value_meta.get(rose.META_PROP_HELP, "") + if title and not help_: + value += "\n" + title + if help_: + value += "\n" + rose.gtk.util.safe_str(help_) + text = name + ": " + str(value) + "\n\n" + text += "Section: " + str(stash_section) + "\n" + text += "Item: " + str(stash_item) + "\n" + text += "Description: " + str(stash_desc) + "\n" + if stash_request_num != "None": + text += str(stash_request_num) + " request(s)" + text = text.strip() + tip.set_text(text) + return True + + def update_request_info(self, request_lookup=None, + changed_request_lookup=None): + """Refresh streq namelist information.""" + if request_lookup is not None: + self.request_lookup = request_lookup + if changed_request_lookup is not None: + self.changed_request_lookup = changed_request_lookup + sect_col_index = self.column_names.index("Section") + item_col_index = self.column_names.index("Item") + streq_info_index = self.column_names.index("?") + num_streqs_index = self.column_names.index("#") + # For speed, pass in the relevant indices here. + user_data = (sect_col_index, item_col_index, + streq_info_index, num_streqs_index) + self._store.foreach(self._update_row_request_info, user_data) + # Loop over any parent rows and sum numbers and info. + parent_iter = self._store.iter_children(None) + while parent_iter is not None: + num_streq_children = 0 + streq_info_children = "" + child_iter = self._store.iter_children(parent_iter) + if child_iter is None: + parent_iter = self._store.iter_next(parent_iter) + continue + while child_iter is not None: + num = self._store.get_value(child_iter, num_streqs_index) + info = self._store.get_value(child_iter, streq_info_index) + if isinstance(num, str) and num.isdigit(): + num_streq_children += int(num) + if info and not streq_info_children: + streq_info_children = info + child_iter = self._store.iter_next(child_iter) + self._store.set_value(parent_iter, num_streqs_index, + str(num_streq_children)) + self._store.set_value(parent_iter, streq_info_index, + streq_info_children) + parent_iter = self._store.iter_next(parent_iter) + + def _update_row_request_info(self, model, path, iter_, user_data): + # Update the streq namelist information for a model row. + (sect_col_index, item_col_index, + streq_info_index, num_streqs_index) = user_data + section = model.get_value(iter_, sect_col_index) + item = model.get_value(iter_, item_col_index) + if section is None or item is None: + model.set_value(iter_, num_streqs_index, None) + model.set_value(iter_, streq_info_index, None) + return + streqs = self.request_lookup.get(section, {}).get(item, {}) + model.set_value(iter_, num_streqs_index, str(len(streqs))) + streq_info = "" + mod_markup = rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + for streq_section in streqs: + if streq_section in self.changed_request_lookup: + streq_info = mod_markup + streq_info + break + model.set_value(iter_, streq_info_index, streq_info) + + def _append_row_data(self, model, path, iter_, data_rows): + # Append new row data. + data_rows.append(model.get(iter_)) + + def _apply_grouping(self, data_rows, column_names, group_index=None, + descending=False): + # Calculate nesting (grouping) for the data. + rows_are_descendants = None + if group_index is None: + return data_rows, column_names, rows_are_descendants + k = group_index + data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] + column_names.insert(0, column_names.pop(k)) + data_rows.sort(lambda x, y: + self._sort_row_data(x, y, 0, descending)) + last_entry = None + rows_are_descendants = [] + for i, row in enumerate(data_rows): + if i > 0 and last_entry == row[0]: + rows_are_descendants.append(True) + else: + rows_are_descendants.append(False) + last_entry = row[0] + return data_rows, column_names, rows_are_descendants + + def _filter_refresh(self, widget=None): + # Hook function that reacts to a change in filter status. + self._view.get_model().get_model().refilter() + + def _filter_visible(self, model, iter_): + # This returns whether a row should be visible. + filt_text = self._filter_widget.get_text() + if not filt_text: + return True + for col_text in model.get(iter_, *list(range(len(self.column_names)))): + if (isinstance(col_text, str) and + filt_text.lower() in col_text.lower()): + return True + child_iter = model.iter_children(iter_) + while child_iter is not None: + if self._filter_visible(model, child_iter): + return True + child_iter = model.iter_next(child_iter) + return False + + def _get_control_widget_hbox(self): + # Build the control widgets for the dialog. + filter_label = Gtk.Label(label= + rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + filter_label.show() + self._filter_widget = Gtk.Entry() + self._filter_widget.set_width_chars( + rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + self._filter_widget.connect("changed", self._filter_refresh) + self._filter_widget.set_tooltip_text("Filter by literal values") + self._filter_widget.show() + group_label = Gtk.Label(label= + rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + group_label.show() + self._group_widget = Gtk.ComboBox() + cell = Gtk.CellRendererText() + self._group_widget.pack_start(cell, True, True, 0) + self._group_widget.add_attribute(cell, 'text', 0) + self._group_widget.show() + self._add_button = rose.gtk.util.CustomButton( + label="Add", + stock_id=Gtk.STOCK_ADD, + tip_text="Add a new request for this entry") + self._add_button.connect("activate", + lambda b: self._handle_add_current_row()) + self._add_button.connect("clicked", + lambda b: self._handle_add_current_row()) + self._refresh_button = rose.gtk.util.CustomButton( + label="Refresh", + stock_id=Gtk.STOCK_REFRESH, + tip_text="Refresh namelist:streq statuses") + self._refresh_button.connect("activate", + lambda b: self.refresh_stash_requests()) + self._refresh_button.connect("clicked", + lambda b: self.refresh_stash_requests()) + self._view_button = rose.gtk.util.CustomButton( + label="View", + tip_text="Select view options", + has_menu=True) + self._view_button.connect("button-press-event", + self._popup_view_menu) + filter_hbox = Gtk.HBox() + filter_hbox.pack_start(group_label, expand=False, fill=False) + filter_hbox.pack_start(self._group_widget, expand=False, fill=False) + filter_hbox.pack_start(filter_label, expand=False, fill=False, + padding=10) + filter_hbox.pack_start(self._filter_widget, expand=False, fill=False) + filter_hbox.pack_end(self._view_button, expand=False, fill=False) + filter_hbox.pack_end(self._refresh_button, expand=False, fill=False) + filter_hbox.pack_end(self._add_button, expand=False, fill=False) + filter_hbox.show() + return filter_hbox + + def _get_current_section_item(self): + """Return the current highlighted section and item.""" + current_path = self._view.get_cursor()[0] + if current_path is None: + return (None, None) + current_iter = self._view.get_model().get_iter(current_path) + return self._get_section_item_from_iter(current_iter) + + def _get_section_item_col_indices(self): + """Return the column indices of the STASH section and item.""" + sect_index = 0 + if self.group_index is not None and self.group_index != sect_index: + sect_index = 1 + item_index = 1 + if self.group_index is not None: + if self.group_index == 0: + item_index = 1 + elif self.group_index == 1: + item_index = 0 + else: + item_index = 2 + return sect_index, item_index + + def _get_section_item_from_iter(self, iter_): + """Return the STASH section and item numbers for this row.""" + sect_index, item_index = self._get_section_item_col_indices() + model = self._view.get_model() + section = model.get_value(iter_, sect_index) + item = model.get_value(iter_, item_index) + return section, item + + def _handle_add_current_row(self): + section, item = self._get_current_section_item() + return self.add_stash_request(section, item) + + def _handle_activation(self, view, path, column): + """React to an activation of a row in the dialog.""" + model = view.get_model() + row_iter = model.get_iter(path) + section, item = self._get_section_item_from_iter(row_iter) + if section is None or item is None: + return False + return self.add_stash_request(section, item) + + def _handle_button_press_event(self, treeview, event): + """React to a button press (mouse click).""" + pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) + if pathinfo is not None: + path, col = pathinfo[0:2] + if event.button != 3: + if event.type == Gdk._2BUTTON_PRESS: + self._handle_activation(treeview, path, col) + else: + self._popup_tree_menu(path, col, event) + + def _handle_group_change(self, combobox): + """Handle grouping (nesting) status changes.""" + model = combobox.get_model() + col_name = model.get_value(combobox.get_active_iter(), 0) + if col_name: + if col_name in self._hidden_column_names: + self._hidden_column_names.remove(col_name) + group_index = self.column_names.index(col_name) + # Any existing grouping changes the order of self.column_names. + if (self.group_index is not None and + group_index <= self.group_index): + group_index -= 1 + else: + group_index = None + if group_index == self.group_index: + return False + self.group_index = group_index + self.generate_tree_view() + return False + + def _launch_record_help(self, menuitem): + """Launch the help from a menu.""" + rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, + menuitem._help_title) + + def _popup_tree_menu(self, path, col, event): + """Launch a menu for this main treeview row.""" + menu = Gtk.Menu() + menu.show() + model = self._view.get_model() + row_iter = model.get_iter(path) + section, item = self._get_section_item_from_iter(row_iter) + if section is None or item is None: + return False + add_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) + add_menuitem.set_label("Add STASH request") + add_menuitem.connect("activate", + lambda i: self.add_stash_request(section, item)) + add_menuitem.show() + menu.append(add_menuitem) + stash_desc_index = self.column_names.index("Description") + stash_desc_value = model.get_value(row_iter, stash_desc_index) + desc_meta = self.stash_meta_lookup.get( + self.STASH_PARSE_DESC_OPT + "=" + str(stash_desc_value), {}) + desc_meta_help = desc_meta.get(rose.META_PROP_HELP) + if desc_meta_help is not None: + help_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) + help_menuitem.set_label("Help") + help_menuitem._help_text = desc_meta_help + help_menuitem._help_title = "Help for %s" % stash_desc_value + help_menuitem.connect("activate", self._launch_record_help) + help_menuitem.show() + menu.append(help_menuitem) + streqs = list(self.request_lookup.get(section, {}).get(item, {}).keys()) + if streqs: + view_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_FIND) + view_menuitem.set_label(label="View...") + view_menuitem.show() + view_menu = Gtk.Menu() + view_menu.show() + view_menuitem.set_submenu(view_menu) + streqs.sort(rose.config.sort_settings) + for streq in streqs: + view_streq_menuitem = Gtk.MenuItem(label=streq) + view_streq_menuitem._section = streq + view_streq_menuitem.connect( + "button-release-event", + lambda m, e: self.navigate_to_stash_request(m._section)) + view_streq_menuitem.show() + view_menu.append(view_streq_menuitem) + menu.append(view_menuitem) + menu.popup(None, None, None, event.button, event.time) + return False + + def _popup_view_menu(self, widget, event): + # Create a menu below the widget for view options. + menu = Gtk.Menu() + meta_menuitem = Gtk.CheckMenuItem(label="Show expanded value info") + if len(self.column_names) == len(self._visible_metadata_columns): + meta_menuitem.set_active(True) + meta_menuitem.connect("toggled", self._toggle_show_more_info) + meta_menuitem.show() + if not self.stash_meta_lookup: + meta_menuitem.set_sensitive(False) + menu.append(meta_menuitem) + col_title_menuitem = Gtk.CheckMenuItem( + label="Show expanded column titles") + if self._should_show_meta_column_titles: + col_title_menuitem.set_active(True) + col_title_menuitem.connect("toggled", + self._toggle_show_meta_column_titles) + col_title_menuitem.show() + if not self.stash_meta_lookup: + col_title_menuitem.set_sensitive(False) + menu.append(col_title_menuitem) + sep = Gtk.SeparatorMenuItem() + sep.show() + menu.append(sep) + show_column_menuitem = Gtk.MenuItem("Show/hide columns") + show_column_menuitem.show() + show_column_menu = Gtk.Menu() + show_column_menuitem.set_submenu(show_column_menu) + menu.append(show_column_menuitem) + for i, column in enumerate(self._view.get_columns()): + col_name = self.column_names[i] + col_title = col_name.replace("_", "__") + if self._should_show_meta_column_titles: + col_meta = self.stash_meta_lookup.get(col_name, {}) + title = col_meta.get(rose.META_PROP_TITLE) + if title is not None: + col_title = title + col_menuitem = Gtk.CheckMenuItem(label=col_title, + use_underline=False) + col_menuitem.show() + col_menuitem.set_active(column.get_visible()) + col_menuitem._connect_args = (col_name,) + col_menuitem.connect( + "toggled", + lambda c: self._toggle_show_column_name(*c._connect_args)) + show_column_menu.append(col_menuitem) + menu.popup(None, None, widget.position_menu, event.button, + event.time, widget) + + def _set_tree_cell_value(self, column, cell, treemodel, iter_): + # Extract an appropriate value for this cell from the model. + cell.set_property("visible", True) + col_index = self._view.get_columns().index(column) + col_title = self.column_names[col_index] + value = self._view.get_model().get_value(iter_, col_index) + if col_title in self._visible_metadata_columns and value is not None: + if col_title == "Section": + key = self.STASH_PARSE_SECT_OPT + "=" + value + else: + key = col_title + "=" + value + value_meta = self.stash_meta_lookup.get(key, {}) + title = value_meta.get(rose.META_PROP_TITLE, "") + if title: + value = title + desc = value_meta.get(rose.META_PROP_DESCRIPTION, "") + if desc: + value += ": " + desc + max_len = 36 + if value is not None and len(value) > max_len and col_index != 0: + cell.set_property("width-chars", max_len) + cell.set_property("ellipsize", Pango.EllipsizeMode.END) + if col_index == 0 and treemodel.iter_parent(iter_) is not None: + cell.set_property("visible", False) + if value is not None and col_title != "?": + value = rose.gtk.util.safe_str(value) + cell.set_property("markup", value) + + def _sort_row_data(self, row1, row2, sort_index, descending=False): + """Handle column sorting.""" + fac = (-1 if descending else 1) + return fac * self.sort_util.cmp_(row1[sort_index], row2[sort_index]) + + def _toggle_show_column_name(self, column_name): + """Handle a show/hide of a particular column.""" + col_index = self.column_names.index(column_name) + column = self._view.get_columns()[col_index] + if column.get_visible(): + return column.set_visible(False) + return column.set_visible(True) + + def _toggle_show_more_info(self, widget, column_name=None): + """Handle a show/hide of extra information.""" + should_show = widget.get_active() + if column_name is None: + column_names = self.column_names + else: + column_names = [column_name] + for name in column_names: + if should_show: + if name not in self._visible_metadata_columns: + self._visible_metadata_columns.append(name) + elif name in self._visible_metadata_columns: + if name != "Section": + self._visible_metadata_columns.remove(name) + self._view.columns_autosize() + + def _toggle_show_meta_column_titles(self, widget): + self._should_show_meta_column_titles = widget.get_active() + self.generate_tree_view() + + def _update_control_sensitivity(self, _=None): + section, item = self._get_current_section_item() + self._add_button.set_sensitive(section is not None and + item is not None) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_util.py b/metomi/rose/config_editor/plugin/um/widget/stash_util.py new file mode 100644 index 000000000..44879373e --- /dev/null +++ b/metomi/rose/config_editor/plugin/um/widget/stash_util.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This holds some shared functionality between stash and stash_add.""" + + +def get_stash_section_meta(stash_meta_lookup, stash_section, + stash_item, stash_description): + """Return a dictionary of metadata properties for this stash record.""" + try: + stash_code = 1000 * int(stash_section) + int(stash_item) + except (TypeError, ValueError): + return {} + meta_key = "code(%d)" % stash_code + return stash_meta_lookup.get(meta_key, {}) diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py new file mode 100644 index 000000000..e25be045d --- /dev/null +++ b/metomi/rose/config_editor/stack.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor + + +class StackItem(object): + + """A dictionary containing stack information.""" + + def __init__(self, page_label, action_text, node, + undo_function, undo_args=None, + group=None, custom_name=None): + self.page_label = page_label + self.action = action_text + self.node = node + if custom_name is None: + self.name = self.node.name + else: + self.name = custom_name + self.group = group + if hasattr(self.node, "value"): + self.value = self.node.value + self.old_value = self.node.old_value + else: + self.value = "" + self.old_value = "" + self.undo_func = undo_function + if undo_args is None: + undo_args = [] + self.undo_args = undo_args + + def __repr__(self): + return (self.action[0].lower() + self.action[1:] + ' ' + self.name + + ", ".join([str(u) for u in self.undo_args])) + + +class StackViewer(Gtk.Window): + + """Window to dynamically display the internal stack.""" + + def __init__(self, undo_stack, redo_stack, undo_func): + """Load a view of the stack.""" + super(StackViewer, self).__init__() + self.set_title(rose.config_editor.STACK_VIEW_TITLE) + self.action_colour_map = { + rose.config_editor.STACK_ACTION_ADDED: + rose.config_editor.COLOUR_STACK_ADDED, + rose.config_editor.STACK_ACTION_APPLIED: + rose.config_editor.COLOUR_STACK_APPLIED, + rose.config_editor.STACK_ACTION_CHANGED: + rose.config_editor.COLOUR_STACK_CHANGED, + rose.config_editor.STACK_ACTION_CHANGED_COMMENTS: + rose.config_editor.COLOUR_STACK_CHANGED_COMMENTS, + rose.config_editor.STACK_ACTION_ENABLED: + rose.config_editor.COLOUR_STACK_ENABLED, + rose.config_editor.STACK_ACTION_IGNORED: + rose.config_editor.COLOUR_STACK_IGNORED, + rose.config_editor.STACK_ACTION_REMOVED: + rose.config_editor.COLOUR_STACK_REMOVED, + rose.config_editor.STACK_ACTION_REVERSED: + rose.config_editor.COLOUR_STACK_REVERSED} + self.undo_func = undo_func + self.undo_stack = undo_stack + self.redo_stack = redo_stack + self.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + self.main_vbox = Gtk.VPaned() + accelerators = Gtk.AccelGroup() + accel_key, accel_mods = Gtk.accelerator_parse("Z") + accelerators.connect_group(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, + lambda a, b, c, d: self.undo_from_log()) + accel_key, accel_mods = Gtk.accelerator_parse("Z") + accelerators.connect_group(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, + lambda a, b, c, d: + self.undo_from_log(redo_mode_on=True)) + self.add_accel_group(accelerators) + self.set_default_size(*rose.config_editor.SIZE_STACK) + self.undo_view = self.get_stack_view(redo_mode_on=False) + self.redo_view = self.get_stack_view(redo_mode_on=True) + undo_vbox = self.get_stack_view_box(self.undo_view, + redo_mode_on=False) + redo_vbox = self.get_stack_view_box(self.redo_view, + redo_mode_on=True) + self.main_vbox.pack1(undo_vbox, resize=True, shrink=True) + self.main_vbox.show() + self.main_vbox.pack2(redo_vbox, resize=False, shrink=True) + self.main_vbox.show() + self.undo_view.connect('size-allocate', self.scroll_view) + self.redo_view.connect('size-allocate', self.scroll_view) + self.add(self.main_vbox) + self.show() + + def get_stack_view_box(self, log_buffer, redo_mode_on=False): + """Return a frame containing a scrolled text view.""" + text_view = log_buffer + text_scroller = Gtk.ScrolledWindow() + text_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + text_scroller.set_shadow_type(Gtk.ShadowType.IN) + text_scroller.add(text_view) + vadj = text_scroller.get_vadjustment() + vadj.set_value(vadj.upper - 0.9 * vadj.page_size) + text_scroller.show() + vbox = Gtk.VBox() + label = Gtk.Label() + if redo_mode_on: + label.set_text('REDO STACK') + self.redo_text_view = text_view + else: + label.set_text('UNDO STACK') + self.undo_text_view = text_view + label.show() + vbox.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + vbox.pack_start(label, expand=False, fill=True, + padding=rose.config_editor.SPACING_SUB_PAGE) + vbox.pack_start(text_scroller, expand=True, fill=True) + vbox.show() + return vbox + + def undo_from_log(self, redo_mode_on=False): + """Drive the main program undo function and update.""" + self.undo_func(redo_mode_on) + self.update() + + def get_stack_view(self, redo_mode_on=False): + """Return a tree view with information from a stack.""" + stack_model = self.get_stack_model(redo_mode_on, make_new_model=True) + stack_view = Gtk.TreeView(stack_model) + columns = {} + cell_text = {} + for title in [rose.config_editor.STACK_COL_NS, + rose.config_editor.STACK_COL_ACT, + rose.config_editor.STACK_COL_NAME, + rose.config_editor.STACK_COL_VALUE, + rose.config_editor.STACK_COL_OLD_VALUE]: + columns[title] = Gtk.TreeViewColumn() + columns[title].set_title(title) + cell_text[title] = Gtk.CellRendererText() + columns[title].pack_start(cell_text[title], True, True, 0) + columns[title].add_attribute(cell_text[title], attribute='markup', + column=len(list(columns.keys())) - 1) + stack_view.append_column(columns[title]) + stack_view.show() + return stack_view + + def get_stack_model(self, redo_mode_on=False, make_new_model=False): + """Return a Gtk.ListStore generated from a stack.""" + stack = [self.undo_stack, self.redo_stack][redo_mode_on] + if make_new_model: + model = Gtk.ListStore(str, str, str, str, str, bool) + else: + model = [self.undo_view.get_model(), + self.redo_view.get_model()][redo_mode_on] + model.clear() + for stack_item in stack: + marked_up_action = stack_item.action + if stack_item.action in self.action_colour_map: + colour = self.action_colour_map[stack_item.action] + marked_up_action = ("" + + stack_item.action + "") + if stack_item.page_label is None: + short_label = 'None' + else: + short_label = re.sub('^/[^/]+/', '', stack_item.page_label) + model.append((short_label, marked_up_action, + stack_item.name, repr(stack_item.value), + repr(stack_item.old_value), False)) + return model + + def scroll_view(self, tree_view, event=None): + """Scroll the parent scrolled window to the bottom.""" + vadj = tree_view.get_parent().get_vadjustment() + if vadj.upper > vadj.lower + vadj.page_size: + vadj.set_value(vadj.upper - 0.95 * vadj.page_size) + + def update(self): + """Reload text views from the undo and redo stacks.""" + if self.undo_text_view.get_parent() is None: + return + self.get_stack_model(redo_mode_on=False) + self.get_stack_model(redo_mode_on=True) diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py new file mode 100644 index 000000000..c15d275e6 --- /dev/null +++ b/metomi/rose/config_editor/status.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import datetime +import sys +import time + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.config_editor +import rose.gtk.console +import rose.reporter + + +class StatusReporter(rose.reporter.Reporter): + + """Handle event notification. + + load_updater must be a rose.gtk.splash.SplashScreenProcess + instance (or have the same interface to update and stop methods). + + status_bar_update_func must be a function that accepts a + rose.reporter.Event, a rose.reporter kind-of-event string, and a + level of importance/verbosity. See rose.reporter for more details. + + """ + + EVENT_KIND_LOAD = "load" + + def __init__(self, load_updater, status_bar_update_func): + self._load_updater = load_updater + self._status_bar_update_func = status_bar_update_func + self._no_load = False + + def event_handler(self, message, kind=None, level=None, prefix=None, + clip=None): + """Handle a message or event.""" + message_kwargs = {} + if isinstance(message, rose.reporter.Event): + if kind is None: + kind = message.kind + if level is None: + level = message.level + message_kwargs = message.kwargs + if kind == self.EVENT_KIND_LOAD and not self._no_load: + return self._load_updater.update(str(message), **message_kwargs) + return self._status_bar_update_func(message, kind, level) + + def report_load_event( + self, text, no_progress=False, new_total_events=None): + """Report a load-related event (to rose.gtk.util.SplashScreen).""" + event = rose.reporter.Event(text, + kind=self.EVENT_KIND_LOAD, + no_progress=no_progress, + new_total_events=new_total_events) + self.report(event) + + def set_no_load(self): + self._no_load = True + + def stop(self): + """Stop the updater.""" + self._load_updater.stop() + + +class StatusBar(Gtk.VBox): + + """Generate the status bar widget.""" + + def __init__(self, verbosity=rose.reporter.Reporter.DEFAULT): + super(StatusBar, self).__init__() + self.verbosity = verbosity + self.num_errors = 0 + self.console = None + hbox = Gtk.HBox() + hbox.show() + self.pack_start(hbox, expand=False, fill=False) + self._generate_error_widget() + hbox.pack_start(self._error_widget, expand=False, fill=False) + vsep_message = Gtk.VSeparator() + vsep_message.show() + vsep_eb = Gtk.EventBox() + vsep_eb.show() + hbox.pack_start(vsep_message, expand=False, fill=False) + hbox.pack_start(vsep_eb, expand=True, fill=True) + self._generate_message_widget() + hbox.pack_end(self._message_widget, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + self.messages = [] + self.show() + + def set_message(self, message, kind=None, level=None): + if isinstance(message, rose.reporter.Event): + if kind is None: + kind = message.kind + if level is None: + level = message.level + if level > self.verbosity: + return + if isinstance(message, Exception): + kind = rose.reporter.Reporter.KIND_ERR + level = rose.reporter.Reporter.FAIL + self.messages.append((kind, str(message), time.time())) + if len(self.messages) > rose.config_editor.STATUS_BAR_MESSAGE_LIMIT: + self.messages.pop(0) + self._update_message_widget(str(message), kind=kind) + self._update_console() + while Gdk.events_pending(): + Gtk.main_iteration() + + def set_num_errors(self, new_num_errors): + """Update the number of errors.""" + if new_num_errors != self.num_errors: + self.num_errors = new_num_errors + self._update_error_widget() + while Gdk.events_pending(): + Gtk.main_iteration() + + def _generate_error_widget(self): + # Generate the error display widget. + self._error_widget = Gtk.HBox() + self._error_widget.show() + locator = rose.resource.ResourceLocator(paths=sys.path) + icon_path = locator.locate( + 'etc/images/rose-config-edit/error_icon.xpm') + image = Gtk.image_new_from_file(icon_path) + image.show() + self._error_widget.pack_start(image, expand=False, fill=False) + self._error_widget_label = Gtk.Label() + self._error_widget_label.show() + self._error_widget.pack_start( + self._error_widget_label, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + self._update_error_widget() + + def _generate_message_widget(self): + # Generate the message display widget. + self._message_widget = Gtk.EventBox() + self._message_widget.show() + message_hbox = Gtk.HBox() + message_hbox.show() + self._message_widget.add(message_hbox) + self._message_widget.connect("enter-notify-event", + self._handle_enter_message_widget) + self._message_widget_error_image = Gtk.Image.new_from_stock( + Gtk.STOCK_DIALOG_ERROR, + Gtk.IconSize.MENU) + self._message_widget_info_image = Gtk.Image.new_from_stock( + Gtk.STOCK_DIALOG_INFO, + Gtk.IconSize.MENU) + self._message_widget_label = Gtk.Label() + self._message_widget_label.show() + vsep = Gtk.VSeparator() + vsep.show() + self._console_launcher = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_INFO, + size=Gtk.IconSize.MENU, + tip_text=rose.config_editor.STATUS_BAR_CONSOLE_TIP, + as_tool=True) + self._console_launcher.connect("clicked", self._launch_console) + style = Gtk.RcStyle() + style.xthickness = 0 + style.ythickness = 0 + setattr(style, "inner-border", [0, 0, 0, 0]) + self._console_launcher.modify_style(style) + message_hbox.pack_start( + self._message_widget_error_image, + expand=False, fill=False) + message_hbox.pack_start( + self._message_widget_info_image, + expand=False, fill=False) + message_hbox.pack_start( + self._message_widget_label, + expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + message_hbox.pack_start( + vsep, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + message_hbox.pack_start( + self._console_launcher, expand=False, fill=False) + + def _update_error_widget(self): + # Update the error display widget. + self._error_widget_label.set_text(str(self.num_errors)) + self._error_widget.set_sensitive((self.num_errors > 0)) + + def _update_message_widget(self, message_text, kind): + # Update the message display widget. + if kind == rose.reporter.Reporter.KIND_ERR: + self._message_widget_error_image.show() + self._message_widget_info_image.hide() + else: + self._message_widget_error_image.hide() + self._message_widget_info_image.show() + last_line = message_text.splitlines()[-1] + self._message_widget_label.set_text(last_line) + + def _handle_enter_message_widget(self, *args): + tooltip_text = "" + for kind, message_text, message_time in self.messages[-5:]: + if kind == rose.reporter.Reporter.KIND_ERR: + prefix = rose.reporter.Reporter.PREFIX_FAIL + else: + prefix = rose.reporter.Reporter.PREFIX_INFO + suffix = datetime.datetime.fromtimestamp(message_time).strftime( + rose.config_editor.EVENT_TIME) + tooltip_text += prefix + " " + message_text + " " + suffix + "\n" + tooltip_text = tooltip_text.rstrip() + self._message_widget_label.set_tooltip_text(tooltip_text) + + def _get_console_messages(self): + err_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + info_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + message_tuples = [] + for kind, message, time_info in self.messages: + if kind == rose.reporter.Reporter.KIND_ERR: + category = err_category + else: + category = info_category + message_tuples.append((category, message, time_info)) + return message_tuples + + def _handle_destroy_console(self): + self.console = None + + def _launch_console(self, *args): + if self.console is not None: + return self.console.present() + message_tuples = self._get_console_messages() + err_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + info_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + window = self.get_toplevel() + self.console = rose.gtk.console.ConsoleWindow( + [err_category, info_category], message_tuples, + [Gtk.STOCK_DIALOG_ERROR, Gtk.STOCK_DIALOG_INFO], + parent=window, + destroy_hook=self._handle_destroy_console) + + def _update_console(self): + if self.console is not None: + self.console.update_messages(self._get_console_messages()) diff --git a/metomi/rose/config_editor/updater.py b/metomi/rose/config_editor/updater.py new file mode 100644 index 000000000..b5634b1f4 --- /dev/null +++ b/metomi/rose/config_editor/updater.py @@ -0,0 +1,760 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import rose.config_editor + + +class Updater(object): + + """This handles the updating of various statuses and displays.""" + + def __init__(self, data, util, reporter, mainwindow, main_handle, + nav_controller, get_pagelist_func, + update_bar_widgets_func, + refresh_metadata_func, + is_pluggable=False): + self.data = data + self.util = util + self.reporter = reporter + self.mainwindow = mainwindow + self.main_handle = main_handle + self.nav_controller = nav_controller + self.get_pagelist_func = get_pagelist_func + self.pagelist = [] # This is the current list of pages open. + self.load_errors = 0 + self.update_bar_widgets_func = update_bar_widgets_func + self.refresh_metadata_func = refresh_metadata_func + self.is_pluggable = is_pluggable + self.nav_panel = None # This may be set later. + + def namespace_data_is_modified(self, namespace): + """Return a string for namespace modifications or null string.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + if config_name is None: + return "" + config_data = self.data.config[config_name] + config_sections = config_data.sections + if config_name == namespace: + # This is the top-level. + if config_name not in self.data.saved_config_names: + return rose.config_editor.TREE_PANEL_TIP_ADDED_CONFIG + section_hashes = [] + for sect_data in list(config_sections.now.values()): + section_hashes.append(sect_data.to_hashable()) + old_section_hashes = [] + for sect_data in list(config_sections.save.values()): + old_section_hashes.append(sect_data.to_hashable()) + if set(section_hashes) ^ set(old_section_hashes): + return rose.config_editor.TREE_PANEL_TIP_CHANGED_CONFIG + allowed_sections = self.data.helper.get_sections_from_namespace( + namespace) + save_var_map = {} + for section in allowed_sections: + for var in config_data.vars.save.get(section, []): + if var.metadata['full_ns'] == namespace: + save_var_map.update({var.metadata['id']: var}) + for var in config_data.vars.now.get(section, []): + if var.metadata['full_ns'] == namespace: + var_id = var.metadata['id'] + save_var = save_var_map.get(var_id) + if save_var is None: + return rose.config_editor.TREE_PANEL_TIP_ADDED_VARS + if save_var.to_hashable() != var.to_hashable(): + # Variable has changed in some form. + return rose.config_editor.TREE_PANEL_TIP_CHANGED_VARS + save_var_map.pop(var_id) + if save_var_map: + # Some variables are now absent. + return rose.config_editor.TREE_PANEL_TIP_REMOVED_VARS + if self.data.helper.get_ns_is_default(namespace): + sections = self.data.helper.get_sections_from_namespace(namespace) + for section in sections: + sect_data = config_sections.now.get(section) + save_sect_data = config_sections.save.get(section) + if (sect_data is None) != (save_sect_data is None): + return rose.config_editor.TREE_PANEL_TIP_DIFF_SECTIONS + if sect_data is not None and save_sect_data is not None: + if sect_data.to_hashable() != save_sect_data.to_hashable(): + return ( + rose.config_editor.TREE_PANEL_TIP_CHANGED_SECTIONS) + return "" + + def update_ns_tree_states(self, namespace): + """Refresh the tree panel states for a single row (namespace).""" + if self.nav_panel is not None: + latent_status = self.data.helper.get_ns_latent_status(namespace) + ignored_status = self.data.helper.get_ns_ignored_status(namespace) + ns_names = namespace.lstrip("/").split("/") + self.nav_panel.update_statuses(ns_names, latent_status, + ignored_status) + + def tree_trigger_update(self, only_this_config=None, + only_this_namespace=None): + """Reload the tree panel, and perform an update. + + If only_this_config is not None, perform an update only on the + particular configuration namespaces. + + If only_this_namespace is not None, perform a selective update + to save time. + + """ + if self.nav_panel is not None: + self.nav_panel.load_tree(None, + self.nav_controller.namespace_tree) + if only_this_namespace is None: + self.update_all(only_this_config=only_this_config) + else: + self.update_all(skip_checking=True, skip_sub_data_update=True) + spaces = only_this_namespace.lstrip("/").split("/") + for i in range(len(spaces), 0, -1): + update_ns = "/" + "/".join(spaces[:i]) + self.update_namespace(update_ns, + skip_sub_data_update=True) + self.update_ns_sub_data(only_this_namespace) + + def refresh_ids(self, config_name, setting_ids, is_loading=False, + are_errors_done=False, skip_update=False): + """Refresh and redraw settings if needed.""" + self.pagelist = self.get_pagelist_func() + nses_to_do = [] + for changed_id in setting_ids: + sect, opt = self.util.get_section_option_from_id(changed_id) + if opt is None: + name = self.data.helper.get_default_section_namespace( + sect, config_name) + if name in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(name) + page = self.pagelist[index] + page.refresh() + else: + var = self.data.helper.get_ns_variable(changed_id, + config_name) + if var is None: + continue + name = var.metadata['full_ns'] + if name in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(name) + page = self.pagelist[index] + page.refresh(changed_id) + if name not in nses_to_do and not are_errors_done: + nses_to_do.append(name) + if not skip_update: + for name in nses_to_do: + self.update_namespace(name, is_loading=is_loading, + skip_sub_data_update=True) + self.update_ns_sub_data(nses_to_do) + + def update_all(self, only_this_config=None, is_loading=False, + skip_checking=False, skip_sub_data_update=False): + """Loop over all namespaces and update.""" + unique_namespaces = self.data.helper.get_all_namespaces( + only_this_config) + + for name in unique_namespaces: + self.data.helper.clear_namespace_cached_statuses(name) + + if only_this_config is None: + configs = list(self.data.config.keys()) + else: + configs = [only_this_config] + for config_name in configs: + self.update_config(config_name) + self.pagelist = self.get_pagelist_func() + + if not skip_checking: + for name in unique_namespaces: + if name in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(name) + page = self.pagelist[index] + self.sync_page_var_lists(page) + self.update_ignored_statuses(name) + self.update_ns_tree_states(name) + self.perform_error_check(is_loading=is_loading) + + for name in unique_namespaces: + if name in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(name) + page = self.pagelist[index] + self.update_tree_status(page) # Faster. + else: + self.update_tree_status(name) + self.update_bar_widgets_func() + self.update_stack_viewer_if_open() + for config_name in configs: + self.update_metadata_id(config_name) + if not skip_sub_data_update: + self.update_ns_sub_data() + + def update_namespace(self, namespace, are_errors_done=False, + is_loading=False, + skip_sub_data_update=False): + """Update driver function. Updates the page if open.""" + self.pagelist = self.get_pagelist_func() + self.data.helper.clear_namespace_cached_statuses(namespace) + if namespace in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(namespace) + page = self.pagelist[index] + self.update_status(page, are_errors_done=are_errors_done, + skip_sub_data_update=skip_sub_data_update) + else: + self.update_sections(namespace) + self.update_ignored_statuses(namespace) + if not are_errors_done and not is_loading: + self.perform_error_check(namespace) + self.update_tree_status(namespace) + if not is_loading: + self.update_bar_widgets_func() + self.update_stack_viewer_if_open() + self.update_ns_tree_states(namespace) + if namespace in list(self.data.config.keys()): + self.update_metadata_id(namespace) + if not skip_sub_data_update: + self.update_ns_sub_data(namespace) + + def update_status(self, page, are_errors_done=False, + skip_sub_data_update=False): + """Update ignored statuses and update the tree statuses.""" + self.pagelist = self.get_pagelist_func() + self.sync_page_var_lists(page) + self.update_sections(page.namespace) + self.update_ignored_statuses(page.namespace) + if not are_errors_done: + self.perform_error_check(page.namespace) + self.update_tree_status(page) + self.update_bar_widgets_func() + self.update_stack_viewer_if_open() + page.update_info() + self.update_ns_tree_states(page.namespace) + if page.namespace in list(self.data.config.keys()): + self.update_metadata_id(page.namespace) + if not skip_sub_data_update: + self.update_ns_sub_data(page.namespace) + + def update_ns_sub_data(self, namespaces=None): + """Update any relevant summary data on another page.""" + if not isinstance(namespaces, list): + namespaces = [namespaces] + for page in self.pagelist: + for namespace in namespaces: + if (namespace is None or + namespace.startswith(page.namespace)): + break + else: + # No namespaces matched this page, skip + continue + page.sub_data = self.data.helper.get_sub_data_for_namespace( + page.namespace) + page.update_sub_data() + + def update_ns_info(self, namespace): + if namespace in [p.namespace for p in self.pagelist]: + index = [p.namespace for p in self.pagelist].index(namespace) + page = self.pagelist[index] + page.update_ignored() + page.update_info() + + def sync_page_var_lists(self, page): + """Make sure the list of page variables has the right members.""" + real, miss = self.data.helper.get_data_for_namespace(page.namespace) + page_real, page_miss = page.panel_data, page.ghost_data + refresh_vars = [] + action_vsets = [(page_real.remove, set(page_real) - set(real)), + (page_real.append, set(real) - set(page_real)), + (page_miss.remove, set(page_miss) - set(miss)), + (page_miss.append, set(miss) - set(page_miss))] + + for action, v_set in action_vsets: + for var in v_set: + if var not in refresh_vars: + refresh_vars.append(var) + for var in v_set: + action(var) + for var in refresh_vars: + page.refresh(var.metadata['id']) + + def update_config(self, namespace): + """Update the config object for the macros.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config = self.data.dump_to_internal_config(config_name) + self.data.config[config_name].config = config + + def update_sections(self, namespace): + """Update the list of sections that are empty.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + ns_sections = self.data.helper.get_sections_from_namespace(namespace) + for section in ns_sections: + sect_data = config_data.sections.now.get(section) + if sect_data is None: + continue + variables = config_data.vars.now.get(section, []) + sect_data.options = [] + if not variables: + if section in config_data.vars.now: + config_data.vars.now.pop(section) + for variable in variables: + var_id = variable.metadata['id'] + option = self.util.get_section_option_from_id(var_id)[1] + sect_data.options.append(option) + + def update_ignored_statuses(self, namespace): + """Refresh the list of ignored variables and update relevant pages.""" + config_name = self.util.split_full_ns(self.data, namespace)[0] + config_data = self.data.config[config_name] + # Check for triggering variables that have changed values + self.data.trigger_id_value_lookup.setdefault(config_name, {}) + trig_id_val_dict = self.data.trigger_id_value_lookup[config_name] + trigger = self.data.trigger[config_name] + updated_ids = [] + + this_ns_triggers = [] + ns_vars, ns_l_vars = self.data.helper.get_data_for_namespace(namespace) + for var in ns_vars + ns_l_vars: + var_id = var.metadata['id'] + if not trigger.check_is_id_trigger(var_id, config_data.meta): + continue + if var in ns_l_vars: + new_val = None + else: + new_val = var.value + old_val = trig_id_val_dict.get(var_id) + if old_val != new_val: # new_val or old_val can be None + this_ns_triggers.append(var_id) + updated_ids += self.update_ignoreds(config_name, + var_id) + + if not this_ns_triggers: + # No reason to update anything. + return False + + var_id_map = {} + for var in config_data.vars.get_all(skip_latent=True): + var_id = var.metadata['id'] + var_id_map.update({var_id: var}) + + update_nses = [] + update_section_nses = [] + for setting_id in updated_ids: + sect, opt = self.util.get_section_option_from_id(setting_id) + if opt is None: + sect_vars = config_data.vars.now.get(sect, []) + name = self.data.helper.get_default_section_namespace( + sect, config_name) + if name not in update_section_nses: + update_section_nses.append(name) + else: + sect_vars = list(config_data.vars.now.get(sect, [])) + sect_vars += list(config_data.vars.latent.get(sect, [])) + for var in list(sect_vars): + if var.metadata['id'] != setting_id: + sect_vars.remove(var) + for var in sect_vars: + var_ns = var.metadata['full_ns'] + var_id = var.metadata['id'] + vsect = self.util.get_section_option_from_id(var_id)[0] + if var_ns not in update_nses: + update_nses.append(var_ns) + if vsect in updated_ids and var_ns not in update_section_nses: + update_section_nses.append(var_ns) + for page in self.pagelist: + if page.namespace in update_nses: + page.update_ignored() # Redraw affected widgets. + if page.namespace in update_section_nses: + page.update_info() + for name in update_nses: + if name != namespace: + # We don't need another update of namespace. + self.update_namespace(name) + for var_id in list(trig_id_val_dict.keys()) + updated_ids: + var = var_id_map.get(var_id) + if var is None: + if var_id in trig_id_val_dict: + trig_id_val_dict.pop(var_id) + else: + trig_id_val_dict.update({var_id: var.value}) + + def update_ignoreds(self, config_name, var_id): + """Update the variable ignored flags ('reasons').""" + config_data = self.data.config[config_name] + trigger = self.data.trigger[config_name] + + meta_config = config_data.meta + config_sections = config_data.sections + config_data_for_trigger = {"sections": config_sections.now, + "variables": config_data.vars.now} + update_ids = trigger.update(var_id, config_data_for_trigger, + meta_config) + update_vars = [] + update_sections = [] + for setting_id in update_ids: + section, option = self.util.get_section_option_from_id(setting_id) + if option is None: + update_sections.append(section) + else: + for var in config_data.vars.now.get(section, []): + if var.metadata['id'] == setting_id: + update_vars.append(var) + break + else: + for var in config_data.vars.latent.get(section, []): + if var.metadata['id'] == setting_id: + update_vars.append(var) + break + triggered_ns_list = [] + this_id = var_id + for namespace, metadata in list(self.data.namespace_meta_lookup.items()): + this_name = self.util.split_full_ns(self.data, namespace) + if this_name != config_name: + continue + for section in update_sections: + if section in metadata['sections']: + triggered_ns_list.append(namespace) + + # Update the sections. + enabled_sections = [s for s in update_sections + if s in trigger.enabled_dict and + s not in trigger.ignored_dict] + for section in update_sections: + # Clear pre-existing errors. + sect_vars = (config_data.vars.now.get(section, []) + + config_data.vars.latent.get(section, [])) + sect_data = config_sections.now.get(section) + if sect_data is None: + sect_data = config_sections.latent[section] + for attribute in rose.config_editor.WARNING_TYPES_IGNORE: + if attribute in sect_data.error: + sect_data.error.pop(attribute) + reason = sect_data.ignored_reason + if section in enabled_sections: + # Trigger-enabled sections + if (rose.variable.IGNORED_BY_USER in reason): + # User-ignored but trigger-enabled + if (meta_config.get( + [section, rose.META_PROP_COMPULSORY]).value == + rose.META_PROP_VALUE_TRUE): + # Doc table: I_u -> E -> compulsory + sect_data.error.update( + {rose.config_editor.WARNING_TYPE_USER_IGNORED: + rose.config_editor.WARNING_NOT_USER_IGNORABLE}) + elif (rose.variable.IGNORED_BY_SYSTEM in reason): + # Normal trigger-enabled sections + reason.pop(rose.variable.IGNORED_BY_SYSTEM) + for var in sect_vars: + name = var.metadata['full_ns'] + if name not in triggered_ns_list: + triggered_ns_list.append(name) + var.ignored_reason.pop( + rose.variable.IGNORED_BY_SECTION, None) + elif section in trigger.ignored_dict: + # Trigger-ignored sections + parents = trigger.ignored_dict.get(section, {}) + if parents: + help_text = "; ".join(list(parents.values())) + else: + help_text = rose.config_editor.IGNORED_STATUS_DEFAULT + reason.update({rose.variable.IGNORED_BY_SYSTEM: help_text}) + for var in sect_vars: + name = var.metadata['full_ns'] + if name not in triggered_ns_list: + triggered_ns_list.append(name) + var.ignored_reason.update( + {rose.variable.IGNORED_BY_SECTION: help_text}) + # Update the variables. + for var in update_vars: + var_id = var.metadata.get('id') + name = var.metadata.get('full_ns') + if name not in triggered_ns_list: + triggered_ns_list.append(name) + if var_id == this_id: + continue + for attribute in rose.config_editor.WARNING_TYPES_IGNORE: + if attribute in var.error: + var.error.pop(attribute) + if (var_id in trigger.enabled_dict and + var_id not in trigger.ignored_dict): + # Trigger-enabled variables + if rose.variable.IGNORED_BY_USER in var.ignored_reason: + # User-ignored but trigger-enabled + # Doc table: I_u -> E + if (var.metadata.get(rose.META_PROP_COMPULSORY) == + rose.META_PROP_VALUE_TRUE): + # Doc table: I_u -> E -> compulsory + var.error.update( + {rose.config_editor.WARNING_TYPE_USER_IGNORED: + rose.config_editor.WARNING_NOT_USER_IGNORABLE}) + elif (rose.variable.IGNORED_BY_SYSTEM in + var.ignored_reason): + # Normal trigger-enabled variables + var.ignored_reason.pop(rose.variable.IGNORED_BY_SYSTEM) + elif var_id in trigger.ignored_dict: + # Trigger-ignored variables + parents = trigger.ignored_dict.get(var_id, {}) + if parents: + help_text = "; ".join(list(parents.values())) + else: + help_text = rose.config_editor.IGNORED_STATUS_DEFAULT + var.ignored_reason.update( + {rose.variable.IGNORED_BY_SYSTEM: help_text}) + for namespace in triggered_ns_list: + self.update_tree_status(namespace) + return update_ids + + def update_tree_status(self, page_or_ns, icon_bool=None, icon_type=None): + """Update the tree statuses.""" + if self.nav_panel is None: + return + if isinstance(page_or_ns, str): + namespace = page_or_ns + config_name = self.util.split_full_ns(self.data, namespace)[0] + errors = [] + ns_vars, ns_l_vars = self.data.helper.get_data_for_namespace( + namespace) + for var in ns_vars + ns_l_vars: + errors += list(var.error.items()) + else: + namespace = page_or_ns.namespace + config_name = self.util.split_full_ns(self.data, namespace)[0] + errors = page_or_ns.validate_errors() + # Add section errors. + config_data = self.data.config[config_name] + ns_sections = self.data.helper.get_sections_from_namespace(namespace) + for section in ns_sections: + if section in config_data.sections.now: + errors += list(config_data.sections.now[section].error.items()) + elif section in config_data.sections.latent: + errors += list(config_data.sections.latent[section].error.items()) + + # Set icons. + name_tree = namespace.lstrip('/').split('/') + if icon_bool is None: + if icon_type == 'changed' or icon_type is None: + change = self.namespace_data_is_modified(namespace) + self.nav_panel.update_change(name_tree, change) + self.nav_panel.set_row_icon(name_tree, bool(change), + ind_type='changed') + if icon_type == 'error' or icon_type is None: + self.nav_panel.set_row_icon(name_tree, len(errors), + ind_type='error') + else: + self.nav_panel.set_row_icon(name_tree, icon_bool, + ind_type=icon_type) + + def update_stack_viewer_if_open(self): + """Update the information in the stack viewer, if open.""" + if self.is_pluggable: + return False + if isinstance(self.mainwindow.log_window, + rose.config_editor.stack.StackViewer): + self.mainwindow.log_window.update() + + def focus_sub_page_if_open(self, namespace, node_id): + """Focus the sub (summary) page for a namespace and id.""" + if "/" not in namespace: + return False + summary_namespace = namespace.rsplit("/", 1)[0] + self.pagelist = self.get_pagelist_func() + page_namespaces = [p.namespace for p in self.pagelist] + if summary_namespace not in page_namespaces: + return False + page = self.pagelist[page_namespaces.index(summary_namespace)] + page.set_sub_focus(node_id) + + def update_metadata_id(self, config_name): + """Update the metadata if the id has changed.""" + config_data = self.data.config[config_name] + new_meta_id = self.data.helper.get_config_meta_flag(config_name) + if config_data.meta_id != new_meta_id: + config_data.meta_id = new_meta_id + self.refresh_metadata_func(config_name=config_name) + + def perform_startup_check(self): + """Fix any relevant type errors.""" + for config_name in self.data.config: + macro_config = self.data.dump_to_internal_config(config_name) + meta_config = self.data.config[config_name].meta + # Duplicate checking + dupl_checker = rose.macros.duplicate.DuplicateChecker() + problem_list = dupl_checker.validate(macro_config, meta_config) + if problem_list: + self.main_handle.handle_macro_validation( + config_name, + 'duplicate.DuplicateChecker.validate', + macro_config, problem_list, no_display=True) + format_checker = rose.macros.format.FormatChecker() + problem_list = format_checker.validate(macro_config, meta_config) + if problem_list: + self.main_handle.handle_macro_validation( + config_name, 'format.FormatChecker.validate', + macro_config, problem_list) + + def perform_error_check(self, namespace=None, is_loading=False): + """Loop through system macros and sum errors.""" + configs = list(self.data.config.keys()) + if namespace is not None: + config_name = self.util.split_full_ns(self.data, + namespace)[0] + configs = [config_name] + # Compulsory checking. + for config_name in configs: + config_data = self.data.config[config_name] + meta = config_data.meta + checker = ( + self.data.builtin_macros[config_name][ + rose.META_PROP_COMPULSORY]) + only_these_sections = None + if namespace is not None: + only_these_sections = ( + self.data.helper.get_sections_from_namespace(namespace)) + config_data_for_compulsory = { + "sections": config_data.sections.now, + "variables": config_data.vars.now + } + bad_list = checker.validate_settings( + config_data_for_compulsory, config_data.meta, + only_these_sections=only_these_sections + ) + self.apply_macro_validation(config_name, + rose.META_PROP_COMPULSORY, bad_list, + namespace, is_loading=is_loading, + is_macro_dynamic=True) + # Value checking. + for config_name in configs: + config_data = self.data.config[config_name] + meta = config_data.meta + checker = ( + self.data.builtin_macros[config_name][rose.META_PROP_TYPE]) + if namespace is None: + real_variables = config_data.vars.get_all(skip_latent=True) + else: + real_variables = ( + self.data.helper.get_data_for_namespace(namespace)[0]) + bad_list = checker.validate_variables(real_variables, meta) + self.apply_macro_validation(config_name, rose.META_PROP_TYPE, + bad_list, + namespace, is_loading=is_loading, + is_macro_dynamic=True) + + def apply_macro_validation(self, config_name, macro_type, bad_list=None, + namespace=None, is_loading=False, + is_macro_dynamic=False): + """Display error icons if a variable is in the wrong state.""" + if bad_list is None: + bad_list = [] + config_data = self.data.config[config_name] + config_sections = config_data.sections + variables = config_data.vars.get_all() + id_error_dict = {} + id_warn_dict = {} + if namespace is None: + ok_sections = (list(config_sections.now.keys()) + + list(config_sections.latent.keys())) + ok_variables = variables + else: + ok_sections = self.data.helper.get_sections_from_namespace( + namespace) + ok_variables = [v for v in variables + if v.metadata.get('full_ns') == namespace] + for section in ok_sections: + sect_data = config_sections.now.get(section) + if sect_data is None: + sect_data = config_sections.latent.get(section) + if sect_data is None: + continue + if macro_type in sect_data.error: + this_error = sect_data.error.pop(macro_type) + id_error_dict.update({section: this_error}) + if macro_type in sect_data.warning: + this_warning = sect_data.warning.pop(macro_type) + id_warn_dict.update({section: this_warning}) + for var in ok_variables: + if macro_type in var.error: + this_error = var.error.pop(macro_type) + id_error_dict.update({var.metadata['id']: this_error}) + if macro_type in var.warning: + this_warning = var.warning.pop(macro_type) + id_warn_dict.update({var.metadata['id']: this_warning}) + if not bad_list: + self.refresh_ids(config_name, + list(id_error_dict.keys()) + list(id_warn_dict.keys()), + is_loading, are_errors_done=is_macro_dynamic) + return + for bad_report in bad_list: + section = bad_report.section + key = bad_report.option + info = bad_report.info + if key is None: + setting_id = section + if (namespace is not None and section not in + self.data.helper.get_sections_from_namespace( + namespace)): + continue + sect_data = config_sections.now.get(section) + if sect_data is None: + sect_data = config_sections.latent.get(section) + if sect_data is None: + continue + if bad_report.is_warning: + sect_data.warning.setdefault(macro_type, info) + else: + sect_data.error.setdefault(macro_type, info) + else: + setting_id = self.util.get_id_from_section_option(section, key) + var = self.data.helper.get_variable_by_id(setting_id, + config_name) + if var is None: + var = self.data.helper.get_variable_by_id(setting_id, + config_name, + latent=True) + if var is None: + continue + if (namespace is not None and + var.metadata['full_ns'] != namespace): + continue + if bad_report.is_warning: + var.warning.setdefault(macro_type, info) + else: + var.error.setdefault(macro_type, info) + if bad_report.is_warning: + map_ = id_warn_dict + else: + map_ = id_error_dict + if is_loading: + self.load_errors += 1 + update_text = rose.config_editor.EVENT_LOAD_ERRORS.format( + self.data.top_level_name, self.load_errors) + + self.reporter.report_load_event(update_text, + no_progress=True) + if setting_id in map_: + # No need for further update, already had warning/error. + map_.pop(setting_id) + else: + # New warning or error. + map_.update({setting_id: info}) + self.refresh_ids(config_name, + list(id_error_dict.keys()) + list(id_warn_dict.keys()), + is_loading, + are_errors_done=is_macro_dynamic) + + def apply_macro_transform(self, config_name, changed_ids, + skip_update=False): + """Refresh pages with changes.""" + self.refresh_ids(config_name, changed_ids, skip_update=skip_update) diff --git a/metomi/rose/config_editor/upgrade_controller.py b/metomi/rose/config_editor/upgrade_controller.py new file mode 100644 index 000000000..7504ecaa8 --- /dev/null +++ b/metomi/rose/config_editor/upgrade_controller.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import copy +import os + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import GObject + +import rose.gtk.util +import rose.macro +import rose.upgrade + + +class UpgradeController(object): + + """Configure the upgrade of configurations.""" + + def __init__(self, app_config_dict, handle_transform_func, + parent_window=None, upgrade_inspector=None): + buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT) + self.window = Gtk.Dialog(buttons=buttons) + self.set_transient_for(parent_window) + self.window.set_title(rose.config_editor.DIALOG_TITLE_UPGRADE) + self.config_dict = {} + self.config_directory_dict = {} + self.config_manager_dict = {} + config_names = sorted(app_config_dict.keys()) + self._config_version_model_dict = {} + self.use_all_versions = False + self.treemodel = Gtk.TreeStore(str, str, str, bool) + self.treeview = rose.gtk.util.TooltipTreeView( + get_tooltip_func=self._get_tooltip) + self.treeview.show() + old_pwd = os.getcwd() + for config_name in config_names: + app_config = app_config_dict[config_name]["config"] + app_directory = app_config_dict[config_name]["directory"] + meta_value = app_config.get_value([rose.CONFIG_SECT_TOP, + rose.CONFIG_OPT_META_TYPE], "") + if len(meta_value.split("/")) < 2: + continue + try: + os.chdir(app_directory) + manager = rose.upgrade.MacroUpgradeManager(app_config) + except OSError: + # This can occur when access is not allowed to metadata files. + continue + self.config_dict[config_name] = app_config + self.config_directory_dict[config_name] = app_directory + self.config_manager_dict[config_name] = manager + self._update_treemodel_data(config_name) + os.chdir(old_pwd) + self.treeview.set_model(self.treemodel) + self.treeview.set_rules_hint(True) + self.treewindow = Gtk.ScrolledWindow() + self.treewindow.show() + self.treewindow.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.NEVER) + columns = rose.config_editor.DIALOG_COLUMNS_UPGRADE + for i, title in enumerate(columns): + column = Gtk.TreeViewColumn() + column.set_title(title) + if self.treemodel.get_column_type(i) == GObject.TYPE_BOOLEAN: + cell = Gtk.CellRendererToggle() + cell.connect("toggled", self._handle_toggle_upgrade, i) + cell.set_property("activatable", True) + elif i == 2: + self._combo_cell = Gtk.CellRendererCombo() + self._combo_cell.set_property("has-entry", False) + self._combo_cell.set_property("editable", True) + try: + self._combo_cell.connect("changed", + self._handle_change_version, 2) + except TypeError: + # PyGTK 2.14 - changed signal. + self._combo_cell.connect("edited", + self._handle_change_version, 2) + cell = self._combo_cell + else: + cell = Gtk.CellRendererText() + if i == len(columns) - 1: + column.pack_start(cell, True, True, 0) + else: + column.pack_start(cell, False, True, 0) + column.set_cell_data_func(cell, self._set_cell_data, i) + self.treeview.append_column(column) + self.treeview.connect("cursor-changed", self._handle_change_cursor) + self.treewindow.add(self.treeview) + self.window.vbox.pack_start( + self.treewindow, expand=True, fill=True, + padding=rose.config_editor.SPACING_PAGE) + label = Gtk.Label(label=rose.config_editor.DIALOG_LABEL_UPGRADE) + label.show() + self.window.vbox.pack_start( + label, True, True, rose.config_editor.SPACING_PAGE) + button_hbox = Gtk.HBox() + button_hbox.show() + all_versions_toggle_button = Gtk.CheckButton( + label=rose.config_editor.DIALOG_LABEL_UPGRADE_ALL, + use_underline=False) + all_versions_toggle_button.set_active(self.use_all_versions) + all_versions_toggle_button.connect("toggled", + self._handle_toggle_all_versions) + all_versions_toggle_button.show() + button_hbox.pack_start(all_versions_toggle_button, expand=False, + fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + self.window.vbox.pack_end(button_hbox, expand=False, fill=False) + self.ok_button = self.window.action_area.get_children()[0] + self.window.set_focus(all_versions_toggle_button) + self.window.set_focus(self.ok_button) + self._set_ok_to_upgrade() + max_size = rose.config_editor.SIZE_MACRO_DIALOG_MAX + my_size = self.window.size_request() + new_size = [-1, -1] + extra = 2 * rose.config_editor.SPACING_PAGE + for i in [0, 1]: + new_size[i] = min([my_size[i] + extra, max_size[i]]) + self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.window.set_default_size(*new_size) + response = self.window.run() + old_pwd = os.getcwd() + if response == Gtk.ResponseType.ACCEPT: + iter_ = self.treemodel.get_iter_first() + while iter_ is not None: + config_name = self.treemodel.get_value(iter_, 0) + curr_version = self.treemodel.get_value(iter_, 1) + next_version = self.treemodel.get_value(iter_, 2) + ok_to_upgrade = self.treemodel.get_value(iter_, 3) + config = self.config_dict[config_name] + manager = self.config_manager_dict[config_name] + directory = self.config_directory_dict[config_name] + if not ok_to_upgrade or next_version == curr_version: + iter_ = self.treemodel.iter_next(iter_) + continue + os.chdir(directory) + manager.set_new_tag(next_version) + macro_config = copy.deepcopy(config) + try: + new_config, change_list = manager.transform( + macro_config, custom_inspector=upgrade_inspector) + except Exception as exc: + rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_ERROR, + type(exc).__name__ + ": " + str(exc), + rose.config_editor.ERROR_UPGRADE.format( + config_name.lstrip("/")) + ) + iter_ = self.treemodel.iter_next(iter_) + continue + macro_id = (type(manager).__name__ + "." + + rose.macro.TRANSFORM_METHOD) + if handle_transform_func(config_name, macro_id, + new_config, change_list, + triggers_ok=True): + meta_config = rose.macro.load_meta_config( + new_config, config_type=rose.SUB_CONFIG_NAME, + ignore_meta_error=True + ) + trig_macro = rose.macros.trigger.TriggerMacro() + macro_config = copy.deepcopy(new_config) + macro_id = ( + rose.upgrade.MACRO_UPGRADE_TRIGGER_NAME + "." + + rose.macro.TRANSFORM_METHOD + ) + if not trig_macro.validate_dependencies(macro_config, + meta_config): + new_trig_config, trig_change_list = ( + rose.macros.trigger.TriggerMacro().transform( + macro_config, meta_config) + ) + handle_transform_func(config_name, macro_id, + new_trig_config, + trig_change_list, + triggers_ok=True) + iter_ = self.treemodel.iter_next(iter_) + os.chdir(old_pwd) + self.window.destroy() + + def _get_tooltip(self, view, row_iter, col_index, tip): + name = self.treeview.get_column(col_index).get_title() + value = str(self.treemodel.get_value(row_iter, col_index)) + tip.set_text(name + ": " + value) + return True + + def _handle_change_cursor(self, view): + path = self.treeview.get_cursor()[0] + iter_ = self.treemodel.get_iter(path) + config_name = self.treemodel.get_value(iter_, 0) + listmodel = self._config_version_model_dict[config_name] + self._combo_cell.set_property("model", listmodel) + self._combo_cell.set_property("text-column", 0) + + def _handle_change_version(self, cell, path, new, col_index): + iter_ = self.treemodel.get_iter(path) + if isinstance(new, str): + new_value = new + else: + new_value = cell.get_property("model").get_value(new, 0) + self.treemodel.set_value(iter_, col_index, new_value) + + def _handle_toggle_all_versions(self, button): + self.use_all_versions = button.get_active() + self.treemodel = Gtk.TreeStore(str, str, str, bool) + self._config_version_model_dict.clear() + for config_name in sorted(self.config_dict.keys()): + self._update_treemodel_data(config_name) + self.treeview.set_model(self.treemodel) + self._set_ok_to_upgrade() + + def _handle_toggle_upgrade(self, cell, path, col_index): + iter_ = self.treemodel.get_iter(path) + value = self.treemodel.get_value(iter_, col_index) + if (self.treemodel.get_value(iter_, 1) == + self.treemodel.get_value(iter_, 2)): + self.treemodel.set_value(iter_, col_index, False) + else: + self.treemodel.set_value(iter_, col_index, not value) + self._set_ok_to_upgrade() + + def _set_ok_to_upgrade(self, *args): + any_upgrades_toggled = False + iter_ = self.treemodel.get_iter_first() + while iter_ is not None: + if self.treemodel.get_value(iter_, 3): + any_upgrades_toggled = True + break + iter_ = self.treemodel.iter_next(iter_) + self.ok_button.set_sensitive(any_upgrades_toggled) + + def _set_cell_data(self, column, cell, model, r_iter, col_index): + if model.get_column_type(col_index) == GObject.TYPE_BOOLEAN: + cell.set_property("active", model.get_value(r_iter, col_index)) + if model.get_value(r_iter, 1) == model.get_value(r_iter, 2): + model.set_value(r_iter, col_index, False) + cell.set_property("inconsistent", True) + cell.set_property("sensitive", False) + else: + cell.set_property("inconsistent", False) + cell.set_property("sensitive", True) + elif col_index == 2: + cell.set_property("text", model.get_value(r_iter, 2)) + else: + text = model.get_value(r_iter, col_index) + if col_index == 0: + text = text.lstrip("/") + cell.set_property("text", text) + + def _update_treemodel_data(self, config_name): + manager = self.config_manager_dict[config_name] + current_tag = manager.tag + next_tag = manager.get_new_tag(only_named=not self.use_all_versions) + if next_tag is None: + self.treemodel.append( + None, [config_name, current_tag, current_tag, False]) + else: + self.treemodel.append( + None, [config_name, current_tag, next_tag, True]) + listmodel = Gtk.ListStore(str) + tags = manager.get_tags(only_named=not self.use_all_versions) + if not tags: + tags = [manager.tag] + for tag in tags: + listmodel.append([tag]) + self._config_version_model_dict[config_name] = listmodel diff --git a/metomi/rose/config_editor/util.py b/metomi/rose/config_editor/util.py new file mode 100644 index 000000000..ca0371a41 --- /dev/null +++ b/metomi/rose/config_editor/util.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +""" +This module contains a utility class to transform between data types. + +It also contains a function to launch an introspective dialog, and +one to import custom plugins. + +""" + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import rose +import rose.gtk.dialog +import rose.gtk.util + + +class Lookup(object): + + """Collection of data lookup functions used by multiple modules.""" + + def __init__(self): + self.section_option_id_lookup = {} + self.full_ns_split_lookup = {} + + def get_id_from_section_option(self, section, option): + """Return a variable id from a section and option.""" + if option is None: + id_ = section + else: + id_ = section + rose.CONFIG_DELIMITER + option + self.section_option_id_lookup[id_] = (section, option) + return id_ + + def get_section_option_from_id(self, var_id): + """Return a section and option from a variable id. + + This uses a dictionary to improve speed, as this method + is called repeatedly with no variation in results. + + """ + if var_id in self.section_option_id_lookup: + return self.section_option_id_lookup[var_id] + split_char = rose.CONFIG_DELIMITER + option_name = var_id.split(split_char)[-1] + section = var_id.replace(split_char + option_name, '', 1) + if option_name == section: + option_name = None + self.section_option_id_lookup[var_id] = (section, option_name) + return section, option_name + + def split_full_ns(self, data, full_namespace): + """Return the config name and the internal namespace from full ns.""" + if full_namespace not in self.full_ns_split_lookup: + for config_name in list(data.config.keys()): + if config_name == '/' + data.top_level_name: + continue + if full_namespace.startswith(config_name + '/'): + sub_space = full_namespace.replace(config_name + '/', + '', 1) + self.full_ns_split_lookup[full_namespace] = (config_name, + sub_space) + break + elif full_namespace == config_name: + sub_space = '' + self.full_ns_split_lookup[full_namespace] = (config_name, + sub_space) + break + else: + # A top level based namespace + config_name = "/" + data.top_level_name + sub_space = full_namespace.replace(config_name + '/', '', 1) + self.full_ns_split_lookup[full_namespace] = (config_name, + sub_space) + return self.full_ns_split_lookup.get(full_namespace, (None, None)) + + +class ImportWidgetError(Exception): + + """An exception raised when an imported widget cannot be used.""" + + def __str__(self): + return self.args[0] + + +def launch_node_info_dialog(node, changes, search_function): + """Launch a dialog displaying attributes of a variable or section.""" + title = node.__class__.__name__ + " " + node.metadata['id'] + text = '' + if changes: + text += (rose.config_editor.DIALOG_NODE_INFO_CHANGES.format(changes) + + "\n") + text += rose.config_editor.DIALOG_NODE_INFO_DATA + try: + att_list = list(vars(node).items()) + except TypeError: + # vars will fail when __slots__ are used. + att_list = node.getattrs() + att_list.sort() + att_list.sort(lambda x, y: (y[0] in ['name', 'value']) - + (x[0] in ['name', 'value'])) + metadata_start_index = len(att_list) + for key, value in sorted(node.metadata.items()): + att_list.append([key, value]) + delim = rose.config_editor.DIALOG_NODE_INFO_DELIMITER + name = rose.config_editor.DIALOG_NODE_INFO_ATTRIBUTE + maxlen = rose.config_editor.DIALOG_NODE_INFO_MAX_LEN + for i, (att_name, att_val) in enumerate(att_list): + if (att_name == 'metadata' or att_name.startswith("_") or + callable(att_val) or att_name == 'old_value'): + continue + if i == metadata_start_index: + text += "\n" + rose.config_editor.DIALOG_NODE_INFO_METADATA + prefix = name.format(att_name) + delim + indent0 = len(prefix) + text += prefix + lenval = maxlen - indent0 + text += _pretty_format_data(att_val, global_indent=indent0, + width=lenval) + text += "\n" + rose.gtk.dialog.run_hyperlink_dialog(Gtk.STOCK_DIALOG_INFO, text, title, + search_function) + + +def launch_error_dialog(exception=None, text=""): + """This will be replaced by rose.reporter utilities.""" + if text: + text += "\n" + if exception is not None: + text += type(exception).__name__ + ": " + str(exception) + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, rose.config_editor.DIALOG_TITLE_ERROR, + modal=False) + + +def text_for_character_widget(text): + """Strip an enclosing single quote pair from a piece of text.""" + if text.startswith("'") and text.endswith("'"): + text = text[1:-1] + text = text.replace("''", "'") + return text + + +def text_from_character_widget(text): + """Surround text with single quotes; escape existing ones.""" + return "'" + text.replace("'", "''") + "'" + + +def text_for_quoted_widget(text): + """Strip an enclosing double quote pair from a piece of text.""" + if text.startswith('"') and text.endswith('"'): + text = text[1:-1] + text = text.replace('\\"', '"') + return text + + +def text_from_quoted_widget(text): + """Surround text with double quotes; escape existing ones.""" + return '"' + text.replace('"', '\\"') + '"' + + +def wrap_string(text, maxlen=72, indent0=0, maxlines=4, sep=","): + """Return a wrapped string - 'textwrap' is not flexible enough for this.""" + lines = [""] + linelen = maxlen - indent0 + for item in text.split(sep): + dtext = rose.gtk.util.safe_str(item) + sep + if lines[-1] and len(lines[-1] + dtext) > linelen: + lines.append("") + linelen = maxlen + lines[-1] += dtext + lines[-1] = lines[-1][:-len(sep)] + if len(lines) > maxlines: + lines = lines[:4] + ["..."] + return "\n".join(lines) + + +def null_cmp(x_item, y_item): + """Compares sort_key and then id of the tuples x_item/y_item.""" + x_sort_key, x_id = x_item[0:2] + y_sort_key, y_id = y_item[0:2] + if x_id == '' or y_id == '': + return (x_id == '') - (y_id == '') + if x_sort_key == y_sort_key: + return rose.config.sort_settings(x_id, y_id) + return cmp(x_sort_key, y_sort_key) + + +def _pretty_format_data(data, global_indent=0, indent=4, width=60): + sub_name = rose.config_editor.DIALOG_NODE_INFO_SUB_ATTRIBUTE + safe_str = rose.gtk.util.safe_str + delim = rose.config_editor.DIALOG_NODE_INFO_DELIMITER + if isinstance(data, dict) and data: + text = "" + for key, val in list(data.items()): + text += "\n" + " " * global_indent + sub_prefix = sub_name.format(safe_str(key)) + delim + indent_next = global_indent + indent + str_val = _pretty_format_data(val, + global_indent=indent_next) + text += sub_prefix + str_val + return text + if isinstance(data, list) and data: + text = ",".join([_pretty_format_data(v) for v in data]) + return wrap_string(text, width, global_indent) + if data != {} and data != []: + return wrap_string(str(data), width, global_indent) + return "" diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py new file mode 100644 index 000000000..b6584d1ce --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re + +import rose +from . import array.entry +from . import array.mixed +from . import array.logical +from . import array.python_list +from . import array.spaced_list +from . import booltoggle +from . import character +from . import combobox +from . import files +from . import intspin +from . import meta +from . import radiobuttons +from . import text +from . import valuehints + + +NON_TEXT_TYPES = ('boolean', 'integer', 'logical', 'python_boolean', + 'python_list', 'real', 'spaced_list') + + +class ValueWidgetHook(object): + + """Provides hook functions for valuewidgets.""" + + def __init__(self, scroll_func=None, focus_func=None): + """Set up commonly used valuewidget hook functions.""" + self._scroll_func = scroll_func + self._focus_func = focus_func + + def trigger_scroll(self, widget, event): + """Set up top-level scrolling on a widget event.""" + if self._scroll_func is None: + return False + return self._scroll_func(widget, event) + + def get_focus(self, widget): + """Set up a trigger based on focusing for a widget.""" + if self._focus_func is None: + return widget.grab_focus() + return self._focus_func(widget) + + def copy(self): + """Return a copy of this instance.""" + return ValueWidgetHook(self.trigger_scroll, self.get_focus) + + +def chooser(value, metadata, error): + """Select an appropriate widget class based on the arguments. + + Note: rose edit overrides this logic if a widget is hard coded. + + """ + m_type = metadata.get(rose.META_PROP_TYPE) + m_values = metadata.get(rose.META_PROP_VALUES) + m_length = metadata.get(rose.META_PROP_LENGTH) + m_hint = metadata.get(rose.META_PROP_VALUE_HINTS) + contains_env = rose.env.contains_env_var(value) + is_list = m_length is not None or isinstance(m_type, list) + + # determine widget by presence of environment variables + if contains_env and (not m_type or m_type in NON_TEXT_TYPES or is_list): + # it is not safe to display the widget as intended due to an env var + if '\n' in value: + return text.TextMultilineValueWidget + else: + return text.RawValueWidget + + # determine widget by metadata length + if is_list: + if isinstance(m_type, list): + # irregular array + return array.mixed.MixedArrayValueWidget + elif m_type in ['logical', 'boolean', 'python_boolean']: + # regular array (boolean) + return array.logical.LogicalArrayValueWidget + else: + # regular array (generic) + return array.entry.EntryArrayValueWidget + + # determine widget by metadata values + if m_values is not None: + if len(m_values) <= 4: + # short list + return radiobuttons.RadioButtonsValueWidget + else: + # long list + return combobox.ComboBoxValueWidget + + # determine widget by metadata type + if m_type == 'integer': + return intspin.IntSpinButtonValueWidget + if m_type == 'meta': + return meta.MetaValueWidget + if m_type == 'str_multi': + return text.TextMultilineValueWidget + if m_type in ["character", "quoted"]: + return character.QuotedTextValueWidget + if m_type == "python_list" and not error: + return array.python_list.PythonListValueWidget + if m_type == "spaced_list" and not error: + return array.spaced_list.SpacedListValueWidget + if m_type in ['logical', 'boolean', 'python_boolean']: + return booltoggle.BoolToggleValueWidget + + # determine widget by metadata hint + if m_hint is not None: + return valuehints.HintsValueWidget + + # fall back to a text widget + if '\n' in value: + return text.TextMultilineValueWidget + else: + return text.RawValueWidget diff --git a/metomi/rose/config_editor/valuewidget/array/__init__.py b/metomi/rose/config_editor/valuewidget/array/__init__.py new file mode 100644 index 000000000..ea1a3150a --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py new file mode 100644 index 000000000..5d3e4ba20 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor.util +import rose.gtk.util +import rose.variable + + +class EntryArrayValueWidget(Gtk.HBox): + + """This is a class to represent multiple array entries.""" + + TIP_ADD = "Add array element" + TIP_DEL = "Remove array element" + TIP_ELEMENT = "Element {0}" + TIP_ELEMENT_CHAR = "Element {0}: '{1}'" + TIP_LEFT = "Move array element left" + TIP_RIGHT = "Move array element right" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(EntryArrayValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.max_length = self.metadata[rose.META_PROP_LENGTH] + + value_array = rose.variable.array_split(self.value) + self.chars_width = max([len(v) for v in value_array] + [1]) + 1 + self.last_selected_src = None + arr_type = self.metadata.get(rose.META_PROP_TYPE) + self.is_char_array = (arr_type == "character") + self.is_quoted_array = (arr_type == "quoted") + # Do not treat character or quoted arrays specially when incorrect. + if self.is_char_array: + checker = rose.macros.value.ValueChecker() + for val in value_array: + if not checker.check_character(val): + self.is_char_array = False + if self.is_quoted_array: + checker = rose.macros.value.ValueChecker() + for val in value_array: + if not checker.check_quoted(val): + self.is_quoted_array = False + if self.is_char_array: + for i, val in enumerate(value_array): + value_array[i] = ( + rose.config_editor.util.text_for_character_widget(val)) + if self.is_quoted_array: + for i, val in enumerate(value_array): + value_array[i] = ( + rose.config_editor.util.text_for_quoted_widget(val)) + # Designate the number of allowed columns - 10 for 4 chars width + self.num_allowed_columns = 3 + self.entry_table = Gtk.Table(rows=1, + columns=self.num_allowed_columns, + homogeneous=True) + self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table.show() + + self.entries = [] + + self.has_titles = False + if "element-titles" in metadata: + self.has_titles = True + + self.generate_entries(value_array) + self.generate_buttons() + self.populate_table() + self.pack_start(self.add_del_button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.entry_table.connect_after('size-allocate', + lambda w, e: self.reshape_table()) + self.connect('focus-in-event', + lambda w, e: self.hook.get_focus(self.get_focus_entry())) + + def get_focus_entry(self): + """Get either the last selected entry or the last one.""" + if self.last_selected_src is not None: + return self.last_selected_src + if len(self.entries) > 0: + return self.entries[-1] + return None + + def get_focus_index(self): + """Get the focus and position within the table of entries.""" + text = '' + for entry in self.entries: + val = entry.get_text() + if self.is_char_array: + val = rose.config_editor.util.text_from_character_widget(val) + elif self.is_quoted_array: + val = rose.config_editor.util.text_from_quoted_widget(val) + prefix = get_next_delimiter(self.value[len(text):], val) + if prefix is None: + return None + if entry == self.entry_table.focus_child: + return len(text + prefix) + entry.get_position() + text += prefix + val + return None + + def set_focus_index(self, focus_index=None): + """Set the focus and position within the table of entries.""" + if focus_index is None: + return + value_array = rose.variable.array_split(self.value) + text = '' + for i, val in enumerate(value_array): + prefix = get_next_delimiter(self.value[len(text):], + val) + if prefix is None: + return + if (len(text + prefix + val) >= focus_index or + i == len(value_array) - 1): + if len(self.entries) > i: + self.entries[i].grab_focus() + val_offset = focus_index - len(text + prefix) + if self.is_char_array or self.is_quoted_array: + val_offset = max([0, val_offset - 1]) + self.entries[i].set_position(val_offset) + return + text += prefix + val + + def generate_entries(self, value_array=None): + """Create the Gtk.Entry objects for elements in the array.""" + if value_array is None: + value_array = rose.variable.array_split(self.value) + entries = [] + for value_item in value_array: + for entry in self.entries: + if entry.get_text() == value_item and entry not in entries: + entries.append(entry) + break + else: + entries.append(self.get_entry(value_item)) + self.entries = entries + + def generate_buttons(self): + """Create the left-right movement arrows and add button.""" + left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow.show() + left_event_box = Gtk.EventBox() + left_event_box.add(left_arrow) + left_event_box.show() + left_event_box.connect('button-press-event', + lambda b, e: self.move_element(-1)) + left_event_box.connect('enter-notify-event', self._handle_arrow_enter) + left_event_box.connect('leave-notify-event', self._handle_arrow_leave) + left_event_box.set_tooltip_text(self.TIP_LEFT) + right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow.show() + right_event_box = Gtk.EventBox() + right_event_box.show() + right_event_box.add(right_arrow) + right_event_box.connect( + 'button-press-event', lambda b, e: self.move_element(1)) + right_event_box.connect('enter-notify-event', self._handle_arrow_enter) + right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.set_tooltip_text(self.TIP_RIGHT) + self.arrow_box = Gtk.HBox() + self.arrow_box.show() + self.arrow_box.pack_start(left_event_box, expand=False, fill=False) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.set_arrow_sensitive(False, False) + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_DEL) + self.del_button.add(del_image) + self.del_button.show() + self.del_button.connect('button-release-event', + lambda b, e: self.remove_entry()) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.button_box = Gtk.HBox() + self.button_box.show() + self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.show() + self.add_button.connect('button-release-event', + lambda b, e: self.add_entry()) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_del_button_box = Gtk.VBox() + self.add_del_button_box.pack_start( + self.add_button, expand=False, fill=False) + self.add_del_button_box.pack_start( + self.del_button, expand=False, fill=False) + self.add_del_button_box.show() + + def _handle_arrow_enter(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.ACTIVE) + + def _handle_arrow_leave(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.NORMAL) + + def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): + """Control the sensitivity of the movement buttons.""" + sens_tuple = (is_left_sensitive, is_right_sensitive) + for i, event_box in enumerate(self.arrow_box.get_children()): + event_box.get_child().set_sensitive(sens_tuple[i]) + if not sens_tuple[i]: + event_box.set_state(Gtk.StateType.NORMAL) + + def move_element(self, num_places_right): + """Move the entry left or right.""" + entry = self.last_selected_src + if entry is None: + return + old_index = self.entries.index(entry) + if (old_index + num_places_right < 0 or + old_index + num_places_right > len(self.entries) - 1): + return + self.entries.remove(entry) + self.entries.insert(old_index + num_places_right, entry) + self.populate_table() + self.setter(entry) + + def get_entry(self, value_item): + """Create a gtk Entry for this array element.""" + entry = Gtk.Entry() + entry.set_text(value_item) + entry.connect('focus-in-event', + self._handle_focus_on_entry) + entry.connect("button-release-event", + self._handle_middle_click_paste) + entry.connect_after("paste-clipboard", self.setter) + entry.connect_after("key-release-event", + lambda e, v: self.setter(e)) + entry.connect_after("button-release-event", + lambda e, v: self.setter(e)) + entry.connect('focus-out-event', + self._handle_focus_off_entry) + entry.set_width_chars(self.chars_width - 1) + entry.show() + return entry + + def populate_table(self, focus_widget=None): + """Populate a table with the array elements, dynamically.""" + position = None + table_widgets = self.entries + [self.button_box] + table_children = self.entry_table.get_children() + if focus_widget is None: + for child in table_children: + if child.is_focus() and isinstance(child, Gtk.Entry): + focus_widget = child + position = focus_widget.get_position() + else: + position = focus_widget.get_position() + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + if (focus_widget is None and self.entry_table.is_focus() and + len(self.entries) > 0): + focus_widget = self.entries[-1] + position = len(focus_widget.get_text()) + num_fields = len(self.entries + [self.button_box]) + num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns + self.entry_table.resize(num_rows_now, self.num_allowed_columns) + if (self.max_length.isdigit() and + len(self.entries) >= int(self.max_length)): + self.add_button.hide() + else: + self.add_button.show() + if (self.max_length.isdigit() and + len(self.entries) <= int(self.max_length)): + self.del_button.hide() + elif len(self.entries) == 0: + self.del_button.hide() + else: + self.del_button.show() + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + index = self.entries.index(self.last_selected_src) + if index == 0: + self.set_arrow_sensitive(False, True) + elif index == len(self.entries) - 1: + self.set_arrow_sensitive(True, False) + if len(self.entries) < 2: + self.set_arrow_sensitive(False, False) + + if self.has_titles: + for col, label in enumerate(self.metadata['element-titles']): + if col >= len(table_widgets) - 1: + break + widget = Gtk.HBox() + label = Gtk.Label(label=self.metadata['element-titles'][col]) + label.show() + widget.pack_start(label, expand=True, fill=True) + widget.show() + self.entry_table.attach(widget, + col, col + 1, + 0, 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + + for i, widget in enumerate(table_widgets): + if isinstance(widget, Gtk.Entry): + if self.is_char_array or self.is_quoted_array: + w_value = widget.get_text() + widget.set_tooltip_text(self.TIP_ELEMENT_CHAR.format( + (i + 1), w_value)) + else: + widget.set_tooltip_text(self.TIP_ELEMENT.format((i + 1))) + row = i // self.num_allowed_columns + if self.has_titles: + row += 1 + column = i % self.num_allowed_columns + self.entry_table.attach(widget, + column, column + 1, + row, row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + if focus_widget is not None: + focus_widget.grab_focus() + focus_widget.set_position(position) + focus_widget.select_region(position, position) + self.grab_focus = lambda: self.hook.get_focus( + self._get_widget_for_focus()) + self.check_resize() + + def reshape_table(self): + """Reshape a table according to the space allocated.""" + total_x_bound = self.entry_table.get_allocation().width + if not len(self.entries): + return False + entries_bound = sum([e.get_allocation().width for e in self.entries]) + each_entry_bound = entries_bound / len(self.entries) + maximum_entry_number = float(total_x_bound) / float(each_entry_bound) + rounded_max = int(maximum_entry_number) + 1 + if rounded_max != self.num_allowed_columns + 2 and rounded_max > 2: + self.num_allowed_columns = max(1, rounded_max - 2) + self.populate_table() + + def add_entry(self): + """Add a new entry (with null text) to the variable array.""" + entry = self.get_entry('') + self.entries.append(entry) + self._adjust_entry_length() + self.populate_table(focus_widget=entry) + if (self.metadata.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE): + self.setter(entry) + + def remove_entry(self): + """Remove the last selected or the last entry.""" + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + entry = self.entries.pop( + self.entries.index(self.last_selected_src)) + self.last_selected_src = None + else: + entry = self.entries.pop() + self.populate_table() + self.setter(entry) + + def setter(self, widget): + """Reconstruct the new variable value from the entry array.""" + val_array = [e.get_text() for e in self.entries] + max_length = max([len(v) for v in val_array] + [1]) + if max_length + 1 != self.chars_width: + self.chars_width = max_length + 1 + self._adjust_entry_length() + if widget is not None and not widget.is_focus(): + widget.grab_focus() + widget.set_position(len(widget.get_text())) + widget.select_region(widget.get_position(), + widget.get_position()) + if self.is_char_array: + for i, val in enumerate(val_array): + val_array[i] = ( + rose.config_editor.util.text_from_character_widget(val)) + elif self.is_quoted_array: + for i, val in enumerate(val_array): + val_array[i] = ( + rose.config_editor.util.text_from_quoted_widget(val)) + entries_have_commas = any("," in v for v in val_array) + new_value = rose.variable.array_join(val_array) + if new_value != self.value: + self.value = new_value + self.set_value(new_value) + if (entries_have_commas and + not (self.is_char_array or self.is_quoted_array)): + new_val_array = rose.variable.array_split(new_value) + if len(new_val_array) != len(self.entries): + self.generate_entries() + focus_index = None + for i, val in enumerate(val_array): + if "," in val: + val_post_comma = val[:val.index(",") + 1] + focus_index = len(rose.variable.array_join( + new_val_array[:i] + [val_post_comma])) + self.populate_table() + self.set_focus_index(focus_index) + return False + + def _adjust_entry_length(self): + for entry in self.entries: + entry.set_width_chars(self.chars_width) + entry.set_max_length(self.chars_width) + self.reshape_table() + + def _get_widget_for_focus(self): + if self.entries: + return self.entries[-1] + return self.entry_table + + def _handle_focus_off_entry(self, widget, event): + if widget == self.last_selected_src: + try: + widget.set_progress_fraction(1.0) + except AttributeError: + widget.drag_highlight() + if widget.get_position() is None: + widget.set_position(len(widget.get_text())) + + def _handle_focus_on_entry(self, widget, event): + if self.last_selected_src is not None: + try: + self.last_selected_src.set_progress_fraction(0.0) + except AttributeError: + self.last_selected_src.drag_unhighlight() + self.last_selected_src = widget + is_start = (widget in self.entries and self.entries[0] == widget) + is_end = (widget in self.entries and self.entries[-1] == widget) + self.set_arrow_sensitive(not is_start, not is_end) + if widget.get_text() != '': + widget.select_region(widget.get_position(), + widget.get_position()) + return False + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self.setter(widget) + return False + + +def get_next_delimiter(array_text, next_element): + """Return the part of array_text immediately preceding next_element.""" + try: + val = array_text.index(next_element) + except ValueError: + # Substring not found. + return + if val == 0 and len(array_text) > 1: # Null or whitespace element. + while array_text[val].isspace(): + val += 1 + if array_text[val] == ",": + val += 1 + return array_text[:val] diff --git a/metomi/rose/config_editor/valuewidget/array/logical.py b/metomi/rose/config_editor/valuewidget/array/logical.py new file mode 100644 index 000000000..46d3a38ac --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/logical.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.gtk.util +import rose.variable + + +class LogicalArrayValueWidget(Gtk.HBox): + + """This is a class to represent an array of logical or boolean types.""" + + TIP_ADD = 'Add array element' + TIP_DEL = 'Delete array element' + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(LogicalArrayValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.max_length = metadata[rose.META_PROP_LENGTH] + value_array = rose.variable.array_split(value) + if metadata.get(rose.META_PROP_TYPE) == "boolean": + # boolean -> true/false + self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_FALSE, + rose.TYPE_BOOLEAN_VALUE_TRUE] + self.label_dict = dict(list(zip(self.allowed_values, + self.allowed_values))) + elif metadata.get(rose.META_PROP_TYPE) == "python_boolean": + # python_boolean -> True/False + self.allowed_values = [rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] + self.label_dict = dict(list(zip(self.allowed_values, + self.allowed_values))) + else: + # logical -> .true./.false. + self.allowed_values = [rose.TYPE_LOGICAL_VALUE_FALSE, + rose.TYPE_LOGICAL_VALUE_TRUE] + self.label_dict = { + rose.TYPE_LOGICAL_VALUE_FALSE: + rose.TYPE_LOGICAL_FALSE_TITLE, + rose.TYPE_LOGICAL_VALUE_TRUE: + rose.TYPE_LOGICAL_TRUE_TITLE} + + imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU)] + self.make_log_image = lambda i: Gtk.Image.new_from_stock(*imgs[i]) + self.chars_width = max([len(v) for v in value_array] + [1]) + 1 + self.num_allowed_columns = 3 + self.entry_table = Gtk.Table(rows=1, + columns=self.num_allowed_columns, + homogeneous=True) + self.entry_table.connect('focus-in-event', + self.hook.trigger_scroll) + self.entry_table.show() + + self.entries = [] + for value_item in value_array: + entry = self.get_entry(value_item) + self.entries.append(entry) + + self.has_titles = False + if "element-titles" in metadata: + self.has_titles = True + + self.generate_buttons() + self.populate_table() + self.pack_start(self.button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.entry_table.connect_after('size-allocate', + lambda w, e: self.reshape_table()) + self.connect('focus-in-event', + lambda w, e: self.hook.get_focus(self.get_focus_entry())) + + def get_focus_entry(self): + """Get either the last selected button or the last one.""" + return self.entries[-1] + + def generate_buttons(self): + """Create the add button.""" + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.connect('button-release-event', + lambda b, e: self.add_entry()) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_ADD) + self.del_button.add(del_image) + self.del_button.show() + self.del_button.connect('button-release-event', + self.remove_entry) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.button_box = Gtk.VBox() + self.button_box.show() + self.button_box.pack_start(self.add_button, expand=False, fill=False) + self.button_box.pack_start(self.del_button, expand=False, fill=False) + + def get_entry(self, value_item): + """Create a widget for this array element.""" + bad_img = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, + Gtk.IconSize.MENU) + button = Gtk.ToggleButton() + button.options = [rose.TYPE_LOGICAL_VALUE_FALSE, + rose.TYPE_LOGICAL_VALUE_TRUE] + button.labels = [rose.TYPE_LOGICAL_FALSE_TITLE, + rose.TYPE_LOGICAL_TRUE_TITLE] + button.set_tooltip_text(value_item) + if value_item in self.allowed_values: + index = self.allowed_values.index(value_item) + button.set_active(index) + button.set_image(self.make_log_image(index)) + button.set_label(button.labels[index]) + else: + button.set_inconsistent(True) + button.set_image(bad_img) + button.connect('toggled', self._switch_state_and_set) + button.show() + return button + + def _switch_state_and_set(self, widget): + state = self.allowed_values[widget.get_active()] + title = self.label_dict[state] + image = self.make_log_image(widget.get_active()) + widget.set_tooltip_text(state) + widget.set_label(title) + widget.set_image(image) + self.setter(widget) + + def populate_table(self): + """Populate a table with the array elements, dynamically.""" + focus = None + table_widgets = self.entries + for child in self.entry_table.get_children(): + if child.is_focus(): + focus = child + if len(self.entry_table.get_children()) < len(table_widgets): + # Newly added widget, set focus to the end + focus = self.entries[-1] + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + if (focus is None and self.entry_table.is_focus() and + len(self.entries) > 0): + focus = self.entries[-1] + num_fields = len(self.entries) + num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns + self.entry_table.resize(num_rows_now, self.num_allowed_columns) + if (self.max_length.isdigit() and + len(self.entries) >= int(self.max_length)): + self.add_button.hide() + else: + self.add_button.show() + if (self.max_length.isdigit() and + len(self.entries) <= int(self.max_length)): + self.del_button.hide() + else: + self.del_button.show() + if self.has_titles: + for col, label in enumerate(self.metadata['element-titles']): + if col >= len(table_widgets): + break + widget = Gtk.HBox() + label = Gtk.Label(label=self.metadata['element-titles'][col]) + label.show() + widget.pack_start(label, expand=True, fill=True) + widget.show() + self.entry_table.attach(widget, + col, col + 1, + 0, 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + + for i, widget in enumerate(table_widgets): + row = i // self.num_allowed_columns + if self.has_titles: + row += 1 + column = i % self.num_allowed_columns + self.entry_table.attach(widget, + column, column + 1, + row, row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + self.grab_focus = lambda: self.hook.get_focus(self.entries[-1]) + self.check_resize() + + def reshape_table(self): + """Reshape a table according to the space allocated.""" + total_x_bound = self.entry_table.get_allocation().width + if not len(self.entries): + return False + entries_bound = sum([e.get_allocation().width for e in self.entries]) + each_entry_bound = entries_bound / len(self.entries) + maximum_entry_number = float(total_x_bound) / float(each_entry_bound) + rounded_max = int(maximum_entry_number) + 1 + if rounded_max != self.num_allowed_columns + 2 and rounded_max > 2: + self.num_allowed_columns = max(1, rounded_max - 2) + self.populate_table() + + def add_entry(self): + """Add a new button to the array.""" + entry = self.get_entry(self.allowed_values[0]) + self.entries.append(entry) + self.populate_table() + self.setter() + + def remove_entry(self, *args): + """Remove a button.""" + if len(self.entries) > 1: + self.entries.pop() + self.populate_table() + self.setter() + + def setter(self, *args): + """Update the value.""" + val_array = [] + for widget in self.entries: + value = widget.get_tooltip_text() + if value is None: + value = '' + val_array.append(value) + new_val = rose.variable.array_join(val_array) + self.value = new_val + self.set_value(new_val) + self.value_array = rose.variable.array_split(self.value) + return False diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py new file mode 100644 index 000000000..ce93af854 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re +import sys + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +from . import entry +import rose.gtk.util +import rose.variable + + +class MixedArrayValueWidget(Gtk.HBox): + + """This is a class to represent a derived type variable as a table. + + The type (variable.metadata['type']) should be a list, e.g. + ['integer', 'real']. There can optionally be a length + (variable.metadata['length'] for derived type arrays. + + This will create a table containing different types (horizontally) + and different array elements (vertically). + + """ + + BAD_COLOUR = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match + TIP_ADD = 'Add array element' + TIP_DELETE = 'Remove last array element' + TIP_INVALID_ENTRY = "Invalid entry - not {0}" + MIN_WIDTH_CHARS = 7 + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(MixedArrayValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.value_array = rose.variable.array_split(value) + self.extra_array = [] # For new rows + self.element_values = [] + self.rows = [] + self.widgets = [] + self.unlimited = (metadata.get(rose.META_PROP_LENGTH) == ':') + if self.unlimited: + self.array_length = 1 + else: + self.array_length = metadata.get(rose.META_PROP_LENGTH, 1) + self.num_cols = len(metadata[rose.META_PROP_TYPE]) + self.types_row = [t for t in + metadata[rose.META_PROP_TYPE]] + log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] + self.make_log_image = lambda i: Gtk.Image.new_from_stock(*log_imgs[i]) + + self.has_titles = False + if "element-titles" in metadata: + self.has_titles = True + + self.set_num_rows() + self.entry_table = Gtk.Table(rows=self.num_rows, + columns=self.num_cols, + homogeneous=False) + self.entry_table.connect('focus-in-event', + self.hook.trigger_scroll) + self.entry_table.show() + for i in range(self.num_rows): + self.insert_row(i) + self.normalise_width_widgets() + self.generate_buttons() + self.pack_start(self.add_del_button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.show() + + def set_num_rows(self): + """Derive the number of columns and rows.""" + if self.CHECK_NAME_IS_ELEMENT(self.metadata['id']): + self.unlimited = False + if self.unlimited: + self.num_rows, rem = divmod(len(self.value_array), self.num_cols) + self.num_rows += [1, 0][rem == 0] + self.max_rows = sys.maxsize + else: + self.num_rows = int(self.array_length) + rem = divmod(len(self.value_array), self.num_cols)[1] + if self.num_rows == 0: + self.num_rows = 1 + self.max_rows = self.num_rows + if rem != 0: + # Then there is an incorrect number of entries. + # Display as entry box. + self.num_rows = 1 + self.max_rows = 1 + self.unlimited = False + self.types_row = ['_error_'] + self.value_array = [self.value] + self.has_titles = False + if self.num_rows == 0: + self.num_rows = 1 + if self.max_rows == 0: + self.max_rows = 1 + if self.has_titles: + self.num_rows += 1 + + def grab_focus(self): + if self.entry_table.focus_child is None: + self.hook.get_focus(self.rows[-1][-1]) + else: + self.hook.get_focus(self.entry_table.focus_child) + + def add_row(self, *args): + """Create a new row of widgets.""" + nrows = self.entry_table.child_get_property( + self.rows[-1][-1], 'top-attach') + self.entry_table.resize(nrows + 2, self.num_cols) + new_values = self.insert_row(nrows + 1) + if any(new_values): + self.value_array = self.value_array + new_values + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + self.set_num_rows() + self.normalise_width_widgets() + self._decide_show_buttons() + return False + + def get_focus_index(self): + text = '' + if not self.value_array: + return 0 + for i_row, widget_list in enumerate(self.rows): + for i, widget in enumerate(widget_list): + try: + val = self.value_array[i_row * self.num_cols + i] + except IndexError: + return None + prefix_text = entry.get_next_delimiter(self.value[len(text):], + val) + if prefix_text is None: + return + if widget == self.entry_table.focus_child: + if hasattr(widget, "get_focus_index"): + position = widget.get_focus_index() + return len(text + prefix_text) + position + else: + for child in widget.get_children(): + if not hasattr(child, "get_position"): + continue + position = child.get_position() + if self.types_row[i] in ["character", "quoted"]: + position += 1 + return len(text + prefix_text) + position + return len(text + prefix_text) + len(val) + text += prefix_text + val + return None + + def set_focus_index(self, focus_index=None): + """Set the focus and position within the table.""" + if focus_index is None: + return + value_array = rose.variable.array_split(self.value) + text = '' + widgets = [] + for widget_list in self.rows: + widgets.extend(widget_list) + types = self.types_row * len(self.rows) + if len(types) == 1: # Special invalid length widget + widgets[0].grab_focus() + if hasattr(widgets[0], "set_focus_index"): + widgets[0].set_focus_index(focus_index) + return + for i, val in enumerate(value_array): + prefix = entry.get_next_delimiter(self.value[len(text):], + val) + if prefix is None: + return + if len(text + prefix + val) >= focus_index: + if len(widgets) > i: + widgets[i].grab_focus() + val_offset = focus_index - len(text + prefix) + if hasattr(widgets[i], "set_focus_index"): + widgets[i].set_focus_index(val_offset) + return + text += prefix + val + + def del_row(self, *args): + """Delete the last row of widgets.""" + nrows = self.entry_table.child_get_property( + self.rows[-1][-1], 'top-attach') + for _ in enumerate(self.types_row): + ent = self.rows[-1][-1] + self.rows[-1].pop(-1) + self.entry_table.remove(ent) + self.rows.pop(-1) + self.entry_table.resize(nrows, self.num_cols) + chop_index = len(self.value_array) - self.num_cols + self.value_array = self.value_array[:chop_index] + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + self.set_num_rows() + self._decide_show_buttons() + return False + + def _decide_show_buttons(self): + """Show or hide the add row and delete row buttons.""" + if len(self.rows) >= self.max_rows and not self.unlimited: + self.add_button.hide() + self.del_button.show() + else: + self.add_button.show() + self.del_button.show() + if len(self.rows) == 1: + self.del_button.hide() + elif len(self.rows) == 2 and self.has_titles: + self.del_button.hide() + else: + self.add_button.show() + + def insert_row(self, row_index): + """Create a row of widgets from type_list.""" + widget_list = [] + new_values = [] + insert_row_index = row_index + for i, el_piece_type in enumerate(self.types_row): + if self.has_titles: + raw_index = row_index - 1 + else: + raw_index = row_index + unwrapped_index = raw_index * self.num_cols + i + value_index = unwrapped_index + while value_index > len(self.value_array) - 1: + value_index -= len(self.types_row) + if value_index < 0: + w_value = rose.variable.get_value_from_metadata( + {rose.META_PROP_TYPE: el_piece_type}) + else: + w_value = self.value_array[value_index] + new_values.append(w_value) + hover_text = '' + w_error = {} + if el_piece_type in ['integer', 'real']: + try: + [int, float][el_piece_type == 'real'](w_value) + except (TypeError, ValueError): + if w_value != '': + hover_text = self.TIP_INVALID_ENTRY.format( + el_piece_type) + w_error = {rose.META_PROP_TYPE: hover_text} + w_meta = {rose.META_PROP_TYPE: el_piece_type} + widget_cls = rose.config_editor.valuewidget.chooser( + w_value, w_meta, w_error) + hook = self.hook + setter = ArrayElementSetter(self.setter, unwrapped_index) + if self.has_titles and row_index == 0: + widget = Gtk.HBox() + label = Gtk.Label(label=self.metadata['element-titles'][i]) + label.show() + widget.pack_start(label, expand=True, fill=True) + else: + widget = widget_cls(w_value, w_meta, setter.set_value, hook) + if hover_text: + widget.set_tooltip_text(hover_text) + widget.show() + self.entry_table.attach(widget, + i, i + 1, + insert_row_index, insert_row_index + 1, + xoptions=Gtk.AttachOptions.SHRINK, + yoptions=Gtk.AttachOptions.SHRINK) + widget_list.append(widget) + self.rows.append(widget_list) + self.widgets.extend(widget_list) + return new_values + + def normalise_width_widgets(self): + if not self.rows: + return + for widget in self.rows[0]: + self._normalise_width_chars(widget) + + def _normalise_width_chars(self, widget): + index = self.widgets.index(widget) + element = index % self.num_cols + max_width = {} + # Get max width + for widgets in self.rows: + e_widget = widgets[element] + i = 0 + child_list = e_widget.get_children() + while child_list: + child = child_list.pop() + if (isinstance(child, Gtk.Label) or + isinstance(child, Gtk.Entry) and + hasattr(child, 'get_text')): + width = len(child.get_text()) + if width > max_width.get(i, -1): + max_width.update({i: width}) + if hasattr(child, 'get_children'): + child_list.extend(child.get_children()) + elif hasattr(child, 'get_child'): + child_list.append(child.get_child()) + i += 1 + for key, value in list(max_width.items()): + if value < self.MIN_WIDTH_CHARS: + max_width[key] = self.MIN_WIDTH_CHARS + # Set max width + for widgets in self.rows: + e_widget = widgets[element] + i = 0 + child_list = e_widget.get_children() + while child_list: + child = child_list.pop() + if (isinstance(child, Gtk.Entry) and + hasattr(child, 'set_width_chars')): + child.set_width_chars(max_width[i]) + if hasattr(child, 'get_children'): + child_list.extend(child.get_children()) + elif hasattr(child, 'get_child'): + child_list.append(child.get_child()) + i += 1 + + def generate_buttons(self): + """Insert an add row and delete row button.""" + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_ADD) + self.del_button.add(del_image) + self.del_button.show() + self.del_button.connect('button-release-event', self.del_row) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.show() + self.add_button.connect('button-release-event', self.add_row) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_del_button_box = Gtk.VBox() + self.add_del_button_box.pack_start( + self.add_button, expand=False, fill=False) + self.add_del_button_box.pack_start( + self.del_button, expand=False, fill=False) + self.add_del_button_box.show() + self._decide_show_buttons() + + def setter(self, array_index, element_value): + """Update the value.""" + widget_row = self.rows[array_index / self.num_cols] + widget = widget_row[array_index % self.num_cols] + self._normalise_width_chars(widget) + i = array_index - len(self.value_array) + if i >= 0: + while len(self.extra_array) <= i: + self.extra_array.append("") + self.extra_array[i] = element_value + ok_index = 0 + j = self.num_cols + while j <= len(self.extra_array): + if (len(self.extra_array[:j]) % self.num_cols == 0 and + all(self.extra_array[:j])): + ok_index = j + else: + break + j += self.num_cols + self.value_array.extend(self.extra_array[:ok_index]) + self.extra_array = self.extra_array[ok_index:] + else: + self.value_array[array_index] = element_value + new_val = rose.variable.array_join(self.value_array) + if new_val != self.value: + self.value = new_val + self.set_value(new_val) + + +class ArrayElementSetter(object): + + """Element widget setter class.""" + + def __init__(self, setter_function, index): + self.setter_function = setter_function + self.index = index + + def set_value(self, value): + self.setter_function(self.index, value) diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py new file mode 100644 index 000000000..870b3b203 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -0,0 +1,454 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import ast + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +from . import entry +import rose.config_editor.util +import rose.gtk.util +import rose.variable + + +class PythonListValueWidget(Gtk.HBox): + + """This is a class to represent a Python-compatible list format.""" + + TIP_ADD = "Add array element" + TIP_DEL = "Remove array element" + TIP_ELEMENT = "Element {0}" + TIP_ELEMENT_CHAR = "Element {0}: '{1}'" + TIP_LEFT = "Move array element left" + TIP_RIGHT = "Move array element right" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(PythonListValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.max_length = ":" + value_array = python_array_split(self.value) + self.chars_width = max([len(v) for v in value_array] + [1]) + 1 + self.last_selected_src = None + # Designate the number of allowed columns - 10 for 4 chars width + self.num_allowed_columns = 3 + self.entry_table = Gtk.Table(rows=1, + columns=self.num_allowed_columns, + homogeneous=True) + self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table.show() + + self.entries = [] + self.generate_entries(value_array) + + self.has_titles = False + if "element-titles" in metadata: + self.has_titles = True + + self.generate_buttons() + self.populate_table() + self.pack_start(self.add_del_button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.entry_table.connect_after('size-allocate', + lambda w, e: self.reshape_table()) + self.connect('focus-in-event', + lambda w, e: self.hook.get_focus(self.get_focus_entry())) + + def get_focus_entry(self): + """Get either the last selected entry or the last one.""" + if self.last_selected_src is not None: + return self.last_selected_src + if len(self.entries) > 0: + return self.entries[-1] + return None + + def get_focus_index(self): + """Get the focus and position within the table of entries.""" + if not self.value.startswith("["): + return + text = '[' + for my_entry in self.entries: + val = my_entry.get_text() + prefix = entry.get_next_delimiter(self.value[len(text):], val) + if prefix is None: + return + if my_entry == self.entry_table.focus_child: + return len(text + prefix) + my_entry.get_position() + text += prefix + val + return None + + def set_focus_index(self, focus_index=None): + """Set the focus and position within the table of entries.""" + if focus_index is None: + return + value_array = python_array_split(self.value) + if not self.value.startswith("["): + return + text = '[' + for i, val in enumerate(value_array): + prefix = entry.get_next_delimiter(self.value[len(text):], + val) + if prefix is None: + return + if (len(text + prefix + val) >= focus_index or + i == len(value_array) - 1): + if len(self.entries) > i: + self.entries[i].grab_focus() + val_offset = focus_index - len(text + prefix) + self.entries[i].set_position(val_offset) + return + text += prefix + val + + def generate_entries(self, value_array=None): + """Create the Gtk.Entry objects for elements in the array.""" + if value_array is None: + value_array = python_array_split(self.value) + entries = [] + for value_item in value_array: + for widget in self.entries: + if widget.get_text() == value_item and widget not in entries: + entries.append(widget) + break + else: + entries.append(self.get_entry(value_item)) + self.entries = entries + + def generate_buttons(self): + """Create the left-right movement arrows and add button.""" + left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow.show() + left_event_box = Gtk.EventBox() + left_event_box.add(left_arrow) + left_event_box.show() + left_event_box.connect('button-press-event', + lambda b, e: self.move_element(-1)) + left_event_box.connect('enter-notify-event', self._handle_arrow_enter) + left_event_box.connect('leave-notify-event', self._handle_arrow_leave) + left_event_box.set_tooltip_text(self.TIP_LEFT) + right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow.show() + right_event_box = Gtk.EventBox() + right_event_box.show() + right_event_box.add(right_arrow) + right_event_box.connect( + 'button-press-event', lambda b, e: self.move_element(1)) + right_event_box.connect('enter-notify-event', self._handle_arrow_enter) + right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.set_tooltip_text(self.TIP_RIGHT) + self.arrow_box = Gtk.HBox() + self.arrow_box.show() + self.arrow_box.pack_start(left_event_box, expand=False, fill=False) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.set_arrow_sensitive(False, False) + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_DEL) + self.del_button.add(del_image) + self.del_button.show() + self.del_button.connect('button-release-event', + lambda b, e: self.remove_entry()) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.button_box = Gtk.HBox() + self.button_box.show() + self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.show() + self.add_button.connect('button-release-event', + lambda b, e: self.add_entry()) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_del_button_box = Gtk.VBox() + self.add_del_button_box.pack_start( + self.add_button, expand=False, fill=False) + self.add_del_button_box.pack_start( + self.del_button, expand=False, fill=False) + self.add_del_button_box.show() + + def _handle_arrow_enter(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.ACTIVE) + + def _handle_arrow_leave(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.NORMAL) + + def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): + """Control the sensitivity of the movement buttons.""" + sens_tuple = (is_left_sensitive, is_right_sensitive) + for i, event_box in enumerate(self.arrow_box.get_children()): + event_box.get_child().set_sensitive(sens_tuple[i]) + if not sens_tuple[i]: + event_box.set_state(Gtk.StateType.NORMAL) + + def move_element(self, num_places_right): + """Move the entry left or right.""" + widget = self.last_selected_src + if widget is None: + return + old_index = self.entries.index(widget) + if (old_index + num_places_right < 0 or + old_index + num_places_right > len(self.entries) - 1): + return + self.entries.remove(widget) + self.entries.insert(old_index + num_places_right, widget) + self.populate_table() + self.setter(widget) + + def get_entry(self, value_item): + """Create a gtk Entry for this array element.""" + widget = Gtk.Entry() + widget.set_text(value_item) + widget.connect('focus-in-event', self._handle_focus_on_entry) + widget.connect("button-release-event", self._handle_middle_click_paste) + widget.connect_after("paste-clipboard", self.setter) + widget.connect_after("key-release-event", lambda e, v: self.setter(e)) + widget.connect_after( + "button-release-event", lambda e, v: self.setter(e)) + widget.connect('focus-out-event', self._handle_focus_off_entry) + widget.set_width_chars(self.chars_width - 1) + widget.show() + return widget + + def populate_table(self, focus_widget=None): + """Populate a table with the array elements, dynamically.""" + position = None + table_widgets = self.entries + [self.button_box] + table_children = self.entry_table.get_children() + if focus_widget is None: + for child in table_children: + if child.is_focus() and isinstance(child, Gtk.Entry): + focus_widget = child + position = focus_widget.get_position() + else: + position = focus_widget.get_position() + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + if (focus_widget is None and self.entry_table.is_focus() and + len(self.entries) > 0): + focus_widget = self.entries[-1] + position = len(focus_widget.get_text()) + num_fields = len(self.entries + [self.button_box]) + num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns + self.entry_table.resize(num_rows_now, self.num_allowed_columns) + if (self.max_length.isdigit() and + len(self.entries) >= int(self.max_length)): + self.add_button.hide() + else: + self.add_button.show() + if (self.max_length.isdigit() and + len(self.entries) <= int(self.max_length)): + self.del_button.hide() + elif len(self.entries) == 0: + self.del_button.hide() + else: + self.del_button.show() + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + index = self.entries.index(self.last_selected_src) + if index == 0: + self.set_arrow_sensitive(False, True) + elif index == len(self.entries) - 1: + self.set_arrow_sensitive(True, False) + if len(self.entries) < 2: + self.set_arrow_sensitive(False, False) + + if self.has_titles: + for col, label in enumerate(self.metadata['element-titles']): + if col >= len(table_widgets) - 1: + break + widget = Gtk.HBox() + label = Gtk.Label(label=self.metadata['element-titles'][col]) + label.show() + widget.pack_start(label, expand=True, fill=True) + widget.show() + self.entry_table.attach(widget, + col, col + 1, + 0, 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + + for i, widget in enumerate(table_widgets): + if isinstance(widget, Gtk.Entry): + widget.set_tooltip_text(self.TIP_ELEMENT.format((i + 1))) + row = i // self.num_allowed_columns + if self.has_titles: + row += 1 + column = i % self.num_allowed_columns + self.entry_table.attach(widget, + column, column + 1, + row, row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + if focus_widget is not None: + focus_widget.grab_focus() + focus_widget.set_position(position) + focus_widget.select_region(position, position) + self.grab_focus = lambda: self.hook.get_focus( + self._get_widget_for_focus()) + self.check_resize() + + def reshape_table(self): + """Reshape a table according to the space allocated.""" + total_x_bound = self.entry_table.get_allocation().width + if not len(self.entries): + return False + entries_bound = sum([e.get_allocation().width for e in self.entries]) + each_entry_bound = entries_bound / len(self.entries) + maximum_entry_number = float(total_x_bound) / float(each_entry_bound) + rounded_max = int(maximum_entry_number) + 1 + if rounded_max != self.num_allowed_columns + 2 and rounded_max > 2: + self.num_allowed_columns = max(1, rounded_max - 2) + self.populate_table() + + def add_entry(self): + """Add a new entry (with null text) to the variable array.""" + widget = self.get_entry('') + self.entries.append(widget) + self._adjust_entry_length() + self.populate_table(focus_widget=widget) + if (self.metadata.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE): + self.setter(widget) + + def remove_entry(self): + """Remove the last selected or the last entry.""" + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + text = self.last_selected_src.get_text() + widget = self.entries.pop( + self.entries.index(self.last_selected_src)) + self.last_selected_src = None + else: + text = self.entries[-1].get_text() + widget = self.entries.pop() + self.populate_table() + if (self.metadata.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE or text): + # Optional, or compulsory but not blank. + self.setter(widget) + + def setter(self, widget): + """Reconstruct the new variable value from the entry array.""" + val_array = [e.get_text() for e in self.entries] + max_length = max([len(v) for v in val_array] + [1]) + if max_length + 1 != self.chars_width: + self.chars_width = max_length + 1 + self._adjust_entry_length() + if widget is not None and not widget.is_focus(): + widget.grab_focus() + widget.set_position(len(widget.get_text())) + widget.select_region(widget.get_position(), + widget.get_position()) + entries_have_commas = any("," in v for v in val_array) + new_value = python_array_join(val_array) + if new_value != self.value: + self.value = new_value + self.set_value(new_value) + if entries_have_commas: + new_val_array = python_array_split(new_value) + if len(new_val_array) != len(self.entries): + self.generate_entries() + focus_index = None + for i, val in enumerate(val_array): + if "," in val: + val_post_comma = val[:val.index(",") + 1] + focus_index = len(python_array_join( + new_val_array[:i] + [val_post_comma])) + self.populate_table() + self.set_focus_index(focus_index) + return False + + def _adjust_entry_length(self): + for widget in self.entries: + widget.set_width_chars(self.chars_width) + widget.set_max_length(self.chars_width) + self.reshape_table() + + def _get_widget_for_focus(self): + if self.entries: + return self.entries[-1] + return self.entry_table + + def _handle_focus_off_entry(self, widget, event): + if widget == self.last_selected_src: + try: + widget.set_progress_fraction(1.0) + except AttributeError: + widget.drag_highlight() + if widget.get_position() is None: + widget.set_position(len(widget.get_text())) + + def _handle_focus_on_entry(self, widget, event): + if self.last_selected_src is not None: + try: + self.last_selected_src.set_progress_fraction(0.0) + except AttributeError: + self.last_selected_src.drag_unhighlight() + self.last_selected_src = widget + is_start = (widget in self.entries and self.entries[0] == widget) + is_end = (widget in self.entries and self.entries[-1] == widget) + self.set_arrow_sensitive(not is_start, not is_end) + if widget.get_text() != '': + widget.select_region(widget.get_position(), + widget.get_position()) + return False + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self.setter(widget) + return False + + +def python_array_join(values): + """Create a Python-compliant list value from values.""" + return "[" + ", ".join(values) + "]" + + +def python_array_split(value): + """Split the value into elements with appropriate string values.""" + try: + value_array = ast.literal_eval(value) + except (SyntaxError, ValueError): + value_no_brackets = value.lstrip("[").rstrip("]") + value_array = rose.variable.array_split(value_no_brackets) + return value_array + cast_value_array = [] + for value in value_array: + if isinstance(value, str): + cast_value_array.append('"' + value + '"') + else: + cast_value_array.append(str(value)) + return cast_value_array diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py new file mode 100644 index 000000000..dd391bb58 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import re +import sys + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +from . import entry +import rose.gtk.util +import rose.variable + + +class RowArrayValueWidget(Gtk.HBox): + + """This is a class to represent a value as part of a row.""" + + BAD_COLOUR = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match + TIP_ADD = 'Add array element' + TIP_DELETE = 'Remove last array element' + TIP_INVALID_ENTRY = "Invalid entry - not {0}" + MIN_WIDTH_CHARS = 7 + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(RowArrayValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.value_array = rose.variable.array_split(value) + self.extra_array = [] # For new rows + self.element_values = [] + self.rows = [] + self.widgets = [] + self.has_length_error = False + self.length = metadata.get(rose.META_PROP_LENGTH) + self.type = metadata.get(rose.META_PROP_TYPE, "raw") + self.num_cols = len(self.value_array) + if arg_str is None: + if isinstance(self.type, list): + self.num_cols = len(self.type) + elif self.length is not None and self.length.isdigit(): + self.num_cols = int(self.length) + else: + self.num_cols = int(arg_str) + self.unlimited = (self.length == ':') + if self.unlimited: + self.array_length = 1 + else: + self.array_length = metadata.get(rose.META_PROP_LENGTH, 1) + log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] + self.make_log_image = lambda i: Gtk.Image.new_from_stock(*log_imgs[i]) + self.set_num_rows() + self.entry_table = Gtk.Table(rows=self.num_rows, + columns=self.num_cols, + homogeneous=True) + self.entry_table.connect('focus-in-event', + self.hook.trigger_scroll) + self.entry_table.show() + for i in range(self.num_rows): + self.insert_row(i) + self.normalise_width_widgets() + self.generate_buttons(is_for_elements=not isinstance(self.type, list)) + self.pack_start(self.add_del_button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.show() + + def set_num_rows(self): + """Derive the number of columns and rows.""" + if not isinstance(self.type, list): + self.num_rows = 1 + self.max_rows = 1 + self.unlimited = False + return + columns = len(self.type) + if self.CHECK_NAME_IS_ELEMENT(self.metadata['id']): + self.unlimited = False + if self.unlimited: + self.num_rows, rem = divmod(len(self.value_array), columns) + self.num_rows += [1, 0][rem == 0] + self.max_rows = sys.maxsize + else: + self.num_rows = int(self.array_length) + rem = divmod(len(self.value_array), columns)[1] + if self.num_rows == 0: + self.num_rows = 1 + self.max_rows = self.num_rows + if rem != 0: + # Then there is an incorrect number of entries. + # Display as entry box. + self.num_rows = 1 + self.max_rows = 1 + self.unlimited = False + self.has_length_error = True + self.value_array = [self.value] + if self.num_rows == 0: + self.num_rows = 1 + if self.max_rows == 0: + self.max_rows = 1 + + def get_type(self, index): + """Get the metadata type for this value index.""" + return self.get_types()[index] + + def get_types(self): + """Get a list of metadata types for the value.""" + if isinstance(self.type, list): + return self.type + return [self.type] * self.num_cols + + def grab_focus(self): + if self.entry_table.focus_child is None: + self.hook.get_focus(self.rows[-1][-1]) + else: + self.hook.get_focus(self.entry_table.focus_child) + + def add_element(self, *args): + """Create a new element (non-derived types).""" + w_value = rose.variable.get_value_from_metadata( + {rose.META_PROP_TYPE: self.type}) + self.value_array = self.value_array + [w_value] + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + for i in range(self.num_rows): + self.insert_row(i) + self.normalise_width_widgets() + self._decide_show_buttons() + + def add_row(self, *args): + """Create a new row of widgets.""" + nrows = self.entry_table.child_get_property( + self.rows[-1][-1], 'top-attach') + self.entry_table.resize(nrows + 2, self.num_cols) + new_values = self.insert_row(nrows + 1) + if any(new_values): + self.value_array = self.value_array + new_values + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + self.set_num_rows() + self.normalise_width_widgets() + self._decide_show_buttons() + return False + + def get_focus_index(self): + text = '' + for i, widget_list in enumerate(self.rows): + for j, widget in enumerate(widget_list): + value_index = i * self.num_cols + j + if value_index > len(self.value_array) - 1: + return len(text) + val = self.value_array[i * self.num_cols + j] + prefix_text = entry.get_next_delimiter(self.value[len(text):], + val) + if prefix_text is None: + return + if widget == self.entry_table.focus_child: + if hasattr(widget, "get_focus_index"): + position = widget.get_focus_index() + return len(text + prefix_text) + position + else: + for child in widget.get_children(): + if not hasattr(child, "get_position"): + continue + position = child.get_position() + if self.get_type(j) in ["character", "quoted"]: + position += 1 + return len(text + prefix_text) + position + return len(text + prefix_text) + len(val) + text += prefix_text + val + return None + + def set_focus_index(self, focus_index=None): + """Set the focus and position within the table.""" + if focus_index is None: + return + value_array = rose.variable.array_split(self.value) + text = '' + widgets = [] + for widget_list in self.rows: + widgets.extend(widget_list) + if self.has_length_error: # Special invalid length widget + widgets[0].grab_focus() + if hasattr(widgets[0], "set_focus_index"): + widgets[0].set_focus_index(focus_index) + return + for i, val in enumerate(value_array): + prefix = entry.get_next_delimiter(self.value[len(text):], val) + if prefix is None: + return + if len(text + prefix + val) >= focus_index: + if len(widgets) > i: + widgets[i].grab_focus() + val_offset = focus_index - len(text + prefix) + if hasattr(widgets[i], "set_focus_index"): + widgets[i].set_focus_index(val_offset) + return + text += prefix + val + + def del_element(self, *args): + """Create a new element (non-derived types).""" + self.value_array.pop() + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + for i in range(self.num_rows): + self.insert_row(i) + self.normalise_width_widgets() + self._decide_show_buttons() + + def del_row(self, *args): + """Delete the last row of widgets.""" + nrows = self.entry_table.child_get_property( + self.rows[-1][-1], 'top-attach') + for _ in enumerate(self.get_types()): + widget = self.rows[-1][-1] + self.rows[-1].pop(-1) + self.entry_table.remove(widget) + self.rows.pop(-1) + self.entry_table.resize(nrows, self.num_cols) + + chop_index = len(self.value_array) - len(self.get_types()) + self.value_array = self.value_array[:chop_index] + self.value = rose.variable.array_join(self.value_array) + self.set_value(self.value) + self.set_num_rows() + self.normalise_width_widgets() + self._decide_show_buttons() + return False + + def _decide_show_buttons(self): + # Show or hide the add row and delete row buttons. + if isinstance(self.type, list): + if len(self.rows) >= self.max_rows and not self.unlimited: + self.add_button.hide() + self.del_button.show() + else: + self.add_button.show() + self.del_button.show() + if len(self.rows) == 1: + self.del_button.hide() + else: + self.add_button.show() + else: + if (self.length is not None and self.length.isdigit() and + len(self.value_array) >= int(self.length)): + self.add_button.hide() + self.del_button.show() + else: + self.add_button.show() + self.del_button.show() + if len(self.value_array) == 1: + self.del_button.hide() + + def insert_row(self, row_index): + """Create a row of widgets from type_list.""" + widget_list = [] + new_values = [] + actual_num_cols = len(self.get_types()) + for i, el_piece_type in enumerate(self.get_types()): + unwrapped_index = row_index * actual_num_cols + i + value_index = unwrapped_index + if (not isinstance(self.type, list) and + value_index >= len(self.value_array)): + widget = Gtk.HBox() + eb0 = Gtk.EventBox() + eb0.show() + widget.pack_start(eb0, expand=True, fill=True) + widget.show() + self.entry_table.attach(widget, + i, i + 1, + row_index, row_index + 1, + xoptions=(Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL), + yoptions=Gtk.AttachOptions.SHRINK) + widget_list.append(widget) + continue + while value_index > len(self.value_array) - 1: + value_index -= actual_num_cols + if value_index < 0: + w_value = rose.variable.get_value_from_metadata( + {rose.META_PROP_TYPE: el_piece_type}) + else: + w_value = self.value_array[value_index] + new_values.append(w_value) + hover_text = '' + w_error = {} + if el_piece_type in ['integer', 'real']: + try: + [int, float][el_piece_type == 'real'](w_value) + except (TypeError, ValueError): + if w_value != '': + hover_text = self.TIP_INVALID_ENTRY.format( + el_piece_type) + w_error = {rose.META_PROP_TYPE: hover_text} + w_meta = {rose.META_PROP_TYPE: el_piece_type} + widget_cls = rose.config_editor.valuewidget.chooser( + w_value, w_meta, w_error) + hook = self.hook + setter = ArrayElementSetter(self.setter, unwrapped_index) + widget = widget_cls(w_value, w_meta, setter.set_value, hook) + if hover_text: + widget.set_tooltip_text(hover_text) + widget.show() + self.entry_table.attach(widget, + i, i + 1, + row_index, row_index + 1, + xoptions=(Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL), + yoptions=Gtk.AttachOptions.SHRINK) + widget_list.append(widget) + self.rows.append(widget_list) + self.widgets.extend(widget_list) + return new_values + + def normalise_width_widgets(self): + if not self.rows: + return + for widget in self.rows[0]: + self._normalise_width_chars(widget) + + def _normalise_width_chars(self, widget): + index = self.widgets.index(widget) + element = index % self.num_cols + max_width = {} + # Get max width + for widgets in self.rows: + if element >= len(widgets): + continue + e_widget = widgets[element] + i = 0 + child_list = e_widget.get_children() + while child_list: + child = child_list.pop() + if isinstance(child, Gtk.Entry) and hasattr(child, 'get_text'): + width = len(child.get_text()) + if width > max_width.get(i, -1): + max_width.update({i: width}) + if hasattr(child, 'get_children'): + child_list.extend(child.get_children()) + elif hasattr(child, 'get_child'): + child_list.append(child.get_child()) + i += 1 + for key, value in list(max_width.items()): + if value < self.MIN_WIDTH_CHARS: + max_width[key] = self.MIN_WIDTH_CHARS + # Set max width + for widgets in self.rows: + if element >= len(widgets): + continue + e_widget = widgets[element] + i = 0 + child_list = e_widget.get_children() + while child_list: + child = child_list.pop() + if (isinstance(child, Gtk.Entry) and + hasattr(child, 'set_width_chars')): + child.set_width_chars(max_width[i]) + if hasattr(child, 'get_children'): + child_list.extend(child.get_children()) + elif hasattr(child, 'get_child'): + child_list.append(child.get_child()) + i += 1 + + def generate_buttons(self, is_for_elements=False): + """Insert an add row and delete row button.""" + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_DELETE) + self.del_button.add(del_image) + self.del_button.show() + if is_for_elements: + delete_func = self.del_element + else: + delete_func = self.del_row + self.del_button.connect('button-release-event', delete_func) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.show() + if is_for_elements: + add_func = self.add_element + else: + add_func = self.add_row + self.add_button.connect('button-release-event', add_func) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_del_button_box = Gtk.VBox() + self.add_del_button_box.pack_start( + self.add_button, expand=False, fill=False) + self.add_del_button_box.pack_start( + self.del_button, expand=False, fill=False) + self.add_del_button_box.show() + self._decide_show_buttons() + + def setter(self, array_index, element_value): + """Update the value.""" + actual_num_cols = len(self.get_types()) + widget_row = self.rows[array_index / actual_num_cols] + widget = widget_row[array_index % actual_num_cols] + self._normalise_width_chars(widget) + i = array_index - len(self.value_array) + if i >= 0: + while len(self.extra_array) <= i: + self.extra_array.append("") + self.extra_array[i] = element_value + ok_index = 0 + j = self.num_cols + while j <= len(self.extra_array): + if (len(self.extra_array[:j]) % self.num_cols == 0 and + all(self.extra_array[:j])): + ok_index = j + else: + break + j += self.num_cols + self.value_array.extend(self.extra_array[:ok_index]) + self.extra_array = self.extra_array[ok_index:] + else: + self.value_array[array_index] = element_value + new_val = rose.variable.array_join(self.value_array) + if new_val != self.value: + self.value = new_val + self.set_value(new_val) + + +class ArrayElementSetter(object): + + """Element widget setter class.""" + + def __init__(self, setter_function, index): + self.setter_function = setter_function + self.index = index + + def set_value(self, value): + self.setter_function(self.index, value) diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py new file mode 100644 index 000000000..2d9a7bf07 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import shlex + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor.util +import rose.gtk.util +import rose.variable + + +class SpacedListValueWidget(Gtk.HBox): + + """This is a class to represent a list separated by spaces.""" + + TIP_ADD = "Add array element" + TIP_DEL = "Remove array element" + TIP_ELEMENT = "Element {0}" + TIP_ELEMENT_CHAR = "Element {0}: '{1}'" + TIP_LEFT = "Move array element left" + TIP_RIGHT = "Move array element right" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(SpacedListValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.last_value = value + self.max_length = ":" + value_array = spaced_array_split(self.value) + self.chars_width = max([len(str(v)) for v in value_array] + [1]) + 1 + self.last_selected_src = None + # Designate the number of allowed columns - 10 for 4 chars width + self.num_allowed_columns = 3 + self.entry_table = Gtk.Table(rows=1, + columns=self.num_allowed_columns, + homogeneous=True) + self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table.show() + + self.entries = [] + self.generate_entries(value_array) + + self.has_titles = False + if "element-titles" in metadata: + self.has_titles = True + + self.generate_buttons() + self.populate_table() + self.pack_start(self.add_del_button_box, expand=False, fill=False) + self.pack_start(self.entry_table, expand=True, fill=True) + self.entry_table.connect_after('size-allocate', + lambda w, e: self.reshape_table()) + self.connect('focus-in-event', + lambda w, e: self.hook.get_focus(self.get_focus_entry())) + + def get_focus_entry(self): + """Get either the last selected entry or the last one.""" + if self.last_selected_src is not None: + return self.last_selected_src + if len(self.entries) > 0: + return self.entries[-1] + return None + + def get_focus_index(self): + """Get the focus and position within the table of entries.""" + text = '' + for my_entry in self.entries: + val = my_entry.get_text() + prefix = get_next_delimiter(self.value[len(text):], val) + if prefix is None: + return + if my_entry == self.entry_table.focus_child: + return len(text + prefix) + my_entry.get_position() + text += prefix + val + return None + + def set_focus_index(self, focus_index=None): + """Set the focus and position within the table of entries.""" + if focus_index is None: + return + value_array = spaced_array_split(self.value) + value_array_old = spaced_array_split(self.last_value) + for i, val in enumerate(value_array): + if i >= len(value_array_old): + self.entries[len(value_array) - 1].grab_focus() + break + if val != value_array_old[i]: + self.entries[i].grab_focus() + break + + def generate_entries(self, value_array=None): + """Create the Gtk.Entry objects for elements in the array.""" + if value_array is None: + value_array = spaced_array_split(self.value) + entries = [] + for value_item in value_array: + for entry in self.entries: + if entry.get_text() == value_item and entry not in entries: + entries.append(entry) + break + else: + entries.append(self.get_entry(value_item)) + self.entries = entries + + def generate_buttons(self): + """Create the left-right movement arrows and add button.""" + left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow.show() + left_event_box = Gtk.EventBox() + left_event_box.add(left_arrow) + left_event_box.show() + left_event_box.connect('button-press-event', + lambda b, e: self.move_element(-1)) + left_event_box.connect('enter-notify-event', self._handle_arrow_enter) + left_event_box.connect('leave-notify-event', self._handle_arrow_leave) + left_event_box.set_tooltip_text(self.TIP_LEFT) + right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow.show() + right_event_box = Gtk.EventBox() + right_event_box.show() + right_event_box.add(right_arrow) + right_event_box.connect( + 'button-press-event', + lambda b, e: self.move_element(1)) + right_event_box.connect('enter-notify-event', self._handle_arrow_enter) + right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.set_tooltip_text(self.TIP_RIGHT) + self.arrow_box = Gtk.HBox() + self.arrow_box.show() + self.arrow_box.pack_start(left_event_box, expand=False, fill=False) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.set_arrow_sensitive(False, False) + del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, + Gtk.IconSize.MENU) + del_image.show() + self.del_button = Gtk.EventBox() + self.del_button.set_tooltip_text(self.TIP_DEL) + self.del_button.add(del_image) + self.del_button.show() + self.del_button.connect('button-release-event', + lambda b, e: self.remove_entry()) + self.del_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.del_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.button_box = Gtk.HBox() + self.button_box.show() + self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_image.show() + self.add_button = Gtk.EventBox() + self.add_button.set_tooltip_text(self.TIP_ADD) + self.add_button.add(add_image) + self.add_button.show() + self.add_button.connect('button-release-event', + lambda b, e: self.add_entry()) + self.add_button.connect('enter-notify-event', + lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) + self.add_button.connect('leave-notify-event', + lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_del_button_box = Gtk.VBox() + self.add_del_button_box.pack_start( + self.add_button, expand=False, fill=False) + self.add_del_button_box.pack_start( + self.del_button, expand=False, fill=False) + self.add_del_button_box.show() + + def _handle_arrow_enter(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.ACTIVE) + + def _handle_arrow_leave(self, arrow_event_box, event): + if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: + arrow_event_box.set_state(Gtk.StateType.NORMAL) + + def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): + """Control the sensitivity of the movement buttons.""" + sens_tuple = (is_left_sensitive, is_right_sensitive) + for i, event_box in enumerate(self.arrow_box.get_children()): + event_box.get_child().set_sensitive(sens_tuple[i]) + if not sens_tuple[i]: + event_box.set_state(Gtk.StateType.NORMAL) + + def move_element(self, num_places_right): + """Move the entry left or right.""" + entry = self.last_selected_src + if entry is None: + return + old_index = self.entries.index(entry) + if (old_index + num_places_right < 0 or + old_index + num_places_right > len(self.entries) - 1): + return + self.entries.remove(entry) + self.entries.insert(old_index + num_places_right, entry) + self.populate_table() + self.setter(entry) + + def get_entry(self, value_item): + """Create a gtk Entry for this array element.""" + entry = Gtk.Entry() + entry.set_text(str(value_item)) + entry.connect('focus-in-event', + self._handle_focus_on_entry) + entry.connect("button-release-event", + self._handle_middle_click_paste) + entry.connect_after("paste-clipboard", self.setter) + entry.connect_after("key-release-event", + lambda e, v: self.setter(e)) + entry.connect_after("button-release-event", + lambda e, v: self.setter(e)) + entry.connect('focus-out-event', + self._handle_focus_off_entry) + entry.set_width_chars(self.chars_width - 1) + entry.show() + return entry + + def populate_table(self, focus_widget=None): + """Populate a table with the array elements, dynamically.""" + position = None + table_widgets = self.entries + [self.button_box] + table_children = self.entry_table.get_children() + if focus_widget is None: + for child in table_children: + if child.is_focus() and isinstance(child, Gtk.Entry): + focus_widget = child + position = focus_widget.get_position() + else: + position = focus_widget.get_position() + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + if (focus_widget is None and self.entry_table.is_focus() and + len(self.entries) > 0): + focus_widget = self.entries[-1] + position = len(focus_widget.get_text()) + num_fields = len(self.entries + [self.button_box]) + num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns + self.entry_table.resize(num_rows_now, self.num_allowed_columns) + if (self.max_length.isdigit() and + len(self.entries) >= int(self.max_length)): + self.add_button.hide() + else: + self.add_button.show() + if (self.max_length.isdigit() and + len(self.entries) <= int(self.max_length)): + self.del_button.hide() + elif len(self.entries) == 0: + self.del_button.hide() + else: + self.del_button.show() + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + index = self.entries.index(self.last_selected_src) + if index == 0: + self.set_arrow_sensitive(False, True) + elif index == len(self.entries) - 1: + self.set_arrow_sensitive(True, False) + if len(self.entries) < 2: + self.set_arrow_sensitive(False, False) + + if self.has_titles: + for col, label in enumerate(self.metadata['element-titles']): + if col >= len(table_widgets) - 1: + break + widget = Gtk.HBox() + label = Gtk.Label(label=self.metadata['element-titles'][col]) + label.show() + widget.pack_start(label, expand=True, fill=True) + widget.show() + self.entry_table.attach(widget, + col, col + 1, + 0, 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + + for i, widget in enumerate(table_widgets): + if isinstance(widget, Gtk.Entry): + widget.set_tooltip_text(self.TIP_ELEMENT.format((i + 1))) + row = i // self.num_allowed_columns + if self.has_titles: + row += 1 + column = i % self.num_allowed_columns + self.entry_table.attach(widget, + column, column + 1, + row, row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK) + if focus_widget is not None: + focus_widget.grab_focus() + focus_widget.set_position(position) + focus_widget.select_region(position, position) + self.grab_focus = lambda: self.hook.get_focus( + self._get_widget_for_focus()) + self.check_resize() + + def reshape_table(self): + """Reshape a table according to the space allocated.""" + total_x_bound = self.entry_table.get_allocation().width + if not len(self.entries): + return False + entries_bound = sum([e.get_allocation().width for e in self.entries]) + each_entry_bound = entries_bound / len(self.entries) + maximum_entry_number = float(total_x_bound) / float(each_entry_bound) + rounded_max = int(maximum_entry_number) + 1 + if rounded_max != self.num_allowed_columns + 2 and rounded_max > 2: + self.num_allowed_columns = max(1, rounded_max - 2) + self.populate_table() + + def add_entry(self): + """Add a new entry (with null text) to the variable array.""" + entry = self.get_entry('') + self.entries.append(entry) + self._adjust_entry_length() + self.populate_table(focus_widget=entry) + if (self.metadata.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE): + self.setter(entry) + + def remove_entry(self): + """Remove the last selected or the last entry.""" + if (self.last_selected_src is not None and + self.last_selected_src in self.entries): + text = self.last_selected_src.get_text() + entry = self.entries.pop( + self.entries.index(self.last_selected_src)) + self.last_selected_src = None + else: + text = self.entries[-1].get_text() + entry = self.entries.pop() + self.populate_table() + if (self.metadata.get(rose.META_PROP_COMPULSORY) != + rose.META_PROP_VALUE_TRUE or text): + # Optional, or compulsory but not blank. + self.setter(entry) + + def setter(self, widget): + """Reconstruct the new variable value from the entry array.""" + val_array = [e.get_text() for e in self.entries] + max_length = max([len(v) for v in val_array] + [1]) + if max_length + 1 != self.chars_width: + self.chars_width = max_length + 1 + self._adjust_entry_length() + if widget is not None and not widget.is_focus(): + widget.grab_focus() + widget.set_position(len(widget.get_text())) + widget.select_region(widget.get_position(), + widget.get_position()) + entries_have_spaces = any(" " in v for v in val_array) + new_value = spaced_array_join(val_array) + if new_value != self.value: + self.last_value = self.value + self.value = new_value + self.set_value(new_value) + if entries_have_spaces: + new_val_array = spaced_array_split(new_value) + if len(new_val_array) != len(self.entries): + self.generate_entries() + focus_index = None + for i, val in enumerate(val_array): + if "" in val: + val_post_comma = val[:val.index("") + 1] + focus_index = len(spaced_array_join( + new_val_array[:i] + [val_post_comma])) + self.populate_table() + self.set_focus_index(focus_index) + return False + + def _adjust_entry_length(self): + for entry in self.entries: + entry.set_width_chars(self.chars_width) + self.reshape_table() + + def _get_widget_for_focus(self): + if self.entries: + return self.entries[-1] + return self.entry_table + + def _handle_focus_off_entry(self, widget, event): + if widget == self.last_selected_src: + try: + widget.set_progress_fraction(1.0) + except AttributeError: + widget.drag_highlight() + if widget.get_position() is None: + widget.set_position(len(widget.get_text())) + + def _handle_focus_on_entry(self, widget, event): + if self.last_selected_src is not None: + try: + self.last_selected_src.set_progress_fraction(0.0) + except AttributeError: + self.last_selected_src.drag_unhighlight() + self.last_selected_src = widget + is_start = (widget in self.entries and self.entries[0] == widget) + is_end = (widget in self.entries and self.entries[-1] == widget) + self.set_arrow_sensitive(not is_start, not is_end) + if widget.get_text() != '': + widget.select_region(widget.get_position(), + widget.get_position()) + return False + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self.setter(widget) + return False + + +def get_next_delimiter(array_text, next_element): + """Return the part of array_text immediately preceding next_element.""" + try: + val = array_text.index(next_element) + except ValueError: + return + return array_text[:val] + + +def spaced_array_join(values): + """Create a Spaced-compliant list value from values.""" + return " ".join(values) + + +def spaced_array_split(value): + """Split the value into elements with appropriate string values.""" + try: + value_array = shlex.split(value) + except (SyntaxError, ValueError): + value_array = rose.variable.array_split(value) + return value_array diff --git a/metomi/rose/config_editor/valuewidget/boolradio.py b/metomi/rose/config_editor/valuewidget/boolradio.py new file mode 100644 index 000000000..d6bcc09af --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/boolradio.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +from . import radiobuttons + + +class BoolValueWidget(radiobuttons.RadioButtonsValueWidget): + + """Produces 'true' and 'false' labelled radio buttons.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(BoolValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.allowed_values = [] + self.label_dict = {} + if metadata.get(rose.META_PROP_TYPE) == "boolean": + self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_TRUE, + rose.TYPE_BOOLEAN_VALUE_FALSE] + else: + self.allowed_values = [rose.TYPE_LOGICAL_VALUE_TRUE, + rose.TYPE_LOGICAL_VALUE_FALSE] + self.label_dict = { + rose.TYPE_LOGICAL_VALUE_TRUE: + rose.TYPE_LOGICAL_TRUE_TITLE, + rose.TYPE_LOGICAL_VALUE_FALSE: + rose.TYPE_LOGICAL_FALSE_TITLE} + + for k, item in enumerate(self.allowed_values): + if item in self.label_dict: + button_label = str(self.label_dict[item]) + else: + button_label = str(item) + self.label_dict.update({item: button_label}) + if k == 0: + radio_button = Gtk.RadioButton(group=None, + label=button_label, + use_underline=False) + radio_button.real_value = item + else: + radio_button = Gtk.RadioButton(group=radio_button, + label=button_label, + use_underline=False) + radio_button.real_value = item + radio_button.set_active(False) + if item == str(value): + radio_button.set_active(True) + radio_button.connect('toggled', self.setter) + self.pack_start(radio_button, False, False, 10) + radio_button.show() + radio_button.connect('focus-in-event', self.hook.trigger_scroll) + self.grab_focus = lambda: self.hook.get_focus(radio_button) + + def setter(self, widget, variable): + if widget.get_active(): + label_value = widget.get_label() + for real_item, label in list(self.label_dict.items()): + if label == label_value: + chosen_value = real_item + break + self.value = chosen_value + self.set_value(chosen_value) + return False diff --git a/metomi/rose/config_editor/valuewidget/booltoggle.py b/metomi/rose/config_editor/valuewidget/booltoggle.py new file mode 100644 index 000000000..8296e38d4 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/booltoggle.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose + + +class BoolToggleValueWidget(Gtk.HBox): + + """Produces a 'true' and 'false' labelled toggle button.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(BoolToggleValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.allowed_values = [] + self.label_dict = {} + if metadata.get(rose.META_PROP_TYPE) == "boolean": + self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_FALSE, + rose.TYPE_BOOLEAN_VALUE_TRUE] + self.label_dict = dict(list(zip(self.allowed_values, + self.allowed_values))) + elif metadata.get(rose.META_PROP_TYPE) == "python_boolean": + self.allowed_values = [rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] + self.label_dict = dict(list(zip(self.allowed_values, + self.allowed_values))) + else: + self.allowed_values = [rose.TYPE_LOGICAL_VALUE_FALSE, + rose.TYPE_LOGICAL_VALUE_TRUE] + self.label_dict = { + rose.TYPE_LOGICAL_VALUE_FALSE: + rose.TYPE_LOGICAL_FALSE_TITLE, + rose.TYPE_LOGICAL_VALUE_TRUE: + rose.TYPE_LOGICAL_TRUE_TITLE} + + imgs = [Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, + Gtk.IconSize.MENU), + Gtk.Image.new_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU)] + self.image_dict = dict(list(zip(self.allowed_values, imgs))) + bad_img = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, + Gtk.IconSize.MENU) + self.button = Gtk.ToggleButton(label=self.value) + if self.value in self.allowed_values: + self.button.set_active(self.allowed_values.index(self.value)) + self.button.set_label(self.label_dict[self.value]) + self.button.set_image(self.image_dict[self.value]) + else: + self.button.set_inconsistent(True) + self.button.set_image(bad_img) + self.button.connect('toggled', self._switch_state_and_set) + self.button.show() + self.pack_start(self.button, expand=False, fill=False) + self.grab_focus = lambda: self.hook.get_focus(self.button) + self.button.connect('focus-in-event', self.hook.trigger_scroll) + + def _switch_state_and_set(self, widget): + state = self.allowed_values[int(widget.get_active())] + title = self.label_dict[state] + image = self.image_dict[state] + widget.set_label(title) + widget.set_image(image) + self.setter(widget) + + def setter(self, widget): + label_value = widget.get_label() + for real_item, label in list(self.label_dict.items()): + if label == label_value: + chosen_value = real_item + break + self.value = chosen_value + self.set_value(chosen_value) + return False diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py new file mode 100644 index 000000000..2a3101e8a --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +from rose import META_PROP_TYPE +import rose.config_editor.util + + +class QuotedTextValueWidget(Gtk.HBox): + + """This class represents 'character' and 'quoted' types in an entry.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(QuotedTextValueWidget, self).__init__(homogeneous=False, + spacing=0) + # Importing here prevents cyclic imports + import rose.macros.value + self.type = metadata.get(META_PROP_TYPE) + checker = rose.macros.value.ValueChecker() + if self.type == "character": + self.type_checker = checker.check_character + self.format_text_in = ( + rose.config_editor.util.text_for_character_widget) + self.format_text_out = ( + rose.config_editor.util.text_from_character_widget) + self.quote_char = "'" + self.esc_quote_chars = "''" + elif self.type == "quoted": + self.type_checker = checker.check_quoted + self.format_text_in = ( + rose.config_editor.util.text_for_quoted_widget) + self.format_text_out = ( + rose.config_editor.util.text_from_quoted_widget) + self.quote_char = '"' + self.esc_quote_chars = '\\"' + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.entry = Gtk.Entry() + insensitive_colour = Gtk.Style().bg[0] + self.entry.modify_bg(Gtk.StateType.INSENSITIVE, + insensitive_colour) + self.in_error = not self.type_checker(self.value) + self.set_entry_text() + self.entry.connect("button-release-event", + self._handle_middle_click_paste) + self.entry.connect_after("paste-clipboard", self.setter) + self.entry.connect_after("key-release-event", + lambda e, v: self.setter(e)) + self.entry.connect_after("button-release-event", + lambda e, v: self.setter(e)) + self.entry.show() + self.pack_start(self.entry, expand=True, fill=True, + padding=0) + self.entry.connect('focus-in-event', self.hook.trigger_scroll) + self.grab_focus = lambda: self.hook.get_focus(self.entry) + + def set_entry_text(self): + """Initialise the text in the widget.""" + raw_text = self.value + if not self.in_error: + self.entry.set_text(self.format_text_in(raw_text)) + else: + self.entry.set_text(raw_text) + + def setter(self, *args): + var_text = self.entry.get_text() + if not self.value or not self.in_error: + # Text was in processed form + var_text = self.format_text_out(var_text) + if var_text != self.value: + self.value = var_text + self.set_value(var_text) + return False + + def get_focus_index(self): + """Retrieve the current cursor index.""" + position = self.entry.get_position() + if self.in_error: + return position + text = self.entry.get_text() + prefix = text[:position] + i = 0 + while prefix: + if self.value[i] == prefix[0]: + prefix = prefix[1:] + i = i + 1 + if not prefix: + break + return i + + def set_focus_index(self, focus_index): + """Set the current cursor index.""" + self.entry.set_position(focus_index - 1) + + def handle_type_error(self, has_error): + """Handle a change in error related to the value. + + We need to distinguish between quote-related errors and errors + related to pattern matching or other attributes. + + """ + position = self.entry.get_position() + text = self.entry.get_text() + was_in_error = self.in_error + self.in_error = not self.type_checker(self.value) + if self.in_error and not was_in_error: + # This is an incoming quote error. + position += 1 + text[:position].count(self.quote_char) + elif was_in_error and not self.in_error: + # This is an outgoing quote error. + position -= 1 + text[:position].count(self.esc_quote_chars) + else: + # The error isn't related to quotes, so don't do anything. + return False + self.set_entry_text() + self.entry.set_position(position) + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self.setter() + return False diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py new file mode 100644 index 000000000..67a104969 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import ast +import shlex + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +import rose.gtk.choice +import rose.gtk.dialog +import rose.opt_parse +import rose.variable + + +class ChoicesValueWidget(Gtk.HBox): + + """This represents a value as actual/available choices. + + Arguments are standard, except for the custom arg_str argument, + set in the metadata. In this case we take a shell command-like + syntax: + + # NAME + # rose.config_editor.valuewidget.choice.ChoicesValueWidget + # + # SYNOPSIS + # rose...Widget [OPTIONS] [CUSTOM_CHOICE_HINT ...] + # + # DESCRIPTION + # Represent available choices as a widget. + # + # OPTIONS + # --all-group=CHOICE + # The CHOICE that includes all other choices. + # For example: ALL, STANDARD + # --choices=CHOICE1[,CHOICE2,CHOICE3...] + # Add a comma-delimited list of choice(s) to the list of + # available choices for the widget. + # This option can be used repeatedly. + # --editable + # Allow custom choices to be entered that are not in choices + # --format=FORMAT + # Specify a different format to convert the list of included + # choices into the variable value. + # The only supported format is "python" which outputs the + # result of repr(my_list) - e.g. VARIABLE=["A", "B"]. + # If not specified, the format will default to rose array + # standard e.g. VARIABLE=A, B. + # --guess-groups + # Extrapolate inter-choice dependencies from their names. + # For example, this would guess that "LINUX" would trigger + # "LINUX_QUICK". + # + # CUSTOM_CHOICE_HINT + # Optional custom choice hints for the user, valid with --editable. + """ + + OPTIONS = { + "all_group": [ + ["--all-group"], + {"action": "store", + "metavar": "CHOICE"}], + "choices": [ + ["--choices"], + {"action": "append", + "default": None, + "metavar": "CHOICE"}], + "editable": [ + ["--editable"], + {"action": "store_true", + "default": False}], + "format": [ + ["--format"], + {"action": "store", + "metavar": "FORMAT"}], + "guess_groups": [ + ["--guess-groups"], + {"action": "store_true", + "default": False}]} + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(ChoicesValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + + self.opt_parser = rose.opt_parse.RoseOptionParser() + self.opt_parser.OPTIONS = self.OPTIONS + self.opt_parser.add_my_options(*list(self.OPTIONS.keys())) + opts, args = self.opt_parser.parse_args(shlex.split(arg_str)) + self.all_group = opts.all_group + self.groups = [] + if opts.choices is not None: + for choices in opts.choices: + self.groups.extend(rose.variable.array_split(choices)) + self.should_edit = opts.editable + self.value_format = opts.format + self.should_guess_groups = opts.guess_groups + self.hints = list(args) + + self.should_show_kinship = self._calc_should_show_kinship() + list_vbox = Gtk.VBox() + list_vbox.show() + self._listview = rose.gtk.choice.ChoicesListView( + self._set_value_listview, + self._get_value_values, + self._handle_search) + self._listview.show() + list_frame = Gtk.Frame() + list_frame.show() + list_frame.add(self._listview) + list_vbox.pack_start(list_frame, expand=False, fill=False) + self.pack_start(list_vbox, expand=True, fill=True) + tree_vbox = Gtk.VBox() + tree_vbox.show() + self._treeview = rose.gtk.choice.ChoicesTreeView( + self._set_value_treeview, + self._get_value_values, + self._get_available_values, + self._get_groups, + self._get_is_implicit) + self._treeview.show() + tree_frame = Gtk.Frame() + tree_frame.show() + tree_frame.add(self._treeview) + tree_vbox.pack_start(tree_frame, expand=True, fill=True) + if self.should_edit: + add_widget = self._get_add_widget() + tree_vbox.pack_end(add_widget, expand=False, fill=False) + self.pack_start(tree_vbox, expand=True, fill=True) + self._listview.connect('focus-in-event', + self.hook.trigger_scroll) + self._treeview.connect('focus-in-event', + self.hook.trigger_scroll) + self.grab_focus = lambda: self.hook.get_focus(self._listview) + + def _handle_search(self, name): + return False + + def _get_add_widget(self): + add_hbox = Gtk.HBox() + add_entry = Gtk.ComboBoxEntry() + add_entry.connect("changed", self._handle_combo_choice) + add_entry.get_child().connect( + "key-press-event", + lambda w, e: self._handle_text_choice(add_entry, e)) + add_entry.set_tooltip_text(rose.config_editor.CHOICE_TIP_ENTER_CUSTOM) + add_entry.show() + self._set_available_hints(add_entry) + add_hbox.pack_end(add_entry, expand=True, fill=True) + add_hbox.show() + return add_hbox + + def _set_available_hints(self, comboboxentry): + model = Gtk.ListStore(str) + values = self._get_value_values() + for hint in self.hints: + if hint not in values: + model.append([hint]) + comboboxentry.set_model(model) + comboboxentry.set_text_column(0) + + def _handle_combo_choice(self, comboboxentry): + iter_ = comboboxentry.get_active_iter() + if iter_ is None: + return False + self._add_custom_choice(comboboxentry, comboboxentry.get_active_text()) + + def _handle_text_choice(self, comboboxentry, event): + if Gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: + self._add_custom_choice(comboboxentry, + comboboxentry.get_child().get_text()) + return False + + def _add_custom_choice(self, comboboxentry, new_name): + entry = comboboxentry.get_child() + if not new_name: + text = rose.config_editor.ERROR_BAD_NAME.format("''") + title = rose.config_editor.DIALOG_TITLE_ERROR + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, title) + return False + new_values = self._get_value_values() + [entry.get_text()] + entry.set_text("") + self._format_and_set_value(" ".join(new_values)) + self._set_available_hints(comboboxentry) + self._listview.refresh() + self._treeview.refresh() + + def _get_value_values(self): + if self.value_format == "python": + try: + values = list(ast.literal_eval(self.value)) + except (SyntaxError, TypeError, ValueError): + values = [] + return values + return rose.variable.array_split(self.value) + + def _get_available_values(self): + return self.groups + + def _calc_should_show_kinship(self): + """Calculate whether to show parent-child relationships. + + Do not show any if any group has more than one parent group. + + """ + for group in self.groups: + grpset = set(group) + if len([g for g in self.groups if set(g).issubset(grpset)]) > 1: + return False + return True + + def _get_groups(self, name, names): + if self.all_group is not None: + default_groups = [self.all_group] + default_groups = [] + if not self.should_guess_groups or not self.should_show_kinship: + return default_groups + ok_groups = [n for n in names if set(n).issubset(name) and n != name] + ok_groups.sort(lambda x, y: set(x).issubset(y) - set(y).issubset(x)) + for group in default_groups: + if group in ok_groups: + ok_groups.remove(group) + return default_groups + ok_groups + + def _get_is_implicit(self, name): + if not self.should_guess_groups: + return False + values = self._get_value_values() + if self.all_group in values: + return True + for group in self.groups: + if group in values and set(group).issubset(name) and group != name: + return True + return False + + def _set_value_listview(self, new_value): + if new_value != self.value: + self._format_and_set_value(new_value) + self._treeview.refresh() + + def _set_value_treeview(self, new_value): + if new_value != self.value: + self._format_and_set_value(new_value) + self._listview.refresh() + + def _format_and_set_value(self, new_value): + if self.value_format == "python": + new_value = repr(shlex.split(new_value)) + else: + new_value = rose.variable.array_join(shlex.split(new_value)) + self.value = new_value + self.set_value(new_value) diff --git a/metomi/rose/config_editor/valuewidget/combobox.py b/metomi/rose/config_editor/valuewidget/combobox.py new file mode 100644 index 000000000..ac95fb2ef --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/combobox.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor + + +class ComboBoxValueWidget(Gtk.HBox): + + """This is a class to add a combo box for a set of variable values. + + It needs to have some allowed values set in the variable metadata. + + """ + + FRAC_X_ALIGN = 0.9 + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(ComboBoxValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + comboboxentry = Gtk.ComboBox() + liststore = Gtk.ListStore(str) + cell = Gtk.CellRendererText() + cell.xalign = self.FRAC_X_ALIGN + comboboxentry.pack_start(cell, True, True, 0) + comboboxentry.add_attribute(cell, 'text', 0) + + var_values = self.metadata[rose.META_PROP_VALUES] + var_titles = self.metadata.get(rose.META_PROP_VALUE_TITLES) + for k, entry in enumerate(var_values): + if var_titles is not None and var_titles[k]: + liststore.append([var_titles[k] + " (" + entry + ")"]) + else: + liststore.append([entry]) + comboboxentry.set_model(liststore) + if self.value in var_values: + index = self.metadata['values'].index(self.value) + comboboxentry.set_active(index) + comboboxentry.connect('changed', self.setter) + comboboxentry.connect('button-press-event', + lambda b: comboboxentry.grab_focus()) + comboboxentry.show() + self.pack_start(comboboxentry, False, False, 0) + self.grab_focus = lambda: self.hook.get_focus(comboboxentry) + self.set_contains_error = (lambda e: + comboboxentry.modify_bg(Gtk.StateType.NORMAL, + self.bad_colour)) + + def setter(self, widget): + index = widget.get_active() + self.value = self.metadata[rose.META_PROP_VALUES][index] + self.set_value(self.value) + return False diff --git a/metomi/rose/config_editor/valuewidget/files.py b/metomi/rose/config_editor/valuewidget/files.py new file mode 100644 index 000000000..e3d264752 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/files.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import os + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import rose.config_editor +import rose.external +import rose.gtk.util + + +class FileChooserValueWidget(Gtk.HBox): + + """This class displays a path, with an open dialog to define a new one.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(FileChooserValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.generate_entry() + self.generate_editor_launcher() + self.open_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_OPEN, + size=Gtk.IconSize.MENU, + as_tool=False, + tip_text="Browse for a filename") + self.open_button.show() + self.open_button.connect("clicked", self.run_and_destroy) + self.pack_end(self.open_button, expand=False, fill=False) + self.edit_button.set_sensitive(os.path.isfile(self.value)) + + def generate_entry(self): + self.entry = Gtk.Entry() + self.entry.set_text(self.value) + self.entry.show() + self.entry.connect("changed", self.setter) + self.entry.connect("focus-in-event", + self.hook.trigger_scroll) + self.pack_start(self.entry, True, True, 0) + self.grab_focus = lambda: self.hook.get_focus(self.entry) + + def run_and_destroy(self, *args): + file_chooser_widget = Gtk.FileChooserDialog( + buttons=(Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT)) + if os.path.exists(os.path.dirname(self.value)): + file_chooser_widget.set_filename(self.value) + response = file_chooser_widget.run() + if response in [Gtk.ResponseType.ACCEPT, Gtk.ResponseType.OK, + Gtk.ResponseType.YES]: + self.entry.set_text(file_chooser_widget.get_filename()) + file_chooser_widget.destroy() + return False + + def generate_editor_launcher(self): + self.edit_button = rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_DND, + size=Gtk.IconSize.MENU, + as_tool=False, + tip_text="Edit the file") + self.edit_button.connect( + "clicked", + lambda b: rose.external.launch_geditor(self.value)) + self.pack_end(self.edit_button, expand=False, fill=False) + + def setter(self, widget): + self.value = widget.get_text() + self.set_value(self.value) + self.edit_button.set_sensitive(os.path.isfile(self.value)) + return False + + +class FileEditorValueWidget(Gtk.HBox): + + """This class creates a button that launches an editor for a file path.""" + + FILE_PROTOCOL = "file://{0}" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(FileEditorValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.generate_editor_launcher() + + def generate_editor_launcher(self): + self.edit_button = rose.gtk.util.CustomButton( + label=rose.config_editor.LABEL_EDIT, + stock_id=Gtk.STOCK_DND, + size=Gtk.IconSize.MENU, + as_tool=False, + tip_text="Edit the file") + self.edit_button.connect("clicked", self.on_click) + self.pack_start(self.edit_button, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + + def retrieve_path(self): + root = self.metadata[rose.config_editor.META_PROP_INTERNAL] + return os.path.join(root, self.value) + + def on_click(self, button): + path = self.retrieve_path() + rose.external.launch_geditor(path) diff --git a/metomi/rose/config_editor/valuewidget/format.py b/metomi/rose/config_editor/valuewidget/format.py new file mode 100644 index 000000000..116a11c44 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/format.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config + + +class FormatsChooserValueWidget(Gtk.HBox): + + """This class allows the addition of section names to a variable value.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(FormatsChooserValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + + if 'values_getter' in self.metadata: + meta = self.metadata + self.values_getter = meta['values_getter'] + else: + self.values_getter = lambda: meta.get('values', []) + num_entries = len(value.split(' ')) + self.entry_table = Gtk.Table(rows=num_entries + 1, columns=1) + self.entry_table.show() + self.entries = [] + for format_name in value.split(): + entry = self.get_entry(format_name) + self.entries.append(entry) + self.add_box = Gtk.HBox() + self.add_box.show() + image = Gtk.Image() + image.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + image.show() + image_event = Gtk.EventBox() + image_event.add(image) + image_event.show() + self.add_box.pack_start( + image_event, expand=False, fill=False, padding=5) + self.data_chooser = Gtk.ComboBoxText() + self.data_chooser.connect('focus-in-event', + lambda d, e: self.load_data_chooser()) + self.data_chooser.connect('changed', lambda d: self.add_new_section()) + self.data_chooser.show() + image_event.connect('button-press-event', + lambda i, w: (self.load_data_chooser() and + self.data_chooser.popup())) + self.add_box.pack_start(self.data_chooser, expand=False, fill=False, + padding=0) + + self.load_data_chooser() + self.populate_table() + self.pack_start(self.entry_table, expand=True, fill=True, padding=20) + + def get_entry(self, format_name): + """Create an entry box for a format name.""" + entry = Gtk.Entry() + entry.set_text(format_name) + entry.connect('focus-in-event', self.hook.trigger_scroll) + entry.connect('changed', self.entry_change_handler) + entry.show() + return entry + + def populate_table(self): + """Create a table for the format list and the add widget.""" + self.load_data_chooser() + for child in self.entry_table.get_children(): + self.entry_table.remove(child) + self.entry_table.resize(rows=len(self.entries) + 1, columns=1) + for i, widget in enumerate(self.entries + [self.add_box]): + self.entry_table.attach( + widget, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) + self.grab_focus = lambda: self.hook.get_focus(self.entries[-1]) + + def add_new_section(self): + value = self.get_active_text(self.data_chooser) + self.data_chooser.set_active(-1) + if value is None: + return False + self.entries.append(self.get_entry(value)) + self.entry_change_handler(self.entries[-1]) + self.populate_table() + return True + + def get_active_text(self, combobox): + index = combobox.get_active() + if index < 0: + return None + return combobox.get_model()[index][0] + + def entry_change_handler(self, entry): + position = entry.get_position() + if entry.get_text() == '' and len(self.entries) > 1: + self.entries.remove(entry) + new_value = ' '.join([e.get_text() for e in self.entries]) + self.value = new_value + self.set_value(new_value) + self.populate_table() + self.update_status() + self.load_data_chooser() + self.data_chooser.set_active(-1) + if entry in self.entries and not entry.is_focus(): + entry.grab_focus() + entry.set_position(position) + return False + + def load_data_chooser(self): + data_model = Gtk.ListStore(str) + options = self.values_getter() + options.sort(rose.config.sort_settings) + for value in options: + if value not in [e.get_text() for e in self.entries]: + data_model.append([str(value)]) + self.data_chooser.set_model(data_model) + return True diff --git a/metomi/rose/config_editor/valuewidget/intspin.py b/metomi/rose/config_editor/valuewidget/intspin.py new file mode 100644 index 000000000..1900b3e16 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/intspin.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import sys + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor + + +class IntSpinButtonValueWidget(Gtk.HBox): + + """This is a class to represent an integer with a spin button.""" + + WARNING_MESSAGE = 'Warning:\n variable value: {0}\n widget value: {1}' + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(IntSpinButtonValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.upper = sys.maxsize + self.lower = -sys.maxsize - 1 + + tooltip_text = None + try: + int_value = int(value) + except (TypeError, ValueError): + int_value = 0 + tooltip_text = self.WARNING_MESSAGE.format(value, int_value) + + value_ok = self.lower <= int_value <= self.upper + + if value_ok: + entry = self.make_spinner(int_value) + signal = 'changed' + else: + entry = Gtk.Entry() + entry.set_text(self.value) + signal = 'activate' + + self.change_id = entry.connect(signal, self.setter) + + entry.set_tooltip_text(tooltip_text) + entry.show() + + self.pack_start(entry, False, False, 0) + + self.warning_img = Gtk.Image() + if not value_ok: + self.warning_img = Gtk.Image() + self.warning_img.set_from_stock(Gtk.STOCK_DIALOG_WARNING, + Gtk.IconSize.MENU) + self.warning_img.set_tooltip_text( + rose.config_editor.WARNING_INTEGER_OUT_OF_BOUNDS) + self.warning_img.show() + self.pack_start(self.warning_img, False, False, 0) + + self.grab_focus = lambda: self.hook.get_focus(entry) + + def make_spinner(self, int_value): + my_adj = Gtk.Adjustment(value=int_value, + upper=self.upper, + lower=self.lower, + step_incr=1) + + spin_button = Gtk.SpinButton(adjustment=my_adj, digits=0) + spin_button.connect('focus-in-event', + self.hook.trigger_scroll) + + spin_button.set_numeric(True) + + return spin_button + + def setter(self, widget): + """Callback on widget value change. + + Note: 1. SpinButton's `.get_value_as_int` method is not reliable. It + returns the spin value but not the value of the text that is typed in + manually. 2. Calling `self.set_value` method with a value that cannot + be cast into an `int` may cause `Segmentation fault` on some version of + GTK, so we'll only call `self.set_value` for a value that can be cast + into an in-range `int` value. + """ + text = widget.get_text() + if text != self.value: + self.value = text + try: + value_ok = self.lower <= int(text) <= self.upper + except ValueError: + value_ok = False + if value_ok: + self.set_value(self.value) + self.warning_img.hide() + else: + self.warning_img.show() + return False diff --git a/metomi/rose/config_editor/valuewidget/meta.py b/metomi/rose/config_editor/valuewidget/meta.py new file mode 100644 index 000000000..0f5990196 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/meta.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + + +class MetaValueWidget(Gtk.HBox): + + """This class generates an entry and button for a metadata flag value.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(MetaValueWidget, self).__init__(homogeneous=False, spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.entry = Gtk.Entry() + self.normal_colour = self.entry.style.text[Gtk.StateType.NORMAL] + self.insens_colour = self.entry.style.text[Gtk.StateType.INSENSITIVE] + self.entry.set_text(self.value) + self.entry.connect("button-release-event", + self._handle_middle_click_paste) + self.entry.connect_after("paste-clipboard", self._check_diff) + self.entry.connect_after("key-release-event", self._check_diff) + self.entry.connect_after("button-release-event", self._check_diff) + self.entry.connect("activate", self._setter) + self.entry.connect("focus-out-event", self._setter) + self.entry.show() + self.button = Gtk.Button(stock=Gtk.STOCK_APPLY) + self.button.connect("clicked", self._setter) + self.button.set_sensitive(False) + self.button.show() + self.pack_start(self.entry, expand=True, fill=True, + padding=0) + self.pack_start(self.button, expand=False, fill=False, + padding=0) + self.entry.connect('focus-in-event', + self.hook.trigger_scroll) + self.grab_focus = lambda: self.hook.get_focus(self.entry) + + def _check_diff(self, *args): + text = self.entry.get_text() + if text == self.value: + self.entry.modify_text(Gtk.StateType.NORMAL, self.normal_colour) + self.button.set_sensitive(False) + else: + self.entry.modify_text(Gtk.StateType.NORMAL, self.insens_colour) + self.button.set_sensitive(True) + if not text: + self.button.set_sensitive(False) + + def _setter(self, *args): + text_value = self.entry.get_text() + if text_value and text_value != self.value: + self.value = self.entry.get_text() + self.set_value(self.value) + self._check_diff() + return False + + def get_focus_index(self): + """Return the cursor position within the variable value.""" + return self.entry.get_position() + + def set_focus_index(self, focus_index=None): + if focus_index is None: + return False + self.entry.set_position(focus_index) + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self._check_diff() + return False diff --git a/metomi/rose/config_editor/valuewidget/radiobuttons.py b/metomi/rose/config_editor/valuewidget/radiobuttons.py new file mode 100644 index 000000000..fed707eb1 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/radiobuttons.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor + + +class RadioButtonsValueWidget(Gtk.HBox): + + """This is a class to represent a value as radio buttons.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(RadioButtonsValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + + var_values = metadata[rose.META_PROP_VALUES] + var_titles = metadata.get(rose.META_PROP_VALUE_TITLES) + + if var_titles: + vbox = Gtk.VBox() + self.pack_start(vbox, False, True, 0) + vbox.show() + + for k, item in enumerate(var_values): + button_label = str(item) + if var_titles is not None and var_titles[k]: + button_label = var_titles[k] + if k == 0: + radio_button = Gtk.RadioButton(group=None, + label=button_label, + use_underline=False) + radio_button.real_value = item + else: + radio_button = Gtk.RadioButton(group=radio_button, + label=button_label, + use_underline=False) + radio_button.real_value = item + if var_titles is not None and var_titles[k]: + radio_button.set_tooltip_text("(" + item + ")") + radio_button.set_active(False) + if item == self.value: + radio_button.set_active(True) + radio_button.connect('toggled', self.setter) + radio_button.connect('button-press-event', self.setter) + radio_button.connect('activate', self.setter) + + if var_titles: + vbox.pack_start(radio_button, False, False, 2) + else: + self.pack_start(radio_button, False, False, 10) + radio_button.show() + radio_button.connect('focus-in-event', + self.hook.trigger_scroll) + + self.grab_focus = lambda: self.hook.get_focus(radio_button) + if len(var_values) == 1 and self.value == var_values[0]: + radio_button.set_sensitive(False) + + def setter(self, widget, event=None): + if widget.get_active(): + self.value = widget.real_value + self.set_value(self.value) + return False diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py new file mode 100644 index 000000000..0d1704694 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""This module contains a value widget for the 'source' file setting.""" + +import shlex + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.config_editor +import rose.formats +import rose.gtk.choice + + +class SourceValueWidget(Gtk.HBox): + + """This class generates a special widget for the file source variable. + + It cheats by passing in a special VariableOperations instance as + arg_str. This is used for search and getting and updating the + available sections. + + """ + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(SourceValueWidget, self).__init__(homogeneous=False, spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.var_ops = arg_str + formats = [f for f in rose.formats.__dict__ if not f.startswith('__')] + self.formats = formats + self.formats_ok = None + self._ok_content_sections = set([None]) + if self.formats_ok is None: + content_sections = self._get_available_sections() + self.formats_ok = bool(content_sections) + vbox = Gtk.VBox() + vbox.show() + formats_check_button = Gtk.CheckButton( + rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL) + formats_check_button.set_active(not self.formats_ok) + formats_check_button.connect("toggled", self._toggle_formats) + formats_check_button.show() + formats_check_hbox = Gtk.HBox() + formats_check_hbox.show() + formats_check_hbox.pack_end(formats_check_button, expand=False, + fill=False) + vbox.pack_start(formats_check_hbox, expand=False, fill=False) + treeviews_hbox = Gtk.HPaned() + treeviews_hbox.show() + self._listview = rose.gtk.choice.ChoicesListView( + self._set_listview, + self._get_included_sources, + self._handle_search, + get_custom_menu_items=self._get_custom_menu_items + ) + self._listview.set_tooltip_text( + rose.config_editor.FILE_CONTENT_PANEL_TIP) + frame = Gtk.Frame() + frame.show() + frame.add(self._listview) + value_vbox = Gtk.VBox() + value_vbox.show() + value_vbox.pack_start(frame, expand=False, fill=False) + value_eb = Gtk.EventBox() + value_eb.show() + value_vbox.pack_start(value_eb, expand=True, fill=True) + + self._available_frame = Gtk.Frame() + self._generate_available_treeview() + adder_value = "" + adder_metadata = {} + adder_set_value = lambda v: None + adder_hook = rose.config_editor.valuewidget.ValueWidgetHook() + self._adder = ( + rose.config_editor.valuewidget.files.FileChooserValueWidget( + adder_value, adder_metadata, adder_set_value, adder_hook)) + self._adder.entry.connect("activate", self._add_file_source) + self._adder.entry.set_tooltip_text( + rose.config_editor.TIP_VALUE_ADD_URI) + self._adder.show() + treeviews_hbox.add1(value_vbox) + treeviews_hbox.add2(self._available_frame) + vbox.pack_start(treeviews_hbox, expand=True, fill=True) + vbox.pack_start(self._adder, expand=True, fill=True) + self.grab_focus = lambda: self.hook.get_focus(self._listview) + self.pack_start(vbox, True, True, 0) + + def _toggle_formats(self, widget): + """Toggle the show/hide of the available format sections.""" + self.formats_ok = not widget.get_active() + if widget.get_active(): + self._available_frame.hide() + else: + self._available_frame.show() + + def _generate_available_treeview(self): + """Generate an available choices widget.""" + existing_widget = self._available_frame.get_child() + if existing_widget is not None: + self._available_frame.remove(existing_widget) + self._available_treeview = rose.gtk.choice.ChoicesTreeView( + self._set_available_treeview, + self._get_included_sources, + self._get_available_sections, + self._get_groups, + title=rose.config_editor.FILE_CONTENT_PANEL_TITLE, + get_is_included=self._get_section_is_included + ) + self._available_treeview.set_tooltip_text( + rose.config_editor.FILE_CONTENT_PANEL_OPT_TIP) + self._available_frame.show() + if not self.formats_ok: + self._available_frame.hide() + self._available_frame.add(self._available_treeview) + + def _get_custom_menu_items(self): + """Return some custom menuitems for use in the list view.""" + menuitem = Gtk.ImageMenuItem( + rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) + image = Gtk.Image.new_from_stock( + Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.MENU) + menuitem.set_image(image) + menuitem.connect( + "button-press-event", self._toggle_menu_optional_status) + menuitem.show() + return [menuitem] + + def _get_included_sources(self): + """Return sections included in the source variable.""" + return shlex.split(self.value) + + def _get_section_is_included(self, section, included_sections=None): + """Return whether a section is included or not.""" + if included_sections is None: + included_sections = self._get_included_sources() + for i, included_section in enumerate(included_sections): + if (included_section.startswith("(") and + included_section.endswith(")")): + included_sections[i] = included_section[1:-1] + return section in included_sections + + def _get_available_sections(self): + """Return sections available to the source variable.""" + ok_content_sections = [] + sections = list(self.var_ops.get_sections(self.metadata["full_ns"])) + for section in sections: + section_has_format = False + for format_ in self.formats: + if section.startswith(format_ + ":"): + section_has_format = True + break + if not section_has_format: + continue + if section.endswith(")"): + section_all = section.rsplit("(", 1)[0] + "(:)" + if section_all not in ok_content_sections: + ok_content_sections.append(section_all) + ok_content_sections.append(section) + ok_content_sections.sort(rose.config.sort_settings) + ok_content_sections.sort(self._sort_settings_duplicate) + return ok_content_sections + + def _get_groups(self, name, available_names): + """Return any groups in available_names that supersede name.""" + name_all = name.rsplit("(", 1)[0] + "(:)" + if name_all in available_names and name != name_all: + return [name_all] + return [] + + def _handle_search(self, name): + """Trigger a search for a section.""" + self.var_ops.search_for_var(self.metadata["full_ns"], name) + + def _set_listview(self, new_value): + """React to a set value request from the list view.""" + self._set_value(new_value) + self._available_treeview._realign() + + def _set_available_treeview(self, new_value): + """React to a set value request from the tree view.""" + new_values = shlex.split(new_value) + # Preserve optional values. + old_values = self._get_included_sources() + for i, value in enumerate(new_values): + if "(" + value + ")" in old_values: + new_values[i] = "(" + value + ")" + new_value = " ".join(new_values) + self._set_value(new_value) + self._listview._populate() + + def _add_file_source(self, entry): + """Add a file to the sources list.""" + url = entry.get_text() + if not url: + return False + if self.value: + new_value = self.value + " " + url + else: + new_value = url + self._set_value(new_value) + self._set_available_treeview(new_value) + entry.set_text("") + + def _set_value(self, new_value): + """Set the source variable value.""" + if new_value != self.value: + self.set_value(new_value) + self.value = new_value + + def _sort_settings_duplicate(self, sect1, sect2): + """Sort settings such that xyz(:) appears above xyz(1).""" + sect1_base = sect1.rsplit("(", 1)[0] + sect2_base = sect2.rsplit("(", 1)[0] + if sect1_base != sect2_base: + return 0 + sect1_ind = sect1.replace(sect1_base, "", 1) + sect2_ind = sect2.replace(sect2_base, "", 1) + return (sect2_ind == "(:)") - (sect1_ind == "(:)") + + def _toggle_menu_optional_status(self, menuitem, event): + """Toggle a source's optional status (surrounding brackets or not).""" + iter_ = menuitem._listview_iter + model = menuitem._listview_model + old_section_value = model.get_value(iter_, 0) + if (old_section_value.startswith("(") and + old_section_value.endswith(")")): + section_value = old_section_value[1:-1] + else: + section_value = "(" + old_section_value + ")" + model.set_value(iter_, 0, section_value) + values = self._get_included_sources() + for i, value in enumerate(values): + if value == old_section_value: + values[i] = section_value + self._set_value(" ".join(values)) diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py new file mode 100644 index 000000000..9c1d87052 --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor +import rose.config_editor.valuewidget +import rose.env +import rose.gtk.util + +ENV_COLOUR = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV) + + +class RawValueWidget(Gtk.HBox): + + """This class generates a basic entry widget for an unformatted value.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(RawValueWidget, self).__init__(homogeneous=False, spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.entry = Gtk.Entry() + insensitive_colour = Gtk.Style().bg[0] + self.entry.modify_bg(Gtk.StateType.INSENSITIVE, insensitive_colour) + self.normal_colour = Gtk.Style().fg[Gtk.StateType.NORMAL] + if rose.env.contains_env_var(self.value): + self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) + self.entry.set_tooltip_text(rose.config_editor.VAR_WIDGET_ENV_INFO) + self.entry.set_text(self.value) + self.entry.connect("button-release-event", + self._handle_middle_click_paste) + self.entry.connect_after("paste-clipboard", self.setter) + self.entry.connect_after("key-release-event", + lambda e, v: self.setter(e)) + self.entry.connect_after("button-release-event", + lambda e, v: self.setter(e)) + self.entry.show() + self.pack_start(self.entry, expand=True, fill=True, padding=0) + self.entry.connect('focus-in-event', + self.hook.trigger_scroll) + self.grab_focus = lambda: self.hook.get_focus(self.entry) + + def setter(self, widget, *args): + new_value = widget.get_text() + if new_value == self.value: + return False + self.value = new_value + self.set_value(self.value) + if rose.env.contains_env_var(self.value): + self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) + self.entry.set_tooltip_text(rose.config_editor.VAR_WIDGET_ENV_INFO) + else: + self.entry.set_tooltip_text(None) + return False + + def get_focus_index(self): + """Return the cursor position within the variable value.""" + return self.entry.get_position() + + def set_focus_index(self, focus_index=None): + if focus_index is None: + return False + self.entry.set_position(focus_index) + + def _handle_middle_click_paste(self, widget, event): + if event.button == 2: + self.setter(widget) + return False + + +class TextMultilineValueWidget(Gtk.HBox): + + """This class displays text with multiple lines.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(TextMultilineValueWidget, self).__init__(homogeneous=False, + spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + + self.entrybuffer = Gtk.TextBuffer() + self.entrybuffer.set_text(self.value) + self.entry = Gtk.TextView(self.entrybuffer) + self.entry.set_wrap_mode(Gtk.WrapMode.WORD) + self.entry.set_left_margin(rose.config_editor.SPACING_SUB_PAGE) + self.entry.set_right_margin(rose.config_editor.SPACING_SUB_PAGE) + self.entry.connect('focus-in-event', self.hook.trigger_scroll) + self.entry.show() + + viewport = Gtk.Viewport() + viewport.add(self.entry) + viewport.show() + + self.grab_focus = lambda: self.hook.get_focus(self.entry) + self.entrybuffer.connect('changed', self.setter) + self.pack_start(viewport, expand=True, fill=True) + + def get_focus_index(self): + """Return the cursor position within the variable value.""" + mark = self.entrybuffer.get_insert() + iter_ = self.entrybuffer.get_iter_at_mark(mark) + return iter_.get_offset() + + def set_focus_index(self, focus_index=None): + """Set the cursor position within the variable value.""" + if focus_index is None: + return False + iter_ = self.entrybuffer.get_iter_at_offset(focus_index) + self.entrybuffer.place_cursor(iter_) + + def setter(self, widget): + text = widget.get_text(widget.get_start_iter(), + widget.get_end_iter()) + if text != self.value: + self.value = text + self.set_value(self.value) + return False diff --git a/metomi/rose/config_editor/valuewidget/valuehints.py b/metomi/rose/config_editor/valuewidget/valuehints.py new file mode 100644 index 000000000..024f0d13e --- /dev/null +++ b/metomi/rose/config_editor/valuewidget/valuehints.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +from gi.repository import GObject +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor.util +import rose.gtk.util +import rose.variable + + +class HintsValueWidget(Gtk.HBox): + """This class generates a widget for entering value-hints.""" + + def __init__(self, value, metadata, set_value, hook, arg_str=None): + super(HintsValueWidget, self).__init__(homogeneous=False, spacing=0) + self.value = value + self.metadata = metadata + self.set_value = set_value + self.hook = hook + self.entry = Gtk.Entry() + self.entry.set_text(self.value) + self.entry.connect_after("paste-clipboard", self._setter) + self.entry.connect_after("key-release-event", self._setter) + self.entry.connect_after("button-release-event", self._setter) + self.entry.show() + GObject.idle_add(self._set_completion, self.metadata) + self.pack_start(self.entry, expand=True, fill=True, + padding=0) + self.entry.connect('focus-in-event', + hook.trigger_scroll) + self.grab_focus = lambda: hook.get_focus(self.entry) + + def _setter(self, *args): + """Alter the variable value and update status.""" + self.value = self.entry.get_text() + self.set_value(self.value) + return False + + def get_focus_index(self): + """Return the cursor position within the variable value.""" + return self.entry.get_position() + + def set_focus_index(self, focus_index=None): + """Set the cursor position within the variable value.""" + if focus_index is None: + return False + self.entry.set_position(focus_index) + + def _set_completion(self, metadata): + """ Return a predictive text model for value-hints.""" + completion = Gtk.EntryCompletion() + model = Gtk.ListStore(str) + var_hints = metadata.get(rose.META_PROP_VALUE_HINTS) + for hint in var_hints: + model.append([hint]) + completion.set_model(model) + completion.set_text_column(0) + completion.set_inline_completion(True) + completion.set_minimum_key_length(0) + self.entry.set_completion(completion) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py new file mode 100644 index 000000000..46b2089c3 --- /dev/null +++ b/metomi/rose/config_editor/variable.py @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import copy +import difflib +import re + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config_editor.keywidget +import rose.config_editor.menuwidget +import rose.config_editor.valuewidget +import rose.config_editor.valuewidget.array.row as row +import rose.config_editor.valuewidget.source +import rose.config_editor.util +import rose.gtk.dialog +import rose.gtk.util +import rose.reporter +import rose.resource + + +class VariableWidget(object): + + """This class generates a set of widgets representing the variable. + + The set of widgets generated depends on the variable metadata, if any. + Altering values using the widgets will alter the variable object as part + of the internal data model. + + """ + + def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, + hide_keywidget_subtext=False): + self.variable = variable + self.key = variable.name + self.value = variable.value + self.meta = variable.metadata + self.is_ghost = is_ghost + self.var_ops = var_ops + if show_modes is None: + show_modes = {} + self.show_modes = show_modes + self.insensitive_colour = Gtk.Style().bg[0] + self.bad_colour = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + self.hidden_colour = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VARIABLE_TEXT_IRRELEVANT) + self.keywidget = self.get_keywidget(variable, show_modes) + self.generate_valuewidget(variable) + self.is_inconsistent = False + if 'type' in variable.error: + self._set_inconsistent(self.valuewidget, variable) + self.errors = list(variable.error.keys()) + self.menuwidget = self.get_menuwidget(variable) + self.generate_labelwidget() + self.generate_contentwidget() + self.yoptions = Gtk.AttachOptions.FILL + self.force_signal_ids = [] + self.is_modified = False + for child_widget in self.get_children(): + setattr(child_widget, 'get_parent', lambda: self) + self.trigger_ignored = lambda v, b: b + self.get_parent = lambda: None + self.is_ignored = False + self.set_ignored() + self.update_status() + + def get_keywidget(self, variable, show_modes): + """Creates the keywidget attribute, based on the variable name. + + Loads 'tooltips' or hover-over text based on the variable metadata. + + """ + widget = rose.config_editor.keywidget.KeyWidget( + variable, self.var_ops, self.launch_help, self.update_status, + show_modes + ) + widget.show() + return widget + + def generate_labelwidget(self): + """Creates the label widget, a composite of key and menu widgets.""" + self.labelwidget = Gtk.VBox() + self.labelwidget.show() + self.labelwidget.set_ignored = self.keywidget.set_ignored + menu_offset = self.menuwidget.size_request()[1] / 2 + key_offset = self.keywidget.get_centre_height() / 2 + menu_vbox = Gtk.VBox() + menu_vbox.pack_start(self.menuwidget, expand=False, fill=False, + padding=max([(key_offset - menu_offset), 0])) + menu_vbox.show() + key_vbox = Gtk.VBox() + key_vbox.pack_start(self.keywidget, expand=False, fill=False, + padding=max([(menu_offset - key_offset) / 2, 0])) + key_vbox.show() + label_content_hbox = Gtk.HBox() + label_content_hbox.pack_start(menu_vbox, expand=False, fill=False) + label_content_hbox.pack_start(key_vbox, expand=False, fill=False) + label_content_hbox.show() + event_box = Gtk.EventBox() + event_box.show() + self.labelwidget.pack_start(label_content_hbox, expand=True, fill=True) + self.labelwidget.pack_start(event_box, expand=True, fill=True) + + def generate_contentwidget(self): + """Create the content widget, a vbox-packed valuewidget.""" + self.contentwidget = Gtk.VBox() + self.contentwidget.show() + content_event_box = Gtk.EventBox() + content_event_box.show() + self.contentwidget.pack_start( + self.valuewidget, expand=False, fill=False) + self.contentwidget.pack_start( + content_event_box, expand=True, fill=True) + + def _valuewidget_set_value(self, value): + # This is called by a valuewidget to change the variable value. + self.var_ops.set_var_value(self.variable, value) + self.update_status() + + def generate_valuewidget(self, variable, override_custom=False, + use_this_valuewidget=None): + """Creates the valuewidget attribute, based on value and metadata.""" + custom_arg = None + if (variable.metadata.get("type") == + rose.config_editor.FILE_TYPE_NORMAL): + use_this_valuewidget = (rose.config_editor. + valuewidget.source.SourceValueWidget) + custom_arg = self.var_ops + set_value = self._valuewidget_set_value + hook_object = rose.config_editor.valuewidget.ValueWidgetHook( + rose.config_editor.false_function, + self._get_focus) + metadata = copy.deepcopy(variable.metadata) + if use_this_valuewidget is not None: + self.valuewidget = use_this_valuewidget(variable.value, + metadata, + set_value, + hook_object, + arg_str=custom_arg) + elif (rose.config_editor.META_PROP_WIDGET in self.meta and + not override_custom): + w_val = self.meta[rose.config_editor.META_PROP_WIDGET] + info = w_val.split(None, 1) + if len(info) > 1: + widget_path, custom_arg = info + else: + widget_path, custom_arg = info[0], None + files = self.var_ops.get_ns_metadata_files(metadata["full_ns"]) + error_handler = lambda e: self.handle_bad_valuewidget( + str(e), variable, set_value) + widget = rose.resource.import_object(widget_path, + files, + error_handler) + if widget is None: + text = rose.config_editor.ERROR_IMPORT_CLASS.format(w_val) + self.handle_bad_valuewidget(text, variable, set_value) + try: + self.valuewidget = widget(variable.value, + metadata, + set_value, + hook_object, + custom_arg) + except Exception as exc: + self.handle_bad_valuewidget(str(exc), variable, set_value) + else: + widget_maker = rose.config_editor.valuewidget.chooser( + variable.value, variable.metadata, + variable.error) + self.valuewidget = widget_maker(variable.value, + metadata, set_value, + hook_object, custom_arg) + for child in self.valuewidget.get_children(): + child.connect('focus-in-event', self.handle_focus_in) + child.connect('focus-out-event', self.handle_focus_out) + if hasattr(child, 'get_children'): + for grandchild in child.get_children(): + grandchild.connect('focus-in-event', self.handle_focus_in) + grandchild.connect('focus-out-event', + self.handle_focus_out) + self.valuewidget.show() + + def handle_bad_valuewidget(self, error_info, variable, set_value): + """Handle a bad custom valuewidget import.""" + text = rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) + rose.reporter.Reporter()( + rose.config_editor.util.ImportWidgetError(text)) + self.generate_valuewidget(variable, override_custom=True) + + def handle_focus_in(self, widget, event): + widget._first_colour = widget.style.base[Gtk.StateType.NORMAL] + new_colour = rose.gtk.util.color_parse( + rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED) + widget.modify_base(Gtk.StateType.NORMAL, new_colour) + + def handle_focus_out(self, widget, event): + if hasattr(widget, "_first_colour"): + widget.modify_base(Gtk.StateType.NORMAL, widget._first_colour) + + def get_menuwidget(self, variable, menuclass=None): + """Create the menuwidget attribute, an option menu button.""" + if menuclass is None: + menuclass = rose.config_editor.menuwidget.MenuWidget + menuwidget = menuclass(variable, + self.var_ops, + lambda: self.remove_from(self.get_parent()), + self.update_status, + self.launch_help) + menuwidget.show() + return menuwidget + + def insert_into(self, container, x_info=None, y_info=None, + no_menuwidget=False): + """Inserts the child widgets of an instance into the 'container'. + + As PyGTK is not that introspective, we need arguments specifying where + the correct area within the widget is - in the case of Gtk.Table + instances, we need the number of columns and the row index. + These arguments are generically named x_info and y_info. + + """ + if not hasattr(container, 'num_removes'): + setattr(container, 'num_removes', 0) + if isinstance(container, Gtk.Table): + row_index = y_info + key_col = 0 + container.attach(self.labelwidget, + key_col, key_col + 1, + row_index, row_index + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.FILL) + container.attach(self.contentwidget, + key_col + 1, key_col + 2, + row_index, row_index + 1, + xpadding=5, + xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + yoptions=self.yoptions) + self.valuewidget.trigger_scroll = ( + lambda b, e: self.force_scroll(b, container)) + setattr(self, 'get_parent', lambda: container) + elif isinstance(container, Gtk.VBox): + container.pack_start(self.labelwidget, expand=False, fill=True, + padding=5) + container.pack_start(self.contentwidget, expand=True, fill=True, + padding=10) + self.valuewidget.trigger_scroll = ( + lambda b, e: self.force_scroll(b, container)) + setattr(self, 'get_parent', lambda: container) + + return container + + def force_scroll(self, widget=None, container=None): + """Adjusts a scrolled window to display the correct widget.""" + y_coordinate = None + if widget is not None: + y_coordinate = widget.get_allocation().y + scroll_container = container.get_parent() + if scroll_container is None: + return False + while not isinstance(scroll_container, Gtk.ScrolledWindow): + scroll_container = scroll_container.get_parent() + vadj = scroll_container.get_vadjustment() + if vadj.upper == 1.0 or y_coordinate == -1: + if not self.force_signal_ids: + self.force_signal_ids.append(vadj.connect_after( + 'changed', + lambda a: self.force_scroll(widget, container))) + else: + for handler_id in self.force_signal_ids: + vadj.handler_block(handler_id) + self.force_signal_ids = [] + vadj.connect('changed', rose.config_editor.false_function) + if y_coordinate is None: + vadj.upper = vadj.upper + 0.08 * vadj.page_size + vadj.set_value(vadj.upper - vadj.page_size) + return False + if y_coordinate == -1: # Bad allocation, don't scroll + return False + if not vadj.value < y_coordinate < vadj.value + 0.95 * vadj.page_size: + vadj.set_value(min(y_coordinate, vadj.upper - vadj.page_size)) + return False + + def remove_from(self, container): + """Removes the child widgets of an instance from the 'container'.""" + container.num_removes += 1 + self.var_ops.remove_var(self.variable) + if isinstance(container, Gtk.Table): + for widget_child in self.get_children(): + for child in container.get_children(): + if child == widget_child: + container.remove(widget_child) + widget_child.destroy() + return container + + def get_children(self): + """Method that returns child widgets - as in some gtk Objects.""" + return [self.labelwidget, self.contentwidget] + + def hide(self): + for widget in self.get_children(): + widget.hide() + + def show(self): + for widget in self.get_children(): + widget.show() + + def set_show_mode(self, show_mode, should_show_mode): + """Sets or unsets special displays for a variable.""" + self.keywidget.set_show_mode(show_mode, should_show_mode) + + def set_ignored(self): + """Sets or unsets a custom ignored state for the widgets.""" + ign_map = self.variable.ignored_reason + self.keywidget.set_ignored() + if ign_map != {}: + # Technically ignored, but could just be ignored by section. + self.is_ignored = True + if "'Ignore'" not in self.menuwidget.option_ui: + self.menuwidget.old_option_ui = self.menuwidget.option_ui + self.menuwidget.old_actions = self.menuwidget.actions + if list(ign_map.keys()) == [rose.variable.IGNORED_BY_SECTION]: + # Not ignored in itself, so give Ignore option. + if "'Enable'" in self.menuwidget.option_ui: + self.menuwidget.option_ui = re.sub( + "", + r"", + self.menuwidget.option_ui) + else: + # Ignored in itself, so needs Enable option. + self.menuwidget.option_ui = re.sub( + "", + r"", + self.menuwidget.option_ui) + self.update_status() + self.set_sensitive(False) + else: + # Enabled. + self.is_ignored = False + if "'Enable'" in self.menuwidget.option_ui: + self.menuwidget.option_ui = re.sub( + "", + r"", + self.menuwidget.option_ui) + self.update_status() + if not self.is_ghost: + self.set_sensitive(True) + + def update_status(self): + """Handles variable modified status.""" + self.set_modified(self.var_ops.is_var_modified(self.variable)) + self.keywidget.update_comment_display() + + def set_modified(self, is_modified=True): + """Applies or unsets a custom 'modified' state for the widgets.""" + if is_modified == self.is_modified: + return False + self.is_modified = is_modified + self.keywidget.set_modified(is_modified) + if not is_modified and isinstance(self.keywidget.entry, Gtk.Entry): + # This variable should now be displayed as a normal variable. + self.valuewidget.trigger_refresh(self.variable.metadata['id']) + + def set_sensitive(self, is_sensitive=True): + """Sets whether the widgets are grayed-out or 'insensitive'.""" + for widget in [self.keywidget, self.valuewidget]: + widget.set_sensitive(is_sensitive) + return False + + def grab_focus(self, focus_container=None, scroll_bottom=False, + index=None): + """Method similar to Gtk.Widget - get the keyboard focus.""" + if hasattr(self, 'valuewidget'): + self.valuewidget.grab_focus() + if (index is not None and + hasattr(self.valuewidget, 'set_focus_index')): + self.valuewidget.set_focus_index(index) + for child in self.valuewidget.get_children(): + if (Gtk.SENSITIVE & child.flags() and + Gtk.PARENT_SENSITIVE & child.flags()): + break + else: + if hasattr(self, 'menuwidget'): + self.menuwidget.get_children()[0].grab_focus() + if scroll_bottom and focus_container is not None: + self.force_scroll(None, container=focus_container) + if hasattr(self, 'keywidget') and self.key == '': + self.keywidget.grab_focus() + return False + + def get_focus_index(self): + """Get the current cursor position in the variable value string.""" + if (hasattr(self, "valuewidget") and + hasattr(self.valuewidget, "get_focus_index")): + return self.valuewidget.get_focus_index() + diff = difflib.SequenceMatcher(None, + self.variable.old_value, + self.variable.value) + # Return all end-of-block indicies for changed blocks + indicies = [x[4] for x in diff.get_opcodes() if x[0] != 'equal'] + if not indicies: + return None + return indicies[-1] + + def launch_help(self, url_mode=False): + """Launch a help dialog or a URL in a web browser.""" + if url_mode: + return self.var_ops.launch_url(self.variable) + if rose.META_PROP_HELP not in self.meta: + return + help_text = None + if self.show_modes.get( + rose.config_editor.SHOW_MODE_CUSTOM_HELP): + format_string = rose.config_editor.CUSTOM_FORMAT_HELP + help_text = rose.variable.expand_format_string( + format_string, self.variable) + if help_text is None: + help_text = self.meta[rose.META_PROP_HELP] + self._launch_help_dialog(help_text) + + def _launch_help_dialog(self, help_text): + """Launch a scrollable dialog for this variable's help text.""" + title = rose.config_editor.DIALOG_HELP_TITLE.format( + self.variable.metadata["id"]) + ns = self.variable.metadata["full_ns"] + search_function = lambda i: self.var_ops.search_for_var(ns, i) + rose.gtk.dialog.run_hyperlink_dialog( + Gtk.STOCK_DIALOG_INFO, help_text, title, search_function) + return False + + def _set_inconsistent(self, valuewidget, variable): + valuewidget.modify_base(Gtk.StateType.NORMAL, self.bad_colour) + self.is_inconsistent = True + widget_list = valuewidget.get_children() + while widget_list: + widget = widget_list.pop() + widget.modify_text(Gtk.StateType.NORMAL, self.bad_colour) + if hasattr(widget, 'set_inconsistent'): + widget.set_inconsistent(True) + if isinstance(widget, Gtk.RadioButton): + widget.set_active(False) + if (hasattr(widget, 'get_group') and + hasattr(widget.get_group(), 'set_inconsistent')): + widget.get_group().set_inconsistent(True) + if isinstance(widget, Gtk.Entry): + widget.modify_fg(Gtk.StateType.NORMAL, self.bad_colour) + if isinstance(widget, Gtk.SpinButton): + try: + v_value = float(variable.value) + w_value = float(widget.get_value()) + except (TypeError, ValueError): + widget.modify_text(Gtk.StateType.NORMAL, self.hidden_colour) + else: + if w_value != v_value: + widget.modify_text(Gtk.StateType.NORMAL, + self.hidden_colour) + if hasattr(widget, 'get_children'): + widget_list.extend(widget.get_children()) + elif hasattr(widget, 'get_child'): + widget_list.append(widget.get_child()) + + def _set_consistent(self, valuewidget, variable): + normal_style = Gtk.Style() + normal_base = normal_style.base[Gtk.StateType.NORMAL] + normal_fg = normal_style.fg[Gtk.StateType.NORMAL] + normal_text = normal_style.text[Gtk.StateType.NORMAL] + valuewidget.modify_base(Gtk.StateType.NORMAL, normal_base) + self.is_inconsistent = True + for widget in valuewidget.get_children(): + widget.modify_text(Gtk.StateType.NORMAL, normal_text) + if hasattr(widget, 'set_inconsistent'): + widget.set_inconsistent(False) + if isinstance(widget, Gtk.Entry): + widget.modify_fg(Gtk.StateType.NORMAL, normal_fg) + if (hasattr(widget, 'get_group') and + hasattr(widget.get_group(), 'set_inconsistent')): + widget.get_group().set_inconsistent(False) + + def _get_focus(self, widget_for_focus): + widget_for_focus.grab_focus() + self.valuewidget.trigger_scroll(widget_for_focus, None) + if isinstance(widget_for_focus, Gtk.Entry): + text_length = len(widget_for_focus.get_text()) + if text_length > 0: + widget_for_focus.set_position(text_length) + widget_for_focus.select_region(text_length, + text_length) + return False + + def needs_type_error_refresh(self): + """Check if self needs to be re-created on 'type' error.""" + if hasattr(self.valuewidget, "handle_type_error"): + return False + return True + + def type_error_refresh(self, variable): + """Handle a type error.""" + if rose.META_PROP_TYPE in variable.error: + self._set_inconsistent(self.valuewidget, variable) + else: + self._set_consistent(self.valuewidget, variable) + self.variable = variable + self.errors = list(variable.error.keys()) + self.valuewidget.handle_type_error(rose.META_PROP_TYPE in self.errors) + self.menuwidget.refresh(variable) + self.keywidget.refresh(variable) + + +class RowVariableWidget(VariableWidget): + + """This class generates a set of widgets for use as a row in a table.""" + + def __init__(self, *args, **kwargs): + self.length = kwargs.pop("length") + super(RowVariableWidget, self).__init__(*args, **kwargs) + + def generate_valuewidget(self, variable, override_custom=False): + """Creates the valuewidget attribute, based on value and metadata.""" + if (rose.META_PROP_LENGTH in variable.metadata or + isinstance(variable.metadata.get(rose.META_PROP_TYPE), list)): + use_this_valuewidget = self.make_row_valuewidget + else: + use_this_valuewidget = None + super(RowVariableWidget, self).generate_valuewidget( + variable, override_custom=override_custom, + use_this_valuewidget=use_this_valuewidget) + + def make_row_valuewidget(self, *args, **kwargs): + kwargs.update({"arg_str": str(self.length)}) + return row.RowArrayValueWidget(*args, **kwargs) diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py new file mode 100644 index 000000000..b2364a2d3 --- /dev/null +++ b/metomi/rose/config_editor/window.py @@ -0,0 +1,777 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import os +import re +import webbrowser + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +import rose.config +import rose.gtk.dialog +import rose.gtk.util +import rose.resource + + +REC_SPLIT_MACRO_TEXT = re.compile( + '(.{' + str(rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + + '})') + + +class MetadataTable(object): + """ + Creates a table from the provided list of paths appending it to the + provided parent. + The current state of the table can be obtained using '.paths'. + """ + def __init__(self, paths, parent): + self.paths = paths + self.parent = parent + self.previous = None + self.draw_table() + + def draw_table(self): + """ Draws the table. """ + # destroy previous table if present + if self.previous: + self.previous.destroy() + + # rows, cols + table = Gtk.Table(len(self.paths), 2) + + # table rows + for i, path in enumerate(self.paths): + label = Gtk.Label(label=path) + label.set_alignment(xalign=0., yalign=0.5) + # component, col_from, col_to, row_from, row_to + table.attach(label, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL, xpadding=15) + label.show() + button = Gtk.Button('Remove') + button.data = path + # component, col_from, col_to, row_from, row_to + table.attach(button, 1, 2, i, i + 1) + button.connect('clicked', self.remove_row) + button.show() + + # append table + self.parent.pack_start(table, True, True, 0) + self.parent.reorder_child(table, 2) + table.show() + + self.previous = table + + def remove_row(self, widget): + """ To be called upon 'remove' button press. """ + self.paths.remove(widget.data) + self.draw_table() + + def add_row(self, path): + """ Creates a new table row from the provided path (as a string). """ + self.paths.append(path) + self.draw_table() + + +class MainWindow(object): + + """Generate the main window and dialog handling for this example.""" + + def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, + nav_panel=None, status_bar=None, notebook=None, + page_change_func=rose.config_editor.false_function, + save_func=rose.config_editor.false_function): + self.window = Gtk.Window() + self.window.set_title(name + ' - ' + + rose.config_editor.LAUNCH_COMMAND) + self.util = rose.config_editor.util.Lookup() + self.window.set_icon(rose.gtk.util.get_icon()) + Gtk.window_set_default_icon_list(self.window.get_icon()) + self.window.set_default_size(*rose.config_editor.SIZE_WINDOW) + self.window.set_destroy_with_parent(False) + self.save_func = save_func + self.top_vbox = Gtk.VBox() + self.log_window = None # The stack viewer. + self.window.add(self.top_vbox) + # Load the menu bar + if menu is not None: + menu.show() + self.top_vbox.pack_start(menu, False, True, 0) + if accelerators is not None: + self.window.add_accel_group(accelerators) + if toolbar is not None: + toolbar.show() + self.top_vbox.pack_start(toolbar, False, True, 0) + # Load the nav_panel and notebook + for signal in ['switch-page', 'focus-tab', 'select-page', + 'change-current-page']: + notebook.connect_after(signal, page_change_func) + self.generate_main_hbox(nav_panel, notebook) + self.top_vbox.pack_start(self.main_hbox, True, True, 0) + self.top_vbox.pack_start(status_bar, expand=False, fill=False) + self.top_vbox.show() + self.window.show() + nav_panel.tree.columns_autosize() + nav_panel.grab_focus() + + def generate_main_hbox(self, nav_panel, notebook): + """Create the main container of the GUI window. + + This contains the tree panel and notebook. + + """ + self.main_hbox = Gtk.HPaned() + self.main_hbox.pack1(nav_panel, resize=False, shrink=False) + self.main_hbox.show() + self.main_hbox.pack2(notebook, resize=True, shrink=True) + self.main_hbox.show() + self.main_hbox.set_position(rose.config_editor.WIDTH_TREE_PANEL) + + def launch_about_dialog(self, somewidget=None): + """Create a dialog showing the 'About' information.""" + rose.gtk.dialog.run_about_dialog( + name=rose.config_editor.PROGRAM_NAME, + copyright_=rose.config_editor.COPYRIGHT, + logo_path="etc/images/rose-logo.png", + website=rose.config_editor.PROJECT_URL) + + def _reload_choices(self, liststore, top_name, add_choices): + liststore.clear() + for full_section_id in add_choices: + section_top_name, section_id = full_section_id.split(':', 1) + if section_top_name == top_name: + liststore.append([section_id]) + + def launch_add_dialog(self, names, add_choices, section_help): + """Launch a dialog asking for a section name.""" + add_dialog = Gtk.Dialog(title=rose.config_editor.DIALOG_TITLE_ADD, + parent=self.window, + buttons=(Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT)) + ok_button = add_dialog.action_area.get_children()[0] + config_label = Gtk.Label(label=rose.config_editor.DIALOG_BODY_ADD_CONFIG) + config_label.show() + label = Gtk.Label(label=rose.config_editor.DIALOG_BODY_ADD_SECTION) + label.show() + config_name_box = Gtk.ComboBoxText() + for name in names: + config_name_box.append_text(name.lstrip("/")) + config_name_box.show() + config_name_box.set_active(0) + section_box = Gtk.Entry() + if section_help is not None: + section_box.set_text(section_help) + section_completion = Gtk.EntryCompletion() + liststore = Gtk.ListStore(str) + section_completion.set_model(liststore) + section_box.set_completion(section_completion) + section_completion.set_text_column(0) + self._reload_choices(liststore, names[0], add_choices) + section_box.show() + config_name_box.connect( + "changed", + lambda c: self._reload_choices( + liststore, names[c.get_active()], add_choices)) + section_box.connect("activate", + lambda s: add_dialog.response(Gtk.ResponseType.OK)) + section_box.connect( + "changed", + lambda s: ok_button.set_sensitive(bool(s.get_text()))) + vbox = Gtk.VBox(spacing=10) + vbox.pack_start(config_label, expand=False, fill=False, padding=5) + vbox.pack_start(config_name_box, expand=False, fill=False, padding=5) + vbox.pack_start(label, expand=False, fill=False, padding=5) + vbox.pack_start(section_box, expand=False, fill=False, padding=5) + vbox.show() + hbox = Gtk.HBox(spacing=10) + hbox.pack_start(vbox, expand=True, fill=True, padding=10) + hbox.show() + add_dialog.vbox.pack_start(hbox, True, True, 0) + section_box.grab_focus() + section_box.set_position(-1) + section_completion.complete() + ok_button.set_sensitive(bool(section_box.get_text())) + response = add_dialog.run() + if response in [Gtk.ResponseType.OK, Gtk.ResponseType.YES, + Gtk.ResponseType.ACCEPT]: + config_name_entered = names[config_name_box.get_active()] + section_name_entered = section_box.get_text() + add_dialog.destroy() + return config_name_entered, section_name_entered + add_dialog.destroy() + return None, None + + def launch_exit_warning_dialog(self): + """Launch a 'really want to quit' dialog.""" + text = 'Save changes before closing?' + exit_dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.NONE, + message_format=text, + parent=self.window) + exit_dialog.add_buttons(Gtk.STOCK_NO, Gtk.ResponseType.REJECT, + Gtk.STOCK_CANCEL, Gtk.ResponseType.CLOSE, + Gtk.STOCK_YES, Gtk.ResponseType.ACCEPT) + exit_dialog.set_title(rose.config_editor.DIALOG_TITLE_SAVE_CHANGES) + exit_dialog.set_modal(True) + exit_dialog.set_keep_above(True) + exit_dialog.action_area.get_children()[1].grab_focus() + response = exit_dialog.run() + exit_dialog.destroy() + if response == Gtk.ResponseType.REJECT: + Gtk.main_quit() + elif response == Gtk.ResponseType.ACCEPT: + save_ok = self.save_func() + if save_ok: + Gtk.main_quit() + return False + + def launch_graph_dialog(self, name_section_dict): + """Launch a dialog asking for a config and section to graph. + + name_section_dict is a dictionary containing config names + as keys, and lists of available sections as values. + + """ + prefs = {} + return self._launch_choose_section_dialog( + name_section_dict, prefs, + rose.config_editor.DIALOG_TITLE_GRAPH, + rose.config_editor.DIALOG_BODY_GRAPH_CONFIG, + rose.config_editor.DIALOG_BODY_GRAPH_SECTION, + null_section_choice=True + ) + + def launch_help_dialog(self, somewidget=None): + """Launch a browser to open the help url.""" + webbrowser.open( + 'https://metomi.github.io/rose/doc/html/index.html', + new=True, + autoraise=True + ) + return False + + def launch_ignore_dialog(self, name_section_dict, prefs, is_ignored): + """Launch a dialog asking for a section name to ignore or enable. + + name_section_dict is a dictionary containing config names + as keys, and lists of available sections as values. + prefs is in the same format, but indicates preferred values. + is_ignored is a bool that controls whether this is an ignore + section dialog or an enable section dialog. + + """ + if is_ignored: + dialog_title = rose.config_editor.DIALOG_TITLE_IGNORE + else: + dialog_title = rose.config_editor.DIALOG_TITLE_ENABLE + config_title = rose.config_editor.DIALOG_BODY_IGNORE_ENABLE_CONFIG + if is_ignored: + section_title = rose.config_editor.DIALOG_BODY_IGNORE_SECTION + else: + section_title = rose.config_editor.DIALOG_BODY_ENABLE_SECTION + return self._launch_choose_section_dialog( + name_section_dict, prefs, + dialog_title, config_title, + section_title) + + def _launch_choose_section_dialog( + self, name_section_dict, prefs, dialog_title, config_title, + section_title, null_section_choice=False, do_target_section=False): + chooser_dialog = Gtk.Dialog( + title=dialog_title, + parent=self.window, + buttons=(Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT)) + config_label = Gtk.Label(label=config_title) + config_label.show() + section_label = Gtk.Label(label=section_title) + section_label.show() + config_name_box = Gtk.ComboBoxText() + name_keys = sorted(list(name_section_dict.keys())) + for k, name in enumerate(name_keys): + config_name_box.append_text(name) + if name in prefs: + config_name_box.set_active(k) + if config_name_box.get_active() == -1: + config_name_box.set_active(0) + config_name_box.show() + section_box = Gtk.VBox() + section_box.show() + null_section_checkbutton = Gtk.CheckButton( + rose.config_editor.DIALOG_LABEL_NULL_SECTION) + null_section_checkbutton.connect( + "toggled", + lambda b: section_box.set_sensitive(not b.get_active()) + ) + if null_section_choice: + null_section_checkbutton.show() + null_section_checkbutton.set_active(True) + index = config_name_box.get_active() + section_combo = self._reload_section_choices( + section_box, + name_section_dict[name_keys[index]], + prefs.get(name_keys[index], [])) + config_name_box.connect( + 'changed', + lambda c: self._reload_section_choices( + section_box, + name_section_dict[name_keys[c.get_active()]], + prefs.get(name_keys[c.get_active()], []))) + vbox = Gtk.VBox(spacing=rose.config_editor.SPACING_PAGE) + vbox.pack_start(config_label, expand=False, fill=False) + vbox.pack_start(config_name_box, expand=False, fill=False) + vbox.pack_start(section_label, expand=False, fill=False) + vbox.pack_start(null_section_checkbutton, expand=False, fill=False) + vbox.pack_start(section_box, expand=False, fill=False) + if do_target_section: + target_section_entry = Gtk.Entry() + self._reload_target_section_entry( + section_combo, target_section_entry, + name_keys[config_name_box.get_active()], name_section_dict + ) + section_combo.connect( + "changed", + lambda combo: self._reload_target_section_entry( + combo, target_section_entry, + name_keys[config_name_box.get_active()], + name_section_dict + ) + ) + target_section_entry.show() + vbox.pack_start(target_section_entry, expand=False, fill=False) + vbox.show() + hbox = Gtk.HBox() + hbox.pack_start(vbox, expand=True, fill=True, + padding=rose.config_editor.SPACING_PAGE) + hbox.show() + chooser_dialog.vbox.pack_start( + hbox, True, True, rose.config_editor.SPACING_PAGE) + section_box.grab_focus() + response = chooser_dialog.run() + if response in [Gtk.ResponseType.OK, Gtk.ResponseType.YES, + Gtk.ResponseType.ACCEPT]: + config_name_entered = name_keys[config_name_box.get_active()] + if null_section_checkbutton.get_active(): + chooser_dialog.destroy() + if do_target_section: + return config_name_entered, None, None + return config_name_entered, None + + for widget in section_box.get_children(): + if hasattr(widget, 'get_active'): + index = widget.get_active() + sections = name_section_dict[config_name_entered] + section_name = sections[index] + if do_target_section: + target_section_name = target_section_entry.get_text() + chooser_dialog.destroy() + if do_target_section: + return (config_name_entered, section_name, + target_section_name) + return config_name_entered, section_name + chooser_dialog.destroy() + if do_target_section: + return None, None, None + return None, None + + def _reload_section_choices(self, vbox, sections, prefs): + for child in vbox.get_children(): + vbox.remove(child) + sections.sort(rose.config.sort_settings) + section_chooser = Gtk.ComboBoxText() + for k, section in enumerate(sections): + section_chooser.append_text(section) + if section in prefs: + section_chooser.set_active(k) + if section_chooser.get_active() == -1 and sections: + section_chooser.set_active(0) + section_chooser.show() + vbox.pack_start(section_chooser, expand=False, fill=False) + return section_chooser + + def _reload_target_section_entry(self, section_combo_box, target_entry, + config_name_entered, name_section_dict): + index = section_combo_box.get_active() + sections = name_section_dict[config_name_entered] + section_name = sections[index] + target_entry.set_text(section_name) + + def launch_macro_changes_dialog( + self, config_name, macro_name, changes_list, mode="transform", + search_func=rose.config_editor.false_function): + """Launch a dialog explaining macro changes.""" + dialog = MacroChangesDialog(self.window, config_name, macro_name, + mode, search_func) + return dialog.display(changes_list) + + def launch_new_config_dialog(self, root_directory): + """Launch a dialog allowing naming of a new configuration.""" + existing_apps = os.listdir(root_directory) + checker_function = lambda t: t not in existing_apps + label = rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_NAME + ok_tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_NAME + err_tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_NAME_ERROR + dialog, container, name_entry = rose.gtk.dialog.get_naming_dialog( + label, checker_function, ok_tip_text, err_tip_text) + dialog.set_title(rose.config_editor.DIALOG_TITLE_CONFIG_CREATE) + meta_hbox = Gtk.HBox() + meta_label = Gtk.Label(label= + rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META) + meta_label.show() + meta_entry = Gtk.Entry() + tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_META + meta_entry.set_tooltip_text(tip_text) + meta_entry.connect( + "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT)) + meta_entry.show() + meta_hbox.pack_start(meta_label, expand=False, fill=False, + padding=rose.config_editor.SPACING_SUB_PAGE) + meta_hbox.pack_start(meta_entry, expand=False, fill=True, + padding=rose.config_editor.SPACING_SUB_PAGE) + meta_hbox.show() + container.pack_start(meta_hbox, expand=False, fill=True, + padding=rose.config_editor.SPACING_PAGE) + response = dialog.run() + name = None + meta = None + if name_entry.get_text(): + name = name_entry.get_text().strip().strip('/') + if meta_entry.get_text(): + meta = meta_entry.get_text().strip() + dialog.destroy() + if response == Gtk.ResponseType.ACCEPT: + return name, meta + return None, None + + def launch_open_dirname_dialog(self): + """Launch a FileChooserDialog and return a directory, or None.""" + open_dialog = Gtk.FileChooserDialog( + title=rose.config_editor.DIALOG_TITLE_OPEN, + action=Gtk.FileChooserAction.OPEN, + buttons=(Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK)) + open_dialog.set_transient_for(self.window) + open_dialog.set_icon(self.window.get_icon()) + open_dialog.set_default_response(Gtk.ResponseType.OK) + config_filter = Gtk.FileFilter() + config_filter.add_pattern(rose.TOP_CONFIG_NAME) + config_filter.add_pattern(rose.SUB_CONFIG_NAME) + config_filter.add_pattern(rose.INFO_CONFIG_NAME) + open_dialog.set_filter(config_filter) + response = open_dialog.run() + if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES]: + config_directory = os.path.dirname(open_dialog.get_filename()) + open_dialog.destroy() + return config_directory + open_dialog.destroy() + return None + + def launch_load_metadata_dialog(self): + """ Launches a dialoge for selecting a metadata path. """ + open_dialog = Gtk.FileChooserDialog( + title=rose.config_editor.DIALOG_TITLE_LOAD_METADATA, + action=Gtk.FileChooserAction.SELECT_FOLDER, + buttons=(Gtk.STOCK_CLOSE, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_ADD, + Gtk.ResponseType.OK)) + open_dialog.set_transient_for(self.window) + open_dialog.set_icon(self.window.get_icon()) + open_dialog.set_default_response(Gtk.ResponseType.OK) + response = open_dialog.run() + if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES]: + config_directory = open_dialog.get_filename() + open_dialog.destroy() + return config_directory + open_dialog.destroy() + return None + + def launch_metadata_manager(self, paths): + """ + Launches a dialogue where users may add or remove custom meta data + paths. + """ + dialog = Gtk.Dialog( + title=rose.config_editor.DIALOG_TITLE_MANAGE_METADATA, + buttons=(Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, + Gtk.ResponseType.OK) + ) + + # add description + label = Gtk.Label(label='Specify metadata paths to override the default ' + 'metadata.\n') + dialog.vbox.pack_start(label, True, True, 0) + label.show() + + # create table of paths + table = MetadataTable(paths, dialog.vbox) + + # create add path button + button = Gtk.Button('Add Path') + + def add_path(): + _path = self.launch_load_metadata_dialog() + if _path: + table.add_row(_path) + button.connect('clicked', lambda b: add_path()) + dialog.vbox.pack_start(button, True, True, 0) + button.show() + + # open the dialogue + response = dialog.run() + if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES]: + # if user clicked 'ok' + dialog.destroy() + return table.paths + else: + dialog.destroy() + return None + + def launch_prefs(self, somewidget=None): + """Launch a dialog explaining preferences.""" + text = rose.config_editor.DIALOG_LABEL_PREFERENCES + title = rose.config_editor.DIALOG_TITLE_PREFERENCES + rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_INFO, text, + title) + return False + + def launch_remove_dialog(self, name_section_dict, prefs): + """Launch a dialog asking for a section name to remove. + + name_section_dict is a dictionary containing config names + as keys, and lists of available sections as values. + prefs is in the same format, but indicates preferred values. + + """ + return self._launch_choose_section_dialog( + name_section_dict, prefs, + rose.config_editor.DIALOG_TITLE_REMOVE, + rose.config_editor.DIALOG_BODY_REMOVE_CONFIG, + rose.config_editor.DIALOG_BODY_REMOVE_SECTION + ) + + def launch_rename_dialog(self, name_section_dict, prefs): + """Launch a dialog asking for a section name to rename. + + name_section_dict is a dictionary containing config names + as keys, and lists of available sections as values. + prefs is in the same format, but indicates preferred values. + + """ + return self._launch_choose_section_dialog( + name_section_dict, prefs, + rose.config_editor.DIALOG_TITLE_RENAME, + rose.config_editor.DIALOG_BODY_RENAME_CONFIG, + rose.config_editor.DIALOG_BODY_RENAME_SECTION, + do_target_section=True + ) + + def launch_view_stack(self, undo_stack, redo_stack, undo_func): + """Load a view of the stack.""" + self.log_window = rose.config_editor.stack.StackViewer( + undo_stack, redo_stack, undo_func) + self.log_window.set_transient_for(self.window) + + +class MacroChangesDialog(Gtk.Dialog): + + """Class to hold a dialog summarising macro results.""" + + COLUMNS = ["Section", "Option", "Type", "Value", "Info"] + MODE_COLOURS = {"transform": rose.config_editor.COLOUR_MACRO_CHANGED, + "validate": rose.config_editor.COLOUR_MACRO_ERROR, + "warn": rose.config_editor.COLOUR_MACRO_WARNING} + MODE_TEXT = {"transform": rose.config_editor.DIALOG_TEXT_MACRO_CHANGED, + "validate": rose.config_editor.DIALOG_TEXT_MACRO_ERROR, + "warn": rose.config_editor.DIALOG_TEXT_MACRO_WARNING} + + def __init__(self, window, config_name, macro_name, mode, search_func): + self.util = rose.config_editor.util.Lookup() + self.short_config_name = config_name.rstrip('/').split('/')[-1] + self.top_config_name = config_name.lstrip('/').split('/')[0] + self.short_macro_name = macro_name.split('.')[-1] + self.for_transform = (mode == "transform") + self.for_validate = (mode == "validate") + self.macro_name = macro_name + self.mode = mode + self.search_func = search_func + if self.for_validate: + title = rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE + button_list = [Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT] + else: + title = rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM + button_list = [Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT] + title = title.format(self.short_macro_name, self.short_config_name) + button_list = tuple(button_list) + super(MacroChangesDialog, self).__init__(buttons=button_list, + parent=window) + if not self.for_transform: + self.set_modal(False) + self.set_title(title.format(macro_name)) + self.label = Gtk.Label() + self.label.show() + if self.for_validate: + stock_id = Gtk.STOCK_DIALOG_WARNING + else: + stock_id = Gtk.STOCK_CONVERT + image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.LARGE_TOOLBAR) + image.show() + hbox = Gtk.HBox() + hbox.pack_start(image, expand=False, fill=False, + padding=rose.config_editor.SPACING_PAGE) + hbox.pack_start(self.label, expand=False, fill=False, + padding=rose.config_editor.SPACING_PAGE) + hbox.show() + self.treewindow = Gtk.ScrolledWindow() + self.treewindow.show() + self.treewindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + self.treeview = rose.gtk.util.TooltipTreeView( + get_tooltip_func=self._get_tooltip) + self.treeview.show() + self.treemodel = Gtk.TreeStore(str, str, str, str, str) + + self.treeview.set_model(self.treemodel) + for i, title in enumerate(self.COLUMNS): + column = Gtk.TreeViewColumn() + column.set_title(title) + cell = Gtk.CellRendererText() + if i == len(self.COLUMNS) - 1: + column.pack_start(cell, True, True, 0) + else: + column.pack_start(cell, False, True, 0) + if title == "Type": + column.set_cell_data_func(cell, self._set_type_markup, i) + else: + column.set_cell_data_func(cell, self._set_markup, i) + self.treeview.append_column(column) + + self.treeview.connect( + "row-activated", self._handle_treeview_activation) + self.treewindow.add(self.treeview) + self.vbox.pack_end(self.treewindow, expand=True, fill=True, + padding=rose.config_editor.SPACING_PAGE) + self.vbox.pack_end(hbox, expand=False, fill=True, + padding=rose.config_editor.SPACING_PAGE) + self.set_focus(self.action_area.get_children()[0]) + + def display(self, changes): + if not changes: + # Shortcut, no changes. + if self.for_validate: + title = rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE_NONE + text = rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_NONE + else: + title = rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM_NONE + text = rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_NONE + title = title.format(self.short_macro_name) + text = rose.gtk.util.safe_str(text) + return rose.gtk.dialog.run_dialog( + rose.gtk.dialog.DIALOG_TYPE_INFO, text, title) + if self.for_validate: + text = rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_ISSUES + else: + text = rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_CHANGES + nums_is_warning = {True: 0, False: 0} + for item in changes: + nums_is_warning[item.is_warning] += 1 + text = text.format(self.short_macro_name, self.short_config_name, + nums_is_warning[False]) + if nums_is_warning[True]: + extra_text = rose.config_editor.DIALOG_LABEL_MACRO_WARN_ISSUES + text = (text.rstrip() + " " + + extra_text.format(nums_is_warning[True])) + self.label.set_markup(text) + changes.sort(lambda x, y: cmp(x.option, y.option)) + changes.sort(lambda x, y: cmp(x.section, y.section)) + changes.sort(lambda x, y: cmp(x.is_warning, y.is_warning)) + last_section = None + last_section_iter = None + for item in changes: + item_mode = self.mode + if item.is_warning: + item_mode = "warn" + item_att_list = [item.section, item.option, item_mode, + item.value, item.info] + if item.section == last_section: + self.treemodel.append(last_section_iter, item_att_list) + else: + sect_att_list = [item.section, None, None, None, None] + last_section_iter = self.treemodel.append(None, sect_att_list) + last_section = item.section + self.treemodel.append(last_section_iter, item_att_list) + self.treeview.expand_all() + max_size = rose.config_editor.SIZE_MACRO_DIALOG_MAX + my_size = self.size_request() + new_size = [-1, -1] + for i in [0, 1]: + new_size[i] = min([my_size[i], max_size[i]]) + self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.set_default_size(*new_size) + if self.for_transform: + response = self.run() + self.destroy() + return (response == Gtk.ResponseType.ACCEPT) + else: + self.show() + self.action_area.get_children()[0].connect( + "clicked", lambda b: self.destroy()) + + def _get_tooltip(self, view, row_iter, col_index, tip): + tip.set_text(view.get_model().get_value(row_iter, col_index)) + return True + + def _set_type_markup(self, column, cell, model, r_iter, col_index): + macro_mode = model.get_value(r_iter, col_index) + if macro_mode is None: + cell.set_property("markup", None) + else: + cell.set_property("markup", self._get_type_markup(macro_mode)) + + def _get_type_markup(self, macro_mode): + colour = self.MODE_COLOURS[macro_mode] + text = self.MODE_TEXT[macro_mode] + return '{1}'.format(colour, text) + + def _set_markup(self, column, cell, model, r_iter, col_index): + text = model.get_value(r_iter, col_index) + if text is None: + cell.set_property("markup", None) + else: + cell.set_property("markup", rose.gtk.util.safe_str(text)) + if col_index == 0: + cell.set_property("visible", (len(model.get_path(r_iter)) == 1)) + + def _handle_treeview_activation(self, view, path, column): + r_iter = view.get_model().get_iter(path) + section = view.get_model().get_value(r_iter, 0) + option = view.get_model().get_value(r_iter, 1) + id_ = self.util.get_id_from_section_option(section, option) + self.search_func(id_) From 83825d03a17d39ebea2442971fad5d9bf9920e98 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Fri, 2 Aug 2024 16:12:37 +0100 Subject: [PATCH 03/42] Convert imports and usage from rose. to metomi.rose. --- .../app/01-types/meta/rose-meta.conf | 4 +- metomi/rose/config_editor/__init__.py | 40 +- metomi/rose/config_editor/data.py | 356 +++++------ metomi/rose/config_editor/data_helper.py | 56 +- metomi/rose/config_editor/keywidget.py | 192 +++--- metomi/rose/config_editor/main.py | 562 +++++++++--------- metomi/rose/config_editor/menu.py | 344 +++++------ metomi/rose/config_editor/menuwidget.py | 90 +-- metomi/rose/config_editor/nav_controller.py | 12 +- metomi/rose/config_editor/nav_panel.py | 60 +- metomi/rose/config_editor/nav_panel_menu.py | 96 +-- metomi/rose/config_editor/ops/group.py | 72 +-- metomi/rose/config_editor/ops/section.py | 116 ++-- metomi/rose/config_editor/ops/variable.py | 136 ++--- metomi/rose/config_editor/page.py | 200 +++---- metomi/rose/config_editor/pagewidget/table.py | 56 +- .../config_editor/panelwidget/filesystem.py | 24 +- .../config_editor/panelwidget/summary_data.py | 106 ++-- .../config_editor/plugin/um/widget/stash.py | 72 +-- .../plugin/um/widget/stash_add.py | 58 +- metomi/rose/config_editor/stack.py | 54 +- metomi/rose/config_editor/status.py | 70 +-- metomi/rose/config_editor/updater.py | 76 +-- .../rose/config_editor/upgrade_controller.py | 52 +- metomi/rose/config_editor/util.py | 40 +- .../config_editor/valuewidget/__init__.py | 12 +- .../config_editor/valuewidget/boolradio.py | 20 +- .../config_editor/valuewidget/booltoggle.py | 26 +- .../config_editor/valuewidget/character.py | 16 +- .../rose/config_editor/valuewidget/choice.py | 36 +- .../config_editor/valuewidget/combobox.py | 8 +- .../rose/config_editor/valuewidget/files.py | 22 +- .../rose/config_editor/valuewidget/format.py | 4 +- .../rose/config_editor/valuewidget/intspin.py | 4 +- .../config_editor/valuewidget/radiobuttons.py | 6 +- .../rose/config_editor/valuewidget/source.py | 32 +- metomi/rose/config_editor/valuewidget/text.py | 24 +- .../config_editor/valuewidget/valuehints.py | 8 +- metomi/rose/config_editor/variable.py | 86 +-- metomi/rose/config_editor/window.py | 176 +++--- .../rose-meta/rose-suite-info/rose-meta.conf | 2 +- metomi/rose/resource.py | 2 +- sphinx/api/configuration/metadata.rst | 4 +- 43 files changed, 1716 insertions(+), 1716 deletions(-) diff --git a/demo/rose-config-edit/demo_meta/app/01-types/meta/rose-meta.conf b/demo/rose-config-edit/demo_meta/app/01-types/meta/rose-meta.conf index aae0b7800..7640ced68 100644 --- a/demo/rose-config-edit/demo_meta/app/01-types/meta/rose-meta.conf +++ b/demo/rose-config-edit/demo_meta/app/01-types/meta/rose-meta.conf @@ -177,7 +177,7 @@ values=1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 description=A variable with 5 values (& value-titles) using radiobuttons value-titles=A title, B title, C title, D title, E title values='a', 'b', 'c', 'd', 'e' -widget[rose-config-edit]=rose.config_editor.valuewidget.radiobuttons.RadioButtonsValueWidget +widget[rose-config-edit]=metomi.rose.config_editor.valuewidget.radiobuttons.RadioButtonsValueWidget [namelist:nl2] duplicate=true @@ -194,7 +194,7 @@ values=4 [namelist:table_nl] description=A page containing a custom table layout -widget[rose-config-edit]=rose.config_editor.pagewidget.table.PageArrayTable +widget[rose-config-edit]=metomi.rose.config_editor.pagewidget.table.PageArrayTable [namelist:table_nl=my_boolean_array] description=A boolean array of length 5 diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index 2c91e44e6..28e9ff788 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -23,14 +23,14 @@ To override constants at runtime, place a section: -[rose-config-edit] +[metomi.rose.config-edit] in your site or user configuration file for Rose, convert the name of the constants to lowercase, and place constant=value lines in the section. For example, to override the "ACCEL_HELP_GUI" constant, you could put the following in your site or user configuration: -[rose-config-edit] +[metomi.rose.config-edit] accel_help_gui="H" The values you enter will be cast by Python's ast.literal_eval, so: @@ -47,8 +47,8 @@ 's/^# \(.*\)\n\(^[^#].*\) = \(.*\)/'\ '\

\2\E=\3\<\/h4\>\\1\<\/p\>\n/p;' | sort -Use this text to update the doc/etc/rose-rug-config-edit/rose.conf.html -text, remembering to add the [rose-config-edit] section. +Use this text to update the doc/etc/metomi.rose.rug-config-edit/metomi.rose.conf.html +text, remembering to add the [metomi.rose.config-edit] section. """ @@ -56,7 +56,7 @@ import os import sys -from rose.resource import ResourceLocator +from metomi.rose.resource import ResourceLocator # Accelerators # Keyboard shortcut mappings. @@ -276,7 +276,7 @@ ERROR_METADATA_CHECKER_TEXT = ( "{0} problem(s) found in metadata at {1}.\n" + "Some functionality has been switched off.\n\n" + - "Run rose metadata-check for more info.") + "Run metomi.rose.metadata-check for more info.") ERROR_MIN_PYGTK_VERSION = "Requires PyGTK version {0}, found {1}." ERROR_MIN_PYGTK_VERSION_TITLE = "Need later PyGTK version to run" ERROR_NO_OUTPUT = "No output found for {0}" @@ -429,7 +429,7 @@ STATUS_BAR_CONSOLE_CATEGORY_ERROR = "Error" STATUS_BAR_CONSOLE_CATEGORY_INFO = "Info" STATUS_BAR_MESSAGE_LIMIT = 1000 -STATUS_BAR_VERBOSITY = 0 # Compare with rose.reporter.Reporter. +STATUS_BAR_VERBOSITY = 0 # Compare with metomi.rose.reporter.Reporter. # Stack action names and presentation STACK_GROUP_ADD = "Add" @@ -534,7 +534,7 @@ DIALOG_LABEL_UPGRADE = ( "Click Upgrade Version cells to change target versions.") DIALOG_LABEL_UPGRADE_ALL = "Populate all possible versions" -DIALOG_TIP_SUITE_RUN_HELP = "Read the help for rose suite-run" +DIALOG_TIP_SUITE_RUN_HELP = "Read the help for metomi.rose.suite-run" DIALOG_TEXT_MACRO_CHANGED = "changed" DIALOG_TEXT_MACRO_ERROR = "error" DIALOG_TEXT_MACRO_WARNING = "warning" @@ -553,7 +553,7 @@ DIALOG_TITLE_EDIT_COMMENTS = "Edit comments for {0}" DIALOG_TITLE_ENABLE = "Enable section" DIALOG_TITLE_ERROR = "Error" -DIALOG_TITLE_GRAPH = "rose metadata-graph" +DIALOG_TITLE_GRAPH = "metomi.rose.metadata-graph" DIALOG_TITLE_IGNORE = "Ignore section" DIALOG_TITLE_INFO = "Information" DIALOG_TITLE_OPEN = "Open configuration" @@ -705,22 +705,22 @@ # Relevant metadata properties -META_PROP_WIDGET = "widget[rose-config-edit]" -META_PROP_WIDGET_SUB_NS = "widget[rose-config-edit:sub-ns]" +META_PROP_WIDGET = "widget[metomi.rose.config-edit]" +META_PROP_WIDGET_SUB_NS = "widget[metomi.rose.config-edit:sub-ns]" # Miscellaneous COPYRIGHT = ( "Copyright (C) 2012-2020 British Crown (Met Office) & Contributors.") -HELP_FILE = "rose-rug-config-edit.html" -LAUNCH_COMMAND = "rose config-edit" -LAUNCH_COMMAND_CONFIG = "rose config-edit -C" -LAUNCH_COMMAND_GRAPH = "rose metadata-graph -C" -LAUNCH_SUITE_RUN = "rose suite-run" -LAUNCH_SUITE_RUN_HELP = "rose help suite-run" +HELP_FILE = "metomi.rose.rug-config-edit.html" +LAUNCH_COMMAND = "metomi.rose.config-edit" +LAUNCH_COMMAND_CONFIG = "metomi.rose.config-edit -C" +LAUNCH_COMMAND_GRAPH = "metomi.rose.metadata-graph -C" +LAUNCH_SUITE_RUN = "metomi.rose.suite-run" +LAUNCH_SUITE_RUN_HELP = "metomi.rose.help suite-run" MAX_APPS_THRESHOLD = 10 MIN_PYGTK_VERSION = (2, 12, 0) -PROGRAM_NAME = "rose edit" -PROJECT_URL = "http://github.com/metomi/rose/" +PROGRAM_NAME = "metomi.rose.edit" +PROJECT_URL = "http://github.com/metomi/metomi.rose." UNTITLED_NAME = "Untitled" VAR_ID_IN_CONFIG = "Variable id {0} from the configuration {1}" @@ -767,4 +767,4 @@ def load_override_config(sections, my_globals=None): my_globals[name] = cast_value -load_override_config(["rose-config-edit"]) +load_override_config(["metomi.rose.config-edit"]) diff --git a/metomi/rose/config_editor/data.py b/metomi/rose/config_editor/data.py index 89dd49088..a297d2366 100644 --- a/metomi/rose/config_editor/data.py +++ b/metomi/rose/config_editor/data.py @@ -19,8 +19,8 @@ # ----------------------------------------------------------------------------- """This module contains: -VarData -- class to store rose.variable.Variable instances -SectData -- class to store rose.section.Section instances +VarData -- class to store metomi.rose.variable.Variable instances +SectData -- class to store metomi.rose.section.Section instances ConfigData -- class to store and process a directory into internal data structures ConfigDataManager -- class to load and process objects in ConfigData @@ -34,19 +34,19 @@ import re import sys -import rose.config -import rose.config_editor.data_helper -import rose.config_tree -import rose.gtk.dialog -import rose.macro -import rose.metadata_check -import rose.resource -import rose.section -import rose.macros.trigger -import rose.variable +import metomi.rose.config +import metomi.rose.config_editor.data_helper +import metomi.rose.config_tree +import metomi.rose.gtk.dialog +import metomi.rose.macro +import metomi.rose.metadata_check +import metomi.rose.resource +import metomi.rose.section +import metomi.rose.macros.trigger +import metomi.rose.variable -REC_NS_SECTION = re.compile(r"^(" + rose.META_PROP_NS + rose.CONFIG_DELIMITER + +REC_NS_SECTION = re.compile(r"^(" + metomi.rose.META_PROP_NS + metomi.rose.CONFIG_DELIMITER + r")(.*)$") @@ -91,7 +91,7 @@ def get_all(self, save=False, skip_latent=False, skip_real=False): def get_var(self, section, option, save=False, skip_latent=False): """Return the variable specified by section, option.""" - var_id = section + rose.CONFIG_DELIMITER + option + var_id = section + metomi.rose.CONFIG_DELIMITER + option if save: nodes = [self.save, self.latent_save] else: @@ -175,13 +175,13 @@ def __init__(self, util, reporter, page_ns_show_modes, no_warn=None): """Load the root configuration and all its sub-configurations.""" self.util = util - self.helper = rose.config_editor.data_helper.ConfigDataHelper( + self.helper = metomi.rose.config_editor.data_helper.ConfigDataHelper( self, util) self.reporter = reporter self.page_ns_show_modes = page_ns_show_modes self.reload_ns_tree_func = reload_ns_tree_func self.config = {} # Stores configuration name: object - self._builtin_value_macro = rose.macros.value.ValueChecker() # value + self._builtin_value_macro = metomi.rose.macros.value.ValueChecker() # value self.builtin_macros = {} # Stores other Rose built-in macro instances self._bad_meta_dir_paths = [] # Stores flawed metadata directories. self.trigger = {} # Stores trigger macro instances per configuration @@ -191,7 +191,7 @@ def __init__(self, util, reporter, page_ns_show_modes, self.namespace_cached_statuses = { 'latent': {}, 'ignored': {}} # Caches ns statuses self._config_section_namespace_map = {} # Store section namespaces - self.locator = rose.resource.ResourceLocator(paths=sys.path) + self.locator = metomi.rose.resource.ResourceLocator(paths=sys.path) if opt_meta_paths is None: self.opt_meta_paths = [] else: @@ -210,7 +210,7 @@ def load(self, top_level_directory, config_obj_dict, config_obj_type_dict = {} if top_level_directory is not None: for filename in os.listdir(top_level_directory): - if filename in [rose.TOP_CONFIG_NAME, rose.SUB_CONFIG_NAME]: + if filename in [metomi.rose.TOP_CONFIG_NAME, metomi.rose.SUB_CONFIG_NAME]: self.load_top_config(top_level_directory, load_all_apps=load_all_apps, load_no_apps=load_no_apps, @@ -237,11 +237,11 @@ def load_top_config(self, top_level_directory, preview=False, self.app_count = 0 if top_level_directory is None: - self.top_level_name = rose.config_editor.UNTITLED_NAME + self.top_level_name = metomi.rose.config_editor.UNTITLED_NAME else: self.top_level_name = os.path.basename(top_level_directory) config_container_dir = os.path.join(top_level_directory, - rose.SUB_CONFIGS_DIR) + metomi.rose.SUB_CONFIGS_DIR) if os.path.isdir(config_container_dir): sub_contents = sorted(os.listdir(config_container_dir)) @@ -257,7 +257,7 @@ def load_top_config(self, top_level_directory, preview=False, self.app_count += 1 if (self.app_count > - rose.config_editor.MAX_APPS_THRESHOLD): + metomi.rose.config_editor.MAX_APPS_THRESHOLD): preview = True for config_dir in sub_contents: @@ -272,12 +272,12 @@ def load_top_config(self, top_level_directory, preview=False, def load_info_config(self, config_directory): """Load any information (discovery) config.""" disc_path = os.path.join(config_directory, - rose.INFO_CONFIG_NAME) + metomi.rose.INFO_CONFIG_NAME) if os.path.isfile(disc_path): config_obj = self.load_config_file(disc_path)[0] self.load_config(config_name="/" + self.top_level_name + "-info", config=config_obj, - config_type=rose.INFO_CONFIG_NAME) + config_type=metomi.rose.INFO_CONFIG_NAME) def load_config(self, config_directory=None, config_name=None, config=None, @@ -291,7 +291,7 @@ def load_config(self, config_directory=None, s_config = copy.deepcopy(config) if not skip_load_event: self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_CONFIG.format( + metomi.rose.config_editor.EVENT_LOAD_CONFIG.format( name.lstrip("/"))) else: config_directory = config_directory.rstrip("/") @@ -299,50 +299,50 @@ def load_config(self, config_directory=None, # One of the sub configurations head, tail = os.path.split(config_directory) name = '' - while tail != rose.SUB_CONFIGS_DIR: + while tail != metomi.rose.SUB_CONFIGS_DIR: name = "/" + os.path.join(tail, name).rstrip('/') head, tail = os.path.split(head) name = "/" + name.lstrip("/") - config_type = rose.SUB_CONFIG_NAME - elif rose.TOP_CONFIG_NAME not in os.listdir(config_directory): + config_type = metomi.rose.SUB_CONFIG_NAME + elif metomi.rose.TOP_CONFIG_NAME not in os.listdir(config_directory): # Just editing a single sub configuration, not a suite name = "/" + self.top_level_name - config_type = rose.SUB_CONFIG_NAME + config_type = metomi.rose.SUB_CONFIG_NAME else: # Make sure we also load any discovery (info) configuration self.load_info_config(config_directory) # A suite configuration name = "/" + self.top_level_name + "-conf" - config_type = rose.TOP_CONFIG_NAME + config_type = metomi.rose.TOP_CONFIG_NAME if not skip_load_event: self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_CONFIG.format( + metomi.rose.config_editor.EVENT_LOAD_CONFIG.format( name.lstrip("/"))) - config_path = os.path.join(config_directory, rose.SUB_CONFIG_NAME) + config_path = os.path.join(config_directory, metomi.rose.SUB_CONFIG_NAME) if not os.path.isfile(config_path): if (os.path.abspath(config_directory) == os.path.abspath(self.top_level_directory)): config_path = os.path.join(config_directory, - rose.TOP_CONFIG_NAME) - config_type = rose.TOP_CONFIG_NAME + metomi.rose.TOP_CONFIG_NAME) + config_type = metomi.rose.TOP_CONFIG_NAME else: - text = rose.config_editor.ERROR_NOT_FOUND.format( + text = metomi.rose.config_editor.ERROR_NOT_FOUND.format( config_path) - title = rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + title = metomi.rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) sys.exit(2) if config_directory != self.top_level_directory and preview: # Load with empty ConfigNodes for initial app access. - config = rose.config.ConfigNode() - s_config = rose.config.ConfigNode() + config = metomi.rose.config.ConfigNode() + s_config = metomi.rose.config.ConfigNode() else: config, s_config = self.load_config_file(config_path) if config_directory != self.top_level_directory and preview: - meta_config_tree = rose.config_tree.ConfigTree() + meta_config_tree = metomi.rose.config_tree.ConfigTree() elif metadata_off: meta_config_tree = self.load_meta_config_tree( config_type=config_type, @@ -355,15 +355,15 @@ def load_config(self, config_directory=None, opt_meta_paths=self.opt_meta_paths ) except IOError as exc: - rose.gtk.dialog.run_exception_dialog(exc) - meta_config_tree = rose.config_tree.ConfigTree() + metomi.rose.gtk.dialog.run_exception_dialog(exc) + meta_config_tree = metomi.rose.config_tree.ConfigTree() meta_config = meta_config_tree.node opt_conf_lookup = self.load_optional_configs(config_directory) macro_module_prefix = self.helper.get_macro_module_prefix(name) meta_files = self.load_meta_files(meta_config_tree) - macros = rose.macro.load_meta_macro_modules( + macros = metomi.rose.macro.load_meta_macro_modules( meta_files, module_prefix=macro_module_prefix) meta_id = self.helper.get_config_meta_flag( name, from_this_config_obj=config) @@ -388,7 +388,7 @@ def load_config(self, config_directory=None, if not skip_load_event: self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_METADATA.format( + metomi.rose.config_editor.EVENT_LOAD_METADATA.format( name.lstrip("/"))) # Process namespaces and ignored statuses. self.load_node_namespaces(name) @@ -399,21 +399,21 @@ def load_config(self, config_directory=None, self.reload_ns_tree_func() def load_config_file(self, config_path): - """Return two copies of the rose.config.ConfigNode at config_path.""" + """Return two copies of the metomi.rose.config.ConfigNode at config_path.""" try: - config = rose.config.load(config_path) - except rose.config.ConfigSyntaxError as exc: - text = rose.config_editor.ERROR_LOAD_SYNTAX.format( + config = metomi.rose.config.load(config_path) + except metomi.rose.config.ConfigSyntaxError as exc: + text = metomi.rose.config_editor.ERROR_LOAD_SYNTAX.format( config_path, exc) - title = rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + title = metomi.rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) sys.exit(2) else: - master_config = rose.config.load(config_path) - rose.macro.standard_format_config(config) - rose.macro.standard_format_config(master_config) + master_config = metomi.rose.config.load(config_path) + metomi.rose.macro.standard_format_config(config) + metomi.rose.macro.standard_format_config(master_config) return config, master_config def load_optional_configs(self, config_directory): @@ -421,43 +421,43 @@ def load_optional_configs(self, config_directory): opt_conf_lookup = {} if config_directory is None: return opt_conf_lookup - opt_dir = os.path.join(config_directory, rose.config.OPT_CONFIG_DIR) + opt_dir = os.path.join(config_directory, metomi.rose.config.OPT_CONFIG_DIR) if not os.path.isdir(opt_dir): return opt_conf_lookup opt_exceptions = {} - opt_glob = os.path.join(opt_dir, rose.GLOB_OPT_CONFIG_FILE) + opt_glob = os.path.join(opt_dir, metomi.rose.GLOB_OPT_CONFIG_FILE) for path in glob.glob(opt_glob): if os.access(path, os.F_OK | os.R_OK): filename = os.path.basename(path) # filename is a null string if path is to a directory. - result = re.match(rose.RE_OPT_CONFIG_FILE, filename) + result = re.match(metomi.rose.RE_OPT_CONFIG_FILE, filename) if not result: continue name = result.group(1) try: - opt_config = rose.config.load(path) + opt_config = metomi.rose.config.load(path) except Exception as exc: opt_exceptions.update({path: exc}) continue opt_conf_lookup.update({name: opt_config}) if opt_exceptions: err_text = "" - err_format = rose.config_editor.ERROR_LOAD_OPT_CONFS_FORMAT + err_format = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS_FORMAT for path, exc in sorted(opt_exceptions.items()): err_text += err_format.format(path, type(exc).__name__, exc) err_text = err_text.rstrip() - text = rose.config_editor.ERROR_LOAD_OPT_CONFS.format(err_text) - title = rose.config_editor.ERROR_LOAD_OPT_CONFS_TITLE - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS.format(err_text) + title = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS_TITLE + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title=title, modal=False) return opt_conf_lookup def load_builtin_macros(self, config_name): """Load Rose builtin macros.""" self.builtin_macros[config_name] = { - rose.META_PROP_COMPULSORY: - rose.macros.compulsory.CompulsoryChecker(), - rose.META_PROP_TYPE: + metomi.rose.META_PROP_COMPULSORY: + metomi.rose.macros.compulsory.CompulsoryChecker(), + metomi.rose.META_PROP_TYPE: self._builtin_value_macro} def load_sections_from_config(self, config_name, save=False): @@ -478,36 +478,36 @@ def load_sections_from_config(self, config_name, save=False): continue meta_data = self.helper.get_metadata_for_config_id( "", config_name) - sect_map.update({"": rose.section.Section("", [section], + sect_map.update({"": metomi.rose.section.Section("", [section], meta_data)}) real_sect_ids.append("") continue meta_data = self.helper.get_metadata_for_config_id(section, config_name) options = list(node.value.keys()) - sect_map.update({section: rose.section.Section(section, options, + sect_map.update({section: metomi.rose.section.Section(section, options, meta_data)}) sect_map[section].comments = list(node.comments) real_sect_ids.append(section) real_sect_basic_ids.extend( - [rose.macro.REC_ID_STRIP_DUPL.sub("", section), - rose.macro.REC_ID_STRIP.sub("", section)] + [metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section), + metomi.rose.macro.REC_ID_STRIP.sub("", section)] ) if node.is_ignored(): reason = {} - if node.state == rose.config.ConfigNode.STATE_SYST_IGNORED: - reason = {rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_CONFIG} + if node.state == metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: + reason = {metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG} elif (node.state == - rose.config.ConfigNode.STATE_USER_IGNORED): - reason = {rose.variable.IGNORED_BY_USER: - rose.config_editor.IGNORED_STATUS_CONFIG} + metomi.rose.config.ConfigNode.STATE_USER_IGNORED): + reason = {metomi.rose.variable.IGNORED_BY_USER: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG} sect_map[section].ignored_reason.update(reason) if "" not in sect_map: # This always exists for a configuration. meta_data = self.helper.get_metadata_for_config_id("", config_name) - sect_map.update({"": rose.section.Section("", [], meta_data)}) + sect_map.update({"": metomi.rose.section.Section("", [], meta_data)}) real_sect_ids.append("") for setting_id, sect_node in list(meta_config.value.items()): if sect_node.is_ignored() or isinstance(sect_node.value, str): @@ -522,13 +522,13 @@ def load_sections_from_config(self, config_name, save=False): continue meta_data.update({prop_opt: opt_node.value}) latent_section_name = section - if (meta_data.get(rose.META_PROP_DUPLICATE) == - rose.META_PROP_VALUE_TRUE): + if (meta_data.get(metomi.rose.META_PROP_DUPLICATE) == + metomi.rose.META_PROP_VALUE_TRUE): latent_section_name = section + "({0})".format( - rose.CONFIG_SETTING_INDEX_DEFAULT) + metomi.rose.CONFIG_SETTING_INDEX_DEFAULT) meta_data.update({'id': latent_section_name}) if section not in ['ns', 'file:*']: - latent_sect_map[latent_section_name] = rose.section.Section( + latent_sect_map[latent_section_name] = metomi.rose.section.Section( latent_section_name, [], meta_data) return sect_map, latent_sect_map @@ -574,17 +574,17 @@ def load_vars_from_config(self, config_name, only_this_section=None, ignored_reason = {} if section_map[section].ignored_reason: ignored_reason.update({ - rose.variable.IGNORED_BY_SECTION: - rose.config_editor.IGNORED_STATUS_CONFIG}) - if node.state == rose.config.ConfigNode.STATE_SYST_IGNORED: + metomi.rose.variable.IGNORED_BY_SECTION: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) + if node.state == metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: ignored_reason.update({ - rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_CONFIG}) + metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) elif (node.state == - rose.config.ConfigNode.STATE_USER_IGNORED): + metomi.rose.config.ConfigNode.STATE_USER_IGNORED): ignored_reason.update({ - rose.variable.IGNORED_BY_USER: - rose.config_editor.IGNORED_STATUS_CONFIG}) + metomi.rose.variable.IGNORED_BY_USER: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) cfg_comments = node.comments var_id = self.util.get_id_from_section_option(section, option) real_var_ids.append(var_id) @@ -601,7 +601,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, var_map[section].pop(i) break var_map[section].append( - rose.variable.Variable( + metomi.rose.variable.Variable( option, node.value, meta_data, @@ -613,7 +613,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, ) if return_copies: var_map_copy[section].append( - rose.variable.Variable( + metomi.rose.variable.Variable( option, node.value, meta_data, @@ -646,12 +646,12 @@ def load_vars_from_config(self, config_name, only_this_section=None, if setting_id in real_var_ids: # This variable isn't missing, so skip. continue - if (meta_config.get_value([section, rose.META_PROP_DUPLICATE]) == - rose.META_PROP_VALUE_TRUE and + if (meta_config.get_value([section, metomi.rose.META_PROP_DUPLICATE]) == + metomi.rose.META_PROP_VALUE_TRUE and section not in basic_dupl_map and config.get([section]) is None): section = section + "({0})".format( - rose.CONFIG_SETTING_INDEX_DEFAULT) + metomi.rose.CONFIG_SETTING_INDEX_DEFAULT) setting_id = self.util.get_id_from_section_option( section, option) flags = self.load_option_flags(config_name, section, option) @@ -661,15 +661,15 @@ def load_vars_from_config(self, config_name, only_this_section=None, sect_data = latent_section_map.get(section) if sect_data is not None and sect_data.ignored_reason: ignored_reason = { - rose.variable.IGNORED_BY_SECTION: - rose.config_editor.IGNORED_STATUS_CONFIG} + metomi.rose.variable.IGNORED_BY_SECTION: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG} meta_data = {} for prop_opt, opt_node in list(sect_node.value.items()): if opt_node.is_ignored(): continue meta_data.update({prop_opt: opt_node.value}) meta_data.update({'id': setting_id}) - value = rose.variable.get_value_from_metadata(meta_data) + value = metomi.rose.variable.get_value_from_metadata(meta_data) latent_var_map.setdefault(section, []) if return_copies: latent_var_map_copy.setdefault(section, []) @@ -680,7 +680,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, if var.metadata['id'] == setting_id: latent_var_map[section].remove(var) latent_var_map[section].append( - rose.variable.Variable( + metomi.rose.variable.Variable( option, value, meta_data, @@ -691,7 +691,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, ) if return_copies: latent_var_map_copy[section].append( - rose.variable.Variable( + metomi.rose.variable.Variable( option, value, meta_data, @@ -705,11 +705,11 @@ def load_vars_from_config(self, config_name, only_this_section=None, return var_map, latent_var_map def _load_dupl_sect_map(self, basic_dupl_map, section): - basic_section = rose.macro.REC_ID_STRIP.sub("", section) + basic_section = metomi.rose.macro.REC_ID_STRIP.sub("", section) if basic_section != section: basic_dupl_map.setdefault(basic_section, []) basic_dupl_map[basic_section].append(section) - mod_section = rose.macro.REC_ID_STRIP_DUPL.sub("", section) + mod_section = metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section) if mod_section != basic_section and mod_section != section: basic_dupl_map.setdefault(mod_section, []) basic_dupl_map[mod_section].append(section) @@ -720,13 +720,13 @@ def load_option_flags(self, config_name, section, option): opt_conf_flags = self._load_opt_conf_flags(config_name, section, option) if opt_conf_flags: - flags.update({rose.config_editor.FLAG_TYPE_OPT_CONF: + flags.update({metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: opt_conf_flags}) return flags def _load_opt_conf_flags(self, config_name, section, option): opt_config_map = self.config[config_name].opt_configs - opt_conf_diff_format = rose.config_editor.VAR_FLAG_TIP_OPT_CONF_STATE + opt_conf_diff_format = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_STATE opt_flags = {} for opt_name in sorted(opt_config_map): opt_config = opt_config_map[opt_name] @@ -746,13 +746,13 @@ def add_section_to_config(self, section, config_name): self.config[config_name].config.set([section]) def dump_to_internal_config(self, config_name, only_this_ns=None): - """Return a rose.config.ConfigNode object from variable info.""" - config = rose.config.ConfigNode() + """Return a metomi.rose.config.ConfigNode object from variable info.""" + config = metomi.rose.config.ConfigNode() var_map = self.config[config_name].vars.now sect_map = self.config[config_name].sections.now - user_ignored_state = rose.config.ConfigNode.STATE_USER_IGNORED - syst_ignored_state = rose.config.ConfigNode.STATE_SYST_IGNORED - enabled_state = rose.config.ConfigNode.STATE_NORMAL + user_ignored_state = metomi.rose.config.ConfigNode.STATE_USER_IGNORED + syst_ignored_state = metomi.rose.config.ConfigNode.STATE_SYST_IGNORED + enabled_state = metomi.rose.config.ConfigNode.STATE_NORMAL sections_to_be_dumped = [] if only_this_ns is None: allowed_sections = set(list(sect_map.keys()) + list(var_map.keys())) @@ -776,10 +776,10 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): value = variable.value var_state = enabled_state if variable.ignored_reason: - if (rose.variable.IGNORED_BY_USER in + if (metomi.rose.variable.IGNORED_BY_USER in variable.ignored_reason): var_state = user_ignored_state - elif (rose.variable.IGNORED_BY_SYSTEM in + elif (metomi.rose.variable.IGNORED_BY_SYSTEM in variable.ignored_reason): var_state = syst_ignored_state var_comments = variable.comments @@ -792,10 +792,10 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): continue section_state = enabled_state if sect_map[section_id].ignored_reason: - if (rose.variable.IGNORED_BY_USER in + if (metomi.rose.variable.IGNORED_BY_USER in sect_map[section_id].ignored_reason): section_state = user_ignored_state - elif (rose.variable.IGNORED_BY_SYSTEM in + elif (metomi.rose.variable.IGNORED_BY_SYSTEM in sect_map[section_id].ignored_reason): section_state = syst_ignored_state node = config.get([section_id]) @@ -809,7 +809,7 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): def load_meta_path(self, config=None, directory=None): """Retrieve the path to the metadata.""" - return rose.macro.load_meta_path(config, directory) + return metomi.rose.macro.load_meta_path(config, directory) def clear_meta_lookups(self, config_name): for ns in list(self.namespace_meta_lookup.keys()): @@ -823,9 +823,9 @@ def load_meta_config_tree(self, config=None, directory=None, config_type=None, opt_meta_paths=None): """Load the main metadata, and any specified in 'config'.""" if config is None: - config = rose.config.ConfigNode() - error_handler = rose.config_editor.util.launch_error_dialog - return rose.macro.load_meta_config_tree( + config = metomi.rose.config.ConfigNode() + error_handler = metomi.rose.config_editor.util.launch_error_dialog + return metomi.rose.macro.load_meta_config_tree( config, directory, config_type=config_type, @@ -848,23 +848,23 @@ def filter_meta_config(self, config_name): meta_config = config_data.meta directory = config_data.directory meta_dir_path = self.load_meta_path(config, directory)[0] - reports = rose.metadata_check.metadata_check(meta_config, + reports = metomi.rose.metadata_check.metadata_check(meta_config, directory) if reports and meta_dir_path not in self._bad_meta_dir_paths: # There are problems with some metadata. - title = rose.config_editor.ERROR_METADATA_CHECKER_TITLE.format( + title = metomi.rose.config_editor.ERROR_METADATA_CHECKER_TITLE.format( meta_dir_path) - text = rose.config_editor.ERROR_METADATA_CHECKER_TEXT.format( + text = metomi.rose.config_editor.ERROR_METADATA_CHECKER_TEXT.format( len(reports), meta_dir_path) self._bad_meta_dir_paths.append(meta_dir_path) reports_map = {None: reports} - reports_text = rose.macro.get_reports_as_text( - reports_map, "rose.metadata_check.MetadataChecker") - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + reports_text = metomi.rose.macro.get_reports_as_text( + reports_map, "metomi.rose.metadata_check.MetadataChecker") + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title, modal=False, extra_text=reports_text) for report in reports: - if report.option != rose.META_PROP_TRIGGER: + if report.option != metomi.rose.META_PROP_TRIGGER: meta_config.unset([report.section, report.option]) def load_ignored_data(self, config_name): @@ -874,18 +874,18 @@ def load_ignored_data(self, config_name): state. 'Doc table' in the comments refers to - doc/rose-configuration-metadata.html#appendix-ignored-config-edit + doc/metomi.rose.configuration-metadata.html#appendix-ignored-config-edit """ - self.trigger[config_name] = rose.macros.trigger.TriggerMacro() + self.trigger[config_name] = metomi.rose.macros.trigger.TriggerMacro() config = self.config[config_name].config sect_map = self.config[config_name].sections.now latent_sect_map = self.config[config_name].sections.latent var_map = self.config[config_name].vars.now latent_var_map = self.config[config_name].vars.latent - config_for_macro = rose.config.ConfigNode() - enabled_state = rose.config.ConfigNode.STATE_NORMAL - syst_ignored_state = rose.config.ConfigNode.STATE_SYST_IGNORED + config_for_macro = metomi.rose.config.ConfigNode() + enabled_state = metomi.rose.config.ConfigNode.STATE_NORMAL + syst_ignored_state = metomi.rose.config.ConfigNode.STATE_SYST_IGNORED # Deliberately reset state information in the macro config. for keylist, node in config.walk(): if len(keylist) == 1 and list(node.value.keys()): @@ -897,7 +897,7 @@ def load_ignored_data(self, config_name): config_for_macro, meta_config) if bad_list: self.trigger[config_name].trigger_family_lookup.clear() - event = rose.config_editor.EVENT_INVALID_TRIGGERS.format( + event = metomi.rose.config_editor.EVENT_INVALID_TRIGGERS.format( config_name.strip("/")) self.reporter.report(event, self.reporter.KIND_ERR) return @@ -924,8 +924,8 @@ def load_ignored_data(self, config_name): sect, opt = self.util.get_section_option_from_id(var_id) if sect.endswith(")"): continue - node = meta_config.get([sect, rose.META_PROP_DUPLICATE]) - if node is not None and node.value == rose.META_PROP_VALUE_TRUE: + node = meta_config.get([sect, metomi.rose.META_PROP_DUPLICATE]) + if node is not None and node.value == metomi.rose.META_PROP_VALUE_TRUE: search_string = sect + "(" for section in sect_map: if section.startswith(search_string): @@ -964,22 +964,22 @@ def load_ignored_data(self, config_name): # For speed, skip the rest of the checking. # Doc table: E -> E continue - comp_val = node_inst.metadata.get(rose.META_PROP_COMPULSORY) - node_is_compulsory = comp_val == rose.META_PROP_VALUE_TRUE + comp_val = node_inst.metadata.get(metomi.rose.META_PROP_COMPULSORY) + node_is_compulsory = comp_val == metomi.rose.META_PROP_VALUE_TRUE ignored_reasons = list(node_inst.ignored_reason.keys()) if trig_cfg_state == syst_ignored_state: # It should be trigger-ignored. # Doc table: * -> I_t info = ignored_dict.get(setting_id) - if rose.variable.IGNORED_BY_SYSTEM not in ignored_reasons: + if metomi.rose.variable.IGNORED_BY_SYSTEM not in ignored_reasons: help_str = ", ".join(list(info.values())) - if rose.variable.IGNORED_BY_USER in ignored_reasons: + if metomi.rose.variable.IGNORED_BY_USER in ignored_reasons: # It is user-ignored but should be trigger-ignored. # Doc table: I_u -> I_t if node_is_compulsory: # Doc table: I_u -> I_t -> compulsory - key = rose.config_editor.WARNING_TYPE_USER_IGNORED - val = getattr(rose.config_editor, + key = metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED + val = getattr(metomi.rose.config_editor, "WARNING_USER_NOT_TRIGGER_IGNORED") node_inst.warning.update({key: val}) else: @@ -991,19 +991,19 @@ def load_ignored_data(self, config_name): if is_latent: # Fix this for latent settings. node_inst.ignored_reason.update({ - rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_CONFIG}) + metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) else: # Flag an error for real settings. node_inst.error.update({ - rose.config_editor.WARNING_TYPE_ENABLED: - (rose.config_editor.WARNING_NOT_IGNORED + + metomi.rose.config_editor.WARNING_TYPE_ENABLED: + (metomi.rose.config_editor.WARNING_NOT_IGNORED + help_str)}) else: # Otherwise, they both agree about trigger-ignored. # Doc table: I_t -> I_t pass - elif rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: + elif metomi.rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: # It should be enabled, but is trigger-ignored. # Doc table: I_t if (setting_id in enabled_dict and @@ -1012,9 +1012,9 @@ def load_ignored_data(self, config_name): # Doc table: I_t -> E parents = self.trigger[config_name].enabled_dict.get( setting_id) - help_str = (rose.config_editor.WARNING_NOT_ENABLED + + help_str = (metomi.rose.config_editor.WARNING_NOT_ENABLED + ', '.join(parents)) - err_type = rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED + err_type = metomi.rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED node_inst.error.update({err_type: help_str}) elif (setting_id not in enabled_dict and setting_id not in ignored_dict): @@ -1023,14 +1023,14 @@ def load_ignored_data(self, config_name): if node_is_compulsory: # This is an error for compulsory variables. # Doc table: I_t -> not trigger -> compulsory - help_str = rose.config_editor.WARNING_NOT_TRIGGER - err_type = rose.config_editor.WARNING_TYPE_NOT_TRIGGER + help_str = metomi.rose.config_editor.WARNING_NOT_TRIGGER + err_type = metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER node_inst.error.update({err_type: help_str}) else: # Overlook for optional variables. # Doc table: I_t -> not trigger -> optional pass - elif rose.variable.IGNORED_BY_USER in ignored_reasons: + elif metomi.rose.variable.IGNORED_BY_USER in ignored_reasons: # It possibly should be enabled, but is user-ignored. # Doc table: I_u # We've already covered I_u -> I_t @@ -1038,8 +1038,8 @@ def load_ignored_data(self, config_name): # Compulsory settings should not be user-ignored. # Doc table: I_u -> E -> compulsory # Doc table: I_u -> not trigger -> compulsory - help_str = rose.config_editor.WARNING_NOT_USER_IGNORABLE - err_type = rose.config_editor.WARNING_TYPE_USER_IGNORED + help_str = metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE + err_type = metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED node_inst.error.update({err_type: help_str}) # Remaining possibilities are not a problem: # Doc table: E -> E, E -> not trigger @@ -1064,14 +1064,14 @@ def load_file_metadata(self, config_name, section_name=None): continue if not sect_node.is_ignored() and section.startswith("file:"): file_sections.append(section) - if (sect_node.get_value([rose.META_PROP_DUPLICATE]) == - rose.META_PROP_VALUE_TRUE): + if (sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) == + metomi.rose.META_PROP_VALUE_TRUE): duplicate_file_sections.append(section) # Remove metadata for individual duplicate sections - no need. for section in list(file_sections): if section in duplicate_file_sections: continue - base_section = rose.macro.REC_ID_STRIP.sub("", section) + base_section = metomi.rose.macro.REC_ID_STRIP.sub("", section) if base_section in duplicate_file_sections: file_sections.remove(section) file_ids = [] @@ -1081,9 +1081,9 @@ def load_file_metadata(self, config_name, section_name=None): if not sect_node.is_ignored() and setting_id.startswith("file:*="): file_ids.append(setting_id) for section in file_sections: - title = meta_config.get_value([section, rose.META_PROP_TITLE]) + title = meta_config.get_value([section, metomi.rose.META_PROP_TITLE]) if title is None: - meta_config.set([section, rose.META_PROP_TITLE], + meta_config.set([section, metomi.rose.META_PROP_TITLE], section.replace("file:", "", 1)) for file_entry in file_ids: sect_node = meta_config.get([file_entry]) @@ -1121,7 +1121,7 @@ def load_ns_for_node(self, node, config_name): """Load a namespace for a variable or section.""" node_id = node.metadata.get('id') section, option = self.util.get_section_option_from_id(node_id) - subspace = node.metadata.get(rose.META_PROP_NS) + subspace = node.metadata.get(metomi.rose.META_PROP_NS) if subspace is None or option is None: new_namespace = self.helper.get_default_section_namespace( section, config_name) @@ -1144,8 +1144,8 @@ def load_metadata_for_namespaces(self, config_name): is_ns = (section == "ns") is_duplicate_section = ( self.util.get_section_option_from_id(section)[1] is None and - sect_node.get_value([rose.META_PROP_DUPLICATE]) == - rose.META_PROP_VALUE_TRUE + sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) == + metomi.rose.META_PROP_VALUE_TRUE ) if is_ns or is_duplicate_section: if is_ns: @@ -1155,7 +1155,7 @@ def load_metadata_for_namespaces(self, config_name): else: namespace = config_name else: - subspace = sect_node.get_value([rose.META_PROP_NS]) + subspace = sect_node.get_value([metomi.rose.META_PROP_NS]) if subspace is None: namespace = ( self.helper.get_default_section_namespace( @@ -1171,7 +1171,7 @@ def load_metadata_for_namespaces(self, config_name): if opt_node.is_ignored(): continue value = meta_config[setting_id][option].value - if option == rose.META_PROP_MACRO: + if option == metomi.rose.META_PROP_MACRO: if option in ns_metadata: ns_metadata[option] += ", " + value else: @@ -1186,14 +1186,14 @@ def load_metadata_for_namespaces(self, config_name): ns_sections.setdefault(ns, []) if sect not in ns_sections[ns]: ns_sections[ns].append(sect) - if rose.META_PROP_MACRO in variable.metadata: - macro_info = variable.metadata[rose.META_PROP_MACRO] + if metomi.rose.META_PROP_MACRO in variable.metadata: + macro_info = variable.metadata[metomi.rose.META_PROP_MACRO] self.namespace_meta_lookup.setdefault(ns, {}) ns_metadata = self.namespace_meta_lookup[ns] - if rose.META_PROP_MACRO in ns_metadata: - ns_metadata[rose.META_PROP_MACRO] += ", " + macro_info + if metomi.rose.META_PROP_MACRO in ns_metadata: + ns_metadata[metomi.rose.META_PROP_MACRO] += ", " + macro_info else: - ns_metadata[rose.META_PROP_MACRO] = macro_info + ns_metadata[metomi.rose.META_PROP_MACRO] = macro_info default_ns_sections = {} for section_data in config_data.sections.get_all(): # Use the default section namespace. @@ -1215,17 +1215,17 @@ def load_metadata_for_namespaces(self, config_name): config_name) for key, value in list(metadata.items()): if (ns_section not in default_ns_sections.get(ns, []) and - key in [rose.META_PROP_TITLE, rose.META_PROP_SORT_KEY, - rose.META_PROP_DESCRIPTION]): + key in [metomi.rose.META_PROP_TITLE, metomi.rose.META_PROP_SORT_KEY, + metomi.rose.META_PROP_DESCRIPTION]): # ns created from variables, not a section - no title. continue - if key == rose.META_PROP_MACRO: + if key == metomi.rose.META_PROP_MACRO: macro_info = value if key in ns_metadata: - ns_metadata[rose.META_PROP_MACRO] += ( + ns_metadata[metomi.rose.META_PROP_MACRO] += ( ", " + macro_info) else: - ns_metadata[rose.META_PROP_MACRO] = macro_info + ns_metadata[metomi.rose.META_PROP_MACRO] = macro_info else: ns_metadata.setdefault(key, value) self.load_namespace_has_sub_data(config_name) @@ -1234,22 +1234,22 @@ def load_metadata_for_namespaces(self, config_name): self.namespace_meta_lookup.setdefault(config_name, {}) self.namespace_meta_lookup[config_name].setdefault( "icon", icon_path) - if self.config[config_name].config_type == rose.TOP_CONFIG_NAME: + if self.config[config_name].config_type == metomi.rose.TOP_CONFIG_NAME: self.namespace_meta_lookup[config_name].setdefault( - rose.META_PROP_TITLE, - rose.config_editor.TITLE_PAGE_SUITE) + metomi.rose.META_PROP_TITLE, + metomi.rose.config_editor.TITLE_PAGE_SUITE) self.namespace_meta_lookup[config_name].setdefault( - rose.META_PROP_SORT_KEY, " 1") - elif self.config[config_name].config_type == rose.INFO_CONFIG_NAME: + metomi.rose.META_PROP_SORT_KEY, " 1") + elif self.config[config_name].config_type == metomi.rose.INFO_CONFIG_NAME: self.namespace_meta_lookup[config_name].setdefault( - rose.META_PROP_TITLE, - rose.config_editor.TITLE_PAGE_INFO) + metomi.rose.META_PROP_TITLE, + metomi.rose.config_editor.TITLE_PAGE_INFO) self.namespace_meta_lookup[config_name].setdefault( - rose.META_PROP_SORT_KEY, " 0") + metomi.rose.META_PROP_SORT_KEY, " 0") def load_namespace_has_sub_data(self, config_name=None): """Load namespace sub-data status.""" - file_ns = "/" + rose.SUB_CONFIG_FILE_DIR + file_ns = "/" + metomi.rose.SUB_CONFIG_FILE_DIR ns_hierarchy = {} for ns in self.namespace_meta_lookup: if config_name is None or ns.startswith(config_name): @@ -1270,6 +1270,6 @@ def load_namespace_has_sub_data(self, config_name=None): for ns, prop_map in list(self.namespace_meta_lookup.items()): if config_name is not None and not ns.startswith(config_name): continue - if (rose.META_PROP_DUPLICATE in prop_map and + if (metomi.rose.META_PROP_DUPLICATE in prop_map and ns_hierarchy.get(ns, [])): prop_map.setdefault("has_sub_data", True) diff --git a/metomi/rose/config_editor/data_helper.py b/metomi/rose/config_editor/data_helper.py index 8ebb2fbdb..720a70914 100644 --- a/metomi/rose/config_editor/data_helper.py +++ b/metomi/rose/config_editor/data_helper.py @@ -20,7 +20,7 @@ import re -import rose.config +import metomi.rose.config REC_ELEMENT_SECTION = re.compile(r"^(.*)\((.+)\)$") @@ -51,8 +51,8 @@ def get_config_has_unsaved_changes(self, config_name): def get_config_meta_flag(self, config_name, from_this_config_obj=None): """Return the metadata id flag.""" for section, option in [ - [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_META_TYPE], - [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_PROJECT]]: + [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_META_TYPE], + [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_PROJECT]]: if from_this_config_obj is not None: type_node = from_this_config_obj.get( [section, option], no_ignore=True) @@ -84,7 +84,7 @@ def get_metadata_for_config_id(self, node_id, config_name): meta_config = config_data.meta if not node_id: return {'id': node_id} - return rose.macro.get_metadata_for_config_id(node_id, meta_config) + return metomi.rose.macro.get_metadata_for_config_id(node_id, meta_config) def get_variable_by_id(self, var_id, config_name, save=False, latent=False): @@ -120,16 +120,16 @@ def get_macro_info_for_namespace(self, ns): config_name = self.util.split_full_ns(self, ns)[0] config_data = self.data.config[config_name] ns_macros_text = self.data.namespace_meta_lookup.get(ns, {}).get( - rose.META_PROP_MACRO, "") + metomi.rose.META_PROP_MACRO, "") if not ns_macros_text: return {} - ns_macros = rose.variable.array_split(ns_macros_text, + ns_macros = metomi.rose.variable.array_split(ns_macros_text, only_this_delim=",") module_prefix = self.get_macro_module_prefix(config_name) for i, ns_macro in enumerate(ns_macros): ns_macros[i] = module_prefix + ns_macro ns_macro_info = {} - macro_tuples = rose.macro.get_macro_class_methods(config_data.macros) + macro_tuples = metomi.rose.macro.get_macro_class_methods(config_data.macros) for module_name, class_name, method_name, docstring in macro_tuples: this_macro_name = ".".join([module_name, class_name]) this_macro_method_name = ".".join([this_macro_name, method_name]) @@ -191,7 +191,7 @@ def get_ns_comment_string(self, ns): config_name = self.util.split_full_ns(self.data, ns)[0] config_data = self.data.config[config_name] sections = self.get_sections_from_namespace(ns) - sections.sort(rose.config.sort_settings) + sections.sort(metomi.rose.config.sort_settings) for section in sections: sect_data = config_data.sections.now.get(section) if sect_data is not None and sect_data.comments: @@ -216,14 +216,14 @@ def get_ns_url_for_variable(self, variable): self.data, variable.metadata["full_ns"])[0] ns_metadata = self.data.namespace_meta_lookup.get( variable.metadata["full_ns"], {}) - ns_url = ns_metadata.get(rose.META_PROP_URL) + ns_url = ns_metadata.get(metomi.rose.META_PROP_URL) if ns_url: return ns_url section = self.util.get_section_option_from_id( variable.metadata["id"])[0] section_object = self.data.config[config_name].sections.get_sect( section) - section_url = section_object.metadata.get(rose.META_PROP_URL) + section_url = section_object.metadata.get(metomi.rose.META_PROP_URL) return section_url def get_sections_from_namespace(self, namespace): @@ -255,12 +255,12 @@ def get_ns_is_default(self, namespace): for variable in config_data.vars.now.get(section, []): if variable.metadata['full_ns'] == namespace: empty = False - if rose.META_PROP_NS not in variable.metadata: + if metomi.rose.META_PROP_NS not in variable.metadata: return True for variable in config_data.vars.latent.get(section, []): if variable.metadata['full_ns'] == namespace: empty = False - if rose.META_PROP_NS not in variable.metadata: + if metomi.rose.META_PROP_NS not in variable.metadata: return True if empty: # An added, non-metadata section with no variables. @@ -293,7 +293,7 @@ def get_missing_sections(self, config_name=None): section not in miss_sections): miss_sections.append(section) full_sections += [config_name + ':' + s for s in miss_sections] - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings full_sections.sort(sorter) return full_sections @@ -309,21 +309,21 @@ def get_default_section_namespace(self, section, config_name): config_data = self.data.config[config_name] meta_config = config_data.meta node = meta_config.get( - [section, rose.META_PROP_NS], no_ignore=True) + [section, metomi.rose.META_PROP_NS], no_ignore=True) if node is not None: subspace = node.value else: match = REC_ELEMENT_SECTION.match(section) if match: node = meta_config.get( - [match.groups()[0], rose.META_PROP_NS]) + [match.groups()[0], metomi.rose.META_PROP_NS]) if node is None or node.is_ignored(): subspace = section.replace('(', '/') subspace = subspace.replace(')', '') subspace = subspace.replace(':', '/') else: subspace = node.value + '/' + str(match.groups()[1]) - elif section.startswith(rose.SUB_CONFIG_FILE_DIR + ":"): + elif section.startswith(metomi.rose.SUB_CONFIG_FILE_DIR + ":"): subspace = section.rstrip('/').replace('/', ':') subspace = subspace.replace(':', '/', 1) else: @@ -342,7 +342,7 @@ def get_format_sections(self, config_name): if (section not in format_keys and ':' in section and not section.startswith('file:')): format_keys.append(section) - format_keys.sort(rose.config.sort_settings) + format_keys.sort(metomi.rose.config.sort_settings) return format_keys def get_icon_path_for_config(self, config_name): @@ -379,10 +379,10 @@ def get_ignored_sections(self, namespace, get_enabled=False): if get_enabled: if not sect_data.ignored_reason: return_sections.append(section) - elif (rose.variable.IGNORED_BY_USER in + elif (metomi.rose.variable.IGNORED_BY_USER in sect_data.ignored_reason): return_sections.append(section) - return_sections.sort(rose.config.sort_settings) + return_sections.sort(metomi.rose.config.sort_settings) return return_sections def get_latent_sections(self, namespace): @@ -397,7 +397,7 @@ def get_latent_sections(self, namespace): for section in sections: if section not in config_data.sections.now: return_sections.append(section) - return_sections.sort(rose.config.sort_settings) + return_sections.sort(metomi.rose.config.sort_settings) return return_sections def get_ns_ignored_status(self, namespace): @@ -408,7 +408,7 @@ def get_ns_ignored_status(self, namespace): config_name = self.util.split_full_ns(self.data, namespace)[0] config_data = self.data.config[config_name] sections = self.get_sections_from_namespace(namespace) - status = rose.config.ConfigNode.STATE_NORMAL + status = metomi.rose.config.ConfigNode.STATE_NORMAL default_section_statuses = {} variable_statuses = {} for section in sections: @@ -428,7 +428,7 @@ def get_ns_ignored_status(self, namespace): cache[namespace] = status return status for key in var.ignored_reason: - if key == rose.variable.IGNORED_BY_SECTION: + if key == metomi.rose.variable.IGNORED_BY_SECTION: # Section ignored statuses need interpreting. var_id = var.metadata["id"] section = self.util.get_section_option_from_id(var_id)[0] @@ -453,14 +453,14 @@ def get_ns_ignored_status(self, namespace): status_counts.sort(lambda x, y: cmp(x[1], y[1])) if not status_counts: cache[namespace] = status - return rose.config.ConfigNode.STATE_NORMAL + return metomi.rose.config.ConfigNode.STATE_NORMAL status = status_counts[0][0] cache[namespace] = status - if status == rose.variable.IGNORED_BY_USER: - return rose.config.ConfigNode.STATE_USER_IGNORED - if status == rose.variable.IGNORED_BY_SYSTEM: - return rose.config.ConfigNode.STATE_SYST_IGNORED - return rose.config.ConfigNode.STATE_NORMAL + if status == metomi.rose.variable.IGNORED_BY_USER: + return metomi.rose.config.ConfigNode.STATE_USER_IGNORED + if status == metomi.rose.variable.IGNORED_BY_SYSTEM: + return metomi.rose.config.ConfigNode.STATE_SYST_IGNORED + return metomi.rose.config.ConfigNode.STATE_NORMAL def get_ns_latent_status(self, namespace): """Return whether a page has no associated content.""" diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index 5f7e3e11d..ffdbbe64c 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -25,10 +25,10 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor -import rose.gtk.dialog -import rose.gtk.util -import rose.variable +import metomi.rose.config_editor +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util +import metomi.rose.variable class KeyWidget(Gtk.VBox): @@ -36,16 +36,16 @@ class KeyWidget(Gtk.VBox): """This class generates a label or entry box for a variable name.""" FLAG_ICON_MAP = { - rose.config_editor.FLAG_TYPE_DEFAULT: Gtk.STOCK_INFO, - rose.config_editor.FLAG_TYPE_ERROR: Gtk.STOCK_DIALOG_WARNING, - rose.config_editor.FLAG_TYPE_FIXED: Gtk.STOCK_DIALOG_AUTHENTICATION, - rose.config_editor.FLAG_TYPE_OPT_CONF: Gtk.STOCK_INDEX, - rose.config_editor.FLAG_TYPE_OPTIONAL: Gtk.STOCK_ABOUT, - rose.config_editor.FLAG_TYPE_NO_META: Gtk.STOCK_DIALOG_QUESTION, + metomi.rose.config_editor.FLAG_TYPE_DEFAULT: Gtk.STOCK_INFO, + metomi.rose.config_editor.FLAG_TYPE_ERROR: Gtk.STOCK_DIALOG_WARNING, + metomi.rose.config_editor.FLAG_TYPE_FIXED: Gtk.STOCK_DIALOG_AUTHENTICATION, + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: Gtk.STOCK_INDEX, + metomi.rose.config_editor.FLAG_TYPE_OPTIONAL: Gtk.STOCK_ABOUT, + metomi.rose.config_editor.FLAG_TYPE_NO_META: Gtk.STOCK_DIALOG_QUESTION, } - MODIFIED_COLOUR = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_CHANGED) + MODIFIED_COLOUR = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_CHANGED) LABEL_X_OFFSET = 0.01 @@ -96,14 +96,14 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, self.update_comment_display() self.entry.show() for key, value in list(self.show_modes.items()): - if key not in [rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, - rose.config_editor.SHOW_MODE_CUSTOM_HELP, - rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + if key not in [metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: self.set_show_mode(key, value) - if (rose.META_PROP_VALUES in self.meta and - len(self.meta[rose.META_PROP_VALUES]) == 1): - self.add_flag(rose.config_editor.FLAG_TYPE_FIXED, - rose.config_editor.VAR_FLAG_TIP_FIXED) + if (metomi.rose.META_PROP_VALUES in self.meta and + len(self.meta[metomi.rose.META_PROP_VALUES]) == 1): + self.add_flag(metomi.rose.config_editor.FLAG_TYPE_FIXED, + metomi.rose.config_editor.VAR_FLAG_TIP_FIXED) event_box.show() self.show() @@ -122,7 +122,7 @@ def add_flag(self, flag_type, tooltip_text=None): event_box.show() event_box.connect("button-press-event", self._toggle_flag_label) self.hbox.pack_end(event_box, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) def get_centre_height(self): """Return the vertical displacement of the centre of this widget.""" @@ -131,15 +131,15 @@ def get_centre_height(self): def handle_launch_help(self, widget, event): """Handle launching help.""" if event.type == Gdk.EventType.BUTTON_PRESS and event.button != 3: - url_mode = (rose.META_PROP_HELP not in self.meta) + url_mode = (metomi.rose.META_PROP_HELP not in self.meta) self.launch_help(url_mode=url_mode) def launch_edit_comments(self, *args): """Launch an edit comments dialog.""" text = "\n".join(self.my_variable.comments) - title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( + title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( self.my_variable.metadata['id']) - rose.gtk.dialog.run_edit_dialog(text, + metomi.rose.gtk.dialog.run_edit_dialog(text, finish_hook=self._edit_finish_hook, title=title) @@ -160,7 +160,7 @@ def remove_flag(self, flag_type): def set_ignored(self): """Update the ignored display.""" self.ignored_label.set_markup( - rose.variable.get_ignored_markup(self.my_variable)) + metomi.rose.variable.get_ignored_markup(self.my_variable)) hover_string = "" if not self.my_variable.ignored_reason: self.ignored_label.set_tooltip_text(None) @@ -195,47 +195,47 @@ def set_modified(self, is_modified): def set_show_mode(self, show_mode, should_show_mode): """Set the display of a mode on or off.""" - if show_mode in [rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, - rose.config_editor.SHOW_MODE_CUSTOM_HELP, - rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + if show_mode in [metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: return self._set_show_custom_meta_text(show_mode, should_show_mode) - if show_mode == rose.config_editor.SHOW_MODE_NO_TITLE: + if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_TITLE: return self._set_show_title(not should_show_mode) - if show_mode == rose.config_editor.SHOW_MODE_NO_DESCRIPTION: - return self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION: + return self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, not should_show_mode) - if show_mode == rose.config_editor.SHOW_MODE_NO_HELP: - return self._set_show_meta_text_mode(rose.META_PROP_HELP, + if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_HELP: + return self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, not should_show_mode) - if show_mode == rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: + if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: if (should_show_mode and - self.meta.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE): + self.meta.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE): return self.add_flag( - rose.config_editor.FLAG_TYPE_OPTIONAL, - rose.config_editor.VAR_FLAG_TIP_OPTIONAL) - return self.remove_flag(rose.config_editor.FLAG_TYPE_OPTIONAL) - if show_mode == rose.config_editor.SHOW_MODE_FLAG_NO_META: + metomi.rose.config_editor.FLAG_TYPE_OPTIONAL, + metomi.rose.config_editor.VAR_FLAG_TIP_OPTIONAL) + return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_OPTIONAL) + if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META: if should_show_mode and len(self.meta) <= 2: - return self.add_flag(rose.config_editor.FLAG_TYPE_NO_META, - rose.config_editor.VAR_FLAG_TIP_NO_META) - return self.remove_flag(rose.config_editor.FLAG_TYPE_NO_META) - if show_mode == rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: - if (should_show_mode and rose.config_editor.FLAG_TYPE_OPT_CONF in + return self.add_flag(metomi.rose.config_editor.FLAG_TYPE_NO_META, + metomi.rose.config_editor.VAR_FLAG_TIP_NO_META) + return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_NO_META) + if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: + if (should_show_mode and metomi.rose.config_editor.FLAG_TYPE_OPT_CONF in self.my_variable.flags): opts_info = self.my_variable.flags[ - rose.config_editor.FLAG_TYPE_OPT_CONF] + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF] info_text = "" - info_format = rose.config_editor.VAR_FLAG_TIP_OPT_CONF_INFO + info_format = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_INFO for opt, diff in sorted(opts_info.items()): info_text += info_format.format(opt, diff) info_text = info_text.rstrip() if info_text: - text = rose.config_editor.VAR_FLAG_TIP_OPT_CONF.format( + text = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF.format( info_text) return self.add_flag( - rose.config_editor.FLAG_TYPE_OPT_CONF, text) - return self.remove_flag(rose.config_editor.FLAG_TYPE_OPT_CONF) + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF, text) + return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_OPT_CONF) def update_comment_display(self): """Update the display of variable comments.""" @@ -243,8 +243,8 @@ def update_comment_display(self): return self._last_var_comments = self.my_variable.comments if (self.my_variable.comments or - rose.config_editor.SHOULD_SHOW_ALL_COMMENTS): - tip_fmt = rose.config_editor.VAR_COMMENT_TIP + metomi.rose.config_editor.SHOULD_SHOW_ALL_COMMENTS): + tip_fmt = metomi.rose.config_editor.VAR_COMMENT_TIP comments = [tip_fmt.format(c) for c in self.my_variable.comments] tooltip_text = "\n".join(comments) comment_widgets = self.comments_box.get_children() @@ -265,7 +265,7 @@ def update_comment_display(self): self._handle_comment_enter_leave, False) self.comments_box.pack_start( edit_eb, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) self.comments_box.show() else: self.comments_box.hide() @@ -273,17 +273,17 @@ def update_comment_display(self): def _get_metadata_formatting(self, mode): """Apply the correct formatting for a metadata property.""" mode_format = "{" + mode + "}" - if (mode == rose.META_PROP_DESCRIPTION and + if (mode == metomi.rose.META_PROP_DESCRIPTION and self.show_modes[ - rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION]): - mode_format = rose.config_editor.CUSTOM_FORMAT_DESCRIPTION - if (mode == rose.META_PROP_HELP and - self.show_modes[rose.config_editor.SHOW_MODE_CUSTOM_HELP]): - mode_format = rose.config_editor.CUSTOM_FORMAT_HELP - if (mode == rose.META_PROP_TITLE and - self.show_modes[rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): - mode_format = rose.config_editor.CUSTOM_FORMAT_TITLE - mode_string = rose.variable.expand_format_string(mode_format, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION]): + mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_DESCRIPTION + if (mode == metomi.rose.META_PROP_HELP and + self.show_modes[metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP]): + mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_HELP + if (mode == metomi.rose.META_PROP_TITLE and + self.show_modes[metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_TITLE + mode_string = metomi.rose.variable.expand_format_string(mode_format, self.my_variable) if mode_string is None: return self.my_variable.metadata[mode] @@ -291,23 +291,23 @@ def _get_metadata_formatting(self, mode): def _set_show_custom_meta_text(self, mode, should_show_mode): """Set the display of a custom format for a metadata property.""" - if mode == rose.config_editor.SHOW_MODE_CUSTOM_TITLE: + if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE: return self._set_show_title( - not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]) - if mode == rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: + not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]) + if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: is_shown = not self.show_modes[ - rose.config_editor.SHOW_MODE_NO_DESCRIPTION] + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION] if is_shown: - self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, False) - self._set_show_meta_text_mode(rose.META_PROP_DESCRIPTION, + self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, True) - if mode == rose.config_editor.SHOW_MODE_CUSTOM_HELP: + if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP: is_shown = not self.show_modes[ - rose.config_editor.SHOW_MODE_NO_HELP] + metomi.rose.config_editor.SHOW_MODE_NO_HELP] if is_shown: - self._set_show_meta_text_mode(rose.META_PROP_HELP, False) - self._set_show_meta_text_mode(rose.META_PROP_HELP, True) + self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, False) + self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, True) def _set_show_meta_text_mode(self, mode, should_show_mode): """Set the display of description or help below the title/name.""" @@ -317,9 +317,9 @@ def _set_show_meta_text_mode(self, mode, should_show_mode): if mode not in self.meta: return mode_text = self._get_metadata_formatting(mode) - mode_text = rose.gtk.util.safe_str(mode_text) - mode_text = rose.config_editor.VAR_FLAG_MARKUP.format(mode_text) - label = rose.gtk.util.get_hyperlink_label(mode_text, search_func) + mode_text = metomi.rose.gtk.util.safe_str(mode_text) + mode_text = metomi.rose.config_editor.VAR_FLAG_MARKUP.format(mode_text) + label = metomi.rose.gtk.util.get_hyperlink_label(mode_text, search_func) label.show() hbox = Gtk.HBox() hbox.show() @@ -327,7 +327,7 @@ def _set_show_meta_text_mode(self, mode, should_show_mode): hbox.set_sensitive(self.entry.get_property("sensitive")) hbox._show_mode = mode self.pack_start(hbox, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) show_mode_widget_indices = [] for i, widget in enumerate(self.get_children()): if hasattr(widget, "_show_mode"): @@ -351,9 +351,9 @@ def _set_show_title(self, should_show_title): if not self.my_variable.name: return False if should_show_title: - if rose.META_PROP_TITLE in self.meta: + if metomi.rose.META_PROP_TITLE in self.meta: title_string = self._get_metadata_formatting( - rose.META_PROP_TITLE) + metomi.rose.META_PROP_TITLE) if title_string != self.entry.get_text(): return self.entry.set_text(title_string) if self.entry.get_text() != self.my_variable.name: @@ -369,8 +369,8 @@ def _toggle_flag_label(self, event_box, event, text=None): widget._flag_type == flag_type): return self.remove(widget) label = Gtk.Label() - markup = rose.gtk.util.safe_str(text) - markup = rose.config_editor.VAR_FLAG_MARKUP.format(markup) + markup = metomi.rose.gtk.util.safe_str(text) + markup = metomi.rose.config_editor.VAR_FLAG_MARKUP.format(markup) label.set_markup(markup) label.show() hbox = Gtk.HBox() @@ -395,23 +395,23 @@ def _handle_comment_click(self, widget, event): def _handle_enter(self, event_box): label_text = self.entry.get_text() tooltip_text = "" - if rose.META_PROP_DESCRIPTION in self.meta: + if metomi.rose.META_PROP_DESCRIPTION in self.meta: tooltip_text = self._get_metadata_formatting( - rose.META_PROP_DESCRIPTION) - if rose.META_PROP_TITLE in self.meta: - if self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]: + metomi.rose.META_PROP_DESCRIPTION) + if metomi.rose.META_PROP_TITLE in self.meta: + if self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]: # Titles are hidden, so show them in the hover-over. tooltip_text += ("\n (" + - rose.META_PROP_TITLE.capitalize() + + metomi.rose.META_PROP_TITLE.capitalize() + ": '" + - self.meta[rose.META_PROP_TITLE] + "')") + self.meta[metomi.rose.META_PROP_TITLE] + "')") elif (self.my_variable.name not in label_text or not self.show_modes[ - rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): # No custom title, or a custom title without the name. tooltip_text += ("\n (" + self.my_variable.name + ")") if self.my_variable.comments: - tip_fmt = rose.config_editor.VAR_COMMENT_TIP + tip_fmt = metomi.rose.config_editor.VAR_COMMENT_TIP if tooltip_text: tooltip_text += "\n" comments = [tip_fmt.format(c) for c in self.my_variable.comments] @@ -425,13 +425,13 @@ def _handle_enter(self, event_box): if tooltip_text == '': tooltip_text = None event_box.set_tooltip_text(tooltip_text) - if (rose.META_PROP_URL not in self.meta and + if (metomi.rose.META_PROP_URL not in self.meta and 'http://' in self.my_variable.value): new_url = re.search('(http://[^ ]+)', self.my_variable.value).group() # This is not very nice. - self.meta.update({rose.META_PROP_URL: new_url}) - if rose.META_PROP_HELP in self.meta or rose.META_PROP_URL in self.meta: + self.meta.update({metomi.rose.META_PROP_URL: new_url}) + if metomi.rose.META_PROP_HELP in self.meta or metomi.rose.META_PROP_URL in self.meta: if isinstance(self.entry, Gtk.Label): self._set_underline(self.entry, underline=True) return False @@ -468,19 +468,19 @@ def _setter(self, widget, variable): """Re-set the name of the variable in the dictionary object.""" new_name = widget.get_text() if variable.name != new_name: - section = variable.metadata['id'].split(rose.CONFIG_DELIMITER)[0] + section = variable.metadata['id'].split(metomi.rose.CONFIG_DELIMITER)[0] if section.startswith("namelist:"): if new_name.lower() != new_name: - text = rose.config_editor.DIALOG_BODY_NL_CASE_CHANGE + text = metomi.rose.config_editor.DIALOG_BODY_NL_CASE_CHANGE text = text.format(new_name.lower()) - title = rose.config_editor.DIALOG_TITLE_NL_CASE_WARNING - new_name = rose.gtk.dialog.run_choices_dialog( + title = metomi.rose.config_editor.DIALOG_TITLE_NL_CASE_WARNING + new_name = metomi.rose.gtk.dialog.run_choices_dialog( text, [new_name.lower(), new_name], title) if new_name is None: return None self.var_ops.remove_var(variable) variable.name = new_name - variable.metadata['id'] = (section + rose.CONFIG_DELIMITER + + variable.metadata['id'] = (section + metomi.rose.CONFIG_DELIMITER + variable.name) self.var_ops.add_var(variable) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 19397ee63..6a9d83cd3 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -56,30 +56,30 @@ gi.require_version('Gtk', '3.0') import gtk # Only used to run the main gtk loop. -import rose.config -import rose.config_editor -import rose.config_editor.data -import rose.config_editor.menu -import rose.config_editor.nav_controller -import rose.config_editor.nav_panel -import rose.config_editor.nav_panel_menu -import rose.config_editor.ops.group -import rose.config_editor.ops.section -import rose.config_editor.ops.variable -import rose.config_editor.page -import rose.config_editor.stack -import rose.config_editor.status -import rose.config_editor.updater -import rose.config_editor.util -import rose.config_editor.variable -import rose.config_editor.window -import rose.gtk.dialog -import rose.gtk.splash -import rose.gtk.util -import rose.macro -import rose.opt_parse -import rose.resource -import rose.macros +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.config_editor.data +import metomi.rose.config_editor.menu +import metomi.rose.config_editor.nav_controller +import metomi.rose.config_editor.nav_panel +import metomi.rose.config_editor.nav_panel_menu +import metomi.rose.config_editor.ops.group +import metomi.rose.config_editor.ops.section +import metomi.rose.config_editor.ops.variable +import metomi.rose.config_editor.page +import metomi.rose.config_editor.stack +import metomi.rose.config_editor.status +import metomi.rose.config_editor.updater +import metomi.rose.config_editor.util +import metomi.rose.config_editor.variable +import metomi.rose.config_editor.window +import metomi.rose.gtk.dialog +import metomi.rose.gtk.splash +import metomi.rose.gtk.util +import metomi.rose.macro +import metomi.rose.opt_parse +import metomi.rose.resource +import metomi.rose.macros class MainController(object): @@ -93,9 +93,9 @@ class MainController(object): plugging into other GTK applications. If pluggable is False, launch the standalone application. - load_updater is a rose.gtk.splash.SplashScreenProcess instance or + load_updater is a metomi.rose.gtk.splash.SplashScreenProcess instance or None, in which case it will be set to a - rose.gtk.splash.NullSplashScreenProcess. + metomi.rose.gtk.splash.NullSplashScreenProcess. load_all_apps is a boolean that overrides the load-on-demand automation to always load all sub configurations at start time. @@ -117,69 +117,69 @@ def __init__(self, config_directory=None, config_objs=None, if config_objs is None: config_objs = {} if pluggable: - rose.macro.add_meta_paths() + metomi.rose.macro.add_meta_paths() if load_updater is None: - load_updater = rose.gtk.splash.NullSplashScreenProcess() + load_updater = metomi.rose.gtk.splash.NullSplashScreenProcess() self.is_pluggable = pluggable self.tab_windows = [] # No child windows yet self.orphan_pages = [] self.undo_stack = [] # Nothing to undo yet self.redo_stack = [] # Nothing to redo yet self.find_hist = {'regex': '', 'ids': []} - self.util = rose.config_editor.util.Lookup() + self.util = metomi.rose.config_editor.util.Lookup() self.metadata_off = metadata_off if opt_meta_paths is None: opt_meta_paths = [] # Set page variable 'verbosity' defaults. self.page_var_show_modes = { - rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: - rose.config_editor.SHOULD_SHOW_CUSTOM_DESCRIPTION, - rose.config_editor.SHOW_MODE_CUSTOM_HELP: - rose.config_editor.SHOULD_SHOW_CUSTOM_HELP, - rose.config_editor.SHOW_MODE_CUSTOM_TITLE: - rose.config_editor.SHOULD_SHOW_CUSTOM_TITLE, - rose.config_editor.SHOW_MODE_FIXED: - rose.config_editor.SHOULD_SHOW_FIXED_VARS, - rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: - rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS, - rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: - rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS, - rose.config_editor.SHOW_MODE_FLAG_NO_META: - rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS, - rose.config_editor.SHOW_MODE_IGNORED: - rose.config_editor.SHOULD_SHOW_IGNORED_VARS, - rose.config_editor.SHOW_MODE_USER_IGNORED: - rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS, - rose.config_editor.SHOW_MODE_LATENT: - rose.config_editor.SHOULD_SHOW_LATENT_VARS, - rose.config_editor.SHOW_MODE_NO_DESCRIPTION: - rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION, - rose.config_editor.SHOW_MODE_NO_HELP: - rose.config_editor.SHOULD_SHOW_NO_HELP, - rose.config_editor.SHOW_MODE_NO_TITLE: - rose.config_editor.SHOULD_SHOW_NO_TITLE + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP: + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_HELP, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE: + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_TITLE, + metomi.rose.config_editor.SHOW_MODE_FIXED: + metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS, + metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS, + metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS, + metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META: + metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS, + metomi.rose.config_editor.SHOW_MODE_IGNORED: + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS, + metomi.rose.config_editor.SHOW_MODE_LATENT: + metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS, + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION: + metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_NO_HELP: + metomi.rose.config_editor.SHOULD_SHOW_NO_HELP, + metomi.rose.config_editor.SHOW_MODE_NO_TITLE: + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE } # Set page tree 'verbosity' defaults. self.page_ns_show_modes = { - rose.config_editor.SHOW_MODE_IGNORED: - rose.config_editor.SHOULD_SHOW_IGNORED_PAGES, - rose.config_editor.SHOW_MODE_USER_IGNORED: - rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES, - rose.config_editor.SHOW_MODE_LATENT: - rose.config_editor.SHOULD_SHOW_LATENT_PAGES, - rose.config_editor.SHOW_MODE_NO_TITLE: - rose.config_editor.SHOULD_SHOW_NO_TITLE + metomi.rose.config_editor.SHOW_MODE_IGNORED: + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES, + metomi.rose.config_editor.SHOW_MODE_LATENT: + metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES, + metomi.rose.config_editor.SHOW_MODE_NO_TITLE: + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE } - self.reporter = rose.config_editor.status.StatusReporter( + self.reporter = metomi.rose.config_editor.status.StatusReporter( load_updater, self.update_status_text ) # Load the top configuration directory - self.data = rose.config_editor.data.ConfigDataManager( + self.data = metomi.rose.config_editor.data.ConfigDataManager( self.util, self.reporter, self.page_ns_show_modes, @@ -189,16 +189,16 @@ def __init__(self, config_directory=None, config_objs=None, ) self.nav_controller = ( - rose.config_editor.nav_controller.NavTreeManager( + metomi.rose.config_editor.nav_controller.NavTreeManager( self.data, self.util, self.reporter, self.tree_trigger_update )) - self.mainwindow = rose.config_editor.window.MainWindow() + self.mainwindow = metomi.rose.config_editor.window.MainWindow() - self.section_ops = rose.config_editor.ops.section.SectionOperations( + self.section_ops = metomi.rose.config_editor.ops.section.SectionOperations( self.data, self.util, self.reporter, self.undo_stack, self.redo_stack, self.check_cannot_enable_setting, @@ -211,7 +211,7 @@ def __init__(self, config_directory=None, config_objs=None, ) self.variable_ops = ( - rose.config_editor.ops.variable.VariableOperations( + metomi.rose.config_editor.ops.variable.VariableOperations( self.data, self.util, self.reporter, self.undo_stack, self.redo_stack, self.section_ops.add_section, @@ -220,7 +220,7 @@ def __init__(self, config_directory=None, config_objs=None, search_id_func=self.perform_find_by_id )) - self.group_ops = rose.config_editor.ops.group.GroupOperations( + self.group_ops = metomi.rose.config_editor.ops.group.GroupOperations( self.data, self.util, self.reporter, self.undo_stack, self.redo_stack, self.section_ops, @@ -231,7 +231,7 @@ def __init__(self, config_directory=None, config_objs=None, ) # Add in the main menu bar and tool bar handler. - self.main_handle = rose.config_editor.menu.MainMenuHandler( + self.main_handle = metomi.rose.config_editor.menu.MainMenuHandler( self.data, self.util, self.reporter, self.mainwindow, self.undo_stack, self.redo_stack, @@ -246,7 +246,7 @@ def __init__(self, config_directory=None, config_objs=None, ) # Add in the navigation panel menu handler. - self.nav_handle = rose.config_editor.nav_panel_menu.NavPanelHandler( + self.nav_handle = metomi.rose.config_editor.nav_panel_menu.NavPanelHandler( self.data, self.util, self.reporter, self.mainwindow, self.undo_stack, self.redo_stack, @@ -260,7 +260,7 @@ def __init__(self, config_directory=None, config_objs=None, self.main_handle.launch_graph ) - self.updater = rose.config_editor.updater.Updater( + self.updater = metomi.rose.config_editor.updater.Updater( self.data, self.util, self.reporter, self.mainwindow, self.main_handle, self.nav_controller, @@ -276,7 +276,7 @@ def __init__(self, config_directory=None, config_objs=None, load_no_apps=load_no_apps) self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_STATUSES.format( + metomi.rose.config_editor.EVENT_LOAD_STATUSES.format( self.data.top_level_name) ) @@ -286,7 +286,7 @@ def __init__(self, config_directory=None, config_objs=None, self.generate_nav_panel() self.generate_status_bar() # Create notebook (tabbed container) and connect signals. - self.notebook = rose.gtk.util.Notebook() + self.notebook = metomi.rose.gtk.util.Notebook() self.updater.nav_panel = getattr(self, "nav_panel", None) @@ -310,13 +310,13 @@ def __init__(self, config_directory=None, config_objs=None, self.handle_page_change) self.updater.update_all(is_loading=True) self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_ERRORS.format( + metomi.rose.config_editor.EVENT_LOAD_ERRORS.format( self.data.top_level_name, self.updater.load_errors )) self.updater.perform_startup_check() self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_DONE.format( + metomi.rose.config_editor.EVENT_LOAD_DONE.format( self.data.top_level_name )) if (self.data.top_level_directory is None and not self.data.config): @@ -330,88 +330,88 @@ def __init__(self, config_directory=None, config_objs=None, def generate_toolbar(self): """Link in the toolbar functionality.""" - self.toolbar = rose.gtk.util.ToolBar( + self.toolbar = metomi.rose.gtk.util.ToolBar( widgets=[ - (rose.config_editor.TOOLBAR_OPEN, 'Gtk.STOCK_OPEN'), - (rose.config_editor.TOOLBAR_SAVE, 'Gtk.STOCK_SAVE'), - (rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + (metomi.rose.config_editor.TOOLBAR_OPEN, 'Gtk.STOCK_OPEN'), + (metomi.rose.config_editor.TOOLBAR_SAVE, 'Gtk.STOCK_SAVE'), + (metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, 'Gtk.STOCK_SPELL_CHECK'), - (rose.config_editor.TOOLBAR_LOAD_APPS, 'Gtk.STOCK_CDROM'), - (rose.config_editor.TOOLBAR_BROWSE, 'Gtk.STOCK_DIRECTORY'), - (rose.config_editor.TOOLBAR_UNDO, 'Gtk.STOCK_UNDO'), - (rose.config_editor.TOOLBAR_REDO, 'Gtk.STOCK_REDO'), - (rose.config_editor.TOOLBAR_ADD, 'Gtk.STOCK_ADD'), - (rose.config_editor.TOOLBAR_REVERT, + (metomi.rose.config_editor.TOOLBAR_LOAD_APPS, 'Gtk.STOCK_CDROM'), + (metomi.rose.config_editor.TOOLBAR_BROWSE, 'Gtk.STOCK_DIRECTORY'), + (metomi.rose.config_editor.TOOLBAR_UNDO, 'Gtk.STOCK_UNDO'), + (metomi.rose.config_editor.TOOLBAR_REDO, 'Gtk.STOCK_REDO'), + (metomi.rose.config_editor.TOOLBAR_ADD, 'Gtk.STOCK_ADD'), + (metomi.rose.config_editor.TOOLBAR_REVERT, 'Gtk.STOCK_REVERT_TO_SAVED'), - (rose.config_editor.TOOLBAR_FIND, 'Gtk.Entry'), - (rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), - (rose.config_editor.TOOLBAR_VALIDATE, + (metomi.rose.config_editor.TOOLBAR_FIND, 'Gtk.Entry'), + (metomi.rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), + (metomi.rose.config_editor.TOOLBAR_VALIDATE, 'Gtk.STOCK_DIALOG_QUESTION'), - (rose.config_editor.TOOLBAR_TRANSFORM, + (metomi.rose.config_editor.TOOLBAR_TRANSFORM, 'Gtk.STOCK_CONVERT'), - (rose.config_editor.TOOLBAR_VIEW_OUTPUT, + (metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, 'Gtk.STOCK_DIRECTORY'), - (rose.config_editor.TOOLBAR_SUITE_GCONTROL, - 'rose-gtk-scheduler') + (metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, + 'metomi.rose.gtk-scheduler') ], sep_on_name=[ - rose.config_editor.TOOLBAR_CHECK_AND_SAVE, - rose.config_editor.TOOLBAR_BROWSE, - rose.config_editor.TOOLBAR_REDO, - rose.config_editor.TOOLBAR_REVERT, - rose.config_editor.TOOLBAR_FIND_NEXT, - rose.config_editor.TOOLBAR_TRANSFORM + metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + metomi.rose.config_editor.TOOLBAR_BROWSE, + metomi.rose.config_editor.TOOLBAR_REDO, + metomi.rose.config_editor.TOOLBAR_REVERT, + metomi.rose.config_editor.TOOLBAR_FIND_NEXT, + metomi.rose.config_editor.TOOLBAR_TRANSFORM ] ) assign = self.toolbar.set_widget_function - assign(rose.config_editor.TOOLBAR_OPEN, self.load_from_file) - assign(rose.config_editor.TOOLBAR_SAVE, self.save_to_file) - assign(rose.config_editor.TOOLBAR_CHECK_AND_SAVE, self.save_to_file, + assign(metomi.rose.config_editor.TOOLBAR_OPEN, self.load_from_file) + assign(metomi.rose.config_editor.TOOLBAR_SAVE, self.save_to_file) + assign(metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, self.save_to_file, [None, True]) - assign(rose.config_editor.TOOLBAR_LOAD_APPS, self.handle_load_all) - assign(rose.config_editor.TOOLBAR_BROWSE, + assign(metomi.rose.config_editor.TOOLBAR_LOAD_APPS, self.handle_load_all) + assign(metomi.rose.config_editor.TOOLBAR_BROWSE, self.main_handle.launch_browser) - assign(rose.config_editor.TOOLBAR_UNDO, self.perform_undo) - assign(rose.config_editor.TOOLBAR_REDO, self.perform_undo, [True]) - assign(rose.config_editor.TOOLBAR_REVERT, self.revert_to_saved_data) - assign(rose.config_editor.TOOLBAR_FIND_NEXT, self._launch_find) - assign(rose.config_editor.TOOLBAR_VALIDATE, + assign(metomi.rose.config_editor.TOOLBAR_UNDO, self.perform_undo) + assign(metomi.rose.config_editor.TOOLBAR_REDO, self.perform_undo, [True]) + assign(metomi.rose.config_editor.TOOLBAR_REVERT, self.revert_to_saved_data) + assign(metomi.rose.config_editor.TOOLBAR_FIND_NEXT, self._launch_find) + assign(metomi.rose.config_editor.TOOLBAR_VALIDATE, self.main_handle.check_all_extra) - assign(rose.config_editor.TOOLBAR_TRANSFORM, + assign(metomi.rose.config_editor.TOOLBAR_TRANSFORM, self.main_handle.transform_default) - assign(rose.config_editor.TOOLBAR_VIEW_OUTPUT, + assign(metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, self.main_handle.launch_output_viewer) - assign(rose.config_editor.TOOLBAR_SUITE_GCONTROL, + assign(metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, self.main_handle.launch_scheduler) self.find_entry = self.toolbar.item_dict.get( - rose.config_editor.TOOLBAR_FIND)['widget'] + metomi.rose.config_editor.TOOLBAR_FIND)['widget'] self.find_entry.connect("activate", self._launch_find) self.find_entry.connect("changed", self._clear_find) add_icon = self.toolbar.item_dict.get( - rose.config_editor.TOOLBAR_ADD)['widget'] + metomi.rose.config_editor.TOOLBAR_ADD)['widget'] add_icon.connect('button_press_event', self.add_page_variable) - custom_text = rose.config_editor.TOOLBAR_SUITE_RUN_MENU - self._toolbar_run_button = rose.gtk.util.CustomMenuButton( + custom_text = metomi.rose.config_editor.TOOLBAR_SUITE_RUN_MENU + self._toolbar_run_button = metomi.rose.gtk.util.CustomMenuButton( stock_id=Gtk.STOCK_MEDIA_PLAY, menu_items=[(custom_text, Gtk.STOCK_MEDIA_PLAY)], menu_funcs=[self.main_handle.get_run_suite_args], - tip_text=rose.config_editor.TOOLBAR_SUITE_RUN) + tip_text=metomi.rose.config_editor.TOOLBAR_SUITE_RUN) self._toolbar_run_button.connect("clicked", self.main_handle.run_suite) self.toolbar.insert(self._toolbar_run_button, -1) self.toolbar.set_widget_sensitive( - rose.config_editor.TOOLBAR_SUITE_GCONTROL, - any(c.config_type == rose.TOP_CONFIG_NAME + metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, + any(c.config_type == metomi.rose.TOP_CONFIG_NAME for c in list(self.data.config.values()))) self.toolbar.set_widget_sensitive( - rose.config_editor.TOOLBAR_VIEW_OUTPUT, - any(c.config_type == rose.TOP_CONFIG_NAME + metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, + any(c.config_type == metomi.rose.TOP_CONFIG_NAME for c in list(self.data.config.values()))) def generate_menubar(self): """Link in the menu functionality and accelerators.""" - self.menubar = rose.config_editor.menu.MenuBar() + self.menubar = metomi.rose.config_editor.menu.MenuBar() self.menu_widgets = {} menu_list = [ ('/TopMenuBar/File/Open...', self.load_from_file), @@ -432,74 +432,74 @@ def generate_menubar(self): ('/TopMenuBar/Edit/Stack', self.main_handle.view_stack), ('/TopMenuBar/View/View fixed vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_FIXED, + metomi.rose.config_editor.SHOW_MODE_FIXED, m.get_active() )), ('/TopMenuBar/View/View ignored vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_IGNORED, + metomi.rose.config_editor.SHOW_MODE_IGNORED, m.get_active() )), ('/TopMenuBar/View/View user-ignored vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_USER_IGNORED, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, m.get_active() )), ('/TopMenuBar/View/View latent vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_LATENT, + metomi.rose.config_editor.SHOW_MODE_LATENT, m.get_active() )), ('/TopMenuBar/View/View ignored pages', lambda m: self._set_page_ns_show_modes( - rose.config_editor.SHOW_MODE_IGNORED, + metomi.rose.config_editor.SHOW_MODE_IGNORED, m.get_active() )), ('/TopMenuBar/View/View user-ignored pages', lambda m: self._set_page_ns_show_modes( - rose.config_editor.SHOW_MODE_USER_IGNORED, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, m.get_active() )), ('/TopMenuBar/View/View latent pages', lambda m: self._set_page_ns_show_modes( - rose.config_editor.SHOW_MODE_LATENT, + metomi.rose.config_editor.SHOW_MODE_LATENT, m.get_active() )), ('/TopMenuBar/View/Flag no-metadata vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_FLAG_NO_META, + metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META, m.get_active() )), ('/TopMenuBar/View/Flag opt config vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_FLAG_OPT_CONF, + metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF, m.get_active() )), ('/TopMenuBar/View/Flag optional vars', lambda m: self._set_page_var_show_modes( - rose.config_editor.SHOW_MODE_FLAG_OPTIONAL, + metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL, m.get_active() )), ('/TopMenuBar/View/View status bar', lambda m: self._set_show_status_bar(m.get_active())), ('/TopMenuBar/Metadata/Prefs/View without descriptions', lambda m: self._set_page_show_modes( - rose.config_editor.SHOW_MODE_NO_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION, m.get_active() )), ('/TopMenuBar/Metadata/Prefs/View without help', lambda m: self._set_page_show_modes( - rose.config_editor.SHOW_MODE_NO_HELP, + metomi.rose.config_editor.SHOW_MODE_NO_HELP, m.get_active() )), ('/TopMenuBar/Metadata/Prefs/View without titles', lambda m: self._set_page_show_modes( - rose.config_editor.SHOW_MODE_NO_TITLE, + metomi.rose.config_editor.SHOW_MODE_NO_TITLE, m.get_active() )), ('/TopMenuBar/Metadata/All V', lambda m: self.main_handle.handle_run_custom_macro( - method_name=rose.macro.VALIDATE_METHOD + method_name=metomi.rose.macro.VALIDATE_METHOD )), ('/TopMenuBar/Metadata/Autofix', lambda m: self.main_handle.transform_default()), @@ -542,33 +542,33 @@ def generate_menubar(self): ] is_toggled = dict( [('/TopMenuBar/View/View fixed vars', - rose.config_editor.SHOULD_SHOW_FIXED_VARS), + metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS), ('/TopMenuBar/View/View ignored vars', - rose.config_editor.SHOULD_SHOW_IGNORED_VARS), + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS), ('/TopMenuBar/View/View user-ignored vars', - rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS), + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS), ('/TopMenuBar/View/View latent vars', - rose.config_editor.SHOULD_SHOW_LATENT_VARS), + metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS), ('/TopMenuBar/Metadata/Prefs/View without descriptions', - rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION), + metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION), ('/TopMenuBar/Metadata/Prefs/View without help', - rose.config_editor.SHOULD_SHOW_NO_HELP), + metomi.rose.config_editor.SHOULD_SHOW_NO_HELP), ('/TopMenuBar/Metadata/Prefs/View without titles', - rose.config_editor.SHOULD_SHOW_NO_TITLE), + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE), ('/TopMenuBar/View/View ignored pages', - rose.config_editor.SHOULD_SHOW_IGNORED_PAGES), + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES), ('/TopMenuBar/View/View user-ignored pages', - rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES), + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES), ('/TopMenuBar/View/View latent pages', - rose.config_editor.SHOULD_SHOW_LATENT_PAGES), + metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES), ('/TopMenuBar/View/Flag opt config vars', - rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS), + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS), ('/TopMenuBar/View/Flag optional vars', - rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS), + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS), ('/TopMenuBar/View/Flag no-metadata vars', - rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS), + metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS), ('/TopMenuBar/View/View status bar', - rose.config_editor.SHOULD_SHOW_STATUS_BAR), + metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR), ('/TopMenuBar/Metadata/Switch off metadata', self.metadata_off)] ) @@ -578,10 +578,10 @@ def generate_menubar(self): if address in is_toggled: widget.set_active(is_toggled[address]) if (address.endswith("View user-ignored pages") and - rose.config_editor.SHOULD_SHOW_IGNORED_PAGES): + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES): widget.set_sensitive(False) if (address.endswith("View user-ignored vars") and - rose.config_editor.SHOULD_SHOW_IGNORED_VARS): + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS): widget.set_sensitive(False) if address.endswith("Reload metadata") and self.metadata_off: widget.set_sensitive(False) @@ -603,7 +603,7 @@ def generate_menubar(self): add_menuitem )) self.main_handle.load_macro_menu(self.menubar) - if not any(c.config_type == rose.TOP_CONFIG_NAME + if not any(c.config_type == metomi.rose.TOP_CONFIG_NAME for c in list(self.data.config.values())): self.menubar.uimanager.get_widget( "/TopMenuBar/Tools/Run Suite").set_sensitive(False) @@ -611,36 +611,36 @@ def generate_menubar(self): self.top_menu = self.menubar.uimanager.get_widget('/TopMenuBar') # Load the keyboard accelerators. accel = { - rose.config_editor.ACCEL_UNDO: + metomi.rose.config_editor.ACCEL_UNDO: self.perform_undo, - rose.config_editor.ACCEL_REDO: + metomi.rose.config_editor.ACCEL_REDO: lambda: self.perform_undo(redo_mode_on=True), - rose.config_editor.ACCEL_FIND: + metomi.rose.config_editor.ACCEL_FIND: self.find_entry.grab_focus, - rose.config_editor.ACCEL_FIND_NEXT: + metomi.rose.config_editor.ACCEL_FIND_NEXT: lambda: self.perform_find(self.find_hist['regex']), - rose.config_editor.ACCEL_HELP_GUI: + metomi.rose.config_editor.ACCEL_HELP_GUI: self.main_handle.help, - rose.config_editor.ACCEL_OPEN: + metomi.rose.config_editor.ACCEL_OPEN: self.load_from_file, - rose.config_editor.ACCEL_SAVE: + metomi.rose.config_editor.ACCEL_SAVE: self.save_to_file, - rose.config_editor.ACCEL_QUIT: + metomi.rose.config_editor.ACCEL_QUIT: self.main_handle.destroy, - rose.config_editor.ACCEL_METADATA_REFRESH: + metomi.rose.config_editor.ACCEL_METADATA_REFRESH: self._refresh_metadata_if_on, - rose.config_editor.ACCEL_SUITE_RUN: + metomi.rose.config_editor.ACCEL_SUITE_RUN: self.main_handle.run_suite, - rose.config_editor.ACCEL_BROWSER: + metomi.rose.config_editor.ACCEL_BROWSER: self.main_handle.launch_browser, - rose.config_editor.ACCEL_TERMINAL: + metomi.rose.config_editor.ACCEL_TERMINAL: self.main_handle.launch_terminal, } self.menubar.set_accelerators(accel) def generate_nav_panel(self): """"Create tree panel and link functions.""" - self.nav_panel = rose.config_editor.nav_panel.PageNavigationPanel( + self.nav_panel = metomi.rose.config_editor.nav_panel.PageNavigationPanel( self.nav_controller.namespace_tree, self.handle_launch_request, self.nav_handle.get_ns_metadata_and_comments, @@ -651,9 +651,9 @@ def generate_nav_panel(self): def generate_status_bar(self): """Create a status bar.""" - self.status_bar = rose.config_editor.status.StatusBar( - verbosity=rose.config_editor.STATUS_BAR_VERBOSITY) - self._set_show_status_bar(rose.config_editor.SHOULD_SHOW_STATUS_BAR) + self.status_bar = metomi.rose.config_editor.status.StatusBar( + verbosity=metomi.rose.config_editor.STATUS_BAR_VERBOSITY) + self._set_show_status_bar(metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR) # ----------------- Page manipulation functions ------------------------------ @@ -665,7 +665,7 @@ def handle_load_all(self, *args): load_these.append(item) load_these.sort() number_of_events = (len(load_these) * - rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) self.reporter.report_load_event( "Loading all preview apps", new_total_events=number_of_events @@ -675,7 +675,7 @@ def handle_load_all(self, *args): self.data.load_config(config_data.directory, preview=False, metadata_off=self.metadata_off) self.reporter.report_load_event( - rose.config_editor.EVENT_LOADED.format(namespace_name[1:]), + metomi.rose.config_editor.EVENT_LOADED.format(namespace_name[1:]), no_progress=True ) self.reload_namespace_tree() @@ -704,7 +704,7 @@ def handle_launch_request(self, namespace_name, as_new=False): if config_data.is_preview: self.reporter.report_load_event( - rose.config_editor.EVENT_LOAD_ATTEMPT.format( + metomi.rose.config_editor.EVENT_LOAD_ATTEMPT.format( namespace_name), new_total_events=3) self.data.load_config(config_data.directory, preview=False, @@ -712,7 +712,7 @@ def handle_launch_request(self, namespace_name, as_new=False): self.reload_namespace_tree() self.nav_panel.update_row_tooltips() self.reporter.report_load_event( - rose.config_editor.EVENT_LOADED.format(namespace_name), + metomi.rose.config_editor.EVENT_LOADED.format(namespace_name), no_progress=True) self.reporter.stop() if hasattr(self, 'menubar'): @@ -750,18 +750,18 @@ def make_page(self, namespace_name): namespace_name) config_data = self.data.config[config_name] ns_metadata = self.data.namespace_meta_lookup.get(namespace_name, {}) - description = ns_metadata.get(rose.META_PROP_DESCRIPTION, '') - duplicate = ns_metadata.get(rose.META_PROP_DUPLICATE) - help_ = ns_metadata.get(rose.META_PROP_HELP) - url = ns_metadata.get(rose.META_PROP_URL) - custom_widget = ns_metadata.get(rose.config_editor.META_PROP_WIDGET) + description = ns_metadata.get(metomi.rose.META_PROP_DESCRIPTION, '') + duplicate = ns_metadata.get(metomi.rose.META_PROP_DUPLICATE) + help_ = ns_metadata.get(metomi.rose.META_PROP_HELP) + url = ns_metadata.get(metomi.rose.META_PROP_URL) + custom_widget = ns_metadata.get(metomi.rose.config_editor.META_PROP_WIDGET) custom_sub_widget = ns_metadata.get( - rose.config_editor.META_PROP_WIDGET_SUB_NS) + metomi.rose.config_editor.META_PROP_WIDGET_SUB_NS) has_sub_data = self.data.helper.is_ns_sub_data(namespace_name) - label = ns_metadata.get(rose.META_PROP_TITLE) + label = ns_metadata.get(metomi.rose.META_PROP_TITLE) if label is None: label = subspace.split('/')[-1] - if duplicate == rose.META_PROP_VALUE_TRUE and not has_sub_data: + if duplicate == metomi.rose.META_PROP_VALUE_TRUE and not has_sub_data: # For example, namelist/foo/1 should be shown as foo(1). label = "(".join(subspace.split('/')[-2:]) + ")" section_data_objects, latent_section_data_objects = ( @@ -770,20 +770,20 @@ def make_page(self, namespace_name): see_also = '' sections = [s for s in ns_metadata.get('sections', [])] for section_name in [s for s in sections if s.startswith('namelist')]: - no_num_name = rose.macro.REC_ID_STRIP_DUPL.sub("", section_name) - no_mod_name = rose.macro.REC_ID_STRIP.sub("", section_name) + no_num_name = metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section_name) + no_mod_name = metomi.rose.macro.REC_ID_STRIP.sub("", section_name) ok_names = [section_name, no_num_name + "(:)", no_mod_name + "(:)"] if no_mod_name != no_num_name: # There's a modifier in the section name. ok_names.append(no_num_name) for section, variables in list(config_data.vars.now.items()): - if not section.startswith(rose.SUB_CONFIG_FILE_DIR): + if not section.startswith(metomi.rose.SUB_CONFIG_FILE_DIR): continue for variable in variables: - if variable.name != rose.FILE_VAR_SOURCE: + if variable.name != metomi.rose.FILE_VAR_SOURCE: continue - var_values = rose.variable.array_split(variable.value) + var_values = metomi.rose.variable.array_split(variable.value) for i, val in enumerate(var_values): if val.startswith("(") and val.endswith(")"): # It is optional - e.g. "(namelist:baz)". @@ -821,7 +821,7 @@ def make_page(self, namespace_name): } if len(sections) == 1: page_metadata.update({"id": sections.pop()}) - sect_ops = rose.config_editor.ops.section.SectionOperations( + sect_ops = metomi.rose.config_editor.ops.section.SectionOperations( self.data, self.util, self.reporter, self.undo_stack, self.redo_stack, self.check_cannot_enable_setting, @@ -832,7 +832,7 @@ def make_page(self, namespace_name): view_page_func=self.view_page, kill_page_func=self.kill_page ) - var_ops = rose.config_editor.ops.variable.VariableOperations( + var_ops = metomi.rose.config_editor.ops.variable.VariableOperations( self.data, self.util, self.reporter, self.undo_stack, self.redo_stack, sect_ops.add_section, @@ -847,7 +847,7 @@ def make_page(self, namespace_name): namespace_name) launch_edit = lambda: self.nav_handle.edit_request( namespace_name) - page = rose.config_editor.page.ConfigPage( + page = metomi.rose.config_editor.page.ConfigPage( page_metadata, data, latent_data, @@ -883,22 +883,22 @@ def _handle_detach_request(self, page, old_window=None): tab_window = Gtk.Window() tab_window.set_icon(self.mainwindow.window.get_icon()) tab_window.add_accel_group(self.menubar.accelerators) - tab_window.set_default_size(*rose.config_editor.SIZE_PAGE_DETACH) + tab_window.set_default_size(*metomi.rose.config_editor.SIZE_PAGE_DETACH) tab_window.connect('destroy-event', lambda w, e: self.tab_windows.remove(w) and False) tab_window.connect('delete-event', lambda w, e: self.tab_windows.remove(w) and False) else: tab_window = old_window - add_button = rose.gtk.util.CustomButton( + add_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_ADD, - tip_text=rose.config_editor.TIP_ADD_TO_PAGE, + tip_text=metomi.rose.config_editor.TIP_ADD_TO_PAGE, size=Gtk.IconSize.LARGE_TOOLBAR, as_tool=True ) - revert_button = rose.gtk.util.CustomButton( + revert_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_REVERT_TO_SAVED, - tip_text=rose.config_editor.TIP_REVERT_PAGE, + tip_text=metomi.rose.config_editor.TIP_REVERT_PAGE, size=Gtk.IconSize.LARGE_TOOLBAR, as_tool=True ) @@ -911,7 +911,7 @@ def _handle_detach_request(self, page, old_window=None): parent = old_window page.reshuffle_for_detached(add_button, revert_button, parent) tab_window.set_title(' - '.join([page.label, self.data.top_level_name, - rose.config_editor.PROGRAM_NAME])) + metomi.rose.config_editor.PROGRAM_NAME])) tab_window.add(page) tab_window.connect_after('focus-in-event', self.handle_page_change) if old_window is None: @@ -952,9 +952,9 @@ def update_page_bar_sensitivity(self, current_page): ns = current_page.namespace metadata = self.data.namespace_meta_lookup.get(ns, {}) get_widget("/TopMenuBar/Page/Page Help").set_sensitive( - rose.META_PROP_HELP in metadata) + metomi.rose.META_PROP_HELP in metadata) get_widget("/TopMenuBar/Page/Page Web Help").set_sensitive( - rose.META_PROP_URL in metadata) + metomi.rose.META_PROP_URL in metadata) def set_current_page_indicator(self, namespace): """Make sure the current page is highlighted in the nav panel.""" @@ -981,7 +981,7 @@ def revert_to_saved_data(self): page.reload_from_data(config_data, ghost_data) self.data.load_node_namespaces(config_name) self.updater.update_status(page) - self.reporter.report(rose.config_editor.EVENT_REVERT.format( + self.reporter.report(metomi.rose.config_editor.EVENT_REVERT.format( namespace.lstrip("/"))) def _get_pagelist(self): @@ -1038,7 +1038,7 @@ def _set_page_ns_show_modes(self, key, is_key_allowed): """Set namespace view options.""" self.page_ns_show_modes[key] = is_key_allowed if (hasattr(self, "menubar") and - key == rose.config_editor.SHOW_MODE_IGNORED): + key == metomi.rose.config_editor.SHOW_MODE_IGNORED): user_ign_item = self.menubar.uimanager.get_widget( "/TopMenuBar/View/View user-ignored pages") user_ign_item.set_sensitive(not is_key_allowed) @@ -1050,7 +1050,7 @@ def _set_page_var_show_modes(self, key, is_key_allowed): for page in self.pagelist: page.react_to_show_modes(key, is_key_allowed) if (hasattr(self, "menubar") and - key == rose.config_editor.SHOW_MODE_IGNORED): + key == metomi.rose.config_editor.SHOW_MODE_IGNORED): user_ign_item = self.menubar.uimanager.get_widget( "/TopMenuBar/View/View user-ignored vars") user_ign_item.set_sensitive(not is_key_allowed) @@ -1146,7 +1146,7 @@ def load_from_file(self, somewidget=None): self.data.load_top_config(dirname) self.data.saved_config_names = set(self.data.config.keys()) self.mainwindow.window.set_title(self.data.top_level_name + - ' - rose-config-editor') + ' - metomi.rose.config-editor') self.updater.update_all() self.updater.perform_startup_check() else: @@ -1176,13 +1176,13 @@ def save_to_file(self, only_config_name=None, check_on_save=False): self.view_page(var.metadata["full_ns"], var.metadata["id"]) page_address = var.metadata["full_ns"].lstrip("/") - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_SAVE_BLANK.format( + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_SAVE_BLANK.format( short_config_name, page_address ), - title=rose.config_editor.ERROR_SAVE_TITLE.format( + title=metomi.rose.config_editor.ERROR_SAVE_TITLE.format( short_config_name), modal=False ) @@ -1208,7 +1208,7 @@ def save_to_file(self, only_config_name=None, check_on_save=False): None ) dialog.set_markup( - rose.config_editor.WARNING_ERRORS_FOUND_ON_SAVE.format( + metomi.rose.config_editor.WARNING_ERRORS_FOUND_ON_SAVE.format( short_config_name )) res = dialog.run() @@ -1219,17 +1219,17 @@ def save_to_file(self, only_config_name=None, check_on_save=False): # Dump the configuration. filename = config_data.config_type if (directory is None and - config_data.config_type == rose.INFO_CONFIG_NAME): + config_data.config_type == metomi.rose.INFO_CONFIG_NAME): directory = self.data.top_level_directory save_path = os.path.join(directory, filename) - rose.macro.pretty_format_config(config, ignore_error=True) + metomi.rose.macro.pretty_format_config(config, ignore_error=True) try: - rose.config.dump(config, save_path) + metomi.rose.config.dump(config, save_path) except (OSError, IOError) as exc: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_SAVE_PATH_FAIL.format(exc), - title=rose.config_editor.ERROR_SAVE_TITLE.format( + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_SAVE_PATH_FAIL.format(exc), + title=metomi.rose.config_editor.ERROR_SAVE_TITLE.format( short_config_name), modal=False ) @@ -1290,30 +1290,30 @@ def _add_config(self, config_name, meta=None): """Add a configuration, optionally with META=TYPE=meta.""" config_short_name = config_name.split("/")[-1] root = os.path.join(self.data.top_level_directory, - rose.SUB_CONFIGS_DIR) - new_path = os.path.join(root, config_short_name, rose.SUB_CONFIG_NAME) - new_config = rose.config.ConfigNode() + metomi.rose.SUB_CONFIGS_DIR) + new_path = os.path.join(root, config_short_name, metomi.rose.SUB_CONFIG_NAME) + new_config = metomi.rose.config.ConfigNode() if meta is not None: new_config.set( - [rose.CONFIG_SECT_TOP, rose.CONFIG_OPT_META_TYPE], + [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_META_TYPE], meta ) try: os.mkdir(os.path.dirname(new_path)) - rose.config.dump(new_config, new_path) + metomi.rose.config.dump(new_config, new_path) except (OSError, IOError) as exc: - text = rose.config_editor.ERROR_CONFIG_CREATE.format( + text = metomi.rose.config_editor.ERROR_CONFIG_CREATE.format( new_path, type(exc), str(exc)) - title = rose.config_editor.ERROR_CONFIG_CREATE_TITLE - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + title = metomi.rose.config_editor.ERROR_CONFIG_CREATE_TITLE + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) return False self.data.load_config(os.path.dirname(new_path), reload_tree_on=True, skip_load_event=True) - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( config_name, - rose.config_editor.STACK_ACTION_ADDED, - rose.variable.Variable('', '', {}), + metomi.rose.config_editor.STACK_ACTION_ADDED, + metomi.rose.variable.Variable('', '', {}), self._remove_config, (config_name, meta) ) @@ -1346,18 +1346,18 @@ def _remove_config(self, config_name, meta=None): try: shutil.rmtree(dirpath) except (shutil.Error, OSError, IOError) as exc: - text = rose.config_editor.ERROR_CONFIG_DELETE.format( + text = metomi.rose.config_editor.ERROR_CONFIG_DELETE.format( dirpath, type(exc), str(exc)) - title = rose.config_editor.ERROR_CONFIG_CREATE_TITLE - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + title = metomi.rose.config_editor.ERROR_CONFIG_CREATE_TITLE + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) return False self.data.config.pop(config_name) self.reload_namespace_tree() - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( config_name, - rose.config_editor.STACK_ACTION_REMOVED, - rose.variable.Variable('', '', {}), + metomi.rose.config_editor.STACK_ACTION_REMOVED, + metomi.rose.variable.Variable('', '', {}), self._add_config, (config_name, meta) ) @@ -1384,9 +1384,9 @@ def update_bar_widgets(self): """Update bar functionality like Undo and Redo.""" if not hasattr(self, 'toolbar'): return False - self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_UNDO, + self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_UNDO, len(self.undo_stack) > 0) - self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_REDO, + self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_REDO, len(self.redo_stack) > 0) self._get_menu_widget('/Undo').set_sensitive(len(self.undo_stack) > 0) self._get_menu_widget('/Redo').set_sensitive(len(self.redo_stack) > 0) @@ -1394,14 +1394,14 @@ def update_bar_widgets(self): len(self.find_hist['ids']) > 0) self._get_menu_widget('/Load All Apps').set_sensitive( self._has_preview_apps()) - self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_LOAD_APPS, + self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_LOAD_APPS, self._has_preview_apps()) if not hasattr(self, "nav_panel"): return False changes, errors = self.nav_panel.get_change_error_totals() self.status_bar.set_num_errors(errors) self._get_menu_widget('/Autofix').set_sensitive(bool(errors)) - self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_TRANSFORM, + self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_TRANSFORM, bool(errors)) self._update_changed_sensitivity(is_changed=bool(changes)) @@ -1412,10 +1412,10 @@ def update_status_text(self, *args, **kwargs): def _update_changed_sensitivity(self, is_changed=False): """Alter sensitivity of 'unsaved changes' related widgets.""" - self.toolbar.set_widget_sensitive(rose.config_editor.TOOLBAR_SAVE, + self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_SAVE, is_changed) self.toolbar.set_widget_sensitive( - rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, is_changed ) self._get_menu_widget('/Save').set_sensitive(is_changed) @@ -1467,7 +1467,7 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): meta_files = self.data.load_meta_files(meta_config_tree) macro_module_prefix = ( self.data.helper.get_macro_module_prefix(config_name)) - macros = rose.macro.load_meta_macro_modules( + macros = metomi.rose.macro.load_meta_macro_modules( meta_files, module_prefix=macro_module_prefix) config_data.meta = meta_config self.data.load_builtin_macros(config_name) @@ -1477,12 +1477,12 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): sects, l_sects = self.data.load_sections_from_config(config_name) s_sects, s_l_sects = self.data.load_sections_from_config( config_name, save=True) - config_data.sections = rose.config_editor.data.SectData( + config_data.sections = metomi.rose.config_editor.data.SectData( sects, l_sects, s_sects, s_l_sects) var, l_var = self.data.load_vars_from_config(config_name) s_var, s_l_var = self.data.load_vars_from_config( config_name, save=True) - config_data.vars = rose.config_editor.data.VarData( + config_data.vars = metomi.rose.config_editor.data.VarData( var, l_var, s_var, s_l_var) config_data.meta_files = meta_files config_data.macros = macros @@ -1574,21 +1574,21 @@ def _launch_find(self, *args): if expression is not None and expression != '': page, var_id = self.perform_find(expression, start_page) if page is None: - text = rose.config_editor.WARNING_NOT_FOUND + text = metomi.rose.config_editor.WARNING_NOT_FOUND try: # Needs PyGTK >= 2.16 self.find_entry.set_icon_from_stock( 0, Gtk.STOCK_DIALOG_WARNING) self.find_entry.set_icon_tooltip_text(0, text) except AttributeError: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_INFO, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, - rose.config_editor.WARNING_NOT_FOUND_TITLE + metomi.rose.config_editor.WARNING_NOT_FOUND_TITLE ) else: if var_id is not None: self.reporter.report( - rose.config_editor.EVENT_FOUND_ID.format(var_id)) + metomi.rose.config_editor.EVENT_FOUND_ID.format(var_id)) self._clear_find() def _clear_find(self, *args): @@ -1632,11 +1632,11 @@ def get_found_page_and_id(self, expression, start_page): try: reg_find = re.compile(expression).search except sre_constants.error as exc: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_NOT_REGEX.format( + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_NOT_REGEX.format( expression, str(exc)), - rose.config_editor.ERROR_BAD_FIND) + metomi.rose.config_editor.ERROR_BAD_FIND) return None, None if self.find_hist['regex'] != expression: self.find_hist['ids'] = [] @@ -1660,8 +1660,8 @@ def get_found_page_and_id(self, expression, start_page): for variable in search_vars: var_id = variable.metadata.get('id') ns = variable.metadata.get('full_ns') - if (rose.META_PROP_TITLE in variable.metadata and - reg_find(variable.metadata[rose.META_PROP_TITLE])): + if (metomi.rose.META_PROP_TITLE in variable.metadata and + reg_find(variable.metadata[metomi.rose.META_PROP_TITLE])): found_ns_vars.setdefault(ns, []) found_ns_vars[ns].append(variable) continue @@ -1732,9 +1732,9 @@ def perform_undo(self, redo_mode_on=False): is_group = len(do_list) > 1 stack_info = [] namespace_id_map = {} - event_text = rose.config_editor.EVENT_UNDO + event_text = metomi.rose.config_editor.EVENT_UNDO if redo_mode_on: - event_text = rose.config_editor.EVENT_REDO + event_text = metomi.rose.config_editor.EVENT_REDO for stack_item in do_list: action = stack_item.action node = stack_item.node @@ -1820,7 +1820,7 @@ def perform_undo(self, redo_mode_on=False): title = stack_item.name else: title = node_id - id_text = rose.config_editor.EVENT_UNDO_ACTION_ID.format( + id_text = metomi.rose.config_editor.EVENT_UNDO_ACTION_ID.format( action, title) self.reporter.report(event_text.format(id_text)) if is_group: @@ -1848,27 +1848,27 @@ def spawn_window(config_directory_path=None, debug_mode=False, opt_meta_paths = [] if not debug_mode: warnings.filterwarnings('ignore') - resourcer = rose.resource.ResourceLocator.default() - rose.gtk.util.rc_setup( - resourcer.locate('rose-config-edit/.gtkrc-2.0')) - rose.gtk.util.setup_stock_icons() - logo = resourcer.locate('images/rose-splash-logo.png') - if rose.config_editor.ICON_PATH_SCHEDULER is None: + resourcer = metomi.rose.resource.ResourceLocator.default() + metomi.rose.gtk.util.rc_setup( + resourcer.locate('metomi.rose.config-edit/.gtkrc-2.0')) + metomi.rose.gtk.util.setup_stock_icons() + logo = resourcer.locate('images/metomi.rose.splash-logo.png') + if metomi.rose.config_editor.ICON_PATH_SCHEDULER is None: gcontrol_icon = None else: try: gcontrol_icon = resourcer.locate( - rose.config_editor.ICON_PATH_SCHEDULER) - except rose.resource.ResourceError: + metomi.rose.config_editor.ICON_PATH_SCHEDULER) + except metomi.rose.resource.ResourceError: gcontrol_icon = None - rose.gtk.util.setup_scheduler_icon(gcontrol_icon) + metomi.rose.gtk.util.setup_scheduler_icon(gcontrol_icon) number_of_events = (get_number_of_configs(config_directory_path) * - rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) if config_directory_path is None: - title = rose.config_editor.UNTITLED_NAME + title = metomi.rose.config_editor.UNTITLED_NAME else: title = config_directory_path.split("/")[-1] - splash_screen = rose.gtk.splash.SplashScreenProcess(logo, title, + splash_screen = metomi.rose.gtk.splash.SplashScreenProcess(logo, title, number_of_events) try: ctrl = MainController(config_directory_path, @@ -1927,11 +1927,11 @@ def spawn_window(config_directory_path=None, debug_mode=False, def spawn_subprocess_window(config_directory_path=None): """Launch a subprocess for a new config editor. Is it safe?""" if config_directory_path is None: - os.system(rose.config_editor.LAUNCH_COMMAND + ' --new &') + os.system(metomi.rose.config_editor.LAUNCH_COMMAND + ' --new &') return elif not os.path.isdir(str(config_directory_path)): return - os.system(rose.config_editor.LAUNCH_COMMAND_CONFIG + + os.system(metomi.rose.config_editor.LAUNCH_COMMAND_CONFIG + config_directory_path + " &") @@ -1940,9 +1940,9 @@ def get_number_of_configs(config_directory_path=None): number_to_load = 0 if config_directory_path is not None: for listing in set(os.listdir(config_directory_path)): - if listing in rose.CONFIG_NAMES: + if listing in metomi.rose.CONFIG_NAMES: number_to_load += 1 - app_dir = os.path.join(config_directory_path, rose.SUB_CONFIGS_DIR) + app_dir = os.path.join(config_directory_path, metomi.rose.SUB_CONFIGS_DIR) if os.path.exists(app_dir): for entry in os.listdir(app_dir): if (os.path.isdir(os.path.join(app_dir, entry)) and @@ -1953,25 +1953,25 @@ def get_number_of_configs(config_directory_path=None): def main(): """Launch from the command line.""" - if (Gtk.pygtk_version[0] < rose.config_editor.MIN_PYGTK_VERSION[0] or - Gtk.pygtk_version[1] < rose.config_editor.MIN_PYGTK_VERSION[1]): + if (Gtk.pygtk_version[0] < metomi.rose.config_editor.MIN_PYGTK_VERSION[0] or + Gtk.pygtk_version[1] < metomi.rose.config_editor.MIN_PYGTK_VERSION[1]): this_version = '{0}.{1}.{2}'.format(*Gtk.pygtk_version) required_version = '{0}.{1}.{2}'.format( - *rose.config_editor.MIN_PYGTK_VERSION) - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( + *metomi.rose.config_editor.MIN_PYGTK_VERSION) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( required_version, this_version), - rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE + metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE ) sys.exit(1) sys.path.append(os.getenv('ROSE_HOME')) - opt_parser = rose.opt_parse.RoseOptionParser() + opt_parser = metomi.rose.opt_parse.RoseOptionParser() opt_parser.add_my_options("conf_dir", "meta_path", "new_mode", "load_no_apps", "load_all_apps", "no_metadata", "no_warn") opts, args = opt_parser.parse_args() - rose.macro.add_meta_paths() + metomi.rose.macro.add_meta_paths() opt_meta_paths = [] if opts.meta_path: for meta_path in opts.meta_path: @@ -1983,7 +1983,7 @@ def main(): if opts.conf_dir: os.chdir(opts.conf_dir) path = os.getcwd() - name_set = set([rose.SUB_CONFIG_NAME, rose.TOP_CONFIG_NAME]) + name_set = set([metomi.rose.SUB_CONFIG_NAME, metomi.rose.TOP_CONFIG_NAME]) while True: if set(os.listdir(path)) & name_set: break @@ -1996,7 +1996,7 @@ def main(): cwd = os.getcwd() if opts.new_mode: cwd = None - rose.gtk.dialog.set_exception_hook_dialog(keep_alive=True) + metomi.rose.gtk.dialog.set_exception_hook_dialog(keep_alive=True) if opts.profile_mode: handle = tempfile.NamedTemporaryFile() cProfile.runctx("""spawn_window(cwd, debug_mode=opts.debug_mode, diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 77bd824f1..e704a23de 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -30,17 +30,17 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.config_editor -import rose.config_editor.upgrade_controller -import rose.external -import rose.gtk.dialog -import rose.gtk.run -import rose.macro -import rose.macros -import rose.popen -import rose.suite_control -import rose.suite_engine_proc +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.config_editor.upgrade_controller +import metomi.rose.external +import metomi.rose.gtk.dialog +import metomi.rose.gtk.run +import metomi.rose.macro +import metomi.rose.macros +import metomi.rose.popen +import metomi.rose.suite_control +import metomi.rose.suite_engine_proc class MenuBar(object): @@ -137,129 +137,129 @@ class MenuBar(object): action_details = [ ('File', None, - rose.config_editor.TOP_MENU_FILE), + metomi.rose.config_editor.TOP_MENU_FILE), ('Open...', Gtk.STOCK_OPEN, - rose.config_editor.TOP_MENU_FILE_OPEN, - rose.config_editor.ACCEL_OPEN), + metomi.rose.config_editor.TOP_MENU_FILE_OPEN, + metomi.rose.config_editor.ACCEL_OPEN), ('Save', Gtk.STOCK_SAVE, - rose.config_editor.TOP_MENU_FILE_SAVE, - rose.config_editor.ACCEL_SAVE), + metomi.rose.config_editor.TOP_MENU_FILE_SAVE, + metomi.rose.config_editor.ACCEL_SAVE), ('Check and save', Gtk.STOCK_SPELL_CHECK, - rose.config_editor.TOP_MENU_FILE_CHECK_AND_SAVE), + metomi.rose.config_editor.TOP_MENU_FILE_CHECK_AND_SAVE), ('Load All Apps', Gtk.STOCK_CDROM, - rose.config_editor.TOP_MENU_FILE_LOAD_APPS), + metomi.rose.config_editor.TOP_MENU_FILE_LOAD_APPS), ('Quit', Gtk.STOCK_QUIT, - rose.config_editor.TOP_MENU_FILE_QUIT, - rose.config_editor.ACCEL_QUIT), + metomi.rose.config_editor.TOP_MENU_FILE_QUIT, + metomi.rose.config_editor.ACCEL_QUIT), ('Edit', None, - rose.config_editor.TOP_MENU_EDIT), + metomi.rose.config_editor.TOP_MENU_EDIT), ('Undo', Gtk.STOCK_UNDO, - rose.config_editor.TOP_MENU_EDIT_UNDO, - rose.config_editor.ACCEL_UNDO), + metomi.rose.config_editor.TOP_MENU_EDIT_UNDO, + metomi.rose.config_editor.ACCEL_UNDO), ('Redo', Gtk.STOCK_REDO, - rose.config_editor.TOP_MENU_EDIT_REDO, - rose.config_editor.ACCEL_REDO), + metomi.rose.config_editor.TOP_MENU_EDIT_REDO, + metomi.rose.config_editor.ACCEL_REDO), ('Stack', Gtk.STOCK_INFO, - rose.config_editor.TOP_MENU_EDIT_STACK), + metomi.rose.config_editor.TOP_MENU_EDIT_STACK), ('Find', Gtk.STOCK_FIND, - rose.config_editor.TOP_MENU_EDIT_FIND, - rose.config_editor.ACCEL_FIND), + metomi.rose.config_editor.TOP_MENU_EDIT_FIND, + metomi.rose.config_editor.ACCEL_FIND), ('Find Next', Gtk.STOCK_FIND, - rose.config_editor.TOP_MENU_EDIT_FIND_NEXT, - rose.config_editor.ACCEL_FIND_NEXT), + metomi.rose.config_editor.TOP_MENU_EDIT_FIND_NEXT, + metomi.rose.config_editor.ACCEL_FIND_NEXT), ('Preferences', Gtk.STOCK_PREFERENCES, - rose.config_editor.TOP_MENU_EDIT_PREFERENCES), + metomi.rose.config_editor.TOP_MENU_EDIT_PREFERENCES), ('View', None, - rose.config_editor.TOP_MENU_VIEW), + metomi.rose.config_editor.TOP_MENU_VIEW), ('Page', None, - rose.config_editor.TOP_MENU_PAGE), + metomi.rose.config_editor.TOP_MENU_PAGE), ('Add variable', Gtk.STOCK_ADD, - rose.config_editor.TOP_MENU_PAGE_ADD), + metomi.rose.config_editor.TOP_MENU_PAGE_ADD), ('Revert', Gtk.STOCK_REVERT_TO_SAVED, - rose.config_editor.TOP_MENU_PAGE_REVERT), + metomi.rose.config_editor.TOP_MENU_PAGE_REVERT), ('Page Info', Gtk.STOCK_INFO, - rose.config_editor.TOP_MENU_PAGE_INFO), + metomi.rose.config_editor.TOP_MENU_PAGE_INFO), ('Page Help', Gtk.STOCK_HELP, - rose.config_editor.TOP_MENU_PAGE_HELP), + metomi.rose.config_editor.TOP_MENU_PAGE_HELP), ('Page Web Help', Gtk.STOCK_HOME, - rose.config_editor.TOP_MENU_PAGE_WEB_HELP), + metomi.rose.config_editor.TOP_MENU_PAGE_WEB_HELP), ('Metadata', None, - rose.config_editor.TOP_MENU_METADATA), + metomi.rose.config_editor.TOP_MENU_METADATA), ('Reload metadata', Gtk.STOCK_REFRESH, - rose.config_editor.TOP_MENU_METADATA_REFRESH, - rose.config_editor.ACCEL_METADATA_REFRESH), + metomi.rose.config_editor.TOP_MENU_METADATA_REFRESH, + metomi.rose.config_editor.ACCEL_METADATA_REFRESH), ('Load custom metadata', Gtk.STOCK_DIRECTORY, - rose.config_editor.TOP_MENU_METADATA_LOAD), + metomi.rose.config_editor.TOP_MENU_METADATA_LOAD), ('Prefs', Gtk.STOCK_PREFERENCES, - rose.config_editor.TOP_MENU_METADATA_PREFERENCES), + metomi.rose.config_editor.TOP_MENU_METADATA_PREFERENCES), ('Upgrade', Gtk.STOCK_GO_UP, - rose.config_editor.TOP_MENU_METADATA_UPGRADE), + metomi.rose.config_editor.TOP_MENU_METADATA_UPGRADE), ('All V', Gtk.STOCK_DIALOG_QUESTION, - rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V), + metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V), ('Autofix', Gtk.STOCK_CONVERT, - rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX), + metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX), ('Extra checks', Gtk.STOCK_DIALOG_QUESTION, - rose.config_editor.TOP_MENU_METADATA_CHECK), + metomi.rose.config_editor.TOP_MENU_METADATA_CHECK), ('Graph', Gtk.STOCK_SORT_ASCENDING, - rose.config_editor.TOP_MENU_METADATA_GRAPH), + metomi.rose.config_editor.TOP_MENU_METADATA_GRAPH), ('Tools', None, - rose.config_editor.TOP_MENU_TOOLS), + metomi.rose.config_editor.TOP_MENU_TOOLS), ('Run Suite', Gtk.STOCK_MEDIA_PLAY, - rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN), + metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN), ('Run Suite default', Gtk.STOCK_MEDIA_PLAY, - rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_DEFAULT, - rose.config_editor.ACCEL_SUITE_RUN), + metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_DEFAULT, + metomi.rose.config_editor.ACCEL_SUITE_RUN), ('Run Suite custom', Gtk.STOCK_EDIT, - rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_CUSTOM), + metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_CUSTOM), ('Browser', Gtk.STOCK_DIRECTORY, - rose.config_editor.TOP_MENU_TOOLS_BROWSER, - rose.config_editor.ACCEL_BROWSER), + metomi.rose.config_editor.TOP_MENU_TOOLS_BROWSER, + metomi.rose.config_editor.ACCEL_BROWSER), ('Terminal', Gtk.STOCK_EXECUTE, - rose.config_editor.TOP_MENU_TOOLS_TERMINAL, - rose.config_editor.ACCEL_TERMINAL), + metomi.rose.config_editor.TOP_MENU_TOOLS_TERMINAL, + metomi.rose.config_editor.ACCEL_TERMINAL), ('View Output', Gtk.STOCK_DIRECTORY, - rose.config_editor.TOP_MENU_TOOLS_VIEW_OUTPUT), - ('Open Suite GControl', "rose-gtk-scheduler", - rose.config_editor.TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL), + metomi.rose.config_editor.TOP_MENU_TOOLS_VIEW_OUTPUT), + ('Open Suite GControl', "metomi.rose.gtk-scheduler", + metomi.rose.config_editor.TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL), ('Help', None, - rose.config_editor.TOP_MENU_HELP), + metomi.rose.config_editor.TOP_MENU_HELP), ('Documentation', Gtk.STOCK_HELP, - rose.config_editor.TOP_MENU_HELP_GUI, - rose.config_editor.ACCEL_HELP_GUI), + metomi.rose.config_editor.TOP_MENU_HELP_GUI, + metomi.rose.config_editor.ACCEL_HELP_GUI), ('About', Gtk.STOCK_DIALOG_INFO, - rose.config_editor.TOP_MENU_HELP_ABOUT)] + metomi.rose.config_editor.TOP_MENU_HELP_ABOUT)] toggle_action_details = [ ('View latent vars', None, - rose.config_editor.TOP_MENU_VIEW_LATENT_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_VARS), ('View fixed vars', None, - rose.config_editor.TOP_MENU_VIEW_FIXED_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_FIXED_VARS), ('View ignored vars', None, - rose.config_editor.TOP_MENU_VIEW_IGNORED_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_VARS), ('View user-ignored vars', None, - rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_VARS), ('View without descriptions', None, - rose.config_editor.TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS), + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS), ('View without help', None, - rose.config_editor.TOP_MENU_VIEW_WITHOUT_HELP), + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_HELP), ('View without titles', None, - rose.config_editor.TOP_MENU_VIEW_WITHOUT_TITLES), + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_TITLES), ('View ignored pages', None, - rose.config_editor.TOP_MENU_VIEW_IGNORED_PAGES), + metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_PAGES), ('View user-ignored pages', None, - rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_PAGES), + metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_PAGES), ('View latent pages', None, - rose.config_editor.TOP_MENU_VIEW_LATENT_PAGES), + metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_PAGES), ('Flag opt config vars', None, - rose.config_editor.TOP_MENU_VIEW_FLAG_OPT_CONF_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPT_CONF_VARS), ('Flag optional vars', None, - rose.config_editor.TOP_MENU_VIEW_FLAG_OPTIONAL_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPTIONAL_VARS), ('Flag no-metadata vars', None, - rose.config_editor.TOP_MENU_VIEW_FLAG_NO_METADATA_VARS), + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_NO_METADATA_VARS), ('View status bar', None, - rose.config_editor.TOP_MENU_VIEW_STATUS_BAR), + metomi.rose.config_editor.TOP_MENU_VIEW_STATUS_BAR), ('Switch off metadata', None, - rose.config_editor.TOP_MENU_METADATA_SWITCH_OFF)] + metomi.rose.config_editor.TOP_MENU_METADATA_SWITCH_OFF)] def __init__(self): self.uimanager = Gtk.UIManager() @@ -295,12 +295,12 @@ def add_macro(self, config_name, modulename, classname, methodname, """Add a macro to the macro menu.""" macro_address = '/TopMenuBar/Metadata' self.uimanager.get_widget(macro_address).get_submenu() - if methodname == rose.macro.VALIDATE_METHOD: + if methodname == metomi.rose.macro.VALIDATE_METHOD: all_v_item = self.uimanager.get_widget(macro_address + "/All V") all_v_item.set_sensitive(True) config_menu_name = config_name.replace('/', ':').replace('_', '__') config_label_name = config_name.split('/')[-1].replace('_', '__') - label = rose.config_editor.TOP_MENU_METADATA_MACRO_CONFIG.format( + label = metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_CONFIG.format( config_label_name) config_address = macro_address + '/' + config_menu_name config_item = self.uimanager.get_widget(config_address) @@ -323,7 +323,7 @@ def add_macro(self, config_name, modulename, classname, methodname, config_item.set_submenu(Gtk.Menu()) macro_fullname = ".".join([modulename, classname, methodname]) macro_fullname = macro_fullname.replace("_", "__") - if methodname == rose.macro.VALIDATE_METHOD: + if methodname == metomi.rose.macro.VALIDATE_METHOD: stock_id = Gtk.STOCK_DIALOG_QUESTION else: stock_id = Gtk.STOCK_CONVERT @@ -336,15 +336,15 @@ def add_macro(self, config_name, modulename, classname, methodname, macro_item.connect("activate", lambda i: run_macro(*i._run_data)) config_item.get_submenu().append(macro_item) - if (methodname == rose.macro.VALIDATE_METHOD): + if (methodname == metomi.rose.macro.VALIDATE_METHOD): for item in config_item.get_submenu().get_children(): - if hasattr(item, "_rose_all_validators"): + if hasattr(item, "_metomi.rose.all_validators"): return False all_item = Gtk.ImageMenuItem(Gtk.STOCK_DIALOG_QUESTION) - all_item._rose_all_validators = True - all_item.set_label(rose.config_editor.MACRO_MENU_ALL_VALIDATORS) + all_item._metomi.rose.all_validators = True + all_item.set_label(metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) all_item.set_tooltip_text( - rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) + metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) all_item.show() all_item._run_data = [config_name, None, None, methodname] all_item.connect("activate", @@ -376,8 +376,8 @@ def __init__(self, data, util, reporter, mainwindow, self.sect_ops = section_ops_inst self.var_ops = variable_ops_inst self.find_ns_id_func = find_ns_id_func - self.bad_colour = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + self.bad_colour = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) def about_dialog(self, args): self.mainwindow.launch_about_dialog() @@ -413,14 +413,14 @@ def check_all_extra(self): self.update_config(config_name) num_errors = self.check_fail_rules(configs_updated=True) num_errors += self.run_custom_macro( - method_name=rose.macro.VALIDATE_METHOD, + method_name=metomi.rose.macro.VALIDATE_METHOD, configs_updated=True) if num_errors: - text = rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL.format( + text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL.format( num_errors) kind = self.reporter.KIND_ERR else: - text = rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL_OK + text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL_OK kind = self.reporter.KIND_OUT self.reporter.report(text, kind=kind) @@ -430,7 +430,7 @@ def check_fail_rules(self, configs_updated=False): for config_name in self.data.config: if not self.data.config[config_name].is_preview: self.update_config(config_name) - macro = rose.macros.rule.FailureRuleChecker() + macro = metomi.rose.macros.rule.FailureRuleChecker() macro_fullname = "rule.FailureRuleChecker.validate" error_count = 0 for config_name in sorted(self.data.config.keys()): @@ -444,13 +444,13 @@ def check_fail_rules(self, configs_updated=False): if return_value: error_count += len(return_value) except Exception as exc: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, str(exc), - rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( macro_fullname)) continue - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings to_id = lambda s: self.util.get_id_from_section_option( s.section, s.option) return_value.sort(lambda x, y: sorter(to_id(x), to_id(y))) @@ -458,11 +458,11 @@ def check_fail_rules(self, configs_updated=False): config, return_value, no_display=(not return_value)) if error_count > 0: - msg = rose.config_editor.EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND + msg = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND info_text = msg.format(error_count) kind = self.reporter.KIND_ERR else: - msg = rose.config_editor.EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS + msg = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS info_text = msg kind = self.reporter.KIND_OUT self.reporter.report(info_text, kind=kind) @@ -491,7 +491,7 @@ def load_macro_menu(self, menubar): for config_name in config_keys: image = self.data.helper.get_icon_path_for_config(config_name) macros = self.data.config[config_name].macros - macro_tuples = rose.macro.get_macro_class_methods(macros) + macro_tuples = metomi.rose.macro.get_macro_class_methods(macros) macro_tuples.sort(tuple_sorter) for macro_mod, macro_cls, macro_func, help_ in macro_tuples: menubar.add_macro(config_name, macro_mod, macro_cls, @@ -518,7 +518,7 @@ def handle_graph(self): for config_name in self.data.config: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(rose.config.sort_settings) + config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) config_name, section = self.mainwindow.launch_graph_dialog( config_sect_dict) if config_name is None: @@ -628,8 +628,8 @@ def run_custom_macro(self, config_name=None, module_name=None, if not configs_updated: self.update_config(name) if method_name is None: - method_names = [rose.macro.VALIDATE_METHOD, - rose.macro.TRANSFORM_METHOD] + method_names = [metomi.rose.macro.VALIDATE_METHOD, + metomi.rose.macro.TRANSFORM_METHOD] else: method_names = [method_name] if module_name is not None and config_name is not None: @@ -648,7 +648,7 @@ def run_custom_macro(self, config_name=None, module_name=None, for method_name in method_names: if (not hasattr(obj, method_name) or obj_name.startswith("_") or - not issubclass(obj, rose.macro.MacroBase)): + not issubclass(obj, metomi.rose.macro.MacroBase)): continue if class_name is not None and obj_name != class_name: continue @@ -656,13 +656,13 @@ def run_custom_macro(self, config_name=None, module_name=None, obj_name, method_name]) err_text = ( - rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( macro_fullname)) try: macro_inst = obj() except Exception as exc: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, str(exc), err_text) continue if hasattr(macro_inst, method_name): @@ -672,7 +672,7 @@ def run_custom_macro(self, config_name=None, module_name=None, os.chdir(old_pwd) if not macro_data: return 0 - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings to_id = lambda s: self.util.get_id_from_section_option(s.section, s.option) config_macro_errors = [] @@ -692,22 +692,22 @@ def run_custom_macro(self, config_name=None, module_name=None, try: return_value = macro_method(macro_config, meta_config, **res) except Exception: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, 'Error in custom macro:\n\n%s' % ( traceback.format_exc()), - rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( macro_fullname)) continue - if methname == rose.macro.TRANSFORM_METHOD: + if methname == metomi.rose.macro.TRANSFORM_METHOD: if (not isinstance(return_value, tuple) or len(return_value) != 2 or not isinstance( - return_value[0], rose.config.ConfigNode) or + return_value[0], metomi.rose.config.ConfigNode) or not isinstance(return_value[1], list)): self._handle_bad_macro_return(macro_fullname, return_value) continue - integrity_exception = rose.macro.check_config_integrity( + integrity_exception = metomi.rose.macro.check_config_integrity( return_value[0]) if integrity_exception is not None: self._handle_bad_macro_return(macro_fullname, @@ -724,7 +724,7 @@ def run_custom_macro(self, config_name=None, module_name=None, macro_fullname, num_changes)) continue - elif methname == rose.macro.VALIDATE_METHOD: + elif methname == metomi.rose.macro.VALIDATE_METHOD: if not isinstance(return_value, list): self._handle_bad_macro_return(macro_fullname, return_value) @@ -741,14 +741,14 @@ def run_custom_macro(self, config_name=None, module_name=None, # Construct a grouped report. config_macro_errors.sort() config_macro_changes.sort() - if rose.macro.VALIDATE_METHOD in method_names: - null_format = rose.config_editor.EVENT_MACRO_VALIDATE_ALL_OK - change_format = rose.config_editor.EVENT_MACRO_VALIDATE_ALL + if metomi.rose.macro.VALIDATE_METHOD in method_names: + null_format = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL_OK + change_format = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL num_issues = sum([e[2] for e in config_macro_errors]) issue_confs = [e[0] for e in config_macro_errors if e[2]] else: - null_format = rose.config_editor.EVENT_MACRO_TRANSFORM_ALL_OK - change_format = rose.config_editor.EVENT_MACRO_TRANSFORM_ALL + null_format = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL_OK + change_format = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL num_issues = sum([e[2] for e in config_macro_changes]) issue_confs = [e[0] for e in config_macro_changes if e[2]] issue_confs = sorted(set(issue_confs)) @@ -767,23 +767,23 @@ def run_custom_macro(self, config_name=None, module_name=None, def _format_macro_config_names(self, config_names): if len(config_names) > 5: - return rose.config_editor.EVENT_MACRO_CONFIGS.format( + return metomi.rose.config_editor.EVENT_MACRO_CONFIGS.format( len(config_names)) config_names = [c.lstrip("/") for c in config_names] return ", ".join(config_names) def _handle_bad_macro_return(self, macro_fullname, info): if isinstance(info, Exception): - text = rose.config_editor.ERROR_BAD_MACRO_EXCEPTION.format( + text = metomi.rose.config_editor.ERROR_BAD_MACRO_EXCEPTION.format( type(info).__name__, str(info)) else: - text = rose.config_editor.ERROR_BAD_MACRO_RETURN.format(info) - summary = rose.config_editor.ERROR_RUN_MACRO_TITLE.format( + text = metomi.rose.config_editor.ERROR_BAD_MACRO_RETURN.format(info) + summary = metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( macro_fullname) self.reporter.report(summary, kind=self.reporter.KIND_ERR) - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, summary) def handle_macro_transforms(self, config_name, macro_name, @@ -825,16 +825,16 @@ def handle_macro_transforms(self, config_name, macro_name, def _report_macro_transform(self, config_name, macro_name, num_changes): name = config_name.lstrip("/") - if macro_name.endswith(rose.macro.TRANSFORM_METHOD): + if macro_name.endswith(metomi.rose.macro.TRANSFORM_METHOD): macro = macro_name.split('.')[-2] else: macro = macro_name.split('.')[-1] kind = self.reporter.KIND_OUT if num_changes: - info_text = rose.config_editor.EVENT_MACRO_TRANSFORM.format( + info_text = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM.format( name, macro, num_changes) else: - info_text = rose.config_editor.EVENT_MACRO_TRANSFORM_OK.format( + info_text = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_OK.format( name, macro) self.reporter.report(info_text, kind=kind) @@ -853,16 +853,16 @@ def handle_macro_validation(self, config_name, macro_name, def _report_macro_validation(self, config_name, macro_name, num_errors): name = config_name.lstrip("/") - if macro_name.endswith(rose.macro.VALIDATE_METHOD): + if macro_name.endswith(metomi.rose.macro.VALIDATE_METHOD): macro = macro_name.split('.')[-2] else: macro = macro_name.split('.')[-1] if num_errors: - info_text = rose.config_editor.EVENT_MACRO_VALIDATE.format( + info_text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE.format( name, macro, num_errors) kind = self.reporter.KIND_ERR else: - info_text = rose.config_editor.EVENT_MACRO_VALIDATE_OK.format( + info_text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_OK.format( name, macro) kind = self.reporter.KIND_OUT self.reporter.report(info_text, kind=kind) @@ -881,7 +881,7 @@ def handle_upgrade(self, only_this_config_name=None): "config": config_data.config, "directory": config_data.directory } - rose.config_editor.upgrade_controller.UpgradeController( + metomi.rose.config_editor.upgrade_controller.UpgradeController( config_dict, self.handle_macro_transforms, parent_window=self.mainwindow.window, upgrade_inspector=self.override_macro_defaults) @@ -899,16 +899,16 @@ def launch_browser(self): if self.data.top_level_directory is None: start_directory = os.getcwd() try: - rose.external.launch_fs_browser(start_directory) - except rose.popen.RosePopenError as exc: - rose.gtk.dialog.run_exception_dialog(exc) + metomi.rose.external.launch_fs_browser(start_directory) + except metomi.rose.popen.RosePopenError as exc: + metomi.rose.gtk.dialog.run_exception_dialog(exc) def launch_graph(self, namespace, allowed_sections=None): try: import pygraphviz except ImportError as exc: - title = rose.config_editor.WARNING_CANNOT_GRAPH - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + title = metomi.rose.config_editor.WARNING_CANNOT_GRAPH + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, str(exc), title) return else: @@ -924,62 +924,62 @@ def launch_graph(self, namespace, allowed_sections=None): else: allowed_sections = ( self.data.helper.get_sections_from_namespace(namespace)) - cmd = (shlex.split(rose.config_editor.LAUNCH_COMMAND_GRAPH) + + cmd = (shlex.split(metomi.rose.config_editor.LAUNCH_COMMAND_GRAPH) + [config_data.directory] + allowed_sections) try: - rose.popen.RosePopener().run_bg( + metomi.rose.popen.RosePopener().run_bg( *cmd, stdout=sys.stdout, stderr=sys.stderr) - except rose.popen.RosePopenError as exc: - rose.gtk.dialog.run_exception_dialog(exc) + except metomi.rose.popen.RosePopenError as exc: + metomi.rose.gtk.dialog.run_exception_dialog(exc) def launch_scheduler(self, *args): """Run the scheduler for a suite open in config edit.""" this_id = self.data.top_level_name - scontrol = rose.suite_control.SuiteControl() + scontrol = metomi.rose.suite_control.SuiteControl() if scontrol.suite_engine_proc.is_suite_registered(this_id): try: return scontrol.gcontrol(this_id) - except rose.suite_control.SuiteNotRunningError as err: - msg = rose.config_editor.DIALOG_TEXT_SUITE_NOT_RUNNING.format( + except metomi.rose.suite_control.SuiteNotRunningError as err: + msg = metomi.rose.config_editor.DIALOG_TEXT_SUITE_NOT_RUNNING.format( str(err)) - return rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + return metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, msg, - rose.config_editor.DIALOG_TITLE_SUITE_NOT_RUNNING) + metomi.rose.config_editor.DIALOG_TITLE_SUITE_NOT_RUNNING) else: - msg = rose.config_editor.DIALOG_TEXT_UNREGISTERED_SUITE.format( + msg = metomi.rose.config_editor.DIALOG_TEXT_UNREGISTERED_SUITE.format( this_id) - return rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + return metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, msg, - rose.config_editor.DIALOG_TITLE_UNREGISTERED_SUITE) + metomi.rose.config_editor.DIALOG_TITLE_UNREGISTERED_SUITE) def launch_terminal(self): # Handle a launch terminal request. try: - rose.external.launch_terminal() - except rose.popen.RosePopenError as exc: - rose.gtk.dialog.run_exception_dialog(exc) + metomi.rose.external.launch_terminal() + except metomi.rose.popen.RosePopenError as exc: + metomi.rose.gtk.dialog.run_exception_dialog(exc) def launch_output_viewer(self): """View a suite's output, if any.""" - seproc = rose.suite_engine_proc.SuiteEngineProcessor.get_processor() + seproc = metomi.rose.suite_engine_proc.SuiteEngineProcessor.get_processor() try: seproc.launch_suite_log_browser(None, self.data.top_level_name) - except rose.suite_engine_proc.NoSuiteLogError: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_NO_OUTPUT.format( + except metomi.rose.suite_engine_proc.NoSuiteLogError: + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_NO_OUTPUT.format( self.data.top_level_name), - rose.config_editor.DIALOG_TITLE_ERROR) + metomi.rose.config_editor.DIALOG_TITLE_ERROR) def get_run_suite_args(self, *args): """Ask the user for custom arguments to suite run.""" - help_cmds = shlex.split(rose.config_editor.LAUNCH_SUITE_RUN_HELP) + help_cmds = shlex.split(metomi.rose.config_editor.LAUNCH_SUITE_RUN_HELP) help_text = subprocess.Popen(help_cmds, stdout=subprocess.PIPE).communicate()[0] - rose.gtk.dialog.run_command_arg_dialog( - rose.config_editor.LAUNCH_SUITE_RUN, + metomi.rose.gtk.dialog.run_command_arg_dialog( + metomi.rose.config_editor.LAUNCH_SUITE_RUN, help_text, self.run_suite_check_args) def run_suite_check_args(self, args): @@ -993,7 +993,7 @@ def run_suite(self, args=None, **kwargs): args = [] for key, value in list(kwargs.items()): args.extend([key, value]) - rose.gtk.run.run_suite(*args) + metomi.rose.gtk.run.run_suite(*args) return False def transform_default(self, only_this_config=None): @@ -1001,24 +1001,24 @@ def transform_default(self, only_this_config=None): if (only_this_config is not None and only_this_config in list(self.data.config.keys())): config_keys = [only_this_config] - text = rose.config_editor.DIALOG_LABEL_AUTOFIX + text = metomi.rose.config_editor.DIALOG_LABEL_AUTOFIX else: config_keys = sorted(self.data.config.keys()) - text = rose.config_editor.DIALOG_LABEL_AUTOFIX_ALL - proceed = rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_WARNING, + text = metomi.rose.config_editor.DIALOG_LABEL_AUTOFIX_ALL + proceed = metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_WARNING, text, - rose.config_editor.DIALOG_TITLE_AUTOFIX, + metomi.rose.config_editor.DIALOG_TITLE_AUTOFIX, cancel=True) if not proceed: return False - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings to_id = lambda s: self.util.get_id_from_section_option(s.section, s.option) for config_name in config_keys: macro_config = self.data.dump_to_internal_config(config_name) meta_config = self.data.config[config_name].meta - macro = rose.macros.DefaultTransforms() + macro = metomi.rose.macros.DefaultTransforms() change_list = macro.transform(macro_config, meta_config)[1] change_list.sort(lambda x, y: sorter(to_id(x), to_id(y))) self.handle_macro_transforms( diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index 8f09a3b38..ac3685027 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -22,22 +22,22 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor -import rose.config_editor.util -import rose.gtk.dialog -import rose.gtk.util +import metomi.rose.config_editor +import metomi.rose.config_editor.util +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util class MenuWidget(Gtk.HBox): """This class generates a button with a menu for variable actions.""" - MENU_ICON_ERRORS = 'rose-gtk-gnome-package-system-errors' - MENU_ICON_WARNINGS = 'rose-gtk-gnome-package-system-warnings' - MENU_ICON_LATENT = 'rose-gtk-gnome-add' - MENU_ICON_LATENT_ERRORS = 'rose-gtk-gnome-add-errors' - MENU_ICON_LATENT_WARNINGS = 'rose-gtk-gnome-add-warnings' - MENU_ICON_NORMAL = 'rose-gtk-gnome-package-system-normal' + MENU_ICON_ERRORS = 'metomi.rose.gtk-gnome-package-system-errors' + MENU_ICON_WARNINGS = 'metomi.rose.gtk-gnome-package-system-warnings' + MENU_ICON_LATENT = 'metomi.rose.gtk-gnome-add' + MENU_ICON_LATENT_ERRORS = 'metomi.rose.gtk-gnome-add-errors' + MENU_ICON_LATENT_WARNINGS = 'metomi.rose.gtk-gnome-add-warnings' + MENU_ICON_NORMAL = 'metomi.rose.gtk-gnome-package-system-normal' def __init__(self, variable, var_ops, remove_func, update_func, launch_help_func): @@ -63,35 +63,35 @@ def load_contents(self): """ - actions = [('Options', 'rose-gtk-gnome-package-system', ''), + actions = [('Options', 'metomi.rose.gtk-gnome-package-system', ''), ('Info', Gtk.STOCK_INFO, - rose.config_editor.VAR_MENU_INFO), + metomi.rose.config_editor.VAR_MENU_INFO), ('Help', Gtk.STOCK_HELP, - rose.config_editor.VAR_MENU_HELP), + metomi.rose.config_editor.VAR_MENU_HELP), ('Web Help', Gtk.STOCK_HOME, - rose.config_editor.VAR_MENU_URL), + metomi.rose.config_editor.VAR_MENU_URL), ('Edit', Gtk.STOCK_EDIT, - rose.config_editor.VAR_MENU_EDIT_COMMENTS), + metomi.rose.config_editor.VAR_MENU_EDIT_COMMENTS), ('Fix Ignore', Gtk.STOCK_CONVERT, - rose.config_editor.VAR_MENU_FIX_IGNORE), + metomi.rose.config_editor.VAR_MENU_FIX_IGNORE), ('Ignore', Gtk.STOCK_NO, - rose.config_editor.VAR_MENU_IGNORE), + metomi.rose.config_editor.VAR_MENU_IGNORE), ('Enable', Gtk.STOCK_YES, - rose.config_editor.VAR_MENU_ENABLE), + metomi.rose.config_editor.VAR_MENU_ENABLE), ('Remove', Gtk.STOCK_DELETE, - rose.config_editor.VAR_MENU_REMOVE), + metomi.rose.config_editor.VAR_MENU_REMOVE), ('Add', Gtk.STOCK_ADD, - rose.config_editor.VAR_MENU_ADD)] - menu_icon_id = 'rose-gtk-gnome-package-system' - is_comp = (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE) + metomi.rose.config_editor.VAR_MENU_ADD)] + menu_icon_id = 'metomi.rose.gtk-gnome-package-system' + is_comp = (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE) if self.is_ghost or is_comp: option_ui_middle = ( option_ui_middle.replace("", '')) - error_types = rose.config_editor.WARNING_TYPES_IGNORE + error_types = metomi.rose.config_editor.WARNING_TYPES_IGNORE if (set(error_types) & set(variable.error.keys()) or set(error_types) & set(variable.warning.keys()) or - (rose.META_PROP_COMPULSORY in variable.error and + (metomi.rose.META_PROP_COMPULSORY in variable.error and not self.is_ghost)): option_ui_middle = ("" + "" + @@ -132,11 +132,11 @@ def load_contents(self): option_ui_middle = ("" + "" + option_ui_middle) - if rose.META_PROP_URL in variable.metadata: + if metomi.rose.META_PROP_URL in variable.metadata: url_ui = "" option_ui_middle += url_ui option_ui = option_ui_start + option_ui_middle + option_ui_end - self.button = rose.gtk.util.CustomButton( + self.button = metomi.rose.gtk.util.CustomButton( stock_id=menu_icon_id, size=Gtk.IconSize.MENU, as_tool=True) @@ -177,18 +177,18 @@ def refresh(self, variable=None): def _set_hover_over(self, variable): hover_string = 'Variable options' if variable.warning: - hover_string = rose.config_editor.VAR_MENU_TIP_WARNING + hover_string = metomi.rose.config_editor.VAR_MENU_TIP_WARNING for warn, warn_info in list(variable.warning.items()): hover_string += "(" + warn + "): " + warn_info + '\n' hover_string = hover_string.rstrip('\n') if variable.error: - hover_string = rose.config_editor.VAR_MENU_TIP_ERROR + hover_string = metomi.rose.config_editor.VAR_MENU_TIP_ERROR for err, err_info in list(variable.error.items()): hover_string += "(" + err + "): " + err_info + '\n' hover_string = hover_string.rstrip('\n') if self.is_ghost: if not variable.error: - hover_string = rose.config_editor.VAR_MENU_TIP_LATENT + hover_string = metomi.rose.config_editor.VAR_MENU_TIP_LATENT self.hover_text = hover_string self.button.set_tooltip_text(self.hover_text) self.button.show() @@ -212,14 +212,14 @@ def _popup_option_menu(self, option_ui, actions, button, time): warnings = list(self.my_variable.warning.keys()) ns = self.my_variable.metadata["full_ns"] search_function = lambda i: self.var_ops.search_for_var(ns, i) - dialog_func = rose.gtk.dialog.run_hyperlink_dialog + dialog_func = metomi.rose.gtk.dialog.run_hyperlink_dialog for error in errors: err_name = error.replace("/", "_") action_name = "Error_" + err_name if "action='" + action_name + "'" not in option_ui: continue err_item = uimanager.get_widget('/Options/' + action_name) - title = rose.config_editor.DIALOG_VARIABLE_ERROR_TITLE.format( + title = metomi.rose.config_editor.DIALOG_VARIABLE_ERROR_TITLE.format( error, self.my_variable.metadata["id"]) err_item.set_tooltip_text(self.my_variable.error[error]) err_item.connect( @@ -232,7 +232,7 @@ def _popup_option_menu(self, option_ui, actions, button, time): if "action='" + action_name + "'" not in option_ui: continue warn_item = uimanager.get_widget('/Options/' + action_name) - title = rose.config_editor.DIALOG_VARIABLE_WARNING_TITLE.format( + title = metomi.rose.config_editor.DIALOG_VARIABLE_WARNING_TITLE.format( warning, self.my_variable.metadata["id"]) warn_item.set_tooltip_text(self.my_variable.warning[warning]) warn_item.connect( @@ -244,12 +244,12 @@ def _popup_option_menu(self, option_ui, actions, button, time): enable_item = None if "action='Ignore'" in option_ui: ignore_item = uimanager.get_widget('/Options/Ignore') - if (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE or self.is_ghost): + if (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE or self.is_ghost): ignore_item.set_sensitive(False) # It is a non-trigger, optional, enabled variable. - new_reason = {rose.variable.IGNORED_BY_USER: - rose.config_editor.IGNORED_STATUS_MANUAL} + new_reason = {metomi.rose.variable.IGNORED_BY_USER: + metomi.rose.config_editor.IGNORED_STATUS_MANUAL} ignore_item.connect( "activate", lambda b: self.var_ops.set_var_ignored( @@ -262,7 +262,7 @@ def _popup_option_menu(self, option_ui, actions, button, time): if "action='Fix Ignore'" in option_ui: fix_ignore_item = uimanager.get_widget('/Options/Fix Ignore') fix_ignore_item.set_tooltip_text( - rose.config_editor.VAR_MENU_TIP_FIX_IGNORE) + metomi.rose.config_editor.VAR_MENU_TIP_FIX_IGNORE) fix_ignore_item.connect( "activate", lambda e: self.var_ops.fix_var_ignored(self.my_variable)) @@ -272,13 +272,13 @@ def _popup_option_menu(self, option_ui, actions, button, time): enable_item.set_sensitive(False) info_item = uimanager.get_widget('/Options/Info') info_item.connect("activate", self._launch_info_dialog) - if (self.my_variable.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE or self.is_ghost): + if (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE or self.is_ghost): remove_item.set_sensitive(False) help_item = uimanager.get_widget('/Options/Help') help_item.connect("activate", lambda b: self.launch_help()) - if rose.META_PROP_HELP not in self.my_variable.metadata: + if metomi.rose.META_PROP_HELP not in self.my_variable.metadata: help_item.set_sensitive(False) url_item = uimanager.get_widget('/Options/Web Help') if url_item is not None and 'url' in self.my_variable.metadata: @@ -299,15 +299,15 @@ def _launch_info_dialog(self, *args): changes = self.var_ops.get_var_changes(self.my_variable) ns = self.my_variable.metadata["full_ns"] search_function = lambda i: self.var_ops.search_for_var(ns, i) - rose.config_editor.util.launch_node_info_dialog(self.my_variable, + metomi.rose.config_editor.util.launch_node_info_dialog(self.my_variable, changes, search_function) def launch_edit(self, *args): text = "\n".join(self.my_variable.comments) - title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( + title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( self.my_variable.metadata['id']) - rose.gtk.dialog.run_edit_dialog(text, + metomi.rose.gtk.dialog.run_edit_dialog(text, finish_hook=self._edit_finish_hook, title=title) @@ -331,7 +331,7 @@ def __init__(self, *args): self.checkbutton.set_active(not self.is_ghost) meta = self.my_variable.metadata if not self.is_ghost and meta.get( - rose.META_PROP_COMPULSORY) == rose.META_PROP_VALUE_TRUE: + metomi.rose.META_PROP_COMPULSORY) == metomi.rose.META_PROP_VALUE_TRUE: self.checkbutton.set_sensitive(False) self.pack_start(self.checkbutton, expand=False, fill=False, padding=0) self.pack_start(self.button, expand=False, fill=False, padding=0) diff --git a/metomi/rose/config_editor/nav_controller.py b/metomi/rose/config_editor/nav_controller.py index 5eb4eb253..a446349f2 100644 --- a/metomi/rose/config_editor/nav_controller.py +++ b/metomi/rose/config_editor/nav_controller.py @@ -19,7 +19,7 @@ # ----------------------------------------------------------------------------- -import rose.config_editor +import metomi.rose.config_editor class NavTreeManager(object): @@ -64,11 +64,11 @@ def reload_namespace_tree(self, only_this_namespace=None, # Reload the information into the tree. if only_this_config is None: configs = list(self.data.config.keys()) - configs.sort(rose.config.sort_settings) + configs.sort(metomi.rose.config.sort_settings) configs.sort( lambda x, y: cmp( - self.data.config[y].config_type == rose.TOP_CONFIG_NAME, - self.data.config[x].config_type == rose.TOP_CONFIG_NAME + self.data.config[y].config_type == metomi.rose.TOP_CONFIG_NAME, + self.data.config[x].config_type == metomi.rose.TOP_CONFIG_NAME ) ) else: @@ -135,8 +135,8 @@ def update_namespace_tree(self, spaces, subtree, prev_spaces): meta.setdefault('title', spaces[0]) latent_status = self.data.helper.get_ns_latent_status(this_ns) ignored_status = self.data.helper.get_ns_ignored_status(this_ns) - statuses = {rose.config_editor.SHOW_MODE_LATENT: latent_status, - rose.config_editor.SHOW_MODE_IGNORED: ignored_status} + statuses = {metomi.rose.config_editor.SHOW_MODE_LATENT: latent_status, + metomi.rose.config_editor.SHOW_MODE_IGNORED: ignored_status} subtree.setdefault(spaces[0], [{}, meta, statuses, change]) prev_spaces += [spaces[0]] self.update_namespace_tree(spaces[1:], subtree[spaces[0]][0], diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index 723bfdf96..9e289ab0a 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -26,11 +26,11 @@ from gi.repository import Gtk from gi.repository import GObject -import rose.config -import rose.config_editor -import rose.config_editor.util -import rose.gtk.util -import rose.resource +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.config_editor.util +import metomi.rose.gtk.util +import metomi.rose.resource class PageNavigationPanel(Gtk.ScrolledWindow): @@ -68,9 +68,9 @@ def __init__(self, namespace_tree, launch_ns_func, self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.set_shadow_type(Gtk.ShadowType.OUT) self._rec_no_expand_leaves = re.compile( - rose.config_editor.TREE_PANEL_NO_EXPAND_LEAVES_REGEX) + metomi.rose.config_editor.TREE_PANEL_NO_EXPAND_LEAVES_REGEX) self.panel_top = Gtk.TreeViewColumn() - self.panel_top.set_title(rose.config_editor.TREE_PANEL_TITLE) + self.panel_top.set_title(metomi.rose.config_editor.TREE_PANEL_TITLE) self.cell_error_icon = Gtk.CellRendererPixbuf() self.cell_changed_icon = Gtk.CellRendererPixbuf() self.cell_title = Gtk.CellRendererText() @@ -92,15 +92,15 @@ def __init__(self, namespace_tree, launch_ns_func, self.data_store = Gtk.TreeStore(GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, str, str, int, int, int, int, bool, str, str, str) - resource_loc = rose.resource.ResourceLocator(paths=sys.path) - image_path = resource_loc.locate('etc/images/rose-config-edit') + resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) + image_path = resource_loc.locate('etc/images/metomi.rose.config-edit') self.null_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + '/null_icon.xpm') self.changed_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + '/change_icon.xpm') self.error_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + '/error_icon.xpm') - self.tree = rose.gtk.util.TooltipTreeView( + self.tree = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self.get_treeview_tooltip) self.tree.append_column(self.panel_top) self.filter_model = self.data_store.filter_new() @@ -147,7 +147,7 @@ def add_cursor_extra(self, widget, event): def _handle_cursor_change(self, *args): current_path = self.tree.get_cursor()[0] if current_path != self._last_tree_activation_path: - GObject.timeout_add(rose.config_editor.TREE_PANEL_KBD_TIMEOUT, + GObject.timeout_add(metomi.rose.config_editor.TREE_PANEL_KBD_TIMEOUT, self._timeout_launch, current_path) def _timeout_launch(self, timeout_path): @@ -169,7 +169,7 @@ def load_tree(self, row, namespace_subtree): def set_expansion(self): """Set the default expanded rows.""" top_rows = self.filter_model.iter_n_children(None) - if top_rows > rose.config_editor.TREE_PANEL_MAX_EXPANDED_ROOTS: + if top_rows > metomi.rose.config_editor.TREE_PANEL_MAX_EXPANDED_ROOTS: return False if top_rows == 1: return self.expand_recursive(no_duplicates=True) @@ -201,9 +201,9 @@ def load_tree_stack(self, row, namespace_subtree): while stack: row, keylist, key, value_meta_tuple = stack[0] value, meta, statuses, change = value_meta_tuple - title = meta[rose.META_PROP_TITLE] - latent_status = statuses[rose.config_editor.SHOW_MODE_LATENT] - ignored_status = statuses[rose.config_editor.SHOW_MODE_IGNORED] + title = meta[metomi.rose.META_PROP_TITLE] + latent_status = statuses[metomi.rose.config_editor.SHOW_MODE_LATENT] + ignored_status = statuses[metomi.rose.config_editor.SHOW_MODE_IGNORED] new_row = self.data_store.append(row, [self.null_icon, self.null_icon, title, @@ -224,35 +224,35 @@ def load_tree_stack(self, row, namespace_subtree): def _set_title_markup(self, column, cell, model, r_iter, index): title = model.get_value(r_iter, index) - title = rose.gtk.util.safe_str(title) + title = metomi.rose.gtk.util.safe_str(title) if len(model.get_path(r_iter)) == 1: - title = rose.config_editor.TITLE_PAGE_ROOT_MARKUP.format(title) + title = metomi.rose.config_editor.TITLE_PAGE_ROOT_MARKUP.format(title) latent_status = model.get_value(r_iter, self.COLUMN_LATENT_STATUS) ignored_status = model.get_value(r_iter, self.COLUMN_IGNORED_STATUS) name = self.get_name(model.get_path(r_iter)) preview_status = self._ask_is_preview(name) if preview_status: - title = rose.config_editor.TITLE_PAGE_PREVIEW_MARKUP.format(title) + title = metomi.rose.config_editor.TITLE_PAGE_PREVIEW_MARKUP.format(title) if latent_status: if self._get_is_latent_sub_tree(model, r_iter): - title = rose.config_editor.TITLE_PAGE_LATENT_MARKUP.format( + title = metomi.rose.config_editor.TITLE_PAGE_LATENT_MARKUP.format( title) if ignored_status: - title = rose.config_editor.TITLE_PAGE_IGNORED_MARKUP.format( + title = metomi.rose.config_editor.TITLE_PAGE_IGNORED_MARKUP.format( ignored_status, title) cell.set_property("markup", title) def sort_tree_items(self, row_item_1, row_item_2): """Sort tree items according to name and sort key.""" - sort_key_1 = row_item_1[1][1].get(rose.META_PROP_SORT_KEY, '~') - sort_key_2 = row_item_2[1][1].get(rose.META_PROP_SORT_KEY, '~') + sort_key_1 = row_item_1[1][1].get(metomi.rose.META_PROP_SORT_KEY, '~') + sort_key_2 = row_item_2[1][1].get(metomi.rose.META_PROP_SORT_KEY, '~') var_id_1 = row_item_1[0] var_id_2 = row_item_2[0] x_key = (sort_key_1, var_id_1) y_key = (sort_key_2, var_id_2) - return rose.config_editor.util.null_cmp(x_key, y_key) + return metomi.rose.config_editor.util.null_cmp(x_key, y_key) def set_row_icon(self, names, ind_count=0, ind_type='changed'): """Set the icons for row status on or off. Check parent icons. @@ -335,21 +335,21 @@ def update_row_tooltips(self): self.COLUMN_CHANGE_INTERNAL) proper_name = self.get_name(path, unfiltered=True) metadata, comment = self._get_metadata_comments_func(proper_name) - description = metadata.get(rose.META_PROP_DESCRIPTION, "") + description = metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") change = self.data_store.get_value( path_iter, self.COLUMN_CHANGE_TEXT) text = title if name != title: text += " (" + name + ")" if mods > 0: - text += " - " + rose.config_editor.TREE_PANEL_MODIFIED + text += " - " + metomi.rose.config_editor.TREE_PANEL_MODIFIED if description: text += ":\n" + description if num_errors > 0: if num_errors == 1: - text += rose.config_editor.TREE_PANEL_ERROR + text += metomi.rose.config_editor.TREE_PANEL_ERROR else: - text += rose.config_editor.TREE_PANEL_ERRORS.format( + text += metomi.rose.config_editor.TREE_PANEL_ERRORS.format( num_errors) if comment: text += "\n" + comment @@ -554,7 +554,7 @@ def expand_recursive(self, start_path=None, no_duplicates=False): start_path = treemodel.get_path(start_iter) if not no_duplicates: return self.tree.expand_row(start_path, open_all=True) - max_depth = rose.config_editor.TREE_PANEL_MAX_EXPANDED_DEPTH + max_depth = metomi.rose.config_editor.TREE_PANEL_MAX_EXPANDED_DEPTH stack = [treemodel.get_iter(start_path)] while stack: iter_ = stack.pop(0) @@ -567,8 +567,8 @@ def expand_recursive(self, start_path=None, no_duplicates=False): while child_iter is not None: child_name = self.get_name(treemodel.get_path(child_iter)) metadata = self._get_metadata_comments_func(child_name)[0] - dupl = metadata.get(rose.META_PROP_DUPLICATE) - child_dups.append(dupl == rose.META_PROP_VALUE_TRUE) + dupl = metadata.get(metomi.rose.META_PROP_DUPLICATE) + child_dups.append(dupl == metomi.rose.META_PROP_VALUE_TRUE) child_iter = treemodel.iter_next(child_iter) if path != start_path: stack.append(treemodel.iter_next(iter_)) diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index d0a08229a..cfe7d12cd 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -26,9 +26,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.config_editor.util -import rose.gtk.dialog +import metomi.rose.config +import metomi.rose.config_editor.util +import metomi.rose.gtk.dialog class NavPanelHandler(object): @@ -110,16 +110,16 @@ def copy_request(self, base_ns, new_section=None, skip_update=False): def create_request(self): """Handle a create configuration request.""" - if not any(v.config_type == rose.TOP_CONFIG_NAME + if not any(v.config_type == metomi.rose.TOP_CONFIG_NAME for v in list(self.data.config.values())): - text = rose.config_editor.WARNING_APP_CONFIG_CREATE - title = rose.config_editor.WARNING_APP_CONFIG_CREATE_TITLE - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text = metomi.rose.config_editor.WARNING_APP_CONFIG_CREATE + title = metomi.rose.config_editor.WARNING_APP_CONFIG_CREATE_TITLE + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) return False # Need an application configuration to be created. root = os.path.join(self.data.top_level_directory, - rose.SUB_CONFIGS_DIR) + metomi.rose.SUB_CONFIGS_DIR) name, meta = self.mainwindow.launch_new_config_dialog(root) if name is None: return False @@ -151,15 +151,15 @@ def ignore_request(self, base_ns, is_ignored): continue if not is_ignored: mode = sect_data.metadata.get( - rose.META_PROP_COMPULSORY) + metomi.rose.META_PROP_COMPULSORY) if (not sect_data.ignored_reason or - mode == rose.META_PROP_VALUE_TRUE): + mode == metomi.rose.META_PROP_VALUE_TRUE): continue config_sect_dict[config_name].append(section) - config_sect_dict[config_name].sort(rose.config.sort_settings) + config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - rose.config.sort_settings) + metomi.rose.config.sort_settings) config_name, section = self.mainwindow.launch_ignore_dialog( config_sect_dict, prefer_name_sections, is_ignored) if config_name in self.data.config and section is not None: @@ -179,19 +179,19 @@ def edit_request(self, base_ns): if not sections: return False if len(sections) > 1: - section = rose.gtk.dialog.run_choices_dialog( - rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_EDIT, + section = metomi.rose.gtk.dialog.run_choices_dialog( + metomi.rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_EDIT, sections, - rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) else: section = sections[0] if section is None: return False - title = rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format(section) + title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format(section) text = "\n".join(config_data.sections.now[section].comments) finish = lambda t: self.sect_ops.set_section_comments( config_name, section, t.splitlines()) - rose.gtk.dialog.run_edit_dialog(text, finish_hook=finish, title=title) + metomi.rose.gtk.dialog.run_edit_dialog(text, finish_hook=finish, title=title) def fix_request(self, base_ns): """Handle a request to auto-fix a configuration.""" @@ -223,7 +223,7 @@ def info_request(self, namespace): for section in sections: sect_data = config_data.sections.now.get(section) if sect_data is not None: - rose.config_editor.util.launch_node_info_dialog( + metomi.rose.config_editor.util.launch_node_info_dialog( sect_data, "", search_function) def graph_request(self, namespace): @@ -244,18 +244,18 @@ def remove_request(self, base_ns): for config_name in config_names: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(rose.config.sort_settings) + config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - rose.config.sort_settings) + metomi.rose.config.sort_settings) config_name, section = self.mainwindow.launch_remove_dialog( config_sect_dict, prefer_name_sections) if config_name in self.data.config and section is not None: start_stack_index = len(self.undo_stack) group = ( - rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time())) + metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time())) config_data = self.data.config[config_name] - variable_sorter = lambda v, w: rose.config.sort_settings( + variable_sorter = lambda v, w: metomi.rose.config.sort_settings( v.metadata['id'], w.metadata['id']) variables = list(config_data.vars.now.get(section, [])) variables.sort(variable_sorter) @@ -279,10 +279,10 @@ def rename_dialog(self, base_ns): for config_name in self.data.config: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(rose.config.sort_settings) + config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - rose.config.sort_settings) + metomi.rose.config.sort_settings) config_name, source_section, target_section = ( self.mainwindow.launch_rename_dialog( config_sect_dict, prefer_name_sections) @@ -306,31 +306,31 @@ def popup_panel_menu(self, base_ns, event): ui_config_string = """ """ actions = [('New', Gtk.STOCK_NEW, - rose.config_editor.TREE_PANEL_NEW_CONFIG), + metomi.rose.config_editor.TREE_PANEL_NEW_CONFIG), ('Add', Gtk.STOCK_ADD, - rose.config_editor.TREE_PANEL_ADD_GENERIC), + metomi.rose.config_editor.TREE_PANEL_ADD_GENERIC), ('Autofix', Gtk.STOCK_CONVERT, - rose.config_editor.TREE_PANEL_AUTOFIX_CONFIG), + metomi.rose.config_editor.TREE_PANEL_AUTOFIX_CONFIG), ('Clone', Gtk.STOCK_COPY, - rose.config_editor.TREE_PANEL_CLONE_SECTION), + metomi.rose.config_editor.TREE_PANEL_CLONE_SECTION), ('Edit', Gtk.STOCK_EDIT, - rose.config_editor.TREE_PANEL_EDIT_SECTION), + metomi.rose.config_editor.TREE_PANEL_EDIT_SECTION), ('Enable', Gtk.STOCK_YES, - rose.config_editor.TREE_PANEL_ENABLE_GENERIC), + metomi.rose.config_editor.TREE_PANEL_ENABLE_GENERIC), ('Graph', Gtk.STOCK_SORT_ASCENDING, - rose.config_editor.TREE_PANEL_GRAPH_SECTION), + metomi.rose.config_editor.TREE_PANEL_GRAPH_SECTION), ('Ignore', Gtk.STOCK_NO, - rose.config_editor.TREE_PANEL_IGNORE_GENERIC), + metomi.rose.config_editor.TREE_PANEL_IGNORE_GENERIC), ('Info', Gtk.STOCK_INFO, - rose.config_editor.TREE_PANEL_INFO_SECTION), + metomi.rose.config_editor.TREE_PANEL_INFO_SECTION), ('Help', Gtk.STOCK_HELP, - rose.config_editor.TREE_PANEL_HELP_SECTION), + metomi.rose.config_editor.TREE_PANEL_HELP_SECTION), ('URL', Gtk.STOCK_HOME, - rose.config_editor.TREE_PANEL_URL_SECTION), + metomi.rose.config_editor.TREE_PANEL_URL_SECTION), ('Remove', Gtk.STOCK_DELETE, - rose.config_editor.TREE_PANEL_REMOVE_GENERIC), + metomi.rose.config_editor.TREE_PANEL_REMOVE_GENERIC), ('Rename', Gtk.STOCK_COPY, - rose.config_editor.TREE_PANEL_RENAME_GENERIC)] + metomi.rose.config_editor.TREE_PANEL_RENAME_GENERIC)] url = None help_ = None is_empty = (not self.data.config) @@ -354,7 +354,7 @@ def popup_panel_menu(self, base_ns, event): action_name) actions.append( (action_name, Gtk.STOCK_ADD, - rose.config_editor.TREE_PANEL_ADD_SECTION.format( + metomi.rose.config_editor.TREE_PANEL_ADD_SECTION.format( section.replace("_", "__"))) ) ui_config_string += '' @@ -374,8 +374,8 @@ def popup_panel_menu(self, base_ns, event): ui_config_string += '' ui_config_string += '' ui_config_string += '' - url = metadata.get(rose.META_PROP_URL) - help_ = metadata.get(rose.META_PROP_HELP) + url = metadata.get(metomi.rose.META_PROP_URL) + help_ = metadata.get(metomi.rose.META_PROP_HELP) if url is not None or help_ is not None: ui_config_string += '' if url is not None: @@ -446,12 +446,12 @@ def popup_panel_menu(self, base_ns, event): if help_ is not None: help_item = uimanager.get_widget('/Popup/Help') help_title = namespace.split('/')[1:] - help_title = rose.config_editor.DIALOG_HELP_TITLE.format( + help_title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format( help_title) search_function = lambda i: self.search_request(namespace, i) help_item.connect( "activate", - lambda b: rose.gtk.dialog.run_hyperlink_dialog( + lambda b: metomi.rose.gtk.dialog.run_hyperlink_dialog( Gtk.STOCK_DIALOG_INFO, help_, help_title, search_function)) if url is not None: @@ -482,8 +482,8 @@ def is_ns_duplicate(self, namespace): sect_data = self.data.config[config_name].sections.now.get(section) if sect_data is None: return False - return (sect_data.metadata.get(rose.META_PROP_DUPLICATE) == - rose.META_PROP_VALUE_TRUE) + return (sect_data.metadata.get(metomi.rose.META_PROP_DUPLICATE) == + metomi.rose.META_PROP_VALUE_TRUE) def get_ns_errors(self, namespace): """Count the number of errors in a namespace.""" @@ -509,18 +509,18 @@ def get_can_show_page(self, latent_status, ignored_status, has_error): # Always show this. return True show_ignored = self.data.page_ns_show_modes[ - rose.config_editor.SHOW_MODE_IGNORED] + metomi.rose.config_editor.SHOW_MODE_IGNORED] show_user_ignored = self.data.page_ns_show_modes[ - rose.config_editor.SHOW_MODE_USER_IGNORED] + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED] show_latent = self.data.page_ns_show_modes[ - rose.config_editor.SHOW_MODE_LATENT] + metomi.rose.config_editor.SHOW_MODE_LATENT] if latent_status: if not show_latent: # Latent page, no latent pages allowed. return False # Latent page, latent pages allowed (but may be ignored...). if ignored_status: - if ignored_status == rose.config.ConfigNode.STATE_USER_IGNORED: + if ignored_status == metomi.rose.config.ConfigNode.STATE_USER_IGNORED: if show_ignored or show_user_ignored: # This is an allowed user-ignored page. return True diff --git a/metomi/rose/config_editor/ops/group.py b/metomi/rose/config_editor/ops/group.py index f1c1e4920..37413449f 100644 --- a/metomi/rose/config_editor/ops/group.py +++ b/metomi/rose/config_editor/ops/group.py @@ -29,8 +29,8 @@ import re import time -import rose.config -import rose.config_editor +import metomi.rose.config +import metomi.rose.config_editor class GroupOperations(object): @@ -55,16 +55,16 @@ def __init__(self, data, util, reporter, undo_stack, redo_stack, def apply_diff(self, config_name, config_diff, origin_name=None, triggers_ok=False, is_reversed=False): - """Apply a rose.config.ConfigNodeDiff object to the config.""" + """Apply a metomi.rose.config.ConfigNodeDiff object to the config.""" state_reason_dict = { - rose.config.ConfigNode.STATE_NORMAL: {}, - rose.config.ConfigNode.STATE_USER_IGNORED: { - rose.variable.IGNORED_BY_USER: - rose.config_editor.IGNORED_STATUS_MACRO + metomi.rose.config.ConfigNode.STATE_NORMAL: {}, + metomi.rose.config.ConfigNode.STATE_USER_IGNORED: { + metomi.rose.variable.IGNORED_BY_USER: + metomi.rose.config_editor.IGNORED_STATUS_MACRO }, - rose.config.ConfigNode.STATE_SYST_IGNORED: { - rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_MACRO + metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: { + metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_MACRO } } nses = [] @@ -91,7 +91,7 @@ def apply_diff(self, config_name, config_diff, origin_name=None, ids.append(var_id) metadata = self.data.helper.get_metadata_for_config_id( var_id, config_name) - variable = rose.variable.Variable(opt, value, metadata) + variable = metomi.rose.variable.Variable(opt, value, metadata) variable.comments = copy.deepcopy(comments) variable.ignored_reason = copy.deepcopy(reason) self.data.load_ns_for_node(variable, config_name) @@ -146,29 +146,29 @@ def apply_diff(self, config_name, config_diff, origin_name=None, if opt is None: ignored_changed = True is_ignored = False - if (rose.variable.IGNORED_BY_USER in old_reason and - rose.variable.IGNORED_BY_USER not in reason): + if (metomi.rose.variable.IGNORED_BY_USER in old_reason and + metomi.rose.variable.IGNORED_BY_USER not in reason): # Enable from user-ignored. is_ignored = False - elif (rose.variable.IGNORED_BY_USER not in old_reason and - rose.variable.IGNORED_BY_USER in reason): + elif (metomi.rose.variable.IGNORED_BY_USER not in old_reason and + metomi.rose.variable.IGNORED_BY_USER in reason): # User-ignore from enabled. is_ignored = True elif (triggers_ok and - rose.variable.IGNORED_BY_SYSTEM not in old_reason and - rose.variable.IGNORED_BY_SYSTEM in reason): + metomi.rose.variable.IGNORED_BY_SYSTEM not in old_reason and + metomi.rose.variable.IGNORED_BY_SYSTEM in reason): # Trigger-ignore. sect_data.error.setdefault( - rose.config_editor.WARNING_TYPE_ENABLED, - rose.config_editor.IGNORED_STATUS_MACRO) + metomi.rose.config_editor.WARNING_TYPE_ENABLED, + metomi.rose.config_editor.IGNORED_STATUS_MACRO) is_ignored = True elif (triggers_ok and - rose.variable.IGNORED_BY_SYSTEM in old_reason and - rose.variable.IGNORED_BY_SYSTEM not in reason): + metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason and + metomi.rose.variable.IGNORED_BY_SYSTEM not in reason): # Enabled from trigger-ignore. sect_data.error.setdefault( - rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED, - rose.config_editor.IGNORED_STATUS_MACRO) + metomi.rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED, + metomi.rose.config_editor.IGNORED_STATUS_MACRO) is_ignored = False else: ignored_changed = False @@ -211,10 +211,10 @@ def apply_diff(self, config_name, config_diff, origin_name=None, ) reverse_diff = config_diff.get_reversed() if is_reversed: - action = rose.config_editor.STACK_ACTION_REVERSED + action = metomi.rose.config_editor.STACK_ACTION_REVERSED else: - action = rose.config_editor.STACK_ACTION_APPLIED - stack_item = rose.config_editor.stack.StackItem( + action = metomi.rose.config_editor.STACK_ACTION_APPLIED + stack_item = metomi.rose.config_editor.stack.StackItem( None, action, reverse_diff, @@ -244,7 +244,7 @@ def add_section_with_options(self, config_name, new_section_name, """ start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time()) self.sect_ops.add_section(config_name, new_section_name, skip_update=True) namespace = self.data.helper.get_default_section_namespace( @@ -256,8 +256,8 @@ def add_section_with_options(self, config_name, new_section_name, if var.name in opt_map: var.value = opt_map.pop(var.name) if (var.name in opt_map or - (var.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE)): + (var.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE)): self.var_ops.add_var(var, skip_update=True) for opt_name, value in list(opt_map.items()): var_id = self.util.get_id_from_section_option( @@ -268,7 +268,7 @@ def add_section_with_options(self, config_name, new_section_name, flags = self.data.load_option_flags(config_name, new_section_name, opt_name) ignored_reason = {} # This may not be safe. - var = rose.variable.Variable(opt_name, value, + var = metomi.rose.variable.Variable(opt_name, value, metadata, ignored_reason, error={}, flags=flags) @@ -282,7 +282,7 @@ def copy_section(self, config_name, section, new_section=None, skip_update=False): """Copy a section and its options.""" start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_COPY + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_COPY + "-" + str(time.time()) config_data = self.data.config[config_name] section_base = re.sub(r'(.*)\(\w+\)$', r"\1", section) existing_sections = [] @@ -309,7 +309,7 @@ def copy_section(self, config_name, section, new_section=None, var_id, config_name) var.process_metadata(metadata) var.metadata['full_ns'] = new_namespace - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings clone_vars.sort(lambda v, w: sorter(v.name, w.name)) if skip_update: for var in clone_vars: @@ -326,7 +326,7 @@ def ignore_sections(self, config_name, sections, is_ignored, skip_update=False, skip_sub_data_update=True): """Implement a mass user-ignore or enable of sections.""" start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_IGNORE + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_IGNORE + "-" + str(time.time()) nses = [] for section in sections: ns = self.data.helper.get_default_section_namespace( @@ -351,7 +351,7 @@ def ignore_sections(self, config_name, sections, is_ignored, def remove_section(self, config_name, section, skip_update=False): """Implement a remove of a section and its options.""" start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) config_data = self.data.config[config_name] variables = config_data.vars.now.get(section, []) for variable in list(variables): @@ -365,7 +365,7 @@ def rename_section(self, config_name, section, target_section, skip_update=False): """Implement a rename of a section and its options.""" start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_RENAME + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_RENAME + "-" + str(time.time()) added_section = self.copy_section(config_name, section, target_section, skip_update=skip_update) @@ -379,7 +379,7 @@ def rename_section(self, config_name, section, target_section, def remove_sections(self, config_name, sections, skip_update=False): """Implement a mass removal of sections.""" start_stack_index = len(self.undo_stack) - group = rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + group = metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) nses = [] for section in sections: ns = self.data.helper.get_default_section_namespace( diff --git a/metomi/rose/config_editor/ops/section.py b/metomi/rose/config_editor/ops/section.py index 864169504..4ed738157 100644 --- a/metomi/rose/config_editor/ops/section.py +++ b/metomi/rose/config_editor/ops/section.py @@ -29,9 +29,9 @@ import gi gi.require_version('Gtk', '3.0') -import rose.config_editor.stack -import rose.gtk.dialog -import rose.gtk.util +import metomi.rose.config_editor.stack +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util class SectionOperations(object): @@ -39,15 +39,15 @@ class SectionOperations(object): """A class to hold functions that act on sections and their storage.""" def __init__(self, data, util, reporter, undo_stack, redo_stack, - check_cannot_enable_func=rose.config_editor.false_function, - update_ns_func=rose.config_editor.false_function, - update_sub_data_func=rose.config_editor.false_function, - update_info_func=rose.config_editor.false_function, - update_comments_func=rose.config_editor.false_function, - update_tree_func=rose.config_editor.false_function, - search_id_func=rose.config_editor.false_function, - view_page_func=rose.config_editor.false_function, - kill_page_func=rose.config_editor.false_function): + check_cannot_enable_func=metomi.rose.config_editor.false_function, + update_ns_func=metomi.rose.config_editor.false_function, + update_sub_data_func=metomi.rose.config_editor.false_function, + update_info_func=metomi.rose.config_editor.false_function, + update_comments_func=metomi.rose.config_editor.false_function, + update_tree_func=metomi.rose.config_editor.false_function, + search_id_func=metomi.rose.config_editor.false_function, + view_page_func=metomi.rose.config_editor.false_function, + kill_page_func=metomi.rose.config_editor.false_function): self.__data = data self.__util = util self.__reporter = reporter @@ -69,10 +69,10 @@ def add_section(self, config_name, section, skip_update=False, config_data = self.__data.config[config_name] new_section_data = None if not section or section in config_data.sections.now: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.ERROR_SECTION_ADD.format(section), - title=rose.config_editor.ERROR_SECTION_ADD_TITLE, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.ERROR_SECTION_ADD.format(section), + title=metomi.rose.config_editor.ERROR_SECTION_ADD_TITLE, modal=False) return if section in config_data.sections.latent: @@ -80,7 +80,7 @@ def add_section(self, config_name, section, skip_update=False, else: metadata = self.__data.helper.get_metadata_for_config_id( section, config_name) - new_section_data = rose.section.Section(section, [], metadata) + new_section_data = metomi.rose.section.Section(section, [], metadata) if comments is not None: new_section_data.comments = copy.deepcopy(comments) if ignored_reason is not None: @@ -100,13 +100,13 @@ def add_section(self, config_name, section, skip_update=False, ns = new_section_data.metadata["full_ns"] if not skip_update: self.trigger_reload_tree(ns) - if rose.META_PROP_DUPLICATE in metadata: + if metomi.rose.META_PROP_DUPLICATE in metadata: self.__data.load_namespace_has_sub_data(config_name) if not skip_undo: copy_section_data = new_section_data.copy() - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( ns, - rose.config_editor.STACK_ACTION_ADDED, + metomi.rose.config_editor.STACK_ACTION_ADDED, copy_section_data, self.remove_section, (config_name, section, skip_update)) @@ -136,53 +136,53 @@ def ignore_section(self, config_name, section, is_ignored, # The section must be enabled and optional. if (not override and ( sect_data.ignored_reason or - sect_data.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE)): - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.WARNING_CANNOT_USER_IGNORE.format( + sect_data.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE)): + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.WARNING_CANNOT_USER_IGNORE.format( section), - rose.config_editor.WARNING_CANNOT_IGNORE_TITLE) + metomi.rose.config_editor.WARNING_CANNOT_IGNORE_TITLE) return [], [] - for error in [rose.config_editor.WARNING_TYPE_USER_IGNORED, - rose.config_editor.WARNING_TYPE_ENABLED]: + for error in [metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED, + metomi.rose.config_editor.WARNING_TYPE_ENABLED]: if error in sect_data.error: sect_data.ignored_reason.update({ - rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_MANUAL}) + metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) sect_data.error.pop(error) break else: sect_data.ignored_reason.update({ - rose.variable.IGNORED_BY_USER: - rose.config_editor.IGNORED_STATUS_MANUAL}) - action = rose.config_editor.STACK_ACTION_IGNORED + metomi.rose.variable.IGNORED_BY_USER: + metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) + action = metomi.rose.config_editor.STACK_ACTION_IGNORED else: # Enable request for this section. # The section must not be justifiably triggered ignored. - ign_errors = [e for e in rose.config_editor.WARNING_TYPES_IGNORE - if e != rose.config_editor.WARNING_TYPE_ENABLED] + ign_errors = [e for e in metomi.rose.config_editor.WARNING_TYPES_IGNORE + if e != metomi.rose.config_editor.WARNING_TYPE_ENABLED] my_errors = list(sect_data.error.keys()) if (not override and - (rose.variable.IGNORED_BY_SYSTEM in + (metomi.rose.variable.IGNORED_BY_SYSTEM in sect_data.ignored_reason) and all([e not in my_errors for e in ign_errors]) and self.check_cannot_enable_setting(config_name, section)): - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, - rose.config_editor.WARNING_CANNOT_ENABLE.format(section), - rose.config_editor.WARNING_CANNOT_ENABLE_TITLE) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.config_editor.WARNING_CANNOT_ENABLE.format(section), + metomi.rose.config_editor.WARNING_CANNOT_ENABLE_TITLE) return [], [] sect_data.ignored_reason.clear() for error in ign_errors: if error in my_errors: sect_data.error.pop(error) - action = rose.config_editor.STACK_ACTION_ENABLED + action = metomi.rose.config_editor.STACK_ACTION_ENABLED ns = sect_data.metadata["full_ns"] copy_sect_data = sect_data.copy() if not skip_undo: - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( ns, action, copy_sect_data, @@ -199,10 +199,10 @@ def ignore_section(self, config_name, section, is_ignored, ids_to_do.append(var.metadata['id']) if is_ignored: var.ignored_reason.update( - {rose.variable.IGNORED_BY_SECTION: - rose.config_editor.IGNORED_STATUS_MANUAL}) - elif rose.variable.IGNORED_BY_SECTION in var.ignored_reason: - var.ignored_reason.pop(rose.variable.IGNORED_BY_SECTION) + {metomi.rose.variable.IGNORED_BY_SECTION: + metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) + elif metomi.rose.variable.IGNORED_BY_SECTION in var.ignored_reason: + var.ignored_reason.pop(metomi.rose.variable.IGNORED_BY_SECTION) else: continue if skip_update: @@ -229,9 +229,9 @@ def remove_section(self, config_name, section, skip_update=False, if ns not in ns_list: ns_list.append(ns) if not skip_undo: - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( namespace, - rose.config_editor.STACK_ACTION_REMOVED, + metomi.rose.config_editor.STACK_ACTION_REMOVED, old_section_data.copy(), self.add_section, (config_name, section, skip_update) @@ -254,9 +254,9 @@ def set_section_comments(self, config_name, section, comments, sect_data.comments = comments if not skip_undo: ns = sect_data.metadata["full_ns"] - stack_item = rose.config_editor.stack.StackItem( + stack_item = metomi.rose.config_editor.stack.StackItem( ns, - rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, + metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, old_sect_data, self.set_section_comments, (config_name, section, last_comments) @@ -295,21 +295,21 @@ def get_section_changes(self, section_object): save_section = config_data.sections.save.get(section) if this_section is None: if save_section is not None: - return rose.config_editor.KEY_TIP_MISSING + return metomi.rose.config_editor.KEY_TIP_MISSING # Ignore both-missing scenarios (no actual diff in output). return "" if save_section is None: - return rose.config_editor.KEY_TIP_ADDED + return metomi.rose.config_editor.KEY_TIP_ADDED if this_section.to_hashable() == save_section.to_hashable(): return "" if this_section.comments != save_section.comments: - return rose.config_editor.KEY_TIP_CHANGED_COMMENTS + return metomi.rose.config_editor.KEY_TIP_CHANGED_COMMENTS # The difference must now be in the ignored state. - if rose.variable.IGNORED_BY_SYSTEM in this_section.ignored_reason: - return rose.config_editor.KEY_TIP_TRIGGER_IGNORED - if rose.variable.IGNORED_BY_USER in this_section.ignored_reason: - return rose.config_editor.KEY_TIP_USER_IGNORED - return rose.config_editor.KEY_TIP_ENABLED + if metomi.rose.variable.IGNORED_BY_SYSTEM in this_section.ignored_reason: + return metomi.rose.config_editor.KEY_TIP_TRIGGER_IGNORED + if metomi.rose.variable.IGNORED_BY_USER in this_section.ignored_reason: + return metomi.rose.config_editor.KEY_TIP_USER_IGNORED + return metomi.rose.config_editor.KEY_TIP_ENABLED def get_ns_metadata_files(self, namespace): """Retrieve filenames within the metadata for this namespace.""" diff --git a/metomi/rose/config_editor/ops/variable.py b/metomi/rose/config_editor/ops/variable.py index 6bc7140ea..18776960c 100644 --- a/metomi/rose/config_editor/ops/variable.py +++ b/metomi/rose/config_editor/ops/variable.py @@ -28,9 +28,9 @@ import time import webbrowser -import rose.variable -import rose.config_editor -import rose.config_editor.stack +import metomi.rose.variable +import metomi.rose.config_editor +import metomi.rose.config_editor.stack class VariableOperations(object): @@ -39,10 +39,10 @@ class VariableOperations(object): def __init__(self, data, util, reporter, undo_stack, redo_stack, add_section_func, - check_cannot_enable_func=rose.config_editor.false_function, - update_ns_func=rose.config_editor.false_function, - ignore_update_func=rose.config_editor.false_function, - search_id_func=rose.config_editor.false_function): + check_cannot_enable_func=metomi.rose.config_editor.false_function, + update_ns_func=metomi.rose.config_editor.false_function, + ignore_update_func=metomi.rose.config_editor.false_function, + search_id_func=metomi.rose.config_editor.false_function): self.__data = data self.__util = util self.__reporter = reporter @@ -88,7 +88,7 @@ def add_var(self, variable, skip_update=False, skip_undo=False): group = None if sect not in config_data.sections.now: start_stack_index = len(self.__undo_stack) - group = (rose.config_editor.STACK_GROUP_ADD + "-" + + group = (metomi.rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time())) self.__add_section_func(config_name, sect) for item in self.__undo_stack[start_stack_index:]: @@ -101,9 +101,9 @@ def add_var(self, variable, skip_update=False, skip_undo=False): config_data.vars.now[sect].append(variable) if not skip_undo: self.__undo_stack.append( - rose.config_editor.stack.StackItem( + metomi.rose.config_editor.stack.StackItem( variable.metadata['full_ns'], - rose.config_editor.STACK_ACTION_ADDED, + metomi.rose.config_editor.STACK_ACTION_ADDED, copy_var, self.remove_var, [copy_var, skip_update], @@ -140,9 +140,9 @@ def remove_var(self, variable, skip_update=False, skip_undo=False): if not skip_undo: copy_var = variable.copy() self.__undo_stack.append( - rose.config_editor.stack.StackItem( + metomi.rose.config_editor.stack.StackItem( variable.metadata['full_ns'], - rose.config_editor.STACK_ACTION_REMOVED, + metomi.rose.config_editor.STACK_ACTION_REMOVED, copy_var, self.add_var, [copy_var, skip_update])) @@ -156,22 +156,22 @@ def fix_var_ignored(self, variable): ignored_reasons = list(variable.ignored_reason.keys()) new_reason_dict = {} # Enable, by default. old_reason = variable.ignored_reason.copy() - if rose.variable.IGNORED_BY_SECTION in old_reason: + if metomi.rose.variable.IGNORED_BY_SECTION in old_reason: # Preserve section-ignored status. new_reason_dict.setdefault( - rose.variable.IGNORED_BY_SECTION, - old_reason[rose.variable.IGNORED_BY_SECTION]) - if rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: + metomi.rose.variable.IGNORED_BY_SECTION, + old_reason[metomi.rose.variable.IGNORED_BY_SECTION]) + if metomi.rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: # Doc table I_t - if rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + if metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error: # Enable new_reason_dict. # Doc table I_t -> E pass - if rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + if metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: pass - elif rose.variable.IGNORED_BY_USER in ignored_reasons: + elif metomi.rose.variable.IGNORED_BY_USER in ignored_reasons: # Doc table I_u - if rose.config_editor.WARNING_TYPE_USER_IGNORED in variable.error: + if metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED in variable.error: # Enable new_reason_dict. # Doc table I_u -> I_t -> *, # I_u -> E -> compulsory, @@ -179,10 +179,10 @@ def fix_var_ignored(self, variable): pass else: # Doc table E - if rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + if metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error: # Doc table E -> I_t -> * - new_reason_dict = {rose.variable.IGNORED_BY_SYSTEM: - rose.config_editor.IGNORED_STATUS_MANUAL} + new_reason_dict = {metomi.rose.variable.IGNORED_BY_SYSTEM: + metomi.rose.config_editor.IGNORED_STATUS_MANUAL} self.set_var_ignored(variable, new_reason_dict) def set_var_ignored(self, variable, new_reason_dict=None, override=False, @@ -190,52 +190,52 @@ def set_var_ignored(self, variable, new_reason_dict=None, override=False, """Set the ignored flag data for the variable. new_reason_dict replaces the variable.ignored_reason attribute, - except for the rose.variable.IGNORED_BY_SECTION key. + except for the metomi.rose.variable.IGNORED_BY_SECTION key. """ if new_reason_dict is None: new_reason_dict = {} variable = self._get_proper_variable(variable) old_reason = variable.ignored_reason.copy() - if rose.variable.IGNORED_BY_SECTION in old_reason: + if metomi.rose.variable.IGNORED_BY_SECTION in old_reason: new_reason_dict.setdefault( - rose.variable.IGNORED_BY_SECTION, - old_reason[rose.variable.IGNORED_BY_SECTION]) - if rose.variable.IGNORED_BY_SECTION not in old_reason: - if rose.variable.IGNORED_BY_SECTION in new_reason_dict: - new_reason_dict.pop(rose.variable.IGNORED_BY_SECTION) + metomi.rose.variable.IGNORED_BY_SECTION, + old_reason[metomi.rose.variable.IGNORED_BY_SECTION]) + if metomi.rose.variable.IGNORED_BY_SECTION not in old_reason: + if metomi.rose.variable.IGNORED_BY_SECTION in new_reason_dict: + new_reason_dict.pop(metomi.rose.variable.IGNORED_BY_SECTION) variable.ignored_reason = new_reason_dict.copy() if not set(old_reason.keys()) ^ set(new_reason_dict.keys()): # No practical difference, so don't do anything. return None # Protect against user-enabling of triggered ignored. if (not override and - rose.variable.IGNORED_BY_SYSTEM in old_reason and - rose.variable.IGNORED_BY_SYSTEM not in new_reason_dict): - if rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason and + metomi.rose.variable.IGNORED_BY_SYSTEM not in new_reason_dict): + if metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: variable.error.pop( - rose.config_editor.WARNING_TYPE_NOT_TRIGGER) + metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER) my_ignored_keys = list(variable.ignored_reason.keys()) - if rose.variable.IGNORED_BY_SECTION in my_ignored_keys: - my_ignored_keys.remove(rose.variable.IGNORED_BY_SECTION) + if metomi.rose.variable.IGNORED_BY_SECTION in my_ignored_keys: + my_ignored_keys.remove(metomi.rose.variable.IGNORED_BY_SECTION) old_ignored_keys = list(old_reason.keys()) - if rose.variable.IGNORED_BY_SECTION in old_ignored_keys: - old_ignored_keys.remove(rose.variable.IGNORED_BY_SECTION) + if metomi.rose.variable.IGNORED_BY_SECTION in old_ignored_keys: + old_ignored_keys.remove(metomi.rose.variable.IGNORED_BY_SECTION) if len(my_ignored_keys) > len(old_ignored_keys): - action_text = rose.config_editor.STACK_ACTION_IGNORED + action_text = metomi.rose.config_editor.STACK_ACTION_IGNORED if (not old_ignored_keys and - rose.config_editor.WARNING_TYPE_ENABLED in variable.error): - variable.error.pop(rose.config_editor.WARNING_TYPE_ENABLED) + metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error): + variable.error.pop(metomi.rose.config_editor.WARNING_TYPE_ENABLED) else: - action_text = rose.config_editor.STACK_ACTION_ENABLED + action_text = metomi.rose.config_editor.STACK_ACTION_ENABLED if not my_ignored_keys: - for err_type in rose.config_editor.WARNING_TYPES_IGNORE: + for err_type in metomi.rose.config_editor.WARNING_TYPES_IGNORE: if err_type in variable.error: variable.error.pop(err_type) if not skip_undo: copy_var = variable.copy() self.__undo_stack.append( - rose.config_editor.stack.StackItem( + metomi.rose.config_editor.stack.StackItem( variable.metadata['full_ns'], action_text, copy_var, @@ -260,9 +260,9 @@ def set_var_value(self, variable, new_value, skip_update=False, if not skip_undo: copy_var = variable.copy() self.__undo_stack.append( - rose.config_editor.stack.StackItem( + metomi.rose.config_editor.stack.StackItem( variable.metadata['full_ns'], - rose.config_editor.STACK_ACTION_CHANGED, + metomi.rose.config_editor.STACK_ACTION_CHANGED, copy_var, self.set_var_value, [copy_var, copy_var.old_value]) @@ -281,9 +281,9 @@ def set_var_comments(self, variable, comments, variable.comments = comments if not skip_undo: self.__undo_stack.append( - rose.config_editor.stack.StackItem( + metomi.rose.config_editor.stack.StackItem( variable.metadata['full_ns'], - rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, + metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, copy_variable, self.set_var_comments, [copy_variable, old_comments]) @@ -369,37 +369,37 @@ def get_var_changes(self, variable): """Return a description of any changed status the variable has.""" if self.is_var_modified(variable): if self.is_var_added(variable): - return rose.config_editor.KEY_TIP_ADDED + return metomi.rose.config_editor.KEY_TIP_ADDED if self.is_var_ghost(variable): - return rose.config_editor.KEY_TIP_MISSING + return metomi.rose.config_editor.KEY_TIP_MISSING old_value = self.get_var_original_value(variable) if variable.value != self.get_var_original_value(variable): - return rose.config_editor.KEY_TIP_CHANGED.format(old_value) + return metomi.rose.config_editor.KEY_TIP_CHANGED.format(old_value) if self.get_var_original_comments(variable) != variable.comments: - return rose.config_editor.KEY_TIP_CHANGED_COMMENTS + return metomi.rose.config_editor.KEY_TIP_CHANGED_COMMENTS if not variable.ignored_reason: - return rose.config_editor.KEY_TIP_ENABLED + return metomi.rose.config_editor.KEY_TIP_ENABLED old_ignore = self.get_var_original_ignore(variable) if len(old_ignore) > len(variable.ignored_reason): - return rose.config_editor.KEY_TIP_ENABLED - if (rose.variable.IGNORED_BY_SYSTEM in variable.ignored_reason and - rose.variable.IGNORED_BY_SYSTEM not in old_ignore): - return rose.config_editor.KEY_TIP_TRIGGER_IGNORED - if (rose.variable.IGNORED_BY_USER in variable.ignored_reason and - rose.variable.IGNORED_BY_USER not in old_ignore): - return rose.config_editor.KEY_TIP_USER_IGNORED - if (rose.variable.IGNORED_BY_SECTION in variable.ignored_reason and - rose.variable.IGNORED_BY_SECTION not in old_ignore): - return rose.config_editor.KEY_TIP_SECTION_IGNORED - return rose.config_editor.KEY_TIP_ENABLED + return metomi.rose.config_editor.KEY_TIP_ENABLED + if (metomi.rose.variable.IGNORED_BY_SYSTEM in variable.ignored_reason and + metomi.rose.variable.IGNORED_BY_SYSTEM not in old_ignore): + return metomi.rose.config_editor.KEY_TIP_TRIGGER_IGNORED + if (metomi.rose.variable.IGNORED_BY_USER in variable.ignored_reason and + metomi.rose.variable.IGNORED_BY_USER not in old_ignore): + return metomi.rose.config_editor.KEY_TIP_USER_IGNORED + if (metomi.rose.variable.IGNORED_BY_SECTION in variable.ignored_reason and + metomi.rose.variable.IGNORED_BY_SECTION not in old_ignore): + return metomi.rose.config_editor.KEY_TIP_SECTION_IGNORED + return metomi.rose.config_editor.KEY_TIP_ENABLED return '' def launch_url(self, variable): """Determine and launch the variable help URL in a web browser.""" - if rose.META_PROP_URL not in variable.metadata: + if metomi.rose.META_PROP_URL not in variable.metadata: return - url = variable.metadata[rose.META_PROP_URL] - if rose.variable.REC_FULL_URL.match(url): + url = variable.metadata[metomi.rose.META_PROP_URL] + if metomi.rose.variable.REC_FULL_URL.match(url): # It is a proper URL by itself - launch it. return self._launch_url(url) # Must be a partial URL (e.g. '#foo') - try to prefix a parent URL. @@ -413,7 +413,7 @@ def _launch_url(self, url): try: webbrowser.open(url) except webbrowser.Error as exc: - rose.gtk.dialog.run_exception_dialog(exc) + metomi.rose.gtk.dialog.run_exception_dialog(exc) def search_for_var(self, config_name_or_namespace, setting_id): """Launch a search for a setting or variable id.""" diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index ce39a9dba..521ef3385 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -27,16 +27,16 @@ from gi.repository import Gtk from gi.repository import Pango -import rose.config_editor.panelwidget -import rose.config_editor.pagewidget -import rose.config_editor.stack -import rose.config_editor.util -import rose.config_editor.variable -import rose.formats -import rose.gtk.dialog -import rose.gtk.util -import rose.resource -import rose.variable +import metomi.rose.config_editor.panelwidget +import metomi.rose.config_editor.pagewidget +import metomi.rose.config_editor.stack +import metomi.rose.config_editor.util +import metomi.rose.config_editor.variable +import metomi.rose.formats +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util +import metomi.rose.resource +import metomi.rose.variable class ConfigPage(Gtk.VBox): @@ -62,7 +62,7 @@ def __init__(self, page_metadata, config_data, ghost_data, section_ops, self.custom_sub_widget = page_metadata.get('widget_sub_ns') self.show_modes = page_metadata.get('show_modes') self.is_duplicate = (page_metadata.get('duplicate') == - rose.META_PROP_VALUE_TRUE) + metomi.rose.META_PROP_VALUE_TRUE) self.section = None if sections: self.section = sections[0] @@ -111,7 +111,7 @@ def get_page(self): self.scrolled_main_window.add_with_viewport(self.scrolled_vbox) self.scrolled_main_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) self.scrolled_main_window.set_border_width( - rose.config_editor.SPACING_SUB_PAGE) + metomi.rose.config_editor.SPACING_SUB_PAGE) self.scrolled_vbox.pack_start(self.main_container, expand=False, fill=True) self.scrolled_main_window.show() @@ -136,7 +136,7 @@ def get_page(self): self.vpaned.pack1(self.scrolled_main_window, resize=False, shrink=True) self.vpaned.pack2(second_panel, resize=True, shrink=True) - self.vpaned.set_position(rose.config_editor.FILE_PANEL_EXPAND) + self.vpaned.set_position(metomi.rose.config_editor.FILE_PANEL_EXPAND) else: self.vpaned.pack1(self.scrolled_main_window, resize=True, shrink=True) @@ -181,8 +181,8 @@ def get_label_widget(self, is_detached=False): self.label_icon.set_from_file(self.icon_path) self.label_icon.show() label_box.pack_start(self.label_icon, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) - close_button = rose.gtk.util.CustomButton( + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + close_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_CLOSE, size=Gtk.IconSize.MENU, as_tool=True) style = Gtk.RcStyle() style.xthickness = 0 @@ -191,7 +191,7 @@ def get_label_widget(self, is_detached=False): close_button.modify_style(style) label_box.pack_start(label_event_box, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) if not is_detached: label_box.pack_end(close_button, expand=False, fill=False) label_box.show() @@ -231,7 +231,7 @@ def _set_tab_tooltip(self, event_box, event): if self.info is not None: tip_text += self.info if self.section is not None: - comment_format = rose.config_editor.VAR_COMMENT_TIP.format + comment_format = metomi.rose.config_editor.VAR_COMMENT_TIP.format for comment_line in self.section.comments: tip_text += "\n" + comment_format(comment_line) event_box.set_tooltip_text(tip_text) @@ -258,13 +258,13 @@ def launch_tab_menu(self, event): ui_config_string_start += """ """ actions = [ - ('Open', Gtk.STOCK_NEW, rose.config_editor.TAB_MENU_OPEN_NEW), - ('Info', Gtk.STOCK_INFO, rose.config_editor.TAB_MENU_INFO), - ('Edit', Gtk.STOCK_EDIT, rose.config_editor.TAB_MENU_EDIT), - ('Help', Gtk.STOCK_HELP, rose.config_editor.TAB_MENU_HELP), + ('Open', Gtk.STOCK_NEW, metomi.rose.config_editor.TAB_MENU_OPEN_NEW), + ('Info', Gtk.STOCK_INFO, metomi.rose.config_editor.TAB_MENU_INFO), + ('Edit', Gtk.STOCK_EDIT, metomi.rose.config_editor.TAB_MENU_EDIT), + ('Help', Gtk.STOCK_HELP, metomi.rose.config_editor.TAB_MENU_HELP), ('Web_Help', Gtk.STOCK_HOME, - rose.config_editor.TAB_MENU_WEB_HELP), - ('Close', Gtk.STOCK_CLOSE, rose.config_editor.TAB_MENU_CLOSE)] + metomi.rose.config_editor.TAB_MENU_WEB_HELP), + ('Close', Gtk.STOCK_CLOSE, metomi.rose.config_editor.TAB_MENU_CLOSE)] if self.help is not None: help_string = """ """ @@ -312,22 +312,22 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): sep.show() sep_vbox = Gtk.VBox() sep_vbox.pack_start(sep, expand=True, fill=True) - sep_vbox.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + sep_vbox.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) sep_vbox.show() - info_button = rose.gtk.util.CustomButton( + info_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, as_tool=True, - tip_text=rose.config_editor.TAB_MENU_INFO) + tip_text=metomi.rose.config_editor.TAB_MENU_INFO) info_button.connect("clicked", lambda m: self.launch_info()) - help_button = rose.gtk.util.CustomButton( + help_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_HELP, as_tool=True, - tip_text=rose.config_editor.TAB_MENU_HELP) + tip_text=metomi.rose.config_editor.TAB_MENU_HELP) help_button.connect("clicked", self.launch_help) - url_button = rose.gtk.util.CustomButton( + url_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_HOME, as_tool=True, - tip_text=rose.config_editor.TAB_MENU_WEB_HELP) + tip_text=metomi.rose.config_editor.TAB_MENU_WEB_HELP) url_button.connect("clicked", self.launch_url) button_hbox.pack_start(add_button, expand=False, fill=False) button_hbox.pack_start(revert_button, expand=False, fill=False) @@ -344,7 +344,7 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): button_frame.show() self.tool_hbox.pack_start(button_frame, expand=False, fill=False) label_box = Gtk.HBox(homogeneous=False, - spacing=rose.config_editor.SPACING_PAGE) + spacing=metomi.rose.config_editor.SPACING_PAGE) label_box.pack_start(self.get_label_widget(is_detached=True, True, True, 0)) label_box.show() self.tool_hbox.pack_start( @@ -361,7 +361,7 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): focus_child.grab_focus() def close_self(self): - """Delete this instance from a rose.gtk.util.Notebook.""" + """Delete this instance from a metomi.rose.gtk.util.Notebook.""" parent = self.get_parent() my_index = parent.get_page_ids().index(self.namespace) parent.remove_page(my_index) @@ -369,8 +369,8 @@ def close_self(self): def launch_help(self, *args): """Launch the page help.""" - title = rose.config_editor.DIALOG_HELP_TITLE.format(self.label) - rose.gtk.dialog.run_hyperlink_dialog( + title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format(self.label) + metomi.rose.gtk.dialog.run_hyperlink_dialog( Gtk.STOCK_DIALOG_INFO, str(self.help), title) def launch_url(self, *args): @@ -403,12 +403,12 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): var_hbox = Gtk.HBox(homogeneous=False) var_hbox.pack_start(button, expand=False, fill=False) var_hbox.pack_start(label, expand=False, fill=True, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) var_hbox.show() info_container.pack_start(var_hbox, expand=False, fill=True) # Add page help. if self.description: - help_label = rose.gtk.util.get_hyperlink_label( + help_label = metomi.rose.gtk.util.get_hyperlink_label( self.description, search_func=self.search_for_id) help_label_window = Gtk.ScrolledWindow() help_label_window.set_policy(Gtk.PolicyType.AUTOMATIC, @@ -426,18 +426,18 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): width, height = help_label_window.size_request() if info == "Blank page - no data": self.main_vpaned.set_position( - rose.config_editor.SIZE_WINDOW[1] * 100) + metomi.rose.config_editor.SIZE_WINDOW[1] * 100) else: - height = min([rose.config_editor.SIZE_WINDOW[1] / 3, + height = min([metomi.rose.config_editor.SIZE_WINDOW[1] / 3, help_label.size_request()[1]]) help_label_window.set_size_request(width, height) help_hbox = Gtk.HBox() help_hbox.pack_start(help_label_window, expand=True, fill=True, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) help_hbox.show() info_container.pack_start( help_hbox, expand=True, fill=True, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) for child in self.info_panel.get_children(): self.info_panel.remove(child) self.info_panel.pack_start(info_container, expand=True, fill=True) @@ -445,7 +445,7 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): def generate_filesystem_panel(self): """Generate a widget to view the file hierarchy.""" self.filesystem_panel = ( - rose.config_editor.panelwidget.filesystem.FileSystemPanel( + metomi.rose.config_editor.panelwidget.filesystem.FileSystemPanel( self.directory)) def generate_sub_data_panel(self, override_custom=False): @@ -465,18 +465,18 @@ def generate_sub_data_panel(self, override_custom=False): widget_path, widget_args = widget_name_args[0], None metadata_files = self.section_ops.get_ns_metadata_files( self.namespace) - widget_dir = rose.META_DIR_WIDGET + widget_dir = metomi.rose.META_DIR_WIDGET metadata_files.sort( lambda x, y: (widget_dir in y) - (widget_dir in x)) prefix = re.sub(r"[^\w]", "_", self.config_name.strip("/")) - prefix += "/" + rose.META_DIR_WIDGET + "/" - custom_widget = rose.resource.import_object( + prefix += "/" + metomi.rose.META_DIR_WIDGET + "/" + custom_widget = metomi.rose.resource.import_object( widget_path, metadata_files, self.handle_bad_custom_sub_widget, module_prefix=prefix) if custom_widget is None: - text = rose.config_editor.ERROR_IMPORT_CLASS.format( + text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format( self.custom_sub_widget) self.handle_bad_custom_sub_widget(text) return False @@ -485,15 +485,15 @@ def generate_sub_data_panel(self, override_custom=False): except Exception as exc: self.handle_bad_custom_sub_widget(str(exc)) else: - panel_module = rose.config_editor.panelwidget.summary_data + panel_module = metomi.rose.config_editor.panelwidget.summary_data self.sub_data_panel = ( panel_module.StandardSummaryDataPanel(*args)) def handle_bad_custom_sub_widget(self, error_info): - text = rose.config_editor.ERROR_IMPORT_WIDGET.format( + text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format( error_info) self.reporter( - rose.config_editor.util.ImportWidgetError(text)) + metomi.rose.config_editor.util.ImportWidgetError(text)) self.generate_sub_data_panel(override_custom=True) def update_sub_data(self): @@ -528,29 +528,29 @@ def _add_var_from_item(item): """ add_ui_end = """ """ actions = [('Add meta', Gtk.STOCK_DIRECTORY, - rose.config_editor.ADD_MENU_META)] + metomi.rose.config_editor.ADD_MENU_META)] section_choices = [] for sect_data in self.sections: if not sect_data.ignored_reason: section_choices.append(sect_data.name) - section_choices.sort(rose.config.sort_settings) + section_choices.sort(metomi.rose.config.sort_settings) if self.ns_is_default and section_choices: add_ui_start = add_ui_start.replace( "'Popup'>", """'Popup'>""") - text = rose.config_editor.ADD_MENU_BLANK + text = metomi.rose.config_editor.ADD_MENU_BLANK if len(section_choices) > 1: - text = rose.config_editor.ADD_MENU_BLANK_MULTIPLE + text = metomi.rose.config_editor.ADD_MENU_BLANK_MULTIPLE actions.insert(0, ('Add blank', Gtk.STOCK_NEW, text)) ghost_list = [v for v in self.ghost_data] - sorter = rose.config.sort_settings + sorter = metomi.rose.config.sort_settings ghost_list.sort(lambda v, w: sorter(v.metadata['id'], w.metadata['id'])) for variable in ghost_list: label_text = variable.name - if (not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE] and - rose.META_PROP_TITLE in variable.metadata): - label_text = variable.metadata[rose.META_PROP_TITLE] + if (not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE] and + metomi.rose.META_PROP_TITLE in variable.metadata): + label_text = variable.metadata[metomi.rose.META_PROP_TITLE] label_text = label_text.replace("_", "__") add_ui_start += ('') @@ -577,7 +577,7 @@ def _add_var_from_item(item): return None named_item.var_id = variable.metadata['id'] tooltip_text = "" - description = variable.metadata.get(rose.META_PROP_DESCRIPTION) + description = variable.metadata.get(metomi.rose.META_PROP_DESCRIPTION) if description: tooltip_text += description + "\n" tooltip_text += "(" + variable.metadata["id"] + ")" @@ -589,10 +589,10 @@ def _add_var_from_item(item): def _launch_section_chooser(self, section_choices): """Choose a section to add a blank variable to.""" - section = rose.gtk.dialog.run_choices_dialog( - rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR, + section = metomi.rose.gtk.dialog.run_choices_dialog( + metomi.rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR, section_choices, - rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) if section is not None: self.add_row(section=section) @@ -614,7 +614,7 @@ def add_row(self, variable=None, section=None): else: sect = section v_id = sect + '=null' + creation_time - variable = rose.variable.Variable('', '', + variable = metomi.rose.variable.Variable('', '', {'id': v_id, 'full_ns': self.namespace}) if section is None and self.section.ignored_reason: @@ -640,12 +640,12 @@ def generate_main_container(self, override_custom=False): widget_path, widget_args = widget_name_args[0], None metadata_files = self.section_ops.get_ns_metadata_files( self.namespace) - custom_widget = rose.resource.import_object( + custom_widget = metomi.rose.resource.import_object( widget_path, metadata_files, self.handle_bad_custom_main_widget) if custom_widget is None: - text = rose.config_editor.ERROR_IMPORT_CLASS.format( + text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format( widget_path) self.handle_bad_custom_main_widget(text) return @@ -659,8 +659,8 @@ def generate_main_container(self, override_custom=False): self.handle_bad_custom_main_widget(exc) else: return - std_table = rose.config_editor.pagewidget.table.PageTable - disc_table = rose.config_editor.pagewidget.table.PageLatentTable + std_table = metomi.rose.config_editor.pagewidget.table.PageTable + disc_table = metomi.rose.config_editor.pagewidget.table.PageLatentTable if self.namespace == "/discovery": self.main_container = disc_table(self.panel_data, self.ghost_data, @@ -674,10 +674,10 @@ def generate_main_container(self, override_custom=False): def handle_bad_custom_main_widget(self, error_info): """Handle a bad custom page widget import.""" - text = rose.config_editor.ERROR_IMPORT_WIDGET.format( + text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format( error_info) self.reporter.report( - rose.config_editor.util.ImportWidgetError(text)) + metomi.rose.config_editor.util.ImportWidgetError(text)) self.generate_main_container(override_custom=True) def validate_errors(self, variable_id=None): @@ -699,7 +699,7 @@ def choose_focus(self, focus_variable=None): """Select a widget to have the focus on page generation.""" if self.custom_widget is not None: return - if self.show_modes[rose.config_editor.SHOW_MODE_LATENT]: + if self.show_modes[metomi.rose.config_editor.SHOW_MODE_LATENT]: for widget in self.get_main_variable_widgets(): if hasattr(widget.get_parent(), 'variable'): if widget.get_parent().variable.name == '': @@ -746,8 +746,8 @@ def refresh(self, only_this_var_id=None): # Then it is an added ghost variable. return self.handle_add_var_widget(variable) # Then it has an existing variable widget. - if ((rose.META_PROP_TYPE in widget.errors) != - (rose.META_PROP_TYPE in variable.error) and + if ((metomi.rose.META_PROP_TYPE in widget.errors) != + (metomi.rose.META_PROP_TYPE in variable.error) and hasattr(widget, "needs_type_error_refresh") and not widget.needs_type_error_refresh()): return widget.type_error_refresh(variable) @@ -870,8 +870,8 @@ def react_to_show_modes(self, mode_key, is_mode_on): self.update_ignored() react_func = getattr(self.main_container, 'show_mode_change') react_func(mode_key, is_mode_on) - elif mode_key in [rose.config_editor.SHOW_MODE_IGNORED, - rose.config_editor.SHOW_MODE_USER_IGNORED]: + elif mode_key in [metomi.rose.config_editor.SHOW_MODE_IGNORED, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED]: self.update_ignored() else: self.refresh() @@ -893,7 +893,7 @@ def update_ignored(self, no_refresh=False): variable.ignored_reason.copy())) target_widgets_done = [] refresh_list = [] - relevant_errs = rose.config_editor.WARNING_TYPES_IGNORE + relevant_errs = metomi.rose.config_editor.WARNING_TYPES_IGNORE for widget in self.get_main_variable_widgets(): if hasattr(widget.get_parent(), 'variable'): target = widget.get_parent() @@ -928,10 +928,10 @@ def update_ignored(self, no_refresh=False): def _check_show_ignored_reason(self, ignored_reason): """Return whether we should show this state.""" mode = self.show_modes - if list(ignored_reason.keys()) == [rose.variable.IGNORED_BY_USER]: - return (mode[rose.config_editor.SHOW_MODE_IGNORED] or - mode[rose.config_editor.SHOW_MODE_USER_IGNORED]) - return mode[rose.config_editor.SHOW_MODE_IGNORED] + if list(ignored_reason.keys()) == [metomi.rose.variable.IGNORED_BY_USER]: + return (mode[metomi.rose.config_editor.SHOW_MODE_IGNORED] or + mode[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED]) + return mode[metomi.rose.config_editor.SHOW_MODE_IGNORED] def _set_widget_ignored(self, widget, help_text, enabled=False): if self._check_show_ignored_reason(widget.variable.ignored_reason): @@ -1025,19 +1025,19 @@ def sort_data(self, column_index=0, ascending=True, ghost=False): else: datavars = self.panel_data for variable in datavars: - title = variable.metadata.get(rose.META_PROP_TITLE, variable.name) + title = variable.metadata.get(metomi.rose.META_PROP_TITLE, variable.name) var_id = variable.metadata.get('id', variable.name) key = ( - variable.metadata.get(rose.META_PROP_SORT_KEY, '~'), + variable.metadata.get(metomi.rose.META_PROP_SORT_KEY, '~'), var_id ) if variable.name == '': key = ('~', '') sorted_data.append((key, title, variable.name, variable.value, variable)) - ascending_cmp = lambda x, y: rose.config_editor.util.null_cmp( + ascending_cmp = lambda x, y: metomi.rose.config_editor.util.null_cmp( x[0], y[0]) - descending_cmp = lambda x, y: rose.config_editor.util.null_cmp( + descending_cmp = lambda x, y: metomi.rose.config_editor.util.null_cmp( x[0], y[0]) if ascending: sorted_data.sort(ascending_cmp) @@ -1054,7 +1054,7 @@ def _macro_menu_launch(self, widget, event): menu = Gtk.Menu() for macro_name, info in sorted(self.custom_macros.items()): method, description = info - if method == rose.macro.TRANSFORM_METHOD: + if method == metomi.rose.macro.TRANSFORM_METHOD: stock_id = Gtk.STOCK_CONVERT else: stock_id = Gtk.STOCK_DIALOG_QUESTION @@ -1104,9 +1104,9 @@ def _get_page_info_widgets(self): self.sub_data is None and not self.latent_sections) if has_no_content: - info = rose.config_editor.PAGE_WARNING_NO_CONTENT - tip = rose.config_editor.PAGE_WARNING_NO_CONTENT_TIP - error_button = rose.gtk.util.CustomButton( + info = metomi.rose.config_editor.PAGE_WARNING_NO_CONTENT + tip = metomi.rose.config_editor.PAGE_WARNING_NO_CONTENT_TIP + error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, as_tool=True, tip_text=tip) @@ -1117,10 +1117,10 @@ def _get_page_info_widgets(self): label_list.append(error_label) if self.section is not None and self.section.ignored_reason: # This adds an ignored warning. - info = rose.config_editor.PAGE_WARNING_IGNORED_SECTION.format( + info = metomi.rose.config_editor.PAGE_WARNING_IGNORED_SECTION.format( self.section.name) - tip = rose.config_editor.PAGE_WARNING_IGNORED_SECTION_TIP - error_button = rose.gtk.util.CustomButton( + tip = metomi.rose.config_editor.PAGE_WARNING_IGNORED_SECTION_TIP + error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_NO, as_tool=True, tip_text=tip) @@ -1129,16 +1129,16 @@ def _get_page_info_widgets(self): error_label.show() button_list.append(error_button) label_list.append(error_label) - elif self.see_also == '' or rose.FILE_VAR_SOURCE not in self.see_also: + elif self.see_also == '' or metomi.rose.FILE_VAR_SOURCE not in self.see_also: # This adds an 'orphaned' warning, only if the section is enabled. if (self.section is not None and self.section.name.startswith('namelist:')): - error_button = rose.gtk.util.CustomButton( + error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_DIALOG_WARNING, as_tool=True, - tip_text=rose.config_editor.ERROR_ORPHAN_SECTION_TIP) + tip_text=metomi.rose.config_editor.ERROR_ORPHAN_SECTION_TIP) error_label = Gtk.Label() - info = rose.config_editor.ERROR_ORPHAN_SECTION.format( + info = metomi.rose.config_editor.ERROR_ORPHAN_SECTION.format( self.section.name) error_label.set_text(info) error_label.show() @@ -1154,33 +1154,33 @@ def _get_page_info_widgets(self): break if not has_data: # This is a latent namespace page. - latent_button = rose.gtk.util.CustomButton( + latent_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, as_tool=True, - tip_text=rose.config_editor.TIP_LATENT_PAGE) + tip_text=metomi.rose.config_editor.TIP_LATENT_PAGE) latent_label = Gtk.Label() - latent_label.set_text(rose.config_editor.PAGE_WARNING_LATENT) + latent_label.set_text(metomi.rose.config_editor.PAGE_WARNING_LATENT) latent_label.show() button_list.append(latent_button) label_list.append(latent_label) # This adds error notification for sections. for sect_data in self.sections + self.latent_sections: for err, info in list(sect_data.error.items()): - error_button = rose.gtk.util.CustomButton( + error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_DIALOG_ERROR, as_tool=True, tip_text=info) error_label = Gtk.Label() - error_label.set_text(rose.config_editor.PAGE_WARNING.format( + error_label.set_text(metomi.rose.config_editor.PAGE_WARNING.format( err, sect_data.name)) error_label.show() button_list.append(error_button) label_list.append(error_label) if list(self.custom_macros.items()): - macro_button = rose.gtk.util.CustomButton( - label=rose.config_editor.LABEL_PAGE_MACRO_BUTTON, + macro_button = metomi.rose.gtk.util.CustomButton( + label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON, stock_id=Gtk.STOCK_EXECUTE, - tip_text=rose.config_editor.TIP_MACRO_RUN_PAGE, + tip_text=metomi.rose.config_editor.TIP_MACRO_RUN_PAGE, as_tool=True, icon_at_start=True, has_menu=True) macro_button.connect("button-press-event", diff --git a/metomi/rose/config_editor/pagewidget/table.py b/metomi/rose/config_editor/pagewidget/table.py index a96dfeb50..a80ad59a5 100644 --- a/metomi/rose/config_editor/pagewidget/table.py +++ b/metomi/rose/config_editor/pagewidget/table.py @@ -24,11 +24,11 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.config_editor.util -import rose.config_editor.variable -import rose.formats -import rose.variable +import metomi.rose.config +import metomi.rose.config_editor.util +import metomi.rose.config_editor.variable +import metomi.rose.formats +import metomi.rose.variable class PageTable(Gtk.Table): @@ -43,7 +43,7 @@ class PageTable(Gtk.Table): MAX_ROWS = 2000 MAX_COLS = 3 - BORDER_WIDTH = rose.config_editor.SPACING_SUB_PAGE + BORDER_WIDTH = metomi.rose.config_editor.SPACING_SUB_PAGE def __init__(self, panel_data, ghost_data, var_ops, show_modes, arg_str=None): @@ -112,7 +112,7 @@ def attach_variable_widgets(self, variable_is_ghost_list, start_index=0): def get_variable_widget(self, variable, is_ghost=False): """Create a variable widget for this variable.""" - return rose.config_editor.variable.VariableWidget( + return metomi.rose.config_editor.variable.VariableWidget( variable, self.var_ops, is_ghost=is_ghost, @@ -156,7 +156,7 @@ def _get_sorted_variables(self): (val.metadata.get("sort-key", "~")), val.metadata["id"]) is_ghost = val in self.ghost_data sort_key_vars.append((sort_key, val, is_ghost)) - sort_key_vars.sort(rose.config_editor.util.null_cmp) + sort_key_vars.sort(metomi.rose.config_editor.util.null_cmp) sort_key_vars.sort(lambda x, y: cmp("=null" in x[1].metadata["id"], "=null" in y[1].metadata["id"])) return [(x[1], x[2]) for x in sort_key_vars] @@ -177,19 +177,19 @@ def _show_and_hide_variable_widgets(self, just_this_widget=None): if variable.error: variable_widget.show() elif (len(variable.metadata.get( - rose.META_PROP_VALUES, [])) == 1 and - not modes[rose.config_editor.SHOW_MODE_FIXED]): + metomi.rose.META_PROP_VALUES, [])) == 1 and + not modes[metomi.rose.config_editor.SHOW_MODE_FIXED]): variable_widget.hide() elif (variable_widget.is_ghost and - not modes[rose.config_editor.SHOW_MODE_LATENT]): + not modes[metomi.rose.config_editor.SHOW_MODE_LATENT]): variable_widget.hide() - elif ((rose.variable.IGNORED_BY_SYSTEM in ign_reason or - rose.variable.IGNORED_BY_SECTION in ign_reason) and - not modes[rose.config_editor.SHOW_MODE_IGNORED]): + elif ((metomi.rose.variable.IGNORED_BY_SYSTEM in ign_reason or + metomi.rose.variable.IGNORED_BY_SECTION in ign_reason) and + not modes[metomi.rose.config_editor.SHOW_MODE_IGNORED]): variable_widget.hide() - elif (rose.variable.IGNORED_BY_USER in ign_reason and - not (modes[rose.config_editor.SHOW_MODE_IGNORED] or - modes[rose.config_editor.SHOW_MODE_USER_IGNORED])): + elif (metomi.rose.variable.IGNORED_BY_USER in ign_reason and + not (modes[metomi.rose.config_editor.SHOW_MODE_IGNORED] or + modes[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED])): variable_widget.hide() else: variable_widget.show() @@ -232,15 +232,15 @@ def attach_variable_widgets(self, variable_is_ghost_list, start_index=0): def get_variable_widget(self, variable, is_ghost=False): """Create a variable widget for this variable.""" - if (rose.META_PROP_LENGTH in variable.metadata or - isinstance(variable.metadata.get(rose.META_PROP_TYPE), list)): - return rose.config_editor.variable.RowVariableWidget( + if (metomi.rose.META_PROP_LENGTH in variable.metadata or + isinstance(variable.metadata.get(metomi.rose.META_PROP_TYPE), list)): + return metomi.rose.config_editor.variable.RowVariableWidget( variable, self.var_ops, is_ghost=is_ghost, show_modes=self.show_modes, length=self.array_length) - return rose.config_editor.variable.VariableWidget( + return metomi.rose.config_editor.variable.VariableWidget( variable, self.var_ops, is_ghost=is_ghost, @@ -250,14 +250,14 @@ def _set_length(self): max_meta_length = 0 max_values_length = 0 for variable in self.panel_data + self.ghost_data: - length = variable.metadata.get(rose.META_PROP_LENGTH) + length = variable.metadata.get(metomi.rose.META_PROP_LENGTH) if (length is not None and length.isdigit() and int(length) > max_meta_length): max_meta_length = int(length) - types = variable.metadata.get(rose.META_PROP_TYPE) + types = variable.metadata.get(metomi.rose.META_PROP_TYPE) if isinstance(types, list) and len(types) > max_meta_length: max_meta_length = len(types) - values_length = len(rose.variable.array_split(variable.value)) + values_length = len(metomi.rose.variable.array_split(variable.value)) if values_length > max_values_length: max_values_length = values_length self.array_length = max([max_meta_length, max_values_length]) @@ -289,15 +289,15 @@ def __init__(self, panel_data, ghost_data, var_ops, show_modes, self.var_ops = var_ops self.show_modes = show_modes self.title_on = ( - not self.show_modes[rose.config_editor.SHOW_MODE_NO_TITLE]) - self.alt_menu_class = rose.config_editor.menuwidget.CheckedMenuWidget + not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]) + self.alt_menu_class = metomi.rose.config_editor.menuwidget.CheckedMenuWidget rownum = 0 v_sort_ids = [] for val in self.panel_data + self.ghost_data: v_sort_ids.append((val.metadata.get("sort-key", ""), val.metadata["id"])) v_sort_ids.sort( - lambda x, y: rose.config.sort_settings( + lambda x, y: metomi.rose.config.sort_settings( x[0] + "~" + x[1], y[0] + "~" + y[1])) v_sort_ids.sort(lambda x, y: cmp("=null" in x[1], "=null" in y[1])) for _, var_id in v_sort_ids: @@ -318,7 +318,7 @@ def __init__(self, panel_data, ghost_data, var_ops, show_modes, def get_variable_widget(self, variable, is_ghost=False): """Create a variable widget for this variable.""" - return rose.config_editor.variable.VariableWidget( + return metomi.rose.config_editor.variable.VariableWidget( variable, self.var_ops, is_ghost=is_ghost, show_modes=self.show_modes) diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index 18dd0501a..7e12079de 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -24,10 +24,10 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor -import rose.external -import rose.gtk.dialog -import rose.gtk.util +import metomi.rose.config_editor +import metomi.rose.external +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util class FileSystemPanel(Gtk.ScrolledWindow): @@ -50,18 +50,18 @@ def __init__(self, directory): this_iter = dirpath_iters[dirpath] filenames.sort() for name in filenames: - if name in rose.CONFIG_NAMES: + if name in metomi.rose.CONFIG_NAMES: continue filepath = os.path.join(dirpath, name) store.append(this_iter, [name, os.path.abspath(filepath)]) for dirname in list(dirnames): if (dirname.startswith(".") or dirname in [ - rose.SUB_CONFIGS_DIR, rose.CONFIG_META_DIR]): + metomi.rose.SUB_CONFIGS_DIR, metomi.rose.CONFIG_META_DIR]): dirnames.remove(dirname) dirnames.sort() view.set_model(store) col = Gtk.TreeViewColumn() - col.set_title(rose.config_editor.TITLE_FILE_PANEL) + col.set_title(metomi.rose.config_editor.TITLE_FILE_PANEL) cell = Gtk.CellRendererText() col.pack_start(cell, True, True, 0) col.set_cell_data_func(cell, @@ -77,11 +77,11 @@ def __init__(self, directory): def _set_path_markup(self, column, cell, model, r_iter, treestore): title = model.get_value(r_iter, 0) - title = rose.gtk.util.safe_str(title) + title = metomi.rose.gtk.util.safe_str(title) cell.set_property("markup", title) def _handle_activation(self, view=None, path=None, col=None): - target_func = rose.external.launch_fs_browser + target_func = metomi.rose.external.launch_fs_browser if path is None: target = self.directory else: @@ -90,11 +90,11 @@ def _handle_activation(self, view=None, path=None, col=None): fs_path = model.get_value(row_iter, 1) target = fs_path if not os.path.isdir(target): - target_func = rose.external.launch_geditor + target_func = metomi.rose.external.launch_geditor try: target_func(target) except Exception as exc: - rose.gtk.dialog.run_exception_dialog(exc) + metomi.rose.gtk.dialog.run_exception_dialog(exc) def _handle_click(self, view, event): pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) @@ -106,7 +106,7 @@ def _handle_click(self, view, event): """ actions = [('Open', Gtk.STOCK_OPEN, - rose.config_editor.FILE_PANEL_MENU_OPEN)] + metomi.rose.config_editor.FILE_PANEL_MENU_OPEN)] uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index 3b36cc9f2..fb1bacae2 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -23,10 +23,10 @@ from gi.repository import Gtk from gi.repository import Pango -import rose.config -import rose.config_editor -import rose.config_editor.util -import rose.gtk.util +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.config_editor.util +import metomi.rose.gtk.util class BaseSummaryDataPanel(Gtk.VBox): @@ -62,16 +62,16 @@ def __init__(self, sections, variables, sect_ops, var_ops, self.sub_ops = sub_ops self.is_duplicate = is_duplicate self.group_index = None - self.util = rose.config_editor.util.Lookup() + self.util = metomi.rose.config_editor.util.Lookup() self.control_widget_hbox = self._get_control_widget_hbox() self.pack_start(self.control_widget_hbox, expand=False, fill=False) self._prev_store = None self._prev_sort_model = None - self._view = rose.gtk.util.TooltipTreeView( + self._view = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self.set_tree_tip, multiple_selection=True) self._view.set_rules_hint(True) - self.sort_util = rose.gtk.util.TreeModelSortUtil( + self.sort_util = metomi.rose.gtk.util.TreeModelSortUtil( self._view.get_model, multi_sort_num=2) self._view.show() self._view.connect("button-press-event", @@ -147,15 +147,15 @@ def _get_custom_menu_items(self, path, column, event): def _get_control_widget_hbox(self): filter_label = Gtk.Label(label= - rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) filter_label.show() self._filter_widget = Gtk.Entry() self._filter_widget.set_width_chars( - rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) self._filter_widget.connect("changed", self._refilter) self._filter_widget.show() group_label = Gtk.Label(label= - rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() @@ -167,7 +167,7 @@ def _get_control_widget_hbox(self): filter_hbox.pack_start(group_label, expand=False, fill=False) filter_hbox.pack_start(self._group_widget, expand=False, fill=False) filter_hbox.pack_start(filter_label, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) filter_hbox.pack_start(self._filter_widget, expand=False, fill=False) filter_hbox.show() return filter_hbox @@ -307,17 +307,17 @@ def add_new_columns(self, treeview, column_names): def get_status_from_data(self, node_data): """Return markup corresponding to changes since the last save.""" text = "" - mod_markup = rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP - err_markup = rose.config_editor.SUMMARY_DATA_PANEL_ERROR_MARKUP + mod_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + err_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_MARKUP if node_data is None: return None - if rose.variable.IGNORED_BY_SYSTEM in node_data.ignored_reason: - text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP - elif rose.variable.IGNORED_BY_USER in node_data.ignored_reason: - text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP - if rose.variable.IGNORED_BY_SECTION in node_data.ignored_reason: - text += rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP - if isinstance(node_data, rose.section.Section): + if metomi.rose.variable.IGNORED_BY_SYSTEM in node_data.ignored_reason: + text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP + elif metomi.rose.variable.IGNORED_BY_USER in node_data.ignored_reason: + text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP + if metomi.rose.variable.IGNORED_BY_SECTION in node_data.ignored_reason: + text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP + if isinstance(node_data, metomi.rose.section.Section): # Modified status section = node_data.metadata["id"] if self.sect_ops.is_section_modified(node_data): @@ -335,7 +335,7 @@ def get_status_from_data(self, node_data): if var.error: text += err_markup break - elif isinstance(node_data, rose.variable.Variable): + elif isinstance(node_data, metomi.rose.variable.Variable): if self.var_ops.is_var_modified(node_data): text += mod_markup if node_data.error: @@ -435,29 +435,29 @@ def _popup_tree_multi_menu(self, event): # Ignore all. ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) ign_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) ign_menuitem.connect("activate", self._ignore_selected_sections, True) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Enable all. ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) ign_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) ign_menuitem.connect("activate", self._ignore_selected_sections, False) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Remove all. rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) rem_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) rem_menuitem.connect("activate", self._remove_selected_sections) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((rose.config_editor.ACCEL_REMOVE, rem_menuitem)) + shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem)) # list shortcut keys accel = Gtk.AccelGroup() @@ -487,7 +487,7 @@ def _popup_tree_menu(self, path, col, event): if this_section is not None: # Jump to section. menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_JUMP_TO) - label = rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( + label = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( this_section.replace("_", "__")) menuitem.set_label(label) menuitem._section = this_section @@ -512,7 +512,7 @@ def _popup_tree_menu(self, path, col, event): # Add section. add_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) add_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) add_menuitem.connect("activate", lambda i: self.add_section()) add_menuitem.show() @@ -520,80 +520,80 @@ def _popup_tree_menu(self, path, col, event): # Copy section. copy_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_COPY) copy_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) copy_menuitem.connect( "activate", lambda i: self.copy_section(this_section)) copy_menuitem.show() menu.append(copy_menuitem) - if (rose.variable.IGNORED_BY_USER in + if (metomi.rose.variable.IGNORED_BY_USER in self.sections[this_section].ignored_reason): # Enable section. enab_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) enab_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) enab_menuitem.connect( "activate", lambda i: self.sub_ops.ignore_section(this_section, False)) enab_menuitem.show() menu.append(enab_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, enab_menuitem)) else: # Ignore section. ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) ign_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) ign_menuitem.connect( "activate", lambda i: self.sub_ops.ignore_section(this_section, True)) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Remove section. rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) rem_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) rem_menuitem.connect( "activate", lambda i: self.remove_section(this_section)) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((rose.config_editor.ACCEL_REMOVE, + shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem)) else: # A group is currently selected. # Ignore all ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) ign_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) ign_menuitem.connect("activate", self._ignore_selected_sections, True) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Enable all ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) ign_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) ign_menuitem.connect("activate", self._ignore_selected_sections, False) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((rose.config_editor.ACCEL_IGNORE, + shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Delete all. rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) rem_menuitem.set_label( - rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) rem_menuitem.connect( "activate", self._remove_selected_sections) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((rose.config_editor.ACCEL_REMOVE, + shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem)) # list shortcut keys @@ -665,7 +665,7 @@ def _ignore_selected_sections(self, _, ignore=None): then enable all sections inserted. """ sections_ = self._get_selected_sections() - ignored = [rose.variable.IGNORED_BY_USER in + ignored = [metomi.rose.variable.IGNORED_BY_USER in self.sections[section_].ignored_reason for section_ in sections_] # If ignore mode is not specified decide whether to ignore or enable. @@ -785,8 +785,8 @@ def get_model_data(self): self.var_id_map[variable.metadata["id"]] = variable if variable.name not in sub_var_names: sub_var_names.append(variable.name) - sub_sect_names.sort(rose.config.sort_settings) - sub_var_names.sort(rose.config.sort_settings) + sub_sect_names.sort(metomi.rose.config.sort_settings) + sub_var_names.sort(metomi.rose.config.sort_settings) data_rows = [] for section in sub_sect_names: row_data = [section] @@ -796,12 +796,12 @@ def get_model_data(self): if var is None: row_data.append(None) else: - row_data.append(rose.gtk.util.safe_str(var.value)) + row_data.append(metomi.rose.gtk.util.safe_str(var.value)) data_rows.append(row_data) if self.is_duplicate: - sect_name = rose.config_editor.SUMMARY_DATA_PANEL_INDEX_TITLE + sect_name = metomi.rose.config_editor.SUMMARY_DATA_PANEL_INDEX_TITLE else: - sect_name = rose.config_editor.SUMMARY_DATA_PANEL_SECTION_TITLE + sect_name = metomi.rose.config_editor.SUMMARY_DATA_PANEL_SECTION_TITLE column_names = [sect_name] column_names += sub_var_names return data_rows, column_names @@ -810,7 +810,7 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): cell.set_property("visible", True) col_index = self._view.get_columns().index(column) value = self._view.get_model().get_value(iter_, col_index) - max_len = rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN + max_len = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN if value is not None and len(value) > max_len and col_index != 0: cell.set_property("width-chars", max_len) cell.set_property("ellipsize", Pango.EllipsizeMode.END) @@ -841,13 +841,13 @@ def set_tree_tip(self, view, row_iter, col_index, tip): return False id_data = self.var_id_map[id_] value = str(view.get_model().get_value(row_iter, col_index)) - tip_text = rose.CONFIG_DELIMITER.join([section, option, value]) - tip_text += id_data.metadata.get(rose.META_PROP_DESCRIPTION, "") + tip_text = metomi.rose.CONFIG_DELIMITER.join([section, option, value]) + tip_text += id_data.metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") if tip_text: tip_text += "\n" for key, value in list(id_data.error.items()): tip_text += ( - rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( key, value)) for key in id_data.ignored_reason: tip_text += key + "\n" diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index 0ea8a75ab..134fe9c27 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -26,20 +26,20 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk -import rose.config -import rose.config_editor.panelwidget.summary_data -import rose.config_tree -import rose.gtk.dialog -import rose.gtk.util -import rose.reporter -import rose.resource +import metomi.rose.config +import metomi.rose.config_editor.panelwidget.summary_data +import metomi.rose.config_tree +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util +import metomi.rose.reporter +import metomi.rose.resource -import rose.config_editor.plugin.um.widget.stash_add as stash_add -import rose.config_editor.plugin.um.widget.stash_util as stash_util +import metomi.rose.config_editor.plugin.um.widget.stash_add as stash_add +import metomi.rose.config_editor.plugin.um.widget.stash_util as stash_util class BaseStashSummaryDataPanelv1( - rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel): + metomi.rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel): """This is a base class for displaying and editing STASH requests. @@ -53,7 +53,7 @@ def get_stashmaster_lookup_dict(self): information. Subclasses *must* override the STASH_PACKAGE_PATH attribute with an - absolute path to a directory containing a rose-app.conf file with + absolute path to a directory containing a metomi.rose.app.conf file with STASH request package information. Subclasses should override the STASHMASTER_PATH attribute with an @@ -212,7 +212,7 @@ def get_model_data(self): sort_list[3] = section sub_sect_names.sort(lambda x, y: cmp(section_sort_keys.get(x), section_sort_keys.get(y))) - sub_var_names.sort(rose.config.sort_settings) + sub_var_names.sort(metomi.rose.config.sort_settings) sub_var_names.sort(lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) - (x != self.STREQ_NL_PACKAGE_OPT)) sub_var_names.sort(lambda x, y: (y == self.STREQ_NL_ITEM_OPT) - @@ -238,7 +238,7 @@ def get_model_data(self): else: desc = stash_props[self.STASH_PARSE_DESC_OPT].strip() row_data.append(desc) - is_enabled = (rose.variable.IGNORED_BY_USER not in + is_enabled = (metomi.rose.variable.IGNORED_BY_USER not in self.sections[section].ignored_reason) row_data.append(str(is_enabled)) for opt in sub_var_names: @@ -247,7 +247,7 @@ def get_model_data(self): if var is None: row_data.append(None) else: - row_data.append(rose.gtk.util.safe_str(var.value)) + row_data.append(metomi.rose.gtk.util.safe_str(var.value)) row_data.append(section) data_rows.append(row_data) # Set the column names and their ordering. @@ -282,15 +282,15 @@ def get_stashmaster_meta_map(self): if self.STASHMASTER_META_PATH is None: return {} try: - config = rose.config_tree.ConfigTreeLoader().load( + config = metomi.rose.config_tree.ConfigTreeLoader().load( self.STASHMASTER_META_PATH, self.STASHMASTER_META_FILENAME).node - except (rose.config.ConfigSyntaxError, IOError, OSError) as exc: - rose.reporter.Reporter()( + except (metomi.rose.config.ConfigSyntaxError, IOError, OSError) as exc: + metomi.rose.reporter.Reporter()( "Error loading STASHmaster metadata resource: " + type(exc).__name__ + ": " + str(exc) + "\n", - kind=rose.reporter.Reporter.KIND_ERR, - level=rose.reporter.Reporter.FAIL + kind=metomi.rose.reporter.Reporter.KIND_ERR, + level=metomi.rose.reporter.Reporter.FAIL ) return {} stash_meta_dict = {} @@ -352,7 +352,7 @@ def set_tree_tip(self, view, row_iter, col_index, tip): self._stashmaster_meta_lookup, stash_section, stash_item, value ) - help_ = metadata.get(rose.META_PROP_HELP) + help_ = metadata.get(metomi.rose.META_PROP_HELP) if help_ is not None: tip_text += "\n\n" + help_ else: @@ -364,7 +364,7 @@ def set_tree_tip(self, view, row_iter, col_index, tip): return True id_data = self.var_id_map[id_] value = str(model.get_value(row_iter, col_index)) - tip_text = rose.CONFIG_DELIMITER.join( + tip_text = metomi.rose.CONFIG_DELIMITER.join( [section, option, value]) + "\n" if (option in self.OPTION_NL_MAP and option in list(self._profile_location_map.keys())): @@ -373,12 +373,12 @@ def set_tree_tip(self, view, row_iter, col_index, tip): profile_sect = self.util.get_section_option_from_id( profile_id)[0] tip_text += "See " + profile_sect - tip_text += id_data.metadata.get(rose.META_PROP_DESCRIPTION, "") + tip_text += id_data.metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") if tip_text: tip_text += "\n" for key, value in list(id_data.error.items()): tip_text += ( - rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( key, value)) for key in id_data.ignored_reason: tip_text += "({0})\n".format(key) @@ -405,7 +405,7 @@ def _get_custom_menu_items(self, path, col, event): if col_title == self.DESCRIPTION_TITLE: meta_key = self.STASH_PARSE_DESC_OPT + "=" + str(value) metadata = self._stashmaster_meta_lookup.get(meta_key, {}) - help_ = metadata.get(rose.META_PROP_HELP) + help_ = metadata.get(metomi.rose.META_PROP_HELP) if help_ is not None: menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) menuitem.set_label(label="Help") @@ -453,8 +453,8 @@ def add_new_stash_request(self, section, item, launch_dialog=False): self.STREQ_NL_ITEM_OPT: item} new_section = self.add_section(None, opt_map=new_opt_map) if launch_dialog: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_INFO, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, "Added request as {0}".format(new_section), "New Request") @@ -495,9 +495,9 @@ def load_stash(self): """Load a STASHmaster file into data structures for later use.""" self._stash_lookup = self.get_stashmaster_lookup_dict() package_config_file = os.path.join(self.STASH_PACKAGE_PATH, - rose.SUB_CONFIG_NAME) - self.package_config = rose.config.ConfigNode() - rose.config.ConfigLoader().load_with_opts(package_config_file, + metomi.rose.SUB_CONFIG_NAME) + self.package_config = metomi.rose.config.ConfigNode() + metomi.rose.config.ConfigLoader().load_with_opts(package_config_file, self.package_config) self.generate_package_lookup() self._stashmaster_meta_lookup = ( @@ -505,11 +505,11 @@ def load_stash(self): def _add_new_diagnostic_launcher(self): # Create a button for launching the "Add new STASH" dialog. - self._add_button = rose.gtk.util.CustomButton( + self._add_button = metomi.rose.gtk.util.CustomButton( label=self.ADD_NEW_STASH_LABEL, stock_id=Gtk.STOCK_ADD, tip_text=self.ADD_NEW_STASH_TIP) - package_button = rose.gtk.util.CustomButton( + package_button = metomi.rose.gtk.util.CustomButton( label=self.PACKAGE_MANAGER_LABEL, tip_text=self.PACKAGE_MANAGER_TIP, has_menu=True) @@ -647,7 +647,7 @@ def _launch_new_diagnostic_window(self, widget=None): def _launch_record_help(self, menuitem): """Launch the help from a menu.""" - rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, + metomi.rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, menuitem._help_title) def _refresh_diagnostic_window(self): @@ -695,7 +695,7 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): if value is None: cell.set_property("markup", None) cell.set_property("visible", False) - max_len = rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN + max_len = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MAX_LEN if value is not None and len(value) > max_len and col_index != 0: cell.set_property("width-chars", max_len) cell.set_property("ellipsize", Pango.EllipsizeMode.END) @@ -704,7 +704,7 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): value = value.split("(")[-1].rstrip(")") if col_index == 0 and treemodel.iter_parent(iter_) is not None: cell.set_property("visible", False) - cell.set_property("markup", rose.gtk.util.safe_str(value)) + cell.set_property("markup", metomi.rose.gtk.util.safe_str(value)) def _update_available_profiles(self): # Retrieve which profiles (namelists like domain) are available. @@ -754,7 +754,7 @@ def _package_menu_launch(self, widget, event): for section, vars_ in list(self.variables.items()): for var in vars_: if var.name == self.STREQ_NL_PACKAGE_OPT: - is_ignored = (rose.variable.IGNORED_BY_USER in + is_ignored = (metomi.rose.variable.IGNORED_BY_USER in self.sections[section].ignored_reason) packages.setdefault(var.value, []) packages[var.value].append(is_ignored) @@ -861,7 +861,7 @@ def _packages_enable(self, only_this_package=None, disable=False): sect = self.util.get_section_option_from_id( var.metadata["id"])[0] if sect not in sections_for_changing: - is_ignored = (rose.variable.IGNORED_BY_USER in + is_ignored = (metomi.rose.variable.IGNORED_BY_USER in self.sections[sect].ignored_reason) if is_ignored != disable: sections_for_changing.append(sect) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index e1fcdc96a..9510e97d4 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -23,11 +23,11 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk -import rose.config -import rose.config_editor -import rose.gtk.util +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.gtk.util -import rose.config_editor.plugin.um.widget.stash_util as stash_util +import metomi.rose.config_editor.plugin.um.widget.stash_util as stash_util class AddStashDiagnosticsPanelv1(Gtk.VBox): @@ -99,17 +99,17 @@ def __init__(self, stash_lookup, request_lookup, for key, metadata in list(self.stash_meta_lookup.items()): if "=" in key: continue - values_string = metadata.get(rose.META_PROP_VALUES, "0, 1") - if len(rose.variable.array_split(values_string)) == 1: + values_string = metadata.get(metomi.rose.META_PROP_VALUES, "0, 1") + if len(metomi.rose.variable.array_split(values_string)) == 1: self._hidden_column_names.append(key) self._should_show_meta_column_titles = False self.control_widget_hbox = self._get_control_widget_hbox() self.pack_start(self.control_widget_hbox, expand=False, fill=False) - self._view = rose.gtk.util.TooltipTreeView( + self._view = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self.set_tree_tip) self._view.set_rules_hint(True) - self.sort_util = rose.gtk.util.TreeModelSortUtil( + self.sort_util = metomi.rose.gtk.util.TreeModelSortUtil( self._view.get_model, 2) self._view.show() self._view.connect("button-press-event", @@ -148,7 +148,7 @@ def generate_tree_view(self, is_startup=False): col_title = column_name.replace("_", "__") if self._should_show_meta_column_titles: col_meta = self.stash_meta_lookup.get(column_name, {}) - title = col_meta.get(rose.META_PROP_TITLE) + title = col_meta.get(metomi.rose.META_PROP_TITLE) if title is not None: col_title = title col.set_title(col_title) @@ -251,7 +251,7 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): return False if name == "?": name = "Requests Status" - if value == rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP: + if value == metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP: value = "changed" else: value = "no changes" @@ -260,7 +260,7 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): if stash_request_num != "None": sect_streqs = self.request_lookup.get(stash_section, {}) streqs = list(sect_streqs.get(stash_item, {}).keys()) - streqs.sort(rose.config.sort_settings) + streqs.sort(metomi.rose.config.sort_settings) if streqs: value = "\n " + "\n ".join(streqs) else: @@ -271,18 +271,18 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): metadata = stash_util.get_stash_section_meta( self.stash_meta_lookup, stash_section, stash_item, value ) - help_ = metadata.get(rose.META_PROP_HELP) + help_ = metadata.get(metomi.rose.META_PROP_HELP) meta_key = self.STASH_PARSE_DESC_OPT + "=" + value else: meta_key = name + "=" + value value_meta = self.stash_meta_lookup.get(meta_key, {}) - title = value_meta.get(rose.META_PROP_TITLE, "") + title = value_meta.get(metomi.rose.META_PROP_TITLE, "") if help_ is None: - help_ = value_meta.get(rose.META_PROP_HELP, "") + help_ = value_meta.get(metomi.rose.META_PROP_HELP, "") if title and not help_: value += "\n" + title if help_: - value += "\n" + rose.gtk.util.safe_str(help_) + value += "\n" + metomi.rose.gtk.util.safe_str(help_) text = name + ": " + str(value) + "\n\n" text += "Section: " + str(stash_section) + "\n" text += "Item: " + str(stash_item) + "\n" @@ -344,7 +344,7 @@ def _update_row_request_info(self, model, path, iter_, user_data): streqs = self.request_lookup.get(section, {}).get(item, {}) model.set_value(iter_, num_streqs_index, str(len(streqs))) streq_info = "" - mod_markup = rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + mod_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP for streq_section in streqs: if streq_section in self.changed_request_lookup: streq_info = mod_markup + streq_info @@ -399,23 +399,23 @@ def _filter_visible(self, model, iter_): def _get_control_widget_hbox(self): # Build the control widgets for the dialog. filter_label = Gtk.Label(label= - rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) filter_label.show() self._filter_widget = Gtk.Entry() self._filter_widget.set_width_chars( - rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) self._filter_widget.connect("changed", self._filter_refresh) self._filter_widget.set_tooltip_text("Filter by literal values") self._filter_widget.show() group_label = Gtk.Label(label= - rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() self._group_widget.pack_start(cell, True, True, 0) self._group_widget.add_attribute(cell, 'text', 0) self._group_widget.show() - self._add_button = rose.gtk.util.CustomButton( + self._add_button = metomi.rose.gtk.util.CustomButton( label="Add", stock_id=Gtk.STOCK_ADD, tip_text="Add a new request for this entry") @@ -423,7 +423,7 @@ def _get_control_widget_hbox(self): lambda b: self._handle_add_current_row()) self._add_button.connect("clicked", lambda b: self._handle_add_current_row()) - self._refresh_button = rose.gtk.util.CustomButton( + self._refresh_button = metomi.rose.gtk.util.CustomButton( label="Refresh", stock_id=Gtk.STOCK_REFRESH, tip_text="Refresh namelist:streq statuses") @@ -431,7 +431,7 @@ def _get_control_widget_hbox(self): lambda b: self.refresh_stash_requests()) self._refresh_button.connect("clicked", lambda b: self.refresh_stash_requests()) - self._view_button = rose.gtk.util.CustomButton( + self._view_button = metomi.rose.gtk.util.CustomButton( label="View", tip_text="Select view options", has_menu=True) @@ -526,7 +526,7 @@ def _handle_group_change(self, combobox): def _launch_record_help(self, menuitem): """Launch the help from a menu.""" - rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, + metomi.rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, menuitem._help_title) def _popup_tree_menu(self, path, col, event): @@ -548,7 +548,7 @@ def _popup_tree_menu(self, path, col, event): stash_desc_value = model.get_value(row_iter, stash_desc_index) desc_meta = self.stash_meta_lookup.get( self.STASH_PARSE_DESC_OPT + "=" + str(stash_desc_value), {}) - desc_meta_help = desc_meta.get(rose.META_PROP_HELP) + desc_meta_help = desc_meta.get(metomi.rose.META_PROP_HELP) if desc_meta_help is not None: help_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) help_menuitem.set_label("Help") @@ -565,7 +565,7 @@ def _popup_tree_menu(self, path, col, event): view_menu = Gtk.Menu() view_menu.show() view_menuitem.set_submenu(view_menu) - streqs.sort(rose.config.sort_settings) + streqs.sort(metomi.rose.config.sort_settings) for streq in streqs: view_streq_menuitem = Gtk.MenuItem(label=streq) view_streq_menuitem._section = streq @@ -612,7 +612,7 @@ def _popup_view_menu(self, widget, event): col_title = col_name.replace("_", "__") if self._should_show_meta_column_titles: col_meta = self.stash_meta_lookup.get(col_name, {}) - title = col_meta.get(rose.META_PROP_TITLE) + title = col_meta.get(metomi.rose.META_PROP_TITLE) if title is not None: col_title = title col_menuitem = Gtk.CheckMenuItem(label=col_title, @@ -639,10 +639,10 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): else: key = col_title + "=" + value value_meta = self.stash_meta_lookup.get(key, {}) - title = value_meta.get(rose.META_PROP_TITLE, "") + title = value_meta.get(metomi.rose.META_PROP_TITLE, "") if title: value = title - desc = value_meta.get(rose.META_PROP_DESCRIPTION, "") + desc = value_meta.get(metomi.rose.META_PROP_DESCRIPTION, "") if desc: value += ": " + desc max_len = 36 @@ -652,7 +652,7 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): if col_index == 0 and treemodel.iter_parent(iter_) is not None: cell.set_property("visible", False) if value is not None and col_title != "?": - value = rose.gtk.util.safe_str(value) + value = metomi.rose.gtk.util.safe_str(value) cell.set_property("markup", value) def _sort_row_data(self, row1, row2, sort_index, descending=False): diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index e25be045d..55118cb00 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -24,7 +24,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor +import metomi.rose.config_editor class StackItem(object): @@ -65,28 +65,28 @@ class StackViewer(Gtk.Window): def __init__(self, undo_stack, redo_stack, undo_func): """Load a view of the stack.""" super(StackViewer, self).__init__() - self.set_title(rose.config_editor.STACK_VIEW_TITLE) + self.set_title(metomi.rose.config_editor.STACK_VIEW_TITLE) self.action_colour_map = { - rose.config_editor.STACK_ACTION_ADDED: - rose.config_editor.COLOUR_STACK_ADDED, - rose.config_editor.STACK_ACTION_APPLIED: - rose.config_editor.COLOUR_STACK_APPLIED, - rose.config_editor.STACK_ACTION_CHANGED: - rose.config_editor.COLOUR_STACK_CHANGED, - rose.config_editor.STACK_ACTION_CHANGED_COMMENTS: - rose.config_editor.COLOUR_STACK_CHANGED_COMMENTS, - rose.config_editor.STACK_ACTION_ENABLED: - rose.config_editor.COLOUR_STACK_ENABLED, - rose.config_editor.STACK_ACTION_IGNORED: - rose.config_editor.COLOUR_STACK_IGNORED, - rose.config_editor.STACK_ACTION_REMOVED: - rose.config_editor.COLOUR_STACK_REMOVED, - rose.config_editor.STACK_ACTION_REVERSED: - rose.config_editor.COLOUR_STACK_REVERSED} + metomi.rose.config_editor.STACK_ACTION_ADDED: + metomi.rose.config_editor.COLOUR_STACK_ADDED, + metomi.rose.config_editor.STACK_ACTION_APPLIED: + metomi.rose.config_editor.COLOUR_STACK_APPLIED, + metomi.rose.config_editor.STACK_ACTION_CHANGED: + metomi.rose.config_editor.COLOUR_STACK_CHANGED, + metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS: + metomi.rose.config_editor.COLOUR_STACK_CHANGED_COMMENTS, + metomi.rose.config_editor.STACK_ACTION_ENABLED: + metomi.rose.config_editor.COLOUR_STACK_ENABLED, + metomi.rose.config_editor.STACK_ACTION_IGNORED: + metomi.rose.config_editor.COLOUR_STACK_IGNORED, + metomi.rose.config_editor.STACK_ACTION_REMOVED: + metomi.rose.config_editor.COLOUR_STACK_REMOVED, + metomi.rose.config_editor.STACK_ACTION_REVERSED: + metomi.rose.config_editor.COLOUR_STACK_REVERSED} self.undo_func = undo_func self.undo_stack = undo_stack self.redo_stack = redo_stack - self.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + self.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) self.main_vbox = Gtk.VPaned() accelerators = Gtk.AccelGroup() accel_key, accel_mods = Gtk.accelerator_parse("Z") @@ -97,7 +97,7 @@ def __init__(self, undo_stack, redo_stack, undo_func): lambda a, b, c, d: self.undo_from_log(redo_mode_on=True)) self.add_accel_group(accelerators) - self.set_default_size(*rose.config_editor.SIZE_STACK) + self.set_default_size(*metomi.rose.config_editor.SIZE_STACK) self.undo_view = self.get_stack_view(redo_mode_on=False) self.redo_view = self.get_stack_view(redo_mode_on=True) undo_vbox = self.get_stack_view_box(self.undo_view, @@ -132,9 +132,9 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): label.set_text('UNDO STACK') self.undo_text_view = text_view label.show() - vbox.set_border_width(rose.config_editor.SPACING_SUB_PAGE) + vbox.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) vbox.pack_start(label, expand=False, fill=True, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) vbox.pack_start(text_scroller, expand=True, fill=True) vbox.show() return vbox @@ -150,11 +150,11 @@ def get_stack_view(self, redo_mode_on=False): stack_view = Gtk.TreeView(stack_model) columns = {} cell_text = {} - for title in [rose.config_editor.STACK_COL_NS, - rose.config_editor.STACK_COL_ACT, - rose.config_editor.STACK_COL_NAME, - rose.config_editor.STACK_COL_VALUE, - rose.config_editor.STACK_COL_OLD_VALUE]: + for title in [metomi.rose.config_editor.STACK_COL_NS, + metomi.rose.config_editor.STACK_COL_ACT, + metomi.rose.config_editor.STACK_COL_NAME, + metomi.rose.config_editor.STACK_COL_VALUE, + metomi.rose.config_editor.STACK_COL_OLD_VALUE]: columns[title] = Gtk.TreeViewColumn() columns[title].set_title(title) cell_text[title] = Gtk.CellRendererText() diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index c15d275e6..d5755eb61 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -26,22 +26,22 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.config_editor -import rose.gtk.console -import rose.reporter +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.gtk.console +import metomi.rose.reporter -class StatusReporter(rose.reporter.Reporter): +class StatusReporter(metomi.rose.reporter.Reporter): """Handle event notification. - load_updater must be a rose.gtk.splash.SplashScreenProcess + load_updater must be a metomi.rose.gtk.splash.SplashScreenProcess instance (or have the same interface to update and stop methods). status_bar_update_func must be a function that accepts a - rose.reporter.Event, a rose.reporter kind-of-event string, and a - level of importance/verbosity. See rose.reporter for more details. + metomi.rose.reporter.Event, a metomi.rose.reporter kind-of-event string, and a + level of importance/verbosity. See metomi.rose.reporter for more details. """ @@ -56,7 +56,7 @@ def event_handler(self, message, kind=None, level=None, prefix=None, clip=None): """Handle a message or event.""" message_kwargs = {} - if isinstance(message, rose.reporter.Event): + if isinstance(message, metomi.rose.reporter.Event): if kind is None: kind = message.kind if level is None: @@ -68,8 +68,8 @@ def event_handler(self, message, kind=None, level=None, prefix=None, def report_load_event( self, text, no_progress=False, new_total_events=None): - """Report a load-related event (to rose.gtk.util.SplashScreen).""" - event = rose.reporter.Event(text, + """Report a load-related event (to metomi.rose.gtk.util.SplashScreen).""" + event = metomi.rose.reporter.Event(text, kind=self.EVENT_KIND_LOAD, no_progress=no_progress, new_total_events=new_total_events) @@ -87,7 +87,7 @@ class StatusBar(Gtk.VBox): """Generate the status bar widget.""" - def __init__(self, verbosity=rose.reporter.Reporter.DEFAULT): + def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): super(StatusBar, self).__init__() self.verbosity = verbosity self.num_errors = 0 @@ -105,12 +105,12 @@ def __init__(self, verbosity=rose.reporter.Reporter.DEFAULT): hbox.pack_start(vsep_eb, expand=True, fill=True) self._generate_message_widget() hbox.pack_end(self._message_widget, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) self.messages = [] self.show() def set_message(self, message, kind=None, level=None): - if isinstance(message, rose.reporter.Event): + if isinstance(message, metomi.rose.reporter.Event): if kind is None: kind = message.kind if level is None: @@ -118,10 +118,10 @@ def set_message(self, message, kind=None, level=None): if level > self.verbosity: return if isinstance(message, Exception): - kind = rose.reporter.Reporter.KIND_ERR - level = rose.reporter.Reporter.FAIL + kind = metomi.rose.reporter.Reporter.KIND_ERR + level = metomi.rose.reporter.Reporter.FAIL self.messages.append((kind, str(message), time.time())) - if len(self.messages) > rose.config_editor.STATUS_BAR_MESSAGE_LIMIT: + if len(self.messages) > metomi.rose.config_editor.STATUS_BAR_MESSAGE_LIMIT: self.messages.pop(0) self._update_message_widget(str(message), kind=kind) self._update_console() @@ -140,9 +140,9 @@ def _generate_error_widget(self): # Generate the error display widget. self._error_widget = Gtk.HBox() self._error_widget.show() - locator = rose.resource.ResourceLocator(paths=sys.path) + locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate( - 'etc/images/rose-config-edit/error_icon.xpm') + 'etc/images/metomi.rose.config-edit/error_icon.xpm') image = Gtk.image_new_from_file(icon_path) image.show() self._error_widget.pack_start(image, expand=False, fill=False) @@ -150,7 +150,7 @@ def _generate_error_widget(self): self._error_widget_label.show() self._error_widget.pack_start( self._error_widget_label, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) self._update_error_widget() def _generate_message_widget(self): @@ -172,10 +172,10 @@ def _generate_message_widget(self): self._message_widget_label.show() vsep = Gtk.VSeparator() vsep.show() - self._console_launcher = rose.gtk.util.CustomButton( + self._console_launcher = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, size=Gtk.IconSize.MENU, - tip_text=rose.config_editor.STATUS_BAR_CONSOLE_TIP, + tip_text=metomi.rose.config_editor.STATUS_BAR_CONSOLE_TIP, as_tool=True) self._console_launcher.connect("clicked", self._launch_console) style = Gtk.RcStyle() @@ -192,10 +192,10 @@ def _generate_message_widget(self): message_hbox.pack_start( self._message_widget_label, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) message_hbox.pack_start( vsep, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) message_hbox.pack_start( self._console_launcher, expand=False, fill=False) @@ -206,7 +206,7 @@ def _update_error_widget(self): def _update_message_widget(self, message_text, kind): # Update the message display widget. - if kind == rose.reporter.Reporter.KIND_ERR: + if kind == metomi.rose.reporter.Reporter.KIND_ERR: self._message_widget_error_image.show() self._message_widget_info_image.hide() else: @@ -218,22 +218,22 @@ def _update_message_widget(self, message_text, kind): def _handle_enter_message_widget(self, *args): tooltip_text = "" for kind, message_text, message_time in self.messages[-5:]: - if kind == rose.reporter.Reporter.KIND_ERR: - prefix = rose.reporter.Reporter.PREFIX_FAIL + if kind == metomi.rose.reporter.Reporter.KIND_ERR: + prefix = metomi.rose.reporter.Reporter.PREFIX_FAIL else: - prefix = rose.reporter.Reporter.PREFIX_INFO + prefix = metomi.rose.reporter.Reporter.PREFIX_INFO suffix = datetime.datetime.fromtimestamp(message_time).strftime( - rose.config_editor.EVENT_TIME) + metomi.rose.config_editor.EVENT_TIME) tooltip_text += prefix + " " + message_text + " " + suffix + "\n" tooltip_text = tooltip_text.rstrip() self._message_widget_label.set_tooltip_text(tooltip_text) def _get_console_messages(self): - err_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR - info_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + err_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + info_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO message_tuples = [] for kind, message, time_info in self.messages: - if kind == rose.reporter.Reporter.KIND_ERR: + if kind == metomi.rose.reporter.Reporter.KIND_ERR: category = err_category else: category = info_category @@ -247,10 +247,10 @@ def _launch_console(self, *args): if self.console is not None: return self.console.present() message_tuples = self._get_console_messages() - err_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR - info_category = rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + err_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + info_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO window = self.get_toplevel() - self.console = rose.gtk.console.ConsoleWindow( + self.console = metomi.rose.gtk.console.ConsoleWindow( [err_category, info_category], message_tuples, [Gtk.STOCK_DIALOG_ERROR, Gtk.STOCK_DIALOG_INFO], parent=window, diff --git a/metomi/rose/config_editor/updater.py b/metomi/rose/config_editor/updater.py index b5634b1f4..514a276f7 100644 --- a/metomi/rose/config_editor/updater.py +++ b/metomi/rose/config_editor/updater.py @@ -18,7 +18,7 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- -import rose.config_editor +import metomi.rose.config_editor class Updater(object): @@ -54,7 +54,7 @@ def namespace_data_is_modified(self, namespace): if config_name == namespace: # This is the top-level. if config_name not in self.data.saved_config_names: - return rose.config_editor.TREE_PANEL_TIP_ADDED_CONFIG + return metomi.rose.config_editor.TREE_PANEL_TIP_ADDED_CONFIG section_hashes = [] for sect_data in list(config_sections.now.values()): section_hashes.append(sect_data.to_hashable()) @@ -62,7 +62,7 @@ def namespace_data_is_modified(self, namespace): for sect_data in list(config_sections.save.values()): old_section_hashes.append(sect_data.to_hashable()) if set(section_hashes) ^ set(old_section_hashes): - return rose.config_editor.TREE_PANEL_TIP_CHANGED_CONFIG + return metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_CONFIG allowed_sections = self.data.helper.get_sections_from_namespace( namespace) save_var_map = {} @@ -75,25 +75,25 @@ def namespace_data_is_modified(self, namespace): var_id = var.metadata['id'] save_var = save_var_map.get(var_id) if save_var is None: - return rose.config_editor.TREE_PANEL_TIP_ADDED_VARS + return metomi.rose.config_editor.TREE_PANEL_TIP_ADDED_VARS if save_var.to_hashable() != var.to_hashable(): # Variable has changed in some form. - return rose.config_editor.TREE_PANEL_TIP_CHANGED_VARS + return metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_VARS save_var_map.pop(var_id) if save_var_map: # Some variables are now absent. - return rose.config_editor.TREE_PANEL_TIP_REMOVED_VARS + return metomi.rose.config_editor.TREE_PANEL_TIP_REMOVED_VARS if self.data.helper.get_ns_is_default(namespace): sections = self.data.helper.get_sections_from_namespace(namespace) for section in sections: sect_data = config_sections.now.get(section) save_sect_data = config_sections.save.get(section) if (sect_data is None) != (save_sect_data is None): - return rose.config_editor.TREE_PANEL_TIP_DIFF_SECTIONS + return metomi.rose.config_editor.TREE_PANEL_TIP_DIFF_SECTIONS if sect_data is not None and save_sect_data is not None: if sect_data.to_hashable() != save_sect_data.to_hashable(): return ( - rose.config_editor.TREE_PANEL_TIP_CHANGED_SECTIONS) + metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_SECTIONS) return "" def update_ns_tree_states(self, namespace): @@ -439,44 +439,44 @@ def update_ignoreds(self, config_name, var_id): sect_data = config_sections.now.get(section) if sect_data is None: sect_data = config_sections.latent[section] - for attribute in rose.config_editor.WARNING_TYPES_IGNORE: + for attribute in metomi.rose.config_editor.WARNING_TYPES_IGNORE: if attribute in sect_data.error: sect_data.error.pop(attribute) reason = sect_data.ignored_reason if section in enabled_sections: # Trigger-enabled sections - if (rose.variable.IGNORED_BY_USER in reason): + if (metomi.rose.variable.IGNORED_BY_USER in reason): # User-ignored but trigger-enabled if (meta_config.get( - [section, rose.META_PROP_COMPULSORY]).value == - rose.META_PROP_VALUE_TRUE): + [section, metomi.rose.META_PROP_COMPULSORY]).value == + metomi.rose.META_PROP_VALUE_TRUE): # Doc table: I_u -> E -> compulsory sect_data.error.update( - {rose.config_editor.WARNING_TYPE_USER_IGNORED: - rose.config_editor.WARNING_NOT_USER_IGNORABLE}) - elif (rose.variable.IGNORED_BY_SYSTEM in reason): + {metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED: + metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE}) + elif (metomi.rose.variable.IGNORED_BY_SYSTEM in reason): # Normal trigger-enabled sections - reason.pop(rose.variable.IGNORED_BY_SYSTEM) + reason.pop(metomi.rose.variable.IGNORED_BY_SYSTEM) for var in sect_vars: name = var.metadata['full_ns'] if name not in triggered_ns_list: triggered_ns_list.append(name) var.ignored_reason.pop( - rose.variable.IGNORED_BY_SECTION, None) + metomi.rose.variable.IGNORED_BY_SECTION, None) elif section in trigger.ignored_dict: # Trigger-ignored sections parents = trigger.ignored_dict.get(section, {}) if parents: help_text = "; ".join(list(parents.values())) else: - help_text = rose.config_editor.IGNORED_STATUS_DEFAULT - reason.update({rose.variable.IGNORED_BY_SYSTEM: help_text}) + help_text = metomi.rose.config_editor.IGNORED_STATUS_DEFAULT + reason.update({metomi.rose.variable.IGNORED_BY_SYSTEM: help_text}) for var in sect_vars: name = var.metadata['full_ns'] if name not in triggered_ns_list: triggered_ns_list.append(name) var.ignored_reason.update( - {rose.variable.IGNORED_BY_SECTION: help_text}) + {metomi.rose.variable.IGNORED_BY_SECTION: help_text}) # Update the variables. for var in update_vars: var_id = var.metadata.get('id') @@ -485,34 +485,34 @@ def update_ignoreds(self, config_name, var_id): triggered_ns_list.append(name) if var_id == this_id: continue - for attribute in rose.config_editor.WARNING_TYPES_IGNORE: + for attribute in metomi.rose.config_editor.WARNING_TYPES_IGNORE: if attribute in var.error: var.error.pop(attribute) if (var_id in trigger.enabled_dict and var_id not in trigger.ignored_dict): # Trigger-enabled variables - if rose.variable.IGNORED_BY_USER in var.ignored_reason: + if metomi.rose.variable.IGNORED_BY_USER in var.ignored_reason: # User-ignored but trigger-enabled # Doc table: I_u -> E - if (var.metadata.get(rose.META_PROP_COMPULSORY) == - rose.META_PROP_VALUE_TRUE): + if (var.metadata.get(metomi.rose.META_PROP_COMPULSORY) == + metomi.rose.META_PROP_VALUE_TRUE): # Doc table: I_u -> E -> compulsory var.error.update( - {rose.config_editor.WARNING_TYPE_USER_IGNORED: - rose.config_editor.WARNING_NOT_USER_IGNORABLE}) - elif (rose.variable.IGNORED_BY_SYSTEM in + {metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED: + metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE}) + elif (metomi.rose.variable.IGNORED_BY_SYSTEM in var.ignored_reason): # Normal trigger-enabled variables - var.ignored_reason.pop(rose.variable.IGNORED_BY_SYSTEM) + var.ignored_reason.pop(metomi.rose.variable.IGNORED_BY_SYSTEM) elif var_id in trigger.ignored_dict: # Trigger-ignored variables parents = trigger.ignored_dict.get(var_id, {}) if parents: help_text = "; ".join(list(parents.values())) else: - help_text = rose.config_editor.IGNORED_STATUS_DEFAULT + help_text = metomi.rose.config_editor.IGNORED_STATUS_DEFAULT var.ignored_reason.update( - {rose.variable.IGNORED_BY_SYSTEM: help_text}) + {metomi.rose.variable.IGNORED_BY_SYSTEM: help_text}) for namespace in triggered_ns_list: self.update_tree_status(namespace) return update_ids @@ -562,7 +562,7 @@ def update_stack_viewer_if_open(self): if self.is_pluggable: return False if isinstance(self.mainwindow.log_window, - rose.config_editor.stack.StackViewer): + metomi.rose.config_editor.stack.StackViewer): self.mainwindow.log_window.update() def focus_sub_page_if_open(self, namespace, node_id): @@ -591,14 +591,14 @@ def perform_startup_check(self): macro_config = self.data.dump_to_internal_config(config_name) meta_config = self.data.config[config_name].meta # Duplicate checking - dupl_checker = rose.macros.duplicate.DuplicateChecker() + dupl_checker = metomi.rose.macros.duplicate.DuplicateChecker() problem_list = dupl_checker.validate(macro_config, meta_config) if problem_list: self.main_handle.handle_macro_validation( config_name, 'duplicate.DuplicateChecker.validate', macro_config, problem_list, no_display=True) - format_checker = rose.macros.format.FormatChecker() + format_checker = metomi.rose.macros.format.FormatChecker() problem_list = format_checker.validate(macro_config, meta_config) if problem_list: self.main_handle.handle_macro_validation( @@ -618,7 +618,7 @@ def perform_error_check(self, namespace=None, is_loading=False): meta = config_data.meta checker = ( self.data.builtin_macros[config_name][ - rose.META_PROP_COMPULSORY]) + metomi.rose.META_PROP_COMPULSORY]) only_these_sections = None if namespace is not None: only_these_sections = ( @@ -632,7 +632,7 @@ def perform_error_check(self, namespace=None, is_loading=False): only_these_sections=only_these_sections ) self.apply_macro_validation(config_name, - rose.META_PROP_COMPULSORY, bad_list, + metomi.rose.META_PROP_COMPULSORY, bad_list, namespace, is_loading=is_loading, is_macro_dynamic=True) # Value checking. @@ -640,14 +640,14 @@ def perform_error_check(self, namespace=None, is_loading=False): config_data = self.data.config[config_name] meta = config_data.meta checker = ( - self.data.builtin_macros[config_name][rose.META_PROP_TYPE]) + self.data.builtin_macros[config_name][metomi.rose.META_PROP_TYPE]) if namespace is None: real_variables = config_data.vars.get_all(skip_latent=True) else: real_variables = ( self.data.helper.get_data_for_namespace(namespace)[0]) bad_list = checker.validate_variables(real_variables, meta) - self.apply_macro_validation(config_name, rose.META_PROP_TYPE, + self.apply_macro_validation(config_name, metomi.rose.META_PROP_TYPE, bad_list, namespace, is_loading=is_loading, is_macro_dynamic=True) @@ -738,7 +738,7 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, map_ = id_error_dict if is_loading: self.load_errors += 1 - update_text = rose.config_editor.EVENT_LOAD_ERRORS.format( + update_text = metomi.rose.config_editor.EVENT_LOAD_ERRORS.format( self.data.top_level_name, self.load_errors) self.reporter.report_load_event(update_text, diff --git a/metomi/rose/config_editor/upgrade_controller.py b/metomi/rose/config_editor/upgrade_controller.py index 7504ecaa8..afaee0444 100644 --- a/metomi/rose/config_editor/upgrade_controller.py +++ b/metomi/rose/config_editor/upgrade_controller.py @@ -26,9 +26,9 @@ from gi.repository import Gtk from gi.repository import GObject -import rose.gtk.util -import rose.macro -import rose.upgrade +import metomi.rose.gtk.util +import metomi.rose.macro +import metomi.rose.upgrade class UpgradeController(object): @@ -41,7 +41,7 @@ def __init__(self, app_config_dict, handle_transform_func, Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT) self.window = Gtk.Dialog(buttons=buttons) self.set_transient_for(parent_window) - self.window.set_title(rose.config_editor.DIALOG_TITLE_UPGRADE) + self.window.set_title(metomi.rose.config_editor.DIALOG_TITLE_UPGRADE) self.config_dict = {} self.config_directory_dict = {} self.config_manager_dict = {} @@ -49,20 +49,20 @@ def __init__(self, app_config_dict, handle_transform_func, self._config_version_model_dict = {} self.use_all_versions = False self.treemodel = Gtk.TreeStore(str, str, str, bool) - self.treeview = rose.gtk.util.TooltipTreeView( + self.treeview = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self._get_tooltip) self.treeview.show() old_pwd = os.getcwd() for config_name in config_names: app_config = app_config_dict[config_name]["config"] app_directory = app_config_dict[config_name]["directory"] - meta_value = app_config.get_value([rose.CONFIG_SECT_TOP, - rose.CONFIG_OPT_META_TYPE], "") + meta_value = app_config.get_value([metomi.rose.CONFIG_SECT_TOP, + metomi.rose.CONFIG_OPT_META_TYPE], "") if len(meta_value.split("/")) < 2: continue try: os.chdir(app_directory) - manager = rose.upgrade.MacroUpgradeManager(app_config) + manager = metomi.rose.upgrade.MacroUpgradeManager(app_config) except OSError: # This can occur when access is not allowed to metadata files. continue @@ -77,7 +77,7 @@ def __init__(self, app_config_dict, handle_transform_func, self.treewindow.show() self.treewindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) - columns = rose.config_editor.DIALOG_COLUMNS_UPGRADE + columns = metomi.rose.config_editor.DIALOG_COLUMNS_UPGRADE for i, title in enumerate(columns): column = Gtk.TreeViewColumn() column.set_title(title) @@ -109,15 +109,15 @@ def __init__(self, app_config_dict, handle_transform_func, self.treewindow.add(self.treeview) self.window.vbox.pack_start( self.treewindow, expand=True, fill=True, - padding=rose.config_editor.SPACING_PAGE) - label = Gtk.Label(label=rose.config_editor.DIALOG_LABEL_UPGRADE) + padding=metomi.rose.config_editor.SPACING_PAGE) + label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_LABEL_UPGRADE) label.show() self.window.vbox.pack_start( - label, True, True, rose.config_editor.SPACING_PAGE) + label, True, True, metomi.rose.config_editor.SPACING_PAGE) button_hbox = Gtk.HBox() button_hbox.show() all_versions_toggle_button = Gtk.CheckButton( - label=rose.config_editor.DIALOG_LABEL_UPGRADE_ALL, + label=metomi.rose.config_editor.DIALOG_LABEL_UPGRADE_ALL, use_underline=False) all_versions_toggle_button.set_active(self.use_all_versions) all_versions_toggle_button.connect("toggled", @@ -125,16 +125,16 @@ def __init__(self, app_config_dict, handle_transform_func, all_versions_toggle_button.show() button_hbox.pack_start(all_versions_toggle_button, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) self.window.vbox.pack_end(button_hbox, expand=False, fill=False) self.ok_button = self.window.action_area.get_children()[0] self.window.set_focus(all_versions_toggle_button) self.window.set_focus(self.ok_button) self._set_ok_to_upgrade() - max_size = rose.config_editor.SIZE_MACRO_DIALOG_MAX + max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX my_size = self.window.size_request() new_size = [-1, -1] - extra = 2 * rose.config_editor.SPACING_PAGE + extra = 2 * metomi.rose.config_editor.SPACING_PAGE for i in [0, 1]: new_size[i] = min([my_size[i] + extra, max_size[i]]) self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) @@ -161,33 +161,33 @@ def __init__(self, app_config_dict, handle_transform_func, new_config, change_list = manager.transform( macro_config, custom_inspector=upgrade_inspector) except Exception as exc: - rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_ERROR, + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, type(exc).__name__ + ": " + str(exc), - rose.config_editor.ERROR_UPGRADE.format( + metomi.rose.config_editor.ERROR_UPGRADE.format( config_name.lstrip("/")) ) iter_ = self.treemodel.iter_next(iter_) continue macro_id = (type(manager).__name__ + "." + - rose.macro.TRANSFORM_METHOD) + metomi.rose.macro.TRANSFORM_METHOD) if handle_transform_func(config_name, macro_id, new_config, change_list, triggers_ok=True): - meta_config = rose.macro.load_meta_config( - new_config, config_type=rose.SUB_CONFIG_NAME, + meta_config = metomi.rose.macro.load_meta_config( + new_config, config_type=metomi.rose.SUB_CONFIG_NAME, ignore_meta_error=True ) - trig_macro = rose.macros.trigger.TriggerMacro() + trig_macro = metomi.rose.macros.trigger.TriggerMacro() macro_config = copy.deepcopy(new_config) macro_id = ( - rose.upgrade.MACRO_UPGRADE_TRIGGER_NAME + "." + - rose.macro.TRANSFORM_METHOD + metomi.rose.upgrade.MACRO_UPGRADE_TRIGGER_NAME + "." + + metomi.rose.macro.TRANSFORM_METHOD ) if not trig_macro.validate_dependencies(macro_config, meta_config): new_trig_config, trig_change_list = ( - rose.macros.trigger.TriggerMacro().transform( + metomi.rose.macros.trigger.TriggerMacro().transform( macro_config, meta_config) ) handle_transform_func(config_name, macro_id, diff --git a/metomi/rose/config_editor/util.py b/metomi/rose/config_editor/util.py index ca0371a41..c89496df0 100644 --- a/metomi/rose/config_editor/util.py +++ b/metomi/rose/config_editor/util.py @@ -29,9 +29,9 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk -import rose -import rose.gtk.dialog -import rose.gtk.util +import metomi.rose +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util class Lookup(object): @@ -47,7 +47,7 @@ def get_id_from_section_option(self, section, option): if option is None: id_ = section else: - id_ = section + rose.CONFIG_DELIMITER + option + id_ = section + metomi.rose.CONFIG_DELIMITER + option self.section_option_id_lookup[id_] = (section, option) return id_ @@ -60,7 +60,7 @@ def get_section_option_from_id(self, var_id): """ if var_id in self.section_option_id_lookup: return self.section_option_id_lookup[var_id] - split_char = rose.CONFIG_DELIMITER + split_char = metomi.rose.CONFIG_DELIMITER option_name = var_id.split(split_char)[-1] section = var_id.replace(split_char + option_name, '', 1) if option_name == section: @@ -107,9 +107,9 @@ def launch_node_info_dialog(node, changes, search_function): title = node.__class__.__name__ + " " + node.metadata['id'] text = '' if changes: - text += (rose.config_editor.DIALOG_NODE_INFO_CHANGES.format(changes) + + text += (metomi.rose.config_editor.DIALOG_NODE_INFO_CHANGES.format(changes) + "\n") - text += rose.config_editor.DIALOG_NODE_INFO_DATA + text += metomi.rose.config_editor.DIALOG_NODE_INFO_DATA try: att_list = list(vars(node).items()) except TypeError: @@ -121,15 +121,15 @@ def launch_node_info_dialog(node, changes, search_function): metadata_start_index = len(att_list) for key, value in sorted(node.metadata.items()): att_list.append([key, value]) - delim = rose.config_editor.DIALOG_NODE_INFO_DELIMITER - name = rose.config_editor.DIALOG_NODE_INFO_ATTRIBUTE - maxlen = rose.config_editor.DIALOG_NODE_INFO_MAX_LEN + delim = metomi.rose.config_editor.DIALOG_NODE_INFO_DELIMITER + name = metomi.rose.config_editor.DIALOG_NODE_INFO_ATTRIBUTE + maxlen = metomi.rose.config_editor.DIALOG_NODE_INFO_MAX_LEN for i, (att_name, att_val) in enumerate(att_list): if (att_name == 'metadata' or att_name.startswith("_") or callable(att_val) or att_name == 'old_value'): continue if i == metadata_start_index: - text += "\n" + rose.config_editor.DIALOG_NODE_INFO_METADATA + text += "\n" + metomi.rose.config_editor.DIALOG_NODE_INFO_METADATA prefix = name.format(att_name) + delim indent0 = len(prefix) text += prefix @@ -137,18 +137,18 @@ def launch_node_info_dialog(node, changes, search_function): text += _pretty_format_data(att_val, global_indent=indent0, width=lenval) text += "\n" - rose.gtk.dialog.run_hyperlink_dialog(Gtk.STOCK_DIALOG_INFO, text, title, + metomi.rose.gtk.dialog.run_hyperlink_dialog(Gtk.STOCK_DIALOG_INFO, text, title, search_function) def launch_error_dialog(exception=None, text=""): - """This will be replaced by rose.reporter utilities.""" + """This will be replaced by metomi.rose.reporter utilities.""" if text: text += "\n" if exception is not None: text += type(exception).__name__ + ": " + str(exception) - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, rose.config_editor.DIALOG_TITLE_ERROR, + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, metomi.rose.config_editor.DIALOG_TITLE_ERROR, modal=False) @@ -183,7 +183,7 @@ def wrap_string(text, maxlen=72, indent0=0, maxlines=4, sep=","): lines = [""] linelen = maxlen - indent0 for item in text.split(sep): - dtext = rose.gtk.util.safe_str(item) + sep + dtext = metomi.rose.gtk.util.safe_str(item) + sep if lines[-1] and len(lines[-1] + dtext) > linelen: lines.append("") linelen = maxlen @@ -201,14 +201,14 @@ def null_cmp(x_item, y_item): if x_id == '' or y_id == '': return (x_id == '') - (y_id == '') if x_sort_key == y_sort_key: - return rose.config.sort_settings(x_id, y_id) + return metomi.rose.config.sort_settings(x_id, y_id) return cmp(x_sort_key, y_sort_key) def _pretty_format_data(data, global_indent=0, indent=4, width=60): - sub_name = rose.config_editor.DIALOG_NODE_INFO_SUB_ATTRIBUTE - safe_str = rose.gtk.util.safe_str - delim = rose.config_editor.DIALOG_NODE_INFO_DELIMITER + sub_name = metomi.rose.config_editor.DIALOG_NODE_INFO_SUB_ATTRIBUTE + safe_str = metomi.rose.gtk.util.safe_str + delim = metomi.rose.config_editor.DIALOG_NODE_INFO_DELIMITER if isinstance(data, dict) and data: text = "" for key, val in list(data.items()): diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py index b6584d1ce..701beb1e0 100644 --- a/metomi/rose/config_editor/valuewidget/__init__.py +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -70,14 +70,14 @@ def copy(self): def chooser(value, metadata, error): """Select an appropriate widget class based on the arguments. - Note: rose edit overrides this logic if a widget is hard coded. + Note: metomi.rose.edit overrides this logic if a widget is hard coded. """ - m_type = metadata.get(rose.META_PROP_TYPE) - m_values = metadata.get(rose.META_PROP_VALUES) - m_length = metadata.get(rose.META_PROP_LENGTH) - m_hint = metadata.get(rose.META_PROP_VALUE_HINTS) - contains_env = rose.env.contains_env_var(value) + m_type = metadata.get(metomi.rose.META_PROP_TYPE) + m_values = metadata.get(metomi.rose.META_PROP_VALUES) + m_length = metadata.get(metomi.rose.META_PROP_LENGTH) + m_hint = metadata.get(metomi.rose.META_PROP_VALUE_HINTS) + contains_env = metomi.rose.env.contains_env_var(value) is_list = m_length is not None or isinstance(m_type, list) # determine widget by presence of environment variables diff --git a/metomi/rose/config_editor/valuewidget/boolradio.py b/metomi/rose/config_editor/valuewidget/boolradio.py index d6bcc09af..81ed44473 100644 --- a/metomi/rose/config_editor/valuewidget/boolradio.py +++ b/metomi/rose/config_editor/valuewidget/boolradio.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor +import metomi.rose.config_editor from . import radiobuttons @@ -39,17 +39,17 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.hook = hook self.allowed_values = [] self.label_dict = {} - if metadata.get(rose.META_PROP_TYPE) == "boolean": - self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_TRUE, - rose.TYPE_BOOLEAN_VALUE_FALSE] + if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": + self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_TRUE, + metomi.rose.TYPE_BOOLEAN_VALUE_FALSE] else: - self.allowed_values = [rose.TYPE_LOGICAL_VALUE_TRUE, - rose.TYPE_LOGICAL_VALUE_FALSE] + self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_TRUE, + metomi.rose.TYPE_LOGICAL_VALUE_FALSE] self.label_dict = { - rose.TYPE_LOGICAL_VALUE_TRUE: - rose.TYPE_LOGICAL_TRUE_TITLE, - rose.TYPE_LOGICAL_VALUE_FALSE: - rose.TYPE_LOGICAL_FALSE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: + metomi.rose.TYPE_LOGICAL_TRUE_TITLE, + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: + metomi.rose.TYPE_LOGICAL_FALSE_TITLE} for k, item in enumerate(self.allowed_values): if item in self.label_dict: diff --git a/metomi/rose/config_editor/valuewidget/booltoggle.py b/metomi/rose/config_editor/valuewidget/booltoggle.py index 8296e38d4..9397796bb 100644 --- a/metomi/rose/config_editor/valuewidget/booltoggle.py +++ b/metomi/rose/config_editor/valuewidget/booltoggle.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose +import metomi.rose class BoolToggleValueWidget(Gtk.HBox): @@ -38,24 +38,24 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.hook = hook self.allowed_values = [] self.label_dict = {} - if metadata.get(rose.META_PROP_TYPE) == "boolean": - self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_FALSE, - rose.TYPE_BOOLEAN_VALUE_TRUE] + if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": + self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_BOOLEAN_VALUE_TRUE] self.label_dict = dict(list(zip(self.allowed_values, self.allowed_values))) - elif metadata.get(rose.META_PROP_TYPE) == "python_boolean": - self.allowed_values = [rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, - rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] + elif metadata.get(metomi.rose.META_PROP_TYPE) == "python_boolean": + self.allowed_values = [metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] self.label_dict = dict(list(zip(self.allowed_values, self.allowed_values))) else: - self.allowed_values = [rose.TYPE_LOGICAL_VALUE_FALSE, - rose.TYPE_LOGICAL_VALUE_TRUE] + self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE] self.label_dict = { - rose.TYPE_LOGICAL_VALUE_FALSE: - rose.TYPE_LOGICAL_FALSE_TITLE, - rose.TYPE_LOGICAL_VALUE_TRUE: - rose.TYPE_LOGICAL_TRUE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: + metomi.rose.TYPE_LOGICAL_FALSE_TITLE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: + metomi.rose.TYPE_LOGICAL_TRUE_TITLE} imgs = [Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py index 2a3101e8a..33ecc25da 100644 --- a/metomi/rose/config_editor/valuewidget/character.py +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -22,8 +22,8 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from rose import META_PROP_TYPE -import rose.config_editor.util +from metomi.rose.import META_PROP_TYPE +import metomi.rose.config_editor.util class QuotedTextValueWidget(Gtk.HBox): @@ -34,23 +34,23 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): super(QuotedTextValueWidget, self).__init__(homogeneous=False, spacing=0) # Importing here prevents cyclic imports - import rose.macros.value + import metomi.rose.macros.value self.type = metadata.get(META_PROP_TYPE) - checker = rose.macros.value.ValueChecker() + checker = metomi.rose.macros.value.ValueChecker() if self.type == "character": self.type_checker = checker.check_character self.format_text_in = ( - rose.config_editor.util.text_for_character_widget) + metomi.rose.config_editor.util.text_for_character_widget) self.format_text_out = ( - rose.config_editor.util.text_from_character_widget) + metomi.rose.config_editor.util.text_from_character_widget) self.quote_char = "'" self.esc_quote_chars = "''" elif self.type == "quoted": self.type_checker = checker.check_quoted self.format_text_in = ( - rose.config_editor.util.text_for_quoted_widget) + metomi.rose.config_editor.util.text_for_quoted_widget) self.format_text_out = ( - rose.config_editor.util.text_from_quoted_widget) + metomi.rose.config_editor.util.text_from_quoted_widget) self.quote_char = '"' self.esc_quote_chars = '\\"' self.value = value diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py index 67a104969..bbbeea8e7 100644 --- a/metomi/rose/config_editor/valuewidget/choice.py +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -25,11 +25,11 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor -import rose.gtk.choice -import rose.gtk.dialog -import rose.opt_parse -import rose.variable +import metomi.rose.config_editor +import metomi.rose.gtk.choice +import metomi.rose.gtk.dialog +import metomi.rose.opt_parse +import metomi.rose.variable class ChoicesValueWidget(Gtk.HBox): @@ -41,10 +41,10 @@ class ChoicesValueWidget(Gtk.HBox): syntax: # NAME - # rose.config_editor.valuewidget.choice.ChoicesValueWidget + # metomi.rose.config_editor.valuewidget.choice.ChoicesValueWidget # # SYNOPSIS - # rose...Widget [OPTIONS] [CUSTOM_CHOICE_HINT ...] + # metomi.rose...Widget [OPTIONS] [CUSTOM_CHOICE_HINT ...] # # DESCRIPTION # Represent available choices as a widget. @@ -64,7 +64,7 @@ class ChoicesValueWidget(Gtk.HBox): # choices into the variable value. # The only supported format is "python" which outputs the # result of repr(my_list) - e.g. VARIABLE=["A", "B"]. - # If not specified, the format will default to rose array + # If not specified, the format will default to metomi.rose.array # standard e.g. VARIABLE=A, B. # --guess-groups # Extrapolate inter-choice dependencies from their names. @@ -106,7 +106,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook - self.opt_parser = rose.opt_parse.RoseOptionParser() + self.opt_parser = metomi.rose.opt_parse.RoseOptionParser() self.opt_parser.OPTIONS = self.OPTIONS self.opt_parser.add_my_options(*list(self.OPTIONS.keys())) opts, args = self.opt_parser.parse_args(shlex.split(arg_str)) @@ -114,7 +114,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.groups = [] if opts.choices is not None: for choices in opts.choices: - self.groups.extend(rose.variable.array_split(choices)) + self.groups.extend(metomi.rose.variable.array_split(choices)) self.should_edit = opts.editable self.value_format = opts.format self.should_guess_groups = opts.guess_groups @@ -123,7 +123,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.should_show_kinship = self._calc_should_show_kinship() list_vbox = Gtk.VBox() list_vbox.show() - self._listview = rose.gtk.choice.ChoicesListView( + self._listview = metomi.rose.gtk.choice.ChoicesListView( self._set_value_listview, self._get_value_values, self._handle_search) @@ -135,7 +135,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.pack_start(list_vbox, expand=True, fill=True) tree_vbox = Gtk.VBox() tree_vbox.show() - self._treeview = rose.gtk.choice.ChoicesTreeView( + self._treeview = metomi.rose.gtk.choice.ChoicesTreeView( self._set_value_treeview, self._get_value_values, self._get_available_values, @@ -166,7 +166,7 @@ def _get_add_widget(self): add_entry.get_child().connect( "key-press-event", lambda w, e: self._handle_text_choice(add_entry, e)) - add_entry.set_tooltip_text(rose.config_editor.CHOICE_TIP_ENTER_CUSTOM) + add_entry.set_tooltip_text(metomi.rose.config_editor.CHOICE_TIP_ENTER_CUSTOM) add_entry.show() self._set_available_hints(add_entry) add_hbox.pack_end(add_entry, expand=True, fill=True) @@ -197,9 +197,9 @@ def _handle_text_choice(self, comboboxentry, event): def _add_custom_choice(self, comboboxentry, new_name): entry = comboboxentry.get_child() if not new_name: - text = rose.config_editor.ERROR_BAD_NAME.format("''") - title = rose.config_editor.DIALOG_TITLE_ERROR - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_ERROR, + text = metomi.rose.config_editor.ERROR_BAD_NAME.format("''") + title = metomi.rose.config_editor.DIALOG_TITLE_ERROR + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title) return False new_values = self._get_value_values() + [entry.get_text()] @@ -216,7 +216,7 @@ def _get_value_values(self): except (SyntaxError, TypeError, ValueError): values = [] return values - return rose.variable.array_split(self.value) + return metomi.rose.variable.array_split(self.value) def _get_available_values(self): return self.groups @@ -271,6 +271,6 @@ def _format_and_set_value(self, new_value): if self.value_format == "python": new_value = repr(shlex.split(new_value)) else: - new_value = rose.variable.array_join(shlex.split(new_value)) + new_value = metomi.rose.variable.array_join(shlex.split(new_value)) self.value = new_value self.set_value(new_value) diff --git a/metomi/rose/config_editor/valuewidget/combobox.py b/metomi/rose/config_editor/valuewidget/combobox.py index ac95fb2ef..cf20c7355 100644 --- a/metomi/rose/config_editor/valuewidget/combobox.py +++ b/metomi/rose/config_editor/valuewidget/combobox.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor +import metomi.rose.config_editor class ComboBoxValueWidget(Gtk.HBox): @@ -49,8 +49,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): comboboxentry.pack_start(cell, True, True, 0) comboboxentry.add_attribute(cell, 'text', 0) - var_values = self.metadata[rose.META_PROP_VALUES] - var_titles = self.metadata.get(rose.META_PROP_VALUE_TITLES) + var_values = self.metadata[metomi.rose.META_PROP_VALUES] + var_titles = self.metadata.get(metomi.rose.META_PROP_VALUE_TITLES) for k, entry in enumerate(var_values): if var_titles is not None and var_titles[k]: liststore.append([var_titles[k] + " (" + entry + ")"]) @@ -72,6 +72,6 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): def setter(self, widget): index = widget.get_active() - self.value = self.metadata[rose.META_PROP_VALUES][index] + self.value = self.metadata[metomi.rose.META_PROP_VALUES][index] self.set_value(self.value) return False diff --git a/metomi/rose/config_editor/valuewidget/files.py b/metomi/rose/config_editor/valuewidget/files.py index e3d264752..aae134987 100644 --- a/metomi/rose/config_editor/valuewidget/files.py +++ b/metomi/rose/config_editor/valuewidget/files.py @@ -24,9 +24,9 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk -import rose.config_editor -import rose.external -import rose.gtk.util +import metomi.rose.config_editor +import metomi.rose.external +import metomi.rose.gtk.util class FileChooserValueWidget(Gtk.HBox): @@ -42,7 +42,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.hook = hook self.generate_entry() self.generate_editor_launcher() - self.open_button = rose.gtk.util.CustomButton( + self.open_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_OPEN, size=Gtk.IconSize.MENU, as_tool=False, @@ -78,14 +78,14 @@ def run_and_destroy(self, *args): return False def generate_editor_launcher(self): - self.edit_button = rose.gtk.util.CustomButton( + self.edit_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_DND, size=Gtk.IconSize.MENU, as_tool=False, tip_text="Edit the file") self.edit_button.connect( "clicked", - lambda b: rose.external.launch_geditor(self.value)) + lambda b: metomi.rose.external.launch_geditor(self.value)) self.pack_end(self.edit_button, expand=False, fill=False) def setter(self, widget): @@ -111,20 +111,20 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_editor_launcher() def generate_editor_launcher(self): - self.edit_button = rose.gtk.util.CustomButton( - label=rose.config_editor.LABEL_EDIT, + self.edit_button = metomi.rose.gtk.util.CustomButton( + label=metomi.rose.config_editor.LABEL_EDIT, stock_id=Gtk.STOCK_DND, size=Gtk.IconSize.MENU, as_tool=False, tip_text="Edit the file") self.edit_button.connect("clicked", self.on_click) self.pack_start(self.edit_button, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) def retrieve_path(self): - root = self.metadata[rose.config_editor.META_PROP_INTERNAL] + root = self.metadata[metomi.rose.config_editor.META_PROP_INTERNAL] return os.path.join(root, self.value) def on_click(self, button): path = self.retrieve_path() - rose.external.launch_geditor(path) + metomi.rose.external.launch_geditor(path) diff --git a/metomi/rose/config_editor/valuewidget/format.py b/metomi/rose/config_editor/valuewidget/format.py index 116a11c44..7364b2b06 100644 --- a/metomi/rose/config_editor/valuewidget/format.py +++ b/metomi/rose/config_editor/valuewidget/format.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config +import metomi.rose.config class FormatsChooserValueWidget(Gtk.HBox): @@ -129,7 +129,7 @@ def entry_change_handler(self, entry): def load_data_chooser(self): data_model = Gtk.ListStore(str) options = self.values_getter() - options.sort(rose.config.sort_settings) + options.sort(metomi.rose.config.sort_settings) for value in options: if value not in [e.get_text() for e in self.entries]: data_model.append([str(value)]) diff --git a/metomi/rose/config_editor/valuewidget/intspin.py b/metomi/rose/config_editor/valuewidget/intspin.py index 1900b3e16..15ee22aaf 100644 --- a/metomi/rose/config_editor/valuewidget/intspin.py +++ b/metomi/rose/config_editor/valuewidget/intspin.py @@ -24,7 +24,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor +import metomi.rose.config_editor class IntSpinButtonValueWidget(Gtk.HBox): @@ -73,7 +73,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.warning_img.set_from_stock(Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU) self.warning_img.set_tooltip_text( - rose.config_editor.WARNING_INTEGER_OUT_OF_BOUNDS) + metomi.rose.config_editor.WARNING_INTEGER_OUT_OF_BOUNDS) self.warning_img.show() self.pack_start(self.warning_img, False, False, 0) diff --git a/metomi/rose/config_editor/valuewidget/radiobuttons.py b/metomi/rose/config_editor/valuewidget/radiobuttons.py index fed707eb1..db4a277fd 100644 --- a/metomi/rose/config_editor/valuewidget/radiobuttons.py +++ b/metomi/rose/config_editor/valuewidget/radiobuttons.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor +import metomi.rose.config_editor class RadioButtonsValueWidget(Gtk.HBox): @@ -37,8 +37,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook - var_values = metadata[rose.META_PROP_VALUES] - var_titles = metadata.get(rose.META_PROP_VALUE_TITLES) + var_values = metadata[metomi.rose.META_PROP_VALUES] + var_titles = metadata.get(metomi.rose.META_PROP_VALUE_TITLES) if var_titles: vbox = Gtk.VBox() diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py index 0d1704694..02848ad99 100644 --- a/metomi/rose/config_editor/valuewidget/source.py +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -25,10 +25,10 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.config_editor -import rose.formats -import rose.gtk.choice +import metomi.rose.config +import metomi.rose.config_editor +import metomi.rose.formats +import metomi.rose.gtk.choice class SourceValueWidget(Gtk.HBox): @@ -48,7 +48,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook self.var_ops = arg_str - formats = [f for f in rose.formats.__dict__ if not f.startswith('__')] + formats = [f for f in metomi.rose.formats.__dict__ if not f.startswith('__')] self.formats = formats self.formats_ok = None self._ok_content_sections = set([None]) @@ -58,7 +58,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): vbox = Gtk.VBox() vbox.show() formats_check_button = Gtk.CheckButton( - rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL) + metomi.rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL) formats_check_button.set_active(not self.formats_ok) formats_check_button.connect("toggled", self._toggle_formats) formats_check_button.show() @@ -69,14 +69,14 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): vbox.pack_start(formats_check_hbox, expand=False, fill=False) treeviews_hbox = Gtk.HPaned() treeviews_hbox.show() - self._listview = rose.gtk.choice.ChoicesListView( + self._listview = metomi.rose.gtk.choice.ChoicesListView( self._set_listview, self._get_included_sources, self._handle_search, get_custom_menu_items=self._get_custom_menu_items ) self._listview.set_tooltip_text( - rose.config_editor.FILE_CONTENT_PANEL_TIP) + metomi.rose.config_editor.FILE_CONTENT_PANEL_TIP) frame = Gtk.Frame() frame.show() frame.add(self._listview) @@ -92,13 +92,13 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): adder_value = "" adder_metadata = {} adder_set_value = lambda v: None - adder_hook = rose.config_editor.valuewidget.ValueWidgetHook() + adder_hook = metomi.rose.config_editor.valuewidget.ValueWidgetHook() self._adder = ( - rose.config_editor.valuewidget.files.FileChooserValueWidget( + metomi.rose.config_editor.valuewidget.files.FileChooserValueWidget( adder_value, adder_metadata, adder_set_value, adder_hook)) self._adder.entry.connect("activate", self._add_file_source) self._adder.entry.set_tooltip_text( - rose.config_editor.TIP_VALUE_ADD_URI) + metomi.rose.config_editor.TIP_VALUE_ADD_URI) self._adder.show() treeviews_hbox.add1(value_vbox) treeviews_hbox.add2(self._available_frame) @@ -120,16 +120,16 @@ def _generate_available_treeview(self): existing_widget = self._available_frame.get_child() if existing_widget is not None: self._available_frame.remove(existing_widget) - self._available_treeview = rose.gtk.choice.ChoicesTreeView( + self._available_treeview = metomi.rose.gtk.choice.ChoicesTreeView( self._set_available_treeview, self._get_included_sources, self._get_available_sections, self._get_groups, - title=rose.config_editor.FILE_CONTENT_PANEL_TITLE, + title=metomi.rose.config_editor.FILE_CONTENT_PANEL_TITLE, get_is_included=self._get_section_is_included ) self._available_treeview.set_tooltip_text( - rose.config_editor.FILE_CONTENT_PANEL_OPT_TIP) + metomi.rose.config_editor.FILE_CONTENT_PANEL_OPT_TIP) self._available_frame.show() if not self.formats_ok: self._available_frame.hide() @@ -138,7 +138,7 @@ def _generate_available_treeview(self): def _get_custom_menu_items(self): """Return some custom menuitems for use in the list view.""" menuitem = Gtk.ImageMenuItem( - rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) + metomi.rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) image = Gtk.Image.new_from_stock( Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.MENU) menuitem.set_image(image) @@ -178,7 +178,7 @@ def _get_available_sections(self): if section_all not in ok_content_sections: ok_content_sections.append(section_all) ok_content_sections.append(section) - ok_content_sections.sort(rose.config.sort_settings) + ok_content_sections.sort(metomi.rose.config.sort_settings) ok_content_sections.sort(self._sort_settings_duplicate) return ok_content_sections diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 9c1d87052..66f87c2b8 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -22,13 +22,13 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor -import rose.config_editor.valuewidget -import rose.env -import rose.gtk.util +import metomi.rose.config_editor +import metomi.rose.config_editor.valuewidget +import metomi.rose.env +import metomi.rose.gtk.util -ENV_COLOUR = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV) +ENV_COLOUR = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV) class RawValueWidget(Gtk.HBox): @@ -45,9 +45,9 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): insensitive_colour = Gtk.Style().bg[0] self.entry.modify_bg(Gtk.StateType.INSENSITIVE, insensitive_colour) self.normal_colour = Gtk.Style().fg[Gtk.StateType.NORMAL] - if rose.env.contains_env_var(self.value): + if metomi.rose.env.contains_env_var(self.value): self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) - self.entry.set_tooltip_text(rose.config_editor.VAR_WIDGET_ENV_INFO) + self.entry.set_tooltip_text(metomi.rose.config_editor.VAR_WIDGET_ENV_INFO) self.entry.set_text(self.value) self.entry.connect("button-release-event", self._handle_middle_click_paste) @@ -68,9 +68,9 @@ def setter(self, widget, *args): return False self.value = new_value self.set_value(self.value) - if rose.env.contains_env_var(self.value): + if metomi.rose.env.contains_env_var(self.value): self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) - self.entry.set_tooltip_text(rose.config_editor.VAR_WIDGET_ENV_INFO) + self.entry.set_tooltip_text(metomi.rose.config_editor.VAR_WIDGET_ENV_INFO) else: self.entry.set_tooltip_text(None) return False @@ -106,8 +106,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entrybuffer.set_text(self.value) self.entry = Gtk.TextView(self.entrybuffer) self.entry.set_wrap_mode(Gtk.WrapMode.WORD) - self.entry.set_left_margin(rose.config_editor.SPACING_SUB_PAGE) - self.entry.set_right_margin(rose.config_editor.SPACING_SUB_PAGE) + self.entry.set_left_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) + self.entry.set_right_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) self.entry.connect('focus-in-event', self.hook.trigger_scroll) self.entry.show() diff --git a/metomi/rose/config_editor/valuewidget/valuehints.py b/metomi/rose/config_editor/valuewidget/valuehints.py index 024f0d13e..902b19914 100644 --- a/metomi/rose/config_editor/valuewidget/valuehints.py +++ b/metomi/rose/config_editor/valuewidget/valuehints.py @@ -23,9 +23,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor.util -import rose.gtk.util -import rose.variable +import metomi.rose.config_editor.util +import metomi.rose.gtk.util +import metomi.rose.variable class HintsValueWidget(Gtk.HBox): @@ -70,7 +70,7 @@ def _set_completion(self, metadata): """ Return a predictive text model for value-hints.""" completion = Gtk.EntryCompletion() model = Gtk.ListStore(str) - var_hints = metadata.get(rose.META_PROP_VALUE_HINTS) + var_hints = metadata.get(metomi.rose.META_PROP_VALUE_HINTS) for hint in var_hints: model.append([hint]) completion.set_model(model) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 46b2089c3..a89012894 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -26,16 +26,16 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor.keywidget -import rose.config_editor.menuwidget -import rose.config_editor.valuewidget -import rose.config_editor.valuewidget.array.row as row -import rose.config_editor.valuewidget.source -import rose.config_editor.util -import rose.gtk.dialog -import rose.gtk.util -import rose.reporter -import rose.resource +import metomi.rose.config_editor.keywidget +import metomi.rose.config_editor.menuwidget +import metomi.rose.config_editor.valuewidget +import metomi.rose.config_editor.valuewidget.array.row as row +import metomi.rose.config_editor.valuewidget.source +import metomi.rose.config_editor.util +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util +import metomi.rose.reporter +import metomi.rose.resource class VariableWidget(object): @@ -60,10 +60,10 @@ def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, show_modes = {} self.show_modes = show_modes self.insensitive_colour = Gtk.Style().bg[0] - self.bad_colour = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) - self.hidden_colour = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_IRRELEVANT) + self.bad_colour = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + self.hidden_colour = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_IRRELEVANT) self.keywidget = self.get_keywidget(variable, show_modes) self.generate_valuewidget(variable) self.is_inconsistent = False @@ -90,7 +90,7 @@ def get_keywidget(self, variable, show_modes): Loads 'tooltips' or hover-over text based on the variable metadata. """ - widget = rose.config_editor.keywidget.KeyWidget( + widget = metomi.rose.config_editor.keywidget.KeyWidget( variable, self.var_ops, self.launch_help, self.update_status, show_modes ) @@ -142,13 +142,13 @@ def generate_valuewidget(self, variable, override_custom=False, """Creates the valuewidget attribute, based on value and metadata.""" custom_arg = None if (variable.metadata.get("type") == - rose.config_editor.FILE_TYPE_NORMAL): - use_this_valuewidget = (rose.config_editor. + metomi.rose.config_editor.FILE_TYPE_NORMAL): + use_this_valuewidget = (metomi.rose.config_editor. valuewidget.source.SourceValueWidget) custom_arg = self.var_ops set_value = self._valuewidget_set_value - hook_object = rose.config_editor.valuewidget.ValueWidgetHook( - rose.config_editor.false_function, + hook_object = metomi.rose.config_editor.valuewidget.ValueWidgetHook( + metomi.rose.config_editor.false_function, self._get_focus) metadata = copy.deepcopy(variable.metadata) if use_this_valuewidget is not None: @@ -157,9 +157,9 @@ def generate_valuewidget(self, variable, override_custom=False, set_value, hook_object, arg_str=custom_arg) - elif (rose.config_editor.META_PROP_WIDGET in self.meta and + elif (metomi.rose.config_editor.META_PROP_WIDGET in self.meta and not override_custom): - w_val = self.meta[rose.config_editor.META_PROP_WIDGET] + w_val = self.meta[metomi.rose.config_editor.META_PROP_WIDGET] info = w_val.split(None, 1) if len(info) > 1: widget_path, custom_arg = info @@ -168,11 +168,11 @@ def generate_valuewidget(self, variable, override_custom=False, files = self.var_ops.get_ns_metadata_files(metadata["full_ns"]) error_handler = lambda e: self.handle_bad_valuewidget( str(e), variable, set_value) - widget = rose.resource.import_object(widget_path, + widget = metomi.rose.resource.import_object(widget_path, files, error_handler) if widget is None: - text = rose.config_editor.ERROR_IMPORT_CLASS.format(w_val) + text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format(w_val) self.handle_bad_valuewidget(text, variable, set_value) try: self.valuewidget = widget(variable.value, @@ -183,7 +183,7 @@ def generate_valuewidget(self, variable, override_custom=False, except Exception as exc: self.handle_bad_valuewidget(str(exc), variable, set_value) else: - widget_maker = rose.config_editor.valuewidget.chooser( + widget_maker = metomi.rose.config_editor.valuewidget.chooser( variable.value, variable.metadata, variable.error) self.valuewidget = widget_maker(variable.value, @@ -201,15 +201,15 @@ def generate_valuewidget(self, variable, override_custom=False, def handle_bad_valuewidget(self, error_info, variable, set_value): """Handle a bad custom valuewidget import.""" - text = rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) - rose.reporter.Reporter()( - rose.config_editor.util.ImportWidgetError(text)) + text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) + metomi.rose.reporter.Reporter()( + metomi.rose.config_editor.util.ImportWidgetError(text)) self.generate_valuewidget(variable, override_custom=True) def handle_focus_in(self, widget, event): widget._first_colour = widget.style.base[Gtk.StateType.NORMAL] - new_colour = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED) + new_colour = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED) widget.modify_base(Gtk.StateType.NORMAL, new_colour) def handle_focus_out(self, widget, event): @@ -219,7 +219,7 @@ def handle_focus_out(self, widget, event): def get_menuwidget(self, variable, menuclass=None): """Create the menuwidget attribute, an option menu button.""" if menuclass is None: - menuclass = rose.config_editor.menuwidget.MenuWidget + menuclass = metomi.rose.config_editor.menuwidget.MenuWidget menuwidget = menuclass(variable, self.var_ops, lambda: self.remove_from(self.get_parent()), @@ -288,7 +288,7 @@ def force_scroll(self, widget=None, container=None): for handler_id in self.force_signal_ids: vadj.handler_block(handler_id) self.force_signal_ids = [] - vadj.connect('changed', rose.config_editor.false_function) + vadj.connect('changed', metomi.rose.config_editor.false_function) if y_coordinate is None: vadj.upper = vadj.upper + 0.08 * vadj.page_size vadj.set_value(vadj.upper - vadj.page_size) @@ -337,7 +337,7 @@ def set_ignored(self): if "'Ignore'" not in self.menuwidget.option_ui: self.menuwidget.old_option_ui = self.menuwidget.option_ui self.menuwidget.old_actions = self.menuwidget.actions - if list(ign_map.keys()) == [rose.variable.IGNORED_BY_SECTION]: + if list(ign_map.keys()) == [metomi.rose.variable.IGNORED_BY_SECTION]: # Not ignored in itself, so give Ignore option. if "'Enable'" in self.menuwidget.option_ui: self.menuwidget.option_ui = re.sub( @@ -424,25 +424,25 @@ def launch_help(self, url_mode=False): """Launch a help dialog or a URL in a web browser.""" if url_mode: return self.var_ops.launch_url(self.variable) - if rose.META_PROP_HELP not in self.meta: + if metomi.rose.META_PROP_HELP not in self.meta: return help_text = None if self.show_modes.get( - rose.config_editor.SHOW_MODE_CUSTOM_HELP): - format_string = rose.config_editor.CUSTOM_FORMAT_HELP - help_text = rose.variable.expand_format_string( + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP): + format_string = metomi.rose.config_editor.CUSTOM_FORMAT_HELP + help_text = metomi.rose.variable.expand_format_string( format_string, self.variable) if help_text is None: - help_text = self.meta[rose.META_PROP_HELP] + help_text = self.meta[metomi.rose.META_PROP_HELP] self._launch_help_dialog(help_text) def _launch_help_dialog(self, help_text): """Launch a scrollable dialog for this variable's help text.""" - title = rose.config_editor.DIALOG_HELP_TITLE.format( + title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format( self.variable.metadata["id"]) ns = self.variable.metadata["full_ns"] search_function = lambda i: self.var_ops.search_for_var(ns, i) - rose.gtk.dialog.run_hyperlink_dialog( + metomi.rose.gtk.dialog.run_hyperlink_dialog( Gtk.STOCK_DIALOG_INFO, help_text, title, search_function) return False @@ -513,13 +513,13 @@ def needs_type_error_refresh(self): def type_error_refresh(self, variable): """Handle a type error.""" - if rose.META_PROP_TYPE in variable.error: + if metomi.rose.META_PROP_TYPE in variable.error: self._set_inconsistent(self.valuewidget, variable) else: self._set_consistent(self.valuewidget, variable) self.variable = variable self.errors = list(variable.error.keys()) - self.valuewidget.handle_type_error(rose.META_PROP_TYPE in self.errors) + self.valuewidget.handle_type_error(metomi.rose.META_PROP_TYPE in self.errors) self.menuwidget.refresh(variable) self.keywidget.refresh(variable) @@ -534,8 +534,8 @@ def __init__(self, *args, **kwargs): def generate_valuewidget(self, variable, override_custom=False): """Creates the valuewidget attribute, based on value and metadata.""" - if (rose.META_PROP_LENGTH in variable.metadata or - isinstance(variable.metadata.get(rose.META_PROP_TYPE), list)): + if (metomi.rose.META_PROP_LENGTH in variable.metadata or + isinstance(variable.metadata.get(metomi.rose.META_PROP_TYPE), list)): use_this_valuewidget = self.make_row_valuewidget else: use_this_valuewidget = None diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index b2364a2d3..462084b79 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -26,14 +26,14 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config -import rose.gtk.dialog -import rose.gtk.util -import rose.resource +import metomi.rose.config +import metomi.rose.gtk.dialog +import metomi.rose.gtk.util +import metomi.rose.resource REC_SPLIT_MACRO_TEXT = re.compile( - '(.{' + str(rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + + '(.{' + str(metomi.rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + '})') @@ -96,15 +96,15 @@ class MainWindow(object): def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, nav_panel=None, status_bar=None, notebook=None, - page_change_func=rose.config_editor.false_function, - save_func=rose.config_editor.false_function): + page_change_func=metomi.rose.config_editor.false_function, + save_func=metomi.rose.config_editor.false_function): self.window = Gtk.Window() self.window.set_title(name + ' - ' + - rose.config_editor.LAUNCH_COMMAND) - self.util = rose.config_editor.util.Lookup() - self.window.set_icon(rose.gtk.util.get_icon()) + metomi.rose.config_editor.LAUNCH_COMMAND) + self.util = metomi.rose.config_editor.util.Lookup() + self.window.set_icon(metomi.rose.gtk.util.get_icon()) Gtk.window_set_default_icon_list(self.window.get_icon()) - self.window.set_default_size(*rose.config_editor.SIZE_WINDOW) + self.window.set_default_size(*metomi.rose.config_editor.SIZE_WINDOW) self.window.set_destroy_with_parent(False) self.save_func = save_func self.top_vbox = Gtk.VBox() @@ -142,15 +142,15 @@ def generate_main_hbox(self, nav_panel, notebook): self.main_hbox.show() self.main_hbox.pack2(notebook, resize=True, shrink=True) self.main_hbox.show() - self.main_hbox.set_position(rose.config_editor.WIDTH_TREE_PANEL) + self.main_hbox.set_position(metomi.rose.config_editor.WIDTH_TREE_PANEL) def launch_about_dialog(self, somewidget=None): """Create a dialog showing the 'About' information.""" - rose.gtk.dialog.run_about_dialog( - name=rose.config_editor.PROGRAM_NAME, - copyright_=rose.config_editor.COPYRIGHT, - logo_path="etc/images/rose-logo.png", - website=rose.config_editor.PROJECT_URL) + metomi.rose.gtk.dialog.run_about_dialog( + name=metomi.rose.config_editor.PROGRAM_NAME, + copyright_=metomi.rose.config_editor.COPYRIGHT, + logo_path="etc/images/metomi.rose.logo.png", + website=metomi.rose.config_editor.PROJECT_URL) def _reload_choices(self, liststore, top_name, add_choices): liststore.clear() @@ -161,16 +161,16 @@ def _reload_choices(self, liststore, top_name, add_choices): def launch_add_dialog(self, names, add_choices, section_help): """Launch a dialog asking for a section name.""" - add_dialog = Gtk.Dialog(title=rose.config_editor.DIALOG_TITLE_ADD, + add_dialog = Gtk.Dialog(title=metomi.rose.config_editor.DIALOG_TITLE_ADD, parent=self.window, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) ok_button = add_dialog.action_area.get_children()[0] - config_label = Gtk.Label(label=rose.config_editor.DIALOG_BODY_ADD_CONFIG) + config_label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_BODY_ADD_CONFIG) config_label.show() - label = Gtk.Label(label=rose.config_editor.DIALOG_BODY_ADD_SECTION) + label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_BODY_ADD_SECTION) label.show() config_name_box = Gtk.ComboBoxText() for name in names: @@ -229,7 +229,7 @@ def launch_exit_warning_dialog(self): exit_dialog.add_buttons(Gtk.STOCK_NO, Gtk.ResponseType.REJECT, Gtk.STOCK_CANCEL, Gtk.ResponseType.CLOSE, Gtk.STOCK_YES, Gtk.ResponseType.ACCEPT) - exit_dialog.set_title(rose.config_editor.DIALOG_TITLE_SAVE_CHANGES) + exit_dialog.set_title(metomi.rose.config_editor.DIALOG_TITLE_SAVE_CHANGES) exit_dialog.set_modal(True) exit_dialog.set_keep_above(True) exit_dialog.action_area.get_children()[1].grab_focus() @@ -253,16 +253,16 @@ def launch_graph_dialog(self, name_section_dict): prefs = {} return self._launch_choose_section_dialog( name_section_dict, prefs, - rose.config_editor.DIALOG_TITLE_GRAPH, - rose.config_editor.DIALOG_BODY_GRAPH_CONFIG, - rose.config_editor.DIALOG_BODY_GRAPH_SECTION, + metomi.rose.config_editor.DIALOG_TITLE_GRAPH, + metomi.rose.config_editor.DIALOG_BODY_GRAPH_CONFIG, + metomi.rose.config_editor.DIALOG_BODY_GRAPH_SECTION, null_section_choice=True ) def launch_help_dialog(self, somewidget=None): """Launch a browser to open the help url.""" webbrowser.open( - 'https://metomi.github.io/rose/doc/html/index.html', + 'https://metomi.github.io/metomi.rose.doc/html/index.html', new=True, autoraise=True ) @@ -279,14 +279,14 @@ def launch_ignore_dialog(self, name_section_dict, prefs, is_ignored): """ if is_ignored: - dialog_title = rose.config_editor.DIALOG_TITLE_IGNORE + dialog_title = metomi.rose.config_editor.DIALOG_TITLE_IGNORE else: - dialog_title = rose.config_editor.DIALOG_TITLE_ENABLE - config_title = rose.config_editor.DIALOG_BODY_IGNORE_ENABLE_CONFIG + dialog_title = metomi.rose.config_editor.DIALOG_TITLE_ENABLE + config_title = metomi.rose.config_editor.DIALOG_BODY_IGNORE_ENABLE_CONFIG if is_ignored: - section_title = rose.config_editor.DIALOG_BODY_IGNORE_SECTION + section_title = metomi.rose.config_editor.DIALOG_BODY_IGNORE_SECTION else: - section_title = rose.config_editor.DIALOG_BODY_ENABLE_SECTION + section_title = metomi.rose.config_editor.DIALOG_BODY_ENABLE_SECTION return self._launch_choose_section_dialog( name_section_dict, prefs, dialog_title, config_title, @@ -318,7 +318,7 @@ def _launch_choose_section_dialog( section_box = Gtk.VBox() section_box.show() null_section_checkbutton = Gtk.CheckButton( - rose.config_editor.DIALOG_LABEL_NULL_SECTION) + metomi.rose.config_editor.DIALOG_LABEL_NULL_SECTION) null_section_checkbutton.connect( "toggled", lambda b: section_box.set_sensitive(not b.get_active()) @@ -337,7 +337,7 @@ def _launch_choose_section_dialog( section_box, name_section_dict[name_keys[c.get_active()]], prefs.get(name_keys[c.get_active()], []))) - vbox = Gtk.VBox(spacing=rose.config_editor.SPACING_PAGE) + vbox = Gtk.VBox(spacing=metomi.rose.config_editor.SPACING_PAGE) vbox.pack_start(config_label, expand=False, fill=False) vbox.pack_start(config_name_box, expand=False, fill=False) vbox.pack_start(section_label, expand=False, fill=False) @@ -362,10 +362,10 @@ def _launch_choose_section_dialog( vbox.show() hbox = Gtk.HBox() hbox.pack_start(vbox, expand=True, fill=True, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) hbox.show() chooser_dialog.vbox.pack_start( - hbox, True, True, rose.config_editor.SPACING_PAGE) + hbox, True, True, metomi.rose.config_editor.SPACING_PAGE) section_box.grab_focus() response = chooser_dialog.run() if response in [Gtk.ResponseType.OK, Gtk.ResponseType.YES, @@ -397,7 +397,7 @@ def _launch_choose_section_dialog( def _reload_section_choices(self, vbox, sections, prefs): for child in vbox.get_children(): vbox.remove(child) - sections.sort(rose.config.sort_settings) + sections.sort(metomi.rose.config.sort_settings) section_chooser = Gtk.ComboBoxText() for k, section in enumerate(sections): section_chooser.append_text(section) @@ -418,7 +418,7 @@ def _reload_target_section_entry(self, section_combo_box, target_entry, def launch_macro_changes_dialog( self, config_name, macro_name, changes_list, mode="transform", - search_func=rose.config_editor.false_function): + search_func=metomi.rose.config_editor.false_function): """Launch a dialog explaining macro changes.""" dialog = MacroChangesDialog(self.window, config_name, macro_name, mode, search_func) @@ -428,29 +428,29 @@ def launch_new_config_dialog(self, root_directory): """Launch a dialog allowing naming of a new configuration.""" existing_apps = os.listdir(root_directory) checker_function = lambda t: t not in existing_apps - label = rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_NAME - ok_tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_NAME - err_tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_NAME_ERROR - dialog, container, name_entry = rose.gtk.dialog.get_naming_dialog( + label = metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_NAME + ok_tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_NAME + err_tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_NAME_ERROR + dialog, container, name_entry = metomi.rose.gtk.dialog.get_naming_dialog( label, checker_function, ok_tip_text, err_tip_text) - dialog.set_title(rose.config_editor.DIALOG_TITLE_CONFIG_CREATE) + dialog.set_title(metomi.rose.config_editor.DIALOG_TITLE_CONFIG_CREATE) meta_hbox = Gtk.HBox() meta_label = Gtk.Label(label= - rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META) + metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META) meta_label.show() meta_entry = Gtk.Entry() - tip_text = rose.config_editor.TIP_CONFIG_CHOOSE_META + tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_META meta_entry.set_tooltip_text(tip_text) meta_entry.connect( "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT)) meta_entry.show() meta_hbox.pack_start(meta_label, expand=False, fill=False, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) meta_hbox.pack_start(meta_entry, expand=False, fill=True, - padding=rose.config_editor.SPACING_SUB_PAGE) + padding=metomi.rose.config_editor.SPACING_SUB_PAGE) meta_hbox.show() container.pack_start(meta_hbox, expand=False, fill=True, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) response = dialog.run() name = None meta = None @@ -466,7 +466,7 @@ def launch_new_config_dialog(self, root_directory): def launch_open_dirname_dialog(self): """Launch a FileChooserDialog and return a directory, or None.""" open_dialog = Gtk.FileChooserDialog( - title=rose.config_editor.DIALOG_TITLE_OPEN, + title=metomi.rose.config_editor.DIALOG_TITLE_OPEN, action=Gtk.FileChooserAction.OPEN, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, @@ -476,9 +476,9 @@ def launch_open_dirname_dialog(self): open_dialog.set_icon(self.window.get_icon()) open_dialog.set_default_response(Gtk.ResponseType.OK) config_filter = Gtk.FileFilter() - config_filter.add_pattern(rose.TOP_CONFIG_NAME) - config_filter.add_pattern(rose.SUB_CONFIG_NAME) - config_filter.add_pattern(rose.INFO_CONFIG_NAME) + config_filter.add_pattern(metomi.rose.TOP_CONFIG_NAME) + config_filter.add_pattern(metomi.rose.SUB_CONFIG_NAME) + config_filter.add_pattern(metomi.rose.INFO_CONFIG_NAME) open_dialog.set_filter(config_filter) response = open_dialog.run() if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, @@ -492,7 +492,7 @@ def launch_open_dirname_dialog(self): def launch_load_metadata_dialog(self): """ Launches a dialoge for selecting a metadata path. """ open_dialog = Gtk.FileChooserDialog( - title=rose.config_editor.DIALOG_TITLE_LOAD_METADATA, + title=metomi.rose.config_editor.DIALOG_TITLE_LOAD_METADATA, action=Gtk.FileChooserAction.SELECT_FOLDER, buttons=(Gtk.STOCK_CLOSE, Gtk.ResponseType.CANCEL, @@ -516,7 +516,7 @@ def launch_metadata_manager(self, paths): paths. """ dialog = Gtk.Dialog( - title=rose.config_editor.DIALOG_TITLE_MANAGE_METADATA, + title=metomi.rose.config_editor.DIALOG_TITLE_MANAGE_METADATA, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, @@ -556,9 +556,9 @@ def add_path(): def launch_prefs(self, somewidget=None): """Launch a dialog explaining preferences.""" - text = rose.config_editor.DIALOG_LABEL_PREFERENCES - title = rose.config_editor.DIALOG_TITLE_PREFERENCES - rose.gtk.dialog.run_dialog(rose.gtk.dialog.DIALOG_TYPE_INFO, text, + text = metomi.rose.config_editor.DIALOG_LABEL_PREFERENCES + title = metomi.rose.config_editor.DIALOG_TITLE_PREFERENCES + metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, title) return False @@ -572,9 +572,9 @@ def launch_remove_dialog(self, name_section_dict, prefs): """ return self._launch_choose_section_dialog( name_section_dict, prefs, - rose.config_editor.DIALOG_TITLE_REMOVE, - rose.config_editor.DIALOG_BODY_REMOVE_CONFIG, - rose.config_editor.DIALOG_BODY_REMOVE_SECTION + metomi.rose.config_editor.DIALOG_TITLE_REMOVE, + metomi.rose.config_editor.DIALOG_BODY_REMOVE_CONFIG, + metomi.rose.config_editor.DIALOG_BODY_REMOVE_SECTION ) def launch_rename_dialog(self, name_section_dict, prefs): @@ -587,15 +587,15 @@ def launch_rename_dialog(self, name_section_dict, prefs): """ return self._launch_choose_section_dialog( name_section_dict, prefs, - rose.config_editor.DIALOG_TITLE_RENAME, - rose.config_editor.DIALOG_BODY_RENAME_CONFIG, - rose.config_editor.DIALOG_BODY_RENAME_SECTION, + metomi.rose.config_editor.DIALOG_TITLE_RENAME, + metomi.rose.config_editor.DIALOG_BODY_RENAME_CONFIG, + metomi.rose.config_editor.DIALOG_BODY_RENAME_SECTION, do_target_section=True ) def launch_view_stack(self, undo_stack, redo_stack, undo_func): """Load a view of the stack.""" - self.log_window = rose.config_editor.stack.StackViewer( + self.log_window = metomi.rose.config_editor.stack.StackViewer( undo_stack, redo_stack, undo_func) self.log_window.set_transient_for(self.window) @@ -605,15 +605,15 @@ class MacroChangesDialog(Gtk.Dialog): """Class to hold a dialog summarising macro results.""" COLUMNS = ["Section", "Option", "Type", "Value", "Info"] - MODE_COLOURS = {"transform": rose.config_editor.COLOUR_MACRO_CHANGED, - "validate": rose.config_editor.COLOUR_MACRO_ERROR, - "warn": rose.config_editor.COLOUR_MACRO_WARNING} - MODE_TEXT = {"transform": rose.config_editor.DIALOG_TEXT_MACRO_CHANGED, - "validate": rose.config_editor.DIALOG_TEXT_MACRO_ERROR, - "warn": rose.config_editor.DIALOG_TEXT_MACRO_WARNING} + MODE_COLOURS = {"transform": metomi.rose.config_editor.COLOUR_MACRO_CHANGED, + "validate": metomi.rose.config_editor.COLOUR_MACRO_ERROR, + "warn": metomi.rose.config_editor.COLOUR_MACRO_WARNING} + MODE_TEXT = {"transform": metomi.rose.config_editor.DIALOG_TEXT_MACRO_CHANGED, + "validate": metomi.rose.config_editor.DIALOG_TEXT_MACRO_ERROR, + "warn": metomi.rose.config_editor.DIALOG_TEXT_MACRO_WARNING} def __init__(self, window, config_name, macro_name, mode, search_func): - self.util = rose.config_editor.util.Lookup() + self.util = metomi.rose.config_editor.util.Lookup() self.short_config_name = config_name.rstrip('/').split('/')[-1] self.top_config_name = config_name.lstrip('/').split('/')[0] self.short_macro_name = macro_name.split('.')[-1] @@ -623,10 +623,10 @@ def __init__(self, window, config_name, macro_name, mode, search_func): self.mode = mode self.search_func = search_func if self.for_validate: - title = rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE + title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE button_list = [Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT] else: - title = rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM + title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM button_list = [Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT] title = title.format(self.short_macro_name, self.short_config_name) @@ -646,14 +646,14 @@ def __init__(self, window, config_name, macro_name, mode, search_func): image.show() hbox = Gtk.HBox() hbox.pack_start(image, expand=False, fill=False, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) hbox.pack_start(self.label, expand=False, fill=False, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) hbox.show() self.treewindow = Gtk.ScrolledWindow() self.treewindow.show() self.treewindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) - self.treeview = rose.gtk.util.TooltipTreeView( + self.treeview = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self._get_tooltip) self.treeview.show() self.treemodel = Gtk.TreeStore(str, str, str, str, str) @@ -677,35 +677,35 @@ def __init__(self, window, config_name, macro_name, mode, search_func): "row-activated", self._handle_treeview_activation) self.treewindow.add(self.treeview) self.vbox.pack_end(self.treewindow, expand=True, fill=True, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) self.vbox.pack_end(hbox, expand=False, fill=True, - padding=rose.config_editor.SPACING_PAGE) + padding=metomi.rose.config_editor.SPACING_PAGE) self.set_focus(self.action_area.get_children()[0]) def display(self, changes): if not changes: # Shortcut, no changes. if self.for_validate: - title = rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE_NONE - text = rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_NONE + title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE_NONE + text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_NONE else: - title = rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM_NONE - text = rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_NONE + title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM_NONE + text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_NONE title = title.format(self.short_macro_name) - text = rose.gtk.util.safe_str(text) - return rose.gtk.dialog.run_dialog( - rose.gtk.dialog.DIALOG_TYPE_INFO, text, title) + text = metomi.rose.gtk.util.safe_str(text) + return metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, title) if self.for_validate: - text = rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_ISSUES + text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_ISSUES else: - text = rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_CHANGES + text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_CHANGES nums_is_warning = {True: 0, False: 0} for item in changes: nums_is_warning[item.is_warning] += 1 text = text.format(self.short_macro_name, self.short_config_name, nums_is_warning[False]) if nums_is_warning[True]: - extra_text = rose.config_editor.DIALOG_LABEL_MACRO_WARN_ISSUES + extra_text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_WARN_ISSUES text = (text.rstrip() + " " + extra_text.format(nums_is_warning[True])) self.label.set_markup(text) @@ -728,7 +728,7 @@ def display(self, changes): last_section = item.section self.treemodel.append(last_section_iter, item_att_list) self.treeview.expand_all() - max_size = rose.config_editor.SIZE_MACRO_DIALOG_MAX + max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX my_size = self.size_request() new_size = [-1, -1] for i in [0, 1]: @@ -765,7 +765,7 @@ def _set_markup(self, column, cell, model, r_iter, col_index): if text is None: cell.set_property("markup", None) else: - cell.set_property("markup", rose.gtk.util.safe_str(text)) + cell.set_property("markup", metomi.rose.gtk.util.safe_str(text)) if col_index == 0: cell.set_property("visible", (len(model.get_path(r_iter)) == 1)) diff --git a/metomi/rose/etc/rose-meta/rose-suite-info/rose-meta.conf b/metomi/rose/etc/rose-meta/rose-suite-info/rose-meta.conf index 20f546cdd..bf3bcf2d4 100644 --- a/metomi/rose/etc/rose-meta/rose-suite-info/rose-meta.conf +++ b/metomi/rose/etc/rose-meta/rose-suite-info/rose-meta.conf @@ -25,7 +25,7 @@ sort-key=02-users-0 [=description] description=A long description of the suite. help=A long description of the suite - multi-lines accepted. Inheritance of the suite is included here. -widget[rose-config-edit]=rose.config_editor.valuewidget.text.TextMultilineValueWidget +widget[rose-config-edit]=metomi.rose.config_editor.valuewidget.text.TextMultilineValueWidget pattern=^.+(?# Must not be empty) sort-key=03-type-1 diff --git a/metomi/rose/resource.py b/metomi/rose/resource.py index e6dc5c2ea..3d8b6ee7b 100644 --- a/metomi/rose/resource.py +++ b/metomi/rose/resource.py @@ -188,7 +188,7 @@ def import_object( """ is_builtin = False module_name = ".".join(import_string.split(".")[:-1]) - if module_name.startswith("rose."): + if module_name.startswith("metomi.rose."): is_builtin = True if module_prefix is None: as_name = module_name diff --git a/sphinx/api/configuration/metadata.rst b/sphinx/api/configuration/metadata.rst index ce587956d..6423f3337 100644 --- a/sphinx/api/configuration/metadata.rst +++ b/sphinx/api/configuration/metadata.rst @@ -772,7 +772,7 @@ The metadata options for a configuration fall into four categories: .. code-block:: rose - widget[rose-config-edit]=rose.config_editor.valuewidget.radiobuttons.RadioButtonsValueWidget + widget[rose-config-edit]=metomi.rose.config_editor.valuewidget.radiobuttons.RadioButtonsValueWidget Another useful Rose built-in widget to use is the array element aligning page widget, @@ -793,7 +793,7 @@ The metadata options for a configuration fall into four categories: .. code-block:: rose [namelist:meal_choices] - widget[rose-config-edit]=rose.config_editor.pagewidget.table.PageArrayTable + widget[rose-config-edit]=metomi.rose.config_editor.pagewidget.table.PageArrayTable to align the elements on the page like this: From e54fe369321170204a6109503c4ab23d72c3a675 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Fri, 2 Aug 2024 16:42:18 +0100 Subject: [PATCH 04/42] Convert missed files to gtk3 and add gtk rose dir. --- metomi/rose/config_editor/__init__.py | 36 +- metomi/rose/config_editor/data.py | 2 +- metomi/rose/config_editor/keywidget.py | 2 +- metomi/rose/config_editor/main.py | 34 +- metomi/rose/config_editor/menu.py | 10 +- metomi/rose/config_editor/menuwidget.py | 18 +- metomi/rose/config_editor/nav_panel.py | 4 +- metomi/rose/config_editor/page.py | 3 +- .../config_editor/panelwidget/filesystem.py | 2 +- .../config_editor/panelwidget/summary_data.py | 2 +- .../config_editor/plugin/um/widget/stash.py | 2 +- .../plugin/um/widget/stash_add.py | 2 +- metomi/rose/config_editor/status.py | 4 +- .../config_editor/valuewidget/__init__.py | 10 +- .../config_editor/valuewidget/array/entry.py | 42 +- .../valuewidget/array/logical.py | 44 +- .../config_editor/valuewidget/array/mixed.py | 36 +- .../valuewidget/array/python_list.py | 16 +- .../config_editor/valuewidget/array/row.py | 42 +- .../valuewidget/array/spaced_list.py | 16 +- .../config_editor/valuewidget/character.py | 2 +- .../rose/config_editor/valuewidget/choice.py | 4 +- metomi/rose/config_editor/window.py | 4 +- metomi/rose/gtk/__init__.py | 8 + metomi/rose/gtk/choice.py | 463 +++++++++++ metomi/rose/gtk/console.py | 192 +++++ metomi/rose/gtk/dialog.py | 759 ++++++++++++++++++ metomi/rose/gtk/run.py | 68 ++ metomi/rose/gtk/splash.py | 309 +++++++ metomi/rose/gtk/util.py | 705 ++++++++++++++++ metomi/rose/reporter.py | 54 ++ 31 files changed, 2725 insertions(+), 170 deletions(-) create mode 100644 metomi/rose/gtk/__init__.py create mode 100644 metomi/rose/gtk/choice.py create mode 100644 metomi/rose/gtk/console.py create mode 100644 metomi/rose/gtk/dialog.py create mode 100644 metomi/rose/gtk/run.py create mode 100755 metomi/rose/gtk/splash.py create mode 100644 metomi/rose/gtk/util.py diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index 28e9ff788..48dbb0896 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -23,14 +23,14 @@ To override constants at runtime, place a section: -[metomi.rose.config-edit] +[rose-config-edit] in your site or user configuration file for Rose, convert the name of the constants to lowercase, and place constant=value lines in the section. For example, to override the "ACCEL_HELP_GUI" constant, you could put the following in your site or user configuration: -[metomi.rose.config-edit] +[rose-config-edit] accel_help_gui="H" The values you enter will be cast by Python's ast.literal_eval, so: @@ -47,8 +47,8 @@ 's/^# \(.*\)\n\(^[^#].*\) = \(.*\)/'\ '\

\2\E=\3\<\/h4\>\\1\<\/p\>\n/p;' | sort -Use this text to update the doc/etc/metomi.rose.rug-config-edit/metomi.rose.conf.html -text, remembering to add the [metomi.rose.config-edit] section. +Use this text to update the doc/etc/rose-rug-config-edit/metomi.rose.conf.html +text, remembering to add the [rose-config-edit] section. """ @@ -276,7 +276,7 @@ ERROR_METADATA_CHECKER_TEXT = ( "{0} problem(s) found in metadata at {1}.\n" + "Some functionality has been switched off.\n\n" + - "Run metomi.rose.metadata-check for more info.") + "Run rose metadata-check for more info.") ERROR_MIN_PYGTK_VERSION = "Requires PyGTK version {0}, found {1}." ERROR_MIN_PYGTK_VERSION_TITLE = "Need later PyGTK version to run" ERROR_NO_OUTPUT = "No output found for {0}" @@ -534,7 +534,7 @@ DIALOG_LABEL_UPGRADE = ( "Click Upgrade Version cells to change target versions.") DIALOG_LABEL_UPGRADE_ALL = "Populate all possible versions" -DIALOG_TIP_SUITE_RUN_HELP = "Read the help for metomi.rose.suite-run" +DIALOG_TIP_SUITE_RUN_HELP = "Read the help for rose suite-run" DIALOG_TEXT_MACRO_CHANGED = "changed" DIALOG_TEXT_MACRO_ERROR = "error" DIALOG_TEXT_MACRO_WARNING = "warning" @@ -553,7 +553,7 @@ DIALOG_TITLE_EDIT_COMMENTS = "Edit comments for {0}" DIALOG_TITLE_ENABLE = "Enable section" DIALOG_TITLE_ERROR = "Error" -DIALOG_TITLE_GRAPH = "metomi.rose.metadata-graph" +DIALOG_TITLE_GRAPH = "rose metadata-graph" DIALOG_TITLE_IGNORE = "Ignore section" DIALOG_TITLE_INFO = "Information" DIALOG_TITLE_OPEN = "Open configuration" @@ -705,22 +705,22 @@ # Relevant metadata properties -META_PROP_WIDGET = "widget[metomi.rose.config-edit]" -META_PROP_WIDGET_SUB_NS = "widget[metomi.rose.config-edit:sub-ns]" +META_PROP_WIDGET = "widget[rose-config-edit]" +META_PROP_WIDGET_SUB_NS = "widget[rose-config-edit:sub-ns]" # Miscellaneous COPYRIGHT = ( "Copyright (C) 2012-2020 British Crown (Met Office) & Contributors.") -HELP_FILE = "metomi.rose.rug-config-edit.html" -LAUNCH_COMMAND = "metomi.rose.config-edit" -LAUNCH_COMMAND_CONFIG = "metomi.rose.config-edit -C" -LAUNCH_COMMAND_GRAPH = "metomi.rose.metadata-graph -C" -LAUNCH_SUITE_RUN = "metomi.rose.suite-run" -LAUNCH_SUITE_RUN_HELP = "metomi.rose.help suite-run" +HELP_FILE = "rose-rug-config-edit.html" +LAUNCH_COMMAND = "rose config-edit" +LAUNCH_COMMAND_CONFIG = "rose config-edit -C" +LAUNCH_COMMAND_GRAPH = "rose metadata-graph -C" +LAUNCH_SUITE_RUN = "rose suite-run" +LAUNCH_SUITE_RUN_HELP = "rose help suite-run" MAX_APPS_THRESHOLD = 10 MIN_PYGTK_VERSION = (2, 12, 0) -PROGRAM_NAME = "metomi.rose.edit" -PROJECT_URL = "http://github.com/metomi/metomi.rose." +PROGRAM_NAME = "rose edit" +PROJECT_URL = "http://github.com/metomi/rose/" UNTITLED_NAME = "Untitled" VAR_ID_IN_CONFIG = "Variable id {0} from the configuration {1}" @@ -767,4 +767,4 @@ def load_override_config(sections, my_globals=None): my_globals[name] = cast_value -load_override_config(["metomi.rose.config-edit"]) +load_override_config(["rose-config-edit"]) diff --git a/metomi/rose/config_editor/data.py b/metomi/rose/config_editor/data.py index a297d2366..596cb7afe 100644 --- a/metomi/rose/config_editor/data.py +++ b/metomi/rose/config_editor/data.py @@ -874,7 +874,7 @@ def load_ignored_data(self, config_name): state. 'Doc table' in the comments refers to - doc/metomi.rose.configuration-metadata.html#appendix-ignored-config-edit + doc/rose-configuration-metadata.html#appendix-ignored-config-edit """ self.trigger[config_name] = metomi.rose.macros.trigger.TriggerMacro() diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index ffdbbe64c..73a039f36 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -23,7 +23,7 @@ from gi.repository import Pango import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config_editor import metomi.rose.gtk.dialog diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 6a9d83cd3..87ea8bcb9 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -54,7 +54,7 @@ import gi gi.require_version('Gtk', '3.0') -import gtk # Only used to run the main gtk loop. +from gi.repository import Gtk import metomi.rose.config import metomi.rose.config_editor @@ -352,7 +352,7 @@ def generate_toolbar(self): (metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, 'Gtk.STOCK_DIRECTORY'), (metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, - 'metomi.rose.gtk-scheduler') + 'rose-gtk-scheduler') ], sep_on_name=[ metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, @@ -1146,7 +1146,7 @@ def load_from_file(self, somewidget=None): self.data.load_top_config(dirname) self.data.saved_config_names = set(self.data.config.keys()) self.mainwindow.window.set_title(self.data.top_level_name + - ' - metomi.rose.config-editor') + ' - rose-config-editor') self.updater.update_all() self.updater.perform_startup_check() else: @@ -1850,9 +1850,9 @@ def spawn_window(config_directory_path=None, debug_mode=False, warnings.filterwarnings('ignore') resourcer = metomi.rose.resource.ResourceLocator.default() metomi.rose.gtk.util.rc_setup( - resourcer.locate('metomi.rose.config-edit/.gtkrc-2.0')) + resourcer.locate('rose-config-edit/.gtkrc-2.0')) metomi.rose.gtk.util.setup_stock_icons() - logo = resourcer.locate('images/metomi.rose.splash-logo.png') + logo = resourcer.locate('images/rose-splash-logo.png') if metomi.rose.config_editor.ICON_PATH_SCHEDULER is None: gcontrol_icon = None else: @@ -1953,18 +1953,18 @@ def get_number_of_configs(config_directory_path=None): def main(): """Launch from the command line.""" - if (Gtk.pygtk_version[0] < metomi.rose.config_editor.MIN_PYGTK_VERSION[0] or - Gtk.pygtk_version[1] < metomi.rose.config_editor.MIN_PYGTK_VERSION[1]): - this_version = '{0}.{1}.{2}'.format(*Gtk.pygtk_version) - required_version = '{0}.{1}.{2}'.format( - *metomi.rose.config_editor.MIN_PYGTK_VERSION) - metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( - required_version, this_version), - metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE - ) - sys.exit(1) + # if (Gtk.pygtk_version[0] < metomi.rose.config_editor.MIN_PYGTK_VERSION[0] or + # Gtk.pygtk_version[1] < metomi.rose.config_editor.MIN_PYGTK_VERSION[1]): + # this_version = '{0}.{1}.{2}'.format(*Gtk.pygtk_version) + # required_version = '{0}.{1}.{2}'.format( + # *metomi.rose.config_editor.MIN_PYGTK_VERSION) + # metomi.rose.gtk.dialog.run_dialog( + # metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + # metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( + # required_version, this_version), + # metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE + # ) + # sys.exit(1) sys.path.append(os.getenv('ROSE_HOME')) opt_parser = metomi.rose.opt_parse.RoseOptionParser() opt_parser.add_my_options("conf_dir", "meta_path", "new_mode", diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index e704a23de..491fddee8 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -39,8 +39,8 @@ import metomi.rose.macro import metomi.rose.macros import metomi.rose.popen -import metomi.rose.suite_control -import metomi.rose.suite_engine_proc +# import metomi.rose.suite_control +# import metomi.rose.suite_engine_proc class MenuBar(object): @@ -219,7 +219,7 @@ class MenuBar(object): metomi.rose.config_editor.ACCEL_TERMINAL), ('View Output', Gtk.STOCK_DIRECTORY, metomi.rose.config_editor.TOP_MENU_TOOLS_VIEW_OUTPUT), - ('Open Suite GControl', "metomi.rose.gtk-scheduler", + ('Open Suite GControl', "rose-gtk-scheduler", metomi.rose.config_editor.TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL), ('Help', None, metomi.rose.config_editor.TOP_MENU_HELP), @@ -338,10 +338,10 @@ def add_macro(self, config_name, modulename, classname, methodname, config_item.get_submenu().append(macro_item) if (methodname == metomi.rose.macro.VALIDATE_METHOD): for item in config_item.get_submenu().get_children(): - if hasattr(item, "_metomi.rose.all_validators"): + if hasattr(item, "_rose_all_validators"): return False all_item = Gtk.ImageMenuItem(Gtk.STOCK_DIALOG_QUESTION) - all_item._metomi.rose.all_validators = True + all_item._rose_all_validators = True all_item.set_label(metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) all_item.set_tooltip_text( metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index ac3685027..5df6dce8f 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -20,7 +20,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config_editor import metomi.rose.config_editor.util @@ -32,12 +32,12 @@ class MenuWidget(Gtk.HBox): """This class generates a button with a menu for variable actions.""" - MENU_ICON_ERRORS = 'metomi.rose.gtk-gnome-package-system-errors' - MENU_ICON_WARNINGS = 'metomi.rose.gtk-gnome-package-system-warnings' - MENU_ICON_LATENT = 'metomi.rose.gtk-gnome-add' - MENU_ICON_LATENT_ERRORS = 'metomi.rose.gtk-gnome-add-errors' - MENU_ICON_LATENT_WARNINGS = 'metomi.rose.gtk-gnome-add-warnings' - MENU_ICON_NORMAL = 'metomi.rose.gtk-gnome-package-system-normal' + MENU_ICON_ERRORS = 'rose-gtk-gnome-package-system-errors' + MENU_ICON_WARNINGS = 'rose-gtk-gnome-package-system-warnings' + MENU_ICON_LATENT = 'rose-gtk-gnome-add' + MENU_ICON_LATENT_ERRORS = 'rose-gtk-gnome-add-errors' + MENU_ICON_LATENT_WARNINGS = 'rose-gtk-gnome-add-warnings' + MENU_ICON_NORMAL = 'rose-gtk-gnome-package-system-normal' def __init__(self, variable, var_ops, remove_func, update_func, launch_help_func): @@ -63,7 +63,7 @@ def load_contents(self): """ - actions = [('Options', 'metomi.rose.gtk-gnome-package-system', ''), + actions = [('Options', 'rose-gtk-gnome-package-system', ''), ('Info', Gtk.STOCK_INFO, metomi.rose.config_editor.VAR_MENU_INFO), ('Help', Gtk.STOCK_HELP, @@ -82,7 +82,7 @@ def load_contents(self): metomi.rose.config_editor.VAR_MENU_REMOVE), ('Add', Gtk.STOCK_ADD, metomi.rose.config_editor.VAR_MENU_ADD)] - menu_icon_id = 'metomi.rose.gtk-gnome-package-system' + menu_icon_id = 'rose-gtk-gnome-package-system' is_comp = (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == metomi.rose.META_PROP_VALUE_TRUE) if self.is_ghost or is_comp: diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index 9e289ab0a..c9531a634 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -23,7 +23,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk, GdkPixbuf from gi.repository import GObject import metomi.rose.config @@ -93,7 +93,7 @@ def __init__(self, namespace_tree, launch_ns_func, str, str, int, int, int, int, bool, str, str, str) resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) - image_path = resource_loc.locate('etc/images/metomi.rose.config-edit') + image_path = resource_loc.locate('etc/images/rose-config-edit') self.null_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + '/null_icon.xpm') self.changed_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 521ef3385..0f63a0f11 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -345,7 +345,8 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): self.tool_hbox.pack_start(button_frame, expand=False, fill=False) label_box = Gtk.HBox(homogeneous=False, spacing=metomi.rose.config_editor.SPACING_PAGE) - label_box.pack_start(self.get_label_widget(is_detached=True, True, True, 0)) + # Had to remove True, True, 0 in below like Ben F + label_box.pack_start(self.get_label_widget(is_detached=True)) label_box.show() self.tool_hbox.pack_start( label_box, expand=True, fill=True, padding=10) diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index 7e12079de..11c6517ce 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -22,7 +22,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config_editor import metomi.rose.external diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index fb1bacae2..cc4647144 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -20,7 +20,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk from gi.repository import Pango import metomi.rose.config diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index 134fe9c27..ae2458e19 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -53,7 +53,7 @@ def get_stashmaster_lookup_dict(self): information. Subclasses *must* override the STASH_PACKAGE_PATH attribute with an - absolute path to a directory containing a metomi.rose.app.conf file with + absolute path to a directory containing a rose-app.conf file with STASH request package information. Subclasses should override the STASHMASTER_PATH attribute with an diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index 9510e97d4..c343e3212 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -21,7 +21,7 @@ from gi.repository import Pango import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config import metomi.rose.config_editor diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index d5755eb61..9d5fb6e0a 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -24,7 +24,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config import metomi.rose.config_editor @@ -142,7 +142,7 @@ def _generate_error_widget(self): self._error_widget.show() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate( - 'etc/images/metomi.rose.config-edit/error_icon.xpm') + 'etc/images/rose-config-edit/error_icon.xpm') image = Gtk.image_new_from_file(icon_path) image.show() self._error_widget.pack_start(image, expand=False, fill=False) diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py index 701beb1e0..887e82d86 100644 --- a/metomi/rose/config_editor/valuewidget/__init__.py +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -20,12 +20,8 @@ import re -import rose -from . import array.entry -from . import array.mixed -from . import array.logical -from . import array.python_list -from . import array.spaced_list +import metomi.rose +from . import array from . import booltoggle from . import character from . import combobox @@ -70,7 +66,7 @@ def copy(self): def chooser(value, metadata, error): """Select an appropriate widget class based on the arguments. - Note: metomi.rose.edit overrides this logic if a widget is hard coded. + Note: rose edit overrides this logic if a widget is hard coded. """ m_type = metadata.get(metomi.rose.META_PROP_TYPE) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 5d3e4ba20..2c1c487b5 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -22,9 +22,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor.util -import rose.gtk.util -import rose.variable +import metomi.rose.config_editor.util +import metomi.rose.gtk.util +import metomi.rose.variable class EntryArrayValueWidget(Gtk.HBox): @@ -45,33 +45,33 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.metadata = metadata self.set_value = set_value self.hook = hook - self.max_length = self.metadata[rose.META_PROP_LENGTH] + self.max_length = self.metadata[metomi.rose.META_PROP_LENGTH] - value_array = rose.variable.array_split(self.value) + value_array = metomi.rose.variable.array_split(self.value) self.chars_width = max([len(v) for v in value_array] + [1]) + 1 self.last_selected_src = None - arr_type = self.metadata.get(rose.META_PROP_TYPE) + arr_type = self.metadata.get(metomi.rose.META_PROP_TYPE) self.is_char_array = (arr_type == "character") self.is_quoted_array = (arr_type == "quoted") # Do not treat character or quoted arrays specially when incorrect. if self.is_char_array: - checker = rose.macros.value.ValueChecker() + checker = metomi.rose.macros.value.ValueChecker() for val in value_array: if not checker.check_character(val): self.is_char_array = False if self.is_quoted_array: - checker = rose.macros.value.ValueChecker() + checker = metomi.rose.macros.value.ValueChecker() for val in value_array: if not checker.check_quoted(val): self.is_quoted_array = False if self.is_char_array: for i, val in enumerate(value_array): value_array[i] = ( - rose.config_editor.util.text_for_character_widget(val)) + metomi.rose.config_editor.util.text_for_character_widget(val)) if self.is_quoted_array: for i, val in enumerate(value_array): value_array[i] = ( - rose.config_editor.util.text_for_quoted_widget(val)) + metomi.rose.config_editor.util.text_for_quoted_widget(val)) # Designate the number of allowed columns - 10 for 4 chars width self.num_allowed_columns = 3 self.entry_table = Gtk.Table(rows=1, @@ -110,9 +110,9 @@ def get_focus_index(self): for entry in self.entries: val = entry.get_text() if self.is_char_array: - val = rose.config_editor.util.text_from_character_widget(val) + val = metomi.rose.config_editor.util.text_from_character_widget(val) elif self.is_quoted_array: - val = rose.config_editor.util.text_from_quoted_widget(val) + val = metomi.rose.config_editor.util.text_from_quoted_widget(val) prefix = get_next_delimiter(self.value[len(text):], val) if prefix is None: return None @@ -125,7 +125,7 @@ def set_focus_index(self, focus_index=None): """Set the focus and position within the table of entries.""" if focus_index is None: return - value_array = rose.variable.array_split(self.value) + value_array = metomi.rose.variable.array_split(self.value) text = '' for i, val in enumerate(value_array): prefix = get_next_delimiter(self.value[len(text):], @@ -146,7 +146,7 @@ def set_focus_index(self, focus_index=None): def generate_entries(self, value_array=None): """Create the Gtk.Entry objects for elements in the array.""" if value_array is None: - value_array = rose.variable.array_split(self.value) + value_array = metomi.rose.variable.array_split(self.value) entries = [] for value_item in value_array: for entry in self.entries: @@ -370,8 +370,8 @@ def add_entry(self): self.entries.append(entry) self._adjust_entry_length() self.populate_table(focus_widget=entry) - if (self.metadata.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE): + if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE): self.setter(entry) def remove_entry(self): @@ -401,26 +401,26 @@ def setter(self, widget): if self.is_char_array: for i, val in enumerate(val_array): val_array[i] = ( - rose.config_editor.util.text_from_character_widget(val)) + metomi.rose.config_editor.util.text_from_character_widget(val)) elif self.is_quoted_array: for i, val in enumerate(val_array): val_array[i] = ( - rose.config_editor.util.text_from_quoted_widget(val)) + metomi.rose.config_editor.util.text_from_quoted_widget(val)) entries_have_commas = any("," in v for v in val_array) - new_value = rose.variable.array_join(val_array) + new_value = metomi.rose.variable.array_join(val_array) if new_value != self.value: self.value = new_value self.set_value(new_value) if (entries_have_commas and not (self.is_char_array or self.is_quoted_array)): - new_val_array = rose.variable.array_split(new_value) + new_val_array = metomi.rose.variable.array_split(new_value) if len(new_val_array) != len(self.entries): self.generate_entries() focus_index = None for i, val in enumerate(val_array): if "," in val: val_post_comma = val[:val.index(",") + 1] - focus_index = len(rose.variable.array_join( + focus_index = len(metomi.rose.variable.array_join( new_val_array[:i] + [val_post_comma])) self.populate_table() self.set_focus_index(focus_index) diff --git a/metomi/rose/config_editor/valuewidget/array/logical.py b/metomi/rose/config_editor/valuewidget/array/logical.py index 46d3a38ac..503551d2b 100644 --- a/metomi/rose/config_editor/valuewidget/array/logical.py +++ b/metomi/rose/config_editor/valuewidget/array/logical.py @@ -22,8 +22,8 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.gtk.util -import rose.variable +import metomi.rose.gtk.util +import metomi.rose.variable class LogicalArrayValueWidget(Gtk.HBox): @@ -40,29 +40,29 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.metadata = metadata self.set_value = set_value self.hook = hook - self.max_length = metadata[rose.META_PROP_LENGTH] - value_array = rose.variable.array_split(value) - if metadata.get(rose.META_PROP_TYPE) == "boolean": + self.max_length = metadata[metomi.rose.META_PROP_LENGTH] + value_array = metomi.rose.variable.array_split(value) + if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": # boolean -> true/false - self.allowed_values = [rose.TYPE_BOOLEAN_VALUE_FALSE, - rose.TYPE_BOOLEAN_VALUE_TRUE] + self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_BOOLEAN_VALUE_TRUE] self.label_dict = dict(list(zip(self.allowed_values, self.allowed_values))) - elif metadata.get(rose.META_PROP_TYPE) == "python_boolean": + elif metadata.get(metomi.rose.META_PROP_TYPE) == "python_boolean": # python_boolean -> True/False - self.allowed_values = [rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, - rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] + self.allowed_values = [metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] self.label_dict = dict(list(zip(self.allowed_values, self.allowed_values))) else: # logical -> .true./.false. - self.allowed_values = [rose.TYPE_LOGICAL_VALUE_FALSE, - rose.TYPE_LOGICAL_VALUE_TRUE] + self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE] self.label_dict = { - rose.TYPE_LOGICAL_VALUE_FALSE: - rose.TYPE_LOGICAL_FALSE_TITLE, - rose.TYPE_LOGICAL_VALUE_TRUE: - rose.TYPE_LOGICAL_TRUE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: + metomi.rose.TYPE_LOGICAL_FALSE_TITLE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: + metomi.rose.TYPE_LOGICAL_TRUE_TITLE} imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), (Gtk.STOCK_APPLY, Gtk.IconSize.MENU)] @@ -134,10 +134,10 @@ def get_entry(self, value_item): bad_img = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU) button = Gtk.ToggleButton() - button.options = [rose.TYPE_LOGICAL_VALUE_FALSE, - rose.TYPE_LOGICAL_VALUE_TRUE] - button.labels = [rose.TYPE_LOGICAL_FALSE_TITLE, - rose.TYPE_LOGICAL_TRUE_TITLE] + button.options = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE] + button.labels = [metomi.rose.TYPE_LOGICAL_FALSE_TITLE, + metomi.rose.TYPE_LOGICAL_TRUE_TITLE] button.set_tooltip_text(value_item) if value_item in self.allowed_values: index = self.allowed_values.index(value_item) @@ -251,8 +251,8 @@ def setter(self, *args): if value is None: value = '' val_array.append(value) - new_val = rose.variable.array_join(val_array) + new_val = metomi.rose.variable.array_join(val_array) self.value = new_val self.set_value(new_val) - self.value_array = rose.variable.array_split(self.value) + self.value_array = metomi.rose.variable.array_split(self.value) return False diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index ce93af854..27ab3b41f 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -26,8 +26,8 @@ from gi.repository import Gtk from . import entry -import rose.gtk.util -import rose.variable +import metomi.rose.gtk.util +import metomi.rose.variable class MixedArrayValueWidget(Gtk.HBox): @@ -43,8 +43,8 @@ class MixedArrayValueWidget(Gtk.HBox): """ - BAD_COLOUR = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + BAD_COLOUR = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match TIP_ADD = 'Add array element' TIP_DELETE = 'Remove last array element' @@ -58,19 +58,19 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.metadata = metadata self.set_value = set_value self.hook = hook - self.value_array = rose.variable.array_split(value) + self.value_array = metomi.rose.variable.array_split(value) self.extra_array = [] # For new rows self.element_values = [] self.rows = [] self.widgets = [] - self.unlimited = (metadata.get(rose.META_PROP_LENGTH) == ':') + self.unlimited = (metadata.get(metomi.rose.META_PROP_LENGTH) == ':') if self.unlimited: self.array_length = 1 else: - self.array_length = metadata.get(rose.META_PROP_LENGTH, 1) - self.num_cols = len(metadata[rose.META_PROP_TYPE]) + self.array_length = metadata.get(metomi.rose.META_PROP_LENGTH, 1) + self.num_cols = len(metadata[metomi.rose.META_PROP_TYPE]) self.types_row = [t for t in - metadata[rose.META_PROP_TYPE]] + metadata[metomi.rose.META_PROP_TYPE]] log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] @@ -139,7 +139,7 @@ def add_row(self, *args): new_values = self.insert_row(nrows + 1) if any(new_values): self.value_array = self.value_array + new_values - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) self.set_num_rows() self.normalise_width_widgets() @@ -180,7 +180,7 @@ def set_focus_index(self, focus_index=None): """Set the focus and position within the table.""" if focus_index is None: return - value_array = rose.variable.array_split(self.value) + value_array = metomi.rose.variable.array_split(self.value) text = '' widgets = [] for widget_list in self.rows: @@ -217,7 +217,7 @@ def del_row(self, *args): self.entry_table.resize(nrows, self.num_cols) chop_index = len(self.value_array) - self.num_cols self.value_array = self.value_array[:chop_index] - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) self.set_num_rows() self._decide_show_buttons() @@ -253,8 +253,8 @@ def insert_row(self, row_index): while value_index > len(self.value_array) - 1: value_index -= len(self.types_row) if value_index < 0: - w_value = rose.variable.get_value_from_metadata( - {rose.META_PROP_TYPE: el_piece_type}) + w_value = metomi.rose.variable.get_value_from_metadata( + {metomi.rose.META_PROP_TYPE: el_piece_type}) else: w_value = self.value_array[value_index] new_values.append(w_value) @@ -267,9 +267,9 @@ def insert_row(self, row_index): if w_value != '': hover_text = self.TIP_INVALID_ENTRY.format( el_piece_type) - w_error = {rose.META_PROP_TYPE: hover_text} - w_meta = {rose.META_PROP_TYPE: el_piece_type} - widget_cls = rose.config_editor.valuewidget.chooser( + w_error = {metomi.rose.META_PROP_TYPE: hover_text} + w_meta = {metomi.rose.META_PROP_TYPE: el_piece_type} + widget_cls = metomi.rose.config_editor.valuewidget.chooser( w_value, w_meta, w_error) hook = self.hook setter = ArrayElementSetter(self.setter, unwrapped_index) @@ -396,7 +396,7 @@ def setter(self, array_index, element_value): self.extra_array = self.extra_array[ok_index:] else: self.value_array[array_index] = element_value - new_val = rose.variable.array_join(self.value_array) + new_val = metomi.rose.variable.array_join(self.value_array) if new_val != self.value: self.value = new_val self.set_value(new_val) diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 870b3b203..8faf2fab8 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -25,9 +25,9 @@ from gi.repository import Gtk from . import entry -import rose.config_editor.util -import rose.gtk.util -import rose.variable +import metomi.rose.config_editor.util +import metomi.rose.gtk.util +import metomi.rose.variable class PythonListValueWidget(Gtk.HBox): @@ -339,8 +339,8 @@ def add_entry(self): self.entries.append(widget) self._adjust_entry_length() self.populate_table(focus_widget=widget) - if (self.metadata.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE): + if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE): self.setter(widget) def remove_entry(self): @@ -355,8 +355,8 @@ def remove_entry(self): text = self.entries[-1].get_text() widget = self.entries.pop() self.populate_table() - if (self.metadata.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE or text): + if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE or text): # Optional, or compulsory but not blank. self.setter(widget) @@ -443,7 +443,7 @@ def python_array_split(value): value_array = ast.literal_eval(value) except (SyntaxError, ValueError): value_no_brackets = value.lstrip("[").rstrip("]") - value_array = rose.variable.array_split(value_no_brackets) + value_array = metomi.rose.variable.array_split(value_no_brackets) return value_array cast_value_array = [] for value in value_array: diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index dd391bb58..b8f8990bc 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -26,16 +26,16 @@ from gi.repository import Gtk from . import entry -import rose.gtk.util -import rose.variable +import metomi.rose.gtk.util +import metomi.rose.variable class RowArrayValueWidget(Gtk.HBox): """This is a class to represent a value as part of a row.""" - BAD_COLOUR = rose.gtk.util.color_parse( - rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + BAD_COLOUR = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match TIP_ADD = 'Add array element' TIP_DELETE = 'Remove last array element' @@ -49,14 +49,14 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.metadata = metadata self.set_value = set_value self.hook = hook - self.value_array = rose.variable.array_split(value) + self.value_array = metomi.rose.variable.array_split(value) self.extra_array = [] # For new rows self.element_values = [] self.rows = [] self.widgets = [] self.has_length_error = False - self.length = metadata.get(rose.META_PROP_LENGTH) - self.type = metadata.get(rose.META_PROP_TYPE, "raw") + self.length = metadata.get(metomi.rose.META_PROP_LENGTH) + self.type = metadata.get(metomi.rose.META_PROP_TYPE, "raw") self.num_cols = len(self.value_array) if arg_str is None: if isinstance(self.type, list): @@ -69,7 +69,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): if self.unlimited: self.array_length = 1 else: - self.array_length = metadata.get(rose.META_PROP_LENGTH, 1) + self.array_length = metadata.get(metomi.rose.META_PROP_LENGTH, 1) log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] @@ -140,10 +140,10 @@ def grab_focus(self): def add_element(self, *args): """Create a new element (non-derived types).""" - w_value = rose.variable.get_value_from_metadata( - {rose.META_PROP_TYPE: self.type}) + w_value = metomi.rose.variable.get_value_from_metadata( + {metomi.rose.META_PROP_TYPE: self.type}) self.value_array = self.value_array + [w_value] - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) for child in self.entry_table.get_children(): self.entry_table.remove(child) @@ -160,7 +160,7 @@ def add_row(self, *args): new_values = self.insert_row(nrows + 1) if any(new_values): self.value_array = self.value_array + new_values - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) self.set_num_rows() self.normalise_width_widgets() @@ -199,7 +199,7 @@ def set_focus_index(self, focus_index=None): """Set the focus and position within the table.""" if focus_index is None: return - value_array = rose.variable.array_split(self.value) + value_array = metomi.rose.variable.array_split(self.value) text = '' widgets = [] for widget_list in self.rows: @@ -225,7 +225,7 @@ def set_focus_index(self, focus_index=None): def del_element(self, *args): """Create a new element (non-derived types).""" self.value_array.pop() - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) for child in self.entry_table.get_children(): self.entry_table.remove(child) @@ -247,7 +247,7 @@ def del_row(self, *args): chop_index = len(self.value_array) - len(self.get_types()) self.value_array = self.value_array[:chop_index] - self.value = rose.variable.array_join(self.value_array) + self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) self.set_num_rows() self.normalise_width_widgets() @@ -303,8 +303,8 @@ def insert_row(self, row_index): while value_index > len(self.value_array) - 1: value_index -= actual_num_cols if value_index < 0: - w_value = rose.variable.get_value_from_metadata( - {rose.META_PROP_TYPE: el_piece_type}) + w_value = metomi.rose.variable.get_value_from_metadata( + {metomi.rose.META_PROP_TYPE: el_piece_type}) else: w_value = self.value_array[value_index] new_values.append(w_value) @@ -317,9 +317,9 @@ def insert_row(self, row_index): if w_value != '': hover_text = self.TIP_INVALID_ENTRY.format( el_piece_type) - w_error = {rose.META_PROP_TYPE: hover_text} - w_meta = {rose.META_PROP_TYPE: el_piece_type} - widget_cls = rose.config_editor.valuewidget.chooser( + w_error = {metomi.rose.META_PROP_TYPE: hover_text} + w_meta = {metomi.rose.META_PROP_TYPE: el_piece_type} + widget_cls = metomi.rose.config_editor.valuewidget.chooser( w_value, w_meta, w_error) hook = self.hook setter = ArrayElementSetter(self.setter, unwrapped_index) @@ -451,7 +451,7 @@ def setter(self, array_index, element_value): self.extra_array = self.extra_array[ok_index:] else: self.value_array[array_index] = element_value - new_val = rose.variable.array_join(self.value_array) + new_val = metomi.rose.variable.array_join(self.value_array) if new_val != self.value: self.value = new_val self.set_value(new_val) diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index 2d9a7bf07..c6dd18bae 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -24,9 +24,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -import rose.config_editor.util -import rose.gtk.util -import rose.variable +import metomi.rose.config_editor.util +import metomi.rose.gtk.util +import metomi.rose.variable class SpacedListValueWidget(Gtk.HBox): @@ -334,8 +334,8 @@ def add_entry(self): self.entries.append(entry) self._adjust_entry_length() self.populate_table(focus_widget=entry) - if (self.metadata.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE): + if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE): self.setter(entry) def remove_entry(self): @@ -350,8 +350,8 @@ def remove_entry(self): text = self.entries[-1].get_text() entry = self.entries.pop() self.populate_table() - if (self.metadata.get(rose.META_PROP_COMPULSORY) != - rose.META_PROP_VALUE_TRUE or text): + if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != + metomi.rose.META_PROP_VALUE_TRUE or text): # Optional, or compulsory but not blank. self.setter(entry) @@ -446,5 +446,5 @@ def spaced_array_split(value): try: value_array = shlex.split(value) except (SyntaxError, ValueError): - value_array = rose.variable.array_split(value) + value_array = metomi.rose.variable.array_split(value) return value_array diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py index 33ecc25da..50f66d5f2 100644 --- a/metomi/rose/config_editor/valuewidget/character.py +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from metomi.rose.import META_PROP_TYPE +from metomi.rose import META_PROP_TYPE import metomi.rose.config_editor.util diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py index bbbeea8e7..d474ea977 100644 --- a/metomi/rose/config_editor/valuewidget/choice.py +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -23,7 +23,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config_editor import metomi.rose.gtk.choice @@ -64,7 +64,7 @@ class ChoicesValueWidget(Gtk.HBox): # choices into the variable value. # The only supported format is "python" which outputs the # result of repr(my_list) - e.g. VARIABLE=["A", "B"]. - # If not specified, the format will default to metomi.rose.array + # If not specified, the format will default to rose array # standard e.g. VARIABLE=A, B. # --guess-groups # Extrapolate inter-choice dependencies from their names. diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index 462084b79..df5a160db 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -149,7 +149,7 @@ def launch_about_dialog(self, somewidget=None): metomi.rose.gtk.dialog.run_about_dialog( name=metomi.rose.config_editor.PROGRAM_NAME, copyright_=metomi.rose.config_editor.COPYRIGHT, - logo_path="etc/images/metomi.rose.logo.png", + logo_path="etc/images/rose-logo.png", website=metomi.rose.config_editor.PROJECT_URL) def _reload_choices(self, liststore, top_name, add_choices): @@ -262,7 +262,7 @@ def launch_graph_dialog(self, name_section_dict): def launch_help_dialog(self, somewidget=None): """Launch a browser to open the help url.""" webbrowser.open( - 'https://metomi.github.io/metomi.rose.doc/html/index.html', + 'https://metomi.github.io/rose/doc/html/index.html', new=True, autoraise=True ) diff --git a/metomi/rose/gtk/__init__.py b/metomi/rose/gtk/__init__.py new file mode 100644 index 000000000..063f7abd5 --- /dev/null +++ b/metomi/rose/gtk/__init__.py @@ -0,0 +1,8 @@ +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except (ImportError, RuntimeError, AssertionError): + INTERACTIVE_ENABLED = False +else: + INTERACTIVE_ENABLED = True diff --git a/metomi/rose/gtk/choice.py b/metomi/rose/gtk/choice.py new file mode 100644 index 000000000..569b09a06 --- /dev/null +++ b/metomi/rose/gtk/choice.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + +import metomi.rose + + +class ChoicesListView(Gtk.TreeView): + + """Class to hold and display an ordered list of strings. + + set_value is a function, accepting a new value string. + get_data is a function that accepts no arguments and returns an + ordered list of included names to display. + handle_search is a function that accepts a name and triggers a + search for it. + title is a string or Gtk.Widget displayed as the column header, if + given. + get_custom_menu_items, if given, should be a function that + accepts no arguments and returns a list of Gtk.MenuItem-derived + instances. The listview model and current TreeIter will be + available as attributes "_listview_model" and "_listview_iter" set + on each menu item to optionally use during the menu item callbacks + - this means that they can use them to modify the model + information. Menuitems that do this should connect to + "button-press-event", as the model cleanup will take place as a + connect_after to the same event. + + """ + + def __init__(self, set_value, get_data, handle_search, + title=metomi.rose.config_editor.CHOICE_TITLE_INCLUDED, + get_custom_menu_items=lambda: []): + super(ChoicesListView, self).__init__() + self._set_value = set_value + self._get_data = get_data + self._handle_search = handle_search + self._get_custom_menu_items = get_custom_menu_items + self.enable_model_drag_dest( + [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + self.enable_model_drag_source( + Gdk.ModifierType.BUTTON1_MASK, [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + self.connect("button-press-event", self._handle_button_press) + self.connect("drag-data-get", self._handle_drag_get) + self.connect_after("drag-data-received", + self._handle_drag_received) + self.set_rules_hint(True) + self.connect("row-activated", self._handle_activation) + self.show() + col = Gtk.TreeViewColumn() + if isinstance(title, Gtk.Widget): + col.set_widget(title) + else: + col.set_title(title) + cell_text = Gtk.CellRendererText() + cell_text.set_property('editable', True) + cell_text.connect('edited', self._handle_edited) + col.pack_start(cell_text, True, True, 0) + col.set_cell_data_func(cell_text, self._set_cell_text) + self.append_column(col) + self._populate() + + def _handle_activation(self, treeview, path, col): + """Handle a click on the main list view - start a search.""" + iter_ = treeview.get_model().get_iter(path) + name = treeview.get_model().get_value(iter_, 0) + self._handle_search(name) + return False + + def _handle_button_press(self, treeview, event): + """Handle a right click event on the main list view.""" + if not hasattr(event, "button") or event.button != 3: + return False + pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) + if pathinfo is None: + return False + iter_ = treeview.get_model().get_iter(pathinfo[0]) + self._popup_menu(iter_, event) + return False + + def _handle_drag_get(self, treeview, drag, sel, info, time): + """Handle an outgoing drag request.""" + model, iter_ = treeview.get_selection().get_selected() + text = model.get_value(iter_, 0) + sel.set_text(text) + model.remove(iter_) # Triggers the 'row-deleted' signal, sets value + if not model.iter_n_children(None): + model.append([metomi.rose.config_editor.CHOICE_LABEL_EMPTY]) + + def _handle_drag_received( + self, treeview, drag, xpos, ypos, sel, info, time): + """Handle an incoming drag request.""" + if sel.data is None: + return False + drop_info = treeview.get_dest_row_at_pos(xpos, ypos) + model = treeview.get_model() + if drop_info: + path, position = drop_info + if (position == Gtk.TreeViewDropPosition.BEFORE or + position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE): + model.insert(path[0], [sel.data]) + else: + model.insert(path[0] + 1, [sel.data]) + else: + model.append([sel.data]) + path = None + self._handle_reordering(model, path) + + def _handle_edited(self, cell, path, new_text): + """Handle cell text so it can be edited. """ + liststore = self.get_model() + iter_ = liststore.get_iter(path) + liststore.set_value(iter_, 0, new_text) + self._handle_reordering() + return + + def _handle_reordering(self, model=None, path=None): + """Handle a drag-and-drop rearrangement in the main list view.""" + if model is None: + model = self.get_model() + ok_values = [] + iter_ = model.get_iter_first() + num_entries = model.iter_n_children(None) + while iter_ is not None: + name = model.get_value(iter_, 0) + next_iter = model.iter_next(iter_) + if name == metomi.rose.config_editor.CHOICE_LABEL_EMPTY: + if num_entries > 1: + model.remove(iter_) + else: + ok_values.append(name) + iter_ = next_iter + new_value = " ".join(ok_values) + self._set_value(new_value) + + def _populate(self): + """Populate the main list view.""" + values = self._get_data() + model = Gtk.ListStore(str) + if not values: + values = [metomi.rose.config_editor.CHOICE_LABEL_EMPTY] + for value in values: + model.append([value]) + model.connect_after("row-deleted", self._handle_reordering) + self.set_model(model) + + def _popup_menu(self, iter_, event): + # Pop up a menu for the main list view. + """Launch a popup menu for add/clone/remove.""" + ui_config_string = """ + + """ + text = metomi.rose.config_editor.CHOICE_MENU_REMOVE + actions = [("Remove", Gtk.STOCK_DELETE, text)] + uimanager = Gtk.UIManager() + actiongroup = Gtk.ActionGroup('Popup') + actiongroup.add_actions(actions) + uimanager.insert_action_group(actiongroup, pos=0) + uimanager.add_ui_from_string(ui_config_string) + remove_item = uimanager.get_widget('/Popup/Remove') + remove_item.connect("activate", + lambda b: self._remove_iter(iter_)) + menu = uimanager.get_widget('/Popup') + for menuitem in self._get_custom_menu_items(): + menuitem._listview_model = self.get_model() + menuitem._listview_iter = iter_ + menuitem.connect_after( + "button-press-event", + lambda b, e: self._handle_reordering() + ) + menu.append(menuitem) + menu.popup(None, None, None, event.button, event.time) + return False + + def _remove_iter(self, iter_): + self.get_model().remove(iter_) + if self.get_model() is None: + # Removing the last iter makes get_model return None... + self._populate() + self._handle_reordering() + self._populate() + + def _set_cell_text(self, column, cell, model, r_iter): + name = model.get_value(r_iter, 0) + if name == metomi.rose.config_editor.CHOICE_LABEL_EMPTY: + cell.set_property("markup", "" + name + "") + else: + cell.set_property("markup", "" + name + "") + + def refresh(self): + """Update the model values.""" + self._populate() + + +class ChoicesTreeView(Gtk.TreeView): + + """Class to hold and display a tree of content. + + set_value is a function, accepting a new value string. + get_data is a function that accepts no arguments and returns a + list of included names. + get_available_data is a function that accepts no arguments and + returns a list of available names. + get_groups is a function that accepts a name and a list of + available names and returns groups that supercede name. + get_is_implicit is an optional function that accepts a name and + returns whether the name is implicitly included in the content. + title is a string displayed as the column header, if given. + get_is_included is an optional function that accepts a name and + an optional list of included names to test whether a + name is already included. + + """ + + def __init__(self, set_value, get_data, get_available_data, + get_groups, get_is_implicit=None, + title=metomi.rose.config_editor.CHOICE_TITLE_AVAILABLE, + get_is_included=None): + super(ChoicesTreeView, self).__init__() + # Generate the 'available' sections view. + self._set_value = set_value + self._get_data = get_data + self._get_available_data = get_available_data + self._get_groups = get_groups + self._get_is_implicit = get_is_implicit + self._get_is_included_func = get_is_included + self.set_headers_visible(True) + self.set_rules_hint(True) + self.enable_model_drag_dest( + [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + self.enable_model_drag_source( + Gdk.ModifierType.BUTTON1_MASK, [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + self.connect_after("button-release-event", self._handle_button) + self.connect("drag-begin", self._handle_drag_begin) + self.connect("drag-data-get", self._handle_drag_get) + self.connect("drag-end", self._handle_drag_end) + self._is_dragging = False + model = Gtk.TreeStore(str, bool, bool) + self.set_model(model) + col = Gtk.TreeViewColumn() + cell_toggle = Gtk.CellRendererToggle() + cell_toggle.connect_after("toggled", self._handle_cell_toggle) + col.pack_start(cell_toggle, False, True, 0) + col.set_cell_data_func(cell_toggle, self._set_cell_state) + self.append_column(col) + col = Gtk.TreeViewColumn() + col.set_title(title) + cell_text = Gtk.CellRendererText() + col.pack_start(cell_text, True, True, 0) + col.set_cell_data_func(cell_text, self._set_cell_text) + self.append_column(col) + self.set_expander_column(col) + self.show() + self._populate() + + def _get_is_included(self, name, ok_names=None): + if self._get_is_included_func is not None: + return self._get_is_included_func(name, ok_names) + if ok_names is None: + ok_names = self._get_available_data() + return name in ok_names + + def _populate(self): + """Populate the 'available' sections view.""" + ok_content_sections = self._get_available_data() + self._ok_content_sections = set(ok_content_sections) + ok_values = self._get_data() + model = self.get_model() + sections_left = list(ok_content_sections) + self._name_iter_map = {} + while sections_left: + name = sections_left.pop(0) + is_included = self._get_is_included(name, ok_values) + groups = self._get_groups(name, ok_content_sections) + if self._get_is_implicit is None: + is_implicit = any( + [self._get_is_included(g, ok_values) for g in groups]) + else: + is_implicit = self._get_is_implicit(name) + if groups: + iter_ = model.append(self._name_iter_map[groups[-1]], + [name, is_included, is_implicit]) + else: + iter_ = model.append(None, [name, is_included, is_implicit]) + self._name_iter_map[name] = iter_ + + def _realign(self): + """Refresh the states in the model.""" + ok_values = self._get_data() + model = self.get_model() + ok_content_sections = self._get_available_data() + for name, iter_ in list(self._name_iter_map.items()): + is_in_value = self._get_is_included(name, ok_values) + if self._get_is_implicit is None: + groups = self._get_groups(name, ok_content_sections) + is_implicit = any( + [self._get_is_included(g, ok_values) for g in groups]) + else: + is_implicit = self._get_is_implicit(name) + if model.get_value(iter_, 1) != is_in_value: + model.set_value(iter_, 1, is_in_value) + if model.get_value(iter_, 2) != is_implicit: + model.set_value(iter_, 2, is_implicit) + + def _set_cell_text(self, column, cell, model, r_iter): + """Set markup for a section depending on its status.""" + section_name = model.get_value(r_iter, 0) + is_in_value = model.get_value(r_iter, 1) + is_implicit = model.get_value(r_iter, 2) + r_iter = model.iter_children(r_iter) + while r_iter is not None: + if model.get_value(r_iter, 1): + is_in_value = True + break + r_iter = model.iter_next(r_iter) + if is_in_value: + cell.set_property("markup", "{0}".format(section_name)) + cell.set_property("sensitive", True) + elif is_implicit: + cell.set_property("markup", "{0}".format(section_name)) + cell.set_property("sensitive", False) + else: + cell.set_property("markup", section_name) + cell.set_property("sensitive", True) + + def _set_cell_state(self, column, cell, model, r_iter): + """Set the check box for a section depending on its status.""" + is_in_value = model.get_value(r_iter, 1) + is_implicit = model.get_value(r_iter, 2) + if is_in_value: + cell.set_property("active", True) + cell.set_property("sensitive", True) + elif is_implicit: + cell.set_property("active", True) + cell.set_property("sensitive", False) + else: + cell.set_property("active", False) + cell.set_property("sensitive", True) + if not self._check_can_add(r_iter): + cell.set_property("sensitive", False) + + def _handle_drag_begin(self, widget, drag): + self._is_dragging = True + + def _handle_drag_end(self, widget, drag): + self._is_dragging = False + + def _handle_drag_get(self, treeview, drag, sel, info, time): + """Handle a drag data get.""" + model, iter_ = treeview.get_selection().get_selected() + if not self._check_can_add(iter_): + return False + name = model.get_value(iter_, 0) + sel.set("text/plain", 8, name) + + def _check_can_add(self, iter_): + """Check whether a name can be added to the data.""" + model = self.get_model() + if model.get_value(iter_, 1) or model.get_value(iter_, 2): + return False + child_iter = model.iter_children(iter_) + while child_iter is not None: + if (model.get_value(child_iter, 1) or + model.get_value(child_iter, 2)): + return False + child_iter = model.iter_next(child_iter) + return True + + def _handle_button(self, treeview, event): + """Connect a left click on the available section to a toggle.""" + if event.button != 1 or self._is_dragging: + return False + pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) + if pathinfo is None: + return False + path, col = pathinfo[0:2] + if treeview.get_columns().index(col) == 1: + self._handle_cell_toggle(None, path) + + def _handle_cell_toggle(self, cell, path, should_turn_off=None): + """Change the content variable value here. + + cell is not used. + path is the name to turn off or on. + should_turn_off is as follows: + None - toggle based on the cell value + False - toggle on + True - toggle off + + """ + text_index = 0 + model = self.get_model() + r_iter = model.get_iter(path) + this_name = model.get_value(r_iter, text_index) + ok_values = self._get_data() + model = self.get_model() + can_add = self._check_can_add(r_iter) + should_add = False + if ((should_turn_off is None or should_turn_off) and + self._get_is_included(this_name, ok_values)): + ok_values.remove(this_name) + elif should_turn_off is None or not should_turn_off: + if not can_add: + return False + should_add = True + ok_values = ok_values + [this_name] + else: + self._realign() + return False + model.set_value(r_iter, 1, should_add) + if model.iter_n_children(r_iter): + self._toggle_internal_base(r_iter, this_name, should_add) + self._set_value(" ".join(ok_values)) + self._realign() + return False + + def _toggle_internal_base(self, base_iter, base_name, added=False): + """Connect a toggle of a group to its children. + + base_iter is the iter pointing to the group + base_name is the name of the group + added is a boolean denoting toggle state + + """ + model = self.get_model() + iter_ = model.iter_children(base_iter) + skip_children = False + while iter_ is not None: + model.set_value(iter_, 2, added) + if not skip_children: + next_iter = model.iter_children(iter_) + if skip_children or next_iter is None: + next_iter = model.iter_next(iter_) + skip_children = False + if next_iter is None: + next_iter = model.iter_parent(iter_) + skip_children = True + iter_ = next_iter + return False + + def refresh(self): + """Refresh the model.""" + self._realign() diff --git a/metomi/rose/gtk/console.py b/metomi/rose/gtk/console.py new file mode 100644 index 000000000..93de4ca28 --- /dev/null +++ b/metomi/rose/gtk/console.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import datetime + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import metomi.rose.resource + + +class ConsoleWindow(Gtk.Window): + + """Create an error console window.""" + + CATEGORY_ALL = "All" + COLUMN_TITLE_CATEGORY = "Type" + COLUMN_TITLE_MESSAGE = "Message" + COLUMN_TITLE_TIME = "Time" + DEFAULT_SIZE = (600, 300) + TITLE = "Error Console" + + def __init__(self, categories, category_message_time_tuples, + category_stock_ids, default_size=None, parent=None, + destroy_hook=None): + super(ConsoleWindow, self).__init__() + if parent is not None: + self.set_transient_for(parent) + if default_size is None: + default_size = self.DEFAULT_SIZE + self.set_default_size(*default_size) + self.set_title(self.TITLE) + self._filter_category = self.CATEGORY_ALL + self.categories = categories + self.category_icons = [] + for id_ in category_stock_ids: + self.category_icons.append( + self.render_icon(id_, Gtk.IconSize.MENU)) + self._destroy_hook = destroy_hook + top_vbox = Gtk.VBox() + top_vbox.show() + self.add(top_vbox) + + message_scrolled_window = Gtk.ScrolledWindow() + message_scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + message_scrolled_window.show() + self._message_treeview = Gtk.TreeView() + self._message_treeview.show() + self._message_treeview.set_rules_hint(True) + + # Set up the category column (icons). + category_column = Gtk.TreeViewColumn() + category_column.set_title(self.COLUMN_TITLE_CATEGORY) + cell_category = Gtk.CellRendererPixbuf() + category_column.pack_start(cell_category, False, True, 0) + category_column.set_cell_data_func(cell_category, + self._set_category_cell, 0) + category_column.set_clickable(True) + category_column.connect("clicked", self._sort_column, 0) + self._message_treeview.append_column(category_column) + + # Set up the message column (info text). + message_column = Gtk.TreeViewColumn() + message_column.set_title(self.COLUMN_TITLE_MESSAGE) + cell_message = Gtk.CellRendererText() + message_column.pack_start(cell_message, False, True, 0) + message_column.add_attribute(cell_message, attribute="text", + column=1) + message_column.set_clickable(True) + message_column.connect("clicked", self._sort_column, 1) + self._message_treeview.append_column(message_column) + + # Set up the time column (text). + time_column = Gtk.TreeViewColumn() + time_column.set_title(self.COLUMN_TITLE_TIME) + cell_time = Gtk.CellRendererText() + time_column.pack_start(cell_time, False, True, 0) + time_column.set_cell_data_func(cell_time, self._set_time_cell, 2) + time_column.set_clickable(True) + time_column.set_sort_indicator(True) + time_column.connect("clicked", self._sort_column, 2) + self._message_treeview.append_column(time_column) + + self._message_store = Gtk.TreeStore(str, str, int) + for category, message, time in category_message_time_tuples: + self._message_store.append(None, [category, message, time]) + filter_model = self._message_store.filter_new() + filter_model.set_visible_func(self._get_should_show) + self._message_treeview.set_model(filter_model) + + message_scrolled_window.add(self._message_treeview) + top_vbox.pack_start(message_scrolled_window, expand=True, fill=True) + + category_hbox = Gtk.HBox() + category_hbox.show() + top_vbox.pack_end(category_hbox, expand=False, fill=False) + for category in categories + [self.CATEGORY_ALL]: + togglebutton = Gtk.ToggleButton(label=category, + use_underline=False) + togglebutton.connect("toggled", + lambda b: self._set_new_filter( + b, category_hbox.get_children())) + togglebutton.show() + category_hbox.pack_start(togglebutton, expand=True, fill=True) + togglebutton.set_active(True) + self.show() + self._scroll_to_end() + self.connect("destroy", self._handle_destroy) + + def _handle_destroy(self, window): + if self._destroy_hook is not None: + self._destroy_hook() + + def _get_should_show(self, model, iter_): + # Determine whether to show a row. + category = model.get_value(iter_, 0) + if self._filter_category not in [self.CATEGORY_ALL, category]: + return False + return True + + def _scroll_to_end(self): + # Scroll the Treeview to the end of the rows. + model = self._message_treeview.get_model() + iter_ = model.get_iter_first() + if iter_ is None: + return + while True: + next_iter = model.iter_next(iter_) + if next_iter is None: + break + iter_ = next_iter + path = model.get_path(iter_) + self._message_treeview.scroll_to_cell(path) + self._message_treeview.set_cursor(path) + self._message_treeview.grab_focus() + + def _set_category_cell(self, column, cell, model, r_iter, index): + category = model.get_value(r_iter, index) + icon = self.category_icons[self.categories.index(category)] + cell.set_property("pixbuf", icon) + + def _set_new_filter(self, togglebutton, togglebuttons): + category = togglebutton.get_label() + if not togglebutton.get_active(): + return False + self._filter_category = category + self._message_treeview.get_model().refilter() + for button in togglebuttons: + if button != togglebutton: + button.set_active(False) + + def _set_time_cell(self, column, cell, model, r_iter, index): + message_time = model.get_value(r_iter, index) + text = datetime.datetime.fromtimestamp(message_time).strftime( + metomi.rose.config_editor.EVENT_TIME_LONG) + cell.set_property("text", text) + + def _sort_column(self, column, index): + # Sort a column. + new_sort_order = Gtk.SortType.ASCENDING + if column.get_sort_order() == Gtk.SortType.ASCENDING: + new_sort_order = Gtk.SortType.DESCENDING + column.set_sort_order(new_sort_order) + for other_column in self._message_treeview.get_columns(): + other_column.set_sort_indicator(column == other_column) + self._message_store.set_sort_column_id(index, new_sort_order) + + def update_messages(self, category_message_time_tuples): + # Update the messages. + self._message_store.clear() + for category, message, time in category_message_time_tuples: + self._message_store.append(None, [category, message, time]) + self._scroll_to_end() diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py new file mode 100644 index 000000000..87374378a --- /dev/null +++ b/metomi/rose/gtk/dialog.py @@ -0,0 +1,759 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +from multiprocessing import Process +import os +import queue +import shlex +from subprocess import Popen, PIPE +import sys +import tempfile +import time +import traceback +import webbrowser + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GdkPixbuf +from gi.repository import GLib +from gi.repository import Pango + +import metomi.rose.gtk.util +import metomi.rose.resource + + +DIALOG_BUTTON_CLOSE = "Close" +DIALOG_LABEL_README = "README" +DIALOG_PADDING = 10 +DIALOG_SUB_PADDING = 5 + +DIALOG_SIZE_PROCESS = (400, 100) +DIALOG_SIZE_SCROLLED_MAX = (600, 600) +DIALOG_SIZE_SCROLLED_MIN = (300, 100) + +DIALOG_TEXT_SHUTDOWN_ASAP = "Shutdown ASAP." +DIALOG_TEXT_SHUTTING_DOWN = "Shutting down." +DIALOG_TEXT_UNCAUGHT_EXCEPTION = ("{0} has crashed. {1}" + + "\n\n{2}: {3}\n{4}") +DIALOG_TITLE_ERROR = "Error" +DIALOG_TITLE_UNCAUGHT_EXCEPTION = "Critical error" +DIALOG_TITLE_EXTRA_INFO = "Further information" +DIALOG_TYPE_ERROR = Gtk.MessageType.ERROR +DIALOG_TYPE_INFO = Gtk.MessageType.INFO +DIALOG_TYPE_WARNING = Gtk.MessageType.WARNING + + +class DialogProcess(object): + + """Run a forked process and display a dialog while it runs. + + cmd_args can either be a list of shell command components + e.g. ['sleep', '100'] or a list containing a python function + followed by any function arguments e.g. [func, '100']. + description is used for the label, if not None + title is used for the title, if not None + stock_id is used for the dialog icon + hide_progress removes the bouncing progress bar + + Returns the exit code of the process. + + """ + + DIALOG_FUNCTION_LABEL = "Executing function" + DIALOG_LOG_LABEL = "Show log" + DIALOG_PROCESS_LABEL = "Executing command" + + def __init__(self, cmd_args, description=None, title=None, + stock_id=Gtk.STOCK_EXECUTE, + hide_progress=False, modal=True, + event_queue=None): + self.proc = None + window = get_dialog_parent() + self.dialog = Gtk.Dialog(buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), + parent=window) + self.dialog.set_modal(modal) + self.dialog.set_default_size(*DIALOG_SIZE_PROCESS) + self._is_destroyed = False + self.dialog.set_icon(self.dialog.render_icon(Gtk.STOCK_EXECUTE, + Gtk.IconSize.MENU)) + self.cmd_args = cmd_args + self.event_queue = event_queue + str_cmd_args = [metomi.rose.gtk.util.safe_str(a) for a in cmd_args] + if description is not None: + str_cmd_args = [description] + if title is None: + self.dialog.set_title(" ".join(str_cmd_args[0:2])) + else: + self.dialog.set_title(title) + if callable(cmd_args[0]): + self.label = Gtk.Label(label=self.DIALOG_FUNCTION_LABEL) + else: + self.label = Gtk.Label(label=self.DIALOG_PROCESS_LABEL) + self.label.set_use_markup(True) + self.label.show() + self.image = Gtk.Image.new_from_stock(stock_id, + Gtk.IconSize.DIALOG) + self.image.show() + image_vbox = Gtk.VBox() + image_vbox.pack_start(self.image, expand=False, fill=False) + image_vbox.show() + top_hbox = Gtk.HBox() + top_hbox.pack_start(image_vbox, expand=False, fill=False, + padding=DIALOG_PADDING) + top_hbox.show() + hbox = Gtk.HBox() + hbox.pack_start(self.label, expand=False, fill=False, + padding=DIALOG_PADDING) + hbox.show() + main_vbox = Gtk.VBox() + main_vbox.show() + main_vbox.pack_start(hbox, expand=False, fill=False, + padding=DIALOG_SUB_PADDING) + + cmd_string = str_cmd_args[0] + if str_cmd_args[1:]: + if callable(cmd_args[0]): + cmd_string += "(" + " ".join(str_cmd_args[1:]) + ")" + else: + cmd_string += " " + " ".join(str_cmd_args[1:]) + self.cmd_label = Gtk.Label() + self.cmd_label.set_markup("" + cmd_string + "") + self.cmd_label.show() + cmd_hbox = Gtk.HBox() + cmd_hbox.pack_start(self.cmd_label, expand=False, fill=False, + padding=DIALOG_PADDING) + cmd_hbox.show() + main_vbox.pack_start(cmd_hbox, expand=False, fill=True, + padding=DIALOG_SUB_PADDING) + # self.dialog.set_modal(True) + self.progress_bar = Gtk.ProgressBar() + self.progress_bar.set_pulse_step(0.1) + self.progress_bar.show() + hbox = Gtk.HBox() + hbox.pack_start(self.progress_bar, expand=True, fill=True, + padding=DIALOG_PADDING) + hbox.show() + main_vbox.pack_start(hbox, expand=False, fill=False, + padding=DIALOG_SUB_PADDING) + top_hbox.pack_start(main_vbox, expand=True, fill=True, + padding=DIALOG_PADDING) + if self.event_queue is None: + self.dialog.vbox.pack_start(top_hbox, expand=True, fill=True) + else: + text_view_scroll = Gtk.ScrolledWindow() + text_view_scroll.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + text_view_scroll.show() + text_view = Gtk.TextView() + text_view.show() + self.text_buffer = text_view.get_buffer() + self.text_tag = self.text_buffer.create_tag() + self.text_tag.set_property("scale", Pango.SCALE_SMALL) + text_view.connect('size-allocate', self._handle_scroll_text_view) + text_view_scroll.add(text_view) + text_expander = Gtk.Expander(self.DIALOG_LOG_LABEL) + text_expander.set_spacing(DIALOG_SUB_PADDING) + text_expander.add(text_view_scroll) + text_expander.show() + top_pane = Gtk.VPaned() + top_pane.pack1(top_hbox, resize=False, shrink=False) + top_pane.show() + self.dialog.vbox.pack_start(top_pane, expand=True, fill=True, + padding=DIALOG_SUB_PADDING) + top_pane.pack2(text_expander, resize=True, shrink=True) + if hide_progress: + progress_bar.hide() + self.ok_button = self.dialog.get_action_area().get_children()[0] + self.ok_button.hide() + for child in self.dialog.vbox.get_children(): + if isinstance(child, Gtk.HSeparator): + child.hide() + self.dialog.show() + + def run(self): + """Launch dialog in child process.""" + stdout = tempfile.TemporaryFile() + stderr = tempfile.TemporaryFile() + self.proc = Process( + target=_sep_process, args=[self.cmd_args, stdout, stderr]) + self.proc.start() + self.dialog.connect("destroy", self._handle_dialog_process_destroy) + while self.proc.is_alive(): + self.progress_bar.pulse() + if self.event_queue is not None: + while True: + try: + new_text = self.event_queue.get(False) + except queue.Empty: + break + end = self.text_buffer.get_end_iter() + self.text_buffer.insert_with_tags(end, new_text, + self.text_tag) + while Gtk.events_pending(): + Gtk.main_iteration() + time.sleep(0.1) + stdout.seek(0) + stderr.seek(0) + if self.proc.exitcode != 0: + if self._is_destroyed: + return self.proc.exitcode + else: + self.image.set_from_stock(Gtk.STOCK_DIALOG_ERROR, + Gtk.IconSize.DIALOG) + self.label.hide() + self.progress_bar.hide() + self.cmd_label.set_markup( + "" + metomi.rose.gtk.util.safe_str(stderr.read()) + "") + self.ok_button.show() + for child in self.dialog.vbox.get_children(): + if isinstance(child, Gtk.HSeparator): + child.show() + self.dialog.run() + self.dialog.destroy() + return self.proc.exitcode + + def _handle_dialog_process_destroy(self, dialog): + if self.proc.is_alive(): + self.proc.terminate() + self._is_destroyed = True + return False + + def _handle_scroll_text_view(self, text_view, event=None): + """Scroll the parent scrolled window to the bottom.""" + vadj = text_view.get_parent().get_vadjustment() + if vadj.upper > vadj.lower + vadj.page_size: + vadj.set_value(vadj.upper - 0.95 * vadj.page_size) + + +def _sep_process(*args): + sys.exit(_process(*args)) + + +def _process(cmd_args, stdout=sys.stdout, stderr=sys.stderr): + if callable(cmd_args[0]): + func = cmd_args.pop(0) + try: + func(*cmd_args) + except Exception as exc: + stderr.write(type(exc).__name__ + ": " + str(exc) + "\n") + stderr.read() + return 1 + return 0 + proc = Popen(cmd_args, stdout=PIPE, stderr=PIPE) + for line in iter(proc.stdout.readline, ""): + stdout.write(line) + for line in iter(proc.stderr.readline, ""): + stderr.write(line) + proc.wait() + stdout.read() # Magically keep it alive!? + stderr.read() + return proc.poll() + + +def run_about_dialog(name=None, copyright_=None, + logo_path=None, website=None): + parent_window = get_dialog_parent() + about_dialog = Gtk.AboutDialog() + about_dialog.set_transient_for(parent_window) + about_dialog.set_name(name) + licence_path = os.path.join(os.getenv("ROSE_HOME"), + metomi.rose.FILEPATH_README) + about_dialog.set_license(open(licence_path, "r").read()) + about_dialog.set_copyright(copyright_) + resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) + logo_path = resource_loc.locate(logo_path) + about_dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file(logo_path)) + about_dialog.set_website(website) + Gtk.about_dialog_set_url_hook( + lambda u, v, w: webbrowser.open(w), about_dialog.get_website()) + about_dialog.run() + about_dialog.destroy() + + +def run_command_arg_dialog(cmd_name, help_text, run_hook): + """Launch a dialog to get extra arguments for a command.""" + checker_function = lambda t: True + dialog, container, name_entry = get_naming_dialog(cmd_name, + checker_function) + dialog.set_title(cmd_name) + help_label = Gtk.stock_lookup(Gtk.STOCK_HELP)[1].strip("_") + help_button = metomi.rose.gtk.util.CustomButton( + stock_id=Gtk.STOCK_HELP, + label=help_label, + size=Gtk.IconSize.LARGE_TOOLBAR) + help_button.connect( + "clicked", + lambda b: run_scrolled_dialog(help_text, title=help_label)) + help_hbox = Gtk.HBox() + help_hbox.pack_start(help_button, expand=False, fill=False) + help_hbox.show() + container.pack_end(help_hbox, expand=False, fill=False) + name_entry.grab_focus() + dialog.connect("response", _handle_command_arg_response, run_hook, + name_entry) + dialog.set_modal(False) + dialog.show() + + +def _handle_command_arg_response(dialog, response, run_hook, entry): + text = entry.get_text() + dialog.destroy() + if response == Gtk.ResponseType.ACCEPT: + run_hook(shlex.split(text)) + + +def run_dialog(dialog_type, text, title=None, modal=True, + cancel=False, extra_text=None): + """Run a simple dialog with an 'OK' button and some text.""" + parent_window = get_dialog_parent() + dialog = Gtk.Dialog(parent=parent_window) + if parent_window is None: + dialog.set_icon(metomi.rose.gtk.util.get_icon()) + if cancel: + dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + if extra_text: + info_button = Gtk.Button(stock=Gtk.STOCK_INFO) + info_button.show() + info_title = DIALOG_TITLE_EXTRA_INFO + info_button.connect( + "clicked", + lambda b: run_scrolled_dialog(extra_text, title=info_title)) + dialog.action_area.pack_start(info_button, expand=False, fill=False) + ok_button = dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) + if dialog_type == Gtk.MessageType.INFO: + stock_id = Gtk.STOCK_DIALOG_INFO + elif dialog_type == Gtk.MessageType.WARNING: + stock_id = Gtk.STOCK_DIALOG_WARNING + elif dialog_type == Gtk.MessageType.QUESTION: + stock_id = Gtk.STOCK_DIALOG_QUESTION + elif dialog_type == Gtk.MessageType.ERROR: + stock_id = Gtk.STOCK_DIALOG_ERROR + else: + stock_id = None + + if stock_id is not None: + dialog.image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.DIALOG) + dialog.image.show() + + dialog.label = Gtk.Label(label=text) + try: + Pango.parse_markup(text) + except GLib.GError: + try: + dialog.label.set_markup(metomi.rose.gtk.util.safe_str(text)) + except Exception: + dialog.label.set_text(text) + else: + dialog.label.set_markup(text) + dialog.label.show() + hbox = Gtk.HBox() + + if stock_id is not None: + image_vbox = Gtk.VBox() + image_vbox.pack_start(dialog.image, expand=False, fill=False, + padding=DIALOG_PADDING) + image_vbox.show() + hbox.pack_start(image_vbox, expand=False, fill=False, + padding=metomi.rose.config_editor.SPACING_PAGE) + + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_border_width(0) + scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + vbox = Gtk.VBox() + vbox.pack_start(dialog.label, expand=True, fill=True) + vbox.show() + scrolled_window.add_with_viewport(vbox) + scrolled_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) + scrolled_window.show() + hbox.pack_start(scrolled_window, expand=True, fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE) + hbox.show() + dialog.vbox.pack_end(hbox, expand=True, fill=True) + + if "\n" in text: + dialog.label.set_line_wrap(False) + dialog.set_resizable(True) + dialog.set_modal(modal) + if title is not None: + dialog.set_title(title) + + _configure_scroll(dialog, scrolled_window) + ok_button.grab_focus() + if modal or cancel: + dialog.show() + response = dialog.run() + dialog.destroy() + return (response == Gtk.ResponseType.OK) + else: + ok_button.connect("clicked", lambda b: dialog.destroy()) + dialog.show() + + +def run_exception_dialog(exception): + """Run a dialog displaying an exception.""" + text = type(exception).__name__ + ": " + str(exception) + return run_dialog(DIALOG_TYPE_ERROR, text, DIALOG_TITLE_ERROR) + + +def run_hyperlink_dialog(stock_id=None, text="", title=None, + search_func=lambda i: False): + """Run a dialog with inserted hyperlinks.""" + parent_window = get_dialog_parent() + dialog = Gtk.Window() + dialog.set_transient_for(parent_window) + dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) + dialog.set_title(title) + dialog.set_modal(False) + top_vbox = Gtk.VBox() + top_vbox.show() + main_hbox = Gtk.HBox(spacing=DIALOG_PADDING) + main_hbox.show() + # Insert the image + image_vbox = Gtk.VBox() + image_vbox.show() + image = Gtk.Image.new_from_stock(stock_id, + size=Gtk.IconSize.DIALOG) + image.show() + image_vbox.pack_start(image, expand=False, fill=False, + padding=DIALOG_PADDING) + main_hbox.pack_start(image_vbox, expand=False, fill=False, + padding=DIALOG_PADDING) + # Apply the text + message_vbox = Gtk.VBox() + message_vbox.show() + label = metomi.rose.gtk.util.get_hyperlink_label(text, search_func) + message_vbox.pack_start(label, expand=True, fill=True, + padding=DIALOG_PADDING) + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_border_width(DIALOG_PADDING) + scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + scrolled_window.add_with_viewport(message_vbox) + scrolled_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) + scrolled_window.show() + vbox = Gtk.VBox() + vbox.pack_start(scrolled_window, expand=True, fill=True) + vbox.show() + main_hbox.pack_start(vbox, expand=True, fill=True) + top_vbox.pack_start(main_hbox, expand=True, fill=True) + # Insert the button + button_box = Gtk.HBox(spacing=DIALOG_PADDING) + button_box.show() + button = metomi.rose.gtk.util.CustomButton(label=DIALOG_BUTTON_CLOSE, + size=Gtk.IconSize.LARGE_TOOLBAR, + stock_id=Gtk.STOCK_CLOSE) + button.connect("clicked", lambda b: dialog.destroy()) + button_box.pack_end(button, expand=False, fill=False, + padding=DIALOG_PADDING) + top_vbox.pack_end(button_box, expand=False, fill=False, + padding=DIALOG_PADDING) + dialog.add(top_vbox) + if "\n" in text: + label.set_line_wrap(False) + dialog.set_resizable(True) + _configure_scroll(dialog, scrolled_window) + dialog.show() + label.set_selectable(True) + button.grab_focus() + + +def run_scrolled_dialog(text, title=None): + """Run a dialog intended for the display of a large amount of text.""" + parent_window = get_dialog_parent() + window = Gtk.Window() + window.set_transient_for(parent_window) + window.set_type_hint(Gdk.WindowTypeHint.DIALOG) + window.set_border_width(DIALOG_SUB_PADDING) + window.set_default_size(*DIALOG_SIZE_SCROLLED_MIN) + if title is not None: + window.set_title(title) + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.show() + label = Gtk.Label() + try: + Pango.parse_markup(text) + except GLib.GError: + label.set_text(text) + else: + label.set_markup(text) + label.show() + filler_eb = Gtk.EventBox() + filler_eb.show() + label_box = Gtk.VBox() + label_box.pack_start(label, expand=False, fill=False) + label_box.pack_start(filler_eb, expand=True, fill=True) + label_box.show() + width, height = label.size_request() + max_width, max_height = DIALOG_SIZE_SCROLLED_MAX + width = min([max_width, width]) + 2 * DIALOG_PADDING + height = min([max_height, height]) + 2 * DIALOG_PADDING + scrolled.add_with_viewport(label_box) + scrolled.get_child().set_shadow_type(Gtk.ShadowType.NONE) + scrolled.set_size_request(width, height) + button = Gtk.Button(stock=Gtk.STOCK_OK) + button.connect("clicked", lambda b: window.destroy()) + button.show() + button.grab_focus() + button_box = Gtk.HBox() + button_box.pack_end(button, expand=False, fill=False) + button_box.show() + main_vbox = Gtk.VBox(spacing=DIALOG_SUB_PADDING) + main_vbox.pack_start(scrolled, expand=True, fill=True) + main_vbox.pack_end(button_box, expand=False, fill=False) + main_vbox.show() + window.add(main_vbox) + window.show() + label.set_selectable(True) + return False + + +def get_naming_dialog(label, checker, ok_tip=None, + err_tip=None): + """Return a dialog, container, and entry for entering a name.""" + button_list = (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) + parent_window = get_dialog_parent() + dialog = Gtk.Dialog(buttons=button_list) + dialog.set_transient_for(parent_window) + dialog.set_modal(True) + ok_button = dialog.action_area.get_children()[0] + main_vbox = Gtk.VBox() + name_hbox = Gtk.HBox() + name_label = Gtk.Label() + name_label.set_text(label) + name_label.show() + name_entry = Gtk.Entry() + name_entry.set_tooltip_text(ok_tip) + name_entry.connect("changed", _name_checker, checker, ok_button, + ok_tip, err_tip) + name_entry.connect( + "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT)) + name_entry.show() + name_hbox.pack_start(name_label, expand=False, fill=False, + padding=DIALOG_SUB_PADDING) + name_hbox.pack_start(name_entry, expand=False, fill=True, + padding=DIALOG_SUB_PADDING) + name_hbox.show() + main_vbox.pack_start(name_hbox, expand=False, fill=True, + padding=DIALOG_PADDING) + main_vbox.show() + hbox = Gtk.HBox() + hbox.pack_start(main_vbox, expand=False, fill=True, + padding=DIALOG_PADDING) + hbox.show() + dialog.vbox.pack_start(hbox, expand=False, fill=True, + padding=DIALOG_PADDING) + return dialog, main_vbox, name_entry + + +def _name_checker(entry, checker, ok_button, ok_tip, err_tip): + good_colour = ok_button.style.text[Gtk.StateType.NORMAL] + bad_colour = metomi.rose.gtk.util.color_parse( + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + name = entry.get_text() + if checker(name): + entry.modify_text(Gtk.StateType.NORMAL, good_colour) + entry.set_tooltip_text(ok_tip) + ok_button.set_sensitive(True) + else: + entry.modify_text(Gtk.StateType.NORMAL, bad_colour) + entry.set_tooltip_text(err_tip) + ok_button.set_sensitive(False) + return False + + +def run_choices_dialog(text, choices, title=None): + """Run a dialog for choosing between a set of options.""" + parent_window = get_dialog_parent() + dialog = Gtk.Dialog(title, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), + parent=parent_window) + dialog.set_border_width(DIALOG_SUB_PADDING) + label = Gtk.Label() + try: + Pango.parse_markup(text) + except GLib.GError: + label.set_text(text) + else: + label.set_markup(text) + dialog.vbox.set_spacing(DIALOG_SUB_PADDING) + dialog.vbox.pack_start(label, expand=False, fill=False) + if len(choices) < 5: + for i, choice in enumerate(choices): + group = None + if i > 0: + group = radio_button + if i == 1: + radio_button.set_active(True) + radio_button = Gtk.RadioButton(group, + label=choice, + use_underline=False) + dialog.vbox.pack_start(radio_button, expand=False, fill=False) + getter = (lambda: + [b.get_label() for b in radio_button.get_group() + if b.get_active()].pop()) + else: + combo_box = Gtk.ComboBoxText() + for choice in choices: + combo_box.append_text(choice) + combo_box.set_active(0) + dialog.vbox.pack_start(combo_box, expand=False, fill=False) + getter = lambda: choices[combo_box.get_active()] + dialog.show_all() + response = dialog.run() + if response == Gtk.ResponseType.ACCEPT: + choice = getter() + dialog.destroy() + return choice + dialog.destroy() + return None + + +def run_edit_dialog(text, finish_hook=None, title=None): + """Run a dialog for editing some text.""" + parent_window = get_dialog_parent() + dialog = Gtk.Dialog(title, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), + parent=parent_window) + + dialog.set_border_width(DIALOG_SUB_PADDING) + + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_border_width(DIALOG_SUB_PADDING) + scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + + text_buffer = Gtk.TextBuffer() + text_buffer.set_text(text) + text_view = Gtk.TextView() + text_view.set_editable(True) + text_view.set_wrap_mode(Gtk.WrapMode.NONE) + text_view.set_buffer(text_buffer) + text_view.show() + + scrolled_window.add_with_viewport(text_view) + scrolled_window.show() + + dialog.vbox.pack_start(scrolled_window, expand=True, fill=True, + padding=0) + get_text = lambda: text_buffer.get_text(text_buffer.get_start_iter(), + text_buffer.get_end_iter()) + + max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX + # defines the minimum acceptable size for the edit dialog + min_size = DIALOG_SIZE_PROCESS + + # hacky solution to get "true" size for dialog + dialog.show() + start_size = dialog.size_request() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + end_size = dialog.size_request() + my_size = (max([start_size[0], end_size[0], min_size[0]]) + 20, + max([start_size[1], end_size[1], min_size[1]]) + 20) + new_size = [-1, -1] + for i in [0, 1]: + new_size[i] = min([my_size[i], max_size[i]]) + dialog.set_size_request(*new_size) + + if finish_hook is None: + response = dialog.run() + if response == Gtk.ResponseType.ACCEPT: + text = get_text().strip() + dialog.destroy() + return text + dialog.destroy() + else: + finish_func = lambda: finish_hook(get_text().strip()) + dialog.connect("response", _handle_edit_dialog_response, finish_func) + dialog.show() + + +def _handle_edit_dialog_response(dialog, response, finish_hook): + if response == Gtk.ResponseType.ACCEPT: + finish_hook() + dialog.destroy() + + +def get_dialog_parent(): + """Find the currently active window, if any, and reparent dialog.""" + ok_windows = [] + max_size = -1 + for window in Gtk.window_list_toplevels(): + if window.get_title() is not None and window.get_toplevel() == window: + ok_windows.append(window) + size_proxy = window.get_size()[0] * window.get_size()[1] + if size_proxy > max_size: + max_size = size_proxy + for window in ok_windows: + if window.is_active(): + return window + for window in ok_windows: + if window.get_size()[0] * window.get_size()[1] == max_size: + return window + + +def set_exception_hook_dialog(keep_alive=False): + """Set a dialog to run once an uncaught exception occurs.""" + prev_hook = sys.excepthook + sys.excepthook = (lambda c, i, t: + _run_exception_dialog(c, i, t, prev_hook, + keep_alive)) + + +def _configure_scroll(dialog, scrolled_window): + """Set scroll window size and scroll policy.""" + # make sure the dialog size doesn't exceed the maximum - if so change it + max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX + my_size = dialog.size_request() + new_size = [-1, -1] + for i, scrollbar_cls in [(0, Gtk.VScrollbar), (1, Gtk.HScrollbar)]: + new_size[i] = min([my_size[i], max_size[i]]) + if new_size[i] < max_size[i]: + # Factor in existence of a scrollbar in the other dimension. + # For horizontal dimension, add width of vertical scroll bar + 2 + # For vertical dimension, add height of horizontal scroll bar + 2 + new_size[i] += scrollbar_cls().size_request()[i] + 2 + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + dialog.set_default_size(*new_size) + + +def _run_exception_dialog(exc_class, exc_inst, tback, hook, keep_alive): + # Handle an uncaught exception. + if exc_class == KeyboardInterrupt: + return False + hook(exc_class, exc_inst, tback) + program_name = metomi.rose.resource.ResourceLocator().get_util_name() + tback_text = metomi.rose.gtk.util.safe_str("".join(traceback.format_tb(tback))) + shutdown_text = DIALOG_TEXT_SHUTTING_DOWN + if keep_alive: + shutdown_text = DIALOG_TEXT_SHUTDOWN_ASAP + text = DIALOG_TEXT_UNCAUGHT_EXCEPTION.format(program_name, + shutdown_text, + exc_class.__name__, + exc_inst, + tback_text) + run_dialog(DIALOG_TYPE_ERROR, text, + title=DIALOG_TITLE_UNCAUGHT_EXCEPTION) + if not keep_alive: + try: + Gtk.main_quit() + except RuntimeError: + pass diff --git a/metomi/rose/gtk/run.py b/metomi/rose/gtk/run.py new file mode 100644 index 000000000..ee77986d3 --- /dev/null +++ b/metomi/rose/gtk/run.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""Miscellaneous gtk mini-applications.""" + +import multiprocessing +from subprocess import check_output + +from metomi.rose.gtk.dialog import DialogProcess, run_dialog, DIALOG_TYPE_WARNING +from metomi.rose.opt_parse import RoseOptionParser +# from metomi.rose.suite_engine_procs.cylc import CylcProcessor +# from metomi.rose.suite_run import SuiteRunner +from metomi.rose.reporter import Reporter, ReporterContextQueue + + +def run_suite(*args): + """Run "rose suite-run [args]" with a GTK dialog.""" + # Set up reporter + queue = multiprocessing.Manager().Queue() + verbosity = Reporter.VV + out_ctx = ReporterContextQueue(Reporter.KIND_OUT, verbosity, queue=queue) + err_ctx = ReporterContextQueue(Reporter.KIND_ERR, verbosity, queue=queue) + event_handler = Reporter(contexts={"stdout": out_ctx, "stderr": err_ctx}, + raise_on_exc=True) + + # Parse arguments + suite_runner = SuiteRunner(event_handler=event_handler) + + # Don't use rose-suite run if Cylc Version is 8.*: + if suite_runner.suite_engine_proc.get_version()[0] == '8': + run_dialog( + DIALOG_TYPE_WARNING, + '`rose suite-run` does not work with Cylc 8 workflows: ' + 'Use `cylc install`.', + 'Cylc Version == 8' + ) + return None + + prog = "rose suite-run" + description = prog + if args: + description += " " + suite_runner.popen.list_to_shell_str(args) + opt_parse = RoseOptionParser(prog=prog) + opt_parse.add_my_options(*suite_runner.OPTIONS) + opts, args = opt_parse.parse_args(list(args)) + + # Invoke the command with a GTK dialog + dialog_process = DialogProcess([suite_runner, opts, args], + description=description, + modal=False, + event_queue=queue) + return dialog_process.run() diff --git a/metomi/rose/gtk/splash.py b/metomi/rose/gtk/splash.py new file mode 100755 index 000000000..377ec8921 --- /dev/null +++ b/metomi/rose/gtk/splash.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- +"""Invoke a splash screen from the command line.""" + +import json +import os +from subprocess import Popen, PIPE +import sys +import threading +import time + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk +from gi.repository import GObject +from gi.repository import Pango + +import metomi.rose.gtk.util +import metomi.rose.popen + +GObject.threads_init() + + +class SplashScreen(Gtk.Window): + + """Run a splash screen that receives update information.""" + + BACKGROUND_COLOUR = "white" # Same as logo background. + PADDING = 10 + SUB_PADDING = 5 + FONT_DESC = "8" + PULSE_FRACTION = 0.05 + TIME_WAIT_FINISH = 500 # Milliseconds. + TIME_IDLE_BEFORE_PULSE = 3000 # Milliseconds. + TIME_INTERVAL_PULSE = 50 # Milliseconds. + + def __init__(self, logo_path, title, total_number_of_events): + super(SplashScreen, self).__init__() + self.set_title(title) + self.set_decorated(False) + self.stopped = False + self.set_icon(metomi.rose.gtk.util.get_icon()) + self.modify_bg(Gtk.StateType.NORMAL, + metomi.rose.gtk.util.color_parse(self.BACKGROUND_COLOUR)) + self.set_gravity(Gdk.GRAVITY_CENTER) + self.set_position(Gtk.WindowPosition.CENTER) + main_vbox = Gtk.VBox() + main_vbox.show() + image = Gtk.image_new_from_file(logo_path) + image.show() + image_hbox = Gtk.HBox() + image_hbox.show() + image_hbox.pack_start(image, expand=False, fill=True) + main_vbox.pack_start(image_hbox, expand=False, fill=True) + self._is_progress_bar_pulsing = False + self._progress_fraction = 0.0 + self.progress_bar = Gtk.ProgressBar() + self.progress_bar.set_pulse_step(self.PULSE_FRACTION) + self.progress_bar.show() + self.progress_bar.modify_font(Pango.FontDescription(self.FONT_DESC)) + self.progress_bar.set_ellipsize(Pango.EllipsizeMode.END) + self._progress_message = None + self.event_count = 0.0 + self.total_number_of_events = float(total_number_of_events) + progress_hbox = Gtk.HBox(spacing=self.SUB_PADDING) + progress_hbox.show() + progress_hbox.pack_start(self.progress_bar, expand=True, fill=True, + padding=self.SUB_PADDING) + main_vbox.pack_start(progress_hbox, expand=False, fill=False, + padding=self.PADDING) + self.add(main_vbox) + if self.total_number_of_events > 0: + self.show() + while Gtk.events_pending(): + Gtk.main_iteration() + + def update(self, event, no_progress=False, new_total_events=None): + """Show text corresponding to an event.""" + text = str(event) + if new_total_events is not None: + self.total_number_of_events = new_total_events + self.event_count = 0.0 + + if not no_progress: + self.event_count += 1.0 + + if self.total_number_of_events == 0: + fraction = 1.0 + else: + fraction = min( + [1.0, self.event_count / self.total_number_of_events]) + self._stop_pulse() + + if not no_progress: + GObject.idle_add(self.progress_bar.set_fraction, fraction) + self._progress_fraction = fraction + + self.progress_bar.set_text(text) + self._progress_message = text + GObject.timeout_add(self.TIME_IDLE_BEFORE_PULSE, + self._start_pulse, fraction, text) + + if fraction == 1.0 and not no_progress: + GObject.timeout_add(self.TIME_WAIT_FINISH, self.finish) + + while Gtk.events_pending(): + Gtk.main_iteration() + + def _start_pulse(self, idle_fraction, idle_message): + """Start the progress bar pulsing (moving side-to-side).""" + if (self._progress_message != idle_message or + self._progress_fraction != idle_fraction): + return False + self._is_progress_bar_pulsing = True + GObject.timeout_add(self.TIME_INTERVAL_PULSE, + self._pulse) + return False + + def _stop_pulse(self): + self._is_progress_bar_pulsing = False + + def _pulse(self): + if self._is_progress_bar_pulsing: + self.progress_bar.pulse() + while Gtk.events_pending(): + Gtk.main_iteration() + return self._is_progress_bar_pulsing + + def finish(self): + """Delete the splash screen.""" + self.stopped = True + GObject.idle_add(self.destroy) + return False + + +class NullSplashScreenProcess(object): + + """Implement a null interface similar to SplashScreenProcess.""" + + def __init__(self, *args): + pass + + def update(self, *args, **kwargs): + pass + + def start(self): + pass + + def stop(self): + pass + + +class SplashScreenProcess(object): + + """Run a separate process that launches a splash screen. + + Communicate via the update method. + + """ + + def __init__(self, *args): + args = [str(a) for a in args] + self.args = args + self._buffer = [] + self._last_buffer_output_time = time.time() + self.start() + + def update(self, *args, **kwargs): + """Communicate via stdin to SplashScreenManager. + + args and kwargs are the update method args, kwargs. + + """ + if self.process is None: + self.start() + if kwargs.get("no_progress"): + return self._update_buffered(*args, **kwargs) + self._flush_buffer() + json_text = json.dumps({"args": args, "kwargs": kwargs}) + self._communicate(json_text) + + def _communicate(self, json_text): + while True: + try: + self.process.stdin.write(json_text + "\n") + except IOError: + self.start() + self.process.stdin.write(json_text + "\n") + else: + break + + def _flush_buffer(self): + if self._buffer: + self._communicate(self._buffer[-1]) + del self._buffer[:] + + def _update_buffered(self, *args, **kwargs): + tinit = time.time() + json_text = json.dumps({"args": args, "kwargs": kwargs}) + if tinit - self._last_buffer_output_time > 0.02: + self._communicate(json_text) + del self._buffer[:] + self._last_buffer_output_time = tinit + else: + self._buffer.append(json_text) + + __call__ = update + + def start(self): + file_name = __file__.rsplit(".", 1)[0] + ".py" + self.process = Popen([file_name] + list(self.args), stdin=PIPE) + + def stop(self): + if self.process is not None and not self.process.stdin.closed: + try: + self.process.communicate(input=json.dumps("stop") + "\n") + except IOError: + pass + self.process = None + + +class SplashScreenUpdaterThread(threading.Thread): + + """Update a splash screen using info from the stdin file object.""" + + def __init__(self, window, stop_event, stdin): + super(SplashScreenUpdaterThread, self).__init__() + self.window = window + self.stop_event = stop_event + self.stdin = stdin + + def run(self): + """Loop over time and wait for stdin lines.""" + GObject.timeout_add(1000, self._check_splash_screen_alive) + while not self.stop_event.is_set(): + time.sleep(0.005) + if self.stop_event.is_set(): + return False + try: + stdin_line = self.stdin.readline() + except IOError: + continue + try: + update_input = json.loads(stdin_line.strip()) + except ValueError: + continue + if update_input == "stop": + self._stop() + continue + GObject.idle_add(self._update_splash_screen, update_input) + + def _stop(self): + self.stop_event.set() + try: + Gtk.main_quit() + except RuntimeError: + # This can result from gtk having already quit. + pass + + def _check_splash_screen_alive(self): + """Check whether the splash screen is finished.""" + if self.window.stopped or self.stop_event.is_set(): + self._stop() + return False + return True + + def _update_splash_screen(self, update_input): + """Update the splash screen with info extracted from stdin.""" + self.window.update(*update_input["args"], **update_input["kwargs"]) + return False + + +def main(): + """Start splash screen.""" + sys.path.append(os.getenv('ROSE_HOME')) + splash_screen = SplashScreen(*sys.argv[1:]) + stop_event = threading.Event() + update_thread = SplashScreenUpdaterThread( + splash_screen, stop_event, sys.stdin) + update_thread.start() + try: + Gtk.main() + except KeyboardInterrupt: + pass + finally: + stop_event.set() + update_thread.join() + + +if __name__ == "__main__": + main() diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py new file mode 100644 index 000000000..7ab2a4f8d --- /dev/null +++ b/metomi/rose/gtk/util.py @@ -0,0 +1,705 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. +# +# This file is part of Rose, a framework for meteorological suites. +# +# Rose is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rose is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rose. If not, see . +# ----------------------------------------------------------------------------- + +import multiprocessing +import queue +import re +import sys +import threading +import webbrowser + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GdkPixbuf +from gi.repository import GObject +from gi.repository import GLib +from gi.repository import Pango + +import metomi.rose.reporter +import metomi.rose.resource + + +REC_HYPERLINK_ID_OR_URL = re.compile( + r"""(?P\b) + (?P[\w:-]+=\w+|https?://[^\s<]+) + (?P\b)""", re.X) +MARKUP_URL_HTML = (r"""\g""" + + r"""\g""" + + r"""\g""") +MARKUP_URL_UNDERLINE = (r"""\g""" + + r"""\g""" + + r"""\g""") + + +class ColourParseError(ValueError): + + """An exception raised when gtk colour parsing fails.""" + + def __str__(self): + return "unable to parse colour specification: %s" % self.args[0] + + +class CustomButton(Gtk.Button): + + """Returns a custom Gtk.Button.""" + + def __init__(self, label=None, stock_id=None, + size=Gtk.IconSize.SMALL_TOOLBAR, tip_text=None, + as_tool=False, icon_at_start=False, has_menu=False): + self.hbox = Gtk.HBox() + self.size = size + self.as_tool = as_tool + self.icon_at_start = icon_at_start + if label is not None: + self.label = Gtk.Label() + self.label.set_text(label) + self.label.show() + + if self.icon_at_start: + self.hbox.pack_end(self.label, expand=False, fill=False, + padding=5) + else: + self.hbox.pack_start(self.label, expand=False, fill=False, + padding=5) + if stock_id is not None: + self.stock_id = stock_id + self.icon = Gtk.Image() + self.icon.set_from_stock(stock_id, size) + self.icon.show() + if self.icon_at_start: + self.hbox.pack_start(self.icon, expand=False, fill=False) + else: + self.hbox.pack_end(self.icon, expand=False, fill=False) + if has_menu: + arrow = Gtk.Arrow(Gtk.ArrowType.DOWN, Gtk.ShadowType.NONE) + arrow.show() + self.hbox.pack_end(arrow, expand=False, fill=False) + self.hbox.reorder_child(arrow, 0) + self.hbox.show() + super(CustomButton, self).__init__() + if self.as_tool: + self.set_relief(Gtk.ReliefStyle.NONE) + self.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + self.add(self.hbox) + self.show() + if tip_text is not None: + self.set_tooltip_text(tip_text) + + def set_stock_id(self, stock_id): + """Set an icon based on the stock id.""" + if hasattr(self, "icon"): + self.hbox.remove(self.icon) + self.icon.set_from_stock(stock_id, self.size) + self.stock_id = stock_id + if self.icon_at_start: + self.hbox.pack_start(self.icon, expand=False, fill=False) + else: + self.hbox.pack_end(self.icon, expand=False, fill=False) + return False + + def set_tip_text(self, new_text): + """Set new tooltip text.""" + self.set_tooltip_text(new_text) + + def position_menu(self, menu, widget): + """Place a drop-down menu carefully below the button.""" + xpos, ypos = widget.get_window().get_origin() + allocated_rectangle = widget.get_allocation() + xpos += allocated_rectangle.x + ypos += allocated_rectangle.y + allocated_rectangle.height + return xpos, ypos, False + + +class CustomExpandButton(Gtk.Button): + + """Custom button for expanding/hiding something""" + + def __init__(self, expander_function=None, + label=None, + size=Gtk.IconSize.SMALL_TOOLBAR, + tip_text=None, + as_tool=False, + icon_at_start=False, + minimised=True): + + self.expander_function = expander_function + self.minimised = minimised + + self.expand_id = Gtk.STOCK_ADD + self.minimise_id = Gtk.STOCK_REMOVE + + if minimised: + self.stock_id = self.expand_id + else: + self.stock_id = self.minimise_id + + self.hbox = Gtk.HBox() + self.size = size + self.as_tool = as_tool + self.icon_at_start = icon_at_start + + if label is not None: + self.label = Gtk.Label() + self.label.set_text(label) + self.label.show() + + if self.icon_at_start: + self.hbox.pack_end(self.label, expand=False, fill=False, + padding=5) + else: + self.hbox.pack_start(self.label, expand=False, fill=False, + padding=5) + self.icon = Gtk.Image() + self.icon.set_from_stock(self.stock_id, size) + self.icon.show() + if self.icon_at_start: + self.hbox.pack_start(self.icon, expand=False, fill=False) + else: + self.hbox.pack_end(self.icon, expand=False, fill=False) + self.hbox.show() + super(CustomExpandButton, self).__init__() + + if self.as_tool: + self.set_relief(Gtk.ReliefStyle.NONE) + self.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + self.add(self.hbox) + self.show() + if tip_text is not None: + self.set_tooltip_text(tip_text) + self.connect("clicked", self.toggle) + + def set_stock_id(self, stock_id): + """Set the icon stock_id""" + if hasattr(self, "icon"): + self.hbox.remove(self.icon) + self.icon.set_from_stock(stock_id, self.size) + self.stock_id = stock_id + if self.icon_at_start: + self.hbox.pack_start(self.icon, expand=False, fill=False) + else: + self.hbox.pack_end(self.icon, expand=False, fill=False) + return False + + def set_tip_text(self, new_text): + """Set the tip text""" + self.set_tooltip_text(new_text) + + def toggle(self, minimise=None): + """Toggle between show/hide states""" + if minimise is not None: + if minimise == self.minimised: + return + self.minimised = not self.minimised + if self.minimised: + self.stock_id = self.expand_id + else: + self.stock_id = self.minimise_id + if self.expander_function is not None: + self.expander_function(set_visibility=not self.minimised) + self.set_stock_id(self.stock_id) + + +class CustomMenuButton(Gtk.MenuToolButton): + + """Custom wrapper for the gtk Menu Tool Button.""" + + def __init__(self, label=None, stock_id=None, + size=Gtk.IconSize.SMALL_TOOLBAR, tip_text=None, + menu_items=[], menu_funcs=[]): + if stock_id is not None: + self.stock_id = stock_id + self.icon = Gtk.Image() + self.icon.set_from_stock(stock_id, size) + self.icon.show() + GObject.GObject.__init__(self, self.icon, label) + self.set_tooltip_text(tip_text) + self.show() + button_menu = Gtk.Menu() + for item_tuple, func in zip(menu_items, menu_funcs): + name = item_tuple[0] + if len(item_tuple) == 1: + new_item = Gtk.MenuItem(name) + else: + new_item = Gtk.ImageMenuItem(stock_id=item_tuple[1]) + new_item.set_label(name) + new_item._func = func + new_item.connect("activate", lambda m: m._func()) + new_item.show() + button_menu.append(new_item) + button_menu.show() + self.set_menu(button_menu) + + +class ToolBar(Gtk.Toolbar): + + """An easier-to-use Gtk.Toolbar.""" + + def __init__(self, widgets=[], sep_on_name=[]): + super(ToolBar, self).__init__() + self.item_dict = {} + self.show() + widgets.reverse() + for name, stock in widgets: + if name in sep_on_name: + separator = Gtk.SeparatorToolItem() + separator.show() + self.insert(separator, 0) + if isinstance(stock, str) and stock.startswith("Gtk."): + stock = getattr(Gtk, stock.replace("Gtk.", "", 1)) + if callable(stock): + widget = stock() + widget.show() + widget.set_tooltip_text(name) + else: + widget = CustomButton(stock_id=stock, tip_text=name, + as_tool=True) + icon_tool_item = Gtk.ToolItem() + icon_tool_item.add(widget) + icon_tool_item.show() + self.item_dict[name] = {"tip": name, "widget": widget, + "func": None} + self.insert(icon_tool_item, 0) + + def set_widget_function(self, name, function, args=[]): + self.item_dict[name]["widget"].args = args + if len(args) > 0: + self.item_dict[name]["widget"].connect("clicked", + lambda b: function(*b.args)) + else: + self.item_dict[name]["widget"].connect("clicked", + lambda b: function()) + + def set_widget_sensitive(self, name, is_sensitive): + self.item_dict[name]["widget"].set_sensitive(is_sensitive) + + +class AsyncStatusbar(Gtk.Statusbar): + + """Wrapper class to add polling a file to statusbar API.""" + + def __init__(self, *args): + super(AsyncStatusbar, self).__init__(*args) + self.show() + self.queue = multiprocessing.Queue() + self.ctx_id = self.get_context_id("_all") + self.should_stop = False + self.connect("destroy", self._handle_destroy) + GObject.timeout_add(1000, self._poll) + + def _handle_destroy(self, *args): + self.should_stop = True + + def _poll(self): + self.update() + return not self.should_stop + + def update(self): + try: + message = self.queue.get(block=False) + except queue.Empty: + pass + else: + self.push(self.ctx_id, message) + + def put(self, message, instant=False): + if instant: + self.push(self.ctx_id, message) + else: + self.queue.put_nowait(message) + self.update() + + +class AsyncLabel(Gtk.Label): + + """Wrapper class to add polling a file to label API.""" + + def __init__(self, *args): + super(AsyncLabel, self).__init__(*args) + self.show() + self.queue = multiprocessing.Queue() + self.should_stop = False + self.connect("destroy", self._handle_destroy) + GObject.timeout_add(1000, self._poll) + + def _handle_destroy(self, *args): + self.should_stop = True + + def _poll(self): + self.update() + return not self.should_stop + + def update(self): + try: + message = self.queue.get(block=False) + except queue.Empty: + pass + else: + self.set_text(message) + + def put(self, message, instant=False): + if instant: + self.set_text(message) + else: + self.queue.put_nowait(message) + self.update() + + +class ThreadedProgressBar(Gtk.ProgressBar): + + """Wrapper class to allow threaded progress bar pulsing.""" + + def __init__(self, *args, **kwargs): + super(ThreadedProgressBar, self).__init__(*args, **kwargs) + self.set_fraction(0.0) + self.set_pulse_step(0.1) + + def start_pulsing(self): + self.stop = False + self.show() + self.thread = threading.Thread() + self.thread.run = lambda: GObject.timeout_add(50, self._run) + self.thread.start() + + def _run(self): + Gdk.threads_enter() + self.pulse() + if self.stop: + self.set_fraction(1.0) + while Gtk.events_pending(): + Gtk.main_iteration() + Gdk.threads_leave() + return not self.stop + + def stop_pulsing(self): + self.stop = True + self.thread.join() + GObject.idle_add(self.hide) + + +class Notebook(Gtk.Notebook): + + """Wrapper class to improve the Gtk.Notebook API.""" + + def __init__(self, *args): + super(Notebook, self).__init__(*args) + self.set_scrollable(True) + self.show() + + def get_pages(self): + """Return all 'page' container widgets.""" + pages = [] + for i in range(self.get_n_pages()): + pages.append(self.get_nth_page(i)) + return pages + + def get_page_labels(self): + """Return all first pieces of text found in page labelwidgets.""" + labels = [] + for i in range(self.get_n_pages()): + nth_page = self.get_nth_page(i) + widgets = [self.get_tab_label(nth_page)] + while not hasattr(widgets[0], "get_text"): + if hasattr(widgets[0], "get_children"): + widgets.extend(widgets[0].get_children()) + elif hasattr(widgets[0], "get_child"): + widgets.append(widgets[0].get_child()) + widgets.pop(0) + labels.append(widgets[0].get_text()) + return labels + + def get_page_ids(self): + """Return the namespace id attributes for all notebook pages.""" + ids = [] + for i in range(self.get_n_pages()): + nth_page = self.get_nth_page(i) + if hasattr(nth_page, "namespace"): + ids.append(nth_page.namespace) + return ids + + def delete_by_label(self, label): + """Remove the (unique) page with this label as title.""" + self.remove_page(self.get_page_labels().index(label)) + + def delete_by_id(self, page_id): + """Use this only with pages with the attribute 'namespace'.""" + self.remove_page(self.get_page_ids().index(page_id)) + + def set_tab_label_packing(self, page): + super(Notebook, self).set_tab_label(page) # check + + +class TooltipTreeView(Gtk.TreeView): + + """Wrapper class for Gtk.TreeView with a better tooltip API. + + It takes two keyword arguments, model as in Gtk.TreeView and + get_tooltip_func which is analogous to the 'query-tooltip' + connector in Gtk.TreeView. + + This should be overridden either at or after initialisation. + It takes four arguments - the Gtk.TreeView, a Gtk.TreeIter and + a column index for the Gtk.TreeView, and a Gtk.ToolTip. + + Return True to display the ToolTip, or False to hide it. + + """ + + def __init__(self, model=None, get_tooltip_func=None, + multiple_selection=False): + super(TooltipTreeView, self).__init__(model) + self.get_tooltip = get_tooltip_func + self.set_has_tooltip(True) + self._last_tooltip_path = None + self._last_tooltip_column = None + self.connect('query-tooltip', self._handle_tooltip) + if multiple_selection: + self.set_rubber_banding(True) + self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + + def _handle_tooltip(self, view, xpos, ypos, kbd_ctx, tip): + """Handle creating a tooltip for the treeview.""" + xpos, ypos = view.convert_widget_to_bin_window_coords(xpos, ypos) + pathinfo = view.get_path_at_pos(xpos, ypos) + if pathinfo is None: + return False + path, column = pathinfo[:2] + if path is None: + return False + if (path != self._last_tooltip_path or + column != self._last_tooltip_column): + self._last_tooltip_path = path + self._last_tooltip_column = column + return False + col_index = view.get_columns().index(column) + row_iter = view.get_model().get_iter(path) + if self.get_tooltip is None: + return False + return self.get_tooltip(view, row_iter, col_index, tip) + + +class TreeModelSortUtil(object): + + """This class contains useful sorting methods for TreeModelSort. + + Arguments: + sort_model_getter_func - a function accepting no arguments that + returns the TreeModelSort. This is necessary if a combination + of TreeModelFilter and TreeModelSort is used. + + Keyword Arguments: + multi_sort_num - the maximum number of columns to sort by. For + example, setting this to 2 means that a single secondary sort + may be applied based on the previous sort column. + + You must connect to both handle_sort_column_change and sort_column + for multi-column sorting. Example code: + + sort_model = Gtk.TreeModelSort(filter_model) + sort_util = TreeModelSortUtil( + lambda: sort_model, + multi_sort_num=2) + for i in range(len(columns)): + sort_model.set_sort_func(i, sort_util.sort_column, i) + sort_model.connect("sort-column-changed", + sort_util.handle_sort_column_change) + + """ + + def __init__(self, sort_model_getter_func, multi_sort_num=1): + self._get_sort_model = sort_model_getter_func + self.multi_sort_num = multi_sort_num + self._sort_columns_stored = [] + + def clear_sort_columns(self): + """Clear any multi-sort information.""" + self._sort_columns_stored = [] + + def cmp_(self, value1, value2): + """Perform a useful form of 'cmp'""" + if (isinstance(value1, str) and isinstance(value2, str)): + if value1.isdigit() and value2.isdigit(): + return cmp(float(value1), float(value2)) + return metomi.rose.config.sort_settings(value1, value2) + return cmp(value1, value2) + + def handle_sort_column_change(self, model): + """Store previous sorting information for multi-column sorts.""" + id_, order = model.get_sort_column_id() + if id_ is None and order is None: + return False + if (self._sort_columns_stored and + self._sort_columns_stored[0][0] == id_): + self._sort_columns_stored.pop(0) + self._sort_columns_stored.insert(0, (id_, order)) + if len(self._sort_columns_stored) > 2: + self._sort_columns_stored.pop() + + def sort_column(self, model, iter1, iter2, col_index): + """Multi-column sort.""" + val1 = model.get_value(iter1, col_index) + val2 = model.get_value(iter2, col_index) + rval = self.cmp_(val1, val2) + # If rval is 1 or -1, no need for a multi-column sort. + if rval == 0: + if isinstance(model, Gtk.TreeModelSort): + this_order = model.get_sort_column_id()[1] + else: + this_order = self._get_sort_model().get_sort_column_id()[1] + cmp_factor = 1 + if this_order == Gtk.SortType.DESCENDING: + # We need to de-invert the sort order for multi sorting. + cmp_factor = -1 + i = 0 + while rval == 0 and i < len(self._sort_columns_stored): + next_id, next_order = self._sort_columns_stored[i] + if next_id == col_index: + i += 1 + continue + next_cmp_factor = cmp_factor * 1 + if next_order == Gtk.SortType.DESCENDING: + # Set the correct order for multi sorting. + next_cmp_factor = cmp_factor * -1 + val1 = model.get_value(iter1, next_id) + val2 = model.get_value(iter2, next_id) + rval = next_cmp_factor * self.cmp_(val1, val2) + i += 1 + return rval + + +def color_parse(color_specification): + """Wrap Gdk.color_parse and report errors with the specification.""" + try: + return Gdk.color_parse(color_specification) + except ValueError: + metomi.rose.reporter.Reporter().report(ColourParseError(color_specification)) + # Return a noticeable colour. + return Gdk.color_parse("#0000FF") # Blue + + +def get_hyperlink_label(text, search_func=lambda i: False): + """Return a label with clickable hyperlinks.""" + label = Gtk.Label() + label.show() + try: + Pango.parse_markup(text) + except GLib.GError: + label.set_text(text) + else: + try: + label.connect("activate-link", + lambda l, u: handle_link(u, search_func)) + except TypeError: # No such signal before PyGTK 2.18 + label.connect("button-release-event", + lambda l, e: extract_link(l, search_func)) + text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_UNDERLINE, text) + label.set_markup(text) + else: + text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_HTML, text) + label.set_markup(text) + return label + + +def get_icon(system="rose"): + """Return a GdkPixbuf.Pixbuf for the system icon.""" + locator = metomi.rose.resource.ResourceLocator(paths=sys.path) + icon_path = locator.locate("etc/images/{0}-icon-trim.svg".format(system)) + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) + except Exception: + icon_path = locator.locate( + "etc/images/{0}-icon-trim.png".format(system)) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) + return pixbuf + + +def handle_link(url, search_function, handle_web=False): + if url.startswith("http"): + if handle_web: + webbrowser.open(url) + else: + search_function(url) + return False + + +def extract_link(label, search_function): + text = label.get_text() + bounds = label.get_selection_bounds() + if not bounds: + return None + lower_bound, upper_bound = bounds + while lower_bound > 0: + if text[lower_bound - 1].isspace(): + break + lower_bound -= 1 + while upper_bound < len(text): + if text[upper_bound].isspace(): + break + upper_bound += 1 + link = text[lower_bound: upper_bound] + if any(c.isspace() for c in link): + return None + handle_link(link, search_function, handle_web=True) + + +def rc_setup(rc_resource): + """Run Gtk.rc_parse on the resource, to setup the gtk settings.""" + Gtk.rc_parse(rc_resource) + + +def setup_scheduler_icon(ipath=None): + """Setup a 'stock' icon for the scheduler""" + new_icon_factory = Gtk.IconFactory() + locator = metomi.rose.resource.ResourceLocator(paths=sys.path) + iname = "rose-gtk-scheduler" + if ipath is None: + new_icon_factory.add( + iname, Gtk.icon_factory_lookup_default(Gtk.STOCK_MISSING_IMAGE)) + else: + path = locator.locate(ipath) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) + new_icon_factory.add(iname, Gtk.IconSet(pixbuf)) + new_icon_factory.add_default() + + +def setup_stock_icons(): + """Setup any additional 'stock' icons.""" + new_icon_factory = Gtk.IconFactory() + locator = metomi.rose.resource.ResourceLocator(paths=sys.path) + for png_icon_name in ["gnome_add", + "gnome_add_errors", + "gnome_add_warnings", + "gnome_package_system", + "gnome_package_system_errors", + "gnome_package_system_warnings"]: + ifile = png_icon_name + ".png" + istring = png_icon_name.replace("_", "-") + path = locator.locate("etc/images/rose-config-edit/" + ifile) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) + new_icon_factory.add("rose-gtk-" + istring, + Gtk.IconSet(pixbuf)) + exp_icon_pixbuf = get_icon() + new_icon_factory.add("rose-exp-logo", Gtk.IconSet(exp_icon_pixbuf)) + new_icon_factory.add_default() + + +def safe_str(value): + """Formats a value safely for use in pango markup.""" + string = str(value).replace("&", "&") + return string.replace(">", ">").replace("<", "<") diff --git a/metomi/rose/reporter.py b/metomi/rose/reporter.py index b8d1efcc4..a2a4df8c9 100644 --- a/metomi/rose/reporter.py +++ b/metomi/rose/reporter.py @@ -16,6 +16,8 @@ # ----------------------------------------------------------------------------- """Reporter for diagnostic messages.""" +import queue +import multiprocessing import doctest import sys @@ -266,6 +268,58 @@ def _tty_colour_err(self, str_): return str_ + +class ReporterContextQueue(ReporterContext): + + """A context for the reporter object. + + It has the following attributes: + kind: + The message kind to report to this context. + (Reporter.KIND_ERR, Reporter.KIND_ERR or None.) + verbosity: + The verbosity of this context. + queue: + The multiprocessing.Queue. + prefix: + The default message prefix (str or callable). + + """ + + def __init__(self, + kind=None, + verbosity=Reporter.DEFAULT, + queue=None, + prefix=None): + ReporterContext.__init__(self, kind, verbosity, None, prefix) + if queue is None: + queue = multiprocessing.Manager().Queue() + self.queue = queue + self.closed = False + self._messages_pending = [] + + def close(self): + self._send_pending_messages() + self.closed = True + + def is_closed(self): + return self.closed + + def write(self, message): + self._messages_pending.append(message) + self._send_pending_messages() + + def _send_pending_messages(self): + while self._messages_pending: + message = self._messages_pending[0] + try: + self.queue.put(message, block=False) + except queue.Full: + break + else: + del self._messages_pending[0] + + class Event: """A base class for events suitable for feeding into a Reporter.""" From 8aec77ccc1d198cd2577d7258b9fee83f5f09ddb Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 5 Aug 2024 14:23:53 +0100 Subject: [PATCH 05/42] Adds image resources for the gui. Move GUI image resources into package (#43) This means that they get included in the wheel file when the package is built, and they actually get found at runtime. Fixes https://github.com/astroDimitrios/rose/issues/37 --- .gitignore | 1 + ACKNOWLEDGEMENT.md | 18 +++++++++--------- metomi/rose/config_editor/main.py | 6 +++--- metomi/rose/config_editor/nav_panel.py | 8 ++++---- metomi/rose/config_editor/status.py | 4 ++-- .../images/rose-config-edit/change_icon.png | Bin 0 -> 682 bytes .../images/rose-config-edit/error_icon.png | Bin 0 -> 757 bytes .../etc/images/rose-config-edit/gnome_add.png | Bin 0 -> 409 bytes .../rose-config-edit/gnome_add_errors.png | Bin 0 -> 379 bytes .../rose-config-edit/gnome_add_warnings.png | Bin 0 -> 417 bytes .../rose-config-edit/gnome_package_system.png | Bin 0 -> 865 bytes .../gnome_package_system_errors.png | Bin 0 -> 697 bytes .../gnome_package_system_warnings.png | Bin 0 -> 792 bytes .../etc/images/rose-config-edit/null_icon.png | Bin 0 -> 425 bytes .../rose/etc}/images/rose-icon-trim.png | Bin .../rose/etc}/images/rose-icon-trim.svg | 0 {etc => metomi/rose/etc}/images/rose-icon.png | Bin {etc => metomi/rose/etc}/images/rose-icon.svg | 0 {etc => metomi/rose/etc}/images/rose-logo.png | Bin metomi/rose/etc/images/rose-splash-logo.png | Bin 0 -> 19363 bytes .../rose/etc}/images/rosie-icon-trim.png | Bin .../rose/etc}/images/rosie-icon-trim.svg | 0 .../rose/etc}/images/rosie-icon.png | Bin .../rose/etc}/images/rosie-icon.svg | 0 metomi/rose/etc/rose-config-edit/.gtkrc-2.0 | 1 + metomi/rose/gtk/util.py | 17 +++++++---------- 26 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 metomi/rose/etc/images/rose-config-edit/change_icon.png create mode 100644 metomi/rose/etc/images/rose-config-edit/error_icon.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_add.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_add_errors.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_add_warnings.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_package_system.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_package_system_errors.png create mode 100644 metomi/rose/etc/images/rose-config-edit/gnome_package_system_warnings.png create mode 100644 metomi/rose/etc/images/rose-config-edit/null_icon.png rename {etc => metomi/rose/etc}/images/rose-icon-trim.png (100%) rename {etc => metomi/rose/etc}/images/rose-icon-trim.svg (100%) rename {etc => metomi/rose/etc}/images/rose-icon.png (100%) rename {etc => metomi/rose/etc}/images/rose-icon.svg (100%) rename {etc => metomi/rose/etc}/images/rose-logo.png (100%) create mode 100644 metomi/rose/etc/images/rose-splash-logo.png rename {etc => metomi/rose/etc}/images/rosie-icon-trim.png (100%) rename {etc => metomi/rose/etc}/images/rosie-icon-trim.svg (100%) rename {etc => metomi/rose/etc}/images/rosie-icon.png (100%) rename {etc => metomi/rose/etc}/images/rosie-icon.svg (100%) create mode 100644 metomi/rose/etc/rose-config-edit/.gtkrc-2.0 diff --git a/.gitignore b/.gitignore index 1794ebeac..37bfb97e0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ etc/opt doc venv metomi_rose.egg-info +build dist node_modules diff --git a/ACKNOWLEDGEMENT.md b/ACKNOWLEDGEMENT.md index d20dff5a3..86e4319b2 100644 --- a/ACKNOWLEDGEMENT.md +++ b/ACKNOWLEDGEMENT.md @@ -3,15 +3,15 @@ Licences for non-Rose works included in this distribution can be found in the licences/ directory. -etc/images/rose-icon.png, -etc/images/rose-icon.svg, -etc/images/rose-icon-trim.png, -etc/images/rose-icon-trim.svg, -etc/images/rose-logo.png, -etc/images/rosie-icon.png, -etc/images/rosie-icon.svg, -etc/images/rosie-icon-trim.png, -etc/images/rosie-icon-trim.svg, +metomi/rose/etc/images/rose-icon.png, +metomi/rose/etc/images/rose-icon.svg, +metomi/rose/etc/images/rose-icon-trim.png, +metomi/rose/etc/images/rose-icon-trim.svg, +metomi/rose/etc/images/rose-logo.png, +metomi/rose/etc/images/rosie-icon.png, +metomi/rose/etc/images/rosie-icon.svg, +metomi/rose/etc/images/rosie-icon-trim.png, +metomi/rose/etc/images/rosie-icon-trim.svg, metomi/rose/etc/rose-all/etc/images/icon.png, metomi/rose/etc/rose-meta/rose-all/etc/images/icon.png * These icons are all derived from the public domain image at diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 87ea8bcb9..07a927f4b 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -1848,11 +1848,11 @@ def spawn_window(config_directory_path=None, debug_mode=False, opt_meta_paths = [] if not debug_mode: warnings.filterwarnings('ignore') - resourcer = metomi.rose.resource.ResourceLocator.default() + resourcer = metomi.rose.resource.ResourceLocator(paths=sys.path) metomi.rose.gtk.util.rc_setup( - resourcer.locate('rose-config-edit/.gtkrc-2.0')) + str(resourcer.locate('etc/rose-config-edit/.gtkrc-2.0'))) metomi.rose.gtk.util.setup_stock_icons() - logo = resourcer.locate('images/rose-splash-logo.png') + logo = resourcer.locate('etc/images/rose-splash-logo.png') if metomi.rose.config_editor.ICON_PATH_SCHEDULER is None: gcontrol_icon = None else: diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index c9531a634..706e0492f 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -93,13 +93,13 @@ def __init__(self, namespace_tree, launch_ns_func, str, str, int, int, int, int, bool, str, str, str) resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) - image_path = resource_loc.locate('etc/images/rose-config-edit') + image_path = str(resource_loc.locate('etc/images/rose-config-edit')) self.null_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/null_icon.xpm') + '/null_icon.png') self.changed_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/change_icon.xpm') + '/change_icon.png') self.error_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/error_icon.xpm') + '/error_icon.png') self.tree = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self.get_treeview_tooltip) self.tree.append_column(self.panel_top) diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 9d5fb6e0a..b62f81a89 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -142,8 +142,8 @@ def _generate_error_widget(self): self._error_widget.show() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate( - 'etc/images/rose-config-edit/error_icon.xpm') - image = Gtk.image_new_from_file(icon_path) + 'etc/images/rose-config-edit/error_icon.png') + image = Gtk.Image.new_from_file(str(icon_path)) image.show() self._error_widget.pack_start(image, expand=False, fill=False) self._error_widget_label = Gtk.Label() diff --git a/metomi/rose/etc/images/rose-config-edit/change_icon.png b/metomi/rose/etc/images/rose-config-edit/change_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a6bd3f07991b5de920805866289c06918deecc43 GIT binary patch literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eW!3-oPYs<_AQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^0By6-e_b7Rnekxx_3E%2?wZwMf>aP1dL- zrDoUk)mP?iyfJOnmE@|Os&*5E)GJK<=5;K%Fm?6S$t$n4&O2}FH{T*b2wkwf50#%!8Jy+D(- zPsvQH#I3NoHgp0@#LE-5Wrc9bPaVm4+6H$?= zQ>KK5Ts`pWimAy~ku9YPY%gBDd@U^@<=AU%W@u`>ysemVffif)nz=8o0PSG#boFyt I=akR{09rx6?*IS* literal 0 HcmV?d00001 diff --git a/metomi/rose/etc/images/rose-config-edit/error_icon.png b/metomi/rose/etc/images/rose-config-edit/error_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb578f1e648b2ff11b02f99fc320d01a066eed6f GIT binary patch literal 757 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM1!3-q3ZvT-3QVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^tANtvf_egaZ&#b4gb5k+DeMsYpefjsCrhEvohhHK1yk7#Ns@5$b_zLBIoufx02Ie9(6jAiKcR z#W6(Ua&p1~vxF3~nDF!oQzlKDIJLRx zsi?@*DWM@DiZ-UDp1i)gxw5jc2I2nq4_r8LmdKI;Vst00nsYx&QzG literal 0 HcmV?d00001 diff --git a/metomi/rose/etc/images/rose-config-edit/gnome_add.png b/metomi/rose/etc/images/rose-config-edit/gnome_add.png new file mode 100644 index 0000000000000000000000000000000000000000..80985a300d73fc63427c9e39489e9a0e118d0b50 GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfWY!fpiZ{MpSg6EY z$Fk{CsI~R1nRl5zZ!USW%uRqLdf$8TdC#9FN{IPdT+w7`Xg>Ux$-z-bsp;)MKl{GN zNB!5VN>=q`_*r5UeB3>m;lw4AwMSQpRWm3&U3@L7G)=&DqSqvso$FS+2#TtGtMaox zSYWcCqht4W?}jb8?F<1z5%M=5)mlicVT+#^|IzzkjhX&bW|h_@7JbU_vRhE6l^NI_G{K!-|~d*Ctpvx z?>Qm$=6_Cx8oy=c5m5%m3f}JjFg<8fVl5-PUAB$uKKpyXaAWXv^>bP0l+XkKJ=>mk literal 0 HcmV?d00001 diff --git a/metomi/rose/etc/images/rose-config-edit/gnome_add_errors.png b/metomi/rose/etc/images/rose-config-edit/gnome_add_errors.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4be143f864790b54aa8b40a14d879b104425f6 GIT binary patch literal 379 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgft+pm6yN%Is~m#{FIG<;t8_h$EjZOnCf&p%7} zs&W0O%e}DeVJAn2!hvw!TWh98cCmM4cidPI9$9lSHNb;y2P69ro-^}351eG*BYvyO zXj5o*l49kS2$^LXr}r*uPO`cyb;V0THA3$-|ED=|_amlmkS^aW$)NwZFzWr+2Mr6X zZoKVeYvz^Mk;%6yY@g0+nWtq(v#w??Se9Bf?b=lxl@HP}_io4UQL70(Y)*K0-AbW|YuPgfR+_j4)N@)U2s6l<) zX5E93f^z~6t5Q#Wjm`dhEKBb7+08S#PQH1z)Xm#^k<;^)GyAW~ePam}VBPR>V@-}4 zvo+t>H@`xcXRY9xbWbTl^@X#5`4NU?&OWT%H9Y$~LOw?2-h1qJ%5zKpCwIp!rANOn z6v?b>o%z)Ez52UvHM<{3o=rU4zQ#>oVh{VrgHCCZYv(c))bDy_wa%X{)_VW^a`!j9(U6g}Th-+R2*nbuATc79qwArPRURpNqTt)USs-MD4} zL^rT9GK@?@Ojt}ZOpI}1j1gnv&(6e^#t=Y7C2DGmlxQ1m>HpMu@3oYfzBgsw`>qQZ z(-cl}H#a9I_nvz$BO>GwE!YFXZ}#5TAr$cr267evb{{k2E;H8flDt zET#M+U&y^6h5Tx3$F_u~Y1xfj0FaOA=`Z&6Yrp(@uO8~@ZIDu8dS_E%-%LhC$QN>( z8DqtPBZEQ+fh*T8mpyOwG=R?^@{XCN`F>yT;fU{h_t)G6reyX{tIciyrNW_!6Wm#hJ-{Kz0IlsDd z$1W?GNP&o;>pB1gkGW2uX*!4q!y`l9-Ie7M5uN6o^B=~>|Evy=3{`yJM_XGQx^A@Q z3%S8{;TqI+y)_<>!}oo-OO74{L02Z7{ig0g8|VDe){gB1UHiMCR7jVveCw^Pt=$0t zUDLPqAAZSXj5FJ|@%KODzT?{0!!Ufg9weJF)@hn1N~ID~sT4bT{B_H5oV@@Li^br& zE=EU3v3%E6rfF_cw(18Es|)l9A!45AVRm-9d}nIHeEO-KA{vbX0OsfC;dvhZ8oOOp zq%H~}b~DEQAflzZz-SnTzfI3ft%YImQMpq7uqoQyGB-D?re-GZH$|IOD%ox-QaF3& z+}rD&WgEW{0N#1`;A zZKdGBgMv^NJeiY!fE0T4=1IliO6$=>57CQQe}P0#w&oxb(w4MVmk{jkc9Sk&XFTji zLKN`>!yKLupLz2<^Mq20ooa{hI1b^HRw;kBLwG*Xu6eCh%2#$N><%~^LboCgSgd(3 zv3Ng|t9<@yDM$WKz^zx9IM2-v6Gymh0*m}!^KzmUX$*OzRmzRj4k1KdkK9+Yn#d7H zW1K8G3o(L&Iq&Q6>0u^+W;kR4UnWfB-A9OBHt(SkQF69{oUY8KA?AbydMfbYQ*UZ+;c9~zIH^dzFb2QRB z8^VGQdGcevKS#e$xlB33S}DcObB47Q${&;s$};66wo>AYhnPG#Mrw?%+CNn{#)L}@ z*6C#CBfh@K*3yW;bV@3w+990S@wrY^OwFcbfDX6UJk~1ZvrtOW4q+y-aG2vyjb16| z7!0^Hrn>hfsz~?^%WQV|rsmjoKuTPCl({DE-n&;gaDZ#qMhG)AT)*x@UE3f}5Ld{V z=`^N%HgRVktMvA7ZdMOoxw5A@F0QYw>5t~Wm3cezK;%MZV>{rdnjcoURr5iY_nR~? zlYe2@Bp;&6I9bZ6|J;S~KAfN9xvLCnUP!#v;F$s8WGU57Z}e~T;+?D9!0A>g-`2d? f=grLV-QK?du2S{wY_VWP00000NkvXXu0mjfK)ymV literal 0 HcmV?d00001 diff --git a/metomi/rose/etc/images/rose-config-edit/gnome_package_system_warnings.png b/metomi/rose/etc/images/rose-config-edit/gnome_package_system_warnings.png new file mode 100644 index 0000000000000000000000000000000000000000..22baf7727f13455cc499c7a67082f96bcc5c6639 GIT binary patch literal 792 zcmV+z1LypSP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyr6 z3l%FKb^Ij&00NjvL_t(I%YBnOXp>PC$A9SG9Nlygv6`Valc0mT`35QCpjAP9wY4>B48`V4K9hKJuR~gqApXa5{^#)H zJcPBDzm-aejlg2ydNOURsf5_ABq?p^PNwYx>Llv}N^-X;#1$nONF~G$CAqP5E^VP8 zr}&3NJwboBC?DB?bMGC+tcSB_IYB1?w_fv28xAW;Qc6M<&52~%PO4Nw2q`!&1xvKy zqmmdYVDE}R*qSOr1_0=YRvWV(w>x8m-(;y&pbxmOfEM85+WE+QnV$=r2}uFgRGIL> z*ebCwZ0*1W$Q5BILlJlapn#kdJlBRb)22)w21oGx3Izbyp2p)FsccW?x+QiXUkG=8O0Umu|{Ma?Spl-NhTNe?JHGnZS%4eS_D0m<|PN#e<{x zxdK^Z>6O64p$w(L5ekJew9E?PI>b^5(e>AHb-50)w%I}aUy${fp$+SkX?sUlYe^-< zJSFMtoWp@F?O-hS=1Y9P!tfN;qC;zNr9gc3;MEs?W<4Hh!~W?8N=b&-#fe8;m>+c! zGv`1tQ$q;1Lh&mv#(pv3V;qHi{~cp2EozE#y9y93l;L9zuPN)hD_chc5x17ieIBDw zhM^24V>u@UE2Q9su}n-ic&V#AegB=ZHeAk^xD;`Tj7(tOW~oHNn8i_CW1(ljp84-D zOs(u*yqTTjKk&6-pOTyo1=;3T5IqCd{qHvdXv6vOA4~#glWF@{8x9uAoHUjL_1@o~ W8YEEyr)NI^0000RdP`(kYX@0Ff!3Ku+TLy z4>2^hGBL9PnaO*h` zAQ1@E6O|g_nda-upao=eFt9QTF)#yJj6lf1D8;}EW-~B&F-pVPL5vzuHB7)D5Jsp6 zss#ZLAO?jmn3fOvZUSWUc)B=-a9mGLP!I<4EEyR8&x)J`WHETU`njxgN@xNAv<6iy literal 0 HcmV?d00001 diff --git a/etc/images/rose-icon-trim.png b/metomi/rose/etc/images/rose-icon-trim.png similarity index 100% rename from etc/images/rose-icon-trim.png rename to metomi/rose/etc/images/rose-icon-trim.png diff --git a/etc/images/rose-icon-trim.svg b/metomi/rose/etc/images/rose-icon-trim.svg similarity index 100% rename from etc/images/rose-icon-trim.svg rename to metomi/rose/etc/images/rose-icon-trim.svg diff --git a/etc/images/rose-icon.png b/metomi/rose/etc/images/rose-icon.png similarity index 100% rename from etc/images/rose-icon.png rename to metomi/rose/etc/images/rose-icon.png diff --git a/etc/images/rose-icon.svg b/metomi/rose/etc/images/rose-icon.svg similarity index 100% rename from etc/images/rose-icon.svg rename to metomi/rose/etc/images/rose-icon.svg diff --git a/etc/images/rose-logo.png b/metomi/rose/etc/images/rose-logo.png similarity index 100% rename from etc/images/rose-logo.png rename to metomi/rose/etc/images/rose-logo.png diff --git a/metomi/rose/etc/images/rose-splash-logo.png b/metomi/rose/etc/images/rose-splash-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..897568bae4901fbe66f8d3cfdfd7bd4a4ef116bb GIT binary patch literal 19363 zcmV*mKuN!eP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01dGK01dGLuOho100007bV*G`2iyV{ z3otj$G*QX`03ZNKL_t(|+U&h|m?T$u?|;szT-7+>5{f9Uw7b$uo6~HV9DBNFI;YC{{Ql_O+0ja1N!Rx= ztmb|8>8ED9rn;v3{nY!O?|IK#5MvCMZP}LXTrsqK36^cymXThzWm~poq|YrA#me%S zW!aXIK6mWZ?N31nm#@RJ{h=ATdo%eEY)&kcjXK6&uK=<Nx z6|<5@4$m%MhGknu`kX;JYde%>E$80Lwv6<-VfrY`(7Rqf{D(g1Kds5*)Asv!TefAS z-;stfuqz}*oH&;J!#`QqbsfiX9LHW_j^nsXY?10^w$A}uR=nRT&j48S01^=3#9ZbN zGcrR6YoNNWTLZXe004v#gb*Qw5CQ-!ZrA=FLRkCt+UxSSy^Qobz#stnD1amY)`7jl zf3Q5)g0pFwhGA%$W*CNLS+#u#f*=S2AynI-X_}^KilRu8Tm-uY{IV@0{mw8e0{{dN zBvaKt9E=xp<{HelZEKn)Nm997&gF91Y%Y;V(+m}lMHreUNCF{(rYVMDg2ABI>lFln zWm%f0Ns=T<5@Wnb^>VmgM*3VgBT`;>%YJtq9~P0 zC7aERj!sOBPbQP8R7UFTUOAH}G9>i-J+5O`D`iMhu~@LXt1A|(_xXGtkB8@Zj^k=< z69lo$^D@%!05cPSMGydrA`c&$=;`VHZ;#T8qh-soOw*KQxm+&K&L)OO=S&mFKP)yCd6 zq!)90c-ofTVHxQ^a>D=^GZ>PmeHkAg{O>FatqEY7rd6xeYPFipW(&o#C`y@3UeaMp z*MOo|=4X#*vhxhXLC9E!DF~e7+5|yGV)gm?xuR64l=YzBU)LBmOLRq|W@d{si5*v6 zq4)Nza2&_)_fr&w5L%qph7i_-v_;K)*)Nun{^JbKjxjlzqLo+L!vp}(b{qi6p`rP` zdk**ZwFQIW#WeW;sh2N`VUFV%h9OB(p-`Niojo#m^yQa#w#ND{xa6wF)^-Pw9+sFK z-90{ZG#Ky#2$7{$Dsn8=Aj^h}HsSUQ59b*~kKazNSn#>c&M}GU{!R;>|oE$G;K>zGM zrtLfeLf|?W001y``v?BsbqJbvuXxwm^DkU^`yJQ*zXrQDM^)3na=F}zlOr?p8sa*d zTgnUshyH&7hV?eYnn{9hY-nz2Zi?15#A1!6t>w!LJlCY_D#K7T%{dkr z8he$cKz&`Hp+15LLf346ply77NK-Sn-F9nhYilqVWLb7u6Vqq>h!Lk%V5(EJaL+4m_%jOGJwQ_RgSU#7<4qM;orx?%B@bN;ia^b}n zm&yst)(H}5ifvn{ zteCQ7U<56*s_VI_iDQN-Gaj$wIEW;9+5>24Rz(`(rusTnH%-$?&Sgz=*0l80!qD{0 z!C0)FVR)A334*m#Y!eKR}l*y56iQdC8)N>=t90I$L}uSTu@wDww~dU3f(jnAbr!Qv4XHTK%OwQ+k*lRN8TE)%|t^x58U@BjS4qYocp z82a@jLkMi!HBEcnO72vY#(<<2>m^Cs)*XkSX$pX|91D^poSH0MwDq&kz3}aDB=ol3 zZn1LPG)+ZO=I0k4dE~ciR-b?2)@x@b)2zS;LyRv#DYBW%nZrXvb2F1(kAQ6gV{GUE z1K{)fbi0^J&AF~&7&>qPPZJa*fs1WJ%BSV*LV_S!kH^Pwg5yG6DLSs@I!1eEYhz>F zu>pmta9H4c;c~@1ba1dH#6+V}kHr}@Az#cEa}EHABs57H9UU1OKAOuUR`!I$ zVGbe$0Mt|iB18}fBCM+tB7ke#x?y4$TeefFDw1SXMWa;KUDqK<%H#3*L-DrOmd?)3 zwzd|MA}Es2&L`&*Ly}_GMb}Xbs#FqJz3YO`&W>O($S@25z_Kh=RSSipEX$TF$Ey5 z<=XIc(#7ccMm9>cty41BSE zTi5k!wMtQJch|}T2Zj=faSVWCnGENn7y%m^0%kNE3c>z(cDY8M0^0H7#JB9TsKWP;{2Rd#JR916>l3}~7nO}~fi z@9U1oV}c-%B;h))VObD>+7x#!4KWO(TCFZDWaiW5XhWY^uI6$lyc{Wt^5EccmT|AS z_Ud)(*4Ne51p)z{=WE0G#bQ{;acT@x6m{0xvc*t;&Qtx4MjAr+z%PIM=x>fP40#sk zu4{k)hab82`iss~L0V;ibpYA3#?T0%;h{urTM#^7`Pzp+{>dBu$49?6Jd{T0HHn|1 zIJdm_|2+NteQ(dJYDk-=SuU4pN{|(eqA2tP3E|^HRx@#WGEUP_*rZy(^rZ z*vnV9ZZ-${6p|t!1SA0qYTK^8E9d4?7jE6!)EwBo=ZTTb(yY?U{x$M3n$9vL;B ze_7B?C6`YdrsL)P45#Lc#m=U-%^NS6m^yKE@D)hn?(Xhfrnq$;?1-D2zGy)+YDInY&Kh$94Ee(@RLn!0K}@|%Z>rP7A=8=9M&{eJ&qcA)FJVd$n| z;#0*iEYAzIl9*bGz8Ja}7l)s#Y!hGm+SlH(A$oQ;b@%PxXE_?aM&yD`&4K5)e|uo{ ziZg>TjDbE1)WZNG001Ds1CP2@@stv7+vcqwxMFceEwihUq3)VR?4Mf z#?Z}pJZ_kxW>$DEpj2hsa1mtkS+Sxyxt#1eq{rh+CT8a63)!shxc=E>36i|+up|}c zIYCuQqhlu=i)JYL(EeA;>5O(2{fWEJvEgsRb$v3k%~Dle=~w8XZfFPfU)D zPbQNoiXv)@Sae<2b=@>g%d%cCxu1JfzjLEBgz!Um{4LLOXJOOZAAjWWZ=NkNKnTnf z0K^iZ8`hF>jPc}T`t@@yU;6Uh4GocReC;8Ip_ZsM;AcPGb@MHku3Xjqww(!MFq=#i zE25?<1PM7#C=~M~0k&`7p_$5tb(gH@S;f%2Dk=~=wxv%_CP&A-y*&ateRILKm0ie* z`4j;W$#Nc#KVK;RDq|JvTp=OeZQ*ITEd>Nv>SUvn&k&0Wig|bR^sm4AhC$isb5!tC^O92#03K2ovG_ zO8b2wK(5d{IOgSxz~Zne&IdiYx{F;ceTV>h0@f7=j=oDTy~^ti=kq1oa%c)>(#hGG@%}z$?LZhI zNFd1jx-_lVG!@s;FeZv6sag>=L#>p{j_q#Q=&NNGp#YfA7&CL6;{b>Nh4_}HMv~+Z zWb5Mf`NB*hH3OkDJ2Pb)c6}@c5mr@&Wjq|`%@=a2Y>HJ)5PSf_OnxpNt80kYtE$W} zq+wdRRy9polFHp(ZMtDps+CH$q#HWV3-xu;YPA9Yj>TgHNtQ}^MJ-W;DN0#Ik$t{q zuP@}-7$X9b0!fGF=0pes{y=zcZW@uGTrR1q?(_K<(xvIisnmRSb}lnAmMT}Rsp%Pj zEtX|!Z5|e<)0UAw7tD3tJ3jbrk|dXA#RP$$-~P80MK#5JU-;-9FYl?W-_R)t{3)G*fvWsl2j~} zvU7>4^=rKA*G6kdi>f!1X+p@!W)mdAB7!wE15u7BmSxGf_(HEB&^1?YZ14?_w_%JO z*YxpzMH5S9Nfg!jT&li4=DIcj21OD1OfFZdtXaLPp`ig{if6qp#-`yy2n<7#_Sad8q5Na@Vi=vsP<|fy5gZ==7$Z;%9uBcj-K*;Y4002o6Ikwr{)X8}wnWDLJ z?InE!n@5I^4-O9cysXdXt4*h!ldDAEp)IC<{4bA^h+X6vV{91qeLwh^AP5jbR;c^_ z-(6#rUfsOzbD#b6_tNPs0020Z-#L0AiRr0~ zb}z=SsZgOP;=b=Z{8onvwJHmaV$IFX z5ZSs}G^~Ow>FIR7Sg4qWZR(Dyz{&B2y{`^Zgs(2r+SJ%=8ahRzTrMY8<@7?XTq&ET zE=pC)bc%%vNwO5l@vP71^OK}oEElR(i6mKBQql`^np`zhiQ{Omz=@S&DPOD$*Xf#K zTjp$HZr}dBblv&n2WHIqmQ{>b``7gRJ!6ji?`fB+x~ASx=wpg_cjNQz*Da5RFkqv%paDqeKy zR)Fn*FJu``eWboF8ftEj8oCh()HOA=RjP7TthP5d@QkyN$<@dEFeLqffbCe8VYs$s z8#2WUg5Mts1sV+iTOy_AM?eOa&qVPCwqH)>gwuh z{g}?Zm1OTYq%p<^4jg;%fxRS2ED6!pzx>;uMWdlbW?0Ts$R=)muX5yAHI-)n1})LksYdHFG=zVg20zw zvGFbOyx6{zqNs2<=n*)Uq3i3zf`_MRiWhqt~sC z)Q~paa5>*BRm4KE$gqsy@#YFef52~b45hiT zpu_#Iea&@`v7tq+~5z8=;pd-y zESpPtyq@mvZjZ;abS2q21NJ*k@rDq-ymN@5siptZ*%ev2vimHAsOSfs(ieX61K;f* zD|+ZM24@#BE|s8*1^K>z)qnU?mSe#OZoiCKvg)H2_CNKR8^3(-FG+H-j1>I(S1*6# zuRs3gPkZrRz_M&86ryR`?+0Gg&Tb~D+u*Gt=`=GbOU zoUfUxC}Shz7`uuoA&RDG6JwOlPeO|x|r>+dkGl_JJW-SM+l9;P&Sy>+mn1&n) zc!5n#W#kEUBE?e0VlfbGjyJR@np!Rwq{{r^{qrY}?_arcbtT$ZA8n3=TL6?Onh1D> zXed%D%9T=XZe~i6tEE!5P{;#BkQ7OhWdF+Ecr2bs49l_-1%@K zGu7GD(9_d1Gm}WAQjthxv6S^(lD^-GlUNTvunhxhkx0jJ@3{NwGg>t`blNMs~xsf$%NwyZkS{ct1ff zi?;&DAtxuNJ3HIoe0->d=$aQIggA~11Olq6dc9tZaXOtV6szHA1IrMFLcYDD1v;){ zS-Nhx?ir-(>SL=n)Qyb|HAS1^@z|?-4qCQTDrSnM(z=1L*FysUXokQ<moxTzc1+b zMrV?V%zUOA)| zh9D3GG5{w*>K*_Yz;pw)8xUc@^#Cdg09e1VbLd2B@wZr(-uB$lPky@Nt!5dEOPXk! zW?7bD7>seLRN^=$5cCj;QB_Is2m}Elp_td>J%0Q!BF=o#nqf+o)xU0_S}Z1#sbJ8T z&u6pgX@Mgyx}dJs7gPv^_V4XZBn(s0ZQa5oiS;62t!yS;u9QiErAeI0PECv+E>}w3 z9f8~4*L~HMtzG?bS9P)(kz#<~M`d#I;IYE?T}PjPZeqoXE{0)drcxXq=p5)jIk>+n zl`q)ZaQm&T7hTfk^+i?D)wKMv6Q!U1G93zWAG~#7=breBFK(+UqA%n#T#(6RW~N73 z%6{LCEr0sHp8hpW&}3yrCrL$?)rm><>21=`*c?e&y&KlfPGx2%r-z4kW%6Z4@bvbs zUA=Zoch5jyM@ywtibk3zCT7#=OgtX8gKc3jJKOo#}Y!8gD6 zE7OyC+X6I0*JdjLpuN5`tFkx-eCJ2Vu#7*uhruR*r%Fu##-O>uedc}|!c#TW08ju@ z003Z40b>#X7hoFz1gP+rK2AKl9Zt=HqeE_?1ZWx*^3vO%t2ph2wO8A=b9t$yV|6k) z?KmpOk-&Ddr3F<{EXQ6SF3}BJ6{h7x_Q3JrKR7>gkjH<}D{b}c=7sUf1o?xSc zgMHigxg_tpid@$a_IU$}oV80vG!liFa2%Y=OpFd4t5$P2zNh^If6}^fbIb6U@s%$h znV6{P8ZIf-#(Mrkw|8H0dCP_8H$MIR%s>42*}D4f-W6-MY~Gy9&CJhDuIOYw_o<#O z=e3nfy}S4I?LK%25uSl=;%i@KDVm6cn|}51tR&7h)%iU_tEi}RGZXF2{Kr4sb>l57 zi`B0CAFv6UVF>T|*opHt;tMv>eSN;ie_MTFhq`aifzh$yVzC&F#@ah$E4unNtlvt~ z9uiSgb6HW=7+<|OKRY}!-rwJ6+x9uOa_Jp}bSjlRc5M2w-@F2#ZrcPxBuNRiJ9BDI zq9}%@f6sjyGBp0-PoeEtpZ_=o000nR3_4rzlRu&H+2ib`<81;!0sssgaqSAQwub}+ z00huQJTi$FiYI`62oOysjM3!T%J!Sl%xcTCR8`IA^SOL!!x%8cxnvqxOic1cJPpx%gj_oiDf7PnH$z}mtNd?-8CI`jg6Y!Hh8G@(B2F~K}Atp z8rw}nsaA`Yi9Ec=G%cTBNTp^cCPpN=@ZRg2?zyY0XJu@1hJNTl4H9ju254P(3dQ8? z%)&RnJ@kot+Hbk3{ZHT5LlW@bzuccl%odCDQnjGS<-2d|>Rl1iZT6SHD%torMbO2v z`20&GyqLcD0+10E#OTPv;L*vU<5SZ!MMJ|hLlXp|DdF|lya|dRP2+$58FpOv zi=RaRfZk5<$d4%OzNz0mGqSb;umQ`!b^Tx+RB`6V06R1!Fd4;Et4p_%T&oG2Nk%}w zH~p@42&gTeE|<%blat9*0b^%iU=>Xh#Zu9>Er=mRBq9hzpv6nHa;5wC-)2R506VUx znwEhXmUdl!ZoX;fflf@ed3b0TMOjod*+gz6;OAy$ve5OqE)>NI!=h3#TPUPgukwEQ zuAa8`0KgY5Vq_5Bz$1?V4cEiqQLS>Nz=F`svSyo_>1H zw#?QRe}7+qqDWIWhDK(>(NJ@1tKZ*z=uqm%_fK_pGzo&Yv$MnJWz(5-p`x3ndiQNz zmtWTG^#>pSHH46)Nzyf4U6&~29vwWIo~y3vzc?J`zVPY3(J?t3X=-ff;nTAK03ZNK zL_t*Pn#+@ywC3&~rE0b!i|#qPtoR&3+Hpa0ZsEj2L>%y) zfa(As3;9>0&9n#{Jfwbj?fM@B|0-IirtmSxNG)IO6U zQKs1X+_pMh%b8ZSr2)4$7a|cCB9}mkTyA#Xpx-daNYG0{+V2x)rbay;#7QG%%ZC-jA=|4CVMKLkHr&35ZGP7-4(zl#v zAc7=Fo*)oG5EM<)B#8)!2!J5aRNXKP2myjM0U7%J0_*2%Ki9^a0APS?8;c;Go~;4^ z06aTk+N#sN8FTF7jo|bK01%$~clsj$IJJH16+(c}(tQAg`1C7AwNW{c9OpR3M*;z2 z?b_zG>-$=}23GW~S(=jAwyo>>{QUgj;X~Q%JRq8KxwLKD)6+ASWt*l`txB!~TnFQt z;*9}>(6X$uC>C;g7uZ9?s-am|USeJUZp!Z`F5H4Unu|}qKut{~gW(mSkf)(BtjIFL zR6|Q0B1qeEMn+CfPtErC@*CIHQxrinL?+h=DW_O034Fk|0q^l5GF~cI8d_GOLkC7C zly@7ZKr&4Y{Kr3f$v^(f?uuBpEJxKW3@}NdPky}l&+py2edj)sU^tEqg?uC98qHIY za5R+~t%@3i=!%Q$8ykHDfyPGVbYf=zfmd?5l5Xg(>(*|Px(+1KgaDY9d-)~x`wz*P zmv$pWGl1{y>zx@Ntx5&QVN}^PH0{uV-K{OHt5*+j9Jkm;i>Kf zmZ?$BB(^fHo+OMBV3z;@0FnSe0HK!!05GpTfC*Id&41i%%Dwyp_QBth0YCiWHyw`s zFHlM`hK}PJhDi`kduQab%QrSQhWl4FZQ3-jxcqBz)VH`%i_-ag-s9){d)Ln=%0ZvE zD0p$`cNQxn7Th}DuQd5(Od^WAB<#lWN!eI^q07$N@`yxY2zPfi?clVlj ztUsO35mY|t3uJP6+s28crK%>+Gp6O<`hiP@=uIaF_n$m@U~)?9?!tfo4KeSJet7jm zkBt2EmnSkgyEWESDb{`E|5QUhW>k_Y<+32){vIF05RUE6C8n#@LrsmSp&<+bbO8nc zK!mW113{KR5Frqb3*i`<$vTIR?4Qew7NmjZh80zbV!RCJ4~*{FR>;ra_Q4O-*ViqU znw(1oWamiY4dG&^5kS!#00WR6Abh97D>n1U7zK728qa2=VJJl2e1rDW2U&{nM#Bw0KSPtQX-Rwc9!xGwWU_Ors`ji1 z(KHDm&<$W)pton;$&q8vZF~Ap-(MFAd&*TgpHHWf^MW9#Qb|$m2mr2&J$@LC#Mf=O zh@$zE6Z)FfZc`&h2>Jc&-FL3O`pS;&yD~4ms*wa=*VGY-1T~kOPRt6N+0Y=={`^a? z=vcX~)iN!x*r)^mh#*NIl0>S59MjDeY_Xj7c_Qz-`PNFcuw%!Lj<(!}P3JeYw)U@F zx%*|Vy}iA)wT0*TbFv`x+*Q2Kge3s!D4-)4KmfpRpW5?E_Mk- zJFWo%c<;^Uf9LN%^LqgPJ;GS6!-=Y@nx=7_*XIu&II#cczxda8zxxIN9Ya%m9h1JdFCbOT^Hq6bn1?cD2i-p@!$CY@4GJ3w!i8rR&CxJ zyXcbZ=Mt0U;*i%vLkKCF*mqd_o9`q|Q)6k0VJJ&82$E!3M%VRZY8FvuI#W~?nB8U{Hq@T zjya=!IgV@SmW!>9&bsv*Rx~z;&)eGf@UM3~|IARWk3T_B0D#7Z*qhed&)g$w?M-xD zul4jO7K@7N_$fBt&^SAjNF*jMz4RK0pr)x1*E%&|u!L#DG{Vt(kC)aoLp5<#Qkq&i zJsg`zEgU~-Y}x1%1ae)tel=Q1%VT4M#e8yf;%H(jMKRFtXGyBLSgbNMQPUoDU4QnO zC*tiFQM7mK=1VO@cWnLqE$gReleVP;*RU;p@v^wCgD8sR7~U6gF@+uH*_VT}^Xj_( zbR?v?7(j>`;`}YwIlJ~7Gcz*;f*5jj^^LWHeAjhX4Gi3M*A3-Lu3X6hfNj$f1VI$V zLcWM8&bIYHK-CnfS}E?^`(kHj|C5hDarfPK53E}G-S2&W_p2|Po6cLmVe{@?)rs+` z)dK^yHm1u+|8X0~c_6efU?7Af0Z9TF20pNVYSWhUAUN-IAhUs)!umL*OTbJ5D>*Z+ zU;iN+LiGBWiEVt>t+eYJU;91|1ekT}yE{8Vs|K1kZ0c)ii7#Eg`nga4Xeqju<@B#= zc&oE?wbpsHR^I7!dU|?#c6MG6qLL&Q3nD`JnWvsC6{UgI>uH7#ha+{7C_)5YqQ5f? zty^kEECxJL&L_y4g&@(?+Ua7j!Q^kVHFpDj$ zv99Y^U47x3hj}qX*V<^#&(9y&e>k;}@9tZxN=2HsSFajSWFz38P9%n0N8GsKJYe$# zv}l?jmlT8s{Jx%^-aWf3lj#MoCkO$Ffx`%h^Z163TQBTz-*=wh@|90img!T?|lm+tWMe}7>K z&j0|p`MkEbr2o}w#Z1$bWjT|{?B2bn_60$34%O) z_((RF<#<-$PbuEDSS^b3=%ubdvDMKt*a~XoLXciql=1xwjiw&_?_E>#w z4l>mGczrM&j>Ti$D|(umS{E|NG+c}!#}R#f?I(s096J1}iw)O>v1t47@YsAR?Yed@ zmkatGfu|6H1jEzJfG@HP^+i3jk1gp;Ri1kYH$9uuW3ny}?kxv=vDyjGjDvaR2t{sgopuy&m4{ zIVs%QR{;uMj}jbsbwT6$(DDw>}(?M`EsRRLb+3mQ^b0Vt($#@e@D!`Gl@p zr$jq|`@5zzajsM@7={TT)zR74*4CZLv6YGi0Jd%S>Z`gK7W~Jr@Bj5f4@^%_=W}9H zOGmL-o|&ChWU;f2;y8+7NrGz26&gfcm8DWB{u@VIMPHpbY{toFvt!a_!tb%ynYLQw#aiygyICdQAY5|f>+ z_QhKTuOI{i!C=rY$_nQR^siZe$-A!UU$w!uXrI@k>r!iLv&Z9k{UbY;k^Un!;@bDD zUwVf|2>tN;Pm5x8@%axw{+Gx6Uh{=ta<=W{jZ3b-`PRkfxx5i`E`V*{x$i@vF#5%# z{8zsv^2Q?=0DM}?{^IBVNf5X*c{sqfrQLS>wQn*?)Ku=p2Ue!j>G9E-`bb2s%%)N& z(%I=kaRCx0NokI2A%qB$%w{sBQqk`h=I0aH?7SeL+GyH!0k(>n^yuWo@nWgK3LHYz z$mqmlPdq(%aIY@qY04QLRky!XvMi^j)dvIi+SQ4%(ZOxoo*x?>OC;uL#$T$k$A@dG zySs5czpB53;mDU>emte@s*Q~TU+v$A1 zkk6*`xnwFebMp9+Lx=WcGnozRLf`$?mJi-`p4S^u6*W1RlNHBwsc5_--rTcx%cZw} z_%p5Tz2R{Eg%@5Z2tuv>*z#EVT*$pY_xYQjed?20NQYvaZ1d{2bV>=L1B<&#}3LjSkDA&;Qx_ zAKxE3bm-`_&pw$-%@<4gWMT>-=h~|ytNJ6JaL{!xE>^3S9#1MeiM)kr? z$%mhEPb5Ylb_#`gv0B=)G1SoLg+ypRTW{NiP$=YLha#!xcgoF;c05LTJnl``8ozu9 zkBm-Rwx&p;sz?CX@42S!)1T-I)vtN-$xtCbnoVbUPq0273HW2t`nF=Jlov%W7jQ7O za@DF>EVj6&@*Hc^|4!^Z1|bZEgC~xUq>`1Tb6lxZ86k7?mR0}$lVAM#r+fW9U-Ly) zQK$Qm?0#i1lMzpe)i(IZt-vdsp2x-x2;T`20swS%x7_ow>vYik*uxKBa>do@h3xxp z`kSS#yfMa>Z9MScmk}c0j2qP)uBLJq3Wa0GPNXyCmgWXYt{N7}Q;egE3}FI8#hPOi zu40%N6O)r;-5t!p%Ha81f_LB6-qRhTXrku7G=(m{FxC(kfiE<&q}kg)bWbSSGM%)$T6KXZ2m}cP+nSk8>-hSpr>(u7@%9VBfNe7ixv1ZxMM7pv zGwtDFzKD;Xm?#yi<#M*J4&8No+buVD*S9o7yj9FLku*~(RMVNcsmXDOmFwOe_C>p! z>aE_+Dt0VgD+`=;>4o7B-PX|4-zut?%%x&8v$L8Zd3=FDq<$f5BvNE&iyiQTpdSwO z8G^?Ncu<=wOg;d?)*$9cm1`0Nm0~UN&5fz)KxUizty=b!!UCB zyr_XIuleA@%uKa>oPnaGF6dI;=WWK2QBBd$w9_;zSCnut{DseVcK0@u6op+|1q4DK z41nvnh#)ABd(92aSHA~xTk{&&0cjW)!(Z$G_ZI6Nx*hp0H6ao!}1i1t7oShNhfVf zNum#aNPt5&yjE{&+)p-4jrBjM=1U%u(P z-+pSbuOI+`VW`i3>fdtN^4%Z4;Vh{72U>00rKmTmFk?DifBaM`bK`YiHT1KS^@b+? z^M8KlTg~)p+L$8ClBk?GIhs!wfJxYzK_ey_ZWM&5?f{O4n&C`OjT||+UsTke{Aztm zbBG{twOT;PUA3~AB7KHwk`$fKm%VOjoF$_)9v|(8o5aDPHB9|sDSu9(o*--Dx7PuE)2z0b_EBb=s_^#pM z69Vt?_%AqiQpuO-wzd{o8<{W8hNJb6q!Gb4w{`eEXmJ&-~l# zOH(Y%xc?Vlf7>HBjNL+Vibfoy8Px<1AOQ;UUSA@eo|u}NolBOgIoD7+nwqY-{Ib?n z>!3wij#3|IE!&t|Sb%mG5rjA3r8&fV0W!tZRPLUC{(2%cch$SDzVx!|hKHwf+1X$q zsH!qVuH&fKHAsTC3=%*m6phdftw?!M5(y|{S0}KpDmu@3EDKG~x`=8?CSx-*siOz? zrILY_t2a_CnJ!fQ{ub3v=5mGR=5EWR`uh3-B+|JCua~tfnIegFhM}lt!_a*mFJiD` zt25J+Gc(f=f?(M1)%q zlg~bVZ10}We&@cATyWvK8mQrLI1pq^{1c+@oW zC;$G*=H|w?ajLbJV;+x}qFr6e>#U#l@}(+Dq!PnpBZW*#SF0>b1j7Edcvo{>GXQQy zl#xYIBvw_8VQ5v;ZBw#sQ#DEP4>wUg3&oeJ> z-=-LDV{4D#3787SumQugjvpK8=vWyJM+t%`6f(s^PE}Rgf#Fa%lTN0Rb1lt{ri-)b zOu1YN2Eq^lMXpo|xx@Q*`8`5+S7)F;R$m_zC1ZXeD@kHi7ORyiFYqlbEtVM;JON3O z>SK+rOJfIDN-15F#43Zaiy-XmUJ;Gf(KI}mSqx@1qlXV+Hx>W3m96EqDrdnVuI$wF7&uAU^&03YPwN!or@37&vHJ~ zrU-_Jg#9$lD22*1&p)?mtJ~SNh9OB+uj+U_6QUcFW1Yu}ccST$RvM3R> zlP@P&+Lz1Ds**^<5CD#)iG@PZG!2sRA%=~{dMFZky)YDvluP+?c4lsDaC&xj>!nw; zcC2tPWci5Tt0!5CCS9-BEEXz;sf-LC^@bwh=1y-Ya%BImiSgqXU3lJNBf)dctMNJ7 zuH`Ld;F)yx>MOtGI>hVWf)JXf&GG2n*KVFmq>c|}AOxz0cRvE+bu<7#RLJ`tJa6ar zeaX2h$G!19r=}VA{_ppE>=S?THd5}|7i3wMq9~b6X6MeAcD}fKY;2^EpS$zU+b+K3 zCYtqN>=cRxmZbnDC8=!L+FUZ7%M?$ZIHsG{nhl#BmylHxo8@pgXjxW1m-Bjr72O?I zyzAYn0Z)$2Dyl<30|5jfp{Y7SlBQ{53{2B-9M?2WK)4h|(KJgE6h+Y#vL!L)+9fYb zdwHLY2?zLnp~h-8r>nVAYJ4g&D``fovC|)@_j^M=KTnd-G+a$LCAF+bl4DuewJC~1 zoX5)tXp*5Rq-tV%VK$S_TDI*vZe65qMR$L1Z*MFfBM5?`7{GXRO@qMIR5hDTO^**& zi|M0-2S;b8F+q}!!}4AW!@5{J8jiQdyP}Z@1lH)-k)e?jmJ1oi18gf+AN=&EKecx4 znudl3K@e(BmS0BtT%%f&#G7vTYGy$O;0>s&p=enuMF{|4RRg;ocA8==#(3K+?q|O0 zP!xD=|D!V|%gRrG{<+Jq*z$idgg&KsmrA9nsi|*%08LmF1U}vik@mA++&?un-Q7K)>8O|=^$0>Z6zl0<)7bF;w|8Z+bzJB9 z%$(V`yWdOlaus)q+NDUcEKBl|*s!J8DB2Wpg`|yJATH20K??MtK+)HtDB6b>D9}C? zMT?}V9}>4sEjN;4+L9$w68A;q<$5<>@@})woHKox_?o5^yH;GsrssPghI7xIb2vQT z`Iqyb|L+hL$+VpHTuHZFj7Uf#k^mwvIW8gy!n91sadlnOG#z*zLI1qiIz=%MQXC}$ zVA0TJX-Gj>)#|2A@M37QxLPT#wMF^+nYrb)mF=<`iX_|Gdo)c`6h%?wt%Q98XDAzSp*B#ro zOvAA)t6nYUGL~H{l!|q=u2pgyg{-RU4x$w$nQH26Z|#HxDR30a5r$DIm&@^JY807`#!3F>Tf`p!jb~?QXk{F-y zD2n(6OeDe)`bYoy7hU_>z%Q}$*~rK*Nm5Ho%d5Zt?K`JW&d%O{^JiCW+h+L?AV|k{ zTHAY%96#TbZYmop14+Y%vdDew_x_+zSgqCco9|2*s>Co{TUWOfiWIALsOk_xf&hf) zcpiq3_B@P%O*6FXI0WfXv?qu(Me!~|9zl2>MhNLz8Dng7@o+dwa}3R}7`gGdtXbyr z%95pR+eU3tH z45Px(R6LqWC8DAjIe6q$`@R91RU8jMk1**wTB} zTlSZK^@qRq>tlcNy?-ngOg{kO0|qV0AY@V$zw`YQ8K{#001FFAL_t)IPIwQQ6&_G^ z+2i2jREZn67`L2ywdBnhHk%Lx;2L>3kv2i1J#YxLs2{2T>4`kboq_7;!B0k489- ztLuhs>zk!bmSGh|1^`G(+_Chj@f+n*fgnLVl}60{;l5kfa_TqzaTbqz~L&kYSf-raZDu`G_1IKi2l8-44o ztB&cBAx{uQf~3mZbx29JYk35wDaIoS!!$iWx-PEOREi=bMT(}CuI>a+F?H2WHaES# zGF8`W>9!VDjO!L)#grIs)*S#S1U(1vj)`?h!$>6J7o%-Nj|G(8v#bUHoIZW@+Q`v= z{-@Xf?r(pzS+E!e?rQJ>hC(oN{{s;z7<-Om{>E>f`R*ToEf$M?QjvzfvF9h&@%?0} zRLZt(%d!q0v<$;gRW+N>PfgBd=BGNlhno|vuInOTt`{`yXr@sGBnFgRR%h)EhlU}n z*AQ}S>>-3SRYjgh*cw8vM(~Fsj$#+AF*wZ_##9Ca(qG(=} zqYT4xJl9B3w8x@B14{ow+x6wk&%b>6`D<5ie*a&uz44QA&!ZRyzW48czh!X)*EPU% zz;%&jo5zl~U;65qum9GUX_{-C0Qsbb0a2y8H8D9hJ8}2ekt3HbUK~DfKv5KoakW}qTFSh8>(-SwzHe%prm4Da zwYIc`Ls0~!LkAvfN$;~AlcZ_j!R4iywdMKq=g%BCFqF-e$|{b86Q*fENJ4?FTh($^ zb4qJX>AhVpo*u~gYP3O+-+XRJqF`a5^<3(B!!aOHwn&x?=B1;H)x?#DF>ABd& zM69jvLaKfG-tFqrbc`bw7Vo~cG8;>E_75Ey93D=^<1Tgyg7ii7-m6&|Q2HapH2~n; z`IG0)p9CHn9i1E*nVFu<%*-s6itmf*>HE}$r_x=W>CUdbe&KF;N@$A{N@pxPmMF4+&(E8NnavjSo2A*ATNEwG;YhuvZY_U=e5+LTPBLXlD>cje0eT=|cG zfi%(6J3KISvbAH76D8O43=JXHa}hRm!$qV^Leh0r)0&vPo6oHyghW}{%1>S07{4>t z90@fS^NRvY%96Y%6A1<$?~g4c0RXzL5B0u6LVo0jpgBQ<_)9?gif=pv;!_>Z&g(`c z9se|SKA*omdhfsf`+7dNa$tDq=+Q$w$7Zv65++yH7g&bk*;qItN|NGXkK=fPB)7Lp zH3Jn2d5mm=AY8{~1v!~Y0qp+t%4=Jt>^CmIeBr{m*4EZgD8z9bK@hI%`kKFGS%zVl zrm1O~s;V2=Ts~h|UEP>j$PtjORg0xuW^l0o<*$EjaBzTS*=n_V{bx59mi1Vo1-b6{ zog2$bqa=a4ySs*l1~)e=Yip}6qC$~WBAK>yZFO~OZRP&pU|(Nv&w4gjuGmMv^kpd= zvu$%}ZlY9NZcexL^c{9xFJH{fP2Mi%?k8g5!$%G(vXWU|&MYmKO2w`1vh5&QmcR3z zKl7Zyk8BMrfHUC1wruBq1m?WIF4mm9UUF@dVOnat9x?x-o$K1 zFaGv7FP}Q~rS|r=a5xMh2yVLLmboIQ`hE7oLAM6bhN9xxBJAF*Q4Kea3Yy?Bap`Ze&-kz4=JyaOryi1_B^Kfiu$ zb>!bca}0ydfB1-0!Jn=w;b%&)EXy=a$8j*me)@!Zy*@WL|IWyGQ@UeeF*7qgmdh?1 zy4v2}#xe{bNB{tY6fX$JjvbOEi6Dr=W@&L@DHe}CarUv^-tI&q>C5MaWb-}#C|&Yc9w~NnIGCelx}X0$73wZB7{^`Ef!0)S`A|aA(Umwbx~c_Fh(>* zg+ifZGA>E7Pg?(+xoz7RW4|J|BuRoG_}kIG{Srv&FFN+`@4J3&4FC^N@y`yQ!F$`+ zzT}~4dZ(pLQB-GV$I{aB)vK=;3OSmlo_+S|bW3wvTdOa1{rSx>jQ{IBJu8RO50XO&=G!PnRJp4QUd0>JdQmNE4&pbJNV83M= z5K`fA$Y-fP#eHu0#P+X!lK7d-7>4oN>?&#vcGLjz8RB2~G;W-8YLvNed_|UJhGD|t zFvb`{$Z?$Ch0l46G0*d|EF*+`n}VV!U)%Q=DM^wZOxD{NgZ~_VforU5jb@EiZ%_Kh z0!lv$o;Y{hH2)rWJfM@Gam(N5B*vI!Spa|^se%w92;xDaHfH&*fajgU|3hkk?7Da` zQ{PVV`%=``!y8LUqicUi48t_2=#OESM{w6;cP?s;`@25fbw8kV0000GMUmqO035*Y z|CAlwkH+c-A$^tDpWltmFz{aF+)gRFizJOUAFKCWv}x$-J0G?jyD6|341wd0zbrK?bkj5iqpB*`- literal 0 HcmV?d00001 diff --git a/etc/images/rosie-icon-trim.png b/metomi/rose/etc/images/rosie-icon-trim.png similarity index 100% rename from etc/images/rosie-icon-trim.png rename to metomi/rose/etc/images/rosie-icon-trim.png diff --git a/etc/images/rosie-icon-trim.svg b/metomi/rose/etc/images/rosie-icon-trim.svg similarity index 100% rename from etc/images/rosie-icon-trim.svg rename to metomi/rose/etc/images/rosie-icon-trim.svg diff --git a/etc/images/rosie-icon.png b/metomi/rose/etc/images/rosie-icon.png similarity index 100% rename from etc/images/rosie-icon.png rename to metomi/rose/etc/images/rosie-icon.png diff --git a/etc/images/rosie-icon.svg b/metomi/rose/etc/images/rosie-icon.svg similarity index 100% rename from etc/images/rosie-icon.svg rename to metomi/rose/etc/images/rosie-icon.svg diff --git a/metomi/rose/etc/rose-config-edit/.gtkrc-2.0 b/metomi/rose/etc/rose-config-edit/.gtkrc-2.0 new file mode 100644 index 000000000..f8bd29549 --- /dev/null +++ b/metomi/rose/etc/rose-config-edit/.gtkrc-2.0 @@ -0,0 +1 @@ +gtk-icon-sizes = "gtk-large-toolbar=20,20:gtk-small-toolbar=20,20:panel-menu=16,16:gtk-button=16,16" diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index 7ab2a4f8d..206eac2e2 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -621,11 +621,11 @@ def get_icon(system="rose"): locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate("etc/images/{0}-icon-trim.svg".format(system)) try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)) except Exception: icon_path = locator.locate( "etc/images/{0}-icon-trim.png".format(system)) - pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)) return pixbuf @@ -665,17 +665,14 @@ def rc_setup(rc_resource): def setup_scheduler_icon(ipath=None): """Setup a 'stock' icon for the scheduler""" - new_icon_factory = Gtk.IconFactory() + theme = Gtk.IconTheme.get_default() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) - iname = "rose-gtk-scheduler" if ipath is None: - new_icon_factory.add( - iname, Gtk.icon_factory_lookup_default(Gtk.STOCK_MISSING_IMAGE)) + theme.load_icon("image-missing", 64, 0) else: path = locator.locate(ipath) - pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) - new_icon_factory.add(iname, Gtk.IconSet(pixbuf)) - new_icon_factory.add_default() + pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(path)) + theme.set_icon(pixbuf) def setup_stock_icons(): @@ -691,7 +688,7 @@ def setup_stock_icons(): ifile = png_icon_name + ".png" istring = png_icon_name.replace("_", "-") path = locator.locate("etc/images/rose-config-edit/" + ifile) - pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(path)) new_icon_factory.add("rose-gtk-" + istring, Gtk.IconSet(pixbuf)) exp_icon_pixbuf = get_icon() From 585875948be9cf9fa305bc3671492ad32a80bee2 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 7 Aug 2024 11:42:31 +0100 Subject: [PATCH 06/42] Updates Pango parse_markup and underline usage --- metomi/rose/config_editor/keywidget.py | 14 +++++--------- metomi/rose/config_editor/page.py | 6 ++---- metomi/rose/gtk/dialog.py | 7 ++++--- metomi/rose/gtk/util.py | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index 73a039f36..aee9c107f 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -175,19 +175,17 @@ def set_modified(self, is_modified): att_list = self.entry.get_attributes() if att_list is None: att_list = Pango.AttrList() - att_list.insert(Pango.AttrForeground( + att_list.insert(Pango.attr_foreground_new( self.MODIFIED_COLOUR.red, self.MODIFIED_COLOUR.green, - self.MODIFIED_COLOUR.blue, - start_index=0, - end_index=-1)) + self.MODIFIED_COLOUR.blue)) self.entry.set_attributes(att_list) else: if isinstance(self.entry, Gtk.Label): att_list = self.entry.get_attributes() if att_list is not None: att_list = att_list.filter( - lambda a: a.type != Pango.ATTR_FOREGROUND) + lambda a: a.klass.type != Pango.AttrType.FOREGROUND) if att_list is None: att_list = Pango.AttrList() @@ -442,12 +440,10 @@ def _set_underline(self, label, underline=False): if att_list is None: att_list = Pango.AttrList() if underline: - att_list.insert(Pango.AttrUnderline(Pango.Underline.SINGLE, - start_index=0, - end_index=-1)) + att_list.insert(Pango.attr_underline_new(Pango.Underline.SINGLE)) else: att_list = att_list.filter(lambda a: - a.type != Pango.ATTR_UNDERLINE) + a.klass.type != Pango.AttrType.UNDERLINE) if att_list is None: att_list = Pango.AttrList() label.set_attributes(att_list) diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 0f63a0f11..e1f104393 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -209,9 +209,7 @@ def _handle_enter_label(self, label_event_box, event=None): att_list = label.get_attributes() if att_list is None: att_list = Pango.AttrList() - att_list.insert(Pango.AttrUnderline(Pango.Underline.SINGLE, - start_index=0, - end_index=-1)) + att_list.insert(Pango.attr_underline_new(Pango.Underline.SINGLE)) label.set_attributes(att_list) def _handle_leave_label(self, label_event_box, event=None): @@ -220,7 +218,7 @@ def _handle_leave_label(self, label_event_box, event=None): if att_list is None: att_list = Pango.AttrList() att_list = att_list.filter(lambda a: - a.type != Pango.ATTR_UNDERLINE) + a.klass.type != Pango.AttrType.UNDERLINE) if att_list is None: # This is messy but necessary. att_list = Pango.AttrList() diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 87374378a..6d8d80556 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -354,7 +354,8 @@ def run_dialog(dialog_type, text, title=None, modal=True, dialog.label = Gtk.Label(label=text) try: - Pango.parse_markup(text) + # could just keep set_text and set_markup? + Pango.parse_markup(text, len(text), "0") except GLib.GError: try: dialog.label.set_markup(metomi.rose.gtk.util.safe_str(text)) @@ -488,7 +489,7 @@ def run_scrolled_dialog(text, title=None): scrolled.show() label = Gtk.Label() try: - Pango.parse_markup(text) + Pango.parse_markup(text, len(text), "0") except GLib.GError: label.set_text(text) else: @@ -589,7 +590,7 @@ def run_choices_dialog(text, choices, title=None): dialog.set_border_width(DIALOG_SUB_PADDING) label = Gtk.Label() try: - Pango.parse_markup(text) + Pango.parse_markup(text, len(text), "0") except GLib.GError: label.set_text(text) else: diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index 206eac2e2..94c5f7b71 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -598,7 +598,7 @@ def get_hyperlink_label(text, search_func=lambda i: False): label = Gtk.Label() label.show() try: - Pango.parse_markup(text) + Pango.parse_markup(text, len(text), "0") except GLib.GError: label.set_text(text) else: From df1b1dc6a7dcb96666d754a43d2cb9042d90587a Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 7 Aug 2024 11:43:42 +0100 Subject: [PATCH 07/42] Fixes function calls to GTK3 versions. * Fixes window top level function call * Fixes accelerator connection * Adds empty input arg for visibility functions * Fixes 2 button press usage * Fixes text buffer call * Fixes use of text buffer in TextView * Fixes select_row code * Fixes flags including SENSITIVE * Fixes insert_action_group calls * Fixes calls to _set_cell_* * Fixes page tab names * Adds in 6th arg to BaseSummaryDataPanel function for visibility functions --- metomi/rose/config_editor/main.py | 2 +- metomi/rose/config_editor/menu.py | 4 ++-- metomi/rose/config_editor/menuwidget.py | 2 +- metomi/rose/config_editor/nav_panel.py | 9 +++++---- metomi/rose/config_editor/nav_panel_menu.py | 2 +- metomi/rose/config_editor/page.py | 4 ++-- .../rose/config_editor/panelwidget/filesystem.py | 4 ++-- .../config_editor/panelwidget/summary_data.py | 10 +++++----- .../config_editor/plugin/um/widget/stash_add.py | 2 +- metomi/rose/config_editor/stack.py | 4 ++-- metomi/rose/config_editor/valuewidget/text.py | 2 +- metomi/rose/config_editor/variable.py | 4 ++-- metomi/rose/gtk/choice.py | 15 +++++++-------- metomi/rose/gtk/console.py | 2 +- metomi/rose/gtk/dialog.py | 5 +++-- 15 files changed, 36 insertions(+), 35 deletions(-) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 07a927f4b..db4a9ba4f 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -740,7 +740,7 @@ def handle_launch_request(self, namespace_name, as_new=False): self.notebook.set_current_page(index) if index != -1: self.notebook.remove_page(index + 1) - self.notebook.set_tab_label_packing(page) + self.notebook.set_tab_label_packing(page, page.labelwidget) def make_page(self, namespace_name): """Look up page data and attributes and call a page constructor.""" diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 491fddee8..fc2f30cd9 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -266,7 +266,7 @@ def __init__(self): self.actiongroup = Gtk.ActionGroup('MenuBar') self.actiongroup.add_actions(self.action_details) self.actiongroup.add_toggle_actions(self.toggle_action_details) - self.uimanager.insert_action_group(self.actiongroup, pos=0) + self.uimanager.insert_action_group(self.actiongroup) self.uimanager.add_ui_from_string(self.ui_config_string) self.macro_ids = [] @@ -277,7 +277,7 @@ def set_accelerators(self, accel_dict): for key_press, accel_func in list(accel_dict.items()): key, mod = Gtk.accelerator_parse(key_press) self.accelerators.lookup[str(key) + str(mod)] = accel_func - self.accelerators.connect_group( + self.accelerators.connect( key, mod, Gtk.AccelFlags.VISIBLE, lambda a, c, k, m: self.accelerators.lookup[str(k) + str(m)]()) diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index 5df6dce8f..ef561e266 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -201,7 +201,7 @@ def _popup_option_menu(self, option_ui, actions, button, time): actiongroup.set_translation_domain('') actiongroup.add_actions(actions) uimanager = Gtk.UIManager() - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(option_ui) remove_item = uimanager.get_widget('/Options/Remove') remove_item.connect("activate", diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index 706e0492f..9d9fbd267 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -389,9 +389,10 @@ def select_row(self, row_names): dest_path = (0,) else: i = 1 - while self.tree.row_expanded(path[:i]) and i <= len(path): + # removed the path[:i] in here + while self.tree.row_expanded(path) and i <= len(path): i += 1 - dest_path = path[:i] + dest_path = path cursor_path = self.tree.get_cursor()[0] if cursor_path != dest_path: self.tree.set_cursor(dest_path) @@ -595,7 +596,7 @@ def _get_is_latent_sub_tree(self, model, iter_): iter_stack.append(model.iter_next(iter_)) return True - def _get_should_show(self, model, iter_): + def _get_should_show(self, model, iter_, _): # Determine whether to show a row. latent_status = model.get_value(iter_, self.COLUMN_LATENT_STATUS) ignored_status = model.get_value(iter_, self.COLUMN_IGNORED_STATUS) @@ -606,7 +607,7 @@ def _get_should_show(self, model, iter_): if is_visible: return True while child_iter is not None: - if self._get_should_show(model, child_iter): + if self._get_should_show(model, child_iter, _): return True child_iter = model.iter_next(child_iter) return False diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index cfe7d12cd..e8a7b4134 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -400,7 +400,7 @@ def popup_panel_menu(self, base_ns, event): uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_config_string) if namespace is None or (is_top or is_empty): new_item = uimanager.get_widget('/Popup/New') diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index e1f104393..754cafa82 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -275,7 +275,7 @@ def launch_tab_menu(self, event): uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_config_string_start + ui_config_string_end) if not self.is_detached: @@ -559,7 +559,7 @@ def _add_var_from_item(item): uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(add_ui) if 'Add blank' in add_ui: blank_item = uimanager.get_widget('/Popup/Add blank') diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index 11c6517ce..f807f64dc 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -98,7 +98,7 @@ def _handle_activation(self, view=None, path=None, col=None): def _handle_click(self, view, event): pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) - if (event.button == 1 and event.type == Gdk._2BUTTON_PRESS and + if (event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS and pathinfo is None): self._handle_activation() if event.button == 3: @@ -110,7 +110,7 @@ def _handle_click(self, view, event): uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_string) if pathinfo is None: path = None diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index cc4647144..39e9ebc2f 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -114,7 +114,7 @@ def get_section_column_index(self): """ raise NotImplementedError() - def set_tree_cell_status(self, column, cell, model, row_iter): + def set_tree_cell_status(self, column, cell, model, row_iter, _): """Add status markup to the cell - e.g. error notification. column is the Gtk.TreeColumn where the cell is @@ -345,7 +345,7 @@ def get_status_from_data(self, node_data): def _refilter(self, widget=None): self._view.get_model().get_model().refilter() - def _filter_visible(self, model, iter_): + def _filter_visible(self, model, iter_, _): filt_text = self._filter_widget.get_text() if not filt_text: return True @@ -355,7 +355,7 @@ def _filter_visible(self, model, iter_): return True child_iter = model.iter_children(iter_) while child_iter is not None: - if self._filter_visible(model, child_iter): + if self._filter_visible(model, child_iter, _): return True child_iter = model.iter_next(child_iter) return False @@ -758,7 +758,7 @@ def add_cell_renderer_for_value(self, col, col_title): col.set_cell_data_func(cell_for_value, self._set_tree_cell_value) - def set_tree_cell_status(self, col, cell, model, row_iter): + def set_tree_cell_status(self, col, cell, model, row_iter, _): """Set the status text for a cell in this column.""" col_index = self._view.get_columns().index(col) sect_index = self.get_section_column_index() @@ -806,7 +806,7 @@ def get_model_data(self): column_names += sub_var_names return data_rows, column_names - def _set_tree_cell_value(self, column, cell, treemodel, iter_): + def _set_tree_cell_value(self, column, cell, treemodel, iter_, _): cell.set_property("visible", True) col_index = self._view.get_columns().index(column) value = self._view.get_model().get_value(iter_, col_index) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index c343e3212..e9222958a 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -499,7 +499,7 @@ def _handle_button_press_event(self, treeview, event): if pathinfo is not None: path, col = pathinfo[0:2] if event.button != 3: - if event.type == Gdk._2BUTTON_PRESS: + if event.type == Gdk.EventType._2BUTTON_PRESS: self._handle_activation(treeview, path, col) else: self._popup_tree_menu(path, col, event) diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index 55118cb00..78e6a8bb8 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -90,10 +90,10 @@ def __init__(self, undo_stack, redo_stack, undo_func): self.main_vbox = Gtk.VPaned() accelerators = Gtk.AccelGroup() accel_key, accel_mods = Gtk.accelerator_parse("Z") - accelerators.connect_group(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, + accelerators.connect(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, lambda a, b, c, d: self.undo_from_log()) accel_key, accel_mods = Gtk.accelerator_parse("Z") - accelerators.connect_group(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, + accelerators.connect(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, lambda a, b, c, d: self.undo_from_log(redo_mode_on=True)) self.add_accel_group(accelerators) diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 66f87c2b8..8b126f49d 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -104,7 +104,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entrybuffer = Gtk.TextBuffer() self.entrybuffer.set_text(self.value) - self.entry = Gtk.TextView(self.entrybuffer) + self.entry = Gtk.TextView(buffer=self.entrybuffer) self.entry.set_wrap_mode(Gtk.WrapMode.WORD) self.entry.set_left_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) self.entry.set_right_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index a89012894..5d265646e 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -394,8 +394,8 @@ def grab_focus(self, focus_container=None, scroll_bottom=False, hasattr(self.valuewidget, 'set_focus_index')): self.valuewidget.set_focus_index(index) for child in self.valuewidget.get_children(): - if (Gtk.SENSITIVE & child.flags() and - Gtk.PARENT_SENSITIVE & child.flags()): + if (self.valuewidget.get_sensitive() & child.get_state_flags() and + self.valuewidget.get_parent().get_sensitive() & child.get_state_flags()): break else: if hasattr(self, 'menuwidget'): diff --git a/metomi/rose/gtk/choice.py b/metomi/rose/gtk/choice.py index 569b09a06..0f2d0e2c8 100644 --- a/metomi/rose/gtk/choice.py +++ b/metomi/rose/gtk/choice.py @@ -76,7 +76,7 @@ def __init__(self, set_value, get_data, handle_search, cell_text.set_property('editable', True) cell_text.connect('edited', self._handle_edited) col.pack_start(cell_text, True, True, 0) - col.set_cell_data_func(cell_text, self._set_cell_text) + col.set_cell_data_func(cell_text, self._set_cell_text, None) self.append_column(col) self._populate() @@ -175,7 +175,7 @@ def _popup_menu(self, iter_, event): uimanager = Gtk.UIManager() actiongroup = Gtk.ActionGroup('Popup') actiongroup.add_actions(actions) - uimanager.insert_action_group(actiongroup, pos=0) + uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_config_string) remove_item = uimanager.get_widget('/Popup/Remove') remove_item.connect("activate", @@ -200,7 +200,7 @@ def _remove_iter(self, iter_): self._handle_reordering() self._populate() - def _set_cell_text(self, column, cell, model, r_iter): + def _set_cell_text(self, column, cell, model, r_iter, _): name = model.get_value(r_iter, 0) if name == metomi.rose.config_editor.CHOICE_LABEL_EMPTY: cell.set_property("markup", "" + name + "") @@ -260,14 +260,13 @@ def __init__(self, set_value, get_data, get_available_data, col = Gtk.TreeViewColumn() cell_toggle = Gtk.CellRendererToggle() cell_toggle.connect_after("toggled", self._handle_cell_toggle) - col.pack_start(cell_toggle, False, True, 0) - col.set_cell_data_func(cell_toggle, self._set_cell_state) + col.set_cell_data_func(cell_toggle, self._set_cell_state, None) self.append_column(col) col = Gtk.TreeViewColumn() col.set_title(title) cell_text = Gtk.CellRendererText() col.pack_start(cell_text, True, True, 0) - col.set_cell_data_func(cell_text, self._set_cell_text) + col.set_cell_data_func(cell_text, self._set_cell_text, None) self.append_column(col) self.set_expander_column(col) self.show() @@ -322,7 +321,7 @@ def _realign(self): if model.get_value(iter_, 2) != is_implicit: model.set_value(iter_, 2, is_implicit) - def _set_cell_text(self, column, cell, model, r_iter): + def _set_cell_text(self, column, cell, model, r_iter, _): """Set markup for a section depending on its status.""" section_name = model.get_value(r_iter, 0) is_in_value = model.get_value(r_iter, 1) @@ -343,7 +342,7 @@ def _set_cell_text(self, column, cell, model, r_iter): cell.set_property("markup", section_name) cell.set_property("sensitive", True) - def _set_cell_state(self, column, cell, model, r_iter): + def _set_cell_state(self, column, cell, model, r_iter, _): """Set the check box for a section depending on its status.""" is_in_value = model.get_value(r_iter, 1) is_implicit = model.get_value(r_iter, 2) diff --git a/metomi/rose/gtk/console.py b/metomi/rose/gtk/console.py index 93de4ca28..706a9ea02 100644 --- a/metomi/rose/gtk/console.py +++ b/metomi/rose/gtk/console.py @@ -130,7 +130,7 @@ def _handle_destroy(self, window): if self._destroy_hook is not None: self._destroy_hook() - def _get_should_show(self, model, iter_): + def _get_should_show(self, model, iter_, _): # Determine whether to show a row. category = model.get_value(iter_, 0) if self._filter_category not in [self.CATEGORY_ALL, category]: diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 6d8d80556..7b5a47b56 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -656,7 +656,8 @@ def run_edit_dialog(text, finish_hook=None, title=None): dialog.vbox.pack_start(scrolled_window, expand=True, fill=True, padding=0) get_text = lambda: text_buffer.get_text(text_buffer.get_start_iter(), - text_buffer.get_end_iter()) + text_buffer.get_end_iter(), + False) max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX # defines the minimum acceptable size for the edit dialog @@ -697,7 +698,7 @@ def get_dialog_parent(): """Find the currently active window, if any, and reparent dialog.""" ok_windows = [] max_size = -1 - for window in Gtk.window_list_toplevels(): + for window in Gtk.Window.list_toplevels(): if window.get_title() is not None and window.get_toplevel() == window: ok_windows.append(window) size_proxy = window.get_size()[0] * window.get_size()[1] From 4684b9a0c3a9b9975dda44f085e4c90ce95e69d7 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 19 Aug 2024 15:28:33 +0100 Subject: [PATCH 08/42] Python 2 to 3 fixes * Fixes inspect module calls * Fixes dict key access --- metomi/rose/config_editor/main.py | 2 +- metomi/rose/config_editor/menu.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index db4a9ba4f..3eff1660c 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -1672,7 +1672,7 @@ def get_found_page_and_id(self, expression, start_page): ns_list.sort(ns_cmp) for ns in ns_list: variables = found_ns_vars[ns] - variables.sort(id_cmp) + variables.sort(key=lambda x: x.metadata['id']) for variable in variables: var_id = variable.metadata['id'] if (config_name, var_id) not in self.find_hist['ids']: diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index fc2f30cd9..284ae562e 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -500,8 +500,8 @@ def load_macro_menu(self, menubar): def inspect_custom_macro(self, macro_meth): """Inspect a custom macro for kwargs and return any""" - arglist = inspect.getargspec(macro_meth).args - defaultlist = inspect.getargspec(macro_meth).defaults + arglist = inspect.getfullargspec(macro_meth).args + defaultlist = inspect.getfullargspec(macro_meth).defaults optionals = {} while defaultlist is not None and len(defaultlist) > 0: if arglist[-1] not in ["self", "config", "meta_config"]: From 2685a37f4a61a86c6c7f5b4dac6b116fb171ff7c Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 7 Aug 2024 11:44:44 +0100 Subject: [PATCH 09/42] Updates scrollbar code. Fix array widget bug which forced widgets to scroll off screen when adding an array element --- .../config_editor/valuewidget/array/entry.py | 27 ++++++++++++++++++- .../valuewidget/array/python_list.py | 22 +++++++++++++++ .../valuewidget/array/spaced_list.py | 22 +++++++++++++++ metomi/rose/config_editor/variable.py | 10 +++---- metomi/rose/gtk/dialog.py | 11 +++++--- 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 2c1c487b5..107c684d3 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -96,12 +96,35 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.connect('focus-in-event', lambda w, e: self.hook.get_focus(self.get_focus_entry())) + def force_scroll(self, widget=None): + """Adjusts a scrolled window to display the correct widget.""" + y_coordinate = None + if widget is not None: + y_coordinate = widget.get_allocation().y + scroll_container = widget.get_parent() + if scroll_container is None: + return False + while not isinstance(scroll_container, Gtk.ScrolledWindow): + scroll_container = scroll_container.get_parent() + vadj = scroll_container.get_vadjustment() + if y_coordinate == -1: # Bad allocation, don't scroll + return False + if y_coordinate is None: + vadj.set_upper(vadj.get_upper() + 0.08 * vadj.get_page_size()) + vadj.set_value(vadj.get_upper() - vadj.get_page_size()) + return False + vadj.set_value(y_coordinate) + return False + def get_focus_entry(self): """Get either the last selected entry or the last one.""" if self.last_selected_src is not None: + print("last selected ------------------------") return self.last_selected_src if len(self.entries) > 0: + print("last entry ------------------------") return self.entries[-1] + print("none ------------------------") return None def get_focus_index(self): @@ -346,7 +369,7 @@ def populate_table(self, focus_widget=None): if focus_widget is not None: focus_widget.grab_focus() focus_widget.set_position(position) - focus_widget.select_region(position, position) + focus_widget.select_region(position, -1) self.grab_focus = lambda: self.hook.get_focus( self._get_widget_for_focus()) self.check_resize() @@ -367,8 +390,10 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" entry = self.get_entry('') + entry.connect('focus-in-event', lambda w, e: self.force_scroll(w)) self.entries.append(entry) self._adjust_entry_length() + self.last_selected_src = entry self.populate_table(focus_widget=entry) if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != metomi.rose.META_PROP_VALUE_TRUE): diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 8faf2fab8..0a5a132d5 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -76,6 +76,26 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.connect('focus-in-event', lambda w, e: self.hook.get_focus(self.get_focus_entry())) + def force_scroll(self, widget=None): + """Adjusts a scrolled window to display the correct widget.""" + y_coordinate = None + if widget is not None: + y_coordinate = widget.get_allocation().y + scroll_container = widget.get_parent() + if scroll_container is None: + return False + while not isinstance(scroll_container, Gtk.ScrolledWindow): + scroll_container = scroll_container.get_parent() + vadj = scroll_container.get_vadjustment() + if y_coordinate == -1: # Bad allocation, don't scroll + return False + if y_coordinate is None: + vadj.set_upper(vadj.get_upper() + 0.08 * vadj.get_page_size()) + vadj.set_value(vadj.get_upper() - vadj.get_page_size()) + return False + vadj.set_value(y_coordinate) + return False + def get_focus_entry(self): """Get either the last selected entry or the last one.""" if self.last_selected_src is not None: @@ -336,6 +356,8 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" widget = self.get_entry('') + widget.connect('focus-in-event', lambda w, e: self.force_scroll(w)) + self.last_selected_src = widget self.entries.append(widget) self._adjust_entry_length() self.populate_table(focus_widget=widget) diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index c6dd18bae..d431f789e 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -76,6 +76,26 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.connect('focus-in-event', lambda w, e: self.hook.get_focus(self.get_focus_entry())) + def force_scroll(self, widget=None): + """Adjusts a scrolled window to display the correct widget.""" + y_coordinate = None + if widget is not None: + y_coordinate = widget.get_allocation().y + scroll_container = widget.get_parent() + if scroll_container is None: + return False + while not isinstance(scroll_container, Gtk.ScrolledWindow): + scroll_container = scroll_container.get_parent() + vadj = scroll_container.get_vadjustment() + if y_coordinate == -1: # Bad allocation, don't scroll + return False + if y_coordinate is None: + vadj.set_upper(vadj.get_upper() + 0.08 * vadj.get_page_size()) + vadj.set_value(vadj.get_upper() - vadj.get_page_size()) + return False + vadj.set_value(y_coordinate) + return False + def get_focus_entry(self): """Get either the last selected entry or the last one.""" if self.last_selected_src is not None: @@ -331,6 +351,8 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" entry = self.get_entry('') + entry.connect('focus-in-event', lambda w, e: self.force_scroll(w)) + self.last_selected_src = entry self.entries.append(entry) self._adjust_entry_length() self.populate_table(focus_widget=entry) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 5d265646e..1c4fdb3e4 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -279,7 +279,7 @@ def force_scroll(self, widget=None, container=None): while not isinstance(scroll_container, Gtk.ScrolledWindow): scroll_container = scroll_container.get_parent() vadj = scroll_container.get_vadjustment() - if vadj.upper == 1.0 or y_coordinate == -1: + if vadj.get_upper() == 1.0 or y_coordinate == -1: if not self.force_signal_ids: self.force_signal_ids.append(vadj.connect_after( 'changed', @@ -290,13 +290,13 @@ def force_scroll(self, widget=None, container=None): self.force_signal_ids = [] vadj.connect('changed', metomi.rose.config_editor.false_function) if y_coordinate is None: - vadj.upper = vadj.upper + 0.08 * vadj.page_size - vadj.set_value(vadj.upper - vadj.page_size) + vadj.set_upper(vadj.get_upper() + 0.08 * vadj.get_page_size()) + vadj.set_value(vadj.get_upper() - vadj.get_page_size()) return False if y_coordinate == -1: # Bad allocation, don't scroll return False - if not vadj.value < y_coordinate < vadj.value + 0.95 * vadj.page_size: - vadj.set_value(min(y_coordinate, vadj.upper - vadj.page_size)) + if not vadj.get_value() < y_coordinate < vadj.get_value() + 0.95 * vadj.get_page_size(): + vadj.set_value(min(y_coordinate, vadj.get_upper() - vadj.get_page_size())) return False def remove_from(self, container): diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 7b5a47b56..bac513e2e 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -724,15 +724,18 @@ def _configure_scroll(dialog, scrolled_window): """Set scroll window size and scroll policy.""" # make sure the dialog size doesn't exceed the maximum - if so change it max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX - my_size = dialog.size_request() + my_size = dialog.get_size() new_size = [-1, -1] - for i, scrollbar_cls in [(0, Gtk.VScrollbar), (1, Gtk.HScrollbar)]: - new_size[i] = min([my_size[i], max_size[i]]) + for i, scrollbar_cls in [(0, Gtk.Scrollbar.new(orientation=Gtk.Orientation.VERTICAL)), (1, Gtk.Scrollbar.new(orientation=Gtk.Orientation.HORIZONTAL))]: + new_size[i] = min(my_size[i], max_size[i]) if new_size[i] < max_size[i]: # Factor in existence of a scrollbar in the other dimension. # For horizontal dimension, add width of vertical scroll bar + 2 # For vertical dimension, add height of horizontal scroll bar + 2 - new_size[i] += scrollbar_cls().size_request()[i] + 2 + new_size[i] += getattr( + scrollbar_cls.get_preferred_size().natural_size, + ["width", "height"][i] + ) + 2 scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) dialog.set_default_size(*new_size) From 6bfa30d93e284c939dd9d372e3264b84ca4e6eab Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 7 Aug 2024 12:03:40 +0100 Subject: [PATCH 10/42] Fixes Icons for buttons, windows and Icon IDs * Menu button init fix in gtk/util.py. * Fix window default icon list. * Replaces Dialog Question stock id --- metomi/rose/config_editor/keywidget.py | 2 +- metomi/rose/config_editor/main.py | 2 +- metomi/rose/config_editor/menu.py | 24 ++-- metomi/rose/config_editor/page.py | 11 +- .../config_editor/panelwidget/summary_data.py | 119 ++++++++++++------ .../config_editor/plugin/um/widget/stash.py | 79 +++++++++--- .../plugin/um/widget/stash_add.py | 27 +++- .../rose/config_editor/valuewidget/source.py | 12 +- metomi/rose/config_editor/window.py | 2 +- metomi/rose/gtk/dialog.py | 2 +- metomi/rose/gtk/splash.py | 2 +- metomi/rose/gtk/util.py | 19 ++- 12 files changed, 215 insertions(+), 86 deletions(-) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index aee9c107f..a6bb4e5d6 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -41,7 +41,7 @@ class KeyWidget(Gtk.VBox): metomi.rose.config_editor.FLAG_TYPE_FIXED: Gtk.STOCK_DIALOG_AUTHENTICATION, metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: Gtk.STOCK_INDEX, metomi.rose.config_editor.FLAG_TYPE_OPTIONAL: Gtk.STOCK_ABOUT, - metomi.rose.config_editor.FLAG_TYPE_NO_META: Gtk.STOCK_DIALOG_QUESTION, + metomi.rose.config_editor.FLAG_TYPE_NO_META: "dialog-question", } MODIFIED_COLOUR = metomi.rose.gtk.util.color_parse( diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 3eff1660c..dfb968fa5 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -346,7 +346,7 @@ def generate_toolbar(self): (metomi.rose.config_editor.TOOLBAR_FIND, 'Gtk.Entry'), (metomi.rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), (metomi.rose.config_editor.TOOLBAR_VALIDATE, - 'Gtk.STOCK_DIALOG_QUESTION'), + "dialog-question"), (metomi.rose.config_editor.TOOLBAR_TRANSFORM, 'Gtk.STOCK_CONVERT'), (metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 284ae562e..a9b1e915d 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -194,11 +194,11 @@ class MenuBar(object): metomi.rose.config_editor.TOP_MENU_METADATA_PREFERENCES), ('Upgrade', Gtk.STOCK_GO_UP, metomi.rose.config_editor.TOP_MENU_METADATA_UPGRADE), - ('All V', Gtk.STOCK_DIALOG_QUESTION, + ('All V', "dialog-question", metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V), ('Autofix', Gtk.STOCK_CONVERT, metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX), - ('Extra checks', Gtk.STOCK_DIALOG_QUESTION, + ('Extra checks', "dialog-question", metomi.rose.config_editor.TOP_MENU_METADATA_CHECK), ('Graph', Gtk.STOCK_SORT_ASCENDING, metomi.rose.config_editor.TOP_MENU_METADATA_GRAPH), @@ -317,7 +317,7 @@ def add_macro(self, config_name, modulename, classname, methodname, self.macro_ids.append(self.uimanager.add_ui_from_string(new_ui)) config_item = self.uimanager.get_widget(config_address) if image_path is not None: - image = Gtk.image_new_from_file(image_path) + image = Gtk.Image.new_from_file(image_path) config_item.set_image(image) if config_item.get_submenu() is None: config_item.set_submenu(Gtk.Menu()) @@ -327,8 +327,13 @@ def add_macro(self, config_name, modulename, classname, methodname, stock_id = Gtk.STOCK_DIALOG_QUESTION else: stock_id = Gtk.STOCK_CONVERT - macro_item = Gtk.ImageMenuItem(stock_id=stock_id) - macro_item.set_label(macro_fullname) + macro_item_box = Gtk.Box() + macro_item_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) + macro_item_label = Gtk.Label(label=macro_fullname) + macro_item = Gtk.MenuItem() + macro_item_box.pack_start(macro_item_icon, False, False, 0) + macro_item_box.pack_start(macro_item_label, False, False, 0) + Gtk.Container.add(macro_item, macro_item_box) macro_item.set_tooltip_text(help_) macro_item.show() macro_item._run_data = [config_name, modulename, classname, @@ -340,9 +345,14 @@ def add_macro(self, config_name, modulename, classname, methodname, for item in config_item.get_submenu().get_children(): if hasattr(item, "_rose_all_validators"): return False - all_item = Gtk.ImageMenuItem(Gtk.STOCK_DIALOG_QUESTION) + all_item_box = Gtk.Box() + all_item_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.MENU) + all_item_label = Gtk.Label(label=metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) + all_item = Gtk.MenuItem() + all_item_box.pack_start(all_item_icon, False, False, 0) + all_item_box.pack_start(all_item, False, False, 0) + Gtk.Container.add(macro_item, macro_item_box) all_item._rose_all_validators = True - all_item.set_label(metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) all_item.set_tooltip_text( metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) all_item.show() diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 754cafa82..07cd465ce 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -1056,9 +1056,14 @@ def _macro_menu_launch(self, widget, event): if method == metomi.rose.macro.TRANSFORM_METHOD: stock_id = Gtk.STOCK_CONVERT else: - stock_id = Gtk.STOCK_DIALOG_QUESTION - macro_menuitem = Gtk.ImageMenuItem(stock_id=stock_id) - macro_menuitem.set_label(macro_name) + stock_id = "dialog-question" + macro_menuitem_box = Gtk.Box() + macro_menuitem_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) + macro_menuitem_label = Gtk.Label(label=macro_name) + macro_menuitem = Gtk.MenuItem() + macro_menuitem_box.pack_start(macro_menuitem_icon, False, False, 0) + macro_menuitem_box.pack_start(macro_menuitem_label, False, False, 0) + Gtk.Container.add(macro_menuitem, macro_menuitem_box) macro_menuitem.set_tooltip_text(description) macro_menuitem.show() macro_menuitem._macro = macro_name diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index 39e9ebc2f..d2c2c1859 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -433,27 +433,39 @@ def _popup_tree_multi_menu(self, event): shortcuts = [] # Ignore all. - ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) - ign_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) + ign_menuitem_box = Gtk.Box() + ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) + ign_menuitem = Gtk.MenuItem() + ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) + ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect("activate", self._ignore_selected_sections, True) ign_menuitem.show() menu.append(ign_menuitem) shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Enable all. - ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) - ign_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) + ign_menuitem_box = Gtk.Box() + ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) + ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) + ign_menuitem = Gtk.MenuItem() + ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) + ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect("activate", self._ignore_selected_sections, False) ign_menuitem.show() menu.append(ign_menuitem) shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Remove all. - rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) - rem_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) + rem_menuitem_box = Gtk.Box() + rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) + rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) + rem_menuitem = Gtk.MenuItem() + rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) + rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) + Gtk.Container.add(rem_menuitem, rem_menuitem_box) rem_menuitem.connect("activate", self._remove_selected_sections) rem_menuitem.show() menu.append(rem_menuitem) @@ -486,10 +498,15 @@ def _popup_tree_menu(self, path, col, event): this_section = model.get_value(row_iter, sect_index) if this_section is not None: # Jump to section. - menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_JUMP_TO) label = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( this_section.replace("_", "__")) - menuitem.set_label(label) + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label=label) + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) menuitem._section = this_section menuitem.connect("activate", lambda i: self.search_function(i._section)) @@ -510,17 +527,25 @@ def _popup_tree_menu(self, path, col, event): # A section is currently selected if this_section is not None: # Add section. - add_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) - add_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) + add_menuitem_box = Gtk.Box() + add_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) + add_menuitem = Gtk.MenuItem() + add_menuitem_box.pack_start(add_menuitem_icon, False, False, 0) + add_menuitem_box.pack_start(add_menuitem_label, False, False, 0) + Gtk.Container.add(add_menuitem, add_menuitem_box) add_menuitem.connect("activate", lambda i: self.add_section()) add_menuitem.show() menu.append(add_menuitem) # Copy section. - copy_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_COPY) - copy_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) + copy_menuitem_box = Gtk.Box() + copy_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_COPY, Gtk.IconSize.MENU) + copy_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) + copy_menuitem = Gtk.MenuItem() + copy_menuitem_box.pack_start(copy_menuitem_icon, False, False, 0) + copy_menuitem_box.pack_start(copy_menuitem_label, False, False, 0) + Gtk.Container.add(copy_menuitem, copy_menuitem_box) copy_menuitem.connect( "activate", lambda i: self.copy_section(this_section)) copy_menuitem.show() @@ -528,9 +553,13 @@ def _popup_tree_menu(self, path, col, event): if (metomi.rose.variable.IGNORED_BY_USER in self.sections[this_section].ignored_reason): # Enable section. - enab_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) - enab_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + enab_menuitem_box = Gtk.Box() + enab_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) + enab_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + enab_menuitem = Gtk.MenuItem() + enab_menuitem_box.pack_start(enab_menuitem_icon, False, False, 0) + enab_menuitem_box.pack_start(enab_menuitem_label, False, False, 0) + Gtk.Container.add(enab_menuitem, enab_menuitem_box) enab_menuitem.connect( "activate", lambda i: self.sub_ops.ignore_section(this_section, @@ -541,9 +570,13 @@ def _popup_tree_menu(self, path, col, event): enab_menuitem)) else: # Ignore section. - ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) - ign_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem_box = Gtk.Box() + ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem = Gtk.MenuItem() + ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) + ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect( "activate", lambda i: self.sub_ops.ignore_section(this_section, @@ -553,9 +586,13 @@ def _popup_tree_menu(self, path, col, event): shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Remove section. - rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) - rem_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem_box = Gtk.Box() + rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) + rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem = Gtk.MenuItem() + rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) + rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) + Gtk.Container.add(rem_menuitem, rem_menuitem_box) rem_menuitem.connect( "activate", lambda i: self.remove_section(this_section)) rem_menuitem.show() @@ -564,9 +601,13 @@ def _popup_tree_menu(self, path, col, event): rem_menuitem)) else: # A group is currently selected. # Ignore all - ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) - ign_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem_box = Gtk.Box() + ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem = Gtk.MenuItem() + ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) + ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect("activate", self._ignore_selected_sections, True) @@ -575,9 +616,13 @@ def _popup_tree_menu(self, path, col, event): shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Enable all - ign_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) - ign_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + ign_menuitem_box = Gtk.Box() + ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) + ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + ign_menuitem = Gtk.MenuItem() + ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) + ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect("activate", self._ignore_selected_sections, False) @@ -586,9 +631,13 @@ def _popup_tree_menu(self, path, col, event): shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem)) # Delete all. - rem_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) - rem_menuitem.set_label( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem_box = Gtk.Box() + rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) + rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem = Gtk.MenuItem() + rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) + rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) + Gtk.Container.add(rem_menuitem, rem_menuitem_box) rem_menuitem.connect( "activate", self._remove_selected_sections) rem_menuitem.show() diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index ae2458e19..bd662fae0 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -406,9 +406,14 @@ def _get_custom_menu_items(self, path, col, event): meta_key = self.STASH_PARSE_DESC_OPT + "=" + str(value) metadata = self._stashmaster_meta_lookup.get(meta_key, {}) help_ = metadata.get(metomi.rose.META_PROP_HELP) - if help_ is not None: - menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) - menuitem.set_label(label="Help") + if help_ is not None: + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_HELP, Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label="Help") + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) menuitem._help_text = help_ menuitem._help_title = "Help for %s" % value menuitem.connect("activate", self._launch_record_help) @@ -418,8 +423,13 @@ def _get_custom_menu_items(self, path, col, event): if value not in self._profile_location_map[col_title]: return [] location = self._profile_location_map[col_title][value] - menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ABOUT) - menuitem.set_label(label="View " + value.strip("'")) + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label="View " + value.strip("'")) + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) menuitem._loc_id = location menuitem.connect("activate", lambda i: self.search_function(i._loc_id)) @@ -437,9 +447,13 @@ def _get_custom_menu_items(self, path, col, event): if profiles_menuitems: profiles_menu = Gtk.Menu() profiles_menu.show() - profiles_root_menuitem = Gtk.ImageMenuItem( - stock_id=Gtk.STOCK_ABOUT) - profiles_root_menuitem.set_label("View...") + profiles_root_menuitem_box = Gtk.Box() + profiles_root_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU) + profiles_root_menuitem_label = Gtk.Label(label="View...") + profiles_root_menuitem = Gtk.MenuItem() + profiles_root_menuitem_box.pack_start(profiles_root_menuitem_icon, False, False, 0) + profiles_root_menuitem_box.pack_start(profiles_root_menuitem_label, False, False, 0) + Gtk.Container.add(profiles_root_menuitem, profiles_root_menuitem_box) profiles_root_menuitem.show() profiles_root_menuitem.set_submenu(profiles_menu) for profiles_menuitem in profiles_menuitems: @@ -764,8 +778,13 @@ def _package_menu_launch(self, widget, event): package_menuitem = Gtk.MenuItem(package_title) package_menuitem.show() package_menu = Gtk.Menu() - enable_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_YES) - enable_menuitem.set_label(label="Enable all") + enable_menuitem_box = Gtk.Box() + enable_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) + enable_menuitem_label = Gtk.Label(label="Enable all") + enable_menuitem = Gtk.MenuItem() + enable_menuitem_box.pack_start(enable_menuitem_icon, False, False, 0) + enable_menuitem_box.pack_start(enable_menuitem_label, False, False, 0) + Gtk.Container.add(enable_menuitem, enable_menuitem_box) enable_menuitem._connect_args = (package, False) enable_menuitem.connect( "button-release-event", @@ -773,17 +792,27 @@ def _package_menu_launch(self, widget, event): enable_menuitem.show() enable_menuitem.set_sensitive(any(ignored_list)) package_menu.append(enable_menuitem) - ignore_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) - ignore_menuitem.set_label(label="Ignore all") + ignore_menuitem_box = Gtk.Box() + ignore_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + ignore_menuitem_label = Gtk.Label(label="Ignore all") + ignore_menuitem = Gtk.MenuItem() + ignore_menuitem_box.pack_start(ignore_menuitem_icon, False, False, 0) + ignore_menuitem_box.pack_start(ignore_menuitem_label, False, False, 0) + Gtk.Container.add(ignore_menuitem, ignore_menuitem_box) ignore_menuitem._connect_args = (package, True) ignore_menuitem.connect( "button-release-event", lambda m, e: self._packages_enable(*m._connect_args)) ignore_menuitem.set_sensitive(any(not i for i in ignored_list)) ignore_menuitem.show() - package_menu.append(ignore_menuitem) - remove_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_REMOVE) - remove_menuitem.set_label(label="Remove all") + package_menu.append(ignore_menuitem) + remove_menuitem_box = Gtk.Box() + remove_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) + remove_menuitem_label = Gtk.Label(label="Remove all") + remove_menuitem = Gtk.MenuItem() + remove_menuitem_box.pack_start(remove_menuitem_icon, False, False, 0) + remove_menuitem_box.pack_start(remove_menuitem_label, False, False, 0) + Gtk.Container.add(remove_menuitem, remove_menuitem_box) remove_menuitem._connect_args = (package,) remove_menuitem.connect( "button-release-event", @@ -791,9 +820,14 @@ def _package_menu_launch(self, widget, event): remove_menuitem.show() package_menu.append(remove_menuitem) package_menuitem.set_submenu(package_menu) - menu.append(package_menuitem) - menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) - menuitem.set_label(label="Import") + menu.append(package_menuitem) + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label="Import") + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) import_menu = Gtk.Menu() new_packages = set(self._package_lookup.keys()) - set(packages.keys()) for new_package in sorted(new_packages): @@ -809,8 +843,13 @@ def _package_menu_launch(self, widget, event): menuitem.set_submenu(import_menu) menuitem.show() menu.append(menuitem) - menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_NO) - menuitem.set_label(label="Disable all packages") + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label="Disable all packages") + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) menuitem.connect("activate", lambda i: self._packages_enable(disable=True)) menuitem.show() diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index e9222958a..f5d1b262d 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -538,8 +538,13 @@ def _popup_tree_menu(self, path, col, event): section, item = self._get_section_item_from_iter(row_iter) if section is None or item is None: return False - add_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_ADD) - add_menuitem.set_label("Add STASH request") + add_menuitem_box = Gtk.Box() + add_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_menuitem_label = Gtk.Label(label="Add STASH request") + add_menuitem = Gtk.MenuItem() + add_menuitem_box.pack_start(add_menuitem_icon, False, False, 0) + add_menuitem_box.pack_start(add_menuitem_label, False, False, 0) + Gtk.Container.add(add_menuitem, add_menuitem_box) add_menuitem.connect("activate", lambda i: self.add_stash_request(section, item)) add_menuitem.show() @@ -550,8 +555,13 @@ def _popup_tree_menu(self, path, col, event): self.STASH_PARSE_DESC_OPT + "=" + str(stash_desc_value), {}) desc_meta_help = desc_meta.get(metomi.rose.META_PROP_HELP) if desc_meta_help is not None: - help_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_HELP) - help_menuitem.set_label("Help") + help_menuitem_box = Gtk.Box() + help_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_HELP, Gtk.IconSize.MENU) + help_menuitem_label = Gtk.Label(label="Help") + help_menuitem = Gtk.MenuItem() + help_menuitem_box.pack_start(help_menuitem_icon, False, False, 0) + help_menuitem_box.pack_start(help_menuitem_label, False, False, 0) + Gtk.Container.add(help_menuitem, help_menuitem_box) help_menuitem._help_text = desc_meta_help help_menuitem._help_title = "Help for %s" % stash_desc_value help_menuitem.connect("activate", self._launch_record_help) @@ -559,8 +569,13 @@ def _popup_tree_menu(self, path, col, event): menu.append(help_menuitem) streqs = list(self.request_lookup.get(section, {}).get(item, {}).keys()) if streqs: - view_menuitem = Gtk.ImageMenuItem(stock_id=Gtk.STOCK_FIND) - view_menuitem.set_label(label="View...") + view_menuitem_box = Gtk.Box() + view_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_FIND, Gtk.IconSize.MENU) + view_menuitem_label = Gtk.Label(label="View...") + view_menuitem = Gtk.MenuItem() + view_menuitem_box.pack_start(view_menuitem_icon, False, False, 0) + view_menuitem_box.pack_start(view_menuitem_label, False, False, 0) + Gtk.Container.add(view_menuitem, view_menuitem_box) view_menuitem.show() view_menu = Gtk.Menu() view_menu.show() diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py index 02848ad99..f9679367e 100644 --- a/metomi/rose/config_editor/valuewidget/source.py +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -137,11 +137,13 @@ def _generate_available_treeview(self): def _get_custom_menu_items(self): """Return some custom menuitems for use in the list view.""" - menuitem = Gtk.ImageMenuItem( - metomi.rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) - image = Gtk.Image.new_from_stock( - Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.MENU) - menuitem.set_image(image) + menuitem_box = Gtk.Box() + menuitem_icon = Gtk.Image.new_from_icon_name("dialog-question", Gtk.IconSize.MENU) + menuitem_label = Gtk.Label(label=metomi.rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) + menuitem = Gtk.MenuItem() + menuitem_box.pack_start(menuitem_icon, False, False, 0) + menuitem_box.pack_start(menuitem_label, False, False, 0) + Gtk.Container.add(menuitem, menuitem_box) menuitem.connect( "button-press-event", self._toggle_menu_optional_status) menuitem.show() diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index df5a160db..67bc857f1 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -103,7 +103,7 @@ def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, metomi.rose.config_editor.LAUNCH_COMMAND) self.util = metomi.rose.config_editor.util.Lookup() self.window.set_icon(metomi.rose.gtk.util.get_icon()) - Gtk.window_set_default_icon_list(self.window.get_icon()) + Gtk.Window.set_default_icon_list([self.window.get_icon()]) self.window.set_default_size(*metomi.rose.config_editor.SIZE_WINDOW) self.window.set_destroy_with_parent(False) self.save_func = save_func diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index bac513e2e..74f7ce902 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -342,7 +342,7 @@ def run_dialog(dialog_type, text, title=None, modal=True, elif dialog_type == Gtk.MessageType.WARNING: stock_id = Gtk.STOCK_DIALOG_WARNING elif dialog_type == Gtk.MessageType.QUESTION: - stock_id = Gtk.STOCK_DIALOG_QUESTION + stock_id = "dialog-question" elif dialog_type == Gtk.MessageType.ERROR: stock_id = Gtk.STOCK_DIALOG_ERROR else: diff --git a/metomi/rose/gtk/splash.py b/metomi/rose/gtk/splash.py index 377ec8921..839839dff 100755 --- a/metomi/rose/gtk/splash.py +++ b/metomi/rose/gtk/splash.py @@ -64,7 +64,7 @@ def __init__(self, logo_path, title, total_number_of_events): self.set_position(Gtk.WindowPosition.CENTER) main_vbox = Gtk.VBox() main_vbox.show() - image = Gtk.image_new_from_file(logo_path) + image = Gtk.Image.new_from_file(logo_path) image.show() image_hbox = Gtk.HBox() image_hbox.show() diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index 94c5f7b71..e3ec7a0eb 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -81,14 +81,18 @@ def __init__(self, label=None, stock_id=None, if stock_id is not None: self.stock_id = stock_id self.icon = Gtk.Image() - self.icon.set_from_stock(stock_id, size) + if stock_id.startswith("gtk") or stock_id.startswith("rose-gtk"): + self.icon.set_from_stock(stock_id, size) + else: + self.icon.set_from_icon_name(stock_id, size) self.icon.show() if self.icon_at_start: self.hbox.pack_start(self.icon, expand=False, fill=False) else: self.hbox.pack_end(self.icon, expand=False, fill=False) if has_menu: - arrow = Gtk.Arrow(Gtk.ArrowType.DOWN, Gtk.ShadowType.NONE) + # not sure if this is correct + arrow = Gtk.Image.new_from_icon_name("pan-down-symbolic", size) arrow.show() self.hbox.pack_end(arrow, expand=False, fill=False) self.hbox.reorder_child(arrow, 0) @@ -228,7 +232,7 @@ def __init__(self, label=None, stock_id=None, self.icon = Gtk.Image() self.icon.set_from_stock(stock_id, size) self.icon.show() - GObject.GObject.__init__(self, self.icon, label) + super().__init__(self, self.icon, label) self.set_tooltip_text(tip_text) self.show() button_menu = Gtk.Menu() @@ -237,8 +241,13 @@ def __init__(self, label=None, stock_id=None, if len(item_tuple) == 1: new_item = Gtk.MenuItem(name) else: - new_item = Gtk.ImageMenuItem(stock_id=item_tuple[1]) - new_item.set_label(name) + new_item_box = Gtk.Box() + new_item_icon = Gtk.Image.new_from_icon_name(item_tuple[1], Gtk.IconSize.MENU) + new_item_label = Gtk.Label(label=name) + new_item = Gtk.MenuItem() + new_item_box.pack_start(new_item_icon, False, False, 0) + new_item_box.pack_start(new_item_label, False, False, 0) + Gtk.Container.add(new_item, new_item_box) new_item._func = func new_item.connect("activate", lambda m: m._func()) new_item.show() From 58a5f98bacddc4fd97f9377b73ea649794a2b72b Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Thu, 8 Aug 2024 10:36:30 +0100 Subject: [PATCH 11/42] Fixes the Rose Gui splash screen --- metomi/rose/config_editor/main.py | 5 --- metomi/rose/config_editor/nav_panel_menu.py | 1 - metomi/rose/config_editor/status.py | 3 +- metomi/rose/gtk/splash.py | 39 ++++++++------------- metomi/rose/macro.py | 9 +++-- metomi/rose/reporter.py | 4 +-- setup.cfg | 1 + 7 files changed, 23 insertions(+), 39 deletions(-) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index dfb968fa5..534ed491a 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -24,7 +24,6 @@ MainController - driver for loading and central coordination. """ - import cProfile import os import pstats @@ -274,12 +273,10 @@ def __init__(self, config_directory=None, config_objs=None, config_obj_type_dict=config_obj_types, load_all_apps=load_all_apps, load_no_apps=load_no_apps) - self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_STATUSES.format( self.data.top_level_name) ) - if not self.is_pluggable: self.generate_toolbar() self.generate_menubar() @@ -287,9 +284,7 @@ def __init__(self, config_directory=None, config_objs=None, self.generate_status_bar() # Create notebook (tabbed container) and connect signals. self.notebook = metomi.rose.gtk.util.Notebook() - self.updater.nav_panel = getattr(self, "nav_panel", None) - # Create the main panel with the menu, toolbar, tree panel, notebook. if not self.is_pluggable: self.mainwindow.load(name=self.data.top_level_name, diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index e8a7b4134..53d061a30 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -94,7 +94,6 @@ def ask_is_preview(self, base_ns): config_data = self.data.config[config_name] return config_data.is_preview except KeyError: - print(config_name) return False def copy_request(self, base_ns, new_section=None, skip_update=False): diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index b62f81a89..6700742cf 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -63,7 +63,8 @@ def event_handler(self, message, kind=None, level=None, prefix=None, level = message.level message_kwargs = message.kwargs if kind == self.EVENT_KIND_LOAD and not self._no_load: - return self._load_updater.update(str(message), **message_kwargs) + ret = self._load_updater.update(str(message), **message_kwargs) + return ret return self._status_bar_update_func(message, kind, level) def report_load_event( diff --git a/metomi/rose/gtk/splash.py b/metomi/rose/gtk/splash.py index 839839dff..c3bb5ee9d 100755 --- a/metomi/rose/gtk/splash.py +++ b/metomi/rose/gtk/splash.py @@ -36,8 +36,6 @@ import metomi.rose.gtk.util import metomi.rose.popen -GObject.threads_init() - class SplashScreen(Gtk.Window): @@ -60,7 +58,7 @@ def __init__(self, logo_path, title, total_number_of_events): self.set_icon(metomi.rose.gtk.util.get_icon()) self.modify_bg(Gtk.StateType.NORMAL, metomi.rose.gtk.util.color_parse(self.BACKGROUND_COLOUR)) - self.set_gravity(Gdk.GRAVITY_CENTER) + self.set_gravity(5) # same as gravity center self.set_position(Gtk.WindowPosition.CENTER) main_vbox = Gtk.VBox() main_vbox.show() @@ -68,8 +66,8 @@ def __init__(self, logo_path, title, total_number_of_events): image.show() image_hbox = Gtk.HBox() image_hbox.show() - image_hbox.pack_start(image, expand=False, fill=True) - main_vbox.pack_start(image_hbox, expand=False, fill=True) + image_hbox.pack_start(image, expand=False, fill=True, padding=0) + main_vbox.pack_start(image_hbox, expand=False, fill=True, padding=0) self._is_progress_bar_pulsing = False self._progress_fraction = 0.0 self.progress_bar = Gtk.ProgressBar() @@ -108,19 +106,15 @@ def update(self, event, no_progress=False, new_total_events=None): fraction = min( [1.0, self.event_count / self.total_number_of_events]) self._stop_pulse() - if not no_progress: GObject.idle_add(self.progress_bar.set_fraction, fraction) self._progress_fraction = fraction - self.progress_bar.set_text(text) self._progress_message = text GObject.timeout_add(self.TIME_IDLE_BEFORE_PULSE, self._start_pulse, fraction, text) - if fraction == 1.0 and not no_progress: GObject.timeout_add(self.TIME_WAIT_FINISH, self.finish) - while Gtk.events_pending(): Gtk.main_iteration() @@ -200,10 +194,10 @@ def update(self, *args, **kwargs): def _communicate(self, json_text): while True: try: - self.process.stdin.write(json_text + "\n") + self.process.stdin.write((json_text + "\n").encode()) except IOError: self.start() - self.process.stdin.write(json_text + "\n") + self.process.stdin.write((json_text + "\n").encode()) else: break @@ -225,15 +219,10 @@ def _update_buffered(self, *args, **kwargs): __call__ = update def start(self): - file_name = __file__.rsplit(".", 1)[0] + ".py" - self.process = Popen([file_name] + list(self.args), stdin=PIPE) + self.process = Popen(["rose", "launch-splash-screen"] + list(self.args), stdin=PIPE) def stop(self): - if self.process is not None and not self.process.stdin.closed: - try: - self.process.communicate(input=json.dumps("stop") + "\n") - except IOError: - pass + self.process.kill() self.process = None @@ -256,6 +245,8 @@ def run(self): return False try: stdin_line = self.stdin.readline() + if not stdin_line: + continue except IOError: continue try: @@ -263,12 +254,11 @@ def run(self): except ValueError: continue if update_input == "stop": - self._stop() + self.stop_event.set() continue GObject.idle_add(self._update_splash_screen, update_input) - - def _stop(self): - self.stop_event.set() + + def stop(self): try: Gtk.main_quit() except RuntimeError: @@ -288,10 +278,10 @@ def _update_splash_screen(self, update_input): return False -def main(): +def main(argv=sys.argv): """Start splash screen.""" sys.path.append(os.getenv('ROSE_HOME')) - splash_screen = SplashScreen(*sys.argv[1:]) + splash_screen = SplashScreen(argv[0], argv[1], argv[2]) stop_event = threading.Event() update_thread = SplashScreenUpdaterThread( splash_screen, stop_event, sys.stdin) @@ -301,7 +291,6 @@ def main(): except KeyboardInterrupt: pass finally: - stop_event.set() update_thread.join() diff --git a/metomi/rose/macro.py b/metomi/rose/macro.py index 9cd8722e1..a09d5bd40 100644 --- a/metomi/rose/macro.py +++ b/metomi/rose/macro.py @@ -399,11 +399,10 @@ def _get_config_sections(self, config_data): if "" not in sections: sections.append("") else: - for key in set( - config_data["sections"].keys() - + config_data["variables"].keys() - ): - sections.append(key) + sections = list(set( + list(config_data["sections"].keys()) + + list(config_data["variables"].keys()) + )) return sections def _get_config_section_options(self, config_data, section): diff --git a/metomi/rose/reporter.py b/metomi/rose/reporter.py index a2a4df8c9..44dc37e3b 100644 --- a/metomi/rose/reporter.py +++ b/metomi/rose/reporter.py @@ -138,8 +138,8 @@ def report(self, message, kind=None, level=None, prefix=None, clip=None): if isinstance(message, bytes): message = message.decode() if callable(self.event_handler): - return self.event_handler(message, kind, level, prefix, clip) - + ret = self.event_handler(message, kind, level, prefix, clip) + return ret if isinstance(message, Event): if kind is None: kind = message.kind diff --git a/setup.cfg b/setup.cfg index 04c368c33..5d6219844 100644 --- a/setup.cfg +++ b/setup.cfg @@ -115,6 +115,7 @@ rose.commands = env-cat = metomi.rose.env_cat:main host-select = metomi.rose.host_select:main host-select-client = metomi.rose.host_select_client:main + launch-splash-screen = metomi.rose.gtk.splash:main macro = metomi.rose.macro:main metadata-check = metomi.rose.metadata_check:main metadata-gen = metomi.rose.metadata_gen:main From 9a33c93cf84df7cf5226f7624326a876fa023221 Mon Sep 17 00:00:00 2001 From: J-J-Abram Date: Mon, 12 Aug 2024 11:45:00 +0100 Subject: [PATCH 12/42] Gtk.VBox changed to GtkBox(...VERTICAL), Gtk.HBox changed to GtkBox(...HORIZONTAL), and padding option added --- metomi/rose/config_editor/keywidget.py | 20 +++--- metomi/rose/config_editor/menu.py | 4 +- metomi/rose/config_editor/menuwidget.py | 2 +- metomi/rose/config_editor/page.py | 54 +++++++------- .../config_editor/panelwidget/summary_data.py | 12 ++-- .../config_editor/plugin/um/widget/stash.py | 4 +- .../plugin/um/widget/stash_add.py | 18 ++--- metomi/rose/config_editor/stack.py | 4 +- metomi/rose/config_editor/status.py | 10 +-- .../rose/config_editor/upgrade_controller.py | 4 +- .../config_editor/valuewidget/array/entry.py | 10 +-- .../valuewidget/array/logical.py | 6 +- .../config_editor/valuewidget/array/mixed.py | 6 +- .../valuewidget/array/python_list.py | 10 +-- .../config_editor/valuewidget/array/row.py | 6 +- .../valuewidget/array/spaced_list.py | 10 +-- .../config_editor/valuewidget/booltoggle.py | 2 +- .../config_editor/valuewidget/character.py | 2 +- .../rose/config_editor/valuewidget/choice.py | 16 ++--- .../config_editor/valuewidget/combobox.py | 2 +- .../rose/config_editor/valuewidget/files.py | 4 +- .../rose/config_editor/valuewidget/format.py | 4 +- .../rose/config_editor/valuewidget/intspin.py | 2 +- metomi/rose/config_editor/valuewidget/meta.py | 2 +- .../config_editor/valuewidget/radiobuttons.py | 4 +- .../rose/config_editor/valuewidget/source.py | 20 +++--- metomi/rose/config_editor/valuewidget/text.py | 4 +- .../config_editor/valuewidget/valuehints.py | 2 +- metomi/rose/config_editor/variable.py | 16 ++--- metomi/rose/config_editor/window.py | 16 ++--- .../widget/meta/lib/python/widget/username.py | 2 +- metomi/rose/gtk/console.py | 10 +-- metomi/rose/gtk/dialog.py | 70 +++++++++---------- metomi/rose/gtk/splash.py | 6 +- metomi/rose/gtk/util.py | 14 ++-- 35 files changed, 189 insertions(+), 189 deletions(-) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index a6bb4e5d6..b596fb2ff 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -31,7 +31,7 @@ import metomi.rose.variable -class KeyWidget(Gtk.VBox): +class KeyWidget(Gtk.Box): """This class generates a label or entry box for a variable name.""" @@ -51,9 +51,9 @@ class KeyWidget(Gtk.VBox): def __init__(self, variable, var_ops, launch_help_func, update_func, show_modes): - super(KeyWidget, self).__init__(homogeneous=False, spacing=0) + super(KeyWidget, self).__init__(homogeneous=False, spacing=0, orientation=Gtk.Orientation.VERTICAL) self.my_variable = variable - self.hbox = Gtk.HBox() + self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox.show() self.pack_start(self.hbox, expand=False, fill=False) self.var_ops = var_ops @@ -65,7 +65,7 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, self._last_var_comments = None self.ignored_label = Gtk.Label() self.ignored_label.show() - self.hbox.pack_start(self.ignored_label, expand=False, fill=False) + self.hbox.pack_start(self.ignored_label, expand=False, fill=False, padding=0) self.set_ignored() if self.my_variable.name != '': self.entry = Gtk.Label() @@ -87,7 +87,7 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, lambda b, w: self._handle_leave(b)) self.hbox.pack_start(event_box, expand=True, fill=True, padding=0) - self.comments_box = Gtk.HBox() + self.comments_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox.pack_start(self.comments_box, expand=False, fill=False) self.grab_focus = self.entry.grab_focus self.set_sensitive(True) @@ -319,9 +319,9 @@ def _set_show_meta_text_mode(self, mode, should_show_mode): mode_text = metomi.rose.config_editor.VAR_FLAG_MARKUP.format(mode_text) label = metomi.rose.gtk.util.get_hyperlink_label(mode_text, search_func) label.show() - hbox = Gtk.HBox() + hbox = Gtk.Box() hbox.show() - hbox.pack_start(label, expand=False, fill=False) + hbox.pack_start(label, expand=False, fill=False, padding=0) hbox.set_sensitive(self.entry.get_property("sensitive")) hbox._show_mode = mode self.pack_start(hbox, expand=False, fill=False, @@ -339,7 +339,7 @@ def _set_show_meta_text_mode(self, mode, should_show_mode): break else: for widget in self.get_children(): - if (isinstance(widget, Gtk.HBox) and + if (isinstance(widget, Gtk.Box) and hasattr(widget, "_show_mode") and widget._show_mode == mode): self.remove(widget) @@ -371,9 +371,9 @@ def _toggle_flag_label(self, event_box, event, text=None): markup = metomi.rose.config_editor.VAR_FLAG_MARKUP.format(markup) label.set_markup(markup) label.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox._flag_type = flag_type - hbox.pack_start(label, expand=False, fill=False) + hbox.pack_start(label, expand=False, fill=False, padding=0) hbox.set_sensitive(self.entry.get_property("sensitive")) hbox.show() self.pack_start(hbox, expand=False, fill=False) diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index a9b1e915d..4b5d07f07 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -394,7 +394,7 @@ def about_dialog(self, args): def get_orphan_container(self, page): """Return a container with the page object inside.""" - box = Gtk.VBox() + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.pack_start(page, expand=True, fill=True) box.show() return box @@ -601,7 +601,7 @@ def override_macro_defaults(self, optionals, methname): entries[key] = entry labels[key] = label table.attach(entry, 1, 2, i, i + 1) - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(label, False, True, 0) table.attach(hbox, 0, 1, i, i + 1) dialog.show_all() diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index ef561e266..8bbcf18f6 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -28,7 +28,7 @@ import metomi.rose.gtk.util -class MenuWidget(Gtk.HBox): +class MenuWidget(Gtk.Box): """This class generates a button with a menu for variable actions.""" diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 07cd465ce..89ca67915 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -39,7 +39,7 @@ import metomi.rose.variable -class ConfigPage(Gtk.VBox): +class ConfigPage(Gtk.Box): """Returns a container for a tab.""" @@ -48,7 +48,7 @@ def __init__(self, page_metadata, config_data, ghost_data, section_ops, reporter, directory=None, sub_data=None, sub_ops=None, launch_info_func=None, launch_edit_func=None, launch_macro_func=None): - super(ConfigPage, self).__init__(homogeneous=False) + super(ConfigPage, self).__init__(homogeneous=False, orientation=Gtk.Orientation.VERTICAL) self.namespace = page_metadata.get('namespace') self.ns_is_default = page_metadata.get('ns_is_default') self.config_name = page_metadata.get('config_name') @@ -106,17 +106,17 @@ def get_page(self): self.scrolled_main_window = Gtk.ScrolledWindow() self.scrolled_main_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.scrolled_vbox = Gtk.VBox() + self.scrolled_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.scrolled_vbox.show() self.scrolled_main_window.add_with_viewport(self.scrolled_vbox) self.scrolled_main_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) self.scrolled_main_window.set_border_width( metomi.rose.config_editor.SPACING_SUB_PAGE) self.scrolled_vbox.pack_start(self.main_container, - expand=False, fill=True) + expand=False, fill=True, padding=0) self.scrolled_main_window.show() self.main_vpaned = Gtk.VPaned() - self.info_panel = Gtk.VBox(homogeneous=False) + self.info_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False) self.info_panel.show() self.update_info() second_panel = None @@ -175,7 +175,7 @@ def get_label_widget(self, is_detached=False): self._handle_enter_label) label_event_box.connect("leave-notify-event", self._handle_leave_label) - label_box = Gtk.HBox(homogeneous=False) + label_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False) if self.icon_path is not None: self.label_icon = Gtk.Image() self.label_icon.set_from_file(self.icon_path) @@ -304,12 +304,12 @@ def trigger_tab_detach(self, widget=None): def reshuffle_for_detached(self, add_button, revert_button, parent): """Reshuffle widgets for detached view.""" focus_child = getattr(self, 'focus_child') - button_hbox = Gtk.HBox(homogeneous=False, spacing=0) - self.tool_hbox = Gtk.HBox(homogeneous=False, spacing=0) + button_hbox = Gtk.Box(homogeneous=False, spacing=0) + self.tool_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0) sep = Gtk.VSeparator() sep.show() - sep_vbox = Gtk.VBox() - sep_vbox.pack_start(sep, expand=True, fill=True) + sep_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + sep_vbox.pack_start(sep, expand=True, fill=True, padding=0) sep_vbox.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) sep_vbox.show() info_button = metomi.rose.gtk.util.CustomButton( @@ -327,21 +327,21 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): as_tool=True, tip_text=metomi.rose.config_editor.TAB_MENU_WEB_HELP) url_button.connect("clicked", self.launch_url) - button_hbox.pack_start(add_button, expand=False, fill=False) - button_hbox.pack_start(revert_button, expand=False, fill=False) - button_hbox.pack_start(sep_vbox, expand=False, fill=False) - button_hbox.pack_start(info_button, expand=False, fill=False) + button_hbox.pack_start(add_button, expand=False, fill=False, padding=0) + button_hbox.pack_start(revert_button, expand=False, fill=False, padding=0) + button_hbox.pack_start(sep_vbox, expand=False, fill=False, padding=0) + button_hbox.pack_start(info_button, expand=False, fill=False, padding=0) if self.help is not None: - button_hbox.pack_start(help_button, expand=False, fill=False) + button_hbox.pack_start(help_button, expand=False, fill=False, padding=0) if self.url is not None: - button_hbox.pack_start(url_button, expand=False, fill=False) + button_hbox.pack_start(url_button, expand=False, fill=False, padding=0) button_hbox.show() button_frame = Gtk.Frame() button_frame.set_shadow_type(Gtk.ShadowType.NONE) button_frame.add(button_hbox) button_frame.show() - self.tool_hbox.pack_start(button_frame, expand=False, fill=False) - label_box = Gtk.HBox(homogeneous=False, + self.tool_hbox.pack_start(button_frame, expand=False, fill=False, padding=0) + label_box = Gtk.Box(homogeneous=False, spacing=metomi.rose.config_editor.SPACING_PAGE) # Had to remove True, True, 0 in below like Ben F label_box.pack_start(self.get_label_widget(is_detached=True)) @@ -393,14 +393,14 @@ def update_info(self): def generate_page_info(self, button_list=None, label_list=None, info=None): """Generate a widget giving information about sections.""" - info_container = Gtk.VBox(homogeneous=False) + info_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False) info_container.show() if button_list is None or label_list is None or info is None: button_list, label_list, info = self._get_page_info_widgets() self._last_info_labels = [l.get_text() for l in label_list] for button, label in zip(button_list, label_list): - var_hbox = Gtk.HBox(homogeneous=False) - var_hbox.pack_start(button, expand=False, fill=False) + var_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False) + var_hbox.pack_start(button, expand=False, fill=False, padding=0) var_hbox.pack_start(label, expand=False, fill=True, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) var_hbox.show() @@ -412,12 +412,12 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): help_label_window = Gtk.ScrolledWindow() help_label_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - help_label_hbox = Gtk.HBox() - help_label_hbox.pack_start(help_label, expand=False, fill=False) + help_label_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + help_label_hbox.pack_start(help_label, expand=False, fill=False, padding=0) help_label_hbox.show() - help_label_vbox = Gtk.VBox() + help_label_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) help_label_vbox.pack_start( - help_label_hbox, expand=False, fill=False) + help_label_hbox, expand=False, fill=False, padding=0) help_label_vbox.show() help_label_window.add_with_viewport(help_label_vbox) help_label_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) @@ -430,7 +430,7 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): height = min([metomi.rose.config_editor.SIZE_WINDOW[1] / 3, help_label.size_request()[1]]) help_label_window.set_size_request(width, height) - help_hbox = Gtk.HBox() + help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) help_hbox.pack_start(help_label_window, expand=True, fill=True, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) help_hbox.show() @@ -801,7 +801,7 @@ def sort_main(self, column_index=0, ascending=True, self.main_container.destroy() self.generate_main_container() self.scrolled_vbox.pack_start(self.main_container, expand=False, - fill=True) + fill=True, padding=0) self.choose_focus(focus_var) self.update_ignored(no_refresh=True) self.trigger_update_status() diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index d2c2c1859..469c4e5a7 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -29,7 +29,7 @@ import metomi.rose.gtk.util -class BaseSummaryDataPanel(Gtk.VBox): +class BaseSummaryDataPanel(Gtk.Box): """A base class for summarising data across many namespaces. @@ -50,7 +50,7 @@ class BaseSummaryDataPanel(Gtk.VBox): def __init__(self, sections, variables, sect_ops, var_ops, search_function, sub_ops, is_duplicate, arg_str=None): - super(BaseSummaryDataPanel, self).__init__() + super(BaseSummaryDataPanel, self).__init__(orientation=Gtk.Orientation.VERTICAL) self.sections = sections self.variables = variables self._section_data_list = None @@ -163,12 +163,12 @@ def _get_control_widget_hbox(self): self._group_widget.add_attribute(cell, 'text', 0) self._group_widget.connect("changed", self._handle_group_change) self._group_widget.show() - filter_hbox = Gtk.HBox() - filter_hbox.pack_start(group_label, expand=False, fill=False) - filter_hbox.pack_start(self._group_widget, expand=False, fill=False) + filter_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + filter_hbox.pack_start(group_label, expand=False, fill=False, padding=0) + filter_hbox.pack_start(self._group_widget, expand=False, fill=False, padding=0) filter_hbox.pack_start(filter_label, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - filter_hbox.pack_start(self._filter_widget, expand=False, fill=False) + filter_hbox.pack_start(self._filter_widget, expand=False, fill=False, padding=0) filter_hbox.show() return filter_hbox diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index bd662fae0..007ab9067 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -528,9 +528,9 @@ def _add_new_diagnostic_launcher(self): tip_text=self.PACKAGE_MANAGER_TIP, has_menu=True) self.control_widget_hbox.pack_end(package_button, expand=False, - fill=False) + fill=False, padding=0) self.control_widget_hbox.pack_end(self._add_button, - expand=False, fill=False) + expand=False, fill=False, padding=0) self._add_button.connect("clicked", self._launch_new_diagnostic_window) package_button.connect("button-press-event", diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index f5d1b262d..cfacf6460 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -30,7 +30,7 @@ import metomi.rose.config_editor.plugin.um.widget.stash_util as stash_util -class AddStashDiagnosticsPanelv1(Gtk.VBox): +class AddStashDiagnosticsPanelv1(Gtk.Box): """Display a grouped set of stash requests to add.""" @@ -82,7 +82,7 @@ def __init__(self, stash_lookup, request_lookup, info. """ - super(AddStashDiagnosticsPanelv1, self).__init__(self) + super(AddStashDiagnosticsPanelv1, self).__init__(self, orientation=Gtk.Orientation.VERTICAL) self.set_property("homogeneous", False) self.stash_lookup = stash_lookup self.request_lookup = request_lookup @@ -437,15 +437,15 @@ def _get_control_widget_hbox(self): has_menu=True) self._view_button.connect("button-press-event", self._popup_view_menu) - filter_hbox = Gtk.HBox() - filter_hbox.pack_start(group_label, expand=False, fill=False) - filter_hbox.pack_start(self._group_widget, expand=False, fill=False) + filter_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + filter_hbox.pack_start(group_label, expand=False, fill=False, padding=0) + filter_hbox.pack_start(self._group_widget, expand=False, fill=False, padding=0) filter_hbox.pack_start(filter_label, expand=False, fill=False, padding=10) - filter_hbox.pack_start(self._filter_widget, expand=False, fill=False) - filter_hbox.pack_end(self._view_button, expand=False, fill=False) - filter_hbox.pack_end(self._refresh_button, expand=False, fill=False) - filter_hbox.pack_end(self._add_button, expand=False, fill=False) + filter_hbox.pack_start(self._filter_widget, expand=False, fill=False, padding=0) + filter_hbox.pack_end(self._view_button, expand=False, fill=False, padding=0) + filter_hbox.pack_end(self._refresh_button, expand=False, fill=False, padding=0) + filter_hbox.pack_end(self._add_button, expand=False, fill=False, padding=0) filter_hbox.show() return filter_hbox diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index 78e6a8bb8..7892f234d 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -123,7 +123,7 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): vadj = text_scroller.get_vadjustment() vadj.set_value(vadj.upper - 0.9 * vadj.page_size) text_scroller.show() - vbox = Gtk.VBox() + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label = Gtk.Label() if redo_mode_on: label.set_text('REDO STACK') @@ -135,7 +135,7 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): vbox.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) vbox.pack_start(label, expand=False, fill=True, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - vbox.pack_start(text_scroller, expand=True, fill=True) + vbox.pack_start(text_scroller, expand=True, fill=True, padding=0) vbox.show() return vbox diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 6700742cf..3b77079dd 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -84,16 +84,16 @@ def stop(self): self._load_updater.stop() -class StatusBar(Gtk.VBox): +class StatusBar(Gtk.Box): """Generate the status bar widget.""" def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): - super(StatusBar, self).__init__() + super(StatusBar, self).__init__(orientation=Gtk.Orientation.VERTICAL) self.verbosity = verbosity self.num_errors = 0 self.console = None - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.show() self.pack_start(hbox, expand=False, fill=False) self._generate_error_widget() @@ -139,7 +139,7 @@ def set_num_errors(self, new_num_errors): def _generate_error_widget(self): # Generate the error display widget. - self._error_widget = Gtk.HBox() + self._error_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self._error_widget.show() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate( @@ -158,7 +158,7 @@ def _generate_message_widget(self): # Generate the message display widget. self._message_widget = Gtk.EventBox() self._message_widget.show() - message_hbox = Gtk.HBox() + message_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) message_hbox.show() self._message_widget.add(message_hbox) self._message_widget.connect("enter-notify-event", diff --git a/metomi/rose/config_editor/upgrade_controller.py b/metomi/rose/config_editor/upgrade_controller.py index afaee0444..8f7c1600b 100644 --- a/metomi/rose/config_editor/upgrade_controller.py +++ b/metomi/rose/config_editor/upgrade_controller.py @@ -114,7 +114,7 @@ def __init__(self, app_config_dict, handle_transform_func, label.show() self.window.vbox.pack_start( label, True, True, metomi.rose.config_editor.SPACING_PAGE) - button_hbox = Gtk.HBox() + button_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) button_hbox.show() all_versions_toggle_button = Gtk.CheckButton( label=metomi.rose.config_editor.DIALOG_LABEL_UPGRADE_ALL, @@ -126,7 +126,7 @@ def __init__(self, app_config_dict, handle_transform_func, button_hbox.pack_start(all_versions_toggle_button, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - self.window.vbox.pack_end(button_hbox, expand=False, fill=False) + self.window.vbox.pack_end(button_hbox, expand=False, fill=False, padding=0) self.ok_button = self.window.action_area.get_children()[0] self.window.set_focus(all_versions_toggle_button) self.window.set_focus(self.ok_button) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 107c684d3..211a0ecd9 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -27,7 +27,7 @@ import metomi.rose.variable -class EntryArrayValueWidget(Gtk.HBox): +class EntryArrayValueWidget(Gtk.Box): """This is a class to represent multiple array entries.""" @@ -202,7 +202,7 @@ def generate_buttons(self): right_event_box.connect('enter-notify-event', self._handle_arrow_enter) right_event_box.connect('leave-notify-event', self._handle_arrow_leave) right_event_box.set_tooltip_text(self.TIP_RIGHT) - self.arrow_box = Gtk.HBox() + self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() self.arrow_box.pack_start(left_event_box, expand=False, fill=False) self.arrow_box.pack_end(right_event_box, expand=False, fill=False) @@ -220,7 +220,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.del_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.button_box = Gtk.HBox() + self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() self.button_box.pack_start(self.arrow_box, expand=False, fill=True) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) @@ -235,7 +235,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.add_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.add_del_button_box = Gtk.VBox() + self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( self.add_button, expand=False, fill=False) self.add_del_button_box.pack_start( @@ -338,7 +338,7 @@ def populate_table(self, focus_widget=None): for col, label in enumerate(self.metadata['element-titles']): if col >= len(table_widgets) - 1: break - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() widget.pack_start(label, expand=True, fill=True) diff --git a/metomi/rose/config_editor/valuewidget/array/logical.py b/metomi/rose/config_editor/valuewidget/array/logical.py index 503551d2b..a86c023b9 100644 --- a/metomi/rose/config_editor/valuewidget/array/logical.py +++ b/metomi/rose/config_editor/valuewidget/array/logical.py @@ -26,7 +26,7 @@ import metomi.rose.variable -class LogicalArrayValueWidget(Gtk.HBox): +class LogicalArrayValueWidget(Gtk.Box): """This is a class to represent an array of logical or boolean types.""" @@ -124,7 +124,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.del_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.button_box = Gtk.VBox() + self.button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.button_box.show() self.button_box.pack_start(self.add_button, expand=False, fill=False) self.button_box.pack_start(self.del_button, expand=False, fill=False) @@ -192,7 +192,7 @@ def populate_table(self): for col, label in enumerate(self.metadata['element-titles']): if col >= len(table_widgets): break - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() widget.pack_start(label, expand=True, fill=True) diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index 27ab3b41f..c6d89ca3a 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -30,7 +30,7 @@ import metomi.rose.variable -class MixedArrayValueWidget(Gtk.HBox): +class MixedArrayValueWidget(Gtk.Box): """This is a class to represent a derived type variable as a table. @@ -274,7 +274,7 @@ def insert_row(self, row_index): hook = self.hook setter = ArrayElementSetter(self.setter, unwrapped_index) if self.has_titles and row_index == 0: - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][i]) label.show() widget.pack_start(label, expand=True, fill=True) @@ -365,7 +365,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.add_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.add_del_button_box = Gtk.VBox() + self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( self.add_button, expand=False, fill=False) self.add_del_button_box.pack_start( diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 0a5a132d5..8da452b9c 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -30,7 +30,7 @@ import metomi.rose.variable -class PythonListValueWidget(Gtk.HBox): +class PythonListValueWidget(Gtk.Box): """This is a class to represent a Python-compatible list format.""" @@ -177,7 +177,7 @@ def generate_buttons(self): right_event_box.connect('enter-notify-event', self._handle_arrow_enter) right_event_box.connect('leave-notify-event', self._handle_arrow_leave) right_event_box.set_tooltip_text(self.TIP_RIGHT) - self.arrow_box = Gtk.HBox() + self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() self.arrow_box.pack_start(left_event_box, expand=False, fill=False) self.arrow_box.pack_end(right_event_box, expand=False, fill=False) @@ -195,7 +195,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.del_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.button_box = Gtk.HBox() + self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() self.button_box.pack_start(self.arrow_box, expand=False, fill=True) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) @@ -210,7 +210,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.add_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.add_del_button_box = Gtk.VBox() + self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( self.add_button, expand=False, fill=False) self.add_del_button_box.pack_start( @@ -309,7 +309,7 @@ def populate_table(self, focus_widget=None): for col, label in enumerate(self.metadata['element-titles']): if col >= len(table_widgets) - 1: break - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() widget.pack_start(label, expand=True, fill=True) diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index b8f8990bc..a7855683a 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -30,7 +30,7 @@ import metomi.rose.variable -class RowArrayValueWidget(Gtk.HBox): +class RowArrayValueWidget(Gtk.Box): """This is a class to represent a value as part of a row.""" @@ -288,7 +288,7 @@ def insert_row(self, row_index): value_index = unwrapped_index if (not isinstance(self.type, list) and value_index >= len(self.value_array)): - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) eb0 = Gtk.EventBox() eb0.show() widget.pack_start(eb0, expand=True, fill=True) @@ -419,7 +419,7 @@ def generate_buttons(self, is_for_elements=False): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.add_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.add_del_button_box = Gtk.VBox() + self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( self.add_button, expand=False, fill=False) self.add_del_button_box.pack_start( diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index d431f789e..9b337fdf3 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -29,7 +29,7 @@ import metomi.rose.variable -class SpacedListValueWidget(Gtk.HBox): +class SpacedListValueWidget(Gtk.Box): """This is a class to represent a list separated by spaces.""" @@ -168,7 +168,7 @@ def generate_buttons(self): right_event_box.connect('enter-notify-event', self._handle_arrow_enter) right_event_box.connect('leave-notify-event', self._handle_arrow_leave) right_event_box.set_tooltip_text(self.TIP_RIGHT) - self.arrow_box = Gtk.HBox() + self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() self.arrow_box.pack_start(left_event_box, expand=False, fill=False) self.arrow_box.pack_end(right_event_box, expand=False, fill=False) @@ -186,7 +186,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.del_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.button_box = Gtk.HBox() + self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() self.button_box.pack_start(self.arrow_box, expand=False, fill=True) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) @@ -201,7 +201,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) self.add_button.connect('leave-notify-event', lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - self.add_del_button_box = Gtk.VBox() + self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( self.add_button, expand=False, fill=False) self.add_del_button_box.pack_start( @@ -304,7 +304,7 @@ def populate_table(self, focus_widget=None): for col, label in enumerate(self.metadata['element-titles']): if col >= len(table_widgets) - 1: break - widget = Gtk.HBox() + widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() widget.pack_start(label, expand=True, fill=True) diff --git a/metomi/rose/config_editor/valuewidget/booltoggle.py b/metomi/rose/config_editor/valuewidget/booltoggle.py index 9397796bb..2c652591e 100644 --- a/metomi/rose/config_editor/valuewidget/booltoggle.py +++ b/metomi/rose/config_editor/valuewidget/booltoggle.py @@ -25,7 +25,7 @@ import metomi.rose -class BoolToggleValueWidget(Gtk.HBox): +class BoolToggleValueWidget(Gtk.Box): """Produces a 'true' and 'false' labelled toggle button.""" diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py index 50f66d5f2..b231dfde3 100644 --- a/metomi/rose/config_editor/valuewidget/character.py +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -26,7 +26,7 @@ import metomi.rose.config_editor.util -class QuotedTextValueWidget(Gtk.HBox): +class QuotedTextValueWidget(Gtk.Box): """This class represents 'character' and 'quoted' types in an entry.""" diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py index d474ea977..3060aec7f 100644 --- a/metomi/rose/config_editor/valuewidget/choice.py +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -32,7 +32,7 @@ import metomi.rose.variable -class ChoicesValueWidget(Gtk.HBox): +class ChoicesValueWidget(Gtk.Box): """This represents a value as actual/available choices. @@ -121,7 +121,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.hints = list(args) self.should_show_kinship = self._calc_should_show_kinship() - list_vbox = Gtk.VBox() + list_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) list_vbox.show() self._listview = metomi.rose.gtk.choice.ChoicesListView( self._set_value_listview, @@ -131,9 +131,9 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): list_frame = Gtk.Frame() list_frame.show() list_frame.add(self._listview) - list_vbox.pack_start(list_frame, expand=False, fill=False) + list_vbox.pack_start(list_frame, expand=False, fill=False, padding=0) self.pack_start(list_vbox, expand=True, fill=True) - tree_vbox = Gtk.VBox() + tree_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) tree_vbox.show() self._treeview = metomi.rose.gtk.choice.ChoicesTreeView( self._set_value_treeview, @@ -145,10 +145,10 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): tree_frame = Gtk.Frame() tree_frame.show() tree_frame.add(self._treeview) - tree_vbox.pack_start(tree_frame, expand=True, fill=True) + tree_vbox.pack_start(tree_frame, expand=True, fill=True, padding=0) if self.should_edit: add_widget = self._get_add_widget() - tree_vbox.pack_end(add_widget, expand=False, fill=False) + tree_vbox.pack_end(add_widget, expand=False, fill=False, padding=0) self.pack_start(tree_vbox, expand=True, fill=True) self._listview.connect('focus-in-event', self.hook.trigger_scroll) @@ -160,7 +160,7 @@ def _handle_search(self, name): return False def _get_add_widget(self): - add_hbox = Gtk.HBox() + add_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) add_entry = Gtk.ComboBoxEntry() add_entry.connect("changed", self._handle_combo_choice) add_entry.get_child().connect( @@ -169,7 +169,7 @@ def _get_add_widget(self): add_entry.set_tooltip_text(metomi.rose.config_editor.CHOICE_TIP_ENTER_CUSTOM) add_entry.show() self._set_available_hints(add_entry) - add_hbox.pack_end(add_entry, expand=True, fill=True) + add_hbox.pack_end(add_entry, expand=True, fill=True, padding=0) add_hbox.show() return add_hbox diff --git a/metomi/rose/config_editor/valuewidget/combobox.py b/metomi/rose/config_editor/valuewidget/combobox.py index cf20c7355..01658b542 100644 --- a/metomi/rose/config_editor/valuewidget/combobox.py +++ b/metomi/rose/config_editor/valuewidget/combobox.py @@ -25,7 +25,7 @@ import metomi.rose.config_editor -class ComboBoxValueWidget(Gtk.HBox): +class ComboBoxValueWidget(Gtk.Box): """This is a class to add a combo box for a set of variable values. diff --git a/metomi/rose/config_editor/valuewidget/files.py b/metomi/rose/config_editor/valuewidget/files.py index aae134987..4bf85e6a2 100644 --- a/metomi/rose/config_editor/valuewidget/files.py +++ b/metomi/rose/config_editor/valuewidget/files.py @@ -29,7 +29,7 @@ import metomi.rose.gtk.util -class FileChooserValueWidget(Gtk.HBox): +class FileChooserValueWidget(Gtk.Box): """This class displays a path, with an open dialog to define a new one.""" @@ -95,7 +95,7 @@ def setter(self, widget): return False -class FileEditorValueWidget(Gtk.HBox): +class FileEditorValueWidget(Gtk.Box): """This class creates a button that launches an editor for a file path.""" diff --git a/metomi/rose/config_editor/valuewidget/format.py b/metomi/rose/config_editor/valuewidget/format.py index 7364b2b06..df8164881 100644 --- a/metomi/rose/config_editor/valuewidget/format.py +++ b/metomi/rose/config_editor/valuewidget/format.py @@ -25,7 +25,7 @@ import metomi.rose.config -class FormatsChooserValueWidget(Gtk.HBox): +class FormatsChooserValueWidget(Gtk.Box): """This class allows the addition of section names to a variable value.""" @@ -49,7 +49,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): for format_name in value.split(): entry = self.get_entry(format_name) self.entries.append(entry) - self.add_box = Gtk.HBox() + self.add_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add_box.show() image = Gtk.Image() image.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) diff --git a/metomi/rose/config_editor/valuewidget/intspin.py b/metomi/rose/config_editor/valuewidget/intspin.py index 15ee22aaf..6f896e9d7 100644 --- a/metomi/rose/config_editor/valuewidget/intspin.py +++ b/metomi/rose/config_editor/valuewidget/intspin.py @@ -27,7 +27,7 @@ import metomi.rose.config_editor -class IntSpinButtonValueWidget(Gtk.HBox): +class IntSpinButtonValueWidget(Gtk.Box): """This is a class to represent an integer with a spin button.""" diff --git a/metomi/rose/config_editor/valuewidget/meta.py b/metomi/rose/config_editor/valuewidget/meta.py index 0f5990196..2c55a7060 100644 --- a/metomi/rose/config_editor/valuewidget/meta.py +++ b/metomi/rose/config_editor/valuewidget/meta.py @@ -23,7 +23,7 @@ from gi.repository import Gtk -class MetaValueWidget(Gtk.HBox): +class MetaValueWidget(Gtk.Box): """This class generates an entry and button for a metadata flag value.""" diff --git a/metomi/rose/config_editor/valuewidget/radiobuttons.py b/metomi/rose/config_editor/valuewidget/radiobuttons.py index db4a277fd..9bcd9240b 100644 --- a/metomi/rose/config_editor/valuewidget/radiobuttons.py +++ b/metomi/rose/config_editor/valuewidget/radiobuttons.py @@ -25,7 +25,7 @@ import metomi.rose.config_editor -class RadioButtonsValueWidget(Gtk.HBox): +class RadioButtonsValueWidget(Gtk.Box): """This is a class to represent a value as radio buttons.""" @@ -41,7 +41,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): var_titles = metadata.get(metomi.rose.META_PROP_VALUE_TITLES) if var_titles: - vbox = Gtk.VBox() + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.pack_start(vbox, False, True, 0) vbox.show() diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py index f9679367e..38cba2605 100644 --- a/metomi/rose/config_editor/valuewidget/source.py +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -31,7 +31,7 @@ import metomi.rose.gtk.choice -class SourceValueWidget(Gtk.HBox): +class SourceValueWidget(Gtk.Box): """This class generates a special widget for the file source variable. @@ -55,18 +55,18 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): if self.formats_ok is None: content_sections = self._get_available_sections() self.formats_ok = bool(content_sections) - vbox = Gtk.VBox() + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.show() formats_check_button = Gtk.CheckButton( metomi.rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL) formats_check_button.set_active(not self.formats_ok) formats_check_button.connect("toggled", self._toggle_formats) formats_check_button.show() - formats_check_hbox = Gtk.HBox() + formats_check_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) formats_check_hbox.show() formats_check_hbox.pack_end(formats_check_button, expand=False, - fill=False) - vbox.pack_start(formats_check_hbox, expand=False, fill=False) + fill=False, padding=0) + vbox.pack_start(formats_check_hbox, expand=False, fill=False, padding=0) treeviews_hbox = Gtk.HPaned() treeviews_hbox.show() self._listview = metomi.rose.gtk.choice.ChoicesListView( @@ -80,12 +80,12 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): frame = Gtk.Frame() frame.show() frame.add(self._listview) - value_vbox = Gtk.VBox() + value_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) value_vbox.show() - value_vbox.pack_start(frame, expand=False, fill=False) + value_vbox.pack_start(frame, expand=False, fill=False, padding=0) value_eb = Gtk.EventBox() value_eb.show() - value_vbox.pack_start(value_eb, expand=True, fill=True) + value_vbox.pack_start(value_eb, expand=True, fill=True, padding=0) self._available_frame = Gtk.Frame() self._generate_available_treeview() @@ -102,8 +102,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self._adder.show() treeviews_hbox.add1(value_vbox) treeviews_hbox.add2(self._available_frame) - vbox.pack_start(treeviews_hbox, expand=True, fill=True) - vbox.pack_start(self._adder, expand=True, fill=True) + vbox.pack_start(treeviews_hbox, expand=True, fill=True, padding=0) + vbox.pack_start(self._adder, expand=True, fill=True, padding=0) self.grab_focus = lambda: self.hook.get_focus(self._listview) self.pack_start(vbox, True, True, 0) diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 8b126f49d..3d687397c 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -31,7 +31,7 @@ metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV) -class RawValueWidget(Gtk.HBox): +class RawValueWidget(Gtk.Box): """This class generates a basic entry widget for an unformatted value.""" @@ -90,7 +90,7 @@ def _handle_middle_click_paste(self, widget, event): return False -class TextMultilineValueWidget(Gtk.HBox): +class TextMultilineValueWidget(Gtk.Box): """This class displays text with multiple lines.""" diff --git a/metomi/rose/config_editor/valuewidget/valuehints.py b/metomi/rose/config_editor/valuewidget/valuehints.py index 902b19914..6b95a3cb3 100644 --- a/metomi/rose/config_editor/valuewidget/valuehints.py +++ b/metomi/rose/config_editor/valuewidget/valuehints.py @@ -28,7 +28,7 @@ import metomi.rose.variable -class HintsValueWidget(Gtk.HBox): +class HintsValueWidget(Gtk.Box): """This class generates a widget for entering value-hints.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 1c4fdb3e4..577fefa13 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -99,22 +99,22 @@ def get_keywidget(self, variable, show_modes): def generate_labelwidget(self): """Creates the label widget, a composite of key and menu widgets.""" - self.labelwidget = Gtk.VBox() + self.labelwidget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.labelwidget.show() self.labelwidget.set_ignored = self.keywidget.set_ignored menu_offset = self.menuwidget.size_request()[1] / 2 key_offset = self.keywidget.get_centre_height() / 2 - menu_vbox = Gtk.VBox() + menu_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) menu_vbox.pack_start(self.menuwidget, expand=False, fill=False, padding=max([(key_offset - menu_offset), 0])) menu_vbox.show() - key_vbox = Gtk.VBox() + key_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) key_vbox.pack_start(self.keywidget, expand=False, fill=False, padding=max([(menu_offset - key_offset) / 2, 0])) key_vbox.show() - label_content_hbox = Gtk.HBox() - label_content_hbox.pack_start(menu_vbox, expand=False, fill=False) - label_content_hbox.pack_start(key_vbox, expand=False, fill=False) + label_content_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + label_content_hbox.pack_start(menu_vbox, expand=False, fill=False, padding=0) + label_content_hbox.pack_start(key_vbox, expand=False, fill=False, padding=0) label_content_hbox.show() event_box = Gtk.EventBox() event_box.show() @@ -123,7 +123,7 @@ def generate_labelwidget(self): def generate_contentwidget(self): """Create the content widget, a vbox-packed valuewidget.""" - self.contentwidget = Gtk.VBox() + self.contentwidget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.contentwidget.show() content_event_box = Gtk.EventBox() content_event_box.show() @@ -257,7 +257,7 @@ def insert_into(self, container, x_info=None, y_info=None, self.valuewidget.trigger_scroll = ( lambda b, e: self.force_scroll(b, container)) setattr(self, 'get_parent', lambda: container) - elif isinstance(container, Gtk.VBox): + elif isinstance(container, Gtk.Box(orientation=Gtk.Orientation.VERTICAL)): container.pack_start(self.labelwidget, expand=False, fill=True, padding=5) container.pack_start(self.contentwidget, expand=True, fill=True, diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index 67bc857f1..72df9fb52 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -107,7 +107,7 @@ def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, self.window.set_default_size(*metomi.rose.config_editor.SIZE_WINDOW) self.window.set_destroy_with_parent(False) self.save_func = save_func - self.top_vbox = Gtk.VBox() + self.top_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.log_window = None # The stack viewer. self.window.add(self.top_vbox) # Load the menu bar @@ -196,13 +196,13 @@ def launch_add_dialog(self, names, add_choices, section_help): section_box.connect( "changed", lambda s: ok_button.set_sensitive(bool(s.get_text()))) - vbox = Gtk.VBox(spacing=10) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) vbox.pack_start(config_label, expand=False, fill=False, padding=5) vbox.pack_start(config_name_box, expand=False, fill=False, padding=5) vbox.pack_start(label, expand=False, fill=False, padding=5) vbox.pack_start(section_box, expand=False, fill=False, padding=5) vbox.show() - hbox = Gtk.HBox(spacing=10) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) hbox.pack_start(vbox, expand=True, fill=True, padding=10) hbox.show() add_dialog.vbox.pack_start(hbox, True, True, 0) @@ -315,7 +315,7 @@ def _launch_choose_section_dialog( if config_name_box.get_active() == -1: config_name_box.set_active(0) config_name_box.show() - section_box = Gtk.VBox() + section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) section_box.show() null_section_checkbutton = Gtk.CheckButton( metomi.rose.config_editor.DIALOG_LABEL_NULL_SECTION) @@ -337,7 +337,7 @@ def _launch_choose_section_dialog( section_box, name_section_dict[name_keys[c.get_active()]], prefs.get(name_keys[c.get_active()], []))) - vbox = Gtk.VBox(spacing=metomi.rose.config_editor.SPACING_PAGE) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=metomi.rose.config_editor.SPACING_PAGE) vbox.pack_start(config_label, expand=False, fill=False) vbox.pack_start(config_name_box, expand=False, fill=False) vbox.pack_start(section_label, expand=False, fill=False) @@ -360,7 +360,7 @@ def _launch_choose_section_dialog( target_section_entry.show() vbox.pack_start(target_section_entry, expand=False, fill=False) vbox.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(vbox, expand=True, fill=True, padding=metomi.rose.config_editor.SPACING_PAGE) hbox.show() @@ -434,7 +434,7 @@ def launch_new_config_dialog(self, root_directory): dialog, container, name_entry = metomi.rose.gtk.dialog.get_naming_dialog( label, checker_function, ok_tip_text, err_tip_text) dialog.set_title(metomi.rose.config_editor.DIALOG_TITLE_CONFIG_CREATE) - meta_hbox = Gtk.HBox() + meta_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) meta_label = Gtk.Label(label= metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META) meta_label.show() @@ -644,7 +644,7 @@ def __init__(self, window, config_name, macro_name, mode, search_func): stock_id = Gtk.STOCK_CONVERT image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.LARGE_TOOLBAR) image.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(image, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_PAGE) hbox.pack_start(self.label, expand=False, fill=False, diff --git a/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py b/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py index 2ad5ebf53..09da53a57 100644 --- a/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py +++ b/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py @@ -16,7 +16,7 @@ import gtk -class UsernameValueWidget(gtk.HBox): +class UsernameValueWidget(Gtk.Box): """This class generates a widget for entering usernames.""" diff --git a/metomi/rose/gtk/console.py b/metomi/rose/gtk/console.py index 706a9ea02..36a3cda8c 100644 --- a/metomi/rose/gtk/console.py +++ b/metomi/rose/gtk/console.py @@ -55,7 +55,7 @@ def __init__(self, categories, category_message_time_tuples, self.category_icons.append( self.render_icon(id_, Gtk.IconSize.MENU)) self._destroy_hook = destroy_hook - top_vbox = Gtk.VBox() + top_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) top_vbox.show() self.add(top_vbox) @@ -108,11 +108,11 @@ def __init__(self, categories, category_message_time_tuples, self._message_treeview.set_model(filter_model) message_scrolled_window.add(self._message_treeview) - top_vbox.pack_start(message_scrolled_window, expand=True, fill=True) + top_vbox.pack_start(message_scrolled_window, expand=True, fill=True, padding=0) - category_hbox = Gtk.HBox() + category_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) category_hbox.show() - top_vbox.pack_end(category_hbox, expand=False, fill=False) + top_vbox.pack_end(category_hbox, expand=False, fill=False, padding=0) for category in categories + [self.CATEGORY_ALL]: togglebutton = Gtk.ToggleButton(label=category, use_underline=False) @@ -120,7 +120,7 @@ def __init__(self, categories, category_message_time_tuples, lambda b: self._set_new_filter( b, category_hbox.get_children())) togglebutton.show() - category_hbox.pack_start(togglebutton, expand=True, fill=True) + category_hbox.pack_start(togglebutton, expand=True, fill=True, padding=0) togglebutton.set_active(True) self.show() self._scroll_to_end() diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 74f7ce902..5abcf84af 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -111,18 +111,18 @@ def __init__(self, cmd_args, description=None, title=None, self.image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.DIALOG) self.image.show() - image_vbox = Gtk.VBox() - image_vbox.pack_start(self.image, expand=False, fill=False) + image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + image_vbox.pack_start(self.image, expand=False, fill=False, padding=0) image_vbox.show() - top_hbox = Gtk.HBox() + top_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) top_hbox.pack_start(image_vbox, expand=False, fill=False, padding=DIALOG_PADDING) top_hbox.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(self.label, expand=False, fill=False, padding=DIALOG_PADDING) hbox.show() - main_vbox = Gtk.VBox() + main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) main_vbox.show() main_vbox.pack_start(hbox, expand=False, fill=False, padding=DIALOG_SUB_PADDING) @@ -136,7 +136,7 @@ def __init__(self, cmd_args, description=None, title=None, self.cmd_label = Gtk.Label() self.cmd_label.set_markup("" + cmd_string + "") self.cmd_label.show() - cmd_hbox = Gtk.HBox() + cmd_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) cmd_hbox.pack_start(self.cmd_label, expand=False, fill=False, padding=DIALOG_PADDING) cmd_hbox.show() @@ -146,7 +146,7 @@ def __init__(self, cmd_args, description=None, title=None, self.progress_bar = Gtk.ProgressBar() self.progress_bar.set_pulse_step(0.1) self.progress_bar.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(self.progress_bar, expand=True, fill=True, padding=DIALOG_PADDING) hbox.show() @@ -155,7 +155,7 @@ def __init__(self, cmd_args, description=None, title=None, top_hbox.pack_start(main_vbox, expand=True, fill=True, padding=DIALOG_PADDING) if self.event_queue is None: - self.dialog.vbox.pack_start(top_hbox, expand=True, fill=True) + self.dialog.vbox.pack_start(top_hbox, expand=True, fill=True, padding=0) else: text_view_scroll = Gtk.ScrolledWindow() text_view_scroll.set_policy(Gtk.PolicyType.NEVER, @@ -301,8 +301,8 @@ def run_command_arg_dialog(cmd_name, help_text, run_hook): help_button.connect( "clicked", lambda b: run_scrolled_dialog(help_text, title=help_label)) - help_hbox = Gtk.HBox() - help_hbox.pack_start(help_button, expand=False, fill=False) + help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + help_hbox.pack_start(help_button, expand=False, fill=False, padding=0) help_hbox.show() container.pack_end(help_hbox, expand=False, fill=False) name_entry.grab_focus() @@ -364,10 +364,10 @@ def run_dialog(dialog_type, text, title=None, modal=True, else: dialog.label.set_markup(text) dialog.label.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if stock_id is not None: - image_vbox = Gtk.VBox() + image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) image_vbox.pack_start(dialog.image, expand=False, fill=False, padding=DIALOG_PADDING) image_vbox.show() @@ -377,8 +377,8 @@ def run_dialog(dialog_type, text, title=None, modal=True, scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_border_width(0) scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) - vbox = Gtk.VBox() - vbox.pack_start(dialog.label, expand=True, fill=True) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.pack_start(dialog.label, expand=True, fill=True, padding=0) vbox.show() scrolled_window.add_with_viewport(vbox) scrolled_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) @@ -386,7 +386,7 @@ def run_dialog(dialog_type, text, title=None, modal=True, hbox.pack_start(scrolled_window, expand=True, fill=True, padding=metomi.rose.config_editor.SPACING_PAGE) hbox.show() - dialog.vbox.pack_end(hbox, expand=True, fill=True) + dialog.vbox.pack_end(hbox, expand=True, fill=True, padding=0) if "\n" in text: dialog.label.set_line_wrap(False) @@ -422,12 +422,12 @@ def run_hyperlink_dialog(stock_id=None, text="", title=None, dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) dialog.set_title(title) dialog.set_modal(False) - top_vbox = Gtk.VBox() + top_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) top_vbox.show() - main_hbox = Gtk.HBox(spacing=DIALOG_PADDING) + main_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=DIALOG_PADDING) main_hbox.show() # Insert the image - image_vbox = Gtk.VBox() + image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) image_vbox.show() image = Gtk.Image.new_from_stock(stock_id, size=Gtk.IconSize.DIALOG) @@ -437,7 +437,7 @@ def run_hyperlink_dialog(stock_id=None, text="", title=None, main_hbox.pack_start(image_vbox, expand=False, fill=False, padding=DIALOG_PADDING) # Apply the text - message_vbox = Gtk.VBox() + message_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) message_vbox.show() label = metomi.rose.gtk.util.get_hyperlink_label(text, search_func) message_vbox.pack_start(label, expand=True, fill=True, @@ -448,13 +448,13 @@ def run_hyperlink_dialog(stock_id=None, text="", title=None, scrolled_window.add_with_viewport(message_vbox) scrolled_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) scrolled_window.show() - vbox = Gtk.VBox() - vbox.pack_start(scrolled_window, expand=True, fill=True) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.pack_start(scrolled_window, expand=True, fill=True, padding=0) vbox.show() - main_hbox.pack_start(vbox, expand=True, fill=True) - top_vbox.pack_start(main_hbox, expand=True, fill=True) + main_hbox.pack_start(vbox, expand=True, fill=True, padding=0) + top_vbox.pack_start(main_hbox, expand=True, fill=True, padding=0) # Insert the button - button_box = Gtk.HBox(spacing=DIALOG_PADDING) + button_box = Gtk.Box(spacing=DIALOG_PADDING) button_box.show() button = metomi.rose.gtk.util.CustomButton(label=DIALOG_BUTTON_CLOSE, size=Gtk.IconSize.LARGE_TOOLBAR, @@ -497,7 +497,7 @@ def run_scrolled_dialog(text, title=None): label.show() filler_eb = Gtk.EventBox() filler_eb.show() - label_box = Gtk.VBox() + label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label_box.pack_start(label, expand=False, fill=False) label_box.pack_start(filler_eb, expand=True, fill=True) label_box.show() @@ -512,12 +512,12 @@ def run_scrolled_dialog(text, title=None): button.connect("clicked", lambda b: window.destroy()) button.show() button.grab_focus() - button_box = Gtk.HBox() + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) button_box.pack_end(button, expand=False, fill=False) button_box.show() - main_vbox = Gtk.VBox(spacing=DIALOG_SUB_PADDING) - main_vbox.pack_start(scrolled, expand=True, fill=True) - main_vbox.pack_end(button_box, expand=False, fill=False) + main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=DIALOG_SUB_PADDING) + main_vbox.pack_start(scrolled, expand=True, fill=True, padding=0) + main_vbox.pack_end(button_box, expand=False, fill=False, padding=0) main_vbox.show() window.add(main_vbox) window.show() @@ -535,8 +535,8 @@ def get_naming_dialog(label, checker, ok_tip=None, dialog.set_transient_for(parent_window) dialog.set_modal(True) ok_button = dialog.action_area.get_children()[0] - main_vbox = Gtk.VBox() - name_hbox = Gtk.HBox() + main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + name_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) name_label = Gtk.Label() name_label.set_text(label) name_label.show() @@ -555,7 +555,7 @@ def get_naming_dialog(label, checker, ok_tip=None, main_vbox.pack_start(name_hbox, expand=False, fill=True, padding=DIALOG_PADDING) main_vbox.show() - hbox = Gtk.HBox() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(main_vbox, expand=False, fill=True, padding=DIALOG_PADDING) hbox.show() @@ -596,7 +596,7 @@ def run_choices_dialog(text, choices, title=None): else: label.set_markup(text) dialog.vbox.set_spacing(DIALOG_SUB_PADDING) - dialog.vbox.pack_start(label, expand=False, fill=False) + dialog.vbox.pack_start(label, expand=False, fill=False, padding=0) if len(choices) < 5: for i, choice in enumerate(choices): group = None @@ -607,7 +607,7 @@ def run_choices_dialog(text, choices, title=None): radio_button = Gtk.RadioButton(group, label=choice, use_underline=False) - dialog.vbox.pack_start(radio_button, expand=False, fill=False) + dialog.vbox.pack_start(radio_button, expand=False, fill=False, padding=0) getter = (lambda: [b.get_label() for b in radio_button.get_group() if b.get_active()].pop()) @@ -616,7 +616,7 @@ def run_choices_dialog(text, choices, title=None): for choice in choices: combo_box.append_text(choice) combo_box.set_active(0) - dialog.vbox.pack_start(combo_box, expand=False, fill=False) + dialog.vbox.pack_start(combo_box, expand=False, fill=False, padding=0) getter = lambda: choices[combo_box.get_active()] dialog.show_all() response = dialog.run() diff --git a/metomi/rose/gtk/splash.py b/metomi/rose/gtk/splash.py index c3bb5ee9d..c8b649540 100755 --- a/metomi/rose/gtk/splash.py +++ b/metomi/rose/gtk/splash.py @@ -60,11 +60,11 @@ def __init__(self, logo_path, title, total_number_of_events): metomi.rose.gtk.util.color_parse(self.BACKGROUND_COLOUR)) self.set_gravity(5) # same as gravity center self.set_position(Gtk.WindowPosition.CENTER) - main_vbox = Gtk.VBox() + main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) main_vbox.show() image = Gtk.Image.new_from_file(logo_path) image.show() - image_hbox = Gtk.HBox() + image_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) image_hbox.show() image_hbox.pack_start(image, expand=False, fill=True, padding=0) main_vbox.pack_start(image_hbox, expand=False, fill=True, padding=0) @@ -78,7 +78,7 @@ def __init__(self, logo_path, title, total_number_of_events): self._progress_message = None self.event_count = 0.0 self.total_number_of_events = float(total_number_of_events) - progress_hbox = Gtk.HBox(spacing=self.SUB_PADDING) + progress_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=self.SUB_PADDING) progress_hbox.show() progress_hbox.pack_start(self.progress_bar, expand=True, fill=True, padding=self.SUB_PADDING) diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index e3ec7a0eb..e6a552efd 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -113,9 +113,9 @@ def set_stock_id(self, stock_id): self.icon.set_from_stock(stock_id, self.size) self.stock_id = stock_id if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False) + self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) else: - self.hbox.pack_end(self.icon, expand=False, fill=False) + self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) return False def set_tip_text(self, new_text): @@ -154,7 +154,7 @@ def __init__(self, expander_function=None, else: self.stock_id = self.minimise_id - self.hbox = Gtk.HBox() + self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.size = size self.as_tool = as_tool self.icon_at_start = icon_at_start @@ -174,9 +174,9 @@ def __init__(self, expander_function=None, self.icon.set_from_stock(self.stock_id, size) self.icon.show() if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False) + self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) else: - self.hbox.pack_end(self.icon, expand=False, fill=False) + self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) self.hbox.show() super(CustomExpandButton, self).__init__() @@ -196,9 +196,9 @@ def set_stock_id(self, stock_id): self.icon.set_from_stock(stock_id, self.size) self.stock_id = stock_id if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False) + self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) else: - self.hbox.pack_end(self.icon, expand=False, fill=False) + self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) return False def set_tip_text(self, new_text): From 7d7b1051ded2a0a838005b97b976f166b95f5fcb Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 12 Aug 2024 16:37:14 +0100 Subject: [PATCH 13/42] Removes hard coded styling and replaces with css Feature/33 separator (#42) * 33 vertical padding added to widgets for a 'soft' distinction * Vertical padding removed, line distinctions added using css styling Removes top and bottom border from main page window --- metomi/rose/config_editor/main.py | 13 +++++- metomi/rose/config_editor/page.py | 12 +++--- metomi/rose/config_editor/status.py | 5 --- .../config_editor/valuewidget/character.py | 3 -- metomi/rose/config_editor/valuewidget/meta.py | 6 +-- metomi/rose/config_editor/valuewidget/text.py | 3 -- metomi/rose/config_editor/variable.py | 2 - metomi/rose/etc/rose-config-edit/style.css | 40 +++++++++++++++++++ 8 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 metomi/rose/etc/rose-config-edit/style.css diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 534ed491a..aa517ef2f 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -53,7 +53,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config import metomi.rose.config_editor @@ -1992,6 +1992,17 @@ def main(): if opts.new_mode: cwd = None metomi.rose.gtk.dialog.set_exception_hook_dialog(keep_alive=True) + + screen = Gdk.Screen.get_default() + provider = Gtk.CssProvider() + style_context = Gtk.StyleContext() + style_context.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + locator = metomi.rose.resource.ResourceLocator(paths=sys.path) + css_path = locator.locate('etc/rose-config-edit/style.css') + provider.load_from_path(str(css_path)) + if opts.profile_mode: handle = tempfile.NamedTemporaryFile() cProfile.runctx("""spawn_window(cwd, debug_mode=opts.debug_mode, diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 89ca67915..a3294b8f0 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -110,8 +110,10 @@ def get_page(self): self.scrolled_vbox.show() self.scrolled_main_window.add_with_viewport(self.scrolled_vbox) self.scrolled_main_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) - self.scrolled_main_window.set_border_width( - metomi.rose.config_editor.SPACING_SUB_PAGE) + self.scrolled_main_window.set_margin_start( + metomi.rose.config_editor.SPACING_SUB_PAGE) # left + self.scrolled_main_window.set_margin_end( + metomi.rose.config_editor.SPACING_SUB_PAGE) # right self.scrolled_vbox.pack_start(self.main_container, expand=False, fill=True, padding=0) self.scrolled_main_window.show() @@ -184,11 +186,7 @@ def get_label_widget(self, is_detached=False): padding=metomi.rose.config_editor.SPACING_SUB_PAGE) close_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_CLOSE, size=Gtk.IconSize.MENU, as_tool=True) - style = Gtk.RcStyle() - style.xthickness = 0 - style.ythickness = 0 - setattr(style, "inner-border", [0, 0, 0, 0]) - close_button.modify_style(style) + Gtk.Widget.set_name(close_button, "page-tab-button") label_box.pack_start(label_event_box, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 3b77079dd..9a735b904 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -179,11 +179,6 @@ def _generate_message_widget(self): tip_text=metomi.rose.config_editor.STATUS_BAR_CONSOLE_TIP, as_tool=True) self._console_launcher.connect("clicked", self._launch_console) - style = Gtk.RcStyle() - style.xthickness = 0 - style.ythickness = 0 - setattr(style, "inner-border", [0, 0, 0, 0]) - self._console_launcher.modify_style(style) message_hbox.pack_start( self._message_widget_error_image, expand=False, fill=False) diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py index b231dfde3..af28d8953 100644 --- a/metomi/rose/config_editor/valuewidget/character.py +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -58,9 +58,6 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook self.entry = Gtk.Entry() - insensitive_colour = Gtk.Style().bg[0] - self.entry.modify_bg(Gtk.StateType.INSENSITIVE, - insensitive_colour) self.in_error = not self.type_checker(self.value) self.set_entry_text() self.entry.connect("button-release-event", diff --git a/metomi/rose/config_editor/valuewidget/meta.py b/metomi/rose/config_editor/valuewidget/meta.py index 2c55a7060..d33b5426a 100644 --- a/metomi/rose/config_editor/valuewidget/meta.py +++ b/metomi/rose/config_editor/valuewidget/meta.py @@ -34,8 +34,6 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook self.entry = Gtk.Entry() - self.normal_colour = self.entry.style.text[Gtk.StateType.NORMAL] - self.insens_colour = self.entry.style.text[Gtk.StateType.INSENSITIVE] self.entry.set_text(self.value) self.entry.connect("button-release-event", self._handle_middle_click_paste) @@ -60,10 +58,10 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): def _check_diff(self, *args): text = self.entry.get_text() if text == self.value: - self.entry.modify_text(Gtk.StateType.NORMAL, self.normal_colour) + # self.entry.modify_text(Gtk.StateType.NORMAL, self.normal_colour) self.button.set_sensitive(False) else: - self.entry.modify_text(Gtk.StateType.NORMAL, self.insens_colour) + # self.entry.modify_text(Gtk.StateType.NORMAL, self.insens_colour) self.button.set_sensitive(True) if not text: self.button.set_sensitive(False) diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 3d687397c..2844d5db7 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -42,9 +42,6 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook self.entry = Gtk.Entry() - insensitive_colour = Gtk.Style().bg[0] - self.entry.modify_bg(Gtk.StateType.INSENSITIVE, insensitive_colour) - self.normal_colour = Gtk.Style().fg[Gtk.StateType.NORMAL] if metomi.rose.env.contains_env_var(self.value): self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) self.entry.set_tooltip_text(metomi.rose.config_editor.VAR_WIDGET_ENV_INFO) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 577fefa13..16b82b055 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -59,7 +59,6 @@ def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, if show_modes is None: show_modes = {} self.show_modes = show_modes - self.insensitive_colour = Gtk.Style().bg[0] self.bad_colour = metomi.rose.gtk.util.color_parse( metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) self.hidden_colour = metomi.rose.gtk.util.color_parse( @@ -207,7 +206,6 @@ def handle_bad_valuewidget(self, error_info, variable, set_value): self.generate_valuewidget(variable, override_custom=True) def handle_focus_in(self, widget, event): - widget._first_colour = widget.style.base[Gtk.StateType.NORMAL] new_colour = metomi.rose.gtk.util.color_parse( metomi.rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED) widget.modify_base(Gtk.StateType.NORMAL, new_colour) diff --git a/metomi/rose/etc/rose-config-edit/style.css b/metomi/rose/etc/rose-config-edit/style.css new file mode 100644 index 000000000..857fbd5e8 --- /dev/null +++ b/metomi/rose/etc/rose-config-edit/style.css @@ -0,0 +1,40 @@ +/* Add padding in-between env cogs and the variable title on pages */ +button > widget > box > image { + padding-right: 10px; +} + +popover > box > button > box > image { + padding-right: 10px; +} + +/* Add an outline to indicate focus when navigating using internal links */ +notebook button:focus { + outline-style: dashed; + outline-color: gray; +} + +/* Fix the close button for tab page labels */ +#page-tab-button { + border: None; + padding-right: 10px; +} + +#page-tab-button:hover { + background-image: None; + box-shadow: None; +} + +#macro-button { + padding: 5px; + margin-top: 10px; + margin-left: 10px; +} + +/* Underline key and variable widgets so rows of widgets are distinct */ +notebook box > paned > paned > scrolledwindow > viewport > box > widget > box { + border-bottom: solid; + border-width: 1px; + border-color: lightgray; + padding: .4em; + margin: 0 -.3em; + } From e9b699a07be4843b0c16caaa4c1f855023c0714a Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 12 Aug 2024 17:44:03 +0100 Subject: [PATCH 14/42] Fixes widget size related calls --- metomi/rose/config_editor/keywidget.py | 2 +- metomi/rose/config_editor/page.py | 5 +++-- metomi/rose/config_editor/stack.py | 6 +++--- metomi/rose/config_editor/variable.py | 2 +- metomi/rose/config_editor/window.py | 5 +++-- metomi/rose/gtk/dialog.py | 10 ++++++---- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index b596fb2ff..b9c7047c6 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -126,7 +126,7 @@ def add_flag(self, flag_type, tooltip_text=None): def get_centre_height(self): """Return the vertical displacement of the centre of this widget.""" - return (self.entry.size_request()[1] / 2) + return (self.entry.get_preferred_size().natural_size.height / 2) def handle_launch_help(self, widget, event): """Handle launching help.""" diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index a3294b8f0..3c1721b8b 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -420,13 +420,14 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): help_label_window.add_with_viewport(help_label_vbox) help_label_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) help_label_window.show() - width, height = help_label_window.size_request() + width = help_label_window.get_preferred_size().natural_size.width + height = help_label_window.get_preferred_size().natural_size.height if info == "Blank page - no data": self.main_vpaned.set_position( metomi.rose.config_editor.SIZE_WINDOW[1] * 100) else: height = min([metomi.rose.config_editor.SIZE_WINDOW[1] / 3, - help_label.size_request()[1]]) + help_label.get_preferred_size().natural_size.height]) help_label_window.set_size_request(width, height) help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) help_hbox.pack_start(help_label_window, expand=True, fill=True, diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index 7892f234d..89161c943 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -121,7 +121,7 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): text_scroller.set_shadow_type(Gtk.ShadowType.IN) text_scroller.add(text_view) vadj = text_scroller.get_vadjustment() - vadj.set_value(vadj.upper - 0.9 * vadj.page_size) + vadj.set_value(vadj.get_upper() - 0.9 * vadj.get_page_size()) text_scroller.show() vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label = Gtk.Label() @@ -192,8 +192,8 @@ def get_stack_model(self, redo_mode_on=False, make_new_model=False): def scroll_view(self, tree_view, event=None): """Scroll the parent scrolled window to the bottom.""" vadj = tree_view.get_parent().get_vadjustment() - if vadj.upper > vadj.lower + vadj.page_size: - vadj.set_value(vadj.upper - 0.95 * vadj.page_size) + if vadj.get_upper() > vadj.get_lower() + vadj.get_page_size(): + vadj.set_value(vadj.get_upper() - 0.95 * vadj.get_page_size()) def update(self): """Reload text views from the undo and redo stacks.""" diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 16b82b055..59a9c3781 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -101,7 +101,7 @@ def generate_labelwidget(self): self.labelwidget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.labelwidget.show() self.labelwidget.set_ignored = self.keywidget.set_ignored - menu_offset = self.menuwidget.size_request()[1] / 2 + menu_offset = self.menuwidget.get_preferred_size().natural_size.height / 2 key_offset = self.keywidget.get_centre_height() / 2 menu_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) menu_vbox.pack_start(self.menuwidget, expand=False, fill=False, diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index 72df9fb52..f5ebd9b0b 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -731,8 +731,9 @@ def display(self, changes): max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX my_size = self.size_request() new_size = [-1, -1] - for i in [0, 1]: - new_size[i] = min([my_size[i], max_size[i]]) + # this needs checking + new_size[0] = min([my_size.width, max_size[0]]) + new_size[1] = min([my_size.height, max_size[1]]) self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.set_default_size(*new_size) if self.for_transform: diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 5abcf84af..4638d6313 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -665,11 +665,13 @@ def run_edit_dialog(text, finish_hook=None, title=None): # hacky solution to get "true" size for dialog dialog.show() - start_size = dialog.size_request() + start_width = dialog.get_preferred_size().natural_size.width + start_height = dialog.get_preferred_size().natural_size.height scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - end_size = dialog.size_request() - my_size = (max([start_size[0], end_size[0], min_size[0]]) + 20, - max([start_size[1], end_size[1], min_size[1]]) + 20) + end_width = dialog.get_preferred_size().natural_size.width + end_height = dialog.get_preferred_size().natural_size.height + my_size = (max([start_width, end_width, min_size[0]]) + 20, + max([start_height, end_height, min_size[1]]) + 20) new_size = [-1, -1] for i in [0, 1]: new_size[i] = min([my_size[i], max_size[i]]) From 15ef07ff7ad461f01a6aa206dc6fe87e5f35f954 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 14 Aug 2024 16:51:54 +0100 Subject: [PATCH 15/42] Fixes pack_start calls --- metomi/rose/config_editor/keywidget.py | 6 +++--- metomi/rose/config_editor/menu.py | 2 +- metomi/rose/config_editor/nav_panel.py | 6 +++--- metomi/rose/config_editor/page.py | 13 ++++++------ .../config_editor/panelwidget/filesystem.py | 2 +- .../config_editor/panelwidget/summary_data.py | 10 +++++----- .../config_editor/plugin/um/widget/stash.py | 4 ++-- .../plugin/um/widget/stash_add.py | 8 ++++---- metomi/rose/config_editor/stack.py | 2 +- metomi/rose/config_editor/status.py | 16 +++++++-------- .../valuewidget/array/__init__.py | 2 ++ .../config_editor/valuewidget/array/entry.py | 14 ++++++------- .../valuewidget/array/logical.py | 8 ++++---- .../config_editor/valuewidget/array/mixed.py | 10 +++++----- .../valuewidget/array/python_list.py | 16 +++++++-------- .../config_editor/valuewidget/array/row.py | 11 +++++----- .../valuewidget/array/spaced_list.py | 16 +++++++-------- .../config_editor/valuewidget/booltoggle.py | 2 +- .../config_editor/valuewidget/combobox.py | 2 +- .../rose/config_editor/valuewidget/files.py | 4 ++-- metomi/rose/config_editor/valuewidget/text.py | 2 +- metomi/rose/config_editor/variable.py | 8 ++++---- metomi/rose/config_editor/window.py | 20 +++++++++---------- metomi/rose/gtk/choice.py | 5 +++-- metomi/rose/gtk/console.py | 6 +++--- metomi/rose/gtk/dialog.py | 4 ++-- metomi/rose/gtk/util.py | 11 ++++++---- 27 files changed, 108 insertions(+), 102 deletions(-) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index b9c7047c6..2a5392024 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -55,7 +55,7 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, self.my_variable = variable self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox.show() - self.pack_start(self.hbox, expand=False, fill=False) + self.pack_start(self.hbox, expand=False, fill=False, padding=0) self.var_ops = var_ops self.meta = variable.metadata self.launch_help = launch_help_func @@ -88,7 +88,7 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, self.hbox.pack_start(event_box, expand=True, fill=True, padding=0) self.comments_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.hbox.pack_start(self.comments_box, expand=False, fill=False) + self.hbox.pack_start(self.comments_box, expand=False, fill=False, padding=0) self.grab_focus = self.entry.grab_focus self.set_sensitive(True) self.set_sensitive = self._set_sensitive @@ -376,7 +376,7 @@ def _toggle_flag_label(self, event_box, event, text=None): hbox.pack_start(label, expand=False, fill=False, padding=0) hbox.set_sensitive(self.entry.get_property("sensitive")) hbox.show() - self.pack_start(hbox, expand=False, fill=False) + self.pack_start(hbox, expand=False, fill=False, padding=0) def _edit_finish_hook(self, text): self.var_ops.set_var_comments(self.my_variable, text.splitlines()) diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 4b5d07f07..5f0e5484f 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -350,7 +350,7 @@ def add_macro(self, config_name, modulename, classname, methodname, all_item_label = Gtk.Label(label=metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) all_item = Gtk.MenuItem() all_item_box.pack_start(all_item_icon, False, False, 0) - all_item_box.pack_start(all_item, False, False, 0) + all_item_box.pack_start(all_item_label, False, False, 0) Gtk.Container.add(macro_item, macro_item_box) all_item._rose_all_validators = True all_item.set_tooltip_text( diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index 9d9fbd267..113dd3da7 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -74,9 +74,9 @@ def __init__(self, namespace_tree, launch_ns_func, self.cell_error_icon = Gtk.CellRendererPixbuf() self.cell_changed_icon = Gtk.CellRendererPixbuf() self.cell_title = Gtk.CellRendererText() - self.panel_top.pack_start(self.cell_error_icon, False, True, 0) - self.panel_top.pack_start(self.cell_changed_icon, False, True, 0) - self.panel_top.pack_start(self.cell_title, False, True, 0) + self.panel_top.pack_start(self.cell_error_icon, False) + self.panel_top.pack_start(self.cell_changed_icon, False) + self.panel_top.pack_start(self.cell_title, False) self.panel_top.add_attribute(self.cell_error_icon, attribute='pixbuf', column=self.COLUMN_ERROR_ICON) diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 3c1721b8b..72d742192 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -145,7 +145,7 @@ def get_page(self): self.vpaned.show() self.main_vpaned.pack2(self.vpaned) self.main_vpaned.show() - self.pack_start(self.main_vpaned, expand=True, fill=True) + self.pack_start(self.main_vpaned, expand=True, fill=True, padding=0) self.show() self.scroll_vadj = self.scrolled_main_window.get_vadjustment() self.scrolled_main_window.connect( @@ -191,7 +191,7 @@ def get_label_widget(self, is_detached=False): label_box.pack_start(label_event_box, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) if not is_detached: - label_box.pack_end(close_button, expand=False, fill=False) + label_box.pack_end(close_button, expand=False, fill=False, padding=0) label_box.show() event_box = Gtk.EventBox() event_box.add(label_box) @@ -341,13 +341,12 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): self.tool_hbox.pack_start(button_frame, expand=False, fill=False, padding=0) label_box = Gtk.Box(homogeneous=False, spacing=metomi.rose.config_editor.SPACING_PAGE) - # Had to remove True, True, 0 in below like Ben F - label_box.pack_start(self.get_label_widget(is_detached=True)) + label_box.pack_start(self.get_label_widget(is_detached=True), False, False, 0) label_box.show() self.tool_hbox.pack_start( label_box, expand=True, fill=True, padding=10) self.tool_hbox.show() - self.pack_start(self.tool_hbox, expand=False, fill=False) + self.pack_start(self.tool_hbox, expand=False, fill=False, padding=0) self.reorder_child(self.tool_hbox, 0) if isinstance(parent, Gtk.Window): if parent.get_child() is not None: @@ -402,7 +401,7 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): var_hbox.pack_start(label, expand=False, fill=True, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) var_hbox.show() - info_container.pack_start(var_hbox, expand=False, fill=True) + info_container.pack_start(var_hbox, expand=False, fill=True, padding=0) # Add page help. if self.description: help_label = metomi.rose.gtk.util.get_hyperlink_label( @@ -438,7 +437,7 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): padding=metomi.rose.config_editor.SPACING_SUB_PAGE) for child in self.info_panel.get_children(): self.info_panel.remove(child) - self.info_panel.pack_start(info_container, expand=True, fill=True) + self.info_panel.pack_start(info_container, expand=True, fill=True, padding=0) def generate_filesystem_panel(self): """Generate a widget to view the file hierarchy.""" diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index f807f64dc..2fae5e575 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -63,7 +63,7 @@ def __init__(self, directory): col = Gtk.TreeViewColumn() col.set_title(metomi.rose.config_editor.TITLE_FILE_PANEL) cell = Gtk.CellRendererText() - col.pack_start(cell, True, True, 0) + col.pack_start(cell, True) col.set_cell_data_func(cell, self._set_path_markup, store) view.append_column(col) diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index 469c4e5a7..1c4459312 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -64,7 +64,7 @@ def __init__(self, sections, variables, sect_ops, var_ops, self.group_index = None self.util = metomi.rose.config_editor.util.Lookup() self.control_widget_hbox = self._get_control_widget_hbox() - self.pack_start(self.control_widget_hbox, expand=False, fill=False) + self.pack_start(self.control_widget_hbox, expand=False, fill=False, padding=0) self._prev_store = None self._prev_sort_model = None self._view = metomi.rose.gtk.util.TooltipTreeView( @@ -83,7 +83,7 @@ def __init__(self, sections, variables, sect_ops, var_ops, self.update() self._window.add(self._view) self._window.show() - self.pack_start(self._window, expand=True, fill=True) + self.pack_start(self._window, expand=True, fill=True, padding=0) self.show() def add_cell_renderer_for_value(self, column, column_title): @@ -159,7 +159,7 @@ def _get_control_widget_hbox(self): group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() - self._group_widget.pack_start(cell, True, True, 0) + self._group_widget.pack_start(cell, True) self._group_widget.add_attribute(cell, 'text', 0) self._group_widget.connect("changed", self._handle_group_change) self._group_widget.show() @@ -295,7 +295,7 @@ def add_new_columns(self, treeview, column_names): col = Gtk.TreeViewColumn() col.set_title(column_name.replace("_", "__")) cell_for_status = Gtk.CellRendererText() - col.pack_start(cell_for_status, False, True, 0) + col.pack_start(cell_for_status, False) col.set_cell_data_func(cell_for_status, self.set_tree_cell_status) self.add_cell_renderer_for_value(col, column_name) @@ -803,7 +803,7 @@ class StandardSummaryDataPanel(BaseSummaryDataPanel): def add_cell_renderer_for_value(self, col, col_title): """Add a CellRendererText for the column.""" cell_for_value = Gtk.CellRendererText() - col.pack_start(cell_for_value, True, True, 0) + col.pack_start(cell_for_value, True) col.set_cell_data_func(cell_for_value, self._set_tree_cell_value) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index 007ab9067..a53f6efe7 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -153,7 +153,7 @@ def add_cell_renderer_for_value(self, col, col_title): self._set_tree_cell_value_combo) elif col_title == self.INCLUDED_TITLE: cell_for_value = Gtk.CellRendererToggle() - col.pack_start(cell_for_value, False, True, 0) + col.pack_start(cell_for_value, False) cell_for_value.set_property("activatable", True) cell_for_value.connect("toggled", self._handle_cell_toggle_change) @@ -161,7 +161,7 @@ def add_cell_renderer_for_value(self, col, col_title): self._set_tree_cell_value_toggle) else: cell_for_value = Gtk.CellRendererText() - col.pack_start(cell_for_value, True, True, 0) + col.pack_start(cell_for_value, True) if (col_title not in [self.SECTION_INDEX_TITLE, self.DESCRIPTION_TITLE]): cell_for_value.set_property("editable", True) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index cfacf6460..04db4160b 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -105,7 +105,7 @@ def __init__(self, stash_lookup, request_lookup, self._should_show_meta_column_titles = False self.control_widget_hbox = self._get_control_widget_hbox() - self.pack_start(self.control_widget_hbox, expand=False, fill=False) + self.pack_start(self.control_widget_hbox, expand=False, fill=False, padding=0) self._view = metomi.rose.gtk.util.TooltipTreeView( get_tooltip_func=self.set_tree_tip) self._view.set_rules_hint(True) @@ -120,14 +120,14 @@ def __init__(self, stash_lookup, request_lookup, self.generate_tree_view(is_startup=True) self._window.add(self._view) self._window.show() - self.pack_start(self._window, expand=True, fill=True) + self.pack_start(self._window, expand=True, fill=True, padding=0) self._update_control_sensitivity() self.show() def add_cell_renderer_for_value(self, column): """Add a cell renderer to represent the model value.""" cell_for_value = Gtk.CellRendererText() - column.pack_start(cell_for_value, True, True, 0) + column.pack_start(cell_for_value, True) column.set_cell_data_func(cell_for_value, self._set_tree_cell_value) @@ -412,7 +412,7 @@ def _get_control_widget_hbox(self): group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() - self._group_widget.pack_start(cell, True, True, 0) + self._group_widget.pack_start(cell, True) self._group_widget.add_attribute(cell, 'text', 0) self._group_widget.show() self._add_button = metomi.rose.gtk.util.CustomButton( diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index 89161c943..0a15a5543 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -158,7 +158,7 @@ def get_stack_view(self, redo_mode_on=False): columns[title] = Gtk.TreeViewColumn() columns[title].set_title(title) cell_text[title] = Gtk.CellRendererText() - columns[title].pack_start(cell_text[title], True, True, 0) + columns[title].pack_start(cell_text[title], True) columns[title].add_attribute(cell_text[title], attribute='markup', column=len(list(columns.keys())) - 1) stack_view.append_column(columns[title]) diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 9a735b904..0c056721a 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -95,15 +95,15 @@ def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): self.console = None hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.show() - self.pack_start(hbox, expand=False, fill=False) + self.pack_start(hbox, expand=False, fill=False, padding=0) self._generate_error_widget() - hbox.pack_start(self._error_widget, expand=False, fill=False) + hbox.pack_start(self._error_widget, expand=False, fill=False, padding=0) vsep_message = Gtk.VSeparator() vsep_message.show() vsep_eb = Gtk.EventBox() vsep_eb.show() - hbox.pack_start(vsep_message, expand=False, fill=False) - hbox.pack_start(vsep_eb, expand=True, fill=True) + hbox.pack_start(vsep_message, expand=False, fill=False, padding=0) + hbox.pack_start(vsep_eb, expand=True, fill=True, padding=0) self._generate_message_widget() hbox.pack_end(self._message_widget, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) @@ -146,7 +146,7 @@ def _generate_error_widget(self): 'etc/images/rose-config-edit/error_icon.png') image = Gtk.Image.new_from_file(str(icon_path)) image.show() - self._error_widget.pack_start(image, expand=False, fill=False) + self._error_widget.pack_start(image, expand=False, fill=False, padding=0) self._error_widget_label = Gtk.Label() self._error_widget_label.show() self._error_widget.pack_start( @@ -181,10 +181,10 @@ def _generate_message_widget(self): self._console_launcher.connect("clicked", self._launch_console) message_hbox.pack_start( self._message_widget_error_image, - expand=False, fill=False) + expand=False, fill=False, padding=0) message_hbox.pack_start( self._message_widget_info_image, - expand=False, fill=False) + expand=False, fill=False, padding=0) message_hbox.pack_start( self._message_widget_label, expand=False, fill=False, @@ -193,7 +193,7 @@ def _generate_message_widget(self): vsep, expand=False, fill=False, padding=metomi.rose.config_editor.SPACING_SUB_PAGE) message_hbox.pack_start( - self._console_launcher, expand=False, fill=False) + self._console_launcher, expand=False, fill=False, padding=0) def _update_error_widget(self): # Update the error display widget. diff --git a/metomi/rose/config_editor/valuewidget/array/__init__.py b/metomi/rose/config_editor/valuewidget/array/__init__.py index ea1a3150a..9e6ad6922 100644 --- a/metomi/rose/config_editor/valuewidget/array/__init__.py +++ b/metomi/rose/config_editor/valuewidget/array/__init__.py @@ -17,3 +17,5 @@ # You should have received a copy of the GNU General Public License # along with Rose. If not, see . # ----------------------------------------------------------------------------- + +from . import python_list \ No newline at end of file diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 211a0ecd9..a9986c100 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -89,8 +89,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_entries(value_array) self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.entry_table.connect_after('size-allocate', lambda w, e: self.reshape_table()) self.connect('focus-in-event', @@ -204,8 +204,8 @@ def generate_buttons(self): right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) self.set_arrow_sensitive(False, False) del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) @@ -222,7 +222,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() @@ -237,9 +237,9 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False) + self.add_button, expand=False, fill=False, padding=0) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False) + self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() def _handle_arrow_enter(self, arrow_event_box, event): diff --git a/metomi/rose/config_editor/valuewidget/array/logical.py b/metomi/rose/config_editor/valuewidget/array/logical.py index a86c023b9..9d4ff79a5 100644 --- a/metomi/rose/config_editor/valuewidget/array/logical.py +++ b/metomi/rose/config_editor/valuewidget/array/logical.py @@ -87,8 +87,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_buttons() self.populate_table() - self.pack_start(self.button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.entry_table.connect_after('size-allocate', lambda w, e: self.reshape_table()) self.connect('focus-in-event', @@ -126,8 +126,8 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.button_box.show() - self.button_box.pack_start(self.add_button, expand=False, fill=False) - self.button_box.pack_start(self.del_button, expand=False, fill=False) + self.button_box.pack_start(self.add_button, expand=False, fill=False, padding=0) + self.button_box.pack_start(self.del_button, expand=False, fill=False, padding=0) def get_entry(self, value_item): """Create a widget for this array element.""" diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index c6d89ca3a..118eff63d 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -91,8 +91,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.insert_row(i) self.normalise_width_widgets() self.generate_buttons() - self.pack_start(self.add_del_button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.show() def set_num_rows(self): @@ -277,7 +277,7 @@ def insert_row(self, row_index): widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][i]) label.show() - widget.pack_start(label, expand=True, fill=True) + widget.pack_start(label, expand=True, fill=True, padding=0) else: widget = widget_cls(w_value, w_meta, setter.set_value, hook) if hover_text: @@ -367,9 +367,9 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False) + self.add_button, expand=False, fill=False, padding=0) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False) + self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() self._decide_show_buttons() diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 8da452b9c..ce073ecfc 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -69,8 +69,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.entry_table.connect_after('size-allocate', lambda w, e: self.reshape_table()) self.connect('focus-in-event', @@ -179,8 +179,8 @@ def generate_buttons(self): right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) self.set_arrow_sensitive(False, False) del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) @@ -197,7 +197,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() @@ -212,9 +212,9 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False) + self.add_button, expand=False, fill=False, padding=0) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False) + self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() def _handle_arrow_enter(self, arrow_event_box, event): @@ -312,7 +312,7 @@ def populate_table(self, focus_widget=None): widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() - widget.pack_start(label, expand=True, fill=True) + widget.pack_start(label, expand=True, fill=True, padding=0) widget.show() self.entry_table.attach(widget, col, col + 1, diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index a7855683a..6c07814b0 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -20,6 +20,7 @@ import re import sys +import math import gi gi.require_version('Gtk', '3.0') @@ -85,8 +86,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.insert_row(i) self.normalise_width_widgets() self.generate_buttons(is_for_elements=not isinstance(self.type, list)) - self.pack_start(self.add_del_button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.show() def set_num_rows(self): @@ -291,7 +292,7 @@ def insert_row(self, row_index): widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) eb0 = Gtk.EventBox() eb0.show() - widget.pack_start(eb0, expand=True, fill=True) + widget.pack_start(eb0, expand=True, fill=True, padding=0) widget.show() self.entry_table.attach(widget, i, i + 1, @@ -421,9 +422,9 @@ def generate_buttons(self, is_for_elements=False): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False) + self.add_button, expand=False, fill=False, padding=0) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False) + self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() self._decide_show_buttons() diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index 9b337fdf3..316212b8b 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -69,8 +69,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False) - self.pack_start(self.entry_table, expand=True, fill=True) + self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.entry_table.connect_after('size-allocate', lambda w, e: self.reshape_table()) self.connect('focus-in-event', @@ -170,8 +170,8 @@ def generate_buttons(self): right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False) + self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) self.set_arrow_sensitive(False, False) del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) @@ -188,7 +188,7 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True) + self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() @@ -203,9 +203,9 @@ def generate_buttons(self): lambda b, e: b.set_state(Gtk.StateType.NORMAL)) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False) + self.add_button, expand=False, fill=False, padding=0) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False) + self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() def _handle_arrow_enter(self, arrow_event_box, event): @@ -307,7 +307,7 @@ def populate_table(self, focus_widget=None): widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) label = Gtk.Label(label=self.metadata['element-titles'][col]) label.show() - widget.pack_start(label, expand=True, fill=True) + widget.pack_start(label, expand=True, fill=True, padding=0) widget.show() self.entry_table.attach(widget, col, col + 1, diff --git a/metomi/rose/config_editor/valuewidget/booltoggle.py b/metomi/rose/config_editor/valuewidget/booltoggle.py index 2c652591e..03b631f49 100644 --- a/metomi/rose/config_editor/valuewidget/booltoggle.py +++ b/metomi/rose/config_editor/valuewidget/booltoggle.py @@ -73,7 +73,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.button.set_image(bad_img) self.button.connect('toggled', self._switch_state_and_set) self.button.show() - self.pack_start(self.button, expand=False, fill=False) + self.pack_start(self.button, expand=False, fill=False, padding=0) self.grab_focus = lambda: self.hook.get_focus(self.button) self.button.connect('focus-in-event', self.hook.trigger_scroll) diff --git a/metomi/rose/config_editor/valuewidget/combobox.py b/metomi/rose/config_editor/valuewidget/combobox.py index 01658b542..0543af0a0 100644 --- a/metomi/rose/config_editor/valuewidget/combobox.py +++ b/metomi/rose/config_editor/valuewidget/combobox.py @@ -46,7 +46,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): liststore = Gtk.ListStore(str) cell = Gtk.CellRendererText() cell.xalign = self.FRAC_X_ALIGN - comboboxentry.pack_start(cell, True, True, 0) + comboboxentry.pack_start(cell, True) comboboxentry.add_attribute(cell, 'text', 0) var_values = self.metadata[metomi.rose.META_PROP_VALUES] diff --git a/metomi/rose/config_editor/valuewidget/files.py b/metomi/rose/config_editor/valuewidget/files.py index 4bf85e6a2..591acb763 100644 --- a/metomi/rose/config_editor/valuewidget/files.py +++ b/metomi/rose/config_editor/valuewidget/files.py @@ -49,7 +49,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): tip_text="Browse for a filename") self.open_button.show() self.open_button.connect("clicked", self.run_and_destroy) - self.pack_end(self.open_button, expand=False, fill=False) + self.pack_end(self.open_button, expand=False, fill=False, padding=0) self.edit_button.set_sensitive(os.path.isfile(self.value)) def generate_entry(self): @@ -86,7 +86,7 @@ def generate_editor_launcher(self): self.edit_button.connect( "clicked", lambda b: metomi.rose.external.launch_geditor(self.value)) - self.pack_end(self.edit_button, expand=False, fill=False) + self.pack_end(self.edit_button, expand=False, fill=False, padding=0) def setter(self, widget): self.value = widget.get_text() diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 2844d5db7..8d9a3f67a 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -114,7 +114,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.grab_focus = lambda: self.hook.get_focus(self.entry) self.entrybuffer.connect('changed', self.setter) - self.pack_start(viewport, expand=True, fill=True) + self.pack_start(viewport, expand=True, fill=True, padding=0) def get_focus_index(self): """Return the cursor position within the variable value.""" diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 59a9c3781..730463573 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -117,8 +117,8 @@ def generate_labelwidget(self): label_content_hbox.show() event_box = Gtk.EventBox() event_box.show() - self.labelwidget.pack_start(label_content_hbox, expand=True, fill=True) - self.labelwidget.pack_start(event_box, expand=True, fill=True) + self.labelwidget.pack_start(label_content_hbox, expand=True, fill=True, padding=0) + self.labelwidget.pack_start(event_box, expand=True, fill=True, padding=0) def generate_contentwidget(self): """Create the content widget, a vbox-packed valuewidget.""" @@ -127,9 +127,9 @@ def generate_contentwidget(self): content_event_box = Gtk.EventBox() content_event_box.show() self.contentwidget.pack_start( - self.valuewidget, expand=False, fill=False) + self.valuewidget, expand=False, fill=False, padding=0) self.contentwidget.pack_start( - content_event_box, expand=True, fill=True) + content_event_box, expand=True, fill=True, padding=0) def _valuewidget_set_value(self, value): # This is called by a valuewidget to change the variable value. diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index f5ebd9b0b..02380b99a 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -125,7 +125,7 @@ def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, notebook.connect_after(signal, page_change_func) self.generate_main_hbox(nav_panel, notebook) self.top_vbox.pack_start(self.main_hbox, True, True, 0) - self.top_vbox.pack_start(status_bar, expand=False, fill=False) + self.top_vbox.pack_start(status_bar, expand=False, fill=False, padding=0) self.top_vbox.show() self.window.show() nav_panel.tree.columns_autosize() @@ -338,11 +338,11 @@ def _launch_choose_section_dialog( name_section_dict[name_keys[c.get_active()]], prefs.get(name_keys[c.get_active()], []))) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=metomi.rose.config_editor.SPACING_PAGE) - vbox.pack_start(config_label, expand=False, fill=False) - vbox.pack_start(config_name_box, expand=False, fill=False) - vbox.pack_start(section_label, expand=False, fill=False) - vbox.pack_start(null_section_checkbutton, expand=False, fill=False) - vbox.pack_start(section_box, expand=False, fill=False) + vbox.pack_start(config_label, expand=False, fill=False, padding=0) + vbox.pack_start(config_name_box, expand=False, fill=False, padding=0) + vbox.pack_start(section_label, expand=False, fill=False, padding=0) + vbox.pack_start(null_section_checkbutton, expand=False, fill=False, padding=0) + vbox.pack_start(section_box, expand=False, fill=False, padding=0) if do_target_section: target_section_entry = Gtk.Entry() self._reload_target_section_entry( @@ -358,7 +358,7 @@ def _launch_choose_section_dialog( ) ) target_section_entry.show() - vbox.pack_start(target_section_entry, expand=False, fill=False) + vbox.pack_start(target_section_entry, expand=False, fill=False, padding=0) vbox.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(vbox, expand=True, fill=True, @@ -406,7 +406,7 @@ def _reload_section_choices(self, vbox, sections, prefs): if section_chooser.get_active() == -1 and sections: section_chooser.set_active(0) section_chooser.show() - vbox.pack_start(section_chooser, expand=False, fill=False) + vbox.pack_start(section_chooser, expand=False, fill=False, padding=0) return section_chooser def _reload_target_section_entry(self, section_combo_box, target_entry, @@ -664,9 +664,9 @@ def __init__(self, window, config_name, macro_name, mode, search_func): column.set_title(title) cell = Gtk.CellRendererText() if i == len(self.COLUMNS) - 1: - column.pack_start(cell, True, True, 0) + column.pack_start(cell, True) else: - column.pack_start(cell, False, True, 0) + column.pack_start(cell, False) if title == "Type": column.set_cell_data_func(cell, self._set_type_markup, i) else: diff --git a/metomi/rose/gtk/choice.py b/metomi/rose/gtk/choice.py index 0f2d0e2c8..51fd2bfeb 100644 --- a/metomi/rose/gtk/choice.py +++ b/metomi/rose/gtk/choice.py @@ -75,7 +75,7 @@ def __init__(self, set_value, get_data, handle_search, cell_text = Gtk.CellRendererText() cell_text.set_property('editable', True) cell_text.connect('edited', self._handle_edited) - col.pack_start(cell_text, True, True, 0) + col.pack_start(cell_text, True) col.set_cell_data_func(cell_text, self._set_cell_text, None) self.append_column(col) self._populate() @@ -260,12 +260,13 @@ def __init__(self, set_value, get_data, get_available_data, col = Gtk.TreeViewColumn() cell_toggle = Gtk.CellRendererToggle() cell_toggle.connect_after("toggled", self._handle_cell_toggle) + col.pack_start(cell_toggle, False) col.set_cell_data_func(cell_toggle, self._set_cell_state, None) self.append_column(col) col = Gtk.TreeViewColumn() col.set_title(title) cell_text = Gtk.CellRendererText() - col.pack_start(cell_text, True, True, 0) + col.pack_start(cell_text, True) col.set_cell_data_func(cell_text, self._set_cell_text, None) self.append_column(col) self.set_expander_column(col) diff --git a/metomi/rose/gtk/console.py b/metomi/rose/gtk/console.py index 36a3cda8c..222e443ca 100644 --- a/metomi/rose/gtk/console.py +++ b/metomi/rose/gtk/console.py @@ -71,7 +71,7 @@ def __init__(self, categories, category_message_time_tuples, category_column = Gtk.TreeViewColumn() category_column.set_title(self.COLUMN_TITLE_CATEGORY) cell_category = Gtk.CellRendererPixbuf() - category_column.pack_start(cell_category, False, True, 0) + category_column.pack_start(cell_category, False) category_column.set_cell_data_func(cell_category, self._set_category_cell, 0) category_column.set_clickable(True) @@ -82,7 +82,7 @@ def __init__(self, categories, category_message_time_tuples, message_column = Gtk.TreeViewColumn() message_column.set_title(self.COLUMN_TITLE_MESSAGE) cell_message = Gtk.CellRendererText() - message_column.pack_start(cell_message, False, True, 0) + message_column.pack_start(cell_message, False) message_column.add_attribute(cell_message, attribute="text", column=1) message_column.set_clickable(True) @@ -93,7 +93,7 @@ def __init__(self, categories, category_message_time_tuples, time_column = Gtk.TreeViewColumn() time_column.set_title(self.COLUMN_TITLE_TIME) cell_time = Gtk.CellRendererText() - time_column.pack_start(cell_time, False, True, 0) + time_column.pack_start(cell_time, False) time_column.set_cell_data_func(cell_time, self._set_time_cell, 2) time_column.set_clickable(True) time_column.set_sort_indicator(True) diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 4638d6313..9ae72f2d7 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -304,7 +304,7 @@ def run_command_arg_dialog(cmd_name, help_text, run_hook): help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) help_hbox.pack_start(help_button, expand=False, fill=False, padding=0) help_hbox.show() - container.pack_end(help_hbox, expand=False, fill=False) + container.pack_end(help_hbox, expand=False, fill=False, padding=0) name_entry.grab_focus() dialog.connect("response", _handle_command_arg_response, run_hook, name_entry) @@ -513,7 +513,7 @@ def run_scrolled_dialog(text, title=None): button.show() button.grab_focus() button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - button_box.pack_end(button, expand=False, fill=False) + button_box.pack_end(button, expand=False, fill=False, padding=0) button_box.show() main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=DIALOG_SUB_PADDING) main_vbox.pack_start(scrolled, expand=True, fill=True, padding=0) diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index e6a552efd..a73a91831 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -63,7 +63,7 @@ class CustomButton(Gtk.Button): def __init__(self, label=None, stock_id=None, size=Gtk.IconSize.SMALL_TOOLBAR, tip_text=None, as_tool=False, icon_at_start=False, has_menu=False): - self.hbox = Gtk.HBox() + self.hbox = Gtk.Box() self.size = size self.as_tool = as_tool self.icon_at_start = icon_at_start @@ -87,14 +87,17 @@ def __init__(self, label=None, stock_id=None, self.icon.set_from_icon_name(stock_id, size) self.icon.show() if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False) + self.hbox.pack_start(self.icon, expand=False, fill=False, + padding=0) else: - self.hbox.pack_end(self.icon, expand=False, fill=False) + self.hbox.pack_end(self.icon, expand=False, fill=False, + padding=0) if has_menu: # not sure if this is correct arrow = Gtk.Image.new_from_icon_name("pan-down-symbolic", size) arrow.show() - self.hbox.pack_end(arrow, expand=False, fill=False) + self.hbox.pack_end(arrow, expand=False, fill=False, + padding=0) self.hbox.reorder_child(arrow, 0) self.hbox.show() super(CustomButton, self).__init__() From 45034993e94b779afc7dd7247675f5858c62be83 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 13 Aug 2024 13:08:16 +0100 Subject: [PATCH 16/42] Removes Gtk.Arrow usage --- .../config_editor/valuewidget/array/entry.py | 26 +++++------------- .../valuewidget/array/python_list.py | 26 +++++------------- .../valuewidget/array/spaced_list.py | 27 +++++-------------- 3 files changed, 21 insertions(+), 58 deletions(-) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index a9986c100..ffac5170b 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -182,25 +182,21 @@ def generate_entries(self, value_array=None): def generate_buttons(self): """Create the left-right movement arrows and add button.""" - left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow = Gtk.ToolButton() + left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() + left_arrow.connect('clicked', lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() - left_event_box.connect('button-press-event', - lambda b, e: self.move_element(-1)) - left_event_box.connect('enter-notify-event', self._handle_arrow_enter) - left_event_box.connect('leave-notify-event', self._handle_arrow_leave) left_event_box.set_tooltip_text(self.TIP_LEFT) - right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow = Gtk.ToolButton() + right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() + right_arrow.connect('clicked', lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() - right_event_box.show() right_event_box.add(right_arrow) - right_event_box.connect( - 'button-press-event', lambda b, e: self.move_element(1)) - right_event_box.connect('enter-notify-event', self._handle_arrow_enter) - right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() @@ -242,14 +238,6 @@ def generate_buttons(self): self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() - def _handle_arrow_enter(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.ACTIVE) - - def _handle_arrow_leave(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.NORMAL) - def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): """Control the sensitivity of the movement buttons.""" sens_tuple = (is_left_sensitive, is_right_sensitive) diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index ce073ecfc..d24456ea2 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -157,25 +157,21 @@ def generate_entries(self, value_array=None): def generate_buttons(self): """Create the left-right movement arrows and add button.""" - left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow = Gtk.ToolButton() + left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() + left_arrow.connect('clicked', lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() - left_event_box.connect('button-press-event', - lambda b, e: self.move_element(-1)) - left_event_box.connect('enter-notify-event', self._handle_arrow_enter) - left_event_box.connect('leave-notify-event', self._handle_arrow_leave) left_event_box.set_tooltip_text(self.TIP_LEFT) - right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow = Gtk.ToolButton() + right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() + right_arrow.connect('clicked', lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() - right_event_box.show() right_event_box.add(right_arrow) - right_event_box.connect( - 'button-press-event', lambda b, e: self.move_element(1)) - right_event_box.connect('enter-notify-event', self._handle_arrow_enter) - right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() @@ -217,14 +213,6 @@ def generate_buttons(self): self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() - def _handle_arrow_enter(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.ACTIVE) - - def _handle_arrow_leave(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.NORMAL) - def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): """Control the sensitivity of the movement buttons.""" sens_tuple = (is_left_sensitive, is_right_sensitive) diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index 316212b8b..81faf19bf 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -147,26 +147,21 @@ def generate_entries(self, value_array=None): def generate_buttons(self): """Create the left-right movement arrows and add button.""" - left_arrow = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.IN) + left_arrow = Gtk.ToolButton() + left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() + left_arrow.connect('clicked', lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() - left_event_box.connect('button-press-event', - lambda b, e: self.move_element(-1)) - left_event_box.connect('enter-notify-event', self._handle_arrow_enter) - left_event_box.connect('leave-notify-event', self._handle_arrow_leave) left_event_box.set_tooltip_text(self.TIP_LEFT) - right_arrow = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.IN) + right_arrow = Gtk.ToolButton() + right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() + right_arrow.connect('clicked', lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() - right_event_box.show() right_event_box.add(right_arrow) - right_event_box.connect( - 'button-press-event', - lambda b, e: self.move_element(1)) - right_event_box.connect('enter-notify-event', self._handle_arrow_enter) - right_event_box.connect('leave-notify-event', self._handle_arrow_leave) + right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() @@ -208,14 +203,6 @@ def generate_buttons(self): self.del_button, expand=False, fill=False, padding=0) self.add_del_button_box.show() - def _handle_arrow_enter(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.ACTIVE) - - def _handle_arrow_leave(self, arrow_event_box, event): - if arrow_event_box.get_child().state != Gtk.StateType.INSENSITIVE: - arrow_event_box.set_state(Gtk.StateType.NORMAL) - def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): """Control the sensitivity of the movement buttons.""" sens_tuple = (is_left_sensitive, is_right_sensitive) From 933a58520ccaf995b0385e6f713b9a31aaadff11 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 14 Aug 2024 10:57:19 +0100 Subject: [PATCH 17/42] Fixes focussing issues on links and widgets --- metomi/rose/config_editor/page.py | 6 +++--- metomi/rose/config_editor/pagewidget/table.py | 2 +- metomi/rose/config_editor/valuewidget/__init__.py | 10 +++++++++- metomi/rose/config_editor/valuewidget/array/entry.py | 2 +- metomi/rose/config_editor/valuewidget/array/mixed.py | 6 +++--- .../config_editor/valuewidget/array/python_list.py | 2 +- metomi/rose/config_editor/valuewidget/array/row.py | 6 +++--- .../config_editor/valuewidget/array/spaced_list.py | 2 +- metomi/rose/config_editor/variable.py | 6 +++--- 9 files changed, 25 insertions(+), 17 deletions(-) diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 72d742192..923b1e071 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -301,7 +301,7 @@ def trigger_tab_detach(self, widget=None): def reshuffle_for_detached(self, add_button, revert_button, parent): """Reshuffle widgets for detached view.""" - focus_child = getattr(self, 'focus_child') + focus_child = self.get_focus_child() button_hbox = Gtk.Box(homogeneous=False, spacing=0) self.tool_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0) sep = Gtk.VSeparator() @@ -792,7 +792,7 @@ def sort_main(self, column_index=0, ascending=True, """ if self.sort_data(column_index, ascending) or remake_forced: focus_var = None - focus_widget = self.get_toplevel().focus_child + focus_widget = self.get_toplevel().get_focus_child() if (focus_widget is not None and hasattr(focus_widget.get_parent(), 'variable')): focus_var = focus_widget.get_parent().variable @@ -831,7 +831,7 @@ def get_widgets_with_attribute(self, att_name, parent_widget=None): def get_main_focus(self): """Retrieve the focus variable widget id.""" widget_list = self.get_main_variable_widgets() - focus_child = getattr(self.main_container, "focus_child") + focus_child = self.main_container.get_focus_child() for widget in widget_list: if focus_child == widget: if hasattr(widget.get_parent(), 'variable'): diff --git a/metomi/rose/config_editor/pagewidget/table.py b/metomi/rose/config_editor/pagewidget/table.py index a80ad59a5..fa887beb2 100644 --- a/metomi/rose/config_editor/pagewidget/table.py +++ b/metomi/rose/config_editor/pagewidget/table.py @@ -132,7 +132,7 @@ def reload_variable_widget(self, variable): variable.metadata.get('id')): if "index" not in focus_dict: focus_dict["index"] = variable_widget.get_focus_index() - if getattr(self, 'focus_child') == child: + if self.get_focus_child() == child: focus_dict["had_focus"] = True top_row = self.child_get(child, 'top_attach')[0] variable_row = top_row diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py index 887e82d86..cd0afdc65 100644 --- a/metomi/rose/config_editor/valuewidget/__init__.py +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -18,6 +18,10 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + import re import metomi.rose @@ -55,7 +59,11 @@ def trigger_scroll(self, widget, event): def get_focus(self, widget): """Set up a trigger based on focusing for a widget.""" if self._focus_func is None: - return widget.grab_focus() + if isinstance(widget, Gtk.Entry): + Gtk.Widget.grab_focus(widget) + return + Gtk.Widget.grab_focus(widget) + return return self._focus_func(widget) def copy(self): diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index ffac5170b..1433f70f1 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -139,7 +139,7 @@ def get_focus_index(self): prefix = get_next_delimiter(self.value[len(text):], val) if prefix is None: return None - if entry == self.entry_table.focus_child: + if entry == self.entry_table.get_focus_child(): return len(text + prefix) + entry.get_position() text += prefix + val return None diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index 118eff63d..e3376673d 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -126,10 +126,10 @@ def set_num_rows(self): self.num_rows += 1 def grab_focus(self): - if self.entry_table.focus_child is None: + if self.entry_table.get_focus_child() is None: self.hook.get_focus(self.rows[-1][-1]) else: - self.hook.get_focus(self.entry_table.focus_child) + self.hook.get_focus(self.entry_table.get_focus_child()) def add_row(self, *args): """Create a new row of widgets.""" @@ -160,7 +160,7 @@ def get_focus_index(self): val) if prefix_text is None: return - if widget == self.entry_table.focus_child: + if widget == self.entry_table.get_focus_child(): if hasattr(widget, "get_focus_index"): position = widget.get_focus_index() return len(text + prefix_text) + position diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index d24456ea2..19b0dd2ff 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -114,7 +114,7 @@ def get_focus_index(self): prefix = entry.get_next_delimiter(self.value[len(text):], val) if prefix is None: return - if my_entry == self.entry_table.focus_child: + if my_entry == self.entry_table.get_focus_child(): return len(text + prefix) + my_entry.get_position() text += prefix + val return None diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index 6c07814b0..4af123856 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -134,10 +134,10 @@ def get_types(self): return [self.type] * self.num_cols def grab_focus(self): - if self.entry_table.focus_child is None: + if self.entry_table.get_focus_child() is None: self.hook.get_focus(self.rows[-1][-1]) else: - self.hook.get_focus(self.entry_table.focus_child) + self.hook.get_focus(self.entry_table.get_focus_child()) def add_element(self, *args): """Create a new element (non-derived types).""" @@ -180,7 +180,7 @@ def get_focus_index(self): val) if prefix_text is None: return - if widget == self.entry_table.focus_child: + if widget == self.entry_table.get_focus_child(): if hasattr(widget, "get_focus_index"): position = widget.get_focus_index() return len(text + prefix_text) + position diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index 81faf19bf..23d84c0f6 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -112,7 +112,7 @@ def get_focus_index(self): prefix = get_next_delimiter(self.value[len(text):], val) if prefix is None: return - if my_entry == self.entry_table.focus_child: + if my_entry == self.entry_table.get_focus_child(): return len(text + prefix) + my_entry.get_position() text += prefix + val return None diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 730463573..191c8917c 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -147,8 +147,7 @@ def generate_valuewidget(self, variable, override_custom=False, custom_arg = self.var_ops set_value = self._valuewidget_set_value hook_object = metomi.rose.config_editor.valuewidget.ValueWidgetHook( - metomi.rose.config_editor.false_function, - self._get_focus) + metomi.rose.config_editor.false_function) metadata = copy.deepcopy(variable.metadata) if use_this_valuewidget is not None: self.valuewidget = use_this_valuewidget(variable.value, @@ -395,7 +394,7 @@ def grab_focus(self, focus_container=None, scroll_bottom=False, if (self.valuewidget.get_sensitive() & child.get_state_flags() and self.valuewidget.get_parent().get_sensitive() & child.get_state_flags()): break - else: + else: # no break if hasattr(self, 'menuwidget'): self.menuwidget.get_children()[0].grab_focus() if scroll_bottom and focus_container is not None: @@ -496,6 +495,7 @@ def _get_focus(self, widget_for_focus): widget_for_focus.grab_focus() self.valuewidget.trigger_scroll(widget_for_focus, None) if isinstance(widget_for_focus, Gtk.Entry): + widget_for_focus.grab_focus_without_selecting() text_length = len(widget_for_focus.get_text()) if text_length > 0: widget_for_focus.set_position(text_length) From 43f8c431dc950ca6de78b2edad07d995c9f0a7f5 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 14 Aug 2024 11:51:38 +0100 Subject: [PATCH 18/42] Fixes log level for plain text messages --- metomi/rose/config_editor/status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 0c056721a..00f00f73a 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -116,8 +116,9 @@ def set_message(self, message, kind=None, level=None): kind = message.kind if level is None: level = message.level - if level > self.verbosity: - return + if level != None: + if level > self.verbosity: + return if isinstance(message, Exception): kind = metomi.rose.reporter.Reporter.KIND_ERR level = metomi.rose.reporter.Reporter.FAIL From f712557cec691d8c50f6807dc45e3368657d8d15 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 14 Aug 2024 16:50:39 +0100 Subject: [PATCH 19/42] Imports submodules in the valuewidget array __init__ file --- metomi/rose/config_editor/valuewidget/array/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metomi/rose/config_editor/valuewidget/array/__init__.py b/metomi/rose/config_editor/valuewidget/array/__init__.py index 9e6ad6922..a28b4b25b 100644 --- a/metomi/rose/config_editor/valuewidget/array/__init__.py +++ b/metomi/rose/config_editor/valuewidget/array/__init__.py @@ -18,4 +18,7 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- -from . import python_list \ No newline at end of file +from . import python_list +from . import logical +from . import mixed +from . import spaced_list \ No newline at end of file From ef5b973a66679ffe8ec9fa547d8bdb31d13b050f Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 14 Aug 2024 16:52:51 +0100 Subject: [PATCH 20/42] Fixes the maths in the array widget calculating the row for multirow arrays --- metomi/rose/config_editor/valuewidget/array/mixed.py | 3 ++- metomi/rose/config_editor/valuewidget/array/row.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index e3376673d..32542cbc4 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -20,6 +20,7 @@ import re import sys +import math import gi gi.require_version('Gtk', '3.0') @@ -375,7 +376,7 @@ def generate_buttons(self): def setter(self, array_index, element_value): """Update the value.""" - widget_row = self.rows[array_index / self.num_cols] + widget_row = self.rows[math.floor(array_index / self.num_cols)] widget = widget_row[array_index % self.num_cols] self._normalise_width_chars(widget) i = array_index - len(self.value_array) diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index 4af123856..7ba627f54 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -431,7 +431,7 @@ def generate_buttons(self, is_for_elements=False): def setter(self, array_index, element_value): """Update the value.""" actual_num_cols = len(self.get_types()) - widget_row = self.rows[array_index / actual_num_cols] + widget_row = self.rows[math.floor(array_index / actual_num_cols)] widget = widget_row[array_index % actual_num_cols] self._normalise_width_chars(widget) i = array_index - len(self.value_array) From 2b131ce8ba7312c4ff7973c5b75f82464f7e602d Mon Sep 17 00:00:00 2001 From: J-J-Abram <98320699+J-J-Abram@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:47:00 +0100 Subject: [PATCH 21/42] Removes Clyc related code * Removed 'Run Suite...' Button * Removed Launch Suite Control _GUI from the 'Tools' dropdown, but Launch Suite Control GUI still exists in the files, need to investigate what this controls in the GUI * Remove 'Launch Suite Control GUI' button with no icon * Removed 'View Output' Button from taskbar * removed comments related to 'View Output' Button * Removed all 'Run Suite' functionality from the 'Tools' dropdown menu * Removed remaining comments related to Run Suite * 'View Output' removed from 'Tools' dropdown menu --- metomi/rose/config_editor/__init__.py | 9 ------ metomi/rose/config_editor/main.py | 45 --------------------------- metomi/rose/config_editor/menu.py | 20 ------------ 3 files changed, 74 deletions(-) diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index 48dbb0896..dc33005f9 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -70,7 +70,6 @@ ACCEL_FIND_NEXT = "G" ACCEL_METADATA_REFRESH = "F5" ACCEL_BROWSER = "B" -ACCEL_SUITE_RUN = "R" ACCEL_TERMINAL = "T" ACCEL_HELP_GUI = "F1" ACCEL_REMOVE = "Delete" @@ -139,9 +138,6 @@ TOP_MENU_METADATA_UPGRADE = "_Upgrade..." TOP_MENU_TOOLS = "_Tools" TOP_MENU_TOOLS_BROWSER = "Launch _File Browser" -TOP_MENU_TOOLS_SUITE_RUN = "_Run Suite" -TOP_MENU_TOOLS_SUITE_RUN_CUSTOM = "_Custom ..." -TOP_MENU_TOOLS_SUITE_RUN_DEFAULT = "_Default" TOP_MENU_TOOLS_TERMINAL = "Launch _Terminal" TOP_MENU_TOOLS_VIEW_OUTPUT = "View _Output" TOP_MENU_HELP = "_Help" @@ -159,13 +155,8 @@ TOOLBAR_REVERT = "Revert page to saved" TOOLBAR_FIND = "Find expression (regex)" TOOLBAR_FIND_NEXT = "Find next" -TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL = "Launch Suite Control _GUI" TOOLBAR_TRANSFORM = "Auto-fix configurations (run built-in transform macros)" TOOLBAR_VALIDATE = "Check fail-if, warn-if, and run all validator macros" -TOOLBAR_SUITE_GCONTROL = "Launch Suite Control GUI" -TOOLBAR_SUITE_RUN = "Run suite" -TOOLBAR_SUITE_RUN_MENU = "Run suite ..." -TOOLBAR_VIEW_OUTPUT = "View Output" TREE_PANEL_TITLE = "Index" TREE_PANEL_ADD_GENERIC = "_Add a new section..." TREE_PANEL_ADD_SECTION = "_Add {0}" diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index aa517ef2f..81dcf620d 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -344,10 +344,6 @@ def generate_toolbar(self): "dialog-question"), (metomi.rose.config_editor.TOOLBAR_TRANSFORM, 'Gtk.STOCK_CONVERT'), - (metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, - 'Gtk.STOCK_DIRECTORY'), - (metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, - 'rose-gtk-scheduler') ], sep_on_name=[ metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, @@ -374,10 +370,6 @@ def generate_toolbar(self): self.main_handle.check_all_extra) assign(metomi.rose.config_editor.TOOLBAR_TRANSFORM, self.main_handle.transform_default) - assign(metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, - self.main_handle.launch_output_viewer) - assign(metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, - self.main_handle.launch_scheduler) self.find_entry = self.toolbar.item_dict.get( metomi.rose.config_editor.TOOLBAR_FIND)['widget'] self.find_entry.connect("activate", self._launch_find) @@ -385,24 +377,6 @@ def generate_toolbar(self): add_icon = self.toolbar.item_dict.get( metomi.rose.config_editor.TOOLBAR_ADD)['widget'] add_icon.connect('button_press_event', self.add_page_variable) - custom_text = metomi.rose.config_editor.TOOLBAR_SUITE_RUN_MENU - self._toolbar_run_button = metomi.rose.gtk.util.CustomMenuButton( - stock_id=Gtk.STOCK_MEDIA_PLAY, - menu_items=[(custom_text, Gtk.STOCK_MEDIA_PLAY)], - menu_funcs=[self.main_handle.get_run_suite_args], - tip_text=metomi.rose.config_editor.TOOLBAR_SUITE_RUN) - self._toolbar_run_button.connect("clicked", self.main_handle.run_suite) - self.toolbar.insert(self._toolbar_run_button, -1) - - self.toolbar.set_widget_sensitive( - metomi.rose.config_editor.TOOLBAR_SUITE_GCONTROL, - any(c.config_type == metomi.rose.TOP_CONFIG_NAME - for c in list(self.data.config.values()))) - - self.toolbar.set_widget_sensitive( - metomi.rose.config_editor.TOOLBAR_VIEW_OUTPUT, - any(c.config_type == metomi.rose.TOP_CONFIG_NAME - for c in list(self.data.config.values()))) def generate_menubar(self): """Link in the menu functionality and accelerators.""" @@ -510,18 +484,10 @@ def generate_menubar(self): lambda m: self.refresh_metadata(m.get_active())), ('/TopMenuBar/Metadata/Upgrade', lambda m: self.main_handle.handle_upgrade()), - ('/TopMenuBar/Tools/Run Suite/Run Suite default', - self.main_handle.run_suite), - ('/TopMenuBar/Tools/Run Suite/Run Suite custom', - self.main_handle.get_run_suite_args), ('/TopMenuBar/Tools/Browser', lambda m: self.main_handle.launch_browser()), ('/TopMenuBar/Tools/Terminal', lambda m: self.main_handle.launch_terminal()), - ('/TopMenuBar/Tools/View Output', - lambda m: self.main_handle.launch_output_viewer()), - ('/TopMenuBar/Tools/Open Suite GControl', - lambda m: self.main_handle.launch_scheduler()), ('/TopMenuBar/Page/Revert', lambda m: self.revert_to_saved_data()), ('/TopMenuBar/Page/Page Info', @@ -598,10 +564,6 @@ def generate_menubar(self): add_menuitem )) self.main_handle.load_macro_menu(self.menubar) - if not any(c.config_type == metomi.rose.TOP_CONFIG_NAME - for c in list(self.data.config.values())): - self.menubar.uimanager.get_widget( - "/TopMenuBar/Tools/Run Suite").set_sensitive(False) self.update_bar_widgets() self.top_menu = self.menubar.uimanager.get_widget('/TopMenuBar') # Load the keyboard accelerators. @@ -624,8 +586,6 @@ def generate_menubar(self): self.main_handle.destroy, metomi.rose.config_editor.ACCEL_METADATA_REFRESH: self._refresh_metadata_if_on, - metomi.rose.config_editor.ACCEL_SUITE_RUN: - self.main_handle.run_suite, metomi.rose.config_editor.ACCEL_BROWSER: self.main_handle.launch_browser, metomi.rose.config_editor.ACCEL_TERMINAL: @@ -1416,11 +1376,6 @@ def _update_changed_sensitivity(self, is_changed=False): self._get_menu_widget('/Save').set_sensitive(is_changed) self._get_menu_widget('/Check and save').set_sensitive(is_changed) self._get_menu_widget('/Graph').set_sensitive(not is_changed) - self._toolbar_run_button.set_sensitive(not is_changed) - self._get_menu_widget('/Run Suite custom').set_sensitive( - not is_changed) - self._get_menu_widget('/Run Suite default').set_sensitive( - not is_changed) def _refresh_metadata_if_on(self, config_name=None): """Reload any metadata, if present - otherwise do nothing.""" diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 5f0e5484f..f16d0b30d 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -39,8 +39,6 @@ import metomi.rose.macro import metomi.rose.macros import metomi.rose.popen -# import metomi.rose.suite_control -# import metomi.rose.suite_engine_proc class MenuBar(object): @@ -109,15 +107,8 @@ class MenuBar(object): - - - - - - - @@ -204,23 +195,12 @@ class MenuBar(object): metomi.rose.config_editor.TOP_MENU_METADATA_GRAPH), ('Tools', None, metomi.rose.config_editor.TOP_MENU_TOOLS), - ('Run Suite', Gtk.STOCK_MEDIA_PLAY, - metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN), - ('Run Suite default', Gtk.STOCK_MEDIA_PLAY, - metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_DEFAULT, - metomi.rose.config_editor.ACCEL_SUITE_RUN), - ('Run Suite custom', Gtk.STOCK_EDIT, - metomi.rose.config_editor.TOP_MENU_TOOLS_SUITE_RUN_CUSTOM), ('Browser', Gtk.STOCK_DIRECTORY, metomi.rose.config_editor.TOP_MENU_TOOLS_BROWSER, metomi.rose.config_editor.ACCEL_BROWSER), ('Terminal', Gtk.STOCK_EXECUTE, metomi.rose.config_editor.TOP_MENU_TOOLS_TERMINAL, metomi.rose.config_editor.ACCEL_TERMINAL), - ('View Output', Gtk.STOCK_DIRECTORY, - metomi.rose.config_editor.TOP_MENU_TOOLS_VIEW_OUTPUT), - ('Open Suite GControl', "rose-gtk-scheduler", - metomi.rose.config_editor.TOP_MENU_TOOLS_OPEN_SUITE_GCONTROL), ('Help', None, metomi.rose.config_editor.TOP_MENU_HELP), ('Documentation', Gtk.STOCK_HELP, From 57a77bda77c66046d074c0368e6b8fb081b1558b Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 19 Aug 2024 11:17:41 +0100 Subject: [PATCH 22/42] Fixes credit/about dialog --- metomi/rose/config_editor/__init__.py | 9 ++++++++- metomi/rose/config_editor/window.py | 3 ++- metomi/rose/gtk/dialog.py | 18 ++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index dc33005f9..2b8590f4a 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -701,7 +701,14 @@ # Miscellaneous COPYRIGHT = ( - "Copyright (C) 2012-2020 British Crown (Met Office) & Contributors.") + """Copyright (C) British Crown (Met Office) & Contributors. + For full terms of use and licenses visit the Rose link above.""") +ABOUT_TEXT = "GUI interface to edit rose suites." +CREDIT = ( + ["Ben Fitzpatrick", ["Principal Developer"]], + ["Dimitrios Theodorakis", ["Migration to Rose 2.0"]], + ["Joseph Abram", ["Migration to Rose 2.0"]] +) HELP_FILE = "rose-rug-config-edit.html" LAUNCH_COMMAND = "rose config-edit" LAUNCH_COMMAND_CONFIG = "rose config-edit -C" diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index 02380b99a..c8f770737 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -150,7 +150,8 @@ def launch_about_dialog(self, somewidget=None): name=metomi.rose.config_editor.PROGRAM_NAME, copyright_=metomi.rose.config_editor.COPYRIGHT, logo_path="etc/images/rose-logo.png", - website=metomi.rose.config_editor.PROJECT_URL) + website=metomi.rose.config_editor.PROJECT_URL, + website_label=metomi.rose.config_editor.PROJECT_URL) def _reload_choices(self, liststore, top_name, add_choices): liststore.clear() diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index 9ae72f2d7..b01e86dd9 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -268,23 +268,21 @@ def _process(cmd_args, stdout=sys.stdout, stderr=sys.stderr): def run_about_dialog(name=None, copyright_=None, - logo_path=None, website=None): + logo_path=None, website=None, website_label=None): parent_window = get_dialog_parent() about_dialog = Gtk.AboutDialog() about_dialog.set_transient_for(parent_window) - about_dialog.set_name(name) - licence_path = os.path.join(os.getenv("ROSE_HOME"), - metomi.rose.FILEPATH_README) - about_dialog.set_license(open(licence_path, "r").read()) + about_dialog.set_program_name(name) about_dialog.set_copyright(copyright_) resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) logo_path = resource_loc.locate(logo_path) - about_dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file(logo_path)) + about_dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file(str(logo_path))) about_dialog.set_website(website) - Gtk.about_dialog_set_url_hook( - lambda u, v, w: webbrowser.open(w), about_dialog.get_website()) - about_dialog.run() - about_dialog.destroy() + about_dialog.set_website_label(website_label) + about_dialog.set_comments(metomi.rose.config_editor.ABOUT_TEXT) + for credit in metomi.rose.config_editor.CREDIT: + about_dialog.add_credit_section(credit[0], credit[1]) + about_dialog.present() def run_command_arg_dialog(cmd_name, help_text, run_hook): From 8b6c4880e8f0e26deec531492234f8b6e72b17e0 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 27 Aug 2024 16:38:43 +0100 Subject: [PATCH 23/42] Fixes sort calls removing cmp usage Fixes issue with duplicate ns sorting app 10 --- metomi/rose/config_editor/data_helper.py | 13 ++++++------ metomi/rose/config_editor/main.py | 7 ++++--- metomi/rose/config_editor/menu.py | 15 +++++++------- metomi/rose/config_editor/nav_controller.py | 8 +++----- metomi/rose/config_editor/nav_panel.py | 6 ++++-- metomi/rose/config_editor/nav_panel_menu.py | 18 +++++++++-------- metomi/rose/config_editor/ops/group.py | 4 +++- metomi/rose/config_editor/page.py | 16 ++++++++------- metomi/rose/config_editor/pagewidget/table.py | 15 +++++++------- .../config_editor/panelwidget/summary_data.py | 16 +++++++++------ .../config_editor/plugin/um/widget/stash.py | 19 +++++++++--------- .../plugin/um/widget/stash_add.py | 20 ++++++++++--------- metomi/rose/config_editor/util.py | 5 ++--- .../rose/config_editor/valuewidget/choice.py | 3 ++- .../rose/config_editor/valuewidget/format.py | 4 +++- .../rose/config_editor/valuewidget/source.py | 5 +++-- metomi/rose/config_editor/window.py | 10 ++++++---- metomi/rose/gtk/util.py | 12 +++++++---- 18 files changed, 111 insertions(+), 85 deletions(-) diff --git a/metomi/rose/config_editor/data_helper.py b/metomi/rose/config_editor/data_helper.py index 720a70914..ef825882b 100644 --- a/metomi/rose/config_editor/data_helper.py +++ b/metomi/rose/config_editor/data_helper.py @@ -19,6 +19,7 @@ # ----------------------------------------------------------------------------- import re +from functools import cmp_to_key import metomi.rose.config @@ -191,7 +192,7 @@ def get_ns_comment_string(self, ns): config_name = self.util.split_full_ns(self.data, ns)[0] config_data = self.data.config[config_name] sections = self.get_sections_from_namespace(ns) - sections.sort(metomi.rose.config.sort_settings) + sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) for section in sections: sect_data = config_data.sections.now.get(section) if sect_data is not None and sect_data.comments: @@ -294,7 +295,7 @@ def get_missing_sections(self, config_name=None): miss_sections.append(section) full_sections += [config_name + ':' + s for s in miss_sections] sorter = metomi.rose.config.sort_settings - full_sections.sort(sorter) + full_sections.sort(key=cmp_to_key(sorter)) return full_sections def get_default_section_namespace(self, section, config_name): @@ -342,7 +343,7 @@ def get_format_sections(self, config_name): if (section not in format_keys and ':' in section and not section.startswith('file:')): format_keys.append(section) - format_keys.sort(metomi.rose.config.sort_settings) + format_keys.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) return format_keys def get_icon_path_for_config(self, config_name): @@ -382,7 +383,7 @@ def get_ignored_sections(self, namespace, get_enabled=False): elif (metomi.rose.variable.IGNORED_BY_USER in sect_data.ignored_reason): return_sections.append(section) - return_sections.sort(metomi.rose.config.sort_settings) + return_sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) return return_sections def get_latent_sections(self, namespace): @@ -397,7 +398,7 @@ def get_latent_sections(self, namespace): for section in sections: if section not in config_data.sections.now: return_sections.append(section) - return_sections.sort(metomi.rose.config.sort_settings) + return_sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) return return_sections def get_ns_ignored_status(self, namespace): @@ -450,7 +451,7 @@ def get_ns_ignored_status(self, namespace): else: object_statuses = variable_statuses status_counts = list(object_statuses.items()) - status_counts.sort(lambda x, y: cmp(x[1], y[1])) + status_counts.sort(key = lambda x: x[1]) if not status_counts: cache[namespace] = status return metomi.rose.config.ConfigNode.STATE_NORMAL diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 81dcf620d..beb71010e 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -34,6 +34,8 @@ import tempfile import warnings +from functools import cmp_to_key + # Ignore add menu related warnings for now, but remove this later. warnings.filterwarnings('ignore', 'instance of invalid non-instantiatable type', @@ -1599,9 +1601,8 @@ def get_found_page_and_id(self, expression, start_page): current_name = self.util.split_full_ns(self.data, current_ns)[0] ns_cmp = lambda x, y: (y == current_ns) - (x == current_ns) name_cmp = lambda x, y: (y == current_name) - (x == current_name) - id_cmp = lambda v, w: cmp(v.metadata['id'], w.metadata['id']) config_keys = sorted(list(self.data.config.keys())) - config_keys.sort(name_cmp) + config_keys.sort(key=cmp_to_key(name_cmp)) for config_name in config_keys: config_data = self.data.config[config_name] search_vars = config_data.vars.get_all( @@ -1619,7 +1620,7 @@ def get_found_page_and_id(self, expression, start_page): found_ns_vars.setdefault(ns, []) found_ns_vars[ns].append(variable) ns_list = sorted(list(found_ns_vars.keys())) - ns_list.sort(ns_cmp) + ns_list.sort(key=cmp_to_key(ns_cmp)) for ns in ns_list: variables = found_ns_vars[ns] variables.sort(key=lambda x: x.metadata['id']) diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index f16d0b30d..a6cfa781e 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -40,6 +40,8 @@ import metomi.rose.macros import metomi.rose.popen +from functools import cmp_to_key + class MenuBar(object): @@ -443,7 +445,7 @@ def check_fail_rules(self, configs_updated=False): sorter = metomi.rose.config.sort_settings to_id = lambda s: self.util.get_id_from_section_option( s.section, s.option) - return_value.sort(lambda x, y: sorter(to_id(x), to_id(y))) + return_value.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) self.handle_macro_validation(config_name, macro_fullname, config, return_value, no_display=(not return_value)) @@ -477,12 +479,11 @@ def load_macro_menu(self, menubar): """Refresh the menu dealing with custom macro launches.""" menubar.clear_macros() config_keys = sorted(list(self.data.config.keys())) - tuple_sorter = lambda x, y: cmp(x[0], y[0]) for config_name in config_keys: image = self.data.helper.get_icon_path_for_config(config_name) macros = self.data.config[config_name].macros macro_tuples = metomi.rose.macro.get_macro_class_methods(macros) - macro_tuples.sort(tuple_sorter) + macro_tuples.sort(key=lambda x: x[0]) for macro_mod, macro_cls, macro_func, help_ in macro_tuples: menubar.add_macro(config_name, macro_mod, macro_cls, macro_func, help_, image, @@ -508,7 +509,7 @@ def handle_graph(self): for config_name in self.data.config: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) + config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) config_name, section = self.mainwindow.launch_graph_dialog( config_sect_dict) if config_name is None: @@ -706,7 +707,7 @@ def run_custom_macro(self, config_name=None, module_name=None, macro_config, change_list = return_value if not change_list: continue - change_list.sort(lambda x, y: sorter(to_id(x), to_id(y))) + change_list.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) num_changes = len(change_list) self.handle_macro_transforms(config_name, macro_fullname, macro_config, change_list) @@ -720,7 +721,7 @@ def run_custom_macro(self, config_name=None, module_name=None, return_value) continue if return_value: - return_value.sort(lambda x, y: sorter(to_id(x), to_id(y))) + return_value.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) config_macro_errors.append((config_name, macro_fullname, len(return_value))) @@ -1010,7 +1011,7 @@ def transform_default(self, only_this_config=None): meta_config = self.data.config[config_name].meta macro = metomi.rose.macros.DefaultTransforms() change_list = macro.transform(macro_config, meta_config)[1] - change_list.sort(lambda x, y: sorter(to_id(x), to_id(y))) + change_list.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) self.handle_macro_transforms( config_name, "Autofixer.transform", macro_config, change_list, triggers_ok=True) diff --git a/metomi/rose/config_editor/nav_controller.py b/metomi/rose/config_editor/nav_controller.py index a446349f2..775648ca0 100644 --- a/metomi/rose/config_editor/nav_controller.py +++ b/metomi/rose/config_editor/nav_controller.py @@ -18,6 +18,7 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- +from functools import cmp_to_key import metomi.rose.config_editor @@ -64,12 +65,9 @@ def reload_namespace_tree(self, only_this_namespace=None, # Reload the information into the tree. if only_this_config is None: configs = list(self.data.config.keys()) - configs.sort(metomi.rose.config.sort_settings) + configs.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) configs.sort( - lambda x, y: cmp( - self.data.config[y].config_type == metomi.rose.TOP_CONFIG_NAME, - self.data.config[x].config_type == metomi.rose.TOP_CONFIG_NAME - ) + key=lambda x: self.data.config[x].config_type == metomi.rose.TOP_CONFIG_NAME ) else: configs = [only_this_config] diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index 113dd3da7..b424683f0 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -32,6 +32,8 @@ import metomi.rose.gtk.util import metomi.rose.resource +from functools import cmp_to_key + class PageNavigationPanel(Gtk.ScrolledWindow): @@ -186,7 +188,7 @@ def load_tree_stack(self, row, namespace_subtree): if row is None: self.data_store.clear() initials = list(namespace_subtree.items()) - initials.sort(self.sort_tree_items) + initials.sort(key=cmp_to_key(self.sort_tree_items)) stack = [] if row is None: start_keylist = [] @@ -217,7 +219,7 @@ def load_tree_stack(self, row, namespace_subtree): name_iter_map["/".join(new_keylist)] = new_row if isinstance(value, dict): newer_initials = list(value.items()) - newer_initials.sort(self.sort_tree_items) + newer_initials.sort(key=cmp_to_key(self.sort_tree_items)) for vals in newer_initials: stack.append([new_row] + [list(new_keylist)] + list(vals)) stack.pop(0) diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index 53d061a30..f4517c649 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -30,6 +30,8 @@ import metomi.rose.config_editor.util import metomi.rose.gtk.dialog +from functools import cmp_to_key + class NavPanelHandler(object): @@ -81,7 +83,7 @@ def add_dialog(self, base_ns): config_names = [ n for n in self.data.config if not self.ask_is_preview(n)] - config_names.sort(lambda x, y: (y == config_name) - (x == config_name)) + config_names.sort(key=cmp_to_key(lambda x, y: (y == config_name) - (x == config_name))) config_name, section = self.mainwindow.launch_add_dialog( config_names, choices_help, help_str) if config_name in self.data.config and section is not None: @@ -155,10 +157,10 @@ def ignore_request(self, base_ns, is_ignored): mode == metomi.rose.META_PROP_VALUE_TRUE): continue config_sect_dict[config_name].append(section) - config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) + config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - metomi.rose.config.sort_settings) + key=cmp_to_key(metomi.rose.config.sort_settings)) config_name, section = self.mainwindow.launch_ignore_dialog( config_sect_dict, prefer_name_sections, is_ignored) if config_name in self.data.config and section is not None: @@ -243,10 +245,10 @@ def remove_request(self, base_ns): for config_name in config_names: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) + config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - metomi.rose.config.sort_settings) + key=cmp_to_key(metomi.rose.config.sort_settings)) config_name, section = self.mainwindow.launch_remove_dialog( config_sect_dict, prefer_name_sections) if config_name in self.data.config and section is not None: @@ -257,7 +259,7 @@ def remove_request(self, base_ns): variable_sorter = lambda v, w: metomi.rose.config.sort_settings( v.metadata['id'], w.metadata['id']) variables = list(config_data.vars.now.get(section, [])) - variables.sort(variable_sorter) + variables.sort(key=cmp_to_key(variable_sorter)) variables.reverse() for variable in variables: self.var_ops.remove_var(variable) @@ -278,10 +280,10 @@ def rename_dialog(self, base_ns): for config_name in self.data.config: config_data = self.data.config[config_name] config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(metomi.rose.config.sort_settings) + config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - metomi.rose.config.sort_settings) + key=cmp_to_key(metomi.rose.config.sort_settings)) config_name, source_section, target_section = ( self.mainwindow.launch_rename_dialog( config_sect_dict, prefer_name_sections) diff --git a/metomi/rose/config_editor/ops/group.py b/metomi/rose/config_editor/ops/group.py index 37413449f..d90cdeeb8 100644 --- a/metomi/rose/config_editor/ops/group.py +++ b/metomi/rose/config_editor/ops/group.py @@ -29,6 +29,8 @@ import re import time +from functools import cmp_to_key + import metomi.rose.config import metomi.rose.config_editor @@ -310,7 +312,7 @@ def copy_section(self, config_name, section, new_section=None, var.process_metadata(metadata) var.metadata['full_ns'] = new_namespace sorter = metomi.rose.config.sort_settings - clone_vars.sort(lambda v, w: sorter(v.name, w.name)) + clone_vars.sort(key=cmp_to_key(lambda v, w: sorter(v.name, w.name))) if skip_update: for var in clone_vars: self.var_ops.add_var(var, skip_update=skip_update) diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 923b1e071..be87514f4 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -38,6 +38,8 @@ import metomi.rose.resource import metomi.rose.variable +from functools import cmp_to_key + class ConfigPage(Gtk.Box): @@ -463,8 +465,8 @@ def generate_sub_data_panel(self, override_custom=False): metadata_files = self.section_ops.get_ns_metadata_files( self.namespace) widget_dir = metomi.rose.META_DIR_WIDGET - metadata_files.sort( - lambda x, y: (widget_dir in y) - (widget_dir in x)) + metadata_files.sort(key=cmp_to_key( + lambda x, y: (widget_dir in y) - (widget_dir in x))) prefix = re.sub(r"[^\w]", "_", self.config_name.strip("/")) prefix += "/" + metomi.rose.META_DIR_WIDGET + "/" custom_widget = metomi.rose.resource.import_object( @@ -530,7 +532,7 @@ def _add_var_from_item(item): for sect_data in self.sections: if not sect_data.ignored_reason: section_choices.append(sect_data.name) - section_choices.sort(metomi.rose.config.sort_settings) + section_choices.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if self.ns_is_default and section_choices: add_ui_start = add_ui_start.replace( "'Popup'>", @@ -541,8 +543,8 @@ def _add_var_from_item(item): actions.insert(0, ('Add blank', Gtk.STOCK_NEW, text)) ghost_list = [v for v in self.ghost_data] sorter = metomi.rose.config.sort_settings - ghost_list.sort(lambda v, w: sorter(v.metadata['id'], - w.metadata['id'])) + ghost_list.sort(key=cmp_to_key(lambda v, w: sorter(v.metadata['id'], + w.metadata['id']))) for variable in ghost_list: label_text = variable.name if (not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE] and @@ -1037,9 +1039,9 @@ def sort_data(self, column_index=0, ascending=True, ghost=False): descending_cmp = lambda x, y: metomi.rose.config_editor.util.null_cmp( x[0], y[0]) if ascending: - sorted_data.sort(ascending_cmp) + sorted_data.sort(key=cmp_to_key(ascending_cmp)) else: - sorted_data.sort(descending_cmp) + sorted_data.sort(key=cmp_to_key(descending_cmp)) if [x[4] for x in sorted_data] == datavars: return False for i, datum in enumerate(sorted_data): diff --git a/metomi/rose/config_editor/pagewidget/table.py b/metomi/rose/config_editor/pagewidget/table.py index fa887beb2..ec848b61c 100644 --- a/metomi/rose/config_editor/pagewidget/table.py +++ b/metomi/rose/config_editor/pagewidget/table.py @@ -30,6 +30,8 @@ import metomi.rose.formats import metomi.rose.variable +from functools import cmp_to_key + class PageTable(Gtk.Table): @@ -68,7 +70,7 @@ def add_variable_widget(self, variable): variable_widget = child.get_parent() if variable_widget not in [x[0] for x in widget_coordinate_list]: widget_coordinate_list.append((variable_widget, top_row)) - widget_coordinate_list.sort(lambda x, y: cmp(x[1], y[1])) + widget_coordinate_list.sort(key=lambda x: x[1]) old_index = None for widget, index in widget_coordinate_list: if widget.variable.metadata["id"] == variable.metadata["id"]: @@ -156,9 +158,8 @@ def _get_sorted_variables(self): (val.metadata.get("sort-key", "~")), val.metadata["id"]) is_ghost = val in self.ghost_data sort_key_vars.append((sort_key, val, is_ghost)) - sort_key_vars.sort(metomi.rose.config_editor.util.null_cmp) - sort_key_vars.sort(lambda x, y: cmp("=null" in x[1].metadata["id"], - "=null" in y[1].metadata["id"])) + sort_key_vars.sort(key=cmp_to_key(lambda x, y: metomi.rose.config_editor.util.null_cmp(x[0], y[0]))) + sort_key_vars.sort(key=lambda x: "=null" in x[1].metadata["id"]) return [(x[1], x[2]) for x in sort_key_vars] def _show_and_hide_variable_widgets(self, just_this_widget=None): @@ -296,10 +297,10 @@ def __init__(self, panel_data, ghost_data, var_ops, show_modes, for val in self.panel_data + self.ghost_data: v_sort_ids.append((val.metadata.get("sort-key", ""), val.metadata["id"])) - v_sort_ids.sort( + v_sort_ids.sort(key=cmp_to_key( lambda x, y: metomi.rose.config.sort_settings( - x[0] + "~" + x[1], y[0] + "~" + y[1])) - v_sort_ids.sort(lambda x, y: cmp("=null" in x[1], "=null" in y[1])) + x[0] + "~" + x[1], y[0] + "~" + y[1]))) + v_sort_ids.sort(key=lambda x: "=null" in x[1]) for _, var_id in v_sort_ids: is_ghost = False for variable in self.panel_data: diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index 1c4459312..f89ce3557 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -28,6 +28,8 @@ import metomi.rose.config_editor.util import metomi.rose.gtk.util +from functools import cmp_to_key + class BaseSummaryDataPanel(Gtk.Box): @@ -755,9 +757,8 @@ def _check_value_iter(self, model, path, iter_, data): return True return False - def _sort_row_data(self, row1, row2, sort_index, descending=False): - fac = (-1 if descending else 1) - return fac * self.sort_util.cmp_(row1[sort_index], row2[sort_index]) + def _sort_row_data(self, row1, row2): + return self.sort_util.cmp_(row1[0], row2[0]) def _handle_group_change(self, combobox): model = combobox.get_model() @@ -784,7 +785,10 @@ def _apply_grouping(self, data_rows, column_names, group_index=None, k = group_index data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] column_names.insert(0, column_names.pop(k)) - data_rows.sort(lambda x, y: self._sort_row_data(x, y, 0, descending)) + if descending: + data_rows.sort(key=cmp_to_key(self._sort_row_data), reverse=True) + else: + data_rows.sort(key=cmp_to_key(self._sort_row_data)) last_entry = None rows_are_descendants = [] for i, row in enumerate(data_rows): @@ -834,8 +838,8 @@ def get_model_data(self): self.var_id_map[variable.metadata["id"]] = variable if variable.name not in sub_var_names: sub_var_names.append(variable.name) - sub_sect_names.sort(metomi.rose.config.sort_settings) - sub_var_names.sort(metomi.rose.config.sort_settings) + sub_sect_names.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + sub_var_names.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) data_rows = [] for section in sub_sect_names: row_data = [section] diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index a53f6efe7..d164bfca5 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -37,6 +37,8 @@ import metomi.rose.config_editor.plugin.um.widget.stash_add as stash_add import metomi.rose.config_editor.plugin.um.widget.stash_util as stash_util +from functools import cmp_to_key + class BaseStashSummaryDataPanelv1( metomi.rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel): @@ -210,15 +212,14 @@ def get_model_data(self): while len(sort_list) < 4: sort_list.append(None) sort_list[3] = section - sub_sect_names.sort(lambda x, y: cmp(section_sort_keys.get(x), - section_sort_keys.get(y))) - sub_var_names.sort(metomi.rose.config.sort_settings) - sub_var_names.sort(lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) - - (x != self.STREQ_NL_PACKAGE_OPT)) - sub_var_names.sort(lambda x, y: (y == self.STREQ_NL_ITEM_OPT) - - (x == self.STREQ_NL_ITEM_OPT)) - sub_var_names.sort(lambda x, y: (y == self.STREQ_NL_SECT_OPT) - - (x == self.STREQ_NL_SECT_OPT)) + sub_sect_names.sort(key=lambda x: section_sort_keys.get(x)) + sub_var_names.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + sub_var_names.sort(key=cmp_to_key(lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) - + (x != self.STREQ_NL_PACKAGE_OPT))) + sub_var_names.sort(key=cmp_to_key(lambda x, y: (y == self.STREQ_NL_ITEM_OPT) - + (x == self.STREQ_NL_ITEM_OPT))) + sub_var_names.sort(key=cmp_to_key(lambda x, y: (y == self.STREQ_NL_SECT_OPT) - + (x == self.STREQ_NL_SECT_OPT))) # Load the data. data_rows = [] for section in sub_sect_names: diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index 04db4160b..d6481211b 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -29,6 +29,7 @@ import metomi.rose.config_editor.plugin.um.widget.stash_util as stash_util +from functools import cmp_to_key class AddStashDiagnosticsPanelv1(Gtk.Box): @@ -173,14 +174,14 @@ def get_model_data_and_columns(self): data_rows = [] columns = ["Section", "Item", "Description", "?", "#"] sections = list(self.stash_lookup.keys()) - sections.sort(self.sort_util.cmp_) + sections.sort(key=cmp_to_key(self.sort_util.cmp_)) props_excess = [self.STASH_PARSE_DESC_OPT, self.STASH_PARSE_ITEM_OPT, self.STASH_PARSE_SECT_OPT] for section in sections: if section == "-1": continue items = list(self.stash_lookup[section].keys()) - items.sort(self.sort_util.cmp_) + items.sort(key=cmp_to_key(self.sort_util.cmp_)) for item in items: data = self.stash_lookup[section][item] this_row = [section, item, data[self.STASH_PARSE_DESC_OPT]] @@ -260,7 +261,7 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): if stash_request_num != "None": sect_streqs = self.request_lookup.get(stash_section, {}) streqs = list(sect_streqs.get(stash_item, {}).keys()) - streqs.sort(metomi.rose.config.sort_settings) + streqs.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if streqs: value = "\n " + "\n ".join(streqs) else: @@ -364,8 +365,10 @@ def _apply_grouping(self, data_rows, column_names, group_index=None, k = group_index data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] column_names.insert(0, column_names.pop(k)) - data_rows.sort(lambda x, y: - self._sort_row_data(x, y, 0, descending)) + if descending: + data_rows.sort(key=cmp_to_key(self._sort_row_data), reverse=True) + else: + data_rows.sort(key=cmp_to_key(self._sort_row_data)) last_entry = None rows_are_descendants = [] for i, row in enumerate(data_rows): @@ -580,7 +583,7 @@ def _popup_tree_menu(self, path, col, event): view_menu = Gtk.Menu() view_menu.show() view_menuitem.set_submenu(view_menu) - streqs.sort(metomi.rose.config.sort_settings) + streqs.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) for streq in streqs: view_streq_menuitem = Gtk.MenuItem(label=streq) view_streq_menuitem._section = streq @@ -670,10 +673,9 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_): value = metomi.rose.gtk.util.safe_str(value) cell.set_property("markup", value) - def _sort_row_data(self, row1, row2, sort_index, descending=False): + def _sort_row_data(self, row1, row2): """Handle column sorting.""" - fac = (-1 if descending else 1) - return fac * self.sort_util.cmp_(row1[sort_index], row2[sort_index]) + return self.sort_util.cmp_(row1[0], row2[0]) def _toggle_show_column_name(self, column_name): """Handle a show/hide of a particular column.""" diff --git a/metomi/rose/config_editor/util.py b/metomi/rose/config_editor/util.py index c89496df0..afa2de378 100644 --- a/metomi/rose/config_editor/util.py +++ b/metomi/rose/config_editor/util.py @@ -116,8 +116,7 @@ def launch_node_info_dialog(node, changes, search_function): # vars will fail when __slots__ are used. att_list = node.getattrs() att_list.sort() - att_list.sort(lambda x, y: (y[0] in ['name', 'value']) - - (x[0] in ['name', 'value'])) + att_list.sort(key=lambda x: (x[0] in ['name', 'value'])) metadata_start_index = len(att_list) for key, value in sorted(node.metadata.items()): att_list.append([key, value]) @@ -202,7 +201,7 @@ def null_cmp(x_item, y_item): return (x_id == '') - (y_id == '') if x_sort_key == y_sort_key: return metomi.rose.config.sort_settings(x_id, y_id) - return cmp(x_sort_key, y_sort_key) + return (x_sort_key > y_sort_key) - (x_sort_key < y_sort_key) def _pretty_format_data(data, global_indent=0, indent=4, width=60): diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py index 3060aec7f..557e0f8c5 100644 --- a/metomi/rose/config_editor/valuewidget/choice.py +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -31,6 +31,7 @@ import metomi.rose.opt_parse import metomi.rose.variable +from functools import cmp_to_key class ChoicesValueWidget(Gtk.Box): @@ -240,7 +241,7 @@ def _get_groups(self, name, names): if not self.should_guess_groups or not self.should_show_kinship: return default_groups ok_groups = [n for n in names if set(n).issubset(name) and n != name] - ok_groups.sort(lambda x, y: set(x).issubset(y) - set(y).issubset(x)) + ok_groups.sort(key=cmp_to_key(lambda x, y: set(x).issubset(y) - set(y).issubset(x))) for group in default_groups: if group in ok_groups: ok_groups.remove(group) diff --git a/metomi/rose/config_editor/valuewidget/format.py b/metomi/rose/config_editor/valuewidget/format.py index df8164881..361e62f3e 100644 --- a/metomi/rose/config_editor/valuewidget/format.py +++ b/metomi/rose/config_editor/valuewidget/format.py @@ -24,6 +24,8 @@ import metomi.rose.config +from functools import cmp_to_key + class FormatsChooserValueWidget(Gtk.Box): @@ -129,7 +131,7 @@ def entry_change_handler(self, entry): def load_data_chooser(self): data_model = Gtk.ListStore(str) options = self.values_getter() - options.sort(metomi.rose.config.sort_settings) + options.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) for value in options: if value not in [e.get_text() for e in self.entries]: data_model.append([str(value)]) diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py index 38cba2605..5bdc62d16 100644 --- a/metomi/rose/config_editor/valuewidget/source.py +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -30,6 +30,7 @@ import metomi.rose.formats import metomi.rose.gtk.choice +from functools import cmp_to_key class SourceValueWidget(Gtk.Box): @@ -180,8 +181,8 @@ def _get_available_sections(self): if section_all not in ok_content_sections: ok_content_sections.append(section_all) ok_content_sections.append(section) - ok_content_sections.sort(metomi.rose.config.sort_settings) - ok_content_sections.sort(self._sort_settings_duplicate) + ok_content_sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + ok_content_sections.sort(key=cmp_to_key(self._sort_settings_duplicate)) return ok_content_sections def _get_groups(self, name, available_names): diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index c8f770737..e83763d41 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -31,6 +31,8 @@ import metomi.rose.gtk.util import metomi.rose.resource +from functools import cmp_to_key + REC_SPLIT_MACRO_TEXT = re.compile( '(.{' + str(metomi.rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + @@ -398,7 +400,7 @@ def _launch_choose_section_dialog( def _reload_section_choices(self, vbox, sections, prefs): for child in vbox.get_children(): vbox.remove(child) - sections.sort(metomi.rose.config.sort_settings) + sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) section_chooser = Gtk.ComboBoxText() for k, section in enumerate(sections): section_chooser.append_text(section) @@ -710,9 +712,9 @@ def display(self, changes): text = (text.rstrip() + " " + extra_text.format(nums_is_warning[True])) self.label.set_markup(text) - changes.sort(lambda x, y: cmp(x.option, y.option)) - changes.sort(lambda x, y: cmp(x.section, y.section)) - changes.sort(lambda x, y: cmp(x.is_warning, y.is_warning)) + changes.sort(key=lambda x: x.option) + changes.sort(key=lambda x: x.section) + changes.sort(key=lambda x: x.is_warning) last_section = None last_section_iter = None for item in changes: diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index a73a91831..e2eb088c6 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -453,8 +453,8 @@ def delete_by_id(self, page_id): """Use this only with pages with the attribute 'namespace'.""" self.remove_page(self.get_page_ids().index(page_id)) - def set_tab_label_packing(self, page): - super(Notebook, self).set_tab_label(page) # check + def set_tab_label_packing(self, page, tab_labelwidget): + super(Notebook, self).set_tab_label(page, tab_labelwidget) class TooltipTreeView(Gtk.TreeView): @@ -545,11 +545,15 @@ def clear_sort_columns(self): def cmp_(self, value1, value2): """Perform a useful form of 'cmp'""" + if value1 is None: + value1 = "None" + if value2 is None: + value2 = "None" if (isinstance(value1, str) and isinstance(value2, str)): if value1.isdigit() and value2.isdigit(): - return cmp(float(value1), float(value2)) + return (float(value1) > float(value2)) - (float(value1) < float(value2)) return metomi.rose.config.sort_settings(value1, value2) - return cmp(value1, value2) + return (value1 > value2) - (value1 < value2) def handle_sort_column_change(self, model): """Store previous sorting information for multi-column sorts.""" From 555b39469f2192089d9c91009d0a5218f00a1068 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 27 Aug 2024 13:39:51 +0100 Subject: [PATCH 24/42] Fixes macro button and submenu --- metomi/rose/config_editor/menu.py | 27 ++++++++-------- metomi/rose/config_editor/page.py | 51 ++++++++++++++++++------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index a6cfa781e..1b1abc261 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -304,20 +304,22 @@ def add_macro(self, config_name, modulename, classname, methodname, if config_item.get_submenu() is None: config_item.set_submenu(Gtk.Menu()) macro_fullname = ".".join([modulename, classname, methodname]) - macro_fullname = macro_fullname.replace("_", "__") + macro_fullname = macro_fullname.replace("__", "_") if methodname == metomi.rose.macro.VALIDATE_METHOD: - stock_id = Gtk.STOCK_DIALOG_QUESTION + stock_id = "dialog-question" else: stock_id = Gtk.STOCK_CONVERT - macro_item_box = Gtk.Box() + macro_item_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) macro_item_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) macro_item_label = Gtk.Label(label=macro_fullname) macro_item = Gtk.MenuItem() - macro_item_box.pack_start(macro_item_icon, False, False, 0) - macro_item_box.pack_start(macro_item_label, False, False, 0) + Gtk.Container.add(macro_item_box, macro_item_icon) + Gtk.Container.add(macro_item_box, macro_item_label) Gtk.Container.add(macro_item, macro_item_box) macro_item.set_tooltip_text(help_) - macro_item.show() + context = Gtk.Widget.get_style_context(macro_item) + Gtk.StyleContext.add_class(context, "macro-item") + macro_item.show_all() macro_item._run_data = [config_name, modulename, classname, methodname] macro_item.connect("activate", @@ -327,17 +329,17 @@ def add_macro(self, config_name, modulename, classname, methodname, for item in config_item.get_submenu().get_children(): if hasattr(item, "_rose_all_validators"): return False - all_item_box = Gtk.Box() - all_item_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.MENU) + all_item_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + all_item_icon = Gtk.Image.new_from_icon_name("dialog-question", Gtk.IconSize.MENU) all_item_label = Gtk.Label(label=metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) all_item = Gtk.MenuItem() - all_item_box.pack_start(all_item_icon, False, False, 0) - all_item_box.pack_start(all_item_label, False, False, 0) - Gtk.Container.add(macro_item, macro_item_box) + Gtk.Container.add(all_item_box, all_item_icon) + Gtk.Container.add(all_item_box, all_item_label) + Gtk.Container.add(all_item, all_item_box) all_item._rose_all_validators = True all_item.set_tooltip_text( metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) - all_item.show() + all_item.show_all() all_item._run_data = [config_name, None, None, methodname] all_item.connect("activate", lambda i: run_macro(*i._run_data)) @@ -589,7 +591,6 @@ def override_macro_defaults(self, optionals, methname): response = dialog.run() if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.CLOSE: res = optionals - dialog.destroy() else: res = {} for key, box in list(entries.items()): diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index be87514f4..5d3368220 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -24,7 +24,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, Gdk from gi.repository import Pango import metomi.rose.config_editor.panelwidget @@ -377,11 +377,11 @@ def launch_url(self, *args): def update_info(self): """Driver routine to update non-variable information.""" - button_list, label_list, _ = self._get_page_info_widgets() + button_list, label_list, info = self._get_page_info_widgets() if [l.get_text() for l in label_list] == self._last_info_labels: # No change - do not redraw. return False - self.generate_page_info(button_list, label_list) + self.generate_page_info(button_list, label_list, info) has_content = (self.info_panel.get_children() and self.info_panel.get_children()[0].get_children()) if self.info_panel in self.main_vpaned.get_children(): @@ -1048,9 +1048,10 @@ def sort_data(self, column_index=0, ascending=True, ghost=False): datavars[i] = datum[4] # variable return True - def _macro_menu_launch(self, widget, event): - # Create a menu below the widget for macro actions. - menu = Gtk.Menu() + def _macro_menu_launch(self): + # Create the popover menu below the widget for macro actions. + self.popover = Gtk.Popover() + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) for macro_name, info in sorted(self.custom_macros.items()): method, description = info if method == metomi.rose.macro.TRANSFORM_METHOD: @@ -1060,7 +1061,7 @@ def _macro_menu_launch(self, widget, event): macro_menuitem_box = Gtk.Box() macro_menuitem_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) macro_menuitem_label = Gtk.Label(label=macro_name) - macro_menuitem = Gtk.MenuItem() + macro_menuitem = Gtk.Button() macro_menuitem_box.pack_start(macro_menuitem_icon, False, False, 0) macro_menuitem_box.pack_start(macro_menuitem_label, False, False, 0) Gtk.Container.add(macro_menuitem, macro_menuitem_box) @@ -1068,12 +1069,15 @@ def _macro_menu_launch(self, widget, event): macro_menuitem.show() macro_menuitem._macro = macro_name macro_menuitem.connect( - "button-release-event", - lambda m, e: self.launch_macro(m._macro)) - menu.append(macro_menuitem) - menu.popup(None, None, widget.position_menu, event.button, - event.time, widget) - + "clicked", + lambda m: self.launch_macro(m._macro)) + macro_menuitem.set_relief(Gtk.ReliefStyle.NONE) + macro_menuitem.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + vbox.pack_start(macro_menuitem, False, True, 10) + vbox.show_all() + self.popover.add(vbox) + self.popover.set_position(Gtk.PositionType.BOTTOM) + def launch_macro(self, macro_name_string): """Launch a macro, if possible.""" class_name = None @@ -1181,16 +1185,19 @@ def _get_page_info_widgets(self): button_list.append(error_button) label_list.append(error_label) if list(self.custom_macros.items()): - macro_button = metomi.rose.gtk.util.CustomButton( + self._macro_menu_launch() + macro_button_icon = Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.MENU) + macro_label = Gtk.Label(label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON) + macro_button = Gtk.MenuButton( + image=macro_button_icon, label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON, - stock_id=Gtk.STOCK_EXECUTE, - tip_text=metomi.rose.config_editor.TIP_MACRO_RUN_PAGE, - as_tool=True, icon_at_start=True, - has_menu=True) - macro_button.connect("button-press-event", - self._macro_menu_launch) - macro_label = Gtk.Label() - macro_label.show() + popover=self.popover, + ) + macro_button.set_relief(Gtk.ReliefStyle.NONE) + macro_button.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + macro_button.show() + Gtk.Widget.set_name(macro_button, "macro-button") + button_list.append(macro_button) label_list.append(macro_label) return button_list, label_list, info From 9890fa8322960be2903d73208a058c60464d0562 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 27 Aug 2024 13:40:51 +0100 Subject: [PATCH 25/42] Fix to deal with comparison of NoneTypes and strings If either section or option in the namespace (ns) are None then this doesn't sort properly with other values. This change ensures None is converted to a string to compare with normal string section/option values. --- metomi/rose/config_editor/util.py | 2 +- metomi/rose/config_editor/window.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/metomi/rose/config_editor/util.py b/metomi/rose/config_editor/util.py index afa2de378..8663da220 100644 --- a/metomi/rose/config_editor/util.py +++ b/metomi/rose/config_editor/util.py @@ -45,7 +45,7 @@ def __init__(self): def get_id_from_section_option(self, section, option): """Return a variable id from a section and option.""" if option is None: - id_ = section + id_ = str(section) else: id_ = section + metomi.rose.CONFIG_DELIMITER + option self.section_option_id_lookup[id_] = (section, option) diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index e83763d41..6e9bcab9c 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -712,8 +712,8 @@ def display(self, changes): text = (text.rstrip() + " " + extra_text.format(nums_is_warning[True])) self.label.set_markup(text) - changes.sort(key=lambda x: x.option) - changes.sort(key=lambda x: x.section) + changes.sort(key=lambda x: str(x.option)) + changes.sort(key=lambda x: str(x.section)) changes.sort(key=lambda x: x.is_warning) last_section = None last_section_iter = None From 0c8a1ad7e5b48dc64e50d35fa69dbe22609e4cb6 Mon Sep 17 00:00:00 2001 From: J-J-Abram <98320699+J-J-Abram@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:22:56 +0100 Subject: [PATCH 26/42] Removing all references to pygtk (#41) --- metomi/rose/config_editor/__init__.py | 3 -- metomi/rose/config_editor/main.py | 30 +++---------------- .../config_editor/plugin/um/widget/stash.py | 12 ++------ .../rose/config_editor/upgrade_controller.py | 9 ++---- metomi/rose/config_editor/variable.py | 8 ++--- metomi/rose/gtk/util.py | 15 +++------- 6 files changed, 17 insertions(+), 60 deletions(-) diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index 2b8590f4a..ed9e8d649 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -268,8 +268,6 @@ "{0} problem(s) found in metadata at {1}.\n" + "Some functionality has been switched off.\n\n" + "Run rose metadata-check for more info.") -ERROR_MIN_PYGTK_VERSION = "Requires PyGTK version {0}, found {1}." -ERROR_MIN_PYGTK_VERSION_TITLE = "Need later PyGTK version to run" ERROR_NO_OUTPUT = "No output found for {0}" ERROR_NOT_FOUND = "Could not find path: {0}" ERROR_NOT_REGEX = "Could not compile expression: {0}\nError info: {1}" @@ -716,7 +714,6 @@ LAUNCH_SUITE_RUN = "rose suite-run" LAUNCH_SUITE_RUN_HELP = "rose help suite-run" MAX_APPS_THRESHOLD = 10 -MIN_PYGTK_VERSION = (2, 12, 0) PROGRAM_NAME = "rose edit" PROJECT_URL = "http://github.com/metomi/rose/" UNTITLED_NAME = "Untitled" diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index beb71010e..674df5830 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -1527,16 +1527,9 @@ def _launch_find(self, *args): page, var_id = self.perform_find(expression, start_page) if page is None: text = metomi.rose.config_editor.WARNING_NOT_FOUND - try: # Needs PyGTK >= 2.16 - self.find_entry.set_icon_from_stock( - 0, Gtk.STOCK_DIALOG_WARNING) - self.find_entry.set_icon_tooltip_text(0, text) - except AttributeError: - metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, - text, - metomi.rose.config_editor.WARNING_NOT_FOUND_TITLE - ) + self.find_entry.set_icon_from_stock( + 0, Gtk.STOCK_DIALOG_WARNING) + self.find_entry.set_icon_tooltip_text(0, text) else: if var_id is not None: self.reporter.report( @@ -1545,10 +1538,7 @@ def _launch_find(self, *args): def _clear_find(self, *args): """Clear any warning icons from the find entry.""" - try: # Needs PyGTK >= 2.16 - self.find_entry.set_icon_from_stock(0, None) - except AttributeError: - pass + self.find_entry.set_icon_from_stock(0, None) def perform_find(self, expression, start_page=None): """Drive the finding of the regex 'expression' within the data.""" @@ -1904,18 +1894,6 @@ def get_number_of_configs(config_directory_path=None): def main(): """Launch from the command line.""" - # if (Gtk.pygtk_version[0] < metomi.rose.config_editor.MIN_PYGTK_VERSION[0] or - # Gtk.pygtk_version[1] < metomi.rose.config_editor.MIN_PYGTK_VERSION[1]): - # this_version = '{0}.{1}.{2}'.format(*Gtk.pygtk_version) - # required_version = '{0}.{1}.{2}'.format( - # *metomi.rose.config_editor.MIN_PYGTK_VERSION) - # metomi.rose.gtk.dialog.run_dialog( - # metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - # metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION.format( - # required_version, this_version), - # metomi.rose.config_editor.ERROR_MIN_PYGTK_VERSION_TITLE - # ) - # sys.exit(1) sys.path.append(os.getenv('ROSE_HOME')) opt_parser = metomi.rose.opt_parse.RoseOptionParser() opt_parser.add_my_options("conf_dir", "meta_path", "new_mode", diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index d164bfca5..c1781861a 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -141,15 +141,9 @@ def add_cell_renderer_for_value(self, col, col_title): cell_for_value.set_property("editable", True) cell_for_value.set_property("model", listmodel) cell_for_value.set_property("text-column", 0) - try: - cell_for_value.connect("changed", - self._handle_cell_combo_change, - col_title) - except TypeError: - # PyGTK 2.14 - changed signal. - cell_for_value.connect("edited", - self._handle_cell_combo_change, - col_title) + cell_for_value.connect("changed", + self._handle_cell_combo_change, + col_title) col.pack_start(cell_for_value, True, True, 0) col.set_cell_data_func(cell_for_value, self._set_tree_cell_value_combo) diff --git a/metomi/rose/config_editor/upgrade_controller.py b/metomi/rose/config_editor/upgrade_controller.py index 8f7c1600b..d90f234e2 100644 --- a/metomi/rose/config_editor/upgrade_controller.py +++ b/metomi/rose/config_editor/upgrade_controller.py @@ -89,13 +89,8 @@ def __init__(self, app_config_dict, handle_transform_func, self._combo_cell = Gtk.CellRendererCombo() self._combo_cell.set_property("has-entry", False) self._combo_cell.set_property("editable", True) - try: - self._combo_cell.connect("changed", - self._handle_change_version, 2) - except TypeError: - # PyGTK 2.14 - changed signal. - self._combo_cell.connect("edited", - self._handle_change_version, 2) + self._combo_cell.connect("changed", + self._handle_change_version, 2) cell = self._combo_cell else: cell = Gtk.CellRendererText() diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 191c8917c..52af7fb6a 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -229,10 +229,10 @@ def insert_into(self, container, x_info=None, y_info=None, no_menuwidget=False): """Inserts the child widgets of an instance into the 'container'. - As PyGTK is not that introspective, we need arguments specifying where - the correct area within the widget is - in the case of Gtk.Table - instances, we need the number of columns and the row index. - These arguments are generically named x_info and y_info. + We need arguments specifying where the correct area within the + widget is - in the case of Gtk.Table instances, we need the + number of columns and the row index. These arguments are + generically named x_info and y_info. """ if not hasattr(container, 'num_removes'): diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index e2eb088c6..d7bc9e772 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -618,17 +618,10 @@ def get_hyperlink_label(text, search_func=lambda i: False): except GLib.GError: label.set_text(text) else: - try: - label.connect("activate-link", - lambda l, u: handle_link(u, search_func)) - except TypeError: # No such signal before PyGTK 2.18 - label.connect("button-release-event", - lambda l, e: extract_link(l, search_func)) - text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_UNDERLINE, text) - label.set_markup(text) - else: - text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_HTML, text) - label.set_markup(text) + label.connect("activate-link", + lambda l, u: handle_link(u, search_func)) + text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_HTML, text) + label.set_markup(text) return label From e558faa9ed0dd3f8224bf7618d4199e418e8f281 Mon Sep 17 00:00:00 2001 From: J-J-Abram <98320699+J-J-Abram@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:13:58 +0100 Subject: [PATCH 27/42] Fixes popup() calls to GTK3 equivalents --- metomi/rose/config_editor/main.py | 2 +- metomi/rose/config_editor/menuwidget.py | 21 +++++++++---------- metomi/rose/config_editor/nav_panel_menu.py | 2 +- metomi/rose/config_editor/page.py | 8 +++---- .../config_editor/panelwidget/filesystem.py | 2 +- .../config_editor/panelwidget/summary_data.py | 7 +++---- .../config_editor/plugin/um/widget/stash.py | 5 ++--- .../plugin/um/widget/stash_add.py | 5 ++--- metomi/rose/gtk/choice.py | 2 +- 9 files changed, 25 insertions(+), 29 deletions(-) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 674df5830..76624621d 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -923,7 +923,7 @@ def add_page_variable(self, widget, event): page = self._get_current_page() if page is None: return False - page.launch_add_menu(event.button, event.time) + page.launch_add_menu(event) def revert_to_saved_data(self): """Reload the page data from saved configuration information.""" diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index 8bbcf18f6..3f8e41412 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -147,15 +147,14 @@ def load_contents(self): self.button.connect( "button-press-event", lambda b, e: self._popup_option_menu( - self.option_ui, self.actions, e.button, e.time)) - # FIXME: Try to popup the menu at the button, instead of the cursor. - self.button.connect( - "activate", - lambda b: self._popup_option_menu( - self.option_ui, - self.actions, - 1, - Gdk.Event(Gdk.KEY_PRESS).time)) + self.option_ui, self.actions, e)) + # # FIXME: Try to popup the menu at the button, instead of the cursor. + # self.button.connect( + # "activate", + # lambda b: self._popup_option_menu( + # self.option_ui, + # self.actions, + # Gdk.Event(Gdk.KEY_PRESS))) self.button.connect( "enter-notify-event", lambda b, e: self._set_hover_over(variable)) @@ -196,7 +195,7 @@ def _set_hover_over(self, variable): def _perform_add(self): self.var_ops.add_var(self.my_variable) - def _popup_option_menu(self, option_ui, actions, button, time): + def _popup_option_menu(self, option_ui, actions, event): actiongroup = Gtk.ActionGroup('Popup') actiongroup.set_translation_domain('') actiongroup.add_actions(actions) @@ -292,7 +291,7 @@ def _popup_option_menu(self, option_ui, actions, button, time): option_menu.attach_to_widget(self.button, lambda m, w: False) option_menu.show() - option_menu.popup(None, None, None, button, time) + option_menu.popup_at_widget(self.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) return False def _launch_info_dialog(self, *args): diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index f4517c649..b54669ba3 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -470,7 +470,7 @@ def popup_panel_menu(self, base_ns, event): rename_section_item.connect( "activate", lambda b: self.rename_dialog(namespace)) menu = uimanager.get_widget('/Popup') - menu.popup(None, None, None, event.button, event.time) + menu.popup_at_pointer(event) return False def is_ns_duplicate(self, namespace): diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index 5d3368220..f6b1a537d 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -157,7 +157,7 @@ def get_page(self): def _handle_click_main_window(self, widget, event): if event.button != 3: return False - self.launch_add_menu(event.button, event.time) + self.launch_add_menu(event) return False def get_label_widget(self, is_detached=False): @@ -294,7 +294,7 @@ def launch_tab_menu(self, event): url_item = uimanager.get_widget('/Popup/Web_Help') url_item.connect("activate", lambda b: self.launch_url()) tab_menu = uimanager.get_widget('/Popup') - tab_menu.popup(None, None, None, event.button, event.time) + tab_menu.popup_at_pointer(event) return False def trigger_tab_detach(self, widget=None): @@ -509,12 +509,12 @@ def update_sub_data(self): self.sub_data_panel.update(self.sub_data["sections"], self.sub_data["variables"]) - def launch_add_menu(self, button, my_time): + def launch_add_menu(self, event): """Pop up a contextual add variable menu.""" add_menu = self.get_add_menu() if add_menu is None: return False - add_menu.popup(None, None, None, button, my_time) + add_menu.popup_at_pointer(event) return False def get_add_menu(self): diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index 2fae5e575..f69326aeb 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -122,4 +122,4 @@ def _handle_click(self, view, event): "activate", lambda m: self._handle_activation(view, path, col)) this_menu = uimanager.get_widget('/Popup') - this_menu.popup(None, None, None, event.button, event.time) + this_menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index f89ce3557..9683d8983 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -486,14 +486,13 @@ def _popup_tree_multi_menu(self, event): Gtk.AccelFlags.VISIBLE ) - menu.popup(None, None, None, event.button, event.time) + menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) return False def _popup_tree_menu(self, path, col, event): """Launch a menu for this main treeview row (single selection).""" shortcuts = [] menu = Gtk.Menu() - menu.show() model = self._view.get_model() row_iter = model.get_iter(path) sect_index = self.get_section_column_index() @@ -659,8 +658,8 @@ def _popup_tree_menu(self, path, col, event): mod, Gtk.AccelFlags.VISIBLE ) - - menu.popup(None, None, None, event.button, event.time) + menu.popup_at_pointer(event) + menu.show_all() return False def add_section(self, section=None, opt_map=None): diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index c1781861a..4eaa6ebc9 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -24,7 +24,7 @@ from gi.repository import Pango import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk +from gi.repository import Gtk, Gdk import metomi.rose.config import metomi.rose.config_editor.panelwidget.summary_data @@ -849,8 +849,7 @@ def _package_menu_launch(self, widget, event): lambda i: self._packages_enable(disable=True)) menuitem.show() menu.append(menuitem) - menu.popup(None, None, widget.position_menu, event.button, - event.time, widget) + menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) def _packages_remove(self, only_this_package=None): # Remove requests and no-longer-needed profiles for packages. diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index d6481211b..1a0e4cedc 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -593,7 +593,7 @@ def _popup_tree_menu(self, path, col, event): view_streq_menuitem.show() view_menu.append(view_streq_menuitem) menu.append(view_menuitem) - menu.popup(None, None, None, event.button, event.time) + menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) return False def _popup_view_menu(self, widget, event): @@ -642,8 +642,7 @@ def _popup_view_menu(self, widget, event): "toggled", lambda c: self._toggle_show_column_name(*c._connect_args)) show_column_menu.append(col_menuitem) - menu.popup(None, None, widget.position_menu, event.button, - event.time, widget) + menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) def _set_tree_cell_value(self, column, cell, treemodel, iter_): # Extract an appropriate value for this cell from the model. diff --git a/metomi/rose/gtk/choice.py b/metomi/rose/gtk/choice.py index 51fd2bfeb..8a349c57a 100644 --- a/metomi/rose/gtk/choice.py +++ b/metomi/rose/gtk/choice.py @@ -189,7 +189,7 @@ def _popup_menu(self, iter_, event): lambda b, e: self._handle_reordering() ) menu.append(menuitem) - menu.popup(None, None, None, event.button, event.time) + menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) return False def _remove_iter(self, iter_): From 6849005e04021345f3004bd7666b6beb9f5c7a3b Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 23 Sep 2024 15:25:39 +0100 Subject: [PATCH 28/42] Fixes to the UM stash panel Fixes invisible text in the stash diag panel --- metomi/rose/config_editor/plugin/um/widget/stash.py | 11 ++++++----- .../rose/config_editor/plugin/um/widget/stash_add.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index 4eaa6ebc9..a0adb4321 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -297,7 +297,7 @@ def get_stashmaster_meta_map(self): stash_meta_dict[address][prop] = node.value return stash_meta_dict - def set_tree_cell_status(self, col, cell, model, row_iter): + def set_tree_cell_status(self, col, cell, model, row_iter, _): """(Override) Set the status-related markup for a cell.""" col_index = self._view.get_columns().index(col) sect_index = self.get_section_column_index() @@ -667,7 +667,7 @@ def _refresh_diagnostic_window(self): self._diag_panel.update_request_info(request_lookup, request_changes) - def _set_tree_cell_value_combo(self, column, cell, treemodel, iter_): + def _set_tree_cell_value_combo(self, column, cell, treemodel, iter_, _): # Extract a value for a combo box cell renderer. cell.set_property("visible", True) cell.set_property("editable", True) @@ -681,7 +681,7 @@ def _set_tree_cell_value_combo(self, column, cell, treemodel, iter_): cell.set_property("visible", False) cell.set_property("text", value) - def _set_tree_cell_value_toggle(self, column, cell, treemodel, iter_): + def _set_tree_cell_value_toggle(self, column, cell, treemodel, iter_, _): # Extract a value for a toggle cell renderer. cell.set_property("visible", True) col_index = self._view.get_columns().index(column) @@ -696,7 +696,7 @@ def _set_tree_cell_value_toggle(self, column, cell, treemodel, iter_): return False cell.set_property("active", value) - def _set_tree_cell_value(self, column, cell, treemodel, iter_): + def _set_tree_cell_value(self, column, cell, treemodel, iter_, _): # Extract a value for a conventional text cell renderer. cell.set_property("visible", True) col_index = self._view.get_columns().index(column) @@ -815,7 +815,8 @@ def _package_menu_launch(self, widget, event): remove_menuitem.show() package_menu.append(remove_menuitem) package_menuitem.set_submenu(package_menu) - menu.append(package_menuitem) + menu.append(package_menuitem) + package_menu.show_all() menuitem_box = Gtk.Box() menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) menuitem_label = Gtk.Label(label="Import") diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index 1a0e4cedc..26dbeca17 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -383,7 +383,7 @@ def _filter_refresh(self, widget=None): # Hook function that reacts to a change in filter status. self._view.get_model().get_model().refilter() - def _filter_visible(self, model, iter_): + def _filter_visible(self, model, iter_, _): # This returns whether a row should be visible. filt_text = self._filter_widget.get_text() if not filt_text: @@ -644,7 +644,7 @@ def _popup_view_menu(self, widget, event): show_column_menu.append(col_menuitem) menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) - def _set_tree_cell_value(self, column, cell, treemodel, iter_): + def _set_tree_cell_value(self, column, cell, treemodel, iter_, _): # Extract an appropriate value for this cell from the model. cell.set_property("visible", True) col_index = self._view.get_columns().index(column) From 2529332a2916ec2af061f476bce1c30663c99991 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Mon, 23 Sep 2024 15:33:37 +0100 Subject: [PATCH 29/42] Update importlib code when importing custom widgets in resource.py --- metomi/rose/resource.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/metomi/rose/resource.py b/metomi/rose/resource.py index 3d8b6ee7b..5826e5e82 100644 --- a/metomi/rose/resource.py +++ b/metomi/rose/resource.py @@ -18,7 +18,7 @@ Convenient functions for searching resource files. """ -from importlib.machinery import SourceFileLoader +import importlib import inspect import os from pathlib import Path @@ -32,6 +32,7 @@ ERROR_LOCATE_OBJECT = "Could not locate {0}" +MODULES = {} class ResourceError(Exception): @@ -172,7 +173,7 @@ def locate(self, key): def import_object( - import_string, from_files, error_handler, module_prefix=None + import_string, from_files, error_handler ): """Import a Python callable. @@ -182,18 +183,14 @@ def import_object( from_files is a list of available Python file paths to search in error_handler is a function that accepts an Exception instance or string and does something appropriate with it. - module_prefix is an optional string to prepend to the module - as an alias - this avoids any clashing between same-name modules. """ is_builtin = False module_name = ".".join(import_string.split(".")[:-1]) + if module_name.startswith("rose."): + module_name = "metomi." + module_name if module_name.startswith("metomi.rose."): is_builtin = True - if module_prefix is None: - as_name = module_name - else: - as_name = module_prefix + module_name class_name = import_string.split(".")[-1] module_fpath = "/".join(import_string.rsplit(".")[:-1]) + ".py" if module_fpath == ".py": @@ -212,7 +209,15 @@ def import_object( for filename in module_files: sys.path.insert(0, os.path.dirname(filename)) try: - module = SourceFileLoader(as_name, filename).load_module() + spec = importlib.util.spec_from_file_location(filename, filename) + if not filename in MODULES: + spec = importlib.util.spec_from_file_location(filename, filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + MODULES[filename] = module + else: + module = MODULES[filename] + sys.path.pop(0) except ImportError as exc: error_handler(exc) sys.path.pop(0) From fc7a0b6e58baf356f1c7a3f5a57c3525460cad98 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 24 Sep 2024 14:51:12 +0100 Subject: [PATCH 30/42] Change search box from an Entry to a SearchEntry box and add default text "Search" --- metomi/rose/config_editor/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 76624621d..4d07f5707 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -340,7 +340,7 @@ def generate_toolbar(self): (metomi.rose.config_editor.TOOLBAR_ADD, 'Gtk.STOCK_ADD'), (metomi.rose.config_editor.TOOLBAR_REVERT, 'Gtk.STOCK_REVERT_TO_SAVED'), - (metomi.rose.config_editor.TOOLBAR_FIND, 'Gtk.Entry'), + (metomi.rose.config_editor.TOOLBAR_FIND, 'Gtk.SearchEntry'), (metomi.rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), (metomi.rose.config_editor.TOOLBAR_VALIDATE, "dialog-question"), @@ -376,6 +376,7 @@ def generate_toolbar(self): metomi.rose.config_editor.TOOLBAR_FIND)['widget'] self.find_entry.connect("activate", self._launch_find) self.find_entry.connect("changed", self._clear_find) + Gtk.Entry.set_placeholder_text(self.find_entry, "Search") add_icon = self.toolbar.item_dict.get( metomi.rose.config_editor.TOOLBAR_ADD)['widget'] add_icon.connect('button_press_event', self.add_page_variable) From ec3a11e441ed16003d5c2d7069431301f927d990 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 25 Sep 2024 11:06:38 +0100 Subject: [PATCH 31/42] Add pygobject as a rose-edit dependency for install (#35) --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 5d6219844..4d02be22e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,8 @@ docs = graph = pygraphviz>1.0,!=1.8 rosa = +rose-edit = + pygobject>=3.48.2 tests = aiosmtpd pytest @@ -95,6 +97,7 @@ all = %(docs)s %(graph)s %(rosa)s + %(rose-edit)s %(tests)s [options.entry_points] From 760ec70f2a6d51ff9772c4a0a9c0aee6016c0e48 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Thu, 17 Oct 2024 14:29:48 +0100 Subject: [PATCH 32/42] Re-enable plotting of Metadata graphs --- metomi/rose/rose.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/metomi/rose/rose.py b/metomi/rose/rose.py index a33d017c8..d60c16fc3 100644 --- a/metomi/rose/rose.py +++ b/metomi/rose/rose.py @@ -95,8 +95,6 @@ def iter_entry_points(name: str): # (ns, sub_cmd): message ('rosa', 'rpmbuild'): 'Rosa RPM Builder has been removed.', - ('rose', 'metadata-graph'): - 'This command has been removed pending re-implementation', ('rose', 'suite-clean'): 'This command has been replaced by: "cylc clean".', ('rose', 'suite-cmp-vc'): From 1fb02195c73c60653775a230e1bac61eeafc0a52 Mon Sep 17 00:00:00 2001 From: J-J-Abram <98320699+J-J-Abram@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:13:35 +0100 Subject: [PATCH 33/42] Tutorial username file updated to GTK3 and Python3 (#45) --- .../tutorial/widget/meta/lib/python/widget/username.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py b/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py index 09da53a57..aef9248f7 100644 --- a/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py +++ b/metomi/rose/etc/tutorial/widget/meta/lib/python/widget/username.py @@ -9,11 +9,11 @@ from functools import partial -import pygtk +import gi -pygtk.require('2.0') +gi.require_version('Gtk', '3.0') # flake8: noqa: E402 -import gtk +from gi.repository import Gtk class UsernameValueWidget(Gtk.Box): @@ -26,7 +26,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.metadata = metadata self.set_value = set_value self.hook = hook - self.entry = gtk.Entry() + self.entry = Gtk.Entry() self.entry.set_text(self.value) self.entry.connect_after("paste-clipboard", self._setter) self.entry.connect_after("key-release-event", self._setter) From 48b8158b69afee6fb7459c93d7d449ae02ea6a85 Mon Sep 17 00:00:00 2001 From: J-J-Abram <98320699+J-J-Abram@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:20:10 +0100 Subject: [PATCH 34/42] Update setter functions to fix python list formatting (#46) --- .../config_editor/valuewidget/array/entry.py | 19 ++++++++++++++++++- .../valuewidget/array/python_list.py | 19 ++++++++++++++++++- .../valuewidget/array/spaced_list.py | 19 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 1433f70f1..0cdcf8838 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -401,7 +401,24 @@ def remove_entry(self): def setter(self, widget): """Reconstruct the new variable value from the entry array.""" - val_array = [e.get_text() for e in self.entries] + val_array = [] + # Prevent str without "" breaking the underlying Python syntax + for e in self.entries: + v = e.get_text() + if v in ("False", "True"): # Boolean + val_array.append(v) + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + val_array.append(v) + elif not v.startswith('"'): # Str - add in leading and trailing " + val_array.append('"' + v + '"') + e.set_text('"' + v + '"') + e.set_position(len(v)+1) + elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + val_array.append(v + '"') + e.set_text(v + '"') + e.set_position(len(v)) + else: + val_array.append(v) max_length = max([len(v) for v in val_array] + [1]) if max_length + 1 != self.chars_width: self.chars_width = max_length + 1 diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 19b0dd2ff..50c78f96b 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -372,7 +372,24 @@ def remove_entry(self): def setter(self, widget): """Reconstruct the new variable value from the entry array.""" - val_array = [e.get_text() for e in self.entries] + val_array = [] + # Prevent str without "" breaking the underlying Python syntax + for e in self.entries: + v = e.get_text() + if v in ("False", "True"): # Boolean + val_array.append(v) + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + val_array.append(v) + elif not v.startswith('"'): # Str - add in leading and trailing " + val_array.append('"' + v + '"') + e.set_text('"' + v + '"') + e.set_position(len(v)+1) + elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + val_array.append(v + '"') + e.set_text(v + '"') + e.set_position(len(v)) + else: + val_array.append(v) max_length = max([len(v) for v in val_array] + [1]) if max_length + 1 != self.chars_width: self.chars_width = max_length + 1 diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index 23d84c0f6..a4c8b3337 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -366,7 +366,24 @@ def remove_entry(self): def setter(self, widget): """Reconstruct the new variable value from the entry array.""" - val_array = [e.get_text() for e in self.entries] + val_array = [] + # Prevent str without "" breaking the underlying Python syntax + for e in self.entries: + v = e.get_text() + if v in ("False", "True"): # Boolean + val_array.append(v) + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + val_array.append(v) + elif not v.startswith('"'): # Str - add in leading and trailing " + val_array.append('"' + v + '"') + e.set_text('"' + v + '"') + e.set_position(len(v)+1) + elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + val_array.append(v + '"') + e.set_text(v + '"') + e.set_position(len(v)) + else: + val_array.append(v) max_length = max([len(v) for v in val_array] + [1]) if max_length + 1 != self.chars_width: self.chars_width = max_length + 1 From a2bd3da6d7df537aded765dbba2041c2210379a2 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 22 Oct 2024 13:49:29 +0100 Subject: [PATCH 35/42] Black formats the gui code and limits line length --- metomi/rose/config_editor/__init__.py | 155 +- metomi/rose/config_editor/data.py | 823 ++++++--- metomi/rose/config_editor/data_helper.py | 154 +- metomi/rose/config_editor/keywidget.py | 384 ++-- metomi/rose/config_editor/main.py | 1600 ++++++++++------- metomi/rose/config_editor/menu.py | 924 ++++++---- metomi/rose/config_editor/menuwidget.py | 310 ++-- metomi/rose/config_editor/nav_controller.py | 74 +- metomi/rose/config_editor/nav_panel.py | 314 ++-- metomi/rose/config_editor/nav_panel_menu.py | 407 +++-- metomi/rose/config_editor/ops/group.py | 353 ++-- metomi/rose/config_editor/ops/section.py | 203 ++- metomi/rose/config_editor/ops/variable.py | 267 ++- metomi/rose/config_editor/page.py | 859 +++++---- .../rose/config_editor/pagewidget/__init__.py | 1 + metomi/rose/config_editor/pagewidget/table.py | 166 +- .../config_editor/panelwidget/__init__.py | 1 + .../config_editor/panelwidget/filesystem.py | 53 +- .../config_editor/panelwidget/summary_data.py | 471 +++-- .../config_editor/plugin/um/widget/stash.py | 404 +++-- .../plugin/um/widget/stash_add.py | 257 ++- .../plugin/um/widget/stash_util.py | 5 +- metomi/rose/config_editor/stack.py | 161 +- metomi/rose/config_editor/status.py | 134 +- metomi/rose/config_editor/updater.py | 473 +++-- .../rose/config_editor/upgrade_controller.py | 146 +- metomi/rose/config_editor/util.py | 84 +- .../config_editor/valuewidget/__init__.py | 32 +- .../valuewidget/array/__init__.py | 4 +- .../config_editor/valuewidget/array/entry.py | 283 +-- .../valuewidget/array/logical.py | 193 +- .../config_editor/valuewidget/array/mixed.py | 165 +- .../valuewidget/array/python_list.py | 240 ++- .../config_editor/valuewidget/array/row.py | 180 +- .../valuewidget/array/spaced_list.py | 239 ++- .../config_editor/valuewidget/boolradio.py | 46 +- .../config_editor/valuewidget/booltoggle.py | 64 +- .../config_editor/valuewidget/character.py | 43 +- .../rose/config_editor/valuewidget/choice.py | 66 +- .../config_editor/valuewidget/combobox.py | 24 +- .../rose/config_editor/valuewidget/files.py | 54 +- .../rose/config_editor/valuewidget/format.py | 52 +- .../rose/config_editor/valuewidget/intspin.py | 33 +- metomi/rose/config_editor/valuewidget/meta.py | 18 +- .../config_editor/valuewidget/radiobuttons.py | 30 +- .../rose/config_editor/valuewidget/source.py | 63 +- metomi/rose/config_editor/valuewidget/text.py | 46 +- .../config_editor/valuewidget/valuehints.py | 11 +- metomi/rose/config_editor/variable.py | 381 ++-- metomi/rose/config_editor/window.py | 539 ++++-- metomi/rose/gtk/__init__.py | 5 +- metomi/rose/gtk/choice.py | 102 +- metomi/rose/gtk/console.py | 55 +- metomi/rose/gtk/dialog.py | 367 ++-- metomi/rose/gtk/run.py | 68 - metomi/rose/gtk/splash.py | 57 +- metomi/rose/gtk/util.py | 201 ++- metomi/rose/reporter.py | 9 +- metomi/rose/resource.py | 15 +- 59 files changed, 8080 insertions(+), 4788 deletions(-) delete mode 100644 metomi/rose/gtk/run.py diff --git a/metomi/rose/config_editor/__init__.py b/metomi/rose/config_editor/__init__.py index ed9e8d649..5af6cc3d1 100644 --- a/metomi/rose/config_editor/__init__.py +++ b/metomi/rose/config_editor/__init__.py @@ -53,7 +53,6 @@ """ import ast -import os import sys from metomi.rose.resource import ResourceLocator @@ -215,15 +214,18 @@ EVENT_MACRO_VALIDATE_ALL = "Custom Validators: {0}: {1} errors" EVENT_MACRO_VALIDATE_ALL_OK = "Custom Validators: {0}: all OK" EVENT_MACRO_VALIDATE_CHECK_ALL = ( - "Custom Validators, FailureRuleChecker: {0} total problems found") + "Custom Validators, FailureRuleChecker: {0} total problems found" +) EVENT_MACRO_VALIDATE_CHECK_ALL_OK = ( - "Custom Validators, FailureRuleChecker: No problems found") + "Custom Validators, FailureRuleChecker: No problems found" +) EVENT_MACRO_VALIDATE_OK = "{1}: {0} is OK" EVENT_MACRO_VALIDATE_NO_PROBLEMS = "Custom Validators: No problems found" EVENT_MACRO_VALIDATE_PROBLEMS_FOUND = "Custom Validators: {0} problems found" EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS = "FailureRuleChecker: No problems found" EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND = ( - "FailureRuleChecker: {0} problems found") + "FailureRuleChecker: {0} problems found" +) EVENT_REDO = "{0}" EVENT_REVERT = "Reverted {0}" EVENT_TIME = "%H:%M:%S" @@ -245,14 +247,18 @@ ERROR_BAD_NAME = "{0}: invalid name" ERROR_BAD_MACRO_EXCEPTION = "Could not apply macro: error: {0}: {1}" ERROR_BAD_MACRO_RETURN = "Bad return value for macro: {0}" -ERROR_BAD_TRIGGER = ("{0}\nfor {1}\n" - "from the configuration {2}. " - "\nDisabling triggers for this configuration.") -ERROR_CONFIG_CREATE = ("Error creating application config at {0}:" + - "\n {1}, {2}") +ERROR_BAD_TRIGGER = ( + "{0}\nfor {1}\n" + "from the configuration {2}. " + "\nDisabling triggers for this configuration." +) +ERROR_CONFIG_CREATE = ( + "Error creating application config at {0}:" + "\n {1}, {2}" +) ERROR_CONFIG_CREATE_TITLE = "Error in creating configuration" -ERROR_CONFIG_DELETE = ("Error deleting application config at {0}:" + - "\n {1}, {2}") +ERROR_CONFIG_DELETE = ( + "Error deleting application config at {0}:" + "\n {1}, {2}" +) ERROR_CONFIG_DELETE_TITLE = "Error in deleting configuration" ERROR_ID_NOT_FOUND = "Could not find resource: {0}" ERROR_FILE_DELETE_FAILED = "Delete failed. {0}" @@ -265,9 +271,10 @@ ERROR_LOAD_SYNTAX = "Could not load path: {0}\n\nSyntax error:\n{0}\n{1}" ERROR_METADATA_CHECKER_TITLE = "Flawed metadata warning" ERROR_METADATA_CHECKER_TEXT = ( - "{0} problem(s) found in metadata at {1}.\n" + - "Some functionality has been switched off.\n\n" + - "Run rose metadata-check for more info.") + "{0} problem(s) found in metadata at {1}.\n" + + "Some functionality has been switched off.\n\n" + + "Run rose metadata-check for more info." +) ERROR_NO_OUTPUT = "No output found for {0}" ERROR_NOT_FOUND = "Could not find path: {0}" ERROR_NOT_REGEX = "Could not compile expression: {0}\nError info: {1}" @@ -290,17 +297,22 @@ PAGE_WARNING_IGNORED_SECTION_TIP = "Ignored section" PAGE_WARNING_LATENT = "Latent page - no data" PAGE_WARNING_NO_CONTENT = "Blank page - no data" -PAGE_WARNING_NO_CONTENT_TIP = ("No associated configuration or summary data " + - "for this page.") +PAGE_WARNING_NO_CONTENT_TIP = ( + "No associated configuration or summary data " + "for this page." +) WARNING_APP_CONFIG_CREATE = "Cannot create another configuration here." WARNING_APP_CONFIG_CREATE_TITLE = "Warning - application configuration." -WARNING_CONFIG_DELETE = ("Cannot remove a whole configuration:\n{0}\n" + - "This must be done externally.") +WARNING_CONFIG_DELETE = ( + "Cannot remove a whole configuration:\n{0}\n" + + "This must be done externally." +) WARNING_CONFIG_DELETE_TITLE = "Can't remove configuration" WARNING_ERRORS_FOUND_ON_SAVE = "Errors found in {0}. Save anyway?" -WARNING_FILE_DELETE = ("Not a configuration file entry!\n" + - "This file must be manually removed" + - " in the filesystem:\n {0}.") +WARNING_FILE_DELETE = ( + "Not a configuration file entry!\n" + + "This file must be manually removed" + + " in the filesystem:\n {0}." +) WARNING_FILE_DELETE_TITLE = "Can't remove filesystem file" WARNING_CANNOT_ENABLE = "Warning - cannot override a trigger setting: {0}" WARNING_CANNOT_ENABLE_TITLE = "Warning - can't enable" @@ -314,14 +326,19 @@ WARNING_NOT_IGNORED = "Should be ignored " WARNING_NOT_TRIGGER = "Not part of the trigger mechanism" WARNING_USER_NOT_TRIGGER_IGNORED = ( - "User-ignored, but should be trigger-ignored") + "User-ignored, but should be trigger-ignored" +) WARNING_NOT_USER_IGNORABLE = "User-ignored, but is compulsory" WARNING_TYPE_ENABLED = "enabled" WARNING_TYPE_TRIGGER_IGNORED = "trigger-ignored" WARNING_TYPE_USER_IGNORED = "user-ignored" WARNING_TYPE_NOT_TRIGGER = "trigger" -WARNING_TYPES_IGNORE = [WARNING_TYPE_ENABLED, WARNING_TYPE_TRIGGER_IGNORED, - WARNING_TYPE_USER_IGNORED, WARNING_TYPE_NOT_TRIGGER] +WARNING_TYPES_IGNORE = [ + WARNING_TYPE_ENABLED, + WARNING_TYPE_TRIGGER_IGNORED, + WARNING_TYPE_USER_IGNORED, + WARNING_TYPE_NOT_TRIGGER, +] WARNING_INTEGER_OUT_OF_BOUNDS = "Warning: integer out of bounds" # Special metadata "type" values @@ -494,8 +511,9 @@ DIALOG_BODY_MACRO_CHANGES = "{0} {1}\n {2}\n" DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH = 150 # Must > raw CHANGES text above DIALOG_BODY_MACRO_CHANGES_NUM_HEIGHT = 3 # > Number, needs more height. -DIALOG_BODY_NL_CASE_CHANGE = ("Mixed-case names cause trouble in namelists." + - "\nSuggested: {0}") +DIALOG_BODY_NL_CASE_CHANGE = ( + "Mixed-case names cause trouble in namelists." + "\nSuggested: {0}" +) DIALOG_BODY_REMOVE_CONFIG = "Choose configuration" DIALOG_BODY_RENAME_CONFIG = "Choose configuration" DIALOG_BODY_REMOVE_SECTION = "Choose the section to remove" @@ -504,32 +522,33 @@ DIALOG_HELP_TITLE = "Help for {0}" DIALOG_LABEL_AUTOFIX = "Run built-in transform (fixer) macros?" DIALOG_LABEL_AUTOFIX_ALL = ( - "Run built-in transform (fixer) macros for all configurations?") + "Run built-in transform (fixer) macros for all configurations?" +) DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR = "Choose a section for the new variable:" DIALOG_LABEL_CHOOSE_SECTION_EDIT = "Choose a section to edit:" DIALOG_LABEL_CONFIG_CHOOSE_META = "Metadata id:" DIALOG_LABEL_CONFIG_CHOOSE_NAME = "New config name:" -DIALOG_LABEL_MACRO_TRANSFORM_CHANGES = ("{0}: {1}\n" + - "changes: {2}") -DIALOG_LABEL_MACRO_TRANSFORM_NONE = ( - "No configuration changes from this macro.") -DIALOG_LABEL_MACRO_VALIDATE_ISSUES = ("{0} {1}\n" + - "errors: {2}") +DIALOG_LABEL_MACRO_TRANSFORM_CHANGES = ( + "{0}: {1}\n" + "changes: {2}" +) +DIALOG_LABEL_MACRO_TRANSFORM_NONE = "No configuration changes from this macro." +DIALOG_LABEL_MACRO_VALIDATE_ISSUES = "{0} {1}\n" + "errors: {2}" DIALOG_LABEL_MACRO_VALIDATE_NONE = "Configuration OK for this macro." -DIALOG_LABEL_MACRO_WARN_ISSUES = ("warnings: {0}") +DIALOG_LABEL_MACRO_WARN_ISSUES = "warnings: {0}" DIALOG_LABEL_NULL_SECTION = "None" -DIALOG_LABEL_PREFERENCES = ("Please edit your site and user " + - "configurations to make changes.") -DIALOG_LABEL_UPGRADE = ( - "Click Upgrade Version cells to change target versions.") +DIALOG_LABEL_PREFERENCES = ( + "Please edit your site and user " + "configurations to make changes." +) +DIALOG_LABEL_UPGRADE = "Click Upgrade Version cells to change target versions." DIALOG_LABEL_UPGRADE_ALL = "Populate all possible versions" DIALOG_TIP_SUITE_RUN_HELP = "Read the help for rose suite-run" DIALOG_TEXT_MACRO_CHANGED = "changed" DIALOG_TEXT_MACRO_ERROR = "error" DIALOG_TEXT_MACRO_WARNING = "warning" -DIALOG_TEXT_SUITE_NOT_RUNNING = ("Cannot launch gcontrol: {0}") -DIALOG_TEXT_UNREGISTERED_SUITE = ("Cannot launch gcontrol: " + - "suite {0} is not registered.") +DIALOG_TEXT_SUITE_NOT_RUNNING = "Cannot launch gcontrol: {0}" +DIALOG_TEXT_UNREGISTERED_SUITE = ( + "Cannot launch gcontrol: " + "suite {0} is not registered." +) DIALOG_TITLE_MACRO_TRANSFORM = "{0} - Changes for {1}" DIALOG_TITLE_MACRO_TRANSFORM_NONE = "{0}" DIALOG_TITLE_MACRO_VALIDATE = "{0} - Issues for {1}" @@ -565,7 +584,7 @@ DIALOG_NODE_INFO_CHANGES = "{0}\n" DIALOG_NODE_INFO_DATA = "Data\n" DIALOG_NODE_INFO_DELIMITER = " " -DIALOG_NODE_INFO_METADATA = ("Metadata\n") +DIALOG_NODE_INFO_METADATA = "Metadata\n" DIALOG_NODE_INFO_MAX_LEN = 80 DIALOG_NODE_INFO_SUB_ATTRIBUTE = "{0}:" STACK_VIEW_TITLE = "Undo and Redo Stack Viewer" @@ -579,12 +598,18 @@ # Configure the colour used to indicate a latent page in the page tree. TITLE_PAGE_LATENT_COLOUR = "grey" -TITLE_PAGE_LATENT_MARKUP = ("{0}" + "") -TITLE_PAGE_PREVIEW_MARKUP = ("{0}" + "") +TITLE_PAGE_LATENT_MARKUP = ( + "{0}" + + "" +) +TITLE_PAGE_PREVIEW_MARKUP = ( + "{0}" + + "" +) TITLE_PAGE_ROOT_MARKUP = "{0}" TITLE_PAGE_SUITE = "suite conf" @@ -698,14 +723,13 @@ META_PROP_WIDGET_SUB_NS = "widget[rose-config-edit:sub-ns]" # Miscellaneous -COPYRIGHT = ( - """Copyright (C) British Crown (Met Office) & Contributors. - For full terms of use and licenses visit the Rose link above.""") +COPYRIGHT = """Copyright (C) British Crown (Met Office) & Contributors. + For full terms of use and licenses visit the Rose link above.""" ABOUT_TEXT = "GUI interface to edit rose suites." CREDIT = ( ["Ben Fitzpatrick", ["Principal Developer"]], ["Dimitrios Theodorakis", ["Migration to Rose 2.0"]], - ["Joseph Abram", ["Migration to Rose 2.0"]] + ["Joseph Abram", ["Migration to Rose 2.0"]], ) HELP_FILE = "rose-rug-config-edit.html" LAUNCH_COMMAND = "rose config-edit" @@ -720,10 +744,10 @@ VAR_ID_IN_CONFIG = "Variable id {0} from the configuration {1}" -_OVERRIDE_WARNING_PRIVATE = ( - "Cannot override: {0}={1} ({2}): not permitted.\n") +_OVERRIDE_WARNING_PRIVATE = "Cannot override: {0}={1} ({2}): not permitted.\n" _OVERRIDE_WARNING_TYPE = ( - "Cannot override: {0}={1}={2}: site/user conf: was {3}, supplied {4}\n") + "Cannot override: {0}={1}={2}: site/user conf: was {3}, supplied {4}\n" +) def false_function(*args, **kwargs): @@ -747,16 +771,23 @@ def load_override_config(sections, my_globals=None): cast_value = node.value name = key.replace("-", "_").upper() orig_value = my_globals[name] - if (not isinstance(orig_value, type(cast_value)) and - orig_value is not None): - sys.stderr.write(_OVERRIDE_WARNING_TYPE.format( - section, key, cast_value, - type(orig_value), type(cast_value)) + if ( + not isinstance(orig_value, type(cast_value)) + and orig_value is not None + ): + sys.stderr.write( + _OVERRIDE_WARNING_TYPE.format( + section, + key, + cast_value, + type(orig_value), + type(cast_value), + ) ) continue if name.startswith("_"): - sys.stderr.write(_OVERRIDE_WARNING_PRIVATE.format( - section, key, name) + sys.stderr.write( + _OVERRIDE_WARNING_PRIVATE.format(section, key, name) ) continue my_globals[name] = cast_value diff --git a/metomi/rose/config_editor/data.py b/metomi/rose/config_editor/data.py index 596cb7afe..08ac11442 100644 --- a/metomi/rose/config_editor/data.py +++ b/metomi/rose/config_editor/data.py @@ -46,12 +46,12 @@ import metomi.rose.variable -REC_NS_SECTION = re.compile(r"^(" + metomi.rose.META_PROP_NS + metomi.rose.CONFIG_DELIMITER + - r")(.*)$") +REC_NS_SECTION = re.compile( + r"^(" + metomi.rose.META_PROP_NS + metomi.rose.CONFIG_DELIMITER + r")(.*)$" +) class VarData(object): - """Stores past, present, and missing variables.""" def __init__(self, v_map, latent_v_map, save_v_map, latent_save_v_map): @@ -100,17 +100,17 @@ def get_var(self, section, option, save=False, skip_latent=False): nodes.pop() for node in nodes: for var in node.get(section, []): - if var.metadata['id'] == var_id: + if var.metadata["id"] == var_id: return var return None class SectData(object): - """Stores past, present, and missing sections.""" - def __init__(self, sections, latent_sections, save_sections, - latent_save_sections): + def __init__( + self, sections, latent_sections, save_sections, latent_save_sections + ): self.now = sections self.latent = latent_sections self.save = save_sections @@ -146,12 +146,23 @@ def get_sect(self, section, save=False, skip_latent=False): class ConfigData(object): - """Stores information about a configuration.""" - def __init__(self, config, s_config, directory, opt_conf_lookup, meta, - meta_id, meta_files, macros, config_type, - var_data=None, sect_data=None, is_preview=False): + def __init__( + self, + config, + s_config, + directory, + opt_conf_lookup, + meta, + meta_id, + meta_files, + macros, + config_type, + var_data=None, + sect_data=None, + is_preview=False, + ): self.config = config self.save_config = s_config self.directory = directory @@ -167,21 +178,29 @@ def __init__(self, config, s_config, directory, opt_conf_lookup, meta, class ConfigDataManager(object): - """Loads the information from the various configurations.""" - def __init__(self, util, reporter, page_ns_show_modes, - reload_ns_tree_func, opt_meta_paths=None, - no_warn=None): + def __init__( + self, + util, + reporter, + page_ns_show_modes, + reload_ns_tree_func, + opt_meta_paths=None, + no_warn=None, + ): """Load the root configuration and all its sub-configurations.""" self.util = util self.helper = metomi.rose.config_editor.data_helper.ConfigDataHelper( - self, util) + self, util + ) self.reporter = reporter self.page_ns_show_modes = page_ns_show_modes self.reload_ns_tree_func = reload_ns_tree_func self.config = {} # Stores configuration name: object - self._builtin_value_macro = metomi.rose.macros.value.ValueChecker() # value + self._builtin_value_macro = ( + metomi.rose.macros.value.ValueChecker() + ) # value self.builtin_macros = {} # Stores other Rose built-in macro instances self._bad_meta_dir_paths = [] # Stores flawed metadata directories. self.trigger = {} # Stores trigger macro instances per configuration @@ -189,7 +208,9 @@ def __init__(self, util, reporter, page_ns_show_modes, self.trigger_id_value_lookup = {} # Stores old values of trigger vars self.namespace_meta_lookup = {} # Stores titles etc of namespaces self.namespace_cached_statuses = { - 'latent': {}, 'ignored': {}} # Caches ns statuses + "latent": {}, + "ignored": {}, + } # Caches ns statuses self._config_section_namespace_map = {} # Store section namespaces self.locator = metomi.rose.resource.ResourceLocator(paths=sys.path) if opt_meta_paths is None: @@ -202,19 +223,30 @@ def __init__(self, util, reporter, page_ns_show_modes, self.saved_config_names = None self.top_level_name = None - def load(self, top_level_directory, config_obj_dict, - config_obj_type_dict=None, load_all_apps=False, - load_no_apps=False, metadata_off=False): + def load( + self, + top_level_directory, + config_obj_dict, + config_obj_type_dict=None, + load_all_apps=False, + load_no_apps=False, + metadata_off=False, + ): """Load configurations and their metadata.""" if config_obj_type_dict is None: config_obj_type_dict = {} if top_level_directory is not None: for filename in os.listdir(top_level_directory): - if filename in [metomi.rose.TOP_CONFIG_NAME, metomi.rose.SUB_CONFIG_NAME]: - self.load_top_config(top_level_directory, - load_all_apps=load_all_apps, - load_no_apps=load_no_apps, - metadata_off=metadata_off) + if filename in [ + metomi.rose.TOP_CONFIG_NAME, + metomi.rose.SUB_CONFIG_NAME, + ]: + self.load_top_config( + top_level_directory, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps, + metadata_off=metadata_off, + ) break else: self.load_top_config(None) @@ -225,13 +257,19 @@ def load(self, top_level_directory, config_obj_dict, self.top_level_directory = None for name, obj in list(config_obj_dict.items()): config_type = config_obj_type_dict.get(name) - self.load_config(config_name=name, config=obj, - config_type=config_type) + self.load_config( + config_name=name, config=obj, config_type=config_type + ) self.saved_config_names = set(self.config.keys()) - def load_top_config(self, top_level_directory, preview=False, - load_all_apps=False, load_no_apps=False, - metadata_off=False): + def load_top_config( + self, + top_level_directory, + preview=False, + load_all_apps=False, + load_no_apps=False, + metadata_off=False, + ): """Load the config at the top level and any sub configs.""" self.top_level_directory = top_level_directory @@ -240,8 +278,9 @@ def load_top_config(self, top_level_directory, preview=False, self.top_level_name = metomi.rose.config_editor.UNTITLED_NAME else: self.top_level_name = os.path.basename(top_level_directory) - config_container_dir = os.path.join(top_level_directory, - metomi.rose.SUB_CONFIGS_DIR) + config_container_dir = os.path.join( + top_level_directory, metomi.rose.SUB_CONFIGS_DIR + ) if os.path.isdir(config_container_dir): sub_contents = sorted(os.listdir(config_container_dir)) @@ -250,40 +289,57 @@ def load_top_config(self, top_level_directory, preview=False, preview = True else: for config_dir in sub_contents: - conf_path = os.path.join(config_container_dir, - config_dir) - if (os.path.isdir(conf_path) and - not config_dir.startswith('.')): + conf_path = os.path.join( + config_container_dir, config_dir + ) + if os.path.isdir( + conf_path + ) and not config_dir.startswith("."): self.app_count += 1 - if (self.app_count > - metomi.rose.config_editor.MAX_APPS_THRESHOLD): + if ( + self.app_count + > metomi.rose.config_editor.MAX_APPS_THRESHOLD + ): preview = True for config_dir in sub_contents: conf_path = os.path.join(config_container_dir, config_dir) - if (os.path.isdir(conf_path) and - not config_dir.startswith('.')): - self.load_config(conf_path, preview=preview, - metadata_off=metadata_off) + if os.path.isdir(conf_path) and not config_dir.startswith( + "." + ): + self.load_config( + conf_path, + preview=preview, + metadata_off=metadata_off, + ) self.load_config(top_level_directory) self.reload_ns_tree_func() def load_info_config(self, config_directory): """Load any information (discovery) config.""" - disc_path = os.path.join(config_directory, - metomi.rose.INFO_CONFIG_NAME) + disc_path = os.path.join( + config_directory, metomi.rose.INFO_CONFIG_NAME + ) if os.path.isfile(disc_path): config_obj = self.load_config_file(disc_path)[0] - self.load_config(config_name="/" + self.top_level_name + "-info", - config=config_obj, - config_type=metomi.rose.INFO_CONFIG_NAME) - - def load_config(self, config_directory=None, - config_name=None, config=None, - config_type=None, reload_tree_on=False, - skip_load_event=False, preview=False, - metadata_off=False): + self.load_config( + config_name="/" + self.top_level_name + "-info", + config=config_obj, + config_type=metomi.rose.INFO_CONFIG_NAME, + ) + + def load_config( + self, + config_directory=None, + config_name=None, + config=None, + config_type=None, + reload_tree_on=False, + skip_load_event=False, + preview=False, + metadata_off=False, + ): """Load the configuration and metadata.""" if config_directory is None: name = "/" + config_name.lstrip("/") @@ -292,19 +348,23 @@ def load_config(self, config_directory=None, if not skip_load_event: self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_CONFIG.format( - name.lstrip("/"))) + name.lstrip("/") + ) + ) else: config_directory = config_directory.rstrip("/") if config_directory != self.top_level_directory: # One of the sub configurations head, tail = os.path.split(config_directory) - name = '' + name = "" while tail != metomi.rose.SUB_CONFIGS_DIR: - name = "/" + os.path.join(tail, name).rstrip('/') + name = "/" + os.path.join(tail, name).rstrip("/") head, tail = os.path.split(head) name = "/" + name.lstrip("/") config_type = metomi.rose.SUB_CONFIG_NAME - elif metomi.rose.TOP_CONFIG_NAME not in os.listdir(config_directory): + elif metomi.rose.TOP_CONFIG_NAME not in os.listdir( + config_directory + ): # Just editing a single sub configuration, not a suite name = "/" + self.top_level_name config_type = metomi.rose.SUB_CONFIG_NAME @@ -317,21 +377,30 @@ def load_config(self, config_directory=None, if not skip_load_event: self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_CONFIG.format( - name.lstrip("/"))) - config_path = os.path.join(config_directory, metomi.rose.SUB_CONFIG_NAME) + name.lstrip("/") + ) + ) + config_path = os.path.join( + config_directory, metomi.rose.SUB_CONFIG_NAME + ) if not os.path.isfile(config_path): - if (os.path.abspath(config_directory) == - os.path.abspath(self.top_level_directory)): - config_path = os.path.join(config_directory, - metomi.rose.TOP_CONFIG_NAME) + if os.path.abspath(config_directory) == os.path.abspath( + self.top_level_directory + ): + config_path = os.path.join( + config_directory, metomi.rose.TOP_CONFIG_NAME + ) config_type = metomi.rose.TOP_CONFIG_NAME else: text = metomi.rose.config_editor.ERROR_NOT_FOUND.format( - config_path) - title = metomi.rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + config_path + ) + title = ( + metomi.rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR + ) metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) sys.exit(2) if config_directory != self.top_level_directory and preview: @@ -345,14 +414,15 @@ def load_config(self, config_directory=None, meta_config_tree = metomi.rose.config_tree.ConfigTree() elif metadata_off: meta_config_tree = self.load_meta_config_tree( - config_type=config_type, - opt_meta_paths=self.opt_meta_paths) + config_type=config_type, opt_meta_paths=self.opt_meta_paths + ) else: try: meta_config_tree = self.load_meta_config_tree( - config, config_directory, + config, + config_directory, config_type=config_type, - opt_meta_paths=self.opt_meta_paths + opt_meta_paths=self.opt_meta_paths, ) except IOError as exc: metomi.rose.gtk.dialog.run_exception_dialog(exc) @@ -364,14 +434,24 @@ def load_config(self, config_directory=None, macro_module_prefix = self.helper.get_macro_module_prefix(name) meta_files = self.load_meta_files(meta_config_tree) macros = metomi.rose.macro.load_meta_macro_modules( - meta_files, module_prefix=macro_module_prefix) + meta_files, module_prefix=macro_module_prefix + ) meta_id = self.helper.get_config_meta_flag( - name, from_this_config_obj=config) + name, from_this_config_obj=config + ) # Initialise configuration data object. - self.config[name] = ConfigData(config, s_config, config_directory, - opt_conf_lookup, meta_config, - meta_id, meta_files, macros, - config_type, is_preview=preview) + self.config[name] = ConfigData( + config, + s_config, + config_directory, + opt_conf_lookup, + meta_config, + meta_id, + meta_files, + macros, + config_type, + is_preview=preview, + ) self.load_builtin_macros(name) self.load_file_metadata(name) @@ -380,16 +460,20 @@ def load_config(self, config_directory=None, # Load section and variable data into the object. sects, l_sects = self.load_sections_from_config(name) s_sects, s_l_sects = self.load_sections_from_config(name) - self.config[name].sections = SectData(sects, l_sects, s_sects, - s_l_sects) + self.config[name].sections = SectData( + sects, l_sects, s_sects, s_l_sects + ) var, l_var, s_var, s_l_var = self.load_vars_from_config( - name, return_copies=True) + name, return_copies=True + ) self.config[name].vars = VarData(var, l_var, s_var, s_l_var) if not skip_load_event: self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_METADATA.format( - name.lstrip("/"))) + name.lstrip("/") + ) + ) # Process namespaces and ignored statuses. self.load_node_namespaces(name) self.load_node_namespaces(name, from_saved=True) @@ -399,16 +483,21 @@ def load_config(self, config_directory=None, self.reload_ns_tree_func() def load_config_file(self, config_path): - """Return two copies of the metomi.rose.config.ConfigNode at config_path.""" + """Return two copies of the + metomi.rose.config.ConfigNode at config_path. + + """ + try: config = metomi.rose.config.load(config_path) except metomi.rose.config.ConfigSyntaxError as exc: text = metomi.rose.config_editor.ERROR_LOAD_SYNTAX.format( - config_path, exc) + config_path, exc + ) title = metomi.rose.config_editor.DIALOG_TITLE_CRITICAL_ERROR metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) sys.exit(2) else: master_config = metomi.rose.config.load(config_path) @@ -421,7 +510,9 @@ def load_optional_configs(self, config_directory): opt_conf_lookup = {} if config_directory is None: return opt_conf_lookup - opt_dir = os.path.join(config_directory, metomi.rose.config.OPT_CONFIG_DIR) + opt_dir = os.path.join( + config_directory, metomi.rose.config.OPT_CONFIG_DIR + ) if not os.path.isdir(opt_dir): return opt_conf_lookup opt_exceptions = {} @@ -446,19 +537,26 @@ def load_optional_configs(self, config_directory): for path, exc in sorted(opt_exceptions.items()): err_text += err_format.format(path, type(exc).__name__, exc) err_text = err_text.rstrip() - text = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS.format(err_text) + text = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS.format( + err_text + ) title = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS_TITLE - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title=title, modal=False) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, + title=title, + modal=False, + ) return opt_conf_lookup def load_builtin_macros(self, config_name): """Load Rose builtin macros.""" self.builtin_macros[config_name] = { - metomi.rose.META_PROP_COMPULSORY: - metomi.rose.macros.compulsory.CompulsoryChecker(), - metomi.rose.META_PROP_TYPE: - self._builtin_value_macro} + metomi.rose.META_PROP_COMPULSORY: ( + metomi.rose.macros.compulsory.CompulsoryChecker() + ), + metomi.rose.META_PROP_TYPE: self._builtin_value_macro, + } def load_sections_from_config(self, config_name, save=False): """Return maps of section objects from the configuration.""" @@ -477,44 +575,69 @@ def load_sections_from_config(self, config_name, save=False): sect_map[""].options.append(section) continue meta_data = self.helper.get_metadata_for_config_id( - "", config_name) - sect_map.update({"": metomi.rose.section.Section("", [section], - meta_data)}) + "", config_name + ) + sect_map.update( + {"": metomi.rose.section.Section("", [section], meta_data)} + ) real_sect_ids.append("") continue - meta_data = self.helper.get_metadata_for_config_id(section, - config_name) + meta_data = self.helper.get_metadata_for_config_id( + section, config_name + ) options = list(node.value.keys()) - sect_map.update({section: metomi.rose.section.Section(section, options, - meta_data)}) + sect_map.update( + { + section: metomi.rose.section.Section( + section, options, meta_data + ) + } + ) sect_map[section].comments = list(node.comments) real_sect_ids.append(section) real_sect_basic_ids.extend( - [metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section), - metomi.rose.macro.REC_ID_STRIP.sub("", section)] + [ + metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section), + metomi.rose.macro.REC_ID_STRIP.sub("", section), + ] ) if node.is_ignored(): reason = {} - if node.state == metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: - reason = {metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG} - elif (node.state == - metomi.rose.config.ConfigNode.STATE_USER_IGNORED): - reason = {metomi.rose.variable.IGNORED_BY_USER: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG} + if ( + node.state + == metomi.rose.config.ConfigNode.STATE_SYST_IGNORED + ): + reason = { + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } + elif ( + node.state + == metomi.rose.config.ConfigNode.STATE_USER_IGNORED + ): + reason = { + metomi.rose.variable.IGNORED_BY_USER: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } sect_map[section].ignored_reason.update(reason) if "" not in sect_map: # This always exists for a configuration. - meta_data = self.helper.get_metadata_for_config_id("", - config_name) - sect_map.update({"": metomi.rose.section.Section("", [], meta_data)}) + meta_data = self.helper.get_metadata_for_config_id("", config_name) + sect_map.update( + {"": metomi.rose.section.Section("", [], meta_data)} + ) real_sect_ids.append("") for setting_id, sect_node in list(meta_config.value.items()): if sect_node.is_ignored() or isinstance(sect_node.value, str): continue section, option = self.util.get_section_option_from_id(setting_id) - if (option is not None or section in real_sect_ids or - section in real_sect_basic_ids): + if ( + option is not None + or section in real_sect_ids + or section in real_sect_basic_ids + ): continue meta_data = {} for prop_opt, opt_node in list(sect_node.value.items()): @@ -522,18 +645,30 @@ def load_sections_from_config(self, config_name, save=False): continue meta_data.update({prop_opt: opt_node.value}) latent_section_name = section - if (meta_data.get(metomi.rose.META_PROP_DUPLICATE) == - metomi.rose.META_PROP_VALUE_TRUE): + if ( + meta_data.get(metomi.rose.META_PROP_DUPLICATE) + == metomi.rose.META_PROP_VALUE_TRUE + ): latent_section_name = section + "({0})".format( - metomi.rose.CONFIG_SETTING_INDEX_DEFAULT) - meta_data.update({'id': latent_section_name}) - if section not in ['ns', 'file:*']: - latent_sect_map[latent_section_name] = metomi.rose.section.Section( - latent_section_name, [], meta_data) + metomi.rose.CONFIG_SETTING_INDEX_DEFAULT + ) + meta_data.update({"id": latent_section_name}) + if section not in ["ns", "file:*"]: + latent_sect_map[latent_section_name] = ( + metomi.rose.section.Section( + latent_section_name, [], meta_data + ) + ) return sect_map, latent_sect_map - def load_vars_from_config(self, config_name, only_this_section=None, - save=False, update=False, return_copies=False): + def load_vars_from_config( + self, + config_name, + only_this_section=None, + save=False, + update=False, + return_copies=False, + ): """Return maps of variables from the configuration""" config_data = self.config[config_name] if save: @@ -573,31 +708,45 @@ def load_vars_from_config(self, config_name, only_this_section=None, flags = self.load_option_flags(config_name, section, option) ignored_reason = {} if section_map[section].ignored_reason: - ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_SECTION: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) + ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_SECTION: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } + ) if node.state == metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: - ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) - elif (node.state == - metomi.rose.config.ConfigNode.STATE_USER_IGNORED): - ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_USER: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) + ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } + ) + elif ( + node.state == metomi.rose.config.ConfigNode.STATE_USER_IGNORED + ): + ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_USER: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } + ) cfg_comments = node.comments var_id = self.util.get_id_from_section_option(section, option) real_var_ids.append(var_id) - meta_data = self.helper.get_metadata_for_config_id(var_id, - config_name) + meta_data = self.helper.get_metadata_for_config_id( + var_id, config_name + ) var_map.setdefault(section, []) if return_copies: var_map_copy.setdefault(section, []) if update: - id_list = [v.metadata['id'] for v in var_map[section]] + id_list = [v.metadata["id"] for v in var_map[section]] if var_id in id_list: for i, var in enumerate(var_map[section]): - if var.metadata['id'] == var_id: + if var.metadata["id"] == var_id: var_map[section].pop(i) break var_map[section].append( @@ -608,7 +757,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, ignored_reason, error={}, flags=flags, - comments=cfg_comments + comments=cfg_comments, ) ) if return_copies: @@ -620,7 +769,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, ignored_reason, error={}, flags=flags, - comments=cfg_comments + comments=cfg_comments, ) ) id_node_stack = list(meta_config.value.items()) @@ -629,13 +778,14 @@ def load_vars_from_config(self, config_name, only_this_section=None, if sect_node.is_ignored() or isinstance(sect_node.value, str): continue section, option = self.util.get_section_option_from_id(setting_id) - if section in ['ns', 'file:*']: + if section in ["ns", "file:*"]: continue if section in basic_dupl_map: # There is a matching duplicate e.g. foo(3) or foo{bar}(1) for dupl_section in basic_dupl_map[section]: dupl_id = self.util.get_id_from_section_option( - dupl_section, option) + dupl_section, option + ) id_node_stack.insert(0, (dupl_id, sect_node)) continue if only_this_section is not None and section != only_this_section: @@ -646,14 +796,20 @@ def load_vars_from_config(self, config_name, only_this_section=None, if setting_id in real_var_ids: # This variable isn't missing, so skip. continue - if (meta_config.get_value([section, metomi.rose.META_PROP_DUPLICATE]) == - metomi.rose.META_PROP_VALUE_TRUE and - section not in basic_dupl_map and - config.get([section]) is None): + if ( + meta_config.get_value( + [section, metomi.rose.META_PROP_DUPLICATE] + ) + == metomi.rose.META_PROP_VALUE_TRUE + and section not in basic_dupl_map + and config.get([section]) is None + ): section = section + "({0})".format( - metomi.rose.CONFIG_SETTING_INDEX_DEFAULT) + metomi.rose.CONFIG_SETTING_INDEX_DEFAULT + ) setting_id = self.util.get_id_from_section_option( - section, option) + section, option + ) flags = self.load_option_flags(config_name, section, option) ignored_reason = {} sect_data = section_map.get(section) @@ -661,23 +817,25 @@ def load_vars_from_config(self, config_name, only_this_section=None, sect_data = latent_section_map.get(section) if sect_data is not None and sect_data.ignored_reason: ignored_reason = { - metomi.rose.variable.IGNORED_BY_SECTION: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG} + metomi.rose.variable.IGNORED_BY_SECTION: ( + metomi.rose.config_editor.IGNORED_STATUS_CONFIG + ) + } meta_data = {} for prop_opt, opt_node in list(sect_node.value.items()): if opt_node.is_ignored(): continue meta_data.update({prop_opt: opt_node.value}) - meta_data.update({'id': setting_id}) + meta_data.update({"id": setting_id}) value = metomi.rose.variable.get_value_from_metadata(meta_data) latent_var_map.setdefault(section, []) if return_copies: latent_var_map_copy.setdefault(section, []) if update: - id_list = [v.metadata['id'] for v in latent_var_map[section]] + id_list = [v.metadata["id"] for v in latent_var_map[section]] if setting_id in id_list: for var in latent_var_map[section]: - if var.metadata['id'] == setting_id: + if var.metadata["id"] == setting_id: latent_var_map[section].remove(var) latent_var_map[section].append( metomi.rose.variable.Variable( @@ -686,7 +844,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, meta_data, ignored_reason, error={}, - flags=flags + flags=flags, ) ) if return_copies: @@ -697,7 +855,7 @@ def load_vars_from_config(self, config_name, only_this_section=None, meta_data, ignored_reason, error={}, - flags=flags + flags=flags, ) ) if return_copies: @@ -717,27 +875,33 @@ def _load_dupl_sect_map(self, basic_dupl_map, section): def load_option_flags(self, config_name, section, option): """Load flags for an option.""" flags = {} - opt_conf_flags = self._load_opt_conf_flags(config_name, - section, option) + opt_conf_flags = self._load_opt_conf_flags( + config_name, section, option + ) if opt_conf_flags: - flags.update({metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: - opt_conf_flags}) + flags.update( + {metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: opt_conf_flags} + ) return flags def _load_opt_conf_flags(self, config_name, section, option): opt_config_map = self.config[config_name].opt_configs - opt_conf_diff_format = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_STATE + opt_conf_diff_format = ( + metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_STATE + ) opt_flags = {} for opt_name in sorted(opt_config_map): opt_config = opt_config_map[opt_name] opt_node = opt_config.get([section, option]) if opt_node is not None: opt_sect_node = opt_config.get([section]) - text = opt_conf_diff_format.format(opt_sect_node.state, - section, - opt_node.state, - option, - opt_node.value) + text = opt_conf_diff_format.format( + opt_sect_node.state, + section, + opt_node.state, + option, + opt_node.value, + ) opt_flags[opt_name] = text return opt_flags @@ -755,10 +919,13 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): enabled_state = metomi.rose.config.ConfigNode.STATE_NORMAL sections_to_be_dumped = [] if only_this_ns is None: - allowed_sections = set(list(sect_map.keys()) + list(var_map.keys())) + allowed_sections = set( + list(sect_map.keys()) + list(var_map.keys()) + ) else: allowed_sections = self.helper.get_sections_from_namespace( - only_this_ns) + only_this_ns + ) for section in sect_map: if only_this_ns is not None and section not in allowed_sections: continue @@ -767,7 +934,7 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): variables = var_map.get(section, []) for variable in variables: if only_this_ns is not None: - if variable.metadata['full_ns'] != only_this_ns: + if variable.metadata["full_ns"] != only_this_ns: continue option = variable.name if not variable.name: @@ -776,15 +943,23 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): value = variable.value var_state = enabled_state if variable.ignored_reason: - if (metomi.rose.variable.IGNORED_BY_USER in - variable.ignored_reason): + if ( + metomi.rose.variable.IGNORED_BY_USER + in variable.ignored_reason + ): var_state = user_ignored_state - elif (metomi.rose.variable.IGNORED_BY_SYSTEM in - variable.ignored_reason): + elif ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in variable.ignored_reason + ): var_state = syst_ignored_state var_comments = variable.comments - config.set([section, option], value, state=var_state, - comments=var_comments) + config.set( + [section, option], + value, + state=var_state, + comments=var_comments, + ) for section_id in sections_to_be_dumped: comments = sect_map[section_id].comments if not section_id: @@ -792,11 +967,15 @@ def dump_to_internal_config(self, config_name, only_this_ns=None): continue section_state = enabled_state if sect_map[section_id].ignored_reason: - if (metomi.rose.variable.IGNORED_BY_USER in - sect_map[section_id].ignored_reason): + if ( + metomi.rose.variable.IGNORED_BY_USER + in sect_map[section_id].ignored_reason + ): section_state = user_ignored_state - elif (metomi.rose.variable.IGNORED_BY_SYSTEM in - sect_map[section_id].ignored_reason): + elif ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in sect_map[section_id].ignored_reason + ): section_state = syst_ignored_state node = config.get([section_id]) if node is None: @@ -813,14 +992,21 @@ def load_meta_path(self, config=None, directory=None): def clear_meta_lookups(self, config_name): for ns in list(self.namespace_meta_lookup.keys()): - if (ns.startswith(config_name) and - self.util.split_full_ns(self, ns)[0] == config_name): + if ( + ns.startswith(config_name) + and self.util.split_full_ns(self, ns)[0] == config_name + ): self.namespace_meta_lookup.pop(ns) if config_name in self._config_section_namespace_map: self._config_section_namespace_map.pop(config_name) - def load_meta_config_tree(self, config=None, directory=None, - config_type=None, opt_meta_paths=None): + def load_meta_config_tree( + self, + config=None, + directory=None, + config_type=None, + opt_meta_paths=None, + ): """Load the main metadata, and any specified in 'config'.""" if config is None: config = metomi.rose.config.ConfigNode() @@ -831,7 +1017,7 @@ def load_meta_config_tree(self, config=None, directory=None, config_type=config_type, error_handler=error_handler, opt_meta_paths=opt_meta_paths, - no_warn=self.no_warn + no_warn=self.no_warn, ) def load_meta_files(self, config_tree): @@ -848,21 +1034,33 @@ def filter_meta_config(self, config_name): meta_config = config_data.meta directory = config_data.directory meta_dir_path = self.load_meta_path(config, directory)[0] - reports = metomi.rose.metadata_check.metadata_check(meta_config, - directory) + reports = metomi.rose.metadata_check.metadata_check( + meta_config, directory + ) if reports and meta_dir_path not in self._bad_meta_dir_paths: # There are problems with some metadata. - title = metomi.rose.config_editor.ERROR_METADATA_CHECKER_TITLE.format( - meta_dir_path) - text = metomi.rose.config_editor.ERROR_METADATA_CHECKER_TEXT.format( - len(reports), meta_dir_path) + title = ( + metomi.rose.config_editor.ERROR_METADATA_CHECKER_TITLE.format( + meta_dir_path + ) + ) + text = ( + metomi.rose.config_editor.ERROR_METADATA_CHECKER_TEXT.format( + len(reports), meta_dir_path + ) + ) self._bad_meta_dir_paths.append(meta_dir_path) reports_map = {None: reports} reports_text = metomi.rose.macro.get_reports_as_text( - reports_map, "metomi.rose.metadata_check.MetadataChecker") - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title, modal=False, - extra_text=reports_text) + reports_map, "metomi.rose.metadata_check.MetadataChecker" + ) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, + title, + modal=False, + extra_text=reports_text, + ) for report in reports: if report.option != metomi.rose.META_PROP_TRIGGER: meta_config.unset([report.section, report.option]) @@ -894,24 +1092,27 @@ def load_ignored_data(self, config_name): config_for_macro.set(keylist, copy.deepcopy(node.value)) meta_config = self.config[config_name].meta bad_list = self.trigger[config_name].validate_dependencies( - config_for_macro, meta_config) + config_for_macro, meta_config + ) if bad_list: self.trigger[config_name].trigger_family_lookup.clear() event = metomi.rose.config_editor.EVENT_INVALID_TRIGGERS.format( - config_name.strip("/")) + config_name.strip("/") + ) self.reporter.report(event, self.reporter.KIND_ERR) return trig_config = self.trigger[config_name].transform( - config_for_macro, meta_config)[0] + config_for_macro, meta_config + )[0] self.trigger_id_value_lookup.setdefault(config_name, {}) var_id_map = {} for variables in list(var_map.values()): for variable in variables: - var_id_map.update({variable.metadata['id']: variable}) + var_id_map.update({variable.metadata["id"]: variable}) latent_var_id_map = {} for variables in list(latent_var_map.values()): for variable in variables: - latent_var_id_map.update({variable.metadata['id']: variable}) + latent_var_id_map.update({variable.metadata["id"]: variable}) trig_ids = list(self.trigger[config_name].trigger_family_lookup.keys()) while trig_ids: var_id = trig_ids.pop() @@ -925,12 +1126,16 @@ def load_ignored_data(self, config_name): if sect.endswith(")"): continue node = meta_config.get([sect, metomi.rose.META_PROP_DUPLICATE]) - if node is not None and node.value == metomi.rose.META_PROP_VALUE_TRUE: + if ( + node is not None + and node.value == metomi.rose.META_PROP_VALUE_TRUE + ): search_string = sect + "(" for section in sect_map: if section.startswith(search_string): new_id = self.util.get_id_from_section_option( - section, opt) + section, opt + ) trig_ids.append(new_id) id_node_map = {} id_node_map.update(sect_map) @@ -942,7 +1147,7 @@ def load_ignored_data(self, config_name): for setting_id, node_inst in list(id_node_map.items()): is_latent = False section, option = self.util.get_section_option_from_id(setting_id) - is_section = (option is None) + is_section = option is None if is_section: if section not in sect_map: is_latent = True @@ -952,15 +1157,19 @@ def load_ignored_data(self, config_name): trig_cfg_node = trig_config.get([section, option]) if trig_cfg_node is None: # Latent variable or sections cannot be user-ignored. - if (setting_id in ignored_dict and - setting_id not in enabled_dict): + if ( + setting_id in ignored_dict + and setting_id not in enabled_dict + ): trig_cfg_state = syst_ignored_state else: trig_cfg_state = enabled_state else: trig_cfg_state = trig_cfg_node.state - if (trig_cfg_state == enabled_state and - not node_inst.ignored_reason): + if ( + trig_cfg_state == enabled_state + and not node_inst.ignored_reason + ): # For speed, skip the rest of the checking. # Doc table: E -> E continue @@ -971,16 +1180,24 @@ def load_ignored_data(self, config_name): # It should be trigger-ignored. # Doc table: * -> I_t info = ignored_dict.get(setting_id) - if metomi.rose.variable.IGNORED_BY_SYSTEM not in ignored_reasons: + if ( + metomi.rose.variable.IGNORED_BY_SYSTEM + not in ignored_reasons + ): help_str = ", ".join(list(info.values())) if metomi.rose.variable.IGNORED_BY_USER in ignored_reasons: # It is user-ignored but should be trigger-ignored. # Doc table: I_u -> I_t if node_is_compulsory: # Doc table: I_u -> I_t -> compulsory - key = metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED - val = getattr(metomi.rose.config_editor, - "WARNING_USER_NOT_TRIGGER_IGNORED") + key = ( + metomi.rose.config_editor + .WARNING_TYPE_USER_IGNORED + ) + val = getattr( + metomi.rose.config_editor, + "WARNING_USER_NOT_TRIGGER_IGNORED", + ) node_inst.warning.update({key: val}) else: # Doc table: I_u -> I_t -> optional @@ -990,15 +1207,26 @@ def load_ignored_data(self, config_name): # Doc table: E -> I_t if is_latent: # Fix this for latent settings. - node_inst.ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_CONFIG}) + node_inst.ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor + .IGNORED_STATUS_CONFIG + ) + } + ) else: # Flag an error for real settings. - node_inst.error.update({ - metomi.rose.config_editor.WARNING_TYPE_ENABLED: - (metomi.rose.config_editor.WARNING_NOT_IGNORED + - help_str)}) + node_inst.error.update( + { + metomi.rose.config_editor + .WARNING_TYPE_ENABLED: ( + metomi.rose.config_editor + .WARNING_NOT_IGNORED + + help_str + ) + } + ) else: # Otherwise, they both agree about trigger-ignored. # Doc table: I_t -> I_t @@ -1006,25 +1234,38 @@ def load_ignored_data(self, config_name): elif metomi.rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: # It should be enabled, but is trigger-ignored. # Doc table: I_t - if (setting_id in enabled_dict and - setting_id not in ignored_dict): + if ( + setting_id in enabled_dict + and setting_id not in ignored_dict + ): # It is a valid trigger. # Doc table: I_t -> E parents = self.trigger[config_name].enabled_dict.get( - setting_id) - help_str = (metomi.rose.config_editor.WARNING_NOT_ENABLED + - ', '.join(parents)) - err_type = metomi.rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED + setting_id + ) + help_str = ( + metomi.rose.config_editor.WARNING_NOT_ENABLED + + ", ".join(parents) + ) + err_type = ( + metomi.rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED + ) node_inst.error.update({err_type: help_str}) - elif (setting_id not in enabled_dict and - setting_id not in ignored_dict): + elif ( + setting_id not in enabled_dict + and setting_id not in ignored_dict + ): # It is not a valid trigger. # Doc table: I_t -> not trigger if node_is_compulsory: # This is an error for compulsory variables. # Doc table: I_t -> not trigger -> compulsory - help_str = metomi.rose.config_editor.WARNING_NOT_TRIGGER - err_type = metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER + help_str = ( + metomi.rose.config_editor.WARNING_NOT_TRIGGER + ) + err_type = ( + metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER + ) node_inst.error.update({err_type: help_str}) else: # Overlook for optional variables. @@ -1038,8 +1279,12 @@ def load_ignored_data(self, config_name): # Compulsory settings should not be user-ignored. # Doc table: I_u -> E -> compulsory # Doc table: I_u -> not trigger -> compulsory - help_str = metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE - err_type = metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED + help_str = ( + metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE + ) + err_type = ( + metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED + ) node_inst.error.update({err_type: help_str}) # Remaining possibilities are not a problem: # Doc table: E -> E, E -> not trigger @@ -1064,8 +1309,10 @@ def load_file_metadata(self, config_name, section_name=None): continue if not sect_node.is_ignored() and section.startswith("file:"): file_sections.append(section) - if (sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) == - metomi.rose.META_PROP_VALUE_TRUE): + if ( + sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) + == metomi.rose.META_PROP_VALUE_TRUE + ): duplicate_file_sections.append(section) # Remove metadata for individual duplicate sections - no need. for section in list(file_sections): @@ -1081,23 +1328,29 @@ def load_file_metadata(self, config_name, section_name=None): if not sect_node.is_ignored() and setting_id.startswith("file:*="): file_ids.append(setting_id) for section in file_sections: - title = meta_config.get_value([section, metomi.rose.META_PROP_TITLE]) + title = meta_config.get_value( + [section, metomi.rose.META_PROP_TITLE] + ) if title is None: - meta_config.set([section, metomi.rose.META_PROP_TITLE], - section.replace("file:", "", 1)) + meta_config.set( + [section, metomi.rose.META_PROP_TITLE], + section.replace("file:", "", 1), + ) for file_entry in file_ids: sect_node = meta_config.get([file_entry]) for meta_prop, opt_node in list(sect_node.value.items()): if opt_node.is_ignored(): continue prop_val = opt_node.value - new_id = section + '=' + file_entry.replace( - 'file:*=', '', 1) + new_id = ( + section + "=" + file_entry.replace("file:*=", "", 1) + ) if meta_config.get([new_id, meta_prop]) is None: meta_config.set([new_id, meta_prop], prop_val) - def load_node_namespaces(self, config_name, only_this_section=None, - from_saved=False): + def load_node_namespaces( + self, config_name, only_this_section=None, from_saved=False + ): """Load namespaces for variables and sections.""" config_sections = self.config[config_name].sections config_vars = self.config[config_name].vars @@ -1119,17 +1372,18 @@ def load_node_namespaces(self, config_name, only_this_section=None, def load_ns_for_node(self, node, config_name): """Load a namespace for a variable or section.""" - node_id = node.metadata.get('id') + node_id = node.metadata.get("id") section, option = self.util.get_section_option_from_id(node_id) subspace = node.metadata.get(metomi.rose.META_PROP_NS) if subspace is None or option is None: new_namespace = self.helper.get_default_section_namespace( - section, config_name) + section, config_name + ) else: - new_namespace = config_name + '/' + subspace - if new_namespace == config_name + '/': + new_namespace = config_name + "/" + subspace + if new_namespace == config_name + "/": new_namespace = config_name - node.metadata['full_ns'] = new_namespace + node.metadata["full_ns"] = new_namespace return new_namespace def load_metadata_for_namespaces(self, config_name): @@ -1139,13 +1393,12 @@ def load_metadata_for_namespaces(self, config_name): for setting_id, sect_node in list(meta_config.value.items()): if sect_node.is_ignored(): continue - section, option = self.util.get_section_option_from_id( - setting_id) - is_ns = (section == "ns") + section, option = self.util.get_section_option_from_id(setting_id) + is_ns = section == "ns" is_duplicate_section = ( - self.util.get_section_option_from_id(section)[1] is None and - sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) == - metomi.rose.META_PROP_VALUE_TRUE + self.util.get_section_option_from_id(section)[1] is None + and sect_node.get_value([metomi.rose.META_PROP_DUPLICATE]) + == metomi.rose.META_PROP_VALUE_TRUE ) if is_ns or is_duplicate_section: if is_ns: @@ -1157,9 +1410,9 @@ def load_metadata_for_namespaces(self, config_name): else: subspace = sect_node.get_value([metomi.rose.META_PROP_NS]) if subspace is None: - namespace = ( - self.helper.get_default_section_namespace( - section, config_name)) + namespace = self.helper.get_default_section_namespace( + section, config_name + ) else: if subspace: namespace = config_name + "/" + subspace @@ -1180,8 +1433,8 @@ def load_metadata_for_namespaces(self, config_name): ns_metadata.update({option: value}) ns_sections = {} # Namespace-sections key value pairs. for variable in config_data.vars.get_all(): - ns = variable.metadata['full_ns'] - var_id = variable.metadata['id'] + ns = variable.metadata["full_ns"] + var_id = variable.metadata["id"] sect = self.util.get_section_option_from_id(var_id)[0] ns_sections.setdefault(ns, []) if sect not in ns_sections[ns]: @@ -1191,7 +1444,9 @@ def load_metadata_for_namespaces(self, config_name): self.namespace_meta_lookup.setdefault(ns, {}) ns_metadata = self.namespace_meta_lookup[ns] if metomi.rose.META_PROP_MACRO in ns_metadata: - ns_metadata[metomi.rose.META_PROP_MACRO] += ", " + macro_info + ns_metadata[metomi.rose.META_PROP_MACRO] += ( + ", " + macro_info + ) else: ns_metadata[metomi.rose.META_PROP_MACRO] = macro_info default_ns_sections = {} @@ -1207,25 +1462,33 @@ def load_metadata_for_namespaces(self, config_name): for ns in ns_sections: self.namespace_meta_lookup.setdefault(ns, {}) ns_metadata = self.namespace_meta_lookup[ns] - ns_metadata['sections'] = ns_sections[ns] + ns_metadata["sections"] = ns_sections[ns] for ns_section in ns_sections[ns]: # Loop over metadata from contributing sections. # Note: rogue-variable section metadata can be overridden. - metadata = self.helper.get_metadata_for_config_id(ns_section, - config_name) + metadata = self.helper.get_metadata_for_config_id( + ns_section, config_name + ) for key, value in list(metadata.items()): - if (ns_section not in default_ns_sections.get(ns, []) and - key in [metomi.rose.META_PROP_TITLE, metomi.rose.META_PROP_SORT_KEY, - metomi.rose.META_PROP_DESCRIPTION]): + if ns_section not in default_ns_sections.get( + ns, [] + ) and key in [ + metomi.rose.META_PROP_TITLE, + metomi.rose.META_PROP_SORT_KEY, + metomi.rose.META_PROP_DESCRIPTION, + ]: # ns created from variables, not a section - no title. continue if key == metomi.rose.META_PROP_MACRO: macro_info = value if key in ns_metadata: ns_metadata[metomi.rose.META_PROP_MACRO] += ( - ", " + macro_info) + ", " + macro_info + ) else: - ns_metadata[metomi.rose.META_PROP_MACRO] = macro_info + ns_metadata[metomi.rose.META_PROP_MACRO] = ( + macro_info + ) else: ns_metadata.setdefault(key, value) self.load_namespace_has_sub_data(config_name) @@ -1233,19 +1496,30 @@ def load_metadata_for_namespaces(self, config_name): icon_path = self.helper.get_icon_path_for_config(config_name) self.namespace_meta_lookup.setdefault(config_name, {}) self.namespace_meta_lookup[config_name].setdefault( - "icon", icon_path) - if self.config[config_name].config_type == metomi.rose.TOP_CONFIG_NAME: + "icon", icon_path + ) + if ( + self.config[config_name].config_type + == metomi.rose.TOP_CONFIG_NAME + ): self.namespace_meta_lookup[config_name].setdefault( metomi.rose.META_PROP_TITLE, - metomi.rose.config_editor.TITLE_PAGE_SUITE) + metomi.rose.config_editor.TITLE_PAGE_SUITE, + ) self.namespace_meta_lookup[config_name].setdefault( - metomi.rose.META_PROP_SORT_KEY, " 1") - elif self.config[config_name].config_type == metomi.rose.INFO_CONFIG_NAME: + metomi.rose.META_PROP_SORT_KEY, " 1" + ) + elif ( + self.config[config_name].config_type + == metomi.rose.INFO_CONFIG_NAME + ): self.namespace_meta_lookup[config_name].setdefault( metomi.rose.META_PROP_TITLE, - metomi.rose.config_editor.TITLE_PAGE_INFO) + metomi.rose.config_editor.TITLE_PAGE_INFO, + ) self.namespace_meta_lookup[config_name].setdefault( - metomi.rose.META_PROP_SORT_KEY, " 0") + metomi.rose.META_PROP_SORT_KEY, " 0" + ) def load_namespace_has_sub_data(self, config_name=None): """Load namespace sub-data status.""" @@ -1265,11 +1539,14 @@ def load_namespace_has_sub_data(self, config_name=None): file_root_ns = alt_config_name + file_ns self.namespace_meta_lookup.setdefault(file_root_ns, {}) self.namespace_meta_lookup[file_root_ns].setdefault( - "has_sub_data", True) + "has_sub_data", True + ) # Duplicate root pages have summary data for their members. for ns, prop_map in list(self.namespace_meta_lookup.items()): if config_name is not None and not ns.startswith(config_name): continue - if (metomi.rose.META_PROP_DUPLICATE in prop_map and - ns_hierarchy.get(ns, [])): + if ( + metomi.rose.META_PROP_DUPLICATE in prop_map + and ns_hierarchy.get(ns, []) + ): prop_map.setdefault("has_sub_data", True) diff --git a/metomi/rose/config_editor/data_helper.py b/metomi/rose/config_editor/data_helper.py index ef825882b..a1e39cc33 100644 --- a/metomi/rose/config_editor/data_helper.py +++ b/metomi/rose/config_editor/data_helper.py @@ -39,24 +39,29 @@ def get_config_has_unsaved_changes(self, config_name): variables = config_data.vars.get_all(skip_latent=True) save_vars = config_data.vars.get_all(save=True, skip_latent=True) sections = config_data.sections.get_all(skip_latent=True) - save_sections = config_data.sections.get_all(save=True, - skip_latent=True) + save_sections = config_data.sections.get_all( + save=True, skip_latent=True + ) now_set = set([v.to_hashable() for v in variables]) save_set = set([v.to_hashable() for v in save_vars]) now_sect_set = set([s.to_hashable() for s in sections]) save_sect_set = set([s.to_hashable() for s in save_sections]) - return (config_name not in self.data.saved_config_names or - now_set ^ save_set or - now_sect_set ^ save_sect_set) + return ( + config_name not in self.data.saved_config_names + or now_set ^ save_set + or now_sect_set ^ save_sect_set + ) def get_config_meta_flag(self, config_name, from_this_config_obj=None): """Return the metadata id flag.""" for section, option in [ - [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_META_TYPE], - [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_PROJECT]]: + [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_META_TYPE], + [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_PROJECT], + ]: if from_this_config_obj is not None: type_node = from_this_config_obj.get( - [section, option], no_ignore=True) + [section, option], no_ignore=True + ) if type_node is not None and type_node.value: return type_node.value continue @@ -84,17 +89,21 @@ def get_metadata_for_config_id(self, node_id, config_name): config_data = self.data.config[config_name] meta_config = config_data.meta if not node_id: - return {'id': node_id} - return metomi.rose.macro.get_metadata_for_config_id(node_id, meta_config) - - def get_variable_by_id(self, var_id, config_name, save=False, - latent=False): + return {"id": node_id} + return metomi.rose.macro.get_metadata_for_config_id( + node_id, meta_config + ) + + def get_variable_by_id( + self, var_id, config_name, save=False, latent=False + ): """Return the matching variable or None.""" sect, opt = self.util.get_section_option_from_id(var_id) return self.data.config[config_name].vars.get_var( - sect, opt, save, skip_latent=not latent) + sect, opt, save, skip_latent=not latent + ) -# ----------------- Data model helper functions ------------------------------ + # ----------------- Data model helper functions --------------------------- def get_data_for_namespace(self, ns, from_saved=False): """Return a list of vars and a list of latent vars for this ns.""" @@ -112,8 +121,8 @@ def get_data_for_namespace(self, ns, from_saved=False): for section in allowed_sections: variables.extend(var_map.get(section, [])) latents.extend(latent_var_map.get(section, [])) - ns_vars = [v for v in variables if v.metadata.get('full_ns') == ns] - ns_latents = [v for v in latents if v.metadata.get('full_ns') == ns] + ns_vars = [v for v in variables if v.metadata.get("full_ns") == ns] + ns_latents = [v for v in latents if v.metadata.get("full_ns") == ns] return ns_vars, ns_latents def get_macro_info_for_namespace(self, ns): @@ -121,16 +130,20 @@ def get_macro_info_for_namespace(self, ns): config_name = self.util.split_full_ns(self, ns)[0] config_data = self.data.config[config_name] ns_macros_text = self.data.namespace_meta_lookup.get(ns, {}).get( - metomi.rose.META_PROP_MACRO, "") + metomi.rose.META_PROP_MACRO, "" + ) if not ns_macros_text: return {} - ns_macros = metomi.rose.variable.array_split(ns_macros_text, - only_this_delim=",") + ns_macros = metomi.rose.variable.array_split( + ns_macros_text, only_this_delim="," + ) module_prefix = self.get_macro_module_prefix(config_name) for i, ns_macro in enumerate(ns_macros): ns_macros[i] = module_prefix + ns_macro ns_macro_info = {} - macro_tuples = metomi.rose.macro.get_macro_class_methods(config_data.macros) + macro_tuples = metomi.rose.macro.get_macro_class_methods( + config_data.macros + ) for module_name, class_name, method_name, docstring in macro_tuples: this_macro_name = ".".join([module_name, class_name]) this_macro_method_name = ".".join([this_macro_name, method_name]) @@ -145,8 +158,7 @@ def get_macro_info_for_namespace(self, ns): def get_section_data_for_namespace(self, ns): """Return real and latent lists of Section objects for this ns.""" - allowed_sections = ( - self.data.helper.get_sections_from_namespace(ns)) + allowed_sections = self.data.helper.get_sections_from_namespace(ns) config_name = self.util.split_full_ns(self.data, ns)[0] config_data = self.data.config[config_name] real_sections = [] @@ -214,16 +226,20 @@ def get_ns_variable(self, var_id, ns): def get_ns_url_for_variable(self, variable): """Return the parent (ns or section) URL property, if any.""" config_name = self.util.split_full_ns( - self.data, variable.metadata["full_ns"])[0] + self.data, variable.metadata["full_ns"] + )[0] ns_metadata = self.data.namespace_meta_lookup.get( - variable.metadata["full_ns"], {}) + variable.metadata["full_ns"], {} + ) ns_url = ns_metadata.get(metomi.rose.META_PROP_URL) if ns_url: return ns_url section = self.util.get_section_option_from_id( - variable.metadata["id"])[0] + variable.metadata["id"] + )[0] section_object = self.data.config[config_name].sections.get_sect( - section) + section + ) section_url = section_object.metadata.get(metomi.rose.META_PROP_URL) return section_url @@ -231,11 +247,11 @@ def get_sections_from_namespace(self, namespace): """Return all sections contributing to a namespace.""" # FIXME: What about files? ns_metadata = self.data.namespace_meta_lookup.get(namespace, {}) - sections = ns_metadata.get('sections', []) + sections = ns_metadata.get("sections", []) if sections: return [s for s in sections] base, subsp = self.util.split_full_ns(self.data, namespace) - ns_section = subsp.replace('/', ':') + ns_section = subsp.replace("/", ":") if ns_section in self.data.config[base].sections.now: sect_data = self.data.config[base].sections.now[ns_section] if sect_data.metadata["full_ns"] == namespace: @@ -254,12 +270,12 @@ def get_ns_is_default(self, namespace): empty = True for section in allowed_sections: for variable in config_data.vars.now.get(section, []): - if variable.metadata['full_ns'] == namespace: + if variable.metadata["full_ns"] == namespace: empty = False if metomi.rose.META_PROP_NS not in variable.metadata: return True for variable in config_data.vars.latent.get(section, []): - if variable.metadata['full_ns'] == namespace: + if variable.metadata["full_ns"] == namespace: empty = False if metomi.rose.META_PROP_NS not in variable.metadata: return True @@ -290,10 +306,12 @@ def get_missing_sections(self, config_name=None): if section not in real_sections: miss_sections.append(section) for section in self.data.config[config_name].vars.latent: - if (section not in real_sections and - section not in miss_sections): + if ( + section not in real_sections + and section not in miss_sections + ): miss_sections.append(section) - full_sections += [config_name + ':' + s for s in miss_sections] + full_sections += [config_name + ":" + s for s in miss_sections] sorter = metomi.rose.config.sort_settings full_sections.sort(key=cmp_to_key(sorter)) return full_sections @@ -301,47 +319,52 @@ def get_missing_sections(self, config_name=None): def get_default_section_namespace(self, section, config_name): """Return the default namespace for the section.""" if config_name not in self.data._config_section_namespace_map: - self.data._config_section_namespace_map.setdefault( - config_name, {}) - section_ns = ( - self.data._config_section_namespace_map[config_name].get( - section)) + self.data._config_section_namespace_map.setdefault(config_name, {}) + section_ns = self.data._config_section_namespace_map[config_name].get( + section + ) if section_ns is None: config_data = self.data.config[config_name] meta_config = config_data.meta node = meta_config.get( - [section, metomi.rose.META_PROP_NS], no_ignore=True) + [section, metomi.rose.META_PROP_NS], no_ignore=True + ) if node is not None: subspace = node.value else: match = REC_ELEMENT_SECTION.match(section) if match: node = meta_config.get( - [match.groups()[0], metomi.rose.META_PROP_NS]) + [match.groups()[0], metomi.rose.META_PROP_NS] + ) if node is None or node.is_ignored(): - subspace = section.replace('(', '/') - subspace = subspace.replace(')', '') - subspace = subspace.replace(':', '/') + subspace = section.replace("(", "/") + subspace = subspace.replace(")", "") + subspace = subspace.replace(":", "/") else: - subspace = node.value + '/' + str(match.groups()[1]) + subspace = node.value + "/" + str(match.groups()[1]) elif section.startswith(metomi.rose.SUB_CONFIG_FILE_DIR + ":"): - subspace = section.rstrip('/').replace('/', ':') - subspace = subspace.replace(':', '/', 1) + subspace = section.rstrip("/").replace("/", ":") + subspace = subspace.replace(":", "/", 1) else: - subspace = section.rstrip('/').replace(':', '/') - section_ns = config_name + '/' + subspace + subspace = section.rstrip("/").replace(":", "/") + section_ns = config_name + "/" + subspace if not subspace: section_ns = config_name self.data._config_section_namespace_map[config_name].update( - {section: section_ns}) + {section: section_ns} + ) return section_ns def get_format_sections(self, config_name): """Return all format-like sections in the current data.""" format_keys = [] for section in self.data.config[config_name].sections.now: - if (section not in format_keys and - ':' in section and not section.startswith('file:')): + if ( + section not in format_keys + and ":" in section + and not section.startswith("file:") + ): format_keys.append(section) format_keys.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) return format_keys @@ -350,7 +373,7 @@ def get_icon_path_for_config(self, config_name): """Return the path to the config identifier icon or None.""" icon_path = None for filename in self.data.config[config_name].meta_files: - if filename.endswith('/images/icon.png'): + if filename.endswith("/images/icon.png"): icon_path = filename break return icon_path @@ -380,8 +403,10 @@ def get_ignored_sections(self, namespace, get_enabled=False): if get_enabled: if not sect_data.ignored_reason: return_sections.append(section) - elif (metomi.rose.variable.IGNORED_BY_USER in - sect_data.ignored_reason): + elif ( + metomi.rose.variable.IGNORED_BY_USER + in sect_data.ignored_reason + ): return_sections.append(section) return_sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) return return_sections @@ -403,7 +428,7 @@ def get_latent_sections(self, namespace): def get_ns_ignored_status(self, namespace): """Return the ignored status for a namespace's data.""" - cache = self.data.namespace_cached_statuses['ignored'] + cache = self.data.namespace_cached_statuses["ignored"] if namespace in cache: return cache[namespace] config_name = self.util.split_full_ns(self.data, namespace)[0] @@ -451,7 +476,7 @@ def get_ns_ignored_status(self, namespace): else: object_statuses = variable_statuses status_counts = list(object_statuses.items()) - status_counts.sort(key = lambda x: x[1]) + status_counts.sort(key=lambda x: x[1]) if not status_counts: cache[namespace] = status return metomi.rose.config.ConfigNode.STATE_NORMAL @@ -465,7 +490,7 @@ def get_ns_ignored_status(self, namespace): def get_ns_latent_status(self, namespace): """Return whether a page has no associated content.""" - cache = self.data.namespace_cached_statuses['latent'] + cache = self.data.namespace_cached_statuses["latent"] if namespace in cache: return cache[namespace] config_name = self.util.split_full_ns(self.data, namespace)[0] @@ -474,8 +499,9 @@ def get_ns_latent_status(self, namespace): for section in sections: if section in config_data.sections.now: # It has a current section associated. - section_namespace = ( - config_data.sections.now[section].metadata["full_ns"]) + section_namespace = config_data.sections.now[section].metadata[ + "full_ns" + ] if section_namespace == namespace: # This is a default page for an existing section. cache[namespace] = False @@ -490,7 +516,7 @@ def get_ns_latent_status(self, namespace): def clear_namespace_cached_statuses(self, namespace): """Reset cached latent, ignored, modified statuses for namespace.""" - if namespace in self.data.namespace_cached_statuses['ignored']: - self.data.namespace_cached_statuses['ignored'].pop(namespace) - if namespace in self.data.namespace_cached_statuses['latent']: - self.data.namespace_cached_statuses['latent'].pop(namespace) + if namespace in self.data.namespace_cached_statuses["ignored"]: + self.data.namespace_cached_statuses["ignored"].pop(namespace) + if namespace in self.data.namespace_cached_statuses["latent"]: + self.data.namespace_cached_statuses["latent"].pop(namespace) diff --git a/metomi/rose/config_editor/keywidget.py b/metomi/rose/config_editor/keywidget.py index 2a5392024..4b475d831 100644 --- a/metomi/rose/config_editor/keywidget.py +++ b/metomi/rose/config_editor/keywidget.py @@ -22,7 +22,8 @@ from gi.repository import Pango import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config_editor @@ -32,26 +33,31 @@ class KeyWidget(Gtk.Box): - """This class generates a label or entry box for a variable name.""" FLAG_ICON_MAP = { metomi.rose.config_editor.FLAG_TYPE_DEFAULT: Gtk.STOCK_INFO, metomi.rose.config_editor.FLAG_TYPE_ERROR: Gtk.STOCK_DIALOG_WARNING, - metomi.rose.config_editor.FLAG_TYPE_FIXED: Gtk.STOCK_DIALOG_AUTHENTICATION, + metomi.rose.config_editor.FLAG_TYPE_FIXED: ( + Gtk.STOCK_DIALOG_AUTHENTICATION + ), metomi.rose.config_editor.FLAG_TYPE_OPT_CONF: Gtk.STOCK_INDEX, metomi.rose.config_editor.FLAG_TYPE_OPTIONAL: Gtk.STOCK_ABOUT, metomi.rose.config_editor.FLAG_TYPE_NO_META: "dialog-question", } MODIFIED_COLOUR = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_CHANGED) + metomi.rose.config_editor.COLOUR_VARIABLE_CHANGED + ) LABEL_X_OFFSET = 0.01 - def __init__(self, variable, var_ops, launch_help_func, update_func, - show_modes): - super(KeyWidget, self).__init__(homogeneous=False, spacing=0, orientation=Gtk.Orientation.VERTICAL) + def __init__( + self, variable, var_ops, launch_help_func, update_func, show_modes + ): + super(KeyWidget, self).__init__( + homogeneous=False, spacing=0, orientation=Gtk.Orientation.VERTICAL + ) self.my_variable = variable self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox.show() @@ -65,45 +71,56 @@ def __init__(self, variable, var_ops, launch_help_func, update_func, self._last_var_comments = None self.ignored_label = Gtk.Label() self.ignored_label.show() - self.hbox.pack_start(self.ignored_label, expand=False, fill=False, padding=0) + self.hbox.pack_start( + self.ignored_label, expand=False, fill=False, padding=0 + ) self.set_ignored() - if self.my_variable.name != '': + if self.my_variable.name != "": self.entry = Gtk.Label() self.entry.set_alignment( - self.LABEL_X_OFFSET, - self.entry.get_alignment()[1]) + self.LABEL_X_OFFSET, self.entry.get_alignment()[1] + ) self.entry.set_text(self.my_variable.name) else: self.entry = Gtk.Entry() - self.entry.modify_text(Gtk.StateType.NORMAL, - self.MODIFIED_COLOUR) - self.entry.connect("focus-out-event", - lambda w, e: self._setter(w, variable)) + self.entry.modify_text(Gtk.StateType.NORMAL, self.MODIFIED_COLOUR) + self.entry.connect( + "focus-out-event", lambda w, e: self._setter(w, variable) + ) event_box = Gtk.EventBox() event_box.add(self.entry) - event_box.connect('enter-notify-event', - lambda b, w: self._handle_enter(b)) - event_box.connect('leave-notify-event', - lambda b, w: self._handle_leave(b)) - self.hbox.pack_start(event_box, expand=True, fill=True, - padding=0) + event_box.connect( + "enter-notify-event", lambda b, w: self._handle_enter(b) + ) + event_box.connect( + "leave-notify-event", lambda b, w: self._handle_leave(b) + ) + self.hbox.pack_start(event_box, expand=True, fill=True, padding=0) self.comments_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.hbox.pack_start(self.comments_box, expand=False, fill=False, padding=0) + self.hbox.pack_start( + self.comments_box, expand=False, fill=False, padding=0 + ) self.grab_focus = self.entry.grab_focus self.set_sensitive(True) self.set_sensitive = self._set_sensitive - event_box.connect('button-press-event', self.handle_launch_help) + event_box.connect("button-press-event", self.handle_launch_help) self.update_comment_display() self.entry.show() for key, value in list(self.show_modes.items()): - if key not in [metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + if key not in [ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE, + ]: self.set_show_mode(key, value) - if (metomi.rose.META_PROP_VALUES in self.meta and - len(self.meta[metomi.rose.META_PROP_VALUES]) == 1): - self.add_flag(metomi.rose.config_editor.FLAG_TYPE_FIXED, - metomi.rose.config_editor.VAR_FLAG_TIP_FIXED) + if ( + metomi.rose.META_PROP_VALUES in self.meta + and len(self.meta[metomi.rose.META_PROP_VALUES]) == 1 + ): + self.add_flag( + metomi.rose.config_editor.FLAG_TYPE_FIXED, + metomi.rose.config_editor.VAR_FLAG_TIP_FIXED, + ) event_box.show() self.show() @@ -121,27 +138,32 @@ def add_flag(self, flag_type, tooltip_text=None): event_box.add(image) event_box.show() event_box.connect("button-press-event", self._toggle_flag_label) - self.hbox.pack_end(event_box, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + self.hbox.pack_end( + event_box, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) def get_centre_height(self): """Return the vertical displacement of the centre of this widget.""" - return (self.entry.get_preferred_size().natural_size.height / 2) + return self.entry.get_preferred_size().natural_size.height / 2 def handle_launch_help(self, widget, event): """Handle launching help.""" if event.type == Gdk.EventType.BUTTON_PRESS and event.button != 3: - url_mode = (metomi.rose.META_PROP_HELP not in self.meta) + url_mode = metomi.rose.META_PROP_HELP not in self.meta self.launch_help(url_mode=url_mode) def launch_edit_comments(self, *args): """Launch an edit comments dialog.""" text = "\n".join(self.my_variable.comments) title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( - self.my_variable.metadata['id']) - metomi.rose.gtk.dialog.run_edit_dialog(text, - finish_hook=self._edit_finish_hook, - title=title) + self.my_variable.metadata["id"] + ) + metomi.rose.gtk.dialog.run_edit_dialog( + text, finish_hook=self._edit_finish_hook, title=title + ) def refresh(self, variable=None): """Reload the contents - however, no need for this at present.""" @@ -150,8 +172,10 @@ def refresh(self, variable=None): def remove_flag(self, flag_type): """Remove the flag from the widget.""" for widget in self.get_children(): - if (isinstance(widget, Gtk.EventBox) and - getattr(widget, "_flag_type", None) == flag_type): + if ( + isinstance(widget, Gtk.EventBox) + and getattr(widget, "_flag_type", None) == flag_type + ): self.remove(widget) if flag_type in self.var_flags: self.var_flags.remove(flag_type) @@ -160,7 +184,8 @@ def remove_flag(self, flag_type): def set_ignored(self): """Update the ignored display.""" self.ignored_label.set_markup( - metomi.rose.variable.get_ignored_markup(self.my_variable)) + metomi.rose.variable.get_ignored_markup(self.my_variable) + ) hover_string = "" if not self.my_variable.ignored_reason: self.ignored_label.set_tooltip_text(None) @@ -175,17 +200,21 @@ def set_modified(self, is_modified): att_list = self.entry.get_attributes() if att_list is None: att_list = Pango.AttrList() - att_list.insert(Pango.attr_foreground_new( - self.MODIFIED_COLOUR.red, - self.MODIFIED_COLOUR.green, - self.MODIFIED_COLOUR.blue)) + att_list.insert( + Pango.attr_foreground_new( + self.MODIFIED_COLOUR.red, + self.MODIFIED_COLOUR.green, + self.MODIFIED_COLOUR.blue, + ) + ) self.entry.set_attributes(att_list) else: if isinstance(self.entry, Gtk.Label): att_list = self.entry.get_attributes() if att_list is not None: att_list = att_list.filter( - lambda a: a.klass.type != Pango.AttrType.FOREGROUND) + lambda a: a.klass.type != Pango.AttrType.FOREGROUND + ) if att_list is None: att_list = Pango.AttrList() @@ -193,55 +222,82 @@ def set_modified(self, is_modified): def set_show_mode(self, show_mode, should_show_mode): """Set the display of a mode on or off.""" - if show_mode in [metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]: + if show_mode in [ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP, + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE, + ]: return self._set_show_custom_meta_text(show_mode, should_show_mode) if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_TITLE: return self._set_show_title(not should_show_mode) if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION: - return self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, - not should_show_mode) + return self._set_show_meta_text_mode( + metomi.rose.META_PROP_DESCRIPTION, not should_show_mode + ) if show_mode == metomi.rose.config_editor.SHOW_MODE_NO_HELP: - return self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, - not should_show_mode) + return self._set_show_meta_text_mode( + metomi.rose.META_PROP_HELP, not should_show_mode + ) if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: - if (should_show_mode and - self.meta.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE): + if ( + should_show_mode + and self.meta.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + ): return self.add_flag( metomi.rose.config_editor.FLAG_TYPE_OPTIONAL, - metomi.rose.config_editor.VAR_FLAG_TIP_OPTIONAL) - return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_OPTIONAL) + metomi.rose.config_editor.VAR_FLAG_TIP_OPTIONAL, + ) + return self.remove_flag( + metomi.rose.config_editor.FLAG_TYPE_OPTIONAL + ) if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META: if should_show_mode and len(self.meta) <= 2: - return self.add_flag(metomi.rose.config_editor.FLAG_TYPE_NO_META, - metomi.rose.config_editor.VAR_FLAG_TIP_NO_META) - return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_NO_META) + return self.add_flag( + metomi.rose.config_editor.FLAG_TYPE_NO_META, + metomi.rose.config_editor.VAR_FLAG_TIP_NO_META, + ) + return self.remove_flag( + metomi.rose.config_editor.FLAG_TYPE_NO_META + ) if show_mode == metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: - if (should_show_mode and metomi.rose.config_editor.FLAG_TYPE_OPT_CONF in - self.my_variable.flags): + if ( + should_show_mode + and metomi.rose.config_editor.FLAG_TYPE_OPT_CONF + in self.my_variable.flags + ): opts_info = self.my_variable.flags[ - metomi.rose.config_editor.FLAG_TYPE_OPT_CONF] + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF + ] info_text = "" - info_format = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_INFO + info_format = ( + metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF_INFO + ) for opt, diff in sorted(opts_info.items()): info_text += info_format.format(opt, diff) info_text = info_text.rstrip() if info_text: - text = metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF.format( - info_text) + text = ( + metomi.rose.config_editor.VAR_FLAG_TIP_OPT_CONF.format( + info_text + ) + ) return self.add_flag( - metomi.rose.config_editor.FLAG_TYPE_OPT_CONF, text) - return self.remove_flag(metomi.rose.config_editor.FLAG_TYPE_OPT_CONF) + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF, text + ) + return self.remove_flag( + metomi.rose.config_editor.FLAG_TYPE_OPT_CONF + ) def update_comment_display(self): """Update the display of variable comments.""" if self.my_variable.comments == self._last_var_comments: return self._last_var_comments = self.my_variable.comments - if (self.my_variable.comments or - metomi.rose.config_editor.SHOULD_SHOW_ALL_COMMENTS): + if ( + self.my_variable.comments + or metomi.rose.config_editor.SHOULD_SHOW_ALL_COMMENTS + ): tip_fmt = metomi.rose.config_editor.VAR_COMMENT_TIP comments = [tip_fmt.format(c) for c in self.my_variable.comments] tooltip_text = "\n".join(comments) @@ -255,15 +311,25 @@ def update_comment_display(self): edit_label.show() edit_eb.add(edit_label) edit_eb.set_tooltip_text(tooltip_text) - edit_eb.connect("button-press-event", - self._handle_comment_click) - edit_eb.connect("enter-notify-event", - self._handle_comment_enter_leave, True) - edit_eb.connect("leave-notify-event", - self._handle_comment_enter_leave, False) + edit_eb.connect( + "button-press-event", self._handle_comment_click + ) + edit_eb.connect( + "enter-notify-event", + self._handle_comment_enter_leave, + True, + ) + edit_eb.connect( + "leave-notify-event", + self._handle_comment_enter_leave, + False, + ) self.comments_box.pack_start( - edit_eb, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + edit_eb, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) self.comments_box.show() else: self.comments_box.hide() @@ -271,18 +337,30 @@ def update_comment_display(self): def _get_metadata_formatting(self, mode): """Apply the correct formatting for a metadata property.""" mode_format = "{" + mode + "}" - if (mode == metomi.rose.META_PROP_DESCRIPTION and - self.show_modes[ - metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION]): + if ( + mode == metomi.rose.META_PROP_DESCRIPTION + and self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION + ] + ): mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_DESCRIPTION - if (mode == metomi.rose.META_PROP_HELP and - self.show_modes[metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP]): + if ( + mode == metomi.rose.META_PROP_HELP + and self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP + ] + ): mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_HELP - if (mode == metomi.rose.META_PROP_TITLE and - self.show_modes[metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + if ( + mode == metomi.rose.META_PROP_TITLE + and self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE + ] + ): mode_format = metomi.rose.config_editor.CUSTOM_FORMAT_TITLE - mode_string = metomi.rose.variable.expand_format_string(mode_format, - self.my_variable) + mode_string = metomi.rose.variable.expand_format_string( + mode_format, self.my_variable + ) if mode_string is None: return self.my_variable.metadata[mode] return mode_string @@ -291,41 +369,59 @@ def _set_show_custom_meta_text(self, mode, should_show_mode): """Set the display of a custom format for a metadata property.""" if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE: return self._set_show_title( - not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]) + not self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_NO_TITLE + ] + ) if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: is_shown = not self.show_modes[ - metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION] + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION + ] if is_shown: - self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, - False) - self._set_show_meta_text_mode(metomi.rose.META_PROP_DESCRIPTION, - True) + self._set_show_meta_text_mode( + metomi.rose.META_PROP_DESCRIPTION, False + ) + self._set_show_meta_text_mode( + metomi.rose.META_PROP_DESCRIPTION, True + ) if mode == metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP: is_shown = not self.show_modes[ - metomi.rose.config_editor.SHOW_MODE_NO_HELP] + metomi.rose.config_editor.SHOW_MODE_NO_HELP + ] if is_shown: - self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, False) + self._set_show_meta_text_mode( + metomi.rose.META_PROP_HELP, False + ) self._set_show_meta_text_mode(metomi.rose.META_PROP_HELP, True) def _set_show_meta_text_mode(self, mode, should_show_mode): """Set the display of description or help below the title/name.""" if should_show_mode: search_func = lambda i: self.var_ops.search_for_var( - self.meta["full_ns"], i) + self.meta["full_ns"], i + ) if mode not in self.meta: return mode_text = self._get_metadata_formatting(mode) mode_text = metomi.rose.gtk.util.safe_str(mode_text) - mode_text = metomi.rose.config_editor.VAR_FLAG_MARKUP.format(mode_text) - label = metomi.rose.gtk.util.get_hyperlink_label(mode_text, search_func) + mode_text = metomi.rose.config_editor.VAR_FLAG_MARKUP.format( + mode_text + ) + label = metomi.rose.gtk.util.get_hyperlink_label( + mode_text, search_func + ) label.show() hbox = Gtk.Box() hbox.show() hbox.pack_start(label, expand=False, fill=False, padding=0) hbox.set_sensitive(self.entry.get_property("sensitive")) hbox._show_mode = mode - self.pack_start(hbox, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + self.pack_start( + hbox, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) show_mode_widget_indices = [] for i, widget in enumerate(self.get_children()): if hasattr(widget, "_show_mode"): @@ -339,9 +435,11 @@ def _set_show_meta_text_mode(self, mode, should_show_mode): break else: for widget in self.get_children(): - if (isinstance(widget, Gtk.Box) and - hasattr(widget, "_show_mode") and - widget._show_mode == mode): + if ( + isinstance(widget, Gtk.Box) + and hasattr(widget, "_show_mode") + and widget._show_mode == mode + ): self.remove(widget) def _set_show_title(self, should_show_title): @@ -351,7 +449,8 @@ def _set_show_title(self, should_show_title): if should_show_title: if metomi.rose.META_PROP_TITLE in self.meta: title_string = self._get_metadata_formatting( - metomi.rose.META_PROP_TITLE) + metomi.rose.META_PROP_TITLE + ) if title_string != self.entry.get_text(): return self.entry.set_text(title_string) if self.entry.get_text() != self.my_variable.name: @@ -363,8 +462,10 @@ def _toggle_flag_label(self, event_box, event, text=None): if text is None: text = event_box.get_child().get_tooltip_text() for widget in self.get_children(): - if (hasattr(widget, "_flag_type") and - widget._flag_type == flag_type): + if ( + hasattr(widget, "_flag_type") + and widget._flag_type == flag_type + ): return self.remove(widget) label = Gtk.Label() markup = metomi.rose.gtk.util.safe_str(text) @@ -395,19 +496,26 @@ def _handle_enter(self, event_box): tooltip_text = "" if metomi.rose.META_PROP_DESCRIPTION in self.meta: tooltip_text = self._get_metadata_formatting( - metomi.rose.META_PROP_DESCRIPTION) + metomi.rose.META_PROP_DESCRIPTION + ) if metomi.rose.META_PROP_TITLE in self.meta: if self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]: # Titles are hidden, so show them in the hover-over. - tooltip_text += ("\n (" + - metomi.rose.META_PROP_TITLE.capitalize() + - ": '" + - self.meta[metomi.rose.META_PROP_TITLE] + "')") - elif (self.my_variable.name not in label_text or - not self.show_modes[ - metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE]): + tooltip_text += ( + "\n (" + + metomi.rose.META_PROP_TITLE.capitalize() + + ": '" + + self.meta[metomi.rose.META_PROP_TITLE] + + "')" + ) + elif ( + self.my_variable.name not in label_text + or not self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE + ] + ): # No custom title, or a custom title without the name. - tooltip_text += ("\n (" + self.my_variable.name + ")") + tooltip_text += "\n (" + self.my_variable.name + ")" if self.my_variable.comments: tip_fmt = metomi.rose.config_editor.VAR_COMMENT_TIP if tooltip_text: @@ -415,21 +523,27 @@ def _handle_enter(self, event_box): comments = [tip_fmt.format(c) for c in self.my_variable.comments] tooltip_text += "\n".join(comments) changes = self.var_ops.get_var_changes(self.my_variable) - if changes != '' and tooltip_text != '': - tooltip_text += '\n\n' + changes + if changes != "" and tooltip_text != "": + tooltip_text += "\n\n" + changes else: tooltip_text += changes tooltip_text.strip() - if tooltip_text == '': + if tooltip_text == "": tooltip_text = None event_box.set_tooltip_text(tooltip_text) - if (metomi.rose.META_PROP_URL not in self.meta and - 'http://' in self.my_variable.value): - new_url = re.search('(http://[^ ]+)', - self.my_variable.value).group() + if ( + metomi.rose.META_PROP_URL not in self.meta + and "http://" in self.my_variable.value + ): + new_url = re.search( + "(http://[^ ]+)", self.my_variable.value + ).group() # This is not very nice. self.meta.update({metomi.rose.META_PROP_URL: new_url}) - if metomi.rose.META_PROP_HELP in self.meta or metomi.rose.META_PROP_URL in self.meta: + if ( + metomi.rose.META_PROP_HELP in self.meta + or metomi.rose.META_PROP_URL in self.meta + ): if isinstance(self.entry, Gtk.Label): self._set_underline(self.entry, underline=True) return False @@ -442,8 +556,9 @@ def _set_underline(self, label, underline=False): if underline: att_list.insert(Pango.attr_underline_new(Pango.Underline.SINGLE)) else: - att_list = att_list.filter(lambda a: - a.klass.type != Pango.AttrType.UNDERLINE) + att_list = att_list.filter( + lambda a: a.klass.type != Pango.AttrType.UNDERLINE + ) if att_list is None: att_list = Pango.AttrList() label.set_attributes(att_list) @@ -464,19 +579,24 @@ def _setter(self, widget, variable): """Re-set the name of the variable in the dictionary object.""" new_name = widget.get_text() if variable.name != new_name: - section = variable.metadata['id'].split(metomi.rose.CONFIG_DELIMITER)[0] + section = variable.metadata["id"].split( + metomi.rose.CONFIG_DELIMITER + )[0] if section.startswith("namelist:"): if new_name.lower() != new_name: text = metomi.rose.config_editor.DIALOG_BODY_NL_CASE_CHANGE text = text.format(new_name.lower()) - title = metomi.rose.config_editor.DIALOG_TITLE_NL_CASE_WARNING + title = ( + metomi.rose.config_editor.DIALOG_TITLE_NL_CASE_WARNING + ) new_name = metomi.rose.gtk.dialog.run_choices_dialog( - text, [new_name.lower(), new_name], - title) + text, [new_name.lower(), new_name], title + ) if new_name is None: return None self.var_ops.remove_var(variable) variable.name = new_name - variable.metadata['id'] = (section + metomi.rose.CONFIG_DELIMITER + - variable.name) + variable.metadata["id"] = ( + section + metomi.rose.CONFIG_DELIMITER + variable.name + ) self.var_ops.add_var(variable) diff --git a/metomi/rose/config_editor/main.py b/metomi/rose/config_editor/main.py index 4d07f5707..4715ff6c0 100644 --- a/metomi/rose/config_editor/main.py +++ b/metomi/rose/config_editor/main.py @@ -37,24 +37,19 @@ from functools import cmp_to_key # Ignore add menu related warnings for now, but remove this later. -warnings.filterwarnings('ignore', - 'instance of invalid non-instantiatable type', - Warning) -warnings.filterwarnings('ignore', - 'g_signal_handlers_disconnect_matched', - Warning) -warnings.filterwarnings('ignore', - 'use set_markup', - Warning) -warnings.filterwarnings('ignore', - 'Unable to show', - Warning) -warnings.filterwarnings('ignore', - 'gdk', - Warning) +warnings.filterwarnings( + "ignore", "instance of invalid non-instantiatable type", Warning +) +warnings.filterwarnings( + "ignore", "g_signal_handlers_disconnect_matched", Warning +) +warnings.filterwarnings("ignore", "use set_markup", Warning) +warnings.filterwarnings("ignore", "Unable to show", Warning) +warnings.filterwarnings("ignore", "gdk", Warning) import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config @@ -84,7 +79,6 @@ class MainController(object): - """The main controller class. Call with a configuration directory and/or a dict of @@ -109,12 +103,21 @@ class MainController(object): """ - RE_ARRAY_ELEMENT = re.compile(r'\([\d:, ]+\)$') - - def __init__(self, config_directory=None, config_objs=None, - config_obj_types=None, pluggable=False, load_updater=None, - load_all_apps=False, load_no_apps=False, metadata_off=False, - opt_meta_paths=None, no_warn=None): + RE_ARRAY_ELEMENT = re.compile(r"\([\d:, ]+\)$") + + def __init__( + self, + config_directory=None, + config_objs=None, + config_obj_types=None, + pluggable=False, + load_updater=None, + load_all_apps=False, + load_no_apps=False, + metadata_off=False, + opt_meta_paths=None, + no_warn=None, + ): if config_objs is None: config_objs = {} if pluggable: @@ -126,7 +129,7 @@ def __init__(self, config_directory=None, config_objs=None, self.orphan_pages = [] self.undo_stack = [] # Nothing to undo yet self.redo_stack = [] # Nothing to redo yet - self.find_hist = {'regex': '', 'ids': []} + self.find_hist = {"regex": "", "ids": []} self.util = metomi.rose.config_editor.util.Lookup() self.metadata_off = metadata_off if opt_meta_paths is None: @@ -134,49 +137,65 @@ def __init__(self, config_directory=None, config_objs=None, # Set page variable 'verbosity' defaults. self.page_var_show_modes = { - metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: - metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_DESCRIPTION, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP: - metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_HELP, - metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE: - metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_TITLE, - metomi.rose.config_editor.SHOW_MODE_FIXED: - metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS, - metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: - metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS, - metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: - metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS, - metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META: - metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS, - metomi.rose.config_editor.SHOW_MODE_IGNORED: - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS, - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: - metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS, - metomi.rose.config_editor.SHOW_MODE_LATENT: - metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS, - metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION: - metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION, - metomi.rose.config_editor.SHOW_MODE_NO_HELP: - metomi.rose.config_editor.SHOULD_SHOW_NO_HELP, - metomi.rose.config_editor.SHOW_MODE_NO_TITLE: - metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE + metomi.rose.config_editor.SHOW_MODE_CUSTOM_DESCRIPTION: ( + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_DESCRIPTION + ), + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP: ( + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_HELP + ), + metomi.rose.config_editor.SHOW_MODE_CUSTOM_TITLE: ( + metomi.rose.config_editor.SHOULD_SHOW_CUSTOM_TITLE + ), + metomi.rose.config_editor.SHOW_MODE_FIXED: ( + metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS + ), + metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL: ( + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS + ), + metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF: ( + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS + ), + metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META: ( + metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS + ), + metomi.rose.config_editor.SHOW_MODE_IGNORED: ( + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS + ), + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: ( + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS + ), + metomi.rose.config_editor.SHOW_MODE_LATENT: ( + metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS + ), + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION: ( + metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION + ), + metomi.rose.config_editor.SHOW_MODE_NO_HELP: ( + metomi.rose.config_editor.SHOULD_SHOW_NO_HELP + ), + metomi.rose.config_editor.SHOW_MODE_NO_TITLE: ( + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE + ), } # Set page tree 'verbosity' defaults. self.page_ns_show_modes = { - metomi.rose.config_editor.SHOW_MODE_IGNORED: - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES, - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: - metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES, - metomi.rose.config_editor.SHOW_MODE_LATENT: - metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES, - metomi.rose.config_editor.SHOW_MODE_NO_TITLE: - metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE + metomi.rose.config_editor.SHOW_MODE_IGNORED: ( + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES + ), + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED: ( + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES + ), + metomi.rose.config_editor.SHOW_MODE_LATENT: ( + metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES + ), + metomi.rose.config_editor.SHOW_MODE_NO_TITLE: ( + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE + ), } self.reporter = metomi.rose.config_editor.status.StatusReporter( - load_updater, - self.update_status_text + load_updater, self.update_status_text ) # Load the top configuration directory @@ -186,56 +205,69 @@ def __init__(self, config_directory=None, config_objs=None, self.page_ns_show_modes, self.reload_namespace_tree, opt_meta_paths=opt_meta_paths, - no_warn=no_warn + no_warn=no_warn, ) self.nav_controller = ( metomi.rose.config_editor.nav_controller.NavTreeManager( - self.data, - self.util, - self.reporter, - self.tree_trigger_update - )) + self.data, self.util, self.reporter, self.tree_trigger_update + ) + ) self.mainwindow = metomi.rose.config_editor.window.MainWindow() - self.section_ops = metomi.rose.config_editor.ops.section.SectionOperations( - self.data, self.util, self.reporter, - self.undo_stack, self.redo_stack, - self.check_cannot_enable_setting, - self.update_namespace, - self.update_namespace_sub_data, - self.update_ns_info, - update_tree_func=self.reload_namespace_tree, - view_page_func=self.view_page, - kill_page_func=self.kill_page + self.section_ops = ( + metomi.rose.config_editor.ops.section.SectionOperations( + self.data, + self.util, + self.reporter, + self.undo_stack, + self.redo_stack, + self.check_cannot_enable_setting, + self.update_namespace, + self.update_namespace_sub_data, + self.update_ns_info, + update_tree_func=self.reload_namespace_tree, + view_page_func=self.view_page, + kill_page_func=self.kill_page, + ) ) self.variable_ops = ( metomi.rose.config_editor.ops.variable.VariableOperations( - self.data, self.util, self.reporter, - self.undo_stack, self.redo_stack, + self.data, + self.util, + self.reporter, + self.undo_stack, + self.redo_stack, self.section_ops.add_section, self.check_cannot_enable_setting, self.update_namespace, - search_id_func=self.perform_find_by_id - )) + search_id_func=self.perform_find_by_id, + ) + ) self.group_ops = metomi.rose.config_editor.ops.group.GroupOperations( - self.data, self.util, self.reporter, - self.undo_stack, self.redo_stack, + self.data, + self.util, + self.reporter, + self.undo_stack, + self.redo_stack, self.section_ops, self.variable_ops, self.view_page, self.update_ns_sub_data, - self.reload_namespace_tree + self.reload_namespace_tree, ) # Add in the main menu bar and tool bar handler. self.main_handle = metomi.rose.config_editor.menu.MainMenuHandler( - self.data, self.util, self.reporter, + self.data, + self.util, + self.reporter, self.mainwindow, - self.undo_stack, self.redo_stack, + self.undo_stack, + self.redo_stack, self.perform_undo, self.update_config, self.apply_macro_transform, @@ -243,41 +275,53 @@ def __init__(self, config_directory=None, config_objs=None, self.group_ops, self.section_ops, self.variable_ops, - self.perform_find_by_ns_id + self.perform_find_by_ns_id, ) # Add in the navigation panel menu handler. - self.nav_handle = metomi.rose.config_editor.nav_panel_menu.NavPanelHandler( - self.data, self.util, self.reporter, - self.mainwindow, - self.undo_stack, self.redo_stack, - self._add_config, - self.group_ops, - self.section_ops, - self.variable_ops, - self.kill_page, - self.reload_namespace_tree, - self.main_handle.transform_default, - self.main_handle.launch_graph + self.nav_handle = ( + metomi.rose.config_editor.nav_panel_menu.NavPanelHandler( + self.data, + self.util, + self.reporter, + self.mainwindow, + self.undo_stack, + self.redo_stack, + self._add_config, + self.group_ops, + self.section_ops, + self.variable_ops, + self.kill_page, + self.reload_namespace_tree, + self.main_handle.transform_default, + self.main_handle.launch_graph, + ) ) self.updater = metomi.rose.config_editor.updater.Updater( - self.data, self.util, self.reporter, - self.mainwindow, self.main_handle, + self.data, + self.util, + self.reporter, + self.mainwindow, + self.main_handle, self.nav_controller, self._get_pagelist, self.update_bar_widgets, self._refresh_metadata_if_on, - self.is_pluggable + self.is_pluggable, ) - self.data.load(config_directory, config_objs, - config_obj_type_dict=config_obj_types, - load_all_apps=load_all_apps, - load_no_apps=load_no_apps) + self.data.load( + config_directory, + config_objs, + config_obj_type_dict=config_obj_types, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps, + ) self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_STATUSES.format( - self.data.top_level_name) + self.data.top_level_name + ) ) if not self.is_pluggable: self.generate_toolbar() @@ -289,63 +333,86 @@ def __init__(self, config_directory=None, config_objs=None, self.updater.nav_panel = getattr(self, "nav_panel", None) # Create the main panel with the menu, toolbar, tree panel, notebook. if not self.is_pluggable: - self.mainwindow.load(name=self.data.top_level_name, - menu=self.top_menu, - accelerators=self.menubar.accelerators, - toolbar=self.toolbar, - nav_panel=self.nav_panel, - status_bar=self.status_bar, - notebook=self.notebook, - page_change_func=self.handle_page_change, - save_func=self.save_to_file) - self.mainwindow.window.connect('destroy', self.main_handle.destroy) - self.mainwindow.window.connect('delete-event', - self.main_handle.destroy) - self.mainwindow.window.connect_after('grab_focus', - self.handle_page_change) - self.mainwindow.window.connect_after('focus-in-event', - self.handle_page_change) + self.mainwindow.load( + name=self.data.top_level_name, + menu=self.top_menu, + accelerators=self.menubar.accelerators, + toolbar=self.toolbar, + nav_panel=self.nav_panel, + status_bar=self.status_bar, + notebook=self.notebook, + page_change_func=self.handle_page_change, + save_func=self.save_to_file, + ) + self.mainwindow.window.connect("destroy", self.main_handle.destroy) + self.mainwindow.window.connect( + "delete-event", self.main_handle.destroy + ) + self.mainwindow.window.connect_after( + "grab_focus", self.handle_page_change + ) + self.mainwindow.window.connect_after( + "focus-in-event", self.handle_page_change + ) self.updater.update_all(is_loading=True) self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_ERRORS.format( - self.data.top_level_name, - self.updater.load_errors - )) + self.data.top_level_name, self.updater.load_errors + ) + ) self.updater.perform_startup_check() self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_DONE.format( self.data.top_level_name - )) - if (self.data.top_level_directory is None and not self.data.config): + ) + ) + if self.data.top_level_directory is None and not self.data.config: self.load_from_file() self.update_bar_widgets() self.performing_undo = False -# ----------------- Setting up main component functions ---------------------- + # ----------------- Setting up main component functions ------------------- def generate_toolbar(self): """Link in the toolbar functionality.""" self.toolbar = metomi.rose.gtk.util.ToolBar( widgets=[ - (metomi.rose.config_editor.TOOLBAR_OPEN, 'Gtk.STOCK_OPEN'), - (metomi.rose.config_editor.TOOLBAR_SAVE, 'Gtk.STOCK_SAVE'), - (metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, - 'Gtk.STOCK_SPELL_CHECK'), - (metomi.rose.config_editor.TOOLBAR_LOAD_APPS, 'Gtk.STOCK_CDROM'), - (metomi.rose.config_editor.TOOLBAR_BROWSE, 'Gtk.STOCK_DIRECTORY'), - (metomi.rose.config_editor.TOOLBAR_UNDO, 'Gtk.STOCK_UNDO'), - (metomi.rose.config_editor.TOOLBAR_REDO, 'Gtk.STOCK_REDO'), - (metomi.rose.config_editor.TOOLBAR_ADD, 'Gtk.STOCK_ADD'), - (metomi.rose.config_editor.TOOLBAR_REVERT, - 'Gtk.STOCK_REVERT_TO_SAVED'), - (metomi.rose.config_editor.TOOLBAR_FIND, 'Gtk.SearchEntry'), - (metomi.rose.config_editor.TOOLBAR_FIND_NEXT, 'Gtk.STOCK_FIND'), - (metomi.rose.config_editor.TOOLBAR_VALIDATE, - "dialog-question"), - (metomi.rose.config_editor.TOOLBAR_TRANSFORM, - 'Gtk.STOCK_CONVERT'), + (metomi.rose.config_editor.TOOLBAR_OPEN, "Gtk.STOCK_OPEN"), + (metomi.rose.config_editor.TOOLBAR_SAVE, "Gtk.STOCK_SAVE"), + ( + metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + "Gtk.STOCK_SPELL_CHECK", + ), + ( + metomi.rose.config_editor.TOOLBAR_LOAD_APPS, + "Gtk.STOCK_CDROM", + ), + ( + metomi.rose.config_editor.TOOLBAR_BROWSE, + "Gtk.STOCK_DIRECTORY", + ), + (metomi.rose.config_editor.TOOLBAR_UNDO, "Gtk.STOCK_UNDO"), + (metomi.rose.config_editor.TOOLBAR_REDO, "Gtk.STOCK_REDO"), + (metomi.rose.config_editor.TOOLBAR_ADD, "Gtk.STOCK_ADD"), + ( + metomi.rose.config_editor.TOOLBAR_REVERT, + "Gtk.STOCK_REVERT_TO_SAVED", + ), + (metomi.rose.config_editor.TOOLBAR_FIND, "Gtk.SearchEntry"), + ( + metomi.rose.config_editor.TOOLBAR_FIND_NEXT, + "Gtk.STOCK_FIND", + ), + ( + metomi.rose.config_editor.TOOLBAR_VALIDATE, + "dialog-question", + ), + ( + metomi.rose.config_editor.TOOLBAR_TRANSFORM, + "Gtk.STOCK_CONVERT", + ), ], sep_on_name=[ metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, @@ -353,267 +420,378 @@ def generate_toolbar(self): metomi.rose.config_editor.TOOLBAR_REDO, metomi.rose.config_editor.TOOLBAR_REVERT, metomi.rose.config_editor.TOOLBAR_FIND_NEXT, - metomi.rose.config_editor.TOOLBAR_TRANSFORM - ] + metomi.rose.config_editor.TOOLBAR_TRANSFORM, + ], ) assign = self.toolbar.set_widget_function assign(metomi.rose.config_editor.TOOLBAR_OPEN, self.load_from_file) assign(metomi.rose.config_editor.TOOLBAR_SAVE, self.save_to_file) - assign(metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, self.save_to_file, - [None, True]) - assign(metomi.rose.config_editor.TOOLBAR_LOAD_APPS, self.handle_load_all) - assign(metomi.rose.config_editor.TOOLBAR_BROWSE, - self.main_handle.launch_browser) + assign( + metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, + self.save_to_file, + [None, True], + ) + assign( + metomi.rose.config_editor.TOOLBAR_LOAD_APPS, self.handle_load_all + ) + assign( + metomi.rose.config_editor.TOOLBAR_BROWSE, + self.main_handle.launch_browser, + ) assign(metomi.rose.config_editor.TOOLBAR_UNDO, self.perform_undo) - assign(metomi.rose.config_editor.TOOLBAR_REDO, self.perform_undo, [True]) - assign(metomi.rose.config_editor.TOOLBAR_REVERT, self.revert_to_saved_data) + assign( + metomi.rose.config_editor.TOOLBAR_REDO, self.perform_undo, [True] + ) + assign( + metomi.rose.config_editor.TOOLBAR_REVERT, self.revert_to_saved_data + ) assign(metomi.rose.config_editor.TOOLBAR_FIND_NEXT, self._launch_find) - assign(metomi.rose.config_editor.TOOLBAR_VALIDATE, - self.main_handle.check_all_extra) - assign(metomi.rose.config_editor.TOOLBAR_TRANSFORM, - self.main_handle.transform_default) + assign( + metomi.rose.config_editor.TOOLBAR_VALIDATE, + self.main_handle.check_all_extra, + ) + assign( + metomi.rose.config_editor.TOOLBAR_TRANSFORM, + self.main_handle.transform_default, + ) self.find_entry = self.toolbar.item_dict.get( - metomi.rose.config_editor.TOOLBAR_FIND)['widget'] + metomi.rose.config_editor.TOOLBAR_FIND + )["widget"] self.find_entry.connect("activate", self._launch_find) self.find_entry.connect("changed", self._clear_find) Gtk.Entry.set_placeholder_text(self.find_entry, "Search") add_icon = self.toolbar.item_dict.get( - metomi.rose.config_editor.TOOLBAR_ADD)['widget'] - add_icon.connect('button_press_event', self.add_page_variable) + metomi.rose.config_editor.TOOLBAR_ADD + )["widget"] + add_icon.connect("button_press_event", self.add_page_variable) def generate_menubar(self): """Link in the menu functionality and accelerators.""" self.menubar = metomi.rose.config_editor.menu.MenuBar() self.menu_widgets = {} menu_list = [ - ('/TopMenuBar/File/Open...', self.load_from_file), - ('/TopMenuBar/File/Save', lambda m: self.save_to_file()), - ('/TopMenuBar/File/Check and save', - lambda m: self.save_to_file(check_on_save=True)), - ('/TopMenuBar/File/Load All Apps', - lambda m: self.handle_load_all()), - ('/TopMenuBar/File/Quit', self.main_handle.destroy), - ('/TopMenuBar/Edit/Undo', - lambda m: self.perform_undo()), - ('/TopMenuBar/Edit/Redo', - lambda m: self.perform_undo(redo_mode_on=True)), - ('/TopMenuBar/Edit/Find', self._launch_find), - ('/TopMenuBar/Edit/Find Next', - lambda m: self.perform_find(self.find_hist['regex'])), - ('/TopMenuBar/Edit/Preferences', self.main_handle.prefs), - ('/TopMenuBar/Edit/Stack', self.main_handle.view_stack), - ('/TopMenuBar/View/View fixed vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_FIXED, - m.get_active() - )), - ('/TopMenuBar/View/View ignored vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_IGNORED, - m.get_active() - )), - ('/TopMenuBar/View/View user-ignored vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, - m.get_active() - )), - ('/TopMenuBar/View/View latent vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_LATENT, - m.get_active() - )), - ('/TopMenuBar/View/View ignored pages', - lambda m: self._set_page_ns_show_modes( - metomi.rose.config_editor.SHOW_MODE_IGNORED, - m.get_active() - )), - ('/TopMenuBar/View/View user-ignored pages', - lambda m: self._set_page_ns_show_modes( - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, - m.get_active() - )), - ('/TopMenuBar/View/View latent pages', - lambda m: self._set_page_ns_show_modes( - metomi.rose.config_editor.SHOW_MODE_LATENT, - m.get_active() - )), - ('/TopMenuBar/View/Flag no-metadata vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META, - m.get_active() - )), - ('/TopMenuBar/View/Flag opt config vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF, - m.get_active() - )), - ('/TopMenuBar/View/Flag optional vars', - lambda m: self._set_page_var_show_modes( - metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL, - m.get_active() - )), - ('/TopMenuBar/View/View status bar', - lambda m: self._set_show_status_bar(m.get_active())), - ('/TopMenuBar/Metadata/Prefs/View without descriptions', - lambda m: self._set_page_show_modes( - metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION, - m.get_active() - )), - ('/TopMenuBar/Metadata/Prefs/View without help', - lambda m: self._set_page_show_modes( - metomi.rose.config_editor.SHOW_MODE_NO_HELP, - m.get_active() - )), - ('/TopMenuBar/Metadata/Prefs/View without titles', - lambda m: self._set_page_show_modes( - metomi.rose.config_editor.SHOW_MODE_NO_TITLE, - m.get_active() - )), - ('/TopMenuBar/Metadata/All V', - lambda m: self.main_handle.handle_run_custom_macro( - method_name=metomi.rose.macro.VALIDATE_METHOD - )), - ('/TopMenuBar/Metadata/Autofix', - lambda m: self.main_handle.transform_default()), - ('/TopMenuBar/Metadata/Extra checks', - lambda m: self.main_handle.check_fail_rules()), - ('/TopMenuBar/Metadata/Graph', - lambda m: self.main_handle.handle_graph()), - ('/TopMenuBar/Metadata/Reload metadata', - lambda m: self._refresh_metadata_if_on()), - ('/TopMenuBar/Metadata/Load custom metadata', - lambda m: self.load_custom_metadata()), - ('/TopMenuBar/Metadata/Switch off metadata', - lambda m: self.refresh_metadata(m.get_active())), - ('/TopMenuBar/Metadata/Upgrade', - lambda m: self.main_handle.handle_upgrade()), - ('/TopMenuBar/Tools/Browser', - lambda m: self.main_handle.launch_browser()), - ('/TopMenuBar/Tools/Terminal', - lambda m: self.main_handle.launch_terminal()), - ('/TopMenuBar/Page/Revert', - lambda m: self.revert_to_saved_data()), - ('/TopMenuBar/Page/Page Info', - lambda m: self.nav_handle.info_request( - self._get_current_page().namespace - )), - ('/TopMenuBar/Page/Page Help', - lambda m: self._get_current_page().launch_help()), - ('/TopMenuBar/Page/Page Web Help', - lambda m: self._get_current_page().launch_url()), - ('/TopMenuBar/Help/Documentation', self.main_handle.help), - ('/TopMenuBar/Help/About', self.main_handle.about_dialog) + ("/TopMenuBar/File/Open...", self.load_from_file), + ("/TopMenuBar/File/Save", lambda m: self.save_to_file()), + ( + "/TopMenuBar/File/Check and save", + lambda m: self.save_to_file(check_on_save=True), + ), + ( + "/TopMenuBar/File/Load All Apps", + lambda m: self.handle_load_all(), + ), + ("/TopMenuBar/File/Quit", self.main_handle.destroy), + ("/TopMenuBar/Edit/Undo", lambda m: self.perform_undo()), + ( + "/TopMenuBar/Edit/Redo", + lambda m: self.perform_undo(redo_mode_on=True), + ), + ("/TopMenuBar/Edit/Find", self._launch_find), + ( + "/TopMenuBar/Edit/Find Next", + lambda m: self.perform_find(self.find_hist["regex"]), + ), + ("/TopMenuBar/Edit/Preferences", self.main_handle.prefs), + ("/TopMenuBar/Edit/Stack", self.main_handle.view_stack), + ( + "/TopMenuBar/View/View fixed vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_FIXED, m.get_active() + ), + ), + ( + "/TopMenuBar/View/View ignored vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_IGNORED, m.get_active() + ), + ), + ( + "/TopMenuBar/View/View user-ignored vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, + m.get_active(), + ), + ), + ( + "/TopMenuBar/View/View latent vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_LATENT, m.get_active() + ), + ), + ( + "/TopMenuBar/View/View ignored pages", + lambda m: self._set_page_ns_show_modes( + metomi.rose.config_editor.SHOW_MODE_IGNORED, m.get_active() + ), + ), + ( + "/TopMenuBar/View/View user-ignored pages", + lambda m: self._set_page_ns_show_modes( + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, + m.get_active(), + ), + ), + ( + "/TopMenuBar/View/View latent pages", + lambda m: self._set_page_ns_show_modes( + metomi.rose.config_editor.SHOW_MODE_LATENT, m.get_active() + ), + ), + ( + "/TopMenuBar/View/Flag no-metadata vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_FLAG_NO_META, + m.get_active(), + ), + ), + ( + "/TopMenuBar/View/Flag opt config vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_FLAG_OPT_CONF, + m.get_active(), + ), + ), + ( + "/TopMenuBar/View/Flag optional vars", + lambda m: self._set_page_var_show_modes( + metomi.rose.config_editor.SHOW_MODE_FLAG_OPTIONAL, + m.get_active(), + ), + ), + ( + "/TopMenuBar/View/View status bar", + lambda m: self._set_show_status_bar(m.get_active()), + ), + ( + "/TopMenuBar/Metadata/Prefs/View without descriptions", + lambda m: self._set_page_show_modes( + metomi.rose.config_editor.SHOW_MODE_NO_DESCRIPTION, + m.get_active(), + ), + ), + ( + "/TopMenuBar/Metadata/Prefs/View without help", + lambda m: self._set_page_show_modes( + metomi.rose.config_editor.SHOW_MODE_NO_HELP, m.get_active() + ), + ), + ( + "/TopMenuBar/Metadata/Prefs/View without titles", + lambda m: self._set_page_show_modes( + metomi.rose.config_editor.SHOW_MODE_NO_TITLE, + m.get_active(), + ), + ), + ( + "/TopMenuBar/Metadata/All V", + lambda m: self.main_handle.handle_run_custom_macro( + method_name=metomi.rose.macro.VALIDATE_METHOD + ), + ), + ( + "/TopMenuBar/Metadata/Autofix", + lambda m: self.main_handle.transform_default(), + ), + ( + "/TopMenuBar/Metadata/Extra checks", + lambda m: self.main_handle.check_fail_rules(), + ), + ( + "/TopMenuBar/Metadata/Graph", + lambda m: self.main_handle.handle_graph(), + ), + ( + "/TopMenuBar/Metadata/Reload metadata", + lambda m: self._refresh_metadata_if_on(), + ), + ( + "/TopMenuBar/Metadata/Load custom metadata", + lambda m: self.load_custom_metadata(), + ), + ( + "/TopMenuBar/Metadata/Switch off metadata", + lambda m: self.refresh_metadata(m.get_active()), + ), + ( + "/TopMenuBar/Metadata/Upgrade", + lambda m: self.main_handle.handle_upgrade(), + ), + ( + "/TopMenuBar/Tools/Browser", + lambda m: self.main_handle.launch_browser(), + ), + ( + "/TopMenuBar/Tools/Terminal", + lambda m: self.main_handle.launch_terminal(), + ), + ("/TopMenuBar/Page/Revert", lambda m: self.revert_to_saved_data()), + ( + "/TopMenuBar/Page/Page Info", + lambda m: self.nav_handle.info_request( + self._get_current_page().namespace + ), + ), + ( + "/TopMenuBar/Page/Page Help", + lambda m: self._get_current_page().launch_help(), + ), + ( + "/TopMenuBar/Page/Page Web Help", + lambda m: self._get_current_page().launch_url(), + ), + ("/TopMenuBar/Help/Documentation", self.main_handle.help), + ("/TopMenuBar/Help/About", self.main_handle.about_dialog), ] is_toggled = dict( - [('/TopMenuBar/View/View fixed vars', - metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS), - ('/TopMenuBar/View/View ignored vars', - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS), - ('/TopMenuBar/View/View user-ignored vars', - metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS), - ('/TopMenuBar/View/View latent vars', - metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS), - ('/TopMenuBar/Metadata/Prefs/View without descriptions', - metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION), - ('/TopMenuBar/Metadata/Prefs/View without help', - metomi.rose.config_editor.SHOULD_SHOW_NO_HELP), - ('/TopMenuBar/Metadata/Prefs/View without titles', - metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE), - ('/TopMenuBar/View/View ignored pages', - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES), - ('/TopMenuBar/View/View user-ignored pages', - metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES), - ('/TopMenuBar/View/View latent pages', - metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES), - ('/TopMenuBar/View/Flag opt config vars', - metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS), - ('/TopMenuBar/View/Flag optional vars', - metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS), - ('/TopMenuBar/View/Flag no-metadata vars', - metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS), - ('/TopMenuBar/View/View status bar', - metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR), - ('/TopMenuBar/Metadata/Switch off metadata', - self.metadata_off)] + [ + ( + "/TopMenuBar/View/View fixed vars", + metomi.rose.config_editor.SHOULD_SHOW_FIXED_VARS, + ), + ( + "/TopMenuBar/View/View ignored vars", + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS, + ), + ( + "/TopMenuBar/View/View user-ignored vars", + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_VARS, + ), + ( + "/TopMenuBar/View/View latent vars", + metomi.rose.config_editor.SHOULD_SHOW_LATENT_VARS, + ), + ( + "/TopMenuBar/Metadata/Prefs/View without descriptions", + metomi.rose.config_editor.SHOULD_SHOW_NO_DESCRIPTION, + ), + ( + "/TopMenuBar/Metadata/Prefs/View without help", + metomi.rose.config_editor.SHOULD_SHOW_NO_HELP, + ), + ( + "/TopMenuBar/Metadata/Prefs/View without titles", + metomi.rose.config_editor.SHOULD_SHOW_NO_TITLE, + ), + ( + "/TopMenuBar/View/View ignored pages", + metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES, + ), + ( + "/TopMenuBar/View/View user-ignored pages", + metomi.rose.config_editor.SHOULD_SHOW_USER_IGNORED_PAGES, + ), + ( + "/TopMenuBar/View/View latent pages", + metomi.rose.config_editor.SHOULD_SHOW_LATENT_PAGES, + ), + ( + "/TopMenuBar/View/Flag opt config vars", + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPT_CONF_VARS, + ), + ( + "/TopMenuBar/View/Flag optional vars", + metomi.rose.config_editor.SHOULD_SHOW_FLAG_OPTIONAL_VARS, + ), + ( + "/TopMenuBar/View/Flag no-metadata vars", + metomi.rose.config_editor.SHOULD_SHOW_FLAG_NO_META_VARS, + ), + ( + "/TopMenuBar/View/View status bar", + metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR, + ), + ( + "/TopMenuBar/Metadata/Switch off metadata", + self.metadata_off, + ), + ] ) - for (address, action) in menu_list: + for address, action in menu_list: widget = self.menubar.uimanager.get_widget(address) self.menu_widgets.update({address: widget}) if address in is_toggled: widget.set_active(is_toggled[address]) - if (address.endswith("View user-ignored pages") and - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES): + if ( + address.endswith("View user-ignored pages") + and metomi.rose.config_editor.SHOULD_SHOW_IGNORED_PAGES + ): widget.set_sensitive(False) - if (address.endswith("View user-ignored vars") and - metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS): + if ( + address.endswith("View user-ignored vars") + and metomi.rose.config_editor.SHOULD_SHOW_IGNORED_VARS + ): widget.set_sensitive(False) if address.endswith("Reload metadata") and self.metadata_off: widget.set_sensitive(False) - widget.connect('activate', action) + widget.connect("activate", action) page_menu = self.menubar.uimanager.get_widget("/TopMenuBar/Page") add_menuitem = self.menubar.uimanager.get_widget( - "/TopMenuBar/Page/Add variable") + "/TopMenuBar/Page/Add variable" + ) page_menu.connect( "activate", lambda m: self.main_handle.load_page_menu( - self.menubar, - add_menuitem, - self._get_current_page() - )) + self.menubar, add_menuitem, self._get_current_page() + ), + ) page_menu.get_submenu().connect( "deactivate", lambda m: self.main_handle.clear_page_menu( - self.menubar, - add_menuitem - )) + self.menubar, add_menuitem + ), + ) self.main_handle.load_macro_menu(self.menubar) self.update_bar_widgets() - self.top_menu = self.menubar.uimanager.get_widget('/TopMenuBar') + self.top_menu = self.menubar.uimanager.get_widget("/TopMenuBar") # Load the keyboard accelerators. accel = { - metomi.rose.config_editor.ACCEL_UNDO: - self.perform_undo, - metomi.rose.config_editor.ACCEL_REDO: - lambda: self.perform_undo(redo_mode_on=True), - metomi.rose.config_editor.ACCEL_FIND: - self.find_entry.grab_focus, - metomi.rose.config_editor.ACCEL_FIND_NEXT: - lambda: self.perform_find(self.find_hist['regex']), - metomi.rose.config_editor.ACCEL_HELP_GUI: - self.main_handle.help, - metomi.rose.config_editor.ACCEL_OPEN: - self.load_from_file, - metomi.rose.config_editor.ACCEL_SAVE: - self.save_to_file, - metomi.rose.config_editor.ACCEL_QUIT: - self.main_handle.destroy, - metomi.rose.config_editor.ACCEL_METADATA_REFRESH: - self._refresh_metadata_if_on, - metomi.rose.config_editor.ACCEL_BROWSER: - self.main_handle.launch_browser, - metomi.rose.config_editor.ACCEL_TERMINAL: - self.main_handle.launch_terminal, + metomi.rose.config_editor.ACCEL_UNDO: self.perform_undo, + metomi.rose.config_editor.ACCEL_REDO: lambda: self.perform_undo( + redo_mode_on=True + ), + metomi.rose.config_editor.ACCEL_FIND: self.find_entry.grab_focus, + metomi.rose.config_editor.ACCEL_FIND_NEXT: ( + lambda: self.perform_find(self.find_hist["regex"]) + ), + metomi.rose.config_editor.ACCEL_HELP_GUI: self.main_handle.help, + metomi.rose.config_editor.ACCEL_OPEN: self.load_from_file, + metomi.rose.config_editor.ACCEL_SAVE: self.save_to_file, + metomi.rose.config_editor.ACCEL_QUIT: self.main_handle.destroy, + metomi.rose.config_editor.ACCEL_METADATA_REFRESH: ( + self._refresh_metadata_if_on + ), + metomi.rose.config_editor.ACCEL_BROWSER: ( + self.main_handle.launch_browser + ), + metomi.rose.config_editor.ACCEL_TERMINAL: ( + self.main_handle.launch_terminal + ), } self.menubar.set_accelerators(accel) def generate_nav_panel(self): - """"Create tree panel and link functions.""" - self.nav_panel = metomi.rose.config_editor.nav_panel.PageNavigationPanel( - self.nav_controller.namespace_tree, - self.handle_launch_request, - self.nav_handle.get_ns_metadata_and_comments, - self.nav_handle.popup_panel_menu, - self.nav_handle.get_can_show_page, - self.nav_handle.ask_is_preview + """ "Create tree panel and link functions.""" + self.nav_panel = ( + metomi.rose.config_editor.nav_panel.PageNavigationPanel( + self.nav_controller.namespace_tree, + self.handle_launch_request, + self.nav_handle.get_ns_metadata_and_comments, + self.nav_handle.popup_panel_menu, + self.nav_handle.get_can_show_page, + self.nav_handle.ask_is_preview, + ) ) def generate_status_bar(self): """Create a status bar.""" self.status_bar = metomi.rose.config_editor.status.StatusBar( - verbosity=metomi.rose.config_editor.STATUS_BAR_VERBOSITY) - self._set_show_status_bar(metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR) + verbosity=metomi.rose.config_editor.STATUS_BAR_VERBOSITY + ) + self._set_show_status_bar( + metomi.rose.config_editor.SHOULD_SHOW_STATUS_BAR + ) -# ----------------- Page manipulation functions ------------------------------ + # ----------------- Page manipulation functions --------------------------- def handle_load_all(self, *args): """Handle a request to load all preview configurations.""" @@ -622,24 +800,30 @@ def handle_load_all(self, *args): if self.data.config[item].is_preview: load_these.append(item) load_these.sort() - number_of_events = (len(load_these) * - metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + number_of_events = ( + len(load_these) * metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + + 2 + ) self.reporter.report_load_event( - "Loading all preview apps", - new_total_events=number_of_events + "Loading all preview apps", new_total_events=number_of_events ) for namespace_name in load_these: config_data = self.data.config[namespace_name] - self.data.load_config(config_data.directory, preview=False, - metadata_off=self.metadata_off) + self.data.load_config( + config_data.directory, + preview=False, + metadata_off=self.metadata_off, + ) self.reporter.report_load_event( - metomi.rose.config_editor.EVENT_LOADED.format(namespace_name[1:]), - no_progress=True + metomi.rose.config_editor.EVENT_LOADED.format( + namespace_name[1:] + ), + no_progress=True, ) self.reload_namespace_tree() self.reporter.stop() self.nav_panel.update_row_tooltips() - if hasattr(self, 'menubar'): + if hasattr(self, "menubar"): self.main_handle.load_macro_menu(self.menubar) self.update_bar_widgets() self.updater.perform_startup_check() @@ -654,8 +838,8 @@ def handle_launch_request(self, namespace_name, as_new=False): in the internal notebook, unless as_new is True. """ - if not namespace_name.startswith('/'): - namespace_name = '/' + namespace_name + if not namespace_name.startswith("/"): + namespace_name = "/" + namespace_name config_name = self.util.split_full_ns(self.data, namespace_name)[0] config_data = self.data.config[config_name] @@ -663,17 +847,23 @@ def handle_launch_request(self, namespace_name, as_new=False): if config_data.is_preview: self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOAD_ATTEMPT.format( - namespace_name), - new_total_events=3) - self.data.load_config(config_data.directory, preview=False, - metadata_off=self.metadata_off) + namespace_name + ), + new_total_events=3, + ) + self.data.load_config( + config_data.directory, + preview=False, + metadata_off=self.metadata_off, + ) self.reload_namespace_tree() self.nav_panel.update_row_tooltips() self.reporter.report_load_event( metomi.rose.config_editor.EVENT_LOADED.format(namespace_name), - no_progress=True) + no_progress=True, + ) self.reporter.stop() - if hasattr(self, 'menubar'): + if hasattr(self, "menubar"): self.main_handle.load_macro_menu(self.menubar) self.update_bar_widgets() self.updater.perform_startup_check() @@ -702,36 +892,43 @@ def handle_launch_request(self, namespace_name, as_new=False): def make_page(self, namespace_name): """Look up page data and attributes and call a page constructor.""" - config_name, subspace = self.util.split_full_ns(self.data, - namespace_name) + config_name, subspace = self.util.split_full_ns( + self.data, namespace_name + ) data, latent_data = self.data.helper.get_data_for_namespace( - namespace_name) + namespace_name + ) config_data = self.data.config[config_name] ns_metadata = self.data.namespace_meta_lookup.get(namespace_name, {}) - description = ns_metadata.get(metomi.rose.META_PROP_DESCRIPTION, '') + description = ns_metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") duplicate = ns_metadata.get(metomi.rose.META_PROP_DUPLICATE) help_ = ns_metadata.get(metomi.rose.META_PROP_HELP) url = ns_metadata.get(metomi.rose.META_PROP_URL) - custom_widget = ns_metadata.get(metomi.rose.config_editor.META_PROP_WIDGET) + custom_widget = ns_metadata.get( + metomi.rose.config_editor.META_PROP_WIDGET + ) custom_sub_widget = ns_metadata.get( - metomi.rose.config_editor.META_PROP_WIDGET_SUB_NS) + metomi.rose.config_editor.META_PROP_WIDGET_SUB_NS + ) has_sub_data = self.data.helper.is_ns_sub_data(namespace_name) label = ns_metadata.get(metomi.rose.META_PROP_TITLE) if label is None: - label = subspace.split('/')[-1] + label = subspace.split("/")[-1] if duplicate == metomi.rose.META_PROP_VALUE_TRUE and not has_sub_data: # For example, namelist/foo/1 should be shown as foo(1). - label = "(".join(subspace.split('/')[-2:]) + ")" + label = "(".join(subspace.split("/")[-2:]) + ")" section_data_objects, latent_section_data_objects = ( - self.data.helper.get_section_data_for_namespace(namespace_name)) + self.data.helper.get_section_data_for_namespace(namespace_name) + ) # Related pages - see_also = '' - sections = [s for s in ns_metadata.get('sections', [])] - for section_name in [s for s in sections if s.startswith('namelist')]: - no_num_name = metomi.rose.macro.REC_ID_STRIP_DUPL.sub("", section_name) + see_also = "" + sections = [s for s in ns_metadata.get("sections", [])] + for section_name in [s for s in sections if s.startswith("namelist")]: + no_num_name = metomi.rose.macro.REC_ID_STRIP_DUPL.sub( + "", section_name + ) no_mod_name = metomi.rose.macro.REC_ID_STRIP.sub("", section_name) - ok_names = [section_name, no_num_name + "(:)", - no_mod_name + "(:)"] + ok_names = [section_name, no_num_name + "(:)", no_mod_name + "(:)"] if no_mod_name != no_num_name: # There's a modifier in the section name. ok_names.append(no_num_name) @@ -741,13 +938,15 @@ def make_page(self, namespace_name): for variable in variables: if variable.name != metomi.rose.FILE_VAR_SOURCE: continue - var_values = metomi.rose.variable.array_split(variable.value) + var_values = metomi.rose.variable.array_split( + variable.value + ) for i, val in enumerate(var_values): if val.startswith("(") and val.endswith(")"): # It is optional - e.g. "(namelist:baz)". var_values[i] = val[1:-1] if set(ok_names) & set(var_values): - var_id = variable.metadata['id'] + var_id = variable.metadata["id"] see_also += ", " + var_id see_also = see_also.replace(", ", "", 1) # Icon @@ -757,10 +956,12 @@ def make_page(self, namespace_name): sub_ops = None if has_sub_data: sub_data = self.data.helper.get_sub_data_for_namespace( - namespace_name) + namespace_name + ) sub_ops = self.group_ops.get_sub_ops_for_namespace(namespace_name) macro_info = self.data.helper.get_macro_info_for_namespace( - namespace_name) + namespace_name + ) page_metadata = { "namespace": namespace_name, "ns_is_default": is_default, @@ -775,36 +976,40 @@ def make_page(self, namespace_name): "see_also": see_also, "config_name": config_name, "show_modes": self.page_var_show_modes, - "icon": icon_path + "icon": icon_path, } if len(sections) == 1: page_metadata.update({"id": sections.pop()}) sect_ops = metomi.rose.config_editor.ops.section.SectionOperations( - self.data, self.util, self.reporter, - self.undo_stack, self.redo_stack, + self.data, + self.util, + self.reporter, + self.undo_stack, + self.redo_stack, self.check_cannot_enable_setting, self.updater.update_namespace, self.updater.update_ns_sub_data, self.updater.update_ns_info, update_tree_func=self.reload_namespace_tree, view_page_func=self.view_page, - kill_page_func=self.kill_page + kill_page_func=self.kill_page, ) var_ops = metomi.rose.config_editor.ops.variable.VariableOperations( - self.data, self.util, self.reporter, - self.undo_stack, self.redo_stack, + self.data, + self.util, + self.reporter, + self.undo_stack, + self.redo_stack, sect_ops.add_section, self.check_cannot_enable_setting, self.updater.update_namespace, - search_id_func=self.perform_find_by_id + search_id_func=self.perform_find_by_id, ) directory = None if namespace_name == config_name: directory = config_data.directory - launch_info = lambda: self.nav_handle.info_request( - namespace_name) - launch_edit = lambda: self.nav_handle.edit_request( - namespace_name) + launch_info = lambda: self.nav_handle.info_request(namespace_name) + launch_edit = lambda: self.nav_handle.edit_request(namespace_name) page = metomi.rose.config_editor.page.ConfigPage( page_metadata, data, @@ -820,7 +1025,7 @@ def make_page(self, namespace_name): sub_ops=sub_ops, launch_info_func=launch_info, launch_edit_func=launch_edit, - launch_macro_func=self.main_handle.handle_run_custom_macro + launch_macro_func=self.main_handle.handle_run_custom_macro, ) # FIXME: These three should go. page.trigger_tab_detach = lambda b: self._handle_detach_request(page) @@ -841,37 +1046,49 @@ def _handle_detach_request(self, page, old_window=None): tab_window = Gtk.Window() tab_window.set_icon(self.mainwindow.window.get_icon()) tab_window.add_accel_group(self.menubar.accelerators) - tab_window.set_default_size(*metomi.rose.config_editor.SIZE_PAGE_DETACH) - tab_window.connect('destroy-event', lambda w, e: - self.tab_windows.remove(w) and False) - tab_window.connect('delete-event', lambda w, e: - self.tab_windows.remove(w) and False) + tab_window.set_default_size( + *metomi.rose.config_editor.SIZE_PAGE_DETACH + ) + tab_window.connect( + "destroy-event", + lambda w, e: self.tab_windows.remove(w) and False, + ) + tab_window.connect( + "delete-event", + lambda w, e: self.tab_windows.remove(w) and False, + ) else: tab_window = old_window add_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_ADD, tip_text=metomi.rose.config_editor.TIP_ADD_TO_PAGE, size=Gtk.IconSize.LARGE_TOOLBAR, - as_tool=True + as_tool=True, ) revert_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_REVERT_TO_SAVED, tip_text=metomi.rose.config_editor.TIP_REVERT_PAGE, size=Gtk.IconSize.LARGE_TOOLBAR, - as_tool=True + as_tool=True, ) - add_button.connect('button_press_event', self.add_page_variable) - revert_button.connect('clicked', - lambda b: self.revert_to_saved_data()) + add_button.connect("button_press_event", self.add_page_variable) + revert_button.connect("clicked", lambda b: self.revert_to_saved_data()) if old_window is None: parent = self.notebook else: parent = old_window page.reshuffle_for_detached(add_button, revert_button, parent) - tab_window.set_title(' - '.join([page.label, self.data.top_level_name, - metomi.rose.config_editor.PROGRAM_NAME])) + tab_window.set_title( + " - ".join( + [ + page.label, + self.data.top_level_name, + metomi.rose.config_editor.PROGRAM_NAME, + ] + ) + ) tab_window.add(page) - tab_window.connect_after('focus-in-event', self.handle_page_change) + tab_window.connect_after("focus-in-event", self.handle_page_change) if old_window is None: self.tab_windows.append(tab_window) tab_window.show() @@ -891,11 +1108,11 @@ def handle_page_change(self, *args): def update_page_bar_sensitivity(self, current_page): """Update the top 'Page' menu and the toolbar.""" - if not hasattr(self, 'toolbar') or not hasattr(self, 'menubar'): + if not hasattr(self, "toolbar") or not hasattr(self, "menubar"): return False - page_icons = ['Add to page...', 'Revert page to saved'] + page_icons = ["Add to page...", "Revert page to saved"] get_widget = self.menubar.uimanager.get_widget - page_menu = get_widget('/TopMenuBar/Page') + page_menu = get_widget("/TopMenuBar/Page") page_menuitems = page_menu.get_submenu().get_children() if current_page is None or not self.notebook.get_n_pages(): for name in page_icons: @@ -910,14 +1127,16 @@ def update_page_bar_sensitivity(self, current_page): ns = current_page.namespace metadata = self.data.namespace_meta_lookup.get(ns, {}) get_widget("/TopMenuBar/Page/Page Help").set_sensitive( - metomi.rose.META_PROP_HELP in metadata) + metomi.rose.META_PROP_HELP in metadata + ) get_widget("/TopMenuBar/Page/Page Web Help").set_sensitive( - metomi.rose.META_PROP_URL in metadata) + metomi.rose.META_PROP_URL in metadata + ) def set_current_page_indicator(self, namespace): """Make sure the current page is highlighted in the nav panel.""" - if hasattr(self, 'nav_panel'): - self.nav_panel.select_row(namespace.lstrip('/').split('/')) + if hasattr(self, "nav_panel"): + self.nav_panel.select_row(namespace.lstrip("/").split("/")) def add_page_variable(self, widget, event): """Launch an add menu based on page content.""" @@ -935,23 +1154,27 @@ def revert_to_saved_data(self): config_name = self.util.split_full_ns(self.data, namespace)[0] self.data.load_node_namespaces(config_name, from_saved=True) config_data, ghost_data = self.data.helper.get_data_for_namespace( - namespace, from_saved=True) + namespace, from_saved=True + ) page.reload_from_data(config_data, ghost_data) self.data.load_node_namespaces(config_name) self.updater.update_status(page) - self.reporter.report(metomi.rose.config_editor.EVENT_REVERT.format( - namespace.lstrip("/"))) + self.reporter.report( + metomi.rose.config_editor.EVENT_REVERT.format( + namespace.lstrip("/") + ) + ) def _get_pagelist(self): """Load an attribute self.pagelist with a list of open pages.""" self.pagelist = [] - if hasattr(self, 'notebook'): + if hasattr(self, "notebook"): for index in range(self.notebook.get_n_pages()): - if hasattr(self.notebook.get_nth_page(index), 'panel_data'): + if hasattr(self.notebook.get_nth_page(index), "panel_data"): self.pagelist.append(self.notebook.get_nth_page(index)) - if hasattr(self, 'tab_windows'): + if hasattr(self, "tab_windows"): for window in self.tab_windows: - if hasattr(window.get_child(), 'panel_data'): + if hasattr(window.get_child(), "panel_data"): self.pagelist.append(window.get_child()) self.pagelist.extend(self.orphan_pages) return self.pagelist @@ -995,10 +1218,13 @@ def _set_page_show_modes(self, key, is_key_allowed): def _set_page_ns_show_modes(self, key, is_key_allowed): """Set namespace view options.""" self.page_ns_show_modes[key] = is_key_allowed - if (hasattr(self, "menubar") and - key == metomi.rose.config_editor.SHOW_MODE_IGNORED): + if ( + hasattr(self, "menubar") + and key == metomi.rose.config_editor.SHOW_MODE_IGNORED + ): user_ign_item = self.menubar.uimanager.get_widget( - "/TopMenuBar/View/View user-ignored pages") + "/TopMenuBar/View/View user-ignored pages" + ) user_ign_item.set_sensitive(not is_key_allowed) def _set_page_var_show_modes(self, key, is_key_allowed): @@ -1007,10 +1233,13 @@ def _set_page_var_show_modes(self, key, is_key_allowed): self._get_pagelist() for page in self.pagelist: page.react_to_show_modes(key, is_key_allowed) - if (hasattr(self, "menubar") and - key == metomi.rose.config_editor.SHOW_MODE_IGNORED): + if ( + hasattr(self, "menubar") + and key == metomi.rose.config_editor.SHOW_MODE_IGNORED + ): user_ign_item = self.menubar.uimanager.get_widget( - "/TopMenuBar/View/View user-ignored vars") + "/TopMenuBar/View/View user-ignored vars" + ) user_ign_item.set_sensitive(not is_key_allowed) def kill_page(self, namespace): @@ -1029,7 +1258,7 @@ def kill_page(self, namespace): else: self.orphan_pages.remove(page) -# ----------------- Update functions ----------------------------------------- + # ----------------- Update functions -------------------------------------- def reload_namespace_tree(self, *args, **kwargs): """Redraw the navigation namespace tree.""" @@ -1059,7 +1288,7 @@ def update_ns_sub_data(self, *args, **kwargs): """Placeholder for updater function of the same name.""" self.updater.update_ns_sub_data(*args, **kwargs) -# ----------------- Page viewer function ------------------------------------- + # ----------------- Page viewer function ---------------------------------- def view_page(self, page_id, var_id=None): """Set focus by namespace (page_id), and optionally by var key.""" @@ -1072,7 +1301,7 @@ def view_page(self, page_id, var_id=None): self.handle_page_change() # Just to make sure. return current_page self._get_pagelist() - if (page_id not in [p.namespace for p in self.pagelist]): + if page_id not in [p.namespace for p in self.pagelist]: self.handle_launch_request(page_id, as_new=True) index = self.notebook.get_current_page() page = self.notebook.get_nth_page(index) @@ -1093,7 +1322,7 @@ def view_page(self, page_id, var_id=None): self.set_current_page_indicator(page_id) return page -# ----------------- Primary menu functions ----------------------------------- + # ----------------- Primary menu functions -------------------------------- def load_from_file(self, somewidget=None): """Open a standard dialogue and load a config file, if selected.""" @@ -1103,8 +1332,9 @@ def load_from_file(self, somewidget=None): if self.data.top_level_directory is None and not self.is_pluggable: self.data.load_top_config(dirname) self.data.saved_config_names = set(self.data.config.keys()) - self.mainwindow.window.set_title(self.data.top_level_name + - ' - rose-config-editor') + self.mainwindow.window.set_title( + self.data.top_level_name + " - rose-config-editor" + ) self.updater.update_all() self.updater.perform_startup_check() else: @@ -1131,18 +1361,20 @@ def save_to_file(self, only_config_name=None, check_on_save=False): vars_ok = True for var in config_data.vars.get_all(skip_latent=True): if not var.name: - self.view_page(var.metadata["full_ns"], - var.metadata["id"]) + self.view_page(var.metadata["full_ns"], var.metadata["id"]) page_address = var.metadata["full_ns"].lstrip("/") metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, metomi.rose.config_editor.ERROR_SAVE_BLANK.format( - short_config_name, - page_address + short_config_name, page_address + ), + title=( + metomi.rose.config_editor + .ERROR_SAVE_TITLE.format( + short_config_name + ) ), - title=metomi.rose.config_editor.ERROR_SAVE_TITLE.format( - short_config_name), - modal=False + modal=False, ) vars_ok = False break @@ -1156,19 +1388,23 @@ def save_to_file(self, only_config_name=None, check_on_save=False): # Run check fail-if, warn-if and validator macros if check_on_save if check_on_save: errors = self.nav_panel.get_change_error_totals( - config_name=short_config_name)[1] + config_name=short_config_name + )[1] if errors > 0: dialog = Gtk.MessageDialog( None, - Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.DialogFlags.MODAL + | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.YES_NO, - None + None, ) dialog.set_markup( - metomi.rose.config_editor.WARNING_ERRORS_FOUND_ON_SAVE.format( + metomi.rose.config_editor + .WARNING_ERRORS_FOUND_ON_SAVE.format( short_config_name - )) + ) + ) res = dialog.run() dialog.destroy() if res == Gtk.ResponseType.NO: @@ -1176,8 +1412,10 @@ def save_to_file(self, only_config_name=None, check_on_save=False): # Dump the configuration. filename = config_data.config_type - if (directory is None and - config_data.config_type == metomi.rose.INFO_CONFIG_NAME): + if ( + directory is None + and config_data.config_type == metomi.rose.INFO_CONFIG_NAME + ): directory = self.data.top_level_directory save_path = os.path.join(directory, filename) metomi.rose.macro.pretty_format_config(config, ignore_error=True) @@ -1188,8 +1426,9 @@ def save_to_file(self, only_config_name=None, check_on_save=False): metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, metomi.rose.config_editor.ERROR_SAVE_PATH_FAIL.format(exc), title=metomi.rose.config_editor.ERROR_SAVE_TITLE.format( - short_config_name), - modal=False + short_config_name + ), + modal=False, ) save_ok = False continue @@ -1234,7 +1473,7 @@ def output_config_objects(self, only_config_name=None): return_dict.update({config_name: config}) return return_dict -# ----------------- Secondary Menu/Dialog handling functions ----------------- + # ----------------- Secondary Menu/Dialog handling functions -------------- def apply_macro_transform(self, *args, **kwargs): """Placeholder for updater module function.""" @@ -1247,33 +1486,44 @@ def apply_macro_validation(self, *args, **kwargs): def _add_config(self, config_name, meta=None): """Add a configuration, optionally with META=TYPE=meta.""" config_short_name = config_name.split("/")[-1] - root = os.path.join(self.data.top_level_directory, - metomi.rose.SUB_CONFIGS_DIR) - new_path = os.path.join(root, config_short_name, metomi.rose.SUB_CONFIG_NAME) + root = os.path.join( + self.data.top_level_directory, metomi.rose.SUB_CONFIGS_DIR + ) + new_path = os.path.join( + root, config_short_name, metomi.rose.SUB_CONFIG_NAME + ) new_config = metomi.rose.config.ConfigNode() if meta is not None: new_config.set( - [metomi.rose.CONFIG_SECT_TOP, metomi.rose.CONFIG_OPT_META_TYPE], - meta + [ + metomi.rose.CONFIG_SECT_TOP, + metomi.rose.CONFIG_OPT_META_TYPE, + ], + meta, ) try: os.mkdir(os.path.dirname(new_path)) metomi.rose.config.dump(new_config, new_path) except (OSError, IOError) as exc: text = metomi.rose.config_editor.ERROR_CONFIG_CREATE.format( - new_path, type(exc), str(exc)) + new_path, type(exc), str(exc) + ) title = metomi.rose.config_editor.ERROR_CONFIG_CREATE_TITLE - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) return False - self.data.load_config(os.path.dirname(new_path), reload_tree_on=True, - skip_load_event=True) + self.data.load_config( + os.path.dirname(new_path), + reload_tree_on=True, + skip_load_event=True, + ) stack_item = metomi.rose.config_editor.stack.StackItem( config_name, metomi.rose.config_editor.STACK_ACTION_ADDED, - metomi.rose.variable.Variable('', '', {}), + metomi.rose.variable.Variable("", "", {}), self._remove_config, - (config_name, meta) + (config_name, meta), ) self.undo_stack.append(stack_item) while self.redo_stack: @@ -1294,30 +1544,34 @@ def _remove_config(self, config_name, meta=None): if name in self.notebook.get_page_ids(): self.notebook.delete_by_id(name) else: - tab_nses = [w.get_child().namespace - for w in self.tab_windows] + tab_nses = [ + w.get_child().namespace for w in self.tab_windows + ] page_window = self.tab_windows[tab_nses.index(name)] page_window.destroy() - self.group_ops.remove_sections(config_name, - list(config_data.sections.now.keys())) + self.group_ops.remove_sections( + config_name, list(config_data.sections.now.keys()) + ) if dirpath is not None: try: shutil.rmtree(dirpath) except (shutil.Error, OSError, IOError) as exc: text = metomi.rose.config_editor.ERROR_CONFIG_DELETE.format( - dirpath, type(exc), str(exc)) + dirpath, type(exc), str(exc) + ) title = metomi.rose.config_editor.ERROR_CONFIG_CREATE_TITLE - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) return False self.data.config.pop(config_name) self.reload_namespace_tree() stack_item = metomi.rose.config_editor.stack.StackItem( config_name, metomi.rose.config_editor.STACK_ACTION_REMOVED, - metomi.rose.variable.Variable('', '', {}), + metomi.rose.variable.Variable("", "", {}), self._add_config, - (config_name, meta) + (config_name, meta), ) self.undo_stack.append(stack_item) while self.redo_stack: @@ -1340,27 +1594,34 @@ def _has_preview_apps(self): def update_bar_widgets(self): """Update bar functionality like Undo and Redo.""" - if not hasattr(self, 'toolbar'): + if not hasattr(self, "toolbar"): return False - self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_UNDO, - len(self.undo_stack) > 0) - self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_REDO, - len(self.redo_stack) > 0) - self._get_menu_widget('/Undo').set_sensitive(len(self.undo_stack) > 0) - self._get_menu_widget('/Redo').set_sensitive(len(self.redo_stack) > 0) - self._get_menu_widget('/Find Next').set_sensitive( - len(self.find_hist['ids']) > 0) - self._get_menu_widget('/Load All Apps').set_sensitive( - self._has_preview_apps()) - self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_LOAD_APPS, - self._has_preview_apps()) + self.toolbar.set_widget_sensitive( + metomi.rose.config_editor.TOOLBAR_UNDO, len(self.undo_stack) > 0 + ) + self.toolbar.set_widget_sensitive( + metomi.rose.config_editor.TOOLBAR_REDO, len(self.redo_stack) > 0 + ) + self._get_menu_widget("/Undo").set_sensitive(len(self.undo_stack) > 0) + self._get_menu_widget("/Redo").set_sensitive(len(self.redo_stack) > 0) + self._get_menu_widget("/Find Next").set_sensitive( + len(self.find_hist["ids"]) > 0 + ) + self._get_menu_widget("/Load All Apps").set_sensitive( + self._has_preview_apps() + ) + self.toolbar.set_widget_sensitive( + metomi.rose.config_editor.TOOLBAR_LOAD_APPS, + self._has_preview_apps(), + ) if not hasattr(self, "nav_panel"): return False changes, errors = self.nav_panel.get_change_error_totals() self.status_bar.set_num_errors(errors) - self._get_menu_widget('/Autofix').set_sensitive(bool(errors)) - self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_TRANSFORM, - bool(errors)) + self._get_menu_widget("/Autofix").set_sensitive(bool(errors)) + self.toolbar.set_widget_sensitive( + metomi.rose.config_editor.TOOLBAR_TRANSFORM, bool(errors) + ) self._update_changed_sensitivity(is_changed=bool(changes)) def update_status_text(self, *args, **kwargs): @@ -1370,15 +1631,15 @@ def update_status_text(self, *args, **kwargs): def _update_changed_sensitivity(self, is_changed=False): """Alter sensitivity of 'unsaved changes' related widgets.""" - self.toolbar.set_widget_sensitive(metomi.rose.config_editor.TOOLBAR_SAVE, - is_changed) self.toolbar.set_widget_sensitive( - metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, - is_changed + metomi.rose.config_editor.TOOLBAR_SAVE, is_changed + ) + self.toolbar.set_widget_sensitive( + metomi.rose.config_editor.TOOLBAR_CHECK_AND_SAVE, is_changed ) - self._get_menu_widget('/Save').set_sensitive(is_changed) - self._get_menu_widget('/Check and save').set_sensitive(is_changed) - self._get_menu_widget('/Graph').set_sensitive(not is_changed) + self._get_menu_widget("/Save").set_sensitive(is_changed) + self._get_menu_widget("/Check and save").set_sensitive(is_changed) + self._get_menu_widget("/Graph").set_sensitive(not is_changed) def _refresh_metadata_if_on(self, config_name=None): """Reload any metadata, if present - otherwise do nothing.""" @@ -1388,9 +1649,10 @@ def _refresh_metadata_if_on(self, config_name=None): def refresh_metadata(self, metadata_off=False, only_this_config=None): """Switch metadata on/off and reloads namespaces.""" self.metadata_off = metadata_off - if hasattr(self, 'menubar'): - self._get_menu_widget('/Reload metadata').set_sensitive( - not self.metadata_off) + if hasattr(self, "menubar"): + self._get_menu_widget("/Reload metadata").set_sensitive( + not self.metadata_off + ) if only_this_config is None: configs = list(self.data.config.keys()) else: @@ -1408,20 +1670,26 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): if metadata_off: meta_config_tree = self.data.load_meta_config_tree( config_type=config_data.config_type, - opt_meta_paths=self.data.opt_meta_paths) + opt_meta_paths=self.data.opt_meta_paths, + ) meta_config = meta_config_tree.node meta_files = self.data.load_meta_files(meta_config_tree) macros = [] else: meta_config_tree = self.data.load_meta_config_tree( - config, directory, config_type=config_data.config_type, - opt_meta_paths=self.data.opt_meta_paths) + config, + directory, + config_type=config_data.config_type, + opt_meta_paths=self.data.opt_meta_paths, + ) meta_config = meta_config_tree.node meta_files = self.data.load_meta_files(meta_config_tree) - macro_module_prefix = ( - self.data.helper.get_macro_module_prefix(config_name)) + macro_module_prefix = self.data.helper.get_macro_module_prefix( + config_name + ) macros = metomi.rose.macro.load_meta_macro_modules( - meta_files, module_prefix=macro_module_prefix) + meta_files, module_prefix=macro_module_prefix + ) config_data.meta = meta_config self.data.load_builtin_macros(config_name) self.data.load_file_metadata(config_name) @@ -1429,14 +1697,18 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): # Load section and variable data into the object. sects, l_sects = self.data.load_sections_from_config(config_name) s_sects, s_l_sects = self.data.load_sections_from_config( - config_name, save=True) + config_name, save=True + ) config_data.sections = metomi.rose.config_editor.data.SectData( - sects, l_sects, s_sects, s_l_sects) + sects, l_sects, s_sects, s_l_sects + ) var, l_var = self.data.load_vars_from_config(config_name) s_var, s_l_var = self.data.load_vars_from_config( - config_name, save=True) + config_name, save=True + ) config_data.vars = metomi.rose.config_editor.data.VarData( - var, l_var, s_var, s_l_var) + var, l_var, s_var, s_l_var + ) config_data.meta_files = meta_files config_data.macros = macros self.data.load_node_namespaces(config_name) @@ -1446,15 +1718,15 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): self.reload_namespace_tree() if self.is_pluggable: self.updater.update_all() - if hasattr(self, 'menubar'): + if hasattr(self, "menubar"): self.main_handle.load_macro_menu(self.menubar) namespaces_updated = [] for config_name in configs: config_data = self.data.config[config_name] for variable in config_data.vars.get_all(skip_latent=True): - ns = variable.metadata.get('full_ns') + ns = variable.metadata.get("full_ns") if ns not in namespaces_updated: - self.updater.update_tree_status(ns, icon_type='changed') + self.updater.update_tree_status(ns, icon_type="changed") namespaces_updated.append(ns) self._get_pagelist() current_page, current_id = self._get_current_page_and_id() @@ -1469,7 +1741,8 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): if config_name not in configs: continue data, missing_data = self.data.helper.get_data_for_namespace( - namespace) + namespace + ) if len(data + missing_data) > 0: new_page = self.make_page(namespace) if new_page is None: @@ -1480,12 +1753,13 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): old_window = self.tab_windows[tab_pages.index(page)] old_window.remove(page) self._handle_detach_request(new_page, old_window) - elif hasattr(self, 'notebook'): + elif hasattr(self, "notebook"): # Replace a notebook page. index = self.notebook.get_page_ids().index(namespace) self.notebook.remove_page(index) - self.notebook.insert_page(new_page, new_page.labelwidget, - index) + self.notebook.insert_page( + new_page, new_page.labelwidget, index + ) else: # Replace an orphan page parent = page.get_parent() @@ -1499,8 +1773,9 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): # Preserve the old current page view, if possible. if current_namespace is not None: - config_name = self.util.split_full_ns(self.data, - current_namespace)[0] + config_name = self.util.split_full_ns( + self.data, current_namespace + )[0] self._get_pagelist() page_namespaces = [page.namespace for page in self.pagelist] if config_name in configs: @@ -1510,13 +1785,14 @@ def refresh_metadata(self, metadata_off=False, only_this_config=None): def load_custom_metadata(self): # open metadata dialog, use list() to pass by value paths = self.mainwindow.launch_metadata_manager( - list(self.data.opt_meta_paths)) + list(self.data.opt_meta_paths) + ) if paths is not None: # if form submitted self.data.opt_meta_paths = paths self.refresh_metadata() -# ----------------- Data-intensive menu functions / utilities ---------------- + # ----------------- Data-intensive menu functions / utilities ------------- def _launch_find(self, *args): """Get the find expression from a dialog.""" @@ -1524,17 +1800,19 @@ def _launch_find(self, *args): self.find_entry.grab_focus() expression = self.find_entry.get_text() start_page = self._get_current_page() - if expression is not None and expression != '': + if expression is not None and expression != "": page, var_id = self.perform_find(expression, start_page) if page is None: text = metomi.rose.config_editor.WARNING_NOT_FOUND self.find_entry.set_icon_from_stock( - 0, Gtk.STOCK_DIALOG_WARNING) + 0, Gtk.STOCK_DIALOG_WARNING + ) self.find_entry.set_icon_tooltip_text(0, text) else: if var_id is not None: self.reporter.report( - metomi.rose.config_editor.EVENT_FOUND_ID.format(var_id)) + metomi.rose.config_editor.EVENT_FOUND_ID.format(var_id) + ) self._clear_find() def _clear_find(self, *args): @@ -1543,7 +1821,7 @@ def _clear_find(self, *args): def perform_find(self, expression, start_page=None): """Drive the finding of the regex 'expression' within the data.""" - if expression == '': + if expression == "": return None, None page_id, var_id = self.get_found_page_and_id(expression, start_page) return self.view_page(page_id, var_id), var_id @@ -1558,14 +1836,15 @@ def perform_find_by_id(self, config_name, setting_id): section, option = self.util.get_section_option_from_id(setting_id) if option is None: page_id = self.data.helper.get_default_section_namespace( - section, config_name) + section, config_name + ) self.view_page(page_id) else: var = self.data.helper.get_variable_by_id(setting_id, config_name) if var is None: - var = self.data.helper.get_variable_by_id(setting_id, - config_name, - latent=True) + var = self.data.helper.get_variable_by_id( + setting_id, config_name, latent=True + ) if var is not None: page_id = var.metadata["full_ns"] self.view_page(page_id, setting_id) @@ -1578,12 +1857,14 @@ def get_found_page_and_id(self, expression, start_page): metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, metomi.rose.config_editor.ERROR_NOT_REGEX.format( - expression, str(exc)), - metomi.rose.config_editor.ERROR_BAD_FIND) + expression, str(exc) + ), + metomi.rose.config_editor.ERROR_BAD_FIND, + ) return None, None - if self.find_hist['regex'] != expression: - self.find_hist['ids'] = [] - self.find_hist['regex'] = expression + if self.find_hist["regex"] != expression: + self.find_hist["ids"] = [] + self.find_hist["regex"] = expression if start_page is None: ns_cmp = lambda x, y: 0 name_cmp = lambda x, y: 0 @@ -1597,13 +1878,18 @@ def get_found_page_and_id(self, expression, start_page): for config_name in config_keys: config_data = self.data.config[config_name] search_vars = config_data.vars.get_all( - skip_latent=not self.page_var_show_modes["latent"]) + skip_latent=not self.page_var_show_modes["latent"] + ) found_ns_vars = {} for variable in search_vars: - var_id = variable.metadata.get('id') - ns = variable.metadata.get('full_ns') - if (metomi.rose.META_PROP_TITLE in variable.metadata and - reg_find(variable.metadata[metomi.rose.META_PROP_TITLE])): + var_id = variable.metadata.get("id") + ns = variable.metadata.get("full_ns") + if ( + metomi.rose.META_PROP_TITLE in variable.metadata + and reg_find( + variable.metadata[metomi.rose.META_PROP_TITLE] + ) + ): found_ns_vars.setdefault(ns, []) found_ns_vars[ns].append(variable) continue @@ -1614,28 +1900,33 @@ def get_found_page_and_id(self, expression, start_page): ns_list.sort(key=cmp_to_key(ns_cmp)) for ns in ns_list: variables = found_ns_vars[ns] - variables.sort(key=lambda x: x.metadata['id']) + variables.sort(key=lambda x: x.metadata["id"]) for variable in variables: - var_id = variable.metadata['id'] - if (config_name, var_id) not in self.find_hist['ids']: - if (not self.page_var_show_modes['fixed'] and - len(variable.metadata.get('values', [])) == 1): + var_id = variable.metadata["id"] + if (config_name, var_id) not in self.find_hist["ids"]: + if ( + not self.page_var_show_modes["fixed"] + and len(variable.metadata.get("values", [])) == 1 + ): continue - if (not self.page_var_show_modes['ignored'] and - variable.ignored_reason): + if ( + not self.page_var_show_modes["ignored"] + and variable.ignored_reason + ): continue - self.find_hist['ids'].append((config_name, var_id)) + self.find_hist["ids"].append((config_name, var_id)) return ns, var_id - if self.find_hist['ids']: - config_name, var_id = self.find_hist['ids'][0] + if self.find_hist["ids"]: + config_name, var_id = self.find_hist["ids"][0] config_data = self.data.config[config_name] var = self.data.helper.get_variable_by_id(var_id, config_name) if var is None: - var = self.data.helper.get_variable_by_id(var_id, config_name, - latent=True) + var = self.data.helper.get_variable_by_id( + var_id, config_name, latent=True + ) if var is not None: - self.find_hist['ids'] = [self.find_hist['ids'][0]] - return var.metadata['full_ns'], var_id + self.find_hist["ids"] = [self.find_hist["ids"][0]] + return var.metadata["full_ns"], var_id return None, None def check_cannot_enable_setting(self, config_name, setting_id): @@ -1666,8 +1957,10 @@ def perform_undo(self, redo_mode_on=False): do_list = [stack[-1]] # We should undo/redo all same-grouped items together. for stack_item in reversed(stack[:-1]): - if (stack_item.group is None or - stack_item.group != do_list[0].group): + if ( + stack_item.group is None + or stack_item.group != do_list[0].group + ): break do_list.append(stack_item) group = do_list[0].group @@ -1682,7 +1975,7 @@ def perform_undo(self, redo_mode_on=False): node = stack_item.node node_id = None try: - node_id = node.metadata['id'] + node_id = node.metadata["id"] except (AttributeError, KeyError): pass # We need to handle namespace and metadata changes @@ -1693,20 +1986,23 @@ def perform_undo(self, redo_mode_on=False): else: # A variable or section opt = self.util.get_section_option_from_id(node_id)[1] - node_is_section = (opt is None) - namespace = node.metadata.get('full_ns') + node_is_section = opt is None + namespace = node.metadata.get("full_ns") if namespace is None: namespace = stack_item.page_label - config_name = self.util.split_full_ns( - self.data, namespace)[0] + config_name = self.util.split_full_ns(self.data, namespace)[0] node.process_metadata( - self.data.helper.get_metadata_for_config_id(node_id, - config_name)) + self.data.helper.get_metadata_for_config_id( + node_id, config_name + ) + ) self.data.load_ns_for_node(node, config_name) - namespace = node.metadata.get('full_ns') - if (not is_group and - self.nav_controller.is_ns_in_tree(namespace) and - not node_is_section): + namespace = node.metadata.get("full_ns") + if ( + not is_group + and self.nav_controller.is_ns_in_tree(namespace) + and not node_is_section + ): page = self.view_page(namespace, node_id) redo_items = [x for x in self.redo_stack] if stack_item.undo_args: @@ -1762,8 +2058,11 @@ def perform_undo(self, redo_mode_on=False): title = stack_item.name else: title = node_id - id_text = metomi.rose.config_editor.EVENT_UNDO_ACTION_ID.format( - action, title) + id_text = ( + metomi.rose.config_editor.EVENT_UNDO_ACTION_ID.format( + action, title + ) + ) self.reporter.report(event_text.format(id_text)) if is_group: group_name = do_list[0].group.split("-")[0] @@ -1778,48 +2077,63 @@ def perform_undo(self, redo_mode_on=False): self.performing_undo = False return True + # ----------------------- System functions ----------------------------------- -def spawn_window(config_directory_path=None, debug_mode=False, - load_all_apps=False, load_no_apps=False, metadata_off=False, - initial_namespaces=None, opt_meta_paths=None, - no_warn=None): +def spawn_window( + config_directory_path=None, + debug_mode=False, + load_all_apps=False, + load_no_apps=False, + metadata_off=False, + initial_namespaces=None, + opt_meta_paths=None, + no_warn=None, +): """Create a window and load the configuration into it. Run Gtk.""" if opt_meta_paths is None: opt_meta_paths = [] if not debug_mode: - warnings.filterwarnings('ignore') + warnings.filterwarnings("ignore") resourcer = metomi.rose.resource.ResourceLocator(paths=sys.path) metomi.rose.gtk.util.rc_setup( - str(resourcer.locate('etc/rose-config-edit/.gtkrc-2.0'))) + str(resourcer.locate("etc/rose-config-edit/.gtkrc-2.0")) + ) metomi.rose.gtk.util.setup_stock_icons() - logo = resourcer.locate('etc/images/rose-splash-logo.png') + logo = resourcer.locate("etc/images/rose-splash-logo.png") if metomi.rose.config_editor.ICON_PATH_SCHEDULER is None: gcontrol_icon = None else: try: gcontrol_icon = resourcer.locate( - metomi.rose.config_editor.ICON_PATH_SCHEDULER) + metomi.rose.config_editor.ICON_PATH_SCHEDULER + ) except metomi.rose.resource.ResourceError: gcontrol_icon = None metomi.rose.gtk.util.setup_scheduler_icon(gcontrol_icon) - number_of_events = (get_number_of_configs(config_directory_path) * - metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + 2) + number_of_events = ( + get_number_of_configs(config_directory_path) + * metomi.rose.config_editor.LOAD_NUMBER_OF_EVENTS + + 2 + ) if config_directory_path is None: title = metomi.rose.config_editor.UNTITLED_NAME else: title = config_directory_path.split("/")[-1] - splash_screen = metomi.rose.gtk.splash.SplashScreenProcess(logo, title, - number_of_events) + splash_screen = metomi.rose.gtk.splash.SplashScreenProcess( + logo, title, number_of_events + ) try: - ctrl = MainController(config_directory_path, - load_updater=splash_screen, - load_all_apps=load_all_apps, - load_no_apps=load_no_apps, - metadata_off=metadata_off, - opt_meta_paths=opt_meta_paths, - no_warn=no_warn) + ctrl = MainController( + config_directory_path, + load_updater=splash_screen, + load_all_apps=load_all_apps, + load_no_apps=load_no_apps, + metadata_off=metadata_off, + opt_meta_paths=opt_meta_paths, + no_warn=no_warn, + ) except BaseException: splash_screen.stop() raise @@ -1828,16 +2142,17 @@ def spawn_window(config_directory_path=None, debug_mode=False, if initial_namespaces: # if the namespace ends with a / remove it for i in range(len(initial_namespaces)): - if (len(initial_namespaces[i]) > 1 and - initial_namespaces[i][-1] == '/'): + if ( + len(initial_namespaces[i]) > 1 + and initial_namespaces[i][-1] == "/" + ): initial_namespaces[i] = initial_namespaces[i][0:-1] # for each partial namespace get the full namespace full_namespaces = [] for namespace in initial_namespaces: - exp = re.compile(r'(.*%s?[^\/]+)' % (re.escape(namespace),)) - for ns in sorted(sorted(ctrl.data.namespace_meta_lookup), - key=len): + exp = re.compile(r"(.*%s?[^\/]+)" % (re.escape(namespace),)) + for ns in sorted(sorted(ctrl.data.namespace_meta_lookup), key=len): match = exp.search(ns) if match: full_namespaces.append(match.groups()[0]) @@ -1846,22 +2161,24 @@ def spawn_window(config_directory_path=None, debug_mode=False, # open each namespace in a new tab for namespace in full_namespaces: # if the namespace begins with a / remove it - if namespace[0] == '/': + if namespace[0] == "/": namespace = namespace[1:] # open namespace try: ctrl.view_page(namespace) except Exception: - print('could not open ' + namespace, file=sys.stderr) + print("could not open " + namespace, file=sys.stderr) # expand namespace in nav_panel - path = ctrl.nav_panel.get_path_from_names(namespace.split('/')) + path = ctrl.nav_panel.get_path_from_names(namespace.split("/")) if path: ctrl.nav_panel.tree.expand_to_path(path) - Gtk.Settings.get_default().set_long_property("gtk-button-images", - True, "main") - Gtk.Settings.get_default().set_long_property("gtk-menu-images", - True, "main") + Gtk.Settings.get_default().set_long_property( + "gtk-button-images", True, "main" + ) + Gtk.Settings.get_default().set_long_property( + "gtk-menu-images", True, "main" + ) splash_screen.stop() Gtk.main() @@ -1869,12 +2186,15 @@ def spawn_window(config_directory_path=None, debug_mode=False, def spawn_subprocess_window(config_directory_path=None): """Launch a subprocess for a new config editor. Is it safe?""" if config_directory_path is None: - os.system(metomi.rose.config_editor.LAUNCH_COMMAND + ' --new &') + os.system(metomi.rose.config_editor.LAUNCH_COMMAND + " --new &") return elif not os.path.isdir(str(config_directory_path)): return - os.system(metomi.rose.config_editor.LAUNCH_COMMAND_CONFIG + - config_directory_path + " &") + os.system( + metomi.rose.config_editor.LAUNCH_COMMAND_CONFIG + + config_directory_path + + " &" + ) def get_number_of_configs(config_directory_path=None): @@ -1884,22 +2204,31 @@ def get_number_of_configs(config_directory_path=None): for listing in set(os.listdir(config_directory_path)): if listing in metomi.rose.CONFIG_NAMES: number_to_load += 1 - app_dir = os.path.join(config_directory_path, metomi.rose.SUB_CONFIGS_DIR) + app_dir = os.path.join( + config_directory_path, metomi.rose.SUB_CONFIGS_DIR + ) if os.path.exists(app_dir): for entry in os.listdir(app_dir): - if (os.path.isdir(os.path.join(app_dir, entry)) and - not entry.startswith('.')): + if os.path.isdir( + os.path.join(app_dir, entry) + ) and not entry.startswith("."): number_to_load += 1 return number_to_load def main(): """Launch from the command line.""" - sys.path.append(os.getenv('ROSE_HOME')) + sys.path.append(os.getenv("ROSE_HOME")) opt_parser = metomi.rose.opt_parse.RoseOptionParser() - opt_parser.add_my_options("conf_dir", "meta_path", "new_mode", - "load_no_apps", "load_all_apps", "no_metadata", - "no_warn") + opt_parser.add_my_options( + "conf_dir", + "meta_path", + "new_mode", + "load_no_apps", + "load_all_apps", + "no_metadata", + "no_warn", + ) opts, args = opt_parser.parse_args() metomi.rose.macro.add_meta_paths() opt_meta_paths = [] @@ -1908,8 +2237,9 @@ def main(): for path in meta_path.split(os.pathsep): opt_meta_paths.append( os.path.abspath( - os.path.expandvars( - os.path.expanduser(path)))) + os.path.expandvars(os.path.expanduser(path)) + ) + ) if opts.conf_dir: os.chdir(opts.conf_dir) path = os.getcwd() @@ -1932,34 +2262,42 @@ def main(): provider = Gtk.CssProvider() style_context = Gtk.StyleContext() style_context.add_provider_for_screen( - screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) locator = metomi.rose.resource.ResourceLocator(paths=sys.path) - css_path = locator.locate('etc/rose-config-edit/style.css') + css_path = locator.locate("etc/rose-config-edit/style.css") provider.load_from_path(str(css_path)) if opts.profile_mode: handle = tempfile.NamedTemporaryFile() - cProfile.runctx("""spawn_window(cwd, debug_mode=opts.debug_mode, + cProfile.runctx( + """spawn_window(cwd, debug_mode=opts.debug_mode, load_all_apps=opts.load_all_apps, load_no_apps=opts.load_no_apps, metadata_off=opts.no_metadata, initial_namespaces=args, opt_meta_paths=opt_meta_paths, no_warn=opts.no_warn) - """, globals(), locals(), handle.name) + """, + globals(), + locals(), + handle.name, + ) pstat = pstats.Stats(handle.name) pstat.strip_dirs().sort_stats("cumulative").print_stats() handle.close() else: - spawn_window(cwd, debug_mode=opts.debug_mode, - load_all_apps=opts.load_all_apps, - load_no_apps=opts.load_no_apps, - metadata_off=opts.no_metadata, - initial_namespaces=args, - opt_meta_paths=opt_meta_paths, - no_warn=opts.no_warn) + spawn_window( + cwd, + debug_mode=opts.debug_mode, + load_all_apps=opts.load_all_apps, + load_no_apps=opts.load_no_apps, + metadata_off=opts.no_metadata, + initial_namespaces=args, + opt_meta_paths=opt_meta_paths, + no_warn=opts.no_warn, + ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/metomi/rose/config_editor/menu.py b/metomi/rose/config_editor/menu.py index 1b1abc261..a19496c46 100644 --- a/metomi/rose/config_editor/menu.py +++ b/metomi/rose/config_editor/menu.py @@ -22,12 +22,12 @@ import inspect import os import shlex -import subprocess import sys import traceback import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -35,7 +35,6 @@ import metomi.rose.config_editor.upgrade_controller import metomi.rose.external import metomi.rose.gtk.dialog -import metomi.rose.gtk.run import metomi.rose.macro import metomi.rose.macros import metomi.rose.popen @@ -44,7 +43,6 @@ class MenuBar(object): - """Generate the menu bar, using the GTK UIManager. Parses the settings in 'ui_config_string'. Connection of buttons is done @@ -129,123 +127,247 @@ class MenuBar(object): """ action_details = [ - ('File', None, - metomi.rose.config_editor.TOP_MENU_FILE), - ('Open...', Gtk.STOCK_OPEN, - metomi.rose.config_editor.TOP_MENU_FILE_OPEN, - metomi.rose.config_editor.ACCEL_OPEN), - ('Save', Gtk.STOCK_SAVE, - metomi.rose.config_editor.TOP_MENU_FILE_SAVE, - metomi.rose.config_editor.ACCEL_SAVE), - ('Check and save', Gtk.STOCK_SPELL_CHECK, - metomi.rose.config_editor.TOP_MENU_FILE_CHECK_AND_SAVE), - ('Load All Apps', Gtk.STOCK_CDROM, - metomi.rose.config_editor.TOP_MENU_FILE_LOAD_APPS), - ('Quit', Gtk.STOCK_QUIT, - metomi.rose.config_editor.TOP_MENU_FILE_QUIT, - metomi.rose.config_editor.ACCEL_QUIT), - ('Edit', None, - metomi.rose.config_editor.TOP_MENU_EDIT), - ('Undo', Gtk.STOCK_UNDO, - metomi.rose.config_editor.TOP_MENU_EDIT_UNDO, - metomi.rose.config_editor.ACCEL_UNDO), - ('Redo', Gtk.STOCK_REDO, - metomi.rose.config_editor.TOP_MENU_EDIT_REDO, - metomi.rose.config_editor.ACCEL_REDO), - ('Stack', Gtk.STOCK_INFO, - metomi.rose.config_editor.TOP_MENU_EDIT_STACK), - ('Find', Gtk.STOCK_FIND, - metomi.rose.config_editor.TOP_MENU_EDIT_FIND, - metomi.rose.config_editor.ACCEL_FIND), - ('Find Next', Gtk.STOCK_FIND, - metomi.rose.config_editor.TOP_MENU_EDIT_FIND_NEXT, - metomi.rose.config_editor.ACCEL_FIND_NEXT), - ('Preferences', Gtk.STOCK_PREFERENCES, - metomi.rose.config_editor.TOP_MENU_EDIT_PREFERENCES), - ('View', None, - metomi.rose.config_editor.TOP_MENU_VIEW), - ('Page', None, - metomi.rose.config_editor.TOP_MENU_PAGE), - ('Add variable', Gtk.STOCK_ADD, - metomi.rose.config_editor.TOP_MENU_PAGE_ADD), - ('Revert', Gtk.STOCK_REVERT_TO_SAVED, - metomi.rose.config_editor.TOP_MENU_PAGE_REVERT), - ('Page Info', Gtk.STOCK_INFO, - metomi.rose.config_editor.TOP_MENU_PAGE_INFO), - ('Page Help', Gtk.STOCK_HELP, - metomi.rose.config_editor.TOP_MENU_PAGE_HELP), - ('Page Web Help', Gtk.STOCK_HOME, - metomi.rose.config_editor.TOP_MENU_PAGE_WEB_HELP), - ('Metadata', None, - metomi.rose.config_editor.TOP_MENU_METADATA), - ('Reload metadata', Gtk.STOCK_REFRESH, - metomi.rose.config_editor.TOP_MENU_METADATA_REFRESH, - metomi.rose.config_editor.ACCEL_METADATA_REFRESH), - ('Load custom metadata', Gtk.STOCK_DIRECTORY, - metomi.rose.config_editor.TOP_MENU_METADATA_LOAD), - ('Prefs', Gtk.STOCK_PREFERENCES, - metomi.rose.config_editor.TOP_MENU_METADATA_PREFERENCES), - ('Upgrade', Gtk.STOCK_GO_UP, - metomi.rose.config_editor.TOP_MENU_METADATA_UPGRADE), - ('All V', "dialog-question", - metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V), - ('Autofix', Gtk.STOCK_CONVERT, - metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX), - ('Extra checks', "dialog-question", - metomi.rose.config_editor.TOP_MENU_METADATA_CHECK), - ('Graph', Gtk.STOCK_SORT_ASCENDING, - metomi.rose.config_editor.TOP_MENU_METADATA_GRAPH), - ('Tools', None, - metomi.rose.config_editor.TOP_MENU_TOOLS), - ('Browser', Gtk.STOCK_DIRECTORY, - metomi.rose.config_editor.TOP_MENU_TOOLS_BROWSER, - metomi.rose.config_editor.ACCEL_BROWSER), - ('Terminal', Gtk.STOCK_EXECUTE, - metomi.rose.config_editor.TOP_MENU_TOOLS_TERMINAL, - metomi.rose.config_editor.ACCEL_TERMINAL), - ('Help', None, - metomi.rose.config_editor.TOP_MENU_HELP), - ('Documentation', Gtk.STOCK_HELP, - metomi.rose.config_editor.TOP_MENU_HELP_GUI, - metomi.rose.config_editor.ACCEL_HELP_GUI), - ('About', Gtk.STOCK_DIALOG_INFO, - metomi.rose.config_editor.TOP_MENU_HELP_ABOUT)] + ("File", None, metomi.rose.config_editor.TOP_MENU_FILE), + ( + "Open...", + Gtk.STOCK_OPEN, + metomi.rose.config_editor.TOP_MENU_FILE_OPEN, + metomi.rose.config_editor.ACCEL_OPEN, + ), + ( + "Save", + Gtk.STOCK_SAVE, + metomi.rose.config_editor.TOP_MENU_FILE_SAVE, + metomi.rose.config_editor.ACCEL_SAVE, + ), + ( + "Check and save", + Gtk.STOCK_SPELL_CHECK, + metomi.rose.config_editor.TOP_MENU_FILE_CHECK_AND_SAVE, + ), + ( + "Load All Apps", + Gtk.STOCK_CDROM, + metomi.rose.config_editor.TOP_MENU_FILE_LOAD_APPS, + ), + ( + "Quit", + Gtk.STOCK_QUIT, + metomi.rose.config_editor.TOP_MENU_FILE_QUIT, + metomi.rose.config_editor.ACCEL_QUIT, + ), + ("Edit", None, metomi.rose.config_editor.TOP_MENU_EDIT), + ( + "Undo", + Gtk.STOCK_UNDO, + metomi.rose.config_editor.TOP_MENU_EDIT_UNDO, + metomi.rose.config_editor.ACCEL_UNDO, + ), + ( + "Redo", + Gtk.STOCK_REDO, + metomi.rose.config_editor.TOP_MENU_EDIT_REDO, + metomi.rose.config_editor.ACCEL_REDO, + ), + ( + "Stack", + Gtk.STOCK_INFO, + metomi.rose.config_editor.TOP_MENU_EDIT_STACK, + ), + ( + "Find", + Gtk.STOCK_FIND, + metomi.rose.config_editor.TOP_MENU_EDIT_FIND, + metomi.rose.config_editor.ACCEL_FIND, + ), + ( + "Find Next", + Gtk.STOCK_FIND, + metomi.rose.config_editor.TOP_MENU_EDIT_FIND_NEXT, + metomi.rose.config_editor.ACCEL_FIND_NEXT, + ), + ( + "Preferences", + Gtk.STOCK_PREFERENCES, + metomi.rose.config_editor.TOP_MENU_EDIT_PREFERENCES, + ), + ("View", None, metomi.rose.config_editor.TOP_MENU_VIEW), + ("Page", None, metomi.rose.config_editor.TOP_MENU_PAGE), + ( + "Add variable", + Gtk.STOCK_ADD, + metomi.rose.config_editor.TOP_MENU_PAGE_ADD, + ), + ( + "Revert", + Gtk.STOCK_REVERT_TO_SAVED, + metomi.rose.config_editor.TOP_MENU_PAGE_REVERT, + ), + ( + "Page Info", + Gtk.STOCK_INFO, + metomi.rose.config_editor.TOP_MENU_PAGE_INFO, + ), + ( + "Page Help", + Gtk.STOCK_HELP, + metomi.rose.config_editor.TOP_MENU_PAGE_HELP, + ), + ( + "Page Web Help", + Gtk.STOCK_HOME, + metomi.rose.config_editor.TOP_MENU_PAGE_WEB_HELP, + ), + ("Metadata", None, metomi.rose.config_editor.TOP_MENU_METADATA), + ( + "Reload metadata", + Gtk.STOCK_REFRESH, + metomi.rose.config_editor.TOP_MENU_METADATA_REFRESH, + metomi.rose.config_editor.ACCEL_METADATA_REFRESH, + ), + ( + "Load custom metadata", + Gtk.STOCK_DIRECTORY, + metomi.rose.config_editor.TOP_MENU_METADATA_LOAD, + ), + ( + "Prefs", + Gtk.STOCK_PREFERENCES, + metomi.rose.config_editor.TOP_MENU_METADATA_PREFERENCES, + ), + ( + "Upgrade", + Gtk.STOCK_GO_UP, + metomi.rose.config_editor.TOP_MENU_METADATA_UPGRADE, + ), + ( + "All V", + "dialog-question", + metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_ALL_V, + ), + ( + "Autofix", + Gtk.STOCK_CONVERT, + metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_AUTOFIX, + ), + ( + "Extra checks", + "dialog-question", + metomi.rose.config_editor.TOP_MENU_METADATA_CHECK, + ), + ( + "Graph", + Gtk.STOCK_SORT_ASCENDING, + metomi.rose.config_editor.TOP_MENU_METADATA_GRAPH, + ), + ("Tools", None, metomi.rose.config_editor.TOP_MENU_TOOLS), + ( + "Browser", + Gtk.STOCK_DIRECTORY, + metomi.rose.config_editor.TOP_MENU_TOOLS_BROWSER, + metomi.rose.config_editor.ACCEL_BROWSER, + ), + ( + "Terminal", + Gtk.STOCK_EXECUTE, + metomi.rose.config_editor.TOP_MENU_TOOLS_TERMINAL, + metomi.rose.config_editor.ACCEL_TERMINAL, + ), + ("Help", None, metomi.rose.config_editor.TOP_MENU_HELP), + ( + "Documentation", + Gtk.STOCK_HELP, + metomi.rose.config_editor.TOP_MENU_HELP_GUI, + metomi.rose.config_editor.ACCEL_HELP_GUI, + ), + ( + "About", + Gtk.STOCK_DIALOG_INFO, + metomi.rose.config_editor.TOP_MENU_HELP_ABOUT, + ), + ] toggle_action_details = [ - ('View latent vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_VARS), - ('View fixed vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_FIXED_VARS), - ('View ignored vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_VARS), - ('View user-ignored vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_VARS), - ('View without descriptions', None, - metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS), - ('View without help', None, - metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_HELP), - ('View without titles', None, - metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_TITLES), - ('View ignored pages', None, - metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_PAGES), - ('View user-ignored pages', None, - metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_PAGES), - ('View latent pages', None, - metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_PAGES), - ('Flag opt config vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPT_CONF_VARS), - ('Flag optional vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPTIONAL_VARS), - ('Flag no-metadata vars', None, - metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_NO_METADATA_VARS), - ('View status bar', None, - metomi.rose.config_editor.TOP_MENU_VIEW_STATUS_BAR), - ('Switch off metadata', None, - metomi.rose.config_editor.TOP_MENU_METADATA_SWITCH_OFF)] + ( + "View latent vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_VARS, + ), + ( + "View fixed vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_FIXED_VARS, + ), + ( + "View ignored vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_VARS, + ), + ( + "View user-ignored vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_VARS, + ), + ( + "View without descriptions", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_DESCRIPTIONS, + ), + ( + "View without help", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_HELP, + ), + ( + "View without titles", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_WITHOUT_TITLES, + ), + ( + "View ignored pages", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_IGNORED_PAGES, + ), + ( + "View user-ignored pages", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_USER_IGNORED_PAGES, + ), + ( + "View latent pages", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_LATENT_PAGES, + ), + ( + "Flag opt config vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPT_CONF_VARS, + ), + ( + "Flag optional vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_OPTIONAL_VARS, + ), + ( + "Flag no-metadata vars", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_FLAG_NO_METADATA_VARS, + ), + ( + "View status bar", + None, + metomi.rose.config_editor.TOP_MENU_VIEW_STATUS_BAR, + ), + ( + "Switch off metadata", + None, + metomi.rose.config_editor.TOP_MENU_METADATA_SWITCH_OFF, + ), + ] def __init__(self): self.uimanager = Gtk.UIManager() - self.actiongroup = Gtk.ActionGroup('MenuBar') + self.actiongroup = Gtk.ActionGroup("MenuBar") self.actiongroup.add_actions(self.action_details) self.actiongroup.add_toggle_actions(self.toggle_action_details) self.uimanager.insert_action_group(self.actiongroup) @@ -260,9 +382,11 @@ def set_accelerators(self, accel_dict): key, mod = Gtk.accelerator_parse(key_press) self.accelerators.lookup[str(key) + str(mod)] = accel_func self.accelerators.connect( - key, mod, + key, + mod, Gtk.AccelFlags.VISIBLE, - lambda a, c, k, m: self.accelerators.lookup[str(k) + str(m)]()) + lambda a, c, k, m: self.accelerators.lookup[str(k) + str(m)](), + ) def clear_macros(self): """Reset menu to original configuration and clear macros.""" @@ -272,30 +396,43 @@ def clear_macros(self): all_v_item = self.uimanager.get_widget("/TopMenuBar/Metadata/All V") all_v_item.set_sensitive(False) - def add_macro(self, config_name, modulename, classname, methodname, - help_, image_path, run_macro): + def add_macro( + self, + config_name, + modulename, + classname, + methodname, + help_, + image_path, + run_macro, + ): """Add a macro to the macro menu.""" - macro_address = '/TopMenuBar/Metadata' + macro_address = "/TopMenuBar/Metadata" self.uimanager.get_widget(macro_address).get_submenu() if methodname == metomi.rose.macro.VALIDATE_METHOD: all_v_item = self.uimanager.get_widget(macro_address + "/All V") all_v_item.set_sensitive(True) - config_menu_name = config_name.replace('/', ':').replace('_', '__') - config_label_name = config_name.split('/')[-1].replace('_', '__') - label = metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_CONFIG.format( - config_label_name) - config_address = macro_address + '/' + config_menu_name + config_menu_name = config_name.replace("/", ":").replace("_", "__") + config_label_name = config_name.split("/")[-1].replace("_", "__") + label = ( + metomi.rose.config_editor.TOP_MENU_METADATA_MACRO_CONFIG.format( + config_label_name + ) + ) + config_address = macro_address + "/" + config_menu_name config_item = self.uimanager.get_widget(config_address) if config_item is None: actiongroup = self.uimanager.get_action_groups()[0] if actiongroup.get_action(config_menu_name) is None: - actiongroup.add_action(Gtk.Action(config_menu_name, - label, - None, None)) + actiongroup.add_action( + Gtk.Action(config_menu_name, label, None, None) + ) new_ui = """ - """.format(config_menu_name) + """.format( + config_menu_name + ) self.macro_ids.append(self.uimanager.add_ui_from_string(new_ui)) config_item = self.uimanager.get_widget(config_address) if image_path is not None: @@ -309,8 +446,12 @@ def add_macro(self, config_name, modulename, classname, methodname, stock_id = "dialog-question" else: stock_id = Gtk.STOCK_CONVERT - macro_item_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - macro_item_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) + macro_item_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=6 + ) + macro_item_icon = Gtk.Image.new_from_icon_name( + stock_id, Gtk.IconSize.MENU + ) macro_item_label = Gtk.Label(label=macro_fullname) macro_item = Gtk.MenuItem() Gtk.Container.add(macro_item_box, macro_item_icon) @@ -320,42 +461,56 @@ def add_macro(self, config_name, modulename, classname, methodname, context = Gtk.Widget.get_style_context(macro_item) Gtk.StyleContext.add_class(context, "macro-item") macro_item.show_all() - macro_item._run_data = [config_name, modulename, classname, - methodname] - macro_item.connect("activate", - lambda i: run_macro(*i._run_data)) + macro_item._run_data = [config_name, modulename, classname, methodname] + macro_item.connect("activate", lambda i: run_macro(*i._run_data)) config_item.get_submenu().append(macro_item) - if (methodname == metomi.rose.macro.VALIDATE_METHOD): + if methodname == metomi.rose.macro.VALIDATE_METHOD: for item in config_item.get_submenu().get_children(): if hasattr(item, "_rose_all_validators"): return False - all_item_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - all_item_icon = Gtk.Image.new_from_icon_name("dialog-question", Gtk.IconSize.MENU) - all_item_label = Gtk.Label(label=metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS) + all_item_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=6 + ) + all_item_icon = Gtk.Image.new_from_icon_name( + "dialog-question", Gtk.IconSize.MENU + ) + all_item_label = Gtk.Label( + label=metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS + ) all_item = Gtk.MenuItem() Gtk.Container.add(all_item_box, all_item_icon) Gtk.Container.add(all_item_box, all_item_label) Gtk.Container.add(all_item, all_item_box) all_item._rose_all_validators = True all_item.set_tooltip_text( - metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP) + metomi.rose.config_editor.MACRO_MENU_ALL_VALIDATORS_TIP + ) all_item.show_all() all_item._run_data = [config_name, None, None, methodname] - all_item.connect("activate", - lambda i: run_macro(*i._run_data)) + all_item.connect("activate", lambda i: run_macro(*i._run_data)) config_item.get_submenu().prepend(all_item) class MainMenuHandler(object): - """Handles signals from the main menu and tool bar.""" - def __init__(self, data, util, reporter, mainwindow, - undo_stack, redo_stack, undo_func, - update_config_func, - apply_macro_transform_func, apply_macro_validation_func, - group_ops_inst, section_ops_inst, variable_ops_inst, - find_ns_id_func): + def __init__( + self, + data, + util, + reporter, + mainwindow, + undo_stack, + redo_stack, + undo_func, + update_config_func, + apply_macro_transform_func, + apply_macro_validation_func, + group_ops_inst, + section_ops_inst, + variable_ops_inst, + find_ns_id_func, + ): self.data = data self.util = util self.reporter = reporter @@ -371,7 +526,8 @@ def __init__(self, data, util, reporter, mainwindow, self.var_ops = variable_ops_inst self.find_ns_id_func = find_ns_id_func self.bad_colour = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR + ) def about_dialog(self, args): self.mainwindow.launch_about_dialog() @@ -385,8 +541,9 @@ def get_orphan_container(self, page): def view_stack(self, args): """Handle a View Stack request.""" - self.mainwindow.launch_view_stack(self.undo_stack, self.redo_stack, - self.perform_undo) + self.mainwindow.launch_view_stack( + self.undo_stack, self.redo_stack, self.perform_undo + ) def destroy(self, *args): """Handle a destroy main program request.""" @@ -407,11 +564,15 @@ def check_all_extra(self): self.update_config(config_name) num_errors = self.check_fail_rules(configs_updated=True) num_errors += self.run_custom_macro( - method_name=metomi.rose.macro.VALIDATE_METHOD, - configs_updated=True) + method_name=metomi.rose.macro.VALIDATE_METHOD, configs_updated=True + ) if num_errors: - text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL.format( - num_errors) + text = ( + metomi.rose + .config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL.format( + num_errors + ) + ) kind = self.reporter.KIND_ERR else: text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_CHECK_ALL_OK @@ -442,21 +603,35 @@ def check_fail_rules(self, configs_updated=False): metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, str(exc), metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( - macro_fullname)) + macro_fullname + ), + ) continue sorter = metomi.rose.config.sort_settings to_id = lambda s: self.util.get_id_from_section_option( - s.section, s.option) - return_value.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) - self.handle_macro_validation(config_name, macro_fullname, - config, return_value, - no_display=(not return_value)) + s.section, s.option + ) + return_value.sort( + key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y))) + ) + self.handle_macro_validation( + config_name, + macro_fullname, + config, + return_value, + no_display=(not return_value), + ) if error_count > 0: - msg = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND + msg = ( + metomi.rose.config_editor + .EVENT_MACRO_VALIDATE_RULE_PROBLEMS_FOUND + ) info_text = msg.format(error_count) kind = self.reporter.KIND_ERR else: - msg = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS + msg = ( + metomi.rose.config_editor.EVENT_MACRO_VALIDATE_RULE_NO_PROBLEMS + ) info_text = msg kind = self.reporter.KIND_OUT self.reporter.report(info_text, kind=kind) @@ -487,9 +662,15 @@ def load_macro_menu(self, menubar): macro_tuples = metomi.rose.macro.get_macro_class_methods(macros) macro_tuples.sort(key=lambda x: x[0]) for macro_mod, macro_cls, macro_func, help_ in macro_tuples: - menubar.add_macro(config_name, macro_mod, macro_cls, - macro_func, help_, image, - self.handle_run_custom_macro) + menubar.add_macro( + config_name, + macro_mod, + macro_cls, + macro_func, + help_, + image, + self.handle_run_custom_macro, + ) def inspect_custom_macro(self, macro_meth): """Inspect a custom macro for kwargs and return any""" @@ -510,10 +691,15 @@ def handle_graph(self): config_sect_dict = {} for config_name in self.data.config: config_data = self.data.config[config_name] - config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + config_sect_dict[config_name] = list( + config_data.sections.now.keys() + ) + config_sect_dict[config_name].sort( + key=cmp_to_key(metomi.rose.config.sort_settings) + ) config_name, section = self.mainwindow.launch_graph_dialog( - config_sect_dict) + config_sect_dict + ) if config_name is None: return False if section is None: @@ -522,8 +708,9 @@ def handle_graph(self): allowed_sections = [section] self.launch_graph(config_name, allowed_sections=allowed_sections) - def check_entry_value(self, entry_widget, dialog, entries, - labels, optionals): + def check_entry_value( + self, entry_widget, dialog, entries, labels, optionals + ): is_valid = True for k, entry in list(entries.items()): this_is_valid = True @@ -564,8 +751,9 @@ def override_macro_defaults(self, optionals, methname): Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, - None) - dialog.set_markup('Specify overrides for macro arguments:') + None, + ) + dialog.set_markup("Specify overrides for macro arguments:") dialog.set_title(methname) table = Gtk.Table(len(list(optionals.items())), 2, False) dialog.vbox.add(table) @@ -577,10 +765,17 @@ def override_macro_defaults(self, optionals, methname): entry.set_text("'" + value + "'") else: entry.set_text(str(value)) - entry.connect("changed", self.check_entry_value, dialog, - entries, labels, optionals) - entry.connect("activate", self.handle_macro_entry_activate, - dialog, entries) + entry.connect( + "changed", + self.check_entry_value, + dialog, + entries, + labels, + optionals, + ) + entry.connect( + "activate", self.handle_macro_entry_activate, dialog, entries + ) entries[key] = entry labels[key] = label table.attach(entry, 1, 2, i, i + 1) @@ -589,7 +784,10 @@ def override_macro_defaults(self, optionals, methname): table.attach(hbox, 0, 1, i, i + 1) dialog.show_all() response = dialog.run() - if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.CLOSE: + if ( + response == Gtk.ResponseType.CANCEL + or response == Gtk.ResponseType.CLOSE + ): res = optionals else: res = {} @@ -603,9 +801,14 @@ def handle_run_custom_macro(self, *args, **kwargs): self.run_custom_macro(*args, **kwargs) return False - def run_custom_macro(self, config_name=None, module_name=None, - class_name=None, method_name=None, - configs_updated=False): + def run_custom_macro( + self, + config_name=None, + module_name=None, + class_name=None, + method_name=None, + configs_updated=False, + ): """Run the custom macro method and launch a dialog.""" old_pwd = os.getcwd() macro_data = [] @@ -620,13 +823,16 @@ def run_custom_macro(self, config_name=None, module_name=None, if not configs_updated: self.update_config(name) if method_name is None: - method_names = [metomi.rose.macro.VALIDATE_METHOD, - metomi.rose.macro.TRANSFORM_METHOD] + method_names = [ + metomi.rose.macro.VALIDATE_METHOD, + metomi.rose.macro.TRANSFORM_METHOD, + ] else: method_names = [method_name] if module_name is not None and config_name is not None: - config_mod_prefix = ( - self.data.helper.get_macro_module_prefix(config_name)) + config_mod_prefix = self.data.helper.get_macro_module_prefix( + config_name + ) if not module_name.startswith(config_mod_prefix): module_name = config_mod_prefix + module_name for config_name in configs: @@ -638,39 +844,53 @@ def run_custom_macro(self, config_name=None, module_name=None, continue for obj_name, obj in inspect.getmembers(module): for method_name in method_names: - if (not hasattr(obj, method_name) or - obj_name.startswith("_") or - not issubclass(obj, metomi.rose.macro.MacroBase)): + if ( + not hasattr(obj, method_name) + or obj_name.startswith("_") + or not issubclass(obj, metomi.rose.macro.MacroBase) + ): continue if class_name is not None and obj_name != class_name: continue - macro_fullname = ".".join([module.__name__, - obj_name, - method_name]) + macro_fullname = ".".join( + [module.__name__, obj_name, method_name] + ) err_text = ( - metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( - macro_fullname)) + metomi.rose.config_editor + .ERROR_RUN_MACRO_TITLE.format( + macro_fullname + ) + ) try: macro_inst = obj() except Exception as exc: metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - str(exc), err_text) + str(exc), + err_text, + ) continue if hasattr(macro_inst, method_name): - macro_data.append((config_name, macro_inst, - module.__name__, obj_name, - method_name)) + macro_data.append( + ( + config_name, + macro_inst, + module.__name__, + obj_name, + method_name, + ) + ) os.chdir(old_pwd) if not macro_data: return 0 sorter = metomi.rose.config.sort_settings - to_id = lambda s: self.util.get_id_from_section_option(s.section, - s.option) + to_id = lambda s: self.util.get_id_from_section_option( + s.section, s.option + ) config_macro_errors = [] config_macro_changes = [] for config_name, macro_inst, modname, objname, methname in macro_data: - macro_fullname = '.'.join([modname, objname, methname]) + macro_fullname = ".".join([modname, objname, methname]) macro_config = self.data.dump_to_internal_config(config_name) config_data = self.data.config[config_name] meta_config = config_data.meta @@ -686,73 +906,95 @@ def run_custom_macro(self, config_name=None, module_name=None, except Exception: metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - 'Error in custom macro:\n\n%s' % ( - traceback.format_exc()), + "Error in custom macro:\n\n%s" % (traceback.format_exc()), metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( - macro_fullname)) + macro_fullname + ), + ) continue if methname == metomi.rose.macro.TRANSFORM_METHOD: - if (not isinstance(return_value, tuple) or - len(return_value) != 2 or - not isinstance( - return_value[0], metomi.rose.config.ConfigNode) or - not isinstance(return_value[1], list)): + if ( + not isinstance(return_value, tuple) + or len(return_value) != 2 + or not isinstance( + return_value[0], metomi.rose.config.ConfigNode + ) + or not isinstance(return_value[1], list) + ): self._handle_bad_macro_return(macro_fullname, return_value) continue integrity_exception = metomi.rose.macro.check_config_integrity( - return_value[0]) + return_value[0] + ) if integrity_exception is not None: - self._handle_bad_macro_return(macro_fullname, - integrity_exception) + self._handle_bad_macro_return( + macro_fullname, integrity_exception + ) continue macro_config, change_list = return_value if not change_list: continue - change_list.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) + change_list.sort( + key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y))) + ) num_changes = len(change_list) - self.handle_macro_transforms(config_name, macro_fullname, - macro_config, change_list) - config_macro_changes.append((config_name, - macro_fullname, - num_changes)) + self.handle_macro_transforms( + config_name, macro_fullname, macro_config, change_list + ) + config_macro_changes.append( + (config_name, macro_fullname, num_changes) + ) continue elif methname == metomi.rose.macro.VALIDATE_METHOD: if not isinstance(return_value, list): - self._handle_bad_macro_return(macro_fullname, - return_value) + self._handle_bad_macro_return(macro_fullname, return_value) continue if return_value: - return_value.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) - config_macro_errors.append((config_name, - macro_fullname, - len(return_value))) - self.handle_macro_validation(config_name, macro_fullname, - macro_config, return_value) + return_value.sort( + key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y))) + ) + config_macro_errors.append( + (config_name, macro_fullname, len(return_value)) + ) + self.handle_macro_validation( + config_name, macro_fullname, macro_config, return_value + ) os.chdir(old_pwd) if class_name is None: # Construct a grouped report. config_macro_errors.sort() config_macro_changes.sort() if metomi.rose.macro.VALIDATE_METHOD in method_names: - null_format = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL_OK - change_format = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL + null_format = ( + metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL_OK + ) + change_format = ( + metomi.rose.config_editor.EVENT_MACRO_VALIDATE_ALL + ) num_issues = sum([e[2] for e in config_macro_errors]) issue_confs = [e[0] for e in config_macro_errors if e[2]] else: - null_format = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL_OK - change_format = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL + null_format = ( + metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL_OK + ) + change_format = ( + metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_ALL + ) num_issues = sum([e[2] for e in config_macro_changes]) issue_confs = [e[0] for e in config_macro_changes if e[2]] issue_confs = sorted(set(issue_confs)) if num_issues: issue_conf_text = self._format_macro_config_names(issue_confs) - self.reporter.report(change_format.format(issue_conf_text, - num_issues), - kind=self.reporter.KIND_ERR) + self.reporter.report( + change_format.format(issue_conf_text, num_issues), + kind=self.reporter.KIND_ERR, + ) else: all_conf_text = self._format_macro_config_names(configs) - self.reporter.report(null_format.format(all_conf_text), - kind=self.reporter.KIND_OUT) + self.reporter.report( + null_format.format(all_conf_text), + kind=self.reporter.KIND_OUT, + ) num_errors = sum([e[2] for e in config_macro_errors]) num_changes = sum([c[2] for c in config_macro_changes]) return num_errors + num_changes @@ -760,27 +1002,37 @@ def run_custom_macro(self, config_name=None, module_name=None, def _format_macro_config_names(self, config_names): if len(config_names) > 5: return metomi.rose.config_editor.EVENT_MACRO_CONFIGS.format( - len(config_names)) + len(config_names) + ) config_names = [c.lstrip("/") for c in config_names] return ", ".join(config_names) def _handle_bad_macro_return(self, macro_fullname, info): if isinstance(info, Exception): text = metomi.rose.config_editor.ERROR_BAD_MACRO_EXCEPTION.format( - type(info).__name__, str(info)) + type(info).__name__, str(info) + ) else: - text = metomi.rose.config_editor.ERROR_BAD_MACRO_RETURN.format(info) + text = metomi.rose.config_editor.ERROR_BAD_MACRO_RETURN.format( + info + ) summary = metomi.rose.config_editor.ERROR_RUN_MACRO_TITLE.format( - macro_fullname) - self.reporter.report(summary, - kind=self.reporter.KIND_ERR) + macro_fullname + ) + self.reporter.report(summary, kind=self.reporter.KIND_ERR) metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, summary) + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, summary + ) - def handle_macro_transforms(self, config_name, macro_name, - macro_config, change_list, no_display=False, - triggers_ok=False): + def handle_macro_transforms( + self, + config_name, + macro_name, + macro_config, + change_list, + no_display=False, + triggers_ok=False, + ): """Calculate needed changes and apply them if prompted to. At the moment trigger-ignore of variables and sections is @@ -802,60 +1054,82 @@ def handle_macro_transforms(self, config_name, macro_name, search = lambda i: self.find_ns_id_func(config_name, i) if not no_display: proceed_ok = self.mainwindow.launch_macro_changes_dialog( - config_name, macro_type, change_list, search_func=search) + config_name, macro_type, change_list, search_func=search + ) if not proceed_ok: self._report_macro_transform(config_name, macro_name, 0) return 0 config_diff = macro_config - self.data.config[config_name].config - changed_ids = self.group_ops.apply_diff(config_name, config_diff, - origin_name=macro_type, - triggers_ok=triggers_ok) - self.apply_macro_transform( - config_name, changed_ids, skip_update=True) + changed_ids = self.group_ops.apply_diff( + config_name, + config_diff, + origin_name=macro_type, + triggers_ok=triggers_ok, + ) + self.apply_macro_transform(config_name, changed_ids, skip_update=True) self._report_macro_transform(config_name, macro_name, len(change_list)) return len(change_list) def _report_macro_transform(self, config_name, macro_name, num_changes): name = config_name.lstrip("/") if macro_name.endswith(metomi.rose.macro.TRANSFORM_METHOD): - macro = macro_name.split('.')[-2] + macro = macro_name.split(".")[-2] else: - macro = macro_name.split('.')[-1] + macro = macro_name.split(".")[-1] kind = self.reporter.KIND_OUT if num_changes: info_text = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM.format( - name, macro, num_changes) + name, macro, num_changes + ) else: - info_text = metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_OK.format( - name, macro) + info_text = ( + metomi.rose.config_editor.EVENT_MACRO_TRANSFORM_OK.format( + name, macro + ) + ) self.reporter.report(info_text, kind=kind) - def handle_macro_validation(self, config_name, macro_name, - macro_config, problem_list, no_display=False): + def handle_macro_validation( + self, + config_name, + macro_name, + macro_config, + problem_list, + no_display=False, + ): """Apply errors and give information to the user.""" macro_type = ".".join(macro_name.split(".")[:-1]) self.apply_macro_validation(config_name, macro_type, problem_list) search = lambda i: self.find_ns_id_func(config_name, i) - self._report_macro_validation(config_name, macro_name, - len(problem_list)) + self._report_macro_validation( + config_name, macro_name, len(problem_list) + ) if not no_display: self.mainwindow.launch_macro_changes_dialog( - config_name, macro_type, problem_list, - mode="validate", search_func=search) + config_name, + macro_type, + problem_list, + mode="validate", + search_func=search, + ) def _report_macro_validation(self, config_name, macro_name, num_errors): name = config_name.lstrip("/") if macro_name.endswith(metomi.rose.macro.VALIDATE_METHOD): - macro = macro_name.split('.')[-2] + macro = macro_name.split(".")[-2] else: - macro = macro_name.split('.')[-1] + macro = macro_name.split(".")[-1] if num_errors: info_text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE.format( - name, macro, num_errors) + name, macro, num_errors + ) kind = self.reporter.KIND_ERR else: - info_text = metomi.rose.config_editor.EVENT_MACRO_VALIDATE_OK.format( - name, macro) + info_text = ( + metomi.rose.config_editor.EVENT_MACRO_VALIDATE_OK.format( + name, macro + ) + ) kind = self.reporter.KIND_OUT self.reporter.report(info_text, kind=kind) @@ -867,16 +1141,20 @@ def handle_upgrade(self, only_this_config_name=None): if config_data.is_preview: continue self.update_config(config_name) - if (only_this_config_name is None or - config_name == only_this_config_name): + if ( + only_this_config_name is None + or config_name == only_this_config_name + ): config_dict[config_name] = { "config": config_data.config, - "directory": config_data.directory + "directory": config_data.directory, } metomi.rose.config_editor.upgrade_controller.UpgradeController( - config_dict, self.handle_macro_transforms, + config_dict, + self.handle_macro_transforms, parent_window=self.mainwindow.window, - upgrade_inspector=self.override_macro_defaults) + upgrade_inspector=self.override_macro_defaults, + ) def help(self, *args): """Handle a GUI help request.""" @@ -900,8 +1178,9 @@ def launch_graph(self, namespace, allowed_sections=None): import pygraphviz except ImportError as exc: title = metomi.rose.config_editor.WARNING_CANNOT_GRAPH - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - str(exc), title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, str(exc), title + ) return else: del pygraphviz @@ -915,37 +1194,20 @@ def launch_graph(self, namespace, allowed_sections=None): allowed_sections = [] else: allowed_sections = ( - self.data.helper.get_sections_from_namespace(namespace)) - cmd = (shlex.split(metomi.rose.config_editor.LAUNCH_COMMAND_GRAPH) + - [config_data.directory] + allowed_sections) + self.data.helper.get_sections_from_namespace(namespace) + ) + cmd = ( + shlex.split(metomi.rose.config_editor.LAUNCH_COMMAND_GRAPH) + + [config_data.directory] + + allowed_sections + ) try: metomi.rose.popen.RosePopener().run_bg( - *cmd, stdout=sys.stdout, stderr=sys.stderr) + *cmd, stdout=sys.stdout, stderr=sys.stderr + ) except metomi.rose.popen.RosePopenError as exc: metomi.rose.gtk.dialog.run_exception_dialog(exc) - def launch_scheduler(self, *args): - """Run the scheduler for a suite open in config edit.""" - this_id = self.data.top_level_name - scontrol = metomi.rose.suite_control.SuiteControl() - if scontrol.suite_engine_proc.is_suite_registered(this_id): - try: - return scontrol.gcontrol(this_id) - except metomi.rose.suite_control.SuiteNotRunningError as err: - msg = metomi.rose.config_editor.DIALOG_TEXT_SUITE_NOT_RUNNING.format( - str(err)) - return metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - msg, - metomi.rose.config_editor.DIALOG_TITLE_SUITE_NOT_RUNNING) - else: - msg = metomi.rose.config_editor.DIALOG_TEXT_UNREGISTERED_SUITE.format( - this_id) - return metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - msg, - metomi.rose.config_editor.DIALOG_TITLE_UNREGISTERED_SUITE) - def launch_terminal(self): # Handle a launch terminal request. try: @@ -955,43 +1217,25 @@ def launch_terminal(self): def launch_output_viewer(self): """View a suite's output, if any.""" - seproc = metomi.rose.suite_engine_proc.SuiteEngineProcessor.get_processor() + seproc = ( + metomi.rose.suite_engine_proc.SuiteEngineProcessor.get_processor() + ) try: seproc.launch_suite_log_browser(None, self.data.top_level_name) except metomi.rose.suite_engine_proc.NoSuiteLogError: metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, metomi.rose.config_editor.ERROR_NO_OUTPUT.format( - self.data.top_level_name), - metomi.rose.config_editor.DIALOG_TITLE_ERROR) - - def get_run_suite_args(self, *args): - """Ask the user for custom arguments to suite run.""" - help_cmds = shlex.split(metomi.rose.config_editor.LAUNCH_SUITE_RUN_HELP) - help_text = subprocess.Popen(help_cmds, - stdout=subprocess.PIPE).communicate()[0] - metomi.rose.gtk.dialog.run_command_arg_dialog( - metomi.rose.config_editor.LAUNCH_SUITE_RUN, - help_text, self.run_suite_check_args) - - def run_suite_check_args(self, args): - if args is None: - return False - self.run_suite(args) - - def run_suite(self, args=None, **kwargs): - """Run the suite, if possible.""" - if not isinstance(args, list): - args = [] - for key, value in list(kwargs.items()): - args.extend([key, value]) - metomi.rose.gtk.run.run_suite(*args) - return False + self.data.top_level_name + ), + metomi.rose.config_editor.DIALOG_TITLE_ERROR, + ) def transform_default(self, only_this_config=None): """Run the Rose built-in transformer macros.""" - if (only_this_config is not None and - only_this_config in list(self.data.config.keys())): + if only_this_config is not None and only_this_config in list( + self.data.config.keys() + ): config_keys = [only_this_config] text = metomi.rose.config_editor.DIALOG_LABEL_AUTOFIX else: @@ -1001,18 +1245,26 @@ def transform_default(self, only_this_config=None): metomi.rose.gtk.dialog.DIALOG_TYPE_WARNING, text, metomi.rose.config_editor.DIALOG_TITLE_AUTOFIX, - cancel=True) + cancel=True, + ) if not proceed: return False sorter = metomi.rose.config.sort_settings - to_id = lambda s: self.util.get_id_from_section_option(s.section, - s.option) + to_id = lambda s: self.util.get_id_from_section_option( + s.section, s.option + ) for config_name in config_keys: macro_config = self.data.dump_to_internal_config(config_name) meta_config = self.data.config[config_name].meta macro = metomi.rose.macros.DefaultTransforms() change_list = macro.transform(macro_config, meta_config)[1] - change_list.sort(key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y)))) + change_list.sort( + key=cmp_to_key(lambda x, y: sorter(to_id(x), to_id(y))) + ) self.handle_macro_transforms( - config_name, "Autofixer.transform", - macro_config, change_list, triggers_ok=True) + config_name, + "Autofixer.transform", + macro_config, + change_list, + triggers_ok=True, + ) diff --git a/metomi/rose/config_editor/menuwidget.py b/metomi/rose/config_editor/menuwidget.py index 3f8e41412..18ed9054e 100644 --- a/metomi/rose/config_editor/menuwidget.py +++ b/metomi/rose/config_editor/menuwidget.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config_editor @@ -29,18 +30,18 @@ class MenuWidget(Gtk.Box): - """This class generates a button with a menu for variable actions.""" - MENU_ICON_ERRORS = 'rose-gtk-gnome-package-system-errors' - MENU_ICON_WARNINGS = 'rose-gtk-gnome-package-system-warnings' - MENU_ICON_LATENT = 'rose-gtk-gnome-add' - MENU_ICON_LATENT_ERRORS = 'rose-gtk-gnome-add-errors' - MENU_ICON_LATENT_WARNINGS = 'rose-gtk-gnome-add-warnings' - MENU_ICON_NORMAL = 'rose-gtk-gnome-package-system-normal' + MENU_ICON_ERRORS = "rose-gtk-gnome-package-system-errors" + MENU_ICON_WARNINGS = "rose-gtk-gnome-package-system-warnings" + MENU_ICON_LATENT = "rose-gtk-gnome-add" + MENU_ICON_LATENT_ERRORS = "rose-gtk-gnome-add-errors" + MENU_ICON_LATENT_WARNINGS = "rose-gtk-gnome-add-warnings" + MENU_ICON_NORMAL = "rose-gtk-gnome-package-system-normal" - def __init__(self, variable, var_ops, remove_func, update_func, - launch_help_func): + def __init__( + self, variable, var_ops, remove_func, update_func, launch_help_func + ): super(MenuWidget, self).__init__(homogeneous=False, spacing=0) self.my_variable = variable self.var_ops = var_ops @@ -63,53 +64,81 @@ def load_contents(self): """ - actions = [('Options', 'rose-gtk-gnome-package-system', ''), - ('Info', Gtk.STOCK_INFO, - metomi.rose.config_editor.VAR_MENU_INFO), - ('Help', Gtk.STOCK_HELP, - metomi.rose.config_editor.VAR_MENU_HELP), - ('Web Help', Gtk.STOCK_HOME, - metomi.rose.config_editor.VAR_MENU_URL), - ('Edit', Gtk.STOCK_EDIT, - metomi.rose.config_editor.VAR_MENU_EDIT_COMMENTS), - ('Fix Ignore', Gtk.STOCK_CONVERT, - metomi.rose.config_editor.VAR_MENU_FIX_IGNORE), - ('Ignore', Gtk.STOCK_NO, - metomi.rose.config_editor.VAR_MENU_IGNORE), - ('Enable', Gtk.STOCK_YES, - metomi.rose.config_editor.VAR_MENU_ENABLE), - ('Remove', Gtk.STOCK_DELETE, - metomi.rose.config_editor.VAR_MENU_REMOVE), - ('Add', Gtk.STOCK_ADD, - metomi.rose.config_editor.VAR_MENU_ADD)] - menu_icon_id = 'rose-gtk-gnome-package-system' - is_comp = (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE) + actions = [ + ("Options", "rose-gtk-gnome-package-system", ""), + ("Info", Gtk.STOCK_INFO, metomi.rose.config_editor.VAR_MENU_INFO), + ("Help", Gtk.STOCK_HELP, metomi.rose.config_editor.VAR_MENU_HELP), + ( + "Web Help", + Gtk.STOCK_HOME, + metomi.rose.config_editor.VAR_MENU_URL, + ), + ( + "Edit", + Gtk.STOCK_EDIT, + metomi.rose.config_editor.VAR_MENU_EDIT_COMMENTS, + ), + ( + "Fix Ignore", + Gtk.STOCK_CONVERT, + metomi.rose.config_editor.VAR_MENU_FIX_IGNORE, + ), + ( + "Ignore", + Gtk.STOCK_NO, + metomi.rose.config_editor.VAR_MENU_IGNORE, + ), + ( + "Enable", + Gtk.STOCK_YES, + metomi.rose.config_editor.VAR_MENU_ENABLE, + ), + ( + "Remove", + Gtk.STOCK_DELETE, + metomi.rose.config_editor.VAR_MENU_REMOVE, + ), + ("Add", Gtk.STOCK_ADD, metomi.rose.config_editor.VAR_MENU_ADD), + ] + menu_icon_id = "rose-gtk-gnome-package-system" + is_comp = ( + self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + ) if self.is_ghost or is_comp: - option_ui_middle = ( - option_ui_middle.replace("", '')) + option_ui_middle = option_ui_middle.replace( + "", "" + ) error_types = metomi.rose.config_editor.WARNING_TYPES_IGNORE - if (set(error_types) & set(variable.error.keys()) or - set(error_types) & set(variable.warning.keys()) or - (metomi.rose.META_PROP_COMPULSORY in variable.error and - not self.is_ghost)): - option_ui_middle = ("" + - "" + - option_ui_middle) + if ( + set(error_types) & set(variable.error.keys()) + or set(error_types) & set(variable.warning.keys()) + or ( + metomi.rose.META_PROP_COMPULSORY in variable.error + and not self.is_ghost + ) + ): + option_ui_middle = ( + "" + + "" + + option_ui_middle + ) if variable.warning: if self.is_ghost: menu_icon_id = self.MENU_ICON_LATENT_WARNINGS else: menu_icon_id = self.MENU_ICON_WARNINGS old_middle = option_ui_middle - option_ui_middle = '' + option_ui_middle = "" for warn in variable.warning: warn_name = warn.replace("/", "_") option_ui_middle += ( - "") + "" + ) w_string = "(" + warn.replace("_", "__") + ")" - actions.append(("Warn_" + warn_name, Gtk.STOCK_DIALOG_INFO, - w_string)) + actions.append( + ("Warn_" + warn_name, Gtk.STOCK_DIALOG_INFO, w_string) + ) option_ui_middle += "" + old_middle if variable.error: if self.is_ghost: @@ -117,29 +146,32 @@ def load_contents(self): else: menu_icon_id = self.MENU_ICON_ERRORS old_middle = option_ui_middle - option_ui_middle = '' + option_ui_middle = "" for err in variable.error: err_name = err.replace("/", "_") - option_ui_middle += ("") + option_ui_middle += ( + "" + ) e_string = "(" + err.replace("_", "__") + ")" - actions.append(("Error_" + err_name, Gtk.STOCK_DIALOG_WARNING, - e_string)) + actions.append( + ("Error_" + err_name, Gtk.STOCK_DIALOG_WARNING, e_string) + ) option_ui_middle += "" + old_middle if self.is_ghost: if not variable.error and not variable.warning: menu_icon_id = self.MENU_ICON_LATENT - option_ui_middle = ("" + - "" + - option_ui_middle) + option_ui_middle = ( + "" + + "" + + option_ui_middle + ) if metomi.rose.META_PROP_URL in variable.metadata: url_ui = "" option_ui_middle += url_ui option_ui = option_ui_start + option_ui_middle + option_ui_end self.button = metomi.rose.gtk.util.CustomButton( - stock_id=menu_icon_id, - size=Gtk.IconSize.MENU, - as_tool=True) + stock_id=menu_icon_id, size=Gtk.IconSize.MENU, as_tool=True + ) self._set_hover_over(variable) self.option_ui = option_ui self.actions = actions @@ -147,7 +179,9 @@ def load_contents(self): self.button.connect( "button-press-event", lambda b, e: self._popup_option_menu( - self.option_ui, self.actions, e)) + self.option_ui, self.actions, e + ), + ) # # FIXME: Try to popup the menu at the button, instead of the cursor. # self.button.connect( # "activate", @@ -156,14 +190,14 @@ def load_contents(self): # self.actions, # Gdk.Event(Gdk.KEY_PRESS))) self.button.connect( - "enter-notify-event", - lambda b, e: self._set_hover_over(variable)) + "enter-notify-event", lambda b, e: self._set_hover_over(variable) + ) self._set_hover_over(variable) self.button.show() def get_centre_height(self): """Return the vertical displacement of the centre of this widget.""" - return (self.size_request()[1] / 2) + return self.size_request()[1] / 2 def refresh(self, variable=None): """Reload the contents.""" @@ -174,17 +208,17 @@ def refresh(self, variable=None): self.load_contents() def _set_hover_over(self, variable): - hover_string = 'Variable options' + hover_string = "Variable options" if variable.warning: hover_string = metomi.rose.config_editor.VAR_MENU_TIP_WARNING for warn, warn_info in list(variable.warning.items()): - hover_string += "(" + warn + "): " + warn_info + '\n' - hover_string = hover_string.rstrip('\n') + hover_string += "(" + warn + "): " + warn_info + "\n" + hover_string = hover_string.rstrip("\n") if variable.error: hover_string = metomi.rose.config_editor.VAR_MENU_TIP_ERROR for err, err_info in list(variable.error.items()): - hover_string += "(" + err + "): " + err_info + '\n' - hover_string = hover_string.rstrip('\n') + hover_string += "(" + err + "): " + err_info + "\n" + hover_string = hover_string.rstrip("\n") if self.is_ghost: if not variable.error: hover_string = metomi.rose.config_editor.VAR_MENU_TIP_LATENT @@ -196,16 +230,15 @@ def _perform_add(self): self.var_ops.add_var(self.my_variable) def _popup_option_menu(self, option_ui, actions, event): - actiongroup = Gtk.ActionGroup('Popup') - actiongroup.set_translation_domain('') + actiongroup = Gtk.ActionGroup("Popup") + actiongroup.set_translation_domain("") actiongroup.add_actions(actions) uimanager = Gtk.UIManager() uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(option_ui) - remove_item = uimanager.get_widget('/Options/Remove') - remove_item.connect("activate", - lambda b: self.trigger_remove()) - edit_item = uimanager.get_widget('/Options/Edit') + remove_item = uimanager.get_widget("/Options/Remove") + remove_item.connect("activate", lambda b: self.trigger_remove()) + edit_item = uimanager.get_widget("/Options/Edit") edit_item.connect("activate", self.launch_edit) errors = list(self.my_variable.error.keys()) warnings = list(self.my_variable.warning.keys()) @@ -217,98 +250,127 @@ def _popup_option_menu(self, option_ui, actions, event): action_name = "Error_" + err_name if "action='" + action_name + "'" not in option_ui: continue - err_item = uimanager.get_widget('/Options/' + action_name) - title = metomi.rose.config_editor.DIALOG_VARIABLE_ERROR_TITLE.format( - error, self.my_variable.metadata["id"]) + err_item = uimanager.get_widget("/Options/" + action_name) + title = ( + metomi.rose.config_editor.DIALOG_VARIABLE_ERROR_TITLE.format( + error, self.my_variable.metadata["id"] + ) + ) err_item.set_tooltip_text(self.my_variable.error[error]) err_item.connect( "activate", - lambda e: dialog_func(Gtk.STOCK_DIALOG_WARNING, - self.my_variable.error[error], - title, search_function)) + lambda e: dialog_func( + Gtk.STOCK_DIALOG_WARNING, + self.my_variable.error[error], + title, + search_function, + ), + ) for warning in warnings: action_name = "Warn_" + warning.replace("/", "_") if "action='" + action_name + "'" not in option_ui: continue - warn_item = uimanager.get_widget('/Options/' + action_name) - title = metomi.rose.config_editor.DIALOG_VARIABLE_WARNING_TITLE.format( - warning, self.my_variable.metadata["id"]) + warn_item = uimanager.get_widget("/Options/" + action_name) + title = ( + metomi.rose.config_editor.DIALOG_VARIABLE_WARNING_TITLE.format( + warning, self.my_variable.metadata["id"] + ) + ) warn_item.set_tooltip_text(self.my_variable.warning[warning]) warn_item.connect( "activate", - lambda e: dialog_func(Gtk.STOCK_DIALOG_INFO, - self.my_variable.warning[warning], - title, search_function)) + lambda e: dialog_func( + Gtk.STOCK_DIALOG_INFO, + self.my_variable.warning[warning], + title, + search_function, + ), + ) ignore_item = None enable_item = None if "action='Ignore'" in option_ui: - ignore_item = uimanager.get_widget('/Options/Ignore') - if (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE or self.is_ghost): + ignore_item = uimanager.get_widget("/Options/Ignore") + if ( + self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + or self.is_ghost + ): ignore_item.set_sensitive(False) # It is a non-trigger, optional, enabled variable. - new_reason = {metomi.rose.variable.IGNORED_BY_USER: - metomi.rose.config_editor.IGNORED_STATUS_MANUAL} + new_reason = { + metomi.rose.variable.IGNORED_BY_USER: ( + metomi.rose.config_editor.IGNORED_STATUS_MANUAL + ) + } ignore_item.connect( "activate", lambda b: self.var_ops.set_var_ignored( - self.my_variable, new_reason)) + self.my_variable, new_reason + ), + ) elif "action='Enable'" in option_ui: - enable_item = uimanager.get_widget('/Options/Enable') + enable_item = uimanager.get_widget("/Options/Enable") enable_item.connect( "activate", - lambda b: self.var_ops.set_var_ignored(self.my_variable, {})) + lambda b: self.var_ops.set_var_ignored(self.my_variable, {}), + ) if "action='Fix Ignore'" in option_ui: - fix_ignore_item = uimanager.get_widget('/Options/Fix Ignore') + fix_ignore_item = uimanager.get_widget("/Options/Fix Ignore") fix_ignore_item.set_tooltip_text( - metomi.rose.config_editor.VAR_MENU_TIP_FIX_IGNORE) + metomi.rose.config_editor.VAR_MENU_TIP_FIX_IGNORE + ) fix_ignore_item.connect( "activate", - lambda e: self.var_ops.fix_var_ignored(self.my_variable)) + lambda e: self.var_ops.fix_var_ignored(self.my_variable), + ) if ignore_item is not None: ignore_item.set_sensitive(False) if enable_item is not None: enable_item.set_sensitive(False) - info_item = uimanager.get_widget('/Options/Info') + info_item = uimanager.get_widget("/Options/Info") info_item.connect("activate", self._launch_info_dialog) - if (self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE or self.is_ghost): + if ( + self.my_variable.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + or self.is_ghost + ): remove_item.set_sensitive(False) - help_item = uimanager.get_widget('/Options/Help') - help_item.connect("activate", - lambda b: self.launch_help()) + help_item = uimanager.get_widget("/Options/Help") + help_item.connect("activate", lambda b: self.launch_help()) if metomi.rose.META_PROP_HELP not in self.my_variable.metadata: help_item.set_sensitive(False) - url_item = uimanager.get_widget('/Options/Web Help') - if url_item is not None and 'url' in self.my_variable.metadata: + url_item = uimanager.get_widget("/Options/Web Help") + if url_item is not None and "url" in self.my_variable.metadata: url_item.connect( - "activate", - lambda b: self.launch_help(url_mode=True)) + "activate", lambda b: self.launch_help(url_mode=True) + ) if self.is_ghost: - add_item = uimanager.get_widget('/Options/Add') + add_item = uimanager.get_widget("/Options/Add") add_item.connect("activate", lambda b: self._perform_add()) - option_menu = uimanager.get_widget('/Options') - option_menu.attach_to_widget(self.button, - lambda m, w: False) + option_menu = uimanager.get_widget("/Options") + option_menu.attach_to_widget(self.button, lambda m, w: False) option_menu.show() - option_menu.popup_at_widget(self.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + option_menu.popup_at_widget( + self.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) return False def _launch_info_dialog(self, *args): changes = self.var_ops.get_var_changes(self.my_variable) ns = self.my_variable.metadata["full_ns"] search_function = lambda i: self.var_ops.search_for_var(ns, i) - metomi.rose.config_editor.util.launch_node_info_dialog(self.my_variable, - changes, - search_function) + metomi.rose.config_editor.util.launch_node_info_dialog( + self.my_variable, changes, search_function + ) def launch_edit(self, *args): text = "\n".join(self.my_variable.comments) title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( - self.my_variable.metadata['id']) - metomi.rose.gtk.dialog.run_edit_dialog(text, - finish_hook=self._edit_finish_hook, - title=title) + self.my_variable.metadata["id"] + ) + metomi.rose.gtk.dialog.run_edit_dialog( + text, finish_hook=self._edit_finish_hook, title=title + ) def _edit_finish_hook(self, text): self.var_ops.set_var_comments(self.my_variable, text.splitlines()) @@ -316,21 +378,25 @@ def _edit_finish_hook(self, text): class CheckedMenuWidget(MenuWidget): - """Represent the menu button with a check box instead.""" def __init__(self, *args): super(CheckedMenuWidget, self).__init__(*args) self.remove(self.button) - for string in ["", - "", - ""]: + for string in [ + "", + "", + "", + ]: self.option_ui = self.option_ui.replace(string, "") self.checkbutton = Gtk.CheckButton() self.checkbutton.set_active(not self.is_ghost) meta = self.my_variable.metadata - if not self.is_ghost and meta.get( - metomi.rose.META_PROP_COMPULSORY) == metomi.rose.META_PROP_VALUE_TRUE: + if ( + not self.is_ghost + and meta.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + ): self.checkbutton.set_sensitive(False) self.pack_start(self.checkbutton, expand=False, fill=False, padding=0) self.pack_start(self.button, expand=False, fill=False, padding=0) diff --git a/metomi/rose/config_editor/nav_controller.py b/metomi/rose/config_editor/nav_controller.py index 775648ca0..9e71c16c0 100644 --- a/metomi/rose/config_editor/nav_controller.py +++ b/metomi/rose/config_editor/nav_controller.py @@ -24,7 +24,6 @@ class NavTreeManager(object): - """This controls the navigation namespace tree structure.""" def __init__(self, data, util, reporter, tree_trigger_update): @@ -38,7 +37,7 @@ def is_ns_in_tree(self, ns): """Determine if the namespace is in the tree or not.""" if ns is None: return False - spaces = ns.lstrip('/').split('/') + spaces = ns.lstrip("/").split("/") subtree = self.namespace_tree while spaces: if spaces[0] not in subtree: @@ -47,14 +46,18 @@ def is_ns_in_tree(self, ns): spaces.pop(0) return True - def reload_namespace_tree(self, only_this_namespace=None, - only_this_config=None, - skip_update=False): + def reload_namespace_tree( + self, + only_this_namespace=None, + only_this_config=None, + skip_update=False, + ): """Make the tree of namespaces and load to the tree panel.""" # Clear the old namespace tree information (selectively if necessary). if only_this_namespace is not None and only_this_config is None: - config_name = self.util.split_full_ns(self.data, - only_this_namespace)[0] + config_name = self.util.split_full_ns( + self.data, only_this_namespace + )[0] only_this_config = config_name clear_namespace = only_this_namespace.rsplit("/", 1)[0] self.clear_namespace_tree(clear_namespace) @@ -67,18 +70,20 @@ def reload_namespace_tree(self, only_this_namespace=None, configs = list(self.data.config.keys()) configs.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) configs.sort( - key=lambda x: self.data.config[x].config_type == metomi.rose.TOP_CONFIG_NAME + key=lambda x: self.data.config[x].config_type + == metomi.rose.TOP_CONFIG_NAME ) else: configs = [only_this_config] for config_name in configs: config_data = self.data.config[config_name] if only_this_namespace: - top_spaces = only_this_namespace.lstrip('/').split('/')[:-1] + top_spaces = only_this_namespace.lstrip("/").split("/")[:-1] else: - top_spaces = config_name.lstrip('/').split('/') - self.update_namespace_tree(top_spaces, self.namespace_tree, - prev_spaces=[]) + top_spaces = config_name.lstrip("/").split("/") + self.update_namespace_tree( + top_spaces, self.namespace_tree, prev_spaces=[] + ) self.data.load_metadata_for_namespaces(config_name) # Load tree from sections (usually vast majority of tree nodes) self.data.load_node_namespaces(config_name) @@ -86,32 +91,36 @@ def reload_namespace_tree(self, only_this_namespace=None, ns = section_data.metadata["full_ns"] self.data.namespace_meta_lookup.setdefault(ns, {}) self.data.namespace_meta_lookup[ns].setdefault( - 'title', ns.split('/')[-1]) - spaces = ns.lstrip('/').split('/') - self.update_namespace_tree(spaces, - self.namespace_tree, - prev_spaces=[]) + "title", ns.split("/")[-1] + ) + spaces = ns.lstrip("/").split("/") + self.update_namespace_tree( + spaces, self.namespace_tree, prev_spaces=[] + ) # Now load tree from variables for var in config_data.vars.get_all(): - ns = var.metadata['full_ns'] + ns = var.metadata["full_ns"] self.data.namespace_meta_lookup.setdefault(ns, {}) self.data.namespace_meta_lookup[ns].setdefault( - 'title', ns.split('/')[-1]) - spaces = ns.lstrip('/').split('/') - self.update_namespace_tree(spaces, - self.namespace_tree, - prev_spaces=[]) + "title", ns.split("/")[-1] + ) + spaces = ns.lstrip("/").split("/") + self.update_namespace_tree( + spaces, self.namespace_tree, prev_spaces=[] + ) if not skip_update: # Perform an update. - self.tree_trigger_update(only_this_config=only_this_config, - only_this_namespace=only_this_namespace) + self.tree_trigger_update( + only_this_config=only_this_config, + only_this_namespace=only_this_namespace, + ) def clear_namespace_tree(self, namespace=None): """Clear the namespace tree, or a subtree from namespace.""" if namespace is None: spaces = [] else: - spaces = namespace.lstrip('/').split('/') + spaces = namespace.lstrip("/").split("/") tree = self.namespace_tree for space in spaces: if space not in tree: @@ -130,12 +139,15 @@ def update_namespace_tree(self, spaces, subtree, prev_spaces): this_ns = "/" + "/".join(prev_spaces + [spaces[0]]) change = "" meta = self.data.namespace_meta_lookup.get(this_ns, {}) - meta.setdefault('title', spaces[0]) + meta.setdefault("title", spaces[0]) latent_status = self.data.helper.get_ns_latent_status(this_ns) ignored_status = self.data.helper.get_ns_ignored_status(this_ns) - statuses = {metomi.rose.config_editor.SHOW_MODE_LATENT: latent_status, - metomi.rose.config_editor.SHOW_MODE_IGNORED: ignored_status} + statuses = { + metomi.rose.config_editor.SHOW_MODE_LATENT: latent_status, + metomi.rose.config_editor.SHOW_MODE_IGNORED: ignored_status, + } subtree.setdefault(spaces[0], [{}, meta, statuses, change]) prev_spaces += [spaces[0]] - self.update_namespace_tree(spaces[1:], subtree[spaces[0]][0], - prev_spaces) + self.update_namespace_tree( + spaces[1:], subtree[spaces[0]][0], prev_spaces + ) diff --git a/metomi/rose/config_editor/nav_panel.py b/metomi/rose/config_editor/nav_panel.py index b424683f0..34123fcd5 100644 --- a/metomi/rose/config_editor/nav_panel.py +++ b/metomi/rose/config_editor/nav_panel.py @@ -22,7 +22,8 @@ import sys import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GdkPixbuf from gi.repository import GObject @@ -36,7 +37,6 @@ class PageNavigationPanel(Gtk.ScrolledWindow): - """Generate the page launcher panel. This contains the namespace groupings as child rows. @@ -58,9 +58,15 @@ class PageNavigationPanel(Gtk.ScrolledWindow): COLUMN_TOOLTIP_TEXT = 10 COLUMN_CHANGE_TEXT = 11 - def __init__(self, namespace_tree, launch_ns_func, - get_metadata_comments_func, - popup_menu_func, ask_can_show_func, ask_is_preview): + def __init__( + self, + namespace_tree, + launch_ns_func, + get_metadata_comments_func, + popup_menu_func, + ask_can_show_func, + ask_is_preview, + ): super(PageNavigationPanel, self).__init__() self._launch_ns_func = launch_ns_func self._get_metadata_comments_func = get_metadata_comments_func @@ -70,7 +76,8 @@ def __init__(self, namespace_tree, launch_ns_func, self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.set_shadow_type(Gtk.ShadowType.OUT) self._rec_no_expand_leaves = re.compile( - metomi.rose.config_editor.TREE_PANEL_NO_EXPAND_LEAVES_REGEX) + metomi.rose.config_editor.TREE_PANEL_NO_EXPAND_LEAVES_REGEX + ) self.panel_top = Gtk.TreeViewColumn() self.panel_top.set_title(metomi.rose.config_editor.TREE_PANEL_TITLE) self.cell_error_icon = Gtk.CellRendererPixbuf() @@ -79,31 +86,50 @@ def __init__(self, namespace_tree, launch_ns_func, self.panel_top.pack_start(self.cell_error_icon, False) self.panel_top.pack_start(self.cell_changed_icon, False) self.panel_top.pack_start(self.cell_title, False) - self.panel_top.add_attribute(self.cell_error_icon, - attribute='pixbuf', - column=self.COLUMN_ERROR_ICON) - self.panel_top.add_attribute(self.cell_changed_icon, - attribute='pixbuf', - column=self.COLUMN_CHANGE_ICON) - self.panel_top.set_cell_data_func(self.cell_title, - self._set_title_markup, - self.COLUMN_TITLE) + self.panel_top.add_attribute( + self.cell_error_icon, + attribute="pixbuf", + column=self.COLUMN_ERROR_ICON, + ) + self.panel_top.add_attribute( + self.cell_changed_icon, + attribute="pixbuf", + column=self.COLUMN_CHANGE_ICON, + ) + self.panel_top.set_cell_data_func( + self.cell_title, self._set_title_markup, self.COLUMN_TITLE + ) # The columns in self.data_store correspond to: error_icon, # change_icon, title, name, error and change totals (4), # latent and ignored statuses, main tip text, and change text. - self.data_store = Gtk.TreeStore(GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, - str, str, int, int, int, int, - bool, str, str, str) + self.data_store = Gtk.TreeStore( + GdkPixbuf.Pixbuf, + GdkPixbuf.Pixbuf, + str, + str, + int, + int, + int, + int, + bool, + str, + str, + str, + ) resource_loc = metomi.rose.resource.ResourceLocator(paths=sys.path) - image_path = str(resource_loc.locate('etc/images/rose-config-edit')) - self.null_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/null_icon.png') - self.changed_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/change_icon.png') - self.error_icon = GdkPixbuf.Pixbuf.new_from_file(image_path + - '/error_icon.png') + image_path = str(resource_loc.locate("etc/images/rose-config-edit")) + self.null_icon = GdkPixbuf.Pixbuf.new_from_file( + image_path + "/null_icon.png" + ) + self.changed_icon = GdkPixbuf.Pixbuf.new_from_file( + image_path + "/change_icon.png" + ) + self.error_icon = GdkPixbuf.Pixbuf.new_from_file( + image_path + "/error_icon.png" + ) self.tree = metomi.rose.gtk.util.TooltipTreeView( - get_tooltip_func=self.get_treeview_tooltip) + get_tooltip_func=self.get_treeview_tooltip + ) self.tree.append_column(self.panel_top) self.filter_model = self.data_store.filter_new() self.filter_model.set_visible_func(self._get_should_show) @@ -112,31 +138,30 @@ def __init__(self, namespace_tree, launch_ns_func, self.name_iter_map = {} self.add(self.tree) self.load_tree(None, namespace_tree) - self.tree.connect('button-press-event', - self.handle_activation) + self.tree.connect("button-press-event", self.handle_activation) self._last_tree_activation_path = None - self.tree.connect('row_activated', - self.handle_activation) - self.tree.connect_after('move-cursor', self._handle_cursor_change) - self.tree.connect('key-press-event', self.add_cursor_extra) + self.tree.connect("row_activated", self.handle_activation) + self.tree.connect_after("move-cursor", self._handle_cursor_change) + self.tree.connect("key-press-event", self.add_cursor_extra) self.panel_top.set_clickable(True) - self.panel_top.connect('clicked', - lambda c: self.collapse_reset()) + self.panel_top.connect("clicked", lambda c: self.collapse_reset()) self.show() self.tree.columns_autosize() - self.tree.connect('enter-notify-event', - lambda t, e: self.update_row_tooltips()) + self.tree.connect( + "enter-notify-event", lambda t, e: self.update_row_tooltips() + ) self.visible_iter_map = {} def get_treeview_tooltip(self, view, row_iter, col_index, tip): """Handle creating a tooltip for the treeview.""" - tip.set_text(self.filter_model.get_value(row_iter, - self.COLUMN_TOOLTIP_TEXT)) + tip.set_text( + self.filter_model.get_value(row_iter, self.COLUMN_TOOLTIP_TEXT) + ) return True def add_cursor_extra(self, widget, event): - left = (event.keyval == Gdk.KEY_Left) - right = (event.keyval == Gdk.KEY_Right) + left = event.keyval == Gdk.KEY_Left + right = event.keyval == Gdk.KEY_Right if left or right: path = widget.get_cursor()[0] if path is not None: @@ -149,15 +174,19 @@ def add_cursor_extra(self, widget, event): def _handle_cursor_change(self, *args): current_path = self.tree.get_cursor()[0] if current_path != self._last_tree_activation_path: - GObject.timeout_add(metomi.rose.config_editor.TREE_PANEL_KBD_TIMEOUT, - self._timeout_launch, current_path) + GObject.timeout_add( + metomi.rose.config_editor.TREE_PANEL_KBD_TIMEOUT, + self._timeout_launch, + current_path, + ) def _timeout_launch(self, timeout_path): current_path = self.tree.get_cursor()[0] - if (current_path == timeout_path and - self._last_tree_activation_path != timeout_path): - self._launch_ns_func(self.get_name(timeout_path), - as_new=False) + if ( + current_path == timeout_path + and self._last_tree_activation_path != timeout_path + ): + self._launch_ns_func(self.get_name(timeout_path), as_new=False) return False def load_tree(self, row, namespace_subtree): @@ -204,17 +233,29 @@ def load_tree_stack(self, row, namespace_subtree): row, keylist, key, value_meta_tuple = stack[0] value, meta, statuses, change = value_meta_tuple title = meta[metomi.rose.META_PROP_TITLE] - latent_status = statuses[metomi.rose.config_editor.SHOW_MODE_LATENT] - ignored_status = statuses[metomi.rose.config_editor.SHOW_MODE_IGNORED] - new_row = self.data_store.append(row, [self.null_icon, - self.null_icon, - title, - key, - 0, 0, 0, 0, - latent_status, - ignored_status, - '', - change]) + latent_status = statuses[ + metomi.rose.config_editor.SHOW_MODE_LATENT + ] + ignored_status = statuses[ + metomi.rose.config_editor.SHOW_MODE_IGNORED + ] + new_row = self.data_store.append( + row, + [ + self.null_icon, + self.null_icon, + title, + key, + 0, + 0, + 0, + 0, + latent_status, + ignored_status, + "", + change, + ], + ) new_keylist = keylist + [key] name_iter_map["/".join(new_keylist)] = new_row if isinstance(value, dict): @@ -228,26 +269,34 @@ def _set_title_markup(self, column, cell, model, r_iter, index): title = model.get_value(r_iter, index) title = metomi.rose.gtk.util.safe_str(title) if len(model.get_path(r_iter)) == 1: - title = metomi.rose.config_editor.TITLE_PAGE_ROOT_MARKUP.format(title) + title = metomi.rose.config_editor.TITLE_PAGE_ROOT_MARKUP.format( + title + ) latent_status = model.get_value(r_iter, self.COLUMN_LATENT_STATUS) ignored_status = model.get_value(r_iter, self.COLUMN_IGNORED_STATUS) name = self.get_name(model.get_path(r_iter)) preview_status = self._ask_is_preview(name) if preview_status: - title = metomi.rose.config_editor.TITLE_PAGE_PREVIEW_MARKUP.format(title) + title = metomi.rose.config_editor.TITLE_PAGE_PREVIEW_MARKUP.format( + title + ) if latent_status: if self._get_is_latent_sub_tree(model, r_iter): - title = metomi.rose.config_editor.TITLE_PAGE_LATENT_MARKUP.format( - title) + title = ( + metomi.rose.config_editor.TITLE_PAGE_LATENT_MARKUP.format( + title + ) + ) if ignored_status: title = metomi.rose.config_editor.TITLE_PAGE_IGNORED_MARKUP.format( - ignored_status, title) + ignored_status, title + ) cell.set_property("markup", title) def sort_tree_items(self, row_item_1, row_item_2): """Sort tree items according to name and sort key.""" - sort_key_1 = row_item_1[1][1].get(metomi.rose.META_PROP_SORT_KEY, '~') - sort_key_2 = row_item_2[1][1].get(metomi.rose.META_PROP_SORT_KEY, '~') + sort_key_1 = row_item_1[1][1].get(metomi.rose.META_PROP_SORT_KEY, "~") + sort_key_2 = row_item_2[1][1].get(metomi.rose.META_PROP_SORT_KEY, "~") var_id_1 = row_item_1[0] var_id_2 = row_item_2[0] @@ -256,7 +305,7 @@ def sort_tree_items(self, row_item_1, row_item_2): return metomi.rose.config_editor.util.null_cmp(x_key, y_key) - def set_row_icon(self, names, ind_count=0, ind_type='changed'): + def set_row_icon(self, names, ind_count=0, ind_type="changed"): """Set the icons for row status on or off. Check parent icons. After updating the row which is specified by a list of namespace @@ -264,16 +313,22 @@ def set_row_icon(self, names, ind_count=0, ind_type='changed'): according to the status of their child row icons. """ - ind_map = {'changed': {'icon_col': self.COLUMN_CHANGE_ICON, - 'icon': self.changed_icon, - 'int_col': self.COLUMN_CHANGE_INTERNAL, - 'total_col': self.COLUMN_CHANGE_TOTAL}, - 'error': {'icon_col': self.COLUMN_ERROR_ICON, - 'icon': self.error_icon, - 'int_col': self.COLUMN_ERROR_INTERNAL, - 'total_col': self.COLUMN_ERROR_TOTAL}} - int_col = ind_map[ind_type]['int_col'] - total_col = ind_map[ind_type]['total_col'] + ind_map = { + "changed": { + "icon_col": self.COLUMN_CHANGE_ICON, + "icon": self.changed_icon, + "int_col": self.COLUMN_CHANGE_INTERNAL, + "total_col": self.COLUMN_CHANGE_TOTAL, + }, + "error": { + "icon_col": self.COLUMN_ERROR_ICON, + "icon": self.error_icon, + "int_col": self.COLUMN_ERROR_INTERNAL, + "total_col": self.COLUMN_ERROR_TOTAL, + }, + } + int_col = ind_map[ind_type]["int_col"] + total_col = ind_map[ind_type]["total_col"] row_path = self.get_path_from_names(names, unfiltered=True) if row_path is None: return False @@ -288,29 +343,34 @@ def set_row_icon(self, names, ind_count=0, ind_type='changed'): self.data_store.set_value(row_iter, int_col, ind_count) self.data_store.set_value(row_iter, total_col, new_total) if new_total > 0: - self.data_store.set_value(row_iter, ind_map[ind_type]['icon_col'], - ind_map[ind_type]['icon']) + self.data_store.set_value( + row_iter, + ind_map[ind_type]["icon_col"], + ind_map[ind_type]["icon"], + ) else: - self.data_store.set_value(row_iter, ind_map[ind_type]['icon_col'], - self.null_icon) + self.data_store.set_value( + row_iter, ind_map[ind_type]["icon_col"], self.null_icon + ) # Now pass information up the tree for parent in [row_path[:i] for i in range(len(row_path) - 1, 0, -1)]: parent_iter = self.data_store.get_iter(parent) - old_parent_total = self.data_store.get_value(parent_iter, - total_col) + old_parent_total = self.data_store.get_value( + parent_iter, total_col + ) new_parent_total = old_parent_total + diff_int_count - self.data_store.set_value(parent_iter, - total_col, - new_parent_total) + self.data_store.set_value(parent_iter, total_col, new_parent_total) if new_parent_total > 0: - self.data_store.set_value(parent_iter, - ind_map[ind_type]['icon_col'], - ind_map[ind_type]['icon']) + self.data_store.set_value( + parent_iter, + ind_map[ind_type]["icon_col"], + ind_map[ind_type]["icon"], + ) else: - self.data_store.set_value(parent_iter, - ind_map[ind_type]['icon_col'], - self.null_icon) + self.data_store.set_value( + parent_iter, ind_map[ind_type]["icon_col"], self.null_icon + ) def update_row_tooltips(self): """Synchronise the icon information with the hover-over text.""" @@ -331,15 +391,18 @@ def update_row_tooltips(self): path_iter = self.data_store.get_iter(path) title = self.data_store.get_value(path_iter, self.COLUMN_TITLE) name = self.data_store.get_value(path_iter, self.COLUMN_NAME) - num_errors = self.data_store.get_value(path_iter, - self.COLUMN_ERROR_INTERNAL) - mods = self.data_store.get_value(path_iter, - self.COLUMN_CHANGE_INTERNAL) + num_errors = self.data_store.get_value( + path_iter, self.COLUMN_ERROR_INTERNAL + ) + mods = self.data_store.get_value( + path_iter, self.COLUMN_CHANGE_INTERNAL + ) proper_name = self.get_name(path, unfiltered=True) metadata, comment = self._get_metadata_comments_func(proper_name) description = metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") change = self.data_store.get_value( - path_iter, self.COLUMN_CHANGE_TEXT) + path_iter, self.COLUMN_CHANGE_TEXT + ) text = title if name != title: text += " (" + name + ")" @@ -352,25 +415,30 @@ def update_row_tooltips(self): text += metomi.rose.config_editor.TREE_PANEL_ERROR else: text += metomi.rose.config_editor.TREE_PANEL_ERRORS.format( - num_errors) + num_errors + ) if comment: text += "\n" + comment if change: text += "\n\n" + change self.data_store.set_value( - path_iter, self.COLUMN_TOOLTIP_TEXT, text) + path_iter, self.COLUMN_TOOLTIP_TEXT, text + ) def update_change(self, row_names, new_change): """Update 'changed' text.""" self._set_row_names_value( - row_names, self.COLUMN_CHANGE_TEXT, new_change) + row_names, self.COLUMN_CHANGE_TEXT, new_change + ) def update_statuses(self, row_names, latent_status, ignored_status): """Update latent and ignored statuses.""" self._set_row_names_value( - row_names, self.COLUMN_LATENT_STATUS, latent_status) + row_names, self.COLUMN_LATENT_STATUS, latent_status + ) self._set_row_names_value( - row_names, self.COLUMN_IGNORED_STATUS, ignored_status) + row_names, self.COLUMN_IGNORED_STATUS, ignored_status + ) def _set_row_names_value(self, row_names, index, value): path = self.get_path_from_names(row_names, unfiltered=True) @@ -448,9 +516,11 @@ def get_change_error_totals(self, config_name=None): errors = 0 while iter_ is not None: iter_changes = self.data_store.get_value( - iter_, self.COLUMN_CHANGE_TOTAL) + iter_, self.COLUMN_CHANGE_TOTAL + ) iter_errors = self.data_store.get_value( - iter_, self.COLUMN_ERROR_TOTAL) + iter_, self.COLUMN_ERROR_TOTAL + ) if iter_changes is not None: changes += iter_changes if iter_errors is not None: @@ -464,18 +534,20 @@ def get_change_error_totals(self, config_name=None): def handle_activation(self, treeview=None, event=None, somewidget=None): """Send a page launch request based on left or middle clicks.""" if event is not None and treeview is not None: - if hasattr(event, 'button'): - pathinfo = treeview.get_path_at_pos(int(event.x), - int(event.y)) + if hasattr(event, "button"): + pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) if pathinfo is not None: path, col, cell_x, _ = pathinfo - if (treeview.get_expander_column() == col and - cell_x < 1 + 18 * len(path)): # Hardwired, bad. + if ( + treeview.get_expander_column() == col + and cell_x < 1 + 18 * len(path) + ): # Hardwired, bad. if event.button != 3: return False else: - return self.expand_recursive(start_path=path, - no_duplicates=True) + return self.expand_recursive( + start_path=path, no_duplicates=True + ) if event.button == 3: self.popup_menu(path, event) else: @@ -512,8 +584,10 @@ def get_name(self, path=None, unfiltered=False): for parent in [path[:i] for i in range(len(path) - 1, 0, -1)]: parent_iter = tree_model.get_iter(parent) full_name = str( - tree_model.get_value(parent_iter, self.COLUMN_NAME) + - "/" + full_name) + tree_model.get_value(parent_iter, self.COLUMN_NAME) + + "/" + + full_name + ) return full_name def get_subtree_names(self, path=None): @@ -529,8 +603,9 @@ def get_subtree_names(self, path=None): path = tree_model.get_path(sub_iters[0]) sub_names.append(self.get_name(path)) for i in range(tree_model.iter_n_children(sub_iters[0])): - sub_iters.append(tree_model.iter_nth_child(sub_iters[0], - i)) + sub_iters.append( + tree_model.iter_nth_child(sub_iters[0], i) + ) sub_iters.pop(0) return sub_names @@ -575,9 +650,11 @@ def expand_recursive(self, start_path=None, no_duplicates=False): child_iter = treemodel.iter_next(child_iter) if path != start_path: stack.append(treemodel.iter_next(iter_)) - if (not all(child_dups) and - len(path) <= max_depth and - not self._rec_no_expand_leaves.search(name)): + if ( + not all(child_dups) + and len(path) <= max_depth + and not self._rec_no_expand_leaves.search(name) + ): self.tree.expand_row(path, open_all=False) stack.append(treemodel.iter_children(iter_)) @@ -604,8 +681,9 @@ def _get_should_show(self, model, iter_, _): ignored_status = model.get_value(iter_, self.COLUMN_IGNORED_STATUS) has_error = bool(model.get_value(iter_, self.COLUMN_ERROR_INTERNAL)) child_iter = model.iter_children(iter_) - is_visible = self._ask_can_show_func(latent_status, ignored_status, - has_error) + is_visible = self._ask_can_show_func( + latent_status, ignored_status, has_error + ) if is_visible: return True while child_iter is not None: diff --git a/metomi/rose/config_editor/nav_panel_menu.py b/metomi/rose/config_editor/nav_panel_menu.py index b54669ba3..fee644528 100644 --- a/metomi/rose/config_editor/nav_panel_menu.py +++ b/metomi/rose/config_editor/nav_panel_menu.py @@ -23,7 +23,8 @@ import webbrowser import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -34,14 +35,25 @@ class NavPanelHandler(object): - """Handles the navigation panel menu.""" - def __init__(self, data, util, reporter, mainwindow, - undo_stack, redo_stack, add_config_func, - group_ops_inst, section_ops_inst, variable_ops_inst, - kill_page_func, reload_ns_tree_func, transform_default_func, - graph_ns_func): + def __init__( + self, + data, + util, + reporter, + mainwindow, + undo_stack, + redo_stack, + add_config_func, + group_ops_inst, + section_ops_inst, + variable_ops_inst, + kill_page_func, + reload_ns_tree_func, + transform_default_func, + graph_ns_func, + ): self.data = data self.util = util self.reporter = reporter @@ -59,21 +71,23 @@ def __init__(self, data, util, reporter, mainwindow, def add_dialog(self, base_ns): """Handle an add section dialog and request.""" - if base_ns is not None and '/' in base_ns: + if base_ns is not None and "/" in base_ns: config_name, subsp = self.util.split_full_ns(self.data, base_ns) config_data = self.data.config[config_name] if config_name == base_ns: - help_str = '' + help_str = "" else: sections = self.data.helper.get_sections_from_namespace( - base_ns) + base_ns + ) if sections == []: - help_str = subsp.replace('/', ':') + help_str = subsp.replace("/", ":") else: help_str = sections[0] - help_str = help_str.split(':', 1)[0] - for config_section in (list(config_data.sections.now.keys()) + - list(config_data.sections.latent.keys())): + help_str = help_str.split(":", 1)[0] + for config_section in list( + config_data.sections.now.keys() + ) + list(config_data.sections.latent.keys()): if config_section.startswith(help_str + ":"): help_str = help_str + ":" else: @@ -82,10 +96,16 @@ def add_dialog(self, base_ns): choices_help = self.data.helper.get_missing_sections(config_name) config_names = [ - n for n in self.data.config if not self.ask_is_preview(n)] - config_names.sort(key=cmp_to_key(lambda x, y: (y == config_name) - (x == config_name))) + n for n in self.data.config if not self.ask_is_preview(n) + ] + config_names.sort( + key=cmp_to_key( + lambda x, y: (y == config_name) - (x == config_name) + ) + ) config_name, section = self.mainwindow.launch_add_dialog( - config_names, choices_help, help_str) + config_names, choices_help, help_str + ) if config_name in self.data.config and section is not None: self.sect_ops.add_section(config_name, section, page_launch=True) @@ -106,21 +126,26 @@ def copy_request(self, base_ns, new_section=None, skip_update=False): return False section = sections.pop() config_name = self.util.split_full_ns(self.data, namespace)[0] - return self.group_ops.copy_section(config_name, section, - skip_update=skip_update) + return self.group_ops.copy_section( + config_name, section, skip_update=skip_update + ) def create_request(self): """Handle a create configuration request.""" - if not any(v.config_type == metomi.rose.TOP_CONFIG_NAME - for v in list(self.data.config.values())): + if not any( + v.config_type == metomi.rose.TOP_CONFIG_NAME + for v in list(self.data.config.values()) + ): text = metomi.rose.config_editor.WARNING_APP_CONFIG_CREATE title = metomi.rose.config_editor.WARNING_APP_CONFIG_CREATE_TITLE - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) return False # Need an application configuration to be created. - root = os.path.join(self.data.top_level_directory, - metomi.rose.SUB_CONFIGS_DIR) + root = os.path.join( + self.data.top_level_directory, metomi.rose.SUB_CONFIGS_DIR + ) name, meta = self.mainwindow.launch_new_config_dialog(root) if name is None: return False @@ -130,11 +155,13 @@ def create_request(self): def ignore_request(self, base_ns, is_ignored): """Handle an ignore or enable section request.""" config_names = list(self.data.config.keys()) - if base_ns is not None and '/' in base_ns: + if base_ns is not None and "/" in base_ns: config_name = self.util.split_full_ns(self.data, base_ns)[0] prefer_name_sections = { - config_name: - self.data.helper.get_sections_from_namespace(base_ns)} + config_name: self.data.helper.get_sections_from_namespace( + base_ns + ) + } else: prefer_name_sections = {} config_sect_dict = {} @@ -152,17 +179,24 @@ def ignore_request(self, base_ns, is_ignored): continue if not is_ignored: mode = sect_data.metadata.get( - metomi.rose.META_PROP_COMPULSORY) - if (not sect_data.ignored_reason or - mode == metomi.rose.META_PROP_VALUE_TRUE): + metomi.rose.META_PROP_COMPULSORY + ) + if ( + not sect_data.ignored_reason + or mode == metomi.rose.META_PROP_VALUE_TRUE + ): continue config_sect_dict[config_name].append(section) - config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + config_sect_dict[config_name].sort( + key=cmp_to_key(metomi.rose.config.sort_settings) + ) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - key=cmp_to_key(metomi.rose.config.sort_settings)) + key=cmp_to_key(metomi.rose.config.sort_settings) + ) config_name, section = self.mainwindow.launch_ignore_dialog( - config_sect_dict, prefer_name_sections, is_ignored) + config_sect_dict, prefer_name_sections, is_ignored + ) if config_name in self.data.config and section is not None: self.sect_ops.ignore_section(config_name, section, is_ignored) @@ -183,16 +217,22 @@ def edit_request(self, base_ns): section = metomi.rose.gtk.dialog.run_choices_dialog( metomi.rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_EDIT, sections, - metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION, + ) else: section = sections[0] if section is None: return False - title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format(section) + title = metomi.rose.config_editor.DIALOG_TITLE_EDIT_COMMENTS.format( + section + ) text = "\n".join(config_data.sections.now[section].comments) finish = lambda t: self.sect_ops.set_section_comments( - config_name, section, t.splitlines()) - metomi.rose.gtk.dialog.run_edit_dialog(text, finish_hook=finish, title=title) + config_name, section, t.splitlines() + ) + metomi.rose.gtk.dialog.run_edit_dialog( + text, finish_hook=finish, title=title + ) def fix_request(self, base_ns): """Handle a request to auto-fix a configuration.""" @@ -225,7 +265,8 @@ def info_request(self, namespace): sect_data = config_data.sections.now.get(section) if sect_data is not None: metomi.rose.config_editor.util.launch_node_info_dialog( - sect_data, "", search_function) + sect_data, "", search_function + ) def graph_request(self, namespace): """Handle a graph request for namespace info.""" @@ -234,30 +275,42 @@ def graph_request(self, namespace): def remove_request(self, base_ns): """Handle a delete section request.""" config_names = list(self.data.config.keys()) - if base_ns is not None and '/' in base_ns: + if base_ns is not None and "/" in base_ns: config_name = self.util.split_full_ns(self.data, base_ns)[0] prefer_name_sections = { - config_name: - self.data.helper.get_sections_from_namespace(base_ns)} + config_name: self.data.helper.get_sections_from_namespace( + base_ns + ) + } else: prefer_name_sections = {} config_sect_dict = {} for config_name in config_names: config_data = self.data.config[config_name] - config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + config_sect_dict[config_name] = list( + config_data.sections.now.keys() + ) + config_sect_dict[config_name].sort( + key=cmp_to_key(metomi.rose.config.sort_settings) + ) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - key=cmp_to_key(metomi.rose.config.sort_settings)) + key=cmp_to_key(metomi.rose.config.sort_settings) + ) config_name, section = self.mainwindow.launch_remove_dialog( - config_sect_dict, prefer_name_sections) + config_sect_dict, prefer_name_sections + ) if config_name in self.data.config and section is not None: start_stack_index = len(self.undo_stack) group = ( - metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time())) + metomi.rose.config_editor.STACK_GROUP_DELETE + + "-" + + str(time.time()) + ) config_data = self.data.config[config_name] variable_sorter = lambda v, w: metomi.rose.config.sort_settings( - v.metadata['id'], w.metadata['id']) + v.metadata["id"], w.metadata["id"] + ) variables = list(config_data.vars.now.get(section, [])) variables.sort(key=cmp_to_key(variable_sorter)) variables.reverse() @@ -269,29 +322,41 @@ def remove_request(self, base_ns): def rename_dialog(self, base_ns): """Handle a rename section dialog and request.""" - if base_ns is not None and '/' in base_ns: + if base_ns is not None and "/" in base_ns: config_name = self.util.split_full_ns(self.data, base_ns)[0] prefer_name_sections = { - config_name: - self.data.helper.get_sections_from_namespace(base_ns)} + config_name: self.data.helper.get_sections_from_namespace( + base_ns + ) + } else: prefer_name_sections = {} config_sect_dict = {} for config_name in self.data.config: config_data = self.data.config[config_name] - config_sect_dict[config_name] = list(config_data.sections.now.keys()) - config_sect_dict[config_name].sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + config_sect_dict[config_name] = list( + config_data.sections.now.keys() + ) + config_sect_dict[config_name].sort( + key=cmp_to_key(metomi.rose.config.sort_settings) + ) if config_name in prefer_name_sections: prefer_name_sections[config_name].sort( - key=cmp_to_key(metomi.rose.config.sort_settings)) + key=cmp_to_key(metomi.rose.config.sort_settings) + ) config_name, source_section, target_section = ( self.mainwindow.launch_rename_dialog( - config_sect_dict, prefer_name_sections) + config_sect_dict, prefer_name_sections + ) ) - if (config_name in self.data.config and - source_section is not None and target_section): + if ( + config_name in self.data.config + and source_section is not None + and target_section + ): self.group_ops.rename_section( - config_name, source_section, target_section) + config_name, source_section, target_section + ) def search_request(self, namespace, setting_id): """Handle a search for an id (hyperlink).""" @@ -306,45 +371,87 @@ def popup_panel_menu(self, base_ns, event): namespace = "/" + base_ns.lstrip("/") ui_config_string = """ """ - actions = [('New', Gtk.STOCK_NEW, - metomi.rose.config_editor.TREE_PANEL_NEW_CONFIG), - ('Add', Gtk.STOCK_ADD, - metomi.rose.config_editor.TREE_PANEL_ADD_GENERIC), - ('Autofix', Gtk.STOCK_CONVERT, - metomi.rose.config_editor.TREE_PANEL_AUTOFIX_CONFIG), - ('Clone', Gtk.STOCK_COPY, - metomi.rose.config_editor.TREE_PANEL_CLONE_SECTION), - ('Edit', Gtk.STOCK_EDIT, - metomi.rose.config_editor.TREE_PANEL_EDIT_SECTION), - ('Enable', Gtk.STOCK_YES, - metomi.rose.config_editor.TREE_PANEL_ENABLE_GENERIC), - ('Graph', Gtk.STOCK_SORT_ASCENDING, - metomi.rose.config_editor.TREE_PANEL_GRAPH_SECTION), - ('Ignore', Gtk.STOCK_NO, - metomi.rose.config_editor.TREE_PANEL_IGNORE_GENERIC), - ('Info', Gtk.STOCK_INFO, - metomi.rose.config_editor.TREE_PANEL_INFO_SECTION), - ('Help', Gtk.STOCK_HELP, - metomi.rose.config_editor.TREE_PANEL_HELP_SECTION), - ('URL', Gtk.STOCK_HOME, - metomi.rose.config_editor.TREE_PANEL_URL_SECTION), - ('Remove', Gtk.STOCK_DELETE, - metomi.rose.config_editor.TREE_PANEL_REMOVE_GENERIC), - ('Rename', Gtk.STOCK_COPY, - metomi.rose.config_editor.TREE_PANEL_RENAME_GENERIC)] + actions = [ + ( + "New", + Gtk.STOCK_NEW, + metomi.rose.config_editor.TREE_PANEL_NEW_CONFIG, + ), + ( + "Add", + Gtk.STOCK_ADD, + metomi.rose.config_editor.TREE_PANEL_ADD_GENERIC, + ), + ( + "Autofix", + Gtk.STOCK_CONVERT, + metomi.rose.config_editor.TREE_PANEL_AUTOFIX_CONFIG, + ), + ( + "Clone", + Gtk.STOCK_COPY, + metomi.rose.config_editor.TREE_PANEL_CLONE_SECTION, + ), + ( + "Edit", + Gtk.STOCK_EDIT, + metomi.rose.config_editor.TREE_PANEL_EDIT_SECTION, + ), + ( + "Enable", + Gtk.STOCK_YES, + metomi.rose.config_editor.TREE_PANEL_ENABLE_GENERIC, + ), + ( + "Graph", + Gtk.STOCK_SORT_ASCENDING, + metomi.rose.config_editor.TREE_PANEL_GRAPH_SECTION, + ), + ( + "Ignore", + Gtk.STOCK_NO, + metomi.rose.config_editor.TREE_PANEL_IGNORE_GENERIC, + ), + ( + "Info", + Gtk.STOCK_INFO, + metomi.rose.config_editor.TREE_PANEL_INFO_SECTION, + ), + ( + "Help", + Gtk.STOCK_HELP, + metomi.rose.config_editor.TREE_PANEL_HELP_SECTION, + ), + ( + "URL", + Gtk.STOCK_HOME, + metomi.rose.config_editor.TREE_PANEL_URL_SECTION, + ), + ( + "Remove", + Gtk.STOCK_DELETE, + metomi.rose.config_editor.TREE_PANEL_REMOVE_GENERIC, + ), + ( + "Rename", + Gtk.STOCK_COPY, + metomi.rose.config_editor.TREE_PANEL_RENAME_GENERIC, + ), + ] url = None help_ = None - is_empty = (not self.data.config) + is_empty = not self.data.config if namespace is not None: config_name = self.util.split_full_ns(self.data, namespace)[0] if self.data.config[config_name].is_preview: return False cloneable = self.is_ns_duplicate(namespace) - is_top = (namespace in list(self.data.config.keys())) + is_top = namespace in list(self.data.config.keys()) is_fixable = bool(self.get_ns_errors(namespace)) has_content = self.data.helper.is_ns_content(namespace) is_unsaved = self.data.helper.get_config_has_unsaved_changes( - config_name) + config_name + ) is_latent = self.data.helper.get_ns_latent_status(namespace) latent_sections = self.data.helper.get_latent_sections(namespace) metadata = self.get_ns_metadata_and_comments(namespace)[0] @@ -352,11 +459,17 @@ def popup_panel_menu(self, base_ns, event): for i, section in enumerate(latent_sections): action_name = "Add {0}".format(i) ui_config_string += ''.format( - action_name) + action_name + ) actions.append( - (action_name, Gtk.STOCK_ADD, - metomi.rose.config_editor.TREE_PANEL_ADD_SECTION.format( - section.replace("_", "__"))) + ( + action_name, + Gtk.STOCK_ADD, + metomi.rose.config_editor + .TREE_PANEL_ADD_SECTION.format( + section.replace("_", "__") + ), + ) ) ui_config_string += '' ui_config_string += '' @@ -399,24 +512,26 @@ def popup_panel_menu(self, base_ns, event): """ ui_config_string += """ """ uimanager = Gtk.UIManager() - actiongroup = Gtk.ActionGroup('Popup') + actiongroup = Gtk.ActionGroup("Popup") actiongroup.add_actions(actions) uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_config_string) if namespace is None or (is_top or is_empty): - new_item = uimanager.get_widget('/Popup/New') + new_item = uimanager.get_widget("/Popup/New") new_item.connect("activate", lambda b: self.create_request()) new_item.set_sensitive(not is_empty) - add_item = uimanager.get_widget('/Popup/Add') + add_item = uimanager.get_widget("/Popup/Add") add_item.connect("activate", lambda b: self.add_dialog(namespace)) add_item.set_sensitive(not is_empty) - enable_item = uimanager.get_widget('/Popup/Enable') + enable_item = uimanager.get_widget("/Popup/Enable") enable_item.connect( - "activate", lambda b: self.ignore_request(namespace, False)) + "activate", lambda b: self.ignore_request(namespace, False) + ) enable_item.set_sensitive(not is_empty) - ignore_item = uimanager.get_widget('/Popup/Ignore') + ignore_item = uimanager.get_widget("/Popup/Ignore") ignore_item.connect( - "activate", lambda b: self.ignore_request(namespace, True)) + "activate", lambda b: self.ignore_request(namespace, True) + ) ignore_item.set_sensitive(not is_empty) if namespace is not None: if is_latent: @@ -427,49 +542,64 @@ def popup_panel_menu(self, base_ns, event): add_item.connect( "activate", lambda b: self.sect_ops.add_section( - config_name, b._section)) + config_name, b._section + ), + ) if cloneable: - clone_item = uimanager.get_widget('/Popup/Clone') - clone_item.connect("activate", - lambda b: self.copy_request(namespace)) + clone_item = uimanager.get_widget("/Popup/Clone") + clone_item.connect( + "activate", lambda b: self.copy_request(namespace) + ) if has_content: - edit_item = uimanager.get_widget('/Popup/Edit') - edit_item.connect("activate", - lambda b: self.edit_request(namespace)) - info_item = uimanager.get_widget('/Popup/Info') - info_item.connect("activate", - lambda b: self.info_request(namespace)) + edit_item = uimanager.get_widget("/Popup/Edit") + edit_item.connect( + "activate", lambda b: self.edit_request(namespace) + ) + info_item = uimanager.get_widget("/Popup/Info") + info_item.connect( + "activate", lambda b: self.info_request(namespace) + ) graph_item = uimanager.get_widget("/Popup/Graph") - graph_item.connect("activate", - lambda b: self.graph_request(namespace)) + graph_item.connect( + "activate", lambda b: self.graph_request(namespace) + ) if is_unsaved: graph_item.set_sensitive(False) if help_ is not None: - help_item = uimanager.get_widget('/Popup/Help') - help_title = namespace.split('/')[1:] - help_title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format( - help_title) + help_item = uimanager.get_widget("/Popup/Help") + help_title = namespace.split("/")[1:] + help_title = ( + metomi.rose.config_editor.DIALOG_HELP_TITLE.format( + help_title + ) + ) search_function = lambda i: self.search_request(namespace, i) help_item.connect( "activate", lambda b: metomi.rose.gtk.dialog.run_hyperlink_dialog( - Gtk.STOCK_DIALOG_INFO, help_, help_title, - search_function)) + Gtk.STOCK_DIALOG_INFO, + help_, + help_title, + search_function, + ), + ) if url is not None: - url_item = uimanager.get_widget('/Popup/URL') - url_item.connect( - "activate", lambda b: webbrowser.open(url)) + url_item = uimanager.get_widget("/Popup/URL") + url_item.connect("activate", lambda b: webbrowser.open(url)) if is_fixable: - autofix_item = uimanager.get_widget('/Popup/Autofix') - autofix_item.connect("activate", - lambda b: self.fix_request(namespace)) - remove_section_item = uimanager.get_widget('/Popup/Remove') + autofix_item = uimanager.get_widget("/Popup/Autofix") + autofix_item.connect( + "activate", lambda b: self.fix_request(namespace) + ) + remove_section_item = uimanager.get_widget("/Popup/Remove") remove_section_item.connect( - "activate", lambda b: self.remove_request(namespace)) - rename_section_item = uimanager.get_widget('/Popup/Rename') + "activate", lambda b: self.remove_request(namespace) + ) + rename_section_item = uimanager.get_widget("/Popup/Rename") rename_section_item.connect( - "activate", lambda b: self.rename_dialog(namespace)) - menu = uimanager.get_widget('/Popup') + "activate", lambda b: self.rename_dialog(namespace) + ) + menu = uimanager.get_widget("/Popup") menu.popup_at_pointer(event) return False @@ -483,8 +613,10 @@ def is_ns_duplicate(self, namespace): sect_data = self.data.config[config_name].sections.now.get(section) if sect_data is None: return False - return (sect_data.metadata.get(metomi.rose.META_PROP_DUPLICATE) == - metomi.rose.META_PROP_VALUE_TRUE) + return ( + sect_data.metadata.get(metomi.rose.META_PROP_DUPLICATE) + == metomi.rose.META_PROP_VALUE_TRUE + ) def get_ns_errors(self, namespace): """Count the number of errors in a namespace.""" @@ -495,7 +627,8 @@ def get_ns_errors(self, namespace): for section in sections: errors += len(config_data.sections.get_sect(section).error) real_data, latent_data = self.data.helper.get_data_for_namespace( - namespace) + namespace + ) errors += sum([len(v.error) for v in real_data + latent_data]) return errors @@ -510,18 +643,24 @@ def get_can_show_page(self, latent_status, ignored_status, has_error): # Always show this. return True show_ignored = self.data.page_ns_show_modes[ - metomi.rose.config_editor.SHOW_MODE_IGNORED] + metomi.rose.config_editor.SHOW_MODE_IGNORED + ] show_user_ignored = self.data.page_ns_show_modes[ - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED] + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED + ] show_latent = self.data.page_ns_show_modes[ - metomi.rose.config_editor.SHOW_MODE_LATENT] + metomi.rose.config_editor.SHOW_MODE_LATENT + ] if latent_status: if not show_latent: # Latent page, no latent pages allowed. return False # Latent page, latent pages allowed (but may be ignored...). if ignored_status: - if ignored_status == metomi.rose.config.ConfigNode.STATE_USER_IGNORED: + if ( + ignored_status + == metomi.rose.config.ConfigNode.STATE_USER_IGNORED + ): if show_ignored or show_user_ignored: # This is an allowed user-ignored page. return True diff --git a/metomi/rose/config_editor/ops/group.py b/metomi/rose/config_editor/ops/group.py index d90cdeeb8..db269d083 100644 --- a/metomi/rose/config_editor/ops/group.py +++ b/metomi/rose/config_editor/ops/group.py @@ -36,14 +36,21 @@ class GroupOperations(object): - """Class to perform actions on groups of sections and/or options.""" - def __init__(self, data, util, reporter, undo_stack, redo_stack, - section_ops_inst, - variable_ops_inst, - view_page_func, update_ns_sub_data_func, - reload_ns_tree_func): + def __init__( + self, + data, + util, + reporter, + undo_stack, + redo_stack, + section_ops_inst, + variable_ops_inst, + view_page_func, + update_ns_sub_data_func, + reload_ns_tree_func, + ): self.data = data self.util = util self.reporter = reporter @@ -55,25 +62,34 @@ def __init__(self, data, util, reporter, undo_stack, redo_stack, self.update_ns_sub_data_func = update_ns_sub_data_func self.reload_ns_tree_func = reload_ns_tree_func - def apply_diff(self, config_name, config_diff, origin_name=None, - triggers_ok=False, is_reversed=False): + def apply_diff( + self, + config_name, + config_diff, + origin_name=None, + triggers_ok=False, + is_reversed=False, + ): """Apply a metomi.rose.config.ConfigNodeDiff object to the config.""" state_reason_dict = { metomi.rose.config.ConfigNode.STATE_NORMAL: {}, metomi.rose.config.ConfigNode.STATE_USER_IGNORED: { - metomi.rose.variable.IGNORED_BY_USER: - metomi.rose.config_editor.IGNORED_STATUS_MACRO + metomi.rose.variable.IGNORED_BY_USER: ( + metomi.rose.config_editor.IGNORED_STATUS_MACRO + ) }, metomi.rose.config.ConfigNode.STATE_SYST_IGNORED: { - metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_MACRO - } + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor.IGNORED_STATUS_MACRO + ) + }, } nses = [] ids = [] # Handle added sections. - for keys, data in sorted(config_diff.get_added(), - key=lambda _: len(_[0])): + for keys, data in sorted( + config_diff.get_added(), key=lambda _: len(_[0]) + ): value, state, comments = data reason = state_reason_dict[state] if len(keys) == 1: @@ -81,25 +97,30 @@ def apply_diff(self, config_name, config_diff, origin_name=None, sect = keys[0] ids.append(sect) nses.append( - self.sect_ops.add_section(config_name, sect, - comments=comments, - ignored_reason=reason, - skip_update=True, - skip_undo=True) + self.sect_ops.add_section( + config_name, + sect, + comments=comments, + ignored_reason=reason, + skip_update=True, + skip_undo=True, + ) ) else: sect, opt = keys var_id = self.util.get_id_from_section_option(sect, opt) ids.append(var_id) metadata = self.data.helper.get_metadata_for_config_id( - var_id, config_name) + var_id, config_name + ) variable = metomi.rose.variable.Variable(opt, value, metadata) variable.comments = copy.deepcopy(comments) variable.ignored_reason = copy.deepcopy(reason) self.data.load_ns_for_node(variable, config_name) nses.append( - self.var_ops.add_var(variable, skip_update=True, - skip_undo=True) + self.var_ops.add_var( + variable, skip_update=True, skip_undo=True + ) ) # Handle modified settings. @@ -126,81 +147,106 @@ def apply_diff(self, config_name, config_diff, origin_name=None, if opt is None: # Section. nses.append( - self.sect_ops.set_section_comments(config_name, sect, - comments, - skip_update=True, - skip_undo=True) + self.sect_ops.set_section_comments( + config_name, + sect, + comments, + skip_update=True, + skip_undo=True, + ) ) else: nses.append( self.var_ops.set_var_comments( - variable, comments, skip_undo=True, - skip_update=True) + variable, + comments, + skip_undo=True, + skip_update=True, + ) ) if opt is not None and value != old_value: # Change the value (has to be a variable). nses.append( self.var_ops.set_var_value( - var, value, skip_undo=True, skip_update=True) + var, value, skip_undo=True, skip_update=True + ) ) if opt is None: ignored_changed = True is_ignored = False - if (metomi.rose.variable.IGNORED_BY_USER in old_reason and - metomi.rose.variable.IGNORED_BY_USER not in reason): + if ( + metomi.rose.variable.IGNORED_BY_USER in old_reason + and metomi.rose.variable.IGNORED_BY_USER not in reason + ): # Enable from user-ignored. is_ignored = False - elif (metomi.rose.variable.IGNORED_BY_USER not in old_reason and - metomi.rose.variable.IGNORED_BY_USER in reason): + elif ( + metomi.rose.variable.IGNORED_BY_USER not in old_reason + and metomi.rose.variable.IGNORED_BY_USER in reason + ): # User-ignore from enabled. is_ignored = True - elif (triggers_ok and - metomi.rose.variable.IGNORED_BY_SYSTEM not in old_reason and - metomi.rose.variable.IGNORED_BY_SYSTEM in reason): + elif ( + triggers_ok + and metomi.rose.variable.IGNORED_BY_SYSTEM + not in old_reason + and metomi.rose.variable.IGNORED_BY_SYSTEM in reason + ): # Trigger-ignore. sect_data.error.setdefault( metomi.rose.config_editor.WARNING_TYPE_ENABLED, - metomi.rose.config_editor.IGNORED_STATUS_MACRO) + metomi.rose.config_editor.IGNORED_STATUS_MACRO, + ) is_ignored = True - elif (triggers_ok and - metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason and - metomi.rose.variable.IGNORED_BY_SYSTEM not in reason): + elif ( + triggers_ok + and metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason + and metomi.rose.variable.IGNORED_BY_SYSTEM not in reason + ): # Enabled from trigger-ignore. sect_data.error.setdefault( metomi.rose.config_editor.WARNING_TYPE_TRIGGER_IGNORED, - metomi.rose.config_editor.IGNORED_STATUS_MACRO) + metomi.rose.config_editor.IGNORED_STATUS_MACRO, + ) is_ignored = False else: ignored_changed = False if ignored_changed: - ignore_nses, ignore_ids = ( - self.sect_ops.ignore_section(config_name, sect, - is_ignored, - override=True, - skip_update=True, - skip_undo=True) + ignore_nses, ignore_ids = self.sect_ops.ignore_section( + config_name, + sect, + is_ignored, + override=True, + skip_update=True, + skip_undo=True, ) nses.extend(ignore_nses) ids.extend(ignore_ids) elif set(reason) != set(old_reason): nses.append( - self.var_ops.set_var_ignored(var, new_reason_dict=reason, - override=True, skip_undo=True, - skip_update=True) + self.var_ops.set_var_ignored( + var, + new_reason_dict=reason, + override=True, + skip_undo=True, + skip_update=True, + ) ) - for keys, data in sorted(config_diff.get_removed(), - key=lambda _: -len(_[0])): + for keys, data in sorted( + config_diff.get_removed(), key=lambda _: -len(_[0]) + ): # Sort so that variables are removed first. sect = keys[0] if len(keys) == 1: ids.append(sect) nses.extend( - self.sect_ops.remove_section(config_name, sect, - skip_update=True, - skip_undo=True)) + self.sect_ops.remove_section( + config_name, sect, skip_update=True, skip_undo=True + ) + ) else: sect = keys[0] opt = keys[1] @@ -209,7 +255,8 @@ def apply_diff(self, config_name, config_diff, origin_name=None, var = self.data.helper.get_variable_by_id(var_id, config_name) nses.append( self.var_ops.remove_var( - var, skip_update=True, skip_undo=True) + var, skip_update=True, skip_undo=True + ) ) reverse_diff = config_diff.get_reversed() if is_reversed: @@ -221,9 +268,14 @@ def apply_diff(self, config_name, config_diff, origin_name=None, action, reverse_diff, self.apply_diff, - (config_name, reverse_diff, origin_name, triggers_ok, - not is_reversed), - custom_name=origin_name + ( + config_name, + reverse_diff, + origin_name, + triggers_ok, + not is_reversed, + ), + custom_name=origin_name, ) self.undo_stack.append(stack_item) del self.redo_stack[:] @@ -238,55 +290,71 @@ def apply_diff(self, config_name, config_diff, origin_name=None, self.sect_ops.trigger_update(config_name) return ids - def add_section_with_options(self, config_name, new_section_name, - opt_map=None): + def add_section_with_options( + self, config_name, new_section_name, opt_map=None + ): """Add a section and any compulsory options. Any option-value pairs in the opt_map dict will also be added. """ start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time()) - self.sect_ops.add_section(config_name, new_section_name, - skip_update=True) + group = ( + metomi.rose.config_editor.STACK_GROUP_ADD + "-" + str(time.time()) + ) + self.sect_ops.add_section( + config_name, new_section_name, skip_update=True + ) namespace = self.data.helper.get_default_section_namespace( - new_section_name, config_name) + new_section_name, config_name + ) config_data = self.data.config[config_name] if opt_map is None: opt_map = {} for var in list(config_data.vars.latent.get(new_section_name, [])): if var.name in opt_map: var.value = opt_map.pop(var.name) - if (var.name in opt_map or - (var.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE)): + if var.name in opt_map or ( + var.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + ): self.var_ops.add_var(var, skip_update=True) for opt_name, value in list(opt_map.items()): var_id = self.util.get_id_from_section_option( - new_section_name, opt_name) + new_section_name, opt_name + ) metadata = self.data.helper.get_metadata_for_config_id( - var_id, config_name) - metadata['full_ns'] = namespace - flags = self.data.load_option_flags(config_name, - new_section_name, opt_name) + var_id, config_name + ) + metadata["full_ns"] = namespace + flags = self.data.load_option_flags( + config_name, new_section_name, opt_name + ) ignored_reason = {} # This may not be safe. - var = metomi.rose.variable.Variable(opt_name, value, - metadata, ignored_reason, - error={}, - flags=flags) + var = metomi.rose.variable.Variable( + opt_name, + value, + metadata, + ignored_reason, + error={}, + flags=flags, + ) self.var_ops.add_var(var, skip_update=True) self.reload_ns_tree_func(namespace) for stack_item in self.undo_stack[start_stack_index:]: stack_item.group = group return new_section_name - def copy_section(self, config_name, section, new_section=None, - skip_update=False): + def copy_section( + self, config_name, section, new_section=None, skip_update=False + ): """Copy a section and its options.""" start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_COPY + "-" + str(time.time()) + group = ( + metomi.rose.config_editor.STACK_GROUP_COPY + "-" + str(time.time()) + ) config_data = self.data.config[config_name] - section_base = re.sub(r'(.*)\(\w+\)$', r"\1", section) + section_base = re.sub(r"(.*)\(\w+\)$", r"\1", section) existing_sections = [] clone_vars = [] existing_sections = list(config_data.vars.now.keys()) @@ -299,18 +367,21 @@ def copy_section(self, config_name, section, new_section=None, while new_section in existing_sections: i += 1 new_section = section_base + "(" + str(i) + ")" - new_namespace = self.sect_ops.add_section(config_name, new_section, - skip_update=skip_update) + new_namespace = self.sect_ops.add_section( + config_name, new_section, skip_update=skip_update + ) if new_namespace is None: # Add failed (section already exists). return for var in clone_vars: var_id = self.util.get_id_from_section_option( - new_section, var.name) + new_section, var.name + ) metadata = self.data.helper.get_metadata_for_config_id( - var_id, config_name) + var_id, config_name + ) var.process_metadata(metadata) - var.metadata['full_ns'] = new_namespace + var.metadata["full_ns"] = new_namespace sorter = metomi.rose.config.sort_settings clone_vars.sort(key=cmp_to_key(lambda v, w: sorter(v.name, w.name))) if skip_update: @@ -324,19 +395,31 @@ def copy_section(self, config_name, section, new_section=None, stack_item.group = group return new_section - def ignore_sections(self, config_name, sections, is_ignored, - skip_update=False, skip_sub_data_update=True): + def ignore_sections( + self, + config_name, + sections, + is_ignored, + skip_update=False, + skip_sub_data_update=True, + ): """Implement a mass user-ignore or enable of sections.""" start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_IGNORE + "-" + str(time.time()) + group = ( + metomi.rose.config_editor.STACK_GROUP_IGNORE + + "-" + + str(time.time()) + ) nses = [] for section in sections: ns = self.data.helper.get_default_section_namespace( - section, config_name) + section, config_name + ) if ns not in nses: nses.append(ns) skipped_nses = self.sect_ops.ignore_section( - config_name, section, is_ignored, skip_update=True)[0] + config_name, section, is_ignored, skip_update=True + )[0] for ns in skipped_nses: if ns not in nses: nses.append(ns) @@ -345,7 +428,8 @@ def ignore_sections(self, config_name, sections, is_ignored, if not skip_update: for ns in nses: self.sect_ops.trigger_update( - ns, skip_sub_data_update=skip_sub_data_update) + ns, skip_sub_data_update=skip_sub_data_update + ) self.sect_ops.trigger_info_update(ns) self.sect_ops.trigger_update(config_name) self.update_ns_sub_data_func(config_name) @@ -353,24 +437,34 @@ def ignore_sections(self, config_name, sections, is_ignored, def remove_section(self, config_name, section, skip_update=False): """Implement a remove of a section and its options.""" start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + group = ( + metomi.rose.config_editor.STACK_GROUP_DELETE + + "-" + + str(time.time()) + ) config_data = self.data.config[config_name] variables = config_data.vars.now.get(section, []) for variable in list(variables): self.var_ops.remove_var(variable, skip_update=True) - self.sect_ops.remove_section(config_name, section, - skip_update=skip_update) + self.sect_ops.remove_section( + config_name, section, skip_update=skip_update + ) for stack_item in self.undo_stack[start_stack_index:]: stack_item.group = group - def rename_section(self, config_name, section, target_section, - skip_update=False): + def rename_section( + self, config_name, section, target_section, skip_update=False + ): """Implement a rename of a section and its options.""" start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_RENAME + "-" + str(time.time()) - added_section = self.copy_section(config_name, section, - target_section, - skip_update=skip_update) + group = ( + metomi.rose.config_editor.STACK_GROUP_RENAME + + "-" + + str(time.time()) + ) + added_section = self.copy_section( + config_name, section, target_section, skip_update=skip_update + ) if added_section is None: # Couldn't add the target section. return @@ -381,11 +475,16 @@ def rename_section(self, config_name, section, target_section, def remove_sections(self, config_name, sections, skip_update=False): """Implement a mass removal of sections.""" start_stack_index = len(self.undo_stack) - group = metomi.rose.config_editor.STACK_GROUP_DELETE + "-" + str(time.time()) + group = ( + metomi.rose.config_editor.STACK_GROUP_DELETE + + "-" + + str(time.time()) + ) nses = [] for section in sections: ns = self.data.helper.get_default_section_namespace( - section, config_name) + section, config_name + ) if ns not in nses: nses.append(ns) self.remove_section(config_name, section, skip_update=True) @@ -408,18 +507,25 @@ def get_sub_ops_for_namespace(self, namespace): self.remove_section, self.remove_sections, get_var_id_values_func=( - self.data.helper.get_sub_data_var_id_value_map)) + self.data.helper.get_sub_data_var_id_value_map + ), + ) class SubDataOperations(object): - """Class to hold a selected set of functions.""" - def __init__(self, config_name, - add_section_func, clone_section_func, - ignore_section_func, ignore_sections_func, - remove_section_func, remove_sections_func, - get_var_id_values_func): + def __init__( + self, + config_name, + add_section_func, + clone_section_func, + ignore_section_func, + ignore_sections_func, + remove_section_func, + remove_sections_func, + get_var_id_values_func, + ): self.config_name = config_name self._add_section_func = add_section_func self._clone_section_func = clone_section_func @@ -431,8 +537,9 @@ def __init__(self, config_name, def add_section(self, new_section_name, opt_map=None): """Add a new section, complete with any compulsory variables.""" - return self._add_section_func(self.config_name, new_section_name, - opt_map=opt_map) + return self._add_section_func( + self.config_name, new_section_name, opt_map=opt_map + ) def clone_section(self, clone_section_name): """Copy a (duplicate) section and all its options.""" @@ -441,29 +548,29 @@ def clone_section(self, clone_section_name): def ignore_section(self, ignore_section_name, is_ignored): """User-ignore or enable a section.""" return self._ignore_section_func( - self.config_name, - ignore_section_name, - is_ignored) + self.config_name, ignore_section_name, is_ignored + ) - def ignore_sections(self, ignore_sections_list, is_ignored, - skip_sub_data_update=True): + def ignore_sections( + self, ignore_sections_list, is_ignored, skip_sub_data_update=True + ): """User-ignore or enable a list of sections.""" return self._ignore_sections_func( self.config_name, ignore_sections_list, is_ignored, - skip_sub_data_update=skip_sub_data_update + skip_sub_data_update=skip_sub_data_update, ) def remove_section(self, remove_section_name): """Remove a section and all its options.""" - return self._remove_section_func(self.config_name, - remove_section_name) + return self._remove_section_func(self.config_name, remove_section_name) def remove_sections(self, remove_sections_list): """Remove a list of sections and all their options.""" - return self._remove_sections_func(self.config_name, - remove_sections_list) + return self._remove_sections_func( + self.config_name, remove_sections_list + ) def get_var_id_values(self): """Return a map of all var id values.""" diff --git a/metomi/rose/config_editor/ops/section.py b/metomi/rose/config_editor/ops/section.py index 4ed738157..55f65115a 100644 --- a/metomi/rose/config_editor/ops/section.py +++ b/metomi/rose/config_editor/ops/section.py @@ -27,7 +27,8 @@ import copy import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") import metomi.rose.config_editor.stack import metomi.rose.gtk.dialog @@ -35,19 +36,25 @@ class SectionOperations(object): - """A class to hold functions that act on sections and their storage.""" - def __init__(self, data, util, reporter, undo_stack, redo_stack, - check_cannot_enable_func=metomi.rose.config_editor.false_function, - update_ns_func=metomi.rose.config_editor.false_function, - update_sub_data_func=metomi.rose.config_editor.false_function, - update_info_func=metomi.rose.config_editor.false_function, - update_comments_func=metomi.rose.config_editor.false_function, - update_tree_func=metomi.rose.config_editor.false_function, - search_id_func=metomi.rose.config_editor.false_function, - view_page_func=metomi.rose.config_editor.false_function, - kill_page_func=metomi.rose.config_editor.false_function): + def __init__( + self, + data, + util, + reporter, + undo_stack, + redo_stack, + check_cannot_enable_func=metomi.rose.config_editor.false_function, + update_ns_func=metomi.rose.config_editor.false_function, + update_sub_data_func=metomi.rose.config_editor.false_function, + update_info_func=metomi.rose.config_editor.false_function, + update_comments_func=metomi.rose.config_editor.false_function, + update_tree_func=metomi.rose.config_editor.false_function, + search_id_func=metomi.rose.config_editor.false_function, + view_page_func=metomi.rose.config_editor.false_function, + kill_page_func=metomi.rose.config_editor.false_function, + ): self.__data = data self.__util = util self.__reporter = reporter @@ -62,9 +69,16 @@ def __init__(self, data, util, reporter, undo_stack, redo_stack, self.view_page_func = view_page_func self.kill_page_func = kill_page_func - def add_section(self, config_name, section, skip_update=False, - page_launch=False, comments=None, ignored_reason=None, - skip_undo=False): + def add_section( + self, + config_name, + section, + skip_update=False, + page_launch=False, + comments=None, + ignored_reason=None, + skip_undo=False, + ): """Add a section to this configuration.""" config_data = self.__data.config[config_name] new_section_data = None @@ -73,14 +87,18 @@ def add_section(self, config_name, section, skip_update=False, metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, metomi.rose.config_editor.ERROR_SECTION_ADD.format(section), title=metomi.rose.config_editor.ERROR_SECTION_ADD_TITLE, - modal=False) + modal=False, + ) return if section in config_data.sections.latent: new_section_data = config_data.sections.latent.pop(section) else: metadata = self.__data.helper.get_metadata_for_config_id( - section, config_name) - new_section_data = metomi.rose.section.Section(section, [], metadata) + section, config_name + ) + new_section_data = metomi.rose.section.Section( + section, [], metadata + ) if comments is not None: new_section_data.comments = copy.deepcopy(comments) if ignored_reason is not None: @@ -89,13 +107,15 @@ def add_section(self, config_name, section, skip_update=False, self.__data.add_section_to_config(section, config_name) self.__data.load_ns_for_node(new_section_data, config_name) self.__data.load_file_metadata(config_name, section) - self.__data.load_vars_from_config(config_name, - only_this_section=section, - update=True) - self.__data.load_node_namespaces(config_name, - only_this_section=section) - metadata = self.__data.helper.get_metadata_for_config_id(section, - config_name) + self.__data.load_vars_from_config( + config_name, only_this_section=section, update=True + ) + self.__data.load_node_namespaces( + config_name, only_this_section=section + ) + metadata = self.__data.helper.get_metadata_for_config_id( + section, config_name + ) new_section_data.process_metadata(metadata) ns = new_section_data.metadata["full_ns"] if not skip_update: @@ -109,7 +129,8 @@ def add_section(self, config_name, section, skip_update=False, metomi.rose.config_editor.STACK_ACTION_ADDED, copy_section_data, self.remove_section, - (config_name, section, skip_update)) + (config_name, section, skip_update), + ) self.__undo_stack.append(stack_item) del self.__redo_stack[:] if page_launch and not skip_update: @@ -118,8 +139,15 @@ def add_section(self, config_name, section, skip_update=False, self.trigger_update(ns) return ns - def ignore_section(self, config_name, section, is_ignored, - override=False, skip_update=False, skip_undo=False): + def ignore_section( + self, + config_name, + section, + is_ignored, + override=False, + skip_update=False, + skip_undo=False, + ): """Ignore or enable a section for this configuration. Returns a list of namespaces that need further updates. This is @@ -134,44 +162,68 @@ def ignore_section(self, config_name, section, is_ignored, if is_ignored: # User-ignore request for this section. # The section must be enabled and optional. - if (not override and ( - sect_data.ignored_reason or - sect_data.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE)): + if not override and ( + sect_data.ignored_reason + or sect_data.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + ): metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - metomi.rose.config_editor.WARNING_CANNOT_USER_IGNORE.format( - section), - metomi.rose.config_editor.WARNING_CANNOT_IGNORE_TITLE) + metomi.rose.config_editor + .WARNING_CANNOT_USER_IGNORE.format( + section + ), + metomi.rose.config_editor.WARNING_CANNOT_IGNORE_TITLE, + ) return [], [] - for error in [metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED, - metomi.rose.config_editor.WARNING_TYPE_ENABLED]: + for error in [ + metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED, + metomi.rose.config_editor.WARNING_TYPE_ENABLED, + ]: if error in sect_data.error: - sect_data.ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) + sect_data.ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor.IGNORED_STATUS_MANUAL + ) + } + ) sect_data.error.pop(error) break else: - sect_data.ignored_reason.update({ - metomi.rose.variable.IGNORED_BY_USER: - metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) + sect_data.ignored_reason.update( + { + metomi.rose.variable.IGNORED_BY_USER: ( + metomi.rose.config_editor.IGNORED_STATUS_MANUAL + ) + } + ) action = metomi.rose.config_editor.STACK_ACTION_IGNORED else: # Enable request for this section. # The section must not be justifiably triggered ignored. - ign_errors = [e for e in metomi.rose.config_editor.WARNING_TYPES_IGNORE - if e != metomi.rose.config_editor.WARNING_TYPE_ENABLED] + ign_errors = [ + e + for e in metomi.rose.config_editor.WARNING_TYPES_IGNORE + if e != metomi.rose.config_editor.WARNING_TYPE_ENABLED + ] my_errors = list(sect_data.error.keys()) - if (not override and - (metomi.rose.variable.IGNORED_BY_SYSTEM in - sect_data.ignored_reason) and - all([e not in my_errors for e in ign_errors]) and - self.check_cannot_enable_setting(config_name, section)): + if ( + not override + and ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in sect_data.ignored_reason + ) + and all([e not in my_errors for e in ign_errors]) + and self.check_cannot_enable_setting(config_name, section) + ): metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - metomi.rose.config_editor.WARNING_CANNOT_ENABLE.format(section), - metomi.rose.config_editor.WARNING_CANNOT_ENABLE_TITLE) + metomi.rose.config_editor.WARNING_CANNOT_ENABLE.format( + section + ), + metomi.rose.config_editor.WARNING_CANNOT_ENABLE_TITLE, + ) return [], [] sect_data.ignored_reason.clear() for error in ign_errors: @@ -187,20 +239,25 @@ def ignore_section(self, config_name, section, is_ignored, action, copy_sect_data, self.ignore_section, - (config_name, section, not is_ignored, True) + (config_name, section, not is_ignored, True), ) self.__undo_stack.append(stack_item) del self.__redo_stack[:] - for var in (config_data.vars.now.get(section, []) + - config_data.vars.latent.get(section, [])): + for var in config_data.vars.now.get( + section, [] + ) + config_data.vars.latent.get(section, []): self.trigger_info_update(var) - if var.metadata['full_ns'] not in nses_to_do: - nses_to_do.append(var.metadata['full_ns']) - ids_to_do.append(var.metadata['id']) + if var.metadata["full_ns"] not in nses_to_do: + nses_to_do.append(var.metadata["full_ns"]) + ids_to_do.append(var.metadata["id"]) if is_ignored: var.ignored_reason.update( - {metomi.rose.variable.IGNORED_BY_SECTION: - metomi.rose.config_editor.IGNORED_STATUS_MANUAL}) + { + metomi.rose.variable.IGNORED_BY_SECTION: ( + metomi.rose.config_editor.IGNORED_STATUS_MANUAL + ) + } + ) elif metomi.rose.variable.IGNORED_BY_SECTION in var.ignored_reason: var.ignored_reason.pop(metomi.rose.variable.IGNORED_BY_SECTION) else: @@ -213,8 +270,9 @@ def ignore_section(self, config_name, section, is_ignored, self.trigger_update(config_name) return [], [] - def remove_section(self, config_name, section, skip_update=False, - skip_undo=False): + def remove_section( + self, config_name, section, skip_update=False, skip_undo=False + ): """Remove a section from this configuration.""" config_data = self.__data.config[config_name] old_section_data = config_data.sections.now.pop(section) @@ -224,7 +282,7 @@ def remove_section(self, config_name, section, skip_update=False, namespace = old_section_data.metadata["full_ns"] ns_list = [namespace] for ns, values in list(self.__data.namespace_meta_lookup.items()): - sections = values.get('sections') + sections = values.get("sections") if sections == [section]: if ns not in ns_list: ns_list.append(ns) @@ -234,7 +292,7 @@ def remove_section(self, config_name, section, skip_update=False, metomi.rose.config_editor.STACK_ACTION_REMOVED, old_section_data.copy(), self.add_section, - (config_name, section, skip_update) + (config_name, section, skip_update), ) for ns in ns_list: self.kill_page_func(ns) @@ -244,8 +302,14 @@ def remove_section(self, config_name, section, skip_update=False, self.trigger_reload_tree(only_this_namespace=namespace) return ns_list - def set_section_comments(self, config_name, section, comments, - skip_update=False, skip_undo=False): + def set_section_comments( + self, + config_name, + section, + comments, + skip_update=False, + skip_undo=False, + ): """Change the comments field for the section object.""" config_data = self.__data.config[config_name] sect_data = config_data.sections.now[section] @@ -259,7 +323,7 @@ def set_section_comments(self, config_name, section, comments, metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, old_sect_data, self.set_section_comments, - (config_name, section, last_comments) + (config_name, section, last_comments), ) self.__undo_stack.append(stack_item) del self.__redo_stack[:] @@ -305,7 +369,10 @@ def get_section_changes(self, section_object): if this_section.comments != save_section.comments: return metomi.rose.config_editor.KEY_TIP_CHANGED_COMMENTS # The difference must now be in the ignored state. - if metomi.rose.variable.IGNORED_BY_SYSTEM in this_section.ignored_reason: + if ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in this_section.ignored_reason + ): return metomi.rose.config_editor.KEY_TIP_TRIGGER_IGNORED if metomi.rose.variable.IGNORED_BY_USER in this_section.ignored_reason: return metomi.rose.config_editor.KEY_TIP_USER_IGNORED diff --git a/metomi/rose/config_editor/ops/variable.py b/metomi/rose/config_editor/ops/variable.py index 18776960c..1a5554da1 100644 --- a/metomi/rose/config_editor/ops/variable.py +++ b/metomi/rose/config_editor/ops/variable.py @@ -34,15 +34,21 @@ class VariableOperations(object): - """A class to hold functions that act on variables and their storage.""" - def __init__(self, data, util, reporter, undo_stack, redo_stack, - add_section_func, - check_cannot_enable_func=metomi.rose.config_editor.false_function, - update_ns_func=metomi.rose.config_editor.false_function, - ignore_update_func=metomi.rose.config_editor.false_function, - search_id_func=metomi.rose.config_editor.false_function): + def __init__( + self, + data, + util, + reporter, + undo_stack, + redo_stack, + add_section_func, + check_cannot_enable_func=metomi.rose.config_editor.false_function, + update_ns_func=metomi.rose.config_editor.false_function, + ignore_update_func=metomi.rose.config_editor.false_function, + search_id_func=metomi.rose.config_editor.false_function, + ): self.__data = data self.__util = util self.__reporter = reporter @@ -57,29 +63,30 @@ def __init__(self, data, util, reporter, undo_stack, redo_stack, def _get_proper_variable(self, possible_copy_variable): # Some variables are just copies, and changes to them # won't affect anything. We need to look up the 'real' variable. - namespace = possible_copy_variable.metadata.get('full_ns') + namespace = possible_copy_variable.metadata.get("full_ns") config_name = self.__util.split_full_ns(self.__data, namespace)[0] - var_id = possible_copy_variable.metadata['id'] + var_id = possible_copy_variable.metadata["id"] return self.__data.helper.get_ns_variable(var_id, config_name) def add_var(self, variable, skip_update=False, skip_undo=False): """Add a variable to the internal list.""" - namespace = variable.metadata.get('full_ns') - var_id = variable.metadata['id'] + namespace = variable.metadata.get("full_ns") + var_id = variable.metadata["id"] sect, opt = self.__util.get_section_option_from_id(var_id) config_name = self.__util.split_full_ns(self.__data, namespace)[0] config_data = self.__data.config[config_name] old_metadata = copy.deepcopy(variable.metadata) flags = self.__data.load_option_flags(config_name, sect, opt) variable.flags.update(flags) - metadata = self.__data.helper.get_metadata_for_config_id(var_id, - config_name) + metadata = self.__data.helper.get_metadata_for_config_id( + var_id, config_name + ) variable.process_metadata(metadata) variable.metadata.update(old_metadata) variables = config_data.vars.now.get(sect, []) copy_var = variable.copy() - v_id = variable.metadata.get('id') - if v_id in [v.metadata.get('id') for v in variables]: + v_id = variable.metadata.get("id") + if v_id in [v.metadata.get("id") for v in variables]: # This is the case of adding a blank variable and # renaming it to an existing variable's name. # At the moment, assume this should just be skipped. @@ -88,8 +95,11 @@ def add_var(self, variable, skip_update=False, skip_undo=False): group = None if sect not in config_data.sections.now: start_stack_index = len(self.__undo_stack) - group = (metomi.rose.config_editor.STACK_GROUP_ADD + "-" + - str(time.time())) + group = ( + metomi.rose.config_editor.STACK_GROUP_ADD + + "-" + + str(time.time()) + ) self.__add_section_func(config_name, sect) for item in self.__undo_stack[start_stack_index:]: item.group = group @@ -102,24 +112,25 @@ def add_var(self, variable, skip_update=False, skip_undo=False): if not skip_undo: self.__undo_stack.append( metomi.rose.config_editor.stack.StackItem( - variable.metadata['full_ns'], + variable.metadata["full_ns"], metomi.rose.config_editor.STACK_ACTION_ADDED, copy_var, self.remove_var, [copy_var, skip_update], - group=group) + group=group, + ) ) del self.__redo_stack[:] if not skip_update: - self.trigger_update(variable.metadata['full_ns']) - return variable.metadata['full_ns'] + self.trigger_update(variable.metadata["full_ns"]) + return variable.metadata["full_ns"] def remove_var(self, variable, skip_update=False, skip_undo=False): """Remove the variable entry from the internal lists.""" variable = self._get_proper_variable(variable) variable.error = {} # Kill any metadata errors before removing. - namespace = variable.metadata.get('full_ns') - var_id = variable.metadata['id'] + namespace = variable.metadata.get("full_ns") + var_id = variable.metadata["id"] sect = self.__util.get_section_option_from_id(var_id)[0] config_name = self.__util.split_full_ns(self.__data, namespace)[0] config_data = self.__data.config[config_name] @@ -141,15 +152,17 @@ def remove_var(self, variable, skip_update=False, skip_undo=False): copy_var = variable.copy() self.__undo_stack.append( metomi.rose.config_editor.stack.StackItem( - variable.metadata['full_ns'], + variable.metadata["full_ns"], metomi.rose.config_editor.STACK_ACTION_REMOVED, copy_var, self.add_var, - [copy_var, skip_update])) + [copy_var, skip_update], + ) + ) del self.__redo_stack[:] if not skip_update: - self.trigger_update(variable.metadata['full_ns']) - return variable.metadata['full_ns'] + self.trigger_update(variable.metadata["full_ns"]) + return variable.metadata["full_ns"] def fix_var_ignored(self, variable): """Fix any variable ignore state errors.""" @@ -160,18 +173,28 @@ def fix_var_ignored(self, variable): # Preserve section-ignored status. new_reason_dict.setdefault( metomi.rose.variable.IGNORED_BY_SECTION, - old_reason[metomi.rose.variable.IGNORED_BY_SECTION]) + old_reason[metomi.rose.variable.IGNORED_BY_SECTION], + ) if metomi.rose.variable.IGNORED_BY_SYSTEM in ignored_reasons: # Doc table I_t - if metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + if ( + metomi.rose.config_editor.WARNING_TYPE_ENABLED + in variable.error + ): # Enable new_reason_dict. # Doc table I_t -> E pass - if metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + if ( + metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER + in variable.error + ): pass elif metomi.rose.variable.IGNORED_BY_USER in ignored_reasons: # Doc table I_u - if metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED in variable.error: + if ( + metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED + in variable.error + ): # Enable new_reason_dict. # Doc table I_u -> I_t -> *, # I_u -> E -> compulsory, @@ -179,14 +202,26 @@ def fix_var_ignored(self, variable): pass else: # Doc table E - if metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error: + if ( + metomi.rose.config_editor.WARNING_TYPE_ENABLED + in variable.error + ): # Doc table E -> I_t -> * - new_reason_dict = {metomi.rose.variable.IGNORED_BY_SYSTEM: - metomi.rose.config_editor.IGNORED_STATUS_MANUAL} + new_reason_dict = { + metomi.rose.variable.IGNORED_BY_SYSTEM: ( + metomi.rose.config_editor.IGNORED_STATUS_MANUAL + ) + } self.set_var_ignored(variable, new_reason_dict) - def set_var_ignored(self, variable, new_reason_dict=None, override=False, - skip_update=False, skip_undo=False): + def set_var_ignored( + self, + variable, + new_reason_dict=None, + override=False, + skip_update=False, + skip_undo=False, + ): """Set the ignored flag data for the variable. new_reason_dict replaces the variable.ignored_reason attribute, @@ -200,7 +235,8 @@ def set_var_ignored(self, variable, new_reason_dict=None, override=False, if metomi.rose.variable.IGNORED_BY_SECTION in old_reason: new_reason_dict.setdefault( metomi.rose.variable.IGNORED_BY_SECTION, - old_reason[metomi.rose.variable.IGNORED_BY_SECTION]) + old_reason[metomi.rose.variable.IGNORED_BY_SECTION], + ) if metomi.rose.variable.IGNORED_BY_SECTION not in old_reason: if metomi.rose.variable.IGNORED_BY_SECTION in new_reason_dict: new_reason_dict.pop(metomi.rose.variable.IGNORED_BY_SECTION) @@ -209,12 +245,18 @@ def set_var_ignored(self, variable, new_reason_dict=None, override=False, # No practical difference, so don't do anything. return None # Protect against user-enabling of triggered ignored. - if (not override and - metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason and - metomi.rose.variable.IGNORED_BY_SYSTEM not in new_reason_dict): - if metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER in variable.error: + if ( + not override + and metomi.rose.variable.IGNORED_BY_SYSTEM in old_reason + and metomi.rose.variable.IGNORED_BY_SYSTEM not in new_reason_dict + ): + if ( + metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER + in variable.error + ): variable.error.pop( - metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER) + metomi.rose.config_editor.WARNING_TYPE_NOT_TRIGGER + ) my_ignored_keys = list(variable.ignored_reason.keys()) if metomi.rose.variable.IGNORED_BY_SECTION in my_ignored_keys: my_ignored_keys.remove(metomi.rose.variable.IGNORED_BY_SECTION) @@ -223,9 +265,14 @@ def set_var_ignored(self, variable, new_reason_dict=None, override=False, old_ignored_keys.remove(metomi.rose.variable.IGNORED_BY_SECTION) if len(my_ignored_keys) > len(old_ignored_keys): action_text = metomi.rose.config_editor.STACK_ACTION_IGNORED - if (not old_ignored_keys and - metomi.rose.config_editor.WARNING_TYPE_ENABLED in variable.error): - variable.error.pop(metomi.rose.config_editor.WARNING_TYPE_ENABLED) + if ( + not old_ignored_keys + and metomi.rose.config_editor.WARNING_TYPE_ENABLED + in variable.error + ): + variable.error.pop( + metomi.rose.config_editor.WARNING_TYPE_ENABLED + ) else: action_text = metomi.rose.config_editor.STACK_ACTION_ENABLED if not my_ignored_keys: @@ -236,20 +283,22 @@ def set_var_ignored(self, variable, new_reason_dict=None, override=False, copy_var = variable.copy() self.__undo_stack.append( metomi.rose.config_editor.stack.StackItem( - variable.metadata['full_ns'], + variable.metadata["full_ns"], action_text, copy_var, self.set_var_ignored, - [copy_var, old_reason, True]) + [copy_var, old_reason, True], + ) ) del self.__redo_stack[:] self.trigger_ignored_update(variable) if not skip_update: - self.trigger_update(variable.metadata['full_ns']) - return variable.metadata['full_ns'] + self.trigger_update(variable.metadata["full_ns"]) + return variable.metadata["full_ns"] - def set_var_value(self, variable, new_value, skip_update=False, - skip_undo=False): + def set_var_value( + self, variable, new_value, skip_update=False, skip_undo=False + ): """Set the value of the variable.""" variable = self._get_proper_variable(variable) if variable.value == new_value: @@ -261,19 +310,21 @@ def set_var_value(self, variable, new_value, skip_update=False, copy_var = variable.copy() self.__undo_stack.append( metomi.rose.config_editor.stack.StackItem( - variable.metadata['full_ns'], + variable.metadata["full_ns"], metomi.rose.config_editor.STACK_ACTION_CHANGED, copy_var, self.set_var_value, - [copy_var, copy_var.old_value]) + [copy_var, copy_var.old_value], + ) ) del self.__redo_stack[:] if not skip_update: - self.trigger_update(variable.metadata['full_ns']) - return variable.metadata['full_ns'] + self.trigger_update(variable.metadata["full_ns"]) + return variable.metadata["full_ns"] - def set_var_comments(self, variable, comments, - skip_update=False, skip_undo=False): + def set_var_comments( + self, variable, comments, skip_update=False, skip_undo=False + ): """Set the comments field for the variable.""" variable = self._get_proper_variable(variable) copy_variable = variable.copy() @@ -282,60 +333,65 @@ def set_var_comments(self, variable, comments, if not skip_undo: self.__undo_stack.append( metomi.rose.config_editor.stack.StackItem( - variable.metadata['full_ns'], + variable.metadata["full_ns"], metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS, copy_variable, self.set_var_comments, - [copy_variable, old_comments]) + [copy_variable, old_comments], + ) ) del self.__redo_stack[:] if not skip_update: - self.trigger_update(variable.metadata['full_ns']) - return variable.metadata['full_ns'] + self.trigger_update(variable.metadata["full_ns"]) + return variable.metadata["full_ns"] def get_var_original_comments(self, variable): """Get the original comments, if any.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] - save_var = self.__data.helper.get_variable_by_id(var_id, config_name, - save=True) + save_var = self.__data.helper.get_variable_by_id( + var_id, config_name, save=True + ) if save_var is None: return None return save_var.comments def get_var_original_ignore(self, variable): """Get the original value, if any.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] - save_var = self.__data.helper.get_variable_by_id(var_id, config_name, - save=True) + save_var = self.__data.helper.get_variable_by_id( + var_id, config_name, save=True + ) if save_var is None: return None return save_var.ignored_reason def get_var_original_value(self, variable): """Get the original value, if any.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] save_variable = self.__data.helper.get_variable_by_id( - var_id, config_name, save=True) + var_id, config_name, save=True + ) if save_variable is None: return None return save_variable.value def is_var_modified(self, variable): """Check against the last saved variable reference.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] - this_variable = self.__data.helper.get_variable_by_id(var_id, - config_name) - save_variable = self.__data.helper.get_variable_by_id(var_id, - config_name, - save=True) + this_variable = self.__data.helper.get_variable_by_id( + var_id, config_name + ) + save_variable = self.__data.helper.get_variable_by_id( + var_id, config_name, save=True + ) if this_variable is None: # Ghost variable, check absence from saved list. if save_variable is not None: @@ -348,22 +404,23 @@ def is_var_modified(self, variable): def is_var_added(self, variable): """Check if missing from the saved variables list.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] - save_variable = self.__data.helper.get_variable_by_id(var_id, - config_name, - save=True) + save_variable = self.__data.helper.get_variable_by_id( + var_id, config_name, save=True + ) return save_variable is None def is_var_ghost(self, variable): """Check if the variable is a latent variable.""" - var_id = variable.metadata['id'] - namespace = variable.metadata['full_ns'] + var_id = variable.metadata["id"] + namespace = variable.metadata["full_ns"] config_name = self.__util.split_full_ns(self.__data, namespace)[0] - this_variable = self.__data.helper.get_variable_by_id(var_id, - config_name) - return (this_variable is None) + this_variable = self.__data.helper.get_variable_by_id( + var_id, config_name + ) + return this_variable is None def get_var_changes(self, variable): """Return a description of any changed status the variable has.""" @@ -374,7 +431,9 @@ def get_var_changes(self, variable): return metomi.rose.config_editor.KEY_TIP_MISSING old_value = self.get_var_original_value(variable) if variable.value != self.get_var_original_value(variable): - return metomi.rose.config_editor.KEY_TIP_CHANGED.format(old_value) + return metomi.rose.config_editor.KEY_TIP_CHANGED.format( + old_value + ) if self.get_var_original_comments(variable) != variable.comments: return metomi.rose.config_editor.KEY_TIP_CHANGED_COMMENTS if not variable.ignored_reason: @@ -382,17 +441,25 @@ def get_var_changes(self, variable): old_ignore = self.get_var_original_ignore(variable) if len(old_ignore) > len(variable.ignored_reason): return metomi.rose.config_editor.KEY_TIP_ENABLED - if (metomi.rose.variable.IGNORED_BY_SYSTEM in variable.ignored_reason and - metomi.rose.variable.IGNORED_BY_SYSTEM not in old_ignore): + if ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in variable.ignored_reason + and metomi.rose.variable.IGNORED_BY_SYSTEM not in old_ignore + ): return metomi.rose.config_editor.KEY_TIP_TRIGGER_IGNORED - if (metomi.rose.variable.IGNORED_BY_USER in variable.ignored_reason and - metomi.rose.variable.IGNORED_BY_USER not in old_ignore): + if ( + metomi.rose.variable.IGNORED_BY_USER in variable.ignored_reason + and metomi.rose.variable.IGNORED_BY_USER not in old_ignore + ): return metomi.rose.config_editor.KEY_TIP_USER_IGNORED - if (metomi.rose.variable.IGNORED_BY_SECTION in variable.ignored_reason and - metomi.rose.variable.IGNORED_BY_SECTION not in old_ignore): + if ( + metomi.rose.variable.IGNORED_BY_SECTION + in variable.ignored_reason + and metomi.rose.variable.IGNORED_BY_SECTION not in old_ignore + ): return metomi.rose.config_editor.KEY_TIP_SECTION_IGNORED return metomi.rose.config_editor.KEY_TIP_ENABLED - return '' + return "" def launch_url(self, variable): """Determine and launch the variable help URL in a web browser.""" @@ -418,7 +485,8 @@ def _launch_url(self, url): def search_for_var(self, config_name_or_namespace, setting_id): """Launch a search for a setting or variable id.""" config_name = self.__util.split_full_ns( - self.__data, config_name_or_namespace)[0] + self.__data, config_name_or_namespace + )[0] self.search_id_func(config_name, setting_id) def get_ns_metadata_files(self, namespace): @@ -430,5 +498,6 @@ def get_sections(self, namespace): """Retrieve all real sections (empty or not) for this ns's config.""" config_name = self.__util.split_full_ns(self.__data, namespace)[0] section_objects = self.__data.config[config_name].sections.get_all( - skip_latent=True) + skip_latent=True + ) return [_.name for _ in section_objects] diff --git a/metomi/rose/config_editor/page.py b/metomi/rose/config_editor/page.py index f6b1a537d..6334c0f62 100644 --- a/metomi/rose/config_editor/page.py +++ b/metomi/rose/config_editor/page.py @@ -23,8 +23,9 @@ import webbrowser import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk from gi.repository import Pango import metomi.rose.config_editor.panelwidget @@ -42,35 +43,50 @@ class ConfigPage(Gtk.Box): - """Returns a container for a tab.""" - def __init__(self, page_metadata, config_data, ghost_data, section_ops, - variable_ops, sections, latent_sections, get_formats_func, - reporter, directory=None, sub_data=None, sub_ops=None, - launch_info_func=None, launch_edit_func=None, - launch_macro_func=None): - super(ConfigPage, self).__init__(homogeneous=False, orientation=Gtk.Orientation.VERTICAL) - self.namespace = page_metadata.get('namespace') - self.ns_is_default = page_metadata.get('ns_is_default') - self.config_name = page_metadata.get('config_name') - self.label = page_metadata.get('label') - self.description = page_metadata.get('description') - self.help = page_metadata.get('help') - self.url = page_metadata.get('url') - self.see_also = page_metadata.get('see_also') - self.custom_macros = page_metadata.get('macro', {}) - self.custom_widget = page_metadata.get('widget') - self.custom_sub_widget = page_metadata.get('widget_sub_ns') - self.show_modes = page_metadata.get('show_modes') - self.is_duplicate = (page_metadata.get('duplicate') == - metomi.rose.META_PROP_VALUE_TRUE) + def __init__( + self, + page_metadata, + config_data, + ghost_data, + section_ops, + variable_ops, + sections, + latent_sections, + get_formats_func, + reporter, + directory=None, + sub_data=None, + sub_ops=None, + launch_info_func=None, + launch_edit_func=None, + launch_macro_func=None, + ): + super(ConfigPage, self).__init__( + homogeneous=False, orientation=Gtk.Orientation.VERTICAL + ) + self.namespace = page_metadata.get("namespace") + self.ns_is_default = page_metadata.get("ns_is_default") + self.config_name = page_metadata.get("config_name") + self.label = page_metadata.get("label") + self.description = page_metadata.get("description") + self.help = page_metadata.get("help") + self.url = page_metadata.get("url") + self.see_also = page_metadata.get("see_also") + self.custom_macros = page_metadata.get("macro", {}) + self.custom_widget = page_metadata.get("widget") + self.custom_sub_widget = page_metadata.get("widget_sub_ns") + self.show_modes = page_metadata.get("show_modes") + self.is_duplicate = ( + page_metadata.get("duplicate") == metomi.rose.META_PROP_VALUE_TRUE + ) self.section = None if sections: self.section = sections[0] self.sections = sections self.latent_sections = latent_sections - self.icon_path = page_metadata.get('icon') + self.icon_path = page_metadata.get("icon") self.reporter = reporter self.directory = directory self.sub_data = sub_data @@ -78,23 +94,24 @@ def __init__(self, page_metadata, config_data, ghost_data, section_ops, self.launch_info = launch_info_func self.launch_edit = launch_edit_func self._launch_macro_func = launch_macro_func - namespaces = self.namespace.strip('/').split('/') + namespaces = self.namespace.strip("/").split("/") namespaces.reverse() self.info = "" if self.description is None: self.info = " - ".join(namespaces[:-1]) else: - if self.description != '': - self.info = self.description + '\n' + if self.description != "": + self.info = self.description + "\n" self.info += " - ".join(namespaces[:-1]) - if self.see_also != '': - self.info += '\n => ' + self.see_also + if self.see_also != "": + self.info += "\n => " + self.see_also self.panel_data = config_data self.ghost_data = ghost_data self.section_ops = section_ops self.variable_ops = variable_ops - self.trigger_ask_for_config_keys = ( - lambda: get_formats_func(self.config_name)) + self.trigger_ask_for_config_keys = lambda: get_formats_func( + self.config_name + ) self.sort_data() self.sort_data(ghost=True) self._last_info_labels = None @@ -106,21 +123,29 @@ def get_page(self): """Generate a container of widgets for page content and a label.""" self.labelwidget = self.get_label_widget() self.scrolled_main_window = Gtk.ScrolledWindow() - self.scrolled_main_window.set_policy(Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC) + self.scrolled_main_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) self.scrolled_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.scrolled_vbox.show() self.scrolled_main_window.add_with_viewport(self.scrolled_vbox) - self.scrolled_main_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) + self.scrolled_main_window.get_child().set_shadow_type( + Gtk.ShadowType.NONE + ) self.scrolled_main_window.set_margin_start( - metomi.rose.config_editor.SPACING_SUB_PAGE) # left + metomi.rose.config_editor.SPACING_SUB_PAGE + ) # left self.scrolled_main_window.set_margin_end( - metomi.rose.config_editor.SPACING_SUB_PAGE) # right - self.scrolled_vbox.pack_start(self.main_container, - expand=False, fill=True, padding=0) + metomi.rose.config_editor.SPACING_SUB_PAGE + ) # right + self.scrolled_vbox.pack_start( + self.main_container, expand=False, fill=True, padding=0 + ) self.scrolled_main_window.show() self.main_vpaned = Gtk.VPaned() - self.info_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False) + self.info_panel = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, homogeneous=False + ) self.info_panel.show() self.update_info() second_panel = None @@ -132,18 +157,23 @@ def get_page(self): second_panel = self.sub_data_panel self.vpaned = Gtk.VPaned() if self.panel_data: - self.vpaned.pack1(self.scrolled_main_window, resize=True, - shrink=True) + self.vpaned.pack1( + self.scrolled_main_window, resize=True, shrink=True + ) if second_panel is not None: self.vpaned.pack2(second_panel, resize=False, shrink=True) elif second_panel is not None: - self.vpaned.pack1(self.scrolled_main_window, resize=False, - shrink=True) + self.vpaned.pack1( + self.scrolled_main_window, resize=False, shrink=True + ) self.vpaned.pack2(second_panel, resize=True, shrink=True) - self.vpaned.set_position(metomi.rose.config_editor.FILE_PANEL_EXPAND) + self.vpaned.set_position( + metomi.rose.config_editor.FILE_PANEL_EXPAND + ) else: - self.vpaned.pack1(self.scrolled_main_window, resize=True, - shrink=True) + self.vpaned.pack1( + self.scrolled_main_window, resize=True, shrink=True + ) self.vpaned.show() self.main_vpaned.pack2(self.vpaned) self.main_vpaned.show() @@ -151,8 +181,8 @@ def get_page(self): self.show() self.scroll_vadj = self.scrolled_main_window.get_vadjustment() self.scrolled_main_window.connect( - "button-press-event", - self._handle_click_main_window) + "button-press-event", self._handle_click_main_window + ) def _handle_click_main_window(self, widget, event): if event.button != 3: @@ -163,9 +193,9 @@ def _handle_click_main_window(self, widget, event): def get_label_widget(self, is_detached=False): """Return a container of widgets for the notebook tab label.""" if is_detached: - location = self.config_name.lstrip('/').split('/') + location = self.config_name.lstrip("/").split("/") location.reverse() - label = Gtk.Label(label=' - '.join([self.label] + location)) + label = Gtk.Label(label=" - ".join([self.label] + location)) self.is_detached = True else: label = Gtk.Label(label=self.label) @@ -175,30 +205,45 @@ def get_label_widget(self, is_detached=False): label_event_box.add(label) label_event_box.show() if self.help or self.url: - label_event_box.connect("enter-notify-event", - self._handle_enter_label) - label_event_box.connect("leave-notify-event", - self._handle_leave_label) - label_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False) + label_event_box.connect( + "enter-notify-event", self._handle_enter_label + ) + label_event_box.connect( + "leave-notify-event", self._handle_leave_label + ) + label_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False + ) if self.icon_path is not None: self.label_icon = Gtk.Image() self.label_icon.set_from_file(self.icon_path) self.label_icon.show() - label_box.pack_start(self.label_icon, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + label_box.pack_start( + self.label_icon, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) close_button = metomi.rose.gtk.util.CustomButton( - stock_id=Gtk.STOCK_CLOSE, size=Gtk.IconSize.MENU, as_tool=True) + stock_id=Gtk.STOCK_CLOSE, size=Gtk.IconSize.MENU, as_tool=True + ) Gtk.Widget.set_name(close_button, "page-tab-button") - label_box.pack_start(label_event_box, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + label_box.pack_start( + label_event_box, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) if not is_detached: - label_box.pack_end(close_button, expand=False, fill=False, padding=0) + label_box.pack_end( + close_button, expand=False, fill=False, padding=0 + ) label_box.show() event_box = Gtk.EventBox() event_box.add(label_box) - close_button.connect('released', lambda b: self.close_self()) - event_box.connect('button_press_event', self._handle_click_tab) + close_button.connect("released", lambda b: self.close_self()) + event_box.connect("button_press_event", self._handle_click_tab) event_box.show() if self.info is not None: event_box.connect("enter-notify-event", self._set_tab_tooltip) @@ -217,8 +262,9 @@ def _handle_leave_label(self, label_event_box, event=None): att_list = label.get_attributes() if att_list is None: att_list = Pango.AttrList() - att_list = att_list.filter(lambda a: - a.klass.type != Pango.AttrType.UNDERLINE) + att_list = att_list.filter( + lambda a: a.klass.type != Pango.AttrType.UNDERLINE + ) if att_list is None: # This is messy but necessary. att_list = Pango.AttrList() @@ -256,13 +302,25 @@ def launch_tab_menu(self, event): ui_config_string_start += """ """ actions = [ - ('Open', Gtk.STOCK_NEW, metomi.rose.config_editor.TAB_MENU_OPEN_NEW), - ('Info', Gtk.STOCK_INFO, metomi.rose.config_editor.TAB_MENU_INFO), - ('Edit', Gtk.STOCK_EDIT, metomi.rose.config_editor.TAB_MENU_EDIT), - ('Help', Gtk.STOCK_HELP, metomi.rose.config_editor.TAB_MENU_HELP), - ('Web_Help', Gtk.STOCK_HOME, - metomi.rose.config_editor.TAB_MENU_WEB_HELP), - ('Close', Gtk.STOCK_CLOSE, metomi.rose.config_editor.TAB_MENU_CLOSE)] + ( + "Open", + Gtk.STOCK_NEW, + metomi.rose.config_editor.TAB_MENU_OPEN_NEW, + ), + ("Info", Gtk.STOCK_INFO, metomi.rose.config_editor.TAB_MENU_INFO), + ("Edit", Gtk.STOCK_EDIT, metomi.rose.config_editor.TAB_MENU_EDIT), + ("Help", Gtk.STOCK_HELP, metomi.rose.config_editor.TAB_MENU_HELP), + ( + "Web_Help", + Gtk.STOCK_HOME, + metomi.rose.config_editor.TAB_MENU_WEB_HELP, + ), + ( + "Close", + Gtk.STOCK_CLOSE, + metomi.rose.config_editor.TAB_MENU_CLOSE, + ), + ] if self.help is not None: help_string = """ """ @@ -273,27 +331,28 @@ def launch_tab_menu(self, event): ui_config_string_end = url_string + ui_config_string_end uimanager = Gtk.UIManager() - actiongroup = Gtk.ActionGroup('Popup') + actiongroup = Gtk.ActionGroup("Popup") actiongroup.add_actions(actions) uimanager.insert_action_group(actiongroup) - uimanager.add_ui_from_string(ui_config_string_start + - ui_config_string_end) + uimanager.add_ui_from_string( + ui_config_string_start + ui_config_string_end + ) if not self.is_detached: - window_item = uimanager.get_widget('/Popup/Open') + window_item = uimanager.get_widget("/Popup/Open") window_item.connect("activate", self.trigger_tab_detach) - close_item = uimanager.get_widget('/Popup/Close') + close_item = uimanager.get_widget("/Popup/Close") close_item.connect("activate", lambda b: self.close_self()) - edit_item = uimanager.get_widget('/Popup/Edit') + edit_item = uimanager.get_widget("/Popup/Edit") edit_item.connect("activate", lambda b: self.launch_edit()) - info_item = uimanager.get_widget('/Popup/Info') + info_item = uimanager.get_widget("/Popup/Info") info_item.connect("activate", lambda b: self.launch_info()) if self.help is not None: - help_item = uimanager.get_widget('/Popup/Help') + help_item = uimanager.get_widget("/Popup/Help") help_item.connect("activate", lambda b: self.launch_help()) if self.url is not None: - url_item = uimanager.get_widget('/Popup/Web_Help') + url_item = uimanager.get_widget("/Popup/Web_Help") url_item.connect("activate", lambda b: self.launch_url()) - tab_menu = uimanager.get_widget('/Popup') + tab_menu = uimanager.get_widget("/Popup") tab_menu.popup_at_pointer(event) return False @@ -305,7 +364,11 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): """Reshuffle widgets for detached view.""" focus_child = self.get_focus_child() button_hbox = Gtk.Box(homogeneous=False, spacing=0) - self.tool_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0) + self.tool_hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + homogeneous=False, + spacing=0, + ) sep = Gtk.VSeparator() sep.show() sep_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) @@ -315,38 +378,55 @@ def reshuffle_for_detached(self, add_button, revert_button, parent): info_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, as_tool=True, - tip_text=metomi.rose.config_editor.TAB_MENU_INFO) + tip_text=metomi.rose.config_editor.TAB_MENU_INFO, + ) info_button.connect("clicked", lambda m: self.launch_info()) help_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_HELP, as_tool=True, - tip_text=metomi.rose.config_editor.TAB_MENU_HELP) + tip_text=metomi.rose.config_editor.TAB_MENU_HELP, + ) help_button.connect("clicked", self.launch_help) url_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_HOME, as_tool=True, - tip_text=metomi.rose.config_editor.TAB_MENU_WEB_HELP) + tip_text=metomi.rose.config_editor.TAB_MENU_WEB_HELP, + ) url_button.connect("clicked", self.launch_url) button_hbox.pack_start(add_button, expand=False, fill=False, padding=0) - button_hbox.pack_start(revert_button, expand=False, fill=False, padding=0) + button_hbox.pack_start( + revert_button, expand=False, fill=False, padding=0 + ) button_hbox.pack_start(sep_vbox, expand=False, fill=False, padding=0) - button_hbox.pack_start(info_button, expand=False, fill=False, padding=0) + button_hbox.pack_start( + info_button, expand=False, fill=False, padding=0 + ) if self.help is not None: - button_hbox.pack_start(help_button, expand=False, fill=False, padding=0) + button_hbox.pack_start( + help_button, expand=False, fill=False, padding=0 + ) if self.url is not None: - button_hbox.pack_start(url_button, expand=False, fill=False, padding=0) + button_hbox.pack_start( + url_button, expand=False, fill=False, padding=0 + ) button_hbox.show() button_frame = Gtk.Frame() button_frame.set_shadow_type(Gtk.ShadowType.NONE) button_frame.add(button_hbox) button_frame.show() - self.tool_hbox.pack_start(button_frame, expand=False, fill=False, padding=0) - label_box = Gtk.Box(homogeneous=False, - spacing=metomi.rose.config_editor.SPACING_PAGE) - label_box.pack_start(self.get_label_widget(is_detached=True), False, False, 0) + self.tool_hbox.pack_start( + button_frame, expand=False, fill=False, padding=0 + ) + label_box = Gtk.Box( + homogeneous=False, spacing=metomi.rose.config_editor.SPACING_PAGE + ) + label_box.pack_start( + self.get_label_widget(is_detached=True), False, False, 0 + ) label_box.show() self.tool_hbox.pack_start( - label_box, expand=True, fill=True, padding=10) + label_box, expand=True, fill=True, padding=10 + ) self.tool_hbox.show() self.pack_start(self.tool_hbox, expand=False, fill=False, padding=0) self.reorder_child(self.tool_hbox, 0) @@ -363,13 +443,14 @@ def close_self(self): parent = self.get_parent() my_index = parent.get_page_ids().index(self.namespace) parent.remove_page(my_index) - parent.emit('select-page', False) + parent.emit("select-page", False) def launch_help(self, *args): """Launch the page help.""" title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format(self.label) metomi.rose.gtk.dialog.run_hyperlink_dialog( - Gtk.STOCK_DIALOG_INFO, str(self.help), title) + Gtk.STOCK_DIALOG_INFO, str(self.help), title + ) def launch_url(self, *args): """Launch the page url help.""" @@ -378,12 +459,16 @@ def launch_url(self, *args): def update_info(self): """Driver routine to update non-variable information.""" button_list, label_list, info = self._get_page_info_widgets() - if [l.get_text() for l in label_list] == self._last_info_labels: + if [ + label.get_text() for label in label_list + ] == self._last_info_labels: # No change - do not redraw. return False self.generate_page_info(button_list, label_list, info) - has_content = (self.info_panel.get_children() and - self.info_panel.get_children()[0].get_children()) + has_content = ( + self.info_panel.get_children() + and self.info_panel.get_children()[0].get_children() + ) if self.info_panel in self.main_vpaned.get_children(): if not has_content: self.main_vpaned.remove(self.info_panel) @@ -392,31 +477,46 @@ def update_info(self): def generate_page_info(self, button_list=None, label_list=None, info=None): """Generate a widget giving information about sections.""" - info_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False) + info_container = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, homogeneous=False + ) info_container.show() if button_list is None or label_list is None or info is None: button_list, label_list, info = self._get_page_info_widgets() - self._last_info_labels = [l.get_text() for l in label_list] + self._last_info_labels = [label.get_text() for label in label_list] for button, label in zip(button_list, label_list): - var_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False) + var_hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False + ) var_hbox.pack_start(button, expand=False, fill=False, padding=0) - var_hbox.pack_start(label, expand=False, fill=True, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + var_hbox.pack_start( + label, + expand=False, + fill=True, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) var_hbox.show() - info_container.pack_start(var_hbox, expand=False, fill=True, padding=0) + info_container.pack_start( + var_hbox, expand=False, fill=True, padding=0 + ) # Add page help. if self.description: help_label = metomi.rose.gtk.util.get_hyperlink_label( - self.description, search_func=self.search_for_id) + self.description, search_func=self.search_for_id + ) help_label_window = Gtk.ScrolledWindow() - help_label_window.set_policy(Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC) + help_label_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) help_label_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - help_label_hbox.pack_start(help_label, expand=False, fill=False, padding=0) + help_label_hbox.pack_start( + help_label, expand=False, fill=False, padding=0 + ) help_label_hbox.show() help_label_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) help_label_vbox.pack_start( - help_label_hbox, expand=False, fill=False, padding=0) + help_label_hbox, expand=False, fill=False, padding=0 + ) help_label_vbox.show() help_label_window.add_with_viewport(help_label_vbox) help_label_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) @@ -425,37 +525,55 @@ def generate_page_info(self, button_list=None, label_list=None, info=None): height = help_label_window.get_preferred_size().natural_size.height if info == "Blank page - no data": self.main_vpaned.set_position( - metomi.rose.config_editor.SIZE_WINDOW[1] * 100) + metomi.rose.config_editor.SIZE_WINDOW[1] * 100 + ) else: - height = min([metomi.rose.config_editor.SIZE_WINDOW[1] / 3, - help_label.get_preferred_size().natural_size.height]) + height = min( + [ + metomi.rose.config_editor.SIZE_WINDOW[1] / 3, + help_label.get_preferred_size().natural_size.height, + ] + ) help_label_window.set_size_request(width, height) help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - help_hbox.pack_start(help_label_window, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + help_hbox.pack_start( + help_label_window, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) help_hbox.show() info_container.pack_start( - help_hbox, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + help_hbox, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) for child in self.info_panel.get_children(): self.info_panel.remove(child) - self.info_panel.pack_start(info_container, expand=True, fill=True, padding=0) + self.info_panel.pack_start( + info_container, expand=True, fill=True, padding=0 + ) def generate_filesystem_panel(self): """Generate a widget to view the file hierarchy.""" self.filesystem_panel = ( metomi.rose.config_editor.panelwidget.filesystem.FileSystemPanel( - self.directory)) + self.directory + ) + ) def generate_sub_data_panel(self, override_custom=False): """Generate a panel giving a summary of other page data.""" - args = (self.sub_data["sections"], - self.sub_data["variables"], - self.section_ops, - self.variable_ops, - self.search_for_id, - self.sub_ops, - self.is_duplicate) + args = ( + self.sub_data["sections"], + self.sub_data["variables"], + self.section_ops, + self.variable_ops, + self.search_for_id, + self.sub_ops, + self.is_duplicate, + ) if self.custom_sub_widget is not None and not override_custom: widget_name_args = self.custom_sub_widget.split(None, 1) if len(widget_name_args) > 1: @@ -463,20 +581,26 @@ def generate_sub_data_panel(self, override_custom=False): else: widget_path, widget_args = widget_name_args[0], None metadata_files = self.section_ops.get_ns_metadata_files( - self.namespace) + self.namespace + ) widget_dir = metomi.rose.META_DIR_WIDGET - metadata_files.sort(key=cmp_to_key( - lambda x, y: (widget_dir in y) - (widget_dir in x))) + metadata_files.sort( + key=cmp_to_key( + lambda x, y: (widget_dir in y) - (widget_dir in x) + ) + ) prefix = re.sub(r"[^\w]", "_", self.config_name.strip("/")) prefix += "/" + metomi.rose.META_DIR_WIDGET + "/" custom_widget = metomi.rose.resource.import_object( widget_path, metadata_files, self.handle_bad_custom_sub_widget, - module_prefix=prefix) + module_prefix=prefix, + ) if custom_widget is None: text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format( - self.custom_sub_widget) + self.custom_sub_widget + ) self.handle_bad_custom_sub_widget(text) return False try: @@ -485,29 +609,31 @@ def generate_sub_data_panel(self, override_custom=False): self.handle_bad_custom_sub_widget(str(exc)) else: panel_module = metomi.rose.config_editor.panelwidget.summary_data - self.sub_data_panel = ( - panel_module.StandardSummaryDataPanel(*args)) + self.sub_data_panel = panel_module.StandardSummaryDataPanel(*args) def handle_bad_custom_sub_widget(self, error_info): - text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format( - error_info) - self.reporter( - metomi.rose.config_editor.util.ImportWidgetError(text)) + text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) + self.reporter(metomi.rose.config_editor.util.ImportWidgetError(text)) self.generate_sub_data_panel(override_custom=True) def update_sub_data(self): """Update the sub (summary) data panel.""" if self.sub_data is None: - if (hasattr(self, "sub_data_panel") and - self.sub_data_panel is not None): + if ( + hasattr(self, "sub_data_panel") + and self.sub_data_panel is not None + ): self.vpaned.remove(self.sub_data_panel) self.sub_data_panel.destroy() self.sub_data_panel = None else: - if (hasattr(self, "sub_data_panel") and - self.sub_data_panel is not None): - self.sub_data_panel.update(self.sub_data["sections"], - self.sub_data["variables"]) + if ( + hasattr(self, "sub_data_panel") + and self.sub_data_panel is not None + ): + self.sub_data_panel.update( + self.sub_data["sections"], self.sub_data["variables"] + ) def launch_add_menu(self, event): """Pop up a contextual add variable menu.""" @@ -520,14 +646,20 @@ def launch_add_menu(self, event): def get_add_menu(self): def _add_var_from_item(item): for variable in self.ghost_data: - if variable.metadata['id'] == item.var_id: + if variable.metadata["id"] == item.var_id: self.add_row(variable) return + add_ui_start = """ """ add_ui_end = """ """ - actions = [('Add meta', Gtk.STOCK_DIRECTORY, - metomi.rose.config_editor.ADD_MENU_META)] + actions = [ + ( + "Add meta", + Gtk.STOCK_DIRECTORY, + metomi.rose.config_editor.ADD_MENU_META, + ) + ] section_choices = [] for sect_data in self.sections: if not sect_data.ignored_reason: @@ -535,55 +667,66 @@ def _add_var_from_item(item): section_choices.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) if self.ns_is_default and section_choices: add_ui_start = add_ui_start.replace( - "'Popup'>", - """'Popup'>""") + "'Popup'>", """'Popup'>""" + ) text = metomi.rose.config_editor.ADD_MENU_BLANK if len(section_choices) > 1: text = metomi.rose.config_editor.ADD_MENU_BLANK_MULTIPLE - actions.insert(0, ('Add blank', Gtk.STOCK_NEW, text)) + actions.insert(0, ("Add blank", Gtk.STOCK_NEW, text)) ghost_list = [v for v in self.ghost_data] sorter = metomi.rose.config.sort_settings - ghost_list.sort(key=cmp_to_key(lambda v, w: sorter(v.metadata['id'], - w.metadata['id']))) + ghost_list.sort( + key=cmp_to_key( + lambda v, w: sorter(v.metadata["id"], w.metadata["id"]) + ) + ) for variable in ghost_list: label_text = variable.name - if (not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE] and - metomi.rose.META_PROP_TITLE in variable.metadata): + if ( + not self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_NO_TITLE + ] + and metomi.rose.META_PROP_TITLE in variable.metadata + ): label_text = variable.metadata[metomi.rose.META_PROP_TITLE] label_text = label_text.replace("_", "__") - add_ui_start += ('') - actions.append((variable.metadata['id'], None, - "_" + label_text)) + add_ui_start += ( + '' + ) + actions.append((variable.metadata["id"], None, "_" + label_text)) add_ui = add_ui_start + add_ui_end uimanager = Gtk.UIManager() - actiongroup = Gtk.ActionGroup('Popup') + actiongroup = Gtk.ActionGroup("Popup") actiongroup.add_actions(actions) uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(add_ui) - if 'Add blank' in add_ui: - blank_item = uimanager.get_widget('/Popup/Add blank') + if "Add blank" in add_ui: + blank_item = uimanager.get_widget("/Popup/Add blank") if len(section_choices) > 1: blank_item.connect( "activate", - lambda b: self._launch_section_chooser(section_choices)) + lambda b: self._launch_section_chooser(section_choices), + ) else: blank_item.connect("activate", lambda b: self.add_row()) for variable in ghost_list: named_item = uimanager.get_widget( - '/Popup/Add meta/' + variable.metadata['id']) + "/Popup/Add meta/" + variable.metadata["id"] + ) if not named_item: return None - named_item.var_id = variable.metadata['id'] + named_item.var_id = variable.metadata["id"] tooltip_text = "" - description = variable.metadata.get(metomi.rose.META_PROP_DESCRIPTION) + description = variable.metadata.get( + metomi.rose.META_PROP_DESCRIPTION + ) if description: tooltip_text += description + "\n" tooltip_text += "(" + variable.metadata["id"] + ")" named_item.set_tooltip_text(tooltip_text) named_item.connect("activate", _add_var_from_item) - if 'Add blank' in add_ui or self.ghost_data: - return uimanager.get_widget('/Popup') + if "Add blank" in add_ui or self.ghost_data: + return uimanager.get_widget("/Popup") return None def _launch_section_chooser(self, section_choices): @@ -591,7 +734,8 @@ def _launch_section_chooser(self, section_choices): section = metomi.rose.gtk.dialog.run_choices_dialog( metomi.rose.config_editor.DIALOG_LABEL_CHOOSE_SECTION_ADD_VAR, section_choices, - metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION) + metomi.rose.config_editor.DIALOG_TITLE_CHOOSE_SECTION, + ) if section is not None: self.add_row(section=section) @@ -607,27 +751,27 @@ def add_row(self, variable=None, section=None): if variable is None: if self.section is None and section is None: return False - creation_time = str(time.time()).replace('.', '_') + creation_time = str(time.time()).replace(".", "_") if section is None: sect = self.section.name else: sect = section - v_id = sect + '=null' + creation_time - variable = metomi.rose.variable.Variable('', '', - {'id': v_id, - 'full_ns': self.namespace}) + v_id = sect + "=null" + creation_time + variable = metomi.rose.variable.Variable( + "", "", {"id": v_id, "full_ns": self.namespace} + ) if section is None and self.section.ignored_reason: # Cannot add to an ignored section. return False self.variable_ops.add_var(variable) - if hasattr(self.main_container, 'add_variable_widget'): + if hasattr(self.main_container, "add_variable_widget"): self.main_container.add_variable_widget(variable) self.trigger_update_status() self.update_ignored() else: self.refresh() self.update_ignored(no_refresh=True) - self.set_main_focus(variable.metadata.get('id')) + self.set_main_focus(variable.metadata.get("id")) def generate_main_container(self, override_custom=False): """Choose a container to interface with variables in panel_data.""" @@ -638,22 +782,25 @@ def generate_main_container(self, override_custom=False): else: widget_path, widget_args = widget_name_args[0], None metadata_files = self.section_ops.get_ns_metadata_files( - self.namespace) + self.namespace + ) custom_widget = metomi.rose.resource.import_object( - widget_path, - metadata_files, - self.handle_bad_custom_main_widget) + widget_path, metadata_files, self.handle_bad_custom_main_widget + ) if custom_widget is None: text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format( - widget_path) + widget_path + ) self.handle_bad_custom_main_widget(text) return try: - self.main_container = custom_widget(self.panel_data, - self.ghost_data, - self.variable_ops, - self.show_modes, - arg_str=widget_args) + self.main_container = custom_widget( + self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes, + arg_str=widget_args, + ) except Exception as exc: self.handle_bad_custom_main_widget(exc) else: @@ -661,22 +808,26 @@ def generate_main_container(self, override_custom=False): std_table = metomi.rose.config_editor.pagewidget.table.PageTable disc_table = metomi.rose.config_editor.pagewidget.table.PageLatentTable if self.namespace == "/discovery": - self.main_container = disc_table(self.panel_data, - self.ghost_data, - self.variable_ops, - self.show_modes) + self.main_container = disc_table( + self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes, + ) else: - self.main_container = std_table(self.panel_data, - self.ghost_data, - self.variable_ops, - self.show_modes) + self.main_container = std_table( + self.panel_data, + self.ghost_data, + self.variable_ops, + self.show_modes, + ) def handle_bad_custom_main_widget(self, error_info): """Handle a bad custom page widget import.""" - text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format( - error_info) + text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) self.reporter.report( - metomi.rose.config_editor.util.ImportWidgetError(text)) + metomi.rose.config_editor.util.ImportWidgetError(text) + ) self.generate_main_container(override_custom=True) def validate_errors(self, variable_id=None): @@ -688,7 +839,7 @@ def validate_errors(self, variable_id=None): return bad_list else: for variable in self.panel_data + self.ghost_data: - if variable.metadata.get('id') == variable_id: + if variable.metadata.get("id") == variable_id: if variable.error == {}: return None return list(variable.error.items()) @@ -700,8 +851,8 @@ def choose_focus(self, focus_variable=None): return if self.show_modes[metomi.rose.config_editor.SHOW_MODE_LATENT]: for widget in self.get_main_variable_widgets(): - if hasattr(widget.get_parent(), 'variable'): - if widget.get_parent().variable.name == '': + if hasattr(widget.get_parent(), "variable"): + if widget.get_parent().variable.name == "": widget.get_parent().grab_focus() return names = [v.name for v in (self.panel_data + self.ghost_data)] @@ -709,11 +860,12 @@ def choose_focus(self, focus_variable=None): return if self.panel_data: for widget in self.get_main_variable_widgets(): - if hasattr(widget.get_parent(), 'variable'): + if hasattr(widget.get_parent(), "variable"): var = widget.get_parent().variable if var.name == focus_variable.name: - if (var.metadata.get('id') == - focus_variable.metadata.get('id')): + if var.metadata.get( + "id" + ) == focus_variable.metadata.get("id"): widget.get_parent().grab_focus() return @@ -724,18 +876,18 @@ def refresh(self, only_this_var_id=None): return self.sort_main(remake_forced=True) variable = None for variable in self.panel_data + self.ghost_data: - if variable.metadata['id'] == only_this_var_id: + if variable.metadata["id"] == only_this_var_id: break else: return self.sort_main(remake_forced=True) - var_id = variable.metadata['id'] + var_id = variable.metadata["id"] widget_for_var = {} for widget in self.get_main_variable_widgets(): - if hasattr(widget, 'variable'): - target_id = widget.variable.metadata['id'] + if hasattr(widget, "variable"): + target_id = widget.variable.metadata["id"] target_widget = widget else: - target_id = widget.get_parent().variable.metadata['id'] + target_id = widget.get_parent().variable.metadata["id"] target_widget = widget.get_parent() widget_for_var.update({target_id: target_widget}) if variable in self.panel_data: @@ -745,10 +897,12 @@ def refresh(self, only_this_var_id=None): # Then it is an added ghost variable. return self.handle_add_var_widget(variable) # Then it has an existing variable widget. - if ((metomi.rose.META_PROP_TYPE in widget.errors) != - (metomi.rose.META_PROP_TYPE in variable.error) and - hasattr(widget, "needs_type_error_refresh") and - not widget.needs_type_error_refresh()): + if ( + (metomi.rose.META_PROP_TYPE in widget.errors) + != (metomi.rose.META_PROP_TYPE in variable.error) + and hasattr(widget, "needs_type_error_refresh") + and not widget.needs_type_error_refresh() + ): return widget.type_error_refresh(variable) else: return self.handle_reload_var_widget(variable) @@ -762,29 +916,28 @@ def refresh(self, only_this_var_id=None): return self.handle_remove_var_widget(variable) def handle_add_var_widget(self, variable): - if hasattr(self.main_container, 'add_variable_widget'): + if hasattr(self.main_container, "add_variable_widget"): self.main_container.add_variable_widget(variable) self.update_ignored() else: self.refresh() - self.set_main_focus(variable.metadata.get('id')) + self.set_main_focus(variable.metadata.get("id")) def handle_reload_var_widget(self, variable): - if hasattr(self.main_container, 'reload_variable_widget'): + if hasattr(self.main_container, "reload_variable_widget"): self.main_container.reload_variable_widget(variable) self.update_ignored() else: self.refresh() def handle_remove_var_widget(self, variable): - if hasattr(self.main_container, 'remove_variable_widget'): + if hasattr(self.main_container, "remove_variable_widget"): self.main_container.remove_variable_widget(variable) self.update_ignored() else: self.refresh() - def sort_main(self, column_index=0, ascending=True, - remake_forced=False): + def sort_main(self, column_index=0, ascending=True, remake_forced=False): """Regenerate a sorted table, according to the arguments. column_index maps as {0: index, 1: title, 2: key, 3: value}. @@ -795,20 +948,22 @@ def sort_main(self, column_index=0, ascending=True, if self.sort_data(column_index, ascending) or remake_forced: focus_var = None focus_widget = self.get_toplevel().get_focus_child() - if (focus_widget is not None and - hasattr(focus_widget.get_parent(), 'variable')): + if focus_widget is not None and hasattr( + focus_widget.get_parent(), "variable" + ): focus_var = focus_widget.get_parent().variable self.main_container.destroy() self.generate_main_container() - self.scrolled_vbox.pack_start(self.main_container, expand=False, - fill=True, padding=0) + self.scrolled_vbox.pack_start( + self.main_container, expand=False, fill=True, padding=0 + ) self.choose_focus(focus_var) self.update_ignored(no_refresh=True) self.trigger_update_status() def get_main_variable_widgets(self): """Return the widgets within the main_container.""" - return self.get_widgets_with_attribute('variable') + return self.get_widgets_with_attribute("variable") def get_widgets_with_attribute(self, att_name, parent_widget=None): """Return widgets with a certain named attribute.""" @@ -819,13 +974,15 @@ def get_widgets_with_attribute(self, att_name, parent_widget=None): i = 0 while i < len(widget_list): widget = widget_list[i] - if not (hasattr(widget.get_parent(), att_name) or - hasattr(widget, att_name)): + if not ( + hasattr(widget.get_parent(), att_name) + or hasattr(widget, att_name) + ): widget_list.pop(i) i -= 1 - if hasattr(widget, 'get_children'): + if hasattr(widget, "get_children"): widget_list.extend(widget.get_children()) - elif hasattr(widget, 'get_child'): + elif hasattr(widget, "get_child"): widget_list.append(widget.get_child()) i += 1 return widget_list @@ -836,49 +993,57 @@ def get_main_focus(self): focus_child = self.main_container.get_focus_child() for widget in widget_list: if focus_child == widget: - if hasattr(widget.get_parent(), 'variable'): - return widget.get_parent().variable.metadata['id'] - elif hasattr(widget, 'variable'): - return widget.variable.metadata['id'] + if hasattr(widget.get_parent(), "variable"): + return widget.get_parent().variable.metadata["id"] + elif hasattr(widget, "variable"): + return widget.variable.metadata["id"] return None def set_main_focus(self, var_id): """Set the main focus on the key-matched variable widget.""" widget_list = self.get_main_variable_widgets() for widget in widget_list: - if (hasattr(widget.get_parent(), 'variable') and - widget.get_parent().variable.metadata['id'] == var_id): + if ( + hasattr(widget.get_parent(), "variable") + and widget.get_parent().variable.metadata["id"] == var_id + ): widget.get_parent().grab_focus(self.main_container) return True for widget in widget_list: - if (hasattr(widget, 'variable') and - widget.variable.metadata['id'] == var_id): + if ( + hasattr(widget, "variable") + and widget.variable.metadata["id"] == var_id + ): widget.grab_focus() return True return False def set_sub_focus(self, node_id): - if (self.sub_data is not None and - hasattr(self, "sub_data_panel") and - hasattr(self.sub_data_panel, "set_focus_node_id")): + if ( + self.sub_data is not None + and hasattr(self, "sub_data_panel") + and hasattr(self.sub_data_panel, "set_focus_node_id") + ): self.sub_data_panel.set_focus_node_id(node_id) def react_to_show_modes(self, mode_key, is_mode_on): self.show_modes[mode_key] = is_mode_on - if hasattr(self.main_container, 'show_mode_change'): + if hasattr(self.main_container, "show_mode_change"): self.update_ignored() - react_func = getattr(self.main_container, 'show_mode_change') + react_func = getattr(self.main_container, "show_mode_change") react_func(mode_key, is_mode_on) - elif mode_key in [metomi.rose.config_editor.SHOW_MODE_IGNORED, - metomi.rose.config_editor.SHOW_MODE_USER_IGNORED]: + elif mode_key in [ + metomi.rose.config_editor.SHOW_MODE_IGNORED, + metomi.rose.config_editor.SHOW_MODE_USER_IGNORED, + ]: self.update_ignored() else: self.refresh() def refresh_widget_status(self): """Refresh the status of all variable widgets.""" - for widget in self.get_widgets_with_attribute('update_status'): - if hasattr(widget.get_parent(), 'update_status'): + for widget in self.get_widgets_with_attribute("update_status"): + if hasattr(widget.get_parent(), "update_status"): widget.get_parent().update_status() else: widget.update_status() @@ -888,33 +1053,36 @@ def update_ignored(self, no_refresh=False): new_tuples = [] for variable in self.panel_data + self.ghost_data: if variable.ignored_reason: - new_tuples.append((variable.metadata['id'], - variable.ignored_reason.copy())) + new_tuples.append( + (variable.metadata["id"], variable.ignored_reason.copy()) + ) target_widgets_done = [] refresh_list = [] relevant_errs = metomi.rose.config_editor.WARNING_TYPES_IGNORE for widget in self.get_main_variable_widgets(): - if hasattr(widget.get_parent(), 'variable'): + if hasattr(widget.get_parent(), "variable"): target = widget.get_parent() - elif hasattr(widget, 'variable'): + elif hasattr(widget, "variable"): target = widget else: continue if target in target_widgets_done: continue for var_id, help_text in [x for x in new_tuples]: - if target.variable.metadata.get('id') == var_id: + if target.variable.metadata.get("id") == var_id: self._set_widget_ignored(target, help_text) new_tuples.remove((var_id, help_text)) break else: - if hasattr(target, 'is_ignored') and target.is_ignored: - self._set_widget_ignored(target, '', enabled=True) - if (any(e in target.errors for e in relevant_errs) or - any(e in target.variable.error for e in relevant_errs)): - if ([e in target.errors for e in relevant_errs] != - [e in target.variable.error for e in relevant_errs]): - refresh_list.append(target.variable.metadata['id']) + if hasattr(target, "is_ignored") and target.is_ignored: + self._set_widget_ignored(target, "", enabled=True) + if any(e in target.errors for e in relevant_errs) or any( + e in target.variable.error for e in relevant_errs + ): + if [e in target.errors for e in relevant_errs] != [ + e in target.variable.error for e in relevant_errs + ]: + refresh_list.append(target.variable.metadata["id"]) target.errors = list(target.variable.error.keys()) target_widgets_done.append(target) if hasattr(self.main_container, "update_ignored"): @@ -927,52 +1095,58 @@ def update_ignored(self, no_refresh=False): def _check_show_ignored_reason(self, ignored_reason): """Return whether we should show this state.""" mode = self.show_modes - if list(ignored_reason.keys()) == [metomi.rose.variable.IGNORED_BY_USER]: - return (mode[metomi.rose.config_editor.SHOW_MODE_IGNORED] or - mode[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED]) + if list(ignored_reason.keys()) == [ + metomi.rose.variable.IGNORED_BY_USER + ]: + return ( + mode[metomi.rose.config_editor.SHOW_MODE_IGNORED] + or mode[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED] + ) return mode[metomi.rose.config_editor.SHOW_MODE_IGNORED] def _set_widget_ignored(self, widget, help_text, enabled=False): if self._check_show_ignored_reason(widget.variable.ignored_reason): - if hasattr(widget, 'set_ignored'): + if hasattr(widget, "set_ignored"): widget.set_ignored() - elif hasattr(widget, 'set_sensitive'): + elif hasattr(widget, "set_sensitive"): widget.set_sensitive(enabled) else: - if hasattr(widget, 'hide') and hasattr(widget, 'show'): - if hasattr(widget, 'set_ignored'): + if hasattr(widget, "hide") and hasattr(widget, "show"): + if hasattr(widget, "set_ignored"): widget.set_ignored() - elif hasattr(widget, 'set_sensitive'): + elif hasattr(widget, "set_sensitive"): widget.set_sensitive(enabled) def reload_from_data(self, new_config_data, new_ghost_data): """Load the new data into the page as gracefully as possible.""" for variable in [v for v in self.panel_data]: # Remove redundant existing variables - var_id = variable.metadata.get('id') - new_id_list = [x.metadata['id'] for x in new_config_data] + var_id = variable.metadata.get("id") + new_id_list = [x.metadata["id"] for x in new_config_data] if var_id not in new_id_list or var_id is None: self.variable_ops.remove_var(variable) for variable in [v for v in self.ghost_data]: # Remove redundant metadata variables. - var_id = variable.metadata.get('id') - new_id_list = [x.metadata['id'] for x in new_ghost_data] + var_id = variable.metadata.get("id") + new_id_list = [x.metadata["id"] for x in new_ghost_data] if var_id not in new_id_list: self.variable_ops.remove_var(variable) # From the ghost list. for variable in new_config_data: # Update or add variables - var_id = variable.metadata['id'] - old_id_list = [x.metadata.get('id') for x in self.panel_data] + var_id = variable.metadata["id"] + old_id_list = [x.metadata.get("id") for x in self.panel_data] if var_id in old_id_list: old_variable = self.panel_data[old_id_list.index(var_id)] old_variable.metadata = variable.metadata if old_variable.value != variable.value: # Reset the value. - self.variable_ops.set_var_value(old_variable, - variable.value) + self.variable_ops.set_var_value( + old_variable, variable.value + ) if old_variable.comments != variable.comments: - self.variable_ops.set_var_comments(old_variable, - variable.comments) + self.variable_ops.set_var_comments( + old_variable, variable.comments + ) old_ign_set = set(old_variable.ignored_reason.keys()) new_ign_set = set(variable.ignored_reason.keys()) if old_ign_set != new_ign_set: @@ -980,20 +1154,21 @@ def reload_from_data(self, new_config_data, new_ghost_data): self.variable_ops.set_var_ignored( old_variable, variable.ignored_reason.copy(), - override=True) + override=True, + ) else: # The types are the same, but pass on the info. old_variable.ignored_reason = ( - variable.ignored_reason.copy()) + variable.ignored_reason.copy() + ) old_variable.error = variable.error.copy() old_variable.warning = variable.warning.copy() else: self.variable_ops.add_var(variable.copy()) for variable in new_ghost_data: # Update or remove variables - var_id = variable.metadata['id'] - old_id_list = [x.metadata.get('id') - for x in self.ghost_data] + var_id = variable.metadata["id"] + old_id_list = [x.metadata.get("id") for x in self.ghost_data] if var_id in old_id_list: index = old_id_list.index(var_id) old_variable = self.ghost_data[index] @@ -1024,20 +1199,25 @@ def sort_data(self, column_index=0, ascending=True, ghost=False): else: datavars = self.panel_data for variable in datavars: - title = variable.metadata.get(metomi.rose.META_PROP_TITLE, variable.name) - var_id = variable.metadata.get('id', variable.name) + title = variable.metadata.get( + metomi.rose.META_PROP_TITLE, variable.name + ) + var_id = variable.metadata.get("id", variable.name) key = ( - variable.metadata.get(metomi.rose.META_PROP_SORT_KEY, '~'), - var_id + variable.metadata.get(metomi.rose.META_PROP_SORT_KEY, "~"), + var_id, + ) + if variable.name == "": + key = ("~", "") + sorted_data.append( + (key, title, variable.name, variable.value, variable) ) - if variable.name == '': - key = ('~', '') - sorted_data.append((key, title, variable.name, - variable.value, variable)) ascending_cmp = lambda x, y: metomi.rose.config_editor.util.null_cmp( - x[0], y[0]) + x[0], y[0] + ) descending_cmp = lambda x, y: metomi.rose.config_editor.util.null_cmp( - x[0], y[0]) + x[0], y[0] + ) if ascending: sorted_data.sort(key=cmp_to_key(ascending_cmp)) else: @@ -1059,25 +1239,31 @@ def _macro_menu_launch(self): else: stock_id = "dialog-question" macro_menuitem_box = Gtk.Box() - macro_menuitem_icon = Gtk.Image.new_from_icon_name(stock_id, Gtk.IconSize.MENU) + macro_menuitem_icon = Gtk.Image.new_from_icon_name( + stock_id, Gtk.IconSize.MENU + ) macro_menuitem_label = Gtk.Label(label=macro_name) macro_menuitem = Gtk.Button() macro_menuitem_box.pack_start(macro_menuitem_icon, False, False, 0) - macro_menuitem_box.pack_start(macro_menuitem_label, False, False, 0) + macro_menuitem_box.pack_start( + macro_menuitem_label, False, False, 0 + ) Gtk.Container.add(macro_menuitem, macro_menuitem_box) macro_menuitem.set_tooltip_text(description) macro_menuitem.show() macro_menuitem._macro = macro_name macro_menuitem.connect( - "clicked", - lambda m: self.launch_macro(m._macro)) + "clicked", lambda m: self.launch_macro(m._macro) + ) macro_menuitem.set_relief(Gtk.ReliefStyle.NONE) - macro_menuitem.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + macro_menuitem.connect( + "leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE) + ) vbox.pack_start(macro_menuitem, False, True, 10) vbox.show_all() self.popover.add(vbox) self.popover.set_position(Gtk.PositionType.BOTTOM) - + def launch_macro(self, macro_name_string): """Launch a macro, if possible.""" class_name = None @@ -1092,7 +1278,8 @@ def launch_macro(self, macro_name_string): config_name=self.config_name, module_name=module_name, class_name=class_name, - method_name=method_name) + method_name=method_name, + ) def search_for_id(self, id_): """Launch a search for variable or section id.""" @@ -1107,17 +1294,18 @@ def _get_page_info_widgets(self): label_list = [] info = "" # No content warning, if applicable. - has_no_content = (self.section is None and - not self.ghost_data and - self.sub_data is None and - not self.latent_sections) + has_no_content = ( + self.section is None + and not self.ghost_data + and self.sub_data is None + and not self.latent_sections + ) if has_no_content: info = metomi.rose.config_editor.PAGE_WARNING_NO_CONTENT tip = metomi.rose.config_editor.PAGE_WARNING_NO_CONTENT_TIP error_button = metomi.rose.gtk.util.CustomButton( - stock_id=Gtk.STOCK_INFO, - as_tool=True, - tip_text=tip) + stock_id=Gtk.STOCK_INFO, as_tool=True, tip_text=tip + ) error_label = Gtk.Label() error_label.set_text(info) error_label.show() @@ -1125,36 +1313,48 @@ def _get_page_info_widgets(self): label_list.append(error_label) if self.section is not None and self.section.ignored_reason: # This adds an ignored warning. - info = metomi.rose.config_editor.PAGE_WARNING_IGNORED_SECTION.format( - self.section.name) + info = ( + metomi.rose.config_editor.PAGE_WARNING_IGNORED_SECTION.format( + self.section.name + ) + ) tip = metomi.rose.config_editor.PAGE_WARNING_IGNORED_SECTION_TIP error_button = metomi.rose.gtk.util.CustomButton( - stock_id=Gtk.STOCK_NO, - as_tool=True, - tip_text=tip) + stock_id=Gtk.STOCK_NO, as_tool=True, tip_text=tip + ) error_label = Gtk.Label() error_label.set_text(info) error_label.show() button_list.append(error_button) label_list.append(error_label) - elif self.see_also == '' or metomi.rose.FILE_VAR_SOURCE not in self.see_also: + elif ( + self.see_also == "" + or metomi.rose.FILE_VAR_SOURCE not in self.see_also + ): # This adds an 'orphaned' warning, only if the section is enabled. - if (self.section is not None and - self.section.name.startswith('namelist:')): + if self.section is not None and self.section.name.startswith( + "namelist:" + ): error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_DIALOG_WARNING, as_tool=True, - tip_text=metomi.rose.config_editor.ERROR_ORPHAN_SECTION_TIP) + tip_text=( + metomi.rose.config_editor.ERROR_ORPHAN_SECTION_TIP + ), + ) error_label = Gtk.Label() info = metomi.rose.config_editor.ERROR_ORPHAN_SECTION.format( - self.section.name) + self.section.name + ) error_label.set_text(info) error_label.show() button_list.append(error_button) label_list.append(error_label) - has_data = (has_no_content or - self.sub_data is not None or - bool(self.panel_data)) + has_data = ( + has_no_content + or self.sub_data is not None + or bool(self.panel_data) + ) if not has_data: for section in self.sections: if section.metadata["full_ns"] == self.namespace: @@ -1165,9 +1365,12 @@ def _get_page_info_widgets(self): latent_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_INFO, as_tool=True, - tip_text=metomi.rose.config_editor.TIP_LATENT_PAGE) + tip_text=metomi.rose.config_editor.TIP_LATENT_PAGE, + ) latent_label = Gtk.Label() - latent_label.set_text(metomi.rose.config_editor.PAGE_WARNING_LATENT) + latent_label.set_text( + metomi.rose.config_editor.PAGE_WARNING_LATENT + ) latent_label.show() button_list.append(latent_button) label_list.append(latent_label) @@ -1177,24 +1380,34 @@ def _get_page_info_widgets(self): error_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_DIALOG_ERROR, as_tool=True, - tip_text=info) + tip_text=info, + ) error_label = Gtk.Label() - error_label.set_text(metomi.rose.config_editor.PAGE_WARNING.format( - err, sect_data.name)) + error_label.set_text( + metomi.rose.config_editor.PAGE_WARNING.format( + err, sect_data.name + ) + ) error_label.show() button_list.append(error_button) label_list.append(error_label) if list(self.custom_macros.items()): self._macro_menu_launch() - macro_button_icon = Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.MENU) - macro_label = Gtk.Label(label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON) + macro_button_icon = Gtk.Image.new_from_icon_name( + "system-run", Gtk.IconSize.MENU + ) + macro_label = Gtk.Label( + label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON + ) macro_button = Gtk.MenuButton( image=macro_button_icon, label=metomi.rose.config_editor.LABEL_PAGE_MACRO_BUTTON, popover=self.popover, ) macro_button.set_relief(Gtk.ReliefStyle.NONE) - macro_button.connect("leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE)) + macro_button.connect( + "leave", lambda b: b.set_relief(Gtk.ReliefStyle.NONE) + ) macro_button.show() Gtk.Widget.set_name(macro_button, "macro-button") diff --git a/metomi/rose/config_editor/pagewidget/__init__.py b/metomi/rose/config_editor/pagewidget/__init__.py index bbe46a78b..e733a1258 100644 --- a/metomi/rose/config_editor/pagewidget/__init__.py +++ b/metomi/rose/config_editor/pagewidget/__init__.py @@ -18,4 +18,5 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- +# flake8: noqa: F401 from . import table diff --git a/metomi/rose/config_editor/pagewidget/table.py b/metomi/rose/config_editor/pagewidget/table.py index ec848b61c..22c1c530d 100644 --- a/metomi/rose/config_editor/pagewidget/table.py +++ b/metomi/rose/config_editor/pagewidget/table.py @@ -21,7 +21,8 @@ import shlex import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -34,7 +35,6 @@ class PageTable(Gtk.Table): - """Return a widget table generated from panel_data. It uses the variable information to create instances of @@ -47,11 +47,12 @@ class PageTable(Gtk.Table): MAX_COLS = 3 BORDER_WIDTH = metomi.rose.config_editor.SPACING_SUB_PAGE - def __init__(self, panel_data, ghost_data, var_ops, show_modes, - arg_str=None): - super(PageTable, self).__init__(rows=self.MAX_ROWS, - columns=self.MAX_COLS, - homogeneous=False) + def __init__( + self, panel_data, ghost_data, var_ops, show_modes, arg_str=None + ): + super(PageTable, self).__init__( + rows=self.MAX_ROWS, columns=self.MAX_COLS, homogeneous=False + ) self.panel_data = panel_data self.ghost_data = ghost_data self.var_ops = var_ops @@ -66,7 +67,7 @@ def add_variable_widget(self, variable): new_variable_widget = self.get_variable_widget(variable) widget_coordinate_list = [] for child in self.get_children(): - top_row = self.child_get(child, 'top_attach')[0] + top_row = self.child_get(child, "top_attach")[0] variable_widget = child.get_parent() if variable_widget not in [x[0] for x in widget_coordinate_list]: widget_coordinate_list.append((variable_widget, top_row)) @@ -84,20 +85,24 @@ def add_variable_widget(self, variable): row_above_new = -1 else: row_above_new = widget_coordinate_list[ - num_vars_above_this - 1][1] + num_vars_above_this - 1 + ][1] for variable_widget, widget_row in widget_coordinate_list: if widget_row > row_above_new: for child in self.get_children(): if child.get_parent() == variable_widget: self.remove(child) new_variable_widget.insert_into( - self, self.MAX_COLS, row_above_new + 1) + self, self.MAX_COLS, row_above_new + 1 + ) self._show_and_hide_variable_widgets(new_variable_widget) rownum = row_above_new + 2 for variable_widget, widget_row in widget_coordinate_list: - if (widget_row > row_above_new and - variable_widget.variable.metadata.get('id') != - variable.metadata.get('id')): + if ( + widget_row > row_above_new + and variable_widget.variable.metadata.get("id") + != variable.metadata.get("id") + ): variable_widget.insert_into(self, self.MAX_COLS, rownum) rownum += 1 else: @@ -118,7 +123,8 @@ def get_variable_widget(self, variable, is_ghost=False): variable, self.var_ops, is_ghost=is_ghost, - show_modes=self.show_modes) + show_modes=self.show_modes, + ) def reload_variable_widget(self, variable): """Reload the widgets for the given variable.""" @@ -129,14 +135,16 @@ def reload_variable_widget(self, variable): variable_row = None for child in self.get_children(): variable_widget = child.get_parent() - if (variable_widget.variable.name == variable.name and - variable_widget.variable.metadata.get('id') == - variable.metadata.get('id')): + if ( + variable_widget.variable.name == variable.name + and variable_widget.variable.metadata.get("id") + == variable.metadata.get("id") + ): if "index" not in focus_dict: focus_dict["index"] = variable_widget.get_focus_index() if self.get_focus_child() == child: focus_dict["had_focus"] = True - top_row = self.child_get(child, 'top_attach')[0] + top_row = self.child_get(child, "top_attach")[0] variable_row = top_row self.remove(child) child.destroy() @@ -155,10 +163,18 @@ def _get_sorted_variables(self): sort_key_vars = [] for val in self.panel_data + self.ghost_data: sort_key = ( - (val.metadata.get("sort-key", "~")), val.metadata["id"]) + (val.metadata.get("sort-key", "~")), + val.metadata["id"], + ) is_ghost = val in self.ghost_data sort_key_vars.append((sort_key, val, is_ghost)) - sort_key_vars.sort(key=cmp_to_key(lambda x, y: metomi.rose.config_editor.util.null_cmp(x[0], y[0]))) + sort_key_vars.sort( + key=cmp_to_key( + lambda x, y: metomi.rose.config_editor.util.null_cmp( + x[0], y[0] + ) + ) + ) sort_key_vars.sort(key=lambda x: "=null" in x[1].metadata["id"]) return [(x[1], x[2]) for x in sort_key_vars] @@ -177,20 +193,26 @@ def _show_and_hide_variable_widgets(self, just_this_widget=None): ign_reason = variable.ignored_reason if variable.error: variable_widget.show() - elif (len(variable.metadata.get( - metomi.rose.META_PROP_VALUES, [])) == 1 and - not modes[metomi.rose.config_editor.SHOW_MODE_FIXED]): + elif ( + len(variable.metadata.get(metomi.rose.META_PROP_VALUES, [])) + == 1 + and not modes[metomi.rose.config_editor.SHOW_MODE_FIXED] + ): variable_widget.hide() - elif (variable_widget.is_ghost and - not modes[metomi.rose.config_editor.SHOW_MODE_LATENT]): + elif ( + variable_widget.is_ghost + and not modes[metomi.rose.config_editor.SHOW_MODE_LATENT] + ): variable_widget.hide() - elif ((metomi.rose.variable.IGNORED_BY_SYSTEM in ign_reason or - metomi.rose.variable.IGNORED_BY_SECTION in ign_reason) and - not modes[metomi.rose.config_editor.SHOW_MODE_IGNORED]): + elif ( + metomi.rose.variable.IGNORED_BY_SYSTEM in ign_reason + or metomi.rose.variable.IGNORED_BY_SECTION in ign_reason + ) and not modes[metomi.rose.config_editor.SHOW_MODE_IGNORED]: variable_widget.hide() - elif (metomi.rose.variable.IGNORED_BY_USER in ign_reason and - not (modes[metomi.rose.config_editor.SHOW_MODE_IGNORED] or - modes[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED])): + elif metomi.rose.variable.IGNORED_BY_USER in ign_reason and not ( + modes[metomi.rose.config_editor.SHOW_MODE_IGNORED] + or modes[metomi.rose.config_editor.SHOW_MODE_USER_IGNORED] + ): variable_widget.hide() else: variable_widget.show() @@ -210,7 +232,6 @@ def update_ignored(self): class PageArrayTable(PageTable): - """Return a widget table that treats array values as row elements.""" def __init__(self, *args, **kwargs): @@ -233,39 +254,46 @@ def attach_variable_widgets(self, variable_is_ghost_list, start_index=0): def get_variable_widget(self, variable, is_ghost=False): """Create a variable widget for this variable.""" - if (metomi.rose.META_PROP_LENGTH in variable.metadata or - isinstance(variable.metadata.get(metomi.rose.META_PROP_TYPE), list)): + if metomi.rose.META_PROP_LENGTH in variable.metadata or isinstance( + variable.metadata.get(metomi.rose.META_PROP_TYPE), list + ): return metomi.rose.config_editor.variable.RowVariableWidget( variable, self.var_ops, is_ghost=is_ghost, show_modes=self.show_modes, - length=self.array_length) + length=self.array_length, + ) return metomi.rose.config_editor.variable.VariableWidget( variable, self.var_ops, is_ghost=is_ghost, - show_modes=self.show_modes) + show_modes=self.show_modes, + ) def _set_length(self): max_meta_length = 0 max_values_length = 0 for variable in self.panel_data + self.ghost_data: length = variable.metadata.get(metomi.rose.META_PROP_LENGTH) - if (length is not None and length.isdigit() and - int(length) > max_meta_length): + if ( + length is not None + and length.isdigit() + and int(length) > max_meta_length + ): max_meta_length = int(length) types = variable.metadata.get(metomi.rose.META_PROP_TYPE) if isinstance(types, list) and len(types) > max_meta_length: max_meta_length = len(types) - values_length = len(metomi.rose.variable.array_split(variable.value)) + values_length = len( + metomi.rose.variable.array_split(variable.value) + ) if values_length > max_values_length: max_values_length = values_length self.array_length = max([max_meta_length, max_values_length]) class PageLatentTable(Gtk.Table): - """Return a widget table generated from panel_data. It uses the variable information to create instances of @@ -279,40 +307,51 @@ class PageLatentTable(Gtk.Table): MAX_ROWS = 2000 MAX_COLS = 3 - def __init__(self, panel_data, ghost_data, var_ops, show_modes, - arg_str=None): + def __init__( + self, panel_data, ghost_data, var_ops, show_modes, arg_str=None + ): super(PageLatentTable, self).__init__( - rows=self.MAX_ROWS, columns=self.MAX_COLS, homogeneous=False) + rows=self.MAX_ROWS, columns=self.MAX_COLS, homogeneous=False + ) self.show() self.num_removes = 0 self.panel_data = panel_data self.ghost_data = ghost_data self.var_ops = var_ops self.show_modes = show_modes - self.title_on = ( - not self.show_modes[metomi.rose.config_editor.SHOW_MODE_NO_TITLE]) - self.alt_menu_class = metomi.rose.config_editor.menuwidget.CheckedMenuWidget + self.title_on = not self.show_modes[ + metomi.rose.config_editor.SHOW_MODE_NO_TITLE + ] + self.alt_menu_class = ( + metomi.rose.config_editor.menuwidget.CheckedMenuWidget + ) rownum = 0 v_sort_ids = [] for val in self.panel_data + self.ghost_data: - v_sort_ids.append((val.metadata.get("sort-key", ""), - val.metadata["id"])) - v_sort_ids.sort(key=cmp_to_key( - lambda x, y: metomi.rose.config.sort_settings( - x[0] + "~" + x[1], y[0] + "~" + y[1]))) + v_sort_ids.append( + (val.metadata.get("sort-key", ""), val.metadata["id"]) + ) + v_sort_ids.sort( + key=cmp_to_key( + lambda x, y: metomi.rose.config.sort_settings( + x[0] + "~" + x[1], y[0] + "~" + y[1] + ) + ) + ) v_sort_ids.sort(key=lambda x: "=null" in x[1]) for _, var_id in v_sort_ids: is_ghost = False for variable in self.panel_data: - if variable.metadata['id'] == var_id: + if variable.metadata["id"] == var_id: break else: for variable in self.ghost_data: - if variable.metadata['id'] == var_id: + if variable.metadata["id"] == var_id: is_ghost = True break variable_widget = self.get_variable_widget( - variable, is_ghost=is_ghost) + variable, is_ghost=is_ghost + ) variable_widget.insert_into(self, self.MAX_COLS, rownum + 1) variable_widget.set_sensitive(not is_ghost) rownum += 1 @@ -320,8 +359,11 @@ def __init__(self, panel_data, ghost_data, var_ops, show_modes, def get_variable_widget(self, variable, is_ghost=False): """Create a variable widget for this variable.""" return metomi.rose.config_editor.variable.VariableWidget( - variable, self.var_ops, is_ghost=is_ghost, - show_modes=self.show_modes) + variable, + self.var_ops, + is_ghost=is_ghost, + show_modes=self.show_modes, + ) def reload_variable_widget(self, variable): """Reload the widgets for the given variable.""" @@ -332,14 +374,16 @@ def reload_variable_widget(self, variable): variable_row = None for child in self.get_children(): variable_widget = child.get_parent() - if (variable_widget.variable.name == variable.name and - variable_widget.variable.metadata.get('id') == - variable.metadata.get('id')): + if ( + variable_widget.variable.name == variable.name + and variable_widget.variable.metadata.get("id") + == variable.metadata.get("id") + ): if "index" not in focus_dict: focus_dict["index"] = variable_widget.get_focus_index() - if getattr(self, 'focus_child') == child: + if getattr(self, "focus_child") == child: focus_dict["had_focus"] = True - top_row = self.child_get(child, 'top_attach')[0] + top_row = self.child_get(child, "top_attach")[0] variable_row = top_row self.remove(child) child.destroy() diff --git a/metomi/rose/config_editor/panelwidget/__init__.py b/metomi/rose/config_editor/panelwidget/__init__.py index 2792550c0..07014eb5a 100644 --- a/metomi/rose/config_editor/panelwidget/__init__.py +++ b/metomi/rose/config_editor/panelwidget/__init__.py @@ -18,5 +18,6 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- +# flake8: noqa: F401 from . import filesystem from . import summary_data diff --git a/metomi/rose/config_editor/panelwidget/filesystem.py b/metomi/rose/config_editor/panelwidget/filesystem.py index f69326aeb..f3b417dea 100644 --- a/metomi/rose/config_editor/panelwidget/filesystem.py +++ b/metomi/rose/config_editor/panelwidget/filesystem.py @@ -21,7 +21,8 @@ import os import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config_editor @@ -31,7 +32,6 @@ class FileSystemPanel(Gtk.ScrolledWindow): - """A class to show underlying files and directories in a Gtk.TreeView.""" def __init__(self, directory): @@ -43,9 +43,10 @@ def __init__(self, directory): for dirpath, dirnames, filenames in os.walk(self.directory): if dirpath not in dirpath_iters: known_path = os.path.dirname(dirpath) - new_iter = store.append(dirpath_iters[known_path], - [os.path.basename(dirpath), - os.path.abspath(dirpath)]) + new_iter = store.append( + dirpath_iters[known_path], + [os.path.basename(dirpath), os.path.abspath(dirpath)], + ) dirpath_iters.update({dirpath: new_iter}) this_iter = dirpath_iters[dirpath] filenames.sort() @@ -55,8 +56,10 @@ def __init__(self, directory): filepath = os.path.join(dirpath, name) store.append(this_iter, [name, os.path.abspath(filepath)]) for dirname in list(dirnames): - if (dirname.startswith(".") or dirname in [ - metomi.rose.SUB_CONFIGS_DIR, metomi.rose.CONFIG_META_DIR]): + if dirname.startswith(".") or dirname in [ + metomi.rose.SUB_CONFIGS_DIR, + metomi.rose.CONFIG_META_DIR, + ]: dirnames.remove(dirname) dirnames.sort() view.set_model(store) @@ -64,8 +67,7 @@ def __init__(self, directory): col.set_title(metomi.rose.config_editor.TITLE_FILE_PANEL) cell = Gtk.CellRendererText() col.pack_start(cell, True) - col.set_cell_data_func(cell, - self._set_path_markup, store) + col.set_cell_data_func(cell, self._set_path_markup, store) view.append_column(col) view.expand_all() view.show() @@ -98,17 +100,25 @@ def _handle_activation(self, view=None, path=None, col=None): def _handle_click(self, view, event): pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) - if (event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS and - pathinfo is None): + if ( + event.button == 1 + and event.type == Gdk.EventType._2BUTTON_PRESS + and pathinfo is None + ): self._handle_activation() if event.button == 3: ui_string = """ """ - actions = [('Open', Gtk.STOCK_OPEN, - metomi.rose.config_editor.FILE_PANEL_MENU_OPEN)] + actions = [ + ( + "Open", + Gtk.STOCK_OPEN, + metomi.rose.config_editor.FILE_PANEL_MENU_OPEN, + ) + ] uimanager = Gtk.UIManager() - actiongroup = Gtk.ActionGroup('Popup') + actiongroup = Gtk.ActionGroup("Popup") actiongroup.add_actions(actions) uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_string) @@ -117,9 +127,14 @@ def _handle_click(self, view, event): col = None else: path, col = pathinfo[:2] - open_item = uimanager.get_widget('/Popup/Open') + open_item = uimanager.get_widget("/Popup/Open") open_item.connect( - "activate", - lambda m: self._handle_activation(view, path, col)) - this_menu = uimanager.get_widget('/Popup') - this_menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + "activate", lambda m: self._handle_activation(view, path, col) + ) + this_menu = uimanager.get_widget("/Popup") + this_menu.popup_at_widget( + event.button, + Gdk.Gravity.SOUTH_WEST, + Gdk.Gravity.NORTH_WEST, + event, + ) diff --git a/metomi/rose/config_editor/panelwidget/summary_data.py b/metomi/rose/config_editor/panelwidget/summary_data.py index 9683d8983..9003f73d5 100644 --- a/metomi/rose/config_editor/panelwidget/summary_data.py +++ b/metomi/rose/config_editor/panelwidget/summary_data.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk from gi.repository import Pango @@ -32,7 +33,6 @@ class BaseSummaryDataPanel(Gtk.Box): - """A base class for summarising data across many namespaces. Subclasses should provide the following methods: @@ -49,10 +49,20 @@ class BaseSummaryDataPanel(Gtk.Box): """ - def __init__(self, sections, variables, sect_ops, var_ops, - search_function, sub_ops, - is_duplicate, arg_str=None): - super(BaseSummaryDataPanel, self).__init__(orientation=Gtk.Orientation.VERTICAL) + def __init__( + self, + sections, + variables, + sect_ops, + var_ops, + search_function, + sub_ops, + is_duplicate, + arg_str=None, + ): + super(BaseSummaryDataPanel, self).__init__( + orientation=Gtk.Orientation.VERTICAL + ) self.sections = sections self.variables = variables self._section_data_list = None @@ -66,22 +76,27 @@ def __init__(self, sections, variables, sect_ops, var_ops, self.group_index = None self.util = metomi.rose.config_editor.util.Lookup() self.control_widget_hbox = self._get_control_widget_hbox() - self.pack_start(self.control_widget_hbox, expand=False, fill=False, padding=0) + self.pack_start( + self.control_widget_hbox, expand=False, fill=False, padding=0 + ) self._prev_store = None self._prev_sort_model = None self._view = metomi.rose.gtk.util.TooltipTreeView( - get_tooltip_func=self.set_tree_tip, - multiple_selection=True) + get_tooltip_func=self.set_tree_tip, multiple_selection=True + ) self._view.set_rules_hint(True) self.sort_util = metomi.rose.gtk.util.TreeModelSortUtil( - self._view.get_model, multi_sort_num=2) + self._view.get_model, multi_sort_num=2 + ) self._view.show() - self._view.connect("button-press-event", - self._handle_button_press_event) - self._view.connect("key-press-event", - self._handle_key_press_event) + self._view.connect( + "button-press-event", self._handle_button_press_event + ) + self._view.connect("key-press-event", self._handle_key_press_event) self._window = Gtk.ScrolledWindow() - self._window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) self.update() self._window.add(self._view) self._window.show() @@ -148,29 +163,42 @@ def _get_custom_menu_items(self, path, column, event): return [] def _get_control_widget_hbox(self): - filter_label = Gtk.Label(label= - metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + filter_label = Gtk.Label( + label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL + ) filter_label.show() self._filter_widget = Gtk.Entry() self._filter_widget.set_width_chars( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR + ) self._filter_widget.connect("changed", self._refilter) self._filter_widget.show() - group_label = Gtk.Label(label= - metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + group_label = Gtk.Label( + label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL + ) group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() self._group_widget.pack_start(cell, True) - self._group_widget.add_attribute(cell, 'text', 0) + self._group_widget.add_attribute(cell, "text", 0) self._group_widget.connect("changed", self._handle_group_change) self._group_widget.show() filter_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - filter_hbox.pack_start(group_label, expand=False, fill=False, padding=0) - filter_hbox.pack_start(self._group_widget, expand=False, fill=False, padding=0) - filter_hbox.pack_start(filter_label, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - filter_hbox.pack_start(self._filter_widget, expand=False, fill=False, padding=0) + filter_hbox.pack_start( + group_label, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_start( + self._group_widget, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_start( + filter_label, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) + filter_hbox.pack_start( + self._filter_widget, expand=False, fill=False, padding=0 + ) filter_hbox.show() return filter_hbox @@ -182,14 +210,15 @@ def update_tree_model(self): self.var_id_map[variable.metadata["id"]] = variable data_rows, column_names = self.get_model_data() data_rows, column_names, rows_are_descendants = self._apply_grouping( - data_rows, column_names, self.group_index) + data_rows, column_names, self.group_index + ) self.column_names = column_names should_redraw = self.column_names != self._last_column_names if data_rows: col_types = [str] * len(data_rows[0]) else: col_types = [] - need_new_store = (should_redraw or self.group_index) + need_new_store = should_redraw or self.group_index if need_new_store: # We need to construct a new TreeModel. if self._prev_sort_model is not None: @@ -222,12 +251,15 @@ def update_tree_model(self): sort_model = Gtk.TreeModelSort(filter_model) for i in range(len(self.column_names)): sort_model.set_sort_func(i, self.sort_util.sort_column, i) - if (self._prev_sort_model is not None and - prev_sort_id[0] is not None): + if ( + self._prev_sort_model is not None + and prev_sort_id[0] is not None + ): sort_model.set_sort_column_id(*prev_sort_id) self._prev_sort_model = sort_model - sort_model.connect("sort-column-changed", - self.sort_util.handle_sort_column_change) + sort_model.connect( + "sort-column-changed", self.sort_util.handle_sort_column_change + ) if should_redraw: self.sort_util.clear_sort_columns() for column in list(self._view.get_columns()): @@ -253,8 +285,10 @@ def update(self, sections=None, variables=None): expanded_sections = set() model = self._view.get_model() self._view.map_expanded_rows( - lambda r, p: - expanded_sections.add(model.get_value(model.get_iter(p), 0))) + lambda r, p: expanded_sections.add( + model.get_value(model.get_iter(p), 0) + ) + ) should_redraw = self.update_tree_model() if should_redraw: @@ -298,8 +332,7 @@ def add_new_columns(self, treeview, column_names): col.set_title(column_name.replace("_", "__")) cell_for_status = Gtk.CellRendererText() col.pack_start(cell_for_status, False) - col.set_cell_data_func(cell_for_status, - self.set_tree_cell_status) + col.set_cell_data_func(cell_for_status, self.set_tree_cell_status) self.add_cell_renderer_for_value(col, column_name) if i < len(column_names) - 1: col.set_resizable(True) @@ -309,16 +342,27 @@ def add_new_columns(self, treeview, column_names): def get_status_from_data(self, node_data): """Return markup corresponding to changes since the last save.""" text = "" - mod_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + mod_markup = ( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + ) err_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_MARKUP if node_data is None: return None if metomi.rose.variable.IGNORED_BY_SYSTEM in node_data.ignored_reason: - text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP + text += ( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_IGNORED_SYST_MARKUP + ) elif metomi.rose.variable.IGNORED_BY_USER in node_data.ignored_reason: - text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP + text += ( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_IGNORED_USER_MARKUP + ) if metomi.rose.variable.IGNORED_BY_SECTION in node_data.ignored_reason: - text += metomi.rose.config_editor.SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP + text += ( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_IGNORED_SECT_MARKUP + ) if isinstance(node_data, metomi.rose.section.Section): # Modified status section = node_data.metadata["id"] @@ -378,8 +422,7 @@ def _handle_activation(self, view, path, column): self.search_function(id_) def _handle_button_press_event(self, treeview, event): - pathinfo = treeview.get_path_at_pos(int(event.x), - int(event.y)) + pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) if pathinfo is not None: path, col = pathinfo[0:2] if event.button == 3: @@ -436,8 +479,15 @@ def _popup_tree_multi_menu(self, event): # Ignore all. ign_menuitem_box = Gtk.Box() - ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) - ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI) + ign_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_NO, Gtk.IconSize.MENU + ) + ign_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_IGNORE_MULTI + ) + ) ign_menuitem = Gtk.MenuItem() ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) @@ -445,12 +495,20 @@ def _popup_tree_multi_menu(self, event): ign_menuitem.connect("activate", self._ignore_selected_sections, True) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - ign_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem) + ) # Enable all. ign_menuitem_box = Gtk.Box() - ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) - ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI) + ign_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_YES, Gtk.IconSize.MENU + ) + ign_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_ENABLE_MULTI + ) + ) ign_menuitem = Gtk.MenuItem() ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) @@ -458,12 +516,20 @@ def _popup_tree_multi_menu(self, event): ign_menuitem.connect("activate", self._ignore_selected_sections, False) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - ign_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem) + ) # Remove all. rem_menuitem_box = Gtk.Box() - rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) - rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI) + rem_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) + rem_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_REMOVE_MULTI + ) + ) rem_menuitem = Gtk.MenuItem() rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) @@ -471,7 +537,9 @@ def _popup_tree_multi_menu(self, event): rem_menuitem.connect("activate", self._remove_selected_sections) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem) + ) # list shortcut keys accel = Gtk.AccelGroup() @@ -479,14 +547,12 @@ def _popup_tree_multi_menu(self, event): for key_press, menuitem in shortcuts: key, mod = Gtk.accelerator_parse(key_press) menuitem.add_accelerator( - 'activate', - accel, - key, - mod, - Gtk.AccelFlags.VISIBLE + "activate", accel, key, mod, Gtk.AccelFlags.VISIBLE ) - menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + menu.popup_at_widget( + event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) return False def _popup_tree_menu(self, path, col, event): @@ -499,18 +565,24 @@ def _popup_tree_menu(self, path, col, event): this_section = model.get_value(row_iter, sect_index) if this_section is not None: # Jump to section. - label = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( - this_section.replace("_", "__")) + label = ( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_GO_TO.format( + this_section.replace("_", "__") + ) + ) menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) + menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU + ) menuitem_label = Gtk.Label(label=label) menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) menuitem_box.pack_start(menuitem_label, False, False, 0) Gtk.Container.add(menuitem, menuitem_box) menuitem._section = this_section - menuitem.connect("activate", - lambda i: self.search_function(i._section)) + menuitem.connect( + "activate", lambda i: self.search_function(i._section) + ) menuitem.show() menu.append(menuitem) sep = Gtk.SeparatorMenuItem() @@ -529,122 +601,211 @@ def _popup_tree_menu(self, path, col, event): if this_section is not None: # Add section. add_menuitem_box = Gtk.Box() - add_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) - add_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD) + add_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_ADD, Gtk.IconSize.MENU + ) + add_menuitem_label = Gtk.Label( + label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ADD + ) add_menuitem = Gtk.MenuItem() add_menuitem_box.pack_start(add_menuitem_icon, False, False, 0) - add_menuitem_box.pack_start(add_menuitem_label, False, False, 0) + add_menuitem_box.pack_start( + add_menuitem_label, False, False, 0 + ) Gtk.Container.add(add_menuitem, add_menuitem_box) - add_menuitem.connect("activate", - lambda i: self.add_section()) + add_menuitem.connect("activate", lambda i: self.add_section()) add_menuitem.show() menu.append(add_menuitem) # Copy section. copy_menuitem_box = Gtk.Box() - copy_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_COPY, Gtk.IconSize.MENU) - copy_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_COPY) + copy_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_COPY, Gtk.IconSize.MENU + ) + copy_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_COPY + ) + ) copy_menuitem = Gtk.MenuItem() - copy_menuitem_box.pack_start(copy_menuitem_icon, False, False, 0) - copy_menuitem_box.pack_start(copy_menuitem_label, False, False, 0) + copy_menuitem_box.pack_start( + copy_menuitem_icon, False, False, 0 + ) + copy_menuitem_box.pack_start( + copy_menuitem_label, False, False, 0 + ) Gtk.Container.add(copy_menuitem, copy_menuitem_box) copy_menuitem.connect( - "activate", lambda i: self.copy_section(this_section)) + "activate", lambda i: self.copy_section(this_section) + ) copy_menuitem.show() menu.append(copy_menuitem) - if (metomi.rose.variable.IGNORED_BY_USER in - self.sections[this_section].ignored_reason): + if ( + metomi.rose.variable.IGNORED_BY_USER + in self.sections[this_section].ignored_reason + ): # Enable section. enab_menuitem_box = Gtk.Box() - enab_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) - enab_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + enab_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_YES, Gtk.IconSize.MENU + ) + enab_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_ENABLE + ) + ) enab_menuitem = Gtk.MenuItem() - enab_menuitem_box.pack_start(enab_menuitem_icon, False, False, 0) - enab_menuitem_box.pack_start(enab_menuitem_label, False, False, 0) + enab_menuitem_box.pack_start( + enab_menuitem_icon, False, False, 0 + ) + enab_menuitem_box.pack_start( + enab_menuitem_label, False, False, 0 + ) Gtk.Container.add(enab_menuitem, enab_menuitem_box) enab_menuitem.connect( "activate", - lambda i: self.sub_ops.ignore_section(this_section, - False)) + lambda i: self.sub_ops.ignore_section( + this_section, False + ), + ) enab_menuitem.show() menu.append(enab_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - enab_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, enab_menuitem) + ) else: # Ignore section. ign_menuitem_box = Gtk.Box() - ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) - ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_NO, Gtk.IconSize.MENU + ) + ign_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_IGNORE + ) + ) ign_menuitem = Gtk.MenuItem() - ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) - ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + ign_menuitem_box.pack_start( + ign_menuitem_icon, False, False, 0 + ) + ign_menuitem_box.pack_start( + ign_menuitem_label, False, False, 0 + ) Gtk.Container.add(ign_menuitem, ign_menuitem_box) ign_menuitem.connect( "activate", - lambda i: self.sub_ops.ignore_section(this_section, - True)) + lambda i: self.sub_ops.ignore_section( + this_section, True + ), + ) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - ign_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem) + ) # Remove section. rem_menuitem_box = Gtk.Box() - rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) - rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) + rem_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_REMOVE + ) + ) rem_menuitem = Gtk.MenuItem() rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) - rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) + rem_menuitem_box.pack_start( + rem_menuitem_label, False, False, 0 + ) Gtk.Container.add(rem_menuitem, rem_menuitem_box) rem_menuitem.connect( - "activate", lambda i: self.remove_section(this_section)) + "activate", lambda i: self.remove_section(this_section) + ) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, - rem_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem) + ) else: # A group is currently selected. # Ignore all ign_menuitem_box = Gtk.Box() - ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) - ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_IGNORE) + ign_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_NO, Gtk.IconSize.MENU + ) + ign_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_IGNORE + ) + ) ign_menuitem = Gtk.MenuItem() ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) - ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + ign_menuitem_box.pack_start( + ign_menuitem_label, False, False, 0 + ) Gtk.Container.add(ign_menuitem, ign_menuitem_box) - ign_menuitem.connect("activate", - self._ignore_selected_sections, - True) + ign_menuitem.connect( + "activate", self._ignore_selected_sections, True + ) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - ign_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem) + ) # Enable all ign_menuitem_box = Gtk.Box() - ign_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) - ign_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_ENABLE) + ign_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_YES, Gtk.IconSize.MENU + ) + ign_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_ENABLE + ) + ) ign_menuitem = Gtk.MenuItem() ign_menuitem_box.pack_start(ign_menuitem_icon, False, False, 0) - ign_menuitem_box.pack_start(ign_menuitem_label, False, False, 0) + ign_menuitem_box.pack_start( + ign_menuitem_label, False, False, 0 + ) Gtk.Container.add(ign_menuitem, ign_menuitem_box) - ign_menuitem.connect("activate", - self._ignore_selected_sections, - False) + ign_menuitem.connect( + "activate", self._ignore_selected_sections, False + ) ign_menuitem.show() menu.append(ign_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_IGNORE, - ign_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_IGNORE, ign_menuitem) + ) # Delete all. rem_menuitem_box = Gtk.Box() - rem_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) - rem_menuitem_label = Gtk.Label(label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_MENU_REMOVE) + rem_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) + rem_menuitem_label = Gtk.Label( + label=( + metomi.rose.config_editor + .SUMMARY_DATA_PANEL_MENU_REMOVE + ) + ) rem_menuitem = Gtk.MenuItem() rem_menuitem_box.pack_start(rem_menuitem_icon, False, False, 0) - rem_menuitem_box.pack_start(rem_menuitem_label, False, False, 0) + rem_menuitem_box.pack_start( + rem_menuitem_label, False, False, 0 + ) Gtk.Container.add(rem_menuitem, rem_menuitem_box) rem_menuitem.connect( - "activate", self._remove_selected_sections) + "activate", self._remove_selected_sections + ) rem_menuitem.show() menu.append(rem_menuitem) - shortcuts.append((metomi.rose.config_editor.ACCEL_REMOVE, - rem_menuitem)) + shortcuts.append( + (metomi.rose.config_editor.ACCEL_REMOVE, rem_menuitem) + ) # list shortcut keys accel = Gtk.AccelGroup() @@ -652,11 +813,7 @@ def _popup_tree_menu(self, path, col, event): for key_press, menuitem in shortcuts: key, mod = Gtk.accelerator_parse(key_press) menuitem.add_accelerator( - 'activate', - accel, - key, - mod, - Gtk.AccelFlags.VISIBLE + "activate", accel, key, mod, Gtk.AccelFlags.VISIBLE ) menu.popup_at_pointer(event) menu.show_all() @@ -694,7 +851,7 @@ def _handle_key_press_event(self, treeview, event): # `Delete` - remove section(s) self._remove_selected_sections() # detect key combination - elif 'GDK_CONTROL_MASK' in event.get_state().value_names: + elif "GDK_CONTROL_MASK" in event.get_state().value_names: # `Ctrl + ?` if event.keyval == Gdk.KEY_i: # `Ctrl + i` - ignore section(s) @@ -715,17 +872,23 @@ def _ignore_selected_sections(self, _, ignore=None): then enable all sections inserted. """ sections_ = self._get_selected_sections() - ignored = [metomi.rose.variable.IGNORED_BY_USER in - self.sections[section_].ignored_reason for - section_ in sections_] + ignored = [ + metomi.rose.variable.IGNORED_BY_USER + in self.sections[section_].ignored_reason + for section_ in sections_ + ] # If ignore mode is not specified decide whether to ignore or enable. if ignore is None: ignore = not all(ignored) # Filter out sections that are already ignored/enabled. - sections_ = [section_ for section_, ignored in zip(sections_, ignored) - if ignored != ignore] + sections_ = [ + section_ + for section_, ignored in zip(sections_, ignored) + if ignored != ignore + ] self.sub_ops.ignore_sections( - sections_, ignore, skip_sub_data_update=False) + sections_, ignore, skip_sub_data_update=False + ) def remove_section(self, section): """Remove a section.""" @@ -743,8 +906,9 @@ def get_section_iter(self, section): """Get the Gtk.TreeIter of this section.""" iters = [] sect_index = self.get_section_column_index() - self._view.get_model().foreach(self._check_value_iter, - [sect_index, section, iters]) + self._view.get_model().foreach( + self._check_value_iter, [sect_index, section, iters] + ) if iters: return iters[0] return None @@ -765,8 +929,10 @@ def _handle_group_change(self, combobox): if col_name: group_index = self.column_names.index(col_name) # Any existing grouping changes the order of self.column_names. - if (self.group_index is not None and - group_index <= self.group_index): + if ( + self.group_index is not None + and group_index <= self.group_index + ): group_index -= 1 else: group_index = None @@ -776,13 +942,14 @@ def _handle_group_change(self, combobox): self.update() return False - def _apply_grouping(self, data_rows, column_names, group_index=None, - descending=False): + def _apply_grouping( + self, data_rows, column_names, group_index=None, descending=False + ): rows_are_descendants = [] if group_index is None: return data_rows, column_names, rows_are_descendants k = group_index - data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] + data_rows = [r[k : k + 1] + r[0:k] + r[k + 1 :] for r in data_rows] column_names.insert(0, column_names.pop(k)) if descending: data_rows.sort(key=cmp_to_key(self._sort_row_data), reverse=True) @@ -800,15 +967,13 @@ def _apply_grouping(self, data_rows, column_names, group_index=None, class StandardSummaryDataPanel(BaseSummaryDataPanel): - """Class that provides a standard interface to summary data.""" def add_cell_renderer_for_value(self, col, col_title): """Add a CellRendererText for the column.""" cell_for_value = Gtk.CellRendererText() col.pack_start(cell_for_value, True) - col.set_cell_data_func(cell_for_value, - self._set_tree_cell_value) + col.set_cell_data_func(cell_for_value, self._set_tree_cell_value) def set_tree_cell_status(self, col, cell, model, row_iter, _): """Set the status text for a cell in this column.""" @@ -824,8 +989,7 @@ def set_tree_cell_status(self, col, cell, model, row_iter, _): option = self.column_names[col_index] id_ = self.util.get_id_from_section_option(section, option) node_data = self.var_id_map.get(id_) - cell.set_property("markup", - self.get_status_from_data(node_data)) + cell.set_property("markup", self.get_status_from_data(node_data)) def get_model_data(self): """Construct a data model of other page data.""" @@ -851,9 +1015,13 @@ def get_model_data(self): row_data.append(metomi.rose.gtk.util.safe_str(var.value)) data_rows.append(row_data) if self.is_duplicate: - sect_name = metomi.rose.config_editor.SUMMARY_DATA_PANEL_INDEX_TITLE + sect_name = ( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_INDEX_TITLE + ) else: - sect_name = metomi.rose.config_editor.SUMMARY_DATA_PANEL_SECTION_TITLE + sect_name = ( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_SECTION_TITLE + ) column_names = [sect_name] column_names += sub_var_names return data_rows, column_names @@ -867,8 +1035,7 @@ def _set_tree_cell_value(self, column, cell, treemodel, iter_, _): cell.set_property("width-chars", max_len) cell.set_property("ellipsize", Pango.EllipsizeMode.END) sect_index = self.get_section_column_index() - if (value is not None and col_index == sect_index and - self.is_duplicate): + if value is not None and col_index == sect_index and self.is_duplicate: value = value.split("(")[-1].rstrip(")") if col_index == 0 and treemodel.iter_parent(iter_) is not None: cell.set_property("visible", False) @@ -893,14 +1060,18 @@ def set_tree_tip(self, view, row_iter, col_index, tip): return False id_data = self.var_id_map[id_] value = str(view.get_model().get_value(row_iter, col_index)) - tip_text = metomi.rose.CONFIG_DELIMITER.join([section, option, value]) + tip_text = metomi.rose.CONFIG_DELIMITER.join( + [section, option, value] + ) tip_text += id_data.metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") if tip_text: tip_text += "\n" for key, value in list(id_data.error.items()): tip_text += ( metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( - key, value)) + key, value + ) + ) for key in id_data.ignored_reason: tip_text += key + "\n" if option is not None: diff --git a/metomi/rose/config_editor/plugin/um/widget/stash.py b/metomi/rose/config_editor/plugin/um/widget/stash.py index a0adb4321..9839ce021 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash.py @@ -23,6 +23,7 @@ from gi.repository import Pango import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk @@ -41,8 +42,8 @@ class BaseStashSummaryDataPanelv1( - metomi.rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel): - + metomi.rose.config_editor.panelwidget.summary_data.BaseSummaryDataPanel +): """This is a base class for displaying and editing STASH requests. It adds editing capability for option values, displays metadata @@ -98,9 +99,11 @@ def get_stashmaster_lookup_dict(self): STREQ_NL_SECT_OPT = "isec" STREQ_NL_ITEM_OPT = "item" STREQ_NL_PACKAGE_OPT = "package" - OPTION_NL_MAP = {"dom_name": "namelist:domain", - "tim_name": "namelist:time", - "use_name": "namelist:use"} + OPTION_NL_MAP = { + "dom_name": "namelist:domain", + "tim_name": "namelist:time", + "use_name": "namelist:use", + } def __init__(self, *args, **kwargs): self.stashmaster_directory_path = kwargs.get("arg_str", "") @@ -141,31 +144,33 @@ def add_cell_renderer_for_value(self, col, col_title): cell_for_value.set_property("editable", True) cell_for_value.set_property("model", listmodel) cell_for_value.set_property("text-column", 0) - cell_for_value.connect("changed", - self._handle_cell_combo_change, - col_title) - col.pack_start(cell_for_value, True, True, 0) - col.set_cell_data_func(cell_for_value, - self._set_tree_cell_value_combo) + cell_for_value.connect( + "changed", self._handle_cell_combo_change, col_title + ) + col.pack_start(cell_for_value, True) + col.set_cell_data_func( + cell_for_value, self._set_tree_cell_value_combo + ) elif col_title == self.INCLUDED_TITLE: cell_for_value = Gtk.CellRendererToggle() col.pack_start(cell_for_value, False) cell_for_value.set_property("activatable", True) - cell_for_value.connect("toggled", - self._handle_cell_toggle_change) - col.set_cell_data_func(cell_for_value, - self._set_tree_cell_value_toggle) + cell_for_value.connect("toggled", self._handle_cell_toggle_change) + col.set_cell_data_func( + cell_for_value, self._set_tree_cell_value_toggle + ) else: cell_for_value = Gtk.CellRendererText() col.pack_start(cell_for_value, True) - if (col_title not in [self.SECTION_INDEX_TITLE, - self.DESCRIPTION_TITLE]): + if col_title not in [ + self.SECTION_INDEX_TITLE, + self.DESCRIPTION_TITLE, + ]: cell_for_value.set_property("editable", True) - cell_for_value.connect("edited", - self._handle_cell_text_change, - col_title) - col.set_cell_data_func(cell_for_value, - self._set_tree_cell_value) + cell_for_value.connect( + "edited", self._handle_cell_text_change, col_title + ) + col.set_cell_data_func(cell_for_value, self._set_tree_cell_value) def get_model_data(self): """(Override) Construct a data model of other page data.""" @@ -208,33 +213,50 @@ def get_model_data(self): sort_list[3] = section sub_sect_names.sort(key=lambda x: section_sort_keys.get(x)) sub_var_names.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) - sub_var_names.sort(key=cmp_to_key(lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) - - (x != self.STREQ_NL_PACKAGE_OPT))) - sub_var_names.sort(key=cmp_to_key(lambda x, y: (y == self.STREQ_NL_ITEM_OPT) - - (x == self.STREQ_NL_ITEM_OPT))) - sub_var_names.sort(key=cmp_to_key(lambda x, y: (y == self.STREQ_NL_SECT_OPT) - - (x == self.STREQ_NL_SECT_OPT))) + sub_var_names.sort( + key=cmp_to_key( + lambda x, y: (y != self.STREQ_NL_PACKAGE_OPT) + - (x != self.STREQ_NL_PACKAGE_OPT) + ) + ) + sub_var_names.sort( + key=cmp_to_key( + lambda x, y: (y == self.STREQ_NL_ITEM_OPT) + - (x == self.STREQ_NL_ITEM_OPT) + ) + ) + sub_var_names.sort( + key=cmp_to_key( + lambda x, y: (y == self.STREQ_NL_SECT_OPT) + - (x == self.STREQ_NL_SECT_OPT) + ) + ) # Load the data. data_rows = [] for section in sub_sect_names: row_data = [] stash_sect_id = self.util.get_id_from_section_option( - section, self.STREQ_NL_SECT_OPT) + section, self.STREQ_NL_SECT_OPT + ) stash_item_id = self.util.get_id_from_section_option( - section, self.STREQ_NL_ITEM_OPT) + section, self.STREQ_NL_ITEM_OPT + ) sect_var = self.var_id_map.get(stash_sect_id) item_var = self.var_id_map.get(stash_item_id) stash_props = None if sect_var is not None and item_var is not None: - stash_props = self._stash_lookup.get( - sect_var.value, {}).get(item_var.value) + stash_props = self._stash_lookup.get(sect_var.value, {}).get( + item_var.value + ) if stash_props is None: row_data.append(None) else: desc = stash_props[self.STASH_PARSE_DESC_OPT].strip() row_data.append(desc) - is_enabled = (metomi.rose.variable.IGNORED_BY_USER not in - self.sections[section].ignored_reason) + is_enabled = ( + metomi.rose.variable.IGNORED_BY_USER + not in self.sections[section].ignored_reason + ) row_data.append(str(is_enabled)) for opt in sub_var_names: id_ = self.util.get_id_from_section_option(section, opt) @@ -277,15 +299,22 @@ def get_stashmaster_meta_map(self): if self.STASHMASTER_META_PATH is None: return {} try: - config = metomi.rose.config_tree.ConfigTreeLoader().load( - self.STASHMASTER_META_PATH, - self.STASHMASTER_META_FILENAME).node + config = ( + metomi.rose.config_tree.ConfigTreeLoader() + .load( + self.STASHMASTER_META_PATH, self.STASHMASTER_META_FILENAME + ) + .node + ) except (metomi.rose.config.ConfigSyntaxError, IOError, OSError) as exc: metomi.rose.reporter.Reporter()( - "Error loading STASHmaster metadata resource: " + - type(exc).__name__ + ": " + str(exc) + "\n", + "Error loading STASHmaster metadata resource: " + + type(exc).__name__ + + ": " + + str(exc) + + "\n", kind=metomi.rose.reporter.Reporter.KIND_ERR, - level=metomi.rose.reporter.Reporter.FAIL + level=metomi.rose.reporter.Reporter.FAIL, ) return {} stash_meta_dict = {} @@ -304,8 +333,10 @@ def set_tree_cell_status(self, col, cell, model, row_iter, _): section = model.get_value(row_iter, sect_index) if section is None: return cell.set_property("markup", None) - if (col_index == sect_index or - self.column_names[col_index] == self.DESCRIPTION_TITLE): + if ( + col_index == sect_index + or self.column_names[col_index] == self.DESCRIPTION_TITLE + ): node_data = self.sections.get(section) else: option = self.column_names[col_index] @@ -313,8 +344,7 @@ def set_tree_cell_status(self, col, cell, model, row_iter, _): return cell.set_property("markup", None) id_ = self.util.get_id_from_section_option(section, option) node_data = self.var_id_map.get(id_) - cell.set_property("markup", - self.get_status_from_data(node_data)) + cell.set_property("markup", self.get_status_from_data(node_data)) def set_tree_tip(self, view, row_iter, col_index, tip): """(Override) Set the TreeView Tooltip.""" @@ -324,14 +354,14 @@ def set_tree_tip(self, view, row_iter, col_index, tip): if section is None: return False col_name = self.column_names[col_index] - stash_section_index = self.column_names.index( - self.STREQ_NL_SECT_OPT) - stash_item_index = self.column_names.index( - self.STREQ_NL_ITEM_OPT) + stash_section_index = self.column_names.index(self.STREQ_NL_SECT_OPT) + stash_item_index = self.column_names.index(self.STREQ_NL_ITEM_OPT) stash_section = model.get_value(row_iter, stash_section_index) stash_item = model.get_value(row_iter, stash_item_index) - if (col_index == sect_index or - col_name in [self.DESCRIPTION_TITLE, self.INCLUDED_TITLE]): + if col_index == sect_index or col_name in [ + self.DESCRIPTION_TITLE, + self.INCLUDED_TITLE, + ]: option = None if section not in self.sections: return False @@ -344,8 +374,10 @@ def set_tree_tip(self, view, row_iter, col_index, tip): if col_name == self.DESCRIPTION_TITLE: value = str(model.get_value(row_iter, col_index)) metadata = stash_util.get_stash_section_meta( - self._stashmaster_meta_lookup, stash_section, stash_item, - value + self._stashmaster_meta_lookup, + stash_section, + stash_item, + value, ) help_ = metadata.get(metomi.rose.META_PROP_HELP) if help_ is not None: @@ -353,20 +385,23 @@ def set_tree_tip(self, view, row_iter, col_index, tip): else: option = self.column_names[col_index] id_ = self.util.get_id_from_section_option(section, option) - if (id_ not in self.var_id_map): - tip.set_text(str( - model.get_value(row_iter, col_index))) + if id_ not in self.var_id_map: + tip.set_text(str(model.get_value(row_iter, col_index))) return True id_data = self.var_id_map[id_] value = str(model.get_value(row_iter, col_index)) - tip_text = metomi.rose.CONFIG_DELIMITER.join( - [section, option, value]) + "\n" - if (option in self.OPTION_NL_MAP and - option in list(self._profile_location_map.keys())): + tip_text = ( + metomi.rose.CONFIG_DELIMITER.join([section, option, value]) + + "\n" + ) + if option in self.OPTION_NL_MAP and option in list( + self._profile_location_map.keys() + ): profile_id = self._profile_location_map[option].get(value) if profile_id is not None: profile_sect = self.util.get_section_option_from_id( - profile_id)[0] + profile_id + )[0] tip_text += "See " + profile_sect tip_text += id_data.metadata.get(metomi.rose.META_PROP_DESCRIPTION, "") if tip_text: @@ -374,7 +409,9 @@ def set_tree_tip(self, view, row_iter, col_index, tip): for key, value in list(id_data.error.items()): tip_text += ( metomi.rose.config_editor.SUMMARY_DATA_PANEL_ERROR_TIP.format( - key, value)) + key, value + ) + ) for key in id_data.ignored_reason: tip_text += "({0})\n".format(key) if option is None: @@ -392,8 +429,10 @@ def _get_custom_menu_items(self, path, col, event): model = self._view.get_model() col_index = self._view.get_columns().index(col) col_title = self.column_names[col_index] - if (col_title not in self.OPTION_NL_MAP and - col_title != self.DESCRIPTION_TITLE): + if ( + col_title not in self.OPTION_NL_MAP + and col_title != self.DESCRIPTION_TITLE + ): return [] iter_ = model.get_iter(path) value = model.get_value(iter_, col_index) @@ -401,9 +440,11 @@ def _get_custom_menu_items(self, path, col, event): meta_key = self.STASH_PARSE_DESC_OPT + "=" + str(value) metadata = self._stashmaster_meta_lookup.get(meta_key, {}) help_ = metadata.get(metomi.rose.META_PROP_HELP) - if help_ is not None: + if help_ is not None: menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_HELP, Gtk.IconSize.MENU) + menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_HELP, Gtk.IconSize.MENU + ) menuitem_label = Gtk.Label(label="Help") menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) @@ -419,15 +460,16 @@ def _get_custom_menu_items(self, path, col, event): return [] location = self._profile_location_map[col_title][value] menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU) + menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_ABOUT, Gtk.IconSize.MENU + ) menuitem_label = Gtk.Label(label="View " + value.strip("'")) menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) menuitem_box.pack_start(menuitem_label, False, False, 0) - Gtk.Container.add(menuitem, menuitem_box) + Gtk.Container.add(menuitem, menuitem_box) menuitem._loc_id = location - menuitem.connect("activate", - lambda i: self.search_function(i._loc_id)) + menuitem.connect("activate", lambda i: self.search_function(i._loc_id)) menuitem.show() menuitems.append(menuitem) profiles_menuitems = [] @@ -435,20 +477,30 @@ def _get_custom_menu_items(self, path, col, event): label = "View " + profile.strip("'") menuitem = Gtk.MenuItem(label=label) menuitem._loc_id = self._profile_location_map[col_title][profile] - menuitem.connect("button-release-event", - lambda i, e: self.search_function(i._loc_id)) + menuitem.connect( + "button-release-event", + lambda i, e: self.search_function(i._loc_id), + ) menuitem.show() profiles_menuitems.append(menuitem) if profiles_menuitems: profiles_menu = Gtk.Menu() profiles_menu.show() profiles_root_menuitem_box = Gtk.Box() - profiles_root_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU) + profiles_root_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_ABOUT, Gtk.IconSize.MENU + ) profiles_root_menuitem_label = Gtk.Label(label="View...") profiles_root_menuitem = Gtk.MenuItem() - profiles_root_menuitem_box.pack_start(profiles_root_menuitem_icon, False, False, 0) - profiles_root_menuitem_box.pack_start(profiles_root_menuitem_label, False, False, 0) - Gtk.Container.add(profiles_root_menuitem, profiles_root_menuitem_box) + profiles_root_menuitem_box.pack_start( + profiles_root_menuitem_icon, False, False, 0 + ) + profiles_root_menuitem_box.pack_start( + profiles_root_menuitem_label, False, False, 0 + ) + Gtk.Container.add( + profiles_root_menuitem, profiles_root_menuitem_box + ) profiles_root_menuitem.show() profiles_root_menuitem.set_submenu(profiles_menu) for profiles_menuitem in profiles_menuitems: @@ -458,14 +510,17 @@ def _get_custom_menu_items(self, path, col, event): def add_new_stash_request(self, section, item, launch_dialog=False): """Add a new streq namelist.""" - new_opt_map = {self.STREQ_NL_SECT_OPT: section, - self.STREQ_NL_ITEM_OPT: item} + new_opt_map = { + self.STREQ_NL_SECT_OPT: section, + self.STREQ_NL_ITEM_OPT: item, + } new_section = self.add_section(None, opt_map=new_opt_map) if launch_dialog: metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, "Added request as {0}".format(new_section), - "New Request") + "New Request", + ) def generate_package_lookup(self): """Store a dictionary of package requests and domains.""" @@ -476,8 +531,9 @@ def generate_package_lookup(self): continue base_sect = sect.rsplit("(", 1)[0] if base_sect == self.STREQ_NL_BASE: - package_node = node.get([self.STREQ_NL_PACKAGE_OPT], - no_ignore=True) + package_node = node.get( + [self.STREQ_NL_PACKAGE_OPT], no_ignore=True + ) if package_node is not None: package = package_node.value self._package_lookup.setdefault(package, {}) @@ -487,9 +543,11 @@ def generate_package_lookup(self): profile_node = node.get([profile], no_ignore=True) if profile_node is not None: self._package_lookup[package].setdefault( - profile, []) + profile, [] + ) self._package_lookup[package][profile].append( - profile_node.value) + profile_node.value + ) continue for profile, profile_nl in list(self.OPTION_NL_MAP.items()): if base_sect == profile_nl: @@ -503,33 +561,36 @@ def generate_package_lookup(self): def load_stash(self): """Load a STASHmaster file into data structures for later use.""" self._stash_lookup = self.get_stashmaster_lookup_dict() - package_config_file = os.path.join(self.STASH_PACKAGE_PATH, - metomi.rose.SUB_CONFIG_NAME) + package_config_file = os.path.join( + self.STASH_PACKAGE_PATH, metomi.rose.SUB_CONFIG_NAME + ) self.package_config = metomi.rose.config.ConfigNode() - metomi.rose.config.ConfigLoader().load_with_opts(package_config_file, - self.package_config) + metomi.rose.config.ConfigLoader().load_with_opts( + package_config_file, self.package_config + ) self.generate_package_lookup() - self._stashmaster_meta_lookup = ( - self.get_stashmaster_meta_map()) + self._stashmaster_meta_lookup = self.get_stashmaster_meta_map() def _add_new_diagnostic_launcher(self): # Create a button for launching the "Add new STASH" dialog. self._add_button = metomi.rose.gtk.util.CustomButton( label=self.ADD_NEW_STASH_LABEL, stock_id=Gtk.STOCK_ADD, - tip_text=self.ADD_NEW_STASH_TIP) + tip_text=self.ADD_NEW_STASH_TIP, + ) package_button = metomi.rose.gtk.util.CustomButton( label=self.PACKAGE_MANAGER_LABEL, tip_text=self.PACKAGE_MANAGER_TIP, - has_menu=True) - self.control_widget_hbox.pack_end(package_button, expand=False, - fill=False, padding=0) - self.control_widget_hbox.pack_end(self._add_button, - expand=False, fill=False, padding=0) - self._add_button.connect("clicked", - self._launch_new_diagnostic_window) - package_button.connect("button-press-event", - self._package_menu_launch) + has_menu=True, + ) + self.control_widget_hbox.pack_end( + package_button, expand=False, fill=False, padding=0 + ) + self.control_widget_hbox.pack_end( + self._add_button, expand=False, fill=False, padding=0 + ) + self._add_button.connect("clicked", self._launch_new_diagnostic_window) + package_button.connect("button-press-event", self._package_menu_launch) def _handle_activation(self, view, path, column): # React to row activation in the TreeView. @@ -554,8 +615,9 @@ def _handle_activation(self, view, path, column): id_ = self.util.get_id_from_section_option(section, option) self.search_function(id_) - def _handle_cell_combo_change(self, combo_cell, path_string, new, - col_title): + def _handle_cell_combo_change( + self, combo_cell, path_string, new, col_title + ): # Handle a Gtk.CellRendererCombo (variable) value change. if isinstance(new, str): new_value = new @@ -570,8 +632,9 @@ def _handle_cell_combo_change(self, combo_cell, path_string, new, self.var_ops.set_var_value(var, new_value) return False - def _handle_cell_text_change(self, text_cell, path_string, new_text, - col_title): + def _handle_cell_text_change( + self, text_cell, path_string, new_text, col_title + ): # Handle a Gtk.CellRendererText (variable) value change. row_iter = self._view.get_model().get_iter(path_string) sect_index = self.get_section_column_index() @@ -600,9 +663,11 @@ def _get_request_lookup(self): request_lookup = {} for section in self.sections: stash_sect_id = self.util.get_id_from_section_option( - section, self.STREQ_NL_SECT_OPT) + section, self.STREQ_NL_SECT_OPT + ) stash_item_id = self.util.get_id_from_section_option( - section, self.STREQ_NL_ITEM_OPT) + section, self.STREQ_NL_ITEM_OPT + ) sect_var = self.var_id_map.get(stash_sect_id) item_var = self.var_id_map.get(stash_item_id) if sect_var is None or item_var is None: @@ -614,7 +679,8 @@ def _get_request_lookup(self): request_lookup[st_sect][st_item][section] = {} for variable in self.variables.get(section, []): request_lookup[st_sect][st_item][section].update( - {variable.name: variable.value}) + {variable.name: variable.value} + ) return request_lookup def _get_request_changes(self): @@ -638,7 +704,8 @@ def _launch_new_diagnostic_window(self, widget=None): request_lookup = self._get_request_lookup() request_changes = self._get_request_changes() add_new_func = lambda s, i: ( - self.add_new_stash_request(s, i, launch_dialog=True)) + self.add_new_stash_request(s, i, launch_dialog=True) + ) self._diag_panel = stash_add.AddStashDiagnosticsPanelv1( self._stash_lookup, request_lookup, @@ -646,7 +713,7 @@ def _launch_new_diagnostic_window(self, widget=None): self._stashmaster_meta_lookup, add_new_func, self.scroll_to_section, - self._refresh_diagnostic_window + self._refresh_diagnostic_window, ) window.add(self._diag_panel) window.set_default_size(900, 800) @@ -656,16 +723,18 @@ def _launch_new_diagnostic_window(self, widget=None): def _launch_record_help(self, menuitem): """Launch the help from a menu.""" - metomi.rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, - menuitem._help_title) + metomi.rose.gtk.dialog.run_scrolled_dialog( + menuitem._help_text, menuitem._help_title + ) def _refresh_diagnostic_window(self): # Refresh information in the "new STASH request" dialog. if self._diag_panel is not None: request_lookup = self._get_request_lookup() request_changes = self._get_request_changes() - self._diag_panel.update_request_info(request_lookup, - request_changes) + self._diag_panel.update_request_info( + request_lookup, request_changes + ) def _set_tree_cell_value_combo(self, column, cell, treemodel, iter_, _): # Extract a value for a combo box cell renderer. @@ -725,8 +794,9 @@ def _update_available_profiles(self): self._available_profile_map[name] = [] for id_, value in list(self.sub_ops.get_var_id_values().items()): section, option = self.util.get_section_option_from_id(id_) - if (option in ok_var_names and - any(section.startswith(n) for n in ok_sect_names)): + if option in ok_var_names and any( + section.startswith(n) for n in ok_sect_names + ): self._profile_location_map.setdefault(option, {}) self._profile_location_map[option].update({value: id_}) self._available_profile_map.setdefault(option, []) @@ -743,7 +813,8 @@ def _package_add(self, package): else: for profile_name in values: sect = self._package_profile_lookup[sect_type].get( - profile_name) + profile_name + ) sections_for_adding.append(sect) sections_for_adding = sorted(set(sections_for_adding)) for section in sections_for_adding: @@ -763,8 +834,10 @@ def _package_menu_launch(self, widget, event): for section, vars_ in list(self.variables.items()): for var in vars_: if var.name == self.STREQ_NL_PACKAGE_OPT: - is_ignored = (metomi.rose.variable.IGNORED_BY_USER in - self.sections[section].ignored_reason) + is_ignored = ( + metomi.rose.variable.IGNORED_BY_USER + in self.sections[section].ignored_reason + ) packages.setdefault(var.value, []) packages[var.value].append(is_ignored) for package in sorted(packages.keys()): @@ -774,51 +847,74 @@ def _package_menu_launch(self, widget, event): package_menuitem.show() package_menu = Gtk.Menu() enable_menuitem_box = Gtk.Box() - enable_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_YES, Gtk.IconSize.MENU) + enable_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_YES, Gtk.IconSize.MENU + ) enable_menuitem_label = Gtk.Label(label="Enable all") enable_menuitem = Gtk.MenuItem() - enable_menuitem_box.pack_start(enable_menuitem_icon, False, False, 0) - enable_menuitem_box.pack_start(enable_menuitem_label, False, False, 0) + enable_menuitem_box.pack_start( + enable_menuitem_icon, False, False, 0 + ) + enable_menuitem_box.pack_start( + enable_menuitem_label, False, False, 0 + ) Gtk.Container.add(enable_menuitem, enable_menuitem_box) enable_menuitem._connect_args = (package, False) enable_menuitem.connect( "button-release-event", - lambda m, e: self._packages_enable(*m._connect_args)) + lambda m, e: self._packages_enable(*m._connect_args), + ) enable_menuitem.show() enable_menuitem.set_sensitive(any(ignored_list)) package_menu.append(enable_menuitem) ignore_menuitem_box = Gtk.Box() - ignore_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + ignore_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_NO, Gtk.IconSize.MENU + ) ignore_menuitem_label = Gtk.Label(label="Ignore all") ignore_menuitem = Gtk.MenuItem() - ignore_menuitem_box.pack_start(ignore_menuitem_icon, False, False, 0) - ignore_menuitem_box.pack_start(ignore_menuitem_label, False, False, 0) + ignore_menuitem_box.pack_start( + ignore_menuitem_icon, False, False, 0 + ) + ignore_menuitem_box.pack_start( + ignore_menuitem_label, False, False, 0 + ) Gtk.Container.add(ignore_menuitem, ignore_menuitem_box) ignore_menuitem._connect_args = (package, True) ignore_menuitem.connect( "button-release-event", - lambda m, e: self._packages_enable(*m._connect_args)) + lambda m, e: self._packages_enable(*m._connect_args), + ) ignore_menuitem.set_sensitive(any(not i for i in ignored_list)) ignore_menuitem.show() - package_menu.append(ignore_menuitem) + package_menu.append(ignore_menuitem) remove_menuitem_box = Gtk.Box() - remove_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_REMOVE, Gtk.IconSize.MENU) + remove_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) remove_menuitem_label = Gtk.Label(label="Remove all") remove_menuitem = Gtk.MenuItem() - remove_menuitem_box.pack_start(remove_menuitem_icon, False, False, 0) - remove_menuitem_box.pack_start(remove_menuitem_label, False, False, 0) + remove_menuitem_box.pack_start( + remove_menuitem_icon, False, False, 0 + ) + remove_menuitem_box.pack_start( + remove_menuitem_label, False, False, 0 + ) Gtk.Container.add(remove_menuitem, remove_menuitem_box) remove_menuitem._connect_args = (package,) remove_menuitem.connect( "button-release-event", - lambda m, e: self._packages_remove(*m._connect_args)) + lambda m, e: self._packages_remove(*m._connect_args), + ) remove_menuitem.show() package_menu.append(remove_menuitem) package_menuitem.set_submenu(package_menu) menu.append(package_menuitem) - package_menu.show_all() + package_menu.show_all() menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_ADD, Gtk.IconSize.MENU + ) menuitem_label = Gtk.Label(label="Import") menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) @@ -831,7 +927,8 @@ def _package_menu_launch(self, widget, event): new_pack_menuitem._connect_args = (new_package,) new_pack_menuitem.connect( "button-release-event", - lambda m, e: self._package_add(*m._connect_args)) + lambda m, e: self._package_add(*m._connect_args), + ) new_pack_menuitem.show() import_menu.append(new_pack_menuitem) if not new_packages: @@ -840,17 +937,22 @@ def _package_menu_launch(self, widget, event): menuitem.show() menu.append(menuitem) menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_NO, Gtk.IconSize.MENU) + menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_NO, Gtk.IconSize.MENU + ) menuitem_label = Gtk.Label(label="Disable all packages") menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) menuitem_box.pack_start(menuitem_label, False, False, 0) Gtk.Container.add(menuitem, menuitem_box) - menuitem.connect("activate", - lambda i: self._packages_enable(disable=True)) + menuitem.connect( + "activate", lambda i: self._packages_enable(disable=True) + ) menuitem.show() menu.append(menuitem) - menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + menu.popup_at_widget( + widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) def _packages_remove(self, only_this_package=None): # Remove requests and no-longer-needed profiles for packages. @@ -860,10 +962,13 @@ def _packages_remove(self, only_this_package=None): for section, vars_ in list(self.variables.items()): for var in vars_: if var.name == self.STREQ_NL_PACKAGE_OPT: - if (only_this_package is None or - var.value == only_this_package): + if ( + only_this_package is None + or var.value == only_this_package + ): sect = self.util.get_section_option_from_id( - var.metadata["id"])[0] + var.metadata["id"] + )[0] if sect not in sections_for_removing: sections_for_removing.append(sect) elif var.name in self.OPTION_NL_MAP: @@ -876,11 +981,13 @@ def _packages_remove(self, only_this_package=None): if all([s in streq_remove_list for s in streq_list]): # This is only referenced by sections about to be removed. profile_id = self._profile_location_map.get( - profile_type, {}).get(name) + profile_type, {} + ).get(name) if profile_id is None: continue profile_section = self.util.get_section_option_from_id( - profile_id)[0] + profile_id + )[0] sections_for_removing.append(profile_section) self.sub_ops.remove_sections(sections_for_removing) @@ -890,13 +997,18 @@ def _packages_enable(self, only_this_package=None, disable=False): for vars_ in list(self.variables.values()): for var in vars_: if var.name == self.STREQ_NL_PACKAGE_OPT: - if (only_this_package is None or - var.value == only_this_package): + if ( + only_this_package is None + or var.value == only_this_package + ): sect = self.util.get_section_option_from_id( - var.metadata["id"])[0] + var.metadata["id"] + )[0] if sect not in sections_for_changing: - is_ignored = (metomi.rose.variable.IGNORED_BY_USER in - self.sections[sect].ignored_reason) + is_ignored = ( + metomi.rose.variable.IGNORED_BY_USER + in self.sections[sect].ignored_reason + ) if is_ignored != disable: sections_for_changing.append(sect) self.sub_ops.ignore_sections(sections_for_changing, disable) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_add.py b/metomi/rose/config_editor/plugin/um/widget/stash_add.py index 26dbeca17..4ceb44517 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_add.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_add.py @@ -20,6 +20,7 @@ from gi.repository import Pango import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk @@ -31,19 +32,24 @@ from functools import cmp_to_key -class AddStashDiagnosticsPanelv1(Gtk.Box): +class AddStashDiagnosticsPanelv1(Gtk.Box): """Display a grouped set of stash requests to add.""" STASH_PARSE_DESC_OPT = "name" STASH_PARSE_ITEM_OPT = "item" STASH_PARSE_SECT_OPT = "sectn" - def __init__(self, stash_lookup, request_lookup, - changed_request_lookup, stash_meta_lookup, - add_stash_request_func, - navigate_to_stash_request_func, - refresh_stash_requests_func): + def __init__( + self, + stash_lookup, + request_lookup, + changed_request_lookup, + stash_meta_lookup, + add_stash_request_func, + navigate_to_stash_request_func, + refresh_stash_requests_func, + ): """Create a widget displaying STASHmaster information. stash_lookup is a nested dictionary that uses STASH section @@ -83,7 +89,9 @@ def __init__(self, stash_lookup, request_lookup, info. """ - super(AddStashDiagnosticsPanelv1, self).__init__(self, orientation=Gtk.Orientation.VERTICAL) + super(AddStashDiagnosticsPanelv1, self).__init__( + self, orientation=Gtk.Orientation.VERTICAL + ) self.set_property("homogeneous", False) self.stash_lookup = stash_lookup self.request_lookup = request_lookup @@ -106,18 +114,25 @@ def __init__(self, stash_lookup, request_lookup, self._should_show_meta_column_titles = False self.control_widget_hbox = self._get_control_widget_hbox() - self.pack_start(self.control_widget_hbox, expand=False, fill=False, padding=0) + self.pack_start( + self.control_widget_hbox, expand=False, fill=False, padding=0 + ) self._view = metomi.rose.gtk.util.TooltipTreeView( - get_tooltip_func=self.set_tree_tip) + get_tooltip_func=self.set_tree_tip + ) self._view.set_rules_hint(True) self.sort_util = metomi.rose.gtk.util.TreeModelSortUtil( - self._view.get_model, 2) + self._view.get_model, 2 + ) self._view.show() - self._view.connect("button-press-event", - self._handle_button_press_event) + self._view.connect( + "button-press-event", self._handle_button_press_event + ) self._view.connect("cursor-changed", self._update_control_sensitivity) self._window = Gtk.ScrolledWindow() - self._window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) self.generate_tree_view(is_startup=True) self._window.add(self._view) self._window.show() @@ -129,8 +144,7 @@ def add_cell_renderer_for_value(self, column): """Add a cell renderer to represent the model value.""" cell_for_value = Gtk.CellRendererText() column.pack_start(cell_for_value, True) - column.set_cell_data_func(cell_for_value, - self._set_tree_cell_value) + column.set_cell_data_func(cell_for_value, self._set_tree_cell_value) def add_stash_request(self, section, item): """Handle an add stash request call.""" @@ -175,8 +189,11 @@ def get_model_data_and_columns(self): columns = ["Section", "Item", "Description", "?", "#"] sections = list(self.stash_lookup.keys()) sections.sort(key=cmp_to_key(self.sort_util.cmp_)) - props_excess = [self.STASH_PARSE_DESC_OPT, self.STASH_PARSE_ITEM_OPT, - self.STASH_PARSE_SECT_OPT] + props_excess = [ + self.STASH_PARSE_DESC_OPT, + self.STASH_PARSE_ITEM_OPT, + self.STASH_PARSE_SECT_OPT, + ] for section in sections: if section == "-1": continue @@ -198,7 +215,8 @@ def get_tree_model(self): """Construct a data model of other page data.""" data_rows, cols = self.get_model_data_and_columns() data_rows, cols, rows_are_descendants = self._apply_grouping( - data_rows, cols, self.group_index) + data_rows, cols, self.group_index + ) self.column_names = cols if data_rows: col_types = [str] * len(data_rows[0]) @@ -220,8 +238,9 @@ def get_tree_model(self): sort_model = Gtk.TreeModelSort(filter_model) for i in range(len(self.column_names)): sort_model.set_sort_func(i, self.sort_util.sort_column, i) - sort_model.connect("sort-column-changed", - self.sort_util.handle_sort_column_change) + sort_model.connect( + "sort-column-changed", self.sort_util.handle_sort_column_change + ) return sort_model def set_tree_tip(self, treeview, row_iter, col_index, tip): @@ -252,7 +271,10 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): return False if name == "?": name = "Requests Status" - if value == metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP: + if ( + value + == metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + ): value = "changed" else: value = "no changes" @@ -294,8 +316,9 @@ def set_tree_tip(self, treeview, row_iter, col_index, tip): tip.set_text(text) return True - def update_request_info(self, request_lookup=None, - changed_request_lookup=None): + def update_request_info( + self, request_lookup=None, changed_request_lookup=None + ): """Refresh streq namelist information.""" if request_lookup is not None: self.request_lookup = request_lookup @@ -306,8 +329,12 @@ def update_request_info(self, request_lookup=None, streq_info_index = self.column_names.index("?") num_streqs_index = self.column_names.index("#") # For speed, pass in the relevant indices here. - user_data = (sect_col_index, item_col_index, - streq_info_index, num_streqs_index) + user_data = ( + sect_col_index, + item_col_index, + streq_info_index, + num_streqs_index, + ) self._store.foreach(self._update_row_request_info, user_data) # Loop over any parent rows and sum numbers and info. parent_iter = self._store.iter_children(None) @@ -326,16 +353,22 @@ def update_request_info(self, request_lookup=None, if info and not streq_info_children: streq_info_children = info child_iter = self._store.iter_next(child_iter) - self._store.set_value(parent_iter, num_streqs_index, - str(num_streq_children)) - self._store.set_value(parent_iter, streq_info_index, - streq_info_children) + self._store.set_value( + parent_iter, num_streqs_index, str(num_streq_children) + ) + self._store.set_value( + parent_iter, streq_info_index, streq_info_children + ) parent_iter = self._store.iter_next(parent_iter) def _update_row_request_info(self, model, path, iter_, user_data): # Update the streq namelist information for a model row. - (sect_col_index, item_col_index, - streq_info_index, num_streqs_index) = user_data + ( + sect_col_index, + item_col_index, + streq_info_index, + num_streqs_index, + ) = user_data section = model.get_value(iter_, sect_col_index) item = model.get_value(iter_, item_col_index) if section is None or item is None: @@ -345,7 +378,9 @@ def _update_row_request_info(self, model, path, iter_, user_data): streqs = self.request_lookup.get(section, {}).get(item, {}) model.set_value(iter_, num_streqs_index, str(len(streqs))) streq_info = "" - mod_markup = metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + mod_markup = ( + metomi.rose.config_editor.SUMMARY_DATA_PANEL_MODIFIED_MARKUP + ) for streq_section in streqs: if streq_section in self.changed_request_lookup: streq_info = mod_markup + streq_info @@ -356,14 +391,15 @@ def _append_row_data(self, model, path, iter_, data_rows): # Append new row data. data_rows.append(model.get(iter_)) - def _apply_grouping(self, data_rows, column_names, group_index=None, - descending=False): + def _apply_grouping( + self, data_rows, column_names, group_index=None, descending=False + ): # Calculate nesting (grouping) for the data. rows_are_descendants = None if group_index is None: return data_rows, column_names, rows_are_descendants k = group_index - data_rows = [r[k:k + 1] + r[0:k] + r[k + 1:] for r in data_rows] + data_rows = [r[k : k + 1] + r[0:k] + r[k + 1 :] for r in data_rows] column_names.insert(0, column_names.pop(k)) if descending: data_rows.sort(key=cmp_to_key(self._sort_row_data), reverse=True) @@ -389,8 +425,10 @@ def _filter_visible(self, model, iter_, _): if not filt_text: return True for col_text in model.get(iter_, *list(range(len(self.column_names)))): - if (isinstance(col_text, str) and - filt_text.lower() in col_text.lower()): + if ( + isinstance(col_text, str) + and filt_text.lower() in col_text.lower() + ): return True child_iter = model.iter_children(iter_) while child_iter is not None: @@ -401,54 +439,74 @@ def _filter_visible(self, model, iter_, _): def _get_control_widget_hbox(self): # Build the control widgets for the dialog. - filter_label = Gtk.Label(label= - metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL) + filter_label = Gtk.Label( + label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_LABEL + ) filter_label.show() self._filter_widget = Gtk.Entry() self._filter_widget.set_width_chars( - metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR) + metomi.rose.config_editor.SUMMARY_DATA_PANEL_FILTER_MAX_CHAR + ) self._filter_widget.connect("changed", self._filter_refresh) self._filter_widget.set_tooltip_text("Filter by literal values") self._filter_widget.show() - group_label = Gtk.Label(label= - metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL) + group_label = Gtk.Label( + label=metomi.rose.config_editor.SUMMARY_DATA_PANEL_GROUP_LABEL + ) group_label.show() self._group_widget = Gtk.ComboBox() cell = Gtk.CellRendererText() self._group_widget.pack_start(cell, True) - self._group_widget.add_attribute(cell, 'text', 0) + self._group_widget.add_attribute(cell, "text", 0) self._group_widget.show() self._add_button = metomi.rose.gtk.util.CustomButton( label="Add", stock_id=Gtk.STOCK_ADD, - tip_text="Add a new request for this entry") - self._add_button.connect("activate", - lambda b: self._handle_add_current_row()) - self._add_button.connect("clicked", - lambda b: self._handle_add_current_row()) + tip_text="Add a new request for this entry", + ) + self._add_button.connect( + "activate", lambda b: self._handle_add_current_row() + ) + self._add_button.connect( + "clicked", lambda b: self._handle_add_current_row() + ) self._refresh_button = metomi.rose.gtk.util.CustomButton( label="Refresh", stock_id=Gtk.STOCK_REFRESH, - tip_text="Refresh namelist:streq statuses") - self._refresh_button.connect("activate", - lambda b: self.refresh_stash_requests()) - self._refresh_button.connect("clicked", - lambda b: self.refresh_stash_requests()) + tip_text="Refresh namelist:streq statuses", + ) + self._refresh_button.connect( + "activate", lambda b: self.refresh_stash_requests() + ) + self._refresh_button.connect( + "clicked", lambda b: self.refresh_stash_requests() + ) self._view_button = metomi.rose.gtk.util.CustomButton( - label="View", - tip_text="Select view options", - has_menu=True) - self._view_button.connect("button-press-event", - self._popup_view_menu) + label="View", tip_text="Select view options", has_menu=True + ) + self._view_button.connect("button-press-event", self._popup_view_menu) filter_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - filter_hbox.pack_start(group_label, expand=False, fill=False, padding=0) - filter_hbox.pack_start(self._group_widget, expand=False, fill=False, padding=0) - filter_hbox.pack_start(filter_label, expand=False, fill=False, - padding=10) - filter_hbox.pack_start(self._filter_widget, expand=False, fill=False, padding=0) - filter_hbox.pack_end(self._view_button, expand=False, fill=False, padding=0) - filter_hbox.pack_end(self._refresh_button, expand=False, fill=False, padding=0) - filter_hbox.pack_end(self._add_button, expand=False, fill=False, padding=0) + filter_hbox.pack_start( + group_label, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_start( + self._group_widget, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_start( + filter_label, expand=False, fill=False, padding=10 + ) + filter_hbox.pack_start( + self._filter_widget, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_end( + self._view_button, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_end( + self._refresh_button, expand=False, fill=False, padding=0 + ) + filter_hbox.pack_end( + self._add_button, expand=False, fill=False, padding=0 + ) filter_hbox.show() return filter_hbox @@ -516,8 +574,10 @@ def _handle_group_change(self, combobox): self._hidden_column_names.remove(col_name) group_index = self.column_names.index(col_name) # Any existing grouping changes the order of self.column_names. - if (self.group_index is not None and - group_index <= self.group_index): + if ( + self.group_index is not None + and group_index <= self.group_index + ): group_index -= 1 else: group_index = None @@ -529,8 +589,9 @@ def _handle_group_change(self, combobox): def _launch_record_help(self, menuitem): """Launch the help from a menu.""" - metomi.rose.gtk.dialog.run_scrolled_dialog(menuitem._help_text, - menuitem._help_title) + metomi.rose.gtk.dialog.run_scrolled_dialog( + menuitem._help_text, menuitem._help_title + ) def _popup_tree_menu(self, path, col, event): """Launch a menu for this main treeview row.""" @@ -542,24 +603,30 @@ def _popup_tree_menu(self, path, col, event): if section is None or item is None: return False add_menuitem_box = Gtk.Box() - add_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_ADD, Gtk.IconSize.MENU) + add_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_ADD, Gtk.IconSize.MENU + ) add_menuitem_label = Gtk.Label(label="Add STASH request") add_menuitem = Gtk.MenuItem() add_menuitem_box.pack_start(add_menuitem_icon, False, False, 0) add_menuitem_box.pack_start(add_menuitem_label, False, False, 0) Gtk.Container.add(add_menuitem, add_menuitem_box) - add_menuitem.connect("activate", - lambda i: self.add_stash_request(section, item)) + add_menuitem.connect( + "activate", lambda i: self.add_stash_request(section, item) + ) add_menuitem.show() menu.append(add_menuitem) stash_desc_index = self.column_names.index("Description") stash_desc_value = model.get_value(row_iter, stash_desc_index) desc_meta = self.stash_meta_lookup.get( - self.STASH_PARSE_DESC_OPT + "=" + str(stash_desc_value), {}) + self.STASH_PARSE_DESC_OPT + "=" + str(stash_desc_value), {} + ) desc_meta_help = desc_meta.get(metomi.rose.META_PROP_HELP) if desc_meta_help is not None: help_menuitem_box = Gtk.Box() - help_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_HELP, Gtk.IconSize.MENU) + help_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_HELP, Gtk.IconSize.MENU + ) help_menuitem_label = Gtk.Label(label="Help") help_menuitem = Gtk.MenuItem() help_menuitem_box.pack_start(help_menuitem_icon, False, False, 0) @@ -570,10 +637,14 @@ def _popup_tree_menu(self, path, col, event): help_menuitem.connect("activate", self._launch_record_help) help_menuitem.show() menu.append(help_menuitem) - streqs = list(self.request_lookup.get(section, {}).get(item, {}).keys()) + streqs = list( + self.request_lookup.get(section, {}).get(item, {}).keys() + ) if streqs: view_menuitem_box = Gtk.Box() - view_menuitem_icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_FIND, Gtk.IconSize.MENU) + view_menuitem_icon = Gtk.Image.new_from_icon_name( + Gtk.STOCK_FIND, Gtk.IconSize.MENU + ) view_menuitem_label = Gtk.Label(label="View...") view_menuitem = Gtk.MenuItem() view_menuitem_box.pack_start(view_menuitem_icon, False, False, 0) @@ -589,11 +660,14 @@ def _popup_tree_menu(self, path, col, event): view_streq_menuitem._section = streq view_streq_menuitem.connect( "button-release-event", - lambda m, e: self.navigate_to_stash_request(m._section)) + lambda m, e: self.navigate_to_stash_request(m._section), + ) view_streq_menuitem.show() view_menu.append(view_streq_menuitem) menu.append(view_menuitem) - menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + menu.popup_at_widget( + event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) return False def _popup_view_menu(self, widget, event): @@ -608,11 +682,13 @@ def _popup_view_menu(self, widget, event): meta_menuitem.set_sensitive(False) menu.append(meta_menuitem) col_title_menuitem = Gtk.CheckMenuItem( - label="Show expanded column titles") + label="Show expanded column titles" + ) if self._should_show_meta_column_titles: col_title_menuitem.set_active(True) - col_title_menuitem.connect("toggled", - self._toggle_show_meta_column_titles) + col_title_menuitem.connect( + "toggled", self._toggle_show_meta_column_titles + ) col_title_menuitem.show() if not self.stash_meta_lookup: col_title_menuitem.set_sensitive(False) @@ -633,16 +709,20 @@ def _popup_view_menu(self, widget, event): title = col_meta.get(metomi.rose.META_PROP_TITLE) if title is not None: col_title = title - col_menuitem = Gtk.CheckMenuItem(label=col_title, - use_underline=False) + col_menuitem = Gtk.CheckMenuItem( + label=col_title, use_underline=False + ) col_menuitem.show() col_menuitem.set_active(column.get_visible()) col_menuitem._connect_args = (col_name,) col_menuitem.connect( "toggled", - lambda c: self._toggle_show_column_name(*c._connect_args)) + lambda c: self._toggle_show_column_name(*c._connect_args), + ) show_column_menu.append(col_menuitem) - menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + menu.popup_at_widget( + widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) def _set_tree_cell_value(self, column, cell, treemodel, iter_, _): # Extract an appropriate value for this cell from the model. @@ -706,5 +786,6 @@ def _toggle_show_meta_column_titles(self, widget): def _update_control_sensitivity(self, _=None): section, item = self._get_current_section_item() - self._add_button.set_sensitive(section is not None and - item is not None) + self._add_button.set_sensitive( + section is not None and item is not None + ) diff --git a/metomi/rose/config_editor/plugin/um/widget/stash_util.py b/metomi/rose/config_editor/plugin/um/widget/stash_util.py index 44879373e..07f398bfe 100644 --- a/metomi/rose/config_editor/plugin/um/widget/stash_util.py +++ b/metomi/rose/config_editor/plugin/um/widget/stash_util.py @@ -20,8 +20,9 @@ """This holds some shared functionality between stash and stash_add.""" -def get_stash_section_meta(stash_meta_lookup, stash_section, - stash_item, stash_description): +def get_stash_section_meta( + stash_meta_lookup, stash_section, stash_item, stash_description +): """Return a dictionary of metadata properties for this stash record.""" try: stash_code = 1000 * int(stash_section) + int(stash_item) diff --git a/metomi/rose/config_editor/stack.py b/metomi/rose/config_editor/stack.py index 0a15a5543..b2e4445ab 100644 --- a/metomi/rose/config_editor/stack.py +++ b/metomi/rose/config_editor/stack.py @@ -21,19 +21,26 @@ import re import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor class StackItem(object): - """A dictionary containing stack information.""" - def __init__(self, page_label, action_text, node, - undo_function, undo_args=None, - group=None, custom_name=None): + def __init__( + self, + page_label, + action_text, + node, + undo_function, + undo_args=None, + group=None, + custom_name=None, + ): self.page_label = page_label self.action = action_text self.node = node @@ -54,12 +61,16 @@ def __init__(self, page_label, action_text, node, self.undo_args = undo_args def __repr__(self): - return (self.action[0].lower() + self.action[1:] + ' ' + self.name + - ", ".join([str(u) for u in self.undo_args])) + return ( + self.action[0].lower() + + self.action[1:] + + " " + + self.name + + ", ".join([str(u) for u in self.undo_args]) + ) class StackViewer(Gtk.Window): - """Window to dynamically display the internal stack.""" def __init__(self, undo_stack, redo_stack, undo_func): @@ -67,22 +78,31 @@ def __init__(self, undo_stack, redo_stack, undo_func): super(StackViewer, self).__init__() self.set_title(metomi.rose.config_editor.STACK_VIEW_TITLE) self.action_colour_map = { - metomi.rose.config_editor.STACK_ACTION_ADDED: - metomi.rose.config_editor.COLOUR_STACK_ADDED, - metomi.rose.config_editor.STACK_ACTION_APPLIED: - metomi.rose.config_editor.COLOUR_STACK_APPLIED, - metomi.rose.config_editor.STACK_ACTION_CHANGED: - metomi.rose.config_editor.COLOUR_STACK_CHANGED, - metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS: - metomi.rose.config_editor.COLOUR_STACK_CHANGED_COMMENTS, - metomi.rose.config_editor.STACK_ACTION_ENABLED: - metomi.rose.config_editor.COLOUR_STACK_ENABLED, - metomi.rose.config_editor.STACK_ACTION_IGNORED: - metomi.rose.config_editor.COLOUR_STACK_IGNORED, - metomi.rose.config_editor.STACK_ACTION_REMOVED: - metomi.rose.config_editor.COLOUR_STACK_REMOVED, - metomi.rose.config_editor.STACK_ACTION_REVERSED: - metomi.rose.config_editor.COLOUR_STACK_REVERSED} + metomi.rose.config_editor.STACK_ACTION_ADDED: ( + metomi.rose.config_editor.COLOUR_STACK_ADDED + ), + metomi.rose.config_editor.STACK_ACTION_APPLIED: ( + metomi.rose.config_editor.COLOUR_STACK_APPLIED + ), + metomi.rose.config_editor.STACK_ACTION_CHANGED: ( + metomi.rose.config_editor.COLOUR_STACK_CHANGED + ), + metomi.rose.config_editor.STACK_ACTION_CHANGED_COMMENTS: ( + metomi.rose.config_editor.COLOUR_STACK_CHANGED_COMMENTS + ), + metomi.rose.config_editor.STACK_ACTION_ENABLED: ( + metomi.rose.config_editor.COLOUR_STACK_ENABLED + ), + metomi.rose.config_editor.STACK_ACTION_IGNORED: ( + metomi.rose.config_editor.COLOUR_STACK_IGNORED + ), + metomi.rose.config_editor.STACK_ACTION_REMOVED: ( + metomi.rose.config_editor.COLOUR_STACK_REMOVED + ), + metomi.rose.config_editor.STACK_ACTION_REVERSED: ( + metomi.rose.config_editor.COLOUR_STACK_REVERSED + ), + } self.undo_func = undo_func self.undo_stack = undo_stack self.redo_stack = redo_stack @@ -90,26 +110,31 @@ def __init__(self, undo_stack, redo_stack, undo_func): self.main_vbox = Gtk.VPaned() accelerators = Gtk.AccelGroup() accel_key, accel_mods = Gtk.accelerator_parse("Z") - accelerators.connect(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, - lambda a, b, c, d: self.undo_from_log()) + accelerators.connect( + accel_key, + accel_mods, + Gtk.AccelFlags.VISIBLE, + lambda a, b, c, d: self.undo_from_log(), + ) accel_key, accel_mods = Gtk.accelerator_parse("Z") - accelerators.connect(accel_key, accel_mods, Gtk.AccelFlags.VISIBLE, - lambda a, b, c, d: - self.undo_from_log(redo_mode_on=True)) + accelerators.connect( + accel_key, + accel_mods, + Gtk.AccelFlags.VISIBLE, + lambda a, b, c, d: self.undo_from_log(redo_mode_on=True), + ) self.add_accel_group(accelerators) self.set_default_size(*metomi.rose.config_editor.SIZE_STACK) self.undo_view = self.get_stack_view(redo_mode_on=False) self.redo_view = self.get_stack_view(redo_mode_on=True) - undo_vbox = self.get_stack_view_box(self.undo_view, - redo_mode_on=False) - redo_vbox = self.get_stack_view_box(self.redo_view, - redo_mode_on=True) + undo_vbox = self.get_stack_view_box(self.undo_view, redo_mode_on=False) + redo_vbox = self.get_stack_view_box(self.redo_view, redo_mode_on=True) self.main_vbox.pack1(undo_vbox, resize=True, shrink=True) self.main_vbox.show() self.main_vbox.pack2(redo_vbox, resize=False, shrink=True) self.main_vbox.show() - self.undo_view.connect('size-allocate', self.scroll_view) - self.redo_view.connect('size-allocate', self.scroll_view) + self.undo_view.connect("size-allocate", self.scroll_view) + self.redo_view.connect("size-allocate", self.scroll_view) self.add(self.main_vbox) self.show() @@ -117,7 +142,9 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): """Return a frame containing a scrolled text view.""" text_view = log_buffer text_scroller = Gtk.ScrolledWindow() - text_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + text_scroller.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS + ) text_scroller.set_shadow_type(Gtk.ShadowType.IN) text_scroller.add(text_view) vadj = text_scroller.get_vadjustment() @@ -126,15 +153,19 @@ def get_stack_view_box(self, log_buffer, redo_mode_on=False): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label = Gtk.Label() if redo_mode_on: - label.set_text('REDO STACK') + label.set_text("REDO STACK") self.redo_text_view = text_view else: - label.set_text('UNDO STACK') + label.set_text("UNDO STACK") self.undo_text_view = text_view label.show() vbox.set_border_width(metomi.rose.config_editor.SPACING_SUB_PAGE) - vbox.pack_start(label, expand=False, fill=True, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + vbox.pack_start( + label, + expand=False, + fill=True, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) vbox.pack_start(text_scroller, expand=True, fill=True, padding=0) vbox.show() return vbox @@ -150,17 +181,22 @@ def get_stack_view(self, redo_mode_on=False): stack_view = Gtk.TreeView(stack_model) columns = {} cell_text = {} - for title in [metomi.rose.config_editor.STACK_COL_NS, - metomi.rose.config_editor.STACK_COL_ACT, - metomi.rose.config_editor.STACK_COL_NAME, - metomi.rose.config_editor.STACK_COL_VALUE, - metomi.rose.config_editor.STACK_COL_OLD_VALUE]: + for title in [ + metomi.rose.config_editor.STACK_COL_NS, + metomi.rose.config_editor.STACK_COL_ACT, + metomi.rose.config_editor.STACK_COL_NAME, + metomi.rose.config_editor.STACK_COL_VALUE, + metomi.rose.config_editor.STACK_COL_OLD_VALUE, + ]: columns[title] = Gtk.TreeViewColumn() columns[title].set_title(title) cell_text[title] = Gtk.CellRendererText() columns[title].pack_start(cell_text[title], True) - columns[title].add_attribute(cell_text[title], attribute='markup', - column=len(list(columns.keys())) - 1) + columns[title].add_attribute( + cell_text[title], + attribute="markup", + column=len(list(columns.keys())) - 1, + ) stack_view.append_column(columns[title]) stack_view.show() return stack_view @@ -171,22 +207,35 @@ def get_stack_model(self, redo_mode_on=False, make_new_model=False): if make_new_model: model = Gtk.ListStore(str, str, str, str, str, bool) else: - model = [self.undo_view.get_model(), - self.redo_view.get_model()][redo_mode_on] + model = [self.undo_view.get_model(), self.redo_view.get_model()][ + redo_mode_on + ] model.clear() for stack_item in stack: marked_up_action = stack_item.action if stack_item.action in self.action_colour_map: colour = self.action_colour_map[stack_item.action] - marked_up_action = ("" + - stack_item.action + "") + marked_up_action = ( + "" + + stack_item.action + + "" + ) if stack_item.page_label is None: - short_label = 'None' + short_label = "None" else: - short_label = re.sub('^/[^/]+/', '', stack_item.page_label) - model.append((short_label, marked_up_action, - stack_item.name, repr(stack_item.value), - repr(stack_item.old_value), False)) + short_label = re.sub("^/[^/]+/", "", stack_item.page_label) + model.append( + ( + short_label, + marked_up_action, + stack_item.name, + repr(stack_item.value), + repr(stack_item.old_value), + False, + ) + ) return model def scroll_view(self, tree_view, event=None): diff --git a/metomi/rose/config_editor/status.py b/metomi/rose/config_editor/status.py index 00f00f73a..f8442e5e9 100644 --- a/metomi/rose/config_editor/status.py +++ b/metomi/rose/config_editor/status.py @@ -23,7 +23,8 @@ import time import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config @@ -33,15 +34,15 @@ class StatusReporter(metomi.rose.reporter.Reporter): - """Handle event notification. load_updater must be a metomi.rose.gtk.splash.SplashScreenProcess instance (or have the same interface to update and stop methods). status_bar_update_func must be a function that accepts a - metomi.rose.reporter.Event, a metomi.rose.reporter kind-of-event string, and a - level of importance/verbosity. See metomi.rose.reporter for more details. + metomi.rose.reporter.Event, a metomi.rose.reporter kind-of-event string, + and a level of importance/verbosity. + See metomi.rose.reporter for more details. """ @@ -52,8 +53,9 @@ def __init__(self, load_updater, status_bar_update_func): self._status_bar_update_func = status_bar_update_func self._no_load = False - def event_handler(self, message, kind=None, level=None, prefix=None, - clip=None): + def event_handler( + self, message, kind=None, level=None, prefix=None, clip=None + ): """Handle a message or event.""" message_kwargs = {} if isinstance(message, metomi.rose.reporter.Event): @@ -68,12 +70,19 @@ def event_handler(self, message, kind=None, level=None, prefix=None, return self._status_bar_update_func(message, kind, level) def report_load_event( - self, text, no_progress=False, new_total_events=None): - """Report a load-related event (to metomi.rose.gtk.util.SplashScreen).""" - event = metomi.rose.reporter.Event(text, - kind=self.EVENT_KIND_LOAD, - no_progress=no_progress, - new_total_events=new_total_events) + self, text, no_progress=False, new_total_events=None + ): + """Report a load-related event + (to metomi.rose.gtk.util.SplashScreen). + + """ + + event = metomi.rose.reporter.Event( + text, + kind=self.EVENT_KIND_LOAD, + no_progress=no_progress, + new_total_events=new_total_events, + ) self.report(event) def set_no_load(self): @@ -85,7 +94,6 @@ def stop(self): class StatusBar(Gtk.Box): - """Generate the status bar widget.""" def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): @@ -97,7 +105,9 @@ def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): hbox.show() self.pack_start(hbox, expand=False, fill=False, padding=0) self._generate_error_widget() - hbox.pack_start(self._error_widget, expand=False, fill=False, padding=0) + hbox.pack_start( + self._error_widget, expand=False, fill=False, padding=0 + ) vsep_message = Gtk.VSeparator() vsep_message.show() vsep_eb = Gtk.EventBox() @@ -105,8 +115,12 @@ def __init__(self, verbosity=metomi.rose.reporter.Reporter.DEFAULT): hbox.pack_start(vsep_message, expand=False, fill=False, padding=0) hbox.pack_start(vsep_eb, expand=True, fill=True, padding=0) self._generate_message_widget() - hbox.pack_end(self._message_widget, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + hbox.pack_end( + self._message_widget, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) self.messages = [] self.show() @@ -116,14 +130,17 @@ def set_message(self, message, kind=None, level=None): kind = message.kind if level is None: level = message.level - if level != None: + if level is not None: if level > self.verbosity: return if isinstance(message, Exception): kind = metomi.rose.reporter.Reporter.KIND_ERR level = metomi.rose.reporter.Reporter.FAIL self.messages.append((kind, str(message), time.time())) - if len(self.messages) > metomi.rose.config_editor.STATUS_BAR_MESSAGE_LIMIT: + if ( + len(self.messages) + > metomi.rose.config_editor.STATUS_BAR_MESSAGE_LIMIT + ): self.messages.pop(0) self._update_message_widget(str(message), kind=kind) self._update_console() @@ -144,15 +161,21 @@ def _generate_error_widget(self): self._error_widget.show() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) icon_path = locator.locate( - 'etc/images/rose-config-edit/error_icon.png') + "etc/images/rose-config-edit/error_icon.png" + ) image = Gtk.Image.new_from_file(str(icon_path)) image.show() - self._error_widget.pack_start(image, expand=False, fill=False, padding=0) + self._error_widget.pack_start( + image, expand=False, fill=False, padding=0 + ) self._error_widget_label = Gtk.Label() self._error_widget_label.show() self._error_widget.pack_start( - self._error_widget_label, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + self._error_widget_label, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) self._update_error_widget() def _generate_message_widget(self): @@ -162,14 +185,15 @@ def _generate_message_widget(self): message_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) message_hbox.show() self._message_widget.add(message_hbox) - self._message_widget.connect("enter-notify-event", - self._handle_enter_message_widget) + self._message_widget.connect( + "enter-notify-event", self._handle_enter_message_widget + ) self._message_widget_error_image = Gtk.Image.new_from_stock( - Gtk.STOCK_DIALOG_ERROR, - Gtk.IconSize.MENU) + Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.MENU + ) self._message_widget_info_image = Gtk.Image.new_from_stock( - Gtk.STOCK_DIALOG_INFO, - Gtk.IconSize.MENU) + Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.MENU + ) self._message_widget_label = Gtk.Label() self._message_widget_label.show() vsep = Gtk.VSeparator() @@ -178,23 +202,36 @@ def _generate_message_widget(self): stock_id=Gtk.STOCK_INFO, size=Gtk.IconSize.MENU, tip_text=metomi.rose.config_editor.STATUS_BAR_CONSOLE_TIP, - as_tool=True) + as_tool=True, + ) self._console_launcher.connect("clicked", self._launch_console) message_hbox.pack_start( self._message_widget_error_image, - expand=False, fill=False, padding=0) + expand=False, + fill=False, + padding=0, + ) message_hbox.pack_start( self._message_widget_info_image, - expand=False, fill=False, padding=0) + expand=False, + fill=False, + padding=0, + ) message_hbox.pack_start( self._message_widget_label, - expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) message_hbox.pack_start( - vsep, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + vsep, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) message_hbox.pack_start( - self._console_launcher, expand=False, fill=False, padding=0) + self._console_launcher, expand=False, fill=False, padding=0 + ) def _update_error_widget(self): # Update the error display widget. @@ -220,14 +257,19 @@ def _handle_enter_message_widget(self, *args): else: prefix = metomi.rose.reporter.Reporter.PREFIX_INFO suffix = datetime.datetime.fromtimestamp(message_time).strftime( - metomi.rose.config_editor.EVENT_TIME) + metomi.rose.config_editor.EVENT_TIME + ) tooltip_text += prefix + " " + message_text + " " + suffix + "\n" tooltip_text = tooltip_text.rstrip() self._message_widget_label.set_tooltip_text(tooltip_text) def _get_console_messages(self): - err_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR - info_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + err_category = ( + metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + ) + info_category = ( + metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + ) message_tuples = [] for kind, message, time_info in self.messages: if kind == metomi.rose.reporter.Reporter.KIND_ERR: @@ -244,14 +286,20 @@ def _launch_console(self, *args): if self.console is not None: return self.console.present() message_tuples = self._get_console_messages() - err_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR - info_category = metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + err_category = ( + metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_ERROR + ) + info_category = ( + metomi.rose.config_editor.STATUS_BAR_CONSOLE_CATEGORY_INFO + ) window = self.get_toplevel() self.console = metomi.rose.gtk.console.ConsoleWindow( - [err_category, info_category], message_tuples, + [err_category, info_category], + message_tuples, [Gtk.STOCK_DIALOG_ERROR, Gtk.STOCK_DIALOG_INFO], parent=window, - destroy_hook=self._handle_destroy_console) + destroy_hook=self._handle_destroy_console, + ) def _update_console(self): if self.console is not None: diff --git a/metomi/rose/config_editor/updater.py b/metomi/rose/config_editor/updater.py index 514a276f7..d8320b100 100644 --- a/metomi/rose/config_editor/updater.py +++ b/metomi/rose/config_editor/updater.py @@ -22,14 +22,21 @@ class Updater(object): - """This handles the updating of various statuses and displays.""" - def __init__(self, data, util, reporter, mainwindow, main_handle, - nav_controller, get_pagelist_func, - update_bar_widgets_func, - refresh_metadata_func, - is_pluggable=False): + def __init__( + self, + data, + util, + reporter, + mainwindow, + main_handle, + nav_controller, + get_pagelist_func, + update_bar_widgets_func, + refresh_metadata_func, + is_pluggable=False, + ): self.data = data self.util = util self.reporter = reporter @@ -64,21 +71,27 @@ def namespace_data_is_modified(self, namespace): if set(section_hashes) ^ set(old_section_hashes): return metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_CONFIG allowed_sections = self.data.helper.get_sections_from_namespace( - namespace) + namespace + ) save_var_map = {} for section in allowed_sections: for var in config_data.vars.save.get(section, []): - if var.metadata['full_ns'] == namespace: - save_var_map.update({var.metadata['id']: var}) + if var.metadata["full_ns"] == namespace: + save_var_map.update({var.metadata["id"]: var}) for var in config_data.vars.now.get(section, []): - if var.metadata['full_ns'] == namespace: - var_id = var.metadata['id'] + if var.metadata["full_ns"] == namespace: + var_id = var.metadata["id"] save_var = save_var_map.get(var_id) if save_var is None: - return metomi.rose.config_editor.TREE_PANEL_TIP_ADDED_VARS + return ( + metomi.rose.config_editor.TREE_PANEL_TIP_ADDED_VARS + ) if save_var.to_hashable() != var.to_hashable(): # Variable has changed in some form. - return metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_VARS + return ( + metomi.rose.config_editor + .TREE_PANEL_TIP_CHANGED_VARS + ) save_var_map.pop(var_id) if save_var_map: # Some variables are now absent. @@ -89,11 +102,15 @@ def namespace_data_is_modified(self, namespace): sect_data = config_sections.now.get(section) save_sect_data = config_sections.save.get(section) if (sect_data is None) != (save_sect_data is None): - return metomi.rose.config_editor.TREE_PANEL_TIP_DIFF_SECTIONS + return ( + metomi.rose.config_editor.TREE_PANEL_TIP_DIFF_SECTIONS + ) if sect_data is not None and save_sect_data is not None: if sect_data.to_hashable() != save_sect_data.to_hashable(): return ( - metomi.rose.config_editor.TREE_PANEL_TIP_CHANGED_SECTIONS) + metomi.rose.config_editor + .TREE_PANEL_TIP_CHANGED_SECTIONS + ) return "" def update_ns_tree_states(self, namespace): @@ -102,11 +119,13 @@ def update_ns_tree_states(self, namespace): latent_status = self.data.helper.get_ns_latent_status(namespace) ignored_status = self.data.helper.get_ns_ignored_status(namespace) ns_names = namespace.lstrip("/").split("/") - self.nav_panel.update_statuses(ns_names, latent_status, - ignored_status) + self.nav_panel.update_statuses( + ns_names, latent_status, ignored_status + ) - def tree_trigger_update(self, only_this_config=None, - only_this_namespace=None): + def tree_trigger_update( + self, only_this_config=None, only_this_namespace=None + ): """Reload the tree panel, and perform an update. If only_this_config is not None, perform an update only on the @@ -117,8 +136,7 @@ def tree_trigger_update(self, only_this_config=None, """ if self.nav_panel is not None: - self.nav_panel.load_tree(None, - self.nav_controller.namespace_tree) + self.nav_panel.load_tree(None, self.nav_controller.namespace_tree) if only_this_namespace is None: self.update_all(only_this_config=only_this_config) else: @@ -126,12 +144,17 @@ def tree_trigger_update(self, only_this_config=None, spaces = only_this_namespace.lstrip("/").split("/") for i in range(len(spaces), 0, -1): update_ns = "/" + "/".join(spaces[:i]) - self.update_namespace(update_ns, - skip_sub_data_update=True) + self.update_namespace(update_ns, skip_sub_data_update=True) self.update_ns_sub_data(only_this_namespace) - def refresh_ids(self, config_name, setting_ids, is_loading=False, - are_errors_done=False, skip_update=False): + def refresh_ids( + self, + config_name, + setting_ids, + is_loading=False, + are_errors_done=False, + skip_update=False, + ): """Refresh and redraw settings if needed.""" self.pagelist = self.get_pagelist_func() nses_to_do = [] @@ -139,17 +162,17 @@ def refresh_ids(self, config_name, setting_ids, is_loading=False, sect, opt = self.util.get_section_option_from_id(changed_id) if opt is None: name = self.data.helper.get_default_section_namespace( - sect, config_name) + sect, config_name + ) if name in [p.namespace for p in self.pagelist]: index = [p.namespace for p in self.pagelist].index(name) page = self.pagelist[index] page.refresh() else: - var = self.data.helper.get_ns_variable(changed_id, - config_name) + var = self.data.helper.get_ns_variable(changed_id, config_name) if var is None: continue - name = var.metadata['full_ns'] + name = var.metadata["full_ns"] if name in [p.namespace for p in self.pagelist]: index = [p.namespace for p in self.pagelist].index(name) page = self.pagelist[index] @@ -158,15 +181,22 @@ def refresh_ids(self, config_name, setting_ids, is_loading=False, nses_to_do.append(name) if not skip_update: for name in nses_to_do: - self.update_namespace(name, is_loading=is_loading, - skip_sub_data_update=True) + self.update_namespace( + name, is_loading=is_loading, skip_sub_data_update=True + ) self.update_ns_sub_data(nses_to_do) - def update_all(self, only_this_config=None, is_loading=False, - skip_checking=False, skip_sub_data_update=False): + def update_all( + self, + only_this_config=None, + is_loading=False, + skip_checking=False, + skip_sub_data_update=False, + ): """Loop over all namespaces and update.""" unique_namespaces = self.data.helper.get_all_namespaces( - only_this_config) + only_this_config + ) for name in unique_namespaces: self.data.helper.clear_namespace_cached_statuses(name) @@ -203,17 +233,24 @@ def update_all(self, only_this_config=None, is_loading=False, if not skip_sub_data_update: self.update_ns_sub_data() - def update_namespace(self, namespace, are_errors_done=False, - is_loading=False, - skip_sub_data_update=False): + def update_namespace( + self, + namespace, + are_errors_done=False, + is_loading=False, + skip_sub_data_update=False, + ): """Update driver function. Updates the page if open.""" self.pagelist = self.get_pagelist_func() self.data.helper.clear_namespace_cached_statuses(namespace) if namespace in [p.namespace for p in self.pagelist]: index = [p.namespace for p in self.pagelist].index(namespace) page = self.pagelist[index] - self.update_status(page, are_errors_done=are_errors_done, - skip_sub_data_update=skip_sub_data_update) + self.update_status( + page, + are_errors_done=are_errors_done, + skip_sub_data_update=skip_sub_data_update, + ) else: self.update_sections(namespace) self.update_ignored_statuses(namespace) @@ -229,8 +266,9 @@ def update_namespace(self, namespace, are_errors_done=False, if not skip_sub_data_update: self.update_ns_sub_data(namespace) - def update_status(self, page, are_errors_done=False, - skip_sub_data_update=False): + def update_status( + self, page, are_errors_done=False, skip_sub_data_update=False + ): """Update ignored statuses and update the tree statuses.""" self.pagelist = self.get_pagelist_func() self.sync_page_var_lists(page) @@ -254,14 +292,14 @@ def update_ns_sub_data(self, namespaces=None): namespaces = [namespaces] for page in self.pagelist: for namespace in namespaces: - if (namespace is None or - namespace.startswith(page.namespace)): + if namespace is None or namespace.startswith(page.namespace): break else: # No namespaces matched this page, skip continue page.sub_data = self.data.helper.get_sub_data_for_namespace( - page.namespace) + page.namespace + ) page.update_sub_data() def update_ns_info(self, namespace): @@ -276,10 +314,12 @@ def sync_page_var_lists(self, page): real, miss = self.data.helper.get_data_for_namespace(page.namespace) page_real, page_miss = page.panel_data, page.ghost_data refresh_vars = [] - action_vsets = [(page_real.remove, set(page_real) - set(real)), - (page_real.append, set(real) - set(page_real)), - (page_miss.remove, set(page_miss) - set(miss)), - (page_miss.append, set(miss) - set(page_miss))] + action_vsets = [ + (page_real.remove, set(page_real) - set(real)), + (page_real.append, set(real) - set(page_real)), + (page_miss.remove, set(page_miss) - set(miss)), + (page_miss.append, set(miss) - set(page_miss)), + ] for action, v_set in action_vsets: for var in v_set: @@ -288,7 +328,7 @@ def sync_page_var_lists(self, page): for var in v_set: action(var) for var in refresh_vars: - page.refresh(var.metadata['id']) + page.refresh(var.metadata["id"]) def update_config(self, namespace): """Update the config object for the macros.""" @@ -311,7 +351,7 @@ def update_sections(self, namespace): if section in config_data.vars.now: config_data.vars.now.pop(section) for variable in variables: - var_id = variable.metadata['id'] + var_id = variable.metadata["id"] option = self.util.get_section_option_from_id(var_id)[1] sect_data.options.append(option) @@ -328,7 +368,7 @@ def update_ignored_statuses(self, namespace): this_ns_triggers = [] ns_vars, ns_l_vars = self.data.helper.get_data_for_namespace(namespace) for var in ns_vars + ns_l_vars: - var_id = var.metadata['id'] + var_id = var.metadata["id"] if not trigger.check_is_id_trigger(var_id, config_data.meta): continue if var in ns_l_vars: @@ -338,8 +378,7 @@ def update_ignored_statuses(self, namespace): old_val = trig_id_val_dict.get(var_id) if old_val != new_val: # new_val or old_val can be None this_ns_triggers.append(var_id) - updated_ids += self.update_ignoreds(config_name, - var_id) + updated_ids += self.update_ignoreds(config_name, var_id) if not this_ns_triggers: # No reason to update anything. @@ -347,7 +386,7 @@ def update_ignored_statuses(self, namespace): var_id_map = {} for var in config_data.vars.get_all(skip_latent=True): - var_id = var.metadata['id'] + var_id = var.metadata["id"] var_id_map.update({var_id: var}) update_nses = [] @@ -357,18 +396,19 @@ def update_ignored_statuses(self, namespace): if opt is None: sect_vars = config_data.vars.now.get(sect, []) name = self.data.helper.get_default_section_namespace( - sect, config_name) + sect, config_name + ) if name not in update_section_nses: update_section_nses.append(name) else: sect_vars = list(config_data.vars.now.get(sect, [])) sect_vars += list(config_data.vars.latent.get(sect, [])) for var in list(sect_vars): - if var.metadata['id'] != setting_id: + if var.metadata["id"] != setting_id: sect_vars.remove(var) for var in sect_vars: - var_ns = var.metadata['full_ns'] - var_id = var.metadata['id'] + var_ns = var.metadata["full_ns"] + var_id = var.metadata["id"] vsect = self.util.get_section_option_from_id(var_id)[0] if var_ns not in update_nses: update_nses.append(var_ns) @@ -398,10 +438,13 @@ def update_ignoreds(self, config_name, var_id): meta_config = config_data.meta config_sections = config_data.sections - config_data_for_trigger = {"sections": config_sections.now, - "variables": config_data.vars.now} - update_ids = trigger.update(var_id, config_data_for_trigger, - meta_config) + config_data_for_trigger = { + "sections": config_sections.now, + "variables": config_data.vars.now, + } + update_ids = trigger.update( + var_id, config_data_for_trigger, meta_config + ) update_vars = [] update_sections = [] for setting_id in update_ids: @@ -410,32 +453,37 @@ def update_ignoreds(self, config_name, var_id): update_sections.append(section) else: for var in config_data.vars.now.get(section, []): - if var.metadata['id'] == setting_id: + if var.metadata["id"] == setting_id: update_vars.append(var) break else: for var in config_data.vars.latent.get(section, []): - if var.metadata['id'] == setting_id: + if var.metadata["id"] == setting_id: update_vars.append(var) break triggered_ns_list = [] this_id = var_id - for namespace, metadata in list(self.data.namespace_meta_lookup.items()): + for namespace, metadata in list( + self.data.namespace_meta_lookup.items() + ): this_name = self.util.split_full_ns(self.data, namespace) if this_name != config_name: continue for section in update_sections: - if section in metadata['sections']: + if section in metadata["sections"]: triggered_ns_list.append(namespace) # Update the sections. - enabled_sections = [s for s in update_sections - if s in trigger.enabled_dict and - s not in trigger.ignored_dict] + enabled_sections = [ + s + for s in update_sections + if s in trigger.enabled_dict and s not in trigger.ignored_dict + ] for section in update_sections: # Clear pre-existing errors. - sect_vars = (config_data.vars.now.get(section, []) + - config_data.vars.latent.get(section, [])) + sect_vars = config_data.vars.now.get( + section, [] + ) + config_data.vars.latent.get(section, []) sect_data = config_sections.now.get(section) if sect_data is None: sect_data = config_sections.latent[section] @@ -445,42 +493,57 @@ def update_ignoreds(self, config_name, var_id): reason = sect_data.ignored_reason if section in enabled_sections: # Trigger-enabled sections - if (metomi.rose.variable.IGNORED_BY_USER in reason): + if metomi.rose.variable.IGNORED_BY_USER in reason: # User-ignored but trigger-enabled - if (meta_config.get( - [section, metomi.rose.META_PROP_COMPULSORY]).value == - metomi.rose.META_PROP_VALUE_TRUE): + if ( + meta_config.get( + [section, metomi.rose.META_PROP_COMPULSORY] + ).value + == metomi.rose.META_PROP_VALUE_TRUE + ): # Doc table: I_u -> E -> compulsory sect_data.error.update( - {metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED: - metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE}) - elif (metomi.rose.variable.IGNORED_BY_SYSTEM in reason): + { + metomi.rose.config_editor + .WARNING_TYPE_USER_IGNORED: ( + metomi.rose.config_editor + .WARNING_NOT_USER_IGNORABLE + ) + } + ) + elif metomi.rose.variable.IGNORED_BY_SYSTEM in reason: # Normal trigger-enabled sections reason.pop(metomi.rose.variable.IGNORED_BY_SYSTEM) for var in sect_vars: - name = var.metadata['full_ns'] + name = var.metadata["full_ns"] if name not in triggered_ns_list: triggered_ns_list.append(name) var.ignored_reason.pop( - metomi.rose.variable.IGNORED_BY_SECTION, None) + metomi.rose.variable.IGNORED_BY_SECTION, None + ) elif section in trigger.ignored_dict: # Trigger-ignored sections parents = trigger.ignored_dict.get(section, {}) if parents: help_text = "; ".join(list(parents.values())) else: - help_text = metomi.rose.config_editor.IGNORED_STATUS_DEFAULT - reason.update({metomi.rose.variable.IGNORED_BY_SYSTEM: help_text}) + help_text = ( + metomi.rose.config_editor.IGNORED_STATUS_DEFAULT + ) + reason.update( + {metomi.rose.variable.IGNORED_BY_SYSTEM: help_text} + ) for var in sect_vars: - name = var.metadata['full_ns'] + name = var.metadata["full_ns"] if name not in triggered_ns_list: triggered_ns_list.append(name) var.ignored_reason.update( - {metomi.rose.variable.IGNORED_BY_SECTION: help_text}) + {metomi.rose.variable.IGNORED_BY_SECTION: help_text} + ) # Update the variables. for var in update_vars: - var_id = var.metadata.get('id') - name = var.metadata.get('full_ns') + var_id = var.metadata.get("id") + name = var.metadata.get("full_ns") if name not in triggered_ns_list: triggered_ns_list.append(name) if var_id == this_id: @@ -488,31 +551,48 @@ def update_ignoreds(self, config_name, var_id): for attribute in metomi.rose.config_editor.WARNING_TYPES_IGNORE: if attribute in var.error: var.error.pop(attribute) - if (var_id in trigger.enabled_dict and - var_id not in trigger.ignored_dict): + if ( + var_id in trigger.enabled_dict + and var_id not in trigger.ignored_dict + ): # Trigger-enabled variables if metomi.rose.variable.IGNORED_BY_USER in var.ignored_reason: # User-ignored but trigger-enabled # Doc table: I_u -> E - if (var.metadata.get(metomi.rose.META_PROP_COMPULSORY) == - metomi.rose.META_PROP_VALUE_TRUE): + if ( + var.metadata.get(metomi.rose.META_PROP_COMPULSORY) + == metomi.rose.META_PROP_VALUE_TRUE + ): # Doc table: I_u -> E -> compulsory var.error.update( - {metomi.rose.config_editor.WARNING_TYPE_USER_IGNORED: - metomi.rose.config_editor.WARNING_NOT_USER_IGNORABLE}) - elif (metomi.rose.variable.IGNORED_BY_SYSTEM in - var.ignored_reason): + { + metomi.rose.config_editor + .WARNING_TYPE_USER_IGNORED: ( + metomi.rose.config_editor + .WARNING_NOT_USER_IGNORABLE + ) + } + ) + elif ( + metomi.rose.variable.IGNORED_BY_SYSTEM + in var.ignored_reason + ): # Normal trigger-enabled variables - var.ignored_reason.pop(metomi.rose.variable.IGNORED_BY_SYSTEM) + var.ignored_reason.pop( + metomi.rose.variable.IGNORED_BY_SYSTEM + ) elif var_id in trigger.ignored_dict: # Trigger-ignored variables parents = trigger.ignored_dict.get(var_id, {}) if parents: help_text = "; ".join(list(parents.values())) else: - help_text = metomi.rose.config_editor.IGNORED_STATUS_DEFAULT + help_text = ( + metomi.rose.config_editor.IGNORED_STATUS_DEFAULT + ) var.ignored_reason.update( - {metomi.rose.variable.IGNORED_BY_SYSTEM: help_text}) + {metomi.rose.variable.IGNORED_BY_SYSTEM: help_text} + ) for namespace in triggered_ns_list: self.update_tree_status(namespace) return update_ids @@ -526,7 +606,8 @@ def update_tree_status(self, page_or_ns, icon_bool=None, icon_type=None): config_name = self.util.split_full_ns(self.data, namespace)[0] errors = [] ns_vars, ns_l_vars = self.data.helper.get_data_for_namespace( - namespace) + namespace + ) for var in ns_vars + ns_l_vars: errors += list(var.error.items()) else: @@ -540,29 +621,36 @@ def update_tree_status(self, page_or_ns, icon_bool=None, icon_type=None): if section in config_data.sections.now: errors += list(config_data.sections.now[section].error.items()) elif section in config_data.sections.latent: - errors += list(config_data.sections.latent[section].error.items()) + errors += list( + config_data.sections.latent[section].error.items() + ) # Set icons. - name_tree = namespace.lstrip('/').split('/') + name_tree = namespace.lstrip("/").split("/") if icon_bool is None: - if icon_type == 'changed' or icon_type is None: + if icon_type == "changed" or icon_type is None: change = self.namespace_data_is_modified(namespace) self.nav_panel.update_change(name_tree, change) - self.nav_panel.set_row_icon(name_tree, bool(change), - ind_type='changed') - if icon_type == 'error' or icon_type is None: - self.nav_panel.set_row_icon(name_tree, len(errors), - ind_type='error') + self.nav_panel.set_row_icon( + name_tree, bool(change), ind_type="changed" + ) + if icon_type == "error" or icon_type is None: + self.nav_panel.set_row_icon( + name_tree, len(errors), ind_type="error" + ) else: - self.nav_panel.set_row_icon(name_tree, icon_bool, - ind_type=icon_type) + self.nav_panel.set_row_icon( + name_tree, icon_bool, ind_type=icon_type + ) def update_stack_viewer_if_open(self): """Update the information in the stack viewer, if open.""" if self.is_pluggable: return False - if isinstance(self.mainwindow.log_window, - metomi.rose.config_editor.stack.StackViewer): + if isinstance( + self.mainwindow.log_window, + metomi.rose.config_editor.stack.StackViewer, + ): self.mainwindow.log_window.update() def focus_sub_page_if_open(self, namespace, node_id): @@ -596,65 +684,88 @@ def perform_startup_check(self): if problem_list: self.main_handle.handle_macro_validation( config_name, - 'duplicate.DuplicateChecker.validate', - macro_config, problem_list, no_display=True) + "duplicate.DuplicateChecker.validate", + macro_config, + problem_list, + no_display=True, + ) format_checker = metomi.rose.macros.format.FormatChecker() problem_list = format_checker.validate(macro_config, meta_config) if problem_list: self.main_handle.handle_macro_validation( - config_name, 'format.FormatChecker.validate', - macro_config, problem_list) + config_name, + "format.FormatChecker.validate", + macro_config, + problem_list, + ) def perform_error_check(self, namespace=None, is_loading=False): """Loop through system macros and sum errors.""" configs = list(self.data.config.keys()) if namespace is not None: - config_name = self.util.split_full_ns(self.data, - namespace)[0] + config_name = self.util.split_full_ns(self.data, namespace)[0] configs = [config_name] # Compulsory checking. for config_name in configs: config_data = self.data.config[config_name] meta = config_data.meta - checker = ( - self.data.builtin_macros[config_name][ - metomi.rose.META_PROP_COMPULSORY]) + checker = self.data.builtin_macros[config_name][ + metomi.rose.META_PROP_COMPULSORY + ] only_these_sections = None if namespace is not None: only_these_sections = ( - self.data.helper.get_sections_from_namespace(namespace)) + self.data.helper.get_sections_from_namespace(namespace) + ) config_data_for_compulsory = { "sections": config_data.sections.now, - "variables": config_data.vars.now + "variables": config_data.vars.now, } bad_list = checker.validate_settings( - config_data_for_compulsory, config_data.meta, - only_these_sections=only_these_sections + config_data_for_compulsory, + config_data.meta, + only_these_sections=only_these_sections, + ) + self.apply_macro_validation( + config_name, + metomi.rose.META_PROP_COMPULSORY, + bad_list, + namespace, + is_loading=is_loading, + is_macro_dynamic=True, ) - self.apply_macro_validation(config_name, - metomi.rose.META_PROP_COMPULSORY, bad_list, - namespace, is_loading=is_loading, - is_macro_dynamic=True) # Value checking. for config_name in configs: config_data = self.data.config[config_name] meta = config_data.meta - checker = ( - self.data.builtin_macros[config_name][metomi.rose.META_PROP_TYPE]) + checker = self.data.builtin_macros[config_name][ + metomi.rose.META_PROP_TYPE + ] if namespace is None: real_variables = config_data.vars.get_all(skip_latent=True) else: - real_variables = ( - self.data.helper.get_data_for_namespace(namespace)[0]) + real_variables = self.data.helper.get_data_for_namespace( + namespace + )[0] bad_list = checker.validate_variables(real_variables, meta) - self.apply_macro_validation(config_name, metomi.rose.META_PROP_TYPE, - bad_list, - namespace, is_loading=is_loading, - is_macro_dynamic=True) - - def apply_macro_validation(self, config_name, macro_type, bad_list=None, - namespace=None, is_loading=False, - is_macro_dynamic=False): + self.apply_macro_validation( + config_name, + metomi.rose.META_PROP_TYPE, + bad_list, + namespace, + is_loading=is_loading, + is_macro_dynamic=True, + ) + + def apply_macro_validation( + self, + config_name, + macro_type, + bad_list=None, + namespace=None, + is_loading=False, + is_macro_dynamic=False, + ): """Display error icons if a variable is in the wrong state.""" if bad_list is None: bad_list = [] @@ -664,14 +775,17 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, id_error_dict = {} id_warn_dict = {} if namespace is None: - ok_sections = (list(config_sections.now.keys()) + - list(config_sections.latent.keys())) + ok_sections = list(config_sections.now.keys()) + list( + config_sections.latent.keys() + ) ok_variables = variables else: ok_sections = self.data.helper.get_sections_from_namespace( - namespace) - ok_variables = [v for v in variables - if v.metadata.get('full_ns') == namespace] + namespace + ) + ok_variables = [ + v for v in variables if v.metadata.get("full_ns") == namespace + ] for section in ok_sections: sect_data = config_sections.now.get(section) if sect_data is None: @@ -687,14 +801,17 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, for var in ok_variables: if macro_type in var.error: this_error = var.error.pop(macro_type) - id_error_dict.update({var.metadata['id']: this_error}) + id_error_dict.update({var.metadata["id"]: this_error}) if macro_type in var.warning: this_warning = var.warning.pop(macro_type) - id_warn_dict.update({var.metadata['id']: this_warning}) + id_warn_dict.update({var.metadata["id"]: this_warning}) if not bad_list: - self.refresh_ids(config_name, - list(id_error_dict.keys()) + list(id_warn_dict.keys()), - is_loading, are_errors_done=is_macro_dynamic) + self.refresh_ids( + config_name, + list(id_error_dict.keys()) + list(id_warn_dict.keys()), + is_loading, + are_errors_done=is_macro_dynamic, + ) return for bad_report in bad_list: section = bad_report.section @@ -702,9 +819,13 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, info = bad_report.info if key is None: setting_id = section - if (namespace is not None and section not in - self.data.helper.get_sections_from_namespace( - namespace)): + if ( + namespace is not None + and section + not in self.data.helper.get_sections_from_namespace( + namespace + ) + ): continue sect_data = config_sections.now.get(section) if sect_data is None: @@ -717,16 +838,19 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, sect_data.error.setdefault(macro_type, info) else: setting_id = self.util.get_id_from_section_option(section, key) - var = self.data.helper.get_variable_by_id(setting_id, - config_name) + var = self.data.helper.get_variable_by_id( + setting_id, config_name + ) if var is None: - var = self.data.helper.get_variable_by_id(setting_id, - config_name, - latent=True) + var = self.data.helper.get_variable_by_id( + setting_id, config_name, latent=True + ) if var is None: continue - if (namespace is not None and - var.metadata['full_ns'] != namespace): + if ( + namespace is not None + and var.metadata["full_ns"] != namespace + ): continue if bad_report.is_warning: var.warning.setdefault(macro_type, info) @@ -738,23 +862,30 @@ def apply_macro_validation(self, config_name, macro_type, bad_list=None, map_ = id_error_dict if is_loading: self.load_errors += 1 - update_text = metomi.rose.config_editor.EVENT_LOAD_ERRORS.format( - self.data.top_level_name, self.load_errors) - - self.reporter.report_load_event(update_text, - no_progress=True) + update_text = ( + metomi.rose.config_editor.EVENT_LOAD_ERRORS.format( + self.data.top_level_name, self.load_errors + ) + ) + + self.reporter.report_load_event( + update_text, no_progress=True + ) if setting_id in map_: # No need for further update, already had warning/error. map_.pop(setting_id) else: # New warning or error. map_.update({setting_id: info}) - self.refresh_ids(config_name, - list(id_error_dict.keys()) + list(id_warn_dict.keys()), - is_loading, - are_errors_done=is_macro_dynamic) - - def apply_macro_transform(self, config_name, changed_ids, - skip_update=False): + self.refresh_ids( + config_name, + list(id_error_dict.keys()) + list(id_warn_dict.keys()), + is_loading, + are_errors_done=is_macro_dynamic, + ) + + def apply_macro_transform( + self, config_name, changed_ids, skip_update=False + ): """Refresh pages with changes.""" self.refresh_ids(config_name, changed_ids, skip_update=skip_update) diff --git a/metomi/rose/config_editor/upgrade_controller.py b/metomi/rose/config_editor/upgrade_controller.py index d90f234e2..5137314fe 100644 --- a/metomi/rose/config_editor/upgrade_controller.py +++ b/metomi/rose/config_editor/upgrade_controller.py @@ -22,7 +22,8 @@ import os import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk from gi.repository import GObject @@ -30,15 +31,25 @@ import metomi.rose.macro import metomi.rose.upgrade +from metomi.rose.macros.trigger import TriggerMacro -class UpgradeController(object): +class UpgradeController(object): """Configure the upgrade of configurations.""" - def __init__(self, app_config_dict, handle_transform_func, - parent_window=None, upgrade_inspector=None): - buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT) + def __init__( + self, + app_config_dict, + handle_transform_func, + parent_window=None, + upgrade_inspector=None, + ): + buttons = ( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_APPLY, + Gtk.ResponseType.ACCEPT, + ) self.window = Gtk.Dialog(buttons=buttons) self.set_transient_for(parent_window) self.window.set_title(metomi.rose.config_editor.DIALOG_TITLE_UPGRADE) @@ -50,14 +61,20 @@ def __init__(self, app_config_dict, handle_transform_func, self.use_all_versions = False self.treemodel = Gtk.TreeStore(str, str, str, bool) self.treeview = metomi.rose.gtk.util.TooltipTreeView( - get_tooltip_func=self._get_tooltip) + get_tooltip_func=self._get_tooltip + ) self.treeview.show() old_pwd = os.getcwd() for config_name in config_names: app_config = app_config_dict[config_name]["config"] app_directory = app_config_dict[config_name]["directory"] - meta_value = app_config.get_value([metomi.rose.CONFIG_SECT_TOP, - metomi.rose.CONFIG_OPT_META_TYPE], "") + meta_value = app_config.get_value( + [ + metomi.rose.CONFIG_SECT_TOP, + metomi.rose.CONFIG_OPT_META_TYPE, + ], + "", + ) if len(meta_value.split("/")) < 2: continue try: @@ -75,8 +92,7 @@ def __init__(self, app_config_dict, handle_transform_func, self.treeview.set_rules_hint(True) self.treewindow = Gtk.ScrolledWindow() self.treewindow.show() - self.treewindow.set_policy(Gtk.PolicyType.NEVER, - Gtk.PolicyType.NEVER) + self.treewindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) columns = metomi.rose.config_editor.DIALOG_COLUMNS_UPGRADE for i, title in enumerate(columns): column = Gtk.TreeViewColumn() @@ -89,8 +105,9 @@ def __init__(self, app_config_dict, handle_transform_func, self._combo_cell = Gtk.CellRendererCombo() self._combo_cell.set_property("has-entry", False) self._combo_cell.set_property("editable", True) - self._combo_cell.connect("changed", - self._handle_change_version, 2) + self._combo_cell.connect( + "changed", self._handle_change_version, 2 + ) cell = self._combo_cell else: cell = Gtk.CellRendererText() @@ -103,25 +120,36 @@ def __init__(self, app_config_dict, handle_transform_func, self.treeview.connect("cursor-changed", self._handle_change_cursor) self.treewindow.add(self.treeview) self.window.vbox.pack_start( - self.treewindow, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) + self.treewindow, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_LABEL_UPGRADE) label.show() self.window.vbox.pack_start( - label, True, True, metomi.rose.config_editor.SPACING_PAGE) + label, True, True, metomi.rose.config_editor.SPACING_PAGE + ) button_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) button_hbox.show() all_versions_toggle_button = Gtk.CheckButton( label=metomi.rose.config_editor.DIALOG_LABEL_UPGRADE_ALL, - use_underline=False) + use_underline=False, + ) all_versions_toggle_button.set_active(self.use_all_versions) - all_versions_toggle_button.connect("toggled", - self._handle_toggle_all_versions) + all_versions_toggle_button.connect( + "toggled", self._handle_toggle_all_versions + ) all_versions_toggle_button.show() - button_hbox.pack_start(all_versions_toggle_button, expand=False, - fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - self.window.vbox.pack_end(button_hbox, expand=False, fill=False, padding=0) + button_hbox.pack_start( + all_versions_toggle_button, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) + self.window.vbox.pack_end( + button_hbox, expand=False, fill=False, padding=0 + ) self.ok_button = self.window.action_area.get_children()[0] self.window.set_focus(all_versions_toggle_button) self.window.set_focus(self.ok_button) @@ -132,7 +160,9 @@ def __init__(self, app_config_dict, handle_transform_func, extra = 2 * metomi.rose.config_editor.SPACING_PAGE for i in [0, 1]: new_size[i] = min([my_size[i] + extra, max_size[i]]) - self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.treewindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) self.window.set_default_size(*new_size) response = self.window.run() old_pwd = os.getcwd() @@ -154,41 +184,56 @@ def __init__(self, app_config_dict, handle_transform_func, macro_config = copy.deepcopy(config) try: new_config, change_list = manager.transform( - macro_config, custom_inspector=upgrade_inspector) + macro_config, custom_inspector=upgrade_inspector + ) except Exception as exc: metomi.rose.gtk.dialog.run_dialog( metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, type(exc).__name__ + ": " + str(exc), metomi.rose.config_editor.ERROR_UPGRADE.format( - config_name.lstrip("/")) + config_name.lstrip("/") + ), ) iter_ = self.treemodel.iter_next(iter_) continue - macro_id = (type(manager).__name__ + "." + - metomi.rose.macro.TRANSFORM_METHOD) - if handle_transform_func(config_name, macro_id, - new_config, change_list, - triggers_ok=True): + macro_id = ( + type(manager).__name__ + + "." + + metomi.rose.macro.TRANSFORM_METHOD + ) + if handle_transform_func( + config_name, + macro_id, + new_config, + change_list, + triggers_ok=True, + ): meta_config = metomi.rose.macro.load_meta_config( - new_config, config_type=metomi.rose.SUB_CONFIG_NAME, - ignore_meta_error=True + new_config, + config_type=metomi.rose.SUB_CONFIG_NAME, + ignore_meta_error=True, ) - trig_macro = metomi.rose.macros.trigger.TriggerMacro() + trig_macro = TriggerMacro() macro_config = copy.deepcopy(new_config) macro_id = ( - metomi.rose.upgrade.MACRO_UPGRADE_TRIGGER_NAME + "." + - metomi.rose.macro.TRANSFORM_METHOD + metomi.rose.upgrade.MACRO_UPGRADE_TRIGGER_NAME + + "." + + metomi.rose.macro.TRANSFORM_METHOD ) - if not trig_macro.validate_dependencies(macro_config, - meta_config): - new_trig_config, trig_change_list = ( - metomi.rose.macros.trigger.TriggerMacro().transform( - macro_config, meta_config) + if not trig_macro.validate_dependencies( + macro_config, meta_config + ): + ( + new_trig_config, + trig_change_list, + ) = TriggerMacro().transform(macro_config, meta_config) + handle_transform_func( + config_name, + macro_id, + new_trig_config, + trig_change_list, + triggers_ok=True, ) - handle_transform_func(config_name, macro_id, - new_trig_config, - trig_change_list, - triggers_ok=True) iter_ = self.treemodel.iter_next(iter_) os.chdir(old_pwd) self.window.destroy() @@ -227,8 +272,9 @@ def _handle_toggle_all_versions(self, button): def _handle_toggle_upgrade(self, cell, path, col_index): iter_ = self.treemodel.get_iter(path) value = self.treemodel.get_value(iter_, col_index) - if (self.treemodel.get_value(iter_, 1) == - self.treemodel.get_value(iter_, 2)): + if self.treemodel.get_value(iter_, 1) == self.treemodel.get_value( + iter_, 2 + ): self.treemodel.set_value(iter_, col_index, False) else: self.treemodel.set_value(iter_, col_index, not value) @@ -268,10 +314,12 @@ def _update_treemodel_data(self, config_name): next_tag = manager.get_new_tag(only_named=not self.use_all_versions) if next_tag is None: self.treemodel.append( - None, [config_name, current_tag, current_tag, False]) + None, [config_name, current_tag, current_tag, False] + ) else: self.treemodel.append( - None, [config_name, current_tag, next_tag, True]) + None, [config_name, current_tag, next_tag, True] + ) listmodel = Gtk.ListStore(str) tags = manager.get_tags(only_named=not self.use_all_versions) if not tags: diff --git a/metomi/rose/config_editor/util.py b/metomi/rose/config_editor/util.py index 8663da220..292367601 100644 --- a/metomi/rose/config_editor/util.py +++ b/metomi/rose/config_editor/util.py @@ -26,6 +26,7 @@ """ import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -35,7 +36,6 @@ class Lookup(object): - """Collection of data lookup functions used by multiple modules.""" def __init__(self): @@ -62,7 +62,7 @@ def get_section_option_from_id(self, var_id): return self.section_option_id_lookup[var_id] split_char = metomi.rose.CONFIG_DELIMITER option_name = var_id.split(split_char)[-1] - section = var_id.replace(split_char + option_name, '', 1) + section = var_id.replace(split_char + option_name, "", 1) if option_name == section: option_name = None self.section_option_id_lookup[var_id] = (section, option_name) @@ -72,30 +72,36 @@ def split_full_ns(self, data, full_namespace): """Return the config name and the internal namespace from full ns.""" if full_namespace not in self.full_ns_split_lookup: for config_name in list(data.config.keys()): - if config_name == '/' + data.top_level_name: + if config_name == "/" + data.top_level_name: continue - if full_namespace.startswith(config_name + '/'): - sub_space = full_namespace.replace(config_name + '/', - '', 1) - self.full_ns_split_lookup[full_namespace] = (config_name, - sub_space) + if full_namespace.startswith(config_name + "/"): + sub_space = full_namespace.replace( + config_name + "/", "", 1 + ) + self.full_ns_split_lookup[full_namespace] = ( + config_name, + sub_space, + ) break elif full_namespace == config_name: - sub_space = '' - self.full_ns_split_lookup[full_namespace] = (config_name, - sub_space) + sub_space = "" + self.full_ns_split_lookup[full_namespace] = ( + config_name, + sub_space, + ) break else: # A top level based namespace config_name = "/" + data.top_level_name - sub_space = full_namespace.replace(config_name + '/', '', 1) - self.full_ns_split_lookup[full_namespace] = (config_name, - sub_space) + sub_space = full_namespace.replace(config_name + "/", "", 1) + self.full_ns_split_lookup[full_namespace] = ( + config_name, + sub_space, + ) return self.full_ns_split_lookup.get(full_namespace, (None, None)) class ImportWidgetError(Exception): - """An exception raised when an imported widget cannot be used.""" def __str__(self): @@ -104,11 +110,13 @@ def __str__(self): def launch_node_info_dialog(node, changes, search_function): """Launch a dialog displaying attributes of a variable or section.""" - title = node.__class__.__name__ + " " + node.metadata['id'] - text = '' + title = node.__class__.__name__ + " " + node.metadata["id"] + text = "" if changes: - text += (metomi.rose.config_editor.DIALOG_NODE_INFO_CHANGES.format(changes) + - "\n") + text += ( + metomi.rose.config_editor.DIALOG_NODE_INFO_CHANGES.format(changes) + + "\n" + ) text += metomi.rose.config_editor.DIALOG_NODE_INFO_DATA try: att_list = list(vars(node).items()) @@ -116,7 +124,7 @@ def launch_node_info_dialog(node, changes, search_function): # vars will fail when __slots__ are used. att_list = node.getattrs() att_list.sort() - att_list.sort(key=lambda x: (x[0] in ['name', 'value'])) + att_list.sort(key=lambda x: (x[0] in ["name", "value"])) metadata_start_index = len(att_list) for key, value in sorted(node.metadata.items()): att_list.append([key, value]) @@ -124,8 +132,12 @@ def launch_node_info_dialog(node, changes, search_function): name = metomi.rose.config_editor.DIALOG_NODE_INFO_ATTRIBUTE maxlen = metomi.rose.config_editor.DIALOG_NODE_INFO_MAX_LEN for i, (att_name, att_val) in enumerate(att_list): - if (att_name == 'metadata' or att_name.startswith("_") or - callable(att_val) or att_name == 'old_value'): + if ( + att_name == "metadata" + or att_name.startswith("_") + or callable(att_val) + or att_name == "old_value" + ): continue if i == metadata_start_index: text += "\n" + metomi.rose.config_editor.DIALOG_NODE_INFO_METADATA @@ -133,11 +145,13 @@ def launch_node_info_dialog(node, changes, search_function): indent0 = len(prefix) text += prefix lenval = maxlen - indent0 - text += _pretty_format_data(att_val, global_indent=indent0, - width=lenval) + text += _pretty_format_data( + att_val, global_indent=indent0, width=lenval + ) text += "\n" - metomi.rose.gtk.dialog.run_hyperlink_dialog(Gtk.STOCK_DIALOG_INFO, text, title, - search_function) + metomi.rose.gtk.dialog.run_hyperlink_dialog( + Gtk.STOCK_DIALOG_INFO, text, title, search_function + ) def launch_error_dialog(exception=None, text=""): @@ -146,9 +160,12 @@ def launch_error_dialog(exception=None, text=""): text += "\n" if exception is not None: text += type(exception).__name__ + ": " + str(exception) - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, metomi.rose.config_editor.DIALOG_TITLE_ERROR, - modal=False) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, + text, + metomi.rose.config_editor.DIALOG_TITLE_ERROR, + modal=False, + ) def text_for_character_widget(text): @@ -187,7 +204,7 @@ def wrap_string(text, maxlen=72, indent0=0, maxlines=4, sep=","): lines.append("") linelen = maxlen lines[-1] += dtext - lines[-1] = lines[-1][:-len(sep)] + lines[-1] = lines[-1][: -len(sep)] if len(lines) > maxlines: lines = lines[:4] + ["..."] return "\n".join(lines) @@ -197,8 +214,8 @@ def null_cmp(x_item, y_item): """Compares sort_key and then id of the tuples x_item/y_item.""" x_sort_key, x_id = x_item[0:2] y_sort_key, y_id = y_item[0:2] - if x_id == '' or y_id == '': - return (x_id == '') - (y_id == '') + if x_id == "" or y_id == "": + return (x_id == "") - (y_id == "") if x_sort_key == y_sort_key: return metomi.rose.config.sort_settings(x_id, y_id) return (x_sort_key > y_sort_key) - (x_sort_key < y_sort_key) @@ -214,8 +231,7 @@ def _pretty_format_data(data, global_indent=0, indent=4, width=60): text += "\n" + " " * global_indent sub_prefix = sub_name.format(safe_str(key)) + delim indent_next = global_indent + indent - str_val = _pretty_format_data(val, - global_indent=indent_next) + str_val = _pretty_format_data(val, global_indent=indent_next) text += sub_prefix + str_val return text if isinstance(data, list) and data: diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py index cd0afdc65..f276cc66b 100644 --- a/metomi/rose/config_editor/valuewidget/__init__.py +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -19,17 +19,15 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk -import re +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk import metomi.rose from . import array from . import booltoggle from . import character from . import combobox -from . import files from . import intspin from . import meta from . import radiobuttons @@ -37,12 +35,18 @@ from . import valuehints -NON_TEXT_TYPES = ('boolean', 'integer', 'logical', 'python_boolean', - 'python_list', 'real', 'spaced_list') +NON_TEXT_TYPES = ( + "boolean", + "integer", + "logical", + "python_boolean", + "python_list", + "real", + "spaced_list", +) class ValueWidgetHook(object): - """Provides hook functions for valuewidgets.""" def __init__(self, scroll_func=None, focus_func=None): @@ -87,7 +91,7 @@ def chooser(value, metadata, error): # determine widget by presence of environment variables if contains_env and (not m_type or m_type in NON_TEXT_TYPES or is_list): # it is not safe to display the widget as intended due to an env var - if '\n' in value: + if "\n" in value: return text.TextMultilineValueWidget else: return text.RawValueWidget @@ -97,7 +101,7 @@ def chooser(value, metadata, error): if isinstance(m_type, list): # irregular array return array.mixed.MixedArrayValueWidget - elif m_type in ['logical', 'boolean', 'python_boolean']: + elif m_type in ["logical", "boolean", "python_boolean"]: # regular array (boolean) return array.logical.LogicalArrayValueWidget else: @@ -114,11 +118,11 @@ def chooser(value, metadata, error): return combobox.ComboBoxValueWidget # determine widget by metadata type - if m_type == 'integer': + if m_type == "integer": return intspin.IntSpinButtonValueWidget - if m_type == 'meta': + if m_type == "meta": return meta.MetaValueWidget - if m_type == 'str_multi': + if m_type == "str_multi": return text.TextMultilineValueWidget if m_type in ["character", "quoted"]: return character.QuotedTextValueWidget @@ -126,7 +130,7 @@ def chooser(value, metadata, error): return array.python_list.PythonListValueWidget if m_type == "spaced_list" and not error: return array.spaced_list.SpacedListValueWidget - if m_type in ['logical', 'boolean', 'python_boolean']: + if m_type in ["logical", "boolean", "python_boolean"]: return booltoggle.BoolToggleValueWidget # determine widget by metadata hint @@ -134,7 +138,7 @@ def chooser(value, metadata, error): return valuehints.HintsValueWidget # fall back to a text widget - if '\n' in value: + if "\n" in value: return text.TextMultilineValueWidget else: return text.RawValueWidget diff --git a/metomi/rose/config_editor/valuewidget/array/__init__.py b/metomi/rose/config_editor/valuewidget/array/__init__.py index a28b4b25b..31b1db0df 100644 --- a/metomi/rose/config_editor/valuewidget/array/__init__.py +++ b/metomi/rose/config_editor/valuewidget/array/__init__.py @@ -18,7 +18,9 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- +# flake8: noqa: F401 + from . import python_list from . import logical from . import mixed -from . import spaced_list \ No newline at end of file +from . import spaced_list diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 0cdcf8838..78ffd97d7 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor.util @@ -28,7 +29,6 @@ class EntryArrayValueWidget(Gtk.Box): - """This is a class to represent multiple array entries.""" TIP_ADD = "Add array element" @@ -39,8 +39,9 @@ class EntryArrayValueWidget(Gtk.Box): TIP_RIGHT = "Move array element right" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(EntryArrayValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(EntryArrayValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -51,8 +52,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.chars_width = max([len(v) for v in value_array] + [1]) + 1 self.last_selected_src = None arr_type = self.metadata.get(metomi.rose.META_PROP_TYPE) - self.is_char_array = (arr_type == "character") - self.is_quoted_array = (arr_type == "quoted") + self.is_char_array = arr_type == "character" + self.is_quoted_array = arr_type == "quoted" # Do not treat character or quoted arrays specially when incorrect. if self.is_char_array: checker = metomi.rose.macros.value.ValueChecker() @@ -67,17 +68,21 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): if self.is_char_array: for i, val in enumerate(value_array): value_array[i] = ( - metomi.rose.config_editor.util.text_for_character_widget(val)) + metomi.rose.config_editor.util.text_for_character_widget( + val + ) + ) if self.is_quoted_array: for i, val in enumerate(value_array): value_array[i] = ( - metomi.rose.config_editor.util.text_for_quoted_widget(val)) + metomi.rose.config_editor.util.text_for_quoted_widget(val) + ) # Designate the number of allowed columns - 10 for 4 chars width self.num_allowed_columns = 3 - self.entry_table = Gtk.Table(rows=1, - columns=self.num_allowed_columns, - homogeneous=True) - self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=1, columns=self.num_allowed_columns, homogeneous=True + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() self.entries = [] @@ -89,12 +94,17 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_entries(value_array) self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start( + self.add_del_button_box, expand=False, fill=False, padding=0 + ) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) - self.entry_table.connect_after('size-allocate', - lambda w, e: self.reshape_table()) - self.connect('focus-in-event', - lambda w, e: self.hook.get_focus(self.get_focus_entry())) + self.entry_table.connect_after( + "size-allocate", lambda w, e: self.reshape_table() + ) + self.connect( + "focus-in-event", + lambda w, e: self.hook.get_focus(self.get_focus_entry()), + ) def force_scroll(self, widget=None): """Adjusts a scrolled window to display the correct widget.""" @@ -129,14 +139,20 @@ def get_focus_entry(self): def get_focus_index(self): """Get the focus and position within the table of entries.""" - text = '' + text = "" for entry in self.entries: val = entry.get_text() if self.is_char_array: - val = metomi.rose.config_editor.util.text_from_character_widget(val) + val = ( + metomi.rose.config_editor.util.text_from_character_widget( + val + ) + ) elif self.is_quoted_array: - val = metomi.rose.config_editor.util.text_from_quoted_widget(val) - prefix = get_next_delimiter(self.value[len(text):], val) + val = metomi.rose.config_editor.util.text_from_quoted_widget( + val + ) + prefix = get_next_delimiter(self.value[len(text) :], val) if prefix is None: return None if entry == self.entry_table.get_focus_child(): @@ -149,14 +165,15 @@ def set_focus_index(self, focus_index=None): if focus_index is None: return value_array = metomi.rose.variable.array_split(self.value) - text = '' + text = "" for i, val in enumerate(value_array): - prefix = get_next_delimiter(self.value[len(text):], - val) + prefix = get_next_delimiter(self.value[len(text) :], val) if prefix is None: return - if (len(text + prefix + val) >= focus_index or - i == len(value_array) - 1): + if ( + len(text + prefix + val) >= focus_index + or i == len(value_array) - 1 + ): if len(self.entries) > i: self.entries[i].grab_focus() val_offset = focus_index - len(text + prefix) @@ -185,7 +202,7 @@ def generate_buttons(self): left_arrow = Gtk.ToolButton() left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() - left_arrow.connect('clicked', lambda x: self.move_element(-1)) + left_arrow.connect("clicked", lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() @@ -193,49 +210,68 @@ def generate_buttons(self): right_arrow = Gtk.ToolButton() right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() - right_arrow.connect('clicked', lambda x: self.move_element(1)) + right_arrow.connect("clicked", lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() right_event_box.add(right_arrow) right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_start( + left_event_box, expand=False, fill=False, padding=0 + ) + self.arrow_box.pack_end( + right_event_box, expand=False, fill=False, padding=0 + ) self.set_arrow_sensitive(False, False) - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_DEL) self.del_button.add(del_image) self.del_button.show() - self.del_button.connect('button-release-event', - lambda b, e: self.remove_entry()) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect( + "button-release-event", lambda b, e: self.remove_entry() + ) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) + self.button_box.pack_start( + self.arrow_box, expand=False, fill=True, padding=0 + ) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() self.add_button.set_tooltip_text(self.TIP_ADD) self.add_button.add(add_image) self.add_button.show() - self.add_button.connect('button-release-event', - lambda b, e: self.add_entry()) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_button.connect( + "button-release-event", lambda b, e: self.add_entry() + ) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False, padding=0) + self.add_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False, padding=0) + self.del_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.show() def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): @@ -252,8 +288,10 @@ def move_element(self, num_places_right): if entry is None: return old_index = self.entries.index(entry) - if (old_index + num_places_right < 0 or - old_index + num_places_right > len(self.entries) - 1): + if ( + old_index + num_places_right < 0 + or old_index + num_places_right > len(self.entries) - 1 + ): return self.entries.remove(entry) self.entries.insert(old_index + num_places_right, entry) @@ -264,17 +302,14 @@ def get_entry(self, value_item): """Create a gtk Entry for this array element.""" entry = Gtk.Entry() entry.set_text(value_item) - entry.connect('focus-in-event', - self._handle_focus_on_entry) - entry.connect("button-release-event", - self._handle_middle_click_paste) + entry.connect("focus-in-event", self._handle_focus_on_entry) + entry.connect("button-release-event", self._handle_middle_click_paste) entry.connect_after("paste-clipboard", self.setter) - entry.connect_after("key-release-event", - lambda e, v: self.setter(e)) - entry.connect_after("button-release-event", - lambda e, v: self.setter(e)) - entry.connect('focus-out-event', - self._handle_focus_off_entry) + entry.connect_after("key-release-event", lambda e, v: self.setter(e)) + entry.connect_after( + "button-release-event", lambda e, v: self.setter(e) + ) + entry.connect("focus-out-event", self._handle_focus_off_entry) entry.set_width_chars(self.chars_width - 1) entry.show() return entry @@ -293,27 +328,34 @@ def populate_table(self, focus_widget=None): position = focus_widget.get_position() for child in self.entry_table.get_children(): self.entry_table.remove(child) - if (focus_widget is None and self.entry_table.is_focus() and - len(self.entries) > 0): + if ( + focus_widget is None + and self.entry_table.is_focus() + and len(self.entries) > 0 + ): focus_widget = self.entries[-1] position = len(focus_widget.get_text()) num_fields = len(self.entries + [self.button_box]) num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns self.entry_table.resize(num_rows_now, self.num_allowed_columns) - if (self.max_length.isdigit() and - len(self.entries) >= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) >= int( + self.max_length + ): self.add_button.hide() else: self.add_button.show() - if (self.max_length.isdigit() and - len(self.entries) <= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) <= int( + self.max_length + ): self.del_button.hide() elif len(self.entries) == 0: self.del_button.hide() else: self.del_button.show() - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): index = self.entries.index(self.last_selected_src) if index == 0: self.set_arrow_sensitive(False, True) @@ -323,43 +365,53 @@ def populate_table(self, focus_widget=None): self.set_arrow_sensitive(False, False) if self.has_titles: - for col, label in enumerate(self.metadata['element-titles']): + for col, label in enumerate(self.metadata["element-titles"]): if col >= len(table_widgets) - 1: break widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label = Gtk.Label(label=self.metadata['element-titles'][col]) + label = Gtk.Label(label=self.metadata["element-titles"][col]) label.show() widget.pack_start(label, expand=True, fill=True) widget.show() - self.entry_table.attach(widget, - col, col + 1, - 0, 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + col, + col + 1, + 0, + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) for i, widget in enumerate(table_widgets): if isinstance(widget, Gtk.Entry): if self.is_char_array or self.is_quoted_array: w_value = widget.get_text() - widget.set_tooltip_text(self.TIP_ELEMENT_CHAR.format( - (i + 1), w_value)) + widget.set_tooltip_text( + self.TIP_ELEMENT_CHAR.format((i + 1), w_value) + ) else: widget.set_tooltip_text(self.TIP_ELEMENT.format((i + 1))) row = i // self.num_allowed_columns if self.has_titles: row += 1 column = i % self.num_allowed_columns - self.entry_table.attach(widget, - column, column + 1, - row, row + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + column, + column + 1, + row, + row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) if focus_widget is not None: focus_widget.grab_focus() focus_widget.set_position(position) focus_widget.select_region(position, -1) self.grab_focus = lambda: self.hook.get_focus( - self._get_widget_for_focus()) + self._get_widget_for_focus() + ) self.check_resize() def reshape_table(self): @@ -377,22 +429,27 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" - entry = self.get_entry('') - entry.connect('focus-in-event', lambda w, e: self.force_scroll(w)) + entry = self.get_entry("") + entry.connect("focus-in-event", lambda w, e: self.force_scroll(w)) self.entries.append(entry) self._adjust_entry_length() self.last_selected_src = entry self.populate_table(focus_widget=entry) - if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE): + if ( + self.metadata.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + ): self.setter(entry) def remove_entry(self): """Remove the last selected or the last entry.""" - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): entry = self.entries.pop( - self.entries.index(self.last_selected_src)) + self.entries.index(self.last_selected_src) + ) self.last_selected_src = None else: entry = self.entries.pop() @@ -405,15 +462,17 @@ def setter(self, widget): # Prevent str without "" breaking the underlying Python syntax for e in self.entries: v = e.get_text() - if v in ("False", "True"): # Boolean + if v in ("False", "True"): # Boolean val_array.append(v) - elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric val_array.append(v) - elif not v.startswith('"'): # Str - add in leading and trailing " + elif not v.startswith('"'): # Str - add in leading and trailing " val_array.append('"' + v + '"') e.set_text('"' + v + '"') - e.set_position(len(v)+1) - elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + e.set_position(len(v) + 1) + elif (not v.endswith('"')) or ( + len(v) == 1 + ): # Str - add in trailing " val_array.append(v + '"') e.set_text(v + '"') e.set_position(len(v)) @@ -426,32 +485,41 @@ def setter(self, widget): if widget is not None and not widget.is_focus(): widget.grab_focus() widget.set_position(len(widget.get_text())) - widget.select_region(widget.get_position(), - widget.get_position()) + widget.select_region( + widget.get_position(), widget.get_position() + ) if self.is_char_array: for i, val in enumerate(val_array): val_array[i] = ( - metomi.rose.config_editor.util.text_from_character_widget(val)) + metomi.rose.config_editor.util.text_from_character_widget( + val + ) + ) elif self.is_quoted_array: for i, val in enumerate(val_array): val_array[i] = ( - metomi.rose.config_editor.util.text_from_quoted_widget(val)) + metomi.rose.config_editor.util.text_from_quoted_widget(val) + ) entries_have_commas = any("," in v for v in val_array) new_value = metomi.rose.variable.array_join(val_array) if new_value != self.value: self.value = new_value self.set_value(new_value) - if (entries_have_commas and - not (self.is_char_array or self.is_quoted_array)): + if entries_have_commas and not ( + self.is_char_array or self.is_quoted_array + ): new_val_array = metomi.rose.variable.array_split(new_value) if len(new_val_array) != len(self.entries): self.generate_entries() focus_index = None for i, val in enumerate(val_array): if "," in val: - val_post_comma = val[:val.index(",") + 1] - focus_index = len(metomi.rose.variable.array_join( - new_val_array[:i] + [val_post_comma])) + val_post_comma = val[: val.index(",") + 1] + focus_index = len( + metomi.rose.variable.array_join( + new_val_array[:i] + [val_post_comma] + ) + ) self.populate_table() self.set_focus_index(focus_index) return False @@ -483,12 +551,11 @@ def _handle_focus_on_entry(self, widget, event): except AttributeError: self.last_selected_src.drag_unhighlight() self.last_selected_src = widget - is_start = (widget in self.entries and self.entries[0] == widget) - is_end = (widget in self.entries and self.entries[-1] == widget) + is_start = widget in self.entries and self.entries[0] == widget + is_end = widget in self.entries and self.entries[-1] == widget self.set_arrow_sensitive(not is_start, not is_end) - if widget.get_text() != '': - widget.select_region(widget.get_position(), - widget.get_position()) + if widget.get_text() != "": + widget.select_region(widget.get_position(), widget.get_position()) return False def _handle_middle_click_paste(self, widget, event): diff --git a/metomi/rose/config_editor/valuewidget/array/logical.py b/metomi/rose/config_editor/valuewidget/array/logical.py index 9d4ff79a5..a348a79e8 100644 --- a/metomi/rose/config_editor/valuewidget/array/logical.py +++ b/metomi/rose/config_editor/valuewidget/array/logical.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.gtk.util @@ -27,15 +28,15 @@ class LogicalArrayValueWidget(Gtk.Box): - """This is a class to represent an array of logical or boolean types.""" - TIP_ADD = 'Add array element' - TIP_DEL = 'Delete array element' + TIP_ADD = "Add array element" + TIP_DEL = "Delete array element" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(LogicalArrayValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(LogicalArrayValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -44,36 +45,48 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): value_array = metomi.rose.variable.array_split(value) if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": # boolean -> true/false - self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, - metomi.rose.TYPE_BOOLEAN_VALUE_TRUE] - self.label_dict = dict(list(zip(self.allowed_values, - self.allowed_values))) + self.allowed_values = [ + metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_BOOLEAN_VALUE_TRUE, + ] + self.label_dict = dict( + list(zip(self.allowed_values, self.allowed_values)) + ) elif metadata.get(metomi.rose.META_PROP_TYPE) == "python_boolean": # python_boolean -> True/False - self.allowed_values = [metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, - metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] - self.label_dict = dict(list(zip(self.allowed_values, - self.allowed_values))) + self.allowed_values = [ + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE, + ] + self.label_dict = dict( + list(zip(self.allowed_values, self.allowed_values)) + ) else: # logical -> .true./.false. - self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, - metomi.rose.TYPE_LOGICAL_VALUE_TRUE] + self.allowed_values = [ + metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE, + ] self.label_dict = { - metomi.rose.TYPE_LOGICAL_VALUE_FALSE: - metomi.rose.TYPE_LOGICAL_FALSE_TITLE, - metomi.rose.TYPE_LOGICAL_VALUE_TRUE: - metomi.rose.TYPE_LOGICAL_TRUE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: ( + metomi.rose.TYPE_LOGICAL_FALSE_TITLE + ), + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: ( + metomi.rose.TYPE_LOGICAL_TRUE_TITLE + ), + } - imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), - (Gtk.STOCK_APPLY, Gtk.IconSize.MENU)] + imgs = [ + (Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + ] self.make_log_image = lambda i: Gtk.Image.new_from_stock(*imgs[i]) self.chars_width = max([len(v) for v in value_array] + [1]) + 1 self.num_allowed_columns = 3 - self.entry_table = Gtk.Table(rows=1, - columns=self.num_allowed_columns, - homogeneous=True) - self.entry_table.connect('focus-in-event', - self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=1, columns=self.num_allowed_columns, homogeneous=True + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() self.entries = [] @@ -89,10 +102,13 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.populate_table() self.pack_start(self.button_box, expand=False, fill=False, padding=0) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) - self.entry_table.connect_after('size-allocate', - lambda w, e: self.reshape_table()) - self.connect('focus-in-event', - lambda w, e: self.hook.get_focus(self.get_focus_entry())) + self.entry_table.connect_after( + "size-allocate", lambda w, e: self.reshape_table() + ) + self.connect( + "focus-in-event", + lambda w, e: self.hook.get_focus(self.get_focus_entry()), + ) def get_focus_entry(self): """Get either the last selected button or the last one.""" @@ -105,39 +121,57 @@ def generate_buttons(self): self.add_button = Gtk.EventBox() self.add_button.set_tooltip_text(self.TIP_ADD) self.add_button.add(add_image) - self.add_button.connect('button-release-event', - lambda b, e: self.add_entry()) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + self.add_button.connect( + "button-release-event", lambda b, e: self.add_entry() + ) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_ADD) self.del_button.add(del_image) self.del_button.show() - self.del_button.connect('button-release-event', - self.remove_entry) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect("button-release-event", self.remove_entry) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.button_box.show() - self.button_box.pack_start(self.add_button, expand=False, fill=False, padding=0) - self.button_box.pack_start(self.del_button, expand=False, fill=False, padding=0) + self.button_box.pack_start( + self.add_button, expand=False, fill=False, padding=0 + ) + self.button_box.pack_start( + self.del_button, expand=False, fill=False, padding=0 + ) def get_entry(self, value_item): """Create a widget for this array element.""" - bad_img = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, - Gtk.IconSize.MENU) + bad_img = Gtk.Image.new_from_stock( + Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU + ) button = Gtk.ToggleButton() - button.options = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, - metomi.rose.TYPE_LOGICAL_VALUE_TRUE] - button.labels = [metomi.rose.TYPE_LOGICAL_FALSE_TITLE, - metomi.rose.TYPE_LOGICAL_TRUE_TITLE] + button.options = [ + metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE, + ] + button.labels = [ + metomi.rose.TYPE_LOGICAL_FALSE_TITLE, + metomi.rose.TYPE_LOGICAL_TRUE_TITLE, + ] button.set_tooltip_text(value_item) if value_item in self.allowed_values: index = self.allowed_values.index(value_item) @@ -147,7 +181,7 @@ def get_entry(self, value_item): else: button.set_inconsistent(True) button.set_image(bad_img) - button.connect('toggled', self._switch_state_and_set) + button.connect("toggled", self._switch_state_and_set) button.show() return button @@ -172,47 +206,60 @@ def populate_table(self): focus = self.entries[-1] for child in self.entry_table.get_children(): self.entry_table.remove(child) - if (focus is None and self.entry_table.is_focus() and - len(self.entries) > 0): + if ( + focus is None + and self.entry_table.is_focus() + and len(self.entries) > 0 + ): focus = self.entries[-1] num_fields = len(self.entries) num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns self.entry_table.resize(num_rows_now, self.num_allowed_columns) - if (self.max_length.isdigit() and - len(self.entries) >= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) >= int( + self.max_length + ): self.add_button.hide() else: self.add_button.show() - if (self.max_length.isdigit() and - len(self.entries) <= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) <= int( + self.max_length + ): self.del_button.hide() else: self.del_button.show() if self.has_titles: - for col, label in enumerate(self.metadata['element-titles']): + for col, label in enumerate(self.metadata["element-titles"]): if col >= len(table_widgets): break widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label = Gtk.Label(label=self.metadata['element-titles'][col]) + label = Gtk.Label(label=self.metadata["element-titles"][col]) label.show() widget.pack_start(label, expand=True, fill=True) widget.show() - self.entry_table.attach(widget, - col, col + 1, - 0, 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + col, + col + 1, + 0, + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) for i, widget in enumerate(table_widgets): row = i // self.num_allowed_columns if self.has_titles: row += 1 column = i % self.num_allowed_columns - self.entry_table.attach(widget, - column, column + 1, - row, row + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + column, + column + 1, + row, + row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) self.grab_focus = lambda: self.hook.get_focus(self.entries[-1]) self.check_resize() @@ -249,7 +296,7 @@ def setter(self, *args): for widget in self.entries: value = widget.get_tooltip_text() if value is None: - value = '' + value = "" val_array.append(value) new_val = metomi.rose.variable.array_join(val_array) self.value = new_val diff --git a/metomi/rose/config_editor/valuewidget/array/mixed.py b/metomi/rose/config_editor/valuewidget/array/mixed.py index 32542cbc4..79a83ba93 100644 --- a/metomi/rose/config_editor/valuewidget/array/mixed.py +++ b/metomi/rose/config_editor/valuewidget/array/mixed.py @@ -23,7 +23,8 @@ import math import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk from . import entry @@ -32,7 +33,6 @@ class MixedArrayValueWidget(Gtk.Box): - """This is a class to represent a derived type variable as a table. The type (variable.metadata['type']) should be a list, e.g. @@ -45,16 +45,18 @@ class MixedArrayValueWidget(Gtk.Box): """ BAD_COLOUR = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) - CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match - TIP_ADD = 'Add array element' - TIP_DELETE = 'Remove last array element' + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR + ) + CHECK_NAME_IS_ELEMENT = re.compile(r".*\(\d+\)$").match + TIP_ADD = "Add array element" + TIP_DELETE = "Remove last array element" TIP_INVALID_ENTRY = "Invalid entry - not {0}" MIN_WIDTH_CHARS = 7 def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(MixedArrayValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(MixedArrayValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -64,17 +66,18 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.element_values = [] self.rows = [] self.widgets = [] - self.unlimited = (metadata.get(metomi.rose.META_PROP_LENGTH) == ':') + self.unlimited = metadata.get(metomi.rose.META_PROP_LENGTH) == ":" if self.unlimited: self.array_length = 1 else: self.array_length = metadata.get(metomi.rose.META_PROP_LENGTH, 1) self.num_cols = len(metadata[metomi.rose.META_PROP_TYPE]) - self.types_row = [t for t in - metadata[metomi.rose.META_PROP_TYPE]] - log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), - (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), - (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] + self.types_row = [t for t in metadata[metomi.rose.META_PROP_TYPE]] + log_imgs = [ + (Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU), + ] self.make_log_image = lambda i: Gtk.Image.new_from_stock(*log_imgs[i]) self.has_titles = False @@ -82,23 +85,24 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.has_titles = True self.set_num_rows() - self.entry_table = Gtk.Table(rows=self.num_rows, - columns=self.num_cols, - homogeneous=False) - self.entry_table.connect('focus-in-event', - self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=self.num_rows, columns=self.num_cols, homogeneous=False + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() for i in range(self.num_rows): self.insert_row(i) self.normalise_width_widgets() self.generate_buttons() - self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start( + self.add_del_button_box, expand=False, fill=False, padding=0 + ) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.show() def set_num_rows(self): """Derive the number of columns and rows.""" - if self.CHECK_NAME_IS_ELEMENT(self.metadata['id']): + if self.CHECK_NAME_IS_ELEMENT(self.metadata["id"]): self.unlimited = False if self.unlimited: self.num_rows, rem = divmod(len(self.value_array), self.num_cols) @@ -116,7 +120,7 @@ def set_num_rows(self): self.num_rows = 1 self.max_rows = 1 self.unlimited = False - self.types_row = ['_error_'] + self.types_row = ["_error_"] self.value_array = [self.value] self.has_titles = False if self.num_rows == 0: @@ -135,7 +139,8 @@ def grab_focus(self): def add_row(self, *args): """Create a new row of widgets.""" nrows = self.entry_table.child_get_property( - self.rows[-1][-1], 'top-attach') + self.rows[-1][-1], "top-attach" + ) self.entry_table.resize(nrows + 2, self.num_cols) new_values = self.insert_row(nrows + 1) if any(new_values): @@ -148,7 +153,7 @@ def add_row(self, *args): return False def get_focus_index(self): - text = '' + text = "" if not self.value_array: return 0 for i_row, widget_list in enumerate(self.rows): @@ -157,8 +162,9 @@ def get_focus_index(self): val = self.value_array[i_row * self.num_cols + i] except IndexError: return None - prefix_text = entry.get_next_delimiter(self.value[len(text):], - val) + prefix_text = entry.get_next_delimiter( + self.value[len(text) :], val + ) if prefix_text is None: return if widget == self.entry_table.get_focus_child(): @@ -182,7 +188,7 @@ def set_focus_index(self, focus_index=None): if focus_index is None: return value_array = metomi.rose.variable.array_split(self.value) - text = '' + text = "" widgets = [] for widget_list in self.rows: widgets.extend(widget_list) @@ -193,8 +199,7 @@ def set_focus_index(self, focus_index=None): widgets[0].set_focus_index(focus_index) return for i, val in enumerate(value_array): - prefix = entry.get_next_delimiter(self.value[len(text):], - val) + prefix = entry.get_next_delimiter(self.value[len(text) :], val) if prefix is None: return if len(text + prefix + val) >= focus_index: @@ -209,7 +214,8 @@ def set_focus_index(self, focus_index=None): def del_row(self, *args): """Delete the last row of widgets.""" nrows = self.entry_table.child_get_property( - self.rows[-1][-1], 'top-attach') + self.rows[-1][-1], "top-attach" + ) for _ in enumerate(self.types_row): ent = self.rows[-1][-1] self.rows[-1].pop(-1) @@ -255,28 +261,31 @@ def insert_row(self, row_index): value_index -= len(self.types_row) if value_index < 0: w_value = metomi.rose.variable.get_value_from_metadata( - {metomi.rose.META_PROP_TYPE: el_piece_type}) + {metomi.rose.META_PROP_TYPE: el_piece_type} + ) else: w_value = self.value_array[value_index] new_values.append(w_value) - hover_text = '' + hover_text = "" w_error = {} - if el_piece_type in ['integer', 'real']: + if el_piece_type in ["integer", "real"]: try: - [int, float][el_piece_type == 'real'](w_value) + [int, float][el_piece_type == "real"](w_value) except (TypeError, ValueError): - if w_value != '': + if w_value != "": hover_text = self.TIP_INVALID_ENTRY.format( - el_piece_type) + el_piece_type + ) w_error = {metomi.rose.META_PROP_TYPE: hover_text} w_meta = {metomi.rose.META_PROP_TYPE: el_piece_type} widget_cls = metomi.rose.config_editor.valuewidget.chooser( - w_value, w_meta, w_error) + w_value, w_meta, w_error + ) hook = self.hook setter = ArrayElementSetter(self.setter, unwrapped_index) if self.has_titles and row_index == 0: widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label = Gtk.Label(label=self.metadata['element-titles'][i]) + label = Gtk.Label(label=self.metadata["element-titles"][i]) label.show() widget.pack_start(label, expand=True, fill=True, padding=0) else: @@ -284,11 +293,15 @@ def insert_row(self, row_index): if hover_text: widget.set_tooltip_text(hover_text) widget.show() - self.entry_table.attach(widget, - i, i + 1, - insert_row_index, insert_row_index + 1, - xoptions=Gtk.AttachOptions.SHRINK, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + i, + i + 1, + insert_row_index, + insert_row_index + 1, + xoptions=Gtk.AttachOptions.SHRINK, + yoptions=Gtk.AttachOptions.SHRINK, + ) widget_list.append(widget) self.rows.append(widget_list) self.widgets.extend(widget_list) @@ -311,15 +324,17 @@ def _normalise_width_chars(self, widget): child_list = e_widget.get_children() while child_list: child = child_list.pop() - if (isinstance(child, Gtk.Label) or - isinstance(child, Gtk.Entry) and - hasattr(child, 'get_text')): + if ( + isinstance(child, Gtk.Label) + or isinstance(child, Gtk.Entry) + and hasattr(child, "get_text") + ): width = len(child.get_text()) if width > max_width.get(i, -1): max_width.update({i: width}) - if hasattr(child, 'get_children'): + if hasattr(child, "get_children"): child_list.extend(child.get_children()) - elif hasattr(child, 'get_child'): + elif hasattr(child, "get_child"): child_list.append(child.get_child()) i += 1 for key, value in list(max_width.items()): @@ -332,45 +347,57 @@ def _normalise_width_chars(self, widget): child_list = e_widget.get_children() while child_list: child = child_list.pop() - if (isinstance(child, Gtk.Entry) and - hasattr(child, 'set_width_chars')): + if isinstance(child, Gtk.Entry) and hasattr( + child, "set_width_chars" + ): child.set_width_chars(max_width[i]) - if hasattr(child, 'get_children'): + if hasattr(child, "get_children"): child_list.extend(child.get_children()) - elif hasattr(child, 'get_child'): + elif hasattr(child, "get_child"): child_list.append(child.get_child()) i += 1 def generate_buttons(self): """Insert an add row and delete row button.""" - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_ADD) self.del_button.add(del_image) self.del_button.show() - self.del_button.connect('button-release-event', self.del_row) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect("button-release-event", self.del_row) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() self.add_button.set_tooltip_text(self.TIP_ADD) self.add_button.add(add_image) self.add_button.show() - self.add_button.connect('button-release-event', self.add_row) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_button.connect("button-release-event", self.add_row) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False, padding=0) + self.add_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False, padding=0) + self.del_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.show() self._decide_show_buttons() @@ -387,8 +414,9 @@ def setter(self, array_index, element_value): ok_index = 0 j = self.num_cols while j <= len(self.extra_array): - if (len(self.extra_array[:j]) % self.num_cols == 0 and - all(self.extra_array[:j])): + if len(self.extra_array[:j]) % self.num_cols == 0 and all( + self.extra_array[:j] + ): ok_index = j else: break @@ -404,7 +432,6 @@ def setter(self, array_index, element_value): class ArrayElementSetter(object): - """Element widget setter class.""" def __init__(self, setter_function, index): diff --git a/metomi/rose/config_editor/valuewidget/array/python_list.py b/metomi/rose/config_editor/valuewidget/array/python_list.py index 50c78f96b..812a9d199 100644 --- a/metomi/rose/config_editor/valuewidget/array/python_list.py +++ b/metomi/rose/config_editor/valuewidget/array/python_list.py @@ -21,7 +21,8 @@ import ast import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk from . import entry @@ -31,7 +32,6 @@ class PythonListValueWidget(Gtk.Box): - """This is a class to represent a Python-compatible list format.""" TIP_ADD = "Add array element" @@ -42,8 +42,9 @@ class PythonListValueWidget(Gtk.Box): TIP_RIGHT = "Move array element right" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(PythonListValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(PythonListValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -54,10 +55,10 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.last_selected_src = None # Designate the number of allowed columns - 10 for 4 chars width self.num_allowed_columns = 3 - self.entry_table = Gtk.Table(rows=1, - columns=self.num_allowed_columns, - homogeneous=True) - self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=1, columns=self.num_allowed_columns, homogeneous=True + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() self.entries = [] @@ -69,12 +70,17 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start( + self.add_del_button_box, expand=False, fill=False, padding=0 + ) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) - self.entry_table.connect_after('size-allocate', - lambda w, e: self.reshape_table()) - self.connect('focus-in-event', - lambda w, e: self.hook.get_focus(self.get_focus_entry())) + self.entry_table.connect_after( + "size-allocate", lambda w, e: self.reshape_table() + ) + self.connect( + "focus-in-event", + lambda w, e: self.hook.get_focus(self.get_focus_entry()), + ) def force_scroll(self, widget=None): """Adjusts a scrolled window to display the correct widget.""" @@ -108,10 +114,10 @@ def get_focus_index(self): """Get the focus and position within the table of entries.""" if not self.value.startswith("["): return - text = '[' + text = "[" for my_entry in self.entries: val = my_entry.get_text() - prefix = entry.get_next_delimiter(self.value[len(text):], val) + prefix = entry.get_next_delimiter(self.value[len(text) :], val) if prefix is None: return if my_entry == self.entry_table.get_focus_child(): @@ -126,14 +132,15 @@ def set_focus_index(self, focus_index=None): value_array = python_array_split(self.value) if not self.value.startswith("["): return - text = '[' + text = "[" for i, val in enumerate(value_array): - prefix = entry.get_next_delimiter(self.value[len(text):], - val) + prefix = entry.get_next_delimiter(self.value[len(text) :], val) if prefix is None: return - if (len(text + prefix + val) >= focus_index or - i == len(value_array) - 1): + if ( + len(text + prefix + val) >= focus_index + or i == len(value_array) - 1 + ): if len(self.entries) > i: self.entries[i].grab_focus() val_offset = focus_index - len(text + prefix) @@ -160,7 +167,7 @@ def generate_buttons(self): left_arrow = Gtk.ToolButton() left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() - left_arrow.connect('clicked', lambda x: self.move_element(-1)) + left_arrow.connect("clicked", lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() @@ -168,49 +175,68 @@ def generate_buttons(self): right_arrow = Gtk.ToolButton() right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() - right_arrow.connect('clicked', lambda x: self.move_element(1)) + right_arrow.connect("clicked", lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() right_event_box.add(right_arrow) right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_start( + left_event_box, expand=False, fill=False, padding=0 + ) + self.arrow_box.pack_end( + right_event_box, expand=False, fill=False, padding=0 + ) self.set_arrow_sensitive(False, False) - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_DEL) self.del_button.add(del_image) self.del_button.show() - self.del_button.connect('button-release-event', - lambda b, e: self.remove_entry()) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect( + "button-release-event", lambda b, e: self.remove_entry() + ) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) + self.button_box.pack_start( + self.arrow_box, expand=False, fill=True, padding=0 + ) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() self.add_button.set_tooltip_text(self.TIP_ADD) self.add_button.add(add_image) self.add_button.show() - self.add_button.connect('button-release-event', - lambda b, e: self.add_entry()) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_button.connect( + "button-release-event", lambda b, e: self.add_entry() + ) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False, padding=0) + self.add_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False, padding=0) + self.del_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.show() def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): @@ -227,8 +253,10 @@ def move_element(self, num_places_right): if widget is None: return old_index = self.entries.index(widget) - if (old_index + num_places_right < 0 or - old_index + num_places_right > len(self.entries) - 1): + if ( + old_index + num_places_right < 0 + or old_index + num_places_right > len(self.entries) - 1 + ): return self.entries.remove(widget) self.entries.insert(old_index + num_places_right, widget) @@ -239,13 +267,14 @@ def get_entry(self, value_item): """Create a gtk Entry for this array element.""" widget = Gtk.Entry() widget.set_text(value_item) - widget.connect('focus-in-event', self._handle_focus_on_entry) + widget.connect("focus-in-event", self._handle_focus_on_entry) widget.connect("button-release-event", self._handle_middle_click_paste) widget.connect_after("paste-clipboard", self.setter) widget.connect_after("key-release-event", lambda e, v: self.setter(e)) widget.connect_after( - "button-release-event", lambda e, v: self.setter(e)) - widget.connect('focus-out-event', self._handle_focus_off_entry) + "button-release-event", lambda e, v: self.setter(e) + ) + widget.connect("focus-out-event", self._handle_focus_off_entry) widget.set_width_chars(self.chars_width - 1) widget.show() return widget @@ -264,27 +293,34 @@ def populate_table(self, focus_widget=None): position = focus_widget.get_position() for child in self.entry_table.get_children(): self.entry_table.remove(child) - if (focus_widget is None and self.entry_table.is_focus() and - len(self.entries) > 0): + if ( + focus_widget is None + and self.entry_table.is_focus() + and len(self.entries) > 0 + ): focus_widget = self.entries[-1] position = len(focus_widget.get_text()) num_fields = len(self.entries + [self.button_box]) num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns self.entry_table.resize(num_rows_now, self.num_allowed_columns) - if (self.max_length.isdigit() and - len(self.entries) >= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) >= int( + self.max_length + ): self.add_button.hide() else: self.add_button.show() - if (self.max_length.isdigit() and - len(self.entries) <= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) <= int( + self.max_length + ): self.del_button.hide() elif len(self.entries) == 0: self.del_button.hide() else: self.del_button.show() - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): index = self.entries.index(self.last_selected_src) if index == 0: self.set_arrow_sensitive(False, True) @@ -294,19 +330,23 @@ def populate_table(self, focus_widget=None): self.set_arrow_sensitive(False, False) if self.has_titles: - for col, label in enumerate(self.metadata['element-titles']): + for col, label in enumerate(self.metadata["element-titles"]): if col >= len(table_widgets) - 1: break widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label = Gtk.Label(label=self.metadata['element-titles'][col]) + label = Gtk.Label(label=self.metadata["element-titles"][col]) label.show() widget.pack_start(label, expand=True, fill=True, padding=0) widget.show() - self.entry_table.attach(widget, - col, col + 1, - 0, 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + col, + col + 1, + 0, + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) for i, widget in enumerate(table_widgets): if isinstance(widget, Gtk.Entry): @@ -315,17 +355,22 @@ def populate_table(self, focus_widget=None): if self.has_titles: row += 1 column = i % self.num_allowed_columns - self.entry_table.attach(widget, - column, column + 1, - row, row + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + column, + column + 1, + row, + row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) if focus_widget is not None: focus_widget.grab_focus() focus_widget.set_position(position) focus_widget.select_region(position, position) self.grab_focus = lambda: self.hook.get_focus( - self._get_widget_for_focus()) + self._get_widget_for_focus() + ) self.check_resize() def reshape_table(self): @@ -343,30 +388,38 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" - widget = self.get_entry('') - widget.connect('focus-in-event', lambda w, e: self.force_scroll(w)) + widget = self.get_entry("") + widget.connect("focus-in-event", lambda w, e: self.force_scroll(w)) self.last_selected_src = widget self.entries.append(widget) self._adjust_entry_length() self.populate_table(focus_widget=widget) - if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE): + if ( + self.metadata.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + ): self.setter(widget) def remove_entry(self): """Remove the last selected or the last entry.""" - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): text = self.last_selected_src.get_text() widget = self.entries.pop( - self.entries.index(self.last_selected_src)) + self.entries.index(self.last_selected_src) + ) self.last_selected_src = None else: text = self.entries[-1].get_text() widget = self.entries.pop() self.populate_table() - if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE or text): + if ( + self.metadata.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + or text + ): # Optional, or compulsory but not blank. self.setter(widget) @@ -376,15 +429,17 @@ def setter(self, widget): # Prevent str without "" breaking the underlying Python syntax for e in self.entries: v = e.get_text() - if v in ("False", "True"): # Boolean + if v in ("False", "True"): # Boolean val_array.append(v) - elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric val_array.append(v) - elif not v.startswith('"'): # Str - add in leading and trailing " + elif not v.startswith('"'): # Str - add in leading and trailing " val_array.append('"' + v + '"') e.set_text('"' + v + '"') - e.set_position(len(v)+1) - elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + e.set_position(len(v) + 1) + elif (not v.endswith('"')) or ( + len(v) == 1 + ): # Str - add in trailing " val_array.append(v + '"') e.set_text(v + '"') e.set_position(len(v)) @@ -397,8 +452,9 @@ def setter(self, widget): if widget is not None and not widget.is_focus(): widget.grab_focus() widget.set_position(len(widget.get_text())) - widget.select_region(widget.get_position(), - widget.get_position()) + widget.select_region( + widget.get_position(), widget.get_position() + ) entries_have_commas = any("," in v for v in val_array) new_value = python_array_join(val_array) if new_value != self.value: @@ -411,9 +467,12 @@ def setter(self, widget): focus_index = None for i, val in enumerate(val_array): if "," in val: - val_post_comma = val[:val.index(",") + 1] - focus_index = len(python_array_join( - new_val_array[:i] + [val_post_comma])) + val_post_comma = val[: val.index(",") + 1] + focus_index = len( + python_array_join( + new_val_array[:i] + [val_post_comma] + ) + ) self.populate_table() self.set_focus_index(focus_index) return False @@ -445,12 +504,11 @@ def _handle_focus_on_entry(self, widget, event): except AttributeError: self.last_selected_src.drag_unhighlight() self.last_selected_src = widget - is_start = (widget in self.entries and self.entries[0] == widget) - is_end = (widget in self.entries and self.entries[-1] == widget) + is_start = widget in self.entries and self.entries[0] == widget + is_end = widget in self.entries and self.entries[-1] == widget self.set_arrow_sensitive(not is_start, not is_end) - if widget.get_text() != '': - widget.select_region(widget.get_position(), - widget.get_position()) + if widget.get_text() != "": + widget.select_region(widget.get_position(), widget.get_position()) return False def _handle_middle_click_paste(self, widget, event): diff --git a/metomi/rose/config_editor/valuewidget/array/row.py b/metomi/rose/config_editor/valuewidget/array/row.py index 7ba627f54..d00bb5f51 100644 --- a/metomi/rose/config_editor/valuewidget/array/row.py +++ b/metomi/rose/config_editor/valuewidget/array/row.py @@ -23,7 +23,8 @@ import math import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk from . import entry @@ -32,20 +33,19 @@ class RowArrayValueWidget(Gtk.Box): - """This is a class to represent a value as part of a row.""" BAD_COLOUR = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) - CHECK_NAME_IS_ELEMENT = re.compile(r'.*\(\d+\)$').match - TIP_ADD = 'Add array element' - TIP_DELETE = 'Remove last array element' + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR + ) + CHECK_NAME_IS_ELEMENT = re.compile(r".*\(\d+\)$").match + TIP_ADD = "Add array element" + TIP_DELETE = "Remove last array element" TIP_INVALID_ENTRY = "Invalid entry - not {0}" MIN_WIDTH_CHARS = 7 def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(RowArrayValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(RowArrayValueWidget, self).__init__(homogeneous=False, spacing=0) self.value = value self.metadata = metadata self.set_value = set_value @@ -66,27 +66,30 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.num_cols = int(self.length) else: self.num_cols = int(arg_str) - self.unlimited = (self.length == ':') + self.unlimited = self.length == ":" if self.unlimited: self.array_length = 1 else: self.array_length = metadata.get(metomi.rose.META_PROP_LENGTH, 1) - log_imgs = [(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), - (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), - (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU)] + log_imgs = [ + (Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + (Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + (Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU), + ] self.make_log_image = lambda i: Gtk.Image.new_from_stock(*log_imgs[i]) self.set_num_rows() - self.entry_table = Gtk.Table(rows=self.num_rows, - columns=self.num_cols, - homogeneous=True) - self.entry_table.connect('focus-in-event', - self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=self.num_rows, columns=self.num_cols, homogeneous=True + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() for i in range(self.num_rows): self.insert_row(i) self.normalise_width_widgets() self.generate_buttons(is_for_elements=not isinstance(self.type, list)) - self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start( + self.add_del_button_box, expand=False, fill=False, padding=0 + ) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) self.show() @@ -98,7 +101,7 @@ def set_num_rows(self): self.unlimited = False return columns = len(self.type) - if self.CHECK_NAME_IS_ELEMENT(self.metadata['id']): + if self.CHECK_NAME_IS_ELEMENT(self.metadata["id"]): self.unlimited = False if self.unlimited: self.num_rows, rem = divmod(len(self.value_array), columns) @@ -142,7 +145,8 @@ def grab_focus(self): def add_element(self, *args): """Create a new element (non-derived types).""" w_value = metomi.rose.variable.get_value_from_metadata( - {metomi.rose.META_PROP_TYPE: self.type}) + {metomi.rose.META_PROP_TYPE: self.type} + ) self.value_array = self.value_array + [w_value] self.value = metomi.rose.variable.array_join(self.value_array) self.set_value(self.value) @@ -156,7 +160,8 @@ def add_element(self, *args): def add_row(self, *args): """Create a new row of widgets.""" nrows = self.entry_table.child_get_property( - self.rows[-1][-1], 'top-attach') + self.rows[-1][-1], "top-attach" + ) self.entry_table.resize(nrows + 2, self.num_cols) new_values = self.insert_row(nrows + 1) if any(new_values): @@ -169,15 +174,16 @@ def add_row(self, *args): return False def get_focus_index(self): - text = '' + text = "" for i, widget_list in enumerate(self.rows): for j, widget in enumerate(widget_list): value_index = i * self.num_cols + j if value_index > len(self.value_array) - 1: return len(text) val = self.value_array[i * self.num_cols + j] - prefix_text = entry.get_next_delimiter(self.value[len(text):], - val) + prefix_text = entry.get_next_delimiter( + self.value[len(text) :], val + ) if prefix_text is None: return if widget == self.entry_table.get_focus_child(): @@ -201,7 +207,7 @@ def set_focus_index(self, focus_index=None): if focus_index is None: return value_array = metomi.rose.variable.array_split(self.value) - text = '' + text = "" widgets = [] for widget_list in self.rows: widgets.extend(widget_list) @@ -211,7 +217,7 @@ def set_focus_index(self, focus_index=None): widgets[0].set_focus_index(focus_index) return for i, val in enumerate(value_array): - prefix = entry.get_next_delimiter(self.value[len(text):], val) + prefix = entry.get_next_delimiter(self.value[len(text) :], val) if prefix is None: return if len(text + prefix + val) >= focus_index: @@ -238,7 +244,8 @@ def del_element(self, *args): def del_row(self, *args): """Delete the last row of widgets.""" nrows = self.entry_table.child_get_property( - self.rows[-1][-1], 'top-attach') + self.rows[-1][-1], "top-attach" + ) for _ in enumerate(self.get_types()): widget = self.rows[-1][-1] self.rows[-1].pop(-1) @@ -269,8 +276,11 @@ def _decide_show_buttons(self): else: self.add_button.show() else: - if (self.length is not None and self.length.isdigit() and - len(self.value_array) >= int(self.length)): + if ( + self.length is not None + and self.length.isdigit() + and len(self.value_array) >= int(self.length) + ): self.add_button.hide() self.del_button.show() else: @@ -287,52 +297,66 @@ def insert_row(self, row_index): for i, el_piece_type in enumerate(self.get_types()): unwrapped_index = row_index * actual_num_cols + i value_index = unwrapped_index - if (not isinstance(self.type, list) and - value_index >= len(self.value_array)): + if not isinstance(self.type, list) and value_index >= len( + self.value_array + ): widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) eb0 = Gtk.EventBox() eb0.show() widget.pack_start(eb0, expand=True, fill=True, padding=0) widget.show() - self.entry_table.attach(widget, - i, i + 1, - row_index, row_index + 1, - xoptions=(Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL), - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + i, + i + 1, + row_index, + row_index + 1, + xoptions=( + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL + ), + yoptions=Gtk.AttachOptions.SHRINK, + ) widget_list.append(widget) continue while value_index > len(self.value_array) - 1: value_index -= actual_num_cols if value_index < 0: w_value = metomi.rose.variable.get_value_from_metadata( - {metomi.rose.META_PROP_TYPE: el_piece_type}) + {metomi.rose.META_PROP_TYPE: el_piece_type} + ) else: w_value = self.value_array[value_index] new_values.append(w_value) - hover_text = '' + hover_text = "" w_error = {} - if el_piece_type in ['integer', 'real']: + if el_piece_type in ["integer", "real"]: try: - [int, float][el_piece_type == 'real'](w_value) + [int, float][el_piece_type == "real"](w_value) except (TypeError, ValueError): - if w_value != '': + if w_value != "": hover_text = self.TIP_INVALID_ENTRY.format( - el_piece_type) + el_piece_type + ) w_error = {metomi.rose.META_PROP_TYPE: hover_text} w_meta = {metomi.rose.META_PROP_TYPE: el_piece_type} widget_cls = metomi.rose.config_editor.valuewidget.chooser( - w_value, w_meta, w_error) + w_value, w_meta, w_error + ) hook = self.hook setter = ArrayElementSetter(self.setter, unwrapped_index) widget = widget_cls(w_value, w_meta, setter.set_value, hook) if hover_text: widget.set_tooltip_text(hover_text) widget.show() - self.entry_table.attach(widget, - i, i + 1, - row_index, row_index + 1, - xoptions=(Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL), - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + i, + i + 1, + row_index, + row_index + 1, + xoptions=(Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL), + yoptions=Gtk.AttachOptions.SHRINK, + ) widget_list.append(widget) self.rows.append(widget_list) self.widgets.extend(widget_list) @@ -357,13 +381,13 @@ def _normalise_width_chars(self, widget): child_list = e_widget.get_children() while child_list: child = child_list.pop() - if isinstance(child, Gtk.Entry) and hasattr(child, 'get_text'): + if isinstance(child, Gtk.Entry) and hasattr(child, "get_text"): width = len(child.get_text()) if width > max_width.get(i, -1): max_width.update({i: width}) - if hasattr(child, 'get_children'): + if hasattr(child, "get_children"): child_list.extend(child.get_children()) - elif hasattr(child, 'get_child'): + elif hasattr(child, "get_child"): child_list.append(child.get_child()) i += 1 for key, value in list(max_width.items()): @@ -378,19 +402,21 @@ def _normalise_width_chars(self, widget): child_list = e_widget.get_children() while child_list: child = child_list.pop() - if (isinstance(child, Gtk.Entry) and - hasattr(child, 'set_width_chars')): + if isinstance(child, Gtk.Entry) and hasattr( + child, "set_width_chars" + ): child.set_width_chars(max_width[i]) - if hasattr(child, 'get_children'): + if hasattr(child, "get_children"): child_list.extend(child.get_children()) - elif hasattr(child, 'get_child'): + elif hasattr(child, "get_child"): child_list.append(child.get_child()) i += 1 def generate_buttons(self, is_for_elements=False): """Insert an add row and delete row button.""" - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_DELETE) @@ -400,11 +426,15 @@ def generate_buttons(self, is_for_elements=False): delete_func = self.del_element else: delete_func = self.del_row - self.del_button.connect('button-release-event', delete_func) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect("button-release-event", delete_func) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() @@ -415,16 +445,22 @@ def generate_buttons(self, is_for_elements=False): add_func = self.add_element else: add_func = self.add_row - self.add_button.connect('button-release-event', add_func) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_button.connect("button-release-event", add_func) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False, padding=0) + self.add_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False, padding=0) + self.del_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.show() self._decide_show_buttons() @@ -442,8 +478,9 @@ def setter(self, array_index, element_value): ok_index = 0 j = self.num_cols while j <= len(self.extra_array): - if (len(self.extra_array[:j]) % self.num_cols == 0 and - all(self.extra_array[:j])): + if len(self.extra_array[:j]) % self.num_cols == 0 and all( + self.extra_array[:j] + ): ok_index = j else: break @@ -459,7 +496,6 @@ def setter(self, array_index, element_value): class ArrayElementSetter(object): - """Element widget setter class.""" def __init__(self, setter_function, index): diff --git a/metomi/rose/config_editor/valuewidget/array/spaced_list.py b/metomi/rose/config_editor/valuewidget/array/spaced_list.py index a4c8b3337..a2219e1f0 100644 --- a/metomi/rose/config_editor/valuewidget/array/spaced_list.py +++ b/metomi/rose/config_editor/valuewidget/array/spaced_list.py @@ -21,7 +21,8 @@ import shlex import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor.util @@ -30,7 +31,6 @@ class SpacedListValueWidget(Gtk.Box): - """This is a class to represent a list separated by spaces.""" TIP_ADD = "Add array element" @@ -41,8 +41,9 @@ class SpacedListValueWidget(Gtk.Box): TIP_RIGHT = "Move array element right" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(SpacedListValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(SpacedListValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -54,10 +55,10 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.last_selected_src = None # Designate the number of allowed columns - 10 for 4 chars width self.num_allowed_columns = 3 - self.entry_table = Gtk.Table(rows=1, - columns=self.num_allowed_columns, - homogeneous=True) - self.entry_table.connect('focus-in-event', self.hook.trigger_scroll) + self.entry_table = Gtk.Table( + rows=1, columns=self.num_allowed_columns, homogeneous=True + ) + self.entry_table.connect("focus-in-event", self.hook.trigger_scroll) self.entry_table.show() self.entries = [] @@ -69,12 +70,17 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.generate_buttons() self.populate_table() - self.pack_start(self.add_del_button_box, expand=False, fill=False, padding=0) + self.pack_start( + self.add_del_button_box, expand=False, fill=False, padding=0 + ) self.pack_start(self.entry_table, expand=True, fill=True, padding=0) - self.entry_table.connect_after('size-allocate', - lambda w, e: self.reshape_table()) - self.connect('focus-in-event', - lambda w, e: self.hook.get_focus(self.get_focus_entry())) + self.entry_table.connect_after( + "size-allocate", lambda w, e: self.reshape_table() + ) + self.connect( + "focus-in-event", + lambda w, e: self.hook.get_focus(self.get_focus_entry()), + ) def force_scroll(self, widget=None): """Adjusts a scrolled window to display the correct widget.""" @@ -106,10 +112,10 @@ def get_focus_entry(self): def get_focus_index(self): """Get the focus and position within the table of entries.""" - text = '' + text = "" for my_entry in self.entries: val = my_entry.get_text() - prefix = get_next_delimiter(self.value[len(text):], val) + prefix = get_next_delimiter(self.value[len(text) :], val) if prefix is None: return if my_entry == self.entry_table.get_focus_child(): @@ -150,7 +156,7 @@ def generate_buttons(self): left_arrow = Gtk.ToolButton() left_arrow.set_icon_name("pan-start-symbolic") left_arrow.show() - left_arrow.connect('clicked', lambda x: self.move_element(-1)) + left_arrow.connect("clicked", lambda x: self.move_element(-1)) left_event_box = Gtk.EventBox() left_event_box.add(left_arrow) left_event_box.show() @@ -158,49 +164,68 @@ def generate_buttons(self): right_arrow = Gtk.ToolButton() right_arrow.set_icon_name("pan-end-symbolic") right_arrow.show() - right_arrow.connect('clicked', lambda x: self.move_element(1)) + right_arrow.connect("clicked", lambda x: self.move_element(1)) right_event_box = Gtk.EventBox() right_event_box.add(right_arrow) right_event_box.show() right_event_box.set_tooltip_text(self.TIP_RIGHT) self.arrow_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.arrow_box.show() - self.arrow_box.pack_start(left_event_box, expand=False, fill=False, padding=0) - self.arrow_box.pack_end(right_event_box, expand=False, fill=False, padding=0) + self.arrow_box.pack_start( + left_event_box, expand=False, fill=False, padding=0 + ) + self.arrow_box.pack_end( + right_event_box, expand=False, fill=False, padding=0 + ) self.set_arrow_sensitive(False, False) - del_image = Gtk.Image.new_from_stock(Gtk.STOCK_REMOVE, - Gtk.IconSize.MENU) + del_image = Gtk.Image.new_from_stock( + Gtk.STOCK_REMOVE, Gtk.IconSize.MENU + ) del_image.show() self.del_button = Gtk.EventBox() self.del_button.set_tooltip_text(self.TIP_DEL) self.del_button.add(del_image) self.del_button.show() - self.del_button.connect('button-release-event', - lambda b, e: self.remove_entry()) - self.del_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.del_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.del_button.connect( + "button-release-event", lambda b, e: self.remove_entry() + ) + self.del_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.del_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.button_box.show() - self.button_box.pack_start(self.arrow_box, expand=False, fill=True, padding=0) + self.button_box.pack_start( + self.arrow_box, expand=False, fill=True, padding=0 + ) add_image = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU) add_image.show() self.add_button = Gtk.EventBox() self.add_button.set_tooltip_text(self.TIP_ADD) self.add_button.add(add_image) self.add_button.show() - self.add_button.connect('button-release-event', - lambda b, e: self.add_entry()) - self.add_button.connect('enter-notify-event', - lambda b, e: b.set_state(Gtk.StateType.ACTIVE)) - self.add_button.connect('leave-notify-event', - lambda b, e: b.set_state(Gtk.StateType.NORMAL)) + self.add_button.connect( + "button-release-event", lambda b, e: self.add_entry() + ) + self.add_button.connect( + "enter-notify-event", + lambda b, e: b.set_state(Gtk.StateType.ACTIVE), + ) + self.add_button.connect( + "leave-notify-event", + lambda b, e: b.set_state(Gtk.StateType.NORMAL), + ) self.add_del_button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add_del_button_box.pack_start( - self.add_button, expand=False, fill=False, padding=0) + self.add_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.pack_start( - self.del_button, expand=False, fill=False, padding=0) + self.del_button, expand=False, fill=False, padding=0 + ) self.add_del_button_box.show() def set_arrow_sensitive(self, is_left_sensitive, is_right_sensitive): @@ -217,8 +242,10 @@ def move_element(self, num_places_right): if entry is None: return old_index = self.entries.index(entry) - if (old_index + num_places_right < 0 or - old_index + num_places_right > len(self.entries) - 1): + if ( + old_index + num_places_right < 0 + or old_index + num_places_right > len(self.entries) - 1 + ): return self.entries.remove(entry) self.entries.insert(old_index + num_places_right, entry) @@ -229,17 +256,14 @@ def get_entry(self, value_item): """Create a gtk Entry for this array element.""" entry = Gtk.Entry() entry.set_text(str(value_item)) - entry.connect('focus-in-event', - self._handle_focus_on_entry) - entry.connect("button-release-event", - self._handle_middle_click_paste) + entry.connect("focus-in-event", self._handle_focus_on_entry) + entry.connect("button-release-event", self._handle_middle_click_paste) entry.connect_after("paste-clipboard", self.setter) - entry.connect_after("key-release-event", - lambda e, v: self.setter(e)) - entry.connect_after("button-release-event", - lambda e, v: self.setter(e)) - entry.connect('focus-out-event', - self._handle_focus_off_entry) + entry.connect_after("key-release-event", lambda e, v: self.setter(e)) + entry.connect_after( + "button-release-event", lambda e, v: self.setter(e) + ) + entry.connect("focus-out-event", self._handle_focus_off_entry) entry.set_width_chars(self.chars_width - 1) entry.show() return entry @@ -258,27 +282,34 @@ def populate_table(self, focus_widget=None): position = focus_widget.get_position() for child in self.entry_table.get_children(): self.entry_table.remove(child) - if (focus_widget is None and self.entry_table.is_focus() and - len(self.entries) > 0): + if ( + focus_widget is None + and self.entry_table.is_focus() + and len(self.entries) > 0 + ): focus_widget = self.entries[-1] position = len(focus_widget.get_text()) num_fields = len(self.entries + [self.button_box]) num_rows_now = 1 + (num_fields - 1) / self.num_allowed_columns self.entry_table.resize(num_rows_now, self.num_allowed_columns) - if (self.max_length.isdigit() and - len(self.entries) >= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) >= int( + self.max_length + ): self.add_button.hide() else: self.add_button.show() - if (self.max_length.isdigit() and - len(self.entries) <= int(self.max_length)): + if self.max_length.isdigit() and len(self.entries) <= int( + self.max_length + ): self.del_button.hide() elif len(self.entries) == 0: self.del_button.hide() else: self.del_button.show() - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): index = self.entries.index(self.last_selected_src) if index == 0: self.set_arrow_sensitive(False, True) @@ -288,19 +319,23 @@ def populate_table(self, focus_widget=None): self.set_arrow_sensitive(False, False) if self.has_titles: - for col, label in enumerate(self.metadata['element-titles']): + for col, label in enumerate(self.metadata["element-titles"]): if col >= len(table_widgets) - 1: break widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label = Gtk.Label(label=self.metadata['element-titles'][col]) + label = Gtk.Label(label=self.metadata["element-titles"][col]) label.show() widget.pack_start(label, expand=True, fill=True, padding=0) widget.show() - self.entry_table.attach(widget, - col, col + 1, - 0, 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + col, + col + 1, + 0, + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) for i, widget in enumerate(table_widgets): if isinstance(widget, Gtk.Entry): @@ -309,17 +344,22 @@ def populate_table(self, focus_widget=None): if self.has_titles: row += 1 column = i % self.num_allowed_columns - self.entry_table.attach(widget, - column, column + 1, - row, row + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.SHRINK) + self.entry_table.attach( + widget, + column, + column + 1, + row, + row + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.SHRINK, + ) if focus_widget is not None: focus_widget.grab_focus() focus_widget.set_position(position) focus_widget.select_region(position, position) self.grab_focus = lambda: self.hook.get_focus( - self._get_widget_for_focus()) + self._get_widget_for_focus() + ) self.check_resize() def reshape_table(self): @@ -337,30 +377,38 @@ def reshape_table(self): def add_entry(self): """Add a new entry (with null text) to the variable array.""" - entry = self.get_entry('') - entry.connect('focus-in-event', lambda w, e: self.force_scroll(w)) + entry = self.get_entry("") + entry.connect("focus-in-event", lambda w, e: self.force_scroll(w)) self.last_selected_src = entry self.entries.append(entry) self._adjust_entry_length() self.populate_table(focus_widget=entry) - if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE): + if ( + self.metadata.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + ): self.setter(entry) def remove_entry(self): """Remove the last selected or the last entry.""" - if (self.last_selected_src is not None and - self.last_selected_src in self.entries): + if ( + self.last_selected_src is not None + and self.last_selected_src in self.entries + ): text = self.last_selected_src.get_text() entry = self.entries.pop( - self.entries.index(self.last_selected_src)) + self.entries.index(self.last_selected_src) + ) self.last_selected_src = None else: text = self.entries[-1].get_text() entry = self.entries.pop() self.populate_table() - if (self.metadata.get(metomi.rose.META_PROP_COMPULSORY) != - metomi.rose.META_PROP_VALUE_TRUE or text): + if ( + self.metadata.get(metomi.rose.META_PROP_COMPULSORY) + != metomi.rose.META_PROP_VALUE_TRUE + or text + ): # Optional, or compulsory but not blank. self.setter(entry) @@ -370,15 +418,17 @@ def setter(self, widget): # Prevent str without "" breaking the underlying Python syntax for e in self.entries: v = e.get_text() - if v in ("False", "True"): # Boolean + if v in ("False", "True"): # Boolean val_array.append(v) - elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric val_array.append(v) - elif not v.startswith('"'): # Str - add in leading and trailing " + elif not v.startswith('"'): # Str - add in leading and trailing " val_array.append('"' + v + '"') e.set_text('"' + v + '"') - e.set_position(len(v)+1) - elif (not v.endswith('"')) or (len(v) == 1): # Str - add in trailing " + e.set_position(len(v) + 1) + elif (not v.endswith('"')) or ( + len(v) == 1 + ): # Str - add in trailing " val_array.append(v + '"') e.set_text(v + '"') e.set_position(len(v)) @@ -391,8 +441,9 @@ def setter(self, widget): if widget is not None and not widget.is_focus(): widget.grab_focus() widget.set_position(len(widget.get_text())) - widget.select_region(widget.get_position(), - widget.get_position()) + widget.select_region( + widget.get_position(), widget.get_position() + ) entries_have_spaces = any(" " in v for v in val_array) new_value = spaced_array_join(val_array) if new_value != self.value: @@ -406,9 +457,12 @@ def setter(self, widget): focus_index = None for i, val in enumerate(val_array): if "" in val: - val_post_comma = val[:val.index("") + 1] - focus_index = len(spaced_array_join( - new_val_array[:i] + [val_post_comma])) + val_post_comma = val[: val.index("") + 1] + focus_index = len( + spaced_array_join( + new_val_array[:i] + [val_post_comma] + ) + ) self.populate_table() self.set_focus_index(focus_index) return False @@ -439,12 +493,11 @@ def _handle_focus_on_entry(self, widget, event): except AttributeError: self.last_selected_src.drag_unhighlight() self.last_selected_src = widget - is_start = (widget in self.entries and self.entries[0] == widget) - is_end = (widget in self.entries and self.entries[-1] == widget) + is_start = widget in self.entries and self.entries[0] == widget + is_end = widget in self.entries and self.entries[-1] == widget self.set_arrow_sensitive(not is_start, not is_end) - if widget.get_text() != '': - widget.select_region(widget.get_position(), - widget.get_position()) + if widget.get_text() != "": + widget.select_region(widget.get_position(), widget.get_position()) return False def _handle_middle_click_paste(self, widget, event): diff --git a/metomi/rose/config_editor/valuewidget/boolradio.py b/metomi/rose/config_editor/valuewidget/boolradio.py index 81ed44473..e78df701f 100644 --- a/metomi/rose/config_editor/valuewidget/boolradio.py +++ b/metomi/rose/config_editor/valuewidget/boolradio.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor @@ -27,12 +28,10 @@ class BoolValueWidget(radiobuttons.RadioButtonsValueWidget): - """Produces 'true' and 'false' labelled radio buttons.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(BoolValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(BoolValueWidget, self).__init__(homogeneous=False, spacing=0) self.value = value self.metadata = metadata self.set_value = set_value @@ -40,16 +39,23 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.allowed_values = [] self.label_dict = {} if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": - self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_TRUE, - metomi.rose.TYPE_BOOLEAN_VALUE_FALSE] + self.allowed_values = [ + metomi.rose.TYPE_BOOLEAN_VALUE_TRUE, + metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, + ] else: - self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_TRUE, - metomi.rose.TYPE_LOGICAL_VALUE_FALSE] + self.allowed_values = [ + metomi.rose.TYPE_LOGICAL_VALUE_TRUE, + metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + ] self.label_dict = { - metomi.rose.TYPE_LOGICAL_VALUE_TRUE: - metomi.rose.TYPE_LOGICAL_TRUE_TITLE, - metomi.rose.TYPE_LOGICAL_VALUE_FALSE: - metomi.rose.TYPE_LOGICAL_FALSE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: ( + metomi.rose.TYPE_LOGICAL_TRUE_TITLE + ), + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: ( + metomi.rose.TYPE_LOGICAL_FALSE_TITLE + ), + } for k, item in enumerate(self.allowed_values): if item in self.label_dict: @@ -58,22 +64,22 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): button_label = str(item) self.label_dict.update({item: button_label}) if k == 0: - radio_button = Gtk.RadioButton(group=None, - label=button_label, - use_underline=False) + radio_button = Gtk.RadioButton( + group=None, label=button_label, use_underline=False + ) radio_button.real_value = item else: - radio_button = Gtk.RadioButton(group=radio_button, - label=button_label, - use_underline=False) + radio_button = Gtk.RadioButton( + group=radio_button, label=button_label, use_underline=False + ) radio_button.real_value = item radio_button.set_active(False) if item == str(value): radio_button.set_active(True) - radio_button.connect('toggled', self.setter) + radio_button.connect("toggled", self.setter) self.pack_start(radio_button, False, False, 10) radio_button.show() - radio_button.connect('focus-in-event', self.hook.trigger_scroll) + radio_button.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(radio_button) def setter(self, widget, variable): diff --git a/metomi/rose/config_editor/valuewidget/booltoggle.py b/metomi/rose/config_editor/valuewidget/booltoggle.py index 03b631f49..e6139d3bf 100644 --- a/metomi/rose/config_editor/valuewidget/booltoggle.py +++ b/metomi/rose/config_editor/valuewidget/booltoggle.py @@ -19,19 +19,20 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose class BoolToggleValueWidget(Gtk.Box): - """Produces a 'true' and 'false' labelled toggle button.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(BoolToggleValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(BoolToggleValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -39,30 +40,43 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.allowed_values = [] self.label_dict = {} if metadata.get(metomi.rose.META_PROP_TYPE) == "boolean": - self.allowed_values = [metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, - metomi.rose.TYPE_BOOLEAN_VALUE_TRUE] - self.label_dict = dict(list(zip(self.allowed_values, - self.allowed_values))) + self.allowed_values = [ + metomi.rose.TYPE_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_BOOLEAN_VALUE_TRUE, + ] + self.label_dict = dict( + list(zip(self.allowed_values, self.allowed_values)) + ) elif metadata.get(metomi.rose.META_PROP_TYPE) == "python_boolean": - self.allowed_values = [metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, - metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE] - self.label_dict = dict(list(zip(self.allowed_values, - self.allowed_values))) + self.allowed_values = [ + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_FALSE, + metomi.rose.TYPE_PYTHON_BOOLEAN_VALUE_TRUE, + ] + self.label_dict = dict( + list(zip(self.allowed_values, self.allowed_values)) + ) else: - self.allowed_values = [metomi.rose.TYPE_LOGICAL_VALUE_FALSE, - metomi.rose.TYPE_LOGICAL_VALUE_TRUE] + self.allowed_values = [ + metomi.rose.TYPE_LOGICAL_VALUE_FALSE, + metomi.rose.TYPE_LOGICAL_VALUE_TRUE, + ] self.label_dict = { - metomi.rose.TYPE_LOGICAL_VALUE_FALSE: - metomi.rose.TYPE_LOGICAL_FALSE_TITLE, - metomi.rose.TYPE_LOGICAL_VALUE_TRUE: - metomi.rose.TYPE_LOGICAL_TRUE_TITLE} + metomi.rose.TYPE_LOGICAL_VALUE_FALSE: ( + metomi.rose.TYPE_LOGICAL_FALSE_TITLE + ), + metomi.rose.TYPE_LOGICAL_VALUE_TRUE: ( + metomi.rose.TYPE_LOGICAL_TRUE_TITLE + ), + } - imgs = [Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, - Gtk.IconSize.MENU), - Gtk.Image.new_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU)] + imgs = [ + Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, Gtk.IconSize.MENU), + Gtk.Image.new_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU), + ] self.image_dict = dict(list(zip(self.allowed_values, imgs))) - bad_img = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, - Gtk.IconSize.MENU) + bad_img = Gtk.Image.new_from_stock( + Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU + ) self.button = Gtk.ToggleButton(label=self.value) if self.value in self.allowed_values: self.button.set_active(self.allowed_values.index(self.value)) @@ -71,11 +85,11 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): else: self.button.set_inconsistent(True) self.button.set_image(bad_img) - self.button.connect('toggled', self._switch_state_and_set) + self.button.connect("toggled", self._switch_state_and_set) self.button.show() self.pack_start(self.button, expand=False, fill=False, padding=0) self.grab_focus = lambda: self.hook.get_focus(self.button) - self.button.connect('focus-in-event', self.hook.trigger_scroll) + self.button.connect("focus-in-event", self.hook.trigger_scroll) def _switch_state_and_set(self, widget): state = self.allowed_values[int(widget.get_active())] diff --git a/metomi/rose/config_editor/valuewidget/character.py b/metomi/rose/config_editor/valuewidget/character.py index af28d8953..5cae01acd 100644 --- a/metomi/rose/config_editor/valuewidget/character.py +++ b/metomi/rose/config_editor/valuewidget/character.py @@ -19,38 +19,43 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk from metomi.rose import META_PROP_TYPE -import metomi.rose.config_editor.util class QuotedTextValueWidget(Gtk.Box): - """This class represents 'character' and 'quoted' types in an entry.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(QuotedTextValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(QuotedTextValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) # Importing here prevents cyclic imports import metomi.rose.macros.value + self.type = metadata.get(META_PROP_TYPE) checker = metomi.rose.macros.value.ValueChecker() if self.type == "character": self.type_checker = checker.check_character self.format_text_in = ( - metomi.rose.config_editor.util.text_for_character_widget) + metomi.rose.config_editor.util.text_for_character_widget + ) self.format_text_out = ( - metomi.rose.config_editor.util.text_from_character_widget) + metomi.rose.config_editor.util.text_from_character_widget + ) self.quote_char = "'" self.esc_quote_chars = "''" elif self.type == "quoted": self.type_checker = checker.check_quoted self.format_text_in = ( - metomi.rose.config_editor.util.text_for_quoted_widget) + metomi.rose.config_editor.util.text_for_quoted_widget + ) self.format_text_out = ( - metomi.rose.config_editor.util.text_from_quoted_widget) + metomi.rose.config_editor.util.text_from_quoted_widget + ) self.quote_char = '"' self.esc_quote_chars = '\\"' self.value = value @@ -60,17 +65,19 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entry = Gtk.Entry() self.in_error = not self.type_checker(self.value) self.set_entry_text() - self.entry.connect("button-release-event", - self._handle_middle_click_paste) + self.entry.connect( + "button-release-event", self._handle_middle_click_paste + ) self.entry.connect_after("paste-clipboard", self.setter) - self.entry.connect_after("key-release-event", - lambda e, v: self.setter(e)) - self.entry.connect_after("button-release-event", - lambda e, v: self.setter(e)) + self.entry.connect_after( + "key-release-event", lambda e, v: self.setter(e) + ) + self.entry.connect_after( + "button-release-event", lambda e, v: self.setter(e) + ) self.entry.show() - self.pack_start(self.entry, expand=True, fill=True, - padding=0) - self.entry.connect('focus-in-event', self.hook.trigger_scroll) + self.pack_start(self.entry, expand=True, fill=True, padding=0) + self.entry.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(self.entry) def set_entry_text(self): diff --git a/metomi/rose/config_editor/valuewidget/choice.py b/metomi/rose/config_editor/valuewidget/choice.py index 557e0f8c5..e195f1e0c 100644 --- a/metomi/rose/config_editor/valuewidget/choice.py +++ b/metomi/rose/config_editor/valuewidget/choice.py @@ -22,7 +22,8 @@ import shlex import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose.config_editor @@ -33,8 +34,8 @@ from functools import cmp_to_key -class ChoicesValueWidget(Gtk.Box): +class ChoicesValueWidget(Gtk.Box): """This represents a value as actual/available choices. Arguments are standard, except for the custom arg_str argument, @@ -79,29 +80,25 @@ class ChoicesValueWidget(Gtk.Box): OPTIONS = { "all_group": [ ["--all-group"], - {"action": "store", - "metavar": "CHOICE"}], + {"action": "store", "metavar": "CHOICE"}, + ], "choices": [ ["--choices"], - {"action": "append", - "default": None, - "metavar": "CHOICE"}], + {"action": "append", "default": None, "metavar": "CHOICE"}, + ], "editable": [ ["--editable"], - {"action": "store_true", - "default": False}], - "format": [ - ["--format"], - {"action": "store", - "metavar": "FORMAT"}], + {"action": "store_true", "default": False}, + ], + "format": [["--format"], {"action": "store", "metavar": "FORMAT"}], "guess_groups": [ ["--guess-groups"], - {"action": "store_true", - "default": False}]} + {"action": "store_true", "default": False}, + ], + } def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(ChoicesValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(ChoicesValueWidget, self).__init__(homogeneous=False, spacing=0) self.value = value self.metadata = metadata self.set_value = set_value @@ -127,7 +124,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self._listview = metomi.rose.gtk.choice.ChoicesListView( self._set_value_listview, self._get_value_values, - self._handle_search) + self._handle_search, + ) self._listview.show() list_frame = Gtk.Frame() list_frame.show() @@ -141,7 +139,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self._get_value_values, self._get_available_values, self._get_groups, - self._get_is_implicit) + self._get_is_implicit, + ) self._treeview.show() tree_frame = Gtk.Frame() tree_frame.show() @@ -151,10 +150,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): add_widget = self._get_add_widget() tree_vbox.pack_end(add_widget, expand=False, fill=False, padding=0) self.pack_start(tree_vbox, expand=True, fill=True) - self._listview.connect('focus-in-event', - self.hook.trigger_scroll) - self._treeview.connect('focus-in-event', - self.hook.trigger_scroll) + self._listview.connect("focus-in-event", self.hook.trigger_scroll) + self._treeview.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(self._listview) def _handle_search(self, name): @@ -166,8 +163,11 @@ def _get_add_widget(self): add_entry.connect("changed", self._handle_combo_choice) add_entry.get_child().connect( "key-press-event", - lambda w, e: self._handle_text_choice(add_entry, e)) - add_entry.set_tooltip_text(metomi.rose.config_editor.CHOICE_TIP_ENTER_CUSTOM) + lambda w, e: self._handle_text_choice(add_entry, e), + ) + add_entry.set_tooltip_text( + metomi.rose.config_editor.CHOICE_TIP_ENTER_CUSTOM + ) add_entry.show() self._set_available_hints(add_entry) add_hbox.pack_end(add_entry, expand=True, fill=True, padding=0) @@ -191,8 +191,9 @@ def _handle_combo_choice(self, comboboxentry): def _handle_text_choice(self, comboboxentry, event): if Gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: - self._add_custom_choice(comboboxentry, - comboboxentry.get_child().get_text()) + self._add_custom_choice( + comboboxentry, comboboxentry.get_child().get_text() + ) return False def _add_custom_choice(self, comboboxentry, new_name): @@ -200,8 +201,9 @@ def _add_custom_choice(self, comboboxentry, new_name): if not new_name: text = metomi.rose.config_editor.ERROR_BAD_NAME.format("''") title = metomi.rose.config_editor.DIALOG_TITLE_ERROR - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, - text, title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_ERROR, text, title + ) return False new_values = self._get_value_values() + [entry.get_text()] entry.set_text("") @@ -241,7 +243,11 @@ def _get_groups(self, name, names): if not self.should_guess_groups or not self.should_show_kinship: return default_groups ok_groups = [n for n in names if set(n).issubset(name) and n != name] - ok_groups.sort(key=cmp_to_key(lambda x, y: set(x).issubset(y) - set(y).issubset(x))) + ok_groups.sort( + key=cmp_to_key( + lambda x, y: set(x).issubset(y) - set(y).issubset(x) + ) + ) for group in default_groups: if group in ok_groups: ok_groups.remove(group) diff --git a/metomi/rose/config_editor/valuewidget/combobox.py b/metomi/rose/config_editor/valuewidget/combobox.py index 0543af0a0..f3fac5a59 100644 --- a/metomi/rose/config_editor/valuewidget/combobox.py +++ b/metomi/rose/config_editor/valuewidget/combobox.py @@ -19,14 +19,14 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor class ComboBoxValueWidget(Gtk.Box): - """This is a class to add a combo box for a set of variable values. It needs to have some allowed values set in the variable metadata. @@ -36,8 +36,7 @@ class ComboBoxValueWidget(Gtk.Box): FRAC_X_ALIGN = 0.9 def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(ComboBoxValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(ComboBoxValueWidget, self).__init__(homogeneous=False, spacing=0) self.value = value self.metadata = metadata self.set_value = set_value @@ -47,7 +46,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): cell = Gtk.CellRendererText() cell.xalign = self.FRAC_X_ALIGN comboboxentry.pack_start(cell, True) - comboboxentry.add_attribute(cell, 'text', 0) + comboboxentry.add_attribute(cell, "text", 0) var_values = self.metadata[metomi.rose.META_PROP_VALUES] var_titles = self.metadata.get(metomi.rose.META_PROP_VALUE_TITLES) @@ -58,17 +57,18 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): liststore.append([entry]) comboboxentry.set_model(liststore) if self.value in var_values: - index = self.metadata['values'].index(self.value) + index = self.metadata["values"].index(self.value) comboboxentry.set_active(index) - comboboxentry.connect('changed', self.setter) - comboboxentry.connect('button-press-event', - lambda b: comboboxentry.grab_focus()) + comboboxentry.connect("changed", self.setter) + comboboxentry.connect( + "button-press-event", lambda b: comboboxentry.grab_focus() + ) comboboxentry.show() self.pack_start(comboboxentry, False, False, 0) self.grab_focus = lambda: self.hook.get_focus(comboboxentry) - self.set_contains_error = (lambda e: - comboboxentry.modify_bg(Gtk.StateType.NORMAL, - self.bad_colour)) + self.set_contains_error = lambda e: comboboxentry.modify_bg( + Gtk.StateType.NORMAL, self.bad_colour + ) def setter(self, widget): index = widget.get_active() diff --git a/metomi/rose/config_editor/valuewidget/files.py b/metomi/rose/config_editor/valuewidget/files.py index 591acb763..fc84ce5bc 100644 --- a/metomi/rose/config_editor/valuewidget/files.py +++ b/metomi/rose/config_editor/valuewidget/files.py @@ -21,6 +21,7 @@ import os import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -30,12 +31,12 @@ class FileChooserValueWidget(Gtk.Box): - """This class displays a path, with an open dialog to define a new one.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(FileChooserValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(FileChooserValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -46,7 +47,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): stock_id=Gtk.STOCK_OPEN, size=Gtk.IconSize.MENU, as_tool=False, - tip_text="Browse for a filename") + tip_text="Browse for a filename", + ) self.open_button.show() self.open_button.connect("clicked", self.run_and_destroy) self.pack_end(self.open_button, expand=False, fill=False, padding=0) @@ -57,22 +59,27 @@ def generate_entry(self): self.entry.set_text(self.value) self.entry.show() self.entry.connect("changed", self.setter) - self.entry.connect("focus-in-event", - self.hook.trigger_scroll) + self.entry.connect("focus-in-event", self.hook.trigger_scroll) self.pack_start(self.entry, True, True, 0) self.grab_focus = lambda: self.hook.get_focus(self.entry) def run_and_destroy(self, *args): file_chooser_widget = Gtk.FileChooserDialog( - buttons=(Gtk.STOCK_CANCEL, - Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, - Gtk.ResponseType.ACCEPT)) + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ) + ) if os.path.exists(os.path.dirname(self.value)): file_chooser_widget.set_filename(self.value) response = file_chooser_widget.run() - if response in [Gtk.ResponseType.ACCEPT, Gtk.ResponseType.OK, - Gtk.ResponseType.YES]: + if response in [ + Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.OK, + Gtk.ResponseType.YES, + ]: self.entry.set_text(file_chooser_widget.get_filename()) file_chooser_widget.destroy() return False @@ -82,10 +89,12 @@ def generate_editor_launcher(self): stock_id=Gtk.STOCK_DND, size=Gtk.IconSize.MENU, as_tool=False, - tip_text="Edit the file") + tip_text="Edit the file", + ) self.edit_button.connect( "clicked", - lambda b: metomi.rose.external.launch_geditor(self.value)) + lambda b: metomi.rose.external.launch_geditor(self.value), + ) self.pack_end(self.edit_button, expand=False, fill=False, padding=0) def setter(self, widget): @@ -96,14 +105,14 @@ def setter(self, widget): class FileEditorValueWidget(Gtk.Box): - """This class creates a button that launches an editor for a file path.""" FILE_PROTOCOL = "file://{0}" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(FileEditorValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(FileEditorValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -116,10 +125,15 @@ def generate_editor_launcher(self): stock_id=Gtk.STOCK_DND, size=Gtk.IconSize.MENU, as_tool=False, - tip_text="Edit the file") + tip_text="Edit the file", + ) self.edit_button.connect("clicked", self.on_click) - self.pack_start(self.edit_button, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + self.pack_start( + self.edit_button, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) def retrieve_path(self): root = self.metadata[metomi.rose.config_editor.META_PROP_INTERNAL] diff --git a/metomi/rose/config_editor/valuewidget/format.py b/metomi/rose/config_editor/valuewidget/format.py index 361e62f3e..772e65833 100644 --- a/metomi/rose/config_editor/valuewidget/format.py +++ b/metomi/rose/config_editor/valuewidget/format.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -28,23 +29,23 @@ class FormatsChooserValueWidget(Gtk.Box): - """This class allows the addition of section names to a variable value.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(FormatsChooserValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(FormatsChooserValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value self.hook = hook - if 'values_getter' in self.metadata: + if "values_getter" in self.metadata: meta = self.metadata - self.values_getter = meta['values_getter'] + self.values_getter = meta["values_getter"] else: - self.values_getter = lambda: meta.get('values', []) - num_entries = len(value.split(' ')) + self.values_getter = lambda: meta.get("values", []) + num_entries = len(value.split(" ")) self.entry_table = Gtk.Table(rows=num_entries + 1, columns=1) self.entry_table.show() self.entries = [] @@ -60,17 +61,23 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): image_event.add(image) image_event.show() self.add_box.pack_start( - image_event, expand=False, fill=False, padding=5) + image_event, expand=False, fill=False, padding=5 + ) self.data_chooser = Gtk.ComboBoxText() - self.data_chooser.connect('focus-in-event', - lambda d, e: self.load_data_chooser()) - self.data_chooser.connect('changed', lambda d: self.add_new_section()) + self.data_chooser.connect( + "focus-in-event", lambda d, e: self.load_data_chooser() + ) + self.data_chooser.connect("changed", lambda d: self.add_new_section()) self.data_chooser.show() - image_event.connect('button-press-event', - lambda i, w: (self.load_data_chooser() and - self.data_chooser.popup())) - self.add_box.pack_start(self.data_chooser, expand=False, fill=False, - padding=0) + image_event.connect( + "button-press-event", + lambda i, w: ( + self.load_data_chooser() and self.data_chooser.popup() + ), + ) + self.add_box.pack_start( + self.data_chooser, expand=False, fill=False, padding=0 + ) self.load_data_chooser() self.populate_table() @@ -80,8 +87,8 @@ def get_entry(self, format_name): """Create an entry box for a format name.""" entry = Gtk.Entry() entry.set_text(format_name) - entry.connect('focus-in-event', self.hook.trigger_scroll) - entry.connect('changed', self.entry_change_handler) + entry.connect("focus-in-event", self.hook.trigger_scroll) + entry.connect("changed", self.entry_change_handler) entry.show() return entry @@ -93,7 +100,8 @@ def populate_table(self): self.entry_table.resize(rows=len(self.entries) + 1, columns=1) for i, widget in enumerate(self.entries + [self.add_box]): self.entry_table.attach( - widget, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) + widget, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL + ) self.grab_focus = lambda: self.hook.get_focus(self.entries[-1]) def add_new_section(self): @@ -114,9 +122,9 @@ def get_active_text(self, combobox): def entry_change_handler(self, entry): position = entry.get_position() - if entry.get_text() == '' and len(self.entries) > 1: + if entry.get_text() == "" and len(self.entries) > 1: self.entries.remove(entry) - new_value = ' '.join([e.get_text() for e in self.entries]) + new_value = " ".join([e.get_text() for e in self.entries]) self.value = new_value self.set_value(new_value) self.populate_table() diff --git a/metomi/rose/config_editor/valuewidget/intspin.py b/metomi/rose/config_editor/valuewidget/intspin.py index 6f896e9d7..a777bf86f 100644 --- a/metomi/rose/config_editor/valuewidget/intspin.py +++ b/metomi/rose/config_editor/valuewidget/intspin.py @@ -21,21 +21,22 @@ import sys import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor class IntSpinButtonValueWidget(Gtk.Box): - """This is a class to represent an integer with a spin button.""" - WARNING_MESSAGE = 'Warning:\n variable value: {0}\n widget value: {1}' + WARNING_MESSAGE = "Warning:\n variable value: {0}\n widget value: {1}" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(IntSpinButtonValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(IntSpinButtonValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -54,11 +55,11 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): if value_ok: entry = self.make_spinner(int_value) - signal = 'changed' + signal = "changed" else: entry = Gtk.Entry() entry.set_text(self.value) - signal = 'activate' + signal = "activate" self.change_id = entry.connect(signal, self.setter) @@ -70,24 +71,24 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.warning_img = Gtk.Image() if not value_ok: self.warning_img = Gtk.Image() - self.warning_img.set_from_stock(Gtk.STOCK_DIALOG_WARNING, - Gtk.IconSize.MENU) + self.warning_img.set_from_stock( + Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU + ) self.warning_img.set_tooltip_text( - metomi.rose.config_editor.WARNING_INTEGER_OUT_OF_BOUNDS) + metomi.rose.config_editor.WARNING_INTEGER_OUT_OF_BOUNDS + ) self.warning_img.show() self.pack_start(self.warning_img, False, False, 0) self.grab_focus = lambda: self.hook.get_focus(entry) def make_spinner(self, int_value): - my_adj = Gtk.Adjustment(value=int_value, - upper=self.upper, - lower=self.lower, - step_incr=1) + my_adj = Gtk.Adjustment( + value=int_value, upper=self.upper, lower=self.lower, step_incr=1 + ) spin_button = Gtk.SpinButton(adjustment=my_adj, digits=0) - spin_button.connect('focus-in-event', - self.hook.trigger_scroll) + spin_button.connect("focus-in-event", self.hook.trigger_scroll) spin_button.set_numeric(True) diff --git a/metomi/rose/config_editor/valuewidget/meta.py b/metomi/rose/config_editor/valuewidget/meta.py index d33b5426a..a3e76798e 100644 --- a/metomi/rose/config_editor/valuewidget/meta.py +++ b/metomi/rose/config_editor/valuewidget/meta.py @@ -19,12 +19,12 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk class MetaValueWidget(Gtk.Box): - """This class generates an entry and button for a metadata flag value.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): @@ -35,8 +35,9 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.hook = hook self.entry = Gtk.Entry() self.entry.set_text(self.value) - self.entry.connect("button-release-event", - self._handle_middle_click_paste) + self.entry.connect( + "button-release-event", self._handle_middle_click_paste + ) self.entry.connect_after("paste-clipboard", self._check_diff) self.entry.connect_after("key-release-event", self._check_diff) self.entry.connect_after("button-release-event", self._check_diff) @@ -47,12 +48,9 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.button.connect("clicked", self._setter) self.button.set_sensitive(False) self.button.show() - self.pack_start(self.entry, expand=True, fill=True, - padding=0) - self.pack_start(self.button, expand=False, fill=False, - padding=0) - self.entry.connect('focus-in-event', - self.hook.trigger_scroll) + self.pack_start(self.entry, expand=True, fill=True, padding=0) + self.pack_start(self.button, expand=False, fill=False, padding=0) + self.entry.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(self.entry) def _check_diff(self, *args): diff --git a/metomi/rose/config_editor/valuewidget/radiobuttons.py b/metomi/rose/config_editor/valuewidget/radiobuttons.py index 9bcd9240b..fc98be03d 100644 --- a/metomi/rose/config_editor/valuewidget/radiobuttons.py +++ b/metomi/rose/config_editor/valuewidget/radiobuttons.py @@ -19,19 +19,20 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor class RadioButtonsValueWidget(Gtk.Box): - """This is a class to represent a value as radio buttons.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(RadioButtonsValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(RadioButtonsValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -50,31 +51,30 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): if var_titles is not None and var_titles[k]: button_label = var_titles[k] if k == 0: - radio_button = Gtk.RadioButton(group=None, - label=button_label, - use_underline=False) + radio_button = Gtk.RadioButton( + group=None, label=button_label, use_underline=False + ) radio_button.real_value = item else: - radio_button = Gtk.RadioButton(group=radio_button, - label=button_label, - use_underline=False) + radio_button = Gtk.RadioButton( + group=radio_button, label=button_label, use_underline=False + ) radio_button.real_value = item if var_titles is not None and var_titles[k]: radio_button.set_tooltip_text("(" + item + ")") radio_button.set_active(False) if item == self.value: radio_button.set_active(True) - radio_button.connect('toggled', self.setter) - radio_button.connect('button-press-event', self.setter) - radio_button.connect('activate', self.setter) + radio_button.connect("toggled", self.setter) + radio_button.connect("button-press-event", self.setter) + radio_button.connect("activate", self.setter) if var_titles: vbox.pack_start(radio_button, False, False, 2) else: self.pack_start(radio_button, False, False, 10) radio_button.show() - radio_button.connect('focus-in-event', - self.hook.trigger_scroll) + radio_button.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(radio_button) if len(var_values) == 1 and self.value == var_values[0]: diff --git a/metomi/rose/config_editor/valuewidget/source.py b/metomi/rose/config_editor/valuewidget/source.py index 5bdc62d16..afa0c595c 100644 --- a/metomi/rose/config_editor/valuewidget/source.py +++ b/metomi/rose/config_editor/valuewidget/source.py @@ -22,7 +22,8 @@ import shlex import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -32,8 +33,8 @@ from functools import cmp_to_key -class SourceValueWidget(Gtk.Box): +class SourceValueWidget(Gtk.Box): """This class generates a special widget for the file source variable. It cheats by passing in a special VariableOperations instance as @@ -49,7 +50,9 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.set_value = set_value self.hook = hook self.var_ops = arg_str - formats = [f for f in metomi.rose.formats.__dict__ if not f.startswith('__')] + formats = [ + f for f in metomi.rose.formats.__dict__ if not f.startswith("__") + ] self.formats = formats self.formats_ok = None self._ok_content_sections = set([None]) @@ -59,25 +62,30 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.show() formats_check_button = Gtk.CheckButton( - metomi.rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL) + metomi.rose.config_editor.FILE_CONTENT_PANEL_FORMAT_LABEL + ) formats_check_button.set_active(not self.formats_ok) formats_check_button.connect("toggled", self._toggle_formats) formats_check_button.show() formats_check_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) formats_check_hbox.show() - formats_check_hbox.pack_end(formats_check_button, expand=False, - fill=False, padding=0) - vbox.pack_start(formats_check_hbox, expand=False, fill=False, padding=0) + formats_check_hbox.pack_end( + formats_check_button, expand=False, fill=False, padding=0 + ) + vbox.pack_start( + formats_check_hbox, expand=False, fill=False, padding=0 + ) treeviews_hbox = Gtk.HPaned() treeviews_hbox.show() self._listview = metomi.rose.gtk.choice.ChoicesListView( self._set_listview, self._get_included_sources, self._handle_search, - get_custom_menu_items=self._get_custom_menu_items + get_custom_menu_items=self._get_custom_menu_items, ) self._listview.set_tooltip_text( - metomi.rose.config_editor.FILE_CONTENT_PANEL_TIP) + metomi.rose.config_editor.FILE_CONTENT_PANEL_TIP + ) frame = Gtk.Frame() frame.show() frame.add(self._listview) @@ -96,10 +104,13 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): adder_hook = metomi.rose.config_editor.valuewidget.ValueWidgetHook() self._adder = ( metomi.rose.config_editor.valuewidget.files.FileChooserValueWidget( - adder_value, adder_metadata, adder_set_value, adder_hook)) + adder_value, adder_metadata, adder_set_value, adder_hook + ) + ) self._adder.entry.connect("activate", self._add_file_source) self._adder.entry.set_tooltip_text( - metomi.rose.config_editor.TIP_VALUE_ADD_URI) + metomi.rose.config_editor.TIP_VALUE_ADD_URI + ) self._adder.show() treeviews_hbox.add1(value_vbox) treeviews_hbox.add2(self._available_frame) @@ -127,10 +138,11 @@ def _generate_available_treeview(self): self._get_available_sections, self._get_groups, title=metomi.rose.config_editor.FILE_CONTENT_PANEL_TITLE, - get_is_included=self._get_section_is_included + get_is_included=self._get_section_is_included, ) self._available_treeview.set_tooltip_text( - metomi.rose.config_editor.FILE_CONTENT_PANEL_OPT_TIP) + metomi.rose.config_editor.FILE_CONTENT_PANEL_OPT_TIP + ) self._available_frame.show() if not self.formats_ok: self._available_frame.hide() @@ -139,14 +151,19 @@ def _generate_available_treeview(self): def _get_custom_menu_items(self): """Return some custom menuitems for use in the list view.""" menuitem_box = Gtk.Box() - menuitem_icon = Gtk.Image.new_from_icon_name("dialog-question", Gtk.IconSize.MENU) - menuitem_label = Gtk.Label(label=metomi.rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL) + menuitem_icon = Gtk.Image.new_from_icon_name( + "dialog-question", Gtk.IconSize.MENU + ) + menuitem_label = Gtk.Label( + label=metomi.rose.config_editor.FILE_CONTENT_PANEL_MENU_OPTIONAL + ) menuitem = Gtk.MenuItem() menuitem_box.pack_start(menuitem_icon, False, False, 0) menuitem_box.pack_start(menuitem_label, False, False, 0) Gtk.Container.add(menuitem, menuitem_box) menuitem.connect( - "button-press-event", self._toggle_menu_optional_status) + "button-press-event", self._toggle_menu_optional_status + ) menuitem.show() return [menuitem] @@ -159,8 +176,9 @@ def _get_section_is_included(self, section, included_sections=None): if included_sections is None: included_sections = self._get_included_sources() for i, included_section in enumerate(included_sections): - if (included_section.startswith("(") and - included_section.endswith(")")): + if included_section.startswith("(") and included_section.endswith( + ")" + ): included_sections[i] = included_section[1:-1] return section in included_sections @@ -181,7 +199,9 @@ def _get_available_sections(self): if section_all not in ok_content_sections: ok_content_sections.append(section_all) ok_content_sections.append(section) - ok_content_sections.sort(key=cmp_to_key(metomi.rose.config.sort_settings)) + ok_content_sections.sort( + key=cmp_to_key(metomi.rose.config.sort_settings) + ) ok_content_sections.sort(key=cmp_to_key(self._sort_settings_duplicate)) return ok_content_sections @@ -247,8 +267,9 @@ def _toggle_menu_optional_status(self, menuitem, event): iter_ = menuitem._listview_iter model = menuitem._listview_model old_section_value = model.get_value(iter_, 0) - if (old_section_value.startswith("(") and - old_section_value.endswith(")")): + if old_section_value.startswith("(") and old_section_value.endswith( + ")" + ): section_value = old_section_value[1:-1] else: section_value = "(" + old_section_value + ")" diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 8d9a3f67a..4e38f874f 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -19,7 +19,8 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor @@ -28,11 +29,11 @@ import metomi.rose.gtk.util ENV_COLOUR = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV) + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_VAL_ENV +) class RawValueWidget(Gtk.Box): - """This class generates a basic entry widget for an unformatted value.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): @@ -44,19 +45,23 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entry = Gtk.Entry() if metomi.rose.env.contains_env_var(self.value): self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) - self.entry.set_tooltip_text(metomi.rose.config_editor.VAR_WIDGET_ENV_INFO) + self.entry.set_tooltip_text( + metomi.rose.config_editor.VAR_WIDGET_ENV_INFO + ) self.entry.set_text(self.value) - self.entry.connect("button-release-event", - self._handle_middle_click_paste) + self.entry.connect( + "button-release-event", self._handle_middle_click_paste + ) self.entry.connect_after("paste-clipboard", self.setter) - self.entry.connect_after("key-release-event", - lambda e, v: self.setter(e)) - self.entry.connect_after("button-release-event", - lambda e, v: self.setter(e)) + self.entry.connect_after( + "key-release-event", lambda e, v: self.setter(e) + ) + self.entry.connect_after( + "button-release-event", lambda e, v: self.setter(e) + ) self.entry.show() self.pack_start(self.entry, expand=True, fill=True, padding=0) - self.entry.connect('focus-in-event', - self.hook.trigger_scroll) + self.entry.connect("focus-in-event", self.hook.trigger_scroll) self.grab_focus = lambda: self.hook.get_focus(self.entry) def setter(self, widget, *args): @@ -67,7 +72,9 @@ def setter(self, widget, *args): self.set_value(self.value) if metomi.rose.env.contains_env_var(self.value): self.entry.modify_text(Gtk.StateType.NORMAL, ENV_COLOUR) - self.entry.set_tooltip_text(metomi.rose.config_editor.VAR_WIDGET_ENV_INFO) + self.entry.set_tooltip_text( + metomi.rose.config_editor.VAR_WIDGET_ENV_INFO + ) else: self.entry.set_tooltip_text(None) return False @@ -88,12 +95,12 @@ def _handle_middle_click_paste(self, widget, event): class TextMultilineValueWidget(Gtk.Box): - """This class displays text with multiple lines.""" def __init__(self, value, metadata, set_value, hook, arg_str=None): - super(TextMultilineValueWidget, self).__init__(homogeneous=False, - spacing=0) + super(TextMultilineValueWidget, self).__init__( + homogeneous=False, spacing=0 + ) self.value = value self.metadata = metadata self.set_value = set_value @@ -105,7 +112,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entry.set_wrap_mode(Gtk.WrapMode.WORD) self.entry.set_left_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) self.entry.set_right_margin(metomi.rose.config_editor.SPACING_SUB_PAGE) - self.entry.connect('focus-in-event', self.hook.trigger_scroll) + self.entry.connect("focus-in-event", self.hook.trigger_scroll) self.entry.show() viewport = Gtk.Viewport() @@ -113,7 +120,7 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): viewport.show() self.grab_focus = lambda: self.hook.get_focus(self.entry) - self.entrybuffer.connect('changed', self.setter) + self.entrybuffer.connect("changed", self.setter) self.pack_start(viewport, expand=True, fill=True, padding=0) def get_focus_index(self): @@ -130,8 +137,7 @@ def set_focus_index(self, focus_index=None): self.entrybuffer.place_cursor(iter_) def setter(self, widget): - text = widget.get_text(widget.get_start_iter(), - widget.get_end_iter()) + text = widget.get_text(widget.get_start_iter(), widget.get_end_iter()) if text != self.value: self.value = text self.set_value(self.value) diff --git a/metomi/rose/config_editor/valuewidget/valuehints.py b/metomi/rose/config_editor/valuewidget/valuehints.py index 6b95a3cb3..62991732f 100644 --- a/metomi/rose/config_editor/valuewidget/valuehints.py +++ b/metomi/rose/config_editor/valuewidget/valuehints.py @@ -20,7 +20,8 @@ from gi.repository import GObject import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor.util @@ -44,10 +45,8 @@ def __init__(self, value, metadata, set_value, hook, arg_str=None): self.entry.connect_after("button-release-event", self._setter) self.entry.show() GObject.idle_add(self._set_completion, self.metadata) - self.pack_start(self.entry, expand=True, fill=True, - padding=0) - self.entry.connect('focus-in-event', - hook.trigger_scroll) + self.pack_start(self.entry, expand=True, fill=True, padding=0) + self.entry.connect("focus-in-event", hook.trigger_scroll) self.grab_focus = lambda: hook.get_focus(self.entry) def _setter(self, *args): @@ -67,7 +66,7 @@ def set_focus_index(self, focus_index=None): self.entry.set_position(focus_index) def _set_completion(self, metadata): - """ Return a predictive text model for value-hints.""" + """Return a predictive text model for value-hints.""" completion = Gtk.EntryCompletion() model = Gtk.ListStore(str) var_hints = metadata.get(metomi.rose.META_PROP_VALUE_HINTS) diff --git a/metomi/rose/config_editor/variable.py b/metomi/rose/config_editor/variable.py index 52af7fb6a..9c2394103 100644 --- a/metomi/rose/config_editor/variable.py +++ b/metomi/rose/config_editor/variable.py @@ -23,7 +23,8 @@ import re import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config_editor.keywidget @@ -39,7 +40,6 @@ class VariableWidget(object): - """This class generates a set of widgets representing the variable. The set of widgets generated depends on the variable metadata, if any. @@ -48,8 +48,14 @@ class VariableWidget(object): """ - def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, - hide_keywidget_subtext=False): + def __init__( + self, + variable, + var_ops, + is_ghost=False, + show_modes=None, + hide_keywidget_subtext=False, + ): self.variable = variable self.key = variable.name self.value = variable.value @@ -60,13 +66,15 @@ def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, show_modes = {} self.show_modes = show_modes self.bad_colour = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR + ) self.hidden_colour = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_IRRELEVANT) + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_IRRELEVANT + ) self.keywidget = self.get_keywidget(variable, show_modes) self.generate_valuewidget(variable) self.is_inconsistent = False - if 'type' in variable.error: + if "type" in variable.error: self._set_inconsistent(self.valuewidget, variable) self.errors = list(variable.error.keys()) self.menuwidget = self.get_menuwidget(variable) @@ -76,7 +84,7 @@ def __init__(self, variable, var_ops, is_ghost=False, show_modes=None, self.force_signal_ids = [] self.is_modified = False for child_widget in self.get_children(): - setattr(child_widget, 'get_parent', lambda: self) + setattr(child_widget, "get_parent", lambda: self) self.trigger_ignored = lambda v, b: b self.get_parent = lambda: None self.is_ignored = False @@ -90,8 +98,11 @@ def get_keywidget(self, variable, show_modes): """ widget = metomi.rose.config_editor.keywidget.KeyWidget( - variable, self.var_ops, self.launch_help, self.update_status, - show_modes + variable, + self.var_ops, + self.launch_help, + self.update_status, + show_modes, ) widget.show() return widget @@ -101,24 +112,42 @@ def generate_labelwidget(self): self.labelwidget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.labelwidget.show() self.labelwidget.set_ignored = self.keywidget.set_ignored - menu_offset = self.menuwidget.get_preferred_size().natural_size.height / 2 + menu_offset = ( + self.menuwidget.get_preferred_size().natural_size.height / 2 + ) key_offset = self.keywidget.get_centre_height() / 2 menu_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - menu_vbox.pack_start(self.menuwidget, expand=False, fill=False, - padding=max([(key_offset - menu_offset), 0])) + menu_vbox.pack_start( + self.menuwidget, + expand=False, + fill=False, + padding=max([(key_offset - menu_offset), 0]), + ) menu_vbox.show() key_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - key_vbox.pack_start(self.keywidget, expand=False, fill=False, - padding=max([(menu_offset - key_offset) / 2, 0])) + key_vbox.pack_start( + self.keywidget, + expand=False, + fill=False, + padding=max([(menu_offset - key_offset) / 2, 0]), + ) key_vbox.show() label_content_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - label_content_hbox.pack_start(menu_vbox, expand=False, fill=False, padding=0) - label_content_hbox.pack_start(key_vbox, expand=False, fill=False, padding=0) + label_content_hbox.pack_start( + menu_vbox, expand=False, fill=False, padding=0 + ) + label_content_hbox.pack_start( + key_vbox, expand=False, fill=False, padding=0 + ) label_content_hbox.show() event_box = Gtk.EventBox() event_box.show() - self.labelwidget.pack_start(label_content_hbox, expand=True, fill=True, padding=0) - self.labelwidget.pack_start(event_box, expand=True, fill=True, padding=0) + self.labelwidget.pack_start( + label_content_hbox, expand=True, fill=True, padding=0 + ) + self.labelwidget.pack_start( + event_box, expand=True, fill=True, padding=0 + ) def generate_contentwidget(self): """Create the content widget, a vbox-packed valuewidget.""" @@ -127,36 +156,47 @@ def generate_contentwidget(self): content_event_box = Gtk.EventBox() content_event_box.show() self.contentwidget.pack_start( - self.valuewidget, expand=False, fill=False, padding=0) + self.valuewidget, expand=False, fill=False, padding=0 + ) self.contentwidget.pack_start( - content_event_box, expand=True, fill=True, padding=0) + content_event_box, expand=True, fill=True, padding=0 + ) def _valuewidget_set_value(self, value): # This is called by a valuewidget to change the variable value. self.var_ops.set_var_value(self.variable, value) self.update_status() - def generate_valuewidget(self, variable, override_custom=False, - use_this_valuewidget=None): + def generate_valuewidget( + self, variable, override_custom=False, use_this_valuewidget=None + ): """Creates the valuewidget attribute, based on value and metadata.""" custom_arg = None - if (variable.metadata.get("type") == - metomi.rose.config_editor.FILE_TYPE_NORMAL): - use_this_valuewidget = (metomi.rose.config_editor. - valuewidget.source.SourceValueWidget) + if ( + variable.metadata.get("type") + == metomi.rose.config_editor.FILE_TYPE_NORMAL + ): + use_this_valuewidget = ( + metomi.rose.config_editor.valuewidget.source.SourceValueWidget + ) custom_arg = self.var_ops set_value = self._valuewidget_set_value hook_object = metomi.rose.config_editor.valuewidget.ValueWidgetHook( - metomi.rose.config_editor.false_function) + metomi.rose.config_editor.false_function + ) metadata = copy.deepcopy(variable.metadata) if use_this_valuewidget is not None: - self.valuewidget = use_this_valuewidget(variable.value, - metadata, - set_value, - hook_object, - arg_str=custom_arg) - elif (metomi.rose.config_editor.META_PROP_WIDGET in self.meta and - not override_custom): + self.valuewidget = use_this_valuewidget( + variable.value, + metadata, + set_value, + hook_object, + arg_str=custom_arg, + ) + elif ( + metomi.rose.config_editor.META_PROP_WIDGET in self.meta + and not override_custom + ): w_val = self.meta[metomi.rose.config_editor.META_PROP_WIDGET] info = w_val.split(None, 1) if len(info) > 1: @@ -165,48 +205,56 @@ def generate_valuewidget(self, variable, override_custom=False, widget_path, custom_arg = info[0], None files = self.var_ops.get_ns_metadata_files(metadata["full_ns"]) error_handler = lambda e: self.handle_bad_valuewidget( - str(e), variable, set_value) - widget = metomi.rose.resource.import_object(widget_path, - files, - error_handler) + str(e), variable, set_value + ) + widget = metomi.rose.resource.import_object( + widget_path, files, error_handler + ) if widget is None: - text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format(w_val) + text = metomi.rose.config_editor.ERROR_IMPORT_CLASS.format( + w_val + ) self.handle_bad_valuewidget(text, variable, set_value) try: - self.valuewidget = widget(variable.value, - metadata, - set_value, - hook_object, - custom_arg) + self.valuewidget = widget( + variable.value, + metadata, + set_value, + hook_object, + custom_arg, + ) except Exception as exc: self.handle_bad_valuewidget(str(exc), variable, set_value) else: widget_maker = metomi.rose.config_editor.valuewidget.chooser( - variable.value, variable.metadata, - variable.error) - self.valuewidget = widget_maker(variable.value, - metadata, set_value, - hook_object, custom_arg) + variable.value, variable.metadata, variable.error + ) + self.valuewidget = widget_maker( + variable.value, metadata, set_value, hook_object, custom_arg + ) for child in self.valuewidget.get_children(): - child.connect('focus-in-event', self.handle_focus_in) - child.connect('focus-out-event', self.handle_focus_out) - if hasattr(child, 'get_children'): + child.connect("focus-in-event", self.handle_focus_in) + child.connect("focus-out-event", self.handle_focus_out) + if hasattr(child, "get_children"): for grandchild in child.get_children(): - grandchild.connect('focus-in-event', self.handle_focus_in) - grandchild.connect('focus-out-event', - self.handle_focus_out) + grandchild.connect("focus-in-event", self.handle_focus_in) + grandchild.connect( + "focus-out-event", self.handle_focus_out + ) self.valuewidget.show() def handle_bad_valuewidget(self, error_info, variable, set_value): """Handle a bad custom valuewidget import.""" text = metomi.rose.config_editor.ERROR_IMPORT_WIDGET.format(error_info) metomi.rose.reporter.Reporter()( - metomi.rose.config_editor.util.ImportWidgetError(text)) + metomi.rose.config_editor.util.ImportWidgetError(text) + ) self.generate_valuewidget(variable, override_custom=True) def handle_focus_in(self, widget, event): new_colour = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED) + metomi.rose.config_editor.COLOUR_VALUEWIDGET_BASE_SELECTED + ) widget.modify_base(Gtk.StateType.NORMAL, new_colour) def handle_focus_out(self, widget, event): @@ -217,51 +265,68 @@ def get_menuwidget(self, variable, menuclass=None): """Create the menuwidget attribute, an option menu button.""" if menuclass is None: menuclass = metomi.rose.config_editor.menuwidget.MenuWidget - menuwidget = menuclass(variable, - self.var_ops, - lambda: self.remove_from(self.get_parent()), - self.update_status, - self.launch_help) + menuwidget = menuclass( + variable, + self.var_ops, + lambda: self.remove_from(self.get_parent()), + self.update_status, + self.launch_help, + ) menuwidget.show() return menuwidget - def insert_into(self, container, x_info=None, y_info=None, - no_menuwidget=False): + def insert_into( + self, container, x_info=None, y_info=None, no_menuwidget=False + ): """Inserts the child widgets of an instance into the 'container'. - We need arguments specifying where the correct area within the - widget is - in the case of Gtk.Table instances, we need the - number of columns and the row index. These arguments are + We need arguments specifying where the correct area within the + widget is - in the case of Gtk.Table instances, we need the + number of columns and the row index. These arguments are generically named x_info and y_info. """ - if not hasattr(container, 'num_removes'): - setattr(container, 'num_removes', 0) + if not hasattr(container, "num_removes"): + setattr(container, "num_removes", 0) if isinstance(container, Gtk.Table): row_index = y_info key_col = 0 - container.attach(self.labelwidget, - key_col, key_col + 1, - row_index, row_index + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=Gtk.AttachOptions.FILL) - container.attach(self.contentwidget, - key_col + 1, key_col + 2, - row_index, row_index + 1, - xpadding=5, - xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, - yoptions=self.yoptions) - self.valuewidget.trigger_scroll = ( - lambda b, e: self.force_scroll(b, container)) - setattr(self, 'get_parent', lambda: container) - elif isinstance(container, Gtk.Box(orientation=Gtk.Orientation.VERTICAL)): - container.pack_start(self.labelwidget, expand=False, fill=True, - padding=5) - container.pack_start(self.contentwidget, expand=True, fill=True, - padding=10) - self.valuewidget.trigger_scroll = ( - lambda b, e: self.force_scroll(b, container)) - setattr(self, 'get_parent', lambda: container) + container.attach( + self.labelwidget, + key_col, + key_col + 1, + row_index, + row_index + 1, + xoptions=Gtk.AttachOptions.FILL, + yoptions=Gtk.AttachOptions.FILL, + ) + container.attach( + self.contentwidget, + key_col + 1, + key_col + 2, + row_index, + row_index + 1, + xpadding=5, + xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + yoptions=self.yoptions, + ) + self.valuewidget.trigger_scroll = lambda b, e: self.force_scroll( + b, container + ) + setattr(self, "get_parent", lambda: container) + elif isinstance( + container, Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + ): + container.pack_start( + self.labelwidget, expand=False, fill=True, padding=5 + ) + container.pack_start( + self.contentwidget, expand=True, fill=True, padding=10 + ) + self.valuewidget.trigger_scroll = lambda b, e: self.force_scroll( + b, container + ) + setattr(self, "get_parent", lambda: container) return container @@ -278,22 +343,31 @@ def force_scroll(self, widget=None, container=None): vadj = scroll_container.get_vadjustment() if vadj.get_upper() == 1.0 or y_coordinate == -1: if not self.force_signal_ids: - self.force_signal_ids.append(vadj.connect_after( - 'changed', - lambda a: self.force_scroll(widget, container))) + self.force_signal_ids.append( + vadj.connect_after( + "changed", + lambda a: self.force_scroll(widget, container), + ) + ) else: for handler_id in self.force_signal_ids: vadj.handler_block(handler_id) self.force_signal_ids = [] - vadj.connect('changed', metomi.rose.config_editor.false_function) + vadj.connect("changed", metomi.rose.config_editor.false_function) if y_coordinate is None: vadj.set_upper(vadj.get_upper() + 0.08 * vadj.get_page_size()) vadj.set_value(vadj.get_upper() - vadj.get_page_size()) return False if y_coordinate == -1: # Bad allocation, don't scroll return False - if not vadj.get_value() < y_coordinate < vadj.get_value() + 0.95 * vadj.get_page_size(): - vadj.set_value(min(y_coordinate, vadj.get_upper() - vadj.get_page_size())) + if ( + not vadj.get_value() + < y_coordinate + < vadj.get_value() + 0.95 * vadj.get_page_size() + ): + vadj.set_value( + min(y_coordinate, vadj.get_upper() - vadj.get_page_size()) + ) return False def remove_from(self, container): @@ -334,19 +408,23 @@ def set_ignored(self): if "'Ignore'" not in self.menuwidget.option_ui: self.menuwidget.old_option_ui = self.menuwidget.option_ui self.menuwidget.old_actions = self.menuwidget.actions - if list(ign_map.keys()) == [metomi.rose.variable.IGNORED_BY_SECTION]: + if list(ign_map.keys()) == [ + metomi.rose.variable.IGNORED_BY_SECTION + ]: # Not ignored in itself, so give Ignore option. if "'Enable'" in self.menuwidget.option_ui: self.menuwidget.option_ui = re.sub( "", r"", - self.menuwidget.option_ui) + self.menuwidget.option_ui, + ) else: # Ignored in itself, so needs Enable option. self.menuwidget.option_ui = re.sub( "", r"", - self.menuwidget.option_ui) + self.menuwidget.option_ui, + ) self.update_status() self.set_sensitive(False) else: @@ -356,7 +434,8 @@ def set_ignored(self): self.menuwidget.option_ui = re.sub( "", r"", - self.menuwidget.option_ui) + self.menuwidget.option_ui, + ) self.update_status() if not self.is_ghost: self.set_sensitive(True) @@ -374,7 +453,7 @@ def set_modified(self, is_modified=True): self.keywidget.set_modified(is_modified) if not is_modified and isinstance(self.keywidget.entry, Gtk.Entry): # This variable should now be displayed as a normal variable. - self.valuewidget.trigger_refresh(self.variable.metadata['id']) + self.valuewidget.trigger_refresh(self.variable.metadata["id"]) def set_sensitive(self, is_sensitive=True): """Sets whether the widgets are grayed-out or 'insensitive'.""" @@ -382,37 +461,43 @@ def set_sensitive(self, is_sensitive=True): widget.set_sensitive(is_sensitive) return False - def grab_focus(self, focus_container=None, scroll_bottom=False, - index=None): + def grab_focus( + self, focus_container=None, scroll_bottom=False, index=None + ): """Method similar to Gtk.Widget - get the keyboard focus.""" - if hasattr(self, 'valuewidget'): + if hasattr(self, "valuewidget"): self.valuewidget.grab_focus() - if (index is not None and - hasattr(self.valuewidget, 'set_focus_index')): + if index is not None and hasattr( + self.valuewidget, "set_focus_index" + ): self.valuewidget.set_focus_index(index) for child in self.valuewidget.get_children(): - if (self.valuewidget.get_sensitive() & child.get_state_flags() and - self.valuewidget.get_parent().get_sensitive() & child.get_state_flags()): + if ( + self.valuewidget.get_sensitive() & child.get_state_flags() + and self.valuewidget.get_parent().get_sensitive() + & child.get_state_flags() + ): break - else: # no break - if hasattr(self, 'menuwidget'): + else: # no break + if hasattr(self, "menuwidget"): self.menuwidget.get_children()[0].grab_focus() if scroll_bottom and focus_container is not None: self.force_scroll(None, container=focus_container) - if hasattr(self, 'keywidget') and self.key == '': + if hasattr(self, "keywidget") and self.key == "": self.keywidget.grab_focus() return False def get_focus_index(self): """Get the current cursor position in the variable value string.""" - if (hasattr(self, "valuewidget") and - hasattr(self.valuewidget, "get_focus_index")): + if hasattr(self, "valuewidget") and hasattr( + self.valuewidget, "get_focus_index" + ): return self.valuewidget.get_focus_index() - diff = difflib.SequenceMatcher(None, - self.variable.old_value, - self.variable.value) + diff = difflib.SequenceMatcher( + None, self.variable.old_value, self.variable.value + ) # Return all end-of-block indicies for changed blocks - indicies = [x[4] for x in diff.get_opcodes() if x[0] != 'equal'] + indicies = [x[4] for x in diff.get_opcodes() if x[0] != "equal"] if not indicies: return None return indicies[-1] @@ -425,10 +510,12 @@ def launch_help(self, url_mode=False): return help_text = None if self.show_modes.get( - metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP): + metomi.rose.config_editor.SHOW_MODE_CUSTOM_HELP + ): format_string = metomi.rose.config_editor.CUSTOM_FORMAT_HELP help_text = metomi.rose.variable.expand_format_string( - format_string, self.variable) + format_string, self.variable + ) if help_text is None: help_text = self.meta[metomi.rose.META_PROP_HELP] self._launch_help_dialog(help_text) @@ -436,11 +523,13 @@ def launch_help(self, url_mode=False): def _launch_help_dialog(self, help_text): """Launch a scrollable dialog for this variable's help text.""" title = metomi.rose.config_editor.DIALOG_HELP_TITLE.format( - self.variable.metadata["id"]) + self.variable.metadata["id"] + ) ns = self.variable.metadata["full_ns"] search_function = lambda i: self.var_ops.search_for_var(ns, i) metomi.rose.gtk.dialog.run_hyperlink_dialog( - Gtk.STOCK_DIALOG_INFO, help_text, title, search_function) + Gtk.STOCK_DIALOG_INFO, help_text, title, search_function + ) return False def _set_inconsistent(self, valuewidget, variable): @@ -450,12 +539,13 @@ def _set_inconsistent(self, valuewidget, variable): while widget_list: widget = widget_list.pop() widget.modify_text(Gtk.StateType.NORMAL, self.bad_colour) - if hasattr(widget, 'set_inconsistent'): + if hasattr(widget, "set_inconsistent"): widget.set_inconsistent(True) if isinstance(widget, Gtk.RadioButton): widget.set_active(False) - if (hasattr(widget, 'get_group') and - hasattr(widget.get_group(), 'set_inconsistent')): + if hasattr(widget, "get_group") and hasattr( + widget.get_group(), "set_inconsistent" + ): widget.get_group().set_inconsistent(True) if isinstance(widget, Gtk.Entry): widget.modify_fg(Gtk.StateType.NORMAL, self.bad_colour) @@ -464,14 +554,17 @@ def _set_inconsistent(self, valuewidget, variable): v_value = float(variable.value) w_value = float(widget.get_value()) except (TypeError, ValueError): - widget.modify_text(Gtk.StateType.NORMAL, self.hidden_colour) + widget.modify_text( + Gtk.StateType.NORMAL, self.hidden_colour + ) else: if w_value != v_value: - widget.modify_text(Gtk.StateType.NORMAL, - self.hidden_colour) - if hasattr(widget, 'get_children'): + widget.modify_text( + Gtk.StateType.NORMAL, self.hidden_colour + ) + if hasattr(widget, "get_children"): widget_list.extend(widget.get_children()) - elif hasattr(widget, 'get_child'): + elif hasattr(widget, "get_child"): widget_list.append(widget.get_child()) def _set_consistent(self, valuewidget, variable): @@ -483,12 +576,13 @@ def _set_consistent(self, valuewidget, variable): self.is_inconsistent = True for widget in valuewidget.get_children(): widget.modify_text(Gtk.StateType.NORMAL, normal_text) - if hasattr(widget, 'set_inconsistent'): + if hasattr(widget, "set_inconsistent"): widget.set_inconsistent(False) if isinstance(widget, Gtk.Entry): widget.modify_fg(Gtk.StateType.NORMAL, normal_fg) - if (hasattr(widget, 'get_group') and - hasattr(widget.get_group(), 'set_inconsistent')): + if hasattr(widget, "get_group") and hasattr( + widget.get_group(), "set_inconsistent" + ): widget.get_group().set_inconsistent(False) def _get_focus(self, widget_for_focus): @@ -499,8 +593,7 @@ def _get_focus(self, widget_for_focus): text_length = len(widget_for_focus.get_text()) if text_length > 0: widget_for_focus.set_position(text_length) - widget_for_focus.select_region(text_length, - text_length) + widget_for_focus.select_region(text_length, text_length) return False def needs_type_error_refresh(self): @@ -517,13 +610,14 @@ def type_error_refresh(self, variable): self._set_consistent(self.valuewidget, variable) self.variable = variable self.errors = list(variable.error.keys()) - self.valuewidget.handle_type_error(metomi.rose.META_PROP_TYPE in self.errors) + self.valuewidget.handle_type_error( + metomi.rose.META_PROP_TYPE in self.errors + ) self.menuwidget.refresh(variable) self.keywidget.refresh(variable) class RowVariableWidget(VariableWidget): - """This class generates a set of widgets for use as a row in a table.""" def __init__(self, *args, **kwargs): @@ -532,14 +626,17 @@ def __init__(self, *args, **kwargs): def generate_valuewidget(self, variable, override_custom=False): """Creates the valuewidget attribute, based on value and metadata.""" - if (metomi.rose.META_PROP_LENGTH in variable.metadata or - isinstance(variable.metadata.get(metomi.rose.META_PROP_TYPE), list)): + if metomi.rose.META_PROP_LENGTH in variable.metadata or isinstance( + variable.metadata.get(metomi.rose.META_PROP_TYPE), list + ): use_this_valuewidget = self.make_row_valuewidget else: use_this_valuewidget = None super(RowVariableWidget, self).generate_valuewidget( - variable, override_custom=override_custom, - use_this_valuewidget=use_this_valuewidget) + variable, + override_custom=override_custom, + use_this_valuewidget=use_this_valuewidget, + ) def make_row_valuewidget(self, *args, **kwargs): kwargs.update({"arg_str": str(self.length)}) diff --git a/metomi/rose/config_editor/window.py b/metomi/rose/config_editor/window.py index 6e9bcab9c..a80cf1112 100644 --- a/metomi/rose/config_editor/window.py +++ b/metomi/rose/config_editor/window.py @@ -23,7 +23,8 @@ import webbrowser import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk import metomi.rose.config @@ -35,8 +36,10 @@ REC_SPLIT_MACRO_TEXT = re.compile( - '(.{' + str(metomi.rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + - '})') + "(.{" + + str(metomi.rose.config_editor.DIALOG_BODY_MACRO_CHANGES_MAX_LENGTH) + + "})" +) class MetadataTable(object): @@ -45,6 +48,7 @@ class MetadataTable(object): provided parent. The current state of the table can be obtained using '.paths'. """ + def __init__(self, paths, parent): self.paths = paths self.parent = parent @@ -52,7 +56,7 @@ def __init__(self, paths, parent): self.draw_table() def draw_table(self): - """ Draws the table. """ + """Draws the table.""" # destroy previous table if present if self.previous: self.previous.destroy() @@ -63,15 +67,23 @@ def draw_table(self): # table rows for i, path in enumerate(self.paths): label = Gtk.Label(label=path) - label.set_alignment(xalign=0., yalign=0.5) + label.set_alignment(xalign=0.0, yalign=0.5) # component, col_from, col_to, row_from, row_to - table.attach(label, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL, xpadding=15) + table.attach( + label, + 0, + 1, + i, + i + 1, + xoptions=Gtk.AttachOptions.FILL, + xpadding=15, + ) label.show() - button = Gtk.Button('Remove') + button = Gtk.Button("Remove") button.data = path # component, col_from, col_to, row_from, row_to table.attach(button, 1, 2, i, i + 1) - button.connect('clicked', self.remove_row) + button.connect("clicked", self.remove_row) button.show() # append table @@ -82,27 +94,35 @@ def draw_table(self): self.previous = table def remove_row(self, widget): - """ To be called upon 'remove' button press. """ + """To be called upon 'remove' button press.""" self.paths.remove(widget.data) self.draw_table() def add_row(self, path): - """ Creates a new table row from the provided path (as a string). """ + """Creates a new table row from the provided path (as a string).""" self.paths.append(path) self.draw_table() class MainWindow(object): - """Generate the main window and dialog handling for this example.""" - def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, - nav_panel=None, status_bar=None, notebook=None, - page_change_func=metomi.rose.config_editor.false_function, - save_func=metomi.rose.config_editor.false_function): + def load( + self, + name="Untitled", + menu=None, + accelerators=None, + toolbar=None, + nav_panel=None, + status_bar=None, + notebook=None, + page_change_func=metomi.rose.config_editor.false_function, + save_func=metomi.rose.config_editor.false_function, + ): self.window = Gtk.Window() - self.window.set_title(name + ' - ' + - metomi.rose.config_editor.LAUNCH_COMMAND) + self.window.set_title( + name + " - " + metomi.rose.config_editor.LAUNCH_COMMAND + ) self.util = metomi.rose.config_editor.util.Lookup() self.window.set_icon(metomi.rose.gtk.util.get_icon()) Gtk.Window.set_default_icon_list([self.window.get_icon()]) @@ -122,12 +142,18 @@ def load(self, name='Untitled', menu=None, accelerators=None, toolbar=None, toolbar.show() self.top_vbox.pack_start(toolbar, False, True, 0) # Load the nav_panel and notebook - for signal in ['switch-page', 'focus-tab', 'select-page', - 'change-current-page']: + for signal in [ + "switch-page", + "focus-tab", + "select-page", + "change-current-page", + ]: notebook.connect_after(signal, page_change_func) self.generate_main_hbox(nav_panel, notebook) self.top_vbox.pack_start(self.main_hbox, True, True, 0) - self.top_vbox.pack_start(status_bar, expand=False, fill=False, padding=0) + self.top_vbox.pack_start( + status_bar, expand=False, fill=False, padding=0 + ) self.top_vbox.show() self.window.show() nav_panel.tree.columns_autosize() @@ -153,27 +179,36 @@ def launch_about_dialog(self, somewidget=None): copyright_=metomi.rose.config_editor.COPYRIGHT, logo_path="etc/images/rose-logo.png", website=metomi.rose.config_editor.PROJECT_URL, - website_label=metomi.rose.config_editor.PROJECT_URL) + website_label=metomi.rose.config_editor.PROJECT_URL, + ) def _reload_choices(self, liststore, top_name, add_choices): liststore.clear() for full_section_id in add_choices: - section_top_name, section_id = full_section_id.split(':', 1) + section_top_name, section_id = full_section_id.split(":", 1) if section_top_name == top_name: liststore.append([section_id]) def launch_add_dialog(self, names, add_choices, section_help): """Launch a dialog asking for a section name.""" - add_dialog = Gtk.Dialog(title=metomi.rose.config_editor.DIALOG_TITLE_ADD, - parent=self.window, - buttons=(Gtk.STOCK_CANCEL, - Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, - Gtk.ResponseType.ACCEPT)) + add_dialog = Gtk.Dialog( + title=metomi.rose.config_editor.DIALOG_TITLE_ADD, + parent=self.window, + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ), + ) ok_button = add_dialog.action_area.get_children()[0] - config_label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_BODY_ADD_CONFIG) + config_label = Gtk.Label( + label=metomi.rose.config_editor.DIALOG_BODY_ADD_CONFIG + ) config_label.show() - label = Gtk.Label(label=metomi.rose.config_editor.DIALOG_BODY_ADD_SECTION) + label = Gtk.Label( + label=metomi.rose.config_editor.DIALOG_BODY_ADD_SECTION + ) label.show() config_name_box = Gtk.ComboBoxText() for name in names: @@ -193,12 +228,15 @@ def launch_add_dialog(self, names, add_choices, section_help): config_name_box.connect( "changed", lambda c: self._reload_choices( - liststore, names[c.get_active()], add_choices)) - section_box.connect("activate", - lambda s: add_dialog.response(Gtk.ResponseType.OK)) + liststore, names[c.get_active()], add_choices + ), + ) section_box.connect( - "changed", - lambda s: ok_button.set_sensitive(bool(s.get_text()))) + "activate", lambda s: add_dialog.response(Gtk.ResponseType.OK) + ) + section_box.connect( + "changed", lambda s: ok_button.set_sensitive(bool(s.get_text())) + ) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) vbox.pack_start(config_label, expand=False, fill=False, padding=5) vbox.pack_start(config_name_box, expand=False, fill=False, padding=5) @@ -214,8 +252,11 @@ def launch_add_dialog(self, names, add_choices, section_help): section_completion.complete() ok_button.set_sensitive(bool(section_box.get_text())) response = add_dialog.run() - if response in [Gtk.ResponseType.OK, Gtk.ResponseType.YES, - Gtk.ResponseType.ACCEPT]: + if response in [ + Gtk.ResponseType.OK, + Gtk.ResponseType.YES, + Gtk.ResponseType.ACCEPT, + ]: config_name_entered = names[config_name_box.get_active()] section_name_entered = section_box.get_text() add_dialog.destroy() @@ -225,14 +266,23 @@ def launch_add_dialog(self, names, add_choices, section_help): def launch_exit_warning_dialog(self): """Launch a 'really want to quit' dialog.""" - text = 'Save changes before closing?' - exit_dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.NONE, - message_format=text, - parent=self.window) - exit_dialog.add_buttons(Gtk.STOCK_NO, Gtk.ResponseType.REJECT, - Gtk.STOCK_CANCEL, Gtk.ResponseType.CLOSE, - Gtk.STOCK_YES, Gtk.ResponseType.ACCEPT) - exit_dialog.set_title(metomi.rose.config_editor.DIALOG_TITLE_SAVE_CHANGES) + text = "Save changes before closing?" + exit_dialog = Gtk.MessageDialog( + buttons=Gtk.ButtonsType.NONE, + message_format=text, + parent=self.window, + ) + exit_dialog.add_buttons( + Gtk.STOCK_NO, + Gtk.ResponseType.REJECT, + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CLOSE, + Gtk.STOCK_YES, + Gtk.ResponseType.ACCEPT, + ) + exit_dialog.set_title( + metomi.rose.config_editor.DIALOG_TITLE_SAVE_CHANGES + ) exit_dialog.set_modal(True) exit_dialog.set_keep_above(True) exit_dialog.action_area.get_children()[1].grab_focus() @@ -255,19 +305,20 @@ def launch_graph_dialog(self, name_section_dict): """ prefs = {} return self._launch_choose_section_dialog( - name_section_dict, prefs, + name_section_dict, + prefs, metomi.rose.config_editor.DIALOG_TITLE_GRAPH, metomi.rose.config_editor.DIALOG_BODY_GRAPH_CONFIG, metomi.rose.config_editor.DIALOG_BODY_GRAPH_SECTION, - null_section_choice=True + null_section_choice=True, ) def launch_help_dialog(self, somewidget=None): """Launch a browser to open the help url.""" webbrowser.open( - 'https://metomi.github.io/rose/doc/html/index.html', + "https://metomi.github.io/rose/doc/html/index.html", new=True, - autoraise=True + autoraise=True, ) return False @@ -285,26 +336,41 @@ def launch_ignore_dialog(self, name_section_dict, prefs, is_ignored): dialog_title = metomi.rose.config_editor.DIALOG_TITLE_IGNORE else: dialog_title = metomi.rose.config_editor.DIALOG_TITLE_ENABLE - config_title = metomi.rose.config_editor.DIALOG_BODY_IGNORE_ENABLE_CONFIG + config_title = ( + metomi.rose.config_editor.DIALOG_BODY_IGNORE_ENABLE_CONFIG + ) if is_ignored: - section_title = metomi.rose.config_editor.DIALOG_BODY_IGNORE_SECTION + section_title = ( + metomi.rose.config_editor.DIALOG_BODY_IGNORE_SECTION + ) else: - section_title = metomi.rose.config_editor.DIALOG_BODY_ENABLE_SECTION + section_title = ( + metomi.rose.config_editor.DIALOG_BODY_ENABLE_SECTION + ) return self._launch_choose_section_dialog( - name_section_dict, prefs, - dialog_title, config_title, - section_title) + name_section_dict, prefs, dialog_title, config_title, section_title + ) def _launch_choose_section_dialog( - self, name_section_dict, prefs, dialog_title, config_title, - section_title, null_section_choice=False, do_target_section=False): + self, + name_section_dict, + prefs, + dialog_title, + config_title, + section_title, + null_section_choice=False, + do_target_section=False, + ): chooser_dialog = Gtk.Dialog( title=dialog_title, parent=self.window, - buttons=(Gtk.STOCK_CANCEL, - Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, - Gtk.ResponseType.ACCEPT)) + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ), + ) config_label = Gtk.Label(label=config_title) config_label.show() section_label = Gtk.Label(label=section_title) @@ -321,10 +387,10 @@ def _launch_choose_section_dialog( section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) section_box.show() null_section_checkbutton = Gtk.CheckButton( - metomi.rose.config_editor.DIALOG_LABEL_NULL_SECTION) + metomi.rose.config_editor.DIALOG_LABEL_NULL_SECTION + ) null_section_checkbutton.connect( - "toggled", - lambda b: section_box.set_sensitive(not b.get_active()) + "toggled", lambda b: section_box.set_sensitive(not b.get_active()) ) if null_section_choice: null_section_checkbutton.show() @@ -333,46 +399,67 @@ def _launch_choose_section_dialog( section_combo = self._reload_section_choices( section_box, name_section_dict[name_keys[index]], - prefs.get(name_keys[index], [])) + prefs.get(name_keys[index], []), + ) config_name_box.connect( - 'changed', + "changed", lambda c: self._reload_section_choices( section_box, name_section_dict[name_keys[c.get_active()]], - prefs.get(name_keys[c.get_active()], []))) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=metomi.rose.config_editor.SPACING_PAGE) + prefs.get(name_keys[c.get_active()], []), + ), + ) + vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=metomi.rose.config_editor.SPACING_PAGE, + ) vbox.pack_start(config_label, expand=False, fill=False, padding=0) vbox.pack_start(config_name_box, expand=False, fill=False, padding=0) vbox.pack_start(section_label, expand=False, fill=False, padding=0) - vbox.pack_start(null_section_checkbutton, expand=False, fill=False, padding=0) + vbox.pack_start( + null_section_checkbutton, expand=False, fill=False, padding=0 + ) vbox.pack_start(section_box, expand=False, fill=False, padding=0) if do_target_section: target_section_entry = Gtk.Entry() self._reload_target_section_entry( - section_combo, target_section_entry, - name_keys[config_name_box.get_active()], name_section_dict + section_combo, + target_section_entry, + name_keys[config_name_box.get_active()], + name_section_dict, ) section_combo.connect( "changed", lambda combo: self._reload_target_section_entry( - combo, target_section_entry, + combo, + target_section_entry, name_keys[config_name_box.get_active()], - name_section_dict - ) + name_section_dict, + ), ) target_section_entry.show() - vbox.pack_start(target_section_entry, expand=False, fill=False, padding=0) + vbox.pack_start( + target_section_entry, expand=False, fill=False, padding=0 + ) vbox.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(vbox, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) + hbox.pack_start( + vbox, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) hbox.show() chooser_dialog.vbox.pack_start( - hbox, True, True, metomi.rose.config_editor.SPACING_PAGE) + hbox, True, True, metomi.rose.config_editor.SPACING_PAGE + ) section_box.grab_focus() response = chooser_dialog.run() - if response in [Gtk.ResponseType.OK, Gtk.ResponseType.YES, - Gtk.ResponseType.ACCEPT]: + if response in [ + Gtk.ResponseType.OK, + Gtk.ResponseType.YES, + Gtk.ResponseType.ACCEPT, + ]: config_name_entered = name_keys[config_name_box.get_active()] if null_section_checkbutton.get_active(): chooser_dialog.destroy() @@ -381,7 +468,7 @@ def _launch_choose_section_dialog( return config_name_entered, None for widget in section_box.get_children(): - if hasattr(widget, 'get_active'): + if hasattr(widget, "get_active"): index = widget.get_active() sections = name_section_dict[config_name_entered] section_name = sections[index] @@ -389,8 +476,11 @@ def _launch_choose_section_dialog( target_section_name = target_section_entry.get_text() chooser_dialog.destroy() if do_target_section: - return (config_name_entered, section_name, - target_section_name) + return ( + config_name_entered, + section_name, + target_section_name, + ) return config_name_entered, section_name chooser_dialog.destroy() if do_target_section: @@ -412,19 +502,30 @@ def _reload_section_choices(self, vbox, sections, prefs): vbox.pack_start(section_chooser, expand=False, fill=False, padding=0) return section_chooser - def _reload_target_section_entry(self, section_combo_box, target_entry, - config_name_entered, name_section_dict): + def _reload_target_section_entry( + self, + section_combo_box, + target_entry, + config_name_entered, + name_section_dict, + ): index = section_combo_box.get_active() sections = name_section_dict[config_name_entered] section_name = sections[index] target_entry.set_text(section_name) def launch_macro_changes_dialog( - self, config_name, macro_name, changes_list, mode="transform", - search_func=metomi.rose.config_editor.false_function): + self, + config_name, + macro_name, + changes_list, + mode="transform", + search_func=metomi.rose.config_editor.false_function, + ): """Launch a dialog explaining macro changes.""" - dialog = MacroChangesDialog(self.window, config_name, macro_name, - mode, search_func) + dialog = MacroChangesDialog( + self.window, config_name, macro_name, mode, search_func + ) return dialog.display(changes_list) def launch_new_config_dialog(self, root_directory): @@ -434,31 +535,48 @@ def launch_new_config_dialog(self, root_directory): label = metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_NAME ok_tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_NAME err_tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_NAME_ERROR - dialog, container, name_entry = metomi.rose.gtk.dialog.get_naming_dialog( - label, checker_function, ok_tip_text, err_tip_text) + dialog, container, name_entry = ( + metomi.rose.gtk.dialog.get_naming_dialog( + label, checker_function, ok_tip_text, err_tip_text + ) + ) dialog.set_title(metomi.rose.config_editor.DIALOG_TITLE_CONFIG_CREATE) meta_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - meta_label = Gtk.Label(label= - metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META) + meta_label = Gtk.Label( + label=metomi.rose.config_editor.DIALOG_LABEL_CONFIG_CHOOSE_META + ) meta_label.show() meta_entry = Gtk.Entry() tip_text = metomi.rose.config_editor.TIP_CONFIG_CHOOSE_META meta_entry.set_tooltip_text(tip_text) meta_entry.connect( - "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT)) + "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT) + ) meta_entry.show() - meta_hbox.pack_start(meta_label, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) - meta_hbox.pack_start(meta_entry, expand=False, fill=True, - padding=metomi.rose.config_editor.SPACING_SUB_PAGE) + meta_hbox.pack_start( + meta_label, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) + meta_hbox.pack_start( + meta_entry, + expand=False, + fill=True, + padding=metomi.rose.config_editor.SPACING_SUB_PAGE, + ) meta_hbox.show() - container.pack_start(meta_hbox, expand=False, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) + container.pack_start( + meta_hbox, + expand=False, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) response = dialog.run() name = None meta = None if name_entry.get_text(): - name = name_entry.get_text().strip().strip('/') + name = name_entry.get_text().strip().strip("/") if meta_entry.get_text(): meta = meta_entry.get_text().strip() dialog.destroy() @@ -471,10 +589,13 @@ def launch_open_dirname_dialog(self): open_dialog = Gtk.FileChooserDialog( title=metomi.rose.config_editor.DIALOG_TITLE_OPEN, action=Gtk.FileChooserAction.OPEN, - buttons=(Gtk.STOCK_CANCEL, - Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, - Gtk.ResponseType.OK)) + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK, + ), + ) open_dialog.set_transient_for(self.window) open_dialog.set_icon(self.window.get_icon()) open_dialog.set_default_response(Gtk.ResponseType.OK) @@ -484,8 +605,11 @@ def launch_open_dirname_dialog(self): config_filter.add_pattern(metomi.rose.INFO_CONFIG_NAME) open_dialog.set_filter(config_filter) response = open_dialog.run() - if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, - Gtk.ResponseType.YES]: + if response in [ + Gtk.ResponseType.OK, + Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES, + ]: config_directory = os.path.dirname(open_dialog.get_filename()) open_dialog.destroy() return config_directory @@ -493,20 +617,26 @@ def launch_open_dirname_dialog(self): return None def launch_load_metadata_dialog(self): - """ Launches a dialoge for selecting a metadata path. """ + """Launches a dialoge for selecting a metadata path.""" open_dialog = Gtk.FileChooserDialog( title=metomi.rose.config_editor.DIALOG_TITLE_LOAD_METADATA, action=Gtk.FileChooserAction.SELECT_FOLDER, - buttons=(Gtk.STOCK_CLOSE, - Gtk.ResponseType.CANCEL, - Gtk.STOCK_ADD, - Gtk.ResponseType.OK)) + buttons=( + Gtk.STOCK_CLOSE, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_ADD, + Gtk.ResponseType.OK, + ), + ) open_dialog.set_transient_for(self.window) open_dialog.set_icon(self.window.get_icon()) open_dialog.set_default_response(Gtk.ResponseType.OK) response = open_dialog.run() - if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, - Gtk.ResponseType.YES]: + if response in [ + Gtk.ResponseType.OK, + Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES, + ]: config_directory = open_dialog.get_filename() open_dialog.destroy() return config_directory @@ -520,15 +650,19 @@ def launch_metadata_manager(self, paths): """ dialog = Gtk.Dialog( title=metomi.rose.config_editor.DIALOG_TITLE_MANAGE_METADATA, - buttons=(Gtk.STOCK_CANCEL, - Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, - Gtk.ResponseType.OK) + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, + Gtk.ResponseType.OK, + ), ) # add description - label = Gtk.Label(label='Specify metadata paths to override the default ' - 'metadata.\n') + label = Gtk.Label( + label="Specify metadata paths to override the default " + "metadata.\n" + ) dialog.vbox.pack_start(label, True, True, 0) label.show() @@ -536,20 +670,24 @@ def launch_metadata_manager(self, paths): table = MetadataTable(paths, dialog.vbox) # create add path button - button = Gtk.Button('Add Path') + button = Gtk.Button("Add Path") def add_path(): _path = self.launch_load_metadata_dialog() if _path: table.add_row(_path) - button.connect('clicked', lambda b: add_path()) + + button.connect("clicked", lambda b: add_path()) dialog.vbox.pack_start(button, True, True, 0) button.show() # open the dialogue response = dialog.run() - if response in [Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT, - Gtk.ResponseType.YES]: + if response in [ + Gtk.ResponseType.OK, + Gtk.ResponseType.ACCEPT, + Gtk.ResponseType.YES, + ]: # if user clicked 'ok' dialog.destroy() return table.paths @@ -561,8 +699,9 @@ def launch_prefs(self, somewidget=None): """Launch a dialog explaining preferences.""" text = metomi.rose.config_editor.DIALOG_LABEL_PREFERENCES title = metomi.rose.config_editor.DIALOG_TITLE_PREFERENCES - metomi.rose.gtk.dialog.run_dialog(metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, - title) + metomi.rose.gtk.dialog.run_dialog( + metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, title + ) return False def launch_remove_dialog(self, name_section_dict, prefs): @@ -574,10 +713,11 @@ def launch_remove_dialog(self, name_section_dict, prefs): """ return self._launch_choose_section_dialog( - name_section_dict, prefs, + name_section_dict, + prefs, metomi.rose.config_editor.DIALOG_TITLE_REMOVE, metomi.rose.config_editor.DIALOG_BODY_REMOVE_CONFIG, - metomi.rose.config_editor.DIALOG_BODY_REMOVE_SECTION + metomi.rose.config_editor.DIALOG_BODY_REMOVE_SECTION, ) def launch_rename_dialog(self, name_section_dict, prefs): @@ -589,39 +729,44 @@ def launch_rename_dialog(self, name_section_dict, prefs): """ return self._launch_choose_section_dialog( - name_section_dict, prefs, + name_section_dict, + prefs, metomi.rose.config_editor.DIALOG_TITLE_RENAME, metomi.rose.config_editor.DIALOG_BODY_RENAME_CONFIG, metomi.rose.config_editor.DIALOG_BODY_RENAME_SECTION, - do_target_section=True + do_target_section=True, ) def launch_view_stack(self, undo_stack, redo_stack, undo_func): """Load a view of the stack.""" self.log_window = metomi.rose.config_editor.stack.StackViewer( - undo_stack, redo_stack, undo_func) + undo_stack, redo_stack, undo_func + ) self.log_window.set_transient_for(self.window) class MacroChangesDialog(Gtk.Dialog): - """Class to hold a dialog summarising macro results.""" COLUMNS = ["Section", "Option", "Type", "Value", "Info"] - MODE_COLOURS = {"transform": metomi.rose.config_editor.COLOUR_MACRO_CHANGED, - "validate": metomi.rose.config_editor.COLOUR_MACRO_ERROR, - "warn": metomi.rose.config_editor.COLOUR_MACRO_WARNING} - MODE_TEXT = {"transform": metomi.rose.config_editor.DIALOG_TEXT_MACRO_CHANGED, - "validate": metomi.rose.config_editor.DIALOG_TEXT_MACRO_ERROR, - "warn": metomi.rose.config_editor.DIALOG_TEXT_MACRO_WARNING} + MODE_COLOURS = { + "transform": metomi.rose.config_editor.COLOUR_MACRO_CHANGED, + "validate": metomi.rose.config_editor.COLOUR_MACRO_ERROR, + "warn": metomi.rose.config_editor.COLOUR_MACRO_WARNING, + } + MODE_TEXT = { + "transform": metomi.rose.config_editor.DIALOG_TEXT_MACRO_CHANGED, + "validate": metomi.rose.config_editor.DIALOG_TEXT_MACRO_ERROR, + "warn": metomi.rose.config_editor.DIALOG_TEXT_MACRO_WARNING, + } def __init__(self, window, config_name, macro_name, mode, search_func): self.util = metomi.rose.config_editor.util.Lookup() - self.short_config_name = config_name.rstrip('/').split('/')[-1] - self.top_config_name = config_name.lstrip('/').split('/')[0] - self.short_macro_name = macro_name.split('.')[-1] - self.for_transform = (mode == "transform") - self.for_validate = (mode == "validate") + self.short_config_name = config_name.rstrip("/").split("/")[-1] + self.top_config_name = config_name.lstrip("/").split("/")[0] + self.short_macro_name = macro_name.split(".")[-1] + self.for_transform = mode == "transform" + self.for_validate = mode == "validate" self.macro_name = macro_name self.mode = mode self.search_func = search_func @@ -630,12 +775,17 @@ def __init__(self, window, config_name, macro_name, mode, search_func): button_list = [Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT] else: title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM - button_list = [Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_APPLY, Gtk.ResponseType.ACCEPT] + button_list = [ + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_APPLY, + Gtk.ResponseType.ACCEPT, + ] title = title.format(self.short_macro_name, self.short_config_name) button_list = tuple(button_list) - super(MacroChangesDialog, self).__init__(buttons=button_list, - parent=window) + super(MacroChangesDialog, self).__init__( + buttons=button_list, parent=window + ) if not self.for_transform: self.set_modal(False) self.set_title(title.format(macro_name)) @@ -648,16 +798,25 @@ def __init__(self, window, config_name, macro_name, mode, search_func): image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.LARGE_TOOLBAR) image.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(image, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_PAGE) - hbox.pack_start(self.label, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_PAGE) + hbox.pack_start( + image, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) + hbox.pack_start( + self.label, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) hbox.show() self.treewindow = Gtk.ScrolledWindow() self.treewindow.show() self.treewindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) self.treeview = metomi.rose.gtk.util.TooltipTreeView( - get_tooltip_func=self._get_tooltip) + get_tooltip_func=self._get_tooltip + ) self.treeview.show() self.treemodel = Gtk.TreeStore(str, str, str, str, str) @@ -677,40 +836,66 @@ def __init__(self, window, config_name, macro_name, mode, search_func): self.treeview.append_column(column) self.treeview.connect( - "row-activated", self._handle_treeview_activation) + "row-activated", self._handle_treeview_activation + ) self.treewindow.add(self.treeview) - self.vbox.pack_end(self.treewindow, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) - self.vbox.pack_end(hbox, expand=False, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) + self.vbox.pack_end( + self.treewindow, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) + self.vbox.pack_end( + hbox, + expand=False, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) self.set_focus(self.action_area.get_children()[0]) def display(self, changes): if not changes: # Shortcut, no changes. if self.for_validate: - title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE_NONE - text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_NONE + title = ( + metomi.rose.config_editor.DIALOG_TITLE_MACRO_VALIDATE_NONE + ) + text = ( + metomi.rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_NONE + ) else: - title = metomi.rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM_NONE - text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_NONE + title = ( + metomi.rose.config_editor.DIALOG_TITLE_MACRO_TRANSFORM_NONE + ) + text = ( + metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_NONE + ) title = title.format(self.short_macro_name) text = metomi.rose.gtk.util.safe_str(text) return metomi.rose.gtk.dialog.run_dialog( - metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, title) + metomi.rose.gtk.dialog.DIALOG_TYPE_INFO, text, title + ) if self.for_validate: text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_VALIDATE_ISSUES else: - text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_CHANGES + text = ( + metomi.rose.config_editor.DIALOG_LABEL_MACRO_TRANSFORM_CHANGES + ) nums_is_warning = {True: 0, False: 0} for item in changes: nums_is_warning[item.is_warning] += 1 - text = text.format(self.short_macro_name, self.short_config_name, - nums_is_warning[False]) + text = text.format( + self.short_macro_name, + self.short_config_name, + nums_is_warning[False], + ) if nums_is_warning[True]: - extra_text = metomi.rose.config_editor.DIALOG_LABEL_MACRO_WARN_ISSUES - text = (text.rstrip() + " " + - extra_text.format(nums_is_warning[True])) + extra_text = ( + metomi.rose.config_editor.DIALOG_LABEL_MACRO_WARN_ISSUES + ) + text = ( + text.rstrip() + " " + extra_text.format(nums_is_warning[True]) + ) self.label.set_markup(text) changes.sort(key=lambda x: str(x.option)) changes.sort(key=lambda x: str(x.section)) @@ -721,8 +906,13 @@ def display(self, changes): item_mode = self.mode if item.is_warning: item_mode = "warn" - item_att_list = [item.section, item.option, item_mode, - item.value, item.info] + item_att_list = [ + item.section, + item.option, + item_mode, + item.value, + item.info, + ] if item.section == last_section: self.treemodel.append(last_section_iter, item_att_list) else: @@ -737,16 +927,19 @@ def display(self, changes): # this needs checking new_size[0] = min([my_size.width, max_size[0]]) new_size[1] = min([my_size.height, max_size[1]]) - self.treewindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.treewindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) self.set_default_size(*new_size) if self.for_transform: response = self.run() self.destroy() - return (response == Gtk.ResponseType.ACCEPT) + return response == Gtk.ResponseType.ACCEPT else: self.show() self.action_area.get_children()[0].connect( - "clicked", lambda b: self.destroy()) + "clicked", lambda b: self.destroy() + ) def _get_tooltip(self, view, row_iter, col_index, tip): tip.set_text(view.get_model().get_value(row_iter, col_index)) diff --git a/metomi/rose/gtk/__init__.py b/metomi/rose/gtk/__init__.py index 063f7abd5..e79c307c6 100644 --- a/metomi/rose/gtk/__init__.py +++ b/metomi/rose/gtk/__init__.py @@ -1,7 +1,8 @@ try: import gi - gi.require_version('Gtk', '3.0') - from gi.repository import Gtk + + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk # noqa: F401 except (ImportError, RuntimeError, AssertionError): INTERACTIVE_ENABLED = False else: diff --git a/metomi/rose/gtk/choice.py b/metomi/rose/gtk/choice.py index 8a349c57a..76d5109c0 100644 --- a/metomi/rose/gtk/choice.py +++ b/metomi/rose/gtk/choice.py @@ -19,14 +19,14 @@ # ----------------------------------------------------------------------------- import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk import metomi.rose class ChoicesListView(Gtk.TreeView): - """Class to hold and display an ordered list of strings. set_value is a function, accepting a new value string. @@ -48,22 +48,30 @@ class ChoicesListView(Gtk.TreeView): """ - def __init__(self, set_value, get_data, handle_search, - title=metomi.rose.config_editor.CHOICE_TITLE_INCLUDED, - get_custom_menu_items=lambda: []): + def __init__( + self, + set_value, + get_data, + handle_search, + title=metomi.rose.config_editor.CHOICE_TITLE_INCLUDED, + get_custom_menu_items=lambda: [], + ): super(ChoicesListView, self).__init__() self._set_value = set_value self._get_data = get_data self._handle_search = handle_search self._get_custom_menu_items = get_custom_menu_items self.enable_model_drag_dest( - [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + [("text/plain", 0, 0)], Gdk.DragAction.MOVE + ) self.enable_model_drag_source( - Gdk.ModifierType.BUTTON1_MASK, [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + Gdk.ModifierType.BUTTON1_MASK, + [("text/plain", 0, 0)], + Gdk.DragAction.MOVE, + ) self.connect("button-press-event", self._handle_button_press) self.connect("drag-data-get", self._handle_drag_get) - self.connect_after("drag-data-received", - self._handle_drag_received) + self.connect_after("drag-data-received", self._handle_drag_received) self.set_rules_hint(True) self.connect("row-activated", self._handle_activation) self.show() @@ -73,8 +81,8 @@ def __init__(self, set_value, get_data, handle_search, else: col.set_title(title) cell_text = Gtk.CellRendererText() - cell_text.set_property('editable', True) - cell_text.connect('edited', self._handle_edited) + cell_text.set_property("editable", True) + cell_text.connect("edited", self._handle_edited) col.pack_start(cell_text, True) col.set_cell_data_func(cell_text, self._set_cell_text, None) self.append_column(col) @@ -108,7 +116,8 @@ def _handle_drag_get(self, treeview, drag, sel, info, time): model.append([metomi.rose.config_editor.CHOICE_LABEL_EMPTY]) def _handle_drag_received( - self, treeview, drag, xpos, ypos, sel, info, time): + self, treeview, drag, xpos, ypos, sel, info, time + ): """Handle an incoming drag request.""" if sel.data is None: return False @@ -116,8 +125,10 @@ def _handle_drag_received( model = treeview.get_model() if drop_info: path, position = drop_info - if (position == Gtk.TreeViewDropPosition.BEFORE or - position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE): + if ( + position == Gtk.TreeViewDropPosition.BEFORE + or position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE + ): model.insert(path[0], [sel.data]) else: model.insert(path[0] + 1, [sel.data]) @@ -127,7 +138,7 @@ def _handle_drag_received( self._handle_reordering(model, path) def _handle_edited(self, cell, path, new_text): - """Handle cell text so it can be edited. """ + """Handle cell text so it can be edited.""" liststore = self.get_model() iter_ = liststore.get_iter(path) liststore.set_value(iter_, 0, new_text) @@ -173,23 +184,23 @@ def _popup_menu(self, iter_, event): text = metomi.rose.config_editor.CHOICE_MENU_REMOVE actions = [("Remove", Gtk.STOCK_DELETE, text)] uimanager = Gtk.UIManager() - actiongroup = Gtk.ActionGroup('Popup') + actiongroup = Gtk.ActionGroup("Popup") actiongroup.add_actions(actions) uimanager.insert_action_group(actiongroup) uimanager.add_ui_from_string(ui_config_string) - remove_item = uimanager.get_widget('/Popup/Remove') - remove_item.connect("activate", - lambda b: self._remove_iter(iter_)) - menu = uimanager.get_widget('/Popup') + remove_item = uimanager.get_widget("/Popup/Remove") + remove_item.connect("activate", lambda b: self._remove_iter(iter_)) + menu = uimanager.get_widget("/Popup") for menuitem in self._get_custom_menu_items(): menuitem._listview_model = self.get_model() menuitem._listview_iter = iter_ menuitem.connect_after( - "button-press-event", - lambda b, e: self._handle_reordering() + "button-press-event", lambda b, e: self._handle_reordering() ) menu.append(menuitem) - menu.popup_at_widget(event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event) + menu.popup_at_widget( + event.button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) return False def _remove_iter(self, iter_): @@ -213,7 +224,6 @@ def refresh(self): class ChoicesTreeView(Gtk.TreeView): - """Class to hold and display a tree of content. set_value is a function, accepting a new value string. @@ -232,10 +242,16 @@ class ChoicesTreeView(Gtk.TreeView): """ - def __init__(self, set_value, get_data, get_available_data, - get_groups, get_is_implicit=None, - title=metomi.rose.config_editor.CHOICE_TITLE_AVAILABLE, - get_is_included=None): + def __init__( + self, + set_value, + get_data, + get_available_data, + get_groups, + get_is_implicit=None, + title=metomi.rose.config_editor.CHOICE_TITLE_AVAILABLE, + get_is_included=None, + ): super(ChoicesTreeView, self).__init__() # Generate the 'available' sections view. self._set_value = set_value @@ -247,9 +263,13 @@ def __init__(self, set_value, get_data, get_available_data, self.set_headers_visible(True) self.set_rules_hint(True) self.enable_model_drag_dest( - [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + [("text/plain", 0, 0)], Gdk.DragAction.MOVE + ) self.enable_model_drag_source( - Gdk.ModifierType.BUTTON1_MASK, [('text/plain', 0, 0)], Gdk.DragAction.MOVE) + Gdk.ModifierType.BUTTON1_MASK, + [("text/plain", 0, 0)], + Gdk.DragAction.MOVE, + ) self.connect_after("button-release-event", self._handle_button) self.connect("drag-begin", self._handle_drag_begin) self.connect("drag-data-get", self._handle_drag_get) @@ -294,12 +314,15 @@ def _populate(self): groups = self._get_groups(name, ok_content_sections) if self._get_is_implicit is None: is_implicit = any( - [self._get_is_included(g, ok_values) for g in groups]) + [self._get_is_included(g, ok_values) for g in groups] + ) else: is_implicit = self._get_is_implicit(name) if groups: - iter_ = model.append(self._name_iter_map[groups[-1]], - [name, is_included, is_implicit]) + iter_ = model.append( + self._name_iter_map[groups[-1]], + [name, is_included, is_implicit], + ) else: iter_ = model.append(None, [name, is_included, is_implicit]) self._name_iter_map[name] = iter_ @@ -314,7 +337,8 @@ def _realign(self): if self._get_is_implicit is None: groups = self._get_groups(name, ok_content_sections) is_implicit = any( - [self._get_is_included(g, ok_values) for g in groups]) + [self._get_is_included(g, ok_values) for g in groups] + ) else: is_implicit = self._get_is_implicit(name) if model.get_value(iter_, 1) != is_in_value: @@ -380,8 +404,9 @@ def _check_can_add(self, iter_): return False child_iter = model.iter_children(iter_) while child_iter is not None: - if (model.get_value(child_iter, 1) or - model.get_value(child_iter, 2)): + if model.get_value(child_iter, 1) or model.get_value( + child_iter, 2 + ): return False child_iter = model.iter_next(child_iter) return True @@ -416,8 +441,9 @@ def _handle_cell_toggle(self, cell, path, should_turn_off=None): model = self.get_model() can_add = self._check_can_add(r_iter) should_add = False - if ((should_turn_off is None or should_turn_off) and - self._get_is_included(this_name, ok_values)): + if ( + should_turn_off is None or should_turn_off + ) and self._get_is_included(this_name, ok_values): ok_values.remove(this_name) elif should_turn_off is None or not should_turn_off: if not can_add: diff --git a/metomi/rose/gtk/console.py b/metomi/rose/gtk/console.py index 222e443ca..f6959c8d8 100644 --- a/metomi/rose/gtk/console.py +++ b/metomi/rose/gtk/console.py @@ -21,6 +21,7 @@ import datetime import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -28,7 +29,6 @@ class ConsoleWindow(Gtk.Window): - """Create an error console window.""" CATEGORY_ALL = "All" @@ -38,9 +38,15 @@ class ConsoleWindow(Gtk.Window): DEFAULT_SIZE = (600, 300) TITLE = "Error Console" - def __init__(self, categories, category_message_time_tuples, - category_stock_ids, default_size=None, parent=None, - destroy_hook=None): + def __init__( + self, + categories, + category_message_time_tuples, + category_stock_ids, + default_size=None, + parent=None, + destroy_hook=None, + ): super(ConsoleWindow, self).__init__() if parent is not None: self.set_transient_for(parent) @@ -53,15 +59,17 @@ def __init__(self, categories, category_message_time_tuples, self.category_icons = [] for id_ in category_stock_ids: self.category_icons.append( - self.render_icon(id_, Gtk.IconSize.MENU)) + self.render_icon(id_, Gtk.IconSize.MENU) + ) self._destroy_hook = destroy_hook top_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) top_vbox.show() self.add(top_vbox) message_scrolled_window = Gtk.ScrolledWindow() - message_scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC) + message_scrolled_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) message_scrolled_window.show() self._message_treeview = Gtk.TreeView() self._message_treeview.show() @@ -72,8 +80,9 @@ def __init__(self, categories, category_message_time_tuples, category_column.set_title(self.COLUMN_TITLE_CATEGORY) cell_category = Gtk.CellRendererPixbuf() category_column.pack_start(cell_category, False) - category_column.set_cell_data_func(cell_category, - self._set_category_cell, 0) + category_column.set_cell_data_func( + cell_category, self._set_category_cell, 0 + ) category_column.set_clickable(True) category_column.connect("clicked", self._sort_column, 0) self._message_treeview.append_column(category_column) @@ -83,8 +92,7 @@ def __init__(self, categories, category_message_time_tuples, message_column.set_title(self.COLUMN_TITLE_MESSAGE) cell_message = Gtk.CellRendererText() message_column.pack_start(cell_message, False) - message_column.add_attribute(cell_message, attribute="text", - column=1) + message_column.add_attribute(cell_message, attribute="text", column=1) message_column.set_clickable(True) message_column.connect("clicked", self._sort_column, 1) self._message_treeview.append_column(message_column) @@ -108,19 +116,27 @@ def __init__(self, categories, category_message_time_tuples, self._message_treeview.set_model(filter_model) message_scrolled_window.add(self._message_treeview) - top_vbox.pack_start(message_scrolled_window, expand=True, fill=True, padding=0) + top_vbox.pack_start( + message_scrolled_window, expand=True, fill=True, padding=0 + ) category_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) category_hbox.show() top_vbox.pack_end(category_hbox, expand=False, fill=False, padding=0) for category in categories + [self.CATEGORY_ALL]: - togglebutton = Gtk.ToggleButton(label=category, - use_underline=False) - togglebutton.connect("toggled", - lambda b: self._set_new_filter( - b, category_hbox.get_children())) + togglebutton = Gtk.ToggleButton( + label=category, use_underline=False + ) + togglebutton.connect( + "toggled", + lambda b: self._set_new_filter( + b, category_hbox.get_children() + ), + ) togglebutton.show() - category_hbox.pack_start(togglebutton, expand=True, fill=True, padding=0) + category_hbox.pack_start( + togglebutton, expand=True, fill=True, padding=0 + ) togglebutton.set_active(True) self.show() self._scroll_to_end() @@ -171,7 +187,8 @@ def _set_new_filter(self, togglebutton, togglebuttons): def _set_time_cell(self, column, cell, model, r_iter, index): message_time = model.get_value(r_iter, index) text = datetime.datetime.fromtimestamp(message_time).strftime( - metomi.rose.config_editor.EVENT_TIME_LONG) + metomi.rose.config_editor.EVENT_TIME_LONG + ) cell.set_property("text", text) def _sort_column(self, column, index): diff --git a/metomi/rose/gtk/dialog.py b/metomi/rose/gtk/dialog.py index b01e86dd9..22ff27caa 100644 --- a/metomi/rose/gtk/dialog.py +++ b/metomi/rose/gtk/dialog.py @@ -19,7 +19,6 @@ # ----------------------------------------------------------------------------- from multiprocessing import Process -import os import queue import shlex from subprocess import Popen, PIPE @@ -27,9 +26,9 @@ import tempfile import time import traceback -import webbrowser import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GdkPixbuf from gi.repository import GLib @@ -50,8 +49,9 @@ DIALOG_TEXT_SHUTDOWN_ASAP = "Shutdown ASAP." DIALOG_TEXT_SHUTTING_DOWN = "Shutting down." -DIALOG_TEXT_UNCAUGHT_EXCEPTION = ("{0} has crashed. {1}" + - "\n\n{2}: {3}\n{4}") +DIALOG_TEXT_UNCAUGHT_EXCEPTION = ( + "{0} has crashed. {1}" + "\n\n{2}: {3}\n{4}" +) DIALOG_TITLE_ERROR = "Error" DIALOG_TITLE_UNCAUGHT_EXCEPTION = "Critical error" DIALOG_TITLE_EXTRA_INFO = "Further information" @@ -61,7 +61,6 @@ class DialogProcess(object): - """Run a forked process and display a dialog while it runs. cmd_args can either be a list of shell command components @@ -80,19 +79,27 @@ class DialogProcess(object): DIALOG_LOG_LABEL = "Show log" DIALOG_PROCESS_LABEL = "Executing command" - def __init__(self, cmd_args, description=None, title=None, - stock_id=Gtk.STOCK_EXECUTE, - hide_progress=False, modal=True, - event_queue=None): + def __init__( + self, + cmd_args, + description=None, + title=None, + stock_id=Gtk.STOCK_EXECUTE, + hide_progress=False, + modal=True, + event_queue=None, + ): self.proc = None window = get_dialog_parent() - self.dialog = Gtk.Dialog(buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), - parent=window) + self.dialog = Gtk.Dialog( + buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), parent=window + ) self.dialog.set_modal(modal) self.dialog.set_default_size(*DIALOG_SIZE_PROCESS) self._is_destroyed = False - self.dialog.set_icon(self.dialog.render_icon(Gtk.STOCK_EXECUTE, - Gtk.IconSize.MENU)) + self.dialog.set_icon( + self.dialog.render_icon(Gtk.STOCK_EXECUTE, Gtk.IconSize.MENU) + ) self.cmd_args = cmd_args self.event_queue = event_queue str_cmd_args = [metomi.rose.gtk.util.safe_str(a) for a in cmd_args] @@ -108,24 +115,26 @@ def __init__(self, cmd_args, description=None, title=None, self.label = Gtk.Label(label=self.DIALOG_PROCESS_LABEL) self.label.set_use_markup(True) self.label.show() - self.image = Gtk.Image.new_from_stock(stock_id, - Gtk.IconSize.DIALOG) + self.image = Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.DIALOG) self.image.show() image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) image_vbox.pack_start(self.image, expand=False, fill=False, padding=0) image_vbox.show() top_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - top_hbox.pack_start(image_vbox, expand=False, fill=False, - padding=DIALOG_PADDING) + top_hbox.pack_start( + image_vbox, expand=False, fill=False, padding=DIALOG_PADDING + ) top_hbox.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(self.label, expand=False, fill=False, - padding=DIALOG_PADDING) + hbox.pack_start( + self.label, expand=False, fill=False, padding=DIALOG_PADDING + ) hbox.show() main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) main_vbox.show() - main_vbox.pack_start(hbox, expand=False, fill=False, - padding=DIALOG_SUB_PADDING) + main_vbox.pack_start( + hbox, expand=False, fill=False, padding=DIALOG_SUB_PADDING + ) cmd_string = str_cmd_args[0] if str_cmd_args[1:]: @@ -137,36 +146,44 @@ def __init__(self, cmd_args, description=None, title=None, self.cmd_label.set_markup("" + cmd_string + "") self.cmd_label.show() cmd_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - cmd_hbox.pack_start(self.cmd_label, expand=False, fill=False, - padding=DIALOG_PADDING) + cmd_hbox.pack_start( + self.cmd_label, expand=False, fill=False, padding=DIALOG_PADDING + ) cmd_hbox.show() - main_vbox.pack_start(cmd_hbox, expand=False, fill=True, - padding=DIALOG_SUB_PADDING) + main_vbox.pack_start( + cmd_hbox, expand=False, fill=True, padding=DIALOG_SUB_PADDING + ) # self.dialog.set_modal(True) self.progress_bar = Gtk.ProgressBar() self.progress_bar.set_pulse_step(0.1) self.progress_bar.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(self.progress_bar, expand=True, fill=True, - padding=DIALOG_PADDING) + hbox.pack_start( + self.progress_bar, expand=True, fill=True, padding=DIALOG_PADDING + ) hbox.show() - main_vbox.pack_start(hbox, expand=False, fill=False, - padding=DIALOG_SUB_PADDING) - top_hbox.pack_start(main_vbox, expand=True, fill=True, - padding=DIALOG_PADDING) + main_vbox.pack_start( + hbox, expand=False, fill=False, padding=DIALOG_SUB_PADDING + ) + top_hbox.pack_start( + main_vbox, expand=True, fill=True, padding=DIALOG_PADDING + ) if self.event_queue is None: - self.dialog.vbox.pack_start(top_hbox, expand=True, fill=True, padding=0) + self.dialog.vbox.pack_start( + top_hbox, expand=True, fill=True, padding=0 + ) else: text_view_scroll = Gtk.ScrolledWindow() - text_view_scroll.set_policy(Gtk.PolicyType.NEVER, - Gtk.PolicyType.AUTOMATIC) + text_view_scroll.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC + ) text_view_scroll.show() text_view = Gtk.TextView() text_view.show() self.text_buffer = text_view.get_buffer() self.text_tag = self.text_buffer.create_tag() self.text_tag.set_property("scale", Pango.SCALE_SMALL) - text_view.connect('size-allocate', self._handle_scroll_text_view) + text_view.connect("size-allocate", self._handle_scroll_text_view) text_view_scroll.add(text_view) text_expander = Gtk.Expander(self.DIALOG_LOG_LABEL) text_expander.set_spacing(DIALOG_SUB_PADDING) @@ -175,11 +192,12 @@ def __init__(self, cmd_args, description=None, title=None, top_pane = Gtk.VPaned() top_pane.pack1(top_hbox, resize=False, shrink=False) top_pane.show() - self.dialog.vbox.pack_start(top_pane, expand=True, fill=True, - padding=DIALOG_SUB_PADDING) + self.dialog.vbox.pack_start( + top_pane, expand=True, fill=True, padding=DIALOG_SUB_PADDING + ) top_pane.pack2(text_expander, resize=True, shrink=True) if hide_progress: - progress_bar.hide() + self.progress_bar.hide() self.ok_button = self.dialog.get_action_area().get_children()[0] self.ok_button.hide() for child in self.dialog.vbox.get_children(): @@ -192,7 +210,8 @@ def run(self): stdout = tempfile.TemporaryFile() stderr = tempfile.TemporaryFile() self.proc = Process( - target=_sep_process, args=[self.cmd_args, stdout, stderr]) + target=_sep_process, args=[self.cmd_args, stdout, stderr] + ) self.proc.start() self.dialog.connect("destroy", self._handle_dialog_process_destroy) while self.proc.is_alive(): @@ -204,8 +223,9 @@ def run(self): except queue.Empty: break end = self.text_buffer.get_end_iter() - self.text_buffer.insert_with_tags(end, new_text, - self.text_tag) + self.text_buffer.insert_with_tags( + end, new_text, self.text_tag + ) while Gtk.events_pending(): Gtk.main_iteration() time.sleep(0.1) @@ -215,12 +235,16 @@ def run(self): if self._is_destroyed: return self.proc.exitcode else: - self.image.set_from_stock(Gtk.STOCK_DIALOG_ERROR, - Gtk.IconSize.DIALOG) + self.image.set_from_stock( + Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.DIALOG + ) self.label.hide() self.progress_bar.hide() self.cmd_label.set_markup( - "" + metomi.rose.gtk.util.safe_str(stderr.read()) + "") + "" + + metomi.rose.gtk.util.safe_str(stderr.read()) + + "" + ) self.ok_button.show() for child in self.dialog.vbox.get_children(): if isinstance(child, Gtk.HSeparator): @@ -267,8 +291,13 @@ def _process(cmd_args, stdout=sys.stdout, stderr=sys.stderr): return proc.poll() -def run_about_dialog(name=None, copyright_=None, - logo_path=None, website=None, website_label=None): +def run_about_dialog( + name=None, + copyright_=None, + logo_path=None, + website=None, + website_label=None, +): parent_window = get_dialog_parent() about_dialog = Gtk.AboutDialog() about_dialog.set_transient_for(parent_window) @@ -288,24 +317,27 @@ def run_about_dialog(name=None, copyright_=None, def run_command_arg_dialog(cmd_name, help_text, run_hook): """Launch a dialog to get extra arguments for a command.""" checker_function = lambda t: True - dialog, container, name_entry = get_naming_dialog(cmd_name, - checker_function) + dialog, container, name_entry = get_naming_dialog( + cmd_name, checker_function + ) dialog.set_title(cmd_name) help_label = Gtk.stock_lookup(Gtk.STOCK_HELP)[1].strip("_") help_button = metomi.rose.gtk.util.CustomButton( stock_id=Gtk.STOCK_HELP, label=help_label, - size=Gtk.IconSize.LARGE_TOOLBAR) + size=Gtk.IconSize.LARGE_TOOLBAR, + ) help_button.connect( - "clicked", - lambda b: run_scrolled_dialog(help_text, title=help_label)) + "clicked", lambda b: run_scrolled_dialog(help_text, title=help_label) + ) help_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) help_hbox.pack_start(help_button, expand=False, fill=False, padding=0) help_hbox.show() container.pack_end(help_hbox, expand=False, fill=False, padding=0) name_entry.grab_focus() - dialog.connect("response", _handle_command_arg_response, run_hook, - name_entry) + dialog.connect( + "response", _handle_command_arg_response, run_hook, name_entry + ) dialog.set_modal(False) dialog.show() @@ -317,8 +349,9 @@ def _handle_command_arg_response(dialog, response, run_hook, entry): run_hook(shlex.split(text)) -def run_dialog(dialog_type, text, title=None, modal=True, - cancel=False, extra_text=None): +def run_dialog( + dialog_type, text, title=None, modal=True, cancel=False, extra_text=None +): """Run a simple dialog with an 'OK' button and some text.""" parent_window = get_dialog_parent() dialog = Gtk.Dialog(parent=parent_window) @@ -332,7 +365,8 @@ def run_dialog(dialog_type, text, title=None, modal=True, info_title = DIALOG_TITLE_EXTRA_INFO info_button.connect( "clicked", - lambda b: run_scrolled_dialog(extra_text, title=info_title)) + lambda b: run_scrolled_dialog(extra_text, title=info_title), + ) dialog.action_area.pack_start(info_button, expand=False, fill=False) ok_button = dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) if dialog_type == Gtk.MessageType.INFO: @@ -366,11 +400,16 @@ def run_dialog(dialog_type, text, title=None, modal=True, if stock_id is not None: image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - image_vbox.pack_start(dialog.image, expand=False, fill=False, - padding=DIALOG_PADDING) + image_vbox.pack_start( + dialog.image, expand=False, fill=False, padding=DIALOG_PADDING + ) image_vbox.show() - hbox.pack_start(image_vbox, expand=False, fill=False, - padding=metomi.rose.config_editor.SPACING_PAGE) + hbox.pack_start( + image_vbox, + expand=False, + fill=False, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_border_width(0) @@ -381,8 +420,12 @@ def run_dialog(dialog_type, text, title=None, modal=True, scrolled_window.add_with_viewport(vbox) scrolled_window.get_child().set_shadow_type(Gtk.ShadowType.NONE) scrolled_window.show() - hbox.pack_start(scrolled_window, expand=True, fill=True, - padding=metomi.rose.config_editor.SPACING_PAGE) + hbox.pack_start( + scrolled_window, + expand=True, + fill=True, + padding=metomi.rose.config_editor.SPACING_PAGE, + ) hbox.show() dialog.vbox.pack_end(hbox, expand=True, fill=True, padding=0) @@ -399,7 +442,7 @@ def run_dialog(dialog_type, text, title=None, modal=True, dialog.show() response = dialog.run() dialog.destroy() - return (response == Gtk.ResponseType.OK) + return response == Gtk.ResponseType.OK else: ok_button.connect("clicked", lambda b: dialog.destroy()) dialog.show() @@ -411,8 +454,9 @@ def run_exception_dialog(exception): return run_dialog(DIALOG_TYPE_ERROR, text, DIALOG_TITLE_ERROR) -def run_hyperlink_dialog(stock_id=None, text="", title=None, - search_func=lambda i: False): +def run_hyperlink_dialog( + stock_id=None, text="", title=None, search_func=lambda i: False +): """Run a dialog with inserted hyperlinks.""" parent_window = get_dialog_parent() dialog = Gtk.Window() @@ -422,24 +466,28 @@ def run_hyperlink_dialog(stock_id=None, text="", title=None, dialog.set_modal(False) top_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) top_vbox.show() - main_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=DIALOG_PADDING) + main_hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=DIALOG_PADDING + ) main_hbox.show() # Insert the image image_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) image_vbox.show() - image = Gtk.Image.new_from_stock(stock_id, - size=Gtk.IconSize.DIALOG) + image = Gtk.Image.new_from_stock(stock_id, size=Gtk.IconSize.DIALOG) image.show() - image_vbox.pack_start(image, expand=False, fill=False, - padding=DIALOG_PADDING) - main_hbox.pack_start(image_vbox, expand=False, fill=False, - padding=DIALOG_PADDING) + image_vbox.pack_start( + image, expand=False, fill=False, padding=DIALOG_PADDING + ) + main_hbox.pack_start( + image_vbox, expand=False, fill=False, padding=DIALOG_PADDING + ) # Apply the text message_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) message_vbox.show() label = metomi.rose.gtk.util.get_hyperlink_label(text, search_func) - message_vbox.pack_start(label, expand=True, fill=True, - padding=DIALOG_PADDING) + message_vbox.pack_start( + label, expand=True, fill=True, padding=DIALOG_PADDING + ) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_border_width(DIALOG_PADDING) scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) @@ -454,14 +502,18 @@ def run_hyperlink_dialog(stock_id=None, text="", title=None, # Insert the button button_box = Gtk.Box(spacing=DIALOG_PADDING) button_box.show() - button = metomi.rose.gtk.util.CustomButton(label=DIALOG_BUTTON_CLOSE, - size=Gtk.IconSize.LARGE_TOOLBAR, - stock_id=Gtk.STOCK_CLOSE) + button = metomi.rose.gtk.util.CustomButton( + label=DIALOG_BUTTON_CLOSE, + size=Gtk.IconSize.LARGE_TOOLBAR, + stock_id=Gtk.STOCK_CLOSE, + ) button.connect("clicked", lambda b: dialog.destroy()) - button_box.pack_end(button, expand=False, fill=False, - padding=DIALOG_PADDING) - top_vbox.pack_end(button_box, expand=False, fill=False, - padding=DIALOG_PADDING) + button_box.pack_end( + button, expand=False, fill=False, padding=DIALOG_PADDING + ) + top_vbox.pack_end( + button_box, expand=False, fill=False, padding=DIALOG_PADDING + ) dialog.add(top_vbox) if "\n" in text: label.set_line_wrap(False) @@ -513,7 +565,9 @@ def run_scrolled_dialog(text, title=None): button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) button_box.pack_end(button, expand=False, fill=False, padding=0) button_box.show() - main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=DIALOG_SUB_PADDING) + main_vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=DIALOG_SUB_PADDING + ) main_vbox.pack_start(scrolled, expand=True, fill=True, padding=0) main_vbox.pack_end(button_box, expand=False, fill=False, padding=0) main_vbox.show() @@ -523,11 +577,14 @@ def run_scrolled_dialog(text, title=None): return False -def get_naming_dialog(label, checker, ok_tip=None, - err_tip=None): +def get_naming_dialog(label, checker, ok_tip=None, err_tip=None): """Return a dialog, container, and entry for entering a name.""" - button_list = (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) + button_list = ( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ) parent_window = get_dialog_parent() dialog = Gtk.Dialog(buttons=button_list) dialog.set_transient_for(parent_window) @@ -540,32 +597,38 @@ def get_naming_dialog(label, checker, ok_tip=None, name_label.show() name_entry = Gtk.Entry() name_entry.set_tooltip_text(ok_tip) - name_entry.connect("changed", _name_checker, checker, ok_button, - ok_tip, err_tip) name_entry.connect( - "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT)) + "changed", _name_checker, checker, ok_button, ok_tip, err_tip + ) + name_entry.connect( + "activate", lambda b: dialog.response(Gtk.ResponseType.ACCEPT) + ) name_entry.show() - name_hbox.pack_start(name_label, expand=False, fill=False, - padding=DIALOG_SUB_PADDING) - name_hbox.pack_start(name_entry, expand=False, fill=True, - padding=DIALOG_SUB_PADDING) + name_hbox.pack_start( + name_label, expand=False, fill=False, padding=DIALOG_SUB_PADDING + ) + name_hbox.pack_start( + name_entry, expand=False, fill=True, padding=DIALOG_SUB_PADDING + ) name_hbox.show() - main_vbox.pack_start(name_hbox, expand=False, fill=True, - padding=DIALOG_PADDING) + main_vbox.pack_start( + name_hbox, expand=False, fill=True, padding=DIALOG_PADDING + ) main_vbox.show() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(main_vbox, expand=False, fill=True, - padding=DIALOG_PADDING) + hbox.pack_start(main_vbox, expand=False, fill=True, padding=DIALOG_PADDING) hbox.show() - dialog.vbox.pack_start(hbox, expand=False, fill=True, - padding=DIALOG_PADDING) + dialog.vbox.pack_start( + hbox, expand=False, fill=True, padding=DIALOG_PADDING + ) return dialog, main_vbox, name_entry def _name_checker(entry, checker, ok_button, ok_tip, err_tip): good_colour = ok_button.style.text[Gtk.StateType.NORMAL] bad_colour = metomi.rose.gtk.util.color_parse( - metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR) + metomi.rose.config_editor.COLOUR_VARIABLE_TEXT_ERROR + ) name = entry.get_text() if checker(name): entry.modify_text(Gtk.StateType.NORMAL, good_colour) @@ -581,10 +644,16 @@ def _name_checker(entry, checker, ok_button, ok_tip, err_tip): def run_choices_dialog(text, choices, title=None): """Run a dialog for choosing between a set of options.""" parent_window = get_dialog_parent() - dialog = Gtk.Dialog(title, - buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), - parent=parent_window) + dialog = Gtk.Dialog( + title, + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ), + parent=parent_window, + ) dialog.set_border_width(DIALOG_SUB_PADDING) label = Gtk.Label() try: @@ -598,17 +667,19 @@ def run_choices_dialog(text, choices, title=None): if len(choices) < 5: for i, choice in enumerate(choices): group = None + radio_button = Gtk.RadioButton( + group, label=choice, use_underline=False + ) if i > 0: group = radio_button if i == 1: radio_button.set_active(True) - radio_button = Gtk.RadioButton(group, - label=choice, - use_underline=False) - dialog.vbox.pack_start(radio_button, expand=False, fill=False, padding=0) - getter = (lambda: - [b.get_label() for b in radio_button.get_group() - if b.get_active()].pop()) + dialog.vbox.pack_start( + radio_button, expand=False, fill=False, padding=0 + ) + getter = lambda: [ + b.get_label() for b in radio_button.get_group() if b.get_active() + ].pop() else: combo_box = Gtk.ComboBoxText() for choice in choices: @@ -629,10 +700,16 @@ def run_choices_dialog(text, choices, title=None): def run_edit_dialog(text, finish_hook=None, title=None): """Run a dialog for editing some text.""" parent_window = get_dialog_parent() - dialog = Gtk.Dialog(title, - buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT), - parent=parent_window) + dialog = Gtk.Dialog( + title, + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.REJECT, + Gtk.STOCK_OK, + Gtk.ResponseType.ACCEPT, + ), + parent=parent_window, + ) dialog.set_border_width(DIALOG_SUB_PADDING) @@ -651,11 +728,10 @@ def run_edit_dialog(text, finish_hook=None, title=None): scrolled_window.add_with_viewport(text_view) scrolled_window.show() - dialog.vbox.pack_start(scrolled_window, expand=True, fill=True, - padding=0) - get_text = lambda: text_buffer.get_text(text_buffer.get_start_iter(), - text_buffer.get_end_iter(), - False) + dialog.vbox.pack_start(scrolled_window, expand=True, fill=True, padding=0) + get_text = lambda: text_buffer.get_text( + text_buffer.get_start_iter(), text_buffer.get_end_iter(), False + ) max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX # defines the minimum acceptable size for the edit dialog @@ -665,11 +741,15 @@ def run_edit_dialog(text, finish_hook=None, title=None): dialog.show() start_width = dialog.get_preferred_size().natural_size.width start_height = dialog.get_preferred_size().natural_size.height - scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) end_width = dialog.get_preferred_size().natural_size.width end_height = dialog.get_preferred_size().natural_size.height - my_size = (max([start_width, end_width, min_size[0]]) + 20, - max([start_height, end_height, min_size[1]]) + 20) + my_size = ( + max([start_width, end_width, min_size[0]]) + 20, + max([start_height, end_height, min_size[1]]) + 20, + ) new_size = [-1, -1] for i in [0, 1]: new_size[i] = min([my_size[i], max_size[i]]) @@ -715,9 +795,9 @@ def get_dialog_parent(): def set_exception_hook_dialog(keep_alive=False): """Set a dialog to run once an uncaught exception occurs.""" prev_hook = sys.excepthook - sys.excepthook = (lambda c, i, t: - _run_exception_dialog(c, i, t, prev_hook, - keep_alive)) + sys.excepthook = lambda c, i, t: _run_exception_dialog( + c, i, t, prev_hook, keep_alive + ) def _configure_scroll(dialog, scrolled_window): @@ -726,17 +806,25 @@ def _configure_scroll(dialog, scrolled_window): max_size = metomi.rose.config_editor.SIZE_MACRO_DIALOG_MAX my_size = dialog.get_size() new_size = [-1, -1] - for i, scrollbar_cls in [(0, Gtk.Scrollbar.new(orientation=Gtk.Orientation.VERTICAL)), (1, Gtk.Scrollbar.new(orientation=Gtk.Orientation.HORIZONTAL))]: + for i, scrollbar_cls in [ + (0, Gtk.Scrollbar.new(orientation=Gtk.Orientation.VERTICAL)), + (1, Gtk.Scrollbar.new(orientation=Gtk.Orientation.HORIZONTAL)), + ]: new_size[i] = min(my_size[i], max_size[i]) if new_size[i] < max_size[i]: # Factor in existence of a scrollbar in the other dimension. # For horizontal dimension, add width of vertical scroll bar + 2 # For vertical dimension, add height of horizontal scroll bar + 2 - new_size[i] += getattr( - scrollbar_cls.get_preferred_size().natural_size, - ["width", "height"][i] - ) + 2 - scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + new_size[i] += ( + getattr( + scrollbar_cls.get_preferred_size().natural_size, + ["width", "height"][i], + ) + + 2 + ) + scrolled_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) dialog.set_default_size(*new_size) @@ -746,17 +834,16 @@ def _run_exception_dialog(exc_class, exc_inst, tback, hook, keep_alive): return False hook(exc_class, exc_inst, tback) program_name = metomi.rose.resource.ResourceLocator().get_util_name() - tback_text = metomi.rose.gtk.util.safe_str("".join(traceback.format_tb(tback))) + tback_text = metomi.rose.gtk.util.safe_str( + "".join(traceback.format_tb(tback)) + ) shutdown_text = DIALOG_TEXT_SHUTTING_DOWN if keep_alive: shutdown_text = DIALOG_TEXT_SHUTDOWN_ASAP - text = DIALOG_TEXT_UNCAUGHT_EXCEPTION.format(program_name, - shutdown_text, - exc_class.__name__, - exc_inst, - tback_text) - run_dialog(DIALOG_TYPE_ERROR, text, - title=DIALOG_TITLE_UNCAUGHT_EXCEPTION) + text = DIALOG_TEXT_UNCAUGHT_EXCEPTION.format( + program_name, shutdown_text, exc_class.__name__, exc_inst, tback_text + ) + run_dialog(DIALOG_TYPE_ERROR, text, title=DIALOG_TITLE_UNCAUGHT_EXCEPTION) if not keep_alive: try: Gtk.main_quit() diff --git a/metomi/rose/gtk/run.py b/metomi/rose/gtk/run.py deleted file mode 100644 index ee77986d3..000000000 --- a/metomi/rose/gtk/run.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (C) 2012-2020 British Crown (Met Office) & Contributors. -# -# This file is part of Rose, a framework for meteorological suites. -# -# Rose is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Rose is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Rose. If not, see . -# ----------------------------------------------------------------------------- -"""Miscellaneous gtk mini-applications.""" - -import multiprocessing -from subprocess import check_output - -from metomi.rose.gtk.dialog import DialogProcess, run_dialog, DIALOG_TYPE_WARNING -from metomi.rose.opt_parse import RoseOptionParser -# from metomi.rose.suite_engine_procs.cylc import CylcProcessor -# from metomi.rose.suite_run import SuiteRunner -from metomi.rose.reporter import Reporter, ReporterContextQueue - - -def run_suite(*args): - """Run "rose suite-run [args]" with a GTK dialog.""" - # Set up reporter - queue = multiprocessing.Manager().Queue() - verbosity = Reporter.VV - out_ctx = ReporterContextQueue(Reporter.KIND_OUT, verbosity, queue=queue) - err_ctx = ReporterContextQueue(Reporter.KIND_ERR, verbosity, queue=queue) - event_handler = Reporter(contexts={"stdout": out_ctx, "stderr": err_ctx}, - raise_on_exc=True) - - # Parse arguments - suite_runner = SuiteRunner(event_handler=event_handler) - - # Don't use rose-suite run if Cylc Version is 8.*: - if suite_runner.suite_engine_proc.get_version()[0] == '8': - run_dialog( - DIALOG_TYPE_WARNING, - '`rose suite-run` does not work with Cylc 8 workflows: ' - 'Use `cylc install`.', - 'Cylc Version == 8' - ) - return None - - prog = "rose suite-run" - description = prog - if args: - description += " " + suite_runner.popen.list_to_shell_str(args) - opt_parse = RoseOptionParser(prog=prog) - opt_parse.add_my_options(*suite_runner.OPTIONS) - opts, args = opt_parse.parse_args(list(args)) - - # Invoke the command with a GTK dialog - dialog_process = DialogProcess([suite_runner, opts, args], - description=description, - modal=False, - event_queue=queue) - return dialog_process.run() diff --git a/metomi/rose/gtk/splash.py b/metomi/rose/gtk/splash.py index c8b649540..df24aecee 100755 --- a/metomi/rose/gtk/splash.py +++ b/metomi/rose/gtk/splash.py @@ -28,8 +28,9 @@ import time import gi + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk +from gi.repository import Gtk from gi.repository import GObject from gi.repository import Pango @@ -38,7 +39,6 @@ class SplashScreen(Gtk.Window): - """Run a splash screen that receives update information.""" BACKGROUND_COLOUR = "white" # Same as logo background. @@ -56,9 +56,11 @@ def __init__(self, logo_path, title, total_number_of_events): self.set_decorated(False) self.stopped = False self.set_icon(metomi.rose.gtk.util.get_icon()) - self.modify_bg(Gtk.StateType.NORMAL, - metomi.rose.gtk.util.color_parse(self.BACKGROUND_COLOUR)) - self.set_gravity(5) # same as gravity center + self.modify_bg( + Gtk.StateType.NORMAL, + metomi.rose.gtk.util.color_parse(self.BACKGROUND_COLOUR), + ) + self.set_gravity(5) # same as gravity center self.set_position(Gtk.WindowPosition.CENTER) main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) main_vbox.show() @@ -78,12 +80,16 @@ def __init__(self, logo_path, title, total_number_of_events): self._progress_message = None self.event_count = 0.0 self.total_number_of_events = float(total_number_of_events) - progress_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=self.SUB_PADDING) + progress_hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=self.SUB_PADDING + ) progress_hbox.show() - progress_hbox.pack_start(self.progress_bar, expand=True, fill=True, - padding=self.SUB_PADDING) - main_vbox.pack_start(progress_hbox, expand=False, fill=False, - padding=self.PADDING) + progress_hbox.pack_start( + self.progress_bar, expand=True, fill=True, padding=self.SUB_PADDING + ) + main_vbox.pack_start( + progress_hbox, expand=False, fill=False, padding=self.PADDING + ) self.add(main_vbox) if self.total_number_of_events > 0: self.show() @@ -104,15 +110,17 @@ def update(self, event, no_progress=False, new_total_events=None): fraction = 1.0 else: fraction = min( - [1.0, self.event_count / self.total_number_of_events]) + [1.0, self.event_count / self.total_number_of_events] + ) self._stop_pulse() if not no_progress: GObject.idle_add(self.progress_bar.set_fraction, fraction) self._progress_fraction = fraction self.progress_bar.set_text(text) self._progress_message = text - GObject.timeout_add(self.TIME_IDLE_BEFORE_PULSE, - self._start_pulse, fraction, text) + GObject.timeout_add( + self.TIME_IDLE_BEFORE_PULSE, self._start_pulse, fraction, text + ) if fraction == 1.0 and not no_progress: GObject.timeout_add(self.TIME_WAIT_FINISH, self.finish) while Gtk.events_pending(): @@ -120,12 +128,13 @@ def update(self, event, no_progress=False, new_total_events=None): def _start_pulse(self, idle_fraction, idle_message): """Start the progress bar pulsing (moving side-to-side).""" - if (self._progress_message != idle_message or - self._progress_fraction != idle_fraction): + if ( + self._progress_message != idle_message + or self._progress_fraction != idle_fraction + ): return False self._is_progress_bar_pulsing = True - GObject.timeout_add(self.TIME_INTERVAL_PULSE, - self._pulse) + GObject.timeout_add(self.TIME_INTERVAL_PULSE, self._pulse) return False def _stop_pulse(self): @@ -146,7 +155,6 @@ def finish(self): class NullSplashScreenProcess(object): - """Implement a null interface similar to SplashScreenProcess.""" def __init__(self, *args): @@ -163,7 +171,6 @@ def stop(self): class SplashScreenProcess(object): - """Run a separate process that launches a splash screen. Communicate via the update method. @@ -219,7 +226,9 @@ def _update_buffered(self, *args, **kwargs): __call__ = update def start(self): - self.process = Popen(["rose", "launch-splash-screen"] + list(self.args), stdin=PIPE) + self.process = Popen( + ["rose", "launch-splash-screen"] + list(self.args), stdin=PIPE + ) def stop(self): self.process.kill() @@ -227,7 +236,6 @@ def stop(self): class SplashScreenUpdaterThread(threading.Thread): - """Update a splash screen using info from the stdin file object.""" def __init__(self, window, stop_event, stdin): @@ -257,7 +265,7 @@ def run(self): self.stop_event.set() continue GObject.idle_add(self._update_splash_screen, update_input) - + def stop(self): try: Gtk.main_quit() @@ -280,11 +288,12 @@ def _update_splash_screen(self, update_input): def main(argv=sys.argv): """Start splash screen.""" - sys.path.append(os.getenv('ROSE_HOME')) + sys.path.append(os.getenv("ROSE_HOME")) splash_screen = SplashScreen(argv[0], argv[1], argv[2]) stop_event = threading.Event() update_thread = SplashScreenUpdaterThread( - splash_screen, stop_event, sys.stdin) + splash_screen, stop_event, sys.stdin + ) update_thread.start() try: Gtk.main() diff --git a/metomi/rose/gtk/util.py b/metomi/rose/gtk/util.py index d7bc9e772..881251ab8 100644 --- a/metomi/rose/gtk/util.py +++ b/metomi/rose/gtk/util.py @@ -26,6 +26,7 @@ import webbrowser import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GdkPixbuf from gi.repository import GObject @@ -39,17 +40,20 @@ REC_HYPERLINK_ID_OR_URL = re.compile( r"""(?P\b) (?P[\w:-]+=\w+|https?://[^\s<]+) - (?P\b)""", re.X) -MARKUP_URL_HTML = (r"""\g""" + - r"""\g""" + - r"""\g""") -MARKUP_URL_UNDERLINE = (r"""\g""" + - r"""\g""" + - r"""\g""") + (?P\b)""", + re.X, +) +MARKUP_URL_HTML = ( + r"""\g""" + + r"""\g""" + + r"""\g""" +) +MARKUP_URL_UNDERLINE = ( + r"""\g""" + r"""\g""" + r"""\g""" +) class ColourParseError(ValueError): - """An exception raised when gtk colour parsing fails.""" def __str__(self): @@ -57,12 +61,18 @@ def __str__(self): class CustomButton(Gtk.Button): - """Returns a custom Gtk.Button.""" - def __init__(self, label=None, stock_id=None, - size=Gtk.IconSize.SMALL_TOOLBAR, tip_text=None, - as_tool=False, icon_at_start=False, has_menu=False): + def __init__( + self, + label=None, + stock_id=None, + size=Gtk.IconSize.SMALL_TOOLBAR, + tip_text=None, + as_tool=False, + icon_at_start=False, + has_menu=False, + ): self.hbox = Gtk.Box() self.size = size self.as_tool = as_tool @@ -73,11 +83,13 @@ def __init__(self, label=None, stock_id=None, self.label.show() if self.icon_at_start: - self.hbox.pack_end(self.label, expand=False, fill=False, - padding=5) + self.hbox.pack_end( + self.label, expand=False, fill=False, padding=5 + ) else: - self.hbox.pack_start(self.label, expand=False, fill=False, - padding=5) + self.hbox.pack_start( + self.label, expand=False, fill=False, padding=5 + ) if stock_id is not None: self.stock_id = stock_id self.icon = Gtk.Image() @@ -87,17 +99,18 @@ def __init__(self, label=None, stock_id=None, self.icon.set_from_icon_name(stock_id, size) self.icon.show() if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False, - padding=0) + self.hbox.pack_start( + self.icon, expand=False, fill=False, padding=0 + ) else: - self.hbox.pack_end(self.icon, expand=False, fill=False, - padding=0) + self.hbox.pack_end( + self.icon, expand=False, fill=False, padding=0 + ) if has_menu: # not sure if this is correct arrow = Gtk.Image.new_from_icon_name("pan-down-symbolic", size) arrow.show() - self.hbox.pack_end(arrow, expand=False, fill=False, - padding=0) + self.hbox.pack_end(arrow, expand=False, fill=False, padding=0) self.hbox.reorder_child(arrow, 0) self.hbox.show() super(CustomButton, self).__init__() @@ -116,7 +129,9 @@ def set_stock_id(self, stock_id): self.icon.set_from_stock(stock_id, self.size) self.stock_id = stock_id if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) + self.hbox.pack_start( + self.icon, expand=False, fill=False, padding=0 + ) else: self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) return False @@ -135,16 +150,18 @@ def position_menu(self, menu, widget): class CustomExpandButton(Gtk.Button): - """Custom button for expanding/hiding something""" - def __init__(self, expander_function=None, - label=None, - size=Gtk.IconSize.SMALL_TOOLBAR, - tip_text=None, - as_tool=False, - icon_at_start=False, - minimised=True): + def __init__( + self, + expander_function=None, + label=None, + size=Gtk.IconSize.SMALL_TOOLBAR, + tip_text=None, + as_tool=False, + icon_at_start=False, + minimised=True, + ): self.expander_function = expander_function self.minimised = minimised @@ -168,16 +185,20 @@ def __init__(self, expander_function=None, self.label.show() if self.icon_at_start: - self.hbox.pack_end(self.label, expand=False, fill=False, - padding=5) + self.hbox.pack_end( + self.label, expand=False, fill=False, padding=5 + ) else: - self.hbox.pack_start(self.label, expand=False, fill=False, - padding=5) + self.hbox.pack_start( + self.label, expand=False, fill=False, padding=5 + ) self.icon = Gtk.Image() self.icon.set_from_stock(self.stock_id, size) self.icon.show() if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) + self.hbox.pack_start( + self.icon, expand=False, fill=False, padding=0 + ) else: self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) self.hbox.show() @@ -199,7 +220,9 @@ def set_stock_id(self, stock_id): self.icon.set_from_stock(stock_id, self.size) self.stock_id = stock_id if self.icon_at_start: - self.hbox.pack_start(self.icon, expand=False, fill=False, padding=0) + self.hbox.pack_start( + self.icon, expand=False, fill=False, padding=0 + ) else: self.hbox.pack_end(self.icon, expand=False, fill=False, padding=0) return False @@ -224,12 +247,17 @@ def toggle(self, minimise=None): class CustomMenuButton(Gtk.MenuToolButton): - """Custom wrapper for the gtk Menu Tool Button.""" - def __init__(self, label=None, stock_id=None, - size=Gtk.IconSize.SMALL_TOOLBAR, tip_text=None, - menu_items=[], menu_funcs=[]): + def __init__( + self, + label=None, + stock_id=None, + size=Gtk.IconSize.SMALL_TOOLBAR, + tip_text=None, + menu_items=[], + menu_funcs=[], + ): if stock_id is not None: self.stock_id = stock_id self.icon = Gtk.Image() @@ -245,12 +273,14 @@ def __init__(self, label=None, stock_id=None, new_item = Gtk.MenuItem(name) else: new_item_box = Gtk.Box() - new_item_icon = Gtk.Image.new_from_icon_name(item_tuple[1], Gtk.IconSize.MENU) + new_item_icon = Gtk.Image.new_from_icon_name( + item_tuple[1], Gtk.IconSize.MENU + ) new_item_label = Gtk.Label(label=name) new_item = Gtk.MenuItem() new_item_box.pack_start(new_item_icon, False, False, 0) new_item_box.pack_start(new_item_label, False, False, 0) - Gtk.Container.add(new_item, new_item_box) + Gtk.Container.add(new_item, new_item_box) new_item._func = func new_item.connect("activate", lambda m: m._func()) new_item.show() @@ -260,7 +290,6 @@ def __init__(self, label=None, stock_id=None, class ToolBar(Gtk.Toolbar): - """An easier-to-use Gtk.Toolbar.""" def __init__(self, widgets=[], sep_on_name=[]): @@ -280,30 +309,35 @@ def __init__(self, widgets=[], sep_on_name=[]): widget.show() widget.set_tooltip_text(name) else: - widget = CustomButton(stock_id=stock, tip_text=name, - as_tool=True) + widget = CustomButton( + stock_id=stock, tip_text=name, as_tool=True + ) icon_tool_item = Gtk.ToolItem() icon_tool_item.add(widget) icon_tool_item.show() - self.item_dict[name] = {"tip": name, "widget": widget, - "func": None} + self.item_dict[name] = { + "tip": name, + "widget": widget, + "func": None, + } self.insert(icon_tool_item, 0) def set_widget_function(self, name, function, args=[]): self.item_dict[name]["widget"].args = args if len(args) > 0: - self.item_dict[name]["widget"].connect("clicked", - lambda b: function(*b.args)) + self.item_dict[name]["widget"].connect( + "clicked", lambda b: function(*b.args) + ) else: - self.item_dict[name]["widget"].connect("clicked", - lambda b: function()) + self.item_dict[name]["widget"].connect( + "clicked", lambda b: function() + ) def set_widget_sensitive(self, name, is_sensitive): self.item_dict[name]["widget"].set_sensitive(is_sensitive) class AsyncStatusbar(Gtk.Statusbar): - """Wrapper class to add polling a file to statusbar API.""" def __init__(self, *args): @@ -339,7 +373,6 @@ def put(self, message, instant=False): class AsyncLabel(Gtk.Label): - """Wrapper class to add polling a file to label API.""" def __init__(self, *args): @@ -374,7 +407,6 @@ def put(self, message, instant=False): class ThreadedProgressBar(Gtk.ProgressBar): - """Wrapper class to allow threaded progress bar pulsing.""" def __init__(self, *args, **kwargs): @@ -406,7 +438,6 @@ def stop_pulsing(self): class Notebook(Gtk.Notebook): - """Wrapper class to improve the Gtk.Notebook API.""" def __init__(self, *args): @@ -458,7 +489,6 @@ def set_tab_label_packing(self, page, tab_labelwidget): class TooltipTreeView(Gtk.TreeView): - """Wrapper class for Gtk.TreeView with a better tooltip API. It takes two keyword arguments, model as in Gtk.TreeView and @@ -473,14 +503,15 @@ class TooltipTreeView(Gtk.TreeView): """ - def __init__(self, model=None, get_tooltip_func=None, - multiple_selection=False): + def __init__( + self, model=None, get_tooltip_func=None, multiple_selection=False + ): super(TooltipTreeView, self).__init__(model) self.get_tooltip = get_tooltip_func self.set_has_tooltip(True) self._last_tooltip_path = None self._last_tooltip_column = None - self.connect('query-tooltip', self._handle_tooltip) + self.connect("query-tooltip", self._handle_tooltip) if multiple_selection: self.set_rubber_banding(True) self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) @@ -494,8 +525,10 @@ def _handle_tooltip(self, view, xpos, ypos, kbd_ctx, tip): path, column = pathinfo[:2] if path is None: return False - if (path != self._last_tooltip_path or - column != self._last_tooltip_column): + if ( + path != self._last_tooltip_path + or column != self._last_tooltip_column + ): self._last_tooltip_path = path self._last_tooltip_column = column return False @@ -507,7 +540,6 @@ def _handle_tooltip(self, view, xpos, ypos, kbd_ctx, tip): class TreeModelSortUtil(object): - """This class contains useful sorting methods for TreeModelSort. Arguments: @@ -549,9 +581,11 @@ def cmp_(self, value1, value2): value1 = "None" if value2 is None: value2 = "None" - if (isinstance(value1, str) and isinstance(value2, str)): + if isinstance(value1, str) and isinstance(value2, str): if value1.isdigit() and value2.isdigit(): - return (float(value1) > float(value2)) - (float(value1) < float(value2)) + return (float(value1) > float(value2)) - ( + float(value1) < float(value2) + ) return metomi.rose.config.sort_settings(value1, value2) return (value1 > value2) - (value1 < value2) @@ -560,8 +594,10 @@ def handle_sort_column_change(self, model): id_, order = model.get_sort_column_id() if id_ is None and order is None: return False - if (self._sort_columns_stored and - self._sort_columns_stored[0][0] == id_): + if ( + self._sort_columns_stored + and self._sort_columns_stored[0][0] == id_ + ): self._sort_columns_stored.pop(0) self._sort_columns_stored.insert(0, (id_, order)) if len(self._sort_columns_stored) > 2: @@ -604,7 +640,9 @@ def color_parse(color_specification): try: return Gdk.color_parse(color_specification) except ValueError: - metomi.rose.reporter.Reporter().report(ColourParseError(color_specification)) + metomi.rose.reporter.Reporter().report( + ColourParseError(color_specification) + ) # Return a noticeable colour. return Gdk.color_parse("#0000FF") # Blue @@ -618,8 +656,9 @@ def get_hyperlink_label(text, search_func=lambda i: False): except GLib.GError: label.set_text(text) else: - label.connect("activate-link", - lambda l, u: handle_link(u, search_func)) + label.connect( + "activate-link", lambda _, u: handle_link(u, search_func) + ) text = REC_HYPERLINK_ID_OR_URL.sub(MARKUP_URL_HTML, text) label.set_markup(text) return label @@ -633,7 +672,8 @@ def get_icon(system="rose"): pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)) except Exception: icon_path = locator.locate( - "etc/images/{0}-icon-trim.png".format(system)) + "etc/images/{0}-icon-trim.png".format(system) + ) pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)) return pixbuf @@ -661,7 +701,7 @@ def extract_link(label, search_function): if text[upper_bound].isspace(): break upper_bound += 1 - link = text[lower_bound: upper_bound] + link = text[lower_bound:upper_bound] if any(c.isspace() for c in link): return None handle_link(link, search_function, handle_web=True) @@ -688,18 +728,19 @@ def setup_stock_icons(): """Setup any additional 'stock' icons.""" new_icon_factory = Gtk.IconFactory() locator = metomi.rose.resource.ResourceLocator(paths=sys.path) - for png_icon_name in ["gnome_add", - "gnome_add_errors", - "gnome_add_warnings", - "gnome_package_system", - "gnome_package_system_errors", - "gnome_package_system_warnings"]: + for png_icon_name in [ + "gnome_add", + "gnome_add_errors", + "gnome_add_warnings", + "gnome_package_system", + "gnome_package_system_errors", + "gnome_package_system_warnings", + ]: ifile = png_icon_name + ".png" istring = png_icon_name.replace("_", "-") path = locator.locate("etc/images/rose-config-edit/" + ifile) pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(path)) - new_icon_factory.add("rose-gtk-" + istring, - Gtk.IconSet(pixbuf)) + new_icon_factory.add("rose-gtk-" + istring, Gtk.IconSet(pixbuf)) exp_icon_pixbuf = get_icon() new_icon_factory.add("rose-exp-logo", Gtk.IconSet(exp_icon_pixbuf)) new_icon_factory.add_default() diff --git a/metomi/rose/reporter.py b/metomi/rose/reporter.py index 44dc37e3b..ce5efe896 100644 --- a/metomi/rose/reporter.py +++ b/metomi/rose/reporter.py @@ -138,7 +138,7 @@ def report(self, message, kind=None, level=None, prefix=None, clip=None): if isinstance(message, bytes): message = message.decode() if callable(self.event_handler): - ret = self.event_handler(message, kind, level, prefix, clip) + ret = self.event_handler(message, kind, level, prefix, clip) return ret if isinstance(message, Event): if kind is None: @@ -268,9 +268,7 @@ def _tty_colour_err(self, str_): return str_ - class ReporterContextQueue(ReporterContext): - """A context for the reporter object. It has the following attributes: @@ -318,10 +316,9 @@ def _send_pending_messages(self): break else: del self._messages_pending[0] - - -class Event: + +class Event: """A base class for events suitable for feeding into a Reporter.""" VV = Reporter.VV diff --git a/metomi/rose/resource.py b/metomi/rose/resource.py index 5826e5e82..38f5fd222 100644 --- a/metomi/rose/resource.py +++ b/metomi/rose/resource.py @@ -34,8 +34,8 @@ MODULES = {} -class ResourceError(Exception): +class ResourceError(Exception): """A named resource not found.""" def __init__(self, key): @@ -209,11 +209,16 @@ def import_object( for filename in module_files: sys.path.insert(0, os.path.dirname(filename)) try: - spec = importlib.util.spec_from_file_location(filename, filename) - if not filename in MODULES: - spec = importlib.util.spec_from_file_location(filename, filename) + spec = ( + importlib.util.spec_from_file_location(filename, filename) + ) + if filename not in MODULES: + spec = ( + importlib.util + .spec_from_file_location(filename, filename) + ) module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + spec.loader.exec_module(module) MODULES[filename] = module else: module = MODULES[filename] From 3ef6e8df953449a26367030beaa2a2a7a526969a Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 27 Nov 2024 16:37:43 +0000 Subject: [PATCH 36/42] Fixes MyPy error in data.py where the exc variable was being overwritten --- metomi/rose/config_editor/data.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/metomi/rose/config_editor/data.py b/metomi/rose/config_editor/data.py index 08ac11442..6566a9222 100644 --- a/metomi/rose/config_editor/data.py +++ b/metomi/rose/config_editor/data.py @@ -534,8 +534,10 @@ def load_optional_configs(self, config_directory): if opt_exceptions: err_text = "" err_format = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS_FORMAT - for path, exc in sorted(opt_exceptions.items()): - err_text += err_format.format(path, type(exc).__name__, exc) + for path, exception in sorted(opt_exceptions.items()): + err_text += err_format.format( + path, type(exception).__name__, exception + ) err_text = err_text.rstrip() text = metomi.rose.config_editor.ERROR_LOAD_OPT_CONFS.format( err_text From b7b6f00c3a012bc7eee7214209efcf75dbb81d61 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Wed, 27 Nov 2024 18:02:56 +0000 Subject: [PATCH 37/42] Updates the test workflow to install the required dependencies for rose-edit --- .github/workflows/test.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89247cbba..a5694957b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -114,7 +114,10 @@ jobs: coreutils \ gnu-sed \ sqlite3 \ - subversion + subversion \ + gtk+3 \ + gobject-introspection \ + adwaita-icon-theme # add GNU coreutils and sed to the user PATH (for actions steps) # (see instructions in brew install output) @@ -139,12 +142,12 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update - sudo apt-get install -y shellcheck sqlite3 at graphviz graphviz-dev + sudo apt-get install -y shellcheck sqlite3 at graphviz graphviz-dev libgirepository1.0-dev python3-gi libgtk-3-dev gobject-introspection gir1.2-gtk-3.0 - name: Install Rose working-directory: rose run: | - pip install ."[tests,docs${{ (startsWith(matrix.os, 'ubuntu') && ',graph,rosa') || '' }}]" + pip install ."[tests,docs,rose-edit${{ (startsWith(matrix.os, 'ubuntu') && ',graph,rosa') || '' }}]" yarn install - name: Install Cylc @@ -244,11 +247,11 @@ jobs: - name: install graphviz run: | sudo apt-get update - sudo apt-get install -y graphviz pkg-config libgraphviz-dev + sudo apt-get install -y graphviz pkg-config libgraphviz-dev libgirepository1.0-dev python3-gi libgtk-3-dev gobject-introspection gir1.2-gtk-3.0 - name: Install Rose run: | - pip install -e .[docs,graph] + pip install -e .[docs,graph,rose-edit] - name: Install Cylc uses: cylc/release-actions/install-cylc-components@v1 From a7ce70a43d007441dca7fbeed081bcaed0ac3b93 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 29 Oct 2024 15:49:15 +0000 Subject: [PATCH 38/42] Removes references in the README and the docs to no GUI being available for Rose 2.0 Removes the extra rose config-edit section from the rose api docs and prevented the splash-screen entry points getting into the docs --- README.md | 2 +- metomi/rose/rose.py | 2 ++ sphinx/api/command-reference.rst | 12 ------------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 80352a01f..db2c71a7c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Rose: a framework for managing and running meteorological suites. #### Rose 2 - Python 3 -- No GUIs +- PyGObject GUI - Web-based GUIs will follow in later Rose 2 releases - `master` branch in the source code diff --git a/metomi/rose/rose.py b/metomi/rose/rose.py index d60c16fc3..68619ca26 100644 --- a/metomi/rose/rose.py +++ b/metomi/rose/rose.py @@ -319,6 +319,8 @@ def _doc(ns): continue if (ns, sub_cmd) in DEAD_ENDS: continue + if (ns == 'rose') and (sub_cmd == 'launch-splash-screen'): + continue print('\n==================================================') print(f'{ns} {sub_cmd}') print('==================================================\n') diff --git a/sphinx/api/command-reference.rst b/sphinx/api/command-reference.rst index 2b479a7db..2335f6b48 100644 --- a/sphinx/api/command-reference.rst +++ b/sphinx/api/command-reference.rst @@ -11,18 +11,6 @@ Rose Commands ---- -.. _command-rose-config-edit: - -rose config-edit -^^^^^^^^^^^^^^^^ - -.. warning:: - - The Rose Edit GUI has not yet been reimplemented in Rose 2. - - The old Rose 2019 (Python 2) GUI remains compatible with Rose 2 - configurations. - .. _command-rose-suite-run: rose suite-run From 924d3061d838c97c08cff4bb90f5b606875bdeba Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 10 Dec 2024 14:08:25 +0000 Subject: [PATCH 39/42] Prevents the launch-splash-screen internal cli being shown when using rose help --- metomi/rose/rose.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metomi/rose/rose.py b/metomi/rose/rose.py index 68619ca26..a92eca958 100644 --- a/metomi/rose/rose.py +++ b/metomi/rose/rose.py @@ -243,6 +243,8 @@ def load_entry_point(entry_point: 'EntryPoint'): def _get_sub_cmds(ns): for ns_, sub_cmd in set(PYTHON_SUB_CMDS) | BASH_SUB_CMDS: if ns_ == ns: + if (ns == 'rose') and (sub_cmd == 'launch-splash-screen'): + continue yield sub_cmd From d520f043bfa9def942165839051d2d7f6247625d Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 14 Jan 2025 14:31:56 +0000 Subject: [PATCH 40/42] Fixes file chooser widget import --- metomi/rose/config_editor/valuewidget/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metomi/rose/config_editor/valuewidget/__init__.py b/metomi/rose/config_editor/valuewidget/__init__.py index f276cc66b..568422635 100644 --- a/metomi/rose/config_editor/valuewidget/__init__.py +++ b/metomi/rose/config_editor/valuewidget/__init__.py @@ -28,6 +28,8 @@ from . import booltoggle from . import character from . import combobox +# flake8: noqa: F401 +from . import files from . import intspin from . import meta from . import radiobuttons From 61ae8258c25985a65a68fe07a8b48da7b8341ef0 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 14 Jan 2025 14:39:58 +0000 Subject: [PATCH 41/42] Fixes multiline text box usage of Gtk.TextBuffer.get_text() --- metomi/rose/config_editor/valuewidget/text.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/metomi/rose/config_editor/valuewidget/text.py b/metomi/rose/config_editor/valuewidget/text.py index 4e38f874f..59b96652e 100644 --- a/metomi/rose/config_editor/valuewidget/text.py +++ b/metomi/rose/config_editor/valuewidget/text.py @@ -137,7 +137,13 @@ def set_focus_index(self, focus_index=None): self.entrybuffer.place_cursor(iter_) def setter(self, widget): - text = widget.get_text(widget.get_start_iter(), widget.get_end_iter()) + print(widget) + text = Gtk.TextBuffer.get_text( + widget, + widget.get_start_iter(), + widget.get_end_iter(), + False + ) if text != self.value: self.value = text self.set_value(self.value) From 3704bd5a189241520e63c97f62f38d218aec9742 Mon Sep 17 00:00:00 2001 From: Dimitrios Theodorakis Date: Tue, 14 Jan 2025 14:51:37 +0000 Subject: [PATCH 42/42] Fixes repeat real array bug where numbers were turned into strings --- metomi/rose/config_editor/valuewidget/array/entry.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/metomi/rose/config_editor/valuewidget/array/entry.py b/metomi/rose/config_editor/valuewidget/array/entry.py index 78ffd97d7..faceb997c 100644 --- a/metomi/rose/config_editor/valuewidget/array/entry.py +++ b/metomi/rose/config_editor/valuewidget/array/entry.py @@ -462,9 +462,14 @@ def setter(self, widget): # Prevent str without "" breaking the underlying Python syntax for e in self.entries: v = e.get_text() + try: + float(v) + v_is_float = True + except: + v_is_float = False if v in ("False", "True"): # Boolean val_array.append(v) - elif (len(v) == 0) or (v[:1].isdigit()): # Empty or numeric + elif (len(v) == 0) or v_is_float: # Empty or numeric val_array.append(v) elif not v.startswith('"'): # Str - add in leading and trailing " val_array.append('"' + v + '"')