Merge pull request #5655 from RaiKoHoff/Dev_Master

fix: error prone manual numbering of menu child-items for checkboxes
This commit is contained in:
Rainer Kottenhoff 2026-04-14 17:01:55 +02:00 committed by GitHub
commit 4b074c85b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1056 additions and 619 deletions

View File

@ -52,7 +52,8 @@ Notepad3 is a Win32 desktop text editor built on the **Scintilla** editing compo
### Core modules (in `src\`)
- **Notepad3.c/h** — Application entry point (`wWinMain`), window procedure, global state structs (`GLOBALS_T`, `SETTINGS_T`, `FLAGS_T`, `PATHS_T`)
- **Notepad3.c/h** — Application entry point (`wWinMain`), window procedure, global state structs (`GLOBALS_T`, `SETTINGS_T`, `FLAGS_T`, `PATHS_T`), `MsgCommand()` dispatcher with sub-handlers
- **Notepad3Util.c/h** — Utility functions extracted from Notepad3.c: bitmap/toolbar image loading (`NP3Util_LoadBitmapFile`, `NP3Util_CreateScaledImageListFromBitmap`), word-wrap configuration (`NP3Util_SetWrapIndentMode`, `NP3Util_SetWrapVisualFlags`), auto-scroll (`NP3Util_AutoScrollStart/Stop`)
- **Edit.c/h** — Text manipulation: find/replace (PCRE2 regex), encoding conversion, clipboard, indentation, sorting, bookmarks, folding, auto-complete
- **Styles.c/h** — Scintilla styling, lexer selection, theme management
- **Dialogs.c/h** — All dialog boxes and UI interactions
@ -243,6 +244,7 @@ Notepad3 follows a **portable-app** design for its configuration file (`Notepad3
- **Admin redirect**: An administrator can place `Notepad3.ini=<path>` in `[Notepad3]` section of the app-directory INI to redirect to a per-user path. Up to 2 levels of redirect are supported. Redirect targets **are** auto-created (the admin intended them to exist).
- **Key paths**: `Paths.IniFile` = active writable INI (empty if none exists), `Paths.IniFileDefault` = fallback path for "Save Settings Now" recovery.
- **Configuration code**: All INI init logic lives in `src\Config\Config.cpp``FindIniFile()``TestIniFile()``CreateIniFile()``LoadSettings()`.
- **Empty INI = no INI**: An empty (0-byte) INI file must produce identical saved output to having no INI file at all. `SettingsVersion` defaults to `CFG_VER_CURRENT` when missing — a missing key does NOT imply a legacy config. Empty lexer sections are removed after style saving (`IniSectionGetKeyCount()`).
- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`.
- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini`

View File

@ -52,7 +52,8 @@ GitHub Actions (`.github/workflows/build.yml`) builds all four platforms (Win32,
| File | Purpose |
|------|---------|
| **Notepad3.c/h** | Entry point (`wWinMain`), window procedure (`MainWndProc`), global state structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`) |
| **Notepad3.c/h** | Entry point (`wWinMain`), window procedure (`MainWndProc`), global state structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`), `MsgCommand()` dispatcher |
| **Notepad3Util.c/h** | Utility functions extracted from Notepad3.c: bitmap/toolbar image loading, word-wrap configuration, auto-scroll (middle-click continuous scroll) |
| **Edit.c/h** | Text manipulation: find/replace (PCRE2 regex), encoding conversion, clipboard, indentation, sorting, bookmarks, folding, auto-complete |
| **Styles.c/h** | Scintilla styling, lexer selection, theme management, margin configuration |
| **Dialogs.c/h** | All dialog boxes, DPI-aware UI interactions, window placement |
@ -73,6 +74,28 @@ MainWndProc (Notepad3.c)
+-- Status Bar (16 configurable fields)
```
### Menu / Command Architecture
Menu items are defined in resource files (`language/np3_*/menu_*.rc`). Command handling in `Notepad3.c` is structured as:
- **`MsgInitMenu()`** — `WM_INITMENU` handler; enables/disables/checks menu items based on current state
- **`MsgCommand()`** — `WM_COMMAND` thin dispatcher; handles timer/notification cases inline, then delegates to static sub-handlers:
| Handler | Scope |
|---------|-------|
| `_HandleFileCommands` | File open/save/print/favorites (`IDM_FILE_*`) |
| `_HandleEncodingCommands` | Encoding & line endings (`IDM_ENCODING_*`, `IDM_LINEENDINGS_*`) |
| `_HandleEditBasicCommands` | Undo/redo/cut/copy/paste/indent (`IDM_EDIT_UNDO`..`CMD_VK_INSERT`) |
| `_HandleEditLineManipulation` | Line modify/sort/join/case (`IDM_EDIT_ENCLOSESELECTION`..`IDM_EDIT_INSERT_GUID`) |
| `_HandleEditTextTransform` | Comments/URL encode/escape/hex (`IDM_EDIT_LINECOMMENT`..`IDM_EDIT_HEX2CHAR`) |
| `_HandleEditFind` | Find/replace/bookmarks/goto (`IDM_EDIT_FINDMATCHINGBRACE`..`IDM_EDIT_GOTOLINE`) |
| `_HandleViewAndSettingsCommands` | View/settings/rendering (`IDM_VIEW_*`, `IDM_SET_*`) |
| `_HandleHelpCommands` | Help/about (`IDM_HELP_*`) |
| `_HandleCmdCommands` | Keyboard shortcuts/navigation/window positioning (`CMD_*`) |
| `_HandleToolbarCommands` | Toolbar button dispatch via `s_ToolbarDispatch[]` table (`IDT_*`) |
Each handler returns `true` if it handled the command, `false` to try the next. All handlers are `static` in `Notepad3.c`.
### Vendored Dependencies
| Directory | Library | Purpose |
@ -139,6 +162,9 @@ Resource-based MUI system with 27+ locales. Each locale has a `np3_LANG_COUNTRY\
- **Admin redirect**: `Notepad3.ini=<path>` in `[Notepad3]` section redirects to per-user path (up to 2 levels)
- Key paths: `Paths.IniFile` (active writable INI), `Paths.IniFileDefault` (fallback for recovery)
- INI init flow: `FindIniFile()` -> `TestIniFile()` -> `CreateIniFile()` -> `LoadSettings()`
- **SettingsVersion default**: `SettingsVersion` defaults to `CFG_VER_CURRENT` when missing from INI — an INI file without `SettingsVersion` is NOT treated as a legacy (pre-versioning) file. This ensures empty INI files and newly created INI files both get current defaults.
- **`bIniFileFromScratch`**: Set when INI file size is 0 bytes. Cleared after `SaveAllSettings()` completes. While true, `MuiLanguage.c` suppresses writing `PreferredLanguageLocaleName` to avoid polluting fresh INI files.
- **Style section saving**: `Style_ToIniSection()` removes empty lexer sections after processing via `IniSectionGetKeyCount()`. `Style_CanonicalSectionToIniCache()` establishes canonical section order but empty sections are cleaned up — only sections with non-default style values appear in the saved INI.
- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`.
- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini`

View File

@ -0,0 +1,21 @@
please make a plan for implementing a kind of "disaster file recovery":
- on application crash/sigkill/sigint/sys-shutdown there should be a directory (user's %APPDATA%/Notepad3/recovery/) where recovery bundle files are kept
- one file to keep the original document (respect encryption)
- an accompaning ini file which keeps the original full filepath, original encoding, and other imortant attributes
- this recovery file should be updated (if save needed / dirty mode) in background every 2000ms (time should be configurable in settings)
- the recovery file bundle should be rmoved, if the application exits normally
- there should be a recovery bundle also for dirty non empty drafts (Untitled / No file path given)
- the mechanism should be multi instances aware (maybe process number in bundle name ?)
- on every application start check the "%APPDATA%/Notepad3/recovery/" if there are unhandled disaster recovery files and
start a new app instance to load the recovery, unhandled means, there is no instance taking care of this recovery file, must be tracked somehow:
+ instance has file loaded with correct path - normal recovery mode (process-id of recovery file matches)
+ file has no instance which is taking care of, new instance has to be started for this recovery file
+ recovery file's process id does not fit to a "care of instance", but new instance has already been startet to take care of
+ if a new instance takes care of the recovery file, the process-id has to be changed accordingly
+ maybe, the commandline interface has to be extended to give an file path for handling and saving in original place
+ maybe the modified date of both files must be checked to do not override newer files
-
- if you have suggestions derived from best practices for disaster recovery, please suggest
- if there are questions/uncertainty please ask

View File

@ -0,0 +1,230 @@
# Notepad3.c Refactoring — Extract Utilities & Split MsgCommand()
## Context
`src/Notepad3.c` was the largest module in the project at **12,985 lines**. It contained ~55 static functions and ~60 static variables spanning unrelated concerns (auto-scroll, file observation, bitmap loading, text input helpers, TinyExpr evaluation, etc.) plus a monolithic `MsgCommand()` dispatcher (2,994 lines, 360+ case statements). This refactoring improves navigability and separation of concerns without changing any behavior.
### Motivation
- **Navigability**: Finding code in a 13K-line file is slow; logically grouped helpers belong in their own modules
- **Maintainability**: The `MsgCommand()` monolith made it hard to reason about individual command groups
- **Encapsulation**: Static variables for unrelated subsystems (auto-scroll, file observation, TinyExpr) were all in global file scope, obscuring their true scope
- **Consistency**: Other modules (`Edit.c`, `Styles.c`, `Dialogs.c`) already follow clean `.c/.h` pair boundaries
### What NOT to extract
These items are intentionally kept in `Notepad3.c`:
- **`_InitGlobals()` / `_CleanUpResources()`** — core app lifecycle, touches everything
- **Message queue helpers** (`_MQ_AppendCmd`, `MQ_ExecuteNext`) — tightly integrated with timer system and UI updates
- **UI update helpers** (`_UpdateStatusbarDelayed`, `_UpdateToolbarDelayed`, `_UpdateTitlebarDelayed`) — depend on message queue + complex global state
- **`_EditSubclassProc()`** — Scintilla subclass glue, belongs near `MainWndProc`
- **`ParseCmdLnOption()`** — command-line parsing, belongs with app startup
- **`MsgInitMenu()`** — reads 20+ state variables to enable/disable menu items; inherently whole-application state
---
## Completed Work
### Part 2: MsgCommand() Split (DONE)
`MsgCommand()` refactored from a **2,994-line** switch into a **73-line thin dispatcher** that delegates to **10 static handler functions**, all remaining in `Notepad3.c`:
| Handler | Cases | Purpose |
|---------|-------|---------|
| `_HandleFileCommands` | ~27 | `IDM_FILE_*` — open/save/print/favorites/grepWin |
| `_HandleEncodingCommands` | ~10 | `IDM_ENCODING_*`, `IDM_LINEENDINGS_*` |
| `_HandleEditBasicCommands` | ~30 | `IDM_EDIT_UNDO`..`CMD_VK_INSERT` — undo/redo/cut/copy/paste/indent |
| `_HandleEditLineManipulation` | ~42 | `IDM_EDIT_ENCLOSESELECTION`..`IDM_EDIT_INSERT_GUID` — line modify/sort/join/case |
| `_HandleEditTextTransform` | ~45 | `IDM_EDIT_LINECOMMENT`..`IDM_EDIT_HEX2CHAR` — comments/encode/escape/hex |
| `_HandleEditFind` | ~21 | `IDM_EDIT_FINDMATCHINGBRACE`..`IDM_EDIT_GOTOLINE` — find/replace/bookmarks |
| `_HandleViewAndSettingsCommands` | ~99 | `IDM_VIEW_*`, `IDM_SET_*` — view/settings/rendering |
| `_HandleHelpCommands` | ~5 | `IDM_HELP_*`, `IDM_SETPASS` |
| `_HandleCmdCommands` | ~90 | `CMD_*` — keyboard shortcuts/navigation/window positioning |
| `_HandleToolbarCommands` | ~30 | `IDT_*` — toolbar dispatch via `s_ToolbarDispatch[]` lookup table |
**Dispatcher pattern:**
```c
LRESULT MsgCommand(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam)
{
// Language/theme menu range checks (inline)
// Timer/notification cases (inline, return immediately)
switch(iLoWParam) { case SCEN_CHANGE: ... return FALSE; ... }
// Handler dispatch chain
if (_HandleFileCommands(hwnd, umsg, wParam, lParam)) { return FALSE; }
if (_HandleEncodingCommands(...)) { return FALSE; }
...
return DefWindowProc(hwnd, umsg, wParam, lParam);
}
```
Each handler returns `true` if handled, `false` to try the next. The 40 repetitive IDT_* toolbar cases were replaced with a `s_ToolbarDispatch[]` lookup table (29 standard entries + 2 special cases: `IDT_EDIT_COPY` falls back to COPYALL, `IDT_EDIT_CLEAR` falls back to `SciCall_ClearAll`).
### Part 1, Phases 1-3: Notepad3Util.c/.h (DONE)
New files created: `src/Notepad3Util.c` (349 lines), `src/Notepad3Util.h` (50 lines).
**Phase 1 — Bitmap/Image Loading** (LOW risk, ~100 LOC):
- `NP3Util_LoadBitmapFile()` — loads toolbar bitmap, validates dimensions
- `NP3Util_CreateScaledImageListFromBitmap()` — creates DPI-scaled image list
- `NP3Util_XXX_CreateScaledImageListFromBitmap()` — legacy variant using fixed `NUMTOOLBITMAPS`
- `NUMTOOLBITMAPS` macro moved from Notepad3.c to Notepad3Util.h
**Phase 2 — Word-Wrap Configuration** (LOW risk, ~100 LOC):
- `NP3Util_SetWrapStartIndent()` — sets wrap start indent based on `Settings.WordWrapIndent`
- `NP3Util_SetWrapIndentMode()` — sets wrap indent mode (same/indent/deep/fixed)
- `NP3Util_SetWrapVisualFlags(HWND)` — sets wrap visual flag symbols
**Phase 3 — Auto-Scroll** (LOW-MED risk, ~200 LOC):
- 6 static variables moved: `s_bAutoScrollMode`, `s_bAutoScrollHeld`, `s_dwAutoScrollStartTick`, `s_ptAutoScrollOrigin`, `s_ptAutoScrollMouse`, `s_dAutoScrollAccumY`
- 4 `AUTOSCROLL_*` constants moved to header
- `NP3Util_AutoScrollStart/Stop()`, `NP3Util_AutoScrollTimerProc()` — core scroll logic
- `NP3Util_IsAutoScrollMode()`, `NP3Util_IsAutoScrollHeld()`, `NP3Util_GetAutoScrollStartTick()`, `NP3Util_SetAutoScrollHeld()`, `NP3Util_AutoScrollUpdateMouse()` — state accessors for `_EditSubclassProc`
**Net result:** `Notepad3.c` reduced from 12,985 to 12,713 lines.
### Files modified
- `src/Notepad3.c` — extracted code removed, call sites updated, `#include "Notepad3Util.h"` added
- `src/Notepad3Util.c` — new implementation file
- `src/Notepad3Util.h` — new header file
- `src/Notepad3.vcxproj``<ClCompile>` and `<ClInclude>` entries added
- `src/Notepad3.vcxproj.filters` — filter entries added (Source Files / Header Files)
- `CLAUDE.md` — Core Modules table updated, Menu/Command Architecture section added
- `.github/copilot-instructions.md` — Core modules list updated
---
## Remaining Work — Phases 4-6
### Phase 4 — TinyExpr Evaluation (~130 LOC, MEDIUM risk)
**Static variables to move** (Notepad3.c line 186-187):
- `s_dExpression` (double) — last evaluated expression result
- `s_iExprError` (te_int_t) — last expression error code
**Functions to move:**
| Current | New | Line | LOC |
|---------|-----|------|-----|
| `_EvalTinyExpr(bool qmark)` | `NP3Util_EvalTinyExpr(bool)` | 2655 | ~150 |
| `_InterpMultiSelectionTinyExpr(te_int_t*)` | `NP3Util_InterpMultiSelectionTinyExpr(te_int_t*)` | 10086 | ~50 |
**New accessor functions needed:**
- `NP3Util_GetLastExpression()` — returns `s_dExpression` (read by `_UpdateStatusbarDelayed`)
- `NP3Util_GetLastExprError()` — returns `s_iExprError` (read by `_UpdateStatusbarDelayed`)
**Call sites to update (3):**
- Line 6839: `_EvalTinyExpr(false)` — in `_HandleCmdCommands`, case `CMD_ENTER_RETURN`
- Line 8921: `_EvalTinyExpr(true)` — in `_MsgNotifyFromEdit`, on `?` char insert
- Line 10399: `s_dExpression = _InterpMultiSelectionTinyExpr(&s_iExprError)` — in `_UpdateStatusbarDelayed`
**Dependencies:** `Settings.EvalTinyExprOnSelection`, `Encoding_SciCP`, `SciCall_*` (all via headers), `te_interp()` (needs `#include "tinyexpr/tinyexpr.h"` in Notepad3Util.c), `AllocMem`/`FreeMem` (via `Helpers.h`).
**Risk:** The statusbar code currently reads `s_dExpression`/`s_iExprError` directly — must switch to getters. The `te_interp()` call works on raw char buffers from Scintilla — encoding-sensitive but mechanically straightforward.
---
### Phase 5 — Text Input Helpers (~300 LOC, MEDIUM risk)
**Static variable to move** (line 189):
- `s_SelectionBuffer` (char*) — dynamically allocated buffer for auto-close bracket/quote tracking
**Functions to move:**
| Current | New | Line | LOC |
|---------|-----|------|-----|
| `_HandleAutoIndent(int)` | `NP3Util_HandleAutoIndent(int)` | 8291 | ~45 |
| `_HandleAutoCloseTags()` | `NP3Util_HandleAutoCloseTags()` | 8338 | ~58 |
| `_SaveSelectionToBuffer()` | `NP3Util_SaveSelectionToBuffer()` | 8398 | ~16 |
| `_EncloseSelectionBuffer(char,char)` | `NP3Util_EncloseSelectionBuffer(char,char)` | 8416 | ~17 |
| `_HandleInsertCheck(SCNotification*)` | `NP3Util_HandleInsertCheck(...)` | 8435 | ~89 |
| `_HandleDeleteCheck(SCNotification*)` | `NP3Util_HandleDeleteCheck(...)` | 8526 | ~60 |
Skip `_IsIMEOpenInNoNativeMode()` (line 8588) — dead code (`#if 0`).
**Lifecycle functions needed:**
- `NP3Util_TextInputInit()` — allocates `s_SelectionBuffer`; called from `MsgCreate`
- `NP3Util_TextInputCleanup()` — frees `s_SelectionBuffer`; called from `_CleanUpResources`
**Call sites to update (5, all in `_MsgNotifyFromEdit`):**
- Line 8635: `_HandleInsertCheck(scn)`
- Line 8642: `_SaveSelectionToBuffer()`
- Line 8658: `_HandleDeleteCheck(scn)`
- Line 8912: `_HandleAutoIndent(ich)`
- Line 8917: `_HandleAutoCloseTags()`
**Dependencies:** `Settings.AutoIndent`, `Settings.AutoCloseQuotes`, `Settings.AutoCloseBrackets`, `Settings.AutoCloseTags` (globals), `SciCall_*`/`Sci_*` (headers), `EditReplaceSelection()` (via `Edit.h`), `AllocMem`/`FreeMem`/`ReAllocMem`/`SizeOfMem` (via `Helpers.h`).
**Risk:** These functions run on the Scintilla notification hot path (`SCN_MODIFIED`, `SCN_CHARADDED`), but are called once per keystroke (not per character in bulk), so function-call overhead is negligible. Main risk is ensuring `s_SelectionBuffer` lifecycle remains correct.
---
### Phase 6 — File Observation (~450 LOC, HIGH risk — do last)
**Static variable to move** (line 483):
- `s_FileChgObsvrData` (`FCOBSRVDATA_T`) — contains event handles (`hEventFileChanged`, `hEventFileDeleted`), file metadata (`fdCurFile`), generation counter (`iObservationGeneration`, uses `InterlockedCompareExchange`/`InterlockedIncrement` seqlock pattern), background worker handle. **48 non-comment references** across Notepad3.c.
**Functions to move:**
| Current | New | Line | LOC |
|---------|-----|------|-----|
| `IsFileReadOnly()` | `NP3Util_IsFileReadOnly()` | 471 | ~15 |
| `IsFileChangedFlagSet()` | `NP3Util_IsFileChangedFlagSet()` | 487 | ~4 |
| `IsFileDeletedFlagSet()` | `NP3Util_IsFileDeletedFlagSet()` | 492 | ~4 |
| `RaiseFlagIfCurrentFileChanged()` | `NP3Util_RaiseFlagIfCurrentFileChanged()` | 497 | ~50 |
| `ResetFileObservationData(bool)` | `NP3Util_ResetFileObservationData(bool)` | 548 | ~20 |
| `IsFileVarLogFile()` | `NP3Util_IsFileVarLogFile()` | 10861 | ~10 |
| `_ResetFileWatchingMode()` | `NP3Util_ResetFileWatchingMode()` | 10871 | ~10 |
| `NotifyIfFileHasChanged()` | `NP3Util_NotifyIfFileHasChanged()` | 12364 | ~20 |
| `WatchTimerProc(...)` | `NP3Util_WatchTimerProc(...)` | 12385 | ~15 |
| `LogRotateTimerProc(...)` | `NP3Util_LogRotateTimerProc(...)` | 12402 | ~25 |
| `AtomicSaveTimerProc(...)` | `NP3Util_AtomicSaveTimerProc(...)` | 12426 | ~50 |
**Lifecycle functions needed:**
- `NP3Util_FileObservationInit()` — creates event handles; replaces code in `InitInstance()` (~lines 1843-1852)
- `NP3Util_FileObservationCleanup()` — destroys worker + event handles; replaces code in `_CleanUpResources()` (~lines 824-833)
- `NP3Util_GetFileObservationData()` — returns `PFCOBSRVDATA_T` pointer for `InstallFileWatching()` to access the struct
**Circular dependency:**
Timer callbacks call back into Notepad3.c:
- `AtomicSaveTimerProc``InstallFileWatching(false)`, `FileSave(FSF_SaveAlways)`
- `LogRotateTimerProc``PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL)`, `InstallFileWatching(true)`
- `_ResetFileWatchingMode``CheckCmd(GetMenu(...))`
**Resolution:** `Notepad3Util.c` already `#include`s `Notepad3.h` which declares these functions. The linker resolves cross-module calls — same pattern as `Edit.c` calling `FileLoad()`.
**Major call sites to update (~48 references):**
- `InitInstance()` — event creation → `NP3Util_FileObservationInit()`
- `_CleanUpResources()` — cleanup → `NP3Util_FileObservationCleanup()`
- `InstallFileWatching()` — direct struct field access → `NP3Util_GetFileObservationData()->`
- `MsgFileChangeNotify()` — reads flags, resets observation data
- `_UpdateTitlebarDelayed()` — calls `IsFileChangedFlagSet()`/`IsFileDeletedFlagSet()`
- `MsgInitMenu()` — calls `IsFileReadOnly()`
- `_HandleViewAndSettingsCommands`, `_HandleCmdCommands` — various flag checks
**Threading concern:** The generation counter uses `InterlockedCompareExchange`/`InterlockedIncrement` for a seqlock pattern (background worker vs. UI thread). Moving the struct doesn't change thread safety, but `NP3Util_GetFileObservationData()` returns a raw pointer — callers must not cache it across calls that could reallocate.
**Risk: HIGH** — 48 reference sites (most mechanical renames), but `InstallFileWatching()` directly manipulates struct fields (worker start/cancel, event wait). Timer proc function pointers in `SetTimer()` calls must be updated. Threading correctness is critical.
---
## Verification Strategy
After each phase:
1. **Build:** `Build\Build_x64.cmd Debug` (minimum) — no compile or link errors
2. **Diff audit:** `git diff` — confirm purely mechanical moves, no logic changes
3. **Smoke test per group:**
- Phase 4 (TinyExpr): Select `1+2` → press `?` → verify result inserted; check statusbar expression display with column selection
- Phase 5 (Text Input): Type `"` → verify auto-close quote; type `{` → verify auto-close bracket; press Enter after `if (...) {` → verify auto-indent; type `<div>` → verify auto-close `</div>`; press Backspace on `""` → verify pair deletion
- Phase 6 (File Observation): Edit file in another editor → Notepad3 must prompt for reload; enable log tail mode (Ctrl+Shift+L) → verify auto-refresh; test atomic save (Settings2.AtomicFileSave=1); test file deletion detection; open/close files rapidly → verify no timer leaks
4. **Full build** after all phases: `Build\BuildAll.cmd Release` (all 4 platforms)
---
## Estimated Final Result
| Metric | Before | After (all phases) |
|--------|--------|---------------------|
| `Notepad3.c` lines | 12,985 | ~11,700 |
| `Notepad3Util.c` lines | 0 | ~1,250 |
| `MsgCommand()` lines | 2,994 | ~73 (dispatcher) |
| Toolbar switch cases | 40 repetitive | dispatch table |
| Static helpers in Notepad3.c | ~55 | ~35 |

