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>

1  2 
lib/android/ui/ui.nit
lib/app/ui.nit
lib/ios/ui/ui.nit
lib/linux/ui.nit

diff --combined lib/android/ui/ui.nit
  # 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 +81,19 @@@ redef class Ap
        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
@@@ -240,26 -256,9 +256,26 @@@ en
  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
diff --combined lib/app/ui.nit
@@@ -18,8 -18,9 +18,8 @@@ module u
  import app_base
  
  # Platform variations
 -# TODO: move on the platform once qualified names are understand in the condition
  import linux::ui is conditional(linux)
 -import android::ui is conditional(android) # FIXME it should be conditional to `android::platform`
 +import android::ui is conditional(android)
  import ios::ui is conditional(ios)
  
  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 -165,12 +164,12 @@@ en
  # 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`
@@@ -210,29 -241,12 +240,29 @@@ class CheckBo
        var is_checked = false is writable
  end
  
 +# Event sent from a `VIEW`
 +class ViewEvent
 +      super AppEvent
 +
 +      # The `VIEW` that raised this event
 +      var sender: VIEW
 +
 +      # Type of the `sender`
 +      type VIEW: View
 +end
 +
  # A `Button` press event
  class ButtonPressEvent
 -      super AppEvent
 +      super ViewEvent
 +
 +      redef type VIEW: Button
 +end
 +
 +# The `CheckBox` `sender` has been toggled
 +class ToggleEvent
 +      super ViewEvent
  
 -      # The `Button` that raised this event
 -      var sender: Button
 +      redef type VIEW: CheckBox
  end
  
  # A layout to visually organize `Control`s
diff --combined lib/ios/ui/ui.nit
@@@ -25,16 -25,16 +25,16 @@@ in "ObjC" `
  @interface NitCallbackReference: NSObject
  
        // Nit object target of the callbacks from UI events
 -      @property (nonatomic) Button nit_button;
 +      @property (nonatomic) View nit_view;
  
        // Actual callback method
 -      -(void) nitOnEvent: (UIButton*) sender;
 +      -(void) nitOnEvent: (UIView*) sender;
  @end
  
  @implementation NitCallbackReference
  
 -      -(void) nitOnEvent: (UIButton*) sender {
 -              Button_on_click(self.nit_button);
 +      -(void) nitOnEvent: (UIView*) sender {
 +              View_on_ios_event(self.nit_view);
        }
  @end
  
@@@ -136,8 -136,6 +136,8 @@@ redef class Vie
        redef type NATIVE: UIView
  
        redef var enabled = null is lazy
 +
 +      private fun on_ios_event do end
  end
  
  redef class CompositeControl
@@@ -182,7 -180,6 +182,6 @@@ redef class Layou
        init
        do
                native.alignment = new UIStackViewAlignment.fill
-               native.distribution = new UIStackViewDistribution.fill_equally
  
                # TODO make customizable
                native.spacing = 4.0
  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
@@@ -255,10 -260,7 +262,10 @@@ redef class CheckBo
        # `UISwitch` acting as the real check box
        var ui_switch: UISwitch is noautoinit
  
 -      init do
 +      redef fun on_ios_event do notify_observers new ToggleEvent(self)
 +
 +      init
 +      do
                # Tweak the layout so it is centered
                layout.native.distribution = new UIStackViewDistribution.fill_proportionally
                layout.native.alignment = new UIStackViewAlignment.center
                var s = new UISwitch
                native.add_arranged_subview s
                ui_switch = s
 +
 +              ui_switch.set_callback self
        end
  
        redef fun text=(text) do lbl.text = text
        redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
  end
  
 +redef class UISwitch
 +      # Register callbacks on this switch to be relayed to `sender`
 +      private fun set_callback(sender: View)
 +      import View.on_ios_event in "ObjC" `{
 +
 +              NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
 +              ncr.nit_view = sender;
 +
 +              // Pin the objects in both Objective-C and Nit GC
 +              View_incr_ref(sender);
 +              ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
 +
 +              [self addTarget:ncr action:@selector(nitOnEvent:)
 +                      forControlEvents:UIControlEventValueChanged];
 +      `}
 +end
 +
  redef class TextInput
  
        redef type NATIVE: UITextField
@@@ -317,25 -300,25 +324,25 @@@ redef class Butto
  
        init do native.set_callback self
  
 +      redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
 +
        redef fun text=(text) do if text != null then native.title = text.to_nsstring
        redef fun text do return native.current_title.to_s
  
 -      private fun on_click do notify_observers new ButtonPressEvent(self)
 -
        redef fun enabled=(enabled) do native.enabled = enabled or else true
        redef fun enabled do return native.enabled
  end
  
  redef class UIButton
        # Register callbacks on this button to be relayed to `sender`
 -      private fun set_callback(sender: Button)
 -      import Button.on_click in "ObjC" `{
 +      private fun set_callback(sender: View)
 +      import View.on_ios_event in "ObjC" `{
  
                NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
 -              ncr.nit_button = sender;
 +              ncr.nit_view = sender;
  
                // Pin the objects in both Objective-C and Nit GC
 -              Button_incr_ref(sender);
 +              View_incr_ref(sender);
                ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
  
                [self addTarget:ncr action:@selector(nitOnEvent:)
diff --combined lib/linux/ui.nit
@@@ -43,7 -43,7 +43,7 @@@ redef class Ap
                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 +55,9 @@@
                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 +67,6 @@@
                app.on_start
                app.on_resume
  
-               native_window.show_all
                gtk_main
  
                app.on_pause
                # 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 +251,21 @@@ redef class Butto
        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("")
@@@ -294,9 -317,6 +317,9 @@@ redef class CheckBo
        redef type NATIVE: GtkCheckButton
        redef var native = new GtkCheckButton
  
 +      redef fun signal(sender, data) do notify_observers new ToggleEvent(self)
 +      init do native.signal_connect("toggled", self, null)
 +
        redef fun text do return native.text
        redef fun text=(value) do native.text = (value or else "").to_s