[0.1.26] macOS phase 2: folder picker, the Trash, the temporary folder, copying images
This commit is contained in:
parent
12d0f30fc4
commit
5571c21d0b
@ -57,14 +57,15 @@ see [Screen capture](#screen-capture).
|
||||
what search needs from every file, is still loading: a line beside the count says so). **Size**
|
||||
sets the thumbnails' size. Click a thumbnail to open it in the Capture window.
|
||||
- **Delete:** hover a thumbnail and click the trash button in its corner. You are asked whether to
|
||||
move the file to the Recycle Bin or to delete it permanently, overwriting it with random bytes
|
||||
move the file to the Recycle Bin (the Trash on macOS, a `deleted` folder in the library on Linux)
|
||||
or to delete it permanently, overwriting it with random bytes
|
||||
first; "Don't ask again" keeps the choice until the app restarts. Only the `.mcap` goes: the
|
||||
`.snagx` it was converted from is untouched.
|
||||
- **Copy:** click a thumbnail to select it (it is outlined, and shown in the Capture window), then
|
||||
**Ctrl-C** copies its image, as the Capture window shows it (annotations and all; a video's
|
||||
frame), and **Ctrl-Shift-C** its full path. Neither acts while you type in the search box. They
|
||||
are `copy_image` and `copy_path` in `[hotkeys]` in `settings.config`. Copying images is Windows
|
||||
only so far.
|
||||
are `copy_image` and `copy_path` in `[hotkeys]` in `settings.config` (**Cmd** for Ctrl on macOS).
|
||||
Copying images works on Windows and macOS, not yet on Linux.
|
||||
- **Ctrl-F** puts the keyboard in the search box, its text selected (`focus_search` in `[hotkeys]`).
|
||||
- **Files added or deleted outside the app** (in Explorer, by another program) appear in the grid
|
||||
or leave it on their own, folders added, renamed or deleted too: the library is watched.
|
||||
@ -245,7 +246,7 @@ register the key, the Event Log says so; choose another, such as `Ctrl-Shift-S
|
||||
|
||||
**Import** (above the grid) brings a folder of screenshots, photos and videos into the library:
|
||||
|
||||
1. **Import from:** the folder, typed or chosen with **Browse...**. Everything under it is looked
|
||||
1. **Import from:** the folder, typed or chosen with **Browse...** (Windows and macOS). Everything under it is looked
|
||||
at, its folders too (not hidden ones, such as `.thumbnails`).
|
||||
2. **Into library folder:** the folder in the library it goes into, such as `SnagitLibraryWork` or
|
||||
`Phone`. It starts as the name of the folder imported from, or the library folder that folder went
|
||||
|
||||
@ -49,7 +49,7 @@ APP_NAME :: "mcap";
|
||||
APP_FOLDER_NAME :: "mcap";
|
||||
GIT_BRANCH :: "check";
|
||||
GIT_HASH :: "-";
|
||||
VERSION :: "0.1.25";
|
||||
VERSION :: "0.1.26";
|
||||
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.25";
|
||||
VERSION :: "0.1.26";
|
||||
// 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";
|
||||
|
||||
@ -1038,9 +1038,9 @@ import_window :: () { using ImGui;
|
||||
import_source_changed();
|
||||
}
|
||||
SameLine();
|
||||
#if OS == .WINDOWS {
|
||||
#if OS == .WINDOWS || OS == .MACOS {
|
||||
if Button("Browse...") {
|
||||
chosen := windows_choose_folder("Choose a folder to import");
|
||||
chosen := platform_choose_folder("Choose a folder to import");
|
||||
if chosen {
|
||||
count := min(chosen.count, importer.source_buffer.count - 1);
|
||||
memcpy(importer.source_buffer.data, chosen.data, count);
|
||||
|
||||
@ -183,7 +183,7 @@ open_file_list_folders :: () {
|
||||
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
|
||||
#if OS == .LINUX 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);
|
||||
@ -359,7 +359,7 @@ open_file_copy_path :: () {
|
||||
}
|
||||
|
||||
open_file_copy_image :: () {
|
||||
#if OS != .WINDOWS {
|
||||
#if OS == .LINUX {
|
||||
toast_show(.Error, "Copying images is not available on this system yet");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ screen_capture_out_path :: (name: string) -> path: string, error: string {
|
||||
return .{}, false, "not available on this system yet";
|
||||
}
|
||||
|
||||
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
|
||||
#if OS == .LINUX platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,8 +190,7 @@ screen_record_leftover_date :: (path: string) -> string {
|
||||
}
|
||||
|
||||
screen_record_folder :: () -> string {
|
||||
#if OS == .WINDOWS return tprint("%/%", windows_expand_environment("%TEMP%"), APP_FOLDER_NAME);
|
||||
else return tprint("/tmp/%", APP_FOLDER_NAME);
|
||||
return tprint("%/%", platform_temporary_folder(), APP_FOLDER_NAME);
|
||||
}
|
||||
|
||||
#if OS == .WINDOWS {
|
||||
|
||||
@ -653,13 +653,13 @@ viewer_list_files :: () -> [..] Listed_File {
|
||||
}
|
||||
|
||||
// Whether path, a file or folder, is where the library keeps captures: under it, and not hidden or
|
||||
// in the deleted folder viewer_delete_capture moves files to off Windows.
|
||||
// in the deleted folder viewer_delete_capture moves files to on Linux.
|
||||
viewer_is_library_path :: (path: string) -> bool {
|
||||
folder := viewer_folder();
|
||||
if path.count <= folder.count + 1 || !begins_with(path, folder) || path[folder.count] != #char "/" return false;
|
||||
relative := advance(path, folder.count + 1);
|
||||
if begins_with(relative, ".") || contains(relative, "/.") return false;
|
||||
#if OS != .WINDOWS if relative == "deleted" || begins_with(relative, "deleted/") return false;
|
||||
#if OS == .LINUX if relative == "deleted" || begins_with(relative, "deleted/") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -837,13 +837,13 @@ viewer_mark_removed :: (index: s32) {
|
||||
}
|
||||
|
||||
Viewer_Delete_Mode :: enum u8 {
|
||||
RECYCLE; // to the Recycle Bin, or the deleted folder elsewhere: can be undone
|
||||
RECYCLE; // to the Recycle Bin, the Trash, or the deleted folder on Linux: can be undone
|
||||
OVERWRITE; // overwritten with random bytes, renamed, then deleted for good
|
||||
}
|
||||
|
||||
// Deletes the capture's file, then takes it out of the grid. RECYCLE: to the Recycle Bin on Windows
|
||||
// (Windows asks first when the drive has none and it would be deleted for good), elsewhere into a
|
||||
// deleted folder beside it. OVERWRITE: viewer_overwrite_and_delete. False, with why, when the file
|
||||
// (Windows asks first when the drive has none and it would be deleted for good), the Trash on macOS,
|
||||
// and on Linux into a deleted folder in the library. OVERWRITE: viewer_overwrite_and_delete. False, with why, when the file
|
||||
// is still there.
|
||||
viewer_delete_capture :: (index: s32, mode: Viewer_Delete_Mode) -> ok: bool, error: string {
|
||||
capture := *viewer.captures[index];
|
||||
@ -860,6 +860,9 @@ viewer_delete_capture :: (index: s32, mode: Viewer_Delete_Mode) -> ok: bool, err
|
||||
result := SHFileOperationW(*operation);
|
||||
if operation.fAnyOperationsAborted return false, "cancelled";
|
||||
if result != 0 return false, tprint("the shell could not delete it (error 0x%)", formatInt(result, base = 16));
|
||||
} else #if OS == .MACOS {
|
||||
ok, error := macos_move_to_trash(capture.path);
|
||||
if !ok return false, tprint("it could not be moved to the Trash: %", error);
|
||||
} else {
|
||||
folder := tprint("%/deleted", viewer_folder());
|
||||
make_directory_if_it_does_not_exist(folder);
|
||||
@ -920,8 +923,9 @@ viewer_delete_now :: (index: s32) {
|
||||
else toast_show(.Success, "Overwrote and deleted %", name);
|
||||
}
|
||||
|
||||
#if OS == .WINDOWS VIEWER_TRASH_NAME :: "Recycle Bin";
|
||||
else VIEWER_TRASH_NAME :: "deleted folder";
|
||||
#if OS == .WINDOWS VIEWER_TRASH_NAME :: "Recycle Bin";
|
||||
else #if OS == .MACOS VIEWER_TRASH_NAME :: "Trash";
|
||||
else VIEWER_TRASH_NAME :: "deleted folder";
|
||||
|
||||
// The confirmation for viewer.delete_index, as a modal popup.
|
||||
viewer_delete_popup :: () { using ImGui;
|
||||
@ -1274,9 +1278,7 @@ viewer_open_proc :: (thread: *Thread) -> s64 {
|
||||
exporting := viewer.open_folder.count > 0;
|
||||
folder := viewer.open_folder;
|
||||
if !exporting {
|
||||
#if OS == .WINDOWS base := windows_expand_environment("%TEMP%");
|
||||
else base := "/tmp";
|
||||
folder = tprint("%/%", base, APP_FOLDER_NAME);
|
||||
folder = tprint("%/%", platform_temporary_folder(), APP_FOLDER_NAME);
|
||||
}
|
||||
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; }
|
||||
@ -1399,10 +1401,10 @@ viewer_export_popup :: () { using ImGui;
|
||||
if custom {
|
||||
SetNextItemWidth(GetFontSize() * 30);
|
||||
InputTextWithHint("##folder", "The folder to export to", viewer.export_buffer.data, viewer.export_buffer.count);
|
||||
#if OS == .WINDOWS {
|
||||
#if OS == .WINDOWS || OS == .MACOS {
|
||||
SameLine();
|
||||
if Button("Browse...") {
|
||||
chosen := windows_choose_folder("Choose a folder to export to");
|
||||
chosen := platform_choose_folder("Choose a folder to export to");
|
||||
if chosen viewer_export_set_buffer(chosen);
|
||||
}
|
||||
}
|
||||
@ -1464,7 +1466,7 @@ viewer_copy_image :: () {
|
||||
index := viewer_selected();
|
||||
if index < 0 { toast_show(.Error, "Select a capture to copy its image"); return; }
|
||||
capture := *viewer.captures[index];
|
||||
#if OS != .WINDOWS {
|
||||
#if OS == .LINUX {
|
||||
toast_show(.Error, "Copying images is not available on this system yet");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -9,6 +9,13 @@ platform_get_path_config_data :: inline () -> string {
|
||||
return XDG.get_path(.CONFIG_HOME);
|
||||
}
|
||||
|
||||
// $TMPDIR, else /tmp, without a separator at the end. Temp.
|
||||
platform_temporary_folder :: () -> string {
|
||||
folder := posix_getenv("TMPDIR");
|
||||
if !folder return "/tmp";
|
||||
return trim_right(copy_string(folder,, temp), "/");
|
||||
}
|
||||
|
||||
// A tag such as ja-JP from the locale variables, or "". Temp.
|
||||
platform_preferred_language :: () -> string {
|
||||
for string.["LC_ALL", "LC_MESSAGES", "LANG"] {
|
||||
|
||||
@ -70,6 +70,93 @@ platform_cjk_font_candidates :: () -> [] Cjk_Font {
|
||||
return list;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- folders, the Trash and the clipboard
|
||||
|
||||
// The user's own temporary folder, NSTemporaryDirectory() (under /var/folders, cleared by macOS),
|
||||
// not /tmp, which every user shares. No separator at the end. Temp.
|
||||
platform_temporary_folder :: () -> string {
|
||||
pool := objc_new(NSAutoreleasePool);
|
||||
defer NSAutoreleasePool.drain(pool);
|
||||
folder := copy_string(to_string(NSTemporaryDirectory()),, temp); // the NSString goes with the pool
|
||||
return trim_right(folder, "/");
|
||||
}
|
||||
|
||||
// Asks for a folder with the system's open panel, over the app's window: its path (temp), or ""
|
||||
// when none was chosen. Main thread; it runs modally until the panel is closed.
|
||||
platform_choose_folder :: (title: string) -> string {
|
||||
pool := objc_new(NSAutoreleasePool);
|
||||
defer NSAutoreleasePool.drain(pool);
|
||||
panel := NSOpenPanel.openPanel();
|
||||
if !panel return "";
|
||||
func_bool :: #type (*void, Selector, BOOL) -> void #c_call;
|
||||
func_string :: #type (*void, Selector, *NSString) -> void #c_call;
|
||||
(cast(func_bool) objc_msgSend)(panel, selector("setCanChooseDirectories:"), YES);
|
||||
(cast(func_bool) objc_msgSend)(panel, selector("setCanChooseFiles:"), NO);
|
||||
(cast(func_bool) objc_msgSend)(panel, selector("setAllowsMultipleSelection:"), NO);
|
||||
(cast(func_bool) objc_msgSend)(panel, selector("setCanCreateDirectories:"), YES);
|
||||
(cast(func_string) objc_msgSend)(panel, selector("setMessage:"), NSString.getTempString(title));
|
||||
(cast(func_string) objc_msgSend)(panel, selector("setPrompt:"), NSString.getTempString("Choose"));
|
||||
save_panel := cast(*NSSavePanel) panel;
|
||||
if NSSavePanel.runModal(save_panel) != NSModalResponseOK return "";
|
||||
url := NSSavePanel.URL(save_panel);
|
||||
if !url return "";
|
||||
return copy_string(to_string(NSURL.path(url)),, temp);
|
||||
}
|
||||
|
||||
// Moves a file to the Trash, where Finder's Put Back returns it to where it was. Error is "" or why not.
|
||||
macos_move_to_trash :: (path: string) -> ok: bool, error: string {
|
||||
pool := objc_new(NSAutoreleasePool);
|
||||
defer NSAutoreleasePool.drain(pool);
|
||||
url := NSURL.fileURLWithPath(NSString.getTempString(path));
|
||||
error: *NSError;
|
||||
func :: #type (*void, Selector, *NSURL, **NSURL, **NSError) -> BOOL #c_call;
|
||||
if (cast(func) objc_msgSend)(NSFileManager.defaultManager(), selector("trashItemAtURL:resultingItemURL:error:"), url, null, *error) == YES return true, "";
|
||||
why := ifx error then copy_string(to_string(NSError.localizedDescription(error)),, temp) else "no reason given";
|
||||
return false, why;
|
||||
}
|
||||
|
||||
// Puts RGBA pixels, top row first, on the clipboard as PNG and TIFF, in sRGB and opaque, as the
|
||||
// Windows version makes them. Every app that pastes images takes one of the two.
|
||||
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
|
||||
if width <= 0 || height <= 0 || pixels.count < cast(s64) width * height * 4 return false;
|
||||
pool := objc_new(NSAutoreleasePool);
|
||||
defer NSAutoreleasePool.drain(pool);
|
||||
|
||||
// AppKit allocates the pixels (planes null) and frees them with the rep.
|
||||
rep := NSBitmapImageRep.initWithBitmapDataPlanes(objc_alloc(NSBitmapImageRep), null, width, height, 8, 4, YES, NO, NSDeviceRGBColorSpace, cast(s64) width * 4, 32);
|
||||
if !rep return false;
|
||||
autorelease(rep);
|
||||
out := NSBitmapImageRep.bitmapData(rep);
|
||||
if !out return false;
|
||||
memcpy(out, pixels.data, cast(s64) width * height * 4);
|
||||
for 0..cast(s64) width * height - 1 out[it * 4 + 3] = 255;
|
||||
|
||||
// Tagged sRGB, as the pixels are, so a paste doesn't shift their colors on a wide-gamut screen.
|
||||
func_object :: #type (*void, Selector) -> *NSObject #c_call;
|
||||
func_retag :: #type (*void, Selector, *NSObject) -> *NSBitmapImageRep #c_call;
|
||||
srgb := (cast(func_object) objc_msgSend)(objc_getClass("NSColorSpace"), selector("sRGBColorSpace"));
|
||||
tagged := (cast(func_retag) objc_msgSend)(rep, selector("bitmapImageRepByRetaggingWithColorSpace:"), srgb);
|
||||
if tagged rep = tagged;
|
||||
|
||||
NSBitmapImageFileTypePNG :: 4;
|
||||
func_png :: #type (*void, Selector, NSUInteger, *NSObject) -> *NSData #c_call;
|
||||
properties := (cast(func_object) objc_msgSend)(objc_getClass("NSDictionary"), selector("dictionary"));
|
||||
png := (cast(func_png) objc_msgSend)(rep, selector("representationUsingType:properties:"), NSBitmapImageFileTypePNG, properties);
|
||||
tiff := cast(*NSData) (cast(func_object) objc_msgSend)(rep, selector("TIFFRepresentation"));
|
||||
if !png && !tiff return false;
|
||||
|
||||
pasteboard := NSPasteboard.generalPasteboard();
|
||||
NSPasteboard.clearContents(pasteboard);
|
||||
func_set :: #type (*void, Selector, *NSData, NSPasteboardType) -> BOOL #c_call;
|
||||
set := false;
|
||||
if png set = (cast(func_set) objc_msgSend)(pasteboard, selector("setData:forType:"), png, NSPasteboardTypePNG) == YES || set;
|
||||
if tiff set = (cast(func_set) objc_msgSend)(pasteboard, selector("setData:forType:"), tiff, NSPasteboardTypeTIFF) == YES || set;
|
||||
return set;
|
||||
}
|
||||
|
||||
NSTemporaryDirectory :: () -> *NSString #foreign macos_foundation;
|
||||
macos_foundation :: #library,system "Foundation"; // the Objective_C module keeps its own to itself
|
||||
|
||||
Mach :: #import "macOS"; // task_info
|
||||
|
||||
#import "Objective_C";
|
||||
|
||||
@ -846,6 +846,13 @@ os_log_error :: () {
|
||||
error_code := GetLastError();
|
||||
log_error(" > GetLastError code: %, %", error_code, get_error_string(error_code));
|
||||
}
|
||||
// %TEMP%, without a separator at the end. Temp.
|
||||
platform_temporary_folder :: () -> string {
|
||||
return trim_right(windows_expand_environment("%TEMP%"), "/\\");
|
||||
}
|
||||
|
||||
platform_choose_folder :: (title: string) -> string { return windows_choose_folder(title); }
|
||||
|
||||
// Asks for a folder with Explorer's own picker, over the app's window: its path (temp), or "" when
|
||||
// none was chosen.
|
||||
windows_choose_folder :: (title: string) -> string {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user