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',