[Arakhnę-Dev] [422] * Add the general Android library.

[ Thread Index | Date Index | More arakhne.org/dev Archives ]


Revision: 422
Author:   galland
Date:     2013-04-11 18:28:25 +0200 (Thu, 11 Apr 2013)
Log Message:
-----------
* Add the general Android library.

Modified Paths:
--------------
    trunk/ui/pom.xml

Added Paths:
-----------
    trunk/ui/ui-android/
    trunk/ui/ui-android/AndroidManifest.xml
    trunk/ui/ui-android/assets/
    trunk/ui/ui-android/gen/
    trunk/ui/ui-android/libs/
    trunk/ui/ui-android/pom.xml
    trunk/ui/ui-android/proguard-project.txt
    trunk/ui/ui-android/project.properties
    trunk/ui/ui-android/res/
    trunk/ui/ui-android/res/drawable/
    trunk/ui/ui-android/res/drawable/colorpicker_arrow_down.png
    trunk/ui/ui-android/res/drawable/colorpicker_arrow_right.png
    trunk/ui/ui-android/res/drawable/colorpicker_cursor.png
    trunk/ui/ui-android/res/drawable/colorpicker_hue.png
    trunk/ui/ui-android/res/drawable/colorpicker_target.png
    trunk/ui/ui-android/res/drawable/ic_menu_save_selector.xml
    trunk/ui/ui-android/res/drawable/ic_menu_save_upload_selector.xml
    trunk/ui/ui-android/res/drawable/ic_menu_up_directory_selector.xml
    trunk/ui/ui-android/res/drawable-hdpi/
    trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_down.png
    trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_right.png
    trunk/ui/ui-android/res/drawable-hdpi/colorpicker_cursor.png
    trunk/ui/ui-android/res/drawable-hdpi/colorpicker_target.png
    trunk/ui/ui-android/res/drawable-hdpi/ic_file.png
    trunk/ui/ui-android/res/drawable-hdpi/ic_folder.png
    trunk/ui/ui-android/res/drawable-ldpi/
    trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_down.png
    trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_right.png
    trunk/ui/ui-android/res/drawable-ldpi/colorpicker_cursor.png
    trunk/ui/ui-android/res/drawable-ldpi/colorpicker_target.png
    trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_gray.png
    trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload.png
    trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload_gray.png
    trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory.png
    trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory_gray.png
    trunk/ui/ui-android/res/drawable-mdpi/
    trunk/ui/ui-android/res/drawable-mdpi/ic_file.png
    trunk/ui/ui-android/res/drawable-mdpi/ic_folder.png
    trunk/ui/ui-android/res/drawable-xhdpi/
    trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_down.png
    trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_right.png
    trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_cursor.png
    trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_target.png
    trunk/ui/ui-android/res/drawable-xhdpi/hatchs.png
    trunk/ui/ui-android/res/drawable-xhdpi/ic_file.png
    trunk/ui/ui-android/res/drawable-xhdpi/ic_folder.png
    trunk/ui/ui-android/res/layout/
    trunk/ui/ui-android/res/layout/about.xml
    trunk/ui/ui-android/res/layout/colorpicker_dialog.xml
    trunk/ui/ui-android/res/layout/colorpicker_pref_widget.xml
    trunk/ui/ui-android/res/layout/filechooser_onefile.xml
    trunk/ui/ui-android/res/layout/filechooser_open.xml
    trunk/ui/ui-android/res/layout/filechooser_save.xml
    trunk/ui/ui-android/res/layout/texteditor.xml
    trunk/ui/ui-android/res/layout-land/
    trunk/ui/ui-android/res/layout-land/colorpicker_dialog.xml
    trunk/ui/ui-android/res/menu/
    trunk/ui/ui-android/res/menu/filechooser_menu.xml
    trunk/ui/ui-android/res/menu/filechooser_open_menu.xml
    trunk/ui/ui-android/res/menu/filechooser_save_menu.xml
    trunk/ui/ui-android/res/menu/texteditor_menu.xml
    trunk/ui/ui-android/res/values/
    trunk/ui/ui-android/res/values/about_strings.xml
    trunk/ui/ui-android/res/values/colorpicker_dimens.xml
    trunk/ui/ui-android/res/values/colorpicker_strings.xml
    trunk/ui/ui-android/res/values/filechooser_dimens.xml
    trunk/ui/ui-android/res/values/filechooser_strings.xml
    trunk/ui/ui-android/res/values/filechooser_styles.xml
    trunk/ui/ui-android/res/values/propertyeditor_strings.xml
    trunk/ui/ui-android/res/values/texteditor_strings.xml
    trunk/ui/ui-android/res/values/zoomableview_strings.xml
    trunk/ui/ui-android/res/values-fr/
    trunk/ui/ui-android/res/values-fr/about_strings.xml
    trunk/ui/ui-android/res/values-fr/colorpicker_strings.xml
    trunk/ui/ui-android/res/values-fr/filechooser_strings.xml
    trunk/ui/ui-android/res/values-fr/propertyeditor_strings.xml
    trunk/ui/ui-android/res/values-fr/texteditor_strings.xml
    trunk/ui/ui-android/res/values-fr/zoomableview_strings.xml
    trunk/ui/ui-android/res/values-land/
    trunk/ui/ui-android/res/values-land/colorpicker_dimens.xml
    trunk/ui/ui-android/res/values-xlarge-land/
    trunk/ui/ui-android/res/values-xlarge-land/colorpicker_dimens.xml
    trunk/ui/ui-android/src/
    trunk/ui/ui-android/src/main/
    trunk/ui/ui-android/src/main/java/
    trunk/ui/ui-android/src/main/java/org/
    trunk/ui/ui-android/src/main/java/org/arakhne/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/about/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/about/AboutDialog.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorButton.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorChangeListener.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerDialog.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreference.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreferenceView.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerView.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/event/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/event/PointerEventAndroid.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/AsyncFileLoader.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooser.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserActivity.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserIconSelector.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListAdapter.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListFragment.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/progress/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/progress/ProgressDialogProgressionListener.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditorView.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditors.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/TablePropertyEditorView.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/texteditor/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/texteditor/TextEditorActivity.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/DroidZoomableGraphics2D.java
    trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/ZoomableView.java
    trunk/ui/ui-android/src/test/
    trunk/ui/ui-android/src/test/java/

Modified: trunk/ui/pom.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android
___________________________________________________________________
Added: svn:ignore
   + .classpath
..metadata
..settings
..project
target
bin


Added: trunk/ui/ui-android/AndroidManifest.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/AndroidManifest.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml


Property changes on: trunk/ui/ui-android/gen
___________________________________________________________________
Added: svn:ignore
   + *


Added: trunk/ui/ui-android/pom.xml
===================================================================
--- trunk/ui/ui-android/pom.xml	                        (rev 0)
+++ trunk/ui/ui-android/pom.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,74 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.arakhne.afc</groupId>
+		<artifactId>arakhneUi</artifactId>
+		<version>1.1-SNAPSHOT</version>
+	</parent>
+
+	<groupId>org.arakhne.afc.ui</groupId>
+	<artifactId>ui-android</artifactId>
+	<name>Android Utilities</name>
+	<packaging>apklib</packaging>
+
+	<properties>
+		<android.sdk.path>/opt/android-sdk</android.sdk.path>
+		<android.platform>15</android.platform>
+		<android.emulator>acer500</android.emulator>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>com.google.android</groupId>
+			<artifactId>support-v4</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.google.android</groupId>
+			<artifactId>android</artifactId>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.arakhne.afc</groupId>
+			<artifactId>util</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.arakhne.afc.ui</groupId>
+			<artifactId>ui-base</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.arakhne.afc.ui</groupId>
+			<artifactId>ui-vector-android</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.arakhne.afc</groupId>
+			<artifactId>arakhneRefs</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>com.jayway.maven.plugins.android.generation2</groupId>
+				<artifactId>android-maven-plugin</artifactId>
+				<configuration>
+					<androidManifestFile>${project.basedir}/AndroidManifest.xml</androidManifestFile>
+					<assetsDirectory>${project.basedir}/assets</assetsDirectory>
+					<resourceDirectory>${project.basedir}/res</resourceDirectory>
+					<nativeLibrariesDirectory>${project.basedir}/native</nativeLibrariesDirectory>
+					<sdk>
+						<path>${android.sdk.path}</path>
+						<platform>${android.platform}</platform>
+					</sdk>
+					<emulator>
+						<avd>${android.emulator}</avd>
+					</emulator>
+					<deleteConflictingFiles>true</deleteConflictingFiles>
+					<undeployBeforeDeploy>true</undeployBeforeDeploy>
+				</configuration>
+				<extensions>true</extensions>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
\ No newline at end of file

Added: trunk/ui/ui-android/proguard-project.txt
===================================================================
--- trunk/ui/ui-android/proguard-project.txt	                        (rev 0)
+++ trunk/ui/ui-android/proguard-project.txt	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

Added: trunk/ui/ui-android/project.properties
===================================================================
--- trunk/ui/ui-android/project.properties	                        (rev 0)
+++ trunk/ui/ui-android/project.properties	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-15
+android.library=true

Added: trunk/ui/ui-android/res/drawable/colorpicker_arrow_down.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/colorpicker_arrow_down.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable/colorpicker_arrow_right.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/colorpicker_arrow_right.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable/colorpicker_cursor.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/colorpicker_cursor.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable/colorpicker_hue.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/colorpicker_hue.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable/colorpicker_target.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/colorpicker_target.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable/ic_menu_save_selector.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/ic_menu_save_selector.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/drawable/ic_menu_save_upload_selector.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/ic_menu_save_upload_selector.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/drawable/ic_menu_up_directory_selector.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable/ic_menu_up_directory_selector.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_down.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_down.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_right.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_arrow_right.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_cursor.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_cursor.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_target.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/colorpicker_target.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-hdpi/ic_file.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/ic_file.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-hdpi/ic_folder.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-hdpi/ic_folder.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_down.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_down.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_right.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_arrow_right.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_cursor.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_cursor.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_target.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/colorpicker_target.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_gray.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_gray.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload_gray.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_save_upload_gray.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory_gray.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-ldpi/ic_menu_up_directory_gray.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-mdpi/ic_file.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-mdpi/ic_file.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-mdpi/ic_folder.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-mdpi/ic_folder.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_down.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_down.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_right.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_arrow_right.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_cursor.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_cursor.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_target.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/colorpicker_target.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/hatchs.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/hatchs.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/ic_file.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/ic_file.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/drawable-xhdpi/ic_folder.png
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/drawable-xhdpi/ic_folder.png
___________________________________________________________________
Added: svn:mime-type
   + image/png

Added: trunk/ui/ui-android/res/layout/about.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/layout/about.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/layout/colorpicker_dialog.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/layout/colorpicker_dialog.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/layout/colorpicker_pref_widget.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/layout/colorpicker_pref_widget.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/layout/filechooser_onefile.xml
===================================================================
--- trunk/ui/ui-android/res/layout/filechooser_onefile.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/layout/filechooser_onefile.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,19 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+    android:layout_width="fill_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:orientation="horizontal" >
+
+    <ImageView
+        android:id="@+id/fileChooserFileIcon"
+        style="@style/fileChooserIcon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/fileChooserFileName"
+        style="@style/fileChooserName"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
\ No newline at end of file

Added: trunk/ui/ui-android/res/layout/filechooser_open.xml
===================================================================
--- trunk/ui/ui-android/res/layout/filechooser_open.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/layout/filechooser_open.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <FrameLayout
+        android:id="@+id/fileChooserExplorerFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginRight="@dimen/fileChooserListPadding"
+        android:layout_marginLeft="@dimen/fileChooserListPadding" />
+    
+</LinearLayout>
\ No newline at end of file

Added: trunk/ui/ui-android/res/layout/filechooser_save.xml
===================================================================
--- trunk/ui/ui-android/res/layout/filechooser_save.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/layout/filechooser_save.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,35 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <TextView
+            android:id="@+id/filechooserTextView1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/enter_filename" />
+
+        <EditText
+            android:id="@+id/fileChooserFilenameField"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="textUri"
+            android:ems="10" >
+            <requestFocus />
+       	</EditText>
+
+     </LinearLayout>
+	        
+	<FrameLayout
+        android:id="@+id/fileChooserExplorerFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginRight="@dimen/fileChooserListPadding"
+        android:layout_marginLeft="@dimen/fileChooserListPadding" />
+	    
+</LinearLayout>
\ No newline at end of file

Added: trunk/ui/ui-android/res/layout/texteditor.xml
===================================================================
--- trunk/ui/ui-android/res/layout/texteditor.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/layout/texteditor.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,18 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <EditText
+        android:id="@+id/textEditor"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:ems="10"
+        android:inputType="textMultiLine"
+        android:scrollHorizontally="true"
+        android:gravity="top" >
+
+        <requestFocus />
+    </EditText>
+    
+</LinearLayout>
\ No newline at end of file

Added: trunk/ui/ui-android/res/layout-land/colorpicker_dialog.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/layout-land/colorpicker_dialog.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/menu/filechooser_menu.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/menu/filechooser_menu.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/menu/filechooser_open_menu.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/menu/filechooser_open_menu.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/menu/filechooser_save_menu.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/menu/filechooser_save_menu.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/menu/texteditor_menu.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/menu/texteditor_menu.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values/about_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values/about_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values/colorpicker_dimens.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values/colorpicker_dimens.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values/colorpicker_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values/colorpicker_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values/filechooser_dimens.xml
===================================================================
--- trunk/ui/ui-android/res/values/filechooser_dimens.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/filechooser_dimens.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,4 @@
+<resources xmlns:android="http://schemas.android.com/apk/res/android";>
+	<dimen name="fileChooserListPadding">0dp</dimen>
+	<dimen name="fileChooserListItemPadding">6dp</dimen>
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values/filechooser_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values/filechooser_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/filechooser_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,7 @@
+<resources>
+    <string name="empty_directory">Empty directory</string>
+    <string name="choose_file">Select a file</string>
+    <string name="enter_filename">File name:</string>
+    <string name="save">Save</string>
+    <string name="up_directory">Parent directory</string>
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values/filechooser_styles.xml
===================================================================
--- trunk/ui/ui-android/res/values/filechooser_styles.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/filechooser_styles.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,13 @@
+<resources>
+    <style name="fileChooserName">
+        <item name="android:layout_marginLeft">@dimen/fileChooserListItemPadding</item>
+        <item name="android:layout_marginRight">@dimen/fileChooserListItemPadding</item>
+        <item name="android:maxLines">2</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+    <style name="fileChooserIcon">
+        <item name="android:layout_marginLeft">@dimen/fileChooserListItemPadding</item>
+	</style>
+	
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values/propertyeditor_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values/propertyeditor_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/propertyeditor_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,8 @@
+<resources>
+
+    <string name="property_dialog_title">Properties</string>
+    <string name="undo_property_edition">the edition of properties</string>
+    <string name="property_name">Name</string>
+    <string name="property_text">Text</string>
+    
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values/texteditor_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values/texteditor_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/texteditor_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,4 @@
+<resources>
+    <string name="edit_text">Edit a text file</string>
+    <string name="save_upload">Save and upload</string>
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values/zoomableview_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values/zoomableview_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values/zoomableview_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,5 @@
+<resources>
+
+    <string name="reset_view_done">View is reset to its defaults</string>
+
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values-fr/about_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-fr/about_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values-fr/colorpicker_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-fr/colorpicker_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values-fr/filechooser_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-fr/filechooser_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values-fr/propertyeditor_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values-fr/propertyeditor_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values-fr/propertyeditor_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,8 @@
+<resources>
+
+    <string name="property_dialog_title">Propriétés</string>
+    <string name="undo_property_edition">l\'édition de propriétés</string>
+    <string name="property_name">Nom</string>
+    <string name="property_text">Texte</string>
+    
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values-fr/texteditor_strings.xml
===================================================================
--- trunk/ui/ui-android/res/values-fr/texteditor_strings.xml	                        (rev 0)
+++ trunk/ui/ui-android/res/values-fr/texteditor_strings.xml	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,4 @@
+<resources>
+    <string name="edit_text">Éditer un fichier texte</string>
+    <string name="save_upload">Enregistrer et valider</string>
+</resources>
\ No newline at end of file

Added: trunk/ui/ui-android/res/values-fr/zoomableview_strings.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-fr/zoomableview_strings.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values-land/colorpicker_dimens.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-land/colorpicker_dimens.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/res/values-xlarge-land/colorpicker_dimens.xml
===================================================================
(Binary files differ)


