From 566484d67e4cbb3cbd729860f6419c6a4fc23d3b Mon Sep 17 00:00:00 2001 From: "METANEOCORTEX\\Kotti" Date: Sun, 25 Oct 2026 11:23:23 +0100 Subject: [PATCH] Fix Open dialog directory precedence and add recent-folder fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve explicit directory arguments, use the latest file’s folder when no default is set, and document behavior with regression coverage. --- docs/domain/file-io.md | 7 ++++++ docs/domain/startup-and-window-lifecycle.md | 4 ++++ readme/MenuEntriesAndCmds.md | 6 ++++-- readme/config/Configuration.md | 4 ++++ readme/faq/FAQ.md | 2 ++ readme/paths/FilePathHandling.md | 21 +++++++++++++++++- src/Dialogs.c | 24 +++++++++++++++++++-- 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/domain/file-io.md b/docs/domain/file-io.md index 1b8680276..64489952e 100644 --- a/docs/domain/file-io.md +++ b/docs/domain/file-io.md @@ -11,6 +11,13 @@ - If temp-file setup fails, saving falls back to writing the target directly. After a successful temp-file write, replacement tries `ReplaceFileW` for an existing target, then `MoveFileExW` with replace/write-through flags; a replacement failure makes the save fail. - `Globals.dwLastError` holds the Win32 error. In `FileSave()`, check `ERROR_ACCESS_DENIED` and `ERROR_PATH_NOT_FOUND` before the generic fallback. +## Open-dialog initial directory + +- `OpenFileDlg()` / `_ResolveOpenInitialDir()` in `src\Dialogs.c` prefer an explicit initial path, then the current file's directory, `Settings.DefaultDirectoryOverride`, and `Settings2.DefaultDirectory`. +- With none of those available and `Settings.SaveRecentFiles` enabled, Open uses the newest MRU entry's parent directory if it exists (the file itself need not exist). Otherwise it uses `Paths.WorkingDirectory`. It does not search older MRU entries. +- MRU paths must be sanitized, unquoted, and expanded with `Path_AbsoluteFromApp(..., true)` before taking the parent directory with the drive-root-safe `_GetParentDir()`. An unavailable parent clears the candidate so the shared resolver supplies the working directory. Save dialogs do not use history. +- An invalid final directory still falls back to Documents; Save also requires a writable directory. Focused coverage: `test\TestOpenDialogDirectory.ps1`. + ## File watching `InstallFileWatching()` uses `FindFirstChangeNotificationW` on the parent directory. Stop watching with `InstallFileWatching(false)` before saving, then reinstall it with `InstallFileWatching(true)` afterward. diff --git a/docs/domain/startup-and-window-lifecycle.md b/docs/domain/startup-and-window-lifecycle.md index fbbdd4bba..ef49a11e1 100644 --- a/docs/domain/startup-and-window-lifecycle.md +++ b/docs/domain/startup-and-window-lifecycle.md @@ -17,3 +17,7 @@ The runtime toggle is `IDM_EDIT_TOGGLE_PASTEBOARD`; `/B` enables Pasteboard Mode - If there is no acknowledging target, the launched process adopts the AppID and opens a new window. - The handoff to `DialogNewWindow` passes `-n` semantics, so a taskbar new-window request bypasses `ReuseWindow`. - Every AppUserModel property set through the window property store must be cleared in `ClearWindowAppUserModelID()` during `WM_DESTROY`. + +## Startup with a directory argument + +- A directory passed as the startup path opens the file dialog rooted in that directory (`Notepad3.c` startup path handling). `OpenFileDlg()` may receive the same `HPATHL` as both output and initial-directory input; it must copy the initial directory before clearing the output path (`Dialogs.c`). diff --git a/readme/MenuEntriesAndCmds.md b/readme/MenuEntriesAndCmds.md index 871c2e90b..32279f8df 100644 --- a/readme/MenuEntriesAndCmds.md +++ b/readme/MenuEntriesAndCmds.md @@ -563,8 +563,10 @@ Editor behaviour, persistence, and platform integration. Most items are toggles | Menu | Description | |---|---| -| **Set to Current File's Directory** | Bind the *Open* / *Save As* default to the current file's directory. | -| **Reset to Default** | Restore the standard "Documents" default. | +| **Set to Current File's Directory** | Store the current file's directory as the *Open* / *Save As* default when no file is open. This is a fixed directory, not a setting that tracks later files. | +| **Reset to Default** | Clear the menu-set directory override and return to the configured `DefaultDirectory` or the normal fallback. | + +An explicitly supplied directory (for example, a directory command-line argument) takes precedence over these defaults, as does the directory of an open file. With no current file or configured default, **Open** can use the most recent file's directory when **Remember Recent Files** is enabled. See [file-dialog starting directories](paths/FilePathHandling.md#file-dialog-starting-directory) for the full order and fallbacks. ### Remember diff --git a/readme/config/Configuration.md b/readme/config/Configuration.md index 706188bea..e4505aefb 100644 --- a/readme/config/Configuration.md +++ b/readme/config/Configuration.md @@ -120,6 +120,10 @@ This pattern is used by **Shift+F5** to update timestamps, e.g., `$Date: 2018/04 Default directory for open/save dialogs when no file is open. Paths can be relative to the Notepad3 program directory. +**Settings → Default Directory → Set to Current File's Directory** stores a separate override (`DefaultDirectoryOverride` under `[Settings]`); **Reset to Default** clears that override without changing this key. An explicit directory or the currently open file's directory takes precedence over both defaults. + +If neither default is defined, **Open** tries the most recent file's existing directory when **Remember Recent Files** is enabled, then the working directory. This history fallback does not apply to **Save As**. See [file-dialog starting directories](../paths/FilePathHandling.md#file-dialog-starting-directory). + #### `DefaultExtension=txt` Default extension for saved files (omit the leading dot). diff --git a/readme/faq/FAQ.md b/readme/faq/FAQ.md index 9068016bb..176abf171 100644 --- a/readme/faq/FAQ.md +++ b/readme/faq/FAQ.md @@ -190,6 +190,8 @@ Yes. Notepad3 always looks beside the exe first — `%APPDATA%` is only consulte `DefaultDirectory=` and `DefaultExtension=txt` under `[Settings2]`. +You can also choose **Settings → Default Directory → Set to Current File's Directory** to store a fixed default folder. With no current file and no default folder defined, **Open** tries the most recent file's directory if **Remember Recent Files** is enabled, then the working directory. See [file-dialog starting directories](../paths/FilePathHandling.md#file-dialog-starting-directory). + ### How do I customize the date/time format for "Update Timestamps" (Shift+F5)? You need to define **both** of the following in `[Settings2]`: diff --git a/readme/paths/FilePathHandling.md b/readme/paths/FilePathHandling.md index a8918bb68..3d61c2fbd 100644 --- a/readme/paths/FilePathHandling.md +++ b/readme/paths/FilePathHandling.md @@ -284,6 +284,25 @@ The Recent Files list in the File menu is persisted in `Notepad3.ini` under `[Recent Files]`. Disable persistence with the `NoSaveRecent` setting. Entries can be removed individually from the MRU dialog. +### File-dialog starting directory + +The **Open** dialog chooses its starting directory in this order: + +1. An explicitly supplied directory. Passing a directory rather than a file on the command line opens the dialog there. +2. The directory of the currently open file. +3. The fixed directory stored by **Settings → Default Directory → Set to Current File's Directory**. +4. `DefaultDirectory` under `[Settings2]`. +5. The most recent file's parent directory, if **Remember Recent Files** is enabled and that directory still exists. +6. The process working directory (set by the launcher, such as a shortcut's **Start in** folder). + +To start Notepad3 with the Open dialog in the current directory, run `Notepad3.exe .`. + +The recent file itself need not still exist. Only the newest history entry is considered; a missing directory falls back to the working directory rather than searching older entries. If the final directory is unavailable, the dialog falls back to **Documents**. + +**Save As** uses the same defaults but does not use recent-file history, and falls back to **Documents** if the selected directory is not writable. **Reset to Default** clears only the menu-set override; it does not clear `DefaultDirectory` or force **Documents**. + +Starting without a file or directory argument does not itself display the Open dialog. **Autoload Recent File** is a separate option that loads the newest history entry at startup; if loaded, that file's directory applies as the current-file directory above. + ### Portable & redirected INI By default `Notepad3.ini` lives next to `Notepad3.exe` (portable mode). The @@ -304,7 +323,7 @@ descriptions. |---|---| | `AtomicFileSave` | Two-phase Save (temp file + ReplaceFileW). | | `ResolveUNCPaths` | When opening a UNC path that's actually a mapped drive's share, prefer the drive-letter form for display. | -| `DefaultDirectory` | Initial directory of the **Open…** dialog. | +| `DefaultDirectory` | Default directory for **Open…** / **Save As** when no file is open; see [starting-directory precedence](#file-dialog-starting-directory). | | `OpenWithDir` | Directory used by **Open with…**. | | `FavoritesDir` | Where the Favorites list lives. | | `GrepWinPath` | Override the grepWin executable lookup. | diff --git a/src/Dialogs.c b/src/Dialogs.c index 045713acd..4e95c8824 100644 --- a/src/Dialogs.c +++ b/src/Dialogs.c @@ -7457,6 +7457,26 @@ static void _CanonicalizeInitialDir(HPATHL hpth_in_out, bool bMustBeWritable) } } +static void _ResolveOpenInitialDir(HPATHL hpth_in_out) +{ + if (Path_IsEmpty(hpth_in_out) && Path_IsEmpty(Paths.CurrentFile) && + Path_IsEmpty(Settings.DefaultDirectoryOverride) && Path_IsEmpty(Settings2.DefaultDirectory) && + Settings.SaveRecentFiles && MRU_Count(Globals.pFileMRU) > 0) { + LPWSTR const fileBuf = Path_WriteAccessBuf(hpth_in_out, PATHLONG_MAX_CCH); + int const fileLength = MRU_Enum(Globals.pFileMRU, 0, fileBuf, (int)Path_GetBufCount(hpth_in_out)); + Path_Sanitize(hpth_in_out); + if (fileLength > 0) { + Path_UnQuoteSpaces(hpth_in_out); + Path_AbsoluteFromApp(hpth_in_out, true); + _GetParentDir(hpth_in_out); + } + if (!Path_IsExistingDirectory(hpth_in_out)) { + Path_Empty(hpth_in_out, false); + } + } + _CanonicalizeInitialDir(hpth_in_out, false); +} + // ============================================================================ // @@ -7691,6 +7711,7 @@ bool OpenFileDlg(HWND hwnd, HPATHL hfile_pth_io, const HPATHL hinidir_pth) if (!hfile_pth_io) { return false; } + HPATHL hpth_dir = Path_Copy(hinidir_pth); if (!Path_IsEmpty(hinidir_pth)) { Path_Empty(hfile_pth_io, false); } @@ -7698,8 +7719,7 @@ bool OpenFileDlg(HWND hwnd, HPATHL hfile_pth_io, const HPATHL hinidir_pth) WCHAR szFilter[EXTENSIONS_FILTER_BUFFER]; Style_GetFileFilterStr(szFilter, COUNTOF(szFilter), szDefExt, COUNTOF(szDefExt), false); - HPATHL hpth_dir = Path_Copy(hinidir_pth); - _CanonicalizeInitialDir(hpth_dir, false); + _ResolveOpenInitialDir(hpth_dir); LPCWSTR defExt = StrIsNotEmpty(szDefExt) ? szDefExt : Settings2.DefaultExtension; LPCWSTR initDir = Path_IsNotEmpty(hpth_dir) ? Path_Get(hpth_dir) : NULL;