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