a07718e2a4ee5d9b62151ae61e3975b0c6771728
[nit.git] / lib / android / ui.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Views and services to use the Android native user interface
18 #
19 # Events, such as a button click, come from the UI thread and are then
20 # passed to the main thread. It is recommended to specialize one of the
21 # methods of the main thread to customize the response to a given event.
22 #
23 # This graph shows the path of a button click:
24 # ~~~raw
25 # UI Thread # Main thread
26 #
27 # User
28 # |
29 # V
30 # Button::click_ui --> Button::click
31 # |
32 # V
33 # App::catch_event
34 # ~~~
35 module ui is min_api_version 14
36
37 import native_app_glue
38 import pthreads::concurrent_collections
39
40 in "Java" `{
41 import android.app.Activity;
42
43 import android.view.Gravity;
44 import android.view.MotionEvent;
45 import android.view.ViewGroup;
46 import android.view.ViewGroup.MarginLayoutParams;
47
48 import android.widget.Button;
49 import android.widget.LinearLayout;
50 import android.widget.GridLayout;
51 import android.widget.PopupWindow;
52 import android.widget.TextView;
53
54 import java.lang.*;
55 import java.util.*;
56 `}
57
58 # An event from the `app.nit` framework
59 interface AppEvent
60 # Reaction to this event
61 fun react do end
62 end
63
64 # A control click event
65 class ClickEvent
66 super AppEvent
67
68 # Sender of this event
69 var sender: Button
70
71 redef fun react do sender.click self
72 end
73
74 # Receiver of events not handled directly by the sender
75 interface EventCatcher
76 fun catch_event(event: AppEvent) do end
77 end
78
79 redef class App
80 super EventCatcher
81
82 # Queue of events to be received by the main thread
83 var event_queue = new ConcurrentList[AppEvent]
84
85 # Call `react` on all `AppEvent` available in `event_queue`
86 protected fun loop_on_ui_callbacks
87 do
88 var queue = event_queue
89 while not queue.is_empty do
90 var event = queue.pop
91 event.react
92 end
93 end
94
95 redef fun run
96 do
97 loop
98 # Process Android events
99 poll_looper 100
100
101 # Process app.nit events
102 loop_on_ui_callbacks
103 end
104 end
105 end
106
107 redef extern class NativeActivity
108
109 # Fill this entire `NativeActivity` with `popup`
110 #
111 # This is a workaround for the use on `takeSurface` in `NativeActivity.java`
112 #
113 # TODO replace NativeActivity by our own NitActivity
114 private fun dedicate_to_popup(popup: NativePopupWindow, popup_layout: NativeViewGroup) in "Java" `{
115 final LinearLayout final_main_layout = new LinearLayout(recv);
116 final ViewGroup final_popup_layout = popup_layout;
117 final PopupWindow final_popup = popup;
118 final Activity final_recv = recv;
119
120 recv.runOnUiThread(new Runnable() {
121 @Override
122 public void run() {
123 MarginLayoutParams params = new MarginLayoutParams(
124 LinearLayout.LayoutParams.MATCH_PARENT,
125 LinearLayout.LayoutParams.MATCH_PARENT);
126
127 final_recv.setContentView(final_main_layout, params);
128
129 final_popup.showAtLocation(final_popup_layout, Gravity.TOP, 0, 40);
130 }
131 });
132 `}
133
134 # Set the main layout of this activity
135 fun content_view=(layout: NativeViewGroup)
136 do
137 var popup = new NativePopupWindow(self)
138 popup.content_view = layout
139 dedicate_to_popup(popup, layout)
140 end
141
142 # Set the real content view of this activity, without hack
143 #
144 # TODO bring use this instead of the hack with `dedicate_to_pupup`
145 private fun real_content_view=(layout: NativeViewGroup) in "Java" `{
146 final ViewGroup final_layout = layout;
147 final Activity final_recv = recv;
148
149 recv.runOnUiThread(new Runnable() {
150 @Override
151 public void run() {
152 final_recv.setContentView(final_layout);
153
154 final_layout.requestFocus();
155 }
156 });
157 `}
158 end
159
160 # An `Object` that raises events
161 abstract class Eventful
162 var event_catcher: EventCatcher = app is lazy, writable
163 end
164
165 #
166 ## Nity classes and services
167 #
168
169 # An Android control with text
170 abstract class TextView
171 super Finalizable
172 super Eventful
173
174 # Native Java variant to this Nity class
175 type NATIVE: NativeTextView
176
177 # The native Java object encapsulated by `self`
178 var native: NATIVE is noinit
179
180 # Get the text of this view
181 fun text: String
182 do
183 var jstr = native.text
184 var str = jstr.to_s
185 jstr.delete_local_ref
186 return str
187 end
188
189 # Set the text of this view
190 fun text=(value: Text)
191 do
192 var jstr = value.to_s.to_java_string
193 native.text = jstr
194 jstr.delete_local_ref
195 end
196
197 # Get whether this view is enabled or not
198 fun enabled: Bool do return native.enabled
199
200 # Set if this view is enabled
201 fun enabled=(val: Bool) do native.enabled = val
202
203 # Set the size of the text in this view at `dpi`
204 fun text_size=(dpi: Numeric) do native.text_size = dpi.to_f
205
206 private var finalized = false
207 redef fun finalize
208 do
209 if not finalized then
210 native.delete_global_ref
211 finalized = true
212 end
213 end
214 end
215
216 # An Android button
217 class Button
218 super TextView
219
220 redef type NATIVE: NativeButton
221
222 init
223 do
224 var native = new NativeButton(app.native_activity, app.event_queue, self)
225 self.native = native.new_global_ref
226 end
227
228 # Click event on the Main thread
229 #
230 # By default, this method calls `app.catch_event`. It can be specialized
231 # with custom behavior or the receiver of `catch_event` can be changed
232 # with `event_catcher=`.
233 fun click(event: AppEvent) do event_catcher.catch_event(event)
234
235 # Click event on the UI thread
236 #
237 # This method is called on the UI thread and redirects the event to `click`
238 # throught `App::event_queue`. In most cases, you should implement `click`
239 # and leave `click_ui` as is.
240 fun click_ui do app.event_queue.add(new ClickEvent(self))
241 end
242
243 # An Android editable text field
244 class EditText
245 super TextView
246
247 redef type NATIVE: NativeEditText
248
249 init
250 do
251 var native = new NativeEditText(app.native_activity)
252 self.native = native.new_global_ref
253 end
254 end
255
256 #
257 ## Native classes
258 #
259
260 # A `View` for Android
261 extern class NativeView in "Java" `{ android.view.View `}
262 super JavaObject
263
264 fun minimum_width=(val: Int) in "Java" `{ recv.setMinimumWidth((int)val); `}
265 fun minimum_height=(val: Int) in "Java" `{ recv.setMinimumHeight((int)val); `}
266 end
267
268 # A collection of `NativeView`
269 extern class NativeViewGroup in "Java" `{ android.view.ViewGroup `}
270 super NativeView
271
272 fun add_view(view: NativeView) in "Java" `{ recv.addView(view); `}
273 end
274
275 # A `NativeViewGroup` organized in a line
276 extern class NativeLinearLayout in "Java" `{ android.widget.LinearLayout `}
277 super NativeViewGroup
278
279 new(context: NativeActivity) in "Java" `{ return new LinearLayout(context); `}
280
281 fun set_vertical in "Java" `{ recv.setOrientation(LinearLayout.VERTICAL); `}
282 fun set_horizontal in "Java" `{ recv.setOrientation(LinearLayout.HORIZONTAL); `}
283
284 redef fun add_view(view) in "Java"
285 `{
286 MarginLayoutParams params = new MarginLayoutParams(
287 LinearLayout.LayoutParams.MATCH_PARENT,
288 LinearLayout.LayoutParams.WRAP_CONTENT);
289 recv.addView(view, params);
290 `}
291
292 fun add_view_with_weight(view: NativeView, weight: Float)
293 in "Java" `{
294 recv.addView(view, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT, (float)weight));
295 `}
296 end
297
298 # A `NativeViewGroup` organized as a grid
299 extern class NativeGridLayout in "Java" `{ android.widget.GridLayout `}
300 super NativeViewGroup
301
302 new(context: NativeActivity) in "Java" `{ return new android.widget.GridLayout(context); `}
303
304 fun row_count=(val: Int) in "Java" `{ recv.setRowCount((int)val); `}
305
306 fun column_count=(val: Int) in "Java" `{ recv.setColumnCount((int)val); `}
307
308 redef fun add_view(view) in "Java" `{ recv.addView(view); `}
309 end
310
311 extern class NativePopupWindow in "Java" `{ android.widget.PopupWindow `}
312 super NativeView
313
314 new (context: NativeActivity) in "Java" `{
315 PopupWindow recv = new PopupWindow(context);
316 recv.setWindowLayoutMode(LinearLayout.LayoutParams.MATCH_PARENT,
317 LinearLayout.LayoutParams.MATCH_PARENT);
318 recv.setClippingEnabled(false);
319 return recv;
320 `}
321
322 fun content_view=(layout: NativeViewGroup) in "Java" `{ recv.setContentView(layout); `}
323 end
324
325 extern class NativeTextView in "Java" `{ android.widget.TextView `}
326 super NativeView
327
328 new (context: NativeActivity) in "Java" `{ return new TextView(context); `}
329
330 fun text: JavaString in "Java" `{ return recv.getText().toString(); `}
331
332 fun text=(value: JavaString) in "Java" `{
333
334 final TextView final_recv = recv;
335 final String final_value = value;
336
337 ((Activity)recv.getContext()).runOnUiThread(new Runnable() {
338 @Override
339 public void run() {
340 final_recv.setText(final_value);
341 }
342 });
343 `}
344
345 fun enabled: Bool in "Java" `{ return recv.isEnabled(); `}
346 fun enabled=(value: Bool) in "Java" `{
347 final TextView final_recv = recv;
348 final boolean final_value = value;
349
350 ((Activity)recv.getContext()).runOnUiThread(new Runnable() {
351 @Override
352 public void run() {
353 final_recv.setEnabled(final_value);
354 }
355 });
356 `}
357
358 fun gravity_center in "Java" `{
359 recv.setGravity(Gravity.CENTER);
360 `}
361
362 fun text_size=(dpi: Float) in "Java" `{
363 recv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, (float)dpi);
364 `}
365 end
366
367 extern class NativeEditText in "Java" `{ android.widget.EditText `}
368 super NativeTextView
369
370 redef type SELF: NativeEditText
371
372 new (context: NativeActivity) in "Java" `{ return new android.widget.EditText(context); `}
373
374 fun width=(val: Int) in "Java" `{ recv.setWidth((int)val); `}
375
376 fun input_type_text in "Java" `{ recv.setInputType(android.text.InputType.TYPE_CLASS_TEXT); `}
377
378 redef fun new_global_ref: SELF import sys, Sys.jni_env `{
379 Sys sys = NativeEditText_sys(recv);
380 JNIEnv *env = Sys_jni_env(sys);
381 return (*env)->NewGlobalRef(env, recv);
382 `}
383 end
384
385 extern class NativeButton in "Java" `{ android.widget.Button `}
386 super NativeTextView
387
388 redef type SELF: NativeButton
389
390 new (context: NativeActivity, queue: ConcurrentList[AppEvent], sender_object: Object) import Button.click_ui in "Java" `{
391 final int final_sender_object = sender_object;
392
393 return new Button(context){
394 @Override
395 public boolean onTouchEvent(MotionEvent event) {
396 if(event.getAction() == MotionEvent.ACTION_DOWN) {
397 Button_click_ui(final_sender_object);
398 return true;
399 }
400 return false;
401 }
402 };
403 `}
404
405 redef fun new_global_ref: SELF import sys, Sys.jni_env `{
406 Sys sys = NativeButton_sys(recv);
407 JNIEnv *env = Sys_jni_env(sys);
408 return (*env)->NewGlobalRef(env, recv);
409 `}
410 end