[0.1.17] mcap.exe FILE opens a file, beside a running mcap in view mode; a Command Line window; thumbnails no longer stuck on "Cannot read"
mcap.exe FILE, with FILE first (as Explorer's Open with passes it), opens an .mcap, image, video or .snagx (app/open_file.jai). With no mcap running, the app starts as usual: an .mcap in the library opens in the Capture window, anything else in the Opened File window, which says where the library has it already (by its hash in .import-index) or imports it into the library folder typed or chosen. With mcap running, a second window opens in view mode: the file alone, with Import, and Open in mcap / Show in mcap, which hand it to the running copy (a new Open_File message) and close the view. The library is not loaded or watched, no model loads, and it has no tray icon, no global hotkeys, and writes no settings or layout. Import takes a single file as its source too (and -import FILE DEST); an .mcap is copied in as it is, while a folder's import still skips them. -open-import DEST imports the opened file as its Import button does, then exits. -h and --help open a Command Line window listing the options, also toggled from the Debug Panel; with mcap running, that one comes forward with it (a Show_Help message). Thumbnails: the "First preview shown" log line, put between creating a preview's texture and its else, had every preview after the first marked failed. Once its texture was released (scrolled far away, or the switch from WARP to the GPU at startup) it was never decoded again and showed "Cannot read".
This commit is contained in:
parent
2bd77e8121
commit
be9ad707c4
31
README.md
31
README.md
@ -284,6 +284,32 @@ What becomes of each file:
|
||||
|
||||
The files imported from are only read: nothing there is changed, moved or deleted.
|
||||
|
||||
## Opening a file
|
||||
|
||||
`mcap.exe FILE`, with FILE first, opens one file: an `.mcap`, or any image, video or `.snagx` that
|
||||
Import takes. This is what Explorer passes to the program a file type opens with (Open with > mcap).
|
||||
|
||||
- **mcap not running:** it starts as usual and shows the file.
|
||||
- **An `.mcap` in the library** is selected and opened in the Capture window.
|
||||
- **Anything else** opens in the **Opened File** window, with its details. If the library has it
|
||||
already, in any folder (found by its hash in `.import-index`), the window says where, with
|
||||
**Show**. If not, **Import** brings it into the library folder typed or chosen from the menu
|
||||
beside the name. That's the Import window's import, of this one file, so the same naming,
|
||||
duplicate check and reading of text and labels apply. Once in, it opens in the Capture window.
|
||||
- **An `.mcap` from outside the library** is copied in as it is, under its own name. (A folder's
|
||||
import skips `.mcap` files.)
|
||||
- **mcap already running:** a second window opens in **view mode**: the file alone, with Fit and
|
||||
Zoom, and the same Import.
|
||||
- **Show in mcap** (for a file the library has) and **Open in mcap** (for any other) hand the file to
|
||||
the running mcap, which comes forward showing it, and close the view.
|
||||
- **Ctrl-C** copies the image, and **Ctrl-Shift-C** the path.
|
||||
- **Escape**, Ctrl-W or the close button ends it.
|
||||
- **What's left out:** the library isn't loaded or watched, and there are no models, OCR, labels or
|
||||
segmentation. There's no tray icon and no PrintScreen. It writes no settings or window layout.
|
||||
- **After an import here:** the running mcap picks the capture up through the library watcher. OCR
|
||||
and labels read it at the next scan, or right away with **Show in mcap** and **OCR** in the
|
||||
Capture window.
|
||||
|
||||
## Searching
|
||||
|
||||
Type in the search box above the grid; the grid shows the matches as you type, with "N of M
|
||||
@ -363,8 +389,10 @@ then in that order instead of by date.
|
||||
|
||||
| Option | What it does |
|
||||
| --- | --- |
|
||||
| `FILE` (first) | Opens FILE: an `.mcap`, image or video; in view mode when mcap is already running. See [Opening a file](#opening-a-file). |
|
||||
| `-open-import DEST` | With FILE: imports it into the library folder DEST as its Import button does, then exits: 0 once the library has it (imported, or there already), else 1. |
|
||||
| `-library FOLDER` | Shows the `.mcap` files in FOLDER (and its folders) instead of `D:\McapIndex\mcap`. |
|
||||
| `-import SOURCE DEST` | Imports the folder SOURCE into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
||||
| `-import SOURCE DEST` | Imports SOURCE, a folder or a single file, into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
||||
| `-search QUERY` | Starts with QUERY in the search box. |
|
||||
| `-cold` | Loads the library as on a cold start: its files and the catalog cache are dropped from the system's file cache first (Windows and Linux; not macOS, which allows it only with `sudo purge`). The `[startup]` lines in the log time each step. With `-stats`, a startup benchmark: `bin\mcap.exe -cold -stats`. |
|
||||
| `-no-catalog-cache` | Reads every capture from its file, as without the catalog cache (for measuring); the cache is still written. |
|
||||
@ -378,6 +406,7 @@ then in that order instead of by date.
|
||||
| `run_tests` | Runs the test suites (debug builds); with `-headless`, exits with 0 if all passed. |
|
||||
| `-headless` | No message boxes: a failure is logged and the app exits with 1. |
|
||||
| `-version` | Prints the version. |
|
||||
| `-h`, `--help` | Opens the Command Line window, this table in the app (also in the Debug Panel). With mcap already running, that one comes forward with it. |
|
||||
|
||||
`-import`, `-scan`, `-stats` and `-erase` still open the window, to show their progress; they are meant for
|
||||
scripts and for trying things on a copy of the library: `bin\mcap.exe -library D:\copy -scan`.
|
||||
|
||||
@ -47,7 +47,7 @@ APP_NAME :: "mcap";
|
||||
APP_FOLDER_NAME :: "mcap";
|
||||
GIT_BRANCH :: "check";
|
||||
GIT_HASH :: "-";
|
||||
VERSION :: "0.1.16";
|
||||
VERSION :: "0.1.17";
|
||||
BUILD_DATE :: "-";
|
||||
DEBUG :: true;
|
||||
COMPILE_TESTS :: true;
|
||||
|
||||
@ -8,7 +8,7 @@ APP_NAME :: "mcap";
|
||||
APP_FOLDER_NAME :: "mcap";
|
||||
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
||||
|
||||
VERSION :: "0.1.16";
|
||||
VERSION :: "0.1.17";
|
||||
// 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";
|
||||
|
||||
14
src/App.jai
14
src/App.jai
@ -24,6 +24,7 @@ USING_WINDOWS :: OS == .WINDOWS;
|
||||
#load "app/background.jai";
|
||||
#load "app/labels_ui.jai";
|
||||
#load "app/import.jai";
|
||||
#load "app/open_file.jai";
|
||||
#load "app/segment_ui.jai";
|
||||
#load "app/library_folders.jai";
|
||||
#load "app/exit_shutdown.jai";
|
||||
@ -186,6 +187,8 @@ App :: struct {
|
||||
show_logger_panel: bool = DEBUG;
|
||||
show_frame_time_graph: bool = DEBUG;
|
||||
show_style_editor: bool;
|
||||
show_help_panel: bool; // the Command Line window (base/debug.jai): -h, --help, or the Debug Panel
|
||||
help_panel_focus: bool; // bring it forward next frame
|
||||
minimize_to_tray_this_frame: bool;
|
||||
restore_window_this_frame: bool;
|
||||
capture_region_this_frame: bool; // the capture_region global hotkey fired: app/screen_capture.jai
|
||||
@ -209,6 +212,9 @@ App :: struct {
|
||||
#if OS == .WINDOWS record_region: RECT; // -record-region X Y W H: -record records that, on the virtual desktop, not the whole screen
|
||||
segment_and_exit: bool; // -segment: segment the library folders set to segment once the catalog has loaded, then exit (0, or 1 if any image failed)
|
||||
import_source, import_dest: string; // -import SOURCE DEST: import SOURCE into the library folder DEST (app/import.jai), then exit
|
||||
open_file: string; // mcap.exe FILE: the file to show (app/open_file.jai); absolute, with / between its folders
|
||||
view_mode: bool; // FILE was given while another instance runs: only it is shown, with Import (app/open_file.jai)
|
||||
open_import_dest: string; // -open-import DEST: FILE imported into the library folder DEST as its Import button does, then exit (0, or 1 if it failed)
|
||||
last_window_restore_time: Apollo_Time;
|
||||
|
||||
imgui_context: *ImGui.ImGuiContext;
|
||||
@ -329,6 +335,8 @@ APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE :: 2; // 1 byte for message-or-done, 1 b
|
||||
App_Shared_Memory_Tag :: enum u8 {
|
||||
Done :: 0;
|
||||
Bring_To_Front :: 1;
|
||||
Open_File :: 2; // the path follows the header: show it (open_file_request)
|
||||
Show_Help :: 3; // mcap --help while this one runs: come forward with the Command Line window
|
||||
}
|
||||
|
||||
APP_SHARED_MEMORY_CHANNEL_BYTES :: 4096; // only one page?
|
||||
@ -341,6 +349,12 @@ APP_WINDOW_TITLE :: #run -> string {
|
||||
git_info, append_debug);
|
||||
}
|
||||
|
||||
// The window's title, ending in a 0 for the OS: in view mode, the file's name first.
|
||||
app_window_title :: () -> string {
|
||||
if !app.view_mode return APP_WINDOW_TITLE;
|
||||
return tprint("% - % (view)\0", path_filename(app.open_file), APP_NAME);
|
||||
}
|
||||
|
||||
APP_TRAY_TOOLTIP_TEXT :: #run -> string {
|
||||
return ifx DEBUG
|
||||
tprint("% % (DEBUG)\0", APP_NAME, VERSION)
|
||||
|
||||
@ -6,16 +6,25 @@ APP_MAIN_WINDOW_LABEL :: "Main";
|
||||
|
||||
// Once, after ImGui, the fonts, the theme and the settings are up and before the first frame.
|
||||
app_ui_startup :: () {
|
||||
// View mode (app/open_file.jai): the file alone. The library is not listed, and no model, scan,
|
||||
// watcher or recording starts.
|
||||
if app.view_mode {
|
||||
import_startup();
|
||||
open_file_startup();
|
||||
return;
|
||||
}
|
||||
viewer_startup();
|
||||
import_startup();
|
||||
ocr_ui_startup();
|
||||
labels_ui_startup();
|
||||
segment_ui_startup(); // the library folders' options: after the viewer, which knows the library
|
||||
screen_record_startup(); // recordings a crash left behind, to offer
|
||||
open_file_startup(); // mcap.exe FILE: after the viewer, which knows whether it is in the library
|
||||
}
|
||||
|
||||
// Every frame, before input is handed to ImGui. Poll background work here.
|
||||
app_ui_frame_start :: () {
|
||||
if app.view_mode return;
|
||||
screen_capture_frame(); // also while hidden in the tray, where the frame ends before the UI
|
||||
ocr_ui_unload_idle_models(); // models unused for a while, also while in the tray
|
||||
}
|
||||
@ -24,6 +33,12 @@ app_ui_frame_start :: () {
|
||||
// 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).
|
||||
app_ui_shutdown :: () {
|
||||
if app.view_mode {
|
||||
app_shutdown_step(.IMPORT);
|
||||
import_shutdown();
|
||||
open_file_shutdown();
|
||||
return;
|
||||
}
|
||||
app_shutdown_step(.CAPTURE);
|
||||
screen_capture_shutdown();
|
||||
app_shutdown_step(.IMPORT);
|
||||
@ -38,6 +53,7 @@ app_ui_shutdown :: () {
|
||||
labels_ui_shutdown(); // after the OCR thread and the background workers have stopped
|
||||
app_shutdown_step(.STATS);
|
||||
stats_ui_shutdown();
|
||||
open_file_shutdown();
|
||||
}
|
||||
|
||||
// Which step of app_ui_shutdown is running, and since when, for the overlay a slow shutdown shows.
|
||||
@ -91,6 +107,10 @@ app_ui_shutdown_status :: () -> string {
|
||||
}
|
||||
|
||||
app_main_window_ui :: () {
|
||||
if app.view_mode {
|
||||
if !app_exit_shutdown_started() open_file_view_mode_ui(); // the whole window (app/open_file.jai)
|
||||
return;
|
||||
}
|
||||
push_imgui_window(APP_MAIN_WINDOW_LABEL);
|
||||
// Once shutdown has started, its thread is stopping the threads these frames hand work to and take
|
||||
// it from (the viewer's workers, the search group, the import, OCR), and freeing what they share:
|
||||
@ -102,5 +122,6 @@ app_main_window_ui :: () {
|
||||
segment_ui_frame();
|
||||
stats_ui_frame();
|
||||
import_frame();
|
||||
open_file_frame(); // after import_frame, which hands the grid what an import of it wrote
|
||||
screen_record_leftovers_popup();
|
||||
}
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
// folder) holds a hash of every .mcap's original image, made once for the files already there and kept
|
||||
// up to date after that; an image whose hash is in it is reported as a duplicate, with where it is.
|
||||
//
|
||||
// The source may also be a single file (app/open_file.jai, or -import FILE DEST): it alone is imported,
|
||||
// and an .mcap, which a folder's import skips, is copied in as it is.
|
||||
//
|
||||
// The import runs on a thread of its own, which lists the files, brings the index up to date, and then
|
||||
// imports with IMPORT_WORKERS threads. New captures go into the grid as they are written; when it ends,
|
||||
// the OCR scan reads and labels them, if asked to.
|
||||
@ -60,6 +63,7 @@ importer: struct {
|
||||
|
||||
// Set by the main thread before the thread starts; read-only while it runs.
|
||||
source: string;
|
||||
single_file: bool; // source is a file, not a folder
|
||||
dest_name: string;
|
||||
dest_folder: string;
|
||||
library: string;
|
||||
@ -137,13 +141,15 @@ import_start :: (source: string, dest_name: string) -> bool {
|
||||
importer.library = copy_string(viewer_folder());
|
||||
importer.dest_folder = sprint("%/%", importer.library, dest);
|
||||
importer.error = "";
|
||||
importer.single_file = !is_directory(source_path);
|
||||
|
||||
// The library is never imported into itself.
|
||||
lower_source := to_lower_copy(importer.source,, temp);
|
||||
lower_library := to_lower_copy(importer.library,, temp);
|
||||
path_overwrite_separators(lower_library, #char "/");
|
||||
if lower_source == lower_library || begins_with(lower_source, tprint("%/", lower_library)) || begins_with(lower_library, tprint("%/", lower_source)) {
|
||||
toast_show(.Error, "The folder to import holds the library, or is inside it");
|
||||
if importer.single_file toast_show(.Error, "% is in the library already", path_filename(importer.source));
|
||||
else toast_show(.Error, "The folder to import holds the library, or is inside it");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -166,8 +172,11 @@ import_start :: (source: string, dest_name: string) -> bool {
|
||||
importer.start = current_time_monotonic();
|
||||
importer.scan_pending = false;
|
||||
|
||||
table_set(*importer.sources, copy_string(dest), copy_string(importer.source));
|
||||
import_sources_save();
|
||||
// Where a library folder's captures come from: a folder, not each file opened on its own.
|
||||
if !importer.single_file {
|
||||
table_set(*importer.sources, copy_string(dest), copy_string(importer.source));
|
||||
import_sources_save();
|
||||
}
|
||||
|
||||
importer.running = true;
|
||||
if importer.thread_started thread_deinit(*importer.thread);
|
||||
@ -197,7 +206,11 @@ import_thread_proc :: (thread: *Thread) -> s64 {
|
||||
// The files to import, sorted so a second run goes through them in the same order.
|
||||
Found :: struct { files: *[..] string; }
|
||||
found := Found.{*importer.files};
|
||||
visited := visit_files(importer.source, true, *found, (info: *File_Visit_Info, found: *Found) {
|
||||
visited := true;
|
||||
if importer.single_file {
|
||||
array_add(*importer.files, copy_string(importer.source));
|
||||
importer.listed = 1;
|
||||
} else visited = visit_files(importer.source, true, *found, (info: *File_Visit_Info, found: *Found) {
|
||||
if info.is_directory {
|
||||
name := info.short_name;
|
||||
if name.count && (name[0] == #char "." || name[0] == #char "$" || name == "System Volume Information") info.descend_into_directory = false;
|
||||
@ -295,6 +308,10 @@ import_worker :: (thread: *Thread) -> s64 {
|
||||
import_file :: (path: string) -> Import_Outcome, text: string, new_path: string {
|
||||
name := path_filename(path);
|
||||
extension := to_lower_copy(path_extension(name),, temp);
|
||||
if extension == MCAP_EXTENSION && importer.single_file {
|
||||
outcome, text, new_path := import_mcap_file(path, name);
|
||||
return outcome, text, new_path;
|
||||
}
|
||||
if array_find(IMPORT_OTHER_EXTENSIONS, extension) return .SKIPPED, extension, "";
|
||||
|
||||
modified, size, stat_ok := file_modtime_and_size(path);
|
||||
@ -378,6 +395,28 @@ import_video_file :: (path: string, name: string, head: [] u8, size: s64, modifi
|
||||
return .IMPORTED, "", new_path;
|
||||
}
|
||||
|
||||
// An .mcap imported on its own: copied into the library folder as it is, under its own name, a few MB
|
||||
// at a time (it may hold a video). A duplicate when the library has its original.
|
||||
import_mcap_file :: (path: string, name: string) -> Import_Outcome, text: string, new_path: string {
|
||||
_, original, parsed, error := mcap_read_without_original(path,, temp);
|
||||
if !parsed return .FAILED, error, "";
|
||||
hash, hashed := import_hash_file(original);
|
||||
if !hashed return .FAILED, "its original cannot be read", "";
|
||||
claimed, where := import_claim(hash);
|
||||
if !claimed return .DUPLICATE, where, "";
|
||||
let_go := true;
|
||||
defer if let_go import_let_go(hash);
|
||||
|
||||
_, size, stat_ok := file_modtime_and_size(path);
|
||||
if !stat_ok return .FAILED, "cannot be read", "";
|
||||
out_path := import_reserve_path(name);
|
||||
written, write_error := mcap_extract_original(.{path = path, offset = 0, size = size}, out_path);
|
||||
new_path := import_written(out_path, hash, written);
|
||||
if !written return .FAILED, write_error, "";
|
||||
let_go = false;
|
||||
return .IMPORTED, "", new_path;
|
||||
}
|
||||
|
||||
// Holds hash for a file being imported, so no other worker imports the same bytes; false, with where
|
||||
// they already are, when the library has them, or another file of this import does.
|
||||
import_claim :: (hash: u64) -> claimed: bool, where: string {
|
||||
@ -853,6 +892,20 @@ import_index_worker :: (thread: *Thread) -> s64 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Where the library holds the original whose hash is given, by the index as last saved in library: a
|
||||
// path inside it ("Phone/2024-05-12_10-15-30_1a2b3c4d.mcap"), or "" for nowhere. Any thread; temp.
|
||||
import_index_find :: (library: string, hash: u64) -> string {
|
||||
data, ok := read_entire_file(tprint("%/%", library, IMPORT_INDEX_NAME), log_errors = false,, temp);
|
||||
if !ok return "";
|
||||
wanted := import_hash_text(hash);
|
||||
for split(data, "\n",, temp) {
|
||||
if !begins_with(it, wanted) continue;
|
||||
parts := split(trim_right(it, "\r"), "\t",, temp);
|
||||
if parts.count == 4 && parts[0] == wanted && file_exists(tprint("%/%", library, parts[3])) return parts[3];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
import_record_set :: (relative: string, record: Index_Record) {
|
||||
pointer := table_find_pointer(*importer.records, relative);
|
||||
if pointer { pointer.* = record; return; }
|
||||
|
||||
@ -38,6 +38,12 @@ main :: () {
|
||||
for app.args if it == "-search" && it_index + 1 < app.args.count app.start_search = app.args[it_index + 1];
|
||||
for app.args if it == "-erase" && it_index + 1 < app.args.count app.erase_and_exit = app.args[it_index + 1];
|
||||
for app.args if it == "-import" && it_index + 2 < app.args.count { app.import_source = app.args[it_index + 1]; app.import_dest = app.args[it_index + 2]; }
|
||||
// mcap.exe FILE, as Explorer's Open with passes it: an .mcap, or an image or video (app/open_file.jai).
|
||||
if app.args.count > 1 && app.args[1].count && app.args[1][0] != #char "-" && app.args[1] != "run_tests" {
|
||||
app.open_file = open_file_normalize(app.args[1]);
|
||||
}
|
||||
for app.args if it == "-open-import" && it_index + 1 < app.args.count app.open_import_dest = app.args[it_index + 1];
|
||||
for app.args if it == "-h" || it == "--help" || it == "-help" { app.show_help_panel = true; app.help_panel_focus = true; } // the Command Line window (base/debug.jai)
|
||||
|
||||
defer report_memory_leaks();
|
||||
|
||||
@ -105,7 +111,7 @@ main :: () {
|
||||
another_instance_owns_channel := false;
|
||||
#if USING_SDL another_instance_owns_channel = app_instance_running;
|
||||
|
||||
if !another_instance_owns_channel && app_bring_running_instance_to_front(channel_key, attempts=1) return;
|
||||
if !another_instance_owns_channel && app_running_instance_reachable(channel_key) another_instance_owns_channel = true;
|
||||
|
||||
if !another_instance_owns_channel {
|
||||
read_channel_success, read_channel, already_exists :=
|
||||
@ -121,7 +127,19 @@ main :: () {
|
||||
}
|
||||
}
|
||||
|
||||
if another_instance_owns_channel {
|
||||
if another_instance_owns_channel && app.open_file {
|
||||
// A file opened while mcap runs: shown here, on its own, in view mode (app/open_file.jai).
|
||||
app.view_mode = true;
|
||||
app.show_debug_panel = false;
|
||||
app.show_logger_panel = false;
|
||||
app.show_frame_time_graph = false;
|
||||
} else if another_instance_owns_channel && app.show_help_panel {
|
||||
// mcap --help while mcap runs: that one comes forward with the Command Line window.
|
||||
if !app_send_to_running_instance(channel_key, 20, .Show_Help) {
|
||||
log_error("Another instance is running, but its shared memory channel could not be reached (it may be running as administrator); exiting.");
|
||||
}
|
||||
return;
|
||||
} else if another_instance_owns_channel {
|
||||
if !app_bring_running_instance_to_front(channel_key, attempts=20) {
|
||||
log_error("Another instance is running, but its shared memory channel could not be reached (it may be running as administrator); exiting.");
|
||||
}
|
||||
@ -142,6 +160,7 @@ main :: () {
|
||||
log(" - GIT_HASH: %", GIT_HASH);
|
||||
log(" - COMPILE_ARGS: %", COMPILE_ARGS);
|
||||
log(" - COMMAND_LINE_ARGUMENTS: %", args_combined);
|
||||
if app.view_mode log(" - VIEW MODE: another instance is running; showing % on its own.", app.open_file);
|
||||
log(" --- \\STARTUP INFO/ --- ");
|
||||
|
||||
app.startup_time = current_time_monotonic();
|
||||
@ -172,7 +191,7 @@ main :: () {
|
||||
app.username = copy_string(get_username());
|
||||
log("Current PC name: %, username: %", app.machine_name, app.username);
|
||||
|
||||
settings_write_back := app_load_user_settings_file();
|
||||
settings_write_back := app_load_user_settings_file() && !app.view_mode; // the running instance owns the file
|
||||
logger_set_event_log_capacity(app_user_settings.event_log_capacity);
|
||||
|
||||
// #Startup:CreateWindow
|
||||
@ -196,7 +215,7 @@ main :: () {
|
||||
|
||||
window_flags : SDL_WindowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
||||
|
||||
platform.window = SDL_CreateWindow(APP_WINDOW_TITLE, w, h, window_flags);
|
||||
platform.window = SDL_CreateWindow(app_window_title().data, w, h, window_flags);
|
||||
ok_or_exit(platform.window != null, "failed to create window with error = %", consume_sdl_error());
|
||||
|
||||
centered := SDL_WINDOWPOS_CENTERED_DISPLAY(display);
|
||||
@ -241,7 +260,7 @@ main :: () {
|
||||
|
||||
hwnd = CreateWindowExW(WS_EX_APPWINDOW,
|
||||
utf8_to_wide(lpszClassName,,temp),
|
||||
utf8_to_wide(APP_WINDOW_TITLE,,temp),
|
||||
utf8_to_wide(app_window_title(),,temp),
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
window_x, window_y, win_width, win_height,
|
||||
null, null, null, null);
|
||||
@ -260,12 +279,13 @@ main :: () {
|
||||
}
|
||||
}
|
||||
#if USING_WINDOWS { // :Win32_Initialize_Extended
|
||||
Win32_Set_Tray_Icon();
|
||||
Win32_Hide_Tray_Icon();
|
||||
defer Win32_Hide_Tray_Icon(); // exiting from the tray would otherwise leave the icon behind
|
||||
// View mode has no tray: closing its window ends it.
|
||||
if !app.view_mode { Win32_Set_Tray_Icon(); Win32_Hide_Tray_Icon(); }
|
||||
defer if !app.view_mode Win32_Hide_Tray_Icon(); // exiting from the tray would otherwise leave the icon behind
|
||||
}
|
||||
// [global_hotkeys] was read with the settings, before there was a window to register them for.
|
||||
global_hotkeys_apply();
|
||||
// The running instance has them; view mode leaves them alone.
|
||||
if !app.view_mode global_hotkeys_apply();
|
||||
defer platform_unregister_global_hotkeys();
|
||||
// #Startup:Graphics
|
||||
#if USING_SDL {
|
||||
@ -406,6 +426,15 @@ app_frame :: (in_live_resize := false) -> bool {
|
||||
command := cast(App_Shared_Memory_Tag)message.user_read_pointer[0];
|
||||
if command == .Bring_To_Front {
|
||||
app.restore_window_this_frame = true;
|
||||
} else if command == .Show_Help {
|
||||
app.restore_window_this_frame = true;
|
||||
app.show_help_panel = true;
|
||||
app.help_panel_focus = true;
|
||||
} else if command == .Open_File && message.user_read_bytes > APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE {
|
||||
// Open in mcap, from a copy in view mode (app/open_file.jai): the path follows the header.
|
||||
path := string.{message.user_read_bytes - APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE, message.user_read_pointer + APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE};
|
||||
app.restore_window_this_frame = true;
|
||||
if !app_exit_shutdown_started() open_file_request(path);
|
||||
}
|
||||
Shared_Channel.reader_done_with_message(*message);
|
||||
}
|
||||
@ -434,13 +463,14 @@ app_frame :: (in_live_resize := false) -> bool {
|
||||
|
||||
if ev.type.(SDL_EventType) == {
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_MINIMIZED;
|
||||
app.minimize_to_tray_this_frame = true;
|
||||
if !app.view_mode app.minimize_to_tray_this_frame = true;
|
||||
|
||||
case SDL_EventType.QUIT;
|
||||
app.should_exit = true;
|
||||
|
||||
case SDL_EventType.WINDOW_CLOSE_REQUESTED;
|
||||
app.minimize_to_tray_this_frame = true;
|
||||
if app.view_mode app.should_exit = true; // view mode has no tray
|
||||
else app.minimize_to_tray_this_frame = true;
|
||||
|
||||
case;
|
||||
}
|
||||
@ -548,10 +578,11 @@ app_frame :: (in_live_resize := false) -> bool {
|
||||
|
||||
|
||||
{ // #UI Start
|
||||
ImGui.DockSpaceOverViewport();
|
||||
if !app.view_mode ImGui.DockSpaceOverViewport(); // view mode is one window filling the main one
|
||||
|
||||
app_debug_panel_ui();
|
||||
app_frame_times_ui();
|
||||
app_help_panel_ui();
|
||||
app_style_editor_ui();
|
||||
app_logger_panel_ui();
|
||||
|
||||
@ -700,8 +731,21 @@ app_shared_memory_channel_key :: () -> string {
|
||||
}
|
||||
}
|
||||
|
||||
// Whether a running instance's channel can be connected to; nothing is sent.
|
||||
app_running_instance_reachable :: (channel_key: string) -> bool {
|
||||
connected, channel := Shared_Channel.writer_connect(channel_key, name_not_found_is_quiet=true);
|
||||
if connected Shared_Channel.writer_disconnect(*channel);
|
||||
return connected;
|
||||
}
|
||||
|
||||
// Returns true once connected to a running instance, whether or not the message got through.
|
||||
app_bring_running_instance_to_front :: (channel_key: string, attempts: int) -> bool {
|
||||
return app_send_to_running_instance(channel_key, attempts, .Bring_To_Front);
|
||||
}
|
||||
|
||||
// Sends tag, and payload after the header, to the running instance. True once connected to it,
|
||||
// whether or not the message got through.
|
||||
app_send_to_running_instance :: (channel_key: string, attempts: int, tag: App_Shared_Memory_Tag, payload := "") -> bool {
|
||||
connected := false;
|
||||
for 1..attempts {
|
||||
connected, app.write_channel = Shared_Channel.writer_connect(channel_key, name_not_found_is_quiet=true);
|
||||
@ -712,18 +756,19 @@ app_bring_running_instance_to_front :: (channel_key: string, attempts: int) -> b
|
||||
defer Shared_Channel.writer_disconnect(*app.write_channel);
|
||||
|
||||
// Reserving fails when the queue is full, e.g. the running instance has stopped polling.
|
||||
message_success, message := Shared_Channel.writer_reserve_message(*app.write_channel, APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE);
|
||||
message_success, message := Shared_Channel.writer_reserve_message(*app.write_channel, APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE + payload.count);
|
||||
if !message_success {
|
||||
log_error("Another instance is running, but no space could be reserved in its message queue; it was not brought to the front.");
|
||||
log_error("Another instance is running, but no space could be reserved in its message queue; % was not sent.", tag);
|
||||
return true;
|
||||
}
|
||||
message.user_write_pointer[0] = cast(u8)App_Shared_Memory_Tag.Bring_To_Front;
|
||||
message.user_write_pointer[0] = cast(u8) tag;
|
||||
message.user_write_pointer[1] = 0;
|
||||
if payload.count memcpy(message.user_write_pointer + APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE, payload.data, payload.count);
|
||||
|
||||
#if USING_WINDOWS AllowSetForegroundWindow(ASFW_ANY);
|
||||
|
||||
if !Shared_Channel.writer_commit_message(*app.write_channel, message) {
|
||||
log_error("Another instance is running, but the bring-to-front message could not be committed.");
|
||||
log_error("Another instance is running, but the % message could not be committed.", tag);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
593
src/app/open_file.jai
Normal file
593
src/app/open_file.jai
Normal file
@ -0,0 +1,593 @@
|
||||
// A file named on the command line, as Explorer's Open with passes it (mcap.exe FILE): an .mcap, or
|
||||
// an image, video or Snagit .snagx such as the Import window takes.
|
||||
//
|
||||
// With no other copy of mcap running, the app starts as usual and shows the file: an .mcap in the
|
||||
// library is selected and opened in the Capture window; anything else opens in the Opened File
|
||||
// window, which says whether the library has it already and, if not, imports it into the library
|
||||
// folder chosen (app/import.jai, with the file as its source). Once in, it opens in the Capture window.
|
||||
//
|
||||
// With mcap already running, this copy starts in view mode (app.view_mode): the file alone, filling
|
||||
// the window, with the same Import, and Open in mcap, which hands the file to the running copy
|
||||
// (App_Shared_Memory_Tag.Open_File, open_file_request) and closes this one. Nothing else starts: the
|
||||
// library is not listed or watched, no model loads, and there is no tray icon, no global hotkey, and
|
||||
// no settings or window layout written. Closing the window, Escape or Ctrl-W ends it.
|
||||
|
||||
OPEN_FILE_WINDOW :: "Opened File";
|
||||
|
||||
Open_File_Kind :: enum u8 { UNKNOWN; MCAP; SNAGX; IMAGE; VIDEO; }
|
||||
|
||||
Open_File_Stage :: enum s32 {
|
||||
IDLE; // nothing opened, or it went to the Capture window
|
||||
DECODING; // the thread is reading the image
|
||||
DECODED; // the image is the main thread's; the thread is looking for the file in the library
|
||||
DONE; // the thread has ended
|
||||
}
|
||||
|
||||
open_file: struct {
|
||||
path: string; // heap; absolute, with / between its folders
|
||||
kind: Open_File_Kind;
|
||||
size: s64;
|
||||
library_path: string; // heap: the capture in the library this file is (an .mcap in it); else ""
|
||||
window_open: bool; // the Opened File window; in view mode the file is the whole window
|
||||
focus: bool; // bring the window forward next frame
|
||||
|
||||
// Read on a thread of its own: a camera RAW, or a video's frame, takes a while.
|
||||
thread: Thread;
|
||||
thread_started: bool;
|
||||
stage: Open_File_Stage; // written by the thread
|
||||
decoded: Decoded; // the thread's until DECODED; then the main thread's, kept for Ctrl-C
|
||||
details: string; // heap, as decoded: "JPEG, 4032 x 3024", "Video, 0:42, h264"
|
||||
error: string; // heap, as decoded: why it cannot be shown
|
||||
match: string; // heap, the thread's until DONE: where the library has it already, inside it
|
||||
uploaded: bool; // texture made from decoded, or tried
|
||||
texture: Gpu_Texture;
|
||||
texture_generation: u64;
|
||||
fit := Viewer_Fit.WINDOW;
|
||||
zoom: float = 100;
|
||||
|
||||
// Importing it (app/import.jai).
|
||||
folders: [..] string; // the library's folders, for the menu beside the name (heap)
|
||||
dest_buffer: [256] u8;
|
||||
importing: bool; // the importer's run is this file's
|
||||
result: string; // heap: what the import did
|
||||
result_ok: bool;
|
||||
imported_path: string; // heap: where the library has it after the import (or had it already)
|
||||
flag_started: bool; // -open-import has pressed Import
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- startup and shutdown
|
||||
|
||||
// A command-line argument as a path: absolute, with / between its folders (heap); "" for a folder.
|
||||
open_file_normalize :: (argument: string) -> string {
|
||||
path := trim(argument);
|
||||
absolute, ok := get_absolute_path(path,, temp);
|
||||
if ok path = absolute;
|
||||
if is_directory(path) return "";
|
||||
result := copy_string(path,, context.default_allocator);
|
||||
path_overwrite_separators(result, #char "/");
|
||||
return result;
|
||||
}
|
||||
|
||||
open_file_startup :: () {
|
||||
open_file.folders.allocator = context.default_allocator;
|
||||
// No OCR in view mode: the running copy reads it, once shown there and read.
|
||||
if app.view_mode importer.read_after = false;
|
||||
if app.open_file open_file_set(app.open_file);
|
||||
}
|
||||
|
||||
// On the shutdown thread: the frames have stopped running the UI.
|
||||
open_file_shutdown :: () {
|
||||
if open_file.thread_started thread_is_done(*open_file.thread, -1);
|
||||
}
|
||||
|
||||
// From the running copy's channel (main.jai): a copy in view mode handed it path.
|
||||
open_file_request :: (path: string) {
|
||||
if open_file.importing { toast_show(.Error, "Still importing %", path_filename(open_file.path)); return; }
|
||||
log("[open] % was handed over by a copy in view mode.", path);
|
||||
open_file_set(path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- opening
|
||||
|
||||
// Shows path: in the Capture window when it is a capture in the library (and the library is shown),
|
||||
// else in the Opened File window, or in view mode the whole window, read on a thread.
|
||||
open_file_set :: (path: string) {
|
||||
open_file_stop();
|
||||
push_allocator(context.default_allocator);
|
||||
free(open_file.path); free(open_file.library_path); free(open_file.result); free(open_file.imported_path);
|
||||
open_file.path = copy_string(path);
|
||||
open_file.library_path = copy_string(open_file_in_library(path));
|
||||
open_file.kind = open_file_kind(path);
|
||||
open_file.result = "";
|
||||
open_file.result_ok = false;
|
||||
open_file.imported_path = "";
|
||||
open_file.fit = .WINDOW;
|
||||
_, size, _ := file_modtime_and_size(path);
|
||||
open_file.size = size;
|
||||
log("[open] Opened % (%, % bytes)%.", path, open_file.kind, size, ifx open_file.library_path then ", in the library" else "");
|
||||
|
||||
if !app.view_mode && open_file.library_path && open_file_show_in_library(open_file.library_path) return;
|
||||
|
||||
open_file.window_open = !app.view_mode;
|
||||
open_file.focus = true;
|
||||
// The library folder it would go into: the one its folder went into before, else its folder's name.
|
||||
dest := import_dest_for(path_strip_filename(path));
|
||||
count := min(dest.count, open_file.dest_buffer.count - 1);
|
||||
memcpy(open_file.dest_buffer.data, dest.data, count);
|
||||
open_file.dest_buffer[count] = 0;
|
||||
open_file_list_folders();
|
||||
|
||||
open_file.stage = .DECODING;
|
||||
open_file.thread_started = thread_init(*open_file.thread, open_file_proc, temporary_storage_size = 16 * 1024 * 1024, name = "open file");
|
||||
if !open_file.thread_started {
|
||||
open_file.error = copy_string("the thread to read it could not start");
|
||||
open_file.stage = .DONE;
|
||||
return;
|
||||
}
|
||||
thread_start(*open_file.thread);
|
||||
}
|
||||
|
||||
// Lets go of the file shown: its thread (waited for), texture and pixels.
|
||||
open_file_stop :: () {
|
||||
if open_file.thread_started {
|
||||
thread_is_done(*open_file.thread, -1);
|
||||
thread_deinit(*open_file.thread);
|
||||
open_file.thread_started = false;
|
||||
}
|
||||
gpu_texture_release(*open_file.texture);
|
||||
viewer_free_pixels(*open_file.decoded);
|
||||
open_file.decoded = .{};
|
||||
push_allocator(context.default_allocator);
|
||||
free(open_file.details); free(open_file.error); free(open_file.match);
|
||||
open_file.details, open_file.error, open_file.match = "", "", "";
|
||||
open_file.uploaded = false;
|
||||
open_file.stage = .IDLE;
|
||||
}
|
||||
|
||||
// The capture at path in the Capture window, added to the grid if it is not there yet. Full mode only.
|
||||
open_file_show_in_library :: (path: string) -> bool {
|
||||
index := viewer_capture_find(path);
|
||||
if index < 0 && viewer_is_library_file(path) && file_exists(path) index = viewer_add_capture(path);
|
||||
if index < 0 return false;
|
||||
viewer_request_full(index);
|
||||
open_file.window_open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// path as the library names it (its folder as viewer_folder() writes it) when it is a capture in the
|
||||
// library; else "". Temp.
|
||||
open_file_in_library :: (path: string) -> string {
|
||||
folder := viewer_folder();
|
||||
#if OS == .WINDOWS inside := begins_with_nocase(path, folder); // D:\ and d:\ alike
|
||||
else inside := begins_with(path, folder);
|
||||
if !inside || path.count <= folder.count + 1 || path[folder.count] != #char "/" return "";
|
||||
in_library := tprint("%/%", folder, advance(path, folder.count + 1));
|
||||
return ifx viewer_is_library_file(in_library) then in_library else "";
|
||||
}
|
||||
|
||||
open_file_kind :: (path: string) -> Open_File_Kind {
|
||||
name := path_filename(path);
|
||||
extension := to_lower_copy(path_extension(name),, temp);
|
||||
if extension == MCAP_EXTENSION return .MCAP;
|
||||
head := import_read_head(path, IMPORT_HEAD_SIZE);
|
||||
if extension == "snagx" && head.count >= 4 && memcmp(head.data, "PK\x03\x04".data, 4) == 0 return .SNAGX;
|
||||
kind := image_kind_from_name(image_kind(head), name);
|
||||
if kind == .VIDEO || (kind == .UNKNOWN && array_find(IMPORT_VIDEO_EXTENSIONS, extension)) return .VIDEO;
|
||||
if kind == .UNKNOWN return .UNKNOWN;
|
||||
return .IMAGE;
|
||||
}
|
||||
|
||||
// The library's folders, for the menu of where to import into.
|
||||
open_file_list_folders :: () {
|
||||
for open_file.folders free(it,, context.default_allocator);
|
||||
array_reset_keeping_memory(*open_file.folders);
|
||||
visit_files(viewer_folder(), false, *open_file.folders, (info: *File_Visit_Info, folders: *[..] string) {
|
||||
if !info.is_directory || !import_name_is_valid(info.short_name) return; // not hidden ones (.thumbnails)
|
||||
#if OS != .WINDOWS if info.short_name == "deleted" return; // where deleted captures go there
|
||||
array_add(folders, copy_string(info.short_name,, context.default_allocator));
|
||||
}, visit_files = false, visit_directories = true);
|
||||
quick_sort(open_file.folders, compare);
|
||||
}
|
||||
|
||||
// The thread: the image first, shown as soon as it is there, then whether the library has the file,
|
||||
// by its hash in the library index (which, for a video, means reading it all).
|
||||
open_file_proc :: (thread: *Thread) -> s64 {
|
||||
context.allocator = context.default_allocator;
|
||||
defer atomic_swap(*open_file.stage, Open_File_Stage.DONE);
|
||||
push_allocator(temp);
|
||||
path := open_file.path; // left alone by the main thread while this runs
|
||||
|
||||
decoded: Decoded;
|
||||
decoded.failed = true;
|
||||
details, error: string;
|
||||
hash: u64;
|
||||
hashed: bool;
|
||||
to_hash: Mcap_Original_Ref; // hashed once the image is shown, when it was not hashed already
|
||||
|
||||
if open_file.kind == {
|
||||
case .MCAP;
|
||||
m, original, ok, read_error := mcap_read_without_original(path);
|
||||
if !ok { error = read_error; }
|
||||
else {
|
||||
image := m.rendered; // annotations and all, or a video's frame
|
||||
if m.rendered_kind != .OWN {
|
||||
bytes, read_ok := mcap_read_original(original);
|
||||
if read_ok image = bytes;
|
||||
else error = "its image cannot be read";
|
||||
}
|
||||
if image.count decoded = viewer_decode_image(path, image);
|
||||
details = ifx m.original_name then tprint("Capture of %", m.original_name) else "Capture";
|
||||
if !decoded.failed details = tprint("%, % x %", details, decoded.source_width, decoded.source_height);
|
||||
to_hash = original;
|
||||
}
|
||||
|
||||
case .SNAGX;
|
||||
data, ok := read_entire_file(path, log_errors = false);
|
||||
if !ok { error = "it cannot be read"; }
|
||||
else {
|
||||
m, _, converted, convert_error := snagx_to_mcap(data);
|
||||
if !converted { error = convert_error; }
|
||||
else {
|
||||
decoded = viewer_decode_image(path, ifx m.rendered_kind == .OWN then m.rendered else m.original);
|
||||
details = "Snagit capture";
|
||||
hash, hashed = import_hash(m.original), true;
|
||||
}
|
||||
}
|
||||
|
||||
case .IMAGE;
|
||||
if open_file.size > IMPORT_MAX_FILE { error = tprint("it is larger than the % MB mcap reads", IMPORT_MAX_FILE / (1024 * 1024)); }
|
||||
else {
|
||||
data, ok := read_entire_file(path, log_errors = false);
|
||||
if !ok { error = "it cannot be read"; }
|
||||
else {
|
||||
bytes := cast([] u8) data;
|
||||
kind := image_kind_from_name(image_kind(bytes), path_filename(path));
|
||||
decoded = viewer_decode_image(path, bytes, kind);
|
||||
details = image_kind_name(kind);
|
||||
if kind == .GIF {
|
||||
frames, duration_ms := gif_info(bytes);
|
||||
if frames > 1 details = tprint("Animated GIF, % frames, %", frames, viewer_duration_text(duration_ms / 1000.0));
|
||||
}
|
||||
if !decoded.failed details = tprint("%, % x %", details, decoded.source_width, decoded.source_height);
|
||||
hash, hashed = import_hash(bytes), true;
|
||||
}
|
||||
}
|
||||
|
||||
case .VIDEO;
|
||||
#if OS == .WINDOWS {
|
||||
image, video, ok, poster_error := video_poster(path);
|
||||
if !ok { error = poster_error; }
|
||||
else {
|
||||
decoded = viewer_decoded_from_image(path, image);
|
||||
details = tprint("Video, %%%, % x %", viewer_duration_text(video.duration), ifx video.codec then tprint(", %", video.codec) else "",
|
||||
ifx video.frame_rate > 0 then tprint(", % fps", FF(video.frame_rate, ifx video.frame_rate == floor(video.frame_rate) then 0 else 2)) else "",
|
||||
decoded.source_width, decoded.source_height);
|
||||
}
|
||||
to_hash = .{path = path, offset = 0, size = open_file.size};
|
||||
} else {
|
||||
error = "videos are read on Windows only";
|
||||
}
|
||||
|
||||
case;
|
||||
error = "it is not an image, a video or a capture";
|
||||
}
|
||||
if !error && decoded.failed error = "it cannot be decoded (see the Event Log)";
|
||||
|
||||
open_file.decoded = decoded;
|
||||
open_file.details = copy_string(details,, context.default_allocator);
|
||||
open_file.error = copy_string(error,, context.default_allocator);
|
||||
atomic_swap(*open_file.stage, Open_File_Stage.DECODED);
|
||||
reset_temporary_storage();
|
||||
|
||||
if !hashed && to_hash.path hash, hashed = import_hash_file(to_hash);
|
||||
if hashed open_file.match = copy_string(import_index_find(viewer_folder(), hash),, context.default_allocator);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- importing
|
||||
|
||||
open_file_import :: (dest: string) {
|
||||
if !import_start(open_file.path, dest) return; // it says why
|
||||
open_file.importing = true;
|
||||
free(open_file.result); free(open_file.imported_path);
|
||||
open_file.result, open_file.imported_path = "", "";
|
||||
}
|
||||
|
||||
// Once the import of the file has ended: what it did, and in full mode the capture in the Capture window.
|
||||
open_file_import_poll :: () {
|
||||
if !open_file.importing || importer.running return;
|
||||
if app.view_mode {
|
||||
// No import_frame here: the thread is this file's to take back, and its paths, which no grid wants.
|
||||
if importer.thread_started {
|
||||
thread_is_done(*importer.thread, -1);
|
||||
thread_deinit(*importer.thread);
|
||||
importer.thread_started = false;
|
||||
}
|
||||
} else if importer.thread_started {
|
||||
return; // import_frame takes it back first, and puts what it wrote in the grid
|
||||
}
|
||||
open_file.importing = false;
|
||||
|
||||
new_path, duplicate, failure: string;
|
||||
lock(*importer.mutex);
|
||||
if app.view_mode {
|
||||
if importer.new_paths.count new_path = copy_string(importer.new_paths[0],, temp);
|
||||
for importer.new_paths free(it,, context.default_allocator);
|
||||
array_reset_keeping_memory(*importer.new_paths);
|
||||
}
|
||||
if importer.duplicate_notes.count duplicate = copy_string(importer.duplicate_notes[0].text,, temp);
|
||||
if importer.failures.count failure = copy_string(importer.failures[0].text,, temp);
|
||||
unlock(*importer.mutex);
|
||||
if !app.view_mode && importer.new_indices.count new_path = viewer.captures[importer.new_indices[0]].path;
|
||||
|
||||
result: string;
|
||||
ok := false;
|
||||
where := new_path;
|
||||
if importer.error result = importer.error;
|
||||
else if new_path { result = tprint("Imported into %.", importer.dest_name); ok = true; }
|
||||
else if duplicate { result = tprint("Already in the library: %", duplicate); ok = true; where = tprint("%/%", viewer_folder(), duplicate); }
|
||||
else if failure result = tprint("Could not import it: %", failure);
|
||||
else if importer.cancel result = "The import was cancelled.";
|
||||
else result = "mcap does not import this kind of file.";
|
||||
log("[open] Import of % into %: %", open_file.path, importer.dest_name, result);
|
||||
|
||||
push_allocator(context.default_allocator);
|
||||
open_file.result = copy_string(result);
|
||||
open_file.result_ok = ok;
|
||||
open_file.imported_path = copy_string(where);
|
||||
if ok open_file_list_folders(); // it may have made one
|
||||
if ok && !app.view_mode open_file_show_in_library(where);
|
||||
}
|
||||
|
||||
// View mode: path handed to the running copy, which shows it; this copy then closes.
|
||||
open_file_send_to_running :: (path: string) {
|
||||
if path.count > APP_SHARED_MEMORY_CHANNEL_BYTES / 2 { toast_show(.Error, "The path is too long to hand over"); return; }
|
||||
key := app_shared_memory_channel_key();
|
||||
defer free(key);
|
||||
if app_send_to_running_instance(key, 5, .Open_File, path) app.should_exit = true;
|
||||
else toast_show(.Error, "mcap is no longer running");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- copying (view mode's Ctrl-C and Ctrl-Shift-C)
|
||||
|
||||
open_file_copy_path :: () {
|
||||
if !open_file.path return;
|
||||
path := open_file.path;
|
||||
#if OS == .WINDOWS path = replace(path, "/", "\\",, temp);
|
||||
clipboard_set_text(path);
|
||||
toast_show(.Success, "Copied the path of %", path_filename(open_file.path));
|
||||
}
|
||||
|
||||
open_file_copy_image :: () {
|
||||
#if OS != .WINDOWS {
|
||||
toast_show(.Error, "Copying images is not available on this system yet");
|
||||
return;
|
||||
}
|
||||
d := *open_file.decoded;
|
||||
if !open_file.uploaded || d.failed || !d.pixels { toast_show(.Error, "There is no image to copy"); return; }
|
||||
if !platform_copy_image_to_clipboard(.{cast(s64) d.width * d.height * 4, d.pixels}, d.width, d.height) {
|
||||
toast_show(.Error, "Could not put % on the clipboard", path_filename(open_file.path));
|
||||
return;
|
||||
}
|
||||
toast_show(.Success, "Copied % (% x %)", path_filename(open_file.path), d.width, d.height);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- each frame
|
||||
|
||||
// Full mode, from app_main_window_ui: the Opened File window.
|
||||
open_file_frame :: () { using ImGui;
|
||||
open_file_poll();
|
||||
if !open_file.window_open {
|
||||
if open_file.stage != .IDLE && !open_file.importing open_file_stop(); // closed: its image goes
|
||||
return;
|
||||
}
|
||||
SetNextWindowSize(.{900, 700}, .FirstUseEver);
|
||||
if open_file.focus { SetNextWindowFocus(); open_file.focus = false; }
|
||||
if Begin(OPEN_FILE_WINDOW, *open_file.window_open) open_file_contents();
|
||||
End();
|
||||
}
|
||||
|
||||
// View mode: the file, filling the main window.
|
||||
open_file_view_mode_ui :: () { using ImGui;
|
||||
open_file_poll();
|
||||
viewport := GetMainViewport();
|
||||
SetNextWindowPos(viewport.WorkPos);
|
||||
SetNextWindowSize(viewport.WorkSize);
|
||||
SetNextWindowViewport(viewport.ID_);
|
||||
flags := WindowFlags.NoDecoration | .NoMove | .NoSavedSettings | .NoDocking | .NoBringToFrontOnFocus;
|
||||
if Begin("View", null, flags) open_file_contents();
|
||||
End();
|
||||
if IsKeyPressed(.Escape) && !IsAnyItemActive() app.should_exit = true;
|
||||
}
|
||||
|
||||
// The thread's image, once there, as a texture (again, after the device changed); the thread taken
|
||||
// back once it has ended; the import's end.
|
||||
open_file_poll :: () {
|
||||
stage := atomic_read(*open_file.stage);
|
||||
if (stage == .DECODED || stage == .DONE) && (!open_file.uploaded || open_file.texture_generation != g_texture_generation) {
|
||||
open_file.uploaded = true;
|
||||
open_file.texture_generation = g_texture_generation;
|
||||
gpu_texture_release(*open_file.texture);
|
||||
d := *open_file.decoded;
|
||||
if !d.failed && d.pixels open_file.texture = gpu_texture_create(d.pixels, d.width, d.height);
|
||||
}
|
||||
if stage == .DONE && open_file.thread_started {
|
||||
thread_is_done(*open_file.thread, -1);
|
||||
thread_deinit(*open_file.thread);
|
||||
open_file.thread_started = false;
|
||||
}
|
||||
open_file_import_poll();
|
||||
if app.open_import_dest open_file_flag_frame();
|
||||
}
|
||||
|
||||
// -open-import DEST: once the file has been read, Import into DEST, as the button does; then exits,
|
||||
// 0 when it is in the library (imported, or there already), 1 when not.
|
||||
open_file_flag_frame :: () {
|
||||
if app.should_exit return;
|
||||
if !app.open_file || open_file.library_path {
|
||||
log("[open] -open-import: %.", ifx app.open_file then "it is in the library" else "no file was opened");
|
||||
app.exit_code = ifx app.open_file then cast(s32) 0 else 1;
|
||||
app.should_exit = true;
|
||||
return;
|
||||
}
|
||||
if !open_file.flag_started {
|
||||
if atomic_read(*open_file.stage) != .DONE return;
|
||||
open_file.flag_started = true;
|
||||
open_file_import(app.open_import_dest);
|
||||
if !open_file.importing { log_error("[open] -open-import % could not start.", app.open_import_dest); app.exit_code = 1; app.should_exit = true; }
|
||||
return;
|
||||
}
|
||||
if open_file.importing return;
|
||||
log("[open] -open-import is done: %", open_file.result);
|
||||
app.exit_code = ifx open_file.result_ok then cast(s32) 0 else 1;
|
||||
app.should_exit = true;
|
||||
}
|
||||
|
||||
open_file_contents :: () { using ImGui;
|
||||
if !open_file.path return;
|
||||
stage := atomic_read(*open_file.stage);
|
||||
shown := stage == .DECODED || stage == .DONE;
|
||||
Text("%", path_filename(open_file.path));
|
||||
SameLine();
|
||||
if shown && open_file.details TextColored(app_theme.palette.dim, "%, %", open_file.details, open_file_size_text(open_file.size));
|
||||
else TextColored(app_theme.palette.dim, "%", open_file_size_text(open_file.size));
|
||||
if !shown { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); }
|
||||
native := open_file.path;
|
||||
#if OS == .WINDOWS native = replace(native, "/", "\\",, temp);
|
||||
TextColored(app_theme.palette.dim, "%", native);
|
||||
|
||||
open_file_library_ui(stage);
|
||||
Separator();
|
||||
|
||||
if shown && open_file.error {
|
||||
TextColored(app_theme.palette.warning, "This file cannot be shown: %.", open_file.error);
|
||||
return;
|
||||
}
|
||||
if open_file.texture.handle open_file_image_ui();
|
||||
}
|
||||
|
||||
// Where the file stands with the library, and Import when it is not in it.
|
||||
open_file_library_ui :: (stage: Open_File_Stage) { using ImGui;
|
||||
library := viewer_folder();
|
||||
if open_file.library_path {
|
||||
relative := advance(open_file.library_path, library.count + 1);
|
||||
folder := trim_right(path_strip_filename(relative), "/");
|
||||
Text("In the library, in %.", ifx folder then folder else "its top folder");
|
||||
if app.view_mode { SameLine(); open_file_show_button(open_file.library_path); }
|
||||
return;
|
||||
}
|
||||
if open_file.result {
|
||||
TextColored(ifx open_file.result_ok then app_theme.palette.success else app_theme.palette.warning, "%", open_file.result);
|
||||
if open_file.result_ok {
|
||||
if app.view_mode && open_file.imported_path { SameLine(); open_file_show_button(open_file.imported_path); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
if stage == .DONE && open_file.match {
|
||||
Text("Already in the library: %", open_file.match);
|
||||
SameLine();
|
||||
where := tprint("%/%", library, open_file.match);
|
||||
if app.view_mode open_file_show_button(where);
|
||||
else if Button("Show") open_file_show_in_library(where);
|
||||
return;
|
||||
}
|
||||
if open_file.kind == .UNKNOWN { TextColored(app_theme.palette.warning, "mcap does not import this kind of file."); return; }
|
||||
if OS != .WINDOWS && open_file.kind == .VIDEO { TextColored(app_theme.palette.warning, "Videos are imported on Windows only."); return; }
|
||||
|
||||
if open_file.importing {
|
||||
if importer.phase == .INDEXING Text("Indexing the library (once; this finds duplicates): %", ocr_scan_progress_text(importer.index_done, importer.index_total));
|
||||
else Text("Importing into %...", importer.dest_name);
|
||||
SameLine();
|
||||
if SmallButton("Cancel") { atomic_swap(*importer.cancel, true); atomic_swap(*importer.paused, false); }
|
||||
return;
|
||||
}
|
||||
|
||||
Text("Import into library folder");
|
||||
SameLine();
|
||||
SetNextItemWidth(GetFontSize() * 16);
|
||||
InputTextWithHint("##dest", "e.g. Photos", open_file.dest_buffer.data, open_file.dest_buffer.count);
|
||||
SameLine();
|
||||
if BeginCombo("##folders", "", .NoPreview) {
|
||||
for open_file.folders {
|
||||
if Selectable(temp_c_string(it)) {
|
||||
count := min(it.count, open_file.dest_buffer.count - 1);
|
||||
memcpy(open_file.dest_buffer.data, it.data, count);
|
||||
open_file.dest_buffer[count] = 0;
|
||||
}
|
||||
}
|
||||
if !open_file.folders.count TextColored(app_theme.palette.dim, "The library has no folders yet");
|
||||
EndCombo();
|
||||
}
|
||||
if IsItemHovered() SetTooltip("The library's folders");
|
||||
dest := trim(to_string(open_file.dest_buffer.data));
|
||||
valid := dest.count > 0 && import_name_is_valid(dest);
|
||||
busy := importer.running; // a folder's import, from the Import window
|
||||
SameLine();
|
||||
if !valid || busy BeginDisabled();
|
||||
if Button("Import") open_file_import(dest);
|
||||
if !valid || busy EndDisabled();
|
||||
if busy && IsItemHovered(.AllowWhenDisabled) SetTooltip("After the import running now");
|
||||
if app.view_mode { SameLine(); open_file_show_button(open_file.path, "Open in mcap"); }
|
||||
|
||||
if dest && !valid TextColored(app_theme.palette.warning, "\"%\" cannot be a folder's name.", dest);
|
||||
else if dest {
|
||||
into := tprint("%/%", library, dest);
|
||||
#if OS == .WINDOWS into = replace(into, "/", "\\",, temp);
|
||||
TextColored(app_theme.palette.dim, "Into %%", into, ifx array_find(open_file.folders, dest) then "" else " (a new folder)");
|
||||
}
|
||||
if !app.view_mode Checkbox("Read text and labels once it is in", *importer.read_after);
|
||||
if stage != .DONE TextColored(app_theme.palette.dim, "Looking for it in the library...");
|
||||
}
|
||||
|
||||
// View mode: a button handing path to the running copy, which shows it.
|
||||
open_file_show_button :: (path: string, label := "Show in mcap") { using ImGui;
|
||||
if Button(temp_c_string(label)) open_file_send_to_running(path);
|
||||
if IsItemHovered() SetTooltip("Shows it in the mcap already running, and closes this window");
|
||||
}
|
||||
|
||||
// The image, sized as the Capture window sizes its own (viewer_full_window).
|
||||
open_file_image_ui :: () { using ImGui;
|
||||
fit := open_file.fit;
|
||||
Text("Fit:");
|
||||
SameLine(); if RadioButton("Window", fit == .WINDOW) fit = .WINDOW;
|
||||
SameLine(); if RadioButton("Width", fit == .WIDTH) fit = .WIDTH;
|
||||
SameLine(); if RadioButton("Height", fit == .HEIGHT) fit = .HEIGHT;
|
||||
SameLine(); if RadioButton("Off", fit == .OFF) fit = .OFF;
|
||||
open_file.fit = fit;
|
||||
SameLine();
|
||||
SetNextItemWidth(260);
|
||||
if SliderFloat("Zoom", *open_file.zoom, VIEWER_MIN_ZOOM, VIEWER_MAX_ZOOM, "%.0f%%", .Logarithmic) open_file.fit = .OFF;
|
||||
SameLine();
|
||||
if SmallButton("100%") { open_file.fit = .OFF; open_file.zoom = 100; }
|
||||
if open_file.kind == .VIDEO {
|
||||
SameLine();
|
||||
if SmallButton("Play in the default player") platform_open_file_in_default_program(open_file.path, path_strip_filename(open_file.path));
|
||||
}
|
||||
|
||||
flags := WindowFlags.HorizontalScrollbar;
|
||||
if open_file.fit == {
|
||||
case .WINDOW; flags = .NoScrollbar;
|
||||
case .WIDTH; flags |= .AlwaysVerticalScrollbar;
|
||||
case .HEIGHT; flags |= .AlwaysHorizontalScrollbar;
|
||||
}
|
||||
BeginChild("image", .{0, 0}, .None, flags);
|
||||
area := GetContentRegionAvail();
|
||||
source_w, source_h := cast(float) open_file.decoded.source_width, cast(float) open_file.decoded.source_height;
|
||||
if source_w > 0 && source_h > 0 && area.x > 0 && area.y > 0 {
|
||||
if open_file.fit == {
|
||||
case .WINDOW; open_file.zoom = 100 * min(area.x / source_w, area.y / source_h);
|
||||
case .WIDTH; open_file.zoom = 100 * area.x / source_w;
|
||||
case .HEIGHT; open_file.zoom = 100 * area.y / source_h;
|
||||
}
|
||||
}
|
||||
zoom := clamp(open_file.zoom, VIEWER_MIN_ZOOM, VIEWER_MAX_ZOOM) / 100;
|
||||
Image(gpu_texture_ref(open_file.texture), .{source_w * zoom, source_h * zoom});
|
||||
EndChild();
|
||||
}
|
||||
|
||||
// 812 KB, 4.2 MB, 1.3 GB. Temp.
|
||||
open_file_size_text :: (bytes: s64) -> string {
|
||||
if bytes < 1024 * 1024 return tprint("% KB", (bytes + 1023) / 1024);
|
||||
if bytes < 1024 * 1024 * 1024 return tprint("% MB", FF(bytes / (1024.0 * 1024.0), 1));
|
||||
return tprint("% GB", FF(bytes / (1024.0 * 1024.0 * 1024.0), 1));
|
||||
}
|
||||
@ -351,6 +351,14 @@ viewer_decode_image :: (path: string, bytes: [] u8, kind := Image_Kind.UNKNOWN)
|
||||
log_error("[viewer] Cannot decode the image in %: %", path, error);
|
||||
return result;
|
||||
}
|
||||
return viewer_decoded_from_image(path, image);
|
||||
}
|
||||
|
||||
// image, which this takes, as a texture can hold it: scaled down when too large for one.
|
||||
viewer_decoded_from_image :: (path: string, image_in: Image) -> Decoded {
|
||||
result: Decoded;
|
||||
result.failed = true;
|
||||
image := image_in;
|
||||
pixels := image.pixels;
|
||||
width, height := image.width, image.height;
|
||||
result.pixels = pixels;
|
||||
@ -446,12 +454,15 @@ viewer_frame_gpu_work :: () {
|
||||
capture.failed = true;
|
||||
} else if !capture.texture.handle {
|
||||
capture.texture = gpu_texture_create(d.pixels, d.width, d.height);
|
||||
if capture.texture.handle array_add(*viewer.with_texture, d.index);
|
||||
if capture.texture.handle && !viewer.first_preview_logged {
|
||||
viewer.first_preview_logged = true;
|
||||
log("[startup] First preview shown at % ms.", FF(startup_ms(), 1));
|
||||
if capture.texture.handle {
|
||||
array_add(*viewer.with_texture, d.index);
|
||||
if !viewer.first_preview_logged {
|
||||
viewer.first_preview_logged = true;
|
||||
log("[startup] First preview shown at % ms.", FF(startup_ms(), 1));
|
||||
}
|
||||
} else {
|
||||
capture.failed = true;
|
||||
}
|
||||
else capture.failed = true;
|
||||
}
|
||||
viewer_free_pixels(*d);
|
||||
}
|
||||
|
||||
@ -30,6 +30,8 @@ app_debug_panel_ui :: () {
|
||||
Text("Time to first frame = % ms", FF(perf.time_to_first_frame_ms, 1));
|
||||
Checkbox("Frame Times window", *app.show_frame_time_graph);
|
||||
ButtonTooltip(to_string(toggle_frame_times_hotkey));
|
||||
Checkbox("Command Line window", *app.show_help_panel);
|
||||
ButtonTooltip("The command-line options; mcap --help opens it");
|
||||
Spacing();
|
||||
|
||||
SeparatorText("Memory Usage");
|
||||
@ -104,6 +106,70 @@ app_frame_times_ui :: () {
|
||||
frame_time_graph();
|
||||
}
|
||||
|
||||
// The command-line options, opened by mcap -h or --help, or from the Debug Panel. Keep in step with
|
||||
// the Command line table in README.md.
|
||||
app_help_panel_ui :: () {
|
||||
if !app.show_help_panel return;
|
||||
using ImGui;
|
||||
|
||||
SetNextWindowSize(imgui_scale(Vector2.{760, 560}), .FirstUseEver);
|
||||
if app.help_panel_focus { SetNextWindowFocus(); app.help_panel_focus = false; }
|
||||
push_imgui_window("Command Line", p_open=*app.show_help_panel);
|
||||
|
||||
Text("% % [FILE] [options]", APP_NAME, VERSION);
|
||||
SameLine();
|
||||
if SmallButton("Copy") {
|
||||
builder: String_Builder;
|
||||
builder.allocator = temp;
|
||||
for APP_COMMAND_LINE_OPTIONS print_to_builder(*builder, "%\t%\n", it.option, it.text);
|
||||
clipboard_set_text(builder_to_string(*builder,, temp));
|
||||
toast_show(.Success, "Copied the command-line options");
|
||||
}
|
||||
TextColored(app_theme.palette.dim, "-import, -scan, -segment, -stats and -erase still open the window, to show their progress. README.md has the details.");
|
||||
Spacing();
|
||||
|
||||
if BeginTable("options", 2, .Borders | .RowBg | .SizingStretchProp | .ScrollY) {
|
||||
TableSetupColumn("Option", .WidthFixed);
|
||||
TableSetupColumn("What it does", .WidthStretch);
|
||||
TableSetupScrollFreeze(0, 1);
|
||||
TableHeadersRow();
|
||||
for APP_COMMAND_LINE_OPTIONS {
|
||||
TableNextRow();
|
||||
TableNextColumn();
|
||||
TextUnformatted(it.option);
|
||||
TableNextColumn();
|
||||
TextWrapped("%", it.text);
|
||||
}
|
||||
EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
Command_Line_Option :: struct { option: string; text: string; }
|
||||
|
||||
APP_COMMAND_LINE_OPTIONS :: Command_Line_Option.[
|
||||
.{"FILE (first)", "Opens FILE: an .mcap, image or video. With mcap already running, it opens in view mode beside it."},
|
||||
.{"-open-import DEST", "With FILE: imports it into the library folder DEST as its Import button does, then exits: 0 once the library has it (imported, or there already), else 1."},
|
||||
.{"-library FOLDER", "Shows the .mcap files in FOLDER (and its folders) instead of D:\\McapIndex\\mcap."},
|
||||
.{"-import SOURCE DEST", "Imports SOURCE, a folder or a single file, into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed."},
|
||||
.{"-search QUERY", "Starts with QUERY in the search box."},
|
||||
.{"-scan", "Scans the library (the chosen OCR model, and labels when the label model is up) once it has loaded, then exits: 0, or 1 if any capture failed."},
|
||||
.{"-segment", "Segments the images not yet segmented in the library folders set to segment, then exits: 0, or 1 if any image failed."},
|
||||
.{"-stats", "Logs the Stats window's numbers once the catalog has loaded, then exits; with -search, for the matches."},
|
||||
.{"-erase WHAT", "Erases ocr, labels, segments or all from every file in the library, then exits (for testing)."},
|
||||
.{"-record SECONDS", "Records the screen under the cursor that long (no overlay), puts it in the library, then exits: 0, or 1 if it failed."},
|
||||
.{"-record-region X Y W H", "With -record: records that region instead."},
|
||||
.{"-record-quit", "With -record: quits while still recording, to check the recording is kept."},
|
||||
.{"-record-ui-visible", "With -record: leaves the border and toolbar in screenshots, to look at them."},
|
||||
.{"-cold", "Loads the library as on a cold start (its files dropped from the system's file cache first). With -stats, a startup benchmark."},
|
||||
.{"-no-catalog-cache", "Reads every capture from its file, as without the catalog cache (for measuring); the cache is still written."},
|
||||
.{"-catalog-threads N", "Loads the catalog on N threads instead of all the background group's (for measuring)."},
|
||||
.{"-allow-multiple-instances", "Runs beside another copy instead of bringing that one forward."},
|
||||
.{"-headless", "No message boxes: a failure is logged and the app exits with 1."},
|
||||
.{"run_tests", "Runs the test suites (debug builds); with -headless, exits with 0 if all passed."},
|
||||
.{"-version", "Prints the version, then exits."},
|
||||
.{"-h, --help", "Opens this window."},
|
||||
];
|
||||
|
||||
frame_time_graph :: () {
|
||||
using ImGui;
|
||||
|
||||
|
||||
@ -184,6 +184,9 @@ WndProc :: (hWnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT
|
||||
if message == {
|
||||
case WM_SYSCOMMAND;
|
||||
SC_MINIMIZE :: 0xF020; SC_CLOSE :: 0xF060;
|
||||
// View mode (app/open_file.jai) has no tray: close ends it, and minimize is the system's own.
|
||||
if app.view_mode && (wParam & 0xFFF0) == SC_CLOSE { app.should_exit = true; return 0; }
|
||||
if app.view_mode && (wParam & 0xFFF0) == SC_MINIMIZE return DefWindowProcW(hWnd, message, wParam, lParam);
|
||||
if (wParam & 0xFFF0) == {
|
||||
case SC_CLOSE; #through;
|
||||
case SC_MINIMIZE;
|
||||
|
||||
@ -67,6 +67,9 @@ app_imgui_config_setup :: () {
|
||||
this_allocation_is_not_a_leak(app_imgui_ini_path.data);
|
||||
|
||||
io := ImGui.GetIO();
|
||||
// View mode (app/open_file.jai) is one window filling the main one: no layout to load, and the
|
||||
// running instance's is left alone.
|
||||
if app.view_mode { io.IniFilename = null; return; }
|
||||
io.IniFilename = app_imgui_ini_path.data;
|
||||
|
||||
// Loaded before the first frame, so ImGui does not go looking for the file itself.
|
||||
|
||||
@ -634,9 +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();
|
||||
// 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_path_hotkey) && !app_exit_shutdown_started() { if app.view_mode open_file_copy_path(); else viewer_copy_path(); }
|
||||
|
||||
if should_exit_this_frame {
|
||||
log("Calling exit hotkey... exiting program!");
|
||||
@ -650,7 +650,9 @@ app_check_key_combinations :: () {
|
||||
// system does, so we have to ignore it if we just restored the window,
|
||||
// OTHERWISE, the window will pop up, and then immediately minimize.
|
||||
// Very stupid!
|
||||
if (to_float64_seconds(app.frame_time - app.last_window_restore_time) > 0.15) {
|
||||
if app.view_mode {
|
||||
app.should_exit = true; // no tray: it closes, as a viewer's Ctrl-W does
|
||||
} else if (to_float64_seconds(app.frame_time - app.last_window_restore_time) > 0.15) {
|
||||
app.minimize_to_tray_this_frame = true;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user