PythonQwt 0.16.2__py3-none-any.whl → 0.16.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/METADATA +1 -1
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/RECORD +18 -12
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/WHEEL +1 -1
- qwt/__init__.py +1 -1
- qwt/null_paintdevice.py +1 -1
- qwt/plot.py +21 -0
- qwt/plot_canvas.py +4 -1
- qwt/qtdesigner.py +211 -0
- qwt/scale_draw.py +1 -1
- qwt/tests/test_issue109_backingstore.py +68 -0
- qwt/tests/test_issue33_sidebyside.py +260 -0
- qwt/tests/test_issue88_mousetracking.py +70 -0
- qwt/tests/test_qtdesigner.py +55 -0
- qwt/tests/test_qtdesigner.ui +31 -0
- qwt/tests/utils.py +2 -0
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/entry_points.txt +0 -0
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/licenses/LICENSE +0 -0
- {pythonqwt-0.16.2.dist-info → pythonqwt-0.16.3.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
pythonqwt-0.16.
|
|
2
|
-
qwt/__init__.py,sha256=
|
|
1
|
+
pythonqwt-0.16.3.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
|
|
2
|
+
qwt/__init__.py,sha256=0eKAVij0c3t6sNd8fHi5OulCvZbC3PAT6hZrJcCSgJU,5990
|
|
3
3
|
qwt/_math.py,sha256=fNcPJcaK-ldCCFB20T2N58LEkQ1lwZY1-q5veXXhwl8,1501
|
|
4
4
|
qwt/color_map.py,sha256=fLRpymqOXtLrhuPrbbCxTvNINbr2GH7pYBG4fZidrcs,11812
|
|
5
5
|
qwt/column_symbol.py,sha256=0aFyQ05ryTKbaSTctkhtXa0pB_tWG444zlH3991_bt0,5760
|
|
@@ -7,11 +7,11 @@ qwt/dyngrid_layout.py,sha256=wgff6mZ1okNhQRRbwaw1I__vOXqGdUG0uafkShuns08,13477
|
|
|
7
7
|
qwt/graphic.py,sha256=rSxvgIAm9ctcyszvZrN_03KpBhKSHabbPYdcrztSta8,28962
|
|
8
8
|
qwt/interval.py,sha256=PMrCQxV9NXGk2JG0MNjJ3zGTNTSSRfa_noxbpm-Okns,12287
|
|
9
9
|
qwt/legend.py,sha256=GBUim67HWgwLat5tOWDpK98fWcFxV-RkxWjoyWd9Gao,31313
|
|
10
|
-
qwt/null_paintdevice.py,sha256=
|
|
10
|
+
qwt/null_paintdevice.py,sha256=MQ_H4yc9qMqjAoCziaP3Sa6Mnu9wMSpbxTDisI7j1Dw,9381
|
|
11
11
|
qwt/painter.py,sha256=k345puF4MAoTye0WBveiTNpCNWXx1K2o62_I0PZYlIA,16536
|
|
12
12
|
qwt/painter_command.py,sha256=7D3goNsX_2Ufr5kCpC3Cr1t8GoD5Yz0G1oIZJKvoGBY,8501
|
|
13
|
-
qwt/plot.py,sha256=
|
|
14
|
-
qwt/plot_canvas.py,sha256
|
|
13
|
+
qwt/plot.py,sha256=qw35nDxdDcrH-5MxijNo73r3xZ7EYb0S1UiHpavhEVE,77707
|
|
14
|
+
qwt/plot_canvas.py,sha256=KK4saSTuoz5rab2xaQCAimlQlNQIo6GE3TVmHhMdsIc,31038
|
|
15
15
|
qwt/plot_curve.py,sha256=ThlfxUjB4yiP_Dh96P7PXusyqUfDWcRcyt31OHvu2vk,37059
|
|
16
16
|
qwt/plot_directpainter.py,sha256=JCWNhCC21L1A1HZnxd-C95QNlrVJbLztmGab-X8gCGc,10165
|
|
17
17
|
qwt/plot_grid.py,sha256=h-cDTzibEzQyX2g25SBg5ysyXwmONmMFy9NcuzFySYc,16607
|
|
@@ -19,9 +19,10 @@ qwt/plot_layout.py,sha256=tD94nsFzWt1LQNFxgZbYsyM66ib6I6wBK3ObS7M1w_o,46183
|
|
|
19
19
|
qwt/plot_marker.py,sha256=6vDHdMsyGClMruWLA4lChUCpoLzmxGNkvmICm0KjP6I,21284
|
|
20
20
|
qwt/plot_renderer.py,sha256=v-fSwicMGQBa_kQx4hJJptlVNpNsKbgWk9jjqG-r_1g,27201
|
|
21
21
|
qwt/plot_series.py,sha256=Wa_Wunu-t_L8vl95RJq6__k-XA8UV2YNIsc8yNGpTT8,10654
|
|
22
|
+
qwt/qtdesigner.py,sha256=9oXSPD3m6vk1Tc8KLUM7daRuELXnOvNumbo1hlcrvJ4,6753
|
|
22
23
|
qwt/qthelpers.py,sha256=GpvLr96Ip8SIGwyFfs0f7ocHyKPB7PEjHBQyrpw84xw,1405
|
|
23
24
|
qwt/scale_div.py,sha256=_iI5cy4s8H6wigE8xsn7W44T2-DqVADqdjQ31uJPa_I,9266
|
|
24
|
-
qwt/scale_draw.py,sha256=
|
|
25
|
+
qwt/scale_draw.py,sha256=ZE8NSm3qSqwGtYo236mUZT5OC7weTwTWo_GZorhAIN4,40545
|
|
25
26
|
qwt/scale_engine.py,sha256=7KQB5k-JXuJMsEMQXBJBNppCN-rTN97UKQCYXdazvqQ,36674
|
|
26
27
|
qwt/scale_map.py,sha256=rHmPv7e9voEFuAIInQJq60mTtIGrLMViNlgJYu20znA,9605
|
|
27
28
|
qwt/scale_widget.py,sha256=p202DrRgMraGbVcubzkatvVXOLbO9CPho729hz6gqa0,27125
|
|
@@ -46,16 +47,21 @@ qwt/tests/test_eventfilter.py,sha256=31vCsCzRcoq3cyMlDAH5fthA45QqeFXKaWyhfZME6rE
|
|
|
46
47
|
qwt/tests/test_highdpi.py,sha256=Xafo016q-RX4918JddBGD60HZ4GpuYJBJokEO15md24,1053
|
|
47
48
|
qwt/tests/test_image.py,sha256=yY_AplKEdm-M7uB5G50p4E68ZzNamYvGSfN5rvlVPPM,6549
|
|
48
49
|
qwt/tests/test_issue107_gdi_leak.py,sha256=7IgZd9vJyD0_ea4CIDJ_SjlYDwYkA94aSRWEqt4y2ew,2203
|
|
50
|
+
qwt/tests/test_issue109_backingstore.py,sha256=2PbNmobG7QsdXhE-vgPiKI6TaXbAz9m6G_qIDIkvMa8,2461
|
|
51
|
+
qwt/tests/test_issue33_sidebyside.py,sha256=Tuu6XcRrV4ucPXmR8Y94uegRwCDWAN40EfuDQyVKHzE,10186
|
|
52
|
+
qwt/tests/test_issue88_mousetracking.py,sha256=jr6iSBb1LIa32Rqt8opBXElC_ar0ru1j8XG1RUM_y5c,2360
|
|
49
53
|
qwt/tests/test_loadtest.py,sha256=ZTHatcGAbLyvS0BpbOWbzPD2Bjz4rckF7oxZsUVQ5KM,1812
|
|
50
54
|
qwt/tests/test_logcurve.py,sha256=XOtR0asSqFfzc0VL0qfXN0fXNvxvLnz5fxmNZ2gzQWk,1129
|
|
51
55
|
qwt/tests/test_mapdemo.py,sha256=e7rvB8IPKYVVii8UgdHSbOHLc3i7eB21EQNKZ151Kc8,3423
|
|
52
56
|
qwt/tests/test_multidemo.py,sha256=YS-P08qcakD41OJy6ZI53ofKOnUvD-2_uum0yOLiIRc,2530
|
|
57
|
+
qwt/tests/test_qtdesigner.py,sha256=DOVapZrBvNAIA1GS0zFyLkA4Wxlyllh5lMhuGVfL2gc,1542
|
|
58
|
+
qwt/tests/test_qtdesigner.ui,sha256=iqxZYkst-ZhBe08YVeptVvbohathyEHFxsHtErpHMj0,685
|
|
53
59
|
qwt/tests/test_relativemargin.py,sha256=Shn0aE7UeETSSaQFpt4DrMOFeMN3p4rmEHKqPhXfQTI,2097
|
|
54
60
|
qwt/tests/test_simple.py,sha256=4opQp_-RkcVdpRyYSh89ddBkeXCPrppB-_2KTiu5vjI,2347
|
|
55
61
|
qwt/tests/test_stylesheet.py,sha256=cuL1iY4pp5ZoMX4OI86amJA6R5SzQsH1JUWLtEiitZ4,950
|
|
56
62
|
qwt/tests/test_symbols.py,sha256=YcGM0_Em8QtcbqpLSU98PxvGo7BIIYviP2o_4Fg2LbI,5607
|
|
57
63
|
qwt/tests/test_vertical.py,sha256=hWhPASIRb1nrd6d3m3dPoccwl5RoCVH_r_C8VK2znMI,3285
|
|
58
|
-
qwt/tests/utils.py,sha256=
|
|
64
|
+
qwt/tests/utils.py,sha256=5DdF53mL9VEn-f2WjXL4LMRO2MaUMFUvsKFprHnN3nw,10646
|
|
59
65
|
qwt/tests/data/PythonQwt.svg,sha256=t-NYdVPqz7BcqtXNWCJvo8udrRmtGh57ksSYWUl1h4o,15232
|
|
60
66
|
qwt/tests/data/bodedemo.png,sha256=8jP1YLE6HdwjEmrQyJclLxiYI4Zs4t0z0BoN1JT3GPQ,24414
|
|
61
67
|
qwt/tests/data/cartesian.png,sha256=Cwbn5Jy3VT-OisFHxzMCVbfdbJacUMZ9uGCD9sCxA-Q,51094
|
|
@@ -78,8 +84,8 @@ qwt/tests/data/symbol.svg,sha256=ONkkohVqpg3OO2-XEZge7UZLqqNyOQXbSpLgHXcTBFU,127
|
|
|
78
84
|
qwt/tests/data/symbols.png,sha256=wiqRBc2MJluJ-am9LtZ0K9DHV9BnreYMiEvfzJUFzCk,55586
|
|
79
85
|
qwt/tests/data/testlauncher.png,sha256=WmucObp30QYYKsVRBs5-6t1aEN0FbIWRmOVFN1gt-vQ,139676
|
|
80
86
|
qwt/tests/data/vertical.png,sha256=EvyG6q6rZm6pTT08Y_nzdBYCnqmoX1QDuEQtp2pyGlw,34076
|
|
81
|
-
pythonqwt-0.16.
|
|
82
|
-
pythonqwt-0.16.
|
|
83
|
-
pythonqwt-0.16.
|
|
84
|
-
pythonqwt-0.16.
|
|
85
|
-
pythonqwt-0.16.
|
|
87
|
+
pythonqwt-0.16.3.dist-info/METADATA,sha256=X3YbzcMwmBSzowUYsNzto4vrDG8cbHUiqNFRDK5GzDM,45515
|
|
88
|
+
pythonqwt-0.16.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
89
|
+
pythonqwt-0.16.3.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
|
|
90
|
+
pythonqwt-0.16.3.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
|
|
91
|
+
pythonqwt-0.16.3.dist-info/RECORD,,
|
qwt/__init__.py
CHANGED
|
@@ -63,7 +63,7 @@ from qwt.symbol import QwtSymbol as QSbl # see deprecated section
|
|
|
63
63
|
from qwt.text import QwtText # noqa: F401
|
|
64
64
|
from qwt.toqimage import array_to_qimage as toQImage # noqa: F401
|
|
65
65
|
|
|
66
|
-
__version__ = "0.16.
|
|
66
|
+
__version__ = "0.16.3"
|
|
67
67
|
QWT_VERSION_STR = "6.1.5"
|
|
68
68
|
|
|
69
69
|
|
qwt/null_paintdevice.py
CHANGED
|
@@ -275,7 +275,7 @@ class QwtNullPaintDevice(QPaintDevice):
|
|
|
275
275
|
elif deviceMetric == QPaintDevice.PdmDevicePixelRatio:
|
|
276
276
|
value = 1
|
|
277
277
|
elif deviceMetric == QPaintDevice.PdmDevicePixelRatioScaled:
|
|
278
|
-
value =
|
|
278
|
+
value = 0x10000
|
|
279
279
|
else:
|
|
280
280
|
value = super(QwtNullPaintDevice, self).metric(deviceMetric)
|
|
281
281
|
return value
|
qwt/plot.py
CHANGED
|
@@ -997,9 +997,30 @@ class QwtPlot(QFrame):
|
|
|
997
997
|
if canvas is not None:
|
|
998
998
|
canvas.setParent(self)
|
|
999
999
|
canvas.installEventFilter(self)
|
|
1000
|
+
# Mouse move events occur over the canvas widget, not the plot frame
|
|
1001
|
+
# itself, so the canvas must share the plot's mouse-tracking state
|
|
1002
|
+
# for `QwtPlot.setMouseTracking` to behave as expected.
|
|
1003
|
+
canvas.setMouseTracking(self.hasMouseTracking())
|
|
1000
1004
|
if self.isVisible():
|
|
1001
1005
|
canvas.show()
|
|
1002
1006
|
|
|
1007
|
+
def setMouseTracking(self, enable):
|
|
1008
|
+
"""
|
|
1009
|
+
Enable or disable mouse tracking for the plot.
|
|
1010
|
+
|
|
1011
|
+
The plot's drawing area is occupied by the canvas widget, so mouse move
|
|
1012
|
+
events over the plot are received by the canvas rather than the plot
|
|
1013
|
+
itself. This override propagates the mouse-tracking state to the canvas
|
|
1014
|
+
so that enabling tracking on the plot delivers mouse move events even
|
|
1015
|
+
when no mouse button is pressed (see Issue #88).
|
|
1016
|
+
|
|
1017
|
+
:param bool enable: True to enable mouse tracking, False to disable it
|
|
1018
|
+
"""
|
|
1019
|
+
super(QwtPlot, self).setMouseTracking(enable)
|
|
1020
|
+
canvas = self.__data.canvas
|
|
1021
|
+
if canvas is not None:
|
|
1022
|
+
canvas.setMouseTracking(enable)
|
|
1023
|
+
|
|
1003
1024
|
def event(self, event):
|
|
1004
1025
|
if event.type() == QEvent.LayoutRequest:
|
|
1005
1026
|
self.updateLayout()
|
qwt/plot_canvas.py
CHANGED
|
@@ -591,7 +591,6 @@ class QwtPlotCanvas(QFrame):
|
|
|
591
591
|
if (
|
|
592
592
|
self.testPaintAttribute(self.BackingStore)
|
|
593
593
|
and self.__data.backingStore is not None
|
|
594
|
-
and not self.__data.backingStore.isNull()
|
|
595
594
|
):
|
|
596
595
|
bs = self.__data.backingStore
|
|
597
596
|
pixelRatio = bs.devicePixelRatio()
|
|
@@ -615,6 +614,10 @@ class QwtPlotCanvas(QFrame):
|
|
|
615
614
|
if self.frameWidth() > 0:
|
|
616
615
|
self.drawBorder(p)
|
|
617
616
|
p.end()
|
|
617
|
+
# Store the regenerated pixmap back: the C++ original uses a
|
|
618
|
+
# reference into the stored pixmap, whereas the rebinding above
|
|
619
|
+
# only updates the local variable.
|
|
620
|
+
self.__data.backingStore = bs
|
|
618
621
|
painter.drawPixmap(0, 0, self.__data.backingStore)
|
|
619
622
|
else:
|
|
620
623
|
if self.testAttribute(Qt.WA_StyledBackground):
|
qwt/qtdesigner.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# Copyright (c) 2015 Pierre Raybaut, for the Qt Designer plugin helpers
|
|
5
|
+
# (see LICENSE file for more details)
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Qt Designer
|
|
9
|
+
-----------
|
|
10
|
+
|
|
11
|
+
The :mod:`qwt.qtdesigner` module provides helpers to integrate :mod:`qwt`
|
|
12
|
+
widgets into Qt Designer:
|
|
13
|
+
|
|
14
|
+
* :py:class:`QwtPlotWidget`
|
|
15
|
+
* :py:func:`loadui`
|
|
16
|
+
* :py:func:`compileui`
|
|
17
|
+
* :py:func:`create_qtdesigner_plugin`
|
|
18
|
+
|
|
19
|
+
:class:`QwtPlotWidget` is a thin :class:`qwt.QwtPlot` subclass exposing the
|
|
20
|
+
standard Qt widget constructor (``__init__(self, parent=None)``). It is the
|
|
21
|
+
widget promoted by the Qt Designer plugin, so that the code generated by
|
|
22
|
+
``uic`` (e.g. ``QwtPlotWidget(parent=...)`` with PyQt6) instantiates it
|
|
23
|
+
correctly. :class:`qwt.QwtPlot` itself keeps its historical constructor.
|
|
24
|
+
|
|
25
|
+
.. note::
|
|
26
|
+
|
|
27
|
+
Qt Designer custom widget plugins rely on
|
|
28
|
+
:class:`QtDesigner.QPyDesignerCustomWidgetPlugin`, which is only available
|
|
29
|
+
with PyQt5/PyQt6. PySide6 does not expose this class.
|
|
30
|
+
|
|
31
|
+
.. autoclass:: QwtPlotWidget
|
|
32
|
+
|
|
33
|
+
.. autofunction:: loadui
|
|
34
|
+
|
|
35
|
+
.. autofunction:: compileui
|
|
36
|
+
|
|
37
|
+
.. autofunction:: create_qtdesigner_plugin
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import io
|
|
43
|
+
|
|
44
|
+
from qtpy import QtGui as QG
|
|
45
|
+
from qtpy import uic
|
|
46
|
+
|
|
47
|
+
from qwt.plot import QwtPlot
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QwtPlotWidget(QwtPlot):
|
|
51
|
+
""":class:`qwt.QwtPlot` widget with the standard Qt constructor.
|
|
52
|
+
|
|
53
|
+
This subclass is meant to be promoted in Qt Designer: unlike
|
|
54
|
+
:class:`qwt.QwtPlot` (whose constructor accepts the historical
|
|
55
|
+
``QwtPlot([title], [parent])`` overloads), it exposes the conventional
|
|
56
|
+
``__init__(self, parent=None)`` signature expected by the code generated
|
|
57
|
+
by ``uic`` (notably PyQt6, which uses ``QwtPlotWidget(parent=...)``).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parent: Parent widget
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, parent=None):
|
|
64
|
+
super().__init__(parent)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def loadui(fname: str, replace_class: str | None = None):
|
|
68
|
+
"""Return Widget or Window class from a Qt Designer ``.ui`` file.
|
|
69
|
+
|
|
70
|
+
When a promoted widget inherits from a class that is not known to the
|
|
71
|
+
``uic`` parser (i.e. not a standard Qt widget), ``loadUiType`` fails to
|
|
72
|
+
resolve the widget hierarchy. Passing the offending base class name as
|
|
73
|
+
``replace_class`` neutralizes it by substituting ``QFrame`` (the base
|
|
74
|
+
class of :class:`qwt.QwtPlot`) before the file is parsed.
|
|
75
|
+
|
|
76
|
+
For a plain :class:`qwt.QwtPlot` widget (which already inherits from
|
|
77
|
+
``QFrame``), no replacement is required and ``replace_class`` can be left
|
|
78
|
+
to ``None``.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
fname: Path to the ``.ui`` file
|
|
82
|
+
replace_class: Base class name to replace by ``QFrame``, or ``None``
|
|
83
|
+
to load the file unchanged
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The generated form class
|
|
87
|
+
"""
|
|
88
|
+
with open(fname) as f:
|
|
89
|
+
uifile_text = f.read()
|
|
90
|
+
if replace_class is not None:
|
|
91
|
+
uifile_text = uifile_text.replace(replace_class, "QFrame")
|
|
92
|
+
ui, base_class = uic.loadUiType(io.StringIO(uifile_text))
|
|
93
|
+
|
|
94
|
+
class Form(base_class, ui):
|
|
95
|
+
"""Form class generated from the ``.ui`` file"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, parent=None):
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self.setupUi(self)
|
|
100
|
+
|
|
101
|
+
return Form
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compileui(fname: str, replace_class: str | None = None) -> None:
|
|
105
|
+
"""Compile a Qt Designer ``.ui`` file into a Python module.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
fname: Path to the ``.ui`` file
|
|
109
|
+
replace_class: Base class name to replace by ``QFrame``, or ``None``
|
|
110
|
+
to compile the file unchanged
|
|
111
|
+
"""
|
|
112
|
+
with open(fname) as f:
|
|
113
|
+
uifile_text = f.read()
|
|
114
|
+
if replace_class is not None:
|
|
115
|
+
uifile_text = uifile_text.replace(replace_class, "QFrame")
|
|
116
|
+
with open(fname.replace(".ui", "_ui.py"), "w") as pyfile:
|
|
117
|
+
uic.compileUi(io.StringIO(uifile_text), pyfile, pyqt3_wrapper=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_qtdesigner_plugin(
|
|
121
|
+
group: str,
|
|
122
|
+
module_name: str,
|
|
123
|
+
class_name: str,
|
|
124
|
+
icon: str | QG.QIcon | None = None,
|
|
125
|
+
tooltip: str = "",
|
|
126
|
+
whatsthis: str = "",
|
|
127
|
+
):
|
|
128
|
+
"""Return a custom Qt Designer plugin class.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
group: Name of the group the widget belongs to in Qt Designer
|
|
132
|
+
module_name: Name of the module where the widget class is defined
|
|
133
|
+
class_name: Name of the widget class
|
|
134
|
+
icon: Icon shown in Qt Designer (path to an image file or
|
|
135
|
+
:class:`QtGui.QIcon` instance)
|
|
136
|
+
tooltip: Tool tip shown in Qt Designer
|
|
137
|
+
whatsthis: "What's this" help text shown in Qt Designer
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A :class:`QtDesigner.QPyDesignerCustomWidgetPlugin` subclass
|
|
141
|
+
|
|
142
|
+
Example::
|
|
143
|
+
|
|
144
|
+
Plugin = create_qtdesigner_plugin(
|
|
145
|
+
group="PythonQwt",
|
|
146
|
+
module_name="qwt.qtdesigner",
|
|
147
|
+
class_name="QwtPlotWidget",
|
|
148
|
+
)
|
|
149
|
+
"""
|
|
150
|
+
from qtpy.QtDesigner import QPyDesignerCustomWidgetPlugin
|
|
151
|
+
|
|
152
|
+
Widget = getattr(__import__(module_name, fromlist=[class_name]), class_name)
|
|
153
|
+
|
|
154
|
+
class CustomWidgetPlugin(QPyDesignerCustomWidgetPlugin):
|
|
155
|
+
"""Qt Designer plugin for the ``%s`` widget""" % class_name
|
|
156
|
+
|
|
157
|
+
def __init__(self, parent=None):
|
|
158
|
+
QPyDesignerCustomWidgetPlugin.__init__(self)
|
|
159
|
+
self.initialized = False
|
|
160
|
+
|
|
161
|
+
def initialize(self, core):
|
|
162
|
+
"""Initialize the plugin"""
|
|
163
|
+
if self.initialized:
|
|
164
|
+
return
|
|
165
|
+
self.initialized = True
|
|
166
|
+
|
|
167
|
+
def isInitialized(self):
|
|
168
|
+
"""Return whether the plugin has been initialized"""
|
|
169
|
+
return self.initialized
|
|
170
|
+
|
|
171
|
+
def createWidget(self, parent):
|
|
172
|
+
"""Return a new instance of the custom widget"""
|
|
173
|
+
return Widget(parent)
|
|
174
|
+
|
|
175
|
+
def name(self):
|
|
176
|
+
"""Return the name of the custom widget class"""
|
|
177
|
+
return class_name
|
|
178
|
+
|
|
179
|
+
def group(self):
|
|
180
|
+
"""Return the name of the group the widget belongs to"""
|
|
181
|
+
return group
|
|
182
|
+
|
|
183
|
+
def icon(self):
|
|
184
|
+
"""Return the icon shown in Qt Designer"""
|
|
185
|
+
if isinstance(icon, QG.QIcon):
|
|
186
|
+
return icon
|
|
187
|
+
elif isinstance(icon, str):
|
|
188
|
+
return QG.QIcon(icon)
|
|
189
|
+
return QG.QIcon()
|
|
190
|
+
|
|
191
|
+
def toolTip(self):
|
|
192
|
+
"""Return the tool tip shown in Qt Designer"""
|
|
193
|
+
return tooltip
|
|
194
|
+
|
|
195
|
+
def whatsThis(self):
|
|
196
|
+
"""Return the "What's this" help text shown in Qt Designer"""
|
|
197
|
+
return whatsthis
|
|
198
|
+
|
|
199
|
+
def isContainer(self):
|
|
200
|
+
"""Return whether the widget is a container"""
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def domXml(self):
|
|
204
|
+
"""Return the XML used to define the widget in Qt Designer"""
|
|
205
|
+
return f'<widget class="{class_name}" name="{class_name.lower()}" />\n'
|
|
206
|
+
|
|
207
|
+
def includeFile(self):
|
|
208
|
+
"""Return the module name where the widget class is defined"""
|
|
209
|
+
return module_name
|
|
210
|
+
|
|
211
|
+
return CustomWidgetPlugin
|
qwt/scale_draw.py
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# (see LICENSE file for more details)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Test for issue 109: the ``QwtPlotCanvas.BackingStore`` paint attribute was
|
|
8
|
+
non-functional.
|
|
9
|
+
|
|
10
|
+
After ``invalidateBackingStore()`` reset the buffer to an empty ``QPixmap()``,
|
|
11
|
+
``paintEvent`` skipped the backing-store branch because of an extra
|
|
12
|
+
``isNull()`` guard, so the double-buffering cache was never regenerated. The
|
|
13
|
+
regenerated pixmap was also only bound to a local variable and never stored
|
|
14
|
+
back. This test ensures the cache is refreshed (and reused) as intended.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from qtpy import QtCore as QC
|
|
19
|
+
from qtpy import QtWidgets as QW
|
|
20
|
+
|
|
21
|
+
from qwt import QwtPlot, QwtPlotCurve
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_plot_with_backing_store():
|
|
25
|
+
plot = QwtPlot()
|
|
26
|
+
plot.resize(QC.QSize(400, 300))
|
|
27
|
+
canvas = plot.canvas()
|
|
28
|
+
canvas.setPaintAttribute(canvas.BackingStore, True)
|
|
29
|
+
x = np.linspace(0, 10, 100)
|
|
30
|
+
QwtPlotCurve.make(x, np.sin(x), "Sine", plot)
|
|
31
|
+
return plot, canvas
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_backing_store_is_regenerated_after_replot():
|
|
35
|
+
"""The backing store must be repopulated when painting after a replot."""
|
|
36
|
+
# A live QApplication must exist before constructing any QWidget, otherwise
|
|
37
|
+
# Qt aborts the process. Tests run in a shared interpreter, but no test
|
|
38
|
+
# keeps a persistent Python reference to the application, so the singleton
|
|
39
|
+
# may be garbage-collected between tests (observed on Linux/PyQt5 in CI).
|
|
40
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
41
|
+
|
|
42
|
+
plot, canvas = _make_plot_with_backing_store()
|
|
43
|
+
plot.show()
|
|
44
|
+
app.processEvents() # lay the canvas out and paint it a first time
|
|
45
|
+
|
|
46
|
+
plot.replot() # invalidates the backing store
|
|
47
|
+
|
|
48
|
+
# Force a paint so the backing store branch runs. ``grab()`` synchronously
|
|
49
|
+
# triggers ``paintEvent`` and works headless (QT_QPA_PLATFORM=offscreen),
|
|
50
|
+
# without depending on the event loop delivering a deferred repaint.
|
|
51
|
+
canvas.grab()
|
|
52
|
+
|
|
53
|
+
bs = canvas.backingStore()
|
|
54
|
+
assert bs is not None
|
|
55
|
+
assert not bs.isNull()
|
|
56
|
+
# The regenerated pixmap must match the canvas size (accounting for the
|
|
57
|
+
# device pixel ratio), proving it was actually rebuilt and stored back.
|
|
58
|
+
assert bs.size() == canvas.size() * bs.devicePixelRatio()
|
|
59
|
+
|
|
60
|
+
plot.close()
|
|
61
|
+
plot.deleteLater()
|
|
62
|
+
app.processEvents()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
67
|
+
test_backing_store_is_regenerated_after_replot()
|
|
68
|
+
print("OK")
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the MIT License
|
|
4
|
+
# Copyright (c) 2024 Pierre Raybaut
|
|
5
|
+
# (see LICENSE file for more details)
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Side-by-side plots sharing a single vertical scale (issue #33)
|
|
9
|
+
--------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
This example answers `issue #33
|
|
12
|
+
<https://github.com/PlotPyStack/PythonQwt/issues/33>`_: how to display several
|
|
13
|
+
vertical plots side-by-side, perfectly aligned on a *single* shared vertical
|
|
14
|
+
scale (here the depth down a drilled core), while each plot keeps its own
|
|
15
|
+
horizontal scale (element concentration, density, ...).
|
|
16
|
+
|
|
17
|
+
Two practical problems are solved here:
|
|
18
|
+
|
|
19
|
+
1. **Vertical alignment.** When the left-most plot shows the depth axis and the
|
|
20
|
+
others do not, their canvases do not line up: an enabled vertical scale
|
|
21
|
+
reserves a "border distance" at both ends (so the extreme tick labels are not
|
|
22
|
+
clipped), which pushes its canvas down by a few pixels. To keep every canvas
|
|
23
|
+
aligned, the depth axis is *enabled on every plot* but only *drawn* on the
|
|
24
|
+
first one; on the other plots its components (labels, ticks, backbone) are
|
|
25
|
+
hidden while the very same border distance is reserved through
|
|
26
|
+
:py:meth:`qwt.scale_widget.QwtScaleWidget.setMinBorderDist`. This survives
|
|
27
|
+
relayout/zoom because :py:meth:`qwt.plot.QwtPlot.updateLayout` always honors
|
|
28
|
+
the minimum border distance.
|
|
29
|
+
|
|
30
|
+
2. **Synchronized zoom/scroll.** A single vertical scroll bar and a pair of
|
|
31
|
+
zoom buttons drive the visible depth window of *all* plots at once, by
|
|
32
|
+
calling :py:meth:`qwt.plot.QwtPlot.setAxisScale` on the shared ``yLeft``
|
|
33
|
+
axis of each plot.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
SHOW = True # Show test in GUI-based test launcher
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
from qtpy import QtCore as QC
|
|
40
|
+
from qtpy import QtGui as QG
|
|
41
|
+
from qtpy import QtWidgets as QW
|
|
42
|
+
|
|
43
|
+
from qwt import (
|
|
44
|
+
QwtLinearScaleEngine,
|
|
45
|
+
QwtPlot,
|
|
46
|
+
QwtPlotCurve,
|
|
47
|
+
QwtPlotGrid,
|
|
48
|
+
QwtScaleDraw,
|
|
49
|
+
)
|
|
50
|
+
from qwt.tests import utils
|
|
51
|
+
|
|
52
|
+
DEPTH_MIN, DEPTH_MAX = 0.0, 120.0 # meters
|
|
53
|
+
SCROLL_RESOLUTION = 100 # scroll bar steps per meter
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def make_drillcore_data():
|
|
57
|
+
"""Return synthetic drill-core logs as ``(title, [(label, x, depth)], unit)``"""
|
|
58
|
+
depth = np.linspace(DEPTH_MIN, DEPTH_MAX, 800)
|
|
59
|
+
rng = np.random.default_rng(42)
|
|
60
|
+
|
|
61
|
+
def log(base, amp, n_bumps, noise):
|
|
62
|
+
signal = base + noise * rng.standard_normal(depth.size)
|
|
63
|
+
for _ in range(n_bumps):
|
|
64
|
+
center = rng.uniform(DEPTH_MIN, DEPTH_MAX)
|
|
65
|
+
width = rng.uniform(3.0, 12.0)
|
|
66
|
+
# Clip the exponent to avoid float64 underflow (numpy may be set to
|
|
67
|
+
# raise on underflow in the test suite).
|
|
68
|
+
signal += amp * np.exp(
|
|
69
|
+
np.clip(-(((depth - center) / width) ** 2), -50.0, 0.0)
|
|
70
|
+
)
|
|
71
|
+
return np.clip(signal, 0, None)
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
(
|
|
75
|
+
"Iron",
|
|
76
|
+
"Fe (%)",
|
|
77
|
+
[("Fe", log(8.0, 18.0, 5, 0.6), depth)],
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"Copper",
|
|
81
|
+
"Cu (ppm)",
|
|
82
|
+
[
|
|
83
|
+
("Cu", log(150.0, 600.0, 4, 20.0), depth),
|
|
84
|
+
("Cu (smoothed)", log(150.0, 580.0, 4, 2.0), depth),
|
|
85
|
+
],
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"Density",
|
|
89
|
+
"rho (g/cm3)",
|
|
90
|
+
[("rho", 2.6 + 0.04 * np.sin(depth / 6.0) + log(0.0, 0.2, 3, 0.01), depth)],
|
|
91
|
+
),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DepthPlot(QwtPlot):
|
|
96
|
+
"""A single drill-core log plot with depth on the (inverted) left axis"""
|
|
97
|
+
|
|
98
|
+
CURVE_COLORS = ("#1f77b4", "#d62728", "#2ca02c", "#9467bd")
|
|
99
|
+
|
|
100
|
+
def _curve_color(self, index):
|
|
101
|
+
"""Return a :py:class:`QColor` for the curve at ``index``"""
|
|
102
|
+
return QG.QColor(self.CURVE_COLORS[index % len(self.CURVE_COLORS)])
|
|
103
|
+
|
|
104
|
+
def __init__(self, title, x_title, curves, show_depth_axis, parent=None):
|
|
105
|
+
super().__init__(parent)
|
|
106
|
+
self.setObjectName(title)
|
|
107
|
+
self.setTitle(title)
|
|
108
|
+
|
|
109
|
+
# By default QwtPlot uses a MinimumExpanding horizontal policy, which
|
|
110
|
+
# makes the surrounding layout treat sizeHint() (not minimumSizeHint())
|
|
111
|
+
# as a hard minimum width. With several plots side-by-side this prevents
|
|
112
|
+
# the widget from being shrunk. An "Ignored" horizontal policy lets every
|
|
113
|
+
# column shrink freely and share the available width equally.
|
|
114
|
+
self.setSizePolicy(QW.QSizePolicy.Ignored, QW.QSizePolicy.Preferred)
|
|
115
|
+
|
|
116
|
+
# Horizontal (data) axis on top, like column headers in a core log
|
|
117
|
+
self.enableAxis(self.xBottom, False)
|
|
118
|
+
self.enableAxis(self.xTop, True)
|
|
119
|
+
self.setAxisTitle(self.xTop, x_title)
|
|
120
|
+
# Limit the number of major ticks: the horizontal axis reserves room for
|
|
121
|
+
# all its tick labels (minLabelDist * majorCount), which otherwise sets a
|
|
122
|
+
# very large minimum width and crowds the labels when the plot is narrow.
|
|
123
|
+
self.setAxisMaxMajor(self.xTop, 4)
|
|
124
|
+
|
|
125
|
+
# Vertical (depth) axis: inverted so depth increases downwards
|
|
126
|
+
self.enableAxis(self.yLeft, True)
|
|
127
|
+
self.axisScaleEngine(self.yLeft).setAttribute(
|
|
128
|
+
QwtLinearScaleEngine.Inverted, True
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if show_depth_axis:
|
|
132
|
+
self.setAxisTitle(self.yLeft, "Depth (m)")
|
|
133
|
+
else:
|
|
134
|
+
# Keep the axis enabled (so the canvas stays aligned with the
|
|
135
|
+
# reference plot) but hide everything it draws.
|
|
136
|
+
scale_draw = self.axisScaleDraw(self.yLeft)
|
|
137
|
+
scale_draw.enableComponent(QwtScaleDraw.Labels, False)
|
|
138
|
+
scale_draw.enableComponent(QwtScaleDraw.Ticks, False)
|
|
139
|
+
scale_draw.enableComponent(QwtScaleDraw.Backbone, False)
|
|
140
|
+
scale_draw.setMinimumExtent(0)
|
|
141
|
+
|
|
142
|
+
QwtPlotGrid.make(self, color=QC.Qt.lightGray, width=0, style=QC.Qt.DotLine)
|
|
143
|
+
|
|
144
|
+
for index, (label, x, depth) in enumerate(curves):
|
|
145
|
+
color = self._curve_color(index)
|
|
146
|
+
curve = QwtPlotCurve.make(x, depth, label, self, linecolor=color)
|
|
147
|
+
curve.setAxes(self.xTop, self.yLeft)
|
|
148
|
+
|
|
149
|
+
self.setAxisScale(self.yLeft, DEPTH_MIN, DEPTH_MAX)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DrillCoreViewer(QW.QWidget):
|
|
153
|
+
"""Several depth plots side-by-side, aligned on a single shared depth scale"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, parent=None):
|
|
156
|
+
super().__init__(parent)
|
|
157
|
+
self.full_span = DEPTH_MAX - DEPTH_MIN
|
|
158
|
+
self.view_span = self.full_span
|
|
159
|
+
self.view_top = DEPTH_MIN
|
|
160
|
+
|
|
161
|
+
self.plots = []
|
|
162
|
+
plots_layout = QW.QHBoxLayout()
|
|
163
|
+
plots_layout.setSpacing(0)
|
|
164
|
+
for index, (title, x_title, curves) in enumerate(make_drillcore_data()):
|
|
165
|
+
plot = DepthPlot(title, x_title, curves, show_depth_axis=index == 0)
|
|
166
|
+
self.plots.append(plot)
|
|
167
|
+
plots_layout.addWidget(plot)
|
|
168
|
+
|
|
169
|
+
# Vertical scroll bar to move the visible depth window
|
|
170
|
+
self.scrollbar = QW.QScrollBar(QC.Qt.Vertical)
|
|
171
|
+
self.scrollbar.valueChanged.connect(self.scroll_to)
|
|
172
|
+
plots_layout.addWidget(self.scrollbar)
|
|
173
|
+
|
|
174
|
+
# Zoom buttons
|
|
175
|
+
zoom_in = QW.QToolButton()
|
|
176
|
+
zoom_in.setText("+")
|
|
177
|
+
zoom_in.setToolTip("Zoom in (depth)")
|
|
178
|
+
zoom_in.clicked.connect(lambda: self.zoom(0.5))
|
|
179
|
+
zoom_out = QW.QToolButton()
|
|
180
|
+
zoom_out.setText("-")
|
|
181
|
+
zoom_out.setToolTip("Zoom out (depth)")
|
|
182
|
+
zoom_out.clicked.connect(lambda: self.zoom(2.0))
|
|
183
|
+
reset = QW.QToolButton()
|
|
184
|
+
reset.setText("Reset")
|
|
185
|
+
reset.clicked.connect(self.reset_view)
|
|
186
|
+
toolbar = QW.QHBoxLayout()
|
|
187
|
+
toolbar.addWidget(QW.QLabel("Depth zoom:"))
|
|
188
|
+
toolbar.addWidget(zoom_in)
|
|
189
|
+
toolbar.addWidget(zoom_out)
|
|
190
|
+
toolbar.addWidget(reset)
|
|
191
|
+
toolbar.addStretch()
|
|
192
|
+
|
|
193
|
+
layout = QW.QVBoxLayout()
|
|
194
|
+
layout.addLayout(toolbar)
|
|
195
|
+
layout.addLayout(plots_layout)
|
|
196
|
+
self.setLayout(layout)
|
|
197
|
+
|
|
198
|
+
# Align the hidden depth axes with the reference one and apply the view.
|
|
199
|
+
QC.QTimer.singleShot(0, self._align_and_refresh)
|
|
200
|
+
|
|
201
|
+
def _align_and_refresh(self):
|
|
202
|
+
"""Force identical vertical border distance on every depth axis"""
|
|
203
|
+
for plot in self.plots:
|
|
204
|
+
plot.replot() # ensure scale divisions are computed
|
|
205
|
+
ref_widget = self.plots[0].axisWidget(self.plots[0].yLeft)
|
|
206
|
+
start, end = ref_widget.getBorderDistHint()
|
|
207
|
+
for plot in self.plots[1:]:
|
|
208
|
+
plot.axisWidget(plot.yLeft).setMinBorderDist(start, end)
|
|
209
|
+
self._apply_view()
|
|
210
|
+
|
|
211
|
+
def _apply_view(self):
|
|
212
|
+
"""Apply the current depth window to every plot and update the scroll bar"""
|
|
213
|
+
bottom = self.view_top + self.view_span
|
|
214
|
+
for plot in self.plots:
|
|
215
|
+
plot.setAxisScale(plot.yLeft, self.view_top, bottom)
|
|
216
|
+
plot.replot()
|
|
217
|
+
self._update_scrollbar()
|
|
218
|
+
|
|
219
|
+
def _update_scrollbar(self):
|
|
220
|
+
"""Reflect the current depth window in the scroll bar geometry"""
|
|
221
|
+
hidden = max(self.full_span - self.view_span, 0.0)
|
|
222
|
+
self.scrollbar.blockSignals(True)
|
|
223
|
+
self.scrollbar.setRange(0, int(round(hidden * SCROLL_RESOLUTION)))
|
|
224
|
+
self.scrollbar.setPageStep(int(round(self.view_span * SCROLL_RESOLUTION)))
|
|
225
|
+
self.scrollbar.setSingleStep(
|
|
226
|
+
int(round(self.view_span * SCROLL_RESOLUTION / 10))
|
|
227
|
+
)
|
|
228
|
+
self.scrollbar.setValue(
|
|
229
|
+
int(round((self.view_top - DEPTH_MIN) * SCROLL_RESOLUTION))
|
|
230
|
+
)
|
|
231
|
+
self.scrollbar.setEnabled(hidden > 0)
|
|
232
|
+
self.scrollbar.blockSignals(False)
|
|
233
|
+
|
|
234
|
+
def scroll_to(self, value):
|
|
235
|
+
"""Scroll bar handler: move the visible depth window"""
|
|
236
|
+
self.view_top = DEPTH_MIN + value / SCROLL_RESOLUTION
|
|
237
|
+
self._apply_view()
|
|
238
|
+
|
|
239
|
+
def zoom(self, factor):
|
|
240
|
+
"""Zoom the visible depth window around its center by ``factor``"""
|
|
241
|
+
center = self.view_top + self.view_span / 2.0
|
|
242
|
+
self.view_span = min(self.full_span, self.view_span * factor)
|
|
243
|
+
self.view_top = center - self.view_span / 2.0
|
|
244
|
+
self.view_top = max(DEPTH_MIN, min(self.view_top, DEPTH_MAX - self.view_span))
|
|
245
|
+
self._apply_view()
|
|
246
|
+
|
|
247
|
+
def reset_view(self):
|
|
248
|
+
"""Restore the full depth range"""
|
|
249
|
+
self.view_span = self.full_span
|
|
250
|
+
self.view_top = DEPTH_MIN
|
|
251
|
+
self._apply_view()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_issue33_sidebyside():
|
|
255
|
+
"""Side-by-side plots sharing a single vertical scale (issue #33)"""
|
|
256
|
+
utils.test_widget(DrillCoreViewer, size=(700, 600))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
test_issue33_sidebyside()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# (see LICENSE file for more details)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Test for issue 88: ``QwtPlot.setMouseTracking`` was ignored.
|
|
8
|
+
|
|
9
|
+
The plot's drawing area is occupied by the canvas widget, so mouse move events
|
|
10
|
+
over the plot are delivered to the canvas rather than to the ``QwtPlot`` frame.
|
|
11
|
+
Calling ``QwtPlot.setMouseTracking(True)`` therefore had no visible effect: the
|
|
12
|
+
canvas kept its default (button-pressed-only) move events. ``QwtPlot`` now
|
|
13
|
+
propagates its mouse-tracking state to the canvas so that the setting behaves
|
|
14
|
+
as documented by ``QWidget.setMouseTracking``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from qtpy import QtWidgets as QW
|
|
18
|
+
|
|
19
|
+
from qwt import QwtPlot, QwtPlotCanvas
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_app():
|
|
23
|
+
# A live QApplication must exist before constructing any QWidget, otherwise
|
|
24
|
+
# Qt aborts the process. Tests run in a shared interpreter, but no test
|
|
25
|
+
# keeps a persistent Python reference to the application, so the singleton
|
|
26
|
+
# may be garbage-collected between tests (observed on Linux/PyQt5 in CI).
|
|
27
|
+
return QW.QApplication.instance() or QW.QApplication([])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_set_mouse_tracking_propagates_to_canvas():
|
|
31
|
+
"""Enabling/disabling mouse tracking on the plot updates the canvas."""
|
|
32
|
+
app = _ensure_app() # keep a reference alive for the duration of the test
|
|
33
|
+
plot = QwtPlot()
|
|
34
|
+
|
|
35
|
+
# By default mouse tracking is off on both the plot and its canvas.
|
|
36
|
+
assert not plot.hasMouseTracking()
|
|
37
|
+
assert not plot.canvas().hasMouseTracking()
|
|
38
|
+
|
|
39
|
+
plot.setMouseTracking(True)
|
|
40
|
+
assert plot.hasMouseTracking()
|
|
41
|
+
assert plot.canvas().hasMouseTracking()
|
|
42
|
+
|
|
43
|
+
plot.setMouseTracking(False)
|
|
44
|
+
assert not plot.hasMouseTracking()
|
|
45
|
+
assert not plot.canvas().hasMouseTracking()
|
|
46
|
+
|
|
47
|
+
plot.close()
|
|
48
|
+
del app
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_set_canvas_inherits_mouse_tracking_state():
|
|
52
|
+
"""A canvas set after enabling tracking inherits the plot's state."""
|
|
53
|
+
app = _ensure_app() # keep a reference alive for the duration of the test
|
|
54
|
+
plot = QwtPlot()
|
|
55
|
+
plot.setMouseTracking(True)
|
|
56
|
+
|
|
57
|
+
new_canvas = QwtPlotCanvas(plot)
|
|
58
|
+
assert not new_canvas.hasMouseTracking()
|
|
59
|
+
|
|
60
|
+
plot.setCanvas(new_canvas)
|
|
61
|
+
assert plot.canvas() is new_canvas
|
|
62
|
+
assert new_canvas.hasMouseTracking()
|
|
63
|
+
|
|
64
|
+
plot.close()
|
|
65
|
+
del app
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
test_set_mouse_tracking_propagates_to_canvas()
|
|
70
|
+
test_set_canvas_inherits_mouse_tracking_state()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# Copyright (c) 2015 Pierre Raybaut, for the Qt Designer plugin test
|
|
5
|
+
# (see LICENSE file for more details)
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Testing PythonQwt Qt Designer plugin
|
|
9
|
+
|
|
10
|
+
This plugin makes the :class:`qwt.QwtPlot` widget available in Qt Designer.
|
|
11
|
+
The test loads a ``.ui`` file embedding a ``QwtPlot`` widget, as Qt Designer
|
|
12
|
+
would generate it once the plugin is installed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
SHOW = True # Show test in GUI-based test launcher
|
|
16
|
+
|
|
17
|
+
import os.path as osp
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pytest
|
|
21
|
+
from qtpy.QtCore import Qt
|
|
22
|
+
|
|
23
|
+
import qwt
|
|
24
|
+
from qwt.qtdesigner import loadui
|
|
25
|
+
from qwt.tests import utils
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
FormClass = loadui(osp.splitext(__file__)[0] + ".ui")
|
|
29
|
+
except Exception as exc: # pragma: no cover - binding-specific uic limitation
|
|
30
|
+
pytest.skip(
|
|
31
|
+
"Qt Designer .ui loading is not supported by this Qt binding (%s)" % exc,
|
|
32
|
+
allow_module_level=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class QtDesignerWindow(FormClass):
|
|
37
|
+
"""Window built from the Qt Designer ``.ui`` file"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
super().__init__()
|
|
41
|
+
plot = self.plotwidget
|
|
42
|
+
plot.setTitle("QtDesigner plugin example")
|
|
43
|
+
x = np.linspace(0.0, 10.0, 500)
|
|
44
|
+
qwt.QwtPlotCurve.make(x, np.sin(x), "y = sin(x)", plot, linecolor="red")
|
|
45
|
+
qwt.QwtPlotGrid.make(plot, color=Qt.lightGray, width=0, style=Qt.DotLine)
|
|
46
|
+
plot.replot()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_qtdesigner():
|
|
50
|
+
"""Qt Designer plugin example"""
|
|
51
|
+
utils.test_widget(QtDesignerWindow, size=(600, 400))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
test_qtdesigner()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<ui version="4.0">
|
|
3
|
+
<class>Dialog</class>
|
|
4
|
+
<widget class="QDialog" name="Dialog">
|
|
5
|
+
<property name="geometry">
|
|
6
|
+
<rect>
|
|
7
|
+
<x>0</x>
|
|
8
|
+
<y>0</y>
|
|
9
|
+
<width>500</width>
|
|
10
|
+
<height>400</height>
|
|
11
|
+
</rect>
|
|
12
|
+
</property>
|
|
13
|
+
<property name="windowTitle">
|
|
14
|
+
<string>Dialog</string>
|
|
15
|
+
</property>
|
|
16
|
+
<layout class="QVBoxLayout" name="verticalLayout">
|
|
17
|
+
<item>
|
|
18
|
+
<widget class="QwtPlotWidget" name="plotwidget"/>
|
|
19
|
+
</item>
|
|
20
|
+
</layout>
|
|
21
|
+
</widget>
|
|
22
|
+
<customwidgets>
|
|
23
|
+
<customwidget>
|
|
24
|
+
<class>QwtPlotWidget</class>
|
|
25
|
+
<extends>QFrame</extends>
|
|
26
|
+
<header>qwt.qtdesigner</header>
|
|
27
|
+
</customwidget>
|
|
28
|
+
</customwidgets>
|
|
29
|
+
<resources/>
|
|
30
|
+
<connections/>
|
|
31
|
+
</ui>
|
qwt/tests/utils.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|