Skip to content

Commit

Permalink
Merge pull request #1403 from astrofrog/qt-fixed-layout-tab
Browse files Browse the repository at this point in the history
Add ability to create fixed layout tabs
  • Loading branch information
astrofrog authored Sep 26, 2017
2 parents d69fca0 + 8b16521 commit 35bceb8
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 32 deletions.
53 changes: 53 additions & 0 deletions doc/customizing_guide/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,57 @@ This example then looks this the following once glue is loaded:
.. image:: images/preferences.png
:align: center

Custom data viewer
------------------

For information on registering a custom data viewer, see
:doc:`full_custom_qt_viewer`.

Custom fixed layout tab
-----------------------

.. note:: this feature is still experimental and may change in future

By default, the main canvas of glue is a free-form canvas where windows can be
moved around and resized. However, it is also possible to construct fixed
layouts to create 'dashboards'. To do this, you should import the ``qt_fixed_layout_tab``
object::

from glue.config import qt_fixed_layout_tab

then use it to decorate a Qt widget that should be used instead of the free-form
canvas area, e.g.::

@qt_fixed_layout_tab
def MyCustomLayout(QWidget):
pass

The widget can be any valid Qt widget - for instance it could be a widget with
a grid layout with data viewer widgets in each cell.

Custom startup actions
----------------------

It is possible to define actions to be carried out in glue once glue is open
and the data has been loaded. These should be written using the
``startup_action`` decorator::

from glue.config import startup_action

@startup_action("action_name")
def my_startup_action(session, data_collection):
# do anything here
return

The function has access to ``session``, which includes for example
``session.application``, and thus gives access to the full state of glue.

Startup actions have to then be explicitly specified using::

glue --startup=action_name

and multiple actions can be given as a comma-separated string.

Complete list of registries
---------------------------

Expand All @@ -345,6 +396,7 @@ provides more information about what the registry is and how it can be used.
Registry name Registry class
========================== =======================================================
``qt_client`` :class:`glue.config.QtClientRegistry`
``qt_fixed_layout_tab`` :class:`glue.config.QtFixedLayoutTabRegistry`
``viewer_tool`` :class:`glue.config.ViewerToolRegistry`
``data_factory`` :class:`glue.config.DataFactoryRegistry`
``data_exporter`` :class:`glue.config.DataExporterRegistry`
Expand All @@ -358,6 +410,7 @@ Registry name Registry class
``preference_panes`` :class:`glue.config.PreferencePanesRegistry`
``fit_plugin`` :class:`glue.config.ProfileFitterRegistry`
``layer_action`` :class:`glue.config.LayerActionRegistry`
``startup_action`` :class:`glue.config.StartupActionRegistry`
========================== =======================================================

.. _lazy_load_plugin:
Expand Down
76 changes: 67 additions & 9 deletions glue/app/qt/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from glue.app.qt.plugin_manager import QtPluginManager
from glue.app.qt.versions import show_glue_info
from glue.app.qt.terminal import glue_terminal, IPythonTerminalError

from glue.config import qt_fixed_layout_tab, qt_client, startup_action

__all__ = ['GlueApplication']
DOCS_URL = 'http://www.glueviz.org'
Expand Down Expand Up @@ -228,6 +228,12 @@ def __init__(self, data_collection=None, session=None):
self.new_tab()
self._update_plot_dashboard(None)

def run_startup_action(self, name):
if name in startup_action.members:
startup_action.members[name](self.session, self.data_collection)
else:
raise Exception("Unknown startup action: {0}".format(name))

def _setup_ui(self):
self._ui = load_ui('application.ui', None,
directory=os.path.dirname(__file__))
Expand Down Expand Up @@ -386,14 +392,14 @@ def new_tab(self):
tab.setCurrentWidget(widget)
widget.subWindowActivated.connect(self._update_plot_dashboard)

def close_tab(self, index):
def close_tab(self, index, warn=True):
""" Close a tab window and all associated data viewers """

# do not delete the last tab
if self.tab_widget.count() == 1:
return

