1/*
2* Copyright (c) 2019-2020 Alecaddd (https://alecaddd.com)
3*
4* This file is part of Akira.
5*
6* Akira is free software: you can redistribute it and/or modify
7* it under the terms of the GNU General Public License as published by
8* the Free Software Foundation, either version 3 of the License, or
9* (at your option) any later version.
10
11* Akira is distributed in the hope that it will be useful,
12* but WITHOUT ANY WARRANTY; without even the implied warranty of
13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14* GNU General Public License for more details.
15
16* You should have received a copy of the GNU General Public License
17* along with Akira. If not, see <https://www.gnu.org/licenses/>.
18*
19* Authored by: Alessandro "Alecaddd" Castellani <castellani.ale@gmail.com>
20* Authored by: Giacomo "giacomoalbe" Alberini <giacomoalbe@gmail.com>
21*/
22
23public class Akira.Layouts.MainCanvas : Gtk.Grid {
24    public const int CANVAS_SIZE = 100000;
25    public const double SCROLL_DISTANCE = 0;
26
27    public Gtk.ScrolledWindow main_scroll;
28    public Akira.Lib.Canvas canvas;
29    public weak Akira.Window window { get; construct; }
30
31    private Gtk.Overlay main_overlay;
32    private Granite.Widgets.OverlayBar overlaybar;
33    private Granite.Widgets.Toast notification;
34
35    private double scroll_origin_x = 0;
36    private double scroll_origin_y = 0;
37
38    public MainCanvas (Akira.Window window) {
39        Object (window: window, orientation: Gtk.Orientation.VERTICAL);
40    }
41
42    construct {
43        get_style_context ().add_class ("main-canvas");
44
45        main_overlay = new Gtk.Overlay ();
46        notification = new Granite.Widgets.Toast (_(""));
47
48        main_scroll = new Gtk.ScrolledWindow (null, null);
49        main_scroll.expand = true;
50
51        // Overlay the scrollbars only if mouse pointer is inside canvas
52        main_scroll.overlay_scrolling = false;
53
54        // Change visibility of canvas scrollbars
55        main_scroll.set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER);
56
57        canvas = new Akira.Lib.Canvas (window);
58        canvas.set_bounds (0, 0, CANVAS_SIZE, CANVAS_SIZE);
59        canvas.set_scale (1.0);
60
61        canvas.canvas_moved.connect ((event_x, event_y) => {
62            // Move scroll window according to normalized mouse delta
63            // relative to the scroll window, so with Canvas' pixel
64            // coordinates translated into ScrolledWindow's one.
65            double event_x_pixel_space = event_x;
66            double event_y_pixel_space = event_y;
67
68            // Convert coordinates to pixel space, which does account for
69            // canvas scale and canvas translation.
70            // Otherwise, delta can start to "diverge" due to the
71            // translation of starting point happening during canvas translation
72            canvas.convert_to_pixels (ref event_x_pixel_space, ref event_y_pixel_space);
73
74            var delta_x = event_x_pixel_space - scroll_origin_x;
75            var delta_y = event_y_pixel_space - scroll_origin_y;
76
77            main_scroll.hadjustment.value -= delta_x;
78            main_scroll.vadjustment.value -= delta_y;
79        });
80
81        canvas.canvas_scroll_set_origin.connect ((origin_x, origin_y) => {
82            // Update scroll origin on Canvas' button_press_event
83            scroll_origin_x = origin_x;
84            scroll_origin_y = origin_y;
85
86            canvas.convert_to_pixels (ref scroll_origin_x, ref scroll_origin_y);
87        });
88
89        canvas.scroll_event.connect (on_scroll);
90
91        main_scroll.add (canvas);
92
93        main_overlay.add (main_scroll);
94        main_overlay.add_overlay (notification);
95
96        add (main_overlay);
97
98        // Set up event listeners.
99        window.event_bus.exporting.connect (on_exporting);
100        window.event_bus.export_completed.connect (on_export_completed);
101        window.event_bus.canvas_notification.connect (trigger_notification);
102    }
103
104    public bool on_scroll (Gdk.EventScroll event) {
105        bool is_shift = (event.state & Gdk.ModifierType.SHIFT_MASK) > 0;
106        bool is_ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK) > 0;
107
108        double delta_x, delta_y;
109        event.get_scroll_deltas (out delta_x, out delta_y);
110
111        if (delta_y < -SCROLL_DISTANCE) {
112            // Scroll UP.
113            if (is_ctrl) {
114                // Divide the delta if it's too high. This fixes the zoom with
115                // the mouse wheel.
116                if (delta_y <= -1) {
117                    delta_y /= 10;
118                }
119                // Get the current zoom before zooming.
120                double old_zoom = canvas.get_scale ();
121                // Zoom in.
122                window.event_bus.update_scale (delta_y * -1);
123                // Adjust zoom based on cursor position.
124                zoom_on_cursor (event, old_zoom);
125            } else if (is_shift) {
126                main_scroll.hadjustment.value += delta_y * 10;
127            } else {
128                main_scroll.vadjustment.value += delta_y * 10;
129            }
130        } else if (delta_y > SCROLL_DISTANCE) {
131            // Scroll DOWN.
132            if (is_ctrl) {
133                // Divide the delta if it's too high. This fixes the zoom with
134                // the mouse wheel.
135                if (delta_y >= 1) {
136                    delta_y /= 10;
137                }
138                // Get the current zoom before zooming.
139                double old_zoom = canvas.get_scale ();
140                // Zoom out.
141                window.event_bus.update_scale (-delta_y);
142                // Adjust zoom based on cursor position.
143                zoom_on_cursor (event, old_zoom);
144            } else if (is_shift) {
145                main_scroll.hadjustment.value += delta_y * 10;
146            } else {
147                main_scroll.vadjustment.value += delta_y * 10;
148            }
149        }
150
151        if (delta_x < -SCROLL_DISTANCE) {
152            main_scroll.hadjustment.value += delta_x * 10;
153        } else if (delta_x > SCROLL_DISTANCE) {
154            main_scroll.hadjustment.value += delta_x * 10;
155        }
156
157        return true;
158    }
159
160    private void zoom_on_cursor (Gdk.EventScroll event, double old_zoom) {
161        // The regular zoom mode shifts the visible viewing area
162        // to center itself (it already has one translation applied)
163        // so you cannot just move the viewing area by the distance
164        // of the current mouse location and the new mouse location.
165
166        // If you want to zoom to your mouse you need to find the
167        // difference between the distances of the current mouse location
168        // in the current view scale to the left view border and the new
169        // mouse location that has the new canvas scale applied to the
170        // new left view border and shift the view by that difference.
171        int width = main_scroll.get_allocated_width ();
172        int height = main_scroll.get_allocated_height ();
173
174        var center_x = main_scroll.hadjustment.value + (width / 2);
175        var center_y = main_scroll.vadjustment.value + (height / 2);
176
177        var old_center_x = (center_x / canvas.get_scale ()) * old_zoom;
178        var old_center_y = (center_y / canvas.get_scale ()) * old_zoom;
179
180        var new_event_x = (event.x / old_zoom) * canvas.get_scale ();
181        var new_event_y = (event.y / old_zoom) * canvas.get_scale ();
182
183        var old_hadjustment = old_center_x - (width / 2);
184        var old_vadjustment = old_center_y - (height / 2);
185
186        main_scroll.hadjustment.value +=
187            (new_event_x - main_scroll.hadjustment.value) - (event.x - old_hadjustment);
188        main_scroll.vadjustment.value +=
189            (new_event_y - main_scroll.vadjustment.value) - (event.y - old_vadjustment);
190    }
191
192    private async void on_exporting (string message) {
193        overlaybar = new Granite.Widgets.OverlayBar (main_overlay);
194        overlaybar.label = message;
195        overlaybar.active = true;
196        show_all ();
197    }
198
199    private async void on_export_completed () {
200        main_overlay.remove (overlaybar);
201        overlaybar = null;
202        yield trigger_notification (_("Export completed!"));
203    }
204
205    private async void trigger_notification (string message) {
206        notification.title = message;
207        notification.send_notification ();
208    }
209}
210