# HG changeset patch
# User Remi Thebault <remi.thebault@gmail.com>
# Date 1604965942 -3600
#      Tue Nov 10 00:52:22 2020 +0100
# Node ID eea53507123172855b8607af37b35424eacdfada
# Parent  0e86e50bebeb1e73385b3d0ff374d7bfec093ad5
Initial implementation of Property Inspector


HIGHLIGHTS

  * a widget with two main views
    - a tree to display and interact with graphics object tree
    - a table, displaying the properties of the object selected in the
      tree

  * the tree receive notifications of graphics object creation/deletion
    to update itself

  * the table provides display and edition of most types of properties


SHORTCOMINGS

  * Not all property types have yet proper editors/painting

  * Updating properties does not always reflect in the figures.
    - updating line color for example not always trigger redraw.
    - when redraw happens, all previous property editions are visible

  * No link to help is provided. 2 possible strategies:
    - compute URL to online help from handle type and property name
    - embed the help in the executable. I have no clue how to do this.


DETAILED CHANGELOG

  * liboctave/util/oct-mutex.h
    - adding unique_lock template class that encapsulate a critical resource
      providing lock/unlock methods. unique_lock is moveable but non-copiable
      and also acts like a smart pointer.
      - not a strictly needed addition, but is used to provide safely the
        gh_manager instance to the GUI

  * libinterp/corefcn/graphics.in.h
    - adding some introspection of graphics properties
      - prop_type() method (returning new property_type enum)
      - as_base_property() method to retrieve a pointer that can be casted
      - as_[type]_property() that dynamic_cast the base property to derived
        type (not used - compiled out at the moment)

  * libinterp/corefcn/event-manager.h
    - adding lifetime events of graphics objects in the interpreter events
      interface. Needed to track creation and removal of graphics objects in
      the tree.

  * libinterp/corefcn/graphics.cc
   - fire the graphics objects lifetime events

  * libui/src/qt-interpreter-events.h
  * libui/src/qt-interpreter-events.cc
    - adding signals for the new graphics objects

  * libgui/src/interpreter-qobject.h
  * libgui/src/interpreter-qobject.cc
    - provide access to the gh_manager through a unique_lock
    - the GUI cannot use the gh_manager without locking it
    - Not very useful today because the gh_manager is not consistently locked
      in the interpreter thread (e.g. when setting/reading properties).
      This could lead to race conditions (although I didn't observe any).
    - May be a better strategy is to have fine grain (per property) locking.

  * libgui/src/property-inspector.h
  * libgui/src/property-inspector.cc
    - implementation of the property inspector
      - simple widget that instantiate the more complex views and models and
        make them talk together

  * libgui/src/property-inspector-tree.h
  * libgui/src/property-inspector-tree.cc
    - implementation of the graphics handle tree model
    - quite straightforward Qt tree model code

  * libgui/src/property-inspector-table.h
  * libgui/src/property-inspector-table.cc
    - implementation of the graphics properties table model and item delegate.
    - to keep code flexible, I tried to take the best decisions based on the
      octave_value of the property rather than based on the derived property
      type, but this is not always possible. (hence the properties
      introspection)
    - there are (at the moment) two QWidget subclasses used as cell editors:
      - matrix_widget
      - color_editor_widget (this one use a *.ui designer file)
    - QComboBox is used for the radio properties
    - default Qt implementation is used for plain strings and numbers

  * libgui/src/property-inspector-color-editor.ui
    - Qt Designer file for the color_editor_widget

  * libgui/src/main-window.h
  * libgui/src/main-window.cc
  * libgui/src/gui-preferences-sc.h
    - instantiate the property inspector and setup shortcuts

  * libgui/src/module.mk
    - adding new property inspector files

diff -r 0e86e50bebeb -r eea535071231 libgui/src/gui-preferences-sc.h
--- a/libgui/src/gui-preferences-sc.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/gui-preferences-sc.h	Tue Nov 10 00:52:22 2020 +0100
@@ -105,6 +105,7 @@ const sc_pref sc_main_window_show_worksp
 const sc_pref sc_main_window_show_editor (sc_main_window + ":show_editor", PRE + CTRL_SHIFT + Qt::Key_4);
 const sc_pref sc_main_window_show_doc (sc_main_window + ":show_doc", PRE + CTRL_SHIFT + Qt::Key_5);
 const sc_pref sc_main_window_show_variable_editor (sc_main_window + ":show_variable_editor", PRE + CTRL_SHIFT + Qt::Key_6);
+const sc_pref sc_main_window_show_property_inspector (sc_main_window + ":show_property_inspector", PRE + CTRL_SHIFT + Qt::Key_7);
 const sc_pref sc_main_window_command (sc_main_window + ":command", PRE + CTRL + Qt::Key_0);
 const sc_pref sc_main_window_history (sc_main_window + ":history", PRE + CTRL + Qt::Key_1);
 const sc_pref sc_main_window_file_browser (sc_main_window + ":file_browser", PRE + CTRL + Qt::Key_2);
@@ -112,6 +113,7 @@ const sc_pref sc_main_window_workspace (
 const sc_pref sc_main_window_editor (sc_main_window + ":editor", PRE + CTRL + Qt::Key_4);
 const sc_pref sc_main_window_doc (sc_main_window + ":doc", PRE + CTRL + Qt::Key_5);
 const sc_pref sc_main_window_variable_editor (sc_main_window + ":variable_editor", PRE + CTRL + Qt::Key_6);
+const sc_pref sc_main_window_property_inspector (sc_main_window + ":property_inspector", PRE + CTRL + Qt::Key_7);
 const sc_pref sc_main_window_previous_dock (sc_main_window + ":previous_widget", PRE + CTRL_ALT + Qt::Key_P);
 const sc_pref sc_main_window_reset (sc_main_window + ":reset", QKeySequence::UnknownKey);
 
diff -r 0e86e50bebeb -r eea535071231 libgui/src/interpreter-qobject.cc
--- a/libgui/src/interpreter-qobject.cc	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/interpreter-qobject.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -143,4 +143,13 @@ namespace octave
   {
     return m_octave_qobj.qt_link ();
   }
+
+  gh_manager_lock interpreter_qobject::get_gh_manager (void)
+  {
+    if (! m_interpreter)
+      throw std::logic_error ("interpreter_qobject::get_gh_manager called "
+                              "before initialization of interpreter");
+
+    return gh_manager_lock (m_interpreter->get_gh_manager ());
+  }
 }
diff -r 0e86e50bebeb -r eea535071231 libgui/src/interpreter-qobject.h
--- a/libgui/src/interpreter-qobject.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/interpreter-qobject.h	Tue Nov 10 00:52:22 2020 +0100
@@ -28,14 +28,22 @@
 
 #include <QObject>
 
+#include "oct-mutex.h"
+
 #include "qt-interpreter-events.h"
 
+
+class gh_manager;
+
 namespace octave
 {
+
   class interpreter;
 
   class base_qobject;
 
+  using gh_manager_lock = ::octave::unique_lock<gh_manager>;
+
   class interpreter_qobject : public QObject
   {
     Q_OBJECT
@@ -52,6 +60,8 @@ namespace octave
 
     void interpreter_event (const meth_callback& meth);
 
+    gh_manager_lock get_gh_manager ();
+
   signals:
 
     void ready (void);
diff -r 0e86e50bebeb -r eea535071231 libgui/src/main-window.cc
--- a/libgui/src/main-window.cc	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/main-window.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -107,13 +107,13 @@ namespace octave
 #endif
   }
 
-  main_window::main_window (base_qobject& oct_qobj)
-    : QMainWindow (), m_octave_qobj (oct_qobj),
-      m_workspace_model (nullptr),
+  main_window::main_window (base_qobject &oct_qobj)
+    : QMainWindow (), m_octave_qobj (oct_qobj), m_workspace_model (nullptr),
       m_status_bar (nullptr), m_command_window (nullptr),
       m_history_window (nullptr), m_file_browser_window (nullptr),
       m_doc_browser_window (nullptr), m_editor_window (nullptr),
       m_workspace_window (nullptr), m_variable_editor_window (nullptr),
+      m_property_inspector_window (nullptr),
       m_external_editor (new external_editor_interface (this, m_octave_qobj)),
       m_active_editor (m_external_editor), m_settings_dlg (nullptr),
       m_find_files_dlg (nullptr), m_set_path_dlg (nullptr),
@@ -176,6 +176,7 @@ namespace octave
     m_editor_window = create_default_editor (this, m_octave_qobj);
     m_variable_editor_window = new variable_editor (this, m_octave_qobj);
     m_workspace_window = new workspace_view (this, m_octave_qobj);
+    m_property_inspector_window = new property_inspector (this, m_octave_qobj);
 
     m_previous_dock = m_command_window;
 
@@ -253,6 +254,7 @@ namespace octave
     delete m_history_window;
     delete m_status_bar;
     delete m_variable_editor_window;
+    delete m_property_inspector_window;
 
     delete m_find_files_dlg;
     delete m_release_notes_window;
@@ -2084,6 +2086,8 @@ namespace octave
 #endif
     addDockWidget (Qt::RightDockWidgetArea, m_variable_editor_window);
     tabifyDockWidget (m_command_window, m_variable_editor_window);
+    addDockWidget(Qt::RightDockWidgetArea, m_property_inspector_window);
+    tabifyDockWidget (m_command_window, m_property_inspector_window);
 
     addDockWidget (Qt::LeftDockWidgetArea, m_file_browser_window);
     addDockWidget (Qt::LeftDockWidgetArea, m_workspace_window);
@@ -2601,8 +2605,13 @@ namespace octave
     m_show_documentation_action = construct_window_menu_item
       (window_menu, tr ("Show Documentation"), true, m_doc_browser_window);
 
-    m_show_variable_editor_action = construct_window_menu_item
-      (window_menu, tr ("Show Variable Editor"), true, m_variable_editor_window);
+    m_show_variable_editor_action
+        = construct_window_menu_item (window_menu, tr ("Show Variable Editor"),
+                                      true, m_variable_editor_window);
+
+    m_show_property_inspector_action = construct_window_menu_item (
+        window_menu, tr ("Show Property Inspector"), true,
+        m_property_inspector_window);
 
     window_menu->addSeparator ();
 
@@ -2627,6 +2636,10 @@ namespace octave
     m_variable_editor_action = construct_window_menu_item
       (window_menu, tr ("Variable Editor"), false, m_variable_editor_window);
 
+    m_property_inspector_action
+        = construct_window_menu_item (window_menu, tr ("Property Inspector"),
+                                      false, m_property_inspector_window);
+
     window_menu->addSeparator ();
 
     m_previous_dock_action = add_action (window_menu, QIcon (),
@@ -2792,6 +2805,7 @@ namespace octave
     scmgr.set_shortcut (m_show_editor_action, sc_main_window_show_editor);
     scmgr.set_shortcut (m_show_documentation_action, sc_main_window_show_doc);
     scmgr.set_shortcut (m_show_variable_editor_action, sc_main_window_show_variable_editor);
+    scmgr.set_shortcut (m_show_property_inspector_action, sc_main_window_show_property_inspector);
     scmgr.set_shortcut (m_command_window_action, sc_main_window_command);
     scmgr.set_shortcut (m_history_action, sc_main_window_history);
     scmgr.set_shortcut (m_workspace_action, sc_main_window_workspace);
@@ -2799,6 +2813,7 @@ namespace octave
     scmgr.set_shortcut (m_editor_action, sc_main_window_editor);
     scmgr.set_shortcut (m_documentation_action, sc_main_window_doc);
     scmgr.set_shortcut (m_variable_editor_action, sc_main_window_variable_editor);
+    scmgr.set_shortcut (m_property_inspector_action, sc_main_window_property_inspector);
     scmgr.set_shortcut (m_previous_dock_action, sc_main_window_previous_dock);
     scmgr.set_shortcut (m_reset_windows_action, sc_main_window_reset);
 
@@ -2828,6 +2843,8 @@ namespace octave
 #endif
     list.append (static_cast<octave_dock_widget *> (m_workspace_window));
     list.append (static_cast<octave_dock_widget *> (m_variable_editor_window));
+    list.append (
+        static_cast<octave_dock_widget *> (m_property_inspector_window));
     return list;
   }
 
diff -r 0e86e50bebeb -r eea535071231 libgui/src/main-window.h
--- a/libgui/src/main-window.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/main-window.h	Tue Nov 10 00:52:22 2020 +0100
@@ -55,6 +55,7 @@
 #include "interpreter-qobject.h"
 #include "octave-dock-widget.h"
 #include "octave-qobject.h"
+#include "property-inspector.h"
 #include "qt-interpreter-events.h"
 #include "set-path-dialog.h"
 #include "terminal-dock-widget.h"
@@ -319,6 +320,7 @@ namespace octave
     file_editor_interface *m_editor_window;
     workspace_view *m_workspace_window;
     variable_editor *m_variable_editor_window;
+    property_inspector *m_property_inspector_window;
     //!@}
 
     external_editor_interface *m_external_editor;
@@ -366,6 +368,7 @@ namespace octave
     QAction *m_show_editor_action;
     QAction *m_show_documentation_action;
     QAction *m_show_variable_editor_action;
+    QAction *m_show_property_inspector_action;
     QAction *m_command_window_action;
     QAction *m_history_action;
     QAction *m_workspace_action;
@@ -373,6 +376,7 @@ namespace octave
     QAction *m_editor_action;
     QAction *m_documentation_action;
     QAction *m_variable_editor_action;
+    QAction *m_property_inspector_action;
     QAction *m_previous_dock_action;
     QAction *m_reset_windows_action;
 
diff -r 0e86e50bebeb -r eea535071231 libgui/src/module.mk
--- a/libgui/src/module.mk	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/module.mk	Tue Nov 10 00:52:22 2020 +0100
@@ -143,6 +143,9 @@ OCTAVE_GUI_SRC_MOC = \
   %reldir%/moc-main-window.cc \
   %reldir%/moc-news-reader.cc \
   %reldir%/moc-octave-qobject.cc \
+  %reldir%/moc-property-inspector.cc \
+  %reldir%/moc-property-inspector-table.cc \
+  %reldir%/moc-property-inspector-tree.cc \
   %reldir%/moc-settings-dialog.cc \
   %reldir%/moc-terminal-dock-widget.cc \
   %reldir%/moc-color-picker.cc \
@@ -173,6 +176,7 @@ DIRSTAMP_FILES += \
   %reldir%/$(octave_dirstamp)
 
 octave_gui_UI = \
+  %reldir%/property-inspector-color-editor.ui \
   %reldir%/settings-dialog.ui
 
 octave_gui_UI_H = $(patsubst %reldir%/%.ui, %reldir%/ui-%.h, $(octave_gui_UI))
@@ -220,6 +224,9 @@ noinst_HEADERS += \
   %reldir%/main-window.h \
   %reldir%/news-reader.h \
   %reldir%/octave-qobject.h \
+  %reldir%/property-inspector.h \
+  %reldir%/property-inspector-table.h \
+  %reldir%/property-inspector-tree.h \
   %reldir%/qt-application.h \
   %reldir%/qt-interpreter-events.h \
   %reldir%/qt-utils.h \
@@ -261,6 +268,9 @@ noinst_HEADERS += \
   %reldir%/news-reader.cc \
   %reldir%/octave-dock-widget.cc \
   %reldir%/octave-qobject.cc \
+  %reldir%/property-inspector.cc \
+  %reldir%/property-inspector-table.cc \
+  %reldir%/property-inspector-tree.cc \
   %reldir%/qt-interpreter-events.cc \
   %reldir%/qt-application.cc \
   %reldir%/resource-manager.cc \
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector-color-editor.ui
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector-color-editor.ui	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>color_editor_widget</class>
+ <widget class="QWidget" name="color_editor_widget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>277</width>
+    <height>179</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_3">
+   <property name="leftMargin">
+    <number>4</number>
+   </property>
+   <property name="topMargin">
+    <number>4</number>
+   </property>
+   <property name="rightMargin">
+    <number>4</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QWidget" name="radioWgt" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_3">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QRadioButton" name="radioRad">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>preset</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="radioBox"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="widget_5" native="true">
+     <layout class="QGridLayout" name="gridLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="0" column="1">
+       <widget class="QWidget" name="rgbWgt1" native="true">
+        <layout class="QHBoxLayout" name="horizontalLayout_5">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QLabel" name="rgbIcon">
+           <property name="minimumSize">
+            <size>
+             <width>80</width>
+             <height>0</height>
+            </size>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="pickBtn">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="text">
+            <string>Pick</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QRadioButton" name="rgbValRad">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>RBG</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0" colspan="2">
+       <widget class="QWidget" name="rgbWgt2" native="true">
+        <layout class="QFormLayout" name="formLayout">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="0">
+          <widget class="QLabel" name="label_2">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="text">
+            <string>Matrix</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="1">
+          <widget class="QWidget" name="widget" native="true">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <property name="leftMargin">
+             <number>0</number>
+            </property>
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <property name="rightMargin">
+             <number>0</number>
+            </property>
+            <property name="bottomMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QDoubleSpinBox" name="rSpn">
+              <property name="decimals">
+               <number>3</number>
+              </property>
+              <property name="maximum">
+               <double>1.000000000000000</double>
+              </property>
+              <property name="singleStep">
+               <double>0.100000000000000</double>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QDoubleSpinBox" name="gSpn">
+              <property name="decimals">
+               <number>3</number>
+              </property>
+              <property name="maximum">
+               <double>1.000000000000000</double>
+              </property>
+              <property name="singleStep">
+               <double>0.100000000000000</double>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QDoubleSpinBox" name="bSpn">
+              <property name="decimals">
+               <number>3</number>
+              </property>
+              <property name="maximum">
+               <double>1.000000000000000</double>
+              </property>
+              <property name="singleStep">
+               <double>0.100000000000000</double>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+         </item>
+         <item row="3" column="0">
+          <widget class="QLabel" name="label">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="text">
+            <string>SVG</string>
+           </property>
+          </widget>
+         </item>
+         <item row="3" column="1">
+          <widget class="QComboBox" name="svgBox">
+           <property name="minimumSize">
+            <size>
+             <width>100</width>
+             <height>0</height>
+            </size>
+           </property>
+           <property name="editable">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="0">
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>Octave</string>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="1">
+          <widget class="QComboBox" name="octaveBox"/>
+         </item>
+        </layout>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="widget_2" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector-table.cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector-table.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,1587 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <algorithm>
+#include <limits>
+#include <sstream>
+#include <vector>
+
+#include <QApplication>
+#include <QButtonGroup>
+#include <QColorDialog>
+#include <QComboBox>
+#include <QDataStream>
+#include <QDebug>
+#include <QGridLayout>
+#include <QHeaderView>
+#include <QPainter>
+#include <QStyle>
+#include <QTableView>
+
+#include "caseless-str.h"
+
+#include "graphics.h"
+#include "interpreter.h"
+
+#include "octave-qobject.h"
+#include "qt-application.h"
+
+#include "property-inspector-table.h"
+#include "ui-property-inspector-color-editor.h"
+
+namespace octave
+{
+
+  enum Column
+  {
+    // COL_NAME,
+    COL_VALUE,
+    COL_TYPE,
+
+    COL_COUNT,
+  };
+
+  // max number of displayed items
+  constexpr int array_disp_limit = 4;
+  // max number of items to allow edition
+  // (TODO: forward to variable editor for bigger data)
+  constexpr int matrix_edit_row_limit = 4;
+  constexpr int matrix_edit_col_limit = 4;
+
+  template <class prop_type>
+  inline QString property_string_value (const base_property *prop)
+  {
+    const auto *p = static_cast<const prop_type *> (prop);
+    return QString::fromStdString (p->string_value ());
+  }
+
+  static QString function_display_text (octave_function *of)
+  {
+    if (of->is_user_function () && of->is_anonymous_function ())
+      {
+        const auto uf = of->user_function_value ();
+        return QString ("(anonymous at %1:%2)")
+            .arg (QString::fromStdString (uf->src_file_name ()),
+                  QString::number (uf->beginning_line ()));
+      }
+
+    return QString ("(%1)").arg (
+        QString::fromStdString (of->canonical_name ()));
+  }
+
+  static bool specific_matrix_item (const base_property *prop)
+  {
+    if (prop->prop_type () == property_type::COLOR)
+      {
+        // color have matrix values but deserve special treatment
+        return false;
+      }
+
+    auto val = prop->get ();
+
+    if (val.is_char_matrix ())
+      return false;
+    if (!val.is_real_matrix ())
+      return false;
+
+    if (val.ndims () != 2)
+      return false;
+
+    const auto matrix = val.matrix_value ();
+
+    return matrix.numel () > 1 && matrix.rows () <= matrix_edit_row_limit
+           && matrix.cols () <= matrix_edit_col_limit;
+  }
+
+  static QString array_display_string (const Matrix &m,
+                                       int lim = array_disp_limit)
+  {
+    const auto dims = m.dims ();
+    if (dims.ndims () == 0)
+      return "[]";
+    if (dims.ndims () == 1)
+      {
+        return "[...]"; // can't be
+      }
+    int count = 0;
+    QStringList rows;
+    for (int r = 0; r < dims.elem (0); ++r)
+      {
+        QStringList cols;
+        for (int c = 0; c < dims.elem (1); ++c)
+          {
+            // to avoid confusion between "," column separator
+            // and "," decimal separator on some locales, we use explicit
+            // C locale conversion (what QString::number does)
+            cols.push_back (QString::number (m (r, c)));
+            if (++count >= lim)
+              {
+                break;
+              }
+          }
+        if (cols.size ())
+          {
+            rows.push_back (cols.join (" , "));
+          }
+        if (count >= lim)
+          break;
+      }
+    QString data = rows.join (" ; ");
+    QString closing = dims.numel () > lim ? "..." : "]";
+    return QString ("[ %1 %2").arg (data, closing);
+  }
+
+  static QVariant property_display_data (const base_property *prop,
+                                         base_qobject &oct_qobj)
+  {
+    // some types require special handling, the other are checked from
+    // the octave_value
+    switch (prop->prop_type ())
+      {
+      case property_type::HANDLE:
+        {
+          const auto *p = static_cast<const handle_property *> (prop);
+          auto gh = p->handle_value ();
+
+          auto gh_mgr = oct_qobj.interpreter_qobj ()->get_gh_manager ();
+          auto obj = gh_mgr->get_object (gh);
+
+          if (!obj)
+            return "(undefined)";
+
+          return QString ("%1 (%2 handle)")
+              .arg (QString::number (gh.value ()),
+                    QString::fromStdString (obj.type ()));
+        }
+      case property_type::CALLBACK:
+        {
+          auto val = prop->get ();
+          if (!val.is_defined () || val.isempty ())
+            {
+              return "(undefined)";
+            }
+          // handling through regular value
+          break;
+        }
+      default:
+        break;
+      }
+
+    auto value = prop->get ();
+    if (!value.is_defined ())
+      {
+        return "(undefined)";
+      }
+    if (value.is_string ())
+      {
+        return QString::fromStdString (value.string_value ());
+      }
+    if (value.is_real_scalar ())
+      {
+        return value.scalar_value ();
+      }
+    if (value.is_function ())
+      {
+        return function_display_text (value.function_value ());
+      }
+    if (value.is_real_matrix ())
+      {
+        return array_display_string (value.matrix_value ());
+      }
+
+    return "";
+  }
+
+  static QString dims_string (const dim_vector &dv)
+  {
+    QStringList dims;
+    for (int i = 0; i < dv.ndims (); ++i)
+      {
+        dims.push_back (QString::number (dv.elem (i)));
+      }
+    return dims.join ('x');
+  }
+
+  static QString property_type_string (base_property *prop)
+  {
+    if (!prop)
+      return "(null)";
+
+    switch (prop->prop_type ())
+      {
+      case property_type::INVALID:
+        return "invalid";
+      case property_type::ROW_VECTOR:
+      case property_type::ARRAY:
+        return QString ("%1 matrix").arg (dims_string (prop->get ().dims ()));
+      case property_type::BOOL:
+        return "boolean";
+      case property_type::CALLBACK:
+        return "callback";
+      case property_type::CHILDREN:
+        return "children";
+      case property_type::COLOR:
+        return "color";
+      case property_type::DOUBLE:
+        return "double";
+      case property_type::DOUBLE_RADIO:
+        return "double radio";
+      case property_type::HANDLE:
+        return "handle";
+      case property_type::RADIO:
+        return "radio";
+      case property_type::STRING:
+        return "string";
+      case property_type::STRING_ARRAY:
+        return "string array";
+      case property_type::TEXT_LABEL:
+        return "text label";
+      case property_type::ANY:
+        return "any";
+      }
+    return "";
+  }
+
+  static bool property_editable (const base_property *prop)
+  {
+    if (!prop)
+      return false;
+
+    const auto value = prop->get ();
+
+    switch (prop->prop_type ())
+      {
+      case property_type::INVALID:
+      case property_type::STRING_ARRAY:
+      case property_type::DOUBLE_RADIO:
+      case property_type::CHILDREN:
+      case property_type::HANDLE:
+      case property_type::CALLBACK:
+      case property_type::ANY:
+        return false;
+      default:
+        break;
+      }
+
+    if (prop->prop_type () == property_type::COLOR)
+      return true;
+
+    return prop->is_radio () || value.is_string () || value.is_real_scalar ()
+           || specific_matrix_item (prop);
+  }
+
+  go_properties_table_model::go_properties_table_model (
+      QObject *p, base_qobject &oct_qobj, const graphics_object &go)
+      : QAbstractTableModel (p), m_oct_qobj{ oct_qobj }, m_go{ go }
+  {
+    if (!go)
+      return;
+
+    const auto name_set = go.get_properties ().all_property_names ();
+
+    static const auto parent = caseless_str ("parent");
+    static const auto children = caseless_str ("children");
+
+    for (const auto &name : name_set)
+      {
+        if (parent.compare (name) || children.compare (name))
+          continue;
+
+        if (name.size () > 1 && name[0] == '_' && name[1] == '_')
+          continue;
+
+        if (m_go.get_properties ().get_property (name).is_hidden ())
+          continue;
+
+        m_names.push_back (name);
+      }
+    std::sort (m_names.begin (), m_names.end ());
+  }
+
+  go_properties_table_model::~go_properties_table_model () {}
+
+  int go_properties_table_model::columnCount (const QModelIndex &) const
+  {
+    return COL_COUNT;
+  }
+
+  int go_properties_table_model::rowCount (const QModelIndex &) const
+  {
+    return static_cast<int> (m_names.size ());
+  }
+
+  QVariant go_properties_table_model::headerData (int section,
+                                                  Qt::Orientation orientation,
+                                                  int role) const
+  {
+    if (orientation == Qt::Vertical)
+      {
+        switch (role)
+          {
+          case Qt::DisplayRole:
+            {
+              if (section < 0 || section > static_cast<int> (m_names.size ()))
+                return QVariant{};
+              return QString::fromStdString (m_names[section]);
+            }
+          case Qt::TextAlignmentRole:
+            return QVariant (Qt::AlignLeft | Qt::AlignVCenter);
+          }
+      }
+    else
+      {
+        switch (role)
+          {
+          case Qt::DisplayRole:
+            switch (section)
+              {
+              // case COL_NAME:
+              //   return tr ("Name");
+              case COL_VALUE:
+                return tr ("Value");
+              case COL_TYPE:
+                return tr ("Type");
+              default:
+                return QVariant{};
+              }
+          case Qt::TextAlignmentRole:
+            return QVariant (Qt::AlignVCenter | Qt::AlignLeft);
+          }
+      }
+
+    return QVariant{};
+  }
+
+  QModelIndex go_properties_table_model::index (int row, int column,
+                                                const QModelIndex &) const
+  {
+    // we place the pointer to the base_property in the index
+    // this is safe because the model keep a reference to the graphics object
+    if (row < 0 || row >= static_cast<int> (m_names.size ()))
+      return QModelIndex{};
+
+    graphics_object go = m_go; // remove constness
+
+    const auto &prop_name = m_names[row];
+    base_property &prop
+        = go.get_properties ().get_property (prop_name).as_base_property ();
+
+    return createIndex (row, column, &prop);
+  }
+
+  Qt::ItemFlags
+  go_properties_table_model::flags (const QModelIndex &index) const
+  {
+    auto f = QAbstractTableModel::flags (index);
+    if (index.column () == COL_VALUE)
+      {
+        const auto *prop
+            = static_cast<base_property *> (index.internalPointer ());
+
+        f.setFlag (Qt::ItemIsEditable, property_editable (prop));
+      }
+    return f;
+  }
+
+  QVariant go_properties_table_model::data (const QModelIndex &index,
+                                            int role) const
+  {
+    auto *prop = static_cast<base_property *> (index.internalPointer ());
+    if (!prop)
+      return QVariant{};
+
+    if (index.row () < 0 || index.row () >= static_cast<int> (m_names.size ()))
+      return QVariant{};
+
+    if (index.column () == COL_VALUE && role == Qt::TextColorRole)
+      {
+        if (!prop->get ().is_defined ())
+          {
+            return QColor (Qt::gray);
+          }
+      }
+
+    if (role == Qt::TextAlignmentRole)
+      {
+        switch (index.column ())
+          {
+          // case COL_NAME:
+          //   return QVariant (Qt::AlignVCenter | Qt::AlignLeft);
+          case COL_VALUE:
+            return QVariant (Qt::AlignVCenter | Qt::AlignLeft);
+          case COL_TYPE:
+            return QVariant (Qt::AlignVCenter | Qt::AlignLeft);
+          default:
+            return QVariant{};
+          }
+      }
+
+    if (role != Qt::DisplayRole && role != Qt::EditRole)
+      return QVariant{};
+
+    switch (index.column ())
+      {
+      // case COL_NAME:
+      //   return QString::fromStdString (prop->get_name ());
+      case COL_VALUE:
+        return property_display_data (prop, m_oct_qobj);
+      case COL_TYPE:
+        return property_type_string (prop);
+      default:
+        return QVariant{};
+      }
+  }
+
+  bool go_properties_table_model::setData (const QModelIndex &index,
+                                           const QVariant &value, int role)
+  {
+    if (index.column () != COL_VALUE)
+      return false;
+    if (role != Qt::EditRole)
+      return false;
+    if (index.row () < 0 || index.row () >= rowCount ())
+      return false;
+
+    auto *prop = static_cast<base_property *> (index.internalPointer ());
+    if (!prop)
+      return false;
+
+    bool res = false;
+
+    const auto typ = static_cast<QMetaType::Type> (value.type ());
+
+    if (typ == QMetaType::Double)
+      res = prop->set (octave_value (value.toDouble ()));
+
+    else if (typ == QMetaType::QString)
+      res = prop->set (octave_value (value.toString ().toStdString ()));
+
+    else if (typ == QMetaType::QByteArray)
+      {
+        auto ba = value.toByteArray ();
+        QDataStream ds (ba);
+        qint64 rows = 0, cols = 0;
+        ds >> rows >> cols;
+        if (ds.status () != QDataStream::Ok)
+          return false;
+
+        Matrix matrix (rows, cols);
+
+        for (octave_idx_type r = 0; r < rows; ++r)
+          {
+            for (octave_idx_type c = 0; c < cols; ++c)
+              {
+                double val = 0;
+                ds >> val;
+                if (ds.status () != QDataStream::Ok)
+                  return false;
+                matrix (r, c) = val;
+              }
+          }
+
+        res = prop->set (octave_value (matrix));
+      }
+
+    if (res)
+      emit dataChanged (index, index);
+
+    return res;
+  }
+
+  go_properties_item_delegate::go_properties_item_delegate (
+      QObject *p, base_qobject &oct_qobj, QTableView *view)
+      : QStyledItemDelegate (p), m_oct_qobj (oct_qobj), m_view{ view }
+  {
+    QObject::connect (this, &go_properties_item_delegate::layoutWillChange,
+                      m_view, &QTableView::resizeRowToContents,
+                      Qt::QueuedConnection);
+  }
+
+  inline int color_int_comp (const double comp)
+  {
+    return static_cast<int> (
+        std::round (255 * std::min (1.0, std::max (0.0, comp))));
+  }
+
+  inline QColor color_from_doubles (const double r, const double g,
+                                    const double b)
+  {
+    return QColor{ color_int_comp (r), color_int_comp (g),
+                   color_int_comp (b) };
+  }
+
+  constexpr int color_cell_padding = 6;
+  constexpr int color_cell_spacing = 6;
+  constexpr int color_cell_icon_width = 30;
+  constexpr int color_cell_icon_height = 15;
+
+  static void paint_color (QPainter *painter,
+                           const QStyleOptionViewItem &option,
+                           const color_property *prop)
+  {
+    if (prop->is_rgb ())
+      {
+        auto m = prop->rgb ();
+        if (m.cols () != 3)
+          return;
+
+        constexpr int padding = color_cell_padding;
+        constexpr int spacing = color_cell_spacing;
+
+        painter->save ();
+
+        int dy = (option.rect.height () - color_cell_icon_height) / 2;
+
+        QRect iconRect = option.rect.adjusted (padding, dy, 0, -dy);
+        iconRect.setWidth (color_cell_icon_width);
+
+        const auto color = color_from_doubles (m (0, 0), m (0, 1), m (0, 2));
+
+        painter->setBrush (QBrush (color));
+        painter->fillRect (iconRect, Qt::SolidPattern);
+        painter->setPen (QPen (color.darker ()));
+        painter->drawRect (iconRect);
+
+        painter->restore ();
+
+        const auto text = array_display_string (m);
+        QRect textBoundingRect = option.fontMetrics.boundingRect (text);
+
+        dy = (option.rect.height () - textBoundingRect.height ()) / 2;
+
+        const auto textRect = option.rect.adjusted (
+            padding + spacing + color_cell_icon_width, dy, -padding, -dy);
+
+        const auto role = option.state.testFlag (QStyle::State_Selected)
+                              ? QPalette::HighlightedText
+                              : QPalette::NoRole;
+
+        QApplication::style ()->drawItemText (
+            painter, textRect, Qt::AlignLeft | Qt::AlignVCenter,
+            option.palette, true, text, role);
+      }
+  }
+
+  void go_properties_item_delegate::paint (QPainter *painter,
+                                           const QStyleOptionViewItem &option,
+                                           const QModelIndex &index) const
+  {
+    if (index.column () == COL_VALUE)
+      {
+        const auto *prop
+            = static_cast<base_property *> (index.internalPointer ());
+
+        if (prop->prop_type () == property_type::COLOR)
+          {
+            const auto *cp = static_cast<const color_property *> (prop);
+            if (cp->is_rgb ())
+              {
+                paint_color (painter, option, cp);
+                return;
+              }
+          }
+        else if (specific_matrix_item (prop))
+          {
+            QLocale loc;
+
+            const auto matrix = prop->get ().matrix_value ();
+            const auto orig = option.rect.topLeft ();
+            const int h = option.rect.height () / matrix.rows ();
+            const int w = option.rect.width () / matrix.cols ();
+            const auto align = Qt::AlignHCenter | Qt::AlignVCenter;
+
+            for (int r = 0; r < matrix.rows (); r++)
+              {
+                for (int c = 0; c < matrix.cols (); c++)
+                  {
+                    const auto tl = orig + QPoint (w * c, h * r);
+                    const auto rect = QRect (tl, QSize (w, h));
+                    auto role = QPalette::NoRole;
+                    if (option.state.testFlag (QStyle::State_Selected))
+                      {
+                        role = QPalette::HighlightedText;
+                      }
+                    QApplication::style ()->drawItemText (
+                        painter, rect, align, option.palette, true,
+                        loc.toString (matrix (r, c)), role);
+                  }
+              }
+            painter->save ();
+            QPen pen (QColor (Qt::gray));
+            painter->setPen (pen);
+            for (int r = 1; r < matrix.rows (); r++)
+              {
+                painter->drawLine (
+                    QPoint (option.rect.left (), orig.y () + r * h),
+                    QPoint (option.rect.right (), orig.y () + r * h));
+              }
+            for (int c = 1; c < matrix.cols (); c++)
+              {
+                painter->drawLine (
+                    QPoint (orig.x () + c * w, option.rect.top ()),
+                    QPoint (orig.x () + c * w, option.rect.bottom ()));
+              }
+            painter->restore ();
+            return;
+          }
+      }
+    return QStyledItemDelegate::paint (painter, option, index);
+  }
+
+  QSize
+  go_properties_item_delegate::sizeHint (const QStyleOptionViewItem &option,
+                                         const QModelIndex &index) const
+  {
+    constexpr auto minRowH = 32;
+    constexpr auto padding = 4;
+
+    if (index.column () == COL_VALUE)
+      {
+        const auto *prop
+            = static_cast<base_property *> (index.internalPointer ());
+
+        if (prop->prop_type () == property_type::COLOR)
+          {
+            const auto *cp = static_cast<const color_property *> (prop);
+            if (cp->is_rgb ())
+              {
+                auto m = cp->rgb ();
+                auto text = array_display_string (m);
+                auto textRect = option.fontMetrics.boundingRect (text);
+                return QSize (2 * color_cell_padding + color_cell_spacing
+                                  + color_cell_icon_width + textRect.width (),
+                              2 * color_cell_padding + textRect.height ());
+              }
+          }
+        else if (specific_matrix_item (prop))
+          {
+            QLocale loc;
+            const auto matrix = prop->get ().matrix_value ();
+
+            // we draw all cells of the same size
+            int maxW = 0, maxH = 0;
+            for (int r = 0; r < matrix.rows (); ++r)
+              {
+                for (int c = 0; c < matrix.cols (); ++c)
+                  {
+                    const auto txt = loc.toString (matrix (r, c));
+                    const auto rct = option.fontMetrics.boundingRect (txt);
+                    maxW = std::max (maxW, rct.width ());
+                    maxH = std::max (maxH, rct.height ());
+                  }
+              }
+            maxW += 2 * padding;
+            maxH += 2 * padding;
+
+            return QSize (
+                maxW * matrix.cols (),
+                std::max (static_cast<int> (maxH * matrix.rows ()), minRowH));
+          }
+      }
+    auto sz = QStyledItemDelegate::sizeHint (option, index);
+    sz.setHeight (std::max (sz.height (), minRowH));
+    return sz;
+  }
+
+  // editing
+  QWidget *go_properties_item_delegate::createEditor (
+      QWidget *parent, const QStyleOptionViewItem &option,
+      const QModelIndex &index) const
+  {
+    if (index.column () == COL_VALUE)
+      {
+        const auto *prop
+            = static_cast<base_property *> (index.internalPointer ());
+
+        if (prop->prop_type () == property_type::COLOR)
+          {
+            // Color editor is quite big. it need to fit in at least three rows
+
+            auto *editor = new color_editor_widget (
+                parent, static_cast<const color_property *> (prop));
+
+            resize_editor_cell (editor, index);
+
+            return editor;
+          }
+
+        if (specific_matrix_item (prop))
+          {
+            return new matrix_widget (parent, prop->get ().matrix_value ());
+          }
+
+        if (prop->is_radio ())
+          {
+            QComboBox *w = new QComboBox (parent);
+            const auto cell = prop->values_as_cell ();
+            const auto strvec = cell.string_vector_value ();
+            for (int i = 0; i < strvec.numel (); ++i)
+              {
+                const auto s = strvec.xelem (i);
+                w->addItem (QString::fromStdString (s));
+              }
+            w->setCurrentText (
+                QString::fromStdString (prop->get ().string_value ()));
+            return w;
+          }
+      }
+    return QStyledItemDelegate::createEditor (parent, option, index);
+  }
+
+  void go_properties_item_delegate::resize_editor_cell (
+      QWidget *editor, const QModelIndex &index) const
+  {
+    const auto sz = editor->sizeHint ();
+    m_former_size
+        = QSize (m_view->horizontalHeader ()->sectionSize (index.column ()),
+                 m_view->verticalHeader ()->sectionSize (index.row ()));
+
+    if (sz.width () > m_former_size.width ())
+      {
+        m_view->horizontalHeader ()->resizeSection (index.column (),
+                                                    sz.width ());
+      }
+    if (sz.height () > m_former_size.height ())
+      {
+        m_view->verticalHeader ()->resizeSection (index.row (), sz.height ());
+      }
+  }
+
+  void
+  go_properties_item_delegate::destroyEditor (QWidget *editor,
+                                              const QModelIndex &index) const
+  {
+    if (m_former_size.isValid ())
+      {
+        m_view->horizontalHeader ()->resizeSection (index.column (),
+                                                    m_former_size.width ());
+        m_view->verticalHeader ()->resizeSection (index.row (),
+                                                  m_former_size.height ());
+        m_former_size = QSize{};
+      }
+
+    QStyledItemDelegate::destroyEditor (editor, index);
+  }
+
+  void
+  go_properties_item_delegate::setEditorData (QWidget *editor,
+                                              const QModelIndex &index) const
+  {
+    if (index.column () == COL_VALUE)
+      {
+        const auto *prop
+            = static_cast<base_property *> (index.internalPointer ());
+
+        auto *combo = dynamic_cast<QComboBox *> (editor);
+        if (combo)
+          {
+            combo->setCurrentText (
+                QString::fromStdString (prop->get ().string_value ()));
+            return;
+          }
+
+        auto *cw = dynamic_cast<color_editor_widget *> (editor);
+        if (cw)
+          {
+            const auto ov = prop->get ();
+            if (ov.isnumeric ())
+              {
+                cw->set (color_editor_val (ov.matrix_value ()));
+              }
+            else
+              {
+                cw->set (color_editor_val (ov.string_value ()));
+              }
+            return;
+          }
+        auto *mw = dynamic_cast<matrix_widget *> (editor);
+        if (mw)
+          {
+            mw->set (prop->get ().matrix_value ());
+            return;
+          }
+      }
+    QStyledItemDelegate::setEditorData (editor, index);
+  }
+
+  static QByteArray matrix_byte_array (const Matrix &mat)
+  {
+    QByteArray ba;
+    {
+      QDataStream ds (&ba, QIODevice::WriteOnly);
+      qint64 rows = mat.rows ();
+      qint64 cols = mat.cols ();
+      ds << rows << cols;
+      for (octave_idx_type r = 0; r < mat.rows (); ++r)
+        {
+          for (octave_idx_type c = 0; c < mat.cols (); ++c)
+            {
+              double val = mat (r, c);
+              ds << val;
+            }
+        }
+    }
+    return ba;
+  }
+
+  void
+  go_properties_item_delegate::setModelData (QWidget *editor,
+                                             QAbstractItemModel *model,
+                                             const QModelIndex &index) const
+  {
+    if (index.column () == COL_VALUE)
+      {
+        // const auto *prop
+        //     = static_cast<base_property *> (index.internalPointer ());
+
+        QComboBox *w = dynamic_cast<QComboBox *> (editor);
+        if (w)
+          {
+            model->setData (index, w->currentText (), Qt::EditRole);
+            return;
+          }
+
+        auto *cw = dynamic_cast<color_editor_widget *> (editor);
+        if (cw)
+          {
+            const auto val = cw->get ();
+            switch (val.type ())
+              {
+              case color_editor_val::STR:
+                model->setData (index,
+                                QString::fromStdString (val.string_value ()),
+                                Qt::EditRole);
+                break;
+              case color_editor_val::RGB:
+                model->setData (index, matrix_byte_array (val.rgb_value ()),
+                                Qt::EditRole);
+              }
+            return;
+          }
+
+        auto *mw = dynamic_cast<matrix_widget *> (editor);
+        if (mw)
+          {
+            bool ok = false;
+            auto mat = mw->get (&ok);
+            if (!ok)
+              return;
+            model->setData (index, matrix_byte_array (mat), Qt::EditRole);
+          }
+      }
+    QStyledItemDelegate::setModelData (editor, model, index);
+  }
+
+  void go_properties_item_delegate::updateEditorGeometry (
+      QWidget *editor, const QStyleOptionViewItem &option,
+      const QModelIndex &index) const
+  {
+    QStyledItemDelegate::updateEditorGeometry (editor, option, index);
+  }
+
+  class numbers_only_key_filter : public QObject
+  {
+  public:
+    numbers_only_key_filter (QObject *p) : QObject (p) {}
+    virtual ~numbers_only_key_filter () {}
+
+  protected:
+    bool eventFilter (QObject *obj, QEvent *ev) override
+    {
+      if (ev->type () == QEvent::KeyPress || ev->type () == QEvent::KeyRelease)
+        {
+          auto *kev = static_cast<QKeyEvent *> (ev);
+          switch (kev->key ())
+            {
+            case Qt::Key_0:
+            case Qt::Key_1:
+            case Qt::Key_2:
+            case Qt::Key_3:
+            case Qt::Key_4:
+            case Qt::Key_5:
+            case Qt::Key_6:
+            case Qt::Key_7:
+            case Qt::Key_8:
+            case Qt::Key_9:
+            case Qt::Key_Period:
+            case Qt::Key_Comma:
+              break;
+            default:
+              return true;
+            }
+        }
+      return QObject::eventFilter (obj, ev);
+    }
+  };
+
+  matrix_widget::matrix_widget (QWidget *parent, const Matrix &matrix)
+      : QWidget (parent)
+  {
+    setAutoFillBackground (true);
+    setFocusPolicy (Qt::StrongFocus);
+
+    m_layout = new QGridLayout (this);
+    m_layout->setSpacing (0);
+    m_layout->setMargin (0);
+
+    auto *numbers_only = new numbers_only_key_filter (this);
+
+    QLocale loc;
+
+    for (int r = 0; r < static_cast<int> (matrix.rows ()); ++r)
+      {
+        for (int c = 0; c < static_cast<int> (matrix.cols ()); ++c)
+          {
+            const double val = matrix (r, c);
+            auto *edit = new QLineEdit (loc.toString (val), this);
+            edit->installEventFilter (numbers_only);
+            m_layout->addWidget (edit, r, c);
+          }
+      }
+  }
+
+  static double parseDouble (const QString &txt)
+  {
+    // we can't just use QLocale::toDouble as it will almost always fail.
+    // In fact it even fails to parse result of QLocale::toString.
+    // so we analyse the string a bit and convert to C locale before parsing.
+    // NaN is returned if parsing doesn't succeed.
+
+    QString C;
+
+    // step 1: remove all whitespace
+    std::copy_if (txt.begin (), txt.end (), std::back_inserter (C),
+                  [] (QChar c) { return !c.isSpace (); });
+
+    // step 2: remove separators (can be dot or comma) and normalize decimal
+    // separator (can also be dot or comma but would be the last one)
+    int dots = C.count ('.');
+    int lastDot = C.lastIndexOf ('.');
+    int commas = C.count (',');
+    int lastComma = C.lastIndexOf (',');
+
+    if (lastDot > lastComma)
+      {
+        // dot is decimal sep, comma (if any) are thousands sep
+        if (dots > 1)
+          {
+            return std::numeric_limits<double>::quiet_NaN ();
+          }
+        C.replace (",", "");
+      }
+    if (lastComma > lastDot)
+      {
+        // comma is decimal sep, dot (if any) are thousands sep
+        if (commas > 1)
+          {
+            return std::numeric_limits<double>::quiet_NaN ();
+          }
+        C.replace (".", "");
+        C.replace (",", ".");
+      }
+
+    bool ok = false;
+    const auto val = C.toDouble (&ok);
+    return ok ? val : std::numeric_limits<double>::quiet_NaN ();
+  }
+
+  Matrix matrix_widget::get (bool *ok) const
+  {
+    Matrix matrix (m_layout->rowCount (), m_layout->columnCount ());
+
+    for (int r = 0; r < static_cast<int> (matrix.rows ()); ++r)
+      {
+        for (int c = 0; c < static_cast<int> (matrix.cols ()); ++c)
+          {
+            auto *item = m_layout->itemAtPosition (r, c);
+            auto *edit
+                = item ? dynamic_cast<QLineEdit *> (item->widget ()) : nullptr;
+            if (!edit)
+              {
+                if (ok)
+                  *ok = false;
+                return matrix;
+              }
+            const double val = parseDouble (edit->text ());
+            if (val != val)
+              {
+                // NaN
+                if (ok)
+                  *ok = false;
+                return matrix;
+              }
+            matrix (r, c) = val;
+          }
+      }
+    if (ok)
+      *ok = true;
+    return matrix;
+  }
+
+  void matrix_widget::set (const Matrix &matrix)
+  {
+    if (matrix.rows () != m_layout->rowCount ()
+        || matrix.cols () != m_layout->columnCount ())
+      return;
+
+    QLocale loc;
+
+    for (int r = 0; r < static_cast<int> (matrix.rows ()); ++r)
+      {
+        for (int c = 0; c < static_cast<int> (matrix.cols ()); ++c)
+          {
+            auto *edit
+                = dynamic_cast<QLineEdit *> (m_layout->itemAtPosition (r, c));
+            if (!edit)
+              continue;
+            edit->setText (loc.toString (matrix (r, c)));
+          }
+      }
+  }
+
+  color_editor_val::color_editor_val (const std::string &str)
+      : m_type{ STR }, m_str{ str }
+  {
+  }
+
+  color_editor_val::color_editor_val (const Matrix &rgb)
+      : m_type{ RGB }, m_rgb{ rgb }
+  {
+  }
+
+  std::string color_editor_val::string_value () const
+  {
+    if (m_type != STR)
+      {
+        throw std::logic_error{ "color_editor_val: not a string" };
+      }
+    return m_str;
+  }
+
+  Matrix color_editor_val::rgb_value () const
+  {
+    if (m_type != RGB)
+      {
+        throw std::logic_error{ "color_editor_val: not RGB" };
+      }
+    return m_rgb;
+  }
+
+  // return #RRGGBB
+  static QString color_web_code (const QColor &color)
+  {
+    char buf[64];
+    if (std::snprintf (buf, 64, "#%02X%02X%02X", color.red (), color.green (),
+                       color.blue ())
+        == 7)
+      {
+        buf[7] = '\0';
+        return QString (buf);
+      }
+
+    return QString{};
+  }
+
+  constexpr auto default_icon_sz = QSize{ 35, 23 };
+
+  static QPixmap color_pixmap (QColor color, QSize size = default_icon_sz)
+  {
+    QPixmap pm{ size };
+    {
+      QPainter painter{ &pm };
+      QRect r{ QPoint{ 0, 0 }, size };
+      painter.setBrush (QBrush{ color });
+      painter.fillRect (r, Qt::SolidPattern);
+      painter.setPen (QPen{ color.darker () });
+      painter.drawRect (r);
+    }
+    return pm;
+  }
+
+  static QIcon color_icon (QColor color, QSize size = default_icon_sz)
+  {
+    auto pm = color_pixmap (color, size);
+    return QIcon{ pm };
+  }
+
+  struct color_entry
+  {
+    QString name;     // e.g. "aliceblue" or "red"
+    QString web_code; // #RRGGBB
+    QColor color;
+    QIcon icon; // for the drop down list
+  };
+
+  static std::vector<QString> svg_color_names ();
+
+  static const std::vector<color_entry> &svg_colors ()
+  {
+    static std::vector<color_entry> result;
+    if (result.size () == 0)
+      {
+        const auto names = svg_color_names ();
+        result.reserve (names.size ());
+        for (const auto &name : names)
+          {
+            const auto color = QColor (name);
+            const auto web_code = color_web_code (color);
+            const auto icon = color_icon (color);
+            result.push_back (color_entry{ name, web_code, color, icon });
+          }
+      }
+    return result;
+  }
+
+  static const std::vector<color_entry> &octave_colors ()
+  {
+    static std::vector<color_entry> result;
+    if (result.size () == 0)
+      {
+        const auto src = std::vector<std::pair<QString, QString>>{
+          { "blue", "#0000FF" },    { "black", "#000000" },
+          { "cyan", "#00FFFF" },    { "green", "#00FF00" },
+          { "magenta", "#FF00FF" }, { "red", "#FF0000" },
+          { "white", "#FFFFFF" },   { "yellow", "#FFFF00" },
+        };
+
+        result.reserve (src.size ());
+        for (const auto &pair : src)
+          {
+            const auto color = QColor (pair.second);
+            const auto icon = color_icon (color);
+            result.push_back (
+                color_entry{ pair.first, pair.second, color, icon });
+          }
+      }
+    return result;
+  }
+
+  // functor for std::find_if
+  struct by_web_code
+  {
+    QString web_code;
+
+    by_web_code (const QString &wc) : web_code{ wc.toUpper () } {}
+
+    bool operator() (const color_entry &entry) const
+    {
+      return entry.web_code == web_code;
+    }
+  };
+
+  color_editor_widget::color_editor_widget (QWidget *parent,
+                                            const color_property *prop)
+      : QWidget{ parent }, ui{ new Ui_color_editor_widget }
+  {
+    ui->setupUi (this);
+    // need to setup a button group because parent widget is not the same
+    auto *grp = new QButtonGroup (this);
+    grp->addButton (ui->radioRad);
+    grp->addButton (ui->rgbValRad);
+    grp->setExclusive (true);
+
+    setAutoFillBackground (true);
+    setFocusPolicy (Qt::StrongFocus);
+
+    auto cell = prop->values_as_cell ();
+    const auto strvec = cell.string_vector_value ();
+    for (int i = 0; i < strvec.numel (); ++i)
+      {
+        const auto s = strvec.xelem (i);
+        ui->radioBox->addItem (QString::fromStdString (s));
+      }
+
+    // if property do not have any radio value, then it only accepts RGB values
+    if (strvec.numel () == 0 && prop->is_rgb ())
+      {
+        ui->radioWgt->setVisible (false);
+        ui->line->setVisible (false);
+        ui->rgbValRad->setVisible (false);
+      }
+
+    ui->octaveBox->addItem ("");
+    for (const auto &col : octave_colors ())
+      {
+        ui->octaveBox->addItem (col.icon, col.name, col.web_code);
+      }
+
+    for (const auto &col : svg_colors ())
+      {
+        ui->svgBox->addItem (col.icon, col.name, col.web_code);
+      }
+
+    if (prop->is_rgb ())
+      {
+        const auto m = prop->rgb ();
+        if (m.rows () != 1 || m.cols () != 3)
+          {
+            throw std::logic_error{ "unconsistent color value" };
+          }
+        const auto col = color_from_doubles (m (0, 0), m (0, 1), m (0, 2));
+        set_rgb (col);
+      }
+    else
+      {
+        ui->radioBox->setCurrentText (
+            QString::fromStdString (prop->current_value ()));
+        set_rgb (QColor ("black"));
+      }
+
+    ui->radioRad->setChecked (prop->is_radio ());
+    ui->radioBox->setEnabled (prop->is_radio ());
+    ui->rgbValRad->setChecked (prop->is_rgb ());
+    ui->rgbWgt1->setEnabled (prop->is_rgb ());
+    ui->rgbWgt2->setEnabled (prop->is_rgb ());
+
+    QObject::connect (ui->radioRad, &QRadioButton::toggled, ui->radioBox,
+                      &QWidget::setEnabled);
+    QObject::connect (ui->rgbValRad, &QRadioButton::toggled, ui->rgbWgt1,
+                      &QWidget::setEnabled);
+    QObject::connect (ui->rgbValRad, &QRadioButton::toggled, ui->rgbWgt2,
+                      &QWidget::setEnabled);
+
+    QObject::connect (ui->rSpn,
+                      QOverload<double>::of (&QDoubleSpinBox::valueChanged),
+                      this, &color_editor_widget::handle_rgb_mat_change);
+    QObject::connect (ui->gSpn,
+                      QOverload<double>::of (&QDoubleSpinBox::valueChanged),
+                      this, &color_editor_widget::handle_rgb_mat_change);
+    QObject::connect (ui->bSpn,
+                      QOverload<double>::of (&QDoubleSpinBox::valueChanged),
+                      this, &color_editor_widget::handle_rgb_mat_change);
+
+    QObject::connect (
+        ui->octaveBox, QOverload<int>::of (&QComboBox::currentIndexChanged),
+        this, &color_editor_widget::handle_rgb_octave_color_change);
+
+    QObject::connect (ui->svgBox, &QComboBox::currentTextChanged, this,
+                      &color_editor_widget::handle_rgb_svg_change);
+
+    QObject::connect (ui->pickBtn, &QPushButton::clicked, this,
+                      &color_editor_widget::handle_rgb_pick);
+  }
+
+  color_editor_widget::~color_editor_widget () { delete ui; }
+
+  void color_editor_widget::set (const color_editor_val &val)
+  {
+    switch (val.type ())
+      {
+      case color_editor_val::STR:
+        ui->radioRad->setChecked (true);
+        ui->radioBox->setCurrentText (
+            QString::fromStdString (val.string_value ()));
+        break;
+      case color_editor_val::RGB:
+        ui->rgbValRad->setChecked (true);
+        const auto m = val.rgb_value ();
+        if (m.cols () != 3)
+          {
+            throw std::logic_error{ "inconsistent color matrix" };
+          }
+        set_rgb (color_from_doubles (m (0, 0), m (0, 1), m (0, 2)));
+        break;
+      }
+  }
+
+  color_editor_val color_editor_widget::get () const
+  {
+    if (ui->radioRad->isChecked ())
+      {
+        return color_editor_val (ui->radioBox->currentText ().toStdString ());
+      }
+    else if (ui->rgbValRad->isChecked ())
+      {
+        auto m = Matrix (1, 3);
+        m (0, 0) = ui->rSpn->value ();
+        m (0, 1) = ui->gSpn->value ();
+        m (0, 2) = ui->bSpn->value ();
+        return color_editor_val (m);
+      }
+    return color_editor_val ("");
+  }
+
+  void color_editor_widget::set_rgb (const QColor &color, rgb_source source)
+  {
+    if (source != rgb_source::spinbox)
+      {
+        ui->rSpn->setValue (color.redF ());
+        ui->gSpn->setValue (color.greenF ());
+        ui->bSpn->setValue (color.blueF ());
+      }
+
+    const auto web_code = color_web_code (color);
+
+    if (source != rgb_source::octave)
+      {
+        const auto &oct_cols = octave_colors ();
+        auto oct_it = std::find_if (oct_cols.begin (), oct_cols.end (),
+                                    by_web_code{ web_code });
+        if (oct_it != oct_cols.end ())
+          {
+            ui->octaveBox->setCurrentIndex (
+                static_cast<int> (oct_it - oct_cols.begin ()) + 1);
+          }
+        else
+          {
+            ui->octaveBox->setCurrentIndex (0);
+          }
+      }
+
+    if (source != rgb_source::svg)
+      {
+        const auto &svg_cols = svg_colors ();
+        auto svg_it = std::find_if (svg_cols.begin (), svg_cols.end (),
+                                    by_web_code{ web_code });
+        if (svg_it != svg_cols.end ())
+          {
+            ui->svgBox->setCurrentIndex (
+                static_cast<int> (svg_it - svg_cols.begin ()));
+          }
+        else
+          {
+            ui->svgBox->setCurrentText (color_web_code (color));
+          }
+      }
+
+    const auto sz = ui->rgbIcon->size ().shrunkBy (QMargins (4, 4, 4, 4));
+    auto pixmap = color_pixmap (color, sz);
+    ui->rgbIcon->setPixmap (pixmap);
+  }
+
+  void color_editor_widget::handle_rgb_mat_change ()
+  {
+    const auto color = color_from_doubles (
+        ui->rSpn->value (), ui->gSpn->value (), ui->bSpn->value ());
+    set_rgb (color, rgb_source::spinbox);
+  }
+
+  void color_editor_widget::handle_rgb_octave_color_change ()
+  {
+    if (ui->octaveBox->currentIndex () <= 0)
+      return;
+
+    const auto &oct_cols = octave_colors ();
+
+    set_rgb (oct_cols[ui->octaveBox->currentIndex () - 1].color,
+             rgb_source::octave);
+  }
+
+  void color_editor_widget::handle_rgb_svg_change ()
+  {
+    // this is an editable combo box, the user may enter web code value, or
+    // svg name directly. If string is not a valid color, indicate by
+    // displaying red text
+
+    const auto value = ui->svgBox->currentText ();
+
+    const auto col = QColor (value);
+    auto *edit = ui->svgBox->lineEdit ();
+    auto palette = edit->palette ();
+    // discriminate #RGB web code (only accept #RRGGBB)
+    if (!col.isValid () || (value.startsWith('#') && value.size() != 7))
+      {
+        palette.setColor (QPalette::Text, QColor ("red"));
+      }
+    else
+      {
+        set_rgb (col, rgb_source::svg);
+        palette.setColor (QPalette::Text, QColor ("black"));
+      }
+    edit->setPalette (palette);
+  }
+
+  void color_editor_widget::handle_rgb_pick ()
+  {
+    auto val = get ();
+    QColor c;
+    if (ui->rgbValRad->isChecked ())
+      {
+        c = color_from_doubles (ui->rSpn->value (), ui->gSpn->value (),
+                                ui->bSpn->value ());
+      }
+    QColorDialog dlg (c, this);
+    dlg.setOption (QColorDialog::ShowAlphaChannel, false);
+    if (dlg.exec () == QColorDialog::Accepted)
+      {
+        set_rgb (dlg.currentColor ());
+      }
+  }
+
+  static std::vector<QString> svg_color_names ()
+  {
+    return std::vector<QString>{ "aliceblue",
+                                 "antiquewhite",
+                                 "aqua",
+                                 "aquamarine",
+                                 "azure",
+                                 "beige",
+                                 "bisque",
+                                 "black",
+                                 "blanchedalmond",
+                                 "blue",
+                                 "blueviolet",
+                                 "brown",
+                                 "burlywood",
+                                 "cadetblue",
+                                 "chartreuse",
+                                 "chocolate",
+                                 "coral",
+                                 "cornflowerblue",
+                                 "cornsilk",
+                                 "crimson",
+                                 "cyan",
+                                 "darkblue",
+                                 "darkcyan",
+                                 "darkgoldenrod",
+                                 "darkgray",
+                                 "darkgreen",
+                                 "darkgrey",
+                                 "darkkhaki",
+                                 "darkmagenta",
+                                 "darkolivegreen",
+                                 "darkorange",
+                                 "darkorchid",
+                                 "darkred",
+                                 "darksalmon",
+                                 "darkseagreen",
+                                 "darkslateblue",
+                                 "darkslategray",
+                                 "darkslategrey",
+                                 "darkturquoise",
+                                 "darkviolet",
+                                 "deeppink",
+                                 "deepskyblue",
+                                 "dimgray",
+                                 "dimgrey",
+                                 "dodgerblue",
+                                 "firebrick",
+                                 "floralwhite",
+                                 "forestgreen",
+                                 "fuchsia",
+                                 "gainsboro",
+                                 "ghostwhite",
+                                 "gold",
+                                 "goldenrod",
+                                 "gray",
+                                 "grey",
+                                 "green",
+                                 "greenyellow",
+                                 "honeydew",
+                                 "hotpink",
+                                 "indianred",
+                                 "indigo",
+                                 "ivory",
+                                 "khaki",
+                                 "lavender",
+                                 "lavenderblush",
+                                 "lawngreen",
+                                 "lemonchiffon",
+                                 "lightblue",
+                                 "lightcoral",
+                                 "lightcyan",
+                                 "lightgoldenrodyellow",
+                                 "lightgray",
+                                 "lightgreen",
+                                 "lightgrey",
+                                 "lightpink",
+                                 "lightsalmon",
+                                 "lightseagreen",
+                                 "lightskyblue",
+                                 "lightslategray",
+                                 "lightslategrey",
+                                 "lightsteelblue",
+                                 "lightyellow",
+                                 "lime",
+                                 "limegreen",
+                                 "linen",
+                                 "magenta",
+                                 "maroon",
+                                 "mediumaquamarine",
+                                 "mediumblue",
+                                 "mediumorchid",
+                                 "mediumpurple",
+                                 "mediumseagreen",
+                                 "mediumslateblue",
+                                 "mediumspringgreen",
+                                 "mediumturquoise",
+                                 "mediumvioletred",
+                                 "midnightblue",
+                                 "mintcream",
+                                 "mistyrose",
+                                 "moccasin",
+                                 "navajowhite",
+                                 "navy",
+                                 "oldlace",
+                                 "olive",
+                                 "olivedrab",
+                                 "orange",
+                                 "orangered",
+                                 "orchid",
+                                 "palegoldenrod",
+                                 "palegreen",
+                                 "paleturquoise",
+                                 "palevioletred",
+                                 "papayawhip",
+                                 "peachpuff",
+                                 "peru",
+                                 "pink",
+                                 "plum",
+                                 "powderblue",
+                                 "purple",
+                                 "red",
+                                 "rosybrown",
+                                 "royalblue",
+                                 "saddlebrown",
+                                 "salmon",
+                                 "sandybrown",
+                                 "seagreen",
+                                 "seashell",
+                                 "sienna",
+                                 "silver",
+                                 "skyblue",
+                                 "slateblue",
+                                 "slategray",
+                                 "slategrey",
+                                 "snow",
+                                 "springgreen",
+                                 "steelblue",
+                                 "tan",
+                                 "teal",
+                                 "thistle",
+                                 "tomato",
+                                 "turquoise",
+                                 "violet",
+                                 "wheat",
+                                 "white",
+                                 "whitesmoke",
+                                 "yellow",
+                                 "yellowgreen" };
+  }
+}
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector-table.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector-table.h	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,200 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#if !defined(octave_property_inspector_table_h)
+#define octave_property_inspector_table_h 1
+
+#include <memory>
+
+#include <QAbstractTableModel>
+#include <QStyledItemDelegate>
+#include <QWidget>
+
+#include "Matrix.h"
+
+#include "graphics-handle.h"
+#include "graphics.h"
+
+class QGridLayout;
+class QTableView;
+
+class color_property;
+
+class Ui_color_editor_widget;
+
+namespace octave
+{
+  class base_qobject;
+
+  class go_properties_table_model : public QAbstractTableModel
+  {
+    Q_OBJECT
+
+  public:
+    go_properties_table_model (QObject *p, base_qobject &oct_qobj,
+                               const graphics_object &go);
+    ~go_properties_table_model ();
+
+    int columnCount (const QModelIndex &parent = QModelIndex{}) const override;
+
+    int rowCount (const QModelIndex &parent = QModelIndex{}) const override;
+
+    QModelIndex index (int row, int column,
+                       const QModelIndex &parent) const override;
+
+    Qt::ItemFlags flags (const QModelIndex &index) const override;
+
+    QVariant headerData (int section, Qt::Orientation orientation,
+                         int role) const override;
+
+    QVariant data (const QModelIndex &index, int role) const override;
+
+    bool setData (const QModelIndex &indes, const QVariant &value,
+                  int role) override;
+
+  private:
+    base_qobject &m_oct_qobj;
+    graphics_object m_go;
+
+    std::vector<std::string> m_names;
+  };
+
+  class go_properties_item_delegate : public QStyledItemDelegate
+  {
+    Q_OBJECT
+  public:
+    go_properties_item_delegate (QObject *parent, base_qobject &oct_qobj,
+                                 QTableView *view);
+    ~go_properties_item_delegate () = default;
+
+    // painting
+    void paint (QPainter *painter, const QStyleOptionViewItem &option,
+                const QModelIndex &index) const override;
+    QSize sizeHint (const QStyleOptionViewItem &option,
+                    const QModelIndex &index) const override;
+
+    // editing
+    QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem &option,
+                           const QModelIndex &index) const override;
+
+    void setEditorData (QWidget *editor,
+                        const QModelIndex &index) const override;
+
+    void setModelData (QWidget *editor, QAbstractItemModel *model,
+                       const QModelIndex &index) const override;
+
+    void updateEditorGeometry (QWidget *editor,
+                               const QStyleOptionViewItem &option,
+                               const QModelIndex &index) const override;
+
+    void destroyEditor (QWidget *editor,
+                        const QModelIndex &index) const override;
+
+  signals:
+
+    void layoutWillChange (int row) const;
+
+  private:
+    void resize_editor_cell (QWidget *editor, const QModelIndex &index) const;
+
+    base_qobject &m_oct_qobj;
+    QTableView *m_view;
+    mutable QSize m_former_size;
+  };
+
+  // widget to edit (small) matrix and arrays
+  class matrix_widget : public QWidget
+  {
+    Q_OBJECT
+  public:
+    matrix_widget (QWidget *parent, const Matrix &matrix);
+
+    virtual ~matrix_widget () = default;
+
+    void set (const Matrix &matrix);
+    Matrix get (bool *ok = nullptr) const;
+
+  private:
+    QGridLayout *m_layout;
+  };
+
+  class color_editor_val
+  {
+  public:
+    enum value_type
+    {
+      STR,
+      RGB,
+    };
+
+    color_editor_val (const std::string &str);
+    color_editor_val (const Matrix &rgb);
+
+    value_type type () const { return m_type; }
+    std::string string_value () const;
+    Matrix rgb_value () const;
+
+  private:
+    value_type m_type;
+    std::string m_str;
+    Matrix m_rgb;
+  };
+
+  class color_editor_widget : public QWidget
+  {
+    Q_OBJECT
+  public:
+    color_editor_widget (QWidget *parent, const color_property *prop);
+    virtual ~color_editor_widget ();
+
+    void set (const color_editor_val &val);
+    color_editor_val get () const;
+
+  signals:
+    void rgb_changed (const QColor &rgb);
+
+  private:
+    enum class rgb_source
+    {
+      spinbox,
+      octave,
+      svg,
+      other,
+    };
+
+  private slots:
+    void set_rgb (const QColor &color, rgb_source source = rgb_source::other);
+    void handle_rgb_mat_change ();
+    void handle_rgb_octave_color_change ();
+    void handle_rgb_svg_change ();
+    void handle_rgb_pick ();
+
+  private:
+    Ui_color_editor_widget *ui;
+  };
+
+}
+
+#endif
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector-tree.cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector-tree.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,345 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <algorithm>
+#include <vector>
+
+#include <QThread>
+
+#include "caseless-str.h"
+
+#include "graphics.h"
+#include "interpreter.h"
+
+#include "octave-qobject.h"
+#include "qt-application.h"
+
+#include "property-inspector-tree.h"
+
+namespace octave
+{
+
+  using node_ref = std::shared_ptr<pi_tree_node>;
+
+  // a node corresponding to a single graphics object
+  class pi_tree_node : public std::enable_shared_from_this<pi_tree_node>
+  {
+  public:
+    pi_tree_node (const graphics_object& gobj, int row,
+                  const node_ref& parent = nullptr)
+        : m_gh{ gobj.get_handle () }, m_row{ row }, m_parent{ parent }
+    {
+    }
+
+    // not in ctor because we need shared_from_this
+    void prepare (gh_manager& gh_mgr)
+    {
+      auto obj = gh_mgr.get_object (m_gh);
+
+      m_type = QString::fromStdString (obj.type ());
+
+      Matrix kids = obj.get_properties ().get_children ();
+      for (octave_idx_type i = 0; i < kids.numel (); ++i)
+        {
+          const auto kid = gh_mgr.get_object (kids (i));
+          if (!kid)
+            continue;
+
+          const auto r = static_cast<int> (m_children.size ());
+          const auto c
+              = std::make_shared<pi_tree_node> (kid, r, shared_from_this ());
+          c->prepare (gh_mgr);
+          m_children.push_back (c);
+        }
+    }
+
+    node_ref parent () const { return m_parent.lock (); }
+    graphics_handle handle () const { return m_gh; }
+    int row () const { return m_row; }
+    QString type () const { return m_type; }
+
+    const std::vector<node_ref>& children () const { return m_children; }
+
+    node_ref lookup (const graphics_object& go)
+    {
+      if (go.get_handle () == m_gh)
+        return shared_from_this ();
+      for (const auto& c : m_children)
+        {
+          auto n = c->lookup (go);
+          if (n)
+            return n;
+        }
+      return nullptr;
+    }
+
+    node_ref add_child (const node_ref& node)
+    {
+      m_children.push_back (node);
+      return node;
+    }
+
+    void rem_child (const node_ref& node)
+    {
+      if (node->row () >= static_cast<int> (m_children.size ()))
+        {
+          // error
+        }
+      if (m_children[node->row ()] != node)
+        {
+          // error
+        }
+      auto it = std::begin (m_children) + node->row ();
+      m_children.erase (it);
+      // reindex lower children
+      for (int r = node->row (); r < static_cast<int> (m_children.size ());
+           ++r)
+        {
+          m_children[r]->m_row = r;
+        }
+    }
+
+  private:
+    graphics_handle m_gh;
+    int m_row;
+    QString m_type;
+    std::weak_ptr<pi_tree_node> m_parent;
+    std::vector<node_ref> m_children;
+  };
+
+  go_tree_model::go_tree_model (QObject* p, base_qobject& oct_qobj)
+      : QAbstractItemModel (p), m_oct_qobj{ oct_qobj }
+  {
+    auto gh_mgr = oct_qobj.interpreter_qobj ()->get_gh_manager ();
+
+    graphics_object root = gh_mgr->get_object (0);
+
+    initialize (*gh_mgr, root);
+  }
+
+  go_tree_model::go_tree_model (QObject* p, base_qobject& oct_qobj,
+                                const graphics_object& root)
+      : QAbstractItemModel (p), m_oct_qobj{ oct_qobj }
+  {
+    auto gh_mgr = oct_qobj.interpreter_qobj ()->get_gh_manager ();
+
+    initialize (*gh_mgr, root);
+  }
+
+  void go_tree_model::initialize (gh_manager& gh_mgr,
+                                  const graphics_object& root)
+  {
+    if (!root)
+      {
+        throw new std::logic_error (
+            "go_tree_model::initialize: invalide root");
+      }
+
+    auto events = m_oct_qobj.get_qt_interpreter_events ();
+
+    QObject::connect (events.get (), &qt_interpreter_events::go_created, this,
+                      &go_tree_model::add_graphics_object);
+
+    QObject::connect (events.get (), &qt_interpreter_events::go_will_delete,
+                      this, &go_tree_model::rem_graphics_object);
+
+    m_root = std::make_shared<pi_tree_node> (root, 0);
+    m_root->prepare (gh_mgr);
+  }
+
+  go_tree_model::~go_tree_model () {}
+
+  QModelIndex go_tree_model::index (int row, int column,
+                                    const QModelIndex& parent) const
+  {
+    auto pnode = get_node (parent);
+    if (!pnode)
+      {
+        // root index
+        return node_index (m_root, column);
+      }
+    else
+      {
+        const auto& children = pnode->children ();
+        if (row >= 0 && row < static_cast<int> (children.size ()))
+          {
+            const auto child = children[row];
+            return node_index (child, column);
+          }
+      }
+    return QModelIndex{};
+  }
+
+  QModelIndex go_tree_model::parent (const QModelIndex& index) const
+  {
+    const auto node = get_node (index);
+    if (node)
+      return node_index (node->parent ());
+    return QModelIndex{};
+  }
+
+  int go_tree_model::columnCount (const QModelIndex&) const { return 2; }
+
+  int go_tree_model::rowCount (const QModelIndex& parent) const
+  {
+    const auto pnode = get_node (parent);
+    if (!pnode)
+      return m_root ? 1 : 0;
+    else
+      {
+        return static_cast<int> (pnode->children ().size ());
+      }
+  }
+
+  QVariant go_tree_model::headerData (int section, Qt::Orientation orientation,
+                                      int role) const
+  {
+    if (orientation != Qt::Horizontal)
+      return QVariant{};
+    if (role != Qt::DisplayRole)
+      return QVariant{};
+    switch (section)
+      {
+      case 0:
+        return "Handle type";
+      case 1:
+        return "Value";
+      default:
+        break;
+      }
+    return QVariant{};
+  }
+
+  QVariant go_tree_model::data (const QModelIndex& index, int role) const
+  {
+    const auto pnode = get_node (index);
+    if (!pnode)
+      return QVariant{};
+    if (role == Qt::DisplayRole)
+      {
+        switch (index.column ())
+          {
+          case 0:
+            return pnode->type ();
+          case 1:
+            return pnode->handle ().value ();
+          default:
+            break;
+          }
+      }
+    return QVariant{};
+  }
+
+  graphics_object go_tree_model::get_go_at (const QModelIndex& index) const
+  {
+
+    const auto node = get_node (index);
+    if (node)
+      {
+        auto gh_mgr = m_oct_qobj.interpreter_qobj ()->get_gh_manager ();
+        const auto gh = node->handle ();
+        return gh_mgr->get_object (gh);
+      }
+    return graphics_object{};
+  }
+
+  void go_tree_model::add_graphics_object (const graphics_object& go)
+  {
+    auto gh_mgr = m_oct_qobj.interpreter_qobj ()->get_gh_manager ();
+
+    const auto ph = go.get_parent ();
+    const auto pnode = lookup (gh_mgr->get_object (ph));
+    if (!pnode)
+      return;
+
+    const auto pindex = node_index (pnode);
+    const auto r = static_cast<int> (pnode->children ().size ());
+
+    beginInsertRows (pindex, r, r);
+
+    const auto node = std::make_shared<pi_tree_node> (go, r, pnode);
+    node->prepare (*gh_mgr);
+    pnode->add_child (node);
+
+    endInsertRows ();
+  }
+
+  void go_tree_model::rem_graphics_object (const graphics_object& go)
+  {
+    const auto node = lookup (go);
+    if (!node)
+      return;
+
+    const auto pnode = node->parent ();
+    const auto pindex = node_index (pnode);
+
+    if (!pnode)
+      throw std::logic_error ("go_tree_model::rem_graphics_object: "
+                              "parentless node");
+
+    beginRemoveRows (pindex, node->row (), node->row ());
+
+    pnode->rem_child (node);
+
+    endRemoveRows ();
+  }
+
+  QModelIndex
+  go_tree_model::node_index (const std::shared_ptr<pi_tree_node>& node,
+                             int column) const
+  {
+    if (!node)
+      return QModelIndex{};
+
+    return createIndex (node->row (), column,
+                        static_cast<void*> (node.get ()));
+  }
+
+  node_ref go_tree_model::get_node (const QModelIndex& index) const
+  {
+    if (!index.isValid ())
+      return nullptr;
+    if (!index.internalPointer ())
+      return nullptr;
+
+    return static_cast<pi_tree_node*> (index.internalPointer ())
+        ->shared_from_this ();
+  }
+
+  node_ref go_tree_model::lookup (const graphics_object& obj) const
+  {
+    if (!obj)
+      return nullptr;
+    if (!m_root)
+      return nullptr;
+
+    return m_root->lookup (obj);
+  }
+
+}
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector-tree.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector-tree.h	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#if !defined(octave_property_inspector_tree_h)
+#define octave_property_inspector_tree_h 1
+
+#include <memory>
+
+#include <QAbstractTableModel>
+
+#include "graphics-handle.h"
+
+class gh_manager;
+class graphics_object;
+
+namespace octave
+{
+  class base_qobject;
+
+  class pi_tree_node;
+
+  class go_tree_model : public QAbstractItemModel
+  {
+    Q_OBJECT
+
+  public:
+    go_tree_model (QObject* p, base_qobject& oct_qobj);
+    go_tree_model (QObject* p, base_qobject& oct_qobj,
+                   const graphics_object& root);
+
+  private:
+    void initialize (gh_manager& gh_mgr, const graphics_object& root);
+
+  public:
+    virtual ~go_tree_model ();
+
+    QModelIndex index (int row, int column,
+                       const QModelIndex& parent) const override;
+    QModelIndex parent (const QModelIndex& index) const override;
+
+    int columnCount (const QModelIndex& parent) const override;
+
+    int rowCount (const QModelIndex& parent) const override;
+
+    QVariant headerData (int section, Qt::Orientation orientation,
+                         int role) const override;
+
+    QVariant data (const QModelIndex& index, int role) const override;
+
+    graphics_object get_go_at (const QModelIndex& index) const;
+
+  private slots:
+    void add_graphics_object (const graphics_object& go);
+    void rem_graphics_object (const graphics_object& go);
+
+  private:
+    base_qobject& m_oct_qobj;
+    std::shared_ptr<pi_tree_node> m_root;
+
+    QModelIndex node_index (const std::shared_ptr<pi_tree_node>& node,
+                            int column = 0) const;
+    std::shared_ptr<pi_tree_node> get_node (const QModelIndex& index) const;
+    std::shared_ptr<pi_tree_node> lookup (const graphics_object& obj) const;
+  };
+}
+
+#endif
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector.cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,121 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <QHeaderView>
+#include <QSplitter>
+#include <QTableView>
+#include <QTreeView>
+
+#include "octave-qobject.h"
+#include "property-inspector-table.h"
+#include "property-inspector-tree.h"
+#include "property-inspector.h"
+#include "qt-application.h"
+
+namespace octave
+{
+
+  property_inspector::property_inspector (QWidget *p, base_qobject &oct_qobj)
+      : octave_dock_widget ("PropertyInspector", p, oct_qobj),
+        m_splitter (new QSplitter (Qt::Horizontal, this)),
+        m_tree (new QTreeView (this)), m_table (new QTableView (this)),
+        m_tree_model (nullptr), m_table_model (nullptr)
+  {
+    set_title (tr ("Property Inspector"));
+    setStatusTip (tr ("Inspect properties."));
+    setWindowIcon (QIcon (":/actions/icons/logo.png"));
+    setAttribute (Qt::WA_AlwaysShowToolTips);
+
+    m_splitter->addWidget (m_tree);
+    m_splitter->addWidget (m_table);
+    m_splitter->setChildrenCollapsible (true);
+
+    m_splitter->setSizes (QIntList{ 300, 600 });
+
+    setWidget (m_splitter);
+
+    m_table->setItemDelegate (
+        new go_properties_item_delegate (this, m_octave_qobj, m_table));
+
+    if (oct_qobj.app_context ().interpreter_initialized ())
+      {
+        initialize_models ();
+      }
+    else
+      {
+        QObject::connect (oct_qobj.interpreter_qobj (),
+                          &interpreter_qobject::ready, this,
+                          &property_inspector::initialize_models);
+      }
+  }
+
+  property_inspector::~property_inspector () = default;
+
+  void property_inspector::initialize_models ()
+  {
+    m_tree_model = new go_tree_model (this, m_octave_qobj);
+    auto *old_model = m_tree->model ();
+    m_tree->setModel (m_tree_model);
+    if (old_model)
+      delete old_model;
+
+    // give the first column some space
+    m_tree->header ()->resizeSection (0, 150);
+
+    QObject::connect (m_tree->selectionModel (),
+                      &QItemSelectionModel::currentChanged, this,
+                      &property_inspector::tree_current_changed);
+  }
+
+  void property_inspector::tree_current_changed (const QModelIndex &current,
+                                                 const QModelIndex &)
+  {
+    auto go = m_tree_model->get_go_at (current);
+    if (!go)
+      return;
+
+    auto *prev_model = m_table->model ();
+
+    auto new_model = std::make_unique<go_properties_table_model> (
+        nullptr, m_octave_qobj, go);
+
+    m_table->setModel (new_model.get ());
+    m_table->resizeColumnsToContents ();
+    m_table->resizeRowsToContents ();
+
+    if (prev_model && prev_model != m_table_model.get ())
+      {
+        // initial empty model placed by Qt - we don't need it anymore
+        delete prev_model;
+      }
+
+    m_table_model = std::move (new_model);
+  }
+
+}
diff -r 0e86e50bebeb -r eea535071231 libgui/src/property-inspector.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgui/src/property-inspector.h	Tue Nov 10 00:52:22 2020 +0100
@@ -0,0 +1,77 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2020 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#if !defined(octave_property_inspector_h)
+#  define octave_property_inspector_h 1
+
+#include <memory>
+
+#  include "octave-dock-widget.h"
+
+class QSplitter;
+class QTreeView;
+class QTableView;
+
+namespace octave
+{
+  class base_qobject;
+
+  class go_tree_model;
+  class go_properties_table_model;
+
+  class property_inspector : public octave_dock_widget
+  {
+    Q_OBJECT
+
+  public:
+    property_inspector (QWidget *parent, base_qobject &oct_qobj);
+
+    ~property_inspector ();
+
+    // No copying!
+
+    property_inspector (const property_inspector &) = delete;
+
+    property_inspector &operator= (const property_inspector &) = delete;
+
+  private slots:
+
+    // we need the interpreter ready, so we defer this to a slot.
+    void initialize_models ();
+
+    void tree_current_changed (const QModelIndex &current,
+                               const QModelIndex &previous);
+
+  private:
+    QSplitter *m_splitter;
+    QTreeView *m_tree;
+    QTableView *m_table;
+
+    go_tree_model *m_tree_model;
+    std::unique_ptr<go_properties_table_model> m_table_model;
+  };
+}
+
+#endif
diff -r 0e86e50bebeb -r eea535071231 libgui/src/qt-interpreter-events.cc
--- a/libgui/src/qt-interpreter-events.cc	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/qt-interpreter-events.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -568,6 +568,22 @@ namespace octave
     emit delete_debugger_pointer_signal (QString::fromStdString (file), line);
   }
 
