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

More docs for main_window flag #118

Merged
merged 19 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from 15 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
41 changes: 41 additions & 0 deletions docs/usage/main_window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Main Window Option

By default, using the Qt backend, magicgui's window is not considered a [QMainWindow](https://doc.qt.io/qt-5/qmainwindow.html) subclass. This allows the GUI to be easily integrated into other Qt applications like [napari](https://napari.org/), but it also means that the shown window lacks a few features such as the top app menu.
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved

To enable that top app menu you should allow the `main_window` flag when decorating your main function:
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved

```python
@magicgui(main_window=True)
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved
def add(num1: int, num2: int) -> int:
"""
Adds the given two numbers, returning the result.

The function assumes that the two numbers can be added and does
not perform any prior checks.

Parameters
----------
num1 , num2 : int
Numbers to be added

Returns
-------
int
Resulting integer

Examples
--------
```
add(2, 3) # returns 5
```
"""
return num1 + num2
```

Running this function will show a GUI with a top app menu bar containing a single entry - "Help", with a "Documentation" option. Clicking on it will pop app an HTML-enabled text box that shows the function's complete documentation:
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved

![Menu example](resources/main_window.png)
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved

This can be very helpful when your tool's functionality is not completely obvious at first glance, or when a few different user-enabled flags may interact in non-trivial ways. Alongside the tooltips for each parameter (which magicgui automatically generates) the GUI will be as well-documented as your code is.

A runnable example which uses the HTML capabilties of this feature can be found in [examples/main_window.py](examples/main_window.py).
HagaiHargil marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 39 additions & 0 deletions examples/main_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pathlib
from enum import Enum

from magicgui import magicgui


class HotdogOptions(Enum):
"""All hotdog possibilities"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂


Hotdog = 1
NotHotdog = 0


@magicgui(main_window=True, layout="form", call_button="Classify", result_widget=True)
def is_hotdog(img: pathlib.Path) -> HotdogOptions:
"""Classify possible hotdog images.

Upload an image and check whether it's an hotdog. For example, this image
will be classified as one: <br><br>

<img src="resources/hotdog.jpg">

Parameters
----------
img : pathlib.Path
Path to a possible hotdog image

Returns
-------
HotdogOptions
True if image contains an hotdog in it
"""
if "hotdog" in img.stem:
return HotdogOptions.Hotdog
return HotdogOptions.NotHotdog


if __name__ == "__main__":
is_hotdog.show(run=True)
10 changes: 3 additions & 7 deletions examples/widget_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Medium(Enum):


@magicgui(
main_window=True,
call_button="Calculate",
layout="vertical",
result_widget=True,
Expand All @@ -35,7 +36,7 @@ def widget_demo(
datetime=datetime.datetime.now(),
filename=Path.home(),
):
"""We can use numpy docstrings to provide tooltips
"""We can use numpy docstrings to provide tooltips.

Parameters
----------
Expand All @@ -56,14 +57,9 @@ def widget_demo(
time : datetime.time, optional
Some time, by default datetime.time(1, 30, 20)
datetime : datetime.datetime, optional
A very specific time and date, by default datetime.datetime.now()
A very specific time and date, by default ``datetime.datetime.now()``
filename : str, optional
Pick a path, by default Path.home()

Returns
-------
[type]
[description]
"""
return locals().values()

Expand Down
49 changes: 46 additions & 3 deletions magicgui/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from typing import TYPE_CHECKING, Callable, Optional, Union, overload
from warnings import warn

from typing_extensions import Literal

if TYPE_CHECKING:
from magicgui.application import AppRef
from magicgui.widgets import FunctionGui
from magicgui.widgets import FunctionGui, MainFunctionGui


@overload
Expand All @@ -18,6 +20,7 @@ def magicgui( # noqa
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
main_window: Literal[False] = False,
app: AppRef = None,
**param_options: dict,
) -> FunctionGui:
Expand All @@ -34,12 +37,47 @@ def magicgui( # noqa
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
main_window: Literal[False] = False,
app: AppRef = None,
**param_options: dict,
) -> Callable[[Callable], FunctionGui]:
...


@overload
def magicgui( # noqa
function: Callable,
*,
layout: str = "horizontal",
labels: bool = True,
tooltips: bool = True,
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
main_window: Literal[True],
app: AppRef = None,
**param_options: dict,
) -> MainFunctionGui:
...


@overload
def magicgui( # noqa
function=None,
*,
layout: str = "horizontal",
labels: bool = True,
tooltips: bool = True,
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
main_window: Literal[True],
app: AppRef = None,
**param_options: dict,
) -> Callable[[Callable], MainFunctionGui]:
...


