8000 Share FigureManager class between gtk3 and gtk4. · matplotlib/matplotlib@bb3c039 · GitHub
[go: up one dir, main page]

Skip to content

Commit bb3c039

Browse files
committed
Share FigureManager class between gtk3 and gtk4.
The gtk version checks are not the nicest, but sharing most of the setup code between gtk3 and gtk4 still seems worthwhile? (Note that on gtk3, it is explicitly OK to not bother to destroy the vbox and toolbar, as gtk3 always destroys child widgets upon parent destruction.)
1 parent 67e69e9 commit bb3c039

File tree

3 files changed

+147
-234
lines changed

3 files changed

+147
-234
lines changed

lib/matplotlib/backends/_backend_gtk.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
"""
44

55
import logging
6+
import sys
67

78
import matplotlib as mpl
89
from matplotlib import _api, backend_tools, cbook
9-
from matplotlib.backend_bases import _Backend, NavigationToolbar2, TimerBase
10+
from matplotlib._pylab_helpers import Gcf
11+
from matplotlib.backend_bases import (
12+
_Backend, FigureManagerBase, NavigationToolbar2, TimerBase)
1013
from matplotlib.backend_tools import Cursors
1114

1215
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
1316
# the desired GTK.
14-
from gi.repository import Gio, GLib, Gtk
17+
from gi.repository import Gdk, Gio, GLib, Gtk
1518

1619

1720
_log = logging.getLogger(__name__)
@@ -109,6 +112,134 @@ def _on_timer(self):
109112
return False
110113

111114

115+
class _FigureManagerGTK(FigureManagerBase):
116+
"""
117+
Attributes
118+
----------
119+
canvas : `FigureCanvas`
120+
The FigureCanvas instance
121+
num : int or str
122+
The Figure number
123+
toolbar : Gtk.Toolbar or Gtk.Box
124+
The toolbar
125+
vbox : Gtk.VBox
126+
The Gtk.VBox containing the canvas and toolbar
127+
window : Gtk.Window
128+
The Gtk.Window
129+
"""
130+
131+
def __init__(self, canvas, num):
132+
self._gtk_ver = gtk_ver = Gtk.get_major_version()
133+
134+
app = _create_application()
135+
self.window = Gtk.Window()
136+
app.add_window(self.window)
137+
super().__init__(canvas, num)
138+
139+
if gtk_ver == 3:
140+
self.window.set_wmclass("matplotlib", "Matplotlib")
141+
icon_ext = "png" if sys.platform == "win32" else "svg"
142+
self.window.set_icon_from_file(
143+
str(cbook._get_data_path(f"images/matplotlib.{icon_ext}")))
144+
145+
self.vbox = Gtk.Box()
146+
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
147+
148+
if gtk_ver == 3:
149+
self.window.add(self.vbox)
150+
self.vbox.show()
151+
self.canvas.show()
152+
self.vbox.pack_start(self.canvas, True, True, 0)
153+
elif gtk_ver == 4:
154+
self.window.set_child(self.vbox)
155+
self.vbox.prepend(self.canvas)
156+
157+
# calculate size for window
158+
w, h = self.canvas.get_width_height()
159+
160+
if self.toolbar is not None:
161+
if gtk_ver == 3:
162+
self.toolbar.show()
163+
self.vbox.pack_end(self.toolbar, False, False, 0)
164+
elif gtk_ver == 4:
165+
sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
166+
sw.set_child(self.toolbar)
167+
self.vbox.append(sw)
168+
min_size, nat_size = self.toolbar.get_preferred_size()
169+
h += nat_size.height
170+
171+
self.window.set_default_size(w, h)
172+
173+
self._destroying = False
174+
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
175+
self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver],
176+
lambda *args: Gcf.destroy(self))
177+
if mpl.is_interactive():
178+
self.window.show()
179+
self.canvas.draw_idle()
180+
181+
self.canvas.grab_focus()
182+
183+
def destroy(self, *args):
184+
if self._destroying:
185+
# Otherwise, this can be called twice when the user presses 'q',
186+
# which calls Gcf.destroy(self), then this destroy(), then triggers
187+
# Gcf.destroy(self) once again via
188+
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
189+
return
190+
self._destroying = True
191+
self.window.destroy()
192+
self.canvas.destroy()
193+
194+
def show(self):
195+
# show the figure window
196+
self.window.show()
197+
self.canvas.draw()
198+
if mpl.rcParams["figure.raise_window"]:
199+
meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver]
200+
if getattr(self.window, meth_name)():
201+
self.window.present()
202+
else:
203+
# If this is called by a callback early during init,
204+
# self.window (a GtkWindow) may not have an associated
205+
# low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
206+
# and present() would crash.
207+
_api.warn_external("Cannot raise window yet to be setup")
208+
209+
def full_screen_toggle(self):
210+
is_fullscreen = {
211+
3: lambda w: (w.get_window().get_state()
212+
& Gdk.WindowState.FULLSCREEN),
213+
4: lambda w: w.is_fullscreen(),
214+
}[self._gtk_ver]
215+
if is_fullscreen(self.window):
216+
self.window.unfullscreen()
217+
else:
218+
self.window.fullscreen()
219+
220+
def get_window_title(self):
221+
return self.window.get_title()
222+
223+
def set_window_title(self, title):
2 BEA4 24+
self.window.set_title(title)
225+
226+
def resize(self, width, height):
227+
width = int(width / self.canvas.device_pixel_ratio)
228+
height = int(height / self.canvas.device_pixel_ratio)
229+
if self.toolbar:
230+
toolbar_size = self.toolbar.size_request()
231+
height += toolbar_size.height
232+
canvas_size = self.canvas.get_allocation()
233+
if self._gtk_ver >= 4 or canvas_size.width == canvas_size.height == 1:
234+
# A canvas size of (1, 1) cannot exist in most cases, because
235+
# window decorations would prevent such a small window. This call
236+
# must be before the window has been mapped and widgets have been
237+
# sized, so just change the window's starting size.
238+
self.window.set_default_size(width, height)
239+
else:
240+
self.window.resize(width, height)
241+
242+
112243
class _NavigationToolbar2GTK(NavigationToolbar2):
113244
# Must be implemented in GTK3/GTK4 backends:
114245
# * __init__

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 7 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
import matplotlib as mpl
88
from matplotlib import _api, backend_tools, cbook
9-
from matplotlib._pylab_helpers import Gcf
10-
from matplotlib.backend_bases import (
11-
FigureCanvasBase, FigureManagerBase, ToolContainerBase)
9+
from matplotlib.backend_bases import FigureCanvasBase, ToolContainerBase
1210
from matplotlib.backend_tools import Cursors
1311
from matplotlib.figure import Figure
1412

@@ -29,7 +27,7 @@
2927
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
3028
from . import _backend_gtk
3129
from ._backend_gtk import (
32-
backend_version, _BackendGTK, _NavigationToolbar2GTK,
30+
backend_version, _BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
3331
TimerGTK as TimerGTK3,
3432
)
3533

@@ -293,130 +291,6 @@ def flush_events(self):
293291
context.iteration(True)
294292

295293

296-
class FigureManagerGTK3(FigureManagerBase):
297-
"""
298-
Attributes
299-
----------
300-
canvas : `FigureCanvas`
301-
The FigureCanvas instance
302-
num : int or str
303-
The Figure number
304-
toolbar : Gtk.Toolbar
305-
The toolbar
306-
vbox : Gtk.VBox
307-
The Gtk.VBox containing the canvas and toolbar
308-
window : Gtk.Window
309-
The Gtk.Window
310-
"""
311-
312-
def __init__(self, canvas, num):
313-
app = _backend_gtk._create_application()
314-
self.window = Gtk.Window()
315-
app.add_window(self.window)
316-
super().__init__(canvas, num)
317-
318-
self.window.set_wmclass("matplotlib", "Matplotlib")
319-
icon_ext = "png" if sys.platform == "win32" else "svg"
320-
self.window.set_icon_from_file(
321-
str(cbook._get_data_path(f"images/matplotlib.{icon_ext}")))
322-
323-
self.vbox = Gtk.Box()
324-
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
325-
self.window.add(self.vbox)
326-
self.vbox.show()
327-
328-
self.canvas.show()
329-
330-
self.vbox.pack_start(self.canvas, True, True, 0)
331-
# calculate size for window
332-
w, h = self.canvas.get_width_height()
333-
334-
if self.toolbar is not None:
335-
self.toolbar.show()
336-
self.vbox.pack_end(self.toolbar, False, False, 0)
337-
min_size, nat_size = self.toolbar.get_preferred_size()
338-
h += nat_size.height
339-
340-
self.window.set_default_size(w, h)
341-
342-
self._destroying = False
343-
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
344-
self.window.connect("delete_event", lambda *args: Gcf.destroy(self))
345-
if mpl.is_interactive():
346-
self.window.show()
347-
self.canvas.draw_idle()
348-
349-
self.canvas.grab_focus()
350-
351-
def destroy(self, *args):
352-
if self._destroying:
353-
# Otherwise, this can be called twice when the user presses 'q',
354-
# which calls Gcf.destroy(self), then this destroy(), then triggers
355-
# Gcf.destroy(self) once again via
356-
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
357-
return
358-
self._destroying = True
359-
self.vbox.destroy()
360-
self.window.destroy()
361-
self.canvas.destroy()
362-
if self.toolbar:
363-
self.toolbar.destroy()
364-
365-
def show(self):
366-
# show the figure window
367-
self.window.show()
368-
self.canvas.draw()
369-
if mpl.rcParams['figure.raise_window']:
370-
if self.window.get_window():
371-
self.window.present()
372-
else:
373-
# If this is called by a callback early during init,
374-
# self.window (a GtkWindow) may not have an associated
375-
# low-level GdkWindow (self.window.get_window()) yet, and
376-
# present() would crash.
377-
_api.warn_external("Cannot raise window yet to be setup")
378-
379-
def full_screen_toggle(self):
380-
if self.window.get_window().get_state() & Gdk.WindowState.FULLSCREEN:
381-
self.window.unfullscreen()
382-
else:
383-
self.window.fullscreen()
384-
385-
def _get_toolbar(self):
386-
# must be inited after the window, drawingArea and figure
387-
# attrs are set
388-
if mpl.rcParams['toolbar'] == 'toolbar2':
389-
toolbar = NavigationToolbar2GTK3(self.canvas)
390-
elif mpl.rcParams['toolbar'] == 'toolmanager':
391-
toolbar = ToolbarGTK3(self.toolmanager)
392-
else:
393-
toolbar = None
394-
return toolbar
395-
396-
def get_window_title(self):
397-
return self.window.get_title()
398-
399-
def set_window_title(self, title):
400-
self.window.set_title(title)
401-
402-
def resize(self, width, height):
403-
"""Set the canvas size in pixels."""
404-
width = int(width / self.canvas.device_pixel_ratio)
405-
height = int(height / self.canvas.device_pixel_ratio)
406-
if self.toolbar:
407-
toolbar_size = self.toolbar.size_request()
408-
height += toolbar_size.height
409-
canvas_size = self.canvas.get_allocation()
410-
if canvas_size.width == canvas_size.height == 1:
411-
# A canvas size of (1, 1) cannot exist in most cases, because
412-
# window decorations would prevent such a small window. This call
413-
# must be before the window has been mapped and widgets have been
414-
# sized, so just change the window's starting size.
415-
self.window.set_default_size(width, height)
416-
else:
417-
self.window.resize(width, height)
418-
419-
420294
class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar):
421295
@_api.delete_parameter("3.6", "window")
422296
def __init__(self, canvas, window=None):
@@ -730,8 +604,11 @@ def error_msg_gtk(msg, parent=None):
730604
FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK)
731605
backend_tools._register_tool_class(
732606
FigureCanvasGTK3, _backend_gtk.RubberbandGTK)
733-
FigureManagerGTK3._toolbar2_class = NavigationToolbar2GTK3
734-
FigureManagerGTK3._toolmanager_toolbar_class = ToolbarGTK3
607+
608+
609+
class FigureManagerGTK3(_FigureManagerGTK):
610+
_toolbar2_class = NavigationToolbar2GTK3
611+
_toolmanager_toolbar_class = ToolbarGTK3
735612

736613

737614
@_BackendGTK.export

0 commit comments

Comments
 (0)
0