Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to GUI selection and Qt support #431

Merged
merged 9 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fastplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from .legends import *
from .widgets import ImageWidget
from .utils import _notebook_print_banner, config
from .utils.gui import run

from wgpu.gui.auto import run
from wgpu.backends.wgpu_native import enumerate_adapters


Expand Down
16 changes: 7 additions & 9 deletions fastplotlib/graphics/selectors/_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
from numbers import Real

import numpy as np

import pygfx

try:
import ipywidgets

HAS_IPYWIDGETS = True
except (ImportError, ModuleNotFoundError):
HAS_IPYWIDGETS = False

from ...utils.gui import IS_JUPYTER
from .._base import Graphic, GraphicCollection
from .._features._selection_features import LinearSelectionFeature
from ._base_selector import BaseSelector


if IS_JUPYTER:
# If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets
import ipywidgets


class LinearSelector(BaseSelector):
@property
def limits(self) -> Tuple[float, float]:
Expand Down Expand Up @@ -240,7 +238,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):

"""

if not HAS_IPYWIDGETS:
if not IS_JUPYTER:
raise ImportError(
"Must installed `ipywidgets` to use `make_ipywidget_slider()`"
)
Expand Down
18 changes: 8 additions & 10 deletions fastplotlib/graphics/selectors/_linear_region.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
from typing import *
from numbers import Real

try:
import ipywidgets

HAS_IPYWIDGETS = True
except (ImportError, ModuleNotFoundError):
HAS_IPYWIDGETS = False

import numpy as np

import pygfx

from ...utils.gui import IS_JUPYTER
from .._base import Graphic, GraphicCollection
from ._base_selector import BaseSelector
from .._features._selection_features import LinearRegionSelectionFeature
from ._base_selector import BaseSelector


if IS_JUPYTER:
# If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets
import ipywidgets


class LinearRegionSelector(BaseSelector):
Expand Down Expand Up @@ -390,7 +388,7 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs):

"""

if not HAS_IPYWIDGETS:
if not IS_JUPYTER:
raise ImportError(
"Must installed `ipywidgets` to use `make_ipywidget_slider()`"
)
Expand Down
40 changes: 5 additions & 35 deletions fastplotlib/layouts/_frame/_frame.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,7 @@
import os

from ._toolbar import ToolBar

from ...graphics import ImageGraphic

from .._utils import CANVAS_OPTIONS_AVAILABLE


class UnavailableOutputContext:
# called when a requested output context is not available
# ex: if trying to force jupyter_rfb canvas but jupyter_rfb is not installed
def __init__(self, context_name, msg):
self.context_name = context_name
self.msg = msg

def __call__(self, *args, **kwargs):
raise ModuleNotFoundError(
f"The following output context is not available: {self.context_name}\n{self.msg}"
)


# TODO: potentially put all output context and toolbars in their own module and have this determination done at import
if CANVAS_OPTIONS_AVAILABLE["jupyter"]:
from ._jupyter_output import JupyterOutputContext
else:
JupyterOutputContext = UnavailableOutputContext(
"Jupyter",
"You must install fastplotlib using the `'notebook'` option to use this context:\n"
'pip install "fastplotlib[notebook]"',
)

if CANVAS_OPTIONS_AVAILABLE["qt"]:
from ._qt_output import QOutputContext
else:
QtOutput = UnavailableOutputContext(
"Qt", "You must install `PyQt6` to use this output context"
)
from ._toolbar import ToolBar


