Merge: app.nit: navigate between windows with the back button
[nit.git] / lib / android / ui / ui.nit
index a37e572..1c40827 100644 (file)
@@ -1,7 +1,5 @@
 # This file is part of NIT (http://www.nitlanguage.org).
 #
-# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
-#
 # 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
 # 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:
+#
+# We cannot rely on `Activity::on_restore_instance_state` to implement
+# `on_restore_state` is it only invoked if there is a bundled state,
+# and we don't use the Android bundled state.
 
 import native_ui
+import log
+import nit_activity
+
+import app::ui
+private import data_store
+import assets
+
+redef class Control
+       # The Android element used to implement `self`
+       fun native: NATIVE is abstract
 
-# An event from the `app.nit` framework
-interface AppEvent
-       # Reaction to this event
-       fun react do end
+       # Type of `native`
+       type NATIVE: JavaObject
 end
 
-# A control click event
-class ClickEvent
-       super AppEvent
+redef class NativeActivity
 
-       # Sender of this event
-       var sender: Button
+       private fun remove_title_bar in "Java" `{
+               self.requestWindowFeature(android.view.Window.FEATURE_NO_TITLE);
+       `}
 
-       redef fun react do sender.click self
-end
+       # 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);
+       `}
 
-# Receiver of events not handled directly by the sender
-interface EventCatcher
-       fun catch_event(event: AppEvent) do end
+       # 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
-       super EventCatcher
-end
+       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
 
-# An `Object` that raises events
-abstract class Eventful
-       var event_catcher: EventCatcher = app is lazy, writable
+       redef fun window=(window)
+       do
+               native_activity.show_fragment(root_layout_id, window.native)
+               super
+       end
 end
 
-#
-## Nity classes and services
-#
+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
 
-# An Android control with text
-abstract class TextView
-       super Finalizable
-       super Eventful
+               return false
+       end
+end
 
-       # Native Java variant to this Nity class
-       type NATIVE: NativeTextView
+# On Android, a window is implemented with the fragment `native`
+redef class Window
+       redef var native = (new Android_app_Fragment(self)).new_global_ref
 
-       # The native Java object encapsulated by `self`
-       var native: NATIVE is noinit
+       redef type NATIVE: Android_app_Fragment
 
-       # Get the text of this view
-       fun text: String
+       # Root high-level view of this window
+       var view: nullable View = null
+
+       redef fun add(item)
        do
-               var jstr = native.text
-               var str = jstr.to_s
-               jstr.delete_local_ref
-               return str
+               if item isa View then view = item
+               super
        end
 
-       # Set the text of this view
-       fun text=(value: Text)
+       private fun on_create_fragment: NativeView
        do
-               var jstr = value.to_s.to_java_string
-               native.text = jstr
-               jstr.delete_local_ref
+               on_create
+
+               var view = view
+               assert view != null else print_error "{class_name} needs a `view` after `Window::on_create` returns"
+               return view.native
        end
+end
 
-       # Get whether this view is enabled or not
-       fun enabled: Bool do return native.enabled
+redef class View
+       redef type NATIVE: NativeView
 
-       # Set if this view is enabled
-       fun enabled=(val: Bool) do native.enabled = val
+       redef fun enabled=(enabled) do native.enabled = enabled or else true
+       redef fun enabled do return native.enabled
+end
 
-       # Set the size of the text in this view at `dpi`
-       fun text_size=(dpi: Numeric) do native.text_size = dpi.to_f
+redef class Layout
+       redef type NATIVE: NativeViewGroup
 
-       private var finalized = false
-       redef fun finalize
+       redef fun add(item)
        do
-               if not finalized then
-                       native.delete_global_ref
-                       finalized = true
-               end
+               super
+
+               assert item isa View
+
+               # FIXME abstract the use either homogeneous or weight to balance views size in a layout
+               native.add_view_with_weight(item.native, 1.0)
+       end
+
+       redef fun remove(item)
+       do
+               super
+               if item isa View then native.remove_view item.native
+       end
+end
+
+redef class HorizontalLayout
+       redef var native do
+               var layout = new NativeLinearLayout(app.native_activity)
+               layout = layout.new_global_ref
+               layout.set_horizontal
+               return layout
        end
 end
 
-# An Android button
-class Button
-       super TextView
+redef class VerticalLayout
+       redef var native do
+               var layout = new NativeLinearLayout(app.native_activity)
+               layout = layout.new_global_ref
+               layout.set_vertical
+               return layout
+       end
+end
 
-       redef type NATIVE: NativeButton
+redef class ListLayout
+       redef type NATIVE: Android_widget_ListView
 
-       init
+       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
-               var native = new NativeButton(app.native_activity, self)
-               self.native = native.new_global_ref
+               super
+               if item isa View then adapter.add item.native
        end
 
-       # Click event
-       #
-       # By default, this method calls `app.catch_event`. It can be specialized
-       # with custom behavior or the receiver of `catch_event` can be changed
-       # with `event_catcher=`.
-       fun click(event: AppEvent) do event_catcher.catch_event(event)
+       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 int final_sender_object = 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
+
+       redef fun text do return native.text.to_s
+       redef fun text=(value) do
+               if value == null then value = ""
+               native.text = value.to_java_string
+       end
+
+       redef fun size=(size) do set_size_native(app.native_activity, native, size or else 1.0)
+
+       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;
 
-       private fun click_from_native do click(new ClickEvent(self))
+               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);
+       `}
 end
 
-# An Android editable text field
-class EditText
-       super TextView
+redef class Label
+       redef type NATIVE: NativeTextView
+       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 int 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
 
-       init
+       redef fun is_password=(value)
        do
-               var native = new NativeEditText(app.activities.first.native)
-               self.native = native.new_global_ref
+               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
+       super Finalizable
+
+       redef type NATIVE: NativeButton
+       redef var native = (new NativeButton(app.native_activity, self)).new_global_ref
+
+       private fun on_click do notify_observers new ButtonPressEvent(self)
+
+       redef fun finalize do native.delete_global_ref
+end
+
 redef class NativeButton
-       new (context: NativeActivity, sender_object: Object)
-       import Button.click_from_native in "Java" `{
+       private new (context: NativeActivity, sender_object: Button)
+       import Button.on_click in "Java" `{
                final int final_sender_object = sender_object;
+               Button_incr_ref(final_sender_object);
 
                return new android.widget.Button(context){
                        @Override
                        public boolean onTouchEvent(android.view.MotionEvent event) {
                                if(event.getAction() == android.view.MotionEvent.ACTION_DOWN) {
-                                       Button_click_from_native(final_sender_object);
+                                       Button_on_click(final_sender_object);
                                        return true;
                                }
                                return false;
@@ -152,3 +332,19 @@ redef class NativeButton
                };
        `}
 end
+
+redef class Android_app_Fragment
+       private new (nit_window: Window)
+       import Window.on_create_fragment in "Java" `{
+               final int final_nit_window = 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