diff --git a/.travis.yml b/.travis.yml index 564b9efc..447b2681 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,13 +32,15 @@ env: - ASTROPY_VERSION=stable - MAIN_CMD='python setup.py' - SETUP_CMD='test' + - PIP_DEPENDENCIES='' - EVENT_TYPE='pull_request push' - - # List other runtime dependencies for the package that are available as - # conda packages here. + + # For this package-template, we include examples of Cython modules, + # so Cython is required for testing. If your package does not include + # Cython code, you can set CONDA_DEPENDENCIES='' - CONDA_DEPENDENCIES='pyqt' - + # List other runtime dependencies for the package that are available as # pip packages here. - PIP_DEPENDENCIES='scipy specutils<=0.2.2 six pyyaml pyqtgraph qtpy py_expression_eval sphinx_rtd_theme sphinx_automodapi' @@ -78,19 +80,16 @@ matrix: # Check for sphinx doc build warnings - we do this first because it # may run for a long time - os: linux - env: SETUP_CMD='build_docs -w' + env: SETUP_CMD='build_docs -w' SPHINX_VERSION='<1.6' - # Now try Astropy dev and LTS vesions with the latest 3.x and 2.7. - - os: linux - env: PYTHON_VERSION=2.7 ASTROPY_VERSION=development - EVENT_TYPE='pull_request push cron' + # Now try Astropy dev with the latest Python and LTS with Python 2.7 and 3.x. - os: linux env: ASTROPY_VERSION=development EVENT_TYPE='pull_request push cron' - os: linux - env: PYTHON_VERSION=2.7 ASTROPY_VERSION=lts NUMPY_VERSION=1.12 + env: PYTHON_VERSION=2.7 ASTROPY_VERSION=lts - os: linux - env: ASTROPY_VERSION=lts NUMPY_VERSION=1.12 + env: ASTROPY_VERSION=lts # Try all python versions and Numpy versions. Since we can assume that # the Numpy developers have taken care of testing Numpy with different @@ -103,15 +102,17 @@ matrix: env: PYTHON_VERSION=3.4 NUMPY_VERSION=1.10 - os: linux env: PYTHON_VERSION=3.5 NUMPY_VERSION=1.11 + - os: linux + env: NUMPY_VERSION=1.12 # Try numpy pre-release - os: linux env: NUMPY_VERSION=prerelease EVENT_TYPE='pull_request push cron' -# # Do a PEP8 test with pycodestyle -# - os: linux -# env: MAIN_CMD='pycodestyle specviz --count' SETUP_CMD='' + # Do a PEP8 test with pycodestyle + - os: linux + env: MAIN_CMD='pycodestyle specviz --count' SETUP_CMD='' allow_failures: # Do a PEP8 test with pycodestyle @@ -132,7 +133,7 @@ install: # in how to install a package, in which case you can have additional # commands in the install: section below. - - git clone git://github.com/astropy/ci-helpers.git + - git clone --depth 1 git://github.com/astropy/ci-helpers.git - source ci-helpers/travis/setup_conda.sh # As described above, using ci-helpers, you should be able to set up an diff --git a/astropy_helpers b/astropy_helpers index 14ca346b..6f31c21c 160000 --- a/astropy_helpers +++ b/astropy_helpers @@ -1 +1 @@ -Subproject commit 14ca346b0da3e92e65bdc398cc8f726cbd7be57d +Subproject commit 6f31c21c8a49ae4ea92bd9b0a7a5e105639c449c diff --git a/docs/api.rst b/docs/api.rst index 997139a2..4f0aabfc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -17,7 +17,12 @@ Data Objects Object Event Handling ^^^^^^^^^^^^^^^^^^^^^ -.. automodapi:: specviz.core.comms +.. automodapi:: specviz.core.events + :no-heading: + +Object Dispatch Machinery +^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodapi:: specviz.core.dispatch :no-heading: Spectrum Layer Plotting diff --git a/setup.py b/setup.py index 9025c45b..f27bdca3 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ PACKAGENAME, [os.path.normpath( os.path.join("data", os.path.relpath(x[0], root_dir), "*")) for x in os.walk(root_dir)]) -package_info['package_data'][PACKAGENAME].append('external/glue/*.ui') +package_info['package_data'][PACKAGENAME].append('third_party/glue/*.ui') # Define entry points for command-line scripts entry_points = {'console_scripts': []} diff --git a/specviz/analysis/models/blackbody.py b/specviz/analysis/models/blackbody.py index 1c27fa47..8c3779d7 100644 --- a/specviz/analysis/models/blackbody.py +++ b/specviz/analysis/models/blackbody.py @@ -4,7 +4,7 @@ from astropy.modeling import Fittable1DModel from astropy.modeling.parameters import Parameter -from astropy.analytic_functions import blackbody_lambda +from astropy.modeling.blackbody import blackbody_lambda __all__ = ['BlackBody'] diff --git a/specviz/analysis/utils.py b/specviz/analysis/utils.py index 2749cb5a..1d39e833 100644 --- a/specviz/analysis/utils.py +++ b/specviz/analysis/utils.py @@ -35,7 +35,7 @@ def resample(data_in, x_in, x_out, y, data_out=None, kind='linear'): array([(4100, 1.0, 1.0), (4300, 1.0, 1.0), (4500, 1.0, 1.0)], dtype=[('wlen', '>> data = np.ones((5,), [('flux', float), ('ivar', float)]) diff --git a/specviz/app.py b/specviz/app.py index 95930e3c..b4419496 100644 --- a/specviz/app.py +++ b/specviz/app.py @@ -29,7 +29,7 @@ from docopt import docopt from .widgets.utils import ICON_PATH -from .core.comms import dispatch +from .core.events import dispatch from .widgets.windows import MainWindow try: @@ -40,7 +40,12 @@ class App(object): - def __init__(self, hide_plugins=False): + def __init__(self, hidden=None, disabled=None): + hidden = hidden or {} + disabled = disabled or {} + + self._instanced_plugins = {} + # Instantiate main window object self._all_tool_bars = {} @@ -53,14 +58,18 @@ def __init__(self, hide_plugins=False): # self.main_window.setDockNestingEnabled(True) # Load system and user plugins - self.load_plugins(hidden=hide_plugins) + self.load_plugins(hidden=hidden, disabled=disabled) # Setup up top-level connections self._setup_connections() # Parse arguments - args = docopt(__doc__, version=version) - self._parse_args(args) + try: + args = docopt(__doc__, version=version) + except SystemExit: + logging.error("Received unknown command line arguments.") + else: + self._parse_args(args) def _parse_args(self, args): if args.get("load", False): @@ -68,37 +77,35 @@ def _parse_args(self, args): dispatch.on_file_read.emit(args.get(""), file_filter=file_filter) - def load_plugins(self, hidden=False): + def load_plugins(self, hidden=None, disabled=None): from .interfaces.registries import plugin_registry - instance_plugins = [x() for x in plugin_registry.members] + self._instanced_plugins = { + x.name:x() for x in plugin_registry.members + if not disabled.get(x.name, False)} - for instance_plugin in sorted(instance_plugins, + for inst_plgn in sorted(self._instanced_plugins.values(), key=lambda x: x.priority): - if instance_plugin.location != 'hidden': - if instance_plugin.location == 'right': + if inst_plgn.location != 'hidden': + if inst_plgn.location == 'right': location = Qt.RightDockWidgetArea - elif instance_plugin.location == 'top': + elif inst_plgn.location == 'top': location = Qt.TopDockWidgetArea else: location = Qt.LeftDockWidgetArea - self.main_window.addDockWidget(location, instance_plugin) + self.main_window.addDockWidget(location, inst_plgn) - if hidden: - instance_plugin.hide() + if hidden.get(inst_plgn.name): + inst_plgn.hide() # Add this dock's visibility action to the menu bar self.menu_docks.addAction( - instance_plugin.toggleViewAction()) - - # Resize the widgets now that they are all present - for ip in instance_plugins[::-1]: - ip.setMinimumSize(ip.sizeHint()) - # QApplication.processEvents() + inst_plgn.toggleViewAction()) # Sort actions based on priority - all_actions = [y for x in instance_plugins for y in x._actions] + all_actions = [y for x in self._instanced_plugins.values() + for y in x._actions] all_categories = {} for act in all_actions: @@ -134,16 +141,6 @@ def _get_tool_bar(self, name, priority): tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) tool_bar.setMovable(False) - tool_bar.setStyleSheet(""" - QToolBar { - icon-size: 32px; - } - - QToolBar QToolButton { - height: 48px; - } - """) - self._all_tool_bars[name] = dict(widget=tool_bar, priority=int(priority), name=name) @@ -219,7 +216,7 @@ def glue_setup(): raise Exception("glue 0.10.2 or later is required for the specviz " "plugin") - from .external.glue.data_viewer import SpecVizViewer + from .third_party.glue.data_viewer import SpecVizViewer from glue.config import qt_client qt_client.add(SpecVizViewer) diff --git a/specviz/core/__init__.py b/specviz/core/__init__.py index 06f068b3..8d78b378 100644 --- a/specviz/core/__init__.py +++ b/specviz/core/__init__.py @@ -1 +1 @@ -from .comms import dispatch, DispatchHandle \ No newline at end of file +from .events import dispatch \ No newline at end of file diff --git a/specviz/core/data.py b/specviz/core/data.py index d666e912..cd75ee30 100644 --- a/specviz/core/data.py +++ b/specviz/core/data.py @@ -4,13 +4,11 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# STDLIB import logging import re -# THIRD-PARTY import numpy as np -from astropy.units import Quantity, LogQuantity, LogUnit, spectral_density, spectral +from astropy.units import Quantity, LogQuantity, LogUnit, spectral_density, spectral, Unit from py_expression_eval import Parser # FIXME: the latest developer version of Astropy removes OrderedDict which is needed by @@ -21,6 +19,7 @@ utils.OrderedDict = OrderedDict from specutils.core.generic import Spectrum1DRef +from astropy.nddata import StdDevUncertainty logging.basicConfig(level=logging.INFO) @@ -50,6 +49,7 @@ def _make_quantity(data, unit): else: return Quantity(data, unit=unit) + class Spectrum1DRefLayer(Spectrum1DRef): """ Class to handle layers in SpecViz. @@ -72,15 +72,21 @@ class Spectrum1DRefLayer(Spectrum1DRef): Arguments passed to the `~spectutils.core.generic.Spectrum1DRef` object. """ - def __init__(self, data, wcs=None, parent=None, layer_mask=None, *args, - **kwargs): - super(Spectrum1DRefLayer, self).__init__(data, wcs=wcs, *args, - **kwargs) + def __init__(self, data, wcs=None, parent=None, layer_mask=None, + uncertainty=None, unit=None, mask=None, *args,**kwargs): + uncertainty = StdDevUncertainty(np.zeros(data.shape)) if uncertainty is None else uncertainty + unit = unit or Unit('') + mask = mask if mask is not None else np.zeros(data.shape).astype(bool) + + super(Spectrum1DRefLayer, self).__init__(data, wcs=wcs, unit=unit, + uncertainty=uncertainty, + mask=mask, + *args,**kwargs) self._parent = parent self._layer_mask = layer_mask @classmethod - def from_parent(cls, parent, layer_mask=None, name=None): + def from_parent(cls, parent, layer_mask=None, name=None, copy=True): """ Create a duplicate child layer from a parent layer @@ -107,7 +113,7 @@ def from_parent(cls, parent, layer_mask=None, name=None): dispersion=parent.dispersion, dispersion_unit=parent.dispersion_unit, layer_mask=layer_mask, parent=parent, meta=parent.meta, - copy=False) + copy=copy) def from_self(self, name="", layer_mask=None): """ @@ -126,10 +132,8 @@ def from_self(self, name="", layer_mask=None): new_layer: The new, parentless, layer. """ - gen_spec = Spectrum1DRef.copy(self, name=name) - return self.from_parent( - parent=gen_spec, layer_mask=layer_mask, name=name + parent=self._parent, layer_mask=layer_mask, name=name, copy=True ) @classmethod @@ -302,6 +306,9 @@ def _evaluate(cls, layers, formula): formula = formula.replace(layer.name, layer.name.replace(" ", "_")) + layer_vars = {layer.name.replace(" ", "_"):layer + for layer in layers} + try: expr = parser.parse(formula) except Exception as e: @@ -309,33 +316,22 @@ def _evaluate(cls, layers, formula): return # Extract variables - vars = expr.variables() + expr_vars = expr.variables() - # List the models in the same order as the variables - # sorted_layers = [next(l for v in vars for l in layers - # if l.name.replace(" ", "_") == v)] - # sorted_layers = [l for v in vars for l in layers - # if l.name.replace(" ", "_") == v] - sorted_layers = [] - - for v in vars: - for l in layers: - if l.name.replace(" ", "_") == v: - sorted_layers.append(l) - break - - if len(sorted_layers) != len(vars): - logging.error("Incorrect layer arithmetic formula: the number " - "of layers does not match the number of variables.") + # Get the union of the sets of variables listed in the expression and + # layer names of the current layer list + union_set = set(layer_vars.keys()).union(set(expr_vars)) + + if len(union_set) != 0: + logging.error("Mis-match between current layer list and expression:" + "%s", union_set) try: - result = parser.evaluate(expr.simplify({}).toString(), - dict(pair for pair in - zip(vars, sorted_layers))) - result._dispersion = sorted_layers[0]._dispersion - result.dispersion_unit = sorted_layers[0].dispersion_unit + result = parser.evaluate(expr.simplify({}).toString(), layer_vars) + result = result.__class__.copy({'dispersion': layers[0].dispersion, + 'dispersion_unit': layers[0].dispersion_unit}) except Exception as e: - logging.error("While evaluating formula: {}".format(e)) + logging.error("While evaluating formula: %s", e) return return result diff --git a/specviz/core/comms.py b/specviz/core/dispatch.py similarity index 60% rename from specviz/core/comms.py rename to specviz/core/dispatch.py index 1c560d39..18241b02 100644 --- a/specviz/core/comms.py +++ b/specviz/core/dispatch.py @@ -85,6 +85,32 @@ class Dispatch(object): """ Central communications object for all events. """ + def setup(self, inst): + """ + Register all methods decorated by `register_listener` + """ + logging.info("Dispatch is now watching: {}".format(inst)) + members = inspect.getmembers(inst, predicate=inspect.ismethod) + + for func_name, func in members: + if hasattr(func, 'wrapped'): + if func.wrapped: + for name in func.event_names: + self._register_listener(name, func) + + def tear_down(self, inst): + """ + Remove all registered methods from their events + """ + logging.info("Dispatch has stopped watching: {}".format(inst)) + members = inspect.getmembers(inst, predicate=inspect.ismethod) + + for func_name, func in members: + if hasattr(func, 'wrapped'): + if func.wrapped: + for name in func.event_names: + self.unregister_listener(name, func) + def register_event(self, name, args=None): """ Add an `EventNode` to the list of possible events @@ -106,7 +132,7 @@ def register_event(self, name, args=None): logging.warning("Event '{}' already exists. Please use a " "different name.".format(name)) - def register_listener(self, name, func): + def _register_listener(self, name, func): """ Add a listener to an event @@ -125,61 +151,7 @@ def register_listener(self, name, func): logging.warning("No such event: {}. Event must be registered " "before listeners can be assigned.".format(name)) - def unregister_listener(self, name, func): - """ - Remove a listener from an event - - Parameters - ---------- - name: str - The event from wich the listener should be removed. - - func: function - The function to be removed - """ - if hasattr(self, name): - call_func = getattr(self, name) - call_func -= func - else: - logging.warning("No such event: {}.".format(name)) - - -class DispatchHandle(object): - """ - Interface for allowing classes to use decorators to define event - listeners. Otherwise, classes would have to define all listeners in the - `init` function using - """ - @staticmethod - def setup(inst): - """ - Register all methods decorated by `register_listener` - """ - logging.info("Dispatch is now watching: {}".format(inst)) - members = inspect.getmembers(inst, predicate=inspect.ismethod) - - for func_name, func in members: - if hasattr(func, 'wrapped'): - if func.wrapped: - for name in func.event_names: - dispatch.register_listener(name, func) - - @staticmethod - def tear_down(inst): - """ - Remove all registered methods from their events - """ - logging.info("Dispatch has stopped watching: {}".format(inst)) - members = inspect.getmembers(inst, predicate=inspect.ismethod) - - for func_name, func in members: - if hasattr(func, 'wrapped'): - if func.wrapped: - for name in func.event_names: - dispatch.unregister_listener(name, func) - - @staticmethod - def register_listener(*args): + def register_listener(self, *args): """ Decorate event listeners """ @@ -199,61 +171,20 @@ def wrapper(*args, **kwargs): return wrapper return decorator + def unregister_listener(self, name, func): + """ + Remove a listener from an event -# Register application-wide events -dispatch = Dispatch() -dispatch.register_event("on_activated_window", args=["window"]) - -dispatch.register_event("on_added_data", args=["data"]) -dispatch.register_event("on_added_window", args=["layer", "window"]) -dispatch.register_event("on_added_plot", args=["plot", "window"]) -dispatch.register_event("on_added_layer", args=["layer"]) -dispatch.register_event("on_added_to_window", args=["layer", "window"]) - -dispatch.register_event("on_show_linelists_window") -dispatch.register_event("on_dismiss_linelists_window") -dispatch.register_event("on_request_linelists") -dispatch.register_event("on_plot_linelists", args=["table_views"]) -dispatch.register_event("on_erase_linelabels") - -dispatch.register_event("on_removed_data", args=["data"]) -dispatch.register_event("on_removed_plot", args=["layer", "window"]) -dispatch.register_event("on_removed_layer", args=["layer", "window"]) -dispatch.register_event("on_removed_model", args=["model", "layer"]) -dispatch.register_event("on_removed_from_window", args=["layer", "window"]) - -dispatch.register_event("on_updated_layer", args=["layer"]) -dispatch.register_event("on_updated_model", args=["model"]) -dispatch.register_event("on_updated_plot", args=["plot", "layer"]) -dispatch.register_event("on_updated_rois", args=["rois"]) -dispatch.register_event("on_updated_stats", args=["stats", "layer"]) - -dispatch.register_event("on_selected_plot", args=["layer", "checked_state"]) -dispatch.register_event("on_selected_window", args=["window"]) -dispatch.register_event("on_selected_layer", args=["layer_item"]) -dispatch.register_event("on_selected_model", args=["model_item"]) - -dispatch.register_event("on_clicked_layer", args=["layer_item"]) -dispatch.register_event("on_changed_layer", args=["layer_item"]) -dispatch.register_event("on_changed_model", args=["model_item"]) -dispatch.register_event("on_copy_model") -dispatch.register_event("on_paste_model", args=["data", "layer"]) - -dispatch.register_event("on_add_data", args=["data"]) -dispatch.register_event("on_add_model", args=["layer", "model"]) -dispatch.register_event("on_add_window", args=["data", "window", "layer"]) -dispatch.register_event("on_add_layer", args=["window", "layer", "from_roi"]) -dispatch.register_event("on_add_roi", args=[]) -dispatch.register_event("on_add_to_window", args=["data", "window"]) - -dispatch.register_event("on_update_model", args=["layer"]) - -dispatch.register_event("on_remove_data", args=["data"]) -dispatch.register_event("on_remove_layer", args=["layer"]) -dispatch.register_event("on_remove_model", args=["model"]) -dispatch.register_event("on_remove_all_data") - -dispatch.register_event("on_file_open", args=["file_name"]) -dispatch.register_event("on_file_read", args=["file_name", "file_filter", "auto_open"]) + Parameters + ---------- + name: str + The event from wich the listener should be removed. -dispatch.register_event("on_status_message", args=["message", "timeout"]) + func: function + The function to be removed + """ + if hasattr(self, name): + call_func = getattr(self, name) + call_func -= func + else: + logging.warning("No such event: {}.".format(name)) \ No newline at end of file diff --git a/specviz/core/events.py b/specviz/core/events.py new file mode 100644 index 00000000..5cc1527c --- /dev/null +++ b/specviz/core/events.py @@ -0,0 +1,60 @@ +from .dispatch import Dispatch + + +dispatch = Dispatch() + +dispatch.register_event("on_activated_window", args=["window"]) + +dispatch.register_event("on_added_data", args=["data"]) +dispatch.register_event("on_added_window", args=["layer", "window"]) +dispatch.register_event("on_added_plot", args=["plot", "window"]) +dispatch.register_event("on_added_layer", args=["layer"]) +dispatch.register_event("on_added_to_window", args=["layer", "window"]) + +dispatch.register_event("on_show_linelists_window") +dispatch.register_event("on_dismiss_linelists_window") +dispatch.register_event("on_request_linelists") +dispatch.register_event("on_plot_linelists", args=["table_views"]) +dispatch.register_event("on_erase_linelabels") + +dispatch.register_event("on_removed_data", args=["data"]) +dispatch.register_event("on_removed_plot", args=["layer", "window"]) +dispatch.register_event("on_removed_layer", args=["layer", "window"]) +dispatch.register_event("on_removed_model", args=["model", "layer"]) +dispatch.register_event("on_removed_from_window", args=["layer", "window"]) + +dispatch.register_event("on_updated_layer", args=["layer"]) +dispatch.register_event("on_updated_model", args=["model"]) +dispatch.register_event("on_updated_plot", args=["plot", "layer"]) +dispatch.register_event("on_updated_rois", args=["rois"]) +dispatch.register_event("on_updated_stats", args=["stats", "layer"]) + +dispatch.register_event("on_selected_plot", args=["layer", "checked_state"]) +dispatch.register_event("on_selected_window", args=["window"]) +dispatch.register_event("on_selected_layer", args=["layer_item"]) +dispatch.register_event("on_selected_model", args=["model_item"]) + +dispatch.register_event("on_clicked_layer", args=["layer_item"]) +dispatch.register_event("on_changed_layer", args=["layer_item"]) +dispatch.register_event("on_changed_model", args=["model_item"]) +dispatch.register_event("on_copy_model") +dispatch.register_event("on_paste_model", args=["data", "layer"]) + +dispatch.register_event("on_add_data", args=["data"]) +dispatch.register_event("on_add_model", args=["layer", "model"]) +dispatch.register_event("on_add_window", args=["data", "window", "layer"]) +dispatch.register_event("on_add_layer", args=["window", "layer", "from_roi"]) +dispatch.register_event("on_add_roi", args=[]) +dispatch.register_event("on_add_to_window", args=["data", "window"]) + +dispatch.register_event("on_update_model", args=["layer"]) + +dispatch.register_event("on_remove_data", args=["data"]) +dispatch.register_event("on_remove_layer", args=["layer"]) +dispatch.register_event("on_remove_model", args=["model"]) +dispatch.register_event("on_remove_all_data") + +dispatch.register_event("on_file_open", args=["file_name"]) +dispatch.register_event("on_file_read", args=["file_name", "file_filter", "auto_open"]) + +dispatch.register_event("on_status_message", args=["message", "timeout"]) \ No newline at end of file diff --git a/specviz/core/plots.py b/specviz/core/plots.py index 653e7efd..a8d45f24 100644 --- a/specviz/core/plots.py +++ b/specviz/core/plots.py @@ -6,7 +6,8 @@ from qtpy.QtGui import * -from astropy.units import spectral_density, spectral +from astropy.units import spectral_density, spectral, Unit + import pyqtgraph as pg import logging import numpy as np @@ -147,16 +148,20 @@ def change_units(self, x, y=None, z=None): """ is_convert_success = [True, True, True] - if x is None or not self._layer.dispersion_unit.is_equivalent( + x = x or Unit('') + y = y or Unit('') + + if not self._layer.dispersion_unit.is_equivalent( x, equivalencies=spectral()): - logging.error("Failed to convert x-axis plot units. {} to" - " {}".format(self._layer.dispersion_unit, x)) + logging.error("Failed to convert x-axis plot units from [{}] to" + " [{}].".format(self._layer.dispersion_unit, x)) x = None is_convert_success[0] = False - if y is None or not self._layer.unit.is_equivalent( + if not self._layer.unit.is_equivalent( y, equivalencies=spectral_density(self.layer.dispersion)): - logging.error("Failed to convert y-axis plot units.") + logging.error("Failed to convert y-axis plot units from [{}] to " + "[{}].".format(self._layer.unit, y)) y = self._layer.unit is_convert_success[1] = False @@ -325,11 +330,11 @@ def update(self, autoscale=False): data = self.layer.data.compressed().value uncert = self.layer.raw_uncertainty.compressed().value - #-- Changes specific for scatter plot rendering + # Change specific marker for scatter plot rendering symbol = 'o' if self.mode == 'scatter' else None pen = None if self.mode == 'scatter' else self.pen - #-- changes specific for histrogram rendering + # Change specific style for histogram rendering stepMode = True if self.mode == 'histogram' else False disp = np.append(disp, disp[-1]) if self.mode == 'histogram' else disp diff --git a/specviz/data/qt/ui/main_window.ui b/specviz/data/qt/ui/main_window.ui new file mode 100644 index 00000000..f0d08949 --- /dev/null +++ b/specviz/data/qt/ui/main_window.ui @@ -0,0 +1,109 @@ + + + MainWindow + + + + 0 + 0 + 1044 + 913 + + + + SpecViz + + + true + + + true + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Plain + + + + + 230 + 230 + 230 + + + + + QMdiArea::SubWindowView + + + false + + + + + + + + + 0 + 0 + 1044 + 22 + + + + + File + + + + + Edit + + + + + Plugins + + + + + + + + + + toolBar + + + false + + + false + + + TopToolBarArea + + + false + + + + + + diff --git a/specviz/data/qt/ui/plugin.ui b/specviz/data/qt/ui/plugin.ui new file mode 100644 index 00000000..2316dfca --- /dev/null +++ b/specviz/data/qt/ui/plugin.ui @@ -0,0 +1,48 @@ + + + plugin + + + + 0 + 0 + 288 + 200 + + + + Plugin + + + + + 0 + 0 + 334 + 261 + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 334 + 261 + + + + + + + + diff --git a/specviz/external/glue/data_viewer.py b/specviz/external/glue/data_viewer.py deleted file mode 100644 index 75bc27de..00000000 --- a/specviz/external/glue/data_viewer.py +++ /dev/null @@ -1,187 +0,0 @@ -import os -from collections import OrderedDict - -from glue.core import Subset -from glue.viewers.common.qt.data_viewer import DataViewer -from glue.core import message as msg -from glue.utils import nonpartial -from glue.viewers.common.qt.toolbar import BasicToolbar - -from ...app import App as Viewer -from ...core import dispatch -from ...core import DispatchHandle - -from .viewer_options import OptionsWidget -from .layer_widget import LayerWidget - - -__all__ = ['SpecVizViewer'] - - -class BaseVizViewer(DataViewer): - def __init__(self, session, parent=None): - super(BaseVizViewer, self).__init__(session, parent=parent) - - # Connect the dataview to the specviz messaging system - DispatchHandle.setup(self) - - # We now set up the options widget. This controls for example which - # attribute should be used to indicate the filenames of the spectra. - self._options_widget = OptionsWidget(data_viewer=self) - - # The layer widget is used to select which data or subset to show. - # We don't use the default layer list, because in this case we want to - # make sure that only one dataset or subset can be selected at any one - # time. - self._layer_widget = LayerWidget() - - # Make sure we update the viewer if either the selected layer or the - # column specifying the filename is changed. - self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( - nonpartial(self._update_options)) - self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( - nonpartial(self._refresh_data)) - self._options_widget.ui.combo_file_attribute.currentIndexChanged.connect( - nonpartial(self._refresh_data)) - - # The following two methods are required by glue - they are used to specify - # which widgets to put in the bottom left and middle left panel. - - def options_widget(self): - return self._options_widget - - def layer_view(self): - return self._layer_widget - - # The following method is required by glue - it is used to subscribe the - # viewer to various messages sent by glue. - - def register_to_hub(self, hub): - - super(BaseVizViewer, self).register_to_hub(hub) - - hub.subscribe(self, msg.SubsetCreateMessage, - handler=self._add_subset) - - hub.subscribe(self, msg.SubsetUpdateMessage, - handler=self._update_subset) - - hub.subscribe(self, msg.SubsetDeleteMessage, - handler=self._remove_subset) - - hub.subscribe(self, msg.DataUpdateMessage, - handler=self._update_data) - - # The following two methods are required by glue - they are what gets called - # when a dataset or subset gets dragged and dropped onto the viewer. - - def add_data(self, data): - if data not in self._layer_widget: - self._layer_widget.add_layer(data) - self._layer_widget.layer = data - self._options_widget.set_data(self._layer_widget.layer) - self._refresh_data() - return True - - def add_subset(self, subset): - if subset not in self._layer_widget: - self._layer_widget.add_layer(subset) - self._layer_widget.layer = subset - self._options_widget.set_data(self._layer_widget.layer) - self._refresh_data() - return True - - # The following four methods are used to receive various messages related - # to updates to data or subsets. - - def _update_data(self, message): - self._refresh_data() - - def _add_subset(self, message): - self.add_subset(message.subset) - - def _update_subset(self, message): - self._refresh_data() - - def _remove_subset(self, message): - if message.subset in self._layer_widget: - self._layer_widget.remove_layer(message.subset) - self._refresh_data() - - # When the selected layer is changed, we need to update the combo box with - # the attributes from which the filename attribute can be selected. The - # following method gets called in this case. - - def _update_options(self): - self._options_widget.set_data(self._layer_widget.layer) - - def _refresh_data(self): - raise NotImplementedError() - - -class SpecVizViewer(BaseVizViewer): - LABEL = "SpecViz Viewer" - - def __init__(self, session, parent=None): - super(SpecVizViewer, self).__init__(session, parent=None) - # We keep a cache of the specviz data objects that correspond to a given - # filename - although this could take up a lot of memory if there are - # many spectra, so maybe this isn't needed - self._specviz_data_cache = OrderedDict() - - # We set up the specviz viewer and controller as done for the standalone - # specviz application - self.viewer = Viewer(hide_plugins=False) - self.setCentralWidget(self.viewer.main_window) - - def initialize_toolbar(self): - pass - - def open_data(self, data): - dispatch.on_add_data.emit(data) - - def _refresh_data(self): - if self._options_widget.file_att is None: - return - - if self._layer_widget.layer is None: - return - - if isinstance(self._layer_widget.layer, Subset): - subset = self._layer_widget.layer - cid = subset.data.id[self._options_widget.file_att] - mask = subset.to_mask(None) - component = subset.data.get_component(cid) - else: - cid = self._layer_widget.layer.id[self._options_widget.file_att] - mask = None - component = self._layer_widget.layer.get_component(cid) - - # Clear current data objects in SpecViz - dispatch.on_remove_all_data.emit() - - if not component.categorical: - return - - filenames = component.labels - path = '/'.join(component._load_log.path.split('/')[:-1]) - - if mask is not None: - filenames = filenames[mask] - - for filename in filenames: - - if filename in self._specviz_data_cache: - data = self._specviz_data_cache[filename] - dispatch.on_add_data.emit(data=data) - - else: - file_name = str(filename) - file_path = os.path.join(path, file_name) - dispatch.on_file_read.emit(file_name=file_path, - file_filter='MOS') - - @DispatchHandle.register_listener('on_added_data') - def _added_data(self, data): - filename = data.name - self._specviz_data_cache[filename] = data diff --git a/specviz/interfaces/loaders.py b/specviz/interfaces/loaders.py index 2ba4a3b8..460648dd 100644 --- a/specviz/interfaces/loaders.py +++ b/specviz/interfaces/loaders.py @@ -19,9 +19,9 @@ def load_yaml_reader(f_path): - - custom_loader = yaml.load(open(f_path, 'r')) - custom_loader.set_filter() + with open(f_path, 'r') as f: + custom_loader = yaml.load(f) + custom_loader.set_filter() # Figure out which of the two generic loaders to associate # this yaml file with diff --git a/specviz/plugins/data_list_plugin.py b/specviz/plugins/data_list_plugin.py index 120327d0..48cfc5d8 100644 --- a/specviz/plugins/data_list_plugin.py +++ b/specviz/plugins/data_list_plugin.py @@ -11,7 +11,7 @@ from ..widgets.wizard import open_wizard from qtpy.uic import loadUi -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from ..widgets.utils import ICON_PATH from ..core.data import Spectrum1DRef from ..core.threads import FileLoadThread @@ -81,7 +81,7 @@ def _file_load_result(self, data, thread, auto_open): self._data_loaded(data, auto_open=auto_open) self._loader_threads.remove(thread) - @DispatchHandle.register_listener("on_add_data") + @dispatch.register_listener("on_add_data") def _data_loaded(self, data, auto_open=True): dispatch.on_added_data.emit(data=data) @@ -113,7 +113,7 @@ def current_data(self): def current_data_item(self): return self.contents.list_widget_data_list.currentItem() - @DispatchHandle.register_listener("on_file_open") + @dispatch.register_listener("on_file_open") def open_file(self, file_name=None): """ Creates a :code:`specutils.core.generic.Spectrum1DRef` object from the `Qt` @@ -154,7 +154,7 @@ def open_file_dialog(self): return file_names[0], self._file_filter - @DispatchHandle.register_listener("on_file_read") + @dispatch.register_listener("on_file_read") def read_file(self, file_name, file_filter=None, auto_open=True): file_load_thread = FileLoadThread() @@ -169,7 +169,7 @@ def read_file(self, file_name, file_filter=None, auto_open=True): file_load_thread(file_name, file_filter) file_load_thread.start() - @DispatchHandle.register_listener("on_added_data") + @dispatch.register_listener("on_added_data") def add_data_item(self, data): """ Adds a `Data` object to the loaded data list widget. @@ -186,7 +186,7 @@ def add_data_item(self, data): self.contents.list_widget_data_list.setCurrentItem(new_item) - @DispatchHandle.register_listener("on_remove_data") + @dispatch.register_listener("on_remove_data") def remove_data_item(self, data=None): if data is None: data = self.current_data @@ -198,7 +198,7 @@ def remove_data_item(self, data=None): dispatch.on_removed_data.emit(data=self.current_data) - @DispatchHandle.register_listener("on_remove_all_data") + @dispatch.register_listener("on_remove_all_data") def remove_all_data(self): print('*' * 100, self.contents.list_widget_data_list.count()) for i in range(self.contents.list_widget_data_list.count()): diff --git a/specviz/plugins/layer_list_plugin.py b/specviz/plugins/layer_list_plugin.py index e3b97132..eae3b3db 100644 --- a/specviz/plugins/layer_list_plugin.py +++ b/specviz/plugins/layer_list_plugin.py @@ -13,7 +13,7 @@ from qtpy.QtGui import QPixmap, QIcon from ..widgets.utils import ICON_PATH -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from ..core.data import Spectrum1DRefLayer, Spectrum1DRef from ..widgets.dialogs import LayerArithmeticDialog from ..widgets.plugin import Plugin @@ -127,7 +127,7 @@ def _copy_model(self): else: self.contents.button_apply_model.setEnabled(False) - @DispatchHandle.register_listener("on_paste_model") + @dispatch.register_listener("on_paste_model") def _paste_model(self, data=None, layer=None): if self._copied_model is None: logging.error("No copied model; unable to paste.") @@ -185,7 +185,7 @@ def all_layers(self): return layers - @DispatchHandle.register_listener("on_added_layer") + @dispatch.register_listener("on_added_layer") def add_layer_item(self, layer, unique=True, *args, **kwargs): """ Adds a `Layer` object to the loaded layer list widget. @@ -226,7 +226,16 @@ def get_layer_item(self, layer): if sec_child.data(0, Qt.UserRole) == layer: return sec_child - @DispatchHandle.register_listener("on_remove_layer") + @dispatch.register_listener("on_remove_data") + def remove_layer_items_with_data(self, data=None): + # Find all layers whose parent is the data object + layers = [x for x in self.all_layers if x._parent == data] + + # Remove each layer + for layer in layers: + dispatch.on_remove_layer.emit(layer=layer) + + @dispatch.register_listener("on_remove_layer") def remove_layer_item(self, layer=None): if layer is None: layer = self.current_layer @@ -276,7 +285,7 @@ def add_layer(self, layer=None, layer_mask=None, window=None, from_roi=True): dispatch.on_add_layer.emit(layer=new_layer, window=window) - @DispatchHandle.register_listener("on_added_plot", "on_updated_plot") + @dispatch.register_listener("on_added_plot", "on_updated_plot") def update_layer_item(self, plot=None, *args, **kwargs): if plot is None: return @@ -292,7 +301,7 @@ def update_layer_item(self, plot=None, *args, **kwargs): layer_item.setIcon(0, icon) layer_item.setCheckState(0, Qt.Checked if plot.checked else Qt.Unchecked) - @DispatchHandle.register_listener("on_selected_layer", "on_changed_layer") + @dispatch.register_listener("on_selected_layer", "on_changed_layer") def _update_layer_name(self, layer_item, checked_state=None, col=0): if layer_item is None: return @@ -387,7 +396,7 @@ def toggle_buttons(self, layer_item): self.contents.button_copy_model.setEnabled(False) - @DispatchHandle.register_listener("on_activated_window") + @dispatch.register_listener("on_activated_window") def update_layer_list(self, window): self.contents.tree_widget_layer_list.clear() @@ -401,7 +410,7 @@ def update_layer_list(self, window): plot = window.get_plot(layer) self.update_layer_item(plot) - @DispatchHandle.register_listener("on_clicked_layer") + @dispatch.register_listener("on_clicked_layer") def _set_layer_visibility(self, layer_item, col=0): """ Toggles the visibility of the plot in the sub window. diff --git a/specviz/plugins/mask_editor_plugin.py b/specviz/plugins/mask_editor_plugin.py index af650bec..767fa933 100644 --- a/specviz/plugins/mask_editor_plugin.py +++ b/specviz/plugins/mask_editor_plugin.py @@ -5,7 +5,7 @@ from qtpy.QtWidgets import (QGroupBox, QHBoxLayout, QPushButton, QVBoxLayout, QCheckBox, QTreeWidget, QTreeWidgetItem) from qtpy.QtCore import * -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from qtpy.QtGui import * import numpy as np @@ -75,7 +75,7 @@ def _toggle_bit(self, item, col=0): self.active_window.update_plot(layer) - @DispatchHandle.register_listener("on_updated_rois") + @dispatch.register_listener("on_updated_rois") def toggle_mask_button(self, rois): if rois: self.button_mask_data.setEnabled(True) @@ -84,7 +84,7 @@ def toggle_mask_button(self, rois): self.button_mask_data.setEnabled(False) self.button_unmask_data.setEnabled(False) - @DispatchHandle.register_listener("on_changed_layer") + @dispatch.register_listener("on_changed_layer") def load_dq_flags(self, layer_item=None, layer=None): self.tree_widget_dq.clear() if layer_item is None and layer is None: diff --git a/specviz/plugins/model_fitting_plugin.py b/specviz/plugins/model_fitting_plugin.py index 2be87cfa..beea324e 100644 --- a/specviz/plugins/model_fitting_plugin.py +++ b/specviz/plugins/model_fitting_plugin.py @@ -3,6 +3,7 @@ """ import logging import os +import re import numpy as np from qtpy import compat @@ -12,7 +13,7 @@ from qtpy.QtGui import QIntValidator, QDoubleValidator from qtpy.uic import loadUi -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch from ..core.data import Spectrum1DRefModelLayer from ..core.threads import FitModelThread from ..interfaces.factories import ModelFactory, FitterFactory @@ -187,7 +188,7 @@ def add_model_layer(self, model): return new_model_layer - @DispatchHandle.register_listener("on_add_model") + @dispatch.register_listener("on_add_model") def add_model_item(self, layer=None, model=None, unique=True): """ Adds an `astropy.modeling.Model` to the loaded model tree widget. @@ -220,15 +221,20 @@ def add_model_item(self, layer=None, model=None, unique=True): name = model.name if not name: - count = 1 + count = 0 root = self.contents.tree_widget_current_models.invisibleRootItem() for i in range(root.childCount()): child = root.child(i) + pre_mod = child.data(0, Qt.UserRole) + pre_name = child.text(0) - if isinstance(model, child.data(0, Qt.UserRole).__class__): - count += 1 + if isinstance(model, pre_mod.__class__): + cur_num = next(iter([int(x) for x in re.findall(r'\d+', pre_name)]), 0) + 1 + + if cur_num > count: + count = cur_num name = model.__class__.__name__.replace('1D', '') + str(count) model._name = name @@ -256,7 +262,7 @@ def add_model_item(self, layer=None, model=None, unique=True): self._update_arithmetic_text(layer) - @DispatchHandle.register_listener("on_update_model") + @dispatch.register_listener("on_update_model") def update_model_item(self, layer): if hasattr(layer.model, '_submodels'): models = layer.model._submodels @@ -283,7 +289,7 @@ def update_model_item(self, layer): self.current_layer.model = layer.model self.contents.tree_widget_current_models.blockSignals(False) - @DispatchHandle.register_listener("on_remove_model") + @dispatch.register_listener("on_remove_model") def remove_model_item(self, model=None): if model is None: model = self.current_model @@ -387,7 +393,7 @@ def get_compound_model(self, model_dict=None, formula=''): return np.sum(models) if len(models) > 1 else models[0] - @DispatchHandle.register_listener("on_update_model") + @dispatch.register_listener("on_update_model") def _update_arithmetic_text(self, layer): if hasattr(layer, '_model'): # If the model is a compound @@ -407,7 +413,7 @@ def _update_arithmetic_text(self, layer): return expr - @DispatchHandle.register_listener("on_selected_model", "on_changed_model") + @dispatch.register_listener("on_selected_model", "on_changed_model") def _update_model_name(self, model_item, col=0): if model_item is None: return @@ -433,7 +439,7 @@ def _update_model_name(self, model_item, col=0): self._update_arithmetic_text(self.current_layer) - @DispatchHandle.register_listener("on_changed_model") + @dispatch.register_listener("on_changed_model") def _update_model_parameters(self, *args, **kwargs): model_layer = self.current_layer model_dict = self.get_model_inputs() @@ -449,7 +455,7 @@ def _update_model_parameters(self, *args, **kwargs): logging.error("Cannot set `ModelLayer` model to new compound " "model.") - @DispatchHandle.register_listener("on_selected_layer") + @dispatch.register_listener("on_selected_layer") def update_model_list(self, layer_item=None, layer=None): self.contents.tree_widget_current_models.clear() self.contents.line_edit_model_arithmetic.clear() @@ -532,7 +538,7 @@ def toggle_buttons(self): # this is also called in response to the "on_remove_model" signal, # however indirectly via the remove_model_item method. - @DispatchHandle.register_listener("on_add_model") + @dispatch.register_listener("on_add_model") def toggle_fitting(self, *args, **kwargs): root = self.contents.tree_widget_current_models.invisibleRootItem() @@ -543,7 +549,7 @@ def toggle_fitting(self, *args, **kwargs): self.contents.group_box_fitting.setEnabled(False) self.contents.button_save_model.setEnabled(False) - @DispatchHandle.register_listener("on_selected_layer") + @dispatch.register_listener("on_selected_layer") def toggle_io(self, layer_item, *args, **kwargs): if layer_item: self.contents.button_load_model.setEnabled(True) diff --git a/specviz/plugins/plot_tools_plugin.py b/specviz/plugins/plot_tools_plugin.py index 69378d16..6b319661 100644 --- a/specviz/plugins/plot_tools_plugin.py +++ b/specviz/plugins/plot_tools_plugin.py @@ -8,7 +8,7 @@ from astropy.units import Unit from ..widgets.utils import ICON_PATH -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from ..widgets.dialogs import TopAxisDialog, UnitChangeDialog from ..widgets.plugin import Plugin @@ -139,7 +139,7 @@ def _toggle_errors(self, state): current_window.disable_errors = not state current_window.set_active_plot(layer) - @DispatchHandle.register_listener("on_activated_window") + @dispatch.register_listener("on_activated_window") def toggle_enabled(self, window): if window: self.button_axis_change.setEnabled(True) diff --git a/specviz/plugins/statistics_plugin.py b/specviz/plugins/statistics_plugin.py index 1df2d8ec..8fb920f9 100644 --- a/specviz/plugins/statistics_plugin.py +++ b/specviz/plugins/statistics_plugin.py @@ -11,7 +11,7 @@ from ..widgets.plugin import Plugin from ..analysis import statistics -from ..core.comms import DispatchHandle +from ..core.events import dispatch from ..widgets.utils import UI_PATH @@ -28,7 +28,7 @@ def setup_ui(self): def setup_connections(self): pass - @DispatchHandle.register_listener("on_updated_rois", "on_selected_layer") + @dispatch.register_listener("on_updated_rois", "on_selected_layer") def update_statistics(self, rois=None, *args, **kwargs): if rois is None: if self.active_window is not None: diff --git a/specviz/plugins/tool_tray_plugin.py b/specviz/plugins/tool_tray_plugin.py index ec8113c3..ed330c48 100644 --- a/specviz/plugins/tool_tray_plugin.py +++ b/specviz/plugins/tool_tray_plugin.py @@ -5,7 +5,7 @@ from ..widgets.utils import ICON_PATH from ..analysis.filters import smooth -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from ..widgets.dialogs import SmoothingDialog from ..widgets.plugin import Plugin @@ -93,7 +93,7 @@ def _perform_smooth(self): dispatch.on_add_layer.emit(layer=new_data) - @DispatchHandle.register_listener("on_activated_window") + @dispatch.register_listener("on_activated_window") def toggle_enabled(self, window): if window: self.button_smooth.setEnabled(True) diff --git a/specviz/external/__init__.py b/specviz/third_party/__init__.py similarity index 100% rename from specviz/external/__init__.py rename to specviz/third_party/__init__.py diff --git a/specviz/external/glue/__init__.py b/specviz/third_party/glue/__init__.py similarity index 100% rename from specviz/external/glue/__init__.py rename to specviz/third_party/glue/__init__.py diff --git a/specviz/third_party/glue/data_viewer.py b/specviz/third_party/glue/data_viewer.py new file mode 100644 index 00000000..6d22a2d1 --- /dev/null +++ b/specviz/third_party/glue/data_viewer.py @@ -0,0 +1,223 @@ +import os +from collections import OrderedDict + +import numpy as np +from glue.core import message as msg +from glue.core import Subset +from glue.utils import nonpartial +from glue.viewers.common.qt.data_viewer import DataViewer +from glue.viewers.common.qt.toolbar import BasicToolbar +from qtpy.QtCore import QSize, Qt +from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget +from spectral_cube import SpectralCube + +from ...app import App +from ...core import dispatch +from ...core.data import Spectrum1DRef +from .layer_widget import LayerWidget +from .viewer_options import OptionsWidget + +__all__ = ['SpecVizViewer'] + + +class SpecVizViewer(DataViewer): + LABEL = "SpecViz Viewer" + + def __init__(self, session, parent=None): + super(SpecVizViewer, self).__init__(session, parent=parent) + + # Connect the dataview to the specviz messaging system + dispatch.setup(self) + + # We now set up the options widget. This controls for example which + # attribute should be used to indicate the filenames of the spectra. + self._options_widget = OptionsWidget(data_viewer=self) + + # The layer widget is used to select which data or subset to show. + # We don't use the default layer list, because in this case we want to + # make sure that only one dataset or subset can be selected at any one + # time. + self._layer_widget = LayerWidget() + + # Make sure we update the viewer if either the selected layer or the + # column specifying the filename is changed. + self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( + nonpartial(self._update_options)) + # self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( + # nonpartial(self._refresh_data)) + # self._options_widget.ui.combo_file_attribute.currentIndexChanged.connect( + # nonpartial(self._refresh_data)) + + # We keep a cache of the specviz data objects that correspond to a given + # filename - although this could take up a lot of memory if there are + # many spectra, so maybe this isn't needed + self._specviz_data_cache = OrderedDict() + + # We set up the specviz viewer and controller as done for the standalone + # specviz application + self.viewer = App(disabled={'Data List': True}, + hidden={'Layer List': True, + 'Statistics': True, + 'Model Fitting': True, + 'Mask Editor': True, + 'Data List': True}) + + # Remove the menubar so that it does not interfere with Glue's + self.viewer.main_window.menu_bar = None + + # Remove Glue's viewer status bar + self.statusBar().hide() + + # Make the main toolbar smaller to fit better inside Glue + for tb in self.viewer._all_tool_bars.values(): + # tb['widget'].setToolButtonStyle(Qt.ToolButtonIconOnly) + tb['widget'].setIconSize(QSize(24, 24)) + + # Set the view mode of mdi area to tabbed so that user aren't confused + mdi_area = self.viewer.main_window.mdi_area + mdi_area.setViewMode(mdi_area.TabbedView) + mdi_area.setDocumentMode(True) + mdi_area.setTabPosition(QTabWidget.South) + + layer_list = self.viewer._instanced_plugins.get('Layer List') + self._layer_list = layer_list.widget() if layer_list is not None else None + + model_fitting = self.viewer._instanced_plugins.get('Model Fitting') + self._model_fitting = model_fitting.widget() if model_fitting is not None else None + + self._unified_options = QWidget() + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self._options_widget) + layout.addWidget(self._layer_widget) + layout.addWidget(self._layer_list) + + self._unified_options.setLayout(layout) + + self.setCentralWidget(self.viewer.main_window) + + # The following method is required by glue - it is used to subscribe the + # viewer to various messages sent by glue. + + def register_to_hub(self, hub): + super(SpecVizViewer, self).register_to_hub(hub) + + hub.subscribe(self, msg.SubsetCreateMessage, + handler=self._add_subset) + + hub.subscribe(self, msg.SubsetUpdateMessage, + handler=self._update_subset) + + hub.subscribe(self, msg.SubsetDeleteMessage, + handler=self._remove_subset) + + hub.subscribe(self, msg.DataUpdateMessage, + handler=self._update_data) + + def _spectrum_from_component(self, layer, component, wcs, mask=None): + data = SpectralCube(component.data, wcs) + + if mask is not None: + data = data.with_mask(mask) + + spec_data = data.sum((1, 2)) + + spec_data = Spectrum1DRef(spec_data.data, + unit=spec_data.unit, + dispersion=data.spectral_axis.data, + dispersion_unit=data.spectral_axis.unit, + wcs=data.wcs) + + # Store the relation between the component and the specviz data. If + # the data exists, first remove the component from specviz and then + # re-add it. + if layer in self._specviz_data_cache: + old_spec_data = self._specviz_data_cache[layer] + dispatch.on_remove_data.emit(old_spec_data) + + self._specviz_data_cache[layer] = spec_data + + dispatch.on_add_to_window.emit(spec_data) + + def _update_combo_boxes(self, data): + if data not in self._layer_widget: + self._layer_widget.add_layer(data) + + self._layer_widget.layer = data + self._options_widget.set_data(self._layer_widget.layer) + + if self._options_widget.file_att is None: + return False + + if self._layer_widget.layer is None: + return False + + return True + + # The following two methods are required by glue - they are what gets called + # when a dataset or subset gets dragged and dropped onto the viewer. + + def add_data(self, data): + if not self._update_combo_boxes(data): + return + + layer = self._layer_widget.layer + cid = layer.id[self._options_widget.file_att] + component = layer.get_component(cid) + + self._spectrum_from_component(layer, component, data.coords.wcs) + + return True + + def add_subset(self, subset): + # We avoid doing any real work here, as adding a subset does not + # simultaneously add the subset mask. We therefore move the + # functionality to the update subset method. + return True + + # The following four methods are used to receive various messages related + # to updates to data or subsets. + + def _update_data(self, message): + print("Updating data") + + def _add_subset(self, message): + self.add_subset(message.subset) + + def _update_subset(self, message): + if not self._update_combo_boxes(message.subset): + return + + subset = self._layer_widget.layer + cid = subset.data.id[self._options_widget.file_att] + mask = subset.to_mask() + component = subset.data.get_component(cid) + + self._spectrum_from_component(subset, component, + subset.data.coords.wcs, mask=mask) + + def _remove_subset(self, message): + if message.subset in self._layer_widget: + self._layer_widget.remove_layer(message.subset) + + subset = self._layer_widget.layer + + spec_data = self._specviz_data_cache.pop(subset) + dispatch.on_remove_data.emit(spec_data) + + # When the selected layer is changed, we need to update the combo box with + # the attributes from which the filename attribute can be selected. The + # following method gets called in this case. + + def _update_options(self): + self._options_widget.set_data(self._layer_widget.layer) + + def initialize_toolbar(self): + pass + + def layer_view(self): + return self._unified_options + + def options_widget(self): + return self._model_fitting diff --git a/specviz/external/glue/layer_widget.py b/specviz/third_party/glue/layer_widget.py similarity index 100% rename from specviz/external/glue/layer_widget.py rename to specviz/third_party/glue/layer_widget.py diff --git a/specviz/external/glue/layer_widget.ui b/specviz/third_party/glue/layer_widget.ui similarity index 53% rename from specviz/external/glue/layer_widget.ui rename to specviz/third_party/glue/layer_widget.ui index 2fbf4a68..ccbd52b8 100644 --- a/specviz/external/glue/layer_widget.ui +++ b/specviz/third_party/glue/layer_widget.ui @@ -13,40 +13,36 @@ Form - - - - - - 75 - true - - + + + QFormLayout::AllNonFixedFieldsGrow + + + 0 + + + 0 + + + 0 + + + 0 + + + - Active layer + Layer - + QComboBox::AdjustToMinimumContentsLength - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/specviz/external/glue/version.py b/specviz/third_party/glue/version.py similarity index 100% rename from specviz/external/glue/version.py rename to specviz/third_party/glue/version.py diff --git a/specviz/external/glue/viewer_options.py b/specviz/third_party/glue/viewer_options.py similarity index 96% rename from specviz/external/glue/viewer_options.py rename to specviz/third_party/glue/viewer_options.py index facacc3a..3a1f667c 100644 --- a/specviz/external/glue/viewer_options.py +++ b/specviz/third_party/glue/viewer_options.py @@ -22,7 +22,7 @@ def __init__(self, parent=None, data_viewer=None): directory=os.path.dirname(__file__)) self.file_helper = ComponentIDComboHelper(self.ui.combo_file_attribute, - data_viewer._data, categorical=True, numeric=False) + data_viewer._data) self._data_viewer = data_viewer diff --git a/specviz/external/glue/viewer_options.ui b/specviz/third_party/glue/viewer_options.ui similarity index 52% rename from specviz/external/glue/viewer_options.ui rename to specviz/third_party/glue/viewer_options.ui index 86837504..24ae3f0d 100644 --- a/specviz/external/glue/viewer_options.ui +++ b/specviz/third_party/glue/viewer_options.ui @@ -13,40 +13,36 @@ Form - - - - - - 75 - true - - + + + QFormLayout::AllNonFixedFieldsGrow + + + 0 + + + 0 + + + 0 + + + 0 + + + - Attribute giving the filename of spectra: + Attribute - + QComboBox::AdjustToMinimumContentsLength - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/specviz/widgets/linelists_window.py b/specviz/widgets/linelists_window.py index 41474b33..be6f60a1 100644 --- a/specviz/widgets/linelists_window.py +++ b/specviz/widgets/linelists_window.py @@ -10,7 +10,7 @@ from qtpy.QtCore import (QSize, QRect, QCoreApplication, QMetaObject, Qt, QAbstractTableModel, QVariant, QSortFilterProxyModel) -from ..core.comms import dispatch +from ..core.events import dispatch #TODO work in progress diff --git a/specviz/widgets/plugin.py b/specviz/widgets/plugin.py index 2fb40949..7e8a1050 100644 --- a/specviz/widgets/plugin.py +++ b/specviz/widgets/plugin.py @@ -1,13 +1,16 @@ from abc import ABCMeta, abstractmethod, abstractproperty import six +import os from qtpy.QtCore import Qt, QRect from qtpy.QtWidgets import (QDockWidget, QScrollArea, QFrame, QWidget, QMenu, QAction, QWidgetAction, QToolButton) from qtpy.QtGui import QIcon +from qtpy.uic import loadUi -from ..core.comms import DispatchHandle +from ..core.events import dispatch from ..interfaces.registries import plugin_registry +from ..widgets.utils import UI_PATH class PluginMeta(type): @@ -40,33 +43,20 @@ def __init__(self, parent=None): self._active_window = None self._current_layer = None - DispatchHandle.setup(self) + dispatch.setup(self) # GUI Setup self.setAllowedAreas(Qt.AllDockWidgetAreas) - self.scroll_area = QScrollArea() - self.scroll_area.setFrameShape(QFrame.NoFrame) - self.scroll_area.setFrameShadow(QFrame.Plain) - self.scroll_area.setLineWidth(0) - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setGeometry(QRect(0, 0, 100, 100)) - # self.scroll_area.setSizePolicy( - # QSizePolicy.Fixed, QSizePolicy.Fixed) - # self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) - # self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + loadUi(os.path.join(UI_PATH, "plugin.ui"), self) - # The main widget inside the scroll area - self.contents = QWidget() - - self.scroll_area.setWidget(self.contents) - - self.setWidget(self.scroll_area) self.setWindowTitle(self.name) self.setup_ui() self.setup_connections() + self.contents.resize(self.contents.sizeHint()) + def _set_name(self, value): if isinstance(value, str): self.name = value @@ -152,11 +142,11 @@ def active_window(self): def current_layer(self): return self._current_layer - @DispatchHandle.register_listener("on_activated_window") + @dispatch.register_listener("on_activated_window") def set_active_window(self, window): self._active_window = window - @DispatchHandle.register_listener("on_selected_layer") + @dispatch.register_listener("on_selected_layer") def set_active_layer(self, layer_item): if layer_item is not None: self._current_layer = layer_item.data(0, Qt.UserRole) diff --git a/specviz/widgets/sub_windows.py b/specviz/widgets/sub_windows.py index 242df017..db04d569 100644 --- a/specviz/widgets/sub_windows.py +++ b/specviz/widgets/sub_windows.py @@ -15,7 +15,7 @@ QLineEdit, QPushButton, QWidget) from qtpy.QtCore import QEvent, Qt -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch from ..core.linelist import ingest, LineList, WAVELENGTH_COLUMN, ID_COLUMN from ..core.plots import LinePlot from ..core.annotation import LineIDMarker @@ -127,7 +127,7 @@ def __init__(self, *args, **kwargs): self.disable_errors = False self.disable_mask = True - DispatchHandle.setup(self) + dispatch.setup(self) self._dynamic_axis = DynamicAxisItem(orientation='top') self._plot_widget = pg.PlotWidget( @@ -306,7 +306,7 @@ def update_axis(self, layer=None, mode=None, **kwargs): def update_plot_item(self): self._plot_item.update() - @DispatchHandle.register_listener("on_update_model") + @dispatch.register_listener("on_update_model") def update_plot(self, layer=None, plot=None): if layer is not None: plot = self.get_plot(layer) @@ -319,10 +319,10 @@ def closeEvent(self, event): # any line lists window that might be still open. dispatch.on_dismiss_linelists_window.emit() - DispatchHandle.tear_down(self) + dispatch.tear_down(self) super(PlotSubWindow, self).closeEvent(event) - @DispatchHandle.register_listener("on_add_layer") + @dispatch.register_listener("on_add_layer") def add_plot(self, layer, window=None): if window is not None and window != self: return @@ -334,44 +334,41 @@ def comp_disp(plot, layer): pstep = np.mean(plot.layer.dispersion[1:] - plot.layer.dispersion[:-1]) - return lstep == pstep - - # print(all(map(lambda p: comp_disp(p, layer=layer), self._plots))) + return np.isclose(lstep.value, pstep.value) if not all(map(lambda p: comp_disp(p, layer=layer), self._plots)): - logging.error("New layer {} does not have the same dispersion as " - "current plot data.".format(layer.name)) + logging.warning("New layer '{}' does not have the same dispersion " + "as current plot data.".format(layer.name)) resample_dialog = ResampleDialog() if resample_dialog.exec_(): - in_data = np.ones(layer.dispersion.data.value.shape, - [('wave', float), ('data', float), + in_data = np.ones(layer.dispersion.shape, + [('wave', float), + ('data', float), ('err', float)]) in_data['wave'] = layer.dispersion.data.value in_data['data'] = layer.data.data.value in_data['err'] = layer.uncertainty.array - pstep = np.mean( - self._plots[0].layer.dispersion.data.value[1:] - - self._plots[0].layer.dispersion.data.value[:-1]) - wave_out = np.arange(layer.dispersion.data.value[0], - layer.dispersion.data.value[-1], pstep) + plot = self._plots[0] - out_data = resample(in_data, 'wave', wave_out, ('data', 'err'), + out_data = resample(in_data, 'wave', plot.layer.dispersion.data.value, + ('data', 'err'), kind=resample_dialog.method) - new_data = layer.__class__.copy( + new_data = layer.copy( layer, data=out_data['data'], - dispersion=wave_out, - uncertainty=layer.uncertainty.__class__( - out_data['err']), - mask=None) + dispersion=out_data['wave'], + uncertainty=layer.uncertainty.__class__(out_data['err']), + dispersion_unit=layer.dispersion_unit) - layer = layer.__class__.from_parent( - new_data, name="Interpolated {}".format(layer.name)) + layer = layer.from_parent( + new_data, + name="Interpolated {}".format(layer.name), + layer_mask=layer.layer_mask) new_plot = LinePlot.from_layer(layer, color=next(self._available_colors)) @@ -383,8 +380,9 @@ def comp_disp(plot, layer): is_convert_success = new_plot.change_units(*self._plot_units) if not is_convert_success[0] or not is_convert_success[1]: - logging.error("Unable to convert units of '{}' to current plot" - " units.".format(new_plot.layer.name)) + logging.error("Unable to convert {} axis units of '{}' to current plot" + " units.".format('x' if not is_convert_success[0] else 'y', + new_plot.layer.name)) dispatch.on_remove_layer.emit(layer=layer) return @@ -406,7 +404,7 @@ def comp_disp(plot, layer): dispatch.on_added_layer.emit(layer=layer) dispatch.on_added_plot.emit(plot=new_plot, window=window) - @DispatchHandle.register_listener("on_removed_layer") + @dispatch.register_listener("on_removed_layer") def remove_plot(self, layer, window=None): if window is not None and window != self: return @@ -453,13 +451,13 @@ def _find_wavelength_range(self): return (amin, amax) - @DispatchHandle.register_listener("on_request_linelists") + @dispatch.register_listener("on_request_linelists") def _request_linelists(self, *args, **kwargs): self.waverange = self._find_wavelength_range() self.linelists = ingest(self.waverange) - @DispatchHandle.register_listener("on_plot_linelists") + @dispatch.register_listener("on_plot_linelists") def _plot_linelists(self, table_views, **kwargs): if not self._is_selected: @@ -534,7 +532,7 @@ def _plot_linelists(self, table_views, **kwargs): plot_item.update() - @DispatchHandle.register_listener("on_erase_linelabels") + @dispatch.register_listener("on_erase_linelabels") def erase_linelabels(self, *args, **kwargs): if self._is_selected: for marker in self._line_labels: @@ -546,7 +544,7 @@ def erase_linelabels(self, *args, **kwargs): # line list window. It remains to be seen if it is what users # actually want. - @DispatchHandle.register_listener("on_activated_window") + @dispatch.register_listener("on_activated_window") def _set_selection_state(self, window): self._is_selected = window == self @@ -556,14 +554,14 @@ def _set_selection_state(self, window): else: self._linelist_window.hide() - @DispatchHandle.register_listener("on_show_linelists_window") + @dispatch.register_listener("on_show_linelists_window") def _show_linelists_window(self, *args, **kwargs): if self._is_selected: if self._linelist_window is None: self._linelist_window = LineListsWindow(self) self._linelist_window.show() - @DispatchHandle.register_listener("on_dismiss_linelists_window") + @dispatch.register_listener("on_dismiss_linelists_window") def _dismiss_linelists_window(self, *args, **kwargs): if self._is_selected and self._linelist_window: self._linelist_window.hide() diff --git a/specviz/widgets/windows.py b/specviz/widgets/windows.py index 931618aa..e2b794fa 100644 --- a/specviz/widgets/windows.py +++ b/specviz/widgets/windows.py @@ -2,67 +2,23 @@ QStatusBar, QMenuBar, QMdiArea) from qtpy.QtCore import Qt, QSize from qtpy.QtGui import QBrush, QColor +from qtpy.uic import loadUi +import os -from ..core.comms import dispatch, DispatchHandle +from ..core.events import dispatch, dispatch from ..core.data import Spectrum1DRefLayer from .sub_windows import PlotSubWindow +from ..widgets.utils import UI_PATH -class UiMainWindow(QMainWindow): - """ - Main application window - """ - def __init__(self, parent=None): - super(UiMainWindow, self).__init__(parent) - DispatchHandle.setup(self) - - self.showMaximized() - self.setMinimumSize(QSize(640, 480)) - self.setDockOptions(QMainWindow.AnimatedDocks) - self.setWindowTitle("SpecViz") - - self.widget_central = QWidget(self) - self.setCentralWidget(self.widget_central) - - # Toolbar - self.layout_vertical = QVBoxLayout(self.widget_central) - - # MDI area setup - self.mdi_area = MdiArea(self.widget_central) - self.mdi_area.setFrameShape(QFrame.StyledPanel) - self.mdi_area.setFrameShadow(QFrame.Plain) - self.mdi_area.setLineWidth(2) - brush = QBrush(QColor(200, 200, 200)) - brush.setStyle(Qt.SolidPattern) - self.mdi_area.setBackground(brush) - self.mdi_area.setAcceptDrops(True) - - self.layout_vertical.addWidget(self.mdi_area) - - # Menu bar setup - self.menu_bar = QMenuBar(self) - - self.menu_file = QMenu(self.menu_bar) - self.menu_file.setTitle("File") - self.menu_edit = QMenu(self.menu_bar) - self.menu_edit.setTitle("Edit") - self.menu_view = QMenu(self.menu_bar) - self.menu_edit.setTitle("View") - - self.menu_docks = QMenu(self.menu_bar) - - self.setMenuBar(self.menu_bar) - - # Status bar setup - self.status_bar = QStatusBar(self) - - self.setStatusBar(self.status_bar) - - -class MainWindow(UiMainWindow): +class MainWindow(QMainWindow): def __init__(self, parent=None, *args, **kwargs): super(MainWindow, self).__init__(parent) + dispatch.setup(self) + + loadUi(os.path.join(UI_PATH, "main_window.ui"), self) + self.mdi_area.subWindowActivated.connect(self._set_activated_window) def _set_activated_window(self, window): @@ -77,28 +33,35 @@ def _set_activated_window(self, window): dispatch.on_activated_window.emit( window=window.widget() if window is not None else None) - @DispatchHandle.register_listener("on_add_window", "on_add_to_window") - def add_sub_window(self, data=None, layer=None, window=None, *args, - **kwargs): + @dispatch.register_listener("on_add_window") + def add_sub_window(self, data=None, layer=None, window=None): layer = layer or Spectrum1DRefLayer.from_parent(data) is_new_window = window is None window = window or PlotSubWindow() - if not isinstance(layer, list): - layer = [layer] - - for l in layer: - dispatch.on_add_layer.emit(layer=l, window=window) - window.setWindowTitle(l.name) + dispatch.on_add_layer.emit(layer=layer, window=window) + window.setWindowTitle(layer.name) if window is not None and is_new_window: mdi_sub_window = self.mdi_area.addSubWindow(window) window.show() self._set_activated_window(mdi_sub_window) - @DispatchHandle.register_listener("on_add_roi") + if self.mdi_area.viewMode() == self.mdi_area.TabbedView: + window.showMaximized() + + @dispatch.register_listener("on_add_to_window") + def add_to_window(self, data=None, window=None): + # Find any sub windows currently active + window = window or next((x.widget() for x in self.mdi_area.subWindowList( + order=self.mdi_area.ActivationHistoryOrder)), None) + + self.add_sub_window(data=data, window=window) + + @dispatch.register_listener("on_add_roi") def add_roi(self, bounds=None, *args, **kwargs): - mdi_sub_window = self.mdi_area.activeSubWindow() + mdi_sub_window = self.mdi_area.activeSubWindow() or next((x for x in self.mdi_area.subWindowList( + order=self.mdi_area.ActivationHistoryOrder)), None) if mdi_sub_window is not None: window = mdi_sub_window.widget() @@ -109,7 +72,7 @@ def get_roi_bounds(self): return sw.get_roi_bounds() - @DispatchHandle.register_listener("on_status_message") + @dispatch.register_listener("on_status_message") def update_message(self, message, timeout=0): self.status_bar.showMessage(message, timeout) diff --git a/specviz/widgets/wizard.py b/specviz/widgets/wizard.py index 823dc659..3f8dca16 100644 --- a/specviz/widgets/wizard.py +++ b/specviz/widgets/wizard.py @@ -23,7 +23,7 @@ from qtpy.QtWidgets import QApplication, QDialog, QVBoxLayout, QWidget, QPlainTextEdit, QPushButton from qtpy.uic import loadUi -from ..core.comms import dispatch +from ..core.events import dispatch from ..core.data import Spectrum1DRef from ..interfaces.loaders import load_yaml_reader from ..widgets.utils import UI_PATH