This PR implement support for mobile-like multiple windows on GNU/Linux with GTK. It uses a single GTK window and a GtkStack showing one app.nit window at a time. This results in a single window with changing content.
The change to ListView provides a scrollable list of items, which is closer to the mobile equivalent.
Support for a back button will be introduced in a future PR.
Pull-Request: #1990
Reviewed-by: Jean Privat <jean@pryen.org>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
+ <script>
+ {{{javascript_header or else ""}}}
+ </script>
</head>
<body>
</body>
</html>"""
+ # Custom JavaScript code added within a `<script>` block to each page
+ var javascript_header: nullable Writable = null is writable
+
redef fun answer(request, turi)
do
# Get existing session
}
`}
end
+
+# Force linearization of print
+#
+# TODO prioritize `android::log`
+redef fun print(object) do super
redef class NativeContext
private fun start_service in "Java" `{
- android.content.Intent indent =
+ android.content.Intent intent =
new android.content.Intent(self, nit.app.NitService.class);
self.startService(intent);
`}
fun enabled: Bool in "Java" `{ return self.isEnabled(); `}
fun enabled=(value: Bool) in "Java" `{ self.setEnabled(value); `}
+
+ # Java implementation: int android.view.View.getId()
+ fun id: Int in "Java" `{ return self.getId(); `}
+
+ # Java implementation: android.view.View.setId(int)
+ fun id=(id: Int) in "Java" `{ self.setId((int)id); `}
end
# A collection of `NativeView`
`}
end
+# Java class: android.app.Fragment
+extern class Android_app_Fragment in "Java" `{ android.app.Fragment `}
+ super JavaObject
+
+ redef fun new_global_ref import sys, Sys.jni_env `{
+ Sys sys = Android_app_Fragment_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 class: android.widget.AbsListView
extern class Android_widget_AbsListView in "Java" `{ android.widget.AbsListView `}
#super Android_widget_AdapterView
#super Android_view_ViewTreeObserver_OnGlobalLayoutListener
#super Android_widget_Filter_FilterListener
#super Android_view_ViewTreeObserver_OnTouchModeChangeListener
- super NativeView
+ super NativeViewGroup
# Java implementation: android.widget.AbsListView.setAdapter(android.widget.Adapter)
fun set_adapter(arg0: Android_widget_ListAdapter) in "Java" `{
fun android_r_style_text_appearance_small: Int in "Java" `{
return android.R.style.TextAppearance_Small;
`}
+
+# Java class: android.widget.Checkable
+extern class Android_widget_Checkable in "Java" `{ android.widget.Checkable `}
+ super JavaObject
+
+ # Java implementation: android.widget.Checkable.setChecked(boolean)
+ fun set_checked(arg0: Bool) in "Java" `{
+ self.setChecked(arg0);
+ `}
+
+ # Java implementation: boolean android.widget.Checkable.isChecked()
+ fun is_checked: Bool in "Java" `{
+ return self.isChecked();
+ `}
+
+ # Java implementation: android.widget.Checkable.toggle()
+ fun toggle in "Java" `{
+ self.toggle();
+ `}
+
+ redef fun new_global_ref import sys, Sys.jni_env `{
+ Sys sys = Android_widget_Checkable_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 abstract class: android.widget.CompoundButton
+extern class Android_widget_CompoundButton in "Java" `{ android.widget.CompoundButton `}
+ super NativeButton
+ super Android_widget_Checkable
+
+ redef fun new_global_ref import sys, Sys.jni_env `{
+ Sys sys = Android_widget_CompoundButton_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 class: android.widget.CheckBox
+extern class Android_widget_CheckBox in "Java" `{ android.widget.CheckBox `}
+ super Android_widget_CompoundButton
+
+ # Java constructor: android.widget.CheckBox
+ new (a: NativeContext) in "Java" `{
+ return new android.widget.CheckBox(a);
+ `}
+
+ redef fun new_global_ref import sys, Sys.jni_env `{
+ Sys sys = Android_widget_CheckBox_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
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
+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: Android_app_Fragment
- redef type NATIVE: NativeActivity
+ # 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
init do native.set_text_appearance(app.native_activity, android_r_style_text_appearance_medium)
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
+
+ redef fun is_checked do return native.is_checked
+ redef fun is_checked=(value) do native.set_checked(value)
+end
+
redef class TextInput
redef type NATIVE: NativeEditText
redef var native = (new NativeEditText(app.native_activity)).new_global_ref
};
`}
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
super TextView
end
+# Toggle control with two states and a label
+class CheckBox
+ super TextView
+
+ # Is this control in the checked/on state?
+ var is_checked = false is writable
+end
+
# A `Button` press event
class ButtonPressEvent
super AppEvent
`}
end
+# Button remaining "pressed-in" when clicked
+extern class GtkToggleButton `{ GtkToggleButton * `}
+ super GtkButton
+
+ # Current state, returns `true` if pressed/checked
+ fun active: Bool `{ return gtk_toggle_button_get_active(self); `}
+
+ # Set current state, `true` for pressed/checked
+ fun active=(value: Bool) `{ gtk_toggle_button_set_active(self, value); `}
+end
+
+# Check box next to a label
+extern class GtkCheckButton `{ GtkCheckButton * `}
+ super GtkToggleButton
+
+ new `{ return (GtkCheckButton *)gtk_check_button_new(); `}
+
+ new with_label(lbl: NativeString) `{ return (GtkCheckButton *)gtk_check_button_new_with_label((gchar *)lbl); `}
+end
redef fun text do return native.text.to_s
end
+# On iOS, check boxes are a layout composed of a label and an `UISwitch`
+redef class CheckBox
+
+ redef type NATIVE: UIStackView
+ redef fun native do return layout.native
+
+ # Root layout implementing this check box
+ var layout = new HorizontalLayout(parent=self.parent)
+
+ # Label with the text
+ var lbl = new Label(parent=layout)
+
+ # `UISwitch` acting as the real check box
+ var ui_switch: UISwitch is noautoinit
+
+ init do
+ # Tweak the layout so it is centered
+ layout.native.distribution = new UIStackViewDistribution.fill_proportionally
+ layout.native.alignment = new UIStackViewAlignment.center
+ layout.native.layout_margins_relative_arrangement = true
+
+ var s = new UISwitch
+ native.add_arranged_subview s
+ ui_switch = s
+ end
+
+ redef fun text=(text) do lbl.text = text
+ redef fun text do return lbl.text
+
+ redef fun is_checked do return ui_switch.on
+ redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
+end
+
redef class TextInput
redef type NATIVE: UITextField
fun spacing=(value: Float) in "ObjC" `{ self.spacing = value; `}
# Wraps: `UIStackView.baselineRelativeArrangement`
- #fun baseline_relative_arrangement: Bool in "ObjC" `{
- # return [self baselineRelativeArrangement];
- #`}
+ fun baseline_relative_arrangement: Bool in "ObjC" `{
+ return self.baselineRelativeArrangement;
+ `}
+
+ # Wraps: `UIStackView.baselineRelativeArrangement`
+ fun baseline_relative_arrangement=(value: Bool) in "ObjC" `{
+ self.baselineRelativeArrangement = value;
+ `}
# Wraps: `UIStackView.layoutMarginsRelativeArrangement`
- #fun layout_margins_relative_arrangement: Bool in "ObjC" `{
- # return [self layoutMarginsRelativeArrangement];
- #`}
+ fun layout_margins_relative_arrangement: Bool in "ObjC" `{
+ return self.layoutMarginsRelativeArrangement;
+ `}
+
+ # Wraps: `UIStackView.layoutMarginsRelativeArrangement`
+ fun layout_margins_relative_arrangement=(value: Bool) in "ObjC" `{
+ self.layoutMarginsRelativeArrangement = value;
+ `}
end
# Defines the orientation of the arranged views in `UIStackView`
new plain in "ObjC" `{ return UITableViewStylePlain; `}
new grouped in "ObjC" `{ return UITableViewStyleGrouped; `}
end
+
+# On/Off button
+extern class UISwitch in "ObjC" `{ UISwitch * `}
+ super UIView
+
+ # Wraps: `[self initWithFrame:(CGRect)frame]`
+ new in "ObjC" `{ return [[UISwitch alloc] initWithFrame: [[UIScreen mainScreen] applicationFrame]]; `}
+
+ # Wraps: `UISwitch.onTintColor`
+# fun on_tint_color: UIColor in "ObjC" `{
+# return [self onTintColor];
+# `}
+
+ # Wraps: `UISwitch.tintColor`
+# fun tint_color: UIColor in "ObjC" `{
+# return [self tintColor];
+# `}
+
+ # Wraps: `UISwitch.thumbTintColor`
+# fun thumb_tint_color: UIColor in "ObjC" `{
+# return [self thumbTintColor];
+# `}
+
+ # Wraps: `UISwitch.onImage`
+# fun on_image: UIImage in "ObjC" `{
+# return [self onImage];
+# `}
+
+ # Wraps: `UISwitch.offImage`
+# fun off_image: UIImage in "ObjC" `{
+# return [self offImage];
+# `}
+
+ # Wraps: `UISwitch.on`
+ fun on: Bool in "ObjC" `{
+ return self.on;
+ `}
+
+ # Wraps: `[self setOn:(BOOL)on animated:(BOOL)animated]`
+ fun set_on_animated(on: Bool, animated: Bool) in "ObjC" `{
+ [self setOn: on animated: animated];
+ `}
+end
module libevent is pkgconfig("libevent")
in "C header" `{
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <fcntl.h>
- #include <errno.h>
- #include <string.h>
- #include <sys/socket.h>
-
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
`}
in "C" `{
+ #include <sys/stat.h>
+ #include <sys/types.h>
+ #include <fcntl.h>
+ #include <errno.h>
+ #include <string.h>
+
+ #include <sys/socket.h>
+ #include <arpa/inet.h>
+ #include <netinet/in.h>
+ #include <netinet/ip.h>
// Protect callbacks for compatibility with light FFI
#ifdef Connection_decr_ref
-
// Callback forwarded to 'Connection.write_callback'
static void c_write_cb(struct bufferevent *bev, Connection ctx) {
Connection_write_callback(ctx);
// Callback forwarded to 'ConnectionFactory.accept_connection'
static void accept_connection_cb(struct evconnlistener *listener, evutil_socket_t fd,
- struct sockaddr *address, int socklen, ConnectionFactory ctx)
+ struct sockaddr *addrin, int socklen, ConnectionFactory ctx)
{
- ConnectionFactory_accept_connection(ctx, listener, fd, address, socklen);
+ ConnectionFactory_accept_connection(ctx, listener, fd, addrin, socklen);
}
#endif
# Accept a connection on `listener`
#
# By default, it creates a new NativeBufferEvent and calls `spawn_connection`.
- fun accept_connection(listener: ConnectionListener, fd: Int, address: Pointer, socklen: Int)
+ fun accept_connection(listener: ConnectionListener, fd: Int, addrin: Pointer, socklen: Int)
do
var base = listener.base
var bev = new NativeBufferEvent.socket(base, fd, bev_opt_close_on_free)
- var conn = spawn_connection(bev)
+
+ # Human representation of remote client address
+ var addr_len = 46 # Longest possible IPv6 address + null byte
+ var addr_buf = new NativeString(addr_len)
+ addr_buf = addrin_to_address(addrin, addr_buf, addr_len)
+ var addr = if addr_buf.address_is_null then
+ "Unknown address"
+ else addr_buf.to_s
+
+ var conn = spawn_connection(bev, addr)
bev.enable ev_read|ev_write
bev.setcb conn
end
# Create a new `Connection` object for `buffer_event`
- fun spawn_connection(buffer_event: NativeBufferEvent): Connection
+ fun spawn_connection(buffer_event: NativeBufferEvent, address: String): Connection
do
return new Connection(buffer_event)
end
end
return listener
end
+
+ # Put string representation of source `address` into `buf`
+ private fun addrin_to_address(address: Pointer, buf: NativeString, buf_len: Int): NativeString `{
+ struct sockaddr *addrin = (struct sockaddr*)address;
+
+ if (addrin->sa_family == AF_INET) {
+ struct in_addr *src = &((struct sockaddr_in*)addrin)->sin_addr;
+ return (char *)inet_ntop(addrin->sa_family, src, buf, buf_len);
+ }
+ else if (addrin->sa_family == AF_INET6) {
+ struct in6_addr *src = &((struct sockaddr_in6*)addrin)->sin6_addr;
+ return (char *)inet_ntop(addrin->sa_family, src, buf, buf_len);
+ }
+ return NULL;
+ `}
end
redef fun text=(value) do native.text = (value or else "").to_s
end
+redef class CheckBox
+ redef type NATIVE: GtkCheckButton
+ redef var native = new GtkCheckButton
+
+ redef fun text do return native.text
+ redef fun text=(value) do native.text = (value or else "").to_s
+
+ redef fun is_checked do return native.active
+ redef fun is_checked=(value) do native.active = value
+end
+
redef class TextInput
redef type NATIVE: GtkEntry
redef var native = new GtkEntry
end
redef class TnitterWeb
- redef var header: String = (new MasterHeader("tnitter", true)).to_s
+ redef var header = (new MasterHeader("tnitter", true)).to_s
+
+ redef fun javascript_header do return tracking_code
end
redef class BenitluxDocument
end
# Set server ID
- if not header.keys.has("Server") then header["Server"] = "unitcorn"
+ if not header.keys.has("Server") then header["Server"] = "nitcorn"
end
# Get this reponse as a string according to HTTP protocol
super
return
end
- print """{{{class_name}}} enter:
-uri="{{{truncated_uri}}}"
-query="{{{request.query_string}}}"
-body:{{{request.body.length}}} bytes"""
+
+ print "{http_server.remote_address}: {class_name} prepare for url:'{request.url}' body:'{request.body}' cookie:'{request.cookie.join(",", ":")}'"
+
var clock = new Clock
+
super
+
var perf = sys.perfs[class_name]
perf.add(clock.lapse)
if perf.count % perfs_print_period == 0 then print "{class_name} perfs: {perf}:"
- print "{class_name} return: uri={truncated_uri}"
+ end
+end
+
+redef class HttpServer
+ redef fun read_http_request(str)
+ do
+ print "{remote_address}: received HTTP request"
+ super
+ end
+
+ redef fun respond(response)
+ do
+ super
+ if log_nitcorn_actions then print "{remote_address}: response header '{response.header.join(",", ":")}' to '{remote_address}'"
end
end
"{timestamp.hour}:{timestamp.min}:{timestamp.sec}: {object}"
end
-# Should the actions be logged ?
+# Should the actions be logged? This may log sensitive data.
fun log_nitcorn_actions: Bool do return false
# Number of actions executed before printing the perfs
private var parser = new HttpRequestParser is lazy
+ # Human readable address of the remote client
+ var remote_address: String
+
redef fun read_http_request(str)
do
var request_object = parser.parse_http_request(str.to_s)
# You can use this to create the first `HttpFactory`, which is the most common.
init and_libevent do init(new NativeEventBase)
- redef fun spawn_connection(buf_ev) do return new HttpServer(buf_ev, self)
+ redef fun spawn_connection(buf_ev, address) do return new HttpServer(buf_ev, self, address)
# Execute the main listening loop to accept connections
#
private var next_session_id_cache: nullable Int = null
# Salt used to hash the session id
- protected var session_salt = "Default unitcorn session salt"
+ protected var session_salt = "Default nitcorn session salt"
end
redef class Int
" This gives us better results for Nit
set noignorecase
- set completeopt=longest,menuone,preview
+ set completeopt=longest,menuone
" Do not predict small 3 letters keywords (or their prefix), they slow down
" prediction and some also require double-enter on end of line.
var mdoc = ndoc.to_mdoc
mpropdef.mdoc = mdoc
mdoc.original_mentity = mpropdef
- else if mpropdef.is_intro and mpropdef.mproperty.visibility >= protected_visibility then
+ else if mpropdef.is_intro and mpropdef.mproperty.visibility >= protected_visibility and mpropdef.name != "new" then
modelbuilder.advice(self, "missing-doc", "Documentation warning: Undocumented property `{mpropdef.mproperty}`")
end