diff --git a/doc/WhatsNew.rst b/doc/WhatsNew.rst index 0b9d032b8..852a83af6 100644 --- a/doc/WhatsNew.rst +++ b/doc/WhatsNew.rst @@ -27,6 +27,8 @@ Ver 5.0.0 (unreleased) same as before) to better reflect what the mode does - Changed icons and cursors from PNG (bitmap) to SVG (vector) format - Added color distribution ("stretch") control to Info plugin +- Added LoaderConfig plugin; allows setting of loader priorities for + various MIME types Ver 4.1.0 (2022-06-30) ====================== diff --git a/doc/manual/plugins.rst b/doc/manual/plugins.rst index ee17055fa..d3e88650e 100644 --- a/doc/manual/plugins.rst +++ b/doc/manual/plugins.rst @@ -52,6 +52,7 @@ Global plugins plugins_global/command plugins_global/saveimage plugins_global/downloads + plugins_global/loaderconfig .. _sec-localplugins: diff --git a/doc/manual/plugins_global/figures/loaderconfig-plugin.png b/doc/manual/plugins_global/figures/loaderconfig-plugin.png new file mode 100644 index 000000000..036c07d19 Binary files /dev/null and b/doc/manual/plugins_global/figures/loaderconfig-plugin.png differ diff --git a/doc/manual/plugins_global/loaderconfig.rst b/doc/manual/plugins_global/loaderconfig.rst new file mode 100644 index 000000000..82840c9e1 --- /dev/null +++ b/doc/manual/plugins_global/loaderconfig.rst @@ -0,0 +1,13 @@ +.. _sec-plugins-LoaderConfig: + +LoaderConfig +============ + +.. image:: figures/loaderconfig-plugin.png + :align: center + :width: 800px + :alt: LoaderConfig plugin + +.. automodapi:: ginga.rv.plugins.LoaderConfig + :no-heading: + :skip: LoaderConfig diff --git a/ginga/rv/main.py b/ginga/rv/main.py index 588347f91..4980ac29a 100644 --- a/ginga/rv/main.py +++ b/ginga/rv/main.py @@ -19,7 +19,7 @@ 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 +from ginga.util import paths, rgb_cms, json, compat, loader # Catch warnings logging.captureWarnings(True) @@ -168,6 +168,9 @@ menu="Header [G]", hidden=False, category='Utils', ptype='global'), 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'), ] @@ -518,6 +521,26 @@ def main(self, options, args): guiHdlr.setFormatter(fmt) logger.addHandler(guiHdlr) + # Set loader priorities, if user has saved any + # (see LoaderConfig plugin) + path = os.path.join(self.basedir, 'loaders.json') + if os.path.exists(path): + try: + with open(path, 'r') as in_f: + loader_dct = json.loads(in_f.read()) + + # set saved priorities for openers + for mimetype, m_dct in loader_dct.items(): + for name, l_dct in m_dct.items(): + opener = loader.get_opener(name) + loader.add_opener(opener, [mimetype], + priority=l_dct['priority'], + note=opener.__doc__) + + except Exception as e: + logger.error(f"failed to process loader file '{path}': {e}", + exc_info=True) + # Load any custom modules if options.modules is not None: modules = options.modules.split(',') diff --git a/ginga/rv/plugins/LoaderConfig.py b/ginga/rv/plugins/LoaderConfig.py new file mode 100644 index 000000000..d253d469b --- /dev/null +++ b/ginga/rv/plugins/LoaderConfig.py @@ -0,0 +1,173 @@ +# This is open-source software licensed under a BSD license. +# Please see the file LICENSE.txt for details. +""" +The ``LoaderConfig`` plugin allows you to configure the file openers that +can be used to load various content into Ginga. + +Registered file openers are associated with file MIME types, and there can +be several openers for a single MIME type. A priority associated +with a MIME type/opener pairing determines which opener will be used +for each type--the lowest priority value will determine which opener will +be used. If there are more than one opener with the same low priority +then the user will be prompted for which opener to use, when opening a +file in Ginga. This plugin can be used to set the opener preferences +and save it to the user's $HOME/.ginga configuration area. + +**Plugin Type: Global** + +``LoaderConfig`` is a global plugin. Only one instance can be opened. + +**Usage** + +After starting the plugin, the display will show all the registered MIME +types and the openers registered for those types, with an associated +priority for each MIME type/opener pairing. + +Select one or more lines and type a priority for them in the box labeled +"Priority:"; press "Set" (or ENTER) to set the priority of those items. + +.. note:: The lower the number, the higher the priority. Negative numbers + are fine and the default priority for a loader is usually 0. + So, for example, if there are two loaders available for a MIME + type and one priority is set to -1 and the other to 0, the one + with -1 will be used without asking the user to choose. + + +Click "Save" to save the priorities to $HOME/.ginga/loaders.json so that +they will be reloaded and used on subsequent restarts of the program. +""" +import os.path + +from ginga import GingaPlugin +from ginga.util.paths import ginga_home +from ginga.util import loader, json +from ginga.gw import Widgets + +__all__ = ['LoaderConfig'] + + +class LoaderConfig(GingaPlugin.GlobalPlugin): + + def __init__(self, fv): + super().__init__(fv) + + self.loader_dct = dict() + + self.columns = [("Name", 'name'), + ("Priority", 'priority'), + ("Note", 'note')] + + 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', auto_expand=True) + tv.add_callback('selected', self.select_cb) + tv.setup_table(self.columns, 2, 'name') + self.w.loader_tbl = tv + + vbox.add_widget(tv, stretch=1) + + tbar = Widgets.HBox() + tbar.set_border_width(4) + tbar.set_spacing(4) + + tbar.add_widget(Widgets.Label('Priority:')) + pri = Widgets.TextEntrySet(editable=True) + pri.set_tooltip("Edit priority of loader (lower=better, negative numbers ok)") + pri.set_enabled(False) + pri.add_callback('activated', self.set_priority_cb) + self.w.pri_edit = pri + tbar.add_widget(pri) + + tbar.add_widget(Widgets.Label(''), stretch=1) + vbox.add_widget(tbar, stretch=0) + + 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("Save") + btn.add_callback('activated', lambda w: self.save_loaders_cb()) + btn.set_tooltip("Save configuration of loaders") + 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): + # create loader table + tree_dct = dict() + for mimetype, dct in loader.loader_by_mimetype.items(): + md = dict() + tree_dct[mimetype] = md + for name, bnch in dct.items(): + md[name] = dict(name=bnch.opener.name, + priority=bnch.priority, + note=bnch.opener.note) + self.loader_dct = tree_dct + + self.w.loader_tbl.set_tree(self.loader_dct) + self.w.loader_tbl.set_optimal_column_widths() + + def stop(self): + self.gui_up = False + + def set_priority_cb(self, w): + sel_dct = self.w.loader_tbl.get_selected() + priority = int(self.w.pri_edit.get_text()) + for mimetype, ld_dct in sel_dct.items(): + for name, m_dct in ld_dct.items(): + self.loader_dct[mimetype][name]['priority'] = priority + # actually change it in the loader registration + opener = loader.get_opener(name) + loader.add_opener(opener, [mimetype], priority=priority, + note=opener.__doc__) + # update the UI table + self.w.loader_tbl.set_tree(self.loader_dct) + + def save_loaders_cb(self): + path = os.path.join(ginga_home, 'loaders.json') + try: + with open(path, 'w') as out_f: + out_f.write(json.dumps(self.loader_dct, indent=4)) + + except Exception as e: + self.logger.error(f"failed to save loader file: {e}", + exc_info=True) + self.fv.show_error(str(e)) + + def select_cb(self, w, dct): + selected = len(dct) > 0 + self.w.pri_edit.set_enabled(selected) + if not selected: + self.w.pri_edit.set_text('') + else: + # only set priority widget if all selected are the same + # unique value + priorities = set([l_dct['priority'] + for m_dct in dct.values() + for l_dct in m_dct.values()]) + if len(priorities) == 1: + self.w.pri_edit.set_text(str(priorities.pop())) + else: + self.w.pri_edit.set_text('') + + def close(self): + self.fv.stop_global_plugin(str(self)) + return True + + def __str__(self): + return 'loaderconfig' diff --git a/ginga/util/io/io_fits.py b/ginga/util/io/io_fits.py index e34b0fa64..391aa5f43 100644 --- a/ginga/util/io/io_fits.py +++ b/ginga/util/io/io_fits.py @@ -220,7 +220,7 @@ class AstropyFitsFileHandler(BaseFitsFileHandler): """ name = 'astropy.io.fits' - mimetypes = ['image/fits', 'image/x-fits'] + mimetypes = ['image/fits', 'image/x-fits', 'application/fits'] @classmethod def check_availability(cls): @@ -562,7 +562,7 @@ class FitsioFileHandler(BaseFitsFileHandler): """For loading FITS (Flexible Image Transport System) data files. """ name = 'fitsio' - mimetypes = ['image/fits', 'image/x-fits'] + mimetypes = ['image/fits', 'image/x-fits', 'application/fits'] @classmethod def check_availability(cls):