import functools
import logging
import os
from pathlib import Path
import sys
import matplotlib as mpl
from matplotlib import backend_tools, cbook
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
StatusbarBase, TimerBase, ToolContainerBase, cursors)
from matplotlib.figure import Figure
from matplotlib.widgets import SubplotTool
try:
import gi
except ImportError as err:
raise ImportError("The GTK3 backends require PyGObject") from err
try:
# :raises ValueError: If module/version is already loaded, already
# required, or unavailable.
gi.require_version("Gtk", "3.0")
except ValueError as e:
# in this case we want to re-raise as ImportError so the
# auto-backend selection logic correctly skips.
raise ImportError from e
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
_log = logging.getLogger(__name__)
backend_version = "%s.%s.%s" % (
Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version())
try:
cursord = {
cursors.MOVE: Gdk.Cursor.new(Gdk.CursorType.FLEUR),
cursors.HAND: Gdk.Cursor.new(Gdk.CursorType.HAND2),
cursors.POINTER: Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR),
cursors.SELECT_REGION: Gdk.Cursor.new(Gdk.CursorType.TCROSS),
cursors.WAIT: Gdk.Cursor.new(Gdk.CursorType.WATCH),
}
except TypeError as exc:
# Happens when running headless. Convert to ImportError to cooperate with
# backend switching.
raise ImportError(exc) from exc
class TimerGTK3(TimerBase):
"""Subclass of `.TimerBase` using GTK3 timer events."""
def __init__(self, *args, **kwargs):
self._timer = None
TimerBase.__init__(self, *args, **kwargs)
def _timer_start(self):
# Need to stop it, otherwise we potentially leak a timer id that will
# never be stopped.
self._timer_stop()
self._timer = GLib.timeout_add(self._interval, self._on_timer)
def _timer_stop(self):
if self._timer is not None:
GLib.source_remove(self._timer)
self._timer = None
def _timer_set_interval(self):
# Only stop and restart it if the timer has already been started
if self._timer is not None:
self._timer_stop()
self._timer_start()
def _on_timer(self):
TimerBase._on_timer(self)
# Gtk timeout_add() requires that the callback returns True if it
# is to be called again.
if self.callbacks and not self._single:
return True
else:
self._timer = None
return False
class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
required_interactive_framework = "gtk3"
_timer_cls = TimerGTK3
keyvald = {65507: 'control',
65505: 'shift',
65513: 'alt',
65508: 'control',
65506: 'shift',
65514: 'alt',
65361: 'left',
65362: 'up',
65363: 'right',
65364: 'down',
65307: 'escape',
65470: 'f1',
65471: 'f2',
65472: 'f3',
65473: 'f4',
65474: 'f5',
65475: 'f6',
65476: 'f7',
65477: 'f8',
65478: 'f9',
65479: 'f10',
65480: 'f11',
65481: 'f12',
65300: 'scroll_lock',
65299: 'break',
65288: 'backspace',
65293: 'enter',
65379: 'insert',
65535: 'delete',
65360: 'home',
65367: 'end',
65365: 'pageup',
65366: 'pagedown',
65438: '0',
65436: '1',
65433: '2',
65435: '3',
65430: '4',
65437: '5',
65432: '6',
65429: '7',
65431: '8',
65434: '9',
65451: '+',
65453: '-',
65450: '*',
65455: '/',
65439: 'dec',
65421: 'enter',
}
# Setting this as a static constant prevents
# this resulting expression from leaking
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.EXPOSURE_MASK
| Gdk.EventMask.KEY_PRESS_MASK
| Gdk.EventMask.KEY_RELEASE_MASK
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
| Gdk.EventMask.POINTER_MOTION_MASK
| Gdk.EventMask.POINTER_MOTION_HINT_MASK
| Gdk.EventMask.SCROLL_MASK)
def __init__(self, figure):
FigureCanvasBase.__init__(self, figure)
GObject.GObject.__init__(self)
self._idle_draw_id = 0
self._lastCursor = None
self._rubberband_rect = None
self.connect('scroll_event', self.scroll_event)
self.connect('button_press_event', self.button_press_event)
self.connect('button_release_event', self.button_release_event)
self.connect('configure_event', self.configure_event)
self.connect('draw', self.on_draw_event)
self.connect('draw', self._post_draw)
self.connect('key_press_event', self.key_press_event)
self.connect('key_release_event', self.key_release_event)
self.connect('motion_notify_event', self.motion_notify_event)
self.connect('leave_notify_event', self.leave_notify_event)
self.connect('enter_notify_event', self.enter_notify_event)
self.connect('size_allocate', self.size_allocate)
self.set_events(self.__class__.event_mask)
self.set_double_buffered(True)
self.set_can_focus(True)
renderer_init = cbook._deprecate_method_override(
__class__._renderer_init, self, allow_empty=True, since="3.3",
addendum="Please initialize the renderer, if needed, in the "
"subclass' __init__; a fully empty _renderer_init implementation "
"may be kept for compatibility with earlier versions of "
"Matplotlib.")
if renderer_init:
renderer_init()
@cbook.deprecated("3.3", alternative="__init__")
def _renderer_init(self):
pass
def destroy(self):
#Gtk.DrawingArea.destroy(self)
self.close_event()
def scroll_event(self, widget, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - event.y
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
return False # finish event propagation?
def button_press_event(self, widget, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - event.y
FigureCanvasBase.button_press_event(
self, x, y, event.button, guiEvent=event)
return False # finish event propagation?
def button_release_event(self, widget, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - event.y
FigureCanvasBase.button_release_event(
self, x, y, event.button, guiEvent=event)
return False # finish event propagation?
def key_press_event(self, widget, event):
key = self._get_key(event)
FigureCanvasBase.key_press_event(self, key, guiEvent=event)
return True # stop event propagation
def key_release_event(self, widget, event):
key = self._get_key(event)
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
return True # stop event propagation
def motion_notify_event(self, widget, event):
if event.is_hint:
t, x, y, state = event.window.get_pointer()
else:
x, y = event.x, event.y
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - y
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
return False # finish event propagation?
def leave_notify_event(self, widget, event):
FigureCanvasBase.leave_notify_event(self, event)
def enter_notify_event(self, widget, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - event.y
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
def size_allocate(self, widget, allocation):
dpival = self.figure.dpi
winch = allocation.width / dpival
hinch = allocation.height / dpival
self.figure.set_size_inches(winch, hinch, forward=False)
FigureCanvasBase.resize_event(self)
self.draw_idle()
def _get_key(self, event):
if event.keyval in self.keyvald:
key = self.keyvald[event.keyval]
elif event.keyval < 256:
key = chr(event.keyval)
else:
key = None
modifiers = [
(Gdk.ModifierType.MOD4_MASK, 'super'),
(Gdk.ModifierType.MOD1_MASK, 'alt'),
(Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
]
for key_mask, prefix in modifiers:
if event.state & key_mask:
key = '{0}+{1}'.format(prefix, key)
return key
def configure_event(self, widget, event):
if widget.get_property("window") is None:
return
w, h = event.width, event.height
if w < 3 or h < 3:
return # empty fig
# resize the figure (in inches)
dpi = self.figure.dpi
self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
return False # finish event propagation?
def _draw_rubberband(self, rect):
self._rubberband_rect = rect
# TODO: Only update the rubberband area.
self.queue_draw()
def _post_draw(self, widget, ctx):
if self._rubberband_rect is None:
return
x0, y0, w, h = self._rubberband_rect
x1 = x0 + w
y1 = y0 + h
# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)
ctx.set_antialias(1)
ctx.set_line_width(1)
ctx.set_dash((3, 3), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()
ctx.set_dash((3, 3), 3)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()
def on_draw_event(self, widget, ctx):
# to be overwritten by GTK3Agg or GTK3Cairo
pass
def draw(self):
# docstring inherited
if self.is_drawable():
self.queue_draw()
def draw_idle(self):
# docstring inherited
if self._idle_draw_id != 0:
return
def idle_draw(*args):
try:
self.draw()
finally:
self._idle_draw_id = 0
return False
self._idle_draw_id = GLib.idle_add(idle_draw)
def flush_events(self):
# docstring inherited
Gdk.threads_enter()
while Gtk.events_pending():
Gtk.main_iteration()
Gdk.flush()
Gdk.threads_leave()
class FigureManagerGTK3(FigureManagerBase):
"""
Attributes
----------
canvas : `FigureCanvas`
The FigureCanvas instance
num : int or str
The Figure number
toolbar : Gtk.Toolbar
The Gtk.Toolbar
vbox : Gtk.VBox
The Gtk.VBox containing the canvas and toolbar
window : Gtk.Window
The Gtk.Window
"""
def __init__(self, canvas, num):
FigureManagerBase.__init__(self, canvas, num)
self.window = Gtk.Window()
self.window.set_wmclass("matplotlib", "Matplotlib")
self.set_window_title("Figure %d" % num)
try:
self.window.set_icon_from_file(window_icon)
except Exception:
# Some versions of gtk throw a glib.GError but not all, so I am not
# sure how to catch it. I am unhappy doing a blanket catch here,
# but am not sure what a better way is - JDH
_log.info('Could not load matplotlib icon: %s', sys.exc_info()[1])
self.vbox = Gtk.Box()
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
self.window.add(self.vbox)
self.vbox.show()
self.canvas.show()
self.vbox.pack_start(self.canvas, True, True, 0)
# calculate size for window
w = int(self.canvas.figure.bbox.width)
h = int(self.canvas.figure.bbox.height)
self.toolbar = self._get_toolbar()
def add_widget(child):
child.show()
self.vbox.pack_end(child, False, False, 0)
size_request = child.size_request()
return size_request.height
if self.toolmanager:
backend_tools.add_tools_to_manager(self.toolmanager)
if self.toolbar:
backend_tools.add_tools_to_container(self.toolbar)
if self.toolbar is not None:
self.toolbar.show()
h += add_widget(self.toolbar)
self.window.set_default_size(w, h)
self._destroying = False
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
self.window.connect("delete_event", lambda *args: Gcf.destroy(self))
if mpl.is_interactive():
self.window.show()
self.canvas.draw_idle()
self.canvas.grab_focus()
def destroy(self, *args):
if self._destroying:
# Otherwise, this can be called twice when the user presses 'q',
# which calls Gcf.destroy(self), then this destroy(), then triggers
# Gcf.destroy(self) once again via
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
return
self._destroying = True
self.vbox.destroy()
self.window.destroy()
self.canvas.destroy()
if self.toolbar:
self.toolbar.destroy()
if (Gcf.get_num_fig_managers() == 0 and not mpl.is_interactive() and
Gtk.main_level() >= 1):
Gtk.main_quit()
def show(self):
# show the figure window
self.window.show()
self.canvas.draw()
if mpl.rcParams['figure.raise_window']:
self.window.present()
def full_screen_toggle(self):
self._full_screen_flag = not self._full_screen_flag
if self._full_screen_flag:
self.window.fullscreen()
else:
self.window.unfullscreen()
_full_screen_flag = False
def _get_toolbar(self):
# must be inited after the window, drawingArea and figure
# attrs are set
if mpl.rcParams['toolbar'] == 'toolbar2':
toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
elif mpl.rcParams['toolbar'] == 'toolmanager':
toolbar = ToolbarGTK3(self.toolmanager)
else:
toolbar = None
return toolbar
def get_window_title(self):
return self.window.get_title()
def set_window_title(self, title):
self.window.set_title(title)
def resize(self, width, height):
"""Set the canvas size in pixels."""
if self.toolbar:
toolbar_size = self.toolbar.size_request()
height += toolbar_size.height
canvas_size = self.canvas.get_allocation()
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
# sized, so just change the window's starting size.
self.window.set_default_size(width, height)
else:
self.window.resize(width, height)
class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
def __init__(self, canvas, window):
self.win = window
GObject.GObject.__init__(self)
self.set_style(Gtk.ToolbarStyle.ICONS)
self._gtk_ids = {}
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
self.insert(Gtk.SeparatorToolItem(), -1)
continue
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))),
Gtk.IconSize.LARGE_TOOLBAR)
self._gtk_ids[text] = tbutton = (
Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
Gtk.ToolButton())
tbutton.set_label(text)
tbutton.set_icon_widget(image)
self.insert(tbutton, -1)
# Save the handler id, so that we can block it as needed.
tbutton._signal_handler = tbutton.connect(
'clicked', getattr(self, callback))
tbutton.set_tooltip_text(tooltip_text)
toolitem = Gtk.SeparatorToolItem()
self.insert(toolitem, -1)
toolitem.set_draw(False)
toolitem.set_expand(True)
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
label = Gtk.Label()
label.set_markup(
'\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}')
toolitem.add(label)
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
self.message = Gtk.Label()
toolitem.add(self.message)
self.show_all()
NavigationToolbar2.__init__(self, canvas)
@cbook.deprecated("3.3")
@property
def ctx(self):
return self.canvas.get_property("window").cairo_create()
def set_message(self, s):
escaped = GLib.markup_escape_text(s)
self.message.set_markup(f'{escaped}')
def set_cursor(self, cursor):
window = self.canvas.get_property("window")
if window is not None:
window.set_cursor(cursord[cursor])
Gtk.main_iteration()
def draw_rubberband(self, event, x0, y0, x1, y1):
height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
self.canvas._draw_rubberband(rect)
def remove_rubberband(self):
self.canvas._draw_rubberband(None)
def _update_buttons_checked(self):
for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
button = self._gtk_ids.get(name)
if button:
with button.handler_block(button._signal_handler):
button.set_active(self.mode.name == active)
def pan(self, *args):
super().pan(*args)
self._update_buttons_checked()
def zoom(self, *args):
super().zoom(*args)
self._update_buttons_checked()
def save_figure(self, *args):
dialog = Gtk.FileChooserDialog(
title="Save the figure",
parent=self.canvas.get_toplevel(),
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
)
for name, fmts \
in self.canvas.get_supported_filetypes_grouped().items():
ff = Gtk.FileFilter()
ff.set_name(name)
for fmt in fmts:
ff.add_pattern("*." + fmt)
dialog.add_filter(ff)
if self.canvas.get_default_filetype() in fmts:
dialog.set_filter(ff)
@functools.partial(dialog.connect, "notify::filter")
def on_notify_filter(*args):
name = dialog.get_filter().get_name()
fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
dialog.set_current_name(
str(Path(dialog.get_current_name()).with_suffix("." + fmt)))
dialog.set_current_folder(mpl.rcParams["savefig.directory"])
dialog.set_current_name(self.canvas.get_default_filename())
dialog.set_do_overwrite_confirmation(True)
response = dialog.run()
fname = dialog.get_filename()
ff = dialog.get_filter() # Doesn't autoadjust to filename :/
fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
dialog.destroy()
if response != Gtk.ResponseType.OK:
return
# Save dir for next time, unless empty str (which means use cwd).
if mpl.rcParams['savefig.directory']:
mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
try:
self.canvas.figure.savefig(fname, format=fmt)
except Exception as e:
error_msg_gtk(str(e), parent=self)
def configure_subplots(self, button):
toolfig = Figure(figsize=(6, 3))
canvas = type(self.canvas)(toolfig)
toolfig.subplots_adjust(top=0.9)
# Need to keep a reference to the tool.
_tool = SubplotTool(self.canvas.figure, toolfig)
w = int(toolfig.bbox.width)
h = int(toolfig.bbox.height)
window = Gtk.Window()
try:
window.set_icon_from_file(window_icon)
except Exception:
# we presumably already logged a message on the
# failure of the main plot, don't keep reporting
pass
window.set_title("Subplot Configuration Tool")
window.set_default_size(w, h)
vbox = Gtk.Box()
vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
window.add(vbox)
vbox.show()
canvas.show()
vbox.pack_start(canvas, True, True, 0)
window.show()
def set_history_buttons(self):
can_backward = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
if 'Back' in self._gtk_ids:
self._gtk_ids['Back'].set_sensitive(can_backward)
if 'Forward' in self._gtk_ids:
self._gtk_ids['Forward'].set_sensitive(can_forward)
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
_icon_extension = '-symbolic.svg'
def __init__(self, toolmanager):
ToolContainerBase.__init__(self, toolmanager)
Gtk.Box.__init__(self)
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
self._message = Gtk.Label()
self.pack_end(self._message, False, False, 0)
self.show_all()
self._groups = {}
self._toolitems = {}
def add_toolitem(self, name, group, position, image_file, description,
toggle):
if toggle:
tbutton = Gtk.ToggleToolButton()
else:
tbutton = Gtk.ToolButton()
tbutton.set_label(name)
if image_file is not None:
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(image_file),
Gtk.IconSize.LARGE_TOOLBAR)
tbutton.set_icon_widget(image)
if position is None:
position = -1
self._add_button(tbutton, group, position)
signal = tbutton.connect('clicked', self._call_tool, name)
tbutton.set_tooltip_text(description)
tbutton.show_all()
self._toolitems.setdefault(name, [])
self._toolitems[name].append((tbutton, signal))
def _add_button(self, button, group, position):
if group not in self._groups:
if self._groups:
self._add_separator()
toolbar = Gtk.Toolbar()
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
self.pack_start(toolbar, False, False, 0)
toolbar.show_all()
self._groups[group] = toolbar
self._groups[group].insert(button, position)
def _call_tool(self, btn, name):
self.trigger_tool(name)
def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return
for toolitem, signal in self._toolitems[name]:
toolitem.handler_block(signal)
toolitem.set_active(toggled)
toolitem.handler_unblock(signal)
def remove_toolitem(self, name):
if name not in self._toolitems:
self.toolmanager.message_event('%s Not in toolbar' % name, self)
return
for group in self._groups:
for toolitem, _signal in self._toolitems[name]:
if toolitem in self._groups[group]:
self._groups[group].remove(toolitem)
del self._toolitems[name]
def _add_separator(self):
sep = Gtk.Separator()
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
self.pack_start(sep, False, True, 0)
sep.show_all()
def set_message(self, s):
self._message.set_label(s)
@cbook.deprecated("3.3")
class StatusbarGTK3(StatusbarBase, Gtk.Statusbar):
def __init__(self, *args, **kwargs):
StatusbarBase.__init__(self, *args, **kwargs)
Gtk.Statusbar.__init__(self)
self._context = self.get_context_id('message')
def set_message(self, s):
self.pop(self._context)
self.push(self._context, s)
class RubberbandGTK3(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
NavigationToolbar2GTK3.draw_rubberband(
self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
def remove_rubberband(self):
NavigationToolbar2GTK3.remove_rubberband(
self._make_classic_style_pseudo_toolbar())
class SaveFigureGTK3(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):
class PseudoToolbar:
canvas = self.figure.canvas
return NavigationToolbar2GTK3.save_figure(PseudoToolbar())
class SetCursorGTK3(backend_tools.SetCursorBase):
def set_cursor(self, cursor):
NavigationToolbar2GTK3.set_cursor(
self._make_classic_style_pseudo_toolbar(), cursor)
class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
@cbook.deprecated("3.2")
@property
def window(self):
if not hasattr(self, "_window"):
self._window = None
return self._window
@window.setter
@cbook.deprecated("3.2")
def window(self, window):
self._window = window
@cbook.deprecated("3.2")
def init_window(self):
if self.window:
return
self.window = Gtk.Window(title="Subplot Configuration Tool")
try:
self.window.window.set_icon_from_file(window_icon)
except Exception:
# we presumably already logged a message on the
# failure of the main plot, don't keep reporting
pass
self.vbox = Gtk.Box()
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
self.window.add(self.vbox)
self.vbox.show()
self.window.connect('destroy', self.destroy)
toolfig = Figure(figsize=(6, 3))
canvas = self.figure.canvas.__class__(toolfig)
toolfig.subplots_adjust(top=0.9)
SubplotTool(self.figure, toolfig)
w = int(toolfig.bbox.width)
h = int(toolfig.bbox.height)
self.window.set_default_size(w, h)
canvas.show()
self.vbox.pack_start(canvas, True, True, 0)
self.window.show()
@cbook.deprecated("3.2")
def destroy(self, *args):
self.window.destroy()
self.window = None
def _get_canvas(self, fig):
return self.canvas.__class__(fig)
def trigger(self, *args):
NavigationToolbar2GTK3.configure_subplots(
self._make_classic_style_pseudo_toolbar(), None)
class HelpGTK3(backend_tools.ToolHelpBase):
def _normalize_shortcut(self, key):
"""
Convert Matplotlib key presses to GTK+ accelerator identifiers.
Related to `FigureCanvasGTK3._get_key`.
"""
special = {
'backspace': 'BackSpace',
'pagedown': 'Page_Down',
'pageup': 'Page_Up',
'scroll_lock': 'Scroll_Lock',
}
parts = key.split('+')
mods = ['<' + mod + '>' for mod in parts[:-1]]
key = parts[-1]
if key in special:
key = special[key]
elif len(key) > 1:
key = key.capitalize()
elif key.isupper():
mods += ['']
return ''.join(mods) + key
def _is_valid_shortcut(self, key):
"""
Check for a valid shortcut to be displayed.
- GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
"""
return 'cmd+' not in key and not key.startswith('MouseButton.')
def _show_shortcuts_window(self):
section = Gtk.ShortcutsSection()
for name, tool in sorted(self.toolmanager.tools.items()):
if not tool.description:
continue
# Putting everything in a separate group allows GTK to
# automatically split them into separate columns/pages, which is
# useful because we have lots of shortcuts, some with many keys
# that are very wide.
group = Gtk.ShortcutsGroup()
section.add(group)
# A hack to remove the title since we have no group naming.
group.forall(lambda widget, data: widget.set_visible(False), None)
shortcut = Gtk.ShortcutsShortcut(
accelerator=' '.join(
self._normalize_shortcut(key)
for key in self.toolmanager.get_tool_keymap(name)
if self._is_valid_shortcut(key)),
title=tool.name,
subtitle=tool.description)
group.add(shortcut)
window = Gtk.ShortcutsWindow(
title='Help',
modal=True,
transient_for=self._figure.canvas.get_toplevel())
section.show() # Must be done explicitly before add!
window.add(section)
window.show_all()
def _show_shortcuts_dialog(self):
dialog = Gtk.MessageDialog(
self._figure.canvas.get_toplevel(),
0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
title="Help")
dialog.run()
dialog.destroy()
def trigger(self, *args):
if Gtk.check_version(3, 20, 0) is None:
self._show_shortcuts_window()
else:
self._show_shortcuts_dialog()
class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
window = self.canvas.get_window()
x, y, width, height = window.get_geometry()
pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
clipboard.set_image(pb)
# Define the file to use as the GTk icon
if sys.platform == 'win32':
icon_filename = 'matplotlib.png'
else:
icon_filename = 'matplotlib.svg'
window_icon = str(cbook._get_data_path('images', icon_filename))
def error_msg_gtk(msg, parent=None):
if parent is not None: # find the toplevel Gtk.Window
parent = parent.get_toplevel()
if not parent.is_toplevel():
parent = None
if not isinstance(msg, str):
msg = ','.join(map(str, msg))
dialog = Gtk.MessageDialog(
parent=parent, type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK,
message_format=msg)
dialog.run()
dialog.destroy()
backend_tools.ToolSaveFigure = SaveFigureGTK3
backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
backend_tools.ToolSetCursor = SetCursorGTK3
backend_tools.ToolRubberband = RubberbandGTK3
backend_tools.ToolHelp = HelpGTK3
backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3
Toolbar = ToolbarGTK3
@_Backend.export
class _BackendGTK3(_Backend):
FigureCanvas = FigureCanvasGTK3
FigureManager = FigureManagerGTK3
@staticmethod
def trigger_manager_draw(manager):
manager.canvas.draw_idle()
@staticmethod
def mainloop():
if Gtk.main_level() == 0:
Gtk.main()