PythonQwt 0.16.0__tar.gz → 0.16.2__tar.gz
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.0/PythonQwt.egg-info → pythonqwt-0.16.2}/PKG-INFO +1 -1
- {pythonqwt-0.16.0 → pythonqwt-0.16.2/PythonQwt.egg-info}/PKG-INFO +1 -1
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt.egg-info/SOURCES.txt +1 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/pyproject.toml +1 -1
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/__init__.py +1 -1
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot.py +39 -17
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_canvas.py +12 -3
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/scale_draw.py +7 -26
- pythonqwt-0.16.2/qwt/tests/test_issue107_gdi_leak.py +72 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/text.py +72 -86
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/LICENSE +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/MANIFEST.in +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt-tests.desktop +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt.egg-info/dependency_links.txt +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt.egg-info/entry_points.txt +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt.egg-info/requires.txt +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/PythonQwt.egg-info/top_level.txt +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/README.md +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/_static/PythonQwt_logo.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/_static/QwtPlot_example.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/_static/panorama.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/_static/symbol_path_example.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/conf.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/bodedemo.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/cartesian.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/cpudemo.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/curvebenchmark1.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/curvebenchmark2.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/curvedemo1.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/curvedemo2.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/data.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/errorbar.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/eventfilter.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/image.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/index.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/logcurve.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/mapdemo.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/multidemo.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/simple.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/examples/vertical.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/index.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/installation.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/issue93_optimization_summary.md +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/overview.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/plot_example.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/graphic.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/index.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/interval.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/plot.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/plot_directpainter.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/plot_layout.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/plot_series.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/scale.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/symbol.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/text.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/toqimage.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/reference/transform.rst +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/doc/symbol_path_example.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/_math.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/color_map.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/column_symbol.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/dyngrid_layout.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/graphic.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/interval.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/legend.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/null_paintdevice.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/painter.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/painter_command.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_curve.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_directpainter.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_grid.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_layout.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_marker.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_renderer.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/plot_series.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/qthelpers.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/scale_div.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/scale_engine.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/scale_map.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/scale_widget.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/symbol.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/__init__.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/comparative_benchmarks.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/conftest.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/PythonQwt.svg +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/bodedemo.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/cartesian.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/cpudemo.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/curvebenchmark1.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/curvebenchmark2.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/curvedemo1.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/curvedemo2.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/data.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/errorbar.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/eventfilter.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/image.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/loadtest.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/logcurve.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/mapdemo.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/multidemo.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/simple.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/stylesheet.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/symbol.svg +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/symbols.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/testlauncher.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/data/vertical.png +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_backingstore.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_bodedemo.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_cartesian.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_cpudemo.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_curvebenchmark1.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_curvebenchmark2.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_curvedemo1.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_curvedemo2.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_data.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_errorbar.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_eventfilter.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_highdpi.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_image.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_loadtest.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_logcurve.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_mapdemo.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_multidemo.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_relativemargin.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_simple.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_stylesheet.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_symbols.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/test_vertical.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/tests/utils.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/toqimage.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/qwt/transform.py +0 -0
- {pythonqwt-0.16.0 → pythonqwt-0.16.2}/setup.cfg +0 -0
|
@@ -59,7 +59,7 @@ version = { attr = "qwt.__version__" }
|
|
|
59
59
|
addopts = "qwt"
|
|
60
60
|
|
|
61
61
|
[tool.ruff]
|
|
62
|
-
exclude = [".git", ".vscode", "build", "dist"]
|
|
62
|
+
exclude = [".git", ".vscode", "build", "dist","venv*",".venv*"]
|
|
63
63
|
line-length = 88 # Same as Black.
|
|
64
64
|
indent-width = 4 # Same as Black.
|
|
65
65
|
target-version = "py39" # Assume Python 3.9.
|
|
@@ -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.2"
|
|
67
67
|
QWT_VERSION_STR = "6.1.5"
|
|
68
68
|
|
|
69
69
|
|
|
@@ -20,6 +20,7 @@ QwtPlotItem
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import math
|
|
23
|
+
import weakref
|
|
23
24
|
|
|
24
25
|
import numpy as np
|
|
25
26
|
from qtpy.QtCore import QEvent, QObject, QRectF, QSize, Qt, Signal
|
|
@@ -1007,7 +1008,12 @@ class QwtPlot(QFrame):
|
|
|
1007
1008
|
return QFrame.event(self, event)
|
|
1008
1009
|
|
|
1009
1010
|
def eventFilter(self, obj, event):
|
|
1010
|
-
|
|
1011
|
+
try:
|
|
1012
|
+
canvas = self.__data.canvas
|
|
1013
|
+
except AttributeError:
|
|
1014
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1015
|
+
return QFrame.eventFilter(self, obj, event)
|
|
1016
|
+
if obj is canvas:
|
|
1011
1017
|
if event.type() == QEvent.Resize:
|
|
1012
1018
|
self.updateCanvasMargins()
|
|
1013
1019
|
elif event.type() == 178: # QEvent.ContentsRectChange:
|
|
@@ -1195,7 +1201,12 @@ class QwtPlot(QFrame):
|
|
|
1195
1201
|
"""
|
|
1196
1202
|
:return: Return a minimum size hint
|
|
1197
1203
|
"""
|
|
1198
|
-
|
|
1204
|
+
try:
|
|
1205
|
+
layout = self.__data.layout
|
|
1206
|
+
except AttributeError:
|
|
1207
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1208
|
+
return QSize()
|
|
1209
|
+
hint = layout.minimumSizeHint(self)
|
|
1199
1210
|
hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth())
|
|
1200
1211
|
return hint
|
|
1201
1212
|
|
|
@@ -1815,16 +1826,23 @@ class QwtPlotItem(object):
|
|
|
1815
1826
|
|
|
1816
1827
|
:py:meth:`detach()`
|
|
1817
1828
|
"""
|
|
1818
|
-
if plot is self.
|
|
1829
|
+
if plot is self.plot():
|
|
1819
1830
|
return
|
|
1820
1831
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1832
|
+
current = self.plot()
|
|
1833
|
+
if current is not None:
|
|
1834
|
+
current.attachItem(self, False)
|
|
1823
1835
|
|
|
1824
|
-
|
|
1836
|
+
# Store the parent plot as a WEAK reference: an item must not keep its
|
|
1837
|
+
# plot (and the plot's GDI-backed child widgets) alive. A strong
|
|
1838
|
+
# back-reference here forms a ``plot <-> item`` reference cycle that can
|
|
1839
|
+
# only be reclaimed by the cyclic garbage collector, which delays the
|
|
1840
|
+
# release of native GDI handles and can lead to GDI-handle exhaustion
|
|
1841
|
+
# on Windows when many plots are created and destroyed.
|
|
1842
|
+
self.__data.plot = None if plot is None else weakref.ref(plot)
|
|
1825
1843
|
|
|
1826
|
-
if
|
|
1827
|
-
|
|
1844
|
+
if plot is not None:
|
|
1845
|
+
plot.attachItem(self, True)
|
|
1828
1846
|
|
|
1829
1847
|
def detach(self):
|
|
1830
1848
|
"""
|
|
@@ -1854,7 +1872,8 @@ class QwtPlotItem(object):
|
|
|
1854
1872
|
"""
|
|
1855
1873
|
:return: attached plot
|
|
1856
1874
|
"""
|
|
1857
|
-
|
|
1875
|
+
ref = self.__data.plot
|
|
1876
|
+
return None if ref is None else ref()
|
|
1858
1877
|
|
|
1859
1878
|
def z(self):
|
|
1860
1879
|
"""
|
|
@@ -1881,11 +1900,12 @@ class QwtPlotItem(object):
|
|
|
1881
1900
|
:py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
|
|
1882
1901
|
"""
|
|
1883
1902
|
if self.__data.z != z:
|
|
1884
|
-
|
|
1885
|
-
|
|
1903
|
+
plot = self.plot()
|
|
1904
|
+
if plot is not None:
|
|
1905
|
+
plot.attachItem(self, False)
|
|
1886
1906
|
self.__data.z = z
|
|
1887
|
-
if
|
|
1888
|
-
|
|
1907
|
+
if plot is not None:
|
|
1908
|
+
plot.attachItem(self, True)
|
|
1889
1909
|
self.itemChanged()
|
|
1890
1910
|
|
|
1891
1911
|
def setTitle(self, title):
|
|
@@ -2091,8 +2111,9 @@ class QwtPlotItem(object):
|
|
|
2091
2111
|
|
|
2092
2112
|
:py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
|
|
2093
2113
|
"""
|
|
2094
|
-
|
|
2095
|
-
|
|
2114
|
+
plot = self.plot()
|
|
2115
|
+
if plot is not None:
|
|
2116
|
+
plot.autoRefresh()
|
|
2096
2117
|
|
|
2097
2118
|
def legendChanged(self):
|
|
2098
2119
|
"""
|
|
@@ -2102,8 +2123,9 @@ class QwtPlotItem(object):
|
|
|
2102
2123
|
|
|
2103
2124
|
:py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
|
|
2104
2125
|
"""
|
|
2105
|
-
|
|
2106
|
-
|
|
2126
|
+
plot = self.plot()
|
|
2127
|
+
if self.testItemAttribute(QwtPlotItem.Legend) and plot is not None:
|
|
2128
|
+
plot.updateLegend(self)
|
|
2107
2129
|
|
|
2108
2130
|
def setAxes(self, xAxis, yAxis):
|
|
2109
2131
|
"""
|
|
@@ -13,6 +13,7 @@ QwtPlotCanvas
|
|
|
13
13
|
:members:
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import weakref
|
|
16
17
|
from collections.abc import Sequence
|
|
17
18
|
|
|
18
19
|
from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt
|
|
@@ -442,7 +443,12 @@ class QwtPlotCanvas(QFrame):
|
|
|
442
443
|
|
|
443
444
|
def __init__(self, plot=None):
|
|
444
445
|
super(QwtPlotCanvas, self).__init__(plot)
|
|
445
|
-
|
|
446
|
+
# Store the parent plot as a WEAK reference to avoid a
|
|
447
|
+
# ``plot <-> canvas`` reference cycle (the plot already owns the canvas
|
|
448
|
+
# through Qt's parent/child relationship). A strong back-reference here
|
|
449
|
+
# would keep the plot alive until the cyclic garbage collector runs,
|
|
450
|
+
# delaying the release of native GDI handles on Windows.
|
|
451
|
+
self.__plot = None if plot is None else weakref.ref(plot)
|
|
446
452
|
self.setFrameStyle(QFrame.Panel | QFrame.Sunken)
|
|
447
453
|
self.setLineWidth(2)
|
|
448
454
|
self.__data = QwtPlotCanvas_PrivateData()
|
|
@@ -456,7 +462,8 @@ class QwtPlotCanvas(QFrame):
|
|
|
456
462
|
"""
|
|
457
463
|
:return: Parent plot widget
|
|
458
464
|
"""
|
|
459
|
-
|
|
465
|
+
ref = self.__plot
|
|
466
|
+
return None if ref is None else ref()
|
|
460
467
|
|
|
461
468
|
def setPaintAttribute(self, attribute, on=True):
|
|
462
469
|
"""
|
|
@@ -695,7 +702,9 @@ class QwtPlotCanvas(QFrame):
|
|
|
695
702
|
else:
|
|
696
703
|
# print('**DEBUG: QwtPlotCanvas.drawCanvas')
|
|
697
704
|
painter.setClipRect(self.contentsRect(), Qt.IntersectClip)
|
|
698
|
-
self.plot()
|
|
705
|
+
plot = self.plot()
|
|
706
|
+
if plot is not None:
|
|
707
|
+
plot.drawCanvas(painter)
|
|
699
708
|
painter.restore()
|
|
700
709
|
if withBackground and hackStyledBackground:
|
|
701
710
|
# Now paint the border on top
|
|
@@ -24,6 +24,7 @@ from datetime import datetime
|
|
|
24
24
|
|
|
25
25
|
from qtpy.QtCore import (
|
|
26
26
|
QLineF,
|
|
27
|
+
QObject,
|
|
27
28
|
QPoint,
|
|
28
29
|
QPointF,
|
|
29
30
|
QRect,
|
|
@@ -50,22 +51,11 @@ _ALIGN_TOP = int(Qt.AlignTop)
|
|
|
50
51
|
_ALIGN_BOTTOM = int(Qt.AlignBottom)
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
class QwtAbstractScaleDraw_PrivateData(
|
|
54
|
-
#
|
|
55
|
-
# base class' ``__init__`` is a measurable cost in tick-heavy renders.
|
|
56
|
-
__slots__ = (
|
|
57
|
-
"spacing",
|
|
58
|
-
"penWidth",
|
|
59
|
-
"minExtent",
|
|
60
|
-
"components",
|
|
61
|
-
"tick_length",
|
|
62
|
-
"tick_lighter_factor",
|
|
63
|
-
"map",
|
|
64
|
-
"scaleDiv",
|
|
65
|
-
"labelCache",
|
|
66
|
-
)
|
|
54
|
+
class QwtAbstractScaleDraw_PrivateData(QObject):
|
|
55
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
67
56
|
|
|
68
57
|
def __init__(self):
|
|
58
|
+
QObject.__init__(self)
|
|
69
59
|
self.spacing = 4
|
|
70
60
|
self.penWidth = 0
|
|
71
61
|
self.minExtent = 0.0
|
|
@@ -492,20 +482,11 @@ class QwtAbstractScaleDraw(object):
|
|
|
492
482
|
self.__data.labelCache.clear()
|
|
493
483
|
|
|
494
484
|
|
|
495
|
-
class QwtScaleDraw_PrivateData(
|
|
496
|
-
#
|
|
497
|
-
# base class' ``__init__`` is a measurable cost in tick-heavy renders.
|
|
498
|
-
__slots__ = (
|
|
499
|
-
"len",
|
|
500
|
-
"alignment",
|
|
501
|
-
"orientation",
|
|
502
|
-
"labelAlignment",
|
|
503
|
-
"labelRotation",
|
|
504
|
-
"labelAutoSize",
|
|
505
|
-
"pos",
|
|
506
|
-
)
|
|
485
|
+
class QwtScaleDraw_PrivateData(QObject):
|
|
486
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
507
487
|
|
|
508
488
|
def __init__(self):
|
|
489
|
+
QObject.__init__(self)
|
|
509
490
|
self.len = 0
|
|
510
491
|
self.alignment = QwtScaleDraw.BottomScale
|
|
511
492
|
# Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment``
|
|
@@ -0,0 +1,72 @@
|
|
|
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 107: GDI handle exhaustion on Windows.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
from qtpy import QtCore as QC
|
|
13
|
+
from qtpy import QtWidgets as QW
|
|
14
|
+
|
|
15
|
+
from qwt import QwtPlot, QwtPlotCurve
|
|
16
|
+
from qwt.tests.utils import TestEnvironment
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_stress_cycle():
|
|
20
|
+
"""Run one cycle of plot creation, rendering and destruction."""
|
|
21
|
+
plot = QwtPlot()
|
|
22
|
+
plot.resize(QC.QSize(800, 600))
|
|
23
|
+
plot.show()
|
|
24
|
+
|
|
25
|
+
# Create many curves with different fonts (via titles) to pressure caches
|
|
26
|
+
for i in range(20):
|
|
27
|
+
curve = QwtPlotCurve(f"Curve {i}")
|
|
28
|
+
x = np.linspace(0, 10, 100)
|
|
29
|
+
y = np.sin(x + i / 10.0)
|
|
30
|
+
curve.setData(x, y)
|
|
31
|
+
curve.attach(plot)
|
|
32
|
+
|
|
33
|
+
plot.replot()
|
|
34
|
+
QW.QApplication.processEvents()
|
|
35
|
+
|
|
36
|
+
plot.close()
|
|
37
|
+
plot.deleteLater()
|
|
38
|
+
QW.QApplication.processEvents()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_gdi_leak_stability():
|
|
42
|
+
"""
|
|
43
|
+
Repeatedly create/render/destroy plots to check for GDI leak stability.
|
|
44
|
+
On Windows, without the fix, this would crash after a certain number of cycles
|
|
45
|
+
due to GDI handle exhaustion.
|
|
46
|
+
"""
|
|
47
|
+
env = TestEnvironment()
|
|
48
|
+
if not env.unattended:
|
|
49
|
+
pytest.skip("This test is for CI/unattended mode only")
|
|
50
|
+
|
|
51
|
+
# Close any top-level widgets left alive by previous tests in the shared
|
|
52
|
+
# QApplication. This test aggressively drives ``deleteLater()`` +
|
|
53
|
+
# ``processEvents()`` in-process; processing deferred deletions while stale
|
|
54
|
+
# widgets from earlier tests are still pending triggers a Qt teardown
|
|
55
|
+
# fast-fail (0xC0000409) on Windows. Starting from a clean widget set makes
|
|
56
|
+
# the test robust inside the full suite (it already passes in isolation).
|
|
57
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
58
|
+
for widget in list(app.topLevelWidgets()):
|
|
59
|
+
widget.close()
|
|
60
|
+
widget.deleteLater()
|
|
61
|
+
app.processEvents()
|
|
62
|
+
app.sendPostedEvents(None, QC.QEvent.DeferredDelete)
|
|
63
|
+
app.processEvents()
|
|
64
|
+
|
|
65
|
+
n_cycles = 50
|
|
66
|
+
for i in range(n_cycles):
|
|
67
|
+
run_stress_cycle()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
72
|
+
test_gdi_leak_stability()
|
|
@@ -46,6 +46,7 @@ QwtRichTextEngine
|
|
|
46
46
|
import math
|
|
47
47
|
import os
|
|
48
48
|
import struct
|
|
49
|
+
from collections import OrderedDict
|
|
49
50
|
|
|
50
51
|
from qtpy.QtCore import QObject, QRectF, QSize, QSizeF, Qt
|
|
51
52
|
from qtpy.QtGui import (
|
|
@@ -208,66 +209,50 @@ class QwtTextEngine(object):
|
|
|
208
209
|
pass
|
|
209
210
|
|
|
210
211
|
|
|
211
|
-
ASCENTCACHE =
|
|
212
|
+
ASCENTCACHE = OrderedDict()
|
|
212
213
|
|
|
213
|
-
#
|
|
214
|
-
# ``tuple_key -> tuple_key`` (slow path). The tuple key is built from a
|
|
215
|
-
# handful of QFont attributes that uniquely identify the *logical* font for
|
|
216
|
-
# metrics purposes. Tick-rendering uses very few distinct fonts in practice
|
|
217
|
-
# so both dicts stay tiny.
|
|
218
|
-
#
|
|
219
|
-
# This replaces the previous ``id(font) -> font.key()`` design. Two reasons:
|
|
220
|
-
#
|
|
221
|
-
# 1. ``QFont.key()`` is a sip dispatch that costs ~3.3 us/call on PyQt5 and
|
|
222
|
-
# ~9.3 us/call on PyQt6 -- it became the single biggest residual hotspot
|
|
223
|
-
# in ``QwtText.textSize`` on PyQt6.
|
|
224
|
-
# 2. PyQt6 returns a fresh Python wrapper around the same QFont on most
|
|
225
|
-
# calls, so ``id(font)`` changes between calls and the id-keyed fast path
|
|
226
|
-
# misses ~92% of the time. The tuple-key second level recovers the hits
|
|
227
|
-
# those misses would have produced, without paying for ``font.key()``.
|
|
228
|
-
#
|
|
229
|
-
# The tuple key uses ``(family, pixelSize-or-pointSizeF, weight, italic,
|
|
230
|
-
# stretch, styleStrategy)``. This is what determines ``QFontMetrics`` output
|
|
231
|
-
# in practice; if two QFonts share these values they share metrics.
|
|
214
|
+
_FM_CACHE_LIMIT = 256 # max QFontMetrics / QFontMetricsF / ascent entries
|
|
232
215
|
|
|
233
|
-
_FONT_KEY_CACHE: dict = {} # id(font) -> tuple_key (fast path)
|
|
234
|
-
_FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts
|
|
235
|
-
# as the "have we seen this logical font" set)
|
|
236
|
-
_FONT_KEY_CACHE_LIMIT = 1024
|
|
237
216
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
217
|
+
# Single-slot, leak-free memo for ``QFont.key()``.
|
|
218
|
+
#
|
|
219
|
+
# ``QFont.key()`` is the only correct, binding-stable identity for metrics
|
|
220
|
+
# caching, but it is a relatively costly sip call (~1 us on PyQt5, ~1.6 us on
|
|
221
|
+
# PyQt6) and it is queried once per tick label. The previous implementation
|
|
222
|
+
# memoized it in an ``id(font) -> (font, key)`` dict which kept a *strong
|
|
223
|
+
# reference* to every distinct QFont wrapper it saw (up to 1024). That is a
|
|
224
|
+
# genuine memory / GDI-handle leak that the bounded ``.clear()`` only masked
|
|
225
|
+
# (see the issue #93 segfault regression).
|
|
226
|
+
#
|
|
227
|
+
# This single-slot memo keeps at most ONE QFont alive (bounded => not a leak)
|
|
228
|
+
# while still skipping the repeated ``font.key()`` call for the very common
|
|
229
|
+
# case of the same painter font being reused across an entire scale-draw pass.
|
|
230
|
+
# Retaining that one font is precisely what makes the ``id()`` comparison safe
|
|
231
|
+
# against id reuse. On a miss the real ``font.key()`` is recomputed, so the
|
|
232
|
+
# result is always lossless and correct.
|
|
233
|
+
_LAST_FONT = None
|
|
234
|
+
_LAST_FONT_ID = None
|
|
235
|
+
_LAST_FONT_KEY = None
|
|
250
236
|
|
|
251
237
|
|
|
252
238
|
def font_key_cached(font):
|
|
253
|
-
"""Return
|
|
239
|
+
"""Return ``font.key()`` using a single-slot, leak-free memo.
|
|
254
240
|
|
|
255
|
-
The returned value is
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
The returned value is the genuine ``QFont.key()`` string and is safe to use
|
|
242
|
+
as a dict key for metrics caches. At most one ``QFont`` is ever retained.
|
|
243
|
+
|
|
244
|
+
:param QFont font: Font
|
|
245
|
+
:return: ``font.key()``
|
|
258
246
|
"""
|
|
247
|
+
global _LAST_FONT, _LAST_FONT_ID, _LAST_FONT_KEY
|
|
259
248
|
fid = id(font)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT:
|
|
268
|
-
_FONT_KEY_CACHE.clear()
|
|
269
|
-
_FONT_KEY_CACHE[fid] = (font, interned)
|
|
270
|
-
return interned
|
|
249
|
+
if fid == _LAST_FONT_ID:
|
|
250
|
+
return _LAST_FONT_KEY
|
|
251
|
+
key = font.key()
|
|
252
|
+
_LAST_FONT = font
|
|
253
|
+
_LAST_FONT_ID = fid
|
|
254
|
+
_LAST_FONT_KEY = key
|
|
255
|
+
return key
|
|
271
256
|
|
|
272
257
|
|
|
273
258
|
def get_screen_resolution():
|
|
@@ -304,27 +289,31 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
304
289
|
|
|
305
290
|
def __init__(self):
|
|
306
291
|
self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
|
|
307
|
-
self._fm_cache =
|
|
308
|
-
self._fm_cache_f =
|
|
309
|
-
self._margins_cache =
|
|
310
|
-
# Fast path: when textMargins is called repeatedly with the same
|
|
311
|
-
# QFont instance, skip the (expensive) font.key() Qt call.
|
|
312
|
-
self._margins_last_id = -1
|
|
313
|
-
self._margins_last_value = None
|
|
292
|
+
self._fm_cache = OrderedDict()
|
|
293
|
+
self._fm_cache_f = OrderedDict()
|
|
294
|
+
self._margins_cache = OrderedDict()
|
|
314
295
|
|
|
315
296
|
def fontmetrics(self, font):
|
|
316
297
|
fid = font_key_cached(font)
|
|
317
298
|
try:
|
|
318
299
|
return self._fm_cache[fid]
|
|
319
300
|
except KeyError:
|
|
320
|
-
|
|
301
|
+
if len(self._fm_cache) >= _FM_CACHE_LIMIT:
|
|
302
|
+
self._fm_cache.popitem(last=False)
|
|
303
|
+
fm = QFontMetrics(font)
|
|
304
|
+
self._fm_cache[fid] = fm
|
|
305
|
+
return fm
|
|
321
306
|
|
|
322
307
|
def fontmetrics_f(self, font):
|
|
323
308
|
fid = font_key_cached(font)
|
|
324
309
|
try:
|
|
325
310
|
return self._fm_cache_f[fid]
|
|
326
311
|
except KeyError:
|
|
327
|
-
|
|
312
|
+
if len(self._fm_cache_f) >= _FM_CACHE_LIMIT:
|
|
313
|
+
self._fm_cache_f.popitem(last=False)
|
|
314
|
+
fm = QFontMetricsF(font)
|
|
315
|
+
self._fm_cache_f[fid] = fm
|
|
316
|
+
return fm
|
|
328
317
|
|
|
329
318
|
def heightForWidth(self, font, flags, text, width):
|
|
330
319
|
"""
|
|
@@ -354,12 +343,15 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
354
343
|
return rect.size()
|
|
355
344
|
|
|
356
345
|
def effectiveAscent(self, font):
|
|
357
|
-
global ASCENTCACHE
|
|
358
346
|
fontKey = font_key_cached(font)
|
|
359
347
|
ascent = ASCENTCACHE.get(fontKey)
|
|
360
348
|
if ascent is not None:
|
|
361
349
|
return ascent
|
|
362
|
-
|
|
350
|
+
if len(ASCENTCACHE) >= _FM_CACHE_LIMIT:
|
|
351
|
+
ASCENTCACHE.popitem(last=False)
|
|
352
|
+
ascent = self.findAscent(font)
|
|
353
|
+
ASCENTCACHE[fontKey] = ascent
|
|
354
|
+
return ascent
|
|
363
355
|
|
|
364
356
|
def findAscent(self, font):
|
|
365
357
|
dummy = "E"
|
|
@@ -402,18 +394,14 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
402
394
|
:param QFont font: Font of the text
|
|
403
395
|
:return: tuple (left, right, top, bottom) representing margins
|
|
404
396
|
"""
|
|
405
|
-
# Fast path: same QFont object as the previous call.
|
|
406
|
-
font_id = id(font)
|
|
407
|
-
if font_id == self._margins_last_id:
|
|
408
|
-
return self._margins_last_value
|
|
409
397
|
fkey = font_key_cached(font)
|
|
410
398
|
cached = self._margins_cache.get(fkey)
|
|
411
399
|
if cached is None:
|
|
412
400
|
fm = self.fontmetrics(font)
|
|
413
401
|
cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
|
|
402
|
+
if len(self._margins_cache) >= _FM_CACHE_LIMIT:
|
|
403
|
+
self._margins_cache.popitem(last=False)
|
|
414
404
|
self._margins_cache[fkey] = cached
|
|
415
|
-
self._margins_last_id = font_id
|
|
416
|
-
self._margins_last_value = cached
|
|
417
405
|
return cached
|
|
418
406
|
|
|
419
407
|
def draw(self, painter, rect, flags, text):
|
|
@@ -557,26 +545,11 @@ class QwtRichTextEngine(QwtTextEngine):
|
|
|
557
545
|
return 0, 0, 0, 0
|
|
558
546
|
|
|
559
547
|
|
|
560
|
-
class QwtText_PrivateData(
|
|
561
|
-
#
|
|
562
|
-
# or events are ever emitted from ``_PrivateData`` containers and the
|
|
563
|
-
# ``QObject.__init__`` call dominates ``QwtText.__init__`` (it is the
|
|
564
|
-
# single most expensive line for tick-label-heavy renders, see
|
|
565
|
-
# https://github.com/PlotPyStack/PythonQwt/issues/93).
|
|
566
|
-
__slots__ = (
|
|
567
|
-
"renderFlags",
|
|
568
|
-
"borderRadius",
|
|
569
|
-
"borderPen",
|
|
570
|
-
"backgroundBrush",
|
|
571
|
-
"paintAttributes",
|
|
572
|
-
"layoutAttributes",
|
|
573
|
-
"textEngine",
|
|
574
|
-
"text",
|
|
575
|
-
"font",
|
|
576
|
-
"color",
|
|
577
|
-
)
|
|
548
|
+
class QwtText_PrivateData(QObject):
|
|
549
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
578
550
|
|
|
579
551
|
def __init__(self):
|
|
552
|
+
QObject.__init__(self)
|
|
580
553
|
self.renderFlags = Qt.AlignCenter
|
|
581
554
|
self.borderRadius = 0
|
|
582
555
|
self.borderPen = Qt.NoPen
|
|
@@ -594,12 +567,20 @@ class QwtText_LayoutCache(object):
|
|
|
594
567
|
def __init__(self):
|
|
595
568
|
self.textSize = None
|
|
596
569
|
self.fontKey = None
|
|
570
|
+
# ``id(font)`` fast path for ``QwtText.textSize``. ``font`` keeps a
|
|
571
|
+
# strong reference to the cached font so its id cannot be reused by a
|
|
572
|
+
# different object while cached (guards against a stale-size false hit).
|
|
573
|
+
# This is a per-instance, single-slot cache: it retains at most one
|
|
574
|
+
# QFont per QwtText and is released with the QwtText, so unlike the old
|
|
575
|
+
# unbounded module-level font cache it cannot leak.
|
|
597
576
|
self.fontId = -1
|
|
577
|
+
self.font = None
|
|
598
578
|
|
|
599
579
|
def invalidate(self):
|
|
600
580
|
self.textSize = None
|
|
601
581
|
self.fontKey = None
|
|
602
582
|
self.fontId = -1
|
|
583
|
+
self.font = None
|
|
603
584
|
|
|
604
585
|
|
|
605
586
|
class QwtText(object):
|
|
@@ -1114,6 +1095,10 @@ class QwtText(object):
|
|
|
1114
1095
|
"""
|
|
1115
1096
|
font = self.usedFont(defaultFont)
|
|
1116
1097
|
cache = self.__layoutCache
|
|
1098
|
+
# Fast path: same font *object* as the previous call on this text.
|
|
1099
|
+
# ``cache.font`` keeps that font alive, so the id comparison is safe
|
|
1100
|
+
# against id reuse, and we skip the (relatively costly) ``font.key()``
|
|
1101
|
+
# call performed by ``font_key_cached``.
|
|
1117
1102
|
font_id = id(font)
|
|
1118
1103
|
if cache.textSize is not None and cache.fontId == font_id:
|
|
1119
1104
|
sz = QSizeF(cache.textSize)
|
|
@@ -1129,6 +1114,7 @@ class QwtText(object):
|
|
|
1129
1114
|
)
|
|
1130
1115
|
cache.fontKey = fkey
|
|
1131
1116
|
cache.fontId = font_id
|
|
1117
|
+
cache.font = font
|
|
1132
1118
|
sz = QSizeF(cache.textSize)
|
|
1133
1119
|
if self.__data.layoutAttributes & self.MinimumLayout:
|
|
1134
1120
|
(left, right, top, bottom) = self.__data.textEngine.textMargins(font)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|