if not os.environ.get('GLUE_TESTING'):
if warn and not os.environ.get('GLUE_TESTING'):
buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
dialog = QtWidgets.QMessageBox.warning(
self, "Confirm Close",
Expand Down Expand Up @@ -434,6 +440,20 @@ def add_widget(self, new_widget, label=None, tab=None,
of new_widget
:type hold_position: bool
"""

# Find first tab that supports addSubWindow
if tab is None:
if hasattr(self.current_tab, 'addSubWindow'):
pass
else:
for tab in range(self.tab_count):
page = self.tab(tab)
if hasattr(page, 'addSubWindow'):
break
else:
self.new_tab()
tab = self.tab_count - 1

page = self.tab(tab)
pos = getattr(new_widget, 'position', None)
sub = new_widget.mdi_wrap()
Expand All @@ -446,6 +466,9 @@ def add_widget(self, new_widget, label=None, tab=None,
page.setActiveSubWindow(sub)
if hold_position and pos is not None:
new_widget.move(pos[0], pos[1])

self.tab_widget.setCurrentWidget(page)

return sub

def _edit_settings(self):
Expand All @@ -458,10 +481,8 @@ def gather_current_tab(self):

def _get_plot_dashboards(self, sub_window):

if not isinstance(sub_window, GlueMdiSubWindow):
return QtWidgets.QWidget(), QtWidgets.QWidget(), ""

widget = sub_window.widget()

if not isinstance(widget, DataViewer):
return QtWidgets.QWidget(), QtWidgets.QWidget(), ""

Expand Down Expand Up @@ -568,6 +589,7 @@ def _create_menu(self):
menu.setTitle("&Canvas")
menu.addAction(self._actions['tab_new'])
menu.addAction(self._actions['viewer_new'])
menu.addAction(self._actions['fixed_layout_tab_new'])
menu.addSeparator()
menu.addAction(self._actions['gather'])
menu.addAction(self._actions['tab_rename'])
Expand Down Expand Up @@ -628,6 +650,17 @@ def _create_actions(self):
a.triggered.connect(nonpartial(self.choose_new_data_viewer))
self._actions['viewer_new'] = a

if len(qt_client.members) == 0:
a.setEnabled(False)

a = action("New Fixed Layout Tab", self,
tip="Create a new tab with a fixed layout")
a.triggered.connect(nonpartial(self.choose_new_fixed_layout_tab))
self._actions['fixed_layout_tab_new'] = a

if len(qt_fixed_layout_tab.members) == 0:
a.setEnabled(False)

a = action('New &Tab', self,
shortcut=QtGui.QKeySequence.AddTab,
tip='Add a new tab')
Expand Down Expand Up @@ -733,12 +766,36 @@ def _create_actions(self):
a.triggered.connect(nonpartial(self.plugin_manager))
self._actions['plugin_manager'] = a

def choose_new_fixed_layout_tab(self):
"""
Creates a new tab with a fixed layout
"""

tab_cls = pick_class(list(qt_fixed_layout_tab.members), title='Fixed layout tab',
label="Choose a new fixed layout tab",
sort=True)

return self.add_fixed_layout_tab(tab_cls)

def add_fixed_layout_tab(self, tab_cls):

tab = tab_cls(session=self.session)

self._total_tab_count += 1

name = 'Tab {0}'.format(self._total_tab_count)
if hasattr(tab, 'LABEL'):
name += ': ' + tab.LABEL
self.tab_widget.addTab(tab, name)
self.tab_widget.setCurrentWidget(tab)
tab.subWindowActivated.connect(self._update_plot_dashboard)

return tab

def choose_new_data_viewer(self, data=None):
""" Create a new visualization window in the current tab
"""

from glue.config import qt_client

if data and data.ndim == 1 and ScatterViewer in qt_client.members:
default = ScatterViewer
elif data and data.ndim > 1 and ImageViewer in qt_client.members:
Expand Down Expand Up @@ -888,6 +945,8 @@ def _create_terminal(self):
self._hide_terminal()

def _toggle_terminal(self):
if self._terminal is None:
self._create_terminal()
if self._terminal.isVisible():
self._hide_terminal()
if self._terminal.isVisible():
Expand Down Expand Up @@ -924,7 +983,6 @@ def start(self, size=None, position=None, block=True, maximized=True):
position : (int, int) Optional
The default position of the application
"""
self._create_terminal()
if maximized:
self.showMaximized()
else:
Expand Down
8 changes: 4 additions & 4 deletions glue/app/qt/tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,18 +366,18 @@ def test_logger_close():

def test_reset_session_terminal():

# Regression test to make sure that the terminal still exists when
# Regression test to make sure that the terminal still works when
# resetting a session

app = GlueApplication()
app2 = app._reset_session(warn=False)

assert app2.has_terminal(create_if_not=False)
assert app2.has_terminal()


def test_open_session_terminal(tmpdir):

# Regression test to make sure that the terminal still exists when
# Regression test to make sure that the terminal still works when
# opening a previous session

session_file = tmpdir.join('test.glu').strpath
Expand All @@ -387,4 +387,4 @@ def test_open_session_terminal(tmpdir):

app2 = app.restore_session(session_file)

assert app2.has_terminal(create_if_not=False)
assert app2.has_terminal()
50 changes: 49 additions & 1 deletion glue/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
'fit_plugin', 'auto_refresh', 'importer', 'DictRegistry',
'preference_panes', 'PreferencePanesRegistry',
'DataExporterRegistry', 'data_exporter', 'layer_action',
'SubsetMaskExporterRegistry', 'SubsetMaskImporterRegistry']
'SubsetMaskExporterRegistry', 'SubsetMaskImporterRegistry',
'StartupActionRegistry', 'startup_action', 'QtFixedLayoutTabRegistry',
'qt_fixed_layout_tab']


