android & benitlux: use NitObject in clients
[nit.git] / lib / android / ui / ui.nit
index 75e1a12..767039b 100644 (file)
 # limitations under the License.
 
 # Views and services to use the Android native user interface
-module ui
+module ui is
+       # `adjustPan` allows to use EditText in a ListLayout
+       android_manifest_activity """android:windowSoftInputMode="adjustPan""""
+end
 
 # Implementation note:
 #
@@ -27,6 +30,7 @@ import nit_activity
 
 import app::ui
 private import data_store
+import assets
 
 redef class Control
        # The Android element used to implement `self`
@@ -36,18 +40,92 @@ redef class Control
        type NATIVE: JavaObject
 end
 
+redef class NativeActivity
+
+       private fun remove_title_bar in "Java" `{
+               self.requestWindowFeature(android.view.Window.FEATURE_NO_TITLE);
+       `}
+
+       # Insert a single layout as the root of the activity window
+       private fun insert_root_layout(root_layout_id: Int)
+       in "Java" `{
+               android.widget.FrameLayout layout = new android.widget.FrameLayout(self);
+               layout.setId((int)root_layout_id);
+               self.setContentView(layout);
+       `}
+
+       # Replace the currently visible fragment, if any, with `native_fragment`
+       private fun show_fragment(root_layout_id: Int, native_fragment: Android_app_Fragment)
+       in "Java" `{
+               android.app.FragmentTransaction transaction = self.getFragmentManager().beginTransaction();
+               transaction.replace((int)root_layout_id, native_fragment);
+               transaction.commit();
+       `}
+end
+
+redef class App
+       redef fun on_create
+       do
+               app.native_activity.remove_title_bar
+               native_activity.insert_root_layout(root_layout_id)
+               super
+       end
+
+       # Identifier of the container holding the fragments
+       private var root_layout_id = 0xFFFF
+
+       redef fun window=(window)
+       do
+               native_activity.show_fragment(root_layout_id, window.native)
+               super
+       end
+
+       redef fun on_start do window.on_start
+
+       redef fun on_destroy do window.on_destroy
+end
+
+redef class CompositeControl
+       redef fun on_start do for i in items do i.on_start
+
+       redef fun on_destroy do for i in items do i.on_destroy
+end
+
+redef class Activity
+       redef fun on_back_pressed
+       do
+               var window = app.window
+               if window.enable_back_button then
+                       window.on_back_button
+                       return true
+               end
+
+               return false
+       end
+end
+
+# On Android, a window is implemented with the fragment `native`
 redef class Window
-       redef var native = app.native_activity.new_global_ref
+       redef var native = (new Android_app_Fragment(self)).new_global_ref
 
-       redef type NATIVE: NativeActivity
+       redef type NATIVE: Android_app_Fragment
+
+       # Root high-level view of this window
+       var view: nullable View = null
 
        redef fun add(item)
        do
+               if item isa View then view = item
                super
+       end
+
+       private fun on_create_fragment: NativeView
+       do
+               on_create
 
-               # FIXME abstract the Android restriction where `content_view` must be a layout
-               assert item isa Layout
-               native.content_view = item.native
+               var view = view
+               assert view != null else print_error "{class_name} needs a `view` after `Window::on_create` returns"
+               return view.native
        end
 end
 
@@ -96,6 +174,51 @@ redef class VerticalLayout
        end
 end
 
+redef class ListLayout
+       redef type NATIVE: Android_widget_ListView
+
+       redef var native do
+               var layout = new Android_widget_ListView(app.native_activity)
+               layout = layout.new_global_ref
+               return layout
+       end
+
+       private var adapter: Android_widget_ArrayAdapter do
+               var adapter = new Android_widget_ArrayAdapter(app.native_activity,
+                       android_r_layout_simple_list_item_1, self)
+               native.set_adapter adapter
+               return adapter.new_global_ref
+       end
+
+       redef fun add(item)
+       do
+               super
+               if item isa View then adapter.add item.native
+       end
+
+       private fun create_view(position: Int): NativeView
+       do
+               var ctrl = items[position]
+               assert ctrl isa View
+               return ctrl.native
+       end
+end
+
+redef class Android_widget_ArrayAdapter
+       private new (context: NativeContext, res: Int, sender: ListLayout)
+       import ListLayout.create_view in "Java" `{
+               final nit.app.NitObject final_sender_object = sender;
+               ListLayout_incr_ref(sender);
+
+               return new android.widget.ArrayAdapter(context, (int)res) {
+                               @Override
+                               public android.view.View getView(int position, android.view.View convertView, android.view.ViewGroup parent) {
+                                       return ListLayout_create_view(final_sender_object, position);
+                               }
+                       };
+       `}
+end
+
 redef class TextView
        redef type NATIVE: NativeTextView
 
@@ -105,13 +228,35 @@ redef class TextView
                native.text = value.to_java_string
        end
 
