Fawkes API Fawkes Development Version
graph_drawing_area.cpp
1
2/***************************************************************************
3 * graph_drawing_area.cpp - Graph drawing area derived from Gtk::DrawingArea
4 *
5 * Created: Wed Mar 18 10:40:00 2009
6 * Copyright 2008-2009 Tim Niemueller [www.niemueller.de]
7 *
8 ****************************************************************************/
9
10/* This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Library General Public License for more details.
19 *
20 * Read the full text in the LICENSE.GPL file in the doc directory.
21 */
22
23#include "graph_drawing_area.h"
24
25#include "gvplugin_skillgui_cairo.h"
26
27#include <core/exception.h>
28#include <sys/time.h>
29
30#include <cmath>
31#include <libgen.h>
32
33/** @class SkillGuiGraphDrawingArea "graph_drawing_area.h"
34 * Graph drawing area.
35 * Derived version of Gtk::DrawingArea that renders a graph via Graphviz.
36 * @author Tim Niemueller
37 */
38
39/** Constructor. */
41{
42 add_events(Gdk::SCROLL_MASK | Gdk::BUTTON_MOTION_MASK);
43
44 gvc_ = gvContext();
45
46 graph_fsm_ = "";
47 graph_ = "";
48
49 bbw_ = bbh_ = pad_x_ = pad_y_ = 0.0;
50 translation_x_ = translation_y_ = 0.0;
51 scale_ = 1.0;
52 scale_override_ = false;
53 update_graph_ = true;
54 recording_ = false;
55
56 gvplugin_skillgui_cairo_setup(gvc_, this);
57
58 fcd_save_ = new Gtk::FileChooserDialog("Save Graph", Gtk::FILE_CHOOSER_ACTION_SAVE);
59 fcd_open_ = new Gtk::FileChooserDialog("Load Graph", Gtk::FILE_CHOOSER_ACTION_OPEN);
60 fcd_recording_ =
61 new Gtk::FileChooserDialog("Recording Directory", Gtk::FILE_CHOOSER_ACTION_CREATE_FOLDER);
62
63 //Add response buttons the the dialog:
64 fcd_save_->add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
65 fcd_save_->add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
66 fcd_open_->add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
67 fcd_open_->add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
68 fcd_recording_->add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
69 fcd_recording_->add_button(Gtk::Stock::OK, Gtk::RESPONSE_OK);
70
71#if GTK_VERSION_GE(3, 0)
72 filter_pdf_ = Gtk::FileFilter::create();
73 filter_svg_ = Gtk::FileFilter::create();
74 filter_png_ = Gtk::FileFilter::create();
75 filter_dot_ = Gtk::FileFilter::create();
76#else
77 filter_pdf_ = new Gtk::FileFilter();
78 filter_svg_ = new Gtk::FileFilter();
79 filter_png_ = new Gtk::FileFilter();
80 filter_dot_ = new Gtk::FileFilter();
81#endif
82 filter_pdf_->set_name("Portable Document Format (PDF)");
83 filter_pdf_->add_pattern("*.pdf");
84 filter_svg_->set_name("Scalable Vector Graphic (SVG)");
85 filter_svg_->add_pattern("*.svg");
86 filter_png_->set_name("Portable Network Graphic (PNG)");
87 filter_png_->add_pattern("*.png");
88 filter_dot_->set_name("DOT Graph");
89 filter_dot_->add_pattern("*.dot");
90#if GTK_VERSION_GE(3, 0)
91 fcd_save_->add_filter(filter_pdf_);
92 fcd_save_->add_filter(filter_svg_);
93 fcd_save_->add_filter(filter_png_);
94 fcd_save_->add_filter(filter_dot_);
95 fcd_save_->set_filter(filter_pdf_);
96
97 fcd_open_->add_filter(filter_dot_);
98 fcd_open_->set_filter(filter_dot_);
99#else
100 fcd_save_->add_filter(*filter_pdf_);
101 fcd_save_->add_filter(*filter_svg_);
102 fcd_save_->add_filter(*filter_png_);
103 fcd_save_->add_filter(*filter_dot_);
104 fcd_save_->set_filter(*filter_pdf_);
105
106 fcd_open_->add_filter(*filter_dot_);
107 fcd_open_->set_filter(*filter_dot_);
108#endif
109
110 add_events(Gdk::SCROLL_MASK | Gdk::BUTTON_MOTION_MASK | Gdk::BUTTON_PRESS_MASK);
111
112#if GTK_VERSION_LT(3, 0)
113 signal_expose_event().connect(sigc::mem_fun(*this, &SkillGuiGraphDrawingArea::on_expose_event));
114#endif
115 signal_button_press_event().connect(
117 signal_motion_notify_event().connect(
119}
120
121/** Unsupported copy constructor.
122 * Always throws Exception.
123 * @param other other instance
124 */
126{
127 throw fawkes::Exception("SkillGuiGraphDrawingArea cannot be copied");
128}
129
130SkillGuiGraphDrawingArea::~SkillGuiGraphDrawingArea()
131{
132 gvFreeContext(gvc_);
133 //delete fcd_;
134 delete fcd_save_;
135 delete fcd_open_;
136 delete fcd_recording_;
137#if GTK_VERSION_GE(3, 0)
138 filter_pdf_.reset();
139 filter_svg_.reset();
140 filter_png_.reset();
141 filter_dot_.reset();
142#else
143 delete filter_pdf_;
144 delete filter_svg_;
145 delete filter_png_;
146 delete filter_dot_;
147#endif
148}
149
150/** Get "update disabled" signal.
151 * @return "update disabled" signal
152 */
153sigc::signal<void>
155{
156 return signal_update_disabled_;
157}
158
159/** Set graph's FSM name.
160 * @param fsm_name name of FSM the graph belongs to
161 */
162void
163SkillGuiGraphDrawingArea::set_graph_fsm(const std::string &fsm_name)
164{
165 if (update_graph_) {
166 if (graph_fsm_ != fsm_name) {
167 scale_override_ = false;
168 }
169 graph_fsm_ = fsm_name;
170 } else {
171 nonupd_graph_fsm_ = fsm_name;
172 }
173}
174
175/** Set graph.
176 * @param graph string representation of the current graph in the dot language.
177 */
178void
179SkillGuiGraphDrawingArea::set_graph(const std::string &graph)
180{
181 if (update_graph_) {
182 graph_ = graph;
183 queue_draw();
184 } else {
185 nonupd_graph_ = graph;
186 }
187
188 if (recording_) {
189 char *tmp;
190#if defined(__MACH__) && defined(__APPLE__)
191 struct timeval t;
192 if (gettimeofday(&t, NULL) == 0) {
193 long int nsec = t.tv_usec * 1000;
194#else
195 timespec t;
196 if (clock_gettime(CLOCK_REALTIME, &t) == 0) {
197 long int nsec = t.tv_nsec;
198#endif
199 struct tm tms;
200 localtime_r(&t.tv_sec, &tms);
201
202 if (asprintf(&tmp,
203 "%s/%s_%04i%02i%02i-%02i%02i%02i.%09li.dot",
204 record_directory_.c_str(),
205 graph_fsm_.c_str(),
206 tms.tm_year + 1900,
207 tms.tm_mon + 1,
208 tms.tm_mday,
209 tms.tm_hour,
210 tms.tm_min,
211 tms.tm_sec,
212 nsec)
213 != -1) {
214 //printf("Would record to filename %s\n", tmp);
215 save_dotfile(tmp);
216 free(tmp);
217 } else {
218 printf("Warning: Could not create file name for recording, skipping graph\n");
219 }
220 } else {
221 printf("Warning: Could not time recording, skipping graph\n");
222 }
223 }
224}
225
226/** Set bounding box.
227 * To be called only by the Graphviz plugin.
228 * @param bbw bounding box width
229 * @param bbh bounding box height
230 */
231void
232SkillGuiGraphDrawingArea::set_bb(double bbw, double bbh)
233{
234 bbw_ = bbw;
235 bbh_ = bbh;
236}
237
238/** Set padding.
239 * To be called only by the Graphviz plugin.
240 * @param pad_x padding in x
241 * @param pad_y padding in y
242 */
243void
244SkillGuiGraphDrawingArea::set_pad(double pad_x, double pad_y)
245{
246 pad_x_ = pad_x;
247 pad_y_ = pad_y;
248}
249
250/** Get padding.
251 * To be called only by the Graphviz plugin.
252 * @param pad_x upon return contains padding in x
253 * @param pad_y upon return contains padding in y
254 */
255void
256SkillGuiGraphDrawingArea::get_pad(double &pad_x, double &pad_y)
257{
258 if (scale_override_) {
259 pad_x = pad_y = 0;
260 } else {
261 pad_x = pad_x_;
262 pad_y = pad_y_;
263 }
264}
265
266/** Set translation.
267 * To be called only by the Graphviz plugin.
268 * @param tx translation in x
269 * @param ty translation in y
270 */
271void
273{
274 translation_x_ = tx;
275 translation_y_ = ty;
276}
277
278/** Set scale.
279 * To be called only by the Graphviz plugin.
280 * @param scale scale value
281 */
282void
284{
285 scale_ = scale;
286}
287
288/** Get scale.
289 * To be called only by the Graphviz plugin.
290 * @return scale value
291 */
292double
294{
295 return scale_;
296}
297
298/** Get translation.
299 * @param tx upon return contains translation value
300 * @param ty upon return contains translation value
301 */
302void
304{
305 tx = translation_x_;
306 ty = translation_y_;
307}
308
309/** Get dimensions
310 * @param width upon return contains width
311 * @param height upon return contains height
312 */
313void
314SkillGuiGraphDrawingArea::get_dimensions(double &width, double &height)
315{
316 Gtk::Allocation alloc = get_allocation();
317 width = alloc.get_width();
318 height = alloc.get_height();
319}
320
321/** Zoom in.
322 * Increases zoom factor by 20, no upper limit.
323 */
324void
326{
327 Gtk::Allocation alloc = get_allocation();
328 scale_ += 0.1;
329 scale_override_ = true;
330 translation_x_ = (alloc.get_width() - bbw_ * scale_) / 2.0;
331 translation_y_ = (alloc.get_height() - bbh_ * scale_) / 2.0 + bbh_ * scale_;
332 queue_draw();
333}
334
335/** Zoom out.
336 * Decreases zoom factor by 20 with a minimum of 1.
337 */
338void
340{
341 scale_override_ = true;
342 if (scale_ > 0.1) {
343 Gtk::Allocation alloc = get_allocation();
344 scale_ -= 0.1;
345 translation_x_ = (alloc.get_width() - bbw_ * scale_) / 2.0;
346 translation_y_ = (alloc.get_height() - bbh_ * scale_) / 2.0 + bbh_ * scale_;
347 queue_draw();
348 }
349}
350
351/** Zoom to fit.
352 * Disables scale override and draws with values suggested by Graphviz plugin.
353 */
354void
356{
357 scale_override_ = false;
358 queue_draw();
359}
360
361/** Zoom reset.
362 * Reset zoom to 1. Enables scale override.
363 */
364void
366{
367 Gtk::Allocation alloc = get_allocation();
368 scale_ = 1.0;
369 scale_override_ = true;
370 translation_x_ = (alloc.get_width() - bbw_) / 2.0 + pad_x_;
371 translation_y_ = (alloc.get_height() - bbh_) / 2.0 + bbh_ - pad_y_;
372 queue_draw();
373}
374
375/** Check if scale override is enabled.
376 * @return true if scale override is enabled, false otherwise
377 */
378bool
380{
381 return scale_override_;
382}
383
384/** Get Cairo context.
385 * This is only valid during the expose event and is only meant for the
386 * Graphviz plugin.
387 * @return Cairo context
388 */
389Cairo::RefPtr<Cairo::Context>
391{
392 return cairo_;
393}
394
395/** Check if graph is being updated.
396 * @return true if the graph will be update if new data is received, false otherwise
397 */
398bool
400{
401 return update_graph_;
402}
403
404/** Set if the graph should be updated on new data.
405 * @param update true to update on new data, false to disable update
406 */
407void
409{
410 if (update && !update_graph_) {
411 if (graph_fsm_ != nonupd_graph_fsm_) {
412 scale_override_ = false;
413 }
414 graph_ = nonupd_graph_;
415 graph_fsm_ = nonupd_graph_fsm_;
416 queue_draw();
417 }
418 update_graph_ = update;
419}
420
421void
422SkillGuiGraphDrawingArea::save_dotfile(const char *filename)
423{
424 FILE *f = fopen(filename, "w");
425 if (f) {
426 if (fwrite(graph_.c_str(), graph_.length(), 1, f) != 1) {
427 // bang, ignored
428 printf("Failed to write dot file '%s'\n", filename);
429 }
430 fclose(f);
431 }
432}
433
434/** Enable/disable recording.
435 * @param recording true to enable recording, false otherwise
436 * @return true if recording is enabled now, false if it is disabled.
437 * Enabling the recording may fail for example if the user chose to abort
438 * the directory creation process.
439 */
440bool
442{
443 if (recording) {
444 Gtk::Window *w = dynamic_cast<Gtk::Window *>(get_toplevel());
445 fcd_recording_->set_transient_for(*w);
446 int result = fcd_recording_->run();
447 if (result == Gtk::RESPONSE_OK) {
448 record_directory_ = fcd_recording_->get_filename();
449 recording_ = true;
450 }
451 fcd_recording_->hide();
452 } else {
453 recording_ = false;
454 }
455 return recording_;
456}
457
458/** save current graph. */
459void
461{
462 Gtk::Window *w = dynamic_cast<Gtk::Window *>(get_toplevel());
463 fcd_save_->set_transient_for(*w);
464
465 int result = fcd_save_->run();
466 if (result == Gtk::RESPONSE_OK) {
467#if GTK_VERSION_GE(3, 0)
468 Glib::RefPtr<Gtk::FileFilter> f = fcd_save_->get_filter();
469#else
470 Gtk::FileFilter *f = fcd_save_->get_filter();
471#endif
472 std::string filename = fcd_save_->get_filename();
473 if (filename != "") {
474 if (f == filter_dot_) {
475 save_dotfile(filename.c_str());
476 } else {
477 Cairo::RefPtr<Cairo::Surface> surface;
478
479 bool write_to_png = false;
480 if (f == filter_pdf_) {
481 surface = Cairo::PdfSurface::create(filename, bbw_, bbh_);
482 } else if (f == filter_svg_) {
483 surface = Cairo::SvgSurface::create(filename, bbw_, bbh_);
484 } else if (f == filter_png_) {
485 surface =
486 Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, (int)ceilf(bbw_), (int)ceilf(bbh_));
487 write_to_png = true;
488 }
489
490 if (surface) {
491 cairo_ = Cairo::Context::create(surface);
492
493 bool old_scale_override = scale_override_;
494 double old_tx = translation_x_;
495 double old_ty = translation_y_;
496 double old_scale = scale_;
497 translation_x_ = pad_x_;
498 translation_y_ = bbh_ - pad_y_;
499 scale_ = 1.0;
500 scale_override_ = true;
501
502 Agraph_t *g = agmemread((char *)graph_.c_str());
503 if (g) {
504 gvLayout(gvc_, g, (char *)"dot");
505 gvRender(gvc_, g, (char *)"skillguicairo", NULL);
506 gvFreeLayout(gvc_, g);
507 agclose(g);
508 }
509
510 if (write_to_png) {
511 surface->write_to_png(filename);
512 }
513
514 cairo_.clear();
515
516 translation_x_ = old_tx;
517 translation_y_ = old_ty;
518 scale_ = old_scale;
519 scale_override_ = old_scale_override;
520 }
521 }
522
523 } else {
524 Gtk::MessageDialog md(*w,
525 "Invalid filename",
526 /* markup */ false,
527 Gtk::MESSAGE_ERROR,
528 Gtk::BUTTONS_OK,
529 /* modal */ true);
530 md.set_title("Invalid File Name");
531 md.run();
532 }
533 }
534
535 fcd_save_->hide();
536}
537
538/** Open a dot graph and display it. */
539void
541{
542 Gtk::Window *w = dynamic_cast<Gtk::Window *>(get_toplevel());
543 fcd_open_->set_transient_for(*w);
544
545 int result = fcd_open_->run();
546 if (result == Gtk::RESPONSE_OK) {
547 update_graph_ = false;
548 graph_ = "";
549 char *basec = strdup(fcd_open_->get_filename().c_str());
550 char *basen = basename(basec);
551 graph_fsm_ = basen;
552 free(basec);
553
554 FILE *f = fopen(fcd_open_->get_filename().c_str(), "r");
555 while (!feof(f)) {
556 char tmp[4096];
557 size_t s;
558 if ((s = fread(tmp, 1, 4096, f)) > 0) {
559 graph_.append(tmp, s);
560 }
561 }
562 fclose(f);
563 signal_update_disabled_.emit();
564 queue_draw();
565 }
566
567 fcd_open_->hide();
568}
569
570#if GTK_VERSION_GE(3, 0)
571/** Draw event handler.
572 * @param cr cairo context
573 * @return true
574 */
575bool
576SkillGuiGraphDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
577#else
578/** Expose event handler.
579 * @param event event info structure.
580 * @return signal return value
581 */
582bool
584#endif
585{
586 // This is where we draw on the window
587 Glib::RefPtr<Gdk::Window> window = get_window();
588 if (window) {
589 //Gtk::Allocation allocation = get_allocation();
590 //const int width = allocation.get_width();
591 //const int height = allocation.get_height();
592
593 // coordinates for the center of the window
594 //int xc, yc;
595 //xc = width / 2;
596 //yc = height / 2;
597#if GTK_VERSION_LT(3, 0)
598 cairo_ = window->create_cairo_context();
599#else
600 cairo_ = cr;
601#endif
602 cairo_->set_source_rgb(1, 1, 1);
603 cairo_->paint();
604
605 Agraph_t *g = agmemread((char *)graph_.c_str());
606 if (g) {
607 gvLayout(gvc_, g, (char *)"dot");
608 gvRender(gvc_, g, (char *)"skillguicairo", NULL);
609 gvFreeLayout(gvc_, g);
610 agclose(g);
611 }
612
613 cairo_.clear();
614 }
615
616 return true;
617}
618
619/** Scroll event handler.
620 * @param event event structure
621 * @return signal return value
622 */
623bool
625{
626 if (event->direction == GDK_SCROLL_UP) {
627 zoom_in();
628 } else if (event->direction == GDK_SCROLL_DOWN) {
629 zoom_out();
630 }
631 return true;
632}
633
634/** Button press event handler.
635 * @param event event data
636 * @return true
637 */
638bool
640{
641 last_mouse_x_ = event->x;
642 last_mouse_y_ = event->y;
643 return true;
644}
645
646/** Mouse motion notify event handler.
647 * @param event event data
648 * @return true
649 */
650bool
652{
653 scale_override_ = true;
654 translation_x_ -= last_mouse_x_ - event->x;
655 translation_y_ -= last_mouse_y_ - event->y;
656 last_mouse_x_ = event->x;
657 last_mouse_y_ = event->y;
658 queue_draw();
659 return true;
660}
void set_scale(double scale)
Set scale.
sigc::signal< void > signal_update_disabled()
Get "update disabled" signal.
Cairo::RefPtr< Cairo::Context > get_cairo()
Get Cairo context.
void get_dimensions(double &width, double &height)
Get dimensions.
void set_graph(const std::string &graph)
Set graph.
void set_bb(double bbw, double bbh)
Set bounding box.
void open()
Open a dot graph and display it.
virtual bool on_expose_event(GdkEventExpose *event)
Expose event handler.
bool get_update_graph()
Check if graph is being updated.
virtual bool on_motion_notify_event(GdkEventMotion *event)
Mouse motion notify event handler.
void set_translation(double tx, double ty)
Set translation.
void save()
save current graph.
virtual bool on_button_press_event(GdkEventButton *event)
Button press event handler.
void set_pad(double pad_x, double pad_y)
Set padding.
void get_pad(double &pad_x, double &pad_y)
Get padding.
SkillGuiGraphDrawingArea()
Constructor.
virtual bool on_scroll_event(GdkEventScroll *event)
Scroll event handler.
void set_update_graph(bool update)
Set if the graph should be updated on new data.
void set_graph_fsm(const std::string &fsm_name)
Set graph's FSM name.
bool set_recording(bool recording)
Enable/disable recording.
void get_translation(double &tx, double &ty)
Get translation.
bool scale_override()
Check if scale override is enabled.
Base class for exceptions in Fawkes.
Definition: exception.h:36