+  void qt_interpreter_events::notify_go_created (const graphics_object& go)
+  {
+    emit go_created (go);
+  }
+
+  void qt_interpreter_events::notify_go_updated (const graphics_object& go,
+                                                 int prop_id)
+  {
+    emit go_updated (go, prop_id);
+  }
+
+  void qt_interpreter_events::notify_go_will_delete (const graphics_object& go)
+  {
+    emit go_will_delete (go);
+  }
+
   void
   qt_interpreter_events::confirm_shutdown_octave (void)
   {
diff -r 0e86e50bebeb -r eea535071231 libgui/src/qt-interpreter-events.h
--- a/libgui/src/qt-interpreter-events.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libgui/src/qt-interpreter-events.h	Tue Nov 10 00:52:22 2020 +0100
@@ -35,15 +35,22 @@
 #include <QString>
 #include <QWaitCondition>
 
+#include "event-manager.h"
+#include "graphics-handle.h"
+
 #include "dialog.h"
 #include "gui-settings.h"
 
-#include "event-manager.h"
-
 // Defined for purposes of sending QList<int> as part of signal.
 typedef QList<int> QIntList;
 
 class octave_value;
+class graphics_object;
+
+namespace QtHandles
+{
+  class qt_graphics_toolkit;
+}
 
 namespace octave
 {
@@ -176,6 +183,12 @@ namespace octave
     void update_breakpoint (bool insert, const std::string& file, int line,
                             const std::string& cond);
 
+    void notify_go_created(const graphics_object& go) override;
+
+    void notify_go_updated(const graphics_object& go, int prop_id) override;
+
+    void notify_go_will_delete(const graphics_object& go) override;
+
     void lock (void) { m_mutex.lock (); }
 
     void wait (void) { m_waitcondition.wait (&m_mutex); }
@@ -256,6 +269,12 @@ namespace octave
 
     void apply_new_settings (void);
 
+    void go_created (const graphics_object& go);
+
+    void go_updated (const graphics_object& go, int prop_id);
+
+    void go_will_delete (const graphics_object& go);
+
   private:
 
     QString gui_preference_adjust (const QString& key, const QString& value);
diff -r 0e86e50bebeb -r eea535071231 libinterp/corefcn/event-manager.h
--- a/libinterp/corefcn/event-manager.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libinterp/corefcn/event-manager.h	Tue Nov 10 00:52:22 2020 +0100
@@ -40,6 +40,7 @@
 
 class octave_value;
 class string_vector;
+class graphics_object;
 
 namespace octave
 {
@@ -241,6 +242,18 @@ namespace octave
     update_breakpoint (bool /*insert*/, const std::string& /*file*/,
                        int /*line*/, const std::string& /*cond*/)
     { }
+
+    virtual void
+    notify_go_created(const graphics_object& /*go*/)
+    { }
+
+    virtual void
+    notify_go_updated(const graphics_object& /*go*/, int /*prop_id*/)
+    { }
+
+    virtual void
+    notify_go_will_delete(const graphics_object& /*go*/)
+    { }
   };
 
   //! Provides threadsafe access to octave.
@@ -609,6 +622,24 @@ namespace octave
         instance->update_breakpoint (insert, file, line, cond);
     }
 
+    void notify_go_created (const graphics_object& go)
+    {
+      if (enabled ())
+        instance->notify_go_created(go);
+    }
+
+    void notify_go_updated (const graphics_object& go, int prop_id)
+    {
+      if (enabled ())
+        instance->notify_go_updated(go, prop_id);
+    }
+
+    void notify_go_will_delete (const graphics_object& go)
+    {
+      if (enabled ())
+        instance->notify_go_will_delete(go);
+    }
+
   private:
 
     interpreter& m_interpreter;
diff -r 0e86e50bebeb -r eea535071231 libinterp/corefcn/genprops.awk
--- a/libinterp/corefcn/genprops.awk	Sun Oct 25 19:28:08 2020 -0700
+++ b/libinterp/corefcn/genprops.awk	Tue Nov 10 00:52:22 2020 +0100
@@ -300,7 +300,7 @@ function emit_declarations ()
   if (class_name && ! base)
     emit_common_declarations();
 
-  printf ("public:\n\n\n  static std::set<std::string> core_property_names (void);\n\n  static std::set<std::string> readonly_property_names (void);\n\n  static bool has_core_property (const caseless_str& pname);\n\n  static bool has_readonly_property (const caseless_str& pname);\n\n  std::set<std::string> all_property_names (void) const;\n\n");
+  printf ("public:\n\n\n  static std::set<std::string> core_property_names (void);\n\n  static std::set<std::string> readonly_property_names (void);\n\n  static bool has_core_property (const caseless_str& pname);\n\n  static bool has_readonly_property (const caseless_str& pname);\n\n  virtual std::set<std::string> all_property_names (void) const;\n\n");
 
   if (! base)
     printf ("  bool has_property (const caseless_str& pname) const;\n\n");
diff -r 0e86e50bebeb -r eea535071231 libinterp/corefcn/graphics.cc
--- a/libinterp/corefcn/graphics.cc	Sun Oct 25 19:28:08 2020 -0700
+++ b/libinterp/corefcn/graphics.cc	Tue Nov 10 00:52:22 2020 +0100
@@ -51,6 +51,7 @@
 #include "defun.h"
 #include "display.h"
 #include "error.h"
+#include "event-manager.h"
 #include "graphics.h"
 #include "input.h"
 #include "interpreter-private.h"
@@ -1261,13 +1262,17 @@ base_property::set (const octave_value& 
 {
   if (do_set (v))
     {
+      gh_manager& gh_mgr
+        = octave::__get_gh_manager__ ("base_property::set");
+
+      octave::event_manager& ev_mgr
+        = octave::__get_event_manager__ ("base_property::set");
+
+      graphics_object go = gh_mgr.get_object (parent);
+
       // Notify graphics toolkit.
       if (id >= 0 && do_notify_toolkit)
         {
-          gh_manager& gh_mgr
-            = octave::__get_gh_manager__ ("base_property::set");
-
-          graphics_object go = gh_mgr.get_object (parent);
           if (go)
             go.update (id);
         }
@@ -1276,6 +1281,9 @@ base_property::set (const octave_value& 
       if (do_run)
         run_listeners (GCB_POSTSET);
 
+      // notify gui of the property change
+      ev_mgr.notify_go_updated(go, id);
+
       return true;
     }
 
@@ -2858,6 +2866,9 @@ gh_manager::free (const graphics_handle&
       if (p == m_handle_map.end ())
         error ("graphics_handle::free: invalid object %g", h.value ());
 
+      // Notify the event-manager
+      m_interpreter.get_event_manager ().notify_go_will_delete (p->second);
+
       base_properties& bp = p->second.get_properties ();
 
       if (! p->second.valid_object () || bp.is_beingdeleted ())
@@ -2912,6 +2923,8 @@ void
 gh_manager::renumber_figure (const graphics_handle& old_gh,
                              const graphics_handle& new_gh)
 {
+  // FIXME: do we have to notify the event manager?
+
   auto p = m_handle_map.find (old_gh);
 
   if (p == m_handle_map.end ())
@@ -2919,10 +2932,14 @@ gh_manager::renumber_figure (const graph
 
   graphics_object go = p->second;
 
+  // m_interpreter.get_event_manager().notify_go_will_delete(go);
+
   m_handle_map.erase (p);
 
   m_handle_map[new_gh] = go;
 
+  // m_interpreter.get_event_manager().notify_go_created(go);
+
   if (old_gh.value () < 0)
     m_handle_free_list.insert (std::ceil (old_gh.value ())
                                - make_handle_fraction ());
@@ -11888,6 +11905,9 @@ gh_manager::make_graphics_handle (const 
   if (notify_toolkit)
     go.initialize ();
 
+  // Notify the event manager
+  m_interpreter.get_event_manager().notify_go_created(go);
+
   return h;
 }
 
@@ -11905,6 +11925,9 @@ gh_manager::make_figure_handle (double v
   if (notify_toolkit)
     go.initialize ();
 
+  // Notify the event manager
+  m_interpreter.get_event_manager().notify_go_created(go);
+
   go.override_defaults ();
 
   return h;
diff -r 0e86e50bebeb -r eea535071231 libinterp/corefcn/graphics.in.h
--- a/libinterp/corefcn/graphics.in.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/libinterp/corefcn/graphics.in.h	Tue Nov 10 00:52:22 2020 +0100
@@ -60,6 +60,25 @@
 
 // ---------------------------------------------------------------------
 
+enum class property_type
+{
+  INVALID,
+  ARRAY,
+  BOOL,
+  CALLBACK,
+  CHILDREN,
+  COLOR,
+  DOUBLE,
+  DOUBLE_RADIO,
+  HANDLE,
+  RADIO,
+  ROW_VECTOR,
+  STRING,
+  STRING_ARRAY,
+  TEXT_LABEL,
+  ANY,
+};
+
 class base_scaler
 {
 public:
@@ -314,6 +333,8 @@ public:
 
   virtual bool is_radio (void) const { return false; }
 
+  virtual property_type prop_type () const { return property_type::INVALID; }
+
   int get_id (void) const { return id; }
 
   void set_id (int d) { id = d; }
@@ -440,6 +461,11 @@ public:
   string_property (const string_property& p)
     : base_property (p), str (p.str) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::STRING;
+  }
+
   octave_value get (void) const
   { return octave_value (str); }
 
@@ -525,6 +551,11 @@ public:
     : base_property (p), desired_type (p.desired_type),
       separator (p.separator), str (p.str) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::STRING_ARRAY;
+  }
+
   octave_value get (void) const
   {
     if (desired_type == string_t)
@@ -737,6 +768,11 @@ public:
     return tmp.isempty ();
   }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::TEXT_LABEL;
+  }
+
   octave_value get (void) const
   {
     if (stored_type == char_t)
@@ -941,6 +977,11 @@ public:
   radio_property (const radio_property& p)
     : base_property (p), vals (p.vals), current_val (p.current_val) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::RADIO;
+  }
+
   octave_value get (void) const { return octave_value (current_val); }
 
   const std::string& current_value (void) const { return current_val; }
@@ -1107,6 +1148,11 @@ public:
       color_val (p.color_val), radio_val (p.radio_val),
       current_val (p.current_val) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::COLOR;
+  }
+
   octave_value get (void) const
   {
     if (current_type == color_t)
@@ -1188,6 +1234,11 @@ public:
       minval (std::pair<double, bool> (octave_NaN, true)),
       maxval (std::pair<double, bool> (octave_NaN, true)) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::DOUBLE;
+  }
+
   octave_value get (void) const { return octave_value (current_val); }
 
   double double_value (void) const { return current_val; }
@@ -1312,6 +1363,11 @@ public:
       dval (p.dval), radio_val (p.radio_val),
       current_val (p.current_val) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::DOUBLE_RADIO;
+  }
+
   octave_value get (void) const
   {
     if (current_type == double_t)
@@ -1401,6 +1457,11 @@ public:
       maxval (std::pair<double, bool> (octave_NaN, true))
   { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::ARRAY;
+  }
+
   octave_value get (void) const { return data; }
 
   void add_constraint (const std::string& type)
@@ -1517,6 +1578,11 @@ public:
     add_constraint (dim_vector (0, 0));
   }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::ROW_VECTOR;
+  }
+
   void add_constraint (const std::string& type)
   {
     array_property::add_constraint (type);
@@ -1608,6 +1674,11 @@ public:
   bool_property (const bool_property& p)
     : radio_property (p) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::BOOL;
+  }
+
   bool is_on (void) const { return is ("on"); }
 
   bool_property& operator = (const octave_value& val)
@@ -1641,6 +1712,11 @@ public:
   handle_property (const handle_property& p)
     : base_property (p), current_val (p.current_val) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::HANDLE;
+  }
+
   octave_value get (void) const { return current_val.as_octave_value (); }
 
   graphics_handle handle_value (void) const { return current_val; }
@@ -1693,6 +1769,8 @@ public:
     return *this;
   }
 
+  virtual property_type prop_type () const override { return property_type::ANY; }
+
   base_property * clone (void) const { return new any_property (*this); }
 
 protected:
@@ -1730,6 +1808,11 @@ public:
     do_init_children (p.children_list);
   }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::CHILDREN;
