Compare commits

..

3 Commits

Author SHA1 Message Date
e12622700d [0.1.20] Ctrl-Shift-M brings mcap to the foreground by default
bring_to_foreground in [global_hotkeys] was "none"; new settings files get Ctrl-Shift-M. A file
that already says "none" keeps it. macOS stays unbound.
2026-10-14 18:54:01 +01:00
c4203cc8cf [0.1.19] Install a new hotkey's default when it is added to settings.config
hotkeys_missing_lines writes the shipped default of an action the file does not mention, but that
save records the hash of what it wrote, so the file was not reloaded and the action stayed unbound
until the file next changed. The bindings installed at startup and on reload now include those
defaults (hotkeys_with_missing_defaults), as in Ex4 1.47.2; a file of an older version still gets
"none". A logger test covers it.
2026-10-14 18:50:34 +01:00
120eba1e23 [0.1.18] Ctrl-F puts the keyboard in the search box
The search box takes the keyboard on the next frame, its window brought to the front and its text
selected, so typing replaces the query. It is focus_search in [hotkeys] (Cmd-F on macOS), and it
acts while typing in another field, such as the Import window's. View mode has no search box.
2026-10-14 18:50:34 +01:00
10 changed files with 81 additions and 11 deletions

View File

@ -63,6 +63,7 @@ see [Screen capture](#screen-capture).
frame), and **Ctrl-Shift-C** its full path. Neither acts while you type in the search box. They 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 are `copy_image` and `copy_path` in `[hotkeys]` in `settings.config`. Copying images is Windows
only so far. only so far.
- **Ctrl-F** puts the keyboard in the search box, its text selected (`focus_search` in `[hotkeys]`).
- **Files added or deleted outside the app** (in Explorer, by another program) appear in the grid - **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. 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. **Refresh** looks at the whole folder again, should the watcher have missed something.

View File

@ -5,7 +5,7 @@
# Delete and PrintScreen. 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 Ctrl-Shift-M bring_to_foreground
# Freezes every screen to drag a region over and capture it, into the clipboard and the library. # 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 # PrintScreen is Snagit's key too: only one program can have it, so quit Snagit or choose another

View File

@ -22,6 +22,9 @@ Ctrl-Shift-Alt-R reset_layout
Ctrl-C copy_image Ctrl-C copy_image
Ctrl-Shift-C copy_path Ctrl-Shift-C copy_path
# Library
Ctrl-F focus_search
# Debug # Debug
Ctrl-Shift-D toggle_debug_panel Ctrl-Shift-D toggle_debug_panel
Ctrl-Shift-L toggle_logger_panel Ctrl-Shift-L toggle_logger_panel

View File

@ -22,6 +22,9 @@ Cmd-Shift-Opt-R reset_layout
Cmd-C copy_image Cmd-C copy_image
Cmd-Shift-C copy_path Cmd-Shift-C copy_path
# Library
Cmd-F focus_search
# Debug # Debug
Cmd-Shift-D toggle_debug_panel Cmd-Shift-D toggle_debug_panel
Cmd-Shift-L toggle_logger_panel Cmd-Shift-L toggle_logger_panel

View File

@ -47,7 +47,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.17"; VERSION :: "0.1.20";
BUILD_DATE :: "-"; BUILD_DATE :: "-";
DEBUG :: true; DEBUG :: true;
COMPILE_TESTS :: true; COMPILE_TESTS :: true;

View File

@ -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.17"; VERSION :: "0.1.20";
// 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";

View File