Property changes on: trunk/ui/ui-android/res/values-xlarge-land/colorpicker_dimens.xml
___________________________________________________________________
Added: svn:mime-type
   + application/xml

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/about/AboutDialog.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/about/AboutDialog.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/about/AboutDialog.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,125 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.about;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.arakhne.afc.ui.android.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.util.Linkify;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/** Dialog that is displaying a "about this app" message.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class AboutDialog extends Dialog {
+
+	private final int applicationIconId;
+	private final String legalAssetName;
+	private final String infoAssetName;
+	
+	/** Create the "About" dialog with the default configuration.
+	 * The description of the application is read for the asset file
+	 * {@code "info.html"} (HTML format).
+	 * The legal text is read from the asset file {@code "legal.html"}
+	 * (HTML format).
+	 * 
+	 * @param context
+	 * @see #AboutDialog(Context, int, String, String)
+	 */
+	public AboutDialog(Context context) {
+		this(context, 0, null, null);
+	}
+
+	/** Create the "About" dialog with the default configuration.
+	 * 
+	 * @param context
+	 * @param applicationIcon is the identifier of the application icon.
+	 * @param infoAsset is the name of the asset file that is a HTML file with the description of the application.
+	 * By default it is {@code "info.html"}.
+	 * @param legalAsset is the name of the asset file that is a HTML file with the legal text to display in the dialog box.
+	 * By default it is {@code "legal.html"}.
+	 */
+	public AboutDialog(Context context, int applicationIcon, String infoAsset, String legalAsset) {
+		super(context);
+		this.applicationIconId = applicationIcon;
+		if (infoAsset==null || infoAsset.isEmpty())
+			this.infoAssetName = "info.html"; //$NON-NLS-1$
+		else
+			this.infoAssetName = infoAsset;
+		if (legalAsset==null || legalAsset.isEmpty())
+			this.legalAssetName = "legal.html"; //$NON-NLS-1$
+		else
+			this.legalAssetName = legalAsset;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		setTitle(R.string.about_this_app);
+		setContentView(R.layout.about);
+		if (this.applicationIconId>0) {
+			ImageView iv = (ImageView)findViewById(R.id.about_application_id);
+			iv.setImageResource(this.applicationIconId);
+		}
+		TextView tv = (TextView)findViewById(R.id.about_legal_text);
+		tv.setText(Html.fromHtml(readAsset(this.legalAssetName)));
+		tv = (TextView)findViewById(R.id.about_info_text);
+		tv.setText(Html.fromHtml(readAsset(this.infoAssetName)));
+		Linkify.addLinks(tv, Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS);
+	}
+
+	private String readAsset(String assetName) {
+		try {
+			InputStream inputStream = getContext().getAssets().open(assetName);
+			try {
+				InputStreamReader in = new InputStreamReader(inputStream);
+				BufferedReader buf = new BufferedReader(in);
+				String line;
+				StringBuilder text = new StringBuilder();
+				while (( line = buf.readLine()) != null) {
+					text.append(line);
+				}
+				return text.toString();
+			}
+			finally {
+				inputStream.close();
+			}
+		}
+		catch (IOException _) {
+			return null;
+		}
+	}
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorButton.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorButton.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorButton.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,198 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.button;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.afc.ui.android.colorpicker.ColorPickerDialog;
+import org.arakhne.afc.ui.android.colorpicker.ColorPickerDialog.OnColorPickerListener;
+import org.arakhne.afc.util.ListenerCollection;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.View;
+import android.widget.Button;
+
+
+/** A button for picking a color.  
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class ColorButton extends Button {
+
+	private final Listener eventListener = new Listener();
+	private final ListenerCollection<ColorChangeListener> listeners = new ListenerCollection<ColorChangeListener>();
+	private Integer color = null;
+	private boolean enableDefaultColor = true;
+
+	/**
+	 * @param context
+	 */
+	public ColorButton(Context context) {
+		super(context, null, android.R.attr.buttonStyleSmall);
+		setClickable(true);
+		setOnClickListener(this.eventListener);
+		setBackgroundResource(R.drawable.hatchs);
+		setText(R.string.default_color);
+	}
+
+	/** Replies if the default color is allowed in this color button.
+	 * 
+	 * @return the default color is allowed in this color button.
+	 */
+	public boolean isDefaultColorEnabled() {
+		return this.enableDefaultColor;
+	}
+
+	/** Set if the default color is allowed in this color button.
+	 * 
+	 * @param enable is the default color is allowed in this color button.
+	 */
+	public void setDefaultColorEnabled(boolean enable) {
+		if (this.enableDefaultColor!=enable) {
+			this.enableDefaultColor = enable;
+			if (!this.enableDefaultColor && this.color==null) {
+				setColor(Color.BLACK);
+			}
+		}
+	}
+
+	/** Change the selected color.
+	 * 
+	 * @param rgb
+	 */
+	public final void setColor(int rgb) {
+		setColor(Integer.valueOf(rgb));
+	}
+
+	/** Change the selected color.
+	 * 
+	 * @param rgb
+	 */
+	public final void setColor(Integer rgb) {
+		if ((rgb!=null || isDefaultColorEnabled())
+				&&((this.color==null && rgb!=null)
+				   || (this.color!=null && !this.color.equals(rgb)))) {
+			this.color = rgb;
+			if (this.color!=null) {
+				setBackgroundColor(this.color.intValue());
+				setText(this.color.toString());
+			}
+			else {
+				setBackgroundResource(R.drawable.hatchs);
+				setText(R.string.default_color);
+			}
+			fireColorChange(this.color);
+		}
+	}
+
+	/** Replies the selected color.
+	 * 
+	 * @return the selected color; or <code>null</code> if the
+	 * default color is selected.
+	 */
+	public Integer getColor() {
+		return this.color;
+	}
+
+	/** Add listener on color changes.
+	 * 
+	 * @param listener
+	 */
+	public void addColorChangeListener(ColorChangeListener listener) {
+		this.listeners.add(ColorChangeListener.class, listener);
+	}
+
+	/** Remove listener on color changes.
+	 * 
+	 * @param listener
+	 */
+	public void removeColorChangeListener(ColorChangeListener listener) {
+		this.listeners.remove(ColorChangeListener.class, listener);
+	}
+
+	/** Notifies listeners about a color change.
+	 * 
+	 * @param color
+	 */
+	public void fireColorChange(int color) {
+		for(ColorChangeListener listener : this.listeners.getListeners(ColorChangeListener.class)) {
+			listener.onSelectedColorChanged(this, color);
+		}
+	}
+
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private class Listener implements OnClickListener, OnColorPickerListener {
+
+		/**
+		 */
+		public Listener() {
+			//
+		}
+
+		@Override
+		public void onClick(View v) {
+			Integer color = getColor();
+			if (color==null) {
+				color = Color.BLACK;
+			}
+			ColorPickerDialog dialog;
+			if (color!=null) { 
+				dialog = new ColorPickerDialog(
+						getContext(),
+						color,
+						isDefaultColorEnabled(),
+						this);
+			}
+			else {
+				dialog = new ColorPickerDialog(
+						getContext(),
+						isDefaultColorEnabled(),
+						this);
+			}
+			dialog.show();
+		}
+
+		@Override
+		public void onColorPickingCanceled(ColorPickerDialog dialog) {
+			//
+		}
+
+		@Override
+		public void onColorPicked(ColorPickerDialog dialog, int color) {
+			setColor(color);
+		}
+
+		@Override
+		public void onDefaultColorPicked(ColorPickerDialog dialog) {
+			setColor(null);
+		}
+
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorChangeListener.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorChangeListener.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/button/ColorChangeListener.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,41 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.button;
+
+import java.util.EventListener;
+
+/** Listener on change of color in a ColorButton.  
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public interface ColorChangeListener extends EventListener {
+
+	/** Invoked when the color selected in a ColorButton has changed.
+	 * 
+	 * @param source
+	 * @param color is the RGBA color
+	 */
+	public void onSelectedColorChanged(ColorButton source, int color);
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerDialog.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerDialog.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerDialog.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,412 @@
+/*
+ * $Id$
+ * 
+ * Copyright 2012, Yuku Sugianto
+ * Copyright 2013, Yuku Sugianto, Stephane Galland
+ * 
+ * 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 org.arakhne.afc.ui.android.colorpicker;
+
+import org.arakhne.afc.ui.android.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+/** A dialog that permits to pick a color.
+ * This view is a panel on which all the colors are painted
+ * and on which the user may click to pick a color.
+ * <p>
+ * The original source code was copied from
+ * {@link "http://code.google.com/p/android-color-picker/"}.
+ * Comments were added, and source code patched for
+ * AFC compliance.
+ * 
+ * @author $Author: yukuku$
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class ColorPickerDialog extends AlertDialog {
+
+	private final OnColorPickerListener listener;
+	private final float[] currentColorHsv = new float[3];
+
+	private final View viewHue;
+	private final ColorPickerView viewSatVal;
+	private final ImageView viewCursor;
+	private final View viewOldColor;
+	private final View viewNewColor;
+	private final ImageView viewTarget;
+	private final ViewGroup viewContainer;
+
+	/**
+	 * Create a ColorPickerDialog with black color.
+	 * Call this only from {@code OnCreateDialog()} or from a background thread.
+	 * 
+	 * @param context
+	 * @param enableDefaultColorButton indicates if the button to select a default color
+	 * is displayed or not. 
+	 * @param listener is a listener on the picking events.
+	 */
+	public ColorPickerDialog(Context context, boolean enableDefaultColorButton, OnColorPickerListener listener) {
+		this(context, 0xff000000, enableDefaultColorButton, listener);
+	}
+
+	/**
+	 * Create a ColorPickerDialog.
+	 * Call this only from {@code OnCreateDialog()} or from a background thread.
+	 * 
+	 * @param context
+	 * @param color is the RGB color
+	 * @param enableDefaultColorButton indicates if the button to select a default color
+	 * is displayed or not. 
+	 * @param listener is a listener on the picking events.
+	 */
+	public ColorPickerDialog(Context context, int color, boolean enableDefaultColorButton, OnColorPickerListener listener) {
+		super(context);
+		this.listener = listener;
+		Color.colorToHSV(color, this.currentColorHsv);
+
+		final View rootView = LayoutInflater.from(context).inflate(R.layout.colorpicker_dialog, null);
+		this.viewHue = rootView.findViewById(R.id.colorpicker_viewHue);
+		this.viewSatVal = (ColorPickerView) rootView.findViewById(R.id.colorpicker_viewSatBri);
+		this.viewCursor = (ImageView) rootView.findViewById(R.id.colorpicker_cursor);
+		this.viewOldColor = rootView.findViewById(R.id.colorpicker_warnaLama);
+		this.viewNewColor = rootView.findViewById(R.id.colorpicker_warnaBaru);
+		this.viewTarget = (ImageView) rootView.findViewById(R.id.colorpicker_target);
+		this.viewContainer = (ViewGroup) rootView.findViewById(R.id.colorpicker_viewContainer);
+
+		this.viewSatVal.setHue(getHue());
+		this.viewOldColor.setBackgroundColor(color);
+		this.viewNewColor.setBackgroundColor(color);
+
+		this.viewHue.setOnTouchListener(new View.OnTouchListener() {
+			@SuppressWarnings("synthetic-access")
+			@Override
+			public boolean onTouch(View v, MotionEvent event) {
+				if (event.getAction() == MotionEvent.ACTION_MOVE
+						|| event.getAction() == MotionEvent.ACTION_DOWN
+						|| event.getAction() == MotionEvent.ACTION_UP) {
+
+					float y = event.getY();
+					if (y < 0.f) y = 0.f;
+					if (y > ColorPickerDialog.this.viewHue.getMeasuredHeight())
+						y = ColorPickerDialog.this.viewHue.getMeasuredHeight() - 0.001f; // to avoid looping from end to start.
+					float hue = 360.f - 360.f / ColorPickerDialog.this.viewHue.getMeasuredHeight() * y;
+					if (hue == 360.f) hue = 0.f;
+
+					setHue(hue);
+					return true;
+				}
+				return false;
+			}
+		});
+
+		this.viewSatVal.setOnTouchListener(new View.OnTouchListener() {
+			@SuppressWarnings("synthetic-access")
+			@Override public boolean onTouch(View v, MotionEvent event) {
+				if (event.getAction() == MotionEvent.ACTION_MOVE
+						|| event.getAction() == MotionEvent.ACTION_DOWN
+						|| event.getAction() == MotionEvent.ACTION_UP) {
+
+					float x = event.getX(); // touch event are in dp units.
+					float y = event.getY();
+
+					if (x < 0.f) x = 0.f;
+					if (x > ColorPickerDialog.this.viewSatVal.getMeasuredWidth())
+						x = ColorPickerDialog.this.viewSatVal.getMeasuredWidth();
+					if (y < 0.f) y = 0.f;
+					if (y > ColorPickerDialog.this.viewSatVal.getMeasuredHeight())
+						y = ColorPickerDialog.this.viewSatVal.getMeasuredHeight();
+
+					setSaturationAndValue(
+							1.f / ColorPickerDialog.this.viewSatVal.getMeasuredWidth() * x,
+							1.f - (1.f / ColorPickerDialog.this.viewSatVal.getMeasuredHeight() * y));
+					return true;
+				}
+				return false;
+			}
+		});
+
+		setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				fireColorPicked();
+			}
+		});
+		setButton(BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				fireColorPickingCanceled();
+			}
+		});
+		setOnCancelListener(new OnCancelListener() {
+			@Override
+			public void onCancel(DialogInterface paramDialogInterface) {
+				fireColorPickingCanceled();
+			}
+		});
+		if (enableDefaultColorButton) {
+			setButton(BUTTON_NEUTRAL,
+				context.getString(R.string.default_color),
+				new OnClickListener() {
+				@Override
+				public void onClick(DialogInterface dialog, int which) {
+					dialog.dismiss();
+					fireDefaultColorPicked();
+				}
+			});
+		}
+		// kill all padding from the dialog window
+		setView(rootView, 0, 0, 0, 0);
+
+		// move cursor & target on first draw
+		ViewTreeObserver vto = rootView.getViewTreeObserver();
+		vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+			@SuppressWarnings("synthetic-access")
+			@Override
+			public void onGlobalLayout() {
+				moveCursor();
+				moveTarget();
+				rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+			}
+		});
+	}
+
+	/** Notifies the listeners about the cancelation of the color picking.
+	 */
+	protected void fireColorPickingCanceled() {
+		if (this.listener != null) {
+			this.listener.onColorPickingCanceled(this);
+		}
+	}
+
+	/** Notifies the listeners about the selection of the color.
+	 */
+	protected void fireColorPicked() {
+		if (this.listener != null) {
+			this.listener.onColorPicked(this, getRGB());
+		}
+	}
+
+	/** Notifies the listeners about the selection of the default color.
+	 */
+	protected void fireDefaultColorPicked() {
+		if (this.listener != null) {
+			this.listener.onDefaultColorPicked(this);
+		}
+	}
+
+	private void moveCursor() {
+		float y = this.viewHue.getMeasuredHeight() - (getHue() * this.viewHue.getMeasuredHeight() / 360.f);
+		if (y == this.viewHue.getMeasuredHeight()) y = 0.f;
+		RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) this.viewCursor.getLayoutParams();
+		layoutParams.leftMargin = (int) (this.viewHue.getLeft() - Math.floor(this.viewCursor.getMeasuredWidth() / 2) - this.viewContainer.getPaddingLeft());
+
+		layoutParams.topMargin = (int) (this.viewHue.getTop() + y - Math.floor(this.viewCursor.getMeasuredHeight() / 2) - this.viewContainer.getPaddingTop());
+
+		this.viewCursor.setLayoutParams(layoutParams);
+	}
+
+	private void moveTarget() {
+		float x = getSaturation() * this.viewSatVal.getMeasuredWidth();
+		float y = (1.f - getValue()) * this.viewSatVal.getMeasuredHeight();
+		RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) this.viewTarget.getLayoutParams();
+		layoutParams.leftMargin = (int) (this.viewSatVal.getLeft() + x - Math.floor(this.viewTarget.getMeasuredWidth() / 2) - this.viewContainer.getPaddingLeft());
+		layoutParams.topMargin = (int) (this.viewSatVal.getTop() + y - Math.floor(this.viewTarget.getMeasuredHeight() / 2) - this.viewContainer.getPaddingTop());
+		this.viewTarget.setLayoutParams(layoutParams);
+	}
+
+	/** Replies the RGB representation of the current selected color.
+	 *  
+	 * @return RGB
+	 */
+	public int getRGB() {
+		return Color.HSVToColor(this.currentColorHsv);
+	}
+
+	/** Set the RGB representation of the current selected color.
+	 *  
+	 * @param rgb
+	 */
+	public void setRGB(int rgb) {
+		float[] hsv = new float[3];
+		Color.colorToHSV(rgb, hsv);
+		setHSV(hsv);
+	}
+
+	/** Replies the HSV representation of the current selected color.
+	 *  
+	 * @return HSV
+	 */
+	public float[] getHSV() {
+		return this.currentColorHsv.clone();
+	}
+
+	/** Set the HSV representation of the current selected color.
+	 *  
+	 * @param hsv
+	 */
+	public final void setHSV(float[] hsv) {
+		setHSV(hsv[0], hsv[1], hsv[2]);
+	}
+
+	/** Set the HSV representation of the current selected color.
+	 *  
+	 * @param hue
+	 * @param saturation
+	 * @param value
+	 */
+	public void setHSV(float hue, float saturation, float value) {
+		if (hue!=this.currentColorHsv[0]
+			||saturation!=this.currentColorHsv[1]
+			||value!=this.currentColorHsv[2]) {
+			this.currentColorHsv[0] = hue;
+			this.currentColorHsv[1] = saturation;
+			this.currentColorHsv[2] = value;
+			
+			this.viewSatVal.setHue(getHue());
+			moveCursor();
+			moveTarget();
+			this.viewNewColor.setBackgroundColor(getRGB());
+		}
+	}
+
+	/** Replies the hue of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @return the hue of the color.
+	 */
+	public float getHue() {
+		return this.currentColorHsv[0];
+	}
+
+	/** Set the hue of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @param hue the hue of the color.
+	 */
+	public void setHue(float hue) {
+		if (hue!=this.currentColorHsv[0]) {
+			this.currentColorHsv[0] = hue;
+			this.viewSatVal.setHue(getHue());
+			moveCursor();
+			this.viewNewColor.setBackgroundColor(getRGB());
+		}
+	}
+
+	/** Replies the saturation of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @return the saturation of the color.
+	 */
+	public float getSaturation() {
+		return this.currentColorHsv[1];
+	}
+
+	/** Set the saturation of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @param sat is the saturation of the color.
+	 */
+	public void setSaturation(float sat) {
+		if (sat!=this.currentColorHsv[1]) {
+			this.currentColorHsv[1] = sat;
+			moveTarget();
+			this.viewNewColor.setBackgroundColor(getRGB());
+		}
+	}
+
+	/** Replies the value of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @return the value of the color.
+	 */
+	public float getValue() {
+		return this.currentColorHsv[2];
+	}
+
+	/** Set the value of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @param val is the value of the color.
+	 */
+	public void setValue(float val) {
+		if (val!=this.currentColorHsv[2]) {
+			this.currentColorHsv[2] = val;
+			moveTarget();
+			this.viewNewColor.setBackgroundColor(getRGB());
+		}
+	}
+
+	/** Set the saturation and the value of the current color, according
+	 * to the HSV representation.
+	 * 
+	 * @param sat is the saturation of the color.
+	 * @param val is the value of the color.
+	 */
+	protected void setSaturationAndValue(float sat, float val) {
+		if (sat!=this.currentColorHsv[1] || val!=this.currentColorHsv[2]) {
+			this.currentColorHsv[1] = sat;
+			this.currentColorHsv[2] = val;
+			moveTarget();
+			this.viewNewColor.setBackgroundColor(getRGB());
+		}
+	}
+
+	/** Listener on color picking in a color picked
+	 * dialog.
+	 * 
+	 * @author $Author: yukuku$
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	public interface OnColorPickerListener {
+
+		/** Invoked when the color picking was canceled.
+		 * 
+		 * @param dialog is the canceled dialog.
+		 */
+		public void onColorPickingCanceled(ColorPickerDialog dialog);
+
+		/** Invoked when the color picking was successfull
+		 * with a selected color.
+		 * 
+		 * @param dialog is the dialog from which the color was selected.
+		 * @param color is the RGB color.
+		 */
+		public void onColorPicked(ColorPickerDialog dialog, int color);
+
+		/** Invoked when the color picking was successfull
+		 * with the default color.
+		 * 
+		 * @param dialog is the dialog from which the color was selected.
+		 */
+		public void onDefaultColorPicked(ColorPickerDialog dialog);
+
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreference.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreference.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreference.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,245 @@
+/*
+ * $Id$
+ * 
+ * Copyright 2012, Yuku Sugianto
+ * Copyright 2013, Yuku Sugianto, Stephane Galland
+ * 
+ * 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 org.arakhne.afc.ui.android.colorpicker;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.afc.ui.android.colorpicker.ColorPickerDialog.OnColorPickerListener;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+
+/** A preference that permits to pick a color in the preference UI.
+ * <p>
+ * The original source code was copied from
+ * {@link "http://code.google.com/p/android-color-picker/"}.
+ * Comments were added, and source code patched for
+ * AFC compliance.
+ * 
+ * @author $Author: yukuku$
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class ColorPickerPreference extends Preference {
+
+	private int rgb;
+
+	/**
+	 * @param context
+	 * @param attrs
+	 */
+	public ColorPickerPreference(Context context, AttributeSet attrs) {
+		super(context, attrs);
+		setWidgetLayoutResource(R.layout.colorpicker_pref_widget);
+	}
+
+	/**
+	 * @param context
+	 * @param attrs
+	 * @param defStyle
+	 */
+	public ColorPickerPreference(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		setWidgetLayoutResource(R.layout.colorpicker_pref_widget);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onBindView(View view) {
+		super.onBindView(view);
+
+		// Set our custom views inside the layout
+		View kotak = view.findViewById(R.id.colorpicker_pref_widget_kotak);
+		if (kotak != null) {
+			kotak.setBackgroundColor(this.rgb);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onClick() {
+		OnColorPickerListener listener = new OnColorPickerListener() {
+			@Override
+			public void onColorPickingCanceled(ColorPickerDialog dialog) {
+				//
+			}
+			@Override
+			public void onColorPicked(ColorPickerDialog dialog, int color) {
+				setRGB(color);
+			}
+			@Override
+			public void onDefaultColorPicked(ColorPickerDialog dialog) {
+				//
+			}
+		};
+		
+		new ColorPickerDialog(getContext(), this.rgb, false, listener).show();
+	}
+	
+	/** Set the rgb preference after asking to the
+	 * client if the change is allowed.
+	 * 
+	 * @param rgb
+	 */
+	public void setRGB(int rgb) {
+		// They don't want the value to be set
+		if (!callChangeListener(rgb)) return;
+		ColorPickerPreference.this.rgb = rgb;
+		persistInt(ColorPickerPreference.this.rgb);
+		notifyChanged();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected Object onGetDefaultValue(TypedArray a, int index) {
+		// This preference type's value type is Integer,
+		// so we read the default value from the attributes
+		// as an Integer.
+		return a.getInteger(index, 0);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+		if (restoreValue) {
+			// Restore state
+			this.rgb = getPersistedInt(this.rgb);
+		}
+		else {
+			// Set state
+			this.rgb = ((Number)defaultValue).intValue();
+			persistInt(this.rgb);
+		}
+	}
+
+	/** {@inheritDoc}
+	 * <p>
+	 * Suppose a client uses this preference type without persisting. We
+	 * must save the instance state so it is able to, for example, survive
+	 * orientation changes.
+	 */
+	@Override
+	protected Parcelable onSaveInstanceState() {
+		Parcelable superState = super.onSaveInstanceState();
+
+		// No need to save instance state since it's persistent
+		if (isPersistent())
+			return superState;
+
+		SavedState myState = new SavedState(superState, this.rgb);
+		return myState;
+	}
+
+	@Override protected void onRestoreInstanceState(Parcelable state) {
+		if (!state.getClass().equals(SavedState.class)) {
+			// Didn't save state for us in onSaveInstanceState
+			super.onRestoreInstanceState(state);
+			return;
+		}
+
+		// Restore the instance state
+		SavedState myState = (SavedState) state;
+		super.onRestoreInstanceState(myState.getSuperState());
+		this.rgb = myState.getRGB();
+		notifyChanged();
+	}
+
+	/**
+	 * SavedState, a subclass of {@link BaseSavedState}, will store the state
+	 * of MyPreference, a subclass of Preference.
+	 * <p>
+	 * It is important to always call through to super methods.
+	 * <p>
+	 * The original source code was copied from
+	 * {@link "http://code.google.com/p/android-color-picker/"}.
+	 * Comments were added, and source code patched for
+	 * AFC compliance.
+	 * 
+	 * @author $Author: yukuku$
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private static class SavedState extends BaseSavedState {
+		
+		private int rgb;
+
+		/**
+		 * @param source
+		 */
+		public SavedState(Parcel source) {
+			super(source);
+			this.rgb = source.readInt();
+		}
+
+		/**
+		 * @param superState
+		 * @param rgb
+		 */
+		public SavedState(Parcelable superState, int rgb) {
+			super(superState);
+			this.rgb = rgb;
+		}
+		
+		/**
+		 * Replies the saved RGB.
+		 * 
+		 * @return RGB
+		 */
+		public int getRGB() {
+			return this.rgb;
+		}
+
+		@Override
+		public void writeToParcel(Parcel dest, int flags) {
+			super.writeToParcel(dest, flags);
+			dest.writeInt(this.rgb);
+		}
+
+		@SuppressWarnings({ "unused", "hiding" })
+		public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+			@Override
+			public SavedState createFromParcel(Parcel in) {
+				return new SavedState(in);
+			}
+
+			@Override
+			public SavedState[] newArray(int size) {
+				return new SavedState[size];
+			}
+		};
+		
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreferenceView.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreferenceView.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerPreferenceView.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,86 @@
+/*
+ * $Id$
+ * 
+ * Copyright 2012, Yuku Sugianto
+ * Copyright 2013, Yuku Sugianto, Stephane Galland
+ * 
+ * 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 org.arakhne.afc.ui.android.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.util.AttributeSet;
+import android.view.View;
+
+/** A view that permits to pick a color in the preference UI.
+ * <p>
+ * The original source code was copied from
+ * {@link "http://code.google.com/p/android-color-picker/"}.
+ * Comments were added, and source code patched for
+ * AFC compliance.
+ * 
+ * @author $Author: yukuku$
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ * @see ColorPickerView for a view outside the preference UI.
+ */
+public class ColorPickerPreferenceView extends View {
+	
+	private final Paint paint;
+	private final float rectSize;
+	private final float strokeWidth;
+
+	/**
+	 * @param context
+	 * @param attrs
+	 */
+	public ColorPickerPreferenceView(Context context, AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	/**
+	 * @param context
+	 * @param attrs
+	 * @param defStyle
+	 */
+	public ColorPickerPreferenceView(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		
+		float density = context.getResources().getDisplayMetrics().density;
+		this.rectSize = (float)Math.floor(24.f * density + 0.5f);
+		this.strokeWidth = (float)Math.floor(1.f * density + 0.5f);
+
+		this.paint = new Paint();
+		this.paint.setColor(0xffffffff);
+		this.paint.setStyle(Style.STROKE);
+		this.paint.setStrokeWidth(this.strokeWidth);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onDraw(Canvas canvas) {
+		super.onDraw(canvas);
+		canvas.drawRect(
+				this.strokeWidth, this.strokeWidth,
+				this.rectSize - this.strokeWidth,
+				this.rectSize - this.strokeWidth,
+				this.paint);
+	}
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerView.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerView.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/colorpicker/ColorPickerView.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,167 @@
+/*
+ * $Id$
+ * 
+ * Copyright 2012, Yuku Sugianto
+ * Copyright 2013, Yuku Sugianto, Stephane Galland
+ * 
+ * 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 org.arakhne.afc.ui.android.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposeShader;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Shader;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.View;
+
+/** A view that permits to pick a color.
+ * This view is a panel on which all the colors are painted
+ * and on which the user may click to pick a color.
+ * <p>
+ * The original source code was copied from
+ * {@link "http://code.google.com/p/android-color-picker/"}.
+ * Comments were added, and source code patched for
+ * AFC compliance.
+ * 
+ * @author $Author: yukuku$
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ * @see ColorPickerPreferenceView for a view inside the preference UI.
+ */
+public class ColorPickerView extends View {
+
+
+	/** Current selected color.
+	 */
+	private final float[] hsv = { 1.f, 1.f, 1.f };
+
+	private final Paint paint = new Paint();
+	private Shader whiteBlackGradientShader = null;
+	private Shader whiteColorGradientShader = null;
+	private Shader composedGradientShader = null;
+
+	/**
+	 * @param context
+	 * @param attrs
+	 */
+	public ColorPickerView(Context context, AttributeSet attrs) {
+		super(context, attrs);
+	}
+
+	/**
+	 * @param context
+	 * @param attrs
+	 * @param defStyle
+	 */
+	public ColorPickerView(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+	}
+
+	private Shader getWhiteToBlackGradientShader() {
+		if (this.whiteBlackGradientShader==null) {
+			this.whiteBlackGradientShader = new LinearGradient(
+					0.f, 0.f, 0.f, getMeasuredHeight(), 
+					0xffffffff, 0xff000000, 
+					TileMode.CLAMP);
+		}
+		return this.whiteBlackGradientShader;
+	}
+
+	private Shader getWhiteToColorGradientShader() {
+		if (this.whiteColorGradientShader==null) {
+			this.whiteColorGradientShader = new LinearGradient(
+					0.f, 0.f, this.getMeasuredWidth(),
+					0.f,
+					0xffffffff,
+					getRGB(),
+					TileMode.CLAMP);
+		}
+		return this.whiteColorGradientShader;
+	}
+
+	private Shader getComposedGradientShader() {
+		if (this.composedGradientShader==null) {
+			this.composedGradientShader = new ComposeShader(
+					getWhiteToBlackGradientShader(),
+					getWhiteToColorGradientShader(), 
+					PorterDuff.Mode.MULTIPLY);
+		}
+		return this.composedGradientShader;
+	}
+
+	private void reinitPaintingObjects() {
+		this.whiteColorGradientShader = null;
+		this.composedGradientShader = null;
+		this.paint.setShader(getComposedGradientShader());
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onDraw(Canvas canvas) {
+		super.onDraw(canvas);
+		canvas.drawRect(
+				0.f, 0.f,
+				this.getMeasuredWidth(), this.getMeasuredHeight(),
+				this.paint);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+		super.onSizeChanged(w, h, oldw, oldh);
+		// Ensure that the gradients will be correctly drawn according
+		// to the size of the view
+		reinitPaintingObjects();
+	}
+
+	/** Replies the RGB representation of the current color.
+	 * 
+	 * @return RGB
+	 */
+	public int getRGB() {
+		return Color.HSVToColor(this.hsv);
+	}
+
+	/** Replies the HSV representation of the current color.
+	 * 
+	 * @return HSV
+	 */
+	public float[] getHSV() {
+		return this.hsv.clone();
+	}
+
+	/** Set the hue of the color according to HSB color definition.
+	 * 
+	 * @param hue
+	 */
+	public void setHue(float hue) {
+		if (this.hsv[0]!=hue) {
+			this.hsv[0] = hue;
+			reinitPaintingObjects();
+			invalidate();
+		}
+	}
+	
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/event/PointerEventAndroid.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/event/PointerEventAndroid.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/event/PointerEventAndroid.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,225 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.event ;
+
+import java.util.EventObject;
+
+import org.arakhne.afc.math.continous.object2d.Circle2f;
+import org.arakhne.afc.math.continous.object2d.Shape2f;
+import org.arakhne.afc.ui.event.PointerEvent;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/** Android implementation of a pointer event.
+ *  
+ * @author $Author: galland$
+ * @version $FullVersion$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class PointerEventAndroid extends EventObject implements PointerEvent {
+
+	private static final long serialVersionUID = -1281311602355739859L;
+
+	private final MotionEvent motionEvent;
+	private long when;
+	private float x;
+	private float y;
+	private boolean consumed = false;
+	
+	/**
+	 * @param source
+	 * @param x
+	 * @param y
+	 * @param me
+	 */
+	public PointerEventAndroid(Object source, float x, float y, MotionEvent me) {
+		super(source);
+		this.motionEvent = me;
+		this.when = me.getEventTime();
+		this.x = x;
+		this.y = y;
+	}
+
+	@Override
+	public int getDeviceId() {
+		return this.motionEvent.getDeviceId();
+	}
+	
+	@Override
+	public long when() {
+		return this.when;
+	}
+	
+	/** Change the timestamp of the event.
+	 * 
+	 * @param when is the new timestamp.
+	 */
+	public void setWhen(long when) {
+		this.when = when;
+	}
+
+	@Override
+	public boolean isConsumed() {
+		return this.consumed;
+	}
+
+	@Override
+	public void consume() {
+		this.consumed = true;		
+	}
+	
+	/** Mark this event as not consumed.
+	 */
+	public void unconsume() {
+		this.consumed = false;
+	}
+
+	@Override
+	public boolean isShiftDown() {
+		return (this.motionEvent.getMetaState()&KeyEvent.META_SHIFT_ON)!=0;
+	}
+
+	@Override
+	public boolean isControlDown() {
+		return (this.motionEvent.getMetaState()&KeyEvent.META_CTRL_ON)!=0;
+	}
+
+	@Override
+	public boolean isMetaDown() {
+		return (this.motionEvent.getMetaState()&KeyEvent.META_META_ON)!=0;
+	}
+
+	@Override
+	public boolean isAltDown() {
+		return (this.motionEvent.getMetaState()&KeyEvent.META_ALT_ON)!=0;
+	}
+
+	@Override
+	public boolean isAltGraphDown() {
+		return false;
+	}
+
+	@Override
+	public boolean isContextualActionTriggered() {
+		return false;
+	}
+	
+	@Override
+	public float getX() {
+		return this.x;
+	}
+
+	@Override
+	public float getY() {
+		return this.y;
+	}
+
+	@Override
+	public int getButton() {
+		return this.motionEvent.getButtonState();
+	}
+
+	@Override
+	public int getClickCount() {
+		return 1;
+	}
+
+	@Override
+	public float getOrientation() {
+		return this.motionEvent.getOrientation();
+	}
+	
+	/** Set the position of the event.
+	 * 
+	 * @param x
+	 * @param y
+	 */
+	public void setXY(float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String toString() {
+		return "device="+getDeviceId()+ //$NON-NLS-1$
+				"\nx="+getX()+ //$NON-NLS-1$
+				"\ny="+getY()+ //$NON-NLS-1$
+				"\nbutton="+getButton()+ //$NON-NLS-1$
+				"\nkeys=0x"+Integer.toHexString(this.motionEvent.getMetaState())+ //$NON-NLS-1$
+				"\norientation="+getOrientation()+ //$NON-NLS-1$
+				"\nconsumed="+isConsumed(); //$NON-NLS-1$
+	}
+
+	@Override
+	public float getXPrecision() {
+		return this.motionEvent.getXPrecision();
+	}
+
+	@Override
+	public float getYPrecision() {
+		return this.motionEvent.getYPrecision();
+	}
+
+	@Override
+	public int getPointerCount() {
+		return this.motionEvent.getPointerCount();
+	}
+
+	@Override
+	public Shape2f getToolArea(int pointerIndex) {
+		float x = this.motionEvent.getX(pointerIndex);
+		float y = this.motionEvent.getY(pointerIndex);
+		float major = this.motionEvent.getToolMajor(pointerIndex);
+		float minor = this.motionEvent.getToolMinor(pointerIndex);
+		// The circle has a minimal radius of 1 pixel.
+		return new Circle2f(x, y,
+				Math.max(
+						Math.max(major, minor),
+						MINIMAL_TOOL_SIZE));
+	}
+
+	@Override
+	public ToolType getToolType(int pointerIndex) {
+		switch(this.motionEvent.getToolType(pointerIndex)) {
+		case MotionEvent.TOOL_TYPE_FINGER:
+			return ToolType.FINGER;
+		case MotionEvent.TOOL_TYPE_STYLUS:
+			return ToolType.STYLUS;
+		case MotionEvent.TOOL_TYPE_MOUSE:
+			return ToolType.MOUSE;
+		case MotionEvent.TOOL_TYPE_ERASER:
+			return ToolType.ERASER;
+		default:
+		}
+		return ToolType.UNKNOW;
+	}
+
+	@Override
+	public boolean isToolAreaSupported() {
+		return true;
+	}
+	
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/AsyncFileLoader.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/AsyncFileLoader.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/AsyncFileLoader.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,182 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.os.FileObserver;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * Asynchronous loader of a list of files.
+ * This loader is watching the file system content
+ * to update the list of the files when this list
+ * was changed by the action of an other application.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+class AsyncFileLoader extends AsyncTaskLoader<List<File>> {
+
+	private final File path;
+	private final FileFilter filter;
+	
+	private FileObserver fileObserver;
+
+	private List<File> bufferedList = null;
+
+	/**
+	 * @param context is the execution context of the task.
+	 * @param path is the path of the directory to explore.
+	 * @param fileFilter is the filter to use.
+	 */
+	public AsyncFileLoader(Context context, File path, FileFilter fileFilter) {
+		super(context);
+		this.path = path;
+		this.filter = fileFilter;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<File> loadInBackground() {
+		File[] array;
+		if (this.filter!=null) {
+			array = this.path.listFiles(this.filter);
+		}
+		else {
+			array = this.path.listFiles();
+		}
+		List<File> list = new ArrayList<File>(array.length);
+		// Dichotomic insertion
+		int f, l, c;
+		File cf;
+		for(File file : array) {
+			if (!file.isHidden() && !file.getName().equalsIgnoreCase("lost.dir")) { //$NON-NLS-1$
+				f = 0;
+				l = list.size()-1;
+				while (f<=l) {
+					c = (f+l)/2;
+					cf = array[c];
+					if (file.compareTo(cf)<=0) { 
+						l = c-1;
+					}
+					else {
+						f = c+1;
+					}
+				}
+				list.add(f,file);
+			}
+		}
+		return list;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onStartLoading() {
+		// A list is already available. Publish it.
+		if (this.bufferedList != null) deliverResult(this.bufferedList);
+
+		if (this.fileObserver == null) {
+			this.fileObserver = new FileObserver(this.path.getAbsolutePath(), 
+					FileObserver.CREATE
+					| FileObserver.DELETE | FileObserver.DELETE_SELF
+					| FileObserver.MOVED_FROM | FileObserver.MOVED_TO
+					| FileObserver.MODIFY | FileObserver.MOVE_SELF) {
+				@Override
+				public void onEvent(int event, String path) {
+					onContentChanged();	
+				}
+			};
+		}
+		this.fileObserver.startWatching();
+
+		if (takeContentChanged() || this.bufferedList==null)
+			forceLoad();
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public void deliverResult(List<File> data) {
+		if (isReset()) {
+			unwatch();
+			return;
+		}
+
+		List<File> old = this.bufferedList;
+		this.bufferedList = data;
+
+		if (isStarted())
+			super.deliverResult(this.bufferedList);
+
+		if (old!=null && old!=data)
+			unwatch();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onStopLoading() {
+		cancelLoad();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onReset() {
+		onStopLoading();
+
+		if (this.bufferedList!=null) {
+			unwatch();
+			this.bufferedList = null;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void onCanceled(List<File> data) {
+		super.onCanceled(data);
+		unwatch();
+	}
+
+	/** Unwatch the state for the given files.
+	 */
+	protected void unwatch() {
+		if (this.fileObserver != null) {
+			this.fileObserver.stopWatching();
+			this.fileObserver = null;
+		}
+	}
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooser.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooser.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooser.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,305 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+import java.io.FileFilter;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+
+
+/**
+ * Utilities about file choosers.  
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class FileChooser {
+
+	/** Show the file chooser for opening a file.
+	 * <p>
+	 * The keys supported by the file chooser are:<ul>
+	 * <li><code>path</code>: the directory path to open at start-up.</li>
+	 * <li><code>fileFilter</code>: the classname of the file filter to use.</li>
+	 * </ul>
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param mimeType is the MIME type of the files to select.
+	 * @param options are the options to pass to the activity.
+	 */
+	public static void showOpenChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			String mimeType,
+			Bundle options) {
+		// Implicitly allow the user to select a particular kind of data
+		Intent target = new Intent(Intent.ACTION_GET_CONTENT); 
+		// The MIME data type filter
+		target.setType(mimeType);
+		// Only return URIs that can be opened with ContentResolver
+		target.addCategory(Intent.CATEGORY_OPENABLE);
+		// Create the chooser Intent
+		Intent intent = Intent.createChooser(
+				target, context.getString(chooserTitle));
+		// Set the extra data to pass to the activity.
+		Bundle opts;
+		if (options!=null) {
+			opts = new Bundle(options);
+		}
+		else {
+			opts = new Bundle();
+		}
+		opts.putBoolean(FileChooserActivity.SAVED_IS_OPEN, true);
+		intent.putExtra(FileChooserActivity.ACTIVITY_OPTIONS, opts);
+		Parcelable innerIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+		if (innerIntent instanceof Intent)
+			((Intent)innerIntent).putExtra(FileChooserActivity.ACTIVITY_OPTIONS, opts);
+		try {
+			context.startActivityForResult(intent, activityResultRequestCode);
+		}
+		catch (ActivityNotFoundException e) {
+			Log.e("FILE_CHOOSER", e.getLocalizedMessage(), e); //$NON-NLS-1$
+		}	
+	}
+
+	/** Show the file chooser for opening a file.
+	 * <p>
+	 * The keys supported by the file chooser are:<ul>
+	 * <li><code>path</code>: the directory path to open at start-up.</li>
+	 * <li><code>fileFilter</code>: the classname of the file filter to use.</li>
+	 * </ul>
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param options are the options to pass to the activity.
+	 */
+	public static void showOpenChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			Bundle options) {
+		showOpenChooser(context, activityResultRequestCode, chooserTitle, "file/*", options); //$NON-NLS-1$
+	}
+
+	/** Show the file chooser for opening a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param directory is the directory to explore.
+	 * @param fileFilter is the file filter to use.
+	 */
+	public static void showOpenChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			File directory,
+			Class<? extends FileFilter> fileFilter) {
+		showOpenChooser(context, activityResultRequestCode, chooserTitle,
+				createOptions(directory, fileFilter));
+	}
+	
+	/** Create options that may be passed to a chooser.
+	 * 
+	 * @param directory is the directory to explore.
+	 * @param fileFilter is the file filter to use.
+	 * @return the options.
+	 */
+	public static Bundle createOptions(
+			File directory,
+			Class<? extends FileFilter> fileFilter) {
+		Bundle bundle = new Bundle();
+		if (directory!=null) {
+			bundle.putString(FileChooserActivity.SAVED_PATH_NAME, directory.getAbsolutePath());
+		}
+		if (fileFilter!=null) {
+			bundle.putString(FileChooserActivity.SAVED_FILE_FILTER, fileFilter.getName());
+		}
+		return bundle;
+	}
+
+	/** Show the file chooser for opening a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param directory is the directory to explore.
+	 * @param fileFilter is the file filter to use.
+	 */
+	public static void showOpenChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			File directory,
+			FileFilter fileFilter) {
+		showOpenChooser(context, activityResultRequestCode, chooserTitle,
+				createOptions(directory, fileFilter!=null ? fileFilter.getClass() : null));
+	}
+
+	/** Show the file chooser for opening a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 */
+	public static void showOpenChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle) {
+		showOpenChooser(context, activityResultRequestCode, chooserTitle, null);
+	}
+
+	/** Show the file chooser for saving a file.
+	 * <p>
+	 * The keys supported by the file chooser are:<ul>
+	 * <li><code>path</code>: the file path to save at start-up.</li>
+	 * <li><code>fileFilter</code>: the classname of the file filter to use.</li>
+	 * </ul>
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param mimeType is the mime type of the files to select.
+	 * @param options are the options to pass to the activity.
+	 */
+	public static void showSaveChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			String mimeType,
+			Bundle options) {
+		// Implicitly allow the user to select a particular kind of data
+		Intent target = new Intent(Intent.ACTION_GET_CONTENT); 
+		// The MIME data type filter
+		target.setType(mimeType);
+		// Only return URIs that can be opened with ContentResolver
+		target.addCategory(Intent.CATEGORY_OPENABLE);
+		// Create the chooser Intent
+		Intent intent = Intent.createChooser(
+				target, context.getString(chooserTitle));
+		// Set the extra data to pass to the activity.
+		Bundle opts;
+		if (options!=null) {
+			opts = new Bundle(options);
+		}
+		else {
+			opts = new Bundle();
+		}
+		opts.putBoolean(FileChooserActivity.SAVED_IS_OPEN, false);
+		intent.putExtra(FileChooserActivity.ACTIVITY_OPTIONS, opts);
+		Parcelable innerIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+		if (innerIntent instanceof Intent)
+			((Intent)innerIntent).putExtra(FileChooserActivity.ACTIVITY_OPTIONS, opts);
+		try {
+			context.startActivityForResult(intent, activityResultRequestCode);
+		}
+		catch (ActivityNotFoundException e) {
+			Log.e("FILE_CHOOSER", e.getLocalizedMessage(), e); //$NON-NLS-1$
+		}	
+	}
+
+	/** Show the file chooser for saving a file.
+	 * <p>
+	 * The keys supported by the file chooser are:<ul>
+	 * <li><code>path</code>: the file path to save at start-up.</li>
+	 * <li><code>fileFilter</code>: the classname of the file filter to use.</li>
+	 * </ul>
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param options are the options to pass to the activity.
+	 */
+	public static void showSaveChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			Bundle options) {
+		showSaveChooser(context, activityResultRequestCode, chooserTitle, "file/*", options); //$NON-NLS-1$
+	}
+
+	/** Show the file chooser for saving a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param file is the filename to use for the saving file.
+	 * @param fileFilter is the file filter to use.
+	 */
+	public static void showSaveChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			File file,
+			Class<? extends FileFilter> fileFilter) {
+		showSaveChooser(context, activityResultRequestCode, chooserTitle,
+				createOptions(file, fileFilter));
+	}
+
+	/** Show the file chooser for saving a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 * @param file is the filename to use for the saving file.
+	 * @param fileFilter is the file filter to use.
+	 */
+	public static void showSaveChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle,
+			File file,
+			FileFilter fileFilter) {
+		showSaveChooser(context, activityResultRequestCode, chooserTitle,
+				createOptions(file, fileFilter!=null ? fileFilter.getClass() : null));
+	}
+
+	/** Show the file chooser for saving a file.
+	 * <p>
+	 * Mime type is "file/*".
+	 * 
+	 * @param context is the execution context.
+	 * @param activityResultRequestCode is the code that may be used to retreive the result of the activity.
+	 * @param chooserTitle is the identifier for the chooser's title.
+	 */
+	public static void showSaveChooser(Activity context, 
+			int activityResultRequestCode, 
+			int chooserTitle) {
+		showSaveChooser(context, activityResultRequestCode, chooserTitle, null);
+	}
+
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserActivity.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserActivity.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserActivity.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,517 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+import java.io.FileFilter;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.vmutil.FileSystem;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.BackStackEntry;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v4.app.FragmentTransaction;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+/**
+ * File chooser embedded inside an activity fragment. 
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class FileChooserActivity extends FragmentActivity implements OnBackStackChangedListener {
+
+	/** Name of the extra data that is containing the options of the file chooser.
+	 */
+	static final String ACTIVITY_OPTIONS = "fileChooserActivityOptions"; //$NON-NLS-1$
+
+	/** Name of the attribute that permits to save the path in the activity.
+	 */
+	public static final String SAVED_PATH_NAME = "path"; //$NON-NLS-1$
+
+	/** Name of the attribute that permits to save the file filter in the activity.
+	 */
+	public static final String SAVED_FILE_FILTER = "fileFilter"; //$NON-NLS-1$
+
+	/** Name of the attribute that permits to indicates if the chooser is for opening a file.
+	 */
+	public static final String SAVED_IS_OPEN = "isOpen"; //$NON-NLS-1$
+
+	/** Name of the preference that permits to store the path.
+	 */
+	private static final String PREFERENCE_FILE_PATH = "lastSelectedPath"; //$NON-NLS-1$
+
+	private static File getFileParameter(String name, File defaultValue, Bundle... bundles) {
+		for(Bundle b : bundles) {
+			if (b!=null) {
+				String absPath = b.getString(name);
+				if (absPath!=null) return new File(absPath);
+			}
+		}
+		return defaultValue;
+	}
+
+	private static FileFilter getFileFilterParameter(String name, FileFilter defaultValue, Bundle... bundles) {
+		for(Bundle b : bundles) {
+			if (b!=null) {
+				String classname = b.getString(name);
+				if (classname!=null) {
+					try {
+						Class<?> type = Class.forName(classname);
+						if (FileFilter.class.isAssignableFrom(type)) {
+							return (FileFilter)type.newInstance();
+						}
+					}
+					catch(Throwable e) {
+						Log.d("FILE_CHOOSER", e.getLocalizedMessage(), e); //$NON-NLS-1$
+					}
+				}
+			}
+		}
+		return defaultValue;
+	}
+
+	/** Listener on the external storage state.
+	 */
+	private BroadcastReceiver storageListener = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			onValidatedFile(null);
+		}
+	};
+
+	/** Current selected file.
+	 */
+	private File path = null;
+
+	/** File filter.
+	 */
+	private FileFilter fileFilter = null;
+
+	/** Icon selector.
+	 */
+	private FileChooserIconSelector iconSelector = null;
+
+	/** Manager of fragments.
+	 */
+	private FragmentManager fragmentManager = null;
+
+	/** Indicates if the file chooser is for opening (<code>true</code>)
+	 * or saving (<code>false</code>) a file.
+	 */
+	private boolean isOpen = true;
+	
+	/** Reference to the save menu item.
+	 */
+	private MenuItem saveItem = null;
+
+	/** Reference to the up-directory menu item.
+	 */
+	private MenuItem upDirectoryItem = null;
+	
+	/** Options given to the activity.
+	 */
+	private Bundle activityOptions = null;
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		Intent intent = getIntent();
+		this.activityOptions = intent.getBundleExtra(ACTIVITY_OPTIONS);
+
+		this.path = getFileParameter(
+				SAVED_PATH_NAME,
+				null, 
+				this.activityOptions, savedInstanceState);
+		
+		if (this.path==null) {
+			SharedPreferences preferences = getPreferences(DEFAULT_KEYS_SEARCH_LOCAL);
+			String filePath = preferences.getString(PREFERENCE_FILE_PATH, null);
+			if (filePath!=null && !filePath.isEmpty()) {
+				this.path = new File(filePath);
+				if (!this.path.isDirectory()) {
+					this.path = this.path.getParentFile();
+				}
+			}
+			if (this.path==null) {
+				this.path = Environment.getExternalStorageDirectory();
+			}
+		}
+		
+		this.fileFilter = getFileFilterParameter(
+				SAVED_FILE_FILTER,
+				null,
+				this.activityOptions, savedInstanceState);
+
+		if (this.activityOptions!=null) {
+			this.isOpen = this.activityOptions.getBoolean(SAVED_IS_OPEN,
+					savedInstanceState!=null ?
+					savedInstanceState.getBoolean(SAVED_IS_OPEN, true) :
+					true);
+		}
+
+		String basename = null;
+		if (!this.isOpen && !this.path.isDirectory()) {
+			basename = this.path.getName();
+		}
+		
+		while (this.path!=null && !this.path.isDirectory()) {
+			this.path = this.path.getParentFile();
+		}
+
+		if (this.path==null) {
+			this.path = Environment.getExternalStorageDirectory();
+		}
+
+		if (this.isOpen) {
+			setContentView(R.layout.filechooser_open);
+		}
+		else {
+			setContentView(R.layout.filechooser_save);
+			EditText editor = (EditText)findViewById(R.id.fileChooserFilenameField);
+			if (basename!=null) editor.setText(basename);
+			editor.addTextChangedListener(new Listener());
+		}
+
+		this.fragmentManager = getSupportFragmentManager();
+		this.fragmentManager.addOnBackStackChangedListener(this);
+
+		setTitle(this.path.getAbsolutePath());
+
+		// Start to listen on directory changes.
+		FileListFragment explorerFragment = FileListFragment.newInstance(
+				this.path, this.fileFilter, this.iconSelector);
+		FragmentTransaction transaction = this.fragmentManager.beginTransaction();
+		transaction.add(
+				R.id.fileChooserExplorerFragment,
+				explorerFragment).commit();
+	}
+	
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		if (this.isOpen) {
+			getMenuInflater().inflate(R.menu.filechooser_open_menu, menu);
+		}
+		else {
+			getMenuInflater().inflate(R.menu.filechooser_save_menu, menu);
+			this.saveItem = menu.findItem(R.id.fileChooserSaveMenu);
+		}
+		this.upDirectoryItem = menu.findItem(R.id.fileChooserParentDirectoryMenu);
+		return true;
+	}
+	
+	@Override
+	public boolean onMenuItemSelected(int featureId, MenuItem item) {
+		if (item.getItemId()==R.id.fileChooserParentDirectoryMenu) {
+			onSelectedFile(this.path.getParentFile());
+			return true;
+		}
+		if (!this.isOpen && item.getItemId()==R.id.fileChooserSaveMenu) {
+			EditText editor = (EditText)findViewById(R.id.fileChooserFilenameField);
+			String basename = editor.getText().toString();
+			basename = FileSystem.basename(basename);
+			onValidatedFile(new File(this.path, basename));
+			return true;
+		}
+		return super.onMenuItemSelected(featureId, item);
+	}
+	
+	@Override
+	public boolean onPrepareOptionsMenu(Menu menu) {
+		updateUpDirectoryAction();
+		if (!this.isOpen) {
+			updateSaveAction();
+		}
+		return true;
+	}
+	
+	/** Update the state of the up-directory action.
+	 */
+	protected void updateUpDirectoryAction() {
+		if (this.upDirectoryItem!=null) {
+			this.upDirectoryItem.setEnabled(this.path.getParentFile()!=null);
+		}
+	}
+
+	/** Update the state of the save action.
+	 */
+	protected void updateSaveAction() {
+		if (this.saveItem!=null) {
+			EditText editor = (EditText)findViewById(R.id.fileChooserFilenameField);
+			String text = editor.getText().toString();
+			this.saveItem.setEnabled(this.path.isDirectory() && !text.isEmpty());
+		}
+	}
+
+	/** Set the file filter to use in the chooser.
+	 * 
+	 * @param fileFilter
+	 */
+	public void setFileFilter(FileFilter fileFilter) {
+		this.fileFilter = fileFilter;
+	}
+
+	/** Replies the file filter to use in the chooser.
+	 * 
+	 * @return the file filter.
+	 */
+	public FileFilter getFileFilter() {
+		return this.fileFilter;
+	}
+
+	/** Set the selector of icon
+	 * 
+	 * @param iconSelector
+	 */
+	public void setIconSelector(FileChooserIconSelector iconSelector) {
+		this.iconSelector = iconSelector;
+	}
+
+	/** Replies the selector of icon.
+	 * 
+	 * @return the icon selector.
+	 */
+	public FileChooserIconSelector getIconSelector() {
+		return this.iconSelector;
+	}
+
+	/** Invoked when the SD card state has changed.
+	 */
+	@Override
+	public void onBackStackChanged() {
+		this.path = Environment.getExternalStorageDirectory();
+
+		int count = this.fragmentManager.getBackStackEntryCount();
+		if (count > 0) {
+			BackStackEntry fragment = this.fragmentManager.getBackStackEntryAt(count - 1);
+			String path = fragment.getName();
+			if (path!=null)
+				this.path = new File(path);
+		}
+
+		setTitle(this.path.getAbsolutePath());
+		invalidateOptionsMenu();
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	protected void onPause() {
+		super.onPause();
+		unregisterStorageListener();
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	protected void onResume() {
+		super.onResume();
+		registerStorageListener();
+	}
+	
+	/** {@inheritDoc}
+	 */
+	@Override
+	protected void onSaveInstanceState(Bundle outState) {
+		if (this.path!=null) {
+			outState.putString(SAVED_PATH_NAME, this.path.getAbsolutePath());
+		}
+		else {
+			outState.remove(SAVED_PATH_NAME);
+		}
+		if (this.fileFilter!=null) {
+			outState.putString(SAVED_FILE_FILTER, this.fileFilter.getClass().getName());
+		}
+		else {
+			outState.remove(SAVED_FILE_FILTER);
+		}
+		outState.putBoolean(SAVED_IS_OPEN, this.isOpen);
+		super.onSaveInstanceState(outState);
+	}
+	
+	@Override
+	protected void onRestoreInstanceState(Bundle savedInstanceState) {
+		super.onRestoreInstanceState(savedInstanceState);
+		this.isOpen = savedInstanceState.getBoolean(SAVED_IS_OPEN);
+		String fileFilterClassname = savedInstanceState.getString(SAVED_FILE_FILTER);
+		if (fileFilterClassname!=null) {
+			try {
+				Class<?> type = Class.forName(fileFilterClassname);
+				if (FileFilter.class.isAssignableFrom(type)) {
+					this.fileFilter = (FileFilter)type.newInstance();
+				}
+			}
+			catch(Throwable _) {
+				//
+			}
+		}
+		String path = savedInstanceState.getString(SAVED_PATH_NAME);
+		if (path!=null) {
+			this.path = new File(path);
+		}
+	}
+
+	/**
+	 * Invoked when the activity is finished with a selected file.
+	 * 
+	 * @param file is the selected file.
+	 */
+	protected void onValidatedFile(File file) {
+		// Force to close the soft keyboard
+		if (!this.isOpen) {
+			InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+            imm.hideSoftInputFromWindow(getWindow().getCurrentFocus().getWindowToken(), 0);
+		}
+		File validatedFile = file;
+		if (validatedFile!=null) {
+			if (!this.isOpen && this.fileFilter instanceof org.arakhne.afc.io.filefilter.FileFilter) {
+				if (!this.fileFilter.accept(validatedFile)) {
+					String ext = ((org.arakhne.afc.io.filefilter.FileFilter)this.fileFilter).getExtensions()[0];
+					validatedFile = FileSystem.addExtension(validatedFile, ext);
+				}
+			}
+
+			if (this.path!=null) {
+				File preferencePath = this.path;
+				while(preferencePath!=null && !preferencePath.isDirectory())
+					preferencePath = preferencePath.getParentFile();
+				if (preferencePath!=null) {
+					SharedPreferences preferences = getPreferences(DEFAULT_KEYS_SEARCH_LOCAL);
+					Editor preferenceEditor = preferences.edit();
+					preferenceEditor.putString(PREFERENCE_FILE_PATH, preferencePath.getAbsolutePath());
+					preferenceEditor.commit();
+					preferenceEditor.apply();
+				}
+			}
+			
+			Intent data = new Intent();
+			data.setData(Uri.fromFile(validatedFile));
+			
+			if (this.activityOptions!=null) {
+				data.putExtras(this.activityOptions);
+			}
+			
+			setResult(RESULT_OK, data);
+			finish();
+		}
+		else {
+			setResult(RESULT_CANCELED);	
+			finish();
+		}
+	}
+
+
+	/**
+	 * Called when the user selects a File.
+	 * 
+	 * @param file is the selected file.
+	 */
+	protected void onSelectedFile(File file) {
+		if (file!=null) {
+			this.path = file;
+
+			if (file.isDirectory()) {
+				FileListFragment explorerFragment = FileListFragment.newInstance(this.path, this.fileFilter, this.iconSelector);
+				FragmentTransaction transaction = this.fragmentManager.beginTransaction();
+				transaction.replace(
+						R.id.fileChooserExplorerFragment,
+						explorerFragment)
+						.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+						.addToBackStack(this.path.getAbsolutePath()).commit();
+			}
+			else {
+				onValidatedFile(file);	
+			}
+		}
+	}
+
+	/**
+	 * Register the external storage BroadcastReceiver.
+	 */
+	private void registerStorageListener() {
+		IntentFilter filter = new IntentFilter();
+		filter.addAction(Intent.ACTION_MEDIA_REMOVED);
+		registerReceiver(this.storageListener, filter);
+	}
+
+	/**
+	 * Unregister the external storage BroadcastReceiver.
+	 */
+	private void unregisterStorageListener() {
+		unregisterReceiver(this.storageListener);
+	}
+	
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private class Listener implements TextWatcher {
+		
+		/**
+		 */
+		public Listener() {
+			//
+		}
+		
+		@Override
+		public void onTextChanged(CharSequence s, int start, int before, int count) {
+			//
+		}
+		
+		@Override
+		public void beforeTextChanged(CharSequence s, int start, int count,
+				int after) {
+			//
+		}
+		
+		@Override
+		public void afterTextChanged(Editable s) {
+			updateSaveAction();
+		}
+		
+	} // class Listener
+
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserIconSelector.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserIconSelector.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileChooserIconSelector.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,43 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+
+/**
+ * This selector permits to select an icon for a file in a file chooser.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public interface FileChooserIconSelector {
+
+	/** Replies the preferred icon for the given file.
+	 * 
+	 * @param file is the file for which the icon must be determined.
+	 * @param defaultIcon is the default icon to use.
+	 * @return the icon.
+	 */
+	public int selectIconFor(File file, int defaultIcon);
+
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListAdapter.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListAdapter.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListAdapter.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,162 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.arakhne.afc.ui.android.R;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * This class provides a drawing adapter for the elements
+ * of the list in a file chooser.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+class FileListAdapter extends BaseAdapter {
+
+	private List<File> files = new ArrayList<File>();
+
+	private final LayoutInflater layoutInflater;
+	private final FileChooserIconSelector iconSelector;
+
+	/**
+	 * @param context is the drawing context.
+	 * @param iconSelector is the selector of icon to use.
+	 */
+	public FileListAdapter(Context context, FileChooserIconSelector iconSelector) {
+		this.layoutInflater = LayoutInflater.from(context);
+		this.iconSelector = iconSelector;
+	}
+	
+	/** Set the list of files.
+	 * 
+	 * @param list
+	 */
+	public void set(List<File> list) {
+		this.files = list;
+		notifyDataSetChanged();
+	}
+	
+	/** Clear the list of files to display.
+	 */
+	public void clear() {
+		this.files.clear();
+		notifyDataSetChanged();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getCount() {
+		return this.files.size();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public File getItem(int position) {
+		return this.files.get(position);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public long getItemId(int position) {
+		return position;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		View row = convertView;
+		ViewHolder holder = null;
+
+		if (row == null) {
+			row = this.layoutInflater.inflate(R.layout.filechooser_onefile, parent, false);
+			holder = new ViewHolder(row);
+			row.setTag(holder);
+		} else {
+			// Reduce, reuse, recycle!
+			holder = (ViewHolder) row.getTag();
+		}
+
+		// Get the file at the current position
+		File file = getItem(position);
+
+		// Set the TextView as the file name
+		holder.nameView.setText(file.getName());
+
+		// If the item is not a directory, use the file icon
+		int icon;
+		if (file.isDirectory()) {
+			icon = R.drawable.ic_folder;
+		}
+		else {
+			icon = R.drawable.ic_file;
+			if (this.iconSelector!=null) {
+				icon = this.iconSelector.selectIconFor(file, icon);
+			}
+		}
+		
+		holder.iconView.setImageResource(icon);
+
+		return row;
+	}
+
+	/**
+	 * This class provides a drawing adapter for the elements
+	 * of the list in a file chooser.
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private static class ViewHolder {
+		
+		public final TextView nameView;
+		public final ImageView iconView;
+
+		public ViewHolder(View row) {
+			this.nameView = (TextView) row.findViewById(R.id.fileChooserFileName);
+			this.iconView = (ImageView) row.findViewById(R.id.fileChooserFileIcon);
+		}
+		
+	}
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListFragment.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListFragment.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/filechooser/FileListFragment.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,167 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.filechooser;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.List;
+
+import org.arakhne.afc.ui.android.R;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * Fragment that displays a list of Files in a given path.
+ * <p>
+ * The path to open at startup should be stored in the
+ * argument with the name "path".
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class FileListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<List<File>> {
+
+	private FileFilter fileFilter = null;
+	private FileChooserIconSelector iconSelector = null;
+	private FileListAdapter adapter = null;
+	private File path;
+
+	/**
+	 * @param path is the he absolute path of the file (directory) to display.
+	 * @param fileFilter is the file filter to use.
+	 * @param iconSelector is the selector of icon.
+	 * @return the new instance.
+	 */
+	public static FileListFragment newInstance(File path, FileFilter fileFilter, FileChooserIconSelector iconSelector) {
+		FileListFragment fragment = new FileListFragment();
+		Bundle args = new Bundle();
+		if (path!=null) {
+			args.putString(FileChooserActivity.SAVED_PATH_NAME, path.getAbsolutePath());
+		}
+		fragment.setArguments(args);
+		fragment.setFileFilter(fileFilter);
+		fragment.setIconSelector(iconSelector);
+		return fragment;
+	}
+
+	/**
+	 */
+	public FileListFragment() {
+		//
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		this.adapter = new FileListAdapter(getActivity(), this.iconSelector);
+		this.path = Environment.getExternalStorageDirectory();
+
+		Bundle args = getArguments();
+		if (args!=null) {
+			String absPath = args.getString(FileChooserActivity.SAVED_PATH_NAME);
+			if (absPath!=null) {
+				this.path = new File(absPath);
+			}
+		}
+	}
+
+	/**
+	 * Set the filte filter to use.
+	 * 
+	 * @param fileFilter
+	 */
+	public void setFileFilter(FileFilter fileFilter) {
+		this.fileFilter = fileFilter;
+	}
+
+	/**
+	 * Set the icon selector to use.
+	 * 
+	 * @param iconSelector
+	 */
+	public void setIconSelector(FileChooserIconSelector iconSelector) {
+		this.iconSelector = iconSelector;
+	}
+
+	@Override
+	public void onActivityCreated(Bundle savedInstanceState) {
+		super.onActivityCreated(savedInstanceState);
+
+		setEmptyText(getString(R.string.empty_directory));
+		setListAdapter(this.adapter);
+		setListShown(false);
+
+		getLoaderManager().initLoader(0, null, this);
+	}
+
+	@Override
+	public void onListItemClick(ListView l, View v, int position, long id) {
+		FileListAdapter adapter = (FileListAdapter) l.getAdapter();
+		if (adapter != null) {
+			this.path = adapter.getItem(position);
+			Activity activity = getActivity();
+			if (activity instanceof FileChooserActivity) {
+				((FileChooserActivity)getActivity()).onSelectedFile(this.path);				
+			}
+
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Loader<List<File>> onCreateLoader(int id, Bundle args) {
+		return new AsyncFileLoader(getActivity(), this.path, this.fileFilter);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void onLoadFinished(Loader<List<File>> loader, List<File> data) {
+		this.adapter.set(data);
+		if (isResumed()) {
+			setListShown(true);
+		}
+		else {
+			setListShownNoAnimation(true);
+		}
+	}
+
+	@Override
+	public void onLoaderReset(Loader<List<File>> loader) {
+		this.adapter.clear();
+	}
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/progress/ProgressDialogProgressionListener.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/progress/ProgressDialogProgressionListener.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/progress/ProgressDialogProgressionListener.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,109 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.progress;
+
+import org.arakhne.afc.progress.ProgressionEvent;
+import org.arakhne.afc.progress.ProgressionListener;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+
+/**
+ * Task progression listener that is linked to a ProgressDialog.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class ProgressDialogProgressionListener implements ProgressionListener {
+
+	private final Activity context;
+	private final ProgressDialog dialog;
+	private final String initialMessage;
+	private String currentMessage;
+
+	/**
+	 * @param context
+	 * @param dialog
+	 * @param initialMessage
+	 */
+	public ProgressDialogProgressionListener(Activity context, ProgressDialog dialog, String initialMessage) {
+		this.context = context;
+		this.dialog = dialog;
+		this.currentMessage = this.initialMessage = initialMessage;
+	}
+
+	@Override
+	public void onProgressionValueChanged(ProgressionEvent event) {
+		int v;
+		v = event.getMaximum() - event.getMinimum();
+		if (this.dialog.getMax()!=v) this.dialog.setMax(v);
+		v = event.getValue() - event.getMinimum();
+		if (this.dialog.getProgress()!=v) this.dialog.setProgress(v);
+		updateComment(event.getComment());
+	}
+
+	@Override
+	public void onProgressionStateChanged(ProgressionEvent event) {
+		this.dialog.setIndeterminate(event.isIndeterminate());
+		updateComment(event.getComment());
+	}
+	
+	private void updateComment(String newComment) {
+		String rComment = newComment;
+		if (rComment==null) rComment = this.initialMessage;
+		if (!rComment.equals(this.currentMessage)) {
+			this.currentMessage = rComment;
+			this.context.runOnUiThread(new CommentUpdater(this.dialog, rComment));
+		}
+	}
+	
+	/**
+	 * Task progression listener that is linked to a ProgressDialog.
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private static class CommentUpdater implements Runnable {
+
+		private final String comment;
+		private final ProgressDialog dialog;
+
+		/**
+		 * @param dialog
+		 * @param comment
+		 */
+		public CommentUpdater(ProgressDialog dialog, String comment) {
+			this.dialog = dialog;
+			this.comment = comment;
+		}
+
+		@Override
+		public void run() {
+			this.dialog.setMessage(this.comment);
+		}
+		
+	} // class CommentUpdater
+	
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditorView.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditorView.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditorView.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,136 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.property;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.arakhne.afc.util.PropertyOwner;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/** Abstract implementation of a property editor inside a view. 
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public abstract class PropertyEditorView extends LinearLayout {
+
+	private Collection<? extends PropertyOwner> editedObjects = null;
+	private boolean isEditable = true;
+	
+	/**
+	 * @param context
+	 */
+	protected PropertyEditorView(Context context) {
+		super(context);
+		View top = onCreateTopContentView();
+		if (top!=null) {
+			addView(top);
+		}
+		if (this.editedObjects!=null) {
+			onCreateFields(this.editedObjects);
+		}
+		LayoutParams layoutParams = new LinearLayout.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT);
+		setLayoutParams(layoutParams);
+	}
+
+	/** Replies if the fragment enables to edit the properties.
+	 * 
+	 * @return <code>true</code> if the properties are editable.
+	 */
+	public boolean isEditable() {
+		return this.isEditable;
+	}
+
+	/** Set if the fragment enables to edit the properties.
+	 * 
+	 * @param editable is <code>true</code> if the properties are editable.
+	 */
+	public void setEditable(boolean editable) {
+		this.isEditable = editable;
+	}
+
+	/** Replies the edited objects.
+	 * 
+	 * @return the edited objects.
+	 */
+	public Collection<? extends PropertyOwner> getEditedObjects() {
+		return this.editedObjects;
+	}
+
+	/** Change the edited object.
+	 * 
+	 * @param editedObjects
+	 */
+	public void setEditedObjects(Collection<? extends PropertyOwner> editedObjects) {
+		if (this.editedObjects!=editedObjects) {
+			if (this.editedObjects!=null) {
+				onResetContentView();
+			}
+			this.editedObjects = editedObjects;
+			if (this.editedObjects!=null) {
+				onCreateFields(this.editedObjects);
+			}
+		}
+	}
+
+	/** Invoked when the content view must be cleared.
+	 */
+	protected abstract void onResetContentView();
+
+	/** Invoked when the top-most (the root) content view must be initiated.
+	 * @return the top view.
+	 */
+	protected abstract View onCreateTopContentView();
+
+	/** Invoked to create the fields of the property editor.
+	 * 
+	 * @param editedObject is the edited object.
+	 */
+	protected abstract void onCreateFields(Collection<? extends PropertyOwner> editedObject);
+
+	/** Replies the properties in the fragment.
+	 * 
+	 * @return the properties.
+	 */
+	public abstract Map<String,Object> getEditedProperties();
+	
+	/** Invoked by the API when the properties of an owner must be set.
+	 * <p>
+	 * This function shuold be overidden by subclasses to enable specific
+	 * settings.
+	 * 
+	 * @param owner is the object to change.
+	 * @param properties are the properties to set.
+	 */
+	@SuppressWarnings("static-method")
+	public void setPropertiesOf(PropertyOwner owner, Map<String,Object> properties) {
+		owner.setProperties(properties);
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditors.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditors.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/PropertyEditors.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,245 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.property;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.afc.ui.undo.AbstractUndoable;
+import org.arakhne.afc.ui.undo.UndoManager;
+import org.arakhne.afc.util.Pair;
+import org.arakhne.afc.util.PropertyOwner;
+import org.arakhne.util.ref.SoftValueHashMap;
+import org.arakhne.vmutil.ReflectionUtil;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ScrollView;
+
+/** Editors of properties. 
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class PropertyEditors {
+
+	/** Stored the mapping between type of object and type of property editor.
+	 */
+	private static final Map<Class<? extends PropertyOwner>,Class<? extends PropertyEditorView>> mapping = new HashMap<Class<? extends PropertyOwner>,Class<? extends PropertyEditorView>>();
+
+	/** Use to accelerate the queries.
+	 */
+	private static final Map<Class<? extends PropertyOwner>,Class<? extends PropertyEditorView>> buffer = new SoftValueHashMap<Class<? extends PropertyOwner>, Class<? extends PropertyEditorView>>();
+
+
+	/** Display the dialog that permits to edit the properties.
+	 * 
+	 * @param context
+	 * @param isEditable indicates if the dialog box allows editions.
+	 * @param undoManager is the undo manager to use.
+	 * @param editedObjects are the edited objects.
+	 */
+	@SuppressWarnings("unchecked")
+	public static void showDialog(final Context context, final boolean isEditable, final UndoManager undoManager, final Collection<? extends PropertyOwner> editedObjects) {
+		//Detect the common type of the given objects
+		Class<? extends PropertyOwner> type = null;
+		if (!editedObjects.isEmpty()) {
+			for(PropertyOwner obj : editedObjects) {
+				if (type==null) {
+					type = obj.getClass();
+				}
+				else {
+					type = (Class<? extends PropertyOwner>)ReflectionUtil.getCommonType(type, obj.getClass());
+				}
+			}
+		}
+
+		// Create the views
+		ScrollView scrollView = new ScrollView(context);
+		LayoutParams layoutParams = new LinearLayout.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT);
+		scrollView.setLayoutParams(layoutParams);
+
+		LinearLayout linearLayout = new LinearLayout(context);
+		linearLayout.setVerticalScrollBarEnabled(true);
+		linearLayout.setHorizontalScrollBarEnabled(false);
+		linearLayout.setScrollbarFadingEnabled(true);
+		layoutParams = new LinearLayout.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT);
+		linearLayout.setLayoutParams(layoutParams);
+		scrollView.addView(linearLayout);
+
+		final PropertyEditorView fragment = PropertyEditors.createFragmentFor(context, type);
+		fragment.setId(123456);
+		fragment.setEditable(isEditable);
+		fragment.setEditedObjects(editedObjects);
+		linearLayout.addView(fragment);
+
+
+		// Initialize the alert dialog builder
+		AlertDialog.Builder builder = new AlertDialog.Builder(context);
+		builder.setTitle(R.string.property_dialog_title);
+		builder.setPositiveButton(
+				android.R.string.ok,
+				new OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						if (isEditable) {
+							Undo undo = new Undo(context.getString(R.string.undo_property_edition), editedObjects, fragment);
+							undo.doEdit();
+							if (undoManager!=null) {
+								undoManager.add(undo);
+							}
+						}
+					}
+				});
+		builder.setNegativeButton(android.R.string.cancel, new OnClickListener() {
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				dialog.dismiss();
+			}
+		});
+		builder.setOnCancelListener(new OnCancelListener() {
+			@Override
+			public void onCancel(DialogInterface dialog) {
+				dialog.dismiss();
+			}
+		});
+		builder.setView(scrollView);
+
+		// Create and show the dialog
+		AlertDialog alert = builder.create();
+		alert.show();
+	}
+
+	/** Register a property editor fragment.
+	 * 
+	 * @param fragmentType is the type of the fragment to create for editing the properties of an object of the given type.
+	 * @param objectType is the type of the object to edit.
+	 */
+	public static void registerEditorFragment(Class<? extends PropertyEditorView> fragmentType, Class<? extends PropertyOwner> objectType) {
+		mapping.put(objectType, fragmentType);
+	}
+
+	/** Create a fragment that permits to edit the properties for
+	 * the given object.
+	 * 
+	 * @param context
+	 * @param editedObjectType
+	 * @return a fragment or <code>null</code> if there is no known fragment.
+	 */
+	static PropertyEditorView createFragmentFor(Context context, Class<? extends PropertyOwner> editedObjectType) {
+		assert(editedObjectType!=null);
+
+		Class<?> objectType = editedObjectType;
+		Class<? extends PropertyEditorView> fragmentType = buffer.get(objectType);
+		while (fragmentType==null && objectType!=null) {
+			fragmentType = mapping.get(objectType);
+			if (fragmentType==null) {
+				Class<?>[] interfaces = objectType.getInterfaces();
+				int i=0;
+				while (fragmentType==null && i<interfaces.length) {
+					fragmentType = mapping.get(interfaces[i]);
+					++i;
+				}
+			}
+			objectType = objectType.getSuperclass();
+		}
+		if (fragmentType!=null) {
+			try {
+				Constructor<? extends PropertyEditorView> cons = fragmentType.getConstructor(Context.class);
+				PropertyEditorView fragment = cons.newInstance(context);
+				buffer.put(editedObjectType,fragmentType);
+				return fragment;
+			}
+			catch (Exception e) {
+				throw new RuntimeException(e);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private static class Undo extends AbstractUndoable {
+
+		private static final long serialVersionUID = -7759672095147438168L;
+
+		private final String label;
+		private final Collection<Pair<PropertyOwner,Map<String,Object>>> originalProperties = new ArrayList<Pair<PropertyOwner,Map<String,Object>>>();
+		private final PropertyEditorView fragment;
+		private final Map<String,Object> newProperties;
+
+		/**
+		 * @param label
+		 * @param editedObjects
+		 * @param fragment
+		 */
+		public Undo(String label, Collection<? extends PropertyOwner> editedObjects, PropertyEditorView fragment) {
+			this.label = label;
+			this.fragment = fragment;
+			this.newProperties = this.fragment.getEditedProperties();
+			for(PropertyOwner owner : editedObjects) {
+				this.originalProperties.add(new Pair<PropertyOwner,Map<String,Object>>(
+						owner,
+						owner.getProperties()));
+			}
+		}
+
+		@Override
+		public String getPresentationName() {
+			return this.label;
+		}
+
+		@Override
+		protected void doEdit() {
+			for(Pair<PropertyOwner,Map<String,Object>> object : this.originalProperties) {
+				this.fragment.setPropertiesOf(object.getA(), this.newProperties);
+			}
+		}
+
+		@Override
+		protected void undoEdit() {
+			for(Pair<PropertyOwner,Map<String,Object>> object : this.originalProperties) {
+				this.fragment.setPropertiesOf(object.getA(), object.getB());
+			}
+		}
+
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/TablePropertyEditorView.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/TablePropertyEditorView.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/property/TablePropertyEditorView.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,1043 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.property;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.afc.ui.android.button.ColorButton;
+import org.arakhne.afc.ui.vector.Color;
+import org.arakhne.afc.ui.vector.Colors;
+import org.arakhne.afc.ui.vector.VectorToolkit;
+
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.text.InputType;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+/** Abstract implementation of a property editor inside a fragment. 
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public abstract class TablePropertyEditorView extends PropertyEditorView {
+
+	/** Replies the field type that is supporting the given Java type.
+	 * 
+	 * @param type
+	 * @return the field type or <code>null</code>.
+	 */
+	protected static FieldType toFieldType(Class<?> type) {
+		if (String.class.isAssignableFrom(type)) {
+			return FieldType.STRING;
+		}
+		if (Long.class.isAssignableFrom(type)) {
+			return FieldType.INTEGER;
+		}
+		if (Integer.class.isAssignableFrom(type)) {
+			return FieldType.INTEGER;
+		}
+		if (Long.class.isAssignableFrom(type)) {
+			return FieldType.INTEGER;
+		}
+		if (Number.class.isAssignableFrom(type)) {
+			return FieldType.FLOAT;
+		}
+		if (Boolean.class.isAssignableFrom(type)) {
+			return FieldType.BOOLEAN;
+		}
+		if (Color.class.isAssignableFrom(type)) {
+			return FieldType.COLOR;
+		}
+		if (URL.class.isAssignableFrom(type)) {
+			return FieldType.URL;
+		}
+		if (URI.class.isAssignableFrom(type)) {
+			return FieldType.EMAIL;
+		}
+		if (Enum.class.isAssignableFrom(type)) {
+			return FieldType.COMBO;
+		}
+		return null;
+	}
+
+	/** Extract a color from a property object.
+	 * 
+	 * @param obj
+	 * @return the color
+	 */
+	public static Color toColor(Object obj) {
+		if (obj instanceof Color) {
+			return (Color)obj;
+		}
+		if (obj instanceof Number) {
+			return VectorToolkit.color(((Number)obj).intValue());
+		}
+		return VectorToolkit.color(obj);
+	}
+
+	private TableLayout tableLayout;
+	private final Map<String,FieldType> fieldTypes = new TreeMap<String,FieldType>();
+
+	/**
+	 * @param context
+	 */
+	protected TablePropertyEditorView(Context context) {
+		super(context);
+	}
+
+	@Override
+	protected void onResetContentView() {
+		if (this.tableLayout!=null) {
+			this.tableLayout.removeAllViews();
+		}
+	}
+
+	@Override
+	protected View onCreateTopContentView() {
+		this.tableLayout = new TableLayout(getContext());
+		this.tableLayout.setOrientation(LinearLayout.VERTICAL);
+		// Layout parameters
+		TableLayout.LayoutParams layoutParams = new TableLayout.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT);
+		this.tableLayout.setLayoutParams(layoutParams);
+		return this.tableLayout;
+	}
+
+	/** Replies the type of a field.
+	 * 
+	 * @param fieldId
+	 * @return the type; or <code>null</code> if the field is unknown.
+	 */
+	protected final FieldType getFieldType(String fieldId) {
+		return this.fieldTypes.get(fieldId);
+	}
+
+	/** Replies the text editor with the given id and of the given type.
+	 * 
+	 * @param fieldId is the identifier of the field.
+	 * @return the view or <code>null</code>.
+	 */
+	protected final TextEditor getTextEditor(String fieldId) {
+		View v = findViewWithTag(fieldId);
+		if (v instanceof EditText) {
+			return new TextEditor((EditText)v); 
+		}
+		if (v instanceof TextView) {
+			return new TextEditor((TextView)v); 
+		}
+		return null;
+	}
+
+	/** Replies the boolean editor with the given id and of the given type.
+	 * 
+	 * @param fieldId is the identifier of the field.
+	 * @return the view or <code>null</code>.
+	 */
+	protected final BooleanEditor getBooleanEditor(String fieldId) {
+		View v = findViewWithTag(fieldId);
+		if (v instanceof Switch) {
+			return new BooleanEditor((Switch)v); 
+		}
+		if (v instanceof CheckBox) {
+			return new BooleanEditor((CheckBox)v); 
+		}
+		if (v instanceof ImageView) {
+			return new BooleanEditor((ImageView)v); 
+		}
+		return null;
+	}
+
+	/** Replies the color editor with the given id and of the given type.
+	 * 
+	 * @param fieldId is the identifier of the field.
+	 * @return the view or <code>null</code>.
+	 */
+	protected final ColorEditor getColorEditor(String fieldId) {
+		View v = findViewWithTag(fieldId);
+		if (v instanceof ColorButton) {
+			return new ColorEditor((ColorButton)v); 
+		}
+		if (v instanceof TextView) {
+			return new ColorEditor((TextView)v); 
+		}
+		return null;
+	}
+
+	/** Replies the color editor with the given id and of the given type.
+	 * 
+	 * @param <T> is the type of the elements in the combo. 
+	 * @param fieldId is the identifier of the field.
+	 * @param type is the type of the elements in the combo. 
+	 * @return the view or <code>null</code>.
+	 */
+	protected final <T> ComboEditor<T> getComboEditor(String fieldId, Class<T> type) {
+		View v = findViewWithTag(fieldId);
+		if (v instanceof ColorButton) {
+			return new ComboEditor<T>((ColorButton)v); 
+		}
+		if (v instanceof TextView) {
+			return new ComboEditor<T>((TextView)v); 
+		}
+		return null;
+	}
+
+	private TextView addLabel(TableRow row, String label) {
+		TextView labelView = new TextView(getContext());
+		labelView.setText(label);
+		row.addView(labelView, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+				1f));
+		return labelView;
+	}
+
+	private TextView addLabel(TableRow row, String label, int style) {
+		TextView labelView = new TextView(getContext());
+		if (style!=0) labelView.setInputType(style);
+		if (label!=null) labelView.setText(label);
+		row.addView(labelView, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+				1f));
+		return labelView;
+	}
+
+	/** Add a field in the editor that permits to enter a string.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addStringField(String label, String fieldId, String value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setText(value);
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row, value, 0));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.STRING);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter an integer.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param isPositiveOnly indicates if only the positives numbers are supported.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addIntegerField(String label, String fieldId, boolean isPositiveOnly, long value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType = InputType.TYPE_CLASS_NUMBER;
+		if (!isPositiveOnly) inputType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			editView.setText(Long.toString(
+					isPositiveOnly ? Math.abs(value) : value));
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row,Long.toString(value), inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.INTEGER);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a floating-point number.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param isPositiveOnly indicates if only the positives numbers are supported.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addFloatField(String label, String fieldId, boolean isPositiveOnly, double value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType = InputType.TYPE_CLASS_NUMBER|InputType.TYPE_NUMBER_FLAG_DECIMAL;
+		if (!isPositiveOnly) inputType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			editView.setText(Double.toString(
+					isPositiveOnly ? Math.abs(value) : value));
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));			
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row,Double.toString(value), inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.FLOAT);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a boolean value.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final BooleanEditor addBooleanField(String label, String fieldId, boolean value) {
+		BooleanEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		if (isEditable()) {
+			Switch editView = new Switch(getContext());
+			editView.setTag(fieldId);
+			editView.setChecked(value);
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new BooleanEditor(editView);
+		}
+		else {
+			ImageView imageView = new ImageView(getContext());
+			row.addView(imageView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new BooleanEditor(imageView);
+			editor.setChecked(value);
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.BOOLEAN);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a textual password.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addTextPasswordField(String label, String fieldId, String value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType =
+				InputType.TYPE_CLASS_TEXT
+				|InputType.TYPE_TEXT_VARIATION_PASSWORD;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			editView.setText(value);
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row, value, inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.PASSWORD);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a numeric password.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addNumPasswordField(String label, String fieldId, int value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType = InputType.TYPE_CLASS_NUMBER
+				|InputType.TYPE_NUMBER_VARIATION_PASSWORD;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			editView.setText(Integer.toString(value));
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row, Integer.toString(value), inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.PASSWORD);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter an email.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addEmailField(String label, String fieldId, URI value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType = InputType.TYPE_CLASS_TEXT
+				|InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			editView.setText(value.toString());
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			editor = new TextEditor(addLabel(row, value.toString(), inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.EMAIL);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter an uri.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final TextEditor addUriField(String label, String fieldId, URL value) {
+		TextEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		int inputType = InputType.TYPE_CLASS_TEXT
+				|InputType.TYPE_TEXT_VARIATION_URI;
+		if (isEditable()) {
+			EditText editView = new EditText(getContext());
+			editView.setTag(fieldId);
+			editView.setInputType(inputType);
+			if (value!=null) editView.setText(value.toExternalForm());
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new TextEditor(editView);
+		}
+		else {
+			String v = null;
+			if (value!=null) v = value.toExternalForm();
+			editor = new TextEditor(addLabel(row, v, inputType));
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.URL);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a color.
+	 * 
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final ColorEditor addColorField(String label, String fieldId, Color value) {
+		ColorEditor editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		// Field
+		if (isEditable()) {
+			ColorButton editView = new ColorButton(getContext());
+			editView.setTag(fieldId);
+			editView.setColor(value==null ? null : value.getRGB());
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new ColorEditor(editView);
+		}
+		else {
+			Color c = value==null ? Colors.BLACK : value;
+			editor = new ColorEditor(addLabel(row, null, 0));
+			editor.setColor(c);
+		}
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.COLOR);
+
+		return editor;
+	}
+
+	/** Add a field in the editor that permits to enter a value with a combo.
+	 * 
+	 * @param <T> is the type of the choices.
+	 * @param label is the label associated with the field.
+	 * @param fieldId is the identifier of the field.
+	 * @param choices are the possible choices in the combo.
+	 * @param value is the value in the field.
+	 * @return the edition view.
+	 */
+	protected final <T> ComboEditor<T> addComboField(String label, String fieldId, List<T> choices, T value) {
+		ComboEditor<T> editor;
+		TableRow row = new TableRow(getContext());
+		// Label
+		addLabel(row, label);
+		if (isEditable()) {
+			// Spinner
+			Spinner editView = new Spinner(getContext());
+			editView.setTag(fieldId);
+			ArrayAdapter<T> adapter = new ArrayAdapter<T>(getContext(), android.R.layout.simple_spinner_item, choices);
+			adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+			editView.setAdapter(adapter);
+			row.addView(editView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new ComboEditor<T>(editView, adapter);
+		}
+		else {
+			TextView textView = new TextView(getContext());
+			textView.setTag(fieldId);
+			row.addView(textView, new TableRow.LayoutParams(
+					android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+					android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
+					1f));
+			editor = new ComboEditor<T>(textView);
+		}
+
+		editor.setValue(value);
+
+		this.tableLayout.addView(row, new TableRow.LayoutParams(
+				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+				android.view.ViewGroup.LayoutParams.WRAP_CONTENT));
+
+		this.fieldTypes.put(fieldId, FieldType.COMBO);
+
+		return editor;
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public Map<String,Object> getEditedProperties() {
+		Map<String,Object> properties = new TreeMap<String,Object>();
+		for(Entry<String,FieldType> entry : this.fieldTypes.entrySet()) {
+			switch(entry.getValue()) {
+			case BOOLEAN:
+			{
+				BooleanEditor editor = getBooleanEditor(entry.getKey());
+				if (editor!=null) {
+					properties.put(entry.getKey(), editor.isChecked());
+				}
+			}
+			break;
+			case STRING:
+			case PASSWORD:
+			{
+				TextEditor editor = getTextEditor(entry.getKey());
+				if (editor!=null) {
+					properties.put(entry.getKey(), editor.getText());
+				}
+			}
+			break;
+			case EMAIL:
+			{
+				TextEditor editor = getTextEditor(entry.getKey());
+				if (editor!=null) {
+					try {
+						properties.put(entry.getKey(), new URI(editor.getText()));
+					}
+					catch (URISyntaxException e) {
+						Log.e(getClass().getName(), e.getLocalizedMessage(), e);
+					}
+				}
+			}
+			break;
+			case URL:
+			{
+				TextEditor editor = getTextEditor(entry.getKey());
+				if (editor!=null) {
+					try {
+						properties.put(entry.getKey(), new URL(editor.getText()));
+					}
+					catch (MalformedURLException e) {
+						Log.e(getClass().getName(), e.getLocalizedMessage(), e);
+					}
+				}
+			}
+			break;
+			case FLOAT:
+			{
+				TextEditor editor = getTextEditor(entry.getKey());
+				if (editor!=null) {
+					try {
+						properties.put(entry.getKey(), Double.parseDouble(editor.getText()));
+					}
+					catch (Exception e) {
+						Log.e(getClass().getName(), e.getLocalizedMessage(), e);
+					}
+				}
+			}
+			break;
+			case INTEGER:
+			{
+				TextEditor editor = getTextEditor(entry.getKey());
+				if (editor!=null) {
+					try {
+						properties.put(entry.getKey(), Long.parseLong(editor.getText()));
+					}
+					catch (Exception e) {
+						Log.e(getClass().getName(), e.getLocalizedMessage(), e);
+					}
+				}
+			}
+			break;
+			case COLOR:
+			{
+				ColorEditor editor = getColorEditor(entry.getKey());
+				if (editor!=null) {
+					properties.put(entry.getKey(), editor.getColor());
+				}
+			}
+			break;
+			case COMBO:
+			{
+				ComboEditor<?> editor = getComboEditor(entry.getKey(), Object.class);
+				if (editor!=null) {
+					Object value = editor.getValue();
+					properties.put(entry.getKey(), value);
+				}
+			}
+			break;
+			default:
+			}
+		}
+		return properties;
+	}
+
+	/** Wrapper to a TextEdit or a TextView depending the editable flag.
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	protected static class TextEditor {
+
+		private final EditText edit;
+		private final TextView view;
+
+
+		/**
+		 * @param edit
+		 */
+		public TextEditor(EditText edit) {
+			this.edit = edit;
+			this.view = null;
+		}
+
+		/**
+		 * @param view
+		 */
+		public TextEditor(TextView view) {
+			this.edit = null;
+			this.view = view;
+		}
+
+		/** Change the text
+		 * 
+		 * @param text
+		 */
+		public void setText(String text) {
+			if (this.edit!=null) {
+				this.edit.setText(text);
+			}
+			else {
+				this.view.setText(text);
+			}
+		}
+
+		/** Replies the text.
+		 * 
+		 * @return the text.
+		 */
+		public String getText() {
+			if (this.edit!=null) {
+				return this.edit.getText().toString();
+			}
+			return this.view.getText().toString();
+		}
+
+	} // class TextEditor
+
+	/** Wrapper to a CheckBox or a ImageView depending the editable flag.
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	protected static class BooleanEditor {
+
+		private final CheckBox edit1;
+		private final Switch edit2;
+		private final ImageView view;
+		private int resourceId = 0;
+
+
+		/**
+		 * @param edit
+		 */
+		public BooleanEditor(CheckBox edit) {
+			this.edit1 = edit;
+			this.edit2 = null;
+			this.view = null;
+		}
+
+		/**
+		 * @param edit
+		 */
+		public BooleanEditor(Switch edit) {
+			this.edit1 = null;
+			this.edit2 = edit;
+			this.view = null;
+		}
+
+		/**
+		 * @param view
+		 */
+		public BooleanEditor(ImageView view) {
+			this.edit1 = null;
+			this.edit2 = null;
+			this.view = view;
+		}
+
+		/** Change the check
+		 * 
+		 * @param checked
+		 */
+		public void setChecked(boolean checked) {
+			if (this.edit2!=null) {
+				this.edit2.setChecked(checked);
+			}
+			else if (this.edit1!=null) {
+				this.edit1.setChecked(checked);
+			}
+			else {
+				this.resourceId = checked ?
+						android.R.drawable.checkbox_on_background :
+							android.R.drawable.checkbox_off_background;
+				this.view.setImageResource(this.resourceId);
+			}
+		}
+
+		/** Replies the check.
+		 * 
+		 * @return the check.
+		 */
+		public boolean isChecked() {
+			if (this.edit2!=null) {
+				return this.edit2.isChecked();
+			}
+			if (this.edit1!=null) {
+				return this.edit1.isChecked();
+			}
+			return this.resourceId==android.R.drawable.checkbox_on_background;
+		}
+
+	} // class BooleanEditor
+
+	/** Wrapper to a ColorButton or a TextView depending the editable flag.
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	protected static class ColorEditor {
+
+		private final ColorButton edit;
+		private final TextView view;
+		private Color color = null;
+
+
+		/**
+		 * @param edit
+		 */
+		public ColorEditor(ColorButton edit) {
+			this.edit = edit;
+			this.view = null;
+		}
+
+		/**
+		 * @param view
+		 */
+		public ColorEditor(TextView view) {
+			this.edit = null;
+			this.view = view;
+		}
+
+		/** Change the color
+		 * 
+		 * @param color
+		 */
+		public void setColor(Color color) {
+			if (this.edit!=null) {
+				if (color==null)
+					this.edit.setColor((Integer)null);
+				else
+					this.edit.setColor(color.getRGB());
+			}
+			else {
+				if (color==null) {
+					this.view.setBackgroundResource(R.drawable.hatchs);
+				}
+				else {
+					this.view.setBackgroundDrawable(new ColorDrawable(this.color.getRGB()));
+				}
+			}
+		}
+
+		/** Replies the color.
+		 * 
+		 * @return the color or <code>null</code> if default color.
+		 */
+		public Color getColor() {
+			if (this.edit!=null) {
+				Integer c = this.edit.getColor();
+				if (c==null) return null;
+				return VectorToolkit.color(c.intValue());
+			}
+			return this.color;
+		}
+
+	} // class ColorEditor
+
+	/** Wrapper to a Spinner or a TextView depending the editable flag.
+	 * 
+	 * @param <T> is the type of the values in the combo.
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	protected static class ComboEditor<T> {
+
+		private final TextView view;
+		private final ArrayAdapter<T> adapter;
+		private final Spinner edit;
+		private T value;
+
+
+		/**
+		 * @param view
+		 */
+		public ComboEditor(TextView view) {
+			this.view = view;
+			this.edit = null;
+			this.adapter = null;
+		}
+
+		/**
+		 * @param edit
+		 * @param adapter
+		 */
+		public ComboEditor(Spinner edit, ArrayAdapter<T> adapter) {
+			this.view = null;
+			this.edit = edit;
+			this.adapter = adapter;
+			this.value = null;
+		}
+
+		/** Change the value
+		 * 
+		 * @param value
+		 */
+		public void setValue(T value) {
+			if (value!=null) {
+				if (this.edit!=null) {
+					int pos = this.adapter.getPosition(value);
+					if (pos>=0) {
+						this.edit.setSelection(pos);
+					}
+				}
+				else if (this.view!=null) {
+					this.view.setText(value.toString());
+				}
+			}
+		}
+
+		/** Replies the value.
+		 * 
+		 * @return the value.
+		 */
+		@SuppressWarnings("unchecked")
+		public T getValue() {
+			if (this.edit!=null) {
+				return (T)this.edit.getSelectedItem();
+			}
+			return this.value;
+		}
+
+	} // class ComboEditor
+
+	/** Types of fields. 
+	 * 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	public enum FieldType {
+		/** String. */
+		STRING,
+		/** Integer. */
+		INTEGER,
+		/** Float. */
+		FLOAT,
+		/** Boolean. */
+		BOOLEAN,
+		/** Color. */
+		COLOR,
+		/** Uri. */
+		URL,
+		/** Email. */
+		EMAIL,
+		/** Password. */
+		PASSWORD,
+		/** Combo. */
+		COMBO;
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/texteditor/TextEditorActivity.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/texteditor/TextEditorActivity.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/texteditor/TextEditorActivity.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,186 @@
+package org.arakhne.afc.ui.android.texteditor;
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.vmutil.FileSystem;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.EditText;
+
+/**
+ * Text editor embedded inside an activity fragment. 
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class TextEditorActivity extends Activity {
+
+	private EditText textView = null;
+	private File file = null;
+	private boolean changedSinceOriginal = false;
+	private boolean changedSinceLastSave = false;
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		
+		setContentView(R.layout.texteditor);
+		
+		this.file = null;
+		this.changedSinceOriginal = false;
+		this.changedSinceLastSave = false;
+		this.textView = (EditText)findViewById(R.id.textEditor);
+		
+		Intent intent = getIntent();
+		
+		this.textView.addTextChangedListener(new TextWatcher() {
+			@Override
+			public void onTextChanged(CharSequence s, int start, int before, int count) {
+				//
+			}
+			@Override
+			public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+				//
+			}
+			@SuppressWarnings("synthetic-access")
+			@Override
+			public void afterTextChanged(Editable s) {
+				if (!TextEditorActivity.this.changedSinceLastSave) {
+					TextEditorActivity.this.changedSinceLastSave = true;
+					TextEditorActivity.this.changedSinceOriginal = true;
+					TextEditorActivity.this.invalidateOptionsMenu();
+				}
+			}
+		});
+		
+		Uri uri = intent.getData();
+		if (uri!=null) {
+			try {
+				URI jURI = new URI(uri.toString());
+				URL jURL = jURI.toURL();
+				
+				try {
+					this.file = FileSystem.convertURLToFile(jURL);
+				}
+				catch(Throwable _) {
+					//
+				}
+				
+				BufferedReader reader = new BufferedReader(new InputStreamReader(jURL.openStream()));
+				try {
+					StringBuilder text = new StringBuilder();
+					char[] buffer = new char[2048];
+					int n = reader.read(buffer, 0, 2048);
+					while (n>0) {
+						text.append(buffer, 0, n);
+						n = reader.read(buffer, 0, 2048);
+					}
+					this.textView.setText(text);
+				}
+				finally {
+					reader.close();
+				}
+			}
+			catch(Throwable e) {
+				Log.w(getClass().getName(), e.getLocalizedMessage(), e);
+			}
+		}
+	}
+	
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.texteditor_menu, menu);
+		return true;
+	}
+	
+	@Override
+	public boolean onPrepareOptionsMenu(Menu menu) {
+		super.onPrepareOptionsMenu(menu);
+		MenuItem item;
+		item = menu.findItem(R.id.textEditorSaveMenu);
+		item.setEnabled(this.file!=null && this.changedSinceLastSave);
+		item = menu.findItem(R.id.textEditorUploadMenu);
+		item.setEnabled(this.file!=null && this.changedSinceOriginal);
+		return true;
+	}
+
+	private void save() {
+		if (this.file!=null && this.changedSinceLastSave) {
+			try {
+				FileWriter fw = new FileWriter(this.file);
+				try {
+					fw.write(this.textView.getText().toString());
+					fw.flush();
+				}
+				finally {
+					fw.close();
+				}
+			}
+			catch(Throwable e) {
+				Log.w(getClass().getName(), e.getLocalizedMessage(), e);
+			}
+			this.changedSinceLastSave = false;
+			invalidateOptionsMenu();
+		}
+	}
+	
+	@Override
+	public boolean onMenuItemSelected(int featureId, MenuItem item) {
+		int id = item.getItemId();
+		if (id==R.id.textEditorSaveMenu) {
+			save();
+			return true;
+		}
+		if (id==R.id.textEditorUploadMenu) {
+			save();
+			Intent data = new Intent();
+			Uri uri = Uri.fromFile(this.file);
+			data.setData(uri);
+			setResult(RESULT_OK, data);
+			finish();
+			return true;
+		}
+		return super.onMenuItemSelected(featureId, item);
+	}
+		
+}
\ No newline at end of file

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/DroidZoomableGraphics2D.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/DroidZoomableGraphics2D.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/DroidZoomableGraphics2D.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,890 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.zoom;
+
+import java.net.URL;
+
+import org.arakhne.afc.math.continous.object2d.Circle2f;
+import org.arakhne.afc.math.continous.object2d.Ellipse2f;
+import org.arakhne.afc.math.continous.object2d.Path2f;
+import org.arakhne.afc.math.continous.object2d.PathElement2f;
+import org.arakhne.afc.math.continous.object2d.PathIterator2f;
+import org.arakhne.afc.math.continous.object2d.Point2f;
+import org.arakhne.afc.math.continous.object2d.Rectangle2f;
+import org.arakhne.afc.math.continous.object2d.RoundRectangle2f;
+import org.arakhne.afc.math.continous.object2d.Segment2f;
+import org.arakhne.afc.math.continous.object2d.Shape2f;
+import org.arakhne.afc.math.generic.PathWindingRule;
+import org.arakhne.afc.math.matrix.Transform2D;
+import org.arakhne.afc.ui.CenteringTransform;
+import org.arakhne.afc.ui.Graphics2DLOD;
+import org.arakhne.afc.ui.StringAnchor;
+import org.arakhne.afc.ui.ZoomableContext;
+import org.arakhne.afc.ui.ZoomableContextUtil;
+import org.arakhne.afc.ui.vector.AbstractVectorGraphics2D;
+import org.arakhne.afc.ui.vector.Color;
+import org.arakhne.afc.ui.vector.Composite;
+import org.arakhne.afc.ui.vector.Font;
+import org.arakhne.afc.ui.vector.FontMetrics;
+import org.arakhne.afc.ui.vector.Image;
+import org.arakhne.afc.ui.vector.ImageObserver;
+import org.arakhne.afc.ui.vector.Stroke;
+import org.arakhne.afc.ui.vector.VectorToolkit;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.Path.FillType;
+import android.graphics.PathEffect;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region.Op;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+/**
+ * This is the droid-based implementation of a VectorGraphics2D and a ZoomableContext.
+ *  
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ */
+public class DroidZoomableGraphics2D extends AbstractVectorGraphics2D implements ZoomableContext {
+
+	/** Convert the droid matrix to Arakhne transformation matrix.
+	 * 
+	 * @param m is the matrix to convert to a transformation.
+	 * @param scale is the scaling factor to apply.
+	 * @return the Arakhne transformation matrix.
+	 */
+	public static Transform2D convertMatrix(Matrix m, float scale) {
+		assert(m!=null);
+		if (m.isIdentity()) return new Transform2D();
+		float[] values = new float[9];
+		m.getValues(values);
+		return new Transform2D(
+				ZoomableContextUtil.pixel2logical_size(values[0], scale),
+				values[1],
+				ZoomableContextUtil.pixel2logical_size(values[2], scale),
+				values[3],
+				ZoomableContextUtil.pixel2logical_size(values[4], scale),
+				ZoomableContextUtil.pixel2logical_size(values[5], scale));
+	}
+
+	/** Convert the Arakhne transformation matrix to droid matrix.
+	 * 
+	 * @param t is the transformation to convert to a matrix.
+	 * @param scale is the scaling factor to apply.
+	 * @return droid matrix.
+	 */
+	public static Matrix convertTransformation(Transform2D t, float scale) {
+		assert(t!=null);
+		Matrix at = new Matrix();
+		if (!t.isIdentity()) {
+			float[] values = new float[] {
+					ZoomableContextUtil.logical2pixel_size(t.m00, scale),
+					t.m01,
+					ZoomableContextUtil.logical2pixel_size(t.m02, scale),
+					t.m10,
+					ZoomableContextUtil.logical2pixel_size(t.m11, scale),
+					ZoomableContextUtil.logical2pixel_size(t.m12, scale),
+					t.m20, t.m21, t.m22
+			};
+			at.setValues(values);
+		}
+		return at;
+	}
+
+	/** Set the color of the given paint to the given color.
+	 * This function tries the Android color objects that
+	 * are able to represent a color (ARGB color, Drawable).
+	 * 
+	 * @param paint
+	 * @param color
+	 */
+	protected static void setColor(Paint paint, Color color) {
+		Object androidColor = VectorToolkit.nativeUIObject(Object.class, color);
+		if (androidColor instanceof Drawable) {
+			//paint.setColor((Drawable)androidColor);
+		}
+		else if (androidColor instanceof Number) {
+			paint.setColor(((Number)androidColor).intValue());
+		}
+	}
+
+
+	/** Android canvas.
+	 */
+	protected final Canvas canvas;
+
+	/** Default color to draw the background.
+	 * 
+	 * @see #background
+	 */
+	protected final Color defaultBackground;
+	
+	/** Default Level-of-Detail.
+	 * 
+	 * @see #levelOfDetail
+	 */
+	protected final Graphics2DLOD defaultLevelOfDetail;
+
+	/** Current Level-of-Detail.
+	 * 
+	 * @see #defaultLevelOfDetail
+	 */
+	protected Graphics2DLOD levelOfDetail;
+
+	/** Android painter to use for filling the shapes.
+	 */
+	protected Paint fillPainter;
+
+	/** Android painter to use for drawing the outlines of the shapes.
+	 */
+	protected Paint linePainter;
+
+	/** Android painter to use for drawing the texts.
+	 */
+	protected Paint fontPainter;
+
+	/** Current background color.
+	 * 
+	 * @see #defaultBackground
+	 */
+	protected Color background;
+
+	private final int initialSaveCount;
+	
+	/** Scaling factor to apply when drawing.
+	 */
+	protected final float scale;
+	
+	/** Transformation to apply to center the objects on the view.
+	 */
+	protected final CenteringTransform centeringTransform;
+	
+	/** Temp rectangle used in internal computations.
+	 */
+	protected final Rectangle2f tmpRect = new Rectangle2f();
+
+	/** Default color to fill the shapes.
+	 * 
+	 * @see #fillPainter
+	 */
+	protected final Color defaultFillColor;
+
+	/** Default color to outline the shapes.
+	 * 
+	 * @see #linePainter
+	 */
+	protected final Color defaultLineColor;
+
+	/** Scaling sensitivity.
+	 */
+	protected final float scalingSensitivity;
+
+	/** Coordinate of the point that is drawn at the center of the view.
+	 */
+	protected final float focusX;
+
+	/** Coordinate of the point that is drawn at the center of the view.
+	 */
+	protected final float focusY;
+
+	/** Maximal value for the scale factor.
+	 */
+	protected final float maxScale;
+
+	/** Minimal value for the scale factor.
+	 */
+	protected final float minScale;
+
+	/**
+	 * @param canvas
+	 * @param defaultFillColor is the default color for filling.
+	 * @param defaultLineColor is the default color for the lines.
+	 * @param scaleFactor is the scaling factor to apply.
+	 * @param centeringTransform is the transformation used to draw the objects at the center of the view.
+	 * @param background is the background color.
+	 * @param isAntiAlias indicates if antialiasing is used when drawing.
+	 * @param scalingSensitivity is the sensitivity of the scaling actions.
+	 * @param focusX is the coordinate of the focus point.
+	 * @param focusY is the coordinate of the focus point.
+	 * @param minScaleFactor is the minimal allowed scaling factor.
+	 * @param maxScaleFactor is the maximal allowed scaling factor.
+	 */
+	public DroidZoomableGraphics2D(
+			Canvas canvas,
+			Color defaultFillColor,
+			Color defaultLineColor,
+			float scaleFactor,
+			CenteringTransform centeringTransform,
+			Color background,
+			boolean isAntiAlias,
+			float scalingSensitivity,
+			float focusX,
+			float focusY,
+			float minScaleFactor,
+			float maxScaleFactor) {
+		super(
+				defaultFillColor,
+				defaultLineColor,
+				null,
+				true,
+				true,
+				null);
+		this.defaultFillColor = defaultFillColor;
+		this.defaultLineColor = defaultLineColor;
+		this.scalingSensitivity = scalingSensitivity;
+		this.focusX = focusX;
+		this.focusY = focusY;
+		this.minScale = minScaleFactor;
+		this.maxScale = maxScaleFactor;
+		this.canvas = canvas;
+		this.scale = scaleFactor;
+		this.centeringTransform = centeringTransform;
+		this.defaultBackground = this.background = background;
+		this.initialSaveCount = canvas.getSaveCount();
+		this.defaultLevelOfDetail = this.levelOfDetail = isAntiAlias ? Graphics2DLOD.NORMAL_LEVEL_OF_DETAIL : Graphics2DLOD.LOW_LEVEL_OF_DETAIL;
+		resetPainters();
+	}
+
+	@Override
+	public void dispose() {
+		this.background = null;
+		this.fillPainter = null;
+		this.fontPainter = null;
+		this.linePainter = null;
+		this.levelOfDetail = null;
+		super.dispose();
+	}
+
+	@Override
+	public void reset() {
+		super.reset();
+		this.levelOfDetail = this.defaultLevelOfDetail;
+		this.background = this.defaultBackground;
+		setFillColor(this.defaultFillColor);
+		setOutlineColor(this.defaultLineColor);
+		resetPainters();
+		while (this.canvas.getSaveCount() > this.initialSaveCount) {
+			this.canvas.restore();
+		}
+	}
+
+	/** Reset the painters to the default attribute values.
+	 */
+	protected void resetPainters() {
+		boolean isNormalLod = this.levelOfDetail.compareTo(Graphics2DLOD.NORMAL_LEVEL_OF_DETAIL)>=0;
+
+		this.fillPainter = new Paint();
+		this.fillPainter.setStyle(Style.FILL);
+		this.fillPainter.setAntiAlias(isNormalLod);
+		this.linePainter = new Paint();
+		this.linePainter.setStyle(Style.STROKE);
+		this.linePainter.setAntiAlias(isNormalLod);
+		this.fontPainter = new Paint();
+		this.fontPainter.setStyle(Style.FILL_AND_STROKE);
+		this.fontPainter.setAntiAlias(isNormalLod);
+		this.fontPainter.setTextSize(
+				ZoomableContextUtil.logical2pixel_size(
+						this.fontPainter.getTextSize(),
+						this.scale));
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	protected void log(String message) {
+		Log.d(toString(), message);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	protected void log(String message, Throwable exception) {
+		Log.d(toString(), message, exception);
+	}
+
+	/** Replies the droid canvas.
+	 * 
+	 * @return the droid canvas.
+	 */
+	public Canvas getCanvas() {
+		return this.canvas;
+	}
+
+	@Override
+	public Color setOutlineColor(Color color) {
+		setColor(this.linePainter, color);
+		setColor(this.fontPainter, color);
+		return super.setOutlineColor(color);
+	}
+
+	@Override
+	public Color setFillColor(Color color) {
+		setColor(this.fillPainter, color);
+		return super.setFillColor(color);
+	}
+
+	@Override
+	public Graphics2DLOD getLOD() {
+		return this.levelOfDetail;
+	}
+
+	/** Change the LOD of the graphics2D.
+	 * 
+	 * @param lod
+	 */
+	public void setLOD(Graphics2DLOD lod) {
+		if (lod!=null) {
+			this.levelOfDetail = lod;
+			boolean isNormalLod = this.levelOfDetail.compareTo(Graphics2DLOD.NORMAL_LEVEL_OF_DETAIL)>=0;
+			this.fillPainter.setAntiAlias(isNormalLod);
+			this.linePainter.setAntiAlias(isNormalLod);
+			this.fontPainter.setAntiAlias(isNormalLod);
+		}
+	}
+
+	@Override
+	public StringAnchor getStringAnchor() {
+		return StringAnchor.LEFT_BASELINE;
+	}
+
+	@Override
+	public void drawPoint(float x, float y) {
+		preDrawing();
+		float px = ZoomableContextUtil.logical2pixel_x(x, this.centeringTransform, this.scale);
+		float py = ZoomableContextUtil.logical2pixel_y(y, this.centeringTransform, this.scale);
+		this.canvas.drawRect(px-.5f, py-.5f, px+.5f, py+.5f, this.fillPainter);
+		postDrawing();
+	}
+
+	@Override
+	public Font getFont() {
+		Paint scaledFont = new Paint(this.fontPainter);
+		scaledFont.setTextSize(
+				ZoomableContextUtil.pixel2logical_size(this.fontPainter.getTextSize(), this.scale));
+		return VectorToolkit.font(scaledFont);
+	}
+
+	@Override
+	public void setFont(Font font) {
+		Paint p = VectorToolkit.nativeUIObject(Paint.class, font);
+		assert(p!=null);
+		this.fontPainter = new Paint(p);
+		this.fontPainter.setTextSize(
+				ZoomableContextUtil.logical2pixel_size(p.getTextSize(), this.scale));
+	}
+
+	@Override
+	public FontMetrics getFontMetrics() {
+		Paint scaledFont = new Paint(this.fontPainter);
+		scaledFont.setTextSize(
+				ZoomableContextUtil.pixel2logical_size(this.fontPainter.getTextSize(), this.scale));
+		return VectorToolkit.fontMetrics(scaledFont);
+	}
+
+	@Override
+	public FontMetrics getFontMetrics(Font f) {
+		return VectorToolkit.fontMetrics(f);
+	}
+
+	@Override
+	public Shape2f getClip() {
+		Rect r = this.canvas.getClipBounds();
+		Rectangle2f rr = new Rectangle2f(r.left, r.right, r.width(), r.height());
+		ZoomableContextUtil.pixel2logical(rr, this.centeringTransform, this.scale);
+		return rr;
+	}
+
+	@Override
+	public void setClip(Shape2f clip) {
+		Rectangle2f r = clip.toBoundingBox();
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scale);
+		this.canvas.clipRect(r.getMinX(), r.getMinY(), r.getMaxX(), r.getMaxY(), Op.REPLACE);
+	}
+
+	@Override
+	public void clip(Shape2f clip) {
+		Rectangle2f r = clip.toBoundingBox();
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scale);
+		this.canvas.clipRect(r.getMinX(), r.getMinY(), r.getMaxX(), r.getMaxY(), Op.UNION);
+	}
+
+	@Override
+	public boolean drawImage(URL imageURL, Image img, float dx1, float dy1,
+			float dx2, float dy2, int sx1, int sy1, int sx2, int sy2) {
+		preDrawing();
+		Bitmap bitmap = VectorToolkit.nativeUIObject(Bitmap.class, img);
+		assert(bitmap!=null);
+		this.tmpRect.setFromCorners(dx1, dy1, dx2, dy2);
+		ZoomableContextUtil.logical2pixel(this.tmpRect, this.centeringTransform, this.scale);
+		Rect srcRect = new Rect(sx1, sy1, sx2, sy2);
+		RectF dstRect = new RectF(this.tmpRect.getMinX(), this.tmpRect.getMinY(), this.tmpRect.getMaxX(), this.tmpRect.getMaxY());
+		this.canvas.drawBitmap(bitmap, srcRect, dstRect, VectorToolkit.getObjectWithId(0, Paint.class));
+		postDrawing();
+		return true;
+	}
+
+	@Override
+	public boolean drawImage(URL imageURL, Image img, float dx1, float dy1,
+			float dx2, float dy2, int sx1, int sy1, int sx2, int sy2,
+			ImageObserver observer) {
+		preDrawing();
+		Bitmap bitmap = VectorToolkit.nativeUIObject(Bitmap.class, img);
+		assert(bitmap!=null);
+		this.tmpRect.setFromCorners(dx1, dy1, dx2, dy2);
+		ZoomableContextUtil.logical2pixel(this.tmpRect, this.centeringTransform, this.scale);
+		Rect srcRect = new Rect(sx1, sy1, sx2, sy2);
+		RectF dstRect = new RectF(this.tmpRect.getMinX(), this.tmpRect.getMinY(), this.tmpRect.getMaxX(), this.tmpRect.getMaxY());
+		this.canvas.drawBitmap(bitmap, srcRect, dstRect, VectorToolkit.getObjectWithId(0, Paint.class));
+		postDrawing();
+		return true;
+	}
+
+	@Override
+	public void clear(Shape2f s) {
+		Paint old = this.fillPainter;
+		this.fillPainter = VectorToolkit.getObjectWithId(0, Paint.class);
+		this.fillPainter.setAlpha(1);
+		Color b = getBackground();
+		if (b!=null) {
+			setColor(this.fillPainter, b);
+		}
+		drawOnCanvas(s, this.fillPainter);
+		this.fillPainter = old;
+		postDrawing();
+	}
+
+	private void drawOnCanvas(Shape2f s, Paint painter) {
+		if (s!=null) {
+			PathIterator2f path = null;
+			if (s instanceof Path2f) {
+				path = ((Path2f)s).getPathIterator();
+			}
+			else {
+				if (s instanceof Rectangle2f) {
+					Rectangle2f r = (Rectangle2f)s;
+					this.tmpRect.set(r);
+					ZoomableContextUtil.logical2pixel(this.tmpRect, this.centeringTransform, this.scale);
+					this.canvas.drawRect(this.tmpRect.getMinX(), this.tmpRect.getMinY(), this.tmpRect.getMaxX(), this.tmpRect.getMaxY(), painter);
+					return;
+				}
+				if (s instanceof Ellipse2f) {
+					Ellipse2f r = (Ellipse2f)s;
+					this.tmpRect.set(r);
+					ZoomableContextUtil.logical2pixel(this.tmpRect, this.centeringTransform, this.scale);
+					this.canvas.drawOval(
+							new RectF(this.tmpRect.getMinX(), this.tmpRect.getMinY(), this.tmpRect.getMaxX(), this.tmpRect.getMaxY()), painter);
+					return;
+				}
+				if (s instanceof Circle2f) {
+					Circle2f r = (Circle2f)s;
+					float cx = ZoomableContextUtil.logical2pixel_x(r.getX(), this.centeringTransform, this.scale);
+					float cy = ZoomableContextUtil.logical2pixel_y(r.getY(), this.centeringTransform, this.scale);
+					float radius = ZoomableContextUtil.logical2pixel_size(r.getRadius(), this.scale);
+					this.canvas.drawCircle(cx, cy, radius, painter);
+					return;
+				}
+				if (s instanceof RoundRectangle2f) {
+					RoundRectangle2f r = (RoundRectangle2f)s;
+					this.tmpRect.set(r);
+					ZoomableContextUtil.logical2pixel(this.tmpRect, this.centeringTransform, this.scale);
+					float aw = ZoomableContextUtil.logical2pixel_size(r.getArcWidth()/2f, this.scale);
+					float ah = ZoomableContextUtil.logical2pixel_size(r.getArcHeight()/2f, this.scale);
+					this.canvas.drawRoundRect(
+							new RectF(this.tmpRect.getMinX(), this.tmpRect.getMinY(), this.tmpRect.getMaxX(), this.tmpRect.getMaxY()),
+							aw, ah, painter);
+					return;
+				}
+				if (s instanceof Segment2f) {
+					Segment2f l = (Segment2f)s;
+					this.canvas.drawLine(
+							ZoomableContextUtil.logical2pixel_x(l.getX1(), this.centeringTransform, this.scale),
+							ZoomableContextUtil.logical2pixel_y(l.getY1(), this.centeringTransform, this.scale),
+							ZoomableContextUtil.logical2pixel_x(l.getX2(), this.centeringTransform, this.scale),
+							ZoomableContextUtil.logical2pixel_y(l.getY2(), this.centeringTransform, this.scale),
+							painter);
+					return;
+				}
+
+				path = s.getPathIterator();
+			}
+			if (path!=null) {
+				path = ZoomableContextUtil.logical2pixel(path, this.centeringTransform, this.scale);
+				Path droidPath = new Path();
+				if (path.getWindingRule()==PathWindingRule.EVEN_ODD) {
+					droidPath.setFillType(FillType.EVEN_ODD);
+				}
+				PathElement2f e;
+				while (path.hasNext()) {
+					e = path.next();
+					switch(e.type) {
+					case MOVE_TO:
+						droidPath.moveTo(e.toX, e.toY);
+						break;
+					case LINE_TO:
+						droidPath.lineTo(e.toX, e.toY);
+						break;
+					case CURVE_TO:
+						droidPath.cubicTo(e.ctrlX1, e.ctrlY1, e.ctrlX2, e.ctrlY2, e.toX, e.toY);
+						break;
+					case QUAD_TO:
+						droidPath.quadTo(e.ctrlX1, e.ctrlY1, e.toX, e.toY);
+						break;
+					case CLOSE:
+						droidPath.close();
+						break;
+					default:
+					}
+				}
+				this.canvas.drawPath(droidPath, painter);
+			}
+			else {
+				throw new IllegalArgumentException(s.toString());
+			}
+		}
+	}
+
+	@Override
+	public void draw(Shape2f s) {
+		preDrawing();
+		if (isInteriorPainted()) {
+			drawOnCanvas(s, this.fillPainter);
+		}
+		String text = getInteriorText();
+		if (text!=null && !text.isEmpty()) {
+			paintClippedString(
+					text,
+					s.toBoundingBox(),
+					s);
+		}
+		if (isOutlineDrawn()) {
+			drawOnCanvas(s, this.linePainter);
+		}
+		postDrawing();
+	}
+	
+	@Override
+	public void drawString(String str, float x, float y) {
+		preDrawing();
+		this.canvas.drawText(str,
+				ZoomableContextUtil.logical2pixel_x(x, this.centeringTransform, this.scale),
+				ZoomableContextUtil.logical2pixel_y(y, this.centeringTransform, this.scale),
+				this.fontPainter);
+		postDrawing();
+	}
+
+	@Override
+	public void drawString(String str, float x, float y, Shape2f clip) {
+		preDrawing();
+		Shape2f c = getClip();
+		clip(clip);
+		this.canvas.drawText(str,
+				ZoomableContextUtil.logical2pixel_x(x, this.centeringTransform, this.scale),
+				ZoomableContextUtil.logical2pixel_y(y, this.centeringTransform, this.scale),
+				this.fontPainter);
+		setClip(c);
+		postDrawing();
+	}
+
+	@Override
+	public void translate(float tx, float ty) {
+		this.canvas.translate(
+				ZoomableContextUtil.logical2pixel_size(tx, this.scale),
+				ZoomableContextUtil.logical2pixel_size(ty, this.scale));
+	}
+
+	@Override
+	public void scale(float sx, float sy) {
+		this.canvas.scale(
+				ZoomableContextUtil.logical2pixel_size(sx, this.scale),
+				ZoomableContextUtil.logical2pixel_size(sy, this.scale));
+	}
+
+	@Override
+	public void rotate(float theta) {
+		this.canvas.rotate((float)Math.toDegrees(theta));
+	}
+
+	@Override
+	public void shear(float shx, float shy) {
+		this.canvas.skew(
+				ZoomableContextUtil.logical2pixel_size(shx, this.scale),
+				ZoomableContextUtil.logical2pixel_size(shy, this.scale));
+	}
+
+	@Override
+	public void transform(Transform2D Tx) {
+		this.canvas.concat(convertTransformation(Tx, this.scale));
+	}
+
+	@Override
+	public Transform2D setTransform(Transform2D Tx) {
+		Transform2D old = convertMatrix(this.canvas.getMatrix(), this.scale);
+		this.canvas.setMatrix(convertTransformation(Tx, this.scale));
+		return old;
+	}
+
+	@Override
+	public Transform2D getTransform() {
+		return convertMatrix(this.canvas.getMatrix(), this.scale);
+	}
+
+	@Override
+	public void setBackground(Color color) {
+		this.background = color;
+	}
+
+	@Override
+	public Color getBackground() {
+		return this.background;
+	}
+
+	@Override
+	public void setComposite(Composite composite) {
+		int alpha = (int)(composite.getAlpha() * 255);
+		this.fillPainter.setAlpha(alpha);
+		this.linePainter.setAlpha(alpha);
+		this.fontPainter.setAlpha(alpha);
+	}
+
+	@Override
+	public Composite getComposite() {
+		return VectorToolkit.composite(1);
+	}
+
+	@Override
+	public void setStroke(Stroke stroke) {
+		this.linePainter.setStrokeMiter(stroke.getMiterLimit());
+		this.fontPainter.setStrokeMiter(stroke.getMiterLimit());
+
+		Cap cap;
+		switch(stroke.getEndCap()) {
+		case BUTT:
+			cap = Cap.BUTT;
+			break;
+		case SQUARE:
+			cap = Cap.SQUARE;
+			break;
+		case ROUND:
+			cap = Cap.ROUND;
+			break;
+		default:
+			throw new IllegalStateException();
+		}
+		this.linePainter.setStrokeCap(cap);
+		this.fontPainter.setStrokeCap(cap);
+
+		Join join;
+		switch(stroke.getLineJoin()) {
+		case BEVEL:
+			join = Join.BEVEL;
+			break;
+		case MITER:
+			join = Join.MITER;
+			break;
+		case ROUND:
+			join = Join.ROUND;
+			break;
+		default:
+			throw new IllegalStateException();
+		}
+		this.linePainter.setStrokeJoin(join);
+		this.fontPainter.setStrokeJoin(join);
+
+		this.linePainter.setStrokeWidth(stroke.getLineWidth());
+		this.fontPainter.setStrokeWidth(stroke.getLineWidth());
+
+		float[] dashes = stroke.getDashArray();
+		PathEffect pe = null;
+		if (dashes!=null && dashes.length>=2) {
+			pe = new DashPathEffect(dashes, stroke.getDashPhase());
+		}
+		this.linePainter.setPathEffect(pe);
+		this.fontPainter.setPathEffect(pe);
+	}
+
+	@Override
+	public Stroke getStroke() {
+		return VectorToolkit.stroke(this.linePainter);
+	}
+
+	@Override
+	public float logical2pixel_size(float l) {
+		return ZoomableContextUtil.logical2pixel_size(l, this.scale);
+	}
+
+	@Override
+	public float logical2pixel_x(float l) {
+		return ZoomableContextUtil.logical2pixel_x(l, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public float logical2pixel_y(float l) {
+		return ZoomableContextUtil.logical2pixel_y(l, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public float pixel2logical_size(float l) {
+		return ZoomableContextUtil.pixel2logical_size(l, this.scale);
+	}
+
+	@Override
+	public float pixel2logical_x(float l) {
+		return ZoomableContextUtil.pixel2logical_x(l, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public float pixel2logical_y(float l) {
+		return ZoomableContextUtil.pixel2logical_y(l, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public PathIterator2f logical2pixel(PathIterator2f p) {
+		return ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public PathIterator2f pixel2logical(PathIterator2f p) {
+		return ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(Segment2f s) {
+		ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(Segment2f s) {
+		ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(RoundRectangle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(RoundRectangle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(Point2f p) {
+		ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(Point2f p) {
+		ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(Ellipse2f e) {
+		ZoomableContextUtil.logical2pixel(e, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(Ellipse2f e) {
+		ZoomableContextUtil.pixel2logical(e, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(Circle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(Circle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void logical2pixel(Rectangle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public void pixel2logical(Rectangle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public Shape2f logical2pixel(Shape2f s) {
+		return ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scale);
+	}
+
+	@Override
+	public Shape2f pixel2logical(Shape2f s) {
+		return ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scale);
+	}
+
+
+	@Override
+	public float getScalingSensitivity() {
+		return this.scalingSensitivity;
+	}
+
+	@Override
+	public float getFocusX() {
+		return this.focusX;
+	}
+
+	@Override
+	public float getFocusY() {
+		return this.focusY;
+	}
+
+	@Override
+	public float getScalingFactor() {
+		return this.scale;
+	}
+
+	@Override
+	public float getMaxScalingFactor() {
+		return this.maxScale;
+	}
+
+	@Override
+	public float getMinScalingFactor() {
+		return this.minScale;
+	}
+
+	@Override
+	public boolean isXAxisInverted() {
+		return this.centeringTransform.isXAxisFlipped();
+	}
+
+	@Override
+	public boolean isYAxisInverted() {
+		return this.centeringTransform.isYAxisFlipped();
+	}
+
+}

Added: trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/ZoomableView.java
===================================================================
--- trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/ZoomableView.java	                        (rev 0)
+++ trunk/ui/ui-android/src/main/java/org/arakhne/afc/ui/android/zoom/ZoomableView.java	2013-04-11 16:28:25 UTC (rev 422)
@@ -0,0 +1,1297 @@
+/* 
+ * $Id$
+ * 
+ * Copyright (C) 2013 Stephane GALLAND.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * This program is free software; you can redistribute it and/or modify
+ */
+package org.arakhne.afc.ui.android.zoom;
+
+import java.util.EventListener;
+
+import org.arakhne.afc.math.continous.object2d.Circle2f;
+import org.arakhne.afc.math.continous.object2d.Ellipse2f;
+import org.arakhne.afc.math.continous.object2d.PathIterator2f;
+import org.arakhne.afc.math.continous.object2d.Point2f;
+import org.arakhne.afc.math.continous.object2d.Rectangle2f;
+import org.arakhne.afc.math.continous.object2d.RoundRectangle2f;
+import org.arakhne.afc.math.continous.object2d.Segment2f;
+import org.arakhne.afc.math.continous.object2d.Shape2f;
+import org.arakhne.afc.ui.CenteringTransform;
+import org.arakhne.afc.ui.ZoomableContext;
+import org.arakhne.afc.ui.ZoomableContextUtil;
+import org.arakhne.afc.ui.android.R;
+import org.arakhne.afc.ui.android.event.PointerEventAndroid;
+import org.arakhne.afc.ui.event.PointerEvent;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.Toast;
+
+/** This abstract view provides the tools to move and scale
+ * the view. It is abstract because it does not draw anything. 
+ * <p>
+ * The implementation of the ZoomableView handles the pointer and key events as following:
+ * <table>
+ * <head>
+ * <tr><th>Event</th><th>Status</th><th>Callback</th><th>Note</th></tr>
+ * </head>
+ * <tr><td>POINTER_PRESSED</td><td>supported</td><td>{@link #onPointerPressed(PointerEvent)}</td><td>Allways called</td></tr>
+ * <tr><td>POINTER_DRAGGED</td><td>supported</td><td>{@link #onPointerDragged(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr>
+ * <tr><td>POINTER_RELEASED</td><td>supported</td><td>{@link #onPointerReleased(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr>
+ * <tr><td>POINTER_MOVED</td><td>not supported</td><td></td><td>Pointer move on a touch screen cannot be detected?</td></tr>
+ * <tr><td>POINTER_CLICK</td><td>not supported</td><td></td><td>See {@link #setOnClickListener(OnClickListener)}</td></tr>
+ * <tr><td>POINTER_LONG_CLICK</td><td>not supported</td><td></td><td>See {@link #setOnLongClickListener(OnLongClickListener)}</td></tr>
+ * <tr><td>KEY_PRESSED</td><td>not supported</td><td></td><td>See {@link #onKeyDown(int, android.view.KeyEvent)}</td></tr>
+ * <tr><td>KEY_RELEASED</td><td>not supported</td><td></td><td>See {@link #onKeyUp(int, android.view.KeyEvent)}</td></tr>
+ * <tr><td>KEY_TYPED</td><td>not supported</td><td></td><td>See {@link #onKeyUp(int, android.view.KeyEvent)}</td></tr>
+ * <body>
+ * </body>
+ * </table>
+ * <p>
+ * The function #@link {@link #onDrawView(Canvas, float, CenteringTransform)}} may
+ * use an instance of the graphical context {@link DroidZoomableGraphics2D} to draw
+ * the elements according to the zooming attributes.
+ * 
+ * @author $Author: galland$
+ * @version $Name$ $Revision$ $Date$
+ * @mavengroupid $GroupId$
+ * @mavenartifactid $ArtifactId$
+ * @see DroidZoomableGraphics2D
+ */
+public abstract class ZoomableView extends View implements ZoomableContext {
+
+	/** Constant that is the identifier representing of an invalid pointer
+	 * on the droid device.
+	 */
+	public static final int INVALID_POINTER_ID = -1;
+
+	/** Current position of the workspace.
+	 * This position permits to scroll the view.
+	 */
+	private float focusX = 0f;
+
+	/** Current position of the workspace.
+	 * This position permits to scroll the view.
+	 */
+	private float focusY = 0f;
+
+	/** Scale gesture detector.
+	 */
+	private final ScaleGestureDetector scaleGestureDetector;
+	
+	/** Manager of the scaling events.
+	 */
+	private final ScaleGestureManager scaleGestureManager;
+
+	/** Move gesture detector.
+	 */
+	private final MoveGestureDetector moveGestureDetector;
+
+	/** Listener on clicks.
+	 */
+	private final ClickListener clickListener;
+
+	/** Current scaling factor.
+	 */
+	private float scaleFactor = 1.f;
+
+	/** Minimal scaling factor.
+	 */
+	private float minScaleFactor = 0.001f;
+
+	/** Maximal scaling factor.
+	 */
+	private float maxScaleFactor = 100.f;
+
+	/** Handler of events.
+	 */
+	private final EventListener globalListener;
+
+	/** Zooming sensibility.
+	 */
+	private float zoomingSensitivity = 1.5f;
+
+	/** Invert the scrolling direction.
+	 */
+	private boolean isInvertScrollingDirection = false;
+
+	/** Indicates if the repaint requests are ignored.
+	 */
+	private boolean isIgnoreRepaint = false;
+	
+	/** Transformation to center the view used when rendering the view.
+	 */
+	private final CenteringTransform centeringTransform = new CenteringTransform();
+
+	/** Indicates if the X axis is inverted or not.
+	 */
+	private boolean isXAxisInverted = false;
+	
+	/** Indicates if the Y axis is inverted or not.
+	 */
+	private boolean isYAxisInverted = false;
+
+	/**
+	 * @param context is the droid context of the view.
+	 */
+	public ZoomableView(Context context) {
+		this(context, null, 0);
+	}
+
+	/**
+	 * @param context is the droid context of the view.
+	 * @param attrs are the attributes of the view.
+	 */
+	public ZoomableView(Context context, AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	/**
+	 * @param context is the droid context of the view.
+	 * @param attrs are the attributes of the view.
+	 * @param defStyle is the style of the view.
+	 */
+	public ZoomableView(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		this.scaleGestureManager = new ScaleGestureManager();
+		this.scaleGestureDetector = new ScaleGestureDetector(context, this.scaleGestureManager);
+		this.moveGestureDetector = new MoveGestureDetector();
+		this.clickListener = new ClickListener();
+		this.globalListener = createEventHandler();
+		setClickable(true);
+		setLongClickable(true);
+		super.setOnLongClickListener(this.clickListener);
+		super.setOnClickListener(this.clickListener);
+	}
+	
+	@Override
+	public final boolean isXAxisInverted() {
+		return this.isXAxisInverted;
+	}
+	
+	@Override
+	public final boolean isYAxisInverted() {
+		return this.isYAxisInverted;
+	}
+
+	/** Invert or not the X axis.
+	 * <p>
+	 * If the X axis is inverted, the positives are to the left;
+	 * otherwise they are to the right (default UI standard).
+	 * 
+	 * @param invert
+	 */
+	public final void setXAxisInverted(boolean invert) {
+		if (invert!=this.isXAxisInverted) {
+			this.isXAxisInverted = invert;
+			onUpdateViewParameters();
+			repaint();
+		}
+	}
+	
+	/** Invert or not the Y axis.
+	 * <p>
+	 * If the Y axis is inverted, the positives are to the left;
+	 * otherwise they are to the right (default UI standard).
+	 * 
+	 * @param invert
+	 */
+	public final void setYAxisInverted(boolean invert) {
+		if (invert!=this.isYAxisInverted) {
+			this.isYAxisInverted = invert;
+			onUpdateViewParameters();
+			repaint();
+		}
+	}
+
+	@Override
+	public final void setOnLongClickListener(OnLongClickListener l) {
+		this.clickListener.onLongClickListener = l;
+	}
+
+	@Override
+	public final void setOnClickListener(OnClickListener l) {
+		this.clickListener.onClickListener = l;
+	}
+
+	/** Set if the repaint requests are ignored or not.
+	 * 
+	 * @param ignore
+	 */
+	public final void setIgnoreRepaint(boolean ignore) {
+		this.isIgnoreRepaint = ignore;
+	}
+
+	/** Replies if the repaint requests are ignored or not.
+	 * 
+	 * @return <code>true</code> if the repaint requests are ignored;
+	 * <code>false</code> if not.
+	 */
+	public final boolean isIgnoreRepaint() {
+		return this.isIgnoreRepaint;
+	}
+
+	/** Invalidate this view wherever this function is invoked.
+	 * If the current thread is the UI-thread, {@link #invalidate()}
+	 * is invoked. If not, {@link #postInvalidate()} is invoked.
+	 * <p>
+	 * If {@link #isIgnoreRepaint()} replies <code>false</code>, this
+	 * function does nothing.
+	 * 
+	 * @see #isIgnoreRepaint()
+	 */
+	public final void repaint() {
+		if (!this.isIgnoreRepaint) {
+			if (Looper.getMainLooper() == Looper.myLooper()) {
+				invalidate();
+			}
+			else {
+				postInvalidate();
+			}
+		}
+	}
+
+	/** Invalidate this view wherever this function is invoked.
+	 * If the current thread is the UI-thread, {@link #invalidate()}
+	 * is invoked. If not, {@link #postInvalidate()} is invoked.
+	 * <p>
+	 * If {@link #isIgnoreRepaint()} replies <code>false</code>, this
+	 * function does nothing.
+	 * 
+	 * @param x
+	 * @param y
+	 * @param width
+	 * @param height
+	 * @see #isIgnoreRepaint()
+	 */
+	public final void repaint(float x, float y, float width, float height) {
+		if (!this.isIgnoreRepaint) {
+			int l = (int)logical2pixel_x(x)-5;
+			int t = (int)logical2pixel_y(y)-5;
+			int r = l + (int)logical2pixel_size(width)+10;
+			int b = t + (int)logical2pixel_size(height)+10;
+			if (Looper.getMainLooper() == Looper.myLooper()) {
+				invalidate(l, t, r, b);
+			}
+			else {
+				postInvalidate(l, t, r, b);
+			}
+		}
+	}
+
+	/** Invalidate this view wherever this function is invoked.
+	 * If the current thread is the UI-thread, {@link #invalidate()}
+	 * is invoked. If not, {@link #postInvalidate()} is invoked.
+	 * <p>
+	 * If {@link #isIgnoreRepaint()} replies <code>false</code>, this
+	 * function does nothing.
+	 * 
+	 * @param r
+	 * @see #isIgnoreRepaint()
+	 */
+	public final void repaint(Rectangle2f r) {
+		if (r!=null)
+			repaint(r.getMinX(), r.getMinY(), r.getWidth(), r.getHeight());
+	}
+
+	/** Invalidate this view wherever this function is invoked.
+	 * If the current thread is the UI-thread, {@link #invalidate()}
+	 * is invoked. If not, {@link #postInvalidate()} is invoked.
+	 * <p>
+	 * If {@link #isIgnoreRepaint()} replies <code>false</code>, this
+	 * function does nothing.
+	 * 
+	 * @param r
+	 * @see #isIgnoreRepaint()
+	 */
+	public final void repaint(Rect r) {
+		if (r!=null && !this.isIgnoreRepaint) {
+			if (Looper.getMainLooper() == Looper.myLooper()) {
+				invalidate(r.left, r.top, r.right, r.bottom);
+			}
+			else {
+				postInvalidate(r.left, r.top, r.right, r.bottom);
+			}
+		}
+	}
+
+	/** Invalidate this view wherever this function is invoked.
+	 * If the current thread is the UI-thread, {@link #invalidate()}
+	 * is invoked. If not, {@link #postInvalidate()} is invoked.
+	 * <p>
+	 * If {@link #isIgnoreRepaint()} replies <code>false</code>, this
+	 * function does nothing.
+	 * 
+	 * @param r
+	 * @see #isIgnoreRepaint()
+	 */
+	public final void repaint(RectF r) {
+		if (r!=null) {
+			repaint(r.left, r.top, r.right, r.bottom);
+		}
+	}
+
+	/** Invoked to create the instance of the event handler that must
+	 * be used by this view.
+	 * 
+	 * @return the event handler.
+	 */
+	protected abstract EventListener createEventHandler();
+
+	/** Replies the event handler of the given type.
+	 * 
+	 * @param type
+	 * @return the event handler of the given type.
+	 */
+	public final <T> T getEventHandler(Class<T> type) {
+		if (this.globalListener!=null && type.isInstance(this.globalListener))
+			return type.cast(this.globalListener);
+		throw new IllegalStateException(type.toString());
+	}
+
+	/** Replies if the direction of moving is inverted.
+	 * 
+	 * @return <code>true</code> if the direction of moving
+	 * is inverted; otherwise <code>false</code>.
+	 */
+	public final boolean isMoveDirectionInverted() {
+		return this.isInvertScrollingDirection;
+	}
+
+	/** Set if the direction of moving is inverted.
+	 * 
+	 * @param invert is <code>true</code> if the direction of moving
+	 * is inverted; otherwise <code>false</code>.
+	 */
+	public final void setMoveDirectionInverted(boolean invert) {
+		this.isInvertScrollingDirection = invert;
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float logical2pixel_size(float l) {
+		/*float s = l * this.scaleFactor;
+		if ((l!=0)&&(s==0f)) s = 1f;
+		return s;*/
+		return ZoomableContextUtil.logical2pixel_size(l, this.scaleFactor);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float logical2pixel_x(float l) {
+		/*float dx = l - this.focusX;
+		dx *= getScalingFactor();
+		return getViewportCenterX() + dx;*/
+		return ZoomableContextUtil.logical2pixel_x(l, this.centeringTransform, this.scaleFactor);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float logical2pixel_y(float l) {
+		/*float dy = l - this.focusY;
+		dy *= getScalingFactor();
+		return getViewportCenterY() + dy;*/
+		return ZoomableContextUtil.logical2pixel_y(l, this.centeringTransform, this.scaleFactor);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float pixel2logical_size(float l) {
+		//return l / this.scaleFactor;
+		return ZoomableContextUtil.pixel2logical_size(l, this.scaleFactor);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float pixel2logical_x(float l) {
+		/*float dx = l - getViewportCenterX();
+		dx /= getScalingFactor();
+		return this.focusX + dx;*/
+		return ZoomableContextUtil.pixel2logical_x(l, this.centeringTransform, this.scaleFactor);
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float pixel2logical_y(float l) {
+		/*float dy = l - getViewportCenterY();
+		dy /= getScalingFactor();
+		return this.focusY + dy;*/
+		return ZoomableContextUtil.pixel2logical_y(l, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public PathIterator2f logical2pixel(PathIterator2f p) {
+		return ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public PathIterator2f pixel2logical(PathIterator2f p) {
+		return ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(Segment2f s) {
+		ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(Segment2f s) {
+		ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(RoundRectangle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(RoundRectangle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(Point2f p) {
+		ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(Point2f p) {
+		ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(Ellipse2f e) {
+		ZoomableContextUtil.logical2pixel(e, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(Ellipse2f e) {
+		ZoomableContextUtil.pixel2logical(e, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(Circle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(Circle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void logical2pixel(Rectangle2f r) {
+		ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public void pixel2logical(Rectangle2f r) {
+		ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public Shape2f logical2pixel(Shape2f s) {
+		return ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor);
+	}
+
+	@Override
+	public Shape2f pixel2logical(Shape2f s) {
+		return ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor);
+	}
+
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getScalingSensitivity() {
+		return this.zoomingSensitivity;
+	}
+
+	/** Replies the sensivility of the {@code zoomIn()}
+	 * and {@code zoomOut()} actions.
+	 * 
+	 * @param sensivility
+	 */
+	public final void setScalingSensitivity(float sensivility) {
+		this.zoomingSensitivity = Math.max(sensivility, Float.MIN_NORMAL);
+	}
+	
+	/** Replies the X coordinate of the center of the viewport (in screen coordinate).
+	 * 
+	 * @return the center of the viewport.
+	 */
+	public final float getViewportCenterX() {
+		return getMeasuredWidth()/2f;
+	}
+
+	/** Replies the Y coordinate of the center of the viewport (in screen coordinate).
+	 * 
+	 * @return the center of the viewport.
+	 */
+	public final float getViewportCenterY() {
+		return getMeasuredHeight()/2f;
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getFocusX() {
+		return pixel2logical_x(getViewportCenterX());
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getFocusY() {
+		return pixel2logical_y(getViewportCenterY());
+	}
+
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getScalingFactor() {
+		return this.scaleFactor;
+	}
+
+	/** Set the scaling factor.
+	 * 
+	 * @param factor is the scaling factor.
+	 * @return <code>true</code> if the scaling factor has changed;
+	 * otherwise <code>false</code>.
+	 */
+	public final boolean setScalingFactor(float factor) {
+		if (setScalingFactorAndFocus(Float.NaN, Float.NaN, factor)) {
+			repaint();
+			return true;
+		}
+		return false;
+	}
+
+	/** Set the scaling factor. This function does not repaint.
+	 *
+	 * @param scalingX is the coordinate of the point (on the screen) where the focus occurs.
+	 * @param scalingY is the coordinate of the point (on the screen) where the focus occurs.
+	 * @param factor is the scaling factor.
+	 * @return <code>true</code> if the scaling factor or the focus point has changed;
+	 * otherwise <code>false</code>.
+	 */
+	protected final boolean setScalingFactorAndFocus(float scalingX, float scalingY, float factor) {
+		// Normalize the scaling factor
+		float normalizedFactor = factor;
+		if (normalizedFactor<this.minScaleFactor)
+			normalizedFactor = this.minScaleFactor;
+		if (normalizedFactor>this.maxScaleFactor)
+			normalizedFactor = this.maxScaleFactor;
+		
+		// Determine the new position of the focus.
+		// The new position of the focus depends on the current position,
+		// the new scaling factor and where the scaling occured.
+		if (!Float.isNaN(scalingX) && !Float.isNaN(scalingY)) {
+			// Get screen coordinates
+			float screenCenterX = getViewportCenterX();
+			float screenCenterY = getViewportCenterY();
+			float vectorToScreenCenterX = screenCenterX - scalingX;
+			float vectorToScreenCenterY = screenCenterY - scalingY;
+
+			// Get logical coordinates
+			float sX = pixel2logical_x(scalingX);
+			float sY = pixel2logical_y(scalingY);
+
+			float newX = sX + vectorToScreenCenterX / normalizedFactor;
+			float newY = sY + vectorToScreenCenterY / normalizedFactor;
+			
+			if (normalizedFactor!=this.scaleFactor || newX!=this.focusX || newY!=this.focusY) {
+				this.scaleFactor = normalizedFactor;
+				this.focusX = newX;
+				this.focusY = newY;
+				onUpdateViewParameters();
+				return true;
+			}
+		}
+		else if (normalizedFactor!=this.scaleFactor) {
+			this.scaleFactor = normalizedFactor;
+			onUpdateViewParameters();
+			return true;
+		}
+		
+		return false;
+	}
+
+	/** Zoom the view in.
+	 */
+	public final void zoomIn() {
+		if (onScale(getFocusX(), getFocusY(), getScalingSensitivity())) {
+			repaint();
+		}
+	}
+
+	/** Zoom the view out.
+	 */
+	public final void zoomOut() {
+		if (onScale(getFocusX(), getFocusY(), 1f/getScalingSensitivity())) {
+			repaint();
+		}
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getMaxScalingFactor() {
+		return this.maxScaleFactor;
+	}
+
+	/** Set the maximal scaling factor allowing in the view
+	 * 
+	 * @param factor is the maximal scaling factor.
+	 */
+	public final void setMaxScalingFactor(float factor) {
+		if (factor>0f && factor!=this.maxScaleFactor) {
+			this.maxScaleFactor = factor;
+			if (this.minScaleFactor>this.maxScaleFactor)
+				this.minScaleFactor = this.maxScaleFactor;
+			if (this.scaleFactor>this.maxScaleFactor)
+				this.scaleFactor = this.maxScaleFactor;
+			repaint();
+		}
+	}
+
+	/** {@inheritDoc}
+	 */
+	@Override
+	public final float getMinScalingFactor() {
+		return this.minScaleFactor;
+	}
+
+	/** Set the minimal scaling factor allowing in the view
+	 * 
+	 * @param factor is the minimal scaling factor.
+	 */
+	public final void setMinScalingFactor(float factor) {
+		if (factor>0f && factor!=this.minScaleFactor) {
+			this.minScaleFactor = factor;
+			if (this.maxScaleFactor<this.minScaleFactor)
+				this.maxScaleFactor = this.minScaleFactor;
+			if (this.scaleFactor<this.minScaleFactor)
+				this.scaleFactor = this.minScaleFactor;
+			repaint();
+		}
+	}
+
+	/** Set the position of the focus point.
+	 * 
+	 * @param x
+	 * @param y
+	 */
+	public final void setFocusPoint(float x, float y) {
+		if (this.focusX!=x || this.focusY!=y) {
+			this.focusX = x;
+			this.focusY = y;
+			onUpdateViewParameters();
+			repaint();
+		}
+	}
+
+	/** Translate the position of the focus point.
+	 * 
+	 * @param dx
+	 * @param dy
+	 */
+	public final void translateFocusPoint(float dx, float dy) {
+		if (dx!=0f || dy!=0f) {
+			this.focusX += dx;
+			this.focusY += dy;
+			onUpdateViewParameters();
+			repaint();
+		}
+	}
+
+	/** Update any viewing parameter according to the
+	 * current value of the focus point and the scaling factor.
+	 * <p>
+	 * This function is invoked when the coordinates of the
+	 * focus point or the scaling factor has been changed to
+	 * ensure that all the drawing attributes are properly set.
+	 */
+	protected void onUpdateViewParameters() {
+		/*float sf = getScalingFactor();
+		float w = getMeasuredWidth() / sf;
+		float h = getMeasuredHeight() / sf;
+		this.translateToCenterX = -(getFocusX() - w/2f);
+		this.translateToCenterY = -(getFocusY() - h/2f);*/
+		float t;
+		
+		t = pixel2logical_size(getViewportCenterX());
+		if (isXAxisInverted()) {
+    		this.centeringTransform.setCenteringX(
+    				true,
+    				-1,
+    				t + this.focusX);
+    	}
+    	else {
+    		this.centeringTransform.setCenteringX(
+    				false,
+    				1,
+    				t - this.focusX);
+    	}
+		
+		t = pixel2logical_size(getViewportCenterY());
+		if (isYAxisInverted()) {
+    		this.centeringTransform.setCenteringY(
+    				true,
+    				-1,
+    				t + this.focusY);
+    	}
+    	else {
+    		this.centeringTransform.setCenteringY(
+    				false,
+    				1,
+    				t - this.focusY);
+    	}
+	}
+
+	/** Replies the preferred position of the focus point.
+	 * 
+	 * @return the preferred position of the focus point.
+	 */
+	protected abstract float getPreferredFocusX();
+
+	/** Replies the preferred position of the focus point.
+	 * 
+	 * @return the preferred position of the focus point.
+	 */
+	protected abstract float getPreferredFocusY();
+
+	/** Reset the view to the default configuration.
+	 * 
+	 * @return <code>true</code> if the view has changed; <code>false</code> otherwise.
+	 */
+	public final boolean resetView() {
+		float px = getPreferredFocusX();
+		float py = getPreferredFocusY();
+		if (this.focusX!=px || this.focusY!=py || getScalingFactor()!=1f) {
+			this.focusX = px;
+			this.focusY = py;
+			if (!setScalingFactor(1f)) {
+				onUpdateViewParameters();
+				repaint(); // Force to refresh the UI
+			}
+
+			toast(getContext().getString(R.string.reset_view_done), false);
+
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+		onUpdateViewParameters();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public final void onDraw(Canvas canvas) {
+		super.onDraw(canvas);
+		if(!isInEditMode()) {
+			onDrawView(canvas, this.scaleFactor, this.centeringTransform);
+		}
+	}
+
+	/** Invoked to paint the view after it is translated and scaled.
+	 * 
+	 * @param canvas is the canvas in which the view must be painted.
+	 * @param scaleFactor is the scaling factor to use for drawing.
+	 * @param centeringTransform is the transform to use to put the draws at the center of the view.
+	 */
+	protected abstract void onDrawView(Canvas canvas, float scaleFactor, CenteringTransform centeringTransform);
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public final boolean onTouchEvent(MotionEvent ev) {
+		// A disabled view that is clickable still consumes the touch
+		// events, it just doesn't respond to them.
+		if (!isEnabled()) return isClickable() || isLongClickable();
+
+		boolean consumed = false;
+		boolean underScalingGesture;
+
+		// Let the ScaleGestureDetector inspect all events.
+		underScalingGesture = this.scaleGestureDetector.onTouchEvent(ev);
+		if (underScalingGesture) {
+			consumed = true;
+		}
+
+		// Let the move gesture detector inspect all events
+		if (this.moveGestureDetector.onTouchEvent(ev,
+				this.scaleGestureDetector.isInProgress(),
+				this.scaleGestureDetector.getFocusX(),
+				this.scaleGestureDetector.getFocusY())) {
+			consumed = true;
+		}
+
+		// Let the inherited function inspect all events for click and long click events.
+		if (!this.scaleGestureDetector.isInProgress() && super.onTouchEvent(ev)) {
+			consumed = true;
+		}
+		
+		return consumed;
+	}
+
+	/** Invoked when the a touch-down event is detected.
+	 * 
+	 * @param e
+	 */
+	protected void onPointerPressed(PointerEvent e) {
+		//
+	}
+
+	/** Invoked when the a touch-up event is detected.
+	 * 
+	 * @param e
+	 */
+	protected void onPointerReleased(PointerEvent e) {
+		//
+	}
+
+	/** Invoked when the pointer is moved with a button down.
+	 * 
+	 * @param e
+	 */
+	protected void onPointerDragged(PointerEvent e) {
+		//
+	}
+
+	/** Invoked when a long-click is detected.
+	 * 
+	 * @param e
+	 */
+	protected void onLongClick(PointerEvent e) {
+		//
+	}
+
+	/** Invoked when a short-click is detected.
+	 * 
+	 * @param e
+	 */
+	protected void onClick(PointerEvent e) {
+		//
+	}
+
+	/**
+	 * Invoked when the view must be scaled.
+	 * <p>
+	 * One of the border effect if this function replies <code>true</code>
+	 * is that the view will be repaint.
+	 * <p>
+	 * The default implementation of this function invokes
+	 * {@link #setScalingFactorAndFocus(float, float, float)}.
+	 * 
+	 * @param focusX is the position of the focal point on the screen.
+	 * @param focusY is the position of the focal point on the screen.
+	 * @param requestedScaleFactor is the new scale factor.
+	 * @return Whether or not the detector should consider this event as handled. 
+	 * If an event was not handled, the detector will continue to accumulate movement 
+	 * until an event is handled. This can be useful if an application, for example, 
+	 * only wants to update scaling factors if the change is greater than 0.01.
+	 * @see #setScalingFactorAndFocus(float, float, float)
+	 */
+	public boolean onScale(float focusX, float focusY, float requestedScaleFactor) {
+		setScalingFactorAndFocus(focusX, focusY, this.scaleFactor * requestedScaleFactor);
+		return true;
+	}
+
+	/** Show a toast message.
+	 * 
+	 * @param message is the message to display
+	 * @param isLong indicates if it is a long-time (<code>true</code>)
+	 * or a short-time (<code>false</code>) message. 
+	 */
+	public final void toast(String message, boolean isLong) {
+		Context context = getContext();
+		if (context instanceof Activity) {
+			((Activity)context).runOnUiThread(new AsynchronousToaster(context, message, isLong));
+		}
+	}
+
+	/** Show a toast message.
+	 * 
+	 * @param message is the message to display
+	 * @param isLong indicates if it is a long-time (<code>true</code>)
+	 * or a short-time (<code>false</code>) message. 
+	 */
+	public final void toast(int message, boolean isLong) {
+		Context context = getContext();
+		if (context instanceof Activity) {
+			((Activity)context).runOnUiThread(new AsynchronousToaster(context, message, isLong));
+		}
+	}
+
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private class ScaleGestureManager extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+		/**
+		 */
+		public ScaleGestureManager() {
+			//
+		}
+		
+		@Override
+		public boolean onScale(ScaleGestureDetector detector) {
+			if (ZoomableView.this.onScale(
+					detector.getFocusX(), detector.getFocusY(),
+					detector.getScaleFactor())) {
+				repaint();
+			}
+			return true;
+		}
+
+	}
+
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private class ClickListener implements OnLongClickListener, OnClickListener {
+
+		public OnLongClickListener onLongClickListener = null;
+		public OnClickListener onClickListener = null;
+		private boolean isClickEnabled = true;
+
+		/**
+		 */
+		public ClickListener() {
+			//
+		}
+
+		/** Reset the internal flags of this click listener.
+		 */
+		public void reset() {
+			this.isClickEnabled = true;
+		}
+
+		@SuppressWarnings("synthetic-access")
+		@Override
+		public boolean onLongClick(View v) {
+			boolean b = false;
+			this.isClickEnabled = false;
+			if (isLongClickable()
+				&& !ZoomableView.this.scaleGestureDetector.isInProgress()) {
+				if (this.onLongClickListener!=null) {
+					b = this.onLongClickListener.onLongClick(v);
+				}
+				if (!b) {
+					PointerEventAndroid e = ZoomableView.this.moveGestureDetector.lastPointerEvent;
+					if (e!=null) {
+						e.unconsume();
+						e.setWhen(System.currentTimeMillis());
+						ZoomableView.this.onLongClick(e);
+						b = e.isConsumed();
+					}
+				}
+			}
+			return b;
+		}
+
+		@SuppressWarnings("synthetic-access")
+		@Override
+		public void onClick(View v) {
+			if (isClickable()
+				&& !ZoomableView.this.scaleGestureDetector.isInProgress()) {
+				if (this.isClickEnabled) {
+					if (this.onClickListener!=null) {
+						this.onClickListener.onClick(v);
+					}
+					PointerEventAndroid e = ZoomableView.this.moveGestureDetector.lastPointerEvent;
+					if (e!=null) {
+						e.unconsume();
+						e.setWhen(System.currentTimeMillis());
+						ZoomableView.this.onClick(e);
+					}
+				}
+			}
+			this.isClickEnabled = true;
+		}
+
+	}
+
+	/** 
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private class MoveGestureDetector {
+
+		/** Coordinate of the last touch.
+		 */
+		private float lastTouchX = Float.NaN;
+
+		/** Coordinate of the last touch.
+		 */
+		private float lastTouchY = Float.NaN;
+
+		/** Indicates if the move gesture is in progress.
+		 */
+		private boolean inProgress = false;
+
+		/** Identifier of the current active pointer.
+		 */
+		private int activePointerId = INVALID_POINTER_ID;
+
+		public PointerEventAndroid lastPointerEvent = null;
+
+		/**
+		 */
+		public MoveGestureDetector() {
+			//
+		}
+
+		/** Invoked for detecting touch gestures.
+		 * 
+		 * @param ev
+		 * @param isScaleGestureInProgression indicates if the scale gesture is under progress
+		 * @param scaleGestureX is the X coordinate of the scale gesture, if under progress; otherwise the value is indetermined.
+		 * @param scaleGestureY is the Y coordinate of the scale gesture, if under progress; otherwise the value is indetermined.
+		 * @return <code>true</code> if the event is consumed;
+		 * <code>false</code> otherwise.
+		 */
+		@SuppressWarnings("synthetic-access")
+		public boolean onTouchEvent(MotionEvent ev,
+				boolean isScaleGestureInProgression,
+				float scaleGestureX,
+				float scaleGestureY) {
+			int pointerIndex, pointerId;
+			float x, y;
+			int action = ev.getActionMasked();
+
+			if (isScaleGestureInProgression) {
+				x = scaleGestureX;
+				y = scaleGestureY;
+			}
+			else {
+				x = ev.getX();
+				y = ev.getY();
+			}
+
+			this.lastPointerEvent = new PointerEventAndroid(
+					ZoomableView.this,
+					x,
+					y,
+					ev);
+
+			boolean consumed = false;
+
+			switch (action) {
+			case MotionEvent.ACTION_DOWN:
+				// The screen was touched. The touch-down event permits
+				// to initialize the attributes.
+				this.activePointerId = INVALID_POINTER_ID;
+				this.lastPointerEvent.unconsume();
+				if (!isScaleGestureInProgression) {
+					onPointerPressed(this.lastPointerEvent);
+				}
+				consumed = this.lastPointerEvent.isConsumed();
+				if (!consumed) {
+					this.inProgress = true;
+					this.activePointerId = ev.getPointerId(0);
+					this.lastTouchX = ev.getX(0);
+					this.lastTouchY = ev.getY(0);
+					consumed = true;
+					this.lastPointerEvent.setXY(this.lastTouchX, this.lastTouchY);
+				}
+				ZoomableView.this.clickListener.reset();
+				break;
+
+			case MotionEvent.ACTION_MOVE:
+				if (this.inProgress) {
+					pointerIndex = ev.findPointerIndex(this.activePointerId);
+					x = ev.getX(pointerIndex);
+					y = ev.getY(pointerIndex);
+
+					this.lastPointerEvent.setXY(x, y);
+					float dx = pixel2logical_size(x - this.lastTouchX);
+					float dy = pixel2logical_size(y - this.lastTouchY);
+
+					this.lastTouchX = x;
+					this.lastTouchY = y;
+
+					if (isMoveDirectionInverted()) {
+						translateFocusPoint(dx, dy);
+					}
+					else {
+						translateFocusPoint(-dx, -dy);
+					}
+
+					// Always consume the event on moves
+					consumed = true;
+				}
+				else if (!isScaleGestureInProgression) {
+					this.lastPointerEvent.unconsume();
+					onPointerDragged(this.lastPointerEvent);
+					consumed = this.lastPointerEvent.isConsumed();
+				}
+				break;
+
+			case MotionEvent.ACTION_UP:
+				this.lastTouchX = ev.getX();
+				this.lastTouchY = ev.getY();
+				if (this.inProgress) {
+					this.inProgress = false;
+					this.activePointerId = INVALID_POINTER_ID;
+					repaint();
+					consumed = true;
+				}
+				else {
+					this.lastPointerEvent.unconsume();
+					if (!isScaleGestureInProgression) {
+						onPointerReleased(this.lastPointerEvent);
+					}
+					consumed = this.lastPointerEvent.isConsumed();
+				}
+				break;
+
+			case MotionEvent.ACTION_CANCEL:
+				this.lastTouchX = ev.getX();
+				this.lastTouchY = ev.getY();
+				if (this.inProgress) {
+					this.inProgress = false;
+					this.activePointerId = INVALID_POINTER_ID;
+					repaint();
+					consumed = true;
+				}
+				else {
+					this.lastPointerEvent.unconsume();
+					if (!isScaleGestureInProgression) {
+						onPointerReleased(this.lastPointerEvent);
+					}
+					consumed = this.lastPointerEvent.isConsumed();
+				}
+				break;
+
+			case MotionEvent.ACTION_POINTER_UP:
+				if (this.inProgress) {
+					pointerIndex = ((ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+					pointerId = ev.getPointerId(pointerIndex);
+					if (pointerId == this.activePointerId) {
+						// This was our active pointer going up. Choose a new
+						// active pointer and adjust accordingly.
+						int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+						this.lastTouchX = ev.getX(newPointerIndex);
+						this.lastTouchY = ev.getY(newPointerIndex);
+						this.activePointerId = ev.getPointerId(newPointerIndex);
+						repaint();
+						consumed = true;
+					}
+					else {
+						this.lastTouchX = ev.getX();
+						this.lastTouchY = ev.getY();
+					}
+				}
+				else {
+					this.lastTouchX = ev.getX();
+					this.lastTouchY = ev.getY();
+				}
+				break;
+
+			default:
+				// The other events are ignored.
+			}
+
+			return consumed;
+		}
+
+	} // class MoveGestureDetector
+
+	/**
+	 * @author $Author: galland$
+	 * @version $Name$ $Revision$ $Date$
+	 * @mavengroupid $GroupId$
+	 * @mavenartifactid $ArtifactId$
+	 */
+	private static class AsynchronousToaster implements Runnable {
+
+		private final Context context;
+		private final String message;
+		private final int messageId;
+		private final boolean isLong;
+
+		/**
+		 * @param context
+		 * @param message is the message to toast
+		 * @param isLong indicates if it is a long-time (<code>true</code>)
+		 * or a short-time (<code>false</code>) message. 
+		 */
+		public AsynchronousToaster(Context context, String message, boolean isLong) {
+			this.context = context;
+			this.message = message;
+			this.messageId = -1;
+			this.isLong = isLong;
+		}
+
+		/**
+		 * @param context
+		 * @param message is the message to toast
+		 * @param isLong indicates if it is a long-time (<code>true</code>)
+		 * or a short-time (<code>false</code>) message. 
+		 */
+		public AsynchronousToaster(Context context, int message, boolean isLong) {
+			this.context = context;
+			this.message = null;
+			this.messageId = message;
+			this.isLong = isLong;
+		}
+
+		@Override
+		public void run() {
+			if (this.message!=null && !this.message.isEmpty()) {
+				Toast.makeText(this.context, this.message,
+						this.isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
+			}
+			else if (this.messageId>=0) {
+				Toast.makeText(this.context, this.messageId,
+						this.isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
+			}
+		}
+	}
+
+}


Mail converted by MHonArc 2.6.19+ http://listengine.tuxfamily.org/