Skip to content

Commit 5373929

Browse files
authored
Merge pull request #2015 from kernc/fix-webengine-qt5
[FIX] Highcharts: Fix freezing on Qt5
2 parents 682a939 + 9ce670f commit 5373929

File tree

4 files changed

+65
-26
lines changed

4 files changed

+65
-26
lines changed

Orange/widgets/_highcharts/orange-selection.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function unselectAllPoints(e) {
3737
e.target.parentElement.tagName.toLowerCase() == 'svg'))
3838
return true;
3939
this.deselectPointsIfNot(false);
40-
_highcharts_bridge.on_selected_points([]);
40+
pybridge._highcharts_on_selected_points([]);
4141
}
4242

4343
function clickedPointSelect(e) {
@@ -49,7 +49,7 @@ function clickedPointSelect(e) {
4949
selected.splice(selected.indexOf(this.index), 1);
5050
} else
5151
points[this.series.index].push(this.index);
52-
_highcharts_bridge.on_selected_points(points);
52+
pybridge._highcharts_on_selected_points(points);
5353
return true;
5454
}
5555

@@ -83,6 +83,6 @@ function rectSelectPoints(e) {
8383
}
8484
}
8585

86-
_highcharts_bridge.on_selected_points(this.getSelectedPointsForExport());
86+
pybridge._highcharts_on_selected_points(this.getSelectedPointsForExport());
8787
return false; // Don't zoom
8888
}

