Skip to content

Commit

Permalink
Initial support for labels (#14)
Browse files Browse the repository at this point in the history
* starting work on labels

* working labels

* fix docstring

* add pull request to github action
  • Loading branch information
tlambert03 committed May 19, 2020
1 parent 54559c1 commit 0b0c930
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 30 deletions.
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

0 comments on commit 0b0c930

Please sign in to comment.