diff --git a/.gitignore b/.gitignore index 34d82ee0..574622dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ - +.DS_Store +*.pyc /.lock-* /build/ /out/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..14b1cdd2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +sudo: false +language: python +python: + - "2.7" + +env: + - PEBBLE_SDK_VERSION=3.9 + +before_install: + - wget https://github.com/pebble/pebble-tool/releases/download/v4.0-rc5/pebble-sdk-4.0-rc5-linux64.tar.bz2?source=travis + - mkdir -p ~/.pebble-sdk + - tar -jxf pebble-sdk-* -C ~/.pebble-sdk + - touch ~/.pebble-sdk/ENABLE_ANALYTICS + - export PEBBLE_SDK=~/.pebble-sdk/pebble-sdk-* + - export PEBBLE=$PEBBLE_SDK/bin/pebble + +install: + - pushd $PEBBLE_SDK + - virtualenv --no-site-packages .env + - source .env/bin/activate + - pip install -r requirements.txt + - deactivate + - popd + - $PEBBLE sdk set-channel beta + - yes | $PEBBLE sdk install $PEBBLE_SDK_VERSION + +script: + - $PEBBLE build diff --git a/README.md b/README.md new file mode 100644 index 00000000..44e64fde --- /dev/null +++ b/README.md @@ -0,0 +1,2096 @@ +Pebble.js +========= + +[![Build Status](https://travis-ci.org/pebble/pebblejs.svg?branch=master)](https://travis-ci.org/pebble/pebblejs) + +Pebble.js lets you write beautiful Pebble applications completely in JavaScript. + +Pebble.js applications run on your phone. They have access to all the resources of your phone (internet connectivity, GPS, almost unlimited memory, etc). Because they are written in JavaScript they are also perfect to make HTTP requests and connect your Pebble to the internet. + +**Warning:** Pebble.js is still in beta, so breaking API changes are possible. Pebble.js is best suited for prototyping and applications that inherently require communication in response to user actions, such as accessing the internet. Please be aware that as a result of Bluetooth round-trips for all actions, Pebble.js apps will use more power and respond slower to user interaction than a similar native app. + +> ![JSConf 2014](http://2014.jsconf.us/img/logo.png) +> +> Pebble.js was announced during JSConf 2014! + +## Getting Started + + * In CloudPebble + + The easiest way to use Pebble.js is in [CloudPebble](https://cloudpebble.net). Select the 'Pebble.js' project type when creating a new project. + + [Build a Pebble.js application now in CloudPebble >](https://cloudpebble.net) + + * With the Pebble SDK + + This option allows you to customize Pebble.js. Follow the [Pebble SDK installation instructions](https://developer.pebble.com/sdk/install/) to install the SDK on your computer and [fork this project](http://github.com/pebble/pebblejs) on Github. + + The main entry point for your application is in the `src/js/app.js` file. For projects with multiple files, you may move `src/js/app.js` to `src/js/app/index.js` instead and create new files under `src/js/app`. + + [Install the Pebble SDK on your computer >](http://developer.pebble.com/sdk/install/) + + +Pebble.js applications follow modern JavaScript best practices. To get started, you just need to call `require('ui')` to load the UI module and start building user interfaces. + +````js +var UI = require('ui'); +```` + +The basic block to build user interface is the [Card]. A Card is a type of [Window] that occupies the entire screen and allows you to display some text in a pre-structured way: a title at the top, a subtitle below it and a body area for larger paragraphs. Cards can be made scrollable to display large quantities of information. You can also add images next to the title, subtitle or in the body area. + +````js +var card = new UI.Card({ + title: 'Hello World', + body: 'This is your first Pebble app!', + scrollable: true +}); +```` + +After creating a card window, push it onto the screen with the `show()` method. + +````js +card.show(); +```` + +To interact with the users, use the buttons or the accelerometer. Add callbacks to a window with the `.on()` method: + +````js +card.on('click', function(e) { + card.subtitle('Button ' + e.button + ' pressed.'); +}); +```` + +Making HTTP connections is very easy with the included `ajax` library. + +````js +var ajax = require('ajax'); +ajax({ url: 'http://api.theysaidso.com/qod.json', type: 'json' }, + function(data) { + card.body(data.contents.quotes[0].quote); + card.title(data.contents.quotes[0].author); + } +); +```` + +You can do much more with Pebble.js: + + - Get accelerometer values + - Display complex UI mixing geometric elements, text and images + - Animate elements on the screen + - Display arbitrary long menus + - Use the GPS and LocalStorage on the phone + - etc! + +Keep reading for the full [API Reference]. + +## Using Images +[Using Images]: #using-images + +You can use images in your Pebble.js application. Currently all images must be embedded in your applications. They will be resized and converted to black and white when you build your project. + +We recommend that you follow these guidelines when preparing your images for Pebble: + + * Resize all images for the screen of Pebble. A fullscreen image will be 144 pixels wide by 168 pixels high. + * Use an image editor or [HyperDither](http://2002-2010.tinrocket.com/software/hyperdither/index.html) to dither your image in black and white. + * Remember that the maximum size for a Pebble application is 100kB. You will quickly reach that limit if you add too many images. + +To add an image in your application, edit the `appinfo.json` file and add your image: + +````js +{ + "type": "png", + "name": "IMAGE_CHOOSE_A_UNIQUE_IDENTIFIER", + "file": "images/your_image.png" +} +```` + +> If you are using CloudPebble, you can add images in your project configuration. + +To reference your image in Pebble.js, you can use the `name` field or the `file` field. + +````js +// These two examples are both valid ways to show the image declared above in a Card +card.icon('images/your_image.png'); +card.icon('IMAGE_CHOOSE_A_UNIQUE_IDENTIFIER'); +```` + +You can also display images with [Image] when using a dynamic [Window]. + +````js +// This is an example of using an image with Image and Window +var UI = require('ui'); +var Vector2 = require('vector2'); + +var wind = new UI.Window({ fullscreen: true }); +var image = new UI.Image({ + position: new Vector2(0, 0), + size: new Vector2(144, 168), + image: 'images/your_image.png' +}); +wind.add(image); +wind.show(); +```` + +## Using Fonts +[Using Fonts]: #using-fonts + +You can use any of the Pebble system fonts in your Pebble.js applications. Please refer to [this Pebble Developer's blog post](https://developer.pebble.com/blog/2013/07/24/Using-Pebble-System-Fonts/) for a list of all the Pebble system fonts. When referring to a font, using lowercase with dashes is recommended. For example, `GOTHIC_18_BOLD` becomes `gothic-18-bold`. + +````js +var Vector2 = require('vector2'); + +var wind = new UI.Window(); +var textfield = new UI.Text({ + position: new Vector2(0, 0), + size: new Vector2(144, 168), + font: 'gothic-18-bold', + text: 'Gothic 18 Bold' +}); +wind.add(textfield); +wind.show(); +```` + +## Using Color +[Using Color]: #using-color + +You can use color in your Pebble.js applications by specifying them in the supported [Color Formats]. Use the [Pebble Color Picker](https://developer.pebble.com/guides/tools-and-resources/color-picker/) to find colors to use. Be sure to maintain [Readability and Contrast] when developing your application. + +### Color Formats +[Color Formats]: #color-formats + +Color can be specified in various ways in your Pebble.js application. The formats are named string, hexadecimal string, and hexadecimal number. Each format has different benefits. + +The following table includes examples of all the supported formats in Pebble.js: + +| Color Format | Examples | +| ------------ | :------: | +| Named String | `'green', 'sunset-orange'` | +| Hexadecimal String | `'#00ff00', '#ff5555'` | +| Hexadecimal String (with alpha) | `'#ff00ff00', '#ffff5555'` | +| Hexadecimal Number | `0x00ff00, 0xff5555` | +| Hexadecimal Number (with alpha) | `0xff00ff00, 0xffff5555` | + +**Named strings** are convenient to remember and read more naturally. They however cannot have the alpha channel be specified with the exception of the named string color `'clear'`. All other named colors are at max opacity. Named colors can also be specified in multiple casing styles, such as hyphenated lowercase `'sunset-orange'`, C constant `'SUNSET_ORANGE'`, Pascal `'SunsetOrange'`, or camel case `'sunsetOrange'`. Use the casing most convenient for you, but do so consistently across your own codebase. + +**Hexadecimal strings** can be used for specifying the exact color desired as Pebble.js will automatically round the color to the supported color of the current platform. Two hexadecimal digits are used to represent the three color channels red, green, blue in that order. + +**Hexadecimal strings (with alpha)** specified with eight digits are parsed as having an alpha channel specified in the first two digits where `00` is clear and `ff` is full opacity. + +**Hexadecimal numbers** can be manipulated directly with the arithmetic and bitwise operators. This is also the format which the configurable framework Clay uses. + +**Hexadecimal numbers (with alpha)** also have an alpha channel specified, but it is recommended to use hexadecimal strings instead for two reasons. The first reason is that `00` also represents full opacity since they are equivalent to six digit hexadecimal numbers which are implicitly at full opacity. The second is that when explicitly representing full opacity as `ff`, some integer logic can cause a signed overflow, resulting in negative color values. Intermediate alpha channels such as `55` or `aa` have no such caveats. + +Various parts of the Pebble.js API support color. Parameters of the type Color can take any of the color formats mentioned in the above table. + +````js +var UI = require('ui'); + +var card = new UI.Card({ + title: 'Using Color', + titleColor: 'sunset-orange', // Named string + subtitle: 'Color', + subtitleColor: '#00dd00', // 6-digit Hexadecimal string + body: 'Format', + bodyColor: 0x9a0036 // 6-digit Hexadecimal number +}); + +card.show(); +```` + +### Readability and Contrast +[Readability and Contrast]: #readability-and-contrast + +When using color or not, be mindful that your users may not have a Pebble supporting color or the reverse. Black and white Pebbles will display colors with medium luminance as a gray checkered pattern which makes text of any color difficult to read. In Pebble.js, you can use [Feature.color()] to use a different value depending on whether color is supported. + +````js +var UI = require('ui'); +var Feature = require('platform/feature'); + +var card = new UI.Card({ + title: 'Using Color', + titleColor: Feature.color('sunset-orange', 'black'), + subtitle: 'Readability', + subtitleColor: Feature.color('#00dd00', 'black'), + body: 'Contrast', + bodyColor: Feature.color(0x9a0036, 'black'), + backgroundColor: Feature.color('light-gray', 'white'), +}); + +card.show(); +```` + +Whether you have a color Pebble or not, you will want to test your app in all platforms. You can see how your app looks in multiple platforms with the following local SDK command or by changing the current platform in CloudPebble. + +> `pebble build && pebble install --emulator=aplite && pebble install --emulator=basalt && pebble install --emulator=chalk` + +Using too much color such as in the previous example can be overwhelming however. Just using one color that stands out in a single place can have a more defined effect and remain readable. + +````js +var card = new UI.Card({ + status: { + color: 'white', + backgroundColor: Feature.color('electric-ultramarine', 'black'), + separator: 'none', + }, + title: 'Using Color', + subtitle: 'Readability', + body: 'Contrast', +}); +```` + +Likewise, if introducing an action bar, you can remove all color from the status bar and instead apply color to the action bar. + +````js +var card = new UI.Card({ + status: true, + action: { + backgroundColor: Feature.color('jazzberry-jam', 'black'), + }, + title: 'Dialog', + subtitle: 'Action', + body: 'Button', +}); +```` + +When changing the background color, note that the status bar also needs its background color changed too if you would like it to match. + +````js +var backgroundColor = Feature.color('light-gray', 'black'); +var card = new UI.Card({ + status: { + backgroundColor: backgroundColor, + separator: Feature.round('none', 'dotted'), + }, + action: { + backgroundColor: 'black', + }, + title: 'Music', + titleColor: Feature.color('orange', 'black'), + subtitle: 'Playing', + body: 'Current Track', + backgroundColor: backgroundColor, +}); +```` + +For a menu, following this style of coloring, you would only set the `highlightBackgroundColor`. + +````js +var menu = new UI.Menu({ + status: { + separator: Feature.round('none', 'dotted'), + }, + highlightBackgroundColor: Feature.color('vivid-violet', 'black'), + sections: [{ + items: [{ title: 'One', subtitle: 'Using Color' }, + { title: 'Color', subtitle: 'Color Formats' }, + { title: 'Hightlight', subtitle: 'Readability' }], + }], +}); + +menu.show(); +```` + +In the examples above, mostly black text on white or light gray is used which has the most contrast. Try to maintain this amount of contrast with text. Using dark gray on light gray for example can be unreadable at certain angles in the sunlight or in darkly lit areas. + +## Feature Detection +[Feature Detection]: #feature-detection + +Pebble.js provides the [Feature] module so that you may perform feature detection. This allows you to change the presentation or behavior of your application based on the capabilities or characteristics of the current Pebble watch that the user is running your application with. + +### Using Feature +[Using Feature]: #using-feature + +During the development of your Pebble.js application, you will want to test your application on all platforms. You can use the following local SDK command or change the current platform in CloudPebble. + +> `pebble build && pebble install --emulator=aplite && pebble install --emulator=basalt && pebble install --emulator=chalk` + +You'll notice that there are a few differing capabilities across platforms, such as having color support or having a round screen. You can use [Feature.color()] and [Feature.round()] respectively in order to test for these capabilities. Most capability functions also have a direct opposite, such as [Feature.blackAndWhite()] and [Feature.rectangle()] respectively. + +The most common way to use [Feature] capability functions is to pass two parameters. + +````js +var UI = require('ui'); +var Feature = require('platform/feature'); + +// Use 'red' if round, otherwise use 'blue' +var color = Feature.round('red', 'blue'); + +var card = new UI.Card({ + title: 'Color', + titleColor: color, +}); + +card.show(); +```` + +You can also call the [Feature] capability functions with no arguments. In these cases, the function will return either `true` or `false` based on whether the capability exists. + +````js +if (Feature.round()) { + // Perform round-only logic + console.log('This is a round device.'); +} +```` + +Among all Pebble platforms, there are characteristics that exist on all platforms, such as the device resolution and the height of the status bar. [Feature] also provides methods which gives additional information about these characteristics, such as [Feature.resolution()] and [Feature.statusBarHeight()]. + +````js +var res = Feature.resolution(); +console.log('Current display height is ' + res.y); +```` + +Check out the [Feature] API Reference for all the capabilities it detects and characteristics it offers. + +### Feature vs Platform +[Feature vs Platform]: #feature-vs-platform + +Pebble.js offers both [Feature] detection and [Platform] detection which are different. When do you use [Feature] detection instead of just changing the logic based on the current [Platform]? Using feature detection allows you to minimize the concerns of your logic, allowing each section of logic to be a single unit that does not rely on anything else unrelated. + +Consider the following [Platform] detection logic: + +````js +var UI = require('ui'); +var Platform = require('platform'); + +var isAplite = (Platform.version() === 'aplite'); +var isChalk = (Platform.version() === 'chalk'); +var card = new UI.Card({ + title: 'Example', + titleColor: isAplite ? 'black' : 'dark-green', + subtitle: isChalk ? 'Hello World!' : 'Hello!', + body: isAplite ? 'Press up or down' : 'Speak to me', +}); + +card.show(); +```` + +The first issue has to do with future proofing. It is checking if the current Pebble has a round screen by seeing if it is on Chalk, however there may be future platforms that have round screens. It can instead use [Feature.round()] which will update to include newer platforms as they are introduced. + +The second issue is unintentional entanglement of different concerns. In the example above, `isAplite` is being used to both determine whether the Pebble is black and white and whether there is a microphone. It is harmless in this small example, but when the code grows, it could potentially change such that a function both sets up the color and interaction based on a single boolean `isAplite`. This mixes color presentation logic with interaction logic. + +Consider the same example using [Feature] detection instead: + +````js +var UI = require('ui'); +var Feature = require('platform/feature'); + +var card = new UI.Card({ + title: 'Example', + titleColor: Feature.color('dark-green', 'black'), + subtitle: Feature.round( 'Hello World!', 'Hello!'), + body: Feature.microphone('Speak to me', 'Press up or down'), +}); + +card.show(); +```` + +Now, if it is necessary to separate the different logic in setting up the card, the individual units can be implemented in separate functions without anything unintentionally mixing the logic together. [Feature] is provided as a module, so it is always available where you decide to move your logic. + +The two examples consist of units of logic that consist of one liners, but if each line was instead large blocks of logic with the `isAplite` boolean used throughout, the entanglement issue would be more difficult to remove from your codebase, hence the recommendation to use [Feature] detection. Of course, for capabilities or characteristics that [Feature] is unable to allow you to discern, use [Platform]. + +# API Reference +[API Reference]: #api-reference + +## Global namespace + +### require(path) + +Loads another JavaScript file allowing you to write a multi-file project. Package loading loosely follows the CommonJS format. `path` is the path to the dependency. + +````js +// src/js/dependency.js +var dep = require('dependency'); +```` + +Exporting is possible by modifying or setting `module.exports` within the required file. The module path is also available as `module.filename`. `require` will look for the module relative to the loading module, the root path, and the Pebble.js library folder `lib` located at `src/js/lib`. + +### Pebble + +The `Pebble` object from [PebbleKit JavaScript](https://developer.pebble.com/guides/pebble-apps/pebblekit-js/) is available as a global variable. Some of the methods it provides have Pebble.js equivalents. When available, it is recommended to use the Pebble.js equivalents as they have more documented features and cleaner interfaces. + +This table lists the current Pebble.js equivalents: + +| Pebble API | Pebble.js Equivalent | +| ------------ | :------: | +| `Pebble.addEventListener('ready', ...)` | Your application automatically starts after it is ready. | +| `Pebble.addEventListener('showConfiguration', ...)` | [Settings.config()] | +| `Pebble.addEventListener('webviewclosed', ...)` | [Settings.config()] with close handler. | + +Use `Pebble` when there is no Pebble.js alternative. Currently, these are the `Pebble` methods that have no direct Pebble.js alternative: + +| Pebble API without Equivalents | Note | +| ------------ | :---: | +| `Pebble.getAccountToken()` | | +| `Pebble.getActiveWatchInfo()` | Use [Platform.version()] if only querying for the platform version. | +| `Pebble.getTimelineToken()` | | +| `Pebble.getWatchToken()` | | +| `Pebble.showSimpleNotificationOnPebble()` | Consider presenting a [Card] or using Pebble Timeline instead. | +| `Pebble.timelineSubscribe()` | | +| `Pebble.timelineSubscriptions()` | | +| `Pebble.timelineUnsubscribe()` |   | + +### localStorage + +`localStorage` is [available for your use](https://developer.pebble.com/guides/communication/using-pebblekit-js/#using-localstorage), but consider using the [Settings] module instead which provides an alternative interface that can save and load JavaScript objects for you. + +````js +var Settings = require('settings'); + +Settings.data('playerInfo', { id: 1, name: 'Gordon Freeman' }); +var playerInfo = Settings.data('playerInfo'); +console.log("Player's name is " + playerInfo.name); +```` + +### XMLHttpRequest + +`XMLHttpRequest` is [available for your use](https://developer.pebble.com/guides/communication/using-pebblekit-js/#using-xmlhttprequest), but consider using the [ajax] module instead which provides a jQuery-like ajax alternative to performing asynchronous and synchronous HTTP requests, with built in support for forms and headers. + +````js +var ajax = require('ajax'); + +ajax({ url: 'http://api.theysaidso.com/qod.json', type: 'json' }, + function(data, status, req) { + console.log('Quote of the day is: ' + data.contents.quotes[0].quote); + } +); +```` + +### window -- browser + +A `window` object is provided with a subset of the standard APIs you would find in a normal browser. Its direct usage is discouraged because available functionalities may differ between the iOS and Android runtime environment. + +More specifically: + + - XHR and WebSocket are supported on iOS and Android + - The `` element is not available on iOS + +If in doubt, please contact [devsupport@getpebble.com](mailto:devsupport@getpebble.com). + +## Clock +[Clock]: #clock + +The Clock module makes working with the [Wakeup] module simpler with its provided time utility functions. + +### Clock + +`Clock` provides a single module of the same name `Clock`. + +````js +var Clock = require('clock'); +```` + + +#### Clock.weekday(weekday, hour, minute[, seconds]) +[Clock.weekday]: #clock-weekday + +Calculates the seconds since the epoch until the next nearest moment of the given weekday and time parameters. `weekday` can either be a string representation of the weekday name such as `sunday`, or the 0-based index number, such as 0 for sunday. `hour` is a number 0-23 with 0-12 indicating the morning or a.m. times. `minute` and `seconds` numbers 0-59. `seconds` is optional. + +The weekday is always the next occurrence and is not limited by the current week. For example, if today is Wednesday, and `'tuesday'` is given for `weekday`, the resulting time will be referring to Tuesday of next week at least 5 days from now. Similarly, if today is Wednesday and `'Thursday'` is given, the time will be referring to tomorrow, the Thursday of the same week, between 0 to 2 days from now. This is useful for specifying the time for [Wakeup.schedule]. + +````js +// Next Tuesday at 6:00 a.m. +var nextTime = Clock.weekday('tuesday', 6, 0); +console.log('Seconds until then: ' + (nextTime - Date.now())); + +var Wakeup = require('wakeup'); + +// Schedule a wakeup event. +Wakeup.schedule( + { time: nextTime }, + function(e) { + if (e.failed) { + console.log('Wakeup set failed: ' + e.error); + } else { + console.log('Wakeup set! Event ID: ' + e.id); + } + } +) +```` + +## Platform +[Platform]: #platform + +`Platform` provides a module of the same name `Platform` and a feature detection module [Feature]. + + +### Platform + +The Platform module allows you to determine the current platform runtime on the watch through its `Platform.version` method. This is to be used when the [Feature] module does not give enough ability to discern whether a feature exists or not. + +````js +var Platform = require('platform'); +```` + + +#### Platform.version() +[Platform.version()]: #platform-version + +`Platform.version` returns the current platform version name as a lowercase string. This can be `'aplite'`, `'basalt'`, or `'chalk'`. Use the following table to determine the platform that `Platform.version` will return. + +| Watch Model | Platform | +| ---- | :----: | +| Pebble Classic | `'aplite'` | +| Pebble Steel Classic | `'aplite'` | +| Pebble Time | `'basalt'` | +| Pebble Time Steel | `'basalt'` | +| Pebble Time Round | `'chalk'` | + +````js +console.log('Current platform is ' + Platform.version()); +```` + +### Feature +[Feature]: #feature + +The Feature module under Platform allows you to perform feature detection, adjusting aspects of your application to the capabilities of the current watch model it is current running on. This allows you to consider the functionality of your application based on the current set of available capabilities or features. The Feature module also provides information about features that exist on all watch models such as `Feature.resolution` which returns the resolution of the current watch model. + +````js +var Feature = require('platform/feature'); + +console.log('Color is ' + Feature.color('avaiable', 'not available')); +console.log('Display width is ' + Feature.resolution().x); +```` + + +#### Feature.color([yes, no]) +[Feature.color()]: #feature-color + +`Feature.color` will return the `yes` parameter if color is supported and `no` if it is not. This is the opposite of [Feature.blackAndWhite()]. When given no parameters, it will return true or false respectively. + +````js +var textColor = Feature.color('oxford-blue', 'black'); + +if (Feature.color()) { + // Perform color-only operation + console.log('Color supported'); +} +```` + + +#### Feature.blackAndWhite([yes, no]) +[Feature.blackAndWhite()]: #feature-blackAndWhite + +`Feature.blackAndWhite` will return the `yes` parameter if only black and white is supported and `no` if it is not. This is the opposite of [Feature.color()]. When given no parameters, it will return true or false respectively. + +````js +var backgroundColor = Feature.blackAndWhite('white', 'clear'); + +if (Feature.blackAndWhite()) { + // Perform black-and-white-only operation + console.log('Black and white only'); +} +```` + + +#### Feature.rectangle([yes, no]) +[Feature.rectangle()]: #feature-rectangle + +`Feature.rectangle` will return the `yes` parameter if the watch screen is rectangular and `no` if it is not. This is the opposite of [Feature.round()]. When given no parameters, it will return true or false respectively. + +````js +var margin = Feature.rectangle(10, 20); + +if (Feature.rectangle()) { + // Perform rectangular display only operation + console.log('Rectangular display'); +} +```` + + +#### Feature.round([yes, no]) +[Feature.round()]: #feature-round + +`Feature.round` will return the `yes` parameter if the watch screen is round and `no` if it is not. This is the opposite of [Feature.rectangle()]. When given no parameters, it will return true or false respectively. + +````js +var textAlign = Feature.round('center', 'left'); + +if (Feature.round()) { + // Perform round display only operation + console.log('Round display'); +} +```` + +#### Feature.microphone([yes, no]) + +`Feature.microphone` will return the `yes` parameter if the watch has a microphone and `no` if it does not. When given no parameters, it will return true or false respectively. Useful for determining whether the `Voice` module will allow transcription or not and changing the UI accordingly. + +````js +var text = Feature.microphone('Say your command.', + 'Select your command.'); + +if (Feature.microphone()) { + // Perform microphone only operation + console.log('Microphone available'); +} +```` + + +#### Feature.resolution() +[Feature.resolution()]: #feature-resolution + +`Feature.resolution` returns a [Vector2] containing the display width as the `x` component and the display height as the `y` component. Use the following table to determine the resolution that `Feature.resolution` will return on a given platform. + +| Platform | Width | Height | Note | +| ---- | :---: | :----: | ------ | +| aplite | 144 | 168 | | +| basalt | 144 | 168 | This is a rounded rectangle, therefore there is small set of pixels at each corner not available. | +| chalk | 180 | 180 | This is a circular display, therefore not all pixels in a 180 by 180 square are available. | + +**NOTE:** [Window]s also have a [Window.size()] method which returns its size as a [Vector2]. Use [Window.size()] when possible. + +````js +var res = Feature.resolution(); +console.log('Current display is ' + res.x + 'x' + res.y); +```` + +#### Feature.actionBarWidth() + +`Feature.actionBarWidth` returns the action bar width based on the platform. This is `30` for rectangular displays and `40` for round displays. Useful for determining the remaining screen real estate in a dynamic [Window] with an action bar visible. + +````js +var rightMargin = Feature.actionBarWidth() + 5; +var elementWidth = Feature.resolution().x - rightMargin; +```` + +**NOTE:** [Window.size()] already takes the action bar into consideration, so use it instead when possible. + + +#### Feature.statusBarHeight() +[Feature.statusBarHeight()]: #feature-statusBarHeight + +`Feature.statusBarHeight` returns the status bar height. This is `16` and can change accordingly if the Pebble Firmware theme ever changes. Useful for determining the remaining screen real estate in a dynamic [Window] with a status bar visible. + +````js +var topMargin = Feature.statusBarHeight() + 5; +var elementHeight = Feature.resolution().y - topMargin; +```` + +**NOTE:** [Window.size()] already takes the status bar into consideration, so use it instead when possible. + +## Settings +[Settings]: #settings + +The Settings module allows you to add a configurable web view to your application and share options with it. Settings also provides two data accessors `Settings.option` and `Settings.data` which are backed by localStorage. Data stored in `Settings.option` is automatically shared with the configurable web view. + +### Settings + +`Settings` provides a single module of the same name `Settings`. + +````js +var Settings = require('settings'); +```` + + +#### Settings.config(options, [open,] close) +[Settings.config()]: #settings-config + +`Settings.config` registers your configurable for use along with `open` and `close` handlers. + +`options` is an object with the following parameters: + +| Name | Type | Argument | Default | Description | +| ---- | :----: | :--------: | --------- | ------------- | +| `url` | string | | | The URL to the configurable. e.g. 'http://www.example.com?name=value' | +| `autoSave` | boolean | (optional) | true | Whether to automatically save the web view response to options | +| `hash` | boolean | (optional) | true | Whether to automatically concatenate the URI encoded json `Settings` options to the URL as the hash component. | + +`open` is an optional callback used to perform any tasks before the webview is open, such as managing the options that will be passed to the web view. + +````js +// Set a configurable with the open callback +Settings.config( + { url: 'http://www.example.com' }, + function(e) { + console.log('opening configurable'); + + // Reset color to red before opening the webview + Settings.option('color', 'red'); + }, + function(e) { + console.log('closed configurable'); + } +); +```` + +`close` is a callback that is called when the webview is closed via `pebblejs://close`. Any arguments passed to `pebblejs://close` is parsed and passed as options to the handler. `Settings` will attempt to parse the response first as URI encoded json and second as form encoded data if the first fails. + +````js +// Set a configurable with just the close callback +Settings.config( + { url: 'http://www.example.com' }, + function(e) { + console.log('closed configurable'); + + // Show the parsed response + console.log(JSON.stringify(e.options)); + + // Show the raw response if parsing failed + if (e.failed) { + console.log(e.response); + } + } +); +```` + +To pass options from your configurable to `Settings.config` `close` in your webview, URI encode your options json as the hash to `pebblejs://close`. This will close your configurable, so you would perform this action in response to the user submitting their changes. + +````js +var options = { color: 'white', border: true }; +document.location = 'pebblejs://close#' + encodeURIComponent(JSON.stringify(options)); +```` + +#### Settings.option + +`Settings.option` is a data accessor built on localStorage that shares the options with the configurable web view. + +#### Settings.option(field, value) + +Saves `value` to `field`. It is recommended that `value` be either a primitive or an object whose data is retained after going through `JSON.stringify` and `JSON.parse`. + +````js +Settings.option('color', 'red'); +```` + +If `value` is undefined or null, the field will be deleted. + +````js +Settings.option('color', null); +```` + +#### Settings.option(field) + +Returns the value of the option in `field`. + +````js +var player = Settings.option('player'); +console.log(player.id); +```` + +#### Settings.option(options) + +Sets multiple options given an `options` object. + +````js +Settings.option({ + color: 'blue', + border: false, +}); +```` + +#### Settings.option() + +Returns all options. The returned options can be modified, but if you want the modifications to be saved, you must call `Settings.option` as a setter. + +````js +var options = Settings.option(); +console.log(JSON.stringify(options)); + +options.counter = (options.counter || 0) + 1; + +// Modifications are not saved until `Settings.option` is called as a setter +Settings.option(options); +```` + +#### Settings.data + +`Settings.data` is a data accessor similar to `Settings.option` except it saves your data in a separate space. This is provided as a way to save data or options that you don't want to pass to a configurable web view. + +While localStorage is still accessible, it is recommended to use `Settings.data`. + +#### Settings.data(field, value) + +Saves `value` to `field`. It is recommended that `value` be either a primitive or an object whose data is retained after going through `JSON.stringify` and `JSON.parse`. + +````js +Settings.data('player', { id: 1, x: 10, y: 10 }); +```` + +If `value` is undefined or null, the field will be deleted. + +````js +Settings.data('player', null); +```` + +#### Settings.data(field) + +Returns the value of the data in `field`. + +````js +var player = Settings.data('player'); +console.log(player.id); +```` + +#### Settings.data(data) + +Sets multiple data given an `data` object. + +````js +Settings.data({ + name: 'Pebble', + player: { id: 1, x: 0, y: 0 }, +}); +```` + +#### Settings.data() + +Returns all data. The returned data can be modified, but if you want the modifications to be saved, you must call `Settings.data` as a setter. + +````js +var data = Settings.data(); +console.log(JSON.stringify(data)); + +data.counter = (data.counter || 0) + 1; + +// Modifications are not saved until `Settings.data` is called as a setter +Settings.data(data); +```` + +## UI +[UI]: #ui + +The UI framework contains all the classes needed to build the user interface of your Pebble applications and interact with the user. + +### Accel +[Accel]: #accel + +The `Accel` module allows you to get events from the accelerometer on Pebble. + +You can use the accelerometer in two different ways: + + - To detect tap events. Those events are triggered when the user flicks his wrist or tap on the Pebble. They are the same events that are used to turn the Pebble back-light on. Tap events come with a property to tell you in which direction the Pebble was shook. Tap events are very battery efficient because they are generated directly by the accelerometer inside Pebble. + - To continuously receive streaming data from the accelerometer. In this mode the Pebble will collect accelerometer samples at a specified frequency (from 10Hz to 100Hz), batch those events in an array and pass those to an event handler. Because the Pebble accelerometer needs to continuously transmit data to the processor and to the Bluetooth radio, this will drain the battery much faster. + +````js +var Accel = require('ui/accel'); +```` + +#### Accel.config(accelConfig) + +This function configures the accelerometer `data` events to your liking. The `tap` event requires no configuration for use. Configuring the accelerometer is a very error prone process, so it is recommended to not configure the accelerometer and use `data` events with the default configuration without calling `Accel.config`. + +`Accel.config` takes an `accelConfig` object with the following properties: + +| Name | Type | Argument | Default | Description | +| ---- | :----: | :--------: | --------- | ------------- | +| `rate` | number | (optional) | 100 | The rate accelerometer data points are generated in hertz. Valid values are 10, 25, 50, and 100. | +| `samples` | number | (optional) | 25 | The number of accelerometer data points to accumulate in a batch before calling the event handler. Valid values are 1 to 25 inclusive. | +| `subscribe` | boolean | (optional) | automatic | Whether to subscribe to accelerometer data events. Accel.accelPeek cannot be used when subscribed. Pebble.js will automatically (un)subscribe for you depending on the amount of accelData handlers registered. | + +The number of callbacks will depend on the configuration of the accelerometer. With the default rate of 100Hz and 25 samples, your callback will be called every 250ms with 25 samples each time. + +**Important:** If you configure the accelerometer to send many `data` events, you will overload the bluetooth connection. We recommend that you send at most 5 events per second. + +#### Accel.peek(callback) + +Peeks at the current accelerometer value. The callback function will be called with the data point as an event. + +````js +Accel.peek(function(e) { + console.log('Current acceleration on axis are: X=' + e.accel.x + ' Y=' + e.accel.y + ' Z=' + e.accel.z); +}); +```` + +#### Accel.on('tap', callback) + +Subscribe to the `Accel` `tap` event. The callback function will be passed an event with the following fields: + + * `axis`: The axis the tap event occurred on: 'x', 'y', or 'z'. + * `direction`: The direction of the tap along the axis: 1 or -1. + +````js +Accel.on('tap', function(e) { + console.log('Tap event on axis: ' + e.axis + ' and direction: ' + e.direction); +}); +```` + +A [Window] may subscribe to the `Accel` `tap` event using the `accelTap` event type. The callback function will only be called when the window is visible. + +````js +wind.on('accelTap', function(e) { + console.log('Tapped the window'); +}); +```` + +#### Accel.on('data', callback) + +Subscribe to the accel 'data' event. The callback function will be passed an event with the following fields: + + * `samples`: The number of accelerometer samples in this event. + * `accel`: The first data point in the batch. This is provided for convenience. + * `accels`: The accelerometer samples in an array. + +One accelerometer data point is an object with the following properties: + +| Property | Type | Description | +| -------- | :----: | ------------ | +| `x` | Number | The acceleration across the x-axis (from left to right when facing your Pebble) | +| `y` | Number | The acceleration across the y-axis (from the bottom of the screen to the top of the screen) | +| `z` | Number | The acceleration across the z-axis (going through your Pebble from the back side of your Pebble to the front side - and then through your head if Pebble is facing you ;) | +| `vibe` | boolean | A boolean indicating whether Pebble was vibrating when this sample was measured. | +| `time` | Number | The amount of ticks in millisecond resolution when this point was measured. | + +````js +Accel.on('data', function(e) { + console.log('Just received ' + e.samples + ' from the accelerometer.'); +}); +```` + +A [Window] may also subscribe to the `Accel` `data` event using the `accelData` event type. The callback function will only be called when the window is visible. + +````js +wind.on('accelData', function(e) { + console.log('Accel data: ' + JSON.stringify(e.accels)); +}); +```` + +### Voice +[Voice]: #voice + +The `Voice` module allows you to interact with Pebble's dictation API on supported platforms (Basalt and Chalk). + +````js +var Voice = require('ui/voice'); +```` + +#### Voice.dictate('start', [confirmDialog,] callback) + +This function starts the dictation UI, and invokes the callback upon completion. The callback is passed an event with the following fields: + +* `err`: A string describing the error, or `null` on success. +* `transcription`: The transcribed string. + +An optional second parameter, `confirmDialog`, can be passed to the `Voice.dictate` method to control whether there should be a confirmation dialog displaying the transcription text after voice input. If `confirmDialog` is set to `false`, the confirmation dialog will be skipped. By default, there will be a confirmation dialog. + +```js +// Start a diction session and skip confirmation +Voice.dictate('start', false, function(e) { + if (e.err) { + console.log('Error: ' + e.err); + return; + } + + main.subtitle('Success: ' + e.transcription); +}); +``` + +**NOTE:** Only one dictation session can be active at any time. Trying to call `Voice.dicate('start', ...)` while another dictation session is in progress will result in the callback being called with an event having the error `"sessionAlreadyInProgress"`. + +#### Voice.dictate('stop') + +This function stops a dictation session that is currently in progress and prevents the session's callback from being invoked. If no session is in progress this method has no effect. + +```js +Voice.dictate('stop'); +``` + +### Window +[Window]: #window + +`Window` is the basic building block in your Pebble.js application. All windows share some common properties and methods. + +Pebble.js provides three types of Windows: + + * [Card]: Displays a title, a subtitle, a banner image and text on a screen. The position of the elements are fixed and cannot be changed. + * [Menu]: Displays a menu on the Pebble screen. This is similar to the standard system menu in Pebble. + * [Window]: The `Window` by itself is the most flexible. It allows you to add different [Element]s ([Circle], [Image], [Line], [Radial], [Rect], [Text], [TimeText]) and to specify a position and size for each of them. [Element]s can also be animated. + +A `Window` can have the following properties: + +| Name | Type | Default | Description | +| ---- | :-------: | --------- | ------------- | +| `clear` | boolean | | | +| `action` | actionDef | None | An action bar will be shown when configured with an `actionDef`. | +| `fullscreen` | boolean | false | When true, the Pebble status bar will not be visible and the window will use the entire screen. | +| `scrollable` | boolean | false | Whether the user can scroll this Window with the up and down button. When this is enabled, single and long click events on the up and down button will not be transmitted to your app. | + + +#### Window actionDef +[Window actionDef]: #window-actiondef + +A `Window` action bar can be displayed by setting its Window `action` property to an `actionDef`. + +An `actionDef` has the following properties: + +| Name | Type | Default | Description | +| ---- | :-------: | --------- | ------------- | +| `up` | Image | None | An image to display in the action bar, next to the up button. | +| `select` | Image | None | An image to display in the action bar, next to the select button. | +| `down` | Image | None | An image to display in the action bar, next to the down button. | +| `backgroundColor` | Color | 'black' | The background color of the action bar. You can set this to 'white' for windows with black backgrounds. | + +````js +// Set action properties during initialization +var card = new UI.Card({ + action: { + up: 'images/action_icon_plus.png', + down: 'images/action_icon_minus.png' + } +}); + +// Set action properties after initialization +card.action({ + up: 'images/action_icon_plus.png', + down: 'images/action_icon_minus.png' +}); + +// Set a single action property +card.action('select', 'images/action_icon_checkmark.png'); + +// Disable the action bar +card.action(false); +```` + +You will need to add images to your project according to the [Using Images] guide in order to display action bar icons. + + +#### Window statusDef +[Window statusDef]: #window-statusdef + +A `Window` status bar can be displayed by setting its Window `status` property to a `statusDef`: + +A `statusDef` has the following properties: + +| Name | Type | Default | Description | +| ---- | :-------: | --------- | ------------- | +| `separator` | string | 'dotted' | The separate between the status bar and the content of the window. Can be `'dotted'` or `'none'`. | +| `color` | Color | 'black' | The foreground color of the status bar used to display the separator and time text. | +| `backgroundColor` | Color | 'white' | The background color of the status bar. You can set this to 'black' for windows with white backgrounds. | + +````js +// Set status properties during initialization +var card = new UI.Card({ + status: { + color: 'white', + backgroundColor: 'black' + } +}); + +// Set status properties after initialization +card.status({ + color: 'white', + backgroundColor: 'black' +}); + +// Set a single status property +card.status('separator', 'none'); + +// Disable the status bar +card.status(false); +```` + +#### Window.show() + +This will push the window to the screen and display it. If user press the 'back' button, they will navigate to the previous screen. + +#### Window.hide() + +This hides the window. + +If the window is currently displayed, this will take the user to the previously displayed window. + +If the window is not currently displayed, this will remove it from the window stack. The user will not be able to get back to it with the back button. + +````js +var splashScreen = new UI.Card({ banner: 'images/splash.png' }); +splashScreen.show(); + +var mainScreen = new UI.Menu(); + +setTimeout(function() { + // Display the mainScreen + mainScreen.show(); + // Hide the splashScreen to avoid showing it when the user press Back. + splashScreen.hide(); +}, 400); +```` + +#### Window.on('click', button, handler) + +Registers a handler to call when `button` is pressed. + +````js +wind.on('click', 'up', function() { + console.log('Up clicked!'); +}); +```` + +You can register a handler for the 'up', 'select', 'down', and 'back' buttons. + +**Note:** You can also register button handlers for `longClick`. + +#### Window.on('longClick', button, handler) + +Just like `Window.on('click', button, handler)` but for 'longClick' events. + +#### Window.on('show', handler) + +Registers a handler to call when the window is shown. This is useful for knowing when a user returns to your window from another. This event is also emitted when programmatically showing the window. This does not include when a Pebble notification popup is exited, revealing your window. + +````js +// Define the handler before showing. +wind.on('show', function() { + console.log('Window is shown!'); +}); + +// The show event will emit, and the handler will be called. +wind.show(); +```` + +#### Window.on('hide', handler) + +Registers a handler to call when the window is hidden. This is useful for knowing when a user exits out of your window or when your window is no longer visible because a different window is pushed on top. This event is also emitted when programmatically hiding the window. This does not include when a Pebble notification popup obstructs your window. + +It is recommended to use this instead of overriding the back button when appropriate. + +````js +wind.on('hide', function() { + console.log('Window is hidden!'); +}); +```` + +#### Window.action(actionDef) + +Nested accessor to the `action` property which takes an `actionDef`. Used to configure the action bar with a new `actionDef`. See [Window actionDef]. + +````js +card.action({ + up: 'images/action_icon_up.png', + down: 'images/action_icon_down.png' +}); +```` + +To disable the action bar after enabling it, `false` can be passed in place of an `actionDef`. + +````js +// Disable the action bar +card.action(false); +```` + +#### Window.action(field, value) + +`Window.action` can also be called with two arguments, `field` and `value`, to set specific fields of the window's `action` property. `field` is the name of a [Window actionDef] property as a string and `value` is the new property value. + +````js +card.action('select', 'images/action_icon_checkmark.png'); +```` + +#### Window.status(statusDef) + +Nested accessor to the `status` property which takes a `statusDef`. Used to configure the status bar with a new `statusDef`. See [Window statusDef]. + +````js +card.status({ + color: 'white', + backgroundColor: 'black' +}); +```` + +To disable the status bar after enabling it, `false` can be passed in place of `statusDef`. + +````js +// Disable the status bar +card.status(false); +```` + +Similarly, `true` can be used as a [Window statusDef] to represent a `statusDef` with all default properties. + +````js +var card = new UI.Card({ status: true }); +card.show(); +```` + +#### Window.status(field, value) + +`Window.status` can also be called with two arguments, `field` and `value`, to set specific fields of the window's `status` property. `field` is the name of a [Window statusDef] property as a string and `value` is the new property value. + +````js +card.status('separator', 'none'); +```` + + +#### Window.size() +[Window.size()]: #window-size + +`Window.size` returns the size of the max viewable content size of the window as a [Vector2] taking into account whether there is an action bar and status bar. A [Window] will return a size that is shorter than a [Window] without for example. + +If the automatic consideration of the action bar and status bar does not satisfy your use case, you can use [Feature.resolution()] to obtain the Pebble's screen resolution as a [Vector2]. + +````js +var wind = new UI.Window({ status: true }); + +var size = wind.size(); +var rect = new UI.Rect({ size: new Vector2(size.x / 4, size.y / 4) }); +wind.add(rect); + +wind.show(); +```` + +### Window (dynamic) + +A [Window] instantiated directly is a dynamic window that can display a completely customizable user interface on the screen. Dynamic windows are initialized empty and will need [Element]s added to it. [Card] and [Menu] will not display elements added to them in this way. + +````js +// Create a dynamic window +var wind = new UI.Window(); + +// Add a rect element +var rect = new UI.Rect({ size: new Vector2(20, 20) }); +wind.add(rect); + +wind.show(); +```` + +#### Window.add(element) + +Adds an element to to the [Window]. The element will be immediately visible. + +#### Window.insert(index, element) + +Inserts an element at a specific index in the list of Element. + +#### Window.remove(element) + +Removes an element from the [Window]. + +#### Window.index(element) + +Returns the index of an element in the [Window] or -1 if the element is not in the window. + +#### Window.each(callback) + +Iterates over all the elements on the [Window]. + +````js +wind.each(function(element) { + console.log('Element: ' + JSON.stringify(element)); +}); +```` + +### Card +[Card]: #card + +A Card is a type of [Window] that allows you to display a title, a subtitle, an image and a body on the screen of Pebble. + +Just like any window, you can initialize a Card by passing an object to the constructor or by calling accessors to change the properties. + +````js +var card = new UI.Card({ + title: 'Hello People!' +}); +card.body('This is the content of my card!'); +```` + +The properties available on a [Card] are: + +| Name | Type | Default | Description | +| ---- | :-------: | --------- | ------------- | +| `title` | string | '' | Text to display in the title field at the top of the screen | +| `titleColor` | Color | 'black' | Text color of the title field | +| `subtitle` | string | '' | Text to display below the title | +| `subtitleColor` | Color | 'black' | Text color of the subtitle field | +| `body` | string | '' | Text to display in the body field | +| `bodyColor` | Color | 'black' | Text color of the body field | +| `icon` | Image | null | An image to display before the title text. Refer to [Using Images] for instructions on how to include images in your app. | +| `subicon` | Image | null | An image to display before the subtitle text. Refer to [Using Images] for instructions on how to include images in your app. | +| `banner` | Image | null | An image to display in the center of the screen. Refer to [Using Images] for instructions on how to include images in your app. | +| `scrollable` | boolean | false | Whether the user can scroll this card with the up and down button. When this is enabled, single and long click events on the up and down button will not be transmitted to your app. | +| `style` | string | 'small' | Selects the font used to display the body. This can be 'small', 'large' or 'mono' | + +A [Card] is also a [Window] and thus also has Window properties. + +The `'small'` and `'large`' styles correspond to the system notification styles. `'mono'` sets a monospace font for the body textfield, enabling more complex text UIs or ASCII art. The `'small'` and `'large'` styles were updated to match the Pebble firmware 3.x design during the 3.11 release. In order to use the older 2.x styles, you may specify `'classic-small'` and `'classic-large'`, however it is encouraged to use the newer styles. + +Note that all text fields will automatically span multiple lines if needed and that you can use '\n' to insert line breaks. + +### Menu +[Menu]: #menu + +A menu is a type of [Window] that displays a standard Pebble menu on the screen of Pebble. + +Just like any window, you can initialize a Menu by passing an object to the constructor or by calling accessors to change the properties. + +The properties available on a [Menu] are: + +| Name | Type | Default | Description | +| ---- |:-------:|---------|-------------| +| `sections` | Array | `[]` | A list of all the sections to display. | +| `backgroundColor` | Color | `white` | The background color of a menu item. | +| `textColor` | Color | `black` | The text color of a menu item. | +| `highlightBackgroundColor` | Color | `black` | The background color of a selected menu item. | +| `highlightTextColor` | Color | `white` | The text color of a selected menu item. | + +A menu contains one or more sections. + +The properties available on a section are: + +| Name | Type | Default | Description | +| ---- |:-------:|---------|-------------| +| `items` | Array | `[]` | A list of all the items to display. | +| `title` | string | '' | Title text of the section header. | +| `backgroundColor` | Color | `white` | The background color of the section header. | +| `textColor` | Color | `black` | The text color of the section header. | + +Each section has a title and contains zero or more items. An item must have a title. Items can also optionally have a subtitle and an icon. + +````js +var menu = new UI.Menu({ + backgroundColor: 'black', + textColor: 'blue', + highlightBackgroundColor: 'blue', + highlightTextColor: 'black', + sections: [{ + title: 'First section', + items: [{ + title: 'First Item', + subtitle: 'Some subtitle', + icon: 'images/item_icon.png' + }, { + title: 'Second item' + }] + }] +}); +```` + +#### Menu.section(sectionIndex, section) + +Define the section to be displayed at `sectionIndex`. See [Menu] for the properties of a section. + +````js +var section = { + title: 'Another section', + items: [{ + title: 'With one item' + }] +}; +menu.section(1, section); +```` + +When called with no `section`, returns the section at the given `sectionIndex`. + +#### Menu.items(sectionIndex, items) + +Define the items to display in a specific section. See [Menu] for the properties of an item. + +````js +menu.items(0, [ { title: 'new item1' }, { title: 'new item2' } ]); +```` + +Whell called with no `items`, returns the items of the section at the given `sectionIndex`. + +#### Menu.item(sectionIndex, itemIndex, item) + +Define the item to display at index `itemIndex` in section `sectionIndex`. See [Menu] for the properties of an item. + +````js +menu.item(0, 0, { title: 'A new item', subtitle: 'replacing the previous one' }); +```` + +When called with no `item`, returns the item at the given `sectionIndex` and `itemIndex`. + +#### Menu.selection(callback) + +Get the currently selected item and section. The callback function will be passed an event with the following fields: + +* `menu`: The menu object. +* `section`: The menu section object. +* `sectionIndex`: The section index of the section of the selected item. +* `item`: The menu item object. +* `itemIndex`: The item index of the selected item. + +````js +menu.selection(function(e) { + console.log('Currently selected item is #' + e.itemIndex + ' of section #' + e.sectionIndex); + console.log('The item is titled "' + e.item.title + '"'); +}); +```` + +#### Menu.selection(sectionIndex, itemIndex) + +Change the selected item and section. + +````js +// Set the menu selection to the first section's third menu item +menu.selection(0, 2); +```` + + +#### Menu.on('select', callback) +[Menu.on('select', callback)]: #menu-on-select-callback + +Registers a callback called when an item in the menu is selected. The callback function will be passed an event with the following fields: + +* `menu`: The menu object. +* `section`: The menu section object. +* `sectionIndex`: The section index of the section of the selected item. +* `item`: The menu item object. +* `itemIndex`: The item index of the selected item. + +**Note:** You can also register a callback for 'longSelect' event, triggered when the user long clicks on an item. + +````js +menu.on('select', function(e) { + console.log('Selected item #' + e.itemIndex + ' of section #' + e.sectionIndex); + console.log('The item is titled "' + e.item.title + '"'); +}); +```` + +#### Menu.on('longSelect', callback) + +Similar to the select callback, except for long select presses. See [Menu.on('select', callback)]. + +### Element +[Element]: #element + +There are seven types of [Element] that can be instantiated at the moment: [Circle], [Image], [Line], [Radial], [Rect], [Text], [TimeText]. + +Most elements share these common properties: + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `position` | Vector2 | | Position of this element in the window. | +| `size` | Vector2 | | Size of this element in this window. [Circle] uses `radius` instead. | +| `borderWidth` | number | 0 | Width of the border of this element. [Line] uses `strokeWidth` instead. | +| `borderColor` | Color | 'clear' | Color of the border of this element. [Line] uses `strokeColor` instead. | +| `backgroundColor` | Color | 'white' | Background color of this element. [Line] has no background. | + +All properties can be initialized by passing an object when creating the Element, and changed with accessors functions that have the same name as the properties. Calling an accessor without a parameter will return the current value. + +````js +var Vector2 = require('vector2'); +var element = new Text({ + position: new Vector2(0, 0), + size: new Vector2(144, 168), +}); +element.borderColor('white'); +console.log('This element background color is: ' + element.backgroundColor()); +```` + +#### Element.index() + +Returns the index of the element in its [Window] or -1 if the element is not part of a window. + +#### Element.remove() + +Removes the element from its [Window]. + +#### Element.animate(animateDef, [duration=400]) + +The `position` and `size` are currently the only Element properties that can be animated. An `animateDef` is object with any supported properties specified. See [Element] for a description of those properties. The default animation duration is 400 milliseconds. + +````js +// Use the element's position and size to avoid allocating more vectors. +var pos = element.position(); +var size = element.size(); + +// Use the *Self methods to also avoid allocating more vectors. +pos.addSelf(size); +size.addSelf(size); + +// Schedule the animation with an animateDef +element.animate({ position: pos, size: size }); +```` + +Each element has its own animation queue. Animations are queued when `Element.animate` is called multiple times at once with the same element. The animations will occur in order, and the first animation will occur immediately. Note that because each element has its own queue, calling `Element.animate` across different elements will result all elements animating the same time. To queue animations across multiple elements, see [Element.queue(callback(next))]. + +When an animation begins, its destination values are saved immediately to the [Element]. + +`Element.animate` is chainable. + +#### Element.animate(field, value, [duration=400]) + +You can also animate a single property by specifying a field by its name. + +````js +var pos = element.position(); +pos.y += 20; +element.animate('position', pos, 1000); +```` + + +#### Element.queue(callback(next)) +[Element.queue(callback(next))]: #element-queue-callback-next + +`Element.queue` can be used to perform tasks that are dependent upon an animation completing, such as preparing the element for a different animation. `Element.queue` can also be used to coordinate animations across different elements. It is recommended to use `Element.queue` instead of a timeout if the same element will be animated after the custom task. + +The `callback` you pass to `Element.queue` will be called with a function `next` as the first parameter. When `next` is called, the next item in the animation queue will begin. Items includes callbacks added by `Element.queue` or animations added by `Element.animate` before an animation is complete. Calling `next` is equivalent to calling `Element.dequeue`. + +````js +element + .animate('position', new Vector2(0, 0) + .queue(function(next) { + this.backgroundColor('white'); + next(); + }) + .animate('position', new Vector2(0, 50)); +```` + +`Element.queue` is chainable. + +#### Element.dequeue() + +`Element.dequeue` can be used to continue executing items in the animation queue. It is useful in cases where the `next` function passed in `Element.queue` callbacks is not available. See [Element.queue(callback(next))] for more information on the animation queue. + +#### Element.position(position) + +Accessor to the `position` property. See [Element]. + +#### Element.size(size) + +Accessor to the `size` property. See [Element]. + +#### Element.borderWidth(width) + +Accessor to the `borderWidth` property. See [Element]. + +#### Element.borderColor(color) + +Accessor to the `borderColor` property. See [Element]. + +#### Element.backgroundColor(color) + +Accessor to the `backgroundColor` property. See [Element]. + +### Line +[Line]: #line + +An [Element] that displays a line on the screen. + +[Line] also has these additional properties: + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `position2` | Vector2 | | Ending position of the line where `position` is the starting position. | +| `strokeWidth` | number | 0 | Width of the line. | +| `strokeColor` | Color | 'clear' | Color of the line. | + +For clarity, [Line] has `strokeWidth` and `strokeColor` instead of `borderWidth` and `borderColor`. + +````js +var wind = new UI.Window(); + +var line = new UI.Line({ + position: new Vector2(10, 10), + position2: new Vector2(72, 84), + strokeColor: 'white', +}); + +wind.add(line); +wind.show(); +```` + +#### Line.position2(position) + +Accessor to the `position2` ending position property. See [Line]. + +#### Line.strokeWidth(width) + +Accessor to the `strokeWidth` property. See [Line]. + +#### Line.strokeColor(color) + +Accessor to the `strokeColor` property. See [Line]. + +### Circle +[Circle]: #circle + +An [Element] that displays a circle on the screen. + +[Circle] also has the additional property `radius` which it uses for size rather than `size`. [Circle] is also different in that it positions its origin at the position, rather than anchoring by its top left. These differences are to keep the graphics operation characteristics that it is built upon. + +````js +var wind = new UI.Window(); + +var circle = new UI.Circle({ + position: new Vector2(72, 84), + radius: 25, + backgroundColor: 'white', +}); + +wind.add(circle); +wind.show(); +```` + +#### Circle.radius(radius) + +Accessor to the `radius` property. See [Circle] + +### Radial +[Radial]: #radial + +An [Element] that can display as an arc, ring, sector of a circle depending on its properties are set. + +[Radial] has these additional properties: + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `radius` | number | 0 | Radius of the radial starting from its outer edge. A sufficiently large radius results in a sector or circle instead of an arc or ring. | +| `angle` | number | 0 | Starting angle in degrees. An arc or sector will be drawn from `angle` to `angle2`. | +| `angle2` | number | 360 | Ending angle in degrees. An ending angle that is 360 beyond the starting angle will result in a ring or circle. | + +#### Radial.radius(radius) + +Accessor to the `radius` property. See [Radial] + +#### Radial.angle(angle) + +Accessor to the `angle` starting angle property. See [Radial] + +#### Radial.angle2(angle) + +Accessor to the `angle2` ending angle property. See [Radial] + +### Rect +[Rect]: #rect + +An [Element] that displays a rectangle on the screen. + +The [Rect] element has the following properties. Just like any other [Element] you can initialize those properties when creating the object or use the accessors. + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `backgroundColor` | string | "white" | Background color of this element ('clear', 'black' or 'white'). | +| `borderColor` | string | "clear" | Color of the border of this element ('clear', 'black',or 'white'). | + +### Text +[Text]: #text + +An [Element] that displays text on the screen. + +The [Text] element has the following properties. Just like any other [Element] you can initialize those properties when creating the object or use the accessors. + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `text` | string | "" | The text to display in this element. | +| `font` | string | | The font to use for that text element. See [Using Fonts] for more information on the different fonts available and how to add your own fonts. | +| `color` | | 'white' | Color of the text ('white', 'black' or 'clear'). | +| `textOverflow` | 'string' | | How to handle text overflow in this text element ('wrap', 'ellipsis' or 'fill'). | +| `textAlign` | 'string' | | How to align text in this element ('left', 'center' or 'right'). | +| `borderColor` | string | 'clear' | Color of the border of this element ('clear', 'black',or 'white'). | +| `backgroundColor` | string | 'clear' | Background color of this element ('clear', 'black' or 'white'). | + +### TimeText +[TimeText]: #timetext + +A [Text] element that displays time formatted text on the screen. + +#### Displaying time in a TimeText element + +If you want to display the current time or date, use the `TimeText` element with a time formatting string in the `text` property. The time to redraw the time text element will be automatically calculated based on the format string. For example, a `TimeText` element with the format `'%M:%S'` will be redrawn every second because of the seconds format `%S`. + +The available formatting options follows the C `strftime()` function: + +| Specifier | Replaced by | Example | +| ----------- | ------------- | --------- | +| %a | An abbreviation for the day of the week. | "Thu" | +| %A | The full name for the day of the week. | "Thursday" | +| %b | An abbreviation for the month name. | "Aug" | +| %B | The full name of the month. | "August" | +| %c | A string representing the complete date and time | "Mon Apr 01 13:13:13 1992" | +| %d | The day of the month, formatted with two digits. | "23" | +| %H | The hour (on a 24-hour clock), formatted with two digits. | "14" | +| %I | The hour (on a 12-hour clock), formatted with two digits. | "02" | +| %j | The count of days in the year, formatted with three digits (from `001` to `366`). | "235" | +| %m | The month number, formatted with two digits. | "08" | +| %M | The minute, formatted with two digits. | "55" | +| %p | Either `AM` or `PM` as appropriate. | "AM" | +| %S | The second, formatted with two digits. | "02" | +| %U | The week number, formatted with two digits (from `00` to `53`; week number 1 is taken as beginning with the first Sunday in a year). See also `%W`. | "33" | +| %w | A single digit representing the day of the week: Sunday is day 0. | "4" | +| %W | Another version of the week number: like `%U`, but counting week 1 as beginning with the first Monday in a year. | "34" | +| %x | A string representing the complete date. | "Mon Apr 01 1992" | +| %X | A string representing the full time of day (hours, minutes, and seconds). | "13:13:13" | +| %y | The last two digits of the year. | "01" | +| %Y | The full year, formatted with four digits to include the century. | "2001" | +| %Z | Defined by ANSI C as eliciting the time zone if available; it is not available in this implementation (which accepts `%Z` but generates no output for it). | | +| %% | A single character, `%`. | "%" | + +#### Text.text(text) + +Sets the text property. See [Text]. + +#### Text.font(font) + +Sets the font property. See [Text]. + +#### Text.color(color) + +Sets the color property. See [Text]. + +#### Text.textOverflow(textOverflow) + +Sets the textOverflow property. See [Text]. + +#### Text.textAlign(textAlign) + +Sets the textAlign property. See [Text]. + +#### Text.updateTimeUnit(updateTimeUnits) + +Sets the updateTimeUnits property. See [Text]. + +#### Text.borderColor(borderColor) + +Sets the borderColor property. See [Text]. + +#### Text.backgroundColor(backgroundColor) + +Sets the backgroundColor property. See [Text]. + +### Image +[Image]: #image + +An [Element] that displays an image on the screen. + +The [Image] element has the following properties. Just like any other [Element] you can initialize those properties when creating the object or use the accessors. + +| Name | Type | Default | Description | +| ------------ | :-------: | --------- | ------------- | +| `image` | string | "" | The resource name or path to the image to display in this element. See [Using Images] for more information and how to add your own images. | +| `compositing` | string | "normal" | The compositing operation used to display the image. See [Image.compositing(compop)] for a list of possible compositing operations. | + + +#### Image.image(image) + +Sets the image property. See [Image]. + + +#### Image.compositing(compop) +[Image.compositing(compop)]: #image-compositing + +Sets the compositing operation to be used when rendering. Specify the compositing operation as a string such as `"invert"`. The following is a list of compositing operations available. + +| Compositing | Description | +| ----------- | :--------------------------------------------------------------------: | +| `"normal"` | Display the image normally. This is the default. | +| `"invert"` | Display the image with inverted colors. | +| `"or"` | White pixels are shown, black pixels are clear. | +| `"and"` | Black pixels are shown, white pixels are clear. | +| `"clear"` | The image's white pixels are painted as black, and the rest are clear. | +| `"set"` | The image's black pixels are painted as white, and the rest are clear. | + +### Vibe +[Vibe]: #vibe + +`Vibe` allows you to trigger vibration on the user wrist. + +#### Vibe.vibrate(type) + +````js +var Vibe = require('ui/vibe'); + +// Send a long vibration to the user wrist +Vibe.vibrate('long'); +```` + +| Name | Type | Argument | Default | Description | +| ---- |:----:|:--------:|---------|-------------| +| `type` | string | optional | `short` | The duration of the vibration. `short`, `long` or `double`. | + +### Light +[Light]: #light + +`Light` allows you to control the Pebble's backlight. +````js +var Light = require('ui/light'); + +// Turn on the light +Light.on('long'); +```` + +#### Light.on() +Turn on the light indefinitely. + +#### Light.auto() +Restore the normal behavior. + +#### Light.trigger() +Trigger the backlight to turn on momentarily, just like if the user shook their wrist. + +## Timeline +[Timeline]: #timeline + +The Timeline module allows your app to handle a launch via a timeline action. This allows you to write a custom handler to manage launch events outside of the app menu. With the Timeline module, you can preform a specific set of actions based on the action which launched the app. + +### Timeline + +`Timeline` provides a single module of the same name `Timeline`. + +````js +var Timeline = require('timeline'); +```` + + +#### Timeline.launch(callback(event)) +[Timeline.launch]: #timeline-launch + +If you wish to change the behavior of your app depending on whether it was launched by a timeline event, and further configure the behavior based on the data associated with the timeline event, use `Timeline.launch` on startup. `Timeline.launch` will immediately call your launch callback asynchronously with a launch event detailing whether or not your app was launched by a timeline event. + +````js +// Query whether we were launched by a timeline event +Timeline.launch(function(e) { + if (e.action) { + console.log('Woke up to timeline event: ' + e.launchCode + '!'); + } else { + console.log('Regular launch not by a timeline event.'); + } +}); +```` + +The `callback` will be called with a timeline launch event. The event has the following properties: + +| Name | Type | Description | +| ---- | :----: | ------------- | +| `action` | boolean | `true` if the app woke up by a timeline event, otherwise `false`. | +| `launchCode` | number | If woken by a timeline event, the code of the action. | + +Note that this means you may have to move portions of your startup logic into the `Timeline.launch` callback or a function called by the callback. This can also add a very small delay to startup behavior because the underlying implementation must query the watch for the launch information. + +## Wakeup +[Wakeup]: #wakeup + +The Wakeup module allows you to schedule your app to wakeup at a specified time using Pebble's wakeup functionality. Whether the user is in a different watchface or app, your app will launch at the specified time. This allows you to write a custom alarm app, for example. If your app is already running, you may also subscribe to receive the wakeup event, which can be useful for more longer lived timers. With the Wakeup module, you can save data to be read on launch and configure your app to behave differently based on launch data. The Wakeup module, like the Settings module, is backed by localStorage. + +### Wakeup + +`Wakeup` provides a single module of the same name `Wakeup`. + +````js +var Wakeup = require('wakeup'); +```` + + +#### Wakeup.schedule(options, callback(event)) +[Wakeup.schedule]: #wakeup-schedule + +Schedules a wakeup event that will wake up the app at the specified time. `callback` will be immediately called asynchronously with whether the wakeup event was successfully set or not. Wakeup events cannot be scheduled within one minute of each other regardless of what app scheduled them. Each app may only schedule up to 8 wakeup events. + +See [Clock.weekday] for setting wakeup events at particular times of a weekday. + +````js +Wakeup.schedule( + { + // Set the wakeup event for one minute from now + time: Date.now() / 1000 + 60, + // Pass data for the app on launch + data: { hello: 'world' } + }, + function(e) { + if (e.failed) { + // Log the error reason + console.log('Wakeup set failed: ' + e.error); + } else { + console.log('Wakeup set! Event ID: ' + e.id); + } + } +); +```` + +The supported `Wakeup.schedule` options are: + +| Name | Type | Argument | Default | Description | +| ---- | :----: | :--------: | --------- | ------------- | +| `time` | number | required | | The time for the app to launch in seconds since the epoch as a number. Time can be specified as a Date object, but is not recommended due to timezone confusion. If using a Date object, no timezone adjustments are necessary if the phone's timezone is properly set. | +| `data` | * | optional | | The data to be saved for the app to read on launch. This is optional. See [Wakeup.launch]. Note that `data` is backed by localStorage and is thus saved on the phone. Data must be JSON serializable as it uses `JSON.stringify` to save the data. | +| `cookie` | number | optional | 0 | A 32-bit unsigned integer to be saved for the app to read on launch. This is an optional alternative to `data` can also be used in combination. The integer is saved on the watch rather than the phone. | +| `notifyIfMissed` | boolean | optional | false | The user can miss a wakeup event if their watch is powered off. Specify `true` if you would like Pebble OS to notify them if they missed the event. | + +Scheduling a wakeup event can result in errors. By providing a `callback`, you can inspect whether or not you have successfully set the wakeup event. The `callback` will be called with a wakeup set result event which has the following properties: + +| Name | Type | Description | +| ---- | :----: | ------------- | +| `id` | number | If successfully set, the wakeup event id. | +| `error` | string | On set failure, the type of error. | +| `failed` | boolean | `true` if the event could not be set, otherwise `false`. | +| `data` | number | The custom `data` that was associated with the wakeup event. | +| `cookie` | number | The custom 32-bit unsigned integer `cookie` that was associated with the wakeup event. | + +Finally, there are multiple reasons why setting a wakeup event can fail. When a wakeup event fails to be set, `error` can be one of the following strings: + +| Error | Description | +| ----- | ------------- | +| `'range'` | Another wakeup event is already scheduled close to the requested time. | +| `'invalidArgument'` | The wakeup event was requested to be set in the past. | +| `'outOfResources'` | The app already has the maximum of 8 wakeup events scheduled. | +| `'internal'` | There was a Pebble OS error in scheduling the event. | + + +#### Wakeup.launch(callback(event)) +[Wakeup.launch]: #wakeup-launch + +If you wish to change the behavior of your app depending on whether it was launched by a wakeup event, and further configure the behavior based on the data associated with the wakeup event, use `Wakeup.launch` on startup. `Wakeup.launch` will immediately call your launch callback asynchronously with a launch event detailing whether or not your app was launched by a wakeup event. + +If you require knowing when a wakeup event occurs while your app is already running, refer to [Wakeup.on('wakeup')] to register a wakeup callback that will be called for both launch wakeup events and wakeup events while already running. + +````js +// Query whether we were launched by a wakeup event +Wakeup.launch(function(e) { + if (e.wakeup) { + console.log('Woke up to ' + e.id + '! data: ' + JSON.stringify(e.data)); + } else { + console.log('Regular launch not by a wakeup event.'); + } +}); +```` + +The `callback` will be called with a wakeup launch event. The event has the following properties: + +| Name | Type | Description | +| ---- | :----: | ------------- | +| `id` | number | If woken by a wakeup event, the wakeup event id. | +| `wakeup` | boolean | `true` if the launch event is a wakeup event, otherwise `false`. | +| `launch` | boolean | `true` if the launch was caused by this wakeup event, otherwise `false`. | +| `data` | number | If woken by a wakeup event, the custom `data` that was associated with the wakeup event. | +| `cookie` | number | If woken by a wakeup event, the custom 32-bit unsigned integer `cookie` that was associated with the wakeup event. | + +**Note:** You may have to move portions of your startup logic into the `Wakeup.launch` callback or a function called by the callback. This can also add a very small delay to startup behavior because the underlying implementation must query the watch for the launch information. + + +#### Wakeup.on('wakeup', handler) +[Wakeup.on('wakeup')]: #wakeup-on-wakeup + +Registers a handler to call when a wakeup event occurs. This includes launch wakeup events and wakeup events while already running. See [Wakeup.launch] for the properties of the wakeup event object to be passed to the handler. + +````js +// Single wakeup event handler example: +Wakeup.on('wakeup', function(e) { + console.log('Wakeup event! ' + JSON.stringify(e)); +}); +```` + +If you want your wakeup handler to only receive wakeup events while already running, you can either test against the `.launch` boolean property, or use a wakeup launch handler to block the event from being sent to additional handlers. Wakeup events are sent to launch wakeup handlers first, then to general wakeup handlers next. + +````js +// Single already-running example: +Wakeup.on('wakeup', function(e) { + if (!e.launch) { + console.log('Already-running wakeup: ' + JSON.stringify(e)); + } +}); +```` + +**Note:** Returning false will also prevent further handlers of the same type from receiving the event. Within each type of handlers, they are passed in registration order. The passing process ends if any handler returns false. + +````js +// Launch + Already-running example: +// Launch wakeup handler +Wakeup.launch(function(e) { + if (e.wakeup) { + console.log('Launch wakeup: ' + JSON.stringify(e)); + } + // Do not pass the event to additional handlers + return false; +}); + +// Since the launch wakeup handler returns false, +// this becomes an already-running wakeup handler +Wakeup.on('wakeup', function(e) { + console.log('Wakeup: ' + JSON.stringify(e)); +}); +```` + + +#### Wakeup.get(id) +[Wakeup.get]: #wakeup-get + +Get the wakeup state information by the wakeup id. A wakeup state has the following properties: + +| Name | Type | Description | +| ---- | :----: | ------------- | +| `id` | number | The wakeup event id. | +| `time` | number | The time for the app to launch. This depends on the data type pass to [Wakeup.schedule]. If a Date object was passed, this can be a string because of localStorage. | +| `data` | number | The custom `data` that was associated with the wakeup event. | +| `cookie` | number | The custom 32-bit unsigned integer `cookie` that was associated with the wakeup event. | +| `notifyIfMissed` | boolean | Whether it was requested for Pebble OS to notify the user if they missed the wakeup event. | + +````js +var wakeup = Wakeup.get(wakeupId); +console.log('Wakeup info: ' + JSON.stringify(wakeup)); +```` + + +#### Wakeup.each(callback(wakeup)) +[Wakeup.each]: #wakeup-each + +Loops through all scheduled wakeup events that have not yet triggered by calling the `callback` for each wakeup event. See [Wakeup.get] for the properties of the `wakeup` object to be passed to the callback. + +````js +var numWakeups = 0; + +// Query all wakeups +Wakeup.each(function(e) { + console.log('Wakeup ' + e.id + ': ' + JSON.stringify(e)); + ++numWakeups; +}); + +main.body('Number of wakeups: ' + numWakeups); +```` + +#### Wakeup.cancel(id) + +Cancels a particular wakeup event by id. The wakeup event id is obtained by the set result callback when setting a wakeup event. See [Wakeup.schedule]. + +#### Wakeup.cancel('all') + +Cancels all wakeup events scheduled by your app. You can check what wakeup events are set before cancelling them all. See [Wakeup.each]. + +## Libraries + +Pebble.js includes several libraries to help you write applications. + +### ajax +[ajax]: #ajax + +This module gives you a very simple and easy way to make HTTP requests. + +````js +var ajax = require('ajax'); + +ajax({ url: 'http://api.theysaidso.com/qod.json', type: 'json' }, + function(data) { + console.log('Quote of the day is: ' + data.contents.quotes[0].quote); + } +); +```` + +#### ajax(options, success, failure) + +The supported options are: + +| Name | Type | Argument | Default | Description | +| ---- | :----: | :--------: | --------- | ------------- | +| `url` | string | | | The URL to make the ajax request to. e.g. 'http://www.example.com?name=value' | +| `method` | string | (optional) | get | The HTTP method to use: 'get', 'post', 'put', 'delete', 'options', or any other standard method supported by the running environment. | +| `type` | string | (optional) | | The content and response format. By default, the content format is 'form' and response format is separately 'text'. Specifying 'json' will have ajax send `data` as json as well as parse the response as json. Specifying 'text' allows you to send custom formatted content and parse the raw response text. If you wish to send form encoded data and parse json, leave `type` undefined and use `JSON.decode` to parse the response data. +| `data` | object | (optional) | | The request body, mainly to be used in combination with 'post' or 'put'. e.g. `{ username: 'guest' }` +| `headers` | object | (optional) | | Custom HTTP headers. Specify additional headers. e.g. `{ 'x-extra': 'Extra Header' }` +| `async` | boolean | (optional) | true | Whether the request will be asynchronous. Specify `false` for a blocking, synchronous request. +| `cache` | boolean | (optional) | true | Whether the result may be cached. Specify `false` to use the internal cache buster which appends the URL with the query parameter `_set` to the current time in milliseconds. | + +The `success` callback will be called if the HTTP request is successful (when the status code is inside [200, 300) or 304). The parameters are the data received from the server, the status code, and the request object. If the option `type: 'json'` was set, the response will automatically be converted to an object, otherwise `data` is a string. + +The `failure` callback is called when an error occurred. The parameters are the same as `success`. + +### Vector2 +[Vector2]: #vector2 + +A 2 dimensional vector. The constructor takes two parameters for the x and y values. + +````js +var Vector2 = require('vector2'); + +var vec = new Vector2(144, 168); +```` + +For more information, see [Vector2 in the three.js reference documentation][three.js Vector2]. +[three.js Vector2]: http://threejs.org/docs/#Reference/Math/Vector2 + +## Examples + +Coming Soon! + +## Acknowledgements + +Pebble.js started as [Simply.JS](http://simplyjs.io), a project by [Meiguro](http://github.com/meiguro). It is now part of the Pebble SDK and supported by Pebble. Contact [devsupport@getpebble.com](mailto:devsupport@getpebble.com) with any questions! + +This documentation uses [Flatdoc](http://ricostacruz.com/flatdoc/#flatdoc). diff --git a/appinfo.json b/appinfo.json index 9a4bbda0..1c2c0c8d 100644 --- a/appinfo.json +++ b/appinfo.json @@ -1,31 +1,48 @@ { - "uuid": "133215f0-cf20-4c05-997b-3c9be5a64e5b", - "shortName": "Simply.js", - "longName": "Simply.js", - "companyName": "Meiguro", - "versionCode": 1, - "versionLabel": "0.3.4", - "capabilities": [ "configurable" ], - "watchapp": { - "watchface": false - }, "appKeys": {}, + "capabilities": [ + "configurable" + ], + "companyName": "Meiguro", + "longName": "Pebble.js", "resources": { "media": [ { + "file": "images/menu_icon.png", "menuIcon": true, - "type": "png", "name": "IMAGE_MENU_ICON", - "file": "images/menu_icon.png" - }, { - "type": "png", + "type": "bitmap" + }, + { + "file": "images/logo_splash.png", "name": "IMAGE_LOGO_SPLASH", - "file": "images/logo_splash.png" - }, { - "type": "font", + "type": "bitmap" + }, + { + "file": "images/tile_splash.png", + "name": "IMAGE_TILE_SPLASH", + "type": "bitmap" + }, + { + "file": "fonts/UbuntuMono-Regular.ttf", "name": "MONO_FONT_14", - "file": "fonts/UbuntuMono-Regular.ttf" + "type": "font" } ] + }, + "sdkVersion": "3", + "shortName": "Pebble.js", + "targetPlatforms": [ + "aplite", + "basalt", + "chalk", + "diorite", + "emery" + ], + "uuid": "133215f0-cf20-4c05-997b-3c9be5a64e5b", + "versionCode": 1, + "versionLabel": "0.4", + "watchapp": { + "watchface": false } -} +} \ No newline at end of file diff --git a/doc.html b/doc.html new file mode 100644 index 00000000..705655f4 --- /dev/null +++ b/doc.html @@ -0,0 +1,55 @@ + + + + + + + + Pebble.js + + + + + + + + + + + + + + + + + + + +
+
+

Pebble.js

+ +
+
+ + +
+
+ +
+ +
+
+ + + diff --git a/jsdoc.json b/jsdoc.json deleted file mode 100644 index 94bca640..00000000 --- a/jsdoc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "source": { - "include": [ - "src/js/ajax.js", - "src/js/simply.js" - ] - } -} diff --git a/releases/simply-js-v0.1.1-for-fw-v2.0-beta3.pbw b/releases/simply-js-v0.1.1-for-fw-v2.0-beta3.pbw deleted file mode 100644 index 4f191a43..00000000 Binary files a/releases/simply-js-v0.1.1-for-fw-v2.0-beta3.pbw and /dev/null differ diff --git a/releases/simply-js-v0.2.1-for-fw-v2.0-beta3.pbw b/releases/simply-js-v0.2.1-for-fw-v2.0-beta3.pbw deleted file mode 100644 index 1d5736ae..00000000 Binary files a/releases/simply-js-v0.2.1-for-fw-v2.0-beta3.pbw and /dev/null differ diff --git a/releases/simply-js-v0.2.2-for-fw-v2.0-beta4.pbw b/releases/simply-js-v0.2.2-for-fw-v2.0-beta4.pbw deleted file mode 100644 index b26f92e2..00000000 Binary files a/releases/simply-js-v0.2.2-for-fw-v2.0-beta4.pbw and /dev/null differ diff --git a/releases/simply-js-v0.2.3-for-fw-v2.0-beta4.pbw b/releases/simply-js-v0.2.3-for-fw-v2.0-beta4.pbw deleted file mode 100644 index f26c0d25..00000000 Binary files a/releases/simply-js-v0.2.3-for-fw-v2.0-beta4.pbw and /dev/null differ diff --git a/releases/simply-js-v0.2.4-for-fw-v2.0-beta4.pbw b/releases/simply-js-v0.2.4-for-fw-v2.0-beta4.pbw deleted file mode 100644 index 662f0ef4..00000000 Binary files a/releases/simply-js-v0.2.4-for-fw-v2.0-beta4.pbw and /dev/null differ diff --git a/releases/simply-js-v0.2.5-for-fw-v2.0-beta5.pbw b/releases/simply-js-v0.2.5-for-fw-v2.0-beta5.pbw deleted file mode 100644 index e5d0b088..00000000 Binary files a/releases/simply-js-v0.2.5-for-fw-v2.0-beta5.pbw and /dev/null differ diff --git a/releases/simply-js-v0.3.0-for-fw-v2.0.0.pbw b/releases/simply-js-v0.3.0-for-fw-v2.0.0.pbw deleted file mode 100644 index 26e4b538..00000000 Binary files a/releases/simply-js-v0.3.0-for-fw-v2.0.0.pbw and /dev/null differ diff --git a/releases/simply-js-v0.3.1-for-fw-v2.0.0.pbw b/releases/simply-js-v0.3.1-for-fw-v2.0.0.pbw deleted file mode 100644 index 51457be6..00000000 Binary files a/releases/simply-js-v0.3.1-for-fw-v2.0.0.pbw and /dev/null differ diff --git a/releases/simply-js-v0.3.2-for-fw-v2.0.1.pbw b/releases/simply-js-v0.3.2-for-fw-v2.0.1.pbw deleted file mode 100644 index 57200d2c..00000000 Binary files a/releases/simply-js-v0.3.2-for-fw-v2.0.1.pbw and /dev/null differ diff --git a/releases/simply-js-v0.3.3-for-fw-v2.0.2.pbw b/releases/simply-js-v0.3.3-for-fw-v2.0.2.pbw deleted file mode 100644 index 85887497..00000000 Binary files a/releases/simply-js-v0.3.3-for-fw-v2.0.2.pbw and /dev/null differ diff --git a/releases/simply-js-v0.3.4-for-fw-v2.0.2.pbw b/releases/simply-js-v0.3.4-for-fw-v2.0.2.pbw deleted file mode 100644 index 28a29f85..00000000 Binary files a/releases/simply-js-v0.3.4-for-fw-v2.0.2.pbw and /dev/null differ diff --git a/resources/images/logo_splash.png b/resources/images/logo_splash.png index 4b9fc5dd..da4494e1 100644 Binary files a/resources/images/logo_splash.png and b/resources/images/logo_splash.png differ diff --git a/resources/images/menu_icon.png b/resources/images/menu_icon.png index 2244ebbe..7e44dc19 100644 Binary files a/resources/images/menu_icon.png and b/resources/images/menu_icon.png differ diff --git a/resources/images/tile_splash.png b/resources/images/tile_splash.png new file mode 100644 index 00000000..87f2e401 Binary files /dev/null and b/resources/images/tile_splash.png differ diff --git a/src/html/demo.js b/src/html/demo.js deleted file mode 100644 index 98d16a2a..00000000 --- a/src/html/demo.js +++ /dev/null @@ -1,23 +0,0 @@ -console.log('Simply.js demo!'); - -simply.on('singleClick', function(e) { - console.log(util2.format('single clicked $button!', e)); - simply.subtitle('Pressed ' + e.button + '!'); -}); - -simply.on('longClick', function(e) { - console.log(util2.format('long clicked $button!', e)); - simply.vibe(); - simply.scrollable(e.button !== 'select'); -}); - -simply.on('accelTap', function(e) { - console.log(util2.format('tapped accel axis $axis $direction!', e)); - simply.subtitle('Tapped ' + (e.direction > 0 ? '+' : '-') + e.axis + '!'); -}); - -simply.setText({ - title: 'Simply Demo!', - body: 'This is a demo. Press buttons or tap the watch!', -}, true); - diff --git a/src/html/settings.html b/src/html/settings.html deleted file mode 100644 index 937c55be..00000000 --- a/src/html/settings.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - -Simply.js Settings - - - - - - - - - - -
-
-

Simply.js Settings

-
-
-
-
-
- - - -
-
-
- - - - diff --git a/src/js/README.md b/src/js/README.md deleted file mode 100644 index 65e8e81b..00000000 --- a/src/js/README.md +++ /dev/null @@ -1,15 +0,0 @@ - -# Simply.js - -Simply.js allows you to write interactive text for your Pebble with just JavaScript. - -This is the API reference of Simply.js generated with JSDoc. - -Simply.js provides the following modules: - - * [simply](simply.html) - The Simply.js framework. - * [ajax](global.html#ajax) - An ajax micro library. - * [require](global.html#require) - A synchronous dependency loader provided by simply. - -Visit [simplyjs.meiguro.com](http://simplyjs.meiguro.com) for more details. - diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 00000000..9255c293 --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,88 @@ +/** + * Welcome to Pebble.js! + * + * This is where you write your app. + */ + +var UI = require('ui'); +var Vector2 = require('vector2'); + +var main = new UI.Card({ + title: 'Pebble.js', + icon: 'images/menu_icon.png', + subtitle: 'Hello World!', + body: 'Press any button.', + subtitleColor: 'indigo', // Named colors + bodyColor: '#9a0036' // Hex colors +}); + +main.show(); + +main.on('click', 'up', function(e) { + var menu = new UI.Menu({ + sections: [{ + items: [{ + title: 'Pebble.js', + icon: 'images/menu_icon.png', + subtitle: 'Can do Menus' + }, { + title: 'Second Item', + subtitle: 'Subtitle Text' + }, { + title: 'Third Item', + }, { + title: 'Fourth Item', + }] + }] + }); + menu.on('select', function(e) { + console.log('Selected item #' + e.itemIndex + ' of section #' + e.sectionIndex); + console.log('The item is titled "' + e.item.title + '"'); + }); + menu.show(); +}); + +main.on('click', 'select', function(e) { + var wind = new UI.Window({ + backgroundColor: 'black' + }); + var radial = new UI.Radial({ + size: new Vector2(140, 140), + angle: 0, + angle2: 300, + radius: 20, + backgroundColor: 'cyan', + borderColor: 'celeste', + borderWidth: 1, + }); + var textfield = new UI.Text({ + size: new Vector2(140, 60), + font: 'gothic-24-bold', + text: 'Dynamic\nWindow', + textAlign: 'center' + }); + var windSize = wind.size(); + // Center the radial in the window + var radialPos = radial.position() + .addSelf(windSize) + .subSelf(radial.size()) + .multiplyScalar(0.5); + radial.position(radialPos); + // Center the textfield in the window + var textfieldPos = textfield.position() + .addSelf(windSize) + .subSelf(textfield.size()) + .multiplyScalar(0.5); + textfield.position(textfieldPos); + wind.add(radial); + wind.add(textfield); + wind.show(); +}); + +main.on('click', 'down', function(e) { + var card = new UI.Card(); + card.title('A Card'); + card.subtitle('Is a Window'); + card.body('The simplest window type in Pebble.js.'); + card.show(); +}); diff --git a/src/js/clock/clock.js b/src/js/clock/clock.js new file mode 100644 index 00000000..fcb760cc --- /dev/null +++ b/src/js/clock/clock.js @@ -0,0 +1,12 @@ +var moment = require('vendor/moment'); + +var Clock = module.exports; + +Clock.weekday = function(weekday, hour, minute, seconds) { + var now = moment(); + var target = moment({ hour: hour, minute: minute, seconds: seconds }).day(weekday); + if (moment.max(now, target) === now) { + target.add(1, 'week'); + } + return target.unix(); +}; diff --git a/src/js/clock/index.js b/src/js/clock/index.js new file mode 100644 index 00000000..9b4e7453 --- /dev/null +++ b/src/js/clock/index.js @@ -0,0 +1,3 @@ +var Clock = require('./clock'); + +module.exports = Clock; diff --git a/src/js/ajax.js b/src/js/lib/ajax.js similarity index 62% rename from src/js/ajax.js rename to src/js/lib/ajax.js index 9665e9f3..89b2f981 100644 --- a/src/js/ajax.js +++ b/src/js/lib/ajax.js @@ -7,19 +7,33 @@ var ajax = (function(){ var formify = function(data) { var params = [], i = 0; for (var name in data) { - params[i++] = encodeURI(name) + '=' + encodeURI(data[name]); + params[i++] = encodeURIComponent(name) + '=' + encodeURIComponent(data[name]); } return params.join('&'); }; +var deformify = function(form) { + var params = {}; + form.replace(/(?:([^=&]*)=?([^&]*)?)(?:&|$)/g, function(_, name, value) { + if (name) { + params[name] = value || true; + } + return _; + }); + return params; +}; + /** * ajax options. There are various properties with url being the only required property. * @typedef ajaxOptions * @property {string} [method='get'] - The HTTP method to use: 'get', 'post', 'put', 'delete', 'options', * or any other standard method supported by the running environment. * @property {string} url - The URL to make the ajax request to. e.g. 'http://www.example.com?name=value' - * @property {string} [type='text'] - The expected response format. Specify 'json' to have ajax parse - * the response as json and pass an object as the data parameter. + * @property {string} [type] - The content and response format. By default, the content format + * is 'form' and response format is separately 'text'. Specifying 'json' will have ajax send `data` + * as json as well as parse the response as json. Specifying 'text' allows you to send custom + * formatted content and parse the raw response text. If you wish to send form encoded data and + * parse json, leave `type` undefined and use `JSON.decode` to parse the response data. * @property {object} [data] - The request body, mainly to be used in combination with 'post' or 'put'. * e.g. { username: 'guest' } * @property {object} headers - Custom HTTP headers. Specify additional headers. @@ -55,7 +69,7 @@ var ajax = function(opt, success, failure) { if (opt.cache === false) { var appendSymbol = url.indexOf('?') === -1 ? '?' : '&'; - url += appendSymbol + '_=' + new Date().getTime(); + url += appendSymbol + '_=' + Date.now(); } var req = new XMLHttpRequest(); @@ -68,23 +82,36 @@ var ajax = function(opt, success, failure) { } } - var data = null; - if (opt.data) { + var data = opt.data; + if (data) { if (opt.type === 'json') { req.setRequestHeader('Content-Type', 'application/json'); data = JSON.stringify(opt.data); - } else { + } else if (opt.type === 'xml') { + req.setRequestHeader('Content-Type', 'text/xml'); + } else if (opt.type !== 'text') { + req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); data = formify(opt.data); } } + var ready = false; req.onreadystatechange = function(e) { - if (req.readyState == 4) { + if (req.readyState === 4 && !ready) { + ready = true; var body = req.responseText; - if (opt.type == 'json') { - body = JSON.parse(body); + var okay = req.status >= 200 && req.status < 300 || req.status === 304; + + try { + if (opt.type === 'json') { + body = JSON.parse(body); + } else if (opt.type === 'form') { + body = deformify(body); + } + } catch (err) { + okay = false; } - var callback = req.status == 200 ? success : failure; + var callback = okay ? success : failure; if (callback) { callback(body, req.status, req); } @@ -95,6 +122,13 @@ var ajax = function(opt, success, failure) { }; ajax.formify = formify; +ajax.deformify = deformify; + +if (typeof module !== 'undefined') { + module.exports = ajax; +} else { + window.ajax = ajax; +} return ajax; diff --git a/src/js/lib/color.js b/src/js/lib/color.js new file mode 100644 index 00000000..11b05efe --- /dev/null +++ b/src/js/lib/color.js @@ -0,0 +1,49 @@ +var Color = {}; + +Color.normalizeString = function(color) { + if (typeof color === 'string') { + if (color.substr(0, 2) === '0x') { + return color.substr(2); + } else if (color[0] === '#') { + return color.substr(1); + } + } + return color; +}; + +Color.rgbUint12To24 = function(color) { + return ((color & 0xf00) << 12) | ((color & 0xf0) << 8) | ((color & 0xf) << 4); +}; + +Color.toArgbUint32 = function(color) { + var argb = color; + if (typeof color !== 'number') { + color = Color.normalizeString(color.toString()); + argb = parseInt(color, 16); + } + if (typeof color === 'string') { + var alpha = 0xff000000; + if (color.length === 3) { + argb = alpha | Color.rgbUint12To14(argb); + } else if (color.length === 6) { + argb = alpha | argb; + } + } + return argb; +}; + +Color.toRgbUint24 = function(color) { + return Color.toArgbUint32(color) & 0xffffff; +}; + +Color.toArgbUint8 = function(color) { + var argb = Color.toArgbUint32(color); + return (((argb >> 24) & 0xc0) | ((argb >> 18) & 0x30) | + ((argb >> 12) & 0xc) | ((argb >> 6) & 0x3)); +}; + +Color.toRgbUint8 = function(color) { + return Color.toArgbUint8(color) & 0x3f; +}; + +module.exports = Color; diff --git a/src/js/lib/emitter.js b/src/js/lib/emitter.js new file mode 100644 index 00000000..35b0a8c5 --- /dev/null +++ b/src/js/lib/emitter.js @@ -0,0 +1,154 @@ + +var Emitter = function() { + this._events = {}; +}; + +Emitter.prototype.wrapHandler = function(handler) { + return handler; +}; + +Emitter.prototype._on = function(type, subtype, handler) { + var typeMap = this._events || ( this._events = {} ); + var subtypeMap = typeMap[type] || ( typeMap[type] = {} ); + (subtypeMap[subtype] || ( subtypeMap[subtype] = [] )).push({ + id: handler, + handler: this.wrapHandler(handler), + }); +}; + +Emitter.prototype._off = function(type, subtype, handler) { + if (!type) { + this._events = {}; + return; + } + var typeMap = this._events; + if (!handler && subtype === 'all') { + delete typeMap[type]; + return; + } + var subtypeMap = typeMap[type]; + if (!subtypeMap) { return; } + if (!handler) { + delete subtypeMap[subtype]; + return; + } + var handlers = subtypeMap[subtype]; + if (!handlers) { return; } + var index = -1; + for (var i = 0, ii = handlers.length; i < ii; ++i) { + if (handlers[i].id === handler) { + index = i; + break; + } + } + if (index === -1) { return; } + handlers.splice(index, 1); +}; + +Emitter.prototype.on = function(type, subtype, handler) { + if (!handler) { + handler = subtype; + subtype = 'all'; + } + this._on(type, subtype, handler); + if (Emitter.onAddHandler) { + Emitter.onAddHandler(type, subtype, handler); + } + if (this.onAddHandler) { + this.onAddHandler(type, subtype, handler); + } +}; + +Emitter.prototype.off = function(type, subtype, handler) { + if (!handler) { + handler = subtype; + subtype = 'all'; + } + this._off(type, subtype, handler); + if (Emitter.onRemoveHandler) { + Emitter.onRemoveHandler(type, subtype, handler); + } + if (this.onRemoveHandler) { + this.onRemoveHandler(type, subtype, handler); + } +}; + +Emitter.prototype.listeners = function(type, subtype) { + if (!subtype) { + subtype = 'all'; + } + var typeMap = this._events; + if (!typeMap) { return; } + var subtypeMap = typeMap[type]; + if (!subtypeMap) { return; } + return subtypeMap[subtype]; +}; + +Emitter.prototype.listenerCount = function(type, subtype) { + var listeners = this.listeners(type, subtype); + return listeners ? listeners.length : 0; +}; + +Emitter.prototype.forEachListener = function(type, subtype, callback) { + var typeMap = this._events; + if (!typeMap) { return; } + var subtypeMap; + if (typeof callback === 'function') { + var handlers = this.listeners(type, subtype); + if (!handlers) { return; } + for (var i = 0, ii = handlers.length; i < ii; ++i) { + callback.call(this, type, subtype, handlers[i]); + } + } else if (typeof subtype === 'function') { + callback = subtype; + subtypeMap = typeMap[type]; + if (!subtypeMap) { return; } + for (subtype in subtypeMap) { + this.forEachListener(type, subtype, callback); + } + } else if (typeof type === 'function') { + callback = type; + for (type in typeMap) { + this.forEachListener(type, callback); + } + } +}; + +var emitToHandlers = function(type, handlers, e) { + if (!handlers) { return; } + for (var i = 0, ii = handlers.length; i < ii; ++i) { + var handler = handlers[i].handler; + if (handler.call(this, e, type, i) === false) { + return false; + } + } + return true; +}; + +Emitter.prototype.emit = function(type, subtype, e) { + if (!e) { + e = subtype; + subtype = null; + } + e.type = type; + if (subtype) { + e.subtype = subtype; + } + var typeMap = this._events; + if (!typeMap) { return; } + var subtypeMap = typeMap[type]; + if (!subtypeMap) { return; } + var hadSubtype = emitToHandlers.call(this, type, subtypeMap[subtype], e); + if (hadSubtype === false) { + return false; + } + var hadAll = emitToHandlers.call(this, type, subtypeMap.all, e); + if (hadAll === false) { + return false; + } + if (hadSubtype || hadAll) { + return true; + } +}; + +module.exports = Emitter; diff --git a/src/js/lib/image.js b/src/js/lib/image.js new file mode 100644 index 00000000..7f7d02ad --- /dev/null +++ b/src/js/lib/image.js @@ -0,0 +1,299 @@ +var PNG = require('vendor/png'); + +var PNGEncoder = require('lib/png-encoder'); + +var image = {}; + +var getPos = function(width, x, y) { + return y * width * 4 + x * 4; +}; + +//! Convert an RGB pixel array into a single grey color +var getPixelGrey = function(pixels, pos) { + return ((pixels[pos] + pixels[pos + 1] + pixels[pos + 2]) / 3) & 0xFF; +}; + +//! Convert an RGB pixel array into a single uint8 2 bitdepth per channel color +var getPixelColorUint8 = function(pixels, pos) { + var r = Math.min(Math.max(parseInt(pixels[pos ] / 64 + 0.5), 0), 3); + var g = Math.min(Math.max(parseInt(pixels[pos + 1] / 64 + 0.5), 0), 3); + var b = Math.min(Math.max(parseInt(pixels[pos + 2] / 64 + 0.5), 0), 3); + return (0x3 << 6) | (r << 4) | (g << 2) | b; +}; + +//! Get an RGB vector from an RGB pixel array +var getPixelColorRGB8 = function(pixels, pos) { + return [pixels[pos], pixels[pos + 1], pixels[pos + 2]]; +}; + +//! Normalize the color channels to be identical +image.greyscale = function(pixels, width, height, converter) { + converter = converter || getPixelGrey; + for (var y = 0, yy = height; y < yy; ++y) { + for (var x = 0, xx = width; x < xx; ++x) { + var pos = getPos(width, x, y); + var newColor = converter(pixels, pos); + for (var i = 0; i < 3; ++i) { + pixels[pos + i] = newColor; + } + } + } +}; + +//! Convert to an RGBA pixel array into a row major matrix raster +image.toRaster = function(pixels, width, height, converter) { + converter = converter || getPixelColorRGB8; + var matrix = []; + for (var y = 0, yy = height; y < yy; ++y) { + var row = matrix[y] = []; + for (var x = 0, xx = width; x < xx; ++x) { + var pos = getPos(width, x, y); + row[x] = converter(pixels, pos); + } + } + return matrix; +}; + +image.dithers = {}; + +image.dithers['floyd-steinberg'] = [ + [ 1, 0, 7/16], + [-1, 1, 3/16], + [ 0, 1, 5/16], + [ 1, 1, 1/16]]; + +image.dithers['jarvis-judice-ninke'] = [ + [ 1, 0, 7/48], + [ 2, 0, 5/48], + [-2, 1, 3/48], + [-1, 1, 5/48], + [ 0, 1, 7/48], + [ 1, 1, 5/48], + [ 2, 1, 3/48], + [-2, 2, 1/48], + [-1, 2, 3/48], + [ 0, 2, 5/48], + [ 1, 2, 3/48], + [ 2, 2, 1/48]]; + +image.dithers.sierra = [ + [ 1, 0, 5/32], + [ 2, 0, 3/32], + [-2, 1, 2/32], + [-1, 1, 4/32], + [ 0, 1, 5/32], + [ 1, 1, 4/32], + [ 2, 1, 2/32], + [-1, 2, 2/32], + [ 0, 2, 3/32], + [ 1, 2, 2/32]]; + +image.dithers['default'] = image.dithers.sierra; + +//! Get the nearest normalized grey color +var getChannelGrey = function(color) { + return color >= 128 ? 255 : 0; +}; + +//! Get the nearest normalized 2 bitdepth color +var getChannel2 = function(color) { + return Math.min(Math.max(parseInt(color / 64 + 0.5), 0) * 64, 255); +}; + +image.dither = function(pixels, width, height, dithers, converter) { + converter = converter || getChannel2; + dithers = dithers || image.dithers['default']; + var numDithers = dithers.length; + for (var y = 0, yy = height; y < yy; ++y) { + for (var x = 0, xx = width; x < xx; ++x) { + var pos = getPos(width, x, y); + for (var i = 0; i < 3; ++i) { + var oldColor = pixels[pos + i]; + var newColor = converter(oldColor); + var error = oldColor - newColor; + pixels[pos + i] = newColor; + for (var j = 0; j < numDithers; ++j) { + var dither = dithers[j]; + var x2 = x + dither[0], y2 = y + dither[1]; + if (x2 >= 0 && x2 < width && y < height) { + pixels[getPos(width, x2, y2) + i] += parseInt(error * dither[2]); + } + } + } + } + } +}; + +//! Dither a pixel buffer by image properties +image.ditherByProps = function(pixels, img, converter) { + if (img.dither) { + var dithers = image.dithers[img.dither]; + image.dither(pixels, img.width, img.height, dithers, converter); + } +}; + +image.resizeNearest = function(pixels, width, height, newWidth, newHeight) { + var newPixels = new Array(newWidth * newHeight * 4); + var widthRatio = width / newWidth; + var heightRatio = height / newHeight; + for (var y = 0, yy = newHeight; y < yy; ++y) { + for (var x = 0, xx = newWidth; x < xx; ++x) { + var x2 = parseInt(x * widthRatio); + var y2 = parseInt(y * heightRatio); + var pos2 = getPos(width, x2, y2); + var pos = getPos(newWidth, x, y); + for (var i = 0; i < 4; ++i) { + newPixels[pos + i] = pixels[pos2 + i]; + } + } + } + return newPixels; +}; + +image.resizeSample = function(pixels, width, height, newWidth, newHeight) { + var newPixels = new Array(newWidth * newHeight * 4); + var widthRatio = width / newWidth; + var heightRatio = height / newHeight; + for (var y = 0, yy = newHeight; y < yy; ++y) { + for (var x = 0, xx = newWidth; x < xx; ++x) { + var x2 = Math.min(parseInt(x * widthRatio), width - 1); + var y2 = Math.min(parseInt(y * heightRatio), height - 1); + var pos = getPos(newWidth, x, y); + for (var i = 0; i < 4; ++i) { + newPixels[pos + i] = ((pixels[getPos(width, x2 , y2 ) + i] + + pixels[getPos(width, x2+1, y2 ) + i] + + pixels[getPos(width, x2 , y2+1) + i] + + pixels[getPos(width, x2+1, y2+1) + i]) / 4) & 0xFF; + } + } + } + return newPixels; +}; + +image.resize = function(pixels, width, height, newWidth, newHeight) { + if (newWidth < width || newHeight < height) { + return image.resizeSample(pixels, width, height, newWidth, newHeight); + } else { + return image.resizeNearest(pixels, width, height, newWidth, newHeight); + } +}; + +//! Resize a pixel buffer by image properties +image.resizeByProps = function(pixels, img) { + if (img.width !== img.originalWidth || img.height !== img.originalHeight) { + return image.resize(pixels, img.originalWidth, img.originalHeight, img.width, img.height); + } else { + return pixels; + } +}; + +//! Convert to a GBitmap with bitdepth 1 +image.toGbitmap1 = function(pixels, width, height) { + var rowBytes = width * 4; + + var gpixels = []; + var growBytes = Math.ceil(width / 32) * 4; + for (var i = 0, ii = height * growBytes; i < ii; ++i) { + gpixels[i] = 0; + } + + for (var y = 0, yy = height; y < yy; ++y) { + for (var x = 0, xx = width; x < xx; ++x) { + var grey = 0; + var pos = getPos(width, x, y); + for (var j = 0; j < 3; ++j) { + grey += pixels[pos + j]; + } + grey /= 3 * 255; + if (grey >= 0.5) { + var gbytePos = y * growBytes + parseInt(x / 8); + gpixels[gbytePos] += 1 << (x % 8); + } + } + } + + var gbitmap = { + width: width, + height: height, + pixelsLength: gpixels.length, + pixels: gpixels, + }; + + return gbitmap; +}; + +//! Convert to a PNG with total color bitdepth 8 +image.toPng8 = function(pixels, width, height) { + var raster = image.toRaster(pixels, width, height, getPixelColorRGB8); + + var palette = []; + var colorMap = {}; + var numColors = 0; + for (var y = 0, yy = height; y < yy; ++y) { + var row = raster[y]; + for (var x = 0, xx = width; x < xx; ++x) { + var color = row[x]; + var hash = getPixelColorUint8(color, 0); + if (!(hash in colorMap)) { + colorMap[hash] = numColors; + palette[numColors++] = color; + } + row[x] = colorMap[hash]; + } + } + + var bitdepth = 8; + var colorType = 3; // 8-bit palette + var bytes = PNGEncoder.encode(raster, bitdepth, colorType, palette); + + var png = { + width: width, + height: height, + pixelsLength: bytes.array.length, + pixels: bytes.array, + }; + + return png; +}; + +//! Set the size maintaining the aspect ratio +image.setSizeAspect = function(img, width, height) { + img.originalWidth = width; + img.originalHeight = height; + if (img.width) { + if (!img.height) { + img.height = parseInt(height * (img.width / width)); + } + } else if (img.height) { + if (!img.width) { + img.width = parseInt(width * (img.height / height)); + } + } else { + img.width = width; + img.height = height; + } +}; + +image.load = function(img, bitdepth, callback) { + PNG.load(img.url, function(png) { + var pixels = png.decode(); + if (bitdepth === 1) { + image.greyscale(pixels, png.width, png.height); + } + image.setSizeAspect(img, png.width, png.height); + pixels = image.resizeByProps(pixels, img); + image.ditherByProps(pixels, img, + bitdepth === 1 ? getChannelGrey : getChannel2); + if (bitdepth === 8) { + img.image = image.toPng8(pixels, img.width, img.height); + } else if (bitdepth === 1) { + img.image = image.toGbitmap1(pixels, img.width, img.height); + } + if (callback) { + callback(img); + } + }); + return img; +}; + +module.exports = image; diff --git a/src/js/lib/myutil.js b/src/js/lib/myutil.js new file mode 100644 index 00000000..e46cb6ae --- /dev/null +++ b/src/js/lib/myutil.js @@ -0,0 +1,86 @@ +var util2 = require('util2'); + +var myutil = {}; + +myutil.shadow = function(a, b) { + for (var k in a) { + if (typeof b[k] === 'undefined') { + b[k] = a[k]; + } + } + return b; +}; + +myutil.defun = function(fn, fargs, fbody) { + if (!fbody) { + fbody = fargs; + fargs = []; + } + return new Function('return function ' + fn + '(' + fargs.join(', ') + ') {' + fbody + '}')(); +}; + +myutil.slog = function() { + var args = []; + for (var i = 0, ii = arguments.length; i < ii; ++i) { + args[i] = util2.toString(arguments[i]); + } + return args.join(' '); +}; + +myutil.toObject = function(key, value) { + if (typeof key === 'object') { + return key; + } + var obj = {}; + obj[key] = value; + return obj; +}; + +myutil.flag = function(flags) { + if (typeof flags === 'boolean') { + return flags; + } + for (var i = 1, ii = arguments.length; i < ii; ++i) { + if (flags[arguments[i]]) { + return true; + } + } + return false; +}; + +myutil.toFlags = function(flags) { + if (typeof flags === 'string') { + flags = myutil.toObject(flags, true); + } else { + flags = !!flags; + } + return flags; +}; + +/** + * Returns an absolute path based on a root path and a relative path. + */ +myutil.abspath = function(root, path) { + if (!path) { + path = root; + } + if (path.match(/^\/\//)) { + var m = root && root.match(/^(\w+:)\/\//); + path = (m ? m[1] : 'http:') + path; + } + if (root && !path.match(/^\w+:\/\//)) { + path = root + path; + } + return path; +}; + +/** + * Converts a name to a C constant name format of UPPER_CASE_UNDERSCORE. + */ +myutil.toCConstantName = function(x) { + x = x.toUpperCase(); + x = x.replace(/[- ]/g, '_'); + return x; +}; + +module.exports = myutil; diff --git a/src/js/lib/png-encoder.js b/src/js/lib/png-encoder.js new file mode 100644 index 00000000..3bfd929b --- /dev/null +++ b/src/js/lib/png-encoder.js @@ -0,0 +1,379 @@ +/** + * PNG Encoder from data-demo + * https://code.google.com/p/data-demo/ + * + * @author mccalluc@yahoo.com + * @license MIT + */ + +var Zlib = require('vendor/zlib'); + +var png = {}; + +png.Bytes = function(data, optional) { + var datum, i; + this.array = []; + + if (!optional) { + + if (data instanceof Array || data instanceof Uint8Array) { + for (i = 0; i < data.length; i++) { + datum = data[i]; + if (datum !== null) { // nulls and undefineds are silently skipped. + if (typeof datum !== "number") { + throw new Error("Expected number, not "+(typeof datum)); + } else if (Math.floor(datum) != datum) { + throw new Error("Expected integer, not "+datum); + } else if (datum < 0 || datum > 255) { + throw new Error("Expected integer in [0,255], not "+datum); + } + this.array.push(datum); + } + } + } + + else if (typeof data == "string") { + for (i = 0; i < data.length; i++) { + datum = data.charCodeAt(i); + if (datum < 0 || datum > 255) { + throw new Error("Characters above 255 not allowed without explicit encoding: "+datum); + } + this.array.push(datum); + } + } + + else if (data instanceof png.Bytes) { + this.array.push.apply(this.array, data.array); + } + + else if (typeof data == "number") { + return new png.Bytes([data]); + } + + else { + throw new Error("Unexpected data type: "+data); + } + + } + + else { // optional is defined. + + // TODO: generalize when generalization is required. + if (typeof data == "number" && + Math.floor(data) == data && + data >= 0 && + (optional.bytes in {4:1, 2:1}) && + // don't change this last one to bit shifts: in JS, 0x100 << 24 == 0. + data < Math.pow(256, optional.bytes)) { + this.array = [ + (data & 0xFF000000) >>> 24, + (data & 0x00FF0000) >>> 16, + (data & 0x0000FF00) >>> 8, + (data & 0x000000FF) + ].slice(-optional.bytes); + } + + else throw new Error("Unexpected data/optional args combination: "+data); + + } +}; + +png.Bytes.prototype.add = function(data, optional) { + // Takes the same arguments as the constructor, + // but appends the new data instead, and returns the modified object. + // (suitable for chaining.) + this.array.push.apply(this.array, new png.Bytes(data, optional).array); + return this; +}; + +png.Bytes.prototype.chunk = function(n) { + // Split the array into chunks of length n. + // Returns an array of arrays. + var buffer = []; + for (var i = 0; i < this.array.length; i += n) { + var slice = this.array.slice(i, i+n); + buffer.push(this.array.slice(i, i+n)); + } + return buffer; +}; + +png.Bytes.prototype.toString = function(n) { + // one optional argument specifies line length in bytes. + // returns a hex dump of the Bytes object. + var chunks = this.chunk(n || 8); + var byte; + var lines = []; + var hex; + var chr; + for (var i = 0; i < chunks.length; i++) { + hex = []; + chr = []; + for (var j = 0; j < chunks[i].length; j++) { + byte = chunks[i][j]; + hex.push( + ((byte < 16) ? "0" : "") + + byte.toString(16) + ); + chr.push( + (byte >=32 && byte <= 126 ) ? + String.fromCharCode(byte) + : "_" + ); + } + lines.push(hex.join(" ")+" "+chr.join("")); + } + return lines.join("\n"); +}; + +png.Bytes.prototype.serialize = function() { + // returns a string whose char codes correspond to the bytes of the array. + // TODO: get rid of this once transition is complete? + return String.fromCharCode.apply(null, this.array); +}; + +png.fromRaster = function(raster, optional_palette, optional_transparency) { + // Given a Raster object, + // and optionally a list of rgb triples, + // and optionally a corresponding list of transparency values (0: clear - 255: opaque) + // return the corresponding PNG as a Bytes object. + + var signature = new png.Bytes([ + 137, 80 /* P */, 78 /* N */, 71 /* G */, 13, 10, 26, 10 + ]); + var ihdr = new png.Chunk.IHDR(raster.width, raster.height, raster.bit_depth, raster.color_type); + var plte = (optional_palette instanceof Array) ? + new png.Chunk.PLTE(optional_palette) : + new png.Bytes([]); + var trns = (optional_transparency instanceof Array) ? + new png.Chunk.tRNS(optional_transparency) : + new png.Bytes([]); + var idat = new png.Chunk.IDAT(raster); + var iend = new png.Chunk.IEND(); // intentionally blank + + // order matters. + return signature.add(ihdr).add(plte).add(trns).add(idat).add(iend); +}; + +png.encode = function(raster, bit_depth, color_type, optional_palette, optional_transparency) { + if (color_type === 0 || color_type === 3) { + raster = new png.Raster(bit_depth, color_type, raster); + } else if (color_type === 2 || color_type === 6) { + raster = new png.Raster_rgb(bit_depth, color_type, raster); + } + return png.fromRaster(raster, optional_palette, optional_transparency); +}; + +png.Chunk = function(type, data) { + // given a four character type, and Bytes, + // calculates the length and the checksum, and creates + // a Bytes object for that png chunk. + + if (!type.match(/^[A-Za-z]{4}$/)) { + throw new Error("Creating PNG chunk: provided type should be four letters, not "+type); + } + + if (!(data instanceof png.Bytes)) { + throw new Error("Creating PNG "+type+" chunk: provided data is not Bytes: "+data); + } + + // CRC calculations are a literal translation of the C code at + // http://www.libpng.org/pub/png/spec/1.0/PNG-CRCAppendix.html + if (!png.crc_table) { + png.crc_table = []; // Table of CRCs of all 8-bit messages. + for (var n = 0; n < 256; n++) { + var c = n; + for (var k = 0; k < 8; k++) { + if (c & 1) { + c = 0xedb88320 ^ (c >>> 1); // C ">>" is JS ">>>" + } else { + c = c >>> 1; // C ">>" is JS ">>>" + } + } + png.crc_table[n] = c; + } + } + + function update_crc(crc, buffer) { + // Update a running CRC with the buffer--the CRC + // should be initialized to all 1's, and the transmitted value + // is the 1's complement of the final running CRC + var c = crc; + var n; + for (n = 0; n < buffer.length; n++) { + c = png.crc_table[(c ^ buffer[n]) & 0xff] ^ (c >>> 8); // C ">>" is JS ">>>" + } + return c; + } + + var type_and_data = new png.Bytes(type).add(data); + var crc = (update_crc(0xffffffff, type_and_data.array) ^ 0xffffffff)>>>0; + // >>>0 converts to unsigned, without changing the bits. + + var length_type_data_checksum = + new png.Bytes(data.array.length,{bytes:4}) + .add(type_and_data) + .add(crc,{bytes:4}); + + return length_type_data_checksum; +}; + +png.Chunk.IHDR = function(width, height, bit_depth, color_type) { + if (!( + // grayscale + (color_type === 0) && (bit_depth in {1:1, 2:1, 4:1, 8:1, 16:1}) || + // rgb + (color_type === 2) && (bit_depth in {8:1, 16:1}) || + // palette + (color_type === 3) && (bit_depth in {1:1, 2:1, 4:1, 8:1}) || + // grayscale + alpha + (color_type === 4) && (bit_depth in {8:1, 16:1}) || + // rgb + alpha + (color_type === 6) && (bit_depth in {8:1, 16:1}) + // http://www.libpng.org/pub/png/spec/1.0/PNG-Chunks.html#C.IHDR + )) { + throw new Error("Invalid color type ("+color_type+") / bit depth ("+bit_depth+") combo"); + } + return new png.Chunk( + "IHDR", + new png.Bytes(width,{bytes:4}) + .add(height,{bytes:4}) + .add([ + bit_depth, + color_type, + 0, // compression method + 0, // filter method + 0 // interlace method + ]) + ); +}; + +png.Chunk.PLTE = function(rgb_list) { + // given a list of RGB triples, + // returns the corresponding PNG PLTE (palette) chunk. + for (var i = 0, ii = rgb_list.length; i < ii; i++) { + var triple = rgb_list[i]; + if (triple.length !== 3) { + throw new Error("This is not a valid RGB triple: "+triple); + } + } + return new png.Chunk( + "PLTE", + new png.Bytes(Array.prototype.concat.apply([], rgb_list)) + ); +}; + +png.Chunk.tRNS = function(alpha_list) { + // given a list of alpha values corresponding to the palette entries, + // returns the corresponding PNG tRNS (transparency) chunk. + return new png.Chunk( + "tRNS", + new png.Bytes(alpha_list) + ); +}; + +png.Raster = function(bit_depth, color_type, raster) { + // takes an array of arrays of greyscale or palette values. + // provides encode(), which returns bytes ready for a PNG IDAT chunk. + + // validate depth and type + if (color_type !== 0 && color_type !== 3) throw new Error("Color type "+color_type+" is unsupported."); + if (bit_depth > 8) throw new Error("Bit depths greater than 8 are unsupported."); + + this.bit_depth = bit_depth; + this.color_type = color_type; + + // validate raster data. + var max_value = (1 << bit_depth) - 1; + var cols = raster[0].length; + for (var row = 0; row < raster.length; row++) { + if (raster[row].length != cols) + throw new Error("Row "+row+" does not have the expected "+cols+" columns."); + for (var col = 0; col < cols; col++) { + if (!(raster[row][col] >= 0 && raster[row][col] <= max_value)) + throw new Error("Image data ("+raster[row][col]+") out of bounds at ("+row+","+col+")"); + } + } + + this.height = raster.length; + this.width = cols; + + this.encode = function() { + // Returns the image data as a single array of bytes, using filter method 0. + var buffer = []; + for (var row = 0; row < raster.length; row++) { + buffer.push(0); // each row gets filter type 0. + for (var col = 0; col < cols; col += 8/bit_depth) { + var byte = 0; + for (var sub = 0; sub < 8/bit_depth; sub++) { + byte <<= bit_depth; + if (col + sub < cols) { + byte |= raster[row][col+sub]; + } + } + if (byte & ~0xFF) throw new Error("Encoded raster byte out of bounds at ("+row+","+col+")"); + buffer.push(byte); + } + } + return buffer; + }; +}; + +png.Raster_rgb = function(bit_depth, color_type, raster) { + // takes an array of arrays of RGB triples. + // provides encode(), which returns bytes ready for a PNG IDAT chunk. + + // validate depth and type + if (color_type != 2 && color_type != 6) throw new Error("Only color types 2 and 6 for RGB."); + if (bit_depth != 8) throw new Error("Bit depths other than 8 are unsupported for RGB."); + + this.bit_depth = bit_depth; + this.color_type = color_type; + + // validate raster data. + var cols = raster[0].length; + for (var row = 0; row < raster.length; row++) { + if (raster[row].length != cols) { + throw new Error("Row "+row+" does not have the expected "+cols+" columns."); + } + for (var col = 0; col < cols; col++) { + if (!(color_type == 2 && raster[row][col].length == 3) && + !(color_type == 6 && raster[row][col].length == 4)) { + throw new Error("Not RGB[A] at ("+row+","+col+")"); + } + for (var i = 0; i < (color_type == 2 ? 3 : 4); i++) { + if (raster[row][col][i]<0 || raster[row][col][i]>255) { + throw new Error("RGB out of range at ("+row+","+col+")"); + } + } + } + } + + this.height = raster.length; + this.width = cols; + + this.encode = function() { + // Returns the image data as a single array of bytes, using filter method 0. + var buffer = []; + for (var row = 0; row < raster.length; row++) { + buffer.push(0); // each row gets filter type 0. + for (var col = 0; col < cols; col++) { + buffer.push.apply(buffer, raster[row][col]); + } + } + return buffer; + }; +}; + +png.Chunk.IDAT = function(raster) { + var encoded = raster.encode(); + var zipped = new Zlib.Deflate(encoded).compress(); + return new png.Chunk("IDAT", new png.Bytes(zipped)); +}; + +png.Chunk.IEND = function() { + return new png.Chunk("IEND", new png.Bytes([])); +}; + +if (typeof module !== 'undefined') { + module.exports = png; +} diff --git a/src/js/lib/safe.js b/src/js/lib/safe.js new file mode 100644 index 00000000..2184199e --- /dev/null +++ b/src/js/lib/safe.js @@ -0,0 +1,214 @@ +/* safe.js - Building a safer world for Pebble.JS Developers + * + * This library provides wrapper around all the asynchronous handlers that developers + * have access to so that error messages are caught and displayed nicely in the pebble tool + * console. + */ + +/* global __loader */ + +var safe = {}; + +/* The name of the concatenated file to translate */ +safe.translateName = 'pebble-js-app.js'; + +safe.indent = ' '; + +/* Translates a source line position to the originating file */ +safe.translatePos = function(name, lineno, colno) { + if (name === safe.translateName) { + var pkg = __loader.getPackageByLineno(lineno); + if (pkg) { + name = pkg.filename; + lineno -= pkg.lineno; + } + } + return name + ':' + lineno + ':' + colno; +}; + +var makeTranslateStack = function(stackLineRegExp, translateLine) { + return function(stack, level) { + var lines = stack.split('\n'); + var firstStackLine = -1; + for (var i = lines.length - 1; i >= 0; --i) { + var m = lines[i].match(stackLineRegExp); + if (!m) { + continue; + } + var line = lines[i] = translateLine.apply(this, m); + if (line) { + firstStackLine = i; + if (line.indexOf(module.filename) !== -1) { + lines.splice(i, 1); + } + } else { + lines.splice(i, lines.length - i); + } + } + if (firstStackLine > -1) { + lines.splice(firstStackLine, level); + } + return lines; + }; +}; + +/* Translates a node style stack trace line */ +var translateLineV8 = function(line, msg, scope, name, lineno, colno) { + var pos = safe.translatePos(name, lineno, colno); + return msg + (scope ? ' ' + scope + ' (' + pos + ')' : pos); +}; + +/* Matches ( '(')? ':' ':' ')'? */ +var stackLineRegExpV8 = /(.+?)(?:\s+([^\s]+)\s+\()?([^\s@:]+):(\d+):(\d+)\)?/; + +safe.translateStackV8 = makeTranslateStack(stackLineRegExpV8, translateLineV8); + +/* Translates an iOS stack trace line to node style */ +var translateLineIOS = function(line, scope, name, lineno, colno) { + var pos = safe.translatePos(name, lineno, colno); + return safe.indent + 'at ' + (scope ? scope + ' (' + pos + ')' : pos); +}; + +/* Matches ( '@' )? ':' ':' */ +var stackLineRegExpIOS = /(?:([^\s@]+)@)?([^\s@:]+):(\d+):(\d+)/; + +safe.translateStackIOS = makeTranslateStack(stackLineRegExpIOS, translateLineIOS); + +/* Translates an Android stack trace line to node style */ +var translateLineAndroid = function(line, msg, scope, name, lineno, colno) { + if (name !== 'jskit_startup.js') { + return translateLineV8(line, msg, scope, name, lineno, colno); + } +}; + +/* Matches '('? filepath ':' ':' ')'? */ +var stackLineRegExpAndroid = /^(.*?)(?:\s+([^\s]+)\s+\()?[^\s\(]*?([^\/]*?):(\d+):(\d+)\)?/; + +safe.translateStackAndroid = makeTranslateStack(stackLineRegExpAndroid, translateLineAndroid); + +/* Translates a stack trace to the originating files */ +safe.translateStack = function(stack, level) { + level = level || 0; + if (Pebble.platform === 'pypkjs') { + return safe.translateStackV8(stack, level); + } else if (stack.match('com.getpebble.android')) { + return safe.translateStackAndroid(stack, level); + } else { + return safe.translateStackIOS(stack, level); + } +}; + +var normalizeIndent = function(lines, pos) { + pos = pos || 0; + var label = lines[pos].match(/^[^\s]* /); + if (label) { + var indent = label[0].replace(/./g, ' '); + for (var i = pos + 1, ii = lines.length; i < ii; i++) { + lines[i] = lines[i].replace(/^\t/, indent); + } + } + return lines; +}; + +safe.translateError = function(err, intro, level) { + var name = err.name; + var message = err.message || err.toString(); + var stack = err.stack; + var result = [intro || 'JavaScript Error:']; + if (message && (!stack || stack.indexOf(message) === -1)) { + if (name && message.indexOf(name + ':') === -1) { + message = name + ': ' + message; + } + result.push(message); + } + if (stack) { + Array.prototype.push.apply(result, safe.translateStack(stack, level)); + } + return normalizeIndent(result, 1).join('\n'); +}; + +/* Dumps error messages to the console. */ +safe.dumpError = function(err, intro, level) { + if (typeof err === 'object') { + console.log(safe.translateError(err, intro, level)); + } else { + console.log('Error: dumpError argument is not an object'); + } +}; + +/* Logs runtime warnings to the console. */ +safe.warn = function(message, level, name) { + var err = new Error(message); + err.name = name || 'Warning'; + safe.dumpError(err, 'Warning:', level); +}; + +/* Takes a function and return a new function with a call to it wrapped in a try/catch statement */ +safe.protect = function(fn) { + return fn ? function() { + try { + fn.apply(this, arguments); + } catch (err) { + safe.dumpError(err); + } + } : undefined; +}; + +/* Wrap event handlers added by Pebble.addEventListener */ +var pblAddEventListener = Pebble.addEventListener; +Pebble.addEventListener = function(eventName, eventCallback) { + pblAddEventListener.call(this, eventName, safe.protect(eventCallback)); +}; + +var pblSendMessage = Pebble.sendAppMessage; +Pebble.sendAppMessage = function(message, success, failure) { + return pblSendMessage.call(this, message, safe.protect(success), safe.protect(failure)); +}; + +/* Wrap setTimeout and setInterval */ +var originalSetTimeout = setTimeout; +window.setTimeout = function(callback, delay) { + if (safe.warnSetTimeoutNotFunction !== false && typeof callback !== 'function') { + safe.warn('setTimeout was called with a `' + (typeof callback) + '` type. ' + + 'Did you mean to pass a function?'); + safe.warnSetTimeoutNotFunction = false; + } + return originalSetTimeout(safe.protect(callback), delay); +}; + +var originalSetInterval = setInterval; +window.setInterval = function(callback, delay) { + if (safe.warnSetIntervalNotFunction !== false && typeof callback !== 'function') { + safe.warn('setInterval was called with a `' + (typeof callback) + '` type. ' + + 'Did you mean to pass a function?'); + safe.warnSetIntervalNotFunction = false; + } + return originalSetInterval(safe.protect(callback), delay); +}; + +/* Wrap the geolocation API Callbacks */ +var watchPosition = navigator.geolocation.watchPosition; +navigator.geolocation.watchPosition = function(success, error, options) { + return watchPosition.call(this, safe.protect(success), safe.protect(error), options); +}; + +var getCurrentPosition = navigator.geolocation.getCurrentPosition; +navigator.geolocation.getCurrentPosition = function(success, error, options) { + return getCurrentPosition.call(this, safe.protect(success), safe.protect(error), options); +}; + +var ajax; + +/* Try to load the ajax library if available and silently fail if it is not found. */ +try { + ajax = require('ajax'); +} catch (err) {} + +/* Wrap the success and failure callback of the ajax library */ +if (ajax) { + ajax.onHandler = function(eventName, callback) { + return safe.protect(callback); + }; +} + +module.exports = safe; diff --git a/src/js/lib/struct.js b/src/js/lib/struct.js new file mode 100644 index 00000000..9989b161 --- /dev/null +++ b/src/js/lib/struct.js @@ -0,0 +1,259 @@ +/** + * struct.js - chainable ArrayBuffer DataView wrapper + * + * @author Meiguro / http://meiguro.com/ + * @license MIT + */ + +var capitalize = function(str) { + return str.charAt(0).toUpperCase() + str.substr(1); +}; + +var struct = function(def) { + this._littleEndian = true; + this._offset = 0; + this._cursor = 0; + this._makeAccessors(def); + this._view = new DataView(new ArrayBuffer(this._size)); + this._def = def; +}; + +struct.types = { + int8: { size: 1 }, + uint8: { size: 1 }, + int16: { size: 2 }, + uint16: { size: 2 }, + int32: { size: 4 }, + uint32: { size: 4 }, + int64: { size: 8 }, + uint64: { size: 8 }, + float32: { size: 2 }, + float64: { size: 4 }, + cstring: { size: 1, dynamic: true }, + data: { size: 0, dynamic: true }, +}; + +var makeDataViewAccessor = function(type, typeName) { + var getName = 'get' + capitalize(typeName); + var setName = 'set' + capitalize(typeName); + type.get = function(offset, little) { + this._advance = type.size; + return this._view[getName](offset, little); + }; + type.set = function(offset, value, little) { + this._advance = type.size; + this._view[setName](offset, value, little); + }; +}; + +for (var k in struct.types) { + var type = struct.types[k]; + makeDataViewAccessor(type, k); +} + +struct.types.bool = struct.types.uint8; + +struct.types.uint64.get = function(offset, little) { + var buffer = this._view; + var a = buffer.getUint32(offset, little); + var b = buffer.getUint32(offset + 4, little); + this._advance = 8; + return ((little ? b : a) << 32) + (little ? a : b); +}; + +struct.types.uint64.set = function(offset, value, little) { + var a = value & 0xFFFFFFFF; + var b = (value >> 32) & 0xFFFFFFFF; + var buffer = this._view; + buffer.setUint32(offset, little ? a : b, little); + buffer.setUint32(offset + 4, little ? b : a, little); + this._advance = 8; +}; + +struct.types.cstring.get = function(offset) { + var chars = []; + var buffer = this._view; + for (var i = offset, ii = buffer.byteLength, j = 0; i < ii && buffer.getUint8(i) !== 0; ++i, ++j) { + chars[j] = String.fromCharCode(buffer.getUint8(i)); + } + this._advance = chars.length + 1; + return decodeURIComponent(escape(chars.join(''))); +}; + +struct.types.cstring.set = function(offset, value) { + value = unescape(encodeURIComponent(value)); + this._grow(offset + value.length + 1); + var i = offset; + var buffer = this._view; + for (var j = 0, jj = value.length; j < jj && value[i] !== '\0'; ++i, ++j) { + buffer.setUint8(i, value.charCodeAt(j)); + } + buffer.setUint8(i, 0); + this._advance = value.length + 1; +}; + +struct.types.data.get = function(offset) { + var length = this._value; + this._cursor = offset; + var buffer = this._view; + var copy = new DataView(new ArrayBuffer(length)); + for (var i = 0; i < length; ++i) { + copy.setUint8(i, buffer.getUint8(i + offset)); + } + this._advance = length; + return copy; +}; + +struct.types.data.set = function(offset, value) { + var length = value.byteLength || value.length; + this._cursor = offset; + this._grow(offset + length); + var buffer = this._view; + if (value instanceof ArrayBuffer) { + value = new DataView(value); + } + for (var i = 0; i < length; ++i) { + buffer.setUint8(i + offset, value instanceof DataView ? value.getUint8(i) : value[i]); + } + this._advance = length; +}; + +struct.prototype._grow = function(target) { + var buffer = this._view; + var size = buffer.byteLength; + if (target <= size) { return; } + while (size < target) { size *= 2; } + var copy = new DataView(new ArrayBuffer(size)); + for (var i = 0; i < buffer.byteLength; ++i) { + copy.setUint8(i, buffer.getUint8(i)); + } + this._view = copy; +}; + +struct.prototype._prevField = function(field) { + field = field || this._access; + var fieldIndex = this._fields.indexOf(field); + return this._fields[fieldIndex - 1]; +}; + +struct.prototype._makeAccessor = function(field) { + this[field.name] = function(value) { + var type = field.type; + + if (field.dynamic) { + var prevField = this._prevField(field); + if (prevField === undefined) { + this._cursor = 0; + } else if (this._access === field) { + this._cursor -= this._advance; + } else if (this._access !== prevField) { + throw new Error('dynamic field requires sequential access'); + } + } else { + this._cursor = field.index; + } + this._access = field; + var result = this; + if (arguments.length === 0) { + result = type.get.call(this, this._offset + this._cursor, this._littleEndian); + this._value = result; + } else { + if (field.transform) { + value = field.transform(value, field); + } + type.set.call(this, this._offset + this._cursor, value, this._littleEndian); + this._value = value; + } + this._cursor += this._advance; + return result; + }; + return this; +}; + +struct.prototype._makeMetaAccessor = function(name, transform) { + this[name] = function(value, field) { + transform.call(this, value, field); + return this; + }; +}; + +struct.prototype._makeAccessors = function(def, index, fields, prefix) { + index = index || 0; + this._fields = ( fields = fields || [] ); + var prevField = fields[fields.length]; + for (var i = 0, ii = def.length; i < ii; ++i) { + var member = def[i]; + var type = member[0]; + if (typeof type === 'string') { + type = struct.types[type]; + } + var name = member[1]; + if (prefix) { + name = prefix + capitalize(name); + } + var transform = member[2]; + if (type instanceof struct) { + if (transform) { + this._makeMetaAccessor(name, transform); + } + this._makeAccessors(type._def, index, fields, name); + index = this._size; + continue; + } + var field = { + index: index, + type: type, + name: name, + transform: transform, + dynamic: type.dynamic || prevField && prevField.dynamic, + }; + this._makeAccessor(field); + fields.push(field); + index += type.size; + prevField = field; + } + this._size = index; + return this; +}; + +struct.prototype.prop = function(def) { + var fields = this._fields; + var i = 0, ii = fields.length, name; + if (arguments.length === 0) { + var obj = {}; + for (; i < ii; ++i) { + name = fields[i].name; + obj[name] = this[name](); + } + return obj; + } + for (; i < ii; ++i) { + name = fields[i].name; + if (name in def) { + this[name](def[name]); + } + } + return this; +}; + +struct.prototype.view = function(view) { + if (arguments.length === 0) { + return this._view; + } + if (view instanceof ArrayBuffer) { + view = new DataView(view); + } + this._view = view; + return this; +}; + +struct.prototype.offset = function(offset) { + if (arguments.length === 0) { + return this._offset; + } + this._offset = offset; + return this; +}; + +module.exports = struct; + diff --git a/src/js/util2.js b/src/js/lib/util2.js similarity index 59% rename from src/js/util2.js rename to src/js/lib/util2.js index 019d4dd4..090185f6 100644 --- a/src/js/util2.js +++ b/src/js/lib/util2.js @@ -2,33 +2,59 @@ * util2.js by Meiguro - MIT License */ -var util2 = (function(util2){ +var util2 = (function(){ + +var util2 = {}; util2.noop = function() {}; -util2.copy = function (a, b) { +util2.count = function(o) { + var i = 0; + for (var k in o) { ++i; } + return i; +}; + +util2.copy = function(a, b) { b = b || (a instanceof Array ? [] : {}); for (var k in a) { b[k] = a[k]; } return b; }; -util2.toInteger = function (x) { +util2.toInteger = function(x) { if (!isNaN(x = parseInt(x))) { return x; } }; -util2.toNumber = function (x) { +util2.toNumber = function(x) { if (!isNaN(x = parseFloat(x))) { return x; } }; -util2.toArray = function (x) { +util2.toString = function(x) { + return typeof x === 'object' ? JSON.stringify.apply(this, arguments) : '' + x; +}; + +util2.toArray = function(x) { if (x instanceof Array) { return x; } + if (x[0]) { return util2.copy(x, []); } return [x]; }; -util2.trim = function (s) { +util2.trim = function(s) { return s ? s.toString().trim() : s; }; +util2.last = function(a) { + return a[a.length-1]; +}; + +util2.inherit = function(child, parent, proto) { + child.prototype = Object.create(parent.prototype); + child.prototype.constructor = child; + if (proto) { + util2.copy(proto, child.prototype); + } + return child.prototype; +}; + var chunkSize = 128; var randomBytes = function(chunkSize) { @@ -39,7 +65,7 @@ var randomBytes = function(chunkSize) { return z.join(''); }; -util2.randomString = function (regex, size, acc) { +util2.randomString = function(regex, size, acc) { if (!size) { return ''; } @@ -60,7 +86,7 @@ util2.randomString = function (regex, size, acc) { var varpat = new RegExp("^([\\s\\S]*?)\\$([_a-zA-Z0-9]+)", "m"); -util2.format = function (text, table) { +util2.format = function(text, table) { var m, z = ''; while ((m = text.match(varpat))) { var subtext = m[0], value = table[m[2]]; @@ -72,6 +98,10 @@ util2.format = function (text, table) { return z; }; +if (typeof module !== 'undefined') { + module.exports = util2; +} + return util2; -})(typeof util2 !== 'undefined' ? util2 : {}); +})(); diff --git a/src/js/lib/vector2.js b/src/js/lib/vector2.js new file mode 100644 index 00000000..b8c3a79a --- /dev/null +++ b/src/js/lib/vector2.js @@ -0,0 +1,174 @@ +/** + * Vector2 from three.js + * https://github.com/mrdoob/three.js + * + * @author mr.doob / http://mrdoob.com/ + * @author philogb / http://blog.thejit.org/ + * @author egraether / http://egraether.com/ + * @author zz85 / http://www.lab4games.net/zz85/blog + */ + +/** + * Create a new vector with given dimensions. + * @param x + * @param y + */ +var Vector2 = function ( x, y ) { + + this.x = x || 0; + this.y = y || 0; + +}; + +Vector2.prototype = { + + constructor: Vector2, + + set: function ( x, y ) { + + this.x = x; + this.y = y; + + return this; + + }, + + copy: function ( v ) { + + this.x = v.x; + this.y = v.y; + + return this; + + }, + + clone: function () { + + return new Vector2( this.x, this.y ); + + }, + + add: function ( v1, v2 ) { + + this.x = v1.x + v2.x; + this.y = v1.y + v2.y; + + return this; + + }, + + addSelf: function ( v ) { + + this.x += v.x; + this.y += v.y; + + return this; + + }, + + sub: function ( v1, v2 ) { + + this.x = v1.x - v2.x; + this.y = v1.y - v2.y; + + return this; + + }, + + subSelf: function ( v ) { + + this.x -= v.x; + this.y -= v.y; + + return this; + + }, + + multiplyScalar: function ( s ) { + + this.x *= s; + this.y *= s; + + return this; + + }, + + divideScalar: function ( s ) { + + if ( s ) { + + this.x /= s; + this.y /= s; + + } else { + + this.set( 0, 0 ); + + } + + return this; + + }, + + + negate: function() { + + return this.multiplyScalar( -1 ); + + }, + + dot: function ( v ) { + + return this.x * v.x + this.y * v.y; + + }, + + lengthSq: function () { + + return this.x * this.x + this.y * this.y; + + }, + + length: function () { + + return Math.sqrt( this.lengthSq() ); + + }, + + normalize: function () { + + return this.divideScalar( this.length() ); + + }, + + distanceTo: function ( v ) { + + return Math.sqrt( this.distanceToSquared( v ) ); + + }, + + distanceToSquared: function ( v ) { + + var dx = this.x - v.x, dy = this.y - v.y; + return dx * dx + dy * dy; + + }, + + + setLength: function ( l ) { + + return this.normalize().multiplyScalar( l ); + + }, + + equals: function( v ) { + + return ( ( v.x === this.x ) && ( v.y === this.y ) ); + + } + +}; + +if (typeof module !== 'undefined') { + module.exports = Vector2; +} diff --git a/src/js/loader.js b/src/js/loader.js new file mode 100644 index 00000000..3cba762a --- /dev/null +++ b/src/js/loader.js @@ -0,0 +1,109 @@ +var __loader = (function() { + +var loader = {}; + +loader.packages = {}; + +loader.packagesLinenoOrder = [{ filename: 'loader.js', lineno: 0 }]; + +loader.extpaths = ['?', '?.js', '?.json', '?/index.js']; + +loader.paths = ['/', 'lib', 'vendor']; + +loader.basepath = function(path) { + return path.replace(/[^\/]*$/, ''); +}; + +var replace = function(a, regexp, b) { + var z; + do { + z = a; + } while (z !== (a = a.replace(regexp, b))); + return z; +}; + +loader.normalize = function(path) { + path = replace(path, /(?:(^|\/)\.?\/)+/g, '$1'); + path = replace(path, /[^\/]*\/\.\.\//, ''); + return path; +}; + +loader.require = function(path, requirer) { + var module = loader.getPackage(path, requirer); + if (!module) { + throw new Error("Cannot find module '" + path + "'"); + } + + if (module.exports) { + return module.exports; + } + + var require = function(path) { return loader.require(path, module); }; + + module.exports = {}; + module.loader(module.exports, module, require); + module.loaded = true; + + return module.exports; +}; + +var compareLineno = function(a, b) { return a.lineno - b.lineno; }; + +loader.define = function(path, lineno, loadfun) { + var module = { + filename: path, + lineno: lineno, + loader: loadfun, + }; + + loader.packages[path] = module; + loader.packagesLinenoOrder.push(module); + loader.packagesLinenoOrder.sort(compareLineno); +}; + +loader.getPackage = function(path, requirer) { + var module; + if (requirer) { + module = loader.getPackageAtPath(loader.basepath(requirer.filename) + '/' + path); + } + + if (!module) { + module = loader.getPackageAtPath(path); + } + + var paths = loader.paths; + for (var i = 0, ii = paths.length; !module && i < ii; ++i) { + var dirpath = paths[i]; + module = loader.getPackageAtPath(dirpath + '/' + path); + } + return module; +}; + +loader.getPackageAtPath = function(path) { + path = loader.normalize(path); + + var module; + var extpaths = loader.extpaths; + for (var i = 0, ii = extpaths.length; !module && i < ii; ++i) { + var filepath = extpaths[i].replace('?', path); + module = loader.packages[filepath]; + } + return module; +}; + +loader.getPackageByLineno = function(lineno) { + var packages = loader.packagesLinenoOrder; + var module; + for (var i = 0, ii = packages.length; i < ii; ++i) { + var next = packages[i]; + if (next.lineno > lineno) { + break; + } + module = next; + } + return module; +}; + +return loader; + +})(); diff --git a/src/js/main.js b/src/js/main.js index e9245770..166bc892 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,9 +1,42 @@ -/* global SimplyPebble */ +/* + * This is the main PebbleJS file. You do not need to modify this file unless + * you want to change the way PebbleJS starts, the script it runs or the libraries + * it loads. + * + * By default, this will run app.js + */ -(function() { +var safe = require('safe'); +var util2 = require('util2'); Pebble.addEventListener('ready', function(e) { - SimplyPebble.init(); -}); + // Initialize the Pebble protocol + require('ui/simply-pebble.js').init(); + + // Backwards compatibility: place moment.js in global scope + // This will be removed in a future update + var moment = require('vendor/moment'); + + var momentPasser = function(methodName) { + return function() { + if (safe.warnGlobalMoment !== false) { + safe.warn("You've accessed moment globally. Pleae use `var moment = require('moment')` instead.\n\t" + + 'moment will not be automatically loaded as a global in future versions.', 1); + safe.warnGlobalMoment = false; + } + return (methodName ? moment[methodName] : moment).apply(this, arguments); + }; + }; -})(); + var globalMoment = momentPasser(); + util2.copy(moment.prototype, globalMoment.prototype); + for (var k in moment) { + var v = moment[k]; + globalMoment[k] = typeof v === 'function' ? momentPasser(k) : v; + } + + window.moment = globalMoment; + + // Load local file + require('./app'); +}); diff --git a/src/js/moment.min.js b/src/js/moment.min.js deleted file mode 100644 index f2534886..00000000 --- a/src/js/moment.min.js +++ /dev/null @@ -1,6 +0,0 @@ -//! moment.js -//! version : 2.5.0 -//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -//! license : MIT -//! momentjs.com -(function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){u(a),g(this,a)}function f(a){var b=o(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b,c){for(var d=Math.abs(a)+"",e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function n(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Qb[a]||Rb[b]||b}return a}function o(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=n(c),b&&(d[b]=a[c]));return d}function p(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}cb[b]=function(e,f){var g,h,i=cb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=cb().utc().set(d,a);return i.call(cb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function r(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function s(a){return t(a)?366:365}function t(a){return a%4===0&&a%100!==0||a%400===0}function u(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[ib]<0||a._a[ib]>11?ib:a._a[jb]<1||a._a[jb]>r(a._a[hb],a._a[ib])?jb:a._a[kb]<0||a._a[kb]>23?kb:a._a[lb]<0||a._a[lb]>59?lb:a._a[mb]<0||a._a[mb]>59?mb:a._a[nb]<0||a._a[nb]>999?nb:-1,a._pf._overflowDayOfYear&&(hb>b||b>jb)&&(b=jb),a._pf.overflow=b)}function v(a){a._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function w(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function x(a){return a?a.toLowerCase().replace("_","-"):a}function y(a,b){return b._isUTC?cb(a).zone(b._offset||0):cb(a).local()}function z(a,b){return b.abbr=a,ob[a]||(ob[a]=new d),ob[a].set(b),ob[a]}function A(a){delete ob[a]}function B(a){var b,c,d,e,f=0,g=function(a){if(!ob[a]&&pb)try{require("./lang/"+a)}catch(b){}return ob[a]};if(!a)return cb.fn._lang;if(!k(a)){if(c=g(a))return c;a=[a]}for(;f0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&m(e,d,!0)>=b-1)break;b--}f++}return cb.fn._lang}function C(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function D(a){var b,c,d=a.match(tb);for(b=0,c=d.length;c>b;b++)d[b]=Vb[d[b]]?Vb[d[b]]:C(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function E(a,b){return a.isValid()?(b=F(b,a.lang()),Sb[b]||(Sb[b]=D(b)),Sb[b](a)):a.lang().invalidDate()}function F(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(ub.lastIndex=0;d>=0&&ub.test(a);)a=a.replace(ub,c),ub.lastIndex=0,d-=1;return a}function G(a,b){var c,d=b._strict;switch(a){case"DDDD":return Gb;case"YYYY":case"GGGG":case"gggg":return d?Hb:xb;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?Ib:yb;case"S":if(d)return Eb;case"SS":if(d)return Fb;case"SSS":case"DDD":return d?Gb:wb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ab;case"a":case"A":return B(b._l)._meridiemParse;case"X":return Db;case"Z":case"ZZ":return Bb;case"T":return Cb;case"SSSS":return zb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Fb:vb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return d?Eb:vb;default:return c=new RegExp(O(N(a.replace("\\","")),"i"))}}function H(a){a=a||"";var b=a.match(Bb)||[],c=b[b.length-1]||[],d=(c+"").match(Nb)||["-",0,0],e=+(60*d[1])+q(d[2]);return"+"===d[0]?-e:e}function I(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[ib]=q(b)-1);break;case"MMM":case"MMMM":d=B(c._l).monthsParse(b),null!=d?e[ib]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[jb]=q(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=q(b));break;case"YY":e[hb]=q(b)+(q(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":e[hb]=q(b);break;case"a":case"A":c._isPm=B(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[kb]=q(b);break;case"m":case"mm":e[lb]=q(b);break;case"s":case"ss":e[mb]=q(b);break;case"S":case"SS":case"SSS":case"SSSS":e[nb]=q(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=H(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function J(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=L(a),a._w&&null==a._a[jb]&&null==a._a[ib]&&(f=function(b){var c=parseInt(b,10);return b?b.length<3?c>68?1900+c:2e3+c:c:null==a._a[hb]?cb().weekYear():a._a[hb]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=Y(f(g.GG),g.W||1,g.E,4,1):(i=B(a._l),j=null!=g.d?U(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&js(e)&&(a._pf._overflowDayOfYear=!0),c=T(e,0,a._dayOfYear),a._a[ib]=c.getUTCMonth(),a._a[jb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[kb]+=q((a._tzm||0)/60),l[lb]+=q((a._tzm||0)%60),a._d=(a._useUTC?T:S).apply(null,l)}}function K(a){var b;a._d||(b=o(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],J(a))}function L(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function M(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=B(a._l),h=""+a._i,i=h.length,j=0;for(d=F(a._f,g).match(tb)||[],b=0;b0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Vb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),I(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[kb]<12&&(a._a[kb]+=12),a._isPm===!1&&12===a._a[kb]&&(a._a[kb]=0),J(a),u(a)}function N(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function O(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function P(a){var b,c,d,e,f;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function Q(a){var b,c=a._i,d=Jb.exec(c);if(d){for(a._pf.iso=!0,b=4;b>0;b--)if(d[b]){a._f=Lb[b-1]+(d[6]||" ");break}for(b=0;4>b;b++)if(Mb[b][1].exec(c)){a._f+=Mb[b][0];break}c.match(Bb)&&(a._f+="Z"),M(a)}else a._d=new Date(c)}function R(b){var c=b._i,d=qb.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?Q(b):k(c)?(b._a=c.slice(0),J(b)):l(c)?b._d=new Date(+c):"object"==typeof c?K(b):b._d=new Date(c)}function S(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function T(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function U(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function V(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function W(a,b,c){var d=gb(Math.abs(a)/1e3),e=gb(d/60),f=gb(e/60),g=gb(f/24),h=gb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",gb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,V.apply({},i)}function X(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=cb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function Y(a,b,c,d,e){var f,g,h=new Date(i(a,6,!0)+"-01-01").getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:s(a-1)+g}}function Z(a){var b=a._i,c=a._f;return"undefined"==typeof a._pf&&v(a),null===b?cb.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=B().preparse(b)),cb.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?P(a):M(a):R(a),new e(a))}function $(a,b){cb.fn[a]=cb.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),cb.updateOffset(this),this):this._d["get"+c+b]()}}function _(a){cb.duration.fn[a]=function(){return this._data[a]}}function ab(a,b){cb.duration.fn["as"+a]=function(){return+this/b}}function bb(a){var b=!1,c=cb;"undefined"==typeof ender&&(a?(fb.moment=function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)},g(fb.moment,c)):fb.moment=cb)}for(var cb,db,eb="2.5.0",fb=this,gb=Math.round,hb=0,ib=1,jb=2,kb=3,lb=4,mb=5,nb=6,ob={},pb="undefined"!=typeof module&&module.exports&&"undefined"!=typeof require,qb=/^\/?Date\((\-?\d+)/i,rb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,sb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,tb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,ub=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,vb=/\d\d?/,wb=/\d{1,3}/,xb=/\d{1,4}/,yb=/[+\-]?\d{1,6}/,zb=/\d+/,Ab=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Bb=/Z|[\+\-]\d\d:?\d\d/gi,Cb=/T/i,Db=/[\+\-]?\d+(\.\d{1,3})?/,Eb=/\d/,Fb=/\d\d/,Gb=/\d{3}/,Hb=/\d{4}/,Ib=/[+\-]?\d{6}/,Jb=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Kb="YYYY-MM-DDTHH:mm:ssZ",Lb=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Mb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Nb=/([\+\-]|\d\d)/gi,Ob="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Pb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Qb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Rb={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Sb={},Tb="DDD w W M D d".split(" "),Ub="M D H h m s w W".split(" "),Vb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},YYYYYY:function(){var a=this.year(),b=a>=0?"+":"-";return b+i(Math.abs(a),6)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return q(this.milliseconds()/100)},SS:function(){return i(q(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},SSSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+":"+i(q(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+i(q(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},Wb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Tb.length;)db=Tb.pop(),Vb[db+"o"]=c(Vb[db],db);for(;Ub.length;)db=Ub.pop(),Vb[db+db]=b(Vb[db],2);for(Vb.DDDD=b(Vb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=cb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=cb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return X(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),cb=function(b,c,d,e){return"boolean"==typeof d&&(e=d,d=a),Z({_i:b,_f:c,_l:d,_strict:e,_isUTC:!1})},cb.utc=function(b,c,d,e){var f;return"boolean"==typeof d&&(e=d,d=a),f=Z({_useUTC:!0,_isUTC:!0,_l:d,_i:b,_f:c,_strict:e}).utc()},cb.unix=function(a){return cb(1e3*a)},cb.duration=function(a,b){var c,d,e,g=a,h=null;return cb.isDuration(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=rb.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:q(h[jb])*c,h:q(h[kb])*c,m:q(h[lb])*c,s:q(h[mb])*c,ms:q(h[nb])*c}):(h=sb.exec(a))&&(c="-"===h[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},g={y:e(h[2]),M:e(h[3]),d:e(h[4]),h:e(h[5]),m:e(h[6]),s:e(h[7]),w:e(h[8])}),d=new f(g),cb.isDuration(a)&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},cb.version=eb,cb.defaultFormat=Kb,cb.updateOffset=function(){},cb.lang=function(a,b){var c;return a?(b?z(x(a),b):null===b?(A(a),a="en"):ob[a]||B(a),c=cb.duration.fn._lang=cb.fn._lang=B(a),c._abbr):cb.fn._lang._abbr},cb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),B(a)},cb.isMoment=function(a){return a instanceof e},cb.isDuration=function(a){return a instanceof f},db=Wb.length-1;db>=0;--db)p(Wb[db]);for(cb.normalizeUnits=function(a){return n(a)},cb.invalid=function(a){var b=cb.utc(0/0);return null!=a?g(b._pf,a):b._pf.userInvalidated=!0,b},cb.parseZone=function(a){return cb(a).parseZone()},g(cb.fn=e.prototype,{clone:function(){return cb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=cb(this).utc();return 00:!1},parsingFlags:function(){return g({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=E(this,a||cb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?cb.duration(+b,a):cb.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?cb.duration(+b,a):cb.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=y(a,this),g=6e4*(this.zone()-f.zone());return b=n(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-cb(this).startOf("month")-(f-cb(f).startOf("month")))/d,e-=6e4*(this.zone()-cb(this).startOf("month").zone()-(f.zone()-cb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return cb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(cb(),a)},calendar:function(){var a=y(cb(),this).startOf("day"),b=this.diff(a,"days",!0),c=-6>b?"sameElse":-1>b?"lastWeek":0>b?"lastDay":1>b?"sameDay":2>b?"nextDay":7>b?"nextWeek":"sameElse";return this.format(this.lang().calendar(c,this))},isLeapYear:function(){return t(this.year())},isDST:function(){return this.zone()+cb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+cb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+y(a,this).startOf(b)},min:function(a){return a=cb.apply(null,arguments),this>a?this:a},max:function(a){return a=cb.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=H(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,cb.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?cb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return r(this.year(),this.month())},dayOfYear:function(a){var b=gb((cb(this).startOf("day")-cb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(a){var b=X(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=X(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=X(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=n(a),this[a]()},set:function(a,b){return a=n(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=B(b),this)}}),db=0;db 0) { + state.ignoreCancelled--; + return; + } + var listener = util2.last(state.listeners); + var options = {}; + var format; + if (e.response) { + options = parseJson(decodeURIComponent(e.response)); + if (typeof options === 'object' && options !== null) { + format = 'json'; + } + if (!format && e.response.match(/(&|=)/)) { + options = ajax.deformify(e.response); + if (util2.count(options) > 0) { + format = 'form'; + } + } + } + if (listener) { + e = { + originalEvent: e, + response: e.response, + originalOptions: state.options, + options: options, + url: listener.params.url, + failed: !format, + format: format, + }; + if (format && listener.params.autoSave !== false) { + e.originalOptions = util2.copy(state.options); + util2.copy(options, state.options); + Settings._saveOptions(); + } + if (listener.close) { + return listener.close(e); + } + } +}; diff --git a/src/js/simply.js b/src/js/simply.js deleted file mode 100644 index 95b38d3f..00000000 --- a/src/js/simply.js +++ /dev/null @@ -1,706 +0,0 @@ -/** - * Simply.js - * @namespace simply - */ -var simply = (function() { - -var noop = typeof util2 !== 'undefined' ? util2.noop : function() {}; - -var simply = {}; - -var buttons = [ - 'back', - 'up', - 'select', - 'down', -]; - -var eventTypes = [ - 'singleClick', - 'longClick', - 'accelTap', - 'accelData', -]; - -simply.state = {}; -simply.packages = {}; -simply.listeners = {}; - -simply.settingsUrl = 'http://meiguro.com/simplyjs/settings.html'; - -simply.init = function() { - if (!simply.inited) { - simply.inited = true; - ajax.onHandler = function(type, handler) { - return simply.wrapHandler(handler, 2); - }; - } - - simply.loadMainScript(); -}; - -simply.wrapHandler = function(handler) { - return simply.impl.wrapHandler.apply(this, arguments); -}; - -simply.begin = function() { -}; - -simply.end = function() { - simply.state.run = false; -}; - -simply.reset = function() { - simply.off(); - - simply.packages = {}; - - simply.state = {}; - simply.state.run = true; - simply.state.numPackages = 0; - - simply.state.button = { - config: {}, - configMode: 'auto', - }; - for (var i = 0, ii = buttons.length; i < ii; i++) { - var button = buttons[i]; - if (button !== 'back') { - simply.state.button.config[buttons[i]] = true; - } - } - - simply.accelInit(); -}; - -/** - * Simply.js event handler callback. - * @callback simply.eventHandler - * @param {simply.event} event - The event object with event specific information. - */ - -var isBackEvent = function(type, subtype) { - return ((type === 'singleClick' || type === 'longClick') && subtype === 'back'); -}; - -simply.onAddHandler = function(type, subtype) { - if (isBackEvent(type, subtype)) { - simply.buttonAutoConfig(); - } else if (type === 'accelData') { - simply.accelAutoSubscribe(); - } -}; - -simply.onRemoveHandler = function(type, subtype) { - if (!type || isBackEvent(type, subtype)) { - simply.buttonAutoConfig(); - } - if (!type || type === 'accelData') { - simply.accelAutoSubscribe(); - } -}; - -simply.countHandlers = function(type, subtype) { - if (!subtype) { - subtype = 'all'; - } - var typeMap = simply.listeners; - var subtypeMap = typeMap[type]; - if (!subtypeMap) { - return 0; - } - var handlers = subtypeMap[subtype]; - return handlers ? handlers.length : 0; -}; - -var checkEventType = function(type) { - if (eventTypes.indexOf(type) === -1) { - throw Error('Invalid event type: ' + type); - } -}; - -/** - * Subscribe to Pebble events. - * See {@link simply.event} for the possible event types to subscribe to. - * Subscribing to a Pebble event requires a handler. An event object will be passed to your handler with event information. - * Events can have a subtype which can be used to filter events before the handler is called. - * @memberOf simply - * @param {string} type - The event type. - * @param {string} [subtype] - The event subtype. - * @param {simply.eventHandler} handler - The event handler. The handler will be called with corresponding event. - * @see simply.event - */ -simply.on = function(type, subtype, handler) { - if (type) { - checkEventType(type); - } - if (!handler) { - handler = subtype; - subtype = 'all'; - } - simply.rawOn(type, subtype, handler); - simply.onAddHandler(type, subtype); -}; - -simply.rawOn = function(type, subtype, handler) { - var typeMap = simply.listeners; - var subtypeMap = (typeMap[type] || ( typeMap[type] = {} )); - (subtypeMap[subtype] || ( subtypeMap[subtype] = [] )).push({ - id: handler, - handler: simply.wrapHandler(handler), - }); -}; - -/** - * Unsubscribe from Pebble events. - * When called without a handler, all handlers of the type and subtype are unsubscribe. - * When called with no parameters, all handlers are unsubscribed. - * @memberOf simply - * @param {string} type - The event type. - * @param {string} [subtype] - The event subtype. - * @param {function} [handler] - The event handler to unsubscribe. - * @see simply.on - */ -simply.off = function(type, subtype, handler) { - if (type) { - checkEventType(type); - } - if (!handler) { - handler = subtype; - subtype = 'all'; - } - simply.rawOff(type, subtype, handler); - simply.onRemoveHandler(type, subtype); -}; - -simply.rawOff = function(type, subtype, handler) { - if (!type) { - simply.listeners = {}; - return; - } - var typeMap = simply.listeners; - if (!handler && subtype === 'all') { - delete typeMap[type]; - return; - } - var subtypeMap = typeMap[type]; - if (!subtypeMap) { - return; - } - if (!handler) { - delete subtypeMap[subtype]; - return; - } - var handlers = subtypeMap[subtype]; - if (!handlers) { - return; - } - var index = -1; - for (var i = 0, ii = handlers.length; i < ii; ++i) { - if (handlers[i].id === handler) { - index = i; - break; - } - } - if (index === -1) { - return; - } - handlers.splice(index, 1); -}; - -simply.emitToHandlers = function(type, handlers, e) { - if (!handlers) { - return; - } - for (var i = 0, ii = handlers.length; i < ii; ++i) { - var handler = handlers[i].handler; - if (handler(e, type, i) === false) { - return true; - } - } - return false; -}; - -simply.emit = function(type, subtype, e) { - if (!simply.state.run) { - return; - } - if (!e) { - e = subtype; - subtype = null; - } - var typeMap = simply.listeners; - var subtypeMap = typeMap[type]; - if (!subtypeMap) { - return; - } - if (simply.emitToHandlers(type, subtypeMap[subtype], e) === true) { - return true; - } - if (simply.emitToHandlers(type, subtypeMap.all, e) === true) { - return true; - } - return false; -}; - -var pathToName = function(path) { - var name = path; - if (typeof name === 'string') { - name = name.replace(simply.basepath(), ''); - } - return name || simply.basename(); -}; - -simply.getPackageByPath = function(path) { - return simply.packages[pathToName(path)]; -}; - -simply.makePackage = function(path) { - var name = pathToName(path); - var saveName = 'script:' + path; - var pkg = simply.packages[name]; - - if (!pkg) { - pkg = simply.packages[name] = { - name: name, - saveName: saveName, - filename: path - }; - } - - return pkg; -}; - -simply.defun = function(fn, fargs, fbody) { - if (!fbody) { - fbody = fargs; - fargs = []; - } - return new Function('return function ' + fn + '(' + fargs.join(', ') + ') {' + fbody + '}')(); -}; - -simply.fexecPackage = function(script, pkg) { - // console shim for Android - var console2 = {}; - for (var k in console) { - console2[k] = console[k]; - } - - console2.log = function() { - var args = []; - for (var i = 0, ii = arguments.length; i < ii; ++i) { - args[i] = arguments[i].toString(); - } - var msg = pkg.name + ': ' + args.join(' '); - var width = 45; - var prefix = (new Array(width + 1)).join('\b'); // erase Simply.js source line - var suffix = msg.length < width ? (new Array(width - msg.length + 1)).join(' ') : 0; - console.log(prefix + msg + suffix); - }; - - // loader - return function() { - if (!simply.state.run) { - return; - } - var exports = pkg.exports; - var result = simply.defun(pkg.execName, - ['module', 'require', 'console', 'Pebble', 'simply'], script) - (pkg, simply.require, console2, Pebble, simply); - - // backwards compatibility for return-style modules - if (pkg.exports === exports && result) { - pkg.exports = result; - } - - return pkg.exports; - }; -}; - -simply.loadScript = function(scriptUrl, async) { - console.log('loading: ' + scriptUrl); - - var pkg = simply.makePackage(scriptUrl); - pkg.exports = {}; - - var loader = noop; - var useScript = function(script) { - loader = simply.fexecPackage(script, pkg); - }; - - ajax({ url: scriptUrl, cache: false, async: async }, function(data) { - if (data && data.length) { - localStorage.setItem(pkg.saveName, data); - useScript(data); - } - }, function(data, status) { - data = localStorage.getItem(pkg.saveName); - if (data && data.length) { - console.log(status + ': failed, loading saved script instead'); - useScript(data); - } - }); - - return simply.impl.loadPackage.call(this, pkg, loader); -}; - -simply.loadMainScriptUrl = function(scriptUrl) { - if (typeof scriptUrl === 'string' && scriptUrl.length && !scriptUrl.match(/^(\w+:)?\/\//)) { - scriptUrl = 'http://' + scriptUrl; - } - - if (scriptUrl) { - localStorage.setItem('mainJsUrl', scriptUrl); - } else { - scriptUrl = localStorage.getItem('mainJsUrl'); - } - - return scriptUrl; -}; - -simply.loadMainScript = function(scriptUrl) { - simply.reset(); - scriptUrl = simply.loadMainScriptUrl(scriptUrl); - if (!scriptUrl) { - return; - } - try { - simply.loadScript(scriptUrl, false); - } catch (e) { - simply.text({ - title: 'Failed to load', - body: scriptUrl, - }, true); - return; - } - simply.begin(); -}; - -simply.basepath = function(path) { - path = path || localStorage.getItem('mainJsUrl'); - return path.replace(/[^\/]*$/, ''); -}; - -simply.basename = function(path) { - path = path || localStorage.getItem('mainJsUrl'); - return path.match(/[^\/]*$/)[0]; -}; - -/** - * Loads external dependencies, allowing you to write a multi-file project. - * Package loading loosely follows the CommonJS format. - * Exporting is possible by modifying or setting module.exports within the required file. - * The module path is also available as module.path. - * This currently only supports a relative path to another JavaScript file. - * @global - * @param {string} path - The path to the dependency. - */ -simply.require = function(path) { - if (!path.match(/\.js$/)) { - path += '.js'; - } - var package = simply.packages[path]; - if (package) { - return package.value; - } - var basepath = simply.basepath(); - return simply.loadScript(basepath + path, false); -}; - -/** - * The button configuration parameter for {@link simply.buttonConfig}. - * The button configuration allows you to enable to disable buttons without having to register or unregister handlers if that is your preferred style. - * You may also enable the back button manually as an alternative to registering a click handler with 'back' as its subtype using {@link simply.on}. - * @typedef {object} simply.buttonConf - * @property {boolean} [back] - Whether to enable the back button. Initializes as false. Simply.js can also automatically register this for you based on the amount of click handlers with subtype 'back'. - * @property {boolean} [up] - Whether to enable the up button. Initializes as true. Note that this is disabled when using {@link simply.scrollable}. - * @property {boolean} [select] - Whether to enable the select button. Initializes as true. - * @property {boolean} [down] - Whether to enable the down button. Initializes as true. Note that this is disabled when using {@link simply.scrollable}. - */ - -/** - * Changes the button configuration. - * See {@link simply.buttonConfig} - * @memberOf simply - * @param {simply.buttonConfig} buttonConf - An object defining the button configuration. - */ -simply.buttonConfig = function(buttonConf, auto) { - var buttonState = simply.state.button; - if (typeof buttonConf === 'undefined') { - var config = {}; - for (var i = 0, ii = buttons.length; i < ii; ++i) { - var k = buttons[i]; - config[k] = buttonConf.config[k]; - } - return config; - } - for (var k in buttonConf) { - if (buttons.indexOf(k) !== -1) { - if (k === 'back') { - buttonState.configMode = buttonConf.back && !auto ? 'manual' : 'auto'; - } - buttonState.config[k] = buttonConf[k]; - } - } - if (simply.impl.buttonConfig) { - return simply.impl.buttonConfig(buttonState.config); - } -}; - -simply.buttonAutoConfig = function() { - var buttonState = simply.state.button; - if (!buttonState || buttonState.configMode !== 'auto') { - return; - } - var singleBackCount = simply.countHandlers('singleClick', 'back'); - var longBackCount = simply.countHandlers('longClick', 'back'); - var useBack = singleBackCount + longBackCount > 0; - if (useBack !== buttonState.config.back) { - buttonState.config.back = useBack; - return simply.buttonConfig(buttonState.config, true); - } -}; - -/** - * The text definition parameter for {@link simply.text}. - * @typedef {object} simply.textDef - * @property {string} [title] - A new title for the first and largest text field. - * @property {string} [subtitle] - A new subtitle for the second large text field. - * @property {string} [body] - A new body for the last text field meant to display large bodies of text. - */ - -/** - * Sets a group of text fields at once. - * For example, passing a text definition { title: 'A', subtitle: 'B', body: 'C' } - * will set the title, subtitle, and body simultaneously. Not all fields need to be specified. - * When setting a single field, consider using the specific text setters simply.title, simply.subtitle, simply.body. - * @memberOf simply - * @param {simply.textDef} textDef - An object defining new text values. - * @param {boolean} [clear] - If true, all other text fields will be cleared. - */ -simply.text = function(textDef, clear) { - return simply.impl.text.apply(this, arguments); -}; - -simply.setText = simply.text; - -/** - * Sets the title field. The title field is the first and largest text field available. - * @memberOf simply - * @param {string} text - The desired text to display. - * @param {boolean} [clear] - If true, all other text fields will be cleared. - */ -simply.title = function(text, clear) { - return simply.impl.textfield('title', text, clear); -}; - -/** - * Sets the subtitle field. The subtitle field is the second large text field available. - * @memberOf simply - * @param {string} text - The desired text to display. - * @param {boolean} [clear] - If true, all other text fields will be cleared. - */ -simply.subtitle = function(text, clear) { - return simply.impl.textfield('subtitle', text, clear); -}; - -/** - * Sets the body field. The body field is the last text field available meant to display large bodies of text. - * This can be used to display entire text interfaces. - * You may even clear the title and subtitle fields in order to display more in the body field. - * @memberOf simply - * @param {string} text - The desired text to display. - * @param {boolean} [clear] - If true, all other text fields will be cleared. - */ -simply.body = function(text, clear) { - return simply.impl.textfield('body', text, clear); -}; - -/** - * Vibrates the Pebble. - * There are three support vibe types: short, long, and double. - * @memberOf simply - * @param {string} [type=short] - The vibe type. - */ -simply.vibe = function() { - return simply.impl.vibe.apply(this, arguments); -}; - -/** - * Enable scrolling in the Pebble UI. - * When scrolling is enabled, up and down button presses are no longer forwarded to JavaScript handlers. - * Single select, long select, and accel tap events are still available to you however. - * @memberOf simply - * @param {boolean} scrollable - Whether to enable a scrollable view. - */ - -simply.scrollable = function(scrollable) { - return simply.impl.scrollable.apply(this, arguments); -}; - -/** - * Enable fullscreen in the Pebble UI. - * Fullscreen removes the Pebble status bar, giving slightly more vertical display height. - * @memberOf simply - * @param {boolean} fullscreen - Whether to enable fullscreen mode. - */ - -simply.fullscreen = function(fullscreen) { - return simply.impl.fullscreen.apply(this, arguments); -}; - -/** - * Set the Pebble UI style. - * The available styles are 'small', 'large', and 'mono'. Small and large correspond to the system notification styles. - * Mono sets a monospace font for the body textfield, enabling more complex text UIs or ASCII art. - * @memberOf simply - * @param {string} type - The type of style to set: 'small', 'large', or 'mono'. - */ - -simply.style = function(type) { - return simply.impl.style.apply(this, arguments); -}; - -simply.accelInit = function() { - simply.state.accel = { - rate: 100, - samples: 25, - subscribe: false, - subscribeMode: 'auto', - listeners: [], - }; -}; - -simply.accelAutoSubscribe = function() { - var accelState = simply.state.accel; - if (!accelState || accelState.subscribeMode !== 'auto') { - return; - } - var subscribe = simply.countHandlers('accelData') > 0; - if (subscribe !== simply.state.accel.subscribe) { - return simply.accelConfig(subscribe, true); - } -}; - -/** - * The accelerometer configuration parameter for {@link simply.accelConfig}. - * The accelerometer data stream is useful for applications such as gesture recognition when accelTap is too limited. - * However, keep in mind that smaller batch sample sizes and faster rates will drastically impact the battery life of both the Pebble and phone because of the taxing use of the processors and Bluetooth modules. - * @typedef {object} simply.accelConf - * @property {number} [rate] - The rate accelerometer data points are generated in hertz. Valid values are 10, 25, 50, and 100. Initializes as 100. - * @property {number} [samples] - The number of accelerometer data points to accumulate in a batch before calling the event handler. Valid values are 1 to 25 inclusive. Initializes as 25. - * @property {boolean} [subscribe] - Whether to subscribe to accelerometer data events. {@link simply.accelPeek} cannot be used when subscribed. Simply.js will automatically (un)subscribe for you depending on the amount of accelData handlers registered. - */ - -/** - * Changes the accelerometer configuration. - * See {@link simply.accelConfig} - * @memberOf simply - * @param {simply.accelConfig} accelConf - An object defining the accelerometer configuration. - */ -simply.accelConfig = function(opt, auto) { - var accelState = simply.state.accel; - if (typeof opt === 'undefined') { - return { - rate: accelState.rate, - samples: accelState.samples, - subscribe: accelState.subscribe, - }; - } else if (typeof opt === 'boolean') { - opt = { subscribe: opt }; - } - for (var k in opt) { - if (k === 'subscribe') { - accelState.subscribeMode = opt[k] && !auto ? 'manual' : 'auto'; - } - accelState[k] = opt[k]; - } - return simply.impl.accelConfig.apply(this, arguments); -}; - -/** - * Peeks at the current accelerometer values. - * @memberOf simply - * @param {simply.eventHandler} callback - A callback function that will be provided the accel data point as an event. - */ -simply.accelPeek = function(callback) { - if (simply.state.accel.subscribe) { - throw Error('Cannot use accelPeek when listening to accelData events'); - } - return simply.impl.accelPeek.apply(this, arguments); -}; - -/** - * Simply.js event. See all the possible event types. Subscribe to events using {@link simply.on}. - * @typedef simply.event - * @see simply.clickEvent - * @see simply.accelTapEvent - * @see simply.accelDataEvent - */ - -/** - * Simply.js button click event. This can either be a single click or long click. - * Use the event type 'singleClick' or 'longClick' to subscribe to these events. - * @typedef simply.clickEvent - * @property {string} button - The button that was pressed: 'back', 'up', 'select', or 'down'. This is also the event subtype. - */ - -simply.emitClick = function(type, button) { - simply.emit(type, button, { - button: button, - }); -}; - -/** - * Simply.js accel tap event. - * Use the event type 'accelTap' to subscribe to these events. - * @typedef simply.accelTapEvent - * @property {string} axis - The axis the tap event occurred on: 'x', 'y', or 'z'. This is also the event subtype. - * @property {number} direction - The direction of the tap along the axis: 1 or -1. - */ - -simply.emitAccelTap = function(axis, direction) { - simply.emit('accelTap', axis, { - axis: axis, - direction: direction, - }); -}; - -/** - * Simply.js accel data point. - * Typical values for gravity is around -1000 on the z axis. - * @typedef simply.accelPoint - * @property {number} x - The acceleration across the x-axis. - * @property {number} y - The acceleration across the y-axis. - * @property {number} z - The acceleration across the z-axis. - * @property {boolean} vibe - Whether the watch was vibrating when measuring this point. - * @property {number} time - The amount of ticks in millisecond resolution when measuring this point. - */ - -/** - * Simply.js accel data event. - * Use the event type 'accelData' to subscribe to these events. - * @typedef simply.accelDataEvent - * @property {number} samples - The number of accelerometer samples in this event. - * @property {simply.accelPoint} accel - The first accel in the batch. This is provided for convenience. - * @property {simply.accelPoint[]} accels - The accelerometer samples in an array. - */ - -simply.emitAccelData = function(accels, callback) { - var e = { - samples: accels.length, - accel: accels[0], - accels: accels, - }; - if (callback) { - return callback(e); - } - simply.emit('accelData', e); -}; - -return simply; - -})(); - -Pebble.require = require; -var require = simply.require; diff --git a/src/js/simply.pebble.js b/src/js/simply.pebble.js deleted file mode 100644 index 923b7cb7..00000000 --- a/src/js/simply.pebble.js +++ /dev/null @@ -1,428 +0,0 @@ -/* global simply */ - -var SimplyPebble = (function() { - -var commands = [{ - name: 'setText', - params: [{ - name: 'title', - }, { - name: 'subtitle', - }, { - name: 'body', - }, { - name: 'clear', - }], -}, { - name: 'singleClick', - params: [{ - name: 'button', - }], -}, { - name: 'longClick', - params: [{ - name: 'button', - }], -}, { - name: 'accelTap', - params: [{ - name: 'axis', - }, { - name: 'direction', - }], -}, { - name: 'vibe', - params: [{ - name: 'type', - }], -}, { - name: 'setScrollable', - params: [{ - name: 'scrollable', - }], -}, { - name: 'setStyle', - params: [{ - name: 'type', - }], -}, { - name: 'setFullscreen', - params: [{ - name: 'fullscreen', - }], -}, { - name: 'accelData', - params: [{ - name: 'transactionId', - }, { - name: 'numSamples', - }, { - name: 'accelData', - }], -}, { - name: 'getAccelData', - params: [{ - name: 'transactionId', - }], -}, { - name: 'configAccelData', - params: [{ - name: 'rate', - }, { - name: 'samples', - }, { - name: 'subscribe', - }], -}, { - name: 'configButtons', - params: [{ - name: 'back', - }, { - name: 'up', - }, { - name: 'select', - }, { - name: 'down', - }], -}]; - -var commandMap = {}; - -for (var i = 0, ii = commands.length; i < ii; ++i) { - var command = commands[i]; - commandMap[command.name] = command; - command.id = i; - - var params = command.params; - if (!params) { - continue; - } - - var paramMap = command.paramMap = {}; - for (var j = 0, jj = params.length; j < jj; ++j) { - var param = params[j]; - paramMap[param.name] = param; - param.id = j + 1; - } -} - -var buttons = [ - 'back', - 'up', - 'select', - 'down', -]; - -var accelAxes = [ - 'x', - 'y', - 'z', -]; - -var vibeTypes = [ - 'short', - 'long', - 'double', -]; - -var styleTypes = [ - 'small', - 'large', - 'mono', -]; - -var SimplyPebble = {}; - -SimplyPebble.init = function() { - simply.impl = SimplyPebble; - simply.init(); -}; - -var getExecPackage = function(execName) { - var packages = simply.packages; - for (var path in packages) { - var package = packages[path]; - if (package && package.execName === execName) { - return path; - } - } -}; - -var getExceptionFile = function(e, level) { - var stack = e.stack.split('\n'); - for (var i = level || 0, ii = stack.length; i < ii; ++i) { - var line = stack[i]; - if (line.match(/^\$\d/)) { - var path = getExecPackage(line); - if (path) { - return path; - } - } - } - return stack[level]; -}; - -var getExceptionScope = function(e, level) { - var stack = e.stack.split('\n'); - for (var i = level || 0, ii = stack.length; i < ii; ++i) { - var line = stack[i]; - if (!line || line.match('native code')) { continue; } - return line.match(/^\$\d/) && getExecPackage(line) || line; - } - return stack[level]; -}; - -var setHandlerPath = function(handler, path, level) { - var level0 = 4; // caller -> wrap -> apply -> wrap -> set - handler.path = path || getExceptionScope(new Error(), (level || 0) + level0) || simply.basename(); - return handler; -}; - -SimplyPebble.papply = function(f, args, path) { - try { - return f.apply(this, args); - } catch (e) { - var scope = !path && getExceptionFile(e) || getExecPackage(path) || path; - console.log(scope + ':' + e.line + ': ' + e + '\n' + e.stack); - simply.text({ - subtitle: scope, - body: e.line + ' ' + e.message, - }, true); - simply.state.run = false; - } -}; - -SimplyPebble.protect = function(f, path) { - return function() { - return SimplyPebble.papply(f, arguments, path); - }; -}; - -SimplyPebble.wrapHandler = function(handler, level) { - if (!handler) { return; } - setHandlerPath(handler, null, level || 1); - var package = simply.packages[handler.path]; - if (package) { - return SimplyPebble.protect(package.fwrap(handler), handler.path); - } else { - return SimplyPebble.protect(handler, handler.path); - } -}; - -var toSafeName = function(name) { - name = name.replace(/[^0-9A-Za-z_$]/g, '_'); - if (name.match(/^[0-9]/)) { - name = '_' + name; - } - return name; -}; - -SimplyPebble.loadPackage = function(pkg, loader) { - pkg.execName = '$' + simply.state.numPackages++ + toSafeName(pkg.name); - pkg.fapply = simply.defun(pkg.execName, ['f', 'args'], 'return f.apply(this, args)'); - pkg.fwrap = function(f) { return function() { return pkg.fapply(f, arguments); }; }; - - return SimplyPebble.papply(loader, null, pkg.name); -}; - -SimplyPebble.onWebViewClosed = function(e) { - if (!e.response) { - return; - } - - var options = JSON.parse(decodeURIComponent(e.response)); - simply.loadMainScript(options.scriptUrl); -}; - -SimplyPebble.getOptions = function() { - return { - scriptUrl: localStorage.getItem('mainJsUrl'), - }; -}; - -SimplyPebble.onShowConfiguration = function(e) { - var options = encodeURIComponent(JSON.stringify(SimplyPebble.getOptions())); - Pebble.openURL(simply.settingsUrl + '#' + options); -}; - -function makePacket(command, def) { - var packet = {}; - packet[0] = command.id; - if (def) { - var paramMap = command.paramMap; - for (var k in def) { - var param = paramMap[k]; - if (param) { - packet[param.id] = def[k]; - } - } - } - return packet; -} - -SimplyPebble.sendPacket = function(packet) { - if (!simply.state.run) { - return; - } - var send; - send = function() { - Pebble.sendAppMessage(packet, util2.noop, send); - }; - send(); -}; - -SimplyPebble.buttonConfig = function(buttonConf) { - var command = commandMap.configButtons; - var packet = makePacket(command, buttonConf); - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.text = function(textDef, clear) { - var command = commandMap.setText; - var packetDef = {}; - for (var k in textDef) { - packetDef[k] = textDef[k].toString(); - } - var packet = makePacket(command, packetDef); - if (clear) { - packet[command.paramMap.clear.id] = 1; - } - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.textfield = function(field, text, clear) { - var command = commandMap.setText; - var packet = makePacket(command); - var param = command.paramMap[field]; - if (param) { - packet[param.id] = text.toString(); - } - if (clear) { - packet[command.paramMap.clear.id] = 1; - } - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.vibe = function(type) { - var command = commandMap.vibe; - var packet = makePacket(command); - var vibeIndex = vibeTypes.indexOf(type); - packet[command.paramMap.type.id] = vibeIndex !== -1 ? vibeIndex : 0; - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.scrollable = function(scrollable) { - if (scrollable === null) { - return simply.state.scrollable === true; - } - simply.state.scrollable = scrollable; - - var command = commandMap.setScrollable; - var packet = makePacket(command); - packet[command.paramMap.scrollable.id] = scrollable ? 1 : 0; - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.fullscreen = function(fullscreen) { - if (fullscreen === null) { - return simply.state.fullscreen === true; - } - simply.state.fullscreen = fullscreen; - - var command = commandMap.setFullscreen; - var packet = makePacket(command); - packet[command.paramMap.fullscreen.id] = fullscreen ? 1 : 0; - SimplyPebble.sendPacket(packet); -} - -SimplyPebble.style = function(type) { - var command = commandMap.setStyle; - var packet = makePacket(command); - var styleIndex = styleTypes.indexOf(type); - packet[command.paramMap.type.id] = styleIndex !== -1 ? styleIndex : 1; - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.accelConfig = function(configDef) { - var command = commandMap.configAccelData; - var packetDef = {}; - for (var k in configDef) { - packetDef[k] = configDef[k]; - } - var packet = makePacket(command, packetDef); - SimplyPebble.sendPacket(packet); -}; - -SimplyPebble.accelPeek = function(callback) { - simply.state.accel.listeners.push(callback); - var command = commandMap.getAccelData; - var packet = makePacket(command); - SimplyPebble.sendPacket(packet); -}; - -readInt = function(packet, width, pos, signed) { - var value = 0; - pos = pos || 0; - for (var i = 0; i < width; ++i) { - value += (packet[pos + i] & 0xFF) << (i * 8); - } - if (signed) { - var mask = 1 << (width * 8 - 1); - if (value & mask) { - value = value - (((mask - 1) << 1) + 1); - } - } - return value; -}; - -SimplyPebble.onAppMessage = function(e) { - var payload = e.payload; - var code = payload[0]; - var command = commands[code]; - - switch (command.name) { - case 'singleClick': - case 'longClick': - var button = buttons[payload[1]]; - simply.emitClick(command.name, button); - break; - case 'accelTap': - var axis = accelAxes[payload[1]]; - simply.emitAccelTap(axis, payload[2]); - break; - case 'accelData': - var transactionId = payload[1]; - var samples = payload[2]; - var data = payload[3]; - var accels = []; - for (var i = 0; i < samples; i++) { - var pos = i * 15; - var accel = { - x: readInt(data, 2, pos, true), - y: readInt(data, 2, pos + 2, true), - z: readInt(data, 2, pos + 4, true), - vibe: readInt(data, 1, pos + 6) ? true : false, - time: readInt(data, 8, pos + 7), - }; - accels[i] = accel; - } - if (typeof transactionId === 'undefined') { - simply.emitAccelData(accels); - } else { - var handlers = simply.state.accel.listeners; - simply.state.accel.listeners = []; - for (var i = 0, ii = handlers.length; i < ii; ++i) { - simply.emitAccelData(accels, handlers[i]); - } - } - break; - } -}; - -Pebble.addEventListener('showConfiguration', SimplyPebble.onShowConfiguration); -Pebble.addEventListener('webviewclosed', SimplyPebble.onWebViewClosed); -Pebble.addEventListener('appmessage', SimplyPebble.onAppMessage); - -return SimplyPebble; - -})(); diff --git a/src/js/simply/simply.js b/src/js/simply/simply.js new file mode 100644 index 00000000..01ac6d21 --- /dev/null +++ b/src/js/simply/simply.js @@ -0,0 +1,38 @@ +/** + * Simply.js + * + * Provides the classic "SimplyJS" API on top of PebbleJS. + * + * Not to be confused with ui/Simply which abstracts the implementation used + * to interface with the underlying hardware. + * + * @namespace simply + */ + +var WindowStack = require('ui/windowstack'); +var Card = require('ui/card'); +var Vibe = require('ui/vibe'); + +var simply = {}; + +simply.text = function(textDef) { + var wind = WindowStack.top(); + if (!wind || !(wind instanceof Card)) { + wind = new Card(textDef); + wind.show(); + } else { + wind.prop(textDef, true); + } +}; + +/** + * Vibrates the Pebble. + * There are three support vibe types: short, long, and double. + * @memberOf simply + * @param {string} [type=short] - The vibe type. + */ +simply.vibe = function(type) { + return Vibe.vibrate(type); +}; + +module.exports = simply; diff --git a/src/js/smartpackage/package-pebble.js b/src/js/smartpackage/package-pebble.js new file mode 100644 index 00000000..72dc9b7a --- /dev/null +++ b/src/js/smartpackage/package-pebble.js @@ -0,0 +1,101 @@ +var myutil = require('myutil'); +var package = require('smartpackage/package'); +var simply = require('simply/simply'); + +var packageImpl = module.exports; + +var getExecPackage = function(execname) { + var packages = package.packages; + for (var path in packages) { + var pkg = packages[path]; + if (pkg && pkg.execname === execname) { + return path; + } + } +}; + +var getExceptionFile = function(e, level) { + var stack = e.stack.split('\n'); + for (var i = level || 0, ii = stack.length; i < ii; ++i) { + var line = stack[i]; + if (line.match(/^\$\d/)) { + var path = getExecPackage(line); + if (path) { + return path; + } + } + } + return stack[level]; +}; + +var getExceptionScope = function(e, level) { + var stack = e.stack.split('\n'); + for (var i = level || 0, ii = stack.length; i < ii; ++i) { + var line = stack[i]; + if (!line || line.match('native code')) { continue; } + return line.match(/^\$\d/) && getExecPackage(line) || line; + } + return stack[level]; +}; + +var setHandlerPath = function(handler, path, level) { + var level0 = 4; // caller -> wrap -> apply -> wrap -> set + handler.path = path || + getExceptionScope(new Error(), (level || 0) + level0) || + package.basename(package.module.filename); + return handler; +}; + +var papply = packageImpl.papply = function(f, args, path) { + try { + return f.apply(this, args); + } catch (e) { + var scope = package.name(!path && getExceptionFile(e) || getExecPackage(path) || path); + console.log(scope + ':' + e.line + ': ' + e + '\n' + e.stack); + simply.text({ + subtitle: scope, + body: e.line + ' ' + e.message, + }, true); + } +}; + +var protect = packageImpl.protect = function(f, path) { + return function() { + return papply(f, arguments, path); + }; +}; + +packageImpl.wrapHandler = function(handler, level) { + if (!handler) { return; } + setHandlerPath(handler, null, level || 1); + var pkg = package.packages[handler.path]; + if (pkg) { + return protect(pkg.fwrap(handler), handler.path); + } else { + return protect(handler, handler.path); + } +}; + +var toSafeName = function(name) { + name = name.replace(/[^0-9A-Za-z_$]/g, '_'); + if (name.match(/^[0-9]/)) { + name = '_' + name; + } + return name; +}; + +var nextId = 1; + +packageImpl.loadPackage = function(pkg, loader) { + pkg.execname = toSafeName(pkg.name) + '$' + nextId++; + pkg.fapply = myutil.defun(pkg.execname, ['f', 'args'], + 'return f.apply(this, args)' + ); + pkg.fwrap = function(f) { + return function() { + return pkg.fapply(f, arguments); + }; + }; + return papply(loader, null, pkg.name); +}; + diff --git a/src/js/smartpackage/package.js b/src/js/smartpackage/package.js new file mode 100644 index 00000000..eea8dc63 --- /dev/null +++ b/src/js/smartpackage/package.js @@ -0,0 +1,174 @@ +var ajax = require('ajax'); +var util2 = require('util2'); +var myutil = require('myutil'); +var Settings = require('settings/settings'); +var simply = require('simply'); + +var package = module.exports; + +package.packages = {}; + +package.basepath = function(path) { + return path.replace(/[^\/]*$/, ''); +}; + +package.basename = function(path) { + return path.match(/[^\/]*$/)[0]; +}; + +/** + * Converts a relative path to an absolute path + * using the path of the currently running script + * (package.module) or optionaly, the given root. + * + * The first argument is optional: + * abspath(path); + * abspath(root, path); + */ +package.abspath = function(root, path) { + // Handle optional first argument + if (!path) { + path = root; + root = null; + } + // Use the package root if no root provided. + if (!root && package.module) { + root = package.basepath(package.module.filename); + } + return myutil.abspath(root, path); +}; + + +package.name = function(rootfile, path) { + if (!path) { + path = rootfile; + rootfile = null; + } + if (!rootfile && package.module) { + rootfile = package.basepath(package.module.filename); + } + var name = path; + if (typeof name === 'string') { + name = name.replace(package.basepath(rootfile), ''); + } + return name || package.basename(rootfile); +}; + +package.get = function(root, path) { + return package.packages[package.abspath(root, path)]; +}; + +package.make = function(path) { + var pkg = package.packages[path]; + if (pkg) { return; } + pkg = package.packages[path] = { + name: package.basename(path), + savename: 'script:' + path, + filename: path + }; + return pkg; +}; + +package.loader = function(pkg, script) { + // console shim + var console2 = util2.copy(console); + + console2.log = function() { + var msg = pkg.name + ': ' + myutil.slog.apply(this, arguments); + var width = 45; + var prefix = (new Array(width + 1)).join('\b'); // erase source line + var suffix = msg.length < width ? (new Array(width - msg.length + 1)).join(' ') : 0; + console.log(prefix + msg + suffix); + }; + + // loader + return function() { + var exports = pkg.exports; + var result = myutil.defun(pkg.execName, + ['module', 'require', 'console', 'Pebble'], script) + (pkg, package.require, console2, Pebble); + + // backwards compatibility for return-style modules + if (pkg.exports === exports && result) { + pkg.exports = result; + } + + return pkg.exports; + }; +}; + +package.loadScript = function(url, async) { + console.log('loading: ' + url); + + var pkg = package.make(url); + + if (!package.module) { + package.module = pkg; + } + + pkg.exports = {}; + + var loader = util2.noop; + var makeLoader = function(script) { + return package.loader(pkg, script); + }; + + ajax({ url: url, cache: false, async: async }, + function(data) { + if (data && data.length) { + localStorage.setItem(pkg.savename, data); + loader = makeLoader(data); + } + }, + function(data, status) { + data = localStorage.getItem(pkg.savename); + if (data && data.length) { + console.log(status + ': failed, loading saved script instead'); + loader = makeLoader(data); + } + } + ); + + return package.impl.loadPackage(pkg, loader); +}; + +package.loadMainScript = function(scriptUrl) { + simply.reset(); + + scriptUrl = Settings.mainScriptUrl(scriptUrl); + if (!scriptUrl) { return; } + + Settings.loadOptions(scriptUrl); + + try { + package.loadScript(scriptUrl, false); + } catch (e) { + simply.text({ + title: 'Failed to load', + body: scriptUrl, + }, true); + return; + } +}; + +/** + * Loads external dependencies, allowing you to write a multi-file project. + * Package loading loosely follows the CommonJS format. + * Exporting is possible by modifying or setting module.exports within the required file. + * The module path is also available as module.path. + * This currently only supports a relative path to another JavaScript file. + * @global + * @param {string} path - The path to the dependency. + */ + +package.require = function(path) { + if (!path.match(/\.js$/)) { + path += '.js'; + } + var pkg = package.get(path); + if (pkg) { + return pkg.exports; + } + path = package.abspath(path); + return package.loadScript(path, false); +}; diff --git a/src/js/timeline/index.js b/src/js/timeline/index.js new file mode 100644 index 00000000..89390e14 --- /dev/null +++ b/src/js/timeline/index.js @@ -0,0 +1,5 @@ +var Timeline = require('./timeline'); + +Timeline.init(); + +module.exports = Timeline; diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js new file mode 100644 index 00000000..d1940ac5 --- /dev/null +++ b/src/js/timeline/timeline.js @@ -0,0 +1,37 @@ +var Timeline = module.exports; + +Timeline.init = function() { + this._launchCallbacks = []; +}; + +Timeline.launch = function(callback) { + if (this._launchEvent) { + callback(this._launchEvent); + } else { + this._launchCallbacks.push(callback); + } +}; + +Timeline.emitAction = function(args) { + var e; + if (args !== undefined) { + e = { + action: true, + launchCode: args, + }; + } else { + e = { + action: false, + }; + } + + this._launchEvent = e; + + var callbacks = this._launchCallbacks; + this._launchCallbacks = []; + for (var i = 0, ii = callbacks.length; i < ii; ++i) { + if (callbacks[i](e) === false) { + return false; + } + } +}; diff --git a/src/js/ui/accel.js b/src/js/ui/accel.js new file mode 100644 index 00000000..ba73f4f9 --- /dev/null +++ b/src/js/ui/accel.js @@ -0,0 +1,157 @@ +var Emitter = require('emitter'); + +var Accel = new Emitter(); + +module.exports = Accel; + +var WindowStack = require('ui/windowstack'); +var Window = require('ui/window'); +var simply = require('ui/simply'); + +var state; + +Accel.init = function() { + if (state) { + Accel.off(); + } + + state = Accel.state = { + rate: 100, + samples: 25, + subscribe: false, + subscribeMode: 'auto', + listeners: [], + }; +}; + +Accel.onAddHandler = function(type, subtype) { + if (type === 'data') { + Accel.autoSubscribe(); + } +}; + +Accel.onRemoveHandler = function(type, subtype) { + if (!type || type === 'accelData') { + Accel.autoSubscribe(); + } +}; + +var accelDataListenerCount = function() { + var count = Accel.listenerCount('data'); + var wind = WindowStack.top(); + if (wind) { + count += wind.listenerCount('accelData'); + } + return count; +}; + +Accel.autoSubscribe = function() { + if (state.subscribeMode !== 'auto') { return; } + var subscribe = (accelDataListenerCount() > 0); + if (subscribe !== state.subscribe) { + return Accel.config(subscribe, true); + } +}; + +/** + * The accelerometer configuration parameter for {@link simply.accelConfig}. + * The accelerometer data stream is useful for applications such as gesture recognition when accelTap is too limited. + * However, keep in mind that smaller batch sample sizes and faster rates will drastically impact the battery life of both the Pebble and phone because of the taxing use of the processors and Bluetooth modules. + * @typedef {object} simply.accelConf + * @property {number} [rate] - The rate accelerometer data points are generated in hertz. Valid values are 10, 25, 50, and 100. Initializes as 100. + * @property {number} [samples] - The number of accelerometer data points to accumulate in a batch before calling the event handler. Valid values are 1 to 25 inclusive. Initializes as 25. + * @property {boolean} [subscribe] - Whether to subscribe to accelerometer data events. {@link simply.accelPeek} cannot be used when subscribed. Simply.js will automatically (un)subscribe for you depending on the amount of accelData handlers registered. + */ + +/** + * Changes the accelerometer configuration. + * See {@link simply.accelConfig} + * @memberOf simply + * @param {simply.accelConfig} accelConf - An object defining the accelerometer configuration. + */ +Accel.config = function(opt, auto) { + if (arguments.length === 0) { + return { + rate: state.rate, + samples: state.samples, + subscribe: state.subscribe, + }; + } else if (typeof opt === 'boolean') { + opt = { subscribe: opt }; + } + for (var k in opt) { + if (k === 'subscribe') { + state.subscribeMode = opt[k] && !auto ? 'manual' : 'auto'; + } + state[k] = opt[k]; + } + return simply.impl.accelConfig(Accel.config()); +}; + +/** + * Peeks at the current accelerometer values. + * @memberOf simply + * @param {simply.eventHandler} callback - A callback function that will be provided the accel data point as an event. + */ +Accel.peek = function(callback) { + if (state.subscribe) { + throw new Error('Cannot use accelPeek when listening to accelData events'); + } + return simply.impl.accelPeek.apply(this, arguments); +}; + +/** + * Simply.js accel tap event. + * Use the event type 'accelTap' to subscribe to these events. + * @typedef simply.accelTapEvent + * @property {string} axis - The axis the tap event occurred on: 'x', 'y', or 'z'. This is also the event subtype. + * @property {number} direction - The direction of the tap along the axis: 1 or -1. + */ + +Accel.emitAccelTap = function(axis, direction) { + var e = { + axis: axis, + direction: direction, + }; + if (Window.emit('accelTap', axis, e) === false) { + return false; + } + Accel.emit('tap', axis, e); +}; + +/** + * Simply.js accel data point. + * Typical values for gravity is around -1000 on the z axis. + * @typedef simply.accelPoint + * @property {number} x - The acceleration across the x-axis. + * @property {number} y - The acceleration across the y-axis. + * @property {number} z - The acceleration across the z-axis. + * @property {boolean} vibe - Whether the watch was vibrating when measuring this point. + * @property {number} time - The amount of ticks in millisecond resolution when measuring this point. + */ + +/** + * Simply.js accel data event. + * Use the event type 'accelData' to subscribe to these events. + * @typedef simply.accelDataEvent + * @property {number} samples - The number of accelerometer samples in this event. + * @property {simply.accelPoint} accel - The first accel in the batch. This is provided for convenience. + * @property {simply.accelPoint[]} accels - The accelerometer samples in an array. + */ + +Accel.emitAccelData = function(accels, callback) { + var e = { + samples: accels.length, + accel: accels[0], + accels: accels, + }; + if (callback) { + return callback(e); + } + if (Window.emit('accelData', null, e) === false) { + return false; + } + Accel.emit('data', e); +}; + +Accel.init(); diff --git a/src/js/ui/card.js b/src/js/ui/card.js new file mode 100644 index 00000000..c7a7fe4e --- /dev/null +++ b/src/js/ui/card.js @@ -0,0 +1,73 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Emitter = require('emitter'); +var WindowStack = require('ui/windowstack'); +var Propable = require('ui/propable'); +var Window = require('ui/window'); +var simply = require('ui/simply'); + +var textProps = [ + 'title', + 'subtitle', + 'body', +]; + +var textColorProps = [ + 'titleColor', + 'subtitleColor', + 'bodyColor', +]; + +var imageProps = [ + 'icon', + 'subicon', + 'banner', +]; + +var actionProps = [ + 'up', + 'select', + 'back', +]; + +var configProps = [ + 'style', + 'backgroundColor' +]; + +var accessorProps = textProps.concat(textColorProps).concat(imageProps).concat(configProps); +var clearableProps = textProps.concat(imageProps); + +var defaults = { + status: true, + backgroundColor: 'white', +}; + +var Card = function(cardDef) { + Window.call(this, myutil.shadow(defaults, cardDef || {})); + this._dynamic = false; +}; + +Card._codeName = 'card'; + +util2.inherit(Card, Window); + +util2.copy(Emitter.prototype, Card.prototype); + +Propable.makeAccessors(accessorProps, Card.prototype); + +Card.prototype._prop = function() { + if (this === WindowStack.top()) { + simply.impl.card.apply(this, arguments); + } +}; + +Card.prototype._clear = function(flags_) { + var flags = myutil.toFlags(flags_); + if (flags === true) { + clearableProps.forEach(Propable.unset.bind(this.state)); + } + Window.prototype._clear.call(this, flags_); +}; + +module.exports = Card; diff --git a/src/js/ui/circle.js b/src/js/ui/circle.js new file mode 100644 index 00000000..00fc2307 --- /dev/null +++ b/src/js/ui/circle.js @@ -0,0 +1,25 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Propable = require('ui/propable'); +var StageElement = require('ui/element'); + +var accessorProps = [ + 'radius', +]; + +var defaults = { + backgroundColor: 'white', + borderColor: 'clear', + borderWidth: 1, +}; + +var Circle = function(elementDef) { + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.CircleType; +}; + +util2.inherit(Circle, StageElement); + +Propable.makeAccessors(accessorProps, Circle.prototype); + +module.exports = Circle; diff --git a/src/js/ui/element.js b/src/js/ui/element.js new file mode 100644 index 00000000..c4759f29 --- /dev/null +++ b/src/js/ui/element.js @@ -0,0 +1,127 @@ +var util2 = require('util2'); +var Vector2 = require('vector2'); +var myutil = require('myutil'); +var WindowStack = require('ui/windowstack'); +var Propable = require('ui/propable'); +var simply = require('ui/simply'); + +var elementProps = [ + 'position', + 'size', + 'backgroundColor', + 'borderColor', + 'borderWidth', +]; + +var accessorProps = elementProps; + +var nextId = 1; + +var StageElement = function(elementDef) { + this.state = elementDef || {}; + this.state.id = nextId++; + if (!this.state.position) { + this.state.position = new Vector2(); + } + if (!this.state.size) { + this.state.size = new Vector2(); + } + this._queue = []; +}; + +var Types = [ + 'NoneType', + 'RectType', + 'LineType', + 'CircleType', + 'RadialType', + 'TextType', + 'ImageType', + 'InverterType', +]; + +Types.forEach(function(name, index) { + StageElement[name] = index; +}); + +util2.copy(Propable.prototype, StageElement.prototype); + +Propable.makeAccessors(accessorProps, StageElement.prototype); + +StageElement.prototype._reset = function() { + this._queue = []; +}; + +StageElement.prototype._id = function() { + return this.state.id; +}; + +StageElement.prototype._type = function() { + return this.state.type; +}; + +StageElement.prototype._prop = function(elementDef) { + if (this.parent === WindowStack.top()) { + simply.impl.stageElement(this._id(), this._type(), this.state); + } +}; + +StageElement.prototype.index = function() { + if (!this.parent) { return -1; } + return this.parent.index(this); +}; + +StageElement.prototype.remove = function(broadcast) { + if (!this.parent) { return this; } + this.parent.remove(this, broadcast); + return this; +}; + +StageElement.prototype._animate = function(animateDef, duration) { + if (this.parent === WindowStack.top()) { + simply.impl.stageAnimate(this._id(), this.state, + animateDef, duration || 400, animateDef.easing || 'easeInOut'); + } +}; + +StageElement.prototype.animate = function(field, value, duration) { + if (typeof field === 'object') { + duration = value; + } + var animateDef = myutil.toObject(field, value); + this.queue(function() { + this._animate(animateDef, duration); + util2.copy(animateDef, this.state); + }); + if (!this.state.animating) { + this.dequeue(); + } + return this; +}; + +StageElement.prototype.queue = function(callback) { + this._queue.push(callback); +}; + +StageElement.prototype.dequeue = function() { + var callback = this._queue.shift(); + if (callback) { + this.state.animating = true; + callback.call(this, this.dequeue.bind(this)); + } else { + this.state.animating = false; + } +}; + +StageElement.emitAnimateDone = function(id) { + var wind = WindowStack.top(); + if (!wind || !wind._dynamic) { return; } + wind.each(function(element) { + if (element._id() === id) { + element.dequeue(); + return false; + } + }); +}; + +module.exports = StageElement; diff --git a/src/js/ui/image.js b/src/js/ui/image.js new file mode 100644 index 00000000..ef45bbd9 --- /dev/null +++ b/src/js/ui/image.js @@ -0,0 +1,26 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Propable = require('ui/propable'); +var StageElement = require('ui/element'); + +var imageProps = [ + 'image', + 'compositing', +]; + +var defaults = { + backgroundColor: 'clear', + borderColor: 'clear', + borderWidth: 1, +}; + +var ImageElement = function(elementDef) { + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.ImageType; +}; + +util2.inherit(ImageElement, StageElement); + +Propable.makeAccessors(imageProps, ImageElement.prototype); + +module.exports = ImageElement; diff --git a/src/js/ui/imageservice.js b/src/js/ui/imageservice.js new file mode 100644 index 00000000..57bb3420 --- /dev/null +++ b/src/js/ui/imageservice.js @@ -0,0 +1,130 @@ +var imagelib = require('lib/image'); +var myutil = require('myutil'); +var Feature = require('platform/feature'); +var Resource = require('ui/resource'); +var simply = require('ui/simply'); + +var ImageService = module.exports; + +var state; + +ImageService.init = function() { + state = ImageService.state = { + cache: {}, + nextId: Resource.items.length + 1, + rootUrl: undefined, + }; +}; + +var makeImageHash = function(image) { + var url = image.url; + var hashPart = ''; + if (image.width) { + hashPart += ',width:' + image.width; + } + if (image.height) { + hashPart += ',height:' + image.height; + } + if (image.dither) { + hashPart += ',dither:' + image.dither; + } + if (hashPart) { + url += '#' + hashPart.substr(1); + } + return url; +}; + +var parseImageHash = function(hash) { + var image = {}; + hash = hash.split('#'); + image.url = hash[0]; + hash = hash[1]; + if (!hash) { return image; } + var args = hash.split(','); + for (var i = 0, ii = args.length; i < ii; ++i) { + var arg = args[i]; + if (arg.match(':')) { + arg = arg.split(':'); + var v = arg[1]; + image[arg[0]] = !isNaN(Number(v)) ? Number(v) : v; + } else { + image[arg] = true; + } + } + return image; +}; + +ImageService.load = function(opt, reset, callback) { + if (typeof opt === 'string') { + opt = parseImageHash(opt); + } + if (typeof reset === 'function') { + callback = reset; + reset = null; + } + var url = myutil.abspath(state.rootUrl, opt.url); + var hash = makeImageHash(opt); + var image = state.cache[hash]; + var fetch = false; + if (image) { + if ((opt.width && image.width !== opt.width) || + (opt.height && image.height !== opt.height) || + (opt.dither && image.dither !== opt.dither)) { + reset = true; + } + if (reset !== true && image.loaded) { + return image.id; + } + } + if (!image || reset === true) { + fetch = true; + image = { + id: state.nextId++, + url: url, + }; + } + image.width = opt.width; + image.height = opt.height; + image.dither = opt.dither; + image.loaded = true; + state.cache[hash] = image; + var onLoad = function() { + simply.impl.image(image.id, image.image); + if (callback) { + var e = { + type: 'image', + image: image.id, + url: image.url, + }; + callback(e); + } + }; + if (fetch) { + var bitdepth = Feature.color(8, 1); + imagelib.load(image, bitdepth, onLoad); + } else { + onLoad(); + } + return image.id; +}; + +ImageService.setRootUrl = function(url) { + state.rootUrl = url; +}; + +/** + * Resolve an image path to an id. If the image is defined in appinfo, the index of the resource is used, + * otherwise a new id is generated for dynamic loading. + */ +ImageService.resolve = function(opt) { + var id = Resource.getId(opt); + return typeof id !== 'undefined' ? id : ImageService.load(opt); +}; + +ImageService.markAllUnloaded = function() { + for (var k in state.cache) { + delete state.cache[k].loaded; + } +}; + +ImageService.init(); diff --git a/src/js/ui/index.js b/src/js/ui/index.js new file mode 100644 index 00000000..8ba16893 --- /dev/null +++ b/src/js/ui/index.js @@ -0,0 +1,18 @@ +var UI = {}; + +UI.Vector2 = require('vector2'); +UI.Window = require('ui/window'); +UI.Card = require('ui/card'); +UI.Menu = require('ui/menu'); +UI.Rect = require('ui/rect'); +UI.Line = require('ui/line'); +UI.Circle = require('ui/circle'); +UI.Radial = require('ui/radial'); +UI.Text = require('ui/text'); +UI.TimeText = require('ui/timetext'); +UI.Image = require('ui/image'); +UI.Inverter = require('ui/inverter'); +UI.Vibe = require('ui/vibe'); +UI.Light = require('ui/light'); + +module.exports = UI; diff --git a/src/js/ui/inverter.js b/src/js/ui/inverter.js new file mode 100644 index 00000000..6ff0b4fa --- /dev/null +++ b/src/js/ui/inverter.js @@ -0,0 +1,12 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var StageElement = require('ui/element'); + +var Inverter = function(elementDef) { + StageElement.call(this, elementDef); + this.state.type = StageElement.InverterType; +}; + +util2.inherit(Inverter, StageElement); + +module.exports = Inverter; diff --git a/src/js/ui/light.js b/src/js/ui/light.js new file mode 100644 index 00000000..804e856d --- /dev/null +++ b/src/js/ui/light.js @@ -0,0 +1,15 @@ +var simply = require('ui/simply'); + +var Light = module.exports; + +Light.on = function() { + simply.impl.light('on'); +}; + +Light.auto = function() { + simply.impl.light('auto'); +}; + +Light.trigger = function() { + simply.impl.light('trigger'); +}; diff --git a/src/js/ui/line.js b/src/js/ui/line.js new file mode 100644 index 00000000..deb8bdae --- /dev/null +++ b/src/js/ui/line.js @@ -0,0 +1,26 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Propable = require('ui/propable'); +var StageElement = require('ui/element'); + +var accessorProps = [ + 'strokeColor', + 'strokeWidth', + 'position2', +]; + +var defaults = { + strokeColor: 'white', + strokeWidth: 1, +}; + +var Line = function(elementDef) { + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.LineType; +}; + +util2.inherit(Line, StageElement); + +Propable.makeAccessors(accessorProps, Line.prototype); + +module.exports = Line; diff --git a/src/js/ui/menu.js b/src/js/ui/menu.js new file mode 100644 index 00000000..2b1d68e3 --- /dev/null +++ b/src/js/ui/menu.js @@ -0,0 +1,384 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Emitter = require('emitter'); +var Platform = require('platform'); +var WindowStack = require('ui/windowstack'); +var Window = require('ui/window'); +var simply = require('ui/simply'); + +var defaults = { + status: true, + backgroundColor: 'white', + textColor: 'black', + highlightBackgroundColor: 'black', + highlightTextColor: 'white', +}; + +var Menu = function(menuDef) { + Window.call(this, myutil.shadow(defaults, menuDef || {})); + this._dynamic = false; + this._sections = {}; + this._selection = { sectionIndex: 0, itemIndex: 0 }; + this._selections = []; +}; + +Menu._codeName = 'menu'; + +util2.inherit(Menu, Window); + +util2.copy(Emitter.prototype, Menu.prototype); + +Menu.prototype._show = function() { + Window.prototype._show.apply(this, arguments); + this._select(); +}; + +Menu.prototype._select = function() { + if (this === WindowStack.top()) { + var select = this._selection; + simply.impl.menuSelection(select.sectionIndex, select.itemIndex); + } +}; + +Menu.prototype._numPreloadItems = (Platform.version() === 'aplite' ? 5 : 50); + +Menu.prototype._prop = function(state, clear, pushing) { + if (this === WindowStack.top()) { + this._resolveMenu(clear, pushing); + this._resolveSection(this._selection); + } +}; + +Menu.prototype.action = function() { + throw new Error("Menus don't support action bars."); +}; + +Menu.prototype.buttonConfig = function() { + throw new Error("Menus don't support changing button configurations."); +}; + +Menu.prototype._buttonAutoConfig = function() {}; + +Menu.prototype._getMetaSection = function(sectionIndex) { + return (this._sections[sectionIndex] || ( this._sections[sectionIndex] = {} )); +}; + +Menu.prototype._getSections = function() { + var sections = this.state.sections; + if (sections instanceof Array) { + return sections; + } + if (typeof sections === 'number') { + sections = new Array(sections); + return (this.state.sections = sections); + } + if (typeof sections === 'function') { + this.sectionsProvider = this.state.sections; + delete this.state.sections; + } + if (this.sectionsProvider) { + sections = this.sectionsProvider.call(this); + if (sections) { + this.state.sections = sections; + return this._getSections(); + } + } + return (this.state.sections = []); +}; + +Menu.prototype._getSection = function(e, create) { + var sections = this._getSections(); + var section = sections[e.sectionIndex]; + if (section) { + return section; + } + if (this.sectionProvider) { + section = this.sectionProvider.call(this, e); + if (section) { + return (sections[e.sectionIndex] = section); + } + } + if (!create) { return; } + return (sections[e.sectionIndex] = {}); +}; + +Menu.prototype._getItems = function(e, create) { + var section = this._getSection(e, create); + if (!section) { + if (e.sectionIndex > 0) { return; } + section = this.state.sections[0] = {}; + } + if (section.items instanceof Array) { + return section.items; + } + if (typeof section.items === 'number') { + return (section.items = new Array(section.items)); + } + if (typeof section.items === 'function') { + this._sections[e.sectionIndex] = section.items; + delete section.items; + } + var itemsProvider = this._getMetaSection(e.sectionIndex).items || this.itemsProvider; + if (itemsProvider) { + var items = itemsProvider.call(this, e); + if (items) { + section.items = items; + return this._getItems(e, create); + } + } + return (section.items = []); +}; + +Menu.prototype._getItem = function(e, create) { + var items = this._getItems(e, create); + var item = items[e.itemIndex]; + if (item) { + return item; + } + var itemProvider = this._getMetaSection(e.sectionIndex).item || this.itemProvider; + if (itemProvider) { + item = itemProvider.call(this, e); + if (item) { + return (items[e.itemIndex] = item); + } + } + if (!create) { return; } + return (items[e.itemIndex] = {}); +}; + +Menu.prototype._resolveMenu = function(clear, pushing) { + var sections = this._getSections(this); + if (this === WindowStack.top()) { + simply.impl.menu(this.state, clear, pushing); + return true; + } +}; + +Menu.prototype._resolveSection = function(e, clear) { + var section = this._getSection(e); + if (!section) { return; } + section = myutil.shadow({ + textColor: this.state.textColor, + backgroundColor: this.state.backgroundColor + }, section); + section.items = this._getItems(e); + if (this === WindowStack.top()) { + simply.impl.menuSection.call(this, e.sectionIndex, section, clear); + var select = this._selection; + if (select.sectionIndex === e.sectionIndex) { + this._preloadItems(select); + } + return true; + } +}; + +Menu.prototype._resolveItem = function(e) { + var item = this._getItem(e); + if (!item) { return; } + if (this === WindowStack.top()) { + simply.impl.menuItem.call(this, e.sectionIndex, e.itemIndex, item); + return true; + } +}; + +Menu.prototype._preloadItems = function(e) { + var select = util2.copy(e); + select.itemIndex = Math.max(0, select.itemIndex - Math.floor(this._numPreloadItems / 2)); + for (var i = 0; i < this._numPreloadItems; ++i) { + this._resolveItem(select); + select.itemIndex++; + } +}; + +Menu.prototype._emitSelect = function(e) { + this._selection = e; + var item = this._getItem(e); + switch (e.type) { + case 'select': + if (item && typeof item.select === 'function') { + if (item.select(e) === false) { + return false; + } + } + break; + case 'longSelect': + if (item && typeof item.longSelect === 'function') { + if (item.longSelect(e) === false) { + return false; + } + } + break; + case 'selection': + var handlers = this._selections; + this._selections = []; + if (item && typeof item.selected === 'function') { + if (item.selected(e) === false) { + return false; + } + } + for (var i = 0, ii = handlers.length; i < ii; ++i) { + if (handlers[i](e) === false) { + break; + } + } + break; + } +}; + +Menu.prototype.sections = function(sections) { + if (typeof sections === 'function') { + delete this.state.sections; + this.sectionsProvider = sections; + this._resolveMenu(); + return this; + } + this.state.sections = sections; + this._resolveMenu(); + return this; +}; + +Menu.prototype.section = function(sectionIndex, section) { + if (typeof sectionIndex === 'object') { + sectionIndex = sectionIndex.sectionIndex || 0; + } else if (typeof sectionIndex === 'function') { + this.sectionProvider = sectionIndex; + return this; + } + var menuIndex = { sectionIndex: sectionIndex }; + if (!section) { + return this._getSection(menuIndex); + } + var sections = this._getSections(); + var prevLength = sections.length; + sections[sectionIndex] = util2.copy(section, sections[sectionIndex]); + if (sections.length !== prevLength) { + this._resolveMenu(); + } + this._resolveSection(menuIndex, typeof section.items !== 'undefined'); + return this; +}; + +Menu.prototype.items = function(sectionIndex, items) { + if (typeof sectionIndex === 'object') { + sectionIndex = sectionIndex.sectionIndex || 0; + } else if (typeof sectionIndex === 'function') { + this.itemsProvider = sectionIndex; + return this; + } + if (typeof items === 'function') { + this._getMetaSection(sectionIndex).items = items; + return this; + } + var menuIndex = { sectionIndex: sectionIndex }; + if (!items) { + return this._getItems(menuIndex); + } + var section = this._getSection(menuIndex, true); + section.items = items; + this._resolveSection(menuIndex, true); + return this; +}; + +Menu.prototype.item = function(sectionIndex, itemIndex, item) { + if (typeof sectionIndex === 'object') { + item = itemIndex || item; + itemIndex = sectionIndex.itemIndex; + sectionIndex = sectionIndex.sectionIndex || 0; + } else if (typeof sectionIndex === 'function') { + this.itemProvider = sectionIndex; + return this; + } + if (typeof itemIndex === 'function') { + item = itemIndex; + itemIndex = null; + } + if (typeof item === 'function') { + this._getMetaSection(sectionIndex).item = item; + return this; + } + var menuIndex = { sectionIndex: sectionIndex, itemIndex: itemIndex }; + if (!item) { + return this._getItem(menuIndex); + } + var items = this._getItems(menuIndex, true); + var prevLength = items.length; + items[itemIndex] = util2.copy(item, items[itemIndex]); + if (items.length !== prevLength) { + this._resolveSection(menuIndex); + } + this._resolveItem(menuIndex); + return this; +}; + +Menu.prototype.selection = function(sectionIndex, itemIndex) { + var callback; + if (typeof sectionIndex === 'function') { + callback = sectionIndex; + sectionIndex = undefined; + } + if (callback) { + this._selections.push(callback); + simply.impl.menuSelection(); + } else { + this._selection = { + sectionIndex: sectionIndex, + itemIndex: itemIndex, + }; + this._select(); + } +}; + +Menu.emit = Window.emit; + +Menu.emitSection = function(sectionIndex) { + var menu = WindowStack.top(); + if (!(menu instanceof Menu)) { return; } + var e = { + menu: menu, + sectionIndex: sectionIndex + }; + e.section = menu._getSection(e); + if (Menu.emit('section', null, e) === false) { + return false; + } + menu._resolveSection(e); +}; + +Menu.emitItem = function(sectionIndex, itemIndex) { + var menu = WindowStack.top(); + if (!(menu instanceof Menu)) { return; } + var e = { + menu: menu, + sectionIndex: sectionIndex, + itemIndex: itemIndex, + }; + e.section = menu._getSection(e); + e.item = menu._getItem(e); + if (Menu.emit('item', null, e) === false) { + return false; + } + menu._resolveItem(e); +}; + +Menu.emitSelect = function(type, sectionIndex, itemIndex) { + var menu = WindowStack.top(); + if (!(menu instanceof Menu)) { return; } + var e = { + menu: menu, + sectionIndex: sectionIndex, + itemIndex: itemIndex, + }; + e.section = menu._getSection(e); + e.item = menu._getItem(e); + switch (type) { + case 'menuSelect': type = 'select'; break; + case 'menuLongSelect': type = 'longSelect'; break; + case 'menuSelection': type = 'selection'; break; + } + if (Menu.emit(type, null, e) === false) { + return false; + } + menu._emitSelect(e); +}; + +module.exports = Menu; diff --git a/src/js/ui/propable.js b/src/js/ui/propable.js new file mode 100644 index 00000000..14af23e1 --- /dev/null +++ b/src/js/ui/propable.js @@ -0,0 +1,107 @@ +var util2 = require('util2'); +var myutil = require('myutil'); + +var Propable = function(def) { + this.state = def || {}; +}; + +Propable.unset = function(k) { + delete this[k]; +}; + +Propable.makeAccessor = function(k) { + return function(value) { + if (arguments.length === 0) { + return this.state[k]; + } + this.state[k] = value; + this._prop(myutil.toObject(k, value)); + return this; + }; +}; + +Propable.makeNestedAccessor = function(k) { + var _k = '_' + k; + return function(field, value, clear) { + var nest = this.state[k]; + if (arguments.length === 0) { + return nest; + } + if (arguments.length === 1 && typeof field === 'string') { + return typeof nest === 'object' ? nest[field] : undefined; + } + if (typeof field === 'boolean') { + value = field; + field = k; + } + if (typeof field === 'object') { + clear = value; + value = undefined; + } + if (clear) { + this._clear(k); + } + if (field !== undefined && typeof nest !== 'object') { + nest = this.state[k] = {}; + } + if (field !== undefined && typeof nest === 'object') { + util2.copy(myutil.toObject(field, value), nest); + } + if (this[_k]) { + this[_k](nest); + } + return this; + }; +}; + +Propable.makeAccessors = function(props, proto) { + proto = proto || {}; + props.forEach(function(k) { + proto[k] = Propable.makeAccessor(k); + }); + return proto; +}; + +Propable.makeNestedAccessors = function(props, proto) { + proto = proto || {}; + props.forEach(function(k) { + proto[k] = Propable.makeNestedAccessor(k); + }); + return proto; +}; + +Propable.prototype.unset = function(k) { + delete this.state[k]; +}; + +Propable.prototype._clear = function(k) { + if (k === undefined || k === true) { + this.state = {}; + } else if (k !== false) { + this.state[k] = {}; + } +}; + +Propable.prototype._prop = function(def) { +}; + +Propable.prototype.prop = function(field, value, clear) { + if (arguments.length === 0) { + return util2.copy(this.state); + } + if (arguments.length === 1 && typeof field !== 'object') { + return this.state[field]; + } + if (typeof field === 'object') { + clear = value; + } + if (clear) { + this._clear(true); + } + var def = myutil.toObject(field, value); + util2.copy(def, this.state); + this._prop(def); + return this; +}; + +module.exports = Propable; diff --git a/src/js/ui/radial.js b/src/js/ui/radial.js new file mode 100644 index 00000000..55b31d14 --- /dev/null +++ b/src/js/ui/radial.js @@ -0,0 +1,51 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var safe = require('safe'); +var Propable = require('ui/propable'); +var StageElement = require('ui/element'); + +var accessorProps = [ + 'radius', + 'angle', + 'angle2', +]; + +var defaults = { + backgroundColor: 'white', + borderColor: 'clear', + borderWidth: 1, + radius: 0, + angle: 0, + angle2: 360, +}; + +var checkProps = function(def) { + if (!def) return; + if ('angleStart' in def && safe.warnAngleStart !== false) { + safe.warn('`angleStart` has been deprecated in favor of `angle` in order to match\n\t' + + "Line's `position` and `position2`. Please use `angle` intead.", 2); + safe.warnAngleStart = false; + } + if ('angleEnd' in def && safe.warnAngleEnd !== false) { + safe.warn('`angleEnd` has been deprecated in favor of `angle2` in order to match\n\t' + + "Line's `position` and `position2`. Please use `angle2` intead.", 2); + safe.warnAngleEnd = false; + } +}; + +var Radial = function(elementDef) { + checkProps(elementDef); + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.RadialType; +}; + +util2.inherit(Radial, StageElement); + +Propable.makeAccessors(accessorProps, Radial.prototype); + +Radial.prototype._prop = function(def) { + checkProps(def); + StageElement.prototype._prop.call(this, def); +}; + +module.exports = Radial; diff --git a/src/js/ui/rect.js b/src/js/ui/rect.js new file mode 100644 index 00000000..6632dc22 --- /dev/null +++ b/src/js/ui/rect.js @@ -0,0 +1,18 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var StageElement = require('ui/element'); + +var defaults = { + backgroundColor: 'white', + borderColor: 'clear', + borderWidth: 1, +}; + +var Rect = function(elementDef) { + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.RectType; +}; + +util2.inherit(Rect, StageElement); + +module.exports = Rect; diff --git a/src/js/ui/resource.js b/src/js/ui/resource.js new file mode 100644 index 00000000..03b8eb22 --- /dev/null +++ b/src/js/ui/resource.js @@ -0,0 +1,28 @@ +var myutil = require('lib/myutil'); +var appinfo = require('appinfo'); + +var resources = (function() { + var resources = appinfo.resources; + return resources && resources.media || []; +})(); + +var Resource = {}; + +Resource.items = resources; + +Resource.getId = function(opt) { + var path = opt; + if (typeof opt === 'object') { + path = opt.url; + } + path = path.replace(/#.*/, ''); + var cname = myutil.toCConstantName(path); + for (var i = 0, ii = resources.length; i < ii; ++i) { + var res = resources[i]; + if (res.name === cname || res.file === path) { + return i + 1; + } + } +}; + +module.exports = Resource; diff --git a/src/js/ui/simply-pebble.js b/src/js/ui/simply-pebble.js new file mode 100644 index 00000000..648dc445 --- /dev/null +++ b/src/js/ui/simply-pebble.js @@ -0,0 +1,1500 @@ +var Color = require('color'); +var struct = require('struct'); +var util2 = require('util2'); +var myutil = require('myutil'); +var Platform = require('platform'); +var Wakeup = require('wakeup'); +var Timeline = require('timeline'); +var Resource = require('ui/resource'); +var Accel = require('ui/accel'); +var Voice = require('ui/voice'); +var ImageService = require('ui/imageservice'); +var WindowStack = require('ui/windowstack'); +var Window = require('ui/window'); +var Menu = require('ui/menu'); +var StageElement = require('ui/element'); +var Vector2 = require('vector2'); + +var simply = require('ui/simply'); + +/** + * This package provides the underlying implementation for the ui/* classes. + * + * This implementation uses PebbleKit JS AppMessage to send commands to a Pebble Watch. + */ + +/** + * First part of this file is defining the commands and types that we will use later. + */ + +var state; + +var BoolType = function(x) { + return x ? 1 : 0; +}; + +var StringType = function(x) { + return (x === undefined) ? '' : '' + x; +}; + +var UTF8ByteLength = function(x) { + return unescape(encodeURIComponent(x)).length; +}; + +var EnumerableType = function(x) { + if (x && x.hasOwnProperty('length')) { + return x.length; + } + return x ? Number(x) : 0; +}; + +var StringLengthType = function(x) { + return UTF8ByteLength(StringType(x)); +}; + +var TimeType = function(x) { + if (x instanceof Date) { + x = x.getTime() / 1000; + } + return (x ? Number(x) : 0) + state.timeOffset; +}; + +var ImageType = function(x) { + if (x && typeof x !== 'number') { + return ImageService.resolve(x); + } + return x ? Number(x) : 0; +}; + +var PositionType = function(x) { + this.positionX(x.x); + this.positionY(x.y); +}; + +var SizeType = function(x) { + this.sizeW(x.x); + this.sizeH(x.y); +}; + +var namedColorMap = { + 'clear': 0x00, + 'black': 0xC0, + 'oxfordBlue': 0xC1, + 'dukeBlue': 0xC2, + 'blue': 0xC3, + 'darkGreen': 0xC4, + 'midnightGreen': 0xC5, + 'cobaltBlue': 0xC6, + 'blueMoon': 0xC7, + 'islamicGreen': 0xC8, + 'jaegerGreen': 0xC9, + 'tiffanyBlue': 0xCA, + 'vividCerulean': 0xCB, + 'green': 0xCC, + 'malachite': 0xCD, + 'mediumSpringGreen': 0xCE, + 'cyan': 0xCF, + 'bulgarianRose': 0xD0, + 'imperialPurple': 0xD1, + 'indigo': 0xD2, + 'electricUltramarine': 0xD3, + 'armyGreen': 0xD4, + 'darkGray': 0xD5, + 'liberty': 0xD6, + 'veryLightBlue': 0xD7, + 'kellyGreen': 0xD8, + 'mayGreen': 0xD9, + 'cadetBlue': 0xDA, + 'pictonBlue': 0xDB, + 'brightGreen': 0xDC, + 'screaminGreen': 0xDD, + 'mediumAquamarine': 0xDE, + 'electricBlue': 0xDF, + 'darkCandyAppleRed': 0xE0, + 'jazzberryJam': 0xE1, + 'purple': 0xE2, + 'vividViolet': 0xE3, + 'windsorTan': 0xE4, + 'roseVale': 0xE5, + 'purpureus': 0xE6, + 'lavenderIndigo': 0xE7, + 'limerick': 0xE8, + 'brass': 0xE9, + 'lightGray': 0xEA, + 'babyBlueEyes': 0xEB, + 'springBud': 0xEC, + 'inchworm': 0xED, + 'mintGreen': 0xEE, + 'celeste': 0xEF, + 'red': 0xF0, + 'folly': 0xF1, + 'fashionMagenta': 0xF2, + 'magenta': 0xF3, + 'orange': 0xF4, + 'sunsetOrange': 0xF5, + 'brilliantRose': 0xF6, + 'shockingPink': 0xF7, + 'chromeYellow': 0xF8, + 'rajah': 0xF9, + 'melon': 0xFA, + 'richBrilliantLavender': 0xFB, + 'yellow': 0xFC, + 'icterine': 0xFD, + 'pastelYellow': 0xFE, + 'white': 0xFF, + 'clearWhite': 0x3F, +}; + +var namedColorMapUpper = (function() { + var map = {}; + for (var k in namedColorMap) { + map[k.toUpperCase()] = namedColorMap[k]; + } + return map; +})(); + +var ColorType = function(color) { + if (typeof color === 'string') { + var name = myutil.toCConstantName(color); + name = name.replace(/_+/g, ''); + if (name in namedColorMapUpper) { + return namedColorMapUpper[name]; + } + } + var argb = Color.toArgbUint8(color); + if ((argb & 0xc0) === 0 && argb !== 0) { + argb = argb | 0xc0; + } + return argb; +}; + +var Font = function(x) { + var id = Resource.getId(x); + if (id) { + return id; + } + x = myutil.toCConstantName(x); + if (!x.match(/^RESOURCE_ID/)) { + x = 'RESOURCE_ID_' + x; + } + x = x.replace(/_+/g, '_'); + return x; +}; + +var TextOverflowMode = function(x) { + switch (x) { + case 'wrap' : return 0; + case 'ellipsis': return 1; + case 'fill' : return 2; + } + return Number(x); +}; + +var TextAlignment = function(x) { + switch (x) { + case 'left' : return 0; + case 'center': return 1; + case 'right' : return 2; + } + return Number(x); +}; + +var TimeUnits = function(x) { + var z = 0; + x = myutil.toObject(x, true); + for (var k in x) { + switch (k) { + case 'seconds': z |= (1 << 0); break; + case 'minutes': z |= (1 << 1); break; + case 'hours' : z |= (1 << 2); break; + case 'days' : z |= (1 << 3); break; + case 'months' : z |= (1 << 4); break; + case 'years' : z |= (1 << 5); break; + } + } + return z; +}; + +var CompositingOp = function(x) { + switch (x) { + case 'assign': + case 'normal': return 0; + case 'assignInverted': + case 'invert': return 1; + case 'or' : return 2; + case 'and' : return 3; + case 'clear' : return 4; + case 'set' : return 5; + } + return Number(x); +}; + +var AnimationCurve = function(x) { + switch (x) { + case 'linear' : return 0; + case 'easeIn' : return 1; + case 'easeOut' : return 2; + case 'easeInOut': return 3; + } + return Number(x); +}; + +var MenuRowAlign = function(x) { + switch(x) { + case 'none' : return 0; + case 'center' : return 1; + case 'top' : return 2; + case 'bottom' : return 3; + } + return x ? Number(x) : 0; +}; + +var makeArrayType = function(types) { + return function(x) { + var index = types.indexOf(x); + if (index !== -1) { + return index; + } + return Number(x); + }; +}; + +var makeFlagsType = function(types) { + return function(x) { + var z = 0; + for (var k in x) { + if (!x[k]) { continue; } + var index = types.indexOf(k); + if (index !== -1) { + z |= 1 << index; + } + } + return z; + }; +}; + +var LaunchReasonTypes = [ + 'system', + 'user', + 'phone', + 'wakeup', + 'worker', + 'quickLaunch', + 'timelineAction' +]; + +var LaunchReasonType = makeArrayType(LaunchReasonTypes); + +var WindowTypes = [ + 'window', + 'menu', + 'card', +]; + +var WindowType = makeArrayType(WindowTypes); + +var ButtonTypes = [ + 'back', + 'up', + 'select', + 'down', +]; + +var ButtonType = makeArrayType(ButtonTypes); + +var ButtonFlagsType = makeFlagsType(ButtonTypes); + +var CardTextTypes = [ + 'title', + 'subtitle', + 'body', +]; + +var CardTextType = makeArrayType(CardTextTypes); + +var CardTextColorTypes = [ + 'titleColor', + 'subtitleColor', + 'bodyColor', +]; + +var CardImageTypes = [ + 'icon', + 'subicon', + 'banner', +]; + +var CardImageType = makeArrayType(CardImageTypes); + +var CardStyleTypes = [ + 'classic-small', + 'classic-large', + 'mono', + 'small', + 'large', +]; + +var CardStyleType = makeArrayType(CardStyleTypes); + +var VibeTypes = [ + 'short', + 'long', + 'double', +]; + +var VibeType = makeArrayType(VibeTypes); + +var LightTypes = [ + 'on', + 'auto', + 'trigger' +]; + +var LightType = makeArrayType(LightTypes); + +var DictationSessionStatus = [ + null, + 'transcriptionRejected', + 'transcriptionRejectedWithError', + 'systemAborted', + 'noSpeechDetected', + 'connectivityError', + 'disabled', + 'internalError', + 'recognizerError', +]; +// Custom Dictation Errors: +DictationSessionStatus[64] = "sessionAlreadyInProgress"; +DictationSessionStatus[65] = "noMicrophone"; + +var StatusBarSeparatorModeTypes = [ + 'none', + 'dotted', +]; + +var StatusBarSeparatorModeType = makeArrayType(StatusBarSeparatorModeTypes); + +var Packet = new struct([ + ['uint16', 'type'], + ['uint16', 'length'], +]); + +var SegmentPacket = new struct([ + [Packet, 'packet'], + ['bool', 'isLast'], + ['data', 'buffer'], +]); + +var ReadyPacket = new struct([ + [Packet, 'packet'], +]); + +var LaunchReasonPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'reason', LaunchReasonType], + ['uint32', 'args'], + ['uint32', 'time'], + ['bool', 'isTimezone'], +]); + +var WakeupSetPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'timestamp', TimeType], + ['int32', 'cookie'], + ['uint8', 'notifyIfMissed', BoolType], +]); + +var WakeupSetResultPacket = new struct([ + [Packet, 'packet'], + ['int32', 'id'], + ['int32', 'cookie'], +]); + +var WakeupCancelPacket = new struct([ + [Packet, 'packet'], + ['int32', 'id'], +]); + +var WakeupEventPacket = new struct([ + [Packet, 'packet'], + ['int32', 'id'], + ['int32', 'cookie'], +]); + +var WindowShowPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'type', WindowType], + ['bool', 'pushing', BoolType], +]); + +var WindowHidePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], +]); + +var WindowShowEventPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], +]); + +var WindowHideEventPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], +]); + +var WindowPropsPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint8', 'backgroundColor', ColorType], + ['bool', 'scrollable', BoolType], + ['bool', 'paging', BoolType], +]); + +var WindowButtonConfigPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'buttonMask', ButtonFlagsType], +]); + +var WindowStatusBarPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'backgroundColor', ColorType], + ['uint8', 'color', ColorType], + ['uint8', 'separator', StatusBarSeparatorModeType], + ['uint8', 'status', BoolType], +]); + +var WindowActionBarPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'up', ImageType], + ['uint32', 'select', ImageType], + ['uint32', 'down', ImageType], + ['uint8', 'backgroundColor', ColorType], + ['uint8', 'action', BoolType], +]); + +var ClickPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'button', ButtonType], +]); + +var LongClickPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'button', ButtonType], +]); + +var ImagePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['int16', 'width'], + ['int16', 'height'], + ['uint16', 'pixelsLength'], + ['data', 'pixels'], +]); + +var CardClearPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'flags'], +]); + +var CardTextPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'index', CardTextType], + ['uint8', 'color', ColorType], + ['cstring', 'text'], +]); + +var CardImagePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'image', ImageType], + ['uint8', 'index', CardImageType], +]); + +var CardStylePacket = new struct([ + [Packet, 'packet'], + ['uint8', 'style', CardStyleType], +]); + +var VibePacket = new struct([ + [Packet, 'packet'], + ['uint8', 'type', VibeType], +]); + +var LightPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'type', LightType], +]); + +var AccelPeekPacket = new struct([ + [Packet, 'packet'], +]); + +var AccelConfigPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'samples'], + ['uint8', 'rate'], + ['bool', 'subscribe', BoolType], +]); + +var AccelData = new struct([ + ['int16', 'x'], + ['int16', 'y'], + ['int16', 'z'], + ['bool', 'vibe'], + ['uint64', 'time'], +]); + +var AccelDataPacket = new struct([ + [Packet, 'packet'], + ['bool', 'peek'], + ['uint8', 'samples'], +]); + +var AccelTapPacket = new struct([ + [Packet, 'packet'], + ['uint8', 'axis'], + ['int8', 'direction'], +]); + +var MenuClearPacket = new struct([ + [Packet, 'packet'], +]); + +var MenuClearSectionPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], +]); + +var MenuPropsPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'sections', EnumerableType], + ['uint8', 'backgroundColor', ColorType], + ['uint8', 'textColor', ColorType], + ['uint8', 'highlightBackgroundColor', ColorType], + ['uint8', 'highlightTextColor', ColorType], +]); + +var MenuSectionPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'items', EnumerableType], + ['uint8', 'backgroundColor', ColorType], + ['uint8', 'textColor', ColorType], + ['uint16', 'titleLength', StringLengthType], + ['cstring', 'title', StringType], +]); + +var MenuGetSectionPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], +]); + +var MenuItemPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], + ['uint32', 'icon', ImageType], + ['uint16', 'titleLength', StringLengthType], + ['uint16', 'subtitleLength', StringLengthType], + ['cstring', 'title', StringType], + ['cstring', 'subtitle', StringType], +]); + +var MenuGetItemPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], +]); + +var MenuSelectionPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], + ['uint8', 'align', MenuRowAlign], + ['bool', 'animated', BoolType], +]); + +var MenuGetSelectionPacket = new struct([ + [Packet, 'packet'], +]); + +var MenuSelectionEventPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], +]); + +var MenuSelectPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], +]); + +var MenuLongSelectPacket = new struct([ + [Packet, 'packet'], + ['uint16', 'section'], + ['uint16', 'item'], +]); + +var StageClearPacket = new struct([ + [Packet, 'packet'], +]); + +var ElementInsertPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint8', 'type'], + ['uint16', 'index'], +]); + +var ElementRemovePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], +]); + +var GPoint = new struct([ + ['int16', 'x'], + ['int16', 'y'], +]); + +var GSize = new struct([ + ['int16', 'w'], + ['int16', 'h'], +]); + +var GRect = new struct([ + [GPoint, 'origin', PositionType], + [GSize, 'size', SizeType], +]); + +var ElementCommonPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + [GPoint, 'position', PositionType], + [GSize, 'size', SizeType], + ['uint16', 'borderWidth', EnumerableType], + ['uint8', 'backgroundColor', ColorType], + ['uint8', 'borderColor', ColorType], +]); + +var ElementRadiusPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint16', 'radius', EnumerableType], +]); + +var ElementAnglePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint16', 'angle', EnumerableType], +]); + +var ElementAngle2Packet = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint16', 'angle2', EnumerableType], +]); + +var ElementTextPacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint8', 'updateTimeUnits', TimeUnits], + ['cstring', 'text', StringType], +]); + +var ElementTextStylePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint8', 'color', ColorType], + ['uint8', 'textOverflow', TextOverflowMode], + ['uint8', 'textAlign', TextAlignment], + ['uint32', 'customFont'], + ['cstring', 'systemFont', StringType], +]); + +var ElementImagePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + ['uint32', 'image', ImageType], + ['uint8', 'compositing', CompositingOp], +]); + +var ElementAnimatePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], + [GPoint, 'position', PositionType], + [GSize, 'size', SizeType], + ['uint32', 'duration'], + ['uint8', 'easing', AnimationCurve], +]); + +var ElementAnimateDonePacket = new struct([ + [Packet, 'packet'], + ['uint32', 'id'], +]); + +var VoiceDictationStartPacket = new struct([ + [Packet, 'packet'], + ['bool', 'enableConfirmation'], +]); + +var VoiceDictationStopPacket = new struct([ + [Packet, 'packet'], +]); + +var VoiceDictationDataPacket = new struct([ + [Packet, 'packet'], + ['int8', 'status'], + ['cstring', 'transcription'], +]); + +var CommandPackets = [ + Packet, + SegmentPacket, + ReadyPacket, + LaunchReasonPacket, + WakeupSetPacket, + WakeupSetResultPacket, + WakeupCancelPacket, + WakeupEventPacket, + WindowShowPacket, + WindowHidePacket, + WindowShowEventPacket, + WindowHideEventPacket, + WindowPropsPacket, + WindowButtonConfigPacket, + WindowStatusBarPacket, + WindowActionBarPacket, + ClickPacket, + LongClickPacket, + ImagePacket, + CardClearPacket, + CardTextPacket, + CardImagePacket, + CardStylePacket, + VibePacket, + LightPacket, + AccelPeekPacket, + AccelConfigPacket, + AccelDataPacket, + AccelTapPacket, + MenuClearPacket, + MenuClearSectionPacket, + MenuPropsPacket, + MenuSectionPacket, + MenuGetSectionPacket, + MenuItemPacket, + MenuGetItemPacket, + MenuSelectionPacket, + MenuGetSelectionPacket, + MenuSelectionEventPacket, + MenuSelectPacket, + MenuLongSelectPacket, + StageClearPacket, + ElementInsertPacket, + ElementRemovePacket, + ElementCommonPacket, + ElementRadiusPacket, + ElementAnglePacket, + ElementAngle2Packet, + ElementTextPacket, + ElementTextStylePacket, + ElementImagePacket, + ElementAnimatePacket, + ElementAnimateDonePacket, + VoiceDictationStartPacket, + VoiceDictationStopPacket, + VoiceDictationDataPacket, +]; + +var accelAxes = [ + 'x', + 'y', + 'z', +]; + +var clearFlagMap = { + action: (1 << 0), + text: (1 << 1), + image: (1 << 2), +}; + +/** + * SimplyPebble object provides the actual methods to communicate with Pebble. + * + * It's an implementation of an abstract interface used by all the other classes. + */ + +var SimplyPebble = {}; + +SimplyPebble.init = function() { + // Register listeners for app message communication + Pebble.addEventListener('appmessage', SimplyPebble.onAppMessage); + + // Register this implementation as the one currently in use + simply.impl = SimplyPebble; + + state = SimplyPebble.state = {}; + + state.timeOffset = new Date().getTimezoneOffset() * -60; + + // Initialize the app message queue + state.messageQueue = new MessageQueue(); + + // Initialize the packet queue + state.packetQueue = new PacketQueue(); + + // Signal the Pebble that the Phone's app message is ready + SimplyPebble.ready(); +}; + +/** + * MessageQueue is an app message queue that guarantees delivery and order. + */ +var MessageQueue = function() { + this._queue = []; + this._sending = false; + + this._consume = this.consume.bind(this); + this._cycle = this.cycle.bind(this); +}; + +MessageQueue.prototype.stop = function() { + this._sending = false; +}; + +MessageQueue.prototype.consume = function() { + this._queue.shift(); + if (this._queue.length === 0) { + return this.stop(); + } + this.cycle(); +}; + +MessageQueue.prototype.checkSent = function(message, fn) { + return function() { + if (message === this._sent) { + fn(); + } + }.bind(this); +}; + +MessageQueue.prototype.cycle = function() { + if (!this._sending) { + return; + } + var head = this._queue[0]; + if (!head) { + return this.stop(); + } + this._sent = head; + var success = this.checkSent(head, this._consume); + var failure = this.checkSent(head, this._cycle); + Pebble.sendAppMessage(head, success, failure); +}; + +MessageQueue.prototype.send = function(message) { + this._queue.push(message); + if (this._sending) { + return; + } + this._sending = true; + this.cycle(); +}; + +var toByteArray = function(packet) { + var type = CommandPackets.indexOf(packet); + var size = Math.max(packet._size, packet._cursor); + packet.packetType(type); + packet.packetLength(size); + + var buffer = packet._view; + var byteArray = new Array(size); + for (var i = 0; i < size; ++i) { + byteArray[i] = buffer.getUint8(i); + } + + return byteArray; +}; + +/** + * PacketQueue is a packet queue that combines multiple packets into a single packet. + * This reduces latency caused by the time spacing between each app message. + */ +var PacketQueue = function() { + this._message = []; + + this._send = this.send.bind(this); +}; + +PacketQueue.prototype._maxPayloadSize = (Platform.version() === 'aplite' ? 1024 : 2044) - 32; + +PacketQueue.prototype.add = function(packet) { + var byteArray = toByteArray(packet); + if (this._message.length + byteArray.length > this._maxPayloadSize) { + this.send(); + } + Array.prototype.push.apply(this._message, byteArray); + clearTimeout(this._timeout); + this._timeout = setTimeout(this._send, 0); +}; + +PacketQueue.prototype.send = function() { + if (this._message.length === 0) { + return; + } + state.messageQueue.send({ 0: this._message }); + this._message = []; +}; + +SimplyPebble.sendMultiPacket = function(packet) { + var byteArray = toByteArray(packet); + var totalSize = byteArray.length; + var segmentSize = state.packetQueue._maxPayloadSize - Packet._size; + for (var i = 0; i < totalSize; i += segmentSize) { + var isLast = (i + segmentSize) >= totalSize; + var buffer = byteArray.slice(i, Math.min(totalSize, i + segmentSize)); + SegmentPacket.isLast((i + segmentSize) >= totalSize).buffer(buffer); + state.packetQueue.add(SegmentPacket); + } +}; + +SimplyPebble.sendPacket = function(packet) { + if (packet._cursor < state.packetQueue._maxPayloadSize) { + state.packetQueue.add(packet); + } else { + SimplyPebble.sendMultiPacket(packet); + } +}; + +SimplyPebble.ready = function() { + SimplyPebble.sendPacket(ReadyPacket); +}; + +SimplyPebble.wakeupSet = function(timestamp, cookie, notifyIfMissed) { + WakeupSetPacket + .timestamp(timestamp) + .cookie(cookie) + .notifyIfMissed(notifyIfMissed); + SimplyPebble.sendPacket(WakeupSetPacket); +}; + +SimplyPebble.wakeupCancel = function(id) { + SimplyPebble.sendPacket(WakeupCancelPacket.id(id === 'all' ? -1 : id)); +}; + +SimplyPebble.windowShow = function(def) { + SimplyPebble.sendPacket(WindowShowPacket.prop(def)); +}; + +SimplyPebble.windowHide = function(id) { + SimplyPebble.sendPacket(WindowHidePacket.id(id)); +}; + +SimplyPebble.windowProps = function(def) { + WindowPropsPacket + .prop(def) + .backgroundColor(def.backgroundColor || 'white'); + SimplyPebble.sendPacket(WindowPropsPacket); +}; + +SimplyPebble.windowButtonConfig = function(def) { + SimplyPebble.sendPacket(WindowButtonConfigPacket.buttonMask(def)); +}; + +var toStatusDef = function(statusDef) { + if (typeof statusDef === 'boolean') { + statusDef = { status: statusDef }; + } + return statusDef; +}; + +SimplyPebble.windowStatusBar = function(def) { + var statusDef = toStatusDef(def); + WindowStatusBarPacket + .separator(statusDef.separator || 'dotted') + .status(typeof def === 'boolean' ? def : def.status !== false) + .color(statusDef.color || 'black') + .backgroundColor(statusDef.backgroundColor || 'white'); + SimplyPebble.sendPacket(WindowStatusBarPacket); +}; + +SimplyPebble.windowStatusBarCompat = function(def) { + if (typeof def.fullscreen === 'boolean') { + SimplyPebble.windowStatusBar(!def.fullscreen); + } else if (def.status !== undefined) { + SimplyPebble.windowStatusBar(def.status); + } +}; + +var toActionDef = function(actionDef) { + if (typeof actionDef === 'boolean') { + actionDef = { action: actionDef }; + } + return actionDef; +}; + +SimplyPebble.windowActionBar = function(def) { + var actionDef = toActionDef(def); + WindowActionBarPacket + .up(actionDef.up) + .select(actionDef.select) + .down(actionDef.down) + .action(typeof def === 'boolean' ? def : def.action !== false) + .backgroundColor(actionDef.backgroundColor || 'black'); + SimplyPebble.sendPacket(WindowActionBarPacket); +}; + +SimplyPebble.image = function(id, gbitmap) { + SimplyPebble.sendPacket(ImagePacket.id(id).prop(gbitmap)); +}; + +var toClearFlags = function(clear) { + if (clear === true || clear === 'all') { + clear = ~0; + } else if (typeof clear === 'string') { + clear = clearFlagMap[clear]; + } else if (typeof clear === 'object') { + var flags = 0; + for (var k in clear) { + if (clear[k] === true) { + flags |= clearFlagMap[k]; + } + } + clear = flags; + } + return clear; +}; + +SimplyPebble.cardClear = function(clear) { + SimplyPebble.sendPacket(CardClearPacket.flags(toClearFlags(clear))); +}; + +SimplyPebble.cardText = function(field, text, color) { + CardTextPacket + .index(field) + .color(color || 'clearWhite') + .text(text || ''); + SimplyPebble.sendPacket(CardTextPacket); +}; + +SimplyPebble.cardImage = function(field, image) { + SimplyPebble.sendPacket(CardImagePacket.index(field).image(image)); +}; + +SimplyPebble.cardStyle = function(field, style) { + SimplyPebble.sendPacket(CardStylePacket.style(style)); +}; + +SimplyPebble.card = function(def, clear, pushing) { + if (arguments.length === 3) { + SimplyPebble.windowShow({ type: 'card', pushing: pushing }); + } + if (clear !== undefined) { + SimplyPebble.cardClear(clear); + } + SimplyPebble.windowProps(def); + SimplyPebble.windowStatusBarCompat(def); + if (def.action !== undefined) { + SimplyPebble.windowActionBar(def.action); + } + for (var k in def) { + var textIndex = CardTextTypes.indexOf(k); + if (textIndex !== -1) { + SimplyPebble.cardText(k, def[k], def[CardTextColorTypes[textIndex]]); + } else if (CardImageTypes.indexOf(k) !== -1) { + SimplyPebble.cardImage(k, def[k]); + } else if (k === 'style') { + SimplyPebble.cardStyle(k, def[k]); + } + } +}; + +SimplyPebble.vibe = function(type) { + SimplyPebble.sendPacket(VibePacket.type(type)); +}; + +SimplyPebble.light = function(type) { + SimplyPebble.sendPacket(LightPacket.type(type)); +}; + +var accelListeners = []; + +SimplyPebble.accelPeek = function(callback) { + accelListeners.push(callback); + SimplyPebble.sendPacket(AccelPeekPacket); +}; + +SimplyPebble.accelConfig = function(def) { + SimplyPebble.sendPacket(AccelConfigPacket.prop(def)); +}; + +SimplyPebble.voiceDictationStart = function(callback, enableConfirmation) { + if (Platform.version() === 'aplite') { + // If there is no microphone, call with an error event + callback({ + 'err': DictationSessionStatus[65], // noMicrophone + 'failed': true, + 'transcription': null, + }); + return; + } else if (state.dictationCallback) { + // If there's a transcription in progress, call with an error event + callback({ + 'err': DictationSessionStatus[64], // dictationAlreadyInProgress + 'failed': true, + 'transcription': null, + }); + return; + } + + // Set the callback and send the packet + state.dictationCallback = callback; + SimplyPebble.sendPacket(VoiceDictationStartPacket.enableConfirmation(enableConfirmation)); +}; + +SimplyPebble.voiceDictationStop = function() { + // Send the message and delete the callback + SimplyPebble.sendPacket(VoiceDictationStopPacket); + delete state.dictationCallback; +}; + +SimplyPebble.onVoiceData = function(packet) { + if (!state.dictationCallback) { + // Something bad happened + console.log("No callback specified for dictation session"); + } else { + var e = { + 'err': DictationSessionStatus[packet.status()], + 'failed': packet.status() !== 0, + 'transcription': packet.transcription(), + }; + // Invoke and delete the callback + state.dictationCallback(e); + delete state.dictationCallback; + } +}; + +SimplyPebble.menuClear = function() { + SimplyPebble.sendPacket(MenuClearPacket); +}; + +SimplyPebble.menuClearSection = function(section) { + SimplyPebble.sendPacket(MenuClearSectionPacket.section(section)); +}; + +SimplyPebble.menuProps = function(def) { + SimplyPebble.sendPacket(MenuPropsPacket.prop(def)); +}; + +SimplyPebble.menuSection = function(section, def, clear) { + if (clear !== undefined) { + SimplyPebble.menuClearSection(section); + } + MenuSectionPacket + .section(section) + .items(def.items) + .backgroundColor(def.backgroundColor) + .textColor(def.textColor) + .titleLength(def.title) + .title(def.title); + SimplyPebble.sendPacket(MenuSectionPacket); +}; + +SimplyPebble.menuItem = function(section, item, def) { + MenuItemPacket + .section(section) + .item(item) + .icon(def.icon) + .titleLength(def.title) + .subtitleLength(def.subtitle) + .title(def.title) + .subtitle(def.subtitle); + SimplyPebble.sendPacket(MenuItemPacket); +}; + +SimplyPebble.menuSelection = function(section, item, align) { + if (section === undefined) { + SimplyPebble.sendPacket(MenuGetSelectionPacket); + return; + } + SimplyPebble.sendPacket(MenuSelectionPacket.section(section).item(item).align(align || 'center')); +}; + +SimplyPebble.menu = function(def, clear, pushing) { + if (typeof pushing === 'boolean') { + SimplyPebble.windowShow({ type: 'menu', pushing: pushing }); + } + if (clear !== undefined) { + SimplyPebble.menuClear(); + } + SimplyPebble.windowProps(def); + SimplyPebble.windowStatusBarCompat(def); + SimplyPebble.menuProps(def); +}; + +SimplyPebble.elementInsert = function(id, type, index) { + SimplyPebble.sendPacket(ElementInsertPacket.id(id).type(type).index(index)); +}; + +SimplyPebble.elementRemove = function(id) { + SimplyPebble.sendPacket(ElementRemovePacket.id(id)); +}; + +SimplyPebble.elementFrame = function(packet, def, altDef) { + var position = def.position || (altDef ? altDef.position : undefined); + var position2 = def.position2 || (altDef ? altDef.position2 : undefined); + var size = def.size || (altDef ? altDef.size : undefined); + if (position && position2) { + size = position2.clone().subSelf(position); + } + packet.position(position); + packet.size(size); +}; + +SimplyPebble.elementCommon = function(id, def) { + if ('strokeColor' in def) { + ElementCommonPacket.borderColor(def.strokeColor); + } + if ('strokeWidth' in def) { + ElementCommonPacket.borderWidth(def.strokeWidth); + } + SimplyPebble.elementFrame(ElementCommonPacket, def); + ElementCommonPacket + .id(id) + .prop(def); + SimplyPebble.sendPacket(ElementCommonPacket); +}; + +SimplyPebble.elementRadius = function(id, def) { + SimplyPebble.sendPacket(ElementRadiusPacket.id(id).radius(def.radius)); +}; + +SimplyPebble.elementAngle = function(id, def) { + SimplyPebble.sendPacket(ElementAnglePacket.id(id).angle(def.angleStart || def.angle)); +}; + +SimplyPebble.elementAngle2 = function(id, def) { + SimplyPebble.sendPacket(ElementAngle2Packet.id(id).angle2(def.angleEnd || def.angle2)); +}; + +SimplyPebble.elementText = function(id, text, timeUnits) { + SimplyPebble.sendPacket(ElementTextPacket.id(id).updateTimeUnits(timeUnits).text(text)); +}; + +SimplyPebble.elementTextStyle = function(id, def) { + ElementTextStylePacket.id(id).prop(def); + var font = Font(def.font); + if (typeof font === 'number') { + ElementTextStylePacket.customFont(font).systemFont(''); + } else { + ElementTextStylePacket.customFont(0).systemFont(font); + } + SimplyPebble.sendPacket(ElementTextStylePacket); +}; + +SimplyPebble.elementImage = function(id, image, compositing) { + SimplyPebble.sendPacket(ElementImagePacket.id(id).image(image).compositing(compositing)); +}; + +SimplyPebble.elementAnimate = function(id, def, animateDef, duration, easing) { + SimplyPebble.elementFrame(ElementAnimatePacket, animateDef, def); + ElementAnimatePacket + .id(id) + .duration(duration) + .easing(easing); + SimplyPebble.sendPacket(ElementAnimatePacket); +}; + +SimplyPebble.stageClear = function() { + SimplyPebble.sendPacket(StageClearPacket); +}; + +SimplyPebble.stageElement = function(id, type, def, index) { + if (index !== undefined) { + SimplyPebble.elementInsert(id, type, index); + } + SimplyPebble.elementCommon(id, def); + switch (type) { + case StageElement.RectType: + case StageElement.CircleType: + SimplyPebble.elementRadius(id, def); + break; + case StageElement.RadialType: + SimplyPebble.elementRadius(id, def); + SimplyPebble.elementAngle(id, def); + SimplyPebble.elementAngle2(id, def); + break; + case StageElement.TextType: + SimplyPebble.elementRadius(id, def); + SimplyPebble.elementTextStyle(id, def); + SimplyPebble.elementText(id, def.text, def.updateTimeUnits); + break; + case StageElement.ImageType: + SimplyPebble.elementRadius(id, def); + SimplyPebble.elementImage(id, def.image, def.compositing); + break; + } +}; + +SimplyPebble.stageRemove = SimplyPebble.elementRemove; + +SimplyPebble.stageAnimate = SimplyPebble.elementAnimate; + +SimplyPebble.stage = function(def, clear, pushing) { + if (arguments.length === 3) { + SimplyPebble.windowShow({ type: 'window', pushing: pushing }); + } + SimplyPebble.windowProps(def); + SimplyPebble.windowStatusBarCompat(def); + if (clear !== undefined) { + SimplyPebble.stageClear(); + } + if (def.action !== undefined) { + SimplyPebble.windowActionBar(def.action); + } +}; + +SimplyPebble.window = SimplyPebble.stage; + +var toArrayBuffer = function(array, length) { + length = length || array.length; + var copy = new DataView(new ArrayBuffer(length)); + for (var i = 0; i < length; ++i) { + copy.setUint8(i, array[i]); + } + return copy; +}; + +SimplyPebble.onLaunchReason = function(packet) { + var reason = LaunchReasonTypes[packet.reason()]; + var args = packet.args(); + var remoteTime = packet.time(); + var isTimezone = packet.isTimezone(); + if (isTimezone) { + state.timeOffset = 0; + } else { + var time = Date.now() / 1000; + var resolution = 60 * 30; + state.timeOffset = Math.round((remoteTime - time) / resolution) * resolution; + } + if (reason === 'timelineAction') { + Timeline.emitAction(args); + } else { + Timeline.emitAction(); + } + if (reason !== 'wakeup') { + Wakeup.emitWakeup(); + } +}; + +SimplyPebble.onWakeupSetResult = function(packet) { + var id = packet.id(); + switch (id) { + case -8: id = 'range'; break; + case -4: id = 'invalidArgument'; break; + case -7: id = 'outOfResources'; break; + case -3: id = 'internal'; break; + } + Wakeup.emitSetResult(id, packet.cookie()); +}; + +SimplyPebble.onAccelData = function(packet) { + var samples = packet.samples(); + var accels = []; + AccelData._view = packet._view; + AccelData._offset = packet._size; + for (var i = 0; i < samples; ++i) { + accels.push(AccelData.prop()); + AccelData._offset += AccelData._size; + } + if (!packet.peek()) { + Accel.emitAccelData(accels); + } else { + var handlers = accelListeners; + accelListeners = []; + for (var j = 0, jj = handlers.length; j < jj; ++j) { + Accel.emitAccelData(accels, handlers[j]); + } + } +}; + +SimplyPebble.onPacket = function(buffer, offset) { + Packet._view = buffer; + Packet._offset = offset; + var packet = CommandPackets[Packet.type()]; + + if (!packet) { + console.log('Received unknown packet: ' + JSON.stringify(buffer)); + return; + } + + packet._view = Packet._view; + packet._offset = offset; + switch (packet) { + case LaunchReasonPacket: + SimplyPebble.onLaunchReason(packet); + break; + case WakeupSetResultPacket: + SimplyPebble.onWakeupSetResult(packet); + break; + case WakeupEventPacket: + Wakeup.emitWakeup(packet.id(), packet.cookie()); + break; + case WindowHideEventPacket: + ImageService.markAllUnloaded(); + WindowStack.emitHide(packet.id()); + break; + case ClickPacket: + Window.emitClick('click', ButtonTypes[packet.button()]); + break; + case LongClickPacket: + Window.emitClick('longClick', ButtonTypes[packet.button()]); + break; + case AccelDataPacket: + SimplyPebble.onAccelData(packet); + break; + case AccelTapPacket: + Accel.emitAccelTap(accelAxes[packet.axis()], packet.direction()); + break; + case MenuGetSectionPacket: + Menu.emitSection(packet.section()); + break; + case MenuGetItemPacket: + Menu.emitItem(packet.section(), packet.item()); + break; + case MenuSelectPacket: + Menu.emitSelect('menuSelect', packet.section(), packet.item()); + break; + case MenuLongSelectPacket: + Menu.emitSelect('menuLongSelect', packet.section(), packet.item()); + break; + case MenuSelectionEventPacket: + Menu.emitSelect('menuSelection', packet.section(), packet.item()); + break; + case ElementAnimateDonePacket: + StageElement.emitAnimateDone(packet.id()); + break; + case VoiceDictationDataPacket: + SimplyPebble.onVoiceData(packet); + break; + } +}; + +SimplyPebble.onAppMessage = function(e) { + var data = e.payload[0]; + + Packet._view = toArrayBuffer(data); + + var offset = 0; + var length = data.length; + + do { + SimplyPebble.onPacket(Packet._view, offset); + + Packet._offset = offset; + offset += Packet.length(); + } while (offset !== 0 && offset < length); +}; + +module.exports = SimplyPebble; + diff --git a/src/js/ui/simply.js b/src/js/ui/simply.js new file mode 100644 index 00000000..a0d5f2d9 --- /dev/null +++ b/src/js/ui/simply.js @@ -0,0 +1,13 @@ +/** + * This file provides an easy way to switch the actual implementation used by all the + * ui objects. + * + * simply.impl provides the actual communication layer to the hardware. + */ + +var simply = {}; + +// Override this with the actual implementation you want to use. +simply.impl = undefined; + +module.exports = simply; diff --git a/src/js/ui/stage.js b/src/js/ui/stage.js new file mode 100644 index 00000000..f8ec59d0 --- /dev/null +++ b/src/js/ui/stage.js @@ -0,0 +1,80 @@ +var util2 = require('util2'); +var Emitter = require('emitter'); +var WindowStack = require('ui/windowstack'); +var simply = require('ui/simply'); + +var Stage = function(stageDef) { + this.state = stageDef || {}; + this._items = []; +}; + +Stage.RectType = 1; +Stage.CircleType = 2; +Stage.RadialType = 6; +Stage.TextType = 3; +Stage.ImageType = 4; +Stage.InverterType = 5; + +util2.copy(Emitter.prototype, Stage.prototype); + +Stage.prototype._show = function() { + this.each(function(element, index) { + element._reset(); + this._insert(index, element); + }.bind(this)); +}; + +Stage.prototype._prop = function() { + if (this === WindowStack.top()) { + simply.impl.stage.apply(this, arguments); + } +}; + +Stage.prototype.each = function(callback) { + this._items.forEach(callback); + return this; +}; + +Stage.prototype.at = function(index) { + return this._items[index]; +}; + +Stage.prototype.index = function(element) { + return this._items.indexOf(element); +}; + +Stage.prototype._insert = function(index, element) { + if (this === WindowStack.top()) { + simply.impl.stageElement(element._id(), element._type(), element.state, index); + } +}; + +Stage.prototype._remove = function(element, broadcast) { + if (broadcast === false) { return; } + if (this === WindowStack.top()) { + simply.impl.stageRemove(element._id()); + } +}; + +Stage.prototype.insert = function(index, element) { + element.remove(false); + this._items.splice(index, 0, element); + element.parent = this; + this._insert(this.index(element), element); + return this; +}; + +Stage.prototype.add = function(element) { + return this.insert(this._items.length, element); +}; + +Stage.prototype.remove = function(element, broadcast) { + var index = this.index(element); + if (index === -1) { return this; } + this._remove(element, broadcast); + this._items.splice(index, 1); + delete element.parent; + return this; +}; + +module.exports = Stage; diff --git a/src/js/ui/tests.js b/src/js/ui/tests.js new file mode 100644 index 00000000..9275ac5b --- /dev/null +++ b/src/js/ui/tests.js @@ -0,0 +1,39 @@ + +var tests = {}; + +tests.setTimeoutErrors = function () { + /* global wind */ + var i = 0; + var interval = setInterval(function() { + clearInterval(interval); + wind.titlex('i = ' + i++); + }, 1000); +}; + +tests.ajaxErrors = function() { + var ajax = require('ajax'); + var ajaxCallback = function(reqStatus, reqBody, request) { + console.logx('broken call'); + }; + ajax({ url: 'http://www.google.fr/' }, ajaxCallback, ajaxCallback); +}; + +tests.geolocationErrors = function () { + navigator.geolocation.getCurrentPosition(function(coords) { + console.logx('Got coords: ' + coords); + }); +}; + +tests.loadAppinfo = function() { + console.log('longName: ' + require('appinfo').longName); +}; + +tests.resolveBultinImagePath = function() { + var ImageService = require('ui/imageservice'); + console.log('image-logo-splash = resource #' + ImageService.resolve('images/logo_splash.png')); +}; + +for (var test in tests) { + console.log('Running test: ' + test); + tests[test](); +} diff --git a/src/js/ui/text.js b/src/js/ui/text.js new file mode 100644 index 00000000..eecd4571 --- /dev/null +++ b/src/js/ui/text.js @@ -0,0 +1,32 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Propable = require('ui/propable'); +var StageElement = require('ui/element'); + +var textProps = [ + 'text', + 'font', + 'color', + 'textOverflow', + 'textAlign', + 'updateTimeUnits', +]; + +var defaults = { + backgroundColor: 'clear', + borderColor: 'clear', + borderWidth: 1, + color: 'white', + font: 'gothic-24', +}; + +var Text = function(elementDef) { + StageElement.call(this, myutil.shadow(defaults, elementDef || {})); + this.state.type = StageElement.TextType; +}; + +util2.inherit(Text, StageElement); + +Propable.makeAccessors(textProps, Text.prototype); + +module.exports = Text; diff --git a/src/js/ui/timetext.js b/src/js/ui/timetext.js new file mode 100644 index 00000000..7054758a --- /dev/null +++ b/src/js/ui/timetext.js @@ -0,0 +1,59 @@ +var util2 = require('util2'); +var Text = require('ui/text'); + +var TimeText = function(elementDef) { + Text.call(this, elementDef); + if (this.state.text) { + this.text(this.state.text); + } +}; + +util2.inherit(TimeText, Text); + +var formatUnits = { + a: 'days', + A: 'days', + b: 'months', + B: 'months', + c: 'seconds', + d: 'days', + H: 'hours', + I: 'hours', + j: 'days', + m: 'months', + M: 'minutes', + p: 'hours', + S: 'seconds', + U: 'days', + w: 'days', + W: 'days', + x: 'days', + X: 'seconds', + y: 'years', + Y: 'years', +}; + +var getUnitsFromText = function(text) { + var units = {}; + text.replace(/%(.)/g, function(_, code) { + var unit = formatUnits[code]; + if (unit) { + units[unit] = true; + } + return _; + }); + return units; +}; + +TimeText.prototype.text = function(text) { + if (arguments.length === 0) { + return this.state.text; + } + this.prop({ + text: text, + updateTimeUnits: getUnitsFromText(text), + }); + return this; +}; + +module.exports = TimeText; diff --git a/src/js/ui/vibe.js b/src/js/ui/vibe.js new file mode 100644 index 00000000..3e23fa21 --- /dev/null +++ b/src/js/ui/vibe.js @@ -0,0 +1,7 @@ +var simply = require('ui/simply'); + +var Vibe = module.exports; + +Vibe.vibrate = function(type) { + simply.impl.vibe(type); +}; diff --git a/src/js/ui/voice.js b/src/js/ui/voice.js new file mode 100644 index 00000000..609ae76f --- /dev/null +++ b/src/js/ui/voice.js @@ -0,0 +1,24 @@ +var simply = require('ui/simply'); + +var Voice = {}; + +Voice.dictate = function(type, confirm, callback) { + type = type.toLowerCase(); + switch (type){ + case 'stop': + simply.impl.voiceDictationStop(); + break; + case 'start': + if (typeof callback === 'undefined') { + callback = confirm; + confirm = true; + } + + simply.impl.voiceDictationStart(callback, confirm); + break; + default: + console.log('Unsupported type passed to Voice.dictate'); + } +}; + +module.exports = Voice; diff --git a/src/js/ui/window.js b/src/js/ui/window.js new file mode 100644 index 00000000..b26112a3 --- /dev/null +++ b/src/js/ui/window.js @@ -0,0 +1,314 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var safe = require('safe'); +var Emitter = require('emitter'); +var Vector2 = require('vector2'); +var Feature = require('platform/feature'); +var Accel = require('ui/accel'); +var WindowStack = require('ui/windowstack'); +var Propable = require('ui/propable'); +var Stage = require('ui/stage'); +var simply = require('ui/simply'); + +var buttons = [ + 'back', + 'up', + 'select', + 'down', +]; + +var configProps = [ + 'fullscreen', + 'style', + 'scrollable', + 'paging', + 'backgroundColor', +]; + +var statusProps = [ + 'status', + 'separator', + 'color', + 'backgroundColor', +]; + +var actionProps = [ + 'action', + 'up', + 'select', + 'back', + 'backgroundColor', +]; + +var accessorProps = configProps; + +var nestedProps = [ + 'action', + 'status', +]; + +var defaults = { + status: false, + backgroundColor: 'black', + scrollable: false, + paging: Feature.round(true, false), +}; + +var nextId = 1; + +var checkProps = function(def) { + if (!def) return; + if ('fullscreen' in def && safe.warnFullscreen !== false) { + safe.warn('`fullscreen` has been deprecated by `status` which allows settings\n\t' + + 'its color and separator in a similar manner to the `action` property.\n\t' + + 'Remove usages of `fullscreen` to enable usage of `status`.', 2); + safe.warnFullscreen = false; + } +}; + +var Window = function(windowDef) { + checkProps(windowDef); + this.state = myutil.shadow(defaults, windowDef || {}); + this.state.id = nextId++; + this._buttonInit(); + this._items = []; + this._dynamic = true; + this._size = new Vector2(); + this.size(); // calculate and set the size +}; + +Window._codeName = 'window'; + +util2.copy(Emitter.prototype, Window.prototype); + +util2.copy(Propable.prototype, Window.prototype); + +util2.copy(Stage.prototype, Window.prototype); + +Propable.makeAccessors(accessorProps, Window.prototype); + +Propable.makeNestedAccessors(nestedProps, Window.prototype); + +Window.prototype._id = function() { + return this.state.id; +}; + +Window.prototype._prop = function(def, clear, pushing) { + checkProps(def); + Stage.prototype._prop.call(this, def, clear, pushing); +}; + +Window.prototype._hide = function(broadcast) { + if (broadcast === false) { return; } + simply.impl.windowHide(this._id()); +}; + +Window.prototype.hide = function() { + WindowStack.remove(this, true); + return this; +}; + +Window.prototype._show = function(pushing) { + this._prop(this.state, true, pushing || false); + this._buttonConfig({}); + if (this._dynamic) { + Stage.prototype._show.call(this, pushing); + } +}; + +Window.prototype.show = function() { + WindowStack.push(this); + return this; +}; + +Window.prototype._insert = function() { + if (this._dynamic) { + Stage.prototype._insert.apply(this, arguments); + } +}; + +Window.prototype._remove = function() { + if (this._dynamic) { + Stage.prototype._remove.apply(this, arguments); + } +}; + +Window.prototype._clearStatus = function() { + statusProps.forEach(Propable.unset.bind(this.state.status)); +}; + +Window.prototype._clearAction = function() { + actionProps.forEach(Propable.unset.bind(this.state.action)); +}; + +Window.prototype._clear = function(flags_) { + var flags = myutil.toFlags(flags_); + if (myutil.flag(flags, 'action')) { + this._clearAction(); + } + if (myutil.flag(flags, 'status')) { + this._clearStatus(); + } + if (flags_ === true || flags_ === undefined) { + Propable.prototype._clear.call(this); + } +}; + +Window.prototype._action = function(actionDef) { + if (this === WindowStack.top()) { + simply.impl.windowActionBar(actionDef); + } +}; + +Window.prototype._status = function(statusDef) { + if (this === WindowStack.top()) { + simply.impl.windowStatusBar(statusDef); + } +}; + +var isBackEvent = function(type, subtype) { + return ((type === 'click' || type === 'longClick') && subtype === 'back'); +}; + +Window.prototype.onAddHandler = function(type, subtype) { + if (isBackEvent(type, subtype)) { + this._buttonAutoConfig(); + } + if (type === 'accelData') { + Accel.autoSubscribe(); + } +}; + +Window.prototype.onRemoveHandler = function(type, subtype) { + if (!type || isBackEvent(type, subtype)) { + this._buttonAutoConfig(); + } + if (!type || type === 'accelData') { + Accel.autoSubscribe(); + } +}; + +Window.prototype._buttonInit = function() { + this._button = { + config: {}, + configMode: 'auto', + }; + for (var i = 0, ii = buttons.length; i < ii; i++) { + var button = buttons[i]; + if (button !== 'back') { + this._button.config[buttons[i]] = true; + } + } +}; + +/** + * The button configuration parameter for {@link simply.buttonConfig}. + * The button configuration allows you to enable to disable buttons without having to register or unregister handlers if that is your preferred style. + * You may also enable the back button manually as an alternative to registering a click handler with 'back' as its subtype using {@link simply.on}. + * @typedef {object} simply.buttonConf + * @property {boolean} [back] - Whether to enable the back button. Initializes as false. Simply.js can also automatically register this for you based on the amount of click handlers with subtype 'back'. + * @property {boolean} [up] - Whether to enable the up button. Initializes as true. Note that this is disabled when using {@link simply.scrollable}. + * @property {boolean} [select] - Whether to enable the select button. Initializes as true. + * @property {boolean} [down] - Whether to enable the down button. Initializes as true. Note that this is disabled when using {@link simply.scrollable}. + */ + +/** + * Changes the button configuration. + * See {@link simply.buttonConfig} + * @memberOf simply + * @param {simply.buttonConfig} buttonConf - An object defining the button configuration. + */ +Window.prototype._buttonConfig = function(buttonConf, auto) { + if (buttonConf === undefined) { + var config = {}; + for (var i = 0, ii = buttons.length; i < ii; ++i) { + var name = buttons[i]; + config[name] = this._button.config[name]; + } + return config; + } + for (var k in buttonConf) { + if (buttons.indexOf(k) !== -1) { + if (k === 'back') { + this._button.configMode = buttonConf.back && !auto ? 'manual' : 'auto'; + } + this._button.config[k] = buttonConf[k]; + } + } + if (simply.impl.windowButtonConfig) { + return simply.impl.windowButtonConfig(this._button.config); + } +}; + +Window.prototype.buttonConfig = function(buttonConf) { + this._buttonConfig(buttonConf); +}; + +Window.prototype._buttonAutoConfig = function() { + if (!this._button || this._button.configMode !== 'auto') { + return; + } + var singleBackCount = this.listenerCount('click', 'back'); + var longBackCount = this.listenerCount('longClick', 'back'); + var useBack = singleBackCount + longBackCount > 0; + if (useBack !== this._button.config.back) { + this._button.config.back = useBack; + return this._buttonConfig(this._button.config, true); + } +}; + +Window.prototype.size = function() { + var state = this.state; + var size = this._size.copy(Feature.resolution()); + if ('status' in state && state.status !== false) { + size.y -= Feature.statusBarHeight(); + } else if ('fullscreen' in state && state.fullscreen === false) { + size.y -= Feature.statusBarHeight(); + } + if ('action' in state && state.action !== false) { + size.x -= Feature.actionBarWidth(); + } + return size; +}; + +Window.prototype._toString = function() { + return '[' + this.constructor._codeName + ' ' + this._id() + ']'; +}; + +Window.prototype._emit = function(type, subtype, e) { + e.window = this; + var klass = this.constructor; + if (klass) { + e[klass._codeName] = this; + } + if (this.emit(type, subtype, e) === false) { + return false; + } +}; + +Window.prototype._emitShow = function(type) { + return this._emit(type, null, {}); +}; + +Window.emit = function(type, subtype, e) { + var wind = WindowStack.top(); + if (wind) { + return wind._emit(type, subtype, e); + } +}; + +/** + * Simply.js button click event. This can either be a single click or long click. + * Use the event type 'click' or 'longClick' to subscribe to these events. + * @typedef simply.clickEvent + * @property {string} button - The button that was pressed: 'back', 'up', 'select', or 'down'. This is also the event subtype. + */ + +Window.emitClick = function(type, button) { + var e = { + button: button, + }; + return Window.emit(type, button, e); +}; + +module.exports = Window; diff --git a/src/js/ui/windowstack.js b/src/js/ui/windowstack.js new file mode 100644 index 00000000..2d0c61b6 --- /dev/null +++ b/src/js/ui/windowstack.js @@ -0,0 +1,127 @@ +var util2 = require('util2'); +var myutil = require('myutil'); +var Emitter = require('emitter'); +var simply = require('ui/simply'); + +var WindowStack = function() { + this.init(); +}; + +util2.copy(Emitter.prototype, WindowStack.prototype); + +WindowStack.prototype.init = function() { + this.off(); + this._items = []; + +}; + +WindowStack.prototype.top = function() { + return util2.last(this._items); +}; + +WindowStack.prototype._emitShow = function(item) { + item.forEachListener(item.onAddHandler); + item._emitShow('show'); + + var e = { + window: item + }; + this.emit('show', e); +}; + +WindowStack.prototype._emitHide = function(item) { + var e = { + window: item + }; + this.emit('hide', e); + + item._emitShow('hide'); + item.forEachListener(item.onRemoveHandler); +}; + +WindowStack.prototype._show = function(item, pushing) { + if (!item) { return; } + item._show(pushing); + this._emitShow(item); +}; + +WindowStack.prototype._hide = function(item, broadcast) { + if (!item) { return; } + this._emitHide(item); + item._hide(broadcast); +}; + +WindowStack.prototype.at = function(index) { + return this._items[index]; +}; + +WindowStack.prototype.index = function(item) { + return this._items.indexOf(item); +}; + +WindowStack.prototype.push = function(item) { + if (item === this.top()) { return; } + this.remove(item); + var prevTop = this.top(); + this._items.push(item); + this._show(item, true); + this._hide(prevTop, false); + console.log('(+) ' + item._toString() + ' : ' + this._toString()); +}; + +WindowStack.prototype.pop = function(broadcast) { + return this.remove(this.top(), broadcast); +}; + +WindowStack.prototype.remove = function(item, broadcast) { + if (typeof item === 'number') { + item = this.get(item); + } + if (!item) { return; } + var index = this.index(item); + if (index === -1) { return item; } + var wasTop = (item === this.top()); + this._items.splice(index, 1); + if (wasTop) { + var top = this.top(); + this._show(top); + this._hide(item, top && top.constructor === item.constructor ? false : broadcast); + } + console.log('(-) ' + item._toString() + ' : ' + this._toString()); + return item; +}; + +WindowStack.prototype.get = function(windowId) { + var items = this._items; + for (var i = 0, ii = items.length; i < ii; ++i) { + var wind = items[i]; + if (wind._id() === windowId) { + return wind; + } + } +}; + +WindowStack.prototype.each = function(callback) { + var items = this._items; + for (var i = 0, ii = items.length; i < ii; ++i) { + if (callback(items[i], i) === false) { + break; + } + } +}; + +WindowStack.prototype.length = function() { + return this._items.length; +}; + +WindowStack.prototype.emitHide = function(windowId) { + var wind = this.get(windowId); + if (wind !== this.top()) { return; } + this.remove(wind); +}; + +WindowStack.prototype._toString = function() { + return this._items.map(function(x){ return x._toString(); }).join(','); +}; + +module.exports = new WindowStack(); diff --git a/src/js/vendor/moment.js b/src/js/vendor/moment.js new file mode 100644 index 00000000..c635ec0b --- /dev/null +++ b/src/js/vendor/moment.js @@ -0,0 +1,3043 @@ +//! moment.js +//! version : 2.9.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com + +(function (undefined) { + /************************************ + Constants + ************************************/ + + var moment, + VERSION = '2.9.0', + // the global-scope this is NOT the global object in Node.js + globalScope = (typeof global !== 'undefined' && (typeof window === 'undefined' || window === global.window)) ? global : this, + oldGlobalMoment, + round = Math.round, + hasOwnProperty = Object.prototype.hasOwnProperty, + i, + + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + + // internal storage for locale config files + locales = {}, + + // extra moment internal properties (plugins register props here) + momentProperties = [], + + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module && module.exports), + + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, + + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, + + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenDigits = /\d+/, // nonzero number of digits + parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO separator) + parseTokenOffsetMs = /[\+\-]?\d+/, // 1234567890123 + parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + + //strict parsing regexes + parseTokenOneDigit = /\d/, // 0 - 9 + parseTokenTwoDigits = /\d\d/, // 00 - 99 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{4}/, // 0000 - 9999 + parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 + parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ], + + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ], + + // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-', '15', '30'] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + + // getter and setter names + proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, + + unitAliases = { + ms : 'millisecond', + s : 'second', + m : 'minute', + h : 'hour', + d : 'day', + D : 'date', + w : 'week', + W : 'isoWeek', + M : 'month', + Q : 'quarter', + y : 'year', + DDD : 'dayOfYear', + e : 'weekday', + E : 'isoWeekday', + gg: 'weekYear', + GG: 'isoWeekYear' + }, + + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, + + // format function strings + formatFunctions = {}, + + // default relative time thresholds + relativeTimeThresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }, + + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), + + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.localeData().monthsShort(this, format); + }, + MMMM : function (format) { + return this.localeData().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.localeData().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.localeData().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.localeData().weekdays(this, format); + }, + w : function () { + return this.week(); + }, + W : function () { + return this.isoWeek(); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + YYYYY : function () { + return leftZeroFill(this.year(), 5); + }, + YYYYYY : function () { + var y = this.year(), sign = y >= 0 ? '+' : '-'; + return sign + leftZeroFill(Math.abs(y), 6); + }, + gg : function () { + return leftZeroFill(this.weekYear() % 100, 2); + }, + gggg : function () { + return leftZeroFill(this.weekYear(), 4); + }, + ggggg : function () { + return leftZeroFill(this.weekYear(), 5); + }, + GG : function () { + return leftZeroFill(this.isoWeekYear() % 100, 2); + }, + GGGG : function () { + return leftZeroFill(this.isoWeekYear(), 4); + }, + GGGGG : function () { + return leftZeroFill(this.isoWeekYear(), 5); + }, + e : function () { + return this.weekday(); + }, + E : function () { + return this.isoWeekday(); + }, + a : function () { + return this.localeData().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.localeData().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return toInt(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(toInt(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + SSSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = this.utcOffset(), + b = '+'; + if (a < 0) { + a = -a; + b = '-'; + } + return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); + }, + ZZ : function () { + var a = this.utcOffset(), + b = '+'; + if (a < 0) { + a = -a; + b = '-'; + } + return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); + }, + z : function () { + return this.zoneAbbr(); + }, + zz : function () { + return this.zoneName(); + }, + x : function () { + return this.valueOf(); + }, + X : function () { + return this.unix(); + }, + Q : function () { + return this.quarter(); + } + }, + + deprecations = {}, + + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], + + updateInProgress = false; + + // Pick the first defined of two or three arguments. dfl comes from + // default. + function dfl(a, b, c) { + switch (arguments.length) { + case 2: return a != null ? a : b; + case 3: return a != null ? a : b != null ? b : c; + default: throw new Error('Implement me'); + } + } + + function hasOwnProp(a, b) { + return hasOwnProperty.call(a, b); + } + + function defaultParsingFlags() { + // We need to deep clone this object, and es5 standard is not very + // helpful. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false + }; + } + + function printMsg(msg) { + if (moment.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } + + function deprecate(msg, fn) { + var firstTime = true; + return extend(function () { + if (firstTime) { + printMsg(msg); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } + + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + printMsg(msg); + deprecations[name] = true; + } + } + + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func, period) { + return function (a) { + return this.localeData().ordinal(func.call(this, a), period); + }; + } + + function monthDiff(a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + return -(wholeMonthDiff + adjust); + } + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + + + function meridiemFixWrap(locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // thie is not supposed to happen + return hour; + } + } + + /************************************ + Constructors + ************************************/ + + function Locale() { + } + + // Moment prototype object + function Moment(config, skipOverflow) { + if (skipOverflow !== false) { + checkOverflow(config); + } + copyConfig(this, config); + this._d = new Date(+config._d); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + moment.updateOffset(this); + updateInProgress = false; + } + } + + // Duration Constructor + function Duration(duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; + + this._data = {}; + + this._locale = moment.localeData(); + + this._bubble(); + } + + /************************************ + Helpers + ************************************/ + + + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } + + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } + + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; + } + + function copyConfig(to, from) { + var i, prop, val; + + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; + } + if (typeof from._i !== 'undefined') { + to._i = from._i; + } + if (typeof from._f !== 'undefined') { + to._f = from._f; + } + if (typeof from._l !== 'undefined') { + to._l = from._l; + } + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; + } + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; + } + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; + } + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; + } + if (typeof from._pf !== 'undefined') { + to._pf = from._pf; + } + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; + } + + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } + + return to; + } + + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; + + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } + + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; + + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } + + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + + return res; + } + + function momentsDifference(base, other) { + var res; + other = makeAs(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } + + return res; + } + + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; + } + + val = typeof val === 'string' ? +val : val; + dur = moment.duration(val, period); + addOrSubtractDurationFromMoment(this, dur, direction); + return this; + }; + } + + function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; + + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); + } + if (months) { + rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + moment.updateOffset(mom, days || months); + } + } + + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } + } + return diffs + lengthDiff; + } + + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function makeList(field) { + var count, setter; + + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } + + moment[field] = function (format, index) { + var i, getter, + method = moment._locale[field], + results = []; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment._locale, m, format || ''); + }; + + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } + + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; + } + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + function weeksInYear(year, dow, doy) { + return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; + } + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + function checkOverflow(m) { + var overflow; + if (m._a && m._pf.overflow === -2) { + overflow = + m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : + m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : + m._a[HOUR] < 0 || m._a[HOUR] > 24 || + (m._a[HOUR] === 24 && (m._a[MINUTE] !== 0 || + m._a[SECOND] !== 0 || + m._a[MILLISECOND] !== 0)) ? HOUR : + m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : + m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : + m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + m._pf.overflow = overflow; + } + } + + function isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0 && + m._pf.bigHour === undefined; + } + } + return m._isValid; + } + + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } + + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, j, next, locale, split; + + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return null; + } + + function loadLocale(name) { + var oldLocale = null; + if (!locales[name] && hasModule) { + try { + oldLocale = moment.locale(); + require('./locale/' + name); + // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales + moment.locale(oldLocale); + } catch (e) { } + } + return locales[name]; + } + + // Return a moment from input, that is local/utc/utcOffset equivalent to + // model. + function makeAs(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (moment.isMoment(input) || isDate(input) ? + +input : +moment(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + moment.updateOffset(res, false); + return res; + } else { + return moment(input).local(); + } + } + + /************************************ + Locale + ************************************/ + + + extend(Locale.prototype, { + + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source); + }, + + _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + months : function (m) { + return this._months[m.month()]; + }, + + _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, + + monthsParse : function (monthName, format, strict) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = moment.utc([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); + } + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } + }, + + _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, + + _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, + + _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, + + weekdaysParse : function (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = moment([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + }, + + _longDateFormat : { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY LT', + LLLL : 'dddd, MMMM D, YYYY LT' + }, + longDateFormat : function (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + }, + + isPM : function (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + }, + + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + + + _calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + calendar : function (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom, [now]) : output; + }, + + _relativeTime : { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }, + + relativeTime : function (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + }, + + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, + + ordinal : function (number) { + return this._ordinal.replace('%d', number); + }, + _ordinal : '%d', + _ordinalParse : /\d{1,2}/, + + preparse : function (string) { + return string; + }, + + postformat : function (string) { + return string; + }, + + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, + + _week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }, + + firstDayOfWeek : function () { + return this._week.dow; + }, + + firstDayOfYear : function () { + return this._week.doy; + }, + + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); + + /************************************ + Formatting + ************************************/ + + + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ''; + for (i = 0; i < length; i++) { + output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } + + format = expandFormat(format, m.localeData()); + + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); + } + + function expandFormat(format, locale) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; + } + + + /************************************ + Parsing + ************************************/ + + + // get the regex to find the next token + function getParseRegexForToken(token, config) { + var a, strict = config._strict; + switch (token) { + case 'Q': + return parseTokenOneDigit; + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + case 'GGGG': + case 'gggg': + return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; + case 'Y': + case 'G': + case 'g': + return parseTokenSignedNumber; + case 'YYYYYY': + case 'YYYYY': + case 'GGGGG': + case 'ggggg': + return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; + case 'S': + if (strict) { + return parseTokenOneDigit; + } + /* falls through */ + case 'SS': + if (strict) { + return parseTokenTwoDigits; + } + /* falls through */ + case 'SSS': + if (strict) { + return parseTokenThreeDigits; + } + /* falls through */ + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + return parseTokenWord; + case 'a': + case 'A': + return config._locale._meridiemParse; + case 'x': + return parseTokenOffsetMs; + case 'X': + return parseTokenTimestampMs; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'SSSS': + return parseTokenDigits; + case 'MM': + case 'DD': + case 'YY': + case 'GG': + case 'gg': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'ww': + case 'WW': + return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + case 'w': + case 'W': + case 'e': + case 'E': + return parseTokenOneOrTwoDigits; + case 'Do': + return strict ? config._locale._ordinalParse : config._locale._ordinalParseLenient; + default : + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i')); + return a; + } + } + + function utcOffsetFromString(string) { + string = string || ''; + var possibleTzMatches = (string.match(parseTokenTimezone) || []), + tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], + parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], + minutes = +(parts[1] * 60) + toInt(parts[2]); + + return parts[0] === '+' ? minutes : -minutes; + } + + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; + + switch (token) { + // QUARTER + case 'Q': + if (input != null) { + datePartArray[MONTH] = (toInt(input) - 1) * 3; + } + break; + // MONTH + case 'M' : // fall through to MM + case 'MM' : + if (input != null) { + datePartArray[MONTH] = toInt(input) - 1; + } + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + a = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (a != null) { + datePartArray[MONTH] = a; + } else { + config._pf.invalidMonth = input; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DD + case 'DD' : + if (input != null) { + datePartArray[DATE] = toInt(input); + } + break; + case 'Do' : + if (input != null) { + datePartArray[DATE] = toInt(parseInt( + input.match(/\d{1,2}/)[0], 10)); + } + break; + // DAY OF YEAR + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + config._dayOfYear = toInt(input); + } + + break; + // YEAR + case 'YY' : + datePartArray[YEAR] = moment.parseTwoDigitYear(input); + break; + case 'YYYY' : + case 'YYYYY' : + case 'YYYYYY' : + datePartArray[YEAR] = toInt(input); + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config._meridiem = input; + // config._isPm = config._locale.isPM(input); + break; + // HOUR + case 'h' : // fall through to hh + case 'hh' : + config._pf.bigHour = true; + /* falls through */ + case 'H' : // fall through to HH + case 'HH' : + datePartArray[HOUR] = toInt(input); + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[MINUTE] = toInt(input); + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[SECOND] = toInt(input); + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + case 'SSSS' : + datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); + break; + // UNIX OFFSET (MILLISECONDS) + case 'x': + config._d = new Date(toInt(input)); + break; + // UNIX TIMESTAMP WITH MS + case 'X': + config._d = new Date(parseFloat(input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config._useUTC = true; + config._tzm = utcOffsetFromString(input); + break; + // WEEKDAY - human + case 'dd': + case 'ddd': + case 'dddd': + a = config._locale.weekdaysParse(input); + // if we didn't get a weekday name, mark the date as invalid + if (a != null) { + config._w = config._w || {}; + config._w['d'] = a; + } else { + config._pf.invalidWeekday = input; + } + break; + // WEEK, WEEK DAY - numeric + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'd': + case 'e': + case 'E': + token = token.substr(0, 1); + /* falls through */ + case 'gggg': + case 'GGGG': + case 'GGGGG': + token = token.substr(0, 2); + if (input) { + config._w = config._w || {}; + config._w[token] = toInt(input); + } + break; + case 'gg': + case 'GG': + config._w = config._w || {}; + config._w[token] = moment.parseTwoDigitYear(input); + } + } + + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; + + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year); + week = dfl(w.W, 1); + weekday = dfl(w.E, 1); + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); + week = dfl(w.w, 1); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < dow) { + ++week; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + } else { + // default to begining of week + weekday = dow; + } + } + temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function dateFromConfig(config) { + var i, date, input = [], currentDate, yearToUse; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); + + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } + + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; + } + } + + function dateFromObject(config) { + var normalizedInput; + + if (config._d) { + return; + } + + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day || normalizedInput.date, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; + + dateFromConfig(config); + } + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } + + // date from string and format string + function makeDateFromStringAndFormat(config) { + if (config._f === moment.ISO_8601) { + parseISO(config); + return; + } + + config._a = []; + config._pf.empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + config._pf.unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } + + // clear _12h flag if hour is <= 12 + if (config._pf.bigHour === true && config._a[HOUR] <= 12) { + config._pf.bigHour = undefined; + } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], + config._meridiem); + dateFromConfig(config); + checkOverflow(config); + } + + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, + + scoreToBeat, + i, + currentScore; + + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._pf = defaultParsingFlags(); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); + + if (!isValid(tempConfig)) { + continue; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; + + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; + + tempConfig._pf.score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + + extend(config, bestMoment || tempConfig); + } + + // date from iso format + function parseISO(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); + + if (match) { + config._pf.iso = true; + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(string)) { + // match[5] should be 'T' or undefined + config._f = isoDates[i][0] + (match[6] || ' '); + break; + } + } + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (string.match(parseTokenTimezone)) { + config._f += 'Z'; + } + makeDateFromStringAndFormat(config); + } else { + config._isValid = false; + } + } + + // date from iso format or fallback + function makeDateFromString(config) { + parseISO(config); + if (config._isValid === false) { + delete config._isValid; + moment.createFromInputFallback(config); + } + } + + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } + + function makeDateFromInput(config) { + var input = config._i, matched; + if (input === undefined) { + config._d = new Date(); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if ((matched = aspNetJsonRegex.exec(input)) !== null) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + dateFromConfig(config); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + moment.createFromInputFallback(config); + } + } + + function makeDate(y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } + + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } + + function parseWeekday(input, locale) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = locale.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } + + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime(posNegDuration, withoutSuffix, locale) { + var duration = moment.duration(posNegDuration).abs(), + seconds = round(duration.as('s')), + minutes = round(duration.as('m')), + hours = round(duration.as('h')), + days = round(duration.as('d')), + months = round(duration.as('M')), + years = round(duration.as('y')), + + args = seconds < relativeTimeThresholds.s && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < relativeTimeThresholds.m && ['mm', minutes] || + hours === 1 && ['h'] || + hours < relativeTimeThresholds.h && ['hh', hours] || + days === 1 && ['d'] || + days < relativeTimeThresholds.d && ['dd', days] || + months === 1 && ['M'] || + months < relativeTimeThresholds.M && ['MM', months] || + years === 1 && ['y'] || ['yy', years]; + + args[2] = withoutSuffix; + args[3] = +posNegDuration > 0; + args[4] = locale; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Week of Year + ************************************/ + + + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), + adjustedMoment; + + + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } + + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } + + adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } + + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; + + d = d === 0 ? 7 : d; + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } + + /************************************ + Top Level Functions + ************************************/ + + function makeMoment(config) { + var input = config._i, + format = config._f, + res; + + config._locale = config._locale || moment.localeData(config._l); + + if (input === null || (format === undefined && input === '')) { + return moment.invalid({nullInput: true}); + } + + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } + + if (moment.isMoment(input)) { + return new Moment(input, true); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } + + res = new Moment(config); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } + + return res; + } + + moment = function (input, format, locale, strict) { + var c; + + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = locale; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); + + return makeMoment(c); + }; + + moment.suppressDeprecationWarnings = false; + + moment.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); + + // Pick a moment m from moments so that m[fn](other) is true for all + // other. This relies on the function fn to be transitive. + // + // moments should either be an array of moment objects or an array, whose + // first element is an array of moment objects. + function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; + } + if (!moments.length) { + return moment(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (moments[i][fn](res)) { + res = moments[i]; + } + } + return res; + } + + moment.min = function () { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); + }; + + moment.max = function () { + var args = [].slice.call(arguments, 0); + + return pickBy('isAfter', args); + }; + + // creating with utc + moment.utc = function (input, format, locale, strict) { + var c; + + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._useUTC = true; + c._isUTC = true; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); + + return makeMoment(c).utc(); + }; + + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso, + diffRes; + + if (moment.isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months + }; + } else if (typeof input === 'number') { + duration = {}; + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = isoDurationRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + parseIso = function (inp) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + }; + duration = { + y: parseIso(match[2]), + M: parseIso(match[3]), + d: parseIso(match[4]), + h: parseIso(match[5]), + m: parseIso(match[6]), + s: parseIso(match[7]), + w: parseIso(match[8]) + }; + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && + ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(moment(duration.from), moment(duration.to)); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); + + if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } + + return ret; + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // constant that refers to the ISO standard + moment.ISO_8601 = function () {}; + + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + moment.momentProperties = momentProperties; + + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + moment.updateOffset = function () {}; + + // This function allows you to set a threshold for relative time strings + moment.relativeTimeThreshold = function (threshold, limit) { + if (relativeTimeThresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return relativeTimeThresholds[threshold]; + } + relativeTimeThresholds[threshold] = limit; + return true; + }; + + moment.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + function (key, value) { + return moment.locale(key, value); + } + ); + + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + moment.locale = function (key, values) { + var data; + if (key) { + if (typeof(values) !== 'undefined') { + data = moment.defineLocale(key, values); + } + else { + data = moment.localeData(key); + } + + if (data) { + moment.duration._locale = moment._locale = data; + } + } + + return moment._locale._abbr; + }; + + moment.defineLocale = function (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); + + // backwards compat for now: also set the locale + moment.locale(name); + + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } + }; + + moment.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + function (key) { + return moment.localeData(key); + } + ); + + // returns locale data + moment.localeData = function (key) { + var locale; + + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return moment._locale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); + }; + + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && hasOwnProp(obj, '_isAMomentObject')); + }; + + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } + + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; + + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } + + return m; + }; + + moment.parseZone = function () { + return moment.apply(null, arguments).parseZone(); + }; + + moment.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; + + moment.isDate = isDate; + + /************************************ + Moment Prototype + ************************************/ + + + extend(moment.fn = Moment.prototype, { + + clone : function () { + return moment(this); + }, + + valueOf : function () { + return +this._d - ((this._offset || 0) * 60000); + }, + + unix : function () { + return Math.floor(+this / 1000); + }, + + toString : function () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + }, + + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, + + toISOString : function () { + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + if ('function' === typeof Date.prototype.toISOString) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, + + isValid : function () { + return isValid(this); + }, + + isDSTShifted : function () { + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } + + return false; + }, + + parsingFlags : function () { + return extend({}, this._pf); + }, + + invalidAt: function () { + return this._pf.overflow; + }, + + utc : function (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + }, + + local : function (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(this._dateUtcOffset(), 'm'); + } + } + return this; + }, + + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.localeData().postformat(output); + }, + + add : createAdder(1, 'add'), + + subtract : createAdder(-1, 'subtract'), + + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, + anchor, diff, output, daysAdjust; + + units = normalizeUnits(units); + + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + diff = this - that; + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + diff; + } + return asFloat ? output : absRound(output); + }, + + from : function (time, withoutSuffix) { + return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + }, + + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function (time) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're locat/utc/offset + // or not. + var now = time || moment(), + sod = makeAs(now, this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.localeData().calendar(format, this, moment(now))); + }, + + isLeapYear : function () { + return isLeapYear(this.year()); + }, + + isDST : function () { + return (this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + }, + + month : makeAccessor('Month', true), + + startOf : function (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } + + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } + + return this; + }, + + endOf: function (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + }, + + isAfter: function (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this > +input; + } else { + inputMs = moment.isMoment(input) ? +input : +moment(input); + return inputMs < +this.clone().startOf(units); + } + }, + + isBefore: function (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this < +input; + } else { + inputMs = moment.isMoment(input) ? +input : +moment(input); + return +this.clone().endOf(units) < inputMs; + } + }, + + isBetween: function (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + }, + + isSame: function (input, units) { + var inputMs; + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this === +input; + } else { + inputMs = +moment(input); + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + }, + + min: deprecate( + 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + function (other) { + other = moment.apply(null, arguments); + return other < this ? this : other; + } + ), + + max: deprecate( + 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function (other) { + other = moment.apply(null, arguments); + return other > this ? this : other; + } + ), + + zone : deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. ' + + 'https://github.com/moment/moment/issues/1779', + function (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } + } + ), + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + utcOffset : function (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = utcOffsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = this._dateUtcOffset(); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addOrSubtractDurationFromMoment(this, + moment.duration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + moment.updateOffset(this, true); + this._changeInProgress = null; + } + } + + return this; + } else { + return this._isUTC ? offset : this._dateUtcOffset(); + } + }, + + isLocal : function () { + return !this._isUTC; + }, + + isUtcOffset : function () { + return this._isUTC; + }, + + isUtc : function () { + return this._isUTC && this._offset === 0; + }, + + zoneAbbr : function () { + return this._isUTC ? 'UTC' : ''; + }, + + zoneName : function () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + }, + + parseZone : function () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(utcOffsetFromString(this._i)); + } + return this; + }, + + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).utcOffset(); + } + + return (this.utcOffset() - input) % 60 === 0; + }, + + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, + + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); + }, + + quarter : function (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + }, + + weekYear : function (input) { + var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; + return input == null ? year : this.add((input - year), 'y'); + }, + + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); + }, + + week : function (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + }, + + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + }, + + weekday : function (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + }, + + isoWeekday : function (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + }, + + isoWeeksInYear : function () { + return weeksInYear(this.year(), 1, 4); + }, + + weeksInYear : function () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, + + set : function (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } + else { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + } + return this; + }, + + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + locale : function (key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = moment.localeData(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + }, + + lang : deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ), + + localeData : function () { + return this._locale; + }, + + _dateUtcOffset : function () { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(this._d.getTimezoneOffset() / 15) * 15; + } + + }); + + function rawMonthSetter(mom, value) { + var dayOfMonth; + + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } + + dayOfMonth = Math.min(mom.date(), + daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } + + function rawGetter(mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } + + function rawSetter(mom, unit, value) { + if (unit === 'Month') { + return rawMonthSetter(mom, value); + } else { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } + + function makeAccessor(unit, keepTime) { + return function (value) { + if (value != null) { + rawSetter(this, unit, value); + moment.updateOffset(this, keepTime); + return this; + } else { + return rawGetter(this, unit); + } + }; + } + + moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false); + moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false); + moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false); + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true); + // moment.fn.month is defined separately + moment.fn.date = makeAccessor('Date', true); + moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true)); + moment.fn.year = makeAccessor('FullYear', true); + moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true)); + + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.months = moment.fn.month; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + moment.fn.quarters = moment.fn.quarter; + + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; + + // alias isUtc for dev-friendliness + moment.fn.isUTC = moment.fn.isUtc; + + /************************************ + Duration Prototype + ************************************/ + + + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; + } + + function yearsToDays (years) { + // years * 365 + absRound(years / 4) - + // absRound(years / 100) + absRound(years / 400); + return years * 146097 / 400; + } + + extend(moment.duration.fn = Duration.prototype, { + + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years = 0; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; + + hours = absRound(minutes / 60); + data.hours = hours % 24; + + days += absRound(hours / 24); + + // Accurately convert days to years, assume start from year 0. + years = absRound(daysToYears(days)); + days -= absRound(yearsToDays(years)); + + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absRound(days / 30); + days %= 30; + + // 12 months -> 1 year + years += absRound(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + }, + + abs : function () { + this._milliseconds = Math.abs(this._milliseconds); + this._days = Math.abs(this._days); + this._months = Math.abs(this._months); + + this._data.milliseconds = Math.abs(this._data.milliseconds); + this._data.seconds = Math.abs(this._data.seconds); + this._data.minutes = Math.abs(this._data.minutes); + this._data.hours = Math.abs(this._data.hours); + this._data.months = Math.abs(this._data.months); + this._data.years = Math.abs(this._data.years); + + return this; + }, + + weeks : function () { + return absRound(this.days() / 7); + }, + + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, + + humanize : function (withSuffix) { + var output = relativeTime(this, !withSuffix, this.localeData()); + + if (withSuffix) { + output = this.localeData().pastFuture(+this, output); + } + + return this.localeData().postformat(output); + }, + + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); + + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; + + this._bubble(); + + return this; + }, + + subtract : function (input, val) { + var dur = moment.duration(input, val); + + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; + + this._bubble(); + + return this; + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, + + as : function (units) { + var days, months; + units = normalizeUnits(units); + + if (units === 'month' || units === 'year') { + days = this._days + this._milliseconds / 864e5; + months = this._months + daysToYears(days) * 12; + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(yearsToDays(this._months / 12)); + switch (units) { + case 'week': return days / 7 + this._milliseconds / 6048e5; + case 'day': return days + this._milliseconds / 864e5; + case 'hour': return days * 24 + this._milliseconds / 36e5; + case 'minute': return days * 24 * 60 + this._milliseconds / 6e4; + case 'second': return days * 24 * 60 * 60 + this._milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + this._milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + }, + + lang : moment.fn.lang, + locale : moment.fn.locale, + + toIsoString : deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead ' + + '(notice the capitals)', + function () { + return this.toISOString(); + } + ), + + toISOString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); + + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (this.asSeconds() < 0 ? '-' : '') + + 'P' + + (years ? years + 'Y' : '') + + (months ? months + 'M' : '') + + (days ? days + 'D' : '') + + ((hours || minutes || seconds) ? 'T' : '') + + (hours ? hours + 'H' : '') + + (minutes ? minutes + 'M' : '') + + (seconds ? seconds + 'S' : ''); + }, + + localeData : function () { + return this._locale; + }, + + toJSON : function () { + return this.toISOString(); + } + }); + + moment.duration.fn.toString = moment.duration.fn.toISOString; + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + for (i in unitMillisecondFactors) { + if (hasOwnProp(unitMillisecondFactors, i)) { + makeDurationGetter(i.toLowerCase()); + } + } + + moment.duration.fn.asMilliseconds = function () { + return this.as('ms'); + }; + moment.duration.fn.asSeconds = function () { + return this.as('s'); + }; + moment.duration.fn.asMinutes = function () { + return this.as('m'); + }; + moment.duration.fn.asHours = function () { + return this.as('h'); + }; + moment.duration.fn.asDays = function () { + return this.as('d'); + }; + moment.duration.fn.asWeeks = function () { + return this.as('weeks'); + }; + moment.duration.fn.asMonths = function () { + return this.as('M'); + }; + moment.duration.fn.asYears = function () { + return this.as('y'); + }; + + /************************************ + Default Locale + ************************************/ + + + // Set default locale, other locale will inherit from English. + moment.locale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); + + /* EMBED_LOCALES */ + + /************************************ + Exposing Moment + ************************************/ + + function makeGlobal(shouldDeprecate) { + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + oldGlobalMoment = globalScope.moment; + if (shouldDeprecate) { + globalScope.moment = deprecate( + 'Accessing Moment through the global scope is ' + + 'deprecated, and will be removed in an upcoming ' + + 'release.', + moment); + } else { + globalScope.moment = moment; + } + } + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + } else if (typeof define === 'function' && define.amd) { + define(function (require, exports, module) { + if (module.config && module.config() && module.config().noGlobal === true) { + // release the global variable + globalScope.moment = oldGlobalMoment; + } + + return moment; + }); + makeGlobal(true); + } else { + makeGlobal(); + } +}).call(this); diff --git a/src/js/vendor/png.js b/src/js/vendor/png.js new file mode 100644 index 00000000..e8f22bbd --- /dev/null +++ b/src/js/vendor/png.js @@ -0,0 +1,464 @@ +// Generated by CoffeeScript 1.4.0 + +/* +# MIT LICENSE +# Copyright (c) 2011 Devon Govett +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this +# software and associated documentation files (the "Software"), to deal in the Software +# without restriction, including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +# to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +var Zlib; +if (typeof require !== 'undefined') { + Zlib = require('zlib'); +} else { + Zlib = window.Zlib; +} + +(function() { + var PNG; + + PNG = (function() { + var APNG_BLEND_OP_OVER, APNG_BLEND_OP_SOURCE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_PREVIOUS, makeImage, scratchCanvas, scratchCtx; + + PNG.load = function(url, canvas, callback) { + var xhr, + _this = this; + if (typeof canvas === 'function') { + callback = canvas; + } + xhr = new XMLHttpRequest; + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + var data, png; + data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer); + png = new PNG(data); + if (typeof (canvas != null ? canvas.getContext : void 0) === 'function') { + png.render(canvas); + } + return typeof callback === "function" ? callback(png) : void 0; + }; + return xhr.send(null); + }; + + APNG_DISPOSE_OP_NONE = 0; + + APNG_DISPOSE_OP_BACKGROUND = 1; + + APNG_DISPOSE_OP_PREVIOUS = 2; + + APNG_BLEND_OP_SOURCE = 0; + + APNG_BLEND_OP_OVER = 1; + + function PNG(data) { + var chunkSize, colors, delayDen, delayNum, frame, i, index, key, section, short, text, _i, _j, _ref; + this.data = data; + this.pos = 8; + this.palette = []; + this.imgData = []; + this.transparency = {}; + this.animation = null; + this.text = {}; + frame = null; + while (true) { + chunkSize = this.readUInt32(); + section = ((function() { + var _i, _results; + _results = []; + for (i = _i = 0; _i < 4; i = ++_i) { + _results.push(String.fromCharCode(this.data[this.pos++])); + } + return _results; + }).call(this)).join(''); + switch (section) { + case 'IHDR': + this.width = this.readUInt32(); + this.height = this.readUInt32(); + this.bits = this.data[this.pos++]; + this.colorType = this.data[this.pos++]; + this.compressionMethod = this.data[this.pos++]; + this.filterMethod = this.data[this.pos++]; + this.interlaceMethod = this.data[this.pos++]; + break; + case 'acTL': + this.animation = { + numFrames: this.readUInt32(), + numPlays: this.readUInt32() || Infinity, + frames: [] + }; + break; + case 'PLTE': + this.palette = this.read(chunkSize); + break; + case 'fcTL': + if (frame) { + this.animation.frames.push(frame); + } + this.pos += 4; + frame = { + width: this.readUInt32(), + height: this.readUInt32(), + xOffset: this.readUInt32(), + yOffset: this.readUInt32() + }; + delayNum = this.readUInt16(); + delayDen = this.readUInt16() || 100; + frame.delay = 1000 * delayNum / delayDen; + frame.disposeOp = this.data[this.pos++]; + frame.blendOp = this.data[this.pos++]; + frame.data = []; + break; + case 'IDAT': + case 'fdAT': + if (section === 'fdAT') { + this.pos += 4; + chunkSize -= 4; + } + data = (frame != null ? frame.data : void 0) || this.imgData; + for (i = _i = 0; 0 <= chunkSize ? _i < chunkSize : _i > chunkSize; i = 0 <= chunkSize ? ++_i : --_i) { + data.push(this.data[this.pos++]); + } + break; + case 'tRNS': + this.transparency = {}; + switch (this.colorType) { + case 3: + this.transparency.indexed = this.read(chunkSize); + short = 255 - this.transparency.indexed.length; + if (short > 0) { + for (i = _j = 0; 0 <= short ? _j < short : _j > short; i = 0 <= short ? ++_j : --_j) { + this.transparency.indexed.push(255); + } + } + break; + case 0: + this.transparency.grayscale = this.read(chunkSize)[0]; + break; + case 2: + this.transparency.rgb = this.read(chunkSize); + } + break; + case 'tEXt': + text = this.read(chunkSize); + index = text.indexOf(0); + key = String.fromCharCode.apply(String, text.slice(0, index)); + this.text[key] = String.fromCharCode.apply(String, text.slice(index + 1)); + break; + case 'IEND': + if (frame) { + this.animation.frames.push(frame); + } + this.colors = (function() { + switch (this.colorType) { + case 0: + case 3: + case 4: + return 1; + case 2: + case 6: + return 3; + } + }).call(this); + this.hasAlphaChannel = (_ref = this.colorType) === 4 || _ref === 6; + colors = this.colors + (this.hasAlphaChannel ? 1 : 0); + this.pixelBitlength = this.bits * colors; + this.colorSpace = (function() { + switch (this.colors) { + case 1: + return 'DeviceGray'; + case 3: + return 'DeviceRGB'; + } + }).call(this); + this.imgData = new Uint8Array(this.imgData); + return; + default: + this.pos += chunkSize; + } + this.pos += 4; + if (this.pos > this.data.length) { + throw new Error("Incomplete or corrupt PNG file"); + } + } + return; + } + + PNG.prototype.read = function(bytes) { + var i, _i, _results; + _results = []; + for (i = _i = 0; 0 <= bytes ? _i < bytes : _i > bytes; i = 0 <= bytes ? ++_i : --_i) { + _results.push(this.data[this.pos++]); + } + return _results; + }; + + PNG.prototype.readUInt32 = function() { + var b1, b2, b3, b4; + b1 = this.data[this.pos++] << 24; + b2 = this.data[this.pos++] << 16; + b3 = this.data[this.pos++] << 8; + b4 = this.data[this.pos++]; + return b1 | b2 | b3 | b4; + }; + + PNG.prototype.readUInt16 = function() { + var b1, b2; + b1 = this.data[this.pos++] << 8; + b2 = this.data[this.pos++]; + return b1 | b2; + }; + + PNG.prototype.decodePixels = function(data) { + var byte, c, col, i, left, length, p, pa, paeth, pb, pc, pixelBytes, pixels, pos, row, scanlineLength, upper, upperLeft, _i, _j, _k, _l, _m; + if (data == null) { + data = this.imgData; + } + if (data.length === 0) { + return new Uint8Array(0); + } + data = new Zlib.Inflate(data); + data = data.decompress(); + pixelBytes = this.pixelBitlength / 8; + scanlineLength = pixelBytes * this.width; + pixels = new Uint8Array(scanlineLength * this.height); + length = data.length; + row = 0; + pos = 0; + c = 0; + while (pos < length) { + switch (data[pos++]) { + case 0: + for (i = _i = 0; _i < scanlineLength; i = _i += 1) { + pixels[c++] = data[pos++]; + } + break; + case 1: + for (i = _j = 0; _j < scanlineLength; i = _j += 1) { + byte = data[pos++]; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + pixels[c++] = (byte + left) % 256; + } + break; + case 2: + for (i = _k = 0; _k < scanlineLength; i = _k += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + pixels[c++] = (upper + byte) % 256; + } + break; + case 3: + for (i = _l = 0; _l < scanlineLength; i = _l += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + pixels[c++] = (byte + Math.floor((left + upper) / 2)) % 256; + } + break; + case 4: + for (i = _m = 0; _m < scanlineLength; i = _m += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + if (row === 0) { + upper = upperLeft = 0; + } else { + upper = pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + upperLeft = col && pixels[(row - 1) * scanlineLength + (col - 1) * pixelBytes + (i % pixelBytes)]; + } + p = left + upper - upperLeft; + pa = Math.abs(p - left); + pb = Math.abs(p - upper); + pc = Math.abs(p - upperLeft); + if (pa <= pb && pa <= pc) { + paeth = left; + } else if (pb <= pc) { + paeth = upper; + } else { + paeth = upperLeft; + } + pixels[c++] = (byte + paeth) % 256; + } + break; + default: + throw new Error("Invalid filter algorithm: " + data[pos - 1]); + } + row++; + } + return pixels; + }; + + PNG.prototype.decodePalette = function() { + var c, i, length, palette, pos, ret, transparency, _i, _ref, _ref1; + palette = this.palette; + transparency = this.transparency.indexed || []; + ret = new Uint8Array((transparency.length || 0) + palette.length); + pos = 0; + length = palette.length; + c = 0; + for (i = _i = 0, _ref = palette.length; _i < _ref; i = _i += 3) { + ret[pos++] = palette[i]; + ret[pos++] = palette[i + 1]; + ret[pos++] = palette[i + 2]; + ret[pos++] = (_ref1 = transparency[c++]) != null ? _ref1 : 255; + } + return ret; + }; + + PNG.prototype.copyToImageData = function(imageData, pixels) { + var alpha, colors, data, i, input, j, k, length, palette, v, _ref; + colors = this.colors; + palette = null; + alpha = this.hasAlphaChannel; + if (this.palette.length) { + palette = (_ref = this._decodedPalette) != null ? _ref : this._decodedPalette = this.decodePalette(); + colors = 4; + alpha = true; + } + data = imageData.data || imageData; + length = data.length; + input = palette || pixels; + i = j = 0; + if (colors === 1) { + while (i < length) { + k = palette ? pixels[i / 4] * 4 : j; + v = input[k++]; + data[i++] = v; + data[i++] = v; + data[i++] = v; + data[i++] = alpha ? input[k++] : 255; + j = k; + } + } else { + while (i < length) { + k = palette ? pixels[i / 4] * 4 : j; + data[i++] = input[k++]; + data[i++] = input[k++]; + data[i++] = input[k++]; + data[i++] = alpha ? input[k++] : 255; + j = k; + } + } + }; + + PNG.prototype.decode = function() { + var ret; + ret = new Uint8Array(this.width * this.height * 4); + this.copyToImageData(ret, this.decodePixels()); + return ret; + }; + + makeImage = function(imageData) { + var img; + scratchCtx.width = imageData.width; + scratchCtx.height = imageData.height; + scratchCtx.clearRect(0, 0, imageData.width, imageData.height); + scratchCtx.putImageData(imageData, 0, 0); + img = new Image; + img.src = scratchCanvas.toDataURL(); + return img; + }; + + PNG.prototype.decodeFrames = function(ctx) { + var frame, i, imageData, pixels, _i, _len, _ref, _results; + if (!this.animation) { + return; + } + _ref = this.animation.frames; + _results = []; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + frame = _ref[i]; + imageData = ctx.createImageData(frame.width, frame.height); + pixels = this.decodePixels(new Uint8Array(frame.data)); + this.copyToImageData(imageData, pixels); + frame.imageData = imageData; + _results.push(frame.image = makeImage(imageData)); + } + return _results; + }; + + PNG.prototype.renderFrame = function(ctx, number) { + var frame, frames, prev; + frames = this.animation.frames; + frame = frames[number]; + prev = frames[number - 1]; + if (number === 0) { + ctx.clearRect(0, 0, this.width, this.height); + } + if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_BACKGROUND) { + ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height); + } else if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_PREVIOUS) { + ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset); + } + if (frame.blendOp === APNG_BLEND_OP_SOURCE) { + ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height); + } + return ctx.drawImage(frame.image, frame.xOffset, frame.yOffset); + }; + + PNG.prototype.animate = function(ctx) { + var doFrame, frameNumber, frames, numFrames, numPlays, _ref, + _this = this; + frameNumber = 0; + _ref = this.animation, numFrames = _ref.numFrames, frames = _ref.frames, numPlays = _ref.numPlays; + return (doFrame = function() { + var f, frame; + f = frameNumber++ % numFrames; + frame = frames[f]; + _this.renderFrame(ctx, f); + if (numFrames > 1 && frameNumber / numFrames < numPlays) { + return _this.animation._timeout = setTimeout(doFrame, frame.delay); + } + })(); + }; + + PNG.prototype.stopAnimation = function() { + var _ref; + return clearTimeout((_ref = this.animation) != null ? _ref._timeout : void 0); + }; + + PNG.prototype.render = function(canvas) { + var ctx, data; + if (canvas._png) { + canvas._png.stopAnimation(); + } + canvas._png = this; + canvas.width = this.width; + canvas.height = this.height; + ctx = canvas.getContext("2d"); + if (this.animation) { + this.decodeFrames(ctx); + return this.animate(ctx); + } else { + data = ctx.createImageData(this.width, this.height); + this.copyToImageData(data, this.decodePixels()); + return ctx.putImageData(data, 0, 0); + } + }; + + return PNG; + + })(); + + if (typeof module !== 'undefined') { + module.exports = PNG; + } else { + window.PNG = PNG; + } + +}).call(this); diff --git a/src/js/vendor/zlib.js b/src/js/vendor/zlib.js new file mode 100644 index 00000000..83683fea --- /dev/null +++ b/src/js/vendor/zlib.js @@ -0,0 +1,49 @@ +/** + * zlib.js Deflate + Inflate + * + * @link https://github.com/imaya/zlib.js + * @author imaya + * @license MIT + **/ +(function() {'use strict';function l(d){throw d;}var v=void 0,x=!0,aa=this;function D(d,a){var c=d.split("."),e=aa;!(c[0]in e)&&e.execScript&&e.execScript("var "+c[0]);for(var b;c.length&&(b=c.shift());)!c.length&&a!==v?e[b]=a:e=e[b]?e[b]:e[b]={}};var F="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function H(d,a){this.index="number"===typeof a?a:0;this.i=0;this.buffer=d instanceof(F?Uint8Array:Array)?d:new (F?Uint8Array:Array)(32768);2*this.buffer.length<=this.index&&l(Error("invalid index"));this.buffer.length<=this.index&&this.f()}H.prototype.f=function(){var d=this.buffer,a,c=d.length,e=new (F?Uint8Array:Array)(c<<1);if(F)e.set(d);else for(a=0;a>>8&255]<<16|N[d>>>16&255]<<8|N[d>>>24&255])>>32-a:N[d]>>8-a);if(8>a+f)g=g<>a-h-1&1,8===++f&&(f=0,e[b++]=N[g],g=0,b===e.length&&(e=this.f()));e[b]=g;this.buffer=e;this.i=f;this.index=b};H.prototype.finish=function(){var d=this.buffer,a=this.index,c;0O;++O){for(var P=O,Q=P,ga=7,P=P>>>1;P;P>>>=1)Q<<=1,Q|=P&1,--ga;fa[O]=(Q<>>0}var N=fa;function ha(d){this.buffer=new (F?Uint16Array:Array)(2*d);this.length=0}ha.prototype.getParent=function(d){return 2*((d-2)/4|0)};ha.prototype.push=function(d,a){var c,e,b=this.buffer,f;c=this.length;b[this.length++]=a;for(b[this.length++]=d;0b[e])f=b[c],b[c]=b[e],b[e]=f,f=b[c+1],b[c+1]=b[e+1],b[e+1]=f,c=e;else break;return this.length}; +ha.prototype.pop=function(){var d,a,c=this.buffer,e,b,f;a=c[0];d=c[1];this.length-=2;c[0]=c[this.length];c[1]=c[this.length+1];for(f=0;;){b=2*f+2;if(b>=this.length)break;b+2c[b]&&(b+=2);if(c[b]>c[f])e=c[f],c[f]=c[b],c[b]=e,e=c[f+1],c[f+1]=c[b+1],c[b+1]=e;else break;f=b}return{index:d,value:a,length:this.length}};function R(d){var a=d.length,c=0,e=Number.POSITIVE_INFINITY,b,f,g,h,k,n,q,r,p,m;for(r=0;rc&&(c=d[r]),d[r]>=1;m=g<<16|r;for(p=n;pS;S++)switch(x){case 143>=S:oa.push([S+48,8]);break;case 255>=S:oa.push([S-144+400,9]);break;case 279>=S:oa.push([S-256+0,7]);break;case 287>=S:oa.push([S-280+192,8]);break;default:l("invalid literal: "+S)} +ia.prototype.j=function(){var d,a,c,e,b=this.input;switch(this.h){case 0:c=0;for(e=b.length;c>>8&255;p[m++]=n&255;p[m++]=n>>>8&255;if(F)p.set(f,m),m+=f.length,p=p.subarray(0,m);else{q=0;for(r=f.length;qu)for(;0< +u--;)G[E++]=0,K[0]++;else for(;0u?u:138,B>u-3&&B=B?(G[E++]=17,G[E++]=B-3,K[17]++):(G[E++]=18,G[E++]=B-11,K[18]++),u-=B;else if(G[E++]=I[t],K[I[t]]++,u--,3>u)for(;0u?u:6,B>u-3&&Bz;z++)ra[z]=ka[gb[z]];for(W=19;4=b:return[265,b-11,1];case 14>=b:return[266,b-13,1];case 16>=b:return[267,b-15,1];case 18>=b:return[268,b-17,1];case 22>=b:return[269,b-19,2];case 26>=b:return[270,b-23,2];case 30>=b:return[271,b-27,2];case 34>=b:return[272, +b-31,2];case 42>=b:return[273,b-35,3];case 50>=b:return[274,b-43,3];case 58>=b:return[275,b-51,3];case 66>=b:return[276,b-59,3];case 82>=b:return[277,b-67,4];case 98>=b:return[278,b-83,4];case 114>=b:return[279,b-99,4];case 130>=b:return[280,b-115,4];case 162>=b:return[281,b-131,5];case 194>=b:return[282,b-163,5];case 226>=b:return[283,b-195,5];case 257>=b:return[284,b-227,5];case 258===b:return[285,b-258,0];default:l("invalid length: "+b)}}var a=[],c,e;for(c=3;258>=c;c++)e=d(c),a[c]=e[2]<<24|e[1]<< +16|e[0];return a}(),wa=F?new Uint32Array(va):va; +function pa(d,a){function c(b,c){var a=b.H,d=[],e=0,f;f=wa[b.length];d[e++]=f&65535;d[e++]=f>>16&255;d[e++]=f>>24;var g;switch(x){case 1===a:g=[0,a-1,0];break;case 2===a:g=[1,a-2,0];break;case 3===a:g=[2,a-3,0];break;case 4===a:g=[3,a-4,0];break;case 6>=a:g=[4,a-5,1];break;case 8>=a:g=[5,a-7,1];break;case 12>=a:g=[6,a-9,2];break;case 16>=a:g=[7,a-13,2];break;case 24>=a:g=[8,a-17,3];break;case 32>=a:g=[9,a-25,3];break;case 48>=a:g=[10,a-33,4];break;case 64>=a:g=[11,a-49,4];break;case 96>=a:g=[12,a- +65,5];break;case 128>=a:g=[13,a-97,5];break;case 192>=a:g=[14,a-129,6];break;case 256>=a:g=[15,a-193,6];break;case 384>=a:g=[16,a-257,7];break;case 512>=a:g=[17,a-385,7];break;case 768>=a:g=[18,a-513,8];break;case 1024>=a:g=[19,a-769,8];break;case 1536>=a:g=[20,a-1025,9];break;case 2048>=a:g=[21,a-1537,9];break;case 3072>=a:g=[22,a-2049,10];break;case 4096>=a:g=[23,a-3073,10];break;case 6144>=a:g=[24,a-4097,11];break;case 8192>=a:g=[25,a-6145,11];break;case 12288>=a:g=[26,a-8193,12];break;case 16384>= +a:g=[27,a-12289,12];break;case 24576>=a:g=[28,a-16385,13];break;case 32768>=a:g=[29,a-24577,13];break;default:l("invalid distance")}f=g;d[e++]=f[0];d[e++]=f[1];d[e++]=f[2];var h,k;h=0;for(k=d.length;h=f;)w[f++]=0;for(f=0;29>=f;)y[f++]=0}w[256]=1;e=0;for(b=a.length;e=b){r&&c(r,-1);f=0;for(g=b-e;fg&&a+gf&&(b=e,f=g);if(258===g)break}return new ta(f,a-b)} +function qa(d,a){var c=d.length,e=new ha(572),b=new (F?Uint8Array:Array)(c),f,g,h,k,n;if(!F)for(k=0;k2*b[m-1]+f[m]&&(b[m]=2*b[m-1]+f[m]),h[m]=Array(b[m]),k[m]=Array(b[m]);for(p=0;pd[p]?(h[m][s]=w,k[m][s]=a,y+=2):(h[m][s]=d[p],k[m][s]=p,++p);n[m]=0;1===f[m]&&e(m)}return g} +function sa(d){var a=new (F?Uint16Array:Array)(d.length),c=[],e=[],b=0,f,g,h,k;f=0;for(g=d.length;f>>=1}return a};function T(d,a){this.l=[];this.m=32768;this.e=this.g=this.c=this.q=0;this.input=F?new Uint8Array(d):d;this.s=!1;this.n=za;this.C=!1;if(a||!(a={}))a.index&&(this.c=a.index),a.bufferSize&&(this.m=a.bufferSize),a.bufferType&&(this.n=a.bufferType),a.resize&&(this.C=a.resize);switch(this.n){case Aa:this.b=32768;this.a=new (F?Uint8Array:Array)(32768+this.m+258);break;case za:this.b=0;this.a=new (F?Uint8Array:Array)(this.m);this.f=this.K;this.t=this.I;this.o=this.J;break;default:l(Error("invalid inflate mode"))}} +var Aa=0,za=1,Ba={F:Aa,D:za}; +T.prototype.p=function(){for(;!this.s;){var d=Y(this,3);d&1&&(this.s=x);d>>>=1;switch(d){case 0:var a=this.input,c=this.c,e=this.a,b=this.b,f=a.length,g=v,h=v,k=e.length,n=v;this.e=this.g=0;c+1>=f&&l(Error("invalid uncompressed block header: LEN"));g=a[c++]|a[c++]<<8;c+1>=f&&l(Error("invalid uncompressed block header: NLEN"));h=a[c++]|a[c++]<<8;g===~h&&l(Error("invalid uncompressed block header: length verify"));c+g>a.length&&l(Error("input buffer is broken"));switch(this.n){case Aa:for(;b+g>e.length;){n= +k-b;g-=n;if(F)e.set(a.subarray(c,c+n),b),b+=n,c+=n;else for(;n--;)e[b++]=a[c++];this.b=b;e=this.f();b=this.b}break;case za:for(;b+g>e.length;)e=this.f({v:2});break;default:l(Error("invalid inflate mode"))}if(F)e.set(a.subarray(c,c+g),b),b+=g,c+=g;else for(;g--;)e[b++]=a[c++];this.c=c;this.b=b;this.a=e;break;case 1:this.o(Ca,Da);break;case 2:Sa(this);break;default:l(Error("unknown BTYPE: "+d))}}return this.t()}; +var Ta=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],Ua=F?new Uint16Array(Ta):Ta,Va=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],Wa=F?new Uint16Array(Va):Va,Xa=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],Ya=F?new Uint8Array(Xa):Xa,Za=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],$a=F?new Uint16Array(Za):Za,ab=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10, +10,11,11,12,12,13,13],bb=F?new Uint8Array(ab):ab,cb=new (F?Uint8Array:Array)(288),Z,db;Z=0;for(db=cb.length;Z=Z?8:255>=Z?9:279>=Z?7:8;var Ca=R(cb),eb=new (F?Uint8Array:Array)(30),fb,hb;fb=0;for(hb=eb.length;fb=g&&l(Error("input buffer is broken")),c|=b[f++]<>>a;d.e=e-a;d.c=f;return h} +function ib(d,a){for(var c=d.g,e=d.e,b=d.input,f=d.c,g=b.length,h=a[0],k=a[1],n,q;e=g);)c|=b[f++]<>>16;d.g=c>>q;d.e=e-q;d.c=f;return n&65535} +function Sa(d){function a(a,b,c){var d,e=this.z,f,g;for(g=0;gf)e>=b&&(this.b=e,c=this.f(),e=this.b),c[e++]=f;else{g=f-257;k=Wa[g];0=b&&(this.b=e,c=this.f(),e=this.b);for(;k--;)c[e]=c[e++-h]}for(;8<=this.e;)this.e-=8,this.c--;this.b=e}; +T.prototype.J=function(d,a){var c=this.a,e=this.b;this.u=d;for(var b=c.length,f,g,h,k;256!==(f=ib(this,d));)if(256>f)e>=b&&(c=this.f(),b=c.length),c[e++]=f;else{g=f-257;k=Wa[g];0b&&(c=this.f(),b=c.length);for(;k--;)c[e]=c[e++-h]}for(;8<=this.e;)this.e-=8,this.c--;this.b=e}; +T.prototype.f=function(){var d=new (F?Uint8Array:Array)(this.b-32768),a=this.b-32768,c,e,b=this.a;if(F)d.set(b.subarray(32768,d.length));else{c=0;for(e=d.length;cc;++c)b[c]=b[a+c];this.b=32768;return b}; +T.prototype.K=function(d){var a,c=this.input.length/this.c+1|0,e,b,f,g=this.input,h=this.a;d&&("number"===typeof d.v&&(c=d.v),"number"===typeof d.G&&(c+=d.G));2>c?(e=(g.length-this.c)/this.u[2],f=258*(e/2)|0,b=fa&&(this.a.length=a),d=this.a);return this.buffer=d};function jb(d){if("string"===typeof d){var a=d.split(""),c,e;c=0;for(e=a.length;c>>0;d=a}for(var b=1,f=0,g=d.length,h,k=0;0>>0};function kb(d,a){var c,e;this.input=d;this.c=0;if(a||!(a={}))a.index&&(this.c=a.index),a.verify&&(this.N=a.verify);c=d[this.c++];e=d[this.c++];switch(c&15){case lb:this.method=lb;break;default:l(Error("unsupported compression method"))}0!==((c<<8)+e)%31&&l(Error("invalid fcheck flag:"+((c<<8)+e)%31));e&32&&l(Error("fdict flag is not supported"));this.B=new T(d,{index:this.c,bufferSize:a.bufferSize,bufferType:a.bufferType,resize:a.resize})} +kb.prototype.p=function(){var d=this.input,a,c;a=this.B.p();this.c=this.B.c;this.N&&(c=(d[this.c++]<<24|d[this.c++]<<16|d[this.c++]<<8|d[this.c++])>>>0,c!==jb(a)&&l(Error("invalid adler-32 checksum")));return a};var lb=8;function mb(d,a){this.input=d;this.a=new (F?Uint8Array:Array)(32768);this.h=$.k;var c={},e;if((a||!(a={}))&&"number"===typeof a.compressionType)this.h=a.compressionType;for(e in a)c[e]=a[e];c.outputBuffer=this.a;this.A=new ia(this.input,c)}var $=na; +mb.prototype.j=function(){var d,a,c,e,b,f,g,h=0;g=this.a;d=lb;switch(d){case lb:a=Math.LOG2E*Math.log(32768)-8;break;default:l(Error("invalid compression method"))}c=a<<4|d;g[h++]=c;switch(d){case lb:switch(this.h){case $.NONE:b=0;break;case $.r:b=1;break;case $.k:b=2;break;default:l(Error("unsupported compression type"))}break;default:l(Error("invalid compression method"))}e=b<<6|0;g[h++]=e|31-(256*c+e)%31;f=jb(this.input);this.A.b=h;g=this.A.j();h=g.length;F&&(g=new Uint8Array(g.buffer),g.length<= +h+4&&(this.a=new Uint8Array(g.length+4),this.a.set(g),g=this.a),g=g.subarray(0,h+4));g[h++]=f>>24&255;g[h++]=f>>16&255;g[h++]=f>>8&255;g[h++]=f&255;return g};function nb(d,a){var c,e,b,f;if(Object.keys)c=Object.keys(a);else for(e in c=[],b=0,a)c[b++]=e;b=0;for(f=c.length;b +#include "simply/simply.h" + +/** + * By default, we 'simply' load Simply and start running it. + */ +int main(void) { + Simply *simply = simply_init(); + app_event_loop(); + simply_deinit(simply); +} diff --git a/src/simply/simply.c b/src/simply/simply.c new file mode 100644 index 00000000..b0a21007 --- /dev/null +++ b/src/simply/simply.c @@ -0,0 +1,46 @@ +#include "simply.h" + +#include "simply_accel.h" +#include "simply_res.h" +#include "simply_splash.h" +#include "simply_stage.h" +#include "simply_menu.h" +#include "simply_msg.h" +#include "simply_ui.h" +#include "simply_window_stack.h" +#include "simply_wakeup.h" +#include "simply_voice.h" + +#include + +Simply *simply_init(void) { + Simply *simply = malloc(sizeof(*simply)); + simply->accel = simply_accel_create(simply); + simply->voice = simply_voice_create(simply); + simply->res = simply_res_create(simply); + simply->splash = simply_splash_create(simply); + simply->stage = simply_stage_create(simply); + simply->menu = simply_menu_create(simply); + simply->msg = simply_msg_create(simply); + simply->ui = simply_ui_create(simply); + simply->window_stack = simply_window_stack_create(simply); + + simply_wakeup_init(simply); + + bool animated = false; + window_stack_push(simply->splash->window, animated); + + return simply; +} + +void simply_deinit(Simply *simply) { + simply_window_stack_destroy(simply->window_stack); + simply_ui_destroy(simply->ui); + simply_msg_destroy(simply->msg); + simply_menu_destroy(simply->menu); + simply_stage_destroy(simply->stage); + simply_res_destroy(simply->res); + simply_accel_destroy(simply->accel); + simply_voice_destroy(simply->voice); + free(simply); +} diff --git a/src/simply/simply.h b/src/simply/simply.h new file mode 100644 index 00000000..ad47e955 --- /dev/null +++ b/src/simply/simply.h @@ -0,0 +1,25 @@ +#pragma once + +#define LOG(...) APP_LOG(APP_LOG_LEVEL_DEBUG, __VA_ARGS__) + +typedef struct Simply Simply; + +struct Simply { + struct SimplyAccel *accel; + struct SimplyVoice *voice; + struct SimplyRes *res; + struct SimplyMsg *msg; + struct SimplyWindowStack *window_stack; + struct SimplySplash *splash; + union { + struct { + struct SimplyStage *stage; + struct SimplyMenu *menu; + struct SimplyUi *ui; + }; + struct SimplyWindow *windows[0]; + }; +}; + +Simply *simply_init(); +void simply_deinit(Simply *); diff --git a/src/simply/simply_accel.c b/src/simply/simply_accel.c new file mode 100644 index 00000000..1fd40f1c --- /dev/null +++ b/src/simply/simply_accel.c @@ -0,0 +1,152 @@ +#include "simply_accel.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include + +typedef Packet AccelPeekPacket; + +typedef struct AccelConfigPacket AccelConfigPacket; + +struct __attribute__((__packed__)) AccelConfigPacket { + Packet packet; + uint16_t num_samples; + AccelSamplingRate rate:8; + bool data_subscribed; +}; + +typedef struct AccelTapPacket AccelTapPacket; + +struct __attribute__((__packed__)) AccelTapPacket { + Packet packet; + AccelAxisType axis:8; + int8_t direction; +}; + +typedef struct AccelDataPacket AccelDataPacket; + +struct __attribute__((__packed__)) AccelDataPacket { + Packet packet; + bool is_peek; + uint8_t num_samples; + AccelData data[]; +}; + +static SimplyAccel *s_accel = NULL; + +static bool send_accel_tap(AccelAxisType axis, int32_t direction) { + AccelTapPacket packet = { + .packet.type = CommandAccelTap, + .packet.length = sizeof(packet), + .axis = axis, + .direction = direction, + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool send_accel_data(SimplyMsg *self, AccelData *data, uint32_t num_samples, bool is_peek) { + size_t data_length = sizeof(AccelData) * num_samples; + size_t length = sizeof(AccelDataPacket) + data_length; + AccelDataPacket *packet = malloc(length); + if (!packet) { + return false; + } + packet->packet = (Packet) { + .type = CommandAccelData, + .length = length, + }; + packet->is_peek = is_peek; + packet->num_samples = num_samples; + memcpy(packet->data, data, data_length); + bool result = simply_msg_send((uint8_t*) packet, length); + free(packet); + return result; +} + +static void handle_accel_data(AccelData *data, uint32_t num_samples) { + send_accel_data(s_accel->simply->msg, data, num_samples, false); +} + +static void set_data_subscribe(SimplyAccel *self, bool subscribe) { + if (self->data_subscribed == subscribe) { + return; + } + if (subscribe) { + accel_data_service_subscribe(self->num_samples, handle_accel_data); + accel_service_set_sampling_rate(self->rate); + } else { + accel_data_service_unsubscribe(); + } + self->data_subscribed = subscribe; +} + +static void handle_accel_tap(AccelAxisType axis, int32_t direction) { + send_accel_tap(axis, direction); +} + +static void accel_peek_timer_callback(void *context) { + Simply *simply = context; + AccelData data = { .x = 0 }; + if (s_accel->data_subscribed) { + accel_service_peek(&data); + } + if (!send_accel_data(simply->msg, &data, 1, true)) { + app_timer_register(10, accel_peek_timer_callback, simply); + } +} + +static void handle_accel_peek_packet(Simply *simply, Packet *data) { + app_timer_register(10, accel_peek_timer_callback, simply); +} + +static void handle_accel_config_packet(Simply *simply, Packet *data) { + AccelConfigPacket *packet = (AccelConfigPacket*) data; + s_accel->num_samples = packet->num_samples; + s_accel->rate = packet->rate; + set_data_subscribe(simply->accel, packet->data_subscribed); +} + +bool simply_accel_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandAccelPeek: + handle_accel_peek_packet(simply, packet); + return true; + case CommandAccelConfig: + handle_accel_config_packet(simply, packet); + return true; + } + return false; +} + +SimplyAccel *simply_accel_create(Simply *simply) { + if (s_accel) { + return s_accel; + } + + SimplyAccel *self = malloc(sizeof(*self)); + *self = (SimplyAccel) { + .simply = simply, + .rate = ACCEL_SAMPLING_100HZ, + .num_samples = 25, + }; + s_accel = self; + + accel_tap_service_subscribe(handle_accel_tap); + + return self; +} + +void simply_accel_destroy(SimplyAccel *self) { + if (!self) { + return; + } + + accel_tap_service_unsubscribe(); + + free(self); + + s_accel = NULL; +} + diff --git a/src/simply/simply_accel.h b/src/simply/simply_accel.h new file mode 100644 index 00000000..3ebc1520 --- /dev/null +++ b/src/simply/simply_accel.h @@ -0,0 +1,21 @@ +#pragma once + +#include "simply_msg.h" + +#include "simply.h" + +#include + +typedef struct SimplyAccel SimplyAccel; + +struct SimplyAccel { + Simply *simply; + uint16_t num_samples; + AccelSamplingRate rate:8; + bool data_subscribed; +}; + +SimplyAccel *simply_accel_create(Simply *simply); +void simply_accel_destroy(SimplyAccel *self); + +bool simply_accel_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_menu.c b/src/simply/simply_menu.c new file mode 100644 index 00000000..2dcd012e --- /dev/null +++ b/src/simply/simply_menu.c @@ -0,0 +1,687 @@ +#include "simply_menu.h" + +#include "simply_res.h" +#include "simply_msg.h" +#include "simply_window_stack.h" + +#include "simply.h" + +#include "util/color.h" +#include "util/display.h" +#include "util/graphics.h" +#include "util/graphics_text.h" +#include "util/menu_layer.h" +#include "util/noop.h" +#include "util/platform.h" +#include "util/string.h" + +#include + +#define MAX_CACHED_SECTIONS 10 + +#define MAX_CACHED_ITEMS IF_APLITE_ELSE(6, 51) + +#define EMPTY_TITLE "" + +#define SPINNER_MS 66 + +typedef Packet MenuClearPacket; + +typedef struct MenuClearSectionPacket MenuClearSectionPacket; + +struct __attribute__((__packed__)) MenuClearSectionPacket { + Packet packet; + uint16_t section; +}; + +typedef struct MenuPropsPacket MenuPropsPacket; + +struct __attribute__((__packed__)) MenuPropsPacket { + Packet packet; + uint16_t num_sections; + GColor8 background_color; + GColor8 text_color; + GColor8 highlight_background_color; + GColor8 highlight_text_color; +}; + +typedef struct MenuSectionPacket MenuSectionPacket; + +struct __attribute__((__packed__)) MenuSectionPacket { + Packet packet; + uint16_t section; + uint16_t num_items; + GColor8 background_color; + GColor8 text_color; + uint16_t title_length; + char title[]; +}; + +typedef struct MenuItemPacket MenuItemPacket; + +struct __attribute__((__packed__)) MenuItemPacket { + Packet packet; + uint16_t section; + uint16_t item; + uint32_t icon; + uint16_t title_length; + uint16_t subtitle_length; + char buffer[]; +}; + +typedef struct MenuItemEventPacket MenuItemEventPacket; + +struct __attribute__((__packed__)) MenuItemEventPacket { + Packet packet; + uint16_t section; + uint16_t item; +}; + +typedef Packet MenuGetSelectionPacket; + +typedef struct MenuSelectionPacket MenuSelectionPacket; + +struct __attribute__((__packed__)) MenuSelectionPacket { + Packet packet; + uint16_t section; + uint16_t item; + MenuRowAlign align:8; + bool animated; +}; + + +static GColor8 s_normal_palette[] = { { GColorBlackARGB8 }, { GColorClearARGB8 } }; +static GColor8 s_inverted_palette[] = { { GColorWhiteARGB8 }, { GColorClearARGB8 } }; + + +static void simply_menu_clear_section_items(SimplyMenu *self, int section_index); +static void simply_menu_clear(SimplyMenu *self); + +static void simply_menu_set_num_sections(SimplyMenu *self, uint16_t num_sections); +static void simply_menu_add_section(SimplyMenu *self, SimplyMenuSection *section); +static void simply_menu_add_item(SimplyMenu *self, SimplyMenuItem *item); + +static MenuIndex simply_menu_get_selection(SimplyMenu *self); +static void simply_menu_set_selection(SimplyMenu *self, MenuIndex menu_index, MenuRowAlign align, bool animated); + +static void refresh_spinner_timer(SimplyMenu *self); + + +static int64_t prv_get_milliseconds(void) { + time_t now_s; + uint16_t now_ms_part; + time_ms(&now_s, &now_ms_part); + return ((int64_t) now_s) * 1000 + now_ms_part; +} + +static bool prv_send_menu_item(Command type, uint16_t section, uint16_t item) { + MenuItemEventPacket packet = { + .packet.type = type, + .packet.length = sizeof(packet), + .section = section, + .item = item, + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool prv_send_menu_get_section(uint16_t index) { + return prv_send_menu_item(CommandMenuGetSection, index, 0); +} + +static bool prv_send_menu_get_item(uint16_t section, uint16_t index) { + return prv_send_menu_item(CommandMenuGetItem, section, index); +} + +static bool prv_send_menu_select_click(uint16_t section, uint16_t index) { + return prv_send_menu_item(CommandMenuSelect, section, index); +} + +static bool prv_send_menu_select_long_click(uint16_t section, uint16_t index) { + return prv_send_menu_item(CommandMenuLongSelect, section, index); +} + +static bool prv_section_filter(List1Node *node, void *data) { + SimplyMenuCommon *section = (SimplyMenuCommon *)node; + const uint16_t section_index = (uint16_t)(uintptr_t) data; + return (section->section == section_index); +} + +static bool prv_item_filter(List1Node *node, void *data) { + SimplyMenuItem *item = (SimplyMenuItem *)node; + const uint32_t cell_index = (uint32_t)(uintptr_t) data; + const uint16_t section_index = cell_index; + const uint16_t row = cell_index >> 16; + return (item->section == section_index && item->item == row); +} + +static bool prv_request_item_filter(List1Node *node, void *data) { + return (((SimplyMenuItem *)node)->title == NULL); +} + +static SimplyMenuSection *prv_get_menu_section(SimplyMenu *self, int index) { + return (SimplyMenuSection*) list1_find(self->menu_layer.sections, prv_section_filter, + (void*)(uintptr_t) index); +} + +static void prv_free_title(char **title) { + if (*title && *title != EMPTY_TITLE) { + free(*title); + *title = NULL; + } +} + +static void prv_destroy_section(SimplyMenu *self, SimplyMenuSection *section) { + if (!section) { return; } + list1_remove(&self->menu_layer.sections, §ion->node); + prv_free_title(§ion->title); + free(section); +} + +static void prv_destroy_section_by_index(SimplyMenu *self, int section) { + SimplyMenuSection *section_node = + (SimplyMenuSection *)list1_find(self->menu_layer.sections, prv_section_filter, + (void *)(uintptr_t)section); + prv_destroy_section(self, section_node); +} + +static SimplyMenuItem *prv_get_menu_item(SimplyMenu *self, int section, int index) { + const uint32_t cell_index = section | (index << 16); + return (SimplyMenuItem *) list1_find(self->menu_layer.items, prv_item_filter, + (void *)(uintptr_t) cell_index); +} + +static void prv_destroy_item(SimplyMenu *self, SimplyMenuItem *item) { + if (!item) { return; } + list1_remove(&self->menu_layer.items, &item->node); + prv_free_title(&item->title); + prv_free_title(&item->subtitle); + free(item); +} + +static void prv_destroy_item_by_index(SimplyMenu *self, int section, int index) { + const uint32_t cell_index = section | (index << 16); + SimplyMenuItem *item = + (SimplyMenuItem *)list1_find(self->menu_layer.items, prv_item_filter, + (void *)(uintptr_t) cell_index); + prv_destroy_item(self, item); +} + +static void prv_add_section(SimplyMenu *self, SimplyMenuSection *section) { + if (list1_size(self->menu_layer.sections) >= MAX_CACHED_SECTIONS) { + prv_destroy_section(self, (SimplyMenuSection *)list1_last(self->menu_layer.sections)); + } + prv_destroy_section_by_index(self, section->section); + list1_prepend(&self->menu_layer.sections, §ion->node); +} + +static void prv_add_item(SimplyMenu *self, SimplyMenuItem *item) { + if (list1_size(self->menu_layer.items) >= MAX_CACHED_ITEMS) { + prv_destroy_item(self, (SimplyMenuItem*) list1_last(self->menu_layer.items)); + } + prv_destroy_item_by_index(self, item->section, item->item); + list1_prepend(&self->menu_layer.items, &item->node); +} + +static void prv_request_menu_section(SimplyMenu *self, uint16_t section_index) { + SimplyMenuSection *section = prv_get_menu_section(self, section_index); + if (section) { return; } + section = malloc(sizeof(*section)); + *section = (SimplyMenuSection) { + .section = section_index, + }; + prv_add_section(self, section); + prv_send_menu_get_section(section_index); +} + +static void prv_request_menu_item(SimplyMenu *self, uint16_t section_index, uint16_t item_index) { + SimplyMenuItem *item = prv_get_menu_item(self, section_index, item_index); + if (item) { return; } + item = malloc(sizeof(*item)); + *item = (SimplyMenuItem) { + .section = section_index, + .item = item_index, + }; + prv_add_item(self, item); + prv_send_menu_get_item(section_index, item_index); +} + +static void prv_mark_dirty(SimplyMenu *self) { + if (self->menu_layer.menu_layer) { + layer_mark_dirty(menu_layer_get_layer(self->menu_layer.menu_layer)); + } +} + +static void prv_reload_data(SimplyMenu *self) { + if (self->menu_layer.menu_layer) { + menu_layer_reload_data(self->menu_layer.menu_layer); + } +} + +static void simply_menu_set_num_sections(SimplyMenu *self, uint16_t num_sections) { + if (num_sections == 0) { + num_sections = 1; + } + self->menu_layer.num_sections = num_sections; + prv_reload_data(self); +} + +static void simply_menu_add_section(SimplyMenu *self, SimplyMenuSection *section) { + if (section->title == NULL) { + section->title = EMPTY_TITLE; + } + prv_add_section(self, section); + prv_reload_data(self); +} + +static void simply_menu_add_item(SimplyMenu *self, SimplyMenuItem *item) { + if (item->title == NULL) { + item->title = EMPTY_TITLE; + } + prv_add_item(self, item); + prv_mark_dirty(self); +} + +static MenuIndex simply_menu_get_selection(SimplyMenu *self) { + if (!self->menu_layer.menu_layer) { + return (MenuIndex) {}; + } + return menu_layer_get_selected_index(self->menu_layer.menu_layer); +} + +static void simply_menu_set_selection(SimplyMenu *self, MenuIndex menu_index, MenuRowAlign align, + bool animated) { + menu_layer_set_selected_index(self->menu_layer.menu_layer, menu_index, align, animated); +} + +static bool prv_send_menu_selection(SimplyMenu *self) { + MenuIndex menu_index = simply_menu_get_selection(self); + return prv_send_menu_item(CommandMenuSelectionEvent, menu_index.section, menu_index.row); +} + +static void spinner_timer_callback(void *data) { + SimplyMenu *self = data; + self->spinner_timer = NULL; + prv_mark_dirty(self); + refresh_spinner_timer(self); +} + +static SimplyMenuItem *get_first_request_item(SimplyMenu *self) { + return (SimplyMenuItem *)list1_find(self->menu_layer.items, prv_request_item_filter, NULL); +} + +static SimplyMenuItem *get_last_request_item(SimplyMenu *self) { + return (SimplyMenuItem *)list1_find_last(self->menu_layer.items, prv_request_item_filter, NULL); +} + +static void refresh_spinner_timer(SimplyMenu *self) { + if (!self->spinner_timer && get_first_request_item(self)) { + self->spinner_timer = app_timer_register(SPINNER_MS, spinner_timer_callback, self); + } +} + +static uint16_t prv_menu_get_num_sections_callback(MenuLayer *menu_layer, void *data) { + SimplyMenu *self = data; + return self->menu_layer.num_sections; +} + +static uint16_t prv_menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, + void *data) { + SimplyMenu *self = data; + SimplyMenuSection *section = prv_get_menu_section(self, section_index); + return section ? section->num_items : 1; +} + +static int16_t prv_menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, + void *data) { + SimplyMenu *self = data; + SimplyMenuSection *section = prv_get_menu_section(self, section_index); + return (section && section->title && + section->title != EMPTY_TITLE ? MENU_CELL_BASIC_HEADER_HEIGHT : 0); +} + +ROUND_USAGE static int16_t prv_menu_get_cell_height_callback(MenuLayer *menu_layer, MenuIndex *cell_index, + void *context) { + if (PBL_IF_ROUND_ELSE(true, false)) { + const bool is_selected = menu_layer_is_index_selected(menu_layer, cell_index); + return is_selected ? MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT : + MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT; + } else { + return MENU_CELL_BASIC_CELL_HEIGHT; + } +} + +static void prv_menu_draw_header_callback(GContext *ctx, const Layer *cell_layer, + uint16_t section_index, void *data) { + SimplyMenu *self = data; + SimplyMenuSection *section = prv_get_menu_section(self, section_index); + if (!section) { + prv_request_menu_section(self, section_index); + return; + } + + list1_remove(&self->menu_layer.sections, §ion->node); + list1_prepend(&self->menu_layer.sections, §ion->node); + + GRect bounds = layer_get_bounds(cell_layer); + + graphics_context_set_fill_color(ctx, gcolor8_get_or(section->title_background, GColorWhite)); + graphics_fill_rect(ctx, bounds, 0, GCornerNone); + + bounds.origin.x += 2; + bounds.origin.y -= 1; + + graphics_context_set_text_color(ctx, gcolor8_get_or(section->title_foreground, GColorBlack)); + + GTextAttributes *title_attributes = graphics_text_attributes_create(); + PBL_IF_ROUND_ELSE( + graphics_text_attributes_enable_paging_on_layer( + title_attributes, (Layer *)menu_layer_get_scroll_layer(self->menu_layer.menu_layer), + &bounds, TEXT_FLOW_DEFAULT_INSET), NOOP); + const GTextAlignment align = PBL_IF_ROUND_ELSE(GTextAlignmentCenter, GTextAlignmentLeft); + graphics_draw_text(ctx, section->title, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD), + bounds, GTextOverflowModeTrailingEllipsis, align, title_attributes); + graphics_text_attributes_destroy(title_attributes); +} + +static void simply_menu_draw_row_spinner(SimplyMenu *self, GContext *ctx, + const Layer *cell_layer) { + GRect bounds = layer_get_bounds(cell_layer); + GPoint center = grect_center_point(&bounds); + + const int16_t min_radius = 4 * bounds.size.h / 24; + const int16_t max_radius = 9 * bounds.size.h / 24; + const int16_t num_lines = 16; + const int16_t num_drawn_lines = 3; + + const int64_t now_ms = prv_get_milliseconds(); + const uint32_t start_index = (now_ms / SPINNER_MS) % num_lines; + + graphics_context_set_antialiased(ctx, true); + + GColor8 stroke_color = + menu_cell_layer_is_highlighted(cell_layer) ? self->menu_layer.highlight_foreground : + self->menu_layer.normal_foreground; + graphics_context_set_stroke_color(ctx, gcolor8_get_or(stroke_color, GColorBlack)); + + for (int16_t i = 0; i < num_drawn_lines; i++) { + const uint32_t angle = (i + start_index) * TRIG_MAX_ANGLE / num_lines; + GPoint a = gpoint_add(center, gpoint_polar(angle, min_radius)); + GPoint b = gpoint_add(center, gpoint_polar(angle, max_radius)); + graphics_draw_line(ctx, a, b); + } +} + +static void prv_menu_draw_row_callback(GContext *ctx, const Layer *cell_layer, + MenuIndex *cell_index, void *data) { + SimplyMenu *self = data; + SimplyMenuSection *section = prv_get_menu_section(self, cell_index->section); + if (!section) { + prv_request_menu_section(self, cell_index->section); + return; + } + + SimplyMenuItem *item = prv_get_menu_item(self, cell_index->section, cell_index->row); + if (!item) { + prv_request_menu_item(self, cell_index->section, cell_index->row); + return; + } + + if (item->title == NULL) { + SimplyMenuItem *last_request = get_last_request_item(self); + if (last_request == item) { + simply_menu_draw_row_spinner(self, ctx, cell_layer); + refresh_spinner_timer(self); + } + return; + } + + list1_remove(&self->menu_layer.items, &item->node); + list1_prepend(&self->menu_layer.items, &item->node); + + SimplyImage *image = simply_res_get_image(self->window.simply->res, item->icon); + GColor8 *palette = NULL; + + if (image && image->is_palette_black_and_white) { + palette = gbitmap_get_palette(image->bitmap); + const bool is_highlighted = menu_cell_layer_is_highlighted(cell_layer); + gbitmap_set_palette(image->bitmap, is_highlighted ? s_inverted_palette : s_normal_palette, + false); + } + + graphics_context_set_alpha_blended(ctx, true); + menu_cell_basic_draw(ctx, cell_layer, item->title, item->subtitle, image ? image->bitmap : NULL); + + if (palette) { + gbitmap_set_palette(image->bitmap, palette, false); + } +} + +static void prv_menu_select_click_callback(MenuLayer *menu_layer, MenuIndex *cell_index, + void *data) { + prv_send_menu_select_click(cell_index->section, cell_index->row); +} + +static void prv_menu_select_long_click_callback(MenuLayer *menu_layer, MenuIndex *cell_index, + void *data) { + prv_send_menu_select_long_click(cell_index->section, cell_index->row); +} + +static void prv_single_click_handler(ClickRecognizerRef recognizer, void *context) { + Window *base_window = layer_get_window(context); + SimplyWindow *window = window_get_user_data(base_window); + simply_window_single_click_handler(recognizer, window); +} + +static void prv_click_config_provider(void *context) { + window_single_click_subscribe(BUTTON_ID_BACK, prv_single_click_handler); + menu_layer_click_config(context); +} + +static void prv_menu_window_load(Window *window) { + SimplyMenu *self = window_get_user_data(window); + + simply_window_load(&self->window); + + Layer *window_layer = window_get_root_layer(window); + GRect frame = layer_get_frame(window_layer); + frame.origin = GPointZero; + + MenuLayer *menu_layer = self->menu_layer.menu_layer = menu_layer_create(frame); + Layer *menu_base_layer = menu_layer_get_layer(menu_layer); + self->window.layer = menu_base_layer; + layer_add_child(window_layer, menu_base_layer); + + menu_layer_set_callbacks(menu_layer, self, (MenuLayerCallbacks){ + .get_num_sections = prv_menu_get_num_sections_callback, + .get_num_rows = prv_menu_get_num_rows_callback, + .get_header_height = prv_menu_get_header_height_callback, +#if defined(PBL_ROUND) + .get_cell_height = prv_menu_get_cell_height_callback, +#endif + .draw_header = prv_menu_draw_header_callback, + .draw_row = prv_menu_draw_row_callback, + .select_click = prv_menu_select_click_callback, + .select_long_click = prv_menu_select_long_click_callback, + }); + + menu_layer_set_click_config_provider_onto_window(menu_layer, prv_click_config_provider, window); +} + +static void prv_menu_window_appear(Window *window) { + SimplyMenu *self = window_get_user_data(window); + simply_window_appear(&self->window); +} + +static void prv_menu_window_disappear(Window *window) { + SimplyMenu *self = window_get_user_data(window); + if (simply_window_disappear(&self->window)) { + simply_res_clear(self->window.simply->res); + simply_menu_clear(self); + } +} + +static void prv_menu_window_unload(Window *window) { + SimplyMenu *self = window_get_user_data(window); + + menu_layer_destroy(self->menu_layer.menu_layer); + self->menu_layer.menu_layer = NULL; + + simply_window_unload(&self->window); +} + +static void simply_menu_clear_section_items(SimplyMenu *self, int section_index) { + SimplyMenuItem *item = NULL; + do { + item = (SimplyMenuItem *)list1_find(self->menu_layer.items, prv_section_filter, + (void *)(uintptr_t) section_index); + prv_destroy_item(self, item); + } while (item); +} + +static void simply_menu_clear(SimplyMenu *self) { + while (self->menu_layer.sections) { + prv_destroy_section(self, (SimplyMenuSection *)self->menu_layer.sections); + } + + while (self->menu_layer.items) { + prv_destroy_item(self, (SimplyMenuItem *)self->menu_layer.items); + } + + prv_reload_data(self); +} + +static void prv_handle_menu_clear_packet(Simply *simply, Packet *data) { + simply_menu_clear(simply->menu); +} + +static void prv_handle_menu_clear_section_packet(Simply *simply, Packet *data) { + MenuClearSectionPacket *packet = (MenuClearSectionPacket *)data; + simply_menu_clear_section_items(simply->menu, packet->section); +} + +static void prv_handle_menu_props_packet(Simply *simply, Packet *data) { + MenuPropsPacket *packet = (MenuPropsPacket *)data; + SimplyMenu *self = simply->menu; + + simply_menu_set_num_sections(self, packet->num_sections); + + if (!self->window.window) { return; } + + window_set_background_color(self->window.window, gcolor8_get_or(packet->background_color, + GColorWhite)); + + SimplyMenuLayer *menu_layer = &self->menu_layer; + if (!menu_layer->menu_layer) { return; } + + menu_layer->normal_background = packet->background_color; + menu_layer->normal_foreground = packet->text_color; + menu_layer->highlight_background = packet->highlight_background_color; + menu_layer->highlight_foreground = packet->highlight_text_color; + + menu_layer_set_normal_colors(menu_layer->menu_layer, + gcolor8_get_or(menu_layer->normal_background, GColorWhite), + gcolor8_get_or(menu_layer->normal_foreground, GColorBlack)); + menu_layer_set_highlight_colors(menu_layer->menu_layer, + gcolor8_get_or(menu_layer->highlight_background, GColorBlack), + gcolor8_get_or(menu_layer->highlight_foreground, GColorWhite)); +} + +static void prv_handle_menu_section_packet(Simply *simply, Packet *data) { + MenuSectionPacket *packet = (MenuSectionPacket *)data; + SimplyMenuSection *section = malloc(sizeof(*section)); + *section = (SimplyMenuSection) { + .section = packet->section, + .num_items = packet->num_items, + .title_foreground = packet->text_color, + .title_background = packet->background_color, + .title = packet->title_length ? strdup2(packet->title) : NULL, + }; + simply_menu_add_section(simply->menu, section); +} + +static void prv_handle_menu_item_packet(Simply *simply, Packet *data) { + MenuItemPacket *packet = (MenuItemPacket *)data; + SimplyMenuItem *item = malloc(sizeof(*item)); + *item = (SimplyMenuItem) { + .section = packet->section, + .item = packet->item, + .title = packet->title_length ? strdup2(packet->buffer) : NULL, + .subtitle = packet->subtitle_length ? strdup2(packet->buffer + packet->title_length + 1) : NULL, + .icon = packet->icon, + }; + simply_menu_add_item(simply->menu, item); +} + +static void prv_handle_menu_get_selection_packet(Simply *simply, Packet *data) { + prv_send_menu_selection(simply->menu); +} + +static void prv_handle_menu_selection_packet(Simply *simply, Packet *data) { + MenuSelectionPacket *packet = (MenuSelectionPacket *)data; + MenuIndex menu_index = { + .section = packet->section, + .row = packet->item, + }; + simply_menu_set_selection(simply->menu, menu_index, packet->align, packet->animated); +} + +bool simply_menu_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandMenuClear: + prv_handle_menu_clear_packet(simply, packet); + return true; + case CommandMenuClearSection: + prv_handle_menu_clear_section_packet(simply, packet); + return true; + case CommandMenuProps: + prv_handle_menu_props_packet(simply, packet); + return true; + case CommandMenuSection: + prv_handle_menu_section_packet(simply, packet); + return true; + case CommandMenuItem: + prv_handle_menu_item_packet(simply, packet); + return true; + case CommandMenuSelection: + prv_handle_menu_selection_packet(simply, packet); + return true; + case CommandMenuGetSelection: + prv_handle_menu_get_selection_packet(simply, packet); + return true; + } + return false; +} + +SimplyMenu *simply_menu_create(Simply *simply) { + SimplyMenu *self = malloc(sizeof(*self)); + *self = (SimplyMenu) { + .window.simply = simply, +#if defined(PBL_ROUND) + .window.status_bar_insets_bottom = true, +#endif + .menu_layer.num_sections = 1, + }; + + static const WindowHandlers s_window_handlers = { + .load = prv_menu_window_load, + .appear = prv_menu_window_appear, + .disappear = prv_menu_window_disappear, + .unload = prv_menu_window_unload, + }; + self->window.window_handlers = &s_window_handlers; + + simply_window_init(&self->window, simply); + simply_window_set_background_color(&self->window, GColor8White); + + return self; +} + +void simply_menu_destroy(SimplyMenu *self) { + if (!self) { + return; + } + + simply_window_deinit(&self->window); + + free(self); +} diff --git a/src/simply/simply_menu.h b/src/simply/simply_menu.h new file mode 100644 index 00000000..8520e8ae --- /dev/null +++ b/src/simply/simply_menu.h @@ -0,0 +1,83 @@ +#pragma once + +#include "simply_window.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include "util/list1.h" + +#include + +//! Default cell height in pixels +#define MENU_CELL_BASIC_CELL_HEIGHT ((const int16_t) 44) + +typedef enum SimplyMenuType SimplyMenuType; + +enum SimplyMenuType { + SimplyMenuTypeNone = 0, + SimplyMenuTypeSection, + SimplyMenuTypeItem, +}; + +typedef struct SimplyMenuLayer SimplyMenuLayer; + +struct SimplyMenuLayer { + MenuLayer *menu_layer; + List1Node *sections; + List1Node *items; + uint16_t num_sections; + GColor8 normal_foreground; + GColor8 normal_background; + GColor8 highlight_foreground; + GColor8 highlight_background; +}; + +typedef struct SimplyMenu SimplyMenu; + +struct SimplyMenu { + SimplyWindow window; + SimplyMenuLayer menu_layer; + AppTimer *spinner_timer; +}; + +typedef struct SimplyMenuCommon SimplyMenuCommon; + +struct SimplyMenuCommon { + List1Node node; + uint16_t section; + char *title; +}; + +typedef struct SimplyMenuCommonMember SimplyMenuCommonMember; + +struct SimplyMenuCommonMember { + union { + SimplyMenuCommon common; + SimplyMenuCommon; + }; +}; + +typedef struct SimplyMenuSection SimplyMenuSection; + +struct SimplyMenuSection { + SimplyMenuCommonMember; + uint16_t num_items; + GColor8 title_foreground; + GColor8 title_background; +}; + +typedef struct SimplyMenuItem SimplyMenuItem; + +struct SimplyMenuItem { + SimplyMenuCommonMember; + char *subtitle; + uint32_t icon; + uint16_t item; +}; + +SimplyMenu *simply_menu_create(Simply *simply); +void simply_menu_destroy(SimplyMenu *self); + +bool simply_menu_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_msg.c b/src/simply/simply_msg.c new file mode 100644 index 00000000..dcfc8065 --- /dev/null +++ b/src/simply/simply_msg.c @@ -0,0 +1,398 @@ +#include "simply_msg.h" + +#include "simply_accel.h" +#include "simply_voice.h" +#include "simply_res.h" +#include "simply_stage.h" +#include "simply_menu.h" +#include "simply_ui.h" +#include "simply_window_stack.h" +#include "simply_wakeup.h" + +#include "simply.h" + +#include "util/dict.h" +#include "util/list1.h" +#include "util/math.h" +#include "util/memory.h" +#include "util/platform.h" +#include "util/string.h" + +#include + +#define SEND_DELAY_MS 10 + +static const size_t APP_MSG_SIZE_INBOUND = IF_APLITE_ELSE(1024, 2044); +static const size_t APP_MSG_SIZE_OUTBOUND = 1024; + +typedef enum VibeType VibeType; + +enum VibeType { + VibeShort = 0, + VibeLong = 1, + VibeDouble = 2, +}; + +typedef enum LightType LightType; + +enum LightType { + LightOn = 0, + LightAuto = 1, + LightTrigger = 2, +}; + +typedef struct SegmentPacket SegmentPacket; + +struct __attribute__((__packed__)) SegmentPacket { + Packet packet; + bool is_last; + uint8_t buffer[]; +}; + +typedef struct ImagePacket ImagePacket; + +struct __attribute__((__packed__)) ImagePacket { + Packet packet; + uint32_t id; + int16_t width; + int16_t height; + uint16_t pixels_length; + uint8_t pixels[]; +}; + +typedef struct VibePacket VibePacket; + +struct __attribute__((__packed__)) VibePacket { + Packet packet; + VibeType type:8; +}; + +typedef struct LightPacket LightPacket; + +struct __attribute__((__packed__)) LightPacket { + Packet packet; + LightType type:8; +}; + +static SimplyMsg *s_msg = NULL; + +static bool s_has_communicated = false; + +typedef struct CommandHandlerEntry CommandHandlerEntry; + +struct CommandHandlerEntry { + int16_t start_type; + int16_t end_type; + PacketHandler handler; +}; + +static void handle_packet(Simply *simply, Packet *packet); + +bool simply_msg_has_communicated() { + return s_has_communicated; +} + +static void destroy_packet(SimplyMsg *self, SimplyPacket *packet) { + if (!packet) { + return; + } + free(packet->buffer); + packet->buffer = NULL; + free(packet); +} + +static void add_receive_packet(SimplyMsg *self, SegmentPacket *packet) { + size_t size = packet->packet.length; + Packet *copy = malloc(size); + memcpy(copy, packet, size); + SimplyPacket *node = malloc0(sizeof(*node)); + node->length = size; + node->buffer = copy; + list1_prepend(&self->receive_queue, &node->node); +} + +static void handle_receive_queue(SimplyMsg *self, SegmentPacket *packet) { + size_t total_length = packet->packet.length - sizeof(SegmentPacket); + for (List1Node *walk = self->receive_queue; walk; walk = walk->next) { + total_length += ((SimplyPacket*) walk)->length - sizeof(SegmentPacket); + } + + void *buffer = malloc(total_length); + void *cursor = buffer + total_length; + SegmentPacket *other = packet; + SimplyPacket *walk = NULL; + while (true) { + size_t copy_size = other->packet.length - sizeof(SegmentPacket); + cursor -= copy_size; + memcpy(cursor, other->buffer, copy_size); + + if (walk) { + list1_remove(&self->receive_queue, &walk->node); + destroy_packet(self, walk); + } + + walk = (SimplyPacket*) self->receive_queue; + if (!walk) { + break; + } + + other = walk->buffer; + } + + handle_packet(self->simply, buffer); + + free(buffer); +} + +static void handle_segment_packet(Simply *simply, Packet *data) { + SegmentPacket *packet = (SegmentPacket*) data; + if (packet->is_last) { + handle_receive_queue(simply->msg, packet); + } else { + add_receive_packet(simply->msg, packet); + } +} + +static void handle_image_packet(Simply *simply, Packet *data) { + ImagePacket *packet = (ImagePacket*) data; + simply_res_add_image(simply->res, packet->id, packet->width, packet->height, packet->pixels, + packet->pixels_length); +} + +static void handle_vibe_packet(Simply *simply, Packet *data) { + VibePacket *packet = (VibePacket*) data; + switch (packet->type) { + case VibeShort: vibes_short_pulse(); break; + case VibeLong: vibes_long_pulse(); break; + case VibeDouble: vibes_double_pulse(); break; + } +} + +static void handle_light_packet(Simply *simply, Packet *data) { + LightPacket *packet = (LightPacket*) data; + switch (packet->type) { + case LightOn: light_enable(true); break; + case LightAuto: light_enable(false); break; + case LightTrigger: light_enable_interaction(); break; + } +} + +static bool simply_base_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandSegment: + handle_segment_packet(simply, packet); + return true; + case CommandImagePacket: + handle_image_packet(simply, packet); + return true; + case CommandVibe: + handle_vibe_packet(simply, packet); + return true; + case CommandLight: + handle_light_packet(simply, packet); + return true; + } + return false; +} + +static void handle_packet(Simply *simply, Packet *packet) { + if (simply_base_handle_packet(simply, packet)) { return; } + if (simply_wakeup_handle_packet(simply, packet)) { return; } + if (simply_window_stack_handle_packet(simply, packet)) { return; } + if (simply_window_handle_packet(simply, packet)) { return; } + if (simply_ui_handle_packet(simply, packet)) { return; } + if (simply_accel_handle_packet(simply, packet)) { return; } + if (simply_voice_handle_packet(simply, packet)) { return; } + if (simply_menu_handle_packet(simply, packet)) { return; } + if (simply_stage_handle_packet(simply, packet)) { return; } +} + +static void received_callback(DictionaryIterator *iter, void *context) { + Tuple *tuple = dict_find(iter, 0); + if (!tuple) { + return; + } + + s_has_communicated = true; + + size_t length = tuple->length; + if (length == 0) { + return; + } + + uint8_t *buffer = tuple->value->data; + while (true) { + Packet *packet = (Packet*) buffer; + handle_packet(context, packet); + + if (packet->length == 0) { + break; + } + + length -= packet->length; + if (length == 0) { + break; + } + + buffer += packet->length; + } +} + +static void dropped_callback(AppMessageResult reason, void *context) { +} + +static void sent_callback(DictionaryIterator *iter, void *context) { +} + +static void failed_callback(DictionaryIterator *iter, AppMessageResult reason, void *context) { + Simply *simply = context; + + if (reason == APP_MSG_NOT_CONNECTED) { + s_has_communicated = false; + + simply_msg_show_disconnected(simply->msg); + } +} + +void simply_msg_show_disconnected(SimplyMsg *self) { + Simply *simply = self->simply; + SimplyUi *ui = simply->ui; + + simply_ui_clear(ui, ~0); + simply_ui_set_text(ui, UiSubtitle, "Disconnected"); + simply_ui_set_text(ui, UiBody, "Run the Pebble Phone App"); + + if (window_stack_get_top_window() != ui->window.window) { + bool was_broadcast = simply_window_stack_set_broadcast(false); + simply_window_stack_show(simply->window_stack, &ui->window, true); + simply_window_stack_set_broadcast(was_broadcast); + } +} + +SimplyMsg *simply_msg_create(Simply *simply) { + if (s_msg) { + return s_msg; + } + + SimplyMsg *self = malloc(sizeof(*self)); + *self = (SimplyMsg) { .simply = simply }; + s_msg = self; + + simply->msg = self; + + app_message_open(APP_MSG_SIZE_INBOUND, APP_MSG_SIZE_OUTBOUND); + + app_message_set_context(simply); + + app_message_register_inbox_received(received_callback); + app_message_register_inbox_dropped(dropped_callback); + app_message_register_outbox_sent(sent_callback); + app_message_register_outbox_failed(failed_callback); + + return self; +} + +void simply_msg_destroy(SimplyMsg *self) { + if (!self) { + return; + } + + app_message_deregister_callbacks(); + + self->simply->msg = NULL; + + free(self); +} + +static bool send_msg(uint8_t *buffer, size_t length) { + DictionaryIterator *iter = NULL; + if (app_message_outbox_begin(&iter) != APP_MSG_OK) { + return false; + } + dict_write_data(iter, 0, buffer, length); + return (app_message_outbox_send() == APP_MSG_OK); +} + +bool simply_msg_send(uint8_t *buffer, size_t length) { + return send_msg(buffer, length); +} + +static void make_multi_packet(SimplyMsg *self, SimplyPacket *packet) { + if (!packet) { + return; + } + size_t length = 0; + SimplyPacket *last; + for (SimplyPacket *walk = packet;;) { + length += walk->length; + SimplyPacket *next = (SimplyPacket*) walk->node.next; + if (!next || length + next->length > APP_MSG_SIZE_OUTBOUND - 2 * sizeof(Tuple)) { + last = next; + break; + } + walk = next; + } + uint8_t *buffer = malloc(length); + if (!buffer) { + return; + } + uint8_t *cursor = buffer; + for (SimplyPacket *walk = packet; walk && walk != last;) { + memcpy(cursor, walk->buffer, walk->length); + cursor += walk->length; + SimplyPacket *next = (SimplyPacket*) walk->node.next; + list1_remove(&self->send_queue, &walk->node); + destroy_packet(self, walk); + walk = next; + } + self->send_buffer = buffer; + self->send_length = length; +} + +static void send_msg_retry(void *data) { + SimplyMsg *self = data; + self->send_timer = NULL; + if (!self->send_buffer) { + make_multi_packet(self, (SimplyPacket*) self->send_queue); + } + if (!self->send_buffer) { + return; + } + if (send_msg(self->send_buffer, self->send_length)) { + free(self->send_buffer); + self->send_buffer = NULL; + self->send_delay_ms = SEND_DELAY_MS; + } else { + self->send_delay_ms *= 2; + } + self->send_timer = app_timer_register(self->send_delay_ms, send_msg_retry, self); +} + +static SimplyPacket *add_packet(SimplyMsg *self, Packet *buffer) { + SimplyPacket *packet = malloc(sizeof(*packet)); + if (!packet) { + free(buffer); + return NULL; + } + *packet = (SimplyPacket) { + .length = buffer->length, + .buffer = buffer, + }; + list1_append(&self->send_queue, &packet->node); + if (self->send_delay_ms <= SEND_DELAY_MS) { + if (self->send_timer) { + app_timer_cancel(self->send_timer); + } + self->send_timer = app_timer_register(SEND_DELAY_MS, send_msg_retry, self); + } + return packet; +} + +bool simply_msg_send_packet(Packet *packet) { + Packet *copy = malloc(packet->length); + if (!copy) { + return false; + } + memcpy(copy, packet, packet->length); + return add_packet(s_msg, copy); +} diff --git a/src/simply/simply_msg.h b/src/simply/simply_msg.h new file mode 100644 index 00000000..35675579 --- /dev/null +++ b/src/simply/simply_msg.h @@ -0,0 +1,46 @@ +#pragma once + +#include "simply_msg_commands.h" + +#include "simply.h" + +#include "util/list1.h" + +#include + +typedef struct SimplyMsg SimplyMsg; + +struct SimplyMsg { + Simply *simply; + List1Node *send_queue; + List1Node *receive_queue; + uint32_t send_delay_ms; + AppTimer *send_timer; + uint8_t *send_buffer; + size_t send_length; +}; + +typedef struct SimplyPacket SimplyPacket; + +struct SimplyPacket { + List1Node node; + uint16_t length; + void *buffer; +}; + +typedef struct Packet Packet; + +struct __attribute__((__packed__)) Packet { + uint16_t type; + uint16_t length; +}; + +typedef void (*PacketHandler)(Simply *simply, Packet *packet); + +SimplyMsg *simply_msg_create(Simply *simply); +void simply_msg_destroy(SimplyMsg *self); +bool simply_msg_has_communicated(); +void simply_msg_show_disconnected(SimplyMsg *self); + +bool simply_msg_send(uint8_t *buffer, size_t length); +bool simply_msg_send_packet(Packet *packet); diff --git a/src/simply/simply_msg_commands.h b/src/simply/simply_msg_commands.h new file mode 100644 index 00000000..c04f7aaa --- /dev/null +++ b/src/simply/simply_msg_commands.h @@ -0,0 +1,62 @@ +#pragma once + +typedef enum Command Command; + +enum Command { + CommandSegment = 1, + CommandReady, + CommandLaunchReason, + CommandWakeupSet, + CommandWakeupSetResult, + CommandWakeupCancel, + CommandWakeupEvent, + CommandWindowShow, + CommandWindowHide, + CommandWindowShowEvent, + CommandWindowHideEvent, + CommandWindowProps, + CommandWindowButtonConfig, + CommandWindowStatusBar, + CommandWindowActionBar, + CommandClick, + CommandLongClick, + CommandImagePacket, + CommandCardClear, + CommandCardText, + CommandCardImage, + CommandCardStyle, + CommandVibe, + CommandLight, + CommandAccelPeek, + CommandAccelConfig, + CommandAccelData, + CommandAccelTap, + CommandMenuClear, + CommandMenuClearSection, + CommandMenuProps, + CommandMenuSection, + CommandMenuGetSection, + CommandMenuItem, + CommandMenuGetItem, + CommandMenuSelection, + CommandMenuGetSelection, + CommandMenuSelectionEvent, + CommandMenuSelect, + CommandMenuLongSelect, + CommandStageClear, + CommandElementInsert, + CommandElementRemove, + CommandElementCommon, + CommandElementRadius, + CommandElementAngle, + CommandElementAngle2, + CommandElementText, + CommandElementTextStyle, + CommandElementImage, + CommandElementAnimate, + CommandElementAnimateDone, + CommandVoiceStart, + CommandVoiceStop, + CommandVoiceData, + NumCommands, +}; diff --git a/src/simply/simply_res.c b/src/simply/simply_res.c new file mode 100644 index 00000000..e60c1529 --- /dev/null +++ b/src/simply/simply_res.c @@ -0,0 +1,239 @@ +#include "simply_res.h" + +#include "util/color.h" +#include "util/graphics.h" +#include "util/memory.h" +#include "util/sdk.h" +#include "util/window.h" + +#include + +static bool id_filter(List1Node *node, void *data) { + return (((SimplyResItemCommon*) node)->id == (uint32_t)(uintptr_t) data); +} + +static void destroy_image(SimplyRes *self, SimplyImage *image) { + if (!image) { + return; + } + + list1_remove(&self->images, &image->node); + gbitmap_destroy(image->bitmap); + free(image->palette); + free(image); +} + +static void destroy_font(SimplyRes *self, SimplyFont *font) { + if (!font) { + return; + } + + list1_remove(&self->fonts, &font->node); + fonts_unload_custom_font(font->font); + free(font); +} + +static void setup_image(SimplyImage *image) { + image->is_palette_black_and_white = gbitmap_is_palette_black_and_white(image->bitmap); + + if (!image->is_palette_black_and_white) { + return; + } + + GColor8 *palette = gbitmap_get_palette(image->bitmap); + GColor8 *palette_copy = malloc0(2 * sizeof(GColor8)); + memcpy(palette_copy, palette, 2 * sizeof(GColor8)); + gbitmap_set_palette(image->bitmap, palette_copy, false); + image->palette = palette_copy; +} + +bool simply_res_evict_image(SimplyRes *self) { + SimplyImage *last_image = (SimplyImage *)list1_last(self->images); + if (!last_image) { + return false; + } + + destroy_image(self, last_image); + return true; +} + +static void add_image(SimplyRes *self, SimplyImage *image) { + list1_prepend(&self->images, &image->node); + + setup_image(image); + + window_stack_schedule_top_window_render(); +} + +typedef GBitmap *(*GBitmapCreator)(SimplyImage *image, void *data); + +static SimplyImage *create_image(SimplyRes *self, GBitmapCreator creator, void *data) { + SimplyImage *image = NULL; + while (!(image = malloc0(sizeof(*image)))) { + if (!simply_res_evict_image(self)) { + return NULL; + } + } + + GBitmap *bitmap = NULL; + while (!(bitmap = creator(image, data))) { + if (!simply_res_evict_image(self)) { + free(image); + return NULL; + } + } + + image->bitmap = bitmap; + + return image; +} + +static GBitmap *create_bitmap_with_id(SimplyImage *image, void *data) { + const uint32_t id = (uint32_t)(uintptr_t) data; + GBitmap *bitmap = gbitmap_create_with_resource(id); + if (bitmap) { + image->id = id; + } + return bitmap; +} + +SimplyImage *simply_res_add_bundled_image(SimplyRes *self, uint32_t id) { + SimplyImage *image = create_image(self, create_bitmap_with_id, (void*)(uintptr_t) id); + if (image) { + add_image(self, image); + } + return image; +} + +typedef struct { + GSize size; + size_t data_length; + const uint8_t *data; +} CreateDataContext; + +SDK_2_USAGE static GBitmap *create_bitmap_with_data(SimplyImage *image, void *data) { + CreateDataContext *ctx = data; + GBitmap *bitmap = gbitmap_create_blank(ctx->size, GBitmapFormat1Bit); + if (bitmap) { + image->bitmap_data = gbitmap_get_data(bitmap); + memcpy(image->bitmap_data, ctx->data, ctx->data_length); + } + return bitmap; +} + +SDK_3_USAGE static GBitmap *create_bitmap_with_png_data(SimplyImage *image, void *data) { + CreateDataContext *ctx = data; + return ctx->data ? gbitmap_create_from_png_data(ctx->data, ctx->data_length) : NULL; +} + +SimplyImage *simply_res_add_image(SimplyRes *self, uint32_t id, int16_t width, int16_t height, + uint8_t *pixels, uint16_t pixels_length) { + SimplyImage *image = (SimplyImage*) list1_find(self->images, id_filter, (void*)(uintptr_t) id); + if (image) { + destroy_image(self, image); + } + + CreateDataContext context = { + .size = GSize(width, height), + .data_length = pixels_length, + .data = pixels, + }; + image = IF_SDK_3_ELSE(create_image(self, create_bitmap_with_png_data, &context), + create_image(self, create_bitmap_with_data, &context)); + if (image) { + image->id = id; + add_image(self, image); + } + return image; +} + +void simply_res_remove_image(SimplyRes *self, uint32_t id) { + SimplyImage *image = (SimplyImage*) list1_find(self->images, id_filter, (void*)(uintptr_t) id); + if (image) { + destroy_image(self, image); + } +} + +SimplyImage *simply_res_auto_image(SimplyRes *self, uint32_t id, bool is_placeholder) { + if (!id) { + return NULL; + } + SimplyImage *image = (SimplyImage*) list1_find(self->images, id_filter, (void*)(uintptr_t) id); + if (image) { + return image; + } + if (id <= self->num_bundled_res) { + return simply_res_add_bundled_image(self, id); + } + if (is_placeholder) { + return simply_res_add_image(self, id, 0, 0, NULL, 0); + } + return NULL; +} + +GFont simply_res_add_custom_font(SimplyRes *self, uint32_t id) { + SimplyFont *font = malloc0(sizeof(*font)); + if (!font) { + return NULL; + } + + ResHandle handle = resource_get_handle(id); + if (!handle) { + return NULL; + } + + GFont custom_font = fonts_load_custom_font(handle); + if (!custom_font) { + free(font); + return NULL; + } + + font->id = id; + font->font = custom_font; + + list1_prepend(&self->fonts, &font->node); + + window_stack_schedule_top_window_render(); + + return font->font; +} + +GFont simply_res_auto_font(SimplyRes *self, uint32_t id) { + if (!id) { + return NULL; + } + SimplyFont *font = (SimplyFont*) list1_find(self->fonts, id_filter, (void*)(uintptr_t) id); + if (font) { + return font->font; + } + if (id <= self->num_bundled_res) { + return simply_res_add_custom_font(self, id); + } + return NULL; +} + +void simply_res_clear(SimplyRes *self) { + while (self->images) { + destroy_image(self, (SimplyImage*) self->images); + } + + while (self->fonts) { + destroy_font(self, (SimplyFont*) self->fonts); + } +} + +SimplyRes *simply_res_create() { + SimplyRes *self = malloc(sizeof(*self)); + *self = (SimplyRes) { .images = NULL }; + + while (resource_get_handle(self->num_bundled_res + 1)) { + ++self->num_bundled_res; + } + + return self; +} + +void simply_res_destroy(SimplyRes *self) { + simply_res_clear(self); + free(self); +} diff --git a/src/simply/simply_res.h b/src/simply/simply_res.h new file mode 100644 index 00000000..8c52d38b --- /dev/null +++ b/src/simply/simply_res.h @@ -0,0 +1,67 @@ +#pragma once + +#include "simply.h" + +#include "util/color.h" +#include "util/list1.h" + +#include + +#define simply_res_get_image(self, id) simply_res_auto_image(self, id, false) + +#define simply_res_get_font(self, id) simply_res_auto_font(self, id) + +typedef struct SimplyRes SimplyRes; + +struct SimplyRes { + List1Node *images; + List1Node *fonts; + uint32_t num_bundled_res; +}; + +typedef struct SimplyResItemCommon SimplyResItemCommon; + +#define SimplyResItemCommonDef { \ + List1Node node; \ + uint32_t id; \ +} + +struct SimplyResItemCommon SimplyResItemCommonDef; + +#define SimplyResItemCommonMember \ + union { \ + struct SimplyResItemCommon common; \ + struct SimplyResItemCommonDef; \ + } + +typedef struct SimplyImage SimplyImage; + +struct SimplyImage { + SimplyResItemCommonMember; + uint8_t *bitmap_data; + GBitmap *bitmap; + GColor8 *palette; + bool is_palette_black_and_white:1; +}; + +typedef struct SimplyFont SimplyFont; + +struct SimplyFont { + SimplyResItemCommonMember; + GFont font; +}; + +SimplyRes *simply_res_create(); +void simply_res_destroy(SimplyRes *self); +void simply_res_clear(SimplyRes *self); + +SimplyImage *simply_res_add_bundled_image(SimplyRes *self, uint32_t id); +SimplyImage *simply_res_add_image(SimplyRes *self, uint32_t id, int16_t width, int16_t height, + uint8_t *pixels, uint16_t pixels_length); +SimplyImage *simply_res_auto_image(SimplyRes *self, uint32_t id, bool is_placeholder); +bool simply_res_evict_image(SimplyRes *self); + +GFont simply_res_add_custom_font(SimplyRes *self, uint32_t id); +GFont simply_res_auto_font(SimplyRes *self, uint32_t id); + +void simply_res_remove_image(SimplyRes *self, uint32_t id); diff --git a/src/simply_splash.c b/src/simply/simply_splash.c similarity index 50% rename from src/simply_splash.c rename to src/simply/simply_splash.c index 111d026d..2aa68756 100644 --- a/src/simply_splash.c +++ b/src/simply/simply_splash.c @@ -1,26 +1,37 @@ #include "simply_splash.h" -#include "simplyjs.h" +#include "simply.h" + +#include "util/graphics.h" #include -static void window_load(Window *window) { - SimplySplash *self = window_get_user_data(window); +void layer_update_callback(Layer *layer, GContext *ctx) { + SimplySplash *self = (SimplySplash*) window_get_user_data((Window*) layer); + + GRect frame = layer_get_frame(layer); + +#if defined(SPLASH_LOGO) + graphics_draw_bitmap_centered(ctx, self->image, frame); +#else + graphics_draw_bitmap_in_rect(ctx, self->image, frame); +#endif +} - self->logo = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_LOGO_SPLASH); - Layer *window_layer = window_get_root_layer(window); - GRect bounds = layer_get_bounds(window_layer); +static void window_load(Window *window) { + SimplySplash *self = window_get_user_data(window); - self->logo_layer = bitmap_layer_create(bounds); - bitmap_layer_set_bitmap(self->logo_layer, self->logo); - bitmap_layer_set_alignment(self->logo_layer, GAlignCenter); - layer_add_child(window_layer, bitmap_layer_get_layer(self->logo_layer)); +#if defined(SPLASH_LOGO) + self->image = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_LOGO_SPLASH); +#else + self->image = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_TILE_SPLASH); +#endif } static void window_disappear(Window *window) { SimplySplash *self = window_get_user_data(window); - bool animated = true; + bool animated = false; window_stack_remove(self->window, animated); simply_splash_destroy(self); } @@ -31,19 +42,21 @@ SimplySplash *simply_splash_create(Simply *simply) { self->window = window_create(); window_set_user_data(self->window, self); - window_set_fullscreen(self->window, true); - window_set_background_color(self->window, GColorBlack); + window_set_fullscreen(self->window, false); + window_set_background_color(self->window, GColorWhite); window_set_window_handlers(self->window, (WindowHandlers) { .load = window_load, .disappear = window_disappear, }); + layer_set_update_proc(window_get_root_layer(self->window), layer_update_callback); + return self; } void simply_splash_destroy(SimplySplash *self) { - bitmap_layer_destroy(self->logo_layer); - gbitmap_destroy(self->logo); + gbitmap_destroy(self->image); + window_destroy(self->window); self->simply->splash = NULL; diff --git a/src/simply_splash.h b/src/simply/simply_splash.h similarity index 78% rename from src/simply_splash.h rename to src/simply/simply_splash.h index eedff8ea..bf4bfdca 100644 --- a/src/simply_splash.h +++ b/src/simply/simply_splash.h @@ -1,6 +1,6 @@ #pragma once -#include "simplyjs.h" +#include "simply.h" #include @@ -9,8 +9,7 @@ typedef struct SimplySplash SimplySplash; struct SimplySplash { Simply *simply; Window *window; - BitmapLayer *logo_layer; - GBitmap *logo; + GBitmap *image; }; SimplySplash *simply_splash_create(Simply *simply); diff --git a/src/simply/simply_stage.c b/src/simply/simply_stage.c new file mode 100644 index 00000000..1b5106ad --- /dev/null +++ b/src/simply/simply_stage.c @@ -0,0 +1,771 @@ +#include "simply_stage.h" + +#include "simply_window.h" +#include "simply_res.h" +#include "simply_msg.h" +#include "simply_window_stack.h" + +#include "simply.h" + +#include "util/color.h" +#include "util/compat.h" +#include "util/graphics.h" +#include "util/inverter_layer.h" +#include "util/memory.h" +#include "util/string.h" +#include "util/window.h" + +#include + +typedef Packet StageClearPacket; + +typedef struct ElementInsertPacket ElementInsertPacket; + +struct __attribute__((__packed__)) ElementInsertPacket { + Packet packet; + uint32_t id; + SimplyElementType type:8; + uint16_t index; +}; + +typedef struct ElementRemovePacket ElementRemovePacket; + +struct __attribute__((__packed__)) ElementRemovePacket { + Packet packet; + uint32_t id; +}; + +typedef struct ElementCommonPacket ElementCommonPacket; + +struct __attribute__((__packed__)) ElementCommonPacket { + Packet packet; + uint32_t id; + GRect frame; + uint16_t border_width; + GColor8 background_color; + GColor8 border_color; +}; + +typedef struct ElementRadiusPacket ElementRadiusPacket; + +struct __attribute__((__packed__)) ElementRadiusPacket { + Packet packet; + uint32_t id; + uint16_t radius; +}; + +typedef struct ElementAnglePacket ElementAnglePacket; + +struct __attribute__((__packed__)) ElementAnglePacket { + Packet packet; + uint32_t id; + uint16_t angle; +}; + +typedef struct ElementAnglePacket ElementAngle2Packet; + +typedef struct ElementTextPacket ElementTextPacket; + +struct __attribute__((__packed__)) ElementTextPacket { + Packet packet; + uint32_t id; + TimeUnits time_units:8; + char text[]; +}; + +typedef struct ElementTextStylePacket ElementTextStylePacket; + +struct __attribute__((__packed__)) ElementTextStylePacket { + Packet packet; + uint32_t id; + GColor8 color; + GTextOverflowMode overflow_mode:8; + GTextAlignment alignment:8; + uint32_t custom_font; + char system_font[]; +}; + +typedef struct ElementImagePacket ElementImagePacket; + +struct __attribute__((__packed__)) ElementImagePacket { + Packet packet; + uint32_t id; + uint32_t image; + GCompOp compositing:8; +}; + +typedef struct ElementAnimatePacket ElementAnimatePacket; + +struct __attribute__((__packed__)) ElementAnimatePacket { + Packet packet; + uint32_t id; + GRect frame; + uint32_t duration; + AnimationCurve curve:8; +}; + +typedef struct ElementAnimateDonePacket ElementAnimateDonePacket; + +struct __attribute__((__packed__)) ElementAnimateDonePacket { + Packet packet; + uint32_t id; +}; + +static void simply_stage_clear(SimplyStage *self); + +static void simply_stage_update(SimplyStage *self); +static void simply_stage_update_ticker(SimplyStage *self); + +static SimplyElementCommon* simply_stage_auto_element(SimplyStage *self, uint32_t id, SimplyElementType type); +static SimplyElementCommon* simply_stage_insert_element(SimplyStage *self, int index, SimplyElementCommon *element); +static SimplyElementCommon* simply_stage_remove_element(SimplyStage *self, SimplyElementCommon *element); + +static void simply_stage_set_element_frame(SimplyStage *self, SimplyElementCommon *element, GRect frame); + +static SimplyAnimation *simply_stage_animate_element(SimplyStage *self, + SimplyElementCommon *element, SimplyAnimation* animation, GRect to_frame); + +static bool send_animate_element_done(SimplyMsg *self, uint32_t id) { + ElementAnimateDonePacket packet = { + .packet.type = CommandElementAnimateDone, + .packet.length = sizeof(packet), + .id = id, + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool id_filter(List1Node *node, void *data) { + return (((SimplyElementCommon*) node)->id == (uint32_t)(uintptr_t) data); +} + +static bool animation_filter(List1Node *node, void *data) { + return (((SimplyAnimation*) node)->animation == (PropertyAnimation*) data); +} + +static bool animation_element_filter(List1Node *node, void *data) { + return (((SimplyAnimation*) node)->element == (SimplyElementCommon*) data); +} + +static void destroy_element(SimplyStage *self, SimplyElementCommon *element) { + if (!element) { return; } + list1_remove(&self->stage_layer.elements, &element->node); + switch (element->type) { + default: break; + case SimplyElementTypeText: + free(((SimplyElementText*) element)->text); + break; + case SimplyElementTypeInverter: + inverter_layer_destroy(((SimplyElementInverter*) element)->inverter_layer); + break; + } + free(element); +} + +static void destroy_animation(SimplyStage *self, SimplyAnimation *animation) { + if (!animation) { return; } + property_animation_destroy(animation->animation); + list1_remove(&self->stage_layer.animations, &animation->node); + free(animation); +} + +void simply_stage_clear(SimplyStage *self) { + simply_window_action_bar_clear(&self->window); + + while (self->stage_layer.elements) { + destroy_element(self, (SimplyElementCommon*) self->stage_layer.elements); + } + + while (self->stage_layer.animations) { + destroy_animation(self, (SimplyAnimation*) self->stage_layer.animations); + } + + simply_stage_update_ticker(self); +} + +static void element_set_graphics_context(GContext *ctx, SimplyStage *self, + SimplyElementCommon *element) { + graphics_context_set_fill_color(ctx, element->background_color); + graphics_context_set_stroke_color(ctx, element->border_color); + graphics_context_set_stroke_width(ctx, element->border_width); +} + +static void rect_element_draw_background(GContext *ctx, SimplyStage *self, + SimplyElementRect *element) { + if (element->common.background_color.a) { + graphics_fill_rect(ctx, element->common.frame, element->radius, GCornersAll); + } +} + +static void rect_element_draw_border(GContext *ctx, SimplyStage *self, SimplyElementRect *element) { + if (element->common.border_color.a) { + graphics_draw_round_rect(ctx, element->common.frame, element->radius); + } +} + +static void rect_element_draw(GContext *ctx, SimplyStage *self, SimplyElementRect *element) { + rect_element_draw_background(ctx, self, element); + rect_element_draw_border(ctx, self, element); +} + +static void line_element_draw(GContext *ctx, SimplyStage *self, SimplyElementLine *element) { + if (element->border_color.a) { + const GPoint end = { element->frame.origin.x + element->frame.size.w, + element->frame.origin.y + element->frame.size.h }; + graphics_draw_line(ctx, element->frame.origin, end); + } +} + +static void circle_element_draw(GContext *ctx, SimplyStage *self, SimplyElementCircle *element) { + if (element->common.background_color.a) { + graphics_fill_circle(ctx, element->common.frame.origin, element->radius); + } + if (element->common.border_color.a) { + graphics_draw_circle(ctx, element->common.frame.origin, element->radius); + } +} + +static void prv_draw_line_polar(GContext *ctx, const GRect *outer_frame, const GRect *inner_frame, + GOvalScaleMode scale_mode, int32_t angle) { + const GPoint a = gpoint_from_polar(*outer_frame, scale_mode, angle); + const GPoint b = gpoint_from_polar(*inner_frame, scale_mode, angle); + graphics_draw_line(ctx, a, b); +} + +static void radial_element_draw(GContext *ctx, SimplyStage *self, SimplyElementRadial *element) { + const GOvalScaleMode scale_mode = GOvalScaleModeFitCircle; + const int32_t angle = DEG_TO_TRIGANGLE(element->angle); + const int32_t angle2 = DEG_TO_TRIGANGLE(element->angle2); + const GRect *frame = &element->rect.common.frame; + if (element->rect.common.background_color.a) { + graphics_fill_radial(ctx, *frame, scale_mode, element->rect.radius, angle, angle2); + } + if (element->rect.common.border_color.a && element->rect.common.border_width) { + graphics_draw_arc(ctx, *frame, scale_mode, angle, angle2); + if (element->rect.radius) { + GRect inner_frame = grect_inset(*frame, GEdgeInsets(element->rect.radius)); + if (inner_frame.size.w) { + prv_draw_line_polar(ctx, frame, &inner_frame, scale_mode, angle); + prv_draw_line_polar(ctx, frame, &inner_frame, scale_mode, angle2); + graphics_draw_arc(ctx, inner_frame, GOvalScaleModeFitCircle, angle, angle2); + } + } + } +} + +static char *format_time(char *format) { + time_t now = time(NULL); + struct tm* tm = localtime(&now); + static char time_text[256]; + strftime(time_text, sizeof(time_text), format, tm); + return time_text; +} + +static void text_element_draw(GContext *ctx, SimplyStage *self, SimplyElementText *element) { + rect_element_draw(ctx, self, &element->rect); + char *text = element->text; + if (element->text_color.a && is_string(text)) { + if (element->time_units) { + text = format_time(text); + } + GFont font = element->font ? element->font : fonts_get_system_font(FONT_KEY_GOTHIC_14); + graphics_context_set_text_color(ctx, gcolor8_get(element->text_color)); + graphics_draw_text(ctx, text, font, element->rect.common.frame, element->overflow_mode, + element->alignment, NULL); + } +} + +static void image_element_draw(GContext *ctx, SimplyStage *self, SimplyElementImage *element) { + graphics_context_set_compositing_mode(ctx, element->compositing); + rect_element_draw_background(ctx, self, &element->rect); + SimplyImage *image = simply_res_get_image(self->window.simply->res, element->image); + if (image && image->bitmap) { + GRect frame = element->rect.common.frame; + if (frame.size.w == 0 && frame.size.h == 0) { + frame = gbitmap_get_bounds(image->bitmap); + } + graphics_draw_bitmap_centered(ctx, image->bitmap, frame); + } + rect_element_draw_border(ctx, self, &element->rect); + graphics_context_set_compositing_mode(ctx, GCompOpAssign); +} + +static void layer_update_callback(Layer *layer, GContext *ctx) { + SimplyStage *self = *(void**) layer_get_data(layer); + + GRect frame = layer_get_frame(layer); + frame.origin = scroll_layer_get_content_offset(self->window.scroll_layer); + frame.origin.x = -frame.origin.x; + frame.origin.y = -frame.origin.y; + + graphics_context_set_antialiased(ctx, true); + + graphics_context_set_fill_color(ctx, gcolor8_get(self->window.background_color)); + graphics_fill_rect(ctx, frame, 0, GCornerNone); + + SimplyElementCommon *element = (SimplyElementCommon *)self->stage_layer.elements; + while (element) { + element_set_graphics_context(ctx, self, element); + int16_t max_y = element->frame.origin.y + element->frame.size.h; + if (max_y > frame.size.h) { + frame.size.h = max_y; + } + switch (element->type) { + case SimplyElementTypeNone: + break; + case SimplyElementTypeRect: + rect_element_draw(ctx, self, (SimplyElementRect *)element); + break; + case SimplyElementTypeLine: + line_element_draw(ctx, self, (SimplyElementLine *)element); + break; + case SimplyElementTypeCircle: + circle_element_draw(ctx, self, (SimplyElementCircle *)element); + break; + case SimplyElementTypeRadial: + radial_element_draw(ctx, self, (SimplyElementRadial *)element); + break; + case SimplyElementTypeText: + text_element_draw(ctx, self, (SimplyElementText *)element); + break; + case SimplyElementTypeImage: + image_element_draw(ctx, self, (SimplyElementImage *)element); + break; + case SimplyElementTypeInverter: + break; + } + element = (SimplyElementCommon*) element->node.next; + } + + if (self->window.is_scrollable) { + frame.origin = GPointZero; + layer_set_frame(layer, frame); + const GSize content_size = scroll_layer_get_content_size(self->window.scroll_layer); + if (!gsize_equal(&frame.size, &content_size)) { + scroll_layer_set_content_size(self->window.scroll_layer, frame.size); + } + } +} + +static size_t prv_get_element_size(SimplyElementType type) { + switch (type) { + case SimplyElementTypeNone: return 0; + case SimplyElementTypeLine: return sizeof(SimplyElementLine); + case SimplyElementTypeRect: return sizeof(SimplyElementRect); + case SimplyElementTypeCircle: return sizeof(SimplyElementCircle); + case SimplyElementTypeRadial: return sizeof(SimplyElementRadial); + case SimplyElementTypeText: return sizeof(SimplyElementText); + case SimplyElementTypeImage: return sizeof(SimplyElementImage); + case SimplyElementTypeInverter: return sizeof(SimplyElementInverter); + } + return 0; +} + +static SimplyElementCommon *prv_create_element(SimplyElementType type) { + SimplyElementCommon *common = malloc0(prv_get_element_size(type)); + if (!common) { + return NULL; + } + switch (type) { + default: return common; + case SimplyElementTypeInverter: { + SimplyElementInverter *element = (SimplyElementInverter *)common; + element->inverter_layer = inverter_layer_create(GRect(0, 0, 0, 0)); + return common; + } + } + return common; +} + +SimplyElementCommon *simply_stage_auto_element(SimplyStage *self, uint32_t id, SimplyElementType type) { + if (!id) { + return NULL; + } + SimplyElementCommon *element = (SimplyElementCommon*) list1_find( + self->stage_layer.elements, id_filter, (void*)(uintptr_t) id); + if (element) { + return element; + } + while (!(element = prv_create_element(type))) { + if (!simply_res_evict_image(self->window.simply->res)) { + return NULL; + } + } + element->id = id; + element->type = type; + return element; +} + +SimplyElementCommon *simply_stage_insert_element(SimplyStage *self, int index, SimplyElementCommon *element) { + simply_stage_remove_element(self, element); + switch (element->type) { + default: break; + case SimplyElementTypeInverter: + layer_add_child(self->stage_layer.layer, + inverter_layer_get_layer(((SimplyElementInverter*) element)->inverter_layer)); + break; + } + return (SimplyElementCommon*) list1_insert(&self->stage_layer.elements, index, &element->node); +} + +SimplyElementCommon *simply_stage_remove_element(SimplyStage *self, SimplyElementCommon *element) { + switch (element->type) { + default: break; + case SimplyElementTypeInverter: + layer_remove_from_parent(inverter_layer_get_layer(((SimplyElementInverter*) element)->inverter_layer)); + break; + } + return (SimplyElementCommon*) list1_remove(&self->stage_layer.elements, &element->node); +} + +void simply_stage_set_element_frame(SimplyStage *self, SimplyElementCommon *element, GRect frame) { + if (element->type != SimplyElementTypeLine) { + grect_standardize(&frame); + } + element->frame = frame; + switch (element->type) { + default: break; + case SimplyElementTypeInverter: { + Layer *layer = inverter_layer_get_layer(((SimplyElementInverter*) element)->inverter_layer); + layer_set_frame(layer, element->frame); + break; + } + } +} + +static void element_frame_setter(void *subject, GRect frame) { + SimplyAnimation *animation = subject; + simply_stage_set_element_frame(animation->stage, animation->element, frame); + simply_stage_update(animation->stage); +} + +static GRect element_frame_getter(void *subject) { + SimplyAnimation *animation = subject; + return animation->element->frame; +} + +static void animation_stopped(Animation *base_animation, bool finished, void *context) { + SimplyStage *self = context; + SimplyAnimation *animation = (SimplyAnimation*) list1_find( + self->stage_layer.animations, animation_filter, base_animation); + if (!animation) { + return; + } + SimplyElementCommon *element = animation->element; + destroy_animation(self, animation); + send_animate_element_done(self->window.simply->msg, element->id); +} + +SimplyAnimation *simply_stage_animate_element(SimplyStage *self, + SimplyElementCommon *element, SimplyAnimation* animation, GRect to_frame) { + if (!animation) { + return NULL; + } + SimplyAnimation *prev_animation = (SimplyAnimation*) list1_find( + self->stage_layer.animations, animation_element_filter, element); + if (prev_animation) { + animation_unschedule((Animation*) prev_animation->animation); + } + + animation->stage = self; + animation->element = element; + + static const PropertyAnimationImplementation implementation = { + .base = { + .update = (AnimationUpdateImplementation) property_animation_update_grect, + .teardown = (AnimationTeardownImplementation) animation_destroy, + }, + .accessors = { + .setter = { .grect = (const GRectSetter) element_frame_setter }, + .getter = { .grect = (const GRectGetter) element_frame_getter }, + }, + }; + + PropertyAnimation *property_animation = property_animation_create(&implementation, animation, NULL, NULL); + if (!property_animation) { + free(animation); + return NULL; + } + + property_animation_set_from_grect(property_animation, &element->frame); + property_animation_set_to_grect(property_animation, &to_frame); + + animation->animation = property_animation; + list1_append(&self->stage_layer.animations, &animation->node); + + Animation *base_animation = (Animation*) property_animation; + animation_set_duration(base_animation, animation->duration); + animation_set_curve(base_animation, animation->curve); + animation_set_handlers(base_animation, (AnimationHandlers) { + .stopped = animation_stopped, + }, self); + + animation_schedule(base_animation); + + return animation; +} + +static void window_load(Window *window) { + SimplyStage * const self = window_get_user_data(window); + + simply_window_load(&self->window); + + // Stage does not yet support text flow + scroll_layer_set_paging(self->window.scroll_layer, false); + + Layer * const window_layer = window_get_root_layer(window); + const GRect frame = { .size = layer_get_frame(window_layer).size }; + + Layer * const layer = layer_create_with_data(frame, sizeof(void *)); + self->stage_layer.layer = layer; + *(void**) layer_get_data(layer) = self; + layer_set_update_proc(layer, layer_update_callback); + scroll_layer_add_child(self->window.scroll_layer, layer); + self->window.use_scroll_layer = true; +} + +static void window_appear(Window *window) { + SimplyStage *self = window_get_user_data(window); + simply_window_appear(&self->window); + + simply_stage_update_ticker(self); +} + +static void window_disappear(Window *window) { + SimplyStage *self = window_get_user_data(window); + if (simply_window_disappear(&self->window)) { + simply_res_clear(self->window.simply->res); + simply_stage_clear(self); + } +} + +static void window_unload(Window *window) { + SimplyStage *self = window_get_user_data(window); + + layer_destroy(self->stage_layer.layer); + self->window.layer = self->stage_layer.layer = NULL; + + simply_window_unload(&self->window); +} + +void simply_stage_update(SimplyStage *self) { + if (self->stage_layer.layer) { + layer_mark_dirty(self->stage_layer.layer); + } +} + +static void handle_tick(struct tm *tick_time, TimeUnits units_changed) { + window_stack_schedule_top_window_render(); +} + +void simply_stage_update_ticker(SimplyStage *self) { + TimeUnits units = 0; + + SimplyElementCommon *element = (SimplyElementCommon*) self->stage_layer.elements; + while (element) { + if (element->type == SimplyElementTypeText) { + units |= ((SimplyElementText*) element)->time_units; + } + element = (SimplyElementCommon*) element->node.next; + } + + if (units) { + tick_timer_service_subscribe(units, handle_tick); + } else { + tick_timer_service_unsubscribe(); + } +} + +static void handle_stage_clear_packet(Simply *simply, Packet *data) { + simply_stage_clear(simply->stage); +} + +static void handle_element_insert_packet(Simply *simply, Packet *data) { + ElementInsertPacket *packet = (ElementInsertPacket*) data; + SimplyElementCommon *element = simply_stage_auto_element(simply->stage, packet->id, packet->type); + if (!element) { + return; + } + simply_stage_insert_element(simply->stage, packet->index, element); + simply_stage_update(simply->stage); +} + +static void handle_element_remove_packet(Simply *simply, Packet *data) { + ElementInsertPacket *packet = (ElementInsertPacket*) data; + SimplyElementCommon *element = simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + simply_stage_remove_element(simply->stage, element); + simply_stage_update(simply->stage); +} + +static void handle_element_common_packet(Simply *simply, Packet *data) { + ElementCommonPacket *packet = (ElementCommonPacket*) data; + SimplyElementCommon *element = simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + simply_stage_set_element_frame(simply->stage, element, packet->frame); + element->background_color = packet->background_color; + element->border_color = packet->border_color; + element->border_width = packet->border_width; + simply_stage_update(simply->stage); +} + +static void handle_element_radius_packet(Simply *simply, Packet *data) { + ElementRadiusPacket *packet = (ElementRadiusPacket*) data; + SimplyElementRect *element = (SimplyElementRect*) simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + element->radius = packet->radius; + simply_stage_update(simply->stage); +}; + +static void handle_element_angle_packet(Simply *simply, Packet *data) { + ElementAnglePacket *packet = (ElementAnglePacket *)data; + SimplyElementRadial *element = + (SimplyElementRadial *)simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + element->angle = packet->angle; + simply_stage_update(simply->stage); +}; + +static void handle_element_angle2_packet(Simply *simply, Packet *data) { + ElementAngle2Packet *packet = (ElementAngle2Packet *)data; + SimplyElementRadial *element = + (SimplyElementRadial *)simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + element->angle2 = packet->angle; + simply_stage_update(simply->stage); +}; + +static void handle_element_text_packet(Simply *simply, Packet *data) { + ElementTextPacket *packet = (ElementTextPacket*) data; + SimplyElementText *element = (SimplyElementText*) simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + if (element->time_units != packet->time_units) { + element->time_units = packet->time_units; + simply_stage_update_ticker(simply->stage); + } + strset(&element->text, packet->text); + simply_stage_update(simply->stage); +} + +static void handle_element_text_style_packet(Simply *simply, Packet *data) { + ElementTextStylePacket *packet = (ElementTextStylePacket*) data; + SimplyElementText *element = (SimplyElementText*) simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + element->text_color = packet->color; + element->overflow_mode = packet->overflow_mode; + element->alignment = packet->alignment; + if (packet->custom_font) { + element->font = simply_res_get_font(simply->res, packet->custom_font); + } else if (packet->system_font[0]) { + element->font = fonts_get_system_font(packet->system_font); + } + simply_stage_update(simply->stage); +} + +static void handle_element_image_packet(Simply *simply, Packet *data) { + ElementImagePacket *packet = (ElementImagePacket*) data; + SimplyElementImage *element = (SimplyElementImage*) simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + element->image = packet->image; + element->compositing = packet->compositing; + simply_stage_update(simply->stage); +} + +static void handle_element_animate_packet(Simply *simply, Packet *data) { + ElementAnimatePacket *packet = (ElementAnimatePacket*) data; + SimplyElementCommon *element = simply_stage_get_element(simply->stage, packet->id); + if (!element) { + return; + } + SimplyAnimation *animation = NULL; + while (!(animation = malloc0(sizeof(*animation)))) { + if (!simply_res_evict_image(simply->res)) { + return; + } + } + animation->duration = packet->duration; + animation->curve = packet->curve; + simply_stage_animate_element(simply->stage, element, animation, packet->frame); +} + +bool simply_stage_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandStageClear: + handle_stage_clear_packet(simply, packet); + return true; + case CommandElementInsert: + handle_element_insert_packet(simply, packet); + return true; + case CommandElementRemove: + handle_element_remove_packet(simply, packet); + return true; + case CommandElementCommon: + handle_element_common_packet(simply, packet); + return true; + case CommandElementRadius: + handle_element_radius_packet(simply, packet); + return true; + case CommandElementAngle: + handle_element_angle_packet(simply, packet); + return true; + case CommandElementAngle2: + handle_element_angle2_packet(simply, packet); + return true; + case CommandElementText: + handle_element_text_packet(simply, packet); + return true; + case CommandElementTextStyle: + handle_element_text_style_packet(simply, packet); + return true; + case CommandElementImage: + handle_element_image_packet(simply, packet); + return true; + case CommandElementAnimate: + handle_element_animate_packet(simply, packet); + return true; + } + return false; +} + +SimplyStage *simply_stage_create(Simply *simply) { + SimplyStage *self = malloc(sizeof(*self)); + *self = (SimplyStage) { .window.simply = simply }; + + static const WindowHandlers s_window_handlers = { + .load = window_load, + .appear = window_appear, + .disappear = window_disappear, + .unload = window_unload, + }; + self->window.window_handlers = &s_window_handlers; + + simply_window_init(&self->window, simply); + simply_window_set_background_color(&self->window, GColor8Black); + + return self; +} + +void simply_stage_destroy(SimplyStage *self) { + if (!self) { + return; + } + + simply_window_deinit(&self->window); + + free(self); +} diff --git a/src/simply/simply_stage.h b/src/simply/simply_stage.h new file mode 100644 index 00000000..e9714f51 --- /dev/null +++ b/src/simply/simply_stage.h @@ -0,0 +1,119 @@ +#pragma once + +#include "simply_window.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include "util/inverter_layer.h" +#include "util/list1.h" +#include "util/color.h" + +#include + +#define simply_stage_get_element(self, id) simply_stage_auto_element(self, id, SimplyElementTypeNone) + +typedef struct SimplyStageLayer SimplyStageLayer; + +typedef struct SimplyStage SimplyStage; + +typedef struct SimplyStageItem SimplyStageItem; + +typedef enum SimplyElementType SimplyElementType; + +enum SimplyElementType { + SimplyElementTypeNone = 0, + SimplyElementTypeRect, + SimplyElementTypeLine, + SimplyElementTypeCircle, + SimplyElementTypeRadial, + SimplyElementTypeText, + SimplyElementTypeImage, + SimplyElementTypeInverter, +}; + +struct SimplyStageLayer { + Layer *layer; + List1Node *elements; + List1Node *animations; +}; + +struct SimplyStage { + SimplyWindow window; + SimplyStageLayer stage_layer; +}; + +typedef struct SimplyElementCommon SimplyElementCommon; + +struct SimplyElementCommon { + List1Node node; + uint32_t id; + SimplyElementType type; + GRect frame; + uint16_t border_width; + GColor8 background_color; + GColor8 border_color; +}; + +typedef struct SimplyElementCommon SimplyElementLine; + +typedef struct SimplyElementRect SimplyElementRect; + +struct SimplyElementRect { + SimplyElementCommon common; + uint16_t radius; +}; + +typedef struct SimplyElementRect SimplyElementCircle; + +typedef struct SimplyElementRadial SimplyElementRadial; + +struct SimplyElementRadial { + SimplyElementRect rect; + uint16_t angle; + uint16_t angle2; +}; + +typedef struct SimplyElementText SimplyElementText; + +struct SimplyElementText { + SimplyElementRect rect; + char *text; + GFont font; + TimeUnits time_units:8; + GColor8 text_color; + GTextOverflowMode overflow_mode:2; + GTextAlignment alignment:2; +}; + +typedef struct SimplyElementImage SimplyElementImage; + +struct SimplyElementImage { + SimplyElementRect rect; + uint32_t image; + GCompOp compositing; +}; + +typedef struct SimplyElementInverter SimplyElementInverter; + +struct SimplyElementInverter { + SimplyElementCommon common; + InverterLayer *inverter_layer; +}; + +typedef struct SimplyAnimation SimplyAnimation; + +struct SimplyAnimation { + List1Node node; + SimplyStage *stage; + SimplyElementCommon *element; + PropertyAnimation *animation; + uint32_t duration; + AnimationCurve curve; +}; + +SimplyStage *simply_stage_create(Simply *simply); +void simply_stage_destroy(SimplyStage *self); + +bool simply_stage_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_ui.c b/src/simply/simply_ui.c new file mode 100644 index 00000000..abb9b6a1 --- /dev/null +++ b/src/simply/simply_ui.c @@ -0,0 +1,502 @@ +#include "simply_ui.h" + +#include "simply_msg.h" +#include "simply_res.h" +#include "simply_window_stack.h" + +#include "simply.h" + +#include "util/compat.h" +#include "util/color.h" +#include "util/graphics.h" +#include "util/graphics_text.h" +#include "util/math.h" +#include "util/noop.h" +#include "util/string.h" +#include "util/window.h" + +#include + +struct __attribute__((__packed__)) SimplyStyle { + const char *title_font; + const char *subtitle_font; + const char *body_font; + int custom_body_font_id; + int8_t title_icon_padding; + int8_t title_padding; + int8_t subtitle_padding; +}; + +enum ClearIndex { + ClearIndex_Action = 0, + ClearIndex_Text, + ClearIndex_Image, + ClearIndex_Style, +}; + +enum StyleIndex { + StyleIndex_ClassicSmall = 0, + StyleIndex_ClassicLarge, + StyleIndex_Mono, + StyleIndex_Small, + StyleIndex_Large, + StyleIndexCount, +}; + +#define StyleIndex_Default StyleIndex_ClassicLarge + +static const SimplyStyle STYLES[StyleIndexCount] = { + [StyleIndex_ClassicSmall] = { + .title_font = FONT_KEY_GOTHIC_18_BOLD, + .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, + .body_font = FONT_KEY_GOTHIC_18, + }, + [StyleIndex_ClassicLarge] = { + .title_font = FONT_KEY_GOTHIC_28_BOLD, + .subtitle_font = FONT_KEY_GOTHIC_28, + .body_font = FONT_KEY_GOTHIC_24_BOLD, + }, + [StyleIndex_Mono] = { + .title_font = FONT_KEY_GOTHIC_24_BOLD, + .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, + .custom_body_font_id = RESOURCE_ID_MONO_FONT_14, + }, + [StyleIndex_Small] = { + .title_icon_padding = 4, + .title_font = FONT_KEY_GOTHIC_18_BOLD, + .title_padding = 2, + .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, + .subtitle_padding = 3, + .body_font = FONT_KEY_GOTHIC_18, + }, + [StyleIndex_Large] = { + .title_icon_padding = 4, + .title_font = FONT_KEY_GOTHIC_18_BOLD, + .title_padding = 3, + .subtitle_font = FONT_KEY_GOTHIC_24_BOLD, + .subtitle_padding = 4, + .body_font = FONT_KEY_GOTHIC_24_BOLD, + }, +}; + +typedef struct CardClearPacket CardClearPacket; + +struct __attribute__((__packed__)) CardClearPacket { + Packet packet; + uint8_t flags; +}; + +typedef struct CardTextPacket CardTextPacket; + +struct __attribute__((__packed__)) CardTextPacket { + Packet packet; + uint8_t index; + GColor8 color; + char text[]; +}; + +typedef struct CardImagePacket CardImagePacket; + +struct __attribute__((__packed__)) CardImagePacket { + Packet packet; + uint32_t image; + uint8_t index; +}; + +typedef struct CardStylePacket CardStylePacket; + +struct __attribute__((__packed__)) CardStylePacket { + Packet packet; + uint8_t style; +}; + +static void mark_dirty(SimplyUi *self) { + if (self->ui_layer.layer) { + layer_mark_dirty(self->ui_layer.layer); + } +} + +void simply_ui_clear(SimplyUi *self, uint32_t clear_mask) { + if (clear_mask & (1 << ClearIndex_Action)) { + simply_window_action_bar_clear(&self->window); + } + if (clear_mask & (1 << ClearIndex_Text)) { + for (int textfield_id = 0; textfield_id < NumUiTextfields; ++textfield_id) { + simply_ui_set_text(self, textfield_id, NULL); + simply_ui_set_text_color(self, textfield_id, GColor8Black); + } + } + if (clear_mask & (1 << ClearIndex_Image)) { + memset(self->ui_layer.imagefields, 0, sizeof(self->ui_layer.imagefields)); + } + if (clear_mask & (1 << ClearIndex_Style)) { + simply_ui_set_style(self, StyleIndex_Default); + } +} + +void simply_ui_set_style(SimplyUi *self, int style_index) { + if (self->ui_layer.custom_body_font) { + fonts_unload_custom_font(self->ui_layer.custom_body_font); + self->ui_layer.custom_body_font = NULL; + } + self->ui_layer.style = &STYLES[style_index]; + if (self->ui_layer.style->custom_body_font_id) { + self->ui_layer.custom_body_font = fonts_load_custom_font( + resource_get_handle(self->ui_layer.style->custom_body_font_id)); + } + mark_dirty(self); +} + +void simply_ui_set_text(SimplyUi *self, SimplyUiTextfieldId textfield_id, const char *str) { + SimplyUiTextfield *textfield = &self->ui_layer.textfields[textfield_id]; + char **str_field = &textfield->text; + strset_truncated(str_field, str); + mark_dirty(self); +} + +void simply_ui_set_text_color(SimplyUi *self, SimplyUiTextfieldId textfield_id, GColor8 color) { + SimplyUiTextfield *textfield = &self->ui_layer.textfields[textfield_id]; + textfield->color = color; + mark_dirty(self); +} + +static void enable_text_flow_and_paging(SimplyUi *self, GTextAttributes *text_attributes, + const GRect *box) { + graphics_text_attributes_enable_paging_on_layer( + text_attributes, (Layer *)self->window.scroll_layer, box, TEXT_FLOW_DEFAULT_INSET); +} + +static void layer_update_callback(Layer *layer, GContext *ctx) { + SimplyUi *self = *(void **)layer_get_data(layer); + + const GTextAlignment text_align = + PBL_IF_ROUND_ELSE((self->window.use_action_bar ? GTextAlignmentRight : GTextAlignmentCenter), + GTextAlignmentLeft); + + GRect window_frame = { + .size = layer_get_frame(scroll_layer_get_layer(self->window.scroll_layer)).size, + }; + GRect frame = window_frame; + + const SimplyStyle *style = self->ui_layer.style; + GFont title_font = fonts_get_system_font(style->title_font); + GFont subtitle_font = fonts_get_system_font(style->subtitle_font); + GFont body_font = self->ui_layer.custom_body_font ? + self->ui_layer.custom_body_font : fonts_get_system_font(style->body_font); + + const int16_t margin_x = 5; + const int16_t margin_top = 2; + const int16_t margin_bottom = self->window.is_scrollable ? 10 : margin_top; + const int16_t image_offset_y = 3; + + GRect text_frame = frame; + text_frame.size.w -= 2 * margin_x; + text_frame.size.h += INT16_MAX / 2; + GPoint cursor = { margin_x, margin_top }; + + if (self->window.use_action_bar) { + text_frame.size.w -= ACTION_BAR_WIDTH + PBL_IF_ROUND_ELSE(TEXT_FLOW_DEFAULT_INSET, 0); + window_frame.size.w -= ACTION_BAR_WIDTH; + } + + graphics_context_set_text_color(ctx, GColorBlack); + + const SimplyUiTextfield *title = &self->ui_layer.textfields[UiTitle]; + const SimplyUiTextfield *subtitle = &self->ui_layer.textfields[UiSubtitle]; + const SimplyUiTextfield *body = &self->ui_layer.textfields[UiBody]; + + GTextAttributes *title_attributes = graphics_text_attributes_create(); + GTextAttributes *subtitle_attributes = graphics_text_attributes_create(); + GTextAttributes *body_attributes = graphics_text_attributes_create(); + + bool has_title = is_string(title->text); + bool has_subtitle = is_string(subtitle->text); + bool has_body = is_string(body->text); + + GSize title_size, subtitle_size; + GPoint title_pos, subtitle_pos, image_pos = GPointZero; + GRect body_rect; + + SimplyImage *title_icon = simply_res_get_image( + self->window.simply->res, self->ui_layer.imagefields[UiTitleIcon]); + SimplyImage *subtitle_icon = simply_res_get_image( + self->window.simply->res, self->ui_layer.imagefields[UiSubtitleIcon]); + SimplyImage *body_image = simply_res_get_image( + self->window.simply->res, self->ui_layer.imagefields[UiBodyImage]); + + GRect title_icon_bounds = + title_icon ? gbitmap_get_bounds(title_icon->bitmap) : GRectZero; + GRect subtitle_icon_bounds = + subtitle_icon ? gbitmap_get_bounds(subtitle_icon->bitmap) : GRectZero; + GRect body_image_bounds; + + if (has_title) { + GRect title_frame = { cursor, text_frame.size }; + if (title_icon) { + title_icon_bounds.origin = title_frame.origin; + title_icon_bounds.origin.y += image_offset_y; + PBL_IF_RECT_ELSE({ + title_frame.origin.x += title_icon_bounds.size.w; + title_frame.size.w -= title_icon_bounds.size.w; + }, { + title_frame.origin.y += title_icon_bounds.size.h + style->title_icon_padding; + }); + } + PBL_IF_ROUND_ELSE( + enable_text_flow_and_paging(self, title_attributes, &title_frame), NOOP); + title_size = graphics_text_layout_get_content_size_with_attributes( + title->text, title_font, title_frame, GTextOverflowModeWordWrap, text_align, + title_attributes); + title_size.w = title_frame.size.w; + title_pos = title_frame.origin; + cursor.y = title_frame.origin.y + title_size.h + style->title_padding; + } + + if (has_subtitle) { + GRect subtitle_frame = { cursor, text_frame.size }; + if (subtitle_icon) { + subtitle_icon_bounds.origin = subtitle_frame.origin; + subtitle_icon_bounds.origin.y += image_offset_y; + PBL_IF_RECT_ELSE({ + subtitle_frame.origin.x += subtitle_icon_bounds.size.w; + subtitle_frame.size.w -= subtitle_icon_bounds.size.w; + }, { + subtitle_frame.origin.y += subtitle_icon_bounds.size.h; + }); + } + PBL_IF_ROUND_ELSE( + enable_text_flow_and_paging(self, subtitle_attributes, &subtitle_frame), NOOP); + subtitle_size = graphics_text_layout_get_content_size_with_attributes( + subtitle->text, subtitle_font, subtitle_frame, GTextOverflowModeWordWrap, text_align, + subtitle_attributes); + subtitle_size.w = subtitle_frame.size.w; + subtitle_pos = subtitle_frame.origin; + if (subtitle_icon) { + subtitle_pos.x += subtitle_icon_bounds.size.w; + } + cursor.y = subtitle_frame.origin.y + subtitle_size.h + style->subtitle_padding; + } + + if (body_image) { + body_image_bounds = gbitmap_get_bounds(body_image->bitmap); + image_pos = cursor; + cursor.y += body_image_bounds.size.h; + } + + if (has_body) { + body_rect = (GRect) { cursor, (self->window.is_scrollable ? text_frame.size : frame.size) }; + body_rect.origin = cursor; + body_rect.size.w = text_frame.size.w; + body_rect.size.h -= cursor.y + margin_bottom; + PBL_IF_ROUND_ELSE( + enable_text_flow_and_paging(self, body_attributes, &body_rect), NOOP); + GSize body_size = graphics_text_layout_get_content_size_with_attributes( + body->text, body_font, body_rect, GTextOverflowModeWordWrap, text_align, body_attributes); + body_size.w = body_rect.size.w; + cursor.y = body_rect.origin.y + body_size.h; + if (self->window.is_scrollable) { + body_rect.size = body_size; + const int new_height = cursor.y + margin_bottom; + frame.size.h = MAX(window_frame.size.h, new_height); + const GSize content_size = scroll_layer_get_content_size(self->window.scroll_layer); + if (!gsize_equal(&frame.size, &content_size)) { + layer_set_frame(layer, frame); + scroll_layer_set_content_size(self->window.scroll_layer, frame.size); + } + } else if (!self->ui_layer.custom_body_font && body_size.h > body_rect.size.h) { + body_font = fonts_get_system_font(FONT_KEY_GOTHIC_18); + } + // For rendering text descenders + body_rect.size.h += margin_bottom; + } + + IF_SDK_2_ELSE(({ + graphics_context_set_fill_color(ctx, GColorBlack); + graphics_fill_rect(ctx, frame, 0, GCornerNone); + }), NONE); + + graphics_context_set_fill_color(ctx, gcolor8_get_or(self->window.background_color, GColorWhite)); + const int radius = IF_SDK_2_ELSE(4, 0); + graphics_fill_rect(ctx, frame, radius, GCornersAll); + + if (title_icon) { + GRect icon_frame = title_icon_bounds; + icon_frame.origin.x = + PBL_IF_ROUND_ELSE((frame.size.w - title_icon_bounds.size.w) / 2, margin_x); + PBL_IF_RECT_ELSE(icon_frame.size.h = title_size.h, NOOP); + graphics_context_set_alpha_blended(ctx, true); + graphics_draw_bitmap_centered(ctx, title_icon->bitmap, icon_frame); + } + if (has_title) { + graphics_context_set_text_color(ctx, gcolor8_get_or(title->color, GColorBlack)); + graphics_draw_text(ctx, title->text, title_font, (GRect) { title_pos, title_size }, + GTextOverflowModeWordWrap, text_align, title_attributes); + } + + if (subtitle_icon) { + GRect subicon_frame = subtitle_icon_bounds; + subicon_frame.origin.x = + PBL_IF_ROUND_ELSE((frame.size.w - subtitle_icon_bounds.size.w) / 2, margin_x); + PBL_IF_RECT_ELSE(subicon_frame.size.h = subtitle_size.h, NOOP); + graphics_context_set_alpha_blended(ctx, true); + graphics_draw_bitmap_centered(ctx, subtitle_icon->bitmap, subicon_frame); + } + if (has_subtitle) { + graphics_context_set_text_color(ctx, gcolor8_get_or(subtitle->color, GColorBlack)); + graphics_draw_text(ctx, subtitle->text, subtitle_font, (GRect) { subtitle_pos, subtitle_size }, + GTextOverflowModeWordWrap, text_align, subtitle_attributes); + } + + if (body_image) { + GRect image_frame = (GRect) { + .origin = { + PBL_IF_ROUND_ELSE(((frame.size.w - body_rect.size.w) / 2), 0), + image_pos.y + image_offset_y, + }, + .size = { window_frame.size.w, body_image_bounds.size.h } + }; + graphics_context_set_alpha_blended(ctx, true); + graphics_draw_bitmap_centered(ctx, body_image->bitmap, image_frame); + } + if (has_body) { + graphics_context_set_text_color(ctx, gcolor8_get_or(body->color, GColorBlack)); + graphics_draw_text(ctx, body->text, body_font, body_rect, + GTextOverflowModeTrailingEllipsis, text_align, body_attributes); + } + + graphics_text_attributes_destroy(title_attributes); + graphics_text_attributes_destroy(subtitle_attributes); + graphics_text_attributes_destroy(body_attributes); +} + +static void show_welcome_text(SimplyUi *self) { + if (simply_msg_has_communicated()) { + return; + } + + simply_msg_show_disconnected(self->window.simply->msg); +} + +static void window_load(Window *window) { + SimplyUi * const self = window_get_user_data(window); + + simply_window_load(&self->window); + + Layer * const window_layer = window_get_root_layer(window); + const GRect frame = { .size = layer_get_frame(window_layer).size }; + + Layer * const layer = layer_create_with_data(frame, sizeof(void *)); + self->ui_layer.layer = layer; + *(void**) layer_get_data(layer) = self; + layer_set_update_proc(layer, layer_update_callback); + scroll_layer_add_child(self->window.scroll_layer, layer); + self->window.use_scroll_layer = true; + + simply_ui_set_style(self, StyleIndex_Default); +} + +static void window_appear(Window *window) { + SimplyUi *self = window_get_user_data(window); + simply_window_appear(&self->window); +} + +static void window_disappear(Window *window) { + SimplyUi *self = window_get_user_data(window); + if (simply_window_disappear(&self->window)) { + simply_res_clear(self->window.simply->res); + } +} + +static void window_unload(Window *window) { + SimplyUi *self = window_get_user_data(window); + + layer_destroy(self->ui_layer.layer); + self->window.layer = self->ui_layer.layer = NULL; + + simply_window_unload(&self->window); +} + +static void handle_card_clear_packet(Simply *simply, Packet *data) { + CardClearPacket *packet = (CardClearPacket*) data; + simply_ui_clear(simply->ui, packet->flags); +} + +static void handle_card_text_packet(Simply *simply, Packet *data) { + CardTextPacket *packet = (CardTextPacket*) data; + SimplyUiTextfieldId textfield_id = packet->index; + if (textfield_id >= NumUiTextfields) { + return; + } + simply_ui_set_text(simply->ui, textfield_id, packet->text); + if (!gcolor8_equal(packet->color, GColor8ClearWhite)) { + simply_ui_set_text_color(simply->ui, textfield_id, packet->color); + } +} + +static void handle_card_image_packet(Simply *simply, Packet *data) { + CardImagePacket *packet = (CardImagePacket*) data; + SimplyUiImagefieldId imagefield_id = packet->index; + if (imagefield_id >= NumUiImagefields) { + return; + } + simply->ui->ui_layer.imagefields[imagefield_id] = packet->image; + window_stack_schedule_top_window_render(); +} + +static void handle_card_style_packet(Simply *simply, Packet *data) { + CardStylePacket *packet = (CardStylePacket*) data; + simply_ui_set_style(simply->ui, packet->style); +} + +bool simply_ui_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandCardClear: + handle_card_clear_packet(simply, packet); + return true; + case CommandCardText: + handle_card_text_packet(simply, packet); + return true; + case CommandCardImage: + handle_card_image_packet(simply, packet); + return true; + case CommandCardStyle: + handle_card_style_packet(simply, packet); + return true; + } + return false; +} + +SimplyUi *simply_ui_create(Simply *simply) { + SimplyUi *self = malloc(sizeof(*self)); + *self = (SimplyUi) { .window.layer = NULL }; + + static const WindowHandlers s_window_handlers = { + .load = window_load, + .appear = window_appear, + .disappear = window_disappear, + .unload = window_unload, + }; + self->window.window_handlers = &s_window_handlers; + + simply_window_init(&self->window, simply); + simply_window_set_background_color(&self->window, GColor8White); + + app_timer_register(10000, (AppTimerCallback) show_welcome_text, self); + + return self; +} + +void simply_ui_destroy(SimplyUi *self) { + if (!self) { + return; + } + + simply_ui_clear(self, ~0); + + fonts_unload_custom_font(self->ui_layer.custom_body_font); + self->ui_layer.custom_body_font = NULL; + + simply_window_deinit(&self->window); + + free(self); +} diff --git a/src/simply/simply_ui.h b/src/simply/simply_ui.h new file mode 100644 index 00000000..d7097b96 --- /dev/null +++ b/src/simply/simply_ui.h @@ -0,0 +1,62 @@ +#pragma once + +#include "simply_window.h" + +#include "simply.h" + +#include + +typedef struct SimplyStyle SimplyStyle; + +typedef enum SimplyUiTextfieldId SimplyUiTextfieldId; + +enum SimplyUiTextfieldId { + UiTitle = 0, + UiSubtitle, + UiBody, + NumUiTextfields, +}; + +typedef enum SimplyUiImagefieldId SimplyUiImagefieldId; + +enum SimplyUiImagefieldId { + UiTitleIcon, + UiSubtitleIcon, + UiBodyImage, + NumUiImagefields, +}; + +typedef struct SimplyUiTextfield SimplyUiTextfield; + +struct SimplyUiTextfield { + char *text; + GColor8 color; +}; + +typedef struct SimplyUiLayer SimplyUiLayer; + +struct SimplyUiLayer { + Layer *layer; + const SimplyStyle *style; + SimplyUiTextfield textfields[3]; + uint32_t imagefields[3]; + GFont custom_body_font; +}; + +typedef struct SimplyUi SimplyUi; + +struct SimplyUi { + SimplyWindow window; + SimplyUiLayer ui_layer; +}; + +SimplyUi *simply_ui_create(Simply *simply); +void simply_ui_destroy(SimplyUi *self); + +void simply_ui_clear(SimplyUi *self, uint32_t clear_mask); + +void simply_ui_set_style(SimplyUi *self, int style_index); +void simply_ui_set_text(SimplyUi *self, SimplyUiTextfieldId textfield_id, const char *str); +void simply_ui_set_text_color(SimplyUi *self, SimplyUiTextfieldId textfield_id, GColor8 color); + +bool simply_ui_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_voice.c b/src/simply/simply_voice.c new file mode 100644 index 00000000..d3f58ee8 --- /dev/null +++ b/src/simply/simply_voice.c @@ -0,0 +1,130 @@ +#include "simply_voice.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include + +#if !defined(PBL_PLATFORM_APLITE) +typedef struct VoiceStartPacket VoiceStartPacket; + +struct __attribute__((__packed__)) VoiceStartPacket { + Packet packet; + bool enable_confirmation; +}; + + +typedef struct VoiceDataPacket VoiceDataPacket; + +struct __attribute__((__packed__)) VoiceDataPacket { + Packet packet; + int8_t status; + char result[]; +}; + +static SimplyVoice *s_voice; + +static bool send_voice_data(int status, char *transcription) { + // Handle NULL Case + if (transcription == NULL) { + return send_voice_data(DictationSessionStatusFailureSystemAborted, ""); + } + + // Handle success case + size_t transcription_length = strlen(transcription) + 1; + size_t packet_length = sizeof(VoiceDataPacket) + transcription_length; + + uint8_t buffer[packet_length]; + VoiceDataPacket *packet = (VoiceDataPacket *)buffer; + *packet = (VoiceDataPacket) { + .packet.type = CommandVoiceData, + .packet.length = packet_length, + .status = (uint8_t) status, + }; + + strncpy(packet->result, transcription, transcription_length); + + return simply_msg_send_packet(&packet->packet); +} + +// Define a callback for the dictation session +static void dictation_session_callback(DictationSession *session, DictationSessionStatus status, + char *transcription, void *context) { + s_voice->in_progress = false; + + // Send the result + send_voice_data(status, transcription); +} + +static void timer_callback_start_dictation(void *data) { + dictation_session_start(s_voice->session); +} + + +static void handle_voice_start_packet(Simply *simply, Packet *data) { + // Send an immediate response if there's already a dictation session in progress + // Status 64 = SessionAlreadyInProgress + if (s_voice->in_progress) { + send_voice_data(64, ""); + return; + } + + // Otherwise, start the timer as soon as possible + // (we start a timer so we can return true as quickly as possible) + s_voice->in_progress = true; + + VoiceStartPacket *packet = (VoiceStartPacket*) data; + dictation_session_enable_confirmation(s_voice->session, packet->enable_confirmation); + s_voice->timer = app_timer_register(0, timer_callback_start_dictation, NULL); +} + +static void handle_voice_stop_packet(Simply *simply, Packet *data) { + // Stop the session and clear the in_progress flag + dictation_session_stop(s_voice->session); + s_voice->in_progress = false; +} + +bool simply_voice_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandVoiceStart: + handle_voice_start_packet(simply, packet); + return true; + case CommandVoiceStop: + handle_voice_stop_packet(simply, packet); + return true; + } + + return false; +} + +SimplyVoice *simply_voice_create(Simply *simply) { + if (s_voice) { + return s_voice; + } + + SimplyVoice *self = malloc(sizeof(*self)); + *self = (SimplyVoice) { + .simply = simply, + .in_progress = false, + }; + + self->session = dictation_session_create(SIMPLY_VOICE_BUFFER_LENGTH, dictation_session_callback, NULL); + + s_voice = self; + return self; +} + +void simply_voice_destroy(SimplyVoice *self) { + if (!self) { + return; + } + + free(self); + s_voice = NULL; +} + +bool simply_voice_dictation_in_progress() { + return s_voice->in_progress; +} +#endif diff --git a/src/simply/simply_voice.h b/src/simply/simply_voice.h new file mode 100644 index 00000000..b6aee7de --- /dev/null +++ b/src/simply/simply_voice.h @@ -0,0 +1,39 @@ +#pragma once + +#include "simply_msg.h" +#include "simply.h" +#include "util/compat.h" + +#include + +#define SIMPLY_VOICE_BUFFER_LENGTH 512 + +typedef struct SimplyVoice SimplyVoice; + +struct SimplyVoice { + Simply *simply; + DictationSession *session; + AppTimer *timer; + + bool in_progress; +}; + +#if defined(PBL_PLATFORM_APLITE) + +#define simply_voice_create(simply) NULL +#define simply_voice_destroy(self) + +#define simply_voice_handle_packet(simply, packet) (false) + +#define simply_voice_dictation_in_progress() (false) + +#else + +SimplyVoice *simply_voice_create(Simply *simply); +void simply_voice_destroy(SimplyVoice *self); + +bool simply_voice_handle_packet(Simply *simply, Packet *packet); + +bool simply_voice_dictation_in_progress(); + +#endif diff --git a/src/simply/simply_wakeup.c b/src/simply/simply_wakeup.c new file mode 100644 index 00000000..a5922480 --- /dev/null +++ b/src/simply/simply_wakeup.c @@ -0,0 +1,135 @@ +#include "simply_wakeup.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include "util/compat.h" + +#include + +typedef struct LaunchReasonPacket LaunchReasonPacket; + +struct __attribute__((__packed__)) LaunchReasonPacket { + Packet packet; + uint32_t reason; + uint32_t args; + uint32_t time; + uint8_t is_timezone:8; +}; + +typedef struct WakeupSetPacket WakeupSetPacket; + +struct __attribute__((__packed__)) WakeupSetPacket { + Packet packet; + time_t timestamp; + int32_t cookie; + uint8_t notify_if_missed; +}; + +typedef struct WakeupSignalPacket WakeupSignalPacket; + +struct __attribute__((__packed__)) WakeupSignalPacket { + Packet packet; + int32_t id; + int32_t cookie; +}; + +typedef struct WakeupCancelPacket WakeupCancelPacket; + +struct __attribute__((__packed__)) WakeupCancelPacket { + Packet packet; + int32_t id; +}; + +typedef struct WakeupSetContext WakeupSetContext; + +struct WakeupSetContext { + WakeupId id; + int32_t cookie; +}; + +static bool send_launch_reason(AppLaunchReason reason, uint32_t args) { + LaunchReasonPacket packet = { + .packet.type = CommandLaunchReason, + .packet.length = sizeof(packet), + .reason = reason, + .args = args, + .time = time(NULL), + .is_timezone = clock_is_timezone_set(), + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool send_wakeup_signal(Command type, WakeupId id, int32_t cookie) { + WakeupSignalPacket packet = { + .packet.type = type, + .packet.length = sizeof(packet), + .id = id, + .cookie = cookie, + }; + return simply_msg_send_packet(&packet.packet); +} + +static void wakeup_handler(WakeupId wakeup_id, int32_t cookie) { + send_wakeup_signal(CommandWakeupEvent, wakeup_id, cookie); +} + +static void wakeup_set_timer_callback(void *data) { + WakeupSetContext *context = data; + send_wakeup_signal(CommandWakeupSetResult, context->id, context->cookie); +} + +static void process_launch_reason() { + AppLaunchReason reason = launch_reason(); + uint32_t args = launch_get_args(); + + send_launch_reason(reason, args); + + WakeupId wakeup_id; + int32_t cookie; + if (reason == APP_LAUNCH_WAKEUP && wakeup_get_launch_event(&wakeup_id, &cookie)) { + wakeup_handler(wakeup_id, cookie); + } +} + +static void handle_wakeup_set(Simply *simply, Packet *data) { + WakeupSetPacket *packet = (WakeupSetPacket*) data; + WakeupId id = wakeup_schedule(packet->timestamp, packet->cookie, packet->notify_if_missed); + + WakeupSetContext *context = malloc(sizeof(*context)); + if (!context) { + return; + } + context->id = id; + context->cookie = packet->cookie; + app_timer_register(10, wakeup_set_timer_callback, context); +} + +static void handle_wakeup_cancel(Simply *simply, Packet *data) { + WakeupCancelPacket *packet = (WakeupCancelPacket*) data; + if (packet->id == -1) { + wakeup_cancel_all(); + } else { + wakeup_cancel(packet->id); + } +} + +bool simply_wakeup_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandReady: + process_launch_reason(); + return false; + case CommandWakeupSet: + handle_wakeup_set(simply, packet); + return true; + case CommandWakeupCancel: + handle_wakeup_cancel(simply, packet); + return true; + } + return false; +} + +void simply_wakeup_init(Simply *simply) { + wakeup_service_subscribe(wakeup_handler); +} diff --git a/src/simply/simply_wakeup.h b/src/simply/simply_wakeup.h new file mode 100644 index 00000000..0dbab297 --- /dev/null +++ b/src/simply/simply_wakeup.h @@ -0,0 +1,11 @@ +#pragma once + +#include "simply_msg.h" + +#include "simply.h" + +#include + +void simply_wakeup_init(Simply *simply); + +bool simply_wakeup_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_window.c b/src/simply/simply_window.c new file mode 100644 index 00000000..e63353e0 --- /dev/null +++ b/src/simply/simply_window.c @@ -0,0 +1,458 @@ +#include "simply_window.h" + +#include "simply_msg.h" +#include "simply_res.h" +#include "simply_menu.h" +#include "simply_window_stack.h" +#include "simply_voice.h" + +#include "simply.h" + +#include "util/graphics.h" +#include "util/scroll_layer.h" +#include "util/status_bar_layer.h" +#include "util/string.h" + +#include + +typedef struct WindowPropsPacket WindowPropsPacket; + +struct __attribute__((__packed__)) WindowPropsPacket { + Packet packet; + uint32_t id; + GColor8 background_color; + bool scrollable; + bool paging; +}; + +typedef struct WindowButtonConfigPacket WindowButtonConfigPacket; + +struct __attribute__((__packed__)) WindowButtonConfigPacket { + Packet packet; + uint8_t button_mask; +}; + +typedef struct WindowStatusBarPacket WindowStatusBarPacket; + +struct __attribute__((__packed__)) WindowStatusBarPacket { + Packet packet; + GColor8 background_color; + GColor8 color; + StatusBarLayerSeparatorMode separator:8; + bool status; +}; + +typedef struct WindowActionBarPacket WindowActionBarPacket; + +struct __attribute__((__packed__)) WindowActionBarPacket { + Packet packet; + uint32_t image[3]; + GColor8 background_color; + bool action; +}; + +typedef struct ClickPacket ClickPacket; + +struct __attribute__((__packed__)) ClickPacket { + Packet packet; + ButtonId button:8; +}; + +typedef ClickPacket LongClickPacket; + + +static GColor8 s_button_palette[] = { { GColorWhiteARGB8 }, { GColorClearARGB8 } }; + + +static void prv_update_layer_placement(SimplyWindow *self, GRect *frame_out); +static void click_config_provider(void *data); + +static bool prv_send_click(SimplyMsg *self, Command type, ButtonId button) { + ClickPacket packet = { + .packet.type = type, + .packet.length = sizeof(packet), + .button = button, + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool prv_send_single_click(SimplyMsg *self, ButtonId button) { + return prv_send_click(self, CommandClick, button); +} + +static bool prv_send_long_click(SimplyMsg *self, ButtonId button) { + return prv_send_click(self, CommandLongClick, button); +} + +static void prv_set_scroll_layer_click_config(SimplyWindow *self) { + if (self->scroll_layer) { + scroll_layer_set_click_config_provider_onto_window( + self->scroll_layer, click_config_provider, self->window, self); + } +} + +void simply_window_set_scrollable(SimplyWindow *self, bool is_scrollable, bool is_paging, + bool animated, bool reset) { + const bool is_state_same = (self->is_scrollable == is_scrollable && + self->is_paging == is_paging); + if (!self->use_scroll_layer || (is_state_same && !reset)) { return; } + + self->is_scrollable = is_scrollable; + self->is_paging = is_paging; + scroll_layer_set_paging(self->scroll_layer, is_paging); + + prv_set_scroll_layer_click_config(self); + + if (!is_scrollable || reset) { + GRect frame = GRectZero; + prv_update_layer_placement(self, &frame); + const bool animated = false; + scroll_layer_set_content_offset(self->scroll_layer, GPointZero, animated); + scroll_layer_set_content_size(self->scroll_layer, frame.size); + } + + if (self->layer) { + layer_mark_dirty(self->layer); + } +} + +static void prv_update_layer_placement(SimplyWindow *self, GRect *frame_out) { + Layer * const main_layer = self->layer ?: scroll_layer_get_layer(self->scroll_layer); + if (!main_layer) { return; } + + GRect frame = { .size = layer_get_frame(window_get_root_layer(self->window)).size }; + + if (self->status_bar_layer) { + Layer * const status_bar_base_layer = status_bar_layer_get_layer(self->status_bar_layer); + const bool has_status_bar = (layer_get_window(status_bar_base_layer) != NULL); + const bool has_action_bar = + (layer_get_window(action_bar_layer_get_layer(self->action_bar_layer)) != NULL); + if (has_status_bar) { + GRect status_frame = { .size = { frame.size.w, STATUS_BAR_LAYER_HEIGHT } }; + frame.origin.y = STATUS_BAR_LAYER_HEIGHT; + frame.size.h -= + PBL_IF_ROUND_ELSE(self->status_bar_insets_bottom, false) ? STATUS_BAR_LAYER_HEIGHT * 2 : + STATUS_BAR_LAYER_HEIGHT; + if (PBL_IF_RECT_ELSE(has_action_bar, false)) { + status_frame.size.w -= ACTION_BAR_WIDTH; + } + layer_set_frame(status_bar_base_layer, status_frame); + } + } + + layer_set_frame(main_layer, frame); + if (frame_out) { + *frame_out = frame; + } +} + + +void simply_window_set_status_bar(SimplyWindow *self, bool use_status_bar) { + self->use_status_bar = use_status_bar; + + status_bar_layer_remove_from_window(self->window, self->status_bar_layer); + + if (use_status_bar) { + status_bar_layer_add_to_window(self->window, self->status_bar_layer); + } + + prv_update_layer_placement(self, NULL); + +#ifdef PBL_SDK_2 + if (!window_stack_contains_window(self->window)) { return; } + + // HACK: Refresh app chrome state + uint32_t id = self->id; + self->id = 0; + Window *window = window_create(); + window_stack_push(window, false); + window_stack_remove(window, false); + window_destroy(window); + self->id = id; +#endif +} + +void simply_window_set_background_color(SimplyWindow *self, GColor8 background_color) { + self->background_color = background_color; + window_set_background_color(self->window, gcolor8_get_or(background_color, GColorBlack)); +} + +void simply_window_set_status_bar_colors(SimplyWindow *self, GColor8 background_color, + GColor8 foreground_color) { + if (self->status_bar_layer) { + status_bar_layer_set_colors(self->status_bar_layer, background_color, foreground_color); + } +} + +void simply_window_set_status_bar_separator_mode(SimplyWindow *self, + StatusBarLayerSeparatorMode separator) { + if (self->status_bar_layer) { + status_bar_layer_set_separator_mode(self->status_bar_layer, separator); + } +} + +void simply_window_set_action_bar(SimplyWindow *self, bool use_action_bar) { + self->use_action_bar = use_action_bar; + + if (!self->action_bar_layer) { return; } + + action_bar_layer_remove_from_window(self->action_bar_layer); + prv_set_scroll_layer_click_config(self); + + if (use_action_bar) { + action_bar_layer_set_context(self->action_bar_layer, self); + action_bar_layer_set_click_config_provider(self->action_bar_layer, click_config_provider); + action_bar_layer_add_to_window(self->action_bar_layer, self->window); + } + + prv_update_layer_placement(self, NULL); +} + +void simply_window_set_action_bar_icon(SimplyWindow *self, ButtonId button, uint32_t id) { + if (!self->action_bar_layer) { return; } + + SimplyImage *icon = simply_res_auto_image(self->simply->res, id, true); + + if (!icon) { + action_bar_layer_clear_icon(self->action_bar_layer, button); + return; + } + + if (icon->is_palette_black_and_white) { + gbitmap_set_palette(icon->bitmap, s_button_palette, false); + } + + action_bar_layer_set_icon(self->action_bar_layer, button, icon->bitmap); + simply_window_set_action_bar(self, true); +} + +void simply_window_set_action_bar_background_color(SimplyWindow *self, GColor8 background_color) { + if (!self->action_bar_layer) { return; } + + s_button_palette[0] = gcolor8_equal(background_color, GColor8White) ? GColor8Black : GColor8White; + + action_bar_layer_set_background_color(self->action_bar_layer, gcolor8_get(background_color)); + simply_window_set_action_bar(self, true); +} + +void simply_window_action_bar_clear(SimplyWindow *self) { + if (!self->action_bar_layer) { return; } + + simply_window_set_action_bar(self, false); + + for (ButtonId button = BUTTON_ID_UP; button <= BUTTON_ID_DOWN; ++button) { + action_bar_layer_clear_icon(self->action_bar_layer, button); + } +} + +void simply_window_set_button(SimplyWindow *self, ButtonId button, bool enable) { + if (enable) { + self->button_mask |= 1 << button; + } else { + self->button_mask &= ~(1 << button); + } +} + +void simply_window_single_click_handler(ClickRecognizerRef recognizer, void *context) { + SimplyWindow *self = context; + ButtonId button = click_recognizer_get_button_id(recognizer); + bool is_enabled = (self->button_mask & (1 << button)); + if (button == BUTTON_ID_BACK) { + if (!simply_msg_has_communicated()) { + bool animated = true; + window_stack_pop(animated); + } else if (!is_enabled) { + simply_window_stack_back(self->simply->window_stack, self); + } + } + if (is_enabled) { + prv_send_single_click(self->simply->msg, button); + } +} + +static void long_click_handler(ClickRecognizerRef recognizer, void *context) { + SimplyWindow *self = context; + ButtonId button = click_recognizer_get_button_id(recognizer); + bool is_enabled = (self->button_mask & (1 << button)); + if (is_enabled) { + prv_send_long_click(self->simply->msg, button); + } +} + +static void click_config_provider(void *context) { + SimplyWindow *self = context; + for (int i = 0; i < NUM_BUTTONS; ++i) { + if (!self->is_scrollable || (i != BUTTON_ID_UP && i != BUTTON_ID_DOWN)) { + window_set_click_context(i, context); + window_single_click_subscribe(i, simply_window_single_click_handler); + window_long_click_subscribe(i, 500, (ClickHandler) long_click_handler, NULL); + } + } + if (self->is_scrollable) { + scroll_layer_click_config(self->scroll_layer); + } +} + +void simply_window_preload(SimplyWindow *self) { + if (self->window) { return; } + + Window *window = self->window = window_create(); + window_set_background_color(window, GColorClear); + window_set_user_data(window, self); + if (self->window_handlers) { + window_set_window_handlers(window, *self->window_handlers); + } +} + +void simply_window_load(SimplyWindow *self) { + Window *window = self->window; + + Layer *window_layer = window_get_root_layer(window); + GRect frame = layer_get_frame(window_layer); + frame.origin = GPointZero; + + self->scroll_layer = scroll_layer_create(frame); + Layer *scroll_base_layer = scroll_layer_get_layer(self->scroll_layer); + layer_add_child(window_layer, scroll_base_layer); + + scroll_layer_set_context(self->scroll_layer, self); + scroll_layer_set_shadow_hidden(self->scroll_layer, true); + + self->status_bar_layer = status_bar_layer_create(); + status_bar_layer_set_separator_mode(self->status_bar_layer, StatusBarLayerSeparatorModeDotted); + simply_window_set_status_bar(self, self->use_status_bar); + + self->action_bar_layer = action_bar_layer_create(); + action_bar_layer_set_context(self->action_bar_layer, self); + + window_set_click_config_provider_with_context(window, click_config_provider, self); + simply_window_set_action_bar(self, self->use_action_bar); +} + +bool simply_window_appear(SimplyWindow *self) { + if (!self->id) { + return false; + } + if (simply_msg_has_communicated()) { + simply_window_stack_send_show(self->simply->window_stack, self); + } + return true; +} + +bool simply_window_disappear(SimplyWindow *self) { + if (!self->id) { + return false; + } + // If the window is disappearing because of the dictation API + if (simply_voice_dictation_in_progress()) { + return false; + } + if (simply_msg_has_communicated()) { + simply_window_stack_send_hide(self->simply->window_stack, self); + } + +#ifdef PBL_PLATFORM_BASALT + simply_window_set_status_bar(self, false); +#endif + + return true; +} + +void simply_window_unload(SimplyWindow *self) { + // Unregister the click config provider + window_set_click_config_provider_with_context(self->window, NULL, NULL); + + scroll_layer_destroy(self->scroll_layer); + self->scroll_layer = NULL; + + action_bar_layer_destroy(self->action_bar_layer); + self->action_bar_layer = NULL; + + status_bar_layer_destroy(self->status_bar_layer); + self->status_bar_layer = NULL; + + window_destroy(self->window); + self->window = NULL; +} + +static void prv_handle_window_props_packet(Simply *simply, Packet *data) { + SimplyWindow *window = simply_window_stack_get_top_window(simply); + if (!window) { return; } + + WindowPropsPacket *packet = (WindowPropsPacket *)data; + simply_window_set_background_color(window, packet->background_color); + const bool is_same_window = (window->id == packet->id); + simply_window_set_scrollable(window, packet->scrollable, packet->paging, is_same_window, + !is_same_window); + window->id = packet->id; +} + +static void prv_handle_window_button_config_packet(Simply *simply, Packet *data) { + WindowButtonConfigPacket *packet = (WindowButtonConfigPacket*) data; + SimplyWindow *window = simply_window_stack_get_top_window(simply); + if (window) { + window->button_mask = packet->button_mask; + } +} + +static void prv_handle_window_status_bar_packet(Simply *simply, Packet *data) { + SimplyWindow *window = simply_window_stack_get_top_window(simply); + if (!window) { return; } + + WindowStatusBarPacket *packet = (WindowStatusBarPacket *)data; + simply_window_set_status_bar_colors(window, packet->background_color, packet->color); + simply_window_set_status_bar_separator_mode(window, packet->separator); + simply_window_set_status_bar(window, packet->status); +} + +static void prv_handle_window_action_bar_packet(Simply *simply, Packet *data) { + SimplyWindow *window = simply_window_stack_get_top_window(simply); + if (!window) { return; } + + WindowActionBarPacket *packet = (WindowActionBarPacket *)data; + simply_window_set_action_bar_background_color(window, packet->background_color); + for (unsigned int i = 0; i < ARRAY_LENGTH(packet->image); ++i) { + simply_window_set_action_bar_icon(window, i + 1, packet->image[i]); + } + simply_window_set_action_bar(window, packet->action); +} + +bool simply_window_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandWindowProps: + prv_handle_window_props_packet(simply, packet); + return true; + case CommandWindowButtonConfig: + prv_handle_window_button_config_packet(simply, packet); + return true; + case CommandWindowStatusBar: + prv_handle_window_status_bar_packet(simply, packet); + return true; + case CommandWindowActionBar: + prv_handle_window_action_bar_packet(simply, packet); + return true; + } + return false; +} + +SimplyWindow *simply_window_init(SimplyWindow *self, Simply *simply) { + self->simply = simply; + self->use_status_bar = false; + + for (int i = 0; i < NUM_BUTTONS; ++i) { + if (i != BUTTON_ID_BACK) { + self->button_mask |= 1 << i; + } + } + + simply_window_preload(self); + + return self; +} + +void simply_window_deinit(SimplyWindow *self) { + if (self) { + window_destroy(self->window); + self->window = NULL; + } +} diff --git a/src/simply/simply_window.h b/src/simply/simply_window.h new file mode 100644 index 00000000..048bb8d6 --- /dev/null +++ b/src/simply/simply_window.h @@ -0,0 +1,61 @@ +#pragma once + +#include "simply_msg.h" + +#include "simply.h" + +#include "util/color.h" +#include "util/sdk.h" +#include "util/status_bar_layer.h" + +#include + +typedef struct SimplyWindow SimplyWindow; + +struct SimplyWindow { + Simply *simply; + Window *window; + StatusBarLayer *status_bar_layer; + ScrollLayer *scroll_layer; + Layer *layer; + ActionBarLayer *action_bar_layer; + const WindowHandlers *window_handlers; + uint32_t id; + ButtonId button_mask:4; + GColor8 background_color; + bool is_scrollable:1; + bool is_paging:1; + bool use_scroll_layer:1; + bool use_status_bar:1; + bool use_action_bar:1; +#if defined(PBL_ROUND) + bool status_bar_insets_bottom:1; +#endif +}; + +SimplyWindow *simply_window_init(SimplyWindow *self, Simply *simply); +void simply_window_deinit(SimplyWindow *self); +void simply_window_show(SimplyWindow *self); +void simply_window_hide(SimplyWindow *self); + +void simply_window_preload(SimplyWindow *self); +void simply_window_load(SimplyWindow *self); +void simply_window_unload(SimplyWindow *self); +bool simply_window_appear(SimplyWindow *self); +bool simply_window_disappear(SimplyWindow *self); + +void simply_window_single_click_handler(ClickRecognizerRef recognizer, void *context); + +void simply_window_set_scrollable(SimplyWindow *self, bool is_scrollable, bool is_paging, + bool animated, bool reset); +void simply_window_set_status_bar(SimplyWindow *self, bool use_status_bar); +void simply_window_set_background_color(SimplyWindow *self, GColor8 background_color); + +void simply_window_set_button(SimplyWindow *self, ButtonId button, bool enable); + +void simply_window_set_action_bar(SimplyWindow *self, bool is_action_bar); +void simply_window_set_action_bar_icon(SimplyWindow *self, ButtonId button, uint32_t id); +void simply_window_set_action_bar_background_color(SimplyWindow *self, GColor8 background_color); +void simply_window_action_bar_clear(SimplyWindow *self); + +bool simply_window_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply/simply_window_stack.c b/src/simply/simply_window_stack.c new file mode 100644 index 00000000..0c3ca8aa --- /dev/null +++ b/src/simply/simply_window_stack.c @@ -0,0 +1,222 @@ +#include "simply_window_stack.h" + +#include "simply_window.h" +#include "simply_msg.h" + +#include "simply.h" + +#include "util/math.h" +#include "util/none.h" +#include "util/platform.h" +#include "util/sdk.h" + +#include + +typedef enum WindowType WindowType; + +enum WindowType { + WindowTypeWindow = 0, + WindowTypeMenu, + WindowTypeCard, + WindowTypeLast, +}; + +typedef struct WindowShowPacket WindowShowPacket; + +struct __attribute__((__packed__)) WindowShowPacket { + Packet packet; + WindowType type:8; + bool pushing; +}; + +typedef struct WindowSignalPacket WindowSignalPacket; + +struct __attribute__((__packed__)) WindowSignalPacket { + Packet packet; + uint32_t id; +}; + +typedef WindowSignalPacket WindowHidePacket; + +typedef WindowHidePacket WindowEventPacket; + +typedef WindowEventPacket WindowShowEventPacket; + +typedef WindowEventPacket WindowHideEventPacket; + +static bool s_broadcast_window = true; + +static bool send_window(SimplyMsg *self, Command type, uint32_t id) { + if (!s_broadcast_window) { + return false; + } + WindowEventPacket packet = { + .packet.type = type, + .packet.length = sizeof(packet), + .id = id, + }; + return simply_msg_send_packet(&packet.packet); +} + +static bool send_window_show(SimplyMsg *self, uint32_t id) { + return send_window(self, CommandWindowShowEvent, id); +} + +static bool send_window_hide(SimplyMsg *self, uint32_t id) { + return send_window(self, CommandWindowHideEvent, id); +} + +bool simply_window_stack_set_broadcast(bool broadcast) { + bool was_broadcast = s_broadcast_window; + s_broadcast_window = broadcast; + return was_broadcast; +} + +SimplyWindow *simply_window_stack_get_top_window(Simply *simply) { + Window *base_window = window_stack_get_top_window(); + if (!base_window) { + return NULL; + } + SimplyWindow *window = window_get_user_data(base_window); + if (!window || (void*) window == simply->splash) { + return NULL; + } + return window; +} + +#ifdef PBL_SDK_3 +static void show_window_sdk_3(SimplyWindowStack *self, SimplyWindow *window, bool is_push) { + const bool animated = (self->simply->splash == NULL); + + if (!animated) { + self->is_showing = true; + window_stack_pop_all(false); + self->is_showing = false; + } + + Window *prev_window = window_stack_get_top_window(); + + simply_window_preload(window); + + if (window->window == prev_window) { + // It's the same window, we can't animate for now + return; + } + + window_stack_push(window->window, animated); + + if (IF_APLITE_ELSE(true, animated)) { + window_stack_remove(prev_window, animated); + } +} +#endif + +#ifdef PBL_SDK_2 +static void show_window_sdk_2(SimplyWindowStack *self, SimplyWindow *window, bool is_push) { + const bool animated = (self->simply->splash == NULL); + + self->is_showing = true; + window_stack_pop_all(!is_push); + self->is_showing = false; + + if (is_push) { + window_stack_push(self->pusher, false); + } + + simply_window_preload(window); + window_stack_push(window->window, animated); + + if (is_push) { + window_stack_remove(self->pusher, false); + } +} +#endif + +void simply_window_stack_show(SimplyWindowStack *self, SimplyWindow *window, bool is_push) { + IF_SDK_3_ELSE(show_window_sdk_3, show_window_sdk_2)(self, window, is_push); +} + +void simply_window_stack_pop(SimplyWindowStack *self, SimplyWindow *window) { + self->is_hiding = true; + if (window->window == window_stack_get_top_window()) { + bool animated = true; + window_stack_pop(animated); + } + self->is_hiding = false; +} + +void simply_window_stack_back(SimplyWindowStack *self, SimplyWindow *window) { + self->is_hiding = true; + simply_window_stack_send_hide(self, window); + self->is_hiding = false; +} + +void simply_window_stack_send_show(SimplyWindowStack *self, SimplyWindow *window) { + if (window->id && self->is_showing) { + send_window_show(self->simply->msg, window->id); + } +} + +void simply_window_stack_send_hide(SimplyWindowStack *self, SimplyWindow *window) { + if (window->id && !self->is_showing) { + send_window_hide(self->simply->msg, window->id); + IF_SDK_2_ELSE({ + if (!self->is_hiding) { + window_stack_push(self->pusher, false); + } + }, NONE); + } +} + +static void handle_window_show_packet(Simply *simply, Packet *data) { + WindowShowPacket *packet = (WindowShowPacket*) data; + SimplyWindow *window = simply->windows[MIN(WindowTypeLast - 1, packet->type)]; + simply_window_stack_show(simply->window_stack, window, packet->pushing); +} + +static void handle_window_hide_packet(Simply *simply, Packet *data) { + WindowHidePacket *packet = (WindowHidePacket*) data; + SimplyWindow *window = simply_window_stack_get_top_window(simply); + if (!window) { + return; + } + if (window->id == packet->id) { + simply_window_stack_pop(simply->window_stack, window); + } +} + +bool simply_window_stack_handle_packet(Simply *simply, Packet *packet) { + switch (packet->type) { + case CommandWindowShow: + handle_window_show_packet(simply, packet); + return true; + case CommandWindowHide: + handle_window_hide_packet(simply, packet); + return true; + } + return false; +} + +SimplyWindowStack *simply_window_stack_create(Simply *simply) { + SimplyWindowStack *self = malloc(sizeof(*self)); + *self = (SimplyWindowStack) { .simply = simply }; + + IF_SDK_2_ELSE({ + self->pusher = window_create(); + }, NONE); + + return self; +} + +void simply_window_stack_destroy(SimplyWindowStack *self) { + if (!self) { + return; + } + + IF_SDK_2_ELSE({ + window_destroy(self->pusher); + self->pusher = NULL; + }, NONE); + + free(self); +} diff --git a/src/simply/simply_window_stack.h b/src/simply/simply_window_stack.h new file mode 100644 index 00000000..7f18d5c2 --- /dev/null +++ b/src/simply/simply_window_stack.h @@ -0,0 +1,36 @@ +#pragma once + +#include "simply_window.h" + +#include "simply_msg.h" + +#include "simply.h" + +#include "util/sdk.h" + +#include + +typedef struct SimplyWindowStack SimplyWindowStack; + +struct SimplyWindowStack { + Simply *simply; + IF_SDK_2_ELSE(Window *pusher, NONE); + bool is_showing:1; + bool is_hiding:1; +}; + +SimplyWindowStack *simply_window_stack_create(Simply *simply); +void simply_window_stack_destroy(SimplyWindowStack *self); + +bool simply_window_stack_set_broadcast(bool broadcast); + +SimplyWindow *simply_window_stack_get_top_window(Simply *simply); + +void simply_window_stack_show(SimplyWindowStack *self, SimplyWindow *window, bool is_push); +void simply_window_stack_pop(SimplyWindowStack *self, SimplyWindow *window); +void simply_window_stack_back(SimplyWindowStack *self, SimplyWindow *window); + +void simply_window_stack_send_show(SimplyWindowStack *self, SimplyWindow *window); +void simply_window_stack_send_hide(SimplyWindowStack *self, SimplyWindow *window); + +bool simply_window_stack_handle_packet(Simply *simply, Packet *packet); diff --git a/src/simply_accel.c b/src/simply_accel.c deleted file mode 100644 index 9446135b..00000000 --- a/src/simply_accel.c +++ /dev/null @@ -1,82 +0,0 @@ -#include "simply_accel.h" - -#include "simply_msg.h" - -#include "simplyjs.h" - -#include - -SimplyAccel *s_accel = NULL; - -static void handle_accel_data(AccelData *data, uint32_t num_samples) { - simply_msg_accel_data(data, num_samples, TRANSACTION_ID_INVALID); -} - -void simply_accel_set_data_rate(SimplyAccel *self, AccelSamplingRate rate) { - self->rate = rate; - accel_service_set_sampling_rate(rate); -} - -void simply_accel_set_data_samples(SimplyAccel *self, uint32_t num_samples) { - self->num_samples = num_samples; - accel_service_set_samples_per_update(num_samples); - if (!self->data_subscribed) { - return; - } - simply_accel_set_data_subscribe(self, false); - simply_accel_set_data_subscribe(self, true); -} - -void simply_accel_set_data_subscribe(SimplyAccel *self, bool subscribe) { - if (self->data_subscribed == subscribe) { - return; - } - if (subscribe) { - accel_data_service_subscribe(self->num_samples, handle_accel_data); - accel_service_set_sampling_rate(self->rate); - } else { - accel_data_service_unsubscribe(); - } - self->data_subscribed = subscribe; -} - -void simply_accel_peek(SimplyAccel *self, AccelData *data) { - if (self->data_subscribed) { - return; - } - accel_service_peek(data); -} - -static void handle_accel_tap(AccelAxisType axis, int32_t direction) { - simply_msg_accel_tap(axis, direction); -} - -SimplyAccel *simply_accel_create(void) { - if (s_accel) { - return s_accel; - } - - SimplyAccel *self = malloc(sizeof(*self)); - *self = (SimplyAccel) { - .rate = ACCEL_SAMPLING_100HZ, - .num_samples = 25, - }; - s_accel = self; - - accel_tap_service_subscribe(handle_accel_tap); - - return self; -} - -void simply_accel_destroy(SimplyAccel *self) { - if (!s_accel) { - return; - } - - accel_tap_service_unsubscribe(); - - free(self); - - s_accel = NULL; -} - diff --git a/src/simply_accel.h b/src/simply_accel.h deleted file mode 100644 index 68636dd6..00000000 --- a/src/simply_accel.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -typedef struct SimplyAccel SimplyAccel; - -struct SimplyAccel { - bool data_subscribed; - AccelSamplingRate rate; - uint32_t num_samples; -}; - -SimplyAccel *simply_accel_create(void); - -void simply_accel_set_data_rate(SimplyAccel *self, AccelSamplingRate rate); - -void simply_accel_set_data_samples(SimplyAccel *self, uint32_t num_samples); - -void simply_accel_set_data_subscribe(SimplyAccel *self, bool subscribe); - -void simply_accel_peek(SimplyAccel *self, AccelData *data); - -void simply_accel_destroy(SimplyAccel *self); - diff --git a/src/simply_msg.c b/src/simply_msg.c deleted file mode 100644 index 2bb01d79..00000000 --- a/src/simply_msg.c +++ /dev/null @@ -1,239 +0,0 @@ -#include "simply_msg.h" - -#include "simply_accel.h" -#include "simply_ui.h" - -#include "simplyjs.h" - -#include - -typedef enum SimplyACmd SimplyACmd; - -enum SimplyACmd { - SimplyACmd_setText = 0, - SimplyACmd_singleClick, - SimplyACmd_longClick, - SimplyACmd_accelTap, - SimplyACmd_vibe, - SimplyACmd_setScrollable, - SimplyACmd_setStyle, - SimplyACmd_setFullscreen, - SimplyACmd_accelData, - SimplyACmd_getAccelData, - SimplyACmd_configAccelData, - SimplyACmd_configButtons, -}; - -typedef enum VibeType VibeType; - -enum VibeType { - VibeShort = 0, - VibeLong = 1, - VibeDouble = 2, -}; - -static void check_splash(Simply *simply) { - if (simply->splash) { - simply_ui_show(simply->ui); - } -} - -static void handle_set_text(DictionaryIterator *iter, Simply *simply) { - SimplyUi *ui = simply->ui; - Tuple *tuple; - bool clear = false; - if ((tuple = dict_find(iter, 4))) { - clear = true; - } - if ((tuple = dict_find(iter, 1)) || clear) { - simply_ui_set_text(ui, &ui->title_text, tuple ? tuple->value->cstring : NULL); - } - if ((tuple = dict_find(iter, 2)) || clear) { - simply_ui_set_text(ui, &ui->subtitle_text, tuple ? tuple->value->cstring : NULL); - } - if ((tuple = dict_find(iter, 3)) || clear) { - simply_ui_set_text(ui, &ui->body_text, tuple ? tuple->value->cstring : NULL); - } - - check_splash(simply); -} - -static void handle_vibe(DictionaryIterator *iter, Simply *simply) { - Tuple *tuple; - if ((tuple = dict_find(iter, 1))) { - switch ((VibeType) tuple->value->int32) { - case VibeShort: vibes_short_pulse(); break; - case VibeLong: vibes_short_pulse(); break; - case VibeDouble: vibes_double_pulse(); break; - } - } -} - -static void handle_set_scrollable(DictionaryIterator *iter, Simply *simply) { - Tuple *tuple; - if ((tuple = dict_find(iter, 1))) { - simply_ui_set_scrollable(simply->ui, tuple->value->int32); - } -} - -static void handle_set_style(DictionaryIterator *iter, Simply *simply) { - Tuple *tuple; - if ((tuple = dict_find(iter, 1))) { - simply_ui_set_style(simply->ui, tuple->value->int32); - } -} - -static void handle_set_fullscreen(DictionaryIterator *iter, Simply *simply) { - Tuple *tuple; - if ((tuple = dict_find(iter, 1))) { - simply_ui_set_fullscreen(simply->ui, tuple->value->int32); - } -} - -static void handle_config_buttons(DictionaryIterator *iter, Simply *simply) { - SimplyUi *ui = simply->ui; - Tuple *tuple; - for (int i = 0; i < NUM_BUTTONS; ++i) { - if ((tuple = dict_find(iter, i + 1))) { - simply_ui_set_button(ui, i, tuple->value->int32); - } - } -} - -static void get_accel_data_timer_callback(void *context) { - Simply *simply = context; - AccelData data = { .x = 0 }; - simply_accel_peek(simply->accel, &data); - if (!simply_msg_accel_data(&data, 1, 0)) { - app_timer_register(10, get_accel_data_timer_callback, simply); - } -} - -static void handle_get_accel_data(DictionaryIterator *iter, Simply *simply) { - app_timer_register(10, get_accel_data_timer_callback, simply); -} - -static void handle_set_accel_config(DictionaryIterator *iter, Simply *simply) { - Tuple *tuple; - if ((tuple = dict_find(iter, 1))) { - simply_accel_set_data_rate(simply->accel, tuple->value->int32); - } - if ((tuple = dict_find(iter, 2))) { - simply_accel_set_data_samples(simply->accel, tuple->value->int32); - } - if ((tuple = dict_find(iter, 3))) { - simply_accel_set_data_subscribe(simply->accel, tuple->value->int32); - } -} - -static void received_callback(DictionaryIterator *iter, void *context) { - Tuple *tuple = dict_find(iter, 0); - if (!tuple) { - return; - } - - switch (tuple->value->uint8) { - case SimplyACmd_setText: - handle_set_text(iter, context); - break; - case SimplyACmd_vibe: - handle_vibe(iter, context); - break; - case SimplyACmd_setScrollable: - handle_set_scrollable(iter, context); - break; - case SimplyACmd_setStyle: - handle_set_style(iter, context); - break; - case SimplyACmd_setFullscreen: - handle_set_fullscreen(iter, context); - break; - case SimplyACmd_getAccelData: - handle_get_accel_data(iter, context); - break; - case SimplyACmd_configAccelData: - handle_set_accel_config(iter, context); - break; - case SimplyACmd_configButtons: - handle_config_buttons(iter, context); - break; - } -} - -static void dropped_callback(AppMessageResult reason, void *context) { -} - -static void sent_callback(DictionaryIterator *iter, void *context) { -} - -static void failed_callback(DictionaryIterator *iter, AppMessageResult reason, Simply *simply) { - SimplyUi *ui = simply->ui; - if (reason == APP_MSG_NOT_CONNECTED) { - simply_ui_set_text(ui, &ui->subtitle_text, "Disconnected"); - simply_ui_set_text(ui, &ui->body_text, "Run the Pebble Phone App"); - - check_splash(simply); - } -} - -void simply_msg_init(Simply *simply) { - const uint32_t size_inbound = 2048; - const uint32_t size_outbound = 512; - app_message_open(size_inbound, size_outbound); - - app_message_set_context(simply); - - app_message_register_inbox_received(received_callback); - app_message_register_inbox_dropped(dropped_callback); - app_message_register_outbox_sent(sent_callback); - app_message_register_outbox_failed((AppMessageOutboxFailed) failed_callback); -} - -void simply_msg_deinit() { - app_message_deregister_callbacks(); -} - -bool simply_msg_single_click(ButtonId button) { - DictionaryIterator *iter = NULL; - if (app_message_outbox_begin(&iter) != APP_MSG_OK) { - return false; - } - dict_write_uint8(iter, 0, SimplyACmd_singleClick); - dict_write_uint8(iter, 1, button); - return (app_message_outbox_send() == APP_MSG_OK); -} - -bool simply_msg_long_click(ButtonId button) { - DictionaryIterator *iter = NULL; - if (app_message_outbox_begin(&iter) != APP_MSG_OK) { - return false; - } - dict_write_uint8(iter, 0, SimplyACmd_longClick); - dict_write_uint8(iter, 1, button); - return (app_message_outbox_send() == APP_MSG_OK); -} - -bool simply_msg_accel_tap(AccelAxisType axis, int32_t direction) { - DictionaryIterator *iter = NULL; - if (app_message_outbox_begin(&iter) != APP_MSG_OK) { - return false; - } - dict_write_uint8(iter, 0, SimplyACmd_accelTap); - dict_write_uint8(iter, 1, axis); - dict_write_int8(iter, 2, direction); - return (app_message_outbox_send() == APP_MSG_OK); -} - -bool simply_msg_accel_data(AccelData *data, uint32_t num_samples, int32_t transaction_id) { - DictionaryIterator *iter = NULL; - if (app_message_outbox_begin(&iter) != APP_MSG_OK) { - return false; - } - dict_write_uint8(iter, 0, SimplyACmd_accelData); - if (transaction_id >= 0) { - dict_write_int32(iter, 1, transaction_id); - } - dict_write_uint8(iter, 2, num_samples); - dict_write_data(iter, 3, (uint8_t*) data, sizeof(*data) * num_samples); - return (app_message_outbox_send() == APP_MSG_OK); -} diff --git a/src/simply_msg.h b/src/simply_msg.h deleted file mode 100644 index 50a5e53c..00000000 --- a/src/simply_msg.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "simplyjs.h" - -#include - -#define TRANSACTION_ID_INVALID (-1) - -void simply_msg_init(Simply *simply); - -void simply_msg_deinit(); - -bool simply_msg_single_click(ButtonId button); - -bool simply_msg_long_click(ButtonId button); - -bool simply_msg_accel_tap(AccelAxisType axis, int32_t direction); - -bool simply_msg_accel_data(AccelData *accel, uint32_t num_samples, int32_t transaction_id); - diff --git a/src/simply_ui.c b/src/simply_ui.c deleted file mode 100644 index 84c386bb..00000000 --- a/src/simply_ui.c +++ /dev/null @@ -1,325 +0,0 @@ -#include "simply_ui.h" - -#include "simply_msg.h" - -#include "simplyjs.h" - -#include - -struct SimplyStyle { - const char* title_font; - const char* subtitle_font; - const char* body_font; - int custom_body_font_id; - GFont custom_body_font; -}; - -static SimplyStyle STYLES[] = { - { - .title_font = FONT_KEY_GOTHIC_24_BOLD, - .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, - .body_font = FONT_KEY_GOTHIC_18, - }, - { - .title_font = FONT_KEY_GOTHIC_28_BOLD, - .subtitle_font = FONT_KEY_GOTHIC_28, - .body_font = FONT_KEY_GOTHIC_24_BOLD, - }, - { - .title_font = FONT_KEY_GOTHIC_24_BOLD, - .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, - .custom_body_font_id = RESOURCE_ID_MONO_FONT_14, - }, -}; - -SimplyUi *s_ui = NULL; - -static void click_config_provider(SimplyUi *self); - -void simply_ui_set_style(SimplyUi *self, int style_index) { - self->style = &STYLES[style_index]; - layer_mark_dirty(self->display_layer); -} - -void simply_ui_set_scrollable(SimplyUi *self, bool is_scrollable) { - self->is_scrollable = is_scrollable; - scroll_layer_set_click_config_onto_window(self->scroll_layer, self->window); - - if (!is_scrollable) { - GRect bounds = layer_get_bounds(window_get_root_layer(self->window)); - layer_set_bounds(self->display_layer, bounds); - const bool animated = true; - scroll_layer_set_content_offset(self->scroll_layer, GPointZero, animated); - scroll_layer_set_content_size(self->scroll_layer, bounds.size); - } - - layer_mark_dirty(self->display_layer); -} - -void simply_ui_set_fullscreen(SimplyUi *self, bool is_fullscreen) { - window_set_fullscreen(self->window, is_fullscreen); - - GRect frame = layer_get_frame(window_get_root_layer(self->window)); - scroll_layer_set_frame(self->scroll_layer, frame); - layer_set_frame(self->display_layer, frame); - - if (!window_stack_contains_window(self->window)) { - return; - } - - // HACK: Refresh app chrome state - Window *window = window_create(); - window_stack_push(window, false); - window_stack_remove(window, false); - window_destroy(window); -} - -void simply_ui_set_button(SimplyUi *self, ButtonId button, bool enable) { - if (enable) { - self->button_mask |= 1 << button; - } else { - self->button_mask &= ~(1 << button); - } -} - -static void set_text(char **str_field, const char *str) { - free(*str_field); - - if (!str || !str[0]) { - *str_field = NULL; - return; - } - - size_t size = strlen(str) + 1; - char *buffer = malloc(size); - strncpy(buffer, str, size); - buffer[size - 1] = '\0'; - - *str_field = buffer; -} - -void simply_ui_set_text(SimplyUi *self, char **str_field, const char *str) { - set_text(str_field, str); - layer_mark_dirty(self->display_layer); -} - -static bool is_string(const char *str) { - return str && str[0]; -} - -void display_layer_update_callback(Layer *layer, GContext *ctx) { - SimplyUi *self = s_ui; - - GRect window_bounds = layer_get_bounds(window_get_root_layer(self->window)); - GRect bounds = layer_get_bounds(layer); - - const SimplyStyle *style = self->style; - GFont title_font = fonts_get_system_font(style->title_font); - GFont subtitle_font = fonts_get_system_font(style->subtitle_font); - GFont body_font = style->custom_body_font ? style->custom_body_font : fonts_get_system_font(style->body_font); - - const int16_t x_margin = 5; - const int16_t y_margin = 2; - - GRect text_bounds = bounds; - text_bounds.size.w -= 2 * x_margin; - text_bounds.size.h += 1000; - GPoint cursor = { x_margin, y_margin }; - - graphics_context_set_text_color(ctx, GColorBlack); - - bool has_title = is_string(self->title_text); - bool has_subtitle = is_string(self->subtitle_text); - bool has_body = is_string(self->body_text); - - GSize title_size, subtitle_size; - GPoint title_pos, subtitle_pos; - GRect body_rect; - - if (has_title) { - title_size = graphics_text_layout_get_content_size(self->title_text, title_font, text_bounds, - GTextOverflowModeWordWrap, GTextAlignmentLeft); - title_size.w = text_bounds.size.w; - title_pos = cursor; - cursor.y += title_size.h; - } - - if (has_subtitle) { - subtitle_size = graphics_text_layout_get_content_size(self->subtitle_text, title_font, text_bounds, - GTextOverflowModeWordWrap, GTextAlignmentLeft); - subtitle_size.w = text_bounds.size.w; - subtitle_pos = cursor; - cursor.y += subtitle_size.h; - } - - if (has_body) { - body_rect = bounds; - body_rect.origin = cursor; - body_rect.size.w -= 2 * x_margin; - body_rect.size.h -= 2 * y_margin + cursor.y; - GSize body_size = graphics_text_layout_get_content_size(self->body_text, body_font, text_bounds, - GTextOverflowModeWordWrap, GTextAlignmentLeft); - if (self->is_scrollable) { - body_rect.size = body_size; - int16_t new_height = cursor.y + 2 * y_margin + body_size.h; - bounds.size.h = window_bounds.size.h > new_height ? window_bounds.size.h : new_height; - layer_set_frame(layer, bounds); - scroll_layer_set_content_size(self->scroll_layer, bounds.size); - } else if (!style->custom_body_font && body_size.h > body_rect.size.h) { - body_font = fonts_get_system_font(FONT_KEY_GOTHIC_18); - } - } - - graphics_context_set_fill_color(ctx, GColorWhite); - graphics_fill_rect(ctx, bounds, 4, GCornersAll); - - if (has_title) { - graphics_draw_text(ctx, self->title_text, title_font, - (GRect) { .origin = title_pos, .size = title_size }, - GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); - } - - if (has_subtitle) { - graphics_draw_text(ctx, self->subtitle_text, subtitle_font, - (GRect) { .origin = subtitle_pos, .size = subtitle_size }, - GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); - } - - if (has_body) { - graphics_draw_text(ctx, self->body_text, body_font, body_rect, - GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); - } -} - -static void single_click_handler(ClickRecognizerRef recognizer, void *context) { - SimplyUi *self = s_ui; - ButtonId button = click_recognizer_get_button_id(recognizer); - bool is_enabled = (self->button_mask & (1 << button)); - if (button == BUTTON_ID_BACK && !is_enabled) { - bool animated = true; - window_stack_pop(animated); - } - if (is_enabled) { - simply_msg_single_click(button); - } -} - -static void long_click_handler(ClickRecognizerRef recognizer, void *context) { - SimplyUi *self = s_ui; - ButtonId button = click_recognizer_get_button_id(recognizer); - bool is_enabled = (self->button_mask & (1 << button)); - if (is_enabled) { - simply_msg_long_click(button); - } -} - -static void click_config_provider(SimplyUi *self) { - for (int i = 0; i < NUM_BUTTONS; ++i) { - if (!self->is_scrollable || (i != BUTTON_ID_UP && i != BUTTON_ID_DOWN)) { - window_single_click_subscribe(i, (ClickHandler) single_click_handler); - window_long_click_subscribe(i, 500, (ClickHandler) long_click_handler, NULL); - } - } -} - -static void show_welcome_text(SimplyUi *self) { - if (self->title_text || self->subtitle_text || self->body_text) { - return; - } - - simply_ui_set_text(self, &self->title_text, "Simply.js"); - simply_ui_set_text(self, &self->subtitle_text, "Write apps with JS!"); - simply_ui_set_text(self, &self->body_text, "Visit simplyjs.io for details."); - - simply_ui_show(self); -} - -static void window_load(Window *window) { - SimplyUi *self = window_get_user_data(window); - - Layer *window_layer = window_get_root_layer(window); - GRect bounds = layer_get_bounds(window_layer); - bounds.origin = GPointZero; - - ScrollLayer *scroll_layer = self->scroll_layer = scroll_layer_create(bounds); - Layer *scroll_base_layer = scroll_layer_get_layer(scroll_layer); - layer_add_child(window_layer, scroll_base_layer); - - Layer *display_layer = self->display_layer = layer_create(bounds); - layer_set_update_proc(display_layer, display_layer_update_callback); - scroll_layer_add_child(scroll_layer, display_layer); - - scroll_layer_set_context(scroll_layer, self); - scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) { - .click_config_provider = (ClickConfigProvider) click_config_provider, - }); - scroll_layer_set_click_config_onto_window(scroll_layer, window); - - simply_ui_set_style(self, 1); - - app_timer_register(10000, (AppTimerCallback) show_welcome_text, self); -} - -static void window_unload(Window *window) { - SimplyUi *self = window_get_user_data(window); - - layer_destroy(self->display_layer); - scroll_layer_destroy(self->scroll_layer); - window_destroy(window); -} - -void simply_ui_show(SimplyUi *self) { - if (!window_stack_contains_window(self->window)) { - bool animated = true; - window_stack_push(self->window, animated); - } -} - -SimplyUi *simply_ui_create(void) { - if (s_ui) { - return s_ui; - } - - for (unsigned int i = 0; i < ARRAY_LENGTH(STYLES); ++i) { - SimplyStyle *style = &STYLES[i]; - if (style->custom_body_font_id) { - style->custom_body_font = fonts_load_custom_font(resource_get_handle(style->custom_body_font_id)); - } - } - - SimplyUi *self = malloc(sizeof(*self)); - *self = (SimplyUi) { .window = NULL }; - s_ui = self; - - for (int i = 0; i < NUM_BUTTONS; ++i) { - if (i != BUTTON_ID_BACK) { - self->button_mask |= 1 << i; - } - } - - Window *window = self->window = window_create(); - window_set_user_data(window, self); - window_set_background_color(window, GColorBlack); - window_set_click_config_provider(window, (ClickConfigProvider) click_config_provider); - window_set_window_handlers(window, (WindowHandlers) { - .unload = window_unload, - }); - - window_load(self->window); - - return self; -} - -void simply_ui_destroy(SimplyUi *self) { - if (!s_ui) { - return; - } - - simply_ui_set_text(self, &self->title_text, NULL); - simply_ui_set_text(self, &self->subtitle_text, NULL); - simply_ui_set_text(self, &self->body_text, NULL); - - free(self); - - s_ui = NULL; -} diff --git a/src/simply_ui.h b/src/simply_ui.h deleted file mode 100644 index e0bada6e..00000000 --- a/src/simply_ui.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include - -typedef struct SimplyStyle SimplyStyle; - -typedef struct SimplyUi SimplyUi; - -struct SimplyUi { - Window *window; - const SimplyStyle *style; - char *title_text; - char *subtitle_text; - char *body_text; - ScrollLayer *scroll_layer; - Layer *display_layer; - bool is_scrollable; - uint32_t button_mask; -}; - -SimplyUi *simply_ui_create(void); - -void simply_ui_destroy(SimplyUi *self); - -void simply_ui_show(SimplyUi *self); - -void simply_ui_set_style(SimplyUi *self, int style_index); - -void simply_ui_set_text(SimplyUi *self, char **str_field, const char *str); - -void simply_ui_set_scrollable(SimplyUi *self, bool is_scrollable); - -void simply_ui_set_fullscreen(SimplyUi *self, bool is_fullscreen); - -void simply_ui_set_button(SimplyUi *self, ButtonId button, bool enable); - diff --git a/src/simplyjs.c b/src/simplyjs.c deleted file mode 100644 index e6cb27f4..00000000 --- a/src/simplyjs.c +++ /dev/null @@ -1,33 +0,0 @@ -#include "simplyjs.h" - -#include "simply_accel.h" -#include "simply_splash.h" -#include "simply_ui.h" -#include "simply_msg.h" - -#include - -static Simply *init(void) { - Simply *simply = malloc(sizeof(*simply)); - simply->accel = simply_accel_create(); - simply->splash = simply_splash_create(simply); - simply->ui = simply_ui_create(); - - bool animated = true; - window_stack_push(simply->splash->window, animated); - - simply_msg_init(simply); - return simply; -} - -static void deinit(Simply *simply) { - simply_msg_deinit(); - simply_ui_destroy(simply->ui); - simply_accel_destroy(simply->accel); -} - -int main(void) { - Simply *simply = init(); - app_event_loop(); - deinit(simply); -} diff --git a/src/simplyjs.h b/src/simplyjs.h deleted file mode 100644 index 0e405821..00000000 --- a/src/simplyjs.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#define LOG(...) APP_LOG(APP_LOG_LEVEL_DEBUG, __VA_ARGS__) - -typedef struct Simply Simply; - -struct Simply { - struct SimplyAccel *accel; - struct SimplySplash *splash; - struct SimplyUi *ui; -}; - diff --git a/src/util/color.h b/src/util/color.h new file mode 100644 index 00000000..29fff9b8 --- /dev/null +++ b/src/util/color.h @@ -0,0 +1,65 @@ +#pragma once + +#include "util/compat.h" + +#include + +#define GColor8White (GColor8){.argb=GColorWhiteARGB8} +#define GColor8Black (GColor8){.argb=GColorBlackARGB8} +#define GColor8Clear (GColor8){.argb=GColorClearARGB8} +#define GColor8ClearWhite (GColor8){.argb=0x3F} + +#ifndef PBL_SDK_3 + +static inline GColor gcolor8_get(GColor8 color) { + switch (color.argb) { + case GColorWhiteARGB8: return GColorWhite; + case GColorBlackARGB8: return GColorBlack; + default: return GColorClear; + } +} + +static inline GColor gcolor8_get_or(GColor8 color, GColor fallback) { + switch (color.argb) { + case GColorWhiteARGB8: return GColorWhite; + case GColorBlackARGB8: return GColorBlack; + case GColorClearARGB8: return GColorClear; + default: return fallback; + } +} + +static inline GColor8 gcolor_get8(GColor color) { + switch (color) { + case GColorWhite: return GColor8White; + case GColorBlack: return GColor8Black; + default: return GColor8Clear; + } +} + +static inline bool gcolor8_equal_native(GColor8 color, GColor other) { + return (color.argb == gcolor_get8(other).argb); +} + +static inline GColor8 gcolor_legible_over(GColor8 background_color) { + const int sum = background_color.r + background_color.g + background_color.b; + const int avg = sum / 3; + return (avg >= 2) ? GColor8Black : GColor8White; +} + +#else + +static inline GColor gcolor8_get(GColor8 color) { + return color; +} + +static inline GColor gcolor8_get_or(GColor8 color, GColor8 fallback) { + return color; +} + +#define gcolor8_equal_native gcolor8_equal + +#endif + +static inline bool gcolor8_equal(GColor8 color, GColor8 other) { + return (color.argb == other.argb); +} diff --git a/src/util/compat.h b/src/util/compat.h new file mode 100644 index 00000000..e6c6ba21 --- /dev/null +++ b/src/util/compat.h @@ -0,0 +1,200 @@ +#pragma once + +#include + +/** + * aplite and SDK 2.9 compatibility utilities + * These are a collection of types and compatibility macros taken from SDK 3.0. + * When possible, they are copied directly without modification. + */ + +// Compatibility definitions for aplite on 2.9 +#if !defined(PBL_PLATFORM_APLITE) && !defined(PBL_PLATFORM_BASALT) && !defined(PBL_PLATFORM_CHALK) && !defined(PBL_PLATFORM_DIORITE) && !defined(PBL_PLATFORM_EMERY) + +#define PBL_SDK_2 + +//! The format of a GBitmap can either be 1-bit or 8-bit. +typedef enum GBitmapFormat { + GBitmapFormat1Bit = 0, //row_size_bytes) +#endif + +//! Convenience function to use SDK 3.0 function to get a `GBitmap`'s `bounds` field. +#ifndef gbitmap_get_bounds +#define gbitmap_get_bounds(bmp) ((bmp)->bounds) +#endif + +//! Convenience function to use SDK 3.0 function to get a `GBitmap`'s `addr` field. +#ifndef gbitmap_get_data +#define gbitmap_get_data(bmp) ((bmp)->addr) +#endif + +//! Convenience function to use SDK 3.0 function to set a `GBitmap`'s `bounds` field. +#ifndef gbitmap_set_bounds +#define gbitmap_set_bounds(bmp, new_bounds) ((bmp)->bounds = (new_bounds)) +#endif + +//! Convenience function to use SDK 3.0 function to set a `GBitmap`'s `addr` field. +//! Modified to support row_size_bytes and free_on_destroy. +//! Change to ifndef when SDK 3.0 supports those additional parameters. +#undef gbitmap_set_data +#define gbitmap_set_data(bmp, data, fmt, rsb, fod) ({ \ + __typeof__(bmp) __gbitmap_tmp_bmp = (bmp); \ + __gbitmap_tmp_bmp->addr = (data); \ + __gbitmap_tmp_bmp->is_heap_allocated = (fod); \ + __gbitmap_tmp_bmp->row_size_bytes = (rsb); \ +}) + +#ifndef gbitmap_get_palette +#define gbitmap_get_palette(bitmap) NULL +#endif + +#ifndef gbitmap_set_palette +#define gbitmap_set_palette(bitmap, palette, free_on_destroy) \ + ((void)(bitmap), (void)(palette), (void)(free_on_destroy)) +#endif + +#ifndef gbitmap_get_format +#define gbitmap_get_format(bitmap) \ + (GBitmapFormat1Bit) +#endif + +typedef TextLayout GTextAttributes; + +#ifndef graphics_text_attributes_create +#define graphics_text_attributes_create() NULL +#endif + +#ifndef graphics_text_attributes_destroy +#define graphics_text_attributes_destroy(text_attributes) +#endif + +#ifndef graphics_text_attributes_enable_screen_text_flow +#define graphics_text_attributes_enable_screen_text_flow(text_attributes, inset) +#endif + +#ifndef graphics_text_attributes_enable_paging +#define graphics_text_attributes_enable_paging(text_attributes, origin_on_screen, page_frame) +#endif + +#ifndef graphics_text_layout_get_content_size_with_attributes +#define graphics_text_layout_get_content_size_with_attributes(text, font, box, overflow_mode, \ + alignment, text_attributes) \ + graphics_text_layout_get_content_size(text, font, box, overflow_mode, alignment) +#endif + +#ifndef MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT +#define MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT ((const int16_t) 84) +#endif + +#ifndef MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT +#define MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT ((const int16_t) 24) +#endif + +#ifndef menu_layer_set_normal_colors +#define menu_layer_set_normal_colors(menu_layer, background_color, text_color) +#endif + +#ifndef menu_layer_set_highlight_colors +#define menu_layer_set_highlight_colors(menu_layer, background_color, text_color) +#endif + +#ifndef menu_cell_layer_is_highlighted +#define menu_cell_layer_is_highlighted(cell_layer) (false) +#endif + +#ifndef menu_layer_is_index_selected +static inline bool menu_layer_is_index_selected(MenuLayer *menu_layer, MenuIndex *cell_index) { + MenuIndex current_index = menu_layer_get_selected_index(menu_layer); + return (current_index.section == cell_index->section && current_index.row == cell_index->row); +} +#endif + +//! Convenience macro to use SDK 3.0 function to set a `PropertyAnimation`'s +//! `values.from.grect` field. +#ifndef property_animation_set_from_grect +#define property_animation_set_from_grect(prop_anim, value_ptr) \ + ((prop_anim)->values.from.grect = *(value_ptr)) +#endif + +//! Convenience macro to use SDK 3.0 function to set a `PropertyAnimation`'s +//! `values.to.grect` field. +#ifndef property_animation_set_to_grect +#define property_animation_set_to_grect(prop_anim, value_ptr) \ + ((prop_anim)->values.to.grect = *(value_ptr)) +#endif + +// Voice API +typedef struct DictationSession DictationSession; +typedef struct DictationSessionStatus DictationSessionStatus; +void dictation_session_start(DictationSession *session); +#define DictationSessionStatusFailureSystemAborted 3 + +#ifndef scroll_layer_set_paging +#define scroll_layer_set_paging(scroll_layer, paging_enabled) +#endif + +#ifndef STATUS_BAR_LAYER_HEIGHT +#define STATUS_BAR_LAYER_HEIGHT 16 +#endif + +#endif + +// Legacy definitions for basalt on 3.0 +// These should eventually be removed in the future +#ifdef PBL_SDK_3 + +#define window_set_fullscreen(window, fullscreen) + +#endif diff --git a/src/util/dict.h b/src/util/dict.h new file mode 100644 index 00000000..79eefa68 --- /dev/null +++ b/src/util/dict.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +static inline void *dict_copy_to_buffer(DictionaryIterator *iter, size_t *length_out) { + size_t length = dict_size(iter); + void *buffer = malloc(length); + if (!buffer) { + return NULL; + } + + memcpy(buffer, iter->dictionary, length); + if (length_out) { + *length_out = length; + } + return buffer; +} + +static inline void dict_copy_from_buffer(DictionaryIterator *iter, void *buffer, size_t length) { + DictionaryIterator iter_copy = *iter; + dict_read_first(&iter_copy); + memcpy(iter->dictionary, buffer, length); + iter->cursor = (void*) iter->dictionary + length; +} diff --git a/src/util/display.h b/src/util/display.h new file mode 100644 index 00000000..a5620270 --- /dev/null +++ b/src/util/display.h @@ -0,0 +1,11 @@ +#pragma once + +#include "util/none.h" + +#if defined(PBL_RECT) +#define RECT_USAGE +#define ROUND_USAGE __attribute__((unused)) +#elif defined(PBL_ROUND) +#define RECT_USAGE __attribute__((unused)) +#define ROUND_USAGE +#endif diff --git a/src/util/graphics.h b/src/util/graphics.h new file mode 100644 index 00000000..d90b70bd --- /dev/null +++ b/src/util/graphics.h @@ -0,0 +1,61 @@ +#pragma once + +#include "util/compat.h" +#include "util/color.h" + +#include + +#ifndef PBL_SDK_3 + +#define GCompOpAlphaBlend GCompOpAnd + +#else + +#define GCompOpAlphaBlend GCompOpSet + +#endif + +static inline GPoint gpoint_neg(const GPoint a) { + return GPoint(-a.x, -a.y); +} + +static inline GPoint gpoint_add(const GPoint a, const GPoint b) { + return GPoint(a.x + b.x, a.y + b.y); +} + +static inline GPoint gpoint_polar(int32_t angle, int16_t radius) { + return GPoint(sin_lookup(angle) * radius / TRIG_MAX_RATIO, + cos_lookup(angle) * radius / TRIG_MAX_RATIO); +} + +static inline GRect grect_center_rect(const GRect *rect_a, const GRect *rect_b) { + return (GRect) { + .origin = { + .x = rect_a->origin.x + (rect_a->size.w - rect_b->size.w) / 2, + .y = rect_a->origin.y + (rect_a->size.h - rect_b->size.h) / 2, + }, + .size = rect_b->size, + }; +} + +static inline void graphics_draw_bitmap_centered(GContext *ctx, GBitmap *bitmap, const GRect frame) { + GRect bounds = gbitmap_get_bounds(bitmap); + graphics_draw_bitmap_in_rect(ctx, bitmap, grect_center_rect(&frame, &bounds)); +} + +static inline void graphics_context_set_alpha_blended(GContext *ctx, bool enable) { + if (enable) { + graphics_context_set_compositing_mode(ctx, GCompOpAlphaBlend); + } else { + graphics_context_set_compositing_mode(ctx, GCompOpAssign); + } +} + +static inline bool gbitmap_is_palette_black_and_white(GBitmap *bitmap) { + if (!bitmap || gbitmap_get_format(bitmap) != GBitmapFormat1BitPalette) { + return false; + } + const GColor8 *palette = gbitmap_get_palette(bitmap); + return (gcolor8_equal(palette[0], GColor8White) && gcolor8_equal(palette[1], GColor8Black)) || + (gcolor8_equal(palette[0], GColor8Black) && gcolor8_equal(palette[1], GColor8White)); +} diff --git a/src/util/graphics_text.h b/src/util/graphics_text.h new file mode 100644 index 00000000..0f8fde54 --- /dev/null +++ b/src/util/graphics_text.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#define TEXT_FLOW_DEFAULT_INSET 8 + +static inline void graphics_text_attributes_enable_paging_on_layer( + GTextAttributes *text_attributes, const Layer *layer, const GRect *box, const int inset) { + graphics_text_attributes_enable_screen_text_flow(text_attributes, inset); + const GPoint origin_on_screen = layer_convert_point_to_screen(layer, box->origin); + const GRect paging_on_screen = + layer_convert_rect_to_screen(layer, (GRect) { .size = layer_get_bounds(layer).size }); + graphics_text_attributes_enable_paging(text_attributes, origin_on_screen, paging_on_screen); +} + diff --git a/src/util/inverter_layer.h b/src/util/inverter_layer.h new file mode 100644 index 00000000..e56ec354 --- /dev/null +++ b/src/util/inverter_layer.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "util/math.h" + +#ifdef PBL_SDK_3 + +typedef struct InverterLayer InverterLayer; +struct InverterLayer; + +InverterLayer *inverter_layer_create(GRect bounds); +void inverter_layer_destroy(InverterLayer *inverter_layer); +Layer *inverter_layer_get_layer(InverterLayer *inverter_layer); + +#endif diff --git a/src/util/list1.h b/src/util/list1.h new file mode 100644 index 00000000..c9b81cf2 --- /dev/null +++ b/src/util/list1.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include + +typedef struct List1Node List1Node; + +struct List1Node { + List1Node *next; +}; + +typedef bool (*List1FilterCallback)(List1Node *node, void *data); + +static inline size_t list1_size(List1Node *node) { + size_t size = 0; + for (; node; node = node->next) { + size++; + } + return size; +} + +static inline List1Node *list1_last(List1Node *node) { + for (; node; node = node->next) { + if (!node->next) { + return node; + } + } + return NULL; +} + +static inline List1Node *list1_prev(List1Node *head, List1Node *node) { + for (List1Node *walk = head, *prev = NULL; walk; walk = walk->next) { + if (walk == node) { + return prev; + } + prev = walk; + } + return NULL; +} + +static inline List1Node *list1_prepend(List1Node **head, List1Node *node) { + node->next = *head; + *head = node; + return node; +} + +static inline List1Node *list1_append(List1Node **head, List1Node *node) { + if (*head) { + list1_last(*head)->next = node; + } else { + *head = node; + } + return node; +} + +static inline int list1_index(List1Node *head, List1Node *node) { + List1Node *walk = head; + for (int i = 0; walk; ++i) { + if (walk == node) { + return i; + } + walk = walk->next; + } + return -1; +} + +static inline List1Node *list1_insert(List1Node **head, int index, List1Node *node) { + List1Node **next_ref = head; + List1Node *walk = *head; + for (int i = 0; walk && i < index; ++i) { + next_ref = &walk->next; + walk = walk->next; + } + node->next = *next_ref; + *next_ref = node; + return node; +} + +static inline List1Node *list1_find_prev(List1Node *node, + List1FilterCallback callback, void *data, List1Node **prev_out) { + for (List1Node *prev = NULL; node; node = node->next) { + if (callback(node, data)) { + if (prev_out) { + *prev_out = prev; + } + return node; + } + prev = node; + } + return NULL; +} + +static inline List1Node *list1_find_last(List1Node *node, List1FilterCallback callback, void *data) { + List1Node *match = NULL; + for (; node; node = node->next) { + if (callback(node, data)) { + match = node; + } + } + return match; +} + +static inline List1Node *list1_find(List1Node *node, List1FilterCallback callback, void *data) { + return list1_find_prev(node, callback, data, NULL); +} + +static inline List1Node *list1_remove_prev(List1Node **head, List1Node *node, List1Node *prev) { + if (!node) { return NULL; } + if (*head == node) { + *head = node->next; + } + if (prev) { + prev->next = node->next; + } + node->next = NULL; + return node; +} + +static inline List1Node *list1_remove(List1Node **head, List1Node *node) { + return list1_remove_prev(head, node, list1_prev(*head, node)); +} + +static inline List1Node *list1_remove_one(List1Node **head, List1FilterCallback callback, void *data) { + List1Node *prev = NULL; + List1Node *node = list1_find_prev(*head, callback, data, &prev); + return list1_remove_prev(head, node, prev); +} diff --git a/src/util/math.h b/src/util/math.h new file mode 100644 index 00000000..6b6cb616 --- /dev/null +++ b/src/util/math.h @@ -0,0 +1,13 @@ +#pragma once + +#define MAX(a, b) ({ \ + __typeof__(a) __max_tmp_a = (a); \ + __typeof__(a) __max_tmp_b = (b); \ + (__max_tmp_a >= __max_tmp_b ? __max_tmp_a : __max_tmp_b); \ +}) + +#define MIN(a, b) ({ \ + __typeof__(a) __min_tmp_a = (a); \ + __typeof__(a) __min_tmp_b = (b); \ + (__min_tmp_a <= __min_tmp_b ? __min_tmp_a : __min_tmp_b); \ +}) diff --git a/src/util/memory.h b/src/util/memory.h new file mode 100644 index 00000000..45bc2cdf --- /dev/null +++ b/src/util/memory.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +static inline void *malloc0(size_t size) { + void *buf = malloc(size); + if (!buf) { + return buf; + } + + memset(buf, 0, size); + return buf; +} + diff --git a/src/util/menu_layer.h b/src/util/menu_layer.h new file mode 100644 index 00000000..d3d49e33 --- /dev/null +++ b/src/util/menu_layer.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "simply/simply.h" + +static inline ClickConfigProvider menu_layer_click_config_provider_accessor(ClickConfigProvider provider) { + static ClickConfigProvider s_provider; + if (provider) { + s_provider = provider; + } + return s_provider; +} + +static inline void menu_layer_click_config(void *context) { + menu_layer_click_config_provider_accessor(NULL)(context); +} + +static inline void menu_layer_set_click_config_provider_onto_window(MenuLayer *menu_layer, + ClickConfigProvider click_config_provider, Window *window) { + menu_layer_set_click_config_onto_window(menu_layer, window); + menu_layer_click_config_provider_accessor(window_get_click_config_provider(window)); + window_set_click_config_provider_with_context(window, click_config_provider, menu_layer); +} diff --git a/src/util/none.h b/src/util/none.h new file mode 100644 index 00000000..eda5568b --- /dev/null +++ b/src/util/none.h @@ -0,0 +1,3 @@ +#pragma once + +#define NONE diff --git a/src/util/noop.h b/src/util/noop.h new file mode 100644 index 00000000..a5e39b95 --- /dev/null +++ b/src/util/noop.h @@ -0,0 +1,3 @@ +#pragma once + +#define NOOP ((void) 0) diff --git a/src/util/platform.h b/src/util/platform.h new file mode 100644 index 00000000..b096c208 --- /dev/null +++ b/src/util/platform.h @@ -0,0 +1,43 @@ +#pragma once + +#include "util/none.h" + +#if defined(PBL_PLATFORM_APLITE) || defined(PBL_SDK_2) +#define IF_APLITE_ELSE(aplite, other) aplite +#define APLITE_USAGE +#else +#define IF_APLITE_ELSE(aplite, other) other +#define APLITE_USAGE __attribute__((unused)) +#endif + +#if defined(PBL_PLATFORM_BASALT) +#define IF_BASALT_ELSE(basalt, other) basalt +#define BASALT_USAGE +#else +#define IF_BASALT_ELSE(basalt, other) other +#define BASALT_USAGE __attribute__((unused)) +#endif + +#if defined(PBL_PLATFORM_CHALK) +#define IF_CHALK_ELSE(chalk, other) chalk +#define CHALK_USAGE +#else +#define IF_CHALK_ELSE(chalk, other) other +#define CHALK_USAGE __attribute__((unused)) +#endif + +#if defined(PBL_PLATFORM_DIORITE) +#define IF_DIORITE_ELSE(diorite, other) diorite +#define DIORITE_USAGE +#else +#define IF_DIORITE_ELSE(diorite, other) other +#define DIORITE_USAGE __attribute__((unused)) +#endif + +#if defined(PBL_PLATFORM_EMERY) +#define IF_EMERY_ELSE(emery, other) emery +#define EMERY_USAGE +#else +#define IF_EMERY_ELSE(emery, other) other +#define EMERY_USAGE __attribute__((unused)) +#endif \ No newline at end of file diff --git a/src/util/scroll_layer.h b/src/util/scroll_layer.h new file mode 100644 index 00000000..73a4e2f6 --- /dev/null +++ b/src/util/scroll_layer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "simply/simply.h" + +static inline ClickConfigProvider scroll_layer_click_config_provider_accessor(ClickConfigProvider provider) { + static ClickConfigProvider s_provider; + if (provider) { + s_provider = provider; + } + return s_provider; +} + +static inline void scroll_layer_click_config(void *context) { + window_set_click_context(BUTTON_ID_UP, context); + window_set_click_context(BUTTON_ID_DOWN, context); + scroll_layer_click_config_provider_accessor(NULL)(context); +} + +static inline void scroll_layer_set_click_config_provider_onto_window(ScrollLayer *scroll_layer, + ClickConfigProvider click_config_provider, Window *window, void *context) { + scroll_layer_set_click_config_onto_window(scroll_layer, window); + scroll_layer_click_config_provider_accessor(window_get_click_config_provider(window)); + window_set_click_config_provider_with_context(window, click_config_provider, context); +} diff --git a/src/util/sdk.h b/src/util/sdk.h new file mode 100644 index 00000000..c1d94362 --- /dev/null +++ b/src/util/sdk.h @@ -0,0 +1,15 @@ +#pragma once + +#include "util/none.h" + +#if defined(PBL_SDK_3) +#define IF_SDK_3_ELSE(sdk3, other) sdk3 +#define IF_SDK_2_ELSE(sdk2, other) other +#define SDK_3_USAGE +#define SDK_2_USAGE __attribute__((unused)) +#elif defined(PBL_SDK_2) +#define IF_SDK_3_ELSE(sdk3, other) other +#define IF_SDK_2_ELSE(sdk2, other) sdk2 +#define SDK_3_USAGE __attribute__((unused)) +#define SDK_2_USAGE +#endif diff --git a/src/util/status_bar_layer.h b/src/util/status_bar_layer.h new file mode 100644 index 00000000..804b4a74 --- /dev/null +++ b/src/util/status_bar_layer.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include "simply/simply.h" + +#ifdef PBL_SDK_2 + +typedef struct StatusBarLayer StatusBarLayer; +struct StatusBarLayer; + +//! Values that are used to indicate the different status bar separator modes. +typedef enum { + //! The default mode. No separator will be shown. + StatusBarLayerSeparatorModeNone = 0, + //! A dotted separator at the bottom of the status bar. + StatusBarLayerSeparatorModeDotted = 1, +} StatusBarLayerSeparatorMode; + +static inline StatusBarLayer *status_bar_layer_create(void) { + return NULL; +} + +static inline void status_bar_layer_destroy(StatusBarLayer *status_bar_layer) { +} + +static inline Layer *status_bar_layer_get_layer(StatusBarLayer *status_bar_layer) { + return (Layer *)status_bar_layer; +} + +static inline void status_bar_layer_set_colors(StatusBarLayer *status_bar_layer, GColor8 background, + GColor8 foreground) { +} + +static inline void status_bar_layer_set_separator_mode(StatusBarLayer *status_bar_layer, + StatusBarLayerSeparatorMode mode) { +} + +static inline void status_bar_layer_add_to_window(Window *window, StatusBarLayer *status_bar_layer) { + window_set_fullscreen(window, false); +} + +static inline void status_bar_layer_remove_from_window(Window *window, StatusBarLayer *status_bar_layer) { + window_set_fullscreen(window, true); +} + +#else + +static inline void status_bar_layer_add_to_window(Window *window, + StatusBarLayer *status_bar_layer) { + if (status_bar_layer) { + Layer *window_layer = window_get_root_layer(window); + layer_add_child(window_layer, status_bar_layer_get_layer(status_bar_layer)); + } +} + +static inline void status_bar_layer_remove_from_window(Window *window, + StatusBarLayer *status_bar_layer) { + if (status_bar_layer) { + layer_remove_from_parent(status_bar_layer_get_layer(status_bar_layer)); + } +} + +#endif diff --git a/src/util/string.h b/src/util/string.h new file mode 100644 index 00000000..c76fbdb4 --- /dev/null +++ b/src/util/string.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +static inline bool is_string(const char *str) { + return str && str[0]; +} + +static inline size_t strlen2(const char *str) { + return is_string(str) ? strlen(str) : 0; +} + +static inline char *strndup2(const char *str, size_t n) { + if (!str) { + return NULL; + } + + char *buffer = malloc(n + 1); + if (!buffer) { + return NULL; + } + + strncpy(buffer, str, n + 1); + buffer[n] = '\0'; + return buffer; +} + +static inline char *strdup2(const char *str) { + return strndup2(str, strlen2(str)); +} + +static inline bool strnset(char **str_field, const char *str, size_t n) { + free(*str_field); + *str_field = NULL; + + if (!is_string(str)) { + return true; + } + + return (*str_field = strndup2(str, n)); +} + +static inline bool strset(char **str_field, const char *str) { + return strnset(str_field, str, strlen2(str)); +} + +static inline void strset_truncated(char **str_field, const char *str) { + size_t n = strlen2(str); + for (; !strnset(str_field, str, n) && n > 1; n /= 2) {} +} diff --git a/src/util/window.h b/src/util/window.h new file mode 100644 index 00000000..62e8e604 --- /dev/null +++ b/src/util/window.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +static inline void window_stack_schedule_top_window_render() { + layer_mark_dirty(window_get_root_layer(window_stack_get_top_window())); +} diff --git a/waftools/aplite_legacy.py b/waftools/aplite_legacy.py new file mode 100644 index 00000000..624f19fd --- /dev/null +++ b/waftools/aplite_legacy.py @@ -0,0 +1,9 @@ +from waflib.Configure import conf + + +@conf +def appinfo_bitmap_to_png(ctx, appinfo_json): + if not ctx.supports_bitmap_resource(): + for res in appinfo_json['resources']['media']: + if res['type'] == 'bitmap': + res['type'] = 'png' diff --git a/waftools/configure_appinfo.py b/waftools/configure_appinfo.py new file mode 100644 index 00000000..d14f0e46 --- /dev/null +++ b/waftools/configure_appinfo.py @@ -0,0 +1,15 @@ +import json + +from waflib.Configure import conf + + +@conf +def configure_appinfo(ctx, transforms): + with open('appinfo.json', 'r') as appinfo_file: + appinfo_json = json.load(appinfo_file) + + for transform in transforms: + transform(appinfo_json) + + with open('appinfo.json', 'w') as appinfo_file: + json.dump(appinfo_json, appinfo_file, indent=2, sort_keys=True, separators=(',', ': ')) diff --git a/waftools/pebble_sdk_version.py b/waftools/pebble_sdk_version.py new file mode 100644 index 00000000..35a38431 --- /dev/null +++ b/waftools/pebble_sdk_version.py @@ -0,0 +1,16 @@ +from waflib.Configure import conf + + +@conf +def compare_sdk_version(ctx, platform, version): + target_env = ctx.all_envs[platform] if platform in ctx.all_envs else ctx.env + target_version = (int(target_env.SDK_VERSION_MAJOR or 0x5) * 0xff + + int(target_env.SDK_VERSION_MINOR or 0x19)) + other_version = int(version[0]) * 0xff + int(version[1]) + diff_version = target_version - other_version + return 0 if diff_version == 0 else diff_version / abs(diff_version) + + +@conf +def supports_bitmap_resource(ctx): + return (ctx.compare_sdk_version('aplite', [0x5, 0x48]) >= 0) diff --git a/wscript b/wscript index 35d540d7..aad84847 100644 --- a/wscript +++ b/wscript @@ -1,43 +1,167 @@ +import json +import os +import re + from waflib.Configure import conf top = '.' out = 'build' + def options(ctx): ctx.load('pebble_sdk') + ctx.load('aplite_legacy', tooldir='waftools') + ctx.load('configure_appinfo', tooldir='waftools') + ctx.load('pebble_sdk_version', tooldir='waftools') + + def configure(ctx): ctx.load('pebble_sdk') + ctx.configure_appinfo([ctx.appinfo_bitmap_to_png]) + + if ctx.env.TARGET_PLATFORMS: + for platform in ctx.env.TARGET_PLATFORMS: + ctx.configure_platform(platform) + else: + ctx.configure_platform() + + def build(ctx): ctx.load('pebble_sdk') + binaries = [] + js_target = ctx.concat_javascript(js_path='src/js') + + if ctx.env.TARGET_PLATFORMS: + for platform in ctx.env.TARGET_PLATFORMS: + ctx.build_platform(platform, binaries=binaries) + + ctx.pbl_bundle(binaries=binaries, + js=js_target) + else: + ctx.env.BUILD_DIR = 'aplite' + ctx.build_platform(binaries=binaries) + + elfs = binaries[0] + ctx.pbl_bundle(elf=elfs['app_elf'], + worker_elf=elfs['worker_elf'] if 'worker_elf' in elfs else None, + js=js_target) + + +@conf +def configure_platform(ctx, platform=None): + if platform is not None: + ctx.setenv(platform, ctx.all_envs[platform]) + + cflags = ctx.env.CFLAGS + cflags = [x for x in cflags if not x.startswith('-std=')] + cflags.extend(['-std=c11', + '-fms-extensions', + '-Wno-address', + '-Wno-type-limits', + '-Wno-missing-field-initializers']) + + ctx.env.CFLAGS = cflags + + +@conf +def build_platform(ctx, platform=None, binaries=None): + if platform is not None: + ctx.set_env(ctx.all_envs[platform]) + + build_worker = os.path.exists('worker_src') + + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), - cflags=['-Wno-type-limits', - '-Wno-address'], - target='pebble-app.elf') + target=app_elf) - js_target = ctx.concat_javascript(js=ctx.path.ant_glob('src/js/**/*.js')) + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'), + target=worker_elf) + else: + binaries.append({'platform': platform, 'app_elf': app_elf}) - ctx.pbl_bundle(elf='pebble-app.elf', - js=js_target) @conf -def concat_javascript(self, *k, **kw): - js_nodes = kw['js'] +def concat_javascript(ctx, js_path=None): + js_nodes = (ctx.path.ant_glob(js_path + '/**/*.js') + + ctx.path.ant_glob(js_path + '/**/*.json') + + ctx.path.ant_glob(js_path + '/**/*.coffee')) if not js_nodes: return [] def concat_javascript_task(task): - cmd = ['cat'] - cmd.extend(['"{}"'.format(x.abspath()) for x in task.inputs]) - cmd.extend(['>', "{}".format(task.outputs[0].abspath())]) - task.exec_command(' '.join(cmd)) + LOADER_PATH = "loader.js" + LOADER_TEMPLATE = ("__loader.define({relpath}, {lineno}, " + + "function(exports, module, require) {{\n{body}\n}});") + JSON_TEMPLATE = "module.exports = {body};" + APPINFO_PATH = "appinfo.json" + + def loader_translate(source, lineno): + return LOADER_TEMPLATE.format( + relpath=json.dumps(source['relpath']), + lineno=lineno, + body=source['body']) + + def coffeescript_compile(relpath, body): + try: + import coffeescript + except ImportError: + ctx.fatal(""" + CoffeeScript file '%s' found, but coffeescript module isn't installed. + You may try `pip install coffeescript` or `easy_install coffeescript`. + """ % (relpath)) + body = coffeescript.compile(body) + # change ".coffee" or ".js.coffee" extensions to ".js" + relpath = re.sub('(\.js)?\.coffee$', '.js', relpath) + return relpath, body + + sources = [] + for node in task.inputs: + relpath = os.path.relpath(node.abspath(), js_path) + with open(node.abspath(), 'r') as f: + body = f.read() + if relpath.endswith('.json'): + body = JSON_TEMPLATE.format(body=body) + elif relpath.endswith('.coffee'): + relpath, body = coffeescript_compile(relpath, body) + + compiled_js_path = os.path.join(out, js_path, relpath) + compiled_js_dir = os.path.dirname(compiled_js_path) + if not os.path.exists(compiled_js_dir): + os.makedirs(compiled_js_dir) + with open(compiled_js_path, 'w') as f: + f.write(body) + + if relpath == LOADER_PATH: + sources.insert(0, body) + else: + sources.append({'relpath': relpath, 'body': body}) + + with open(APPINFO_PATH, 'r') as f: + body = JSON_TEMPLATE.format(body=f.read()) + sources.append({'relpath': APPINFO_PATH, 'body': body}) + + sources.append('__loader.require("main");') + + with open(task.outputs[0].abspath(), 'w') as f: + lineno = 1 + for source in sources: + if type(source) is dict: + body = loader_translate(source, lineno) + else: + body = source + f.write(body + '\n') + lineno += body.count('\n') + 1 - js_target = self.path.make_node('build/src/js/pebble-js-app.js') + js_target = ctx.path.make_node('build/src/js/pebble-js-app.js') - self(rule=concat_javascript_task, + ctx(rule=concat_javascript_task, source=js_nodes, target=js_target)