diff --git a/.gitignore b/.gitignore index 219983d..ec71b99 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .settings *.swp +developer_key.* +properties.mk \ No newline at end of file diff --git a/README.md b/README.md index 694aed0..dfa2d58 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Pomodoro for Garmin devices using Connect IQ ## Development To run the project, you can either import the project into Eclipse the usual way. Or use the `Makefile`: + * Copy `properties.mk.example` to `properties.mk` * Edit `properties.mk` file and make sure the paths there are valid on your computer. Change the `DEVICE` variable if you want/need. * Run `make run` to build the project and run the Connect IQ simulator on the chosen `DEVICE`. diff --git a/properties.mk b/properties.mk.example similarity index 100% rename from properties.mk rename to properties.mk.example diff --git a/source/GarmodoroApp.mc b/source/GarmodoroApp.mc index 4512b7c..474cbf2 100644 --- a/source/GarmodoroApp.mc +++ b/source/GarmodoroApp.mc @@ -1,20 +1,18 @@ using Toybox.Application as App; -using Toybox.Timer as Timer; +using Pomodoro; class GarmodoroApp extends App.AppBase { function initialize() { AppBase.initialize(); + Pomodoro.initialize(); } function onStart(state) { - timer = new Timer.Timer(); - tickTimer = new Timer.Timer(); } function onStop(state) { - tickTimer.stop(); - timer.stop(); + Pomodoro.stopTimers(); } function getInitialView() { diff --git a/source/GarmodoroDelegate.mc b/source/GarmodoroDelegate.mc index f0d4af4..f65de74 100644 --- a/source/GarmodoroDelegate.mc +++ b/source/GarmodoroDelegate.mc @@ -1,83 +1,24 @@ using Toybox.Application as App; -using Toybox.Attention as Attention; using Toybox.WatchUi as Ui; - -var timer; -var tickTimer; -var minutes = 0; -var pomodoroNumber = 1; -var isPomodoroTimerStarted = false; -var isBreakTimerStarted = false; - -function ping( dutyCycle, length ) { - if ( Attention has :vibrate ) { - Attention.vibrate( [ new Attention.VibeProfile( dutyCycle, length ) ] ); - } -} - -function play( tone ) { - if ( Attention has :playTone && ! App.getApp().getProperty( "muteSounds" ) ) { - Attention.playTone( tone ); - } -} - -function isLongBreak() { - return ( pomodoroNumber % App.getApp().getProperty( "numberOfPomodorosBeforeLongBreak" ) ) == 0; -} - -function resetMinutes() { - minutes = App.getApp().getProperty( "pomodoroLength" ); -} +using Pomodoro; class GarmodoroDelegate extends Ui.BehaviorDelegate { function initialize() { Ui.BehaviorDelegate.initialize(); } - function pomodoroCallback() { - minutes -= 1; - - if ( minutes == 0 ) { - play( 10 ); // Attention.TONE_LAP - ping( 100, 1500 ); - tickTimer.stop(); - timer.stop(); - isPomodoroTimerStarted = false; - minutes = App.getApp().getProperty( isLongBreak() ? "longBreakLength" : "shortBreakLength" ); - - timer.start( method( :breakCallback ), 60 * 1000, true ); - isBreakTimerStarted = true; - } - - Ui.requestUpdate(); + function onBack() { + Ui.popView( Ui.SLIDE_RIGHT ); + return true; } - function breakCallback() { - minutes -= 1; - - if ( minutes == 0 ) { - play( 7 ); // Attention.TONE_INTERVAL_ALERT - ping( 100, 1500 ); - timer.stop(); - - isBreakTimerStarted = false; - pomodoroNumber += 1; - resetMinutes(); + function onSelect() { + if ( Pomodoro.isInReadyState() ) { + Pomodoro.transitionToState( Pomodoro.stateRunning ); + Ui.requestUpdate(); + } else { // pomodoro is in running or break state + onMenu(); } - - Ui.requestUpdate(); - } - - function shouldTick() { - return App.getApp().getProperty( "tickStrength" ) > 0; - } - - function tickCallback() { - ping( App.getApp().getProperty( "tickStrength" ), App.getApp().getProperty( "tickDuration" ) ); - } - - function onBack() { - Ui.popView( Ui.SLIDE_RIGHT ); return true; } @@ -89,23 +30,10 @@ class GarmodoroDelegate extends Ui.BehaviorDelegate { return true; } - function onSelect() { - if ( isBreakTimerStarted || isPomodoroTimerStarted ) { - Ui.pushView( new Rez.Menus.StopMenu(), new StopMenuDelegate(), Ui.SLIDE_UP ); - return true; - } - - play( 1 ); // Attention.TONE_START - ping( 75, 1500 ); - resetMinutes(); - timer.start( method( :pomodoroCallback ), 60 * 1000, true ); - if ( me.shouldTick() ) { - tickTimer.start( method( :tickCallback ), 1000, true ); - } - isPomodoroTimerStarted = true; - - Ui.requestUpdate(); - + // also called from onSelect() when Pomodoro running or in break + function onMenu() { + Ui.pushView( new Rez.Menus.StopMenu(), + new StopMenuDelegate(), Ui.SLIDE_UP ); return true; } } diff --git a/source/GarmodoroView.mc b/source/GarmodoroView.mc index 72454c9..283921d 100644 --- a/source/GarmodoroView.mc +++ b/source/GarmodoroView.mc @@ -4,6 +4,7 @@ using Toybox.System as System; using Toybox.Time; using Toybox.Time.Gregorian; using Toybox.Lang; +using Pomodoro; class GarmodoroView extends Ui.View { hidden var pomodoroSubtitle; @@ -11,9 +12,10 @@ class GarmodoroView extends Ui.View { hidden var longBreakLabel; hidden var readyLabel; + // all elements are centered in X direction hidden var centerX; - hidden var centerY; + // all offsets are in Y direction hidden var pomodoroOffset; hidden var captionOffset; hidden var readyLabelOffset; @@ -24,80 +26,128 @@ class GarmodoroView extends Ui.View { View.initialize(); } - function onLayout( dc ) { + hidden function loadResources() { pomodoroSubtitle = Ui.loadResource( Rez.Strings.PomodoroSubtitle ); shortBreakLabel = Ui.loadResource( Rez.Strings.ShortBreakLabel ); longBreakLabel = Ui.loadResource( Rez.Strings.LongBreakLabel ); readyLabel = Ui.loadResource( Rez.Strings.ReadyLabel ); + } - var height = dc.getHeight(); - centerX = dc.getWidth() / 2; - centerY = height / 2; - var mediumOffset = Gfx.getFontHeight( Gfx.FONT_MEDIUM ); - var mediumOffsetHalf = mediumOffset / 2; - var mildOffset = Gfx.getFontHeight( Gfx.FONT_NUMBER_MILD ); - var screenShape = System.getDeviceSettings().screenShape; + hidden function calculateDrawingPositions( dc ) { + me.centerX = dc.getWidth() / 2; - me.timeOffset = height - mildOffset; + // offsets relative to the top and bottom of the watch face me.pomodoroOffset = 5; - if ( System.SCREEN_SHAPE_RECTANGLE != screenShape ) { - me.pomodoroOffset += mediumOffset; + + var heightOfFontMild = Gfx.getFontHeight( Gfx.FONT_NUMBER_MILD ); + me.timeOffset = dc.getHeight() - heightOfFontMild; + + // offsets relative to the center + var centerY = dc.getHeight() / 2; + var heightOfFontLarge = Gfx.getFontHeight( Gfx.FONT_LARGE ); + me.readyLabelOffset = centerY - heightOfFontLarge /2; + + var heightOfFontHot = Gfx.getFontHeight( Gfx.FONT_NUMBER_THAI_HOT ); + me.minutesOffset = centerY - heightOfFontHot / 2; + + var heightOfFontTiny = Gfx.getFontHeight( Gfx.FONT_TINY ); + me.captionOffset = me.timeOffset - heightOfFontTiny; + + me.adjustOffsetsForRoundScreen(); + } + + // 'special' case: non rectangular screens + hidden function adjustOffsetsForRoundScreen() { + var screenShape = System.getDeviceSettings().screenShape; + if ( screenShape != System.SCREEN_SHAPE_RECTANGLE ) { + var heightOfFontMedium = Gfx.getFontHeight( Gfx.FONT_MEDIUM ); + me.pomodoroOffset += heightOfFontMedium; + me.timeOffset -= 5; } + } - me.readyLabelOffset = me.centerY - ( Gfx.getFontHeight( Gfx.FONT_LARGE ) / 2 ); - me.minutesOffset = me.centerY - ( Gfx.getFontHeight( Gfx.FONT_NUMBER_THAI_HOT ) / 2 ); - me.captionOffset = me.timeOffset - Gfx.getFontHeight( Gfx.FONT_TINY ); + function onLayout( dc ) { + me.loadResources(); + me.calculateDrawingPositions( dc ); } function onShow() { } + function onHide() { + } + function onUpdate( dc ) { - dc.setColor( Gfx.COLOR_TRANSPARENT, Gfx.COLOR_BLACK ); - dc.clear(); - if ( isBreakTimerStarted ) { - dc.setColor( Gfx.COLOR_GREEN, Gfx.COLOR_TRANSPARENT ); - dc.drawText( me.centerX, me.pomodoroOffset, Gfx.FONT_MEDIUM, isLongBreak() ? me.longBreakLabel : me.shortBreakLabel, Gfx.TEXT_JUSTIFY_CENTER ); - me.drawMinutes( dc ); - - dc.setColor( Gfx.COLOR_DK_GREEN, Gfx.COLOR_TRANSPARENT ); - me.drawCaption( dc ); - } else if ( isPomodoroTimerStarted ) { - dc.setColor( Gfx.COLOR_YELLOW, Gfx.COLOR_TRANSPARENT ); - me.drawMinutes( dc ); - dc.setColor( Gfx.COLOR_ORANGE, Gfx.COLOR_TRANSPARENT ); - me.drawCaption( dc ); - } else { - dc.setColor( Gfx.COLOR_ORANGE, Gfx.COLOR_TRANSPARENT ); - dc.drawText( me.centerX, me.readyLabelOffset, Gfx.FONT_LARGE, me.readyLabel, Gfx.TEXT_JUSTIFY_CENTER ); + me.drawBackground( dc, Gfx.COLOR_BLACK ); + + if ( Pomodoro.isInBreakState() ) { + me.drawBreakLabel( dc, Gfx.COLOR_GREEN ); + me.drawMinutes( dc, Gfx.COLOR_GREEN ); + me.drawCaption( dc, Gfx.COLOR_DK_GREEN ); + } else if ( Pomodoro.isInRunningState() ) { + me.drawPomodoroLabel( dc, Gfx.COLOR_LT_GRAY ); + me.drawMinutes( dc, Gfx.COLOR_YELLOW ); + me.drawCaption( dc, Gfx.COLOR_ORANGE ); + } else { // Pomodoro is in ready state + me.drawPomodoroLabel( dc, Gfx.COLOR_LT_GRAY ); + me.drawReadyLabel( dc, Gfx.COLOR_ORANGE ); } - if ( ! isBreakTimerStarted ) { - dc.setColor( Gfx.COLOR_LT_GRAY, Gfx.COLOR_TRANSPARENT ); - dc.drawText( me.centerX, me.pomodoroOffset, Gfx.FONT_MEDIUM, "Pomodoro #" + pomodoroNumber, Gfx.TEXT_JUSTIFY_CENTER ); - } + me.drawTime( dc, Gfx.COLOR_LT_GRAY ); + } + + hidden function drawBackground( dc, backgroundColor ) { + dc.setColor( Gfx.COLOR_TRANSPARENT, backgroundColor ); + dc.clear(); + } - dc.setColor( Gfx.COLOR_LT_GRAY, Gfx.COLOR_TRANSPARENT ); - dc.drawText( self.centerX, self.timeOffset, Gfx.FONT_NUMBER_MILD, self.getTime(), Gfx.TEXT_JUSTIFY_CENTER ); + hidden function drawPomodoroLabel( dc, foregroundColor ) { + var pomodoroLabel = "Pomodoro #" + Pomodoro.getIteration(); + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.pomodoroOffset, Gfx.FONT_MEDIUM, + pomodoroLabel, Gfx.TEXT_JUSTIFY_CENTER ); } - hidden function drawMinutes( dc ) { - dc.drawText( me.centerX, me.minutesOffset, Gfx.FONT_NUMBER_THAI_HOT, minutes.format( "%02d" ), Gfx.TEXT_JUSTIFY_CENTER ); + hidden function drawBreakLabel( dc, foregroundColor ) { + var labelForBreak = Pomodoro.isLongBreak() ? + me.longBreakLabel : + me.shortBreakLabel; + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.pomodoroOffset, Gfx.FONT_MEDIUM, + labelForBreak, Gfx.TEXT_JUSTIFY_CENTER ); } - hidden function drawCaption( dc ) { - dc.drawText( me.centerX, me.captionOffset, Gfx.FONT_TINY, me.pomodoroSubtitle, Gfx.TEXT_JUSTIFY_CENTER ); + hidden function drawReadyLabel( dc, foregroundColor ) { + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.readyLabelOffset, Gfx.FONT_LARGE, + me.readyLabel, Gfx.TEXT_JUSTIFY_CENTER ); } - function onHide() { + hidden function drawMinutes( dc, foregroundColor ) { + var minutesAsText = Pomodoro.getMinutesLeft(); + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.minutesOffset, Gfx.FONT_NUMBER_THAI_HOT, + minutesAsText, Gfx.TEXT_JUSTIFY_CENTER ); + } + + hidden function drawCaption( dc, foregroundColor ) { + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.captionOffset, Gfx.FONT_TINY, + me.pomodoroSubtitle, Gfx.TEXT_JUSTIFY_CENTER ); + } + + hidden function drawTime( dc, foregroundColor ) { + dc.setColor( foregroundColor, Gfx.COLOR_TRANSPARENT ); + dc.drawText( me.centerX, me.timeOffset, Gfx.FONT_NUMBER_MILD, + me.getTime(), Gfx.TEXT_JUSTIFY_CENTER ); } - function getTime() { + hidden function getTime() { var today = Gregorian.info( Time.now(), Time.FORMAT_SHORT ); return Lang.format( "$1$:$2$", [ - today.hour.format( "%02d" ), - today.min.format( "%02d" ), - ] ); + today.hour.format( "%02d" ), + today.min.format( "%02d" ), + ] ); } } diff --git a/source/Pomodoro.mc b/source/Pomodoro.mc new file mode 100644 index 0000000..d5b85d5 --- /dev/null +++ b/source/Pomodoro.mc @@ -0,0 +1,164 @@ +using Toybox.Application as App; +using Toybox.WatchUi as Ui; +using Toybox.Attention as Attention; +using Toybox.Timer as Timer; +using Toybox.Lang as Lang; + +// core Pomodoro functionality is a singleton, hence no class +module Pomodoro { + var minuteTimer; + var tickTimer; + // pomodoro states: ready -> running -> break -> ready ... + enum { + stateReady, + stateRunning, + stateBreak + } + var currentState = stateReady; + var pomodoroIteration = 1; + var minutesLeft = 0; + // cached app properties to reduce battery load + var tickStrength; + var tickDuration; + + // called when app is started for the first time + function initialize() { + tickStrength = App.getApp().getProperty( "tickStrength" ); + tickDuration = App.getApp().getProperty( "tickDuration" ); + + minuteTimer = new Timer.Timer(); + tickTimer = new Timer.Timer(); + // continuously refreshes current time displayed on watch + beginMinuteCountdown(); + } + + function vibrate( dutyCycle, length ) { + if ( Attention has :vibrate ) { + Attention.vibrate([ new Attention.VibeProfile( + dutyCycle, length ) ] ); + } + } + + // if not muted + function playAttentionTone( tone ) { + var isMuted = App.getApp().getProperty( "muteSounds" ); + if ( ! isMuted && Attention has :playTone ) { + Attention.playTone( tone ); + } + } + + function isInBreakState() { + return currentState == stateBreak; + } + + function isInRunningState() { + return currentState == stateRunning; + } + + function isInReadyState() { + return currentState == stateReady; + } + + function isLongBreak() { + var groupLength = App.getApp().getProperty( + "numberOfPomodorosBeforeLongBreak" ); + return ( pomodoroIteration % groupLength ) == 0; + } + + function resetMinutesForBreak() { + var breakVariant = isLongBreak() ? + "longBreakLength" : + "shortBreakLength"; + minutesLeft = App.getApp().getProperty( breakVariant ); + } + + function resetMinutesForPomodoro() { + minutesLeft = App.getApp().getProperty( "pomodoroLength" ); + } + + // for GarmodoroView + function getMinutesLeft() { + return minutesLeft.format( "%02d" ); + } + + // for GarmodoroView + function getIteration() { + return pomodoroIteration; + } + + // called by StopMenuDelegate + function resetFromMenu() { + playAttentionTone( 9 ); // Attention.TONE_RESET + vibrate( 50, 1500 ); + + pomodoroIteration = 0; + transitionToState( stateReady ); + } + + function countdownMinutes() { + minutesLeft -= 1; + + if ( minutesLeft == 0 ) { + if( isInRunningState() ) { + transitionToState( stateBreak ); + } else if (isInBreakState()) { + transitionToState( stateReady ); + } else { + // nothing to do in ready state + } + } + + Ui.requestUpdate(); + } + + function beginMinuteCountdown() { + var countdown = new Lang.Method(Pomodoro, :countdownMinutes); + minuteTimer.start( countdown, 60 * 1000, true ); + } + + function makeTickingSound() { + vibrate( tickStrength, tickDuration ); + } + + function shouldTick() { + return App.getApp().getProperty( "tickStrength" ) > 0; + } + + // one tick every second + function beginTickingIfEnabled() { + if ( shouldTick() ) { + var makeTick = new Lang.Method(Pomodoro, :makeTickingSound); + tickTimer.start( makeTick, 1000, true ); + } + } + + function stopTimers() { + tickTimer.stop(); + minuteTimer.stop(); + } + + function transitionToState( targetState ) { + stopTimers(); + currentState = targetState; + + if( targetState == stateReady ) { + playAttentionTone( 7 ); // Attention.TONE_INTERVAL_ALERT + vibrate( 100, 1500 ); + + pomodoroIteration += 1; + } else if( targetState== stateRunning ) { + playAttentionTone( 1 ); // Attention.TONE_START + vibrate( 75, 1500 ); + + resetMinutesForPomodoro(); + beginTickingIfEnabled(); + } else { // targetState == stateBreak + playAttentionTone( 10 ); // Attention.TONE_LAP + vibrate( 100, 1500 ); + + resetMinutesForBreak(); + } + + beginMinuteCountdown(); + } +} diff --git a/source/StopMenuDelegate.mc b/source/StopMenuDelegate.mc index c07936d..e9dbc6e 100644 --- a/source/StopMenuDelegate.mc +++ b/source/StopMenuDelegate.mc @@ -1,6 +1,7 @@ using Toybox.Attention as Attention; using Toybox.System as System; using Toybox.WatchUi as Ui; +using Pomodoro; class StopMenuDelegate extends Ui.MenuInputDelegate { function initialize() { @@ -9,16 +10,7 @@ class StopMenuDelegate extends Ui.MenuInputDelegate { function onMenuItem( item ) { if ( item == :restart ) { - play( 9 ); // Attention.TONE_RESET - ping( 50, 1500 ); - - tickTimer.stop(); - timer.stop(); - - resetMinutes(); - pomodoroNumber = 1; - isPomodoroTimerStarted = false; - isBreakTimerStarted = false; + Pomodoro.resetFromMenu(); Ui.requestUpdate(); } else if ( item == :exit ) {