File diff suppressed because it is too large Load Diff

View File

@ -1077,6 +1077,7 @@
<ClCompile Include="Helpers.c" />
<ClCompile Include="MuiLanguage.c" />
<ClCompile Include="Notepad3.c" />
<ClCompile Include="Notepad3Util.c" />
<ClCompile Include="Print.cpp" />
<ClCompile Include="Resample.c" />
<ClCompile Include="StyleLexers\styleLexASM.c" />
@ -1180,6 +1181,7 @@
<ClInclude Include="Helpers.h" />
<ClInclude Include="MuiLanguage.h" />
<ClInclude Include="Notepad3.h" />
<ClInclude Include="Notepad3Util.h" />
<ClInclude Include="Resample.h" />
<ClInclude Include="SciCall.h" />
<ClInclude Include="StyleLexers\EditLexer.h" />

View File

@ -72,6 +72,9 @@
<ClCompile Include="Notepad3.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Notepad3Util.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Encoding.c">
<Filter>Source Files</Filter>
</ClCompile>
@ -470,6 +473,9 @@
<ClInclude Include="Notepad3.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Notepad3Util.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VersionEx.h">
<Filter>Resource Files</Filter>
</ClInclude>

349
src/Notepad3Util.c Normal file
View File

@ -0,0 +1,349 @@
// encoding: UTF-8
/******************************************************************************
* *
* *
* Notepad3 *
* *
* Notepad3Util.c *
* Utility functions extracted from Notepad3.c *
* Based on code from Notepad2, (c) Florian Balmer 1996-2011 *
* *
* (c) Rizonesoft 2008-2026 *
* https://rizonesoft.com *
* *
* *
*******************************************************************************/
#include "Helpers.h"
#include <commctrl.h>
#include <shlobj.h>
#include <shlwapi.h>
#include "PathLib.h"
#include "Dialogs.h"
#include "Encoding.h"
#include "MuiLanguage.h"
#include "Notepad3.h"
#include "Notepad3Util.h"
// ============================================================================
// --- Bitmap / Image Loading ---
// ============================================================================
//=============================================================================
//
// NP3Util_LoadBitmapFile()
//
HBITMAP NP3Util_LoadBitmapFile(const HPATHL hpath)
{
HBITMAP hbmp = NULL;
if (Path_IsExistingFile(hpath)) {
hbmp = (HBITMAP)LoadImage(NULL, Path_Get(hpath), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE);
bool bDimOK = false;
int height = 16;
if (hbmp) {
BITMAP bmp = { 0 };
GetObject(hbmp, sizeof(BITMAP), &bmp);
height = bmp.bmHeight;
bDimOK = (bmp.bmWidth >= (height * NUMTOOLBITMAPS));
}
if (!bDimOK) {
InfoBoxLng(MB_ICONWARNING, L"NotSuitableToolbarDim", IDS_MUI_ERR_BITMAP, Path_Get(hpath),
(height * NUMTOOLBITMAPS), height, NUMTOOLBITMAPS);
}
}
else {
WCHAR displayName[80];
Path_GetDisplayName(displayName, 80, hpath, L"<unknown>", false);
InfoBoxLng(MB_ICONWARNING, NULL, IDS_MUI_ERR_LOADFILE, displayName);
}
return hbmp;
}
//=============================================================================
//
// NP3Util_XXX_CreateScaledImageListFromBitmap()
//
HIMAGELIST NP3Util_XXX_CreateScaledImageListFromBitmap(HWND hWnd, HBITMAP hBmp)
{
BITMAP bmp = { 0 };
GetObject(hBmp, sizeof(BITMAP), &bmp);
int const mod = bmp.bmWidth % NUMTOOLBITMAPS;
int const cx = (bmp.bmWidth - mod) / NUMTOOLBITMAPS;
int const cy = bmp.bmHeight;
HIMAGELIST himl = ImageList_Create(cx, cy, ILC_COLOR32 | ILC_MASK, NUMTOOLBITMAPS, NUMTOOLBITMAPS);
ImageList_AddMasked(himl, hBmp, CLR_DEFAULT);
UINT const dpi = Scintilla_GetWindowDPI(hWnd);
if (!Settings.DpiScaleToolBar || (dpi == USER_DEFAULT_SCREEN_DPI)) {
return himl; // default DPI, we are done
}
// Scale button icons/images
int const scx = ScaleIntToDPI(hWnd, cx);
int const scy = ScaleIntToDPI(hWnd, cy);
HIMAGELIST hsciml = ImageList_Create(scx, scy, ILC_COLOR32 | ILC_MASK | ILC_HIGHQUALITYSCALE, NUMTOOLBITMAPS, NUMTOOLBITMAPS);
for (int i = 0; i < NUMTOOLBITMAPS; ++i) {
HICON const hicon = ImageList_GetIcon(himl, i, ILD_TRANSPARENT | ILD_PRESERVEALPHA | ILD_SCALE);
ImageList_AddIcon(hsciml, hicon);
DestroyIcon(hicon);
}
ImageList_Destroy(himl);
return hsciml;
}
//=============================================================================
//
// NP3Util_CreateScaledImageListFromBitmap()
//
HIMAGELIST NP3Util_CreateScaledImageListFromBitmap(HWND hWnd, HBITMAP hBmp)
{
BITMAP bmp = { 0 };
GetObject(hBmp, sizeof(BITMAP), &bmp);
int const numOfToolBitmaps = (int)(bmp.bmWidth / bmp.bmHeight);
int const mod = bmp.bmWidth % numOfToolBitmaps;
int const cx = (bmp.bmWidth - mod) / numOfToolBitmaps;
int const cy = bmp.bmHeight;
HIMAGELIST himl = ImageList_Create(cx, cy, ILC_COLOR32 | ILC_MASK, numOfToolBitmaps, numOfToolBitmaps);
ImageList_AddMasked(himl, hBmp, CLR_DEFAULT);
UINT const dpi = Scintilla_GetWindowDPI(hWnd);
if (!Settings.DpiScaleToolBar || (dpi == USER_DEFAULT_SCREEN_DPI)) {
return himl; // default DPI, we are done
}
// Scale button icons/images
int const scx = ScaleIntToDPI(hWnd, cx);
int const scy = ScaleIntToDPI(hWnd, cy);
HIMAGELIST hsciml = ImageList_Create(scx, scy, ILC_COLOR32 | ILC_MASK | ILC_HIGHQUALITYSCALE, numOfToolBitmaps, numOfToolBitmaps);
for (int i = 0; i < numOfToolBitmaps; ++i) {
HICON const hicon = ImageList_GetIcon(himl, i, ILD_TRANSPARENT | ILD_PRESERVEALPHA | ILD_SCALE);
ImageList_AddIcon(hsciml, hicon);
DestroyIcon(hicon);
}
ImageList_Destroy(himl);
return hsciml;
}
// ============================================================================
// --- Word-Wrap Configuration ---
// ============================================================================
//=============================================================================
//
// NP3Util_SetWrapStartIndent()
//
void NP3Util_SetWrapStartIndent(void)
{
int i = 0;
switch (Settings.WordWrapIndent) {
case 1:
i = 1;
break;
case 2:
i = 2;
break;
case 3:
i = (Globals.fvCurFile.iIndentWidth) ? 1 * Globals.fvCurFile.iIndentWidth : 1 * Globals.fvCurFile.iTabWidth;
break;
case 4:
i = (Globals.fvCurFile.iIndentWidth) ? 2 * Globals.fvCurFile.iIndentWidth : 2 * Globals.fvCurFile.iTabWidth;
break;
default:
break;
}
SciCall_SetWrapStartIndent(i);
}
//=============================================================================
//
// NP3Util_SetWrapIndentMode()
//
void NP3Util_SetWrapIndentMode(void)
{
BeginWaitCursorUID(Flags.bHugeFileLoadState, IDS_MUI_SB_WRAP_LINES);
Sci_SetWrapModeEx(GET_WRAP_MODE());
if (Settings.WordWrapIndent == 5) {
SciCall_SetWrapIndentMode(SC_WRAPINDENT_SAME);
} else if (Settings.WordWrapIndent == 6) {
SciCall_SetWrapIndentMode(SC_WRAPINDENT_INDENT);
} else if (Settings.WordWrapIndent == 7) {
SciCall_SetWrapIndentMode(SC_WRAPINDENT_DEEPINDENT);
} else {
NP3Util_SetWrapStartIndent();
SciCall_SetWrapIndentMode(SC_WRAPINDENT_FIXED);
}
EndWaitCursor();
}
//=============================================================================
//
// NP3Util_SetWrapVisualFlags()
//
void NP3Util_SetWrapVisualFlags(HWND hwndEditCtrl)
{
UNREFERENCED_PARAMETER(hwndEditCtrl);
if (Settings.ShowWordWrapSymbols) {
int wrapVisualFlags = 0;
int wrapVisualFlagsLocation = 0;
if (Settings.WordWrapSymbols == 0) {
Settings.WordWrapSymbols = 22;
}
switch (Settings.WordWrapSymbols % 10) {
case 1:
wrapVisualFlags |= SC_WRAPVISUALFLAG_END;
wrapVisualFlagsLocation |= SC_WRAPVISUALFLAGLOC_END_BY_TEXT;
break;
case 2:
wrapVisualFlags |= SC_WRAPVISUALFLAG_END;
break;
}
switch (((Settings.WordWrapSymbols % 100) - (Settings.WordWrapSymbols % 10)) / 10) {
case 1:
wrapVisualFlags |= SC_WRAPVISUALFLAG_START;
wrapVisualFlagsLocation |= SC_WRAPVISUALFLAGLOC_START_BY_TEXT;
break;
case 2:
wrapVisualFlags |= SC_WRAPVISUALFLAG_START;
break;
}
SciCall_SetWrapVisualFlags(wrapVisualFlags);
SciCall_SetWrapVisualFlagsLocation(wrapVisualFlagsLocation);
} else {
SciCall_SetWrapVisualFlags(0);
}
}
// ============================================================================
// --- Auto-Scroll (middle-click continuous scroll, Firefox-style) ---
// ============================================================================
static bool s_bAutoScrollMode = false;
static bool s_bAutoScrollHeld = false;
static ULONGLONG s_dwAutoScrollStartTick = 0;
static POINT s_ptAutoScrollOrigin = { 0, 0 };
static POINT s_ptAutoScrollMouse = { 0, 0 };
static double s_dAutoScrollAccumY = 0.0;
bool NP3Util_IsAutoScrollMode(void)
{
return s_bAutoScrollMode;
}
bool NP3Util_IsAutoScrollHeld(void)
{
return s_bAutoScrollHeld;
}
ULONGLONG NP3Util_GetAutoScrollStartTick(void)
{
return s_dwAutoScrollStartTick;
}
void NP3Util_SetAutoScrollHeld(bool held)
{
s_bAutoScrollHeld = held;
}
void NP3Util_AutoScrollUpdateMouse(POINT pt)
{
s_ptAutoScrollMouse = pt;
}
//=============================================================================
//
// NP3Util_AutoScrollStop()
//
void NP3Util_AutoScrollStop(HWND hwndEdit)
{
if (s_bAutoScrollMode) {
KillTimer(hwndEdit, ID_AUTOSCROLLTIMER);
ReleaseCapture();
SciCall_SetCursor(SC_CURSORNORMAL);
s_bAutoScrollMode = false;
s_bAutoScrollHeld = false;
s_dAutoScrollAccumY = 0.0;
}
}
//=============================================================================
//
// NP3Util_AutoScrollTimerProc()
//
void CALLBACK NP3Util_AutoScrollTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
UNREFERENCED_PARAMETER(uMsg);
UNREFERENCED_PARAMETER(idEvent);
UNREFERENCED_PARAMETER(dwTime);
if (!s_bAutoScrollMode) {
KillTimer(hwnd, ID_AUTOSCROLLTIMER);
return;
}
int const deltaY = s_ptAutoScrollMouse.y - s_ptAutoScrollOrigin.y;
if (abs(deltaY) <= AUTOSCROLL_DEADZONE) {
s_dAutoScrollAccumY = 0.0;
return;
}
// Speed: proportional to distance beyond dead zone
double const speed = (double)(deltaY - (deltaY > 0 ? AUTOSCROLL_DEADZONE : -AUTOSCROLL_DEADZONE)) / AUTOSCROLL_DIVISOR;
s_dAutoScrollAccumY += speed;
DocLn const linesToScroll = (DocLn)s_dAutoScrollAccumY;
if (linesToScroll != 0) {
SciCall_LineScroll(0, linesToScroll);
s_dAutoScrollAccumY -= (double)linesToScroll;
}
}
//=============================================================================
//
// NP3Util_AutoScrollStart()
//
void NP3Util_AutoScrollStart(HWND hwndEdit, POINT pt)
{
s_bAutoScrollMode = true;
s_bAutoScrollHeld = false;
s_dwAutoScrollStartTick = GetTickCount64();
s_ptAutoScrollOrigin = pt;
s_ptAutoScrollMouse = pt;
s_dAutoScrollAccumY = 0.0;
SetCapture(hwndEdit);
SetCursor(LoadCursor(NULL, IDC_SIZEALL));
SetTimer(hwndEdit, ID_AUTOSCROLLTIMER, AUTOSCROLL_TIMER_MS, NP3Util_AutoScrollTimerProc);
}

