Skip to content

Commit

Permalink
Merge pull request #381 from astrofrog/split-out-qt
Browse files Browse the repository at this point in the history
Refactor viewer classes to split out Qt from non-Qt part, and define Jupyter viewers
  • Loading branch information
astrofrog authored May 23, 2024
2 parents 43c5803 + bd4e35f commit 7a66783
Show file tree
Hide file tree
Showing 74 changed files with 1,307 additions and 414 deletions.
35 changes: 25 additions & 10 deletions .github/workflows/ci_workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,34 @@ jobs:
- libegl1-mesa
envs: |
# Standard tests
# Tests without Qt. For now glfw can't run on MacOS runners in headless mode
# so we only run on Linux and Windows.
- linux: py38-test
- linux: py39-test
- linux: py310-test-pyqt6
- linux: py311-test-dev-pyqt6
- windows: py39-test
- linux: py310-test
- windows: py311-test
- linux: py311-test-dev
- macos: py38-test
- macos: py39-test-dev
- macos: py310-test-pyqt6
# Tests with Jupyter
- linux: py38-test-jupyter
- windows: py39-test-jupyter
- linux: py310-test-jupyter
- windows: py311-test-jupyter
# Tests with Qt
- linux: py38-test-pyqt63
- linux: py39-test-pyside63
- linux: py310-test-pyqt64
- linux: py311-test-dev-pyqt64
# - macos: py38-test-pyqt63 # segfault
- macos: py39-test-pyside63
- macos: py310-test-pyqt64
- macos: py311-test-dev-pyqt64
- windows: py38-test-pyqt63
- windows: py39-test-pyside63
- windows: py310-test-pyqt64
- windows: py311-test-dev-pyqt64
- windows: py38-test
- windows: py39-test-dev
- windows: py310-test-pyqt6
publish:
needs: tests
Expand Down
13 changes: 12 additions & 1 deletion glue_vispy_viewers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import importlib.metadata

__version__ = importlib.metadata.version('glue-vispy-viewers')
__version__ = importlib.metadata.version("glue-vispy-viewers")

try:
import OpenGL # noqa
except ImportError:
raise ImportError("The PyOpenGL package is required for this plugin")
else:
del OpenGL

# Ensure we can read old session files prior to the Qt/Jupyter split
from glue.core.state import PATH_PATCHES

PATH_PATCHES[
"glue_vispy_viewers.scatter.scatter_viewer.VispyScatterViewer"
] = "glue_vispy_viewers.scatter.qt.scatter_viewer.VispyScatterViewer"
PATH_PATCHES[
"glue_vispy_viewers.volume.volume_viewer.VispyVolumeViewer"
] = "glue_vispy_viewers.volume.qt.volume_viewer.VispyVolumeViewer"
del PATH_PATCHES
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions glue_vispy_viewers/common/jupyter/toolbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import weakref
from glue_jupyter.common.toolbar_vuetify import BasicJupyterToolbar
from ..toolbar import VispyViewerToolbarMixin

__all__ = ['VispyJupyterToolbar']


class VispyJupyterToolbar(VispyViewerToolbarMixin, BasicJupyterToolbar):

def __init__(self, viewer=None, **kwargs):
BasicJupyterToolbar.__init__(self, viewer, **kwargs)
self._viewer = weakref.ref(viewer)

@property
def viewer(self):
return self._viewer()
Empty file.
52 changes: 52 additions & 0 deletions glue_vispy_viewers/common/qt/data_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from glue_qt.viewers.common.data_viewer import DataViewer

from qtpy import QtWidgets

from .viewer_options import VispyOptionsWidget

from ..vispy_data_viewer import BaseVispyViewerMixin

from .toolbar import VispyQtToolbar

from . import tools # noqa

BROKEN_PYQT5_MESSAGE = ("The version of PyQt5 you are using does not appear to "
"support OpenGL. See <a href='http://docs.glueviz.org/en"
"/stable/known_issues.html#d-viewers-not-working-on-linux"
"-with-pyqt5'>here</a> for more information about fixing "
"this issue.")


