v0.3.0 : migration des dialogues vers wNim + renommage NimPulse + installeur une-ligne
This commit is contained in:
+4
-4
@@ -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
|
||||
|
||||
+290
-529
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+17
-17
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user