1/*
2* Copyright (c) 2009-2013 Yorba Foundation
3*               2017 elementary LLC.
4*
5* This program is free software; you can redistribute it and/or
6* modify it under the terms of the GNU Lesser General Public
7* License as published by the Free Software Foundation; either
8* version 2.1 of the License, or (at your option) any later version.
9*
10* This program is distributed in the hope that it will be useful,
11* but WITHOUT ANY WARRANTY; without even the implied warranty of
12* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13* General Public License for more details.
14*
15* You should have received a copy of the GNU General Public
16* License along with this program; if not, write to the
17* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18* Boston, MA 02110-1301 USA
19*/
20
21public class RGBHistogramManipulator : Gtk.DrawingArea {
22    private enum LocationCode { LEFT_NUB, RIGHT_NUB, LEFT_TROUGH, RIGHT_TROUGH,
23                                INSENSITIVE_AREA
24                              }
25    private const int NUB_SIZE = 13;
26    private const int NUB_HALF_WIDTH = NUB_SIZE / 2;
27    private const int NUB_V_NUDGE = 4;
28    private const int TROUGH_WIDTH = 256 + (2 * NUB_HALF_WIDTH);
29    private const int TROUGH_HEIGHT = 4;
30    private const int TROUGH_BOTTOM_OFFSET = 1;
31    private const int CONTROL_WIDTH = TROUGH_WIDTH + 2;
32    private const int CONTROL_HEIGHT = 118;
33    private const int NUB_V_POSITION = CONTROL_HEIGHT - TROUGH_HEIGHT - TROUGH_BOTTOM_OFFSET
34                                       - (NUB_SIZE - TROUGH_HEIGHT) / 2 - NUB_V_NUDGE - 2;
35    private int left_nub_max = 255 - NUB_SIZE - 1;
36    private int right_nub_min = NUB_SIZE + 1;
37
38    private static Gtk.Widget dummy_slider = null;
39    private static Gtk.Widget dummy_frame = null;
40    private static Gtk.WidgetPath slider_draw_path = new Gtk.WidgetPath ();
41    private static Gtk.WidgetPath frame_draw_path = new Gtk.WidgetPath ();
42    private static bool paths_setup = false;
43
44    private RGBHistogram histogram = null;
45    private int left_nub_position = 0;
46    private int right_nub_position = 255;
47    private bool is_left_nub_tracking = false;
48    private bool is_right_nub_tracking = false;
49    private int track_start_x = 0;
50    private int track_nub_start_position = 0;
51
52    private Gdk.Pixbuf? drag_nub_pixbuf = null;
53
54    private Gdk.Pixbuf? get_drag_nub_pixbuf () {
55        if (drag_nub_pixbuf == null) {
56            try {
57                drag_nub_pixbuf = new Gdk.Pixbuf.from_resource ("/io/elementary/photos/icons/drag-nub.svg");
58            } catch (Error err) {
59                error ("Can't load drag nub image: %s", err.message);
60            }
61        }
62
63        return drag_nub_pixbuf;
64    }
65
66    public RGBHistogramManipulator () {
67        set_size_request (CONTROL_WIDTH, CONTROL_HEIGHT);
68
69        if (dummy_slider == null)
70            dummy_slider = new Gtk.Scale (Gtk.Orientation.HORIZONTAL, null);
71
72        if (dummy_frame == null)
73            dummy_frame = new Gtk.Frame (null);
74
75        if (!paths_setup) {
76            slider_draw_path.append_type (typeof (Gtk.Scale));
77            slider_draw_path.iter_add_class (0, "scale");
78            slider_draw_path.iter_add_class (0, "range");
79
80            frame_draw_path.append_type (typeof (Gtk.Frame));
81            frame_draw_path.iter_add_class (0, "default");
82
83            paths_setup = true;
84        }
85
86        add_events (Gdk.EventMask.BUTTON_PRESS_MASK);
87        add_events (Gdk.EventMask.BUTTON_RELEASE_MASK);
88        add_events (Gdk.EventMask.BUTTON_MOTION_MASK);
89
90        button_press_event.connect (on_button_press);
91        button_release_event.connect (on_button_release);
92        motion_notify_event.connect (on_button_motion);
93    }
94
95    private LocationCode hit_test_point (int x, int y) {
96        if (y < NUB_V_POSITION)
97            return LocationCode.INSENSITIVE_AREA;
98
99        if ((x > left_nub_position) && (x < left_nub_position + NUB_SIZE))
100            return LocationCode.LEFT_NUB;
101
102        if ((x > right_nub_position) && (x < right_nub_position + NUB_SIZE))
103            return LocationCode.RIGHT_NUB;
104
105        if (y < (NUB_V_POSITION + NUB_V_NUDGE + 1))
106            return LocationCode.INSENSITIVE_AREA;
107
108        if ((x - left_nub_position) * (x - left_nub_position) <
109                (x - right_nub_position) * (x - right_nub_position))
110            return LocationCode.LEFT_TROUGH;
111        else
112            return LocationCode.RIGHT_TROUGH;
113    }
114
115    private bool on_button_press (Gdk.EventButton event_record) {
116        LocationCode loc = hit_test_point ((int) event_record.x, (int) event_record.y);
117
118        switch (loc) {
119        case LocationCode.LEFT_NUB:
120            track_start_x = ((int) event_record.x);
121            track_nub_start_position = left_nub_position;
122            is_left_nub_tracking = true;
123            return true;
124
125        case LocationCode.RIGHT_NUB:
126            track_start_x = ((int) event_record.x);
127            track_nub_start_position = right_nub_position;
128            is_right_nub_tracking = true;
129            return true;
130
131        case LocationCode.LEFT_TROUGH:
132            left_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
133            left_nub_position = left_nub_position.clamp (0, left_nub_max);
134            force_update ();
135            nub_position_changed ();
136            update_nub_extrema ();
137            return true;
138
139        case LocationCode.RIGHT_TROUGH:
140            right_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
141            right_nub_position = right_nub_position.clamp (right_nub_min, 255);
142            force_update ();
143            nub_position_changed ();
144            update_nub_extrema ();
145            return true;
146
147        default:
148            return false;
149        }
150    }
151
152    private bool on_button_release (Gdk.EventButton event_record) {
153        if (is_left_nub_tracking || is_right_nub_tracking) {
154            nub_position_changed ();
155            update_nub_extrema ();
156        }
157
158        is_left_nub_tracking = false;
159        is_right_nub_tracking = false;
160
161        return false;
162    }
163
164    private bool on_button_motion (Gdk.EventMotion event_record) {
165        if ((!is_left_nub_tracking) && (!is_right_nub_tracking))
166            return false;
167
168        if (is_left_nub_tracking) {
169            int track_x_delta = ((int) event_record.x) - track_start_x;
170            left_nub_position = (track_nub_start_position + track_x_delta);
171            left_nub_position = left_nub_position.clamp (0, left_nub_max);
172        } else { /* right nub is tracking */
173            int track_x_delta = ((int) event_record.x) - track_start_x;
174            right_nub_position = (track_nub_start_position + track_x_delta);
175            right_nub_position = right_nub_position.clamp (right_nub_min, 255);
176        }
177
178        force_update ();
179        return true;
180    }
181
182    public override bool draw (Cairo.Context ctx) {
183        Gtk.Border padding = get_style_context ().get_padding (Gtk.StateFlags.NORMAL);
184
185        Gdk.Rectangle area = Gdk.Rectangle ();
186        area.x = padding.left;
187        area.y = padding.top;
188        area.width = RGBHistogram.GRAPHIC_WIDTH + padding.right;
189        area.height = RGBHistogram.GRAPHIC_HEIGHT + padding.bottom;
190
191        draw_histogram_frame (ctx, area);
192        draw_histogram (ctx, area);
193        draw_trough (ctx, area);
194        draw_nub (ctx, area, left_nub_position);
195        draw_nub (ctx, area, right_nub_position);
196
197        return true;
198    }
199
200    private void draw_histogram_frame (Cairo.Context ctx, Gdk.Rectangle area) {
201        // the framed area is inset and slightly smaller than the overall histogram
202        // control area
203        Gdk.Rectangle framed_area = area;
204        framed_area.x += 5;
205        framed_area.y += 1;
206        framed_area.width -= 8;
207        framed_area.height -= 12;
208
209        Gtk.StyleContext stylectx = dummy_frame.get_style_context ();
210        stylectx.save ();
211
212        stylectx.get_path ().append_type (typeof (Gtk.Frame));
213        stylectx.get_path ().iter_add_class (0, "default");
214        stylectx.add_class (Gtk.STYLE_CLASS_TROUGH);
215        stylectx.set_junction_sides (Gtk.JunctionSides.TOP | Gtk.JunctionSides.BOTTOM |
216                                     Gtk.JunctionSides.LEFT | Gtk.JunctionSides.RIGHT);
217
218        stylectx.render_frame (ctx, framed_area.x, framed_area.y, framed_area.width,
219                               framed_area.height);
220
221        stylectx.restore ();
222    }
223
224    private void draw_histogram (Cairo.Context ctx, Gdk.Rectangle area) {
225        if (histogram == null)
226            return;
227
228        Gdk.Pixbuf histogram_graphic = histogram.get_graphic ().copy ();
229        unowned uchar[] pixel_data = histogram_graphic.get_pixels ();
230
231        int edge_blend_red = 0;
232        int edge_blend_green = 0;
233        int edge_blend_blue = 0;
234        int body_blend_red = 20;
235        int body_blend_green = 20;
236        int body_blend_blue = 20;
237
238        if (left_nub_position > 0) {
239            int edge_pixel_index = histogram_graphic.n_channels * left_nub_position;
240            for (int i = 0; i < histogram_graphic.height; i++) {
241                int body_pixel_index = i * histogram_graphic.rowstride;
242                int row_last_pixel = body_pixel_index + histogram_graphic.n_channels *
243                                     left_nub_position;
244                while (body_pixel_index < row_last_pixel) {
245                    pixel_data[body_pixel_index] =
246                        (uchar) ((pixel_data[body_pixel_index] + body_blend_red) / 2);
247                    pixel_data[body_pixel_index + 1] =
248                        (uchar) ((pixel_data[body_pixel_index + 1] + body_blend_green) / 2);
249                    pixel_data[body_pixel_index + 2] =
250                        (uchar) ((pixel_data[body_pixel_index + 2] + body_blend_blue) / 2);
251
252                    body_pixel_index += histogram_graphic.n_channels;
253                }
254
255                pixel_data[edge_pixel_index] =
256                    (uchar) ((pixel_data[edge_pixel_index] + edge_blend_red) / 2);
257                pixel_data[edge_pixel_index + 1] =
258                    (uchar) ((pixel_data[edge_pixel_index + 1] + edge_blend_green) / 2);
259                pixel_data[edge_pixel_index + 2] =
260                    (uchar) ((pixel_data[edge_pixel_index + 2] + edge_blend_blue) / 2);
261
262                edge_pixel_index += histogram_graphic.rowstride;
263            }
264        }
265
266        edge_blend_red = 250;
267        edge_blend_green = 250;
268        edge_blend_blue = 250;
269        body_blend_red = 200;
270        body_blend_green = 200;
271        body_blend_blue = 200;
272
273        if (right_nub_position < 255) {
274            int edge_pixel_index = histogram_graphic.n_channels * right_nub_position;
275            for (int i = 0; i < histogram_graphic.height; i++) {
276                int body_pixel_index = i * histogram_graphic.rowstride +
277                                       histogram_graphic.n_channels * 255;
278                int row_last_pixel = i * histogram_graphic.rowstride +
279                                     histogram_graphic.n_channels * right_nub_position;
280                while (body_pixel_index > row_last_pixel) {
281                    pixel_data[body_pixel_index] =
282                        (uchar) ((pixel_data[body_pixel_index] + body_blend_red) / 2);
283                    pixel_data[body_pixel_index + 1] =
284                        (uchar) ((pixel_data[body_pixel_index + 1] + body_blend_green) / 2);
285                    pixel_data[body_pixel_index + 2] =
286                        (uchar) ((pixel_data[body_pixel_index + 2] + body_blend_blue) / 2);
287
288                    body_pixel_index -= histogram_graphic.n_channels;
289                }
290                pixel_data[edge_pixel_index] =
291                    (uchar) ((pixel_data[edge_pixel_index] + edge_blend_red) / 2);
292                pixel_data[edge_pixel_index + 1] =
293                    (uchar) ((pixel_data[edge_pixel_index + 1] + edge_blend_green) / 2);
294                pixel_data[edge_pixel_index + 2] =
295                    (uchar) ((pixel_data[edge_pixel_index + 2] + edge_blend_blue) / 2);
296
297                edge_pixel_index += histogram_graphic.rowstride;
298            }
299        }
300
301        Gdk.cairo_set_source_pixbuf (ctx, histogram_graphic, area.x + NUB_HALF_WIDTH, area.y + 2);
302        ctx.paint ();
303    }
304
305    private void draw_trough (Cairo.Context ctx, Gdk.Rectangle area) {
306        int trough_x = area.x;
307        int trough_y = area.y + (CONTROL_HEIGHT - TROUGH_HEIGHT - TROUGH_BOTTOM_OFFSET - 3);
308
309        Gtk.StyleContext stylectx = dummy_slider.get_style_context ();
310        stylectx.save ();
311
312        stylectx.get_path ().append_type (typeof (Gtk.Scale));
313        stylectx.get_path ().iter_add_class (0, "scale");
314        stylectx.add_class (Gtk.STYLE_CLASS_TROUGH);
315
316        stylectx.render_activity (ctx, trough_x, trough_y, TROUGH_WIDTH, TROUGH_HEIGHT);
317
318        stylectx.restore ();
319    }
320
321    private void draw_nub (Cairo.Context ctx, Gdk.Rectangle area, int position) {
322        Gdk.cairo_set_source_pixbuf (ctx, get_drag_nub_pixbuf (), area.x + position, area.y + NUB_V_POSITION);
323        ctx.paint ();
324    }
325
326    private void force_update () {
327        get_window ().invalidate_rect (null, true);
328        get_window ().process_updates (true);
329    }
330
331    private void update_nub_extrema () {
332        right_nub_min = left_nub_position + NUB_SIZE + 1;
333        left_nub_max = right_nub_position - NUB_SIZE - 1;
334    }
335
336    public signal void nub_position_changed ();
337
338    public void update_histogram (Gdk.Pixbuf source_pixbuf) {
339        histogram = new RGBHistogram (source_pixbuf);
340        force_update ();
341    }
342
343    public int get_left_nub_position () {
344        return left_nub_position;
345    }
346
347    public int get_right_nub_position () {
348        return right_nub_position;
349    }
350
351    public void set_left_nub_position (int user_nub_pos) {
352        assert ((user_nub_pos >= 0) && (user_nub_pos <= 255));
353        left_nub_position = user_nub_pos.clamp (0, left_nub_max);
354        update_nub_extrema ();
355    }
356
357    public void set_right_nub_position (int user_nub_pos) {
358        assert ((user_nub_pos >= 0) && (user_nub_pos <= 255));
359        right_nub_position = user_nub_pos.clamp (right_nub_min, 255);
360        update_nub_extrema ();
361    }
362}
363