.
1 /*
2 Copyright (C) 2012s 2013 2014 Johan Mattsson
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 using Cairo;
19 using Gtk;
20 using Gdk;
21 using BirdFont;
22 using WebKit;
23 using Gdk;
24 using Notify;
25
26 namespace BirdFont {
27
28 public class GtkWindow : Gtk.Window, NativeWindow {
29
30 Box list_box;
31 Box canvas_box;
32
33 WebView html_canvas;
34 ScrolledWindow html_box;
35
36 Box tab_box;
37
38 GlyphCanvasArea glyph_canvas_area;
39
40 Clipboard clipboard;
41 string clipboard_svg = "";
42 string inkscape_clipboard = "";
43
44 Scrollbar scrollbar;
45 bool scrollbar_supress_signal = false;
46
47 Gtk.Window tooltip_window = new Gtk.Window ();
48
49 ToolboxCanvas toolbox;
50
51 Task background_task = new Task(idle);
52
53 public GtkWindow (string title) {
54 scrollbar = new Scrollbar (Orientation.VERTICAL, new Adjustment (0, 0, 1, 1, 0.01, 0.1));
55 ((Gtk.Window)this).set_title ("BirdFont");
56 }
57
58 public static void idle () {
59 }
60
61 public void init () {
62 Signal.connect(this, "notify::is-active", (GLib.Callback) window_focus, null);
63
64 scrollbar.value_changed.connect (() => {
65 double p;
66
67 if (!scrollbar_supress_signal) {
68 p = scrollbar.get_value () / (1 - scrollbar.adjustment.page_size);
69 FontDisplay display = MainWindow.get_current_display ();
70 display.scroll_to (p);
71 }
72 });
73
74 delete_event.connect (() => {
75 MenuTab.quit ();
76 return true;
77 });
78
79 set_size_and_position ();
80
81 glyph_canvas_area = new GlyphCanvasArea (MainWindow.glyph_canvas);
82
83 html_canvas = new WebView ();
84 html_box = new ScrolledWindow (null, null);
85 html_box.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
86 html_box.add (html_canvas);
87
88 MainWindow.get_tab_bar ().signal_tab_selected.connect ((f, tab) => {
89 string uri = "";
90 string html = "";
91 FontDisplay fd = tab.get_display ();
92
93 scrollbar.set_visible (fd.has_scrollbar ());
94
95 if (fd.get_name () == "Preview") {
96 uri = Preview.get_uri ();
97 html = Preview.get_html_with_absolute_paths ();
98
99 try {
100 html_canvas.load_html (html, uri);
101 } catch (Error e) {
102 warning (e.message);
103 warning ("Failed to load html into canvas.");
104 }
105
106 // show the webview when loading has finished
107 html_box.set_visible (true);
108 glyph_canvas_area.set_visible (false);
109 } else {
110 html_box.set_visible (false);
111 glyph_canvas_area.set_visible (true);
112 }
113 });
114
115 // Hide this canvas when window is realized and flip canvas
116 // visibility in tab selection signal.
117 html_canvas.draw.connect ((t, e) => {
118 glyph_canvas_area.set_visible (false);
119 return false;
120 });
121
122 canvas_box = new Box (Orientation.HORIZONTAL, 0);
123 canvas_box.pack_start (glyph_canvas_area, true, true, 0);
124 canvas_box.pack_start (html_box, true, true, 0);
125 canvas_box.pack_start (scrollbar, false, true, 0);
126 tab_box = new Box (Orientation.VERTICAL, 0);
127
128 tab_box.pack_start (new TabbarCanvas (MainWindow.get_tab_bar ()), false, false, 0);
129 tab_box.pack_start (canvas_box, true, true, 0);
130
131 toolbox = new ToolboxCanvas (MainWindow.get_toolbox ());
132 list_box = new Box (Orientation.HORIZONTAL, 0);
133 list_box.pack_start (toolbox, false, false, 0);
134 list_box.pack_start (tab_box, true, true, 0);
135
136 Box vbox = new Box (Orientation.VERTICAL, 0);
137 vbox.pack_start(list_box, true, true, 0);
138 add (vbox);
139
140 try {
141 set_icon_from_file ((!) SearchPaths.find_file (null, "birdfont_window_icon.png").get_path ());
142 } catch (GLib.Error e) {
143 warning (e.message);
144 }
145
146 key_press_event.connect ((t, event) => {
147 TabContent.key_press (event.keyval);
148
149 return false;
150 });
151
152 key_release_event.connect ((t, event) => {
153 TabContent.key_release (event.keyval);
154
155 return false;
156 });
157
158 size_allocate.connect(() => {
159 GlyphCanvas.redraw ();
160 });
161
162 scrollbar.set_visible (false);
163
164 show_all ();
165 MainWindow.open_recent_files_tab ();
166 }
167
168 public void window_focus (void* data) {
169 TabContent.reset_modifier ();
170 }
171
172 public static void reset_modifier (ModifierType flags) {
173 if ((flags & ModifierType.CONTROL_MASK) == 0) {
174 TabContent.key_release (Key.CTRL_RIGHT);
175 TabContent.key_release (Key.CTRL_LEFT);
176 }
177
178 if ((flags & ModifierType.SHIFT_MASK) == 0) {
179 TabContent.key_release (Key.SHIFT_LEFT);
180 TabContent.key_release (Key.SHIFT_RIGHT);
181 }
182
183 if ((flags & ModifierType.MOD1_MASK) == 0) {
184 TabContent.key_release (Key.ALT_LEFT);
185 TabContent.key_release (Key.ALT_RIGHT);
186 }
187
188 if ((flags & ModifierType.MOD5_MASK) == 0) {
189 TabContent.key_release (Key.LOGO_LEFT);
190 TabContent.key_release (Key.LOGO_RIGHT);
191 }
192 }
193
194 public void font_loaded () {
195 Font f = BirdFont.get_current_font ();
196 set_title (@"$(f.full_name)");
197 }
198
199 public void set_scrollbar_size (double size) {
200 scrollbar.adjustment.page_size = size;
201 scrollbar.set_visible (size != 0);
202 }
203
204 public void set_scrollbar_position (double position) {
205 scrollbar_supress_signal = true;
206 scrollbar.adjustment.value = position * (1 - scrollbar.adjustment.page_size);
207 scrollbar_supress_signal = false;
208 }
209
210 public void dump_clipboard_content (Clipboard clipboard, SelectionData selection_data) {
211 string d;
212 return_if_fail (!is_null (selection_data));
213 d = (string) ((!) selection_data);
214 stdout.printf (d);
215 }
216
217 public void dump_clipboard_target (Clipboard clipboard, Atom[] atoms) {
218 foreach (Atom target in atoms) {
219 print ("Target: " + target.name () + "\n");
220 clipboard.request_contents (target, dump_clipboard_content);
221 }
222 }
223
224 public void dump_clipboard () {
225 clipboard.request_targets (dump_clipboard_target);
226 }
227
228 public string get_clipboard_data () {
229 SelectionData? selection_data;
230 Atom target;
231 string? t;
232
233 target = Atom.intern_static_string ("image/x-inkscape-svg");
234 selection_data = clipboard.wait_for_contents (target);
235
236 if (!is_null (selection_data)) {
237 return (string) (((!) selection_data).get_data ());
238 }
239
240 t = clipboard.wait_for_text ();
241 if (t != null) {
242 return (!) t;
243 }
244
245 return "";
246 }
247
248 public void set_inkscape_clipboard (string inkscape_clipboard_data) {
249 if (BirdFont.mac) {
250 clipboard.set_text (inkscape_clipboard_data, -1);
251 } else {
252 TargetEntry t = { "image/x-inkscape-svg", 0, 0 };
253 TargetEntry[] targets = { t };
254 inkscape_clipboard = inkscape_clipboard_data;
255
256 // we can not add data to this closure because the third argument
257 // is owner and not private data.
258 clipboard.set_with_owner (targets,
259
260 // obtain clipboard data
261 (clipboard, selection_data, info, owner) => {
262 Atom type;
263 uchar[] data = (uchar[])(!)((GtkWindow*)owner)->inkscape_clipboard.to_utf8 ();
264 type = Atom.intern_static_string ("image/x-inkscape-svg");
265 selection_data.set (type, 8, data);
266 },
267
268 // clear clipboard data
269 (clipboard, user_data) => {
270 },
271
272 this);
273 }
274 }
275
276 public void set_clipboard_text (string text) {
277 clipboard.set_text (text, -1);
278 }
279
280 public string get_clipboard_text () {
281 string? t;
282
283 t = clipboard.wait_for_text ();
284 if (t != null) {
285 return ((!) t).dup ();
286 }
287
288 return "".dup ();
289 }
290
291 public void set_clipboard (string svg) {
292 TargetEntry t = { "image/svg+xml", 0, 0 };
293 TargetEntry[] targets = { t };
294 clipboard_svg = svg;
295 clipboard.set_with_owner (targets,
296
297 // obtain clipboard data
298 (clipboard, selection_data, info, owner) => {
299 Atom type;
300 uchar[] data = (uchar[])(!)((GtkWindow*)owner)->clipboard_svg.to_utf8 ();
301 type = Atom.intern_static_string ("image/svg+xml");
302 selection_data.set (type, 0, data);
303 },
304
305 // clear clipboard data
306 (clipboard, user_data) => {
307 },
308
309 this);
310 }
311
312 public void update_window_size () {
313 int w, h;
314 get_size (out w, out h);
315
316 Preferences.set ("window_width", @"$w");
317 Preferences.set ("window_height", @"$h");
318 }
319
320 private void set_size_and_position () {
321 int w = Preferences.get_window_width ();
322 int h = Preferences.get_window_height ();
323 set_default_size (w, h);
324 }
325
326 public void quit () {
327 Gtk.main_quit ();
328 }
329
330 public void file_chooser (string title, FileChooser fc, uint flags) {
331 string? fn = null;
332 bool folder;
333 if (BirdFont.get_arguments () .has_argument ("--windows")) {
334 folder = (flags & FileChooser.DIRECTORY) > 0;
335 MenuTab.show_file_dialog_tab (title, fc, folder);
336 } else {
337 if ((flags & FileChooser.DIRECTORY) > 0) {
338 if ((flags & FileChooser.LOAD) > 0) {
339 fn = show_file_chooser (title, FileChooserAction.SELECT_FOLDER, Stock.OPEN);
340 } else if ((flags & FileChooser.SAVE) > 0) {
341 fn = show_file_chooser (title, FileChooserAction.SELECT_FOLDER, Stock.SAVE);
342 } else {
343 warning ("Open or save is not set.");
344 }
345 } else if ((flags & FileChooser.LOAD) > 0) {
346 fn = show_file_chooser (title, FileChooserAction.OPEN, Stock.OPEN);
347 } else if ((flags & FileChooser.SAVE) > 0) {
348 fn = show_file_chooser (title, FileChooserAction.SAVE, Stock.SAVE);
349 } else {
350 warning ("Unknown type");
351 }
352 }
353
354 fc.selected (fn);
355 }
356
357 public string? show_file_chooser (string title, FileChooserAction action, string label) {
358 string? fn = null;
359 FileChooserDialog file_chooser = new FileChooserDialog (title, this, action, Stock.CANCEL, ResponseType.CANCEL, label, ResponseType.ACCEPT);
360 Font font = BirdFont.get_current_font ();
361 int i;
362 string last_folder;
363
364 last_folder = Preferences.get ("last_folder");
365
366 try {
367 if (last_folder == "") {
368 file_chooser.set_current_folder_file (font.get_folder ());
369 } else {
370 file_chooser.set_current_folder_file (File.new_for_path (last_folder));
371 }
372 } catch (GLib.Error e) {
373 stderr.printf (e.message);
374 }
375
376 if (file_chooser.run () == ResponseType.ACCEPT) {
377 GlyphCanvas.redraw ();
378 fn = file_chooser.get_filename ();
379 }
380
381 file_chooser.destroy ();
382
383 if (fn != null) {
384 i = ((!) fn).last_index_of ("/");
385 if (i > -1) {
386 last_folder = ((!) fn).substring (0, i);
387 Preferences.set ("last_folder", @"$last_folder");
388 }
389 }
390
391 return fn;
392 }
393
394 public bool convert_to_png (string from, string to) {
395 Pixbuf pixbuf;
396 string folder;
397 int i;
398
399 try {
400 i = to.last_index_of ("/");
401 if (i != -1) {
402 folder = to.substring (0, i);
403 DirUtils.create (folder, 0xFFFFFF);
404 }
405
406 pixbuf = new Pixbuf.from_file (from);
407 pixbuf.save (to, "png");
408 } catch (GLib.Error e) {
409 warning (e.message);
410 return false;
411 }
412
413 return true;
414 }
415
416 public void run_background_thread (Task t) {
417 unowned Thread<void*> bg;
418
419 MenuTab.start_background_thread ();
420 background_task = t;
421
422 try {
423 bg = Thread.create<void*> (this.background_thread, true);
424 } catch (GLib.Error e) {
425 warning (e.message);
426 }
427 }
428
429 public void run_non_blocking_background_thread (Task t) {
430 unowned Thread<void*> bg;
431
432 try {
433 bg = Thread.create<void*> (t.perform_task, true);
434 } catch (GLib.Error e) {
435 warning (e.message);
436 }
437 }
438
439 public void* background_thread () {
440 background_task.run ();
441 MenuTab.stop_background_thread ();
442 return null;
443 }
444
445 /** Run export in a background thread. */
446 public void export_font () {
447 unowned Thread<void*> export_thread;
448
449 MenuTab.start_background_thread ();
450
451 try {
452 export_thread = Thread.create<void*> (this.export_thread, true);
453 } catch (GLib.Error e) {
454 warning (e.message);
455 }
456 }
457
458 public void* export_thread () {
459 IdleSource idle = new IdleSource ();
460
461 ExportCallback.export_fonts ();
462 MenuTab.stop_background_thread ();
463 MenuTab.signal_file_exported ();
464
465 idle.set_callback (() => {
466 Notify.Notification export_notification;
467 export_notification = new Notify.Notification ("BirdFont", t_("Your fonts have been exported."), null);
468 export_notification.show ();
469 return false;
470 });
471 idle.attach (null);
472
473 return null;
474 }
475
476 /** Load font in a background thread. */
477 public void load () {
478 unowned Thread<void*> thread;
479
480 MenuTab.start_background_thread ();
481
482 try {
483 thread = Thread.create<void*> (this.loading_thread, true);
484 } catch (GLib.Error e) {
485 warning (e.message);
486 }
487 }
488
489 public void* loading_thread () {
490 BirdFont.get_current_font ().load ();
491 MenuTab.stop_background_thread ();
492 MenuTab.signal_file_loaded ();
493 return null;
494 }
495
496 /** Save font in a background thread. */
497 public void save () {
498 unowned Thread<void*> thread;
499
500 MenuTab.start_background_thread ();
501
502 try {
503 thread = Thread.create<void*> (this.saving_thread, true);
504 } catch (GLib.Error e) {
505 warning (e.message);
506 }
507 }
508
509 public void* saving_thread () {
510 BirdFont.get_current_font ().save ();
511 MenuTab.stop_background_thread ();
512 MenuTab.signal_file_saved ();
513 return null;
514 }
515
516 public void load_background_image () {
517 unowned Thread<void*> thread;
518
519 MenuTab.start_background_thread ();
520
521 try {
522 thread = Thread.create<void*> (this.background_image_thread, true);
523 } catch (GLib.Error e) {
524 warning (e.message);
525 }
526 }
527
528 public void* background_image_thread () {
529 BackgroundTool.load_background_image ();
530 MenuTab.stop_background_thread ();
531 return null;
532 }
533
534 public bool can_export () {
535 return true;
536 }
537
538 public void set_cursor (int visible) {
539 if (visible != NativeWindow.VISIBLE) {
540 get_window ().set_cursor (new Cursor (CursorType.BLANK_CURSOR));
541 } else {
542 get_window ().set_cursor (new Cursor (CursorType.ARROW));
543 }
544 }
545
546 public double get_screen_scale () {
547 string? scale = Environment.get_variable ("GDK_SCALE");
548 double factor;
549
550 if (scale == null) {
551 return 1;
552 }
553
554 if (double.try_parse ((!) scale, out factor)) {
555 return factor;
556 }
557
558 return 1;
559 }
560 }
561
562 class TabbarCanvas : DrawingArea {
563 TabBar tabbar;
564
565 public TabbarCanvas (TabBar tb) {
566 tabbar = tb;
567
568 add_events (EventMask.BUTTON_PRESS_MASK | EventMask.POINTER_MOTION_MASK | EventMask.LEAVE_NOTIFY_MASK);
569
570 motion_notify_event.connect ((t, e)=> {
571 Gtk.Allocation alloc;
572 tabbar.motion (e.x, e.y);
573 get_allocation (out alloc);
574 queue_draw_area (0, 0, alloc.width, alloc.height);
575 return true;
576 });
577
578 button_press_event.connect ((t, e)=> {
579 Gtk.Allocation alloc;
580 get_allocation (out alloc);
581 GtkWindow.reset_modifier (e.state);
582 tabbar.select_tab_click (e.x, e.y, alloc.width, alloc.height);
583 queue_draw_area (0, 0, alloc.width, alloc.height);
584 return true;
585 });
586
587 draw.connect ((t, e)=> {
588 Gtk.Allocation alloc;
589 Context cr;
590
591 cr = cairo_create (get_window ());
592 get_allocation (out alloc);
593
594 tabbar.draw (cr, alloc.width, alloc.height);
595 return true;
596 });
597
598 tabbar.signal_tab_selected.connect ((t) => {
599 Gtk.Allocation alloc;
600 get_allocation (out alloc);
601 queue_draw_area (0, 0, alloc.width, alloc.height);
602 });
603
604 tabbar.redraw_tab_bar.connect ((x, y, w, h) => {
605 queue_draw_area (x, y, w, h);
606 });
607
608 set_size_request (20, 38);
609 }
610
611 }
612
613 class ToolboxCanvas : DrawingArea {
614 Toolbox tb;
615
616 public ToolboxCanvas (Toolbox toolbox) {
617 tb = toolbox;
618
619 realize.connect (() => {
620 Gtk.Allocation allocation;
621 get_allocation (out allocation);
622 Toolbox.allocation_width = allocation.width;
623 Toolbox.allocation_height = allocation.height;
624 Toolbox.redraw_tool_box ();
625 });
626
627 tb.redraw.connect ((x, y, w, h) => {
628 queue_draw_area (x, y, w, h);
629 });
630
631 button_press_event.connect ((se, e)=> {
632 if (e.type == EventType.2BUTTON_PRESS) {
633 tb.double_click (e.button, e.x, e.y);
634 } else {
635 tb.press (e.button, e.x, e.y);
636 }
637 return true;
638 });
639
640 button_release_event.connect ((se, e)=> {
641 tb.release (e.button, e.x, e.y);
642 return true;
643 });
644
645 motion_notify_event.connect ((sen, e)=> {
646 // FIXME: e.y is two pixels off in GTK under Gnome
647 tb.move (e.x, e.y);
648 return true;
649 });
650
651 draw.connect ((t, e)=> {
652 Gtk.Allocation allocation;
653 get_allocation (out allocation);
654
655 Context cw = cairo_create(get_window());
656 Toolbox.allocation_width = allocation.width;
657 Toolbox.allocation_height = allocation.height;
658 tb.draw (allocation.width, allocation.height, cw);
659
660 return true;
661 });
662
663 scroll_event.connect ((t, e)=> {
664 if (e.direction == Gdk.ScrollDirection.UP) {
665 tb.scroll_up (e.x, e.y);
666 } else if (e.direction == Gdk.ScrollDirection.DOWN) {
667 tb.scroll_down (e.x, e.y);
668 }
669 return true;
670 });
671
672 add_events (EventMask.BUTTON_PRESS_MASK | EventMask.BUTTON_RELEASE_MASK | EventMask.POINTER_MOTION_MASK | EventMask.LEAVE_NOTIFY_MASK | EventMask.SCROLL_MASK);
673
674 set_size_request (212, 100);
675
676 leave_notify_event.connect ((t, e)=> {
677 tb.reset_active_tool ();
678 return true;
679 });
680
681 }
682 }
683
684 public class GlyphCanvasArea : DrawingArea {
685 GlyphCanvas glyph_canvas;
686 WidgetAllocation alloc = new WidgetAllocation ();
687 uint32 last_release = 0;
688 uint32 last_press = 0;
689
690 public GlyphCanvasArea (GlyphCanvas gc) {
691 int event_flags;
692
693 glyph_canvas = gc;
694
695 event_flags = EventMask.BUTTON_PRESS_MASK;
696 event_flags |= EventMask.BUTTON_RELEASE_MASK;
697 event_flags |= EventMask.POINTER_MOTION_MASK;
698 event_flags |= EventMask.LEAVE_NOTIFY_MASK;
699 event_flags |= EventMask.SCROLL_MASK;
700
701 add_events (event_flags);
702
703 bool button_down = false;
704
705 glyph_canvas.signal_redraw_area.connect ((x, y, w, h) => {
706 queue_draw_area ((int)x, (int)y, (int)w, (int)h);
707 });
708
709 draw.connect ((t, e)=> {
710 Gtk.Allocation allocation;
711 get_allocation (out allocation);
712
713 alloc = new WidgetAllocation ();
714
715 alloc.width = allocation.width;
716 alloc.height = allocation.height;
717 alloc.x = allocation.x;
718 alloc.y = allocation.y;
719
720 Context cw = cairo_create (get_window());
721
722 Surface s = new Surface.similar (cw.get_target (), Cairo.Content.COLOR_ALPHA, alloc.width, alloc.height);
723 Context c = new Context (s);
724
725 TabContent.draw (alloc, c);
726
727 cw.save ();
728 cw.set_source_surface (c.get_target (), 0, 0);
729 cw.paint ();
730 cw.restore ();
731
732 return true;
733 });
734
735 button_press_event.connect ((t, e)=> {
736 GtkWindow.reset_modifier (e.state);
737
738 if (button_down) {
739 warning (@"Button already is down. $(e.button)");
740 }
741
742 if (e.time < last_press) {
743 warning ("Discarding event.");
744 return true;
745 }
746
747 last_press = e.time;
748
749 if (e.type == EventType.BUTTON_PRESS) {
750 TabContent.button_press (e.button, e.x, e.y);
751 button_down = true;
752 } else if (e.type == EventType.2BUTTON_PRESS) {
753 TabContent.double_click (e.button, e.x, e.y);
754 }
755
756 return true;
757 });
758
759 button_release_event.connect ((t, e)=> {
760 if (!button_down) {
761 warning (@"Button is not down $(e.button)");
762 }
763
764 if (e.time < last_release) {
765 warning ("Discarding event.");
766 return true;
767 }
768
769 if (e.type == EventType.BUTTON_RELEASE) {
770 TabContent.button_release ((int) e.button, e.x, e.y);
771 last_release = e.time;
772 button_down = false;
773 }
774
775 return true;
776 });
777
778 motion_notify_event.connect ((t, e)=> {
779 TabContent.motion_notify (e.x, e.y);
780 return true;
781 });
782
783 scroll_event.connect ((t, e)=> {
784 if (e.direction == Gdk.ScrollDirection.UP) {
785 TabContent.scroll_wheel_up (e.x, e.y);
786 } else if (e.direction == Gdk.ScrollDirection.DOWN) {
787 TabContent.scroll_wheel_down (e.x, e.y) ;
788 }
789
790 TabContent.button_release (2, e.x, e.y);
791 return true;
792 });
793
794 can_focus = true;
795 }
796 }
797
798 }
799