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