diff --git a/README.adoc b/README.adoc index e053618..4267a30 100644 --- a/README.adoc +++ b/README.adoc @@ -2,6 +2,8 @@ DMenu.mc is an implementation of a dynamic menu where the menu ordering and length can be changed and an optional value line can be shown below the menu item name. +image:misc/dmenu_f3.png[] + It is similar to how the Alarm Clock menu (menu -> Clock -> Alarm Clock) works. A rather trivial app is included to demonstrate its use. Every other alarm will show an icon and selecting an alarm will toggle the On/Off status. diff --git a/manifest.xml b/manifest.xml index ac33095..d1292b5 100644 --- a/manifest.xml +++ b/manifest.xml @@ -4,6 +4,7 @@ + diff --git a/misc/dmenu_f3.png b/misc/dmenu_f3.png new file mode 100644 index 0000000..83a28de Binary files /dev/null and b/misc/dmenu_f3.png differ diff --git a/source/AlarmDelegate.mc b/source/AlarmDelegate.mc index f199c1e..c45b9ca 100644 --- a/source/AlarmDelegate.mc +++ b/source/AlarmDelegate.mc @@ -19,7 +19,7 @@ class AlarmDelegate extends Ui.MenuInputDelegate { { if (item.id == :AddAlarm) { - var time = [Math.rand () % 12, Math.rand () % 60]; + var time = [Math.rand () % 12, (Math.rand () % 60).format("%02d")]; var timeStr = Lang.format("$1$:$2$ am", time); var on = [true, false][Math.rand () % 2]; diff --git a/source/DMenu.mc b/source/DMenu.mc index 969a430..7089573 100644 --- a/source/DMenu.mc +++ b/source/DMenu.mc @@ -146,32 +146,36 @@ class DMenu extends Ui.View //const ANIM_TIME = 0.3; const ANIM_TIME = 0; //disable animation - function updateIndex (offset) + + // NOTE: animation seems to be broken on physical devices like 630 (and maybe its + // siblings like 230, 235, and 735XT) + // https://forums.garmin.com/forum/developers/connect-iq/158754- + // + // I don't recommend enabling animation unless you are sure it will work + // on the devices you are targetting. + function updateIndex (offset, wrap) { if (menuArray.size () <= 1) { return; } - if (offset == 1) + nextIndex = index + offset; + if (!wrap && (nextIndex < 0 || nextIndex >= menuArray.size())) { - // Scroll down. Use 1000 as end value as cannot use 1. Scale as necessary in draw call. - if (ANIM_TIME > 0) - { - Ui.animate (drawMenu, :t, Ui.ANIM_TYPE_LINEAR, 1000, 0, ANIM_TIME, method(:animateComplete)); - } + nextIndex = index; + return; } - else + + // Don't animate anything if we are wrap scrolling past the bottom + // It just looks weird no matter what + if (!(offset > 0 && nextIndex >= menuArray.size()) && ANIM_TIME > 0) { - // Scroll up. - if (ANIM_TIME > 0) - { - Ui.animate (drawMenu, :t, Ui.ANIM_TYPE_LINEAR, -1000, 0, ANIM_TIME, method(:animateComplete)); - } + // Scroll up/down. Use +/-1000 as end value as cannot use 1. Scale as necessary in draw call. + drawMenu.t = offset * 1000; + Ui.animate (drawMenu, :t, Ui.ANIM_TYPE_LINEAR, drawMenu.t, 0, ANIM_TIME, method(:animateComplete)); } - nextIndex = index + offset; - // Cope with a 'feature' in modulo operator not handling -ve numbers as desired. nextIndex = nextIndex < 0 ? menuArray.size () + nextIndex : nextIndex; @@ -207,14 +211,10 @@ class DMenu extends Ui.View drawMenu.draw (dc); - // Draw the decorations. - var h3 = height / 3; - dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE); - dc.setPenWidth (2); - dc.drawLine (0, h3, width, h3); - dc.drawLine (0, h3 * 2, width, h3 * 2); - - drawArrows (dc); + if (drawMenu.t == 0) + { + drawArrows (dc); + } } const GAP = 5; @@ -259,6 +259,9 @@ class DrawMenu extends Ui.Drawable { const TITLE_FONT = Gfx.FONT_SMALL; + var animateSeparators = true; // 935 animates separators, older watches like 230 and fenix 3 don't + var fillEmptySpace = true; // 935 fills unused space with black, fenix 3 doesn't + var t = 0; // 'time' in the animation cycle 0...1000 or -1000...0. var index, nextIndex, menu; @@ -282,10 +285,50 @@ class DrawMenu extends Ui.Drawable // Depending on where we are in the menu and in the animation some of // these will be unnecessary but it is easier to draw everything and // rely on clipping to avoid unnecessary drawing calls. - drawTitle (dc, y - nextIndex * h3 - h3); + + // Don't animate the title if we're wrap scrolling + var titleY; + if ((nextIndex == 0 && t < 0) || (nextIndex == 1)) // normal scrolling + { + titleY = y; + } + else // wrap scroll + { + titleY = h3; + } + drawTitle (dc, titleY - nextIndex * h3 - h3); + for (var i = -2; i < 3; i++) { drawItem (dc, nextIndex + i, y + h3 * i, i == 0); + + } + + dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE); + dc.setPenWidth(2); + + var separatorY = animateSeparators ? y : h3; + + /// Line above first item that is only visible + // when animating + if (t > 0 && nextIndex != 0) + { + dc.drawLine(0, separatorY - h3, dc.getWidth(), separatorY - h3); + } + + // Line below first item + if (nextIndex > 0 || !animateSeparators) + { + dc.drawLine(0, separatorY, dc.getWidth(), separatorY); + } + + // Line below second item + dc.drawLine(0, separatorY + h3, dc.getWidth(), separatorY + h3); + + // Line below third item that is only visible when animating + if (t < 0 && nextIndex != menu.menuArray.size()-1) + { + dc.drawLine(0, separatorY + 2 * h3, dc.getWidth(), separatorY + 2 * h3); } } @@ -300,8 +343,9 @@ class DrawMenu extends Ui.Drawable return; } - dc.setColor (Gfx.COLOR_BLACK, Gfx.COLOR_WHITE); - dc.fillRectangle (0, y, width, h3); + dc.setColor (Gfx.COLOR_BLACK, Gfx.COLOR_WHITE); +// dc.fillRectangle (0, y, width, h3); + dc.fillRectangle (0, 0, width, y+h3); // this looks better if we were animating title, which we're not if (menu.title != null) { @@ -315,11 +359,22 @@ class DrawMenu extends Ui.Drawable // highlight is the selected menu item that can optionally show a value. function drawItem (dc, idx, y, highlight) { - var h3 = dc.getHeight () / 3; + var height = dc.getHeight(); + var h3 = height / 3; + + if (idx >= menu.menuArray.size()) + { + if (fillEmptySpace) + { + dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE); + dc.fillRectangle (0, y, dc.getWidth(), height); // using height instead of (height-y) because of rounding issues when animating + } + return; + } // Cannot see item if it doesn't exist or will not be visible. - if (idx < 0 || idx >= menu.menuArray.size () || - menu.menuArray[idx] == null || y > dc.getHeight () || y < -h3) + if (idx < 0 || + menu.menuArray[idx] == null || y > height || y < -h3) { return; } @@ -345,15 +400,14 @@ class DMenuDelegate extends Ui.BehaviorDelegate var d = swipeEvent.getDirection(); if (d == WatchUi.SWIPE_UP) { - return onNextPage(); + return onNextPage_touch(); } if (d == WatchUi.SWIPE_DOWN) { - return onPreviousPage(); + return onPreviousPage_touch(); } return false; - } function onTap(clickEvent) @@ -368,11 +422,11 @@ class DMenuDelegate extends Ui.BehaviorDelegate var h3 = menu.menuHeight / 3; if (c[1] > h3*2) { - return onNextPage(); + return onNextPage_touch(); } else if (c[1] < h3) { - return onPreviousPage(); + return onPreviousPage_touch(); } } @@ -383,20 +437,51 @@ class DMenuDelegate extends Ui.BehaviorDelegate return false; } + + protected function onNextPage_touch() + { + menu.updateIndex(1, false); + return true; + } - - function onNextPage() + protected function onPreviousPage_touch() { - menu.updateIndex (1); + menu.updateIndex(-1, false); + return true; + } + + function onNextPage_key() + { + menu.updateIndex(1, true); return true; } - function onPreviousPage () + function onPreviousPage_key() { - menu.updateIndex (-1); + menu.updateIndex(-1, true); return true; } + // Sadly we can't handle onNextPage/onPreviousPage(), because + // device behaviour is inconsistent: + // - Vivoactive, VAHR and 630 trigger these behaviours on left/right swipe + // (which doesn't make sense for vertical menu) + // - Vivoactive does not even support up/down swipe, so tap support + // is mandatory + // - VA3 triggers these behaviour on up/down swipe + // + // The only thing to do is to handle the keys, swipes, and taps + // directly + function onNextPage() + { + return false; + } + + function onPreviousPage() + { + return false; + } + function onSelect () { return false; @@ -405,6 +490,16 @@ class DMenuDelegate extends Ui.BehaviorDelegate function onKey(keyEvent) { var k = keyEvent.getKey(); + if (k == WatchUi.KEY_UP) + { + return onPreviousPage_key(); + } + if (k == WatchUi.KEY_DOWN) + { + return onNextPage_key(); + } + + if (k == WatchUi.KEY_START || k == WatchUi.KEY_ENTER ) { userMenuDelegate.onMenuItem (menu.selectedItem ());