A simple, small calculator placed discreetly near the Windows Taskbar or anywhere on the screen
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: add implementation plan and update spec (decimal-only, hex removed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Marco Maroni (Jul 14, 2026, 2:51 PM +0200) aa99be99 8007aad0

+1555 -9
+1537
docs/superpowers/plans/2026-07-14-win11-tray-calculator.md
··· 1 + # Windows 11 Tray Calculator Implementation Plan 2 + 3 + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 4 + 5 + **Goal:** Migrate the TaskbarCalculator DeskBand shell extension to a standalone C++ Win32 system-tray application with a persistent always-on-top calculator window, packaged as MSIX. 6 + 7 + **Architecture:** Drop the in-process COM DeskBand host. Keep the `calc` engine, the keyboard-input translation, the display formatting, and the theming; wrap them in a normal `.exe` with a `WinMain`, a tray icon, and a top-level tool window. ATL is retained (static) for `CString` and `CWindowImpl`. 8 + 9 + **Tech Stack:** C++17, Win32, ATL (static), Windows Shell (`Shell_NotifyIcon`, `SHAppBarMessage`), C++/WinRT (`StartupTask`), MSIX (`MakeAppx`/`SignTool`), Visual Studio / MSBuild. 10 + 11 + ## Global Constraints 12 + 13 + - Language: **C++17** (`/std:c++17`); Unicode build (`UNICODE` / `_UNICODE`), `TCHAR`/`_T()` throughout. 14 + - ATL retained, **static** (`UseOfAtl=Static`). No MFC. 15 + - Primary platform: **x64**. Windows 11. 16 + - Toolset/SDK: **v143 + Windows SDK 10.0.22621.0+** preferred (needed for C++/WinRT `StartupTask`). Fallback: whatever is installed (see Task 1); if `StartupTask` is unavailable, use the `HKCU\...\Run` fallback in `StartupManager`. 17 + - Settings location: `HKCU\Software\TaskbarCalculator` (values `PosX`, `PosY`, `Visible`, all `REG_DWORD`). 18 + - MSIX StartupTask `TaskId` = **`TaskbarCalculatorStartup`** (must match manifest and code exactly). 19 + - Calculator is **decimal-only**, **keyboard-only**. No Lire/Euro, no hex. 20 + - Commit after every task. Work happens on branch `feature/win11-tray-calculator` (already created). 21 + - Command examples assume the **"x64 Native Tools Command Prompt for VS"** (so `cl`, `msbuild`, `makeappx`, `signtool` are on `PATH`). 22 + 23 + --- 24 + 25 + ### Task 1: Verify build environment 26 + 27 + **Files:** 28 + - Create: `docs/superpowers/BUILD-ENV.md` 29 + 30 + **Interfaces:** 31 + - Consumes: nothing. 32 + - Produces: recorded toolset/SDK decision referenced by Task 6 (vcxproj) and Task 8 (WinRT availability). 33 + 34 + - [ ] **Step 1: Detect installed Visual Studio / MSBuild** 35 + 36 + Run: 37 + ``` 38 + where msbuild 39 + where cl 40 + ``` 41 + Expected: paths under a VS 2022 (or 2019/2017) install. If `msbuild`/`cl` are not found, open the "x64 Native Tools Command Prompt for VS" and retry. 42 + 43 + - [ ] **Step 2: List installed Windows 10/11 SDKs** 44 + 45 + Run (PowerShell): 46 + ``` 47 + Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\Include" | Select-Object Name 48 + ``` 49 + Expected: one or more folders like `10.0.22621.0`, `10.0.19041.0`, `10.0.17134.0`. 50 + 51 + - [ ] **Step 3: Check MakeAppx / SignTool presence** 52 + 53 + Run (PowerShell): 54 + ``` 55 + Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Include makeappx.exe,signtool.exe | Select-Object -First 4 FullName 56 + ``` 57 + Expected: at least one `makeappx.exe` and one `signtool.exe` under an SDK `bin\<version>\x64` folder. 58 + 59 + - [ ] **Step 4: Record decisions** 60 + 61 + Write `docs/superpowers/BUILD-ENV.md` capturing the actual findings, e.g.: 62 + ```markdown 63 + # Build Environment 64 + 65 + - MSBuild: <path/version> 66 + - Toolset chosen: v143 (fallback: v141 if v143 absent) 67 + - Windows SDK chosen: 10.0.22621.0 (fallback: <highest installed>) 68 + - C++/WinRT StartupTask available: yes | no (no => StartupManager uses Run-key fallback) 69 + - makeappx.exe: <path> 70 + - signtool.exe: <path> 71 + ``` 72 + Fill in the real values observed in Steps 1–3. 73 + 74 + - [ ] **Step 5: Commit** 75 + 76 + ```bash 77 + git add docs/superpowers/BUILD-ENV.md 78 + git commit -m "docs: record build environment for Windows 11 migration" 79 + ``` 80 + 81 + --- 82 + 83 + ### Task 2: Decouple the calc engine from the precompiled header 84 + 85 + No behavior change. This makes `Calculator.cpp` compile standalone (for unit tests) and initializes `m_precision`. 86 + 87 + **Files:** 88 + - Modify: `Calculator.cpp:1` (includes) and `Calculator.cpp:13-23` (constructor) 89 + - Test: none yet (compile-only verification) 90 + 91 + **Interfaces:** 92 + - Consumes: nothing. 93 + - Produces: `calc` compiles without `StdAfx.h`; `m_precision` defaults to `2`. 94 + 95 + - [ ] **Step 1: Replace the PCH include in `Calculator.cpp`** 96 + 97 + Change the top of `Calculator.cpp` from: 98 + ```cpp 99 + #include "StdAfx.h" 100 + #include "calculator.h" 101 + #include <string> 102 + ``` 103 + to: 104 + ```cpp 105 + #include <windows.h> 106 + #include <tchar.h> 107 + #include <cmath> 108 + #include <cstdio> 109 + #include <cstdlib> 110 + #include <crtdbg.h> 111 + #include "Calculator.h" 112 + ``` 113 + 114 + - [ ] **Step 2: Initialize `m_precision` in the constructor** 115 + 116 + In `Calculator.cpp`, in `calc::calc()`, add `m_precision = 2;` after `m_current_oper=0;`: 117 + ```cpp 118 + calc::calc() 119 + { 120 + m_oper = new double[m_max_num_oper]; 121 + m_current_oper=0; 122 + m_precision = 2; 123 + 124 + m_operator=undefined_oper; 125 + m_prev_state=undefined_state; 126 + m_state=init; 127 + m_currentBase = decimal; 128 + m_lastDecimalNumber = 0; 129 + } 130 + ``` 131 + 132 + - [ ] **Step 3: Tell the VS project not to use the PCH for this file** 133 + 134 + In `TaskbarCalculator.vcxproj`, find every `<ClCompile Include="Calculator.cpp">` entry (one per configuration, or add a config-agnostic one) and set it to not use precompiled headers. Add this item (inside the existing `<ItemGroup>` that lists `ClCompile` files), replacing a bare `<ClCompile Include="Calculator.cpp" />` if present: 135 + ```xml 136 + <ClCompile Include="Calculator.cpp"> 137 + <PrecompiledHeader>NotUsing</PrecompiledHeader> 138 + </ClCompile> 139 + ``` 140 + 141 + - [ ] **Step 4: Build the main project to confirm no regression** 142 + 143 + Run: 144 + ``` 145 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Build 146 + ``` 147 + Expected: build succeeds (this is still the DLL at this point; converting to EXE happens in Task 6). If v141/old SDK is the only toolset, the project may need the Task 6 retarget first — if so, note it and proceed; Task 3 verifies the engine independently of the VS project. 148 + 149 + - [ ] **Step 5: Commit** 150 + 151 + ```bash 152 + git add Calculator.cpp TaskbarCalculator.vcxproj 153 + git commit -m "refactor: decouple calc engine from PCH, default precision to 2" 154 + ``` 155 + 156 + --- 157 + 158 + ### Task 3: Engine unit-test harness + arithmetic characterization tests 159 + 160 + **Files:** 161 + - Create: `tests/TestHarness.h` 162 + - Create: `tests/CalcTests.cpp` 163 + - Create: `tests/build-and-run.cmd` 164 + 165 + **Interfaces:** 166 + - Consumes: `calc::change_state(TCHAR, LPCTSTR, LPTSTR)`, `calc::set_precision(unsigned)`. 167 + - Produces: `feed(calc&, const wchar_t* keys, wchar_t* out)` test helper; `TEST(name)` / `EXPECT_WSTR` macros; a runnable `CalcTests.exe`. 168 + 169 + - [ ] **Step 1: Write the test harness header** 170 + 171 + Create `tests/TestHarness.h`: 172 + ```cpp 173 + #pragma once 174 + #include <cstdio> 175 + #include <cwchar> 176 + #include <vector> 177 + #include <functional> 178 + 179 + struct TestCase { const char* name; std::function<bool()> fn; }; 180 + inline std::vector<TestCase>& registry() { static std::vector<TestCase> r; return r; } 181 + struct Registrar { Registrar(const char* n, std::function<bool()> f) { registry().push_back({ n, f }); } }; 182 + 183 + #define TEST(name) \ 184 + static bool name(); \ 185 + static Registrar reg_##name(#name, name); \ 186 + static bool name() 187 + 188 + #define EXPECT_WSTR(actual, expected) do { \ 189 + if (wcscmp((actual), (expected)) != 0) { \ 190 + wprintf(L" expected [%s] got [%s]\n", (expected), (actual)); return false; } \ 191 + } while(0) 192 + 193 + inline int run_all() { 194 + int failed = 0; 195 + for (auto& tc : registry()) { 196 + bool ok = false; 197 + try { ok = tc.fn(); } catch (...) { ok = false; } 198 + printf("[%s] %s\n", ok ? "PASS" : "FAIL", tc.name); 199 + if (!ok) failed++; 200 + } 201 + printf("\n%d/%d passed\n", (int)registry().size() - failed, (int)registry().size()); 202 + return failed ? 1 : 0; 203 + } 204 + ``` 205 + 206 + - [ ] **Step 2: Write the failing arithmetic tests** 207 + 208 + Create `tests/CalcTests.cpp`: 209 + ```cpp 210 + #include <windows.h> 211 + #include <tchar.h> 212 + #include "../Calculator.h" 213 + #include "TestHarness.h" 214 + 215 + // Feed a sequence of command chars through the engine, threading the 216 + // output string back in as the next input (as the UI does). 217 + static void feed(calc& c, const wchar_t* keys, wchar_t* out) { 218 + wcscpy(out, L"0"); 219 + wchar_t in[1024]; 220 + for (const wchar_t* p = keys; *p; ++p) { 221 + wcscpy(in, out); 222 + c.change_state(*p, in, out); 223 + } 224 + } 225 + 226 + TEST(add) { 227 + calc c; c.set_precision(2); 228 + wchar_t out[1024]; 229 + feed(c, L"2+3=", out); 230 + EXPECT_WSTR(out, L"5"); 231 + return true; 232 + } 233 + 234 + TEST(subtract) { 235 + calc c; c.set_precision(2); 236 + wchar_t out[1024]; 237 + feed(c, L"7-2=", out); 238 + EXPECT_WSTR(out, L"5"); 239 + return true; 240 + } 241 + 242 + TEST(multiply) { 243 + calc c; c.set_precision(2); 244 + wchar_t out[1024]; 245 + feed(c, L"6*7=", out); 246 + EXPECT_WSTR(out, L"42"); 247 + return true; 248 + } 249 + 250 + TEST(divide) { 251 + calc c; c.set_precision(2); 252 + wchar_t out[1024]; 253 + feed(c, L"8/2=", out); 254 + EXPECT_WSTR(out, L"4"); 255 + return true; 256 + } 257 + 258 + TEST(decimal_and_add) { 259 + calc c; c.set_precision(2); 260 + wchar_t out[1024]; 261 + feed(c, L"1.5+1.5=", out); 262 + EXPECT_WSTR(out, L"3"); 263 + return true; 264 + } 265 + 266 + TEST(chained_operations) { 267 + calc c; c.set_precision(2); 268 + wchar_t out[1024]; 269 + feed(c, L"2+3+4=", out); 270 + EXPECT_WSTR(out, L"9"); 271 + return true; 272 + } 273 + 274 + int wmain() { return run_all(); } 275 + ``` 276 + 277 + - [ ] **Step 3: Write the build-and-run script** 278 + 279 + Create `tests/build-and-run.cmd`: 280 + ```bat 281 + @echo off 282 + setlocal 283 + cd /d "%~dp0" 284 + cl /nologo /EHsc /std:c++17 /DUNICODE /D_UNICODE /I.. /Fe:CalcTests.exe CalcTests.cpp ..\Calculator.cpp 285 + if errorlevel 1 exit /b 1 286 + CalcTests.exe 287 + ``` 288 + 289 + - [ ] **Step 4: Run tests to verify they pass** 290 + 291 + Run (from an x64 Native Tools Command Prompt): 292 + ``` 293 + tests\build-and-run.cmd 294 + ``` 295 + Expected: `6/6 passed`. (These characterize the *existing* engine behavior; they should pass immediately. If any fail, the trace assumption was wrong — inspect the printed `expected/got` and correct the expected value to match real behavior before continuing.) 296 + 297 + - [ ] **Step 5: Commit** 298 + 299 + ```bash 300 + git add tests/TestHarness.h tests/CalcTests.cpp tests/build-and-run.cmd 301 + git commit -m "test: add calc engine harness and arithmetic characterization tests" 302 + ``` 303 + 304 + --- 305 + 306 + ### Task 4: Engine tests — functions and error paths 307 + 308 + **Files:** 309 + - Modify: `tests/CalcTests.cpp` (add tests before `wmain`) 310 + 311 + **Interfaces:** 312 + - Consumes: `feed`, `TEST`, `EXPECT_WSTR` from Task 3. 313 + - Produces: coverage for sqrt (`Q`), reciprocal (`I`), percent (`%`), negate (`P`), `000` (`Z`), reset (`N`), divide-by-zero, overflow. 314 + 315 + - [ ] **Step 1: Add the failing function/error tests** 316 + 317 + Insert these tests into `tests/CalcTests.cpp` immediately above `int wmain()`: 318 + ```cpp 319 + TEST(square_root) { 320 + calc c; c.set_precision(2); 321 + wchar_t out[1024]; 322 + feed(c, L"9Q", out); // Q = CALC_SQRT 323 + EXPECT_WSTR(out, L"3"); 324 + return true; 325 + } 326 + 327 + TEST(reciprocal) { 328 + calc c; c.set_precision(2); 329 + wchar_t out[1024]; 330 + feed(c, L"4I", out); // I = CALC_I (inverse, 1/x) 331 + EXPECT_WSTR(out, L"0.25"); 332 + return true; 333 + } 334 + 335 + TEST(percent) { 336 + calc c; c.set_precision(2); 337 + wchar_t out[1024]; 338 + feed(c, L"200*10%", out); // % = CALC_PERC 339 + EXPECT_WSTR(out, L"20"); 340 + return true; 341 + } 342 + 343 + TEST(negate) { 344 + calc c; c.set_precision(2); 345 + wchar_t out[1024]; 346 + feed(c, L"5P", out); // P = CALC_NEG 347 + EXPECT_WSTR(out, L"-5"); 348 + return true; 349 + } 350 + 351 + TEST(triple_zero) { 352 + calc c; c.set_precision(2); 353 + wchar_t out[1024]; 354 + feed(c, L"5Z", out); // Z = CALC_000 355 + EXPECT_WSTR(out, L"5000"); 356 + return true; 357 + } 358 + 359 + TEST(reset) { 360 + calc c; c.set_precision(2); 361 + wchar_t out[1024]; 362 + feed(c, L"123N", out); // N = CALC_RESET 363 + EXPECT_WSTR(out, L"0"); 364 + return true; 365 + } 366 + 367 + TEST(divide_by_zero) { 368 + calc c; c.set_precision(2); 369 + wchar_t out[1024]; 370 + feed(c, L"5/0=", out); 371 + EXPECT_WSTR(out, L"Error: division by zero"); 372 + return true; 373 + } 374 + 375 + TEST(overflow) { 376 + calc c; c.set_precision(2); 377 + wchar_t out[1024]; 378 + feed(c, L"99999999999999999999999999", out); // 26 digits > MAX_LEN_RESULT (25) 379 + EXPECT_WSTR(out, L"Error: overflow"); 380 + return true; 381 + } 382 + ``` 383 + 384 + - [ ] **Step 2: Run tests** 385 + 386 + Run: 387 + ``` 388 + tests\build-and-run.cmd 389 + ``` 390 + Expected: `14/14 passed`. If a value differs, correct the expected string to the observed behavior (characterization) and re-run. 391 + 392 + - [ ] **Step 3: Commit** 393 + 394 + ```bash 395 + git add tests/CalcTests.cpp 396 + git commit -m "test: cover sqrt, reciprocal, percent, negate, 000, reset, div-by-zero, overflow" 397 + ``` 398 + 399 + --- 400 + 401 + ### Task 5: Remove Lire/Euro and dead hex code from the engine 402 + 403 + **Files:** 404 + - Modify: `Calculator.h` 405 + - Modify: `Calculator.cpp` 406 + - Modify: `tests/CalcTests.cpp` (add no-op guard tests) 407 + 408 + **Interfaces:** 409 + - Consumes: existing engine tests (must stay green). 410 + - Produces: decimal-only `calc` with no `base`, `changeBase`, `toDecimal`, `restoreDecimalBase`, `changeEuroLire`, `get_current_base`; `CALC_EURO`/`CALC_L`/`CALC_A..F`/`CALC_X` no longer act. 411 + 412 + - [ ] **Step 1: Add guard tests that dropped keys are inert** 413 + 414 + Add to `tests/CalcTests.cpp` above `wmain`: 415 + ```cpp 416 + TEST(euro_key_is_noop) { 417 + calc c; c.set_precision(2); 418 + wchar_t out[1024]; 419 + feed(c, L"5R", out); // R = CALC_EURO (removed) -> must not convert 420 + EXPECT_WSTR(out, L"5"); 421 + return true; 422 + } 423 + 424 + TEST(hex_toggle_key_is_noop) { 425 + calc c; c.set_precision(2); 426 + wchar_t out[1024]; 427 + feed(c, L"255X", out); // X = CALC_X (removed) -> stays decimal text 428 + EXPECT_WSTR(out, L"255"); 429 + return true; 430 + } 431 + ``` 432 + 433 + - [ ] **Step 2: Run tests to confirm they already pass** 434 + 435 + Run: `tests\build-and-run.cmd` 436 + Expected: `16/16 passed`. (`R` already falls through today because `first_oper` calls `changeEuroLire`, which *does* convert — so `euro_key_is_noop` will **FAIL** now, printing `expected [5] got [0.00]`. Good: that failure is the point — it proves the removal is needed.) 437 + 438 + - [ ] **Step 3: Remove Lire/Euro from `Calculator.cpp`** 439 + 440 + Delete the four `case CALC_EURO: case CALC_L: changeEuroLire(newString, newChar); break;` blocks (in states `first_oper`, `second_oper`, `result`). Delete the `calc::changeEuroLire` function definition entirely. 441 + 442 + - [ ] **Step 4: Remove dead hex code from `Calculator.cpp`** 443 + 444 + - In state `init`, delete the `case CALC_A: … case CALC_F:` block and the `case CALC_0` `else addChar(...)` hex branch — simplify `CALC_0` to: 445 + ```cpp 446 + case CALC_0: 447 + m_state=first_oper; 448 + break; 449 + case CALC_000: 450 + m_state=first_oper; 451 + break; 452 + ``` 453 + - In state `operation`, delete the line `restoreDecimalBase(newString);`. 454 + - Delete the function definitions `calc::changeBase`, `calc::toDecimal`, `calc::restoreDecimalBase`, and `calc::get_current_base`. 455 + 456 + - [ ] **Step 5: Remove the corresponding declarations and members from `Calculator.h`** 457 + 458 + - Delete the defines `CALC_EURO`, `CALC_L`, `CALC_A`..`CALC_F`, `CALC_X`, `CALC_I` is **kept** (reciprocal). Delete `CHANGE_LIRE_EURO`. 459 + - Delete `base get_current_base();`, the `typedef enum _base {...} base;`, `base m_currentBase;`, `double m_lastDecimalNumber;`. 460 + - Delete method declarations `changeEuroLire`, `changeBase`, `restoreDecimalBase`, `toDecimal`. 461 + - Remove `m_currentBase`/`m_lastDecimalNumber` initialization lines from the constructor in `Calculator.cpp`. 462 + 463 + - [ ] **Step 6: Run tests** 464 + 465 + Run: `tests\build-and-run.cmd` 466 + Expected: `16/16 passed` (now `euro_key_is_noop` and `hex_toggle_key_is_noop` pass because those keys hit no case and the string is unchanged). 467 + 468 + - [ ] **Step 7: Commit** 469 + 470 + ```bash 471 + git add Calculator.h Calculator.cpp tests/CalcTests.cpp 472 + git commit -m "feat: remove Lire/Euro conversion and dead hex code (decimal-only engine)" 473 + ``` 474 + 475 + --- 476 + 477 + ### Task 6: Convert the project from DLL to standalone EXE 478 + 479 + Turns the build into a windowed `.exe` with a stub `WinMain`, removes the COM/DeskBand plumbing. 480 + 481 + **Files:** 482 + - Create: `App.cpp` 483 + - Delete: `CalculatorDeskBand.cpp`, `CalculatorDeskBand.h`, `DllMain.cpp`, `DllMain.h`, `TaskbarCalculatorDeskBandDll.cpp`, `TaskbarCalculator.def`, `TaskbarCalculator.rgs`, `TaskbarCalculator.idl`, `Guids.h`, `AboutDialog.cpp`, `AboutDialog.h` 484 + - Modify: `TaskbarCalculator.vcxproj`, `TaskbarCalculator.vcxproj.filters` 485 + 486 + **Interfaces:** 487 + - Consumes: nothing yet. 488 + - Produces: `int WINAPI wWinMain(...)` entry point producing a running (empty) process. 489 + 490 + - [ ] **Step 1: Write a stub `App.cpp`** 491 + 492 + Create `App.cpp`: 493 + ```cpp 494 + #include "stdafx.h" 495 + 496 + int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) 497 + { 498 + MessageBox(NULL, L"Taskbar Calculator (stub)", L"TaskbarCalculator", MB_OK); 499 + return 0; 500 + } 501 + ``` 502 + 503 + - [ ] **Step 2: Delete the DeskBand/COM source files** 504 + 505 + ```bash 506 + git rm CalculatorDeskBand.cpp CalculatorDeskBand.h DllMain.cpp DllMain.h \ 507 + TaskbarCalculatorDeskBandDll.cpp TaskbarCalculator.def TaskbarCalculator.rgs \ 508 + TaskbarCalculator.idl Guids.h AboutDialog.cpp AboutDialog.h 509 + ``` 510 + (`Utils.cpp` still `#include "Guids.h"` — remove that include in the next step.) 511 + 512 + - [ ] **Step 3: Fix `Utils.cpp` include** 513 + 514 + In `Utils.cpp`, delete the line `#include "Guids.h"`. 515 + 516 + - [ ] **Step 4: Retarget and convert the vcxproj** 517 + 518 + Edit `TaskbarCalculator.vcxproj`: 519 + - For every `<PropertyGroup Label="Configuration">`, set: 520 + ```xml 521 + <ConfigurationType>Application</ConfigurationType> 522 + <PlatformToolset>v143</PlatformToolset> 523 + ``` 524 + (use `v141` only if Task 1 found nothing newer). 525 + - Set `<WindowsTargetPlatformVersion>` to the value chosen in Task 1 (e.g. `10.0.22621.0`). 526 + - In each `<ClCompile>` `<ItemDefinitionGroup>` section add C++17: 527 + ```xml 528 + <LanguageStandard>stdcpp17</LanguageStandard> 529 + ``` 530 + - In each `<Link>` section set the Windows subsystem and remove any `.def`/`RegisterOutput`: 531 + ```xml 532 + <SubSystem>Windows</SubSystem> 533 + ``` 534 + Delete any `<ModuleDefinitionFile>TaskbarCalculator.def</ModuleDefinitionFile>` and any `<RegisterOutput>true</RegisterOutput>` lines. 535 + - In the `<ItemGroup>` of `<ClCompile>`/`<ClInclude>`, remove entries for the deleted files and add `<ClCompile Include="App.cpp" />`. 536 + 537 + - [ ] **Step 5: Sync the filters file** 538 + 539 + Edit `TaskbarCalculator.vcxproj.filters`: remove entries for the deleted files and add `<ClCompile Include="App.cpp"><Filter>Source Files</Filter></ClCompile>`. 540 + 541 + - [ ] **Step 6: Build and run the stub** 542 + 543 + Run: 544 + ``` 545 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Rebuild 546 + x64\Debug\TaskbarCalculator.exe 547 + ``` 548 + Expected: build succeeds; running shows the stub message box, then exits. 549 + 550 + - [ ] **Step 7: Commit** 551 + 552 + ```bash 553 + git add -A 554 + git commit -m "build: convert project from DeskBand DLL to standalone EXE (stub WinMain)" 555 + ``` 556 + 557 + --- 558 + 559 + ### Task 7: Settings module (window position + visibility) 560 + 561 + **Files:** 562 + - Create: `Settings.h`, `Settings.cpp` 563 + - Modify: `TaskbarCalculator.vcxproj`, `.filters` (add the two files) 564 + 565 + **Interfaces:** 566 + - Consumes: nothing. 567 + - Produces: 568 + ```cpp 569 + namespace Settings { 570 + bool LoadWindowPos(POINT& pt); // true if a saved position exists 571 + void SaveWindowPos(POINT pt); 572 + bool LoadVisible(bool def); // returns saved visibility or `def` 573 + void SaveVisible(bool visible); 574 + } 575 + ``` 576 + 577 + - [ ] **Step 1: Write the header** 578 + 579 + Create `Settings.h`: 580 + ```cpp 581 + #pragma once 582 + #include <windows.h> 583 + 584 + namespace Settings { 585 + bool LoadWindowPos(POINT& pt); 586 + void SaveWindowPos(POINT pt); 587 + bool LoadVisible(bool def); 588 + void SaveVisible(bool visible); 589 + } 590 + ``` 591 + 592 + - [ ] **Step 2: Write the implementation** 593 + 594 + Create `Settings.cpp`: 595 + ```cpp 596 + #include "stdafx.h" 597 + #include "Settings.h" 598 + 599 + namespace { 600 + const wchar_t* kKey = L"Software\\TaskbarCalculator"; 601 + 602 + bool ReadDword(const wchar_t* name, DWORD& out) { 603 + HKEY h; 604 + if (RegOpenKeyExW(HKEY_CURRENT_USER, kKey, 0, KEY_QUERY_VALUE, &h) != ERROR_SUCCESS) 605 + return false; 606 + DWORD type = 0, val = 0, size = sizeof(val); 607 + LONG r = RegQueryValueExW(h, name, nullptr, &type, (LPBYTE)&val, &size); 608 + RegCloseKey(h); 609 + if (r != ERROR_SUCCESS || type != REG_DWORD) return false; 610 + out = val; 611 + return true; 612 + } 613 + 614 + void WriteDword(const wchar_t* name, DWORD val) { 615 + HKEY h; 616 + if (RegCreateKeyExW(HKEY_CURRENT_USER, kKey, 0, nullptr, 0, 617 + KEY_SET_VALUE, nullptr, &h, nullptr) != ERROR_SUCCESS) 618 + return; 619 + RegSetValueExW(h, name, 0, REG_DWORD, (const BYTE*)&val, sizeof(val)); 620 + RegCloseKey(h); 621 + } 622 + } 623 + 624 + namespace Settings { 625 + bool LoadWindowPos(POINT& pt) { 626 + DWORD x, y; 627 + if (ReadDword(L"PosX", x) && ReadDword(L"PosY", y)) { 628 + pt.x = (LONG)(INT)x; pt.y = (LONG)(INT)y; 629 + return true; 630 + } 631 + return false; 632 + } 633 + void SaveWindowPos(POINT pt) { 634 + WriteDword(L"PosX", (DWORD)pt.x); 635 + WriteDword(L"PosY", (DWORD)pt.y); 636 + } 637 + bool LoadVisible(bool def) { 638 + DWORD v; 639 + return ReadDword(L"Visible", v) ? (v != 0) : def; 640 + } 641 + void SaveVisible(bool visible) { 642 + WriteDword(L"Visible", visible ? 1 : 0); 643 + } 644 + } 645 + ``` 646 + 647 + - [ ] **Step 3: Add the files to the project** 648 + 649 + In `TaskbarCalculator.vcxproj` add `<ClCompile Include="Settings.cpp" />` and `<ClInclude Include="Settings.h" />`; mirror in `.filters`. 650 + 651 + - [ ] **Step 4: Build to confirm it compiles** 652 + 653 + Run: 654 + ``` 655 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Build 656 + ``` 657 + Expected: build succeeds. 658 + 659 + - [ ] **Step 5: Manually verify round-trip** 660 + 661 + Temporarily add to `wWinMain` (before the message box), build, run once, then check the registry: 662 + ```cpp 663 + Settings::SaveWindowPos(POINT{ 111, 222 }); 664 + ``` 665 + Run (PowerShell): 666 + ``` 667 + Get-ItemProperty HKCU:\Software\TaskbarCalculator | Select-Object PosX,PosY 668 + ``` 669 + Expected: `PosX = 111`, `PosY = 222`. Then **remove** the temporary line. 670 + 671 + - [ ] **Step 6: Commit** 672 + 673 + ```bash 674 + git add Settings.h Settings.cpp TaskbarCalculator.vcxproj TaskbarCalculator.vcxproj.filters 675 + git commit -m "feat: add Settings module (registry-backed window position and visibility)" 676 + ``` 677 + 678 + --- 679 + 680 + ### Task 8: StartupManager module (start-at-login) 681 + 682 + **Files:** 683 + - Create: `StartupManager.h`, `StartupManager.cpp` 684 + - Modify: `TaskbarCalculator.vcxproj`, `.filters` 685 + 686 + **Interfaces:** 687 + - Consumes: MSIX StartupTask id `TaskbarCalculatorStartup` (Task 13 manifest). 688 + - Produces: 689 + ```cpp 690 + namespace Startup { 691 + enum class State { Enabled, Disabled, Unavailable }; 692 + State Get(); // queries MSIX StartupTask; Unavailable when unpackaged 693 + bool Set(bool on); // returns true if the request was applied 694 + } 695 + ``` 696 + 697 + - [ ] **Step 1: Write the header** 698 + 699 + Create `StartupManager.h`: 700 + ```cpp 701 + #pragma once 702 + 703 + namespace Startup { 704 + enum class State { Enabled, Disabled, Unavailable }; 705 + State Get(); 706 + bool Set(bool on); 707 + } 708 + ``` 709 + 710 + - [ ] **Step 2: Write the implementation (WinRT with unpackaged fallback)** 711 + 712 + Create `StartupManager.cpp`: 713 + ```cpp 714 + #include "stdafx.h" 715 + #include "StartupManager.h" 716 + 717 + #include <winrt/Windows.Foundation.h> 718 + #include <winrt/Windows.ApplicationModel.h> 719 + 720 + using namespace winrt; 721 + using namespace winrt::Windows::ApplicationModel; 722 + 723 + namespace { 724 + const wchar_t* kTaskId = L"TaskbarCalculatorStartup"; 725 + } 726 + 727 + namespace Startup { 728 + 729 + State Get() { 730 + try { 731 + auto task = StartupTask::GetAsync(kTaskId).get(); 732 + switch (task.State()) { 733 + case StartupTaskState::Enabled: 734 + case StartupTaskState::EnabledByPolicy: 735 + return State::Enabled; 736 + default: 737 + return State::Disabled; 738 + } 739 + } catch (...) { 740 + return State::Unavailable; // unpackaged / API missing 741 + } 742 + } 743 + 744 + bool Set(bool on) { 745 + try { 746 + auto task = StartupTask::GetAsync(kTaskId).get(); 747 + if (on) { 748 + auto result = task.RequestEnableAsync().get(); 749 + return result == StartupTaskState::Enabled 750 + || result == StartupTaskState::EnabledByPolicy; 751 + } else { 752 + task.Disable(); 753 + return true; 754 + } 755 + } catch (...) { 756 + return false; 757 + } 758 + } 759 + 760 + } 761 + ``` 762 + 763 + - [ ] **Step 3: Enable C++/WinRT in the project** 764 + 765 + In `TaskbarCalculator.vcxproj`, ensure `<LanguageStandard>stdcpp17</LanguageStandard>` is set (done in Task 6) and add, in each `<ClCompile>` `<ItemDefinitionGroup>`: 766 + ```xml 767 + <AdditionalOptions>%(AdditionalOptions) /await</AdditionalOptions> 768 + ``` 769 + Add `<ClCompile Include="StartupManager.cpp" />` and `<ClInclude Include="StartupManager.h" />` to the item groups; mirror in `.filters`. 770 + 771 + - [ ] **Step 4: Build** 772 + 773 + Run: 774 + ``` 775 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Build 776 + ``` 777 + Expected: build succeeds. If the C++/WinRT headers are not found, the SDK chosen in Task 1 lacks cppwinrt — install a 10.0.19041.0+ SDK or set the fallback: replace the body of `Get`/`Set` with an `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` implementation (documented in `docs/superpowers/BUILD-ENV.md`). 778 + 779 + - [ ] **Step 5: Manual smoke test (unpackaged expectation)** 780 + 781 + Temporarily call `Startup::Get()` from `wWinMain` and message-box the result. Run unpackaged (`x64\Debug\TaskbarCalculator.exe`): expected `Unavailable` (correct — StartupTask needs package identity, added in Task 13). Remove the temporary code. 782 + 783 + - [ ] **Step 6: Commit** 784 + 785 + ```bash 786 + git add StartupManager.h StartupManager.cpp TaskbarCalculator.vcxproj TaskbarCalculator.vcxproj.filters 787 + git commit -m "feat: add StartupManager (MSIX StartupTask with unpackaged fallback)" 788 + ``` 789 + 790 + --- 791 + 792 + ### Task 9: Tray icon, owner window, single-instance, About/Exit 793 + 794 + **Files:** 795 + - Create: `TrayIcon.h`, `TrayIcon.cpp` 796 + - Create: `AboutDialog.cpp` (raw Win32 dialog proc; the old MFC one was deleted) 797 + - Modify: `App.cpp` (full owner window + message loop + single instance) 798 + - Modify: `TaskbarCalculator.vcxproj`, `.filters` 799 + 800 + **Interfaces:** 801 + - Consumes: `IDI_ICON1` (resource), `IDD_ABOUT`/`IDC_SYSLINK1` (resource), `Startup::Get/Set` (Task 8). 802 + - Produces: 803 + - `class TrayIcon { bool Add(HWND owner, UINT cbMsg, UINT iconId, const wchar_t* tip); void Remove(); void ShowContextMenu(HWND owner, bool startupChecked); };` 804 + - App constants: `WM_TRAY_CALLBACK` (`WM_APP+1`), menu ids `IDM_SHOWHIDE=1`, `IDM_STARTUP=2`, `IDM_ABOUT=3`, `IDM_EXIT=4`. 805 + - `INT_PTR CALLBACK AboutDialogProc(HWND, UINT, WPARAM, LPARAM);` 806 + - `extern UINT g_wmTaskbarCreated;` (registered `"TaskbarCreated"` message). 807 + 808 + - [ ] **Step 1: Write `TrayIcon.h`** 809 + 810 + ```cpp 811 + #pragma once 812 + #include <windows.h> 813 + #include <shellapi.h> 814 + 815 + class TrayIcon { 816 + public: 817 + bool Add(HWND owner, UINT cbMsg, UINT iconId, const wchar_t* tip); 818 + void Remove(); 819 + void ShowContextMenu(HWND owner, bool startupChecked, bool startupAvailable); 820 + private: 821 + NOTIFYICONDATAW m_nid{}; 822 + bool m_added = false; 823 + }; 824 + ``` 825 + 826 + - [ ] **Step 2: Write `TrayIcon.cpp`** 827 + 828 + ```cpp 829 + #include "stdafx.h" 830 + #include "TrayIcon.h" 831 + #include "resource.h" 832 + 833 + // Menu command ids (shared with App.cpp via TrayCommands.h-free convention). 834 + enum { IDM_SHOWHIDE = 1, IDM_STARTUP = 2, IDM_ABOUT = 3, IDM_EXIT = 4 }; 835 + 836 + bool TrayIcon::Add(HWND owner, UINT cbMsg, UINT iconId, const wchar_t* tip) { 837 + m_nid = {}; 838 + m_nid.cbSize = sizeof(m_nid); 839 + m_nid.hWnd = owner; 840 + m_nid.uID = iconId; 841 + m_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; 842 + m_nid.uCallbackMessage = cbMsg; 843 + m_nid.hIcon = (HICON)LoadImage(_AtlBaseModule.GetModuleInstance(), 844 + MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, 845 + GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), 0); 846 + lstrcpynW(m_nid.szTip, tip, ARRAYSIZE(m_nid.szTip)); 847 + m_added = Shell_NotifyIconW(NIM_ADD, &m_nid) != FALSE; 848 + return m_added; 849 + } 850 + 851 + void TrayIcon::Remove() { 852 + if (m_added) { Shell_NotifyIconW(NIM_DELETE, &m_nid); m_added = false; } 853 + if (m_nid.hIcon) { DestroyIcon(m_nid.hIcon); m_nid.hIcon = nullptr; } 854 + } 855 + 856 + void TrayIcon::ShowContextMenu(HWND owner, bool startupChecked, bool startupAvailable) { 857 + HMENU menu = CreatePopupMenu(); 858 + AppendMenuW(menu, MF_STRING, IDM_SHOWHIDE, L"Show / Hide"); 859 + AppendMenuW(menu, MF_STRING | (startupChecked ? MF_CHECKED : 0) 860 + | (startupAvailable ? 0 : MF_GRAYED), 861 + IDM_STARTUP, L"Start at login"); 862 + AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); 863 + AppendMenuW(menu, MF_STRING, IDM_ABOUT, L"About"); 864 + AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit"); 865 + 866 + POINT pt; GetCursorPos(&pt); 867 + SetForegroundWindow(owner); // so the menu dismisses correctly 868 + TrackPopupMenu(menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, owner, nullptr); 869 + DestroyMenu(menu); 870 + } 871 + ``` 872 + 873 + - [ ] **Step 3: Write `AboutDialog.cpp` (raw Win32, ported from the old DeskBand proc)** 874 + 875 + ```cpp 876 + #include "stdafx.h" 877 + #include <commctrl.h> 878 + 879 + INT_PTR CALLBACK AboutDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) 880 + { 881 + switch (uMsg) { 882 + case WM_INITDIALOG: 883 + return TRUE; 884 + case WM_NOTIFY: 885 + switch (((LPNMHDR)lParam)->code) { 886 + case NM_CLICK: 887 + case NM_RETURN: { 888 + PNMLINK p = (PNMLINK)lParam; 889 + if (p->item.iLink == 0) 890 + ShellExecuteW(NULL, L"open", p->item.szUrl, NULL, NULL, SW_SHOW); 891 + break; 892 + } 893 + } 894 + break; 895 + case WM_COMMAND: 896 + switch (LOWORD(wParam)) { 897 + case IDOK: 898 + case IDCANCEL: 899 + EndDialog(hwndDlg, TRUE); 900 + return TRUE; 901 + } 902 + break; 903 + } 904 + return FALSE; 905 + } 906 + ``` 907 + 908 + - [ ] **Step 4: Write the real `App.cpp` (owner window, single instance, message loop)** 909 + 910 + Replace `App.cpp` with: 911 + ```cpp 912 + #include "stdafx.h" 913 + #include "TrayIcon.h" 914 + #include "StartupManager.h" 915 + #include "resource.h" 916 + 917 + enum { IDM_SHOWHIDE = 1, IDM_STARTUP = 2, IDM_ABOUT = 3, IDM_EXIT = 4 }; 918 + static const UINT WM_TRAY_CALLBACK = WM_APP + 1; 919 + static const UINT TRAY_ICON_ID = 1; 920 + 921 + UINT g_wmTaskbarCreated = 0; 922 + static TrayIcon g_tray; 923 + 924 + INT_PTR CALLBACK AboutDialogProc(HWND, UINT, WPARAM, LPARAM); // AboutDialog.cpp 925 + 926 + // Placeholder until Task 10 introduces the calculator window. 927 + static void ToggleCalculator() { /* wired in Task 10 */ } 928 + 929 + static LRESULT CALLBACK OwnerWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) 930 + { 931 + if (msg == g_wmTaskbarCreated) { // Explorer restarted 932 + g_tray.Add(hWnd, WM_TRAY_CALLBACK, TRAY_ICON_ID, L"Taskbar Calculator"); 933 + return 0; 934 + } 935 + switch (msg) { 936 + case WM_TRAY_CALLBACK: 937 + if (LOWORD(lParam) == WM_LBUTTONUP) { 938 + ToggleCalculator(); 939 + } else if (LOWORD(lParam) == WM_RBUTTONUP) { 940 + Startup::State s = Startup::Get(); 941 + g_tray.ShowContextMenu(hWnd, s == Startup::State::Enabled, 942 + s != Startup::State::Unavailable); 943 + } 944 + return 0; 945 + case WM_COMMAND: 946 + switch (LOWORD(wParam)) { 947 + case IDM_SHOWHIDE: ToggleCalculator(); return 0; 948 + case IDM_STARTUP: Startup::Set(Startup::Get() != Startup::State::Enabled); return 0; 949 + case IDM_ABOUT: 950 + DialogBoxW(_AtlBaseModule.GetModuleInstance(), 951 + MAKEINTRESOURCE(IDD_ABOUT), hWnd, AboutDialogProc); 952 + return 0; 953 + case IDM_EXIT: 954 + DestroyWindow(hWnd); 955 + return 0; 956 + } 957 + return 0; 958 + case WM_DESTROY: 959 + g_tray.Remove(); 960 + PostQuitMessage(0); 961 + return 0; 962 + } 963 + return DefWindowProc(hWnd, msg, wParam, lParam); 964 + } 965 + 966 + int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int) 967 + { 968 + // Single instance: a second launch signals the first and exits. 969 + HANDLE mutex = CreateMutexW(nullptr, TRUE, L"TaskbarCalculator_SingleInstance"); 970 + if (GetLastError() == ERROR_ALREADY_EXISTS) { 971 + HWND existing = FindWindowW(L"TaskbarCalculatorOwner", nullptr); 972 + if (existing) PostMessageW(existing, WM_COMMAND, IDM_SHOWHIDE, 0); 973 + return 0; 974 + } 975 + 976 + g_wmTaskbarCreated = RegisterWindowMessageW(L"TaskbarCreated"); 977 + 978 + WNDCLASSW wc{}; 979 + wc.lpfnWndProc = OwnerWndProc; 980 + wc.hInstance = hInst; 981 + wc.lpszClassName = L"TaskbarCalculatorOwner"; 982 + RegisterClassW(&wc); 983 + 984 + HWND owner = CreateWindowW(wc.lpszClassName, L"", 0, 0, 0, 0, 0, 985 + HWND_MESSAGE, nullptr, hInst, nullptr); 986 + 987 + g_tray.Add(owner, WM_TRAY_CALLBACK, TRAY_ICON_ID, L"Taskbar Calculator"); 988 + 989 + MSG m; 990 + while (GetMessage(&m, nullptr, 0, 0)) { 991 + TranslateMessage(&m); 992 + DispatchMessage(&m); 993 + } 994 + if (mutex) { ReleaseMutex(mutex); CloseHandle(mutex); } 995 + return (int)m.wParam; 996 + } 997 + ``` 998 + > Note: `HWND_MESSAGE` windows do not receive the broadcast `TaskbarCreated`. This is acceptable for the stub; Task 11 promotes the owner to a hidden top-level window (still no taskbar button) so the re-add path works. Keep the code above as-is for this task. 999 + 1000 + - [ ] **Step 5: Add files to the project** 1001 + 1002 + Add `TrayIcon.cpp`, `TrayIcon.h`, `AboutDialog.cpp` to `TaskbarCalculator.vcxproj` and `.filters`. Add `#include <commctrl.h>` reliance: ensure `#pragma comment(lib, "comctl32.lib")` exists in `stdafx.h` (add it if missing). 1003 + 1004 + - [ ] **Step 6: Build and run** 1005 + 1006 + Run: 1007 + ``` 1008 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Rebuild 1009 + x64\Debug\TaskbarCalculator.exe 1010 + ``` 1011 + Expected: a tray icon appears. Right-click → menu with *Show/Hide*, *Start at login* (grayed, because unpackaged → `Unavailable`), *About* (opens the dialog; the SysLink is clickable), *Exit* (removes the icon and quits). Left-click does nothing yet (Task 10). Launching a second instance does not add a second icon. 1012 + 1013 + - [ ] **Step 7: Commit** 1014 + 1015 + ```bash 1016 + git add App.cpp TrayIcon.h TrayIcon.cpp AboutDialog.cpp stdafx.h TaskbarCalculator.vcxproj TaskbarCalculator.vcxproj.filters 1017 + git commit -m "feat: tray icon, owner window, single-instance guard, About/Exit" 1018 + ``` 1019 + 1020 + --- 1021 + 1022 + ### Task 10: Rework CalculatorWindow into a standalone top-level window 1023 + 1024 + Repurpose the existing `CCalculatorWindow` (ATL `CWindowImpl`) from a DeskBand child into an always-on-top tool window, reusing the input/formatting logic verbatim. 1025 + 1026 + **Files:** 1027 + - Modify: `CalculatorWindow.h`, `CalculatorWindow.cpp` 1028 + - Modify: `App.cpp` (create the window; wire `ToggleCalculator`) 1029 + 1030 + **Interfaces:** 1031 + - Consumes: `calc` (Task 5), `CVisualStyle`. 1032 + - Produces: 1033 + ```cpp 1034 + class CCalculatorWindow : public CWindowImpl<CCalculatorWindow> { 1035 + public: 1036 + BOOL CreateStandalone(); // creates the top-level popup, hidden 1037 + void ToggleVisible(); // show (restore topmost) / hide 1038 + bool IsShown() const; 1039 + }; 1040 + ``` 1041 + 1042 + - [ ] **Step 1: Strip DeskBand coupling from `CalculatorWindow.h`** 1043 + 1044 + - Remove `#include`/usage of `IInputObjectSite`, `m_pDeskBand`, `m_spInputObjectSite`. 1045 + - Change the constructor signature to `CCalculatorWindow();` (no args). 1046 + - Replace the old `BOOL Create(HWND, LPUNKNOWN, LPUNKNOWN);` with: 1047 + ```cpp 1048 + BOOL CreateStandalone(); 1049 + void ToggleVisible(); 1050 + bool IsShown() const { return m_shown; } 1051 + ``` 1052 + - Remove `CalcMinimalSize`, `CalcIdealSize`'s DeskBand usage stays (used for sizing). Keep `FormatString`, `ReadRegionalSettings`, `OnEditKeyDown`, `OnEditChar`, `ProcessChar`. 1053 + - Add private members: `bool m_shown = false;`. 1054 + - In the message map add handlers (declared below): `WM_CLOSE`, `WM_LBUTTONDOWN`, `WM_MOVE`. 1055 + 1056 + - [ ] **Step 2: Rewrite `Create` as `CreateStandalone` in `CalculatorWindow.cpp`** 1057 + 1058 + Replace the body of the old `Create(...)` with: 1059 + ```cpp 1060 + BOOL CCalculatorWindow::CreateStandalone() 1061 + { 1062 + RECT rc = { 0, 0, 220, 48 }; 1063 + // Top-level popup, always-on-top, tool window (no taskbar/Alt-Tab button). 1064 + HWND hwnd = __super::Create(NULL, rc, L"Taskbar Calculator", 1065 + WS_POPUP | WS_CAPTION | WS_SYSMENU, 1066 + WS_EX_TOPMOST | WS_EX_TOOLWINDOW); 1067 + if (!hwnd) return FALSE; 1068 + 1069 + m_hwndEdit = CreateWindowEx(WS_EX_CLIENTEDGE, TEXT("EDIT"), TEXT("0"), 1070 + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | ES_RIGHT | ES_NUMBER, 1071 + 0, 0, 0, 0, m_hWnd, NULL, _AtlBaseModule.GetModuleInstance(), 0); 1072 + 1073 + SetWindowFont(m_hwndEdit, m_ptrVisualStyle->GetFont(), TRUE); 1074 + wpOrigEditProc = (WNDPROC)::SetWindowLongPtr(m_hwndEdit, GWLP_WNDPROC, (LONG_PTR)WndEditProc); 1075 + if (wpOrigEditProc == 0) return FALSE; 1076 + ::SetWindowLongPtr(m_hwndEdit, GWLP_USERDATA, (LONG_PTR)this); 1077 + 1078 + POINTL ideal = CalcIdealSize(); 1079 + SetWindowPos(NULL, 0, 0, ideal.x + 24, ideal.y + 16, SWP_NOMOVE | SWP_NOZORDER); 1080 + return TRUE; 1081 + } 1082 + 1083 + void CCalculatorWindow::ToggleVisible() 1084 + { 1085 + if (m_shown) { 1086 + ShowWindow(SW_HIDE); 1087 + m_shown = false; 1088 + } else { 1089 + SetWindowPos(HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); 1090 + SetForegroundWindow(m_hWnd); 1091 + ::SetFocus(m_hwndEdit); 1092 + m_shown = true; 1093 + } 1094 + } 1095 + ``` 1096 + 1097 + - [ ] **Step 3: Remove the DeskBand focus notification in `OnFocus`** 1098 + 1099 + In `CalculatorWindow.cpp` `OnFocus`, delete the `if (m_spInputObjectSite) m_spInputObjectSite->OnFocusChangeIS(...)` block. Keep the `ReadRegionalSettings()` on focus: 1100 + ```cpp 1101 + LRESULT CCalculatorWindow::OnFocus(UINT uMsg, WPARAM, LPARAM, BOOL&) { 1102 + m_fHasFocus = (uMsg == WM_SETFOCUS); 1103 + if (m_fHasFocus) ReadRegionalSettings(); 1104 + return 0L; 1105 + } 1106 + ``` 1107 + 1108 + - [ ] **Step 4: Simplify `FormatString` (no more base check)** 1109 + 1110 + In `CalculatorWindow.cpp` `FormatString`, remove the `if (m_calc.get_current_base() == calc::decimal)` guard (the method no longer exists) and always run the decimal-separator formatting body: 1111 + ```cpp 1112 + bool CCalculatorWindow::FormatString(LPCTSTR strInput, LPTSTR strOutput) { 1113 + _tcscpy_s(strOutput, 1024, strInput); 1114 + LPTSTR cDecSep = _tcschr(strOutput, CALC_P); 1115 + if (cDecSep != NULL) *cDecSep = m_cDecSepStandard; 1116 + _tcsrev(strOutput); 1117 + // ... (rest of the existing formatting body, unchanged) ... 1118 + return true; 1119 + } 1120 + ``` 1121 + Keep the remaining formatting loop exactly as it is today. 1122 + 1123 + - [ ] **Step 5: Add `WM_CLOSE` (hide, don't destroy) and `WM_LBUTTONDOWN` (drag)** 1124 + 1125 + Add to the message map in `CalculatorWindow.h`: 1126 + ```cpp 1127 + MESSAGE_HANDLER(WM_CLOSE, OnClose) 1128 + MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown) 1129 + ``` 1130 + Add the handlers in `CalculatorWindow.cpp`: 1131 + ```cpp 1132 + LRESULT CCalculatorWindow::OnClose(UINT, WPARAM, LPARAM, BOOL&) { 1133 + ShowWindow(SW_HIDE); 1134 + m_shown = false; 1135 + return 0; // do NOT destroy; hide to tray 1136 + } 1137 + LRESULT CCalculatorWindow::OnLButtonDown(UINT, WPARAM, LPARAM, BOOL&) { 1138 + // let the user drag the window by its body 1139 + ReleaseCapture(); 1140 + SendMessage(WM_NCLBUTTONDOWN, HTCAPTION, 0); 1141 + return 0; 1142 + } 1143 + ``` 1144 + Declare `OnClose` and `OnLButtonDown` in the private handler section of the header. 1145 + 1146 + - [ ] **Step 6: Wire the window into `App.cpp`** 1147 + 1148 + In `App.cpp`: 1149 + - add `#include "CalculatorWindow.h"`; 1150 + - add a global `static CCalculatorWindow g_calc;` 1151 + - implement `ToggleCalculator`: 1152 + ```cpp 1153 + static void ToggleCalculator() { g_calc.ToggleVisible(); } 1154 + ``` 1155 + - in `wWinMain`, after `g_tray.Add(...)` and before the message loop: 1156 + ```cpp 1157 + g_calc.CreateStandalone(); 1158 + ``` 1159 + 1160 + - [ ] **Step 7: Build and run** 1161 + 1162 + Run: 1163 + ``` 1164 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Rebuild 1165 + x64\Debug\TaskbarCalculator.exe 1166 + ``` 1167 + Expected: left-click the tray icon → the calculator window appears (always-on-top). Typing digits/operators works; `=` computes; the caption "X" hides it (does not exit). Left-click again toggles visibility. It stays on top when another window is focused. Dragging the body moves it. 1168 + 1169 + - [ ] **Step 8: Commit** 1170 + 1171 + ```bash 1172 + git add CalculatorWindow.h CalculatorWindow.cpp App.cpp 1173 + git commit -m "feat: standalone always-on-top calculator window, reusing input/formatting" 1174 + ``` 1175 + 1176 + --- 1177 + 1178 + ### Task 11: Position persistence + first-run tray anchoring + robust re-add 1179 + 1180 + **Files:** 1181 + - Modify: `CalculatorWindow.cpp`, `CalculatorWindow.h` (WM_MOVE save + anchoring) 1182 + - Modify: `App.cpp` (owner as hidden top-level so `TaskbarCreated` works) 1183 + 1184 + **Interfaces:** 1185 + - Consumes: `Settings::LoadWindowPos/SaveWindowPos` (Task 7). 1186 + - Produces: window opens at saved position or anchored above the tray; moves are persisted. 1187 + 1188 + - [ ] **Step 1: Add anchoring + saved-position logic to `CreateStandalone`** 1189 + 1190 + In `CalculatorWindow.cpp`, add a helper and call it at the end of `CreateStandalone` (replacing the `SetWindowPos` sizing-only call): 1191 + ```cpp 1192 + void CCalculatorWindow::PlaceInitial() { 1193 + RECT wr; GetWindowRect(&wr); 1194 + int w = wr.right - wr.left, h = wr.bottom - wr.top; 1195 + 1196 + POINT pt; 1197 + if (Settings::LoadWindowPos(pt)) { 1198 + SetWindowPos(NULL, pt.x, pt.y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); 1199 + return; 1200 + } 1201 + // First run: anchor bottom-right, just above the notification area. 1202 + APPBARDATA abd{}; abd.cbSize = sizeof(abd); 1203 + RECT work; SystemParametersInfo(SPI_GETWORKAREA, 0, &work, 0); 1204 + int x = work.right - w - 8; 1205 + int y = work.bottom - h - 8; 1206 + if (SHAppBarMessage(ABM_GETTASKBARPOS, &abd)) { 1207 + // keep above the taskbar's top edge when it's at the bottom 1208 + if (abd.rc.top > work.top) y = abd.rc.top - h - 8; 1209 + } 1210 + SetWindowPos(NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); 1211 + } 1212 + ``` 1213 + Declare `void PlaceInitial();` in the header. In `CreateStandalone`, after setting the size, call `PlaceInitial();`. Add `#include "Settings.h"` at the top of `CalculatorWindow.cpp`. 1214 + 1215 + - [ ] **Step 2: Persist position on move** 1216 + 1217 + Add to the message map: `MESSAGE_HANDLER(WM_EXITSIZEMOVE, OnExitSizeMove)`. Implement: 1218 + ```cpp 1219 + LRESULT CCalculatorWindow::OnExitSizeMove(UINT, WPARAM, LPARAM, BOOL&) { 1220 + RECT wr; GetWindowRect(&wr); 1221 + Settings::SaveWindowPos(POINT{ wr.left, wr.top }); 1222 + return 0; 1223 + } 1224 + ``` 1225 + Declare `OnExitSizeMove` in the header. 1226 + 1227 + - [ ] **Step 3: Make the owner window a hidden top-level window (fix TaskbarCreated)** 1228 + 1229 + In `App.cpp`, change the owner window creation from `HWND_MESSAGE` to a hidden top-level tool window so it still receives the broadcast `TaskbarCreated`: 1230 + ```cpp 1231 + HWND owner = CreateWindowExW(WS_EX_TOOLWINDOW, wc.lpszClassName, L"", 1232 + WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInst, nullptr); 1233 + // never shown 1234 + ``` 1235 + (No `ShowWindow` call → it stays invisible, and being top-level it receives `WM_TASKBARCREATED`.) 1236 + 1237 + - [ ] **Step 4: Build and run** 1238 + 1239 + Run: 1240 + ``` 1241 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Rebuild 1242 + x64\Debug\TaskbarCalculator.exe 1243 + ``` 1244 + Expected: first launch → window anchored bottom-right above the tray. Drag it elsewhere, exit, relaunch → it reappears where you left it. Restart Explorer (Task Manager → restart Windows Explorer) → the tray icon reappears. 1245 + 1246 + - [ ] **Step 5: Commit** 1247 + 1248 + ```bash 1249 + git add CalculatorWindow.h CalculatorWindow.cpp App.cpp 1250 + git commit -m "feat: persist window position, anchor above tray on first run, survive Explorer restart" 1251 + ``` 1252 + 1253 + --- 1254 + 1255 + ### Task 12: Wire start-at-login toggle end-to-end 1256 + 1257 + Already partly wired in Task 9 (`IDM_STARTUP` calls `Startup::Set`). This task confirms the menu reflects real state and behaves when unavailable. 1258 + 1259 + **Files:** 1260 + - Modify: `App.cpp` (no-op if already correct; verify check/gray logic) 1261 + 1262 + **Interfaces:** 1263 + - Consumes: `Startup::Get/Set`. 1264 + - Produces: menu item checked when enabled, grayed when unavailable. 1265 + 1266 + - [ ] **Step 1: Confirm the menu-building call passes real state** 1267 + 1268 + In `App.cpp` `OwnerWndProc`, verify the `WM_RBUTTONUP` branch calls: 1269 + ```cpp 1270 + Startup::State s = Startup::Get(); 1271 + g_tray.ShowContextMenu(hWnd, s == Startup::State::Enabled, 1272 + s != Startup::State::Unavailable); 1273 + ``` 1274 + (Present since Task 9 — leave as-is.) 1275 + 1276 + - [ ] **Step 2: Guard `IDM_STARTUP` against the unavailable case** 1277 + 1278 + Update the `IDM_STARTUP` handler: 1279 + ```cpp 1280 + case IDM_STARTUP: { 1281 + Startup::State s = Startup::Get(); 1282 + if (s != Startup::State::Unavailable) 1283 + Startup::Set(s != Startup::State::Enabled); 1284 + return 0; 1285 + } 1286 + ``` 1287 + 1288 + - [ ] **Step 3: Build** 1289 + 1290 + Run: 1291 + ``` 1292 + msbuild TaskbarCalculator.sln /p:Configuration=Debug /p:Platform=x64 /t:Build 1293 + ``` 1294 + Expected: build succeeds. Unpackaged, the item is grayed (verified fully in Task 13 once packaged). 1295 + 1296 + - [ ] **Step 4: Commit** 1297 + 1298 + ```bash 1299 + git add App.cpp 1300 + git commit -m "feat: reflect and guard start-at-login state in the tray menu" 1301 + ``` 1302 + 1303 + --- 1304 + 1305 + ### Task 13: MSIX packaging with startup task 1306 + 1307 + **Files:** 1308 + - Create: `Packaging/AppxManifest.xml` 1309 + - Create: `Packaging/MakeCert.ps1` 1310 + - Create: `Packaging/BuildMsix.ps1` 1311 + - Create: `Packaging/Assets/README.md` (icon-asset requirements note) 1312 + 1313 + **Interfaces:** 1314 + - Consumes: built `x64\Release\TaskbarCalculator.exe`; StartupTask id `TaskbarCalculatorStartup` (Task 8). 1315 + - Produces: an installable `TaskbarCalculator.msix`, self-signed; enables the working `Start at login` toggle. 1316 + 1317 + - [ ] **Step 1: Author the package manifest** 1318 + 1319 + Create `Packaging/AppxManifest.xml`: 1320 + ```xml 1321 + <?xml version="1.0" encoding="utf-8"?> 1322 + <Package 1323 + xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" 1324 + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" 1325 + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" 1326 + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"> 1327 + 1328 + <Identity Name="TaskbarCalculator" 1329 + Publisher="CN=TaskbarCalculatorDev" 1330 + Version="1.0.0.0" 1331 + ProcessorArchitecture="x64" /> 1332 + 1333 + <Properties> 1334 + <DisplayName>Taskbar Calculator</DisplayName> 1335 + <PublisherDisplayName>Marco Maroni</PublisherDisplayName> 1336 + <Logo>Assets\StoreLogo.png</Logo> 1337 + </Properties> 1338 + 1339 + <Dependencies> 1340 + <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" 1341 + MaxVersionTested="10.0.22621.0" /> 1342 + </Dependencies> 1343 + 1344 + <Resources><Resource Language="en-us" /></Resources> 1345 + 1346 + <Capabilities> 1347 + <rescap:Capability Name="runFullTrust" /> 1348 + </Capabilities> 1349 + 1350 + <Applications> 1351 + <Application Id="TaskbarCalculator" 1352 + Executable="TaskbarCalculator.exe" 1353 + EntryPoint="Windows.FullTrustApplication"> 1354 + <uap:VisualElements 1355 + DisplayName="Taskbar Calculator" 1356 + Description="Always-on-top tray calculator" 1357 + Square150x150Logo="Assets\Square150x150Logo.png" 1358 + Square44x44Logo="Assets\Square44x44Logo.png" 1359 + BackgroundColor="transparent" /> 1360 + <Extensions> 1361 + <desktop:Extension Category="windows.startupTask" 1362 + Executable="TaskbarCalculator.exe" 1363 + EntryPoint="Windows.FullTrustApplication"> 1364 + <desktop:StartupTask TaskId="TaskbarCalculatorStartup" 1365 + Enabled="false" 1366 + DisplayName="Taskbar Calculator" /> 1367 + </desktop:Extension> 1368 + </Extensions> 1369 + </Application> 1370 + </Applications> 1371 + </Package> 1372 + ``` 1373 + 1374 + - [ ] **Step 2: Note the required PNG assets** 1375 + 1376 + Create `Packaging/Assets/README.md`: 1377 + ```markdown 1378 + Provide these PNG assets (referenced by AppxManifest.xml): 1379 + - StoreLogo.png (50x50) 1380 + - Square150x150Logo.png (150x150) 1381 + - Square44x44Logo.png (44x44) 1382 + 1383 + Generate them from icon1.ico (e.g. with any image editor or ImageMagick: 1384 + `magick icon1.ico -resize 150x150 Square150x150Logo.png`). Placeholder solid-color 1385 + PNGs are acceptable for local testing. 1386 + ``` 1387 + Create the three PNGs (any valid PNGs of the right sizes) in `Packaging/Assets/`. 1388 + 1389 + - [ ] **Step 3: Self-signed certificate script** 1390 + 1391 + Create `Packaging/MakeCert.ps1`: 1392 + ```powershell 1393 + # Creates a self-signed cert whose subject matches the manifest Publisher. 1394 + $subject = "CN=TaskbarCalculatorDev" 1395 + $cert = New-SelfSignedCertificate -Type Custom -Subject $subject ` 1396 + -KeyUsage DigitalSignature -FriendlyName "TaskbarCalculator Dev" ` 1397 + -CertStoreLocation "Cert:\CurrentUser\My" ` 1398 + -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") 1399 + $pwd = ConvertTo-SecureString -String "TaskbarCalc!" -Force -AsPlainText 1400 + Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" ` 1401 + -FilePath "$PSScriptRoot\TaskbarCalculator.pfx" -Password $pwd | Out-Null 1402 + Write-Host "PFX written. To trust it, run (elevated):" 1403 + Write-Host " Import-PfxCertificate -FilePath '$PSScriptRoot\TaskbarCalculator.pfx' -CertStoreLocation Cert:\LocalMachine\TrustedPeople -Password (ConvertTo-SecureString 'TaskbarCalc!' -Force -AsPlainText)" 1404 + ``` 1405 + 1406 + - [ ] **Step 4: Package + sign script** 1407 + 1408 + Create `Packaging/BuildMsix.ps1`: 1409 + ```powershell 1410 + param([string]$Config = "Release") 1411 + $ErrorActionPreference = "Stop" 1412 + $root = Split-Path $PSScriptRoot -Parent 1413 + $exeDir = Join-Path $root "x64\$Config" 1414 + $layout = Join-Path $PSScriptRoot "layout" 1415 + 1416 + # Locate SDK tools 1417 + $makeappx = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter makeappx.exe | 1418 + Where-Object FullName -like "*x64*" | Select-Object -Last 1).FullName 1419 + $signtool = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe | 1420 + Where-Object FullName -like "*x64*" | Select-Object -Last 1).FullName 1421 + 1422 + # Stage layout 1423 + if (Test-Path $layout) { Remove-Item $layout -Recurse -Force } 1424 + New-Item -ItemType Directory -Path $layout | Out-Null 1425 + Copy-Item "$PSScriptRoot\AppxManifest.xml" $layout 1426 + Copy-Item "$PSScriptRoot\Assets" $layout -Recurse 1427 + Copy-Item "$exeDir\TaskbarCalculator.exe" $layout 1428 + 1429 + # Pack + sign 1430 + & $makeappx pack /d $layout /p "$PSScriptRoot\TaskbarCalculator.msix" /overwrite 1431 + & $signtool sign /fd SHA256 /a /f "$PSScriptRoot\TaskbarCalculator.pfx" ` 1432 + /p "TaskbarCalc!" "$PSScriptRoot\TaskbarCalculator.msix" 1433 + Write-Host "Built $PSScriptRoot\TaskbarCalculator.msix" 1434 + ``` 1435 + 1436 + - [ ] **Step 5: Build Release, create cert, package** 1437 + 1438 + Run (x64 Native Tools Command Prompt, or PowerShell for the .ps1 parts): 1439 + ``` 1440 + msbuild TaskbarCalculator.sln /p:Configuration=Release /p:Platform=x64 /t:Rebuild 1441 + powershell -ExecutionPolicy Bypass -File Packaging\MakeCert.ps1 1442 + powershell -ExecutionPolicy Bypass -File Packaging\BuildMsix.ps1 1443 + ``` 1444 + Expected: `TaskbarCalculator.msix` is produced and signed. 1445 + 1446 + - [ ] **Step 6: Trust the cert and install** 1447 + 1448 + Run the elevated `Import-PfxCertificate` line printed by `MakeCert.ps1`, then: 1449 + ``` 1450 + Add-AppxPackage -Path Packaging\TaskbarCalculator.msix 1451 + ``` 1452 + Expected: the app installs, appears in Start, launches to the tray. Right-click → *Start at login* is now **enabled and checkable** (no longer grayed). Toggle it on, then check: 1453 + ``` 1454 + Get-StartApps | Where-Object Name -like "*Calculator*" 1455 + ``` 1456 + and verify the entry appears under Settings → Apps → Startup. 1457 + 1458 + - [ ] **Step 7: Commit** 1459 + 1460 + ```bash 1461 + git add Packaging/AppxManifest.xml Packaging/MakeCert.ps1 Packaging/BuildMsix.ps1 Packaging/Assets 1462 + git commit -m "feat: MSIX packaging with self-signed cert and startup task" 1463 + ``` 1464 + > `Packaging/layout/`, `*.pfx`, and `*.msix` are ignored by `.gitignore` (added in the git-setup commit) — do not commit build artifacts or the private key. 1465 + 1466 + --- 1467 + 1468 + ### Task 14: Manual verification pass + docs finalize 1469 + 1470 + **Files:** 1471 + - Create: `docs/superpowers/VERIFICATION.md` 1472 + - Modify: `CLAUDE.md` 1473 + 1474 + **Interfaces:** 1475 + - Consumes: the installed MSIX build. 1476 + - Produces: a recorded verification checklist and updated repository guidance. 1477 + 1478 + - [ ] **Step 1: Run the manual verification checklist** 1479 + 1480 + Create `docs/superpowers/VERIFICATION.md` and record PASS/FAIL for each: 1481 + ```markdown 1482 + # Manual Verification — Windows 11 Tray Calculator 1483 + 1484 + - [ ] Tray icon appears on launch 1485 + - [ ] Left-click toggles the window show/hide 1486 + - [ ] Window is always-on-top: stays visible above another focused app 1487 + - [ ] Keyboard input works: 2 + 3 = 5; div-by-zero shows error; sqrt/percent/1x/±/000 1488 + - [ ] Regional decimal/thousands separators respected (change in Control Panel, retype) 1489 + - [ ] Drag moves the window; position remembered after exit+relaunch 1490 + - [ ] First run anchors bottom-right above the tray 1491 + - [ ] "X" hides to tray (does not exit) 1492 + - [ ] Start-at-login toggle enables/disables (packaged); grayed when unpackaged 1493 + - [ ] Tray icon returns after restarting Windows Explorer 1494 + - [ ] Second launch does not create a duplicate icon (single instance) 1495 + - [ ] Exit removes the tray icon and quits 1496 + ``` 1497 + 1498 + - [ ] **Step 2: Update `CLAUDE.md` to describe the new architecture** 1499 + 1500 + Replace the DeskBand-centric sections of `CLAUDE.md` with the tray-app reality: entry point `wWinMain` in `App.cpp`; components `TrayIcon`, `CalculatorWindow` (standalone always-on-top), `calc` (decimal-only), `Settings`, `StartupManager`; build with `msbuild` (Application/.exe); tests via `tests\build-and-run.cmd`; packaging via `Packaging\BuildMsix.ps1`. Remove the "register with regsvr32" instructions and the DeskBand/COM description. 1501 + 1502 + - [ ] **Step 3: Commit** 1503 + 1504 + ```bash 1505 + git add docs/superpowers/VERIFICATION.md CLAUDE.md 1506 + git commit -m "docs: verification checklist and updated CLAUDE.md for the tray app" 1507 + ``` 1508 + 1509 + - [ ] **Step 4: Final review of the branch** 1510 + 1511 + Run: 1512 + ```bash 1513 + git log --oneline main..feature/win11-tray-calculator 1514 + ``` 1515 + Expected: the full task sequence, each an independently reviewable commit. The branch is ready for a future `git remote add` + push. 1516 + 1517 + --- 1518 + 1519 + ## Self-Review 1520 + 1521 + **Spec coverage:** 1522 + - Standalone `.exe` (from DLL) → Task 6. ✅ 1523 + - Tray icon + context menu + Show/Hide/About/Exit → Task 9. ✅ 1524 + - Persistent always-on-top, no auto-hide, hide-to-tray on "X" → Task 10. ✅ 1525 + - First-run anchor above tray + draggable + remembered position → Task 11. ✅ 1526 + - Keyboard-only input reuse (`OnEditKeyDown`/`OnEditChar`/`FormatString`/`ReadRegionalSettings`) → Task 10. ✅ 1527 + - Features kept minus Lire/Euro, minus hex (decimal-only) → Task 5. ✅ 1528 + - Start-at-login toggle via MSIX StartupTask + unpackaged fallback → Tasks 8, 12, 13. ✅ 1529 + - MSIX + self-signed cert → Task 13. ✅ 1530 + - Single-instance + Explorer-restart resilience → Tasks 9, 11. ✅ 1531 + - Engine unit tests (TDD) → Tasks 2–5. ✅ 1532 + - Manual verification checklist → Task 14. ✅ 1533 + - Git baseline/branch → done pre-plan; final review Task 14. ✅ 1534 + 1535 + **Placeholder scan:** No "TBD"/"implement later". The only intentional runtime placeholder is `ToggleCalculator()` empty in Task 9, explicitly filled in Task 10 (noted in-line). 1536 + 1537 + **Type consistency:** `Startup::State`/`Get`/`Set`, `Settings::LoadWindowPos`/`SaveWindowPos`, `TrayIcon::Add`/`Remove`/`ShowContextMenu`, `CCalculatorWindow::CreateStandalone`/`ToggleVisible`/`IsShown`/`PlaceInitial`, menu ids `IDM_SHOWHIDE/STARTUP/ABOUT/EXIT`, `WM_TRAY_CALLBACK`, `TaskId "TaskbarCalculatorStartup"` — used consistently across tasks.
+18 -9
docs/superpowers/specs/2026-07-14-win11-tray-calculator-design.md
··· 53 53 body is also draggable. 54 54 - **Input:** **keyboard only**, reusing the existing `EDIT` control and 55 55 `OnEditKeyDown` / `OnEditChar` translation. No clickable button pad. 56 - - **Calculator features:** keep **all existing functions except Lire↔Euro** — i.e. four 57 - operations, square root, percent, sign inversion, the `000` key, and the 58 - decimal/hexadecimal base toggle. Remove the Lire↔Euro conversion (`CALC_EURO`, `CALC_L`, 59 - and `calc::changeEuroLire`, fixed rate `1936.27`). 56 + - **Calculator features:** keep the working functions — four operations, square root, 57 + percent, sign inversion, reciprocal (1/x), the `000` key, clear/reset, and backspace. 58 + Remove the Lire↔Euro conversion (`CALC_EURO`, `CALC_L`, and `calc::changeEuroLire`, fixed 59 + rate `1936.27`). Also **remove the decimal/hexadecimal base support entirely**: it is 60 + currently **dead code** — every `case CALC_X: changeBase(...)` is commented out and the 61 + `hex` base is never entered — so `changeBase`, `toDecimal`, `restoreDecimalBase`, the 62 + `base`/`m_currentBase`/`m_lastDecimalNumber` members, the `CALC_A`–`CALC_F` hex digits, 63 + and the `CALC_X` key are all dropped. Result: a decimal-only calculator. 60 64 - **Start at login:** yes, **toggleable** from the tray menu, via the MSIX 61 65 `windows.startupTask` extension (default: disabled). 62 66 - **Packaging:** MSIX; **self-signed** certificate for local use. ··· 89 93 `OnEditKeyDown` / `OnEditChar`, `FormatString`, and `ReadRegionalSettings` nearly 90 94 verbatim. *Depends on:* `calc`, `CVisualStyle`, Settings. 91 95 92 - 4. **`calc`** — engine unchanged **except Lire/Euro removed**. 96 + 4. **`calc`** — engine cleaned up: **Lire/Euro removed** and the **dead hex/base code 97 + removed** (`changeBase`, `toDecimal`, `restoreDecimalBase`, base members, `CALC_A`–`F`, 98 + `CALC_X`). `m_precision` is initialized to `2` in the constructor (currently 99 + uninitialized until `set_precision`). Decimal-only. 93 100 94 101 5. **`CVisualStyle`** — theming/font, reused as-is. 95 102 ··· 171 178 ## Testing 172 179 173 180 - **Unit tests for the `calc` engine** (pure, UI-independent → highest value): sequences 174 - such as `2+3=` → `5`, division by zero, `√`, percent, sign inversion, decimal/hex toggle, 175 - and precision/formatting. A small separate test `.exe` (lightweight header-only 176 - framework). The repository currently has no tests; they are introduced here. Implement 177 - this part with **TDD**. 181 + such as `2+3=` → `5`, division by zero, `√`, percent, sign inversion, reciprocal (1/x), 182 + the `000` key, and reset. A small separate test `.exe` (lightweight header-only 183 + framework), built directly with `cl.exe`. The repository currently has no tests; they are 184 + introduced here. Implement this part with **TDD**. The engine is decoupled from the 185 + precompiled header so it compiles standalone. 178 186 - **Manual verification checklist** for tray/window: show/hide from the tray, always-on-top 179 187 *persisting* above another active application, drag + remembered position across 180 188 sessions, start-at-login toggle, tray icon re-appearing after an Explorer restart, and ··· 195 203 196 204 - Clickable button pad (keyboard-only by decision). 197 205 - Lire↔Euro conversion (removed). 206 + - Hexadecimal mode (removed as dead code; decimal-only). 198 207 - Production code signing / Store submission. 199 208 - Connecting or pushing to the remote repository. 200 209 - Multi-monitor anchor heuristics beyond "primary work area" (drag + remembered position