croptool.c (27730B)
1 /* 2 * Copyright (c) 2021-2026 lumidify <nobody@lumidify.org> 3 * 4 * Permission to use, copy, modify, and/or distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 #include <math.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #include <unistd.h> 21 22 #include <X11/X.h> 23 #include <X11/Xlib.h> 24 #include <X11/Xutil.h> 25 #include <X11/keysym.h> 26 #include <X11/cursorfont.h> 27 28 #include <Imlib2.h> 29 30 #include "common.h" 31 32 /* The number of pixels to check on each side when checking 33 * if a corner or edge of the selection box was clicked 34 * (in order to change the size of the box) */ 35 static int COLLISION_PADDING = 10; 36 /* The color of the selection box */ 37 static const char *SELECTION_COLOR1 = "#000000"; 38 /* The second selection color - when tab is pressed */ 39 static const char *SELECTION_COLOR2 = "#FFFFFF"; 40 /* The width of the selection line */ 41 static int LINE_WIDTH = 2; 42 /* When set to 1, the display is redrawn on window resize */ 43 static short RESIZE_REDRAW = 1; 44 /* When set to 1, the selection is redrawn continually, 45 not just when the mouse button is released */ 46 static short SELECTION_REDRAW = 1; 47 /* 48 The command printed for each image. 49 %w: Width of cropped area. 50 %h: Height of cropped area. 51 %l: Left side of cropped area. 52 %r: Right side of cropped area. 53 %t: Top side of cropped area. 54 %b: Bottom side of cropped area. 55 %f: Filename of image. 56 */ 57 static const char *CMD_FORMAT = "croptool_crop %wx%h+%l+%t '%f'"; 58 /* Size of Imlib2 in-memory cache in MiB */ 59 static int CACHE_SIZE = 4; 60 61 extern char *optarg; 62 extern int optind; 63 64 struct Rect { 65 int x0; 66 int y0; 67 int x1; 68 int y1; 69 }; 70 71 struct Point { 72 int x; 73 int y; 74 }; 75 76 /* Note that the ordering of the points in the rectangles is 77 used to store which side is being dragged when resizing 78 a selection. This means that the points need to be sorted 79 using sort_coordinates if they need to be in a canonical 80 form such that x0 <= x1 and y0 <= y1. */ 81 struct Selection { 82 /* scaled_rect can always be calculated from real_rect, 83 but it's convenient to have it stored separately for 84 functions like set_cursor_for_selection that use it a lot */ 85 struct Rect real_rect; 86 struct Rect scaled_rect; 87 ImageSize sz; 88 char rect_valid; 89 /* sz_valid should be implied for the current selection 90 by state.ctx.cur_image being valid, but this makes it 91 a bit more explicit */ 92 char sz_valid; 93 }; 94 95 static struct { 96 GraphicsContext ctx; 97 98 struct Selection *selections; 99 char **filenames; 100 int cur_selection; 101 int num_files; 102 int cursor_x; 103 int cursor_y; 104 struct Point move_handle; 105 XColor col1; 106 XColor col2; 107 int cur_col; 108 struct { 109 struct Rect rect; 110 char valid; 111 char scaled; 112 } copied_selection; 113 char moving; 114 char resizing; 115 char lock_x; 116 char lock_y; 117 char print_on_exit; 118 } state; 119 120 static struct { 121 Cursor top; 122 Cursor bottom; 123 Cursor left; 124 Cursor right; 125 Cursor topleft; 126 Cursor topright; 127 Cursor bottomleft; 128 Cursor bottomright; 129 Cursor grab; 130 } cursors; 131 132 static void usage(void); 133 static void mainloop(void); 134 static void setup(int argc, char *argv[]); 135 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1); 136 static void swap(int *a, int *b); 137 static void redraw(void); 138 static void print_cmd(const char *filename, int x, int y, int w, int h, int dry_run); 139 static void print_selection(struct Selection *sel, const char *filename); 140 static int collide_point(int x, int y, int x_point, int y_point); 141 static int collide_line(int x, int y, int x0, int y0, int x1, int y1); 142 static int collide_rect(int x, int y, struct Rect rect); 143 static void switch_color(void); 144 static void scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret); 145 static void set_scaled_selection_rect(struct Selection *sel, struct Rect rect); 146 static void set_real_selection_rect(struct Selection *sel, struct Rect rect); 147 static void copy_selection(int scaled); 148 static void paste_selection(void); 149 static void select_all(void); 150 static void clear_selection(void); 151 static void queue_update(int x, int y, int w, int h); 152 static void queue_rectangle_redraw(int x0, int y0, int x1, int y1); 153 static void set_cursor(struct Rect rect); 154 static void reset_cursor(void); 155 static void set_cursor_for_selection(struct Selection *sel); 156 static void drag_motion(XEvent event); 157 static void resize_window(int w, int h); 158 static void button_release(void); 159 static void button_press(XEvent event); 160 static int key_press(XEvent event); 161 162 static void 163 usage(void) { 164 fprintf(stderr, "USAGE: croptool [-mr] [-f format] " 165 "[-w width] [-c padding] [-p color] [-s color] " 166 "[-z size] file ...\n"); 167 } 168 169 int 170 main(int argc, char *argv[]) { 171 char c; 172 173 while ((c = getopt(argc, argv, "f:w:c:mrp:s:z:")) != -1) { 174 switch (c) { 175 case 'f': 176 CMD_FORMAT = optarg; 177 break; 178 case 'm': 179 RESIZE_REDRAW = 0; 180 break; 181 case 'r': 182 SELECTION_REDRAW = 0; 183 break; 184 case 'p': 185 SELECTION_COLOR1 = optarg; 186 break; 187 case 's': 188 SELECTION_COLOR2 = optarg; 189 break; 190 case 'c': 191 if (parse_int(optarg, 1, 99, &COLLISION_PADDING)) { 192 fprintf(stderr, "Invalid collision padding.\n"); 193 exit(1); 194 } 195 break; 196 case 'w': 197 if (parse_int(optarg, 1, 99, &LINE_WIDTH)) { 198 fprintf(stderr, "Invalid line width.\n"); 199 exit(1); 200 } 201 break; 202 case 'z': 203 if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) { 204 fprintf(stderr, "Invalid cache size.\n"); 205 exit(1); 206 } 207 break; 208 default: 209 usage(); 210 exit(1); 211 break; 212 } 213 } 214 /* print warning if command format is invalid */ 215 print_cmd("", 0, 0, 0, 0, 1); 216 217 argc -= optind; 218 argv += optind; 219 if (argc < 1) { 220 usage(); 221 exit(1); 222 } 223 setup(argc, argv); 224 225 mainloop(); 226 227 if (state.print_on_exit) { 228 for (int i = 0; i < argc; i++) { 229 if (state.selections[i].rect_valid) { 230 print_selection(&state.selections[i], state.filenames[i]); 231 } 232 } 233 } 234 235 cleanup(); 236 237 return 0; 238 } 239 240 static void 241 mainloop(void) { 242 XEvent event; 243 int running = 1; 244 245 while (running) { 246 do { 247 XNextEvent(state.ctx.dpy, &event); 248 switch (event.type) { 249 case Expose: 250 if (RESIZE_REDRAW) 251 queue_update(event.xexpose.x, event.xexpose.y, 252 event.xexpose.width, event.xexpose.height); 253 break; 254 case ConfigureNotify: 255 if (RESIZE_REDRAW) 256 resize_window( 257 event.xconfigure.width, 258 event.xconfigure.height 259 ); 260 break; 261 case ButtonPress: 262 if (event.xbutton.button == Button1) 263 button_press(event); 264 break; 265 case ButtonRelease: 266 if (event.xbutton.button == Button1) 267 button_release(); 268 break; 269 case MotionNotify: 270 drag_motion(event); 271 break; 272 case KeyPress: 273 running = key_press(event); 274 break; 275 case ClientMessage: 276 if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg) 277 running = 0; 278 default: 279 break; 280 } 281 } while (XPending(state.ctx.dpy)); 282 283 redraw(); 284 } 285 } 286 287 static void 288 setup(int argc, char *argv[]) { 289 state.selections = malloc(argc * sizeof(struct Selection)); 290 if (!state.selections) { 291 fprintf(stderr, "Unable to allocate memory.\n"); 292 exit(1); 293 } 294 state.num_files = argc; 295 state.filenames = argv; 296 state.cur_selection = -1; 297 state.moving = 0; 298 state.resizing = 0; 299 state.lock_x = 0; 300 state.lock_y = 0; 301 state.cursor_x = 0; 302 state.cursor_y = 0; 303 state.cur_col = 1; 304 state.print_on_exit = 0; 305 state.copied_selection.valid = 0; 306 307 for (int i = 0; i < argc; i++) { 308 state.selections[i].rect_valid = 0; 309 state.selections[i].sz_valid = 0; 310 } 311 312 setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE); 313 314 if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR1, &state.col1)) { 315 fprintf(stderr, "Primary color invalid.\n"); 316 exit(1); 317 } 318 XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1); 319 if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR2, &state.col2)) { 320 fprintf(stderr, "Secondary color invalid.\n"); 321 exit(1); 322 } 323 XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2); 324 325 cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side); 326 cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side); 327 cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side); 328 cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side); 329 cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner); 330 cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner); 331 cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner); 332 cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner); 333 cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur); 334 335 next_picture(state.cur_selection, state.filenames, state.num_files, 0); 336 /* Only map window here so the program exits immediately if 337 there are no loadable images, without first opening the 338 window and closing it again immediately */ 339 XMapWindow(state.ctx.dpy, state.ctx.win); 340 redraw(); 341 } 342 343 void 344 cleanup(void) { 345 free(state.selections); 346 cleanup_x(&state.ctx); 347 } 348 349 /* TODO: Escape filename properly 350 * -> But how? Since the format can be set by the user, 351 * it isn't really clear *what* needs to be escaped. */ 352 static void 353 print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) { 354 short percent = 0; 355 const char *c; 356 int length = 0; 357 int start_index = 0; 358 /* FIXME: just use putc instead of this complex printf dance */ 359 for (c = CMD_FORMAT; *c != '\0'; c++) { 360 if (percent) 361 start_index++; 362 if (*c == '%') { 363 if (length) { 364 if (!dry_run) 365 printf("%.*s", length, CMD_FORMAT + start_index); 366 start_index += length; 367 length = 0; 368 } 369 if (percent && !dry_run) 370 printf("%%"); 371 percent++; 372 percent %= 2; 373 start_index++; 374 } else if (percent && *c == 'w') { 375 if (!dry_run) 376 printf("%d", w); 377 percent = 0; 378 } else if (percent && *c == 'h') { 379 if (!dry_run) 380 printf("%d", h); 381 percent = 0; 382 } else if (percent && *c == 'l') { 383 if (!dry_run) 384 printf("%d", x); 385 percent = 0; 386 } else if (percent && *c == 't') { 387 if (!dry_run) 388 printf("%d", y); 389 percent = 0; 390 } else if (percent && *c == 'r') { 391 if (!dry_run) 392 printf("%d", x + w); 393 percent = 0; 394 } else if (percent && *c == 'b') { 395 if (!dry_run) 396 printf("%d", y + h); 397 percent = 0; 398 } else if (percent && *c == 'f') { 399 if (!dry_run) 400 printf("%s", filename); 401 percent = 0; 402 } else if (percent) { 403 if (dry_run) { 404 fprintf(stderr, 405 "Warning: Unknown substitution '%c' " 406 "in format string.\n", *c 407 ); 408 } else { 409 printf("%%%c", *c); 410 } 411 percent = 0; 412 } else { 413 length++; 414 } 415 } 416 if (!dry_run) { 417 if (length) 418 printf("%.*s", length, CMD_FORMAT + start_index); 419 printf("\n"); 420 } 421 } 422 423 static void 424 redraw(void) { 425 if (!state.ctx.dirty) 426 return; 427 if (!state.ctx.cur_image || state.cur_selection < 0) { 428 clear_screen(&state.ctx); 429 swap_buffers(&state.ctx); 430 return; 431 } 432 433 /* draw the parts of the image that need to be redrawn */ 434 struct Selection *sel = &state.selections[state.cur_selection]; 435 /* Should never be invalid at this point, but just check anyways */ 436 if (sel->sz_valid) { 437 draw_image_updates(&state.ctx, &sel->sz); 438 wipe_around_image(&state.ctx, &sel->sz); 439 } 440 441 /* draw the rectangle */ 442 if (sel->rect_valid) { 443 struct Rect rect = sel->scaled_rect; 444 XColor col = state.cur_col == 1 ? state.col1 : state.col2; 445 XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel); 446 sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); 447 XDrawRectangle( 448 state.ctx.dpy, state.ctx.drawable, state.ctx.gc, 449 rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0 450 ); 451 } 452 swap_buffers(&state.ctx); 453 } 454 455 static void 456 swap(int *a, int *b) { 457 int tmp = *a; 458 *a = *b; 459 *b = tmp; 460 } 461 462 /* sort rectangle coordinates into their canonical 463 * form so *x1 - *x0 >= 0 and *y1 - *y0 >= 0 */ 464 static void 465 sort_coordinates(int *x0, int *y0, int *x1, int *y1) { 466 if (*x0 > *x1) 467 swap(x0, x1); 468 if(*y0 > *y1) 469 swap(y0, y1); 470 } 471 472 static void 473 print_selection(struct Selection *sel, const char *filename) { 474 if (!sel->rect_valid) 475 return; 476 int x0 = sel->real_rect.x0; 477 int y0 = sel->real_rect.y0; 478 int x1 = sel->real_rect.x1; 479 int y1 = sel->real_rect.y1; 480 sort_coordinates(&x0, &y0, &x1, &y1); 481 /* The box is completely outside of the picture. */ 482 if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h) 483 return; 484 /* Cut the bounding box if it goes past the end of the picture. */ 485 x0 = x0 < 0 ? 0 : x0; 486 y0 = y0 < 0 ? 0 : y0; 487 x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1; 488 y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1; 489 print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0); 490 } 491 492 static int 493 collide_point(int x, int y, int x_point, int y_point) { 494 return (abs(x - x_point) <= COLLISION_PADDING) && 495 (abs(y - y_point) <= COLLISION_PADDING); 496 } 497 498 static int 499 collide_line(int x, int y, int x0, int y0, int x1, int y1) { 500 sort_coordinates(&x0, &y0, &x1, &y1); 501 /* this expects a valid line */ 502 if (x0 == x1) { 503 return (abs(x - x0) <= COLLISION_PADDING) && 504 (y0 <= y) && (y <= y1); 505 } else { 506 return (abs(y - y0) <= COLLISION_PADDING) && 507 (x0 <= x) && (x <= x1); 508 } 509 } 510 511 static int 512 collide_rect(int x, int y, struct Rect rect) { 513 int x0 = rect.x0, x1 = rect.x1; 514 int y0 = rect.y0, y1 = rect.y1; 515 sort_coordinates(&x0, &y0, &x1, &y1); 516 return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1); 517 } 518 519 static void 520 button_press(XEvent event) { 521 if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid) 522 return; 523 struct Selection *sel = &state.selections[state.cur_selection]; 524 struct Rect *rect = &sel->scaled_rect; 525 int x = event.xbutton.x; 526 int y = event.xbutton.y; 527 int x0 = rect->x0, x1 = rect->x1; 528 int y0 = rect->y0, y1 = rect->y1; 529 /* if old rect was invalid, just set it to something 530 that will definitely not collide with the mouse position */ 531 if (!sel->rect_valid) { 532 x0 = y0 = x1 = y1 = -200; 533 } else { 534 /* erase old rectangle */ 535 queue_rectangle_redraw(x0, y0, x1, y1); 536 } 537 if (collide_point(x, y, x0, y0)) { 538 *rect = (struct Rect){x1, y1, x, y}; 539 } else if (collide_point(x, y, x1, y1)) { 540 rect->x1 = x; 541 rect->y1 = y; 542 } else if (collide_point(x, y, x0, y1)) { 543 rect->x0 = rect->x1; 544 rect->x1 = x; 545 rect->y1 = y; 546 } else if (collide_point(x, y, x1, y0)) { 547 rect->y0 = y1; 548 rect->x1 = x; 549 rect->y1 = y; 550 } else if (collide_line(x, y, x0, y0, x1, y0)) { 551 state.lock_y = 1; 552 swap(&rect->x0, &rect->x1); 553 rect->y0 = rect->y1; 554 rect->y1 = y; 555 } else if (collide_line(x, y, x0, y0, x0, y1)) { 556 state.lock_x = 1; 557 swap(&rect->y0, &rect->y1); 558 rect->x0 = rect->x1; 559 rect->x1 = x; 560 } else if (collide_line(x, y, x1, y1, x0, y1)) { 561 state.lock_y = 1; 562 rect->y1 = y; 563 } else if (collide_line(x, y, x1, y1, x1, y0)) { 564 state.lock_x = 1; 565 rect->x1 = x; 566 } else if (collide_rect(x, y, (struct Rect){x0, y0, x1, y1})) { 567 state.moving = 1; 568 state.move_handle.x = x; 569 state.move_handle.y = y; 570 } else { 571 *rect = (struct Rect){x, y, x, y}; 572 } 573 state.resizing = 1; 574 set_scaled_selection_rect(sel, *rect); 575 } 576 577 static void 578 queue_update(int x, int y, int w, int h) { 579 if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid) 580 return; 581 struct Selection *sel = &state.selections[state.cur_selection]; 582 queue_area_update(&state.ctx, &sel->sz, x, y, w, h); 583 } 584 585 static void 586 button_release(void) { 587 state.moving = 0; 588 state.resizing = 0; 589 state.lock_x = 0; 590 state.lock_y = 0; 591 /* redraw everything if automatic redrawing of the rectangle 592 is disabled (so it's redrawn when the mouse is released) */ 593 if (!SELECTION_REDRAW) 594 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h); 595 } 596 597 static void 598 resize_window(int w, int h) { 599 int actual_w, actual_h; 600 struct Selection *sel; 601 state.ctx.window_w = w; 602 state.ctx.window_h = h; 603 604 if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid) 605 return; 606 sel = &state.selections[state.cur_selection]; 607 get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h); 608 if (actual_w != sel->sz.scaled_w || actual_h != sel->sz.scaled_h) { 609 sel->sz.scaled_w = actual_w; 610 sel->sz.scaled_h = actual_h; 611 if (sel->rect_valid) 612 set_real_selection_rect(sel, sel->real_rect); 613 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h); 614 615 } 616 } 617 618 /* queue the redrawing of a rectangular area on the image - 619 * this queues four updates, one for each side of the rectangle, 620 * with the width or height (depending on which side) of the 621 * rectangle being determined by the configured line width */ 622 static void 623 queue_rectangle_redraw(int x0, int y0, int x1, int y1) { 624 sort_coordinates(&x0, &y0, &x1, &y1); 625 queue_update( 626 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 627 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 628 x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); 629 queue_update( 630 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 631 y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0, 632 x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); 633 queue_update( 634 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 635 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 636 LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); 637 queue_update( 638 x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0, 639 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 640 LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); 641 /* this has to be set manually here in case the entire 642 rectangle is outside the image area, as that will 643 cause queue_area_update (called by queue_update) to 644 return immediately without setting the flag, but 645 redraw() still needs to know that the area outside 646 the image needs to be redrawn */ 647 state.ctx.dirty = 1; 648 } 649 650 /* set the appropriate cursor based on the 651 * current mouse position and a cropping rectangle */ 652 static void 653 set_cursor(struct Rect rect) { 654 Cursor c = None; 655 sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); 656 if (collide_point( 657 state.cursor_x, state.cursor_y, 658 rect.x0, rect.y0)) { 659 c = cursors.topleft; 660 } else if (collide_point( 661 state.cursor_x, state.cursor_y, 662 rect.x1, rect.y0)) { 663 c = cursors.topright; 664 } else if (collide_point( 665 state.cursor_x, state.cursor_y, 666 rect.x0, rect.y1)) { 667 c = cursors.bottomleft; 668 } else if (collide_point( 669 state.cursor_x, state.cursor_y, 670 rect.x1, rect.y1)) { 671 c = cursors.bottomright; 672 } else if (collide_line( 673 state.cursor_x, state.cursor_y, 674 rect.x0, rect.y0, rect.x1, rect.y0)) { 675 c = cursors.top; 676 } else if (collide_line( 677 state.cursor_x, state.cursor_y, 678 rect.x1, rect.y1, rect.x0, rect.y1)) { 679 c = cursors.bottom; 680 } else if (collide_line( 681 state.cursor_x, state.cursor_y, 682 rect.x1, rect.y1, rect.x1, rect.y0)) { 683 c = cursors.right; 684 } else if (collide_line( 685 state.cursor_x, state.cursor_y, 686 rect.x0, rect.y0, rect.x0, rect.y1)) { 687 c = cursors.left; 688 } else if (collide_rect(state.cursor_x, state.cursor_y, rect)) { 689 c = cursors.grab; 690 } 691 XDefineCursor(state.ctx.dpy, state.ctx.win, c); 692 } 693 694 static void 695 reset_cursor(void) { 696 XDefineCursor(state.ctx.dpy, state.ctx.win, None); 697 } 698 699 static void 700 set_cursor_for_selection(struct Selection *sel) { 701 if (sel->rect_valid) 702 set_cursor(sel->scaled_rect); 703 else 704 reset_cursor(); 705 } 706 707 static void 708 drag_motion(XEvent event) { 709 if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid) 710 return; 711 struct Selection *sel = &state.selections[state.cur_selection]; 712 struct Rect *rect = &sel->scaled_rect; 713 714 /* don't allow coordinates to go below 0 */ 715 if (event.xbutton.x >= 0) 716 state.cursor_x = event.xbutton.x; 717 else 718 state.cursor_x = 0; 719 if (event.xbutton.y >= 0) 720 state.cursor_y = event.xbutton.y; 721 else 722 state.cursor_y = 0; 723 724 int x0 = rect->x0, x1 = rect->x1; 725 int y0 = rect->y0, y1 = rect->y1; 726 sort_coordinates(&x0, &y0, &x1, &y1); 727 /* redraw the old rectangle */ 728 if (SELECTION_REDRAW && (state.moving || state.resizing)) 729 queue_rectangle_redraw(x0, y0, x1, y1); 730 if (state.moving) { 731 int x_delta = state.cursor_x - state.move_handle.x; 732 int y_delta = state.cursor_y - state.move_handle.y; 733 /* don't allow coordinates to go below 0 */ 734 int x_realdelta = x0 + x_delta >= 0 ? x_delta : -x0; 735 int y_realdelta = y0 + y_delta >= 0 ? y_delta : -y0; 736 rect->x0 += x_realdelta; 737 rect->x1 += x_realdelta; 738 rect->y0 += y_realdelta; 739 rect->y1 += y_realdelta; 740 state.move_handle.x = state.cursor_x; 741 state.move_handle.y = state.cursor_y; 742 } else if (state.resizing) { 743 if (!state.lock_y) 744 rect->x1 = state.cursor_x; 745 if (!state.lock_x) 746 rect->y1 = state.cursor_y; 747 } else { 748 set_cursor_for_selection(sel); 749 /* return directly to skip the redraw */ 750 return; 751 } 752 set_scaled_selection_rect(sel, *rect); 753 set_cursor_for_selection(sel); 754 755 /* redraw the new rectangle */ 756 if (SELECTION_REDRAW) 757 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 758 } 759 760 static void 761 scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret) { 762 sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); 763 rect.x0 = round(rect.x0 * scale); 764 rect.y0 = round(rect.y0 * scale); 765 rect.x1 = round(rect.x1 * scale); 766 rect.y1 = round(rect.y1 * scale); 767 *rect_ret = rect; 768 } 769 770 /* Only the width is taken into account for the scaling functions since the 771 aspect ratio should have been preserved anyways. Of course, this could 772 lead to tiny inaccuracies since the scaled width and height are truncated 773 to integers. */ 774 static void 775 set_scaled_selection_rect(struct Selection *sel, struct Rect rect) { 776 sel->scaled_rect = rect; 777 scale_selection_rect( 778 (double)sel->sz.orig_w / sel->sz.scaled_w, rect, &sel->real_rect 779 ); 780 sel->rect_valid = 1; 781 } 782 783 static void 784 set_real_selection_rect(struct Selection *sel, struct Rect rect) { 785 sel->real_rect = rect; 786 scale_selection_rect( 787 (double)sel->sz.scaled_w / sel->sz.orig_w, rect, &sel->scaled_rect 788 ); 789 sel->rect_valid = 1; 790 } 791 792 static void 793 copy_selection(int scaled) { 794 if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid) 795 return; 796 if (scaled) { 797 state.copied_selection.rect = state.selections[state.cur_selection].scaled_rect; 798 state.copied_selection.scaled = 1; 799 } else { 800 state.copied_selection.rect = state.selections[state.cur_selection].real_rect; 801 state.copied_selection.scaled = 0; 802 } 803 state.copied_selection.valid = 1; 804 } 805 806 static void 807 paste_selection(void) { 808 if (!state.copied_selection.valid || state.cur_selection < 0 || 809 !state.selections[state.cur_selection].sz_valid) { 810 return; 811 } 812 struct Selection *sel = &state.selections[state.cur_selection]; 813 /* clear the old rectangle */ 814 struct Rect *rect = &sel->scaled_rect; 815 if (sel->rect_valid) 816 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 817 if (state.copied_selection.scaled) 818 set_scaled_selection_rect(sel, state.copied_selection.rect); 819 else 820 set_real_selection_rect(sel, state.copied_selection.rect); 821 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 822 } 823 824 static void 825 select_all(void) { 826 if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid) 827 return; 828 struct Selection *sel = &state.selections[state.cur_selection]; 829 struct Rect *rect = &sel->scaled_rect; 830 if (sel->rect_valid) 831 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 832 set_real_selection_rect(sel, (struct Rect){0, 0, sel->sz.orig_w, sel->sz.orig_h}); 833 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 834 } 835 836 /* change the shown image 837 * new_selection is the index of the new selection 838 * copy_box determines whether the cropping rectangle of the current 839 * selection should be copied (i.e. this is a true value when return 840 * is pressed) */ 841 void 842 change_picture(Imlib_Image new_image, int new_selection, int copy_box) { 843 int orig_w, orig_h, actual_w, actual_h; 844 /* set window title to filename */ 845 XSetStandardProperties( 846 state.ctx.dpy, state.ctx.win, 847 state.filenames[new_selection], 848 NULL, None, NULL, 0, NULL 849 ); 850 if (state.ctx.cur_image) { 851 imlib_context_set_image(state.ctx.cur_image); 852 imlib_free_image(); 853 } 854 state.ctx.cur_image = new_image; 855 imlib_context_set_image(state.ctx.cur_image); 856 int old_selection = state.cur_selection; 857 state.cur_selection = new_selection; 858 859 orig_w = imlib_image_get_width(); 860 orig_h = imlib_image_get_height(); 861 get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h); 862 863 struct Selection *sel = &state.selections[state.cur_selection]; 864 if (sel->sz_valid && (sel->sz.orig_w != orig_w || sel->sz.orig_h != orig_h)) { 865 fprintf( 866 stderr, 867 "WARNING: Image '%s' changed its size since it was previously opened.\n", 868 state.filenames[state.cur_selection] 869 ); 870 } 871 sel->sz = (ImageSize){orig_w, orig_h, actual_w, actual_h}; 872 sel->sz_valid = 1; 873 if (copy_box && old_selection >= 0 && old_selection < state.num_files) { 874 struct Selection *old = &state.selections[old_selection]; 875 if (old->rect_valid) { 876 set_scaled_selection_rect(sel, old->scaled_rect); 877 sel->rect_valid = 1; 878 } else { 879 /* FIXME: an alternative would be to just leave the 880 selection alone instead of clearing it if copy_box 881 is true and the previous selection was not valid */ 882 sel->rect_valid = 0; 883 } 884 } else if (sel->rect_valid) { 885 /* Always recalculate scaled selection in case the image scale 886 changed (i.e. the window was resized) */ 887 set_real_selection_rect(sel, sel->real_rect); 888 } 889 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h); 890 891 /* set the cursor since the cropping rectangle may have changed */ 892 set_cursor_for_selection(sel); 893 } 894 895 static void 896 clear_selection(void) { 897 if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid) 898 return; 899 struct Selection *sel = &state.selections[state.cur_selection]; 900 struct Rect *rect = &sel->scaled_rect; 901 if (sel->rect_valid) 902 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 903 sel->rect_valid = 0; 904 set_cursor_for_selection(sel); 905 } 906 907 static void 908 switch_color(void) { 909 if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid) 910 return; 911 state.cur_col = state.cur_col == 1 ? 2 : 1; 912 struct Selection *sel = &state.selections[state.cur_selection]; 913 struct Rect *rect = &sel->scaled_rect; 914 if (sel->rect_valid) 915 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 916 } 917 918 static int 919 key_press(XEvent event) { 920 XWindowAttributes attrs; 921 char buf[32]; 922 KeySym sym; 923 XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL); 924 switch (sym) { 925 case XK_Left: 926 last_picture(state.cur_selection, state.filenames, 0); 927 break; 928 case XK_Right: 929 next_picture(state.cur_selection, state.filenames, state.num_files, 0); 930 break; 931 case XK_Return: 932 if (event.xkey.state & ShiftMask) 933 last_picture(state.cur_selection, state.filenames, 1); 934 else 935 next_picture(state.cur_selection, state.filenames, state.num_files, 1); 936 break; 937 case XK_Delete: 938 clear_selection(); 939 break; 940 case XK_Tab: 941 switch_color(); 942 break; 943 case XK_space: 944 XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs); 945 resize_window(attrs.width, attrs.height); 946 /* queue update separately so it also redraws when 947 size didn't change */ 948 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h); 949 break; 950 case XK_q: 951 state.print_on_exit = 1; 952 return 0; 953 case XK_y: 954 copy_selection(1); 955 break; 956 case XK_p: 957 paste_selection(); 958 break; 959 case XK_Y: 960 copy_selection(0); 961 break; 962 case XK_a: 963 if (event.xkey.state & ControlMask) 964 select_all(); 965 break; 966 default: 967 break; 968 } 969 return 1; 970 }