11import functools
22import importlib
3+ import operator
34import os
45import signal
56import sys
1516from matplotlib .backends .qt_editor ._formsubplottool import UiSubplotTool
1617from . import qt_compat
1718from .qt_compat import (
18- QtCore , QtGui , QtWidgets , __version__ , QT_API ,
19+ QtCore , QtGui , QtWidgets , Qt , __version__ , QT_API ,
1920 _devicePixelRatioF , _isdeleted , _setDevicePixelRatio ,
2021)
2122
2223backend_version = __version__
2324
24- # SPECIAL_KEYS are keys that do *not* return their unicode name
25- # instead they have manually specified names
26- SPECIAL_KEYS = {QtCore .Qt .Key_Control : 'control' ,
27- QtCore .Qt .Key_Shift : 'shift' ,
28- QtCore .Qt .Key_Alt : 'alt' ,
29- QtCore .Qt .Key_Meta : 'super' ,
30- QtCore .Qt .Key_Return : 'enter' ,
31- QtCore .Qt .Key_Left : 'left' ,
32- QtCore .Qt .Key_Up : 'up' ,
33- QtCore .Qt .Key_Right : 'right' ,
34- QtCore .Qt .Key_Down : 'down' ,
35- QtCore .Qt .Key_Escape : 'escape' ,
36- QtCore .Qt .Key_F1 : 'f1' ,
37- QtCore .Qt .Key_F2 : 'f2' ,
38- QtCore .Qt .Key_F3 : 'f3' ,
39- QtCore .Qt .Key_F4 : 'f4' ,
40- QtCore .Qt .Key_F5 : 'f5' ,
41- QtCore .Qt .Key_F6 : 'f6' ,
42- QtCore .Qt .Key_F7 : 'f7' ,
43- QtCore .Qt .Key_F8 : 'f8' ,
44- QtCore .Qt .Key_F9 : 'f9' ,
45- QtCore .Qt .Key_F10 : 'f10' ,
46- QtCore .Qt .Key_F11 : 'f11' ,
47- QtCore .Qt .Key_F12 : 'f12' ,
48- QtCore .Qt .Key_Home : 'home' ,
49- QtCore .Qt .Key_End : 'end' ,
50- QtCore .Qt .Key_PageUp : 'pageup' ,
51- QtCore .Qt .Key_PageDown : 'pagedown' ,
52- QtCore .Qt .Key_Tab : 'tab' ,
53- QtCore .Qt .Key_Backspace : 'backspace' ,
54- QtCore .Qt .Key_Enter : 'enter' ,
55- QtCore .Qt .Key_Insert : 'insert' ,
56- QtCore .Qt .Key_Delete : 'delete' ,
57- QtCore .Qt .Key_Pause : 'pause' ,
58- QtCore .Qt .Key_SysReq : 'sysreq' ,
59- QtCore .Qt .Key_Clear : 'clear' , }
25+ # Enums are specified using numeric values because 1) they are only available
26+ # as enum attributes on PyQt6 and only available as Qt attributes on PyQt<5.11;
27+ # 2) Foos = QFlags<Foo> is exported as Qt.Foos on PyQt6 but Qt.Foo on PyQt5.
28+
29+ # SPECIAL_KEYS are Qt::Key that do *not* return their unicode name
30+ # instead they have manually specified names.
31+ SPECIAL_KEYS = {
32+ 0x1000000 : 'escape' ,
33+ 0x1000001 : 'tab' ,
34+ 0x1000003 : 'backspace' ,
35+ 0x1000004 : 'enter' ,
36+ 0x1000005 : 'enter' ,
37+ 0x1000006 : 'insert' ,
38+ 0x1000007 : 'delete' ,
39+ 0x1000008 : 'pause' ,
40+ 0x100000a : 'sysreq' ,
41+ 0x100000b : 'clear' ,
42+ 0x1000010 : 'home' ,
43+ 0x1000011 : 'end' ,
44+ 0x1000012 : 'left' ,
45+ 0x1000013 : 'up' ,
46+ 0x1000014 : 'right' ,
47+ 0x1000015 : 'down' ,
48+ 0x1000016 : 'pageup' ,
49+ 0x1000017 : 'pagedown' ,
50+ 0x1000020 : 'shift' ,
51+ 0x1000021 : 'control' ,
52+ 0x1000022 : 'super' ,
53+ 0x1000023 : 'alt' ,
54+ 0x1000030 : 'f1' ,
55+ 0x1000031 : 'f2' ,
56+ 0x1000032 : 'f3' ,
57+ 0x1000033 : 'f4' ,
58+ 0x1000034 : 'f5' ,
59+ 0x1000035 : 'f6' ,
60+ 0x1000036 : 'f7' ,
61+ 0x1000037 : 'f8' ,
62+ 0x1000038 : 'f9' ,
63+ 0x1000039 : 'f10' ,
64+ 0x100003a : 'f11' ,
65+ 0x100003b : 'f12' ,
66+ }
6067if sys .platform == 'darwin' :
6168 # in OSX, the control and super (aka cmd/apple) keys are switched, so
6269 # switch them back.
63- SPECIAL_KEYS .update ({QtCore .Qt .Key_Control : 'cmd' , # cmd/apple key
64- QtCore .Qt .Key_Meta : 'control' ,
65- })
70+ SPECIAL_KEYS .update ({
71+ 0x01000021 : 'cmd' , # cmd/apple key
72+ 0x01000022 : 'control' ,
73+ })
6674# Define which modifier keys are collected on keyboard events.
67- # Elements are (Modifier Flag , Qt Key) tuples.
75+ # Elements are (Qt::KeyboardModifier(s) , Qt:: Key) tuples.
6876# Order determines the modifier order (ctrl+alt+...) reported by Matplotlib.
6977_MODIFIER_KEYS = [
70- (QtCore . Qt . ShiftModifier , QtCore . Qt . Key_Shift ),
71- (QtCore . Qt . ControlModifier , QtCore . Qt . Key_Control ),
72- (QtCore . Qt . AltModifier , QtCore . Qt . Key_Alt ),
73- (QtCore . Qt . MetaModifier , QtCore . Qt . Key_Meta ),
78+ (0x02000000 , 0x01000020 ), # shift
79+ (0x04000000 , 0x01000021 ), # control
80+ (0x08000000 , 0x01000023 ), # alt
81+ (0x10000000 , 0x01000022 ), # meta
7482]
7583cursord = {
76- cursors .MOVE : QtCore . Qt .SizeAllCursor ,
77- cursors .HAND : QtCore . Qt .PointingHandCursor ,
78- cursors .POINTER : QtCore . Qt .ArrowCursor ,
79- cursors .SELECT_REGION : QtCore . Qt .CrossCursor ,
80- cursors .WAIT : QtCore . Qt .WaitCursor ,
81- }
84+ cursors .MOVE : Qt .CursorShape ( 9 ), # SizeAllCursor
85+ cursors .HAND : Qt .CursorShape ( 13 ), # PointingHandCursor
86+ cursors .POINTER : Qt .CursorShape ( 0 ), # ArrowCursor
87+ cursors .SELECT_REGION : Qt .CursorShape ( 2 ), # CrossCursor
88+ cursors .WAIT : Qt .CursorShape ( 3 ), # WaitCursor
89+ }
8290SUPER = 0 # Deprecated.
8391ALT = 1 # Deprecated.
8492CTRL = 2 # Deprecated.
8795 (SPECIAL_KEYS [key ], mod , key ) for mod , key in _MODIFIER_KEYS ]
8896
8997
98+ def _to_int (x ):
99+ return x .value if QT_API == "PyQt6" else int (x )
100+
101+
90102# make place holder
91103qApp = None
92104
@@ -140,17 +152,17 @@ def _allow_super_init(__init__):
140152 Decorator for ``__init__`` to allow ``super().__init__`` on PyQt4/PySide2.
141153 """
142154
143- if QT_API == "PyQt5" :
155+ if QT_API in [ "PyQt5" , "PyQt6" ] :
144156
145157 return __init__
146158
147159 else :
148- # To work around lack of cooperative inheritance in PyQt4, PySide,
149- # and PySide2 , when calling FigureCanvasQT.__init__, we temporarily
160+ # To work around lack of cooperative inheritance in PyQt4 and
161+ # PySide{,2,6} , when calling FigureCanvasQT.__init__, we temporarily
150162 # patch QWidget.__init__ by a cooperative version, that first calls
151163 # QWidget.__init__ with no additional arguments, and then finds the
152164 # next class in the MRO with an __init__ that does support cooperative
153- # inheritance (i.e., not defined by the PyQt4, PySide, PySide2, sip
165+ # inheritance (i.e., not defined by the PyQt4 or sip, or PySide{,2,6}
154166 # or Shiboken packages), and manually call its `__init__`, once again
155167 # passing the additional arguments.
156168
@@ -162,7 +174,9 @@ def cooperative_qwidget_init(self, *args, **kwargs):
162174 next_coop_init = next (
163175 cls for cls in mro [mro .index (QtWidgets .QWidget ) + 1 :]
164176 if cls .__module__ .split ("." )[0 ] not in [
165- "PyQt4" , "sip" , "PySide" , "PySide2" , "Shiboken" ])
177+ "PyQt4" , "sip" ,
178+ "PySide" , "PySide2" , "PySide6" , "Shiboken" ,
179+ ])
166180 next_coop_init .__init__ (self , * args , ** kwargs )
167181
168182 @functools .wraps (__init__ )
@@ -207,13 +221,13 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase):
207221 required_interactive_framework = "qt5"
208222 _timer_cls = TimerQT
209223
210- # map Qt button codes to MouseEvent's ones:
211- buttond = { QtCore . Qt . LeftButton : MouseButton .LEFT ,
212- QtCore . Qt . MidButton : MouseButton .MIDDLE ,
213- QtCore . Qt . RightButton : MouseButton .RIGHT ,
214- QtCore . Qt . XButton1 : MouseButton .BACK ,
215- QtCore . Qt . XButton2 : MouseButton .FORWARD ,
216- }
224+ buttond = { # Map Qt::MouseButton(s) to MouseEvents.
225+ 0x01 : MouseButton .LEFT ,
226+ 0x02 : MouseButton .RIGHT ,
227+ 0x04 : MouseButton .MIDDLE ,
228+ 0x08 : MouseButton .BACK ,
229+ 0x10 : MouseButton .FORWARD ,
230+ }
217231
218232 @_allow_super_init
219233 def __init__ (self , figure ):
@@ -233,11 +247,11 @@ def __init__(self, figure):
233247 self ._is_drawing = False
234248 self ._draw_rect_callback = lambda painter : None
235249
236- self .setAttribute (QtCore . Qt .WA_OpaquePaintEvent )
250+ self .setAttribute (4 ) # Qt.WidgetAttribute. WA_OpaquePaintEvent
237251 self .setMouseTracking (True )
238252 self .resize (* self .get_width_height ())
239253
240- palette = QtGui .QPalette (QtCore . Qt . white )
254+ palette = QtGui .QPalette (QtGui . QColor ( " white" ) )
241255 self .setPalette (palette )
242256
243257 def _update_figure_dpi (self ):
@@ -283,7 +297,7 @@ def get_width_height(self):
283297
284298 def enterEvent (self , event ):
285299 try :
286- x , y = self .mouseEventCoords (event . pos ( ))
300+ x , y = self .mouseEventCoords (self . _get_position ( event ))
287301 except AttributeError :
288302 # the event from PyQt4 does not include the position
289303 x = y = None
@@ -293,6 +307,9 @@ def leaveEvent(self, event):
293307 QtWidgets .QApplication .restoreOverrideCursor ()
294308 FigureCanvasBase .leave_notify_event (self , guiEvent = event )
295309
310+ _get_position = operator .methodcaller (
311+ "position" if QT_API in ["PyQt6" , "PySide6" ] else "pos" )
312+
296313 def mouseEventCoords (self , pos ):
297314 """
298315 Calculate mouse coordinates in physical pixels.
@@ -310,34 +327,34 @@ def mouseEventCoords(self, pos):
310327 return x * dpi_ratio , y * dpi_ratio
311328
312329 def mousePressEvent (self , event ):
313- x , y = self .mouseEventCoords (event . pos ( ))
314- button = self .buttond .get (event .button ())
330+ x , y = self .mouseEventCoords (self . _get_position ( event ))
331+ button = self .buttond .get (_to_int ( event .button () ))
315332 if button is not None :
316333 FigureCanvasBase .button_press_event (self , x , y , button ,
317334 guiEvent = event )
318335
319336 def mouseDoubleClickEvent (self , event ):
320- x , y = self .mouseEventCoords (event . pos ( ))
321- button = self .buttond .get (event .button ())
337+ x , y = self .mouseEventCoords (self . _get_position ( event ))
338+ button = self .buttond .get (_to_int ( event .button () ))
322339 if button is not None :
323340 FigureCanvasBase .button_press_event (self , x , y ,
324341 button , dblclick = True ,
325342 guiEvent = event )
326343
327344 def mouseMoveEvent (self , event ):
328- x , y = self .mouseEventCoords (event )
345+ x , y = self .mouseEventCoords (self . _get_position ( event ) )
329346 FigureCanvasBase .motion_notify_event (self , x , y , guiEvent = event )
330347
331348 def mouseReleaseEvent (self , event ):
332- x , y = self .mouseEventCoords (event )
333- button = self .buttond .get (event .button ())
349+ x , y = self .mouseEventCoords (self . _get_position ( event ) )
350+ button = self .buttond .get (_to_int ( event .button () ))
334351 if button is not None :
335352 FigureCanvasBase .button_release_event (self , x , y , button ,
336353 guiEvent = event )
337354
338355 if QtCore .qVersion () >= "5." :
339356 def wheelEvent (self , event ):
340- x , y = self .mouseEventCoords (event )
357+ x , y = self .mouseEventCoords (self . _get_position ( event ) )
341358 # from QWheelEvent::delta doc
342359 if event .pixelDelta ().x () == 0 and event .pixelDelta ().y () == 0 :
343360 steps = event .angleDelta ().y () / 120
@@ -368,6 +385,9 @@ def keyReleaseEvent(self, event):
368385 FigureCanvasBase .key_release_event (self , key , guiEvent = event )
369386
370387 def resizeEvent (self , event ):
388+ frame = sys ._getframe ()
389+ if frame .f_code is frame .f_back .f_code : # Prevent PyQt6 recursion.
390+ return
371391 w = event .size ().width () * self ._dpi_ratio
372392 h = event .size ().height () * self ._dpi_ratio
373393 dpival = self .figure .dpi
@@ -388,7 +408,7 @@ def minumumSizeHint(self):
388408
389409 def _get_key (self , event ):
390410 event_key = event .key ()
391- event_mods = int (event .modifiers ()) # actually a bitmask
411+ event_mods = _to_int (event .modifiers ()) # actually a bitmask
392412
393413 # get names of the pressed modifier keys
394414 # 'control' is named 'control' when a standalone key, but 'ctrl' when a
@@ -433,7 +453,7 @@ def start_event_loop(self, timeout=0):
433453 if timeout > 0 :
434454 timer = QtCore .QTimer .singleShot (int (timeout * 1000 ),
435455 event_loop .quit )
436- event_loop . exec_ ( )
456+ qt_compat . _exec ( event_loop )
437457
438458 def stop_event_loop (self , event = None ):
439459 # docstring inherited
@@ -575,7 +595,7 @@ def __init__(self, canvas, num):
575595 # StrongFocus accepts both tab and click to focus and will enable the
576596 # canvas to process event without clicking.
577597 # https://doc.qt.io/qt-5/qt.html#FocusPolicy-enum
578- self .canvas .setFocusPolicy (QtCore . Qt . StrongFocus )
598+ self .canvas .setFocusPolicy (0x1 | 0x2 | 0x8 ) # StrongFocus
579599 self .canvas .setFocus ()
580600
581601 self .window .raise_ ()
@@ -654,8 +674,8 @@ class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar):
654674 def __init__ (self , canvas , parent , coordinates = True ):
655675 """coordinates: should we show the coordinates on the right?"""
656676 QtWidgets .QToolBar .__init__ (self , parent )
657- self .setAllowedAreas (
658- QtCore . Qt .TopToolBarArea | QtCore . Qt . BottomToolBarArea )
677+ self .setAllowedAreas ( # Qt::TopToolBarArea | BottomToolBarArea
678+ Qt .ToolBarAreas ( 0x4 | 0x8 ) )
659679
660680 self .coordinates = coordinates
661681 self ._actions = {} # mapping of toolitem method names to QActions.
@@ -677,11 +697,10 @@ def __init__(self, canvas, parent, coordinates=True):
677697 # will resize this label instead of the buttons.
678698 if self .coordinates :
679699 self .locLabel = QtWidgets .QLabel ("" , self )
680- self .locLabel .setAlignment (
681- QtCore . Qt .AlignRight | QtCore . Qt . AlignVCenter )
700+ self .locLabel .setAlignment ( # Qt::AlignRight | AlignVCenter
701+ Qt .Alignment ( 0x02 | 0x80 ) )
682702 self .locLabel .setSizePolicy (
683- QtWidgets .QSizePolicy (QtWidgets .QSizePolicy .Expanding ,
684- QtWidgets .QSizePolicy .Ignored ))
703+ QtWidgets .QSizePolicy (7 , 8 )) # Expanding, Ignored
685704 labelAction = self .addWidget (self .locLabel )
686705 labelAction .setVisible (True )
687706
@@ -714,8 +733,8 @@ def _icon(self, name):
714733 _setDevicePixelRatio (pm , _devicePixelRatioF (self ))
715734 if self .palette ().color (self .backgroundRole ()).value () < 128 :
716735 icon_color = self .palette ().color (self .foregroundRole ())
717- mask = pm .createMaskFromColor (QtGui . QColor ( 'black' ),
718- QtCore . Qt .MaskOutColor )
736+ mask = pm .createMaskFromColor (
737+ QtGui . QColor ( 'black' ), 1 ) # Qt.MaskMode. MaskOutColor
719738 pm .fill (icon_color )
720739 pm .setMask (mask )
721740 return QtGui .QIcon (pm )
@@ -785,7 +804,7 @@ def configure_subplots(self):
785804 image = str (cbook ._get_data_path ('images/matplotlib.png' ))
786805 dia = SubplotToolQt (self .canvas .figure , self .canvas .parent ())
787806 dia .setWindowIcon (QtGui .QIcon (image ))
788- dia . exec_ ( )
807+ qt_compat . _exec ( dia )
789808
790809 def save_figure (self , * args ):
791810 filetypes = self .canvas .get_supported_filetypes_grouped ()
@@ -874,7 +893,7 @@ def _export_values(self):
874893 QtGui .QFontMetrics (text .document ().defaultFont ())
875894 .size (0 , text .toPlainText ()).height () + 20 )
876895 text .setMaximumSize (size )
877- dialog . exec_ ( )
896+ qt_compat . _exec ( dialog )
878897
879898 def _on_value_changed (self ):
880899 self ._figure .subplots_adjust (** {attr : self ._widgets [attr ].value ()
@@ -899,14 +918,13 @@ class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar):
899918 def __init__ (self , toolmanager , parent ):
900919 ToolContainerBase .__init__ (self , toolmanager )
901920 QtWidgets .QToolBar .__init__ (self , parent )
902- self .setAllowedAreas (
903- QtCore . Qt .TopToolBarArea | QtCore . Qt . BottomToolBarArea )
921+ self .setAllowedAreas ( # Qt::TopToolBarArea | BottomToolBarArea
922+ Qt .ToolBarAreas ( 0x4 | 0x8 ) )
904923 message_label = QtWidgets .QLabel ("" )
905- message_label .setAlignment (
906- QtCore . Qt .AlignRight | QtCore . Qt . AlignVCenter )
924+ message_label .setAlignment ( # Qt::AlignRight | AlignVCenter
925+ Qt .Alignment ( 0x02 | 0x80 ) )
907926 message_label .setSizePolicy (
908- QtWidgets .QSizePolicy (QtWidgets .QSizePolicy .Expanding ,
909- QtWidgets .QSizePolicy .Ignored ))
927+ QtWidgets .QSizePolicy (7 , 8 )) # Expanding, Ignored
910928 self ._message_action = self .addWidget (message_label )
911929 self ._toolitems = {}
912930 self ._groups = {}
@@ -1031,7 +1049,7 @@ def mainloop():
10311049 if is_python_signal_handler :
10321050 signal .signal (signal .SIGINT , signal .SIG_DFL )
10331051 try :
1034- qApp . exec_ ( )
1052+ qt_compat . _exec ( qApp )
10351053 finally :
10361054 # reset the SIGINT exception handler
10371055 if is_python_signal_handler :
0 commit comments