diff --git a/doc/WhatsNew.rst b/doc/WhatsNew.rst index 6f3cac234..92e8d4ebd 100644 --- a/doc/WhatsNew.rst +++ b/doc/WhatsNew.rst @@ -31,6 +31,8 @@ Ver 5.0.0 (unreleased) various MIME types - Fixed an issue with the "none-move" event, affected Crosshair plugin and "hover" event on canvas items +- Added PluginConfig plugin; allows configuration of all Ginga plugins + graphically; can enable/disable, change menu categories, etc. Ver 4.1.0 (2022-06-30) ====================== diff --git a/doc/dev_manual/modes.rst b/doc/dev_manual/modes.rst index de0bf3870..30b7bbb0e 100644 --- a/doc/dev_manual/modes.rst +++ b/doc/dev_manual/modes.rst @@ -188,5 +188,5 @@ callback. .. note:: To see what attributes are available in each event, see the ``KeyEvent``, ``PointEvent``, ``ScrollEvent``, ``PanEvent``, and - ``PinchEvent`` in the :ref:`api` (look under `ginga.Bindings`). + ``PinchEvent`` in the :ref:`api` (look under `ginga.events`). diff --git a/doc/manual/plugins.rst b/doc/manual/plugins.rst index d3e88650e..d514db948 100644 --- a/doc/manual/plugins.rst +++ b/doc/manual/plugins.rst @@ -53,6 +53,7 @@ Global plugins plugins_global/saveimage plugins_global/downloads plugins_global/loaderconfig + plugins_global/pluginconfig .. _sec-localplugins: diff --git a/doc/manual/plugins_global/figures/pluginconfig-plugin.png b/doc/manual/plugins_global/figures/pluginconfig-plugin.png new file mode 100644 index 000000000..2de260e7a Binary files /dev/null and b/doc/manual/plugins_global/figures/pluginconfig-plugin.png differ diff --git a/doc/manual/plugins_global/pluginconfig.rst b/doc/manual/plugins_global/pluginconfig.rst new file mode 100644 index 000000000..63acc0457 --- /dev/null +++ b/doc/manual/plugins_global/pluginconfig.rst @@ -0,0 +1,13 @@ +.. _sec-plugins-PluginConfig: + +PluginConfig +============ + +.. image:: figures/pluginconfig-plugin.png + :align: center + :width: 1024px + :alt: PluginConfig plugin + +.. automodapi:: ginga.rv.plugins.PluginConfig + :no-heading: + :skip: PluginConfig diff --git a/doc/ref_api.rst b/doc/ref_api.rst index 1f9cf1e6f..a2f4569cc 100644 --- a/doc/ref_api.rst +++ b/doc/ref_api.rst @@ -27,6 +27,9 @@ Reference/API .. automodapi:: ginga.Bindings :no-inheritance-diagram: +.. automodapi:: ginga.events + :no-inheritance-diagram: + .. automodapi:: ginga.rv.main :no-inheritance-diagram: diff --git a/ginga/examples/configs/general.cfg b/ginga/examples/configs/general.cfg index 7a48a7a3c..6bf6740aa 100644 --- a/ginga/examples/configs/general.cfg +++ b/ginga/examples/configs/general.cfg @@ -143,12 +143,13 @@ channel_prefix = "Image" # temp directory (as defined by Python's 'tempfile' module) #download_folder = None -# Name of a layout file to configure the GUI -layout_file = 'layout.json' - # Name of a file to configure the set of available plugins and where they # should appear -plugin_file = 'plugins.json' +plugin_file = 'plugins.yml' + +# Name of a file to configure the set of available loaders and which has +# priority +loader_file = 'loaders.yml' # Confirm program exits with a dialog? confirm_shutdown = True diff --git a/ginga/gw/Desktop.py b/ginga/gw/Desktop.py index 13fa87b9d..a39a53204 100644 --- a/ginga/gw/Desktop.py +++ b/ginga/gw/Desktop.py @@ -8,6 +8,8 @@ import math import os.path +import yaml + from ginga.misc import Bunch, Callback from ginga.gw import Widgets, Viewers from ginga.util import json @@ -696,15 +698,13 @@ def write_layout_conf(self, lo_file, layout=None): self.record_sizes() - _n, ext = os.path.splitext(lo_file) # write layout + _n, ext = os.path.splitext(lo_file) with open(lo_file, 'w') as out_f: - if ext.lower() == '.json': - out_f.write(json.dumps(layout, indent=2)) + if ext.lower() in ['.yml', '.yaml']: + out_f.write(yaml.dump(layout, indent=2)) else: - # older, python format - import pprint - pprint.pprint(layout, out_f) + out_f.write(json.dumps(layout, indent=2)) def read_layout_conf(self, lo_file): # read layout @@ -712,12 +712,10 @@ def read_layout_conf(self, lo_file): buf = in_f.read() _n, ext = os.path.splitext(lo_file) - if ext.lower() == '.json': - layout = json.loads(buf) + if ext.lower() in ['.yml', '.yaml']: + layout = yaml.safe_load(buf) else: - # older, python format - import ast - layout = ast.literal_eval(buf) + layout = json.loads(buf) return layout def build_desktop(self, layout, lo_file=None, widget_dict=None): diff --git a/ginga/gw/PluginManager.py b/ginga/gw/PluginManager.py index a78ae7dca..929b17211 100644 --- a/ginga/gw/PluginManager.py +++ b/ginga/gw/PluginManager.py @@ -40,6 +40,8 @@ def __init__(self, logger, gshell, ds, mm): self.enable_callback(name) def load_plugin(self, name, spec, chinfo=None): + if not spec.get('enabled', True): + return try: module = self.mm.get_module(spec.module) className = spec.get('klass', spec.module) diff --git a/ginga/rv/Control.py b/ginga/rv/Control.py index bc91447f1..c7277ef69 100644 --- a/ginga/rv/Control.py +++ b/ginga/rv/Control.py @@ -328,8 +328,9 @@ def add_local_plugin(self, spec): pfx = spec.get('pfx', pluginconfpfx) path = spec.get('path', None) - self.mm.load_module(spec.module, pfx=pfx, path=path) self.plugins.append(spec) + if spec.get('enabled', True): + self.mm.load_module(spec.module, pfx=pfx, path=path) except Exception as e: self.logger.error("Unable to load local plugin '%s': %s" % ( @@ -342,8 +343,9 @@ def add_global_plugin(self, spec): pfx = spec.get('pfx', pluginconfpfx) path = spec.get('path', None) - self.mm.load_module(spec.module, pfx=pfx, path=path) self.plugins.append(spec) + if spec.get('enabled', True): + self.mm.load_module(spec.module, pfx=pfx, path=path) self.gpmon.load_plugin(name, spec) @@ -352,8 +354,6 @@ def add_global_plugin(self, spec): name, str(e))) def add_plugin(self, spec): - if not spec.get('enabled', True): - return ptype = spec.get('ptype', 'local') if ptype == 'global': self.add_global_plugin(spec) @@ -1774,6 +1774,8 @@ def add_plugin_menu(self, name, spec): # NOTE: self.w.menu_plug is a ginga.Widgets wrapper if 'menu_plug' not in self.w: return + if not spec.get('enabled', True): + return category = spec.get('category', None) categories = None if category is not None: diff --git a/ginga/rv/main.py b/ginga/rv/main.py index 4980ac29a..fef820027 100644 --- a/ginga/rv/main.py +++ b/ginga/rv/main.py @@ -14,12 +14,15 @@ import logging.handlers import threading +# 3rd party +import yaml + # Local application imports from ginga.misc.Bunch import Bunch from ginga.misc import Task, ModuleManager, Settings, log import ginga.version as version import ginga.toolkit as ginga_toolkit -from ginga.util import paths, rgb_cms, json, compat, loader +from ginga.util import paths, rgb_cms, compat, loader # Catch warnings logging.captureWarnings(True) @@ -72,105 +75,128 @@ # hidden plugins, started at program initialization Bunch(module='Operations', workspace='operations', start=True, hidden=True, category='System', menu="Operations [G]", - ptype='global'), + ptype='global', enabled=True), Bunch(module='Toolbar', workspace='toolbar', start=True, - hidden=True, category='System', menu="Toolbar [G]", ptype='global'), + hidden=True, category='System', menu="Toolbar [G]", ptype='global', + enabled=True), Bunch(module='Pan', workspace='uleft', start=True, - hidden=True, category='System', menu="Pan [G]", ptype='global'), + hidden=True, category='System', menu="Pan [G]", ptype='global', + enabled=True), Bunch(module='Info', tab='Synopsis', workspace='lleft', start=True, - hidden=True, category='System', menu="Info [G]", ptype='global'), + hidden=True, category='System', menu="Info [G]", ptype='global', + enabled=True), Bunch(module='Thumbs', tab='Thumbs', workspace='right', start=True, - hidden=True, category='System', menu="Thumbs [G]", ptype='global'), + hidden=True, category='System', menu="Thumbs [G]", ptype='global', + enabled=True), Bunch(module='Contents', tab='Contents', workspace='right', start=True, - hidden=True, category='System', menu="Contents [G]", ptype='global'), + hidden=True, category='System', menu="Contents [G]", ptype='global', + enabled=True), Bunch(module='Colorbar', workspace='cbar', start=True, - hidden=True, category='System', menu="Colorbar [G]", ptype='global'), + hidden=True, category='System', menu="Colorbar [G]", ptype='global', + enabled=True), Bunch(module='Cursor', workspace='readout', start=True, - hidden=True, category='System', menu="Cursor [G]", ptype='global'), + hidden=True, category='System', menu="Cursor [G]", ptype='global', + enabled=True), Bunch(module='Errors', tab='Errors', workspace='right', start=True, - hidden=True, category='System', menu="Errors [G]", ptype='global'), + hidden=True, category='System', menu="Errors [G]", ptype='global', + enabled=True), Bunch(module='Downloads', tab='Downloads', workspace='right', start=False, - menu="Downloads [G]", category='Utils', ptype='global'), + menu="Downloads [G]", category='Utils', ptype='global', enabled=True), # optional, user-started plugins Bunch(module='Blink', tab='Blink Channels', workspace='right', start=False, - menu="Blink Channels [G]", category='Analysis', ptype='global'), + menu="Blink Channels [G]", category='Analysis', ptype='global', + enabled=True), Bunch(module='Blink', workspace='dialogs', menu='Blink Images', - category='Analysis', ptype='local'), + category='Analysis', ptype='local', enabled=True), Bunch(module='Crosshair', workspace='left', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='Cuts', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='LineProfile', workspace='dialogs', - category='Analysis.Datacube', ptype='local'), + category='Analysis.Datacube', ptype='local', enabled=True), Bunch(module='Histogram', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='Overlays', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='Pick', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='PixTable', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='TVMark', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='TVMask', workspace='dialogs', category='Analysis', - ptype='local'), + ptype='local', enabled=True), Bunch(module='WCSMatch', tab='WCSMatch', workspace='right', start=False, - menu="WCS Match [G]", category='Analysis', ptype='global'), + menu="WCS Match [G]", category='Analysis', ptype='global', + enabled=True), Bunch(module='Command', tab='Command', workspace='lleft', start=False, - menu="Command Line [G]", category='Debug', ptype='global'), + menu="Command Line [G]", category='Debug', ptype='global', + enabled=True), Bunch(module='Log', tab='Log', workspace='right', start=False, - menu="Logger Info [G]", category='Debug', ptype='global'), + menu="Logger Info [G]", category='Debug', ptype='global', + enabled=True), Bunch(module='MultiDim', workspace='lleft', category='Navigation', - ptype='local'), + ptype='local', enabled=True), Bunch(module='RC', tab='RC', workspace='right', start=False, - menu="Remote Control [G]", category='Remote', ptype='global'), + menu="Remote Control [G]", category='Remote', ptype='global', + enabled=True), Bunch(module='SAMP', tab='SAMP', workspace='right', start=False, - menu="SAMP Client [G]", category='Remote', ptype='global'), - Bunch(module='Compose', workspace='dialogs', category='RGB', ptype='local'), + menu="SAMP Client [G]", category='Remote', ptype='global', + enabled=False), + Bunch(module='Compose', workspace='dialogs', category='RGB', ptype='local', + enabled=False), Bunch(module='ScreenShot', workspace='dialogs', category='RGB', - ptype='local'), + ptype='local', enabled=True), Bunch(module='ColorMapPicker', tab='ColorMapPicker', menu="Set Color Map [G]", workspace='right', start=False, - category='RGB', ptype='global'), + category='RGB', ptype='global', enabled=True), Bunch(module='ColorMapPicker', menu="Set Color Map", workspace='dialogs', category='RGB', - ptype='local'), + ptype='local', enabled=True), Bunch(module='PlotTable', workspace='dialogs', category='Table', - ptype='local'), + ptype='local', enabled=True), Bunch(module='Catalogs', workspace='dialogs', category='Utils', - ptype='local'), + ptype='local', enabled=True), Bunch(module='Drawing', workspace='dialogs', category='Utils', - ptype='local'), + ptype='local', enabled=True), Bunch(module='AutoLoad', workspace='dialogs', category='Utils', - ptype='local'), - #Bunch(module='Pipeline', workspace='dialogs', category='Utils', - # ptype='local'), + ptype='local', enabled=False), + Bunch(module='Pipeline', workspace='dialogs', category='Utils', + ptype='local', enabled=False), Bunch(module='FBrowser', workspace='dialogs', category='Utils', - ptype='local'), + ptype='local', enabled=True), Bunch(module='ChangeHistory', tab='History', workspace='right', - menu="History [G]", start=False, category='Utils', ptype='global'), - Bunch(module='Mosaic', workspace='dialogs', category='Utils', ptype='local'), - Bunch(module='Collage', workspace='dialogs', category='Utils', ptype='local'), + menu="History [G]", start=False, category='Utils', ptype='global', + enabled=True), + Bunch(module='Mosaic', workspace='dialogs', category='Utils', ptype='local', + enabled=True), + Bunch(module='Collage', workspace='dialogs', category='Utils', ptype='local', + enabled=True), Bunch(module='FBrowser', tab='Open File', workspace='right', - menu="Open File [G]", start=False, category='Utils', ptype='global'), + menu="Open File [G]", start=False, category='Utils', ptype='global', + enabled=True), Bunch(module='Preferences', workspace='dialogs', category='Utils', - ptype='local'), - Bunch(module='Ruler', workspace='dialogs', category='Utils', ptype='local'), + ptype='local', enabled=True), + Bunch(module='Ruler', workspace='dialogs', category='Utils', ptype='local', + enabled=True), # TODO: Add SaveImage to File menu. Bunch(module='SaveImage', tab='SaveImage', workspace='right', - menu="Save File [G]", start=False, category='Utils', ptype='global'), + menu="Save File [G]", start=False, category='Utils', ptype='global', + enabled=True), Bunch(module='WCSAxes', workspace='dialogs', category='Utils', - ptype='local'), - Bunch(module='WBrowser', tab='Help', workspace='channels', start=False, - menu="Help [G]", category='Help', ptype='global'), + ptype='local', enabled=True), Bunch(module='Header', tab='Header', workspace='left', start=False, - menu="Header [G]", hidden=False, category='Utils', ptype='global'), + menu="Header [G]", hidden=False, category='Utils', ptype='global', + enabled=True), Bunch(module='Zoom', tab='Zoom', workspace='left', start=False, menu="Zoom [G]", category='Utils', ptype='global'), Bunch(module='LoaderConfig', tab='Loaders', workspace='channels', start=False, menu="LoaderConfig [G]", category='Debug', - ptype='global'), + ptype='global', enabled=True), + Bunch(module='PluginConfig', tab='Plugins', workspace='channels', + start=False, menu="PluginConfig [G]", category='Debug', + ptype='global', enabled=True), ] @@ -189,12 +215,24 @@ def __init__(self, layout=default_layout, plugins=plugins, appname='ginga', self.channels = channels self.default_plugins = plugins self.plugins = [] + self.plugin_dct = dict() def add_plugin_spec(self, spec): self.plugins.append(spec) + plugin_name = self.get_plugin_name(spec) + self.plugin_dct[plugin_name] = spec def clear_default_plugins(self): self.plugins = [] + self.plugin_dct = dict() + + def get_plugin_name(self, spec): + module = spec['module'] + if '.' in module: + module = module.split('.')[-1] + klass = spec.get('klass', None) + name = module if klass is None else klass + return name def add_default_plugins(self, except_global=[], except_local=[]): """ @@ -348,8 +386,8 @@ def main(self, options, args): font_scaling_factor=None, save_layout=True, use_opengl=False, - layout_file='layout', - plugin_file='plugins.json', + layout_file='layout.json', + plugin_file='plugins.yml', channel_prefix="Image") settings.load(onError='silent') @@ -472,7 +510,7 @@ def main(self, options, args): settings.set(use_opengl=True) layout_file = os.path.join(self.basedir, settings.get('layout_file', - 'layout')) + 'layout.json')) ginga_shell.set_layout(self.layout, layout_file=layout_file, save_layout=settings.get('save_layout', True)) @@ -488,20 +526,6 @@ def main(self, options, args): # Build desired layout ginga_shell.build_toplevel(ignore_saved_layout=options.norestore) - # Does user have a customized plugin setup? If so, override the - # default plugins to be that - plugin_file = settings.get('plugin_file', None) - if plugin_file is not None: - plugin_file = os.path.join(self.basedir, plugin_file) - if os.path.exists(plugin_file): - logger.info("Reading plugin file '%s'..." % (plugin_file)) - try: - with open(plugin_file, 'r') as in_f: - buf = in_f.read() - self.plugins = json.loads(buf) - except Exception as e: - logger.error("Error reading plugin file: %s" % (str(e))) - # Did user specify a particular geometry? if options.geometry: ginga_shell.set_geometry(options.geometry) @@ -513,6 +537,7 @@ def main(self, options, args): disabled_plugins = settings.get('disable_plugins', []) if not isinstance(disabled_plugins, list): disabled_plugins = disabled_plugins.lower().split(',') + disabled_plugins = set(disabled_plugins) # Add GUI log handler (for "Log" global plugin) guiHdlr = GuiLogHandler(ginga_shell) @@ -523,11 +548,11 @@ def main(self, options, args): # Set loader priorities, if user has saved any # (see LoaderConfig plugin) - path = os.path.join(self.basedir, 'loaders.json') + path = os.path.join(self.basedir, 'loaders.yml') if os.path.exists(path): try: with open(path, 'r') as in_f: - loader_dct = json.loads(in_f.read()) + loader_dct = yaml.safe_load(in_f.read()) # set saved priorities for openers for mimetype, m_dct in loader_dct.items(): @@ -561,6 +586,7 @@ def main(self, options, args): spec = Bunch(name=plugin_name, module=plugin_name, ptype='global', tab=plugin_name, menu=menu_name, category="Custom", + enabled=True, workspace='right', pfx=pfx) self.add_plugin_spec(spec) @@ -582,14 +608,44 @@ def main(self, options, args): pfx = None spec = Bunch(module=plugin_name, workspace='dialogs', ptype='local', category="Custom", - hidden=False, pfx=pfx) + hidden=False, pfx=pfx, enabled=True) self.add_plugin_spec(spec) - # Mark disabled plugins + # Does user have a saved plugin setup? If so, check which + # plugins should be disabled, or have a customized category or + # workspace + plugin_file = settings.get('plugin_file', None) + if plugin_file is not None: + plugin_file = os.path.join(self.basedir, plugin_file) + if os.path.exists(plugin_file): + logger.info("Reading plugin file '%s'..." % (plugin_file)) + try: + with open(plugin_file, 'r') as in_f: + buf = in_f.read() + _plugins = yaml.safe_load(buf) + + for dct in _plugins: + plugin_name = self.get_plugin_name(dct) + if plugin_name in self.plugin_dct: + spec = self.plugin_dct[plugin_name] + # user can configure: + # enabled/category/workspace/hidden/start + spec['enabled'] = dct.get('enabled', True) + spec['category'] = dct.get('category', None) + spec['hidden'] = dct.get('hidden', False) + spec['workspace'] = dct.get('workspace', 'dialogs') + if spec['ptype'] == 'global': + spec['start'] = dct.get('start', False) + + except Exception as e: + logger.error(f"Error reading plugin file: {e}", + exc_info=True) + + # Mark disabled plugins (command-line has precedence) for spec in self.plugins: - if spec.get('enabled', None) is None: - spec['enabled'] = (False if spec.module.lower() in disabled_plugins - else True) + if spec.module.lower() in disabled_plugins: + spec['enabled'] = False + # submit plugin specs to shell ginga_shell.set_plugins(self.plugins) diff --git a/ginga/rv/plugins/LoaderConfig.py b/ginga/rv/plugins/LoaderConfig.py index d253d469b..9f5719efd 100644 --- a/ginga/rv/plugins/LoaderConfig.py +++ b/ginga/rv/plugins/LoaderConfig.py @@ -38,9 +38,11 @@ """ import os.path +import yaml + from ginga import GingaPlugin from ginga.util.paths import ginga_home -from ginga.util import loader, json +from ginga.util import loader from ginga.gw import Widgets __all__ = ['LoaderConfig'] @@ -139,10 +141,10 @@ def set_priority_cb(self, w): self.w.loader_tbl.set_tree(self.loader_dct) def save_loaders_cb(self): - path = os.path.join(ginga_home, 'loaders.json') + path = os.path.join(ginga_home, 'loaders.yml') try: with open(path, 'w') as out_f: - out_f.write(json.dumps(self.loader_dct, indent=4)) + out_f.write(yaml.dump(self.loader_dct, indent=4)) except Exception as e: self.logger.error(f"failed to save loader file: {e}", diff --git a/ginga/rv/plugins/Operations.py b/ginga/rv/plugins/Operations.py index 6755275a5..cf7e84fe0 100644 --- a/ginga/rv/plugins/Operations.py +++ b/ginga/rv/plugins/Operations.py @@ -123,7 +123,7 @@ def start(self): plugins = self.fv.get_plugins() for spec in plugins: - if spec.get('hidden', False): + if spec.get('hidden', False) or not spec.get('enabled', True): continue self.fv.error_wrap(self.add_operation, self.fv, spec) diff --git a/ginga/rv/plugins/PluginConfig.py b/ginga/rv/plugins/PluginConfig.py new file mode 100644 index 000000000..3ec67014f --- /dev/null +++ b/ginga/rv/plugins/PluginConfig.py @@ -0,0 +1,292 @@ +# This is open-source software licensed under a BSD license. +# Please see the file LICENSE.txt for details. +""" +The ``PluginConfig`` plugin allows you to configure the plugins that +are visible in your menus. + +**Plugin Type: Global** + +``PluginConfig`` is a global plugin. Only one instance can be opened. + +**Usage** + +PluginConfig is used to configure plugins to be used in Ginga. The items +that can be configured for each plugin include: + +* whether it is enabled (and therefore whether it shows up in the menus) +* the category of the plugin (used to construct the menu hierarchy) +* the workspace in which the plugin will open +* if a global plugin, whether it starts automatically when the reference + viewer starts +* Whether the plugin name should be hidden (not show up in plugin + activation menus) + +When PluginConfig starts, it will show a table of plugins. To edit the +above attributes for plugins, click "Edit", which will bring up a dialog +for editing the table. + +For each plugin you want to configure, click on an entry in the main table +and then adjust the settings in the dialog, then click "Set" in the dialog +to reflect the changes back into the table. If you don't click "Set", +nothing is changed in the table. When you are done editing configurations, +click "Close" on the dialog to close the editing dialog. + +.. note:: It is not recommended to change the workspace for a plugin + unless you choose a compatibly-sized workspace to the original, + as the plugin may not display correctly. If in doubt, leave + the workspace unchanged. Also, disabling plugins in the + "Systems" category may cause some expected features to stop + working. + + +.. important:: To make the changes persist across Ginga restarts, click + "Save" to save the settings (to `$HOME/.ginga/plugins.json`). + Restart Ginga to see changes to the menus (via "category" + changes). **Remove this file manually if you want to reset + the plugin configurations to the defaults**. + + +""" +import os.path + +import yaml + +from ginga import GingaPlugin +from ginga.util.paths import ginga_home +from ginga.gw import Widgets + +__all__ = ['PluginConfig'] + + +class PluginConfig(GingaPlugin.GlobalPlugin): + + def __init__(self, fv): + super().__init__(fv) + + self.yes_no = {True: 'yes', False: 'no'} + self.plugin_dct = dict() + + self.columns = [("Name", 'name'), + ("Enabled", 'enabled'), + ("Type", 'ptype'), + ("Category", 'category'), + ("Workspace", 'workspace'), + ("Hidden", 'hidden'), + ("Auto Start", 'start')] + + self.gui_up = False + + def build_gui(self, container): + vbox = Widgets.VBox() + vbox.set_spacing(1) + + tv = Widgets.TreeView(sortable=True, use_alt_row_color=True, + selection='multiple') + tv.add_callback('selected', self.select_cb) + tv.setup_table(self.columns, 0, 'name') + self.w.plugin_tbl = tv + self.w.edit_dialog = None + + vbox.add_widget(tv, stretch=1) + + btns = Widgets.HBox() + btns.set_border_width(4) + btns.set_spacing(4) + + btn = Widgets.Button("Close") + btn.add_callback('activated', lambda w: self.close()) + btns.add_widget(btn) + btn = Widgets.Button("Help") + btn.add_callback('activated', lambda w: self.help()) + btns.add_widget(btn, stretch=0) + btn = Widgets.Button("Edit") + btn.add_callback('activated', self.edit_plugin_selections_cb) + btn.set_tooltip("Edit configuration of selected plugins") + btns.add_widget(btn, stretch=0) + btn = Widgets.Button("Save") + btn.add_callback('activated', lambda w: self.save_plugins_cb()) + btn.set_tooltip("Save configuration of plugins\n" + "(restart Ginga to see changes to menus)") + btns.add_widget(btn, stretch=0) + + btns.add_widget(Widgets.Label(''), stretch=1) + vbox.add_widget(btns, stretch=0) + container.add_widget(vbox, stretch=1) + + self.gui_up = True + + def start(self): + if len(self.plugin_dct) == 0: + # create plugin table if we haven't already done so + dct = dict() + for spec in self.fv.plugins: + module = spec['module'] + klass = spec.get('klass', None) + name = module if klass is None else klass + enabled = spec.get('enabled', True) + ptype = spec['ptype'] + start = 'n/a' if ptype == 'local' else self.yes_no[spec.get('start', False)] + dct[name] = dict(name=name, + module=module, + enabled=self.yes_no[enabled], + ptype=spec['ptype'], + category=spec.get('category', 'Custom'), + workspace=spec.get('workspace', 'Dialogs'), + hidden=self.yes_no[spec.get('hidden', False)], + start=start) + self.plugin_dct = dct + + self.w.plugin_tbl.set_tree(self.plugin_dct) + self.w.plugin_tbl.set_optimal_column_widths() + + def stop(self): + self.gui_up = False + + def edit_plugin_selections_cb(self, w): + # build dialog for editing + captions = (("Enabled", 'checkbox'), + ("Category:", 'label', "category", 'textentry'), + ("Workspace:", 'label', "workspace", 'textentry'), + ("Auto start", 'checkbox', "Hidden", 'checkbox'), + ) + + w, b = Widgets.build_info(captions, orientation='vertical') + self.w.update(b) + b.enabled.set_tooltip("Enable plugin(s)") + b.enabled.set_enabled(False) + b.category.set_tooltip("Edit menu category of plugin(s)") + b.category.set_enabled(False) + b.workspace.set_tooltip("Edit workspace of plugin(s)") + b.workspace.set_enabled(False) + b.auto_start.set_tooltip("Start plugin(s) at program startup\n" + "(for global plugins only)") + b.auto_start.set_enabled(False) + b.hidden.set_tooltip("Hide plugin(s) names from program menus") + b.hidden.set_enabled(False) + + dialog = Widgets.Dialog(title="Edit Plugin Configuration", + flags=0, + modal=False, + buttons=[['Set', 0], ['Close', 1]], + parent=self.w.plugin_tbl) + dialog.add_callback('activated', + lambda w, rsp: self.edit_cb(w, rsp)) + self.w.edit_dialog = dialog + + box = dialog.get_content_area() + box.set_border_width(4) + box.add_widget(w, stretch=0) + + sel_dct = self.w.plugin_tbl.get_selected() + self.select_cb(self.w.plugin_tbl, sel_dct) + + self.fv.ds.show_dialog(self.w.edit_dialog) + + def edit_cb(self, w, rsp): + if rsp == 1: + # close + self.fv.ds.remove_dialog(w) + self.w.edit_dialog = None + return + + enabled = self.w.enabled.get_state() + category = self.w.category.get_text().strip() + workspace = self.w.workspace.get_text().strip() + hidden = self.w.hidden.get_state() + start = self.w.auto_start.get_state() + + if len(category) == 0: + self.fv.show_error("Category field should not be empty", + raisetab=True) + return + + if len(workspace) == 0: + self.fv.show_error("Workspace field should not be empty", + raisetab=True) + return + + sel_dct = self.w.plugin_tbl.get_selected() + for name, pl_dct in sel_dct.items(): + self.plugin_dct[name]['enabled'] = 'yes' if enabled else 'no' + self.plugin_dct[name]['category'] = category + self.plugin_dct[name]['workspace'] = workspace + if self.plugin_dct[name]['ptype'] == 'global': + self.plugin_dct[name]['start'] = 'yes' if start else 'no' + self.plugin_dct[name]['hidden'] = 'yes' if hidden else 'no' + + self.w.plugin_tbl.set_tree(self.plugin_dct) + + def save_plugins_cb(self): + path = os.path.join(ginga_home, 'plugins.yml') + _plugins = [] + for key, dct in self.plugin_dct.items(): + d = dct.copy() + d['enabled'] = (d['enabled'] == 'yes') + if d['ptype'] == 'global': + d['start'] = (d['start'] == 'yes') + d['hidden'] = (d['hidden'] == 'yes') + _plugins.append(d) + try: + with open(path, 'w') as out_f: + out_f.write(yaml.dump(_plugins)) + + except Exception as e: + self.logger.error(f"failed to save plugin file: {e}", + exc_info=True) + self.fv.show_error(str(e)) + + def select_cb(self, w, dct): + if self.w.edit_dialog is None: + return + selected = len(dct) > 0 + self.w.enabled.set_enabled(selected) + enabled = set([p_dct['enabled'] for p_dct in dct.values()]) + if len(enabled) == 1: + self.w.enabled.set_state(enabled.pop() == 'yes') + else: + self.w.enabled.set_state(True) + + self.w.category.set_enabled(selected) + # only set category widget if all selected are the same + # unique value + categories = set([p_dct['category'] for p_dct in dct.values()]) + if len(categories) == 1: + self.w.category.set_text(categories.pop()) + else: + self.w.category.set_text('') + + self.w.workspace.set_enabled(selected) + # only set workspace widget if all selected are the same + # unique value + workspaces = set([p_dct['workspace'] for p_dct in dct.values()]) + if len(workspaces) == 1: + self.w.workspace.set_text(workspaces.pop()) + else: + self.w.workspace.set_text('') + + # only enable auto start widget if at least one ptype is 'global' + ptypes = set([p_dct['ptype'] for p_dct in dct.values()]) + self.w.auto_start.set_enabled('global' in ptypes) + + starts = set([p_dct['start'] for p_dct in dct.values()]) + if len(starts) == 1: + auto_start = (starts.pop() == 'yes') + self.w.auto_start.set_state(auto_start) + else: + self.w.auto_start.set_state(False) + + # only set hidden widget if all selected are hidden == 'yes' + self.w.hidden.set_enabled(selected) + hiddens = set([p_dct['hidden'] for p_dct in dct.values()]) + if len(hiddens) == 1: + hidden = (hiddens.pop() == 'yes') + self.w.hidden.set_state(hidden) + else: + self.w.hidden.set_state(False) + + def close(self): + self.fv.stop_global_plugin(str(self)) + return True + + def __str__(self): + return 'pluginconfig' diff --git a/setup.cfg b/setup.cfg index 62dd0a5f8..d561e1851 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ install_requires = qtpy>=2.0.1 astropy>=5.0 pillow>=9.2 + pyyaml>=6.0 tomli>=2.0.1; python_full_version < '3.11.0a7' setup_requires = setuptools_scm