diff --git a/packages/media/cpp/CMakeLists.txt b/packages/media/cpp/CMakeLists.txt index fd3c0b43..335873dd 100644 --- a/packages/media/cpp/CMakeLists.txt +++ b/packages/media/cpp/CMakeLists.txt @@ -153,6 +153,7 @@ if(WIN32) target_sources(media-img PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/win/register_explorer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/win/resize_ui.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/win/resize_progress_ui.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/win/media-img-win.manifest" ) endif() diff --git a/packages/media/cpp/dist/media-img.exe b/packages/media/cpp/dist/media-img.exe index 9e40d072..0e3f78e6 100644 Binary files a/packages/media/cpp/dist/media-img.exe and b/packages/media/cpp/dist/media-img.exe differ diff --git a/packages/media/cpp/dist/media-img.pdb b/packages/media/cpp/dist/media-img.pdb index 344e673a..b2358a49 100644 Binary files a/packages/media/cpp/dist/media-img.pdb and b/packages/media/cpp/dist/media-img.pdb differ diff --git a/packages/media/cpp/installer.nsi b/packages/media/cpp/installer.nsi index c2e607aa..440af7eb 100644 --- a/packages/media/cpp/installer.nsi +++ b/packages/media/cpp/installer.nsi @@ -176,6 +176,7 @@ Section "Install" SetOutPath "$INSTDIR\scripts" File "..\scripts\explorer-resize.ps1" File "..\scripts\explorer-convert.ps1" + File "..\scripts\explorer-ui.ps1" SetOutPath "$INSTDIR" WriteUninstaller "$INSTDIR\Uninstall.exe" @@ -230,6 +231,7 @@ Section "Uninstall" RMDir /r "$INSTDIR\vips-modules-8.18" Delete "$INSTDIR\scripts\explorer-resize.ps1" Delete "$INSTDIR\scripts\explorer-convert.ps1" + Delete "$INSTDIR\scripts\explorer-ui.ps1" RMDir "$INSTDIR\scripts" Delete "$INSTDIR\Uninstall.exe" RMDir "$INSTDIR" diff --git a/packages/media/cpp/src/core/glob_paths.cpp b/packages/media/cpp/src/core/glob_paths.cpp index 3ae5791c..8b3eeae3 100644 --- a/packages/media/cpp/src/core/glob_paths.cpp +++ b/packages/media/cpp/src/core/glob_paths.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace fs = std::filesystem; @@ -17,6 +18,13 @@ bool trailing_sep(const std::string &s) { return !s.empty() && (s.back() == '/' || s.back() == '\\'); } +static void trim_in_place(std::string &s) { + while (!s.empty() && std::isspace(static_cast(s.front()))) + s.erase(0, 1); + while (!s.empty() && std::isspace(static_cast(s.back()))) + s.pop_back(); +} + void replace_all(std::string &s, const std::string &from, const std::string &to) { if (from.empty()) return; @@ -75,6 +83,46 @@ std::vector expand_input_paths(const std::string &input_spec, std:: if (is_http_url(input_spec)) return {input_spec}; + /** Windows Explorer multi-select: semicolon-separated absolute paths ( ';' is invalid in filenames ). */ + if (input_spec.find(';') != std::string::npos && !has_glob_tokens(input_spec)) { + std::vector parts; + std::string cur; + for (char c : input_spec) { + if (c == ';') { + trim_in_place(cur); + if (!cur.empty()) + parts.push_back(std::move(cur)); + cur.clear(); + } else { + cur.push_back(c); + } + } + trim_in_place(cur); + if (!cur.empty()) + parts.push_back(std::move(cur)); + if (parts.size() < 2) { + err_out = "invalid semicolon-separated input list"; + return {}; + } + std::vector out; + out.reserve(parts.size()); + for (const auto &p : parts) { + fs::path fp(p); + std::error_code ec; + if (!fs::is_regular_file(fp, ec) || ec) { + err_out = "not a file: " + p; + return {}; + } + fs::path canon = fs::weakly_canonical(fs::absolute(fp), ec); + if (ec) + canon = fs::absolute(fp); + out.push_back(canon.generic_string()); + } + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; + } + if (!has_glob_tokens(input_spec) && input_spec.find("**") == std::string::npos) { fs::path p = fs::absolute(fs::path(input_spec)); std::error_code ec; diff --git a/packages/media/cpp/src/core/output_path.cpp b/packages/media/cpp/src/core/output_path.cpp index fe38c9af..fc06055e 100644 --- a/packages/media/cpp/src/core/output_path.cpp +++ b/packages/media/cpp/src/core/output_path.cpp @@ -62,7 +62,8 @@ std::string utf8_truncate_255_bytes(std::string s) { return s; } -std::string derive_basename_before_sanitize(const std::string &in, const std::string &format_cli) { +std::string derive_basename_before_sanitize(const std::string &in, const std::string &format_cli, + const std::string &stem_suffix) { const std::string ext_fmt = ext_for_format(format_cli); if (is_http_url(in)) { @@ -70,17 +71,27 @@ std::string derive_basename_before_sanitize(const std::string &in, const std::st fs::path bp(fn); std::string stem = bp.stem().string(); std::string ext = bp.extension().string(); + if (!stem_suffix.empty()) + stem += stem_suffix; if (!format_cli.empty()) return stem + "." + ext_fmt; if (ext.empty()) return stem + ".jpg"; - return fn; + if (stem_suffix.empty()) + return fn; + return stem + ext; } fs::path p(in); + std::string stem = p.stem().string(); + std::string ext = p.extension().string(); + if (!stem_suffix.empty()) + stem += stem_suffix; if (!format_cli.empty()) - return p.stem().string() + "." + ext_fmt; - return p.filename().string(); + return stem + "." + ext_fmt; + if (stem_suffix.empty()) + return p.filename().string(); + return stem + ext; } } // namespace @@ -139,7 +150,7 @@ std::string sanitize_filename(std::string input) { } std::string default_output_path_for_resize(const std::string &input_spec, const std::string &format_cli, - std::string &err_out) { + std::string &err_out, const std::string &stem_suffix) { err_out.clear(); std::string expand_err; std::vector inputs = expand_input_paths(input_spec, expand_err); @@ -156,7 +167,7 @@ std::string default_output_path_for_resize(const std::string &input_spec, const return {}; } - std::string base = derive_basename_before_sanitize(inputs[0], format_cli); + std::string base = derive_basename_before_sanitize(inputs[0], format_cli, stem_suffix); std::string safe = sanitize_filename(base); if (safe.empty()) { const std::string ext = format_cli.empty() ? std::string("jpg") : ext_for_format(format_cli); @@ -166,12 +177,17 @@ std::string default_output_path_for_resize(const std::string &input_spec, const } std::error_code ec; - fs::path cwd = fs::current_path(ec); - if (ec) { - err_out = "resize: cannot get current directory: " + ec.message(); - return {}; + fs::path out_dir; + if (is_http_url(inputs[0])) { + out_dir = fs::current_path(ec); + if (ec) { + err_out = "resize: cannot get current directory: " + ec.message(); + return {}; + } + } else { + out_dir = fs::path(inputs[0]).parent_path(); } - return (cwd / safe).lexically_normal().string(); + return (out_dir / safe).lexically_normal().string(); } } // namespace media diff --git a/packages/media/cpp/src/core/output_path.hpp b/packages/media/cpp/src/core/output_path.hpp index 6ae42699..17cfa1fb 100644 --- a/packages/media/cpp/src/core/output_path.hpp +++ b/packages/media/cpp/src/core/output_path.hpp @@ -12,11 +12,12 @@ std::string sanitize_filename(std::string input); /** * When CLI omits output: resolve exactly one input (URL or path / glob → one file) and build - * `/`. `format_cli` empty means infer extension from input; for URLs - * without an extension, default is `.jpg`. + * `/` for local files (HTTP(S) inputs still use cwd). `format_cli` + * empty means infer extension from input; for URLs without an extension, default is `.jpg`. + * `stem_suffix` is appended to the stem before the extension (e.g. "_resized"). * @return empty and set err_out on failure (e.g. glob matches multiple files). */ std::string default_output_path_for_resize(const std::string &input_spec, const std::string &format_cli, - std::string &err_out); + std::string &err_out, const std::string &stem_suffix = {}); } // namespace media diff --git a/packages/media/cpp/src/core/resize.cpp b/packages/media/cpp/src/core/resize.cpp index 70d6ddeb..f5ae5317 100644 --- a/packages/media/cpp/src/core/resize.cpp +++ b/packages/media/cpp/src/core/resize.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include #include @@ -186,7 +188,8 @@ fs::path unique_temp_download_path(std::string &err_out) { } // namespace bool resize_batch(const std::string &input_spec, const std::string &output_spec, const ResizeOptions &opt, - std::string &err_out, ResizeBatchResult *out_stats) { + std::string &err_out, ResizeBatchResult *out_stats, + const std::function &progress) { if (out_stats) { out_stats->count = 0; out_stats->outputs.clear(); @@ -201,7 +204,11 @@ bool resize_batch(const std::string &input_spec, const std::string &output_spec, err_out = "no resize jobs"; return false; } - for (const auto &job : jobs) { + const std::size_t n = jobs.size(); + for (std::size_t i = 0; i < n; ++i) { + const auto &job = jobs[i]; + if (progress) + progress(i, n); std::error_code ec; std::filesystem::create_directories(job.second.parent_path(), ec); std::string one_err; diff --git a/packages/media/cpp/src/core/resize.hpp b/packages/media/cpp/src/core/resize.hpp index 491ee165..551dbc89 100644 --- a/packages/media/cpp/src/core/resize.hpp +++ b/packages/media/cpp/src/core/resize.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -59,6 +61,12 @@ struct ResizeOptions { int url_timeout_sec = 5; /** Max redirects when fetching URL inputs. Default 20. */ int url_max_redirects = 20; + + /** + * When the resize dialog (or future callers) omit an explicit output path, append this to the stem + * before the extension (e.g. "_resized"). Used only by default_output_path_for_resize; empty = unchanged name. + */ + std::string output_stem_suffix; }; /** Defaults for `serve` / `ipc` when JSON omits `cache` / `cache_dir`. */ @@ -79,9 +87,11 @@ struct ResizeBatchResult { /** * One or more inputs (glob `*` `?` `**` or a single file) paired to output file(s) or a directory. * Stops on first failure; `err_out` names the failing input when possible. + * @param progress If set, invoked before each file with (0-based index, total count). */ bool resize_batch(const std::string& input_spec, const std::string& output_spec, const ResizeOptions& opt, - std::string& err_out, ResizeBatchResult* out_stats = nullptr); + std::string& err_out, ResizeBatchResult* out_stats = nullptr, + const std::function& progress = {}); /** Merge JSON keys into `opt` (REST / IPC). Unknown keys ignored. */ void apply_resize_options_from_json(const nlohmann::json& j, ResizeOptions& opt); diff --git a/packages/media/cpp/src/main.cpp b/packages/media/cpp/src/main.cpp index 21a146c3..8483592f 100644 --- a/packages/media/cpp/src/main.cpp +++ b/packages/media/cpp/src/main.cpp @@ -3,6 +3,18 @@ #include #include +namespace { +std::string join_src_semicolons(const std::vector &v) { + std::string s; + for (size_t i = 0; i < v.size(); ++i) { + if (i) + s += ';'; + s += v[i]; + } + return s; +} +} // namespace + #include #include @@ -18,7 +30,9 @@ #include "http/serve.hpp" #include "ipc/ipc_serve.hpp" #if defined(_WIN32) +#include "core/glob_paths.hpp" #include "win/register_explorer.hpp" +#include "win/resize_progress_ui.hpp" #include "win/resize_ui.hpp" #endif @@ -64,7 +78,7 @@ int main(int argc, char **argv) { std::string in_path; std::string out_path; - std::string src_flag; + std::vector src_list; std::string dst_flag; int max_w = 0; int max_h = 0; @@ -87,7 +101,8 @@ int main(int argc, char **argv) { resize_cmd->add_option( "output", out_path, "Output file/dir, or omit when there is exactly one input → write under cwd (sanitized name)"); - resize_cmd->add_option("--src", src_flag, "Same as positional input; glob batch with --dst"); + resize_cmd->add_option("--src", src_list, "Input (repeat for multiple); use with --dst; Explorer passes several files") + ->expected(-1); resize_cmd->add_option("--dst", dst_flag, "Same as positional output; directory if multiple inputs"); resize_cmd->add_option("--max-width", max_w, "Target / max width (0 = no limit)"); resize_cmd->add_option("--max-height", max_h, "Target / max height (0 = no limit)"); @@ -122,7 +137,7 @@ int main(int argc, char **argv) { #if defined(_WIN32) bool resize_ui = false; resize_cmd->add_flag("--ui", resize_ui, - "Windows: show native dialog to pick input/output and resize options (cannot be used with --src/--dst)"); + "Windows: native dialog for paths/options; optional --src/--dst seed the dialog"); #endif std::string host = "127.0.0.1"; @@ -159,6 +174,7 @@ int main(int argc, char **argv) { std::string reg_media_bin; std::string reg_explorer_script; std::string reg_explorer_convert_script; + std::string reg_explorer_ui_script; std::string reg_widths = "1980,1200,800,400"; auto *reg_cmd = app.add_subcommand( "register-explorer", @@ -172,6 +188,8 @@ int main(int argc, char **argv) { "Path to explorer-resize.ps1 (default: \\scripts\\explorer-resize.ps1)"); reg_cmd->add_option("--explorer-convert-script", reg_explorer_convert_script, "Path to explorer-convert.ps1 (default: \\scripts\\explorer-convert.ps1)"); + reg_cmd->add_option("--explorer-ui-script", reg_explorer_ui_script, + "Path to explorer-ui.ps1 (default: \\scripts\\explorer-ui.ps1)"); reg_cmd->add_option("--widths", reg_widths)->default_val("1980,1200,800,400"); CLI11_PARSE(app, argc, argv); @@ -183,8 +201,8 @@ int main(int argc, char **argv) { #if defined(_WIN32) if (resize_ui) { - if (!src_flag.empty() || !dst_flag.empty()) { - std::cerr << "resize: --ui cannot be used with --src or --dst\n"; + if (!src_list.empty() && !in_path.empty()) { + std::cerr << "resize: use either positional input or --src, not both\n"; return 1; } media::ResizeOptions initial{}; @@ -208,8 +226,8 @@ int main(int argc, char **argv) { initial.url_timeout_sec = url_timeout_sec; initial.url_max_redirects = url_max_redirects; - std::string ui_in = in_path; - std::string ui_out = out_path; + std::string ui_in = src_list.empty() ? in_path : join_src_semicolons(src_list); + std::string ui_out = dst_flag.empty() ? out_path : dst_flag; if (!media::win::show_resize_ui(opt, ui_in, ui_out, initial)) return 0; in_path_e = std::move(ui_in); @@ -217,20 +235,20 @@ int main(int argc, char **argv) { } else #endif { - const bool use_src_dst = !src_flag.empty() || !dst_flag.empty(); + const bool use_src_dst = !src_list.empty() || !dst_flag.empty(); if (use_src_dst) { - if (src_flag.empty() || dst_flag.empty()) { - std::cerr << "resize: --src and --dst must be used together\n"; + if (src_list.empty() || dst_flag.empty()) { + std::cerr << "resize: --src (one or more) and --dst must be used together\n"; return 1; } - in_path_e = src_flag; + in_path_e = join_src_semicolons(src_list); out_path_e = dst_flag; } else if (in_path.empty()) { std::cerr << "resize: provide input (and output, or omit output to write under the current directory)\n"; return 1; } else if (out_path.empty()) { std::string derr; - out_path_e = media::default_output_path_for_resize(in_path, format, derr); + out_path_e = media::default_output_path_for_resize(in_path, format, derr, {}); if (out_path_e.empty()) { std::cerr << derr << "\n"; return 1; @@ -264,7 +282,7 @@ int main(int argc, char **argv) { if (out_path_e.empty()) { std::string derr; - out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr); + out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr, opt.output_stem_suffix); if (out_path_e.empty()) { std::cerr << derr << "\n"; return 1; @@ -273,7 +291,24 @@ int main(int argc, char **argv) { std::string err; media::ResizeBatchResult batch; - if (!media::resize_batch(in_path_e, out_path_e, opt, err, &batch)) { +#if defined(_WIN32) + bool batch_ok; + { + std::string preview_err; + auto jobs_preview = media::pair_resize_paths(in_path_e, out_path_e, preview_err); + if (!preview_err.empty()) { + std::cerr << preview_err << "\n"; + return 1; + } + if (jobs_preview.size() > 1) + batch_ok = media::win::run_resize_batch_with_progress_ui(in_path_e, out_path_e, opt, err, &batch); + else + batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch); + } +#else + const bool batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch); +#endif + if (!batch_ok) { std::cerr << err << "\n"; return 1; } @@ -321,6 +356,7 @@ int main(int argc, char **argv) { o.media_bin = reg_media_bin; o.explorer_script = reg_explorer_script; o.explorer_convert_script = reg_explorer_convert_script; + o.explorer_ui_script = reg_explorer_ui_script; o.widths = reg_widths; return media::win::register_explorer_run(o); #else diff --git a/packages/media/cpp/src/win/register_explorer.cpp b/packages/media/cpp/src/win/register_explorer.cpp index c6c7e60d..60755cdd 100644 --- a/packages/media/cpp/src/win/register_explorer.cpp +++ b/packages/media/cpp/src/win/register_explorer.cpp @@ -90,6 +90,10 @@ std::wstring default_explorer_convert_script(const std::wstring &exe_dir) { return default_script_path(exe_dir, L"explorer-convert.ps1"); } +std::wstring default_explorer_ui_script(const std::wstring &exe_dir) { + return default_script_path(exe_dir, L"explorer-ui.ps1"); +} + bool parse_widths(const std::string &s, std::vector &out) { out.clear(); std::string cur; @@ -144,7 +148,8 @@ std::vector ext_dot_variants(const std::string &canon_lower_ascii) struct AssocTarget { std::wstring classes_suffix; - std::wstring path_token; + /** `Directory\\Background` uses `%V`; file/folder targets use `%1`. */ + bool is_background = false; }; LONG create_key(HKEY root, const std::wstring &rel, HKEY *out) { @@ -164,20 +169,30 @@ bool set_default_sz(HKEY key, const std::wstring &value) { return RegSetValueExW(key, nullptr, 0, REG_SZ, data, cb) == ERROR_SUCCESS; } -std::wstring build_resize_command_line(const std::wstring &script, const std::wstring &media, - const std::wstring &path_token, int w, bool inplace) { - std::wstring cmd = - L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + script + - L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\" -MaxWidth " + std::to_wstring(w) + - L" -Mode "; +/** Single path per invocation (Document model — default; works with cascaded `subCommands`). `%*` is not expanded. */ +static const wchar_t k_explorer_path_one[] = L" \"%1\""; +/** Folder background: working directory under cursor. */ +static const wchar_t k_explorer_background_folder[] = L" \"%V\""; + +std::wstring build_resize_command_line(const std::wstring &script, const std::wstring &media, int w, bool inplace, + const std::wstring &path_suffix) { + std::wstring cmd = L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + script + + L"\" -MediaImg \"" + media + L"\" -MaxWidth " + std::to_wstring(w) + L" -Mode "; cmd += inplace ? L"InPlace" : L"Copy"; + cmd += path_suffix; return cmd; } std::wstring build_convert_command_line(const std::wstring &convert_script, const std::wstring &media, - const std::wstring &path_token) { + const std::wstring &path_suffix) { return L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + convert_script + - L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\""; + L"\" -MediaImg \"" + media + L"\"" + path_suffix; +} + +std::wstring build_resize_ui_command_line(const std::wstring &ui_script, const std::wstring &media, + const std::wstring &path_suffix) { + return L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + ui_script + + L"\" -MediaImg \"" + media + L"\"" + path_suffix; } bool write_verbed_command(HKEY root, const std::wstring &shell_group, const std::wstring &id, @@ -201,9 +216,10 @@ bool write_verbed_command(HKEY root, const std::wstring &shell_group, const std: return ok; } -bool register_one_target(HKEY root, const std::wstring &classes_suffix, const std::wstring &path_token, - const std::wstring &group, const std::wstring &resize_script, - const std::wstring &convert_script, const std::wstring &media, const std::vector &widths) { +bool register_one_target(HKEY root, const std::wstring &classes_suffix, const std::wstring &group, + const std::wstring &resize_script, const std::wstring &convert_script, + const std::wstring &ui_script, const std::wstring &media, const std::vector &widths, + const std::wstring &path_suffix) { const std::wstring base = std::wstring(L"Software\\Classes\\") + classes_suffix; const std::wstring shell_group = base + L"\\shell\\" + group; @@ -223,7 +239,7 @@ bool register_one_target(HKEY root, const std::wstring &classes_suffix, const st auto write_resize = [&](const std::wstring &id, const std::wstring &mui_name, bool inplace) -> bool { return write_verbed_command(root, shell_group, id, mui_name, - build_resize_command_line(resize_script, media, path_token, w, inplace)); + build_resize_command_line(resize_script, media, w, inplace, path_suffix)); }; if (!write_resize(id_in, name_in, true)) @@ -232,7 +248,11 @@ bool register_one_target(HKEY root, const std::wstring &classes_suffix, const st return false; } - const std::wstring cvt_line = build_convert_command_line(convert_script, media, path_token); + const std::wstring ui_line = build_resize_ui_command_line(ui_script, media, path_suffix); + if (!write_verbed_command(root, shell_group, L"rszopt", L"Resize with options…", ui_line)) + return false; + + const std::wstring cvt_line = build_convert_command_line(convert_script, media, path_suffix); if (!write_verbed_command(root, shell_group, L"cvtjpg", L"Convert to JPG", cvt_line)) return false; @@ -257,12 +277,11 @@ std::vector build_file_association_targets() { for (const std::wstring &dot : ext_dot_variants(canon)) { AssocTarget t; t.classes_suffix = L"SystemFileAssociations\\" + dot; - t.path_token = L"%1"; out.push_back(std::move(t)); } } - out.push_back(AssocTarget{L"Directory", L"%1"}); - out.push_back(AssocTarget{L"Directory\\Background", L"%V"}); + out.push_back(AssocTarget{L"Directory", false}); + out.push_back(AssocTarget{L"Directory\\Background", true}); return out; } @@ -280,6 +299,8 @@ int register_explorer_run(const RegisterExplorerOptions &opt) { std::wstring convert_script_w = opt.explorer_convert_script.empty() ? default_explorer_convert_script(exe_dir) : utf8_to_wide(opt.explorer_convert_script); + std::wstring ui_script_w = + opt.explorer_ui_script.empty() ? default_explorer_ui_script(exe_dir) : utf8_to_wide(opt.explorer_ui_script); std::vector widths; if (!parse_widths(opt.widths, widths)) { @@ -300,6 +321,10 @@ int register_explorer_run(const RegisterExplorerOptions &opt) { std::cerr << "register-explorer: convert script not found: " << wide_to_utf8(convert_script_w) << "\n"; return 1; } + if (!file_exists_w(ui_script_w)) { + std::cerr << "register-explorer: UI script not found: " << wide_to_utf8(ui_script_w) << "\n"; + return 1; + } } const std::wstring group_w = utf8_to_wide(opt.group); @@ -341,16 +366,22 @@ int register_explorer_run(const RegisterExplorerOptions &opt) { } if (opt.dry) { - std::cout << "Dry run (sample for %1):\n"; - const std::wstring sample_tok = L"%1"; + std::cout << "Dry run (per-file `%1`; multi-select runs once per file; folder background uses `%V`):\n"; for (int w : widths) { - std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, true)) + std::cout << " " + << wide_to_utf8( + build_resize_command_line(resize_script_w, media_w, w, true, k_explorer_path_one)) << "\n"; - std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, false)) + std::cout << " " + << wide_to_utf8( + build_resize_command_line(resize_script_w, media_w, w, false, k_explorer_path_one)) << "\n"; } - std::cout << " " << wide_to_utf8(build_convert_command_line(convert_script_w, media_w, sample_tok)) << "\n"; - std::cout << "Also register folder background with %V.\n"; + std::cout << " " << wide_to_utf8(build_convert_command_line(convert_script_w, media_w, k_explorer_path_one)) + << "\n"; + std::cout << " " << wide_to_utf8(build_resize_ui_command_line(ui_script_w, media_w, k_explorer_path_one)) + << "\n"; + std::cout << " (under folder background: suffix uses %V)\n"; std::cout << "File types: " << (sizeof(k_canonical_ext) / sizeof(k_canonical_ext[0])) << " canonical extensions × 3 case variants under SystemFileAssociations.\n"; return 0; @@ -359,8 +390,9 @@ int register_explorer_run(const RegisterExplorerOptions &opt) { const std::vector targets = build_file_association_targets(); for (const auto &t : targets) { - if (!register_one_target(HKEY_CURRENT_USER, t.classes_suffix, t.path_token, group_w, resize_script_w, - convert_script_w, media_w, widths)) { + const std::wstring &suffix = t.is_background ? k_explorer_background_folder : k_explorer_path_one; + if (!register_one_target(HKEY_CURRENT_USER, t.classes_suffix, group_w, resize_script_w, convert_script_w, + ui_script_w, media_w, widths, suffix)) { std::cerr << "register-explorer: failed to register a context-menu target\n"; return 1; } diff --git a/packages/media/cpp/src/win/register_explorer.hpp b/packages/media/cpp/src/win/register_explorer.hpp index 7a36ee4d..d33fe4ba 100644 --- a/packages/media/cpp/src/win/register_explorer.hpp +++ b/packages/media/cpp/src/win/register_explorer.hpp @@ -15,6 +15,8 @@ struct RegisterExplorerOptions { std::string explorer_script; /** Empty = \\scripts\\explorer-convert.ps1 */ std::string explorer_convert_script; + /** Empty = \\scripts\\explorer-ui.ps1 (multi-select → one UI) */ + std::string explorer_ui_script; /** Comma-separated positive integers */ std::string widths{"1980,1200,800,400"}; }; diff --git a/packages/media/cpp/src/win/resize_progress_ui.cpp b/packages/media/cpp/src/win/resize_progress_ui.cpp new file mode 100644 index 00000000..a43db90b --- /dev/null +++ b/packages/media/cpp/src/win/resize_progress_ui.cpp @@ -0,0 +1,164 @@ +#include "resize_progress_ui.hpp" + +#include "core/glob_paths.hpp" +#include "core/resize.hpp" + +#include +#include + +#include +#include + +#pragma comment(lib, "Comctl32.lib") +#pragma comment(lib, "User32.lib") + +namespace media::win { + +namespace { + +enum : int { + IDC_PROG_BAR = 200, + IDC_PROG_TEXT = 201, +}; + +enum : UINT { WM_PI_PROG = WM_USER + 70, WM_PI_DONE = WM_USER + 71 }; + +struct ProgCtx { + HWND root{}; + HWND bar{}; + HWND text{}; +}; + +static LRESULT CALLBACK ProgWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + auto *ctx = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (msg) { + case WM_CREATE: + return 0; + case WM_PI_PROG: { + if (!ctx || !ctx->bar || !ctx->text) + return 0; + const std::size_t one_based = static_cast(wp); + const std::size_t total = static_cast(lp); + if (total > 0) { + SendMessageW(ctx->bar, PBM_SETRANGE32, 0, static_cast(total - 1)); + SendMessageW(ctx->bar, PBM_SETPOS, static_cast(one_based - 1), 0); + } + wchar_t line[256]{}; + swprintf_s(line, L"Processing %zu of %zu…", one_based, total); + SetWindowTextW(ctx->text, line); + return 0; + } + case WM_PI_DONE: + PostQuitMessage(0); + return 0; + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + return 0; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } +} + +static const wchar_t kProgClass[] = L"MediaImgResizeProgress"; + +static bool register_prog_class() { + static bool done = false; + if (done) + return true; + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = ProgWndProc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = reinterpret_cast(COLOR_3DFACE + 1); + wc.lpszClassName = kProgClass; + if (!RegisterClassExW(&wc)) { + if (GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return false; + } + done = true; + return true; +} + +} // namespace + +bool run_resize_batch_with_progress_ui(const std::string &input_spec, const std::string &output_spec, + const ResizeOptions &opt, std::string &err_out, ResizeBatchResult *out_stats) { + std::string pair_err; + auto jobs = media::pair_resize_paths(input_spec, output_spec, pair_err); + if (!pair_err.empty()) { + err_out = pair_err; + return false; + } + if (jobs.empty()) { + err_out = "no resize jobs"; + return false; + } + if (jobs.size() == 1) + return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats); + + INITCOMMONCONTROLSEX icc{}; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_PROGRESS_CLASS; + InitCommonControlsEx(&icc); + + if (!register_prog_class()) + return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats); + + ProgCtx ctx{}; + const int PW = 420; + const int PH = 120; + RECT r{}; + SystemParametersInfoW(SPI_GETWORKAREA, 0, &r, 0); + const int sx = r.left + ((r.right - r.left) - PW) / 2; + const int sy = r.top + ((r.bottom - r.top) - PH) / 2; + + HWND hwnd = CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, kProgClass, L"media-img — resizing", + WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN, sx, sy, PW, PH, nullptr, nullptr, + GetModuleHandleW(nullptr), nullptr); + if (!hwnd) + return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats); + + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(&ctx)); + ctx.root = hwnd; + + const int pad = 16; + ctx.text = CreateWindowExW(0, L"STATIC", L"Starting…", WS_CHILD | WS_VISIBLE, pad, pad, PW - pad * 2, 22, hwnd, + (HMENU)IDC_PROG_TEXT, nullptr, nullptr); + ctx.bar = CreateWindowExW(0, PROGRESS_CLASSW, L"", WS_CHILD | WS_VISIBLE | PBS_SMOOTH, pad, pad + 28, PW - pad * 2, + 24, hwnd, (HMENU)IDC_PROG_BAR, nullptr, nullptr); + SendMessageW(ctx.bar, PBM_SETRANGE32, 0, static_cast(jobs.size() - 1)); + + bool ok = false; + std::string worker_err; + + std::thread worker([&]() { + ok = media::resize_batch( + input_spec, output_spec, opt, worker_err, out_stats, + [&](std::size_t i, std::size_t total) { + PostMessageW(hwnd, WM_PI_PROG, static_cast(i + 1), static_cast(total)); + }); + PostMessageW(hwnd, WM_PI_DONE, 0, 0); + }); + + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + + MSG msg; + while (GetMessageW(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + worker.join(); + if (IsWindow(hwnd)) + DestroyWindow(hwnd); + + if (!ok) + err_out = std::move(worker_err); + return ok; +} + +} // namespace media::win diff --git a/packages/media/cpp/src/win/resize_progress_ui.hpp b/packages/media/cpp/src/win/resize_progress_ui.hpp new file mode 100644 index 00000000..ab363e79 --- /dev/null +++ b/packages/media/cpp/src/win/resize_progress_ui.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "core/resize.hpp" + +#include + +namespace media::win { + +/** Windows: run `resize_batch` with a native progress bar when multiple files; single job uses `resize_batch` only. */ +bool run_resize_batch_with_progress_ui(const std::string &input_spec, const std::string &output_spec, + const ResizeOptions &opt, std::string &err_out, ResizeBatchResult *out_stats); + +} // namespace media::win diff --git a/packages/media/cpp/src/win/resize_ui.cpp b/packages/media/cpp/src/win/resize_ui.cpp index cc854d42..346a97d4 100644 --- a/packages/media/cpp/src/win/resize_ui.cpp +++ b/packages/media/cpp/src/win/resize_ui.cpp @@ -56,6 +56,8 @@ enum : int { IDC_CHK_ENLARGE = 114, IDC_CHK_AUTOROT = 115, IDC_CHK_STRIP = 116, + IDC_STATIC_PRESET = 117, + IDC_COMBO_OUT_PRESET = 118, IDC_BTN_OK = IDOK, IDC_BTN_CANCEL = IDCANCEL, }; @@ -88,6 +90,8 @@ struct UiState { HWND root{}; HWND h_in{}; HWND h_out{}; + HWND h_btn_in{}; + HWND h_btn_out{}; HWND h_mw{}; HWND h_mh{}; HWND h_fit{}; @@ -95,12 +99,38 @@ struct UiState { HWND h_enlarge{}; HWND h_autorot{}; HWND h_strip{}; + HWND h_out_preset{}; + int y_preset{}; + int x0{16}; + int lw{108}; + int gap{8}; + int btn_w{90}; + int margin_r{14}; + int y_in{}; + int y_out{}; ResizeOptions *opt{}; std::string *in_path{}; std::string *out_path{}; bool accepted{false}; }; +static void layout_stretch_path_rows(UiState *st, int client_w) { + if (!st || !st->h_in || !st->h_out || client_w < 320) + return; + const int edit_x = st->x0 + st->lw + st->gap; + const int browse_x = client_w - st->margin_r - st->btn_w; + if (browse_x < edit_x + 80) + return; + const int ew = browse_x - st->gap - edit_x; + const int eh = 26; + MoveWindow(st->h_in, edit_x, st->y_in, ew, eh, TRUE); + MoveWindow(st->h_btn_in, browse_x, st->y_in, st->btn_w, 28, TRUE); + if (st->h_out_preset) + MoveWindow(st->h_out_preset, edit_x, st->y_preset, ew, 180, TRUE); + MoveWindow(st->h_out, edit_x, st->y_out, ew, eh, TRUE); + MoveWindow(st->h_btn_out, browse_x, st->y_out, st->btn_w, 28, TRUE); +} + static void set_utf8_edit(HWND h, const std::string &utf8) { SetWindowTextW(h, utf8_to_wide(utf8).c_str()); } @@ -120,8 +150,9 @@ static void browse_open(HWND owner, HWND h_edit) { OPENFILENAMEW of{}; of.lStructSize = sizeof(of); of.hwndOwner = owner; - of.lpstrFilter = L"Images\0*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic;*.jpe;*.jfif\0" - L"All\0*.*\0\0"; + of.lpstrFilter = + L"Images\0*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic;*.jpe;*.jfif;*.jf\0" + L"All\0*.*\0\0"; of.nFilterIndex = 1; of.lpstrFile = buf; of.nMaxFile = MAX_PATH * 4; @@ -153,61 +184,89 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { st = reinterpret_cast(cs->lpCreateParams); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(st)); st->root = hwnd; - const int pad = 12; - const int x0 = 20; - int y = pad + 6; - const int lw = 128; - const int gap = 10; - const int ew = 340; - const int btn_w = 96; - const int btn_h = 28; - const int eh = 24; + st->x0 = 14; + st->lw = 108; + st->gap = 8; + st->btn_w = 90; + st->margin_r = 14; + const int x0 = st->x0; + const int lw = st->lw; + const int gap = st->gap; + const int btn_w = st->btn_w; + const int margin_r = st->margin_r; + int y = 10; + const int ew = 300; + const int btn_h = 26; + const int eh = 22; const int edit_x = x0 + lw + gap; const int browse_x = edit_x + ew + gap; - CreateWindowExW(0, L"STATIC", L"Input image:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + CreateWindowExW(0, L"STATIC", L"Input image:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd, (HMENU)IDC_STATIC_IN, nullptr, nullptr); + st->y_in = y; st->h_in = CreateWindowExW( WS_EX_CLIENTEDGE, L"EDIT", L"", - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_IN, nullptr, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 2, hwnd, (HMENU)IDC_EDIT_IN, nullptr, nullptr); - CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h, - hwnd, (HMENU)IDC_BTN_IN, nullptr, nullptr); - y += 44; + st->h_btn_in = CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, + btn_w, btn_h, hwnd, (HMENU)IDC_BTN_IN, nullptr, nullptr); + y += 34; - CreateWindowExW(0, L"STATIC", L"Output (optional):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + st->y_preset = y; + CreateWindowExW(0, L"STATIC", L"Output preset:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd, + (HMENU)IDC_STATIC_PRESET, nullptr, nullptr); + st->h_out_preset = CreateWindowExW( + WS_EX_CLIENTEDGE, L"COMBOBOX", L"", + WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 1, ew, 180, hwnd, + (HMENU)IDC_COMBO_OUT_PRESET, nullptr, nullptr); + { + const wchar_t *presets[] = {L"Next to source (default name)", L"Next to source (stem_resized)", + L"Use path below (custom)"}; + for (const wchar_t *t : presets) + SendMessageW(st->h_out_preset, CB_ADDSTRING, 0, reinterpret_cast(t)); + int pr = 0; + if (!st->out_path->empty()) + pr = 2; + else if (st->opt->output_stem_suffix == "_resized") + pr = 1; + SendMessageW(st->h_out_preset, CB_SETCURSEL, static_cast(pr), 0); + } + y += 34; + + CreateWindowExW(0, L"STATIC", L"Custom path (optional):", WS_CHILD | WS_VISIBLE, x0, y + 1, lw + 20, 18, hwnd, (HMENU)IDC_STATIC_OUT, nullptr, nullptr); + st->y_out = y; st->h_out = CreateWindowExW( WS_EX_CLIENTEDGE, L"EDIT", L"", - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_OUT, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 2, hwnd, (HMENU)IDC_EDIT_OUT, nullptr, nullptr); - CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h, - hwnd, (HMENU)IDC_BTN_OUT, nullptr, nullptr); - y += 48; + st->h_btn_out = CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, + btn_w, btn_h, hwnd, (HMENU)IDC_BTN_OUT, nullptr, nullptr); + y += 34; - CreateWindowExW(0, L"STATIC", L"Max width:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + CreateWindowExW(0, L"STATIC", L"Max width:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd, (HMENU)IDC_STATIC_MW, nullptr, nullptr); wchar_t mw[32]{}; swprintf_s(mw, L"%d", st->opt->max_width); st->h_mw = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mw, - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 108, eh + 4, hwnd, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 96, eh + 2, hwnd, (HMENU)IDC_EDIT_MW, nullptr, nullptr); - const int mh_label_x = edit_x + 108 + gap + 24; - CreateWindowExW(0, L"STATIC", L"Max height:", WS_CHILD | WS_VISIBLE, mh_label_x, y + 2, 100, 20, hwnd, + const int mh_label_x = edit_x + 96 + gap + 16; + CreateWindowExW(0, L"STATIC", L"Max height:", WS_CHILD | WS_VISIBLE, mh_label_x, y + 1, 88, 18, hwnd, (HMENU)IDC_STATIC_MH, nullptr, nullptr); wchar_t mh[32]{}; swprintf_s(mh, L"%d", st->opt->max_height); st->h_mh = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mh, - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, mh_label_x + 100 + gap, y, 108, eh + 4, hwnd, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, mh_label_x + 88 + gap, y, 96, eh + 2, hwnd, (HMENU)IDC_EDIT_MH, nullptr, nullptr); - y += 44; + y += 34; - CreateWindowExW(0, L"STATIC", L"Fit:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, (HMENU)IDC_STATIC_FIT, + CreateWindowExW(0, L"STATIC", L"Fit:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd, (HMENU)IDC_STATIC_FIT, nullptr, nullptr); st->h_fit = CreateWindowExW(WS_EX_CLIENTEDGE, L"COMBOBOX", L"", - WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 2, 240, - 200, hwnd, (HMENU)IDC_COMBO_FIT, nullptr, nullptr); + WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 1, 220, + 180, hwnd, (HMENU)IDC_COMBO_FIT, nullptr, nullptr); const wchar_t *fits[] = {L"inside", L"cover", L"contain", L"fill", L"outside"}; for (const wchar_t *f : fits) SendMessageW(st->h_fit, CB_ADDSTRING, 0, reinterpret_cast(f)); @@ -226,41 +285,46 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { } SendMessageW(st->h_fit, CB_SETCURSEL, static_cast(sel), 0); } - y += 40; + y += 32; - CreateWindowExW(0, L"STATIC", L"Quality (1–100):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw + 48, 20, hwnd, + CreateWindowExW(0, L"STATIC", L"Quality (1–100):", WS_CHILD | WS_VISIBLE, x0, y + 1, lw + 40, 18, hwnd, (HMENU)IDC_STATIC_Q, nullptr, nullptr); wchar_t q[16]{}; swprintf_s(q, L"%d", st->opt->quality); st->h_q = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", q, - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 72, eh + 4, hwnd, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 64, eh + 2, hwnd, (HMENU)IDC_EDIT_Q, nullptr, nullptr); - y += 44; + y += 32; st->h_enlarge = - CreateWindowExW(0, L"BUTTON", L"Allow enlargement", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0, y, 220, - 26, hwnd, (HMENU)IDC_CHK_ENLARGE, nullptr, nullptr); + CreateWindowExW(0, L"BUTTON", L"Allow enlargement", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0, y, 210, + 22, hwnd, (HMENU)IDC_CHK_ENLARGE, nullptr, nullptr); SendMessageW(st->h_enlarge, BM_SETCHECK, st->opt->without_enlargement ? BST_UNCHECKED : BST_CHECKED, 0); st->h_autorot = - CreateWindowExW(0, L"BUTTON", L"EXIF autorotate", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0 + 240, y, 200, - 26, hwnd, (HMENU)IDC_CHK_AUTOROT, nullptr, nullptr); + CreateWindowExW(0, L"BUTTON", L"EXIF autorotate", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0 + 228, y, 188, + 22, hwnd, (HMENU)IDC_CHK_AUTOROT, nullptr, nullptr); SendMessageW(st->h_autorot, BM_SETCHECK, st->opt->autorotate ? BST_CHECKED : BST_UNCHECKED, 0); - y += 34; + y += 26; st->h_strip = CreateWindowExW(0, L"BUTTON", L"Strip metadata on save", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, - x0, y, 280, 26, hwnd, (HMENU)IDC_CHK_STRIP, nullptr, nullptr); + x0, y, 268, 22, hwnd, (HMENU)IDC_CHK_STRIP, nullptr, nullptr); SendMessageW(st->h_strip, BM_SETCHECK, st->opt->strip_metadata ? BST_CHECKED : BST_UNCHECKED, 0); - y += 42; + y += 34; - const int btn_row_w = 100; + const int btn_row_w = 92; CreateWindowExW(0, L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, browse_x - btn_row_w - gap - btn_row_w, - y, btn_row_w, 32, hwnd, (HMENU)IDC_BTN_OK, nullptr, nullptr); + y, btn_row_w, 28, hwnd, (HMENU)IDC_BTN_OK, nullptr, nullptr); CreateWindowExW(0, L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x - btn_row_w, y, btn_row_w, - 32, hwnd, (HMENU)IDC_BTN_CANCEL, nullptr, nullptr); + 28, hwnd, (HMENU)IDC_BTN_CANCEL, nullptr, nullptr); set_utf8_edit(st->h_in, *st->in_path); set_utf8_edit(st->h_out, *st->out_path); apply_message_font(hwnd); + { + RECT cr{}; + GetClientRect(hwnd, &cr); + layout_stretch_path_rows(st, cr.right); + } return 0; } case WM_COMMAND: { @@ -286,7 +350,18 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { MessageBoxW(hwnd, L"Choose an input image.", L"media-img", MB_OK | MB_ICONWARNING); return 0; } - *st->out_path = get_utf8_edit(st->h_out); + { + const int ps = static_cast(SendMessageW(st->h_out_preset, CB_GETCURSEL, 0, 0)); + st->opt->output_stem_suffix.clear(); + if (ps == 0) { + *st->out_path = {}; + } else if (ps == 1) { + *st->out_path = {}; + st->opt->output_stem_suffix = "_resized"; + } else { + *st->out_path = get_utf8_edit(st->h_out); + } + } wchar_t bmw[32]{}; GetWindowTextW(st->h_mw, bmw, 32); @@ -323,6 +398,33 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { } return 0; } + case WM_CTLCOLORSTATIC: { + HDC hdc = reinterpret_cast(wp); + SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); + SetBkColor(hdc, GetSysColor(COLOR_3DFACE)); + return reinterpret_cast(GetSysColorBrush(COLOR_3DFACE)); + } + case WM_CTLCOLOREDIT: + case WM_CTLCOLORLISTBOX: { + HDC hdc = reinterpret_cast(wp); + SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); + SetBkColor(hdc, GetSysColor(COLOR_WINDOW)); + return reinterpret_cast(GetSysColorBrush(COLOR_WINDOW)); + } + case WM_SIZE: { + if (st && (wp == SIZE_RESTORED || wp == SIZE_MAXIMIZED)) { + RECT cr{}; + GetClientRect(hwnd, &cr); + layout_stretch_path_rows(st, cr.right); + } + return DefWindowProcW(hwnd, msg, wp, lp); + } + case WM_GETMINMAXINFO: { + auto *mmi = reinterpret_cast(lp); + mmi->ptMinTrackSize.x = 460; + mmi->ptMinTrackSize.y = 300; + return 0; + } case WM_CLOSE: if (st) st->accepted = false; @@ -367,17 +469,16 @@ bool show_resize_ui(ResizeOptions &opt, std::string &input_path, std::string &ou state.in_path = &input_path; state.out_path = &output_path; - const int W = 620; - const int H = 452; + const int W = 520; + const int H = 430; RECT r{}; SystemParametersInfoW(SPI_GETWORKAREA, 0, &r, 0); const int sx = r.left + ((r.right - r.left) - W) / 2; const int sy = r.top + ((r.bottom - r.top) - H) / 2; - HWND hwnd = - CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, kClassName, L"media-img — resize", - WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN, sx, sy, W, H, nullptr, nullptr, GetModuleHandleW(nullptr), - &state); + const DWORD style = (WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX) | WS_CLIPCHILDREN; + HWND hwnd = CreateWindowExW(WS_EX_WINDOWEDGE, kClassName, L"media-img — resize", style, sx, sy, W, H, nullptr, + nullptr, GetModuleHandleW(nullptr), &state); if (!hwnd) return false;