Phase 2: 10-icon menu ring
Rotating ring modeled after the watch Controls menu: UP/DOWN spin,
START/STOP selects. Selection point sits at −30° (≈ 2 o'clock) so it
lines up with the physical enter button on 5-button round Garmins.
Icons are rasterized at 80×80 with automaticPalette="false" and
scaled via drawBitmap2 to stay crisp at any display resolution. Long
German compounds ("Einsatzbeginn", "Beweismittel", "Letzten löschen")
wrap to two lines via a Config array so the center label never
overlaps the surrounding icons. Selected index is persisted in
Application.Storage and restored on next launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
@ -1,13 +1,13 @@
|
||||||
<drawables>
|
<drawables>
|
||||||
<bitmap id="LauncherIcon" filename="launcher_icon.svg"/>
|
<bitmap id="LauncherIcon" filename="launcher_icon.svg"/>
|
||||||
<bitmap id="IconHistory" filename="icon_history.svg"/>
|
<bitmap id="IconHistory" filename="icon_history.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconEvent" filename="icon_event.svg"/>
|
<bitmap id="IconEvent" filename="icon_event.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconStart" filename="icon_start.svg"/>
|
<bitmap id="IconStart" filename="icon_start.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconEnd" filename="icon_end.svg"/>
|
<bitmap id="IconEnd" filename="icon_end.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconArrival" filename="icon_arrival.svg"/>
|
<bitmap id="IconArrival" filename="icon_arrival.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconArrest" filename="icon_arrest.svg"/>
|
<bitmap id="IconArrest" filename="icon_arrest.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconForce" filename="icon_force.svg"/>
|
<bitmap id="IconForce" filename="icon_force.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconEvidence" filename="icon_evidence.svg"/>
|
<bitmap id="IconEvidence" filename="icon_evidence.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconSighting" filename="icon_sighting.svg"/>
|
<bitmap id="IconSighting" filename="icon_sighting.svg" automaticPalette="false"/>
|
||||||
<bitmap id="IconDelete" filename="icon_delete.svg"/>
|
<bitmap id="IconDelete" filename="icon_delete.svg" automaticPalette="false"/>
|
||||||
</drawables>
|
</drawables>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FF0088"/>
|
<circle cx="20" cy="20" r="18" fill="#FF0088"/>
|
||||||
<circle cx="13" cy="20" r="6" fill="none" stroke="#000000" stroke-width="3"/>
|
<circle cx="13" cy="20" r="6" fill="none" stroke="#000000" stroke-width="3"/>
|
||||||
<circle cx="27" cy="20" r="6" fill="none" stroke="#000000" stroke-width="3"/>
|
<circle cx="27" cy="20" r="6" fill="none" stroke="#000000" stroke-width="3"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 361 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FFDD00"/>
|
<circle cx="20" cy="20" r="18" fill="#FFDD00"/>
|
||||||
<path d="M20 8 C14 8 10 12 10 18 C10 24 20 32 20 32 C20 32 30 24 30 18 C30 12 26 8 20 8 Z" fill="#000000"/>
|
<path d="M20 8 C14 8 10 12 10 18 C10 24 20 32 20 32 C20 32 30 24 30 18 C30 12 26 8 20 8 Z" fill="#000000"/>
|
||||||
<circle cx="20" cy="18" r="4" fill="#FFDD00"/>
|
<circle cx="20" cy="18" r="4" fill="#FFDD00"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FF2222"/>
|
<circle cx="20" cy="20" r="18" fill="#FF2222"/>
|
||||||
<rect x="11" y="14" width="18" height="3" fill="#000000"/>
|
<rect x="11" y="14" width="18" height="3" fill="#000000"/>
|
||||||
<rect x="17" y="10" width="6" height="3" fill="#000000"/>
|
<rect x="17" y="10" width="6" height="3" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#00AAFF"/>
|
<circle cx="20" cy="20" r="18" fill="#00AAFF"/>
|
||||||
<polyline points="11,21 18,28 30,14" stroke="#000000" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
<polyline points="11,21 18,28 30,14" stroke="#000000" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 275 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FFAA00"/>
|
<circle cx="20" cy="20" r="18" fill="#FFAA00"/>
|
||||||
<rect x="18" y="10" width="4" height="20" fill="#000000"/>
|
<rect x="18" y="10" width="4" height="20" fill="#000000"/>
|
||||||
<rect x="10" y="18" width="20" height="4" fill="#000000"/>
|
<rect x="10" y="18" width="20" height="4" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 263 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#00E5E5"/>
|
<circle cx="20" cy="20" r="18" fill="#00E5E5"/>
|
||||||
<rect x="8" y="14" width="24" height="16" rx="2" fill="#000000"/>
|
<rect x="8" y="14" width="24" height="16" rx="2" fill="#000000"/>
|
||||||
<rect x="15" y="11" width="10" height="4" fill="#000000"/>
|
<rect x="15" y="11" width="10" height="4" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 368 B After Width: | Height: | Size: 368 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#AA00FF"/>
|
<circle cx="20" cy="20" r="18" fill="#AA00FF"/>
|
||||||
<rect x="11" y="14" width="18" height="14" rx="2" fill="#000000"/>
|
<rect x="11" y="14" width="18" height="14" rx="2" fill="#000000"/>
|
||||||
<rect x="13" y="11" width="3" height="6" fill="#000000"/>
|
<rect x="13" y="11" width="3" height="6" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 390 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FFFFFF"/>
|
<circle cx="20" cy="20" r="18" fill="#FFFFFF"/>
|
||||||
<circle cx="11" cy="13" r="1.5" fill="#000000"/>
|
<circle cx="11" cy="13" r="1.5" fill="#000000"/>
|
||||||
<rect x="15" y="12" width="14" height="2" fill="#000000"/>
|
<rect x="15" y="12" width="14" height="2" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 477 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#FF8800"/>
|
<circle cx="20" cy="20" r="18" fill="#FF8800"/>
|
||||||
<circle cx="13" cy="22" r="6" fill="#000000"/>
|
<circle cx="13" cy="22" r="6" fill="#000000"/>
|
||||||
<circle cx="27" cy="22" r="6" fill="#000000"/>
|
<circle cx="27" cy="22" r="6" fill="#000000"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="20" cy="20" r="18" fill="#00CC33"/>
|
<circle cx="20" cy="20" r="18" fill="#00CC33"/>
|
||||||
<polygon points="15,10 30,20 15,30" fill="#000000"/>
|
<polygon points="15,10 30,20 15,30" fill="#000000"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 196 B |
|
|
@ -1,18 +1,6 @@
|
||||||
<strings>
|
<strings>
|
||||||
<string id="AppName">Einsatzprotokoll</string>
|
<string id="AppName">Einsatzprotokoll</string>
|
||||||
|
|
||||||
<!-- Menu labels -->
|
|
||||||
<string id="menu_history">Verlauf</string>
|
|
||||||
<string id="menu_general">Ereignis</string>
|
|
||||||
<string id="menu_start">Einsatzbeginn</string>
|
|
||||||
<string id="menu_end">Einsatzende</string>
|
|
||||||
<string id="menu_arrival">Eintreffen</string>
|
|
||||||
<string id="menu_arrest">Festnahme</string>
|
|
||||||
<string id="menu_force">Zwanganwendung</string>
|
|
||||||
<string id="menu_evidence">Beweismittel</string>
|
|
||||||
<string id="menu_sighting">Sichtung</string>
|
|
||||||
<string id="menu_delete">Letzten löschen</string>
|
|
||||||
|
|
||||||
<!-- Loading / result screens -->
|
<!-- Loading / result screens -->
|
||||||
<string id="loading_gps">Standort wird bestimmt</string>
|
<string id="loading_gps">Standort wird bestimmt</string>
|
||||||
<string id="error_gps">Standort konnte nicht bestimmt werden</string>
|
<string id="error_gps">Standort konnte nicht bestimmt werden</string>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ module Config {
|
||||||
const GPS_TIMEOUT_MS = 10000;
|
const GPS_TIMEOUT_MS = 10000;
|
||||||
const GPS_TARGET_ACCURACY_M = 5;
|
const GPS_TARGET_ACCURACY_M = 5;
|
||||||
|
|
||||||
|
// --- Menu ring -----------------------------------------------------
|
||||||
|
// Angle (degrees) at which the selected icon sits relative to the
|
||||||
|
// screen center. 0° = 3 o'clock, negative = upper half. −30° ≈ 2
|
||||||
|
// o'clock which lines up with the START/STOP button on most 5-
|
||||||
|
// button round Garmins (Forerunner 265, Fenix 7, Epix 2, Venu 3 …).
|
||||||
|
const SELECTION_ANGLE_DEG = -30.0;
|
||||||
|
|
||||||
// --- Animation / UI timings ----------------------------------------
|
// --- Animation / UI timings ----------------------------------------
|
||||||
const SUCCESS_DISPLAY_MS = 2500;
|
const SUCCESS_DISPLAY_MS = 2500;
|
||||||
const ERROR_DISPLAY_MS = 5000;
|
const ERROR_DISPLAY_MS = 5000;
|
||||||
|
|
@ -44,19 +51,20 @@ module Config {
|
||||||
const ACTION_DELETE = "delete";
|
const ACTION_DELETE = "delete";
|
||||||
|
|
||||||
// Menu item metadata. Order matches display order (index 0 is first
|
// Menu item metadata. Order matches display order (index 0 is first
|
||||||
// selected). Each entry: key, drawable id, string id, highlight color.
|
// selected). Long German compounds are split into two lines so the
|
||||||
|
// center label never overlaps the surrounding icons.
|
||||||
function menuItems() as Array<Dictionary> {
|
function menuItems() as Array<Dictionary> {
|
||||||
return [
|
return [
|
||||||
{ :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :label => Rez.Strings.menu_history, :color => 0xFFFFFF },
|
{ :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :lines => ["Verlauf"], :color => 0xFFFFFF },
|
||||||
{ :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :label => Rez.Strings.menu_general, :color => 0xFFAA00 },
|
{ :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :lines => ["Ereignis"], :color => 0xFFAA00 },
|
||||||
{ :key => EVENT_START, :icon => Rez.Drawables.IconStart, :label => Rez.Strings.menu_start, :color => 0x00FF00 },
|
{ :key => EVENT_START, :icon => Rez.Drawables.IconStart, :lines => ["Einsatz", "beginn"], :color => 0x00FF00 },
|
||||||
{ :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :label => Rez.Strings.menu_end, :color => 0x00AAFF },
|
{ :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :lines => ["Einsatz", "ende"], :color => 0x00AAFF },
|
||||||
{ :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :label => Rez.Strings.menu_arrival, :color => 0xFFFF00 },
|
{ :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :lines => ["Eintreffen"], :color => 0xFFFF00 },
|
||||||
{ :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :label => Rez.Strings.menu_arrest, :color => 0xFF0088 },
|
{ :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :lines => ["Festnahme"], :color => 0xFF0088 },
|
||||||
{ :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :label => Rez.Strings.menu_force, :color => 0xAA00FF },
|
{ :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :lines => ["Zwang"], :color => 0xAA00FF },
|
||||||
{ :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :label => Rez.Strings.menu_evidence, :color => 0x00FFFF },
|
{ :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :lines => ["Beweis", "mittel"], :color => 0x00FFFF },
|
||||||
{ :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :label => Rez.Strings.menu_sighting, :color => 0xFF8800 },
|
{ :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :lines => ["Sichtung"], :color => 0xFF8800 },
|
||||||
{ :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :label => Rez.Strings.menu_delete, :color => 0xFF2222 }
|
{ :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :lines => ["Letzten", "löschen"], :color => 0xFF2222 }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class EinsatzprotokollApp extends Application.AppBase {
|
||||||
function onStop(state as Dictionary?) as Void {}
|
function onStop(state as Dictionary?) as Void {}
|
||||||
|
|
||||||
function getInitialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] {
|
function getInitialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] {
|
||||||
return [ new EinsatzprotokollView() ];
|
var view = new MenuView();
|
||||||
|
return [ view, new MenuDelegate(view) ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import Toybox.Graphics;
|
|
||||||
import Toybox.Lang;
|
|
||||||
import Toybox.WatchUi;
|
|
||||||
|
|
||||||
class EinsatzprotokollView extends WatchUi.View {
|
|
||||||
|
|
||||||
function initialize() {
|
|
||||||
View.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUpdate(dc as Dc) as Void {
|
|
||||||
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
|
|
||||||
dc.clear();
|
|
||||||
var cx = dc.getWidth() / 2;
|
|
||||||
var cy = dc.getHeight() / 2;
|
|
||||||
dc.drawText(cx, cy, Graphics.FONT_MEDIUM, "Einsatzprotokoll",
|
|
||||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,23 +6,35 @@ import Toybox.Lang;
|
||||||
// Garmin display.
|
// Garmin display.
|
||||||
module LayoutMetrics {
|
module LayoutMetrics {
|
||||||
|
|
||||||
// --- Menu ring geometry --------------------------------------------
|
// --- Base helpers ---------------------------------------------------
|
||||||
function centerX(dc as Dc) as Number { return dc.getWidth() / 2; }
|
function centerX(dc as Dc) as Number { return dc.getWidth() / 2; }
|
||||||
function centerY(dc as Dc) as Number { return dc.getHeight() / 2; }
|
function centerY(dc as Dc) as Number { return dc.getHeight() / 2; }
|
||||||
|
|
||||||
// Radius on which the 10 icons sit.
|
function minDim(dc as Dc) as Number {
|
||||||
function ringRadius(dc as Dc) as Number {
|
return (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
||||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
|
||||||
return (min * 0.38).toNumber();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Menu ring geometry --------------------------------------------
|
||||||
function iconSize(dc as Dc) as Number {
|
function iconSize(dc as Dc) as Number {
|
||||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
return (minDim(dc) * 0.16).toNumber();
|
||||||
return (min * 0.14).toNumber();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedIconSize(dc as Dc) as Number {
|
function selectedIconSize(dc as Dc) as Number {
|
||||||
return (iconSize(dc) * 1.3).toNumber();
|
return (iconSize(dc) * 1.25).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Radius chosen so the accent ring around the selected icon sits
|
||||||
|
// ~5px from the display edge on any device.
|
||||||
|
function ringRadius(dc as Dc) as Number {
|
||||||
|
var half = minDim(dc) / 2;
|
||||||
|
var selHalf = selectedIconSize(dc) / 2;
|
||||||
|
var edgeMargin = (minDim(dc) * 0.02).toNumber() + 5;
|
||||||
|
return (half - selHalf - edgeMargin).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
function accentPenWidth(dc as Dc) as Number {
|
||||||
|
var w = (minDim(dc) * 0.01).toNumber();
|
||||||
|
return (w < 2) ? 2 : w;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- History view sections -----------------------------------------
|
// --- History view sections -----------------------------------------
|
||||||
|
|
@ -40,13 +52,11 @@ module LayoutMetrics {
|
||||||
|
|
||||||
// --- Loading / delete arc radius ------------------------------------
|
// --- Loading / delete arc radius ------------------------------------
|
||||||
function edgeArcRadius(dc as Dc) as Number {
|
function edgeArcRadius(dc as Dc) as Number {
|
||||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
return ((minDim(dc) / 2) - 4).toNumber();
|
||||||
return (min * 0.48).toNumber();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function edgeArcPenWidth(dc as Dc) as Number {
|
function edgeArcPenWidth(dc as Dc) as Number {
|
||||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
var w = (minDim(dc) * 0.025).toNumber();
|
||||||
var w = (min * 0.025).toNumber();
|
|
||||||
return (w < 3) ? 3 : w;
|
return (w < 3) ? 3 : w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
source/MenuDelegate.mc
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Toybox.Lang;
|
||||||
|
import Toybox.WatchUi;
|
||||||
|
|
||||||
|
// Routes menu input. UP/DOWN rotate the ring; START/STOP selects.
|
||||||
|
// Dispatch of event-type keys is wired up in later phases — for now
|
||||||
|
// each selection just logs and no-ops.
|
||||||
|
class MenuDelegate extends WatchUi.BehaviorDelegate {
|
||||||
|
|
||||||
|
private var _view as MenuView;
|
||||||
|
|
||||||
|
function initialize(view as MenuView) {
|
||||||
|
BehaviorDelegate.initialize();
|
||||||
|
_view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNextPage() as Boolean {
|
||||||
|
_view.rotateNext();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPreviousPage() as Boolean {
|
||||||
|
_view.rotatePrev();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelect() as Boolean {
|
||||||
|
var item = _view.selectedItem();
|
||||||
|
Logger.log("menu.select: " + (item[:key] as String));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
source/MenuView.mc
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
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);
|
||||||
|
var font = Graphics.FONT_TINY;
|
||||||
|
var lineHeight = dc.getFontHeight(font);
|
||||||
|
var totalHeight = lines.size() * lineHeight;
|
||||||
|
var startY = cy - totalHeight / 2 + lineHeight / 2;
|
||||||
|
for (var j = 0; j < lines.size(); j++) {
|
||||||
|
dc.drawText(cx, startY + j * lineHeight, font, lines[j],
|
||||||
|
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||