MobileWebApp sample. Showcases GWT providing a single app providing specialized views for Desktop, Mobile and Tablet devices. Uses App Engine as a backend.
Requires Google App Engine SDK 1.4.2 or later.
Review at http://gwt-code-reviews.appspot.com/1427803
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10041 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/build.xml b/samples/build.xml
index b5061e1..f735d99 100644
--- a/samples/build.xml
+++ b/samples/build.xml
@@ -34,6 +34,10 @@
<gwt.ant dir="mail" />
</target>
+ <target name="mobilewebapp" description="Build mobile web app">
+ <gwt.ant dir="mobilewebapp" target="source" />
+ </target>
+
<target name="showcase" description="Build showcase">
<gwt.ant dir="showcase" />
</target>
@@ -54,6 +58,7 @@
<antcall target="json" />
<antcall target="logexample" />
<antcall target="mail" />
+ <antcall target="mobilewebapp" />
<antcall target="showcase" />
<antcall target="validation" />
<!-- don't include validationtck, it is not really a sample -->
diff --git a/samples/common.ant.xml b/samples/common.ant.xml
index 4d994e3..d68aac9 100755
--- a/samples/common.ant.xml
+++ b/samples/common.ant.xml
@@ -55,6 +55,12 @@
</then>
</if>
<if>
+ <available file="README.txt"/>
+ <then>
+ <copy tofile="${sample.build}/README.txt" file="README.txt"/>
+ </then>
+ </if>
+ <if>
<available file="user-build.xml"/>
<then>
<copy tofile="${sample.build}/build.xml" file="user-build.xml"/>
diff --git a/samples/mobilewebapp/README.txt b/samples/mobilewebapp/README.txt
new file mode 100644
index 0000000..23ca6b4
--- /dev/null
+++ b/samples/mobilewebapp/README.txt
@@ -0,0 +1,28 @@
+-- Preparation --
+
+Use Ant to build this project (http://ant.apache.org/). Ant uses the
+'build.xml' file in this folder, which describes exactly how to build
+your project. This file has been tested to work against Ant 1.7.1.
+The following assumes 'ant' is on your command line path.
+
+This project uses Google App Engine
+(https://appengine.google.com/). You can download the Google App
+Engine SDK from http://code.google.com/appengine/. The Ant build.xml
+script needs to know where your Google App Engine SDK is
+installed. Create a file named local.properties in the same folder as
+the build.xml file. It needs to contain a definition for the
+appengine.sdk property. For example, if the Google App Engine SDK is
+installed at /opt/appengine-sdk, a line in the local.properties file
+should contain:
+
+appengine.sdk=/opt/appengine-sdk
+
+-- Build from the command line with Ant --
+
+To run development mode, just type 'ant devmode'.
+
+To compile your project for deployment, just type 'ant'.
+
+To compile and also bundle into a .war file, type 'ant war'.
+
+For a full listing of other targets, type 'ant -p'.
diff --git a/samples/mobilewebapp/build.xml b/samples/mobilewebapp/build.xml
new file mode 100755
index 0000000..7cf1f95
--- /dev/null
+++ b/samples/mobilewebapp/build.xml
@@ -0,0 +1,14 @@
+<project name="mobilewebapp" default="build" basedir=".">
+ <property name="sample.root" value="mobilewebapp" />
+ <property name="sample.module" value="MobileWebApp" />
+ <property name="sample.path" value="src/main/java" />
+ <import file="../common.ant.xml" />
+ <!-- these are after the common.ant.xml so they have gwt.tools... -->
+ <path id="sample.extraclasspath">
+ <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA.jar" />
+ <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA-sources.jar" />
+ </path>
+ <fileset id="sample.server.libs" dir="${gwt.tools.lib}">
+ <include name="javax/validation/validation-api-1.0.0.GA-sources.jar" />
+ </fileset>
+</project>
diff --git a/samples/mobilewebapp/src/META-INF/jdoconfig.xml b/samples/mobilewebapp/src/META-INF/jdoconfig.xml
new file mode 100644
index 0000000..5f56aa1
--- /dev/null
+++ b/samples/mobilewebapp/src/META-INF/jdoconfig.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
+
+ <persistence-manager-factory name="transactions-optional">
+ <property name="javax.jdo.PersistenceManagerFactoryClass"
+ value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
+ <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
+ <property name="javax.jdo.option.NontransactionalRead" value="true"/>
+ <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
+ <property name="javax.jdo.option.RetainValues" value="true"/>
+ <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
+ </persistence-manager-factory>
+</jdoconfig>
diff --git a/samples/mobilewebapp/src/META-INF/persistence.xml b/samples/mobilewebapp/src/META-INF/persistence.xml
new file mode 100644
index 0000000..f6e4224
--- /dev/null
+++ b/samples/mobilewebapp/src/META-INF/persistence.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
+ <persistence-unit name="transactions-optional">
+ <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
+ <properties>
+ <property name="datanucleus.NontransactionalRead" value="true" />
+ <property name="datanucleus.NontransactionalWrite" value="true" />
+ <property name="datanucleus.ConnectionURL" value="appengine" />
+ </properties>
+ </persistence-unit>
+</persistence>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml
new file mode 100644
index 0000000..19d3ef4
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- Defines the formfactor property and its provider function. -->
+<module>
+
+ <!-- Determine if we are in a mobile browser. -->
+ <define-property name="formfactor" values="desktop,tablet,mobile"/>
+
+ <property-provider name="formfactor">
+ <![CDATA[
+ // Look for the formfactor as a url argument.
+ var args = location.search;
+ var start = args.indexOf("formfactor");
+ if (start >= 0) {
+ var value = args.substring(start);
+ var begin = value.indexOf("=") + 1;
+ var end = value.indexOf("&");
+ if (end == -1) {
+ end = value.length;
+ }
+ return value.substring(begin, end);
+ }
+
+ // Detect form factor from user agent.
+ var ua = navigator.userAgent.toLowerCase();
+ if (ua.indexOf("iphone") != -1 || ua.indexOf("ipod") != -1) {
+ // iphone and ipod.
+ return "mobile";
+ } else if (ua.indexOf("ipad") != -1) {
+ // ipad.
+ return "tablet";
+ } else if (ua.indexOf("android") != -1 || ua.indexOf("mobile") != -1) {
+ /*
+ * Android - determine the form factor of android devices based on the diagonal screen
+ * size. Anything under six inches is a phone, anything over six inches is a tablet.
+ */
+ var dpi = 160;
+ var width = $wnd.screen.width / dpi;
+ var height = $wnd.screen.height / dpi;
+ var size = Math.sqrt(width*width + height*height);
+ return (size < 6) ? "mobile" : "tablet";
+ }
+
+ // Everything else is a desktop.
+ return "desktop";
+ ]]>
+ </property-provider>
+
+</module>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
new file mode 100644
index 0000000..033d73b
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module rename-to='mobilewebapp'>
+ <!-- Inherit the core Web Toolkit stuff. -->
+ <inherits name='com.google.gwt.user.User'/>
+ <inherits name='com.google.gwt.sample.mobilewebapp.FormFactor'/>
+
+ <!-- Inherit RequestFactory. -->
+ <inherits name='com.google.web.bindery.requestfactory.RequestFactory'/>
+
+ <!-- Inherit Activities and Places. -->
+ <inherits name='com.google.gwt.activity.Activity'/>
+ <inherits name='com.google.gwt.place.Place'/>
+
+ <!-- Inherit the default GWT style sheet. You can change -->
+ <!-- the theme of your GWT application by uncommenting -->
+ <!-- any one of the following lines. -->
+ <inherits name='com.google.gwt.user.theme.clean.Clean'/>
+ <!-- <inherits name='com.google.gwt.user.theme.standard.Standard'/> -->
+ <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
+ <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/> -->
+
+ <!-- Other module inherits -->
+
+ <!-- Specify the app entry point class. -->
+ <entry-point class='com.google.gwt.sample.mobilewebapp.client.MobileWebApp'/>
+
+ <!-- Use ClientFactoryImpl by default -->
+ <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImpl">
+ <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+ </replace-with>
+
+ <!-- Use ClientFactoryImplMobile for mobile form factor. -->
+ <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImplMobile">
+ <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+ <when-property-is name="formfactor" value="mobile"/>
+ </replace-with>
+
+ <!-- Use ClientFactoryImplTablet for tablet form factor. -->
+ <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImplTablet">
+ <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+ <when-property-is name="formfactor" value="tablet"/>
+ </replace-with>
+
+ <!-- Specify the paths for translatable code -->
+ <source path='client'/>
+ <source path='shared'/>
+</module>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
new file mode 100644
index 0000000..d2dc4df
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+
+/**
+ * A factory for retrieving objects used throughout the application.
+ */
+public interface ClientFactory {
+
+ /**
+ * Get the event bus.
+ *
+ * @return the {@link EventBus} used throughout the app
+ */
+ EventBus getEventBus();
+
+ /**
+ * Get the local {@link Storage} object if supported.
+ *
+ * @return the local {@link Storage} object
+ */
+ Storage getLocalStorageIfSupported();
+
+ /**
+ * Get the {@link PlaceController}.
+ *
+ * @return the place controller
+ */
+ PlaceController getPlaceController();
+
+ /**
+ * Get the RequestFactory used to query the server.
+ *
+ * @return the request factory
+ */
+ MobileWebAppRequestFactory getRequestFactory();
+
+ /**
+ * Get the UI shell.
+ *
+ * @return the shell
+ */
+ MobileWebAppShell getShell();
+
+ /**
+ * Get an implementation of {@link TaskEditView}.
+ */
+ TaskEditView getTaskEditView();
+
+ /**
+ * Get an implementation of {@link TaskListView}.
+ */
+ TaskListView getTaskListView();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
new file mode 100644
index 0000000..dd68aad
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.web.bindery.requestfactory.gwt.client.DefaultRequestTransport;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.DesktopTaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.DesktopTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.MobileWebAppShellDesktop;
+
+/**
+ * Default implementation of {@link ClientFactory}. Used by desktop version.
+ */
+class ClientFactoryImpl implements ClientFactory {
+
+ /**
+ * The URL argument used to enable or disable local storage.
+ */
+ private static final String STORAGE_URL_ARG = "storage";
+
+ private final EventBus eventBus = new SimpleEventBus();
+ private final PlaceController placeController = new PlaceController(eventBus);
+ private final MobileWebAppRequestFactory requestFactory;
+ private MobileWebAppShell shell;
+ private final Storage localStorage;
+ private TaskEditView taskEditView;
+ private TaskListView taskListView;
+
+ public ClientFactoryImpl() {
+ DefaultRequestTransport requestTransport = new DefaultRequestTransport();
+ requestFactory = GWT.create(MobileWebAppRequestFactory.class);
+ requestFactory.initialize(eventBus, requestTransport);
+
+ // Initialize local storage.
+ String storageUrlValue = Window.Location.getParameter(STORAGE_URL_ARG);
+ if (storageUrlValue == null || storageUrlValue.startsWith("t")) {
+ localStorage = Storage.getLocalStorageIfSupported();
+ } else {
+ localStorage = null;
+ }
+ }
+
+ public EventBus getEventBus() {
+ return eventBus;
+ }
+
+ public Storage getLocalStorageIfSupported() {
+ return localStorage;
+ }
+
+ public PlaceController getPlaceController() {
+ return placeController;
+ }
+
+ public MobileWebAppRequestFactory getRequestFactory() {
+ return requestFactory;
+ }
+
+ public MobileWebAppShell getShell() {
+ if (shell == null) {
+ shell = createShell();
+ }
+ return shell;
+ }
+
+ public TaskEditView getTaskEditView() {
+ if (taskEditView == null) {
+ taskEditView = createTaskEditView();
+ }
+ return taskEditView;
+ }
+
+ public TaskListView getTaskListView() {
+ if (taskListView == null) {
+ taskListView = createTaskListView();
+ }
+ return taskListView;
+ }
+
+ /**
+ * Create the application UI shell.
+ *
+ * @return the UI shell
+ */
+ protected MobileWebAppShell createShell() {
+ return new MobileWebAppShellDesktop(this);
+ }
+
+ /**
+ * Create a {@link TaskEditView}.
+ *
+ * @return a new {@link TaskEditView}
+ */
+ protected TaskEditView createTaskEditView() {
+ return new DesktopTaskEditView();
+ }
+
+ /**
+ * Create a {@link TaskListView}.
+ *
+ * @return a new {@link TaskListView}
+ */
+ protected TaskListView createTaskListView() {
+ return new DesktopTaskListView();
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
new file mode 100644
index 0000000..ba4f31c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileWebAppShellMobile;
+
+/**
+ * Mobile version of {@link ClientFactory}.
+ */
+public class ClientFactoryImplMobile extends ClientFactoryImpl {
+
+ @Override
+ protected MobileWebAppShell createShell() {
+ return new MobileWebAppShellMobile(this);
+ }
+
+ @Override
+ protected TaskEditView createTaskEditView() {
+ return new MobileTaskEditView();
+ }
+
+ @Override
+ protected TaskListView createTaskListView() {
+ return new MobileTaskListView();
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
new file mode 100644
index 0000000..12b3699
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.tablet.MobileWebAppShellTablet;
+import com.google.gwt.sample.mobilewebapp.client.tablet.TabletTaskEditView;
+
+/**
+ * Tablet version of {@link ClientFactory}.
+ */
+public class ClientFactoryImplTablet extends ClientFactoryImpl {
+
+ @Override
+ protected MobileWebAppShell createShell() {
+ return new MobileWebAppShellTablet(this);
+ }
+
+ @Override
+ protected TaskEditView createTaskEditView() {
+ return new TabletTaskEditView();
+ }
+
+ @Override
+ protected TaskListView createTaskListView() {
+ // Use the mobile list view on tablets.
+ return new MobileTaskListView();
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
new file mode 100644
index 0000000..d2cc221
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.activity.shared.ActivityManager;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceChangeEvent;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.gwt.place.shared.PlaceHistoryHandler;
+import com.google.gwt.place.shared.PlaceHistoryHandler.DefaultHistorian;
+import com.google.gwt.place.shared.PlaceHistoryHandler.Historian;
+import com.google.gwt.sample.mobilewebapp.client.activity.AppActivityMapper;
+import com.google.gwt.sample.mobilewebapp.client.place.AppPlaceHistoryMapper;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
+
+/**
+ * Entry point classes define <code>onModuleLoad()</code>.
+ */
+public class MobileWebApp implements EntryPoint {
+
+ static final String HISTORY_SAVE_KEY = "SAVEPLACE";
+ final Storage storage = Storage.getLocalStorageIfSupported();
+
+ /**
+ * This is the entry point method.
+ */
+ public void onModuleLoad() {
+ /*
+ * Initialize the ClientFactory, which includes common resources used
+ * throughout the app. We bundle them all into a ClientFactory so we only
+ * have to pass one argument around.
+ */
+ ClientFactory clientFactory = GWT.create(ClientFactory.class);
+ EventBus eventBus = clientFactory.getEventBus();
+ PlaceController placeController = clientFactory.getPlaceController();
+
+ /*
+ * Add the main application shell to the RootLayoutPanel. The shell includes
+ * the header bar at the top and a content area.
+ */
+ MobileWebAppShell shell = clientFactory.getShell();
+ RootLayoutPanel.get().add(shell);
+
+ /*
+ * Start ActivityManager for the main widget with our ActivityMapper. The
+ * ActivityManager starts an activity based on the current place.
+ */
+ ActivityMapper activityMapper = new AppActivityMapper(clientFactory);
+ ActivityManager activityManager = new ActivityManager(activityMapper, eventBus);
+ activityManager.setDisplay(shell);
+
+ /*
+ * Start PlaceHistoryHandler with our PlaceHistoryMapper. The
+ * PlaceHistoryHandler chooses the correct place based on the history token
+ * in the URL.
+ */
+ final AppPlaceHistoryMapper historyMapper = GWT.create(AppPlaceHistoryMapper.class);
+ Historian historian = (Historian) GWT.create(DefaultHistorian.class);
+
+ PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper, historian);
+ Place savedPlace = null;
+ if (storage != null) {
+ try {
+ // wrap in try-catch in case stored value is invalid
+ savedPlace = historyMapper.getPlace(storage.getItem(HISTORY_SAVE_KEY));
+ } catch (Throwable t) {
+ // ignore error
+ }
+ }
+ if (savedPlace == null) {
+ savedPlace = new TaskListPlace(true);
+ }
+ historyHandler.register(placeController, eventBus, savedPlace);
+
+ // Go to the place represented in the URL else default place.
+ historyHandler.handleCurrentHistory();
+
+ /* Hook up storage to listen to event bus */
+ eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
+ public void onPlaceChange(PlaceChangeEvent event) {
+ Place newPlace = event.getNewPlace();
+ if (storage != null) {
+ storage.setItem(HISTORY_SAVE_KEY, historyMapper.getToken(newPlace));
+ }
+ }
+ });
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java
new file mode 100644
index 0000000..1900a4c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+
+/**
+ * Request factory for this app.
+ */
+public interface MobileWebAppRequestFactory extends RequestFactory {
+
+ /**
+ * Create a new {@link TaskRequest}.
+ */
+ TaskRequest taskRequest();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
new file mode 100644
index 0000000..fe94209
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.gwt.user.client.ui.IsWidget;
+
+/**
+ * An interface that describes the UI Shell.
+ */
+public interface MobileWebAppShell extends AcceptsOneWidget, IsWidget {
+
+ /**
+ * Check if the task list is included in the shell. If true, a
+ * {@link com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity}
+ * will not be started.
+ *
+ * @return true if included, false if not
+ */
+ boolean isTaskListIncluded();
+
+ /**
+ * Set the handler to invoke when the add button is pressed. If no handler is
+ * specified, the button is hidden.
+ *
+ * @param handler the handler to add to the button, or null to hide
+ */
+ void setAddButtonHandler(ClickHandler handler);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java
new file mode 100644
index 0000000..91e8686
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.ResizeComposite;
+
+/**
+ * Base class for UI shell.
+ */
+public abstract class MobileWebAppShellBase extends ResizeComposite implements
+ MobileWebAppShell {
+
+ /**
+ * Calculate the orientation based on the screen dimensions.
+ *
+ * @return true if portrait, false if lansdcape
+ */
+ private static boolean calculateOrientationPortrait() {
+ return Window.getClientHeight() > Window.getClientWidth();
+ }
+
+ /**
+ * The registration for the window resize handler.
+ */
+ private HandlerRegistration windowResizeHandler;
+
+ /**
+ * The current orientation of the app.
+ */
+ private boolean isOrientationPortrait;
+
+ /**
+ * Adjust the orientation based on the current window height and width. This
+ * is a no-op by default, but subclasses can override it.
+ *
+ * @param isPortrait true if in portrait orientation, false if landscape
+ */
+ protected void adjustOrientation(boolean isPortrait) {
+ // No-op by default.
+ }
+
+ /**
+ * Check if we are in portrait or landscape orientation.
+ *
+ * @return true if portrait, false if lansdcape
+ */
+ protected boolean isOrientationPortrait() {
+ return isOrientationPortrait;
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ /*
+ * Listen for changes in the window size so we can adjust the orientation of
+ * the app. This will also catch orientation changes on mobile devices.
+ */
+ windowResizeHandler = Window.addResizeHandler(new ResizeHandler() {
+ public void onResize(ResizeEvent event) {
+ if (isOrientationPortrait != calculateOrientationPortrait()) {
+ isOrientationPortrait = !isOrientationPortrait;
+ adjustOrientation(isOrientationPortrait);
+ }
+ }
+ });
+
+ // Initialize the orientation. Do not animate when we first load the shell.
+ isOrientationPortrait = calculateOrientationPortrait();
+ adjustOrientation(isOrientationPortrait);
+ }
+
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+ windowResizeHandler.removeHandler();
+ windowResizeHandler = null;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java
new file mode 100644
index 0000000..65fa8ec
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.server.domain.Task;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+import java.util.List;
+
+/**
+ * Remote request for {@link Task}.
+ */
+@Service(Task.class)
+public interface TaskRequest extends RequestContext {
+
+ /**
+ * Create a {@link Request} for all tasks.
+ *
+ * @return a {@link Request}
+ */
+ Request<List<TaskProxy>> findAllTasks();
+
+ /**
+ * Create a {@link Request} to find a Task by id.
+ *
+ * @param id the task id
+ * @return a {@link Request}
+ */
+ Request<TaskProxy> findTask(Long id);
+
+ /**
+ * Persist a Task instance in the datastore.
+ *
+ * @return an {@link InstanceRequest}
+ */
+ InstanceRequest<TaskProxy, Void> persist();
+
+ /**
+ * Remove a Task instance from the datastore.
+ *
+ * @return an {@link InstanceRequest}
+ */
+ InstanceRequest<TaskProxy, Void> remove();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
new file mode 100644
index 0000000..ac387d1
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.Activity;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+
+/**
+ * A mapping of places to activities used by this application.
+ */
+public class AppActivityMapper implements ActivityMapper {
+
+ private final ClientFactory clientFactory;
+
+ public AppActivityMapper(ClientFactory clientFactory) {
+ this.clientFactory = clientFactory;
+ }
+
+ public Activity getActivity(Place place) {
+ if (place instanceof TaskListPlace) {
+ // The list of tasks.
+ if (!clientFactory.getShell().isTaskListIncluded()) {
+ // Do not start a task list activity if the task list is always visible.
+ return new TaskListActivity(clientFactory, ((TaskListPlace) place).isTaskListStale());
+ }
+ } else if (place instanceof TaskEditPlace) {
+ // Editable view of a task.
+ return new TaskEditActivity((TaskEditPlace) place, clientFactory);
+ }
+ return null;
+ }
+
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
new file mode 100644
index 0000000..29f5b95
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.AbstractActivity;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.TaskRequest;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+
+/**
+ * Activity that presents a task to be edited.
+ */
+public class TaskEditActivity extends AbstractActivity implements TaskEditView.Presenter {
+
+ private static final int ONE_HOUR = 3600000;
+
+ private final ClientFactory clientFactory;
+
+ /**
+ * A boolean indicating whether or not this activity is still active. The user
+ * might move to another activity while this one is loading, in which case we
+ * do not want to do any more work.
+ */
+ private boolean isDead = false;
+
+ /**
+ * The current task being edited.
+ */
+ private TaskProxy task;
+
+ /**
+ * The ID of the current task being edited.
+ */
+ private final Long taskId;
+
+ /**
+ * Construct a new {@link TaskEditActivity}.
+ *
+ * @param place the task being edited
+ * @param clientFactory the {@link ClientFactory} of shared resources
+ */
+ public TaskEditActivity(TaskEditPlace place, ClientFactory clientFactory) {
+ this.taskId = place.getTaskId();
+ this.clientFactory = clientFactory;
+ }
+
+ public void deleteTask() {
+ if (task == null) {
+ doCancelTask();
+ } else {
+ doDeleteTask();
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ // Ignore all incoming responses to the requests from this activity.
+ isDead = true;
+ clientFactory.getTaskEditView().setLocked(false);
+ }
+
+ @Override
+ public void onStop() {
+ // Ignore all incoming responses to the requests from this activity.
+ isDead = true;
+ clientFactory.getTaskEditView().setLocked(false);
+ }
+
+ public void saveTask(boolean addToCalendar) {
+ if (task == null) {
+ doCreateTask(addToCalendar);
+ } else {
+ doUpdateTask();
+ }
+ }
+
+ public void start(AcceptsOneWidget container, EventBus eventBus) {
+ // Hide the 'add' button in the shell.
+ clientFactory.getShell().setAddButtonHandler(null);
+
+ // Set the presenter on the view.
+ final TaskEditView view = clientFactory.getTaskEditView();
+ view.setPresenter(this);
+
+ // Clear the display until the task is loaded.
+ showTask(null);
+
+ if (taskId != null) {
+ // Lock the display until the task is loaded.
+ view.setLocked(true);
+
+ // Load the existing task.
+ clientFactory.getRequestFactory().taskRequest().findTask(this.taskId).fire(
+ new Receiver<TaskProxy>() {
+ @Override
+ public void onSuccess(TaskProxy response) {
+ // Early exit if this activity has already been cancelled.
+ if (isDead) {
+ return;
+ }
+
+ // Show the task.
+ showTask(response);
+ view.setLocked(false);
+ }
+ });
+ }
+
+ // Display the view.
+ container.setWidget(view.asWidget());
+ }
+
+ /**
+ * Cancel the current task.
+ */
+ private void doCancelTask() {
+ clientFactory.getPlaceController().goTo(new TaskListPlace(false));
+ }
+
+ /**
+ * Create a new task.
+ *
+ * @param addToCalendar true to add the task to the calendar
+ */
+ private void doCreateTask(final boolean addToCalendar) {
+ TaskRequest request = clientFactory.getRequestFactory().taskRequest();
+ final TaskProxy toCreate = request.create(TaskProxy.class);
+ populateTaskFromView(toCreate);
+ request.persist().using(toCreate).fire(new Receiver<Void>() {
+ @Override
+ public void onSuccess(Void response) {
+ onTaskCreated(toCreate, addToCalendar);
+ }
+ });
+ }
+
+ /**
+ * Delete the current task.
+ */
+ private void doDeleteTask() {
+ if (task == null) {
+ return;
+ }
+
+ // Delete the task in the data store.
+ final TaskProxy toDelete = this.task;
+ clientFactory.getRequestFactory().taskRequest().remove().using(toDelete).fire(
+ new Receiver<Void>() {
+ @Override
+ public void onSuccess(Void response) {
+ onTaskDeleted(toDelete);
+ }
+ });
+ }
+
+ /**
+ * Update the current task.
+ */
+ private void doUpdateTask() {
+ if (task == null) {
+ return;
+ }
+
+ // Create a mutable version of the current task.
+ TaskRequest request = clientFactory.getRequestFactory().taskRequest();
+ final TaskProxy toEdit = request.edit(task);
+ populateTaskFromView(toEdit);
+
+ // Persist the changes.
+ request.persist().using(toEdit).fire(new Receiver<Void>() {
+ @Override
+ public void onSuccess(Void response) {
+ onTaskUpdated(toEdit);
+ }
+ });
+ }
+
+ /**
+ * Notify the user of a message.
+ *
+ * @param message the message to display
+ */
+ private void notify(String message) {
+ // TODO Add notification pop-up
+ }
+
+ /**
+ * Called when a task has been successfully created.
+ *
+ * @param task the task that was created
+ * @param addToCalendar true to add the task to the calendar
+ */
+ private void onTaskCreated(TaskProxy task, boolean addToCalendar) {
+ // Notify the user that the task was created.
+ notify("Created task '" + task.getName() + "'");
+
+ // Return to the task list.
+ clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+ }
+
+ /**
+ * Called when a task has been successfully deleted.
+ *
+ * @param task the task that was deleted
+ */
+ private void onTaskDeleted(TaskProxy task) {
+ // Notify the user that the task was deleted.
+ notify("Task Deleted");
+
+ // Return to the task list.
+ clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+ }
+
+ /**
+ * Called when a task has been successfully updated.
+ *
+ * @param task the task that was updated
+ */
+ private void onTaskUpdated(TaskProxy task) {
+ // Notify the user that the task was updated.
+ notify("Task Updated");
+
+ // Return to the task list.
+ clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+ }
+
+ /**
+ * Populate the specified task using the values the user specified in the
+ * view.
+ *
+ * @param task the task to populate
+ */
+ private void populateTaskFromView(TaskProxy task) {
+ TaskEditView view = clientFactory.getTaskEditView();
+ task.setName(view.getName());
+ task.setNotes(view.getNotes());
+ task.setDueDate(view.getDueDate());
+ }
+
+ /**
+ * Show the specified task in the view.
+ *
+ * @param task the task to show
+ */
+ private void showTask(TaskProxy task) {
+ this.task = task;
+ TaskEditView view = clientFactory.getTaskEditView();
+ if (task == null) {
+ // Create a new task.
+ view.setEditing(false);
+ view.setDueDate(null);
+ view.setName("");
+ view.setNotes("");
+ } else {
+ // Edit an existing task.
+ view.setEditing(true);
+ view.setDueDate(task.getDueDate());
+ view.setName(task.getName());
+ view.setNotes(task.getNotes());
+ }
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
new file mode 100644
index 0000000..026a6ce
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.user.client.ui.IsWidget;
+
+import java.util.Date;
+
+/**
+ * A view of {@link TaskEditActivity}.
+ */
+public interface TaskEditView extends IsWidget {
+
+ /**
+ * The presenter for this view.
+ */
+ public static interface Presenter {
+ /**
+ * Delete the current task or cancel the creation of a task.
+ */
+ void deleteTask();
+
+ /**
+ * Create a new task or save the current task based on the values in the
+ * inputs.
+ *
+ * @param addToCalendar true to add the task to the calendar
+ */
+ void saveTask(boolean addToCalendar);
+ }
+
+ Date getDueDate();
+
+ String getName();
+
+ String getNotes();
+
+ /**
+ * Set the due date of the task.
+ */
+ void setDueDate(Date date);
+
+ /**
+ * Specify whether the view is editing an existing task or creating a new
+ * task.
+ *
+ * @param isEditing true if editing, false if creating
+ */
+ void setEditing(boolean isEditing);
+
+ /**
+ * Lock or unlock the UI so the user cannot enter data. The UI is locked until
+ * the task is loaded.
+ *
+ * @param locked true to lock, false to unlock
+ */
+ void setLocked(boolean locked);
+
+ /**
+ * Set the task name.
+ */
+ void setName(String name);
+
+ /**
+ * Set the notes associated with the task.
+ */
+ void setNotes(String notes);
+
+ /**
+ * Set the {@link Presenter} for this view.
+ *
+ * @param presenter the presenter
+ */
+ void setPresenter(Presenter presenter);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
new file mode 100644
index 0000000..832931a
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.AbstractActivity;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxyImpl;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.ServerFailure;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Activity that presents a list of tasks.
+ */
+public class TaskListActivity extends AbstractActivity implements TaskListView.Presenter {
+
+ /**
+ * Event fired when the task list is updated.
+ */
+ public static class TaskListUpdateEvent extends GwtEvent<TaskListActivity.TaskListUpdateHandler> {
+
+ /**
+ * Handler type.
+ */
+ private static Type<TaskListUpdateHandler> TYPE;
+
+ /**
+ * Gets the type associated with this event.
+ *
+ * @return returns the handler type
+ */
+ public static Type<TaskListUpdateHandler> getType() {
+ if (TYPE == null) {
+ TYPE = new Type<TaskListUpdateHandler>();
+ }
+ return TYPE;
+ }
+
+ private final List<TaskProxy> tasks;
+
+ public TaskListUpdateEvent(List<TaskProxy> tasks) {
+ this.tasks = tasks;
+ }
+
+ @Override
+ public Type<TaskListUpdateHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ public List<TaskProxy> getTasks() {
+ return tasks;
+ }
+
+ @Override
+ protected void dispatch(TaskListUpdateHandler handler) {
+ handler.onTaskListUpdated(this);
+ }
+ }
+
+ /**
+ * Handler for {@link TaskListUpdateEvent}.
+ */
+ public static interface TaskListUpdateHandler extends EventHandler {
+
+ /**
+ * Called when the task list is updated.
+ */
+ void onTaskListUpdated(TaskListUpdateEvent event);
+ }
+
+ private static final String TASKLIST_SAVE_KEY = "TASKLIST";
+ private static final String TASKSEP = "&&";
+ private static final String FIELDSEP = "@@";
+ private static final String FIELDEMPTY = "***";
+
+ /**
+ * The delay in milliseconds between calls to refresh the task list.
+ */
+ private static final int REFRESH_DELAY = 5000;
+
+ /**
+ * Convert a task proxy list into a string.
+ */
+ private static String getStringFromTaskProxy(List<TaskProxy> list) {
+ StringBuilder sb = new StringBuilder();
+ for (TaskProxy proxy : list) {
+ sb.append(proxy.getDueDate() != null ? proxy.getDueDate().getTime() : FIELDEMPTY);
+ sb.append(FIELDSEP);
+ sb.append(proxy.getId() != null ? proxy.getId() : "");
+ sb.append(FIELDSEP);
+ String name = proxy.getName();
+ sb.append(name != null && name.length() > 0 ? proxy.getName() : FIELDEMPTY);
+ sb.append(FIELDSEP);
+ String notes = proxy.getNotes();
+ sb.append(notes != null && notes.length() > 0 ? proxy.getNotes() : FIELDEMPTY);
+ sb.append(TASKSEP);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parse a task proxy list from a string.
+ */
+ private static List<TaskProxy> getTaskProxyFromString(String taskProxyList) {
+ ArrayList<TaskProxy> list = new ArrayList<TaskProxy>(0);
+ if (taskProxyList == null) {
+ return list;
+ }
+ // taskproxy1&&taskproxy2&&taskproxy3&&...
+ String taskProxyStrings[] = taskProxyList.split(TASKSEP);
+ for (String taskProxyString : taskProxyStrings) {
+ if (taskProxyString == null) {
+ continue;
+ }
+ // date@@id@@name@@notes
+ String taskProxyStringData[] = taskProxyString.split(FIELDSEP);
+ if (taskProxyStringData.length >= 4) {
+ // collect the fields
+ String dateString = taskProxyStringData[0];
+ String idString = taskProxyStringData[1];
+ String nameString = taskProxyStringData[2];
+ if (FIELDEMPTY.equals(nameString)) {
+ nameString = null;
+ }
+ String notesString = taskProxyStringData[3];
+ if (FIELDEMPTY.equals(notesString)) {
+ notesString = null;
+ }
+ // parse the numerical fields
+ Date dueDate = null;
+ try {
+ dueDate = new Date(Long.parseLong(dateString));
+ } catch (NumberFormatException nfe) {
+ }
+ Long idLong = 0L;
+ try {
+ idLong = Long.parseLong(idString);
+ } catch (NumberFormatException nfe) {
+ }
+ // create and populate the TaskProxy
+ TaskProxyImpl taskProxy = new TaskProxyImpl();
+ taskProxy.setDueDate(dueDate);
+ taskProxy.setId(idLong);
+ taskProxy.setName(nameString);
+ taskProxy.setNotes(notesString);
+ list.add(taskProxy);
+ }
+ }
+ return list;
+ }
+
+ private final Storage storage;
+
+ /**
+ * A boolean indicating that we should clear the task list when started.
+ */
+ private final boolean clearTaskList;
+
+ private final ClientFactory clientFactory;
+
+ /**
+ * A boolean indicating whether or not this activity is still active. The user
+ * might move to another activity while this one is loading, in which case we
+ * do not want to do any more work.
+ */
+ private boolean isDead = false;
+
+ /**
+ * The refresh timer used to periodically refresh the task list.
+ */
+ private Timer refreshTimer;
+
+ /**
+ * Construct a new {@link TaskListActivity}.
+ *
+ * @param clientFactory the {@link ClientFactory} of shared resources
+ * @param clearTaskList true to clear the task list, false not to
+ */
+ public TaskListActivity(ClientFactory clientFactory, boolean clearTaskList) {
+ this.clientFactory = clientFactory;
+ this.clearTaskList = clearTaskList;
+ this.storage = clientFactory.getLocalStorageIfSupported();
+ }
+
+ @Override
+ public void onCancel() {
+ killActivity();
+ }
+
+ @Override
+ public void onStop() {
+ killActivity();
+ }
+
+ public void selectTask(TaskProxy selected) {
+ // Go into edit mode when a task is selected.
+ clientFactory.getPlaceController().goTo(TaskEditPlace.createTaskEditPlace(selected.getId()));
+ }
+
+ public void start(AcceptsOneWidget container, EventBus eventBus) {
+ // Add a handler to the 'add' button in the shell.
+ clientFactory.getShell().setAddButtonHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ clientFactory.getPlaceController().goTo(TaskEditPlace.getTaskCreatePlace());
+ }
+ });
+
+ // Set the presenter on the view.
+ final TaskListView view = clientFactory.getTaskListView();
+ view.setPresenter(this);
+
+ // Clear the task list and display it.
+ if (clearTaskList) {
+ view.clearList();
+ }
+ container.setWidget(view);
+
+ // Create a timer to periodically refresh the task list.
+ refreshTimer = new Timer() {
+ @Override
+ public void run() {
+ refreshTaskList();
+ }
+ };
+
+ // Load the saved task list from storage
+ if (storage != null) { // if storage is supported
+ String taskString = storage.getItem(TASKLIST_SAVE_KEY);
+ if (taskString != null) {
+ List<TaskProxy> list = getTaskProxyFromString(taskString);
+ setTasks(list);
+ }
+ }
+
+ // Request the task list now.
+ refreshTaskList();
+ }
+
+ /**
+ * Kill this activity.
+ */
+ private void killActivity() {
+ // Ignore all incoming responses to the requests from this activity.
+ isDead = true;
+
+ // Kill the refresh timer.
+ if (refreshTimer != null) {
+ refreshTimer.cancel();
+ }
+ }
+
+ /**
+ * Refresh the task list.
+ */
+ private void refreshTaskList() {
+ clientFactory.getRequestFactory().taskRequest().findAllTasks().fire(
+ new Receiver<List<TaskProxy>>() {
+ @Override
+ public void onFailure(ServerFailure error) {
+ // ignore
+ }
+
+ @Override
+ public void onSuccess(List<TaskProxy> response) {
+ // Early exit if this activity has already been canceled.
+ if (isDead) {
+ return;
+ }
+
+ // Display the tasks in the view.
+ if (response == null) {
+ setTasks(Collections.<TaskProxy> emptyList());
+ } else {
+ setTasks(response);
+
+ // save the response to storage
+ if (storage != null) { // if storage is supported
+ String responseString = getStringFromTaskProxy(response);
+ storage.setItem(TASKLIST_SAVE_KEY, responseString);
+ }
+ }
+
+ // Restart the timer.
+ refreshTimer.schedule(REFRESH_DELAY);
+ }
+ });
+ }
+
+ /**
+ * Set the list of tasks.
+ */
+ private void setTasks(List<TaskProxy> tasks) {
+ clientFactory.getTaskListView().setTasks(tasks);
+ clientFactory.getEventBus().fireEventFromSource(new TaskListUpdateEvent(tasks), this);
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
new file mode 100644
index 0000000..2bc0aa9
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.List;
+
+/**
+ * A view of a {@link TaskListActivity}.
+ */
+public interface TaskListView extends IsWidget {
+
+ /**
+ * The presenter for this view.
+ */
+ public static interface Presenter {
+
+ /**
+ * Select a task.
+ *
+ * @param selected the select task
+ */
+ void selectTask(TaskProxy selected);
+ }
+
+ /**
+ * Clear the list of tasks.
+ */
+ void clearList();
+
+ /**
+ * Set the {@link Presenter} for this view.
+ *
+ * @param presenter the presenter
+ */
+ void setPresenter(Presenter presenter);
+
+ /**
+ * Set the list of tasks to display.
+ *
+ * @param tasks the list of tasks
+ */
+ void setTasks(List<TaskProxy> tasks);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
new file mode 100644
index 0000000..349793f
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class DesktopTaskEditView extends Composite implements TaskEditView {
+
+ /**
+ * The UiBinder interface.
+ */
+ interface DesktopTaskEditViewUiBinder extends
+ UiBinder<Widget, DesktopTaskEditView> {
+ }
+
+ /**
+ * The UiBinder used to generate the view.
+ */
+ private static DesktopTaskEditViewUiBinder uiBinder = GWT.create(DesktopTaskEditViewUiBinder.class);
+
+ /**
+ * The formatter used to format the date.
+ */
+ private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+ /**
+ * The glass panel used to lock the UI.
+ */
+ private static PopupPanel glassPanel;
+
+ /**
+ * Show or hide the glass panel used to lock the UI will the task loads.
+ *
+ * @param visible true to show, false to hide
+ */
+ private static void setGlassPanelVisible(boolean visible) {
+ // Initialize the panel.
+ if (glassPanel == null) {
+ glassPanel = new DecoratedPopupPanel(false, true);
+ glassPanel.setWidget(new Label("Loading..."));
+ }
+
+ if (visible) {
+ // Show the loading panel.
+ glassPanel.center();
+ } else {
+ // Hide the loading panel.
+ glassPanel.hide();
+ }
+ }
+
+ /**
+ * The checkbox used to specify that the task should be added to the calendar.
+ */
+ @UiField
+ CheckBox calendarCheckbox;
+
+ /**
+ * The button used to select the date.
+ */
+ @UiField
+ Button dateButton;
+
+ /**
+ * The button used to delete a task or cancel changes.
+ */
+ @UiField
+ Button deleteButton;
+
+ /**
+ * The text box used to enter the task name.
+ */
+ @UiField
+ TextBoxBase nameBox;
+
+ /**
+ * The text box used to enter task notes.
+ */
+ @UiField
+ TextBoxBase notesBox;
+
+ /**
+ * The text box used to save changes or create a new task.
+ */
+ @UiField
+ Button saveButton;
+
+ /**
+ * The current date value.
+ */
+ private Date currentDate;
+
+ /**
+ * The popup panel that contains a date picker for selecting the date.
+ */
+ private final PopupPanel datePickerPopup;
+
+ /**
+ * The {@link Presenter} for this view.
+ */
+ private Presenter presenter;
+
+ /**
+ * Construct a new {@link DesktopTaskEditView}.
+ *
+ * @param presenter the {@link Presenter} that handles interactions
+ */
+ public DesktopTaskEditView() {
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // Create the datePickerPopup.
+ final DatePicker datePicker = new DatePicker();
+ datePickerPopup = new PopupPanel(true, true);
+ datePickerPopup.setWidget(datePicker);
+ datePickerPopup.setGlassEnabled(true);
+
+ /*
+ * When the user clicks on the date button, open a date picker so they can
+ * select a date.
+ */
+ dateButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ datePicker.setValue(currentDate, false);
+ datePickerPopup.center();
+ }
+ });
+ datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+ public void onValueChange(ValueChangeEvent<Date> event) {
+ setDueDate(event.getValue());
+ datePickerPopup.hide();
+ }
+ });
+
+ // Create a new task or modify the current task when done is pressed.
+ saveButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.saveTask(calendarCheckbox.getValue());
+ }
+ }
+ });
+
+ // Delete the current task or cancel when delete is pressed.
+ deleteButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.deleteTask();
+ }
+ }
+ });
+ }
+
+ public Date getDueDate() {
+ return currentDate;
+ }
+
+ public String getName() {
+ return nameBox.getValue();
+ }
+
+ public String getNotes() {
+ return notesBox.getValue();
+ }
+
+ public void setDueDate(Date date) {
+ boolean wasNull = (currentDate == null);
+ currentDate = date;
+ if (date == null) {
+ dateButton.setText("Set due date");
+ calendarCheckbox.setValue(false);
+ calendarCheckbox.setEnabled(false);
+ } else {
+ dateButton.setText(dateFormat.format(date));
+ calendarCheckbox.setEnabled(true);
+ if (wasNull) {
+ calendarCheckbox.setValue(true);
+ }
+ }
+ }
+
+ public void setEditing(boolean isEditing) {
+ if (isEditing) {
+ deleteButton.setText("Delete item");
+ } else {
+ deleteButton.setText("Cancel");
+ }
+ }
+
+ public void setLocked(boolean locked) {
+ setGlassPanelVisible(locked);
+ }
+
+ public void setName(String name) {
+ nameBox.setText(name);
+ }
+
+ public void setNotes(String notes) {
+ notesBox.setText(notes);
+ }
+
+ public void setPresenter(Presenter presenter) {
+ this.presenter = presenter;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml
new file mode 100644
index 0000000..2e167fe
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml
@@ -0,0 +1,141 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+ <ui:style>
+ .title {
+ padding: 4px 10px;
+ font-size: 14pt;
+ font-weight: bold;
+ color: #666;
+ }
+
+ .editForm {
+ padding: 10px;
+ background: white;
+ }
+
+ .label {
+ color: #333;
+ font-size: 10pt;
+ padding-bottom: 3px;
+ }
+
+ .field {
+ width: 300px;
+ }
+
+ .textBoxWrapper {
+ margin-right: 10px;
+ }
+
+ .checkbox {
+ color: #3f3f3f;
+ }
+
+ .notesBox {
+ height: 6em;
+ }
+
+ .button {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ color: #3f3f3f;
+ }
+
+ .dateButton {
+ text-align: center;
+ }
+
+ .buttonPanel {
+ padding: 10px;
+ margin-top: 15px;
+ }
+
+ .saveButton {
+ width: 130px;
+ margin-right: 5px;
+ }
+
+ .deleteButton {
+ width: 130px;
+ margin-left: 5px;
+ color: white;
+ background: #940000;
+ }
+ </ui:style>
+
+ <!-- Edit Form. -->
+ <g:HTMLPanel
+ addStyleNames="{style.editForm}">
+ <table
+ cellspacing="10"
+ align="center">
+
+ <!-- Form title. -->
+ <tr>
+ <td
+ align='center'
+ colspan='2'
+ class="{style.title}">Task Details</td>
+ </tr>
+
+ <!-- Task name. -->
+ <tr>
+ <td
+ class="{style.label}">Task Name:</td>
+ <td
+ class="{style.textBoxWrapper}">
+ <g:TextBox
+ addStyleNames="{style.field}"
+ ui:field="nameBox" />
+ </td>
+ </tr>
+
+ <!-- Task notes. -->
+ <tr>
+ <td
+ class="{style.label}">Notes:</td>
+ <td
+ class="{style.textBoxWrapper}">
+ <g:TextArea
+ addStyleNames="{style.field} {style.notesBox}"
+ ui:field="notesBox" />
+ </td>
+ </tr>
+
+ <!-- Task due date. -->
+ <tr>
+ <td
+ class="{style.label}">Due date:</td>
+ <td>
+ <g:Button
+ addStyleNames="{style.field} {style.button} {style.dateButton}"
+ ui:field="dateButton">Set due date</g:Button>
+
+ <!-- Add to calendar. -->
+ <br />
+ <g:CheckBox
+ addStyleNames="{style.checkbox}"
+ ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+ </td>
+ </tr>
+
+ <!-- Button panel. -->
+ <tr
+ class="{style.buttonPanel}">
+ <td></td>
+ <td
+ align='center'>
+ <g:Button
+ ui:field="saveButton"
+ addStyleNames="{style.button} {style.saveButton}">Done</g:Button>
+ <g:Button
+ ui:field="deleteButton"
+ addStyleNames="{style.button} {style.deleteButton}">Delete Item</g:Button>
+ </td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
new file mode 100644
index 0000000..18b84f4
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.cell.client.DateCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellTable;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.TextColumn;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.NoSelectionModel;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * View used to display the list of Tasks.
+ */
+public class DesktopTaskListView extends Composite implements TaskListView {
+
+ /**
+ * The UiBinder interface.
+ */
+ interface DesktopTaskListViewUiBinder extends
+ UiBinder<Widget, DesktopTaskListView> {
+ }
+
+ /**
+ * The UiBinder used to generate the view.
+ */
+ private static DesktopTaskListViewUiBinder uiBinder = GWT.create(DesktopTaskListViewUiBinder.class);
+
+ /**
+ * Displays the list of tasks.
+ */
+ @UiField(provided = true)
+ CellTable<TaskProxy> taskList;
+
+ /**
+ * The presenter for this view.
+ */
+ private Presenter presenter;
+
+ /**
+ * Construct a new {@link DesktopTaskListView}.
+ *
+ * @param presenter the {@link Presenter} that handles this view
+ */
+ public DesktopTaskListView() {
+ // Create the CellTable.
+ taskList = new CellTable<TaskProxy>();
+ taskList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+ taskList.setWidth("100%");
+
+ // Add the task name column.
+ Column<TaskProxy, String> nameColumn = new TextColumn<TaskProxy>() {
+ @Override
+ public String getValue(TaskProxy object) {
+ return (object == null) ? null : object.getName();
+ }
+ };
+ taskList.addColumn(nameColumn, "Task");
+
+ // Add the task notes column.
+ Column<TaskProxy, String> notesColumn = new TextColumn<TaskProxy>() {
+ @Override
+ public String getValue(TaskProxy object) {
+ return (object == null) ? "" : object.getNotes();
+ }
+ };
+ taskList.addColumn(notesColumn, "Description");
+
+ // Add the task due date column.
+ Column<TaskProxy, Date> dateColumn = new Column<TaskProxy, Date>(
+ new DateCell()) {
+ @Override
+ public Date getValue(TaskProxy object) {
+ return (object == null) ? null : object.getDueDate();
+ }
+ };
+ taskList.addColumn(dateColumn, "Due Date");
+
+ /*
+ * Inform the presenter when the user selects a task from the task list.
+ */
+ final NoSelectionModel<TaskProxy> selectionModel = new NoSelectionModel<TaskProxy>();
+ taskList.setSelectionModel(selectionModel);
+ selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+ public void onSelectionChange(SelectionChangeEvent event) {
+ // Edit the task.
+ if (presenter != null) {
+ presenter.selectTask(selectionModel.getLastSelectedObject());
+ }
+ }
+ });
+
+ // Initialize the widget.
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ public void clearList() {
+ taskList.setVisibleRangeAndClearData(taskList.getVisibleRange(), true);
+ }
+
+ public void setPresenter(Presenter presenter) {
+ this.presenter = presenter;
+ }
+
+ public void setSelectionModel(SelectionModel<TaskProxy> selectionModel) {
+ taskList.setSelectionModel(selectionModel);
+ }
+
+ public void setTasks(List<TaskProxy> tasks) {
+ taskList.setRowData(tasks);
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml
new file mode 100644
index 0000000..266cf6d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+ <ui:style>
+
+ </ui:style>
+
+ <g:ScrollPanel>
+ <c:CellTable
+ ui:field="taskList" />
+ </g:ScrollPanel>
+
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css
new file mode 100644
index 0000000..b0c7222
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css
@@ -0,0 +1,9 @@
+.cellListEvenItem,.cellListOddItem {
+ margin: 8px;
+ border: 1px solid #666;
+ -moz-border-radius: 20px 8px;
+ border-radius: 20px 8px;
+ padding: 18px 10px;
+ font-size: 12pt;
+ background-color: white;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
new file mode 100644
index 0000000..42a713d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.canvas.dom.client.CssColor;
+import com.google.gwt.cell.client.AbstractCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceChangeEvent;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellList;
+import com.google.gwt.user.cellview.client.CellList.Style;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.HasOneWidget;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SingleSelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity.TaskListUpdateEvent;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Desktop version of the UI shell.
+ */
+public class MobileWebAppShellDesktop extends MobileWebAppShellBase {
+
+ /**
+ * CSS override used for the main menu.
+ */
+ interface MainMenuStyle extends CellList.Style {
+ }
+
+ interface MobileWebAppShellDesktopUiBinder extends
+ UiBinder<Widget, MobileWebAppShellDesktop> {
+ }
+
+ /**
+ * A ClientBundle that provides images for this widget.
+ */
+ interface Resources extends CellList.Resources {
+ /**
+ * The styles used in the main menu. We extend
+ * {@link CellList.Style#DEFAULT_CSS} with the styles defined in
+ * MainMenuCellList.css.
+ */
+ @Source({"MainMenuCellList.css", CellList.Style.DEFAULT_CSS})
+ Style cellListStyle();
+ }
+
+ /**
+ * An item in the main menu that maps to a specific place.
+ */
+ private static class MainMenuItem {
+ private final String name;
+ private final Place place;
+
+ /**
+ * Construct a new {@link MainMenuItem}.
+ *
+ * @param name the display name
+ * @param place the place to open when selected
+ */
+ public MainMenuItem(String name, Place place) {
+ this.name = name;
+ this.place = place;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Place getPlace() {
+ return place;
+ }
+ }
+
+ /**
+ * The cell used to render a {@link MainMenuItem}.
+ */
+ private static class MainMenuItemCell extends AbstractCell<MainMenuItem> {
+
+ @Override
+ public void render(com.google.gwt.cell.client.Cell.Context context, MainMenuItem value,
+ SafeHtmlBuilder sb) {
+ if (value == null) {
+ return;
+ }
+ sb.appendEscaped(value.getName());
+ }
+ }
+
+ /**
+ * The URL attribute that determines whether or not to include the pie chart.
+ */
+ private static final String CHART_URL_ATTRIBUTE = "chart";
+
+ private static MobileWebAppShellDesktopUiBinder uiBinder = GWT
+ .create(MobileWebAppShellDesktopUiBinder.class);
+
+ /**
+ * The main menu list.
+ */
+ @UiField(provided = true)
+ CellList<MainMenuItem> mainMenu;
+
+ /**
+ * The container that holds content.
+ */
+ @UiField
+ DeckLayoutPanel contentContainer;
+
+ @UiField
+ DockLayoutPanel leftNav;
+
+ /**
+ * The container that holds the pie chart.
+ */
+ @UiField
+ HasOneWidget pieChartContainer;
+
+ /**
+ * A boolean indicating that we have not yet seen the first content widget.
+ */
+ private boolean firstContentWidget = true;
+
+ /**
+ * A pie chart showing a snapshot of the tasks.
+ */
+ private PieChart pieChart;
+
+ /**
+ * Construct a new {@link MobileWebAppShellDesktop}.
+ *
+ * @param clientFactory the {@link ClientFactory} of shared resources
+ */
+ public MobileWebAppShellDesktop(final ClientFactory clientFactory) {
+ // Initialize the main menu.
+ Resources resources = GWT.create(Resources.class);
+ mainMenu = new CellList<MainMenuItem>(new MainMenuItemCell(), resources);
+ mainMenu.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+
+ // We don't expect to have more than 30 menu items.
+ mainMenu.setVisibleRange(0, 30);
+
+ // Add items to the main menu.
+ final List<MainMenuItem> menuItems = new ArrayList<MainMenuItem>();
+ menuItems.add(new MainMenuItem("Task List", new TaskListPlace(false)));
+ menuItems.add(new MainMenuItem("Add Task", TaskEditPlace.getTaskCreatePlace()));
+ mainMenu.setRowData(menuItems);
+
+ // Choose a place when a menu item is selected.
+ final SingleSelectionModel<MainMenuItem> selectionModel =
+ new SingleSelectionModel<MainMenuItem>();
+ selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+ public void onSelectionChange(SelectionChangeEvent event) {
+ MainMenuItem selected = selectionModel.getSelectedObject();
+ if (selected != null) {
+ clientFactory.getPlaceController().goTo(selected.getPlace());
+ }
+ }
+ });
+ mainMenu.setSelectionModel(selectionModel);
+
+ // Update selection based on the current place.
+ clientFactory.getEventBus().addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
+ public void onPlaceChange(PlaceChangeEvent event) {
+ Place place = event.getNewPlace();
+ for (MainMenuItem menuItem : menuItems) {
+ if (place == menuItem.getPlace()) {
+ // We found a match in the main menu.
+ selectionModel.setSelected(menuItem, true);
+ return;
+ }
+ }
+
+ // We didn't find a match in the main menu.
+ selectionModel.setSelected(null, true);
+ }
+ });
+
+ // Initialize this widget.
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // Initialize the pie chart.
+ pieChart = PieChart.createIfSupported();
+ String chartUrlValue = Window.Location.getParameter(CHART_URL_ATTRIBUTE);
+ if (chartUrlValue != null && chartUrlValue.startsWith("f")) {
+ // Chart manually disabled.
+ leftNav.remove(1); // Pie Chart.
+ leftNav.remove(0); // Pie chart legend.
+ } else if (pieChart == null) {
+ // Chart not supported.
+ pieChartContainer.setWidget(new Label("Try upgrading to a modern browser to enable charts."));
+ } else {
+ // Chart supported.
+ pieChart.setWidth("90%");
+ pieChart.setHeight("90%");
+ pieChart.getElement().getStyle().setMarginLeft(5.0, Unit.PCT);
+ pieChart.getElement().getStyle().setMarginTop(5.0, Unit.PCT);
+ pieChartContainer.setWidget(pieChart);
+ }
+
+ // Initialize the add button.
+ setAddButtonHandler(null);
+
+ /*
+ * Add both views to the DeckLayoutPanel so we can animate between them.
+ * Using a DeckLayoutPanel here works because we only have two views, and we
+ * always know that the edit view should animate in from the right side of
+ * the screen. A more complex app will require more complex logic to figure
+ * out which direction to animate.
+ */
+ contentContainer.add(clientFactory.getTaskListView());
+ contentContainer.add(clientFactory.getTaskEditView());
+ contentContainer.setAnimationDuration(800);
+
+ // Listen for events from the task list activity.
+ clientFactory.getEventBus().addHandler(TaskListUpdateEvent.getType(),
+ new TaskListActivity.TaskListUpdateHandler() {
+ public void onTaskListUpdated(TaskListUpdateEvent event) {
+ updatePieChart(event.getTasks());
+ }
+ });
+ }
+
+ public boolean isTaskListIncluded() {
+ return false;
+ }
+
+ /**
+ * Set the handler to invoke when the add button is pressed. If no handler is
+ * specified, the button is hidden.
+ *
+ * @param handler the handler to add to the button, or null to hide
+ */
+ public void setAddButtonHandler(ClickHandler handler) {
+ // No-op: Adding a task is handled in the main menu.
+ }
+
+ /**
+ * Set the widget to display in content area.
+ *
+ * @param content the {@link Widget} to display
+ */
+ public void setWidget(IsWidget content) {
+ contentContainer.setWidget(content);
+
+ // Do not animate the first time we show a widget.
+ if (firstContentWidget) {
+ firstContentWidget = false;
+ contentContainer.animate(0);
+ }
+ }
+
+ /**
+ * Update the pie chart with the list of tasks.
+ *
+ * @param tasks the list of tasks
+ */
+ @SuppressWarnings("deprecation")
+ private void updatePieChart(List<TaskProxy> tasks) {
+ if (pieChart == null) {
+ return;
+ }
+
+ // Calculate the slices based on the due date.
+ double pastDue = 0;
+ double dueSoon = 0;
+ double onTime = 0;
+ double noDate = 0;
+ final Date now = new Date();
+ final Date tomorrow = new Date(now.getYear(), now.getMonth(), now.getDate() + 1, 23, 59, 59);
+ for (TaskProxy task : tasks) {
+ Date dueDate = task.getDueDate();
+ if (dueDate == null) {
+ noDate++;
+ } else if (dueDate.before(now)) {
+ pastDue++;
+ } else if (dueDate.before(tomorrow)) {
+ dueSoon++;
+ } else {
+ onTime++;
+ }
+ }
+
+ // Update the pie chart.
+ pieChart.clearSlices();
+ pieChart.addSlice(pastDue, CssColor.make(255, 100, 100));
+ pieChart.addSlice(dueSoon, CssColor.make(255, 200, 100));
+ pieChart.addSlice(onTime, CssColor.make(100, 255, 100));
+ pieChart.addSlice(noDate, CssColor.make(200, 200, 200));
+ pieChart.redraw();
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml
new file mode 100644
index 0000000..0d06261
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml
@@ -0,0 +1,155 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:a="urn:import:com.google.gwt.sample.mobilewebapp.client.activity"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui"
+ xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+ xmlns:d="urn:import:com.google.gwt.sample.mobilewebapp.client.desktop">
+
+ <ui:style>
+ .header {
+ border-bottom: 1px solid #666;
+ padding-top: 6px;
+ padding-left: 20px;
+ }
+
+ .headerWord {
+ font-size: 20pt;
+ font-weight: bold;
+ color: #889;
+ text-shadow: 5px 5px 6px #aaa;
+ -moz-transform: matrix(1, -0.04, 0, 1, 0pt, 0pt);
+ -webkit-transform: matrix(1, -0.04, 0, 1, 0pt, 0pt);
+ padding-right: 14px;
+ }
+
+ .leftNav {
+ border-right: 1px solid #666;
+ }
+
+ .mainMenuContainer {
+ background: #eee;
+ border-bottom: 1px solid #666;
+ }
+
+ .legendColor {
+ height: 1em;
+ width: 1em;
+ margin-left: 5px;
+ }
+
+ .legendText {
+ font-size: 80%
+ }
+ </ui:style>
+
+ <g:DockLayoutPanel
+ unit="PT">
+
+ <!-- Header -->
+ <g:north
+ size="40">
+ <g:HTMLPanel
+ addStyleNames="{style.header}">
+ <table>
+ <tr>
+ <td>
+ <div
+ class="{style.headerWord}">Task</div>
+ </td>
+ <td>
+ <div
+ class="{style.headerWord}">Manager</div>
+ </td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+ </g:north>
+
+
+ <g:center>
+ <g:DockLayoutPanel
+ unit="EM">
+
+ <!-- Left Nav. -->
+ <g:west
+ size="20">
+ <g:DockLayoutPanel
+ unit="EM"
+ ui:field="leftNav"
+ addStyleNames="{style.leftNav}">
+ <!-- Main Menu. -->
+ <g:center>
+ <g:ScrollPanel
+ addStyleNames="{style.mainMenuContainer}">
+ <c:CellList
+ ui:field="mainMenu" />
+ </g:ScrollPanel>
+ </g:center>
+
+ <!-- Pie Chart Legand. -->
+ <g:south
+ size="6">
+ <g:HTMLPanel>
+ <table
+ cellspacing="0"
+ cellpadding="2">
+ <tr>
+ <td>
+ <div
+ style="background: #faa"
+ class="{style.legendColor}"></div>
+ </td>
+ <td
+ class="{style.legendText}">Past Due</td>
+ </tr>
+ <tr>
+ <td>
+ <div
+ style="background: #fca"
+ class="{style.legendColor}"></div>
+ </td>
+ <td
+ class="{style.legendText}">Due Tomorrow</td>
+ </tr>
+ <tr>
+ <td>
+ <div
+ style="background: #afa"
+ class="{style.legendColor}"></div>
+ </td>
+ <td
+ class="{style.legendText}">On Time</td>
+ </tr>
+ <tr>
+ <td>
+ <div
+ style="background: #aaa"
+ class="{style.legendColor}"></div>
+ </td>
+ <td
+ class="{style.legendText}">No Due Date</td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+ </g:south>
+
+ <!-- Pie Chart. -->
+ <g:south
+ size="20">
+ <g:SimpleLayoutPanel
+ ui:field="pieChartContainer" />
+ </g:south>
+ </g:DockLayoutPanel>
+ </g:west>
+
+ <!-- Content. -->
+ <g:center>
+ <g:DeckLayoutPanel
+ ui:field="contentContainer" />
+ </g:center>
+ </g:DockLayoutPanel>
+ </g:center>
+
+ </g:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java
new file mode 100644
index 0000000..679cd75
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.canvas.client.Canvas;
+import com.google.gwt.canvas.dom.client.Context2d;
+import com.google.gwt.canvas.dom.client.FillStrokeStyle;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.PartialSupport;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.RequiresResize;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A pie chart representation of data.
+ */
+@PartialSupport
+public class PieChart extends Composite implements RequiresResize {
+
+ /**
+ * Information about a slice of pie.
+ */
+ private static class Slice {
+ private final double weight;
+ private final FillStrokeStyle fill;
+
+ public Slice(double weight, FillStrokeStyle fill) {
+ this.weight = weight;
+ this.fill = fill;
+ }
+ }
+
+ /**
+ * The number of radians in a circle.
+ */
+ private static final double RADIANS_IN_CIRCLE = 2 * Math.PI;
+
+ /**
+ * Return a new {@link Canvas} if supported, and null otherwise.
+ *
+ * @return a new {@link Canvas} if supported, and null otherwise
+ */
+ public static PieChart createIfSupported() {
+ return isSupported() ? new PieChart() : null;
+ }
+
+ /**
+ * Runtime check for whether the canvas element is supported in this browser.
+ *
+ * @return whether the canvas element is supported
+ */
+ public static boolean isSupported() {
+ return Canvas.isSupported();
+ }
+
+ private final Canvas canvas;
+ private final List<Slice> slices = new ArrayList<Slice>();
+
+ /**
+ * Create using factory methods.
+ */
+ private PieChart() {
+ canvas = Canvas.createIfSupported();
+ canvas.setCoordinateSpaceHeight(300);
+ canvas.setCoordinateSpaceWidth(300);
+ initWidget(canvas);
+ }
+
+ /**
+ * Add a slice to the chart.
+ *
+ * @param weight the weight of the slice
+ * @param fill the fill color
+ */
+ public void addSlice(double weight, FillStrokeStyle fill) {
+ slices.add(new Slice(weight, fill));
+ }
+
+ /**
+ * Clear all slices.
+ */
+ public void clearSlices() {
+ slices.clear();
+ }
+
+ public void onResize() {
+ redraw();
+ }
+
+ /**
+ * Redraw the pie chart.
+ */
+ public void redraw() {
+ if (!isAttached()) {
+ return;
+ }
+
+ // Get the dimensions of the chart.
+ int width = canvas.getCoordinateSpaceWidth();
+ int height = canvas.getCoordinateSpaceHeight();
+ double radius = Math.min(width, height) / 2.0;
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+
+ // Clear the context.
+ Context2d context = canvas.getContext2d();
+ context.clearRect(0, 0, width, height);
+
+ // Get the total weight of all slices.
+ double totalWeight = 0;
+ for (Slice slice : slices) {
+ totalWeight += slice.weight;
+ }
+
+ // Draw the slices.
+ double startAngle = -0.5 * Math.PI;
+ for (Slice slice : slices) {
+ double weight = slice.weight / totalWeight;
+ double endAngle = startAngle + (weight * RADIANS_IN_CIRCLE);
+ context.setFillStyle(slice.fill);
+ context.beginPath();
+ context.moveTo(cx, cy);
+ context.arc(cx, cy, radius, startAngle, endAngle);
+ context.fill();
+ startAngle = endAngle;
+ }
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ public void execute() {
+ redraw();
+ }
+ });
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css
new file mode 100644
index 0000000..122da68
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css
@@ -0,0 +1,6 @@
+.cellListEvenItem,.cellListOddItem {
+ border-bottom: 1px solid #bbb;
+ padding: 22px 10px;
+ font-size: 14pt;
+ color: #333;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
new file mode 100644
index 0000000..a2341bd
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.widget.client.TextButton;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class MobileTaskEditView extends Composite implements TaskEditView {
+
+ /**
+ * The UiBinder interface.
+ */
+ interface MobileTaskEditViewUiBinder extends
+ UiBinder<Widget, MobileTaskEditView> {
+ }
+
+ /**
+ * The UiBinder used to generate the view.
+ */
+ private static MobileTaskEditViewUiBinder uiBinder = GWT.create(MobileTaskEditViewUiBinder.class);
+
+ /**
+ * The formatter used to format the date.
+ */
+ private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+ /**
+ * The glass panel used to lock the UI.
+ */
+ private static PopupPanel glassPanel;
+
+ /**
+ * Show or hide the glass panel used to lock the UI will the task loads.
+ *
+ * @param visible true to show, false to hide
+ */
+ private static void setGlassPanelVisible(boolean visible) {
+ // Initialize the panel.
+ if (glassPanel == null) {
+ glassPanel = new DecoratedPopupPanel(false, true);
+ glassPanel.setWidget(new Label("Loading..."));
+ }
+
+ if (visible) {
+ // Show the loading panel.
+ glassPanel.center();
+ } else {
+ // Hide the loading panel.
+ glassPanel.hide();
+ }
+ }
+
+ /**
+ * The checkbox used to specify that the task should be added to the calendar.
+ */
+ @UiField
+ CheckBox calendarCheckbox;
+
+ /**
+ * The button used to select the date.
+ */
+ @UiField
+ TextButton dateButton;
+
+ /**
+ * The button used to delete a task or cancel changes.
+ */
+ @UiField
+ TextButton deleteButton;
+
+ /**
+ * The text box used to enter the task name.
+ */
+ @UiField
+ TextBoxBase nameBox;
+
+ /**
+ * The text box used to enter task notes.
+ */
+ @UiField
+ TextBoxBase notesBox;
+
+ /**
+ * The text box used to save changes or create a new task.
+ */
+ @UiField
+ TextButton saveButton;
+
+ /**
+ * The current date value.
+ */
+ private Date currentDate;
+
+ /**
+ * The popup panel that contains a date picker for selecting the date.
+ */
+ private final PopupPanel datePickerPopup;
+
+ /**
+ * The {@link Presenter} for this view.
+ */
+ private Presenter presenter;
+
+ /**
+ * Construct a new {@link MobileTaskEditView}.
+ *
+ * @param presenter the {@link Presenter} that handles interactions
+ */
+ public MobileTaskEditView() {
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // if this is a web app
+ // Create the datePickerPopup.
+ final DatePicker datePicker = new DatePicker();
+ datePickerPopup = new PopupPanel(true, true);
+ datePickerPopup.setWidget(datePicker);
+ datePickerPopup.setGlassEnabled(true);
+
+ /*
+ * When the user clicks on the date button, open a date picker so they can
+ * select a date.
+ */
+ dateButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ datePicker.setValue(currentDate, false);
+ datePickerPopup.center();
+ }
+ });
+ datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+ public void onValueChange(ValueChangeEvent<Date> event) {
+ setDueDate(event.getValue());
+ datePickerPopup.hide();
+ }
+ });
+
+ // Create a new task or modify the current task when done is pressed.
+ saveButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.saveTask(calendarCheckbox.getValue());
+ }
+ }
+ });
+
+ // Delete the current task or cancel when delete is pressed.
+ deleteButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.deleteTask();
+ }
+ }
+ });
+ }
+
+ public Date getDueDate() {
+ return currentDate;
+ }
+
+ public String getName() {
+ return nameBox.getValue();
+ }
+
+ public String getNotes() {
+ return notesBox.getValue();
+ }
+
+ public void setDueDate(Date date) {
+ boolean wasNull = (currentDate == null);
+ currentDate = date;
+ if (date == null) {
+ dateButton.setText("Set due date");
+ calendarCheckbox.setValue(false);
+ calendarCheckbox.setEnabled(false);
+ } else {
+ dateButton.setText(dateFormat.format(date));
+ calendarCheckbox.setEnabled(true);
+ if (wasNull) {
+ calendarCheckbox.setValue(true);
+ }
+ }
+ }
+
+ public void setEditing(boolean isEditing) {
+ if (isEditing) {
+ deleteButton.setText("Delete item");
+ } else {
+ deleteButton.setText("Cancel");
+ }
+ }
+
+ public void setLocked(boolean locked) {
+ setGlassPanelVisible(locked);
+ }
+
+ public void setName(String name) {
+ nameBox.setText(name);
+ }
+
+ public void setNotes(String notes) {
+ notesBox.setText(notes);
+ }
+
+ public void setPresenter(Presenter presenter) {
+ this.presenter = presenter;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml
new file mode 100644
index 0000000..d831f4e
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml
@@ -0,0 +1,141 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui"
+ xmlns:w="urn:import:com.google.gwt.widget.client">
+
+ <ui:style>
+ .outer {
+ background: #eee;
+ }
+
+ .title {
+ background: #393939;
+ color: white;
+ padding: 4px 10px;
+ font-size: 10pt;
+ }
+
+ .editForm {
+ padding: 10px;
+ background: white;
+ }
+
+ .label {
+ color: #666;
+ font-size: 10pt;
+ padding-bottom: 3px;
+ }
+
+ .field {
+ width: 100%;
+ margin-bottom: 12px;
+ }
+
+ .textBoxWrapper {
+ margin-right: 10px;
+ }
+
+ .checkbox {
+ color: #3f3f3f;
+ }
+
+ .nameBox {
+ height: 2em;
+ }
+
+ .notesBox {
+ height: 4em;
+ }
+
+ .buttonPanel {
+ width: 100%;
+ padding: 10px;
+ margin-top: 15px;
+ }
+
+ .button {
+ width: 100%;
+ }
+ </ui:style>
+
+ <g:DockLayoutPanel
+ unit="PT">
+ <!-- Title. -->
+ <g:north
+ size="18">
+ <g:Label
+ addStyleNames="{style.title}">DETAILS</g:Label>
+ </g:north>
+
+ <g:center>
+ <g:ScrollPanel
+ addStyleNames="{style.outer}">
+ <g:HTMLPanel>
+
+ <!-- Edit Form. -->
+ <div
+ class="{style.editForm}">
+ <!-- Task name. -->
+ <div
+ class="{style.label}">What</div>
+ <div
+ class="{style.textBoxWrapper}">
+ <g:TextArea
+ addStyleNames="{style.field} {style.nameBox}"
+ ui:field="nameBox" />
+ </div>
+
+ <!-- Task notes. -->
+ <div
+ class="{style.label}">Notes</div>
+ <div
+ class="{style.textBoxWrapper}">
+ <g:TextArea
+ addStyleNames="{style.field} {style.notesBox}"
+ ui:field="notesBox" />
+ </div>
+
+ <!-- Task due date. -->
+ <div
+ class="{style.label}">Due date</div>
+ <w:TextButton
+ addStyleNames="{style.field}"
+ ui:field="dateButton">Set due date</w:TextButton>
+
+ <!-- Add to calendar. -->
+ <g:CheckBox
+ addStyleNames="{style.checkbox}"
+ ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+ </div>
+
+ <!-- Button panel. -->
+ <table
+ class="{style.buttonPanel}"
+ cellspacing="0"
+ cellpadding="0">
+ <tr>
+ <td
+ align="center"
+ style="width:50%;padding-right:5px;">
+ <w:TextButton
+ ui:field="saveButton"
+ addStyleNames="{style.button}">Done</w:TextButton>
+ </td>
+ <td
+ align="center"
+ style="width:50%;padding-left:5px;">
+ <w:TextButton
+ ui:field="deleteButton"
+ decoration="NEGATIVE"
+ addStyleNames="{style.button}">Delete Item</w:TextButton>
+ </td>
+ </tr>
+ </table>
+
+ </g:HTMLPanel>
+ </g:ScrollPanel>
+ </g:center>
+ </g:DockLayoutPanel>
+
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
new file mode 100644
index 0000000..49bd93b
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellList;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.NoSelectionModel;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.List;
+
+/**
+ * View used to display the list of Tasks.
+ */
+public class MobileTaskListView extends Composite implements TaskListView {
+
+ /**
+ * Resources used by the mobile CellList.
+ */
+ static interface CellListResources extends CellList.Resources {
+ @Source({CellList.Style.DEFAULT_CSS, "MobileCellList.css"})
+ CellListStyle cellListStyle();
+ }
+
+ /**
+ * Styles used by the mobile CellList.
+ */
+ static interface CellListStyle extends CellList.Style {
+ }
+
+ /**
+ * The UiBinder interface.
+ */
+ interface MobileTaskListViewUiBinder extends
+ UiBinder<Widget, MobileTaskListView> {
+ }
+
+ /**
+ * The UiBinder used to generate the view.
+ */
+ private static MobileTaskListViewUiBinder uiBinder = GWT
+ .create(MobileTaskListViewUiBinder.class);
+
+ /**
+ * Displays the list of tasks.
+ */
+ @UiField(provided = true)
+ CellList<TaskProxy> taskList;
+
+ /**
+ * The presenter for this view.
+ */
+ private Presenter presenter;
+
+ /**
+ * Construct a new {@link MobileTaskListView}.
+ *
+ * @param presenter the {@link Presenter} that handles this view
+ */
+ public MobileTaskListView() {
+ // Create the CellList.
+ CellListResources cellListRes = GWT.create(CellListResources.class);
+ taskList = new CellList<TaskProxy>(new TaskProxyCell(), cellListRes);
+ taskList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+
+ /*
+ * Inform the presenter when the user selects a task from the task list. We
+ * use a NoSelectionModel because we don't want the task to remain selected,
+ * we just want to be notified of the selection event.
+ */
+ final NoSelectionModel<TaskProxy> selectionModel = new NoSelectionModel<TaskProxy>();
+ taskList.setSelectionModel(selectionModel);
+ selectionModel
+ .addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+ public void onSelectionChange(SelectionChangeEvent event) {
+ // Edit the task.
+ if (presenter != null) {
+ presenter.selectTask(selectionModel.getLastSelectedObject());
+ }
+ }
+ });
+
+ // Initialize the widget.
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ public void clearList() {
+ taskList.setVisibleRangeAndClearData(taskList.getVisibleRange(), true);
+ }
+
+ public void setPresenter(Presenter presenter) {
+ this.presenter = presenter;
+ }
+
+ public void setSelectionModel(SelectionModel<TaskProxy> selectionModel) {
+ taskList.setSelectionModel(selectionModel);
+ }
+
+ public void setTasks(List<TaskProxy> tasks) {
+ taskList.setRowData(tasks);
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml
new file mode 100644
index 0000000..2dc3497
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+ <ui:style>
+
+ </ui:style>
+
+ <g:ScrollPanel>
+ <c:CellList
+ ui:field="taskList" />
+ </g:ScrollPanel>
+
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
new file mode 100644
index 0000000..04d0087
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.LayoutPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+
+/**
+ * Mobile version of the UI shell.
+ */
+public class MobileWebAppShellMobile extends MobileWebAppShellBase {
+
+ interface MobileWebAppShellMobileUiBinder extends UiBinder<Widget, MobileWebAppShellMobile> {
+ }
+
+ private static MobileWebAppShellMobileUiBinder uiBinder = GWT
+ .create(MobileWebAppShellMobileUiBinder.class);
+
+ /**
+ * The width of the menu bar in landscape mode in EX.
+ */
+ private static final double LANDSCAPE_MENU_WIDTH_EX = 8.0;
+
+ /**
+ * The height of the menu bar in portrait mode in PT.
+ */
+ private static final double PORTRAIT_MENU_HEIGHT_PT = 28.0;
+
+ /**
+ * The button used to add items.
+ */
+ @UiField
+ Button addButton;
+
+ /**
+ * The widget that wraps the add button.
+ */
+ @UiField
+ Widget addButtonContainer;
+
+ /**
+ * The widget that wraps the back button.
+ */
+ @UiField
+ Widget backButtonContainer;
+
+ /**
+ * The panel that holds the current content.
+ */
+ @UiField
+ DeckLayoutPanel contentContainer;
+
+ /**
+ * The panel used for layout.
+ */
+ @UiField
+ LayoutPanel layoutPanel;
+
+ @UiField
+ Widget titleBar;
+
+ @UiField
+ Element titleElem;
+
+ /**
+ * A reference to the handler for the add button.
+ */
+ private HandlerRegistration addButtonHandler;
+
+ /**
+ * A boolean indicating that we have not yet seen the first content widget.
+ */
+ private boolean firstContentWidget = true;
+
+ /**
+ * Construct a new {@link MobileWebAppShellMobile}.
+ *
+ * @param clientFactory the {@link ClientFactory} of shared resources
+ */
+ public MobileWebAppShellMobile(ClientFactory clientFactory) {
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // Initialize the add button.
+ setAddButtonHandler(null);
+
+ /*
+ * Add both views to the DeckLayoutPanel so we can animate between them.
+ * Using a DeckLayoutPanel here works because we only have two views, and we
+ * always know that the edit view should animate in from the right side of
+ * the screen. A more complex app will require more complex logic to figure
+ * out which direction to animate.
+ */
+ contentContainer.add(clientFactory.getTaskListView());
+ contentContainer.add(clientFactory.getTaskEditView());
+ contentContainer.setAnimationDuration(500);
+ }
+
+ public boolean isTaskListIncluded() {
+ return false;
+ }
+
+ /**
+ * Set the handler to invoke when the add button is pressed. If no handler is
+ * specified, the button is hidden.
+ *
+ * @param handler the handler to add to the button, or null to hide
+ */
+ public void setAddButtonHandler(ClickHandler handler) {
+ // Clear the old handler.
+ if (addButtonHandler != null) {
+ addButtonHandler.removeHandler();
+ addButtonHandler = null;
+ }
+
+ if (handler == null) {
+ // Hide the button.
+ addButton.setVisible(false);
+ } else {
+ // Show the button and add the handler.
+ addButton.setVisible(true);
+ addButtonHandler = addButton.addClickHandler(handler);
+ }
+ }
+
+ /**
+ * Set the widget to display in content area.
+ *
+ * @param content the {@link Widget} to display
+ */
+ public void setWidget(IsWidget content) {
+ contentContainer.setWidget(content);
+
+ // Do not animate the first time we show a widget.
+ if (firstContentWidget) {
+ firstContentWidget = false;
+ contentContainer.animate(0);
+ }
+ }
+
+ @Override
+ protected void adjustOrientation(boolean isPortrait) {
+ if (isPortrait) {
+ // Portrait.
+ layoutPanel.setWidgetTopHeight(titleBar, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT, Unit.PT);
+ layoutPanel.setWidgetLeftRight(titleBar, 0, Unit.PX, 0, Unit.PX);
+ titleElem.getStyle().clearDisplay();
+
+ layoutPanel
+ .setWidgetTopBottom(contentContainer, PORTRAIT_MENU_HEIGHT_PT, Unit.PT, 0, Unit.PX);
+ layoutPanel.setWidgetLeftRight(contentContainer, 0, Unit.EX, 0, Unit.PX);
+
+ layoutPanel.setWidgetTopHeight(addButtonContainer, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT,
+ Unit.PT);
+ layoutPanel.setWidgetRightWidth(addButtonContainer, 8, Unit.PX, 3, Unit.EX);
+
+ layoutPanel.setWidgetTopHeight(backButtonContainer, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT,
+ Unit.PT);
+ layoutPanel.setWidgetLeftWidth(backButtonContainer, 8, Unit.PX, 6, Unit.EX);
+ } else {
+ // Landscape.
+ layoutPanel.setWidgetTopBottom(titleBar, 0, Unit.PX, 0, Unit.PX);
+ layoutPanel.setWidgetLeftWidth(titleBar, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX, Unit.EX);
+ titleElem.getStyle().setDisplay(Display.NONE);
+
+ layoutPanel.setWidgetTopBottom(contentContainer, 0, Unit.PX, 0, Unit.PX);
+ layoutPanel
+ .setWidgetLeftRight(contentContainer, LANDSCAPE_MENU_WIDTH_EX, Unit.EX, 0, Unit.PX);
+
+ layoutPanel.setWidgetTopHeight(addButtonContainer, 5, Unit.PX, 4, Unit.EX);
+ layoutPanel.setWidgetLeftWidth(addButtonContainer, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX,
+ Unit.EX);
+
+ layoutPanel.setWidgetBottomHeight(backButtonContainer, 5, Unit.PX, 4, Unit.EX);
+ layoutPanel.setWidgetLeftWidth(backButtonContainer, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX,
+ Unit.EX);
+ }
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml
new file mode 100644
index 0000000..f25a9b3
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml
@@ -0,0 +1,85 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui"
+ xmlns:c="urn:import:com.google.gwt.user.cellview.client">
+
+ <ui:style>
+ .backButton {
+ display: none;
+ }
+
+ .header {
+ background: #1a1a1a;
+ color: white;
+ font-size: 18pt;
+ line-height: 28pt;
+ text-align: center;
+ font-weight: bold;
+ }
+
+ .addButton {
+ color: white;
+ font-size: 18pt;
+ background: none;
+ border: none;
+ text-align: right;
+ font-weight: bold;
+ }
+ </ui:style>
+
+ <g:LayoutPanel
+ ui:field='layoutPanel'>
+
+ <!-- Title -->
+ <g:layer
+ top='0px'
+ height='28pt'
+ left='0px'
+ right='0px'>
+ <g:HTMLPanel
+ ui:field='titleBar'
+ addStyleNames="{style.header}">
+ <div
+ ui:field='titleElem'>Task List</div>
+ </g:HTMLPanel>
+ </g:layer>
+
+ <!-- Back button. -->
+ <g:layer
+ top='0px'
+ height='28pt'
+ left='0px'
+ width='6ex'>
+ <g:HTMLPanel
+ ui:field='backButtonContainer'>
+ <g:Button
+ styleName="{style.backButton}">Back</g:Button>
+ </g:HTMLPanel>
+ </g:layer>
+
+ <!-- Add button. -->
+ <g:layer
+ top="0px"
+ height="28pt"
+ right="8px"
+ width="3ex">
+ <g:HTMLPanel
+ ui:field='addButtonContainer'>
+ <g:Button
+ ui:field="addButton"
+ styleName="{style.addButton}">+</g:Button>
+ </g:HTMLPanel>
+ </g:layer>
+
+ <!-- Content -->
+ <g:layer
+ top='28pt'
+ bottom='0px'
+ left='0px'
+ right='0px'>
+ <g:DeckLayoutPanel
+ ui:field="contentContainer" />
+ </g:layer>
+ </g:LayoutPanel>
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
new file mode 100644
index 0000000..d1d4292
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.cell.client.AbstractCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
+import com.google.gwt.safehtml.client.SafeHtmlTemplates;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.Date;
+
+/**
+ * A {@link com.google.gwt.cell.client.Cell} used to render a {@link TaskProxy}.
+ */
+public class TaskProxyCell extends AbstractCell<TaskProxy> {
+
+ /**
+ * The template used by this cell.
+ *
+ */
+ interface Template extends SafeHtmlTemplates {
+ @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;\"> </div>")
+ SafeHtml noDate(String name);
+
+ @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;color:#999;\">Due: {1}</div>")
+ SafeHtml onTime(String name, String date);
+
+ @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;color:red;\">Due: {1}</div>")
+ SafeHtml pastDue(String name, String date);
+ }
+
+ private static Template template;
+ private final DateTimeFormat dateFormat = DateTimeFormat.getFormat(PredefinedFormat.DATE_LONG);
+
+ public TaskProxyCell() {
+ if (template == null) {
+ template = GWT.create(Template.class);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void render(com.google.gwt.cell.client.Cell.Context context, TaskProxy value,
+ SafeHtmlBuilder sb) {
+ if (value == null) {
+ return;
+ }
+
+ Date date = value.getDueDate();
+ Date today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ if (date == null) {
+ sb.append(template.noDate(value.getName()));
+ } else if (date.before(today)) {
+ sb.append(template.pastDue(value.getName(), dateFormat.format(date)));
+ } else {
+ sb.append(template.onTime(value.getName(), dateFormat.format(date)));
+ }
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java
new file mode 100644
index 0000000..cc48944
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.PlaceHistoryMapper;
+import com.google.gwt.place.shared.WithTokenizers;
+
+/**
+ * This interface is the hub of your application's navigation system. It links
+ * the {@link com.google.gwt.place.shared.Place Place}s your user navigates to
+ * with the browser history system — that is, it makes the browser's back
+ * and forth buttons work for you, and also makes each spot in your app
+ * bookmarkable.
+ */
+@WithTokenizers({TaskListPlace.Tokenizer.class, TaskEditPlace.Tokenizer.class})
+public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java
new file mode 100644
index 0000000..025654f
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+
+/**
+ * The place in the app that show a task in an editable view.
+ */
+public class TaskEditPlace extends Place {
+
+ /**
+ * The tokenizer for this place.
+ */
+ public static class Tokenizer implements PlaceTokenizer<TaskEditPlace> {
+
+ private static final String NO_ID = "create";
+
+ public TaskEditPlace getPlace(String token) {
+ try {
+ // Parse the task ID from the URL.
+ Long taskId = Long.parseLong(token);
+ return new TaskEditPlace(taskId);
+ } catch (NumberFormatException e) {
+ // If the ID cannot be parsed, assume we are creating a task.
+ return new TaskEditPlace(null);
+ }
+ }
+
+ public String getToken(TaskEditPlace place) {
+ Long taskId = place.getTaskId();
+ return (taskId == null) ? NO_ID : taskId.toString();
+ }
+ }
+
+ /**
+ * The singleton instance of this place used for creation.
+ */
+ private static TaskEditPlace singleton;
+
+ /**
+ * Create an instance of {@link TaskEditPlace} associated with the specified
+ * task ID.
+ *
+ * @param taskId the ID of the task to edit
+ * @return the place
+ */
+ public static TaskEditPlace createTaskEditPlace(Long taskId) {
+ return new TaskEditPlace(taskId);
+ }
+
+ /**
+ * Get the singleton instance of the {@link TaskEditPlace} used to create a
+ * new task.
+ *
+ * @return the place
+ */
+ public static TaskEditPlace getTaskCreatePlace() {
+ if (singleton == null) {
+ singleton = new TaskEditPlace(null);
+ }
+ return singleton;
+ }
+
+ private final Long taskId;
+
+ /**
+ * Construct a new {@link TaskEditPlace} for the specified task id.
+ *
+ * @param taskId the ID of the task to edit
+ */
+ private TaskEditPlace(Long taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * Get the ID of the task to edit.
+ *
+ * @return the ID of the task, or null if creating a new task
+ */
+ public Long getTaskId() {
+ return taskId;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java
new file mode 100644
index 0000000..8f9f539
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+
+/**
+ * The place in the app that shows a list of tasks.
+ */
+public class TaskListPlace extends Place {
+
+ /**
+ * The tokenizer for this place. TaskList doesn't have any state, so we don't
+ * have anything to encode.
+ */
+ public static class Tokenizer implements PlaceTokenizer<TaskListPlace> {
+
+ public TaskListPlace getPlace(String token) {
+ return new TaskListPlace(true);
+ }
+
+ public String getToken(TaskListPlace place) {
+ return "";
+ }
+ }
+
+ private final boolean taskListStale;
+
+ /**
+ * Construct a new {@link TaskListPlace}.
+ *
+ * @param taskListStale true if the task list is stale and should be cleared
+ */
+ public TaskListPlace(boolean taskListStale) {
+ this.taskListStale = taskListStale;
+ }
+
+ /**
+ * Check if the task list is stale and should be cleared.
+ *
+ * @return true if stale, false if not
+ */
+ public boolean isTaskListStale() {
+ return taskListStale;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
new file mode 100644
index 0000000..966abbe
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+
+/**
+ * Tablet version of the UI shell.
+ */
+public class MobileWebAppShellTablet extends MobileWebAppShellBase {
+
+ interface MobileWebAppShellTabletUiBinder extends UiBinder<Widget, MobileWebAppShellTablet> {
+ }
+
+ private static MobileWebAppShellTabletUiBinder uiBinder = GWT
+ .create(MobileWebAppShellTabletUiBinder.class);
+
+ /**
+ * The width of the task list in landscape mode in PCT.
+ */
+ private static final double LANDSCAPE_TASK_LIST_WIDTH_PCT = 30.0;
+
+ /**
+ * The button used to add items.
+ */
+ @UiField
+ Button addButton;
+
+ /**
+ * The container that holds content.
+ */
+ @UiField
+ DeckLayoutPanel contentContainer;
+
+ /**
+ * The widget displayed when the user has not selected a task.
+ */
+ @UiField
+ Widget contentEmptyMessage;
+
+ /**
+ * The DockLayoutPanel that splits the task list and the task edit views.
+ */
+ @UiField
+ DockLayoutPanel splitPanel;
+
+ /**
+ * The container that holds the tast list.
+ */
+ @UiField
+ SimplePanel taskListContainer;
+
+ /**
+ * A reference to the handler for the add button.
+ */
+ private HandlerRegistration addButtonHandler;
+
+ /**
+ * The {@link ClientFactory} of shared resources.
+ */
+ private final ClientFactory clientFactory;
+
+ /**
+ * A boolean indicating that we have not yet seen the first content widget.
+ */
+ private boolean firstContentWidget = true;
+
+ /**
+ * The main task list, which is always visible.
+ */
+ private TaskListActivity taskListActivity;
+
+ /**
+ * Construct a new {@link MobileWebAppShellTablet}.
+ *
+ * @param clientFactory the {@link ClientFactory} of shared resources
+ */
+ public MobileWebAppShellTablet(final ClientFactory clientFactory) {
+ this.clientFactory = clientFactory;
+
+ // Inject the tablet specific styles.
+ TabletResources resources = GWT.create(TabletResources.class);
+ resources.tabletStyles().ensureInjected();
+
+ // Initialize this widget.
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // Initialize the add button.
+ setAddButtonHandler(null);
+ }
+
+ public boolean isTaskListIncluded() {
+ return !isOrientationPortrait();
+ }
+
+ /**
+ * Set the handler to invoke when the add button is pressed. If no handler is
+ * specified, the button is hidden.
+ *
+ * @param handler the handler to add to the button, or null to hide
+ */
+ public void setAddButtonHandler(ClickHandler handler) {
+ // Clear the old handler.
+ if (addButtonHandler != null) {
+ addButtonHandler.removeHandler();
+ addButtonHandler = null;
+ }
+
+ if (handler == null) {
+ // Hide the button.
+ addButton.setVisible(false);
+ } else {
+ // Show the button and add the handler.
+ addButton.setVisible(true);
+ addButtonHandler = addButton.addClickHandler(handler);
+ }
+ }
+
+ /**
+ * Set the widget to display in content area.
+ *
+ * @param content the {@link Widget} to display
+ */
+ public void setWidget(IsWidget content) {
+ contentContainer.setWidget((content == null) ? contentEmptyMessage : content);
+
+ // Do not animate the first time we show a widget.
+ if (firstContentWidget) {
+ firstContentWidget = false;
+ contentContainer.animate(0);
+ }
+ }
+
+ @Override
+ protected void adjustOrientation(boolean isPortrait) {
+ if (isPortrait) {
+ // Hide the static task list view.
+ if (taskListActivity != null) {
+ taskListActivity.onStop();
+ taskListActivity = null;
+ }
+ splitPanel.setWidgetSize(taskListContainer, 0);
+
+ /*
+ * Add both views to the DeckLayoutPanel so we can animate between them.
+ * Using a DeckLayoutPanel here works because we only have two views, and
+ * we always know that the edit view should animate in from the right side
+ * of the screen. A more complex app will require more complex logic to
+ * figure out which direction to animate.
+ */
+ contentContainer.insert(clientFactory.getTaskListView(), 0);
+ contentContainer.setAnimationDuration(500);
+
+ // Ensure that something is displayed.
+ Widget curWidget = contentContainer.getVisibleWidget();
+ if (curWidget == null || curWidget == contentEmptyMessage) {
+ clientFactory.getPlaceController().goTo(new TaskListPlace(false));
+ contentContainer.animate(0);
+ }
+ } else {
+ // Show the static task list view.
+ splitPanel.setWidgetSize(taskListContainer, LANDSCAPE_TASK_LIST_WIDTH_PCT);
+ if (taskListActivity == null) {
+ taskListActivity = new TaskListActivity(clientFactory, false);
+ taskListActivity.start(taskListContainer, clientFactory.getEventBus());
+
+ // DeckLayoutPanel sets the display to none, so we need to clear it.
+ clientFactory.getTaskListView().asWidget().getElement().getStyle().clearDisplay();
+ }
+
+ // Do not use animations when the task list is always visible.
+ contentContainer.setAnimationDuration(0);
+
+ // Ensure that the task list view is not displayed as content.
+ if (contentContainer.getVisibleWidget() == null) {
+ contentContainer.setWidget(contentEmptyMessage);
+ }
+ }
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml
new file mode 100644
index 0000000..a29c961
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml
@@ -0,0 +1,106 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:a="urn:import:com.google.gwt.sample.mobilewebapp.client.activity"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui"
+ xmlns:c="urn:import:com.google.gwt.user.cellview.client">
+
+ <ui:style>
+ .header {
+ background: #1a1a1a;
+ position: relative;
+ }
+
+ .headerText {
+ color: white;
+ font-size: 18pt;
+ line-height: 28pt;
+ text-align: center;
+ font-weight: bold;
+ }
+
+ .addButtonContainer {
+ position: absolute;
+ top: 5px;
+ right: 3px;
+ }
+
+ .addButton {
+ color: white;
+ font-size: 18pt;
+ background: none;
+ border: none;
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .taskList {
+ border-right: 4px solid #eee;
+ }
+
+ .contentEmptyMessage {
+ padding: 10px;
+ font-size: 28pt;
+ }
+ </ui:style>
+
+ <g:DockLayoutPanel
+ unit="PT">
+
+ <!-- Header -->
+ <g:north
+ size="32">
+ <g:HTMLPanel
+ addStyleNames="{style.header}">
+ <table
+ style="width:100%;height:100%">
+ <tr>
+ <td
+ align="right"
+ valign="middle">
+ <g:Label
+ addStyleNames="{style.headerText}">
+ App Name
+ </g:Label>
+ </td>
+ </tr>
+ </table>
+
+ <!-- Add Button. -->
+ <div
+ class="{style.addButtonContainer}">
+ <g:Button
+ ui:field="addButton"
+ styleName="{style.addButton}">+</g:Button>
+ </div>
+ </g:HTMLPanel>
+ </g:north>
+
+ <g:center>
+ <g:DockLayoutPanel
+ unit="PCT"
+ ui:field="splitPanel">
+
+ <!-- Task List. -->
+ <g:west
+ size="30">
+ <g:SimpleLayoutPanel
+ addStyleNames="{style.taskList}"
+ ui:field="taskListContainer" />
+ </g:west>
+
+ <!-- Content. -->
+ <g:center>
+ <g:DeckLayoutPanel
+ ui:field="contentContainer">
+ <g:HTML
+ ui:field="contentEmptyMessage"
+ addStyleNames="{style.contentEmptyMessage}">
+ « Select a task from the list</g:HTML>
+ </g:DeckLayoutPanel>
+ </g:center>
+ </g:DockLayoutPanel>
+ </g:center>
+
+ </g:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java
new file mode 100644
index 0000000..53d4f54
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.resources.client.CssResource.NotStrict;
+
+/**
+ * Resources used by the tablet version of the app.
+ */
+public interface TabletResources extends ClientBundle {
+
+ /**
+ * Style overrides specific to the tablet version.
+ */
+ @NotStrict
+ CssResource tabletStyles();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
new file mode 100644
index 0000000..1e29850
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class TabletTaskEditView extends Composite implements TaskEditView {
+
+ /**
+ * The UiBinder interface.
+ */
+ interface TabletTaskEditViewUiBinder extends UiBinder<Widget, TabletTaskEditView> {
+ }
+
+ /**
+ * The UiBinder used to generate the view.
+ */
+ private static TabletTaskEditViewUiBinder uiBinder = GWT.create(TabletTaskEditViewUiBinder.class);
+
+ /**
+ * The formatter used to format the date.
+ */
+ private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+ /**
+ * The glass panel used to lock the UI.
+ */
+ private static PopupPanel glassPanel;
+
+ /**
+ * Show or hide the glass panel used to lock the UI will the task loads.
+ *
+ * @param visible true to show, false to hide
+ */
+ private static void setGlassPanelVisible(boolean visible) {
+ // Initialize the panel.
+ if (glassPanel == null) {
+ glassPanel = new DecoratedPopupPanel(false, true);
+ glassPanel.setGlassEnabled(true);
+ glassPanel.setWidget(new Label("Loading..."));
+ }
+
+ if (visible) {
+ // Show the loading panel.
+ glassPanel.center();
+ } else {
+ // Hide the loading panel.
+ glassPanel.hide();
+ }
+ }
+
+ /**
+ * The checkbox used to specify that the task should be added to the calendar.
+ */
+ @UiField
+ CheckBox calendarCheckbox;
+
+ /**
+ * The button used to select the date.
+ */
+ @UiField
+ Button dateButton;
+
+ /**
+ * The button used to delete a task or cancel changes.
+ */
+ @UiField
+ Button deleteButton;
+
+ /**
+ * The text box used to enter the task name.
+ */
+ @UiField
+ TextBoxBase nameBox;
+
+ /**
+ * The text box used to enter task notes.
+ */
+ @UiField
+ TextBoxBase notesBox;
+
+ /**
+ * The text box used to save changes or create a new task.
+ */
+ @UiField
+ Button saveButton;
+
+ /**
+ * The current date value.
+ */
+ private Date currentDate;
+
+ /**
+ * The popup panel that contains a date picker for selecting the date.
+ */
+ private final PopupPanel datePickerPopup;
+
+ /**
+ * The {@link Presenter} for this view.
+ */
+ private Presenter presenter;
+
+ /**
+ * Construct a new {@link TabletTaskEditView}.
+ *
+ * @param presenter the {@link Presenter} that handles interactions
+ */
+ public TabletTaskEditView() {
+ initWidget(uiBinder.createAndBindUi(this));
+
+ // Create the datePickerPopup.
+ final DatePicker datePicker = new DatePicker();
+ datePickerPopup = new PopupPanel(true, true);
+ datePickerPopup.setWidget(datePicker);
+ datePickerPopup.setGlassEnabled(true);
+
+ /*
+ * When the user clicks on the date button, open a date picker so they can
+ * select a date.
+ */
+ dateButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ datePicker.setValue(currentDate, false);
+ datePickerPopup.center();
+ }
+ });
+ datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+ public void onValueChange(ValueChangeEvent<Date> event) {
+ setDueDate(event.getValue());
+ datePickerPopup.hide();
+ }
+ });
+
+ // Create a new task or modify the current task when done is pressed.
+ saveButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.saveTask(calendarCheckbox.getValue());
+ }
+ }
+ });
+
+ // Delete the current task or cancel when delete is pressed.
+ deleteButton.addClickHandler(new ClickHandler() {
+ public void onClick(ClickEvent event) {
+ if (presenter != null) {
+ presenter.deleteTask();
+ }
+ }
+ });
+ }
+
+ public Date getDueDate() {
+ return currentDate;
+ }
+
+ public String getName() {
+ return nameBox.getValue();
+ }
+
+ public String getNotes() {
+ return notesBox.getValue();
+ }
+
+ public void setDueDate(Date date) {
+ boolean wasNull = (currentDate == null);
+ currentDate = date;
+ if (date == null) {
+ dateButton.setText("Set due date");
+ calendarCheckbox.setValue(false);
+ calendarCheckbox.setEnabled(false);
+ } else {
+ dateButton.setText(dateFormat.format(date));
+ calendarCheckbox.setEnabled(true);
+ if (wasNull) {
+ calendarCheckbox.setValue(true);
+ }
+ }
+ }
+
+ public void setEditing(boolean isEditing) {
+ if (isEditing) {
+ deleteButton.setText("Delete item");
+ } else {
+ deleteButton.setText("Cancel");
+ }
+ }
+
+ public void setLocked(boolean locked) {
+ setGlassPanelVisible(locked);
+ }
+
+ public void setName(String name) {
+ nameBox.setText(name);
+ }
+
+ public void setNotes(String notes) {
+ notesBox.setText(notes);
+ }
+
+ public void setPresenter(Presenter presenter) {
+ this.presenter = presenter;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml
new file mode 100644
index 0000000..a957f54
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml
@@ -0,0 +1,160 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+ xmlns:ui="urn:ui:com.google.gwt.uibinder"
+ xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+ <ui:style>
+ .outer {
+ background: #eee;
+ }
+
+ .title {
+ background: #393939;
+ color: white;
+ padding: 4px 10px;
+ font-size: 20pt;
+ }
+
+ .editForm {
+ padding: 10px;
+ background: white;
+ }
+
+ .label {
+ color: #666;
+ font-size: 20pt;
+ padding-bottom: 3px;
+ }
+
+ .field {
+ width: 100%;
+ margin-bottom: 12px;
+ font-size: 20pt;
+ }
+
+ .textBoxWrapper {
+ margin-right: 10px;
+ }
+
+ .checkbox {
+ color: #3f3f3f;
+ font-size: 20pt;
+ }
+
+ .nameBox {
+ height: 2em;
+ }
+
+ .notesBox {
+ height: 4em;
+ }
+
+ .button {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ color: #3f3f3f;
+ font-size: 20pt;
+ }
+
+ .dateButton {
+ text-align: left;
+ }
+
+ .buttonPanel {
+ width: 100%;
+ padding: 10px;
+ margin-top: 15px;
+ }
+
+ .saveButton {
+ width: 100%;
+ }
+
+ .deleteButton {
+ color: white;
+ background: #940000;
+ width: 100%;
+ border-radius: 3px;
+ -moz-border-radius: 3px;
+ }
+ </ui:style>
+
+ <g:DockLayoutPanel
+ unit="PT">
+ <!-- Title. -->
+ <g:north
+ size="28">
+ <g:Label
+ addStyleNames="{style.title}">DETAILS</g:Label>
+ </g:north>
+
+ <g:center>
+ <g:ScrollPanel
+ addStyleNames="{style.outer}">
+ <g:HTMLPanel>
+
+ <!-- Edit Form. -->
+ <div
+ class="{style.editForm}">
+ <!-- Task name. -->
+ <div
+ class="{style.label}">What</div>
+ <div
+ class="{style.textBoxWrapper}">
+ <g:TextArea
+ addStyleNames="{style.field} {style.nameBox}"
+ ui:field="nameBox" />
+ </div>
+
+ <!-- Task notes. -->
+ <div
+ class="{style.label}">Notes</div>
+ <div
+ class="{style.textBoxWrapper}">
+ <g:TextArea
+ addStyleNames="{style.field} {style.notesBox}"
+ ui:field="notesBox" />
+ </div>
+
+ <!-- Task due date. -->
+ <div
+ class="{style.label}">Due date</div>
+ <g:Button
+ addStyleNames="{style.field} {style.button} {style.dateButton}"
+ ui:field="dateButton">Set due date</g:Button>
+
+ <!-- Add to calendar. -->
+ <g:CheckBox
+ addStyleNames="{style.checkbox}"
+ ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+ </div>
+
+ <!-- Button panel. -->
+ <table
+ class="{style.buttonPanel}"
+ cellspacing="0"
+ cellpadding="0">
+ <tr>
+ <td
+ align="center"
+ style="width:50%;padding-right:5px;">
+ <g:Button
+ ui:field="saveButton"
+ addStyleNames="{style.button} {style.saveButton}">Done</g:Button>
+ </td>
+ <td
+ align="center"
+ style="width:50%;padding-left:5px;">
+ <g:Button
+ ui:field="deleteButton"
+ addStyleNames="{style.button} {style.deleteButton}">Delete Item</g:Button>
+ </td>
+ </tr>
+ </table>
+
+ </g:HTMLPanel>
+ </g:ScrollPanel>
+ </g:center>
+ </g:DockLayoutPanel>
+
+</ui:UiBinder>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css
new file mode 100644
index 0000000..064c117
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css
@@ -0,0 +1,4 @@
+.datePickerDay,.datePickerWeekdayLabel,.datePickerWeekendLabel,td.datePickerMonth
+ {
+ font-size: 30pt;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java
new file mode 100644
index 0000000..1fb7db8
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.server.domain;
+
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Persistence;
+
+/**
+ * Factory for creating EntityManager.
+ */
+public final class EMF {
+
+ private static final EntityManagerFactory emfInstance =
+ Persistence.createEntityManagerFactory("transactions-optional");
+
+ public static EntityManagerFactory get() {
+ return emfInstance;
+ }
+
+ private EMF() {
+ // nothing
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java
new file mode 100644
index 0000000..46e7183
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.server.domain;
+
+import java.util.Date;
+import java.util.List;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EntityManager;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Query;
+import javax.persistence.Version;
+
+/**
+ * A task used in the task list.
+ */
+@Entity
+public class Task {
+
+ /**
+ * Find all tasks.
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Task> findAllTasks() {
+ EntityManager em = entityManager();
+ try {
+ Query query = em.createQuery("select o from Task o");
+ List<Task> list = query.getResultList();
+
+ /*
+ * If this is the first time running the app, populate the datastore with
+ * some default tasks and re-query the datastore for them.
+ */
+ if (list.size() == 0) {
+ populateDatastore();
+ list = query.getResultList();
+
+ /*
+ * Workaround for this issue: http://code.google.com/p/datanucleus-appengine/issues/detail?id=24
+ */
+ list.size();
+ }
+
+ return list;
+ } finally {
+ em.close();
+ }
+ };
+
+ /**
+ * Find a {@link Task} by id.
+ *
+ * @param id the {@link Task} id
+ * @return the associated {@link Task}, or null if not found
+ */
+ public static Task findTask(Long id) {
+ if (id == null) {
+ return null;
+ }
+ EntityManager em = entityManager();
+ try {
+ Task task = em.find(Task.class, id);
+ return task;
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Create an entity manager to interact with the database.
+ *
+ * @return an {@link EntityManager} instance
+ */
+ private static EntityManager entityManager() {
+ return EMF.get().createEntityManager();
+ }
+
+ /**
+ * Populate the datastore with some default tasks. We do this to make the app
+ * more intuitive on first use.
+ *
+ * @return the local {@link Task} map
+ */
+ @SuppressWarnings("deprecation")
+ private static void populateDatastore() {
+ {
+ // Task 0.
+ Task task0 = new Task();
+ task0.setName("Beat Angry Birds");
+ task0.setNotes("This game is impossible!");
+ task0.setDueDate(new Date(100, 4, 20));
+ task0.persist();
+ }
+ {
+ // Task 1.
+ Task task1 = new Task();
+ task1.setName("Make a million dollars");
+ task1.setNotes("Then spend it all on Android apps");
+ task1.persist();
+ }
+ {
+ // Task 2.
+ Task task2 = new Task();
+ task2.setName("Buy a dozen eggs");
+ task2.setNotes("of the chicken variety");
+ task2.persist();
+ }
+ {
+ // Task 3.
+ Task task3 = new Task();
+ task3.setName("Complete all tasks");
+ task3.persist();
+ }
+ }
+
+ @Id
+ @Column(name = "id")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Version
+ @Column(name = "version")
+ private Integer version;
+
+ private Date dueDate;
+ private String name;
+ private String notes;
+
+ /**
+ * Get the due date of the Task.
+ */
+ public Date getDueDate() {
+ return dueDate;
+ }
+
+ /**
+ * Get the unique ID of the Task.
+ */
+ public Long getId() {
+ return id;
+ }
+
+ /**
+ * Get the name of the Task.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get the notes associated with the task.
+ */
+ public String getNotes() {
+ return notes;
+ }
+
+ /**
+ * Get the version of this datastore object.
+ */
+ public Integer getVersion() {
+ return version;
+ }
+
+ /**
+ * Persist this object in the data store.
+ */
+ public void persist() {
+ EntityManager em = entityManager();
+ try {
+ em.persist(this);
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Remove this object from the data store.
+ */
+ public void remove() {
+ EntityManager em = entityManager();
+ try {
+ Task task = em.find(Task.class, this.id);
+ em.remove(task);
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Set the due date of the task.
+ *
+ * @param dueDate the due date, or null if no due date
+ */
+ public void setDueDate(Date dueDate) {
+ this.dueDate = dueDate;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ /**
+ * Set the name of the task.
+ *
+ * @param name the task name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Set the notes associated with the task.
+ *
+ * @param notes the notes
+ */
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java
new file mode 100644
index 0000000..d6ddb9d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.shared;
+
+/**
+ * <p>
+ * FieldVerifier validates that the name the user enters is valid.
+ * </p>
+ * <p>
+ * This class is in the <code>shared</code> package because we use it in both
+ * the client code and on the server. On the client, we verify that the name is
+ * valid before sending an RPC request so the user doesn't have to wait for a
+ * network round trip to get feedback. On the server, we verify that the name is
+ * correct to ensure that the input is correct regardless of where the RPC
+ * originates.
+ * </p>
+ * <p>
+ * When creating a class that is used on both the client and the server, be sure
+ * that all code is translatable and does not use native JavaScript. Code that
+ * is note translatable (such as code that interacts with a database or the file
+ * system) cannot be compiled into client side JavaScript. Code that uses native
+ * JavaScript (such as Widgets) cannot be run on the server.
+ * </p>
+ */
+public class FieldVerifier {
+
+ /**
+ * Verifies that the specified name is valid for our service.
+ *
+ * In this example, we only require that the name is at least four
+ * characters. In your application, you can use more complex checks to ensure
+ * that usernames, passwords, email addresses, URLs, and other fields have the
+ * proper syntax.
+ *
+ * @param name the name to validate
+ * @return true if valid, false if invalid
+ */
+ public static boolean isValidName(String name) {
+ if (name == null) {
+ return false;
+ }
+ return name.length() > 3;
+ }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java
new file mode 100644
index 0000000..59ed5bf
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.shared;
+
+import com.google.gwt.sample.mobilewebapp.server.domain.Task;
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+import java.util.Date;
+
+/**
+ * A task used in the task list.
+ */
+@ProxyFor(Task.class)
+public interface TaskProxy extends EntityProxy {
+
+ Date getDueDate();
+
+ Long getId();
+
+ String getName();
+
+ String getNotes();
+
+ void setDueDate(Date dueDate);
+
+ void setName(String name);
+
+ void setNotes(String notes);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java
new file mode 100644
index 0000000..0b0572c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.shared;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxyId;
+
+import java.util.Date;
+
+/**
+ * A task used in the task list.
+ */
+public class TaskProxyImpl implements TaskProxy {
+ private Date dueDate;
+ private Long id;
+ private String name;
+ private String notes;
+
+ public TaskProxyImpl() {
+ }
+
+ public Date getDueDate() {
+ return dueDate;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setDueDate(Date dueDate) {
+ this.dueDate = dueDate;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public EntityProxyId<?> stableId() {
+ return null;
+ }
+}
diff --git a/samples/mobilewebapp/src/log4j.properties b/samples/mobilewebapp/src/log4j.properties
new file mode 100644
index 0000000..d9c3edc
--- /dev/null
+++ b/samples/mobilewebapp/src/log4j.properties
@@ -0,0 +1,24 @@
+# A default log4j configuration for log4j users.
+#
+# To use this configuration, deploy it into your application's WEB-INF/classes
+# directory. You are also encouraged to edit it as you like.
+
+# Configure the console as our one appender
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n
+
+# tighten logging on the DataNucleus Categories
+log4j.category.DataNucleus.JDO=WARN, A1
+log4j.category.DataNucleus.Persistence=WARN, A1
+log4j.category.DataNucleus.Cache=WARN, A1
+log4j.category.DataNucleus.MetaData=WARN, A1
+log4j.category.DataNucleus.General=WARN, A1
+log4j.category.DataNucleus.Utility=WARN, A1
+log4j.category.DataNucleus.Transaction=WARN, A1
+log4j.category.DataNucleus.Datastore=WARN, A1
+log4j.category.DataNucleus.ClassLoading=WARN, A1
+log4j.category.DataNucleus.Plugin=WARN, A1
+log4j.category.DataNucleus.ValueGeneration=WARN, A1
+log4j.category.DataNucleus.Enhancer=WARN, A1
+log4j.category.DataNucleus.SchemaTool=WARN, A1
diff --git a/samples/mobilewebapp/user-build.xml b/samples/mobilewebapp/user-build.xml
new file mode 100644
index 0000000..9d05428
--- /dev/null
+++ b/samples/mobilewebapp/user-build.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<project name="MobileWebApp" default="build" basedir=".">
+ <property file="local.properties" />
+ <!-- Arguments to gwtc and devmode targets -->
+ <property name="gwt.args" value="" />
+ <property name="src.dir" value="src" />
+ <property name="war.dir" value="war" />
+ <property name="gwt.sdk" value="../.." />
+
+ <fail unless="appengine.sdk">Missing property:
+ The property 'appengine.sdk' is not set.
+ Either specity the property by passing an argument
+
+ -Dappengine.sdk="path-to-your-appengine-sdk"
+
+ or create a 'local.properties' file containing the
+ location of the App Engine SDK. i.e. the line:
+
+ appengine.sdk=path-to-your-appengine-sdk
+ </fail>
+
+ <!-- Configure AppEngine macros -->
+ <import file="${appengine.sdk}/config/user/ant-macros.xml" />
+
+ <path id="project.class.path">
+ <pathelement location="${war.dir}/WEB-INF/classes"/>
+ <pathelement location="${gwt.sdk}/gwt-user.jar"/>
+ <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/>
+ <fileset dir="${gwt.sdk}" includes="validation-api-1.0.0.GA-sources.jar"/>
+ <fileset dir="${gwt.sdk}" includes="validation-api-1.0.0.GA.jar"/>
+ <!-- Add any additional non-server libs (such as JUnit) -->
+ <fileset dir="${war.dir}/WEB-INF/lib" includes="**/*.jar"/>
+ </path>
+
+ <path id="tools.class.path">
+ <path refid="project.class.path"/>
+ <pathelement location="${appengine.sdk}/lib/appengine-tools-api.jar"/>
+ <fileset dir="${appengine.sdk}/lib/tools">
+ <include name="**/asm-*.jar"/>
+ <include name="**/datanucleus-enhancer-*.jar"/>
+ </fileset>
+ </path>
+
+ <target name="appengine-copyjars"
+ description="Copies the App Engine JARs to the WAR.">
+ <copy
+ todir="${war.dir}/WEB-INF/lib"
+ flatten="true">
+ <fileset dir="${appengine.sdk}/lib/user">
+ <include name="**/*.jar" />
+ </fileset>
+ </copy>
+ </target>
+
+ <target name="libs" description="Copy libs to WEB-INF/lib">
+ <mkdir dir="${war.dir}/WEB-INF/lib" />
+ <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet.jar" />
+ <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet-deps.jar" />
+ <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/validation-api-1.0.0.GA.jar"/>
+ <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/validation-api-1.0.0.GA-sources.jar"/>
+ <!-- Add any additional server libs that need to be copied -->
+ </target>
+
+ <target name="javac" depends="libs,appengine-copyjars" description="Compile java source to bytecode">
+ <mkdir dir="${war.dir}/WEB-INF/classes"/>
+ <javac srcdir="${src.dir}/" includes="**" encoding="utf-8"
+ destdir="${war.dir}/WEB-INF/classes"
+ source="1.5" target="1.5" nowarn="true"
+ debug="true" debuglevel="lines,vars,source">
+ <classpath refid="project.class.path"/>
+ </javac>
+ <copy todir="${war.dir}/WEB-INF/classes">
+ <fileset dir="${src.dir}/" excludes="**/*.java"/>
+ </copy>
+ </target>
+
+ <target name="gwtc" depends="javac" description="GWT compile to JavaScript (production mode)">
+ <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler">
+ <classpath>
+ <pathelement location="${src.dir}/"/>
+ <path refid="project.class.path"/>
+ </classpath>
+ <!-- add jvmarg -Xss16M or similar if you see a StackOverflowError -->
+ <jvmarg value="-Xmx256M"/>
+ <arg line="-war"/>
+ <arg value="${war.dir}/"/>
+ <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+ <arg line="${gwt.args}"/>
+ <arg value="com.google.gwt.sample.mobilewebapp.MobileWebApp"/>
+ </java>
+ </target>
+
+ <target name="datanucleusenhance" depends="javac"
+ description="Performs JDO enhancement on compiled data classes.">
+ <enhance_war war="${war.dir}/" />
+ </target>
+
+ <target name="devmode" depends="javac,datanucleusenhance" description="Run development mode">
+ <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode">
+ <classpath>
+ <pathelement location="${src.dir}/"/>
+ <path refid="project.class.path"/>
+ <path refid="tools.class.path"/>
+ </classpath>
+ <jvmarg value="-Xmx256M"/>
+ <jvmarg value="-javaagent:${appengine.sdk}/lib/agent/appengine-agent.jar"/>
+ <arg value="-startupUrl"/>
+ <arg value="MobileWebApp.html"/>
+ <arg line="-war"/>
+ <arg value="${war.dir}/"/>
+ <arg value="-server"/>
+ <arg value="com.google.appengine.tools.development.gwt.AppEngineLauncher"/>
+ <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+ <arg line="${gwt.args}"/>
+ <arg value="com.google.gwt.sample.mobilewebapp.MobileWebApp"/>
+ </java>
+ </target>
+
+ <target name="build" depends="gwtc,datanucleusenhance" description="Build this project" />
+
+ <target name="war" depends="build" description="Create a war file">
+ <zip destfile="MobileWebApp.war" basedir="${war.dir}/"/>
+ </target>
+
+ <target name="clean" description="Cleans this project">
+ <delete file="MobileWebApp.war" failonerror="false" />
+ <delete dir="war/WEB-INF/appengine-generated/" failonerror="false" />
+ <delete dir="war/WEB-INF/classes/com" failonerror="false" />
+ <delete dir="war/WEB-INF/classes/META-INF" failonerror="false" />
+ <delete file="war/WEB-INF/classes/log4j.properties" failonerror="false" />
+ <delete dir="war/WEB-INF/deploy/" failonerror="false" />
+ <delete dir="war/WEB-INF/lib/" failonerror="false" />
+ <delete dir="war/mobilewebapp/" failonerror="false" />
+ </target>
+
+</project>
diff --git a/samples/mobilewebapp/war/MobileWebApp.css b/samples/mobilewebapp/war/MobileWebApp.css
new file mode 100644
index 0000000..73d994b
--- /dev/null
+++ b/samples/mobilewebapp/war/MobileWebApp.css
@@ -0,0 +1,38 @@
+/** Add css rules here for your application. */
+
+
+/** Example rules used by the template application (remove for your app) */
+h1 {
+ font-size: 2em;
+ font-weight: bold;
+ color: #777777;
+ margin: 40px 0px 70px;
+ text-align: center;
+}
+
+.sendButton {
+ display: block;
+ font-size: 16pt;
+}
+
+/** Most GWT widgets already have a style name defined */
+.gwt-DialogBox {
+ width: 400px;
+}
+
+.dialogVPanel {
+ margin: 5px;
+}
+
+.gwt-CheckBox-disabled {
+ color: #bbb !important;
+}
+
+.serverResponseLabelError {
+ color: red;
+}
+
+/** Set ids using widget.getElement().setId("idOfElement") */
+#closeButton {
+ margin: 15px 6px 6px;
+}
diff --git a/samples/mobilewebapp/war/MobileWebApp.html b/samples/mobilewebapp/war/MobileWebApp.html
new file mode 100644
index 0000000..d8508cc
--- /dev/null
+++ b/samples/mobilewebapp/war/MobileWebApp.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<!-- The DOCTYPE declaration above will set the -->
+<!-- browser's rendering engine into -->
+<!-- "Standards Mode". Replacing this declaration -->
+<!-- with a "Quirks Mode" doctype may lead to some -->
+<!-- differences in layout. -->
+
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+
+ <!-- -->
+ <!-- These meta tags tell the mobile browser that the app is -->
+ <!-- formatted for mobile browsers and should not be zoomed in by -->
+ <!-- default. -->
+ <!-- -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="viewport" content="width=device-width, user-scalable=no">
+
+ <!-- -->
+ <!-- Consider inlining CSS to reduce the number of requested files -->
+ <!-- -->
+ <link type="text/css" rel="stylesheet" href="MobileWebApp.css">
+
+ <!-- -->
+ <!-- Any title is fine -->
+ <!-- -->
+ <title>Web Application Starter Project</title>
+
+ <!-- -->
+ <!-- This script loads your compiled module. -->
+ <!-- If you add any GWT meta tags, they must -->
+ <!-- be added before this line. -->
+ <!-- -->
+ <script type="text/javascript" language="javascript" src="mobilewebapp/mobilewebapp.nocache.js"></script>
+ </head>
+
+ <!-- -->
+ <!-- The body can have arbitrary html, or -->
+ <!-- you can leave the body empty if you want -->
+ <!-- to create a completely dynamic UI. -->
+ <!-- -->
+ <body>
+
+ <!-- OPTIONAL: include this if you want history support -->
+ <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
+
+ <!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
+ <noscript>
+ <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
+ Your web browser must have JavaScript enabled
+ in order for this application to display correctly.
+ </div>
+ </noscript>
+ </body>
+</html>
diff --git a/samples/mobilewebapp/war/WEB-INF/appengine-web.xml b/samples/mobilewebapp/war/WEB-INF/appengine-web.xml
new file mode 100644
index 0000000..3e626c5
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/appengine-web.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
+
+ <!-- Configure serving/caching of GWT files -->
+ <static-files>
+ <include path="**" />
+
+ <!-- The following line requires App Engine 1.3.2 SDK -->
+ <include path="**.nocache.*" expiration="0s" />
+
+ <include path="**.cache.*" expiration="365d" />
+ <exclude path="**.gwt.rpc" />
+ </static-files>
+
+ <!-- Configure java.util.logging -->
+ <system-properties>
+ <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+ </system-properties>
+
+</appengine-web-app>
diff --git a/samples/mobilewebapp/war/WEB-INF/classes/marker b/samples/mobilewebapp/war/WEB-INF/classes/marker
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/classes/marker
diff --git a/samples/mobilewebapp/war/WEB-INF/logging.properties b/samples/mobilewebapp/war/WEB-INF/logging.properties
new file mode 100644
index 0000000..a172066
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/logging.properties
@@ -0,0 +1,13 @@
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+# <system-properties>
+# <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+# </system-properties>
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/samples/mobilewebapp/war/WEB-INF/web.xml b/samples/mobilewebapp/war/WEB-INF/web.xml
new file mode 100644
index 0000000..e19e911
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/web.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE web-app
+ PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+ "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<web-app>
+
+ <!-- RequestFactory -->
+ <servlet>
+ <servlet-name>requestFactoryServlet</servlet-name>
+ <servlet-class>com.google.web.bindery.requestfactory.server.RequestFactoryServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>requestFactoryServlet</servlet-name>
+ <url-pattern>/gwtRequest</url-pattern>
+ </servlet-mapping>
+
+ <!-- Default page to serve -->
+ <welcome-file-list>
+ <welcome-file>MobileWebApp.html</welcome-file>
+ </welcome-file-list>
+
+</web-app>