Compare commits
3 Commits
e12622700d
...
c351ca92c0
| Author | SHA1 | Date | |
|---|---|---|---|
| c351ca92c0 | |||
| c5ef13f146 | |||
| 7cee21122c |
@ -272,7 +272,10 @@ What becomes of each file:
|
|||||||
The date is the phone's (Apple's creation date), else one in its name, else the video's own
|
The date is the phone's (Apple's creation date), else one in its name, else the video's own
|
||||||
creation time, else the file's time; the phone and its location are kept too. The thumbnail shows a
|
creation time, else the file's time; the phone and its location are kept too. The thumbnail shows a
|
||||||
play mark and how long it runs; **Play in the default player** in the Capture window writes the
|
play mark and how long it runs; **Play in the default player** in the Capture window writes the
|
||||||
video out to the temporary folder and opens it.
|
video out to the temporary folder and opens it. **Export...** (any capture) writes the file as it
|
||||||
|
was imported to a folder it asks for: one of the `folder:` lines under `[export]` in the settings
|
||||||
|
file (the first is the default; none: the Downloads folder), or **Custom...**, which can be added to
|
||||||
|
them. A file of that name already there, and different, is not overwritten.
|
||||||
- **Snagit `.snagx`:** the same `.mcap` as `snagit-converter mcap` makes, byte for byte, named as
|
- **Snagit `.snagx`:** the same `.mcap` as `snagit-converter mcap` makes, byte for byte, named as
|
||||||
the `.snagx` is.
|
the `.snagx` is.
|
||||||
- **Duplicates:** an image already in the library, in any of its folders, is not stored again; the
|
- **Duplicates:** an image already in the library, in any of its folders, is not stored again; the
|
||||||
|
|||||||
@ -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.20";
|
VERSION :: "0.1.23";
|
||||||
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.20";
|
VERSION :: "0.1.23";
|
||||||
// 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";
|
||||||
|
|||||||
@ -53,6 +53,7 @@ ocr_ui: struct {
|
|||||||
show_boxes := true;
|
show_boxes := true;
|
||||||
match_cursor: s64 = -1; // which match Next and Previous last went to
|
match_cursor: s64 = -1; // which match Next and Previous last went to
|
||||||
scroll_to_match: bool;
|
scroll_to_match: bool;
|
||||||
|
menu_text: string; // heap: the line right-clicked, for its menu's Copy
|
||||||
|
|
||||||
window_open := false;
|
window_open := false;
|
||||||
toast_shown := false;
|
toast_shown := false;
|
||||||
@ -823,6 +824,8 @@ ocr_ui_frame :: () { using ImGui;
|
|||||||
if section && viewer.full_index >= 0 {
|
if section && viewer.full_index >= 0 {
|
||||||
Separator();
|
Separator();
|
||||||
Text("Text of % (% , % lines):", viewer.captures[viewer.full_index].name, section.model, section.lines.count);
|
Text("Text of % (% , % lines):", viewer.captures[viewer.full_index].name, section.model, section.lines.count);
|
||||||
|
SameLine();
|
||||||
|
if SmallButton("Copy all text") ocr_copy_all_text(section);
|
||||||
BeginChild("ocr text");
|
BeginChild("ocr text");
|
||||||
for section.lines TextUnformatted(it.text);
|
for section.lines TextUnformatted(it.text);
|
||||||
EndChild();
|
EndChild();
|
||||||
@ -849,6 +852,17 @@ text_with_links :: (text: string) { using ImGui;
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- the Capture window
|
// ---------------------------------------------------------------- the Capture window
|
||||||
|
|
||||||
|
// Puts the reading on the clipboard, a line of text to each of its lines.
|
||||||
|
ocr_copy_all_text :: (section: *Mcap_Ocr_Section) {
|
||||||
|
builder: String_Builder;
|
||||||
|
builder.allocator = temp;
|
||||||
|
for section.lines {
|
||||||
|
if it_index append(*builder, "\n");
|
||||||
|
append(*builder, it.text);
|
||||||
|
}
|
||||||
|
clipboard_set_text(builder_to_string(*builder,, temp));
|
||||||
|
}
|
||||||
|
|
||||||
// The reading shown over the capture in the Capture window: the overlay's choice, else the chosen
|
// The reading shown over the capture in the Capture window: the overlay's choice, else the chosen
|
||||||
// model's, else the first there is.
|
// model's, else the first there is.
|
||||||
ocr_overlay_section :: () -> *Mcap_Ocr_Section {
|
ocr_overlay_section :: () -> *Mcap_Ocr_Section {
|
||||||
@ -910,6 +924,8 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui;
|
|||||||
SameLine();
|
SameLine();
|
||||||
}
|
}
|
||||||
Checkbox("Boxes", *ocr_ui.show_boxes);
|
Checkbox("Boxes", *ocr_ui.show_boxes);
|
||||||
|
SameLine();
|
||||||
|
if SmallButton("Copy all text") ocr_copy_all_text(section);
|
||||||
if viewer.search_terms.count {
|
if viewer.search_terms.count {
|
||||||
matches := 0;
|
matches := 0;
|
||||||
for section.lines if search_line_matches(it.text, viewer.search_terms) matches += 1;
|
for section.lines if search_line_matches(it.text, viewer.search_terms) matches += 1;
|
||||||
@ -943,6 +959,7 @@ ocr_ui_capture_overlay :: (index: s32, image_min: ImGui.ImVec2, zoom: float) { u
|
|||||||
plain := GetColorU32(.PlotHistogram, ifx searching then 0.35 else 1.0);
|
plain := GetColorU32(.PlotHistogram, ifx searching then 0.35 else 1.0);
|
||||||
matching := GetColorU32(.PlotLinesHovered);
|
matching := GetColorU32(.PlotLinesHovered);
|
||||||
match_number := 0;
|
match_number := 0;
|
||||||
|
clicked: string; // the line right-clicked this frame; the last drawn, which is on top, where they overlap
|
||||||
for section.lines {
|
for section.lines {
|
||||||
p0 := ImVec2.{image_min.x + it.x * scale_x, image_min.y + it.y * scale_y};
|
p0 := ImVec2.{image_min.x + it.x * scale_x, image_min.y + it.y * scale_y};
|
||||||
p1 := ImVec2.{p0.x + it.width * scale_x, p0.y + it.height * scale_y};
|
p1 := ImVec2.{p0.x + it.width * scale_x, p0.y + it.height * scale_y};
|
||||||
@ -963,6 +980,21 @@ ocr_ui_capture_overlay :: (index: s32, image_min: ImGui.ImVec2, zoom: float) { u
|
|||||||
} else {
|
} else {
|
||||||
draw.AddQuad(draw, q[0], q[1], q[2], q[3], plain, 1.5);
|
draw.AddQuad(draw, q[0], q[1], q[2], q[3], plain, 1.5);
|
||||||
}
|
}
|
||||||
if IsMouseHoveringRect(p0, p1) SetTooltip("% (%)", it.text, FF(it.confidence, 2));
|
if IsMouseHoveringRect(p0, p1) {
|
||||||
|
SetTooltip("% (%)", it.text, FF(it.confidence, 2));
|
||||||
|
if IsWindowHovered() && IsMouseClicked(.Right) clicked = it.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click a box for its menu. The text is kept, since the reading may change while the menu is up.
|
||||||
|
MENU :: "##ocr_line_menu";
|
||||||
|
if clicked {
|
||||||
|
free(ocr_ui.menu_text,, context.default_allocator);
|
||||||
|
ocr_ui.menu_text = copy_string(clicked,, context.default_allocator);
|
||||||
|
OpenPopup(MENU);
|
||||||
|
}
|
||||||
|
if BeginPopup(MENU) {
|
||||||
|
if MenuItem("Copy") clipboard_set_text(ocr_ui.menu_text);
|
||||||
|
EndPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,15 +149,24 @@ viewer: struct {
|
|||||||
full_fit := Viewer_Fit.WINDOW;
|
full_fit := Viewer_Fit.WINDOW;
|
||||||
full_zoom: float = 100; // percent of the size it was captured at; set by the fit, unless OFF
|
full_zoom: float = 100; // percent of the size it was captured at; set by the fit, unless OFF
|
||||||
|
|
||||||
// A video or animation's original, written out to open in the default player (viewer_open_original).
|
// A capture's original, written out to open in the default player (viewer_open_original), or
|
||||||
|
// exported to a folder (viewer_export_original).
|
||||||
open_thread: Thread;
|
open_thread: Thread;
|
||||||
open_started: bool;
|
open_started: bool;
|
||||||
open_busy: bool; // set by the main thread, cleared by the open thread when it ends
|
open_busy: bool; // set by the main thread, cleared by the open thread when it ends
|
||||||
open_source: string; // the .mcap; heap, main thread's until the thread starts
|
open_source: string; // the .mcap; heap, main thread's until the thread starts
|
||||||
open_name: string;
|
open_name: string;
|
||||||
|
open_folder: string; // where to export it; "" to write it to the temporary folder and open it
|
||||||
open_result: string; // the file written, or ""; heap, the open thread's until it ends
|
open_result: string; // the file written, or ""; heap, the open thread's until it ends
|
||||||
open_error: string;
|
open_error: string;
|
||||||
|
|
||||||
|
// The export being asked about (viewer_export_popup).
|
||||||
|
export_index: s32 = -1; // the capture to export, while the popup is up
|
||||||
|
export_choice: s64; // which of viewer_export_choices, or -1 for Custom...
|
||||||
|
export_buffer: [1024] u8; // the folder typed in for Custom...
|
||||||
|
export_add: bool; // Custom...: add the folder to [export]
|
||||||
|
export_make_default: bool;
|
||||||
|
|
||||||
// The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image).
|
// The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image).
|
||||||
copy_thread: Thread;
|
copy_thread: Thread;
|
||||||
copy_started: bool;
|
copy_started: bool;
|
||||||
@ -1135,6 +1144,7 @@ viewer_ui :: () { using ImGui;
|
|||||||
viewer_grid();
|
viewer_grid();
|
||||||
EndChild();
|
EndChild();
|
||||||
viewer_delete_popup();
|
viewer_delete_popup();
|
||||||
|
viewer_export_popup();
|
||||||
|
|
||||||
viewer_full_window();
|
viewer_full_window();
|
||||||
}
|
}
|
||||||
@ -1197,17 +1207,28 @@ viewer_duration_text :: (seconds: float64) -> string {
|
|||||||
// Writes the capture's original (a video, an animated GIF) as it was imported to a file in the
|
// Writes the capture's original (a video, an animated GIF) as it was imported to a file in the
|
||||||
// temporary folder, on a thread of its own, as it may be large; viewer_open_frame opens it.
|
// temporary folder, on a thread of its own, as it may be large; viewer_open_frame opens it.
|
||||||
viewer_open_original :: (index: s32) {
|
viewer_open_original :: (index: s32) {
|
||||||
|
viewer_write_original(index, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the capture's original as it was imported into folder, on a thread of its own, as it may be
|
||||||
|
// large; viewer_open_frame says where it went.
|
||||||
|
viewer_export_original :: (index: s32, folder: string) {
|
||||||
|
viewer_write_original(index, folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewer_write_original :: (index: s32, folder: string) {
|
||||||
if viewer.open_started return;
|
if viewer.open_started return;
|
||||||
capture := *viewer.captures[index];
|
capture := *viewer.captures[index];
|
||||||
push_allocator(context.default_allocator);
|
push_allocator(context.default_allocator);
|
||||||
free(viewer.open_source); free(viewer.open_name); free(viewer.open_result); free(viewer.open_error);
|
free(viewer.open_source); free(viewer.open_name); free(viewer.open_folder); free(viewer.open_result); free(viewer.open_error);
|
||||||
viewer.open_source = copy_string(capture.path);
|
viewer.open_source = copy_string(capture.path);
|
||||||
viewer.open_name = copy_string(capture.name);
|
viewer.open_name = copy_string(capture.name);
|
||||||
|
viewer.open_folder = copy_string(folder);
|
||||||
viewer.open_result = "";
|
viewer.open_result = "";
|
||||||
viewer.open_error = "";
|
viewer.open_error = "";
|
||||||
viewer.open_busy = true;
|
viewer.open_busy = true;
|
||||||
viewer.open_started = thread_init(*viewer.open_thread, viewer_open_proc, temporary_storage_size = 256 * 1024, name = "open original");
|
viewer.open_started = thread_init(*viewer.open_thread, viewer_open_proc, temporary_storage_size = 256 * 1024, name = "write original");
|
||||||
if !viewer.open_started { viewer.open_busy = false; toast_show(.Error, "Could not open %", capture.name); return; }
|
if !viewer.open_started { viewer.open_busy = false; toast_show(.Error, "Could not write % out", capture.name); return; }
|
||||||
thread_start(*viewer.open_thread);
|
thread_start(*viewer.open_thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1217,14 +1238,28 @@ viewer_open_proc :: (thread: *Thread) -> s64 {
|
|||||||
m, original, ok, error := mcap_read_without_original(viewer.open_source,, temp);
|
m, original, ok, error := mcap_read_without_original(viewer.open_source,, temp);
|
||||||
if !ok { viewer.open_error = copy_string(error); return 0; }
|
if !ok { viewer.open_error = copy_string(error); return 0; }
|
||||||
|
|
||||||
#if OS == .WINDOWS base := windows_expand_environment("%TEMP%");
|
exporting := viewer.open_folder.count > 0;
|
||||||
else base := "/tmp";
|
folder := viewer.open_folder;
|
||||||
folder := tprint("%/%", base, APP_FOLDER_NAME);
|
if !exporting {
|
||||||
|
#if OS == .WINDOWS base := windows_expand_environment("%TEMP%");
|
||||||
|
else base := "/tmp";
|
||||||
|
folder = tprint("%/%", base, APP_FOLDER_NAME);
|
||||||
|
}
|
||||||
make_directory_if_it_does_not_exist(folder, recursive = true);
|
make_directory_if_it_does_not_exist(folder, recursive = true);
|
||||||
|
if !is_directory(folder) { viewer.open_error = copy_string(tprint("% is not a folder that can be written to", folder)); return 0; }
|
||||||
extension := path_extension(m.original_name);
|
extension := path_extension(m.original_name);
|
||||||
path := tprint("%/%.%", folder, viewer.open_name, ifx extension then extension else "bin");
|
if !extension extension = "bin";
|
||||||
|
path := tprint("%/%.%", folder, viewer.open_name, extension);
|
||||||
_, size, exists := file_modtime_and_size(path);
|
_, size, exists := file_modtime_and_size(path);
|
||||||
if !exists || size != original.size { // written out before, it is used again
|
// Written out before, it is used again. An export does not overwrite another file of that name.
|
||||||
|
if exporting && exists && size != original.size {
|
||||||
|
for 2..1000 {
|
||||||
|
path = tprint("%/% (%).%", folder, viewer.open_name, it, extension);
|
||||||
|
_, size, exists = file_modtime_and_size(path);
|
||||||
|
if !exists || size == original.size break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists || size != original.size {
|
||||||
ok, error = mcap_extract_original(original, path);
|
ok, error = mcap_extract_original(original, path);
|
||||||
if !ok { viewer.open_error = copy_string(error); return 0; }
|
if !ok { viewer.open_error = copy_string(error); return 0; }
|
||||||
}
|
}
|
||||||
@ -1232,14 +1267,141 @@ viewer_open_proc :: (thread: *Thread) -> s64 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each frame: opens what viewer_open_proc wrote, once it is done.
|
// Each frame: opens what viewer_open_proc wrote, or says where it was exported, once it is done.
|
||||||
viewer_open_frame :: () {
|
viewer_open_frame :: () {
|
||||||
if !viewer.open_started || viewer.open_busy return;
|
if !viewer.open_started || viewer.open_busy return;
|
||||||
thread_is_done(*viewer.open_thread, -1);
|
thread_is_done(*viewer.open_thread, -1);
|
||||||
thread_deinit(*viewer.open_thread);
|
thread_deinit(*viewer.open_thread);
|
||||||
viewer.open_started = false;
|
viewer.open_started = false;
|
||||||
if viewer.open_result platform_open_file_in_default_program(viewer.open_result, path_strip_filename(viewer.open_result));
|
if !viewer.open_result toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error);
|
||||||
else toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error);
|
else if viewer.open_folder toast_show(.Success, "Exported % to %", viewer.open_name, viewer.open_result);
|
||||||
|
else platform_open_file_in_default_program(viewer.open_result, path_strip_filename(viewer.open_result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The folders Export offers: the [export] folder lines in settings.config, else the Downloads folder.
|
||||||
|
viewer_export_choices :: () -> [] string {
|
||||||
|
if app_user_settings.export_folders return app_user_settings.export_folders;
|
||||||
|
#if OS == .WINDOWS downloads := tprint("%/Downloads", windows_expand_environment("%USERPROFILE%"));
|
||||||
|
else downloads := posix_expand_home("~/Downloads");
|
||||||
|
choices := NewArray(1, string,, temp);
|
||||||
|
choices[0] = downloads;
|
||||||
|
return choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asks where to export the capture's original, starting from the default folder.
|
||||||
|
viewer_export_ask :: (index: s32) {
|
||||||
|
viewer.export_index = index;
|
||||||
|
viewer.export_choice = 0;
|
||||||
|
viewer.export_add = false;
|
||||||
|
viewer.export_make_default = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewer_export_set_buffer :: (folder: string) {
|
||||||
|
count := min(folder.count, viewer.export_buffer.count - 1);
|
||||||
|
memcpy(viewer.export_buffer.data, folder.data, count);
|
||||||
|
viewer.export_buffer[count] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puts folder in [export]: first when it is to be the default, else at the end if it is not there yet.
|
||||||
|
viewer_export_remember :: (folder: string, make_default: bool) {
|
||||||
|
folders := *app_user_settings.export_folders;
|
||||||
|
changed := false;
|
||||||
|
if !folders.count {
|
||||||
|
// The list was only the Downloads folder: it stays offered.
|
||||||
|
array_add(folders, copy_string(viewer_export_choices()[0],, context.default_allocator));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
at := -1;
|
||||||
|
for folders.* if it == folder { at = it_index; break; }
|
||||||
|
if make_default && at != 0 {
|
||||||
|
copy := copy_string(folder,, context.default_allocator);
|
||||||
|
if at > 0 { free(folders.*[at],, context.default_allocator); array_ordered_remove_by_index(folders, at); }
|
||||||
|
array_insert_at(folders, copy, 0);
|
||||||
|
changed = true;
|
||||||
|
} else if at < 0 {
|
||||||
|
array_add(folders, copy_string(folder,, context.default_allocator));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if changed app_save_user_settings_file_and_toast("the export folders");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The folder for viewer.export_index, as a modal popup.
|
||||||
|
viewer_export_popup :: () { using ImGui;
|
||||||
|
POPUP :: "Export capture";
|
||||||
|
if viewer.export_index >= 0 && !IsPopupOpen(POPUP) OpenPopup(POPUP);
|
||||||
|
if !BeginPopupModal(POPUP, null, .AlwaysAutoResize) return;
|
||||||
|
index := viewer.export_index;
|
||||||
|
if index < 0 || index >= viewer.captures.count || viewer.captures[index].removed {
|
||||||
|
viewer.export_index = -1;
|
||||||
|
CloseCurrentPopup();
|
||||||
|
EndPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capture := *viewer.captures[index];
|
||||||
|
Text("Export %", capture.name);
|
||||||
|
TextColored(app_theme.palette.dim, "Writes the file as it was imported, unchanged, into the folder.");
|
||||||
|
Spacing();
|
||||||
|
|
||||||
|
choices := viewer_export_choices();
|
||||||
|
if viewer.export_choice >= choices.count viewer.export_choice = 0;
|
||||||
|
custom := viewer.export_choice < 0;
|
||||||
|
SetNextItemWidth(GetFontSize() * 30);
|
||||||
|
if BeginCombo("##folders", temp_c_string(ifx custom then "Custom..." else choices[viewer.export_choice])) {
|
||||||
|
for choices {
|
||||||
|
if Selectable(temp_c_string(it), it_index == viewer.export_choice) viewer.export_choice = it_index;
|
||||||
|
if it_index == 0 && IsItemHovered() SetTooltip("The default");
|
||||||
|
}
|
||||||
|
Separator();
|
||||||
|
if Selectable("Custom...", custom) && !custom {
|
||||||
|
viewer.export_choice = -1;
|
||||||
|
viewer_export_set_buffer(choices[0]); // somewhere to start from
|
||||||
|
}
|
||||||
|
EndCombo();
|
||||||
|
}
|
||||||
|
SameLine();
|
||||||
|
Text("Folder");
|
||||||
|
custom = viewer.export_choice < 0;
|
||||||
|
|
||||||
|
folder: string;
|
||||||
|
if custom {
|
||||||
|
SetNextItemWidth(GetFontSize() * 30);
|
||||||
|
InputTextWithHint("##folder", "The folder to export to", viewer.export_buffer.data, viewer.export_buffer.count);
|
||||||
|
#if OS == .WINDOWS {
|
||||||
|
SameLine();
|
||||||
|
if Button("Browse...") {
|
||||||
|
chosen := windows_choose_folder("Choose a folder to export to");
|
||||||
|
if chosen viewer_export_set_buffer(chosen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folder = trim(to_string(viewer.export_buffer.data));
|
||||||
|
Checkbox("Add it to the list", *viewer.export_add);
|
||||||
|
if IsItemHovered() SetTooltip("Adds a folder line under [export] in %", APP_USER_SETTINGS_FILE_NAME);
|
||||||
|
} else {
|
||||||
|
folder = choices[viewer.export_choice];
|
||||||
|
}
|
||||||
|
if custom || viewer.export_choice > 0 {
|
||||||
|
Checkbox("Make it the default", *viewer.export_make_default);
|
||||||
|
if IsItemHovered() SetTooltip("Puts it first under [export] in %", APP_USER_SETTINGS_FILE_NAME);
|
||||||
|
}
|
||||||
|
Spacing();
|
||||||
|
|
||||||
|
// Enter and Escape are the open dropdown's while it is up.
|
||||||
|
keys := IsWindowFocused();
|
||||||
|
if !folder BeginDisabled();
|
||||||
|
if Button("Export") || (folder && keys && IsKeyPressed(.Enter)) {
|
||||||
|
viewer.export_index = -1;
|
||||||
|
CloseCurrentPopup();
|
||||||
|
if (custom || viewer.export_choice > 0) && viewer.export_make_default viewer_export_remember(folder, true);
|
||||||
|
else if custom && viewer.export_add viewer_export_remember(folder, false);
|
||||||
|
viewer_export_original(index, folder);
|
||||||
|
}
|
||||||
|
if !folder EndDisabled();
|
||||||
|
SameLine();
|
||||||
|
if Button("Cancel") || (keys && IsKeyPressed(.Escape)) {
|
||||||
|
viewer.export_index = -1;
|
||||||
|
CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- copying the selected capture
|
// ---------------------------------------------------------------- copying the selected capture
|
||||||
@ -1413,6 +1575,13 @@ viewer_full_window :: () { using ImGui;
|
|||||||
TextColored(app_theme.palette.dim, "% x %", viewer.full_source_width, viewer.full_source_height);
|
TextColored(app_theme.palette.dim, "% x %", viewer.full_source_width, viewer.full_source_height);
|
||||||
}
|
}
|
||||||
if viewer.full_loading { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); }
|
if viewer.full_loading { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); }
|
||||||
|
SameLine();
|
||||||
|
exporting := viewer.open_started;
|
||||||
|
if exporting BeginDisabled();
|
||||||
|
if SmallButton("Export...") viewer_export_ask(viewer.full_index);
|
||||||
|
if exporting EndDisabled();
|
||||||
|
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported, unchanged, to a folder");
|
||||||
|
if exporting && viewer.open_folder { SameLine(); TextColored(app_theme.palette.dim, "Exporting..."); }
|
||||||
TextColored(app_theme.palette.dim, "Left / Right: previous / next");
|
TextColored(app_theme.palette.dim, "Left / Right: previous / next");
|
||||||
|
|
||||||
// Zoom is a percentage of the size the capture was taken at. A fit sets it from the window, larger
|
// Zoom is a percentage of the size the capture was taken at. A fit sets it from the window, larger
|
||||||
@ -1445,7 +1614,7 @@ viewer_full_window :: () { using ImGui;
|
|||||||
if SmallButton(temp_c_string(ifx entry.media == .VIDEO then "Play in the default player" else "Open in the default viewer")) viewer_open_original(viewer.full_index);
|
if SmallButton(temp_c_string(ifx entry.media == .VIDEO then "Play in the default player" else "Open in the default viewer")) viewer_open_original(viewer.full_index);
|
||||||
if busy EndDisabled();
|
if busy EndDisabled();
|
||||||
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported to the temporary folder, and opens it");
|
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported to the temporary folder, and opens it");
|
||||||
if busy { SameLine(); TextColored(app_theme.palette.dim, "Writing it out..."); }
|
if busy && !viewer.open_folder { SameLine(); TextColored(app_theme.palette.dim, "Writing it out..."); }
|
||||||
}
|
}
|
||||||
ocr_ui_capture_controls(viewer.full_index, capture.path); // app/ocr_ui.jai
|
ocr_ui_capture_controls(viewer.full_index, capture.path); // app/ocr_ui.jai
|
||||||
labels_ui_capture_line(viewer.full_index); // app/labels_ui.jai
|
labels_ui_capture_line(viewer.full_index); // app/labels_ui.jai
|
||||||
|
|||||||
@ -230,6 +230,19 @@ settings_tests :: () {
|
|||||||
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);
|
||||||
|
|
||||||
|
{
|
||||||
|
group("[export] folder lines");
|
||||||
|
data := tprint("@version %\n[export]\nfolder: D:\\Exports\nfolder:\nfolder: \" E:\\Shared #2\"\n", APP_USER_SETTINGS_VERSION);
|
||||||
|
settings: App_User_Settings;
|
||||||
|
ok, seen := settings_parse("logger_tests", data, *settings);
|
||||||
|
defer settings_free(*settings);
|
||||||
|
expect(ok && seen.export_folder && settings.export_folders.count == 2, "gave seen % and % folders", seen.export_folder, settings.export_folders.count);
|
||||||
|
if settings.export_folders.count == 2 {
|
||||||
|
expect(settings.export_folders[0] == "D:\\Exports" && settings.export_folders[1] == " E:\\Shared #2",
|
||||||
|
"gave % and %", settings.export_folders[0], settings.export_folders[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A section that does not mention an action gets its default installed right away, since the save
|
// 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.
|
// that writes it into the file does not reload it; a pre-upgrade file gets "none" instead.
|
||||||
{
|
{
|
||||||
|
|||||||
@ -293,6 +293,9 @@ app_user_settings: App_User_Settings;
|
|||||||
App_User_Settings :: struct {
|
App_User_Settings :: struct {
|
||||||
theme: string; // "" means the default theme; heap-allocated
|
theme: string; // "" means the default theme; heap-allocated
|
||||||
ocr_model: string; // [ocr] model: the OCR model's name in models.config; "" means the default; heap-allocated
|
ocr_model: string; // [ocr] model: the OCR model's name in models.config; "" means the default; heap-allocated
|
||||||
|
// [export] folder, one line each: the folders Export offers for a capture's original, the first
|
||||||
|
// the default. None means the Downloads folder. Heap-allocated.
|
||||||
|
export_folders: [..] string;
|
||||||
|
|
||||||
// Of the monitor the window first opens on.
|
// Of the monitor the window first opens on.
|
||||||
window_width_fraction: float = 0.80;
|
window_width_fraction: float = 0.80;
|
||||||
@ -323,6 +326,8 @@ App_User_Settings :: struct {
|
|||||||
settings_free :: (s: *App_User_Settings) {
|
settings_free :: (s: *App_User_Settings) {
|
||||||
free(s.theme,, context.default_allocator);
|
free(s.theme,, context.default_allocator);
|
||||||
free(s.ocr_model,, context.default_allocator);
|
free(s.ocr_model,, context.default_allocator);
|
||||||
|
for s.export_folders free(it,, context.default_allocator);
|
||||||
|
array_free(s.export_folders);
|
||||||
free(s.video_capture,, context.default_allocator);
|
free(s.video_capture,, context.default_allocator);
|
||||||
array_free(s.hotkey_bindings);
|
array_free(s.hotkey_bindings);
|
||||||
array_free(s.global_hotkey_bindings);
|
array_free(s.global_hotkey_bindings);
|
||||||
@ -382,11 +387,11 @@ app_load_user_settings_file :: () -> write_back: bool {
|
|||||||
if hotkeys_missing.count || global_missing.count return true;
|
if hotkeys_missing.count || global_missing.count return true;
|
||||||
|
|
||||||
#if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet
|
#if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet
|
||||||
return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone);
|
return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.export_folder && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone);
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings_Seen :: struct {
|
Settings_Seen :: struct {
|
||||||
theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model: bool;
|
theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model, export_folder: bool;
|
||||||
video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool;
|
video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,13 +403,14 @@ settings_file_hash: u64;
|
|||||||
Settings_App_Values :: struct {
|
Settings_App_Values :: struct {
|
||||||
theme: string;
|
theme: string;
|
||||||
ocr_model: string;
|
ocr_model: string;
|
||||||
|
export_folders: string; // the list, a line each
|
||||||
font_size_base: float;
|
font_size_base: float;
|
||||||
font_scale_main: float;
|
font_scale_main: float;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The app values as of the last load or save. A save keeps what the file says for a setting that
|
// The app values as of the last load or save. A save keeps what the file says for a setting that
|
||||||
// has not changed in the app since, so an edit made by hand while the app runs is not overwritten.
|
// has not changed in the app since, so an edit made by hand while the app runs is not overwritten.
|
||||||
settings_synced: Settings_App_Values; // theme heap-allocated
|
settings_synced: Settings_App_Values; // its strings heap-allocated
|
||||||
|
|
||||||
settings_app_values :: () -> Settings_App_Values {
|
settings_app_values :: () -> Settings_App_Values {
|
||||||
values: Settings_App_Values;
|
values: Settings_App_Values;
|
||||||
@ -412,6 +418,7 @@ settings_app_values :: () -> Settings_App_Values {
|
|||||||
values.theme = ifx entry then entry.name else app_user_settings.theme;
|
values.theme = ifx entry then entry.name else app_user_settings.theme;
|
||||||
if !values.theme.count values.theme = APP_DEFAULT_THEME_NAME;
|
if !values.theme.count values.theme = APP_DEFAULT_THEME_NAME;
|
||||||
values.ocr_model = ifx app_user_settings.ocr_model.count then app_user_settings.ocr_model else OCR_DEFAULT_MODEL;
|
values.ocr_model = ifx app_user_settings.ocr_model.count then app_user_settings.ocr_model else OCR_DEFAULT_MODEL;
|
||||||
|
values.export_folders = join(..app_user_settings.export_folders, "\n",, temp);
|
||||||
|
|
||||||
values.font_size_base = app_user_settings.font_size_base;
|
values.font_size_base = app_user_settings.font_size_base;
|
||||||
values.font_scale_main = app_user_settings.font_scale_main;
|
values.font_scale_main = app_user_settings.font_scale_main;
|
||||||
@ -428,9 +435,11 @@ settings_app_values :: () -> Settings_App_Values {
|
|||||||
settings_note_synced :: () {
|
settings_note_synced :: () {
|
||||||
free(settings_synced.theme,, context.default_allocator);
|
free(settings_synced.theme,, context.default_allocator);
|
||||||
free(settings_synced.ocr_model,, context.default_allocator);
|
free(settings_synced.ocr_model,, context.default_allocator);
|
||||||
|
free(settings_synced.export_folders,, context.default_allocator);
|
||||||
settings_synced = settings_app_values();
|
settings_synced = settings_app_values();
|
||||||
settings_synced.theme = copy_string(settings_synced.theme,, context.default_allocator);
|
settings_synced.theme = copy_string(settings_synced.theme,, context.default_allocator);
|
||||||
settings_synced.ocr_model = copy_string(settings_synced.ocr_model,, context.default_allocator);
|
settings_synced.ocr_model = copy_string(settings_synced.ocr_model,, context.default_allocator);
|
||||||
|
settings_synced.export_folders = copy_string(settings_synced.export_folders,, context.default_allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses the file's contents into out, replacing its theme and hotkey bindings. ok is false when
|
// Parses the file's contents into out, replacing its theme and hotkey bindings. ok is false when
|
||||||
@ -445,6 +454,9 @@ settings_parse :: (path: string, file_data: string, out: *App_User_Settings) ->
|
|||||||
array_reset(*out.hotkey_bindings);
|
array_reset(*out.hotkey_bindings);
|
||||||
out.global_hotkey_bindings.allocator = context.default_allocator;
|
out.global_hotkey_bindings.allocator = context.default_allocator;
|
||||||
array_reset(*out.global_hotkey_bindings);
|
array_reset(*out.global_hotkey_bindings);
|
||||||
|
for out.export_folders free(it,, context.default_allocator);
|
||||||
|
out.export_folders.allocator = context.default_allocator;
|
||||||
|
array_reset(*out.export_folders);
|
||||||
|
|
||||||
seen_version := false;
|
seen_version := false;
|
||||||
version: s64;
|
version: s64;
|
||||||
@ -506,6 +518,10 @@ settings_parse :: (path: string, file_data: string, out: *App_User_Settings) ->
|
|||||||
free(out.ocr_model,, context.default_allocator);
|
free(out.ocr_model,, context.default_allocator);
|
||||||
out.ocr_model = copy_string(settings_unquote(value),, context.default_allocator);
|
out.ocr_model = copy_string(settings_unquote(value),, context.default_allocator);
|
||||||
seen.ocr_model = true;
|
seen.ocr_model = true;
|
||||||
|
} else if section == "[export]" && key == "folder" {
|
||||||
|
folder := settings_unquote(value); // an empty line says the list is empty
|
||||||
|
if folder.count array_add(*out.export_folders, copy_string(folder,, context.default_allocator));
|
||||||
|
seen.export_folder = true;
|
||||||
} else if section == "[ui]" && key == "theme" {
|
} else if section == "[ui]" && key == "theme" {
|
||||||
free(out.theme,, context.default_allocator);
|
free(out.theme,, context.default_allocator);
|
||||||
out.theme = copy_string(settings_unquote(value),, context.default_allocator);
|
out.theme = copy_string(settings_unquote(value),, context.default_allocator);
|
||||||
@ -633,6 +649,7 @@ app_save_user_settings_file :: () -> bool {
|
|||||||
values := settings_app_values();
|
values := settings_app_values();
|
||||||
theme := values.theme;
|
theme := values.theme;
|
||||||
ocr_model := values.ocr_model;
|
ocr_model := values.ocr_model;
|
||||||
|
export_folders: [] string = app_user_settings.export_folders;
|
||||||
font_size_base := values.font_size_base;
|
font_size_base := values.font_size_base;
|
||||||
font_scale_main := values.font_scale_main;
|
font_scale_main := values.font_scale_main;
|
||||||
|
|
||||||
@ -663,6 +680,7 @@ app_save_user_settings_file :: () -> bool {
|
|||||||
if disk_ok {
|
if disk_ok {
|
||||||
if seen.theme && values.theme == settings_synced.theme && on_disk.theme.count theme = on_disk.theme;
|
if seen.theme && values.theme == settings_synced.theme && on_disk.theme.count theme = on_disk.theme;
|
||||||
if seen.ocr_model && values.ocr_model == settings_synced.ocr_model && on_disk.ocr_model.count ocr_model = on_disk.ocr_model;
|
if seen.ocr_model && values.ocr_model == settings_synced.ocr_model && on_disk.ocr_model.count ocr_model = on_disk.ocr_model;
|
||||||
|
if seen.export_folder && values.export_folders == settings_synced.export_folders export_folders = on_disk.export_folders;
|
||||||
if seen.font_size && values.font_size_base == settings_synced.font_size_base font_size_base = on_disk.font_size_base;
|
if seen.font_size && values.font_size_base == settings_synced.font_size_base font_size_base = on_disk.font_size_base;
|
||||||
if seen.font_scale && values.font_scale_main == settings_synced.font_scale_main font_scale_main = on_disk.font_scale_main;
|
if seen.font_scale && values.font_scale_main == settings_synced.font_scale_main font_scale_main = on_disk.font_scale_main;
|
||||||
if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction;
|
if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction;
|
||||||
@ -695,6 +713,12 @@ app_save_user_settings_file :: () -> bool {
|
|||||||
append(*builder, "[ocr]\n");
|
append(*builder, "[ocr]\n");
|
||||||
append(*builder, "# The OCR model, by its name in ocr-models/models.config: ppocr-v5-mobile (light) or ppocr-v5-server (heavy).\n");
|
append(*builder, "# The OCR model, by its name in ocr-models/models.config: ppocr-v5-mobile (light) or ppocr-v5-server (heavy).\n");
|
||||||
print_to_builder(*builder, "model: %\n\n", settings_quote_if_needed(ocr_model));
|
print_to_builder(*builder, "model: %\n\n", settings_quote_if_needed(ocr_model));
|
||||||
|
append(*builder, "[export]\n");
|
||||||
|
append(*builder, "# The folders Export (in the Capture window) offers to write a capture to as it was imported, a\n");
|
||||||
|
append(*builder, "# folder: line each; the first is the default. None: the Downloads folder.\n");
|
||||||
|
for export_folders print_to_builder(*builder, "folder: %\n", settings_quote_if_needed(it));
|
||||||
|
if !export_folders append(*builder, "folder:\n");
|
||||||
|
append(*builder, "\n");
|
||||||
append(*builder, "[video]\n");
|
append(*builder, "[video]\n");
|
||||||
append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n");
|
append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n");
|
||||||
append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n");
|
append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user