Merge: app.nit: navigate between windows with the back button
authorJean Privat <jean@pryen.org>
Tue, 17 May 2016 20:17:43 +0000 (16:17 -0400)
committerJean Privat <jean@pryen.org>
Tue, 17 May 2016 20:17:43 +0000 (16:17 -0400)
This should be the last big feature of _app.nit_ needed for my thesis!

Intro services to navigate between multiple windows of the same app.
Windows are added to a stack and popped back to the visible state on pressing the back key.
This behavior was implemented for Android by intercepting events raised by the "hardware" back key.
iOS offers the same features natively so there is no adaptation needed as of now.
On GNU/Linux with GTK+, a button is added to the window header when there's a window to go back to.

These feature will probably have to be tweaked in the future, but they are enough for the Benilux client app. Notably, the life-cylce of each individual window has to be reconsidered and we will probably need some adaptation on iOS to have a better integration.

This PR also fixes a few bugs. The `EditText` losing focus in a `ListLayout` is fixed by a change to the Android manifest file, and vertical layout should now look better on iOS.

I took the opportunity to add other key events while working on the Android back key support.
These will be useful when migrating games from the old low-level implementation on the NDK to our custom `NitActivity.java`.

Pull-Request: #2100
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Romain Chanoir <romain.chanoir@viacesi.fr>

contrib/tnitter/src/tnitter_app.nit
examples/calculator/src/calculator.nit
lib/android/NitActivity.java
lib/android/key_event.nit [new file with mode: 0644]
lib/android/nit_activity.nit
lib/android/ui/ui.nit
lib/app/ui.nit
lib/ios/ui/ui.nit
lib/linux/ui.nit

index fccad93..7a47218 100644 (file)
@@ -42,7 +42,7 @@ redef class App
        redef fun on_create
        do
                # Create the main window
-               window = new TnitterWindow
+               push_window new TnitterWindow
                super
        end
 end
index 4068f34..c9db316 100644 (file)
@@ -38,7 +38,7 @@ redef class App
                if debug then print "App::on_create"
 
                # Create the main window
-               window = new CalculatorWindow
+               push_window new CalculatorWindow
                super
        end
 end
