From d045ffcf7d37f60df09cac32698ee4c4b10e1a42 Mon Sep 17 00:00:00 2001 From: David Maciejak Date: Tue, 7 Mar 2023 19:46:50 +0800 Subject: [PATCH] Add a screenshot capture feature This patch adds a feature to take screenshots directly from Window Maker. Having the feature embeded direclty inside Window Maker allows us to take advantage of how Window Maker is managing and handling Windows. Three new actions can be bound to a key shortcut from WPrefs. The screenshot files are saved in ~/GNUstep/Library/WindowMaker/Screenshots/ dir, with a "screenshot_%Y-%m-%d_at_%H:%M:%S" format followed by the extension. Preferably as a PNG or JPG file if available. Meaning, to work Window Maker via WRaster needs to support at least one of those format. "Capture the entire screen" is quite standard, it takes a screenshot of the whole screen area (even in multiheads env). "Capture a portion of the screen" requires the user to draw a rectangle which will be captured. Those two first are quite straightforward, just taking a live picture of the screen. The last one is "Capture a window" which works in best effort mode, it catures the focused window. As Window Maker by default is not using any compositor (like for example Xcompmgr) it can only dump the content displayed on the screen. If a window is minimized or out of the screen, there is high chance the image will be split or some area greyed in case other windows overlapped it. --- WPrefs.app/KeyboardShortcuts.c | 5 +- WindowMaker/Defaults/WindowMaker.in | 3 + src/WindowMaker.h | 1 + src/defaults.c | 8 ++ src/event.c | 18 +++ src/keybind.h | 9 ++ src/screen.c | 250 ++++++++++++++++++++++++++++++++++++ src/screen.h | 10 +- src/startup.c | 1 + 9 files changed, 303 insertions(+), 2 deletions(-) diff --git a/WPrefs.app/KeyboardShortcuts.c b/WPrefs.app/KeyboardShortcuts.c index a6e223c8..cc89d71e 100644 --- a/WPrefs.app/KeyboardShortcuts.c +++ b/WPrefs.app/KeyboardShortcuts.c @@ -158,7 +158,10 @@ static struct keyOption { { "RunKey", N_("Run application") }, { "ExitKey", N_("Exit Window Maker") }, { "DockRaiseLowerKey", N_("Raise/Lower Dock") }, - { "ClipRaiseLowerKey", N_("Raise/Lower Clip") } + { "ClipRaiseLowerKey", N_("Raise/Lower Clip") }, + { "ScreenCaptureKey", N_("Capture the entire screen") }, + { "WindowCaptureKey", N_("Capture a window") }, + { "PartialCaptureKey", N_("Capture a portion of the screen") } #ifdef XKB_MODELOCK ,{ "ToggleKbdModeKey", N_("Toggle keyboard language") } #endif /* XKB_MODELOCK */ diff --git a/WindowMaker/Defaults/WindowMaker.in b/WindowMaker/Defaults/WindowMaker.in index 71575507..e57e9b9f 100644 --- a/WindowMaker/Defaults/WindowMaker.in +++ b/WindowMaker/Defaults/WindowMaker.in @@ -233,6 +233,9 @@ ScreenSwitchKey = None; RunKey = None; ExitKey = None; + ScreenCaptureKey = Print; + WindowCaptureKey = None; + PartialCaptureKey = None; NormalCursor = (builtin, left_ptr); ArrowCursor = (builtin, top_left_arrow); MoveCursor = (builtin, fleur); diff --git a/src/WindowMaker.h b/src/WindowMaker.h index e1b49295..823f3990 100644 --- a/src/WindowMaker.h +++ b/src/WindowMaker.h @@ -119,6 +119,7 @@ typedef enum { WCUR_QUESTION, WCUR_TEXT, WCUR_SELECT, + WCUR_CAPTURE, WCUR_ROOT, WCUR_EMPTY, diff --git a/src/defaults.c b/src/defaults.c index f9dc3010..f6120f2c 100644 --- a/src/defaults.c +++ b/src/defaults.c @@ -790,6 +790,12 @@ WDefaultEntry optionList[] = { NULL, getKeybind, setKeyGrab, NULL, NULL}, {"ExitKey", "None", (void *)WKBD_EXIT, NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"ScreenCaptureKey", "None", (void *)WKBD_PRINTS, + NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"WindowCaptureKey", "None", (void *)WKBD_PRINTW, + NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"PartialCaptureKey", "None", (void *)WKBD_PRINTP, + NULL, getKeybind, setKeyGrab, NULL, NULL}, #ifdef KEEP_XKB_LOCK_STATUS {"ToggleKbdModeKey", "None", (void *)WKBD_TOGGLE, @@ -826,6 +832,8 @@ WDefaultEntry optionList[] = { NULL, getCursor, setCursor, NULL, NULL}, {"SelectCursor", "(builtin, cross)", (void *)WCUR_SELECT, NULL, getCursor, setCursor, NULL, NULL}, + {"CaptureCursor", "(builtin, crosshair)", (void *)WCUR_CAPTURE, + NULL, getCursor, setCursor, NULL, NULL}, {"DialogHistoryLines", "500", NULL, &wPreferences.history_lines, getInt, NULL, NULL, NULL}, {"CycleActiveHeadOnly", "NO", NULL, diff --git a/src/event.c b/src/event.c index f0958274..c33f239f 100644 --- a/src/event.c +++ b/src/event.c @@ -1863,6 +1863,24 @@ static void handleKeyPress(XEvent * event) break; } + case WKBD_PRINTS: + { + ScreenCapture(scr, PRINT_SCREEN); + break; + } + + case WKBD_PRINTW: + { + ScreenCapture(scr, PRINT_WINDOW); + break; + } + + case WKBD_PRINTP: + { + ScreenCapture(scr, PRINT_PARTIAL); + break; + } + case WKBD_NEXTWSLAYER: case WKBD_PREVWSLAYER: { diff --git a/src/keybind.h b/src/keybind.h index c2b5dbb8..fb32f42b 100644 --- a/src/keybind.h +++ b/src/keybind.h @@ -148,6 +148,15 @@ enum { /* open "exit" dialog */ WKBD_EXIT, + /* screen print */ + WKBD_PRINTS, + + /* window print */ + WKBD_PRINTW, + + /* partial print */ + WKBD_PRINTP, + #ifdef KEEP_XKB_LOCK_STATUS WKBD_TOGGLE, #endif diff --git a/src/screen.c b/src/screen.c index 6a7358fa..271614a4 100644 --- a/src/screen.c +++ b/src/screen.c @@ -24,6 +24,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -54,10 +58,12 @@ #include "geomview.h" #include "wmspec.h" #include "rootmenu.h" +#include "misc.h" #include "xinerama.h" #include +#include #include "defaults.h" @@ -593,6 +599,7 @@ static void createInternalWindows(WScreen * scr) scr->workspace_name = XCreateWindow(dpy, scr->root_win, 0, 0, 10, 10, 0, scr->w_depth, CopyFromParent, scr->w_visual, vmask, &attribs); + scr->mini_screenshot_timeout = 0; } /* @@ -1126,3 +1133,246 @@ int wScreenKeepInside(WScreen * scr, int *x, int *y, int width, int height) return moved; } + +static XImage *imageCaptureArea(WScreen *scr) +{ + XEvent event; + int quit = 0; + int xp = -1; + int yp = -1; + int w = 0, h = 0; + int x = xp, y = yp; + + if (XGrabPointer(dpy, scr->root_win, False, ButtonMotionMask + | ButtonReleaseMask | ButtonPressMask, GrabModeAsync, + GrabModeAsync, None, wPreferences.cursor[WCUR_CAPTURE], CurrentTime) != Success) { + return NULL; + } + + XGrabServer(dpy); + + while (!quit) { + WMMaskEvent(dpy, ButtonReleaseMask | PointerMotionMask | ButtonPressMask | KeyPressMask, &event); + + switch (event.type) { + case ButtonPress: + if (event.xbutton.button == Button1) { + xp = event.xbutton.x_root; + yp = event.xbutton.y_root; + } + break; + case ButtonRelease: + if (event.xbutton.button == Button1) { + quit = 1; + if (w > 0 && h > 0) { + XDrawRectangle(dpy, scr->root_win, scr->frame_gc, x, y, w, h); + XUngrabServer(dpy); + XUngrabPointer(dpy, CurrentTime); + return XGetImage(dpy, scr->root_win, x, y, w, h, AllPlanes, ZPixmap); + } + } + break; + case MotionNotify: + XDrawRectangle(dpy, scr->root_win, scr->frame_gc, x, y, w, h); + x = event.xmotion.x_root; + if (x < xp) { + w = xp - x; + } else { + w = x - xp; + x = xp; + } + y = event.xmotion.y_root; + if (y < yp) { + h = yp - y; + } else { + h = y - yp; + y = yp; + } + XDrawRectangle(dpy, scr->root_win, scr->frame_gc, x, y, w, h); + break; + case KeyPress: + if (W_KeycodeToKeysym(dpy, event.xkey.keycode, 0) == XK_Escape) + quit = 1; + break; + default: + WMHandleEvent(&event); + break; + } + } + + XUngrabServer(dpy); + XUngrabPointer(dpy, CurrentTime); + return NULL; +} + +static void hideMiniScreenshot(void *data) +{ + WScreen *scr = (WScreen *) data; + + if (time(NULL) < scr->mini_screenshot_timeout) { + scr->mini_screenshot_timer = WMAddTimerHandler(WORKSPACE_NAME_FADE_DELAY, hideMiniScreenshot, scr); + } else { + XWindowAttributes attr; + + WMDeleteTimerHandler(scr->mini_screenshot_timer); + if (XGetWindowAttributes(dpy, scr->mini_screenshot, &attr)) + slide_window(scr->mini_screenshot, attr.x, attr.y, attr.x + attr.width, attr.y); + XUnmapWindow(dpy, scr->mini_screenshot); + XDestroyWindow(dpy, scr->mini_screenshot); + scr->mini_screenshot_timeout = 0; + } +} + +static void showMiniScreenshot(WScreen *scr, RImage *img) +{ + Pixmap pix; + int x = scr->scr_width - img->width - 20; + int y = scr->scr_height - img->height - 20; + + if (!scr->mini_screenshot_timeout) { + Window win; + + win = XCreateSimpleWindow(dpy, scr->root_win, x, y, + img->width, img->height, scr->frame_border_width, 0, 0); + scr->mini_screenshot = win; + } + RConvertImage(scr->rcontext, img, &pix); + XMapWindow(dpy, scr->mini_screenshot); + XCopyArea(dpy, pix, scr->mini_screenshot, scr->rcontext->copy_gc, 0, 0, img->width, img->height, 0, 0); + XFlush(dpy); + + scr->mini_screenshot_timeout = time(NULL) + 2; + scr->mini_screenshot_timer = WMAddTimerHandler(WORKSPACE_NAME_FADE_DELAY, hideMiniScreenshot, scr); +} + +void ScreenCapture(WScreen *scr, int mode) +{ + time_t s; + short i = 0; + struct tm *tm_info; + char index_str[12] = ""; + char filename_date_part[60]; + char filename[60]; + char *filepath; + char *screenshot_dir; + RImage *img = NULL; + RImage *scale_img = NULL; + +#ifdef USE_PNG + char *filetype = ".png"; +#else +#ifdef USE_JPEG + char *filetype = ".jpg"; +#else + char *filetype = NULL; +#endif +#endif + + if (!filetype) { + werror(_("Unable to find a proper screenshot image format")); + return; + } + + screenshot_dir = wstrconcat(wusergnusteppath(), "/Library/WindowMaker/Screenshots/"); + if (-1 == mkdir(screenshot_dir, 0700) && errno != EEXIST) { + wfree(screenshot_dir); + werror(_("Unable to create screenshot directory: %s"), strerror(errno)); + return; + } + + s = time(NULL); + tm_info = localtime(&s); + strftime(filename_date_part, sizeof(filename_date_part), "screenshot_%Y-%m-%d_at_%H:%M:%S", tm_info); + strcpy(filename, filename_date_part); + + filepath = wstrconcat(screenshot_dir, strcat(filename, filetype)); + while (access(filepath, F_OK) == 0 && i < 600) { + i++; + strcpy(filename, filename_date_part); + sprintf(index_str, "_%d", i); + strncat(filename, index_str, sizeof(filename) - strlen(filename) - 1); + wfree(filepath); + filepath = wstrconcat(screenshot_dir, strcat(filename, filetype)); + } + + /* cannot generate an available filename ?! */ + if (i == 600) { + wfree(filepath); + wfree(screenshot_dir); + werror(_("Could not generate a free screenshot filename")); + return; + } + + switch (mode) { + case PRINT_WINDOW: + WWindow *wwin = scr->focused_window; + if (wwin && !wwin->flags.shaded) { + /* + * check if hint WM_TAKE_FOCUS is set, if it's the case + * we can take screenshot of the out of screen window + */ + if (wwin->focus_mode >= WFM_LOCALLY_ACTIVE) { + img = RCreateImageFromDrawable(scr->rcontext, wwin->client_win, None); + } + else { + /* we will only capture the visible window part */ + XImage *pimg; + int x_crop = 0; + int y_crop = 0; + int w_crop = wwin->client.width; + int h_crop = wwin->client.height; + + if (wwin->client.x > 0) + x_crop = wwin->client.x; + if (wwin->client.y > 0) + y_crop = wwin->client.y; + + if (wwin->client.x + wwin->client.width > scr->scr_width) + w_crop = scr->scr_width - wwin->client.x; + if (wwin->client.y + wwin->client.height > scr->scr_height) + h_crop = scr->scr_height - wwin->client.y; + + pimg = XGetImage(dpy, scr->root_win, x_crop, y_crop, + (wwin->client.x > 0)?w_crop:w_crop + wwin->client.x, + (wwin->client.y > 0)?h_crop:h_crop + wwin->client.y, + AllPlanes, ZPixmap); + + if (pimg) { + img = RCreateImageFromXImage(scr->rcontext, pimg, None); + XDestroyImage(pimg); + } + } + } + break; + case PRINT_PARTIAL: + XImage *pimg; + + pimg = imageCaptureArea(scr); + if (pimg) { + img = RCreateImageFromXImage(scr->rcontext, pimg, None); + XDestroyImage(pimg); + } + break; + default: + /* PRINT_SCREEN*/ + img = RCreateImageFromDrawable(scr->rcontext, scr->root_win, None); + } + + if (img) { +#ifdef USE_PNG + if (RSaveTitledImage(img, filepath, (char *)(filetype + 1), "Screenshot from Window Maker")) { +#else + if (RSaveTitledImage(img, filepath, (char *)(filetype + 1), "Screenshot from Window Maker")) { +#endif + scale_img = RSmoothScaleImage(img, scr->scr_width / 10, scr->scr_height / 10); + showMiniScreenshot(scr, scale_img); + RReleaseImage(scale_img); +#ifdef DEBUG + wmessage("screenshot filepath: %s", filepath); +#endif + } + RReleaseImage(img); + } + wfree(filepath); + wfree(screenshot_dir); +} diff --git a/src/screen.h b/src/screen.h index df861834..818861fa 100644 --- a/src/screen.h +++ b/src/screen.h @@ -27,6 +27,9 @@ #include +#define PRINT_SCREEN 1 +#define PRINT_WINDOW 2 +#define PRINT_PARTIAL 3 typedef struct { WMRect *screens; @@ -271,6 +274,11 @@ typedef struct _WScreen { WMHandlerID *workspace_name_timer; struct WorkspaceNameData *workspace_name_data; + /* mini screenshot data */ + Window mini_screenshot; + time_t mini_screenshot_timeout; + WMHandlerID *mini_screenshot_timer; + /* for raise-delay */ WMHandlerID *autoRaiseTimer; Window autoRaiseWindow; /* window that is scheduled to be @@ -314,7 +322,7 @@ void wScreenRestoreState(WScreen *scr); int wScreenBringInside(WScreen *scr, int *x, int *y, int width, int height); int wScreenKeepInside(WScreen *scr, int *x, int *y, int width, int height); - +void ScreenCapture(WScreen *scr, int mode); /* in startup.c */ WScreen *wScreenWithNumber(int i); diff --git a/src/startup.c b/src/startup.c index 985f0665..ddaf789f 100644 --- a/src/startup.c +++ b/src/startup.c @@ -493,6 +493,7 @@ void StartUp(Bool defaultScreenOnly) wPreferences.cursor[WCUR_QUESTION] = XCreateFontCursor(dpy, XC_question_arrow); wPreferences.cursor[WCUR_TEXT] = XCreateFontCursor(dpy, XC_xterm); /* odd name??? */ wPreferences.cursor[WCUR_SELECT] = XCreateFontCursor(dpy, XC_cross); + wPreferences.cursor[WCUR_CAPTURE] = XCreateFontCursor(dpy, XC_crosshair); Pixmap cur = XCreatePixmap(dpy, DefaultRootWindow(dpy), 16, 16, 1); GC gc = XCreateGC(dpy, cur, 0, NULL); -- 2.11.4.GIT