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