1 |
from __future__ import division |
2 |
|
3 |
import os, sys |
4 |
def fn_name(): return sys._getframe(1).f_code.co_name |
5 |
|
6 |
try: |
7 |
from gi.repository import Gtk, Gdk, GObject |
8 |
from gi.repository import Pango |
9 |
import cairo |
10 |
except ImportError: |
11 |
raise ImportError("GTK3 backend requires pygobject to be installed.") |
12 |
|
13 |
import matplotlib |
14 |
from matplotlib._pylab_helpers import Gcf |
15 |
from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ |
16 |
FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase |
17 |
from matplotlib.backend_bases import ShowBase |
18 |
from matplotlib.backends import backend_cairo |
19 |
|
20 |
from matplotlib.cbook import is_string_like, is_writable_file_like |
21 |
from matplotlib.colors import colorConverter |
22 |
from matplotlib.figure import Figure |
23 |
from matplotlib.widgets import SubplotTool |
24 |
|
25 |
from matplotlib import lines |
26 |
from matplotlib import cbook |
27 |
from matplotlib import verbose |
28 |
|
29 |
backend_version = "0.1.0" |
30 |
|
31 |
_debug = False |
32 |
#_debug = True |
33 |
|
34 |
if _debug: print "using backend_gtk3" |
35 |
|
36 |
# the true dots per inch on the screen; should be display dependent |
37 |
# 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 |
38 |
PIXELS_PER_INCH = 96 |
39 |
|
40 |
cursord = { |
41 |
cursors.MOVE : Gdk.Cursor.new(Gdk.CursorType.FLEUR), |
42 |
cursors.HAND : Gdk.Cursor.new(Gdk.CursorType.HAND2), |
43 |
cursors.POINTER : Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR), |
44 |
cursors.SELECT_REGION : Gdk.Cursor.new(Gdk.CursorType.TCROSS), |
45 |
} |
46 |
|
47 |
def draw_if_interactive(): |
48 |
""" |
49 |
Is called after every pylab drawing command |
50 |
""" |
51 |
if matplotlib.is_interactive(): |
52 |
figManager = Gcf.get_active() |
53 |
if figManager is not None: |
54 |
figManager.canvas.draw_idle() |
55 |
|
56 |
class Show(ShowBase): |
57 |
def mainloop(self): |
58 |
if Gtk.main_level() == 0: |
59 |
Gtk.main() |
60 |
|
61 |
show = Show() |
62 |
|
63 |
def new_figure_manager(num, *args, **kwargs): |
64 |
""" |
65 |
Create a new figure manager instance |
66 |
""" |
67 |
FigureClass = kwargs.pop('FigureClass', Figure) |
68 |
thisFig = FigureClass(*args, **kwargs) |
69 |
canvas = FigureCanvasGTK3(thisFig) |
70 |
manager = FigureManagerGTK3(canvas, num) |
71 |
return manager |
72 |
|
73 |
|
74 |
class TimerGTK3(TimerBase): |
75 |
''' |
76 |
Subclass of :class:`backend_bases.TimerBase` that uses GTK3 for timer events. |
77 |
|
78 |
Attributes: |
79 |
* interval: The time between timer events in milliseconds. Default |
80 |
is 1000 ms. |
81 |
* single_shot: Boolean flag indicating whether this timer should |
82 |
operate as single shot (run once and then stop). Defaults to False. |
83 |
* callbacks: Stores list of (func, args) tuples that will be called |
84 |
upon timer events. This list can be manipulated directly, or the |
85 |
functions add_callback and remove_callback can be used. |
86 |
''' |
87 |
def _timer_start(self): |
88 |
# Need to stop it, otherwise we potentially leak a timer id that will |
89 |
# never be stopped. |
90 |
self._timer_stop() |
91 |
self._timer = GObject.timeout_add(self._interval, self._on_timer) |
92 |
|
93 |
def _timer_stop(self): |
94 |
if self._timer is not None: |
95 |
GObject.source_remove(self._timer) |
96 |
self._timer = None |
97 |
|
98 |
def _timer_set_interval(self): |
99 |
# Only stop and restart it if the timer has already been started |
100 |
if self._timer is not None: |
101 |
self._timer_stop() |
102 |
self._timer_start() |
103 |
|
104 |
def _on_timer(self): |
105 |
TimerBase._on_timer(self) |
106 |
|
107 |
# Gtk timeout_add() requires that the callback returns True if it |
108 |
# is to be called again. |
109 |
if len(self.callbacks) > 0 and not self._single: |
110 |
return True |
111 |
else: |
112 |
self._timer = None |
113 |
return False |
114 |
|
115 |
class RendererGTK3Cairo (backend_cairo.RendererCairo): |
116 |
def set_context (self, ctx): |
117 |
self.gc.ctx = ctx |
118 |
|
119 |
class FigureCanvasGTK3 (Gtk.DrawingArea, backend_cairo.FigureCanvasCairo): |
120 |
keyvald = {65507 : 'control', |
121 |
65505 : 'shift', |
122 |
65513 : 'alt', |
123 |
65508 : 'control', |
124 |
65506 : 'shift', |
125 |
65514 : 'alt', |
126 |
65361 : 'left', |
127 |
65362 : 'up', |
128 |
65363 : 'right', |
129 |
65364 : 'down', |
130 |
65307 : 'escape', |
131 |
65470 : 'f1', |
132 |
65471 : 'f2', |
133 |
65472 : 'f3', |
134 |
65473 : 'f4', |
135 |
65474 : 'f5', |
136 |
65475 : 'f6', |
137 |
65476 : 'f7', |
138 |
65477 : 'f8', |
139 |
65478 : 'f9', |
140 |
65479 : 'f10', |
141 |
65480 : 'f11', |
142 |
65481 : 'f12', |
143 |
65300 : 'scroll_lock', |
144 |
65299 : 'break', |
145 |
65288 : 'backspace', |
146 |
65293 : 'enter', |
147 |
65379 : 'insert', |
148 |
65535 : 'delete', |
149 |
65360 : 'home', |
150 |
65367 : 'end', |
151 |
65365 : 'pageup', |
152 |
65366 : 'pagedown', |
153 |
65438 : '0', |
154 |
65436 : '1', |
155 |
65433 : '2', |
156 |
65435 : '3', |
157 |
65430 : '4', |
158 |
65437 : '5', |
159 |
65432 : '6', |
160 |
65429 : '7', |
161 |
65431 : '8', |
162 |
65434 : '9', |
163 |
65451 : '+', |
164 |
65453 : '-', |
165 |
65450 : '*', |
166 |
65455 : '/', |
167 |
65439 : 'dec', |
168 |
65421 : 'enter', |
169 |
} |
170 |
|
171 |
# Setting this as a static constant prevents |
172 |
# this resulting expression from leaking |
173 |
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK | |
174 |
Gdk.EventMask.BUTTON_RELEASE_MASK | |
175 |
Gdk.EventMask.EXPOSURE_MASK | |
176 |
Gdk.EventMask.KEY_PRESS_MASK | |
177 |
Gdk.EventMask.KEY_RELEASE_MASK | |
178 |
Gdk.EventMask.ENTER_NOTIFY_MASK | |
179 |
Gdk.EventMask.LEAVE_NOTIFY_MASK | |
180 |
Gdk.EventMask.POINTER_MOTION_MASK | |
181 |
Gdk.EventMask.POINTER_MOTION_HINT_MASK) |
182 |
|
183 |
def __init__(self, figure): |
184 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
185 |
FigureCanvasBase.__init__(self, figure) |
186 |
GObject.GObject.__init__(self) |
187 |
|
188 |
self._idle_draw_id = 0 |
189 |
self._need_redraw = True |
190 |
self._lastCursor = None |
191 |
|
192 |
self.connect('scroll_event', self.scroll_event) |
193 |
self.connect('button_press_event', self.button_press_event) |
194 |
self.connect('button_release_event', self.button_release_event) |
195 |
self.connect('configure_event', self.configure_event) |
196 |
self.connect('draw', self.on_draw_event) |
197 |
self.connect('key_press_event', self.key_press_event) |
198 |
self.connect('key_release_event', self.key_release_event) |
199 |
self.connect('motion_notify_event', self.motion_notify_event) |
200 |
self.connect('leave_notify_event', self.leave_notify_event) |
201 |
self.connect('enter_notify_event', self.enter_notify_event) |
202 |
|
203 |
self.set_events(self.__class__.event_mask) |
204 |
|
205 |
self.set_double_buffered(False) |
206 |
self.set_can_focus(True) |
207 |
self._renderer_init() |
208 |
self._idle_event_id = GObject.idle_add(self.idle_event) |
209 |
|
210 |
def destroy(self): |
211 |
#Gtk.DrawingArea.destroy(self) |
212 |
self.close_event() |
213 |
GObject.source_remove(self._idle_event_id) |
214 |
if self._idle_draw_id != 0: |
215 |
GObject.source_remove(self._idle_draw_id) |
216 |
|
217 |
def scroll_event(self, widget, event): |
218 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
219 |
x = event.x |
220 |
# flipy so y=0 is bottom of canvas |
221 |
y = self.get_allocation().height - event.y |
222 |
if event.direction==Gdk.ScrollDirection.UP: |
223 |
step = 1 |
224 |
else: |
225 |
step = -1 |
226 |
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) |
227 |
return False # finish event propagation? |
228 |
|
229 |
def button_press_event(self, widget, event): |
230 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
231 |
x = event.x |
232 |
# flipy so y=0 is bottom of canvas |
233 |
y = self.get_allocation().height - event.y |
234 |
FigureCanvasBase.button_press_event(self, x, y, event.button, guiEvent=event) |
235 |
return False # finish event propagation? |
236 |
|
237 |
def button_release_event(self, widget, event): |
238 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
239 |
x = event.x |
240 |
# flipy so y=0 is bottom of canvas |
241 |
y = self.get_allocation().height - event.y |
242 |
FigureCanvasBase.button_release_event(self, x, y, event.button, guiEvent=event) |
243 |
return False # finish event propagation? |
244 |
|
245 |
def key_press_event(self, widget, event): |
246 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
247 |
key = self._get_key(event) |
248 |
if _debug: print "hit", key |
249 |
FigureCanvasBase.key_press_event(self, key, guiEvent=event) |
250 |
return False # finish event propagation? |
251 |
|
252 |
def key_release_event(self, widget, event): |
253 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
254 |
key = self._get_key(event) |
255 |
if _debug: print "release", key |
256 |
FigureCanvasBase.key_release_event(self, key, guiEvent=event) |
257 |
return False # finish event propagation? |
258 |
|
259 |
def motion_notify_event(self, widget, event): |
260 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
261 |
if event.is_hint: |
262 |
t, x, y, state = event.window.get_pointer() |
263 |
else: |
264 |
x, y, state = event.x, event.y, event.get_state() |
265 |
|
266 |
# flipy so y=0 is bottom of canvas |
267 |
y = self.get_allocation().height - y |
268 |
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) |
269 |
return False # finish event propagation? |
270 |
|
271 |
def leave_notify_event(self, widget, event): |
272 |
FigureCanvasBase.leave_notify_event(self, event) |
273 |
|
274 |
def enter_notify_event(self, widget, event): |
275 |
FigureCanvasBase.enter_notify_event(self, event) |
276 |
|
277 |
def _get_key(self, event): |
278 |
if event.keyval in self.keyvald: |
279 |
key = self.keyvald[event.keyval] |
280 |
elif event.keyval <256: |
281 |
key = chr(event.keyval) |
282 |
else: |
283 |
key = None |
284 |
|
285 |
#ctrl = event.get_state() & Gdk.EventMask.CONTROL |
286 |
#shift = event.get_state() & Gdk.EventMask.SHIFT |
287 |
return key |
288 |
|
289 |
|
290 |
def configure_event(self, widget, event): |
291 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
292 |
if widget.get_property("window") is None: |
293 |
return |
294 |
w, h = event.width, event.height |
295 |
if w < 3 or h < 3: |
296 |
return # empty fig |
297 |
|
298 |
# resize the figure (in inches) |
299 |
dpi = self.figure.dpi |
300 |
self.figure.set_size_inches (w/dpi, h/dpi) |
301 |
self._need_redraw = True |
302 |
|
303 |
return False # finish event propagation? |
304 |
|
305 |
|
306 |
def draw(self): |
307 |
self._need_redraw = True |
308 |
if self.get_visible() and self.get_mapped(): |
309 |
self.queue_draw() |
310 |
# do a synchronous draw (its less efficient than an async draw, |
311 |
# but is required if/when animation is used) |
312 |
self.get_property("window").process_updates (False) |
313 |
|
314 |
def draw_idle(self): |
315 |
def idle_draw(*args): |
316 |
self.draw() |
317 |
self._idle_draw_id = 0 |
318 |
return False |
319 |
if self._idle_draw_id == 0: |
320 |
self._idle_draw_id = GObject.idle_add(idle_draw) |
321 |
|
322 |
def _renderer_init(self): |
323 |
"""use cairo renderer""" |
324 |
if _debug: print '%s.%s()' % (self.__class__.__name__, fn_name()) |
325 |
self._renderer = RendererGTK3Cairo(self.figure.dpi) |
326 |
|
327 |
def _render_figure(self, width, height): |
328 |
"""use cairo renderer""" |
329 |
self._renderer.set_width_height (width, height) |
330 |
self.figure.draw (self._renderer) |
331 |
|
332 |
def on_draw_event(self, widget, ctx): |
333 |
""" GtkDrawable draw event, like expose_event in GTK 2.X |
334 |
""" |
335 |
if _debug: print 'FigureCanvasGTK3.%s' % fn_name() |
336 |
|
337 |
if self._need_redraw: |
338 |
self._renderer.set_context(ctx) |
339 |
allocation = self.get_allocation() |
340 |
x, y, w, h = allocation.x, allocation.y, allocation.width, allocation.height |
341 |
self._render_figure(w, h) |
342 |
self._need_redraw = False |
343 |
|
344 |
return False # finish event propagation? |
345 |
|
346 |
def get_default_filetype(self): |
347 |
return 'png' |
348 |
|
349 |
def new_timer(self, *args, **kwargs): |
350 |
""" |
351 |
Creates a new backend-specific subclass of :class:`backend_bases.Timer`. |
352 |
This is useful for getting periodic events through the backend's native |
353 |
event loop. Implemented only for backends with GUIs. |
354 |
|
355 |
optional arguments: |
356 |
|
357 |
*interval* |
358 |
Timer interval in milliseconds |
359 |
*callbacks* |
360 |
Sequence of (func, args, kwargs) where func(*args, **kwargs) will |
361 |
be executed by the timer every *interval*. |
362 |
""" |
363 |
return TimerGTK3(*args, **kwargs) |
364 |
|
365 |
def flush_events(self): |
366 |
Gdk.threads_enter() |
367 |
while Gtk.events_pending(): |
368 |
Gtk.main_iteration(True) |
369 |
Gdk.flush() |
370 |
Gdk.threads_leave() |
371 |
|
372 |
def start_event_loop(self,timeout): |
373 |
FigureCanvasBase.start_event_loop_default(self,timeout) |
374 |
start_event_loop.__doc__=FigureCanvasBase.start_event_loop_default.__doc__ |
375 |
|
376 |
def stop_event_loop(self): |
377 |
FigureCanvasBase.stop_event_loop_default(self) |
378 |
stop_event_loop.__doc__=FigureCanvasBase.stop_event_loop_default.__doc__ |
379 |
|
380 |
FigureCanvas = FigureCanvasGTK3 |
381 |
|
382 |
class FigureManagerGTK3(FigureManagerBase): |
383 |
""" |
384 |
Public attributes |
385 |
|
386 |
canvas : The FigureCanvas instance |
387 |
num : The Figure number |
388 |
toolbar : The Gtk.Toolbar (gtk only) |
389 |
vbox : The Gtk.VBox containing the canvas and toolbar (gtk only) |
390 |
window : The Gtk.Window (gtk only) |
391 |
""" |
392 |
def __init__(self, canvas, num): |
393 |
if _debug: print 'FigureManagerGTK3.%s' % fn_name() |
394 |
FigureManagerBase.__init__(self, canvas, num) |
395 |
|
396 |
self.window = Gtk.Window() |
397 |
self.window.set_title("Figure %d" % num) |
398 |
if (window_icon): |
399 |
try: |
400 |
self.window.set_icon_from_file(window_icon) |
401 |
except: |
402 |
# some versions of gtk throw a glib.GError but not |
403 |
# all, so I am not sure how to catch it. I am unhappy |
404 |
# diong a blanket catch here, but an not sure what a |
405 |
# better way is - JDH |
406 |
verbose.report('Could not load matplotlib icon: %s' % sys.exc_info()[1]) |
407 |
|
408 |
self.vbox = Gtk.Box() |
409 |
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) |
410 |
self.window.add(self.vbox) |
411 |
self.vbox.show() |
412 |
|
413 |
self.canvas.show() |
414 |
|
415 |
# attach a show method to the figure for pylab ease of use |
416 |
self.canvas.figure.show = lambda *args: self.window.show() |
417 |
|
418 |
self.vbox.pack_start(self.canvas, True, True, 0) |
419 |
|
420 |
self.toolbar = self._get_toolbar(canvas) |
421 |
|
422 |
# calculate size for window |
423 |
w = int (self.canvas.figure.bbox.width) |
424 |
h = int (self.canvas.figure.bbox.height) |
425 |
|
426 |
if self.toolbar is not None: |
427 |
self.toolbar.show() |
428 |
self.vbox.pack_end(self.toolbar, False, False, 0) |
429 |
size_request = self.toolbar.size_request() |
430 |
h += size_request.height |
431 |
|
432 |
self.window.set_default_size (w, h) |
433 |
|
434 |
def destroy(*args): |
435 |
Gcf.destroy(num) |
436 |
self.window.connect("destroy", destroy) |
437 |
self.window.connect("delete_event", destroy) |
438 |
if matplotlib.is_interactive(): |
439 |
self.window.show() |
440 |
|
441 |
def notify_axes_change(fig): |
442 |
'this will be called whenever the current axes is changed' |
443 |
if self.toolbar is not None: self.toolbar.update() |
444 |
self.canvas.figure.add_axobserver(notify_axes_change) |
445 |
|
446 |
self.canvas.grab_focus() |
447 |
|
448 |
def destroy(self, *args): |
449 |
if _debug: print 'FigureManagerGTK3.%s' % fn_name() |
450 |
self.vbox.destroy() |
451 |
self.window.destroy() |
452 |
self.canvas.destroy() |
453 |
if self.toolbar: |
454 |
self.toolbar.destroy() |
455 |
self.__dict__.clear() #Is this needed? Other backends don't have it. |
456 |
|
457 |
if Gcf.get_num_fig_managers()==0 and \ |
458 |
not matplotlib.is_interactive() and \ |
459 |
Gtk.main_level() >= 1: |
460 |
Gtk.main_quit() |
461 |
|
462 |
def show(self): |
463 |
# show the figure window |
464 |
self.window.show() |
465 |
|
466 |
def full_screen_toggle (self): |
467 |
self._full_screen_flag = not self._full_screen_flag |
468 |
if self._full_screen_flag: |
469 |
self.window.fullscreen() |
470 |
else: |
471 |
self.window.unfullscreen() |
472 |
_full_screen_flag = False |
473 |
|
474 |
|
475 |
def _get_toolbar(self, canvas): |
476 |
# must be inited after the window, drawingArea and figure |
477 |
# attrs are set |
478 |
if matplotlib.rcParams['toolbar'] == 'classic': |
479 |
toolbar = NavigationToolbar (canvas, self.window) |
480 |
elif matplotlib.rcParams['toolbar'] == 'toolbar2': |
481 |
toolbar = NavigationToolbar2GTK3 (canvas, self.window) |
482 |
else: |
483 |
toolbar = None |
484 |
return toolbar |
485 |
|
486 |
def set_window_title(self, title): |
487 |
self.window.set_title(title) |
488 |
|
489 |
def resize(self, width, height): |
490 |
'set the canvas size in pixels' |
491 |
#_, _, cw, ch = self.canvas.allocation |
492 |
#_, _, ww, wh = self.window.allocation |
493 |
#self.window.resize (width-cw+ww, height-ch+wh) |
494 |
self.window.resize(width, height) |
495 |
|
496 |
|
497 |
class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): |
498 |
# list of toolitems to add to the toolbar, format is: |
499 |
# text, tooltip_text, image_file, callback(str) |
500 |
toolitems = ( |
501 |
('Home', 'Reset original view', 'home.png', 'home'), |
502 |
('Back', 'Back to previous view','back.png', 'back'), |
503 |
('Forward', 'Forward to next view','forward.png', 'forward'), |
504 |
('Pan', 'Pan axes with left mouse, zoom with right', 'move.png','pan'), |
505 |
('Zoom', 'Zoom to rectangle','zoom_to_rect.png', 'zoom'), |
506 |
(None, None, None, None), |
507 |
('Subplots', 'Configure subplots','subplots.png', 'configure_subplots'), |
508 |
('Save', 'Save the figure','filesave.png', 'save_figure'), |
509 |
) |
510 |
|
511 |
def __init__(self, canvas, window): |
512 |
self.win = window |
513 |
GObject.GObject.__init__(self) |
514 |
NavigationToolbar2.__init__(self, canvas) |
515 |
self.ctx = None |
516 |
|
517 |
def set_message(self, s): |
518 |
self.message.set_label(s) |
519 |
|
520 |
def set_cursor(self, cursor): |
521 |
self.canvas.get_property("window").set_cursor(cursord[cursor]) |
522 |
#self.canvas.set_cursor(cursord[cursor]) |
523 |
|
524 |
def release(self, event): |
525 |
try: del self._pixmapBack |
526 |
except AttributeError: pass |
527 |
|
528 |
def dynamic_update(self): |
529 |
# legacy method; new method is canvas.draw_idle |
530 |
self.canvas.draw_idle() |
531 |
|
532 |
def draw_rubberband(self, event, x0, y0, x1, y1): |
533 |
'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744' |
534 |
self.ctx = self.canvas.get_property("window").cairo_create() |
535 |
|
536 |
# todo: instead of redrawing the entire figure, copy the part of |
537 |
# the figure that was covered by the previous rubberband rectangle |
538 |
self.canvas.draw() |
539 |
|
540 |
height = self.canvas.figure.bbox.height |
541 |
y1 = height - y1 |
542 |
y0 = height - y0 |
543 |
w = abs(x1 - x0) |
544 |
h = abs(y1 - y0) |
545 |
rect = [int(val)for val in min(x0,x1), min(y0, y1), w, h] |
546 |
|
547 |
self.ctx.new_path() |
548 |
self.ctx.set_line_width(0.5) |
549 |
self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3]) |
550 |
self.ctx.set_source_rgb(0, 0, 0) |
551 |
self.ctx.stroke() |
552 |
|
553 |
def _init_toolbar(self): |
554 |
self.set_style(Gtk.ToolbarStyle.ICONS) |
555 |
basedir = os.path.join(matplotlib.rcParams['datapath'],'images') |
556 |
|
557 |
for text, tooltip_text, image_file, callback in self.toolitems: |
558 |
if text is None: |
559 |
self.insert( Gtk.SeparatorToolItem(), -1 ) |
560 |
continue |
561 |
fname = os.path.join(basedir, image_file) |
562 |
image = Gtk.Image() |
563 |
image.set_from_file(fname) |
564 |
tbutton = Gtk.ToolButton() |
565 |
tbutton.set_label(text) |
566 |
tbutton.set_icon_widget(image) |
567 |
self.insert(tbutton, -1) |
568 |
tbutton.connect('clicked', getattr(self, callback)) |
569 |
tbutton.set_tooltip_text(tooltip_text) |
570 |
|
571 |
toolitem = Gtk.SeparatorToolItem() |
572 |
self.insert(toolitem, -1) |
573 |
toolitem.set_draw(False) |
574 |
toolitem.set_expand(True) |
575 |
|
576 |
toolitem = Gtk.ToolItem() |
577 |
self.insert(toolitem, -1) |
578 |
self.message = Gtk.Label() |
579 |
toolitem.add(self.message) |
580 |
|
581 |
self.show_all() |
582 |
|
583 |
def get_filechooser(self): |
584 |
return FileChooserDialog( |
585 |
title='Save the figure', |
586 |
parent=self.win, |
587 |
filetypes=self.canvas.get_supported_filetypes(), |
588 |
default_filetype=self.canvas.get_default_filetype()) |
589 |
|
590 |
def save_figure(self, *args): |
591 |
fname, format = self.get_filechooser().get_filename_from_user() |
592 |
if fname: |
593 |
try: |
594 |
self.canvas.print_figure(fname, format=format) |
595 |
except Exception, e: |
596 |
error_msg_gtk(str(e), parent=self) |
597 |
|
598 |
def configure_subplots(self, button): |
599 |
toolfig = Figure(figsize=(6,3)) |
600 |
canvas = self._get_canvas(toolfig) |
601 |
toolfig.subplots_adjust(top=0.9) |
602 |
tool = SubplotTool(self.canvas.figure, toolfig) |
603 |
|
604 |
w = int (toolfig.bbox.width) |
605 |
h = int (toolfig.bbox.height) |
606 |
|
607 |
|
608 |
window = Gtk.Window() |
609 |
if (window_icon): |
610 |
try: window.set_icon_from_file(window_icon) |
611 |
except: |
612 |
# we presumably already logged a message on the |
613 |
# failure of the main plot, don't keep reporting |
614 |
pass |
615 |
window.set_title("Subplot Configuration Tool") |
616 |
window.set_default_size(w, h) |
617 |
vbox = Gtk.Box() |
618 |
vbox.set_property("orientation", Gtk.Orientation.VERTICAL) |
619 |
window.add(vbox) |
620 |
vbox.show() |
621 |
|
622 |
canvas.show() |
623 |
vbox.pack_start(canvas, True, True, 0) |
624 |
window.show() |
625 |
|
626 |
def _get_canvas(self, fig): |
627 |
return FigureCanvasGTK3(fig) |
628 |
|
629 |
|
630 |
class NavigationToolbar(Gtk.Toolbar): |
631 |
""" |
632 |
Public attributes |
633 |
|
634 |
canvas - the FigureCanvas (Gtk.DrawingArea) |
635 |
win - the Gtk.Window |
636 |
|
637 |
""" |
638 |
# list of toolitems to add to the toolbar, format is: |
639 |
# text, tooltip_text, image, callback(str), callback_arg, scroll(bool) |
640 |
toolitems = ( |
641 |
('Left', 'Pan left with click or wheel mouse (bidirectional)', |
642 |
Gtk.STOCK_GO_BACK, 'panx', -1, True), |
643 |
('Right', 'Pan right with click or wheel mouse (bidirectional)', |
644 |
Gtk.STOCK_GO_FORWARD, 'panx', 1, True), |
645 |
('Zoom In X', |
646 |
'Zoom In X (shrink the x axis limits) with click or wheel' |
647 |
' mouse (bidirectional)', |
648 |
Gtk.STOCK_ZOOM_IN, 'zoomx', 1, True), |
649 |
('Zoom Out X', |
650 |
'Zoom Out X (expand the x axis limits) with click or wheel' |
651 |
' mouse (bidirectional)', |
652 |
Gtk.STOCK_ZOOM_OUT, 'zoomx', -1, True), |
653 |
(None, None, None, None, None, None,), |
654 |
('Up', 'Pan up with click or wheel mouse (bidirectional)', |
655 |
Gtk.STOCK_GO_UP, 'pany', 1, True), |
656 |
('Down', 'Pan down with click or wheel mouse (bidirectional)', |
657 |
Gtk.STOCK_GO_DOWN, 'pany', -1, True), |
658 |
('Zoom In Y', |
659 |
'Zoom in Y (shrink the y axis limits) with click or wheel' |
660 |
' mouse (bidirectional)', |
661 |
Gtk.STOCK_ZOOM_IN, 'zoomy', 1, True), |
662 |
('Zoom Out Y', |
663 |
'Zoom Out Y (expand the y axis limits) with click or wheel' |
664 |
' mouse (bidirectional)', |
665 |
Gtk.STOCK_ZOOM_OUT, 'zoomy', -1, True), |
666 |
(None, None, None, None, None, None,), |
667 |
('Save', 'Save the figure', |
668 |
Gtk.STOCK_SAVE, 'save_figure', None, False), |
669 |
) |
670 |
|
671 |
def __init__(self, canvas, window): |
672 |
""" |
673 |
figManager is the FigureManagerGTK3 instance that contains the |
674 |
toolbar, with attributes figure, window and drawingArea |
675 |
|
676 |
""" |
677 |
GObject.GObject.__init__(self) |
678 |
|
679 |
self.canvas = canvas |
680 |
# Note: Gtk.Toolbar already has a 'window' attribute |
681 |
self.win = window |
682 |
|
683 |
self.set_style(Gtk.ToolbarStyle.ICONS) |
684 |
|
685 |
self._create_toolitems() |
686 |
self.update = self._update |
687 |
self.fileselect = FileChooserDialog( |
688 |
title='Save the figure', |
689 |
parent=self.win, |
690 |
filetypes=self.canvas.get_supported_filetypes(), |
691 |
default_filetype=self.canvas.get_default_filetype()) |
692 |
self.show_all() |
693 |
self.update() |
694 |
|
695 |
def _create_toolitems(self): |
696 |
iconSize = Gtk.IconSize.SMALL_TOOLBAR |
697 |
|
698 |
for text, tooltip_text, image_num, callback, callback_arg, scroll \ |
699 |
in self.toolitems: |
700 |
if text is None: |
701 |
self.insert( Gtk.SeparatorToolItem(), -1 ) |
702 |
continue |
703 |
image = Gtk.Image() |
704 |
image.set_from_stock(image_num, iconSize) |
705 |
tbutton = Gtk.ToolButton() |
706 |
tbutton.set_label(text) |
707 |
tbutton.set_icon_widget(image) |
708 |
self.insert(tbutton, -1) |
709 |
if callback_arg: |
710 |
tbutton.connect('clicked', getattr(self, callback), |
711 |
callback_arg) |
712 |
else: |
713 |
tbutton.connect('clicked', getattr(self, callback)) |
714 |
if scroll: |
715 |
tbutton.connect('scroll_event', getattr(self, callback)) |
716 |
tbutton.set_tooltip_text(tooltip_text) |
717 |
|
718 |
# Axes toolitem, is empty at start, update() adds a menu if >=2 axes |
719 |
self.axes_toolitem = Gtk.ToolItem() |
720 |
self.insert(self.axes_toolitem, 0) |
721 |
self.axes_toolitem.set_tooltip_text( |
722 |
'Select axes that controls affect') |
723 |
|
724 |
align = Gtk.Alignment (xalign=0.5, yalign=0.5, xscale=0.0, yscale=0.0) |
725 |
self.axes_toolitem.add(align) |
726 |
|
727 |
self.menubutton = Gtk.Button ("Axes") |
728 |
align.add (self.menubutton) |
729 |
|
730 |
def position_menu (menu): |
731 |
"""Function for positioning a popup menu. |
732 |
Place menu below the menu button, but ensure it does not go off |
733 |
the bottom of the screen. |
734 |
The default is to popup menu at current mouse position |
735 |
""" |
736 |
x0, y0 = self.window.get_origin() |
737 |
x1, y1, m = self.window.get_pointer() |
738 |
x2, y2 = self.menubutton.get_pointer() |
739 |
sc_h = self.get_screen().get_height() |
740 |
w, h = menu.size_request() |
741 |
|
742 |
x = x0 + x1 - x2 |
743 |
y = y0 + y1 - y2 + self.menubutton.allocation.height |
744 |
y = min(y, sc_h - h) |
745 |
return x, y, True |
746 |
|
747 |
def button_clicked (button, data=None): |
748 |
self.axismenu.popup (None, None, position_menu, 0, |
749 |
Gtk.get_current_event_time()) |
750 |
|
751 |
self.menubutton.connect ("clicked", button_clicked) |
752 |
|
753 |
|
754 |
def _update(self): |
755 |
# called by __init__() and FigureManagerGTK3 |
756 |
|
757 |
self._axes = self.canvas.figure.axes |
758 |
|
759 |
if len(self._axes) >= 2: |
760 |
self.axismenu = self._make_axis_menu() |
761 |
self.menubutton.show_all() |
762 |
else: |
763 |
self.menubutton.hide() |
764 |
|
765 |
self.set_active(range(len(self._axes))) |
766 |
|
767 |
|
768 |
def _make_axis_menu(self): |
769 |
# called by self._update*() |
770 |
|
771 |
def toggled(item, data=None): |
772 |
if item == self.itemAll: |
773 |
for item in items: item.set_active(True) |
774 |
elif item == self.itemInvert: |
775 |
for item in items: |
776 |
item.set_active(not item.get_active()) |
777 |
|
778 |
ind = [i for i,item in enumerate(items) if item.get_active()] |
779 |
self.set_active(ind) |
780 |
|
781 |
menu = Gtk.Menu() |
782 |
|
783 |
self.itemAll = Gtk.MenuItem("All") |
784 |
menu.append(self.itemAll) |
785 |
self.itemAll.connect("activate", toggled) |
786 |
|
787 |
self.itemInvert = Gtk.MenuItem("Invert") |
788 |
menu.append(self.itemInvert) |
789 |
self.itemInvert.connect("activate", toggled) |
790 |
|
791 |
items = [] |
792 |
for i in range(len(self._axes)): |
793 |
item = Gtk.CheckMenuItem("Axis %d" % (i+1)) |
794 |
menu.append(item) |
795 |
item.connect("toggled", toggled) |
796 |
item.set_active(True) |
797 |
items.append(item) |
798 |
|
799 |
menu.show_all() |
800 |
return menu |
801 |
|
802 |
|
803 |
def set_active(self, ind): |
804 |
self._ind = ind |
805 |
self._active = [ self._axes[i] for i in self._ind ] |
806 |
|
807 |
def panx(self, button, direction): |
808 |
'panx in direction' |
809 |
|
810 |
for a in self._active: |
811 |
a.xaxis.pan(direction) |
812 |
self.canvas.draw() |
813 |
self._need_redraw = True |
814 |
return True |
815 |
|
816 |
def pany(self, button, direction): |
817 |
'pany in direction' |
818 |
for a in self._active: |
819 |
a.yaxis.pan(direction) |
820 |
self.canvas.draw() |
821 |
return True |
822 |
|
823 |
def zoomx(self, button, direction): |
824 |
'zoomx in direction' |
825 |
for a in self._active: |
826 |
a.xaxis.zoom(direction) |
827 |
self.canvas.draw() |
828 |
return True |
829 |
|
830 |
def zoomy(self, button, direction): |
831 |
'zoomy in direction' |
832 |
for a in self._active: |
833 |
a.yaxis.zoom(direction) |
834 |
self.canvas.draw() |
835 |
return True |
836 |
|
837 |
def get_filechooser(self): |
838 |
return FileChooserDialog( |
839 |
title='Save the figure', |
840 |
parent=self.win, |
841 |
filetypes=self.canvas.get_supported_filetypes(), |
842 |
default_filetype=self.canvas.get_default_filetype()) |
843 |
|
844 |
def save_figure(self, *args): |
845 |
fname, format = self.get_filechooser().get_filename_from_user() |
846 |
if fname: |
847 |
try: |
848 |
self.canvas.print_figure(fname, format=format) |
849 |
except Exception, e: |
850 |
error_msg_gtk(str(e), parent=self) |
851 |
|
852 |
|
853 |
class FileChooserDialog(Gtk.FileChooserDialog): |
854 |
"""GTK+ file selector which remembers the last file/directory |
855 |
selected and presents the user with a menu of supported image formats |
856 |
""" |
857 |
def __init__ (self, |
858 |
title = 'Save file', |
859 |
parent = None, |
860 |
action = Gtk.FileChooserAction.SAVE, |
861 |
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
862 |
Gtk.STOCK_SAVE, Gtk.ResponseType.OK), |
863 |
path = None, |
864 |
filetypes = [], |
865 |
default_filetype = None |
866 |
): |
867 |
super (FileChooserDialog, self).__init__ (title, parent, action, |
868 |
buttons) |
869 |
self.set_default_response (Gtk.ResponseType.OK) |
870 |
|
871 |
if not path: path = os.getcwd() + os.sep |
872 |
|
873 |
# create an extra widget to list supported image formats |
874 |
self.set_current_folder (path) |
875 |
self.set_current_name ('image.' + default_filetype) |
876 |
|
877 |
hbox = Gtk.Box(spacing=10) |
878 |
hbox.pack_start(Gtk.Label(label="File Format:"), False, False, 0) |
879 |
|
880 |
liststore = Gtk.ListStore(GObject.TYPE_STRING) |
881 |
cbox = Gtk.ComboBox() #liststore) |
882 |
cbox.set_model(liststore) |
883 |
cell = Gtk.CellRendererText() |
884 |
cbox.pack_start(cell, True) |
885 |
cbox.add_attribute(cell, 'text', 0) |
886 |
hbox.pack_start(cbox, False, False, 0) |
887 |
|
888 |
self.filetypes = filetypes |
889 |
self.sorted_filetypes = filetypes.items() |
890 |
self.sorted_filetypes.sort() |
891 |
default = 0 |
892 |
for i, (ext, name) in enumerate(self.sorted_filetypes): |
893 |
liststore.append(["%s (*.%s)" % (name, ext)]) |
894 |
if ext == default_filetype: |
895 |
default = i |
896 |
cbox.set_active(default) |
897 |
self.ext = default_filetype |
898 |
|
899 |
def cb_cbox_changed (cbox, data=None): |
900 |
"""File extension changed""" |
901 |
head, filename = os.path.split(self.get_filename()) |
902 |
root, ext = os.path.splitext(filename) |
903 |
ext = ext[1:] |
904 |
new_ext = self.sorted_filetypes[cbox.get_active()][0] |
905 |
self.ext = new_ext |
906 |
|
907 |
if ext in self.filetypes: |
908 |
filename = root + '.' + new_ext |
909 |
elif ext == '': |
910 |
filename = filename.rstrip('.') + '.' + new_ext |
911 |
|
912 |
self.set_current_name (filename) |
913 |
cbox.connect ("changed", cb_cbox_changed) |
914 |
|
915 |
hbox.show_all() |
916 |
self.set_extra_widget(hbox) |
917 |
|
918 |
def get_filename_from_user (self): |
919 |
while True: |
920 |
filename = None |
921 |
if self.run() != int(Gtk.ResponseType.OK): |
922 |
break |
923 |
filename = self.get_filename() |
924 |
break |
925 |
|
926 |
self.hide() |
927 |
return filename, self.ext |
928 |
|
929 |
class DialogLineprops: |
930 |
""" |
931 |
A GUI dialog for controlling lineprops |
932 |
""" |
933 |
signals = ( |
934 |
'on_combobox_lineprops_changed', |
935 |
'on_combobox_linestyle_changed', |
936 |
'on_combobox_marker_changed', |
937 |
'on_colorbutton_linestyle_color_set', |
938 |
'on_colorbutton_markerface_color_set', |
939 |
'on_dialog_lineprops_okbutton_clicked', |
940 |
'on_dialog_lineprops_cancelbutton_clicked', |
941 |
) |
942 |
|
943 |
linestyles = [ls for ls in lines.Line2D.lineStyles if ls.strip()] |
944 |
linestyled = dict([ (s,i) for i,s in enumerate(linestyles)]) |
945 |
|
946 |
|
947 |
markers = [m for m in lines.Line2D.markers if cbook.is_string_like(m)] |
948 |
|
949 |
markerd = dict([(s,i) for i,s in enumerate(markers)]) |
950 |
|
951 |
def __init__(self, lines): |
952 |
import Gtk.glade |
953 |
|
954 |
datadir = matplotlib.get_data_path() |
955 |
gladefile = os.path.join(datadir, 'lineprops.glade') |
956 |
if not os.path.exists(gladefile): |
957 |
raise IOError('Could not find gladefile lineprops.glade in %s'%datadir) |
958 |
|
959 |
self._inited = False |
960 |
self._updateson = True # suppress updates when setting widgets manually |
961 |
self.wtree = Gtk.glade.XML(gladefile, 'dialog_lineprops') |
962 |
self.wtree.signal_autoconnect(dict([(s, getattr(self, s)) for s in self.signals])) |
963 |
|
964 |
self.dlg = self.wtree.get_widget('dialog_lineprops') |
965 |
|
966 |
self.lines = lines |
967 |
|
968 |
cbox = self.wtree.get_widget('combobox_lineprops') |
969 |
cbox.set_active(0) |
970 |
self.cbox_lineprops = cbox |
971 |
|
972 |
cbox = self.wtree.get_widget('combobox_linestyles') |
973 |
for ls in self.linestyles: |
974 |
cbox.append_text(ls) |
975 |
cbox.set_active(0) |
976 |
self.cbox_linestyles = cbox |
977 |
|
978 |
cbox = self.wtree.get_widget('combobox_markers') |
979 |
for m in self.markers: |
980 |
cbox.append_text(m) |
981 |
cbox.set_active(0) |
982 |
self.cbox_markers = cbox |
983 |
self._lastcnt = 0 |
984 |
self._inited = True |
985 |
|
986 |
|
987 |
def show(self): |
988 |
'populate the combo box' |
989 |
self._updateson = False |
990 |
# flush the old |
991 |
cbox = self.cbox_lineprops |
992 |
for i in range(self._lastcnt-1,-1,-1): |
993 |
cbox.remove_text(i) |
994 |
|
995 |
# add the new |
996 |
for line in self.lines: |
997 |
cbox.append_text(line.get_label()) |
998 |
cbox.set_active(0) |
999 |
|
1000 |
self._updateson = True |
1001 |
self._lastcnt = len(self.lines) |
1002 |
self.dlg.show() |
1003 |
|
1004 |
def get_active_line(self): |
1005 |
'get the active line' |
1006 |
ind = self.cbox_lineprops.get_active() |
1007 |
line = self.lines[ind] |
1008 |
return line |
1009 |
|
1010 |
|
1011 |
def get_active_linestyle(self): |
1012 |
'get the active lineinestyle' |
1013 |
ind = self.cbox_linestyles.get_active() |
1014 |
ls = self.linestyles[ind] |
1015 |
return ls |
1016 |
|
1017 |
def get_active_marker(self): |
1018 |
'get the active lineinestyle' |
1019 |
ind = self.cbox_markers.get_active() |
1020 |
m = self.markers[ind] |
1021 |
return m |
1022 |
|
1023 |
def _update(self): |
1024 |
'update the active line props from the widgets' |
1025 |
if not self._inited or not self._updateson: return |
1026 |
line = self.get_active_line() |
1027 |
ls = self.get_active_linestyle() |
1028 |
marker = self.get_active_marker() |
1029 |
line.set_linestyle(ls) |
1030 |
line.set_marker(marker) |
1031 |
|
1032 |
button = self.wtree.get_widget('colorbutton_linestyle') |
1033 |
color = button.get_color() |
1034 |
r, g, b = [val/65535. for val in color.red, color.green, color.blue] |
1035 |
line.set_color((r,g,b)) |
1036 |
|
1037 |
button = self.wtree.get_widget('colorbutton_markerface') |
1038 |
color = button.get_color() |
1039 |
r, g, b = [val/65535. for val in color.red, color.green, color.blue] |
1040 |
line.set_markerfacecolor((r,g,b)) |
1041 |
|
1042 |
line.figure.canvas.draw() |
1043 |
|
1044 |
|
1045 |
|
1046 |
def on_combobox_lineprops_changed(self, item): |
1047 |
'update the widgets from the active line' |
1048 |
if not self._inited: return |
1049 |
self._updateson = False |
1050 |
line = self.get_active_line() |
1051 |
|
1052 |
ls = line.get_linestyle() |
1053 |
if ls is None: ls = 'None' |
1054 |
self.cbox_linestyles.set_active(self.linestyled[ls]) |
1055 |
|
1056 |
marker = line.get_marker() |
1057 |
if marker is None: marker = 'None' |
1058 |
self.cbox_markers.set_active(self.markerd[marker]) |
1059 |
|
1060 |
r,g,b = colorConverter.to_rgb(line.get_color()) |
1061 |
color = Gdk.Color(*[int(val*65535) for val in r,g,b]) |
1062 |
button = self.wtree.get_widget('colorbutton_linestyle') |
1063 |
button.set_color(color) |
1064 |
|
1065 |
r,g,b = colorConverter.to_rgb(line.get_markerfacecolor()) |
1066 |
color = Gdk.Color(*[int(val*65535) for val in r,g,b]) |
1067 |
button = self.wtree.get_widget('colorbutton_markerface') |
1068 |
button.set_color(color) |
1069 |
self._updateson = True |
1070 |
|
1071 |
def on_combobox_linestyle_changed(self, item): |
1072 |
self._update() |
1073 |
|
1074 |
def on_combobox_marker_changed(self, item): |
1075 |
self._update() |
1076 |
|
1077 |
def on_colorbutton_linestyle_color_set(self, button): |
1078 |
self._update() |
1079 |
|
1080 |
def on_colorbutton_markerface_color_set(self, button): |
1081 |
'called colorbutton marker clicked' |
1082 |
self._update() |
1083 |
|
1084 |
def on_dialog_lineprops_okbutton_clicked(self, button): |
1085 |
self._update() |
1086 |
self.dlg.hide() |
1087 |
|
1088 |
def on_dialog_lineprops_cancelbutton_clicked(self, button): |
1089 |
self.dlg.hide() |
1090 |
|
1091 |
# set icon used when windows are minimized |
1092 |
try: |
1093 |
|
1094 |
if sys.platform == 'win32': |
1095 |
icon_filename = 'matplotlib.png' |
1096 |
else: |
1097 |
icon_filename = 'matplotlib.svg' |
1098 |
window_icon = os.path.join(matplotlib.rcParams['datapath'], 'images', icon_filename) |
1099 |
except: |
1100 |
window_icon = None |
1101 |
verbose.report('Could not load matplotlib icon: %s' % sys.exc_info()[1]) |
1102 |
|
1103 |
def error_msg_gtk(msg, parent=None): |
1104 |
if parent is not None: # find the toplevel Gtk.Window |
1105 |
parent = parent.get_toplevel() |
1106 |
if not parent.is_toplevel(): |
1107 |
parent = None |
1108 |
|
1109 |
if not is_string_like(msg): |
1110 |
msg = ','.join(map(str,msg)) |
1111 |
|
1112 |
dialog = Gtk.MessageDialog( |
1113 |
parent = parent, |
1114 |
type = Gtk.MessageType.ERROR, |
1115 |
buttons = Gtk.ButtonsType.OK, |
1116 |
message_format = msg) |
1117 |
dialog.run() |
1118 |
dialog.destroy() |
1119 |
|
1120 |
|
1121 |
FigureManager = FigureManagerGTK3 |