Repository navigation
Releases
v1.3.2
Full Changelog: v1.3.1...v1.3.2
v1.3.1
What's Changed
- fix(widgets): keep snapped widgets from cascading after minimize by @Vanchestery in #24
Full Changelog: v1.3.0...v1.3.1
v1.3.0
What's Changed
- feat: always-on-top setting, hide/show widgets, and restore snap/size by @Vanchestery in #20
- docs: add current UI screenshots to README by @Vanchestery in #21
Full Changelog: v1.2.0...v1.3.0
v1.2.0
v1.2.0 — calculation & UX expansion
Шесть блоков фич плюс архитектурный фундамент под округление как первоклассный концепт ГСМ-учёта. Все блоки разбиты по Conventional Commits, каждый с отдельной историей.
Что нового
F. Контекстная команда процентов в стиле Windows Calc Standard.
Кнопка ± заменена на %. Логика контекстная — зависит от ожидающего оператора:
100 + 10 %→100 + 10 = 110100 − 10 %→90100 × 10 %→10(10% от 100)100 ÷ 10 %→100010 %без оператора →0.1
Хоткей Shift+5. В Engineering-режиме % работает упрощённо (value/100), как Windows Calc Scientific. Цепочка после % корректно закрывается оператором (100 + 10% + 5 = 115) через флаг _justResolvedPercent.
G. Копирование значений в буфер обмена.
Калькулятор: Ctrl+C + иконка-кнопка справа от дисплея + правый клик → контекстное меню. Копирует округлённое значение дисплея (по K). Виджет: то же UI, копирует только число из результата (без единиц).
Новый сервис IClipboardService с try/catch на COM-ошибки (буфер занят другим процессом — тихо не копируем, не падаем).
H. Авто-сохранение изменений виджета.
Юзер двигает слайдер decimal places или меняет плотность — изменения автоматически пишутся в widgets.json через 500 мс после последнего изменения. Debounce защищает от мелкого спама на диск.
Новые абстракции IDebouncer / IDebouncerFactory (реализация на DispatcherTimer). Re-fetch виджета перед сохранением предотвращает race с одновременным редактированием через Add Widget. Flush при Dispose — последнее изменение не теряется при закрытии окна.
I. Закреплённые виджеты + панель в главном окне. Новая левая панель «Избранное» — зеркало панели истории, со своим тогглом в топ-баре (иконка ★). Клик по плитке открывает виджет.
Новый сервис IFavoritesService (Id-only, событие FavoritesChanged) + событие WidgetsChanged на IWidgetService. Закрепление через две точки: кнопка в Add Widget или чекбокс в форме редактирования. Удаление виджета автоматически убирает его из избранного. Состояние видимости панели сохраняется в window-state.json.
J. Магнитное прилипание виджетов к главному окну. Winamp-style: подвёл виджет на ~12 пикселей к грани главного — щёлк, прилип. Когда главное окно двигают — все прилипшие виджеты едут вместе. Отлепляется когда юзер тянет виджет дальше от грани.
Чистая логика в MagnetismCalculator (без WPF-зависимостей, юнит-тестируется). Сервис IWindowMagnetismService связывает калькулятор с LocationChanged событиями WPF. Флаг _isPositioningProgrammatically разрывает рекурсию при программном позиционировании.
fix: Учёт невидимых теней DWM через DWMWA_EXTENDED_FRAME_BOUNDS — без него был визуальный зазор ~14px между прилипшими окнами. Теперь pixel-perfect alignment, DPI-aware через HwndSource.TransformFromDevice.
K. Режим округления (первоклассный концепт).
Циклическая кнопка в топ-баре переключает три режима: ∞ (без округления, default), 1 (до целых), 0.1 (до десятых). Применяется ко всему «наружному»: дисплей, аккумулятор калькулятора, история, вставка из виджета, копирование в буфер.
По К.5=Y (ГСМ-учётная семантика): каждый = округляет результат, и аккумулятор реально становится округлённым. Цепочка 100 + 50.7 = → 151 → + 0.4 = → 151 (а не 151.1). В Engineering округление только на финальном =.
Banker's rounding (MidpointRounding.ToEven) — стандарт IEEE 754 и финансовых расчётов, исключает систематический биас на больших объёмах округлений.
Архитектурные моменты
-
Чистая логика отдельно от WPF.
RoundingFormatter,MagnetismCalculator— статические функции без зависимостей отSystem.Windows.Window(только отSystem.Windows.Rect/Thickness). Юнит-тестируется без UI thread. -
События в сервисах для рефреша UI.
IFavoritesService.FavoritesChangedиIWidgetService.WidgetsChanged— pub-sub, MainViewModel слушает оба. ЦиклAdd Widget делает edit → Service raises событие → MainViewModel перерисовывает Favoritesработает без прямых ссылок между VM. -
Updateсервисов защищает инварианты.WidgetService.Updateне даёт обнулитьIsBuiltIn;IFavoritesService.Addидемпотентен (повторный вызов — no-op, не дубликаты). -
Composition over inheritance для layout.
ComputeMinWidth(favs, hist)— чистая функция, легко расширяется (если в v1.3 добавится 3-я панель).
Тесты
Добавлено ~70 новых тестов через все блоки:
| Блок | Тесты |
|---|---|
| F | 9 CalculatorService.ResolvePercent + 11 MainViewModel percent UX |
| G | 5 CopyDisplayCommand + 4 CopyResultCommand |
| H | 9 WidgetViewModel auto-save (TestDebouncer для детерминизма) |
| I | 13 FavoritesService + 6 MainViewModel favorites |
| J | 14 MagnetismCalculator (4 грани + threshold edge cases + ambiguity tiebreak) + 5 insets |
| K | 18 RoundingFormatter + 11 MainViewModel rounding behaviour |
Все зелёные.
UX-улучшения помимо больших фич
- Кнопка
±удалена (унарный минус почти не используется в ГСМ; место отдано под%). - Виджет ужат ранее в v1.1 — теперь компактнее и пиксель-перфектно прилипает к главному окну.
- WrapPanel для кнопок в Add Widget — 5 кнопок не вылезают за окно при узком ресайзе.
- Иконки топ-бара: ★ (избранное), ∞/1/0.1 (округление) — единообразно с Segoe MDL2 Assets.
Известные ограничения
- Resize главного окна мышью НЕ двигает прилипшие виджеты (только перетаскивание за заголовок).
LocationChangedне фаерится на ресайз, толькоSizeChanged. Можно поправить в v1.3, если будет нужно. - Если виджет переименовали через Add Widget пока он открыт — заголовок окна виджета остаётся со старым именем до перезагрузки. Известно с v1.1, не задача v1.2.
v1.1.0
v1.1.0 — UX improvements after first user testing
После публикации v1.0 и первого теста "в бою" собрал пачку пожеланий и багов. Вот что вошло в v1.1.
Что нового
A. Фикс сброса дисплея после виджета.
После «Вставить в калькулятор» следующая нажатая цифра теперь корректно начинает новое число, а не дописывается к вставленному.
B. Тёмные надписи в списке виджетов.
Имена виджетов в «Добавить виджет» больше не сливаются с фоном на тёмной теме — явно задан Foreground=TextForeground, чтобы перебить стандартный SystemColors.ControlText из ListBoxItem.ControlTemplate.
C. Память позиции и размера главного окна.
- Первый запуск — окно по центру экрана, размер
Auto(SizeToContent=WidthAndHeight). - Позиция/размер запоминаются между сессиями в
%AppData%/GsmCalculator/window-state.json— отдельно от session state (позиция помнится всегда, даже без явного сохранения сессии). - Восстановление учитывает multi-monitor: если сохранённая позиция вне экрана (
ScreenHelper.IsOnScreen), окно центрируется заново. - Используется
Window.RestoreBounds— корректно сохраняется размер Normal-state даже если окно было свернуто/развернуто.
D. Toggle панели истории + auto-размер.
- Новая кнопка-иконка
в топ-баре прячет/показывает панель истории. - При скрытии окно сжимается до 360px (только калькулятор), при показе разворачивается до 540px минимум.
- Состояние видимости истории сохраняется в
MainWindowState.IsHistoryVisible.
E. Редактирование виджетов (включая встроенные).
- В окне «Добавить виджет» появилась кнопка «Изменить» (доступна для любого виджета).
CreateWidgetWindowпереиспользуется в двух режимах через optionalWidget? toEdit— DRY поверх одних и тех же полей и валидации.WidgetService.Update()защищает инвариантIsBuiltInна уровне сервиса: даже если вызывающий код подсунетIsBuiltIn=falseдля встроенного виджета, флаг останетсяtrue. Тест явно проверяет это.
Бонус — иконки и компактность
- Три кнопки топ-бара (
+, настройки, история) заменены на иконки Segoe MDL2 Assets (E710,E713,E81C) сToolTip— позволило сузить главное окно в компактном режиме. - Виджет ужат: ширина 300 → 215, высота кнопок и шрифты пропорционально уменьшены.
Технические детали
- Новый сервис
IWindowStateService+ helperScreenHelper.IsOnScreen(вынесен изWidgetWindowService). MainViewModel.NotifyDisplayConsumed()— публичный API, дёргается изWidgetViewModelпослеGetDisplayValue.- Архитектура: physical window layout (
MainWindowState) отделён от пользовательских настроек (AppSettings).
Тесты
Добавлено 13 новых тестов:
- 5 для
MainViewModel(display consume, history toggle, label localization) - 4 для
WindowStateService(load missing/corrupt, round-trip, dir creation) - 4 для
WidgetService.Update(preserveIsBuiltIn, persist, unknown id no-op, empty name throws)
Все зелёные.
What's Changed
- v1.1: UX improvements after first user testing by @Vanchestery in #10
New Contributors
- @Vanchestery made their first contribution in #10
Full Changelog: v1.0.0...v1.1.0
v1.0.0
Full Changelog: https://github.com/Vanchestery/GsmCalculator/commits/v1.0.0