+  }
+
   children_property& operator = (const octave_value& val)
   {
     set (val);
@@ -1909,6 +1992,11 @@ public:
   callback_property (const callback_property& p)
     : base_property (p), callback (p.callback) { }
 
+  virtual property_type prop_type () const override
+  {
+    return property_type::CALLBACK;
+  }
+
   octave_value get (void) const { return callback; }
 
   OCTINTERP_API void execute (const octave_value& data = octave_value ()) const;
@@ -1947,6 +2035,24 @@ private:
 
 // ---------------------------------------------------------------------
 
+#if 0
+
+namespace detail {
+
+template<class prop_type>
+inline prop_type& prop_cast(base_property *rep, const char *who)
+{
+  auto *p = dynamic_cast<prop_type*>(rep);
+  if (!p) {
+    throw std::logic_error(std::string(who) + ": invalid property cast");
+  }
+  return *p;
+}
+
+}
+
+#endif // 0
+
 class property
 {
 public:
@@ -2037,6 +2143,11 @@ public:
   void run_listeners (listener_mode mode = GCB_POSTSET)
   { rep->run_listeners (mode); }
 
+  property_type prop_type() const
+  {
+    return rep->prop_type();
+  }
+
   OCTINTERP_API static
   property create (const std::string& name, const graphics_handle& parent,
                    const caseless_str& type,
@@ -2045,25 +2156,180 @@ public:
   property clone (void) const
   { return property (rep->clone ()); }
 
+  const base_property& as_base_property (void) const { return *rep; }
+
+  base_property& as_base_property (void) { return *rep; }
+
 #if 0
-  const string_property& as_string_property (void) const
-  { return *(dynamic_cast<string_property *> (rep)); }
-
-  const radio_property& as_radio_property (void) const
-  { return *(dynamic_cast<radio_property *> (rep)); }
+
+  const array_property& as_array_property (void) const
+  {
+    return detail::prop_cast<const array_property> (
+        rep, "property::as_array_property");
+  }
+
+  array_property& as_array_property (void)
+  {
+    return detail::prop_cast<array_property> (rep,
+                                              "property::as_array_property");
+  }
+
+  const bool_property& as_bool_property (void) const
+  {
+    return detail::prop_cast<const bool_property> (
+        rep, "property::as_bool_property");
+  }
+
+  bool_property& as_bool_property (void)
+  {
+    return detail::prop_cast<bool_property> (rep,
+                                             "property::as_bool_property");
+  }
+
+  const callback_property& as_callback_property (void) const
+  {
+    return detail::prop_cast<const callback_property> (
+        rep, "property::as_callback_property");
+  }
+
+  callback_property& as_callback_property (void)
+  {
+    return detail::prop_cast<callback_property> (
+        rep, "property::as_callback_property");
+  }
+
+  const children_property& as_children_property (void) const
+  {
+    return detail::prop_cast<const children_property> (
+        rep, "property::as_children_property");
+  }
+
+  children_property& as_children_property (void)
+  {
+    return detail::prop_cast<children_property> (
+        rep, "property::as_children_property");
+  }
 
   const color_property& as_color_property (void) const
-  { return *(dynamic_cast<color_property *> (rep)); }
+  {
+    return detail::prop_cast<const color_property> (
+        rep, "property::as_color_property");
+  }
+
+  color_property& as_color_property (void)
+  {
+    return detail::prop_cast<color_property> (rep,
+                                              "property::as_color_property");
+  }
 
   const double_property& as_double_property (void) const
-  { return *(dynamic_cast<double_property *> (rep)); }
-
-  const bool_property& as_bool_property (void) const
-  { return *(dynamic_cast<bool_property *> (rep)); }
+  {
+    return detail::prop_cast<const double_property> (
+        rep, "property::as_double_property");
+  }
+
+  double_property& as_double_property (void)
+  {
+    return detail::prop_cast<double_property> (rep,
+                                               "property::as_double_property");
+  }
+
+  const double_radio_property& as_double_radio_property (void) const
+  {
+    return detail::prop_cast<const double_radio_property> (
+        rep, "property::as_double_radio_property");
+  }
+
+  double_radio_property& as_double_radio_property (void)
+  {
+    return detail::prop_cast<double_radio_property> (
+        rep, "property::as_double_radio_property");
+  }
 
   const handle_property& as_handle_property (void) const
-  { return *(dynamic_cast<handle_property *> (rep)); }
-#endif
+  {
+    return detail::prop_cast<const handle_property> (
+        rep, "property::as_handle_property");
+  }
+
+  handle_property& as_handle_property (void)
+  {
+    return detail::prop_cast<handle_property> (rep,
+                                               "property::as_handle_property");
+  }
+
+  const radio_property& as_radio_property (void) const
+  {
+    return detail::prop_cast<const radio_property> (
+        rep, "property::as_radio_property");
+  }
+
+  radio_property& as_radio_property (void)
+  {
+    return detail::prop_cast<radio_property> (rep,
+                                              "property::as_radio_property");
+  }
+
+  const row_vector_property& as_row_vector_property (void) const
+  {
+    return detail::prop_cast<const row_vector_property> (
+        rep, "property::as_row_vector_property");
+  }
+
+  row_vector_property& as_row_vector_property (void)
+  {
+    return detail::prop_cast<row_vector_property> (
+        rep, "property::as_row_vector_property");
+  }
+
+  const string_property& as_string_property (void) const
+  {
+    return detail::prop_cast<const string_property> (
+        rep, "property::as_string_property");
+  }
+
+  string_property& as_string_property (void)
+  {
+    return detail::prop_cast<string_property> (rep,
+                                               "property::as_string_property");
+  }
+
+  const string_array_property& as_string_array_property (void) const
+  {
+    return detail::prop_cast<const string_array_property> (
+        rep, "property::as_string_array_property");
+  }
+
+  string_array_property& as_string_array_property (void)
+  {
+    return detail::prop_cast<string_array_property> (
+        rep, "property::as_string_array_property");
+  }
+
+  const text_label_property& as_text_label_property (void) const
+  {
+    return detail::prop_cast<const text_label_property> (
+        rep, "property::as_text_label_property");
+  }
+
+  text_label_property& as_text_label_property (void)
+  {
+    return detail::prop_cast<text_label_property> (
+        rep, "property::as_text_label_property");
+  }
+
+  const any_property& as_any_property (void) const
+  {
+    return detail::prop_cast<const any_property> (rep,
+                                                  "property::as_any_property");
+  }
+
+  any_property& as_any_property (void)
+  {
+    return detail::prop_cast<any_property> (rep, "property::as_any_property");
+  }
+
+#endif // 0
 
 private:
   base_property *rep;
@@ -6728,4 +6994,21 @@ OCTINTERP_API graphics_handle gca (void)
 
 OCTINTERP_API void close_all_figures (void);
 
+// property introspection system
+
+struct graphics_property_info
+{
+  std::string name;
+  int id;
+  property_type type;
+};
+
+struct graphics_handle_type_info
+{
+  std::string type;
+  std::vector<graphics_property_info> static_prop_info;
+};
+
+
+
 #endif
diff -r 0e86e50bebeb -r eea535071231 liboctave/util/oct-mutex.h
--- a/liboctave/util/oct-mutex.h	Sun Oct 25 19:28:08 2020 -0700
+++ b/liboctave/util/oct-mutex.h	Tue Nov 10 00:52:22 2020 +0100
@@ -26,6 +26,8 @@
 #if ! defined (octave_oct_mutex_h)
 #define octave_oct_mutex_h 1
 
+#include <stdexcept>
+
 #include "octave-config.h"
 
 #include "oct-refcount.h"
@@ -145,6 +147,48 @@ namespace octave
     bool m_lock_result;
   };
 
+  // a moveable, non-copiable lock that acts like a smart pointer
+  // the type must implement lock and unlock. (it also work with plain mutex)
+  template <class T>
+  class unique_lock
+  {
+  public:
+
+    // may be holding a ref is better
+    unique_lock (T& r) : rep (r), owned (true) { rep.lock (); }
+
+    ~unique_lock ()
+    {
+      if (owned)
+        rep.unlock ();
+    }
+
+    unique_lock (const unique_lock&) = delete;
+    unique_lock& operator= (const unique_lock&) = delete;
+
+    unique_lock (unique_lock&& other) : rep (other.rep), owned (other.owned)
+    {
+      other.owned = false;
+    }
+
+    T* operator->()
+    {
+      if (!owned)
+        throw std::logic_error { "access of non-owned locked resource" };
+      return &rep;
+    }
+
+    T& operator* ()
+    {
+      if (!owned)
+        throw std::logic_error { "access of non-owned locked resource" };
+      return rep;
+    }
+
+  private:
+    T& rep;
+    bool owned;
+  };
 
   class
   OCTAVE_API
