Skip to content

Commit

Permalink
Refactoring the WindowGTK classes as part 1 of the larger MEP27 refac…
Browse files Browse the repository at this point in the history
…tor, splitting up previous PRs into smaller easier to review chunks.
  • Loading branch information
OceanWolf committed Aug 9, 2024
1 parent a833d99 commit b86ebb1
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 57 deletions.
168 changes: 167 additions & 1 deletion lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1686,7 +1686,59 @@ def save_args_and_handle_sigint(*args):
old_sigint_handler(*handler_args)


class FigureCanvasBase:
class ExpandableBase(object):
"""
Base class for GUI elements that can expand to fill the area given to them
by the encapsulating container (e.g. the main window).
At the moment this class does not do anything apart from mark such classes,
but this may well change at a later date, PRs welcome.
"""
pass

class FlowBase(object):
"""
Base mixin class for all GUI elements that can flow, aka laid out in
different directions.
The MPL window class deals with the manipulation of this mixin, so users
don't actually need to interact with this class.
Classes the implement this class must override the _update_flow method.
"""
flow_types = ['horizontal', 'vertical']

def __init__(self, flow='horizontal', flow_locked=False, **kwargs):
super(FlowBase, self).__init__(**kwargs)
self.flow_locked = flow_locked
self.flow = flow

@property
def flow(self):
"""
The direction of flow, one of the strings in `flow_type`.
"""
return FlowBase.flow_types[self._flow]

@flow.setter
def flow(self, flow):
if self.flow_locked:
return

try:
self._flow = FlowBase.flow_types.index(flow)
except ValueError:
raise ValueError('Flow (%s), not in list %s' % (flow, FlowBase.flow_types))

self._update_flow()

def _update_flow(self):
"""
Classes that extend FlowBase must override this method.
You can use the internal property self._flow whereby
flow_types[self._flow] gives the current flow.
"""
raise NotImplementedError


class FigureCanvasBase(ExpandableBase):
"""
The canvas the figure renders into.
Expand Down Expand Up @@ -2581,6 +2633,120 @@ class NonGuiException(Exception):
pass


class WindowEvent(object):
def __init__(self, name, window):
self.name = name
self.window = window


class WindowBase(cbook.EventEmitter):
"""The base class to show a window on screen.
Parameters
----------
title : str
The title of the window.
"""

def __init__(self, title, **kwargs):
super(WindowBase, self).__init__(**kwargs)

def show(self):
"""
For GUI backends, show the figure window and redraw.
For non-GUI backends, raise an exception to be caught
by :meth:`~matplotlib.figure.Figure.show`, for an
optional warning.
"""
raise NonGuiException()

def destroy(self):
"""Destroys the window"""
pass

def set_fullscreen(self, fullscreen):
"""Whether to show the window fullscreen or not, GUI only.
Parameters
----------
fullscreen : bool
True for yes, False for no.
"""
pass

def set_default_size(self, width, height):
"""Sets the default size of the window, defaults to a simple resize.
Parameters
----------
width : int
The default width (in pixels) of the window.
height : int
The default height (in pixels) of the window.
"""
self.resize(width, height)

def resize(self, width, height):
""""For gui backends, resizes the window.
Parameters
----------
width : int
The new width (in pixels) for the window.
height : int
The new height (in pixels) for the window.
"""
pass

def get_window_title(self):
"""
Get the title text of the window containing the figure.
Return None for non-GUI backends (e.g., a PS backend).
Returns
-------
str : The window's title.
"""
return 'image'

def set_window_title(self, title):
"""
Set the title text of the window containing the figure. Note that
this has no effect for non-GUI backends (e.g., a PS backend).
Parameters
----------
title : str
The title of the window.
"""
pass

def add_element(self, element, place):
""" Adds a gui widget to the window.
This has no effect for non-GUI backends and properties only apply
to those backends that support them, or have a suitable workaround.
Parameters
----------
element : A gui element.
The element to add to the window
place : string
The location to place the element, either compass points north,
east, south, west, or center.
"""
pass

def destroy_event(self, *args):
"""Fires this event when the window wants to destroy itself.
Note this method should hook up to the backend's internal window's
close event.
"""
s = 'window_destroy_event'
event = WindowEvent(s, self)
self._callbacks.process(s, event)


class FigureManagerBase:
"""
A backend-independent abstraction of a figure container and controller.
Expand Down
165 changes: 112 additions & 53 deletions lib/matplotlib/backends/_backend_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase)
TimerBase, WindowBase, ExpandableBase)
from matplotlib.backend_tools import Cursors

import gi
Expand Down Expand Up @@ -118,6 +118,109 @@ class _FigureCanvasGTK(FigureCanvasBase):
_timer_cls = TimerGTK


_flow = [Gtk.Orientation.HORIZONTAL, Gtk.Orientation.VERTICAL]


class _WindowGTK(WindowBase, Gtk.Window):
# Must be implemented in GTK3/GTK4 backends:
# * _add_element - to add an widget to a container
# * _setup_signals
# * _get_self - a method to ensure that we have been fully initialised

def __init__(self, title, **kwargs):
super().__init__(title=title, **kwargs)

self.set_window_title(title)

self._layout = {}
self._setup_box('_outer', Gtk.Orientation.VERTICAL, False, None)
self._setup_box('north', Gtk.Orientation.VERTICAL, False, '_outer')
self._setup_box('_middle', Gtk.Orientation.HORIZONTAL, True, '_outer')
self._setup_box('south', Gtk.Orientation.VERTICAL, False, '_outer')

