1from __future__ import (absolute_import, division, print_function,
2                        unicode_literals)
3
4import six
5
6import logging
7import os
8import sys
9
10import matplotlib
11from matplotlib import backend_tools, rcParams
12from matplotlib._pylab_helpers import Gcf
13from matplotlib.backend_bases import (
14    _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
15    StatusbarBase, TimerBase, ToolContainerBase, cursors)
16from matplotlib.backend_managers import ToolManager
17from matplotlib.figure import Figure
18from matplotlib.widgets import SubplotTool
19from ._gtk3_compat import GLib, GObject, Gtk, Gdk
20
21
22_log = logging.getLogger(__name__)
23
24backend_version = "%s.%s.%s" % (
25    Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version())
26
27# the true dots per inch on the screen; should be display dependent
28# see http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5 for some info about screen dpi
29PIXELS_PER_INCH = 96
30
31cursord = {
32    cursors.MOVE          : Gdk.Cursor.new(Gdk.CursorType.FLEUR),
33    cursors.HAND          : Gdk.Cursor.new(Gdk.CursorType.HAND2),
34    cursors.POINTER       : Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR),
35    cursors.SELECT_REGION : Gdk.Cursor.new(Gdk.CursorType.TCROSS),
36    cursors.WAIT          : Gdk.Cursor.new(Gdk.CursorType.WATCH),
37    }
38
39
40class TimerGTK3(TimerBase):
41    '''
42    Subclass of :class:`backend_bases.TimerBase` using GTK3 for timer events.
43
44    Attributes
45    ----------
46    interval : int
47        The time between timer events in milliseconds. Default is 1000 ms.
48    single_shot : bool
49        Boolean flag indicating whether this timer should operate as single
50        shot (run once and then stop). Defaults to False.
51    callbacks : list
52        Stores list of (func, args) tuples that will be called upon timer
53        events. This list can be manipulated directly, or the functions
54        `add_callback` and `remove_callback` can be used.
55
56    '''
57    def _timer_start(self):
58        # Need to stop it, otherwise we potentially leak a timer id that will
59        # never be stopped.
60        self._timer_stop()
61        self._timer = GLib.timeout_add(self._interval, self._on_timer)
62
63    def _timer_stop(self):
64        if self._timer is not None:
65            GLib.source_remove(self._timer)
66            self._timer = None
67
68    def _timer_set_interval(self):
69        # Only stop and restart it if the timer has already been started
70        if self._timer is not None:
71            self._timer_stop()
72            self._timer_start()
73
74    def _on_timer(self):
75        TimerBase._on_timer(self)
76
77        # Gtk timeout_add() requires that the callback returns True if it
78        # is to be called again.
79        if len(self.callbacks) > 0 and not self._single:
80            return True
81        else:
82            self._timer = None
83            return False
84
85
86class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
87    keyvald = {65507 : 'control',
88               65505 : 'shift',
89               65513 : 'alt',
90               65508 : 'control',
91               65506 : 'shift',
92               65514 : 'alt',
93               65361 : 'left',
94               65362 : 'up',
95               65363 : 'right',
96               65364 : 'down',
97               65307 : 'escape',
98               65470 : 'f1',
99               65471 : 'f2',
100               65472 : 'f3',
101               65473 : 'f4',
102               65474 : 'f5',
103               65475 : 'f6',
104               65476 : 'f7',
105               65477 : 'f8',
106               65478 : 'f9',
107               65479 : 'f10',
108               65480 : 'f11',
109               65481 : 'f12',
110               65300 : 'scroll_lock',
111               65299 : 'break',
112               65288 : 'backspace',
113               65293 : 'enter',
114               65379 : 'insert',
115               65535 : 'delete',
116               65360 : 'home',
117               65367 : 'end',
118               65365 : 'pageup',
119               65366 : 'pagedown',
120               65438 : '0',
121               65436 : '1',
122               65433 : '2',
123               65435 : '3',
124               65430 : '4',
125               65437 : '5',
126               65432 : '6',
127               65429 : '7',
128               65431 : '8',
129               65434 : '9',
130               65451 : '+',
131               65453 : '-',
132               65450 : '*',
133               65455 : '/',
134               65439 : 'dec',
135               65421 : 'enter',
136               }
137
138    # Setting this as a static constant prevents
139    # this resulting expression from leaking
140    event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK   |
141                  Gdk.EventMask.BUTTON_RELEASE_MASK |
142                  Gdk.EventMask.EXPOSURE_MASK       |
143                  Gdk.EventMask.KEY_PRESS_MASK      |
144                  Gdk.EventMask.KEY_RELEASE_MASK    |
145                  Gdk.EventMask.ENTER_NOTIFY_MASK   |
146                  Gdk.EventMask.LEAVE_NOTIFY_MASK   |
147                  Gdk.EventMask.POINTER_MOTION_MASK |
148                  Gdk.EventMask.POINTER_MOTION_HINT_MASK|
149                  Gdk.EventMask.SCROLL_MASK)
150
151    def __init__(self, figure):
152        FigureCanvasBase.__init__(self, figure)
153        GObject.GObject.__init__(self)
154
155        self._idle_draw_id  = 0
156        self._lastCursor    = None
157
158        self.connect('scroll_event',         self.scroll_event)
159        self.connect('button_press_event',   self.button_press_event)
160        self.connect('button_release_event', self.button_release_event)
161        self.connect('configure_event',      self.configure_event)
162        self.connect('draw',                 self.on_draw_event)
163        self.connect('key_press_event',      self.key_press_event)
164        self.connect('key_release_event',    self.key_release_event)
165        self.connect('motion_notify_event',  self.motion_notify_event)
166        self.connect('leave_notify_event',   self.leave_notify_event)
167        self.connect('enter_notify_event',   self.enter_notify_event)
168        self.connect('size_allocate',        self.size_allocate)
169
170        self.set_events(self.__class__.event_mask)
171
172        self.set_double_buffered(True)
173        self.set_can_focus(True)
174        self._renderer_init()
175        default_context = GLib.main_context_get_thread_default() or GLib.main_context_default()
176
177    def destroy(self):
178        #Gtk.DrawingArea.destroy(self)
179        self.close_event()
180        if self._idle_draw_id != 0:
181            GLib.source_remove(self._idle_draw_id)
182
183    def scroll_event(self, widget, event):
184        x = event.x
185        # flipy so y=0 is bottom of canvas
186        y = self.get_allocation().height - event.y
187        if event.direction==Gdk.ScrollDirection.UP:
188            step = 1
189        else:
190            step = -1
191        FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
192        return False  # finish event propagation?
193
194    def button_press_event(self, widget, event):
195        x = event.x
196        # flipy so y=0 is bottom of canvas
197        y = self.get_allocation().height - event.y
198        FigureCanvasBase.button_press_event(self, x, y, event.button, guiEvent=event)
199        return False  # finish event propagation?
200
201    def button_release_event(self, widget, event):
202        x = event.x
203        # flipy so y=0 is bottom of canvas
204        y = self.get_allocation().height - event.y
205        FigureCanvasBase.button_release_event(self, x, y, event.button, guiEvent=event)
206        return False  # finish event propagation?
207
208    def key_press_event(self, widget, event):
209        key = self._get_key(event)
210        FigureCanvasBase.key_press_event(self, key, guiEvent=event)
211        return True  # stop event propagation
212
213    def key_release_event(self, widget, event):
214        key = self._get_key(event)
215        FigureCanvasBase.key_release_event(self, key, guiEvent=event)
216        return True  # stop event propagation
217
218    def motion_notify_event(self, widget, event):
219        if event.is_hint:
220            t, x, y, state = event.window.get_pointer()
221        else:
222            x, y, state = event.x, event.y, event.get_state()
223
224        # flipy so y=0 is bottom of canvas
225        y = self.get_allocation().height - y
226        FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
227        return False  # finish event propagation?
228
229    def leave_notify_event(self, widget, event):
230        FigureCanvasBase.leave_notify_event(self, event)
231
232    def enter_notify_event(self, widget, event):
233        FigureCanvasBase.enter_notify_event(self, event)
234
235    def size_allocate(self, widget, allocation):
236        dpival = self.figure.dpi
237        winch = allocation.width / dpival
238        hinch = allocation.height / dpival
239        self.figure.set_size_inches(winch, hinch, forward=False)
240        FigureCanvasBase.resize_event(self)
241        self.draw_idle()
242
243    def _get_key(self, event):
244        if event.keyval in self.keyvald:
245            key = self.keyvald[event.keyval]
246        elif event.keyval < 256:
247            key = chr(event.keyval)
248        else:
249            key = None
250
251        modifiers = [
252                     (Gdk.ModifierType.MOD4_MASK, 'super'),
253                     (Gdk.ModifierType.MOD1_MASK, 'alt'),
254                     (Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
255                    ]
256        for key_mask, prefix in modifiers:
257            if event.state & key_mask:
258                key = '{0}+{1}'.format(prefix, key)
259
260        return key
261
262    def configure_event(self, widget, event):
263        if widget.get_property("window") is None:
264            return
265        w, h = event.width, event.height
266        if w < 3 or h < 3:
267            return # empty fig
268        # resize the figure (in inches)
269        dpi = self.figure.dpi
270        self.figure.set_size_inches(w/dpi, h/dpi, forward=False)
271        return False  # finish event propagation?
272
273    def on_draw_event(self, widget, ctx):
274        # to be overwritten by GTK3Agg or GTK3Cairo
275        pass
276
277    def draw(self):
278        if self.get_visible() and self.get_mapped():
279            self.queue_draw()
280            # do a synchronous draw (its less efficient than an async draw,
281            # but is required if/when animation is used)
282            self.get_property("window").process_updates (False)
283
284    def draw_idle(self):
285        if self._idle_draw_id != 0:
286            return
287        def idle_draw(*args):
288            try:
289                self.draw()
290            finally:
291                self._idle_draw_id = 0
292            return False
293        self._idle_draw_id = GLib.idle_add(idle_draw)
294
295    def new_timer(self, *args, **kwargs):
296        """
297        Creates a new backend-specific subclass of :class:`backend_bases.Timer`.
298        This is useful for getting periodic events through the backend's native
299        event loop. Implemented only for backends with GUIs.
300
301        Other Parameters
302        ----------------
303        interval : scalar
304            Timer interval in milliseconds
305        callbacks : list
306            Sequence of (func, args, kwargs) where ``func(*args, **kwargs)``
307            will be executed by the timer every *interval*.
308        """
309        return TimerGTK3(*args, **kwargs)
310
311    def flush_events(self):
312        Gdk.threads_enter()
313        while Gtk.events_pending():
314            Gtk.main_iteration()
315        Gdk.flush()
316        Gdk.threads_leave()
317
318
319class FigureManagerGTK3(FigureManagerBase):
320    """
321    Attributes
322    ----------
323    canvas : `FigureCanvas`
324        The FigureCanvas instance
325    num : int or str
326        The Figure number
327    toolbar : Gtk.Toolbar
328        The Gtk.Toolbar  (gtk only)
329    vbox : Gtk.VBox
330        The Gtk.VBox containing the canvas and toolbar (gtk only)
331    window : Gtk.Window
332        The Gtk.Window   (gtk only)
333
334    """
335    def __init__(self, canvas, num):
336        FigureManagerBase.__init__(self, canvas, num)
337
338        self.window = Gtk.Window()
339        self.window.set_wmclass("matplotlib", "Matplotlib")
340        self.set_window_title("Figure %d" % num)
341        try:
342            self.window.set_icon_from_file(window_icon)
343        except (SystemExit, KeyboardInterrupt):
344            # re-raise exit type Exceptions
345            raise
346        except:
347            # some versions of gtk throw a glib.GError but not
348            # all, so I am not sure how to catch it.  I am unhappy
349            # doing a blanket catch here, but am not sure what a
350            # better way is - JDH
351            _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1])
352
353        self.vbox = Gtk.Box()
354        self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
355        self.window.add(self.vbox)
356        self.vbox.show()
357
358        self.canvas.show()
359
360        self.vbox.pack_start(self.canvas, True, True, 0)
361        # calculate size for window
362        w = int (self.canvas.figure.bbox.width)
363        h = int (self.canvas.figure.bbox.height)
364
365        self.toolmanager = self._get_toolmanager()
366        self.toolbar = self._get_toolbar()
367        self.statusbar = None
368
369        def add_widget(child, expand, fill, padding):
370            child.show()
371            self.vbox.pack_end(child, False, False, 0)
372            size_request = child.size_request()
373            return size_request.height
374
375        if self.toolmanager:
376            backend_tools.add_tools_to_manager(self.toolmanager)
377            if self.toolbar:
378                backend_tools.add_tools_to_container(self.toolbar)
379                self.statusbar = StatusbarGTK3(self.toolmanager)
380                h += add_widget(self.statusbar, False, False, 0)
381                h += add_widget(Gtk.HSeparator(), False, False, 0)
382
383        if self.toolbar is not None:
384            self.toolbar.show()
385            h += add_widget(self.toolbar, False, False, 0)
386
387        self.window.set_default_size (w, h)
388
389        def destroy(*args):
390            Gcf.destroy(num)
391        self.window.connect("destroy", destroy)
392        self.window.connect("delete_event", destroy)
393        if matplotlib.is_interactive():
394            self.window.show()
395            self.canvas.draw_idle()
396
397        def notify_axes_change(fig):
398            'this will be called whenever the current axes is changed'
399            if self.toolmanager is not None:
400                pass
401            elif self.toolbar is not None:
402                self.toolbar.update()
403        self.canvas.figure.add_axobserver(notify_axes_change)
404
405        self.canvas.grab_focus()
406
407    def destroy(self, *args):
408        self.vbox.destroy()
409        self.window.destroy()
410        self.canvas.destroy()
411        if self.toolbar:
412            self.toolbar.destroy()
413
414        if (Gcf.get_num_fig_managers() == 0 and
415                not matplotlib.is_interactive() and
416                Gtk.main_level() >= 1):
417            Gtk.main_quit()
418
419    def show(self):
420        # show the figure window
421        self.window.show()
422        self.window.present()
423
424    def full_screen_toggle (self):
425        self._full_screen_flag = not self._full_screen_flag
426        if self._full_screen_flag:
427            self.window.fullscreen()
428        else:
429            self.window.unfullscreen()
430    _full_screen_flag = False
431
432    def _get_toolbar(self):
433        # must be inited after the window, drawingArea and figure
434        # attrs are set
435        if rcParams['toolbar'] == 'toolbar2':
436            toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
437        elif rcParams['toolbar'] == 'toolmanager':
438            toolbar = ToolbarGTK3(self.toolmanager)
439        else:
440            toolbar = None
441        return toolbar
442
443    def _get_toolmanager(self):
444        # must be initialised after toolbar has been set
445        if rcParams['toolbar'] == 'toolmanager':
446            toolmanager = ToolManager(self.canvas.figure)
447        else:
448            toolmanager = None
449        return toolmanager
450
451    def get_window_title(self):
452        return self.window.get_title()
453
454    def set_window_title(self, title):
455        self.window.set_title(title)
456
457    def resize(self, width, height):
458        'set the canvas size in pixels'
459        #_, _, cw, ch = self.canvas.allocation
460        #_, _, ww, wh = self.window.allocation
461        #self.window.resize (width-cw+ww, height-ch+wh)
462        self.window.resize(width, height)
463
464
465class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
466    def __init__(self, canvas, window):
467        self.win = window
468        GObject.GObject.__init__(self)
469        NavigationToolbar2.__init__(self, canvas)
470        self.ctx = None
471
472    def set_message(self, s):
473        self.message.set_label(s)
474
475    def set_cursor(self, cursor):
476        self.canvas.get_property("window").set_cursor(cursord[cursor])
477        Gtk.main_iteration()
478
479    def release(self, event):
480        try: del self._pixmapBack
481        except AttributeError: pass
482
483    def draw_rubberband(self, event, x0, y0, x1, y1):
484        'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744'
485        self.ctx = self.canvas.get_property("window").cairo_create()
486
487        # todo: instead of redrawing the entire figure, copy the part of
488        # the figure that was covered by the previous rubberband rectangle
489        self.canvas.draw()
490
491        height = self.canvas.figure.bbox.height
492        y1 = height - y1
493        y0 = height - y0
494        w = abs(x1 - x0)
495        h = abs(y1 - y0)
496        rect = [int(val) for val in (min(x0,x1), min(y0, y1), w, h)]
497
498        self.ctx.new_path()
499        self.ctx.set_line_width(0.5)
500        self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
501        self.ctx.set_source_rgb(0, 0, 0)
502        self.ctx.stroke()
503
504    def _init_toolbar(self):
505        self.set_style(Gtk.ToolbarStyle.ICONS)
506        basedir = os.path.join(rcParams['datapath'],'images')
507
508        for text, tooltip_text, image_file, callback in self.toolitems:
509            if text is None:
510                self.insert( Gtk.SeparatorToolItem(), -1 )
511                continue
512            fname = os.path.join(basedir, image_file + '.png')
513            image = Gtk.Image()
514            image.set_from_file(fname)
515            tbutton = Gtk.ToolButton()
516            tbutton.set_label(text)
517            tbutton.set_icon_widget(image)
518            self.insert(tbutton, -1)
519            tbutton.connect('clicked', getattr(self, callback))
520            tbutton.set_tooltip_text(tooltip_text)
521
522        toolitem = Gtk.SeparatorToolItem()
523        self.insert(toolitem, -1)
524        toolitem.set_draw(False)
525        toolitem.set_expand(True)
526
527        toolitem = Gtk.ToolItem()
528        self.insert(toolitem, -1)
529        self.message = Gtk.Label()
530        toolitem.add(self.message)
531
532        self.show_all()
533
534    def get_filechooser(self):
535        fc = FileChooserDialog(
536            title='Save the figure',
537            parent=self.win,
538            path=os.path.expanduser(rcParams['savefig.directory']),
539            filetypes=self.canvas.get_supported_filetypes(),
540            default_filetype=self.canvas.get_default_filetype())
541        fc.set_current_name(self.canvas.get_default_filename())
542        return fc
543
544    def save_figure(self, *args):
545        chooser = self.get_filechooser()
546        fname, format = chooser.get_filename_from_user()
547        chooser.destroy()
548        if fname:
549            startpath = os.path.expanduser(rcParams['savefig.directory'])
550            # Save dir for next time, unless empty str (i.e., use cwd).
551            if startpath != "":
552                rcParams['savefig.directory'] = (
553                    os.path.dirname(six.text_type(fname)))
554            try:
555                self.canvas.figure.savefig(fname, format=format)
556            except Exception as e:
557                error_msg_gtk(str(e), parent=self)
558
559    def configure_subplots(self, button):
560        toolfig = Figure(figsize=(6,3))
561        canvas = self._get_canvas(toolfig)
562        toolfig.subplots_adjust(top=0.9)
563        tool =  SubplotTool(self.canvas.figure, toolfig)
564
565        w = int(toolfig.bbox.width)
566        h = int(toolfig.bbox.height)
567
568        window = Gtk.Window()
569        try:
570            window.set_icon_from_file(window_icon)
571        except (SystemExit, KeyboardInterrupt):
572            # re-raise exit type Exceptions
573            raise
574        except:
575            # we presumably already logged a message on the
576            # failure of the main plot, don't keep reporting
577            pass
578        window.set_title("Subplot Configuration Tool")
579        window.set_default_size(w, h)
580        vbox = Gtk.Box()
581        vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
582        window.add(vbox)
583        vbox.show()
584
585        canvas.show()
586        vbox.pack_start(canvas, True, True, 0)
587        window.show()
588
589    def _get_canvas(self, fig):
590        return self.canvas.__class__(fig)
591
592
593class FileChooserDialog(Gtk.FileChooserDialog):
594    """GTK+ file selector which remembers the last file/directory
595    selected and presents the user with a menu of supported image formats
596    """
597    def __init__ (self,
598                  title   = 'Save file',
599                  parent  = None,
600                  action  = Gtk.FileChooserAction.SAVE,
601                  buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
602                             Gtk.STOCK_SAVE,   Gtk.ResponseType.OK),
603                  path    = None,
604                  filetypes = [],
605                  default_filetype = None
606                  ):
607        super (FileChooserDialog, self).__init__ (title, parent, action,
608                                                  buttons)
609        self.set_default_response (Gtk.ResponseType.OK)
610
611        if not path: path = os.getcwd() + os.sep
612
613        # create an extra widget to list supported image formats
614        self.set_current_folder (path)
615        self.set_current_name ('image.' + default_filetype)
616
617        hbox = Gtk.Box(spacing=10)
618        hbox.pack_start(Gtk.Label(label="File Format:"), False, False, 0)
619
620        liststore = Gtk.ListStore(GObject.TYPE_STRING)
621        cbox = Gtk.ComboBox() #liststore)
622        cbox.set_model(liststore)
623        cell = Gtk.CellRendererText()
624        cbox.pack_start(cell, True)
625        cbox.add_attribute(cell, 'text', 0)
626        hbox.pack_start(cbox, False, False, 0)
627
628        self.filetypes = filetypes
629        self.sorted_filetypes = sorted(six.iteritems(filetypes))
630        default = 0
631        for i, (ext, name) in enumerate(self.sorted_filetypes):
632            liststore.append(["%s (*.%s)" % (name, ext)])
633            if ext == default_filetype:
634                default = i
635        cbox.set_active(default)
636        self.ext = default_filetype
637
638        def cb_cbox_changed (cbox, data=None):
639            """File extension changed"""
640            head, filename = os.path.split(self.get_filename())
641            root, ext = os.path.splitext(filename)
642            ext = ext[1:]
643            new_ext = self.sorted_filetypes[cbox.get_active()][0]
644            self.ext = new_ext
645
646            if ext in self.filetypes:
647                filename = root + '.' + new_ext
648            elif ext == '':
649                filename = filename.rstrip('.') + '.' + new_ext
650
651            self.set_current_name (filename)
652        cbox.connect ("changed", cb_cbox_changed)
653
654        hbox.show_all()
655        self.set_extra_widget(hbox)
656
657    def get_filename_from_user (self):
658        while True:
659            filename = None
660            if self.run() != int(Gtk.ResponseType.OK):
661                break
662            filename = self.get_filename()
663            break
664
665        return filename, self.ext
666
667
668class RubberbandGTK3(backend_tools.RubberbandBase):
669    def __init__(self, *args, **kwargs):
670        backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
671        self.ctx = None
672
673    def draw_rubberband(self, x0, y0, x1, y1):
674        # 'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/
675        # Recipe/189744'
676        self.ctx = self.figure.canvas.get_property("window").cairo_create()
677
678        # todo: instead of redrawing the entire figure, copy the part of
679        # the figure that was covered by the previous rubberband rectangle
680        self.figure.canvas.draw()
681
682        height = self.figure.bbox.height
683        y1 = height - y1
684        y0 = height - y0
685        w = abs(x1 - x0)
686        h = abs(y1 - y0)
687        rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)]
688
689        self.ctx.new_path()
690        self.ctx.set_line_width(0.5)
691        self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
692        self.ctx.set_source_rgb(0, 0, 0)
693        self.ctx.stroke()
694
695
696class ToolbarGTK3(ToolContainerBase, Gtk.Box):
697    _icon_extension = '.png'
698    def __init__(self, toolmanager):
699        ToolContainerBase.__init__(self, toolmanager)
700        Gtk.Box.__init__(self)
701        self.set_property("orientation", Gtk.Orientation.VERTICAL)
702
703        self._toolarea = Gtk.Box()
704        self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL)
705        self.pack_start(self._toolarea, False, False, 0)
706        self._toolarea.show_all()
707        self._groups = {}
708        self._toolitems = {}
709
710    def add_toolitem(self, name, group, position, image_file, description,
711                     toggle):
712        if toggle:
713            tbutton = Gtk.ToggleToolButton()
714        else:
715            tbutton = Gtk.ToolButton()
716        tbutton.set_label(name)
717
718        if image_file is not None:
719            image = Gtk.Image()
720            image.set_from_file(image_file)
721            tbutton.set_icon_widget(image)
722
723        if position is None:
724            position = -1
725
726        self._add_button(tbutton, group, position)
727        signal = tbutton.connect('clicked', self._call_tool, name)
728        tbutton.set_tooltip_text(description)
729        tbutton.show_all()
730        self._toolitems.setdefault(name, [])
731        self._toolitems[name].append((tbutton, signal))
732
733    def _add_button(self, button, group, position):
734        if group not in self._groups:
735            if self._groups:
736                self._add_separator()
737            toolbar = Gtk.Toolbar()
738            toolbar.set_style(Gtk.ToolbarStyle.ICONS)
739            self._toolarea.pack_start(toolbar, False, False, 0)
740            toolbar.show_all()
741            self._groups[group] = toolbar
742        self._groups[group].insert(button, position)
743
744    def _call_tool(self, btn, name):
745        self.trigger_tool(name)
746
747    def toggle_toolitem(self, name, toggled):
748        if name not in self._toolitems:
749            return
750        for toolitem, signal in self._toolitems[name]:
751            toolitem.handler_block(signal)
752            toolitem.set_active(toggled)
753            toolitem.handler_unblock(signal)
754
755    def remove_toolitem(self, name):
756        if name not in self._toolitems:
757            self.toolmanager.message_event('%s Not in toolbar' % name, self)
758            return
759
760        for group in self._groups:
761            for toolitem, _signal in self._toolitems[name]:
762                if toolitem in self._groups[group]:
763                    self._groups[group].remove(toolitem)
764        del self._toolitems[name]
765
766    def _add_separator(self):
767        sep = Gtk.Separator()
768        sep.set_property("orientation", Gtk.Orientation.VERTICAL)
769        self._toolarea.pack_start(sep, False, True, 0)
770        sep.show_all()
771
772
773class StatusbarGTK3(StatusbarBase, Gtk.Statusbar):
774    def __init__(self, *args, **kwargs):
775        StatusbarBase.__init__(self, *args, **kwargs)
776        Gtk.Statusbar.__init__(self)
777        self._context = self.get_context_id('message')
778
779    def set_message(self, s):
780        self.pop(self._context)
781        self.push(self._context, s)
782
783
784class SaveFigureGTK3(backend_tools.SaveFigureBase):
785
786    def get_filechooser(self):
787        fc = FileChooserDialog(
788            title='Save the figure',
789            parent=self.figure.canvas.manager.window,
790            path=os.path.expanduser(rcParams['savefig.directory']),
791            filetypes=self.figure.canvas.get_supported_filetypes(),
792            default_filetype=self.figure.canvas.get_default_filetype())
793        fc.set_current_name(self.figure.canvas.get_default_filename())
794        return fc
795
796    def trigger(self, *args, **kwargs):
797        chooser = self.get_filechooser()
798        fname, format_ = chooser.get_filename_from_user()
799        chooser.destroy()
800        if fname:
801            startpath = os.path.expanduser(rcParams['savefig.directory'])
802            if startpath == '':
803                # explicitly missing key or empty str signals to use cwd
804                rcParams['savefig.directory'] = startpath
805            else:
806                # save dir for next time
807                rcParams['savefig.directory'] = os.path.dirname(
808                    six.text_type(fname))
809            try:
810                self.figure.canvas.print_figure(fname, format=format_)
811            except Exception as e:
812                error_msg_gtk(str(e), parent=self)
813
814
815class SetCursorGTK3(backend_tools.SetCursorBase):
816    def set_cursor(self, cursor):
817        self.figure.canvas.get_property("window").set_cursor(cursord[cursor])
818
819
820class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
821    def __init__(self, *args, **kwargs):
822        backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
823        self.window = None
824
825    def init_window(self):
826        if self.window:
827            return
828        self.window = Gtk.Window(title="Subplot Configuration Tool")
829
830        try:
831            self.window.window.set_icon_from_file(window_icon)
832        except (SystemExit, KeyboardInterrupt):
833            # re-raise exit type Exceptions
834            raise
835        except:
836            # we presumably already logged a message on the
837            # failure of the main plot, don't keep reporting
838            pass
839
840        self.vbox = Gtk.Box()
841        self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
842        self.window.add(self.vbox)
843        self.vbox.show()
844        self.window.connect('destroy', self.destroy)
845
846        toolfig = Figure(figsize=(6, 3))
847        canvas = self.figure.canvas.__class__(toolfig)
848
849        toolfig.subplots_adjust(top=0.9)
850        SubplotTool(self.figure, toolfig)
851
852        w = int(toolfig.bbox.width)
853        h = int(toolfig.bbox.height)
854
855        self.window.set_default_size(w, h)
856
857        canvas.show()
858        self.vbox.pack_start(canvas, True, True, 0)
859        self.window.show()
860
861    def destroy(self, *args):
862        self.window.destroy()
863        self.window = None
864
865    def _get_canvas(self, fig):
866        return self.canvas.__class__(fig)
867
868    def trigger(self, sender, event, data=None):
869        self.init_window()
870        self.window.present()
871
872
873# Define the file to use as the GTk icon
874if sys.platform == 'win32':
875    icon_filename = 'matplotlib.png'
876else:
877    icon_filename = 'matplotlib.svg'
878window_icon = os.path.join(
879    matplotlib.rcParams['datapath'], 'images', icon_filename)
880
881
882def error_msg_gtk(msg, parent=None):
883    if parent is not None: # find the toplevel Gtk.Window
884        parent = parent.get_toplevel()
885        if not parent.is_toplevel():
886            parent = None
887
888    if not isinstance(msg, six.string_types):
889        msg = ','.join(map(str, msg))
890
891    dialog = Gtk.MessageDialog(
892        parent         = parent,
893        type           = Gtk.MessageType.ERROR,
894        buttons        = Gtk.ButtonsType.OK,
895        message_format = msg)
896    dialog.run()
897    dialog.destroy()
898
899
900backend_tools.ToolSaveFigure = SaveFigureGTK3
901backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
902backend_tools.ToolSetCursor = SetCursorGTK3
903backend_tools.ToolRubberband = RubberbandGTK3
904
905Toolbar = ToolbarGTK3
906
907
908@_Backend.export
909class _BackendGTK3(_Backend):
910    FigureCanvas = FigureCanvasGTK3
911    FigureManager = FigureManagerGTK3
912
913    @staticmethod
914    def trigger_manager_draw(manager):
915        manager.canvas.draw_idle()
916
917    @staticmethod
918    def mainloop():
919        if Gtk.main_level() == 0:
920            Gtk.main()
921