@ -134,6 +134,7 @@ viewer: struct {
visible_generation: s64; // counts rebuilds of visible visible_generation: s64; // counts rebuilds of visible
sorted_by_date: bool; sorted_by_date: bool;
search_buffer: [256] u8; search_buffer: [256] u8;
focus_search: bool; // Ctrl-F (focus_search in [hotkeys]): the search box takes the keyboard next frame
search_terms: [..] Search_Term; search_terms: [..] Search_Term;
folders: [..] string; // the library's folders holding captures, sorted; "" for its top folders: [..] string; // the library's folders holding captures, sorted; "" for its top
folder_filter: string; // the folder the grid shows; "" for all (heap) folder_filter: string; // the folder the grid shows; "" for all (heap)
@ -1059,6 +1060,11 @@ viewer_ui :: () { using ImGui;
return; return;
} }
if viewer.focus_search {
viewer.focus_search = false;
SetWindowFocus(); // SetKeyboardFocusHere alone leaves another window in front
SetKeyboardFocusHere(); // tabbing in: its text is selected, so typing replaces the query
}
SetNextItemWidth(min(GetContentRegionAvail().x * 0.5, 520)); SetNextItemWidth(min(GetContentRegionAvail().x * 0.5, 520));
if InputTextWithHint("##search", "Search text, titles, URLs, apps, notes, labels. \"a phrase\" ocr: title: url: app: note: label: like: folder: has:", viewer.search_buffer.data, viewer.search_buffer.count) { if InputTextWithHint("##search", "Search text, titles, URLs, apps, notes, labels. \"a phrase\" ocr: title: url: app: note: label: like: folder: has:", viewer.search_buffer.data, viewer.search_buffer.count) {
viewer_search_changed(); viewer_search_changed();

View File

@ -229,6 +229,38 @@ settings_tests :: () {
defer settings_free(*settings); defer settings_free(*settings);
expect(ok && !seen.event_log && settings.event_log_capacity == EVENT_LOG_CAPACITY_DEFAULT, expect(ok && !seen.event_log && settings.event_log_capacity == EVENT_LOG_CAPACITY_DEFAULT,
"a file without the key gave seen % and capacity %", seen.event_log, settings.event_log_capacity); "a file without the key gave seen % and capacity %", seen.event_log, settings.event_log_capacity);
// A section that does not mention an action gets its default installed right away, since the save
// that writes it into the file does not reload it; a pre-upgrade file gets "none" instead.
{
group("hotkeys missing from [hotkeys]");
hotkey_actions_init();
defaults := hotkey_defaults();
bindings: [..] Hotkey_Binding;
bindings.allocator = temp;
errors := hotkeys_parse_section("logger_tests", "[hotkeys]\nCtrl-Shift-Q program_exit\nnone minimize_to_tray\n", hotkey_actions, *bindings);
expect(errors == 0 && bindings.count == 2, "the partial section gave % bindings and % errors", bindings.count, errors);
exit_action := hotkey_find_action(hotkey_actions, "program_exit");
minimize := hotkey_find_action(hotkey_actions, "minimize_to_tray");
focus_search := hotkey_find_action(hotkey_actions, "focus_search");
installed := hotkeys_with_missing_defaults(hotkey_actions, bindings, defaults, APP_USER_SETTINGS_VERSION);
count_for :: (bindings: [] Hotkey_Binding, action: s32) -> int {
count := 0;
for bindings if it.action == action count += 1;
return count;
}
expect(count_for(installed, exit_action) == 1, "program_exit has % bindings, not just its own 1", count_for(installed, exit_action));
expect(count_for(installed, minimize) == 1, "minimize_to_tray's \"none\" was joined by % more", count_for(installed, minimize) - 1);
expect(count_for(installed, focus_search) == count_for(defaults, focus_search) && count_for(defaults, focus_search) > 0,
"focus_search got % of its % default bindings", count_for(installed, focus_search), count_for(defaults, focus_search));
missing, _ := hotkeys_missing_lines(hotkey_actions, installed, defaults, APP_USER_SETTINGS_VERSION);
expect(missing == "", "with the defaults added, these are still missing:%", missing);
old := hotkeys_with_missing_defaults(hotkey_actions, bindings, defaults, APP_USER_SETTINGS_VERSION - 1);
expect(old.count == bindings.count, "a pre-upgrade file got % default bindings", old.count - bindings.count);
}
} }
#scope_export #scope_export

View File

@ -167,6 +167,9 @@ reset_layout_hotkey: Hotkey;
copy_image_hotkey: Hotkey = .{ignore_when_text_field_active = true}; copy_image_hotkey: Hotkey = .{ignore_when_text_field_active = true};
copy_path_hotkey: Hotkey = .{ignore_when_text_field_active = true}; copy_path_hotkey: Hotkey = .{ignore_when_text_field_active = true};
// The search box (app/viewer.jai). Also while typing elsewhere, as in the Import window's fields.
focus_search_hotkey: Hotkey;
Hotkey_Action :: struct { Hotkey_Action :: struct {
name: string; // as written in [hotkeys] name: string; // as written in [hotkeys]
hotkey: *Hotkey; hotkey: *Hotkey;
@ -189,6 +192,7 @@ hotkey_actions_build :: (out: *[..] Hotkey_Action) {
add(out, "reset_layout", *reset_layout_hotkey); add(out, "reset_layout", *reset_layout_hotkey);
add(out, "copy_image", *copy_image_hotkey); add(out, "copy_image", *copy_image_hotkey);
add(out, "copy_path", *copy_path_hotkey); add(out, "copy_path", *copy_path_hotkey);
add(out, "focus_search", *focus_search_hotkey);
} }
hotkey_actions: [..] Hotkey_Action; hotkey_actions: [..] Hotkey_Action;
@ -289,9 +293,7 @@ hotkeys_missing_lines :: (actions: [] Hotkey_Action, bindings: [] Hotkey_Binding
new_active_count := 0; new_active_count := 0;
for action, action_index: actions { for action, action_index: actions {
mentioned := false; if hotkeys_mention_action(bindings, action_index) continue;
for bindings if it.action == action_index { mentioned = true; break; }
if mentioned continue;
if !header_written { if !header_written {
append(*builder, "\n# Added automatically; new since this file was last saved.\n"); append(*builder, "\n# Added automatically; new since this file was last saved.\n");
@ -310,6 +312,28 @@ hotkeys_missing_lines :: (actions: [] Hotkey_Action, bindings: [] Hotkey_Binding
return builder_to_string(*builder,, temp), new_active_count; return builder_to_string(*builder,, temp), new_active_count;
} }
// The bindings to install for a section as read: its own, plus the shipped defaults of the actions
// it does not mention, which hotkeys_missing_lines gives the file in the save that follows. That
// save records the hash of what it wrote, so the file is not reloaded, and without these a new
// action would stay unbound until the file next changed. temp.
hotkeys_with_missing_defaults :: (actions: [] Hotkey_Action, bindings: [] Hotkey_Binding, defaults: [] Hotkey_Binding, file_version: s64) -> [] Hotkey_Binding {
if file_version < APP_USER_SETTINGS_VERSION return bindings; // these get "none"
result: [..] Hotkey_Binding;
result.allocator = temp;
array_add(*result, ..bindings);
for action, action_index: actions {
if hotkeys_mention_action(bindings, action_index) continue;
for defaults if it.action == action_index array_add(*result, it);
}
return result;
}
hotkeys_mention_action :: (bindings: [] Hotkey_Binding, action_index: int) -> bool {
for bindings if it.action == action_index return true;
return false;
}
// ---------------------------------------------------------------- the [global_hotkeys] section // ---------------------------------------------------------------- the [global_hotkeys] section
// 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
@ -637,6 +661,7 @@ app_check_key_combinations :: () {
// The viewer is the shutdown thread's once shutdown has started. View mode has the file alone (app/open_file.jai). // The viewer is the shutdown thread's once shutdown has started. View mode has the file alone (app/open_file.jai).
if check_key_combination(*copy_image_hotkey) && !app_exit_shutdown_started() { if app.view_mode open_file_copy_image(); else viewer_copy_image(); } if check_key_combination(*copy_image_hotkey) && !app_exit_shutdown_started() { if app.view_mode open_file_copy_image(); else viewer_copy_image(); }
if check_key_combination(*copy_path_hotkey) && !app_exit_shutdown_started() { if app.view_mode open_file_copy_path(); else viewer_copy_path(); } if check_key_combination(*copy_path_hotkey) && !app_exit_shutdown_started() { if app.view_mode open_file_copy_path(); else viewer_copy_path(); }
if check_key_combination(*focus_search_hotkey) && !app_exit_shutdown_started() && !app.view_mode viewer.focus_search = true; // view mode has no search
if should_exit_this_frame { if should_exit_this_frame {
log("Calling exit hotkey... exiting program!"); log("Calling exit hotkey... exiting program!");

View File

@ -366,11 +366,11 @@ app_load_user_settings_file :: () -> write_back: bool {
bindings: [] Hotkey_Binding = hotkey_defaults(); bindings: [] Hotkey_Binding = hotkey_defaults();
if ok && seen.hotkeys bindings = app_user_settings.hotkey_bindings; if ok && seen.hotkeys bindings = app_user_settings.hotkey_bindings;
hotkeys_install(bindings); hotkeys_install(hotkeys_with_missing_defaults(hotkey_actions, bindings, hotkey_defaults(), version));
global_bindings: [] Hotkey_Binding = global_hotkey_defaults(); global_bindings: [] Hotkey_Binding = global_hotkey_defaults();
if ok && seen.global_hotkeys global_bindings = app_user_settings.global_hotkey_bindings; if ok && seen.global_hotkeys global_bindings = app_user_settings.global_hotkey_bindings;
global_hotkeys_install(global_bindings); global_hotkeys_install(hotkeys_with_missing_defaults(global_hotkey_actions, global_bindings, global_hotkey_defaults(), version));
if !ok return false; if !ok return false;
@ -804,7 +804,7 @@ app_reload_user_settings_if_changed :: () {
settings_file_hash = hash; settings_file_hash = hash;
fresh: App_User_Settings; fresh: App_User_Settings;
ok, seen, _ := settings_parse(path, file_data, *fresh); ok, seen, version := settings_parse(path, file_data, *fresh);
if !ok { if !ok {
settings_free(*fresh); settings_free(*fresh);
log_error("[settings] Could not apply the changes to %; keeping the current settings.", path); log_error("[settings] Could not apply the changes to %; keeping the current settings.", path);
@ -832,11 +832,11 @@ app_reload_user_settings_if_changed :: () {
bindings: [] Hotkey_Binding = hotkey_defaults(); bindings: [] Hotkey_Binding = hotkey_defaults();
if seen.hotkeys bindings = app_user_settings.hotkey_bindings; if seen.hotkeys bindings = app_user_settings.hotkey_bindings;
hotkeys_install(bindings); hotkeys_install(hotkeys_with_missing_defaults(hotkey_actions, bindings, hotkey_defaults(), version));
global_bindings: [] Hotkey_Binding = global_hotkey_defaults(); global_bindings: [] Hotkey_Binding = global_hotkey_defaults();
if seen.global_hotkeys global_bindings = app_user_settings.global_hotkey_bindings; if seen.global_hotkeys global_bindings = app_user_settings.global_hotkey_bindings;
global_hotkeys_install(global_bindings); global_hotkeys_install(hotkeys_with_missing_defaults(global_hotkey_actions, global_bindings, global_hotkey_defaults(), version));
global_hotkeys_reapply(); global_hotkeys_reapply();
settings_note_synced(); settings_note_synced();