class Frame:
Expand Down Expand Up @@ -158,6 +124,8 @@ def show(

# return the appropriate OutputContext based on the current canvas
if self.canvas.__class__.__name__ == "JupyterWgpuCanvas":
from ._jupyter_output import JupyterOutputContext # noqa - inline import
kushalkolar marked this conversation as resolved.
Show resolved Hide resolved

self._output = JupyterOutputContext(
frame=self,
make_toolbar=toolbar,
Expand All @@ -167,6 +135,8 @@ def show(
)

elif self.canvas.__class__.__name__ == "QWgpuCanvas":
from ._qt_output import QOutputContext # noqa - inline import

self._output = QOutputContext(
frame=self, make_toolbar=toolbar, add_widgets=add_widgets
)
Expand Down
3 changes: 1 addition & 2 deletions fastplotlib/layouts/_frame/_qt_output.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from PyQt6 import QtWidgets

from ...utils.gui import QtWidgets
from ._qt_toolbar import QToolbar


Expand Down
3 changes: 1 addition & 2 deletions fastplotlib/layouts/_frame/_qt_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import traceback
from typing import *

from PyQt6 import QtWidgets, QtCore

from ...utils.gui import QtCore, QtWidgets
from ...graphics.selectors import PolygonSelector
from ._toolbar import ToolBar
from ._qtoolbar_template import Ui_QToolbar
Expand Down
5 changes: 2 additions & 3 deletions fastplotlib/layouts/_frame/_qtoolbar_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets
from ...utils.gui import QtGui, QtCore, QtWidgets


class Ui_QToolbar(object):
Expand All @@ -30,7 +29,7 @@ def setupUi(self, QToolbar):
self.maintain_aspect_button = QtWidgets.QPushButton(parent=QToolbar)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
font.setWeight(QtGui.QFont.Weight.Bold)
self.maintain_aspect_button.setFont(font)
self.maintain_aspect_button.setCheckable(True)
self.maintain_aspect_button.setObjectName("maintain_aspect_button")
Expand Down
12 changes: 3 additions & 9 deletions fastplotlib/layouts/_gridplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pygfx

from wgpu.gui.auto import WgpuCanvas
from wgpu.gui import WgpuCanvasBase

from ._frame import Frame
from ._utils import make_canvas_and_renderer, create_controller, create_camera
Expand All @@ -22,7 +22,7 @@ def __init__(
cameras: Union[str, list, np.ndarray] = "2d",
controller_types: Union[str, list, np.ndarray] = None,
controller_ids: Union[str, list, np.ndarray] = None,
canvas: Union[str, WgpuCanvas, pygfx.Texture] = None,
canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None,
renderer: pygfx.WgpuRenderer = None,
size: Tuple[int, int] = (500, 300),
names: Union[list, np.ndarray] = None,
Expand Down Expand Up @@ -219,12 +219,6 @@ def __init__(
for cam in cams[1:]:
_controller.add_camera(cam)

if canvas is None:
canvas = WgpuCanvas()

if renderer is None:
renderer = pygfx.renderers.WgpuRenderer(canvas)

self._canvas = canvas
self._renderer = renderer

Expand Down Expand Up @@ -266,7 +260,7 @@ def __init__(
Frame.__init__(self)

@property
def canvas(self) -> WgpuCanvas:
def canvas(self) -> WgpuCanvasBase:
"""The canvas associated to this GridPlot"""
return self._canvas

Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/layouts/_plot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import *

import pygfx
from wgpu.gui.auto import WgpuCanvas
from wgpu.gui import WgpuCanvasBase

from ._subplot import Subplot
from ._frame import Frame
Expand All @@ -11,7 +11,7 @@
class Plot(Subplot, Frame, RecordMixin):
def __init__(
self,
canvas: Union[str, WgpuCanvas] = None,
canvas: Union[str, WgpuCanvasBase] = None,
renderer: pygfx.WgpuRenderer = None,
camera: Union[str, pygfx.PerspectiveCamera] = "2d",
controller: Union[str, pygfx.Controller] = None,
Expand Down
6 changes: 3 additions & 3 deletions fastplotlib/layouts/_plot_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pygfx
from pylinalg import vec_transform, vec_unproject
from wgpu.gui.auto import WgpuCanvas
from wgpu.gui import WgpuCanvasBase

from ._utils import create_camera, create_controller
from ..graphics._base import Graphic
Expand All @@ -29,7 +29,7 @@ def __init__(
camera: Union[pygfx.PerspectiveCamera],
controller: Union[pygfx.Controller],
scene: pygfx.Scene,
canvas: WgpuCanvas,
canvas: WgpuCanvasBase,
renderer: pygfx.WgpuRenderer,
name: str = None,
):
Expand Down Expand Up @@ -122,7 +122,7 @@ def scene(self) -> pygfx.Scene:
return self._scene

@property
def canvas(self) -> WgpuCanvas:
def canvas(self) -> WgpuCanvasBase:
"""Canvas associated to the plot area"""
return self._canvas

Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/layouts/_subplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pygfx

from wgpu.gui.auto import WgpuCanvas
from wgpu.gui import WgpuCanvasBase

from ..graphics import TextGraphic
from ._utils import make_canvas_and_renderer, create_camera, create_controller
Expand All @@ -20,7 +20,7 @@ def __init__(
parent_dims: Tuple[int, int] = None,
camera: Union[str, pygfx.PerspectiveCamera] = "2d",
controller: Union[str, pygfx.Controller] = None,
canvas: Union[str, WgpuCanvas, pygfx.Texture] = None,
canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None,
renderer: pygfx.WgpuRenderer = None,
name: str = None,
):
Expand Down
74 changes: 8 additions & 66 deletions fastplotlib/layouts/_utils.py
kushalkolar marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,88 +1,30 @@
from typing import *
import importlib

import pygfx
from pygfx import WgpuRenderer, Texture
from wgpu.gui import WgpuCanvasBase

# default auto-determined canvas
from wgpu.gui.auto import WgpuCanvas
from wgpu.gui.base import WgpuCanvasBase


# TODO: this determination can be better
try:
from wgpu.gui.jupyter import JupyterWgpuCanvas
except ImportError:
JupyterWgpuCanvas = False

try:
import PyQt6
from wgpu.gui.qt import QWgpuCanvas
except ImportError:
QWgpuCanvas = False

try:
from wgpu.gui.glfw import GlfwWgpuCanvas
except ImportError:
GlfwWgpuCanvas = False


CANVAS_OPTIONS = ["jupyter", "glfw", "qt"]
CANVAS_OPTIONS_AVAILABLE = {
"jupyter": JupyterWgpuCanvas,
"glfw": GlfwWgpuCanvas,
"qt": QWgpuCanvas,
}


def auto_determine_canvas():
try:
ip = get_ipython()
if ip.has_trait("kernel"):
if hasattr(ip.kernel, "app"):
if ip.kernel.app.__class__.__name__ == "QApplication":
return QWgpuCanvas
else:
return JupyterWgpuCanvas
except NameError:
pass

else:
if CANVAS_OPTIONS_AVAILABLE["qt"]:
return QWgpuCanvas
elif CANVAS_OPTIONS_AVAILABLE["glfw"]:
return GlfwWgpuCanvas

# We go with the wgpu auto guess
# for example, offscreen canvas etc.
return WgpuCanvas
from ..utils import gui


def make_canvas_and_renderer(
canvas: Union[str, WgpuCanvas, Texture, None], renderer: [WgpuRenderer, None]
canvas: Union[str, WgpuCanvasBase, Texture, None], renderer: [WgpuRenderer, None]
):
"""
Parses arguments and returns the appropriate canvas and renderer instances
as a tuple (canvas, renderer)
"""

if canvas is None:
Canvas = auto_determine_canvas()
canvas = Canvas(max_fps=60)

canvas = gui.WgpuCanvas(max_fps=60)
elif isinstance(canvas, str):
if canvas not in CANVAS_OPTIONS:
raise ValueError(f"str canvas argument must be one of: {CANVAS_OPTIONS}")
elif not CANVAS_OPTIONS_AVAILABLE[canvas]:
raise ImportError(
f"The {canvas} framework is not installed for using this canvas"
)
else:
canvas = CANVAS_OPTIONS_AVAILABLE[canvas](max_fps=60)

m = importlib.import_module("wgpu.gui." + canvas)
canvas = m.WgpuCanvas(max_fps=60)
elif not isinstance(canvas, (WgpuCanvasBase, Texture)):
raise ValueError(
f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture"
f" or a str from the following options: {CANVAS_OPTIONS}"
f" or a str with the wgpu gui backend name."
)

if renderer is None:
Expand Down
Loading
Loading