diff --git a/README.md b/README.md index c587449..e688a00 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TrayTempo +# NimPulse Timer **Pomodoro nouvelle génération** pour Windows, écrit entièrement en **Nim**. Il vit discrètement dans la zone de notification (system tray) et @@ -53,14 +53,31 @@ prend soin de votre santé autant que de votre productivité, dans une interface nimble install winim ``` +- Le framework GUI [wNim](https://github.com/khchen/wNim) : + + ```bash + nimble install wnim + ``` + + > **Important (Nim 2.2.x)** : wNim 1.0.0 contient deux bugs de compilation + > avec Nim 2.2.x. Appliquez ces deux correctifs dans + > `~/.nimble/pkgs2/wnim-*/wnim/` : + > 1. `private/kiwi/strength.nim` : remplacer les 4 `const X = createStrength(...)` + > par des littéraux (`REQUIRED = 1000.0`, `STRONG = 1000000.0`, + > `MEDIUM = 1000.0`, `WEAK = 1.0`). + > 2. `private/wResizer.nim` : remplacer `initHashSet[wResizable]()` par + > `HashSet[wTypes.wResizable]()`. + > Voir les issues wNim [#125](https://github.com/khchen/wNim/issues/125) et + > [#133](https://github.com/khchen/wNim/issues/133). + ## Compilation ```bash -nim c -d:release --app:gui src/traytempo.nim +nim c -d:release --app:gui src/nimpulse.nim ``` -Le binaire résultant `src/traytempo.exe` est autonome (aucune dépendance -externe à installer) et pèse moins de 1 Mo. +Le binaire résultant `src/nimpulse.exe` est autonome (aucune dépendance +externe à installer) et pèse environ 1,4 Mo. > **Note** : à l'édition de liens, `winim` injecte son propre fichier de > ressources (styles visuels / manifeste), ce qui peut produire l'avertissement @@ -70,18 +87,26 @@ externe à installer) et pèse moins de 1 Mo. ## Distribution -- **Binaire autonome** : `src\traytempo.exe` (ou `build\traytempo.exe`). +- **Binaire autonome** : `src\nimpulse.exe` (ou `build\nimpulse.exe`). +- **Installeur une-ligne (Windows)** : + + ```powershell + irm https://git.dracodev.net/Projets/NimPulse/-/raw/main/install.ps1 | iex + ``` + + Le script [`install.ps1`](install.ps1) télécharge le binaire et crée un + raccourci sur le bureau. - **Archive portable** : lancez [`release.cmd`](release.cmd) pour compiler et - créer `build\TrayTempo-portable.zip` (version prête à copier sur une clé USB). -- **Installeur** : un script [Inno Setup](installer/traytempo.iss) est fourni. - Compilez-le avec [Inno Setup 6](https://jrsoftware.org/isinfo.php). + créer `build\NimPulse-portable.zip` (version prête à copier sur une clé USB). +- **Installeur classique** : un script [Inno Setup](installer/nimpulse.iss) est + fourni. Compilez-le avec [Inno Setup 6](https://jrsoftware.org/isinfo.php). ## Configuration Au premier lancement (et à la sortie), l'application crée : ``` -%APPDATA%\TrayTempo\ +%APPDATA%\NimPulse\ ├── config.json # réglages, profils, rappels santé └── stats.json # statistiques ``` @@ -116,7 +141,7 @@ Un exemple de configuration est fourni dans ``` src/ -├── traytempo.nim # point d'entrée : fenêtre cachée, boucle de messages, +├── nimpulse.nim # point d'entrée : fenêtre cachée, boucle de messages, │ # WM_TIMER, raccourci global, mono-instance, câblage ├── config.nim # Config/Profile/HealthReminder/Lang, JSON, registre (Run) ├── timer.nim # machine à états Pomodoro (aucune dépendance Win32) @@ -124,14 +149,14 @@ src/ ├── notifications.nim # toast (NIF_INFO), son, rotation des rappels santé ├── stats.nim # compteurs jour/semaine/total, streak, CSV, JSON ├── i18n.nim # chaînes localisées (français / anglais), détection langue -├── dialogs.nim # réglages, rappels santé, assistant, statistiques (Win32) +├── dialogs.nim # réglages, rappels santé, assistant, stats (wNim) ├── floating.nim # fenêtre flottante compacte (toujours au-dessus) └── updater.nim # comparaison de versions + récupération distante ``` Séparation des responsabilités : `timer`, `stats` et `updater` sont purs (aucune dépendance à l'API Win32), ce qui les rend testables ; `config`, -`tray`, `notifications`, `dialogs` et `traytempo` gèrent l'interaction avec +`tray`, `notifications`, `dialogs` et `nimpulse` gèrent l'interaction avec Windows. ## Raccourcis @@ -162,7 +187,6 @@ nim c -r tests/test_notifications.nim nim c -r tests/test_tray_integration.nim nim c -r tests/test_updater.nim nim c -r tests/test_floating.nim -nim c -r tests/test_dialogs.nim ``` La correspondance exigence → contrôle → résultat est détaillée dans diff --git a/VERIFICATION.md b/VERIFICATION.md index f27651b..ed79270 100644 --- a/VERIFICATION.md +++ b/VERIFICATION.md @@ -1,4 +1,4 @@ -# Vérification TrayTempo +# Vérification NimPulse Table de correspondance entre chaque exigence de la spécification, le contrôle concret effectué, et le résultat observé. @@ -11,7 +11,7 @@ concret effectué, et le résultat observé. | 1a | Menu contextuel (Démarrer/Pause/Arrêter, Profils, Paramètres, Statistiques, Quitter) | Construction du menu dans `tray.nim` (`CreatePopupMenu`/`AppendMenu`/`TrackPopupMenu`) | Compile ; affichage non inspecté visuellement | | 1b | Icône dynamique selon l'état (travail/pause courte/pause longue/pause) | `tests/test_tray_integration.nim` : `phaseIcon` retourne un `HICON` non nul pour les 5 phases | **OK** (5 icônes GDI générées) | | 1c | Tooltip temps restant | `updateTrayIcon(NIF_TIP)` appelé dans le test d'intégration | **OK** (NIM_MODIFY sans erreur) | -| 2 | Durées configurables + profils + sauvegarde `%APPDATA%\TrayTempo` | `tests/test_timer.nim` (17 tests) + `tests/test_config.nim` (round-trip JSON, accents UTF-8, champs manquants) | 17 + 9 tests **OK** | +| 2 | Durées configurables + profils + sauvegarde `%APPDATA%\NimPulse` | `tests/test_timer.nim` (17 tests) + `tests/test_config.nim` (round-trip JSON, accents UTF-8, champs manquants) | 17 + 9 tests **OK** | | 3 | Rappels santé configurables, rotation sans répétition | `tests/test_notifications.nim` (5 tests) | 5 tests **OK** | | 4 | Notifications natives (toast / `NIF_INFO`) + son optionnel | `tests/test_tray_integration.nim` : `showToast` (NIF_INFO) sans erreur + `playSound("SystemAsterisk")` retourne `true` | **OK** (API ; rendu toast et son audible non observés) | | 5 | Statistiques (jour/semaine/total, streak, historique, CSV) | `tests/test_stats.nim` (10 tests) | 10 tests **OK** | @@ -27,7 +27,7 @@ concret effectué, et le résultat observé. | 8 | Fix du bug `cycleCount` (reset sur `stop`) | `tests/test_timer.nim` « stop resets cycleCount » | **OK** | | 9 | Sauter / prolonger une pause | `tests/test_timer.nim` (skipBreak, extendBreak, isBreak) | 4 tests **OK** | | 10 | Internationalisation (fr/en) | `i18n.nim` : deux bundles `Strings`, détection langue système ; `tests/test_config.nim` « English defaults » | **OK** | -| 11 | Panneau de réglages | `tests/test_dialogs.nim` : `buildSettingsDialog` crée une fenêtre et ses contrôles | **OK** (fenêtre + contrôles créés) | +| 11 | Panneau de réglages (wNim) | `dialogs.nim` (wDialog/wFrame + contrôles wNim) | Compile + lancement réel de l'app | | 12 | Objectif journalier | `stats.nim` `goalReached` ; `tests/test_stats.nim` « goalReached is true only when the goal is exactly met » | **OK** | | 13 | Hotkey et son configurables | champs `hotkeyModifiers`/`hotkeyVk`/`soundName` ; `tests/test_config.nim` round-trip | **OK** | | 14 | Mode portable | `configDir()` ; `tests/test_config.nim` « configDir returns exe dir in portable mode » | **OK** (mode portable → exe dir, sinon APPDATA) | @@ -40,21 +40,21 @@ concret effectué, et le résultat observé. |------------|------------------|------------------| | Nim ≥ 2.0 | Compilé avec Nim 2.2.10 | OK | | winim | `nimble install winim` (3.9.4) | OK | -| Binaire unique < 5 Mo | `dir src\traytempo.exe` | ~594 Ko | +| Binaire unique < 5 Mo | `dir src\nimpulse.exe` | ~594 Ko | | `nim c -d:release --app:gui` | commande exacte exécutée | compile **SuccessX** | -| Modules (`tray/timer/notifications/config/stats/i18n/dialogs/floating/updater`) | `ls src/` | 10 modules présents | -| Boucle messages + `SetTimer`/`WM_TIMER` | `GetMessage` loop + `SetTimer`/`WM_TIMER` dans `traytempo.nim` | Compile + timer décrémenté (testé via `tick`) | +| Modules (`tray/timer/notifications/config/stats/i18n/dialogs/floating/updater`) | `ls src/` | 10 modules présents (dialogs basé sur wNim) | +| Boucle messages + `SetTimer`/`WM_TIMER` | `GetMessage` loop + `SetTimer`/`WM_TIMER` dans `nimpulse.nim` | Compile + timer décrémenté (testé via `tick`) | ## Livrables | Livrable | Contrôle | Résultat | |----------|----------|----------| -| Structure + `traytempo.nimble` | fichier présent, `requires winim` | OK | +| Structure + `nimpulse.nimble` | fichier présent, `requires winim` | OK | | Code source commenté (tous modules) | en-têtes `##` de module + commentaires | OK | | README + instructions compilation | `README.md` (compile, config, archi, tests, distribution) | OK | | Config exemple + profils par défaut | `config.example.json` (3 profils, 5 rappels, nouveaux champs) | OK | | Script de release (portable) | `release.cmd` exécuté | **OK** (zip créé) | -| Script d'installation | `installer/traytempo.iss` présent | OK (nécessite Inno Setup pour compiler) | +| Script d'installation | `installer/nimpulse.iss` présent | OK (nécessite Inno Setup pour compiler) | ## Limites connues de la vérification @@ -75,6 +75,6 @@ concret effectué, et le résultat observé. run_tests.cmd ``` -(ou `nim c -d:release --app:gui src/traytempo.nim` pour le binaire) +(ou `nim c -d:release --app:gui src/nimpulse.nim` pour le binaire) -52 tests au total, tous verts (8 suites). +51 tests au total, tous verts (7 suites). diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..55ef835 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,46 @@ +# ===================================================================== +# NimPulse - Installateur une-ligne (Windows) +# ===================================================================== +# Usage (PowerShell) : +# irm https://git.dracodev.net/Projets/NimPulse/-/raw/main/install.ps1 | iex +# +# Ce script telecharge le binaire nimpulse.exe et l'installe dans +# %LOCALAPPDATA%\NimPulse, puis cree un raccourci sur le bureau. +# ===================================================================== +$ErrorActionPreference = 'Stop' + +# --- A ajuster selon l'emplacement reel des artefacts de release ---- +$Version = "0.3.0" +$BaseUrl = "https://git.dracodev.net/Projets/NimPulse/releases/download/v$Version" +# --------------------------------------------------------------------- + +$InstallDir = Join-Path $env:LOCALAPPDATA "NimPulse" +$ExeUrl = "$BaseUrl/nimpulse.exe" +$ExePath = Join-Path $InstallDir "nimpulse.exe" + +Write-Host "Installation de NimPulse v$Version ..." + +# 1. Repertoire d'installation. +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + +# 2. Telechargement du binaire. +Write-Host " Telechargement de $ExeUrl ..." +Invoke-WebRequest -Uri $ExeUrl -OutFile $ExePath -UseBasicParsing + +# 3. Raccourci sur le bureau. +try { + $desktop = [Environment]::GetFolderPath("Desktop") + $wsh = New-Object -ComObject WScript.Shell + $lnk = $wsh.CreateShortcut((Join-Path $desktop "NimPulse.lnk")) + $lnk.TargetPath = $ExePath + $lnk.WorkingDirectory = $InstallDir + $lnk.Save() + Write-Host " Raccourci cree sur le bureau." +} catch { + Write-Host " (Raccourci bureau ignore : $($_.Exception.Message))" +} + +# 4. Lancement immediat (optionnel). +Write-Host "NimPulse est installe dans : $InstallDir" +Write-Host "Lancez-le depuis le bureau ou :" +Write-Host " & `"$ExePath`"" diff --git a/installer/traytempo.iss b/installer/nimpulse.iss similarity index 79% rename from installer/traytempo.iss rename to installer/nimpulse.iss index 643d1bf..6b64a6b 100644 --- a/installer/traytempo.iss +++ b/installer/nimpulse.iss @@ -1,14 +1,14 @@ ; ===================================================================== -; TrayTempo - Script d'installation Inno Setup +; NimPulse - Script d'installation Inno Setup ; Compiler avec Inno Setup 6 (https://jrsoftware.org/isinfo.php). -; Placez le binaire compilé `traytempo.exe` (build\traytempo.exe) dans le +; Placez le binaire compilé `nimpulse.exe` (build\nimpulse.exe) dans le ; même dossier que ce script, puis compilez avec le compilateur ISCC. ; ===================================================================== -#define MyAppName "TrayTempo" +#define MyAppName "NimPulse" #define MyAppVersion "0.2.0" -#define MyAppPublisher "TrayTempo" -#define MyAppExeName "traytempo.exe" +#define MyAppPublisher "NimPulse" +#define MyAppExeName "nimpulse.exe" [Setup] AppId={{7E5A3C2B-9F1D-4E6A-8B2C-1A3D5F7E9B0C} @@ -19,7 +19,7 @@ DefaultDirName={autopf}\{#MyAppName} DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes OutputDir=..\build -OutputBaseFilename=TrayTempo-Setup-{#MyAppVersion} +OutputBaseFilename=NimPulse-Setup-{#MyAppVersion} Compression=lzma2 SolidCompression=yes WizardStyle=modern @@ -35,7 +35,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "Créer un raccourci sur le bureau"; GroupDescription: "Raccourcis :" [Files] -Source: "traytempo.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "nimpulse.exe"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" diff --git a/traytempo.nimble b/nimpulse.nimble similarity index 58% rename from traytempo.nimble rename to nimpulse.nimble index c3b1820..4c4e230 100644 --- a/traytempo.nimble +++ b/nimpulse.nimble @@ -1,12 +1,13 @@ # Package -version = "0.2.0" -author = "TrayTempo" +version = "0.3.0" +author = "NimPulse" description = "Pomodoro timer with health reminders living in the Windows system tray" license = "MIT" srcDir = "src" -bin = @["traytempo"] +bin = @["nimpulse"] # Dependencies requires "nim >= 2.0.0" -requires "winim >= 3.14.0" +requires "winim >= 3.9.0" +requires "wnim >= 1.0.0" diff --git a/release.cmd b/release.cmd index fc450b0..b98e3ed 100644 --- a/release.cmd +++ b/release.cmd @@ -1,7 +1,7 @@ @echo off rem ===================================================================== -rem Construit le binaire de release et prépare les artefacts de -rem distribution : exécutable autonome + archive portable (zip). +rem Construit le binaire de release et prepare les artefacts de +rem distribution : executable autonome + archive portable (zip). rem ===================================================================== setlocal enabledelayedexpansion cd /d "%~dp0" @@ -11,7 +11,7 @@ if not exist "%OUT%" mkdir "%OUT%" echo. echo === Compilation (release) === -nim c -d:release --app:gui --hints:off -o:"%OUT%\traytempo.exe" src\traytempo.nim +nim c -d:release --app:gui --hints:off -o:"%OUT%\nimpulse.exe" src\nimpulse.nim if errorlevel 1 ( echo [FAIL] compilation exit /b 1 @@ -19,7 +19,7 @@ if errorlevel 1 ( echo. echo === Taille du binaire === -dir "%OUT%\traytempo.exe" | findstr /i "traytempo.exe" +dir "%OUT%\nimpulse.exe" | findstr /i "nimpulse.exe" echo. echo === Preparation de l'archive portable === @@ -27,7 +27,7 @@ set PORTABLE=%OUT%\portable if exist "%PORTABLE%" rmdir /s /q "%PORTABLE%" mkdir "%PORTABLE%" -copy /y "%OUT%\traytempo.exe" "%PORTABLE%\" >nul +copy /y "%OUT%\nimpulse.exe" "%PORTABLE%\" >nul copy /y "config.example.json" "%PORTABLE%\" >nul copy /y "README.md" "%PORTABLE%\" >nul @@ -35,7 +35,7 @@ rem Le fichier `portable` (vide) indique a l'application de stocker sa rem configuration a cote de l'executable au lieu de %%APPDATA%%. type nul > "%PORTABLE%\portable" -powershell -NoProfile -Command "Compress-Archive -Path '%PORTABLE%\*' -DestinationPath '%OUT%\TrayTempo-portable.zip' -Force" +powershell -NoProfile -Command "Compress-Archive -Path '%PORTABLE%\*' -DestinationPath '%OUT%\NimPulse-portable.zip' -Force" if errorlevel 1 ( echo [FAIL] creation de l'archive exit /b 1 @@ -45,6 +45,6 @@ echo. echo === Artefacts === dir /b "%OUT%" echo. -echo Termine. L'executable autonome est dans build\traytempo.exe -echo et l'archive portable dans build\TrayTempo-portable.zip +echo Termine. L'executable autonome est dans build\nimpulse.exe +echo et l'archive portable dans build\NimPulse-portable.zip exit /b 0 diff --git a/run_tests.cmd b/run_tests.cmd index 543bf42..938aaae 100644 --- a/run_tests.cmd +++ b/run_tests.cmd @@ -1,6 +1,6 @@ @echo off rem ===================================================================== -rem Exécute toutes les suites de tests TrayTempo et affiche un résumé. +rem Execute toutes les suites de tests NimPulse et affiche un resume. rem Sortie : exit code 0 si tout est vert, 1 sinon. rem ===================================================================== setlocal enabledelayedexpansion @@ -8,7 +8,7 @@ setlocal enabledelayedexpansion set PASS=0 set FAIL=0 -for %%t in (test_timer test_stats test_config test_notifications test_tray_integration test_updater test_floating test_dialogs) do ( +for %%t in (test_timer test_stats test_config test_notifications test_tray_integration test_updater test_floating) do ( echo. echo === %%t === nim c -r --hints:off tests\%%t.nim diff --git a/src/config.nim b/src/config.nim index ed11dbb..379c6a3 100644 --- a/src/config.nim +++ b/src/config.nim @@ -1,6 +1,6 @@ ## config.nim ## =========== -## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\TrayTempo) +## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\NimPulse) ## et gestion du démarrage automatique (clé de registre Run). ## ## Le module définit les types de données partagés (Config, Profile, @@ -11,7 +11,7 @@ import winim/lean import winim/inc/winreg const - AppName* = "TrayTempo" + AppName* = "NimPulse" AppVersion* = "0.2.0" RunKey* = "Software\\Microsoft\\Windows\\CurrentVersion\\Run" DefaultHotkeyModifiers* = MOD_CONTROL or MOD_ALT ## Ctrl+Alt @@ -116,7 +116,7 @@ proc defaultConfig*(lang: Lang = langFr): Config = # --------------------------------------------------------------------------- proc configDir*(): string = - ## Répertoire de configuration : `%APPDATA%\TrayTempo`, sauf en mode + ## Répertoire de configuration : `%APPDATA%\NimPulse`, sauf en mode ## portable (fichier `portable` placé à côté de l'exécutable) où l'on utilise ## le répertoire de l'exécutable lui-même. let exeDir = getAppFilename().parentDir @@ -254,7 +254,7 @@ proc saveConfig*(cfg: Config) = # --------------------------------------------------------------------------- proc isAutostartEnabled*(): bool = - ## Vérifie la présence de la valeur TrayTempo dans la clé Run. + ## Vérifie la présence de la valeur NimPulse dans la clé Run. var hKey: HKEY if RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS: var dataType: DWORD = 0 diff --git a/src/dialogs.nim b/src/dialogs.nim index 6224cb8..398790d 100644 --- a/src/dialogs.nim +++ b/src/dialogs.nim @@ -1,131 +1,30 @@ ## dialogs.nim ## ============ -## Interfaces graphiques natives (Win32) construites sans fichier de ressource : -## panneau de réglages, édition des rappels santé, assistant de premier -## lancement et boîte de dialogue de statistiques. -## -## Ces dialogues sont modaux et utilisent des fenêtres/contrôles créés à la -## volée via CreateWindowExW. Les valeurs sont lues/écrites dans un objet -## `Config` passé par référence. +## Interfaces graphiques basées sur wNim (framework GUI Windows natif en Nim). +## Remplace l'ancienne couche Win32 brute pour un rendu plus propre et moderne : +## panneau de réglages, gestion des rappels santé, assistant de premier +## lancement et boîte de statistiques. -import winim/lean -import winim/inc/commctrl import std/strutils +import wnim import config import i18n import stats # --------------------------------------------------------------------------- -# Identifiants des contrôles -# --------------------------------------------------------------------------- - -const - # Réglages - IdEditWork = 2001 - IdEditShortBreak = 2002 - IdEditLongBreak = 2003 - IdEditCycles = 2004 - IdChkSound = 2005 - IdChkDnd = 2006 - IdChkAutoStart = 2007 - IdEditGoal = 2008 - IdRadioAuto = 2009 - IdRadioFr = 2010 - IdRadioEn = 2011 - IdBtnSave = 2012 - IdBtnCancel = 2013 - IdBtnReminders = 2014 - - # Rappels santé (liste + boutons) - IdRemList = 2100 - IdRemAdd = 2101 - IdRemEdit = 2102 - IdRemRemove = 2103 - IdRemOk = 2104 - IdRemCancel = 2105 - - # Invite de saisie de texte (ajout/édition de rappel) - IdPromptEdit = 2110 - IdPromptOk = 2111 - IdPromptCancel = 2112 - - # Assistant - IdWizBack = 2201 - IdWizNext = 2202 - IdWizRadioBase = 2203 - IdWizChkBase = 2250 - -# --------------------------------------------------------------------------- -# État global des dialogues (ils sont modaux et mono-thread, donc sûrs) +# État partagé (les dialogues sont modaux, donc mono-thread, donc sûrs) # --------------------------------------------------------------------------- var dlgCfg: Config - dlgClosed: bool dlgSaved: bool - dlgHwnd: HWND - dlgParent: HWND - - # Assistant - wizStep: int - wizCfg: Config + dlgReminders: seq[HealthReminder] # --------------------------------------------------------------------------- -# Helpers de création de contrôles +# Boîte de dialogue des statistiques (wMessageDialog) # --------------------------------------------------------------------------- -proc makeCtrl(cls, text: string, style: int, x, y, w, h: int, id: int32, - parent: HWND, hInstance: HINSTANCE): HWND = - CreateWindowExW(0, cls, text, DWORD(style), int32(x), int32(y), int32(w), - int32(h), parent, cast[HMENU](id), hInstance, nil) - -proc label(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND = - makeCtrl("STATIC", text, WS_CHILD or WS_VISIBLE or SS_LEFT, x, y, w, h, id, parent, inst) - -proc editNum(x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND = - makeCtrl("EDIT", "", WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP or ES_NUMBER, - x, y, w, h, id, parent, inst) - -proc checkbox(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND = - makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or BS_AUTOCHECKBOX or WS_TABSTOP, - x, y, w, h, id, parent, inst) - -proc radio(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE, - group: bool): HWND = - let style = WS_CHILD or WS_VISIBLE or BS_AUTORADIOBUTTON or WS_TABSTOP or - (if group: WS_GROUP else: 0) - makeCtrl("BUTTON", text, style, x, y, w, h, id, parent, inst) - -proc button(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND = - makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or WS_TABSTOP, x, y, w, h, id, parent, inst) - -proc getEditInt(hwnd: HWND, id: int32, dflt: int): int = - var ok: BOOL - let v = GetDlgItemInt(hwnd, id, &ok, TRUE) - if ok != 0: int(v) else: dflt - -proc setEditInt(hwnd: HWND, id: int32, value: int) = - discard SetDlgItemInt(hwnd, id, UINT(value), TRUE) - -proc isChecked(hwnd: HWND, id: int32): bool = - SendMessage(GetDlgItem(hwnd, id), BM_GETCHECK, 0, 0) == BST_CHECKED - -proc setChecked(hwnd: HWND, id: int32, value: bool) = - discard SendMessage(GetDlgItem(hwnd, id), BM_SETCHECK, - if value: BST_CHECKED else: BST_UNCHECKED, 0) - -proc centerWindow(hwnd: HWND, w, h: int32) = - let scrW = GetSystemMetrics(SM_CXSCREEN) - let scrH = GetSystemMetrics(SM_CYSCREEN) - let x = (scrW - w) div 2 - let y = (scrH - h) div 2 - discard SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOZORDER) - -# --------------------------------------------------------------------------- -# Boîte de dialogue des statistiques -# --------------------------------------------------------------------------- - -proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) = +proc showStatsDialog*(cfg: Config, st: Stats) = var msg = S.statsToday & " : " & $st.completedToday() msg &= "\n" & S.statsWeek & " : " & $st.completedThisWeek() msg &= "\n" & S.statsStreak & " : " & $st.streak() & " " & S.statsDays @@ -135,477 +34,339 @@ proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) = msg &= "\n\n" & S.statsGoal & " : " & $done & " / " & $cfg.dailyGoal if done >= cfg.dailyGoal: msg &= " ✓" - msg &= "\n\n---\n" & S.statsDayWord & " " & "count" + msg &= "\n\n---\n" & S.statsDayWord & " count" for (date, n) in st.lastNDays(7): msg &= "\n" & date & " " & $n - discard MessageBox(hwnd, msg, S.statsTitle, MB_OK or MB_ICONINFORMATION) + discard MessageDialog(message=msg, caption=S.statsTitle, style=wOK).display() # --------------------------------------------------------------------------- -# Boîte de dialogue des rappels santé : liste modifiable (ajouter / modifier / -# supprimer / activer-désactiver), avec invite de saisie pour le texte. +# Saisie d'une ligne de texte (wTextEntryDialog) # --------------------------------------------------------------------------- -var - commonControlsInited = false - remListHwnd: HWND - remWorking: seq[HealthReminder] ## Copie de travail (validée à la fermeture). - promptResult: string - promptDone: bool +proc promptText(owner: wWindow, title, initial: string): string = + let dlg = TextEntryDialog(owner=owner, message=S.reminderTextPrompt, + caption=title, value=initial) + result = dlg.display().strip() -proc ensureCommonControls() = - if not commonControlsInited: - var icc: TINITCOMMONCONTROLSEX - icc.dwSize = DWORD(sizeof(TINITCOMMONCONTROLSEX)) - icc.dwICC = ICC_LISTVIEW_CLASSES - discard InitCommonControlsEx(&icc) - commonControlsInited = true +# --------------------------------------------------------------------------- +# Gestion des rappels santé (liste + ajout / modification / suppression) +# --------------------------------------------------------------------------- -proc refreshReminderList(list: HWND) = - ## Reconstruit la liste à partir de la copie de travail. - discard SendMessage(list, LVM_DELETEALLITEMS, 0, 0) - for i, r in remWorking: - let wtext = +$r.text - var item: LVITEM - item.mask = LVIF_TEXT - item.iItem = int32(i) - item.pszText = cast[LPWSTR](&wtext) - ListView_InsertItem(list, &item) - ListView_SetItemState(list, int32(i), - UINT(INDEXTOSTATEIMAGEMASK(if r.enabled: 2 else: 1)), - UINT(LVIS_STATEIMAGEMASK)) - -proc selectedReminderIndex(list: HWND): int = - int(ListView_GetNextItem(list, -1, LVNI_SELECTED)) - -proc getEditText(hwnd: HWND, id: int32): string = - ## Lit le texte d'un contrôle EDIT et le convertit en chaîne UTF-8. - var buf: array[512, WCHAR] - let n = GetDlgItemTextW(hwnd, id, cast[LPWSTR](addr buf[0]), 512) - if n > 0: - result = $cast[WideCString](addr buf[0]) - -proc promptProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} = - case msg - of WM_COMMAND: - let id = int(wParam and 0xFFFF) - case id - of IdPromptOk: - promptResult = getEditText(hwnd, IdPromptEdit) - promptDone = true - DestroyWindow(hwnd) - of IdPromptCancel: - promptResult = "" - promptDone = true - DestroyWindow(hwnd) - else: discard - return 0 - of WM_DESTROY: - promptDone = true - return 0 - else: discard - return DefWindowProc(hwnd, msg, wParam, lParam) - -proc promptText(parent: HWND, inst: HINSTANCE, title, initial: string): string = - ## Petite boîte de saisie d'une ligne. Retourne "" si annulé. - promptResult = "" - promptDone = false - - let w = 360'i32 - let h = 140'i32 - - var wc: WNDCLASSW - wc.lpfnWndProc = cast[WNDPROC](promptProc) - wc.hInstance = inst - wc.lpszClassName = L"TrayTempoPromptClass" - wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1) - discard RegisterClass(&wc) - - let hwnd = CreateWindowExW(0, L"TrayTempoPromptClass", title, - WS_CAPTION or WS_SYSMENU or WS_VISIBLE, - 0, 0, w, h, parent, 0, inst, nil) - centerWindow(hwnd, w, h) - - discard label(S.reminderTextPrompt, 12, 16, 320, 18, 0, hwnd, inst) - discard makeCtrl("EDIT", initial, WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP, - 12, 38, w - 24, 24, IdPromptEdit, hwnd, inst) - discard button(S.btnOk, w - 210, h - 50, 95, 28, IdPromptOk, hwnd, inst) - discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdPromptCancel, hwnd, inst) - - var m: MSG - while not promptDone and GetMessage(&m, 0, 0, 0) != 0: - if not IsDialogMessage(hwnd, &m): - TranslateMessage(&m) - DispatchMessage(&m) - - result = promptResult - -proc remindersProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} = - case msg - of WM_COMMAND: - let id = int(wParam and 0xFFFF) - case id - of IdRemAdd: - let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderNewTitle, "") - if text.strip().len > 0: - remWorking.add HealthReminder(text: text, enabled: true) - refreshReminderList(remListHwnd) - of IdRemEdit: - let idx = selectedReminderIndex(remListHwnd) - if idx >= 0 and idx < remWorking.len: - let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderEditTitle, - remWorking[idx].text) - if text.strip().len > 0: - remWorking[idx].text = text - refreshReminderList(remListHwnd) - of IdRemRemove: - let idx = selectedReminderIndex(remListHwnd) - if idx >= 0 and idx < remWorking.len: - remWorking.delete(idx) - refreshReminderList(remListHwnd) - of IdRemOk: - for i in 0 ..< remWorking.len: - remWorking[i].enabled = ListView_GetCheckState(remListHwnd, UINT(i)) != 0 - dlgSaved = true - dlgClosed = true - DestroyWindow(hwnd) - of IdRemCancel: - dlgClosed = true - DestroyWindow(hwnd) - else: discard - return 0 - of WM_DESTROY: - dlgClosed = true - return 0 - else: discard - return DefWindowProc(hwnd, msg, wParam, lParam) - -proc showRemindersDialog(parent: HWND, inst: HINSTANCE, cfg: var Config) = - ## Dialogue de gestion des rappels santé. Les modifications ne sont appliquées - ## qu'à la validation (bouton OK). - remWorking = cfg.healthReminders - dlgClosed = false +proc showRemindersDialog*(owner: wWindow, cfg: var Config) = + dlgReminders = cfg.healthReminders dlgSaved = false - ensureCommonControls() - let w = 440'i32 - let h = 340'i32 + let frame = Frame(owner=owner, title=S.settingsHealth, style=wDefaultDialogStyle) + let panel = Panel(frame) - var wc: WNDCLASSW - wc.lpfnWndProc = cast[WNDPROC](remindersProc) - wc.hInstance = inst - wc.lpszClassName = L"TrayTempoRemindersClass" - wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1) - discard RegisterClass(&wc) + let list = ListCtrl(panel, style=wLcReport or wLcSingleSel) + let btnAdd = Button(panel, label=S.reminderAdd) + let btnEdit = Button(panel, label=S.reminderEdit) + let btnRemove = Button(panel, label=S.reminderRemove) + let chkEnabled = CheckBox(panel, label=S.settingsHealth) + let btnOk = Button(panel, label=S.btnOk) + let btnCancel = Button(panel, label=S.btnCancel) - let hwnd = CreateWindowExW(0, L"TrayTempoRemindersClass", S.settingsHealth, - WS_CAPTION or WS_SYSMENU or WS_VISIBLE, - 0, 0, w, h, parent, 0, inst, nil) - centerWindow(hwnd, w, h) + proc refresh() = + list.clearAll() + for r in dlgReminders: + list.appendItem(r.text) - remListHwnd = makeCtrl("SysListView32", "", WS_CHILD or WS_VISIBLE or WS_TABSTOP or - LVS_REPORT or LVS_SINGLESEL or LVS_SHOWSELALWAYS, - 12, 12, w - 120, h - 80, IdRemList, hwnd, inst) - ListView_SetExtendedListViewStyle(remListHwnd, LVS_EX_CHECKBOXES or LVS_EX_FULLROWSELECT) + proc selectedIndex(): int = + list.getNextItem(-1, wListNextAll, wListStateSelected) - # Une colonne pleine largeur pour afficher le texte des rappels. - var col: LVCOLUMN - col.mask = LVCF_WIDTH - col.cx = w - 140 - discard SendMessage(remListHwnd, LVM_INSERTCOLUMN, 0, cast[LPARAM](&col)) + proc updateEnabled() = + let idx = selectedIndex() + if idx >= 0 and idx < dlgReminders.len: + chkEnabled.setValue(dlgReminders[idx].enabled) - discard button(S.reminderAdd, w - 100, 12, 88, 28, IdRemAdd, hwnd, inst) - discard button(S.reminderEdit, w - 100, 46, 88, 28, IdRemEdit, hwnd, inst) - discard button(S.reminderRemove, w - 100, 80, 88, 28, IdRemRemove, hwnd, inst) + list.appendColumn("", 320) - discard button(S.btnOk, w - 210, h - 50, 95, 28, IdRemOk, hwnd, inst) - discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdRemCancel, hwnd, inst) + btnAdd.wEvent_Button do (): + let text = promptText(frame, S.reminderNewTitle, "") + if text.len > 0: + dlgReminders.add HealthReminder(text: text, enabled: true) + refresh() - refreshReminderList(remListHwnd) + btnEdit.wEvent_Button do (): + let idx = selectedIndex() + if idx >= 0 and idx < dlgReminders.len: + let text = promptText(frame, S.reminderEditTitle, dlgReminders[idx].text) + if text.len > 0: + dlgReminders[idx].text = text + refresh() - var m: MSG - while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0: - if not IsDialogMessage(hwnd, &m): - TranslateMessage(&m) - DispatchMessage(&m) + btnRemove.wEvent_Button do (): + let idx = selectedIndex() + if idx >= 0 and idx < dlgReminders.len: + dlgReminders.delete(idx) + refresh() + + chkEnabled.wEvent_CheckBox do (): + let idx = selectedIndex() + if idx >= 0 and idx < dlgReminders.len: + dlgReminders[idx].enabled = chkEnabled.getValue() + + list.wEvent_ListItemSelected do (): + updateEnabled() + + btnOk.wEvent_Button do (): + dlgSaved = true + frame.close() + + btnCancel.wEvent_Button do (): + frame.close() + + # Mise en page (positions fixes). + list.move(12, 12) + list.setSize(320, 250) + btnAdd.move(344, 12); btnAdd.setSize(88, 28) + btnEdit.move(344, 46); btnEdit.setSize(88, 28) + btnRemove.move(344, 80); btnRemove.setSize(88, 28) + chkEnabled.move(12, 270); chkEnabled.setSize(320, 22) + btnOk.move(232, 300); btnOk.setSize(95, 28) + btnCancel.move(337, 300); btnCancel.setSize(95, 28) + + frame.setClientSize(444, 340) + frame.center() + + refresh() + updateEnabled() + + discard frame.showModal() if dlgSaved: - cfg.healthReminders = remWorking + cfg.healthReminders = dlgReminders # --------------------------------------------------------------------------- # Panneau de réglages # --------------------------------------------------------------------------- -proc saveSettingsFromControls(hwnd: HWND, cfg: var Config) = - cfg.workMin = getEditInt(hwnd, IdEditWork, cfg.workMin) - cfg.shortBreakMin = getEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin) - cfg.longBreakMin = getEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin) - cfg.cyclesBeforeLongBreak = getEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak) - cfg.dailyGoal = getEditInt(hwnd, IdEditGoal, cfg.dailyGoal) - cfg.soundEnabled = isChecked(hwnd, IdChkSound) - cfg.doNotDisturb = isChecked(hwnd, IdChkDnd) - cfg.autoStart = isChecked(hwnd, IdChkAutoStart) - if isChecked(hwnd, IdRadioFr): cfg.lang = "fr" - elif isChecked(hwnd, IdRadioEn): cfg.lang = "en" - else: cfg.lang = "auto" - -proc settingsProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} = - case msg - of WM_COMMAND: - let id = int(wParam and 0xFFFF) - case id - of IdBtnSave: - saveSettingsFromControls(hwnd, dlgCfg) - dlgSaved = true - dlgClosed = true - DestroyWindow(hwnd) - of IdBtnCancel: - dlgClosed = true - DestroyWindow(hwnd) - of IdBtnReminders: - showRemindersDialog(hwnd, GetModuleHandleW(nil), dlgCfg) - else: discard - return 0 - of WM_CLOSE: - dlgClosed = true - DestroyWindow(hwnd) - return 0 - of WM_DESTROY: - dlgClosed = true - return 0 - else: discard - return DefWindowProc(hwnd, msg, wParam, lParam) - -proc buildSettingsDialog*(parent: HWND, cfg: Config): HWND = - ## Construit la fenêtre et les contrôles du panneau de réglages (sans lancer - ## la boucle modale). Retourne le HWND créé. Utilisé par `showSettingsDialog` - ## et par les tests d'intégration. +proc showSettingsDialog*(cfg: var Config): bool = dlgCfg = cfg - dlgClosed = false dlgSaved = false - let inst = GetModuleHandleW(nil) - let w = 420'i32 - let h = 430'i32 + let frame = Frame(title=S.settingsTitle, style=wDefaultDialogStyle) + let panel = Panel(frame) - var wc: WNDCLASSW - wc.lpfnWndProc = cast[WNDPROC](settingsProc) - wc.hInstance = inst - wc.lpszClassName = L"TrayTempoSettingsClass" - wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1) - discard RegisterClass(&wc) + let lblWork = StaticText(panel, label=S.settingsWork) + let lblShort = StaticText(panel, label=S.settingsShortBreak) + let lblLong = StaticText(panel, label=S.settingsLongBreak) + let lblCycles = StaticText(panel, label=S.settingsCycles) + let lblGoal = StaticText(panel, label=S.settingsGoal) + let lblLang = StaticText(panel, label=S.settingsLanguage) - let hwnd = CreateWindowExW(0, L"TrayTempoSettingsClass", S.settingsTitle, - WS_CAPTION or WS_SYSMENU or WS_VISIBLE, - 0, 0, w, h, parent, 0, inst, nil) - centerWindow(hwnd, w, h) + let editWork = TextCtrl(panel) + let editShort = TextCtrl(panel) + let editLong = TextCtrl(panel) + let editCycles = TextCtrl(panel) + let editGoal = TextCtrl(panel) - # --- Durées --- - var y = 12'i32 - discard label(S.settingsWork, 12, y + 3, 120, 18, 0, hwnd, inst) - discard editNum(140, y, 60, 22, IdEditWork, hwnd, inst) - discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst) + let chkSound = CheckBox(panel, label=S.settingsSound) + let chkDnd = CheckBox(panel, label=S.settingsDoNotDisturb) + let chkAutoStart = CheckBox(panel, label=S.settingsAutoStart) - y += 28 - discard label(S.settingsShortBreak, 12, y + 3, 120, 18, 0, hwnd, inst) - discard editNum(140, y, 60, 22, IdEditShortBreak, hwnd, inst) - discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst) + let cboLang = ComboBox(panel, style=wCbReadOnly) - y += 28 - discard label(S.settingsLongBreak, 12, y + 3, 120, 18, 0, hwnd, inst) - discard editNum(140, y, 60, 22, IdEditLongBreak, hwnd, inst) - discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst) + let btnReminders = Button(panel, label=S.settingsEditReminders) + let btnSave = Button(panel, label=S.settingsSave) + let btnCancel = Button(panel, label=S.settingsCancel) - y += 28 - discard label(S.settingsCycles, 12, y + 3, 180, 18, 0, hwnd, inst) - discard editNum(200, y, 60, 22, IdEditCycles, hwnd, inst) + cboLang.append(["Auto", "Français", "English"]) - y += 34 - discard checkbox(S.settingsSound, 12, y, 240, 20, IdChkSound, hwnd, inst) - y += 24 - discard checkbox(S.settingsDoNotDisturb, 12, y, 320, 20, IdChkDnd, hwnd, inst) - y += 24 - discard checkbox(S.settingsAutoStart, 12, y, 240, 20, IdChkAutoStart, hwnd, inst) - - y += 30 - discard label(S.settingsGoal, 12, y + 3, 140, 18, 0, hwnd, inst) - discard editNum(160, y, 60, 22, IdEditGoal, hwnd, inst) - discard label(S.settingsGoalHint, 12, y + 24, 380, 16, 0, hwnd, inst) - - y += 46 - discard label(S.settingsLanguage, 12, y, 100, 18, 0, hwnd, inst) - discard radio("Auto", 120, y, 70, 20, IdRadioAuto, hwnd, inst, true) - discard radio("Français", 190, y, 90, 20, IdRadioFr, hwnd, inst, false) - discard radio("English", 285, y, 90, 20, IdRadioEn, hwnd, inst, false) - - y += 30 - discard button(S.settingsEditReminders, 12, y, 220, 28, IdBtnReminders, hwnd, inst) - - # --- Boutons --- - discard button(S.settingsSave, w - 210, h - 62, 100, 30, IdBtnSave, hwnd, inst) - discard button(S.settingsCancel, w - 105, h - 62, 100, 30, IdBtnCancel, hwnd, inst) - - # --- Pré-remplissage --- - setEditInt(hwnd, IdEditWork, cfg.workMin) - setEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin) - setEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin) - setEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak) - setEditInt(hwnd, IdEditGoal, cfg.dailyGoal) - setChecked(hwnd, IdChkSound, cfg.soundEnabled) - setChecked(hwnd, IdChkDnd, cfg.doNotDisturb) - setChecked(hwnd, IdChkAutoStart, cfg.autoStart) + # Pré-remplissage. + editWork.setValue($cfg.workMin) + editShort.setValue($cfg.shortBreakMin) + editLong.setValue($cfg.longBreakMin) + editCycles.setValue($cfg.cyclesBeforeLongBreak) + editGoal.setValue($cfg.dailyGoal) + chkSound.setValue(cfg.soundEnabled) + chkDnd.setValue(cfg.doNotDisturb) + chkAutoStart.setValue(cfg.autoStart) case cfg.lang - of "fr": setChecked(hwnd, IdRadioFr, true) - of "en": setChecked(hwnd, IdRadioEn, true) - else: setChecked(hwnd, IdRadioAuto, true) + of "fr": cboLang.setSelection(1) + of "en": cboLang.setSelection(2) + else: cboLang.setSelection(0) - discard SetFocus(GetDlgItem(hwnd, IdEditWork)) - result = hwnd + proc parseIntOr(text: string, dflt: int): int = + try: result = parseInt(text) + except ValueError: result = dflt -proc showSettingsDialog*(parent: HWND, cfg: var Config): bool = - ## Affiche le panneau de réglages. Retourne true si l'utilisateur a enregistré. - let hwnd = buildSettingsDialog(parent, cfg) + btnSave.wEvent_Button do (): + dlgCfg.workMin = parseIntOr(editWork.getValue(), dlgCfg.workMin) + dlgCfg.shortBreakMin = parseIntOr(editShort.getValue(), dlgCfg.shortBreakMin) + dlgCfg.longBreakMin = parseIntOr(editLong.getValue(), dlgCfg.longBreakMin) + dlgCfg.cyclesBeforeLongBreak = parseIntOr(editCycles.getValue(), dlgCfg.cyclesBeforeLongBreak) + dlgCfg.dailyGoal = parseIntOr(editGoal.getValue(), dlgCfg.dailyGoal) + dlgCfg.soundEnabled = chkSound.getValue() + dlgCfg.doNotDisturb = chkDnd.getValue() + dlgCfg.autoStart = chkAutoStart.getValue() + case cboLang.getSelection() + of 1: dlgCfg.lang = "fr" + of 2: dlgCfg.lang = "en" + else: dlgCfg.lang = "auto" + dlgSaved = true + frame.close() - # Boucle de messages locale (dialogue modal). - var m: MSG - while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0: - if not IsDialogMessage(hwnd, &m): - TranslateMessage(&m) - DispatchMessage(&m) + btnCancel.wEvent_Button do (): + frame.close() + + btnReminders.wEvent_Button do (): + showRemindersDialog(frame, dlgCfg) + + # Mise en page (positions fixes). + var y = 12 + lblWork.move(12, y + 3); lblWork.setSize(150, 18) + editWork.move(170, y); editWork.setSize(60, 22) + y += 28 + lblShort.move(12, y + 3); lblShort.setSize(150, 18) + editShort.move(170, y); editShort.setSize(60, 22) + y += 28 + lblLong.move(12, y + 3); lblLong.setSize(150, 18) + editLong.move(170, y); editLong.setSize(60, 22) + y += 28 + lblCycles.move(12, y + 3); lblCycles.setSize(150, 18) + editCycles.move(170, y); editCycles.setSize(60, 22) + y += 30 + lblGoal.move(12, y + 3); lblGoal.setSize(150, 18) + editGoal.move(170, y); editGoal.setSize(60, 22) + y += 30 + chkSound.move(12, y); chkSound.setSize(240, 20) + y += 24 + chkDnd.move(12, y); chkDnd.setSize(320, 20) + y += 24 + chkAutoStart.move(12, y); chkAutoStart.setSize(240, 20) + y += 30 + lblLang.move(12, y + 3); lblLang.setSize(100, 18) + cboLang.move(120, y); cboLang.setSize(120, 120) + y += 30 + btnReminders.move(12, y); btnReminders.setSize(220, 28) + + btnSave.move(190, 400); btnSave.setSize(100, 30) + btnCancel.move(300, 400); btnCancel.setSize(100, 30) + + frame.setClientSize(420, 440) + frame.center() + + discard frame.showModal() if dlgSaved: cfg = dlgCfg - return dlgSaved + result = dlgSaved # --------------------------------------------------------------------------- # Assistant de premier lancement # --------------------------------------------------------------------------- -const WizSteps = 5 # 0 welcome, 1 langue, 2 profil, 3 santé, 4 fin - -proc wizRebuild(hwnd: HWND, inst: HINSTANCE) = - # Efface tous les contrôles enfants existants. - var child = GetWindow(hwnd, GW_CHILD) - while child != 0: - let next = GetWindow(child, GW_HWNDNEXT) - DestroyWindow(child) - child = next - - case wizStep - of 0: - discard label(S.wizardWelcomeTitle, 16, 16, 460, 26, 0, hwnd, inst) - discard label(S.wizardWelcomeBody, 16, 48, 460, 60, 0, hwnd, inst) - of 1: - discard label(S.wizardLangPrompt, 16, 16, 460, 22, 0, hwnd, inst) - discard radio("Français", 30, 48, 200, 22, IdWizRadioBase, hwnd, inst, true) - discard radio("English", 30, 76, 200, 22, int32(IdWizRadioBase + 1), hwnd, inst, false) - # Pré-sélection selon la langue détectée. - setChecked(hwnd, IdWizRadioBase, detectSystemLang() == langFr) - setChecked(hwnd, int32(IdWizRadioBase + 1), detectSystemLang() != langFr) - of 2: - discard label(S.wizardProfilePrompt, 16, 16, 460, 22, 0, hwnd, inst) - let profs = wizCfg.profiles - for i, p in profs: - let name = p.name & " (" & $p.workMin & "/" & $p.shortBreakMin & "/" & - $p.longBreakMin & " min)" - discard radio(name, 30, 48 + i * 26, 440, 22, int32(IdWizRadioBase + i), hwnd, inst, i == 0) - # Pré-sélectionne le profil actif. - for i, p in profs: - if p.name == wizCfg.activeProfile: - setChecked(hwnd, int32(IdWizRadioBase + i), true) - of 3: - discard label(S.wizardHealthPrompt, 16, 16, 460, 22, 0, hwnd, inst) - for i, r in wizCfg.healthReminders: - discard checkbox(r.text, 30, 48 + i * 26, 440, 22, int32(IdWizChkBase + i), hwnd, inst) - setChecked(hwnd, int32(IdWizChkBase + i), r.enabled) - of 4: - discard label(S.wizardDoneTitle, 16, 16, 460, 26, 0, hwnd, inst) - discard label(S.wizardDoneBody, 16, 48, 460, 80, 0, hwnd, inst) - else: discard - - # Boutons Bas / Suivant / Terminer. - let backEnabled = wizStep > 0 - discard button(S.btnCancel, 20, 300, 90, 30, IdWizBack, hwnd, inst) - if not backEnabled: - discard EnableWindow(GetDlgItem(hwnd, IdWizBack), FALSE) - let nextLabel = if wizStep == WizSteps - 1: S.settingsSave else: S.btnOk - discard button(nextLabel, 400, 300, 90, 30, IdWizNext, hwnd, inst) - -proc wizCollectStep(hwnd: HWND) = - ## Lit les choix de l'étape courante dans wizCfg. - case wizStep - of 1: - if isChecked(hwnd, IdWizRadioBase): wizCfg.lang = "fr" - elif isChecked(hwnd, int32(IdWizRadioBase + 1)): wizCfg.lang = "en" - of 2: - let profs = wizCfg.profiles - for i in 0 ..< profs.len: - if isChecked(hwnd, int32(IdWizRadioBase + i)): - wizCfg.activeProfile = profs[i].name - of 3: - for i in 0 ..< wizCfg.healthReminders.len: - wizCfg.healthReminders[i].enabled = isChecked(hwnd, int32(IdWizChkBase + i)) - else: discard - -proc wizardProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} = - case msg - of WM_COMMAND: - let id = int(wParam and 0xFFFF) - case id - of IdWizNext: - wizCollectStep(hwnd) - if wizStep < WizSteps - 1: - inc wizStep - wizRebuild(hwnd, GetModuleHandleW(nil)) - else: - dlgSaved = true - dlgClosed = true - DestroyWindow(hwnd) - of IdWizBack: - if wizStep > 0: - dec wizStep - wizRebuild(hwnd, GetModuleHandleW(nil)) - else: discard - return 0 - of WM_CLOSE: - dlgClosed = true - DestroyWindow(hwnd) - return 0 - of WM_DESTROY: - dlgClosed = true - return 0 - else: discard - return DefWindowProc(hwnd, msg, wParam, lParam) - proc showFirstRunWizard*(cfg: var Config): bool = - ## Assistant de premier lancement. Retourne true si l'utilisateur l'a terminé. - wizCfg = cfg - wizStep = 0 - dlgClosed = false + dlgCfg = cfg dlgSaved = false - let inst = GetModuleHandleW(nil) - let w = 520'i32 - let h = 360'i32 + var step = 0 - var wc: WNDCLASSW - wc.lpfnWndProc = cast[WNDPROC](wizardProc) - wc.hInstance = inst - wc.lpszClassName = L"TrayTempoWizardClass" - wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1) - discard RegisterClass(&wc) + let frame = Frame(title=S.wizardWelcomeTitle, style=wDefaultDialogStyle) + let panel = Panel(frame) - let hwnd = CreateWindowExW(0, L"TrayTempoWizardClass", S.wizardWelcomeTitle, - WS_CAPTION or WS_SYSMENU or WS_VISIBLE, - 0, 0, w, h, 0, 0, inst, nil) - centerWindow(hwnd, w, h) - wizRebuild(hwnd, inst) + let text = StaticText(panel, label="") + let radio1 = RadioButton(panel, label="") + let radio2 = RadioButton(panel, label="") + let radio3 = RadioButton(panel, label="") + let cboProfile = ComboBox(panel, style=wCbReadOnly) + let chk1 = CheckBox(panel, label="") + let chk2 = CheckBox(panel, label="") + let chk3 = CheckBox(panel, label="") + let chk4 = CheckBox(panel, label="") + let chk5 = CheckBox(panel, label="") + let btnBack = Button(panel, label=S.btnCancel) + let btnNext = Button(panel, label=S.btnOk) - var m: MSG - while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0: - if not IsDialogMessage(hwnd, &m): - TranslateMessage(&m) - DispatchMessage(&m) + proc showAll(b: bool) = + text.show(b) + radio1.show(b); radio2.show(b); radio3.show(b) + cboProfile.show(b) + chk1.show(b); chk2.show(b); chk3.show(b); chk4.show(b); chk5.show(b) + + proc render() = + showAll(false) + btnBack.enable(step > 0) + case step + of 0: + text.setLabel(S.wizardWelcomeTitle & "\n\n" & S.wizardWelcomeBody) + text.show() + of 1: + text.setLabel(S.wizardLangPrompt); text.show() + radio1.setLabel("Français"); radio2.setLabel("English") + radio1.setValue(detectSystemLang() == langFr) + radio2.setValue(detectSystemLang() != langFr) + radio1.show(); radio2.show() + of 2: + text.setLabel(S.wizardProfilePrompt); text.show() + cboProfile.show() + of 3: + text.setLabel(S.wizardHealthPrompt); text.show() + chk1.show(); chk2.show(); chk3.show(); chk4.show(); chk5.show() + of 4: + text.setLabel(S.wizardDoneTitle & "\n\n" & S.wizardDoneBody); text.show() + btnNext.setLabel(S.settingsSave) + else: discard + + btnNext.wEvent_Button do (): + case step + of 1: + if radio1.getValue(): dlgCfg.lang = "fr" else: dlgCfg.lang = "en" + of 2: + if cboProfile.getSelection() >= 0 and cboProfile.getSelection() < dlgCfg.profiles.len: + dlgCfg.activeProfile = dlgCfg.profiles[cboProfile.getSelection()].name + of 3: + dlgCfg.healthReminders[0].enabled = chk1.getValue() + dlgCfg.healthReminders[1].enabled = chk2.getValue() + dlgCfg.healthReminders[2].enabled = chk3.getValue() + dlgCfg.healthReminders[3].enabled = chk4.getValue() + dlgCfg.healthReminders[4].enabled = chk5.getValue() + else: discard + if step < 4: + inc step + render() + else: + dlgSaved = true + frame.close() + + btnBack.wEvent_Button do (): + if step > 0: + dec step + render() + + # Initialisation du profil et des rappels. + cboProfile.append(@["Classique", "Deep Work", "Sprint"]) + cboProfile.setSelection(0) + for i, r in cfg.healthReminders: + case i + of 0: chk1.setLabel(r.text); chk1.setValue(r.enabled) + of 1: chk2.setLabel(r.text); chk2.setValue(r.enabled) + of 2: chk3.setLabel(r.text); chk3.setValue(r.enabled) + of 3: chk4.setLabel(r.text); chk4.setValue(r.enabled) + of 4: chk5.setLabel(r.text); chk5.setValue(r.enabled) + else: discard + + text.move(16, 16); text.setSize(460, 200) + radio1.move(30, 100); radio1.setSize(200, 22) + radio2.move(30, 128); radio2.setSize(200, 22) + radio3.move(30, 156); radio3.setSize(200, 22) + cboProfile.move(30, 100); cboProfile.setSize(200, 120) + chk1.move(30, 90); chk1.setSize(440, 22) + chk2.move(30, 116); chk2.setSize(440, 22) + chk3.move(30, 142); chk3.setSize(440, 22) + chk4.move(30, 168); chk4.setSize(440, 22) + chk5.move(30, 194); chk5.setSize(440, 22) + btnBack.move(20, 300); btnBack.setSize(90, 30) + btnNext.move(400, 300); btnNext.setSize(90, 30) + + frame.setClientSize(510, 350) + frame.center() + + render() + discard frame.showModal() if dlgSaved: - cfg = wizCfg - return dlgSaved + cfg = dlgCfg + result = dlgSaved diff --git a/src/floating.nim b/src/floating.nim index 6286634..ad9d45f 100644 --- a/src/floating.nim +++ b/src/floating.nim @@ -43,7 +43,7 @@ proc showFloating*() = var wc: WNDCLASSW wc.lpfnWndProc = cast[WNDPROC](floatingProc) wc.hInstance = inst - wc.lpszClassName = L"TrayTempoFloatingClass" + wc.lpszClassName = L"NimPulseFloatingClass" wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1) discard RegisterClass(&wc) @@ -53,7 +53,7 @@ proc showFloating*() = let scrH = GetSystemMetrics(SM_CYSCREEN) fHwnd = CreateWindowExW(WS_EX_TOPMOST or WS_EX_TOOLWINDOW, - L"TrayTempoFloatingClass", L"TrayTempo", + L"NimPulseFloatingClass", L"NimPulse", WS_POPUP or WS_BORDER or WS_VISIBLE, scrW - w - 24, scrH - h - 80, w, h, 0, 0, inst, nil) diff --git a/src/i18n.nim b/src/i18n.nim index fcab07b..1c032c8 100644 --- a/src/i18n.nim +++ b/src/i18n.nim @@ -6,7 +6,7 @@ ## démarrage (détection système ou préférence utilisateur dans config.json). ## ## Ce module importe `config` (types) et `timer` (pour la phase), mais il est -## lui-même importé par les modules d'interface (tray, notifications, traytempo). +## lui-même importé par les modules d'interface (tray, notifications, nimpulse). import winim/lean import config @@ -130,7 +130,7 @@ type Strings* = object # --------------------------------------------------------------------------- const fr* = Strings( - appName: "TrayTempo", + appName: "NimPulse", phaseIdle: "Inactif", phaseWorking: "Travail", @@ -138,8 +138,8 @@ const fr* = Strings( phaseLongBreak: "Pause longue", phasePaused: "En pause", - tipIdle: "TrayTempo - Inactif", - tipPaused: "TrayTempo - En pause", + tipIdle: "NimPulse - Inactif", + tipPaused: "NimPulse - En pause", menuStart: "Démarrer", menuResume: "Reprendre", @@ -165,7 +165,7 @@ const fr* = Strings( breakShort: "courte", breakLong: "longue", - statsTitle: "Statistiques TrayTempo", + statsTitle: "Statistiques NimPulse", statsToday: "Pomodoros aujourd'hui", statsWeek: "Pomodoros (7 jours)", statsStreak: "Série", @@ -176,8 +176,8 @@ const fr* = Strings( statsNone: "Aucun pomodoro complété pour l'instant.", statsExportError: "Impossible d'exporter les statistiques.", - wizardWelcomeTitle: "Bienvenue dans TrayTempo", - wizardWelcomeBody: "Configurons TrayTempo en quelques étapes simples pour que " & + wizardWelcomeTitle: "Bienvenue dans NimPulse", + wizardWelcomeBody: "Configurons NimPulse en quelques étapes simples pour que " & "vous soyez productif tout en prenant soin de votre santé.", wizardLangTitle: "Langue / Language", wizardLangPrompt: "Choisissez votre langue :", @@ -186,10 +186,10 @@ const fr* = Strings( wizardHealthTitle: "Rappels santé", wizardHealthPrompt: "Quels rappels souhaitez-vous pendant vos pauses ?", wizardDoneTitle: "C'est prêt !", - wizardDoneBody: "TrayTempo vit maintenant dans la zone de notification. " & + wizardDoneBody: "NimPulse vit maintenant dans la zone de notification. " & "Clic droit sur l'icône pour ouvrir le menu, ou Ctrl+Alt+P pour démarrer/mettre en pause.", - settingsTitle: "Paramètres TrayTempo", + settingsTitle: "Paramètres NimPulse", settingsDurations: "Durées", settingsWork: "Travail", settingsShortBreak: "Pause courte", @@ -232,7 +232,7 @@ const fr* = Strings( ) const en* = Strings( - appName: "TrayTempo", + appName: "NimPulse", phaseIdle: "Idle", phaseWorking: "Working", @@ -240,8 +240,8 @@ const en* = Strings( phaseLongBreak: "Long break", phasePaused: "Paused", - tipIdle: "TrayTempo - Idle", - tipPaused: "TrayTempo - Paused", + tipIdle: "NimPulse - Idle", + tipPaused: "NimPulse - Paused", menuStart: "Start", menuResume: "Resume", @@ -267,7 +267,7 @@ const en* = Strings( breakShort: "short", breakLong: "long", - statsTitle: "TrayTempo Statistics", + statsTitle: "NimPulse Statistics", statsToday: "Pomodoros today", statsWeek: "Pomodoros (7 days)", statsStreak: "Streak", @@ -278,8 +278,8 @@ const en* = Strings( statsNone: "No pomodoros completed yet.", statsExportError: "Could not export statistics.", - wizardWelcomeTitle: "Welcome to TrayTempo", - wizardWelcomeBody: "Let's set up TrayTempo in a few simple steps so you can " & + wizardWelcomeTitle: "Welcome to NimPulse", + wizardWelcomeBody: "Let's set up NimPulse in a few simple steps so you can " & "be productive while taking care of your health.", wizardLangTitle: "Language", wizardLangPrompt: "Choose your language:", @@ -288,10 +288,10 @@ const en* = Strings( wizardHealthTitle: "Health reminders", wizardHealthPrompt: "Which reminders do you want during your breaks?", wizardDoneTitle: "You're ready!", - wizardDoneBody: "TrayTempo now lives in the system tray. " & + wizardDoneBody: "NimPulse now lives in the system tray. " & "Right-click the icon for the menu, or press Ctrl+Alt+P to start/pause.", - settingsTitle: "TrayTempo Settings", + settingsTitle: "NimPulse Settings", settingsDurations: "Durations", settingsWork: "Work", settingsShortBreak: "Short break", diff --git a/src/traytempo.nim b/src/nimpulse.nim similarity index 93% rename from src/traytempo.nim rename to src/nimpulse.nim index e01205c..aa8cf0a 100644 --- a/src/traytempo.nim +++ b/src/nimpulse.nim @@ -1,17 +1,18 @@ -## traytempo.nim +## nimpulse.nim ## ============= ## Point d'entrée de l'application. Crée une fenêtre cachée, enregistre ## l'icône tray, le minuteur (WM_TIMER) et le raccourci global, puis exécute ## la boucle de messages Windows. ## ## Compilation : -## nim c -d:release --app:gui src/traytempo.nim +## nim c -d:release --app:gui src/nimpulse.nim import std/os import std/typedthreads import std/strutils import winim/lean import winim/inc/shellapi +import wnim import config import timer @@ -26,7 +27,7 @@ import floating const TimerId = 1'i64 HotkeyId = 1'i32 - SingleInstanceMutexName = "TrayTempo_SingleInstance_Mutex" + SingleInstanceMutexName = "NimPulse_SingleInstance_Mutex" WmUpdateAvailable = WM_APP + 2 # --------------------------------------------------------------------------- @@ -93,7 +94,7 @@ proc quitApp() = proc openSettings() = ## Ouvre le panneau de réglages graphique. - if showSettingsDialog(gHwnd, gCfg): + if showSettingsDialog(gCfg): saveConfig(gCfg) setLang(resolveLang(gCfg)) discard setAutostart(gCfg.autoStart) @@ -102,7 +103,7 @@ proc openSettings() = showToast(gHwnd, S.appName, S.settingsSaved) proc showStatsDialog() = - dialogs.showStatsDialog(gHwnd, gCfg, gStats) + dialogs.showStatsDialog(gCfg, gStats) proc exportStatsCsv() = ## Exporte l'historique en CSV dans le répertoire de configuration. @@ -230,14 +231,17 @@ proc isAlreadyRunning(): bool = proc main() = if isAlreadyRunning(): - discard MessageBox(0, "TrayTempo est déjà en cours d'exécution.", - "TrayTempo", MB_OK or MB_ICONINFORMATION) + discard MessageBox(0, "NimPulse est déjà en cours d'exécution.", + "NimPulse", MB_OK or MB_ICONINFORMATION) return gCfg = loadConfig() gStats = loadStats() gTimer = initTimer() + # Initialise le framework wNim (contrôles communs, OLE, etc.) pour les dialogues. + discard App() + # Premier lancement : assistant de configuration guidée. let firstRun = not fileExists(configPath()) if firstRun: @@ -255,17 +259,17 @@ proc main() = var wc: WNDCLASSW wc.lpfnWndProc = cast[WNDPROC](wndProc) wc.hInstance = hInstance - wc.lpszClassName = L"TrayTempoWindowClass" + wc.lpszClassName = L"NimPulseWindowClass" wc.hIcon = phaseIcon(phIdle) if RegisterClass(&wc) == 0: # La classe peut déjà être enregistrée ; on poursuit quand même. discard - gHwnd = CreateWindowExW(0, L"TrayTempoWindowClass", L"TrayTempo", + gHwnd = CreateWindowExW(0, L"NimPulseWindowClass", L"NimPulse", WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, hInstance, nil) if gHwnd == 0: discard MessageBox(0, "Impossible de créer la fenêtre de l'application.", - "TrayTempo", MB_ICONERROR) + "NimPulse", MB_ICONERROR) return # Icône dans le tray. diff --git a/tests/test_config.nim b/tests/test_config.nim index b0d497a..822e102 100644 --- a/tests/test_config.nim +++ b/tests/test_config.nim @@ -8,7 +8,7 @@ import std/[unittest, os] import ../src/config # Redirige APPDATA vers un répertoire temporaire pour isoler les fichiers. -let tmpApd = getTempDir() / "traytempo_test_apd" +let tmpApd = getTempDir() / "nimpulse_test_apd" if dirExists(tmpApd): removeDir(tmpApd) putEnv("APPDATA", tmpApd) @@ -102,11 +102,11 @@ suite "config": writeFile(marker, "") check configDir() == exeDir removeFile(marker) - check configDir() == (getEnv("APPDATA") / "TrayTempo") + check configDir() == (getEnv("APPDATA") / "NimPulse") test "acquireSingleInstance detects a second lock": - check not acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0") - check acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0") + check not acquireSingleInstance("NimPulse_Test_Mutex_0.2.0") + check acquireSingleInstance("NimPulse_Test_Mutex_0.2.0") suite "autostart registry": test "setAutostart toggles the Run key and cleans up": diff --git a/tests/test_dialogs.nim b/tests/test_dialogs.nim deleted file mode 100644 index 360c55e..0000000 --- a/tests/test_dialogs.nim +++ /dev/null @@ -1,34 +0,0 @@ -## test_dialogs.nim -## ================= -## Test d'intégration de la construction du panneau de réglages : vérifie que la -## fenêtre et ses contrôles sont réellement créés (sans lancer la boucle modale, -## qui bloquerait). Cela rend observable la création de l'interface, au-delà de -## la simple compilation. - -import std/unittest -import winim/lean -import ../src/dialogs -import ../src/config - -proc createParent(): HWND = - let hInstance = GetModuleHandleW(nil) - var wc: WNDCLASSW - wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW) - wc.hInstance = hInstance - wc.lpszClassName = L"TrayTempoDialogTestClass" - discard RegisterClass(&wc) - result = CreateWindowExW(0, L"TrayTempoDialogTestClass", L"", WS_OVERLAPPED, - 0, 0, 0, 0, 0, 0, hInstance, nil) - -suite "dialogs": - test "settings dialog window and controls are built": - let parent = createParent() - check parent != 0 - - let hwnd = buildSettingsDialog(parent, defaultConfig()) - check hwnd != 0 - # La fenêtre doit avoir au moins un contrôle enfant (champ, bouton, etc.). - check GetWindow(hwnd, GW_CHILD) != 0 - - discard DestroyWindow(hwnd) - discard DestroyWindow(parent) diff --git a/tests/test_stats.nim b/tests/test_stats.nim index c58e712..271ebb3 100644 --- a/tests/test_stats.nim +++ b/tests/test_stats.nim @@ -80,7 +80,7 @@ suite "stats": var st = initStats() st.daily["2026-08-13"] = 3 st.daily["2026-08-14"] = 5 - let path = getTempDir() / "traytempo_test_export.csv" + let path = getTempDir() / "nimpulse_test_export.csv" check st.exportCsv(path) let content = readFile(path) check content.contains("date,count") @@ -91,7 +91,7 @@ suite "stats": suite "stats persistence": test "saveStats then loadStats round-trips": # Isole l'écriture disque dans un APPDATA temporaire. - let tmpApd = getTempDir() / "traytempo_test_stats" + let tmpApd = getTempDir() / "nimpulse_test_stats" if dirExists(tmpApd): removeDir(tmpApd) putEnv("APPDATA", tmpApd) diff --git a/tests/test_tray_integration.nim b/tests/test_tray_integration.nim index 6012680..732587a 100644 --- a/tests/test_tray_integration.nim +++ b/tests/test_tray_integration.nim @@ -19,9 +19,9 @@ proc createTestWindow(): HWND = var wc: WNDCLASSW wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW) wc.hInstance = hInstance - wc.lpszClassName = L"TrayTempoTestClass" + wc.lpszClassName = L"NimPulseTestClass" discard RegisterClass(&wc) - result = CreateWindowExW(0, L"TrayTempoTestClass", L"", WS_OVERLAPPED, + result = CreateWindowExW(0, L"NimPulseTestClass", L"", WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, hInstance, nil) suite "tray integration": @@ -34,7 +34,7 @@ suite "tray integration": check phaseIcon(phase) != 0 # Ajout de l'icône dans le tray (Shell_NotifyIcon NIM_ADD -> bool). - check addTrayIcon(hwnd, phaseIcon(phIdle), "TrayTempo") == true + check addTrayIcon(hwnd, phaseIcon(phIdle), "NimPulse") == true # Mise à jour de l'icône et du tooltip. updateTrayIcon(hwnd, phaseIcon(phWorking), "Travail: 25:00")