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

Initial support for labels #14

Merged
merged 6 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all 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 .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: tests

on: push
on: [push, pull_request]

jobs:
test:
Expand Down
45 changes: 36 additions & 9 deletions magicgui/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
import sys
from contextlib import contextmanager
from enum import Enum, EnumMeta
from typing import Any, Callable, Dict, Iterable, NamedTuple, Optional, Type, Tuple

from qtpy.QtCore import Signal, Qt

try:
from qtpy.QtCore import SignalInstance as SignalInstanceType
except ImportError:
from qtpy.QtCore import pyqtBoundSignal as SignalInstanceType
from typing import Any, Callable, Dict, Iterable, NamedTuple, Optional, Tuple, Type

from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import (
QAbstractButton,
QAbstractSlider,
Expand All @@ -26,17 +20,24 @@
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLayout,
QLineEdit,
QPushButton,
QSlider,
QSpinBox,
QSplitter,
QStatusBar,
QTabWidget,
QVBoxLayout,
QWidget,
QSlider,
)

try:
from qtpy.QtCore import SignalInstance as SignalInstanceType
except ImportError:
from qtpy.QtCore import pyqtBoundSignal as SignalInstanceType


@contextmanager
def event_loop():
Expand Down Expand Up @@ -91,6 +92,32 @@ class Layout(Enum, metaclass=HelpfulEnum):
grid = QGridLayout
form = QFormLayout

@staticmethod
def addWidget(layout: QLayout, widget: QWidget, label: str = ""):
"""Add widget to arbitrary layout with optional label."""
if isinstance(layout, QFormLayout):
return layout.addRow(label, widget)
elif isinstance(layout, (QHBoxLayout, QVBoxLayout)):
if label:
label_widget = QLabel(label)
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
layout.addWidget(label_widget)
return layout.addWidget(widget)

@staticmethod
def insertWidget(layout: QLayout, position: int, widget: QWidget, label: str = ""):
"""Add widget to arbitrary layout at position, with optional label."""
if position < 0:
position = layout.count() + position + 1
if isinstance(layout, QFormLayout):
layout.insertRow(position, label, widget)
else:
layout.insertWidget(position, widget)
if label:
label_widget = QLabel(label)
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
layout.insertWidget(position, label_widget)


class QDataComboBox(QComboBox):
"""A CombBox subclass that emits data objects when the index changes."""
Expand Down
20 changes: 13 additions & 7 deletions magicgui/_tests/test_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,20 @@ def get_layout_items(layout):
assert get_layout_items(gui.layout()) == ["c", "b", "a"]


def test_add_at_position(qtbot):
@pytest.mark.parametrize("labels", [True, False], ids=["with-labels", "no-labels"])
def test_add_at_position(labels, qtbot):
"""Test that adding widghet with position option puts widget in the right place."""

def func(a=1, b=2, c=3):
pass

def get_layout_items(layout):
return [layout.itemAt(i).widget().objectName() for i in range(layout.count())]
items = [layout.itemAt(i).widget().objectName() for i in range(layout.count())]
if labels:
items = list(filter(None, items))
return items

gui = magicgui(func).Gui()
gui = magicgui(func, labels=labels).Gui()
assert get_layout_items(gui.layout()) == ["a", "b", "c"]
gui.set_widget("new", 1, position=1)
assert get_layout_items(gui.layout()) == ["a", "new", "b", "c"]
Expand Down Expand Up @@ -488,8 +492,10 @@ def test_parent_changed(qtbot, magic_widget):

def test_layout_raises(qtbot):
"""Test that unrecognized layouts raise an error."""
with pytest.raises(KeyError):

@magicgui(layout="df")
def test(a=1):
pass
@magicgui(layout="df")
def test(a=1):
pass