index 3744988..cbe6ad4 100644 (file)
@@ -17,6 +17,7 @@ package nit.app;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.view.KeyEvent;
 
 /*
  * Entry point to Nit applications on Android, redirect most calls to Nit
@@ -47,6 +48,11 @@ public class NitActivity extends Activity {
        protected native void nitOnDestroy(int activity);
        protected native void nitOnSaveInstanceState(int activity, Bundle savedInstanceState);
        protected native void nitOnRestoreInstanceState(int activity, Bundle savedInstanceState);
+       protected native boolean nitOnBackPressed(int activity);
+       protected native boolean nitOnKeyDown(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyLongPress(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyMultiple(int activity, int keyCode, int count, KeyEvent event);
+       protected native boolean nitOnKeyUp(int activity, int keyCode, KeyEvent event);
 
        /*
         * Implementation of OS callbacks
@@ -108,4 +114,34 @@ public class NitActivity extends Activity {
                super.onRestoreInstanceState(savedInstanceState);
                nitOnRestoreInstanceState(nitActivity, savedInstanceState);
        }
+
+       @Override
+       public void onBackPressed() {
+               if (!nitOnBackPressed(nitActivity))
+                       super.onBackPressed();
+       }
+
+       @Override
+       public boolean onKeyDown(int keyCode, KeyEvent event) {
+               return nitOnKeyDown(nitActivity, keyCode, event)
+                       || super.onKeyDown(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+               return nitOnKeyLongPress(nitActivity, keyCode, event)
+                       || super.onKeyLongPress(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+               return nitOnKeyMultiple(nitActivity, keyCode, count, event)
+                       || super.onKeyMultiple(keyCode, count, event);
+       }
+
+       @Override
+       public boolean onKeyUp(int keyCode, KeyEvent event) {
+               return nitOnKeyUp(nitActivity, keyCode, event)
+                       || super.onKeyUp(keyCode, event);
+       }
 }
diff --git a/lib/android/key_event.nit b/lib/android/key_event.nit
new file mode 100644 (file)
index 0000000..b5b39e4
--- /dev/null
@@ -0,0 +1,192 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# 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.
+
+module key_event
+
+import platform
+
+# Java class: android.view.KeyEvent
+extern class NativeKeyEvent in "Java" `{ android.view.KeyEvent `}
+       super JavaObject
+
+       # Java implementation: boolean android.view.KeyEvent.isSystem()
+       fun is_system: Bool in "Java" `{
+               return self.isSystem();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.setSource(int)
+       fun set_source(arg0: Int) in "Java" `{
+               self.setSource((int)arg0);
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getMetaState()
+       fun meta_state: Int in "Java" `{
+               return self.getMetaState();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getModifiers()
+       fun modifiers: Int in "Java" `{
+               return self.getModifiers();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getFlags()
+       fun flags: Int in "Java" `{
+               return self.getFlags();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasNoModifiers()
+       fun has_no_modifiers: Bool in "Java" `{
+               return self.hasNoModifiers();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasModifiers(int)
+       fun has_modifiers(arg0: Int): Bool in "Java" `{
+               return self.hasModifiers((int)arg0);
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isAltPressed()
+       fun is_alt_pressed: Bool in "Java" `{
+               return self.isAltPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isShiftPressed()
+       fun is_shift_pressed: Bool in "Java" `{
+               return self.isShiftPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isSymPressed()
+       fun is_sym_pressed: Bool in "Java" `{
+               return self.isSymPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCtrlPressed()
+       fun is_ctrl_pressed: Bool in "Java" `{
+               return self.isCtrlPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isMetaPressed()
+       fun is_meta_pressed: Bool in "Java" `{
+               return self.isMetaPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isFunctionPressed()
+       fun is_function_pressed: Bool in "Java" `{
+               return self.isFunctionPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCapsLockOn()
+       fun is_caps_lock_on: Bool in "Java" `{
+               return self.isCapsLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isNumLockOn()
+       fun is_num_lock_on: Bool in "Java" `{
+               return self.isNumLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isScrollLockOn()
+       fun is_scroll_lock_on: Bool in "Java" `{
+               return self.isScrollLockOn();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getAction()
+       fun action: Int in "Java" `{
+               return self.getAction();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCanceled()
+       fun is_canceled: Bool in "Java" `{
+               return self.isCanceled();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.startTracking()
+       fun start_tracking in "Java" `{
+               self.startTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isTracking()
+       fun is_tracking: Bool in "Java" `{
+               return self.isTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isLongPress()
+       fun is_long_press: Bool in "Java" `{
+               return self.isLongPress();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getKeyCode()
+       fun key_code: Int in "Java" `{
+               return self.getKeyCode();
+       `}
+
+       # Java implementation: java.lang.String android.view.KeyEvent.getCharacters()
+       fun characters: JavaString in "Java" `{
+               return self.getCharacters();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getScanCode()
+       fun scan_code: Int in "Java" `{
+               return self.getScanCode();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getRepeatCount()
+       fun repeat_count: Int in "Java" `{
+               return self.getRepeatCount();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getDownTime()
+       fun down_time: Int in "Java" `{
+               return self.getDownTime();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getEventTime()
+       fun event_time: Int in "Java" `{
+               return self.getEventTime();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getDisplayLabel()
+       fun display_label: Char in "Java" `{
+               return self.getDisplayLabel();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getUnicodeChar()
+       fun unicode_char: Int in "Java" `{
+               return self.getUnicodeChar();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getNumber()
+       fun number: Char in "Java" `{
+               return self.getNumber();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isPrintingKey()
+       fun is_printing_key: Bool in "Java" `{
+               return self.isPrintingKey();
+       `}
+
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeKeyEvent_sys(self);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, self);
+       `}
+
+       redef fun pop_from_local_frame_with_env(jni_env) `{
+               return (*jni_env)->PopLocalFrame(jni_env, self);
+       `}
+end
+
+# Java getter: android.view.KeyEvent.KEYCODE_BACK
+fun android_view_key_event_keycode_back: Int in "Java" `{
+       return android.view.KeyEvent.KEYCODE_BACK;
+`}
index 87d9d22..1668147 100644 (file)
@@ -43,6 +43,7 @@ end
 import platform
 import log
 import activities
+import key_event
 import bundle
 import dalvik
 
@@ -133,6 +134,36 @@ in "C body" `{
        {
                Activity_on_restore_instance_state((Activity)nit_activity, saved_state);
        }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnBackPressed
+         (JNIEnv *env, jobject java_activity, jint nit_activity)
+       {
+               return (jboolean)Activity_on_back_pressed((Activity)nit_activity);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyDown
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_down((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyLongPress
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_long_press((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyMultiple
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jint count, jobject event)
+       {
+               return (jboolean)Activity_on_key_multiple((Activity)nit_activity, keyCode, count, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyUp
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_up((Activity)nit_activity, keyCode, event);
+       }
 `}
 
 # Wrapper to our Java `NitActivity`
@@ -158,7 +189,10 @@ redef class App
        Activity.on_create, Activity.on_destroy,
        Activity.on_start, Activity.on_restart, Activity.on_stop,
        Activity.on_pause, Activity.on_resume,
-       Activity.on_save_instance_state, Activity.on_restore_instance_state `{
+       Activity.on_save_instance_state, Activity.on_restore_instance_state,
+       Activity.on_back_pressed,
+       Activity.on_key_down, Activity.on_key_long_press,
+       Activity.on_key_multiple, Activity.on_key_up `{
                App_incr_ref(self);
                global_app = self;
        `}
@@ -251,6 +285,31 @@ class Activity
 
        # Notification from Android, the current device configuration has changed
        fun on_configuration_changed do end
+
+       # The back key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_back_pressed: Bool do return false
+
+       # A key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_down(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been long pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_long_press(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # Multiple down/up pairs of the same key have occurred in a row
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_multiple(key_code, count: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been released
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_up(key_code: Int, event: NativeKeyEvent): Bool do return false
 end
 
 # Set up global data in C and leave it to Android to callback Java, which we relay to Nit
index e9ea641..1c40827 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:
 #
@@ -78,6 +81,19 @@ redef class App
        end
 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 = (new Android_app_Fragment(self)).new_global_ref
index 1b2b7ae..0afe7aa 100644 (file)
@@ -27,8 +27,32 @@ redef class App
 
        # The current `Window` of this activity
        #
-       # This attribute must be set by refinements of `App`.
-       var window: Window is writable
+       # This attribute is set by `push_window`.
+       var window: Window is noinit
+
+       # Make visible and push `window` on the top of `pop_window`
+       #
+       # This method must be called at least once within `App::on_create`.
+       # It can be called at any times while the app is active.
+       fun push_window(window: Window)
+       do
+               window_stack.add window
+               self.window = window
+       end
+
+       # Pop the current `window` from the stack and show the previous one
+       #
+       # Require: `window_stack.not_empty`
+       fun pop_window
+       do
+               assert window_stack.not_empty
+               window_stack.pop
+               window = window_stack.last
+               window.on_resume
+       end
+
+       # Stack of active windows
+       var window_stack = new Array[Window]
 
        redef fun on_create do window.on_create
 
@@ -140,6 +164,12 @@ end
 # A window, root of the `Control` tree
 class Window
        super CompositeControl
+
+       # Should the back button be shown and used to go back to a previous window?
+       fun enable_back_button: Bool do return app.window_stack.length > 1
+
+       # The back button has been pressed, usually to open the previous window
+       fun on_back_button do app.pop_window
 end
 
 # A viewable `Control`
index ad4aaac..acf3018 100644 (file)
@@ -182,7 +182,6 @@ redef class Layout
        init
        do
                native.alignment = new UIStackViewAlignment.fill
-               native.distribution = new UIStackViewDistribution.fill_equally
 
                # TODO make customizable
                native.spacing = 4.0
@@ -199,11 +198,19 @@ redef class Layout
 end
 
 redef class HorizontalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.horizontal
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.horizontal
+               native.distribution = new UIStackViewDistribution.fill_equally
+       end
 end
 
 redef class VerticalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.vertical
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.vertical
+               native.distribution = new UIStackViewDistribution.equal_spacing
+       end
 end
 
 redef class Label
index fa850d4..1e26f17 100644 (file)
@@ -43,7 +43,7 @@ redef class App
                bar.title = "app.nit" # TODO offer a portable API to name windows
                bar.show_close_button = true
 
-               # TODO add back button
+               bar.add back_button.native
 
                return bar
        end
@@ -55,6 +55,9 @@ redef class App
                return stack
        end
 
+       # Button on the header bar to go back
+       var back_button = new BackButton is lazy
+
        # On GNU/Linux, we go through all the callbacks once,
        # there is no complex life-cycle.
        redef fun run
@@ -64,7 +67,6 @@ redef class App
                app.on_start
                app.on_resume
 
-               native_window.show_all
                gtk_main
 
                app.on_pause
@@ -88,7 +90,13 @@ redef class App
                # improved with GTK 3.18 and interpolate_size.
                native_window.resizable = false
 
+               native_window.show_all
+
                super
+
+               if window.enable_back_button then
+                       back_button.native.show
+               else back_button.native.hide
        end
 end
 
@@ -243,6 +251,21 @@ redef class Button
        init do native.signal_connect("clicked", self, null)
 end
 
+# Button to go back between windows
+class BackButton
+       super Button
+
+       # TODO i18n
+       redef fun text=(value) do super(value or else "Back")
+
+       redef fun signal(sender, data)
+       do
+               super
+
+               app.window.on_back_button
+       end
+end
+
 redef class Label
        redef type NATIVE: GtkLabel
        redef var native = new GtkLabel("")