From 2eb80a4ca6d89e0c0bcc4c263d368c9ee14a097a Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Mon, 11 May 2026 23:28:28 -0700 Subject: [PATCH] Sally v1.0.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unicode path preservation through file ops, F-key launchers, and Find dialog, plus Find dialog dark-mode polish and the review-comment fixes that followed. Unicode path preservation - Panel paths held as wide; REG_SZ Unicode persistence. - Directory history with wide twins (deferred flush, decoupled serializer). - Snooper wide variants via FindFirstChangeNotificationW. - Main window registered Unicode-aware; worker source paths rebound. - F5 copy/move: CFileData::NameW allocation broadened, BuildScriptFile lossy-leaf fallback hardened, pathW refreshed after ANSI path split, SourceNameWExplicit / TargetNameWExplicit COperation flags so PopulateWidePathsFromAnsi and ReanchorWideSourcePaths preserve explicitly-set wide names. - F4 edit and Alt+F3 view routed through ::CreateProcessW with wide cmdLine + cwd; \$(FullPath)/\$(Name)/\$(Path) recovered via post-hoc lossy→wide substring substitution (both with- and without-trailing- backslash flavors of \$(Path)). Process/thread handles registered via HANDLES_ADD so the matching HANDLES(CloseHandle) finds them. - Find dialog Unicode support: open-as-Unicode dialog, wide seed for "Look in", wide-aware combo subclass, post-Transfer override message. LookInTextW is authoritative only while the Unicode edit control is enabled — invariant enforced at read site, write site, and the constructor seed moved to the override handler. Find dialog dark-mode polish - Recover Find dialog borders / clean up borders. - Skin Find combo edit rims; prevent Find combo theme recursion. - Polish Find advanced controls; tone down Find separators. Review-comment fixes rolled into the flatten - regedt plugin x86 C2695: WINAPI + virtual...override on GetCurrentPathW to match the new CPluginFSInterfaceAbstract virtual. - SalSplitWindowsPathW given a dedicated SAL_MAX_LONG_PATH wide buffer; wide mask re-converted via WideToAnsi into the ANSI path buffer past the null (mask no longer reads stale pre-splitter bytes; DBCS-safe). - AlpDX wide-capacity tracking via CStaticText::AlpDXAllocated; both SetText (ANSI) and SetTextW realloc on demand (DBCS-safe). - SalParsePathW preserves the split point: boundaryOff = end - buffer (correct for both existing-file break and new-dir loop). - HDROP paste honors pastePath when set by the shell-extension context-menu Paste (previously pasted into the panel root). - AskCannotMove ANSI args repacked to (fileName, destPath, errorText) matching the dialog handler and the wide sibling; AskCannotMoveErr wrapper updated to pass args in signature order. Verification: sally Debug x64 builds clean; plugin_regedt Release x86 builds clean (-A Win32); 466 gtest cases pass across 12 suites. Closes #56. --- .gitignore | 1 + cmake/targets/salamand_sources.cmake | 2 + src/DialogWorkerObserver.h | 142 +- src/cfgdlg.h | 2 + src/common/BuildScript.cpp | 49 +- src/common/CreateDirectoryFlow.cpp | 357 +++++ src/common/CreateDirectoryFlow.h | 47 + src/common/IWorkerObserver.h | 75 + src/common/Win32FileSystem.cpp | 2 +- src/common/WorkdirsHistorySerializer.cpp | 115 ++ src/common/WorkdirsHistorySerializer.h | 89 ++ src/common/dib.cpp | 2 +- src/common/find/FindDialogSeed.h | 131 ++ src/common/fsutil.cpp | 2 +- src/common/handles.cpp | 21 +- src/common/handles.h | 10 +- src/common/str.h | 6 +- src/common/unicode/CopyNamePolicy.h | 6 + src/common/unicode/NameRenderPolicy.h | 38 + src/common/unicode/PanelPathPolicy.h | 417 ++++++ src/common/unicode/helpers.h | 29 + src/common/widepath.cpp | 91 +- src/common/widepath.h | 35 + src/consts.h | 12 +- src/darkmode.cpp | 308 ++++- src/dialogs.cpp | 147 +- src/dialogs.h | 34 +- src/dialogs_file_transforms.cpp | 150 +- src/dialogs_highlight_registry.cpp | 21 + src/dialogs_tip_of_day.cpp | 31 +- src/execute.cpp | 9 + src/execute.h | 9 + src/files_window_actions.cpp | 380 ++++- src/files_window_clipboard_paths.cpp | 221 ++- src/files_window_copy_move.cpp | 241 +++- src/files_window_delete_email.cpp | 182 ++- src/files_window_directory_read.cpp | 39 +- src/files_window_navigation.cpp | 64 +- src/files_window_pack_unpack.cpp | 35 +- src/files_window_path_state.cpp | 188 ++- src/files_window_view_edit.cpp | 602 ++++---- src/files_window_windowproc.cpp | 17 +- src/fileswnd.h | 44 +- src/filter.cpp | 806 ++++++++++- src/find.cpp | 389 +++++- src/find.h | 44 +- src/find_dialog_actions.cpp | 70 +- src/find_dialog_results.cpp | 1600 ++++++++++++++++++++-- src/gui.cpp | 280 +++- src/gui.h | 15 +- src/lang/texts.rc2 | 10 +- src/main_window_commands_help.cpp | 81 +- src/main_window_config_persistence.cpp | 98 +- src/main_window_dragdrop_shell.cpp | 13 +- src/main_window_ui_basics.cpp | 133 +- src/mainwnd.h | 11 +- src/plugins.h | 10 + src/plugins/regedt/fs_paths.cpp | 2 +- src/plugins/regedt/regedt.h | 12 +- src/plugins/shared/spl_fs.h | 122 ++ src/plugins/shared/spl_vers.h | 6 +- src/plugins_fs_encapsulation.cpp | 221 ++- src/salamand.h | 37 +- src/salamander_entry_lifecycle.cpp | 37 +- src/salamander_path_utils.cpp | 356 +++-- src/salamander_path_validation.cpp | 489 ++++++- src/shellib.cpp | 94 ++ src/shellib.h | 3 + src/shellsup.cpp | 8 +- src/snooper.cpp | 144 +- src/snooper.h | 9 + src/stswnd.cpp | 213 ++- src/stswnd.h | 3 + src/texts.rh2 | 4 + src/tserver/dialogs.cpp | 4 +- src/tserver/registry.cpp | 21 +- src/tserver/tablist.cpp | 8 +- src/tserver/window.cpp | 57 +- src/viewer.cpp | 8 +- src/viewer.h | 10 +- src/viewer_interaction_scrolling.cpp | 30 + src/viewer_thread_buffering.cpp | 130 +- src/worker.cpp | 276 ++-- src/worker.h | 32 + 84 files changed, 9128 insertions(+), 1171 deletions(-) create mode 100644 src/common/CreateDirectoryFlow.cpp create mode 100644 src/common/CreateDirectoryFlow.h create mode 100644 src/common/WorkdirsHistorySerializer.cpp create mode 100644 src/common/WorkdirsHistorySerializer.h create mode 100644 src/common/find/FindDialogSeed.h create mode 100644 src/common/unicode/PanelPathPolicy.h diff --git a/.gitignore b/.gitignore index cbbb5e2d5..e76301868 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude/ __pycache__/ install/ # OS files diff --git a/cmake/targets/salamand_sources.cmake b/cmake/targets/salamand_sources.cmake index 984aa3509..0d2b55025 100644 --- a/cmake/targets/salamand_sources.cmake +++ b/cmake/targets/salamand_sources.cmake @@ -27,6 +27,8 @@ set(SALAMANDER_SOURCES "${SAL_SRC}/common/trace.cpp" "${SAL_SRC}/common/widepath.cpp" "${SAL_SRC}/common/fsutil.cpp" + "${SAL_SRC}/common/CreateDirectoryFlow.cpp" + "${SAL_SRC}/common/WorkdirsHistorySerializer.cpp" "${SAL_SRC}/common/Win32PathService.cpp" "${SAL_SRC}/common/Win32FileSystem.cpp" "${SAL_SRC}/common/Win32Clipboard.cpp" diff --git a/src/DialogWorkerObserver.h b/src/DialogWorkerObserver.h index eea360726..99366b467 100644 --- a/src/DialogWorkerObserver.h +++ b/src/DialogWorkerObserver.h @@ -13,6 +13,8 @@ class CDialogWorkerObserver : public IWorkerObserver { + // Non-owning bridge state. The progress dialog and worker-owned flags outlive + // the observer while a file operation is active. HWND m_hProgressDlg; HANDLE m_workerNotSuspended; BOOL* m_cancelWorker; @@ -97,16 +99,42 @@ class CDialogWorkerObserver : public IWorkerObserver return ret; } + int AskFileErrorW(const char* title, const char* fileName, const wchar_t* fileNameW, + const char* errorText) override + { + int ret = IDCANCEL; + void* data[5]; + data[0] = &ret; + data[1] = const_cast(title); + data[2] = const_cast(fileName); + data[3] = const_cast(fileNameW); + data[4] = const_cast(errorText); + SendMessage(m_hProgressDlg, WM_USER_DIALOG, 100, (LPARAM)data); + return ret; + } + int AskFileErrorById(int titleId, const char* fileName, DWORD win32Error) override { return AskFileError(LoadStr(titleId), fileName, GetErrorText(win32Error)); } + int AskFileErrorByIdW(int titleId, const char* fileName, const wchar_t* fileNameW, + DWORD win32Error) override + { + return AskFileErrorW(LoadStr(titleId), fileName, fileNameW, GetErrorText(win32Error)); + } + int AskFileErrorByIds(int titleId, const char* fileName, int errorTextId) override { return AskFileError(LoadStr(titleId), fileName, LoadStr(errorTextId)); } + int AskFileErrorByIdsW(int titleId, const char* fileName, const wchar_t* fileNameW, + int errorTextId) override + { + return AskFileErrorW(LoadStr(titleId), fileName, fileNameW, LoadStr(errorTextId)); + } + int AskOverwrite(const char* sourceName, const char* sourceInfo, const char* targetName, const char* targetInfo) override { @@ -121,6 +149,24 @@ class CDialogWorkerObserver : public IWorkerObserver return ret; } + int AskOverwriteW(const char* sourceName, const wchar_t* sourceNameW, + const char* sourceInfo, const char* targetName, + const wchar_t* targetNameW, const char* targetInfo, + bool dirOverwrite = false) override + { + int ret = IDCANCEL; + void* data[7]; + data[0] = &ret; + data[1] = const_cast(sourceName); + data[2] = const_cast(sourceNameW); + data[3] = const_cast(sourceInfo); + data[4] = const_cast(targetName); + data[5] = const_cast(targetNameW); + data[6] = const_cast(targetInfo); + SendMessage(m_hProgressDlg, WM_USER_DIALOG, dirOverwrite ? 107 : 101, (LPARAM)data); + return ret; + } + int AskHiddenOrSystem(const char* title, const char* fileName, const char* actionText) override { @@ -142,20 +188,62 @@ class CDialogWorkerObserver : public IWorkerObserver int AskCannotMove(const char* errorText, const char* fileName, const char* destPath, bool isDirectory) override { + // The dialog handler at dialogs.cpp:874-879 (case 3) and :890-894 + // (case 4) constructs CCannotMoveDlg(HWindow, IDD_..., data[1], + // data[2], data[3]) where the CCannotMoveDlg ctor takes + // (HWND, dlgId, fileName, targetPath, errorText). The wide variant + // AskCannotMoveW + cases 103/104 pack the same order (fileName at + // dataW[1], destPath at dataW[3], errorText at dataW[5]); ANSI must + // match. The prior packing here had errorText at [1] and fileName + // at [2], so the dialog showed the error text in the file-name slot + // and the destination path in the error slot. int ret = IDCANCEL; char* data[4]; data[0] = (char*)&ret; - data[1] = const_cast(errorText); - data[2] = const_cast(fileName); - data[3] = const_cast(destPath); + data[1] = const_cast(fileName); + data[2] = const_cast(destPath); + data[3] = const_cast(errorText); SendMessage(m_hProgressDlg, WM_USER_DIALOG, isDirectory ? 4 : 3, (LPARAM)data); return ret; } + int AskCannotMoveW(const char* errorText, const char* fileName, + const wchar_t* fileNameW, const char* destPath, + const wchar_t* destPathW, bool isDirectory) override + { + int ret = IDCANCEL; + void* data[6]; + data[0] = &ret; + data[1] = const_cast(fileName); + data[2] = const_cast(fileNameW); + data[3] = const_cast(destPath); + data[4] = const_cast(destPathW); + data[5] = const_cast(errorText); + SendMessage(m_hProgressDlg, WM_USER_DIALOG, isDirectory ? 104 : 103, (LPARAM)data); + return ret; + } + int AskCannotMoveErr(const char* sourceName, const char* targetName, DWORD win32Error, bool isDirectory) override { - return AskCannotMove(sourceName, targetName, GetErrorText(win32Error), isDirectory); + // AskCannotMove signature is (errorText, fileName, destPath, + // isDirectory). The wide sibling AskCannotMoveErrW already calls + // AskCannotMoveW(GetErrorText(...), sourceName, ...) in this order. + // Before this commit the wrapper passed (sourceName, targetName, + // errorText) and AskCannotMove packed those into the wrong slots + // (errorText/fileName/destPath); the two bugs composed to deliver + // the right data to the dialog. After the packing was corrected in + // this same patch, this wrapper must also pass args in the + // signature's order. + return AskCannotMove(GetErrorText(win32Error), sourceName, targetName, isDirectory); + } + + int AskCannotMoveErrW(const char* sourceName, const wchar_t* sourceNameW, + const char* targetName, const wchar_t* targetNameW, + DWORD win32Error, bool isDirectory) override + { + return AskCannotMoveW(GetErrorText(win32Error), sourceName, sourceNameW, + targetName, targetNameW, isDirectory); } void NotifyError(const char* title, const char* fileName, const char* errorText) override @@ -167,11 +255,28 @@ class CDialogWorkerObserver : public IWorkerObserver SendMessage(m_hProgressDlg, WM_USER_DIALOG, 5, (LPARAM)data); } + void NotifyErrorW(const char* title, const char* fileName, const wchar_t* fileNameW, + const char* errorText) override + { + void* data[4]; + data[0] = const_cast(title); + data[1] = const_cast(fileName); + data[2] = const_cast(fileNameW); + data[3] = const_cast(errorText); + SendMessage(m_hProgressDlg, WM_USER_DIALOG, 105, (LPARAM)data); + } + void NotifyErrorById(int titleId, const char* fileName, int detailId) override { NotifyError(LoadStr(titleId), fileName, LoadStr(detailId)); } + void NotifyErrorByIdW(int titleId, const char* fileName, const wchar_t* fileNameW, + int detailId) override + { + NotifyErrorW(LoadStr(titleId), fileName, fileNameW, LoadStr(detailId)); + } + int AskADSReadError(const char* fileName, const char* adsName) override { int ret = IDCANCEL; @@ -240,6 +345,22 @@ class CDialogWorkerObserver : public IWorkerObserver return ret; } + int AskCopyPermErrorW(const char* sourceFile, const wchar_t* sourceFileW, + const char* targetFile, const wchar_t* targetFileW, + const char* errorText) override + { + int ret = IDCANCEL; + void* data[6]; + data[0] = &ret; + data[1] = const_cast(sourceFile); + data[2] = const_cast(sourceFileW); + data[3] = const_cast(targetFile); + data[4] = const_cast(targetFileW); + data[5] = const_cast(errorText); + SendMessage(m_hProgressDlg, WM_USER_DIALOG, 110, (LPARAM)data); + return ret; + } + int AskCopyDirTimeError(const char* dirName, DWORD errorCode) override { int ret = IDCANCEL; @@ -251,6 +372,19 @@ class CDialogWorkerObserver : public IWorkerObserver return ret; } + int AskCopyDirTimeErrorW(const char* dirName, const wchar_t* dirNameW, + DWORD errorCode) override + { + int ret = IDCANCEL; + void* data[4]; + data[0] = &ret; + data[1] = const_cast(dirName); + data[2] = const_cast(dirNameW); + data[3] = (void*)(DWORD_PTR)errorCode; + SendMessage(m_hProgressDlg, WM_USER_DIALOG, 111, (LPARAM)data); + return ret; + } + int AskEncryptionLoss(bool isEncrypted, const char* fileName, bool isDir) override { int ret = IDCANCEL; diff --git a/src/cfgdlg.h b/src/cfgdlg.h index 992b324c4..b4a421743 100644 --- a/src/cfgdlg.h +++ b/src/cfgdlg.h @@ -325,12 +325,14 @@ struct CConfiguration // the history arrays are destroyed in the ClearHistory() method char* SelectHistory[SELECT_HISTORY_SIZE]; char* CopyHistory[COPY_HISTORY_SIZE]; + wchar_t* CopyHistoryW[COPY_HISTORY_SIZE]; char* EditHistory[EDIT_HISTORY_SIZE]; char* ChangeDirHistory[CHANGEDIR_HISTORY_SIZE]; char* FileListHistory[FILELIST_HISTORY_SIZE]; char* CreateDirHistory[CREATEDIR_HISTORY_SIZE]; char* QuickRenameHistory[QUICKRENAME_HISTORY_SIZE]; char* EditNewHistory[EDITNEW_HISTORY_SIZE]; + wchar_t* CreateDirHistoryW[CREATEDIR_HISTORY_SIZE]; wchar_t* QuickRenameHistoryW[QUICKRENAME_HISTORY_SIZE]; wchar_t* EditNewHistoryW[EDITNEW_HISTORY_SIZE]; char* ConvertHistory[CONVERT_HISTORY_SIZE]; diff --git a/src/common/BuildScript.cpp b/src/common/BuildScript.cpp index 29f5a822f..66462f70a 100644 --- a/src/common/BuildScript.cpp +++ b/src/common/BuildScript.cpp @@ -29,6 +29,24 @@ static char* AllocFullPath(const char* dir, const char* name) return buf; } +static std::wstring SnapshotPathW(const std::string& ansiPath, const std::wstring& widePath) +{ + if (!widePath.empty()) + return widePath; + if (ansiPath.empty()) + return std::wstring(); + + int len = MultiByteToWideChar(CP_ACP, 0, ansiPath.c_str(), -1, NULL, 0); + if (len <= 0) + return std::wstring(); + + std::wstring out((size_t)len, L'\0'); + if (MultiByteToWideChar(CP_ACP, 0, ansiPath.c_str(), -1, out.data(), len) == 0) + return std::wstring(); + out.resize((size_t)len - 1); + return out; +} + BOOL BuildScriptFromSnapshot( const CSelectionSnapshot& snapshot, const CBuildConfig& config, @@ -38,6 +56,9 @@ BOOL BuildScriptFromSnapshot( if (script == NULL) return FALSE; + const std::wstring sourcePathW = SnapshotPathW(snapshot.SourcePath, snapshot.SourcePathW); + const std::wstring targetPathW = SnapshotPathW(snapshot.TargetPath, snapshot.TargetPathW); + // Configure COperations fields from snapshot options script->IsCopyOrMoveOperation = (snapshot.Action == EActionType::Copy || snapshot.Action == EActionType::Move); script->IsCopyOperation = (snapshot.Action == EActionType::Copy); @@ -90,9 +111,9 @@ BOOL BuildScriptFromSnapshot( op.TargetName = NULL; // Set wide path if available (with \\?\ prefix for long paths) if (!item.NameW.empty()) - op.SetSourceNameW(snapshot.SourcePath.c_str(), item.NameW); + op.SetSourceNameW(sourcePathW, item.NameW); else if (!snapshot.SourcePath.empty()) - op.SetSourceNameW(snapshot.SourcePath.c_str(), std::wstring(item.Name.begin(), item.Name.end())); + op.SetSourceNameW(sourcePathW, std::wstring(item.Name.begin(), item.Name.end())); script->DirsCount++; script->Add(op); @@ -110,9 +131,9 @@ BOOL BuildScriptFromSnapshot( return FALSE; op.TargetName = NULL; if (!item.NameW.empty()) - op.SetSourceNameW(snapshot.SourcePath.c_str(), item.NameW); + op.SetSourceNameW(sourcePathW, item.NameW); else if (!snapshot.SourcePath.empty()) - op.SetSourceNameW(snapshot.SourcePath.c_str(), std::wstring(item.Name.begin(), item.Name.end())); + op.SetSourceNameW(sourcePathW, std::wstring(item.Name.begin(), item.Name.end())); script->FilesCount++; script->Add(op); @@ -145,14 +166,14 @@ BOOL BuildScriptFromSnapshot( // Set wide paths (with \\?\ prefix for long paths) if (!item.NameW.empty()) { - dirOp.SetSourceNameW(snapshot.SourcePath.c_str(), item.NameW); - dirOp.SetTargetNameW(snapshot.TargetPath.c_str(), item.NameW); + dirOp.SetSourceNameW(sourcePathW, item.NameW); + dirOp.SetTargetNameW(targetPathW, item.NameW); } else if (!snapshot.SourcePath.empty()) { std::wstring nameW(item.Name.begin(), item.Name.end()); - dirOp.SetSourceNameW(snapshot.SourcePath.c_str(), nameW); - dirOp.SetTargetNameW(snapshot.TargetPath.c_str(), nameW); + dirOp.SetSourceNameW(sourcePathW, nameW); + dirOp.SetTargetNameW(targetPathW, nameW); } script->DirsCount++; @@ -181,9 +202,9 @@ BOOL BuildScriptFromSnapshot( return FALSE; delOp.TargetName = NULL; if (!item.NameW.empty()) - delOp.SetSourceNameW(snapshot.SourcePath.c_str(), item.NameW); + delOp.SetSourceNameW(sourcePathW, item.NameW); else if (!snapshot.SourcePath.empty()) - delOp.SetSourceNameW(snapshot.SourcePath.c_str(), std::wstring(item.Name.begin(), item.Name.end())); + delOp.SetSourceNameW(sourcePathW, std::wstring(item.Name.begin(), item.Name.end())); script->Add(delOp); if (!script->IsGood()) @@ -214,14 +235,14 @@ BOOL BuildScriptFromSnapshot( // Set wide paths (with \\?\ prefix for long paths) if (!item.NameW.empty()) { - op.SetSourceNameW(snapshot.SourcePath.c_str(), item.NameW); - op.SetTargetNameW(snapshot.TargetPath.c_str(), item.NameW); + op.SetSourceNameW(sourcePathW, item.NameW); + op.SetTargetNameW(targetPathW, item.NameW); } else if (!snapshot.SourcePath.empty()) { std::wstring nameW(item.Name.begin(), item.Name.end()); - op.SetSourceNameW(snapshot.SourcePath.c_str(), nameW); - op.SetTargetNameW(snapshot.TargetPath.c_str(), nameW); + op.SetSourceNameW(sourcePathW, nameW); + op.SetTargetNameW(targetPathW, nameW); } script->FilesCount++; diff --git a/src/common/CreateDirectoryFlow.cpp b/src/common/CreateDirectoryFlow.cpp new file mode 100644 index 000000000..bcfc249cc --- /dev/null +++ b/src/common/CreateDirectoryFlow.cpp @@ -0,0 +1,357 @@ +// SPDX-FileCopyrightText: 2026 Sally Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "precomp.h" + +#include "CreateDirectoryFlow.h" + +#include "../consts.h" +#include "IFileSystem.h" +#include "fsutil.h" +#include "unicode/helpers.h" + +namespace +{ +IFileSystem* GetActiveFileSystem() +{ + return gFileSystem != NULL ? gFileSystem : GetWin32FileSystem(); +} + +bool IsManualCreateLeafInvalid(const wchar_t* path) +{ + if (path == NULL) + return false; + + const wchar_t* name = wcsrchr(path, L'\\'); + name = name != NULL ? name + 1 : path; + int nameLen = (int)wcslen(name); + return nameLen > 0 && (*name <= L' ' || name[nameLen - 1] <= L' ' || name[nameLen - 1] == L'.'); +} +} // namespace + +namespace sally::filesystem +{ +void NormalizeManualCreateInputW(std::wstring& inputPath) +{ + for (wchar_t& ch : inputPath) + { + if (ch == L'/') + ch = L'\\'; + } + RemoveDoubleBackslashesW(inputPath); + + if (!inputPath.empty()) + { + wchar_t* writable = inputPath.data(); + wchar_t* leaf = wcsrchr(writable, L'\\'); + MakeValidFileNameComponentW(leaf != NULL ? leaf + 1 : writable); + } +} + +bool PrepareCreateDirectoryTargetW(const std::wstring& inputPath, + const wchar_t* currentDir, + CreateDirectoryPlan& plan, + CreateDirectoryFailure* failure) +{ + std::wstring normalized = inputPath; + NormalizeManualCreateInputW(normalized); + + int errTextId = 0; + std::wstring nextFocus; + if (!SalGetFullNameW(normalized, &errTextId, currentDir, &nextFocus, NULL, FALSE)) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kResolve; + failure->path = inputPath; + failure->errorTextId = errTextId; + failure->errorCode = 0; + } + return false; + } + + if (normalized.size() >= SAL_MAX_LONG_PATH) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kResolve; + failure->path = normalized; + failure->errorTextId = IDS_TOOLONGPATH; + failure->errorCode = ERROR_FILENAME_EXCED_RANGE; + } + return false; + } + + std::wstring parentPath = normalized; + if (!CutDirectoryW(parentPath)) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kResolve; + failure->path = normalized; + failure->errorTextId = IDS_PATHISINVALID; + failure->errorCode = ERROR_INVALID_NAME; + } + return false; + } + + plan.fullPath = normalized; + plan.parentPath = parentPath; + plan.nextFocus = !nextFocus.empty() ? nextFocus : GetFileNameW(normalized.c_str()); + return true; +} + +bool DirectoryExistsW(const std::wstring& path) +{ + if (path.empty()) + return false; + + DWORD attrs = GetActiveFileSystem()->GetFileAttributes(path.c_str()); + return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; +} + +bool IsManualCreateLeafInvalidW(const std::wstring& path) +{ + return IsManualCreateLeafInvalid(path.c_str()); +} + +bool EnsureDirectoryTreeExistsW(const std::wstring& dirPath, + bool manualCreate, + std::wstring* firstCreatedDir, + CreateDirectoryFailure* failure) +{ + if (firstCreatedDir != NULL) + firstCreatedDir->clear(); + + if (dirPath.empty()) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path.clear(); + failure->errorCode = ERROR_INVALID_NAME; + failure->errorTextId = IDS_PATHISINVALID; + } + return false; + } + + IFileSystem* fileSystem = GetActiveFileSystem(); + DWORD attrs = fileSystem->GetFileAttributes(dirPath.c_str()); + if (attrs != INVALID_FILE_ATTRIBUTES) + { + if ((attrs & FILE_ATTRIBUTE_DIRECTORY) != 0) + return true; + + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = dirPath; + failure->errorCode = ERROR_ALREADY_EXISTS; + failure->errorTextId = 0; + } + return false; + } + + std::wstring rootPath = GetRootPathW(dirPath.c_str()); + if (rootPath.empty() || dirPath.size() <= rootPath.size()) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = dirPath; + failure->errorCode = ERROR_INVALID_NAME; + failure->errorTextId = IDS_PATHISINVALID; + } + return false; + } + + std::wstring existingPath = dirPath; + while (!existingPath.empty()) + { + attrs = fileSystem->GetFileAttributes(existingPath.c_str()); + if (attrs != INVALID_FILE_ATTRIBUTES) + break; + if (!CutDirectoryW(existingPath)) + break; + } + + if (attrs == INVALID_FILE_ATTRIBUTES) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = dirPath; + failure->errorCode = GetLastError(); + failure->errorTextId = IDS_CREATEDIRFAILED; + } + return false; + } + + if ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = existingPath; + failure->errorCode = ERROR_ALREADY_EXISTS; + failure->errorTextId = 0; + } + return false; + } + + std::wstring current = existingPath; + SalPathAddBackslashW(current); + size_t currentLen = current.size(); + const wchar_t* tail = dirPath.c_str() + existingPath.size(); + if (*tail == L'\\') + tail++; + + bool firstCreatedCaptured = false; + while (*tail != 0) + { + bool invalidName = manualCreate && *tail <= L' '; + const wchar_t* slash = wcschr(tail, L'\\'); + if (slash == NULL) + slash = tail + wcslen(tail); + + current.append(tail, slash - tail); + if (!current.empty() && (current.back() <= L' ' || current.back() == L'.')) + invalidName = true; + + if (invalidName) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = current; + failure->errorCode = ERROR_INVALID_NAME; + failure->errorTextId = 0; + } + return false; + } + + FileResult createResult = fileSystem->CreateDirectory(current.c_str()); + if (!createResult.success && createResult.errorCode != ERROR_ALREADY_EXISTS) + { + if (failure != NULL) + { + failure->stage = CreateDirectoryFailure::kParent; + failure->path = current; + failure->errorCode = createResult.errorCode; + failure->errorTextId = 0; + } + return false; + } + + if (createResult.success && firstCreatedDir != NULL && !firstCreatedCaptured) + { + *firstCreatedDir = current; + firstCreatedCaptured = true; + } + + current.push_back(L'\\'); + currentLen = current.size(); + tail = *slash == L'\\' ? slash + 1 : slash; + } + + return true; +} +} // namespace sally::filesystem + +BOOL SalCreateDirectoryExW(const wchar_t* name, DWORD* err) +{ + if (err != NULL) + *err = 0; + if (name == NULL || *name == 0) + { + if (err != NULL) + *err = ERROR_INVALID_NAME; + SetLastError(ERROR_INVALID_NAME); + return FALSE; + } + + IFileSystem* fileSystem = GetActiveFileSystem(); + std::wstring nameW(name); + std::wstring createNameW = MakeCopyWithBackslashIfNeededW(name); + bool nameUnmodified = (createNameW == nameW); + + if (::CreateDirectoryW(createNameW.c_str(), NULL)) + return TRUE; + + DWORD errLoc = GetLastError(); + if (nameUnmodified && + (errLoc == ERROR_FILE_EXISTS || errLoc == ERROR_ALREADY_EXISTS)) + { + WIN32_FIND_DATAW data; + HANDLE find = fileSystem != NULL ? fileSystem->FindFirstFile(nameW.c_str(), &data) + : INVALID_HANDLE_VALUE; + if (find != INVALID_HANDLE_VALUE) + { + HANDLES(FindClose(find)); + + const wchar_t* targetNameW = SalPathFindFileNameW(name); + char altNameA[14] = {}; + WideCharToMultiByte(CP_ACP, 0, data.cAlternateFileName, -1, altNameA, _countof(altNameA), NULL, NULL); + + std::string targetNameA = WideToAnsi(targetNameW); + std::string fileNameA = WideToAnsi(data.cFileName); + if (StrICmp(targetNameA.c_str(), altNameA) == 0 && + StrICmp(targetNameA.c_str(), fileNameA.c_str()) != 0) + { + std::wstring tmpNameW = nameW; + CutDirectoryW(tmpNameW); + SalPathAddBackslashW(tmpNameW); + size_t tmpNamePartPos = tmpNameW.size(); + std::wstring origFullNameW = tmpNameW; + SalPathAppendW(origFullNameW, data.cFileName); + tmpNameW = origFullNameW; + + DWORD num = (GetTickCount() / 10) % 0xFFF; + DWORD origFullNameAttr = GetFileAttributesW(origFullNameW.c_str()); + while (1) + { + wchar_t tmpSuffix[8]; + swprintf(tmpSuffix, _countof(tmpSuffix), L"sal%03X", num++); + tmpNameW.resize(tmpNamePartPos); + tmpNameW += tmpSuffix; + if (MoveFileW(origFullNameW.c_str(), tmpNameW.c_str())) + break; + + DWORD moveErr = GetLastError(); + if (moveErr != ERROR_FILE_EXISTS && moveErr != ERROR_ALREADY_EXISTS) + { + tmpNameW.clear(); + break; + } + } + + if (!tmpNameW.empty()) + { + BOOL createDirDone = ::CreateDirectoryW(nameW.c_str(), NULL); + if (!MoveFileW(tmpNameW.c_str(), origFullNameW.c_str())) + { + TRACE_I("Unexpected situation: unable to rename file from tmp-name to original long file name!"); + if (createDirDone) + { + if (RemoveDirectoryW(nameW.c_str())) + createDirDone = FALSE; + if (!MoveFileW(tmpNameW.c_str(), origFullNameW.c_str())) + TRACE_E("Fatal unexpected situation: unable to rename file from tmp-name to original long file name!"); + } + } + else if ((origFullNameAttr & FILE_ATTRIBUTE_ARCHIVE) == 0) + { + SetFileAttributesW(origFullNameW.c_str(), origFullNameAttr); + } + + if (createDirDone) + return TRUE; + } + } + } + } + + if (err != NULL) + *err = errLoc; + return FALSE; +} diff --git a/src/common/CreateDirectoryFlow.h b/src/common/CreateDirectoryFlow.h new file mode 100644 index 000000000..a6eb51d33 --- /dev/null +++ b/src/common/CreateDirectoryFlow.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Sally Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class IFileSystem; + +namespace sally::filesystem +{ +struct CreateDirectoryPlan +{ + std::wstring fullPath; + std::wstring parentPath; + std::wstring nextFocus; +}; + +struct CreateDirectoryFailure +{ + enum Stage + { + kResolve, + kParent, + kLeaf + } stage = kResolve; + + std::wstring path; + DWORD errorCode = 0; + int errorTextId = 0; +}; + +void NormalizeManualCreateInputW(std::wstring& inputPath); +bool PrepareCreateDirectoryTargetW(const std::wstring& inputPath, + const wchar_t* currentDir, + CreateDirectoryPlan& plan, + CreateDirectoryFailure* failure = nullptr); +bool DirectoryExistsW(const std::wstring& path); +bool IsManualCreateLeafInvalidW(const std::wstring& path); +bool EnsureDirectoryTreeExistsW(const std::wstring& dirPath, + bool manualCreate, + std::wstring* firstCreatedDir = nullptr, + CreateDirectoryFailure* failure = nullptr); +} // namespace sally::filesystem + +BOOL SalCreateDirectoryExW(const wchar_t* name, DWORD* err); diff --git a/src/common/IWorkerObserver.h b/src/common/IWorkerObserver.h index 4d021e342..7f2baa2d8 100644 --- a/src/common/IWorkerObserver.h +++ b/src/common/IWorkerObserver.h @@ -20,6 +20,11 @@ // Forward declarations for data types used by overwrite dialogs struct CProgressData; +struct CWorkerFileErrorDialogData; +struct CWorkerOverwriteDialogData; +struct CWorkerCannotMoveDialogData; +struct CWorkerCopyPermErrorDialogData; +struct CWorkerCopyDirTimeErrorDialogData; // Standard dialog return values (matching resource IDs from worker.cpp) // IDRETRY, IDYES, IDNO, IDCANCEL are from windows.h @@ -65,19 +70,47 @@ class IWorkerObserver // Generic file error with retry/skip/cancel options. // Returns IDRETRY, IDB_SKIP, IDB_SKIPALL, IDCANCEL, or IDB_IGNORE. virtual int AskFileError(const char* title, const char* fileName, const char* errorText) = 0; + virtual int AskFileErrorW(const char* title, const char* fileName, const wchar_t* fileNameW, + const char* errorText) + { + (void)fileNameW; + return AskFileError(title, fileName, errorText); + } // ID-based variant — worker passes IDS_* constant + Win32 error code, // observer handles localization (LoadStr / GetErrorText). virtual int AskFileErrorById(int titleId, const char* fileName, DWORD win32Error) = 0; + virtual int AskFileErrorByIdW(int titleId, const char* fileName, const wchar_t* fileNameW, + DWORD win32Error) + { + (void)fileNameW; + return AskFileErrorById(titleId, fileName, win32Error); + } // Variant where both title and error text are string resource IDs. virtual int AskFileErrorByIds(int titleId, const char* fileName, int errorTextId) = 0; + virtual int AskFileErrorByIdsW(int titleId, const char* fileName, const wchar_t* fileNameW, + int errorTextId) + { + (void)fileNameW; + return AskFileErrorByIds(titleId, fileName, errorTextId); + } // --- Overwrite confirmation (message ID 1) --- // Ask whether to overwrite a file. Shows source and target info. // Returns IDYES, IDB_ALL (yes to all), IDB_SKIP, IDB_SKIPALL, IDCANCEL. virtual int AskOverwrite(const char* sourceName, const char* sourceInfo, const char* targetName, const char* targetInfo) = 0; + virtual int AskOverwriteW(const char* sourceName, const wchar_t* sourceNameW, + const char* sourceInfo, const char* targetName, + const wchar_t* targetNameW, const char* targetInfo, + bool dirOverwrite = false) + { + (void)sourceNameW; + (void)targetNameW; + (void)dirOverwrite; + return AskOverwrite(sourceName, sourceInfo, targetName, targetInfo); + } // --- Hidden/system file confirmation (message ID 2) --- // Returns IDYES, IDB_ALL, IDB_SKIP, IDB_SKIPALL, IDCANCEL. @@ -91,18 +124,46 @@ class IWorkerObserver // Returns IDRETRY, IDB_SKIP, IDB_SKIPALL, IDCANCEL. virtual int AskCannotMove(const char* errorText, const char* fileName, const char* destPath, bool isDirectory) = 0; + virtual int AskCannotMoveW(const char* errorText, const char* fileName, + const wchar_t* fileNameW, const char* destPath, + const wchar_t* destPathW, bool isDirectory) + { + (void)fileNameW; + (void)destPathW; + return AskCannotMove(errorText, fileName, destPath, isDirectory); + } // Variant with Win32 error code — observer formats error text. virtual int AskCannotMoveErr(const char* sourceName, const char* targetName, DWORD win32Error, bool isDirectory) = 0; + virtual int AskCannotMoveErrW(const char* sourceName, const wchar_t* sourceNameW, + const char* targetName, const wchar_t* targetNameW, + DWORD win32Error, bool isDirectory) + { + (void)sourceNameW; + (void)targetNameW; + return AskCannotMoveErr(sourceName, targetName, win32Error, isDirectory); + } // --- Simple error notification (message ID 5) --- // Informational only — no return value expected. virtual void NotifyError(const char* title, const char* fileName, const char* errorText) = 0; + virtual void NotifyErrorW(const char* title, const char* fileName, const wchar_t* fileNameW, + const char* errorText) + { + (void)fileNameW; + NotifyError(title, fileName, errorText); + } // ID-based variant — worker passes IDS_* constants, observer handles localization. virtual void NotifyErrorById(int titleId, const char* fileName, int detailId) = 0; + virtual void NotifyErrorByIdW(int titleId, const char* fileName, const wchar_t* fileNameW, + int detailId) + { + (void)fileNameW; + NotifyErrorById(titleId, fileName, detailId); + } // --- ADS read error (message ID 6) --- // Returns IDB_SKIP, IDB_SKIPALL, IDB_IGNORE, IDB_ALL (ignore all), IDCANCEL. @@ -130,10 +191,24 @@ class IWorkerObserver // Returns IDRETRY, IDB_SKIP, IDB_SKIPALL, IDB_IGNORE, IDB_ALL (ignore all), IDCANCEL. virtual int AskCopyPermError(const char* sourceFile, const char* targetFile, const char* errorText) = 0; + virtual int AskCopyPermErrorW(const char* sourceFile, const wchar_t* sourceFileW, + const char* targetFile, const wchar_t* targetFileW, + const char* errorText) + { + (void)sourceFileW; + (void)targetFileW; + return AskCopyPermError(sourceFile, targetFile, errorText); + } // --- Error copying directory time (message ID 11) --- // Returns IDRETRY, IDB_IGNORE, IDB_ALL (ignore all), IDCANCEL. virtual int AskCopyDirTimeError(const char* dirName, DWORD errorCode) = 0; + virtual int AskCopyDirTimeErrorW(const char* dirName, const wchar_t* dirNameW, + DWORD errorCode) + { + (void)dirNameW; + return AskCopyDirTimeError(dirName, errorCode); + } // --- Confirm encryption loss (message ID 12) --- // Returns IDYES, IDB_ALL (yes to all), IDB_SKIP, IDB_SKIPALL, IDCANCEL. diff --git a/src/common/Win32FileSystem.cpp b/src/common/Win32FileSystem.cpp index 37c65e2de..7e0494a2d 100644 --- a/src/common/Win32FileSystem.cpp +++ b/src/common/Win32FileSystem.cpp @@ -333,7 +333,7 @@ class Win32FileSystem : public IFileSystem SetLastError(LastErrorOr(ERROR_INVALID_PARAMETER)); return INVALID_HANDLE_VALUE; } - return ::FindFirstFileW(lp.Get(), findData); + return HANDLES_Q(FindFirstFileW(lp.Get(), findData)); } BOOL FindNextFile(HANDLE findHandle, WIN32_FIND_DATAW* findData) override diff --git a/src/common/WorkdirsHistorySerializer.cpp b/src/common/WorkdirsHistorySerializer.cpp new file mode 100644 index 000000000..55c24ec8e --- /dev/null +++ b/src/common/WorkdirsHistorySerializer.cpp @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2026 Open Salamander Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "WorkdirsHistorySerializer.h" + +#include "common/unicode/helpers.h" + +#include + +namespace sally::path::history +{ +namespace +{ +// Format a 1-based index as a wide REG_SZ value name. Matches the legacy +// ANSI itoa(... + 1, ...) used by CPathHistory::SaveToRegistry. +std::wstring IndexValueNameW(int oneBasedIndex) +{ + wchar_t buf[16]; + swprintf_s(buf, 16, L"%d", oneBasedIndex); + return std::wstring(buf); +} +} // namespace + +std::wstring SerializeEntry(const Entry& entry) +{ + std::wstring payload = entry.nameW; + if (entry.kind == EntryKind::Archive || entry.kind == EntryKind::PluginFS) + { + payload.push_back(L':'); + payload += entry.userPartW; + } + return payload; +} + +bool ParseEntry(const std::wstring& payloadW, Entry& outEntry, PluginFSPathDetectorA detector) +{ + if (payloadW.size() < 2) + return false; + + // Disk or archive shape: leading "\\\\" (UNC) or position-1 ':' (drive). + if ((payloadW[0] == L'\\' && payloadW[1] == L'\\') || payloadW[1] == L':') + { + // Look for the *next* ':' after position 2; that's the archive + // separator. Drive-letter colons are at position 1, so starting at + // 2 skips them. + size_t sep = payloadW.find(L':', 2); + if (sep == std::wstring::npos) + { + outEntry.kind = EntryKind::Disk; + outEntry.nameW = payloadW; + outEntry.userPartW.clear(); + } + else + { + outEntry.kind = EntryKind::Archive; + outEntry.nameW = payloadW.substr(0, sep); + outEntry.userPartW = payloadW.substr(sep + 1); + } + return true; + } + + // Plugin FS shape: dispatch through the injected detector. Tests that + // don't care about plugin FS pass nullptr; production wires IsPluginFSPath. + if (detector == nullptr) + return false; + + std::string payloadA = WideToAnsi(payloadW); + std::string fsName; + std::string userPart; + if (!detector(payloadA.c_str(), fsName, userPart)) + return false; + + outEntry.kind = EntryKind::PluginFS; + outEntry.nameW = AnsiToWide(fsName.c_str()); + outEntry.userPartW = AnsiToWide(userPart.c_str()); + return true; +} + +void WriteEntries(IRegistry* reg, HKEY historyKey, const std::vector& entries) +{ + if (reg == nullptr || historyKey == nullptr) + return; + + for (size_t i = 0; i < entries.size(); ++i) + { + const std::wstring payload = SerializeEntry(entries[i]); + const std::wstring name = IndexValueNameW(static_cast(i) + 1); + reg->SetString(historyKey, name.c_str(), payload.c_str()); + } +} + +void ReadEntries(IRegistry* reg, HKEY historyKey, std::vector& outEntries, + PluginFSPathDetectorA detector) +{ + outEntries.clear(); + if (reg == nullptr || historyKey == nullptr) + return; + + for (int i = 1;; ++i) + { + const std::wstring name = IndexValueNameW(i); + std::wstring payload; + RegistryResult result = reg->GetString(historyKey, name.c_str(), payload); + if (!result.success) + break; // first miss terminates the loop — matches the legacy reader. + + Entry entry; + if (ParseEntry(payload, entry, detector)) + outEntries.push_back(std::move(entry)); + // Silently skip unparseable entries; the legacy reader emitted + // TRACE_E in this case but the serializer is intentionally + // dependency-light for headless tests. + } +} +} // namespace sally::path::history diff --git a/src/common/WorkdirsHistorySerializer.h b/src/common/WorkdirsHistorySerializer.h new file mode 100644 index 000000000..fd7d2c9eb --- /dev/null +++ b/src/common/WorkdirsHistorySerializer.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 Open Salamander Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/IRegistry.h" + +#include +#include +#include + +namespace sally::path::history +{ +// Plain-data DirHistory entry projection. +// +// CPathHistory holds CPathHistoryItem, which carries plugin FS interface +// pointers, icons, and TIndirectArray storage — all heavyweight and +// hard to construct in tests. The serializer operates on this minimal +// projection so the registry round-trip can be exercised without any +// of that machinery. +// +// kind matches CPathHistoryItem's `Type` field (0 = disk, 1 = archive, +// 2 = plugin FS) so adapters in CPathHistory can map both directions +// trivially. +enum class EntryKind +{ + Disk = 0, + Archive = 1, + PluginFS = 2, +}; + +struct Entry +{ + EntryKind kind; + std::wstring nameW; + std::wstring userPartW; // empty for Disk +}; + +// Serialize one Entry to the wide REG_SZ payload. +// +// Disk: nameW alone. +// Archive / PluginFS: nameW + L":" + userPartW (matches the legacy ANSI +// format CPathHistory::SaveToRegistry produced, so a Win32 ANSI reader +// of the new wide value still gets a parseable — if lossy — string). +std::wstring SerializeEntry(const Entry& entry); + +// Plugin-FS detector callback. The serializer is decoupled from the rest +// of the sally object graph so headless tests can link against a tiny +// dependency surface; the production CPathHistory adapter wires +// IsPluginFSPath through this hook. When the callback returns true, it +// must populate outFsName and outUserPart with ANSI-narrow strings (plugin +// FS names and user-parts are ANSI today). Tests that don't care about +// plugin FS pass nullptr and the parser rejects non-disk shapes. +using PluginFSPathDetectorA = bool (*)(const char* path, + std::string& outFsName, + std::string& outUserPart); + +// Parse a wide REG_SZ payload back into an Entry. +// +// Detection rules (mirror the legacy ANSI parser): +// - leading "\\\\" or position-1 ':' → disk root or archive depending +// on whether a ':' separator is present at or after position 2. +// - otherwise → plugin FS via the injected detector (when supplied); +// plugin FS user-parts are still ANSI today, so the detector hands +// back ANSI strings that ParseEntry widens. +// +// Returns false when the payload is too short or unrecognisable, or when +// the plugin FS shape is encountered with no detector; the +// CPathHistory::LoadFromRegistry caller logs and skips such entries. +bool ParseEntry(const std::wstring& payloadW, Entry& outEntry, + PluginFSPathDetectorA detector = nullptr); + +// Round-trip helpers used by CPathHistory::SaveToRegistry / LoadFromRegistry. +// +// WriteEntries clears the existing values and writes one REG_SZ Unicode +// per entry, indexed "1", "2", ... — same indexing scheme the legacy +// ANSI writer used. +// +// ReadEntries iterates "1", "2", ... and stops at the first missing +// index (matches the legacy reader's termination rule). Entries that +// fail ParseEntry are silently skipped — the caller has already seen +// any older invalid data and should not re-warn. +// +// Both helpers operate on `IRegistry*` so a test harness can swap in a +// mock if it wants; the production path uses the global gRegistry. +void WriteEntries(IRegistry* reg, HKEY historyKey, const std::vector& entries); +void ReadEntries(IRegistry* reg, HKEY historyKey, std::vector& outEntries, + PluginFSPathDetectorA detector = nullptr); +} // namespace sally::path::history diff --git a/src/common/dib.cpp b/src/common/dib.cpp index ad010c681..67cb62b5a 100644 --- a/src/common/dib.cpp +++ b/src/common/dib.cpp @@ -76,7 +76,7 @@ HBITMAP LoadBitmapAndMapColors(HINSTANCE hInst, HRSRC hRsrc, int mapCount, HDC hDCScreen = HANDLES(GetDC(NULL)); LPBYTE lpBits; lpBits = (LPBYTE)(lpBitmap + 1); - lpBits += (1 << (lpBitmapInfo->biBitCount)) * sizeof(RGBQUAD); + lpBits += ((size_t)1 << lpBitmapInfo->biBitCount) * sizeof(RGBQUAD); HBITMAP hbm = HANDLES(CreateDIBitmap(hDCScreen, lpBitmapInfo, CBM_INIT, lpBits, (LPBITMAPINFO)lpBitmapInfo, DIB_RGB_COLORS)); if (hbm == NULL) diff --git a/src/common/find/FindDialogSeed.h b/src/common/find/FindDialogSeed.h new file mode 100644 index 000000000..c211e6710 --- /dev/null +++ b/src/common/find/FindDialogSeed.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2026 Open Salamander Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/unicode/helpers.h" + +#include +#include +#include + +namespace sally::find +{ +// The Find dialog's "Look in" seed. +// +// Built from the panel that opens the dialog; consumed by CFindDialog at +// WM_INITDIALOG. The dialog's existing data flow seeds the visible "Look in" +// combo via Transfer(ttDataToWindow), which uses ANSI WM_SETTEXT — so a +// Unicode-only panel root (e.g. C:\Temp\zz中文) renders as zz?? in the +// edit field. The seed carries both mirrors so the dialog can override the +// ANSI render with SetDlgItemTextW *after* Transfer runs, but only when +// the override is actually needed (the ASCII-only common case round-trips +// cleanly through CP_ACP and skips the override entirely). +struct LookInSeed +{ + std::string ansi; // legacy mirror; what Transfer(ttDataToWindow) seeds + std::wstring wide; // wide source-of-truth; what SetDlgItemTextW gets +}; + +// Build a seed from the panel's caches. +// +// If `widePath` is non-null and non-empty, `wide` is the wide cache verbatim +// and `ansi` is the ANSI cache as-is (lossy for non-CP_ACP roots — that's +// fine, the wide member is what survives). If `widePath` is null/empty, +// `wide` is AnsiToWide(ansiPath) so callers without a wide cache (plugin FS +// today) still get a usable wide field. +inline LookInSeed BuildLookInSeed(const char* ansiPath, const wchar_t* widePath) +{ + LookInSeed seed; + seed.ansi = ansiPath != nullptr ? ansiPath : ""; + if (widePath != nullptr && widePath[0] != L'\0') + seed.wide = widePath; + else + seed.wide = AnsiToWide(ansiPath != nullptr ? ansiPath : ""); + return seed; +} + +// Returns true when the dialog should override the Transfer-populated ANSI +// edit text with a SetDlgItemTextW call after Transfer runs. +// +// The override only fires when the wide seed genuinely carries non-ASCII +// bytes that CP_ACP can't represent. In the common ASCII-only case the +// override is skipped — the ANSI Transfer already produced a faithful +// render and reissuing the same text via SetDlgItemTextW would be busy +// work. Centralizing the decision here means the dialog stays a thin +// presenter; the seed-vs-override question is unit-testable without +// instantiating any Win32 dialog. +inline bool ShouldOverrideEditWithWide(const LookInSeed& seed) +{ + if (seed.wide.empty()) + return false; + // Round-trip the wide through CP_ACP; if the result matches the wide + // seed, the ANSI render Transfer produced is already faithful. + std::string ansiRoundTrip = WideToAnsi(seed.wide); + std::wstring backToWide = AnsiToWide(ansiRoundTrip.c_str()); + return backToWide != seed.wide; +} + +inline void TrimLookInPathW(std::wstring& path) +{ + size_t first = 0; + while (first < path.length() && path[first] <= L' ') + first++; + + size_t last = path.length(); + while (last > first && path[last - 1] <= L' ') + last--; + + if (first != 0 || last != path.length()) + path = path.substr(first, last - first); +} + +inline void KeepSingleTrailingSeparatorW(std::wstring& path) +{ + if (path.empty()) + return; + + size_t end = path.length(); + while (end > 1 && (path[end - 1] == L'\\' || path[end - 1] == L'/') && + (path[end - 2] == L'\\' || path[end - 2] == L'/')) + { + end--; + } + path.resize(end); +} + +inline std::vector SplitLookInPathsW(const std::wstring& text) +{ + std::vector paths; + std::wstring current; + + for (size_t i = 0; i < text.length(); i++) + { + if (text[i] == L';') + { + if (i + 1 < text.length() && text[i + 1] == L';') + { + current.push_back(L';'); + i++; + } + else + { + TrimLookInPathW(current); + KeepSingleTrailingSeparatorW(current); + if (!current.empty()) + paths.push_back(current); + current.clear(); + } + } + else + current.push_back(text[i]); + } + + TrimLookInPathW(current); + KeepSingleTrailingSeparatorW(current); + if (!current.empty()) + paths.push_back(current); + + return paths; +} +} // namespace sally::find diff --git a/src/common/fsutil.cpp b/src/common/fsutil.cpp index 2f8dabe4d..cf5ba67ca 100644 --- a/src/common/fsutil.cpp +++ b/src/common/fsutil.cpp @@ -58,7 +58,7 @@ SalFileInfo GetFileInfoW(const wchar_t* fullPath) info.FileName = findData.cFileName; if (findData.cAlternateFileName[0] != L'\0') info.AlternateName = findData.cAlternateFileName; - FindClose(hFind); + HANDLES(FindClose(hFind)); } else { diff --git a/src/common/handles.cpp b/src/common/handles.cpp index 23658dfe7..6f31e7e8f 100644 --- a/src/common/handles.cpp +++ b/src/common/handles.cpp @@ -402,6 +402,8 @@ const char* __GetHandlesOrigin(C__HandlesOrigin origin) return "GlobalLock"; case __hoFindFirstChangeNotification: return "FindFirstChangeNotification"; + case __hoFindFirstChangeNotificationW: + return "FindFirstChangeNotificationW"; case __hoGetEnvironmentStrings: return "GetEnvironmentStrings"; case __hoLocalAlloc: @@ -2202,16 +2204,27 @@ BOOL C__Handles::GlobalUnlock(HGLOBAL hMem) } HANDLE -C__Handles::FindFirstChangeNotification(LPCTSTR lpPathName, BOOL bWatchSubtree, - DWORD dwNotifyFilter) +C__Handles::FindFirstChangeNotificationA(LPCSTR lpPathName, BOOL bWatchSubtree, + DWORD dwNotifyFilter) { - HANDLE ret = ::FindFirstChangeNotification(lpPathName, bWatchSubtree, - dwNotifyFilter); + HANDLE ret = ::FindFirstChangeNotificationA(lpPathName, bWatchSubtree, + dwNotifyFilter); CheckCreate(ret != INVALID_HANDLE_VALUE, __htChangeNotification, __hoFindFirstChangeNotification, ret, GetLastError()); return ret; } +HANDLE +C__Handles::FindFirstChangeNotificationW(LPCWSTR lpPathName, BOOL bWatchSubtree, + DWORD dwNotifyFilter) +{ + HANDLE ret = ::FindFirstChangeNotificationW(lpPathName, bWatchSubtree, + dwNotifyFilter); + CheckCreate(ret != INVALID_HANDLE_VALUE, __htChangeNotification, + __hoFindFirstChangeNotificationW, ret, GetLastError()); + return ret; +} + BOOL C__Handles::FindCloseChangeNotification(HANDLE hChangeHandle) { #ifdef MULTITHREADED_HANDLES_ENABLE diff --git a/src/common/handles.h b/src/common/handles.h index 539ac62d2..e32909ae4 100644 --- a/src/common/handles.h +++ b/src/common/handles.h @@ -192,6 +192,7 @@ enum C__HandlesOrigin __hoGlobalReAlloc, __hoGlobalLock, __hoFindFirstChangeNotification, + __hoFindFirstChangeNotificationW, __hoGetEnvironmentStrings, __hoLocalAlloc, __hoLocalReAlloc, @@ -605,8 +606,13 @@ class C__Handles BOOL GlobalUnlock(HGLOBAL hMem); - HANDLE FindFirstChangeNotification(LPCTSTR lpPathName, BOOL bWatchSubtree, - DWORD dwNotifyFilter); + HANDLE FindFirstChangeNotificationA(LPCSTR lpPathName, BOOL bWatchSubtree, + DWORD dwNotifyFilter); + + // Wide variant — needed by the Snooper's wide path support so the + // change-notification handle is opened against the real Unicode path. + HANDLE FindFirstChangeNotificationW(LPCWSTR lpPathName, BOOL bWatchSubtree, + DWORD dwNotifyFilter); BOOL FindCloseChangeNotification(HANDLE hChangeHandle); diff --git a/src/common/str.h b/src/common/str.h index 2b869ba95..bb69ebf2f 100644 --- a/src/common/str.h +++ b/src/common/str.h @@ -195,7 +195,7 @@ inline int SWPrintFToEnd_s(WCHAR (&_Dst)[_Size], const WCHAR* _Format, ...) { va_list _ArgList; va_start(_ArgList, _Format); - int len = wcslen(_Dst); + size_t len = wcslen(_Dst); return vswprintf_s(_Dst + len, _Size - len, _Format, _ArgList); } @@ -203,7 +203,7 @@ inline int SWPrintFToEnd_s(WCHAR* _Dst, size_t _SizeInWords, const WCHAR* _Forma { va_list _ArgList; va_start(_ArgList, _Format); - int len = (int)wcslen(_Dst); + size_t len = wcslen(_Dst); return vswprintf_s(_Dst + len, _SizeInWords - len, _Format, _ArgList); } @@ -218,7 +218,7 @@ inline int SPrintFToEnd_s(char (&_Dst)[_Size], const char* _Format, ...) { va_list _ArgList; va_start(_ArgList, _Format); - int len = strlen(_Dst); + size_t len = strlen(_Dst); return vsprintf_s(_Dst + len, _Size - len, _Format, _ArgList); } diff --git a/src/common/unicode/CopyNamePolicy.h b/src/common/unicode/CopyNamePolicy.h index 60e1d5dfd..524366a06 100644 --- a/src/common/unicode/CopyNamePolicy.h +++ b/src/common/unicode/CopyNamePolicy.h @@ -69,6 +69,12 @@ inline bool TryGenerateUniqueCopyName(const std::wstring& directoryWithBackslash { if (directoryWithBackslash.empty() || originalName.empty() || copyToken.empty()) return false; + if (directoryWithBackslash.back() != L'\\' && directoryWithBackslash.back() != L'/') + return false; + + DWORD dirAttrs = GetFileAttributesW(directoryWithBackslash.c_str()); + if (dirAttrs == INVALID_FILE_ATTRIBUTES || (dirAttrs & FILE_ATTRIBUTE_DIRECTORY) == 0) + return false; for (int copyIndex = 1; copyIndex < 10000; copyIndex++) { diff --git a/src/common/unicode/NameRenderPolicy.h b/src/common/unicode/NameRenderPolicy.h index 30ce0fce9..b0590e21e 100644 --- a/src/common/unicode/NameRenderPolicy.h +++ b/src/common/unicode/NameRenderPolicy.h @@ -7,6 +7,13 @@ namespace sally::unicode { +struct NameWidthMeasurementPlan +{ + bool UseWide = false; + int NameLength = 0; + int ExtensionLength = 0; +}; + inline int GetWideNameLengthForNameColumn(const wchar_t* nameW, bool isDir, bool sortDirsByExt, @@ -37,4 +44,35 @@ inline const wchar_t* GetWideExtensionStart(const wchar_t* nameW) return dot + 1; } + +inline NameWidthMeasurementPlan BuildNameWidthMeasurementPlan(const char* nameA, + int nameLenA, + const char* extA, + const wchar_t* nameW, + bool isDir, + bool sortDirsByExt, + bool extensionInSeparateColumn) +{ + NameWidthMeasurementPlan plan; + plan.UseWide = (nameW != NULL); + + if (plan.UseWide) + { + plan.NameLength = GetWideNameLengthForNameColumn(nameW, isDir, sortDirsByExt, extensionInSeparateColumn); + const wchar_t* extPosW = extensionInSeparateColumn ? GetWideExtensionStart(nameW) : NULL; + plan.ExtensionLength = extPosW != NULL ? (int)wcslen(extPosW) : 0; + return plan; + } + + if (nameA == NULL) + return plan; + + plan.NameLength = nameLenA; + if (extensionInSeparateColumn && extA != NULL && extA[0] != 0 && extA > nameA + 1 && (!isDir || sortDirsByExt)) + { + plan.NameLength = (int)(extA - nameA - 1); + plan.ExtensionLength = nameLenA - (int)(extA - nameA); + } + return plan; +} } // namespace sally::unicode diff --git a/src/common/unicode/PanelPathPolicy.h b/src/common/unicode/PanelPathPolicy.h new file mode 100644 index 000000000..0a21fe808 --- /dev/null +++ b/src/common/unicode/PanelPathPolicy.h @@ -0,0 +1,417 @@ +// SPDX-FileCopyrightText: 2026 Sally Authors +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/unicode/helpers.h" + +#include +#include +#include + +namespace sally::unicode +{ +// Returns true when the wide-cache value is populated (non-null and non-empty). +// +// Use this to gate the wide-vs-ANSI dispatch when a panel keeps both a wide +// source-of-truth path/name and a lossy ANSI mirror. Whenever this returns +// true, downstream code must prefer the wide cache; the ANSI cache may +// contain `?`-mangled bytes for non-CP_ACP characters and routing to ANSI +// validators with that buffer will misreport the path as nonexistent. +// +// Centralized here so the single decision can be tested in isolation and +// reused everywhere a "wide cache populated?" question is asked. +inline bool HasWidePathW(const wchar_t* widePath) +{ + return widePath != NULL && widePath[0] != L'\0'; +} + +// Rebinds an ANSI path that was built on top of a panel's lossy ANSI cache +// onto the panel's wide source-of-truth prefix. +// +// Use case: legacy script builders (BuildScriptMain/Dir/File) thread the +// panel's ANSI `Path` into operation entries' SourceName. For Unicode-only +// roots (e.g. C:\Temp\zz中文) that ANSI cache contains `?`-mangled bytes, +// and the wide mirror SourceNameW that COperations::Add derives from it via +// CP_ACP round-trip is equally lossy. After the script is built, call this +// function once per op to compute a wide path that pins the panel-root +// segment to the real wide cache while preserving the (typically ASCII) +// suffix that lives below the panel root. +// +// Returns the rebound wide path on success, or an empty string when: +// - any input is null/empty +// - `ansiPath` does not start with `anchorAnsi` (case-insensitive, +// trailing-slash tolerant) followed by either end-of-string or a +// directory separator. (Prevents a sibling like `C:\Temp\zz??-suffix` +// from being matched against `C:\Temp\zz??`.) +// +// The returned path does not carry the `\\?\` long-path prefix; the caller +// applies that decoration if needed. +inline std::wstring RebindAnsiPathToWideAnchor(const char* ansiPath, + const char* anchorAnsi, + const wchar_t* anchorWide) +{ + if (ansiPath == NULL || ansiPath[0] == '\0') + return std::wstring(); + if (anchorAnsi == NULL || anchorAnsi[0] == '\0') + return std::wstring(); + if (anchorWide == NULL || anchorWide[0] == L'\0') + return std::wstring(); + + size_t cmpLen = strlen(anchorAnsi); + if (cmpLen > 0 && (anchorAnsi[cmpLen - 1] == '\\' || anchorAnsi[cmpLen - 1] == '/')) + --cmpLen; + if (cmpLen == 0) + return std::wstring(); + + if (strlen(ansiPath) < cmpLen) + return std::wstring(); + if (_strnicmp(ansiPath, anchorAnsi, cmpLen) != 0) + return std::wstring(); + + const char nextCh = ansiPath[cmpLen]; + if (nextCh != '\0' && nextCh != '\\' && nextCh != '/') + return std::wstring(); + + std::wstring rebound(anchorWide); + if (!rebound.empty() && (rebound.back() == L'\\' || rebound.back() == L'/')) + rebound.pop_back(); + + const char* remainder = ansiPath + cmpLen; + if (*remainder != '\0') + { + // The remainder is the portion of the path beyond the panel root; + // typical content is ASCII filenames / subdirectory names. CP_ACP is + // sufficient there because the panel root is the only place the + // ANSI cache mangling could have occurred. (If a Unicode-only + // segment existed *below* the panel root we would still be lossy + // here; that path is not yet observed in the wild and would require + // threading wide names through the script builders end-to-end.) + std::wstring wideRemainder = AnsiToWide(remainder); + if (!wideRemainder.empty() && wideRemainder[0] != L'\\' && wideRemainder[0] != L'/') + rebound.push_back(L'\\'); + rebound += wideRemainder; + } + + return rebound; +} + +inline bool IsPathSeparatorA(char ch) +{ + return ch == '\\' || ch == '/'; +} + +inline bool IsPathSeparatorW(wchar_t ch) +{ + return ch == L'\\' || ch == L'/'; +} + +inline size_t RootLengthA(const char* path, size_t len) +{ + if (path == NULL || len == 0) + return 0; + + if (len >= 2 && IsPathSeparatorA(path[0]) && IsPathSeparatorA(path[1])) + { + size_t pos = 2; + while (pos < len && !IsPathSeparatorA(path[pos])) + ++pos; + if (pos == len) + return len; + + ++pos; + while (pos < len && !IsPathSeparatorA(path[pos])) + ++pos; + if (pos == len) + return len; + + return pos + 1; + } + + if (len >= 2 && path[1] == ':') + { + if (len >= 3 && IsPathSeparatorA(path[2])) + return 3; + return 2; + } + + if (IsPathSeparatorA(path[0])) + return 1; + + return 0; +} + +inline size_t RootLengthW(const wchar_t* path, size_t len) +{ + if (path == NULL || len == 0) + return 0; + + if (len >= 2 && IsPathSeparatorW(path[0]) && IsPathSeparatorW(path[1])) + { + size_t pos = 2; + while (pos < len && !IsPathSeparatorW(path[pos])) + ++pos; + if (pos == len) + return len; + + ++pos; + while (pos < len && !IsPathSeparatorW(path[pos])) + ++pos; + if (pos == len) + return len; + + return pos + 1; + } + + if (len >= 2 && path[1] == L':') + { + if (len >= 3 && IsPathSeparatorW(path[2])) + return 3; + return 2; + } + + if (IsPathSeparatorW(path[0])) + return 1; + + return 0; +} + +inline size_t TrimTrailingPathSeparatorsA(const char* path, size_t len) +{ + size_t rootLen = RootLengthA(path, len); + while (len > rootLen && IsPathSeparatorA(path[len - 1])) + --len; + return len; +} + +inline std::wstring TrimTrailingPathSeparatorsW(std::wstring path) +{ + size_t rootLen = RootLengthW(path.c_str(), path.length()); + while (path.length() > rootLen && IsPathSeparatorW(path.back())) + path.pop_back(); + return path; +} + +inline bool AnsiPathPrefixMatchesAtBoundary(const char* path, size_t pathLen, + const char* prefix, size_t prefixLen) +{ + if (path == NULL || prefix == NULL || prefixLen == 0 || prefixLen > pathLen) + return false; + if (_strnicmp(path, prefix, prefixLen) != 0) + return false; + if (pathLen == prefixLen) + return true; + if (prefixLen == RootLengthA(prefix, prefixLen)) + return true; + + return IsPathSeparatorA(path[prefixLen]); +} + +inline int CountAnsiPathComponents(const char* path, size_t len) +{ + int count = 0; + size_t pos = 0; + while (pos < len) + { + while (pos < len && IsPathSeparatorA(path[pos])) + ++pos; + if (pos >= len) + break; + + ++count; + while (pos < len && !IsPathSeparatorA(path[pos])) + ++pos; + } + + return count; +} + +inline bool CutLastWidePathComponent(std::wstring& path) +{ + path = TrimTrailingPathSeparatorsW(path); + + size_t rootLen = RootLengthW(path.c_str(), path.length()); + if (path.length() <= rootLen) + return false; + + size_t slash = path.find_last_of(L"\\/"); + if (slash == std::wstring::npos) + return false; + + if (slash + 1 <= rootLen) + path.resize(rootLen); + else + path.resize(slash); + + return true; +} + +inline std::wstring AppendAnsiPathSuffixToWideRoot(std::wstring wideRoot, const char* suffix) +{ + std::wstring wideSuffix = AnsiToWide(suffix); + if (wideSuffix.empty()) + return wideRoot; + + if (!IsPathSeparatorW(wideSuffix[0]) && !wideRoot.empty() && !IsPathSeparatorW(wideRoot.back())) + wideRoot.push_back(L'\\'); + wideRoot += wideSuffix; + return wideRoot; +} + +inline std::wstring MapRelatedAnsiPathToWidePath(const char* ansiPath, + const char* anchorAnsi, + const wchar_t* anchorWide) +{ + if (ansiPath == NULL || ansiPath[0] == '\0') + return std::wstring(); + if (anchorAnsi == NULL || anchorAnsi[0] == '\0') + return std::wstring(); + if (anchorWide == NULL || anchorWide[0] == L'\0') + return std::wstring(); + + size_t pathLen = TrimTrailingPathSeparatorsA(ansiPath, strlen(ansiPath)); + size_t anchorLen = TrimTrailingPathSeparatorsA(anchorAnsi, strlen(anchorAnsi)); + if (pathLen == 0 || anchorLen == 0) + return std::wstring(); + + std::wstring wideAnchor = TrimTrailingPathSeparatorsW(std::wstring(anchorWide)); + if (wideAnchor.empty()) + return std::wstring(); + + if (AnsiPathPrefixMatchesAtBoundary(ansiPath, pathLen, anchorAnsi, anchorLen)) + { + if (pathLen == anchorLen) + return wideAnchor; + + return AppendAnsiPathSuffixToWideRoot(wideAnchor, ansiPath + anchorLen); + } + + if (AnsiPathPrefixMatchesAtBoundary(anchorAnsi, anchorLen, ansiPath, pathLen)) + { + int componentsToCut = CountAnsiPathComponents(anchorAnsi + pathLen, anchorLen - pathLen); + std::wstring mapped = wideAnchor; + while (componentsToCut-- > 0) + { + if (!CutLastWidePathComponent(mapped)) + return std::wstring(); + } + return mapped; + } + + return std::wstring(); +} + +inline std::wstring EffectiveItemNameW(const char* nameA, const wchar_t* nameW) +{ + if (HasWidePathW(nameW)) + return std::wstring(nameW); + + return AnsiToWide(nameA); +} + +// The wide-twin pair carried by a deferred history item. Plain data, no +// ownership of plugin FS or icons, so it can be constructed in tests without +// dragging the salamand.h object graph (CPathHistory / TIndirectArray / +// CPluginFSInterfaceAbstract) into the test binary. +// +// Used by CPathHistory's deferred-flush block (the unlock path that re-adds +// a parked NewItem after Lock = FALSE). The flush originally re-added the +// item by calling AddPathUnique with only the ANSI fields, silently +// dropping the wide twins. Centralizing the args composition here means +// future refactors can't re-introduce the same loss. +struct DeferredHistoryWideTwins +{ + std::wstring pathOrArchiveOrFSNameW; + std::wstring archivePathOrFSUserPartW; +}; + +// Extracts the wide-twin pointers a flush-site should pass to AddPathUnique. +// Returns nullptr for either pointer when the corresponding wide string is +// empty so the AddPathUnique constructor's "ignore empty wide" guard does +// the right thing without the caller having to special-case it (plugin FS +// items today carry empty wide twins; the constructor's guard then leaves +// the item's wide fields empty, matching round-1/2 behavior exactly). +inline void DeferredHistoryWidePointers(const DeferredHistoryWideTwins& twins, + const wchar_t*& outNameW, + const wchar_t*& outUserPartW) +{ + outNameW = twins.pathOrArchiveOrFSNameW.empty() ? nullptr : twins.pathOrArchiveOrFSNameW.c_str(); + outUserPartW = twins.archivePathOrFSUserPartW.empty() ? nullptr : twins.archivePathOrFSUserPartW.c_str(); +} + +inline bool HasTrailingSlashW(const std::wstring& path) +{ + return !path.empty() && (path.back() == L'\\' || path.back() == L'/'); +} + +inline std::wstring BuildPanelChildPathW(const std::wstring& parentPathW, + const char* childNameA, + const wchar_t* childNameW) +{ + std::wstring childName = EffectiveItemNameW(childNameA, childNameW); + if (parentPathW.empty()) + return childName; + if (childName.empty()) + return parentPathW; + + std::wstring result = parentPathW; + if (!HasTrailingSlashW(result)) + result.push_back(L'\\'); + result += childName; + return result; +} + +inline bool TryExactAnsiFallback(const std::wstring& value, std::string& ansi) +{ + ansi.clear(); + if (value.empty()) + return true; + + BOOL usedDefaultChar = FALSE; + int required = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, value.c_str(), -1, + NULL, 0, NULL, &usedDefaultChar); + if (required <= 0 || usedDefaultChar) + return false; + + std::string converted((size_t)required, '\0'); + usedDefaultChar = FALSE; + if (WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, value.c_str(), -1, + converted.data(), required, NULL, &usedDefaultChar) == 0 || + usedDefaultChar) + { + return false; + } + converted.resize((size_t)required - 1); + + int roundTripLen = MultiByteToWideChar(CP_ACP, 0, converted.c_str(), -1, NULL, 0); + if (roundTripLen <= 0) + return false; + + std::wstring roundTrip((size_t)roundTripLen, L'\0'); + if (MultiByteToWideChar(CP_ACP, 0, converted.c_str(), -1, roundTrip.data(), roundTripLen) == 0) + return false; + roundTrip.resize((size_t)roundTripLen - 1); + if (roundTrip != value) + return false; + + ansi = std::move(converted); + return true; +} + +inline bool WidePathNeedsExactPreservation(const wchar_t* widePath) +{ + if (!HasWidePathW(widePath)) + return false; + + std::string ansi; + return !TryExactAnsiFallback(std::wstring(widePath), ansi); +} + +inline std::wstring EffectivePanelPathW(const char* ansiPath, const wchar_t* widePath) +{ + if (HasWidePathW(widePath)) + return std::wstring(widePath); + + return AnsiToWide(ansiPath); +} +} // namespace sally::unicode diff --git a/src/common/unicode/helpers.h b/src/common/unicode/helpers.h index fdfbc9572..03cfa246e 100644 --- a/src/common/unicode/helpers.h +++ b/src/common/unicode/helpers.h @@ -45,6 +45,35 @@ inline void WideToAnsi(const std::wstring& s, char* buffer, int bufferSize) buffer[bufferSize - 1] = '\0'; // Ensure null termination } +// Trim spaces from the beginning and spaces/dots from the end of a filename +// component, matching Explorer's manual-create behavior. +inline BOOL MakeValidFileNameComponentW(wchar_t* path) +{ + if (path == NULL) + return FALSE; + + BOOL changed = FALSE; + wchar_t* n = path; + while (*n != 0 && *n <= L' ') + n++; + if (n > path) + { + memmove(path, n, (wcslen(n) + 1) * sizeof(wchar_t)); + changed = TRUE; + } + + n = path + wcslen(path); + while (n > path && (*(n - 1) <= L' ' || *(n - 1) == L'.')) + n--; + if (*n != 0) + { + *n = 0; + changed = TRUE; + } + + return changed; +} + // Format a wide string using printf-style formatting, returns std::wstring template inline std::wstring FormatStrW(const wchar_t* format, Args... args) diff --git a/src/common/widepath.cpp b/src/common/widepath.cpp index be906c871..cccf0eb0e 100644 --- a/src/common/widepath.cpp +++ b/src/common/widepath.cpp @@ -438,6 +438,24 @@ HANDLE SalLPCreateFile( hTemplateFile); } +HANDLE SalLPCreateFileWide( + const wchar_t* fileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile) +{ + IFileSystem* fs = GetActiveFileSystem(); + if (fs != NULL) + return fs->CreateFile(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + + return CreateFileW(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); +} + DWORD SalLPGetFileAttributes(const char* fileName) { IFileSystem* fs = GetActiveFileSystem(); @@ -562,6 +580,15 @@ HANDLE SalLPFindFirstFile(const char* fileName, WIN32_FIND_DATAW* findData) return FindFirstFileW(widePath.Get(), findData); } +HANDLE SalLPFindFirstFileWide(const wchar_t* fileName, WIN32_FIND_DATAW* findData) +{ + IFileSystem* fs = GetActiveFileSystem(); + if (fs != NULL) + return fs->FindFirstFile(fileName, findData); + + return FindFirstFileW(fileName, findData); +} + BOOL SalLPFindNextFile(HANDLE hFindFile, WIN32_FIND_DATAW* findData) { IFileSystem* fs = GetActiveFileSystem(); @@ -648,6 +675,28 @@ HANDLE SalLPCreateFileTracked( return h; } +HANDLE SalLPCreateFileTrackedWide( + const wchar_t* fileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile, + const char* srcFile, + int srcLine) +{ + HANDLE h = SalLPCreateFileWide(fileName, dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, + dwFlagsAndAttributes, hTemplateFile); + + DWORD err = GetLastError(); + __Handles.SetInfo(srcFile, srcLine, __otQuiet) + .CheckCreate(h != INVALID_HANDLE_VALUE, __htFile, __hoCreateFile, h, err, TRUE); + + return h; +} + HANDLE SalLPFindFirstFileTracked( const char* fileName, WIN32_FIND_DATAA* findData, @@ -656,10 +705,32 @@ HANDLE SalLPFindFirstFileTracked( { HANDLE h = SalLPFindFirstFileA(fileName, findData); - // Track the handle using Salamander's handle tracking system - DWORD err = GetLastError(); - __Handles.SetInfo(srcFile, srcLine, __otQuiet) - .CheckCreate(h != INVALID_HANDLE_VALUE, __htFindFile, __hoFindFirstFile, h, err, TRUE); + if (GetActiveFileSystem() == NULL) + { + // Track fallback handles. The active Win32 IFileSystem tracks in its FindFirstFile + // implementation so direct IFileSystem callers and wrapper callers behave alike. + DWORD err = GetLastError(); + __Handles.SetInfo(srcFile, srcLine, __otQuiet) + .CheckCreate(h != INVALID_HANDLE_VALUE, __htFindFile, __hoFindFirstFile, h, err, TRUE); + } + + return h; +} + +HANDLE SalLPFindFirstFileTrackedWide( + const wchar_t* fileName, + WIN32_FIND_DATAW* findData, + const char* srcFile, + int srcLine) +{ + HANDLE h = SalLPFindFirstFileWide(fileName, findData); + + if (GetActiveFileSystem() == NULL) + { + DWORD err = GetLastError(); + __Handles.SetInfo(srcFile, srcLine, __otQuiet) + .CheckCreate(h != INVALID_HANDLE_VALUE, __htFindFile, __hoFindFirstFile, h, err, TRUE); + } return h; } @@ -672,10 +743,14 @@ HANDLE SalLPFindFirstFileTrackedW( { HANDLE h = SalLPFindFirstFile(fileName, findData); - // Track the handle using Salamander's handle tracking system - DWORD err = GetLastError(); - __Handles.SetInfo(srcFile, srcLine, __otQuiet) - .CheckCreate(h != INVALID_HANDLE_VALUE, __htFindFile, __hoFindFirstFile, h, err, TRUE); + if (GetActiveFileSystem() == NULL) + { + // Track fallback handles. The active Win32 IFileSystem tracks in its FindFirstFile + // implementation so direct IFileSystem callers and wrapper callers behave alike. + DWORD err = GetLastError(); + __Handles.SetInfo(srcFile, srcLine, __otQuiet) + .CheckCreate(h != INVALID_HANDLE_VALUE, __htFindFile, __hoFindFirstFile, h, err, TRUE); + } return h; } diff --git a/src/common/widepath.h b/src/common/widepath.h index 285c87b1c..99ad1b19f 100644 --- a/src/common/widepath.h +++ b/src/common/widepath.h @@ -420,6 +420,15 @@ HANDLE SalLPCreateFile( DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); +HANDLE SalLPCreateFileWide( + const wchar_t* fileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile); + // GetFileAttributes wrapper that supports long paths DWORD SalLPGetFileAttributes(const char* fileName); @@ -445,6 +454,8 @@ BOOL SalLPCopyFile(const char* existingFileName, const char* newFileName, BOOL f // Note: Returns wide find data; caller must convert if needed HANDLE SalLPFindFirstFile(const char* fileName, WIN32_FIND_DATAW* findData); +HANDLE SalLPFindFirstFileWide(const wchar_t* fileName, WIN32_FIND_DATAW* findData); + // FindNextFile wrapper that supports long paths // Note: Returns wide find data; caller must convert if needed BOOL SalLPFindNextFile(HANDLE hFindFile, WIN32_FIND_DATAW* findData); @@ -472,6 +483,9 @@ BOOL SalLPFindNextFileA(HANDLE hFindFile, WIN32_FIND_DATAA* findData); #define SalCreateFileH(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile) \ SalLPCreateFileTracked(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile, __FILE__, __LINE__) +#define SalCreateFileWideH(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile) \ + SalLPCreateFileTrackedWide(fileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile, __FILE__, __LINE__) + HANDLE SalLPCreateFileTracked( const char* fileName, DWORD dwDesiredAccess, @@ -483,6 +497,17 @@ HANDLE SalLPCreateFileTracked( const char* srcFile, int srcLine); +HANDLE SalLPCreateFileTrackedWide( + const wchar_t* fileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile, + const char* srcFile, + int srcLine); + // FindFirstFile with handle tracking - use instead of HANDLES_Q(FindFirstFile(...)) #define SalFindFirstFileH(fileName, findData) \ SalLPFindFirstFileTracked(fileName, findData, __FILE__, __LINE__) @@ -496,17 +521,27 @@ HANDLE SalLPFindFirstFileTracked( // FindFirstFile (wide data) with handle tracking - use instead of HANDLES_Q(FindFirstFileW(...)) #define SalFindFirstFileHW(fileName, findData) SalLPFindFirstFileTrackedW(fileName, findData, __FILE__, __LINE__) +#define SalFindFirstFileWideH(fileName, findData) SalLPFindFirstFileTrackedWide(fileName, findData, __FILE__, __LINE__) + HANDLE SalLPFindFirstFileTrackedW( const char* fileName, WIN32_FIND_DATAW* findData, const char* srcFile, int srcLine); +HANDLE SalLPFindFirstFileTrackedWide( + const wchar_t* fileName, + WIN32_FIND_DATAW* findData, + const char* srcFile, + int srcLine); + #else // !HANDLES_ENABLE // In release builds, just use the regular wrapper #define SalCreateFileH SalLPCreateFile +#define SalCreateFileWideH SalLPCreateFileWide #define SalFindFirstFileH SalLPFindFirstFileA #define SalFindFirstFileHW SalLPFindFirstFile +#define SalFindFirstFileWideH SalLPFindFirstFileWide #endif // HANDLES_ENABLE diff --git a/src/consts.h b/src/consts.h index 752cb3df3..869aa44be 100644 --- a/src/consts.h +++ b/src/consts.h @@ -420,6 +420,8 @@ BOOL SalCheckAndRestorePathW(HWND parent, const wchar_t* path, BOOL tryNet); // if resulting path 'path' is accessible BOOL SalCheckAndRestorePathWithCut(HWND parent, char* path, BOOL& tryNet, DWORD& err, DWORD& lastErr, BOOL& pathInvalid, BOOL& cut, BOOL donotReconnect); +BOOL SalCheckAndRestorePathWithCutW(HWND parent, std::wstring& path, BOOL& tryNet, DWORD& err, DWORD& lastErr, + BOOL& pathInvalid, BOOL& cut, BOOL donotReconnect); // Recognizes what type of path (FS/windows/archive) it is and handles splitting it into // its parts (for FS it's fs-name and fs-user-part, for archive it's path-to-archive and @@ -452,6 +454,9 @@ BOOL SalParsePath(HWND parent, char* path, int& type, BOOL& isDir, char*& second const char* errorTitle, char* nextFocus, BOOL curPathIsDiskOrArchive, const char* curPath, const char* curArchivePath, int* error, int pathBufSize); +BOOL SalParsePathW(HWND parent, std::wstring& path, int& type, BOOL& isDir, wchar_t*& secondPart, + const wchar_t* errorTitle, std::wstring* nextFocus, BOOL curPathIsDiskOrArchive, + const wchar_t* curPath, const wchar_t* curArchivePath, int* error); // Gets existing part and operation mask from windows target path; allows creating non-existing part; // on success returns TRUE and existing windows target path (in 'path') and found operation mask @@ -470,6 +475,9 @@ BOOL SalParsePath(HWND parent, char* path, int& type, BOOL& isDir, char*& second BOOL SalSplitWindowsPath(HWND parent, const char* title, const char* errorTitle, int selCount, char* path, char* secondPart, BOOL pathIsDir, BOOL backslashAtEnd, const char* dirName, const char* curDiskPath, char*& mask); +BOOL SalSplitWindowsPathW(HWND parent, const wchar_t* title, const wchar_t* errorTitle, int selCount, + wchar_t* path, wchar_t* secondPart, BOOL pathIsDir, BOOL backslashAtEnd, + const wchar_t* dirName, const wchar_t* curDiskPath, wchar_t*& mask); // Gets existing part and operation mask from target path; recognizes non-existing part; on // success returns TRUE, relative path to create (in 'newDirs'), existing target path (in 'path'; @@ -682,7 +690,7 @@ void RemoveEmptyDirs(const char* dir); // and return values are not described BOOL ViewFileInt(HWND parent, const char* name, BOOL altView, DWORD handlerID, BOOL returnLock, HANDLE& lock, BOOL& lockOwner, BOOL addToHistory, int enumFileNamesSourceUID, - int enumFileNamesLastFileIndex); + int enumFileNamesLastFileIndex, const wchar_t* nameW = NULL); // Converts string ('str' of length 'len') to unsigned __int64 (can be preceded by // '+' sign; ignores white-spaces at beginning and end of string); @@ -1413,6 +1421,8 @@ extern const char *LOW_MEMORY, *SAVEBITS_CLASSNAME, *SHELLEXECUTE_CLASSNAME; +extern const wchar_t* CMAINWINDOW_CLASSNAMEW; + extern const char* STR_NONE; // "(none)" - plug-ins: for DLLName and Version if they are unknown extern char DefaultDir['z' - 'a' + 1][SAL_MAX_LONG_PATH]; // where to go on drive change diff --git a/src/darkmode.cpp b/src/darkmode.cpp index 709abc217..d65a1703f 100644 --- a/src/darkmode.cpp +++ b/src/darkmode.cpp @@ -42,17 +42,22 @@ const COLORREF DIALOG_DARK_DISABLED_TEXT = RGB(160, 160, 160); const COLORREF DIALOG_DARK_HIGHLIGHT = RGB(0, 120, 215); const COLORREF DIALOG_DARK_INACTIVE_SELECTION = RGB(75, 75, 78); const COLORREF DIALOG_DARK_TOOLTIP_BG = RGB(43, 43, 43); +const COLORREF DIALOG_DARK_FRAME = RGB(62, 62, 66); +const COLORREF DIALOG_DARK_SUBTLE_LINE = RGB(55, 55, 58); const TCHAR* IMMERSIVE_COLOR_SET_PARAM = TEXT("ImmersiveColorSet"); const TCHAR* WINDOWS_THEME_ELEMENT_PARAM = TEXT("WindowsThemeElement"); const TCHAR* BUTTON_CLASS_NAME = TEXT("Button"); const TCHAR* COMBOBOX_CLASS_NAME = TEXT("ComboBox"); const TCHAR* EDIT_CLASS_NAME = TEXT("Edit"); +const TCHAR* HEADER_CLASS_NAME = TEXT("SysHeader32"); const TCHAR* LISTBOX_CLASS_NAME = TEXT("ListBox"); const TCHAR* SCROLLBAR_CLASS_NAME = TEXT("ScrollBar"); const TCHAR* STATIC_CLASS_NAME = TEXT("Static"); const WCHAR* UXTHEME_DARKMODE_EXPLORER = L"DarkMode_Explorer"; const WCHAR* UXTHEME_EXPLORER = L"Explorer"; const UINT_PTR GROUPBOX_SUBCLASS_ID = 1; +const UINT_PTR STATIC_EDGE_SUBCLASS_ID = 2; +const UINT_PTR HEADER_SUBCLASS_ID = 4; HBRUSH DialogDarkBrush = NULL; HBRUSH DialogDarkInputBrush = NULL; @@ -181,6 +186,29 @@ void EnsureDialogBrushes() DialogDarkInputBrush = CreateSolidBrush(DIALOG_DARK_INPUT_BG); } +void FillRectSolid(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldColor = SetDCBrushColor(hdc, color); + FillRect(hdc, rect, (HBRUSH)GetStockObject(DC_BRUSH)); + SetDCBrushColor(hdc, oldColor); + SelectObject(hdc, oldBrush); +} + +void DrawRectOutline(HDC hdc, const RECT* rect, COLORREF color) +{ + if (rect == NULL || rect->right <= rect->left || rect->bottom <= rect->top) + return; + + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(NULL_BRUSH)); + COLORREF oldColor = SetDCPenColor(hdc, color); + Rectangle(hdc, rect->left, rect->top, rect->right, rect->bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + BOOL HasClassName(HWND hwnd, LPCTSTR expectedClassName) { if (hwnd == NULL || expectedClassName == NULL || !IsWindow(hwnd)) @@ -193,6 +221,20 @@ BOOL HasClassName(HWND hwnd, LPCTSTR expectedClassName) return _tcsicmp(className, expectedClassName) == 0; } +BOOL HasAnyClassName(HWND hwnd, LPCTSTR firstClassName, LPCTSTR secondClassName = NULL, LPCTSTR thirdClassName = NULL) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return FALSE; + + TCHAR className[64] = {0}; + if (GetClassName(hwnd, className, _countof(className)) == 0) + return FALSE; + + return (firstClassName != NULL && _tcsicmp(className, firstClassName) == 0) || + (secondClassName != NULL && _tcsicmp(className, secondClassName) == 0) || + (thirdClassName != NULL && _tcsicmp(className, thirdClassName) == 0); +} + BOOL IsGroupBox(HWND hwnd) { if (!HasClassName(hwnd, BUTTON_CLASS_NAME)) @@ -202,12 +244,159 @@ BOOL IsGroupBox(HWND hwnd) return (style & BS_TYPEMASK) == BS_GROUPBOX; } +BOOL IsStaticEdge(HWND hwnd) +{ + if (!HasClassName(hwnd, STATIC_CLASS_NAME)) + return FALSE; + + LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + switch (style & SS_TYPEMASK) + { + case SS_ETCHEDHORZ: + case SS_ETCHEDVERT: + case SS_GRAYFRAME: + case SS_BLACKFRAME: + case SS_WHITEFRAME: + return TRUE; + } + return FALSE; +} + void InvalidateGroupBox(HWND hwnd) { if (hwnd != NULL && IsWindow(hwnd)) InvalidateRect(hwnd, NULL, TRUE); } +BOOL PaintDarkStaticEdge(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + switch (style & SS_TYPEMASK) + { + case SS_ETCHEDHORZ: + { + FillRectSolid(hdc, &client, colors.DialogBackground); + int y = max(client.top, min(client.bottom - 1, (client.top + client.bottom) / 2)); + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, DIALOG_DARK_SUBTLE_LINE); + MoveToEx(hdc, client.left, y, NULL); + LineTo(hdc, client.right, y); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + break; + } + + case SS_ETCHEDVERT: + { + FillRectSolid(hdc, &client, colors.DialogBackground); + int x = max(client.left, min(client.right - 1, (client.left + client.right) / 2)); + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, DIALOG_DARK_SUBTLE_LINE); + MoveToEx(hdc, x, client.top, NULL); + LineTo(hdc, x, client.bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + break; + } + + default: + FillRectSolid(hdc, &client, colors.DialogBackground); + InflateRect(&client, -1, -1); + if (client.right > client.left && client.bottom > client.top) + DrawRectOutline(hdc, &client, DIALOG_DARK_SUBTLE_LINE); + break; + } + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + +BOOL PaintDarkHeader(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FillRectSolid(hdc, &client, colors.InputBackground); + + HFONT hFont = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); + HFONT hOldFont = NULL; + if (hFont != NULL) + hOldFont = (HFONT)SelectObject(hdc, hFont); + + int oldBkMode = SetBkMode(hdc, TRANSPARENT); + COLORREF oldTextColor = SetTextColor(hdc, colors.InputText); + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldPenColor = SetDCPenColor(hdc, DIALOG_DARK_FRAME); + + int count = Header_GetItemCount(hwnd); + for (int i = 0; i < count; i++) + { + RECT itemRect; + if (!Header_GetItemRect(hwnd, i, &itemRect)) + continue; + + TCHAR text[256] = {0}; + HDITEM item = {0}; + item.mask = HDI_TEXT | HDI_FORMAT; + item.pszText = text; + item.cchTextMax = _countof(text); + Header_GetItem(hwnd, i, &item); + + RECT textRect = itemRect; + textRect.left += 6; + textRect.right -= 6; + DWORD flags = DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS | DT_NOPREFIX; + if ((item.fmt & HDF_CENTER) != 0) + flags |= DT_CENTER; + else if ((item.fmt & HDF_RIGHT) != 0) + flags |= DT_RIGHT; + else + flags |= DT_LEFT; + DrawText(hdc, text, -1, &textRect, flags); + + MoveToEx(hdc, itemRect.right - 1, itemRect.top, NULL); + LineTo(hdc, itemRect.right - 1, itemRect.bottom); + } + + MoveToEx(hdc, client.left, client.bottom - 1, NULL); + LineTo(hdc, client.right, client.bottom - 1); + + SetDCPenColor(hdc, oldPenColor); + SelectObject(hdc, oldPen); + SetTextColor(hdc, oldTextColor); + SetBkMode(hdc, oldBkMode); + if (hOldFont != NULL) + SelectObject(hdc, hOldFont); + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + DWORD GetGroupBoxTextFlags(HWND hwnd) { LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); @@ -345,6 +534,99 @@ BOOL PaintDarkGroupBox(HWND hwnd, HDC paintDC) return TRUE; } +LRESULT CALLBACK StaticEdgeSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + UNREFERENCED_PARAMETER(dwRefData); + + switch (uMsg) + { + case WM_PAINT: + { + if (DarkMode_ShouldUseDark() && PaintDarkStaticEdge(hwnd, NULL)) + return 0; + break; + } + + case WM_PRINTCLIENT: + { + if (DarkMode_ShouldUseDark() && PaintDarkStaticEdge(hwnd, (HDC)wParam)) + return 0; + break; + } + + case WM_ERASEBKGND: + { + if (DarkMode_ShouldUseDark()) + return TRUE; + break; + } + + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + InvalidateRect(hwnd, NULL, TRUE); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, StaticEdgeSubclassProc, uIdSubclass); + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +LRESULT CALLBACK HeaderSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + UNREFERENCED_PARAMETER(dwRefData); + + switch (uMsg) + { + case WM_PAINT: + { + if (DarkMode_ShouldUseDark() && PaintDarkHeader(hwnd, NULL)) + return 0; + break; + } + + case WM_PRINTCLIENT: + { + if (DarkMode_ShouldUseDark() && PaintDarkHeader(hwnd, (HDC)wParam)) + return 0; + break; + } + + case WM_ERASEBKGND: + { + if (DarkMode_ShouldUseDark()) + return TRUE; + break; + } + + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + case WM_SIZE: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + InvalidateRect(hwnd, NULL, TRUE); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, HeaderSubclassProc, uIdSubclass); + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + LRESULT CALLBACK GroupBoxSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) { UNREFERENCED_PARAMETER(dwRefData); @@ -448,6 +730,13 @@ void ApplyListTreeThemeToControl(HWND hwnd, BOOL useDark) if (_tcsicmp(className, WC_LISTVIEW) == 0) { ApplyWindowTheme(hwnd, useDark, UXTHEME_EXPLORER); + HWND hHeader = ListView_GetHeader(hwnd); + if (hHeader != NULL) + { + ApplyWindowTheme(hHeader, useDark, UXTHEME_EXPLORER); + SetWindowSubclass(hHeader, HeaderSubclassProc, HEADER_SUBCLASS_ID, 0); + InvalidateRect(hHeader, NULL, TRUE); + } DarkModeColors colors; DarkMode_GetColors(&colors); COLORREF bgColor = colors.InputBackground; @@ -479,6 +768,15 @@ void ApplyListTreeThemeToControl(HWND hwnd, BOOL useDark) return; } + if (_tcsicmp(className, WC_HEADER) == 0 || + _tcsicmp(className, HEADER_CLASS_NAME) == 0) + { + ApplyWindowTheme(hwnd, useDark, UXTHEME_EXPLORER); + SetWindowSubclass(hwnd, HeaderSubclassProc, HEADER_SUBCLASS_ID, 0); + InvalidateRect(hwnd, NULL, TRUE); + return; + } + if (_tcsicmp(className, TOOLTIPS_CLASS) == 0) { ApplyWindowTheme(hwnd, useDark); @@ -486,9 +784,17 @@ void ApplyListTreeThemeToControl(HWND hwnd, BOOL useDark) return; } + if (_tcsicmp(className, STATIC_CLASS_NAME) == 0) + { + ApplyWindowTheme(hwnd, useDark); + if (IsStaticEdge(hwnd)) + SetWindowSubclass(hwnd, StaticEdgeSubclassProc, STATIC_EDGE_SUBCLASS_ID, 0); + InvalidateRect(hwnd, NULL, TRUE); + return; + } + if (_tcsicmp(className, BUTTON_CLASS_NAME) == 0 || _tcsicmp(className, EDIT_CLASS_NAME) == 0 || - _tcsicmp(className, STATIC_CLASS_NAME) == 0 || _tcsicmp(className, UPDOWN_CLASS) == 0) { ApplyWindowTheme(hwnd, useDark); diff --git a/src/dialogs.cpp b/src/dialogs.cpp index 214a0ce6e..a52bef454 100644 --- a/src/dialogs.cpp +++ b/src/dialogs.cpp @@ -305,9 +305,11 @@ unsigned ThreadProgressDlgBody(void* parameter) CConvertData* convertData = (data->ConvertData != NULL ? &convertDataCopy : NULL); CPathBuffer workPath1; // Heap-allocated for long path support lstrcpyn(workPath1, data->Script->WorkPath1, workPath1.Size()); + std::wstring workPath1W = data->Script->WorkPath1W; BOOL workPath1InclSubDirs = data->Script->WorkPath1InclSubDirs; CPathBuffer workPath2; // Heap-allocated for long path support lstrcpyn(workPath2, data->Script->WorkPath2, workPath2.Size()); + std::wstring workPath2W = data->Script->WorkPath2W; BOOL workPath2InclSubDirs = data->Script->WorkPath2InclSubDirs; CProgressDialog dlg(NULL, data->Script, data->Caption, attrsData, convertData, TRUE, data); @@ -315,9 +317,13 @@ unsigned ThreadProgressDlgBody(void* parameter) if (res == 0 || res == -1 || res == IDABORT) // failed to open the dialog or worker thread SetEvent(data->ContEvent); // let the main thread continue (opening the dialog or starting the operation failed) - if (workPath1[0] != 0) + if (!workPath1W.empty()) + MainWindow->PostChangeOnPathNotificationW(workPath1W.c_str(), workPath1InclSubDirs); + else if (workPath1[0] != 0) MainWindow->PostChangeOnPathNotification(workPath1, workPath1InclSubDirs); - if (workPath2[0] != 0) + if (!workPath2W.empty()) + MainWindow->PostChangeOnPathNotificationW(workPath2W.c_str(), workPath2InclSubDirs); + else if (workPath2[0] != 0) MainWindow->PostChangeOnPathNotification(workPath2, workPath2InclSubDirs); TRACE_I("End"); return 0; @@ -475,7 +481,9 @@ BOOL CProgressDialog::FlushCachedData() OperationText->SetText(OperationCache); if (Source != NULL && Script != NULL) { - if (Script->RemapNameFrom != NULL) + if (!SourceCacheW.empty() && Script->RemapNameFrom == NULL) + Source->SetTextToDblQuotesIfNeededW(SourceCacheW.c_str()); + else if (Script->RemapNameFrom != NULL) { CPathBuffer name; // Heap-allocated for long path support Source->SetTextToDblQuotesIfNeeded(RemapNames(name, name.Size(), SourceCache, Script)); @@ -485,7 +493,12 @@ BOOL CProgressDialog::FlushCachedData() } SetWindowText(HPreposition, PrepositionCache); if (Target != NULL) - Target->SetTextToDblQuotesIfNeeded(TargetCache); + { + if (!TargetCacheW.empty()) + Target->SetTextToDblQuotesIfNeededW(TargetCacheW.c_str()); + else + Target->SetTextToDblQuotesIfNeeded(TargetCache); + } CacheIsDirty = FALSE; } @@ -725,6 +738,8 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) lstrcpyn(PrepositionCache, data->Preposition, 100); lstrcpyn(SourceCache, data->Source, SourceCache.Size()); lstrcpyn(TargetCache, data->Target, TargetCache.Size()); + SourceCacheW = data->SourceW != NULL ? data->SourceW : L""; + TargetCacheW = data->TargetW != NULL ? data->TargetW : L""; CacheIsDirty = TRUE; } @@ -793,6 +808,15 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) char** data = (char**)lParam; switch (wParam) { + case 100: + { + void** dataW = (void**)lParam; + CFileErrorDlg dlg(HWindow, (const char*)dataW[1], (const char*)dataW[2], (const char*)dataW[4], + FALSE, 0, (const wchar_t*)dataW[3]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 0: { CFileErrorDlg dlg(HWindow, data[1], data[2], data[3]); @@ -800,6 +824,26 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 101: + { + void** dataW = (void**)lParam; + COverwriteDlg dlg(HWindow, (const char*)dataW[1], (const char*)dataW[3], + (const char*)dataW[4], (const char*)dataW[6], FALSE, FALSE, + (const wchar_t*)dataW[2], (const wchar_t*)dataW[5]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + + case 107: + { + void** dataW = (void**)lParam; + COverwriteDlg dlg(HWindow, (const char*)dataW[1], (const char*)dataW[3], + (const char*)dataW[4], (const char*)dataW[6], FALSE, TRUE, + (const wchar_t*)dataW[2], (const wchar_t*)dataW[5]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 1: { CPathBuffer tmpName; // Heap-allocated for long path support @@ -834,6 +878,15 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 103: + { + void** dataW = (void**)lParam; + CCannotMoveDlg dlg(HWindow, IDD_CANNOTMOVE, (char*)dataW[1], (char*)dataW[3], (char*)dataW[5], + (const wchar_t*)dataW[2], (const wchar_t*)dataW[4]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 4: { CCannotMoveDlg dlg(HWindow, IDD_RENAMEDIR, data[1], data[2], data[3]); @@ -841,6 +894,15 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 104: + { + void** dataW = (void**)lParam; + CCannotMoveDlg dlg(HWindow, IDD_RENAMEDIR, (char*)dataW[1], (char*)dataW[3], (char*)dataW[5], + (const wchar_t*)dataW[2], (const wchar_t*)dataW[4]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 5: { CFileErrorDlg dlg(HWindow, data[0], data[1], data[2], FALSE, IDD_ERROR3); @@ -848,6 +910,15 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 105: + { + void** dataW = (void**)lParam; + CFileErrorDlg dlg(HWindow, (const char*)dataW[0], (const char*)dataW[1], (const char*)dataW[3], + FALSE, IDD_ERROR3, (const wchar_t*)dataW[2]); + dlg.Execute(); + break; + } + case 6: { CErrorReadingADSDlg dlg(HWindow, data[1], data[2]); @@ -883,6 +954,16 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 110: + { + void** dataW = (void**)lParam; + CErrorCopyingPermissionsDlg dlg(HWindow, (const char*)dataW[1], (const char*)dataW[3], + (DWORD)(DWORD_PTR)dataW[5], + (const wchar_t*)dataW[2], (const wchar_t*)dataW[4]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 11: { CErrorCopyingDirTimeDlg dlg(HWindow, data[1], (DWORD)(DWORD_PTR)data[2]); @@ -890,6 +971,15 @@ CProgressDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case 111: + { + void** dataW = (void**)lParam; + CErrorCopyingDirTimeDlg dlg(HWindow, (const char*)dataW[1], (DWORD)(DWORD_PTR)dataW[3], + (const wchar_t*)dataW[2]); + *(int*)dataW[0] = (int)dlg.Execute(); + break; + } + case 12: { CConfirmEncryptionLossDlg dlg(HWindow, (BOOL)(INT_PTR)data[1], data[2], (BOOL)(INT_PTR)data[3]); @@ -1539,10 +1629,11 @@ MENU_TEMPLATE_ITEM ProgressDialogMenu2[] = // CFileErrorDlg::CFileErrorDlg(HWND parent, const char* caption, const char* file, const char* error, - BOOL noSkip, int altRes) : CCommonDialog(HLanguage, altRes == 0 ? (noSkip ? IDD_CREATEDIRERR : IDD_CANNOTOPEN) : altRes, parent) + BOOL noSkip, int altRes, const wchar_t* fileW) : CCommonDialog(HLanguage, altRes == 0 ? (noSkip ? IDD_CREATEDIRERR : IDD_CANNOTOPEN) : altRes, parent) { Caption = caption; File = file; + FileW = fileW; Error = error; } @@ -1558,7 +1649,12 @@ CFileErrorDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) CStaticText* name; if ((name = new CStaticText(HWindow, IDS_FILENAME, STF_PATH_ELLIPSIS)) != NULL) - name->SetTextToDblQuotesIfNeeded(File); + { + if (FileW != NULL) + name->SetTextToDblQuotesIfNeededW(FileW); + else + name->SetTextToDblQuotesIfNeeded(File); + } else TRACE_E(LOW_MEMORY); @@ -1651,7 +1747,8 @@ MENU_TEMPLATE_ITEM FileErrorDlgMenu[] = COverwriteDlg::COverwriteDlg(HWND parent, const char* sourceName, const char* sourceAttr, const char* targetName, const char* targetAttr, BOOL yesnocancel, - BOOL dirOverwrite) : CCommonDialog(HLanguage, + BOOL dirOverwrite, const wchar_t* sourceNameW, + const wchar_t* targetNameW) : CCommonDialog(HLanguage, dirOverwrite ? IDD_DIROVERWRITE : (yesnocancel ? IDD_OVERWRITE2 : IDD_OVERWRITE), parent) { @@ -1659,6 +1756,8 @@ COverwriteDlg::COverwriteDlg(HWND parent, const char* sourceName, const char* so SourceAttr = sourceAttr; TargetName = targetName; TargetAttr = targetAttr; + SourceNameW = sourceNameW; + TargetNameW = targetNameW; } INT_PTR @@ -1671,11 +1770,21 @@ COverwriteDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { CStaticText *source, *target; if ((source = new CStaticText(HWindow, IDS_SOURCENAME, STF_PATH_ELLIPSIS)) != NULL) - source->SetTextToDblQuotesIfNeeded(SourceName); + { + if (SourceNameW != NULL) + source->SetTextToDblQuotesIfNeededW(SourceNameW); + else + source->SetTextToDblQuotesIfNeeded(SourceName); + } else TRACE_E(LOW_MEMORY); if ((target = new CStaticText(HWindow, IDS_TARGETNAME, STF_PATH_ELLIPSIS)) != NULL) - target->SetTextToDblQuotesIfNeeded(TargetName); + { + if (TargetNameW != NULL) + target->SetTextToDblQuotesIfNeededW(TargetNameW); + else + target->SetTextToDblQuotesIfNeeded(TargetName); + } else TRACE_E(LOW_MEMORY); @@ -1760,11 +1869,15 @@ CHiddenOrSystemDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) // CCannotMoveDlg::CCannotMoveDlg(HWND parent, int resID, char* sourceName, - char* targetName, char* error) : CCommonDialog(HLanguage, resID, parent) + char* targetName, char* error, + const wchar_t* sourceNameW, + const wchar_t* targetNameW) : CCommonDialog(HLanguage, resID, parent) { SourceName = sourceName; TargetName = targetName; Error = error; + SourceNameW = sourceNameW; + TargetNameW = targetNameW; } INT_PTR @@ -1777,11 +1890,21 @@ CCannotMoveDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { CStaticText *source, *target; if ((source = new CStaticText(HWindow, IDS_SOURCENAME, STF_PATH_ELLIPSIS)) != NULL) - source->SetTextToDblQuotesIfNeeded(SourceName); + { + if (SourceNameW != NULL) + source->SetTextToDblQuotesIfNeededW(SourceNameW); + else + source->SetTextToDblQuotesIfNeeded(SourceName); + } else TRACE_E(LOW_MEMORY); if ((target = new CStaticText(HWindow, IDS_TARGETNAME, STF_PATH_ELLIPSIS)) != NULL) - target->SetTextToDblQuotesIfNeeded(TargetName); + { + if (TargetNameW != NULL) + target->SetTextToDblQuotesIfNeededW(TargetNameW); + else + target->SetTextToDblQuotesIfNeeded(TargetName); + } else TRACE_E(LOW_MEMORY); diff --git a/src/dialogs.h b/src/dialogs.h index de41ce59e..d93a97e55 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -94,6 +94,8 @@ class CCopyMoveMoreDialog : public CCommonDialog int PathBufSize; char** History; int HistoryCount; + wchar_t** HistoryW; + int HistoryWCount; CCriteriaData* CriteriaInOut; // used to transfer data in and out of the dialog (on OK) CCriteriaData* Criteria; // allocated because static declaration would require juggling headers BOOL HavePermissions; @@ -108,9 +110,10 @@ class CCopyMoveMoreDialog : public CCommonDialog CButton* MoreButton; // Unicode support for filenames that cannot be represented in ANSI + BOOL UseUnicodeInput; std::wstring PathW; // Unicode input path (set via SetUnicodePath) std::wstring ResultW; // Unicode result (populated on OK) - HWND HUnicodeEdit; // Overlay Unicode edit control (when PathW is set) + CUnicodeNameInputController UnicodeInput; public: // 'history' determines whether the dialog will contain a combobox (TRUE) or an editline (FALSE) @@ -118,12 +121,13 @@ class CCopyMoveMoreDialog : public CCommonDialog CCopyMoveMoreDialog(HWND parent, char* path, int pathBufSize, char* title, CTruncatedString* subject, DWORD helpID, char* history[], int historyCount, CCriteriaData* criteriaInOut, - BOOL havePermissions, BOOL supportsADS); + BOOL havePermissions, BOOL supportsADS, + wchar_t* historyW[] = NULL, int historyWCount = 0); ~CCopyMoveMoreDialog(); void SetUnicodePath(const std::wstring& pathW); const std::wstring& GetUnicodeResult() const { return ResultW; } - BOOL IsUnicodeMode() const { return !PathW.empty(); } + BOOL IsUnicodeMode() const { return UseUnicodeInput; } virtual void Validate(CTransferInfo& ti); virtual void Transfer(CTransferInfo& ti); @@ -394,6 +398,8 @@ class CProgressDialog : public CCommonDialog char PrepositionCache[100]; CPathBuffer SourceCache; CPathBuffer TargetCache; + std::wstring SourceCacheW; + std::wstring TargetCacheW; // values are stored and drawn only when the timer fires BOOL OperationProgressCacheIsDirty; @@ -409,7 +415,7 @@ class CFileErrorDlg : public CCommonDialog { public: CFileErrorDlg(HWND parent, const char* caption, const char* file, const char* error, - BOOL noSkip = FALSE, int altRes = 0); + BOOL noSkip = FALSE, int altRes = 0, const wchar_t* fileW = NULL); protected: virtual INT_PTR DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -417,6 +423,7 @@ class CFileErrorDlg : public CCommonDialog const char *Caption, *File, *Error; + const wchar_t* FileW; }; // @@ -458,13 +465,16 @@ class CErrorCopyingPermissionsDlg : public CCommonDialog { public: CErrorCopyingPermissionsDlg(HWND parent, const char* sourceFile, - const char* targetFile, DWORD error); + const char* targetFile, DWORD error, + const wchar_t* sourceFileW = NULL, const wchar_t* targetFileW = NULL); protected: virtual INT_PTR DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam); const char* SourceFile; const char* TargetFile; + const wchar_t* SourceFileW; + const wchar_t* TargetFileW; DWORD Error; }; @@ -474,12 +484,14 @@ class CErrorCopyingPermissionsDlg : public CCommonDialog class CErrorCopyingDirTimeDlg : public CCommonDialog { public: - CErrorCopyingDirTimeDlg(HWND parent, const char* targetFile, DWORD error); + CErrorCopyingDirTimeDlg(HWND parent, const char* targetFile, DWORD error, + const wchar_t* targetFileW = NULL); protected: virtual INT_PTR DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam); const char* TargetFile; + const wchar_t* TargetFileW; DWORD Error; }; @@ -491,7 +503,8 @@ class COverwriteDlg : public CCommonDialog public: COverwriteDlg(HWND parent, const char* sourceName, const char* sourceAttr, const char* targetName, const char* targetAttr, BOOL yesnocancel = FALSE, - BOOL dirOverwrite = FALSE); + BOOL dirOverwrite = FALSE, const wchar_t* sourceNameW = NULL, + const wchar_t* targetNameW = NULL); protected: virtual INT_PTR DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -500,6 +513,8 @@ class COverwriteDlg : public CCommonDialog *SourceAttr, *TargetName, *TargetAttr; + const wchar_t *SourceNameW, + *TargetNameW; }; // @@ -574,7 +589,8 @@ class CCannotMoveDlg : public CCommonDialog { public: CCannotMoveDlg(HWND parent, int resID, char* sourceName, char* targetName, - char* error); + char* error, const wchar_t* sourceNameW = NULL, + const wchar_t* targetNameW = NULL); protected: virtual INT_PTR DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -582,6 +598,8 @@ class CCannotMoveDlg : public CCommonDialog char *SourceName, *TargetName, *Error; + const wchar_t *SourceNameW, + *TargetNameW; }; // diff --git a/src/dialogs_file_transforms.cpp b/src/dialogs_file_transforms.cpp index 625896df8..d55a28787 100644 --- a/src/dialogs_file_transforms.cpp +++ b/src/dialogs_file_transforms.cpp @@ -603,7 +603,11 @@ CCopyMoveDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL processed = FALSE; if (DirectoryHelper) - processed = OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); + { + processed = UnicodeInput.IsEnabled() + ? OnDirectoryKeyDownW((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, UnicodeInput.GetControlHandle()) + : OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); + } if (!processed) processed = OnKeyDownHandleSelectAll((DWORD)lParam, HWindow, IDE_PATH); SetWindowLongPtr(HWindow, DWLP_MSGRESULT, processed); @@ -612,7 +616,10 @@ CCopyMoveDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_BUTTON: { - OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); + if (UnicodeInput.IsEnabled()) + OnDirectoryButtonW(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam, UnicodeInput.GetControlHandle()); + else + OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); return 0; } @@ -718,7 +725,8 @@ MENU_TEMPLATE_ITEM EditNewFileDialogMenu[] = CCopyMoveMoreDialog::CCopyMoveMoreDialog(HWND parent, char* path, int pathBufSize, char* title, CTruncatedString* subject, DWORD helpID, char* history[], int historyCount, CCriteriaData* criteriaInOut, - BOOL havePermissions, BOOL supportsADS) + BOOL havePermissions, BOOL supportsADS, + wchar_t* historyW[], int historyWCount) : CCommonDialog(HLanguage, IDD_COPYMOVEMOREDIALOG, helpID, parent) { if (history == NULL) @@ -730,6 +738,8 @@ CCopyMoveMoreDialog::CCopyMoveMoreDialog(HWND parent, char* path, int pathBufSiz PathBufSize = pathBufSize; History = history; HistoryCount = historyCount; + HistoryW = historyW; + HistoryWCount = historyWCount; CriteriaInOut = criteriaInOut; Criteria = new CCriteriaData(); *Criteria = *CriteriaInOut; @@ -737,13 +747,17 @@ CCopyMoveMoreDialog::CCopyMoveMoreDialog(HWND parent, char* path, int pathBufSiz HavePermissions = havePermissions; SupportsADS = supportsADS; MoreButton = NULL; - HUnicodeEdit = NULL; + UseUnicodeInput = FALSE; } void CCopyMoveMoreDialog::SetUnicodePath(const std::wstring& pathW) { + UseUnicodeInput = TRUE; PathW = pathW; ResultW.clear(); +#ifndef _UNICODE + UnicodeWnd = TRUE; +#endif } CCopyMoveMoreDialog::~CCopyMoveMoreDialog() @@ -771,40 +785,39 @@ void CCopyMoveMoreDialog::Transfer(CTransferInfo& ti) } else { - // Get Unicode result if Unicode edit control is present - if (HUnicodeEdit != NULL) + if (UnicodeInput.IsEnabled()) { - int len = GetWindowTextLengthW(HUnicodeEdit); - if (len > 0) - { - std::vector buffer(len + 1); - GetWindowTextW(HUnicodeEdit, buffer.data(), len + 1); - ResultW = buffer.data(); - } - // Also convert to ANSI for the Path buffer (fallback) + ResultW = UnicodeInput.GetText(); + PathW = ResultW; WideCharToMultiByte(CP_ACP, 0, ResultW.c_str(), -1, Path, PathBufSize, "?", NULL); } else { SendMessage(hWnd, WM_GETTEXT, PathBufSize, (LPARAM)Path); + int lenW = GetWindowTextLengthW(hWnd); + if (lenW > 0) + { + std::vector buffer((size_t)lenW + 1); + GetWindowTextW(hWnd, buffer.data(), lenW + 1); + ResultW.assign(buffer.data()); + } + else + ResultW.clear(); } AddValueToStdHistoryValues(History, HistoryCount, Path, FALSE); + if (HistoryW != NULL && HistoryWCount > 0) + { + std::wstring wide = ResultW.empty() ? AnsiToWide(Path) : ResultW; + if (!wide.empty()) + AddValueToStdHistoryValuesW(HistoryW, HistoryWCount, wide.c_str(), FALSE); + } } } } else { - // Get Unicode result from overlay edit if present - if (ti.Type == ttDataFromWindow && HUnicodeEdit != NULL) - { - int len = GetWindowTextLengthW(HUnicodeEdit); - if (len > 0) - { - std::vector buffer(len + 1); - GetWindowTextW(HUnicodeEdit, buffer.data(), len + 1); - ResultW = buffer.data(); - } - } + if (ti.Type == ttDataFromWindow && UnicodeInput.IsEnabled()) + ResultW = UnicodeInput.GetText(); ti.EditLine(IDE_PATH, Path, PathBufSize); } TransferCriteriaControls(ti); @@ -1160,46 +1173,33 @@ CCopyMoveMoreDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (!Criteria->IsDirty()) // collapse the dialog if Criteria do not contain any data DisplayMore(FALSE, TRUE); - // If Unicode path is set, create an overlay Unicode edit control - if (!PathW.empty()) + INT_PTR ret = CCommonDialog::DialogProc(uMsg, wParam, lParam); + + HWND hCombo = GetDlgItem(HWindow, IDE_PATH); + if (UseUnicodeInput && hCombo != NULL) { - HWND hCombo = GetDlgItem(HWindow, IDE_PATH); - if (hCombo != NULL) + TRACE_I("CCopyMoveMoreDialog: UseUnicodeInput=" << UseUnicodeInput << " hCombo=" << (void*)hCombo + << " PathW.len=" << PathW.length() + << " PathW.hasNonAscii=" << (PathW.find_first_not_of(L" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~") != std::wstring::npos ? 1 : 0) + << " Path[0..20]=" << std::string(Path, (std::min)((size_t)20, strlen(Path)))); + if (UnicodeInput.EnableForCombo(HWindow, IDE_PATH, PathW, HistoryW, HistoryWCount, PathBufSize, -1)) { - COMBOBOXINFO cbi = {sizeof(COMBOBOXINFO)}; - if (GetComboBoxInfo(hCombo, &cbi) && cbi.hwndItem) - { - RECT editRect; - GetWindowRect(cbi.hwndItem, &editRect); - MapWindowPoints(NULL, HWindow, (LPPOINT)&editRect, 2); - - HFONT hFont = (HFONT)SendMessage(hCombo, WM_GETFONT, 0, 0); - - HUnicodeEdit = CreateWindowExW( - 0, L"EDIT", PathW.c_str(), - WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, - editRect.left, editRect.top, - editRect.right - editRect.left, editRect.bottom - editRect.top, - HWindow, NULL, HInstance, NULL); - - if (HUnicodeEdit != NULL) - { - ShowWindow(cbi.hwndItem, SW_HIDE); - if (hFont != NULL) - SendMessage(HUnicodeEdit, WM_SETFONT, (WPARAM)hFont, TRUE); - SetWindowSubclass(HUnicodeEdit, UnicodeEditSubclassProc, 1, (DWORD_PTR)hCombo); - PostMessage(HUnicodeEdit, EM_SETSEL, 0, -1); // select all - SetFocus(HUnicodeEdit); - } - } + // Leave the hidden ANSI combo subclassed; the Unicode controller owns the visible control. + TRACE_I("CCopyMoveMoreDialog: Unicode combo created, GetText().len=" << UnicodeInput.GetText().length()); + } + else + { + TRACE_E("CCopyMoveMoreDialog: EnableForCombo FAILED!"); } } - break; + return UnicodeInput.IsEnabled() ? FALSE : ret; } case WM_USER_KEYDOWN: { - BOOL processed = OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); + BOOL processed = UnicodeInput.IsEnabled() + ? OnDirectoryKeyDownW((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, UnicodeInput.GetControlHandle()) + : OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); if (!processed) processed = OnKeyDownHandleSelectAll((DWORD)lParam, HWindow, IDE_PATH); SetWindowLongPtr(HWindow, DWLP_MSGRESULT, processed); @@ -1208,10 +1208,19 @@ CCopyMoveMoreDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_BUTTON: { - OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); + if (UnicodeInput.IsEnabled()) + OnDirectoryButtonW(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam, UnicodeInput.GetControlHandle()); + else + OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); return 0; } + case WM_DESTROY: + { + UnicodeInput.Reset(); + break; + } + case WM_USER_BUTTONDROPDOWN: { if (LOWORD(wParam) == IDC_MORE) @@ -1294,18 +1303,14 @@ MENU_TEMPLATE_ITEM CopyMoveMoreDialogMenu[] = case WM_COMMAND: { - // Handle combobox selection change - update overlay edit if present - if (LOWORD(wParam) == IDE_PATH && HIWORD(wParam) == CBN_SELCHANGE && HUnicodeEdit != NULL) + if (LOWORD(wParam) == IDE_PATH && UnicodeInput.IsEnabled()) { - HWND hCombo = (HWND)lParam; - CPathBuffer ansiText; - int len = (int)SendMessage(hCombo, WM_GETTEXT, ansiText.Size(), (LPARAM)ansiText.Get()); - if (len > 0) + HWND hCombo2 = (HWND)lParam; + if (hCombo2 == UnicodeInput.GetControlHandle()) { - std::wstring wideText(ansiText.Size(), L'\0'); - int wideLen = MultiByteToWideChar(CP_ACP, 0, ansiText, -1, &wideText[0], (int)wideText.size()); - wideText.resize(wideLen > 0 ? wideLen - 1 : 0); - SetWindowTextW(HUnicodeEdit, wideText.c_str()); + BOOL isDropdownOpen = (BOOL)SendMessage(hCombo2, CB_GETDROPPEDSTATE, 0, 0); + if (sally::unicode::ShouldSyncUnicodeComboSelection(HIWORD(wParam), isDropdownOpen)) + UnicodeInput.SyncSelectionToEdit(); } } @@ -1493,7 +1498,9 @@ CChangeDirDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_KEYDOWN: { - BOOL processed = OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); + BOOL processed = (HUnicodeEdit != NULL) + ? OnDirectoryKeyDownW((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, HUnicodeEdit) + : OnDirectoryKeyDown((DWORD)lParam, HWindow, IDE_PATH, PathBufSize, IDB_BROWSE); if (!processed) processed = OnKeyDownHandleSelectAll((DWORD)lParam, HWindow, IDE_PATH); SetWindowLongPtr(HWindow, DWLP_MSGRESULT, processed); @@ -1502,7 +1509,10 @@ CChangeDirDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_BUTTON: { - OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); + if (HUnicodeEdit != NULL) + OnDirectoryButtonW(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam, HUnicodeEdit); + else + OnDirectoryButton(HWindow, IDE_PATH, PathBufSize, IDB_BROWSE, wParam, lParam); return 0; } diff --git a/src/dialogs_highlight_registry.cpp b/src/dialogs_highlight_registry.cpp index f981ad52a..a221a212c 100644 --- a/src/dialogs_highlight_registry.cpp +++ b/src/dialogs_highlight_registry.cpp @@ -422,6 +422,8 @@ CConfiguration::CConfiguration() SelectHistory[i] = NULL; for (i = 0; i < COPY_HISTORY_SIZE; i++) CopyHistory[i] = NULL; + for (i = 0; i < COPY_HISTORY_SIZE; i++) + CopyHistoryW[i] = NULL; for (i = 0; i < EDIT_HISTORY_SIZE; i++) EditHistory[i] = NULL; for (i = 0; i < CHANGEDIR_HISTORY_SIZE; i++) @@ -434,6 +436,8 @@ CConfiguration::CConfiguration() QuickRenameHistory[i] = NULL; for (i = 0; i < EDITNEW_HISTORY_SIZE; i++) EditNewHistory[i] = NULL; + for (i = 0; i < CREATEDIR_HISTORY_SIZE; i++) + CreateDirHistoryW[i] = NULL; for (i = 0; i < QUICKRENAME_HISTORY_SIZE; i++) QuickRenameHistoryW[i] = NULL; for (i = 0; i < EDITNEW_HISTORY_SIZE; i++) @@ -587,6 +591,14 @@ void CConfiguration::ClearHistory() CopyHistory[i] = NULL; } } + for (i = 0; i < COPY_HISTORY_SIZE; i++) + { + if (CopyHistoryW[i] != NULL) + { + free(CopyHistoryW[i]); + CopyHistoryW[i] = NULL; + } + } for (i = 0; i < EDIT_HISTORY_SIZE; i++) { @@ -642,6 +654,15 @@ void CConfiguration::ClearHistory() } } + for (i = 0; i < CREATEDIR_HISTORY_SIZE; i++) + { + if (CreateDirHistoryW[i] != NULL) + { + free(CreateDirHistoryW[i]); + CreateDirHistoryW[i] = NULL; + } + } + for (i = 0; i < QUICKRENAME_HISTORY_SIZE; i++) { if (QuickRenameHistoryW[i] != NULL) diff --git a/src/dialogs_tip_of_day.cpp b/src/dialogs_tip_of_day.cpp index 0cb855c88..8b9a7dc29 100644 --- a/src/dialogs_tip_of_day.cpp +++ b/src/dialogs_tip_of_day.cpp @@ -2258,10 +2258,14 @@ CErrorSettingAttrsDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) // CErrorCopyingPermissionsDlg::CErrorCopyingPermissionsDlg(HWND parent, const char* sourceFile, - const char* targetFile, DWORD error) : CCommonDialog(HLanguage, IDD_CANNOTCOPYPERMISSIONS, parent) + const char* targetFile, DWORD error, + const wchar_t* sourceFileW, + const wchar_t* targetFileW) : CCommonDialog(HLanguage, IDD_CANNOTCOPYPERMISSIONS, parent) { SourceFile = sourceFile; TargetFile = targetFile; + SourceFileW = sourceFileW; + TargetFileW = targetFileW; Error = error; } @@ -2275,12 +2279,22 @@ CErrorCopyingPermissionsDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { CStaticText* name; if ((name = new CStaticText(HWindow, IDS_SOURCENAME, STF_PATH_ELLIPSIS)) != NULL) - name->SetTextToDblQuotesIfNeeded(SourceFile); + { + if (SourceFileW != NULL) + name->SetTextToDblQuotesIfNeededW(SourceFileW); + else + name->SetTextToDblQuotesIfNeeded(SourceFile); + } else TRACE_E(LOW_MEMORY); if ((name = new CStaticText(HWindow, IDS_TARGETNAME, STF_PATH_ELLIPSIS)) != NULL) - name->SetTextToDblQuotesIfNeeded(TargetFile); + { + if (TargetFileW != NULL) + name->SetTextToDblQuotesIfNeededW(TargetFileW); + else + name->SetTextToDblQuotesIfNeeded(TargetFile); + } else TRACE_E(LOW_MEMORY); @@ -2311,9 +2325,11 @@ CErrorCopyingPermissionsDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) // CErrorCopyingDirTimeDlg // -CErrorCopyingDirTimeDlg::CErrorCopyingDirTimeDlg(HWND parent, const char* targetFile, DWORD error) : CCommonDialog(HLanguage, IDD_CANNOTCOPYDIRTIME, parent) +CErrorCopyingDirTimeDlg::CErrorCopyingDirTimeDlg(HWND parent, const char* targetFile, DWORD error, + const wchar_t* targetFileW) : CCommonDialog(HLanguage, IDD_CANNOTCOPYDIRTIME, parent) { TargetFile = targetFile; + TargetFileW = targetFileW; Error = error; } @@ -2327,7 +2343,12 @@ CErrorCopyingDirTimeDlg::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { CStaticText* name; if ((name = new CStaticText(HWindow, IDS_TARGETNAME, STF_PATH_ELLIPSIS)) != NULL) - name->SetTextToDblQuotesIfNeeded(TargetFile); + { + if (TargetFileW != NULL) + name->SetTextToDblQuotesIfNeededW(TargetFileW); + else + name->SetTextToDblQuotesIfNeeded(TargetFile); + } else TRACE_E(LOW_MEMORY); diff --git a/src/execute.cpp b/src/execute.cpp index 273fe72ca..0cb81ec08 100644 --- a/src/execute.cpp +++ b/src/execute.cpp @@ -27,6 +27,15 @@ CComboboxEdit::CComboboxEdit() SelEnd = -1; } +#ifndef _UNICODE +CComboboxEdit::CComboboxEdit(BOOL unicodeWnd) + : CWindow(ooAllocated, unicodeWnd) +{ + SelStart = 0; + SelEnd = -1; +} +#endif + LRESULT CComboboxEdit::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { diff --git a/src/execute.h b/src/execute.h index 263d44a7d..db47813ef 100644 --- a/src/execute.h +++ b/src/execute.h @@ -20,6 +20,15 @@ class CComboboxEdit : public CWindow public: CComboboxEdit(); +#ifndef _UNICODE + // Subclass the combo's edit child as a Unicode window. Required when the + // combo lives inside a Unicode dialog and the dialog issues + // SetDlgItemTextW into it: the combo control itself is Unicode, but + // AttachToWindow's default ANSI subclass would flip the edit child's + // WindowProc to ANSI, and WM_SETTEXT W→A would convert through CP_ACP + // on its way to the edit. Unicode subclass keeps the W path intact. + explicit CComboboxEdit(BOOL unicodeWnd); +#endif void GetSel(DWORD* start, DWORD* end); diff --git a/src/files_window_actions.cpp b/src/files_window_actions.cpp index efbd33f70..9bf461add 100644 --- a/src/files_window_actions.cpp +++ b/src/files_window_actions.cpp @@ -26,8 +26,11 @@ extern "C" #include "salshlib.h" #include "shellib.h" #include "common/widepath.h" +#include "common/fsutil.h" #include "ui/IPrompter.h" #include "common/unicode/helpers.h" +#include "common/unicode/NameRenderPolicy.h" +#include "common/unicode/PanelPathPolicy.h" // // **************************************************************************** @@ -45,7 +48,10 @@ void CFilesWindow::HandsOff(BOOL off) } else { - ChangeDirectory((CFilesWindow*)this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + if (sally::unicode::HasWidePathW(GetPathW())) + ChangeDirectoryW((CFilesWindow*)this, GetPathW(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + else + ChangeDirectory((CFilesWindow*)this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); HANDLES(EnterCriticalSection(&TimeCounterSection)); int t1 = MyTimeCounter++; HANDLES(LeaveCriticalSection(&TimeCounterSection)); @@ -224,21 +230,29 @@ void CFilesWindow::Execute(int index) // backup data for TopIndexMem strcpy(path, GetPath()); int topIndex = ListBox->GetTopIndex(); + std::wstring archiveNameW; if (!linkIsFile) { // construction of full archive name for ChangePathToArchive - strcpy(fullName, GetPath()); - if (!SalPathAppend(fullName, fileName, fullName.Size())) + archiveNameW = sally::unicode::BuildPanelChildPathW(GetPathW(), fileName, file->NameW); + if (archiveNameW.length() >= SAL_MAX_LONG_PATH - 2) { gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(IDS_TOOLONGNAME)); UpdateWindow(HWindow); EndStopRefresh(); return; } + std::string fullNameA; + if (sally::unicode::TryExactAnsiFallback(archiveNameW, fullNameA)) + fullName.Assign(fullNameA.c_str()); + else + fullName[0] = 0; } + else + archiveNameW = AnsiToWide(fullName); BOOL noChange; - if (ChangePathToArchive(fullName, "", -1, NULL, FALSE, &noChange)) // entering the archive successfully + if (ChangePathToArchiveW(archiveNameW.c_str(), L"", -1, NULL, FALSE, &noChange)) // entering the archive successfully { if (linkIsFile) TopIndexMem.Clear(); // long jump @@ -331,18 +345,25 @@ void CFilesWindow::Execute(int index) int caretIndex = GetCaretIndex(); // new path - strcpy(fullName, path); - if (!SalPathAppend(fullName, dir->Name, fullName.Size())) + std::wstring fullNameW = sally::unicode::BuildPanelChildPathW(GetPathW(), dir->Name, dir->NameW); + if (fullNameW.length() >= SAL_MAX_LONG_PATH - 2) { gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(IDS_TOOLONGNAME)); EndStopRefresh(); return; } + std::string fullNameA; + BOOL fullNameHasExactAnsi = sally::unicode::TryExactAnsiFallback(fullNameW, fullNameA) ? TRUE : FALSE; + if (fullNameHasExactAnsi) + fullName.Assign(fullNameA.c_str()); + else + fullName[0] = 0; // Vista: we handle unlistable junction points: change path to junction point target CPathBuffer junctTgtPath; int repPointType; - if (GetPathDriveType() == DRIVE_FIXED && (dir->Attr & FILE_ATTRIBUTE_REPARSE_POINT) && + if (fullNameHasExactAnsi && + GetPathDriveType() == DRIVE_FIXED && (dir->Attr & FILE_ATTRIBUTE_REPARSE_POINT) && GetReparsePointDestination(fullName, junctTgtPath, junctTgtPath.Size(), &repPointType, TRUE) && repPointType == 2 /* JUNCTION POINT */ && SalPathAppend(fullName, "*", fullName.Size())) @@ -368,7 +389,7 @@ void CFilesWindow::Execute(int index) BOOL noChange; BOOL refresh = TRUE; - if (ChangePathToDisk(HWindow, fullName, -1, NULL, &noChange, FALSE)) + if (ChangePathToDiskW(HWindow, fullNameW.c_str(), -1, NULL, &noChange, FALSE)) { TopIndexMem.Push(path, topIndex); // we remember top index for return } @@ -421,7 +442,10 @@ void CFilesWindow::Execute(int index) // actual path change BOOL noChange; - if (!ChangePathToDisk(HWindow, GetPath(), topIndex, path, &noChange)) + BOOL exitedArchive = (sally::unicode::HasWidePathW(GetPathW())) + ? ChangePathToDiskW(HWindow, GetPathW(), topIndex, path, &noChange) + : ChangePathToDisk(HWindow, GetPath(), topIndex, path, &noChange); + if (!exitedArchive) { // failed to shorten the path - reject-close-archive or long jump if (!noChange) TopIndexMem.Clear(); // long jump @@ -461,8 +485,16 @@ void CFilesWindow::Execute(int index) topIndex = -1; // actual path change - if (!ChangePathToArchive(GetZIPArchive(), path, topIndex, prevDir)) // "always false" - { // failed to shorten path - long jump + BOOL shortenedArchPath; + if (sally::unicode::HasWidePathW(GetZIPArchiveW())) + { + std::wstring zipPathW = AnsiToWide(path); + shortenedArchPath = ChangePathToArchiveW(GetZIPArchiveW(), zipPathW.c_str(), topIndex, prevDir); + } + else + shortenedArchPath = ChangePathToArchive(GetZIPArchive(), path, topIndex, prevDir); + if (!shortenedArchPath) // "always false" + { // failed to shorten path - long jump TopIndexMem.Clear(); } } @@ -483,7 +515,15 @@ void CFilesWindow::Execute(int index) else { BOOL noChange; - if (ChangePathToArchive(GetZIPArchive(), fullName, -1, NULL, FALSE, &noChange)) // "always true" + BOOL enteredSubdir; + if (sally::unicode::HasWidePathW(GetZIPArchiveW())) + { + std::wstring fullNameW = AnsiToWide(fullName); + enteredSubdir = ChangePathToArchiveW(GetZIPArchiveW(), fullNameW.c_str(), -1, NULL, FALSE, &noChange); + } + else + enteredSubdir = ChangePathToArchive(GetZIPArchive(), fullName, -1, NULL, FALSE, &noChange); + if (enteredSubdir) // "always true" { TopIndexMem.Push(doublePath, topIndex); // we remember top index for return } @@ -1361,7 +1401,8 @@ void CFilesWindow::CloseCurrentPath(HWND parent, BOOL cancel, BOOL detachFS, BOO { const char* path = GetPath(); // HICON hIcon = GetFileOrPathIconAux(path, FALSE, TRUE); // we retrieve the icon - MainWindow->DirHistoryAddPathUnique(0, path, NULL, NULL /*hIcon*/, NULL, NULL); + MainWindow->DirHistoryAddPathUnique(0, path, NULL, NULL /*hIcon*/, NULL, NULL, + GetPathW(), nullptr); if (!newPathIsTheSame) UserWorkedOnThisPath = FALSE; } @@ -1388,7 +1429,8 @@ void CFilesWindow::CloseCurrentPath(HWND parent, BOOL cancel, BOOL detachFS, BOO { if (UserWorkedOnThisPath) { - MainWindow->DirHistoryAddPathUnique(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, NULL); + MainWindow->DirHistoryAddPathUnique(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, NULL, + GetZIPArchiveW(), GetZIPPathW()); if (!newPathIsTheSame) UserWorkedOnThisPath = FALSE; } @@ -1533,13 +1575,15 @@ void CFilesWindow::RefreshPathHistoryData() // we try to record a new top-index and focus-name if (Is(ptZIPArchive)) { - PathHistory->ChangeActualPathData(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, topIndex, file->Name); + PathHistory->ChangeActualPathData(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, topIndex, file->Name, + GetZIPArchiveW(), GetZIPPathW()); } else { if (Is(ptDisk)) { - PathHistory->ChangeActualPathData(0, GetPath(), NULL, NULL, NULL, topIndex, file->Name); + PathHistory->ChangeActualPathData(0, GetPath(), NULL, NULL, NULL, topIndex, file->Name, + GetPathW(), nullptr); } else { @@ -1564,13 +1608,14 @@ void CFilesWindow::RemoveCurrentPathFromHistory() if (Is(ptZIPArchive)) { - PathHistory->RemoveActualPath(1, GetZIPArchive(), GetZIPPath(), NULL, NULL); + PathHistory->RemoveActualPath(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, + GetZIPArchiveW(), GetZIPPathW()); } else { if (Is(ptDisk)) { - PathHistory->RemoveActualPath(0, GetPath(), NULL, NULL, NULL); + PathHistory->RemoveActualPath(0, GetPath(), NULL, NULL, NULL, GetPathW(), nullptr); } else { @@ -1621,6 +1666,17 @@ BOOL CFilesWindow::ChangePathToDisk(HWND parent, const char* path, int suggested //TRACE_I("change-to-disk: begin"); + if (Is(ptDisk) && path != NULL) + { + std::wstring mappedWidePath = sally::unicode::MapRelatedAnsiPathToWidePath(path, GetPath(), GetPathW()); + if (!mappedWidePath.empty() && mappedWidePath != AnsiToWide(path)) + { + return ChangePathToDiskW(parent, mappedWidePath.c_str(), suggestedTopIndex, suggestedFocusName, noChange, + refreshListBox, canForce, isRefresh, failReason, + shorterPathWarning, tryCloseReason); + } + } + if (strlen(path) >= SAL_MAX_LONG_PATH - 2) { gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(IDS_TOOLONGNAME)); @@ -1960,6 +2016,155 @@ BOOL CFilesWindow::ChangePathToDisk(HWND parent, const char* path, int suggested return ret; } +BOOL CFilesWindow::ChangePathToDiskW(HWND parent, const wchar_t* path, int suggestedTopIndex, + const char* suggestedFocusName, BOOL* noChange, + BOOL refreshListBox, BOOL canForce, BOOL isRefresh, int* failReason, + BOOL shorterPathWarning, int tryCloseReason) +{ + (void)shorterPathWarning; + + std::wstring requestedPath = path != NULL ? path : L""; + std::string tracePath = WideToAnsi(requestedPath); + CALL_STACK_MESSAGE9("CFilesWindow::ChangePathToDiskW(, %s, %d, %s, , %d, %d, %d, , %d, %d)", tracePath.c_str(), + suggestedTopIndex, suggestedFocusName, refreshListBox, canForce, isRefresh, + shorterPathWarning, tryCloseReason); + + if (requestedPath.length() >= SAL_MAX_LONG_PATH - 2) + { + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(IDS_TOOLONGNAME)); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + + RefreshPathHistoryData(); + + if (noChange != NULL) + *noChange = TRUE; + + if (!isRefresh) + MainWindow->CancelPanelsUI(); + MainWindow->UpdateDefaultDir(TRUE); + + int errTextID; + if (!SalGetFullNameW(requestedPath, &errTextID, Is(ptDisk) ? GetPathW() : NULL)) + { + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(errTextID)); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + + std::wstring changedPath = requestedPath; + BOOL tryNet = !CriticalShutdown; + DWORD err, lastErr; + BOOL pathInvalid, cut; + if (!SalCheckAndRestorePathWithCutW(parent, changedPath, tryNet, err, lastErr, pathInvalid, cut, FALSE)) + { + if (pathInvalid || err == ERROR_USER_TERMINATED) + return FALSE; + SalCheckPathW(TRUE, requestedPath.c_str(), lastErr != ERROR_SUCCESS ? lastErr : err, TRUE, parent); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + if (cut) + { + suggestedTopIndex = -1; + suggestedFocusName = NULL; + } + + BOOL setWait = (GetCursor() != LoadCursor(NULL, IDC_WAIT)); + HCURSOR oldCur = NULL; + if (setWait) + oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); + BeginStopRefresh(); + + BOOL detachFS; + if (!PrepareCloseCurrentPath(parent, canForce, TRUE, detachFS, tryCloseReason)) + { + EndStopRefresh(); + if (setWait) + SetCursor(oldCur); + if (failReason != NULL) + *failReason = CHPPFR_CANNOTCLOSEPATH; + return FALSE; + } + + BOOL updateIcon = TRUE; + if (UseSystemIcons || UseThumbnails) + SleepIconCacheThread(); + + CloseCurrentPath(parent, FALSE, detachFS, FALSE, isRefresh, TRUE); + + if (DirectoryLine != NULL) + DirectoryLine->HideThrobberAndSecurityIcon(); + + SetPanelType(ptDisk); + SetPathW(changedPath.c_str()); + + if (updateIcon || !GetNetworkDrive()) + UpdateDriveIcon(FALSE); + + if (noChange != NULL) + *noChange = FALSE; + + BOOL cannotList = !CommonRefresh(parent, suggestedTopIndex, suggestedFocusName, refreshListBox, TRUE, isRefresh); + if (isRefresh && !cannotList && GetMonitorChanges() && !AutomaticRefresh) + { + Sleep(400); + cannotList = !CommonRefresh(parent, suggestedTopIndex, suggestedFocusName, refreshListBox, TRUE, isRefresh); + } + + if (cannotList) + { + // Mirror ANSI ChangePathToDisk recovery: shorten to the nearest accessible parent; + // if no parent is reachable, fall back to the user-configured rescue path or the first fixed drive. + BOOL recovered = FALSE; + std::wstring tryPath = changedPath; + while (CutDirectoryW(tryPath)) + { + DWORD probeErr = SalCheckPathW(FALSE, tryPath.c_str(), ERROR_SUCCESS, FALSE, parent); + if (probeErr != ERROR_SUCCESS) + continue; + SetPathW(tryPath.c_str()); + BOOL stillCannot = !CommonRefresh(parent, -1, NULL, refreshListBox, TRUE, isRefresh); + if (!stillCannot) + { + recovered = TRUE; + break; + } + } + + EndStopRefresh(); + if (setWait) + SetCursor(oldCur); + + if (recovered) + { + if (failReason != NULL) + *failReason = CHPPFR_SHORTERPATH; + return FALSE; + } + + // No accessible parent was found within the same root; long-jump out via the rescue/fixed-drive helper. + TopIndexMem.Clear(); + ChangeToRescuePathOrFixedDrive(parent, noChange, refreshListBox, canForce, tryCloseReason, failReason); + if (failReason != NULL && *failReason == CHPPFR_SUCCESS) + *failReason = CHPPFR_SHORTERPATH; + return FALSE; + } + + EndStopRefresh(); + if (setWait) + SetCursor(oldCur); + + BOOL openedRequestedPath = IsTheSamePathW(GetPathW(), requestedPath.c_str()); + if (failReason != NULL) + *failReason = openedRequestedPath ? CHPPFR_SUCCESS : CHPPFR_SHORTERPATH; + return openedRequestedPath; +} + BOOL CFilesWindow::ChangePathToArchive(const char* archive, const char* archivePath, int suggestedTopIndex, const char* suggestedFocusName, BOOL forceUpdate, BOOL* noChange, BOOL refreshListBox, @@ -2048,7 +2253,10 @@ BOOL CFilesWindow::ChangePathToArchive(const char* archive, const char* archiveP if (forceUpdateInt) // a path change is required; opening the archive failed, go back to disk { // we're certainly in an archive (it's a panel refresh of an archive) // if possible, exit the archive (possibly all the way to the "fixed-drive") - ChangePathToDisk(HWindow, GetPath(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); + if (sally::unicode::HasWidePathW(GetPathW())) + ChangePathToDiskW(HWindow, GetPathW(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); + else + ChangePathToDisk(HWindow, GetPath(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); } else { @@ -2256,7 +2464,10 @@ BOOL CFilesWindow::ChangePathToArchive(const char* archive, const char* archiveP if (err != ERROR_USER_TERMINATED) { // if possible, exit the archive (possibly all the way to the "fixed-drive") - ChangePathToDisk(HWindow, GetPath(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); + if (sally::unicode::HasWidePathW(GetPathW())) + ChangePathToDiskW(HWindow, GetPathW(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); + else + ChangePathToDisk(HWindow, GetPath(), -1, NULL, noChange, refreshListBox, FALSE, isRefresh); } else // user pressed ESC -> the path is probably inaccessible, we go straight to the "fixed-drive" { @@ -2366,7 +2577,14 @@ BOOL CFilesWindow::ChangePathToArchive(const char* archive, const char* archiveP { if (UserWorkedOnThisPath) { - MainWindow->DirHistoryAddPathUnique(1, GetZIPArchive(), currentPath, NULL, NULL, NULL); + // Archive root is wide-correct via GetZIPArchiveW(); the internal path + // 'currentPath' is the saved ANSI from before the change so wide it + // through AnsiToWide (typical ZIP entries are ASCII; Unicode-only + // internal entries would still be lossy, matching the existing ANSI + // fidelity). + std::wstring currentPathW = AnsiToWide(currentPath); + MainWindow->DirHistoryAddPathUnique(1, GetZIPArchive(), currentPath, NULL, NULL, NULL, + GetZIPArchiveW(), currentPathW.c_str()); UserWorkedOnThisPath = FALSE; } @@ -2378,6 +2596,58 @@ BOOL CFilesWindow::ChangePathToArchive(const char* archive, const char* archiveP return ok; } +BOOL CFilesWindow::ChangePathToArchiveW(const wchar_t* archive, const wchar_t* archivePath, + int suggestedTopIndex, const char* suggestedFocusName, + BOOL forceUpdate, BOOL* noChange, BOOL refreshListBox, + int* failReason, BOOL isRefresh, BOOL canFocusFileName, + BOOL isHistory) +{ + std::wstring archiveW = archive != NULL ? archive : L""; + std::wstring archivePathW = archivePath != NULL ? archivePath : L""; + int errTextID; + if (!SalGetFullNameW(archiveW, &errTextID, Is(ptDisk) ? GetPathW() : NULL)) + { + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), LoadStrW(errTextID)); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + + std::string archiveA; + if (!sally::unicode::TryExactAnsiFallback(archiveW, archiveA)) + { + std::wstring shortArchiveW = GetShortPathW(archiveW.c_str()); + if (shortArchiveW.empty() || !sally::unicode::TryExactAnsiFallback(shortArchiveW, archiveA)) + { + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), archiveW.c_str()); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + } + + std::string archivePathA; + if (!sally::unicode::TryExactAnsiFallback(archivePathW, archivePathA)) + { + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), archivePathW.c_str()); + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + return FALSE; + } + + BOOL ret = ChangePathToArchive(archiveA.c_str(), archivePathA.c_str(), suggestedTopIndex, + suggestedFocusName, forceUpdate, noChange, refreshListBox, + failReason, isRefresh, canFocusFileName, isHistory); + if (ret && Is(ptZIPArchive)) + { + SetZIPArchiveW(archiveW.c_str()); + std::wstring archiveDirW = GetDirectoryW(archiveW.c_str()); + if (!archiveDirW.empty()) + SetPathW(archiveDirW.c_str()); + } + return ret; +} + BOOL CFilesWindow::ChangeAndListPathOnFS(const char* fsName, int fsNameIndex, const char* fsUserPart, CPluginFSInterfaceEncapsulation& pluginFS, CSalamanderDirectory* dir, CPluginDataInterfaceAbstract*& pluginData, BOOL& shorterPath, @@ -2781,7 +3051,7 @@ BOOL CFilesWindow::ChangePathToPluginFS(const char* fsName, const char* fsUserPa DirectoryLine->HideThrobberAndSecurityIcon(); SetPanelType(ptPluginFS); - SetPath(GetPath()); // detach the path from Snooper (stop monitoring changes on Path) + SetPathW(GetPathW()); // detach the path from Snooper (stop monitoring changes on Path) SetPluginFS(pluginFS.GetInterface(), plugin->DLLName.c_str(), plugin->Version.c_str(), plugin->GetPluginInterfaceForFS()->GetInterface(), plugin->GetPluginInterface()->GetInterface(), @@ -3163,6 +3433,27 @@ BOOL CFilesWindow::ChangePathToPluginFS(const char* fsName, const char* fsUserPa } } +BOOL CFilesWindow::ChangePathToPluginFSW(const wchar_t* fsName, const wchar_t* fsUserPart, int suggestedTopIndex, + const char* suggestedFocusName, BOOL forceUpdate, int mode, + BOOL* noChange, BOOL refreshListBox, int* failReason, BOOL isRefresh, + BOOL canFocusFileName, BOOL convertPathToInternal) +{ + std::string fsNameA; + std::string fsUserPartA; + if (!sally::unicode::TryExactAnsiFallback(fsName != NULL ? fsName : L"", fsNameA) || + !sally::unicode::TryExactAnsiFallback(fsUserPart != NULL ? fsUserPart : L"", fsUserPartA)) + { + if (failReason != NULL) + *failReason = CHPPFR_INVALIDPATH; + gPrompter->ShowError(LoadStrW(IDS_ERRORCHANGINGDIR), fsUserPart != NULL ? fsUserPart : L""); + return FALSE; + } + + return ChangePathToPluginFS(fsNameA.c_str(), fsUserPartA.c_str(), suggestedTopIndex, + suggestedFocusName, forceUpdate, mode, noChange, refreshListBox, + failReason, isRefresh, canFocusFileName, convertPathToInternal); +} + BOOL CFilesWindow::ChangePathToDetachedFS(int fsIndex, int suggestedTopIndex, const char* suggestedFocusName, BOOL refreshListBox, int* failReason, const char* newFSName, @@ -3313,7 +3604,7 @@ BOOL CFilesWindow::ChangePathToDetachedFS(int fsIndex, int suggestedTopIndex, DirectoryLine->HideThrobberAndSecurityIcon(); SetPanelType(ptPluginFS); - SetPath(GetPath()); // detach the path from Snooper (stop monitoring changes on Path) + SetPathW(GetPathW()); // detach the path from Snooper (stop monitoring changes on Path) SetPluginFS(pluginFS->GetInterface(), plugin->DLLName.c_str(), plugin->Version.c_str(), plugin->GetPluginInterfaceForFS()->GetInterface(), plugin->GetPluginInterface()->GetInterface(), @@ -3683,26 +3974,57 @@ void CFilesWindow::RefreshListBox(int suggestedXOffset, //--- name BOOL extIsInExtColumn = extColumnIsVisible && (!isDir || Configuration.SortDirsByExt) && f->Ext[0] != 0 && f->Ext > f->Name + 1; // exception for names like ".htaccess"; they appear in Name even though they are extensions + sally::unicode::NameWidthMeasurementPlan widthPlan = + sally::unicode::BuildNameWidthMeasurementPlan( + f->Name, + f->NameLen, + f->Ext, + f->NameW, + isDir != FALSE, + Configuration.SortDirsByExt != FALSE, + extIsInExtColumn != FALSE); if (Columns[0].FixedWidth == 0 || (autoWidthColumns & VIEW_SHOW_EXTENSION) && extIsInExtColumn) { - AlterFileName(formatedFileName, f->Name, f->NameLen, // preparation of the formatted name to also compute the width of the separate Ext column - Configuration.FileNameFormat, 0, isDir); if (Columns[0].FixedWidth == 0) { - nameLen = extIsInExtColumn ? (int)(f->Ext - f->Name - 1) : f->NameLen; - - GetTextExtentPoint32(dc, formatedFileName, nameLen, &act); + if (widthPlan.UseWide) + { + GetTextExtentPoint32W(dc, f->NameW, widthPlan.NameLength, &act); + } + else + { + AlterFileName(formatedFileName, f->Name, f->NameLen, // preparation of the formatted name to also compute the width of the separate Ext column + Configuration.FileNameFormat, 0, isDir); + nameLen = widthPlan.NameLength; + GetTextExtentPoint32(dc, formatedFileName, nameLen, &act); + } act.cx += 1 + IconSizes[ICONSIZE_16] + 1 + 2 + SPACE_WIDTH; if (columnWidthName < act.cx) columnWidthName = act.cx; if (nameColWidths != NULL) nameColWidths[i] = act.cx; } + else if (!f->UseWideName()) + { + AlterFileName(formatedFileName, f->Name, f->NameLen, // preparation of the formatted name to also compute the width of the separate Ext column + Configuration.FileNameFormat, 0, isDir); + } } //--- extension if ((autoWidthColumns & VIEW_SHOW_EXTENSION) && extIsInExtColumn) { - GetTextExtentPoint32(dc, formatedFileName + (int)(f->Ext - f->Name), (int)(f->NameLen - (f->Ext - f->Name)), &act); + if (widthPlan.UseWide) + { + const wchar_t* extPosW = sally::unicode::GetWideExtensionStart(f->NameW); + if (extPosW != NULL && widthPlan.ExtensionLength > 0) + GetTextExtentPoint32W(dc, extPosW, widthPlan.ExtensionLength, &act); + else + act.cx = 0; + } + else + { + GetTextExtentPoint32(dc, formatedFileName + (int)(f->Ext - f->Name), widthPlan.ExtensionLength, &act); + } act.cx += SPACE_WIDTH; if (columnWidthExt < act.cx) columnWidthExt = act.cx; diff --git a/src/files_window_clipboard_paths.cpp b/src/files_window_clipboard_paths.cpp index 91622a952..272fce9b5 100644 --- a/src/files_window_clipboard_paths.cpp +++ b/src/files_window_clipboard_paths.cpp @@ -73,6 +73,104 @@ BOOL CFilesWindow::ParsePath(char* path, int& type, BOOL& isDir, char*& secondPa Is(ptDisk) || Is(ptZIPArchive), curPath, curArchivePath, error, pathBufSize); } +BOOL CFilesWindow::ParsePathW(std::wstring& path, int& type, BOOL& isDir, wchar_t*& secondPart, + const wchar_t* errorTitle, std::wstring* nextFocus, int* error) +{ + CALL_STACK_MESSAGE_NONE + std::wstring curPath; + const wchar_t* curArchivePath = NULL; + GetGeneralPathW(curPath); + if (Is(ptZIPArchive)) + { + SalPathAddBackslashW(curPath); + curArchivePath = GetZIPArchiveW(); + } + return SalParsePathW(HWindow, path, type, isDir, secondPart, errorTitle, nextFocus, + Is(ptDisk) || Is(ptZIPArchive), curPath.c_str(), curArchivePath, error); +} + +static BOOL BuildCopyMoveDataFromHDrop(IDataObject* dataObj, BOOL copy, CCopyMoveData** outData) +{ + if (outData == NULL) + return FALSE; + *outData = NULL; + if (dataObj == NULL) + return FALSE; + + FORMATETC formatEtc; + formatEtc.cfFormat = CF_HDROP; + formatEtc.ptd = NULL; + formatEtc.dwAspect = DVASPECT_CONTENT; + formatEtc.lindex = -1; + formatEtc.tymed = TYMED_HGLOBAL; + + STGMEDIUM stgMedium; + stgMedium.tymed = TYMED_HGLOBAL; + stgMedium.hGlobal = NULL; + stgMedium.pUnkForRelease = NULL; + + if (dataObj->GetData(&formatEtc, &stgMedium) != S_OK) + return FALSE; + + BOOL ok = FALSE; + DROPFILES* data = (DROPFILES*)HANDLES(GlobalLock(stgMedium.hGlobal)); + if (data != NULL) + { + CCopyMoveData* array = new CCopyMoveData(100, 50); + if (array != NULL) + { + array->MakeCopyOfName = copy; + if (data->fWide) + { + const wchar_t* fileW = (const wchar_t*)(((const char*)data) + data->pFiles); + while (*fileW != 0) + { + CCopyMoveRecord* record = new CCopyMoveRecord(fileW, (const wchar_t*)NULL); + if (record == NULL) + break; + array->Add(record); + if (!array->IsGood()) + { + array->ResetState(); + break; + } + while (*fileW++ != 0) + ; + } + } + else + { + const char* fileA = ((const char*)data) + data->pFiles; + while (*fileA != 0) + { + CCopyMoveRecord* record = new CCopyMoveRecord(fileA, (const char*)NULL); + if (record == NULL) + break; + array->Add(record); + if (!array->IsGood()) + { + array->ResetState(); + break; + } + while (*fileA++ != 0) + ; + } + } + + if (array->IsGood()) + { + *outData = array; + ok = TRUE; + } + else + DestroyCopyMoveData(array); + } + } + HANDLES(GlobalUnlock(stgMedium.hGlobal)); + ReleaseStgMedium(&stgMedium); + return ok; +} + int CFilesWindow::GetPanelCode() { CALL_STACK_MESSAGE_NONE @@ -247,49 +345,102 @@ BOOL CFilesWindow::ClipboardPaste(BOOL onlyLinks, BOOL onlyTest, const char* pas if (ownRutine) // execute our own routine - copy or move { - if (pastePath != NULL) - lstrcpyn(DropPath, pastePath, DropPath.Size()); - else - lstrcpyn(DropPath, GetPath(), DropPath.Size()); - CImpDropTarget* dropTarget = new CImpDropTarget(MainWindow->HWindow, DoCopyMove, this, - GetCurrentDirClipboard, this, - DropEnd, this, NULL, NULL, NULL, NULL, - UseOwnRutine, DoDragDropOper, this, - NULL, NULL); - if (dropTarget != NULL) + if (Is(ptDisk) && ourClipDataObject) { - OurClipDataObject = ourClipDataObject; - POINTL pt; - pt.x = pt.y = 0; - DWORD eff = effect; - dropTarget->DragEnter(dataObj, 0, pt, &effect); - effect = eff; - dropTarget->DragOver(0, pt, &effect); - effect = eff; - dropTarget->Drop(dataObj, 0, pt, &eff); - - FocusFirstNewItem = TRUE; // if it will be a single new file, let the focus find it - - dropTarget->Release(); - OurClipDataObject = FALSE; - if (effect == DROPEFFECT_MOVE) + CCopyMoveData* array = NULL; + if (BuildCopyMoveDataFromHDrop(dataObj, effect == DROPEFFECT_COPY, &array)) { - if (OpenClipboard(HWindow)) + // Honor the caller-supplied pastePath when present (set by + // the shell extension's folder-context-menu Paste at + // shellsup.cpp). The earlier FakeDataObject branch above + // already honors it; this HDROP branch was dropping it on + // the floor and pasting into the panel root instead of + // the clicked folder. + CPathBuffer targetPath; + std::wstring targetPathW; + const BOOL haveExplicitPath = + pastePath != NULL && pastePath[0] != '\0'; + if (haveExplicitPath) { - EmptyClipboard(); - CloseClipboard(); + lstrcpyn(targetPath, pastePath, targetPath.Size()); + targetPathW = AnsiToWide(pastePath); } else - TRACE_E("OpenClipboard() has failed!"); - } + { + lstrcpyn(targetPath, GetPath(), targetPath.Size()); + targetPathW = GetPathW(); + } + DropCopyMove(effect == DROPEFFECT_COPY, targetPath, + targetPathW.empty() ? NULL : targetPathW.c_str(), + array); + DestroyCopyMoveData(array); - if (effect != DROPEFFECT_COPY) - { - IdleRefreshStates = TRUE; // force state variable check on next Idle - IdleCheckClipboard = TRUE; // also enable clipboard checking + FocusFirstNewItem = TRUE; // if it will be a single new file, let the focus find it + + if (effect == DROPEFFECT_MOVE) + { + if (OpenClipboard(HWindow)) + { + EmptyClipboard(); + CloseClipboard(); + } + else + TRACE_E("OpenClipboard() has failed!"); + } + + if (effect != DROPEFFECT_COPY) + { + IdleRefreshStates = TRUE; // force state variable check on next Idle + IdleCheckClipboard = TRUE; // also enable clipboard checking + } } + } + else + { + if (pastePath != NULL) + lstrcpyn(DropPath, pastePath, DropPath.Size()); + else + lstrcpyn(DropPath, GetPath(), DropPath.Size()); + CImpDropTarget* dropTarget = new CImpDropTarget(MainWindow->HWindow, DoCopyMove, this, + GetCurrentDirClipboard, this, + DropEnd, this, NULL, NULL, NULL, NULL, + UseOwnRutine, DoDragDropOper, this, + NULL, NULL); + if (dropTarget != NULL) + { + OurClipDataObject = ourClipDataObject; + POINTL pt; + pt.x = pt.y = 0; + DWORD eff = effect; + dropTarget->DragEnter(dataObj, 0, pt, &effect); + effect = eff; + dropTarget->DragOver(0, pt, &effect); + effect = eff; + dropTarget->Drop(dataObj, 0, pt, &eff); + + FocusFirstNewItem = TRUE; // if it will be a single new file, let the focus find it + + dropTarget->Release(); + OurClipDataObject = FALSE; + if (effect == DROPEFFECT_MOVE) + { + if (OpenClipboard(HWindow)) + { + EmptyClipboard(); + CloseClipboard(); + } + else + TRACE_E("OpenClipboard() has failed!"); + } + + if (effect != DROPEFFECT_COPY) + { + IdleRefreshStates = TRUE; // force state variable check on next Idle + IdleCheckClipboard = TRUE; // also enable clipboard checking + } - // panel refresh is performed in DropCopyMove + // panel refresh is performed in DropCopyMove + } } } else diff --git a/src/files_window_copy_move.cpp b/src/files_window_copy_move.cpp index f98360f2a..d49357584 100644 --- a/src/files_window_copy_move.cpp +++ b/src/files_window_copy_move.cpp @@ -19,6 +19,7 @@ #include "ui/IPrompter.h" #include "common/unicode/helpers.h" #include "common/unicode/CopyNamePolicy.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/IEnvironment.h" #include "common/widepath.h" @@ -30,7 +31,7 @@ CSelectionSnapshot CFilesWindow::TakeSnapshot(CActionType type, int selCount, { CSelectionSnapshot snap; snap.SourcePath = GetPath(); - // TODO: snap.SourcePathW when wide path is available + snap.SourcePathW = GetPathW(); // Map CActionType to EActionType switch (type) @@ -402,6 +403,7 @@ BOOL CFilesWindow::MoveFiles(const char* source, const char* target, const char* } BOOL scriptOK = TRUE; // result of script creation, success? + std::wstring sourceDirW = AnsiToWide(sourceDir); do { if (file.cFileName[0] != 0 && @@ -416,7 +418,7 @@ BOOL CFilesWindow::MoveFiles(const char* source, const char* target, const char* targetPathState, targetSupADS, targetIsFAT32, NULL, fileNameA, NULL, NULL, NULL, file.dwFileAttributes, NULL, TRUE, FALSE, fastDirectoryMove, NULL, NULL, &file.ftLastWriteTime, - srcAndTgtPathsFlags, file.cFileName)) + srcAndTgtPathsFlags, sourceDirW.c_str(), file.cFileName)) { scriptOK = FALSE; break; @@ -429,7 +431,7 @@ BOOL CFilesWindow::MoveFiles(const char* source, const char* target, const char* fileNameA, NULL, CQuadWord(file.nFileSizeLow, file.nFileSizeHigh), NULL, NULL, file.dwFileAttributes, NULL, FALSE, NULL, - srcAndTgtPathsFlags, file.cFileName)) + srcAndTgtPathsFlags, file.cFileName, AnsiToWide(sourceDir).c_str())) { scriptOK = FALSE; break; @@ -551,7 +553,7 @@ void AddStringToNames(TIndirectArray* usedNames, const char* txt) } BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* targetDir, - CCopyMoveData* data) + const wchar_t* targetDirW, CCopyMoveData* data) { CALL_STACK_MESSAGE3("CFilesWindow::BuildScriptMain2(, %d, %s, )", copy, targetDir); if (!script->IsGood()) @@ -643,6 +645,11 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target CPathBuffer mapNameBuf; // Heap-allocated for long path support strcpy(targetPath, targetDir); SalPathAddBackslash(targetPath, targetPath.Size()); + std::wstring targetPathWide = (targetDirW != NULL && targetDirW[0] != L'\0') + ? std::wstring(targetDirW) + : (Is(ptDisk) ? std::wstring(GetPathW()) : AnsiToWide(targetPath)); + if (!targetPathWide.empty() && targetPathWide[targetPathWide.length() - 1] != L'\\') + targetPathWide += L'\\'; BOOL targetIsFAT32 /*, targetSupEFS*/; BOOL targetSupADS = IsPathOnVolumeSupADS(targetPath, &targetIsFAT32); script->TargetPathSupADS = targetSupADS; @@ -650,7 +657,14 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target // script->TargetPathSupEFS = targetSupEFS; CTargetPathState targetPathState = GetTargetPathState(tpsUnknown, targetPath); char* targetName = targetPath + strlen(targetPath); - const std::wstring targetDirWithBackslashW = AnsiToWide(targetPath); + const std::wstring& targetDirWithBackslashW = targetPathWide; + auto targetCandidateExistsW = [&](const char* candidateNameA) -> bool + { + if (candidateNameA == NULL) + return false; + std::wstring candidateW = targetDirWithBackslashW + AnsiToWide(candidateNameA); + return GetFileAttributesW(candidateW.c_str()) != INVALID_FILE_ATTRIBUTES; + }; BOOL makeCopyOfName = data->MakeCopyOfName; std::unique_ptr> usedNames; // RAII: auto-deleted when scope exits std::vector usedNamesW; @@ -673,6 +687,14 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target const wchar_t* mapNameW = NULL; std::wstring mapNameWide; wchar_t* fileNameW = data->At(i)->FileNameW; // Wide filename for Unicode support + std::wstring itemSourcePathW; + const wchar_t* itemSourcePathWPtr = NULL; + if (fileNameW != NULL) + { + itemSourcePathW = fileNameW; + CutDirectoryW(itemSourcePathW); + itemSourcePathWPtr = itemSourcePathW.c_str(); + } // Extract just the filename part from the wide path (if available) wchar_t* wideNameOnly = NULL; @@ -734,8 +756,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target // mapName must be NULL here, otherwise data->MakeCopyOfName could not be TRUE if (!handledWideCopyName && ((isKnown = ContainsString(usedNames.get(), targetName)) != 0 || - GetFileAttributesW(AnsiToWide(targetPath).c_str()) != INVALID_FILE_ATTRIBUTES) - ) + targetCandidateExistsW(targetName))) { // name already exists, we must generate a new one if (!isKnown) AddStringToNames(usedNames.get(), targetName); @@ -830,7 +851,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target if (strlen(targetName) < MAX_PATH) // name assembly succeeded, otherwise we ignore the result { if ((isKnown = ContainsString(usedNames.get(), targetName)) != 0 || - GetFileAttributesW(AnsiToWide(targetPath).c_str()) != INVALID_FILE_ATTRIBUTES) + targetCandidateExistsW(targetName)) { if (!isKnown) AddStringToNames(usedNames.get(), targetName); @@ -863,7 +884,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target if (strlen(targetName) < MAX_PATH) // name assembly succeeded, otherwise we ignore the result { if ((isKnown = ContainsString(usedNames.get(), targetName)) != 0 || - GetFileAttributesW(AnsiToWide(targetPath).c_str()) != INVALID_FILE_ATTRIBUTES) + targetCandidateExistsW(targetName)) { if (!isKnown) AddStringToNames(usedNames.get(), targetName); @@ -918,7 +939,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target if (strlen(targetName) < MAX_PATH) // name assembly succeeded, otherwise we ignore the result { if ((isKnown = ContainsString(usedNames.get(), targetName)) != 0 || - GetFileAttributesW(AnsiToWide(targetPath).c_str()) != INVALID_FILE_ATTRIBUTES) + targetCandidateExistsW(targetName)) { if (!isKnown) AddStringToNames(usedNames.get(), targetName); @@ -952,7 +973,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target if (strlen(targetName) < MAX_PATH) // name assembly succeeded, otherwise we ignore the result { if ((isKnown = ContainsString(usedNames.get(), targetName)) != 0 || - GetFileAttributesW(AnsiToWide(targetPath).c_str()) != INVALID_FILE_ATTRIBUTES) + targetCandidateExistsW(targetName)) { if (!isKnown) AddStringToNames(usedNames.get(), targetName); @@ -983,7 +1004,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target targetPathState, targetSupADS, targetIsFAT32, NULL, s + 1, NULL, NULL, mapName, attrs, NULL, TRUE, TRUE, fastDirectoryMove, NULL, NULL, NULL, srcAndTgtPathsFlags, - wideNameOnly)) + itemSourcePathWPtr, wideNameOnly)) { SetCurrentDirectoryToSystem(); return FALSE; // usedNames auto-deleted by unique_ptr @@ -1016,7 +1037,9 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target if (!BuildScriptFile(script, type, sourcePath, sourceSupADS, targetPath, targetPathState, targetSupADS, targetIsFAT32, NULL, s + 1, NULL, size, NULL, mapName, attrs, NULL, TRUE, - NULL, srcAndTgtPathsFlags, wideNameOnly, mapNameW)) + NULL, srcAndTgtPathsFlags, wideNameOnly, + itemSourcePathWPtr, mapNameW, + targetDirWithBackslashW.c_str())) { SetCurrentDirectoryToSystem(); return FALSE; // usedNames auto-deleted by unique_ptr @@ -1065,7 +1088,7 @@ BOOL CFilesWindow::BuildScriptMain2(COperations* script, BOOL copy, char* target return TRUE; } -void CFilesWindow::DropCopyMove(BOOL copy, char* targetPath, CCopyMoveData* data) +void CFilesWindow::DropCopyMove(BOOL copy, char* targetPath, const wchar_t* targetPathW, CCopyMoveData* data) { CALL_STACK_MESSAGE3("CFilesWindow::DropCopyMove(%d, %s, )", copy, targetPath); if (!FilesActionInProgress) @@ -1105,7 +1128,7 @@ void CFilesWindow::DropCopyMove(BOOL copy, char* targetPath, CCopyMoveData* data HCURSOR oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); - BOOL res = BuildScriptMain2(script, copy, targetPath, data); + BOOL res = BuildScriptMain2(script, copy, targetPath, targetPathW, data); // swapped so the main window can be activated (must not be disabled), otherwise it switches to another app EnableWindow(MainWindow->HWindow, TRUE); @@ -1185,7 +1208,8 @@ BOOL CFilesWindow::BuildScriptMain(COperations* script, CActionType type, char* targetPath, char* mask, int selCount, int* selection, CFileData* oneFile, CAttrsData* attrsData, CChangeCaseData* chCaseData, - BOOL onlySize, CCriteriaData* filterCriteria) + BOOL onlySize, CCriteriaData* filterCriteria, + const wchar_t* targetPathW) { CALL_STACK_MESSAGE5("CFilesWindow::BuildScriptMain(, %d, %s, %s, %d, , , , , ,)", type, targetPath, mask, selCount); @@ -1284,6 +1308,8 @@ BOOL CFilesWindow::BuildScriptMain(COperations* script, CActionType type, CPathBuffer sourcePath; // Heap-allocated for long path support strcpy(sourcePath, GetPath()); + std::wstring sourcePathWide = sally::unicode::EffectivePanelPathW(GetPath(), GetPathW()); + const wchar_t* sourcePathWArg = !sourcePathWide.empty() ? sourcePathWide.c_str() : NULL; BOOL sourceSupADS = FALSE; BOOL targetSupADS = FALSE; @@ -1397,7 +1423,7 @@ BOOL CFilesWindow::BuildScriptMain(COperations* script, CActionType type, useName, useDOSName, attrsData, NULL, oneFile->Attr, chCaseData, TRUE, onlySize, fastDirectoryMove, filterCriteria, NULL, &oneFile->LastWrite, - srcAndTgtPathsFlags, useNameW)) + srcAndTgtPathsFlags, sourcePathWArg, useNameW, targetPathW)) { SetCurrentDirectoryToSystem(); return FALSE; @@ -1469,7 +1495,8 @@ BOOL CFilesWindow::BuildScriptMain(COperations* script, CActionType type, targetPathState, targetSupADS, targetIsFAT32, mask, useName, useDOSName, oneFile->Size, attrsData, NULL, oneFile->Attr, chCaseData, onlySize, NULL, - srcAndTgtPathsFlags, useNameW)) + srcAndTgtPathsFlags, useNameW, + sourcePathWArg, NULL, targetPathW)) { SetCurrentDirectoryToSystem(); return FALSE; @@ -1561,7 +1588,8 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s DWORD sourceDirAttr, CChangeCaseData* chCaseData, BOOL firstLevelDir, BOOL onlySize, BOOL fastDirectoryMove, CCriteriaData* filterCriteria, BOOL* canDelUpperDirAfterMove, FILETIME* sourceDirTime, - DWORD srcAndTgtPathsFlags, wchar_t* dirNameW) + DWORD srcAndTgtPathsFlags, const wchar_t* sourcePathW, + wchar_t* dirNameW, const wchar_t* targetPathW) { SLOW_CALL_STACK_MESSAGE16("CFilesWindow::BuildScriptDir(, %d, %s, %d, %s, %d, %d, %d, %s, %s, , , %s, 0x%X, , %d, %d, %d, , , , 0x%X)", type, sourcePath, sourcePathSupADS, targetPath, @@ -1571,6 +1599,10 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s CPathBuffer text; CPathBuffer finalName; // +200 is a reserve (Windows creates paths longer than MAX_PATH) BOOL sourcePathIsNet = (srcAndTgtPathsFlags & OPFL_SRCPATH_IS_NET) != 0; // valid only for atCopy and atMove + const std::wstring effectiveSourcePathW = (sourcePathW != NULL && sourcePathW[0] != L'\0') ? std::wstring(sourcePathW) : std::wstring(); + const std::wstring effectiveTargetPathW = (targetPathW != NULL && targetPathW[0] != L'\0') ? std::wstring(targetPathW) : std::wstring(); + const std::wstring effectiveDirNameW = (dirNameW != NULL) ? std::wstring(dirNameW) : AnsiToWide(dirName != NULL ? dirName : ""); + std::wstring currentSourcePathW; script->DirsCount++; COperation op; @@ -1608,6 +1640,20 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s while (*s != 0) *st++ = *s++; *st = 0; + if (!effectiveSourcePathW.empty()) + currentSourcePathW = sally::unicode::BuildPanelChildPathW(effectiveSourcePathW, dirName, dirNameW); + auto setCurrentDirSourceNameW = [&](COperation& operation) { + if (!currentSourcePathW.empty()) + { + operation.SetSourceNameW(currentSourcePathW, std::wstring()); + return; + } + if (!effectiveDirNameW.empty()) + { + std::string parentSourcePath(sourcePath, sourceEnd - sourcePath); + operation.SetSourceNameW(parentSourcePath.c_str(), effectiveDirNameW); + } + }; //--- build the path to targetDirName char* targetEnd = NULL; BOOL checkNewDirName = FALSE; @@ -1748,6 +1794,12 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s op.SourceName = NULL; goto _ERROR; } + if (!currentSourcePathW.empty() || dirNameW != NULL) + { + setCurrentDirSourceNameW(op); + if (!effectiveTargetPathW.empty()) + op.SetTargetNameW(effectiveTargetPathW, effectiveDirNameW); + } *sourceEnd = 0; // restoring sourcePath *targetEnd = 0; // restoring targetPath script->Add(op); @@ -1821,6 +1873,12 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s op.SourceName = NULL; goto _ERROR; } + if (!currentSourcePathW.empty() || dirNameW != NULL) + { + setCurrentDirSourceNameW(op); + if (!effectiveTargetPathW.empty()) + op.SetTargetNameW(effectiveTargetPathW, effectiveDirNameW); + } createDirIndex = script->Add(op); if (!script->IsGood()) { @@ -1968,6 +2026,12 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s op.SourceName = NULL; goto _ERROR; } + if (!currentSourcePathW.empty() || dirNameW != NULL) + { + setCurrentDirSourceNameW(op); + if (!effectiveTargetPathW.empty()) + op.SetTargetNameW(effectiveTargetPathW, effectiveDirNameW); + } createDirIndex = script->Add(op); if (!script->IsGood()) { @@ -2079,7 +2143,18 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s { WIN32_FIND_DATAW f; strcpy(st, "\\*"); - HANDLE search = SalFindFirstFileHW(sourcePath, &f); + std::wstring searchPathW; + HANDLE search; + if (!currentSourcePathW.empty()) + { + searchPathW = currentSourcePathW; + if (!sally::unicode::HasTrailingSlashW(searchPathW)) + searchPathW += L'\\'; + searchPathW += L"*"; + search = SalFindFirstFileWideH(searchPathW.c_str(), &f); + } + else + search = SalFindFirstFileHW(sourcePath, &f); *st = 0; // remove "\\*" if (search == INVALID_HANDLE_VALUE) { @@ -2187,7 +2262,9 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s cAltNameA[0] != 0 ? cAltNameA : NULL, attrsData, NULL, f.dwFileAttributes, chCaseData, FALSE, onlySize, fastDirectoryMove, filterCriteria, &canDelDirAfterMove, - &f.ftLastWriteTime, srcAndTgtPathsFlags, f.cFileName)) + &f.ftLastWriteTime, srcAndTgtPathsFlags, + !currentSourcePathW.empty() ? currentSourcePathW.c_str() : NULL, + f.cFileName)) { BUILD_ERROR: HANDLES(FindClose(search)); @@ -2207,7 +2284,8 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s cAltNameA[0] != 0 ? cAltNameA : NULL, CQuadWord(f.nFileSizeLow, f.nFileSizeHigh), attrsData, NULL, f.dwFileAttributes, chCaseData, onlySize, &f.ftLastWriteTime, - srcAndTgtPathsFlags, f.cFileName)) + srcAndTgtPathsFlags, f.cFileName, + !currentSourcePathW.empty() ? currentSourcePathW.c_str() : NULL)) goto BUILD_ERROR; } else @@ -2270,11 +2348,20 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s BOOL sameName = strcmp(op.SourceName + offset, op.TargetName + offset) == 0; if (!sameName) { - if (dirNameW != NULL) + if (!currentSourcePathW.empty() || dirNameW != NULL) { - op.SetSourceNameW(sourcePath, dirNameW); - std::wstring alteredW = AlterFileNameW(dirNameW, chCaseData->FileNameFormat, chCaseData->Change, TRUE); - op.SetTargetNameW(sourcePath, alteredW); + setCurrentDirSourceNameW(op); + std::wstring alteredW = AlterFileNameW(effectiveDirNameW.c_str(), chCaseData->FileNameFormat, chCaseData->Change, TRUE); + if (!currentSourcePathW.empty()) + { + std::wstring parentSourcePathW = effectiveSourcePathW; + op.SetTargetNameW(parentSourcePathW, alteredW); + } + else + { + std::string parentSourcePath(sourcePath, sourceEnd - sourcePath); + op.SetTargetNameW(parentSourcePath.c_str(), alteredW); + } } script->Add(op); } @@ -2361,8 +2448,8 @@ BOOL CFilesWindow::BuildScriptDir(COperations* script, CActionType type, char* s if (!skipTooLongSrcNameErr) { op.TargetName = NULL; - if (dirNameW != NULL) - op.SetSourceNameW(sourcePath, dirNameW); + if (!currentSourcePathW.empty() || dirNameW != NULL) + setCurrentDirSourceNameW(op); script->Add(op); if (!script->IsGood()) { @@ -2455,7 +2542,9 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* CAttrsData* attrsData, char* mapName, DWORD sourceFileAttr, CChangeCaseData* chCaseData, BOOL onlySize, FILETIME* fileLastWriteTime, DWORD srcAndTgtPathsFlags, - wchar_t* fileNameW, const wchar_t* mapNameW) + wchar_t* fileNameW, const wchar_t* sourcePathW, + const wchar_t* mapNameW, + const wchar_t* targetPathW) { SLOW_CALL_STACK_MESSAGE14("CFilesWindow::BuildScriptFile(, %d, %s, %d, %s, %d, %d, %d, %s, %s, , , , %s, 0x%X, , %d, , 0x%X)", type, sourcePath, sourcePathSupADS, targetPath, targetPathState, targetPathSupADS, @@ -2465,6 +2554,45 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* CQuadWord fileSizeLoc = fileSize; CPathBuffer message; COperation op; + const std::wstring effectiveSourcePathW = (sourcePathW != NULL && sourcePathW[0] != L'\0') + ? std::wstring(sourcePathW) + : std::wstring(); + const std::wstring effectiveTargetPathW = (targetPathW != NULL && targetPathW[0] != L'\0') + ? std::wstring(targetPathW) + : std::wstring(); + // Only fall back to AnsiToWide when the ANSI mirror is round-trip clean + // (no '?' substitution chars). Otherwise the wide name would gain literal + // '?' characters and CreateFileW later fails with error 123 -- the exact + // symptom of the Unicode-leaf F5 bug. When refusing the fallback we leave + // effectiveFileNameW empty; the downstream gate at !effectiveFileNameW.empty() + // already skips SetSourceNameW in that case (op proceeds with SourceName only, + // SourceNameW is widened by PopulateWidePathsFromAnsi from the still-lossy + // ANSI, and the existing error path surfaces clearly). + const std::wstring effectiveFileNameW = [&]() -> std::wstring { + if (fileNameW != NULL) + return std::wstring(fileNameW); + if (fileName == NULL || fileName[0] == '\0') + return std::wstring(); + if (strchr(fileName, '?') != NULL) + return std::wstring(); + return AnsiToWide(fileName); + }(); + auto setFileSourceNameW = [&](COperation& operation) { + if (effectiveFileNameW.empty()) + return; + if (!effectiveSourcePathW.empty()) + operation.SetSourceNameW(effectiveSourcePathW, effectiveFileNameW); + else + operation.SetSourceNameW(sourcePath, effectiveFileNameW); + }; + auto setFileTargetNameW = [&](COperation& operation, const std::wstring& wideFileName) { + if (wideFileName.empty()) + return; + if (!effectiveSourcePathW.empty()) + operation.SetTargetNameW(effectiveSourcePathW, wideFileName); + else + operation.SetTargetNameW(sourcePath, wideFileName); + }; switch (type) { case atCopy: @@ -2533,17 +2661,30 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* } } - // Set wide paths early (before ADS handling) if Unicode filename is provided - if (fileNameW != NULL) + // Set wide paths early whenever we have a usable wide directory path. + if (!effectiveFileNameW.empty()) { - op.SetSourceNameW(sourcePath, fileNameW); + if (!effectiveSourcePathW.empty()) + op.SetSourceNameW(effectiveSourcePathW, effectiveFileNameW); + else + op.SetSourceNameW(sourcePath, effectiveFileNameW); if (mapNameW != NULL && mapNameW[0] != L'\0') - op.SetTargetNameW(targetPath, mapNameW); + { + if (!effectiveTargetPathW.empty()) + op.SetTargetNameW(effectiveTargetPathW, mapNameW); + else + op.SetTargetNameW(targetPath, mapNameW); + } else { // For target, use the same wide filename if mask is NULL or "*.*" if (mapName == NULL && (mask == NULL || strcmp(mask, "*.*") == 0)) - op.SetTargetNameW(targetPath, fileNameW); + { + if (!effectiveTargetPathW.empty()) + op.SetTargetNameW(effectiveTargetPathW, effectiveFileNameW); + else + op.SetTargetNameW(targetPath, effectiveFileNameW); + } } } @@ -2856,8 +2997,7 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* return skip; } op.TargetName = NULL; - if (fileNameW != NULL) - op.SetSourceNameW(sourcePath, fileNameW); + setFileSourceNameW(op); script->Add(op); if (!script->IsGood()) { @@ -2885,14 +3025,20 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* if (name[l - 1] != '\\') name[l++] = '\\'; memmove(name + l, fileName, 1 + strlen(fileName)); // name is always < MAX_PATH + std::wstring nameW; + if (!effectiveSourcePathW.empty() && !effectiveFileNameW.empty()) + nameW = sally::unicode::BuildPanelChildPathW(effectiveSourcePathW, fileName, fileNameW); CQuadWord s; DWORD err = NO_ERROR; if (FileBasedCompression && !onlySize && // if compression is even possible (sourceFileAttr & (FILE_ATTRIBUTE_COMPRESSED | FILE_ATTRIBUTE_SPARSE_FILE))) // if the file is compressed or sparse (sparse file) { - s.LoDWord = GetCompressedFileSize(name, &s.HiDWord); + if (!nameW.empty()) + s.LoDWord = GetCompressedFileSizeW(nameW.c_str(), &s.HiDWord); + else + s.LoDWord = GetCompressedFileSize(name, &s.HiDWord); err = GetLastError(); - if (err == ERROR_FILE_NOT_FOUND && fileDOSName != NULL && strcmp(fileName, fileDOSName) != 0) + if (nameW.empty() && err == ERROR_FILE_NOT_FOUND && fileDOSName != NULL && strcmp(fileName, fileDOSName) != 0) { // workaround for computing the size of a file that must be accessed via DOS-name when we cannot do it via the UNICODE name (the multibyte version of the name converted back to UNICODE doesn't match the original file name) memmove(name + l, fileDOSName, 1 + strlen(fileDOSName)); // name is always < MAX_PATH s.LoDWord = GetCompressedFileSize(name, &s.HiDWord); @@ -2910,7 +3056,8 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* if (!script->SkipAllCountSizeErrors) { // TODO: Use wide format string when IDS_GETCOMPRFILESIZEERROR supports %ls - std::wstring nameW = AnsiToWide(name); + if (nameW.empty()) + nameW = AnsiToWide(name); script->SkipAllCountSizeErrors = gPrompter->AskYesNo(LoadStrW(IDS_ERRORTITLE), (nameW + L": " + GetErrorTextW(err)).c_str()).type == PromptResult::kYes; @@ -2965,8 +3112,7 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* op.Size = fileSizeLoc; else op.Size = CONVERT_MIN_FILE_SIZE; // zero/small files take at least as long as files of size CONVERT_MIN_FILE_SIZE - if (fileNameW != NULL) - op.SetSourceNameW(sourcePath, fileNameW); + setFileSourceNameW(op); script->Add(op); if (!script->IsGood()) { @@ -2992,10 +3138,9 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* { return skip; } - op.TargetName = (char*)(DWORD_PTR)((GetFileAttributesW(AnsiToWide(op.SourceName).c_str()) & attrsData->AttrAnd) | attrsData->AttrOr); + setFileSourceNameW(op); + op.TargetName = (char*)(DWORD_PTR)((op.GetSourceAttributes() & attrsData->AttrAnd) | attrsData->AttrOr); op.OwnsTargetName = false; // TargetName stores attributes, not a pointer - if (fileNameW != NULL) - op.SetSourceNameW(sourcePath, fileNameW); script->Add(op); if (!script->IsGood()) { @@ -3033,11 +3178,11 @@ BOOL CFilesWindow::BuildScriptFile(COperations* script, CActionType type, char* BOOL sameName = strcmp(op.SourceName + offset, op.TargetName + offset) == 0; if (!sameName) { - if (fileNameW != NULL) + setFileSourceNameW(op); + if (!effectiveFileNameW.empty()) { - op.SetSourceNameW(sourcePath, fileNameW); - std::wstring alteredW = AlterFileNameW(fileNameW, chCaseData->FileNameFormat, chCaseData->Change, FALSE); - op.SetTargetNameW(sourcePath, alteredW); + std::wstring alteredW = AlterFileNameW(effectiveFileNameW.c_str(), chCaseData->FileNameFormat, chCaseData->Change, FALSE); + setFileTargetNameW(op, alteredW); } script->Add(op); } diff --git a/src/files_window_delete_email.cpp b/src/files_window_delete_email.cpp index 3b65254a9..a075cab51 100644 --- a/src/files_window_delete_email.cpp +++ b/src/files_window_delete_email.cpp @@ -17,6 +17,7 @@ #include "common/fsutil.h" #include "common/IFileSystem.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/widepath.h" #include "common/IEnvironment.h" @@ -268,9 +269,12 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count //--- build the target path for copy/move CPathBuffer path; // +200 is a reserve (Windows can create paths longer than MAX_PATH) target->GetGeneralPath(path, path.Size()); + std::wstring pathW = AnsiToWide(path); if (target->Is(ptDisk)) { SalPathAppend(path, "*.*", path.Size()); + pathW = target->GetPathW(); + SalPathAppendW(pathW, L"*.*"); } else { @@ -492,17 +496,19 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count (type == atCopy) ? LoadStr(IDS_COPY) : LoadStr(IDS_MOVE), &str, (type == atCopy) ? IDD_COPYDIALOG : IDD_MOVEDIALOG, Configuration.CopyHistory, COPY_HISTORY_SIZE, - &criteria, havePermissions, supportsADS); - // Note: SetUnicodePath is only needed when the path contains Unicode - // characters that cannot be represented in ANSI (e.g., from CFileData::NameW). - // Do NOT call it for normal ANSI paths — it creates an overlay edit control - // that breaks combobox focus and dropdown behavior. + &criteria, havePermissions, supportsADS, + Configuration.CopyHistoryW, COPY_HISTORY_SIZE); + if (pathW != AnsiToWide(path)) + copyMoveDlg.SetUnicodePath(pathW); res = (int)copyMoveDlg.Execute(); if (res == IDOK && copyMoveDlg.IsUnicodeMode()) { - std::string ansiPath = WideToAnsi(copyMoveDlg.GetUnicodeResult()); + pathW = copyMoveDlg.GetUnicodeResult(); + std::string ansiPath = WideToAnsi(pathW); lstrcpyn(path, ansiPath.c_str(), path.Size()); } + else if (res == IDOK) + pathW = AnsiToWide(path); if (!havePermissions) criteria.CopySecurity = FALSE; if (res != IDOK) @@ -526,10 +532,36 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count int pathType; BOOL pathIsDir; char* secondPart; + std::wstring parsedPathW; + wchar_t* secondPartW = NULL; + std::wstring nextFocusW; CPathBuffer textBuf; - if (ParsePath(path, pathType, pathIsDir, secondPart, - type == atCopy ? LoadStr(IDS_ERRORCOPY) : LoadStr(IDS_ERRORMOVE), - count <= 1 ? nextFocus.Get() : NULL, NULL, path.Size())) + BOOL useWideTargetPath = copyMoveDlg.IsUnicodeMode() && target->Is(ptDisk); + BOOL parsedOk = FALSE; + if (useWideTargetPath) + { + parsedPathW = pathW; + for (size_t slash = 0; slash < parsedPathW.length(); ++slash) + if (parsedPathW[slash] == L'/') + parsedPathW[slash] = L'\\'; + parsedOk = ParsePathW(parsedPathW, pathType, pathIsDir, secondPartW, + type == atCopy ? LoadStrW(IDS_ERRORCOPY) : LoadStrW(IDS_ERRORMOVE), + count <= 1 ? &nextFocusW : NULL, NULL); + if (parsedOk) + { + std::string ansiPath = WideToAnsi(parsedPathW); + lstrcpyn(path, ansiPath.c_str(), path.Size()); + if (!nextFocusW.empty()) + lstrcpyn(nextFocus, WideToAnsi(nextFocusW).c_str(), nextFocus.Size()); + } + } + else + { + parsedOk = ParsePath(path, pathType, pathIsDir, secondPart, + type == atCopy ? LoadStr(IDS_ERRORCOPY) : LoadStr(IDS_ERRORMOVE), + count <= 1 ? nextFocus.Get() : NULL, NULL, path.Size()); + } + if (parsedOk) { // use 'if' instead of a 'switch' to ensure that 'break' and 'continue' work correctly if (pathType == PATH_TYPE_WINDOWS) // Windows path (drive + UNC) @@ -545,14 +577,115 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count } CFileData* dir = (count == 0) ? f : ((indexes[0] < Dirs->Count) ? &Dirs->At(indexes[0]) : &Files->At(indexes[0] - Dirs->Count)); + BOOL splitOk = FALSE; + if (useWideTargetPath) + { + // SalSplitWindowsPathW / SalSplitGeneralPathW mutate + // the buffer assuming SAL_MAX_LONG_PATH writable + // capacity (they SalPathAddBackslashW past wcslen, + // wcscpy "*.*" at wcslen+1, memmove separators in + // and out). std::wstring::data() only guarantees + // size()+1 writable — give the splitter a dedicated + // wide buffer instead of letting it scribble past + // parsedPathW's allocation. + std::vector splitBuf(SAL_MAX_LONG_PATH, 0); + lstrcpynW(splitBuf.data(), parsedPathW.c_str(), (int)splitBuf.size()); + wchar_t* secondPartInSplitBuf = splitBuf.data() + + (secondPartW != NULL ? (secondPartW - parsedPathW.data()) : 0); + wchar_t* maskW = NULL; + splitOk = SalSplitWindowsPathW(HWindow, LoadStrW(type == atCopy ? IDS_COPY : IDS_MOVE), + LoadStrW(type == atCopy ? IDS_ERRORCOPY : IDS_ERRORMOVE), + count, splitBuf.data(), secondPartInSplitBuf, pathIsDir, backslashAtEnd || mustBePath, + dir->NameW != NULL ? dir->NameW : AnsiToWide(dir->Name).c_str(), GetPathW(), maskW); + if (splitOk) + { + // The splitter inserts an internal NUL at the + // path/mask separator. assign(const wchar_t*) + // stops at that NUL, so parsedPathW captures + // just the path portion. + parsedPathW.assign(splitBuf.data()); + std::string ansiPath = WideToAnsi(parsedPathW); + lstrcpyn(path, ansiPath.c_str(), path.Size()); + + // Convert the wide mask (if present) into the + // ANSI path buffer, placed just past the null + // terminator lstrcpyn wrote — same layout the + // legacy ANSI splitter would have produced. + // Without this re-conversion, `mask` would + // point at stale pre-splitter bytes in the + // path buffer; that worked accidentally for + // CP1252 with '?' substitution but corrupts + // the mask under DBCS code pages or when the + // splitter rewrites the mask (e.g. "*.*" + // synthesized for a directory-only target). + const int pathLen = (int)strlen(path); + if (maskW != NULL) + { + std::string maskA = WideToAnsi(maskW); + const int maskBytesWithNul = (int)maskA.length() + 1; + if (pathLen + 1 + maskBytesWithNul <= path.Size()) + { + memcpy(path + pathLen + 1, maskA.c_str(), + (size_t)maskBytesWithNul); + mask = path + pathLen + 1; + } + else + { + mask = NULL; + } + } + else + { + mask = NULL; + } - if (SalSplitWindowsPath(HWindow, LoadStr(type == atCopy ? IDS_COPY : IDS_MOVE), - LoadStr(type == atCopy ? IDS_ERRORCOPY : IDS_ERRORMOVE), - count, path, secondPart, pathIsDir, backslashAtEnd || mustBePath, - dir->Name, GetPath(), mask)) + // secondPart points into the freshly written + // ANSI buffer; compute its byte offset from + // the splitter's wide result. For ASCII paths + // the wide-to-ANSI mapping is identity, so + // the offset survives directly. For mixed + // CJK content under CP_ACP=1252, each lossy + // wide char maps to one '?' byte, also 1-to-1. + // Under DBCS code pages this can drift; treat + // it as a separate known limitation tracked + // alongside the larger Find/dropdown wide + // propagation work in kb/unicode/TODO.md. + if (secondPartInSplitBuf <= splitBuf.data() + wcslen(splitBuf.data())) + secondPart = path + (secondPartInSplitBuf - splitBuf.data()); + else + { + // Boundary fell past the path portion's + // NUL → secondPart was the mask. Point + // it at the mask we just wrote into path. + secondPart = (mask != NULL) ? mask : (path + pathLen); + } + } + } + else + { + splitOk = SalSplitWindowsPath(HWindow, LoadStr(type == atCopy ? IDS_COPY : IDS_MOVE), + LoadStr(type == atCopy ? IDS_ERRORCOPY : IDS_ERRORMOVE), + count, path, secondPart, pathIsDir, backslashAtEnd || mustBePath, + dir->Name, GetPath(), mask); + } + if (splitOk) { if (nextFocus[0] != 0 && secondPart[0] == 0) copyToExistingDir = TRUE; + // After SalSplitWindowsPath{,W} truncates 'path' at the + // mask separator, refresh the wide cache so callers that + // consume auxTargetPathW downstream do not see a stale + // pathW that still contains the mask (e.g. "*.*"). Wide + // branch already has the truncated form in parsedPathW; + // ANSI branch must re-derive from the freshly truncated + // ANSI path. Without this, BuildScriptFile composes + // targetNameW = + leaf, producing paths + // like "C:\Foo\*.*\file.txt" that fail CreateFileW with + // ERROR_INVALID_NAME (123). + if (useWideTargetPath) + pathW = parsedPathW; + else + pathW = AnsiToWide(path); break; // exit the Copy/Move loop and perform the operation } else @@ -1052,9 +1185,16 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count char* auxTargetPath = NULL; if (type == atCopy || type == atMove) auxTargetPath = path; + const wchar_t* auxTargetPathW = NULL; + if ((type == atCopy || type == atMove) && !pathW.empty()) + auxTargetPathW = pathW.c_str(); BOOL res2 = BuildScriptMain(script, type, auxTargetPath, mask, count, indexes.get(), f, NULL, &changeCaseData, countSizeMode != 0, - criteriaPtr); + criteriaPtr, auxTargetPathW); + // Repair only auto-widened source roots that still came from the ANSI + // panel cache. Operations with explicit PathW+NameW are left intact. + if (res2 && Is(ptDisk) && sally::unicode::HasWidePathW(GetPathW())) + script->ReanchorWideSourcePaths(GetPath(), GetPathW()); // if there's nothing to do, don't show the progress dialog BOOL emptyScript = script->Count == 0 && type != atCountSize; @@ -1120,11 +1260,15 @@ void CFilesWindow::FilesAction(CActionType type, CFilesWindow* target, int count { // change in the target directory and its subdirectories script->SetWorkPath1(path, TRUE); + if (!pathW.empty()) + script->SetWorkPath1W(pathW.c_str(), TRUE); } if (type == atMove) { // change in the target directory and its subdirectories script->SetWorkPath2(path, TRUE); + if (!pathW.empty()) + script->SetWorkPath2W(pathW.c_str(), TRUE); } } @@ -1469,13 +1613,19 @@ void CFilesWindow::ChangePathToOtherPanelPath() if (panel->Is(ptDisk)) { - ChangePathToDisk(HWindow, panel->GetPath()); + if (sally::unicode::HasWidePathW(panel->GetPathW())) + ChangePathToDiskW(HWindow, panel->GetPathW()); + else + ChangePathToDisk(HWindow, panel->GetPath()); } else { if (panel->Is(ptZIPArchive)) { - ChangePathToArchive(panel->GetZIPArchive(), panel->GetZIPPath()); + if (sally::unicode::HasWidePathW(panel->GetZIPArchiveW())) + ChangePathToArchiveW(panel->GetZIPArchiveW(), panel->GetZIPPathW()); + else + ChangePathToArchive(panel->GetZIPArchive(), panel->GetZIPPath()); } else { diff --git a/src/files_window_directory_read.cpp b/src/files_window_directory_read.cpp index f6bf1da13..4d5ccc64a 100644 --- a/src/files_window_directory_read.cpp +++ b/src/files_window_directory_read.cpp @@ -16,10 +16,12 @@ #include "drivelst.h" #include "snooper.h" #include "zip.h" +#include "common/IFileSystem.h" #include "shiconov.h" #include "common/widepath.h" #include "ui/IPrompter.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/IEnvironment.h" // @@ -37,6 +39,11 @@ int DeltaForTotalCount(int total) return delta; } +static IFileSystem* GetPanelFileSystem() +{ + return gFileSystem != NULL ? gFileSystem : GetWin32FileSystem(); +} + #ifndef _WIN64 BOOL AddWin64RedirectedDir(const char* path, CFilesArray* dirs, WIN32_FIND_DATA* fileData, @@ -209,6 +216,7 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) *st++ = '\\'; strcpy(st, "*"); char* fileNameEnd = st; + std::wstring fileNameW = sally::unicode::BuildPanelChildPathW(GetPathW(), NULL, L"*"); //--- preparing for reading icons if (UseSystemIcons) { @@ -306,7 +314,8 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) CPathBuffer ansiFileName; // Heap-allocated for long path support; ANSI conversion buffer for cFileName (also used as scratch) BOOL nameConversionLossy = FALSE; // TRUE if wide->ANSI conversion lost characters HANDLE search; - search = SalFindFirstFileHW(fileName, &fileDataW); + IFileSystem* fileSystem = GetPanelFileSystem(); + search = SalFindFirstFileWideH(fileNameW.c_str(), &fileDataW); if (search == INVALID_HANDLE_VALUE) { DWORD err = GetLastError(); @@ -341,7 +350,7 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) BOOL showErr = TRUE; if (err == ERROR_INVALID_PARAMETER || err == ERROR_NOT_READY) { - DWORD attrs = GetFileAttributesW(AnsiToWide(GetPath()).c_str()); + DWORD attrs = fileSystem->GetFileAttributes(GetPathW()); if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) && (attrs & FILE_ATTRIBUTE_REPARSE_POINT)) @@ -551,7 +560,25 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) file.NameLen = len; //--- wide name (for Unicode filenames not representable in ANSI) - if (nameConversionLossy) + // Allocate NameW whenever the original wide name contains any + // non-ASCII codepoint. The CP_ACP lossy detector above is a + // necessary but not sufficient trigger: under e.g. CP_ACP=949 + // (Korean) a Korean filename round-trips and detection says + // "not lossy", yet downstream consumers that later AnsiToWide() + // through a possibly-different CP_ACP still need the original + // wide form. Strict superset of prior behavior (lossy rows + // still get NameW); only added cost is one DupStr per non-ASCII + // row that previously had NameW=NULL. + BOOL hasNonAscii = FALSE; + for (const wchar_t* wp = fileDataW.cFileName; *wp; ++wp) + { + if ((unsigned)*wp > 0x7f) + { + hasNonAscii = TRUE; + break; + } + } + if (nameConversionLossy || hasNonAscii) file.NameW = DupStr(fileDataW.cFileName); else file.NameW = NULL; // Reset from previous iteration @@ -911,8 +938,7 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) RefreshListBox(0, -1, -1, FALSE, FALSE); // TODO: Use wide format string when available - std::wstring pathW = AnsiToWide(GetPath()); - gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), (pathW + L": " + GetErrorTextW(err)).c_str()); + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), (std::wstring(GetPathW()) + L": " + GetErrorTextW(err)).c_str()); } } @@ -920,8 +946,9 @@ BOOL CFilesWindow::ReadDirectory(HWND parent, BOOL isRefresh) { upDir = FALSE; *(fileNameEnd - 1) = 0; // it's not logical, but times ".." are from current directory + std::wstring currentPathW = GetPathW(); if (!UNCRootUpDir) - search = SalFindFirstFileHW(fileName, &fileDataW); + search = SalFindFirstFileWideH(currentPathW.c_str(), &fileDataW); else search = INVALID_HANDLE_VALUE; if (search == INVALID_HANDLE_VALUE) diff --git a/src/files_window_navigation.cpp b/src/files_window_navigation.cpp index 8711ab0fc..35bc874ad 100644 --- a/src/files_window_navigation.cpp +++ b/src/files_window_navigation.cpp @@ -16,6 +16,7 @@ #include "toolbar.h" #include "ui/IPrompter.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" //***************************************************************************** // @@ -2519,14 +2520,21 @@ void CFilesWindow::RefreshDirectory(BOOL probablyUselessRefresh, BOOL forceReloa { case ptDisk: { - result = ChangePathToDisk(HWindow, GetPath(), -1, NULL, &noChange, FALSE, FALSE, TRUE); + if (sally::unicode::HasWidePathW(GetPathW())) + result = ChangePathToDiskW(HWindow, GetPathW(), -1, NULL, &noChange, FALSE, FALSE, TRUE); + else + result = ChangePathToDisk(HWindow, GetPath(), -1, NULL, &noChange, FALSE, FALSE, TRUE); break; } case ptZIPArchive: { - result = ChangePathToArchive(GetZIPArchive(), GetZIPPath(), -1, NULL, TRUE, &noChange, - FALSE, NULL, TRUE); + if (sally::unicode::HasWidePathW(GetZIPArchiveW())) + result = ChangePathToArchiveW(GetZIPArchiveW(), GetZIPPathW(), -1, NULL, TRUE, &noChange, + FALSE, NULL, TRUE); + else + result = ChangePathToArchive(GetZIPArchive(), GetZIPPath(), -1, NULL, TRUE, &noChange, + FALSE, NULL, TRUE); // path valid + no change, we perform common-refresh because of Ctrl+H (requires read-dir) if (result && noChange) @@ -2935,7 +2943,54 @@ void CFilesWindow::RefreshDirectory(BOOL probablyUselessRefresh, BOOL forceReloa // we find the index of the focus item BOOL foundFocus = FALSE; - if (NextFocusName[0] != 0) + if (!NextFocusNameW.empty()) + { + MainWindow->CancelPanelsUI(); // cancel QuickSearch and QuickEdit + + std::wstring nextFocusName = NextFocusNameW; + size_t begin = 0; + while (begin < nextFocusName.size() && nextFocusName[begin] <= L' ') + begin++; + size_t end = nextFocusName.size(); + while (end > begin && nextFocusName[end - 1] <= L' ') + end--; + nextFocusName = nextFocusName.substr(begin, end - begin); + + int found = -1; + for (i = 0; i < count; i++) + { + CFileData* f = (i < Dirs->Count) ? &Dirs->At(i) : &Files->At(i - Dirs->Count); + std::wstring fallbackNameW; + const wchar_t* itemNameW = f->NameW; + if (itemNameW == NULL) + { + fallbackNameW = AnsiToWide(f->Name); + itemNameW = fallbackNameW.c_str(); + } + if ((int)wcslen(itemNameW) == (int)nextFocusName.size() && + _wcsicmp(itemNameW, nextFocusName.c_str()) == 0 && + (firstNewItemIsDir == -1 /* we don't know what it is */ || + firstNewItemIsDir == 0 /* is file */ && i >= Dirs->Count || + firstNewItemIsDir == 1 /* is directory */ && i < Dirs->Count)) + { + foundFocus = TRUE; + ensureFocusIndexVisible = TRUE; + wholeItemVisible = TRUE; + if (wcscmp(itemNameW, nextFocusName.c_str()) == 0) + { + focusIndex = i; + break; + } + if (found == -1) + found = i; + } + } + if (i == count && found != -1) + focusIndex = found; + NextFocusNameW.clear(); + NextFocusName[0] = 0; + } + else if (NextFocusName[0] != 0) { MainWindow->CancelPanelsUI(); // cancel QuickSearch and QuickEdit int l = (int)strlen(NextFocusName); // trim trailing spaces @@ -2974,6 +3029,7 @@ void CFilesWindow::RefreshDirectory(BOOL probablyUselessRefresh, BOOL forceReloa if (i == count && found != -1) focusIndex = found; // found: ignore-case NextFocusName[0] = 0; + NextFocusNameW.clear(); } // first, we search for the old focus in the new listing (according to the case-sensitivity of the current listing) diff --git a/src/files_window_pack_unpack.cpp b/src/files_window_pack_unpack.cpp index 365841f56..ea260b507 100644 --- a/src/files_window_pack_unpack.cpp +++ b/src/files_window_pack_unpack.cpp @@ -16,6 +16,7 @@ #include "common/IFileSystem.h" #include "common/unicode/helpers.h" #include "common/widepath.h" +#include "common/fsutil.h" #include "common/IEnvironment.h" // @@ -2034,31 +2035,32 @@ void CFilesWindow::CalculateOccupiedZIPSpace(int countSizeMode) } void CFilesWindow::AcceptChangeOnPathNotification(const char* path, BOOL includingSubdirs) +{ + AcceptChangeOnPathNotificationW(AnsiToWide(path).c_str(), includingSubdirs); +} + +void CFilesWindow::AcceptChangeOnPathNotificationW(const wchar_t* path, BOOL includingSubdirs) { CALL_STACK_MESSAGE3("CFilesWindow::AcceptChangeOnPathNotification(%s, %d)", - path, includingSubdirs); + path != NULL ? WideToAnsi(path).c_str() : "", includingSubdirs); BOOL refresh = FALSE; if ((Is(ptDisk) || Is(ptZIPArchive)) && (!AutomaticRefresh || GetNetworkDrive())) { // test the equality of paths or at least their prefix (we only care about disk paths, // FS paths in 'path' are automatically excluded because they can never match GetPath()) - CPathBuffer path1; // Heap-allocated for long path support - CPathBuffer path2; // Heap-allocated for long path support - lstrcpyn(path1, path, path1.Size()); - lstrcpyn(path2, GetPath(), path2.Size()); // for archives this is the path to the archive - SalPathRemoveBackslash(path1); - SalPathRemoveBackslash(path2); - int len1 = (int)strlen(path1); - refresh = !includingSubdirs && StrICmp(path1, path2) == 0 || // exact match - includingSubdirs && StrNICmp(path1, path2, len1) == 0 && // prefix match - (path2[len1] == 0 || path2[len1] == '\\'); - if (Is(ptDisk) && !refresh && CutDirectory(path1)) // pointless for archives + std::wstring path1 = path != NULL ? path : L""; + std::wstring path2 = GetPathW(); // for archives this is the path to the archive + SalPathRemoveBackslashW(path1); + SalPathRemoveBackslashW(path2); + refresh = !includingSubdirs && IsTheSamePathW(path1.c_str(), path2.c_str()) || // exact match + includingSubdirs && PathStartsWithW(path2.c_str(), path1.c_str()); + if (Is(ptDisk) && !refresh && CutDirectoryW(path1)) // pointless for archives { - SalPathRemoveBackslash(path1); + SalPathRemoveBackslashW(path1); // on NTFS the last subdirectory timestamp also changes (unfortunately visible only after entering // that subdirectory, but perhaps it will be fixed eventually, so refresh proactively) - refresh = StrICmp(path1, path2) == 0; + refresh = IsTheSamePathW(path1.c_str(), path2.c_str()); } if (refresh) { @@ -2074,13 +2076,14 @@ void CFilesWindow::AcceptChangeOnPathNotification(const char* path, BOOL includi { // the EnterPlugin+LeavePlugin section must be exposed up to here (not wrapped inside the interface) EnterPlugin(); - GetPluginFS()->AcceptChangeOnPathNotification(GetPluginFS()->GetPluginFSName(), path, includingSubdirs); + std::string pathA = path != NULL ? WideToAnsi(path) : std::string(); + GetPluginFS()->AcceptChangeOnPathNotification(GetPluginFS()->GetPluginFSName(), pathA.c_str(), includingSubdirs); LeavePlugin(); } } if (Is(ptDisk) && !refresh && // only disks have free space (archives do not and FS is handled elsewhere) - HasTheSameRootPath(path, GetPath()) // same root -> possible change in free disk space size + HasTheSameRootPathW(path, GetPathW()) // same root -> possible change in free disk space size /* && (!AutomaticRefresh || // commented out because notifications do not arrive for subdirectory changes on auto-refreshed paths causing the free space info to remain invalid !IsTheSamePath(path, GetPath()))*/ diff --git a/src/files_window_path_state.cpp b/src/files_window_path_state.cpp index 861b74774..493f2b5e1 100644 --- a/src/files_window_path_state.cpp +++ b/src/files_window_path_state.cpp @@ -6,6 +6,7 @@ #include "ui/IPrompter.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/IEnvironment.h" #include "cfgdlg.h" @@ -24,6 +25,44 @@ #include "shiconov.h" #include "common/widepath.h" +namespace +{ +bool GetVolumeRootForPathW(const std::wstring& path, std::wstring& root) +{ + if (path.empty()) + return false; + + std::vector buffer(path.length() + MAX_PATH + 1); + if (GetVolumePathNameW(path.c_str(), buffer.data(), (DWORD)buffer.size())) + { + root = buffer.data(); + return !root.empty(); + } + + return false; +} + +BOOL GetVolumeInformationForPathW(const std::wstring& path, LPDWORD flags) +{ + std::wstring root; + if (!GetVolumeRootForPathW(path, root)) + return FALSE; + + DWORD dummyMaximumComponentLength; + return GetVolumeInformationW(root.c_str(), NULL, 0, NULL, + &dummyMaximumComponentLength, flags, NULL, 0); +} + +UINT GetDriveTypeForPathW(const std::wstring& path) +{ + std::wstring root; + if (GetVolumeRootForPathW(path, root)) + return GetDriveTypeW(root.c_str()); + + return GetDriveTypeW(path.c_str()); +} +} // namespace + // // **************************************************************************** // CFilesWindowAncestor @@ -45,6 +84,8 @@ CFilesWindowAncestor::CFilesWindowAncestor() ArchiveDir = NULL; ZIPArchive[0] = 0; ZIPPath[0] = 0; + ZIPArchiveW.clear(); + ZIPPathW.clear(); PluginFS.Init(NULL, NULL, NULL, NULL, NULL, NULL, -1, 0, 0, 0); PluginFSDir = NULL; @@ -54,6 +95,7 @@ CFilesWindowAncestor::CFilesWindowAncestor() CPathBuffer buf; // Heap-allocated for long path support EnvGetSystemDirectoryA(gEnvironment, buf, buf.Size()); GetRootPath(Path, buf); + PathW = AnsiToWide(Path); OnlyDetachFSListing = FALSE; NewFSFiles = NULL; @@ -86,7 +128,15 @@ CFilesWindowAncestor::CheckPath(BOOL echo, const char* path, DWORD err, BOOL pos parent = (parent == NULL) ? HWindow : parent; if (path == NULL) + { + // Route the implicit-path case through the wide cache when populated so + // Unicode-only disk paths (e.g. zz中文) are not misreported as missing + // by SalCheckPath fed with the lossy ANSI mirror. + const wchar_t* pathW = GetPathW(); + if (sally::unicode::HasWidePathW(pathW)) + return SalCheckPathW(echo, pathW, err, postRefresh, parent); path = GetPath(); + } return SalCheckPath(echo, path, err, postRefresh, parent); } @@ -224,15 +274,72 @@ BOOL CFilesWindowAncestor::GetGeneralPath(char* buf, int bufSize, BOOL convertFS void CFilesWindowAncestor::SetPath(const char* path) { CALL_STACK_MESSAGE2("CFilesWindowAncestor::SetPath(%s)", path); - if (SuppressAutoRefresh && (!Is(ptDisk) || !IsTheSamePath(path, Path))) + SetPathW(AnsiToWide(path).c_str()); +} + +BOOL CFilesWindowAncestor::GetGeneralPathW(std::wstring& buf, BOOL convertFSPathToExternal) +{ + CALL_STACK_MESSAGE_NONE + buf.clear(); + if (Is(ptDisk)) + { + buf = GetPathW(); + return TRUE; + } + if (Is(ptZIPArchive)) + { + buf = GetZIPArchiveW(); + if (GetZIPPathW()[0] != 0) + { + if (GetZIPPathW()[0] != L'\\') + buf += L'\\'; + buf += GetZIPPathW(); + } + return TRUE; + } + if (Is(ptPluginFS)) + { + if (PluginFS.NotEmpty()) + { + std::wstring userPartW; + if (PluginFS.GetCurrentPathW(userPartW)) + { + buf = AnsiToWide(PluginFS.GetPluginFSName()); + buf += L":"; + buf += userPartW; + return TRUE; + } + } + if (convertFSPathToExternal) + { + CPathBuffer ansi; + if (GetGeneralPath(ansi, ansi.Size(), TRUE)) + { + buf = AnsiToWide(ansi); + return TRUE; + } + } + } + return FALSE; +} + +void CFilesWindowAncestor::SetPathW(const wchar_t* path) +{ + std::wstring newPath = path != NULL ? path : L""; + std::string pathA = WideToAnsi(newPath); + + CALL_STACK_MESSAGE2("CFilesWindowAncestor::SetPathW(%s)", pathA.c_str()); + if (SuppressAutoRefresh && (!Is(ptDisk) || !IsTheSamePath(pathA.c_str(), Path))) SuppressAutoRefresh = FALSE; DetachDirectory((CFilesWindow*)this); - strcpy(Path, path); + PathW = newPath; + if (!Path.Assign(pathA.c_str())) + Path.Clear(); //--- detection of file-based compression/encryption and FAT32 - DWORD dummy1, flags; - if ((Is(ptDisk) || Is(ptZIPArchive)) && - MyGetVolumeInformation(path, NULL, NULL, NULL, NULL, 0, NULL, &dummy1, &flags, NULL, 0)) + DWORD flags; + if ((Is(ptDisk) || Is(ptZIPArchive)) && !newPath.empty() && + GetVolumeInformationForPathW(newPath, &flags)) { ((CFilesWindow*)this)->FileBasedCompression = (flags & FS_FILE_COMPRESSION) != 0 && Is(ptDisk); ((CFilesWindow*)this)->FileBasedEncryption = (flags & FILE_SUPPORTS_ENCRYPTION) != 0 && Is(ptDisk); @@ -249,7 +356,7 @@ void CFilesWindowAncestor::SetPath(const char* path) DriveType = DRIVE_UNKNOWN; if (!Is(ptPluginFS)) // pluginFS handles changes differently... { - DriveType = MyGetDriveType(Path); + DriveType = GetDriveTypeForPathW(newPath); switch (DriveType) { case DRIVE_REMOVABLE: @@ -290,7 +397,15 @@ void CFilesWindowAncestor::SetPath(const char* path) MonitorChanges = FALSE; if (MonitorChanges) - AddDirectory((CFilesWindow*)this, Path, DriveType == DRIVE_REMOVABLE || DriveType == DRIVE_FIXED); + { + // Prefer the wide path so FindFirstChangeNotificationW receives the + // real Unicode bytes — the ANSI mirror would have CP_ACP-mangled the + // panel root and the snooper would never fire for non-CP_ACP paths. + if (sally::unicode::HasWidePathW(PathW.c_str())) + AddDirectoryW((CFilesWindow*)this, PathW.c_str(), DriveType == DRIVE_REMOVABLE || DriveType == DRIVE_FIXED); + else + AddDirectory((CFilesWindow*)this, Path, DriveType == DRIVE_REMOVABLE || DriveType == DRIVE_FIXED); + } else // if changes are not monitored, Snooper does not call SetAutomaticRefresh -> we do it here { ((CFilesWindow*)this)->SetAutomaticRefresh(FALSE, TRUE); @@ -343,19 +458,36 @@ CFilesWindowAncestor::GetPluginDataForPluginIface() void CFilesWindowAncestor::SetZIPPath(const char* path) { CALL_STACK_MESSAGE_NONE + SetZIPPathW(AnsiToWide(path).c_str()); +} + +void CFilesWindowAncestor::SetZIPPathW(const wchar_t* path) +{ + CALL_STACK_MESSAGE_NONE + if (path == NULL) + path = L""; if (*path == '\\') path++; // ZIPPath will not start with '\\' - int l = (int)strlen(path); - if (l > 0 && path[l - 1] == '\\') - l--; // ZIPPath will not end with '\\' - memcpy(ZIPPath, path, l); - ZIPPath[l] = 0; + ZIPPathW = path; + if (!ZIPPathW.empty() && ZIPPathW.back() == L'\\') + ZIPPathW.pop_back(); // ZIPPath will not end with '\\' + + std::string pathA = WideToAnsi(ZIPPathW); + ZIPPath.Assign(pathA.c_str()); } void CFilesWindowAncestor::SetZIPArchive(const char* archive) { CALL_STACK_MESSAGE_NONE - strcpy(ZIPArchive, archive); + SetZIPArchiveW(AnsiToWide(archive).c_str()); +} + +void CFilesWindowAncestor::SetZIPArchiveW(const wchar_t* archive) +{ + CALL_STACK_MESSAGE_NONE + ZIPArchiveW = archive != NULL ? archive : L""; + std::string archiveA = WideToAnsi(ZIPArchiveW); + ZIPArchive.Assign(archiveA.c_str()); } BOOL CFilesWindowAncestor::SamePath(CFilesWindowAncestor* other) @@ -1401,6 +1533,7 @@ CFilesWindow::CFilesWindow(CMainWindow* parent) SetValidFileData(VALID_DATA_ALL); AutomaticRefresh = TRUE; NextFocusName[0] = 0; + NextFocusNameW.clear(); DontClearNextFocusName = FALSE; LastRefreshTime = 0; FilesActionInProgress = FALSE; @@ -1694,13 +1827,14 @@ void CFilesWindow::DirectoryLineSetText() strcat(ZIPbuf, GetZIPPath()); } path = ZIPbuf; - PathHistory->AddPath(1, GetZIPArchive(), GetZIPPath(), NULL, NULL); + PathHistory->AddPath(1, GetZIPArchive(), GetZIPPath(), NULL, NULL, + GetZIPArchiveW(), GetZIPPathW()); } else { if (Is(ptDisk)) { - PathHistory->AddPath(0, GetPath(), NULL, NULL, NULL); + PathHistory->AddPath(0, GetPath(), NULL, NULL, NULL, GetPathW(), nullptr); path = GetPath(); } else @@ -1729,7 +1863,16 @@ void CFilesWindow::DirectoryLineSetText() { CPathBuffer buf; // Heap-allocated for long path support int pathLen = (int)strlen(path); - if (Is(ptDisk) || Is(ptZIPArchive)) + if (Is(ptDisk)) + { + std::wstring bufW = GetPathW(); + int pathLenW = (int)bufW.size(); + if (!bufW.empty() && bufW.back() != L'\\') + bufW.push_back(L'\\'); + bufW += AnsiToWide(Filter.GetMasksString()); + DirectoryLine->SetTextW(bufW.c_str(), pathLenW); + } + else if (Is(ptZIPArchive)) { int l = pathLen; memcpy(buf, path, l); @@ -1748,11 +1891,15 @@ void CFilesWindow::DirectoryLineSetText() lstrcpyn(buf + l, Filter.GetMasksString(), MAX_PATH); } } - DirectoryLine->SetText(buf, pathLen); + if (!Is(ptDisk)) + DirectoryLine->SetText(buf, pathLen); } else { - DirectoryLine->SetText(path); + if (Is(ptDisk)) + DirectoryLine->SetTextW(GetPathW()); + else + DirectoryLine->SetText(path); } } @@ -2183,7 +2330,10 @@ void CFilesWindow::GotoRoot() { if (Is(ptZIPArchive) && GetZIPPath()[0] != 0) // we are not in the root of the archive -> go there { - ChangePathToArchive(GetZIPArchive(), ""); + if (sally::unicode::HasWidePathW(GetZIPArchiveW())) + ChangePathToArchiveW(GetZIPArchiveW(), L""); + else + ChangePathToArchive(GetZIPArchive(), ""); } else // go to the root of the Windows path { diff --git a/src/files_window_view_edit.cpp b/src/files_window_view_edit.cpp index ae743fc4f..a7c0c2f74 100644 --- a/src/files_window_view_edit.cpp +++ b/src/files_window_view_edit.cpp @@ -22,10 +22,12 @@ #include "menu.h" #include "common/widepath.h" #include "common/fsutil.h" +#include "common/CreateDirectoryFlow.h" #include "ui/IPrompter.h" #include "common/IFileSystem.h" #include "common/unicode/AnsiFallbackPolicy.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/unicode/NameFallbackRecovery.h" #include "common/unicode/PathIdentityPolicy.h" #include "common/unicode/RenameRetryPolicy.h" @@ -69,29 +71,6 @@ BOOL FileNameInvalidForManualCreateW(const wchar_t* path) return FALSE; } -BOOL MakeValidFileNameW(wchar_t* path) -{ - // trim spaces at the beginning and spaces and dots at the end of the name (Explorer behavior) - BOOL ch = FALSE; - wchar_t* n = path; - while (*n != 0 && *n <= L' ') - n++; - if (n > path) - { - memmove(path, n, (wcslen(n) + 1) * sizeof(wchar_t)); - ch = TRUE; - } - n = path + wcslen(path); - while (n > path && (*(n - 1) <= L' ' || *(n - 1) == L'.')) - n--; - if (*n != 0) - { - *n = 0; - ch = TRUE; - } - return ch; -} - void RepairLossyQuickRenameHistoryForCurrentName(wchar_t* historyW[], int historyCount, const char* currentAnsiName, const wchar_t* currentWideName) { @@ -222,6 +201,10 @@ void CFilesWindow::Convert() BOOL res = BuildScriptMain(script, convertDlg.SubDirs ? atRecursiveConvert : atConvert, NULL, NULL, count, indexes.get(), f, NULL, NULL, FALSE, &filter); + // Repair only auto-widened source roots that still came from the ANSI + // panel cache. Operations with explicit PathW+NameW are left intact. + if (res && Is(ptDisk) && sally::unicode::HasWidePathW(GetPathW())) + script->ReanchorWideSourcePaths(GetPath(), GetPathW()); if (script->Count == 0) res = FALSE; // reordered to allow the main window to activate (must not be disabled), otherwise it switches to another app @@ -762,7 +745,9 @@ void CFilesWindow::FindFile() return; } - OpenFindDialog(MainWindow->HWindow, Is(ptDisk) ? GetPath() : ""); + OpenFindDialog(MainWindow->HWindow, + Is(ptDisk) ? GetPath() : "", + Is(ptDisk) ? GetPathW() : L""); } void CFilesWindow::ViewFile(char* name, BOOL altView, DWORD handlerID, int enumFileNamesSourceUID, @@ -797,6 +782,7 @@ void CFilesWindow::ViewFile(char* name, BOOL altView, DWORD handlerID, int enumF BOOL useDiskCache = FALSE; // TRUE only for ZIP - uses disk-cache BOOL arcCacheCacheCopies = TRUE; // cache copies in disk-cache unless the archiver plugin requests otherwise CPathBuffer dcFileName; // ZIP: name for disk-cache + std::wstring viewNameW; if (name == NULL) { int i = GetCaretIndex(); @@ -807,22 +793,27 @@ void CFilesWindow::ViewFile(char* name, BOOL altView, DWORD handlerID, int enumF { if (enumFileNamesLastFileIndex == -1) enumFileNamesLastFileIndex = i - Dirs->Count; - lstrcpyn(path, GetPath(), path.Size()); - if (GetPath()[strlen(GetPath()) - 1] != '\\') - strcat(path, "\\"); - char* s = path + strlen(path); - // Long paths are now supported - just append the name - strcpy(s, f->Name); - // try whether the file name is valid, otherwise try its DOS name - // (handles files accessible only through Unicode or DOS names) - if (f->DosName != NULL && SalLPGetFileAttributes(path) == INVALID_FILE_ATTRIBUTES) + viewNameW = sally::unicode::BuildPanelChildPathW( + sally::unicode::EffectivePanelPathW(GetPath(), GetPathW()), + f->Name, + f->NameW); + std::string pathA; + if (!sally::unicode::TryExactAnsiFallback(viewNameW, pathA)) + pathA = WideToAnsi(viewNameW); + lstrcpyn(path, pathA.c_str(), path.Size()); + + if (f->DosName != NULL && SalLPGetFileAttributes(path) == INVALID_FILE_ATTRIBUTES && + sally::unicode::TryExactAnsiFallback(viewNameW, pathA)) { DWORD err = GetLastError(); if (err == ERROR_FILE_NOT_FOUND || err == ERROR_INVALID_NAME) { + char* s = path + strlen(path); + while (s > path && *(s - 1) != '\\') + s--; strcpy(s, f->DosName); if (SalLPGetFileAttributes(path) == INVALID_FILE_ATTRIBUTES) // still error -> revert to the long name - strcpy(s, f->Name); + lstrcpyn(path, pathA.c_str(), path.Size()); } } name = path; @@ -983,7 +974,8 @@ void CFilesWindow::ViewFile(char* name, BOOL altView, DWORD handlerID, int enumF HANDLE lock = NULL; BOOL lockOwner = FALSE; ViewFileInt(HWindow, name, altView, handlerID, useDiskCache, lock, lockOwner, addToHistory, - enumFileNamesSourceUID, enumFileNamesLastFileIndex); + enumFileNamesSourceUID, enumFileNamesLastFileIndex, + !viewNameW.empty() ? viewNameW.c_str() : NULL); if (useDiskCache) { @@ -1000,7 +992,7 @@ void CFilesWindow::ViewFile(char* name, BOOL altView, DWORD handlerID, int enumF BOOL ViewFileInt(HWND parent, const char* name, BOOL altView, DWORD handlerID, BOOL returnLock, HANDLE& lock, BOOL& lockOwner, BOOL addToHistory, int enumFileNamesSourceUID, - int enumFileNamesLastFileIndex) + int enumFileNamesLastFileIndex, const wchar_t* nameW) { BOOL success = FALSE; lock = NULL; @@ -1125,10 +1117,10 @@ BOOL ViewFileInt(HWND parent, const char* name, BOOL altView, DWORD handlerID, B break; } - STARTUPINFO si; + STARTUPINFOW si; PROCESS_INFORMATION pi; - memset(&si, 0, sizeof(STARTUPINFO)); - si.cb = sizeof(STARTUPINFO); + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); si.dwX = place.rcNormalPosition.left; si.dwY = place.rcNormalPosition.top; si.dwXSize = place.rcNormalPosition.right - place.rcNormalPosition.left; @@ -1137,49 +1129,116 @@ BOOL ViewFileInt(HWND parent, const char* name, BOOL altView, DWORD handlerID, B STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOWNORMAL; - CPathBuffer cmdLine; // Heap-allocated for long path support - lstrcpyn(cmdLine, expCommand, cmdLine.Size()); - AddDoubleQuotesIfNeeded(cmdLine, cmdLine.Size()); // CreateProcess wants the name with spaces in quotes (otherwise it tries various variants, see help) - int len = (int)strlen(cmdLine); - int lArgs = (int)strlen(expArguments); - if (len + lArgs + 2 <= cmdLine.Size()) + // Wide-aware command-line assembly. Same shape as EditFile: + // ExpandCommand/Arguments/InitDir run ANSI through CP_ACP, then we + // convert to wide and substitute the lossy CP_ACP mirrors of $(FullPath), + // $(Name), $(Path) (in both with-trailing-slash and without flavors) + // with their wide counterparts derived from the caller-supplied nameW. + // Without this CreateProcessA fails with ERROR_DIRECTORY (267) when the + // panel root is non-CP_ACP, even though the panel rendered it correctly. + std::wstring effectiveNameW; + if (nameW != NULL && nameW[0] != L'\0') + effectiveNameW = nameW; + else if (name != NULL) + effectiveNameW = AnsiToWide(name); + + std::wstring expCommandW = AnsiToWide(expCommand.Get()); + std::wstring expArgumentsW = AnsiToWide(expArguments.Get()); + std::wstring expInitDirW = AnsiToWide(expInitDir.Get()); + + if (!effectiveNameW.empty() && name != NULL && name[0] != '\0') { - cmdLine[len] = ' '; - memcpy(cmdLine + len + 1, expArguments, lArgs + 1); - - MainWindow->SetDefaultDirectories(); + const std::wstring lossyFullPathW = AnsiToWide(name); + auto replaceAll = [](std::wstring& s, const std::wstring& from, const std::wstring& to) { + if (from.empty() || from == to) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::wstring::npos) + { + s.replace(pos, from.size(), to); + pos += to.size(); + } + }; + replaceAll(expArgumentsW, lossyFullPathW, effectiveNameW); + replaceAll(expInitDirW, lossyFullPathW, effectiveNameW); - if (expInitDir[0] == 0) // this should never happen - { - strcpy(expInitDir, name); - CutDirectory(expInitDir); - } - if (!HANDLES(CreateProcess(NULL, cmdLine, NULL, NULL, FALSE, - NORMAL_PRIORITY_CLASS, NULL, expInitDir, &si, &pi))) + const char* leafA = strrchr(name, '\\'); + const size_t lastSlashW = effectiveNameW.find_last_of(L'\\'); + if (leafA != NULL && lastSlashW != std::wstring::npos) { - DWORD err = GetLastError(); - CPathBuffer buff; // Heap-allocated for long path support - sprintf(buff.Get(), LoadStr(IDS_ERROREXECVIEW), expCommand.Get(), GetErrorText(err)); - gPrompter->ShowError(LoadStr(IDS_ERRORTITLE), buff.Get()); - } - else - { - success = TRUE; - if (returnLock) + const std::wstring lossyLeafW = AnsiToWide(leafA + 1); + const std::wstring leafW = effectiveNameW.substr(lastSlashW + 1); + replaceAll(expArgumentsW, lossyLeafW, leafW); + replaceAll(expInitDirW, lossyLeafW, leafW); + + std::string dirA(name, (size_t)(leafA - name + 1)); + std::wstring lossyDirWithSlash = AnsiToWide(dirA.c_str()); + std::wstring dirWithSlash = effectiveNameW.substr(0, lastSlashW + 1); + replaceAll(expArgumentsW, lossyDirWithSlash, dirWithSlash); + replaceAll(expInitDirW, lossyDirWithSlash, dirWithSlash); + if (!dirA.empty() && dirA.back() == '\\') + dirA.pop_back(); + if (!dirA.empty()) { - lock = pi.hProcess; - lockOwner = TRUE; + std::wstring lossyDirNoSlash = AnsiToWide(dirA.c_str()); + std::wstring dirNoSlash = (lastSlashW == 0) + ? std::wstring() + : effectiveNameW.substr(0, lastSlashW); + replaceAll(expArgumentsW, lossyDirNoSlash, dirNoSlash); + replaceAll(expInitDirW, lossyDirNoSlash, dirNoSlash); } - else - HANDLES(CloseHandle(pi.hProcess)); - HANDLES(CloseHandle(pi.hThread)); } } + + MainWindow->SetDefaultDirectories(); + + if (expInitDirW.empty()) // matches the original ANSI "this should never happen" branch + { + expInitDirW = effectiveNameW.empty() ? AnsiToWide(name) : effectiveNameW; + size_t lastSlash = expInitDirW.find_last_of(L'\\'); + if (lastSlash != std::wstring::npos) + expInitDirW.resize(lastSlash); + else + expInitDirW.clear(); + } + + std::wstring cmdLineW = expCommandW; + if (!cmdLineW.empty() && cmdLineW.front() != L'"' && + cmdLineW.find(L' ') != std::wstring::npos) + { + cmdLineW.insert(cmdLineW.begin(), L'"'); + cmdLineW.push_back(L'"'); + } + cmdLineW.push_back(L' '); + cmdLineW.append(expArgumentsW); + + BOOL launched = ::CreateProcessW(NULL, cmdLineW.data(), NULL, NULL, FALSE, + NORMAL_PRIORITY_CLASS, NULL, + expInitDirW.empty() ? NULL : expInitDirW.c_str(), + &si, &pi); + if (!launched) + { + DWORD err = GetLastError(); + std::wstring msg = FormatStrW(LoadStrW(IDS_ERROREXECVIEW), + expCommandW.c_str(), + GetErrorTextW(err)); + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), msg.c_str()); + } else { - gPrompter->ShowError(LoadStr(IDS_ERRORTITLE), LoadStr(IDS_ERROREXECVIEW)); + HANDLES_ADD(__htProcess, __hoCreateProcess, pi.hProcess); + HANDLES_ADD(__htThread, __hoCreateProcess, pi.hThread); + success = TRUE; + if (returnLock) + { + lock = pi.hProcess; + lockOwner = TRUE; + } + else + HANDLES(CloseHandle(pi.hProcess)); + HANDLES(CloseHandle(pi.hThread)); + } } - } break; } @@ -1201,14 +1260,30 @@ BOOL ViewFileInt(HWND parent, const char* name, BOOL altView, DWORD handlerID, B HANDLE lockAux = NULL; BOOL lockOwnerAux = FALSE; - if (OpenViewer(name, vtText, - place.rcNormalPosition.left, - place.rcNormalPosition.top, - place.rcNormalPosition.right - place.rcNormalPosition.left, - place.rcNormalPosition.bottom - place.rcNormalPosition.top, - place.showCmd, - returnLock, &lockAux, &lockOwnerAux, NULL, - enumFileNamesSourceUID, enumFileNamesLastFileIndex)) + BOOL viewerOpened = FALSE; + if (nameW != NULL && nameW[0] != L'\0') + { + viewerOpened = OpenViewerW(nameW, name, vtText, + place.rcNormalPosition.left, + place.rcNormalPosition.top, + place.rcNormalPosition.right - place.rcNormalPosition.left, + place.rcNormalPosition.bottom - place.rcNormalPosition.top, + place.showCmd, + returnLock, &lockAux, &lockOwnerAux, NULL, + enumFileNamesSourceUID, enumFileNamesLastFileIndex); + } + else + { + viewerOpened = OpenViewer(name, vtText, + place.rcNormalPosition.left, + place.rcNormalPosition.top, + place.rcNormalPosition.right - place.rcNormalPosition.left, + place.rcNormalPosition.bottom - place.rcNormalPosition.top, + place.showCmd, + returnLock, &lockAux, &lockOwnerAux, NULL, + enumFileNamesSourceUID, enumFileNamesLastFileIndex); + } + if (viewerOpened) { success = TRUE; if (returnLock && lockAux != NULL) @@ -1290,6 +1365,13 @@ void CFilesWindow::EditFile(char* name, DWORD handlerID) BOOL addToHistory = name != NULL && Is(ptDisk); + // Wide twin of `name`. Built from GetPathW() + f->NameW when available so + // that Unicode panel paths (e.g. "C:\Temp\SalLongPathTest\zz中文\한글_x.txt") + // can be passed to CreateProcessW without going through CP_ACP — otherwise + // the working-directory parameter resolves to "zz??" and CreateProcessA + // returns ERROR_DIRECTORY (267). + std::wstring nameW; + // if viewing/editing from the panel, obtain the full long name if (name == NULL) { @@ -1335,6 +1417,20 @@ void CFilesWindow::EditFile(char* name, DWORD handlerID) } name = path; addToHistory = TRUE; + // Build the wide twin while f is still in scope. Prefer + // f->NameW (preserved Unicode leaf, populated by directory + // read for any non-ASCII filename) over AnsiToWide(f->Name), + // which would re-introduce CP_ACP loss for Korean/CJK leaves. + if (sally::unicode::HasWidePathW(GetPathW())) + { + nameW = GetPathW(); + if (!nameW.empty() && nameW.back() != L'\\') + nameW += L'\\'; + if (f->NameW != NULL) + nameW += f->NameW; + else if (f->Name != NULL) + nameW += AnsiToWide(f->Name); + } } } else @@ -1343,6 +1439,13 @@ void CFilesWindow::EditFile(char* name, DWORD handlerID) } } + // Fallback wide twin when name was supplied by an external caller + // (Find dialog, history). No preserved CFileData here; the best we can + // do is AnsiToWide. The Find dialog's wide propagation is a separate + // stream (tracked in kb/unicode/TODO.md item 3). + if (nameW.empty() && name != NULL) + nameW = AnsiToWide(name); + // obtain the full DOS name CPathBuffer dosName; // Heap-allocated for long path support if (GetShortPathName(name, dosName, dosName.Size()) == 0) @@ -1438,10 +1541,10 @@ void CFilesWindow::EditFile(char* name, DWORD handlerID) return; } - STARTUPINFO si; + STARTUPINFOW si; PROCESS_INFORMATION pi; - memset(&si, 0, sizeof(STARTUPINFO)); - si.cb = sizeof(STARTUPINFO); + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); si.dwX = place.rcNormalPosition.left; si.dwY = place.rcNormalPosition.top; si.dwXSize = place.rcNormalPosition.right - place.rcNormalPosition.left; @@ -1450,42 +1553,116 @@ void CFilesWindow::EditFile(char* name, DWORD handlerID) STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOWNORMAL; - CPathBuffer cmdLine; // Heap-allocated for long path support - lstrcpyn(cmdLine, expCommand, cmdLine.Size()); - AddDoubleQuotesIfNeeded(cmdLine, cmdLine.Size()); // CreateProcess wants the name with spaces in quotes (otherwise it tries various variants, see help) - int len = (int)strlen(cmdLine); - int lArgs = (int)strlen(expArguments); - if (len + lArgs + 2 <= cmdLine.Size()) + // Wide-aware command-line assembly. The ANSI ExpandCommand/Arguments/InitDir + // above expanded the templates through CP_ACP, so any $(FullPath), $(Name) or + // $(Path) substitutions that referenced a non-CP_ACP filename came out lossy + // (e.g. "zz??\??_Korea_22a_9.txt"). Convert each piece to wide via AnsiToWide + // and then substitute the lossy CP_ACP-mirrored substrings with their wide + // counterparts derived from nameW. This recovers the common $(FullPath), + // $(Name) and $(Path) cases without touching the var-expansion engine. + std::wstring expCommandW = AnsiToWide(expCommand.Get()); + std::wstring expArgumentsW = AnsiToWide(expArguments.Get()); + std::wstring expInitDirW = AnsiToWide(expInitDir.Get()); + + if (!nameW.empty() && name != NULL && name[0] != '\0') { - cmdLine[len] = ' '; - memcpy(cmdLine + len + 1, expArguments, lArgs + 1); - - MainWindow->SetDefaultDirectories(); + const std::wstring lossyFullPathW = AnsiToWide(name); + auto replaceAll = [](std::wstring& s, const std::wstring& from, const std::wstring& to) { + if (from.empty() || from == to) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::wstring::npos) + { + s.replace(pos, from.size(), to); + pos += to.size(); + } + }; + replaceAll(expArgumentsW, lossyFullPathW, nameW); + replaceAll(expInitDirW, lossyFullPathW, nameW); - if (expInitDir[0] == 0) // this should never happen - { - strcpy(expInitDir, name); - CutDirectory(expInitDir); - } - if (!HANDLES(CreateProcess(NULL, cmdLine, NULL, NULL, FALSE, - NORMAL_PRIORITY_CLASS, NULL, expInitDir, &si, &pi))) + const char* leafA = strrchr(name, '\\'); + const size_t lastSlashW = nameW.find_last_of(L'\\'); + if (leafA != NULL && lastSlashW != std::wstring::npos) { - DWORD err = GetLastError(); - CPathBuffer buff; // Heap-allocated for long path support - sprintf(buff.Get(), LoadStr(IDS_ERROREXECEDIT), expCommand.Get(), GetErrorText(err)); - gPrompter->ShowError(LoadStr(IDS_ERRORTITLE), buff.Get()); + const std::wstring lossyLeafW = AnsiToWide(leafA + 1); + const std::wstring leafW = nameW.substr(lastSlashW + 1); + replaceAll(expArgumentsW, lossyLeafW, leafW); + replaceAll(expInitDirW, lossyLeafW, leafW); + + // Path component matched in two flavors: $(Path) expansion + // returns the directory with a trailing backslash, but the + // ANSI ExpandInitDir strips the trailing backslash from its + // result before returning (CreateProcess's lpCurrentDirectory + // convention). Do both replacements; the longer-with-slash + // form is tried first so it doesn't get shadowed by the + // shorter no-slash form's substitution. + std::string dirA(name, (size_t)(leafA - name + 1)); + std::wstring lossyDirWithSlash = AnsiToWide(dirA.c_str()); + std::wstring dirWithSlash = nameW.substr(0, lastSlashW + 1); + replaceAll(expArgumentsW, lossyDirWithSlash, dirWithSlash); + replaceAll(expInitDirW, lossyDirWithSlash, dirWithSlash); + if (!dirA.empty() && dirA.back() == '\\') + dirA.pop_back(); + if (!dirA.empty()) + { + std::wstring lossyDirNoSlash = AnsiToWide(dirA.c_str()); + std::wstring dirNoSlash = (lastSlashW == 0) + ? std::wstring() + : nameW.substr(0, lastSlashW); + replaceAll(expArgumentsW, lossyDirNoSlash, dirNoSlash); + replaceAll(expInitDirW, lossyDirNoSlash, dirNoSlash); + } } + } + + MainWindow->SetDefaultDirectories(); + + if (expInitDirW.empty()) // belt-and-suspenders fallback (parallel of the original ANSI "this should never happen" branch) + { + expInitDirW = nameW.empty() ? AnsiToWide(name) : nameW; + size_t lastSlash = expInitDirW.find_last_of(L'\\'); + if (lastSlash != std::wstring::npos) + expInitDirW.resize(lastSlash); else - { - HANDLES(CloseHandle(pi.hProcess)); - HANDLES(CloseHandle(pi.hThread)); - } + expInitDirW.clear(); + } + + // Assemble wide cmdLine = quoted command + ' ' + arguments. Quote the + // command (binary path) if it contains spaces and isn't already quoted. + std::wstring cmdLineW = expCommandW; + if (!cmdLineW.empty() && cmdLineW.front() != L'"' && + cmdLineW.find(L' ') != std::wstring::npos) + { + cmdLineW.insert(cmdLineW.begin(), L'"'); + cmdLineW.push_back(L'"'); + } + cmdLineW.push_back(L' '); + cmdLineW.append(expArgumentsW); + + // CreateProcessW: the lpCommandLine buffer must be writable per MSDN. + // std::wstring::data() returns a writable contiguous buffer (C++17+). + // Bypass the HANDLES wrapper around ::CreateProcessW (C__Handles only + // provides an ANSI CreateProcess shim) and register the resulting + // process+thread handles with HANDLES_ADD so the close calls below + // can match against the tracker. + BOOL launched = ::CreateProcessW(NULL, cmdLineW.data(), NULL, NULL, FALSE, + NORMAL_PRIORITY_CLASS, NULL, + expInitDirW.empty() ? NULL : expInitDirW.c_str(), + &si, &pi); + if (!launched) + { + DWORD err = GetLastError(); + std::wstring msg = FormatStrW(LoadStrW(IDS_ERROREXECEDIT), + expCommandW.c_str(), + GetErrorTextW(err)); + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), msg.c_str()); } else { - CPathBuffer buff; - sprintf(buff, LoadStr(IDS_ERROREXECEDIT), expCommand.Get(), LoadStr(IDS_TOOLONGNAME)); - gPrompter->ShowError(LoadStr(IDS_ERRORTITLE), buff.Get()); + HANDLES_ADD(__htProcess, __hoCreateProcess, pi.hProcess); + HANDLES_ADD(__htThread, __hoCreateProcess, pi.hThread); + HANDLES(CloseHandle(pi.hProcess)); + HANDLES(CloseHandle(pi.hThread)); } } } @@ -1607,7 +1784,7 @@ void CFilesWindow::EditNewFile() { wchar_t* writable = &inputW[0]; wchar_t* lastCompNameW = wcsrchr(writable, L'\\'); - MakeValidFileNameW(lastCompNameW != NULL ? lastCompNameW + 1 : writable); + MakeValidFileNameComponentW(lastCompNameW != NULL ? lastCompNameW + 1 : writable); } pathW = inputW; WideToAnsi(pathW, path.Get(), path.Size()); // keep ANSI fallback for legacy callers @@ -2083,6 +2260,7 @@ void CFilesWindow::CreateDir(CFilesWindow* target) CPathBuffer path, nextFocus; // Heap-allocated for long path support *path = 0; *nextFocus = 0; + std::wstring pathW; // restore DefaultDir MainWindow->UpdateDefaultDir(MainWindow->GetActivePanel() == this); @@ -2094,88 +2272,87 @@ void CFilesWindow::CreateDir(CFilesWindow* target) CCopyMoveDialog dlg(HWindow, path, path.Size(), LoadStr(IDS_CREATEDIRECTORY_TITLE), &subject, IDD_CREATEDIRDIALOG, Configuration.CreateDirHistory, CREATEDIR_HISTORY_SIZE, - FALSE); + FALSE, + Configuration.CreateDirHistoryW, CREATEDIR_HISTORY_SIZE); + dlg.SetUnicodePath(pathW); CREATE_AGAIN: if (dlg.Execute() == IDOK) { UpdateWindow(MainWindow->HWindow); - - // for disk paths we flip '/' to '\\' and eliminate duplicate backslashes - SlashesToBackslashesAndRemoveDups(path); - - // clean the name from undesirable characters at the beginning and end - // we do this only for the last component; the previous ones already exist and it doesn't matter - // (the system handles it) or they are checked during creation and an error is shown - // (we don't clean them, we let the user do some work, it's easy enough) - char* lastCompName = strrchr(path.Get(), '\\'); - MakeValidFileName(lastCompName != NULL ? lastCompName + 1 : path.Get()); - - int errTextID; - if (!SalGetFullName(path, &errTextID, Is(ptDisk) ? GetPath() : NULL, nextFocus, NULL, path.Size()) || - strlen(path) >= SAL_MAX_LONG_PATH) + pathW = dlg.GetUnicodeResult(); + if (pathW.empty()) + pathW = AnsiToWide(path.Get()); + dlg.SetUnicodePath(pathW); + + sally::filesystem::CreateDirectoryPlan plan; + sally::filesystem::CreateDirectoryFailure failure; + if (!sally::filesystem::PrepareCreateDirectoryTargetW(pathW, GetPathW(), plan, &failure)) { - if (strlen(path) >= SAL_MAX_LONG_PATH) - errTextID = IDS_TOOLONGPATH; - /* even if the string is empty we want an error message - if (errTextID == IDS_EMPTYNAMENOTALLOWED) - { - EndStopRefresh(); // snooper will start again now - return; // empty string, nothing to do - } - */ - gPrompter->ShowError(LoadStrW(IDS_ERRORCREATINGDIR), LoadStrW(errTextID)); + gPrompter->ShowError(LoadStrW(IDS_ERRORCREATINGDIR), LoadStrW(failure.errorTextId)); goto CREATE_AGAIN; } - else + + std::wstring rootPathW = GetRootPathW(plan.fullPath.c_str()); + if (SalCheckPathW(TRUE, rootPathW.c_str(), ERROR_SUCCESS, TRUE, HWindow) != ERROR_SUCCESS) + goto CREATE_AGAIN; + + if (!sally::filesystem::DirectoryExistsW(plan.parentPath)) { - CPathBuffer checkPath; // Heap-allocated for long path support - GetRootPath(checkPath, path); - if (CheckPath(TRUE, checkPath) != ERROR_SUCCESS) - goto CREATE_AGAIN; - strcpy(checkPath, path); - CutDirectory(checkPath); - CPathBuffer newDir; // Heap-allocated for long path support - if (!CheckAndCreateDirectory(checkPath, HWindow, FALSE, NULL, 0, newDir, TRUE, TRUE)) - goto CREATE_AGAIN; - if (newDir[0] != 0) + bool createParents = true; + if (Configuration.CnfrmCreateDir) { - // change only in the directory where the first directory was created (other directories did not exist, no reason to report changes there) - CutDirectory(newDir); - MainWindow->PostChangeOnPathNotification(newDir, FALSE); + std::wstring msg = FormatStrW(LoadStrW(IDS_CREATEDIRECTORY), plan.parentPath.c_str()); + bool dontShow = !Configuration.CnfrmCreateDir; + PromptResult res = gPrompter->ConfirmWithCheckbox(LoadStrW(IDS_QUESTION), msg.c_str(), + LoadStrW(IDS_DONTSHOWAGAINCD), &dontShow); + Configuration.CnfrmCreateDir = !dontShow; + createParents = res.type == PromptResult::kOk; } + if (!createParents) + goto CREATE_AGAIN; + } - //--- path creation - while (1) - { - HCURSOR oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); + std::wstring firstCreatedDirW; + while (!sally::filesystem::EnsureDirectoryTreeExistsW(plan.parentPath, true, &firstCreatedDirW, &failure)) + { + std::wstring errorText = failure.errorTextId != 0 ? LoadStrW(failure.errorTextId) : GetErrorTextW(failure.errorCode); + if (gPrompter->AskRetryCancel(LoadStrW(IDS_ERRORCREATINGDIR), errorText.c_str()).type != PromptResult::kRetry) + goto CREATE_AGAIN; + } - DWORD err; - BOOL invalidName = FileNameInvalidForManualCreate(path); - if (!invalidName && SalCreateDirectoryEx(path, &err)) - { - SetCursor(oldCur); - if (*nextFocus != 0) - strcpy(NextFocusName, nextFocus); + if (!firstCreatedDirW.empty()) + { + std::wstring notifyPathW = firstCreatedDirW; + CutDirectoryW(notifyPathW); + MainWindow->PostChangeOnPathNotificationW(notifyPathW.c_str(), FALSE); + } - // change only in the directory where the directory was created - MainWindow->PostChangeOnPathNotification(checkPath, FALSE); + while (1) + { + HCURSOR oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); - EndStopRefresh(); // snooper will start again now - return; - } - else - { - if (invalidName) - err = ERROR_INVALID_NAME; - SetCursor(oldCur); + DWORD err = ERROR_SUCCESS; + BOOL invalidName = sally::filesystem::IsManualCreateLeafInvalidW(plan.fullPath); + if (!invalidName && SalCreateDirectoryExW(plan.fullPath.c_str(), &err)) + { + SetCursor(oldCur); - CFileErrorDlg dlg2(HWindow, LoadStr(IDS_ERRORCREATINGDIR), path, GetErrorText(err), FALSE, IDD_ERROR3); - dlg2.Execute(); - goto CREATE_AGAIN; - } + NextFocusNameW = plan.nextFocus; + WideToAnsi(NextFocusNameW, NextFocusName, NextFocusName.Size()); + MainWindow->PostChangeOnPathNotificationW(plan.parentPath.c_str(), FALSE); + + EndStopRefresh(); // snooper will start again now + return; } + + if (invalidName) + err = ERROR_INVALID_NAME; + SetCursor(oldCur); + + if (gPrompter->AskRetryCancel(LoadStrW(IDS_ERRORCREATINGDIR), GetErrorTextW(err)).type != PromptResult::kRetry) + goto CREATE_AGAIN; } } } @@ -2504,22 +2681,8 @@ void CFilesWindow::RenameFileInternalW(CFileData* f, const std::wstring& newName return; } - // Convert current path to wide string - std::wstring pathW = AnsiToWide(GetPath()); - - // Build source path using the original wide name (or convert ANSI name if no wide name) - std::wstring srcPath = pathW; - if (!srcPath.empty() && srcPath.back() != L'\\') - srcPath += L'\\'; - if (f->NameW != NULL) - { - srcPath += f->NameW; - } - else - { - // Convert ANSI filename to Unicode (no MAX_PATH limit) - srcPath += AnsiToWide(f->Name); - } + std::wstring pathW = sally::unicode::EffectivePanelPathW(GetPath(), GetPathW()); + std::wstring srcPath = sally::unicode::BuildPanelChildPathW(pathW, f->Name, f->NameW); // Build target path std::wstring tgtPath = pathW; @@ -2533,16 +2696,11 @@ void CFilesWindow::RenameFileInternalW(CFileData* f, const std::wstring& newName return; // no-op rename (same name) } - // Convert paths to ANSI for SalLPMoveFile (which expects ANSI and converts to wide internally) - // For true Unicode support, we need to use the wide path directly with MoveFileW - // Use the \\?\ prefix for long path support - std::wstring srcPathLong = L"\\\\?\\" + srcPath; - std::wstring tgtPathLong = L"\\\\?\\" + tgtPath; - // Check if other panel needs to be notified BOOL handsOFF = FALSE; CFilesWindow* otherPanel = MainWindow->GetNonActivePanel(); - std::wstring otherPathW = AnsiToWide(otherPanel->GetPath()); + std::wstring otherPathW = sally::unicode::EffectivePanelPathW(otherPanel->GetPath(), + otherPanel->GetPathW()); if (otherPathW.length() >= srcPath.length() && _wcsnicmp(srcPath.c_str(), otherPathW.c_str(), srcPath.length()) == 0 && @@ -2554,29 +2712,18 @@ void CFilesWindow::RenameFileInternalW(CFileData* f, const std::wstring& newName *mayChange = TRUE; - // Perform the rename using wide API - BOOL moveRet = MoveFileW(srcPathLong.c_str(), tgtPathLong.c_str()); - DWORD err = 0; - if (!moveRet) - { - err = GetLastError(); - // Try without the long path prefix if it failed - if (err == ERROR_INVALID_NAME || err == ERROR_PATH_NOT_FOUND) - { - moveRet = MoveFileW(srcPath.c_str(), tgtPath.c_str()); - if (!moveRet) - err = GetLastError(); - } - } + IFileSystem* fileSystem = gFileSystem != NULL ? gFileSystem : GetWin32FileSystem(); + FileResult moveResult = fileSystem->MoveFile(srcPath.c_str(), tgtPath.c_str()); - if (moveRet) + if (moveResult.success) { - // Convert new name to ANSI for NextFocusName (best effort) - WideCharToMultiByte(CP_ACP, 0, newName.c_str(), -1, NextFocusName, NextFocusName.Size(), "?", NULL); + NextFocusNameW = newName; + WideToAnsi(newName, NextFocusName, NextFocusName.Size()); *tryAgain = FALSE; } else { + DWORD err = moveResult.errorCode; if (err != ERROR_SUCCESS) gPrompter->ShowError(LoadStrW(IDS_ERRORRENAMINGFILE), GetErrorTextW(err)); *tryAgain = sally::unicode::ShouldRetryUnicodeRenameAfterError(err); @@ -2610,7 +2757,7 @@ void CFilesWindow::RenameFile(int specialIndex) BOOL isDir = i < Dirs->Count; f = isDir ? &Dirs->At(i) : &Files->At(i - Dirs->Count); - BOOL useUnicode = f->UseWideName(); + BOOL useUnicode = f->UseWideName() || sally::unicode::WidePathNeedsExactPreservation(GetPathW()); CPathBuffer formatedFileName; // Heap-allocated for long path support AlterFileName(formatedFileName, f->Name, -1, Configuration.FileNameFormat, 0, isDir); @@ -2648,15 +2795,8 @@ void CFilesWindow::RenameFile(int specialIndex) BeginSuspendMode(); // snooper takes a break BOOL mayChange = FALSE; - int renameAttempt = 0; while (1) { - renameAttempt++; - TRACE_I("RenameFile: attempt=" << renameAttempt - << " f->Name=" << (f->Name ? f->Name : "(null)") - << " f->NameW=" << (f->NameW ? "non-null" : "NULL") - << " f->NameW[0]=" << (f->NameW ? (int)f->NameW[0] : -1) - << " dlg.PathW.len=" << dlg.GetUnicodeResult().length()); // if no item is selected, select the one under focus and store its name CPathBuffer temporarySelected; // Heap-allocated for long path support SelectFocusedItemAndGetName(temporarySelected, temporarySelected.Size()); @@ -2670,9 +2810,9 @@ void CFilesWindow::RenameFile(int specialIndex) { if (useUnicode) { - const wchar_t* dotW = wcsrchr(f->NameW, L'.'); - if (dotW != NULL && dotW > f->NameW) - selectionEnd = (int)(dotW - f->NameW); + const wchar_t* dotW = wcsrchr(initialRenameNameW.c_str(), L'.'); + if (dotW != NULL && dotW > initialRenameNameW.c_str()) + selectionEnd = (int)(dotW - initialRenameNameW.c_str()); } else { @@ -2702,9 +2842,11 @@ void CFilesWindow::RenameFile(int specialIndex) AnsiToWide(f->Name), f->NameW); } - BOOL pathTooLong = (strlen(GetPath()) >= MAX_PATH); + BOOL pathTooLong = (wcslen(GetPathW()) >= MAX_PATH || strlen(GetPath()) >= MAX_PATH); BOOL unicodeNeedsWidePath = !renameResultW.empty() && WideStringUsesAnsiFallback(renameResultW); - if ((!renameResultW.empty() && (useUnicode || unicodeNeedsWidePath)) || pathTooLong) + BOOL panelPathNeedsWide = sally::unicode::WidePathNeedsExactPreservation(GetPathW()); + if ((!renameResultW.empty() && (useUnicode || unicodeNeedsWidePath || panelPathNeedsWide)) || + pathTooLong) { std::wstring newNameW; if (!renameResultW.empty()) @@ -2733,7 +2875,7 @@ void CFilesWindow::RenameFile(int specialIndex) if (mayChange) { // change in the directory shown in the panel and, if a directory was renamed, then also in subdirectories - MainWindow->PostChangeOnPathNotification(GetPath(), isDir); + MainWindow->PostChangeOnPathNotificationW(GetPathW(), isDir); } // if a Salamander window is active, end suspend mode diff --git a/src/files_window_windowproc.cpp b/src/files_window_windowproc.cpp index 33066bae6..aa48b9d4b 100644 --- a/src/files_window_windowproc.cpp +++ b/src/files_window_windowproc.cpp @@ -20,6 +20,7 @@ extern "C" } #include "salshlib.h" #include "zip.h" +#include "common/unicode/PanelPathPolicy.h" //**************************************************************************** @@ -168,7 +169,10 @@ CFilesWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case DBT_DEVICEQUERYREMOVEFAILED: { // TRACE_I("WM_DEVICECHANGE: DBT_DEVICEQUERYREMOVEFAILED"); - ChangeDirectory(this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + if (sally::unicode::HasWidePathW(GetPathW())) + ChangeDirectoryW(this, GetPathW(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + else + ChangeDirectory(this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); return TRUE; } @@ -476,7 +480,12 @@ CFilesWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (err == ERROR_SUCCESS) { if (GetMonitorChanges()) // snooper might have removed it from the list - ChangeDirectory(this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + { + if (sally::unicode::HasWidePathW(GetPathW())) + ChangeDirectoryW(this, GetPathW(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + else + ChangeDirectory(this, GetPath(), MyGetDriveType(GetPath()) == DRIVE_REMOVABLE); + } } else { @@ -779,7 +788,7 @@ CFilesWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (data != NULL) { FocusFirstNewItem = TRUE; - DropCopyMove(data->Copy, data->TargetPath, data->Data); + DropCopyMove(data->Copy, data->TargetPath, data->TargetPathW.c_str(), data->Data); DestroyCopyMoveData(data->Data); delete data; } @@ -880,6 +889,7 @@ CFilesWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (strcmp(f->Name, NextFocusName) == 0) // file found exactly { NextFocusName[0] = 0; + NextFocusNameW.clear(); SetCaretIndex(i, FALSE); break; } @@ -890,6 +900,7 @@ CFilesWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (i == total && found != -1) { NextFocusName[0] = 0; + NextFocusNameW.clear(); SetCaretIndex(found, FALSE); } } diff --git a/src/fileswnd.h b/src/fileswnd.h index 14cef70ed..7ed521a7c 100644 --- a/src/fileswnd.h +++ b/src/fileswnd.h @@ -7,6 +7,8 @@ #include "common/widepath.h" #include "common/CSelectionSnapshot.h" +#include + #define NUM_OF_CHECKTHREADS 30 // maximum number of threads for "non-blocking" path accessibility tests #define ICONOVR_REFRESH_PERIOD 2000 // minimum interval between icon-overlay refreshes in the panel (see IconOverlaysChangedOnPath) #define MIN_DELAY_BETWEENINACTIVEREFRESHES 2000 // minimum refresh interval when the main window is inactive @@ -61,6 +63,7 @@ struct CTmpDropData { BOOL Copy; CPathBuffer TargetPath; + std::wstring TargetPathW; CCopyMoveData* Data; }; @@ -480,6 +483,7 @@ class CFilesWindowAncestor : public CWindow // the real object core - everything { private: CPathBuffer Path; // path for a ptDisk panel - normal ("c:\path") or UNC ("\\server\share\path") + std::wstring PathW; // wide source of truth for disk paths; Path is the ANSI compatibility cache BOOL SuppressAutoRefresh; // TRUE if the user canceled directory listing during reading and chose temporary auto-refresh suppression CPanelType PanelType; // type of panel (disk, archive, plugin FS) @@ -491,6 +495,8 @@ class CFilesWindowAncestor : public CWindow // the real object core - everything CSalamanderDirectory* ArchiveDir; // content of the open archive; basic data - array of CFileData CPathBuffer ZIPArchive; // path to the open archive CPathBuffer ZIPPath; // path inside the open archive + std::wstring ZIPArchiveW; + std::wstring ZIPPathW; FILETIME ZIPArchiveDate; // archive date (used for the ".." date and during refresh) CQuadWord ZIPArchiveSize; // archive size - used to detect archive changes @@ -550,8 +556,10 @@ class CFilesWindowAncestor : public CWindow // the real object core - everything // CPluginInterfaceForFSAbstract::ConvertPathToExternal() is called // it returns TRUE if the path fits into the buffer completely, otherwise a truncated path is returned BOOL GetGeneralPath(char* buf, int bufSize, BOOL convertFSPathToExternal = FALSE); + BOOL GetGeneralPathW(std::wstring& buf, BOOL convertFSPathToExternal = FALSE); const char* GetPath() { return Path; } + const wchar_t* GetPathW() { return PathW.c_str(); } BOOL Is(CPanelType type) { return type == PanelType; } CPanelType GetPanelType() { return PanelType; } BOOL GetMonitorChanges() { return MonitorChanges; } @@ -560,16 +568,21 @@ class CFilesWindowAncestor : public CWindow // the real object core - everything CSalamanderDirectory* GetArchiveDir() { return ArchiveDir; } const char* GetZIPArchive() { return ZIPArchive; } const char* GetZIPPath() { return ZIPPath; } + const wchar_t* GetZIPArchiveW() { return ZIPArchiveW.c_str(); } + const wchar_t* GetZIPPathW() { return ZIPPathW.c_str(); } FILETIME GetZIPArchiveDate() { return ZIPArchiveDate; } BOOL IsSameZIPArchiveSize(const CQuadWord& size) { return ZIPArchiveSize == size; } CQuadWord GetZIPArchiveSize() { return ZIPArchiveSize; } BOOL GetSuppressAutoRefresh() { return SuppressAutoRefresh; } void SetPath(const char* path); + void SetPathW(const wchar_t* path); void SetMonitorChanges(BOOL monitorChanges) { MonitorChanges = monitorChanges; } void SetPanelType(CPanelType type) { PanelType = type; } void SetZIPPath(const char* path); + void SetZIPPathW(const wchar_t* path); void SetZIPArchive(const char* archive); + void SetZIPArchiveW(const wchar_t* archive); void SetArchiveDir(CSalamanderDirectory* dir) { ArchiveDir = dir; } void SetZIPArchiveDate(FILETIME& time) { ZIPArchiveDate = time; } void SetZIPArchiveSize(const CQuadWord& size) { ZIPArchiveSize = size; } @@ -773,6 +786,7 @@ class CFilesWindow : public CFilesWindowAncestor CPathBuffer DropPath; // buffer for the current directory used in a drop operation CPathBuffer NextFocusName; // the name that will receive focus on the next refresh + std::wstring NextFocusNameW; // exact UTF-16 focus target when ANSI cache is lossy BOOL DontClearNextFocusName; // TRUE = do not clear NextFocusName when the main Salamander window is activated BOOL FocusFirstNewItem; // refresh: should the newly added item be selected? (for system New) CTopIndexMem TopIndexMem; // memory of top index for Execute() @@ -925,6 +939,7 @@ class CFilesWindow : public CFilesWindowAncestor // called to inform the panel about changes on 'path'; when 'includingSubdirs' is TRUE, // changes may also occur in subdirectories void AcceptChangeOnPathNotification(const char* path, BOOL includingSubdirs); + void AcceptChangeOnPathNotificationW(const wchar_t* path, BOOL includingSubdirs); // called to notify the panel about icon overlay changes on 'path' (mainly from Tortoise SVN) void IconOverlaysChangedOnPath(const char* path); @@ -956,6 +971,8 @@ class CFilesWindow : public CFilesWindowAncestor // it, the path is copied to 'nextFocus' (see SalGetFullName) BOOL ParsePath(char* path, int& type, BOOL& isDir, char*& secondPart, const char* errorTitle, char* nextFocus, int* error, int pathBufSize); + BOOL ParsePathW(std::wstring& path, int& type, BOOL& isDir, wchar_t*& secondPart, + const wchar_t* errorTitle, std::wstring* nextFocus, int* error); void Execute(int index); // enter + l_dblclk // file from archive: execute (edit==FALSE) or edit (edit + editWithMenuParent != NULL means @@ -1070,6 +1087,11 @@ class CFilesWindow : public CFilesWindowAncestor BOOL refreshListBox = TRUE, BOOL canForce = FALSE, BOOL isRefresh = FALSE, int* failReason = NULL, BOOL shorterPathWarning = TRUE, int tryCloseReason = FSTRYCLOSE_CHANGEPATH); + BOOL ChangePathToDiskW(HWND parent, const wchar_t* path, int suggestedTopIndex = -1, + const char* suggestedFocusName = NULL, BOOL* noChange = NULL, + BOOL refreshListBox = TRUE, BOOL canForce = FALSE, BOOL isRefresh = FALSE, + int* failReason = NULL, BOOL shorterPathWarning = TRUE, + int tryCloseReason = FSTRYCLOSE_CHANGEPATH); // changes to an archive path; only absolute Windows paths are allowed (archive is UNC or C:\path\archive) // if suggestedTopIndex != -1, the top index will be set; // if suggestedFocusName != NULL, and present in the new list, it will be focused; @@ -1089,6 +1111,10 @@ class CFilesWindow : public CFilesWindowAncestor const char* suggestedFocusName = NULL, BOOL forceUpdate = FALSE, BOOL* noChange = NULL, BOOL refreshListBox = TRUE, int* failReason = NULL, BOOL isRefresh = FALSE, BOOL canFocusFileName = FALSE, BOOL isHistory = FALSE); + BOOL ChangePathToArchiveW(const wchar_t* archive, const wchar_t* archivePath = L"", int suggestedTopIndex = -1, + const char* suggestedFocusName = NULL, BOOL forceUpdate = FALSE, + BOOL* noChange = NULL, BOOL refreshListBox = TRUE, int* failReason = NULL, + BOOL isRefresh = FALSE, BOOL canFocusFileName = FALSE, BOOL isHistory = FALSE); // change path to the plug-in FS; // if suggestedTopIndex != -1 the top index will be set; // if suggestedFocusName != NULL and present in the new list, it will be focused; @@ -1115,6 +1141,11 @@ class CFilesWindow : public CFilesWindowAncestor int mode = 2, BOOL* noChange = NULL, BOOL refreshListBox = TRUE, int* failReason = NULL, BOOL isRefresh = FALSE, BOOL canFocusFileName = FALSE, BOOL convertPathToInternal = FALSE); + BOOL ChangePathToPluginFSW(const wchar_t* fsName, const wchar_t* fsUserPart, int suggestedTopIndex = -1, + const char* suggestedFocusName = NULL, BOOL forceUpdate = FALSE, + int mode = 2, BOOL* noChange = NULL, BOOL refreshListBox = TRUE, + int* failReason = NULL, BOOL isRefresh = FALSE, + BOOL canFocusFileName = FALSE, BOOL convertPathToInternal = FALSE); // change path to a detached plug-in FS (in MainWindow->DetachedFSList at index 'fsIndex'); // if suggestedTopIndex != -1, the top index will be set; // if suggestedFocusName != NULL and present in the new list, it will be selected; @@ -1298,7 +1329,7 @@ class CFilesWindow : public CFilesWindowAncestor void RenameFile(int specialIndex = -1); void RenameFileInternal(CFileData* f, const char* formatedFileName, BOOL* mayChange, BOOL* tryAgain); void RenameFileInternalW(CFileData* f, const std::wstring& newName, BOOL* mayChange, BOOL* tryAgain); - void DropCopyMove(BOOL copy, char* targetPath, CCopyMoveData* data); + void DropCopyMove(BOOL copy, char* targetPath, const wchar_t* targetPathW, CCopyMoveData* data); // performs deletion using the SHFileOperation API function (only when deleting to the Recycle Bin) BOOL DeleteThroughRecycleBin(int* selection, int selCount, CFileData* oneFile); @@ -1310,7 +1341,8 @@ class CFilesWindow : public CFilesWindowAncestor char* mask, int selCount, int* selection, CFileData* oneFile, CAttrsData* attrsData, CChangeCaseData* chCaseData, BOOL onlySize, - CCriteriaData* filterCriteria); + CCriteriaData* filterCriteria, + const wchar_t* targetPathW = NULL); BOOL BuildScriptDir(COperations* script, CActionType type, char* sourcePath, BOOL sourcePathSupADS, char* targetPath, CTargetPathState targetPathState, BOOL targetPathSupADS, BOOL targetPathIsFAT32, char* mask, char* dirName, @@ -1318,16 +1350,18 @@ class CFilesWindow : public CFilesWindowAncestor DWORD sourceDirAttr, CChangeCaseData* chCaseData, BOOL firstLevelDir, BOOL onlySize, BOOL fastDirectoryMove, CCriteriaData* filterCriteria, BOOL* canDelUpperDirAfterMove, FILETIME* sourceDirTime, - DWORD srcAndTgtPathsFlags, wchar_t* dirNameW = NULL); + DWORD srcAndTgtPathsFlags, const wchar_t* sourcePathW = NULL, + wchar_t* dirNameW = NULL, const wchar_t* targetPathW = NULL); BOOL BuildScriptFile(COperations* script, CActionType type, char* sourcePath, BOOL sourcePathSupADS, char* targetPath, CTargetPathState targetPathState, BOOL targetPathSupADS, BOOL targetPathIsFAT32, char* mask, char* fileName, char* fileDOSName, const CQuadWord& fileSize, CAttrsData* attrsData, char* mapName, DWORD sourceFileAttr, CChangeCaseData* chCaseData, BOOL onlySize, FILETIME* fileLastWriteTime, DWORD srcAndTgtPathsFlags, - wchar_t* fileNameW = NULL, const wchar_t* mapNameW = NULL); + wchar_t* fileNameW = NULL, const wchar_t* sourcePathW = NULL, + const wchar_t* mapNameW = NULL, const wchar_t* targetPathW = NULL); BOOL BuildScriptMain2(COperations* script, BOOL copy, char* targetDir, - CCopyMoveData* data); + const wchar_t* targetDirW, CCopyMoveData* data); virtual LRESULT WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/filter.cpp b/src/filter.cpp index 0db923914..ebce84505 100644 --- a/src/filter.cpp +++ b/src/filter.cpp @@ -1,11 +1,14 @@ -// SPDX-FileCopyrightText: 2023 Open Salamander Authors +// SPDX-FileCopyrightText: 2023 Open Salamander Authors // SPDX-FileCopyrightText: 2026 Sally Authors // SPDX-License-Identifier: GPL-2.0-or-later #include "precomp.h" +#include "darkmode.h" #include "ui/IPrompter.h" +#include + // Attributes const char* FILTERCRITERIA_ATTRIBUTESMASK_REG = "Attributes Mask"; const char* FILTERCRITERIA_ATTRIBUTESVALUE_REG = "Attributes Value"; @@ -52,6 +55,770 @@ const char* OLD_FINDOPTIONSITEM_HOUR_REG = "Hour"; const char* OLD_FINDOPTIONSITEM_MINUTE_REG = "Minute"; const char* OLD_FINDOPTIONSITEM_SECOND_REG = "Second"; +static const UINT_PTR FILTER_DARK_SKIN_SUBCLASS_ID = 1; +static const COLORREF FILTER_DARK_LINE = RGB(55, 55, 58); +static const COLORREF FILTER_DARK_FRAME = RGB(62, 62, 66); +static const COLORREF FILTER_DARK_BUTTON = RGB(52, 52, 56); +static const COLORREF FILTER_DARK_SECTION_LINE = RGB(82, 82, 86); + +enum CFilterDarkSkinKind +{ + fdskStaticLine, + fdskEdit, + fdskCombo, + fdskUpDown, + fdskDateTime +}; + +struct CFilterDarkSkinState +{ + LONG_PTR Style; + LONG_PTR ExStyle; + CFilterDarkSkinKind Kind; +}; + +static LRESULT CALLBACK FilterDarkSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + +static void FilterFillRectSolid(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldColor = SetDCBrushColor(hdc, color); + FillRect(hdc, rect, (HBRUSH)GetStockObject(DC_BRUSH)); + SetDCBrushColor(hdc, oldColor); + SelectObject(hdc, oldBrush); +} + +static void FilterDrawRectOutline(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(NULL_BRUSH)); + COLORREF oldColor = SetDCPenColor(hdc, color); + Rectangle(hdc, rect->left, rect->top, rect->right, rect->bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static void FilterDrawDownArrow(HDC hdc, const RECT* rect, COLORREF color) +{ + int centerX = (rect->left + rect->right) / 2; + int centerY = (rect->top + rect->bottom) / 2; + POINT arrow[3] = { + {centerX - 3, centerY - 1}, + {centerX + 4, centerY - 1}, + {centerX, centerY + 3}, + }; + + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldPenColor = SetDCPenColor(hdc, color); + COLORREF oldBrushColor = SetDCBrushColor(hdc, color); + Polygon(hdc, arrow, 3); + SetDCBrushColor(hdc, oldBrushColor); + SetDCPenColor(hdc, oldPenColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static void FilterDrawUpArrow(HDC hdc, const RECT* rect, COLORREF color) +{ + int centerX = (rect->left + rect->right) / 2; + int centerY = (rect->top + rect->bottom) / 2; + POINT arrow[3] = { + {centerX - 3, centerY + 2}, + {centerX + 4, centerY + 2}, + {centerX, centerY - 2}, + }; + + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldPenColor = SetDCPenColor(hdc, color); + COLORREF oldBrushColor = SetDCBrushColor(hdc, color); + Polygon(hdc, arrow, 3); + SetDCBrushColor(hdc, oldBrushColor); + SetDCPenColor(hdc, oldPenColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static void FilterDrawCheckMark(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, color); + int left = rect->left + max(2, (rect->right - rect->left) / 4); + int midY = (rect->top + rect->bottom) / 2; + MoveToEx(hdc, left, midY, NULL); + LineTo(hdc, left + 3, midY + 3); + LineTo(hdc, rect->right - 2, rect->top + 3); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); +} + +static void SetFilterWindowStyle(HWND hwnd, LONG_PTR style) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + if (GetWindowLongPtr(hwnd, GWL_STYLE) == style) + return; + + SetWindowLongPtr(hwnd, GWL_STYLE, style); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); +} + +static void SetFilterWindowExStyle(HWND hwnd, LONG_PTR exStyle) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + if (GetWindowLongPtr(hwnd, GWL_EXSTYLE) == exStyle) + return; + + SetWindowLongPtr(hwnd, GWL_EXSTYLE, exStyle); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); +} + +static BOOL GetFilterChildRectInDialog(HWND hDialog, int ctrlID, RECT* rect) +{ + if (hDialog == NULL || rect == NULL || !IsWindow(hDialog)) + return FALSE; + + HWND hChild = GetDlgItem(hDialog, ctrlID); + if (hChild == NULL || !IsWindow(hChild)) + return FALSE; + + if (!GetWindowRect(hChild, rect)) + return FALSE; + MapWindowPoints(NULL, hDialog, (POINT*)rect, 2); + return TRUE; +} + +static void PaintFilterDarkEditFrame(HWND hwnd) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return; + + HDC hdc = GetWindowDC(hwnd); + if (hdc == NULL) + return; + + RECT window; + GetWindowRect(hwnd, &window); + OffsetRect(&window, -window.left, -window.top); + + RECT client; + GetClientRect(hwnd, &client); + MapWindowPoints(hwnd, NULL, (POINT*)&client, 2); + RECT screenWindow; + GetWindowRect(hwnd, &screenWindow); + OffsetRect(&client, -screenWindow.left, -screenWindow.top); + + int savedDC = SaveDC(hdc); + ExcludeClipRect(hdc, client.left, client.top, client.right, client.bottom); + FilterFillRectSolid(hdc, &window, colors.InputBackground); + RestoreDC(hdc, savedDC); + + FilterDrawRectOutline(hdc, &window, FILTER_DARK_FRAME); + ReleaseDC(hwnd, hdc); +} + +static BOOL PaintFilterDarkStaticLine(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FilterFillRectSolid(hdc, &client, colors.DialogBackground); + int y = max(client.top, min(client.bottom - 1, (client.top + client.bottom) / 2)); + + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FILTER_DARK_LINE); + MoveToEx(hdc, client.left, y, NULL); + LineTo(hdc, client.right, y); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + +static BOOL GetFilterComboText(HWND hwnd, LPTSTR text, int textLen) +{ + if (text == NULL || textLen <= 0) + return FALSE; + + text[0] = 0; + int curSel = (int)SendMessage(hwnd, CB_GETCURSEL, 0, 0); + if (curSel >= 0) + { + SendMessage(hwnd, CB_GETLBTEXT, curSel, (LPARAM)text); + text[textLen - 1] = 0; + return text[0] != 0; + } + + GetWindowText(hwnd, text, textLen); + text[textLen - 1] = 0; + return text[0] != 0; +} + +static BOOL PaintFilterDarkCombo(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FilterFillRectSolid(hdc, &client, colors.InputBackground); + + int buttonWidth = max(GetSystemMetrics(SM_CXVSCROLL), client.bottom - client.top); + RECT button = client; + button.left = max(client.left + 1, client.right - buttonWidth - 1); + button.top = client.top + 1; + button.right = client.right - 1; + button.bottom = client.bottom - 1; + + RECT textRect = client; + textRect.left += 4; + textRect.right = max(textRect.left, button.left - 3); + + HFONT hFont = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); + HFONT hOldFont = NULL; + if (hFont != NULL) + hOldFont = (HFONT)SelectObject(hdc, hFont); + + TCHAR text[256]; + GetFilterComboText(hwnd, text, _countof(text)); + + int oldBkMode = SetBkMode(hdc, TRANSPARENT); + COLORREF oldTextColor = SetTextColor(hdc, IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText); + DrawText(hdc, text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS); + SetTextColor(hdc, oldTextColor); + SetBkMode(hdc, oldBkMode); + if (hOldFont != NULL) + SelectObject(hdc, hOldFont); + + if (button.right > button.left && button.bottom > button.top) + { + FilterFillRectSolid(hdc, &button, FILTER_DARK_BUTTON); + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FILTER_DARK_LINE); + MoveToEx(hdc, button.left, button.top, NULL); + LineTo(hdc, button.left, button.bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + FilterDrawDownArrow(hdc, &button, IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText); + } + + FilterDrawRectOutline(hdc, &client, FILTER_DARK_FRAME); + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + +static BOOL PaintFilterDarkUpDown(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FilterFillRectSolid(hdc, &client, FILTER_DARK_BUTTON); + + int midY = (client.top + client.bottom) / 2; + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FILTER_DARK_LINE); + MoveToEx(hdc, client.left, midY, NULL); + LineTo(hdc, client.right, midY); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + + RECT up = client; + up.bottom = midY; + RECT down = client; + down.top = midY; + COLORREF arrowColor = IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText; + FilterDrawUpArrow(hdc, &up, arrowColor); + FilterDrawDownArrow(hdc, &down, arrowColor); + FilterDrawRectOutline(hdc, &client, FILTER_DARK_FRAME); + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + +static BOOL GetFilterDateTimeText(HWND hwnd, LPTSTR text, int textLen, BOOL* hasValue) +{ + if (text == NULL || textLen <= 0) + return FALSE; + + text[0] = 0; + if (hasValue != NULL) + *hasValue = FALSE; + + SYSTEMTIME st; + LRESULT state = DateTime_GetSystemtime(hwnd, &st); + if (state != GDT_VALID) + return FALSE; + + if (hasValue != NULL) + *hasValue = TRUE; + + LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + BOOL ok; + if ((style & DTS_UPDOWN) != 0) + ok = GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, text, textLen) != 0; + else + ok = GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, NULL, text, textLen) != 0; + if (!ok) + text[0] = 0; + text[textLen - 1] = 0; + return ok; +} + +static BOOL PaintFilterDarkDateTime(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = BeginPaint(hwnd, &ps); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FilterFillRectSolid(hdc, &client, colors.InputBackground); + + LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + int buttonWidth = max(GetSystemMetrics(SM_CXVSCROLL), client.bottom - client.top); + RECT button = client; + button.left = max(client.left + 1, client.right - buttonWidth - 1); + button.top = client.top + 1; + button.right = client.right - 1; + button.bottom = client.bottom - 1; + + RECT textRect = client; + textRect.left += 4; + textRect.right = max(textRect.left, button.left - 3); + + BOOL hasValue = FALSE; + if ((style & DTS_SHOWNONE) != 0) + { + int boxSize = max(9, min(13, client.bottom - client.top - 4)); + RECT check = { + client.left + 4, + client.top + max(1, (client.bottom - client.top - boxSize) / 2), + client.left + 4 + boxSize, + client.top + max(1, (client.bottom - client.top - boxSize) / 2) + boxSize}; + FilterFillRectSolid(hdc, &check, colors.InputBackground); + FilterDrawRectOutline(hdc, &check, FILTER_DARK_FRAME); + + TCHAR probe[8]; + GetFilterDateTimeText(hwnd, probe, _countof(probe), &hasValue); + if (hasValue) + FilterDrawCheckMark(hdc, &check, IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText); + textRect.left = check.right + 4; + } + + TCHAR text[128]; + GetFilterDateTimeText(hwnd, text, _countof(text), &hasValue); + + HFONT hFont = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); + HFONT hOldFont = NULL; + if (hFont != NULL) + hOldFont = (HFONT)SelectObject(hdc, hFont); + + int oldBkMode = SetBkMode(hdc, TRANSPARENT); + COLORREF oldTextColor = SetTextColor(hdc, IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText); + DrawText(hdc, text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS); + SetTextColor(hdc, oldTextColor); + SetBkMode(hdc, oldBkMode); + if (hOldFont != NULL) + SelectObject(hdc, hOldFont); + + if (button.right > button.left && button.bottom > button.top) + { + FilterFillRectSolid(hdc, &button, FILTER_DARK_BUTTON); + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FILTER_DARK_LINE); + MoveToEx(hdc, button.left, button.top, NULL); + LineTo(hdc, button.left, button.bottom); + if ((style & DTS_UPDOWN) != 0) + { + int midY = (button.top + button.bottom) / 2; + MoveToEx(hdc, button.left, midY, NULL); + LineTo(hdc, button.right, midY); + } + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + + COLORREF arrowColor = IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText; + if ((style & DTS_UPDOWN) != 0) + { + RECT up = button; + up.bottom = (button.top + button.bottom) / 2; + RECT down = button; + down.top = up.bottom; + FilterDrawUpArrow(hdc, &up, arrowColor); + FilterDrawDownArrow(hdc, &down, arrowColor); + } + else + FilterDrawDownArrow(hdc, &button, arrowColor); + } + + FilterDrawRectOutline(hdc, &client, FILTER_DARK_FRAME); + + if (paintDC == NULL) + EndPaint(hwnd, &ps); + return TRUE; +} + +static void PaintFilterDarkFrame(HWND hwnd) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return; + + HDC hdc = GetWindowDC(hwnd); + if (hdc == NULL) + return; + + RECT rect; + GetWindowRect(hwnd, &rect); + OffsetRect(&rect, -rect.left, -rect.top); + FilterDrawRectOutline(hdc, &rect, FILTER_DARK_FRAME); + ReleaseDC(hwnd, hdc); +} + +static void SetFilterDateTimeCalendarColors(HWND hwnd, BOOL useDark) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + DarkModeColors colors; + DarkMode_GetColors(&colors); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_BACKGROUND, useDark ? colors.DialogBackground : CLR_DEFAULT); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_MONTHBK, useDark ? colors.InputBackground : CLR_DEFAULT); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_TEXT, useDark ? colors.InputText : CLR_DEFAULT); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_TITLEBK, useDark ? FILTER_DARK_BUTTON : CLR_DEFAULT); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_TITLETEXT, useDark ? colors.InputText : CLR_DEFAULT); + SendMessage(hwnd, DTM_SETMCCOLOR, MCSC_TRAILINGTEXT, useDark ? colors.DisabledText : CLR_DEFAULT); +} + +static void ApplyFilterDarkSkin(HWND hwnd, CFilterDarkSkinKind kind) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + DWORD_PTR data = 0; + CFilterDarkSkinState* state = NULL; + if (GetWindowSubclass(hwnd, FilterDarkSkinSubclassProc, FILTER_DARK_SKIN_SUBCLASS_ID, &data)) + state = (CFilterDarkSkinState*)data; + else + { + state = new CFilterDarkSkinState; + if (state == NULL) + return; + state->Style = GetWindowLongPtr(hwnd, GWL_STYLE); + state->ExStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE); + state->Kind = kind; + if (!SetWindowSubclass(hwnd, FilterDarkSkinSubclassProc, FILTER_DARK_SKIN_SUBCLASS_ID, (DWORD_PTR)state)) + { + delete state; + return; + } + } + + state->Kind = kind; + BOOL useDark = DarkMode_ShouldUseDark(); + LONG_PTR style = state->Style; + LONG_PTR exStyle = state->ExStyle; + if (useDark && kind != fdskStaticLine) + { + style &= ~WS_BORDER; + exStyle &= ~(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); + } + + SetFilterWindowStyle(hwnd, style); + SetFilterWindowExStyle(hwnd, exStyle); + + if (kind == fdskStaticLine) + ShowWindow(hwnd, useDark ? SW_HIDE : SW_SHOWNA); + + if (kind != fdskStaticLine) + SetWindowTheme(hwnd, useDark ? L"" : NULL, NULL); + + if (kind == fdskCombo) + { + COMBOBOXINFO cbi = {0}; + cbi.cbSize = sizeof(cbi); + if (GetComboBoxInfo(hwnd, &cbi) && cbi.hwndList != NULL && IsWindow(cbi.hwndList)) + SetWindowTheme(cbi.hwndList, useDark ? L"DarkMode_Explorer" : NULL, NULL); + } + else if (kind == fdskDateTime) + SetFilterDateTimeCalendarColors(hwnd, useDark); + + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); +} + +static BOOL PaintFilterCriteriaDialogSectionLines(HWND hDialog, HDC paintDC) +{ + if (!DarkMode_ShouldUseDark()) + return FALSE; + + HDC hdc = paintDC; + if (hdc == NULL) + hdc = GetDC(hDialog); + if (hdc == NULL) + return FALSE; + + int lineIDs[] = {IDC_STATIC_2, IDC_STATIC_4, IDC_STATIC_6, IDC_STATIC_8}; + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FILTER_DARK_SECTION_LINE); + for (int i = 0; i < _countof(lineIDs); i++) + { + RECT rect; + if (!GetFilterChildRectInDialog(hDialog, lineIDs[i], &rect)) + continue; + + int y = max(rect.top, min(rect.bottom - 1, (rect.top + rect.bottom) / 2)); + MoveToEx(hdc, rect.left, y, NULL); + LineTo(hdc, rect.right, y); + } + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + + if (paintDC == NULL) + ReleaseDC(hDialog, hdc); + return TRUE; +} + +static void ApplyFilterCriteriaDialogTheme(HWND hDialog) +{ + if (hDialog == NULL || !IsWindow(hDialog)) + return; + + DarkMode_ApplyTitleBar(hDialog); + DarkMode_ApplyListTreeThemeRecursive(hDialog); + + int lineIDs[] = {IDC_STATIC_2, IDC_STATIC_4, IDC_STATIC_6, IDC_STATIC_8}; + for (int i = 0; i < _countof(lineIDs); i++) + ApplyFilterDarkSkin(GetDlgItem(hDialog, lineIDs[i]), fdskStaticLine); + + int editIDs[] = {IDC_FFA_SIZEMIN_VALUE, IDC_FFA_SIZEMAX_VALUE, IDC_FFA_TIMEDURING_VALUE}; + for (int i = 0; i < _countof(editIDs); i++) + ApplyFilterDarkSkin(GetDlgItem(hDialog, editIDs[i]), fdskEdit); + + int upDownIDs[] = {IDC_FFA_SIZEMIN_UPDOWN, IDC_FFA_SIZEMAX_UPDOWN, IDC_FFA_TIMEDURING_UPDOWN}; + for (int i = 0; i < _countof(upDownIDs); i++) + ApplyFilterDarkSkin(GetDlgItem(hDialog, upDownIDs[i]), fdskUpDown); + + int comboIDs[] = {IDC_FFA_SIZEMIN_UNITS, IDC_FFA_SIZEMAX_UNITS, IDC_FFA_TIMEDURING_UNITS}; + for (int i = 0; i < _countof(comboIDs); i++) + ApplyFilterDarkSkin(GetDlgItem(hDialog, comboIDs[i]), fdskCombo); + + int dateTimeIDs[] = {IDC_FFA_FROM_DATE, IDC_FFA_FROM_TIME, IDC_FFA_TO_DATE, IDC_FFA_TO_TIME}; + for (int i = 0; i < _countof(dateTimeIDs); i++) + ApplyFilterDarkSkin(GetDlgItem(hDialog, dateTimeIDs[i]), fdskDateTime); + + InvalidateRect(hDialog, NULL, TRUE); + RedrawWindow(hDialog, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); +} + +static void RedrawFilterCriteriaDialogSkin(HWND hDialog) +{ + if (hDialog == NULL || !DarkMode_ShouldUseDark()) + return; + + int controlIDs[] = { + IDC_STATIC_2, IDC_STATIC_4, IDC_STATIC_6, IDC_STATIC_8, + IDC_FFA_SIZEMIN_VALUE, IDC_FFA_SIZEMAX_VALUE, IDC_FFA_TIMEDURING_VALUE, + IDC_FFA_SIZEMIN_UPDOWN, IDC_FFA_SIZEMAX_UPDOWN, IDC_FFA_TIMEDURING_UPDOWN, + IDC_FFA_SIZEMIN_UNITS, IDC_FFA_SIZEMAX_UNITS, IDC_FFA_TIMEDURING_UNITS, + IDC_FFA_FROM_DATE, IDC_FFA_FROM_TIME, IDC_FFA_TO_DATE, IDC_FFA_TO_TIME}; + + for (int i = 0; i < _countof(controlIDs); i++) + { + HWND hCtrl = GetDlgItem(hDialog, controlIDs[i]); + if (hCtrl != NULL && IsWindow(hCtrl)) + RedrawWindow(hCtrl, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); + } +} + +static LRESULT CALLBACK FilterDarkSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + CFilterDarkSkinState* state = (CFilterDarkSkinState*)dwRefData; + CFilterDarkSkinKind kind = state != NULL ? state->Kind : fdskEdit; + + switch (uMsg) + { + case WM_NCPAINT: + { + if (DarkMode_ShouldUseDark() && kind != fdskStaticLine) + { + if (kind == fdskEdit) + PaintFilterDarkEditFrame(hwnd); + else + PaintFilterDarkFrame(hwnd); + return 0; + } + break; + } + + case WM_PAINT: + { + if (DarkMode_ShouldUseDark()) + { + switch (kind) + { + case fdskStaticLine: + if (PaintFilterDarkStaticLine(hwnd, NULL)) + return 0; + break; + + case fdskCombo: + if (PaintFilterDarkCombo(hwnd, NULL)) + return 0; + break; + + case fdskUpDown: + if (PaintFilterDarkUpDown(hwnd, NULL)) + return 0; + break; + + case fdskDateTime: + if (PaintFilterDarkDateTime(hwnd, NULL)) + return 0; + break; + + case fdskEdit: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + PaintFilterDarkEditFrame(hwnd); + return ret; + } + } + } + break; + } + + case WM_PRINTCLIENT: + { + if (DarkMode_ShouldUseDark()) + { + switch (kind) + { + case fdskStaticLine: + if (PaintFilterDarkStaticLine(hwnd, (HDC)wParam)) + return 0; + break; + + case fdskCombo: + if (PaintFilterDarkCombo(hwnd, (HDC)wParam)) + return 0; + break; + + case fdskUpDown: + if (PaintFilterDarkUpDown(hwnd, (HDC)wParam)) + return 0; + break; + + case fdskDateTime: + if (PaintFilterDarkDateTime(hwnd, (HDC)wParam)) + return 0; + break; + + default: + break; + } + } + break; + } + + case WM_ERASEBKGND: + { + DarkModeColors colors; + if (DarkMode_GetColors(&colors)) + { + RECT client; + GetClientRect(hwnd, &client); + FilterFillRectSolid((HDC)wParam, &client, kind == fdskStaticLine ? colors.DialogBackground : colors.InputBackground); + return TRUE; + } + break; + } + + case WM_CTLCOLORLISTBOX: + { + HBRUSH hBrush = DarkMode_GetDialogCtlColorBrush(uMsg, (HDC)wParam, (HWND)lParam); + if (hBrush != NULL) + return (LRESULT)hBrush; + break; + } + + case WM_ENABLE: + case WM_SETTEXT: + case WM_SIZE: + case WM_SETFOCUS: + case WM_KILLFOCUS: + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + case CB_SETCURSEL: + case CB_ADDSTRING: + case CB_DELETESTRING: + case CB_RESETCONTENT: + case DTM_SETSYSTEMTIME: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, FilterDarkSkinSubclassProc, uIdSubclass); + delete state; + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + // leap year #define IsLeapYear(_yr) ((!((_yr) % 400) || ((_yr) % 100) && !((_yr) % 4)) ? TRUE : FALSE) @@ -1247,6 +2014,8 @@ void CFilterCriteriaDialog::EnableControls() // disable the control that currently has focus; we handle it if (hFocus != NULL && !IsWindowEnabled(hFocus)) SendMessage(HWindow, WM_NEXTDLGCTL, (WPARAM)GetDlgItem(HWindow, IDC_FFA_SIZEMIN), TRUE); + + RedrawFilterCriteriaDialogSkin(HWindow); } INT_PTR @@ -1255,6 +2024,20 @@ CFilterCriteriaDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) CALL_STACK_MESSAGE4("CFilterCriteriaDialog::DialogProc(0x%X, 0x%IX, 0x%IX)", uMsg, wParam, lParam); switch (uMsg) { + case WM_PAINT: + { + INT_PTR ret = CCommonDialog::DialogProc(uMsg, wParam, lParam); + PaintFilterCriteriaDialogSectionLines(HWindow, NULL); + return ret; + } + + case WM_PRINTCLIENT: + { + INT_PTR ret = CCommonDialog::DialogProc(uMsg, wParam, lParam); + PaintFilterCriteriaDialogSectionLines(HWindow, (HDC)wParam); + return ret; + } + case WM_INITDIALOG: { // attach the UpDown control to the edit line @@ -1276,6 +2059,19 @@ CFilterCriteriaDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (!EnableDirectory) EnableWindow(GetDlgItem(HWindow, IDC_FFA_ATTRDIRECTORY), FALSE); + ApplyFilterCriteriaDialogTheme(HWindow); + break; + } + + case WM_CTLCOLORDLG: + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORBTN: + case WM_CTLCOLORLISTBOX: + { + HBRUSH hBrush = DarkMode_GetDialogCtlColorBrush(uMsg, (HDC)wParam, (HWND)lParam); + if (hBrush != NULL) + return (INT_PTR)hBrush; break; } @@ -1333,6 +2129,14 @@ CFilterCriteriaDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) EnableControls(); break; } + + case WM_SETTINGCHANGE: + case WM_THEMECHANGED: + case WM_SYSCOLORCHANGE: + { + ApplyFilterCriteriaDialogTheme(HWindow); + break; + } } return CCommonDialog::DialogProc(uMsg, wParam, lParam); } diff --git a/src/find.cpp b/src/find.cpp index adcdc6fef..12018396e 100644 --- a/src/find.cpp +++ b/src/find.cpp @@ -812,12 +812,16 @@ BOOL CDuplicateCandidates::GetMD5Digest(CGrepData* data, CFoundFilesData* file, CPathBuffer fullPath; // Heap-allocated for long path support lstrcpyn(fullPath, file->Path.c_str(), fullPath.Size()); SalPathAppend(fullPath, file->Name.c_str(), fullPath.Size()); + std::wstring fullPathW = !file->PathW.empty() ? file->PathW : AnsiToWide(file->Path.c_str()); + if (!fullPathW.empty() && fullPathW[fullPathW.length() - 1] != L'\\' && fullPathW[fullPathW.length() - 1] != L'/') + fullPathW.push_back(L'\\'); + fullPathW += !file->NameW.empty() ? file->NameW : AnsiToWide(file->Name.c_str()); data->SearchingText->Set(fullPath); // set the current file // open the file for reading with sequential access - HANDLE hFile = HANDLES_Q(CreateFileW(AnsiToWide(fullPath).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL)); + HANDLE hFile = SalCreateFileWideH(fullPathW.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hFile != INVALID_HANDLE_VALUE) { BYTE buffer[DUPLICATES_BUFFER_SIZE]; @@ -1132,8 +1136,17 @@ void CDuplicateCandidates::Examine(CGrepData* data) // void CSearchForData::Set(const char* dir, const char* masksGroup, BOOL includeSubDirs) +{ + Set(dir, NULL, masksGroup, includeSubDirs); +} + +void CSearchForData::Set(const char* dir, const wchar_t* dirW, const char* masksGroup, BOOL includeSubDirs) { strcpy(Dir, dir); + if (dirW != NULL && dirW[0] != L'\0') + DirW = dirW; + else + DirW = AnsiToWide(dir); MasksGroup.SetMasksString(masksGroup); IncludeSubDirs = includeSubDirs; } @@ -1342,7 +1355,8 @@ BOOL TestFileContentAux(BOOL& ok, CQuadWord& fileOffset, const CQuadWord& totalS } } -BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* data, BOOL isLink) +BOOL TestFileContentW(DWORD sizeLow, DWORD sizeHigh, const wchar_t* pathW, + const char* pathA, CGrepData* data, BOOL isLink) { CQuadWord totalSize(sizeLow, sizeHigh); CQuadWord fileOffset(0, 0); @@ -1352,12 +1366,12 @@ BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* if (totalSize > CQuadWord(0, 0) || isLink) { DWORD err = ERROR_SUCCESS; - data->SearchingText->Set(path); // set the current file - HANDLE hFile = HANDLES_Q(CreateFileW(AnsiToWide(path).c_str(), GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, - OPEN_EXISTING, - FILE_FLAG_SEQUENTIAL_SCAN, - NULL)); + data->SearchingText->Set(pathA); // set the current file + HANDLE hFile = SalCreateFileWideH(pathW, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, + FILE_FLAG_SEQUENTIAL_SCAN, + NULL); BOOL getLinkFileSizeErr = FALSE; if (hFile != INVALID_HANDLE_VALUE) { @@ -1390,7 +1404,7 @@ BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* // let the file view be examined DWORD diff = (DWORD)(fileOffset - mapFileOffset).Value; BOOL err2 = !TestFileContentAux(ok, fileOffset, totalSize, viewSize - diff, - path, txt + diff, data); + pathA, txt + diff, data); HANDLES(UnmapViewOfFile(txt)); if (err2 || ok) break; @@ -1419,7 +1433,7 @@ BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* FIND_LOG_ITEM log; log.Flags = FLI_ERROR; log.Text = buf; - log.Path = path; + log.Path = pathA; SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); } } @@ -1427,9 +1441,15 @@ BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* return ok; } -BOOL AddFoundItem(const char* path, const char* name, DWORD sizeLow, DWORD sizeHigh, - DWORD attr, const FILETIME* lastWrite, BOOL isDir, CGrepData* data, - CDuplicateCandidates* duplicateCandidates) +BOOL TestFileContent(DWORD sizeLow, DWORD sizeHigh, const char* path, CGrepData* data, BOOL isLink) +{ + std::wstring pathW = AnsiToWide(path); + return TestFileContentW(sizeLow, sizeHigh, pathW.c_str(), path, data, isLink); +} + +BOOL AddFoundItemEx(const char* path, const char* name, const wchar_t* pathW, const wchar_t* nameW, + DWORD sizeLow, DWORD sizeHigh, DWORD attr, const FILETIME* lastWrite, BOOL isDir, + CGrepData* data, CDuplicateCandidates* duplicateCandidates) { if (duplicateCandidates != NULL && isDir) // directories are irrelevant to us when searching for duplicates return TRUE; @@ -1437,7 +1457,7 @@ BOOL AddFoundItem(const char* path, const char* name, DWORD sizeLow, DWORD sizeH CFoundFilesData* foundData = new CFoundFilesData; if (foundData != NULL) { - BOOL good = foundData->Set(path, name, + BOOL good = foundData->Set(path, name, pathW, nameW, CQuadWord(sizeLow, sizeHigh), attr, lastWrite, isDir); if (good) @@ -1499,6 +1519,22 @@ BOOL AddFoundItem(const char* path, const char* name, DWORD sizeLow, DWORD sizeH return TRUE; } +BOOL AddFoundItem(const char* path, const char* name, DWORD sizeLow, DWORD sizeHigh, + DWORD attr, const FILETIME* lastWrite, BOOL isDir, CGrepData* data, + CDuplicateCandidates* duplicateCandidates) +{ + return AddFoundItemEx(path, name, NULL, NULL, sizeLow, sizeHigh, attr, lastWrite, isDir, + data, duplicateCandidates); +} + +BOOL AddFoundItemW(const char* path, const char* name, const wchar_t* pathW, const wchar_t* nameW, + DWORD sizeLow, DWORD sizeHigh, DWORD attr, const FILETIME* lastWrite, BOOL isDir, + CGrepData* data, CDuplicateCandidates* duplicateCandidates) +{ + return AddFoundItemEx(path, name, pathW, nameW, sizeLow, sizeHigh, attr, lastWrite, isDir, + data, duplicateCandidates); +} + // 'dirStack' stores directories for late grepping. Otherwise, // during searching in the current directory, recursive searching in subdirectories would occur. With this // trick all files and directories matching the criteria are found first and @@ -1775,6 +1811,273 @@ void SearchDirectory(CPathBuffer& path, char* end, int startPathLen, *end = 0; } +static std::wstring GetDirectoryWithoutSearchBackslashW(const std::wstring& pathW, size_t endIndex) +{ + std::wstring result(pathW, 0, endIndex); + if (result.length() > 3 && (result[result.length() - 1] == L'\\' || result[result.length() - 1] == L'/')) + result.resize(result.length() - 1); + return result; +} + +static BOOL WideFindPathFits(size_t baseLen, size_t extraLen) +{ + return baseLen + extraLen < SAL_MAX_LONG_PATH; +} + +static void WideFileNameToAnsi(const wchar_t* fileNameW, char* fileNameA, int fileNameASize) +{ + if (fileNameASize <= 0) + return; + int copied = WideCharToMultiByte(CP_ACP, 0, fileNameW, -1, fileNameA, fileNameASize, NULL, NULL); + if (copied <= 0) + fileNameA[0] = 0; + fileNameA[fileNameASize - 1] = 0; +} + +void SearchDirectoryW(std::wstring& pathW, size_t endIndex, int startPathLen, + CMaskGroup* masksGroup, BOOL includeSubDirs, CGrepData* data, + TDirectArray* dirStack, int dirStackCount, + CDuplicateCandidates* duplicateCandidates, + CFindIgnore* ignoreList, CPathBuffer& message) +{ + std::wstring pathWithSlashW(pathW, 0, endIndex); + std::string pathWithSlashA = WideToAnsi(pathWithSlashW); + SLOW_CALL_STACK_MESSAGE6("SearchDirectoryW(%s, , %d, %s, %d, , , %d, , )", + pathWithSlashA.c_str(), startPathLen, masksGroup->GetMasksString(), + includeSubDirs, dirStackCount); + + if (ignoreList != NULL && ignoreList->Contains(pathWithSlashA.c_str(), startPathLen)) + { + FIND_LOG_ITEM log; + log.Flags = FLI_INFO; + log.Text = LoadStr(IDS_FINDLOG_SKIP); + log.Path = pathWithSlashA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + return; + } + + if (!WideFindPathFits(endIndex, 1)) + { + FIND_LOG_ITEM log; + log.Flags = FLI_ERROR; + log.Text = LoadStr(IDS_TOOLONGNAME); + log.Path = pathWithSlashA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + return; + } + + WIN32_FIND_DATAW file; + std::wstring findMaskW = pathWithSlashW; + findMaskW.push_back(L'*'); + HANDLE find = SalFindFirstFileWideH(findMaskW.c_str(), &file); + if (find != INVALID_HANDLE_VALUE) + { + std::wstring displayDirW = GetDirectoryWithoutSearchBackslashW(pathW, endIndex); + std::string displayDirA = WideToAnsi(displayDirW); + data->SearchingText->Set(displayDirA.c_str()); // set the current path + + int dirStackEnterCount = 0; // number of items before starting the search at this level + if (dirStack != NULL) + dirStackEnterCount = dirStackCount; + BOOL testFindNextErr = TRUE; + + do + { + char cFileNameA[MAX_PATH]; + WideFileNameToAnsi(file.cFileName, cFileNameA, MAX_PATH); + BOOL isDir = (file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + BOOL ignoreDir = isDir && (wcscmp(file.cFileName, L".") == 0 || wcscmp(file.cFileName, L"..") == 0); + size_t fileNameLenW = wcslen(file.cFileName); + if (ignoreDir || WideFindPathFits(endIndex, fileNameLenW)) + { + // after finding an item without displaying it and once 0.5 s have passed since the last redraw, + // we request the listview to redraw + if (data->NeedRefresh && GetTickCount() - data->FoundVisibleTick >= 500) + { + SendMessage(data->HWindow, WM_USER_ADDFILE, 0, 0); + data->NeedRefresh = FALSE; + } + + if (cFileNameA[0] != 0 && !ignoreDir) + { + // add all files and directories except "." and ".." + + // test the criteria attributes, size, date and time + CQuadWord size(file.nFileSizeLow, file.nFileSizeHigh); + if (data->Criteria.Test(file.dwFileAttributes, &size, &file.ftLastWriteTime)) + { + // file name + // let the extension be resolved if ext==NULL + if (masksGroup->AgreeMasks(cFileNameA, NULL)) // mask is OK + { + BOOL ok; + if (data->Grep) + { + // content + if (isDir) + ok = FALSE; // a directory cannot be grepped + else + { + std::wstring fullPathW = pathWithSlashW; + fullPathW.append(file.cFileName); + std::string fullPathA = WideToAnsi(fullPathW); + // links: file.nFileSizeLow == 0 && file.nFileSizeHigh == 0, the file size + // must be additionally obtained via SalGetFileSize() + BOOL isLink = (file.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + ok = TestFileContentW(file.nFileSizeLow, file.nFileSizeHigh, + fullPathW.c_str(), fullPathA.c_str(), + data, isLink); + } + } + else + ok = TRUE; + + // if the item matches all criteria, + // add it to the list of found items + if (ok) + { + AddFoundItemW(displayDirA.c_str(), cFileNameA, + displayDirW.c_str(), file.cFileName, + file.nFileSizeLow, file.nFileSizeHigh, + file.dwFileAttributes, &file.ftLastWriteTime, isDir, data, + duplicateCandidates); + } + } + } + } + if (isDir && includeSubDirs && !ignoreDir) // directory + not "." or ".." + { + if (WideFindPathFits(endIndex, fileNameLenW + 1 /* backslash */)) + { + BOOL searchNow = TRUE; + + if (dirStack != NULL) + { + // just store for later search + wchar_t* newFileName = new wchar_t[fileNameLenW + 1]; + if (newFileName != NULL) + { + wcscpy_s(newFileName, fileNameLenW + 1, file.cFileName); + if (dirStackCount < dirStack->Count) + { + // no need to assign an item - we have space + dirStack->At(dirStackCount) = newFileName; + dirStackCount++; + searchNow = FALSE; + } + else + { + // we must allocate a new item in the array + dirStack->Add(newFileName); + if (dirStack->IsGood()) + { + dirStackCount++; + searchNow = FALSE; + } + else + { + dirStack->ResetState(); + delete[] newFileName; + } + } + } + } + + if (searchNow) + { + // out of memory - we will not use dirStack + pathW.resize(endIndex); + pathW.append(file.cFileName); + pathW.push_back(L'\\'); + SearchDirectoryW(pathW, pathW.length(), startPathLen, masksGroup, includeSubDirs, data, + NULL, 0, duplicateCandidates, ignoreList, message); + pathW.resize(endIndex); + } + } + else + { + std::wstring fullPathW = pathWithSlashW; + fullPathW.append(file.cFileName); + std::string fullPathA = WideToAnsi(fullPathW); + FIND_LOG_ITEM log; + log.Flags = FLI_ERROR; + log.Text = LoadStr(IDS_TOOLONGNAME); + log.Path = fullPathA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + } + } + } + else // too long file-name + { + std::wstring fullPathW = pathWithSlashW; + fullPathW.append(file.cFileName); + std::string fullPathA = WideToAnsi(fullPathW); + FIND_LOG_ITEM log; + log.Flags = FLI_ERROR; + log.Text = LoadStr(IDS_TOOLONGNAME); + log.Path = fullPathA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + } + if (data->StopSearch) + { + testFindNextErr = FALSE; + break; + } + } while (SalLPFindNextFile(find, &file)); + DWORD err = GetLastError(); + HANDLES(FindClose(find)); + + if (testFindNextErr && err != ERROR_NO_MORE_FILES) + { + sprintf(message, LoadStr(IDS_DIRERRORFORMAT), GetErrorText(err)); + FIND_LOG_ITEM log; + log.Flags = FLI_ERROR; + log.Text = message; + log.Path = displayDirA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + } + + // search through directories + if (dirStack != NULL) + { + int i; + for (i = dirStackEnterCount; i < dirStackCount; i++) + { + wchar_t* newFileName = (wchar_t*)dirStack->At(i); + if (!data->StopSearch) // may be set during SearchDirectoryW + { + pathW.resize(endIndex); + pathW.append(newFileName); + pathW.push_back(L'\\'); + SearchDirectoryW(pathW, pathW.length(), startPathLen, masksGroup, includeSubDirs, data, + dirStack, dirStackCount, duplicateCandidates, ignoreList, message); + pathW.resize(endIndex); + } + } + // and release data from this level + for (i = dirStackEnterCount; i < dirStackCount; i++) + delete[] dirStack->At(i); + } + } + else + { + DWORD err = GetLastError(); + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_NO_MORE_FILES) + { + std::wstring displayDirW = GetDirectoryWithoutSearchBackslashW(pathW, endIndex); + std::string displayDirA = WideToAnsi(displayDirW); + sprintf(message, LoadStr(IDS_DIRERRORFORMAT), GetErrorText(err)); + + FIND_LOG_ITEM log; + log.Flags = FLI_ERROR | FLI_IGNORE; + log.Text = message; + log.Path = displayDirA.c_str(); + SendMessage(data->HWindow, WM_USER_ADDLOG, (WPARAM)&log, 0); + } + } + pathW.resize(endIndex); +} + void RefineData(CMaskGroup* masksGroup, CGrepData* data) { int refineCount = data->FoundFilesListView->GetDataForRefineCount(); @@ -1821,10 +2124,18 @@ void RefineData(CMaskGroup* masksGroup, CGrepData* data) if (fullPath[strlen(fullPath) - 1] != '\\') strcat(fullPath, "\\"); strcat(fullPath, refineData->Name.c_str()); + std::wstring fullPathW = !refineData->PathW.empty() + ? refineData->PathW + : AnsiToWide(refineData->Path.c_str()); + if (!fullPathW.empty() && fullPathW[fullPathW.length() - 1] != L'\\' && fullPathW[fullPathW.length() - 1] != L'/') + fullPathW.push_back(L'\\'); + fullPathW += !refineData->NameW.empty() + ? refineData->NameW + : AnsiToWide(refineData->Name.c_str()); // links: refineData->Size == 0, the file size must be additionally obtained via SalGetFileSize() BOOL isLink = (refineData->Attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0; // size == 0, the file size must be obtained via SalGetFileSize() - ok = TestFileContent(refineData->Size.LoDWord, refineData->Size.HiDWord, - fullPath, data, isLink); + ok = TestFileContentW(refineData->Size.LoDWord, refineData->Size.HiDWord, + fullPathW.c_str(), fullPath, data, isLink); } } @@ -1833,10 +2144,11 @@ void RefineData(CMaskGroup* masksGroup, CGrepData* data) if (data->Refine == 1 && ok || data->Refine == 2 && !ok) { - AddFoundItem(refineData->Path.c_str(), refineData->Name.c_str(), - refineData->Size.LoDWord, refineData->Size.HiDWord, - refineData->Attr, &refineData->LastWrite, - refineData->IsDir, data, NULL); + AddFoundItemW(refineData->Path.c_str(), refineData->Name.c_str(), + refineData->PathW.c_str(), refineData->NameW.c_str(), + refineData->Size.LoDWord, refineData->Size.HiDWord, + refineData->Attr, &refineData->LastWrite, + refineData->IsDir, data, NULL); } } } @@ -1852,7 +2164,6 @@ unsigned GrepThreadFBody(void* ptr) data->NeedRefresh = FALSE; data->Criteria.PrepareForTest(); CPathBuffer path; // Heap-allocated for long path support - char* end; if (data->Refine != 0) { if (data->Data->Count > 0) @@ -1864,7 +2175,6 @@ unsigned GrepThreadFBody(void* ptr) lstrcpyn(path + len, "\\", path.Size() - len); len++; } - end = path + len; CMaskGroup* mg = &data->Data->At(0)->MasksGroup; int errorPos; if (mg->PrepareMasks(errorPos)) @@ -1900,15 +2210,22 @@ unsigned GrepThreadFBody(void* ptr) int i; for (i = 0; i < data->Data->Count; i++) { - lstrcpyn(path, data->Data->At(i)->Dir, path.Size()); - int len = (int)strlen(path); - if (path[len - 1] != '\\') + CSearchForData* searchData = data->Data->At(i); + std::wstring pathW = !searchData->DirW.empty() + ? searchData->DirW + : AnsiToWide(searchData->Dir); + if (!pathW.empty() && pathW[pathW.length() - 1] != L'\\' && pathW[pathW.length() - 1] != L'/') + pathW.push_back(L'\\'); + + CPathBuffer pathA; // legacy mirror for ignore-list prefix lengths and status/log fallbacks + lstrcpyn(pathA, searchData->Dir, pathA.Size()); + int len = (int)strlen(pathA); + if (len > 0 && pathA[len - 1] != '\\' && pathA[len - 1] != '/') { - lstrcpyn(path + len, "\\", path.Size() - len); + lstrcpyn(pathA + len, "\\", pathA.Size() - len); len++; } - end = path + len; - CMaskGroup* mg = &data->Data->At(i)->MasksGroup; + CMaskGroup* mg = &searchData->MasksGroup; int errorPos; if (!mg->PrepareMasks(errorPos)) { @@ -1917,11 +2234,11 @@ unsigned GrepThreadFBody(void* ptr) break; } - BOOL includeSubDirs = data->Data->At(i)->IncludeSubDirs; - TDirectArray* dirStack = NULL; // see description at SearchDirectory + BOOL includeSubDirs = searchData->IncludeSubDirs; + TDirectArray* dirStack = NULL; // see description at SearchDirectoryW if (includeSubDirs) { - dirStack = new TDirectArray(1000, 1000); + dirStack = new TDirectArray(1000, 1000); if (dirStack == NULL) TRACE_E(LOW_MEMORY); // the algorithm will run even without the stack } @@ -1941,8 +2258,8 @@ unsigned GrepThreadFBody(void* ptr) } CPathBuffer message; // Heap-allocated for long path support - SearchDirectory(path, end, (int)(end - path), mg, includeSubDirs, data, dirStack, 0, - duplicateCandidates, ignoreList, message); + SearchDirectoryW(pathW, pathW.length(), len, mg, includeSubDirs, data, dirStack, 0, + duplicateCandidates, ignoreList, message); if (ignoreList != NULL) delete ignoreList; @@ -2172,13 +2489,13 @@ DWORD WINAPI ThreadFindDialogMessageLoop(void* param) return ThreadFindDialogMessageLoopEH(param); } -BOOL OpenFindDialog(HWND hCenterAgainst, const char* initPath) +BOOL OpenFindDialog(HWND hCenterAgainst, const char* initPath, const wchar_t* initPathW) { CALL_STACK_MESSAGE3("OpenFindDialog(0x%p, %s)", hCenterAgainst, initPath); HCURSOR hOldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); - CFindDialog* findDlg = new CFindDialog(hCenterAgainst, initPath); + CFindDialog* findDlg = new CFindDialog(hCenterAgainst, initPath, initPathW); if (findDlg != NULL && findDlg->IsGood()) { CTFDData data; diff --git a/src/find.h b/src/find.h index fff6f45c1..d0ba01a0f 100644 --- a/src/find.h +++ b/src/find.h @@ -5,6 +5,8 @@ #pragma once #include "common/widepath.h" +#include "common/find/FindDialogSeed.h" +#include "ui/UnicodeNameInputController.h" // structure for adding messages to the Find Log; sent as the message parameter // of WM_USER_ADDLOG; parameters will be copied into the log data (can be deallocated after returning) @@ -74,6 +76,7 @@ class CMenuBar; struct CSearchForData { CPathBuffer Dir; + std::wstring DirW; CMaskGroup MasksGroup; BOOL IncludeSubDirs; @@ -82,7 +85,13 @@ struct CSearchForData Set(dir, masksGroup, includeSubDirs); } + CSearchForData(const char* dir, const wchar_t* dirW, const char* masksGroup, BOOL includeSubDirs) + { + Set(dir, dirW, masksGroup, includeSubDirs); + } + void Set(const char* dir, const char* masksGroup, BOOL includeSubDirs); + void Set(const char* dir, const wchar_t* dirW, const char* masksGroup, BOOL includeSubDirs); const char* GetText(int i) { switch (i) @@ -499,6 +508,8 @@ struct CFoundFilesData { std::string Name; std::string Path; + std::wstring NameW; + std::wstring PathW; CQuadWord Size; DWORD Attr; FILETIME LastWrite; @@ -529,11 +540,17 @@ struct CFoundFilesData ~CFoundFilesData() = default; BOOL Set(const char* path, const char* name, const CQuadWord& size, DWORD attr, const FILETIME* lastWrite, BOOL isDir); + BOOL Set(const char* path, const char* name, const wchar_t* pathW, const wchar_t* nameW, + const CQuadWord& size, DWORD attr, const FILETIME* lastWrite, BOOL isDir); // if 'i' refers to Name or Path, returns a pointer to the corresponding variable // otherwise fills the buffer 'text' (must be at least 50 characters long) with the appropriate value // and returns a pointer to 'text' // 'fileNameFormat' determines formatting of names of found items char* GetText(int i, char* text, int fileNameFormat); + std::wstring GetTextW(int i, int fileNameFormat) const; + std::wstring GetNameTextW(int fileNameFormat) const; + std::wstring GetFullNameW() const; + std::wstring GetFullNameTextW(int fileNameFormat) const; }; class CFoundFilesListView : public CWindow @@ -672,6 +689,24 @@ class CButton; class CFindDialog : public CCommonDialog { protected: + // Wide cache for the "Look in" edit field. Authoritative ONLY while + // LookInUnicodeInput.IsEnabled() is TRUE. While the Unicode edit control + // is active, every write to the wide control mirrors into this cache (see + // Transfer ttDataFromWindow, Validate, browse, insert-drives). While the + // Unicode control is disabled, the cache must stay empty so downstream + // readers fall through to AnsiToWide(Data.LookInText) — the live ANSI + // combo is the source of truth in that mode. See InitialLookInSeed for + // the constructor-time bootstrap that decides which mode the dialog runs + // in, and sally::find::ShouldOverrideEditWithWide for the decision. + std::wstring LookInTextW; + // Seed captured at construction time from the opening panel's + // (GetPath(), GetPathW()) pair. Consumed once by the deferred + // WM_USER_FIND_LOOKIN_W_OVERRIDE handler to decide whether to enable the + // Unicode edit control and, if so, to plant the initial wide text into + // both the control and LookInTextW. Not consulted elsewhere. + sally::find::LookInSeed InitialLookInSeed; + CUnicodeNameInputController LookInUnicodeInput; + // data needed for laying out the dialog BOOL FirstWMSize; int VMargin; // space on the left and right between the dialog frame and controls @@ -713,6 +748,7 @@ class CFindDialog : public CCommonDialog // CFindAdvancedDialog FindAdvanced; CFoundFilesListView* FoundFilesListView; CPathBuffer FoundFilesDataTextBuffer; // for obtaining text from CFoundFilesData::GetText + std::wstring FoundFilesDataTextBufferW; // for obtaining text from CFoundFilesData::GetTextW CFindTBHeader* TBHeader; BOOL SearchInProgress; BOOL CanClose; // the window can be closed (we are not inside a method of this object) @@ -748,7 +784,7 @@ class CFindDialog : public CCommonDialog CStateOfFindCloseQueryEnum StateOfFindCloseQuery; // main thread asks the Find thread whether the window can close; unsynchronized, used only during shutdown, more than enough... public: - CFindDialog(HWND hCenterAgainst, const char* initPath); + CFindDialog(HWND hCenterAgainst, const char* initPath, const wchar_t* initPathW = nullptr); ~CFindDialog(); virtual void Validate(CTransferInfo& ti); @@ -869,7 +905,11 @@ class CFindDialogQueue : public CWindowQueue // externs // -BOOL OpenFindDialog(HWND hCenterAgainst, const char* initPath); +// Open the Find dialog seeded with the panel's path. `initPathW` carries +// the wide source-of-truth; pass nullptr when only the ANSI mirror is +// available. The wide pointer lets the dialog render Unicode-only roots +// (e.g. zz中文) without CP_ACP loss in the visible "Look in" edit field. +BOOL OpenFindDialog(HWND hCenterAgainst, const char* initPath, const wchar_t* initPathW = nullptr); extern CFindOptions FindOptions; extern CFindIgnore FindIgnore; diff --git a/src/find_dialog_actions.cpp b/src/find_dialog_actions.cpp index d864b6c26..cd07ce977 100644 --- a/src/find_dialog_actions.cpp +++ b/src/find_dialog_actions.cpp @@ -16,12 +16,48 @@ #include "plugins.h" #include "fileswnd.h" #include "dialogs.h" +#include "darkmode.h" //**************************************************************************** // // CFindDialog (continued find_dialog_results.cpp) // +static void FillRectSolid(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldColor = SetDCBrushColor(hdc, color); + FillRect(hdc, rect, (HBRUSH)GetStockObject(DC_BRUSH)); + SetDCBrushColor(hdc, oldColor); + SelectObject(hdc, oldBrush); +} + +static void DrawRectOutline(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(NULL_BRUSH)); + COLORREF oldColor = SetDCPenColor(hdc, color); + Rectangle(hdc, rect->left, rect->top, rect->right, rect->bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static void ApplyFindTBHeaderFrameStyle(HWND hwnd) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + LONG_PTR exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE); + LONG_PTR newExStyle = DarkMode_ShouldUseDark() ? (exStyle & ~WS_EX_STATICEDGE) : (exStyle | WS_EX_STATICEDGE); + if (newExStyle != exStyle) + { + SetWindowLongPtr(hwnd, GWL_EXSTYLE, newExStyle); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } +} + void CFindDialog::OnHideSelection() { HWND hListView = FoundFilesListView->HWindow; @@ -161,7 +197,9 @@ void CFindDialog::OnDelete(BOOL toRecycle) if (lastFocusedIndex != -1) { CFoundFilesData* lastItem = FoundFilesListView->At(lastFocusedIndex); - lastFocusedItem.Set(lastItem->Path.c_str(), lastItem->Name.c_str(), lastItem->Size, lastItem->Attr, &lastItem->LastWrite, lastItem->IsDir); + lastFocusedItem.Set(lastItem->Path.c_str(), lastItem->Name.c_str(), + lastItem->PathW.c_str(), lastItem->NameW.c_str(), + lastItem->Size, lastItem->Attr, &lastItem->LastWrite, lastItem->IsDir); } CShellExecuteWnd shellExecuteWnd; @@ -280,6 +318,8 @@ void CFindDialog::OnColorsChange() MenuBarHeight = MenuBar->GetNeededHeight(); LayoutControls(); } + DarkMode_ApplyListTreeThemeRecursive(HWindow); + RedrawWindow(HWindow, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); } //**************************************************************************** @@ -304,6 +344,7 @@ CFindTBHeader::CFindTBHeader(HWND hDlg, int ctrlID) DWORD exStyle = (DWORD)GetWindowLongPtr(HWindow, GWL_EXSTYLE); exStyle |= WS_EX_STATICEDGE; SetWindowLongPtr(HWindow, GWL_EXSTYLE, exStyle); + ApplyFindTBHeaderFrameStyle(HWindow); LogToolBar = NULL; HWarningIcon = NULL; @@ -400,12 +441,16 @@ BOOL CFindTBHeader::CreateLogToolbar(BOOL errors, BOOL infos) void CFindTBHeader::OnColorsChange() { + ApplyFindTBHeaderFrameStyle(HWindow); if (ToolBar != NULL) { ToolBar->SetImageList(HGrayToolBarImageList); ToolBar->SetHotImageList(HHotToolBarImageList); ToolBar->OnColorsChanged(); } + if (LogToolBar != NULL) + LogToolBar->OnColorsChanged(); + RedrawWindow(HWindow, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); } int CFindTBHeader::GetNeededHeight() @@ -529,7 +574,16 @@ CFindTBHeader::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) HFONT hOldFont = (HFONT)SelectObject(hdc, (HFONT)SendMessage(HWindow, WM_GETFONT, 0, 0)); int oldBkMode = SetBkMode(hdc, TRANSPARENT); - FillRect(hdc, &r, (HBRUSH)(COLOR_3DFACE + 1)); + DarkModeColors colors; + BOOL useDark = DarkMode_GetColors(&colors); + if (useDark) + { + FillRectSolid(hdc, &r, colors.DialogBackground); + DrawRectOutline(hdc, &r, colors.Border); + SetTextColor(hdc, colors.DialogText); + } + else + FillRect(hdc, &r, (HBRUSH)(COLOR_3DFACE + 1)); DrawText(hdc, Text, -1, &tr, DT_SINGLELINE | DT_RIGHT | DT_VCENTER); SetBkMode(hdc, oldBkMode); SelectObject(hdc, hOldFont); @@ -599,6 +653,14 @@ CFindTBHeader::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) break; } + case WM_SETTINGCHANGE: + case WM_THEMECHANGED: + case WM_SYSCOLORCHANGE: + { + OnColorsChange(); + break; + } + case WM_COMMAND: { if ((HWND)lParam == ToolBar->HWindow) @@ -1357,7 +1419,9 @@ void CFindDialog::OnDrag(BOOL rightMouseButton) if (lastFocusedIndex != -1) { CFoundFilesData* lastItem = FoundFilesListView->At(lastFocusedIndex); - lastFocusedItem.Set(lastItem->Path.c_str(), lastItem->Name.c_str(), lastItem->Size, lastItem->Attr, &lastItem->LastWrite, lastItem->IsDir); + lastFocusedItem.Set(lastItem->Path.c_str(), lastItem->Name.c_str(), + lastItem->PathW.c_str(), lastItem->NameW.c_str(), + lastItem->Size, lastItem->Attr, &lastItem->LastWrite, lastItem->IsDir); } CMyEnumFileNamesData data; diff --git a/src/find_dialog_results.cpp b/src/find_dialog_results.cpp index 694038343..90bff6dd7 100644 --- a/src/find_dialog_results.cpp +++ b/src/find_dialog_results.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Open Salamander Authors +// SPDX-FileCopyrightText: 2023 Open Salamander Authors // SPDX-FileCopyrightText: 2026 Sally Authors // SPDX-License-Identifier: GPL-2.0-or-later @@ -6,7 +6,9 @@ #include "menu.h" #include "ui/IPrompter.h" +#include "common/unicode/ComboSyncPolicy.h" #include "common/unicode/helpers.h" +#include "common/find/FindDialogSeed.h" #include "cfgdlg.h" #include "mainwnd.h" #include "plugins.h" @@ -21,6 +23,7 @@ #include "darkmode.h" #include +#include const char* MINIMIZED_FINDING_CAPTION = "(%d) %s [%s %s]"; const char* NORMAL_FINDING_CAPTION = "%s [%s %s]"; @@ -28,6 +31,933 @@ const char* NORMAL_FINDING_CAPTION = "%s [%s %s]"; BOOL FindManageInUse = FALSE; BOOL FindIgnoreInUse = FALSE; +static const UINT_PTR FIND_COMBO_SKIN_SUBCLASS_ID = 1; +static const UINT_PTR FIND_COMBO_EDIT_SKIN_SUBCLASS_ID = 2; +static const UINT_PTR FIND_ADVANCED_TEXT_SKIN_SUBCLASS_ID = 3; +static const UINT_PTR FIND_STATUS_SKIN_SUBCLASS_ID = 1; +static const UINT WM_USER_FIND_DELAYED_THEME = WM_APP + 413; +// Replace the "Look in" combo with a Unicode combo after the framework's +// TransferData(ttDataToWindow) runs. The legacy Transfer path is ANSI and may +// already have populated the combo with lossy text such as "zz??"; by posting +// this work we can copy that history and then make the wide seed visible. +static const UINT WM_USER_FIND_LOOKIN_W_OVERRIDE = WM_APP + 414; +static const COLORREF FIND_DARK_LINE = RGB(55, 55, 58); +static const COLORREF FIND_DARK_FRAME = RGB(62, 62, 66); +static const COLORREF FIND_DARK_BUTTON = RGB(52, 52, 56); +static const COLORREF FIND_DARK_SECTION_LINE = RGB(82, 82, 86); + +struct CFindComboSkinState +{ + LONG_PTR ComboStyle; + LONG_PTR ComboExStyle; + LONG_PTR EditStyle; + LONG_PTR EditExStyle; + HWND HEdit; + BOOL EditStyleKnown; + BOOL EditExStyleKnown; + BOOL ApplyingTheme; + BOOL ThemeKnown; + BOOL LastUseDark; +}; + +struct CFindAdvancedTextSkinState +{ + LONG_PTR Style; + LONG_PTR ExStyle; +}; + +static LRESULT CALLBACK FindComboSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); +static LRESULT CALLBACK FindComboEditSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); +static LRESULT CALLBACK FindAdvancedTextSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); +static LRESULT CALLBACK FindStatusSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); +static void ApplyFindComboEditSkin(HWND hEdit, BOOL useDark); + +static void CopyWideLookInToAnsiMirror(const std::wstring& textW, CPathBuffer& textA) +{ + int bufferSize = (int)textA.Size(); + if (bufferSize <= 0) + return; + + BOOL usedDefaultChar = FALSE; + int copied = WideCharToMultiByte(CP_ACP, 0, textW.c_str(), -1, + textA.Get(), bufferSize, "?", &usedDefaultChar); + if (copied <= 0) + textA[0] = 0; + textA[bufferSize - 1] = 0; +} + +static BOOL IsFindLookInFocus(HWND hFocus, HWND hCombo) +{ + if (hFocus == NULL || hCombo == NULL) + return FALSE; + if (hFocus == hCombo) + return TRUE; + + COMBOBOXINFO cbi = {0}; + cbi.cbSize = sizeof(cbi); + return GetComboBoxInfo(hCombo, &cbi) && hFocus == cbi.hwndItem; +} + +static void EscapeFindLookInPathSeparatorsW(std::wstring& text) +{ + for (size_t pos = 0; pos < text.length(); pos++) + { + if (text[pos] == L';') + { + text.insert(pos, 1, L';'); + pos++; + } + } +} + +static void ReplaceFindLookInSelectionW(CUnicodeNameInputController& input, + const std::wstring& replacement, + DWORD start, DWORD end) +{ + std::wstring text = input.GetText(); + size_t startPos = start; + size_t endPos = end; + if (startPos > text.length()) + startPos = text.length(); + if (endPos > text.length()) + endPos = text.length(); + if (endPos < startPos) + endPos = startPos; + + std::wstring insert = replacement; + EscapeFindLookInPathSeparatorsW(insert); + + if (startPos > 0) + { + size_t leftIndex = startPos - 1; + size_t scan = leftIndex; + while (scan < text.length() && text[scan] == L';') + { + if (scan == 0) + { + scan = text.length(); + break; + } + scan--; + } + size_t semicolonCount = (scan == text.length()) ? leftIndex + 1 : leftIndex - scan; + if ((semicolonCount & 1) == 0) + insert.insert(0, L"; "); + } + if (endPos < text.length() && + (text[endPos] != L';' || endPos + 1 < text.length() && text[endPos + 1] == L';')) + { + insert.append(L"; "); + } + + text.replace(startPos, endPos - startPos, insert); + input.SetText(text); + + DWORD caret = (DWORD)(startPos + insert.length()); + SendMessage(input.GetControlHandle(), CB_SETEDITSEL, 0, MAKELPARAM(caret, caret)); + SetFocus(input.GetControlHandle()); +} + +static int CompareFindTextW(const std::wstring& left, const std::wstring& right) +{ + if (Configuration.SortDetectNumbers) + return StrCmpLogicalW(left.c_str(), right.c_str()); + if (Configuration.SortUsesLocale) + { + int ret = CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + left.c_str(), -1, right.c_str(), -1); + if (ret != 0) + return ret - CSTR_EQUAL; + } + return _wcsicmp(left.c_str(), right.c_str()); +} + +static BOOL FindTextMatchesW(const std::wstring& text, const wchar_t* pattern, BOOL partial) +{ + if (pattern == NULL) + return FALSE; + + if (!partial) + return CompareFindTextW(text, pattern) == 0; + + size_t patternLen = wcslen(pattern); + if (patternLen == 0) + return TRUE; + if (text.length() < patternLen) + return FALSE; + + int ret = CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + text.c_str(), (int)patternLen, pattern, (int)patternLen); + if (ret != 0) + return ret == CSTR_EQUAL; + return _wcsnicmp(text.c_str(), pattern, patternLen) == 0; +} + +static int FindListItemByNameW(CFoundFilesListView* listView, int startIndex, UINT flags, const wchar_t* pattern) +{ + int ret = -1; + if ((flags & LVFI_STRING) == 0 && (flags & LVFI_PARTIAL) == 0) + return ret; + + // The control normally sends LVFI_STRING only, so keep the historical + // prefix-search behavior while comparing against the wide item name. + BOOL partial = TRUE; + int count = listView->GetCount(); + if (startIndex < 0) + startIndex = 0; + if (startIndex > count) + startIndex = count; + int i; + for (i = startIndex; i < count; i++) + { + const CFoundFilesData* item = listView->At(i); + if (FindTextMatchesW(item->NameW, pattern, partial)) + return i; + } + + if (flags & LVFI_WRAP) + { + for (i = 0; i < startIndex; i++) + { + const CFoundFilesData* item = listView->At(i); + if (FindTextMatchesW(item->NameW, pattern, partial)) + return i; + } + } + return ret; +} + +static void RedrawFindResultItem(HWND hListView, int itemIndex) +{ + if (hListView == NULL || itemIndex < 0 || itemIndex >= ListView_GetItemCount(hListView)) + return; + + ListView_RedrawItems(hListView, itemIndex, itemIndex); + + RECT itemRect; + if (ListView_GetItemRect(hListView, itemIndex, &itemRect, LVIR_BOUNDS)) + InvalidateRect(hListView, &itemRect, TRUE); +} + +static void FindFillRectSolid(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldColor = SetDCBrushColor(hdc, color); + FillRect(hdc, rect, (HBRUSH)GetStockObject(DC_BRUSH)); + SetDCBrushColor(hdc, oldColor); + SelectObject(hdc, oldBrush); +} + +static void FindDrawRectOutline(HDC hdc, const RECT* rect, COLORREF color) +{ + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(NULL_BRUSH)); + COLORREF oldColor = SetDCPenColor(hdc, color); + Rectangle(hdc, rect->left, rect->top, rect->right, rect->bottom); + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static void FindDrawComboArrow(HDC hdc, const RECT* rect, COLORREF color) +{ + int centerX = (rect->left + rect->right) / 2; + int centerY = (rect->top + rect->bottom) / 2; + POINT arrow[3] = { + {centerX - 3, centerY - 1}, + {centerX + 4, centerY - 1}, + {centerX, centerY + 3}, + }; + + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + HGDIOBJ oldBrush = SelectObject(hdc, GetStockObject(DC_BRUSH)); + COLORREF oldPenColor = SetDCPenColor(hdc, color); + COLORREF oldBrushColor = SetDCBrushColor(hdc, color); + Polygon(hdc, arrow, 3); + SetDCBrushColor(hdc, oldBrushColor); + SetDCPenColor(hdc, oldPenColor); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); +} + +static BOOL GetChildRectInParent(HWND hParent, HWND hChild, RECT* rect) +{ + if (hParent == NULL || hChild == NULL || rect == NULL || !IsWindow(hParent) || !IsWindow(hChild)) + return FALSE; + + if (!GetWindowRect(hChild, rect)) + return FALSE; + MapWindowPoints(NULL, hParent, (POINT*)rect, 2); + return TRUE; +} + +static BOOL GetFindChildRectInDialog(HWND hDialog, int ctrlID, RECT* rect) +{ + HWND hChild = GetDlgItem(hDialog, ctrlID); + return GetChildRectInParent(hDialog, hChild, rect); +} + +static void SetFindWindowExStyle(HWND hwnd, LONG_PTR exStyle) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + if (GetWindowLongPtr(hwnd, GWL_EXSTYLE) == exStyle) + return; + + SetWindowLongPtr(hwnd, GWL_EXSTYLE, exStyle); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); +} + +static void SetFindWindowStyle(HWND hwnd, LONG_PTR style) +{ + if (hwnd == NULL || !IsWindow(hwnd)) + return; + + if (GetWindowLongPtr(hwnd, GWL_STYLE) == style) + return; + + SetWindowLongPtr(hwnd, GWL_STYLE, style); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); +} + +static void UpdateFindComboSkin(HWND hCombo, CFindComboSkinState* state) +{ + if (hCombo == NULL || state == NULL || !IsWindow(hCombo)) + return; + + COMBOBOXINFO cbi = {0}; + cbi.cbSize = sizeof(cbi); + GetComboBoxInfo(hCombo, &cbi); + + BOOL editChanged = cbi.hwndItem != NULL && cbi.hwndItem != state->HEdit; + if (editChanged) + { + state->HEdit = cbi.hwndItem; + state->EditStyle = GetWindowLongPtr(cbi.hwndItem, GWL_STYLE); + state->EditExStyle = GetWindowLongPtr(cbi.hwndItem, GWL_EXSTYLE); + state->EditStyleKnown = TRUE; + state->EditExStyleKnown = TRUE; + } + + DarkModeColors colors; + BOOL useDark = DarkMode_GetColors(&colors); + LONG_PTR darkStyleMask = WS_BORDER; + LONG_PTR darkEdgeMask = WS_EX_CLIENTEDGE | WS_EX_STATICEDGE; + SetFindWindowStyle(hCombo, useDark ? (state->ComboStyle & ~darkStyleMask) : state->ComboStyle); + SetFindWindowExStyle(hCombo, useDark ? (state->ComboExStyle & ~darkEdgeMask) : state->ComboExStyle); + + if (state->HEdit != NULL && state->EditStyleKnown && IsWindow(state->HEdit)) + SetFindWindowStyle(state->HEdit, useDark ? (state->EditStyle & ~darkStyleMask) : state->EditStyle); + if (state->HEdit != NULL && state->EditExStyleKnown && IsWindow(state->HEdit)) + SetFindWindowExStyle(state->HEdit, useDark ? (state->EditExStyle & ~darkEdgeMask) : state->EditExStyle); + + if (!state->ApplyingTheme && + (!state->ThemeKnown || state->LastUseDark != useDark || editChanged)) + { + state->ApplyingTheme = TRUE; + SetWindowTheme(hCombo, useDark ? L"" : NULL, NULL); + ApplyFindComboEditSkin(state->HEdit, useDark); + if (cbi.hwndList != NULL && IsWindow(cbi.hwndList)) + SetWindowTheme(cbi.hwndList, useDark ? L"DarkMode_Explorer" : NULL, NULL); + state->ThemeKnown = TRUE; + state->LastUseDark = useDark; + state->ApplyingTheme = FALSE; + } + + RedrawWindow(hCombo, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); +} + +static void ApplyFindComboSkin(HWND hCombo) +{ + if (hCombo == NULL || !IsWindow(hCombo)) + return; + + DWORD_PTR data = 0; + CFindComboSkinState* state = NULL; + if (GetWindowSubclass(hCombo, FindComboSkinSubclassProc, FIND_COMBO_SKIN_SUBCLASS_ID, &data)) + state = (CFindComboSkinState*)data; + else + { + state = new CFindComboSkinState; + if (state == NULL) + return; + state->ComboStyle = GetWindowLongPtr(hCombo, GWL_STYLE); + state->ComboExStyle = GetWindowLongPtr(hCombo, GWL_EXSTYLE); + state->EditStyle = 0; + state->EditExStyle = 0; + state->HEdit = NULL; + state->EditStyleKnown = FALSE; + state->EditExStyleKnown = FALSE; + state->ApplyingTheme = FALSE; + state->ThemeKnown = FALSE; + state->LastUseDark = FALSE; + if (!SetWindowSubclass(hCombo, FindComboSkinSubclassProc, FIND_COMBO_SKIN_SUBCLASS_ID, (DWORD_PTR)state)) + { + delete state; + return; + } + } + + UpdateFindComboSkin(hCombo, state); +} + +static void ApplyFindComboSkins(HWND hDialog) +{ + int comboIDs[] = {IDC_FIND_NAMED, IDC_FIND_LOOKIN, IDC_FIND_CONTAINING}; + for (int i = 0; i < _countof(comboIDs); i++) + ApplyFindComboSkin(GetDlgItem(hDialog, comboIDs[i])); +} + +static void ApplyFindSeparatorLines(HWND hDialog) +{ + int lineIDs[] = {IDC_FIND_LINE1, IDC_FIND_LINE2}; + BOOL useDark = DarkMode_ShouldUseDark(); + for (int i = 0; i < _countof(lineIDs); i++) + { + HWND hLine = GetDlgItem(hDialog, lineIDs[i]); + if (hLine != NULL && IsWindow(hLine)) + ShowWindow(hLine, useDark ? SW_HIDE : SW_SHOWNA); + } +} + +static BOOL PaintFindDialogSeparatorLines(HWND hDialog, HDC paintDC) +{ + if (!DarkMode_ShouldUseDark()) + return FALSE; + + HDC hdc = paintDC; + if (hdc == NULL) + hdc = GetDC(hDialog); + if (hdc == NULL) + return FALSE; + + int lineIDs[] = {IDC_FIND_LINE1, IDC_FIND_LINE2}; + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FIND_DARK_SECTION_LINE); + for (int i = 0; i < _countof(lineIDs); i++) + { + RECT rect; + if (!GetFindChildRectInDialog(hDialog, lineIDs[i], &rect)) + continue; + int y = max(rect.top, min(rect.bottom - 1, (rect.top + rect.bottom) / 2)); + MoveToEx(hdc, rect.left, y, NULL); + LineTo(hdc, rect.right, y); + } + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); + + if (paintDC == NULL) + ReleaseDC(hDialog, hdc); + return TRUE; +} + +static BOOL PaintFindDarkCombo(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + if (client.right <= client.left || client.bottom <= client.top) + return TRUE; + + COMBOBOXINFO cbi = {0}; + cbi.cbSize = sizeof(cbi); + GetComboBoxInfo(hwnd, &cbi); + + RECT editRect; + BOOL haveEditRect = GetChildRectInParent(hwnd, cbi.hwndItem, &editRect); + + int savedDC = SaveDC(paintDC); + if (haveEditRect) + ExcludeClipRect(paintDC, editRect.left, editRect.top, editRect.right, editRect.bottom); + + FindFillRectSolid(paintDC, &client, colors.InputBackground); + + int buttonWidth = max(GetSystemMetrics(SM_CXVSCROLL), client.bottom - client.top); + RECT button = client; + button.left = max(client.left + 1, client.right - buttonWidth - 1); + button.top = client.top + 1; + button.right = client.right - 1; + button.bottom = client.bottom - 1; + if (button.right > button.left && button.bottom > button.top) + { + FindFillRectSolid(paintDC, &button, FIND_DARK_BUTTON); + HGDIOBJ oldPen = SelectObject(paintDC, GetStockObject(DC_PEN)); + COLORREF oldPenColor = SetDCPenColor(paintDC, FIND_DARK_LINE); + MoveToEx(paintDC, button.left, button.top, NULL); + LineTo(paintDC, button.left, button.bottom); + SetDCPenColor(paintDC, oldPenColor); + SelectObject(paintDC, oldPen); + FindDrawComboArrow(paintDC, &button, IsWindowEnabled(hwnd) ? colors.InputText : colors.DisabledText); + } + + RestoreDC(paintDC, savedDC); + FindDrawRectOutline(paintDC, &client, FIND_DARK_FRAME); + return TRUE; +} + +static void PaintFindDarkComboFrame(HWND hwnd) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return; + + HDC hdc = GetWindowDC(hwnd); + if (hdc == NULL) + return; + + RECT rect; + GetWindowRect(hwnd, &rect); + OffsetRect(&rect, -rect.left, -rect.top); + FindDrawRectOutline(hdc, &rect, FIND_DARK_FRAME); + ReleaseDC(hwnd, hdc); +} + +static void PaintFindDarkComboEditFrame(HWND hwnd) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return; + + HDC hdc = GetWindowDC(hwnd); + if (hdc == NULL) + return; + + RECT window; + GetWindowRect(hwnd, &window); + OffsetRect(&window, -window.left, -window.top); + + RECT client; + GetClientRect(hwnd, &client); + MapWindowPoints(hwnd, NULL, (POINT*)&client, 2); + RECT screenWindow; + GetWindowRect(hwnd, &screenWindow); + OffsetRect(&client, -screenWindow.left, -screenWindow.top); + + int savedDC = SaveDC(hdc); + ExcludeClipRect(hdc, client.left, client.top, client.right, client.bottom); + FindFillRectSolid(hdc, &window, colors.InputBackground); + RestoreDC(hdc, savedDC); + + FindDrawRectOutline(hdc, &window, FIND_DARK_FRAME); + ReleaseDC(hwnd, hdc); +} + +static void ApplyFindComboEditSkin(HWND hEdit, BOOL useDark) +{ + if (hEdit == NULL || !IsWindow(hEdit)) + return; + + SetWindowSubclass(hEdit, FindComboEditSkinSubclassProc, FIND_COMBO_EDIT_SKIN_SUBCLASS_ID, 0); + SetWindowTheme(hEdit, useDark ? L"" : NULL, NULL); + RedrawWindow(hEdit, NULL, NULL, RDW_INVALIDATE | RDW_FRAME); +} + +static LRESULT CALLBACK FindComboEditSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + UNREFERENCED_PARAMETER(dwRefData); + + switch (uMsg) + { + case WM_NCPAINT: + { + if (DarkMode_ShouldUseDark()) + { + PaintFindDarkComboEditFrame(hwnd); + return 0; + } + break; + } + + case WM_PAINT: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + if (DarkMode_ShouldUseDark()) + PaintFindDarkComboEditFrame(hwnd); + return ret; + } + + case WM_ERASEBKGND: + { + DarkModeColors colors; + if (DarkMode_GetColors(&colors)) + { + RECT client; + GetClientRect(hwnd, &client); + FindFillRectSolid((HDC)wParam, &client, colors.InputBackground); + return TRUE; + } + break; + } + + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + case WM_ENABLE: + case WM_SIZE: + case WM_SETFOCUS: + case WM_KILLFOCUS: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, FindComboEditSkinSubclassProc, uIdSubclass); + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static LRESULT CALLBACK FindComboSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + CFindComboSkinState* state = (CFindComboSkinState*)dwRefData; + + switch (uMsg) + { + case WM_NCPAINT: + { + if (DarkMode_ShouldUseDark()) + { + PaintFindDarkComboFrame(hwnd); + return 0; + } + break; + } + + case WM_PAINT: + { + if (DarkMode_ShouldUseDark()) + { + PAINTSTRUCT ps; + HDC hdc = HANDLES(BeginPaint(hwnd, &ps)); + if (hdc != NULL) + PaintFindDarkCombo(hwnd, hdc); + HANDLES(EndPaint(hwnd, &ps)); + return 0; + } + break; + } + + case WM_PRINTCLIENT: + { + if (DarkMode_ShouldUseDark() && PaintFindDarkCombo(hwnd, (HDC)wParam)) + return 0; + break; + } + + case WM_ERASEBKGND: + { + if (DarkMode_ShouldUseDark()) + return TRUE; + break; + } + + case WM_CTLCOLOREDIT: + case WM_CTLCOLORLISTBOX: + { + HBRUSH hBrush = DarkMode_GetDialogCtlColorBrush(uMsg, (HDC)wParam, (HWND)lParam); + if (hBrush != NULL) + return (LRESULT)hBrush; + break; + } + + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + case WM_ENABLE: + case WM_SIZE: + case WM_SETFOCUS: + case WM_KILLFOCUS: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + if (state != NULL && !state->ApplyingTheme) + UpdateFindComboSkin(hwnd, state); + else + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, FindComboSkinSubclassProc, uIdSubclass); + delete state; + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static LRESULT CALLBACK FindAdvancedTextSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + CFindAdvancedTextSkinState* state = (CFindAdvancedTextSkinState*)dwRefData; + + switch (uMsg) + { + case WM_NCPAINT: + { + if (DarkMode_ShouldUseDark()) + { + PaintFindDarkComboEditFrame(hwnd); + return 0; + } + break; + } + + case WM_PAINT: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + if (DarkMode_ShouldUseDark()) + PaintFindDarkComboEditFrame(hwnd); + return ret; + } + + case WM_ERASEBKGND: + { + DarkModeColors colors; + if (DarkMode_GetColors(&colors)) + { + RECT client; + GetClientRect(hwnd, &client); + FindFillRectSolid((HDC)wParam, &client, colors.InputBackground); + return TRUE; + } + break; + } + + case WM_ENABLE: + case WM_SETTEXT: + case WM_SIZE: + case WM_SETFOCUS: + case WM_KILLFOCUS: + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, FindAdvancedTextSkinSubclassProc, uIdSubclass); + delete state; + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static void ApplyFindAdvancedTextSkin(HWND hDialog) +{ + HWND hEdit = GetDlgItem(hDialog, IDC_FIND_ADVANCED_TEXT); + if (hEdit == NULL || !IsWindow(hEdit)) + return; + + DWORD_PTR data = 0; + CFindAdvancedTextSkinState* state = NULL; + if (GetWindowSubclass(hEdit, FindAdvancedTextSkinSubclassProc, FIND_ADVANCED_TEXT_SKIN_SUBCLASS_ID, &data)) + state = (CFindAdvancedTextSkinState*)data; + else + { + state = new CFindAdvancedTextSkinState; + if (state == NULL) + return; + state->Style = GetWindowLongPtr(hEdit, GWL_STYLE); + state->ExStyle = GetWindowLongPtr(hEdit, GWL_EXSTYLE); + if (!SetWindowSubclass(hEdit, FindAdvancedTextSkinSubclassProc, FIND_ADVANCED_TEXT_SKIN_SUBCLASS_ID, (DWORD_PTR)state)) + { + delete state; + return; + } + } + + BOOL useDark = DarkMode_ShouldUseDark(); + SetFindWindowStyle(hEdit, useDark ? (state->Style & ~WS_BORDER) : state->Style); + SetFindWindowExStyle(hEdit, useDark ? (state->ExStyle & ~(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)) : state->ExStyle); + SetWindowTheme(hEdit, useDark ? L"" : NULL, NULL); + RedrawWindow(hEdit, NULL, NULL, RDW_INVALIDATE | RDW_FRAME); +} + +static void DrawFindStatusSizeGrip(HDC hdc, const RECT* client) +{ + HGDIOBJ oldPen = SelectObject(hdc, GetStockObject(DC_PEN)); + COLORREF oldColor = SetDCPenColor(hdc, FIND_DARK_LINE); + int right = client->right - 4; + int bottom = client->bottom - 4; + for (int i = 0; i < 3; i++) + { + int offset = i * 4; + MoveToEx(hdc, right - offset - 8, bottom, NULL); + LineTo(hdc, right, bottom - offset - 8); + } + SetDCPenColor(hdc, oldColor); + SelectObject(hdc, oldPen); +} + +static BOOL PaintFindDarkStatusBar(HWND hwnd, HDC paintDC) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return FALSE; + + PAINTSTRUCT ps; + HDC hdc = paintDC; + if (hdc == NULL) + hdc = HANDLES(BeginPaint(hwnd, &ps)); + if (hdc == NULL) + return FALSE; + + RECT client; + GetClientRect(hwnd, &client); + FindFillRectSolid(hdc, &client, colors.DialogBackground); + + HFONT hFont = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); + HFONT hOldFont = NULL; + if (hFont != NULL) + hOldFont = (HFONT)SelectObject(hdc, hFont); + + int oldBkMode = SetBkMode(hdc, TRANSPARENT); + COLORREF oldTextColor = SetTextColor(hdc, colors.DialogText); + + int partCount = (int)SendMessage(hwnd, SB_GETPARTS, 0, 0); + if (partCount <= 0) + partCount = 1; + + for (int i = 0; i < partCount; i++) + { + RECT part = client; + if (partCount > 1) + SendMessage(hwnd, SB_GETRECT, i, (LPARAM)&part); + part.left += 4; + part.right -= 4; + if (part.right <= part.left) + continue; + + char text[1024] = {0}; + DWORD textInfo = (DWORD)SendMessage(hwnd, SB_GETTEXT, i, (LPARAM)text); + if ((HIWORD(textInfo) & SBT_OWNERDRAW) != 0) + { + DRAWITEMSTRUCT di = {0}; + di.CtlType = ODT_STATIC; + di.CtlID = IDC_FIND_STATUS; + di.itemID = i; + di.itemAction = ODA_DRAWENTIRE; + di.hwndItem = hwnd; + di.hDC = hdc; + di.rcItem = part; + SendMessage(GetParent(hwnd), WM_DRAWITEM, IDC_FIND_STATUS, (LPARAM)&di); + } + else if (text[0] != 0) + { + DrawText(hdc, text, -1, &part, DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + } + + if ((GetWindowLongPtr(hwnd, GWL_STYLE) & SBARS_SIZEGRIP) != 0) + DrawFindStatusSizeGrip(hdc, &client); + + SetTextColor(hdc, oldTextColor); + SetBkMode(hdc, oldBkMode); + if (hOldFont != NULL) + SelectObject(hdc, hOldFont); + + if (paintDC == NULL) + HANDLES(EndPaint(hwnd, &ps)); + return TRUE; +} + +static void PaintFindDarkStatusFrame(HWND hwnd) +{ + DarkModeColors colors; + if (!DarkMode_GetColors(&colors)) + return; + + HDC hdc = GetWindowDC(hwnd); + if (hdc == NULL) + return; + + RECT rect; + GetWindowRect(hwnd, &rect); + OffsetRect(&rect, -rect.left, -rect.top); + FindFillRectSolid(hdc, &rect, colors.DialogBackground); + ReleaseDC(hwnd, hdc); +} + +static LRESULT CALLBACK FindStatusSkinSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + UNREFERENCED_PARAMETER(dwRefData); + + switch (uMsg) + { + case WM_PAINT: + { + if (DarkMode_ShouldUseDark() && PaintFindDarkStatusBar(hwnd, NULL)) + return 0; + break; + } + + case WM_PRINTCLIENT: + { + if (DarkMode_ShouldUseDark() && PaintFindDarkStatusBar(hwnd, (HDC)wParam)) + return 0; + break; + } + + case WM_NCPAINT: + { + if (DarkMode_ShouldUseDark()) + { + PaintFindDarkStatusFrame(hwnd); + return TRUE; + } + break; + } + + case WM_ERASEBKGND: + { + if (DarkMode_ShouldUseDark()) + return TRUE; + break; + } + + case SB_SETTEXTA: + case SB_SETTEXTW: + case SB_SETPARTS: + case WM_SIZE: + case WM_ENABLE: + case WM_THEMECHANGED: + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: + { + LRESULT ret = DefSubclassProc(hwnd, uMsg, wParam, lParam); + if (DarkMode_ShouldUseDark()) + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); + return ret; + } + + case WM_NCDESTROY: + { + RemoveWindowSubclass(hwnd, FindStatusSkinSubclassProc, uIdSubclass); + break; + } + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + static void ApplyFindResultsListColors(HWND hDialog) { HWND hListView = GetDlgItem(hDialog, IDC_FIND_RESULTS); @@ -40,13 +970,48 @@ static void ApplyFindResultsListColors(HWND hDialog) return; } + SetWindowTheme(hListView, L"Explorer", NULL); + HWND hHeader = ListView_GetHeader(hListView); + if (hHeader != NULL) + SetWindowTheme(hHeader, L"Explorer", NULL); + COLORREF bgColor = GetSysColor(COLOR_WINDOW); ListView_SetBkColor(hListView, bgColor); ListView_SetTextBkColor(hListView, bgColor); ListView_SetTextColor(hListView, GetSysColor(COLOR_WINDOWTEXT)); + if (hHeader != NULL) + InvalidateRect(hHeader, NULL, TRUE); InvalidateRect(hListView, NULL, TRUE); } +static void ApplyFindStatusBarColors(HWND hStatusBar) +{ + if (hStatusBar == NULL || !IsWindow(hStatusBar)) + return; + + DarkModeColors colors; + BOOL useDark = DarkMode_GetColors(&colors); + SetWindowSubclass(hStatusBar, FindStatusSkinSubclassProc, FIND_STATUS_SKIN_SUBCLASS_ID, 0); + SetWindowTheme(hStatusBar, useDark ? L"" : NULL, NULL); + SendMessage(hStatusBar, SB_SETBKCOLOR, 0, useDark ? colors.DialogBackground : CLR_DEFAULT); + RedrawWindow(hStatusBar, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); +} + +static void ApplyFindDialogTheme(HWND hDialog, HWND hStatusBar) +{ + if (hDialog == NULL || !IsWindow(hDialog)) + return; + + DarkMode_ApplyTitleBar(hDialog); + DarkMode_ApplyListTreeThemeRecursive(hDialog); + ApplyFindComboSkins(hDialog); + ApplyFindSeparatorLines(hDialog); + ApplyFindAdvancedTextSkin(hDialog); + ApplyFindResultsListColors(hDialog); + ApplyFindStatusBarColors(hStatusBar); + RedrawWindow(hDialog, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); +} + //**************************************************************************** // // CFindTBHeader @@ -94,11 +1059,19 @@ void CFindOptions::InitMenu(CMenuPopup* popup, BOOL enabled, int originalCount) BOOL CFoundFilesData::Set(const char* path, const char* name, const CQuadWord& size, DWORD attr, const FILETIME* lastWrite, BOOL isDir) +{ + return Set(path, name, NULL, NULL, size, attr, lastWrite, isDir); +} + +BOOL CFoundFilesData::Set(const char* path, const char* name, const wchar_t* pathW, const wchar_t* nameW, + const CQuadWord& size, DWORD attr, const FILETIME* lastWrite, BOOL isDir) { CALL_STACK_MESSAGE_NONE // CALL_STACK_MESSAGE5("CFoundFilesData::Set(%s, %s, %g, 0x%X, )", path, name, size.GetDouble(), attr); Path = path; Name = name; + PathW = pathW != NULL && pathW[0] != L'\0' ? pathW : AnsiToWide(path); + NameW = nameW != NULL && nameW[0] != L'\0' ? nameW : AnsiToWide(name); Size = size; Attr = attr; LastWrite = *lastWrite; @@ -169,6 +1142,90 @@ char* CFoundFilesData::GetText(int i, char* text, int fileNameFormat) return text; } +std::wstring CFoundFilesData::GetNameTextW(int fileNameFormat) const +{ + return AlterFileNameW(NameW.c_str(), fileNameFormat, 0, IsDir != 0); +} + +std::wstring CFoundFilesData::GetFullNameW() const +{ + std::wstring fullName = PathW; + if (!fullName.empty() && fullName[fullName.length() - 1] != L'\\') + fullName += L'\\'; + fullName += NameW; + return fullName; +} + +std::wstring CFoundFilesData::GetFullNameTextW(int fileNameFormat) const +{ + std::wstring fullName = PathW; + if (!fullName.empty() && fullName[fullName.length() - 1] != L'\\') + fullName += L'\\'; + fullName += GetNameTextW(fileNameFormat); + return fullName; +} + +std::wstring CFoundFilesData::GetTextW(int i, int fileNameFormat) const +{ + switch (i) + { + case 0: + return GetNameTextW(fileNameFormat); + + case 1: + return PathW; + + case 2: + { + if (IsDir) + return AnsiToWide(DirColumnStr.c_str()); + + char number[50]; + NumberToStr(number, Size); + return AnsiToWide(number); + } + + case 3: + { + wchar_t buffer[100]; + SYSTEMTIME st; + FILETIME ft; + if (FileTimeToLocalFileTime(&LastWrite, &ft) && + FileTimeToSystemTime(&ft, &st)) + { + if (GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, NULL, buffer, _countof(buffer)) == 0) + swprintf_s(buffer, L"%u.%u.%u", st.wDay, st.wMonth, st.wYear); + } + else + wcscpy_s(buffer, LoadStrW(IDS_INVALID_DATEORTIME)); + return buffer; + } + + case 4: + { + wchar_t buffer[100]; + SYSTEMTIME st; + FILETIME ft; + if (FileTimeToLocalFileTime(&LastWrite, &ft) && + FileTimeToSystemTime(&ft, &st)) + { + if (GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, buffer, _countof(buffer)) == 0) + swprintf_s(buffer, L"%u:%02u:%02u", st.wHour, st.wMinute, st.wSecond); + } + else + wcscpy_s(buffer, LoadStrW(IDS_INVALID_DATEORTIME)); + return buffer; + } + + default: + { + char attrs[20]; + GetAttrsString(attrs, Attr); + return AnsiToWide(attrs); + } + } +} + //**************************************************************************** // // CFoundFilesListView @@ -368,16 +1425,8 @@ void CFoundFilesListView::CheckAndRemoveSelectedItems(BOOL forceRemove, int last BOOL remove = forceRemove; if (!forceRemove) { - CPathBuffer fullPath; // Heap-allocated for long path support - int pathLen = lstrlen(ptr->Path.c_str()); - memmove(fullPath.Get(), ptr->Path.c_str(), pathLen + 1); - if (ptr->Path.c_str()[pathLen - 1] != '\\') - { - fullPath[pathLen] = '\\'; - fullPath[pathLen + 1] = '\0'; - } - lstrcat(fullPath, ptr->Name.c_str()); - remove = (GetFileAttributesW(AnsiToWide(fullPath).c_str()) == -1); + std::wstring fullPath = ptr->GetFullNameW(); + remove = (GetFileAttributesW(fullPath.c_str()) == INVALID_FILE_ATTRIBUTES); } if (remove) { @@ -404,8 +1453,8 @@ void CFoundFilesListView::CheckAndRemoveSelectedItems(BOOL forceRemove, int last { CFoundFilesData* ptr = Data[i]; if (lastFocusedItem != NULL && - !lastFocusedItem->Name.empty() && strcmp(ptr->Name.c_str(), lastFocusedItem->Name.c_str()) == 0 && - !lastFocusedItem->Path.empty() && strcmp(ptr->Path.c_str(), lastFocusedItem->Path.c_str()) == 0) + !lastFocusedItem->NameW.empty() && CompareFindTextW(ptr->NameW, lastFocusedItem->NameW) == 0 && + !lastFocusedItem->PathW.empty() && CompareFindTextW(ptr->PathW, lastFocusedItem->PathW) == 0) { selectIndex = i; break; @@ -630,14 +1679,13 @@ int CFoundFilesListView::CompareFunc(CFoundFilesData* f1, CFoundFilesData* f2, i { case 0: { - res = RegSetStrICmp(f1->Name.c_str(), f2->Name.c_str()); + res = CompareFindTextW(f1->NameW, f2->NameW); break; } case 1: { - res = RegSetStrICmp(f1->Path.c_str(), f2->Path.c_str()); - break; + res = CompareFindTextW(f1->PathW, f2->PathW); break; } @@ -751,7 +1799,7 @@ int CFoundFilesListView::CompareDuplicatesFunc(CFoundFilesData* f1, CFoundFilesD if (byName) { // by name - res = RegSetStrICmp(f1->Name.c_str(), f2->Name.c_str()); + res = CompareFindTextW(f1->NameW, f2->NameW); if (res == 0) { // by size @@ -787,7 +1835,7 @@ int CFoundFilesListView::CompareDuplicatesFunc(CFoundFilesData* f1, CFoundFilesD if (f1->Size == f2->Size) { // by name - res = RegSetStrICmp(f1->Name.c_str(), f2->Name.c_str()); + res = CompareFindTextW(f1->NameW, f2->NameW); if (res == 0) { // by group @@ -807,7 +1855,7 @@ int CFoundFilesListView::CompareDuplicatesFunc(CFoundFilesData* f1, CFoundFilesD } } if (res == 0) - res = RegSetStrICmp(f1->Path.c_str(), f2->Path.c_str()); + res = CompareFindTextW(f1->PathW, f2->PathW); return res; } @@ -1315,10 +2363,18 @@ BOOL CFoundFilesListView::InitColumns() // CFindDialog // -CFindDialog::CFindDialog(HWND hCenterAgainst, const char* initPath) +CFindDialog::CFindDialog(HWND hCenterAgainst, const char* initPath, const wchar_t* initPathW) : CCommonDialog(HLanguage, IDD_FIND, NULL, ooStandard, hCenterAgainst), SearchForData(50, 10) { +#ifndef _UNICODE + // Open as a Unicode dialog so the dialog template parses W and child + // controls are registered Unicode. Without this, CDialog::Execute uses + // DialogBoxParam (ANSI) and the Look-in replacement combo starts life + // behind an ANSI dialog boundary. + UnicodeWnd = TRUE; +#endif + // data needed to lay out the dialog FirstWMSize = TRUE; VMargin = 0; @@ -1365,7 +2421,14 @@ CFindDialog::CFindDialog(HWND hCenterAgainst, const char* initPath) ZeroOnDestroy = NULL; OleInitialized = FALSE; ProcessingEscape = FALSE; +#ifndef _UNICODE + // Keep the legacy edit subclass Unicode-capable for the fallback path. + // The non-CP_ACP Look-in case now uses CUnicodeNameInputController so it + // does not depend on this subclass for visible Unicode text. + EditLine = new CComboboxEdit(TRUE); +#else EditLine = new CComboboxEdit(); +#endif OKButton = NULL; FileNameFormat = Configuration.FileNameFormat; @@ -1387,6 +2450,14 @@ CFindDialog::CFindDialog(HWND hCenterAgainst, const char* initPath) } // data for controls + // + // The seed is stashed (not consumed here) so the deferred + // WM_USER_FIND_LOOKIN_W_OVERRIDE handler can decide whether to enable the + // Unicode edit control. We deliberately do NOT plant the wide form into + // LookInTextW from the constructor: LookInTextW is authoritative only + // when the Unicode control is enabled, and the override handler is the + // sole site that flips both states together. + InitialLookInSeed = sally::find::BuildLookInSeed(initPath, initPathW); if (Data.NamedText[0] == 0) lstrcpy(Data.NamedText, "*.*"); if (Data.LookInText[0] == 0) @@ -1622,6 +2693,8 @@ void CFindDialog::LayoutControls() HANDLES(EndDeferWindowPos(hdwp)); } SetTwoStatusParts(TwoParts); + ApplyFindComboSkins(HWindow); + ApplyFindStatusBarColors(HStatusBar); } void CFindDialog::SetContentVisible(BOOL visible) @@ -1717,7 +2790,10 @@ void CFindDialog::Validate(CTransferInfo& ti) if (ti.IsGood()) { - SendMessage(hLookInWnd, WM_GETTEXT, Data.LookInText.Size(), (LPARAM)Data.LookInText.Get()); + if (LookInUnicodeInput.IsEnabled()) + CopyWideLookInToAnsiMirror(LookInUnicodeInput.GetText(), Data.LookInText); + else + SendMessage(hLookInWnd, WM_GETTEXT, Data.LookInText.Size(), (LPARAM)Data.LookInText.Get()); BuildSerchForData(); if (SearchForData.Count == 0) @@ -1737,8 +2813,35 @@ void CFindDialog::Transfer(CTransferInfo& ti) { HistoryComboBox(HWindow, ti, IDC_FIND_NAMED, Data.NamedText, Data.NamedText.Size(), FALSE, FIND_NAMED_HISTORY_SIZE, FindNamedHistory); - HistoryComboBox(HWindow, ti, IDC_FIND_LOOKIN, Data.LookInText, Data.LookInText.Size(), - FALSE, FIND_LOOKIN_HISTORY_SIZE, FindLookInHistory); + if (LookInUnicodeInput.IsEnabled()) + { + if (ti.Type == ttDataToWindow) + { + SendMessage(LookInUnicodeInput.GetControlHandle(), CB_LIMITTEXT, + Data.LookInText.Size() - 1, 0); + LookInUnicodeInput.SetText(LookInTextW.empty() + ? AnsiToWide(Data.LookInText.Get()) + : LookInTextW); + } + else + { + LookInTextW = LookInUnicodeInput.GetText(); + CopyWideLookInToAnsiMirror(LookInTextW, Data.LookInText); + if (ti.IsGood() && Data.LookInText[0] != 0) + AddValueToStdHistoryValues(FindLookInHistory, FIND_LOOKIN_HISTORY_SIZE, + Data.LookInText.Get(), FALSE); + } + } + else + { + HistoryComboBox(HWindow, ti, IDC_FIND_LOOKIN, Data.LookInText, Data.LookInText.Size(), + FALSE, FIND_LOOKIN_HISTORY_SIZE, FindLookInHistory); + // When the Unicode edit control is not active, the wide cache must + // not outlive its authoritative window: drop it on the way out so + // any unguarded future reader cannot pick up a stale seed. + if (ti.Type == ttDataFromWindow) + LookInTextW.clear(); + } ti.CheckBox(IDC_FIND_INCLUDE_SUBDIR, Data.SubDirectories); HistoryComboBox(HWindow, ti, IDC_FIND_CONTAINING, Data.GrepText, GREP_TEXT_LEN, @@ -1763,12 +2866,26 @@ void CFindDialog::LoadControls(int index) { CALL_STACK_MESSAGE2("CFindDialog::LoadControls(0x%X)", index); Data = *FindOptions.At(index); + BOOL keepCurrentLookIn = Data.LookInText[0] == 0; // if any edit line is empty, keep its previous value if (Data.NamedText[0] == 0) GetDlgItemText(HWindow, IDC_FIND_NAMED, Data.NamedText, Data.NamedText.Size()); - if (Data.LookInText[0] == 0) - GetDlgItemText(HWindow, IDC_FIND_LOOKIN, Data.LookInText, Data.LookInText.Size()); + if (keepCurrentLookIn) + { + if (LookInUnicodeInput.IsEnabled()) + { + LookInTextW = LookInUnicodeInput.GetText(); + CopyWideLookInToAnsiMirror(LookInTextW, Data.LookInText); + } + else + { + GetDlgItemText(HWindow, IDC_FIND_LOOKIN, Data.LookInText, Data.LookInText.Size()); + LookInTextW.clear(); + } + } + else + LookInTextW.clear(); if (Data.GrepText[0] == 0) GetDlgItemText(HWindow, IDC_FIND_CONTAINING, Data.GrepText, GREP_TEXT_LEN); @@ -1853,68 +2970,42 @@ void CFindDialog::BuildSerchForData() SearchForData.DestroyMembers(); - CPathBuffer path; // Heap-allocated for long path support - lstrcpy(path, Data.LookInText); - begin = path; - do + // LookInTextW is authoritative only while the Unicode edit control is + // active. When that control is disabled, the ANSI combo (Data.LookInText, + // just refreshed from the control by Validate via WM_GETTEXT) is the + // source of truth; consulting LookInTextW here would prefer the + // constructor-seeded panel path over the user's edited target. + std::wstring lookInTextW; + if (LookInUnicodeInput.IsEnabled()) { - end = begin; - while (*end != 0) - { - if (*end == ';') - { - if (*(end + 1) != ';') - break; - else - memmove(end, end + 1, strlen(end + 1) + 1); // shift left (";;" -> ";") - } - end++; - } - char* tmp = end - 1; - if (*end == ';') - { - *end = 0; - end++; - } - // while (*end == ';') end++; // always false because ";;" -> ";" and it's a regular character, not a separator + lookInTextW = LookInUnicodeInput.GetText(); + LookInTextW = lookInTextW; + } + else + lookInTextW = AnsiToWide(Data.LookInText.Get()); - // remove spaces before the path - while (*begin == ' ') - begin++; - // remove spaces after the path - if (tmp > begin) - { - while (tmp > begin && *tmp <= ' ') - tmp--; - *(tmp + 1) = 0; // there might already be '\0'; otherwise add it - } - // remove redundant slashes/backslashes at the end of the path (keep at most one) - if (tmp > begin) - { - while (tmp > begin && (*tmp == '/' || *tmp == '\\')) - tmp--; - if (*(tmp + 1) == '/' || *(tmp + 1) == '\\') - tmp++; // leave one - *(tmp + 1) = 0; // there might already be '\0'; otherwise add it - } + std::vector paths = sally::find::SplitLookInPathsW(lookInTextW); + for (size_t i = 0; i < paths.size(); i++) + { + if (paths[i].empty()) + continue; - if (*begin != 0) + std::string pathA = WideToAnsi(paths[i]); + if (pathA.empty()) + pathA = "?"; + + CSearchForData* item = new CSearchForData(pathA.c_str(), paths[i].c_str(), named, Data.SubDirectories); + if (item != NULL) { - CSearchForData* item = new CSearchForData(begin, named, Data.SubDirectories); - if (item != NULL) + SearchForData.Add(item); + if (!SearchForData.IsGood()) { - SearchForData.Add(item); - if (!SearchForData.IsGood()) - { - SearchForData.ResetState(); - delete item; - return; - } + SearchForData.ResetState(); + delete item; + return; } } - if (*end != 0) - begin = end; - } while (*end != 0); + } } void CFindDialog::StartSearch(WORD command) @@ -2326,11 +3417,14 @@ void CFindDialog::UpdateListViewItems() LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL); // for the first added data, select the first item if (GrepData.FoundVisibleCount == 0 && count > 0) + { ListView_SetItemState(FoundFilesListView->HWindow, 0, LVIS_FOCUSED | LVIS_SELECTED, LVIS_FOCUSED | LVIS_SELECTED) + RedrawFindResultItem(FoundFilesListView->HWindow, 0); + } - // write the number of items above the list view - TBHeader->SetFoundCount(count); + // write the number of items above the list view + TBHeader->SetFoundCount(count); TBHeader->SetErrorsInfosCount(Log.GetErrorCount(), Log.GetInfoCount()); // when minimized, display the item count in the title @@ -2698,41 +3792,37 @@ void CFindDialog::OnCopyNameToClipboard(CCopyNameToClipboardModeEnum mode) if (index < 0) return; CFoundFilesData* data = FoundFilesListView->At(index); - CPathBuffer buff; - buff[0] = 0; + std::wstring textW; switch (mode) { case cntcmFullName: { - lstrcpyn(buff, data->Path.c_str(), buff.Size()); - int len = (int)strlen(buff); - if (len > 0 && buff[len - 1] != '\\') - strcat(buff, "\\"); - AlterFileName(buff + strlen(buff), data->Name.c_str(), -1, FileNameFormat, 0, data->IsDir); + textW = data->GetFullNameTextW(FileNameFormat); break; } case cntcmName: { - AlterFileName(buff, data->Name.c_str(), -1, FileNameFormat, 0, data->IsDir); + textW = data->GetNameTextW(FileNameFormat); break; } case cntcmFullPath: { - lstrcpyn(buff, data->Path.c_str(), buff.Size()); + textW = data->PathW; break; } case cntcmUNCName: { + CPathBuffer buff; AlterFileName(buff, data->Name.c_str(), -1, FileNameFormat, 0, data->IsDir); CopyUNCPathToClipboard(data->Path.c_str(), buff, data->IsDir, HWindow); - break; + return; } } if (mode != cntcmUNCName) - CopyTextToClipboard(buff); + CopyTextToClipboardW(textW.c_str()); } BOOL CFindDialog::IsMenuBarMessage(CONST MSG* lpMsg) @@ -2884,6 +3974,20 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) SLOW_CALL_STACK_MESSAGE4("CFindDialog::DialogProc(0x%X, 0x%IX, 0x%IX)", uMsg, wParam, lParam); switch (uMsg) { + case WM_PAINT: + { + INT_PTR ret = CCommonDialog::DialogProc(uMsg, wParam, lParam); + PaintFindDialogSeparatorLines(HWindow, NULL); + return ret; + } + + case WM_PRINTCLIENT: + { + INT_PTR ret = CCommonDialog::DialogProc(uMsg, wParam, lParam); + PaintFindDialogSeparatorLines(HWindow, (HDC)wParam); + return ret; + } + case WM_INITDIALOG: { FindDialogQueue.Add(new CWindowQueueItem(HWindow)); @@ -2918,6 +4022,7 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) // construct the list view FoundFilesListView = new CFoundFilesListView(HWindow, IDC_FIND_RESULTS, this); + ListView_SetUnicodeFormat(FoundFilesListView->HWindow, TRUE); SetFullRowSelect(Configuration.FindFullRowSelect); @@ -2976,7 +4081,6 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) LayoutControls(); FoundFilesListView->InitColumns(); SetContentVisible(Configuration.SearchFileContent); - ApplyFindResultsListColors(HWindow); // remove WS_TABSTOP from IDC_FIND_ADVANCED_TEXT DWORD style = (DWORD)GetWindowLongPtr(GetDlgItem(HWindow, IDC_FIND_ADVANCED_TEXT), GWL_STYLE); @@ -3007,6 +4111,10 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) ShowWindow(GetDlgItem(HWindow, IDC_FIND_INCLUDE_ARCHIVES), FALSE); EnableControls(); + ApplyFindDialogTheme(HWindow, HStatusBar); + PostMessage(HWindow, WM_USER_FIND_DELAYED_THEME, 0, 0); + // Defer the wide "Look in" override past the framework's ANSI Transfer. + PostMessage(HWindow, WM_USER_FIND_LOOKIN_W_OVERRIDE, 0, 0); break; } @@ -3047,6 +4155,50 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_COLORCHANGEFIND: { OnColorsChange(); + ApplyFindDialogTheme(HWindow, HStatusBar); + return TRUE; + } + + case WM_USER_FIND_DELAYED_THEME: + { + ApplyFindDialogTheme(HWindow, HStatusBar); + return TRUE; + } + + case WM_USER_FIND_LOOKIN_W_OVERRIDE: + { + // Replace the ANSI-rendered "Look in" combo when the round-trip + // predicate says the Transfer-populated text is lossy. ASCII-only + // paths skip the replacement; the dialog renders exactly as before. + // + // Only fire when the current ANSI text still matches the seed we + // captured at construction. If AutoLoad or LoadControls swapped in a + // different preset, the user implicitly chose that preset's text and + // we must not override it with the panel's wide form. This mirrors + // the gating the constructor used to do before LookInTextW was made + // a strictly-IsEnabled-coupled cache. + if (InitialLookInSeed.ansi != Data.LookInText.Get()) + return TRUE; + const sally::find::LookInSeed& seed = InitialLookInSeed; + if (sally::find::ShouldOverrideEditWithWide(seed)) + { + HWND hLegacyCombo = GetDlgItem(HWindow, IDC_FIND_LOOKIN); + HWND hFocus = GetFocus(); + BOOL focusWasLookIn = IsFindLookInFocus(hFocus, hLegacyCombo); + if (LookInUnicodeInput.EnableForCombo(HWindow, IDC_FIND_LOOKIN, seed.wide, + NULL, 0, Data.LookInText.Size(), -1)) + { + ApplyFindComboSkin(LookInUnicodeInput.GetControlHandle()); + if (!focusWasLookIn && hFocus != NULL && IsWindow(hFocus)) + SetFocus(hFocus); + // Plant the wide cache only now that the Unicode control is + // live: the cache must be authoritative iff the control is + // active, so the two are populated in the same step. + LookInTextW = seed.wide; + } + else + SetDlgItemTextW(HWindow, IDC_FIND_LOOKIN, seed.wide.c_str()); + } return TRUE; } @@ -3068,7 +4220,14 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_CLEARHISTORY: { ClearComboboxListbox(GetDlgItem(HWindow, IDC_FIND_NAMED)); - ClearComboboxListbox(GetDlgItem(HWindow, IDC_FIND_LOOKIN)); + if (LookInUnicodeInput.IsEnabled()) + { + std::wstring lookInText = LookInUnicodeInput.GetText(); + SendMessageW(LookInUnicodeInput.GetControlHandle(), CB_RESETCONTENT, 0, 0); + LookInUnicodeInput.SetText(lookInText); + } + else + ClearComboboxListbox(GetDlgItem(HWindow, IDC_FIND_LOOKIN)); ClearComboboxListbox(GetDlgItem(HWindow, IDC_FIND_CONTAINING)); return TRUE; } @@ -3265,6 +4424,16 @@ CFindDialog::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { if (FoundFilesListView != NULL && ListView_GetEditControl(FoundFilesListView->HWindow) != NULL) return 0; // the list view sends some commands while editing + if (LOWORD(wParam) == IDC_FIND_LOOKIN && LookInUnicodeInput.IsEnabled()) + { + HWND hCombo = (HWND)lParam; + if (hCombo == LookInUnicodeInput.GetControlHandle()) + { + BOOL isDropdownOpen = (BOOL)SendMessage(hCombo, CB_GETDROPPEDSTATE, 0, 0); + if (sally::unicode::ShouldSyncUnicodeComboSelection(HIWORD(wParam), isDropdownOpen)) + LookInUnicodeInput.SyncSelectionToEdit(); + } + } if (LOWORD(wParam) >= CM_FIND_OPTIONS_FIRST && LOWORD(wParam) <= CM_FIND_OPTIONS_LAST) { LoadControls(LOWORD(wParam) - CM_FIND_OPTIONS_FIRST); @@ -3547,6 +4716,33 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = if (cmd == 1) { // Browse... + if (LookInUnicodeInput.IsEnabled()) + { + HWND hCombo = LookInUnicodeInput.GetControlHandle(); + std::wstring current = LookInUnicodeInput.GetText(); + DWORD start = 0; + DWORD end = 0; + SendMessage(hCombo, CB_GETEDITSEL, (WPARAM)&start, (LPARAM)&end); + + std::wstring pathW; + size_t startPos = start; + size_t endPos = end; + if (startPos > current.length()) + startPos = current.length(); + if (endPos > current.length()) + endPos = current.length(); + if (endPos > startPos) + pathW.assign(current, startPos, endPos - startPos); + + if (GetTargetDirectoryW(HWindow, HWindow, LoadStrW(IDS_CHANGE_DIRECTORY), + LoadStrW(IDS_BROWSECHANGEDIRTEXT), pathW, FALSE, pathW.c_str())) + { + ReplaceFindLookInSelectionW(LookInUnicodeInput, pathW, start, end); + LookInTextW = LookInUnicodeInput.GetText(); + } + return TRUE; + } + CPathBuffer path; char buff[1024]; DWORD start, end; @@ -3595,7 +4791,14 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = return TRUE; } if (cmd == 3 || cmd == 4) - InsertDrives(EditLine->HWindow, cmd == 4); // local drives (3) || all drives (4) + { + HWND hLookIn = LookInUnicodeInput.IsEnabled() + ? LookInUnicodeInput.GetControlHandle() + : EditLine->HWindow; + InsertDrives(hLookIn, cmd == 4); // local drives (3) || all drives (4) + if (LookInUnicodeInput.IsEnabled()) + LookInTextW = LookInUnicodeInput.GetText(); + } } return 0; } @@ -3815,6 +5018,13 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = if (wParam == IDC_FIND_STATUS) { DRAWITEMSTRUCT* di = (DRAWITEMSTRUCT*)lParam; + DarkModeColors colors; + BOOL useDark = DarkMode_GetColors(&colors); + if (useDark) + { + FindFillRectSolid(di->hDC, &di->rcItem, colors.DialogBackground); + SetTextColor(di->hDC, colors.DialogText); + } int prevBkMode = SetBkMode(di->hDC, TRANSPARENT); CPathBuffer buff; SearchingText.Get(buff, buff.Size()); @@ -3859,6 +5069,19 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = case WM_NOTIFY: { + if (wParam == IDC_FIND_STATUS && ((LPNMHDR)lParam)->code == NM_CUSTOMDRAW) + { + LPNMCUSTOMDRAW cd = (LPNMCUSTOMDRAW)lParam; + DarkModeColors colors; + if (cd->dwDrawStage == CDDS_PREPAINT && DarkMode_GetColors(&colors)) + { + SetTextColor(cd->hdc, colors.DialogText); + SetBkColor(cd->hdc, colors.DialogBackground); + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; + } + } + if (wParam == IDC_FIND_RESULTS) { switch (((LPNMHDR)lParam)->code) @@ -3907,116 +5130,163 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = if (cd->nmcd.dwDrawStage == CDDS_ITEMPREPAINT) { - // request notification of CDDS_ITEMPREPAINT | CDDS_SUBITEM + // Ask for subitem notifications: the Name/icon column stays + // native, while the Path column gets upstream-style compaction. SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_NOTIFYSUBITEMDRAW); return TRUE; } if (cd->nmcd.dwDrawStage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM)) { - CFoundFilesData* item = FoundFilesListView->At((int)cd->nmcd.dwItemSpec); + int itemIndex = (int)cd->nmcd.dwItemSpec; + CFoundFilesData* item = FoundFilesListView->At(itemIndex); + if (item == NULL) + { + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; + } - // we'd like to draw the Path column ourselves (with ellipsis for paths) + BOOL selected = (ListView_GetItemState(FoundFilesListView->HWindow, itemIndex, LVIS_SELECTED) & LVIS_SELECTED) != 0; + BOOL duplicate = GrepData.FindDuplicates && item->Different == 1; + + // Draw only the Path column ourselves, just like upstream. + // This keeps the Name/icon column's focus and selection UX native. if (cd->iSubItem == 1) { HDC hDC = cd->nmcd.hdc; - // if the cache DC does not exist yet, try to create it if (CacheBitmap == NULL) { CacheBitmap = new CBitmap(); - if (CacheBitmap != NULL) - CacheBitmap->CreateBmp(hDC, 1, 1); + if (CacheBitmap != NULL && !CacheBitmap->CreateBmp(hDC, 1, 1)) + { + delete CacheBitmap; + CacheBitmap = NULL; + } } if (CacheBitmap == NULL) - break; // out of memory; let the list view draw it; we're done + { + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; + } + + RECT r; + if (!ListView_GetSubItemRect(FoundFilesListView->HWindow, itemIndex, cd->iSubItem, LVIR_BOUNDS, &r)) + { + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; + } - RECT r; // rectangle around the sub item - ListView_GetSubItemRect(FoundFilesListView->HWindow, cd->nmcd.dwItemSpec, cd->iSubItem, LVIR_BOUNDS, &r); - RECT r2; // rectangle same size as r but shifted to origin + RECT r2; r2.left = 0; r2.top = 0; r2.right = r.right - r.left; r2.bottom = r.bottom - r.top; - // enlarge the cache bitmap if necessary - if (CacheBitmap->NeedEnlarge(r2.right, r2.bottom)) - CacheBitmap->Enlarge(r2.right, r2.bottom); + if (CacheBitmap->NeedEnlarge(r2.right, r2.bottom) && !CacheBitmap->Enlarge(r2.right, r2.bottom)) + { + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; + } - // fill the background with the default color DarkModeColors colors; - DarkMode_GetColors(&colors); - COLORREF bkColor = (GrepData.FindDuplicates && item->Different == 1) ? colors.InactiveSelection : colors.InputBackground; - COLORREF textColor = colors.InputText; + BOOL useDark = DarkMode_GetColors(&colors); + COLORREF bkColor = useDark ? (duplicate ? colors.InactiveSelection : colors.InputBackground) + : GetSysColor(duplicate ? COLOR_3DFACE : COLOR_WINDOW); + COLORREF textColor = useDark ? colors.InputText : GetSysColor(COLOR_WINDOWTEXT); - if (Configuration.FindFullRowSelect) + if (Configuration.FindFullRowSelect && selected) { - if (ListView_GetItemState(FoundFilesListView->HWindow, cd->nmcd.dwItemSpec, LVIS_SELECTED) & LVIS_SELECTED) + if (GetFocus() == FoundFilesListView->HWindow) { - if (GetFocus() == FoundFilesListView->HWindow) - { - bkColor = colors.Highlight; - textColor = colors.HighlightText; - } - else + bkColor = useDark ? colors.Highlight : GetSysColor(COLOR_HIGHLIGHT); + textColor = useDark ? colors.HighlightText : GetSysColor(COLOR_HIGHLIGHTTEXT); + } + else + { + if (useDark) { if (colors.InactiveSelection != colors.InputBackground) bkColor = colors.InactiveSelection; else { - // for high contrast color schemes bkColor = colors.Highlight; textColor = colors.HighlightText; } } + else + { + if (GetSysColor(COLOR_3DFACE) != GetSysColor(COLOR_WINDOW)) + bkColor = GetSysColor(COLOR_3DFACE); + else + { + // high-contrast color schemes may not distinguish 3DFACE + bkColor = GetSysColor(COLOR_HIGHLIGHT); + textColor = GetSysColor(COLOR_HIGHLIGHTTEXT); + } + } } } - SetBkColor(CacheBitmap->HMemDC, bkColor); - ExtTextOut(CacheBitmap->HMemDC, 0, 0, ETO_OPAQUE, &r2, "", 0, NULL); - SetBkMode(CacheBitmap->HMemDC, TRANSPARENT); + COLORREF oldBkColor = SetBkColor(CacheBitmap->HMemDC, bkColor); + int oldBkMode = SetBkMode(CacheBitmap->HMemDC, TRANSPARENT); + ExtTextOutW(CacheBitmap->HMemDC, 0, 0, ETO_OPAQUE, &r2, L"", 0, NULL); - // draw the text with path shortening r2.left += 5; r2.right -= 5; - CFoundFilesData* item2 = FoundFilesListView->At((int)cd->nmcd.dwItemSpec); - SelectObject(CacheBitmap->HMemDC, (HFONT)SendMessage(FoundFilesListView->HWindow, WM_GETFONT, 0, 0)); - int oldTextColor = SetTextColor(CacheBitmap->HMemDC, textColor); - - // DT_PATH_ELLIPSIS doesn't work on some strings and causing clipped text to be printed - // PathCompactPath() requires a copy in a local buffer but doesn't clip text - CPathBuffer buff; - strncpy_s(buff, buff.Size(), item2->Path.c_str(), _TRUNCATE); - PathCompactPath(CacheBitmap->HMemDC, buff, r2.right - r2.left); - DrawText(CacheBitmap->HMemDC, buff, -1, &r2, - DT_VCENTER | DT_LEFT | DT_NOPREFIX | DT_SINGLELINE); - // DrawText(CacheBitmap->HMemDC, item2->Path, -1, &r2, - // DT_VCENTER | DT_LEFT | DT_NOPREFIX | DT_SINGLELINE | DT_PATH_ELLIPSIS); + + HFONT font = (HFONT)SendMessage(FoundFilesListView->HWindow, WM_GETFONT, 0, 0); + HGDIOBJ oldFont = NULL; + if (font != NULL) + oldFont = SelectObject(CacheBitmap->HMemDC, font); + COLORREF oldTextColor = SetTextColor(CacheBitmap->HMemDC, textColor); + + if (r2.right > r2.left) + { + CWidePathBuffer buff(item->PathW.c_str()); + PathCompactPathW(CacheBitmap->HMemDC, buff, (UINT)(r2.right - r2.left)); + DrawTextW(CacheBitmap->HMemDC, buff, -1, &r2, + DT_VCENTER | DT_LEFT | DT_NOPREFIX | DT_SINGLELINE); + } + SetTextColor(CacheBitmap->HMemDC, oldTextColor); + if (oldFont != NULL) + SelectObject(CacheBitmap->HMemDC, oldFont); + SetBkMode(CacheBitmap->HMemDC, oldBkMode); + SetBkColor(CacheBitmap->HMemDC, oldBkColor); - // copy the cache to the list view BitBlt(hDC, r.left, r.top, r.right - r.left, r.bottom - r.top, CacheBitmap->HMemDC, 0, 0, SRCCOPY); - // disable default drawing SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_SKIPDEFAULT); return TRUE; } - if (GrepData.FindDuplicates && item->Different == 1) + if (duplicate && !selected) { DarkModeColors colors; - DarkMode_GetColors(&colors); - cd->clrTextBk = colors.InactiveSelection; + if (DarkMode_GetColors(&colors)) + { + cd->clrTextBk = colors.InactiveSelection; + cd->clrText = colors.InputText; + } + else + { + cd->clrTextBk = GetSysColor(COLOR_3DFACE); + cd->clrText = GetSysColor(COLOR_WINDOWTEXT); + } SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_NEWFONT); return TRUE; } - break; + + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, CDRF_DODEFAULT); + return TRUE; } break; } +#ifndef _UNICODE case LVN_ODFINDITEM: { // assist the list view with quick search @@ -4081,6 +5351,17 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = SetWindowLongPtr(HWindow, DWLP_MSGRESULT, ret); return TRUE; } +#endif + + case LVN_ODFINDITEMW: + { + NMLVFINDITEMW* pFindInfo = (NMLVFINDITEMW*)lParam; + int ret = FindListItemByNameW(FoundFilesListView, pFindInfo->iStart, + pFindInfo->lvfi.flags, pFindInfo->lvfi.psz); + + SetWindowLongPtr(HWindow, DWLP_MSGRESULT, ret); + return TRUE; + } case LVN_COLUMNCLICK: { @@ -4090,6 +5371,7 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = break; } +#ifndef _UNICODE case LVN_GETDISPINFO: { LV_DISPINFO* info = (LV_DISPINFO*)lParam; @@ -4100,9 +5382,32 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = info->item.pszText = item->GetText(info->item.iSubItem, FoundFilesDataTextBuffer, FileNameFormat); break; } +#endif + + case LVN_GETDISPINFOW: + { + NMLVDISPINFOW* info = (NMLVDISPINFOW*)lParam; + CFoundFilesData* item = FoundFilesListView->At(info->item.iItem); + if (info->item.mask & LVIF_IMAGE) + info->item.iImage = item->IsDir ? 0 : 1; + if (info->item.mask & LVIF_TEXT) + { + FoundFilesDataTextBufferW = item->GetTextW(info->item.iSubItem, FileNameFormat); + info->item.pszText = const_cast(FoundFilesDataTextBufferW.c_str()); + } + break; + } case LVN_ITEMCHANGED: { + NMLISTVIEW* lv = (NMLISTVIEW*)lParam; + if (lv->iItem >= 0 && + (lv->uChanged & LVIF_STATE) != 0 && + ((lv->uOldState ^ lv->uNewState) & (LVIS_SELECTED | LVIS_FOCUSED)) != 0) + { + RedrawFindResultItem(FoundFilesListView->HWindow, lv->iItem); + } + EnableToolBar(); if (!IsSearchInProgress()) UpdateStatusBar = TRUE; // the text will be set during Idle time @@ -4317,6 +5622,8 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = if (SearchInProgress) StopSearch(); + LookInUnicodeInput.Reset(); + if (!DlgFailed) { // store the width of the Name column @@ -4390,7 +5697,8 @@ MENU_TEMPLATE_ITEM FindLookInBrowseMenu[] = case WM_THEMECHANGED: case WM_SYSCOLORCHANGE: { - ApplyFindResultsListColors(HWindow); + OnColorsChange(); + ApplyFindDialogTheme(HWindow, HStatusBar); break; } } diff --git a/src/gui.cpp b/src/gui.cpp index c2eec759d..85afad87d 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -19,6 +19,20 @@ #include "mainwnd.h" +static std::string StaticTextWideToAnsi(const std::wstring& text) +{ + if (text.empty()) + return std::string(); + + int required = WideCharToMultiByte(CP_ACP, 0, text.c_str(), (int)text.size(), NULL, 0, NULL, NULL); + if (required <= 0) + return std::string(); + + std::string result(required, '\0'); + WideCharToMultiByte(CP_ACP, 0, text.c_str(), (int)text.size(), &result[0], required, NULL, NULL); + return result; +} + //**************************************************************************** // // CGuiBitmap @@ -446,7 +460,9 @@ CStaticText::CStaticText(HWND hDlg, int ctrlID, DWORD flags) TextLen = 0; Text2 = NULL; Text2Len = 0; + UseWideText = FALSE; AlpDX = NULL; + AlpDXAllocated = 0; Allocated = 0; Bitmap = NULL; HFont = NULL; @@ -540,7 +556,7 @@ BOOL CStaticText::SetText(const char* text) if (text == NULL) text = ""; - if (Text != NULL && strcmp(Text, text) == 0) + if (!UseWideText && Text != NULL && strcmp(Text, text) == 0) return TRUE; int l = (int)strlen(text) + 1; @@ -570,12 +586,35 @@ BOOL CStaticText::SetText(const char* text) return FALSE; } AlpDX = newAlpDX; + AlpDXAllocated = l + ST_ALLOC_GRANULARITY; Text2 = newText2; } Text = newText; Allocated = l + ST_ALLOC_GRANULARITY; } + // Allocated tracks ANSI byte capacity of Text; AlpDX is sized in ints to + // hold TextLen (which is l-1 here). A prior SetTextW under a DBCS code + // page can leave Allocated sized for the multi-byte ANSI fallback while + // AlpDXAllocated is sized only for the wide codepoint count — the new + // ANSI text can then fit Allocated while overrunning AlpDX. Realloc + // AlpDX whenever its tracked capacity is short for the new TextLen. + else if ((Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) && + (AlpDX == NULL || l > AlpDXAllocated)) + { + const int needAlpDX = l + ST_ALLOC_GRANULARITY; + int* newAlpDX = (int*)realloc(AlpDX, needAlpDX * sizeof(int)); + if (newAlpDX == NULL) + { + TRACE_E(LOW_MEMORY); + return FALSE; + } + AlpDX = newAlpDX; + AlpDXAllocated = needAlpDX; + } memmove(Text, text, l); + TextW.clear(); + Text2W.clear(); + UseWideText = FALSE; TextLen = l - 1; PrepareForPaint(); @@ -585,6 +624,71 @@ BOOL CStaticText::SetText(const char* text) return TRUE; } +BOOL CStaticText::SetTextW(const wchar_t* text) +{ + std::wstring newText = text != NULL ? text : L""; + if (UseWideText && TextW == newText) + return TRUE; + + std::string fallbackText = StaticTextWideToAnsi(newText); + int l = (int)fallbackText.size() + 1; + if (Allocated < l) + { + char* newTextA = (char*)realloc(Text, l + ST_ALLOC_GRANULARITY); + if (newTextA == NULL) + { + TRACE_E(LOW_MEMORY); + return FALSE; + } + if (Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) + { + const int needAlpDX = max(1, (int)newText.size()) + ST_ALLOC_GRANULARITY; + int* newAlpDX = (int*)realloc(AlpDX, needAlpDX * sizeof(int)); + if (newAlpDX == NULL) + { + TRACE_E(LOW_MEMORY); + free(newTextA); + return FALSE; + } + AlpDX = newAlpDX; + AlpDXAllocated = needAlpDX; + } + Text = newTextA; + Allocated = l + ST_ALLOC_GRANULARITY; + } + // Allocated tracks the ANSI byte capacity of Text. AlpDX, however, must + // hold TextLen ints, and TextLen for wide text is the wide codepoint + // count — which can exceed the ANSI fallback length (e.g. CJK chars on a + // DBCS code page, or characters that fall back to '?'). Realloc AlpDX + // whenever its tracked capacity is short for the new wide TextLen, even + // when the ANSI fallback still fits in Allocated. + else if ((Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) && + (AlpDX == NULL || (int)newText.size() > AlpDXAllocated)) + { + const int needAlpDX = max(1, (int)newText.size()) + ST_ALLOC_GRANULARITY; + int* newAlpDX = (int*)realloc(AlpDX, needAlpDX * sizeof(int)); + if (newAlpDX == NULL) + { + TRACE_E(LOW_MEMORY); + return FALSE; + } + AlpDX = newAlpDX; + AlpDXAllocated = needAlpDX; + } + + memmove(Text, fallbackText.c_str(), l); + TextW = newText; + Text2W.clear(); + UseWideText = TRUE; + TextLen = (int)TextW.size(); + + PrepareForPaint(); + + InvalidateRect(HWindow, NULL, FALSE); + UpdateWindow(HWindow); + return TRUE; +} + BOOL CStaticText::SetTextToDblQuotesIfNeeded(const char* text) { CALL_STACK_MESSAGE2("CStaticText::SetTextToDblQuotesIfNeeded(%s)", text); @@ -602,6 +706,22 @@ BOOL CStaticText::SetTextToDblQuotesIfNeeded(const char* text) return SetText(text); } +BOOL CStaticText::SetTextToDblQuotesIfNeededW(const wchar_t* text) +{ + if (text != NULL) + { + int len = (int)wcslen(text); + if (len > 0 && (text[0] <= L' ' || text[len - 1] <= L' ') && len < 2 * MAX_PATH) + { + std::wstring quoted = L"\""; + quoted += text; + quoted += L"\""; + return SetTextW(quoted.c_str()); + } + } + return SetTextW(text); +} + void CStaticText::PrepareForPaint() { ClipDraw = FALSE; @@ -617,7 +737,134 @@ void CStaticText::PrepareForPaint() HDC hDC = HANDLES(GetDC(HWindow)); HFONT hOldFont = (HFONT)SelectObject(hDC, HFont); SIZE sz; - if (Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) + if (UseWideText) + { + if (Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) + { + if (Flags & STF_END_ELLIPSIS) + { + int fitChars; + GetTextExtentExPointW(hDC, TextW.c_str(), TextLen, Width, &fitChars, AlpDX, &sz); + + if (fitChars < TextLen) + { + SIZE ellipsisSZ; + GetTextExtentPoint32W(hDC, L"...", 3, &ellipsisSZ); + int ellipsisWidth = ellipsisSZ.cx; + + while (fitChars > 0 && AlpDX[fitChars - 1] + ellipsisWidth > Width) + fitChars--; + if (fitChars > 0) + { + Text2W.assign(TextW.c_str(), TextW.c_str() + fitChars); + TextWidth = AlpDX[fitChars - 1]; + Text2Len = fitChars; + } + else + { + Text2W.clear(); + TextWidth = 0; + Text2Len = 0; + } + Text2W += L"..."; + TextWidth += ellipsisWidth; + Text2Len += 3; + + Text2Draw = TRUE; + } + else + { + TextWidth = sz.cx; + Text2W.clear(); + } + } + else + { + GetTextExtentExPointW(hDC, TextW.c_str(), TextLen, 0, NULL, AlpDX, &sz); + + if (sz.cx > Width) + { + SIZE ellipsisSZ; + GetTextExtentPoint32W(hDC, L"...", 3, &ellipsisSZ); + int ellipsisWidth = ellipsisSZ.cx; + + const wchar_t separator = (wchar_t)(unsigned char)PathSeparator; + const wchar_t* p = TextW.c_str() + TextLen - 1; + while (*p != separator && p > TextW.c_str()) + p--; + const wchar_t* p2 = p; + if (p > TextW.c_str()) + p--; + int pIndex = (int)(p - TextW.c_str()); + + if (ellipsisWidth + sz.cx - AlpDX[pIndex] > Width) + { + while (pIndex < TextLen && (ellipsisWidth + sz.cx - AlpDX[pIndex] > Width)) + pIndex++; + + pIndex++; + Text2W = L"..."; + Text2Len = 3; + TextWidth = ellipsisWidth; + if (pIndex < TextLen) + { + Text2W.append(TextW.c_str() + pIndex); + Text2Len += TextLen - pIndex; + TextWidth += sz.cx - AlpDX[pIndex - 1]; + } + } + else + { + int rightPartWidth = sz.cx - AlpDX[pIndex]; + while (pIndex >= 0 && (AlpDX[pIndex] + ellipsisWidth + rightPartWidth) > Width) + pIndex--; + + Text2Len = 0; + TextWidth = 0; + Text2W.clear(); + if (pIndex >= 0) + { + Text2W.assign(TextW.c_str(), TextW.c_str() + pIndex + 1); + Text2Len += pIndex + 1; + TextWidth += AlpDX[pIndex]; + } + Text2W += L"..."; + Text2Len += 3; + TextWidth += ellipsisWidth; + Text2W.append(p2); + Text2Len += TextLen - (int)(p2 - TextW.c_str()); + TextWidth += rightPartWidth; + } + + Text2Draw = TRUE; + } + else + { + TextWidth = sz.cx; + Text2W.clear(); + } + } + TextHeight = sz.cy; + } + else + { + if (Flags & STF_HANDLEPREFIX) + { + RECT r; + GetClientRect(HWindow, &r); + DrawTextW(hDC, TextW.c_str(), TextLen, &r, DT_CALCRECT | DT_SINGLELINE | DT_LEFT); + TextWidth = r.right; + TextHeight = r.bottom; + } + else + { + GetTextExtentPoint32W(hDC, TextW.c_str(), TextLen, &sz); + TextWidth = sz.cx + 1; + TextHeight = sz.cy; + } + } + } + else if (Flags & (STF_PATH_ELLIPSIS | STF_END_ELLIPSIS)) { if (Flags & STF_END_ELLIPSIS) { @@ -1121,28 +1368,29 @@ CStaticText::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) if (UIState & UISF_HIDEACCEL) drawFlags |= DT_HIDEPREFIX; - DrawText(hDC, Text, TextLen, &r, drawFlags); + if (UseWideText) + DrawTextW(hDC, TextW.c_str(), TextLen, &r, drawFlags); + else + DrawTextA(hDC, Text, TextLen, &r, drawFlags); } else { - const char* text; - int textLen; - if (Text2Draw) + DWORD drawFlags = (bkErased) ? 0 : ETO_OPAQUE; + drawFlags |= ETO_CLIPPED; + + int xOffset = GetTextXOffset(); + if (UseWideText) { - text = Text2; - textLen = Text2Len; + const wchar_t* text = Text2Draw ? Text2W.c_str() : TextW.c_str(); + int textLen = Text2Draw ? Text2Len : TextLen; + ExtTextOutW(hDC, r.left + xOffset, r.top, drawFlags, &r, text, textLen, NULL); } else { - text = Text; - textLen = TextLen; + const char* text = Text2Draw ? Text2 : Text; + int textLen = Text2Draw ? Text2Len : TextLen; + ExtTextOutA(hDC, r.left + xOffset, r.top, drawFlags, &r, text, textLen, NULL); } - DWORD drawFlags = (bkErased) ? 0 : ETO_OPAQUE; - // if (ClipDraw) // same problem as above - drawFlags |= ETO_CLIPPED; - - int xOffset = GetTextXOffset(); - ExtTextOut(hDC, r.left + xOffset, r.top, drawFlags, &r, text, textLen, NULL); } if (Flags & STF_DOTUNDERLINE) diff --git a/src/gui.h b/src/gui.h index b48e9cc0b..a3cfeb69e 100644 --- a/src/gui.h +++ b/src/gui.h @@ -69,6 +69,7 @@ class CStaticText : public CWindow // sets Text, returns TRUE on success and FALSE on memory shortage BOOL SetText(const char* text); + BOOL SetTextW(const wchar_t* text); // warning, returned Text may be NULL const char* GetText() { return Text; } @@ -76,6 +77,7 @@ class CStaticText : public CWindow // sets Text (if it starts or ends with a space, puts it in double quotes), // returns TRUE on success and FALSE on memory shortage BOOL SetTextToDblQuotesIfNeeded(const char* text); + BOOL SetTextToDblQuotesIfNeededW(const wchar_t* text); // on some filesystems there can be a different path separator // must be different from '\0'; @@ -110,13 +112,24 @@ class CStaticText : public CWindow DWORD Flags; // flags for control behavior char* Text; // allocated text + std::wstring TextW; // wide text source when Unicode rendering is needed int TextLen; // string length char* Text2; // allocated text containing ellipsis; used only with STF_END_ELLIPSIS or STF_PATH_ELLIPSIS + std::wstring Text2W; // wide ellipsized text for Unicode rendering int Text2Len; // Text2 length int* AlpDX; // array of substring lengths; used only with STF_END_ELLIPSIS or STF_PATH_ELLIPSIS + int AlpDXAllocated; // AlpDX capacity, in ints. Tracked separately from + // Allocated (which is ANSI byte capacity of Text) + // because SetTextW sizes AlpDX by wide codepoint count + // — under DBCS code pages or when wide-then-ANSI calls + // interleave, Allocated and AlpDX capacity diverge. + // GetTextExtentExPointW writes TextLen ints into AlpDX, + // so this must be >= TextLen whenever ellipsis flags + // are set. int TextWidth; // text width in points int TextHeight; // text height in points - int Allocated; // size of allocated buffer 'Text' and 'AlpDX' + int Allocated; // size of allocated buffer 'Text' (ANSI bytes) + BOOL UseWideText; // render using TextW/Text2W instead of ANSI buffers int Width, Height; // static dimensions CBitmap* Bitmap; // cache for drawing; used only with STF_CACHED_PAINT HFONT HFont; // font handle used for text drawing diff --git a/src/lang/texts.rc2 b/src/lang/texts.rc2 index 057813d8e..e9f7e6b25 100644 --- a/src/lang/texts.rc2 +++ b/src/lang/texts.rc2 @@ -1248,10 +1248,12 @@ STRINGTABLE IDS_REINSTALLPLUGIN, "%s plugin (%s) has been changed. Please reinstall (remove and add) this plugin (in menu Plugins / Plugins Manager)." IDS_PLUGININVALID, "%s plugin (%s) has not been loaded. Plugin is not valid Sally Plugin or some plugin internal error has occurred." IDS_PLUGININVALID2, "This plugin (%s) has not been loaded. Plugin is not valid Sally Plugin or some plugin internal error has occurred." - IDS_OLDPLUGINVERSION, "%s plugin (%s) is not supported by this version of Sally. Please get updated version of this plugin." - IDS_OLDPLUGINVERSION2, "The plugin (%s) is not supported by this version of Sally. Please get updated version of this plugin." - IDS_OLDPLUGINVERSION_CONFIRM, "%s plugin (%s) targets legacy commercial SDK version 4.0 (required version 102).\nIt may be ABI-incompatible with this Sally build and can be unstable.\n\nDo you want to load this plugin anyway?\nChoosing YES permanently trusts this plugin path." - IDS_OLDPLUGINVERSION_CONFIRM2, "The plugin (%s) targets legacy commercial SDK version 4.0 (required version 102).\nIt may be ABI-incompatible with this Sally build and can be unstable.\n\nDo you want to load this plugin anyway?\nChoosing YES permanently trusts this plugin path." +IDS_OLDPLUGINVERSION, "%s plugin (%s) is not supported by this version of Sally. Please get updated version of this plugin." +IDS_OLDPLUGINVERSION2, "The plugin (%s) is not supported by this version of Sally. Please get updated version of this plugin." +IDS_OLDPLUGINVERSION_CONFIRM, "%s plugin (%s) targets legacy commercial SDK version 4.0 (required version 102).\nIt may be ABI-incompatible with this Sally build and can be unstable.\n\nDo you want to load this plugin anyway?\nChoosing YES permanently trusts this plugin path." +IDS_OLDPLUGINVERSION_CONFIRM2, "The plugin (%s) targets legacy commercial SDK version 4.0 (required version 102).\nIt may be ABI-incompatible with this Sally build and can be unstable.\n\nDo you want to load this plugin anyway?\nChoosing YES permanently trusts this plugin path." +IDS_BROKENFSPLUGINVERSION, "%s plugin (%s) targets broken Sally SDK version 104 for file-system plugins.\nThis transitional FS ABI is unsafe and is no longer supported.\n\nPlease rebuild or reinstall an updated version of this plugin." +IDS_BROKENFSPLUGINVERSION2, "The plugin (%s) targets broken Sally SDK version 104 for file-system plugins.\nThis transitional FS ABI is unsafe and is no longer supported.\n\nPlease rebuild or reinstall an updated version of this plugin." IDS_UPDATEFAILED, "Packing of updated file(s) into archive has failed. Do you want to try it again? If you choose NO, you'll get back to the Archive Update dialog, so you'll be able to copy updated file(s) to chosen disk folder (for later packing into the archive)." diff --git a/src/main_window_commands_help.cpp b/src/main_window_commands_help.cpp index 87daa764e..4e1c83c2f 100644 --- a/src/main_window_commands_help.cpp +++ b/src/main_window_commands_help.cpp @@ -7,6 +7,7 @@ #include "ui/IPrompter.h" #include "common/IFileSystem.h" +#include "common/fsutil.h" #include "common/unicode/helpers.h" #include "common/IEnvironment.h" #include "common/IRegistry.h" @@ -755,6 +756,7 @@ void CMainWindow::PostChangeOnPathNotification(const char* path, BOOL includingS // add this notification to the array (for later processing) CChangeNotifData data; lstrcpyn(data.Path, path, MAX_PATH); + data.PathW = NULL; data.IncludingSubdirs = includingSubdirs; ChangeNotifArray.Add(data); if (!ChangeNotifArray.IsGood()) @@ -769,6 +771,61 @@ void CMainWindow::PostChangeOnPathNotification(const char* path, BOOL includingS HANDLES(LeaveCriticalSection(&DispachChangeNotifCS)); } +static wchar_t* DupWideChangeNotifPath(const wchar_t* path) +{ + if (path == NULL) + return NULL; + + size_t len = wcslen(path); + wchar_t* copy = (wchar_t*)malloc((len + 1) * sizeof(wchar_t)); + if (copy != NULL) + memcpy(copy, path, (len + 1) * sizeof(wchar_t)); + return copy; +} + +static void FreeChangeNotifData(CChangeNotifData& data) +{ + if (data.PathW != NULL) + { + free(data.PathW); + data.PathW = NULL; + } +} + +static void ClearPendingChangeNotifArray(TDirectArray& array) +{ + for (int i = 0; i < array.Count; i++) + FreeChangeNotifData(array[i]); + array.DestroyMembers(); + array.ResetState(); +} + +void CMainWindow::PostChangeOnPathNotificationW(const wchar_t* path, BOOL includingSubdirs) +{ + CALL_STACK_MESSAGE3("CMainWindow::PostChangeOnPathNotificationW(%S, %d)", path, includingSubdirs); + + HANDLES(EnterCriticalSection(&DispachChangeNotifCS)); + + CChangeNotifData data; + data.Path[0] = 0; + data.PathW = DupWideChangeNotifPath(path); + data.IncludingSubdirs = includingSubdirs; + if (data.PathW != NULL || path == NULL) + ChangeNotifArray.Add(data); + if (!ChangeNotifArray.IsGood()) + { + FreeChangeNotifData(data); + ChangeNotifArray.ResetState(); + } + + HANDLES(EnterCriticalSection(&TimeCounterSection)); + int t1 = MyTimeCounter++; + HANDLES(LeaveCriticalSection(&TimeCounterSection)); + PostMessage(HWindow, WM_USER_DISPACHCHANGENOTIF, 0, t1); + + HANDLES(LeaveCriticalSection(&DispachChangeNotifCS)); +} + void CMainWindowWindowProcAux(IContextMenu* menu2, CMINVOKECOMMANDINFO& ici) { CALL_STACK_MESSAGE_NONE @@ -3405,6 +3462,7 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = case CM_LEFTREFRESH: // refresh the left panel { LeftPanel->NextFocusName[0] = 0; + LeftPanel->NextFocusNameW.clear(); while (SnooperSuspended) EndSuspendMode(); // safety catch to resume refreshing while (StopRefresh) @@ -3422,6 +3480,7 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = case CM_RIGHTREFRESH: // refresh the right panel { RightPanel->NextFocusName[0] = 0; + RightPanel->NextFocusNameW.clear(); while (SnooperSuspended) EndSuspendMode(); // safety catch to resume refreshing while (StopRefresh) @@ -3439,6 +3498,7 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = case CM_ACTIVEREFRESH: // refresh the right panel { activePanel->NextFocusName[0] = 0; + activePanel->NextFocusNameW.clear(); while (SnooperSuspended) EndSuspendMode(); // safety catch to resume refreshing while (StopRefresh) @@ -4712,6 +4772,7 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = else { CPathBuffer path; // Heap-allocated for long path support + std::wstring pathW; BOOL includingSubdirs; BOOL ok = TRUE; while (1) @@ -4720,14 +4781,18 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = if (ChangeNotifArray.Count > 0) { CChangeNotifData* item = &ChangeNotifArray[ChangeNotifArray.Count - 1]; - strcpy(path, item->Path); + pathW = item->PathW != NULL ? item->PathW : AnsiToWide(item->Path); + if (item->PathW != NULL) + WideToAnsi(pathW, path, path.Size()); + else + strcpy(path, item->Path); includingSubdirs = item->IncludingSubdirs; + FreeChangeNotifData(*item); ChangeNotifArray.Delete(ChangeNotifArray.Count - 1); if (!ChangeNotifArray.IsGood()) { ChangeNotifArray.ResetState(); - ChangeNotifArray.DestroyMembers(); - ChangeNotifArray.ResetState(); + ClearPendingChangeNotifArray(ChangeNotifArray); ok = FALSE; } } @@ -4748,11 +4813,11 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = if (GetNonActivePanel() != NULL) // non-active panel first (due to timestamps of subdirectory changes on NTFS) { - GetNonActivePanel()->AcceptChangeOnPathNotification(path, includingSubdirs); + GetNonActivePanel()->AcceptChangeOnPathNotificationW(pathW.c_str(), includingSubdirs); } if (GetActivePanel() != NULL) // then the active panel { - GetActivePanel()->AcceptChangeOnPathNotification(path, includingSubdirs); + GetActivePanel()->AcceptChangeOnPathNotificationW(pathW.c_str(), includingSubdirs); } if (DetachedFSList->Count > 0) @@ -6184,11 +6249,17 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] = if (wParam == TRUE) // activating the app { if (!LeftPanel->DontClearNextFocusName) + { LeftPanel->NextFocusName[0] = 0; + LeftPanel->NextFocusNameW.clear(); + } else LeftPanel->DontClearNextFocusName = FALSE; if (!RightPanel->DontClearNextFocusName) + { RightPanel->NextFocusName[0] = 0; + RightPanel->NextFocusNameW.clear(); + } else RightPanel->DontClearNextFocusName = FALSE; if (Windows7AndLater && IsIconic(HWindow)) diff --git a/src/main_window_config_persistence.cpp b/src/main_window_config_persistence.cpp index cfae3fc77..65065d1d5 100644 --- a/src/main_window_config_persistence.cpp +++ b/src/main_window_config_persistence.cpp @@ -231,11 +231,13 @@ const char* CONFIG_VIEWERHISTORY_REG = "Viewer History"; const char* CONFIG_COMMANDHISTORY_REG = "Command History"; const char* CONFIG_SELECTHISTORY_REG = "Select History"; const char* CONFIG_COPYHISTORY_REG = "Copy History"; +const char* CONFIG_COPYHISTORYW_REG = "Copy History W"; const char* CONFIG_CHANGEDIRHISTORY_REG = "ChangeDir History"; const char* CONFIG_FILELISTHISTORY_REG = "File List History"; const char* CONFIG_CREATEDIRHISTORY_REG = "Create Directory History"; const char* CONFIG_QUICKRENAMEHISTORY_REG = "Quick Rename History"; const char* CONFIG_EDITNEWHISTORY_REG = "Edit New History"; +const char* CONFIG_CREATEDIRHISTORYW_REG = "Create Directory History W"; const char* CONFIG_QUICKRENAMEHISTORYW_REG = "Quick Rename History W"; const char* CONFIG_EDITNEWHISTORYW_REG = "Edit New History W"; const char* CONFIG_CONVERTHISTORY_REG = "Convert History"; @@ -1283,7 +1285,21 @@ void CMainWindow::SavePanelConfig(CFilesWindow* panel, HKEY hSalamander, const c DWORD value; value = panel->HeaderLineVisible; SetValue(actKey, PANEL_HEADER_REG, REG_DWORD, &value, sizeof(DWORD)); - SetValue(actKey, PANEL_PATH_REG, REG_SZ, panel->GetPath(), -1); + // Wide-only persistence: write the panel path as REG_SZ Unicode so Unicode-only + // disk roots (e.g. C:\Temp\zz中文) survive across restart. Older Sally builds + // reading this config will not find a usable PANEL_PATH and fall back to the + // rescue path on first launch (then resave). User-approved clean break. + if (gRegistry != NULL) + { + gRegistry->SetString(actKey, AnsiToWideReg(PANEL_PATH_REG).c_str(), + panel->GetPathW()); + } + else + { + // gRegistry should always be set in the running app; this is a defensive + // fallback so a misconfigured test harness still records something. + SetValue(actKey, PANEL_PATH_REG, REG_SZ, panel->GetPath(), -1); + } value = panel->GetViewTemplateIndex(); SetValue(actKey, PANEL_VIEW_REG, REG_DWORD, &value, sizeof(DWORD)); value = panel->SortType; @@ -1996,6 +2012,8 @@ void CMainWindow::SaveConfig(HWND parent) SELECT_HISTORY_SIZE, !Configuration.SaveHistory); SaveHistory(actKey, CONFIG_COPYHISTORY_REG, Configuration.CopyHistory, COPY_HISTORY_SIZE, !Configuration.SaveHistory); + SaveHistoryW(actKey, CONFIG_COPYHISTORYW_REG, Configuration.CopyHistoryW, + COPY_HISTORY_SIZE, !Configuration.SaveHistory); SaveHistory(actKey, CONFIG_CHANGEDIRHISTORY_REG, Configuration.ChangeDirHistory, CHANGEDIR_HISTORY_SIZE, !Configuration.SaveHistory); @@ -2013,6 +2031,8 @@ void CMainWindow::SaveConfig(HWND parent) FILELIST_HISTORY_SIZE, !Configuration.SaveHistory); SaveHistory(actKey, CONFIG_CREATEDIRHISTORY_REG, Configuration.CreateDirHistory, CREATEDIR_HISTORY_SIZE, !Configuration.SaveHistory); + SaveHistoryW(actKey, CONFIG_CREATEDIRHISTORYW_REG, Configuration.CreateDirHistoryW, + CREATEDIR_HISTORY_SIZE, !Configuration.SaveHistory); SaveHistory(actKey, CONFIG_QUICKRENAMEHISTORY_REG, Configuration.QuickRenameHistory, QUICKRENAME_HISTORY_SIZE, !Configuration.SaveHistory); SaveHistory(actKey, CONFIG_EDITNEWHISTORY_REG, Configuration.EditNewHistory, @@ -2327,37 +2347,30 @@ void CMainWindow::SaveConfig(HWND parent) } } -void CMainWindow::LoadPanelConfig(CPathBuffer& panelPath, CFilesWindow* panel, HKEY hSalamander, const char* reg) +void CMainWindow::LoadPanelConfig(CPathBuffer& panelPath, std::wstring& panelPathW, CFilesWindow* panel, HKEY hSalamander, const char* reg) { - auto readPanelPathViaRegistryService = [](HKEY key, CPathBuffer& outPath) -> BOOL - { - if (gRegistry == NULL || key == NULL) - return FALSE; - - std::wstring panelPathW; - RegistryResult result = gRegistry->GetString(key, AnsiToWideReg(PANEL_PATH_REG).c_str(), panelPathW); - if (!result.success) - return FALSE; - - int requiredChars = WideCharToMultiByte(CP_ACP, 0, panelPathW.c_str(), -1, NULL, 0, NULL, NULL); - if (requiredChars <= 0 || !outPath.EnsureCapacity(requiredChars)) - return FALSE; - - return WideCharToMultiByte(CP_ACP, 0, panelPathW.c_str(), -1, outPath.Get(), outPath.Size(), NULL, NULL) > 0; - }; - HKEY actKey; if (OpenKey(hSalamander, reg, actKey)) { DWORD value; - DWORD panelPathSize = 0; - BOOL panelPathLoaded = readPanelPathViaRegistryService(actKey, panelPath); - if (!panelPathLoaded && - GetSize(actKey, PANEL_PATH_REG, REG_SZ, panelPathSize) && - panelPath.EnsureCapacity((int)panelPathSize) && - GetValue(actKey, PANEL_PATH_REG, REG_SZ, panelPath.Get(), panelPath.Size())) + BOOL panelPathLoaded = FALSE; + // Read the panel path as REG_SZ Unicode (matches the wide-only save in + // SavePanelConfig). Keep panelPathW as the source-of-truth for the apply + // path; populate the legacy ANSI panelPath via WideToAnsi for any caller + // that still consumes it (only the rescue-path log paths today). Older + // Sally configs that wrote ANSI PANEL_PATH will now miss on the wide read + // and fall through to the rescue path on first launch — user-approved + // clean break. + if (gRegistry != NULL) { - panelPathLoaded = TRUE; + RegistryResult result = gRegistry->GetString(actKey, AnsiToWideReg(PANEL_PATH_REG).c_str(), panelPathW); + if (result.success) + { + panelPathLoaded = TRUE; + std::string panelPathA = WideToAnsi(panelPathW); + if (!panelPath.Assign(panelPathA.c_str())) + panelPath.Clear(); + } } if (panelPathLoaded) @@ -3658,15 +3671,23 @@ BOOL CMainWindow::LoadConfig(BOOL importingOldConfig, const CCommandLineParams* // if (Configuration.SelectHistory[0] != NULL) // load the initial state of +/- selection as well // strcpy(SelectionMask, Configuration.SelectHistory[0]); LoadHistory(actKey, CONFIG_COPYHISTORY_REG, Configuration.CopyHistory, COPY_HISTORY_SIZE); + LoadHistoryW(actKey, CONFIG_COPYHISTORYW_REG, Configuration.CopyHistoryW, COPY_HISTORY_SIZE); LoadHistory(actKey, CONFIG_CHANGEDIRHISTORY_REG, Configuration.ChangeDirHistory, CHANGEDIR_HISTORY_SIZE); LoadHistory(actKey, CONFIG_VIEWERHISTORY_REG, ViewerHistory, VIEWER_HISTORY_SIZE); LoadHistory(actKey, CONFIG_COMMANDHISTORY_REG, Configuration.EditHistory, EDIT_HISTORY_SIZE); LoadHistory(actKey, CONFIG_FILELISTHISTORY_REG, Configuration.FileListHistory, FILELIST_HISTORY_SIZE); + if (IsWideHistoryEmpty(Configuration.CopyHistoryW, COPY_HISTORY_SIZE)) + SeedWideHistoryFromAnsi(Configuration.CopyHistoryW, COPY_HISTORY_SIZE, + Configuration.CopyHistory, COPY_HISTORY_SIZE); LoadHistory(actKey, CONFIG_CREATEDIRHISTORY_REG, Configuration.CreateDirHistory, CREATEDIR_HISTORY_SIZE); + LoadHistoryW(actKey, CONFIG_CREATEDIRHISTORYW_REG, Configuration.CreateDirHistoryW, CREATEDIR_HISTORY_SIZE); LoadHistory(actKey, CONFIG_QUICKRENAMEHISTORY_REG, Configuration.QuickRenameHistory, QUICKRENAME_HISTORY_SIZE); LoadHistory(actKey, CONFIG_EDITNEWHISTORY_REG, Configuration.EditNewHistory, EDITNEW_HISTORY_SIZE); LoadHistoryW(actKey, CONFIG_QUICKRENAMEHISTORYW_REG, Configuration.QuickRenameHistoryW, QUICKRENAME_HISTORY_SIZE); LoadHistoryW(actKey, CONFIG_EDITNEWHISTORYW_REG, Configuration.EditNewHistoryW, EDITNEW_HISTORY_SIZE); + if (IsWideHistoryEmpty(Configuration.CreateDirHistoryW, CREATEDIR_HISTORY_SIZE)) + SeedWideHistoryFromAnsi(Configuration.CreateDirHistoryW, CREATEDIR_HISTORY_SIZE, + Configuration.CreateDirHistory, CREATEDIR_HISTORY_SIZE); if (IsWideHistoryEmpty(Configuration.QuickRenameHistoryW, QUICKRENAME_HISTORY_SIZE)) SeedWideHistoryFromAnsi(Configuration.QuickRenameHistoryW, QUICKRENAME_HISTORY_SIZE, Configuration.QuickRenameHistory, QUICKRENAME_HISTORY_SIZE); @@ -3791,10 +3812,16 @@ BOOL CMainWindow::LoadConfig(BOOL importingOldConfig, const CCommandLineParams* CPathBuffer rightPanelPath; EnvGetSystemDirectoryA(gEnvironment, leftPanelPath, leftPanelPath.Size()); strcpy(rightPanelPath, leftPanelPath); + // Wide source-of-truth for the panel path; populated by LoadPanelConfig from the + // REG_SZ Unicode value SavePanelConfig wrote. The ANSI mirror above remains for + // rescue-path bookkeeping only. The system directory is OS-supplied and ASCII, + // so AnsiToWide is safe as the seed value. + std::wstring leftPanelPathW = AnsiToWide(leftPanelPath); + std::wstring rightPanelPathW = AnsiToWide(rightPanelPath); CPathBuffer sysDefDir; lstrcpyn(sysDefDir, DefaultDir[LowerCase[leftPanelPath[0]] - 'a'], sysDefDir.Size()); - LoadPanelConfig(leftPanelPath, LeftPanel, salamander, SALAMANDER_LEFTP_REG); - LoadPanelConfig(rightPanelPath, RightPanel, salamander, SALAMANDER_RIGHTP_REG); + LoadPanelConfig(leftPanelPath, leftPanelPathW, LeftPanel, salamander, SALAMANDER_LEFTP_REG); + LoadPanelConfig(rightPanelPath, rightPanelPathW, RightPanel, salamander, SALAMANDER_RIGHTP_REG); CloseKey(salamander); salamander = NULL; @@ -3982,16 +4009,17 @@ BOOL CMainWindow::LoadConfig(BOOL importingOldConfig, const CCommandLineParams* if (rightPanelPathSet) RightPanel->RefreshVisibleItemsArray(); - // leftPanelPath and rightPanelPath are only disk paths; we don't store archives or FS paths + // leftPanelPath and rightPanelPath are only disk paths; we don't store archives or FS paths. + // Apply via the wide variants so Unicode-only roots survive validation and reopening. DWORD err, lastErr; BOOL pathInvalid, cut; BOOL tryNet = TRUE; if (!leftPanelPathSet) { - if (SalCheckAndRestorePathWithCut(LeftPanel->HWindow, leftPanelPath, tryNet, - err, lastErr, pathInvalid, cut, TRUE)) + if (SalCheckAndRestorePathWithCutW(LeftPanel->HWindow, leftPanelPathW, tryNet, + err, lastErr, pathInvalid, cut, TRUE)) { - LeftPanel->ChangePathToDisk(LeftPanel->HWindow, leftPanelPath); + LeftPanel->ChangePathToDiskW(LeftPanel->HWindow, leftPanelPathW.c_str()); } else LeftPanel->ChangeToRescuePathOrFixedDrive(LeftPanel->HWindow); @@ -4002,10 +4030,10 @@ BOOL CMainWindow::LoadConfig(BOOL importingOldConfig, const CCommandLineParams* tryNet = TRUE; if (!rightPanelPathSet) { - if (SalCheckAndRestorePathWithCut(RightPanel->HWindow, rightPanelPath, tryNet, - err, lastErr, pathInvalid, cut, TRUE)) + if (SalCheckAndRestorePathWithCutW(RightPanel->HWindow, rightPanelPathW, tryNet, + err, lastErr, pathInvalid, cut, TRUE)) { - RightPanel->ChangePathToDisk(RightPanel->HWindow, rightPanelPath); + RightPanel->ChangePathToDiskW(RightPanel->HWindow, rightPanelPathW.c_str()); } else RightPanel->ChangeToRescuePathOrFixedDrive(RightPanel->HWindow); diff --git a/src/main_window_dragdrop_shell.cpp b/src/main_window_dragdrop_shell.cpp index f422b4301..d8e8ea532 100644 --- a/src/main_window_dragdrop_shell.cpp +++ b/src/main_window_dragdrop_shell.cpp @@ -54,12 +54,15 @@ void CMainWindow::ClearPluginFSFromHistory(CPluginFSInterfaceAbstract* fs) void CMainWindow::DirHistoryAddPathUnique(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS) + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { if (CanAddToDirHistory) { DirHistory->AddPathUnique(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, hIcon, - pluginFS, curPluginFS); + pluginFS, curPluginFS, + pathOrArchiveOrFSNameW, archivePathOrFSUserPartW); if (LeftPanel != NULL) LeftPanel->DirectoryLine->SetHistory(DirHistory->HasPaths()); if (RightPanel != NULL) @@ -76,13 +79,15 @@ void CMainWindow::DirHistoryRemoveActualPath(CFilesWindow* panel) { if (panel->Is(ptZIPArchive)) { - DirHistory->RemoveActualPath(1, panel->GetZIPArchive(), panel->GetZIPPath(), NULL, NULL); + DirHistory->RemoveActualPath(1, panel->GetZIPArchive(), panel->GetZIPPath(), NULL, NULL, + panel->GetZIPArchiveW(), panel->GetZIPPathW()); } else { if (panel->Is(ptDisk)) { - DirHistory->RemoveActualPath(0, panel->GetPath(), NULL, NULL, NULL); + DirHistory->RemoveActualPath(0, panel->GetPath(), NULL, NULL, NULL, + panel->GetPathW(), nullptr); } else { diff --git a/src/main_window_ui_basics.cpp b/src/main_window_ui_basics.cpp index 10bd77d7b..307ddf218 100644 --- a/src/main_window_ui_basics.cpp +++ b/src/main_window_ui_basics.cpp @@ -22,6 +22,9 @@ #include "gui.h" #include "execute.h" #include "jumplist.h" +#include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" +#include "common/fsutil.h" #include "versinfo.rh2" @@ -293,6 +296,10 @@ BOOL CHotPathItems::Load1_52(HKEY hKey) CMainWindow::CMainWindow() : ChangeNotifArray(3, 5) { +#ifndef _UNICODE + UnicodeWnd = TRUE; // main window is registered with RegisterClassExW so the caption preserves Unicode + DefWndProc = GetDefWindowProc(); // re-derive after flipping UnicodeWnd so unhandled messages reach DefWindowProcW +#endif HANDLES(InitializeCriticalSection(&DispachChangeNotifCS)); LastDispachChangeNotifTime = 0; NeedToResentDispachChangeNotif = FALSE; @@ -1234,8 +1241,14 @@ void CMainWindow::RefreshDiskFreeSpace() void CMainWindow::RefreshDirs() { - LeftPanel->ChangePathToDisk(LeftPanel->HWindow, LeftPanel->GetPath()); - RightPanel->ChangePathToDisk(RightPanel->HWindow, RightPanel->GetPath()); + if (sally::unicode::HasWidePathW(LeftPanel->GetPathW())) + LeftPanel->ChangePathToDiskW(LeftPanel->HWindow, LeftPanel->GetPathW()); + else + LeftPanel->ChangePathToDisk(LeftPanel->HWindow, LeftPanel->GetPath()); + if (sally::unicode::HasWidePathW(RightPanel->GetPathW())) + RightPanel->ChangePathToDiskW(RightPanel->HWindow, RightPanel->GetPathW()); + else + RightPanel->ChangePathToDisk(RightPanel->HWindow, RightPanel->GetPath()); } // for passing the path to the configuration dialog @@ -1848,15 +1861,77 @@ void CMainWindow::GetFormatedPathForTitle(char* path) } } +static std::wstring GetFormatedPathForTitleW(CMainWindow* mainWindow) +{ + CFilesWindow* panel = mainWindow->GetActivePanel(); + if (panel == NULL) + return L""; + + if (!panel->Is(ptDisk) && !panel->Is(ptZIPArchive)) + { + CPathBuffer pathA; + mainWindow->GetFormatedPathForTitle(pathA); + return AnsiToWide(pathA); + } + + std::wstring pathW = panel->Is(ptDisk) ? panel->GetPathW() : panel->GetZIPArchiveW(); + if (panel->Is(ptZIPArchive) && panel->GetZIPPathW()[0] != 0) + { + if (panel->GetZIPPathW()[0] != L'\\') + pathW += L"\\"; + pathW += panel->GetZIPPathW(); + } + + switch (Configuration.TitleBarMode) + { + case TITLE_BAR_MODE_COMPOSITE: + { + std::wstring trimStart; + std::wstring trimEnd; + std::wstring rootPathW = GetRootPathW(pathW.c_str()); + size_t chars = rootPathW.size(); + size_t lastSlash = std::wstring::npos; + while (chars < pathW.size()) + { + if (pathW[chars] == L'\\' && chars + 1 < pathW.size()) + lastSlash = chars; + chars++; + } + if (!rootPathW.empty() && lastSlash != std::wstring::npos && lastSlash > rootPathW.size()) + pathW = pathW.substr(0, rootPathW.size()) + L"..." + pathW.substr(lastSlash); + break; + } + + case TITLE_BAR_MODE_DIRECTORY: + { + std::wstring rootPathW = GetRootPathW(pathW.c_str()); + size_t chars = rootPathW.size(); + size_t pos = pathW.find_last_of(L'\\'); + if (pos != std::wstring::npos && pos + 1 < pathW.size() && pos + 1 >= chars) + pathW = pathW.substr(pos + 1); + break; + } + + case TITLE_BAR_MODE_FULLPATH: + default: + break; + } + return pathW; +} + void CMainWindow::SetWindowTitle(const char* text) { CALL_STACK_MESSAGE2("CMainWindow::SetWindowTitle(%s)", text); + const BOOL useDefaultTitle = (text == NULL); char buff[1000]; ::GetWindowText(HWindow, buff, 1000); buff[999] = 0; + wchar_t buffW[1000]; + ::GetWindowTextW(HWindow, buffW, 1000); + buffW[999] = 0; CPathBuffer stdWndName; - if (text == NULL) + if (useDefaultTitle) { // provide default content stdWndName[0] = 0; @@ -1914,7 +1989,57 @@ void CMainWindow::SetWindowTitle(const char* text) text = stdWndName; } - if (strcmp(text, buff) != 0) + std::wstring textW; + if (useDefaultTitle) + { + std::wstring pathW = GetFormatedPathForTitleW(this); + + if (Configuration.UseTitleBarPrefixForced && Configuration.TitleBarPrefixForced[0] != 0) + { + textW += AnsiToWide(Configuration.TitleBarPrefixForced); + textW += L" - "; + } + else if (Configuration.UseTitleBarPrefix && Configuration.TitleBarPrefix[0] != 0) + { + textW += AnsiToWide(Configuration.TitleBarPrefix); + textW += L" - "; + } + + if (Configuration.TitleBarShowPath && !pathW.empty()) + { + textW += pathW; + textW += L" - "; + } + + textW += AnsiToWide(MAINWINDOW_NAME); +#if defined(GIT_VERSION_AVAILABLE) && defined(GIT_VERSION) + textW += L" "; + textW += AnsiToWide(GIT_VERSION); + textW += L" ("; + textW += AnsiToWide(SAL_VER_PLATFORM); + textW += L")"; +#else + textW += L" "; + textW += AnsiToWide(VERSINFO_VERSION); +#endif + if (RunningAsAdmin) + { + textW += L" ("; + textW += LoadStrW(IDS_AS_ADMIN_TITLE); + textW += L")"; + } +#ifdef X64_STRESS_TEST + textW += L" ST"; +#endif + } + + if (useDefaultTitle) + { + ::SetWindowTextW(HWindow, textW.c_str()); + if (Configuration.StatusArea) + SetTrayIconText(WideToAnsi(textW).c_str()); + } + else if (strcmp(text, buff) != 0) { ::SetWindowText(HWindow, text); if (Configuration.StatusArea) diff --git a/src/mainwnd.h b/src/mainwnd.h index b648499e1..b57241763 100644 --- a/src/mainwnd.h +++ b/src/mainwnd.h @@ -308,6 +308,7 @@ enum CMainWindowsHitTestEnum struct CChangeNotifData { char Path[MAX_PATH]; + wchar_t* PathW; BOOL IncludingSubdirs; }; @@ -484,6 +485,7 @@ class CMainWindow : public CMainWindowAncestor // and to all opened FS from plugins (both panels and FS can respond by refreshing their content); // can be called from any thread void PostChangeOnPathNotification(const char* path, BOOL includingSubdirs); + void PostChangeOnPathNotificationW(const wchar_t* path, BOOL includingSubdirs); // these functions have no effect if CFilesWindow::CanBeFocused is not satisfied void ChangePanel(BOOL force = FALSE); // respects EditMode; activates the inactive panel; (ignores ZOOM if force is TRUE) @@ -494,10 +496,15 @@ class CMainWindow : public CMainWindowAncestor void CompareDirectories(DWORD flags); // flags are a combination of COMPARE_DIRECTORIES_xxx // ensures DirHistory->AddPathUnique is called and correctly updates the panel's SetHistory + // pathOrArchiveOrFSNameW / archivePathOrFSUserPartW carry the wide source-of-truth so the + // history can replay through ChangePathToDiskW / ChangePathToArchiveW; pass nullptr when + // only the ANSI byte stream is available (plugin FS today). void DirHistoryAddPathUnique(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS); + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); // ensures DirHistory->RemoveActualPath is called and correctly updates the panel's SetHistory void DirHistoryRemoveActualPath(CFilesWindow* panel); @@ -516,7 +523,7 @@ class CMainWindow : public CMainWindowAncestor void SaveConfig(HWND parent = NULL); // parent: NULL = MainWindow->HWindow BOOL LoadConfig(BOOL importingOldConfig, const CCommandLineParams* cmdLineParams); void SavePanelConfig(CFilesWindow* panel, HKEY hSalamander, const char* reg); - void LoadPanelConfig(CPathBuffer& panelPath, CFilesWindow* panel, HKEY hSalamander, const char* reg); + void LoadPanelConfig(CPathBuffer& panelPath, std::wstring& panelPathW, CFilesWindow* panel, HKEY hSalamander, const char* reg); void DeleteOldConfigurations(BOOL* deleteConfigurations, BOOL autoImportConfig, const char* autoImportConfigFromKey, BOOL doNotDeleteImportedCfg); diff --git a/src/plugins.h b/src/plugins.h index 72b52e3b0..64c72d3f3 100644 --- a/src/plugins.h +++ b/src/plugins.h @@ -1103,6 +1103,16 @@ class CPluginFSInterfaceEncapsulation return r; } + BOOL GetCurrentPathW(std::wstring& userPart); + BOOL GetFullNameW(CFileData& file, int isDir, std::wstring& fullName); + BOOL GetFullFSPathW(HWND parent, const std::wstring& fsName, std::wstring& path, BOOL& success); + BOOL GetRootPathW(std::wstring& userPart); + BOOL IsCurrentPathW(int currentFSNameIndex, int fsNameIndex, const std::wstring& userPart); + BOOL IsOurPathW(int currentFSNameIndex, int fsNameIndex, const std::wstring& userPart); + BOOL ChangePathW(int currentFSNameIndex, std::wstring& fsName, int fsNameIndex, + const std::wstring& userPart, std::wstring* cutFileName, + BOOL* pathWasCut, BOOL forceRefresh, int mode); + // in the debug version we count OpenedPDCounter, therefore it must be in the .cpp module BOOL ListCurrentPath(CSalamanderDirectoryAbstract* dir, CPluginDataInterfaceAbstract*& pluginData, diff --git a/src/plugins/regedt/fs_paths.cpp b/src/plugins/regedt/fs_paths.cpp index 338bf1083..0904971f0 100644 --- a/src/plugins/regedt/fs_paths.cpp +++ b/src/plugins/regedt/fs_paths.cpp @@ -45,7 +45,7 @@ BOOL CPluginFSInterface::SetNewPath(WCHAR* newPath) return TRUE; } -BOOL CPluginFSInterface::GetCurrentPathW(WCHAR* userPart, int size) +BOOL WINAPI CPluginFSInterface::GetCurrentPathW(wchar_t* userPart, int size) { CALL_STACK_MESSAGE2("CPluginFSInterface::GetCurrentPathW(, %d)", size); // !!! report an error if the buffer is too small diff --git a/src/plugins/regedt/regedt.h b/src/plugins/regedt/regedt.h index 6b56828dd..6a102bc50 100644 --- a/src/plugins/regedt/regedt.h +++ b/src/plugins/regedt/regedt.h @@ -317,11 +317,21 @@ class CPluginFSInterface : public CPluginFSInterfaceAbstract BOOL SetNewPath(WCHAR* newPath); - BOOL GetCurrentPathW(WCHAR* userPart, int size); + // Wide-native implementation that overrides the base bridge. The base + // virtual at spl_fs.h:796 takes (wchar_t*, int) with WINAPI; matching the + // calling convention is required so x86 does not reject this as C2695. + virtual BOOL WINAPI GetCurrentPathW(wchar_t* userPart, int size) override; virtual BOOL WINAPI GetCurrentPath(char* userPart); virtual BOOL WINAPI GetFullName(CFileData& file, int isDir, char* buf, int bufSize); + // regedt-internal 3-arg helper. NOTE: this hides the inherited 5-arg + // base virtual GetFullFSPathW(HWND, const wchar_t*, wchar_t*, int, BOOL&) + // by C++ name-lookup rules. That is acceptable today because all in-tree + // callers go through this 3-arg helper; Salamander framework code reaches + // the base virtual via CPluginFSInterfaceAbstract* (vtable dispatch). + // If a future change needs to override the 5-arg variant from regedt, + // rename this helper (e.g. BuildFullFSPathW) and add an explicit override. BOOL GetFullFSPathW(WCHAR* path, int pathSize, BOOL& success); virtual BOOL WINAPI GetFullFSPath(HWND parent, const char* fsName, char* path, int pathSize, BOOL& success); diff --git a/src/plugins/shared/spl_fs.h b/src/plugins/shared/spl_fs.h index 7961ef262..7d815ae2c 100644 --- a/src/plugins/shared/spl_fs.h +++ b/src/plugins/shared/spl_fs.h @@ -231,6 +231,43 @@ class CSalamanderForViewFileOnFSAbstract #define SALCMDLINE_MAXLEN 8192 // maximum length of command from Salamander command line +inline BOOL SPLFSWideToAnsiExact(const wchar_t* src, char* dst, int dstSize) +{ + if (dst == NULL || dstSize <= 0) + return FALSE; + dst[0] = 0; + if (src == NULL) + return TRUE; + + BOOL usedDefaultChar = FALSE; + int converted = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, src, -1, dst, dstSize, NULL, &usedDefaultChar); + if (converted == 0 || usedDefaultChar) + { + dst[0] = 0; + return FALSE; + } + dst[dstSize - 1] = 0; + return TRUE; +} + +inline BOOL SPLFSAnsiToWide(const char* src, wchar_t* dst, int dstSize) +{ + if (dst == NULL || dstSize <= 0) + return FALSE; + dst[0] = 0; + if (src == NULL) + return TRUE; + + int converted = MultiByteToWideChar(CP_ACP, 0, src, -1, dst, dstSize); + if (converted == 0) + { + dst[0] = 0; + return FALSE; + } + dst[dstSize - 1] = 0; + return TRUE; +} + class CPluginFSInterfaceAbstract { #ifdef INSIDE_SALAMANDER @@ -753,6 +790,91 @@ class CPluginFSInterfaceAbstract // e.g. FTPS displays a dialog with the server certificate); 'parent' is the suggested parent of the dialog virtual void WINAPI ShowSecurityInfo(HWND parent) = 0; + // Optional wide path ABI. Plugins built for SALLY_PLUGIN_WIDE_FS_VERSION or newer may + // override these methods to preserve exact UTF-16 paths. The default implementations + // bridge to the legacy ANSI methods only when the conversion is exact. + virtual BOOL WINAPI GetCurrentPathW(wchar_t* userPart, int userPartSize) + { + char userPartA[MAX_PATH]; + if (!GetCurrentPath(userPartA)) + return FALSE; + return SPLFSAnsiToWide(userPartA, userPart, userPartSize); + } + + virtual BOOL WINAPI GetFullNameW(CFileData& file, int isDir, wchar_t* buf, int bufSize) + { + char bufA[MAX_PATH]; + if (!GetFullName(file, isDir, bufA, MAX_PATH)) + return FALSE; + return SPLFSAnsiToWide(bufA, buf, bufSize); + } + + virtual BOOL WINAPI GetFullFSPathW(HWND parent, const wchar_t* fsName, wchar_t* path, int pathSize, + BOOL& success) + { + char fsNameA[MAX_PATH]; + char pathA[MAX_PATH]; + if (!SPLFSWideToAnsiExact(fsName, fsNameA, MAX_PATH) || + !SPLFSWideToAnsiExact(path, pathA, MAX_PATH)) + { + success = FALSE; + return FALSE; + } + BOOL ret = GetFullFSPath(parent, fsNameA, pathA, MAX_PATH, success); + if (ret && success) + return SPLFSAnsiToWide(pathA, path, pathSize); + return ret; + } + + virtual BOOL WINAPI GetRootPathW(wchar_t* userPart, int userPartSize) + { + char userPartA[MAX_PATH]; + if (!GetRootPath(userPartA)) + return FALSE; + return SPLFSAnsiToWide(userPartA, userPart, userPartSize); + } + + virtual BOOL WINAPI IsCurrentPathW(int currentFSNameIndex, int fsNameIndex, const wchar_t* userPart) + { + char userPartA[MAX_PATH]; + return SPLFSWideToAnsiExact(userPart, userPartA, MAX_PATH) && + IsCurrentPath(currentFSNameIndex, fsNameIndex, userPartA); + } + + virtual BOOL WINAPI IsOurPathW(int currentFSNameIndex, int fsNameIndex, const wchar_t* userPart) + { + char userPartA[MAX_PATH]; + return SPLFSWideToAnsiExact(userPart, userPartA, MAX_PATH) && + IsOurPath(currentFSNameIndex, fsNameIndex, userPartA); + } + + virtual BOOL WINAPI ChangePathW(int currentFSNameIndex, wchar_t* fsName, int fsNameIndex, + const wchar_t* userPart, wchar_t* cutFileName, int cutFileNameSize, + BOOL* pathWasCut, BOOL forceRefresh, int mode) + { + char fsNameA[MAX_PATH]; + char userPartA[MAX_PATH]; + char cutFileNameA[MAX_PATH]; + if (!SPLFSWideToAnsiExact(fsName, fsNameA, MAX_PATH) || + !SPLFSWideToAnsiExact(userPart, userPartA, MAX_PATH)) + { + if (cutFileName != NULL && cutFileNameSize > 0) + cutFileName[0] = 0; + return FALSE; + } + cutFileNameA[0] = 0; + BOOL ret = ChangePath(currentFSNameIndex, fsNameA, fsNameIndex, userPartA, + cutFileName != NULL ? cutFileNameA : NULL, + pathWasCut, forceRefresh, mode); + if (ret) + { + SPLFSAnsiToWide(fsNameA, fsName, MAX_PATH); + if (cutFileName != NULL) + SPLFSAnsiToWide(cutFileNameA, cutFileName, cutFileNameSize); + } + return ret; + } + /* remaining to be completed: // calculate occupied space on FS (Alt+F10 + Ctrl+Shift+F10 + calc. needed space + spacebar key in panel) #define FS_SERVICE_CALCULATEOCCUPIEDSPACE diff --git a/src/plugins/shared/spl_vers.h b/src/plugins/shared/spl_vers.h index 4041a7209..9bceb8723 100644 --- a/src/plugins/shared/spl_vers.h +++ b/src/plugins/shared/spl_vers.h @@ -202,8 +202,12 @@ // 101 - 4.0 beta 1 (DB177) // 102 - 4.0 // 103 - 5.0 +// 104 - 5.0 + broken transitional wide plugin FS ABI (must not be used for FS plugins) +// 105 - 5.0 + fixed optional wide plugin FS path ABI -#define LAST_VERSION_OF_SALAMANDER 103 +#define LAST_VERSION_OF_SALAMANDER 105 +#define SALLY_PLUGIN_BROKEN_WIDE_FS_VERSION 104 +#define SALLY_PLUGIN_WIDE_FS_VERSION 105 #define REQUIRE_LAST_VERSION_OF_SALAMANDER "This plugin requires Sally 1.0 (" SAL_VER_PLATFORM ") or later." #endif // __SPL_VERS_H diff --git a/src/plugins_fs_encapsulation.cpp b/src/plugins_fs_encapsulation.cpp index f660c9fad..41bf5e6d4 100644 --- a/src/plugins_fs_encapsulation.cpp +++ b/src/plugins_fs_encapsulation.cpp @@ -7,6 +7,8 @@ #include "ui/IPrompter.h" #include "common/IRegistry.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" +#include "plugins/shared/spl_vers.h" #include "menu.h" #include "cfgdlg.h" #include "plugins.h" @@ -21,6 +23,14 @@ CPlugins Plugins; +static bool ShouldRejectBrokenWideFSPlugin(CPluginInterfaceEncapsulation& pluginIface, + int builtForVersion) +{ + return builtForVersion == SALLY_PLUGIN_BROKEN_WIDE_FS_VERSION && + pluginIface.NotEmpty() && + pluginIface.GetInterfaceForFS() != NULL; +} + static IRegistry* GetPluginsRegistry() { return gRegistry != nullptr ? gRegistry : GetWin32Registry(); @@ -118,6 +128,194 @@ BOOL CPluginFSInterfaceEncapsulation::IsPathFromThisFS(const char* fsName, const return FALSE; // not our path } +BOOL CPluginFSInterfaceEncapsulation::GetCurrentPathW(std::wstring& userPart) +{ + CALL_STACK_MESSAGE3("CPluginFSInterfaceEncapsulation::GetCurrentPathW() (%s v. %s)", + DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + std::vector buf(SAL_MAX_LONG_PATH); + EnterPlugin(); + BOOL r = Interface->GetCurrentPathW(buf.data(), (int)buf.size()); + LeavePlugin(); + if (r) + userPart = buf.data(); + return r; + } + + CPathBuffer userPartA; + if (!GetCurrentPath(userPartA)) + return FALSE; + userPart = AnsiToWide(userPartA); + return TRUE; +} + +BOOL CPluginFSInterfaceEncapsulation::GetFullNameW(CFileData& file, int isDir, std::wstring& fullName) +{ + CALL_STACK_MESSAGE4("CPluginFSInterfaceEncapsulation::GetFullNameW(, %d, ,) (%s v. %s)", + isDir, DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + std::vector buf(SAL_MAX_LONG_PATH); + EnterPlugin(); + BOOL r = Interface->GetFullNameW(file, isDir, buf.data(), (int)buf.size()); + LeavePlugin(); + if (r) + fullName = buf.data(); + return r; + } + + CPathBuffer fullNameA; + if (!GetFullName(file, isDir, fullNameA, fullNameA.Size())) + return FALSE; + fullName = AnsiToWide(fullNameA); + return TRUE; +} + +BOOL CPluginFSInterfaceEncapsulation::GetFullFSPathW(HWND parent, const std::wstring& fsName, + std::wstring& path, BOOL& success) +{ + CALL_STACK_MESSAGE3("CPluginFSInterfaceEncapsulation::GetFullFSPathW() (%s v. %s)", + DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + std::vector pathBuf(SAL_MAX_LONG_PATH); + lstrcpynW(pathBuf.data(), path.c_str(), (int)pathBuf.size()); + EnterPlugin(); + BOOL r = Interface->GetFullFSPathW(parent, fsName.c_str(), pathBuf.data(), (int)pathBuf.size(), success); + LeavePlugin(); + if (r && success) + path = pathBuf.data(); + return r; + } + + std::string fsNameA; + std::string pathA; + if (!sally::unicode::TryExactAnsiFallback(fsName, fsNameA) || + !sally::unicode::TryExactAnsiFallback(path, pathA)) + { + success = FALSE; + return FALSE; + } + + CPathBuffer pathBuf(pathA.c_str()); + BOOL r = GetFullFSPath(parent, fsNameA.c_str(), pathBuf, pathBuf.Size(), success); + if (r && success) + path = AnsiToWide(pathBuf); + return r; +} + +BOOL CPluginFSInterfaceEncapsulation::GetRootPathW(std::wstring& userPart) +{ + CALL_STACK_MESSAGE3("CPluginFSInterfaceEncapsulation::GetRootPathW() (%s v. %s)", + DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + std::vector buf(SAL_MAX_LONG_PATH); + EnterPlugin(); + BOOL r = Interface->GetRootPathW(buf.data(), (int)buf.size()); + LeavePlugin(); + if (r) + userPart = buf.data(); + return r; + } + + CPathBuffer userPartA; + if (!GetRootPath(userPartA)) + return FALSE; + userPart = AnsiToWide(userPartA); + return TRUE; +} + +BOOL CPluginFSInterfaceEncapsulation::IsCurrentPathW(int currentFSNameIndex, int fsNameIndex, + const std::wstring& userPart) +{ + CALL_STACK_MESSAGE5("CPluginFSInterfaceEncapsulation::IsCurrentPathW(%d, %d,) (%s v. %s)", + currentFSNameIndex, fsNameIndex, DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + EnterPlugin(); + BOOL r = Interface->IsCurrentPathW(currentFSNameIndex, fsNameIndex, userPart.c_str()); + LeavePlugin(); + return r; + } + + std::string userPartA; + return sally::unicode::TryExactAnsiFallback(userPart, userPartA) && + IsCurrentPath(currentFSNameIndex, fsNameIndex, userPartA.c_str()); +} + +BOOL CPluginFSInterfaceEncapsulation::IsOurPathW(int currentFSNameIndex, int fsNameIndex, + const std::wstring& userPart) +{ + CALL_STACK_MESSAGE5("CPluginFSInterfaceEncapsulation::IsOurPathW(%d, %d,) (%s v. %s)", + currentFSNameIndex, fsNameIndex, DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + EnterPlugin(); + BOOL r = Interface->IsOurPathW(currentFSNameIndex, fsNameIndex, userPart.c_str()); + LeavePlugin(); + return r; + } + + std::string userPartA; + return sally::unicode::TryExactAnsiFallback(userPart, userPartA) && + IsOurPath(currentFSNameIndex, fsNameIndex, userPartA.c_str()); +} + +BOOL CPluginFSInterfaceEncapsulation::ChangePathW(int currentFSNameIndex, std::wstring& fsName, + int fsNameIndex, const std::wstring& userPart, + std::wstring* cutFileName, BOOL* pathWasCut, + BOOL forceRefresh, int mode) +{ + CALL_STACK_MESSAGE7("CPluginFSInterfaceEncapsulation::ChangePathW(%d, , %d, , , , %d, %d) (%s v. %s)", + currentFSNameIndex, fsNameIndex, forceRefresh, mode, DLLName, Version); + if (BuiltForVersion >= SALLY_PLUGIN_WIDE_FS_VERSION) + { + wchar_t fsNameBuf[MAX_PATH]; + wchar_t cutFileNameBuf[SAL_MAX_LONG_PATH]; + lstrcpynW(fsNameBuf, fsName.c_str(), MAX_PATH); + cutFileNameBuf[0] = 0; + EnterPlugin(); + BOOL r = Interface->ChangePathW(currentFSNameIndex, fsNameBuf, fsNameIndex, userPart.c_str(), + cutFileName != NULL ? cutFileNameBuf : NULL, + SAL_MAX_LONG_PATH, pathWasCut, forceRefresh, mode); + CALL_STACK_MESSAGE1("CPluginFSInterface::GetSupportedServices()"); + SupportedServices = Interface->GetSupportedServices(); + LeavePlugin(); + if (r) + { + fsName = fsNameBuf; + if (cutFileName != NULL) + *cutFileName = cutFileNameBuf; + } + return r; + } + + std::string fsNameA; + std::string userPartA; + if (!sally::unicode::TryExactAnsiFallback(fsName, fsNameA) || + !sally::unicode::TryExactAnsiFallback(userPart, userPartA)) + { + if (cutFileName != NULL) + cutFileName->clear(); + return FALSE; + } + + CPathBuffer fsNameBuf(fsNameA.c_str()); + CPathBuffer cutFileNameBuf; + BOOL r = ChangePath(currentFSNameIndex, fsNameBuf, fsNameIndex, userPartA.c_str(), + cutFileName != NULL ? cutFileNameBuf.Get() : NULL, + pathWasCut, forceRefresh, mode); + if (r) + { + fsName = AnsiToWide(fsNameBuf); + if (cutFileName != NULL) + *cutFileName = AnsiToWide(cutFileNameBuf); + } + return r; +} + BOOL CPluginFSInterfaceEncapsulation::ListCurrentPath(CSalamanderDirectoryAbstract* dir, CPluginDataInterfaceAbstract*& pluginData, int& iconsType, BOOL forceRefresh) @@ -2107,6 +2305,7 @@ BOOL CPluginData::InitDLL(HWND parent, BOOL quiet, BOOL waitCursor, BOOL showUns LeavePlugin(); SalamanderGeneral.Init(PluginIface.GetInterface()); + BOOL pluginNethoodChanged = FALSE; if (!PluginIface.NotEmpty()) { PluginIsNethood = oldPluginIsNethood; // when the entry point fails, we restore the original value @@ -2114,9 +2313,29 @@ BOOL CPluginData::InitDLL(HWND parent, BOOL quiet, BOOL waitCursor, BOOL showUns } else { - if (PluginIsNethood != oldPluginIsNethood) + pluginNethoodChanged = PluginIsNethood != oldPluginIsNethood; + if (pluginNethoodChanged) refreshUNCRootPaths = TRUE; } + + if (!oldVer && ShouldRejectBrokenWideFSPlugin(PluginIface, BuiltForVersion)) + { + PluginIsNethood = oldPluginIsNethood; + PluginUsesPasswordManager = oldPluginUsesPasswordManager; + if (pluginNethoodChanged) + refreshUNCRootPaths = FALSE; + oldVer = TRUE; + suppressOldVerError = TRUE; + if (!quiet) + { + std::wstring msg; + if (Name.empty() || Name[0] == 0) + msg = FormatStrW(LoadStrW(IDS_BROKENFSPLUGINVERSION2), AnsiToWide(s).c_str()); + else + msg = FormatStrW(LoadStrW(IDS_BROKENFSPLUGINVERSION), AnsiToWide(Name.c_str()).c_str(), AnsiToWide(s).c_str()); + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), msg.c_str()); + } + } } else // probably unnecessary, just to be safe { diff --git a/src/salamand.h b/src/salamand.h index 90b7e2cbe..2a2521c56 100644 --- a/src/salamand.h +++ b/src/salamand.h @@ -136,8 +136,14 @@ class CPathHistoryItem { protected: int Type; // type: 0 is a disk, 1 is an archive, 2 is FS - std::string PathOrArchiveOrFSName; // disk path or archive name or FS name + std::string PathOrArchiveOrFSName; // disk path or archive name or FS name (legacy ANSI mirror) std::string ArchivePathOrFSUserPart; // path in an archive or the user part of an FS path + // Wide source-of-truth twins. Populated for Type==0 (disk) and Type==1 (archive) + // when the recorder has the panel's PathW / ZIPArchiveW / ZIPPathW available. + // Empty for Type==2 (plugin FS) until plugin FS user-parts gain wide support. + // Empty also when populated from an old code path that only had ANSI bytes. + std::wstring PathOrArchiveOrFSNameW; + std::wstring ArchivePathOrFSUserPartW; HICON HIcon; // icon corresponding to the path (may be NULL); the icon will be destroyed in the destructor CPluginFSInterfaceAbstract* PluginFS; // only for Type==2: the last used interface for the FS path @@ -147,7 +153,9 @@ class CPathHistoryItem public: CPathHistoryItem(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, - CPluginFSInterfaceAbstract* pluginFS); + CPluginFSInterfaceAbstract* pluginFS, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); ~CPathHistoryItem(); // change of top index and focused name (repeated addition of one path to the history) @@ -180,14 +188,23 @@ class CPathHistory // clears all history entries void ClearHistory(); - // adds a path to the history + // adds a path to the history. + // pathOrArchiveOrFSNameW / archivePathOrFSUserPartW carry the wide source-of-truth + // when available (disk: panel->GetPathW(); archive: panel->GetZIPArchiveW() and + // panel->GetZIPPathW()); pass nullptr for plugin FS or when only ANSI bytes are + // available. The wide twins are what Execute() replays through ChangePathToDiskW / + // ChangePathToArchiveW so that Unicode-only roots survive round-tripping. void AddPath(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, - CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS); + CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); // adds a path to the history only if the path is not already present (see Alt+F12; for FS it overwrites pluginFS with the newest one) void AddPathUnique(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS); + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); // changes the data (top index and focused name) of the current path only if the given path // matches the current path in the history @@ -195,14 +212,18 @@ class CPathHistory const char* archivePathOrFSUserPart, CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS, - int topIndex, const char* focusedName); + int topIndex, const char* focusedName, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); // deletes the current path from the history only if the given path matches the current // path in the history void RemoveActualPath(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS); + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW = nullptr, + const wchar_t* archivePathOrFSUserPartW = nullptr); // populates the menu with items // IDs will start from one and correspond to the index parameter when calling the Execute() method @@ -1116,9 +1137,11 @@ BOOL FindLanguageFromPrevVerOfSal(char* slgName); BOOL CreateKeyForwarder(HWND hDialog, int ctrlID); // call after receiving the WM_USER_KEYDOWN message; returns TRUE if the key was processed DWORD OnDirectoryKeyDown(DWORD keyCode, HWND hDialog, int editID, int editBufSize, int buttonID); +DWORD OnDirectoryKeyDownW(DWORD keyCode, HWND hDialog, int editID, int editBufSize, int buttonID, HWND hUnicodeCtrl); // call after receiving the WM_USER_BUTTON message; ensures the menu behind the 'buttonID' button is opened // and subsequently fills the 'editID' edit line void OnDirectoryButton(HWND hDialog, int editID, int editBufSize, int buttonID, WPARAM wParam, LPARAM lParam); +void OnDirectoryButtonW(HWND hDialog, int editID, int editBufSize, int buttonID, WPARAM wParam, LPARAM lParam, HWND hUnicodeCtrl); // call after receiving the WM_USER_BUTTON message; ensures Ctrl+A works on systems up to Windows Vista, // where the shortcut is already supported system-wide diff --git a/src/salamander_entry_lifecycle.cpp b/src/salamander_entry_lifecycle.cpp index 81ccb1bcc..4cbfec06d 100644 --- a/src/salamander_entry_lifecycle.cpp +++ b/src/salamander_entry_lifecycle.cpp @@ -229,6 +229,7 @@ const char* SALCF_FAKE_SRCFSPATH = "SalFakeSrcFSPath"; const char* MAINWINDOW_NAME = "Sally"; const char* CMAINWINDOW_CLASSNAME = "SalamanderMainWindowVer25"; +const wchar_t* CMAINWINDOW_CLASSNAMEW = L"SalamanderMainWindowVer25"; const char* SAVEBITS_CLASSNAME = "SalamanderSaveBits"; const char* SHELLEXECUTE_CLASSNAME = "SalamanderShellExecute"; @@ -4397,29 +4398,29 @@ int WinMainBody(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR cmdLine, NULL, CFILESBOX_CLASSNAME, NULL) && - CMainWindow::RegisterUniversalClass(CS_DBLCLKS, - 0, - 0, - HANDLES(LoadIcon(HInstance, - MAKEINTRESOURCE(IDI_SALAMANDER))), - LoadCursor(NULL, IDC_ARROW), - (HBRUSH)(COLOR_WINDOW + 1), - NULL, - CMAINWINDOW_CLASSNAME, - NULL)) + CMainWindow::RegisterUniversalClassW(CS_DBLCLKS, + 0, + 0, + HANDLES(LoadIcon(HInstance, + MAKEINTRESOURCE(IDI_SALAMANDER))), + LoadCursor(NULL, IDC_ARROW), + (HBRUSH)(COLOR_WINDOW + 1), + NULL, + CMAINWINDOW_CLASSNAMEW, + NULL)) { MainWindow = new CMainWindow; if (MainWindow != NULL) { MainWindow->CmdShow = cmdShow; - if (MainWindow->Create(CMAINWINDOW_CLASSNAME, - "", - WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, - CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, - NULL, - NULL, - HInstance, - MainWindow)) + if (MainWindow->CreateW(CMAINWINDOW_CLASSNAMEW, + L"", + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, + NULL, + NULL, + HInstance, + MainWindow)) { SetMessagesParent(MainWindow->HWindow); PluginMsgBoxParent = MainWindow->HWindow; diff --git a/src/salamander_path_utils.cpp b/src/salamander_path_utils.cpp index 9e5c43ef9..23df80415 100644 --- a/src/salamander_path_utils.cpp +++ b/src/salamander_path_utils.cpp @@ -16,10 +16,15 @@ #include "shellib.h" #include "menu.h" #include "common/widepath.h" +#include "common/fsutil.h" +#include "common/WorkdirsHistorySerializer.h" + +#include #include "ui/IPrompter.h" #include "common/IFileSystem.h" #include "common/IEnvironment.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" CUserMenuIconBkgndReader UserMenuIconBkgndReader; @@ -1703,7 +1708,9 @@ CToolTipWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) CPathHistoryItem::CPathHistoryItem(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, - CPluginFSInterfaceAbstract* pluginFS) + CPluginFSInterfaceAbstract* pluginFS, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { Type = type; HIcon = hIcon; @@ -1727,6 +1734,24 @@ CPathHistoryItem::CPathHistoryItem(int type, const char* pathOrArchiveOrFSName, { PathOrArchiveOrFSName = root.Get(); } + if (pathOrArchiveOrFSNameW != nullptr && pathOrArchiveOrFSNameW[0] != L'\0') + { + // Mirror the ANSI normalization above for the wide twin: drop the + // trailing backslash unless the path is exactly a root. + std::wstring rootW = GetRootPathW(pathOrArchiveOrFSNameW); + const wchar_t* eW = pathOrArchiveOrFSNameW + wcslen(pathOrArchiveOrFSNameW); + if ((int)rootW.size() < eW - pathOrArchiveOrFSNameW || + pathOrArchiveOrFSNameW[0] == L'\\') + { + if (eW > pathOrArchiveOrFSNameW && *(eW - 1) == L'\\') + --eW; + PathOrArchiveOrFSNameW.assign(pathOrArchiveOrFSNameW, eW - pathOrArchiveOrFSNameW); + } + else + { + PathOrArchiveOrFSNameW = std::move(rootW); + } + } } else { @@ -1736,6 +1761,10 @@ CPathHistoryItem::CPathHistoryItem(int type, const char* pathOrArchiveOrFSName, PluginFS = pluginFS; PathOrArchiveOrFSName = pathOrArchiveOrFSName; ArchivePathOrFSUserPart = archivePathOrFSUserPart; + if (pathOrArchiveOrFSNameW != nullptr && pathOrArchiveOrFSNameW[0] != L'\0') + PathOrArchiveOrFSNameW = pathOrArchiveOrFSNameW; + if (archivePathOrFSUserPartW != nullptr && archivePathOrFSUserPartW[0] != L'\0') + ArchivePathOrFSUserPartW = archivePathOrFSUserPartW; } else TRACE_E("CPathHistoryItem::CPathHistoryItem(): unknown 'type'"); @@ -1889,8 +1918,19 @@ BOOL CPathHistoryItem::Execute(CFilesWindow* panel) BOOL clear = TRUE; if (Type == 0) // disk { - if (!panel->ChangePathToDisk(panel->HWindow, PathOrArchiveOrFSName.c_str(), TopIndex, FocusedName.empty() ? NULL : FocusedName.c_str(), NULL, - TRUE, FALSE, FALSE, &failReason)) + BOOL diskOk; + if (sally::unicode::HasWidePathW(PathOrArchiveOrFSNameW.c_str())) + { + diskOk = panel->ChangePathToDiskW(panel->HWindow, PathOrArchiveOrFSNameW.c_str(), TopIndex, + FocusedName.empty() ? NULL : FocusedName.c_str(), NULL, + TRUE, FALSE, FALSE, &failReason); + } + else + { + diskOk = panel->ChangePathToDisk(panel->HWindow, PathOrArchiveOrFSName.c_str(), TopIndex, FocusedName.empty() ? NULL : FocusedName.c_str(), NULL, + TRUE, FALSE, FALSE, &failReason); + } + if (!diskOk) { if (failReason == CHPPFR_CANNOTCLOSEPATH) { @@ -1903,8 +1943,19 @@ BOOL CPathHistoryItem::Execute(CFilesWindow* panel) { if (Type == 1) // archive { - if (!panel->ChangePathToArchive(PathOrArchiveOrFSName.c_str(), ArchivePathOrFSUserPart.c_str(), TopIndex, - FocusedName.empty() ? NULL : FocusedName.c_str(), FALSE, NULL, TRUE, &failReason, FALSE, FALSE, TRUE)) + BOOL archOk; + if (sally::unicode::HasWidePathW(PathOrArchiveOrFSNameW.c_str())) + { + const wchar_t* archivePathW = ArchivePathOrFSUserPartW.empty() ? L"" : ArchivePathOrFSUserPartW.c_str(); + archOk = panel->ChangePathToArchiveW(PathOrArchiveOrFSNameW.c_str(), archivePathW, TopIndex, + FocusedName.empty() ? NULL : FocusedName.c_str(), FALSE, NULL, TRUE, &failReason, FALSE, FALSE, TRUE); + } + else + { + archOk = panel->ChangePathToArchive(PathOrArchiveOrFSName.c_str(), ArchivePathOrFSUserPart.c_str(), TopIndex, + FocusedName.empty() ? NULL : FocusedName.c_str(), FALSE, NULL, TRUE, &failReason, FALSE, FALSE, TRUE); + } + if (!archOk) { if (failReason == CHPPFR_CANNOTCLOSEPATH) { @@ -2260,8 +2311,19 @@ void CPathHistory::Execute(int index, BOOL forward, CFilesWindow* panel, BOOL al if (NewItem != NULL) { + // Forward the wide twins NewItem already carries. Without these the + // unlock flush would silently degrade a Unicode-only history entry + // back to its lossy ANSI mirror — see DeferredHistoryWidePointers in + // PanelPathPolicy.h for the rationale. + sally::unicode::DeferredHistoryWideTwins twins{ + NewItem->PathOrArchiveOrFSNameW, + NewItem->ArchivePathOrFSUserPartW}; + const wchar_t* nameW = nullptr; + const wchar_t* userPartW = nullptr; + sally::unicode::DeferredHistoryWidePointers(twins, nameW, userPartW); AddPathUnique(NewItem->Type, NewItem->PathOrArchiveOrFSName.c_str(), NewItem->ArchivePathOrFSUserPart.c_str(), - NewItem->HIcon, NewItem->PluginFS, NULL); + NewItem->HIcon, NewItem->PluginFS, NULL, + nameW, userPartW); NewItem->HIcon = NULL; // AddPathUnique method took over responsibility for icon destruction delete NewItem; NewItem = NULL; @@ -2294,11 +2356,14 @@ void CPathHistory::ChangeActualPathData(int type, const char* pathOrArchiveOrFSN const char* archivePathOrFSUserPart, CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS, - int topIndex, const char* focusedName) + int topIndex, const char* focusedName, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { if (Paths.Count > 0) { - CPathHistoryItem n(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, NULL, pluginFS); + CPathHistoryItem n(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, NULL, pluginFS, + pathOrArchiveOrFSNameW, archivePathOrFSUserPartW); CPathHistoryItem* n2 = NULL; if (ForwardIndex != -1) { @@ -2318,7 +2383,9 @@ void CPathHistory::ChangeActualPathData(int type, const char* pathOrArchiveOrFSN void CPathHistory::RemoveActualPath(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS) + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { if (Lock) return; @@ -2326,7 +2393,8 @@ void CPathHistory::RemoveActualPath(int type, const char* pathOrArchiveOrFSName, { if (ForwardIndex == -1) { - CPathHistoryItem n(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, NULL, pluginFS); + CPathHistoryItem n(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, NULL, pluginFS, + pathOrArchiveOrFSNameW, archivePathOrFSUserPartW); CPathHistoryItem* n2 = Paths[Paths.Count - 1]; if (n.IsTheSamePath(*n2, curPluginFS)) // same paths -> delete record Paths.Delete(Paths.Count - 1); @@ -2337,13 +2405,16 @@ void CPathHistory::RemoveActualPath(int type, const char* pathOrArchiveOrFSName, } void CPathHistory::AddPath(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, - CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS) + CPluginFSInterfaceAbstract* pluginFS, CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { if (Lock) return; CPathHistoryItem* n = new CPathHistoryItem(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, - NULL, pluginFS); + NULL, pluginFS, + pathOrArchiveOrFSNameW, archivePathOrFSUserPartW); if (n == NULL) { TRACE_E(LOW_MEMORY); @@ -2392,10 +2463,13 @@ void CPathHistory::AddPath(int type, const char* pathOrArchiveOrFSName, const ch void CPathHistory::AddPathUnique(int type, const char* pathOrArchiveOrFSName, const char* archivePathOrFSUserPart, HICON hIcon, CPluginFSInterfaceAbstract* pluginFS, - CPluginFSInterfaceEncapsulation* curPluginFS) + CPluginFSInterfaceEncapsulation* curPluginFS, + const wchar_t* pathOrArchiveOrFSNameW, + const wchar_t* archivePathOrFSUserPartW) { CPathHistoryItem* n = new CPathHistoryItem(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, - hIcon, pluginFS); + hIcon, pluginFS, + pathOrArchiveOrFSNameW, archivePathOrFSUserPartW); if (Lock) { if (NewItem != NULL) @@ -2472,106 +2546,100 @@ void CPathHistory::SaveToRegistry(HKEY hKey, const char* name, BOOL onlyClear) if (!onlyClear) // if key should not just be cleared, save values from history { - int index = 0; - char buf[10]; - CPathBuffer path; - int i; - for (i = 0; i < Paths.Count; i++) + // Project the in-memory CPathHistoryItem list into the plain-data + // sally::path::history::Entry stream the serializer consumes. Wide + // twins are preferred when populated; ANSI mirrors are widened + // through CP_ACP for plugin FS items (still ANSI-only today). + std::vector entries; + entries.reserve(Paths.Count); + for (int i = 0; i < Paths.Count; i++) { CPathHistoryItem* item = Paths[i]; + sally::path::history::Entry entry; switch (item->Type) { - case 0: // disk - { - strcpy(path, item->PathOrArchiveOrFSName.c_str()); + case 0: + entry.kind = sally::path::history::EntryKind::Disk; break; - } - - // archive & FS: use ':' character as separator of two path parts - // during load we'll determine path type based on this character - case 1: // archive - case 2: // FS - { - lstrcpyn(path, item->PathOrArchiveOrFSName.c_str(), path.Size()); - StrNCat(path, ":", path.Size()); - if (!item->ArchivePathOrFSUserPart.empty()) - StrNCat(path, item->ArchivePathOrFSUserPart.c_str(), path.Size()); + case 1: + entry.kind = sally::path::history::EntryKind::Archive; + break; + case 2: + entry.kind = sally::path::history::EntryKind::PluginFS; break; - } default: - { - TRACE_E("CPathHistory::SaveToRegistry() uknown path type"); + TRACE_E("CPathHistory::SaveToRegistry() unknown path type"); continue; } + entry.nameW = !item->PathOrArchiveOrFSNameW.empty() + ? item->PathOrArchiveOrFSNameW + : AnsiToWide(item->PathOrArchiveOrFSName.c_str()); + if (entry.kind != sally::path::history::EntryKind::Disk) + { + entry.userPartW = !item->ArchivePathOrFSUserPartW.empty() + ? item->ArchivePathOrFSUserPartW + : AnsiToWide(item->ArchivePathOrFSUserPart.c_str()); } - itoa(index + 1, buf, 10); - SetValue(historyKey, buf, REG_SZ, path, (DWORD)strlen(path) + 1); - index++; + entries.push_back(std::move(entry)); } + + sally::path::history::WriteEntries(gRegistry, historyKey, entries); } CloseKey(historyKey); } } +namespace +{ +// Bridge: the serializer's plugin-FS detector callback delegates to the +// existing ANSI IsPluginFSPath helper. Lives in this TU rather than in the +// serializer itself so the headless test target doesn't need to link any +// of consts.h's plugin-FS machinery. +bool PluginFSPathDetectorBridge(const char* path, + std::string& outFsName, + std::string& outUserPart) +{ + char fsName[256] = {0}; + const char* userPart = nullptr; + if (!IsPluginFSPath(path, fsName, &userPart)) + return false; + outFsName.assign(fsName); + outUserPart.assign(userPart != nullptr ? userPart : ""); + return true; +} +} // namespace + void CPathHistory::LoadFromRegistry(HKEY hKey, const char* name) { ClearHistory(); HKEY historyKey; if (OpenKey(hKey, name, historyKey)) { - CPathBuffer path; - CPathBuffer fsName; - const char* pathOrArchiveOrFSName = path; - const char* archivePathOrFSUserPart = NULL; - char buf[10]; - int type; - int i; - for (i = 0;; i++) + // Read the wide REG_SZ stream through the serializer; plugin FS + // detection is bridged through PluginFSPathDetectorBridge so the + // serializer module itself stays decoupled from consts.h. + std::vector entries; + sally::path::history::ReadEntries(gRegistry, historyKey, entries, + &PluginFSPathDetectorBridge); + + for (const sally::path::history::Entry& entry : entries) { - itoa(i + 1, buf, 10); - if (GetValue(historyKey, buf, REG_SZ, path, path.Size())) - { - if (strlen(path) >= 2) - { - // path can be of type - // 0 (disk): "C:\???" or "\\server\???" - // 1 (archive): "C:\???:" or "\\server\???:" - // 2 (FS): "XY:???" - type = -1; // don't add - if ((path[0] == '\\' && path[1] == '\\') || path[1] == ':') - { - // it's type==0 (disk) or type==1 (archive) - pathOrArchiveOrFSName = path; - char* separator = strchr(path + 2, ':'); - if (separator == NULL) - { - type = 0; - archivePathOrFSUserPart = NULL; - } - else - { - *separator = 0; - type = 1; - archivePathOrFSUserPart = separator + 1; - } - } - else - { - // candidate for FS path - if (IsPluginFSPath(path, fsName, &archivePathOrFSUserPart)) - { - pathOrArchiveOrFSName = fsName; - type = 2; - } - } - if (type != -1) - AddPath(type, pathOrArchiveOrFSName, archivePathOrFSUserPart, NULL, NULL); - else - TRACE_E("CPathHistory::LoadFromRegistry() invalid path: " << path); - } - } - else - break; + // Project Entry back into AddPath's argument tuple. ANSI mirrors + // are computed via WideToAnsi so the legacy paths that still read + // them (plugin FS user-parts, IsTheSamePath comparisons) keep + // populated values. + int type = static_cast(entry.kind); + std::string nameA = WideToAnsi(entry.nameW.c_str()); + std::string userPartA = WideToAnsi(entry.userPartW.c_str()); + const char* userPartPtr = entry.kind == sally::path::history::EntryKind::Disk + ? NULL + : userPartA.c_str(); + const wchar_t* userPartWPtr = entry.kind == sally::path::history::EntryKind::Disk + ? nullptr + : (entry.userPartW.empty() ? nullptr : entry.userPartW.c_str()); + AddPath(type, nameA.c_str(), userPartPtr, NULL, NULL, + entry.nameW.empty() ? nullptr : entry.nameW.c_str(), + userPartWPtr); } CloseKey(historyKey); } @@ -4052,6 +4120,7 @@ DWORD OnKeyDownHandleSelectAll(DWORD keyCode, HWND hDialog, int editID) } void InvokeDirectoryMenuCommand(DWORD cmd, HWND hDialog, int editID, int editBufSize); +void InvokeDirectoryMenuCommandW(DWORD cmd, HWND hDialog, int editID, int editBufSize, HWND hUnicodeCtrl); void OnDirectoryButton(HWND hDialog, int editID, int editBufSize, int buttonID, WPARAM wParam, LPARAM lParam) { @@ -4060,6 +4129,13 @@ void OnDirectoryButton(HWND hDialog, int editID, int editBufSize, int buttonID, InvokeDirectoryMenuCommand(cmd, hDialog, editID, editBufSize); } +void OnDirectoryButtonW(HWND hDialog, int editID, int editBufSize, int buttonID, WPARAM wParam, LPARAM lParam, HWND hUnicodeCtrl) +{ + BOOL selectMenuItem = LOWORD(lParam); + DWORD cmd = TrackDirectoryMenu(hDialog, buttonID, selectMenuItem); + InvokeDirectoryMenuCommandW(cmd, hDialog, editID, editBufSize, hUnicodeCtrl); +} + DWORD OnDirectoryKeyDown(DWORD keyCode, HWND hDialog, int editID, int editBufSize, int buttonID) { BOOL controlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0; @@ -4108,6 +4184,48 @@ DWORD OnDirectoryKeyDown(DWORD keyCode, HWND hDialog, int editID, int editBufSiz return FALSE; } +DWORD OnDirectoryKeyDownW(DWORD keyCode, HWND hDialog, int editID, int editBufSize, int buttonID, HWND hUnicodeCtrl) +{ + BOOL controlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + BOOL altPressed = (GetKeyState(VK_MENU) & 0x8000) != 0; + BOOL shiftPressed = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + + if (!controlPressed && !shiftPressed && altPressed && keyCode == VK_RIGHT) + { + OnDirectoryButtonW(hDialog, editID, editBufSize, buttonID, MAKELPARAM(buttonID, 0), MAKELPARAM(TRUE, 0), hUnicodeCtrl); + return TRUE; + } + if (controlPressed && !shiftPressed && !altPressed) + { + switch (keyCode) + { + case 'B': + InvokeDirectoryMenuCommandW(DIRECTORY_COMMAND_BROWSE, hDialog, editID, editBufSize, hUnicodeCtrl); + return TRUE; + case 219: + case 221: + InvokeDirectoryMenuCommandW((keyCode == 219) ? DIRECTORY_COMMAND_LEFT : DIRECTORY_COMMAND_RIGHT, hDialog, editID, editBufSize, hUnicodeCtrl); + return TRUE; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '0': + { + int index = keyCode == '0' ? 9 : keyCode - '1'; + InvokeDirectoryMenuCommandW(DIRECTORY_COMMAND_HOTPATHF + index, hDialog, editID, editBufSize, hUnicodeCtrl); + return TRUE; + } + } + } + return FALSE; +} + void InvokeDirectoryMenuCommand(DWORD cmd, HWND hDialog, int editID, int editBufSize) { CPathBuffer path; @@ -4172,6 +4290,62 @@ void InvokeDirectoryMenuCommand(DWORD cmd, HWND hDialog, int editID, int editBuf } } +void InvokeDirectoryMenuCommandW(DWORD cmd, HWND hDialog, int editID, int editBufSize, HWND hUnicodeCtrl) +{ + std::wstring pathW; + BOOL setPathToEdit = FALSE; + switch (cmd) + { + case 0: + return; + + case DIRECTORY_COMMAND_BROWSE: + { + int len = GetWindowTextLengthW(hUnicodeCtrl != NULL ? hUnicodeCtrl : GetDlgItem(hDialog, editID)); + std::vector current((size_t)len + 1); + GetWindowTextW(hUnicodeCtrl != NULL ? hUnicodeCtrl : GetDlgItem(hDialog, editID), current.data(), len + 1); + pathW = current.data(); + wchar_t caption[100]; + GetWindowTextW(hDialog, caption, _countof(caption)); + if (GetTargetDirectoryW(hDialog, hDialog, caption, LoadStrW(IDS_BROWSETARGETDIRECTORY), pathW, FALSE, pathW.c_str())) + setPathToEdit = TRUE; + break; + } + + case DIRECTORY_COMMAND_LEFT: + case DIRECTORY_COMMAND_RIGHT: + { + CFilesWindow* panel = (cmd == DIRECTORY_COMMAND_LEFT) ? MainWindow->LeftPanel : MainWindow->RightPanel; + if (panel != NULL && panel->GetGeneralPathW(pathW, TRUE)) + setPathToEdit = TRUE; + break; + } + + default: + { + if (cmd >= DIRECTORY_COMMAND_HOTPATHF && cmd <= DIRECTORY_COMMAND_HOTPATHL) + { + CPathBuffer pathA; + if (MainWindow->GetExpandedHotPath(hDialog, cmd - DIRECTORY_COMMAND_HOTPATHF, pathA, pathA.Size())) + { + pathW = AnsiToWide(pathA); + setPathToEdit = TRUE; + } + } + else + TRACE_E("Unknown cmd=" << cmd); + } + } + + if (setPathToEdit) + { + if (hUnicodeCtrl != NULL) + SetWindowTextW(hUnicodeCtrl, pathW.c_str()); + else + SetWindowTextW(GetDlgItem(hDialog, editID), pathW.c_str()); + } +} + //**************************************************************************** // // CKeyForwarder diff --git a/src/salamander_path_validation.cpp b/src/salamander_path_validation.cpp index 07c9f959b..d99cfabcf 100644 --- a/src/salamander_path_validation.cpp +++ b/src/salamander_path_validation.cpp @@ -15,6 +15,7 @@ #include "ui/IPrompter.h" #include "common/IRegistry.h" #include "common/unicode/helpers.h" +#include "common/unicode/PanelPathPolicy.h" #include "common/IFileSystem.h" #include "common/fsutil.h" @@ -562,19 +563,124 @@ DWORD SalCheckPath(BOOL echo, const char* path, DWORD err, BOOL postRefresh, HWN return lastError; } -// Wide wrapper: converts to ANSI and calls SalCheckPath. -// The underlying implementation uses ANSI thread paths and UI dialogs. -// Full wide conversion requires threading infrastructure changes. DWORD SalCheckPathW(BOOL echo, const wchar_t* path, DWORD err, BOOL postRefresh, HWND parent) { - std::string pathA = WideToAnsi(path); - return SalCheckPath(echo, pathA.c_str(), err, postRefresh, parent); + std::string tracePath = WideToAnsi(path); + CALL_STACK_MESSAGE5("SalCheckPathW(%d, %s, 0x%X, %d, )", echo, tracePath.c_str(), err, postRefresh); + + HANDLES(EnterCriticalSection(&CheckPathCS)); + + static BOOL called = FALSE; + if (called) + { + HANDLES(LeaveCriticalSection(&CheckPathCS)); + TRACE_I("SalCheckPathW: recursive call (in one thread) is not allowed!"); + return 666; + } + called = TRUE; + + BeginStopRefresh(); + + BOOL valid = FALSE; + DWORD lastError = ERROR_SUCCESS; + + if (err == ERROR_SUCCESS) + { + if (path == NULL || *path == L'\0') + { + lastError = ERROR_PATH_NOT_FOUND; + } + else + { + DWORD attrs = GetFileAttributesW(path); + valid = attrs != INVALID_FILE_ATTRIBUTES; + if (!valid) + { + lastError = GetLastError(); + if (lastError == ERROR_INVALID_PARAMETER) + lastError = ERROR_NOT_READY; + } + } + } + else + { + lastError = err; + err = ERROR_SUCCESS; + } + + if ((err == ERROR_USER_TERMINATED || echo) && !valid) + { + switch (lastError) + { + case (DWORD)ERROR_USER_TERMINATED: + break; + + case ERROR_DIRECTORY: + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_BAD_PATHNAME: + { + std::wstring msg = FormatStrW(LoadStrW(IDS_DIRNAMEINVALID), path != NULL ? path : L""); + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), msg.c_str()); + break; + } + + default: + gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), GetErrorTextW(lastError)); + break; + } + } + + EndStopRefresh(postRefresh); + called = FALSE; + + HANDLES(LeaveCriticalSection(&CheckPathCS)); + return valid ? ERROR_SUCCESS : lastError; } BOOL SalCheckAndRestorePathW(HWND parent, const wchar_t* path, BOOL tryNet) { - std::string pathA = WideToAnsi(path); - return SalCheckAndRestorePath(parent, pathA.c_str(), tryNet); + CALL_STACK_MESSAGE3("SalCheckAndRestorePathW(, %s, %d)", WideToAnsi(path).c_str(), tryNet); + DWORD err; + if ((err = SalCheckPathW(FALSE, path, ERROR_SUCCESS, TRUE, parent)) != ERROR_SUCCESS) + { + BOOL ok = FALSE; + BOOL pathInvalid = FALSE; + if (tryNet && err != ERROR_USER_TERMINATED && path != NULL) + { + tryNet = FALSE; + if (path[0] != L'\0' && path[1] == L':' && + ((path[0] >= L'a' && path[0] <= L'z') || (path[0] >= L'A' && path[0] <= L'Z'))) + { + if (CheckAndRestoreNetworkConnection(parent, (char)path[0], pathInvalid)) + { + if ((err = SalCheckPathW(FALSE, path, ERROR_SUCCESS, TRUE, parent)) == ERROR_SUCCESS) + ok = TRUE; + } + } + else + { + std::string pathA; + if (sally::unicode::TryExactAnsiFallback(path, pathA) && + CheckAndConnectUNCNetworkPath(parent, pathA.c_str(), pathInvalid, FALSE)) + { + if ((err = SalCheckPathW(FALSE, path, ERROR_SUCCESS, TRUE, parent)) == ERROR_SUCCESS) + ok = TRUE; + } + } + } + if (!ok) + { + if (pathInvalid || + err == ERROR_USER_TERMINATED || + SalCheckPathW(TRUE, path, err, TRUE, parent) != ERROR_SUCCESS) + { + return FALSE; + } + } + } + + return TRUE; } BOOL SalCheckAndRestorePath(HWND parent, const char* path, BOOL tryNet) @@ -696,6 +802,72 @@ BOOL SalCheckAndRestorePathWithCut(HWND parent, char* path, BOOL& tryNet, DWORD& return !pathInvalid && err == ERROR_SUCCESS; } +BOOL SalCheckAndRestorePathWithCutW(HWND parent, std::wstring& path, BOOL& tryNet, DWORD& err, DWORD& lastErr, + BOOL& pathInvalid, BOOL& cut, BOOL donotReconnect) +{ + CALL_STACK_MESSAGE4("SalCheckAndRestorePathWithCutW(, %s, %d, , , , , %d)", WideToAnsi(path).c_str(), tryNet, + donotReconnect); + + pathInvalid = FALSE; + cut = FALSE; + lastErr = ERROR_SUCCESS; + BOOL semTimeoutOccured = FALSE; + +_CHECK_AGAIN: + + while ((err = SalCheckPathW(FALSE, path.c_str(), ERROR_SUCCESS, TRUE, parent)) != ERROR_SUCCESS) + { + if (err == ERROR_SEM_TIMEOUT && !semTimeoutOccured) + { + semTimeoutOccured = TRUE; + Sleep(300); + continue; + } + if (err == ERROR_USER_TERMINATED) + break; + if (tryNet) + { + tryNet = FALSE; + if (path.length() >= 2 && path[1] == L':' && + ((path[0] >= L'a' && path[0] <= L'z') || (path[0] >= L'A' && path[0] <= L'Z'))) + { + if (!donotReconnect && CheckAndRestoreNetworkConnection(parent, (char)path[0], pathInvalid)) + continue; + } + else + { + std::string pathA; + if (sally::unicode::TryExactAnsiFallback(path, pathA) && + CheckAndConnectUNCNetworkPath(parent, pathA.c_str(), pathInvalid, donotReconnect)) + { + continue; + } + } + if (pathInvalid) + break; + } + lastErr = err; + if (!IsDirError(err)) + break; + if (!CutDirectoryW(path)) + break; + cut = TRUE; + } + + if (tryNet && err != ERROR_USER_TERMINATED) + { + tryNet = FALSE; + std::string pathA; + if (sally::unicode::TryExactAnsiFallback(path, pathA) && + CheckAndConnectUNCNetworkPath(parent, pathA.c_str(), pathInvalid, donotReconnect)) + { + goto _CHECK_AGAIN; + } + } + + return !pathInvalid && err == ERROR_SUCCESS; +} + BOOL SalParsePath(HWND parent, char* path, int& type, BOOL& isDir, char*& secondPart, const char* errorTitle, char* nextFocus, BOOL curPathIsDiskOrArchive, const char* curPath, const char* curArchivePath, int* error, @@ -950,6 +1122,226 @@ BOOL SalParsePath(HWND parent, char* path, int& type, BOOL& isDir, char*& second } } +BOOL SalParsePathW(HWND parent, std::wstring& path, int& type, BOOL& isDir, wchar_t*& secondPart, + const wchar_t* errorTitle, std::wstring* nextFocus, BOOL curPathIsDiskOrArchive, + const wchar_t* curPath, const wchar_t* curArchivePath, int* error) +{ + CALL_STACK_MESSAGE_NONE + type = -1; + secondPart = NULL; + isDir = FALSE; + if (nextFocus != NULL) + nextFocus->clear(); + if (error != NULL) + *error = 0; + +PARSE_AGAIN_W: + int len = (int)path.length(); + BOOL backslashAtEnd = (len > 0 && path[len - 1] == L'\\'); + BOOL mustBePath = (len == 2 && LowerCase[(unsigned char)path[0]] >= 'a' && LowerCase[(unsigned char)path[0]] <= 'z' && + path[1] == L':'); + + if (nextFocus != NULL && !mustBePath) + { + size_t pos = path.find(L'\\'); + if (pos == std::wstring::npos || pos + 1 == path.length()) + { + size_t focusLen = (pos == std::wstring::npos) ? path.length() : pos; + if (focusLen < MAX_PATH) + *nextFocus = path.substr(0, focusLen); + } + } + + int errTextID; + if (!SalGetFullNameW(path, &errTextID, curPathIsDiskOrArchive ? curPath : NULL, nextFocus, NULL, curPathIsDiskOrArchive)) + { + if (errTextID == IDS_EMPTYNAMENOTALLOWED) + { + if (curPath == NULL) + { + if (error != NULL) + *error = SPP_EMPTYPATHNOTALLOWED; + } + else + { + path = curPath; + goto PARSE_AGAIN_W; + } + } + else + { + if (errTextID == IDS_INCOMLETEFILENAME) + { + if (error != NULL) + *error = SPP_INCOMLETEPATH; + if (!curPathIsDiskOrArchive) + return FALSE; + } + else if (error != NULL) + *error = SPP_WINDOWSPATHERROR; + } + std::wstring msg = FormatStrW(LoadStrW(IDS_PATHERRORFORMAT), path.c_str(), LoadStrW(errTextID)); + gPrompter->ShowError(errorTitle, msg.c_str()); + if (backslashAtEnd || mustBePath) + SalPathAddBackslashW(path); + return FALSE; + } + + if (curArchivePath != NULL && _wcsicmp(path.c_str(), curArchivePath) == 0) + { + SalPathAddBackslashW(path); + backslashAtEnd = TRUE; + } + + std::wstring root = GetRootPathW(path.c_str()); + BOOL tryNet = !curPathIsDiskOrArchive || curPath == NULL || !HasTheSameRootPathW(root.c_str(), curPath); + if (!SalCheckAndRestorePathW(parent, root.c_str(), tryNet)) + { + if (backslashAtEnd || mustBePath) + SalPathAddBackslashW(path); + if (error != NULL) + *error = SPP_WINDOWSPATHERROR; + return FALSE; + } + +FIND_AGAIN_W: + wchar_t* buffer = path.data(); + wchar_t* end = buffer + path.length(); + wchar_t* afterRoot = buffer + root.length(); + if (afterRoot > buffer && *(afterRoot - 1) == L'\\') + ; + else if (*afterRoot == L'\\') + afterRoot++; + wchar_t lastChar = 0; + BOOL hasMask = FALSE; + if (end > afterRoot) + { + wchar_t* end2 = end; + while (*--end2 != L'\\') + { + if (*end2 == L'*' || *end2 == L'?') + hasMask = TRUE; + } + if (hasMask) + { + CutSpacesFromBothSidesW(end2 + 1); + end = end2; + lastChar = *end; + *end = 0; + path.resize((size_t)(end - buffer)); + buffer = path.data(); + end = buffer + path.length(); + afterRoot = buffer + root.length(); + } + } + + HCURSOR oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); + isDir = TRUE; + std::wstring text; + while (end > afterRoot) + { + if (*(end - 1) != L'\\') + { + DWORD attrs = GetFileAttributesW(path.c_str()); + if (attrs != 0xFFFFFFFF) + { + if ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0) + { + if (lastChar != 0 || backslashAtEnd || mustBePath) + { + std::string ansiPath = WideToAnsi(path); + if (PackerFormatConfig.PackIsArchive(ansiPath.c_str())) + { + type = PATH_TYPE_ARCHIVE; + isDir = FALSE; + break; + } + text = LoadStrW(IDS_NOTARCHIVEPATH); + if (error != NULL) + *error = SPP_NOTARCHIVEFILE; + break; + } + isDir = FALSE; + while (*--end != L'\\') + ; + lastChar = *end; + break; + } + break; + } + else + { + DWORD err = GetLastError(); + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_INVALID_NAME && + err != ERROR_PATH_NOT_FOUND && err != ERROR_BAD_PATHNAME && + err != ERROR_DIRECTORY) + { + text = GetErrorTextW(err); + if (error != NULL) + *error = SPP_WINDOWSPATHERROR; + break; + } + } + } + *end = lastChar; + while (*--end != L'\\') + ; + lastChar = *end; + *end = 0; + path.resize((size_t)(end - buffer)); + buffer = path.data(); + end = buffer + path.length(); + afterRoot = buffer + root.length(); + } + // Capture the boundary between the existing-path prefix and the + // non-existent leaf/mask before the buffer-trick resize below grows + // path back to its original length. Use `end - buffer` rather than + // `path.length()`: the new-dir/mask loop path always resizes path to + // match `end`, so the two agree; but the existing-file break at + // lines 1264-1268 walks `end` back to the previous '\\' WITHOUT + // resizing path, leaving path.length() at the full original length. + // Taking path.length() there would lose the boundary the same way the + // pre-fix code did and secondPart would point at the terminator. + const size_t boundaryOff = (size_t)(end - buffer); + if (end <= buffer + path.length()) + *end = lastChar; + path.resize(wcslen(buffer)); + SetCursor(oldCur); + + if (text.empty()) + { + buffer = path.data(); + // Restore end to the boundary captured above, not to the new + // terminator. Without this, secondPart would be returned empty for + // copy/move targets with a non-existent leaf or mask, and + // SalSplitWindowsPathW would treat the whole input as an existing + // directory. + end = buffer + boundaryOff; + if (*end == L'\\') + end++; + if (isDir && *end != 0 && !hasMask && wcschr(end, L'\\') == NULL) + { + BOOL changeNextFocus = nextFocus != NULL && !nextFocus->empty() && _wcsicmp(nextFocus->c_str(), end) == 0; + if (MakeValidFileNameComponentW(end)) + { + path.resize(wcslen(buffer)); + if (changeNextFocus) + *nextFocus = end; + goto FIND_AGAIN_W; + } + } + secondPart = end; + type = PATH_TYPE_WINDOWS; + return TRUE; + } + + std::wstring msg = FormatStrW(LoadStrW(IDS_PATHERRORFORMAT), path.c_str(), text.c_str()); + gPrompter->ShowError(errorTitle, msg.c_str()); + if (backslashAtEnd || mustBePath) + SalPathAddBackslashW(path); + return FALSE; +} + BOOL SalSplitWindowsPath(HWND parent, const char* title, const char* errorTitle, int selCount, char* path, char* secondPart, BOOL pathIsDir, BOOL backslashAtEnd, const char* dirName, const char* curDiskPath, char*& mask) @@ -1044,6 +1436,89 @@ BOOL SalSplitWindowsPath(HWND parent, const char* title, const char* errorTitle, return FALSE; } +BOOL SalSplitWindowsPathW(HWND parent, const wchar_t* title, const wchar_t* errorTitle, int selCount, + wchar_t* path, wchar_t* secondPart, BOOL pathIsDir, BOOL backslashAtEnd, + const wchar_t* dirName, const wchar_t* curDiskPath, wchar_t*& mask) +{ + std::wstring root = GetRootPathW(path); + wchar_t* afterRoot = path + root.length() - 1; + if (*afterRoot == L'\\') + afterRoot++; + + std::vector newDirs(SAL_MAX_LONG_PATH, 0); + if (SalSplitGeneralPathW(parent, title, errorTitle, selCount, path, afterRoot, secondPart, + pathIsDir, backslashAtEnd, dirName, curDiskPath, mask, newDirs.data(), NULL)) + { + if (mask - 1 > path && *(mask - 2) == L'\\' && + (mask - 1 > afterRoot || *path == L'\\')) + { + memmove(mask - 2, mask - 1, (wcslen(mask) + 2) * sizeof(wchar_t)); + mask--; + } + + if (newDirs[0] != 0) + { + size_t prefixLen = (size_t)(secondPart - path); + memmove(newDirs.data() + prefixLen, newDirs.data(), (wcslen(newDirs.data()) + 1) * sizeof(wchar_t)); + memmove(newDirs.data(), path, prefixLen * sizeof(wchar_t)); + newDirs[prefixLen + wcslen(newDirs.data() + prefixLen)] = 0; + SalPathRemoveBackslashW(newDirs.data()); + + BOOL ok = TRUE; + wchar_t* st = newDirs.data() + prefixLen; + while (1) + { + BOOL invalidPath = *st != 0 && *st <= L' '; + wchar_t* slash = wcschr(st, L'\\'); + if (slash != NULL) + { + if (slash > st && (*(slash - 1) <= L' ' || *(slash - 1) == L'.')) + invalidPath = TRUE; + *slash = 0; + } + else if (*st != 0) + { + wchar_t* end = st + wcslen(st) - 1; + if (*end <= L' ' || *end == L'.') + invalidPath = TRUE; + } + + if (invalidPath || !CreateDirectoryW(newDirs.data(), NULL)) + { + DWORD lastErr = invalidPath ? ERROR_INVALID_NAME : GetLastError(); + if (lastErr != ERROR_ALREADY_EXISTS) + { + std::wstring msg = FormatStrW(LoadStrW(IDS_CREATEDIRFAILED), newDirs.data()); + gPrompter->ShowError(errorTitle, msg.c_str()); + ok = FALSE; + break; + } + } + + if (slash != NULL) + *slash = L'\\'; + else + break; + st = slash + 1; + } + + std::wstring changesRoot(path, prefixLen); + MainWindow->PostChangeOnPathNotificationW(changesRoot.c_str(), FALSE); + if (!ok) + { + wchar_t* e = path + wcslen(path); + if (e > path && *(e - 1) != L'\\') + *e++ = L'\\'; + if (e != mask) + memmove(e, mask, (wcslen(mask) + 1) * sizeof(wchar_t)); + return FALSE; + } + } + return TRUE; + } + return FALSE; +} + BOOL SalSplitGeneralPath(HWND parent, const char* title, const char* errorTitle, int selCount, char* path, char* afterRoot, char* secondPart, BOOL pathIsDir, BOOL backslashAtEnd, const char* dirName, const char* curPath, char*& mask, char* newDirs, diff --git a/src/shellib.cpp b/src/shellib.cpp index 154d600c7..a579edcc1 100644 --- a/src/shellib.cpp +++ b/src/shellib.cpp @@ -14,6 +14,7 @@ extern "C" #include "salshlib.h" #include "common/widepath.h" #include "common/unicode/helpers.h" +#include "common/fsutil.h" // original location in fileswnd.h (here only because of MakeCopyOfName in CImpDropTarget::ProcessClipboardData) extern BOOL OurClipDataObject; // TRUE during "paste" of our IDataObject @@ -2302,6 +2303,13 @@ struct CBrowseData HWND HCenterWindow; }; +struct CBrowseDataW +{ + const wchar_t* Title; + const wchar_t* InitDir; + HWND HCenterWindow; +}; + int CALLBACK DirectoryBrowse(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) { CALL_STACK_MESSAGE4("DirectoryBrowse(, 0x%X, 0x%IX, 0x%IX)", uMsg, lParam, lpData); @@ -2337,6 +2345,34 @@ int CALLBACK DirectoryBrowse(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) return 0; } +int CALLBACK DirectoryBrowseW(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) +{ + CALL_STACK_MESSAGE4("DirectoryBrowseW(, 0x%X, 0x%IX, 0x%IX)", uMsg, lParam, lpData); + if (uMsg == BFFM_INITIALIZED) + { + MultiMonCenterWindow(hwnd, ((CBrowseDataW*)lpData)->HCenterWindow, FALSE); + SetWindowTextW(hwnd, ((CBrowseDataW*)lpData)->Title); + if (((CBrowseDataW*)lpData)->InitDir != NULL) + { + std::wstring path = GetRootPathW(((CBrowseDataW*)lpData)->InitDir); + if (path.length() < wcslen(((CBrowseDataW*)lpData)->InitDir)) + { + path = ((CBrowseDataW*)lpData)->InitDir; + if (!path.empty() && path[path.length() - 1] == L'\\') + path.resize(path.length() - 1); + } + SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)path.c_str()); + } + } + if (uMsg == BFFM_SELCHANGED && (ITEMIDLIST*)lParam != NULL) + { + CWidePathBuffer path; + BOOL ret = SHGetPathFromIDListW((ITEMIDLIST*)lParam, path); + SendMessage(hwnd, BFFM_ENABLEOK, 0, ret); + } + return 0; +} + BOOL GetTargetDirectoryAux(HWND parent, HWND hCenterWindow, const char* title, const char* comment, char* path, BOOL onlyNet, const char* initDir) @@ -2491,6 +2527,16 @@ void ResolveNetHoodPath(char* path) } } +void ResolveNetHoodPathW(std::wstring& path) +{ + std::string ansi = WideToAnsi(path); + if (!ansi.empty()) + { + ResolveNetHoodPath(ansi.data()); + path = AnsiToWide(ansi.c_str()); + } +} + BOOL GetTargetDirectory(HWND parent, HWND hCenterWindow, const char* title, const char* comment, char* path, BOOL onlyNet, const char* initDir) @@ -2502,6 +2548,54 @@ BOOL GetTargetDirectory(HWND parent, HWND hCenterWindow, return ret; } +BOOL GetTargetDirectoryW(HWND parent, HWND hCenterWindow, const wchar_t* title, const wchar_t* comment, + std::wstring& path, BOOL onlyNet, const wchar_t* initDir) +{ + ITEMIDLIST* pidl; + if (onlyNet) + SHGetSpecialFolderLocation(parent, CSIDL_NETWORK, &pidl); + else + pidl = NULL; + + wchar_t display[MAX_PATH]; + BROWSEINFOW bi; + ZeroMemory(&bi, sizeof(bi)); + bi.hwndOwner = parent; + bi.pidlRoot = pidl; + bi.pszDisplayName = display; + bi.lpszTitle = comment; + bi.ulFlags = BIF_RETURNONLYFSDIRS; + bi.lpfn = DirectoryBrowseW; + CBrowseDataW bd; + bd.Title = title; + bd.InitDir = initDir; + bd.HCenterWindow = hCenterWindow; + bi.lParam = (LPARAM)&bd; + LPITEMIDLIST res = SHBrowseForFolderW(&bi); + BOOL ret = FALSE; + if (res != NULL) + { + CWidePathBuffer out; + if (SHGetPathFromIDListW(res, out)) + { + path = out; + ret = TRUE; + } + } + IMalloc* alloc; + if ((pidl != NULL || res != NULL) && SUCCEEDED(CoGetMalloc(1, &alloc))) + { + if (pidl != NULL && alloc->DidAlloc(pidl) == 1) + alloc->Free(pidl); + if (res != NULL && alloc->DidAlloc(res) == 1) + alloc->Free(res); + alloc->Release(); + } + if (ret) + ResolveNetHoodPathW(path); + return ret; +} + //***************************************************************************** // // GetNewOrBackgroundMenu diff --git a/src/shellib.h b/src/shellib.h index a9b1cbf13..cf265c06a 100644 --- a/src/shellib.h +++ b/src/shellib.h @@ -43,11 +43,14 @@ void OpenFolderAndFocusItem(HWND hOwnerWindow, const char* dir, const char* item // hCenterWindow - window to which the dialog will be centered BOOL GetTargetDirectory(HWND parent, HWND hCenterWindow, const char* title, const char* comment, char* path, BOOL onlyNet = FALSE, const char* initDir = NULL); +BOOL GetTargetDirectoryW(HWND parent, HWND hCenterWindow, const wchar_t* title, const wchar_t* comment, + std::wstring& path, BOOL onlyNet = FALSE, const wchar_t* initDir = NULL); // detects whether it is a NetHood path (directory with target.lnk), // optionally resolves target.lnk and returns the path in 'path'; 'path' is an in/out path // (min. MAX_PATH characters) void ResolveNetHoodPath(char* path); +void ResolveNetHoodPathW(std::wstring& path); class CMenuNew; diff --git a/src/shellsup.cpp b/src/shellsup.cpp index 445669574..f2152a374 100644 --- a/src/shellsup.cpp +++ b/src/shellsup.cpp @@ -154,6 +154,10 @@ BOOL DoCopyMove(BOOL copy, char* targetDir, CCopyMoveData* data, void* param) { tmp->Copy = copy; strcpy(tmp->TargetPath, targetDir); + if (panel != NULL && panel->Is(ptDisk)) + tmp->TargetPathW = panel->GetPathW(); + else + tmp->TargetPathW = AnsiToWide(targetDir); tmp->Data = data; PostMessage(panel->HWindow, WM_USER_DROPCOPYMOVE, (WPARAM)tmp, 0); return TRUE; @@ -772,7 +776,7 @@ static BOOL CollectSelectedPathsW(CFilesWindow* panel, const int* indexes, int i if (panel == NULL || indexes == NULL || indexCount <= 0) return FALSE; - std::wstring basePathW = AnsiToWide(panel->GetPath()); + std::wstring basePathW = panel->Is(ptDisk) ? std::wstring(panel->GetPathW()) : AnsiToWide(panel->GetPath()); if (!basePathW.empty() && basePathW.back() != L'\\') basePathW += L'\\'; @@ -1951,7 +1955,7 @@ void ShellAction(CFilesWindow* panel, CShellAction action, BOOL useSelection, { std::vector selectedPathsW; BOOL hasWideName = FALSE; - if (CollectSelectedPathsW(panel, idxs, idxCount, selectedPathsW, hasWideName) && hasWideName) + if (CollectSelectedPathsW(panel, idxs, idxCount, selectedPathsW, hasWideName)) { sally::clipboard::HDropWideDataObject* wideDataObject = new sally::clipboard::HDropWideDataObject(selectedPathsW); if (wideDataObject != NULL) diff --git a/src/snooper.cpp b/src/snooper.cpp index 7571988b0..4904ca234 100644 --- a/src/snooper.cpp +++ b/src/snooper.cpp @@ -8,6 +8,7 @@ #include "fileswnd.h" #include "mainwnd.h" #include "snooper.h" +#include "common/unicode/helpers.h" CWindowArray WindowArray(10, 5); CObjectArray ObjectArray(10, 5); @@ -564,6 +565,54 @@ void TerminateThread() HANDLES(DeleteCriticalSection(&SafeFindCloseCS)); } +void AddDirectoryW(CFilesWindow* win, const wchar_t* pathW, BOOL registerDevNotification) +{ + std::string tracePath = WideToAnsi(pathW != nullptr ? pathW : L""); + CALL_STACK_MESSAGE3("AddDirectoryW(, %s, %d)", tracePath.c_str(), registerDevNotification); + SetEvent(WantDataEvent); + WaitForSingleObject(DataUsageMutex, INFINITE); + SetEvent(WantDataEvent); + + // Mirror the ANSI variant's space/dot guard, but in wide form, so paths + // like "C:\\foo \\" survive into FindFirstChangeNotificationW intact. + std::wstring pathCopy = MakeCopyWithBackslashIfNeededW(pathW != nullptr ? pathW : L""); + HANDLE h = HANDLES_Q(FindFirstChangeNotificationW(pathCopy.c_str(), FALSE, + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_LAST_WRITE)); + if (h != INVALID_HANDLE_VALUE) + { + win->SetAutomaticRefresh(TRUE); + WindowArray.Add(win); + ObjectArray.Add(h); + + if (registerDevNotification) + { + DEV_BROADCAST_HANDLE dbh; + memset(&dbh, 0, sizeof(dbh)); + dbh.dbch_size = sizeof(dbh); + dbh.dbch_devicetype = DBT_DEVTYP_HANDLE; + dbh.dbch_handle = h; + if (win->DeviceNotification != NULL) + { + TRACE_E("AddDirectoryW(): unexpected situation: win->DeviceNotification != NULL"); + UnregisterDeviceNotification(win->DeviceNotification); + } + win->DeviceNotification = RegisterDeviceNotificationW(win->HWindow, &dbh, DEVICE_NOTIFY_WINDOW_HANDLE); + } + } + else + { + win->SetAutomaticRefresh(FALSE); + TRACE_W("Unable to receive change notifications for directory '" << tracePath.c_str() << "' (auto-refresh will not work)."); + } + + ReleaseMutex(DataUsageMutex); + WaitForSingleObject(ContinueEvent, INFINITE); +} + void AddDirectory(CFilesWindow* win, const char* path, BOOL registerDevNotification) { CALL_STACK_MESSAGE3("AddDirectory(, %s, %d)", path, registerDevNotification); @@ -575,7 +624,7 @@ void AddDirectory(CFilesWindow* win, const char* path, BOOL registerDevNotificat // trims spaces/dots and thus works with a different path CPathBuffer pathCopy; // Heap-allocated for long path support MakeCopyWithBackslashIfNeeded(path, pathCopy); - HANDLE h = HANDLES_Q(FindFirstChangeNotification(path, FALSE, + HANDLE h = HANDLES_Q(FindFirstChangeNotification(pathCopy, FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | @@ -683,6 +732,95 @@ DWORD WINAPI ThreadFindCloseChangeNotification(void* param) return ThreadFindCloseChangeNotificationEH(param); } +void ChangeDirectoryW(CFilesWindow* win, const wchar_t* newPathW, BOOL registerDevNotification) +{ + std::string tracePath = WideToAnsi(newPathW != nullptr ? newPathW : L""); + CALL_STACK_MESSAGE3("ChangeDirectoryW(, %s, %d)", tracePath.c_str(), registerDevNotification); + SetEvent(WantDataEvent); + WaitForSingleObject(DataUsageMutex, INFINITE); + SetEvent(WantDataEvent); + BOOL registerDevNot = FALSE; + HANDLE registerDevNotHandle = NULL; + if (win->DeviceNotification != NULL) + { + UnregisterDeviceNotification(win->DeviceNotification); + win->DeviceNotification = NULL; + } + + std::wstring newPathCopy = MakeCopyWithBackslashIfNeededW(newPathW != nullptr ? newPathW : L""); + + int i; + for (i = 0; i < WindowArray.Count; i++) + if (win == WindowArray[i]) + { + HANDLES(EnterCriticalSection(&SafeFindCloseCS)); + SafeFindCloseCNArr.Add(ObjectArray[i]); + if (!SafeFindCloseCNArr.IsGood()) + SafeFindCloseCNArr.ResetState(); + HANDLES(LeaveCriticalSection(&SafeFindCloseCS)); + ResetEvent(SafeFindCloseFinished); + SetEvent(SafeFindCloseStart); + WaitForSingleObject(SafeFindCloseFinished, 200); + + ObjectArray[i] = HANDLES_Q(FindFirstChangeNotificationW(newPathCopy.c_str(), FALSE, + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_LAST_WRITE)); + if ((HANDLE)ObjectArray[i] == INVALID_HANDLE_VALUE) + { + win->SetAutomaticRefresh(FALSE); + ObjectArray.Delete(i); + WindowArray.Delete(i); + TRACE_W("Unable to receive change notifications for directory '" << tracePath.c_str() << "' (auto-refresh will not work)."); + } + else if (registerDevNotification) + { + registerDevNot = TRUE; + registerDevNotHandle = (HANDLE)ObjectArray[i]; + } + break; + } + if (i == WindowArray.Count) + { + HANDLE h = HANDLES_Q(FindFirstChangeNotificationW(newPathCopy.c_str(), FALSE, + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_LAST_WRITE)); + if (h != INVALID_HANDLE_VALUE) + { + win->SetAutomaticRefresh(TRUE); + WindowArray.Add(win); + ObjectArray.Add(h); + if (registerDevNotification) + { + registerDevNot = TRUE; + registerDevNotHandle = h; + } + } + else + { + win->SetAutomaticRefresh(FALSE); + TRACE_W("Unable to receive change notifications for directory '" << tracePath.c_str() << "' (auto-refresh will not work)."); + } + } + if (registerDevNot) + { + DEV_BROADCAST_HANDLE dbh; + memset(&dbh, 0, sizeof(dbh)); + dbh.dbch_size = sizeof(dbh); + dbh.dbch_devicetype = DBT_DEVTYP_HANDLE; + dbh.dbch_handle = registerDevNotHandle; + win->DeviceNotification = RegisterDeviceNotificationW(win->HWindow, &dbh, DEVICE_NOTIFY_WINDOW_HANDLE); + } + + ReleaseMutex(DataUsageMutex); + WaitForSingleObject(ContinueEvent, INFINITE); +} + void ChangeDirectory(CFilesWindow* win, const char* newPath, BOOL registerDevNotification) { CALL_STACK_MESSAGE3("ChangeDirectory(, %s, %d)", newPath, registerDevNotification); @@ -717,7 +855,7 @@ void ChangeDirectory(CFilesWindow* win, const char* newPath, BOOL registerDevNot // trims spaces/dots and works with a different path CPathBuffer newPathCopy; // Heap-allocated for long path support MakeCopyWithBackslashIfNeeded(newPath, newPathCopy); - ObjectArray[i] = HANDLES_Q(FindFirstChangeNotification(newPath, FALSE, + ObjectArray[i] = HANDLES_Q(FindFirstChangeNotification(newPathCopy, FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | @@ -747,7 +885,7 @@ void ChangeDirectory(CFilesWindow* win, const char* newPath, BOOL registerDevNot // trims spaces/dots and works with a different path CPathBuffer newPathCopy; // Heap-allocated for long path support MakeCopyWithBackslashIfNeeded(newPath, newPathCopy); - HANDLE h = HANDLES_Q(FindFirstChangeNotification(newPath, FALSE, + HANDLE h = HANDLES_Q(FindFirstChangeNotification(newPathCopy, FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | diff --git a/src/snooper.h b/src/snooper.h index 2910fe1ef..bbe159df3 100644 --- a/src/snooper.h +++ b/src/snooper.h @@ -11,6 +11,15 @@ extern int SnooperSuspended; void AddDirectory(CFilesWindow* win, const char* path, BOOL registerDevNotification); // new directory for snooper void ChangeDirectory(CFilesWindow* win, const char* newPath, BOOL registerDevNotification); // change of specified directory + +// Wide siblings of AddDirectory / ChangeDirectory. Use these when the panel's +// wide path cache is populated (sally::unicode::HasWidePathW(panel->GetPathW())) +// so that FindFirstChangeNotificationW receives the real Unicode bytes instead +// of the lossy CP_ACP mirror — otherwise background change notifications stop +// working for paths outside the active code page. +void AddDirectoryW(CFilesWindow* win, const wchar_t* pathW, BOOL registerDevNotification); +void ChangeDirectoryW(CFilesWindow* win, const wchar_t* newPathW, BOOL registerDevNotification); + void DetachDirectory(CFilesWindow* win, BOOL waitForHandleClosure = FALSE, BOOL closeDevNotifification = TRUE); // no longer need to snoop BOOL InitializeThread(); diff --git a/src/stswnd.cpp b/src/stswnd.cpp index 13d534b51..f54e7ae4a 100644 --- a/src/stswnd.cpp +++ b/src/stswnd.cpp @@ -13,6 +13,7 @@ #include "shellib.h" #include "svg.h" #include "darkmode.h" +#include "common/fsutil.h" #include "common/unicode/helpers.h" static COLORREF GetStatusBkColor(BOOL activeCaption, BOOL showPanelCaption) @@ -67,6 +68,7 @@ CStatusWindow::CStatusWindow(CFilesWindow* filesWindow, int border, CObjectOrigi { CALL_STACK_MESSAGE_NONE Text = NULL; + UseWideText = FALSE; AlpDX = NULL; Allocated = 0; PathLen = -1; @@ -161,6 +163,8 @@ BOOL CStatusWindow::SetSubTexts(DWORD* subTexts, DWORD subTextsCount) BOOL CStatusWindow::SetText(const char* txt, int pathLen) { CALL_STACK_MESSAGE3("CStatusWindow::SetText(%s, %d)", txt, pathLen); + UseWideText = FALSE; + TextW.clear(); if (Text != NULL && strcmp(Text, txt) == 0) { PathLen = pathLen; @@ -206,6 +210,67 @@ BOOL CStatusWindow::SetText(const char* txt, int pathLen) return TRUE; } +BOOL CStatusWindow::SetTextW(const wchar_t* txt, int pathLen) +{ + std::wstring newText = txt != NULL ? txt : L""; + CALL_STACK_MESSAGE3("CStatusWindow::SetTextW(%s, %d)", WideToAnsi(newText).c_str(), pathLen); + if (UseWideText && TextW == newText) + { + PathLen = pathLen; + return TRUE; + } + + HotTrackItemsMeasured = FALSE; + HotItem = NULL; + LastHotItem = NULL; + + TextW = newText; + UseWideText = TRUE; + + std::string fallbackText = WideToAnsi(TextW); + int l = (int)fallbackText.size() + 1; + if (Allocated < l) + { + char* newAnsiText = (char*)realloc(Text, l); + if (newAnsiText == NULL) + { + TRACE_E(LOW_MEMORY); + return FALSE; + } + Text = newAnsiText; + Allocated = l; + } + memmove(Text, fallbackText.c_str(), l); + + int requiredDx = max(1, (int)TextW.size() + 1); + int* newAlpDX = (int*)realloc(AlpDX, requiredDx * sizeof(int)); + if (newAlpDX == NULL) + { + TRACE_E(LOW_MEMORY); + return FALSE; + } + AlpDX = newAlpDX; + + PathLen = pathLen; + TextLen = (int)TextW.size(); + + if (SubTexts != NULL) + { + SubTextsCount = 0; + free(SubTexts); + SubTexts = NULL; + } + + BuildHotTrackItems(); + + if (MouseCaptured) + WindowProc(WM_MOUSELEAVE, 0, 0); + + if (HWindow != NULL) + InvalidateRect(HWindow, NULL, FALSE); + return TRUE; +} + void CStatusWindow::BuildHotTrackItems() { CALL_STACK_MESSAGE1("CStatusWindow::BuildHotTrackItems()"); @@ -220,7 +285,57 @@ void CStatusWindow::BuildHotTrackItems() // populate HotTrackItems CHotTrackItem item; HotTrackItems.DestroyMembers(); - if (Text != NULL) + if (UseWideText) + { + int pathLen = (PathLen != -1) ? PathLen : (int)TextW.size(); + SIZE s; + GetTextExtentExPointW(dc, TextW.c_str(), TextLen, 0, NULL, AlpDX, &s); + + if (FilesWindow->Is(ptDisk) || FilesWindow->Is(ptZIPArchive)) + { + int chars; + if (TextW.size() >= 2 && TextW[0] == L'\\' && TextW[1] == L'\\' && + (TextW.size() < 6 || TextW[2] != L'.' || TextW[3] != L'\\' || TextW[4] == 0 || TextW[5] != L':') && + Plugins.GetFirstNethoodPluginFSName()) + { + chars = 2; + } + else + { + std::wstring rootPathW = GetRootPathW(TextW.c_str()); + chars = (int)rootPathW.size(); + + BOOL isDotDriveFormat = TextW.size() >= 6 && TextW[0] == L'\\' && TextW[1] == L'\\' && + TextW[2] == L'.' && TextW[3] == L'\\' && TextW[4] != 0 && TextW[5] == L':'; + if (chars > pathLen || !isDotDriveFormat && chars > 3) + chars--; + } + + BOOL exit; + do + { + item.Offset = 0; + item.PixelsOffset = 0; + item.Chars = chars; + item.Pixels = chars != 0 ? (WORD)AlpDX[chars - 1] : 0; + HotTrackItems.Add(item); + + if (chars < pathLen && TextW[chars] == L'\\') + chars++; + + exit = TRUE; + while (chars < pathLen) + { + exit = FALSE; + if (TextW[chars] == L'\\') + break; + chars++; + } + } while (!exit); + } + HotTrackItemsMeasured = TRUE; + } + else if (Text != NULL) { // this crashed in SS2.0: execution address = 0x7800D9B0 // strlen was called when Text was still NULL @@ -309,7 +424,35 @@ void CStatusWindow::BuildHotTrackItems() // populate HotTrackItems CHotTrackItem item; HotTrackItems.DestroyMembers(); - if (Text != NULL) + if (UseWideText) + { + SIZE s; + GetTextExtentExPointW(dc, TextW.c_str(), TextLen, 0, NULL, AlpDX, &s); + + DWORD len = TextLen; + SIZE sOffset; + SIZE sSub; + DWORD i; + for (i = 0; i < (DWORD)SubTextsCount; i++) + { + WORD charOffset = LOWORD(SubTexts[i]); + WORD charLen = HIWORD(SubTexts[i]); + if (charOffset + charLen > (WORD)len) + { + TRACE_E("charOffset + charLen >= len"); + continue; + } + GetTextExtentPoint32W(dc, TextW.c_str(), charOffset, &sOffset); + GetTextExtentPoint32W(dc, TextW.c_str() + charOffset, charLen, &sSub); + item.PixelsOffset = (WORD)sOffset.cx; + item.Pixels = (WORD)sSub.cx; + item.Offset = charOffset; + item.Chars = charLen; + HotTrackItems.Add(item); + HotTrackItemsMeasured = TRUE; + } + } + else if (Text != NULL) { // get positions of all characters SIZE s; @@ -635,7 +778,13 @@ void CStatusWindow::LayoutWindow() void CStatusWindow::GetHotText(char* buffer, int bufSize) { CALL_STACK_MESSAGE_NONE - if (HotItem != NULL && Text != NULL) + if (HotItem != NULL && UseWideText) + { + std::wstring hotText = TextW.substr(HotItem->Offset, HotItem->Chars); + std::string hotTextA = WideToAnsi(hotText); + lstrcpyn(buffer, hotTextA.c_str(), bufSize); + } + else if (HotItem != NULL && Text != NULL) { lstrcpyn(buffer, Text + HotItem->Offset, min(HotItem->Chars + 1, bufSize)); // for Directory Line with plugin FS, allow plugin to make final path adjustments (adding ']' for VMS paths in FTP) @@ -794,6 +943,15 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl SetBkMode(dc, TRANSPARENT); HFONT oldFont = (HFONT)SelectObject(dc, EnvFont); + auto drawTextAt = [&](int x, int y, int start, int count) + { + if (count <= 0) + return; + if (UseWideText) + ExtTextOutW(dc, x, y, 0, NULL, TextW.c_str() + start, count, NULL); + else + ExtTextOut(dc, x, y, 0, NULL, Text + start, count, NULL); + }; SIZE s; RECT tmpR; @@ -1008,7 +1166,7 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl { if (truncateEnd) { // without truncation or truncated end - ExtTextOut(dc, TextRect.left, textY, 0, NULL, Text, min(visibleChars, firstClipChar), NULL); + drawTextAt(TextRect.left, textY, 0, min(visibleChars, firstClipChar)); if (visibleChars < min(TextLen, firstClipChar)) // if end was truncated -> append "..." { int offset = (visibleChars > 0) ? AlpDX[visibleChars - 1] : 0; @@ -1019,12 +1177,12 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl { // truncated part after root folder // root part int rootChars = HotTrackItems[0].Chars; - ExtTextOut(dc, TextRect.left, textY, 0, NULL, Text, rootChars, NULL); + drawTextAt(TextRect.left, textY, 0, rootChars); // "..." ExtTextOut(dc, TextRect.left + AlpDX[rootChars - 1], textY, 0, NULL, "...", 3, NULL); // remainder - ExtTextOut(dc, TextRect.left + AlpDX[rootChars - 1] + TextEllipsisWidthEnv, - textY, 0, NULL, Text + TextLen - visibleChars, visibleChars, NULL); + drawTextAt(TextRect.left + AlpDX[rootChars - 1] + TextEllipsisWidthEnv, + textY, TextLen - visibleChars, visibleChars); } } @@ -1033,7 +1191,7 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl { // without truncation or truncated end int visibleChars2 = visibleChars - lastClipChar; - ExtTextOut(dc, TextRect.left + AlpDX[lastClipChar - 1], textY, 0, NULL, Text + lastClipChar, visibleChars2, NULL); + drawTextAt(TextRect.left + AlpDX[lastClipChar - 1], textY, lastClipChar, visibleChars2); if (visibleChars < TextLen) // if end was truncated -> append "..." { int offset = (visibleChars > 0) ? AlpDX[visibleChars - 1] : 0; @@ -1057,8 +1215,8 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl if (firstChar < rootChars + EllipsedChars) // need to skip possible backslash that would fall into ellipsis firstChar = rootChars + EllipsedChars; } - ExtTextOut(dc, TextRect.left + AlpDX[firstChar - 1] - EllipsedWidth + TextEllipsisWidthEnv, - textY, 0, NULL, Text + firstChar, TextLen - firstChar, NULL); + drawTextAt(TextRect.left + AlpDX[firstChar - 1] - EllipsedWidth + TextEllipsisWidthEnv, + textY, firstChar, TextLen - firstChar); } // display hot track item @@ -1091,8 +1249,7 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl } if (showChars > 0) { - ExtTextOut(dc, TextRect.left + hotItem->PixelsOffset, textY, 0, NULL, - Text + hotItem->Offset, showChars, NULL); + drawTextAt(TextRect.left + hotItem->PixelsOffset, textY, hotItem->Offset, showChars); } } else @@ -1100,7 +1257,7 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl int showChars = hotItem->Chars; int rootChars = HotTrackItems[0].Chars; - ExtTextOut(dc, TextRect.left, textY, 0, NULL, Text, rootChars, NULL); + drawTextAt(TextRect.left, textY, 0, rootChars); if (showChars > rootChars) { // "..." @@ -1108,8 +1265,8 @@ void CStatusWindow::Paint(HDC hdc, BOOL highlightText, BOOL highlightHotTrackOnl if (showChars - rootChars - EllipsedChars > 0) { // remainder - ExtTextOut(dc, TextRect.left + AlpDX[rootChars - 1] + TextEllipsisWidthEnv, - textY, 0, NULL, Text + rootChars + EllipsedChars, showChars - rootChars - EllipsedChars, NULL); + drawTextAt(TextRect.left + AlpDX[rootChars - 1] + TextEllipsisWidthEnv, + textY, rootChars + EllipsedChars, showChars - rootChars - EllipsedChars); } } } @@ -2085,15 +2242,23 @@ CStatusWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) //if (HotItem->Chars != (int)TextLen) // this condition failed when filter was attached if (HotItem != lastItem) { - // path truncation - CPathBuffer path; // Heap-allocated for long path support - strncpy(path, Text, HotItem->Chars); - path[HotItem->Chars] = 0; - - if (FilesWindow->Is(ptPluginFS) && FilesWindow->GetPluginFS()->NotEmpty()) - FilesWindow->GetPluginFS()->CompleteDirectoryLineHotPath(path, path.Size()); - - FilesWindow->ChangeDir(path, -1, NULL, 2 /* as back/forward in history*/, NULL, FALSE); + if (UseWideText && FilesWindow->Is(ptDisk)) + { + std::wstring pathW = TextW.substr(0, HotItem->Chars); + FilesWindow->ChangePathToDiskW(FilesWindow->HWindow, pathW.c_str(), -1, NULL, NULL, FALSE); + } + else + { + // path truncation + CPathBuffer path; // Heap-allocated for long path support + strncpy(path, Text, HotItem->Chars); + path[HotItem->Chars] = 0; + + if (FilesWindow->Is(ptPluginFS) && FilesWindow->GetPluginFS()->NotEmpty()) + FilesWindow->GetPluginFS()->CompleteDirectoryLineHotPath(path, path.Size()); + + FilesWindow->ChangeDir(path, -1, NULL, 2 /* as back/forward in history*/, NULL, FALSE); + } } else { diff --git a/src/stswnd.h b/src/stswnd.h index c7c160b43..ee66e9254 100644 --- a/src/stswnd.h +++ b/src/stswnd.h @@ -71,6 +71,8 @@ class CStatusWindow : public CWindow int Border; // separator line at top/bottom char* Text; + std::wstring TextW; + BOOL UseWideText; int TextLen; // number of characters in 'Text' without terminator char* Size; int PathLen; // -1 (path is the whole Text), otherwise path length in Text (rest is filter) @@ -139,6 +141,7 @@ class CStatusWindow : public CWindow // sets 'text' in the status line, 'pathLen' defines the path length (rest is filter), // if 'pathLen' is not used (path is the full 'text') it equals -1 BOOL SetText(const char* text, int pathLen = -1); + BOOL SetTextW(const wchar_t* text, int pathLen = -1); // builds HotTrackItems array: for disks and archivers based on backslashes // and for FS it asks the plugin diff --git a/src/texts.rh2 b/src/texts.rh2 index 29ada366b..54315a4f2 100644 --- a/src/texts.rh2 +++ b/src/texts.rh2 @@ -1461,6 +1461,10 @@ #define IDS_OLDPLUGINVERSION_CONFIRM 12027 // legacy req==102 plugin: ask whether to allow loading anyway (name unknown) #define IDS_OLDPLUGINVERSION_CONFIRM2 12028 +// file-system plugin was built against the broken transitional SDK 104 ABI (name known) +#define IDS_BROKENFSPLUGINVERSION 12029 +// file-system plugin was built against the broken transitional SDK 104 ABI (name unknown) +#define IDS_BROKENFSPLUGINVERSION2 12030 // edit or assoc from archive: update failed, do you want to try it again? #define IDS_UPDATEFAILED 12033 diff --git a/src/tserver/dialogs.cpp b/src/tserver/dialogs.cpp index 47601f281..77137d681 100644 --- a/src/tserver/dialogs.cpp +++ b/src/tserver/dialogs.cpp @@ -142,7 +142,7 @@ INT_PTR CPSPGeneral::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { case PSN_APPLY: { - int ret = CPropSheetPage::DialogProc(uMsg, wParam, lParam); + INT_PTR ret = CPropSheetPage::DialogProc(uMsg, wParam, lParam); LONG oldExStyle = GetWindowLong(MainWindow->HWindow, GWL_EXSTYLE); if (((oldExStyle & WS_EX_TOOLWINDOW) != 0) != ConfigData.UseToolbarCaption) { @@ -230,7 +230,7 @@ INT_PTR CPSPView::DialogProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { case PSN_APPLY: { - int ret = CPropSheetPage::DialogProc(uMsg, wParam, lParam); + INT_PTR ret = CPropSheetPage::DialogProc(uMsg, wParam, lParam); PostMessage(MainWindow->TabList->HWindow, WM_USER_HEADER_CHANGE, 0, 0); Registry.Save(); return ret; diff --git a/src/tserver/registry.cpp b/src/tserver/registry.cpp index 61079dc08..35000868b 100644 --- a/src/tserver/registry.cpp +++ b/src/tserver/registry.cpp @@ -42,6 +42,11 @@ const WCHAR REGERR_HKEY_CLASSES_ROOT[] = L"HKEY_CLASSES_ROOT"; const WCHAR REGERR_HKEY_CURRENT_USER[] = L"HKEY_CURRENT_USER"; const WCHAR REGERR_HKEY_UNKNOWN[] = L"?"; +static DWORD RegStringByteSize(const WCHAR* value) +{ + return (DWORD)(sizeof(WCHAR) * (wcslen(value) + 1)); +} + //***************************************************************************** // // GetHKeyName @@ -139,7 +144,7 @@ BOOL CReg::GetSize(HKEY hKey, const WCHAR* name, DWORD type, DWORD& dataSize, BO BOOL CRegBOOL::Save(HKEY hKey, const WCHAR* name, void* data) { const WCHAR* val = *(BOOL*)data ? L"True" : L"False"; - return SaveVoid(hKey, name, REG_SZ, val, sizeof(WCHAR) * (wcslen(val) + 1)); + return SaveVoid(hKey, name, REG_SZ, val, RegStringByteSize(val)); } BOOL CRegBOOL::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -195,7 +200,7 @@ BOOL CRegInt::Save(HKEY hKey, const WCHAR* name, void* data) { WCHAR buff[20]; swprintf_s(buff, L"%d", *(int*)data); - return SaveVoid(hKey, name, REG_SZ, buff, sizeof(WCHAR) * (wcslen(buff) + 1)); + return SaveVoid(hKey, name, REG_SZ, buff, RegStringByteSize(buff)); } BOOL CRegInt::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -216,7 +221,7 @@ BOOL CRegDouble::Save(HKEY hKey, const WCHAR* name, void* data) { WCHAR buff[30]; swprintf_s(buff, L"%f", *(double*)data); - return SaveVoid(hKey, name, REG_SZ, buff, sizeof(WCHAR) * (wcslen(buff) + 1)); + return SaveVoid(hKey, name, REG_SZ, buff, RegStringByteSize(buff)); } BOOL CRegDouble::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -241,7 +246,7 @@ BOOL CRegCOLORREF::Save(HKEY hKey, const WCHAR* name, void* data) BYTE g = GetGValue(color); BYTE b = GetBValue(color); swprintf_s(buff, L"%d,%d,%d", r, g, b); - return SaveVoid(hKey, name, REG_SZ, buff, sizeof(WCHAR) * (wcslen(buff) + 1)); + return SaveVoid(hKey, name, REG_SZ, buff, RegStringByteSize(buff)); } BOOL CRegCOLORREF::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -278,7 +283,7 @@ BOOL CRegWindowPlacement::Save(HKEY hKey, const WCHAR* name, void* data) wp->rcNormalPosition.top, wp->rcNormalPosition.right, wp->rcNormalPosition.bottom); - return SaveVoid(hKey, name, REG_SZ, buff, sizeof(WCHAR) * (wcslen(buff) + 1)); + return SaveVoid(hKey, name, REG_SZ, buff, RegStringByteSize(buff)); } BOOL CRegWindowPlacement::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -326,7 +331,7 @@ BOOL CRegLogFont::Save(HKEY hKey, const WCHAR* name, void* data) lf->lfQuality, lf->lfPitchAndFamily, lf->lfFaceName); - return SaveVoid(hKey, name, REG_SZ, buff, sizeof(WCHAR) * (wcslen(buff) + 1)); + return SaveVoid(hKey, name, REG_SZ, buff, RegStringByteSize(buff)); } BOOL CRegLogFont::Load(HKEY hKey, const WCHAR* name, void* data, BOOL* notFound) @@ -492,7 +497,7 @@ CRegistry::CRegistry() BOOL CRegistry::RegisterPath(HRegistryPath& hRegistryPath, HKEY hRootKey, const WCHAR* path, ...) { - int len = 1; + size_t len = 1; const WCHAR* iterator = path; va_list params; va_start(params, path); @@ -531,7 +536,7 @@ BOOL CRegistry::RegisterPath(HRegistryPath& hRegistryPath, HKEY hRootKey, const } va_end(params); - int lenNewPath = wcslen(newPath); + size_t lenNewPath = wcslen(newPath); if (lenNewPath > 0 && newPath[lenNewPath - 1] == L'\\') newPath[lenNewPath - 1] = 0; // W95 Hell diff --git a/src/tserver/tablist.cpp b/src/tserver/tablist.cpp index c270e6d42..3c5ed4a19 100644 --- a/src/tserver/tablist.cpp +++ b/src/tserver/tablist.cpp @@ -618,10 +618,10 @@ void CTabList::GetText(int iItem, int index, WCHAR* buff, int buffMax, BOOL pref const WCHAR* s = Data.Messages[iItem].Message; if (preferEndOfText) // we want to return the end of the text (e.g., for a tooltip) { - int len = wcslen(s); - if (len + 1 > buffMax) + size_t len = wcslen(s); + if (len + 1 > (size_t)buffMax) { - s += len - (buffMax - 1); + s += len - ((size_t)buffMax - 1); if (IS_LOW_SURROGATE(*s)) s++; // UTF-16 can contain surrogate pairs; skip the second code unit so the text does not start in the middle of a character } @@ -835,7 +835,7 @@ CTabList::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) // draw ERRORS in red if (cd->nmcd.dwDrawStage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM)) { - int index = cd->nmcd.dwItemSpec; + int index = (int)cd->nmcd.dwItemSpec; if (index >= 0 && index < Data.Messages.Count) { if (IsErrorMsg(Data.Messages[index].Type)) diff --git a/src/tserver/window.cpp b/src/tserver/window.cpp index 456397690..28c8da333 100644 --- a/src/tserver/window.cpp +++ b/src/tserver/window.cpp @@ -236,6 +236,23 @@ void AddChars(WCHAR* buffer, int count, WCHAR chr = L' ') *p = 0; } +void AddChars(WCHAR* buffer, size_t count, WCHAR chr = L' ') +{ + WCHAR* p; + p = buffer + wcslen(buffer); + for (size_t i = 0; i < count; i++) + { + *p = chr; + p++; + } + *p = 0; +} + +DWORD StringByteCount(const WCHAR* text) +{ + return (DWORD)(sizeof(WCHAR) * wcslen(text)); +} + void CMainWindow::ExportAllMessages() { WCHAR fileName[MAX_PATH]; @@ -286,18 +303,18 @@ void CMainWindow::ExportAllMessages() const WCHAR* strLine = L"Line"; const WCHAR* strMessage = L"Message"; - DWORD maxPID = wcslen(strPID); - DWORD maxUPID = wcslen(strUPID); - DWORD maxPName = wcslen(strPName); - DWORD maxTID = wcslen(strTID); - DWORD maxUTID = wcslen(strUTID); - DWORD maxTName = wcslen(strTName); - DWORD maxDate = wcslen(strDate); - DWORD maxTime = wcslen(strTime); - DWORD maxCounter = wcslen(strCounter); - DWORD maxModule = wcslen(strModule); - DWORD maxLine = wcslen(strLine); - DWORD maxMessage = wcslen(strMessage); + size_t maxPID = wcslen(strPID); + size_t maxUPID = wcslen(strUPID); + size_t maxPName = wcslen(strPName); + size_t maxTID = wcslen(strTID); + size_t maxUTID = wcslen(strUTID); + size_t maxTName = wcslen(strTName); + size_t maxDate = wcslen(strDate); + size_t maxTime = wcslen(strTime); + size_t maxCounter = wcslen(strCounter); + size_t maxModule = wcslen(strModule); + size_t maxLine = wcslen(strLine); + size_t maxMessage = wcslen(strMessage); for (int i = 0; i < Data.Messages.Count; i++) { @@ -373,7 +390,7 @@ void CMainWindow::ExportAllMessages() // print the header swprintf_s(line, L"\xFEFF" /* BOM */ L"Trace Server Log File\r\n\r\n"); - WriteFile(file, line, sizeof(WCHAR) * wcslen(line), &written, NULL); + WriteFile(file, line, StringByteCount(line), &written, NULL); // separator @@ -428,7 +445,7 @@ void CMainWindow::ExportAllMessages() wcscat_s(separator, L"\r\n"); - WriteFile(file, separator, sizeof(WCHAR) * wcslen(separator), &written, NULL); + WriteFile(file, separator, StringByteCount(separator), &written, NULL); wcscpy_s(line, L" |"); @@ -485,9 +502,9 @@ void CMainWindow::ExportAllMessages() SWPrintFToEnd_s(line, L"\r\n"); - WriteFile(file, line, sizeof(WCHAR) * wcslen(line), &written, NULL); + WriteFile(file, line, StringByteCount(line), &written, NULL); - WriteFile(file, separator, sizeof(WCHAR) * wcslen(separator), &written, NULL); + WriteFile(file, separator, StringByteCount(separator), &written, NULL); // dump the rows into the file for (int i = 0; i < Data.Messages.Count; i++) @@ -567,9 +584,9 @@ void CMainWindow::ExportAllMessages() SWPrintFToEnd_s(line, L"%s", buff); SWPrintFToEnd_s(line, L"\r\n"); - WriteFile(file, line, sizeof(WCHAR) * wcslen(line), &written, NULL); + WriteFile(file, line, StringByteCount(line), &written, NULL); } - WriteFile(file, separator, sizeof(WCHAR) * wcslen(separator), &written, NULL); + WriteFile(file, separator, StringByteCount(separator), &written, NULL); HANDLES(CloseHandle(file)); } } @@ -967,14 +984,14 @@ CMainWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_INCORRECT_VERSION: { - MESSAGE_EW(NULL, L"Incorrect version of client, connection refused." << L"\nClient PID: " << lParam << L"\nClient version: " << wParam << L"\nServer version: " << TRACE_SERVER_VERSION, MB_OK); + MESSAGE_EW(NULL, L"Incorrect version of client, connection refused." << L"\nClient PID: " << lParam << L"\nClient version: " << wParam << L"\nServer version: " << (DWORD)TRACE_SERVER_VERSION, MB_OK); return 0; } case WM_USER_SHOWSYSTEMERROR: { MESSAGE_EW(NULL, L"Error during reading client->server pipe:\n" - << errW(wParam), + << errW((DWORD)wParam), MB_OK); return 0; } diff --git a/src/viewer.cpp b/src/viewer.cpp index 2e62bf2f8..4d85ccdc2 100644 --- a/src/viewer.cpp +++ b/src/viewer.cpp @@ -16,6 +16,7 @@ #include "execute.h" #include "gui.h" #include "darkmode.h" +#include "common/unicode/helpers.h" const char* CVIEWERWINDOW_CLASSNAME = "Salamander's Viewer Window"; @@ -556,15 +557,18 @@ CViewerWindow::CViewerWindow(const char* fileName, CViewType type, const char* c TextEncoding = Sally::Unicode::BomEncoding::LegacyBytes; TextContentOffset = 0; if (fileName == NULL) - FileName.clear(); // error + ClearViewedFile(); // error else { CPathBuffer name; // Heap-allocated for long path support lstrcpyn(name, fileName, name.Size()); if (SalGetFullName(name, NULL, NULL, NULL, NULL, name.Size())) + { FileName = (const char*)name; + FileNameW = AnsiToWide(name); + } else - FileName.clear(); + ClearViewedFile(); } Buffer = (unsigned char*)malloc(VIEW_BUFFER_SIZE); Seek = 0; diff --git a/src/viewer.h b/src/viewer.h index d9295c4b3..97e95f6c0 100644 --- a/src/viewer.h +++ b/src/viewer.h @@ -125,6 +125,7 @@ class CViewerWindow : public CWindow ~CViewerWindow(); void OpenFile(const char* file, const char* caption, BOOL wholeCaption); // does not manage Lock + void OpenFileW(const wchar_t* file, const char* caption, BOOL wholeCaption); // does not manage Lock virtual BOOL Is(int type) { return type == otViewerWindow || CWindow::Is(type); } BOOL IsGood() { return Buffer != NULL && ViewerFont != NULL; } @@ -166,6 +167,8 @@ class CViewerWindow : public CWindow protected: void FatalFileErrorOccured(DWORD repeatCmd = -1); // called when a file error occurs (viewer refresh/clear is required) + HANDLE OpenViewedFile(DWORD flags) const; + void ClearViewedFile(); void OnVScroll(); @@ -329,7 +332,8 @@ class CViewerWindow : public CWindow int SalMessageBoxViewerPaintBlocked(HWND hParent, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType); unsigned char* Buffer; // buffer with size VIEW_BUFFER_SIZE - std::string FileName; // currently viewed file + std::string FileName; // currently viewed file, ANSI compatibility mirror + std::wstring FileNameW; // currently viewed file, exact UTF-16 path when available __int64 Seek, // offset of byte 0 in Buffer within the file Loaded, // number of valid bytes in Buffer OriginX, // first displayed column (in characters) @@ -464,3 +468,7 @@ BOOL OpenViewer(const char* name, CViewType mode, int left, int top, int width, UINT showCmd, BOOL returnLock, HANDLE* lock, BOOL* lockOwner, CSalamanderPluginViewerData* viewerData, int enumFileNamesSourceUID, int enumFileNamesLastFileIndex); +BOOL OpenViewerW(const wchar_t* nameW, const char* nameA, CViewType mode, int left, int top, + int width, int height, UINT showCmd, BOOL returnLock, HANDLE* lock, + BOOL* lockOwner, CSalamanderPluginViewerData* viewerData, + int enumFileNamesSourceUID, int enumFileNamesLastFileIndex); diff --git a/src/viewer_interaction_scrolling.cpp b/src/viewer_interaction_scrolling.cpp index a7f31f9a6..d1888c289 100644 --- a/src/viewer_interaction_scrolling.cpp +++ b/src/viewer_interaction_scrolling.cpp @@ -26,6 +26,36 @@ BOOL ViewerActive(HWND hwnd) void CViewerWindow::SetViewerCaption() { + if (!FileNameW.empty()) + { + std::wstring captionW; + if (Caption.empty()) + captionW = FileNameW; + else + captionW = AnsiToWide(Caption.c_str()); + if (Caption.empty() || !WholeCaption) + { + if (!captionW.empty()) + captionW += L" - "; + captionW += AnsiToWide(LoadStr(IDS_VIEWERTITLE)); + if (CodeType > 0) + { + char codeName[200]; + CodeTables.GetCodeName(CodeType, codeName, 200); + RemoveAmpersands(codeName); + char* s = codeName + strlen(codeName); + while (s > codeName && *(s - 1) == ' ') + s--; + *s = 0; + captionW += L" - ["; + captionW += AnsiToWide(codeName); + captionW += L"]"; + } + } + SetWindowTextW(HWindow, captionW.c_str()); + return; + } + CPathBuffer caption; if (Caption.empty()) { diff --git a/src/viewer_thread_buffering.cpp b/src/viewer_thread_buffering.cpp index 3361072ef..577431c5a 100644 --- a/src/viewer_thread_buffering.cpp +++ b/src/viewer_thread_buffering.cpp @@ -20,6 +20,7 @@ struct CTVData int Left, Top, Width, Height; CViewerWindow* View; const char* Name; + const wchar_t* NameW; UINT ShowCmd; BOOL Success; const char* Caption; @@ -50,7 +51,13 @@ unsigned ThreadViewerMessageLoopBody(void* parameter) CTVData* data = (CTVData*)parameter; CViewerWindow* view = data->View; CPathBuffer name; - strcpy(name, data->Name); + if (data->Name != NULL) + strcpy(name, data->Name); + else + name[0] = 0; + std::wstring nameW; + if (data->NameW != NULL && data->NameW[0] != L'\0') + nameW = data->NameW; CPathBuffer captionBuf; const char* caption = NULL; BOOL wholeCaption = FALSE; @@ -113,7 +120,12 @@ unsigned ThreadViewerMessageLoopBody(void* parameter) if (ok) // if the window was created, run the application loop { CALL_STACK_MESSAGE1("ThreadViewerMessageLoopBody::message_loop"); - if (SalGetFullName(name, NULL, NULL, NULL, NULL, name.Size())) + if (!nameW.empty()) + { + if (SalGetFullNameW(nameW)) + view->OpenFileW(nameW.c_str(), caption, wholeCaption); + } + else if (SalGetFullName(name, NULL, NULL, NULL, NULL, name.Size())) view->OpenFile(name, caption, wholeCaption); MSG msg; @@ -163,9 +175,19 @@ BOOL OpenViewer(const char* name, CViewType mode, int left, int top, int width, UINT showCmd, BOOL returnLock, HANDLE* lock, BOOL* lockOwner, CSalamanderPluginViewerData* viewerData, int enumFileNamesSourceUID, int enumFileNamesLastFileIndex) +{ + return OpenViewerW(NULL, name, mode, left, top, width, height, showCmd, returnLock, + lock, lockOwner, viewerData, enumFileNamesSourceUID, + enumFileNamesLastFileIndex); +} + +BOOL OpenViewerW(const wchar_t* nameW, const char* nameA, CViewType mode, int left, int top, + int width, int height, UINT showCmd, BOOL returnLock, HANDLE* lock, + BOOL* lockOwner, CSalamanderPluginViewerData* viewerData, + int enumFileNamesSourceUID, int enumFileNamesLastFileIndex) { CALL_STACK_MESSAGE11("OpenViewer(%s, %d, %d, %d, %d, %d, %u, %d, , , , %d, %d)", - name, mode, left, top, width, height, showCmd, returnLock, + nameA != NULL ? nameA : "", mode, left, top, width, height, showCmd, returnLock, enumFileNamesSourceUID, enumFileNamesLastFileIndex); CSalamanderPluginInternalViewerData* intViewerData = NULL; if (viewerData != NULL && viewerData->Size == sizeof(CSalamanderPluginInternalViewerData)) @@ -192,9 +214,11 @@ BOOL OpenViewer(const char* name, CViewType mode, int left, int top, int width, data.Top = top; data.Width = width; data.Height = height; - data.Name = name; + data.Name = nameA; + data.NameW = nameW; data.ShowCmd = showCmd; data.Caption = intViewerData != NULL ? intViewerData->Caption : NULL; + data.WholeCaption = intViewerData != NULL ? intViewerData->WholeCaption : FALSE; DWORD ThreadID; HANDLE loop = HANDLES(CreateThread(NULL, 0, ThreadViewerMessageLoop, &data, 0, &ThreadID)); @@ -353,14 +377,13 @@ void CViewerWindow::CodeCharacters(unsigned char* start, unsigned char* end) BOOL CViewerWindow::LoadBefore(HANDLE* hFile) { CALL_STACK_MESSAGE1("CViewerWindow::LoadBefore()"); - if (FileName.empty()) + if (FileName.empty() && FileNameW.empty()) return FALSE; HANDLE file; if (hFile == NULL || *hFile == NULL) { - file = SalCreateFileH(FileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, - OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); + file = OpenViewedFile(FILE_FLAG_SEQUENTIAL_SCAN); if (hFile != NULL && file != INVALID_HANDLE_VALUE) *hFile = file; } @@ -454,8 +477,7 @@ BOOL CViewerWindow::LoadBefore(HANDLE* hFile) if (!ret && kill) // possibly end working with this file { - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); @@ -476,8 +498,7 @@ BOOL CViewerWindow::LoadBefore(HANDLE* hFile) { DWORD err = GetLastError(); Seek = Loaded = 0; - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); @@ -498,14 +519,13 @@ BOOL CViewerWindow::LoadBefore(HANDLE* hFile) BOOL CViewerWindow::LoadBehind(HANDLE* hFile) { CALL_STACK_MESSAGE1("CViewerWindow::LoadBehind()"); - if (FileName.empty()) + if (FileName.empty() && FileNameW.empty()) return FALSE; HANDLE file; if (hFile == NULL || *hFile == NULL) { - file = SalCreateFileH(FileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, - FILE_FLAG_SEQUENTIAL_SCAN, NULL); + file = OpenViewedFile(FILE_FLAG_SEQUENTIAL_SCAN); if (hFile != NULL && file != INVALID_HANDLE_VALUE) *hFile = file; } @@ -608,8 +628,7 @@ BOOL CViewerWindow::LoadBehind(HANDLE* hFile) if (!ret && kill) // possibly end working with this file { - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); @@ -630,8 +649,7 @@ BOOL CViewerWindow::LoadBehind(HANDLE* hFile) { DWORD err = GetLastError(); Seek = Loaded = 0; - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); @@ -684,6 +702,7 @@ void CViewerWindow::OpenFile(const char* file, const char* caption, BOOL wholeCa else WholeCaption = FALSE; FileName = (const char*)fileName; + FileNameW = AnsiToWide(fileName); TooBigSelAction = 0; CanSwitchToHex = TRUE; CanSwitchQuietlyToHex = TRUE; @@ -713,6 +732,70 @@ void CViewerWindow::OpenFile(const char* file, const char* caption, BOOL wholeCa CanSwitchQuietlyToHex = FALSE; } +void CViewerWindow::OpenFileW(const wchar_t* file, const char* caption, BOOL wholeCaption) +{ + std::wstring fileNameW = file != NULL ? std::wstring(file) : std::wstring(); + CALL_STACK_MESSAGE3("CViewerWindow::OpenFileW(%S, %s)", fileNameW.c_str(), caption); + + Caption.clear(); + if (caption != NULL) + { + Caption = caption; + WholeCaption = wholeCaption; + } + else + WholeCaption = FALSE; + FileNameW = fileNameW; + FileName = WideToAnsi(FileNameW); + TooBigSelAction = 0; + CanSwitchToHex = TRUE; + CanSwitchQuietlyToHex = TRUE; + OriginX = 0; + SeekY = 0; + ExitTextMode = FALSE; + ForceTextMode = FALSE; + CodeType = 0; + UseCodeTable = FALSE; + TextEncoding = Sally::Unicode::BomEncoding::LegacyBytes; + TextContentOffset = 0; + BOOL fatalErr = FALSE; + FileChanged(NULL, FALSE, fatalErr, TRUE); + if (fatalErr) + FatalFileErrorOccured(); + if (fatalErr || ExitTextMode) + { + CanSwitchQuietlyToHex = FALSE; + return; + } + if (FileNameW.empty()) + SetWindowText(HWindow, LoadStr(IDS_VIEWERTITLE)); + else + SetViewerCaption(); + InvalidateRect(HWindow, NULL, FALSE); + UpdateWindow(HWindow); + CanSwitchQuietlyToHex = FALSE; +} + +HANDLE CViewerWindow::OpenViewedFile(DWORD flags) const +{ + if (!FileNameW.empty()) + { + return SalCreateFileWideH(FileNameW.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, flags, NULL); + } + + return SalCreateFileH(FileName.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, flags, NULL); +} + +void CViewerWindow::ClearViewedFile() +{ + FileName.clear(); + FileNameW.clear(); +} + void CViewerWindow::ReleaseMouseDrag() { if (MouseDrag) @@ -775,7 +858,7 @@ void CViewerWindow::FileChanged(HANDLE file, BOOL testOnlyFileSize, BOOL& fatalE fatalErr = FALSE; if (calledHeightChanged != NULL) *calledHeightChanged = FALSE; - if (FileName.empty()) + if (FileName.empty() && FileNameW.empty()) return; const char* s = strrchr(FileName.c_str(), '\\'); @@ -792,8 +875,7 @@ void CViewerWindow::FileChanged(HANDLE file, BOOL testOnlyFileSize, BOOL& fatalE BOOL close; if (file == NULL) { - file = SalCreateFileH(FileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, - FILE_FLAG_SEQUENTIAL_SCAN, NULL); + file = OpenViewedFile(FILE_FLAG_SEQUENTIAL_SCAN); close = TRUE; } else @@ -817,8 +899,7 @@ void CViewerWindow::FileChanged(HANDLE file, BOOL testOnlyFileSize, BOOL& fatalE ReleaseMouseDrag(); FirstLineSize = LastLineSize = ViewSize = 0; LastFindSeekY = -1; - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); @@ -1001,8 +1082,7 @@ void CViewerWindow::FileChanged(HANDLE file, BOOL testOnlyFileSize, BOOL& fatalE ReleaseMouseDrag(); FirstLineSize = LastLineSize = ViewSize = 0; LastFindSeekY = -1; - FileName.clear(); - FileName.clear(); + ClearViewedFile(); if (!Caption.empty()) { Caption.clear(); diff --git a/src/worker.cpp b/src/worker.cpp index d9598a25f..423a3ffe6 100644 --- a/src/worker.cpp +++ b/src/worker.cpp @@ -6,6 +6,7 @@ #include "cfgdlg.h" #include "worker.h" +#include "common/CreateDirectoryFlow.h" #include "common/fsutil.h" #include "common/CopyStrategy.h" #include "common/widepath.h" @@ -14,6 +15,7 @@ #include "DialogWorkerObserver.h" #include "common/unicode/helpers.h" #include "common/unicode/PathIdentityPolicy.h" +#include "common/unicode/PanelPathPolicy.h" #include #include @@ -508,45 +510,61 @@ void COperation::PopulateWidePathsFromAnsi() } } -void COperation::SetSourceNameW(const char* ansiPath, const std::wstring& wideFileName) +static std::wstring BuildOperationNameW(std::wstring widePath, const std::wstring& wideFileName) { - if (ansiPath == NULL) - return; - - // Convert directory path to wide - int pathLen = MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, NULL, 0); - if (pathLen == 0) - return; - - std::wstring widePath; - widePath.resize(pathLen); - MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, &widePath[0], pathLen); - widePath.resize(pathLen - 1); // Remove null terminator from size - if (!wideFileName.empty()) { - // Append wide filename if (!widePath.empty() && widePath.back() != L'\\') widePath += L'\\'; widePath += wideFileName; } - // Add \\?\ prefix for long paths - if (widePath.length() >= SAL_LONG_PATH_THRESHOLD) + if (widePath.length() >= SAL_LONG_PATH_THRESHOLD && + widePath.compare(0, 4, L"\\\\?\\") != 0) { if (widePath.length() >= 2 && widePath[0] == L'\\' && widePath[1] == L'\\') - { - // UNC path: \\server\share -> \\?\UNC\server\share widePath = L"\\\\?\\UNC\\" + widePath.substr(2); - } else - { - // Local path: C:\foo -> \\?\C:\foo widePath = L"\\\\?\\" + widePath; - } } - SourceNameW = std::move(widePath); + return widePath; +} + +void COperations::ReanchorWideSourcePaths(const char* anchorAnsi, const wchar_t* anchorWide) +{ + for (size_t i = 0; i < m_ops.size(); ++i) + { + COperation& op = m_ops[i]; + if (!op.OwnsSourceName || op.SourceName == NULL || op.SourceNameWExplicit) + continue; + + std::wstring rebound = sally::unicode::RebindAnsiPathToWideAnchor( + op.SourceName, anchorAnsi, anchorWide); + if (rebound.empty()) + continue; + + op.SourceNameW = BuildOperationNameW(std::move(rebound), std::wstring()); + op.SourceNameWExplicit = true; + } +} + +void COperation::SetSourceNameW(const char* ansiPath, const std::wstring& wideFileName) +{ + if (ansiPath == NULL) + return; + + // Convert directory path to wide + int pathLen = MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, NULL, 0); + if (pathLen == 0) + return; + + std::wstring widePath; + widePath.resize(pathLen); + MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, &widePath[0], pathLen); + widePath.resize(pathLen - 1); // Remove null terminator from size + + SetSourceNameW(widePath, wideFileName); } void COperation::SetTargetNameW(const char* ansiPath, const std::wstring& wideFileName) @@ -564,30 +582,19 @@ void COperation::SetTargetNameW(const char* ansiPath, const std::wstring& wideFi MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, &widePath[0], pathLen); widePath.resize(pathLen - 1); // Remove null terminator from size - if (!wideFileName.empty()) - { - // Append wide filename - if (!widePath.empty() && widePath.back() != L'\\') - widePath += L'\\'; - widePath += wideFileName; - } + SetTargetNameW(widePath, wideFileName); +} - // Add \\?\ prefix for long paths - if (widePath.length() >= SAL_LONG_PATH_THRESHOLD) - { - if (widePath.length() >= 2 && widePath[0] == L'\\' && widePath[1] == L'\\') - { - // UNC path: \\server\share -> \\?\UNC\server\share - widePath = L"\\\\?\\UNC\\" + widePath.substr(2); - } - else - { - // Local path: C:\foo -> \\?\C:\foo - widePath = L"\\\\?\\" + widePath; - } - } +void COperation::SetSourceNameW(const std::wstring& widePath, const std::wstring& wideFileName) +{ + SourceNameW = BuildOperationNameW(widePath, wideFileName); + SourceNameWExplicit = true; +} - TargetNameW = std::move(widePath); +void COperation::SetTargetNameW(const std::wstring& widePath, const std::wstring& wideFileName) +{ + TargetNameW = BuildOperationNameW(widePath, wideFileName); + TargetNameWExplicit = true; } @@ -3703,7 +3710,8 @@ void DoCopyFileLoopOrig(HANDLE& in, HANDLE& out, void* buffer, int& limitBufferS ret = IDCANCEL; if (err == NO_ERROR && read != written) err = ERROR_DISK_FULL; - ret = observer.AskFileErrorById(IDS_ERRORWRITINGFILE, op->TargetName, err); + ret = observer.AskFileErrorByIdW(IDS_ERRORWRITINGFILE, op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, err); switch (ret) { case IDRETRY: // on a network we must reopen the handle; local access forbids it due to sharing @@ -5278,7 +5286,9 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, goto SKIP_COPY; int ret = IDCANCEL; - ret = observer.AskFileErrorById(IDS_ERRORWRITINGFILE, op->TargetName, ERROR_DISK_FULL); + ret = observer.AskFileErrorByIdW(IDS_ERRORWRITINGFILE, op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + ERROR_DISK_FULL); switch (ret) { case IDRETRY: @@ -5444,7 +5454,8 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, goto SKIP_COPY; int ret = IDCANCEL; - ret = observer.AskFileErrorById(IDS_ERRORWRITINGFILE, op->TargetName, err); + ret = observer.AskFileErrorByIdW(IDS_ERRORWRITINGFILE, op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, err); switch (ret) { case IDRETRY: @@ -5523,7 +5534,11 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, ret = IDB_IGNORE; else { - ret = observer.AskCopyPermError(op->SourceName, op->TargetName, (char*)(DWORD_PTR)err); + ret = observer.AskCopyPermErrorW(op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + (char*)(DWORD_PTR)err); } switch (ret) { @@ -5646,7 +5661,11 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, else { // show the prompt - ret = observer.AskOverwrite(op->TargetName, tAttr, op->SourceName, sAttr); + ret = observer.AskOverwriteW(op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + tAttr, op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + sAttr); } switch (ret) { @@ -5902,7 +5921,10 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, int ret; ret = IDCANCEL; - ret = observer.AskFileErrorById(errDeletingFile ? IDS_ERRORDELETINGFILE : IDS_ERROROPENINGFILE, op->TargetName, err); + ret = observer.AskFileErrorByIdW(errDeletingFile ? IDS_ERRORDELETINGFILE : IDS_ERROROPENINGFILE, + op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + err); switch (ret) { case IDRETRY: @@ -5939,7 +5961,8 @@ BOOL DoCopyFile(COperation* op, IWorkerObserver& observer, void* buffer, int ret; ret = IDCANCEL; - ret = observer.AskFileErrorById(IDS_ERROROPENINGFILE, op->SourceName, err); + ret = observer.AskFileErrorByIdW(IDS_ERROROPENINGFILE, op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, err); switch (ret) { case IDRETRY: @@ -6020,7 +6043,11 @@ BOOL DoMoveFile(COperation* op, IWorkerObserver& observer, void* buffer, ret = IDB_IGNORE; else { - ret = observer.AskCopyPermError(op->SourceName, op->TargetName, (char*)(DWORD_PTR)srcSecurity.SrcError); + ret = observer.AskCopyPermErrorW(op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + (char*)(DWORD_PTR)srcSecurity.SrcError); } switch (ret) { @@ -6098,7 +6125,11 @@ BOOL DoMoveFile(COperation* op, IWorkerObserver& observer, void* buffer, ret = IDB_IGNORE; else { - ret = observer.AskCopyPermError(op->SourceName, op->TargetName, (char*)(DWORD_PTR)err); + ret = observer.AskCopyPermErrorW(op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + (char*)(DWORD_PTR)err); } switch (ret) { @@ -6296,7 +6327,11 @@ BOOL DoMoveFile(COperation* op, IWorkerObserver& observer, void* buffer, else { // display the prompt - ret = observer.AskOverwrite(op->TargetName, tAttr, op->SourceName, sAttr); + ret = observer.AskOverwriteW(op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + tAttr, op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + sAttr); } switch (ret) { @@ -6390,7 +6425,8 @@ BOOL DoMoveFile(COperation* op, IWorkerObserver& observer, void* buffer, int ret; ret = IDCANCEL; - ret = observer.AskFileErrorById(IDS_ERROROVERWRITINGFILE, op->TargetName, err2); + ret = observer.AskFileErrorByIdW(IDS_ERROROVERWRITINGFILE, op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, err2); switch (ret) { case IDRETRY: @@ -6433,7 +6469,11 @@ BOOL DoMoveFile(COperation* op, IWorkerObserver& observer, void* buffer, { int ret; ret = IDCANCEL; - ret = observer.AskCannotMoveErr(op->SourceName, op->TargetName, err, dir != FALSE); + ret = observer.AskCannotMoveErrW(op->SourceName, + op->HasWideSource() ? op->SourceNameW.c_str() : NULL, + op->TargetName, + op->HasWideTarget() ? op->TargetNameW.c_str() : NULL, + err, dir != FALSE); switch (ret) { case IDRETRY: @@ -6704,95 +6744,7 @@ BOOL DoDeleteFile(IWorkerObserver& observer, char* name, const CQuadWord& size, BOOL SalCreateDirectoryEx(const char* name, DWORD* err) { - if (err != NULL) - *err = 0; - // if the name ends with a space/dot we must append '\\', otherwise CreateDirectory - // quietly trims the trailing spaces/dots and creates a different directory - std::wstring nameW = AnsiToWide(name); - std::wstring nameCrDirW = MakeCopyWithBackslashIfNeededW(nameW.c_str()); - bool nameUnmodified = (nameCrDirW == nameW); // true when no trailing space/dot fixup was needed - if (CreateDirectoryW(nameCrDirW.c_str(), NULL)) - return TRUE; - else - { - DWORD errLoc = GetLastError(); - if (nameUnmodified && // a name ending with a space/dot cannot collide with a DOS name - (errLoc == ERROR_FILE_EXISTS || // check whether this is only overwriting the file's DOS name - errLoc == ERROR_ALREADY_EXISTS)) - { - IFileSystem* fileSystem = GetWorkerFileSystem(); - WIN32_FIND_DATAW data; - HANDLE find = fileSystem != NULL ? fileSystem->FindFirstFile(nameW.c_str(), &data) - : INVALID_HANDLE_VALUE; - if (find != INVALID_HANDLE_VALUE) - { - HANDLES(FindClose(find)); - const char* tgtName = SalPathFindFileName(name); - char cAltNameA[14]; - WideCharToMultiByte(CP_ACP, 0, data.cAlternateFileName, -1, cAltNameA, 14, NULL, NULL); - // Compare tgtName against the ANSI alternate (DOS) name; use wide cFileName directly - char cFileNameA[MAX_PATH]; - WideCharToMultiByte(CP_ACP, 0, data.cFileName, -1, cFileNameA, MAX_PATH, NULL, NULL); - if (StrICmp(tgtName, cAltNameA) == 0 && // match only for the DOS name - StrICmp(tgtName, cFileNameA) != 0) // (the full name differs) - { - // rename ("tidy up") the file/directory whose DOS name conflicts to a temporary 8.3 name (no extra DOS name needed) - std::wstring tmpNameW = nameW; - CutDirectoryW(tmpNameW); - SalPathAddBackslashW(tmpNameW); - size_t tmpNamePartPos = tmpNameW.size(); - std::wstring origFullNameW = tmpNameW; - SalPathAppendW(origFullNameW, data.cFileName); // use wide cFileName directly — no ANSI lossy conversion - tmpNameW = origFullNameW; - { - DWORD num = (GetTickCount() / 10) % 0xFFF; - DWORD origFullNameAttr = GetFileAttributesW(origFullNameW.c_str()); - while (1) - { - wchar_t tmpSuffix[8]; - swprintf(tmpSuffix, _countof(tmpSuffix), L"sal%03X", num++); - tmpNameW.resize(tmpNamePartPos); - tmpNameW += tmpSuffix; - if (MoveFileW(origFullNameW.c_str(), tmpNameW.c_str())) - break; - DWORD e = GetLastError(); - if (e != ERROR_FILE_EXISTS && e != ERROR_ALREADY_EXISTS) - { - tmpNameW.clear(); - break; - } - } - if (!tmpNameW.empty()) // if we managed to "tidy up" the conflicting file, retry the create - { // and then restore the original name of the "tidied" file - BOOL createDirDone = CreateDirectoryW(nameW.c_str(), NULL); - if (!MoveFileW(tmpNameW.c_str(), origFullNameW.c_str())) - { // this can apparently happen: inexplicably Windows creates a file named origFullName instead of name (the DOS name) - TRACE_I("Unexpected situation: unable to rename file from tmp-name to original long file name!"); - if (createDirDone) - { - if (RemoveDirectoryW(nameW.c_str())) - createDirDone = FALSE; - if (!MoveFileW(tmpNameW.c_str(), origFullNameW.c_str())) - TRACE_E("Fatal unexpected situation: unable to rename file from tmp-name to original long file name!"); - } - } - else - { - if ((origFullNameAttr & FILE_ATTRIBUTE_ARCHIVE) == 0) - SetFileAttributesW(origFullNameW.c_str(), origFullNameAttr); // leave it without extra handling or retries; not important (normally toggles unpredictably) - } - - if (createDirDone) - return TRUE; - } - } - } - } - } - if (err != NULL) - *err = errLoc; - } - return FALSE; + return SalCreateDirectoryExW(AnsiToWide(name).c_str(), err); } // ANSI wrapper — delegates to wide version @@ -6863,7 +6815,7 @@ BOOL DoCopyDirTime(IWorkerObserver& observer, const char* targetName, FILETIME* ret = IDB_IGNORE; else { - ret = observer.AskCopyDirTimeError(targetName, error); + ret = observer.AskCopyDirTimeErrorW(targetName, targetNameW.empty() ? NULL : targetNameW.c_str(), error); } switch (ret) { @@ -8243,6 +8195,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->SourceName; pd.Preposition = workerState.OpStrCopyingPrep; pd.Target = op->TargetName; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8265,6 +8219,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->SourceName; pd.Preposition = workerState.OpStrMovingPrep; pd.Target = op->TargetName; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8291,6 +8247,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->TargetName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8383,6 +8341,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->TargetName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8412,6 +8372,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8461,6 +8423,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8477,6 +8441,8 @@ unsigned ThreadWorkerBody(void* parameter) pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8607,6 +8573,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = workerState.OpStrCopyingPrep; pd.Target = op->TargetName; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8626,6 +8594,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = workerState.OpStrMovingPrep; pd.Target = op->TargetName; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8646,6 +8616,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8663,6 +8635,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->TargetName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideTarget() ? op->TargetNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8712,6 +8686,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8763,6 +8739,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); @@ -8777,6 +8755,8 @@ BOOL RunWorkerDirect(COperations* script, IWorkerObserver& observer, pd.Source = op->SourceName; pd.Preposition = ""; pd.Target = ""; + pd.SourceW = op->HasWideSource() ? op->SourceNameW.c_str() : NULL; + pd.TargetW = NULL; observer.SetOperationInfo(&pd); observer.SetProgress(0, CaclProg(totalDone, script->TotalSize)); diff --git a/src/worker.h b/src/worker.h index c7146c275..519a24b8b 100644 --- a/src/worker.h +++ b/src/worker.h @@ -82,6 +82,8 @@ struct CProgressData *Source, *Preposition, *Target; + const wchar_t *SourceW, + *TargetW; }; // @@ -227,6 +229,8 @@ struct COperation *TargetName; std::wstring SourceNameW; // Unicode source path (for long path and Unicode filename support) std::wstring TargetNameW; // Unicode target path (for long path and Unicode filename support) + bool SourceNameWExplicit = false; // true when SetSourceNameW supplied a real wide path/name + bool TargetNameWExplicit = false; // true when SetTargetNameW supplied a real wide path/name DWORD Attr; DWORD OpFlags; // combination of OPFL_xxx, see above @@ -261,6 +265,7 @@ struct COperation : Opcode(other.Opcode), Size(other.Size), FileSize(other.FileSize), SourceName(other.SourceName), TargetName(other.TargetName), SourceNameW(std::move(other.SourceNameW)), TargetNameW(std::move(other.TargetNameW)), + SourceNameWExplicit(other.SourceNameWExplicit), TargetNameWExplicit(other.TargetNameWExplicit), Attr(other.Attr), OpFlags(other.OpFlags), OwnsSourceName(other.OwnsSourceName), OwnsTargetName(other.OwnsTargetName) { @@ -291,6 +296,8 @@ struct COperation TargetName = other.TargetName; SourceNameW = std::move(other.SourceNameW); TargetNameW = std::move(other.TargetNameW); + SourceNameWExplicit = other.SourceNameWExplicit; + TargetNameWExplicit = other.TargetNameWExplicit; Attr = other.Attr; OpFlags = other.OpFlags; OwnsSourceName = other.OwnsSourceName; @@ -317,6 +324,8 @@ struct COperation // wideFileName: the actual Unicode filename (from CFileData::NameW), or empty to widen ANSI void SetSourceNameW(const char* ansiPath, const std::wstring& wideFileName); void SetTargetNameW(const char* ansiPath, const std::wstring& wideFileName); + void SetSourceNameW(const std::wstring& widePath, const std::wstring& wideFileName); + void SetTargetNameW(const std::wstring& widePath, const std::wstring& wideFileName); // File operation helpers that automatically use wide paths when available HANDLE OpenSourceFile(DWORD flags) const; @@ -387,6 +396,15 @@ class COperations Count = 0; } + // Re-anchor auto-widened SourceNameW values so paths under `anchorAnsi` + // are rebound to `anchorWide`. Script builders increasingly call + // SetSourceNameW with the panel PathW and CFileData::NameW; those explicit + // values are already richer than SourceName and must not be overwritten. + // + // Matching is case-insensitive on the prefix bytes; ops whose SourceName + // does not start with `anchorAnsi` are left untouched. + void ReanchorWideSourcePaths(const char* anchorAnsi, const wchar_t* anchorWide); + public: CQuadWord TotalSize; // WARNING: not the byte size of the files (usable only for progress calculations) CQuadWord CompressedSize; // sum of file sizes after compression @@ -437,6 +455,8 @@ class COperations BOOL WorkPath1InclSubDirs; // TRUE/FALSE = with/without subdirectories (first path) CPathBuffer WorkPath2; // when non-empty string second path processed (used for change notifications) BOOL WorkPath2InclSubDirs; // TRUE/FALSE = with/without subdirectories (second path) + std::wstring WorkPath1W; + std::wstring WorkPath2W; std::string WaitInQueueSubject; // text for the "waiting in queue" state: dialog title std::string WaitInQueueFrom; // text for the "waiting in queue" state: top line (From) @@ -481,6 +501,18 @@ class COperations WorkPath2InclSubDirs = inclSubDirs; } + void SetWorkPath1W(const wchar_t* path, BOOL inclSubDirs) + { + WorkPath1W = path != NULL ? path : L""; + WorkPath1InclSubDirs = inclSubDirs; + } + + void SetWorkPath2W(const wchar_t* path, BOOL inclSubDirs) + { + WorkPath2W = path != NULL ? path : L""; + WorkPath2InclSubDirs = inclSubDirs; + } + void SetTFS(const CQuadWord& TFS); void SetTFSandProgressSize(const CQuadWord& TFS, const CQuadWord& pSize, int* limitBufferSize = NULL, int bufferSize = 0);