with pytest.raises(KeyError):
test.Gui()
39 changes: 26 additions & 13 deletions magicgui/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ def __init__(
self,
func: Callable,
*,
layout: api.Layout = api.Layout.horizontal,
layout: Union[api.Layout, str] = "horizontal",
labels: bool = True,
call_button: Union[bool, str] = False,
auto_call: bool = False,
parent: api.WidgetType = None,
Expand All @@ -139,8 +140,11 @@ def __init__(
----------
func : Callable
The function being decorated
layout : api.Layout, optional
The type of layout to use, by default api.Layout.horizontal
layout : api.Layout or str, optional
The type of layout to use. If string, must be one of {'horizontal',
'vertical', 'form', 'grid'}, by default "horizontal"
labels : bool
Whether labels are shown in the widget. by default True
call_button : bool or str, optional
If True, create an additional button that calls the original function when
clicked. If a ``str``, set the button text. by default False
Expand All @@ -160,6 +164,11 @@ def __init__(
signature, and the value MUST be a dict.
"""
super().__init__(parent=parent)
self._with_labels = labels
layout = api.Layout[layout] if isinstance(layout, str) else layout
if labels and layout == api.Layout.vertical:
layout = api.Layout.form
self.setLayout(layout.value(self))
# this is how the original function object knows that an object has been created
setattr(func, "_widget", self)
self.param_names = []
Expand All @@ -169,7 +178,6 @@ def __init__(
# mapping of param name, parameter type. Will be set in set_widget.
self._arg_types: Dict[str, Type] = dict()
self.func = func
self.setLayout(layout.value(self))
self.param_options = param_options

# TODO: should we let required positional args get skipped?
Expand All @@ -189,7 +197,7 @@ def __init__(
)
# using lambda because the clicked signal returns a value
self.call_button.clicked.connect(lambda checked: self.__call__())
self.layout().addWidget(self.call_button)
api.Layout.addWidget(self.layout(), self.call_button)

# a convenience, allows widgets to change their choices depending on context
# particularly useful if a downstream library has registered a type with a
Expand Down Expand Up @@ -363,17 +371,20 @@ class instance using setattr(self, self.WIDGET_ATTR.format(name), widget).
setattr(self, name, value)

# add the widget to the layout (appended, or at a specific position)
label = name if self._with_labels else ""

if position is not None:
if not isinstance(position, int):
raise TypeError(
f"`position` argument must be of type int. got: {type(position)}"
)
if position < 0:
position = self.layout().count() + position + 1
self.layout().insertWidget(position, widget)
if self._with_labels:
position *= 2
if position < 0:
position += 1
api.Layout.insertWidget(self.layout(), position, widget, label=label)
else:
self.layout().addWidget(widget)

api.Layout.addWidget(self.layout(), widget, label=label)
return widget

@classmethod
Expand Down Expand Up @@ -551,6 +562,7 @@ def __repr__(self):
def magicgui(
function: Optional[Callable] = None,
layout: Union[api.Layout, str] = "horizontal",
labels: bool = True,
call_button: Union[bool, str] = False,
auto_call: bool = False,
parent: api.WidgetType = None,
Expand All @@ -567,6 +579,8 @@ def magicgui(
layout : api.Layout or str, optional
The type of layout to use. If string, must be one of {'horizontal', 'vertical',
'form', 'grid'}, by default "horizontal"
labels : bool
Whether labels are shown in the widget. by default True
call_button : bool or str, optional
If True, create an additional button that calls the original function when
clicked. If a ``str``, set the button text. by default False
Expand Down Expand Up @@ -605,8 +619,6 @@ def magicgui(
if not isinstance(value, dict):
raise TypeError(f"value for keyword argument {key} should be a dict")

_layout = api.Layout[layout] if isinstance(layout, str) else layout

def inner_func(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
Expand All @@ -622,7 +634,8 @@ class MagicGui(MagicGuiBase):
def __init__(self, show=False) -> None:
super().__init__(
func,
layout=_layout,
layout=layout,
labels=labels,
call_button=call_button,
auto_call=auto_call,
parent=parent,
Expand Down