Orange/widgets/highcharts.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,25 +123,52 @@ def __init__(self,
123123
if enable_select and not selection_callback:
124124
raise ValueError('enable_select requires selection_callback')
125125

126+
if enable_select:
127+
# We need to make sure the _Bridge object below with the selection
128+
# callback is exposed in JS via QWebChannel.registerObject() and
129+
# not through WebviewWidget.exposeObject() as the latter mechanism
130+
# doesn't transmit QObjects correctly.
131+
class _Bridge(QObject):
132+
@pyqtSlot('QVariantList')
133+
def _highcharts_on_selected_points(self, points):
134+
selection_callback([np.sort(selected).astype(int)
135+
for selected in points])
136+
if bridge is None:
137+
bridge = _Bridge()
138+
else:
139+
# Thus, we patch existing user-passed bridge with our
140+
# selection callback method
141+
attrs = bridge.__dict__.copy()
142+
attrs['_highcharts_on_selected_points'] = _Bridge._highcharts_on_selected_points
143+
assert isinstance(bridge, QObject), 'bridge needs to be a QObject'
144+
_Bridge = type(bridge.__class__.__name__,
145+
bridge.__class__.__mro__,
146+
attrs)
147+
bridge = _Bridge()
148+
126149
super().__init__(parent, bridge, debug=debug)
127150

128151
self.highchart = highchart
129152
self.enable_zoom = enable_zoom
130153
enable_point_select = '+' in enable_select
131154
enable_rect_select = enable_select.replace('+', '')
155+
156+
self._update_options_dict(options, enable_zoom, enable_select,
157+
enable_point_select, enable_rect_select,
158+
kwargs)
159+
160+
with open(self._HIGHCHARTS_HTML) as html:
161+
self.setHtml(html.read() % dict(javascript=javascript,
162+
options=json(options)),
163+
self.toFileURL(dirname(self._HIGHCHARTS_HTML)) + '/')
164+
165+
def _update_options_dict(self, options, enable_zoom, enable_select,
166+
enable_point_select, enable_rect_select, kwargs):
132167
if enable_zoom:
133168
_merge_dicts(options, _kwargs_options(dict(
134169
mapNavigation_enableMouseWheelZoom=True,
135170
mapNavigation_enableButtons=False)))
136171
if enable_select:
137-
138-
class _Bridge(QObject):
139-
@pyqtSlot('QVariantList')
140-
def on_selected_points(self, points):
141-
selection_callback([np.sort(selected).astype(int)
142-
for selected in points])
143-
144-
self.exposeObject('_highcharts_bridge', _Bridge())
145172
_merge_dicts(options, _kwargs_options(dict(
146173
chart_events_click='/**/unselectAllPoints/**/')))
147174
if enable_point_select:
@@ -155,11 +182,6 @@ def on_selected_points(self, points):
155182
if kwargs:
156183
_merge_dicts(options, _kwargs_options(kwargs))
157184

158-
with open(self._HIGHCHARTS_HTML) as html:
159-
self.setHtml(html.read() % dict(javascript=javascript,
160-
options=json(options)),
161-
self.toFileURL(dirname(self._HIGHCHARTS_HTML)) + '/')
162-
163185
def contextMenuEvent(self, event):
164186
""" Zoom out on right click. Also disable context menu."""
165187
if self.enable_zoom:
@@ -232,7 +254,7 @@ def svg(self):
232254

233255
def main():
234256
""" A simple test. """
235-
from AnyQt.QtGui import QApplication
257+
from AnyQt.QtWidgets import QApplication
236258
app = QApplication([])
237259

238260
def _on_selected_points(points):
@@ -251,4 +273,3 @@ def _on_selected_points(points):
251273

252274
if __name__ == '__main__':
253275
main()
254-

Orange/widgets/tests/test_highcharts.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import time
22
import os
3+
import sys
34
import unittest
45

5-
from AnyQt.QtCore import Qt, QPoint
6+
from AnyQt.QtCore import Qt, QPoint, QObject
67
from AnyQt.QtWidgets import qApp
78
from AnyQt.QtTest import QTest
89

@@ -12,23 +13,34 @@
1213

1314

1415
class SelectionScatter(Highchart):
15-
def __init__(self, selected_indices_callback):
16-
super().__init__(enable_select='xy+',
16+
def __init__(self, bridge, selected_indices_callback):
17+
super().__init__(bridge=bridge,
18+
enable_select='xy+',
1719
selection_callback=selected_indices_callback,
1820
options=dict(chart=dict(type='scatter')))
1921

2022

2123
class HighchartTest(WidgetTest):
2224
@unittest.skipIf(os.environ.get('APPVEYOR'), 'test stalls on AppVeyor')
25+
@unittest.skipIf(sys.version_info[:2] <= (3, 4),
26+
'the second iteration stalls on Travis / Py3.4')
2327
def test_selection(self):
28+
29+
class NoopBridge(QObject):
30+
pass
31+
32+
for bridge in (NoopBridge(), None):
33+
self._selection_test(bridge)
34+
35+
def _selection_test(self, bridge):
2436
data = Table('iris')
2537
selected_indices = []
2638

2739
def selection_callback(indices):
2840
nonlocal selected_indices
2941
selected_indices = indices
3042

31-
scatter = SelectionScatter(selection_callback)
43+
scatter = SelectionScatter(bridge, selection_callback)
3244
scatter.chart(options=dict(series=[dict(data=data.X[:, :2])]))
3345
scatter.show()
3446

@@ -65,5 +77,6 @@ def selection_callback(indices):
6577

6678
self.assertFalse(len(selected_indices))
6779

80+
# Test Esc hiding
6881
QTest.keyClick(scatter, Qt.Key_Escape)
6982
self.assertTrue(scatter.isHidden())

Orange/widgets/utils/webview.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
(extends QWebView), as available.
66
"""
77
import os
8+
from os.path import join, dirname, abspath
89
import warnings
910
from random import random
1011
from collections.abc import Mapping, Set, Sequence, Iterable
@@ -13,7 +14,6 @@
1314

1415
from urllib.parse import urljoin
1516
from urllib.request import pathname2url
16-
from os.path import join, dirname, abspath
1717

1818
import numpy as np
1919

@@ -60,7 +60,7 @@ def hideWindow(self):
6060
while isinstance(w, QWidget):
6161
if w.windowFlags() & (Qt.Window | Qt.Dialog):
6262
return w.hide()
63-
w = w.parent()
63+
w = w.parent() if callable(w.parent) else w.parent
6464

6565

6666
if HAVE_WEBENGINE:
@@ -92,7 +92,7 @@ def __init__(self, parent=None, bridge=None, *, debug=False, **kwargs):
9292
warnings.warn(
9393
'To debug QWebEngineView, set environment variable '
9494
'QTWEBENGINE_REMOTE_DEBUGGING={port} and then visit '
95-
'http://localhost:{port}/ in a Chromium-based browser. '
95+
'http://127.0.0.1:{port}/ in a Chromium-based browser. '
9696
'See https://doc.qt.io/qt-5/qtwebengine-debugging.html '
9797
'This has also been done for you.'.format(port=port))
9898
super().__init__(parent,
@@ -113,7 +113,7 @@ def __init__(self, parent=None, bridge=None, *, debug=False, **kwargs):
113113
with open(_WEBENGINE_INIT_WEBCHANNEL, encoding="utf-8") as f:
114114
init_webchannel_src = f.read()
115115
self._onloadJS(source + init_webchannel_src %
116-
dict(exposeObject_prefix=self._EXPOSED_OBJ_PREFIX),
116+
dict(exposeObject_prefix=self._EXPOSED_OBJ_PREFIX),
117117
name='webchannel_init',
118118
injection_point=QWebEngineScript.DocumentCreation)
119119
else:
@@ -456,6 +456,11 @@ def __init__(self, parent):
456456
self._objects = {}
457457

458458
def send_object(self, name, obj):
459+
if isinstance(obj, QObject):
460+
raise ValueError(
461+
"QWebChannel doesn't transmit QObject instances. If you "
462+
"need a QObject available in JavaScript, pass it as a "
463+
"bridge in WebviewWidget constructor.")
459464
id = next(self._id_gen)
460465
value = self._objects[id] = dict(id=id, name=name, obj=obj)
461466
# Wait till JS is connected to receive objects

0 commit comments

Comments
 (0)