einsatzprotokoll/source/MenuView.mc
EiSiMo 025d3007db Phase 3: event creation flow (GPS + success / error)
GpsService runs a single-shot position request with a hard timeout
and early-exit when Config.GPS_TARGET_ACCURACY_M is reached.
LoadingView renders a circular progress bar around the edge plus the
"Standort wird bestimmt" prompt; on callback it persists a new Event
via EventStore.add and transitions to SuccessView (green checkmark,
short vibration, auto-close) or ErrorView (red alert, 3× vibration,
German message, longer hold). TextUtils extracts the shared
multi-line centered text rendering so MenuView, LoadingView and
ErrorView all render wrapped German text consistently. Positioning
permission added to manifest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:36:22 +02:00

102 lines
3.6 KiB
MonkeyC

import Toybox.Application;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
import Toybox.WatchUi;
// Rotating 10-icon ring modelled after the watch Controls menu.
// Selection point is fixed at the 3 o'clock position (START/STOP
// button height). UP/DOWN input rotates the ring.
class MenuView extends WatchUi.View {
const STORAGE_KEY = "menu_idx";
private var _items as Array<Dictionary>;
private var _selectedIndex as Number = 0;
private var _bitmaps as Dictionary = {};
function initialize() {
View.initialize();
_items = Config.menuItems();
var stored = Application.Storage.getValue(STORAGE_KEY);
if (stored instanceof Number && stored >= 0 && stored < _items.size()) {
_selectedIndex = stored;
}
}
function onLayout(dc as Dc) as Void {
for (var i = 0; i < _items.size(); i++) {
var iconId = _items[i][:icon];
_bitmaps[iconId] = WatchUi.loadResource(iconId) as BitmapResource;
}
}
function rotateNext() as Void {
_selectedIndex = (_selectedIndex + 1) % _items.size();
Application.Storage.setValue(STORAGE_KEY, _selectedIndex);
WatchUi.requestUpdate();
}
function rotatePrev() as Void {
_selectedIndex = (_selectedIndex - 1 + _items.size()) % _items.size();
Application.Storage.setValue(STORAGE_KEY, _selectedIndex);
WatchUi.requestUpdate();
}
function selectedItem() as Dictionary {
return _items[_selectedIndex];
}
function onUpdate(dc as Dc) as Void {
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
dc.clear();
var cx = LayoutMetrics.centerX(dc);
var cy = LayoutMetrics.centerY(dc);
var radius = LayoutMetrics.ringRadius(dc);
var baseSize = LayoutMetrics.iconSize(dc);
var selSize = LayoutMetrics.selectedIconSize(dc);
var n = _items.size();
var selectionAngle = Config.SELECTION_ANGLE_DEG * Math.PI / 180.0;
for (var i = 0; i < n; i++) {
var offset = i - _selectedIndex;
var angle = selectionAngle + (2.0 * Math.PI * offset) / n;
var x = (cx + radius * Math.cos(angle)).toNumber();
var y = (cy + radius * Math.sin(angle)).toNumber();
var isSelected = (i == _selectedIndex);
var targetSize = isSelected ? selSize : baseSize;
var iconId = _items[i][:icon];
var bmp = _bitmaps[iconId] as BitmapResource;
if (bmp != null) {
_drawScaledIcon(dc, bmp, x, y, targetSize);
}
if (isSelected) {
dc.setPenWidth(LayoutMetrics.accentPenWidth(dc));
dc.setColor(Config.COLOR_ACCENT, Config.COLOR_BG);
dc.drawCircle(x, y, targetSize / 2 + 5);
}
}
var lines = _items[_selectedIndex][:lines] as Array<String>;
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
TextUtils.drawCentered(dc, lines, cx, cy, Graphics.FONT_TINY);
}
// Scales a bitmap to the requested pixel size and draws it centered
// at (cx, cy). Uses drawBitmap2 so icons stay crisp on any resolution.
private function _drawScaledIcon(dc as Dc, bmp as BitmapResource,
cx as Number, cy as Number,
targetSize as Number) as Void {
var bmpW = bmp.getWidth();
var scale = targetSize.toFloat() / bmpW.toFloat();
dc.drawBitmap2(
cx - targetSize / 2,
cy - targetSize / 2,
bmp,
{ :scaleX => scale, :scaleY => scale }
);
}
}