CFG_DIR = os.path.join(os.path.expanduser('~'), '.glue')
Expand Down Expand Up @@ -478,6 +480,18 @@ class CustomWidget(QMainWindow):
"""


class QtFixedLayoutTabRegistry(Registry):
"""
Stores Qt pre-defined tabs (non-MDI)
New widgets can be registered via::
@qt_fixed_layout_tab
class CustomTab(QWidget):
...
"""


class ViewerToolRegistry(DictRegistry):

def add(self, tool_cls):
Expand All @@ -496,6 +510,37 @@ def __call__(self, tool_cls):
return tool_cls


class StartupActionRegistry(DictRegistry):

def add(self, startup_name, startup_function):
"""
Add a startup function to the registry. This is a function that will
get called once glue has been started and any data loaded, and can
be used to set up specific layouts and create links.
Startup actions are triggered by either specifying comma-separated names
of actions on the command-line::
glue --startup=mystartupaction
or by passing an iterable of startup action names to the ``startup``
keyword of ``GlueApplication``.
The startup function will be given the session object and the data
collection object.
"""
if startup_name in self.members:
raise ValueError("A startup action with the name '{0}' already exists".format(startup_name))
else:
self.members[startup_name] = startup_function

def __call__(self, name):
def adder(func):
self.add(name, func)
return func
return adder


class LinkFunctionRegistry(Registry):

"""Stores functions to convert between quantities
Expand Down Expand Up @@ -624,7 +669,9 @@ def __call__(self, state=None):

return self.state


qt_client = QtClientRegistry()
qt_fixed_layout_tab = QtFixedLayoutTabRegistry()
viewer_tool = ViewerToolRegistry()
link_function = LinkFunctionRegistry()
link_helper = LinkHelperRegistry()
Expand All @@ -637,6 +684,7 @@ def __call__(self, state=None):
menubar_plugin = MenubarPluginRegistry()
preference_panes = PreferencePanesRegistry()
qglue_parser = QGlueParserRegistry()
startup_action = StartupActionRegistry()

# watch loaded data files for changes?
auto_refresh = BooleanSetting(False)
Expand Down
7 changes: 5 additions & 2 deletions glue/core/application_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def _update_undo_redo_enabled(self):
raise NotImplementedError()

@classmethod
def add_datasets(cls, data_collection, datasets):
def add_datasets(cls, data_collection, datasets, auto_merge=False):
""" Utility method to interactively add datasets to a
data_collection
Expand Down Expand Up @@ -269,7 +269,10 @@ def add_datasets(cls, data_collection, datasets):
if not other:
continue

merges, label = cls._choose_merge(data, other)
if auto_merge:
merges, label = [data] + other, data.label
else:
merges, label = cls._choose_merge(data, other)

if merges:
data_collection.merge(*merges, label=label)
Expand Down
6 changes: 6 additions & 0 deletions glue/external/modest_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ def invalidate_cache(self):
def get_cursor_data(self, event):
return None

def contains(self, mouseevent):
if self._A is None or self._A.shape is None:
return False
else:
return super(ModestImage, self).contains(mouseevent)

def set_extent(self, extent):
self._full_extent = extent
self.invalidate_cache()
Expand Down
Loading

0 comments on commit 35bceb8

Please sign in to comment.