[0.1.16] Ctrl-C copies the selected capture's image, Ctrl-Shift-C its full path

The selected capture is the one outlined in the grid and shown in the Capture window. Its image is
copied as the Capture window shows it, decoded on a thread of its own and put on the clipboard as
screen captures are (Windows only so far); its path is copied as the system writes paths. Both are
actions in [hotkeys] (copy_image, copy_path), which do not act while typing in the search box.
Settings files at the current version get their default bindings at the next start; the comment
saying to raise APP_USER_SETTINGS_VERSION for a new action had it backwards.
This commit is contained in:
Musa Mahmood 2026-10-13 07:36:52 +01:00
parent 22cad98500
commit 03f23f7122
7 changed files with 108 additions and 4 deletions

View File

@ -58,6 +58,11 @@ see [Screen capture](#screen-capture).
move the file to the Recycle Bin or to delete it permanently, overwriting it with random bytes
first; "Don't ask again" keeps the choice until the app restarts. Only the `.mcap` goes: the
`.snagx` it was converted from is untouched.
- **Copy:** click a thumbnail to select it (it is outlined, and shown in the Capture window), then
**Ctrl-C** copies its image, as the Capture window shows it (annotations and all; a video's
frame), and **Ctrl-Shift-C** its full path. Neither acts while you type in the search box. They
are `copy_image` and `copy_path` in `[hotkeys]` in `settings.config`. Copying images is Windows
only so far.
- **Files added or deleted outside the app** (in Explorer, by another program) appear in the grid
or leave it on their own, folders added, renamed or deleted too: the library is watched.
**Refresh** looks at the whole folder again, should the watcher have missed something.

View File

@ -18,6 +18,10 @@ Ctrl-Equal increase_font_size
Ctrl-Alt-Space reset_window
Ctrl-Shift-Alt-R reset_layout
# Library: the selected capture
Ctrl-C copy_image
Ctrl-Shift-C copy_path
# Debug
Ctrl-Shift-D toggle_debug_panel
Ctrl-Shift-L toggle_logger_panel

View File

@ -18,6 +18,10 @@ Cmd-Equal increase_font_size
Cmd-Opt-Shift-Space reset_window
Cmd-Shift-Opt-R reset_layout
# Library: the selected capture
Cmd-C copy_image
Cmd-Shift-C copy_path
# Debug
Cmd-Shift-D toggle_debug_panel
Cmd-Shift-L toggle_logger_panel

View File

@ -47,7 +47,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check";
GIT_HASH :: "-";
VERSION :: "0.1.15";
VERSION :: "0.1.16";
BUILD_DATE :: "-";
DEBUG :: true;
COMPILE_TESTS :: true;

View File

@ -8,7 +8,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.1.15";
VERSION :: "0.1.16";
// 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.
build_directory := "bin";

View File

@ -156,6 +156,14 @@ viewer: struct {
open_name: string;
open_result: string; // the file written, or ""; heap, the open thread's until it ends
open_error: string;
// The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image).
copy_thread: Thread;
copy_started: bool;
copy_busy: bool; // set by the main thread, cleared by the copy thread when it ends
copy_source: string; // the .mcap; heap, main thread's until the thread starts
copy_name: string;
copy_done: Decoded; // the copy thread's until it ends
}
// ---------------------------------------------------------------- startup and shutdown
@ -229,6 +237,7 @@ viewer_shutdown :: () {
for 1..viewer.workers_started signal(*viewer.wake);
for 0..viewer.workers_started - 1 thread_is_done(*viewer.workers[it], -1);
if viewer.open_started thread_is_done(*viewer.open_thread, -1);
if viewer.copy_started thread_is_done(*viewer.copy_thread, -1);
background_shutdown();
catalog_cache_save_at_shutdown(); // the workers have stopped: nothing changes the entries now
viewer_watch_stop();
@ -451,6 +460,7 @@ viewer_frame_gpu_work :: () {
viewer_apply_catalog_updates();
catalog_cache_frame(); // saves the catalog cache once it is complete and has changed (app/catalog.jai)
viewer_open_frame();
viewer_copy_frame();
segment_overlay_frame(); // the Capture window's segments: what arrived, and its texture (app/segment_ui.jai)
background_frame(); // takes back search results, and starts a search when visible_dirty (app/background.jai)
}
@ -1215,6 +1225,77 @@ viewer_open_frame :: () {
else toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error);
}
// ---------------------------------------------------------------- copying the selected capture
// The selected capture: the one outlined in the grid and shown in the Capture window. -1 when none.
viewer_selected :: () -> s32 {
index := viewer.full_index;
if index < 0 || index >= viewer.captures.count || viewer.captures[index].removed return -1;
return index;
}
// Ctrl-Shift-C (copy_path in [hotkeys]): the selected capture's path, as the system writes paths.
viewer_copy_path :: () {
index := viewer_selected();
if index < 0 { toast_show(.Error, "Select a capture to copy its path"); return; }
capture := *viewer.captures[index];
path := capture.path;
#if OS == .WINDOWS path = replace(path, "/", "\\",, temp);
clipboard_set_text(path);
toast_show(.Success, "Copied the path of %", capture.name);
}
// Ctrl-C (copy_image in [hotkeys]): the selected capture's image, as the Capture window shows it
// (annotations and all; a video's frame), decoded on a thread of its own and put on the clipboard
// by viewer_copy_frame.
viewer_copy_image :: () {
index := viewer_selected();
if index < 0 { toast_show(.Error, "Select a capture to copy its image"); return; }
capture := *viewer.captures[index];
#if OS != .WINDOWS {
toast_show(.Error, "Copying images is not available on this system yet");
return;
}
if viewer.copy_started { toast_show(.Error, "Still copying %", viewer.copy_name); return; }
push_allocator(context.default_allocator);
free(viewer.copy_source); free(viewer.copy_name);
viewer.copy_source = copy_string(capture.path);
viewer.copy_name = copy_string(capture.name);
viewer.copy_done = .{};
viewer.copy_busy = true;
// Reading an image takes a few MB of temporary storage, as on the viewer's workers.
viewer.copy_started = thread_init(*viewer.copy_thread, viewer_copy_proc, temporary_storage_size = 1024 * 1024, name = "copy image");
if !viewer.copy_started { viewer.copy_busy = false; toast_show(.Error, "Could not copy %", capture.name); return; }
thread_start(*viewer.copy_thread);
}
viewer_copy_proc :: (thread: *Thread) -> s64 {
context.allocator = context.default_allocator;
defer atomic_swap(*viewer.copy_busy, false);
viewer.copy_done = viewer_decode_full(viewer.copy_source);
return 0;
}
// Each frame: puts what viewer_copy_proc decoded on the clipboard, once it is done.
viewer_copy_frame :: () {
if !viewer.copy_started || viewer.copy_busy return;
thread_is_done(*viewer.copy_thread, -1);
thread_deinit(*viewer.copy_thread);
viewer.copy_started = false;
done := *viewer.copy_done;
defer viewer_free_pixels(done);
if done.failed { toast_show(.Error, "Could not read the image of %; see the Event Log", viewer.copy_name); return; }
pixels := []u8.{cast(s64) done.width * done.height * 4, done.pixels};
if !platform_copy_image_to_clipboard(pixels, done.width, done.height) {
log_error("[viewer] Could not put the image of % on the clipboard.", viewer.copy_source);
toast_show(.Error, "Could not put % on the clipboard", viewer.copy_name);
return;
}
scaled := ifx done.width != done.source_width || done.height != done.source_height then tprint(", scaled down from % x %", done.source_width, done.source_height) else "";
log("[viewer] Copied the image of % (% x %%).", viewer.copy_source, done.width, done.height, scaled);
toast_show(.Success, "Copied % (% x %%)", viewer.copy_name, done.width, done.height, scaled);
}
viewer_grid :: () { using ImGui;
style := GetStyle();
cell := viewer.thumb_size;

View File

@ -148,8 +148,9 @@ to_string_all_bindings :: (hotkey: Hotkey) -> string {
// The bindings are read from settings.config, or from DEFAULT_HOTKEYS_DATA when the file has
// no [hotkeys] section; see hotkeys_install. once and ignore_when_text_field_active belong to the
// action and are set here. To add an action: declare its Hotkey here, add it to
// hotkey_actions_build and app_check_key_combinations, bind it in both assets/data/default_hotkeys
// files, and raise APP_USER_SETTINGS_VERSION so existing settings files get its default.
// hotkey_actions_build and app_check_key_combinations, and bind it in both
// assets/data/default_hotkeys files. Settings files at APP_USER_SETTINGS_VERSION get its default
// binding at the next start (hotkeys_missing_lines); raising the version would give them "none".
program_exit_hotkey: Hotkey;
minimize_to_tray_hotkey: Hotkey;
toggle_debug_panel_hotkey: Hotkey;
@ -162,6 +163,10 @@ increase_font_size_hotkey: Hotkey;
reset_window_hotkey: Hotkey;
reset_layout_hotkey: Hotkey;
// The selected capture (app/viewer.jai). Not while typing, so Ctrl-C in the search box copies its text.
copy_image_hotkey: Hotkey = .{ignore_when_text_field_active = true};
copy_path_hotkey: Hotkey = .{ignore_when_text_field_active = true};
Hotkey_Action :: struct {
name: string; // as written in [hotkeys]
hotkey: *Hotkey;
@ -182,6 +187,8 @@ hotkey_actions_build :: (out: *[..] Hotkey_Action) {
add(out, "increase_font_size", *increase_font_size_hotkey);
add(out, "reset_window", *reset_window_hotkey);
add(out, "reset_layout", *reset_layout_hotkey);
add(out, "copy_image", *copy_image_hotkey);
add(out, "copy_path", *copy_path_hotkey);
}
hotkey_actions: [..] Hotkey_Action;
@ -627,6 +634,9 @@ app_check_key_combinations :: () {
else toast_show(.Error, "Could not reset the window");
}
if check_key_combination(*reset_layout_hotkey) imgui_reset_layout_requested = true; // applied between frames
// The viewer is the shutdown thread's once shutdown has started.
if check_key_combination(*copy_image_hotkey) && !app_exit_shutdown_started() viewer_copy_image();
if check_key_combination(*copy_path_hotkey) && !app_exit_shutdown_started() viewer_copy_path();
if should_exit_this_frame {
log("Calling exit hotkey... exiting program!");