class BaseVispyViewer(BaseVispyViewerMixin, DataViewer):

_options_cls = VispyOptionsWidget
subtools = {'save': ['vispy:save']}

_toolbar_cls = VispyQtToolbar

def __init__(self, session, state=None, parent=None):
super().__init__(session, state=state, parent=parent)
self.setup_widget_and_callbacks()
self.setCentralWidget(self._vispy_widget.canvas.native)

def paintEvent(self, *args, **kwargs):
super().paintEvent(*args, **kwargs)
if self._opengl_ok is None:
self._opengl_ok = self._vispy_widget.canvas.native.context() is not None
if not self._opengl_ok:
QtWidgets.QMessageBox.critical(self, "Error", BROKEN_PYQT5_MESSAGE)
self.close(warn=False)
self._vispy_widget.canvas.native.close()

def show_status(self, text):
statusbar = self.statusBar()
statusbar.showMessage(text)


# If imageio is available, we can add the record icon
try:
import imageio # noqa
except ImportError:
pass
else:
BaseVispyViewer.tools.insert(1, 'vispy:record')
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import os
import sys
import pytest
from mock import patch
from unittest.mock import patch

from glue_qt.app import GlueApplication
from glue.core import Data

from ...scatter.scatter_viewer import VispyScatterViewer
from ....scatter.qt.scatter_viewer import VispyScatterViewer

from .. import tools # noqa:
from ... import tools # noqa:

try:
import imageio # noqa
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import numpy as np
import pytest
import sys
from mock import patch
from unittest.mock import patch

from glue.core import Data, DataCollection
from glue_qt.app import GlueApplication

from glue.core.tests.util import simple_session

from ..vispy_data_viewer import BaseVispyViewer
from ...volume.volume_viewer import VispyVolumeViewer
from ...scatter.scatter_viewer import VispyScatterViewer
from ..data_viewer import BaseVispyViewer
from ....volume.qt.volume_viewer import VispyVolumeViewer
from ....scatter.qt.scatter_viewer import VispyScatterViewer

IS_WIN = sys.platform == 'win32'

Expand Down
17 changes: 17 additions & 0 deletions glue_vispy_viewers/common/qt/toolbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import weakref

from glue_qt.viewers.common.toolbar import BasicToolbar
from ..toolbar import VispyViewerToolbarMixin

__all__ = ['VispyQtToolbar']


class VispyQtToolbar(VispyViewerToolbarMixin, BasicToolbar):

def __init__(self, viewer=None, **kwargs):
BasicToolbar.__init__(self, viewer, **kwargs)
self._viewer = weakref.ref(viewer)

@property
def viewer(self):
return self._viewer()
83 changes: 83 additions & 0 deletions glue_vispy_viewers/common/qt/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import os

from qtpy import QtGui, compat

from glue.viewers.common.tool import Tool, CheckableTool

from glue.config import viewer_tool

from vispy import app, io

RECORD_START_ICON = os.path.join(os.path.dirname(__file__), 'glue_record_start.png')
RECORD_STOP_ICON = os.path.join(os.path.dirname(__file__), 'glue_record_stop.png')


@viewer_tool
class SaveTool(Tool):

icon = 'glue_filesave'
tool_id = 'vispy:save'
action_text = 'Save the figure to a file'
tool_tip = 'Save the figure to a file'

def activate(self):
outfile, file_filter = compat.getsavefilename(caption='Save File',
filters='PNG Files (*.png);;'
'JPEG Files (*.jpeg);;'
'TIFF Files (*.tiff);;',
selectedfilter='PNG Files (*.png);;')

# This indicates that the user cancelled
if not outfile:
return
img = self.viewer._vispy_widget.canvas.render()
try:
file_filter = str(file_filter).split()[0]
io.imsave(outfile, img, format=file_filter)
except ImportError:
# TODO: give out a window to notify that only .png file format is supported
if '.' not in outfile:
outfile += '.png'
io.write_png(outfile, img)