50
src/Notepad3Util.h Normal file
View File

@ -0,0 +1,50 @@
// encoding: UTF-8
/******************************************************************************
* *
* *
* Notepad3 *
* *
* Notepad3Util.h *
* Utility functions extracted from Notepad3.c *
* Based on code from Notepad2, (c) Florian Balmer 1996-2011 *
* *
* (c) Rizonesoft 2008-2026 *
* https://rizonesoft.com *
* *
* *
*******************************************************************************/
#pragma once
#ifndef _NP3_NOTEPAD3UTIL_H_
#define _NP3_NOTEPAD3UTIL_H_
#include "TypeDefs.h"
#include "SciCall.h"
// --- Bitmap / Image Loading ---
#define NUMTOOLBITMAPS (31)
HBITMAP NP3Util_LoadBitmapFile(const HPATHL hpath);
HIMAGELIST NP3Util_CreateScaledImageListFromBitmap(HWND hWnd, HBITMAP hBmp);
HIMAGELIST NP3Util_XXX_CreateScaledImageListFromBitmap(HWND hWnd, HBITMAP hBmp);
// --- Word-Wrap Configuration ---
void NP3Util_SetWrapStartIndent(void);
void NP3Util_SetWrapIndentMode(void);
void NP3Util_SetWrapVisualFlags(HWND hwndEditCtrl);
// --- Auto-Scroll (middle-click continuous scroll) ---
#define AUTOSCROLL_TIMER_MS 30
#define AUTOSCROLL_DEADZONE 15
#define AUTOSCROLL_DIVISOR 60.0
#define AUTOSCROLL_CLICK_THRESHOLD_MS 200
bool NP3Util_IsAutoScrollMode(void);
void NP3Util_AutoScrollStop(HWND hwndEdit);
void NP3Util_AutoScrollStart(HWND hwndEdit, POINT pt);
void NP3Util_AutoScrollUpdateMouse(POINT pt);
bool NP3Util_IsAutoScrollHeld(void);
ULONGLONG NP3Util_GetAutoScrollStartTick(void);
void NP3Util_SetAutoScrollHeld(bool held);
void CALLBACK NP3Util_AutoScrollTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
#endif // _NP3_NOTEPAD3UTIL_H_

View File

@ -555,7 +555,6 @@ bool Style_InsertThemesMenu(HMENU hMenuBar)
WCHAR wchMenuItemStrg[128] = { L'\0' };
GetLngString(IDS_MUI_MENU_THEMES, wchMenuItemStrg, COUNTOF(wchMenuItemStrg));
//bool const res = InsertMenu(hMenuBar, pos, MF_BYPOSITION | MF_POPUP | MF_STRING, (UINT_PTR)s_hmenuThemes, wchMenuItemStrg);
bool const res = InsertMenu(hMenuBar, IDM_VIEW_SCHEMECONFIG, MF_BYCOMMAND | MF_POPUP | MF_STRING, (UINT_PTR)s_hmenuThemes, wchMenuItemStrg);
unsigned const iTheme = Globals.uCurrentThemeIndex;