croptool.c (23657B)
1 /* 2 * Copyright (c) 2021-2024 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 struct Selection { 77 struct Rect rect; 78 ImageSize sz; 79 char valid; 80 }; 81 82 static struct { 83 GraphicsContext ctx; 84 85 struct Selection *selections; 86 char **filenames; 87 int cur_selection; 88 int num_files; 89 int cursor_x; 90 int cursor_y; 91 struct Point move_handle; 92 XColor col1; 93 XColor col2; 94 int cur_col; 95 char moving; 96 char resizing; 97 char lock_x; 98 char lock_y; 99 char print_on_exit; 100 } state; 101 102 static struct { 103 Cursor top; 104 Cursor bottom; 105 Cursor left; 106 Cursor right; 107 Cursor topleft; 108 Cursor topright; 109 Cursor bottomleft; 110 Cursor bottomright; 111 Cursor grab; 112 } cursors; 113 114 static void usage(void); 115 static void mainloop(void); 116 static void setup(int argc, char *argv[]); 117 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1); 118 static void swap(int *a, int *b); 119 static void redraw(void); 120 static void print_cmd(const char *filename, int x, int y, int w, int h, int dry_run); 121 static void print_selection(struct Selection *sel, const char *filename); 122 static int collide_point(int x, int y, int x_point, int y_point); 123 static int collide_line(int x, int y, int x0, int y0, int x1, int y1); 124 static int collide_rect(int x, int y, struct Rect rect); 125 static void switch_color(void); 126 static void clear_selection(void); 127 static void set_selection( 128 struct Selection *sel, int rect_x0, int rect_y0, int rect_x1, 129 int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h 130 ); 131 static void queue_update(int x, int y, int w, int h); 132 static void queue_rectangle_redraw(int x0, int y0, int x1, int y1); 133 static void set_cursor(struct Rect rect); 134 static void drag_motion(XEvent event); 135 static void resize_window(int w, int h); 136 static void button_release(void); 137 static void button_press(XEvent event); 138 static int key_press(XEvent event); 139 140 static void 141 usage(void) { 142 fprintf(stderr, "USAGE: croptool [-mr] [-f format] " 143 "[-w width] [-c padding] [-p color] [-s color] " 144 "[-z size] file ...\n"); 145 } 146 147 int 148 main(int argc, char *argv[]) { 149 char c; 150 151 while ((c = getopt(argc, argv, "f:w:c:mrp:s:z:")) != -1) { 152 switch (c) { 153 case 'f': 154 CMD_FORMAT = optarg; 155 break; 156 case 'm': 157 RESIZE_REDRAW = 0; 158 break; 159 case 'r': 160 SELECTION_REDRAW = 0; 161 break; 162 case 'p': 163 SELECTION_COLOR1 = optarg; 164 break; 165 case 's': 166 SELECTION_COLOR2 = optarg; 167 break; 168 case 'c': 169 if (parse_int(optarg, 1, 99, &COLLISION_PADDING)) { 170 fprintf(stderr, "Invalid collision padding.\n"); 171 exit(1); 172 } 173 break; 174 case 'w': 175 if (parse_int(optarg, 1, 99, &LINE_WIDTH)) { 176 fprintf(stderr, "Invalid line width.\n"); 177 exit(1); 178 } 179 break; 180 case 'z': 181 if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) { 182 fprintf(stderr, "Invalid cache size.\n"); 183 exit(1); 184 } 185 break; 186 default: 187 usage(); 188 exit(1); 189 break; 190 } 191 } 192 /* print warning if command format is invalid */ 193 print_cmd("", 0, 0, 0, 0, 1); 194 195 argc -= optind; 196 argv += optind; 197 if (argc < 1) { 198 usage(); 199 exit(1); 200 } 201 setup(argc, argv); 202 203 mainloop(); 204 205 if (state.print_on_exit) { 206 for (int i = 0; i < argc; i++) { 207 if (state.selections[i].valid) { 208 print_selection(&state.selections[i], state.filenames[i]); 209 } 210 } 211 } 212 213 cleanup(); 214 215 return 0; 216 } 217 218 static void 219 mainloop(void) { 220 XEvent event; 221 int running = 1; 222 223 while (running) { 224 do { 225 XNextEvent(state.ctx.dpy, &event); 226 switch (event.type) { 227 case Expose: 228 if (RESIZE_REDRAW) 229 queue_update(event.xexpose.x, event.xexpose.y, 230 event.xexpose.width, event.xexpose.height); 231 break; 232 case ConfigureNotify: 233 if (RESIZE_REDRAW) 234 resize_window( 235 event.xconfigure.width, 236 event.xconfigure.height 237 ); 238 break; 239 case ButtonPress: 240 if (event.xbutton.button == Button1) 241 button_press(event); 242 break; 243 case ButtonRelease: 244 if (event.xbutton.button == Button1) 245 button_release(); 246 break; 247 case MotionNotify: 248 drag_motion(event); 249 break; 250 case KeyPress: 251 running = key_press(event); 252 break; 253 case ClientMessage: 254 if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg) 255 running = 0; 256 default: 257 break; 258 } 259 } while (XPending(state.ctx.dpy)); 260 261 redraw(); 262 } 263 } 264 265 static void 266 setup(int argc, char *argv[]) { 267 state.selections = malloc(argc * sizeof(struct Selection)); 268 if (!state.selections) { 269 fprintf(stderr, "Unable to allocate memory.\n"); 270 exit(1); 271 } 272 state.num_files = argc; 273 state.filenames = argv; 274 state.cur_selection = -1; 275 state.moving = 0; 276 state.resizing = 0; 277 state.lock_x = 0; 278 state.lock_y = 0; 279 state.cursor_x = 0; 280 state.cursor_y = 0; 281 state.cur_col = 1; 282 state.print_on_exit = 0; 283 284 for (int i = 0; i < argc; i++) { 285 state.selections[i].valid = 0; 286 } 287 288 setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE); 289 290 if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR1, &state.col1)) { 291 fprintf(stderr, "Primary color invalid.\n"); 292 exit(1); 293 } 294 XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1); 295 if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR2, &state.col2)) { 296 fprintf(stderr, "Secondary color invalid.\n"); 297 exit(1); 298 } 299 XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2); 300 301 cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side); 302 cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side); 303 cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side); 304 cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side); 305 cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner); 306 cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner); 307 cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner); 308 cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner); 309 cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur); 310 311 next_picture(state.cur_selection, state.filenames, state.num_files, 0); 312 /* Only map window here so the program exits immediately if 313 there are no loadable images, without first opening the 314 window and closing it again immediately */ 315 XMapWindow(state.ctx.dpy, state.ctx.win); 316 redraw(); 317 } 318 319 void 320 cleanup(void) { 321 free(state.selections); 322 cleanup_x(&state.ctx); 323 } 324 325 /* TODO: Escape filename properly 326 * -> But how? Since the format can be set by the user, 327 * it isn't really clear *what* needs to be escaped. */ 328 static void 329 print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) { 330 short percent = 0; 331 const char *c; 332 int length = 0; 333 int start_index = 0; 334 /* FIXME: just use putc instead of this complex printf dance */ 335 for (c = CMD_FORMAT; *c != '\0'; c++) { 336 if (percent) 337 start_index++; 338 if (*c == '%') { 339 if (length) { 340 if (!dry_run) 341 printf("%.*s", length, CMD_FORMAT + start_index); 342 start_index += length; 343 length = 0; 344 } 345 if (percent && !dry_run) 346 printf("%%"); 347 percent++; 348 percent %= 2; 349 start_index++; 350 } else if (percent && *c == 'w') { 351 if (!dry_run) 352 printf("%d", w); 353 percent = 0; 354 } else if (percent && *c == 'h') { 355 if (!dry_run) 356 printf("%d", h); 357 percent = 0; 358 } else if (percent && *c == 'l') { 359 if (!dry_run) 360 printf("%d", x); 361 percent = 0; 362 } else if (percent && *c == 't') { 363 if (!dry_run) 364 printf("%d", y); 365 percent = 0; 366 } else if (percent && *c == 'r') { 367 if (!dry_run) 368 printf("%d", x + w); 369 percent = 0; 370 } else if (percent && *c == 'b') { 371 if (!dry_run) 372 printf("%d", y + h); 373 percent = 0; 374 } else if (percent && *c == 'f') { 375 if (!dry_run) 376 printf("%s", filename); 377 percent = 0; 378 } else if (percent) { 379 if (dry_run) { 380 fprintf(stderr, 381 "Warning: Unknown substitution '%c' " 382 "in format string.\n", *c 383 ); 384 } else { 385 printf("%%%c", *c); 386 } 387 percent = 0; 388 } else { 389 length++; 390 } 391 } 392 if (!dry_run) { 393 if (length) 394 printf("%.*s", length, CMD_FORMAT + start_index); 395 printf("\n"); 396 } 397 } 398 399 static void 400 redraw(void) { 401 if (!state.ctx.dirty) 402 return; 403 if (!state.ctx.cur_image || state.cur_selection < 0) { 404 clear_screen(&state.ctx); 405 swap_buffers(&state.ctx); 406 return; 407 } 408 409 /* draw the parts of the image that need to be redrawn */ 410 struct Selection *sel = &state.selections[state.cur_selection]; 411 draw_image_updates(&state.ctx, &sel->sz); 412 413 wipe_around_image(&state.ctx, &sel->sz); 414 415 /* draw the rectangle */ 416 struct Rect rect = sel->rect; 417 if (rect.x0 != -200) { 418 XColor col = state.cur_col == 1 ? state.col1 : state.col2; 419 XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel); 420 sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); 421 XDrawRectangle( 422 state.ctx.dpy, state.ctx.drawable, state.ctx.gc, 423 rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0 424 ); 425 } 426 swap_buffers(&state.ctx); 427 } 428 429 static void 430 swap(int *a, int *b) { 431 int tmp = *a; 432 *a = *b; 433 *b = tmp; 434 } 435 436 /* sort rectangle coordinates into their canonical 437 * form so *x1 - *x0 >= 0 and *y1 - *y0 >= 0 */ 438 static void 439 sort_coordinates(int *x0, int *y0, int *x1, int *y1) { 440 if (*x0 > *x1) 441 swap(x0, x1); 442 if(*y0 > *y1) 443 swap(y0, y1); 444 } 445 446 static void 447 print_selection(struct Selection *sel, const char *filename) { 448 /* The box was never actually used */ 449 if (sel->rect.x0 == -200) 450 return; 451 double scale = (double)sel->sz.orig_w / sel->sz.scaled_w; 452 int x0 = sel->rect.x0, y0 = sel->rect.y0; 453 int x1 = sel->rect.x1, y1 = sel->rect.y1; 454 sort_coordinates(&x0, &y0, &x1, &y1); 455 x0 = round(x0 * scale); 456 y0 = round(y0 * scale); 457 x1 = round(x1 * scale); 458 y1 = round(y1 * scale); 459 /* The box is completely outside of the picture. */ 460 if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h) 461 return; 462 /* Cut the bounding box if it goes past the end of the picture. */ 463 x0 = x0 < 0 ? 0 : x0; 464 y0 = y0 < 0 ? 0 : y0; 465 x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1; 466 y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1; 467 print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0); 468 } 469 470 static int 471 collide_point(int x, int y, int x_point, int y_point) { 472 return (abs(x - x_point) <= COLLISION_PADDING) && 473 (abs(y - y_point) <= COLLISION_PADDING); 474 } 475 476 static int 477 collide_line(int x, int y, int x0, int y0, int x1, int y1) { 478 sort_coordinates(&x0, &y0, &x1, &y1); 479 /* this expects a valid line */ 480 if (x0 == x1) { 481 return (abs(x - x0) <= COLLISION_PADDING) && 482 (y0 <= y) && (y <= y1); 483 } else { 484 return (abs(y - y0) <= COLLISION_PADDING) && 485 (x0 <= x) && (x <= x1); 486 } 487 } 488 489 static int 490 collide_rect(int x, int y, struct Rect rect) { 491 int x0 = rect.x0, x1 = rect.x1; 492 int y0 = rect.y0, y1 = rect.y1; 493 sort_coordinates(&x0, &y0, &x1, &y1); 494 return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1); 495 } 496 497 static void 498 button_press(XEvent event) { 499 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 500 return; 501 struct Rect *rect = &state.selections[state.cur_selection].rect; 502 int x = event.xbutton.x; 503 int y = event.xbutton.y; 504 int x0 = rect->x0, x1 = rect->x1; 505 int y0 = rect->y0, y1 = rect->y1; 506 /* erase old rectangle */ 507 queue_rectangle_redraw(x0, y0, x1, y1); 508 if (collide_point(x, y, x0, y0)) { 509 rect->x0 = x1; 510 rect->y0 = y1; 511 rect->x1 = x; 512 rect->y1 = y; 513 } else if (collide_point(x, y, x1, y1)) { 514 rect->x1 = x; 515 rect->y1 = y; 516 } else if (collide_point(x, y, x0, y1)) { 517 rect->x0 = rect->x1; 518 rect->x1 = x; 519 rect->y1 = y; 520 } else if (collide_point(x, y, x1, y0)) { 521 rect->y0 = y1; 522 rect->x1 = x; 523 rect->y1 = y; 524 } else if (collide_line(x, y, x0, y0, x1, y0)) { 525 state.lock_y = 1; 526 swap(&rect->x0, &rect->x1); 527 rect->y0 = rect->y1; 528 rect->y1 = y; 529 } else if (collide_line(x, y, x0, y0, x0, y1)) { 530 state.lock_x = 1; 531 swap(&rect->y0, &rect->y1); 532 rect->x0 = rect->x1; 533 rect->x1 = x; 534 } else if (collide_line(x, y, x1, y1, x0, y1)) { 535 state.lock_y = 1; 536 rect->y1 = y; 537 } else if (collide_line(x, y, x1, y1, x1, y0)) { 538 state.lock_x = 1; 539 rect->x1 = x; 540 } else if (collide_rect(x, y, *rect)) { 541 state.moving = 1; 542 state.move_handle.x = x; 543 state.move_handle.y = y; 544 } else { 545 rect->x0 = x; 546 rect->y0 = y; 547 rect->x1 = x; 548 rect->y1 = y; 549 } 550 state.resizing = 1; 551 } 552 553 static void 554 queue_update(int x, int y, int w, int h) { 555 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 556 return; 557 struct Selection *sel = &state.selections[state.cur_selection]; 558 queue_area_update(&state.ctx, &sel->sz, x, y, w, h); 559 } 560 561 static void 562 button_release(void) { 563 state.moving = 0; 564 state.resizing = 0; 565 state.lock_x = 0; 566 state.lock_y = 0; 567 /* redraw everything if automatic redrawing of the rectangle 568 is disabled (so it's redrawn when the mouse is released) */ 569 if (!SELECTION_REDRAW) 570 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h); 571 } 572 573 static void 574 resize_window(int w, int h) { 575 int actual_w, actual_h; 576 struct Selection *sel; 577 state.ctx.window_w = w; 578 state.ctx.window_h = h; 579 580 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 581 return; 582 sel = &state.selections[state.cur_selection]; 583 get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h); 584 if (actual_w != sel->sz.scaled_w) { 585 if (sel->rect.x0 != -200) { 586 /* If there is a selection, we need to convert it to 587 * the new scale. This only takes width into account 588 * because the aspect ratio should have been preserved 589 * anyways */ 590 double scale = (double)actual_w / sel->sz.scaled_w; 591 sel->rect.x0 = round(sel->rect.x0 * scale); 592 sel->rect.y0 = round(sel->rect.y0 * scale); 593 sel->rect.x1 = round(sel->rect.x1 * scale); 594 sel->rect.y1 = round(sel->rect.y1 * scale); 595 } 596 sel->sz.scaled_w = actual_w; 597 sel->sz.scaled_h = actual_h; 598 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h); 599 } 600 } 601 602 /* queue the redrawing of a rectangular area on the image - 603 * this queues four updates, one for each side of the rectangle, 604 * with the width or height (depending on which side) of the 605 * rectangle being determined by the configured line width */ 606 static void 607 queue_rectangle_redraw(int x0, int y0, int x1, int y1) { 608 sort_coordinates(&x0, &y0, &x1, &y1); 609 queue_update( 610 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 611 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 612 x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); 613 queue_update( 614 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 615 y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0, 616 x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); 617 queue_update( 618 x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, 619 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 620 LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); 621 queue_update( 622 x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0, 623 y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, 624 LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); 625 } 626 627 /* set the appropriate cursor based on the 628 * current mouse position and a cropping rectangle */ 629 static void 630 set_cursor(struct Rect rect) { 631 Cursor c = None; 632 sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); 633 if (collide_point( 634 state.cursor_x, state.cursor_y, 635 rect.x0, rect.y0)) { 636 c = cursors.topleft; 637 } else if (collide_point( 638 state.cursor_x, state.cursor_y, 639 rect.x1, rect.y0)) { 640 c = cursors.topright; 641 } else if (collide_point( 642 state.cursor_x, state.cursor_y, 643 rect.x0, rect.y1)) { 644 c = cursors.bottomleft; 645 } else if (collide_point( 646 state.cursor_x, state.cursor_y, 647 rect.x1, rect.y1)) { 648 c = cursors.bottomright; 649 } else if (collide_line( 650 state.cursor_x, state.cursor_y, 651 rect.x0, rect.y0, rect.x1, rect.y0)) { 652 c = cursors.top; 653 } else if (collide_line( 654 state.cursor_x, state.cursor_y, 655 rect.x1, rect.y1, rect.x0, rect.y1)) { 656 c = cursors.bottom; 657 } else if (collide_line( 658 state.cursor_x, state.cursor_y, 659 rect.x1, rect.y1, rect.x1, rect.y0)) { 660 c = cursors.right; 661 } else if (collide_line( 662 state.cursor_x, state.cursor_y, 663 rect.x0, rect.y0, rect.x0, rect.y1)) { 664 c = cursors.left; 665 } else if (collide_rect(state.cursor_x, state.cursor_y, rect)) { 666 c = cursors.grab; 667 } 668 XDefineCursor(state.ctx.dpy, state.ctx.win, c); 669 } 670 671 static void 672 drag_motion(XEvent event) { 673 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 674 return; 675 struct Selection *sel = &state.selections[state.cur_selection]; 676 struct Rect *rect = &sel->rect; 677 678 /* don't allow coordinates to go below 0 */ 679 if (event.xbutton.x >= 0) 680 state.cursor_x = event.xbutton.x; 681 else 682 state.cursor_x = 0; 683 if (event.xbutton.y >= 0) 684 state.cursor_y = event.xbutton.y; 685 else 686 state.cursor_y = 0; 687 688 int x0 = rect->x0, x1 = rect->x1; 689 int y0 = rect->y0, y1 = rect->y1; 690 sort_coordinates(&x0, &y0, &x1, &y1); 691 /* redraw the old rectangle */ 692 if (SELECTION_REDRAW && (state.moving || state.resizing)) 693 queue_rectangle_redraw(x0, y0, x1, y1); 694 if (state.moving) { 695 int x_delta = state.cursor_x - state.move_handle.x; 696 int y_delta = state.cursor_y - state.move_handle.y; 697 /* don't allow coordinates to go below 0 */ 698 int x_realdelta = x0 + x_delta >= 0 ? x_delta : -x0; 699 int y_realdelta = y0 + y_delta >= 0 ? y_delta : -y0; 700 rect->x0 += x_realdelta; 701 rect->x1 += x_realdelta; 702 rect->y0 += y_realdelta; 703 rect->y1 += y_realdelta; 704 state.move_handle.x = state.cursor_x; 705 state.move_handle.y = state.cursor_y; 706 } else if (state.resizing) { 707 if (!state.lock_y) 708 rect->x1 = state.cursor_x; 709 if (!state.lock_x) 710 rect->y1 = state.cursor_y; 711 } else { 712 set_cursor(*rect); 713 return; 714 } 715 set_cursor(*rect); 716 717 /* redraw the new rectangle */ 718 if (SELECTION_REDRAW) 719 queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1); 720 } 721 722 static void 723 set_selection( 724 struct Selection *sel, int rect_x0, int rect_y0, int rect_x1, 725 int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h) { 726 727 sel->rect.x0 = rect_x0; 728 sel->rect.y0 = rect_y0; 729 sel->rect.x1 = rect_x1; 730 sel->rect.y1 = rect_y1; 731 sel->sz.orig_w = orig_w; 732 sel->sz.orig_h = orig_h; 733 sel->sz.scaled_w = scaled_w; 734 sel->sz.scaled_h = scaled_h; 735 } 736 737 /* change the shown image 738 * new_selection is the index of the new selection 739 * copy_box determines whether the cropping rectangle of the current 740 * selection should be copied (i.e. this is a true value when return 741 * is pressed) */ 742 void 743 change_picture(Imlib_Image new_image, int new_selection, int copy_box) { 744 int orig_w, orig_h, actual_w, actual_h; 745 /* set window title to filename */ 746 XSetStandardProperties( 747 state.ctx.dpy, state.ctx.win, 748 state.filenames[new_selection], 749 NULL, None, NULL, 0, NULL 750 ); 751 if (state.ctx.cur_image) { 752 imlib_context_set_image(state.ctx.cur_image); 753 imlib_free_image(); 754 } 755 state.ctx.cur_image = new_image; 756 imlib_context_set_image(state.ctx.cur_image); 757 int old_selection = state.cur_selection; 758 state.cur_selection = new_selection; 759 760 orig_w = imlib_image_get_width(); 761 orig_h = imlib_image_get_height(); 762 get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h); 763 764 struct Selection *sel = &state.selections[state.cur_selection]; 765 if (copy_box && old_selection >= 0 && old_selection < state.num_files) { 766 struct Selection *old = &state.selections[old_selection]; 767 set_selection( 768 sel, 769 old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1, 770 orig_w, orig_h, actual_w, actual_h 771 ); 772 } else if (!sel->valid) { 773 /* Just fill it with -200 so we can check 774 * later if it has been used yet */ 775 set_selection( 776 sel, 777 -200, -200, -200, -200, 778 orig_w, orig_h, actual_w, actual_h 779 ); 780 } else if (sel->rect.x0 != -200 && actual_w != sel->sz.scaled_w) { 781 /* If there is a selection, we need to convert it to the 782 * new scale. This only takes width into account because 783 * the aspect ratio should have been preserved anyways */ 784 double scale = (double)actual_w / sel->sz.scaled_w; 785 sel->rect.x0 = round(sel->rect.x0 * scale); 786 sel->rect.y0 = round(sel->rect.y0 * scale); 787 sel->rect.x1 = round(sel->rect.x1 * scale); 788 sel->rect.y1 = round(sel->rect.y1 * scale); 789 } 790 sel->sz.scaled_w = actual_w; 791 sel->sz.scaled_h = actual_h; 792 sel->valid = 1; 793 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h); 794 795 /* set the cursor since the cropping rectangle may have changed */ 796 set_cursor(sel->rect); 797 } 798 799 static void 800 clear_selection(void) { 801 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 802 return; 803 struct Selection *sel = &state.selections[state.cur_selection]; 804 sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200; 805 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h); 806 } 807 808 static void 809 switch_color(void) { 810 if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) 811 return; 812 state.cur_col = state.cur_col == 1 ? 2 : 1; 813 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h); 814 } 815 816 static int 817 key_press(XEvent event) { 818 XWindowAttributes attrs; 819 char buf[32]; 820 KeySym sym; 821 XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL); 822 switch (sym) { 823 case XK_Left: 824 last_picture(state.cur_selection, state.filenames, 0); 825 break; 826 case XK_Right: 827 next_picture(state.cur_selection, state.filenames, state.num_files, 0); 828 break; 829 case XK_Return: 830 if (event.xkey.state & ShiftMask) 831 last_picture(state.cur_selection, state.filenames, 1); 832 else 833 next_picture(state.cur_selection, state.filenames, state.num_files, 1); 834 break; 835 case XK_Delete: 836 clear_selection(); 837 break; 838 case XK_Tab: 839 switch_color(); 840 break; 841 case XK_space: 842 XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs); 843 resize_window(attrs.width, attrs.height); 844 /* queue update separately so it also redraws when 845 size didn't change */ 846 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h); 847 break; 848 case XK_q: 849 state.print_on_exit = 1; 850 return 0; 851 default: 852 break; 853 } 854 return 1; 855 }