backend_qt5.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. import functools
  2. import os
  3. import re
  4. import signal
  5. import sys
  6. import traceback
  7. import matplotlib
  8. from matplotlib import backend_tools, cbook
  9. from matplotlib._pylab_helpers import Gcf
  10. from matplotlib.backend_bases import (
  11. _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
  12. TimerBase, cursors, ToolContainerBase, StatusbarBase, MouseButton)
  13. import matplotlib.backends.qt_editor.figureoptions as figureoptions
  14. from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool
  15. from matplotlib.backend_managers import ToolManager
  16. from .qt_compat import (
  17. QtCore, QtGui, QtWidgets, _isdeleted, _getSaveFileName,
  18. is_pyqt5, __version__, QT_API)
  19. backend_version = __version__
  20. # SPECIAL_KEYS are keys that do *not* return their unicode name
  21. # instead they have manually specified names
  22. SPECIAL_KEYS = {QtCore.Qt.Key_Control: 'control',
  23. QtCore.Qt.Key_Shift: 'shift',
  24. QtCore.Qt.Key_Alt: 'alt',
  25. QtCore.Qt.Key_Meta: 'super',
  26. QtCore.Qt.Key_Return: 'enter',
  27. QtCore.Qt.Key_Left: 'left',
  28. QtCore.Qt.Key_Up: 'up',
  29. QtCore.Qt.Key_Right: 'right',
  30. QtCore.Qt.Key_Down: 'down',
  31. QtCore.Qt.Key_Escape: 'escape',
  32. QtCore.Qt.Key_F1: 'f1',
  33. QtCore.Qt.Key_F2: 'f2',
  34. QtCore.Qt.Key_F3: 'f3',
  35. QtCore.Qt.Key_F4: 'f4',
  36. QtCore.Qt.Key_F5: 'f5',
  37. QtCore.Qt.Key_F6: 'f6',
  38. QtCore.Qt.Key_F7: 'f7',
  39. QtCore.Qt.Key_F8: 'f8',
  40. QtCore.Qt.Key_F9: 'f9',
  41. QtCore.Qt.Key_F10: 'f10',
  42. QtCore.Qt.Key_F11: 'f11',
  43. QtCore.Qt.Key_F12: 'f12',
  44. QtCore.Qt.Key_Home: 'home',
  45. QtCore.Qt.Key_End: 'end',
  46. QtCore.Qt.Key_PageUp: 'pageup',
  47. QtCore.Qt.Key_PageDown: 'pagedown',
  48. QtCore.Qt.Key_Tab: 'tab',
  49. QtCore.Qt.Key_Backspace: 'backspace',
  50. QtCore.Qt.Key_Enter: 'enter',
  51. QtCore.Qt.Key_Insert: 'insert',
  52. QtCore.Qt.Key_Delete: 'delete',
  53. QtCore.Qt.Key_Pause: 'pause',
  54. QtCore.Qt.Key_SysReq: 'sysreq',
  55. QtCore.Qt.Key_Clear: 'clear', }
  56. # define which modifier keys are collected on keyboard events.
  57. # elements are (Matplotlib modifier names, Modifier Flag, Qt Key) tuples
  58. SUPER = 0
  59. ALT = 1
  60. CTRL = 2
  61. SHIFT = 3
  62. MODIFIER_KEYS = [('super', QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta),
  63. ('alt', QtCore.Qt.AltModifier, QtCore.Qt.Key_Alt),
  64. ('ctrl', QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control),
  65. ('shift', QtCore.Qt.ShiftModifier, QtCore.Qt.Key_Shift),
  66. ]
  67. if sys.platform == 'darwin':
  68. # in OSX, the control and super (aka cmd/apple) keys are switched, so
  69. # switch them back.
  70. SPECIAL_KEYS.update({QtCore.Qt.Key_Control: 'cmd', # cmd/apple key
  71. QtCore.Qt.Key_Meta: 'control',
  72. })
  73. MODIFIER_KEYS[0] = ('cmd', QtCore.Qt.ControlModifier,
  74. QtCore.Qt.Key_Control)
  75. MODIFIER_KEYS[2] = ('ctrl', QtCore.Qt.MetaModifier,
  76. QtCore.Qt.Key_Meta)
  77. cursord = {
  78. cursors.MOVE: QtCore.Qt.SizeAllCursor,
  79. cursors.HAND: QtCore.Qt.PointingHandCursor,
  80. cursors.POINTER: QtCore.Qt.ArrowCursor,
  81. cursors.SELECT_REGION: QtCore.Qt.CrossCursor,
  82. cursors.WAIT: QtCore.Qt.WaitCursor,
  83. }
  84. # make place holder
  85. qApp = None
  86. def _create_qApp():
  87. """
  88. Only one qApp can exist at a time, so check before creating one.
  89. """
  90. global qApp
  91. if qApp is None:
  92. app = QtWidgets.QApplication.instance()
  93. if app is None:
  94. # check for DISPLAY env variable on X11 build of Qt
  95. if is_pyqt5():
  96. try:
  97. from PyQt5 import QtX11Extras
  98. is_x11_build = True
  99. except ImportError:
  100. is_x11_build = False
  101. else:
  102. is_x11_build = hasattr(QtGui, "QX11Info")
  103. if is_x11_build:
  104. display = os.environ.get('DISPLAY')
  105. if display is None or not re.search(r':\d', display):
  106. raise RuntimeError('Invalid DISPLAY variable')
  107. try:
  108. QtWidgets.QApplication.setAttribute(
  109. QtCore.Qt.AA_EnableHighDpiScaling)
  110. except AttributeError: # Attribute only exists for Qt>=5.6.
  111. pass
  112. qApp = QtWidgets.QApplication(["matplotlib"])
  113. qApp.lastWindowClosed.connect(qApp.quit)
  114. else:
  115. qApp = app
  116. if is_pyqt5():
  117. try:
  118. qApp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
  119. except AttributeError:
  120. pass
  121. def _allow_super_init(__init__):
  122. """
  123. Decorator for ``__init__`` to allow ``super().__init__`` on PyQt4/PySide2.
  124. """
  125. if QT_API == "PyQt5":
  126. return __init__
  127. else:
  128. # To work around lack of cooperative inheritance in PyQt4, PySide,
  129. # and PySide2, when calling FigureCanvasQT.__init__, we temporarily
  130. # patch QWidget.__init__ by a cooperative version, that first calls
  131. # QWidget.__init__ with no additional arguments, and then finds the
  132. # next class in the MRO with an __init__ that does support cooperative
  133. # inheritance (i.e., not defined by the PyQt4, PySide, PySide2, sip
  134. # or Shiboken packages), and manually call its `__init__`, once again
  135. # passing the additional arguments.
  136. qwidget_init = QtWidgets.QWidget.__init__
  137. def cooperative_qwidget_init(self, *args, **kwargs):
  138. qwidget_init(self)
  139. mro = type(self).__mro__
  140. next_coop_init = next(
  141. cls for cls in mro[mro.index(QtWidgets.QWidget) + 1:]
  142. if cls.__module__.split(".")[0] not in [
  143. "PyQt4", "sip", "PySide", "PySide2", "Shiboken"])
  144. next_coop_init.__init__(self, *args, **kwargs)
  145. @functools.wraps(__init__)
  146. def wrapper(self, *args, **kwargs):
  147. with cbook._setattr_cm(QtWidgets.QWidget,
  148. __init__=cooperative_qwidget_init):
  149. __init__(self, *args, **kwargs)
  150. return wrapper
  151. class TimerQT(TimerBase):
  152. """
  153. Subclass of `.TimerBase` that uses Qt timer events.
  154. Attributes
  155. ----------
  156. interval : int
  157. The time between timer events in milliseconds. Default is 1000 ms.
  158. single_shot : bool
  159. Boolean flag indicating whether this timer should
  160. operate as single shot (run once and then stop). Defaults to False.
  161. callbacks : list
  162. Stores list of (func, args) tuples that will be called upon timer
  163. events. This list can be manipulated directly, or the functions
  164. `add_callback` and `remove_callback` can be used.
  165. """
  166. def __init__(self, *args, **kwargs):
  167. TimerBase.__init__(self, *args, **kwargs)
  168. # Create a new timer and connect the timeout() signal to the
  169. # _on_timer method.
  170. self._timer = QtCore.QTimer()
  171. self._timer.timeout.connect(self._on_timer)
  172. self._timer_set_interval()
  173. def __del__(self):
  174. # The check for deletedness is needed to avoid an error at animation
  175. # shutdown with PySide2.
  176. if not _isdeleted(self._timer):
  177. self._timer_stop()
  178. def _timer_set_single_shot(self):
  179. self._timer.setSingleShot(self._single)
  180. def _timer_set_interval(self):
  181. self._timer.setInterval(self._interval)
  182. def _timer_start(self):
  183. self._timer.start()
  184. def _timer_stop(self):
  185. self._timer.stop()
  186. class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase):
  187. required_interactive_framework = "qt5"
  188. # map Qt button codes to MouseEvent's ones:
  189. buttond = {QtCore.Qt.LeftButton: MouseButton.LEFT,
  190. QtCore.Qt.MidButton: MouseButton.MIDDLE,
  191. QtCore.Qt.RightButton: MouseButton.RIGHT,
  192. QtCore.Qt.XButton1: MouseButton.BACK,
  193. QtCore.Qt.XButton2: MouseButton.FORWARD,
  194. }
  195. @_allow_super_init
  196. def __init__(self, figure):
  197. _create_qApp()
  198. super().__init__(figure=figure)
  199. self.figure = figure
  200. # We don't want to scale up the figure DPI more than once.
  201. # Note, we don't handle a signal for changing DPI yet.
  202. figure._original_dpi = figure.dpi
  203. self._update_figure_dpi()
  204. # In cases with mixed resolution displays, we need to be careful if the
  205. # dpi_ratio changes - in this case we need to resize the canvas
  206. # accordingly. We could watch for screenChanged events from Qt, but
  207. # the issue is that we can't guarantee this will be emitted *before*
  208. # the first paintEvent for the canvas, so instead we keep track of the
  209. # dpi_ratio value here and in paintEvent we resize the canvas if
  210. # needed.
  211. self._dpi_ratio_prev = None
  212. self._draw_pending = False
  213. self._is_drawing = False
  214. self._draw_rect_callback = lambda painter: None
  215. self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
  216. self.setMouseTracking(True)
  217. self.resize(*self.get_width_height())
  218. palette = QtGui.QPalette(QtCore.Qt.white)
  219. self.setPalette(palette)
  220. def _update_figure_dpi(self):
  221. dpi = self._dpi_ratio * self.figure._original_dpi
  222. self.figure._set_dpi(dpi, forward=False)
  223. @property
  224. def _dpi_ratio(self):
  225. # Not available on Qt4 or some older Qt5.
  226. try:
  227. # self.devicePixelRatio() returns 0 in rare cases
  228. return self.devicePixelRatio() or 1
  229. except AttributeError:
  230. return 1
  231. def _update_dpi(self):
  232. # As described in __init__ above, we need to be careful in cases with
  233. # mixed resolution displays if dpi_ratio is changing between painting
  234. # events.
  235. # Return whether we triggered a resizeEvent (and thus a paintEvent)
  236. # from within this function.
  237. if self._dpi_ratio != self._dpi_ratio_prev:
  238. # We need to update the figure DPI.
  239. self._update_figure_dpi()
  240. self._dpi_ratio_prev = self._dpi_ratio
  241. # The easiest way to resize the canvas is to emit a resizeEvent
  242. # since we implement all the logic for resizing the canvas for
  243. # that event.
  244. event = QtGui.QResizeEvent(self.size(), self.size())
  245. self.resizeEvent(event)
  246. # resizeEvent triggers a paintEvent itself, so we exit this one
  247. # (after making sure that the event is immediately handled).
  248. return True
  249. return False
  250. def get_width_height(self):
  251. w, h = FigureCanvasBase.get_width_height(self)
  252. return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
  253. def enterEvent(self, event):
  254. try:
  255. x, y = self.mouseEventCoords(event.pos())
  256. except AttributeError:
  257. # the event from PyQt4 does not include the position
  258. x = y = None
  259. FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
  260. def leaveEvent(self, event):
  261. QtWidgets.QApplication.restoreOverrideCursor()
  262. FigureCanvasBase.leave_notify_event(self, guiEvent=event)
  263. def mouseEventCoords(self, pos):
  264. """Calculate mouse coordinates in physical pixels
  265. Qt5 use logical pixels, but the figure is scaled to physical
  266. pixels for rendering. Transform to physical pixels so that
  267. all of the down-stream transforms work as expected.
  268. Also, the origin is different and needs to be corrected.
  269. """
  270. dpi_ratio = self._dpi_ratio
  271. x = pos.x()
  272. # flip y so y=0 is bottom of canvas
  273. y = self.figure.bbox.height / dpi_ratio - pos.y()
  274. return x * dpi_ratio, y * dpi_ratio
  275. def mousePressEvent(self, event):
  276. x, y = self.mouseEventCoords(event.pos())
  277. button = self.buttond.get(event.button())
  278. if button is not None:
  279. FigureCanvasBase.button_press_event(self, x, y, button,
  280. guiEvent=event)
  281. def mouseDoubleClickEvent(self, event):
  282. x, y = self.mouseEventCoords(event.pos())
  283. button = self.buttond.get(event.button())
  284. if button is not None:
  285. FigureCanvasBase.button_press_event(self, x, y,
  286. button, dblclick=True,
  287. guiEvent=event)
  288. def mouseMoveEvent(self, event):
  289. x, y = self.mouseEventCoords(event)
  290. FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
  291. def mouseReleaseEvent(self, event):
  292. x, y = self.mouseEventCoords(event)
  293. button = self.buttond.get(event.button())
  294. if button is not None:
  295. FigureCanvasBase.button_release_event(self, x, y, button,
  296. guiEvent=event)
  297. if is_pyqt5():
  298. def wheelEvent(self, event):
  299. x, y = self.mouseEventCoords(event)
  300. # from QWheelEvent::delta doc
  301. if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0:
  302. steps = event.angleDelta().y() / 120
  303. else:
  304. steps = event.pixelDelta().y()
  305. if steps:
  306. FigureCanvasBase.scroll_event(
  307. self, x, y, steps, guiEvent=event)
  308. else:
  309. def wheelEvent(self, event):
  310. x = event.x()
  311. # flipy so y=0 is bottom of canvas
  312. y = self.figure.bbox.height - event.y()
  313. # from QWheelEvent::delta doc
  314. steps = event.delta() / 120
  315. if event.orientation() == QtCore.Qt.Vertical:
  316. FigureCanvasBase.scroll_event(
  317. self, x, y, steps, guiEvent=event)
  318. def keyPressEvent(self, event):
  319. key = self._get_key(event)
  320. if key is not None:
  321. FigureCanvasBase.key_press_event(self, key, guiEvent=event)
  322. def keyReleaseEvent(self, event):
  323. key = self._get_key(event)
  324. if key is not None:
  325. FigureCanvasBase.key_release_event(self, key, guiEvent=event)
  326. def resizeEvent(self, event):
  327. # _dpi_ratio_prev will be set the first time the canvas is painted, and
  328. # the rendered buffer is useless before anyways.
  329. if self._dpi_ratio_prev is None:
  330. return
  331. w = event.size().width() * self._dpi_ratio
  332. h = event.size().height() * self._dpi_ratio
  333. dpival = self.figure.dpi
  334. winch = w / dpival
  335. hinch = h / dpival
  336. self.figure.set_size_inches(winch, hinch, forward=False)
  337. # pass back into Qt to let it finish
  338. QtWidgets.QWidget.resizeEvent(self, event)
  339. # emit our resize events
  340. FigureCanvasBase.resize_event(self)
  341. def sizeHint(self):
  342. w, h = self.get_width_height()
  343. return QtCore.QSize(w, h)
  344. def minumumSizeHint(self):
  345. return QtCore.QSize(10, 10)
  346. def _get_key(self, event):
  347. event_key = event.key()
  348. event_mods = int(event.modifiers()) # actually a bitmask
  349. # get names of the pressed modifier keys
  350. # bit twiddling to pick out modifier keys from event_mods bitmask,
  351. # if event_key is a MODIFIER, it should not be duplicated in mods
  352. mods = [name for name, mod_key, qt_key in MODIFIER_KEYS
  353. if event_key != qt_key and (event_mods & mod_key) == mod_key]
  354. try:
  355. # for certain keys (enter, left, backspace, etc) use a word for the
  356. # key, rather than unicode
  357. key = SPECIAL_KEYS[event_key]
  358. except KeyError:
  359. # unicode defines code points up to 0x0010ffff
  360. # QT will use Key_Codes larger than that for keyboard keys that are
  361. # are not unicode characters (like multimedia keys)
  362. # skip these
  363. # if you really want them, you should add them to SPECIAL_KEYS
  364. MAX_UNICODE = 0x10ffff
  365. if event_key > MAX_UNICODE:
  366. return None
  367. key = chr(event_key)
  368. # qt delivers capitalized letters. fix capitalization
  369. # note that capslock is ignored
  370. if 'shift' in mods:
  371. mods.remove('shift')
  372. else:
  373. key = key.lower()
  374. mods.reverse()
  375. return '+'.join(mods + [key])
  376. def new_timer(self, *args, **kwargs):
  377. # docstring inherited
  378. return TimerQT(*args, **kwargs)
  379. def flush_events(self):
  380. # docstring inherited
  381. qApp.processEvents()
  382. def start_event_loop(self, timeout=0):
  383. # docstring inherited
  384. if hasattr(self, "_event_loop") and self._event_loop.isRunning():
  385. raise RuntimeError("Event loop already running")
  386. self._event_loop = event_loop = QtCore.QEventLoop()
  387. if timeout:
  388. timer = QtCore.QTimer.singleShot(timeout * 1000, event_loop.quit)
  389. event_loop.exec_()
  390. def stop_event_loop(self, event=None):
  391. # docstring inherited
  392. if hasattr(self, "_event_loop"):
  393. self._event_loop.quit()
  394. def draw(self):
  395. """Render the figure, and queue a request for a Qt draw.
  396. """
  397. # The renderer draw is done here; delaying causes problems with code
  398. # that uses the result of the draw() to update plot elements.
  399. if self._is_drawing:
  400. return
  401. with cbook._setattr_cm(self, _is_drawing=True):
  402. super().draw()
  403. self.update()
  404. def draw_idle(self):
  405. """Queue redraw of the Agg buffer and request Qt paintEvent.
  406. """
  407. # The Agg draw needs to be handled by the same thread matplotlib
  408. # modifies the scene graph from. Post Agg draw request to the
  409. # current event loop in order to ensure thread affinity and to
  410. # accumulate multiple draw requests from event handling.
  411. # TODO: queued signal connection might be safer than singleShot
  412. if not (getattr(self, '_draw_pending', False) or
  413. getattr(self, '_is_drawing', False)):
  414. self._draw_pending = True
  415. QtCore.QTimer.singleShot(0, self._draw_idle)
  416. def _draw_idle(self):
  417. with self._idle_draw_cntx():
  418. if not self._draw_pending:
  419. return
  420. self._draw_pending = False
  421. if self.height() < 0 or self.width() < 0:
  422. return
  423. try:
  424. self.draw()
  425. except Exception:
  426. # Uncaught exceptions are fatal for PyQt5, so catch them.
  427. traceback.print_exc()
  428. def drawRectangle(self, rect):
  429. # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
  430. # to be called at the end of paintEvent.
  431. if rect is not None:
  432. def _draw_rect_callback(painter):
  433. pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio,
  434. QtCore.Qt.DotLine)
  435. painter.setPen(pen)
  436. painter.drawRect(*(pt / self._dpi_ratio for pt in rect))
  437. else:
  438. def _draw_rect_callback(painter):
  439. return
  440. self._draw_rect_callback = _draw_rect_callback
  441. self.update()
  442. class MainWindow(QtWidgets.QMainWindow):
  443. closing = QtCore.Signal()
  444. def closeEvent(self, event):
  445. self.closing.emit()
  446. QtWidgets.QMainWindow.closeEvent(self, event)
  447. class FigureManagerQT(FigureManagerBase):
  448. """
  449. Attributes
  450. ----------
  451. canvas : `FigureCanvas`
  452. The FigureCanvas instance
  453. num : int or str
  454. The Figure number
  455. toolbar : qt.QToolBar
  456. The qt.QToolBar
  457. window : qt.QMainWindow
  458. The qt.QMainWindow
  459. """
  460. def __init__(self, canvas, num):
  461. FigureManagerBase.__init__(self, canvas, num)
  462. self.canvas = canvas
  463. self.window = MainWindow()
  464. self.window.closing.connect(canvas.close_event)
  465. self.window.closing.connect(self._widgetclosed)
  466. self.window.setWindowTitle("Figure %d" % num)
  467. image = str(cbook._get_data_path('images/matplotlib.svg'))
  468. self.window.setWindowIcon(QtGui.QIcon(image))
  469. # Give the keyboard focus to the figure instead of the
  470. # manager; StrongFocus accepts both tab and click to focus and
  471. # will enable the canvas to process event w/o clicking.
  472. # ClickFocus only takes the focus is the window has been
  473. # clicked
  474. # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or
  475. # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum
  476. self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus)
  477. self.canvas.setFocus()
  478. self.window._destroying = False
  479. self.toolmanager = self._get_toolmanager()
  480. self.toolbar = self._get_toolbar(self.canvas, self.window)
  481. self.statusbar = None
  482. if self.toolmanager:
  483. backend_tools.add_tools_to_manager(self.toolmanager)
  484. if self.toolbar:
  485. backend_tools.add_tools_to_container(self.toolbar)
  486. self.statusbar = StatusbarQt(self.window, self.toolmanager)
  487. if self.toolbar is not None:
  488. self.window.addToolBar(self.toolbar)
  489. if not self.toolmanager:
  490. # add text label to status bar
  491. statusbar_label = QtWidgets.QLabel()
  492. self.window.statusBar().addWidget(statusbar_label)
  493. self.toolbar.message.connect(statusbar_label.setText)
  494. tbs_height = self.toolbar.sizeHint().height()
  495. else:
  496. tbs_height = 0
  497. # resize the main window so it will display the canvas with the
  498. # requested size:
  499. cs = canvas.sizeHint()
  500. sbs = self.window.statusBar().sizeHint()
  501. height = cs.height() + tbs_height + sbs.height()
  502. self.window.resize(cs.width(), height)
  503. self.window.setCentralWidget(self.canvas)
  504. if matplotlib.is_interactive():
  505. self.window.show()
  506. self.canvas.draw_idle()
  507. self.window.raise_()
  508. def full_screen_toggle(self):
  509. if self.window.isFullScreen():
  510. self.window.showNormal()
  511. else:
  512. self.window.showFullScreen()
  513. def _widgetclosed(self):
  514. if self.window._destroying:
  515. return
  516. self.window._destroying = True
  517. try:
  518. Gcf.destroy(self.num)
  519. except AttributeError:
  520. pass
  521. # It seems that when the python session is killed,
  522. # Gcf can get destroyed before the Gcf.destroy
  523. # line is run, leading to a useless AttributeError.
  524. def _get_toolbar(self, canvas, parent):
  525. # must be inited after the window, drawingArea and figure
  526. # attrs are set
  527. if matplotlib.rcParams['toolbar'] == 'toolbar2':
  528. toolbar = NavigationToolbar2QT(canvas, parent, False)
  529. elif matplotlib.rcParams['toolbar'] == 'toolmanager':
  530. toolbar = ToolbarQt(self.toolmanager, self.window)
  531. else:
  532. toolbar = None
  533. return toolbar
  534. def _get_toolmanager(self):
  535. if matplotlib.rcParams['toolbar'] == 'toolmanager':
  536. toolmanager = ToolManager(self.canvas.figure)
  537. else:
  538. toolmanager = None
  539. return toolmanager
  540. def resize(self, width, height):
  541. # these are Qt methods so they return sizes in 'virtual' pixels
  542. # so we do not need to worry about dpi scaling here.
  543. extra_width = self.window.width() - self.canvas.width()
  544. extra_height = self.window.height() - self.canvas.height()
  545. self.canvas.resize(width, height)
  546. self.window.resize(width + extra_width, height + extra_height)
  547. def show(self):
  548. self.window.show()
  549. self.window.activateWindow()
  550. self.window.raise_()
  551. def destroy(self, *args):
  552. # check for qApp first, as PySide deletes it in its atexit handler
  553. if QtWidgets.QApplication.instance() is None:
  554. return
  555. if self.window._destroying:
  556. return
  557. self.window._destroying = True
  558. if self.toolbar:
  559. self.toolbar.destroy()
  560. self.window.close()
  561. def get_window_title(self):
  562. return self.window.windowTitle()
  563. def set_window_title(self, title):
  564. self.window.setWindowTitle(title)
  565. class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar):
  566. message = QtCore.Signal(str)
  567. def __init__(self, canvas, parent, coordinates=True):
  568. """coordinates: should we show the coordinates on the right?"""
  569. self.canvas = canvas
  570. self.parent = parent
  571. self.coordinates = coordinates
  572. self._actions = {}
  573. """A mapping of toolitem method names to their QActions"""
  574. QtWidgets.QToolBar.__init__(self, parent)
  575. NavigationToolbar2.__init__(self, canvas)
  576. def _icon(self, name, color=None):
  577. if is_pyqt5():
  578. name = name.replace('.png', '_large.png')
  579. pm = QtGui.QPixmap(os.path.join(self.basedir, name))
  580. if hasattr(pm, 'setDevicePixelRatio'):
  581. pm.setDevicePixelRatio(self.canvas._dpi_ratio)
  582. if color is not None:
  583. mask = pm.createMaskFromColor(QtGui.QColor('black'),
  584. QtCore.Qt.MaskOutColor)
  585. pm.fill(color)
  586. pm.setMask(mask)
  587. return QtGui.QIcon(pm)
  588. def _init_toolbar(self):
  589. self.basedir = str(cbook._get_data_path('images'))
  590. background_color = self.palette().color(self.backgroundRole())
  591. foreground_color = self.palette().color(self.foregroundRole())
  592. icon_color = (foreground_color
  593. if background_color.value() < 128 else None)
  594. for text, tooltip_text, image_file, callback in self.toolitems:
  595. if text is None:
  596. self.addSeparator()
  597. else:
  598. a = self.addAction(self._icon(image_file + '.png', icon_color),
  599. text, getattr(self, callback))
  600. self._actions[callback] = a
  601. if callback in ['zoom', 'pan']:
  602. a.setCheckable(True)
  603. if tooltip_text is not None:
  604. a.setToolTip(tooltip_text)
  605. if text == 'Subplots':
  606. a = self.addAction(self._icon("qt4_editor_options.png",
  607. icon_color),
  608. 'Customize', self.edit_parameters)
  609. a.setToolTip('Edit axis, curve and image parameters')
  610. # Add the (x, y) location widget at the right side of the toolbar
  611. # The stretch factor is 1 which means any resizing of the toolbar
  612. # will resize this label instead of the buttons.
  613. if self.coordinates:
  614. self.locLabel = QtWidgets.QLabel("", self)
  615. self.locLabel.setAlignment(
  616. QtCore.Qt.AlignRight | QtCore.Qt.AlignTop)
  617. self.locLabel.setSizePolicy(
  618. QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
  619. QtWidgets.QSizePolicy.Ignored))
  620. labelAction = self.addWidget(self.locLabel)
  621. labelAction.setVisible(True)
  622. @cbook.deprecated("3.1")
  623. @property
  624. def buttons(self):
  625. return {}
  626. @cbook.deprecated("3.1")
  627. @property
  628. def adj_window(self):
  629. return None
  630. def edit_parameters(self):
  631. axes = self.canvas.figure.get_axes()
  632. if not axes:
  633. QtWidgets.QMessageBox.warning(
  634. self.parent, "Error", "There are no axes to edit.")
  635. return
  636. elif len(axes) == 1:
  637. ax, = axes
  638. else:
  639. titles = [
  640. ax.get_label() or
  641. ax.get_title() or
  642. " - ".join(filter(None, [ax.get_xlabel(), ax.get_ylabel()])) or
  643. f"<anonymous {type(ax).__name__}>"
  644. for ax in axes]
  645. duplicate_titles = [
  646. title for title in titles if titles.count(title) > 1]
  647. for i, ax in enumerate(axes):
  648. if titles[i] in duplicate_titles:
  649. titles[i] += f" (id: {id(ax):#x})" # Deduplicate titles.
  650. item, ok = QtWidgets.QInputDialog.getItem(
  651. self.parent, 'Customize', 'Select axes:', titles, 0, False)
  652. if not ok:
  653. return
  654. ax = axes[titles.index(item)]
  655. figureoptions.figure_edit(ax, self)
  656. def _update_buttons_checked(self):
  657. # sync button checkstates to match active mode
  658. if 'pan' in self._actions:
  659. self._actions['pan'].setChecked(self._active == 'PAN')
  660. if 'zoom' in self._actions:
  661. self._actions['zoom'].setChecked(self._active == 'ZOOM')
  662. def pan(self, *args):
  663. super().pan(*args)
  664. self._update_buttons_checked()
  665. def zoom(self, *args):
  666. super().zoom(*args)
  667. self._update_buttons_checked()
  668. def set_message(self, s):
  669. self.message.emit(s)
  670. if self.coordinates:
  671. self.locLabel.setText(s)
  672. def set_cursor(self, cursor):
  673. self.canvas.setCursor(cursord[cursor])
  674. def draw_rubberband(self, event, x0, y0, x1, y1):
  675. height = self.canvas.figure.bbox.height
  676. y1 = height - y1
  677. y0 = height - y0
  678. rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
  679. self.canvas.drawRectangle(rect)
  680. def remove_rubberband(self):
  681. self.canvas.drawRectangle(None)
  682. def configure_subplots(self):
  683. image = str(cbook._get_data_path('images/matplotlib.png'))
  684. dia = SubplotToolQt(self.canvas.figure, self.canvas.parent())
  685. dia.setWindowIcon(QtGui.QIcon(image))
  686. dia.exec_()
  687. def save_figure(self, *args):
  688. filetypes = self.canvas.get_supported_filetypes_grouped()
  689. sorted_filetypes = sorted(filetypes.items())
  690. default_filetype = self.canvas.get_default_filetype()
  691. startpath = os.path.expanduser(
  692. matplotlib.rcParams['savefig.directory'])
  693. start = os.path.join(startpath, self.canvas.get_default_filename())
  694. filters = []
  695. selectedFilter = None
  696. for name, exts in sorted_filetypes:
  697. exts_list = " ".join(['*.%s' % ext for ext in exts])
  698. filter = '%s (%s)' % (name, exts_list)
  699. if default_filetype in exts:
  700. selectedFilter = filter
  701. filters.append(filter)
  702. filters = ';;'.join(filters)
  703. fname, filter = _getSaveFileName(self.canvas.parent(),
  704. "Choose a filename to save to",
  705. start, filters, selectedFilter)
  706. if fname:
  707. # Save dir for next time, unless empty str (i.e., use cwd).
  708. if startpath != "":
  709. matplotlib.rcParams['savefig.directory'] = (
  710. os.path.dirname(fname))
  711. try:
  712. self.canvas.figure.savefig(fname)
  713. except Exception as e:
  714. QtWidgets.QMessageBox.critical(
  715. self, "Error saving file", str(e),
  716. QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton)
  717. def set_history_buttons(self):
  718. can_backward = self._nav_stack._pos > 0
  719. can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
  720. if 'back' in self._actions:
  721. self._actions['back'].setEnabled(can_backward)
  722. if 'forward' in self._actions:
  723. self._actions['forward'].setEnabled(can_forward)
  724. class SubplotToolQt(UiSubplotTool):
  725. def __init__(self, targetfig, parent):
  726. UiSubplotTool.__init__(self, None)
  727. self._figure = targetfig
  728. for lower, higher in [("bottom", "top"), ("left", "right")]:
  729. self._widgets[lower].valueChanged.connect(
  730. lambda val: self._widgets[higher].setMinimum(val + .001))
  731. self._widgets[higher].valueChanged.connect(
  732. lambda val: self._widgets[lower].setMaximum(val - .001))
  733. self._attrs = ["top", "bottom", "left", "right", "hspace", "wspace"]
  734. self._defaults = {attr: vars(self._figure.subplotpars)[attr]
  735. for attr in self._attrs}
  736. # Set values after setting the range callbacks, but before setting up
  737. # the redraw callbacks.
  738. self._reset()
  739. for attr in self._attrs:
  740. self._widgets[attr].valueChanged.connect(self._on_value_changed)
  741. for action, method in [("Export values", self._export_values),
  742. ("Tight layout", self._tight_layout),
  743. ("Reset", self._reset),
  744. ("Close", self.close)]:
  745. self._widgets[action].clicked.connect(method)
  746. def _export_values(self):
  747. # Explicitly round to 3 decimals (which is also the spinbox precision)
  748. # to avoid numbers of the form 0.100...001.
  749. dialog = QtWidgets.QDialog()
  750. layout = QtWidgets.QVBoxLayout()
  751. dialog.setLayout(layout)
  752. text = QtWidgets.QPlainTextEdit()
  753. text.setReadOnly(True)
  754. layout.addWidget(text)
  755. text.setPlainText(
  756. ",\n".join("{}={:.3}".format(attr, self._widgets[attr].value())
  757. for attr in self._attrs))
  758. # Adjust the height of the text widget to fit the whole text, plus
  759. # some padding.
  760. size = text.maximumSize()
  761. size.setHeight(
  762. QtGui.QFontMetrics(text.document().defaultFont())
  763. .size(0, text.toPlainText()).height() + 20)
  764. text.setMaximumSize(size)
  765. dialog.exec_()
  766. def _on_value_changed(self):
  767. self._figure.subplots_adjust(**{attr: self._widgets[attr].value()
  768. for attr in self._attrs})
  769. self._figure.canvas.draw_idle()
  770. def _tight_layout(self):
  771. self._figure.tight_layout()
  772. for attr in self._attrs:
  773. widget = self._widgets[attr]
  774. widget.blockSignals(True)
  775. widget.setValue(vars(self._figure.subplotpars)[attr])
  776. widget.blockSignals(False)
  777. self._figure.canvas.draw_idle()
  778. def _reset(self):
  779. for attr, value in self._defaults.items():
  780. self._widgets[attr].setValue(value)
  781. class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar):
  782. def __init__(self, toolmanager, parent):
  783. ToolContainerBase.__init__(self, toolmanager)
  784. QtWidgets.QToolBar.__init__(self, parent)
  785. self._toolitems = {}
  786. self._groups = {}
  787. @property
  788. def _icon_extension(self):
  789. if is_pyqt5():
  790. return '_large.png'
  791. return '.png'
  792. def add_toolitem(
  793. self, name, group, position, image_file, description, toggle):
  794. button = QtWidgets.QToolButton(self)
  795. button.setIcon(self._icon(image_file))
  796. button.setText(name)
  797. if description:
  798. button.setToolTip(description)
  799. def handler():
  800. self.trigger_tool(name)
  801. if toggle:
  802. button.setCheckable(True)
  803. button.toggled.connect(handler)
  804. else:
  805. button.clicked.connect(handler)
  806. self._toolitems.setdefault(name, [])
  807. self._add_to_group(group, name, button, position)
  808. self._toolitems[name].append((button, handler))
  809. def _add_to_group(self, group, name, button, position):
  810. gr = self._groups.get(group, [])
  811. if not gr:
  812. sep = self.addSeparator()
  813. gr.append(sep)
  814. before = gr[position]
  815. widget = self.insertWidget(before, button)
  816. gr.insert(position, widget)
  817. self._groups[group] = gr
  818. def _icon(self, name):
  819. pm = QtGui.QPixmap(name)
  820. if hasattr(pm, 'setDevicePixelRatio'):
  821. pm.setDevicePixelRatio(self.toolmanager.canvas._dpi_ratio)
  822. return QtGui.QIcon(pm)
  823. def toggle_toolitem(self, name, toggled):
  824. if name not in self._toolitems:
  825. return
  826. for button, handler in self._toolitems[name]:
  827. button.toggled.disconnect(handler)
  828. button.setChecked(toggled)
  829. button.toggled.connect(handler)
  830. def remove_toolitem(self, name):
  831. for button, handler in self._toolitems[name]:
  832. button.setParent(None)
  833. del self._toolitems[name]
  834. class StatusbarQt(StatusbarBase, QtWidgets.QLabel):
  835. def __init__(self, window, *args, **kwargs):
  836. StatusbarBase.__init__(self, *args, **kwargs)
  837. QtWidgets.QLabel.__init__(self)
  838. window.statusBar().addWidget(self)
  839. def set_message(self, s):
  840. self.setText(s)
  841. class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase):
  842. def trigger(self, *args):
  843. NavigationToolbar2QT.configure_subplots(
  844. self._make_classic_style_pseudo_toolbar())
  845. class SaveFigureQt(backend_tools.SaveFigureBase):
  846. def trigger(self, *args):
  847. NavigationToolbar2QT.save_figure(
  848. self._make_classic_style_pseudo_toolbar())
  849. class SetCursorQt(backend_tools.SetCursorBase):
  850. def set_cursor(self, cursor):
  851. NavigationToolbar2QT.set_cursor(
  852. self._make_classic_style_pseudo_toolbar(), cursor)
  853. class RubberbandQt(backend_tools.RubberbandBase):
  854. def draw_rubberband(self, x0, y0, x1, y1):
  855. NavigationToolbar2QT.draw_rubberband(
  856. self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
  857. def remove_rubberband(self):
  858. NavigationToolbar2QT.remove_rubberband(
  859. self._make_classic_style_pseudo_toolbar())
  860. class HelpQt(backend_tools.ToolHelpBase):
  861. def trigger(self, *args):
  862. QtWidgets.QMessageBox.information(None, "Help", self._get_help_html())
  863. class ToolCopyToClipboardQT(backend_tools.ToolCopyToClipboardBase):
  864. def trigger(self, *args, **kwargs):
  865. pixmap = self.canvas.grab()
  866. qApp.clipboard().setPixmap(pixmap)
  867. backend_tools.ToolSaveFigure = SaveFigureQt
  868. backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt
  869. backend_tools.ToolSetCursor = SetCursorQt
  870. backend_tools.ToolRubberband = RubberbandQt
  871. backend_tools.ToolHelp = HelpQt
  872. backend_tools.ToolCopyToClipboard = ToolCopyToClipboardQT
  873. @_Backend.export
  874. class _BackendQT5(_Backend):
  875. FigureCanvas = FigureCanvasQT
  876. FigureManager = FigureManagerQT
  877. @staticmethod
  878. def trigger_manager_draw(manager):
  879. manager.canvas.draw_idle()
  880. @staticmethod
  881. def mainloop():
  882. old_signal = signal.getsignal(signal.SIGINT)
  883. # allow SIGINT exceptions to close the plot window.
  884. is_python_signal_handler = old_signal is not None
  885. if is_python_signal_handler:
  886. signal.signal(signal.SIGINT, signal.SIG_DFL)
  887. try:
  888. qApp.exec_()
  889. finally:
  890. # reset the SIGINT exception handler
  891. if is_python_signal_handler:
  892. signal.signal(signal.SIGINT, old_signal)