diff --git a/src/Layouts/Partials/BorderItem.vala b/src/Layouts/Partials/BorderItem.vala index 0289f2188..d495f937e 100644 --- a/src/Layouts/Partials/BorderItem.vala +++ b/src/Layouts/Partials/BorderItem.vala @@ -17,12 +17,14 @@ * along with Akira. If not, see . * * Authored by: Alessandro "alecaddd" Castellani + * Authored by: Ivan "isneezy" Vilanculo */ public class Akira.Layouts.Partials.BorderItem : Gtk.Grid { public weak Akira.Window window { get; construct; } private Gtk.Grid color_chooser; + private Gtk.Button eyedropper_button; private Gtk.Button hidden_button; private Gtk.Button delete_button; private Gtk.Image hidden_button_icon; @@ -31,6 +33,7 @@ public class Akira.Layouts.Partials.BorderItem : Gtk.Grid { public Akira.Partials.ColorField color_container; private Gtk.Popover color_popover; private Gtk.Grid color_picker; + private Akira.Utils.ColorPicker eyedropper; private Gtk.ColorChooserWidget color_chooser_widget; public Akira.Models.BordersItemModel model { get; construct; } @@ -165,6 +168,15 @@ public class Akira.Layouts.Partials.BorderItem : Gtk.Grid { color_chooser.attach (color_container, 1, 0, 1, 1); color_chooser.attach (tickness_container, 2, 0, 1, 1); + eyedropper_button = new Gtk.Button (); + eyedropper_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + eyedropper_button.get_style_context ().add_class ("button-rounded"); + eyedropper_button.can_focus = false; + eyedropper_button.valign = Gtk.Align.CENTER; + eyedropper_button.set_tooltip_text (_("Pick color")); + eyedropper_button.add (new Gtk.Image.from_icon_name ("preferences-color-symbolic", + Gtk.IconSize.SMALL_TOOLBAR)); + hidden_button = new Gtk.Button (); hidden_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); hidden_button.get_style_context ().add_class ("button-rounded"); @@ -191,20 +203,36 @@ public class Akira.Layouts.Partials.BorderItem : Gtk.Grid { color_popover.add (color_picker); attach (color_chooser, 0, 0, 1, 1); - attach (hidden_button, 1, 0, 1, 1); - attach (delete_button, 2, 0, 1, 1); + attach (eyedropper_button, 1, 0, 1, 1); + attach (hidden_button, 2, 0, 1, 1); + attach (delete_button, 3, 0, 1, 1); set_color_chooser_color (); set_button_color (); } private void create_event_bindings () { + eyedropper_button.clicked.connect (on_eyedropper_click); delete_button.clicked.connect (on_delete_item); hidden_button.clicked.connect (toggle_visibility); model.notify.connect (on_model_changed); color_chooser_widget.notify["rgba"].connect (on_color_changed); } + private void on_eyedropper_click () { + eyedropper = new Akira.Utils.ColorPicker (); + eyedropper.show_all (); + + eyedropper.picked.connect ((picked_color) => { + color_chooser_widget.set_rgba (picked_color); + eyedropper.close (); + }); + + eyedropper.cancelled.connect (() => { + eyedropper.close (); + }); + } + private void on_model_changed () { model.item.reset_colors (); set_button_color (); diff --git a/src/Layouts/Partials/FillItem.vala b/src/Layouts/Partials/FillItem.vala index 09c5edaae..234537870 100644 --- a/src/Layouts/Partials/FillItem.vala +++ b/src/Layouts/Partials/FillItem.vala @@ -18,12 +18,14 @@ * * Authored by: Giacomo "giacomoalbe" Alberini * Authored by: Alessandro "alecaddd" Castellani + * Authored by: Ivan "isneezy" Vilanculo */ public class Akira.Layouts.Partials.FillItem : Gtk.Grid { public weak Akira.Window window { get; construct; } private Gtk.Grid fill_chooser; + private Gtk.Button eyedropper_button; private Gtk.Button hidden_button; private Gtk.Button delete_button; private Gtk.Image hidden_button_icon; @@ -33,6 +35,7 @@ public class Akira.Layouts.Partials.FillItem : Gtk.Grid { private Gtk.Popover color_popover; private Gtk.Grid color_picker; private Gtk.ColorChooserWidget color_chooser_widget; + private Akira.Utils.ColorPicker eyedropper; public Akira.Models.FillsItemModel model { get; construct; } @@ -167,6 +170,15 @@ public class Akira.Layouts.Partials.FillItem : Gtk.Grid { fill_chooser.attach (color_container, 1, 0, 1, 1); fill_chooser.attach (opacity_container, 2, 0, 1, 1); + eyedropper_button = new Gtk.Button (); + eyedropper_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + eyedropper_button.get_style_context ().add_class ("button-rounded"); + eyedropper_button.can_focus = false; + eyedropper_button.valign = Gtk.Align.CENTER; + eyedropper_button.set_tooltip_text (_("Pick color")); + eyedropper_button.add (new Gtk.Image.from_icon_name ("preferences-color-symbolic", + Gtk.IconSize.SMALL_TOOLBAR)); + hidden_button = new Gtk.Button (); hidden_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); hidden_button.get_style_context ().add_class ("button-rounded"); @@ -193,20 +205,36 @@ public class Akira.Layouts.Partials.FillItem : Gtk.Grid { color_popover.add (color_picker); attach (fill_chooser, 0, 0, 1, 1); - attach (hidden_button, 1, 0, 1, 1); - attach (delete_button, 2, 0, 1, 1); + attach (eyedropper_button, 1, 0, 1, 1); + attach (hidden_button, 2, 0, 1, 1); + attach (delete_button, 3, 0, 1, 1); set_color_chooser_color (); set_button_color (); } private void create_event_bindings () { + eyedropper_button.clicked.connect (on_eyedropper_click); delete_button.clicked.connect (on_delete_item); hidden_button.clicked.connect (toggle_visibility); model.notify.connect (on_model_changed); color_chooser_widget.notify["rgba"].connect (on_color_changed); } + private void on_eyedropper_click () { + eyedropper = new Akira.Utils.ColorPicker (); + eyedropper.show_all (); + + eyedropper.picked.connect ((picked_color) => { + color_chooser_widget.set_rgba (picked_color); + eyedropper.close (); + }); + + eyedropper.cancelled.connect (() => { + eyedropper.close (); + }); + } + private void on_model_changed () { model.item.reset_colors (); set_button_color (); diff --git a/src/Services/ActionManager.vala b/src/Services/ActionManager.vala index bf0b50290..d20dea1e4 100644 --- a/src/Services/ActionManager.vala +++ b/src/Services/ActionManager.vala @@ -17,6 +17,7 @@ * along with Akira. If not, see . * * Authored by: Alessandro "Alecaddd" Castellani +* Authored by: Ivan "isneezy" Vilanculo */ public class Akira.Services.ActionManager : Object { @@ -64,6 +65,7 @@ public class Akira.Services.ActionManager : Object { public const string ACTION_FLIP_V = "action_flip_v"; public const string ACTION_ESCAPE = "action_escape"; public const string ACTION_SHORTCUTS = "action_shortcuts"; + public const string ACTION_PICK_COLOR = "action_pick_color"; public static Gee.MultiMap action_accelerators = new Gee.HashMultiMap (); public static Gee.MultiMap typing_accelerators = new Gee.HashMultiMap (); @@ -101,6 +103,7 @@ public class Akira.Services.ActionManager : Object { { ACTION_FLIP_V, action_flip_v }, { ACTION_ESCAPE, action_escape }, { ACTION_SHORTCUTS, action_shortcuts }, + { ACTION_PICK_COLOR, action_pick_color }, }; public ActionManager (Akira.Application akira_app, Akira.Window window) { @@ -137,6 +140,7 @@ public class Akira.Services.ActionManager : Object { action_accelerators.set (ACTION_FLIP_H, "bracketleft"); action_accelerators.set (ACTION_FLIP_V, "bracketright"); action_accelerators.set (ACTION_SHORTCUTS, "F1"); + action_accelerators.set (ACTION_PICK_COLOR, "c"); typing_accelerators.set (ACTION_ESCAPE, "Escape"); typing_accelerators.set (ACTION_ARTBOARD_TOOL, "a"); @@ -452,6 +456,41 @@ public class Akira.Services.ActionManager : Object { dialog.present (); } + private void action_pick_color () { + weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas; + // Interrupt if no item is selected. + if (canvas.selected_bound_manager.selected_items.length () == 0) { + return; + } + foreach (var item in canvas.selected_bound_manager.selected_items) { + // Hide the ghost bound manager. + item.bounds_manager.hide (); + } + bool is_holding_shift = false; + var color_picker = new Akira.Utils.ColorPicker (); + color_picker.show_all (); + color_picker.key_pressed.connect (e => { + is_holding_shift = e.keyval == Gdk.Key.Shift_L; + }); + color_picker.key_released.connect (e => { + is_holding_shift = e.keyval == Gdk.Key.Shift_L; + }); + color_picker.cancelled.connect (() => { + color_picker.close (); + }); + color_picker.picked.connect (color => { + foreach (var item in canvas.selected_bound_manager.selected_items) { + if (is_holding_shift) { + item.border_color_string = Utils.Color.rgba_to_hex_string (color); + } else { + item.color_string = Utils.Color.rgba_to_hex_string (color); + } + item.load_colors (); + } + color_picker.close (); + }); + } + public static void action_from_group (string action_name, ActionGroup? action_group) { action_group.activate_action (action_name, null); } diff --git a/src/Utils/Color.vala b/src/Utils/Color.vala index becd7ed38..0250b55fb 100644 --- a/src/Utils/Color.vala +++ b/src/Utils/Color.vala @@ -24,6 +24,10 @@ public class Akira.Utils.Color : Object { var rgba = Gdk.RGBA (); rgba.parse (rgba_string); + return rgba_to_hex_string (rgba); + } + + public static string rgba_to_hex_string (Gdk.RGBA rgba) { return "#%02x%02x%02x".printf ( (int) (rgba.red * 255), (int) (rgba.green * 255), diff --git a/src/Utils/ColorPicker.vala b/src/Utils/ColorPicker.vala new file mode 100644 index 000000000..10c2e86f2 --- /dev/null +++ b/src/Utils/ColorPicker.vala @@ -0,0 +1,342 @@ +/* +* Copyright (c) 2020 Alecaddd (https://alecaddd.com) +* +* This file is part of Akira. +* +* Akira is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Akira is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. + +* You should have received a copy of the GNU General Public License +* along with Akira. If not, see . +* +* Authored by: Ivan "isneezy" Vilanculo +* Ported from: https://github.com/ColorPicker/RonnyDo +*/ + +public class Akira.Utils.ColorPicker : Gtk.Window { + public signal void picked (Gdk.RGBA color); + public signal void cancelled (); + public signal void moved (Gdk.RGBA color); + public signal void key_pressed (Gdk.EventKey e); + public signal void key_released (Gdk.EventKey e); + + const string DARK_BORDER_COLOR_STRING = "#333333"; + private Gdk.RGBA dark_border_color = Gdk.RGBA (); + + const string BRIGHT_BORDER_COLOR_STRING = "#FFFFFF"; + private Gdk.RGBA bright_border_color = Gdk.RGBA (); + + + // 1. Snapsize is the amount of pixel going to be magnified by the zoomlevel. + // 2. The snapsize must be odd to have a 1px magnifier center. + // 3. Asure that snapsize*max_zoomlevel+shadow_width*2 is smaller than 2 * get_screen ().get_display ().get_maximal_cursor_size() + // Valid: snapsize = 31, max_zoomlevel = 7, shadow_width = 15 --> 247px + // get_maximal_cursor_size = 128 --> 256px + // Otherwise the cursor starts to flicker. See https://github.com/stuartlangridge/ColourPicker/issues/6#issuecomment-277972290 + // and https://github.com/RonnyDo/ColorPicker/issues/19 + int snapsize = 31; + int min_zoomlevel = 2; + int max_zoomlevel = 7; + int zoomlevel = 6; + int shadow_width = 15; + + private Gdk.Cursor magnifier = null; + + construct { + app_paintable = true; + decorated = false; + resizable = false; + set_visual (get_screen ().get_rgba_visual ()); + type = Gtk.WindowType.POPUP; + } + + + public ColorPicker () { + stick (); + set_resizable (true); + set_deletable (false); + set_skip_taskbar_hint (true); + set_skip_pager_hint (true); + set_keep_above (true); + + + dark_border_color.parse (DARK_BORDER_COLOR_STRING); + bright_border_color.parse (BRIGHT_BORDER_COLOR_STRING); + + // TODO remove the zoom level restauration if we do not need it + // restore zoomlevel + // if (settings.zoomlevel >= min_zoomlevel && settings.zoomlevel <= max_zoomlevel) { + // zoomlevel = settings.zoomlevel; + // } + + var display = Gdk.Display.get_default (); + Gdk.Monitor monitor = display.get_primary_monitor (); + Gdk.Rectangle geom = monitor.get_geometry (); + set_default_size (geom.width, geom.height); + } + + + public override bool button_release_event (Gdk.EventButton e) { + // button_1 is left mouse button + if (e.button == 1) { + Gdk.RGBA color = get_color_at ((int) e.x_root, (int) e.y_root); + picked (color); + // button_3 is right mouse button + } else if (e.button == 3) { + cancelled (); + } + + return true; + } + + + public override bool draw (Cairo.Context cr) { + return false; + } + + + public override bool motion_notify_event (Gdk.EventMotion e) { + Gdk.RGBA color = get_color_at ((int) e.x_root, (int) e.y_root); + + moved (color); + + set_magnifier_cursor (); + + return true; + } + + + public override bool scroll_event (Gdk.EventScroll e) { + switch (e.direction) { + case Gdk.ScrollDirection.UP: + if (zoomlevel < max_zoomlevel) { + zoomlevel++; + } + set_magnifier_cursor (); + break; + case Gdk.ScrollDirection.DOWN: + if (zoomlevel > min_zoomlevel) { + zoomlevel--; + } + set_magnifier_cursor (); + break; + default: + break; + } + + return true; + } + + public void set_magnifier_cursor () { + var manager = Gdk.Display.get_default ().get_default_seat (); + + // get cursor position + int px, py; + get_window ().get_device_position (manager.get_pointer (), out px, out py, null); + + var radius = snapsize * zoomlevel / 2; + + // get a small area (snap) meant to be zoomed + var snapped_pixbuf = snap (px - snapsize / 2, py - snapsize / 2, snapsize, snapsize); + + // Zoom that screenshot up, and grab a snapsize-sized piece from the middle + var scaled_pb = snapped_pixbuf.scale_simple ( + snapsize * zoomlevel + shadow_width * 2 , + snapsize * zoomlevel + shadow_width * 2 , + Gdk.InterpType.NEAREST + ); + + + // Create the base surface for our cursor + var base_surface = new Cairo.ImageSurface ( + Cairo.Format.ARGB32, + snapsize * zoomlevel + shadow_width * 2 , + snapsize * zoomlevel + shadow_width * 2 + ); + + var base_context = new Cairo.Context (base_surface); + + + // Create the circular path on our base surface + base_context.arc (radius + shadow_width, radius + shadow_width, radius, 0, 2 * Math.PI); + + // Paste in the screenshot + Gdk.cairo_set_source_pixbuf (base_context, scaled_pb, 0, 0); + + // Clip to that circular path, keeping the path around for later, and paint the pasted screenshot + base_context.save (); + base_context.clip_preserve (); + base_context.paint (); + base_context.restore (); + + + // Draw a shadow as outside magnifier border + double shadow_alpha = 0.6; + base_context.set_line_width (1); + + for (int i = 0; i <= shadow_width; i++) { + base_context.arc ( + radius + shadow_width, radius + shadow_width, + radius + shadow_width - i, 0, 2 * Math.PI + ); + Gdk.RGBA shadow_color = Gdk.RGBA (); + shadow_color.parse (DARK_BORDER_COLOR_STRING); + shadow_color.alpha = shadow_alpha / ((shadow_width - i + 1) * (shadow_width - i + 1)); + Gdk.cairo_set_source_rgba (base_context, shadow_color); + base_context.stroke (); + } + + + // Draw an outside bright magnifier border + Gdk.cairo_set_source_rgba (base_context, bright_border_color); + base_context.arc (radius + shadow_width, radius + shadow_width, radius - 1, 0, 2 * Math.PI); + base_context.stroke (); + + + // Draw inside square + base_context.set_line_width (1); + + Gdk.cairo_set_source_rgba (base_context, dark_border_color); + base_context.move_to (radius + shadow_width - zoomlevel, radius + shadow_width - zoomlevel); + base_context.line_to (radius + shadow_width + zoomlevel, radius + shadow_width - zoomlevel); + base_context.line_to (radius + shadow_width + zoomlevel, radius + shadow_width + zoomlevel); + base_context.line_to (radius + shadow_width - zoomlevel, radius + shadow_width + zoomlevel); + base_context.close_path (); + base_context.stroke (); + + Gdk.cairo_set_source_rgba (base_context, bright_border_color); + base_context.move_to (radius + shadow_width - zoomlevel + 1, radius + shadow_width - zoomlevel + 1); + base_context.line_to (radius + shadow_width + zoomlevel - 1, radius + shadow_width - zoomlevel + 1); + base_context.line_to (radius + shadow_width + zoomlevel - 1, radius + shadow_width + zoomlevel - 1); + base_context.line_to (radius + shadow_width - zoomlevel + 1, radius + shadow_width + zoomlevel - 1); + base_context.close_path (); + base_context.stroke (); + + + magnifier = new Gdk.Cursor.from_surface ( + get_screen ().get_display (), + base_surface, + base_surface.get_width () / 2, + base_surface.get_height () / 2); + + // Set the cursor + manager.grab ( + get_window (), + Gdk.SeatCapabilities.ALL, + true, + magnifier, + new Gdk.Event (Gdk.EventType.BUTTON_PRESS | Gdk.EventType.MOTION_NOTIFY | Gdk.EventType.SCROLL), + null); + + } + + + public Gdk.Pixbuf? snap (int x, int y, int w, int h) { + var root = Gdk.get_default_root_window (); + + var screenshot = Gdk.pixbuf_get_from_window (root, x, y, w, h); + return screenshot; + } + + + public override bool key_press_event (Gdk.EventKey e) { + var manager = Gdk.Display.get_default ().get_default_seat (); + int px, py; + get_window ().get_device_position (manager.get_pointer (), out px, out py, null); + + switch (e.keyval) { + case Gdk.Key.Escape: + cancelled (); + break; + case Gdk.Key.Return: + Gdk.RGBA color = get_color_at (px, py); + picked (color); + break; + case Gdk.Key.Up: + manager.get_pointer ().warp (get_screen (), px, py - 1); + break; + case Gdk.Key.Down: + manager.get_pointer ().warp (get_screen (), px, py + 1); + break; + case Gdk.Key.Left: + manager.get_pointer ().warp (get_screen (), px - 1, py); + break; + case Gdk.Key.Right: + manager.get_pointer ().warp (get_screen (), px + 1, py); + break; + } + + key_pressed (e); + + return true; + } + + public override bool key_release_event (Gdk.EventKey e) { + key_released (e); + + return true; + } + + public Gdk.RGBA get_color_at (int x, int y) { + var root = Gdk.get_default_root_window (); + Gdk.Pixbuf? pixbuf = Gdk.pixbuf_get_from_window (root, x, y, 1, 1); + + if (pixbuf != null) { + // see https://hackage.haskell.org/package/gtk3-0.14.6/docs/Graphics-UI-Gtk-Gdk-Pixbuf.html + uint8 red = pixbuf.get_pixels ()[0]; + uint8 green = pixbuf.get_pixels ()[1]; + uint8 blue = pixbuf.get_pixels ()[2]; + + Gdk.RGBA color = Gdk.RGBA (); + string spec = "rgb(" + red.to_string () + "," + green.to_string () + "," + blue.to_string () + ")"; + if (color.parse (spec)) { + return color; + } else { + stdout.printf ("ERROR: Parse pixel rgb values failed."); + } + } + + // fallback: default RGBA color + stdout.printf ("ERROR: Gdk.pixbuf_get_from_window failed"); + return Gdk.RGBA (); + } + + + public override void show_all () { + base.show_all (); + + var manager = Gdk.Display.get_default ().get_default_seat (); + var window = get_window (); + + var status = manager.grab ( + window, + Gdk.SeatCapabilities.ALL, + false, + new Gdk.Cursor.for_display (window.get_display (), Gdk.CursorType.CROSSHAIR), + new Gdk.Event (Gdk.EventType.BUTTON_PRESS | Gdk.EventType.BUTTON_RELEASE | Gdk.EventType.MOTION_NOTIFY), + null); + + if (status != Gdk.GrabStatus.SUCCESS) { + manager.ungrab (); + } + + // show magnifier + set_magnifier_cursor (); + } + + public new void close () { + // TODO remove the zoom level saving if we do not need it + // save zoomlevel + // settings.zoomlevel = zoomlevel; + + get_window ().set_cursor (null); + base.close (); + } +} diff --git a/src/meson.build b/src/meson.build index bd5bece13..6d6c1310c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -35,6 +35,7 @@ sources = files( 'Utils/AffineTransform.vala', 'Utils/Color.vala', 'Utils/Image.vala', + 'Utils/ColorPicker.vala', 'Layouts/HeaderBar.vala', 'Layouts/LeftSideBar.vala',