diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml index 67da7a7..d0febbd 100644 --- a/resources/drawables/drawables.xml +++ b/resources/drawables/drawables.xml @@ -1,13 +1,13 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/resources/drawables/icon_arrest.svg b/resources/drawables/icon_arrest.svg index 0c1a65c..e818da7 100644 --- a/resources/drawables/icon_arrest.svg +++ b/resources/drawables/icon_arrest.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_arrival.svg b/resources/drawables/icon_arrival.svg index eec8e37..6a163a5 100644 --- a/resources/drawables/icon_arrival.svg +++ b/resources/drawables/icon_arrival.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_delete.svg b/resources/drawables/icon_delete.svg index 6f98e07..1e85d1b 100644 --- a/resources/drawables/icon_delete.svg +++ b/resources/drawables/icon_delete.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_end.svg b/resources/drawables/icon_end.svg index f9987a9..c01eea4 100644 --- a/resources/drawables/icon_end.svg +++ b/resources/drawables/icon_end.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_event.svg b/resources/drawables/icon_event.svg index dc842c3..9735068 100644 --- a/resources/drawables/icon_event.svg +++ b/resources/drawables/icon_event.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_evidence.svg b/resources/drawables/icon_evidence.svg index 378ac70..9fd1be6 100644 --- a/resources/drawables/icon_evidence.svg +++ b/resources/drawables/icon_evidence.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_force.svg b/resources/drawables/icon_force.svg index ff0ad00..01c683b 100644 --- a/resources/drawables/icon_force.svg +++ b/resources/drawables/icon_force.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_history.svg b/resources/drawables/icon_history.svg index e93fb6e..9f09c0e 100644 --- a/resources/drawables/icon_history.svg +++ b/resources/drawables/icon_history.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_sighting.svg b/resources/drawables/icon_sighting.svg index 4a31eac..e222e16 100644 --- a/resources/drawables/icon_sighting.svg +++ b/resources/drawables/icon_sighting.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/drawables/icon_start.svg b/resources/drawables/icon_start.svg index 009b1c4..09b8731 100644 --- a/resources/drawables/icon_start.svg +++ b/resources/drawables/icon_start.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index de7f99f..1ba4c4d 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -1,18 +1,6 @@ Einsatzprotokoll - - Verlauf - Ereignis - Einsatzbeginn - Einsatzende - Eintreffen - Festnahme - Zwanganwendung - Beweismittel - Sichtung - Letzten löschen - Standort wird bestimmt Standort konnte nicht bestimmt werden diff --git a/source/Config.mc b/source/Config.mc index 73e2f9a..e3a86db 100644 --- a/source/Config.mc +++ b/source/Config.mc @@ -12,6 +12,13 @@ module Config { const GPS_TIMEOUT_MS = 10000; 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 ---------------------------------------- const SUCCESS_DISPLAY_MS = 2500; const ERROR_DISPLAY_MS = 5000; @@ -44,19 +51,20 @@ module Config { const ACTION_DELETE = "delete"; // 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 { return [ - { :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :label => Rez.Strings.menu_history, :color => 0xFFFFFF }, - { :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :label => Rez.Strings.menu_general, :color => 0xFFAA00 }, - { :key => EVENT_START, :icon => Rez.Drawables.IconStart, :label => Rez.Strings.menu_start, :color => 0x00FF00 }, - { :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :label => Rez.Strings.menu_end, :color => 0x00AAFF }, - { :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :label => Rez.Strings.menu_arrival, :color => 0xFFFF00 }, - { :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :label => Rez.Strings.menu_arrest, :color => 0xFF0088 }, - { :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :label => Rez.Strings.menu_force, :color => 0xAA00FF }, - { :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :label => Rez.Strings.menu_evidence, :color => 0x00FFFF }, - { :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :label => Rez.Strings.menu_sighting, :color => 0xFF8800 }, - { :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :label => Rez.Strings.menu_delete, :color => 0xFF2222 } + { :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :lines => ["Verlauf"], :color => 0xFFFFFF }, + { :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :lines => ["Ereignis"], :color => 0xFFAA00 }, + { :key => EVENT_START, :icon => Rez.Drawables.IconStart, :lines => ["Einsatz", "beginn"], :color => 0x00FF00 }, + { :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :lines => ["Einsatz", "ende"], :color => 0x00AAFF }, + { :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :lines => ["Eintreffen"], :color => 0xFFFF00 }, + { :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :lines => ["Festnahme"], :color => 0xFF0088 }, + { :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :lines => ["Zwang"], :color => 0xAA00FF }, + { :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :lines => ["Beweis", "mittel"], :color => 0x00FFFF }, + { :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :lines => ["Sichtung"], :color => 0xFF8800 }, + { :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :lines => ["Letzten", "löschen"], :color => 0xFF2222 } ]; } } diff --git a/source/EinsatzprotokollApp.mc b/source/EinsatzprotokollApp.mc index 39fc72a..f2db9e9 100644 --- a/source/EinsatzprotokollApp.mc +++ b/source/EinsatzprotokollApp.mc @@ -16,6 +16,7 @@ class EinsatzprotokollApp extends Application.AppBase { function onStop(state as Dictionary?) as Void {} function getInitialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] { - return [ new EinsatzprotokollView() ]; + var view = new MenuView(); + return [ view, new MenuDelegate(view) ]; } } diff --git a/source/EinsatzprotokollView.mc b/source/EinsatzprotokollView.mc deleted file mode 100644 index bcf88ca..0000000 --- a/source/EinsatzprotokollView.mc +++ /dev/null @@ -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); - } -} diff --git a/source/LayoutMetrics.mc b/source/LayoutMetrics.mc index 99b46d8..84cd88d 100644 --- a/source/LayoutMetrics.mc +++ b/source/LayoutMetrics.mc @@ -6,23 +6,35 @@ import Toybox.Lang; // Garmin display. module LayoutMetrics { - // --- Menu ring geometry -------------------------------------------- + // --- Base helpers --------------------------------------------------- function centerX(dc as Dc) as Number { return dc.getWidth() / 2; } function centerY(dc as Dc) as Number { return dc.getHeight() / 2; } - // Radius on which the 10 icons sit. - function ringRadius(dc as Dc) as Number { - var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); - return (min * 0.38).toNumber(); + function minDim(dc as Dc) as Number { + return (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); } + // --- Menu ring geometry -------------------------------------------- function iconSize(dc as Dc) as Number { - var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); - return (min * 0.14).toNumber(); + return (minDim(dc) * 0.16).toNumber(); } 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 ----------------------------------------- @@ -40,13 +52,11 @@ module LayoutMetrics { // --- Loading / delete arc radius ------------------------------------ function edgeArcRadius(dc as Dc) as Number { - var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); - return (min * 0.48).toNumber(); + return ((minDim(dc) / 2) - 4).toNumber(); } function edgeArcPenWidth(dc as Dc) as Number { - var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); - var w = (min * 0.025).toNumber(); + var w = (minDim(dc) * 0.025).toNumber(); return (w < 3) ? 3 : w; } } diff --git a/source/MenuDelegate.mc b/source/MenuDelegate.mc new file mode 100644 index 0000000..29aea85 --- /dev/null +++ b/source/MenuDelegate.mc @@ -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; + } +} diff --git a/source/MenuView.mc b/source/MenuView.mc new file mode 100644 index 0000000..c112019 --- /dev/null +++ b/source/MenuView.mc @@ -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; + 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; + 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 } + ); + } +}