-       # Size of the text
-       fun text_size: Float do return native.text_size
+       redef fun size=(size) do set_size_native(app.native_activity, native, size or else 1.0)
 
-       # Size of the text
-       fun text_size=(text_size: nullable Float) do
-               if text_size != null then native.text_size = text_size
-       end
+       private fun set_size_native(context: NativeContext, view: NativeTextView, size: Float)
+       in "Java" `{
+               int s;
+               if (size == 1.0d)
+                       s = android.R.style.TextAppearance_Medium;
+               else if (size < 1.0d)
+                       s = android.R.style.TextAppearance_Small;
+               else // if (size > 1.0d)
+                       s = android.R.style.TextAppearance_Large;
+
+               view.setTextAppearance(context, s);
+       `}
+
+       redef fun align=(align) do set_align_native(native, align or else 0.0)
+
+       private fun set_align_native(view: NativeTextView, align: Float)
+       in "Java" `{
+               int g;
+               if (align == 0.5d)
+                       g = android.view.Gravity.CENTER_HORIZONTAL;
+               else if (align < 0.5d)
+                       g = android.view.Gravity.LEFT;
+               else // if (align > 0.5d)
+                       g = android.view.Gravity.RIGHT;
+
+               view.setGravity(g | android.view.Gravity.CENTER_VERTICAL);
+       `}
 end
 
 redef class Label
@@ -119,9 +264,54 @@ redef class Label
        redef var native do return (new NativeTextView(app.native_activity)).new_global_ref
 end
 
+redef class CheckBox
+       redef type NATIVE: Android_widget_CompoundButton
+       redef var native do return (new Android_widget_CheckBox(app.native_activity)).new_global_ref
+       init do set_callback_on_toggle(native)
+
+       redef fun is_checked do return native.is_checked
+       redef fun is_checked=(value) do native.set_checked(value)
+
+       private fun on_toggle do notify_observers new ToggleEvent(self)
+
+       private fun set_callback_on_toggle(view: NATIVE)
+       import on_toggle in "Java" `{
+               final nit.app.NitObject final_sender_object = self;
+               CheckBox_incr_ref(final_sender_object);
+
+               view.setOnCheckedChangeListener(
+                       new android.widget.CompoundButton.OnCheckedChangeListener() {
+                               @Override
+                               public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
+                                       CheckBox_on_toggle(final_sender_object);
+                               }
+                       });
+       `}
+end
+
 redef class TextInput
        redef type NATIVE: NativeEditText
        redef var native = (new NativeEditText(app.native_activity)).new_global_ref
+
+       redef fun is_password=(value)
+       do
+               native.is_password = value or else false
+               super
+       end
+end
+
+redef class NativeEditText
+
+       # Configure this view to hide passwords
+       fun is_password=(value: Bool) in "Java" `{
+               if (value) {
+                       self.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
+                       self.setTransformationMethod(android.text.method.PasswordTransformationMethod.getInstance());
+               } else {
+                       self.setInputType(android.text.InputType.TYPE_CLASS_TEXT);
+                       self.setTransformationMethod(null);
+               }
+       `}
 end
 
 redef class Button
@@ -138,17 +328,52 @@ end
 redef class NativeButton
        private new (context: NativeActivity, sender_object: Button)
        import Button.on_click in "Java" `{
-               final int final_sender_object = sender_object;
+               final nit.app.NitObject final_sender_object = sender_object;
+               Button_incr_ref(final_sender_object);
 
-               return new android.widget.Button(context){
+               return new android.widget.Button(context) {
                        @Override
                        public boolean onTouchEvent(android.view.MotionEvent event) {
-                               if(event.getAction() == android.view.MotionEvent.ACTION_DOWN) {
+                               if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
                                        Button_on_click(final_sender_object);
                                        return true;
+                               } else if (event.getAction() == android.view.MotionEvent.ACTION_DOWN) {
+                                       return true;
                                }
                                return false;
                        }
                };
        `}
 end
+
+redef class Android_app_Fragment
+       private new (nit_window: Window)
+       import Window.on_create_fragment in "Java" `{
+               final nit.app.NitObject final_nit_window = nit_window;
+               Window_incr_ref(nit_window);
+
+               return new android.app.Fragment(){
+                       @Override
+                       public android.view.View onCreateView(android.view.LayoutInflater inflater,
+                               android.view.ViewGroup container, android.os.Bundle state) {
+
+                               return Window_on_create_fragment(final_nit_window);
+                       }
+               };
+       `}
+end
+
+redef class Text
+       redef fun open_in_browser
+       do to_java_string.native_open_in_browser(app.native_activity)
+end
+
+redef class JavaString
+       private fun native_open_in_browser(context: NativeContext)
+       in "Java" `{
+               android.content.Intent intent = new android.content.Intent(
+                       android.content.Intent.ACTION_VIEW,
+                       android.net.Uri.parse(self));
+               context.startActivity(intent);
+       `}
+end