@viewer_tool
class RecordTool(CheckableTool):

icon = RECORD_START_ICON
tool_id = 'vispy:record'
action_text = 'Record an animation'
tool_tip = 'Start/Stop the recording'

def __init__(self, viewer):
super(RecordTool, self).__init__(viewer=viewer)
self.record_timer = app.Timer(connect=self.record)
self.writer = None
self.next_action = 'start'

def activate(self):

# pop up a window for file saving
outfile, file_filter = compat.getsavefilename(caption='Save Animation',
filters='GIF Files (*.gif);;')

# if outfile is not set, the user cancelled
if outfile:
import imageio
self.set_icon(RECORD_STOP_ICON)
self.writer = imageio.get_writer(outfile)
self.record_timer.start(0.1)

def deactivate(self):

self.record_timer.stop()
if self.writer is not None:
self.writer.close()
self.set_icon(RECORD_START_ICON)

def set_icon(self, icon):
self.viewer.toolbar.actions[self.tool_id].setIcon(QtGui.QIcon(icon))

def record(self, event):
im = self.viewer._vispy_widget.canvas.render()
self.writer.append_data(im)
File renamed without changes.
File renamed without changes.
28 changes: 16 additions & 12 deletions glue_vispy_viewers/common/selection_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,23 @@ class VispyMouseMode(CheckableTool):

def __init__(self, viewer):
super(VispyMouseMode, self).__init__(viewer)
self._vispy_widget = viewer._vispy_widget
self.current_visible_array = None

def activate(self):
self.viewer.toolbar.activate_tool(self)
self.reset()

def deactivate(self):
self.viewer.toolbar.deactivate_tool(self)
self.reset()

def reset(self):
pass

@property
def _vispy_widget(self):
return self.viewer._vispy_widget

def get_visible_data(self):
visible = []
# Loop over visible layer artists
Expand Down Expand Up @@ -85,11 +99,7 @@ class LassoSelectionMode(VispyMouseMode):
def __init__(self, viewer):
super(LassoSelectionMode, self).__init__(viewer)
self.line = Line(color='purple',
width=2, method='agg',
parent=self._vispy_widget.canvas.scene)

def activate(self):
self.reset()
width=2, method='agg')

def reset(self):
self.line_pos = []
Expand Down Expand Up @@ -133,9 +143,6 @@ def __init__(self, viewer):
self.rectangle = Rectangle(center=(0, 0), width=1, height=1, border_width=2,
color=(0, 0, 0, 0), border_color='purple')

def activate(self):
self.reset()

def reset(self):
self.corner1 = None
self.corner2 = None
Expand Down Expand Up @@ -188,9 +195,6 @@ def __init__(self, viewer):
self.ellipse = Ellipse(center=(0, 0), radius=1, border_width=2,
color=(0, 0, 0, 0), border_color='purple')

def activate(self):
self.reset()

def reset(self):
self.center = None
self.radius = 0
Expand Down
5 changes: 3 additions & 2 deletions glue_vispy_viewers/common/tests/test_3d_axis_visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ def test_3d_axis_visual():
fov=0., distance=4.0)
AxesVisual3D(view=view, axis_color='red', transform=scene_transform)

canvas.native.show()
canvas.native.close()
if hasattr(canvas.native, 'show'): # Qt
canvas.native.show()
canvas.native.close()
3 changes: 2 additions & 1 deletion glue_vispy_viewers/common/tests/test_vispy_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def test_vispy_widget():

w = VispyWidgetHelper(viewer_state=d)

w.canvas.native.show()
if hasattr(w.canvas.native, 'show'): # Qt
w.canvas.native.show()

# Try adding marker visuals to the scene
positions = np.random.random((1000, 3))
Expand Down
Loading

0 comments on commit 7a66783

Please sign in to comment.