1010from matplotlib ._pylab_helpers import Gcf
1111from matplotlib .backend_bases import (
1212 _Backend , FigureCanvasBase , FigureManagerBase , NavigationToolbar2 ,
13- TimerBase )
13+ TimerBase , WindowBase , ExpandableBase )
1414from matplotlib .backend_tools import Cursors
1515
1616import gi
@@ -118,6 +118,109 @@ class _FigureCanvasGTK(FigureCanvasBase):
118118 _timer_cls = TimerGTK
119119
120120
121+ _flow = [Gtk .Orientation .HORIZONTAL , Gtk .Orientation .VERTICAL ]
122+
123+
124+ class _WindowGTK (WindowBase , Gtk .Window ):
125+ # Must be implemented in GTK3/GTK4 backends:
126+ # * _add_element - to add an widget to a container
127+ # * _setup_signals
128+ # * _get_self - a method to ensure that we have been fully initialised
129+
130+ def __init__ (self , title , ** kwargs ):
131+ super ().__init__ (title = title , ** kwargs )
132+
133+ self .set_window_title (title )
134+
135+ self ._layout = {}
136+ self ._setup_box ('_outer' , Gtk .Orientation .VERTICAL , False , None )
137+ self ._setup_box ('north' , Gtk .Orientation .VERTICAL , False , '_outer' )
138+ self ._setup_box ('_middle' , Gtk .Orientation .HORIZONTAL , True , '_outer' )
139+ self ._setup_box ('south' , Gtk .Orientation .VERTICAL , False , '_outer' )
140+
141+ self ._setup_box ('west' , Gtk .Orientation .HORIZONTAL , False , '_middle' )
142+ self ._setup_box ('center' , Gtk .Orientation .VERTICAL , True , '_middle' )
143+ self ._setup_box ('east' , Gtk .Orientation .HORIZONTAL , False , '_middle' )
144+
145+ self .set_child (self ._layout ['_outer' ])
146+
147+ self ._setup_signals ()
148+
149+ def _setup_box (self , name , orientation , grow , parent ):
150+ self ._layout [name ] = Gtk .Box (orientation = orientation )
151+ if parent :
152+ self ._add_element (self ._layout [parent ], self ._layout [name ], True , grow )
153+ self ._layout [name ].show ()
154+
155+ def add_element (self , element , place ):
156+ element .show ()
157+
158+ # Get the flow of the element (the opposite of the container)
159+ flow_index = not _flow .index (self ._layout [place ].get_orientation ())
160+ flow = _flow [flow_index ]
161+ separator = Gtk .Separator (orientation = flow )
162+ separator .show ()
163+
164+ try :
165+ element .flow = element .flow_types [flow_index ]
166+ except AttributeError :
167+ pass
168+
169+ # Determine if this element should fill all the space given to it
170+ expand = isinstance (element , ExpandableBase )
171+
172+ if place in ['north' , 'west' , 'center' ]:
173+ to_start = True
174+ elif place in ['south' , 'east' ]:
175+ to_start = False
176+ else :
177+ raise KeyError ('Unknown value for place, %s' % place )
178+
179+ self ._add_element (self ._layout [place ], element , to_start , expand )
180+ self ._add_element (self ._layout [place ], separator , to_start , False )
181+
182+ h = 0
183+ for e in [element , separator ]:
184+ min_size , nat_size = e .get_preferred_size ()
185+ h += nat_size .height
186+
187+ return h
188+
189+ def set_default_size (self , width , height ):
190+ Gtk .Window .set_default_size (self , width , height )
191+
192+ def show (self ):
193+ # show the window
194+ Gtk .Window .show (self )
195+ if mpl .rcParams ["figure.raise_window" ]:
196+ if self ._get_self ():
197+ self .present ()
198+ else :
199+ # If this is called by a callback early during init,
200+ # self.window (a GtkWindow) may not have an associated
201+ # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
202+ # and present() would crash.
203+ _api .warn_external ("Cannot raise window yet to be setup" )
204+
205+ def destroy (self ):
206+ Gtk .Window .destroy (self )
207+
208+ def set_fullscreen (self , fullscreen ):
209+ if fullscreen :
210+ self .fullscreen ()
211+ else :
212+ self .unfullscreen ()
213+
214+ def get_window_title (self ):
215+ return self .get_title ()
216+
217+ def set_window_title (self , title ):
218+ self .set_title (title )
219+
220+ def resize (self , width , height ):
221+ Gtk .Window .resize (self , width , height )
222+
223+
121224class _FigureManagerGTK (FigureManagerBase ):
122225 """
123226 Attributes
@@ -135,51 +238,22 @@ class _FigureManagerGTK(FigureManagerBase):
135238 """
136239
137240 def __init__ (self , canvas , num ):
138- self ._gtk_ver = gtk_ver = Gtk .get_major_version ()
139-
140241 app = _create_application ()
141- self .window = Gtk . Window ( )
242+ self .window = self . _window_class ( 'Matplotlib Figure Manager' )
142243 app .add_window (self .window )
143244 super ().__init__ (canvas , num )
144245
145- if gtk_ver == 3 :
146- self .window .set_wmclass ("matplotlib" , "Matplotlib" )
147- icon_ext = "png" if sys .platform == "win32" else "svg"
148- self .window .set_icon_from_file (
149- str (cbook ._get_data_path (f"images/matplotlib.{ icon_ext } " )))
150-
151- self .vbox = Gtk .Box ()
152- self .vbox .set_property ("orientation" , Gtk .Orientation .VERTICAL )
153-
154- if gtk_ver == 3 :
155- self .window .add (self .vbox )
156- self .vbox .show ()
157- self .canvas .show ()
158- self .vbox .pack_start (self .canvas , True , True , 0 )
159- elif gtk_ver == 4 :
160- self .window .set_child (self .vbox )
161- self .vbox .prepend (self .canvas )
162-
163- # calculate size for window
246+ self .window .add_element (self .canvas , 'center' )
164247 w , h = self .canvas .get_width_height ()
165248
166- if self .toolbar is not None :
167- if gtk_ver == 3 :
168- self .toolbar .show ()
169- self .vbox .pack_end (self .toolbar , False , False , 0 )
170- elif gtk_ver == 4 :
171- sw = Gtk .ScrolledWindow (vscrollbar_policy = Gtk .PolicyType .NEVER )
172- sw .set_child (self .toolbar )
173- self .vbox .append (sw )
174- min_size , nat_size = self .toolbar .get_preferred_size ()
175- h += nat_size .height
249+ if self .toolbar :
250+ h += self .window .add_element (self .toolbar , 'south' ) # put in ScrolledWindow in GTK4?
176251
177252 self .window .set_default_size (w , h )
178253
179254 self ._destroying = False
180- self .window .connect ("destroy" , lambda * args : Gcf .destroy (self ))
181- self .window .connect ({3 : "delete_event" , 4 : "close-request" }[gtk_ver ],
182- lambda * args : Gcf .destroy (self ))
255+ self .window .mpl_connect ('window_destroy_event' , lambda * args : Gcf .destroy (self ))
256+
183257 if mpl .is_interactive ():
184258 self .window .show ()
185259 self .canvas .draw_idle ()
@@ -220,24 +294,9 @@ def show(self):
220294 # show the figure window
221295 self .window .show ()
222296 self .canvas .draw ()
223- if mpl .rcParams ["figure.raise_window" ]:
224- meth_name = {3 : "get_window" , 4 : "get_surface" }[self ._gtk_ver ]
225- if getattr (self .window , meth_name )():
226- self .window .present ()
227- else :
228- # If this is called by a callback early during init,
229- # self.window (a GtkWindow) may not have an associated
230- # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
231- # and present() would crash.
232- _api .warn_external ("Cannot raise window yet to be setup" )
233297
234298 def full_screen_toggle (self ):
235- is_fullscreen = {
236- 3 : lambda w : (w .get_window ().get_state ()
237- & Gdk .WindowState .FULLSCREEN ),
238- 4 : lambda w : w .is_fullscreen (),
239- }[self ._gtk_ver ]
240- if is_fullscreen (self .window ):
299+ if self .window .is_fullscreen ():
241300 self .window .unfullscreen ()
242301 else :
243302 self .window .fullscreen ()
@@ -255,7 +314,7 @@ def resize(self, width, height):
255314 min_size , nat_size = self .toolbar .get_preferred_size ()
256315 height += nat_size .height
257316 canvas_size = self .canvas .get_allocation ()
258- if self . _gtk_ver >= 4 or canvas_size .width == canvas_size .height == 1 :
317+ if canvas_size .width == canvas_size .height == 1 :
259318 # A canvas size of (1, 1) cannot exist in most cases, because
260319 # window decorations would prevent such a small window. This call
261320 # must be before the window has been mapped and widgets have been
0 commit comments