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