def magicgui(
function: Optional[Callable] = None,
*,
Expand All @@ -49,6 +87,7 @@ def magicgui(
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
main_window: bool = False,
app: AppRef = None,
**param_options: dict,
):
Expand All @@ -75,6 +114,8 @@ def magicgui(
result_widget : bool, optional
Whether to display a LineEdit widget the output of the function when called,
by default False
main_window : bool
Whether this widget should be treated as the main app window, with menu bar.
app : magicgui.Application or str, optional
A backend to use, by default ``None`` (use the default backend.)

Expand Down Expand Up @@ -114,9 +155,11 @@ def magicgui(
result_widget = True

def inner_func(func: Callable) -> FunctionGui:
from magicgui.widgets import FunctionGui
from magicgui.widgets import FunctionGui, MainFunctionGui

cls = MainFunctionGui if main_window else FunctionGui

func_gui = FunctionGui(
func_gui = cls(
function=func,
call_button=call_button,
layout=layout,
Expand Down
2 changes: 2 additions & 0 deletions magicgui/backends/_qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
FloatSpinBox,
Label,
LineEdit,
MainWindow,
ProgressBar,
PushButton,
RadioButton,
Expand All @@ -32,6 +33,7 @@
"FloatSpinBox",
"Label",
"LineEdit",
"MainWindow",
"ProgressBar",
"PushButton",
"RadioButton",
Expand Down
41 changes: 39 additions & 2 deletions magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Widget implementations (adaptors) for the Qt backend."""
from typing import Any, Iterable, Optional, Sequence, Tuple, Union
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union

import qtpy
from qtpy import QtWidgets as QtW
Expand Down Expand Up @@ -203,10 +203,16 @@ def __init__(self):
super().__init__(QtW.QLineEdit, "text", "setText", "textChanged")


class TextEdit(QBaseStringWidget):
class TextEdit(QBaseStringWidget, _protocols.SupportsReadOnly):
def __init__(self):
super().__init__(QtW.QTextEdit, "toPlainText", "setText", "textChanged")

def _mgui_set_read_only(self, value: bool) -> None:
self._qwidget.setReadOnly(value)

def _mgui_get_read_only(self) -> bool:
return self._qwidget.isReadOnly()


# NUMBERS

Expand Down Expand Up @@ -324,6 +330,37 @@ def _mgui_get_orientation(self) -> str:
return "vertical"


class MainWindow(Container):
def __init__(self, layout="vertical"):
super().__init__(layout=layout)
self._main_window = QtW.QMainWindow()
self._main_window.setCentralWidget(self._qwidget)
self._main_menu = self._main_window.menuBar()
self._menus: Dict[str, QtW.QMenu] = {}

def _mgui_show_widget(self):
self._main_window.show()

def _mgui_hide_widget(self):
self._main_window.hide()

def _mgui_get_native_widget(self) -> QtW.QMainWindow:
return self._main_window

def _mgui_create_menu_item(
self, menu_name: str, action_name: str, callback=None, shortcut=None
):
menu = self._menus.setdefault(
menu_name, self._main_menu.addMenu(f"&{menu_name}")
)
action = QtW.QAction(action_name, self._main_window)
if shortcut is not None:
action.setShortcut(shortcut)
if callback is not None:
action.triggered.connect(callback)
menu.addAction(action)


class SpinBox(QBaseRangedWidget):
def __init__(self):
super().__init__(QtW.QSpinBox)
Expand Down
5 changes: 4 additions & 1 deletion magicgui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
LineEdit,
LiteralEvalLineEdit,
LogSlider,
MainWindow,
ProgressBar,
PushButton,
RadioButton,
Expand All @@ -36,7 +37,7 @@
TextEdit,
TimeEdit,
)
from ._function_gui import FunctionGui
from ._function_gui import FunctionGui, MainFunctionGui
from ._table import Table

#: Aliases for compatibility with ipywidgets. (WIP)
Expand Down Expand Up @@ -73,6 +74,8 @@
"LineEdit",
"LiteralEvalLineEdit",
"LogSlider",
"MainFunctionGui",
"MainWindow",
"PushButton",
"ProgressBar",
"RadioButton",
Expand Down
3 changes: 2 additions & 1 deletion magicgui/widgets/_bases/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(
"""
from .button_widget import ButtonWidget
from .categorical_widget import CategoricalWidget
from .container_widget import ContainerWidget
from .container_widget import ContainerWidget, MainWindowWidget
from .create_widget import create_widget
from .ranged_widget import RangedWidget, TransformedRangedWidget
from .slider_widget import SliderWidget
Expand All @@ -55,6 +55,7 @@ def __init__(
"ButtonWidget",
"CategoricalWidget",
"ContainerWidget",
"MainWindowWidget",
"RangedWidget",
"SliderWidget",
"TransformedRangedWidget",
Expand Down
15 changes: 15 additions & 0 deletions magicgui/widgets/_bases/container_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,18 @@ def labels(self, value: bool):
for index, _ in enumerate(self):
widget = self.pop(index)
self.insert(index, widget)


class MainWindowWidget(ContainerWidget):
"""Top level Application widget that can contain other widgets."""

_widget: _protocols.MainWindowProtocol

def create_menu_item(
self, menu_name: str, item_name: str, callback=None, shortcut=None
):
"""Create a menu item ``item_name`` under menu ``menu_name``.

``menu_name`` will be created if it does not already exist.
"""
self._widget._mgui_create_menu_item(menu_name, item_name, callback, shortcut)
15 changes: 15 additions & 0 deletions magicgui/widgets/_bases/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ def orientation(self, value: str) -> None:
"Only horizontal and vertical orientation are currently supported"
)
self._widget._mgui_set_orientation(value)


class _ReadOnlyMixin:
"""Properties for classes wrapping widgets that support read-only."""

_widget: _protocols.SupportsReadOnly

@property
def read_only(self) -> bool:
"""read_only of the widget."""
return self._widget._mgui_get_read_only()

@read_only.setter
def read_only(self, value: bool) -> None:
self._widget._mgui_set_read_only(value)
Loading