self._setup_box('west', Gtk.Orientation.HORIZONTAL, False, '_middle')
self._setup_box('center', Gtk.Orientation.VERTICAL, True, '_middle')
self._setup_box('east', Gtk.Orientation.HORIZONTAL, False, '_middle')

self.set_child(self._layout['_outer'])

self._setup_signals()

def _setup_box(self, name, orientation, grow, parent):
self._layout[name] = Gtk.Box(orientation=orientation)
if parent:
self._add_element(self._layout[parent], self._layout[name], True, grow)
self._layout[name].show()

def add_element(self, element, place):
element.show()

# Get the flow of the element (the opposite of the container)
flow_index = not _flow.index(self._layout[place].get_orientation())
flow = _flow[flow_index]
separator = Gtk.Separator(orientation=flow)
separator.show()

try:
element.flow = element.flow_types[flow_index]
except AttributeError:
pass

# Determine if this element should fill all the space given to it
expand = isinstance(element, ExpandableBase)

if place in ['north', 'west', 'center']:
to_start = True
elif place in ['south', 'east']:
to_start = False
else:
raise KeyError('Unknown value for place, %s' % place)

self._add_element(self._layout[place], element, to_start, expand)
self._add_element(self._layout[place], separator, to_start, False)

h = 0
for e in [element, separator]:
min_size, nat_size = e.get_preferred_size()
h += nat_size.height

return h

def set_default_size(self, width, height):
Gtk.Window.set_default_size(self, width, height)

def show(self):
# show the window
Gtk.Window.show(self)
if mpl.rcParams["figure.raise_window"]:
if self._get_self():
self.present()
else:
# If this is called by a callback early during init,
# self.window (a GtkWindow) may not have an associated
# low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
# and present() would crash.
_api.warn_external("Cannot raise window yet to be setup")

def destroy(self):
Gtk.Window.destroy(self)

def set_fullscreen(self, fullscreen):
if fullscreen:
self.fullscreen()
else:
self.unfullscreen()

def get_window_title(self):
return self.get_title()

def set_window_title(self, title):
self.set_title(title)

def resize(self, width, height):
Gtk.Window.resize(self, width, height)


class _FigureManagerGTK(FigureManagerBase):
"""
Attributes
Expand All @@ -135,51 +238,22 @@ class _FigureManagerGTK(FigureManagerBase):
"""

def __init__(self, canvas, num):
self._gtk_ver = gtk_ver = Gtk.get_major_version()

app = _create_application()
self.window = Gtk.Window()
self.window = self._window_class('Matplotlib Figure Manager')
app.add_window(self.window)
super().__init__(canvas, num)

if gtk_ver == 3:
self.window.set_wmclass("matplotlib", "Matplotlib")
icon_ext = "png" if sys.platform == "win32" else "svg"
self.window.set_icon_from_file(
str(cbook._get_data_path(f"images/matplotlib.{icon_ext}")))

self.vbox = Gtk.Box()
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)

if gtk_ver == 3:
self.window.add(self.vbox)
self.vbox.show()
self.canvas.show()
self.vbox.pack_start(self.canvas, True, True, 0)
elif gtk_ver == 4:
self.window.set_child(self.vbox)
self.vbox.prepend(self.canvas)

# calculate size for window
self.window.add_element(self.canvas, 'center')
w, h = self.canvas.get_width_height()

if self.toolbar is not None:
if gtk_ver == 3:
self.toolbar.show()
self.vbox.pack_end(self.toolbar, False, False, 0)
elif gtk_ver == 4:
sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
sw.set_child(self.toolbar)
self.vbox.append(sw)
min_size, nat_size = self.toolbar.get_preferred_size()
h += nat_size.height
if self.toolbar:
h += self.window.add_element(self.toolbar, 'south') # put in ScrolledWindow in GTK4?

self.window.set_default_size(w, h)

self._destroying = False
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver],
lambda *args: Gcf.destroy(self))
self.window.mpl_connect('window_destroy_event', lambda *args: Gcf.destroy(self))

if mpl.is_interactive():
self.window.show()
self.canvas.draw_idle()
Expand Down Expand Up @@ -220,24 +294,9 @@ def show(self):
# show the figure window
self.window.show()
self.canvas.draw()
if mpl.rcParams["figure.raise_window"]:
meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver]
if getattr(self.window, meth_name)():
self.window.present()
else:
# If this is called by a callback early during init,
# self.window (a GtkWindow) may not have an associated
# low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
# and present() would crash.
_api.warn_external("Cannot raise window yet to be setup")

def full_screen_toggle(self):
is_fullscreen = {
3: lambda w: (w.get_window().get_state()
& Gdk.WindowState.FULLSCREEN),
4: lambda w: w.is_fullscreen(),
}[self._gtk_ver]
if is_fullscreen(self.window):
if self.window.is_fullscreen():
self.window.unfullscreen()
else:
self.window.fullscreen()
Expand All @@ -255,7 +314,7 @@ def resize(self, width, height):
min_size, nat_size = self.toolbar.get_preferred_size()
height += nat_size.height
canvas_size = self.canvas.get_allocation()
if self._gtk_ver >= 4 or canvas_size.width == canvas_size.height == 1:
if canvas_size.width == canvas_size.height == 1:
# A canvas size of (1, 1) cannot exist in most cases, because
# window decorations would prevent such a small window. This call
# must be before the window has been mapped and widgets have been
Expand Down
Loading

0 comments on commit b86ebb1

Please sign in to comment.