Merge: app.nit on GNU/Linux: implement multiple windows support using a GtkStack
authorJean Privat <jean@pryen.org>
Tue, 22 Mar 2016 00:02:12 +0000 (20:02 -0400)
committerJean Privat <jean@pryen.org>
Tue, 22 Mar 2016 00:02:12 +0000 (20:02 -0400)
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>

18 files changed:
contrib/tnitter/src/action.nit
lib/android/http_request.nit
lib/android/service/service.nit
lib/android/ui/native_ui.nit
lib/android/ui/ui.nit
lib/app/ui.nit
lib/gtk/v3_4/gtk_widgets_ext.nit
lib/ios/ui/ui.nit
lib/ios/ui/uikit.nit
lib/libevent.nit
lib/linux/ui.nit
lib/nitcorn/examples/src/xymus_net.nit
lib/nitcorn/http_response.nit
lib/nitcorn/log.nit
lib/nitcorn/reactor.nit
lib/nitcorn/sessions.nit
misc/vim/plugin/nit.vim
src/modelize/modelize_property.nit

index e40e309..ad41a71 100644 (file)
@@ -82,6 +82,9 @@ class TnitterWeb
        <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>
 
@@ -93,6 +96,9 @@ class TnitterWeb
 </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
index 2b9fd14..d4e9b0f 100644 (file)
@@ -110,3 +110,8 @@ private extern class JavaHttpResponse in "Java" `{ org.apache.http.HttpResponse
                }
        `}
 end
+
+# Force linearization of print
+#
+# TODO prioritize `android::log`
+redef fun print(object) do super
index b27bdac..901a0e2 100644 (file)
@@ -137,7 +137,7 @@ end
 
 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);
        `}
index b6a3460..e4dd670 100644 (file)
@@ -54,6 +54,12 @@ extern class NativeView in "Java" `{ android.view.View `}
 
        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`
@@ -448,6 +454,21 @@ extern class Android_widget_ArrayAdapter in "Java" `{ android.widget.ArrayAdapte
        `}
 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
@@ -455,7 +476,7 @@ extern class Android_widget_AbsListView in "Java" `{ android.widget.AbsListView
        #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" `{
@@ -992,3 +1013,69 @@ fun android_r_style_text_appearance_medium: Int 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
index d6cb45e..5d428ba 100644 (file)
@@ -37,18 +37,69 @@ redef class Control
        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
 
@@ -166,6 +217,14 @@ redef class Label
        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
@@ -220,3 +279,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
index c23d9cd..727b716 100644 (file)
@@ -178,6 +178,14 @@ class Label
        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
index 3102dbd..25265c4 100644 (file)
@@ -258,3 +258,22 @@ extern class GtkColorButton `{GtkColorButton *`}
        `}
 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
index bf666cf..96d1e8a 100644 (file)
@@ -203,6 +203,39 @@ redef class Label
        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
index ea538ce..c162b84 100644 (file)
@@ -495,14 +495,24 @@ extern class UIStackView in "ObjC" `{ UIStackView * `}
        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`
@@ -596,3 +606,46 @@ extern class UITableViewStyle in "ObjC" `{ UITableViewStyle* `}
        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
index 84d21d7..36de40f 100644 (file)
 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);
@@ -59,9 +61,9 @@ in "C" `{
 
        // 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
 
@@ -399,17 +401,26 @@ class ConnectionFactory
        # 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
@@ -423,4 +434,19 @@ class ConnectionFactory
                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
index 6ab0777..23c1f91 100644 (file)
@@ -229,6 +229,17 @@ redef class Label
        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
index 365b517..95cd31d 100644 (file)
@@ -120,7 +120,9 @@ class OpportunityMasterHeader
 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
index 1690864..3d0fd33 100644 (file)
@@ -70,7 +70,7 @@ class HttpResponse
                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
index c0d6d52..ede6d06 100644 (file)
@@ -27,16 +27,30 @@ redef class Action
                        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
 
@@ -52,7 +66,7 @@ redef fun print_error(object) do
        "{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
index 0fb5cad..e888361 100644 (file)
@@ -35,6 +35,9 @@ class HttpServer
 
        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)
@@ -131,7 +134,7 @@ class HttpFactory
        # 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
        #
index 8ae26ac..15fa9cc 100644 (file)
@@ -62,7 +62,7 @@ redef class Sys
        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
index 6824882..582e5fb 100644 (file)
@@ -46,7 +46,7 @@ function NitComplete()
 
                " 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.
index 6588268..573f200 100644 (file)
@@ -589,7 +589,7 @@ redef class APropdef
                        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