[0.1.1] Screen capture: PrintScreen freezes the screens, a dragged region goes to the clipboard and the library
- capture_region, a new [global_hotkeys] action, on PrintScreen by default (Windows only; none on macOS). Existing settings files get the line appended on the next launch. When the key cannot be registered, the log names Snagit and Windows' "Print screen opens screen capture" setting. - app/screen_capture_win32.jai: every screen copied the moment the key fires (BitBlt with CAPTUREBLT over the virtual desktop), shown darkened in a topmost GDI window in a modal loop of its own, so it works with mcap hidden in the tray. Dragging shows the region bright and outlined; a magnifier shows the pixels under the cursor with its position or the selection's size; arrow keys move a pixel (ten with Shift). A click or Enter takes the whole screen under the cursor; Escape, a right click or losing the focus cancels. Only the parts that change are redrawn. - app/screen_capture.jai: the region goes on the clipboard (CF_DIB) at once, then a thread writes it into the library's "Screen Captures" folder as a PNG .mcap: metadata.json with the window under the region's middle (its title as WindowName, its program's description as AppName) and capture.json (where on the desktop, the program's path, the hash). mcap then comes to the front showing it, and OCR, labels and segmentation read it as they read an import. - A "screen capture" group in the import tests: the file written, its pixels, preview and Capture Text read back. README and the format's README describe it.
This commit is contained in:
parent
864ead49c9
commit
7f8bcdd1ad
30
README.md
30
README.md
@ -9,7 +9,9 @@ and labels are tested on Windows with an NVIDIA GPU).
|
|||||||
The `.mcap` files come from the **Import** window, which takes any folder of images (PNG, JPEG, HEIC,
|
The `.mcap` files come from the **Import** window, which takes any folder of images (PNG, JPEG, HEIC,
|
||||||
AVIF, WebP, GIF, BMP, TIFF, camera RAW), videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG) and Snagit
|
AVIF, WebP, GIF, BMP, TIFF, camera RAW), videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG) and Snagit
|
||||||
`.snagx` captures, or from `snagit-converter`
|
`.snagx` captures, or from `snagit-converter`
|
||||||
(`D:\McapIndex\SnagitConverter`); `modules/Capture_File/README.md` describes the format.
|
(`D:\McapIndex\SnagitConverter`); `modules/Capture_File/README.md` describes the format. On
|
||||||
|
Windows, **PrintScreen** captures a region of the screen straight into the library, as Snagit does;
|
||||||
|
see [Screen capture](#screen-capture).
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@ -148,6 +150,32 @@ counted (click one to search for it), and the commonest words (click one to sear
|
|||||||
of them are segmented, it also shows **What is in the images**: how many images hold each class
|
of them are segmented, it also shows **What is in the images**: how many images hold each class
|
||||||
(person, sky, dog ...). Click one to search `has:` for it.
|
(person, sky, dog ...). Click one to search `has:` for it.
|
||||||
|
|
||||||
|
## Screen capture
|
||||||
|
|
||||||
|
On Windows, **PrintScreen** captures any part of the screen, from any app, even while mcap is hidden
|
||||||
|
in the tray:
|
||||||
|
|
||||||
|
1. **The screens freeze** the moment it is pressed: everything on every screen is copied and shown
|
||||||
|
darkened over them, so menus, tooltips and video hold still while you choose.
|
||||||
|
2. **Drag a rectangle** over what you want. It shows at full brightness, outlined. A magnifier beside
|
||||||
|
the cursor shows the pixels under it, with the cursor's position, or the selection's size while
|
||||||
|
dragging. The arrow keys move the cursor a pixel at a time, ten with Shift.
|
||||||
|
3. **Let go** to take it. A **click** without dragging, or **Enter**, takes the whole screen the
|
||||||
|
cursor is on. **Escape**, or a right click, cancels; a right click while dragging starts the
|
||||||
|
selection again.
|
||||||
|
|
||||||
|
What is taken is on the clipboard at once, ready to paste. It is also saved into the library, in
|
||||||
|
the folder **Screen Captures**, as a PNG in an `.mcap` with the window it was taken from (the window
|
||||||
|
under the middle of the selection): its title as the window title and its program as the app, so
|
||||||
|
searches find it as they find Snagit's. mcap then comes to the front showing it in the Capture
|
||||||
|
window, and OCR and labels read it once they are free (and segmentation, if the folder is set to
|
||||||
|
segment).
|
||||||
|
|
||||||
|
The key is the `capture_region` line of `[global_hotkeys]` in the settings file. Only one program can
|
||||||
|
have PrintScreen: if Snagit is running it has it, and so does Windows while Settings >
|
||||||
|
Accessibility > Keyboard > "Use the Print screen key to open screen capture" is on. When mcap cannot
|
||||||
|
register the key, the Event Log says so; choose another, such as `Ctrl-Shift-S capture_region`.
|
||||||
|
|
||||||
## Importing
|
## Importing
|
||||||
|
|
||||||
**Import** (above the grid) brings a folder of screenshots, photos and videos into the library:
|
**Import** (above the grid) brings a folder of screenshots, photos and videos into the library:
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
[global_hotkeys]
|
[global_hotkeys]
|
||||||
# Registered with the system, so they work from any application, even while the app is hidden in the tray.
|
# Registered with the system, so they work from any application, even while the app is hidden in the tray.
|
||||||
# The format is that of [hotkeys], including "none" to turn an action off; keys are letters,
|
# The format is that of [hotkeys], including "none" to turn an action off; keys are letters,
|
||||||
# digits, Space, F1-F24, Enter, Tab, Escape, the arrows, PageUp, PageDown, Home, End, Insert and
|
# digits, Space, F1-F24, Enter, Tab, Escape, the arrows, PageUp, PageDown, Home, End, Insert,
|
||||||
# Delete. For example: Ctrl-Alt-Space bring_to_foreground
|
# Delete and PrintScreen. For example: Ctrl-Alt-Space bring_to_foreground
|
||||||
# Changes apply as soon as the file is saved.
|
# Changes apply as soon as the file is saved.
|
||||||
|
|
||||||
none bring_to_foreground
|
none bring_to_foreground
|
||||||
|
|
||||||
|
# Freezes every screen to drag a region over and capture it, into the clipboard and the library.
|
||||||
|
# PrintScreen is Snagit's key too: only one program can have it, so quit Snagit or choose another
|
||||||
|
# key, such as Ctrl-Shift-S. Windows takes it as well while Settings > Accessibility > Keyboard >
|
||||||
|
# "Use the Print screen key to open screen capture" is on.
|
||||||
|
PrintScreen capture_region
|
||||||
|
|||||||
@ -5,3 +5,4 @@
|
|||||||
# Changes apply as soon as the file is saved.
|
# Changes apply as soon as the file is saved.
|
||||||
|
|
||||||
none bring_to_foreground
|
none bring_to_foreground
|
||||||
|
none capture_region
|
||||||
|
|||||||
@ -29,7 +29,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
GIT_BRANCH :: "check";
|
GIT_BRANCH :: "check";
|
||||||
GIT_HASH :: "-";
|
GIT_HASH :: "-";
|
||||||
VERSION :: "0.1.0";
|
VERSION :: "0.1.1";
|
||||||
BUILD_DATE :: "-";
|
BUILD_DATE :: "-";
|
||||||
DEBUG :: true;
|
DEBUG :: true;
|
||||||
COMPILE_TESTS :: true;
|
COMPILE_TESTS :: true;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
||||||
|
|
||||||
VERSION :: "0.1.0";
|
VERSION :: "0.1.1";
|
||||||
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
||||||
// builds never write over the ones you run by hand.
|
// builds never write over the ones you run by hand.
|
||||||
build_directory := "bin";
|
build_directory := "bin";
|
||||||
|
|||||||
@ -6,7 +6,8 @@ then the original and rendered image, then any large chunks. There is no ZIP and
|
|||||||
it; the images and videos keep their own compression.
|
it; the images and videos keep their own compression.
|
||||||
|
|
||||||
A capture is a Snagit capture (from a `.snagx`), an imported image (a photo or screenshot in any image
|
A capture is a Snagit capture (from a `.snagx`), an imported image (a photo or screenshot in any image
|
||||||
format), or an imported video (the mcap app's Import window).
|
format), an imported video (the mcap app's Import window), or a screen capture (the mcap app's
|
||||||
|
PrintScreen).
|
||||||
|
|
||||||
The parts a library browser needs (header, text, OCR, front chunks and preview) come first, so it reads
|
The parts a library browser needs (header, text, OCR, front chunks and preview) come first, so it reads
|
||||||
the start of each file and never touches the images; a search, which needs no preview, reads less still.
|
the start of each file and never touches the images; a search, which needs no preview, reads less still.
|
||||||
@ -94,6 +95,29 @@ Orientation: 6 and, when the EXIF has them: Camer
|
|||||||
|
|
||||||
So the parts a library reads from Snagit's `metadata.json` (date, title, app) work for photos too.
|
So the parts a library reads from Snagit's `metadata.json` (date, title, app) work for photos too.
|
||||||
|
|
||||||
|
## Screen captures
|
||||||
|
|
||||||
|
A region of the screen taken with the mcap app's PrintScreen is stored as a PNG original with no
|
||||||
|
rendered image (kind 0), and its Capture Text holds two files:
|
||||||
|
|
||||||
|
```
|
||||||
|
[[file "metadata.json"]]
|
||||||
|
AppName: "Google Chrome" the program of the window under the region's middle:
|
||||||
|
its description, else its file's name; "" when unknown
|
||||||
|
CaptureDate: "2026-10-10 14:05:09" when it was taken, local time, as Snagit writes it
|
||||||
|
WindowName: "Assign Footprints - KiCad" that window's title
|
||||||
|
|
||||||
|
[[file "capture.json"]]
|
||||||
|
Region: "selection" or "screen": a click took the whole screen
|
||||||
|
Left: -1920 on the virtual desktop, whose origin is the primary
|
||||||
|
Top: 40 screen's top left, so it can be negative
|
||||||
|
Width: 700
|
||||||
|
Height: 300
|
||||||
|
AppPath: "C:/Program Files/Google/Chrome/Application/chrome.exe"
|
||||||
|
ContentHash: "5c1e9a0d77b2f413" 64-bit FNV-1a of the PNG's bytes
|
||||||
|
Format: "PNG"
|
||||||
|
```
|
||||||
|
|
||||||
## Imported videos
|
## Imported videos
|
||||||
|
|
||||||
A video imported from a folder (MP4, MOV, MKV, WebM, AVI, WMV, MPEG; read with FFmpeg, on Windows) is
|
A video imported from a folder (MP4, MOV, MKV, WebM, AVI, WMV, MPEG; read with FFmpeg, on Windows) is
|
||||||
|
|||||||
@ -27,6 +27,8 @@ USING_WINDOWS :: OS == .WINDOWS;
|
|||||||
#load "app/segment_ui.jai";
|
#load "app/segment_ui.jai";
|
||||||
#load "app/library_folders.jai";
|
#load "app/library_folders.jai";
|
||||||
#load "app/exit_shutdown.jai";
|
#load "app/exit_shutdown.jai";
|
||||||
|
#load "app/screen_capture.jai";
|
||||||
|
#if OS == .WINDOWS #load "app/screen_capture_win32.jai";
|
||||||
|
|
||||||
#load "base/circle_buffer.jai";
|
#load "base/circle_buffer.jai";
|
||||||
#load "base/debug.jai";
|
#load "base/debug.jai";
|
||||||
@ -182,6 +184,7 @@ App :: struct {
|
|||||||
show_style_editor: bool;
|
show_style_editor: bool;
|
||||||
minimize_to_tray_this_frame: bool;
|
minimize_to_tray_this_frame: bool;
|
||||||
restore_window_this_frame: bool;
|
restore_window_this_frame: bool;
|
||||||
|
capture_region_this_frame: bool; // the capture_region global hotkey fired: app/screen_capture.jai
|
||||||
is_master: bool; // Are we the first instance of this app launched?
|
is_master: bool; // Are we the first instance of this app launched?
|
||||||
allow_multiple_instances: bool; // Skips the single-instance check, so a dev build can run beside an installed one.
|
allow_multiple_instances: bool; // Skips the single-instance check, so a dev build can run beside an installed one.
|
||||||
should_exit_early: bool;
|
should_exit_early: bool;
|
||||||
|
|||||||
@ -15,12 +15,15 @@ app_ui_startup :: () {
|
|||||||
|
|
||||||
// Every frame, before input is handed to ImGui. Poll background work here.
|
// Every frame, before input is handed to ImGui. Poll background work here.
|
||||||
app_ui_frame_start :: () {
|
app_ui_frame_start :: () {
|
||||||
|
screen_capture_frame(); // also while hidden in the tray, where the frame ends before the UI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
|
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
|
||||||
// stop background work here. Must not touch ImGui, since the main thread is still drawing frames
|
// stop background work here. Must not touch ImGui, since the main thread is still drawing frames
|
||||||
// (the overlay; app_main_window_ui stops running the app's frames once shutdown starts).
|
// (the overlay; app_main_window_ui stops running the app's frames once shutdown starts).
|
||||||
app_ui_shutdown :: () {
|
app_ui_shutdown :: () {
|
||||||
|
app_shutdown_step(.CAPTURE);
|
||||||
|
screen_capture_shutdown();
|
||||||
app_shutdown_step(.IMPORT);
|
app_shutdown_step(.IMPORT);
|
||||||
import_shutdown(); // before the viewer: it hands the viewer what it writes
|
import_shutdown(); // before the viewer: it hands the viewer what it writes
|
||||||
app_shutdown_step(.VIEWER);
|
app_shutdown_step(.VIEWER);
|
||||||
@ -36,7 +39,7 @@ app_ui_shutdown :: () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Which step of app_ui_shutdown is running, and since when, for the overlay a slow shutdown shows.
|
// Which step of app_ui_shutdown is running, and since when, for the overlay a slow shutdown shows.
|
||||||
App_Shutdown_Step :: enum s32 { NONE; IMPORT; VIEWER; OCR; SEGMENT; LABELS; STATS; }
|
App_Shutdown_Step :: enum s32 { NONE; CAPTURE; IMPORT; VIEWER; OCR; SEGMENT; LABELS; STATS; }
|
||||||
app_shutdown: struct {
|
app_shutdown: struct {
|
||||||
step: App_Shutdown_Step;
|
step: App_Shutdown_Step;
|
||||||
step_started_ns: s64; // monotonic
|
step_started_ns: s64; // monotonic
|
||||||
@ -54,6 +57,7 @@ app_ui_shutdown_status :: () -> string {
|
|||||||
text: string;
|
text: string;
|
||||||
if step == {
|
if step == {
|
||||||
case .NONE; return "";
|
case .NONE; return "";
|
||||||
|
case .CAPTURE; text = "Saving a screen capture.";
|
||||||
case .IMPORT;
|
case .IMPORT;
|
||||||
if importer.phase == .INDEXING text = "Stopping the import: it is indexing the library, and stops after the files it is on.";
|
if importer.phase == .INDEXING text = "Stopping the import: it is indexing the library, and stops after the files it is on.";
|
||||||
else if importer.running text = "Stopping the import: finishing the files it is on (a large video takes a while to copy).";
|
else if importer.running text = "Stopping the import: finishing the files it is on (a large video takes a while to copy).";
|
||||||
|
|||||||
@ -466,6 +466,13 @@ app_frame :: (in_live_resize := false) -> bool {
|
|||||||
platform_restore_window();
|
platform_restore_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not from a live-resize frame: choosing the region runs a modal loop of its own.
|
||||||
|
if app.capture_region_this_frame && !in_live_resize {
|
||||||
|
app.capture_region_this_frame = false;
|
||||||
|
screen_capture_take();
|
||||||
|
app.capture_region_this_frame = false; // the hotkey pressed again while choosing
|
||||||
|
}
|
||||||
|
|
||||||
if app.should_exit {
|
if app.should_exit {
|
||||||
app_exit_shutdown_start();
|
app_exit_shutdown_start();
|
||||||
if app_exit_shutdown_poll() return false;
|
if app_exit_shutdown_poll() return false;
|
||||||
|
|||||||
203
src/app/screen_capture.jai
Normal file
203
src/app/screen_capture.jai
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// Screen capture, as Snagit does it: PrintScreen (the capture_region global hotkey, from any app,
|
||||||
|
// even with mcap hidden in the tray) freezes every screen, and a rectangle dragged over them is taken
|
||||||
|
// (app/screen_capture_win32.jai; Windows only so far). What is taken goes onto the clipboard at once,
|
||||||
|
// and into the library as an .mcap in the folder SCREEN_CAPTURE_FOLDER: the pixels as a PNG, with
|
||||||
|
// the window it was taken from in metadata.json, its title as WindowName and its program as AppName,
|
||||||
|
// as Snagit records them, so searches find it the same way. mcap then comes to the front showing
|
||||||
|
// it in the Capture window, and OCR and labels read it once they are free.
|
||||||
|
//
|
||||||
|
// The PNG is made and the file written on a thread of its own, as a large capture takes a second or
|
||||||
|
// so to compress.
|
||||||
|
|
||||||
|
SCREEN_CAPTURE_FOLDER :: "Screen Captures"; // in the library
|
||||||
|
|
||||||
|
Screen_Capture_Job :: struct {
|
||||||
|
shot: Screen_Shot;
|
||||||
|
date: string; // CaptureDate; heap
|
||||||
|
thread: Thread;
|
||||||
|
busy: bool; // cleared by the thread as it ends
|
||||||
|
path: string; // what it wrote; heap
|
||||||
|
error: string; // or why it could not; heap
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_capture: struct {
|
||||||
|
jobs: [..] *Screen_Capture_Job;
|
||||||
|
scan_pending: [..] s32; // captures OCR and labels have yet to read
|
||||||
|
segment_pending: [..] s32; // and then segmentation, when the folder is set to segment
|
||||||
|
}
|
||||||
|
|
||||||
|
// From app_frame when the hotkey fired, outside the ImGui frame: lets a region be chosen, copies it,
|
||||||
|
// and starts writing it into the library.
|
||||||
|
screen_capture_take :: () {
|
||||||
|
if app_exit_shutdown_started() return;
|
||||||
|
date := import_calendar_text(to_calendar(current_time_consensus(), .LOCAL));
|
||||||
|
start := current_time_monotonic();
|
||||||
|
shot, ok, error := platform_screen_capture_select();
|
||||||
|
if !ok {
|
||||||
|
if error { log_error("[capture] %.", error); toast_show(.Error, "Screen capture: %", error); }
|
||||||
|
else log("[capture] Cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("[capture] % x % at %, % (%) from \"%\" (%), chosen in % s.", shot.width, shot.height, shot.left, shot.top,
|
||||||
|
ifx shot.whole_screen then "the whole screen" else "a region", shot.window_title, shot.app_name,
|
||||||
|
FF(to_float64_seconds(current_time_monotonic() - start), 1));
|
||||||
|
|
||||||
|
if !platform_copy_image_to_clipboard(shot.pixels, shot.width, shot.height) log_error("[capture] Could not put the capture on the clipboard.");
|
||||||
|
|
||||||
|
job := New(Screen_Capture_Job,, context.default_allocator);
|
||||||
|
job.shot = shot;
|
||||||
|
job.date = copy_string(date,, context.default_allocator);
|
||||||
|
job.busy = true;
|
||||||
|
if !thread_init(*job.thread, screen_capture_save_proc, temporary_storage_size = 1024 * 1024, name = "screen capture") {
|
||||||
|
log_error("[capture] Could not start the thread to save the capture.");
|
||||||
|
toast_show(.Error, "The screen capture could not be saved");
|
||||||
|
screen_capture_job_free(job);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread_start(*job.thread, job);
|
||||||
|
screen_capture.jobs.allocator = context.default_allocator;
|
||||||
|
array_add(*screen_capture.jobs, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every frame, even while hidden in the tray: the captures written, into the grid and the Capture
|
||||||
|
// window, and then read.
|
||||||
|
screen_capture_frame :: () {
|
||||||
|
if app_exit_shutdown_started() return;
|
||||||
|
for job: screen_capture.jobs {
|
||||||
|
if atomic_read(*job.busy) continue;
|
||||||
|
thread_is_done(*job.thread, -1);
|
||||||
|
thread_deinit(*job.thread);
|
||||||
|
if job.error {
|
||||||
|
log_error("[capture] Could not save the capture: %", job.error);
|
||||||
|
toast_show(.Error, "The screen capture could not be saved: %", job.error);
|
||||||
|
} else {
|
||||||
|
log("[capture] Saved %.", job.path);
|
||||||
|
index := viewer_add_capture(job.path);
|
||||||
|
if index >= 0 {
|
||||||
|
viewer_request_full(index);
|
||||||
|
screen_capture.scan_pending.allocator = context.default_allocator;
|
||||||
|
screen_capture.segment_pending.allocator = context.default_allocator;
|
||||||
|
array_add(*screen_capture.scan_pending, index);
|
||||||
|
if library_folder_segments(SCREEN_CAPTURE_FOLDER) array_add(*screen_capture.segment_pending, index);
|
||||||
|
}
|
||||||
|
app.restore_window_this_frame = true;
|
||||||
|
toast_show(.Success, "% x % captured and copied to the clipboard", job.shot.width, job.shot.height);
|
||||||
|
}
|
||||||
|
screen_capture_job_free(job);
|
||||||
|
remove job;
|
||||||
|
}
|
||||||
|
|
||||||
|
// As an import's are, once the models are free: OCR and labels, then segmentation.
|
||||||
|
if screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running && ocr_ui.engine && labels_ui_settled() {
|
||||||
|
ocr_scan_start(screen_capture.scan_pending);
|
||||||
|
array_reset_keeping_memory(*screen_capture.scan_pending);
|
||||||
|
}
|
||||||
|
// Once their catalog entries are in: the scan takes only images, and they say which.
|
||||||
|
if screen_capture.segment_pending.count && !screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running {
|
||||||
|
catalogued := true;
|
||||||
|
for screen_capture.segment_pending if !viewer.captures[it].catalog && !viewer.captures[it].removed { catalogued = false; break; }
|
||||||
|
if catalogued {
|
||||||
|
segment_scan_start(screen_capture.segment_pending, quiet = true);
|
||||||
|
array_reset_keeping_memory(*screen_capture.segment_pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the shutdown thread: a capture being written is finished, not lost.
|
||||||
|
screen_capture_shutdown :: () {
|
||||||
|
for screen_capture.jobs thread_is_done(*it.thread, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_capture_job_free :: (job: *Screen_Capture_Job) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
screen_shot_free(*job.shot);
|
||||||
|
free(job.date);
|
||||||
|
free(job.path);
|
||||||
|
free(job.error);
|
||||||
|
free(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_capture_save_proc :: (thread: *Thread) -> s64 {
|
||||||
|
job := cast(*Screen_Capture_Job) thread.data;
|
||||||
|
context.allocator = context.default_allocator;
|
||||||
|
defer atomic_swap(*job.busy, false);
|
||||||
|
path, error := screen_capture_write(job.shot, job.date);
|
||||||
|
if error job.error = copy_string(error);
|
||||||
|
else job.path = copy_string(path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the capture into the library: its path, or why it could not be (temp).
|
||||||
|
screen_capture_write :: (shot: Screen_Shot, date: string) -> path: string, error: string {
|
||||||
|
image := Image.{pixels = shot.pixels.data, width = shot.width, height = shot.height};
|
||||||
|
png, _, _, png_ok, png_error := image_preview_png(image, max(shot.width, shot.height)); // at its own size
|
||||||
|
if !png_ok return "", png_error;
|
||||||
|
preview, preview_width, preview_height, preview_ok, preview_error := image_preview_png(image, MCAP_PREVIEW_MAX_SIDE);
|
||||||
|
if !preview_ok return "", preview_error;
|
||||||
|
hash := import_hash(png);
|
||||||
|
|
||||||
|
doc := New(Capture_Document,, temp);
|
||||||
|
capture_document_init(doc, temp);
|
||||||
|
doc.comment = tprint("Captured by % %.", APP_NAME, VERSION);
|
||||||
|
metadata := capture_add_file(doc, "metadata.json");
|
||||||
|
import_add(doc, metadata, "AppName", import_text(shot.app_name));
|
||||||
|
import_add(doc, metadata, "CaptureDate", date);
|
||||||
|
import_add(doc, metadata, "WindowName", import_text(shot.window_title));
|
||||||
|
info := capture_add_file(doc, "capture.json");
|
||||||
|
import_add(doc, info, "Region", ifx shot.whole_screen then "screen" else "selection");
|
||||||
|
import_add_number(doc, info, "Left", xx shot.left);
|
||||||
|
import_add_number(doc, info, "Top", xx shot.top);
|
||||||
|
import_add_number(doc, info, "Width", xx shot.width);
|
||||||
|
import_add_number(doc, info, "Height", xx shot.height);
|
||||||
|
if shot.app_path import_add(doc, info, "AppPath", import_text(shot.app_path));
|
||||||
|
import_add(doc, info, "ContentHash", import_hash_text(hash));
|
||||||
|
import_add(doc, info, "Format", "PNG");
|
||||||
|
|
||||||
|
name := import_out_name(date, hash);
|
||||||
|
text, text_ok := import_document_text(doc, name);
|
||||||
|
if !text_ok return "", "its Capture Text does not read back cleanly (see the Event Log)";
|
||||||
|
|
||||||
|
m: Mcap;
|
||||||
|
m.text = text;
|
||||||
|
m.preview = .{width = cast(u16) preview_width, height = cast(u16) preview_height, png = preview};
|
||||||
|
m.original_name = tprint("%.png", path_strip_extension(name));
|
||||||
|
m.original = png;
|
||||||
|
m.rendered_kind = .NONE;
|
||||||
|
serializer, serialized, serialize_error := mcap_serialize(m,, temp);
|
||||||
|
if !serialized return "", serialize_error;
|
||||||
|
|
||||||
|
folder := tprint("%/%", viewer_folder(), SCREEN_CAPTURE_FOLDER);
|
||||||
|
if !make_directory_if_it_does_not_exist(folder, recursive = true) return "", tprint("% could not be made", folder);
|
||||||
|
path := tprint("%/%", folder, name);
|
||||||
|
number := 2;
|
||||||
|
while file_exists(path) {
|
||||||
|
path = tprint("%/%_%.%", folder, path_strip_extension(name), number, MCAP_EXTENSION);
|
||||||
|
number += 1;
|
||||||
|
}
|
||||||
|
written, write_error := mcap_write(path, cast([] u8) string.{serializer.count, serializer.data});
|
||||||
|
if !written return "", write_error;
|
||||||
|
return path, "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OS != .WINDOWS {
|
||||||
|
Screen_Shot :: struct {
|
||||||
|
pixels: [] u8;
|
||||||
|
width, height: s32;
|
||||||
|
left, top: s32;
|
||||||
|
whole_screen: bool;
|
||||||
|
window_title, app_name, app_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_shot_free :: (shot: *Screen_Shot) {
|
||||||
|
array_free(shot.pixels,, context.default_allocator);
|
||||||
|
shot.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_screen_capture_select :: () -> Screen_Shot, ok: bool, error: string {
|
||||||
|
return .{}, false, "not available on this system yet";
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
652
src/app/screen_capture_win32.jai
Normal file
652
src/app/screen_capture_win32.jai
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
// The screen capture overlay on Windows (app/screen_capture.jai is the rest). The moment the hotkey
|
||||||
|
// fires, everything on every screen is copied, and shown frozen and darkened in a window over all of
|
||||||
|
// them. A rectangle dragged over it shows at full brightness, and letting go takes it; a click takes
|
||||||
|
// the whole screen the cursor is on. A magnifier beside the cursor shows the pixels under it, with the
|
||||||
|
// position or the selection's size, so a region can be chosen to the pixel; the arrow keys move the
|
||||||
|
// cursor a pixel at a time (ten with Shift). Escape or a right click cancels.
|
||||||
|
//
|
||||||
|
// Drawn with GDI into a bitmap the size of the desktop, so it needs neither ImGui nor the D3D11 device
|
||||||
|
// and works while the main window is hidden in the tray. Only the parts that change are drawn again.
|
||||||
|
// The selection runs in a modal loop of its own: platform_screen_capture_select returns once a region
|
||||||
|
// is taken or the capture is cancelled.
|
||||||
|
|
||||||
|
// The region taken, and the window it was taken from.
|
||||||
|
Screen_Shot :: struct {
|
||||||
|
pixels: [] u8; // RGBA, rows packed; heap
|
||||||
|
width, height: s32;
|
||||||
|
left, top: s32; // on the virtual desktop, whose origin is the primary screen's top left
|
||||||
|
whole_screen: bool; // a click took the screen under the cursor
|
||||||
|
window_title: string; // the window under the region's center; heap, as are the two below
|
||||||
|
app_name: string; // its program's description ("Google Chrome"), or its file name
|
||||||
|
app_path: string; // its program's file
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_shot_free :: (shot: *Screen_Shot) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
array_free(shot.pixels);
|
||||||
|
free(shot.window_title);
|
||||||
|
free(shot.app_name);
|
||||||
|
free(shot.app_path);
|
||||||
|
shot.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freezes the screens and lets a region be chosen. ok is false when it was cancelled (error "") or
|
||||||
|
// could not start (error says why).
|
||||||
|
platform_screen_capture_select :: () -> Screen_Shot, ok: bool, error: string {
|
||||||
|
shot: Screen_Shot;
|
||||||
|
fallback_window := GetForegroundWindow(); // before the overlay takes the focus
|
||||||
|
|
||||||
|
if !overlay_freeze() { overlay_release(); return shot, false, "the screen could not be copied"; }
|
||||||
|
defer overlay_release();
|
||||||
|
if !overlay_open() return shot, false, "the capture window could not be opened";
|
||||||
|
|
||||||
|
overlay.app_context = *context;
|
||||||
|
while !overlay.done {
|
||||||
|
msg: MSG;
|
||||||
|
got := GetMessageW(*msg, null, 0, 0);
|
||||||
|
if got == -1 { overlay.cancelled = true; break; }
|
||||||
|
if got == 0 { // WM_QUIT: the app is asked to end; the main loop sees it again
|
||||||
|
PostQuitMessage(cast(s32) msg.wParam);
|
||||||
|
overlay.cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
TranslateMessage(*msg);
|
||||||
|
DispatchMessageW(*msg);
|
||||||
|
}
|
||||||
|
DestroyWindow(overlay.hwnd);
|
||||||
|
overlay.hwnd = null;
|
||||||
|
if overlay.cancelled return shot, false, "";
|
||||||
|
|
||||||
|
r := overlay.result;
|
||||||
|
shot.width = r.right - r.left;
|
||||||
|
shot.height = r.bottom - r.top;
|
||||||
|
if shot.width <= 0 || shot.height <= 0 return shot, false, "";
|
||||||
|
shot.left = overlay.left + r.left;
|
||||||
|
shot.top = overlay.top + r.top;
|
||||||
|
shot.whole_screen = overlay.whole_screen;
|
||||||
|
|
||||||
|
// The desktop's bitmap is BGRA (its alpha undefined), top-down.
|
||||||
|
shot.pixels = NewArray(cast(s64) shot.width * shot.height * 4, u8, initialized = false,, context.default_allocator);
|
||||||
|
out := shot.pixels.data;
|
||||||
|
for y: r.top..r.bottom - 1 {
|
||||||
|
row := overlay.screen_pixels + cast(s64) y * overlay.width;
|
||||||
|
for x: r.left..r.right - 1 {
|
||||||
|
p := row[x];
|
||||||
|
out[0] = cast,trunc(u8) (p >> 16);
|
||||||
|
out[1] = cast,trunc(u8) (p >> 8);
|
||||||
|
out[2] = cast,trunc(u8) p;
|
||||||
|
out[3] = 255;
|
||||||
|
out += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window under the region's middle, now that the overlay is gone; else the one in front.
|
||||||
|
center := POINT.{shot.left + shot.width / 2, shot.top + shot.height / 2};
|
||||||
|
window := WindowFromPoint(center);
|
||||||
|
if window window = GetAncestor(window, GA_ROOT);
|
||||||
|
if !window window = fallback_window;
|
||||||
|
if window overlay_window_info(*shot, window);
|
||||||
|
return shot, true, "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puts the RGBA pixels on the clipboard as a bitmap, which every program pastes.
|
||||||
|
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
|
||||||
|
row_bytes := cast(s64) width * 4;
|
||||||
|
size := size_of(BITMAPINFOHEADER) + row_bytes * height;
|
||||||
|
memory := GlobalAlloc(GMEM_MOVEABLE, cast(SIZE_T) size);
|
||||||
|
if !memory return false;
|
||||||
|
data := cast(*u8) GlobalLock(memory);
|
||||||
|
if !data { GlobalFree(memory); return false; }
|
||||||
|
|
||||||
|
header := cast(*BITMAPINFOHEADER) data;
|
||||||
|
header.* = .{};
|
||||||
|
header.biSize = size_of(BITMAPINFOHEADER);
|
||||||
|
header.biWidth = width;
|
||||||
|
header.biHeight = height; // bottom-up, as most programs expect of CF_DIB
|
||||||
|
header.biPlanes = 1;
|
||||||
|
header.biBitCount = 32;
|
||||||
|
header.biCompression = BI_RGB;
|
||||||
|
header.biSizeImage = cast(u32) (row_bytes * height);
|
||||||
|
out := data + size_of(BITMAPINFOHEADER);
|
||||||
|
for y: 0..height - 1 {
|
||||||
|
in := pixels.data + (height - 1 - y) * row_bytes;
|
||||||
|
for x: 0..width - 1 {
|
||||||
|
out[0] = in[2]; out[1] = in[1]; out[2] = in[0]; out[3] = 255;
|
||||||
|
out += 4; in += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobalUnlock(memory);
|
||||||
|
|
||||||
|
if !OpenClipboard(platform.hwnd) { GlobalFree(memory); return false; }
|
||||||
|
defer CloseClipboard();
|
||||||
|
EmptyClipboard();
|
||||||
|
if !SetClipboardData(CF_DIB, memory) { GlobalFree(memory); return false; } // the clipboard owns it once set
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
overlay: struct {
|
||||||
|
hwnd: HWND;
|
||||||
|
class_registered: bool;
|
||||||
|
app_context: *#Context;
|
||||||
|
|
||||||
|
left, top: s32; // the virtual desktop's top left
|
||||||
|
width, height: s32; // and size, in pixels
|
||||||
|
scale: float; // the DPI scale of the screen the overlay's window counts as on
|
||||||
|
|
||||||
|
screen_dc, dim_dc, back_dc: HDC; // the screens as copied; the same darkened; what is drawn
|
||||||
|
screen_bitmap, dim_bitmap, back_bitmap: HBITMAP;
|
||||||
|
screen_old, dim_old, back_old: HGDIOBJ;
|
||||||
|
screen_pixels: *u32;
|
||||||
|
font, font_old: HFONT;
|
||||||
|
accent_brush, black_brush, label_brush, frame_brush: HBRUSH;
|
||||||
|
|
||||||
|
cursor: POINT; // in the overlay's pixels, as everything below
|
||||||
|
dragging: bool;
|
||||||
|
anchor: POINT;
|
||||||
|
drawn: Overlay_Layout; // what is on screen, to draw again where it was
|
||||||
|
|
||||||
|
active: bool; // has had the focus; losing it after cancels
|
||||||
|
done, cancelled: bool;
|
||||||
|
result: RECT;
|
||||||
|
whole_screen: bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Overlay_Layout :: struct {
|
||||||
|
valid: bool;
|
||||||
|
dragging: bool;
|
||||||
|
selection: RECT; // the pixels selected, while dragging
|
||||||
|
cursor: POINT;
|
||||||
|
magnifier: RECT; // its frame, and the label under it
|
||||||
|
label: RECT;
|
||||||
|
text: [48] u16;
|
||||||
|
text_count: s32;
|
||||||
|
}
|
||||||
|
|
||||||
|
MAGNIFIER_PIXELS :: 21; // the pixels it shows across: odd, so one is in the middle
|
||||||
|
MAGNIFIER_ZOOM :: 6; // each shown this many pixels wide, at 100%
|
||||||
|
MAGNIFIER_OFFSET :: 24; // from the cursor
|
||||||
|
BORDER :: 2; // the selection's outline, at 100%
|
||||||
|
CLICK_SLOP :: 3; // a drag smaller than this either way is a click
|
||||||
|
|
||||||
|
ACCENT_COLOR :: 0x0000A5FF; // COLORREF, 0x00BBGGRR: orange
|
||||||
|
LABEL_COLOR :: 0x00202020;
|
||||||
|
FRAME_COLOR :: 0x00FFFFFF;
|
||||||
|
TEXT_COLOR :: 0x00FFFFFF;
|
||||||
|
|
||||||
|
OVERLAY_CLASS :: "mcapScreenCapture";
|
||||||
|
|
||||||
|
// Copies every screen into a bitmap, and a darkened copy of it.
|
||||||
|
overlay_freeze :: () -> bool {
|
||||||
|
overlay.left = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||||
|
overlay.top = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||||
|
overlay.width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||||
|
overlay.height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||||
|
if overlay.width <= 0 || overlay.height <= 0 return false;
|
||||||
|
|
||||||
|
desktop := GetDC(null);
|
||||||
|
if !desktop return false;
|
||||||
|
defer ReleaseDC(null, desktop);
|
||||||
|
|
||||||
|
dim_pixels: *u32;
|
||||||
|
ok: bool;
|
||||||
|
ok, overlay.screen_dc, overlay.screen_bitmap, overlay.screen_old, overlay.screen_pixels = overlay_bitmap(desktop);
|
||||||
|
if !ok return false;
|
||||||
|
ok, overlay.dim_dc, overlay.dim_bitmap, overlay.dim_old, dim_pixels = overlay_bitmap(desktop);
|
||||||
|
if !ok return false;
|
||||||
|
back_pixels: *u32;
|
||||||
|
ok, overlay.back_dc, overlay.back_bitmap, overlay.back_old, back_pixels = overlay_bitmap(desktop);
|
||||||
|
if !ok return false;
|
||||||
|
|
||||||
|
// CAPTUREBLT takes layered windows too, such as tooltips and menus with shadows.
|
||||||
|
if !BitBlt(overlay.screen_dc, 0, 0, overlay.width, overlay.height, desktop, overlay.left, overlay.top, cast(ROP) (cast(u32) ROP.SRCCOPY | CAPTUREBLT)) return false;
|
||||||
|
|
||||||
|
count := cast(s64) overlay.width * overlay.height;
|
||||||
|
for 0..count - 1 dim_pixels[it] = (overlay.screen_pixels[it] >> 1) & 0x7F7F7F7F; // half as bright
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_bitmap :: (desktop: HDC) -> ok: bool, dc: HDC, bitmap: HBITMAP, old: HGDIOBJ, pixels: *u32 {
|
||||||
|
info: BITMAPINFO;
|
||||||
|
info.bmiHeader.biSize = size_of(BITMAPINFOHEADER);
|
||||||
|
info.bmiHeader.biWidth = overlay.width;
|
||||||
|
info.bmiHeader.biHeight = -overlay.height; // top-down
|
||||||
|
info.bmiHeader.biPlanes = 1;
|
||||||
|
info.bmiHeader.biBitCount = 32;
|
||||||
|
info.bmiHeader.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
dc := CreateCompatibleDC(desktop);
|
||||||
|
if !dc return false, null, null, null, null;
|
||||||
|
bits: *void;
|
||||||
|
bitmap := CreateDIBSection(dc, *info, DIB_RGB_COLORS, *bits, null, 0);
|
||||||
|
if !bitmap || !bits { DeleteDC(dc); return false, null, null, null, null; }
|
||||||
|
old := SelectObject(dc, bitmap);
|
||||||
|
return true, dc, bitmap, old, cast(*u32) bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_open :: () -> bool {
|
||||||
|
instance := GetModuleHandleW(null);
|
||||||
|
if !overlay.class_registered {
|
||||||
|
wc: WNDCLASSEXW;
|
||||||
|
wc.cbSize = size_of(WNDCLASSEXW);
|
||||||
|
wc.lpfnWndProc = xx overlay_proc;
|
||||||
|
wc.hInstance = instance;
|
||||||
|
wc.hCursor = LoadCursorW(null, IDC_CROSS);
|
||||||
|
wc.lpszClassName = utf8_to_wide(OVERLAY_CLASS,, context.default_allocator);
|
||||||
|
if RegisterClassExW(*wc) == 0 return false;
|
||||||
|
overlay.class_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.cursor = .{};
|
||||||
|
overlay.dragging = false;
|
||||||
|
overlay.drawn = .{};
|
||||||
|
overlay.active = false;
|
||||||
|
overlay.done = false;
|
||||||
|
overlay.cancelled = false;
|
||||||
|
overlay.whole_screen = false;
|
||||||
|
|
||||||
|
// Topmost, and a tool window so it is not in the taskbar or Alt+Tab.
|
||||||
|
overlay.hwnd = CreateWindowExW(WS_EX_TOPMOST | WS_EX_TOOLWINDOW, utf8_to_wide(OVERLAY_CLASS,, temp), utf8_to_wide("Screen capture",, temp),
|
||||||
|
WS_POPUP, overlay.left, overlay.top, overlay.width, overlay.height, null, null, instance, null);
|
||||||
|
if !overlay.hwnd return false;
|
||||||
|
|
||||||
|
dpi := GetDpiForWindow(overlay.hwnd);
|
||||||
|
overlay.scale = ifx dpi then dpi / 96.0 else 1.0;
|
||||||
|
font_height := cast(s32) (15 * overlay.scale + 0.5);
|
||||||
|
overlay.font = CreateFontW(-font_height, 0, 0, 0, 600, 0, 0, 0, 1, 0, 0, CLEARTYPE_QUALITY, 0, utf8_to_wide("Segoe UI",, temp));
|
||||||
|
if overlay.font overlay.font_old = SelectObject(overlay.back_dc, overlay.font);
|
||||||
|
overlay.accent_brush = CreateSolidBrush(ACCENT_COLOR);
|
||||||
|
overlay.black_brush = CreateSolidBrush(0);
|
||||||
|
overlay.label_brush = CreateSolidBrush(LABEL_COLOR);
|
||||||
|
overlay.frame_brush = CreateSolidBrush(FRAME_COLOR);
|
||||||
|
|
||||||
|
cursor: POINT;
|
||||||
|
GetCursorPos(*cursor);
|
||||||
|
overlay.cursor = .{cursor.x - overlay.left, cursor.y - overlay.top};
|
||||||
|
|
||||||
|
ShowWindow(overlay.hwnd, SW_SHOW);
|
||||||
|
SetForegroundWindow(overlay.hwnd); // allowed: the hotkey made this the app the user last used
|
||||||
|
overlay_update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_release :: () {
|
||||||
|
release :: (dc: *HDC, bitmap: *HBITMAP, old: HGDIOBJ) {
|
||||||
|
if dc.* {
|
||||||
|
SelectObject(dc.*, old);
|
||||||
|
DeleteDC(dc.*);
|
||||||
|
dc.* = null;
|
||||||
|
}
|
||||||
|
if bitmap.* { DeleteObject(bitmap.*); bitmap.* = null; }
|
||||||
|
}
|
||||||
|
if overlay.font {
|
||||||
|
if overlay.back_dc SelectObject(overlay.back_dc, overlay.font_old);
|
||||||
|
DeleteObject(overlay.font);
|
||||||
|
overlay.font = null;
|
||||||
|
}
|
||||||
|
release(*overlay.screen_dc, *overlay.screen_bitmap, overlay.screen_old);
|
||||||
|
release(*overlay.dim_dc, *overlay.dim_bitmap, overlay.dim_old);
|
||||||
|
release(*overlay.back_dc, *overlay.back_bitmap, overlay.back_old);
|
||||||
|
overlay.screen_pixels = null;
|
||||||
|
for brush: HBRUSH.[overlay.accent_brush, overlay.black_brush, overlay.label_brush, overlay.frame_brush] if brush DeleteObject(brush);
|
||||||
|
overlay.accent_brush, overlay.black_brush, overlay.label_brush, overlay.frame_brush = null, null, null, null;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_proc :: (hwnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT #c_call {
|
||||||
|
if !overlay.app_context return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
push_context overlay.app_context.* {
|
||||||
|
if message == {
|
||||||
|
case WM_SETCURSOR;
|
||||||
|
SetCursor(LoadCursorW(null, IDC_CROSS));
|
||||||
|
return 1;
|
||||||
|
case WM_ERASEBKGND;
|
||||||
|
return 1;
|
||||||
|
case WM_DPICHANGED;
|
||||||
|
return 0; // the window keeps covering the desktop, whatever DPI it counts as at
|
||||||
|
case WM_PAINT;
|
||||||
|
overlay_paint();
|
||||||
|
return 0;
|
||||||
|
case WM_ACTIVATE;
|
||||||
|
if (wParam & 0xFFFF) == WA_INACTIVE {
|
||||||
|
// Another window came in front: a stuck overlay would hide it. Not once done, as closing
|
||||||
|
// the overlay sends this too.
|
||||||
|
if overlay.active && !overlay.done overlay_cancel();
|
||||||
|
} else {
|
||||||
|
overlay.active = true;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_MOUSEMOVE;
|
||||||
|
overlay.cursor = overlay_point(lParam);
|
||||||
|
overlay_update();
|
||||||
|
return 0;
|
||||||
|
case WM_LBUTTONDOWN;
|
||||||
|
overlay.cursor = overlay_point(lParam);
|
||||||
|
overlay.anchor = overlay.cursor;
|
||||||
|
overlay.dragging = true;
|
||||||
|
SetCapture(hwnd);
|
||||||
|
overlay_update();
|
||||||
|
return 0;
|
||||||
|
case WM_LBUTTONUP;
|
||||||
|
if overlay.dragging {
|
||||||
|
ReleaseCapture();
|
||||||
|
overlay.cursor = overlay_point(lParam);
|
||||||
|
overlay_take();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_RBUTTONDOWN;
|
||||||
|
if overlay.dragging { // let go of the selection, and start again
|
||||||
|
overlay.dragging = false;
|
||||||
|
ReleaseCapture();
|
||||||
|
overlay_update();
|
||||||
|
} else {
|
||||||
|
overlay_cancel();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_KEYDOWN;
|
||||||
|
overlay_key(wParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_point :: (lParam: LPARAM) -> POINT {
|
||||||
|
return .{cast(s32) LOWORD(lParam), cast(s32) HIWORD(lParam)}; // signed: a capture reaches past the edges
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_key :: (key: WPARAM) {
|
||||||
|
step: s32 = ifx GetKeyState(VK_SHIFT) < 0 then cast(s32) 10 else 1;
|
||||||
|
dx, dy: s32;
|
||||||
|
if key == {
|
||||||
|
case VK_ESCAPE; overlay_cancel(); return;
|
||||||
|
case VK_RETURN;
|
||||||
|
if overlay.dragging ReleaseCapture();
|
||||||
|
overlay_take();
|
||||||
|
return;
|
||||||
|
case VK_LEFT; dx = -step;
|
||||||
|
case VK_RIGHT; dx = step;
|
||||||
|
case VK_UP; dy = -step;
|
||||||
|
case VK_DOWN; dy = step;
|
||||||
|
case; return;
|
||||||
|
}
|
||||||
|
// Moving the cursor sends WM_MOUSEMOVE, which does the rest.
|
||||||
|
x := clamp(overlay.cursor.x + dx, 0, overlay.width - 1);
|
||||||
|
y := clamp(overlay.cursor.y + dy, 0, overlay.height - 1);
|
||||||
|
capture_set_cursor_pos(overlay.left + x, overlay.top + y);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_cancel :: () {
|
||||||
|
overlay.cancelled = true;
|
||||||
|
overlay.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The selection, or on a click the screen under the cursor.
|
||||||
|
overlay_take :: () {
|
||||||
|
selection := overlay_selection();
|
||||||
|
if !overlay.dragging || (selection.right - selection.left < CLICK_SLOP && selection.bottom - selection.top < CLICK_SLOP) {
|
||||||
|
selection = overlay_monitor_rect(overlay.cursor);
|
||||||
|
overlay.whole_screen = true;
|
||||||
|
}
|
||||||
|
overlay.dragging = false;
|
||||||
|
selection.left = clamp(selection.left, 0, overlay.width);
|
||||||
|
selection.right = clamp(selection.right, 0, overlay.width);
|
||||||
|
selection.top = clamp(selection.top, 0, overlay.height);
|
||||||
|
selection.bottom = clamp(selection.bottom, 0, overlay.height);
|
||||||
|
overlay.result = selection;
|
||||||
|
overlay.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From the anchor to the cursor, both pixels in it.
|
||||||
|
overlay_selection :: () -> RECT {
|
||||||
|
a, c := overlay.anchor, overlay.cursor;
|
||||||
|
c.x = clamp(c.x, 0, overlay.width - 1);
|
||||||
|
c.y = clamp(c.y, 0, overlay.height - 1);
|
||||||
|
return .{min(a.x, c.x), min(a.y, c.y), max(a.x, c.x) + 1, max(a.y, c.y) + 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The screen a point is on, in the overlay's pixels.
|
||||||
|
overlay_monitor_rect :: (point: POINT) -> RECT {
|
||||||
|
monitor := MonitorFromPoint(.{point.x + overlay.left, point.y + overlay.top}, MONITOR_DEFAULTTONEAREST);
|
||||||
|
info: MONITORINFO;
|
||||||
|
info.cbSize = size_of(MONITORINFO);
|
||||||
|
if !monitor || !GetMonitorInfoW(monitor, *info) return .{0, 0, overlay.width, overlay.height};
|
||||||
|
r := info.rcMonitor;
|
||||||
|
return .{r.left - overlay.left, r.top - overlay.top, r.right - overlay.left, r.bottom - overlay.top};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Works out what to draw for the cursor and selection now, and has what changed drawn again: where
|
||||||
|
// each part was, and where it is.
|
||||||
|
overlay_update :: () {
|
||||||
|
layout: Overlay_Layout;
|
||||||
|
layout.valid = true;
|
||||||
|
layout.dragging = overlay.dragging;
|
||||||
|
layout.cursor = .{clamp(overlay.cursor.x, 0, overlay.width - 1), clamp(overlay.cursor.y, 0, overlay.height - 1)};
|
||||||
|
if layout.dragging layout.selection = overlay_selection();
|
||||||
|
|
||||||
|
text: string;
|
||||||
|
if layout.dragging text = tprint("% × %", layout.selection.right - layout.selection.left, layout.selection.bottom - layout.selection.top);
|
||||||
|
else text = tprint("%, %", layout.cursor.x + overlay.left, layout.cursor.y + overlay.top);
|
||||||
|
wide := utf8_to_wide(text,, temp);
|
||||||
|
while wide && wide[layout.text_count] && layout.text_count < layout.text.count layout.text_count += 1;
|
||||||
|
memcpy(layout.text.data, wide, layout.text_count * size_of(u16));
|
||||||
|
|
||||||
|
// The magnifier below and right of the cursor, or on the other side where the screen ends.
|
||||||
|
zoom := overlay_zoom();
|
||||||
|
side := MAGNIFIER_PIXELS * zoom + 2; // and its frame
|
||||||
|
offset := cast(s32) (MAGNIFIER_OFFSET * overlay.scale);
|
||||||
|
padding := cast(s32) (4 * overlay.scale);
|
||||||
|
extent: SIZE;
|
||||||
|
GetTextExtentPoint32W(overlay.back_dc, layout.text.data, layout.text_count, *extent);
|
||||||
|
label_height := extent.cy + 2 * padding;
|
||||||
|
screen := overlay_monitor_rect(layout.cursor);
|
||||||
|
x := layout.cursor.x + offset;
|
||||||
|
y := layout.cursor.y + offset;
|
||||||
|
if x + side > screen.right x = layout.cursor.x - offset - side;
|
||||||
|
if y + side + label_height > screen.bottom y = layout.cursor.y - offset - side - label_height;
|
||||||
|
layout.magnifier = .{x, y, x + side, y + side};
|
||||||
|
label_width := max(side, extent.cx + 2 * padding);
|
||||||
|
layout.label = .{x, y + side, x + label_width, y + side + label_height};
|
||||||
|
|
||||||
|
previous := overlay.drawn;
|
||||||
|
overlay.drawn = layout;
|
||||||
|
if previous.valid overlay_invalidate(previous);
|
||||||
|
overlay_invalidate(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_zoom :: () -> s32 {
|
||||||
|
return max(cast(s32) (MAGNIFIER_ZOOM * overlay.scale + 0.5), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_invalidate :: (layout: Overlay_Layout) {
|
||||||
|
invalidate :: (r: RECT) {
|
||||||
|
rect := r;
|
||||||
|
InvalidateRect(overlay.hwnd, *rect, .FALSE);
|
||||||
|
}
|
||||||
|
border := overlay_border();
|
||||||
|
if layout.dragging {
|
||||||
|
s := layout.selection;
|
||||||
|
invalidate(.{s.left - border, s.top - border, s.right + border, s.bottom + border});
|
||||||
|
} else {
|
||||||
|
invalidate(.{0, layout.cursor.y, overlay.width, layout.cursor.y + 1});
|
||||||
|
invalidate(.{layout.cursor.x, 0, layout.cursor.x + 1, overlay.height});
|
||||||
|
}
|
||||||
|
invalidate(layout.magnifier);
|
||||||
|
invalidate(layout.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_border :: () -> s32 {
|
||||||
|
return max(cast(s32) (BORDER * overlay.scale + 0.5), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws what has changed into the back bitmap, clipped to it, and puts it on screen.
|
||||||
|
overlay_paint :: () {
|
||||||
|
region := CreateRectRgn(0, 0, 0, 0);
|
||||||
|
if region GetUpdateRgn(overlay.hwnd, region, .FALSE);
|
||||||
|
paint: PAINTSTRUCT;
|
||||||
|
dc := BeginPaint(overlay.hwnd, *paint);
|
||||||
|
if region SelectClipRgn(overlay.back_dc, region);
|
||||||
|
overlay_draw(overlay.back_dc, overlay.drawn);
|
||||||
|
r := paint.rcPaint;
|
||||||
|
BitBlt(dc, r.left, r.top, r.right - r.left, r.bottom - r.top, overlay.back_dc, r.left, r.top, .SRCCOPY);
|
||||||
|
SelectClipRgn(overlay.back_dc, null);
|
||||||
|
if region DeleteObject(region);
|
||||||
|
EndPaint(overlay.hwnd, *paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_draw :: (dc: HDC, layout: Overlay_Layout) {
|
||||||
|
fill :: (r: RECT, brush: HBRUSH) #expand {
|
||||||
|
rect := r;
|
||||||
|
FillRect(`dc, *rect, brush);
|
||||||
|
}
|
||||||
|
BitBlt(dc, 0, 0, overlay.width, overlay.height, overlay.dim_dc, 0, 0, .SRCCOPY);
|
||||||
|
if !layout.valid return;
|
||||||
|
|
||||||
|
border := overlay_border();
|
||||||
|
if layout.dragging {
|
||||||
|
s := layout.selection;
|
||||||
|
BitBlt(dc, s.left, s.top, s.right - s.left, s.bottom - s.top, overlay.screen_dc, s.left, s.top, .SRCCOPY);
|
||||||
|
// The outline just outside it, so every pixel taken shows as it is.
|
||||||
|
fill(.{s.left - border, s.top - border, s.right + border, s.top}, overlay.accent_brush);
|
||||||
|
fill(.{s.left - border, s.bottom, s.right + border, s.bottom + border}, overlay.accent_brush);
|
||||||
|
fill(.{s.left - border, s.top, s.left, s.bottom}, overlay.accent_brush);
|
||||||
|
fill(.{s.right, s.top, s.right + border, s.bottom}, overlay.accent_brush);
|
||||||
|
} else {
|
||||||
|
fill(.{0, layout.cursor.y, overlay.width, layout.cursor.y + 1}, overlay.accent_brush);
|
||||||
|
fill(.{layout.cursor.x, 0, layout.cursor.x + 1, overlay.height}, overlay.accent_brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The magnifier: the pixels around the cursor, as copied, each zoom pixels wide. Past the
|
||||||
|
// desktop's edges it is black.
|
||||||
|
m := layout.magnifier;
|
||||||
|
fill(m, overlay.frame_brush);
|
||||||
|
inner := RECT.{m.left + 1, m.top + 1, m.right - 1, m.bottom - 1};
|
||||||
|
fill(inner, overlay.black_brush);
|
||||||
|
zoom := overlay_zoom();
|
||||||
|
half :: MAGNIFIER_PIXELS / 2;
|
||||||
|
source := RECT.{layout.cursor.x - half, layout.cursor.y - half, layout.cursor.x + half + 1, layout.cursor.y + half + 1};
|
||||||
|
shown := RECT.{max(source.left, 0), max(source.top, 0), min(source.right, overlay.width), min(source.bottom, overlay.height)};
|
||||||
|
if shown.right > shown.left && shown.bottom > shown.top {
|
||||||
|
SetStretchBltMode(dc, .COLORONCOLOR);
|
||||||
|
StretchBlt(dc, inner.left + (shown.left - source.left) * zoom, inner.top + (shown.top - source.top) * zoom,
|
||||||
|
(shown.right - shown.left) * zoom, (shown.bottom - shown.top) * zoom,
|
||||||
|
overlay.screen_dc, shown.left, shown.top, shown.right - shown.left, shown.bottom - shown.top, .SRCCOPY);
|
||||||
|
}
|
||||||
|
// The pixel under the cursor, outlined.
|
||||||
|
c := RECT.{inner.left + half * zoom, inner.top + half * zoom, inner.left + (half + 1) * zoom, inner.top + (half + 1) * zoom};
|
||||||
|
fill(.{c.left - 1, c.top - 1, c.right + 1, c.top}, overlay.accent_brush);
|
||||||
|
fill(.{c.left - 1, c.bottom, c.right + 1, c.bottom + 1}, overlay.accent_brush);
|
||||||
|
fill(.{c.left - 1, c.top, c.left, c.bottom}, overlay.accent_brush);
|
||||||
|
fill(.{c.right, c.top, c.right + 1, c.bottom}, overlay.accent_brush);
|
||||||
|
|
||||||
|
// Under it, the position, or the selection's size.
|
||||||
|
fill(layout.label, overlay.label_brush);
|
||||||
|
SetBkMode(dc, TRANSPARENT);
|
||||||
|
SetTextColor(dc, TEXT_COLOR);
|
||||||
|
text := layout.text;
|
||||||
|
padding := cast(s32) (4 * overlay.scale);
|
||||||
|
TextOutW(dc, layout.label.left + padding, layout.label.top + padding, text.data, layout.text_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window's title and its program.
|
||||||
|
overlay_window_info :: (shot: *Screen_Shot, window: HWND) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
title: [1024] u16;
|
||||||
|
count := GetWindowTextW(window, title.data, title.count);
|
||||||
|
if count > 0 {
|
||||||
|
text, ok := wide_to_utf8(title.data, count,, temp);
|
||||||
|
if ok shot.window_title = copy_string(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
process_id: u32;
|
||||||
|
GetWindowThreadProcessId(window, *process_id);
|
||||||
|
if !process_id return;
|
||||||
|
process := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, .FALSE, process_id);
|
||||||
|
if !process return;
|
||||||
|
defer CloseHandle(process);
|
||||||
|
path: [1024] u16;
|
||||||
|
length: u32 = path.count;
|
||||||
|
if !QueryFullProcessImageNameW(process, 0, path.data, *length) return;
|
||||||
|
text, ok := wide_to_utf8(path.data, cast(s32) length,, temp);
|
||||||
|
if !ok return;
|
||||||
|
shot.app_path = copy_string(text);
|
||||||
|
description := overlay_file_description(path.data);
|
||||||
|
shot.app_name = copy_string(ifx description then description else path_strip_extension(path_filename(text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A program's description, as Task Manager shows it: "Google Chrome" for chrome.exe. "" when it has none. Temp.
|
||||||
|
overlay_file_description :: (path: *u16) -> string {
|
||||||
|
handle: u32;
|
||||||
|
size := GetFileVersionInfoSizeW(path, *handle);
|
||||||
|
if !size return "";
|
||||||
|
data := NewArray(size, u8, initialized = false,, temp);
|
||||||
|
if !GetFileVersionInfoW(path, 0, size, data.data) return "";
|
||||||
|
|
||||||
|
language := "040904b0"; // US English, Unicode: when it does not say
|
||||||
|
translation: *u16;
|
||||||
|
length: u32;
|
||||||
|
if VerQueryValueW(data.data, utf8_to_wide("\\VarFileInfo\\Translation",, temp), xx *translation, *length) != .FALSE && length >= 4 {
|
||||||
|
language = tprint("%1%2", formatInt(translation[0], base = 16, minimum_digits = 4), formatInt(translation[1], base = 16, minimum_digits = 4));
|
||||||
|
}
|
||||||
|
value: *u16;
|
||||||
|
key := utf8_to_wide(tprint("\\StringFileInfo\\%\\FileDescription", language),, temp);
|
||||||
|
if !VerQueryValueW(data.data, key, xx *value, *length) || length <= 1 return "";
|
||||||
|
text, ok := wide_to_utf8(value, cast(s32) (length - 1),, temp); // length counts the terminator
|
||||||
|
return ifx ok then trim(text) else "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- bindings
|
||||||
|
|
||||||
|
gdi32 :: #library,system "gdi32";
|
||||||
|
version_dll :: #library,system "version";
|
||||||
|
|
||||||
|
HFONT :: HANDLE;
|
||||||
|
HRGN :: HANDLE;
|
||||||
|
|
||||||
|
PAINTSTRUCT :: struct {
|
||||||
|
hdc: HDC;
|
||||||
|
fErase: BOOL;
|
||||||
|
rcPaint: RECT;
|
||||||
|
fRestore: BOOL;
|
||||||
|
fIncUpdate: BOOL;
|
||||||
|
rgbReserved: [32] u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CAPTUREBLT :: 0x40000000;
|
||||||
|
GA_ROOT :: 2;
|
||||||
|
WA_INACTIVE :: 0;
|
||||||
|
WM_ACTIVATE :: 0x0006;
|
||||||
|
TRANSPARENT :: 1;
|
||||||
|
CLEARTYPE_QUALITY :: 5;
|
||||||
|
PROCESS_QUERY_LIMITED_INFORMATION :: 0x1000;
|
||||||
|
|
||||||
|
GetMessageW :: (msg: *MSG, hwnd: HWND, filter_min: u32, filter_max: u32) -> s32 #foreign user32;
|
||||||
|
BeginPaint :: (hwnd: HWND, paint: *PAINTSTRUCT) -> HDC #foreign user32;
|
||||||
|
EndPaint :: (hwnd: HWND, paint: *PAINTSTRUCT) -> BOOL #foreign user32;
|
||||||
|
FillRect :: (dc: HDC, rect: *RECT, brush: HBRUSH) -> s32 #foreign user32;
|
||||||
|
GetUpdateRgn :: (hwnd: HWND, region: HRGN, erase: BOOL) -> s32 #foreign user32;
|
||||||
|
GetDpiForWindow :: (hwnd: HWND) -> u32 #foreign user32;
|
||||||
|
GetWindowTextW :: (hwnd: HWND, text: *u16, max_count: s32) -> s32 #foreign user32;
|
||||||
|
GetWindowThreadProcessId :: (hwnd: HWND, process_id: *u32) -> u32 #foreign user32;
|
||||||
|
WindowFromPoint :: (point: POINT) -> HWND #foreign user32;
|
||||||
|
GetAncestor :: (hwnd: HWND, flags: u32) -> HWND #foreign user32;
|
||||||
|
capture_set_cursor_pos :: (x: s32, y: s32) -> BOOL #foreign user32 "SetCursorPos";
|
||||||
|
|
||||||
|
CreateDIBSection :: (dc: HDC, info: *BITMAPINFO, usage: u32, bits: **void, section: HANDLE, offset: u32) -> HBITMAP #foreign gdi32;
|
||||||
|
CreateRectRgn :: (left: s32, top: s32, right: s32, bottom: s32) -> HRGN #foreign gdi32;
|
||||||
|
SelectClipRgn :: (dc: HDC, region: HRGN) -> s32 #foreign gdi32;
|
||||||
|
SetTextColor :: (dc: HDC, color: u32) -> u32 #foreign gdi32;
|
||||||
|
SetBkMode :: (dc: HDC, mode: s32) -> s32 #foreign gdi32;
|
||||||
|
TextOutW :: (dc: HDC, x: s32, y: s32, text: *u16, count: s32) -> BOOL #foreign gdi32;
|
||||||
|
GetTextExtentPoint32W :: (dc: HDC, text: *u16, count: s32, size: *SIZE) -> BOOL #foreign gdi32;
|
||||||
|
CreateFontW :: (height: s32, width: s32, escapement: s32, orientation: s32, weight: s32, italic: u32, underline: u32,
|
||||||
|
strike_out: u32, char_set: u32, out_precision: u32, clip_precision: u32, quality: u32,
|
||||||
|
pitch_and_family: u32, face: *u16) -> HFONT #foreign gdi32;
|
||||||
|
|
||||||
|
OpenProcess :: (access: u32, inherit: BOOL, process_id: u32) -> HANDLE #foreign kernel32;
|
||||||
|
QueryFullProcessImageNameW :: (process: HANDLE, flags: u32, name: *u16, size: *u32) -> BOOL #foreign kernel32;
|
||||||
|
GlobalFree :: (memory: HGLOBAL) -> HGLOBAL #foreign kernel32;
|
||||||
|
|
||||||
|
GetFileVersionInfoSizeW :: (path: *u16, handle: *u32) -> u32 #foreign version_dll;
|
||||||
|
GetFileVersionInfoW :: (path: *u16, handle: u32, length: u32, data: *void) -> BOOL #foreign version_dll;
|
||||||
|
VerQueryValueW :: (block: *void, sub_block: *u16, buffer: **void, length: *u32) -> BOOL #foreign version_dll;
|
||||||
@ -298,6 +298,7 @@ platform_apply_global_hotkeys :: (actions: [] Hotkey_Action) { using platform;
|
|||||||
id := global_hotkey_id(action_index, binding_index);
|
id := global_hotkey_id(action_index, binding_index);
|
||||||
if !RegisterHotKey(hwnd, id, modifiers, vk) {
|
if !RegisterHotKey(hwnd, id, modifiers, vk) {
|
||||||
log_error("[hotkeys] Could not register the global hotkey % (%); another program may be using it.", to_string(combo), action.name);
|
log_error("[hotkeys] Could not register the global hotkey % (%); another program may be using it.", to_string(combo), action.name);
|
||||||
|
if vk == VK_SNAPSHOT log_error("[hotkeys] PrintScreen is taken by Snagit when it runs, and by Windows when Settings > Accessibility > Keyboard > \"Use the Print screen key to open screen capture\" is on.");
|
||||||
array_add(*failed, to_string(combo));
|
array_add(*failed, to_string(combo));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -323,6 +324,7 @@ win32_virtual_key :: (key: ImGui.Key) -> u32 {
|
|||||||
if k >= cast(s64) ImGui.Key._0 && k <= cast(s64) ImGui.Key._9 return cast(u32) (#char "0" + k - cast(s64) ImGui.Key._0);
|
if k >= cast(s64) ImGui.Key._0 && k <= cast(s64) ImGui.Key._9 return cast(u32) (#char "0" + k - cast(s64) ImGui.Key._0);
|
||||||
if k >= cast(s64) ImGui.Key.F1 && k <= cast(s64) ImGui.Key.F24 return cast(u32) (VK_F1 + k - cast(s64) ImGui.Key.F1);
|
if k >= cast(s64) ImGui.Key.F1 && k <= cast(s64) ImGui.Key.F24 return cast(u32) (VK_F1 + k - cast(s64) ImGui.Key.F1);
|
||||||
if key == {
|
if key == {
|
||||||
|
case .PrintScreen; return VK_SNAPSHOT;
|
||||||
case .Space; return VK_SPACE;
|
case .Space; return VK_SPACE;
|
||||||
case .Enter; return VK_RETURN;
|
case .Enter; return VK_RETURN;
|
||||||
case .Tab; return VK_TAB;
|
case .Tab; return VK_TAB;
|
||||||
|
|||||||
@ -600,6 +600,76 @@ segment_overlay_tests :: () {
|
|||||||
file_delete(bare);
|
file_delete(bare);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A screen capture written into the library (app/screen_capture.jai): a PNG of its pixels, a preview,
|
||||||
|
// and the window it came from where search reads it.
|
||||||
|
screen_capture_tests :: () {
|
||||||
|
group("screen capture");
|
||||||
|
#if OS == .WINDOWS library := tprint("%/mcap-tests/library", windows_expand_environment("%TEMP%"));
|
||||||
|
else library := "/tmp/mcap-tests/library";
|
||||||
|
saved_library := app.library_folder;
|
||||||
|
app.library_folder = library;
|
||||||
|
defer app.library_folder = saved_library;
|
||||||
|
|
||||||
|
WIDTH :: 700;
|
||||||
|
HEIGHT :: 300;
|
||||||
|
shot: Screen_Shot;
|
||||||
|
shot.width, shot.height = WIDTH, HEIGHT;
|
||||||
|
shot.left, shot.top = -1920, 40; // on a screen left of the primary one
|
||||||
|
shot.pixels = NewArray(WIDTH * HEIGHT * 4, u8,, temp);
|
||||||
|
for y: 0..HEIGHT - 1 for x: 0..WIDTH - 1 {
|
||||||
|
p := shot.pixels.data + (y * WIDTH + x) * 4;
|
||||||
|
p[0], p[1], p[2], p[3] = cast,trunc(u8) x, cast,trunc(u8) y, 200, 255;
|
||||||
|
}
|
||||||
|
shot.window_title = "Assign Footprints \x01- KiCad"; // a control character, as titles can hold
|
||||||
|
shot.app_name = "KiCad";
|
||||||
|
shot.app_path = "C:/Program Files/KiCad/bin/kicad.exe";
|
||||||
|
|
||||||
|
path, error := screen_capture_write(shot, "2026-10-10 14:05:09");
|
||||||
|
expect(!error, "written: %", error);
|
||||||
|
expect(begins_with(path, tprint("%/%/2026-10-10_14-05-09_", library, SCREEN_CAPTURE_FOLDER)), "named by its date, in its folder: %", path);
|
||||||
|
again, again_error := screen_capture_write(shot, "2026-10-10 14:05:09");
|
||||||
|
expect(!again_error && again != path && ends_with(again, "_2.mcap"), "the same again gets a name of its own: %", again);
|
||||||
|
|
||||||
|
m, parsed := mcap_parse(cast([] u8) read_entire_file(path,, temp));
|
||||||
|
expect(parsed, "it reads back");
|
||||||
|
if parsed {
|
||||||
|
expect(m.rendered_kind == .NONE && ends_with(m.original_name, ".png"), "the original is a PNG: %", m.original_name);
|
||||||
|
image, decoded := image_decode(m.original);
|
||||||
|
expect(decoded && image.width == WIDTH && image.height == HEIGHT, "at its own size");
|
||||||
|
if decoded {
|
||||||
|
expect(memcmp(image.pixels, shot.pixels.data, shot.pixels.count) == 0, "every pixel as it was");
|
||||||
|
image_free(*image);
|
||||||
|
}
|
||||||
|
expect(max(m.preview.width, m.preview.height) == MCAP_PREVIEW_MAX_SIDE, "a preview: % x %", m.preview.width, m.preview.height);
|
||||||
|
|
||||||
|
text :: (object: Capture_Value, key: string) -> string {
|
||||||
|
value := capture_object_find(*object, key);
|
||||||
|
return ifx value && value.kind == .STRING then value.text else "";
|
||||||
|
}
|
||||||
|
number :: (object: Capture_Value, key: string) -> float64 {
|
||||||
|
value := capture_object_find(*object, key);
|
||||||
|
return ifx value && value.kind == .NUMBER then value.number else 0;
|
||||||
|
}
|
||||||
|
doc: Capture_Document;
|
||||||
|
capture_document_init(*doc, temp);
|
||||||
|
expect(capture_text_parse(*doc, path, m.text), "its Capture Text reads");
|
||||||
|
metadata := capture_find_file(*doc, "metadata.json");
|
||||||
|
capture := capture_find_file(*doc, "capture.json");
|
||||||
|
expect(metadata && capture, "metadata.json and capture.json");
|
||||||
|
if metadata {
|
||||||
|
expect(text(metadata.root, "WindowName") == "Assign Footprints - KiCad", "WindowName: %", text(metadata.root, "WindowName"));
|
||||||
|
expect(text(metadata.root, "AppName") == "KiCad", "AppName");
|
||||||
|
expect(text(metadata.root, "CaptureDate") == "2026-10-10 14:05:09", "CaptureDate");
|
||||||
|
}
|
||||||
|
if capture {
|
||||||
|
expect(number(capture.root, "Left") == -1920 && number(capture.root, "Top") == 40, "where on the desktop");
|
||||||
|
expect(text(capture.root, "Region") == "selection", "a selection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_delete(path);
|
||||||
|
file_delete(again);
|
||||||
|
}
|
||||||
|
|
||||||
// What mcap_chunks_size counts, here where it cannot be seen.
|
// What mcap_chunks_size counts, here where it cannot be seen.
|
||||||
mcap_chunks_size_for_test :: (chunks: [] Mcap_Chunk) -> s64 {
|
mcap_chunks_size_for_test :: (chunks: [] Mcap_Chunk) -> s64 {
|
||||||
size: s64 = 4;
|
size: s64 = 4;
|
||||||
@ -623,5 +693,6 @@ run_import_tests :: (builder: *String_Builder = null) -> bool {
|
|||||||
segment_chunk_tests();
|
segment_chunk_tests();
|
||||||
library_folders_tests();
|
library_folders_tests();
|
||||||
segment_overlay_tests();
|
segment_overlay_tests();
|
||||||
|
screen_capture_tests();
|
||||||
return report();
|
return report();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -308,12 +308,15 @@ hotkeys_missing_lines :: (actions: [] Hotkey_Action, bindings: [] Hotkey_Binding
|
|||||||
// Registered with the OS, so they fire from any app: RegisterHotKey on Windows, Carbon's
|
// Registered with the OS, so they fire from any app: RegisterHotKey on Windows, Carbon's
|
||||||
// RegisterEventHotKey on macOS, none on Linux. They are never matched against ImGui's key state.
|
// RegisterEventHotKey on macOS, none on Linux. They are never matched against ImGui's key state.
|
||||||
bring_to_foreground_hotkey: Hotkey;
|
bring_to_foreground_hotkey: Hotkey;
|
||||||
|
capture_region_hotkey: Hotkey;
|
||||||
|
|
||||||
GLOBAL_HOTKEY_BRING_TO_FOREGROUND :: 0; // indices into global_hotkey_actions
|
GLOBAL_HOTKEY_BRING_TO_FOREGROUND :: 0; // indices into global_hotkey_actions
|
||||||
GLOBAL_HOTKEY_ACTION_COUNT :: 1; // sizes the platforms' registration arrays
|
GLOBAL_HOTKEY_CAPTURE_REGION :: 1;
|
||||||
|
GLOBAL_HOTKEY_ACTION_COUNT :: 2; // sizes the platforms' registration arrays
|
||||||
|
|
||||||
global_hotkey_actions_build :: (out: *[..] Hotkey_Action) {
|
global_hotkey_actions_build :: (out: *[..] Hotkey_Action) {
|
||||||
array_add(out, .{"bring_to_foreground", *bring_to_foreground_hotkey, false});
|
array_add(out, .{"bring_to_foreground", *bring_to_foreground_hotkey, false});
|
||||||
|
array_add(out, .{"capture_region", *capture_region_hotkey, true}); // app/screen_capture.jai
|
||||||
}
|
}
|
||||||
|
|
||||||
global_hotkey_actions: [..] Hotkey_Action;
|
global_hotkey_actions: [..] Hotkey_Action;
|
||||||
@ -369,6 +372,7 @@ global_hotkeys_install :: (bindings: [] Hotkey_Binding) {
|
|||||||
for global_hotkey_actions it.hotkey.binding_count = 0;
|
for global_hotkey_actions it.hotkey.binding_count = 0;
|
||||||
for bindings {
|
for bindings {
|
||||||
if it.explicit_none continue;
|
if it.explicit_none continue;
|
||||||
|
if OS != .WINDOWS && global_hotkey_actions[it.action].windows_only continue;
|
||||||
hotkey := global_hotkey_actions[it.action].hotkey;
|
hotkey := global_hotkey_actions[it.action].hotkey;
|
||||||
if hotkey.binding_count == MAX_HOTKEY_BINDINGS {
|
if hotkey.binding_count == MAX_HOTKEY_BINDINGS {
|
||||||
log("[settings] % already has % bindings; ignoring %.", global_hotkey_actions[it.action].name, MAX_HOTKEY_BINDINGS, to_string(it.combo));
|
log("[settings] % already has % bindings; ignoring %.", global_hotkey_actions[it.action].name, MAX_HOTKEY_BINDINGS, to_string(it.combo));
|
||||||
@ -403,6 +407,7 @@ global_hotkey_fired :: (id: s64) #c_call {
|
|||||||
if id < 1 return;
|
if id < 1 return;
|
||||||
action := (id - 1) / MAX_HOTKEY_BINDINGS;
|
action := (id - 1) / MAX_HOTKEY_BINDINGS;
|
||||||
if action == GLOBAL_HOTKEY_BRING_TO_FOREGROUND app.restore_window_this_frame = true;
|
if action == GLOBAL_HOTKEY_BRING_TO_FOREGROUND app.restore_window_this_frame = true;
|
||||||
|
if action == GLOBAL_HOTKEY_CAPTURE_REGION app.capture_region_this_frame = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses the lines of section in data, or every line when data has no such header. Returns the
|
// Parses the lines of section in data, or every line when data has no such header. Returns the
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user