Skip to content

Commit f2e2f83

Browse files
committed
New widgets.selectdialog.SelectDialog
1 parent 477b453 commit f2e2f83

File tree

7 files changed

+247
-47
lines changed

7 files changed

+247
-47
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ New major release:
1616
* Reorganized modules: see documentation for details (section "Development")
1717
* Removed "Sift" demo as there is now a far better real-world example with the
1818
[DataLab](https://codra-ingenierie-informatique.github.io/DataLab/) project
19-
* Added dozen of new features and more than 30 bug fixes thanks to the merge with the [guiqwt](https://github.com/PlotPyStack/guiqwt) project
19+
* Integrated more than 30 bug fixes thanks to the merge with the [guiqwt](https://github.com/PlotPyStack/guiqwt) project
20+
* Added dozen of new features thanks to the merge with the [guiqwt]()
21+
* Added other new features:
22+
* ``widgets.selectdialog.SelectDialog``: a dialog box to select items using a shape tool (segment, rectangle or custom)
2023

2124
## Version 1.2.1 ##
2225

doc/features/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Plot widgets with integrated plot manager:
4949
panels/index
5050
fit
5151
pyplot
52+
selectdialog
5253
io
5354
signals
5455
mathutils/index

doc/features/selectdialog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.. automodule:: plotpy.widgets.selectdialog

plotpy/plot/plotwidget.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,7 @@ class PlotDialogMeta(type(QW.QDialog), abc.ABCMeta):
483483

484484

485485
class PlotDialog(QW.QDialog, AbstractPlotDialogWindow, metaclass=PlotDialogMeta):
486-
"""
487-
Construct a PlotDialog object: plotting dialog box with integrated
488-
plot manager
486+
"""Plotting dialog box with integrated plot manager
489487
490488
Args:
491489
parent: parent widget
@@ -623,9 +621,7 @@ class PlotWindowMeta(type(QW.QMainWindow), abc.ABCMeta):
623621

624622

625623
class PlotWindow(QW.QMainWindow, AbstractPlotDialogWindow, metaclass=PlotWindowMeta):
626-
"""
627-
Construct a PlotWindow object: plotting window with integrated plot
628-
manager
624+
"""Plotting window with integrated plot manager
629625
630626
Args:
631627
parent: parent widget
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# For licensing and distribution details, please read carefully xgrid/__init__.py
4+
5+
"""
6+
Get rectangular selection from image
7+
"""
8+
9+
# guitest: show
10+
11+
import numpy as np
12+
from guidata.env import execenv
13+
from guidata.qthelpers import qt_app_context
14+
15+
from plotpy.builder import make
16+
from plotpy.tests.gui.test_get_segment import SEG_AXES_COORDS, PatchedSelectDialog
17+
from plotpy.tools import RectangleTool
18+
from plotpy.widgets.selectdialog import select_with_shape_tool
19+
20+
21+
def test_get_rectangle():
22+
"""Test get_rectangle"""
23+
with qt_app_context():
24+
image = make.image(data=np.random.rand(200, 200), colormap="gray")
25+
shape = select_with_shape_tool(
26+
None, RectangleTool, image, "Test", tooldialogclass=PatchedSelectDialog
27+
)
28+
rect = shape.get_rect()
29+
if execenv.unattended:
30+
assert [round(i) for i in list(rect)] == SEG_AXES_COORDS
31+
elif rect is not None:
32+
print("Area:", rect)
33+
34+
35+
if __name__ == "__main__":
36+
test_get_rectangle()

plotpy/tests/gui/test_get_segment.py

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
# (see plotpy/LICENSE for details)
55

66
"""
7-
AnnotatedSegmentTool test
7+
Test ``get_segment`` feature: select a segment on an image.
88
99
This plotpy tool provide a MATLAB-like "ginput" feature.
1010
"""
1111

1212
# guitest: show
1313

14-
import os
15-
1614
import numpy as np
1715
import qtpy.QtCore as QC
1816
from guidata.env import execenv
@@ -21,52 +19,43 @@
2119
from plotpy.builder import make
2220
from plotpy.config import _
2321
from plotpy.coords import axes_to_canvas
24-
from plotpy.tools import AnnotatedSegmentTool, SelectTool
22+
from plotpy.tools import AnnotatedSegmentTool
23+
from plotpy.widgets.selectdialog import SelectDialog, select_with_shape_tool
2524

2625
SEG_AXES_COORDS = [20, 20, 70, 70]
2726

2827

29-
def get_segment(*items):
30-
"""Show image and return selected segment coordinates"""
31-
win = make.dialog(
32-
wintitle=_("Select a segment then press OK to accept"), edit=True, toolbar=True
33-
)
34-
default = win.manager.add_tool(SelectTool)
35-
win.manager.set_default_tool(default)
36-
segtool: AnnotatedSegmentTool = win.manager.add_tool(
37-
AnnotatedSegmentTool, title="Test", switch_to_default_tool=True
38-
)
39-
segtool.activate()
40-
plot = win.manager.get_plot()
41-
for item in items:
42-
plot.add_item(item)
43-
plot.set_active_item(item)
44-
if execenv.unattended:
45-
segtool.add_shape_to_plot(
46-
win.manager.get_plot(),
47-
QC.QPointF(*axes_to_canvas(item, *SEG_AXES_COORDS[:2])),
48-
QC.QPointF(*axes_to_canvas(item, *SEG_AXES_COORDS[2:])),
49-
)
50-
win.show()
51-
return win, segtool
28+
class PatchedSelectDialog(SelectDialog):
29+
"""Patched SelectDialog"""
30+
31+
def set_image_and_tool(self, item, toolclass, **kwargs):
32+
"""Reimplement SelectDialog method"""
33+
super().set_image_and_tool(item, toolclass, **kwargs)
34+
if execenv.unattended:
35+
self.sel_tool.add_shape_to_plot(
36+
self.manager.get_plot(),
37+
QC.QPointF(*axes_to_canvas(item, *SEG_AXES_COORDS[:2])),
38+
QC.QPointF(*axes_to_canvas(item, *SEG_AXES_COORDS[2:])),
39+
)
5240

5341

5442
def test_get_segment():
5543
"""Test get_segment"""
56-
filename = os.path.join(os.path.dirname(__file__), "brain.png")
57-
with qt_app_context(exec_loop=True):
58-
image = make.image(filename=filename, colormap="bone")
59-
_win_persist, segtool = get_segment(image)
60-
61-
shape = segtool.get_last_final_shape()
62-
rect = shape.get_rect()
63-
if execenv.unattended:
64-
assert [round(i) for i in list(rect)] == SEG_AXES_COORDS
65-
elif rect is not None:
66-
print(
67-
"Distance:",
68-
np.sqrt((rect[2] - rect[0]) ** 2 + (rect[3] - rect[1]) ** 2),
44+
with qt_app_context():
45+
image = make.image(data=np.random.rand(200, 200), colormap="gray")
46+
shape = select_with_shape_tool(
47+
None,
48+
AnnotatedSegmentTool,
49+
image,
50+
"Test",
51+
tooldialogclass=PatchedSelectDialog,
6952
)
53+
rect = shape.get_rect()
54+
if execenv.unattended:
55+
assert [round(i) for i in list(rect)] == SEG_AXES_COORDS
56+
elif rect is not None:
57+
distance = np.sqrt((rect[2] - rect[0]) ** 2 + (rect[3] - rect[1]) ** 2)
58+
print("Distance:", distance)
7059

7160

7261
if __name__ == "__main__":

plotpy/widgets/selectdialog.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# -*- coding: utf-8
2+
3+
"""
4+
selectdialog
5+
------------
6+
7+
The `selectdialog` module provides a dialog box to select an area of the plot
8+
using a tool:
9+
10+
* :py:class:`.widgets.selectdialog.SelectDialog`: dialog box
11+
* :py:func:`.widgets.selectdialog.select_with_shape_tool`: function to
12+
select an area with a shape tool and return the rectangle
13+
* :py:func:`.widgets.selectdialog.set_items_unselectable`: function to set
14+
items unselectable except for the given item
15+
16+
Example: get segment
17+
~~~~~~~~~~~~~~~~~~~~
18+
19+
.. literalinclude:: ../../plotpy/tests/gui/test_get_segment.py
20+
21+
Example: get rectangle
22+
~~~~~~~~~~~~~~~~~~~~~~
23+
24+
.. literalinclude:: ../../plotpy/tests/gui/test_get_rectangle.py
25+
26+
Reference
27+
~~~~~~~~~
28+
29+
.. autoclass:: SelectDialog
30+
:members:
31+
.. autofunction:: select_with_shape_tool
32+
.. autofunction:: set_items_unselectable
33+
"""
34+
35+
from __future__ import annotations
36+
37+
from typing import TYPE_CHECKING
38+
39+
from guidata.configtools import get_icon
40+
from guidata.qthelpers import exec_dialog
41+
from qtpy import QtWidgets as QW
42+
from qtpy.QtWidgets import QWidget # only to help intersphinx find QWidget
43+
from qwt.plot import QwtPlotItem
44+
45+
from plotpy.config import _
46+
from plotpy.items import AbstractShape, ImageItem
47+
from plotpy.panels.base import PanelWidget
48+
from plotpy.plot import BasePlot, PlotDialog, PlotOptions
49+
from plotpy.tools import RectangularShapeTool, SelectTool
50+
51+
if TYPE_CHECKING: # pragma: no cover
52+
from plotpy.panels.base import PanelWidget
53+
54+
55+
class SelectDialog(PlotDialog):
56+
"""Plot dialog box to select an area of the plot using a tool
57+
58+
Args:
59+
parent: parent widget
60+
toolbar: show/hide toolbar
61+
options: plot options
62+
panels: additionnal panels
63+
auto_tools: If True, the plot tools are automatically registered.
64+
If False, the user must register the tools manually.
65+
title: The window title
66+
icon: The window icon
67+
edit: If True, the plot is editable
68+
"""
69+
70+
def __init__(
71+
self,
72+
parent: QWidget | None = None,
73+
toolbar: bool = False,
74+
options: PlotOptions | None = None,
75+
panels: list[PanelWidget] | None = None,
76+
auto_tools: bool = True,
77+
title: str = "PlotPy",
78+
icon: str = "plotpy.svg",
79+
edit: bool = False,
80+
) -> None:
81+
super().__init__(
82+
parent, toolbar, options, panels, auto_tools, title, icon, edit
83+
)
84+
self.sel_tool: RectangularShapeTool | None = None
85+
86+
def set_image_and_tool(
87+
self, item: ImageItem, toolclass: RectangularShapeTool, **kwargs
88+
) -> None:
89+
"""Set the image item to be displayed and the tool to be used
90+
91+
Args:
92+
item: Image item
93+
toolclass: Tool class
94+
kwargs: Keyword arguments for the tool class
95+
"""
96+
default = self.manager.add_tool(SelectTool)
97+
self.manager.set_default_tool(default)
98+
self.sel_tool: RectangularShapeTool = self.manager.add_tool(
99+
toolclass,
100+
switch_to_default_tool=True,
101+
**kwargs,
102+
) # pylint: disable=attribute-defined-outside-init
103+
self.sel_tool.activate()
104+
set_ok_btn_enabled = self.button_box.button(QW.QDialogButtonBox.Ok).setEnabled
105+
set_ok_btn_enabled(False)
106+
self.sel_tool.SIG_TOOL_JOB_FINISHED.connect(lambda: set_ok_btn_enabled(True))
107+
plot = self.get_plot()
108+
plot.add_item(item)
109+
plot.set_active_item(item)
110+
item.set_selectable(False)
111+
item.set_readonly(True)
112+
plot.unselect_item(item)
113+
114+
def get_new_shape(self) -> AbstractShape:
115+
"""Get newly created shape
116+
117+
Returns:
118+
Newly created shape
119+
"""
120+
return self.sel_tool.get_last_final_shape()
121+
122+
123+
def set_items_unselectable(plot: BasePlot, except_item: QwtPlotItem = None) -> None:
124+
"""Set items unselectable except for the given item"""
125+
for item_i in plot.get_items():
126+
if except_item is None:
127+
item_i.set_selectable(False)
128+
else:
129+
item_i.set_selectable(item_i is except_item)
130+
131+
132+
def select_with_shape_tool(
133+
parent: QW.QWidget,
134+
toolclass: RectangularShapeTool,
135+
item: ImageItem,
136+
title: str = None,
137+
size: tuple[int, int] = None,
138+
other_items: list[QwtPlotItem] = [],
139+
tooldialogclass: SelectDialog = SelectDialog,
140+
icon=None,
141+
**kwargs,
142+
) -> AbstractShape:
143+
"""Select an area with a shape tool and return the rectangle
144+
145+
Args:
146+
parent: Parent widget
147+
toolclass: Tool class
148+
item: Image item
149+
title: Dialog title
150+
size: Dialog size
151+
other_items: Other items to be displayed
152+
tooldialogclass: Tool dialog class
153+
icon: Icon
154+
kwargs: Keyword arguments for the tool class
155+
156+
Returns:
157+
Selected shape
158+
"""
159+
if title is None:
160+
title = "Select an area then press OK to accept"
161+
if icon is not None:
162+
icon = get_icon(icon) if isinstance(icon, str) else icon
163+
win: SelectDialog = tooldialogclass(parent, title=title, edit=True, icon=icon)
164+
win.set_image_and_tool(item, toolclass, **kwargs)
165+
plot = win.get_plot()
166+
for other_item in other_items:
167+
plot.add_item(other_item)
168+
set_items_unselectable(plot)
169+
if size is not None:
170+
win.resize(*size)
171+
win.show()
172+
if exec_dialog(win):
173+
return win.get_new_shape()
174+
return None

0 commit comments

Comments
 (0)