# HG changeset patch # User Remi Thebault # Date 1607255478 -3600 # Sun Dec 06 12:51:18 2020 +0100 # Node ID d6cea9acecdf869862b62e6599857c6cccb95d02 # Parent 95ec5841c9d6f52ce28c167a7d501078a4123965 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 * Currently not usable outside the GUI. It is wished to have the PI usable from the terminal through the missing 'inspect' command. 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/item-hyperlink-delegate.h * libgui/src/item-hyperlink-delegate.cc - class that makes easy having hyperlinks in item models, without having to instantiate one widget per cell. * 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 * 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 (at the moment) two specific widgets used as cell editors: - matrix_widget - color_editor_widget (this one use a *.ui designer file) * 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 --git a/libgui/src/gui-preferences-all.h b/libgui/src/gui-preferences-all.h --- a/libgui/src/gui-preferences-all.h +++ b/libgui/src/gui-preferences-all.h @@ -37,6 +37,7 @@ #include "gui-preferences-mw.h" #include "gui-preferences-nr.h" #include "gui-preferences-pd.h" +#include "gui-preferences-pi.h" #include "gui-preferences-ve.h" #include "gui-preferences-sc.h" #include "gui-preferences-sd.h" diff --git a/libgui/src/gui-preferences-pi.h b/libgui/src/gui-preferences-pi.h new file mode 100644 --- /dev/null +++ b/libgui/src/gui-preferences-pi.h @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2017-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if ! defined (octave_gui_preferences_pi_h) +#define octave_gui_preferences_pi_h 1 + +#include "gui-preferences.h" + +#include + +// Variable Editor preferences + +const gui_pref +pi_tree_alternate_rows ("property_inspector/tree_alternate_rows", QVariant (false)); + +const gui_pref +pi_table_row_height ("property_inspector/table_row_height", QVariant ()); + +const gui_pref +pi_table_alternate_rows ("property_inspector/table_alternate_rows", QVariant (false)); + + +#endif // octave_gui_preferences_pi_h diff --git a/libgui/src/gui-preferences-sc.h b/libgui/src/gui-preferences-sc.h --- a/libgui/src/gui-preferences-sc.h +++ b/libgui/src/gui-preferences-sc.h @@ -112,6 +112,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); @@ -119,6 +120,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 --git a/libgui/src/icons/hsplitter-handle.png b/libgui/src/icons/hsplitter-handle.png new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3247c8d0a32ec0d2218c47458f2dab026310e61f GIT binary patch literal 147 zc%17D@N?(olHy`uVBq!ia0vp^tUzqe!3HFc+--0MQk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0uto-U3d7QM+C85uv$A2@KJ p!B1FBBGhtKQOMI3US6S03^%JI+IDe-uK}uM@O1TaS?83{1OV(+EHnTB diff --git a/libgui/src/icons/hsplitter-handle.svg b/libgui/src/icons/hsplitter-handle.svg new file mode 100644 --- /dev/null +++ b/libgui/src/icons/hsplitter-handle.svg @@ -0,0 +1,1900 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libgui/src/interpreter-qobject.cc b/libgui/src/interpreter-qobject.cc --- a/libgui/src/interpreter-qobject.cc +++ b/libgui/src/interpreter-qobject.cc @@ -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 --git a/libgui/src/interpreter-qobject.h b/libgui/src/interpreter-qobject.h --- a/libgui/src/interpreter-qobject.h +++ b/libgui/src/interpreter-qobject.h @@ -28,14 +28,22 @@ #include +#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; + 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 --git a/libgui/src/item-hyperlink-delegate.cc b/libgui/src/item-hyperlink-delegate.cc new file mode 100644 --- /dev/null +++ b/libgui/src/item-hyperlink-delegate.cc @@ -0,0 +1,242 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +#include "item-hyperlink-delegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace octave +{ + + constexpr int default_alignment = Qt::AlignCenter; + + static const QColor unvisited_color = QColor (Qt::darkBlue).lighter(); + static const QColor visited_color = QColor (Qt::darkMagenta); + + static QSet visited_links; + + static int item_alignment (const QModelIndex &index) + { + const auto model_alignment = index.data (Qt::TextAlignmentRole); + return model_alignment.isValid () ? model_alignment.toInt () + : default_alignment; + } + + item_hyperlink_delegate::item_hyperlink_delegate (QObject *parent, + QAbstractItemView *view) + : QObject{ parent }, m_view{ view }, m_font_metrics{ QFont{} } + { + } + + item_hyperlink_delegate::~item_hyperlink_delegate () = default; + + void item_hyperlink_delegate::mouse_move_event (QMouseEvent *ev) + { + const auto *m = m_view->model (); + if (!m) + return; + + if (m_clicked_hyperlink && !m_clicked_hl_rect.contains (ev->pos ())) + { + // handling mouse drag. Dragging out of hyperlink cancels the click + m_clicked_hyperlink = hyperlink{}; + const auto ind = m_clicked_index; + m_clicked_index = QModelIndex{}; + m_clicked_hl_rect = QRect{}; + + if (m_cursor_set) + { + m_view->setCursor (m_old_cursor); + m_cursor_set = false; + } + + m_view->update (ind); + return; + } + + const auto index = m_view->indexAt (ev->pos ()); + if (!index.isValid ()) + return; + + const auto hl + = qvariant_cast (m->data (index, Qt::DisplayRole)); + if (hl.isEmpty ()) + { + if (m_cursor_set) + { + m_view->setCursor (m_old_cursor); + m_cursor_set = false; + } + return; + } + + const auto ir = m_view->visualRect (index); + const auto alignment = item_alignment (index); + const auto br = QApplication::style ()->itemTextRect ( + m_font_metrics, ir, alignment, true, hl.title ()); + const auto within = br.contains (ev->pos ()); + + if (within && !m_cursor_set) + { + m_old_cursor = m_view->cursor (); + m_view->setCursor (Qt::PointingHandCursor); + m_cursor_set = true; + } + else if (!within && m_cursor_set) + { + m_view->setCursor (m_old_cursor); + m_cursor_set = false; + } + } + + bool item_hyperlink_delegate::mouse_press_event (QMouseEvent *ev) + { + const auto *m = m_view->model (); + if (!m) + return false; + + const auto index = m_view->indexAt (ev->pos ()); + if (!index.isValid ()) + return false; + + const auto hl + = qvariant_cast (m->data (index, Qt::DisplayRole)); + if (hl.isEmpty ()) + return false; + + const auto ir = m_view->visualRect (index); + const auto alignment = item_alignment (index); + const auto br = QApplication::style ()->itemTextRect ( + m_font_metrics, ir, alignment, true, hl.title ()); + + if (br.contains (ev->pos ())) + { + m_click_pos = ev->pos (); + m_clicked_hyperlink = hl; + m_clicked_index = index; + m_clicked_hl_rect = br; + m_view->update (index); + return true; + } + return false; + } + + void item_hyperlink_delegate::mouse_release_event (QMouseEvent *ev) + { + Q_UNUSED (ev) + + if (m_clicked_hyperlink) + { + QDesktopServices::openUrl (m_clicked_hyperlink.url ()); + + visited_links.insert (m_clicked_hyperlink.url ()); + + m_clicked_hyperlink = hyperlink{}; + const auto ind = m_clicked_index; + m_clicked_index = QModelIndex{}; + m_clicked_hl_rect = QRect{}; + + m_view->update (ind); + } + } + + QSize item_hyperlink_delegate::size_hint (const QStyleOptionViewItem &option, + const QModelIndex &index) + { + const auto hl = qvariant_cast (index.data (Qt::DisplayRole)); + + if (hl.isEmpty ()) + return QSize{}; + + if (!m_font_metrics_set) + { + set_font_metrics (option.fontMetrics); + } + + return m_font_metrics.boundingRect (hl.title ()).size (); + } + + void item_hyperlink_delegate::paint (QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) + { + const auto hl = qvariant_cast (index.data (Qt::DisplayRole)); + + if (hl.isEmpty ()) + return; + + if (!m_font_metrics_set) + { + set_font_metrics (option.fontMetrics); + } + + QFont font = option.font; + font.setUnderline (true); + + QColor textColor = unvisited_color; + if (option.state.testFlag (QStyle::State_Selected)) + { + // hard-code the color only if not highlighted + // (otherwise we can have blue over blue) + textColor = option.palette.color (QPalette::HighlightedText); + } + else if (visited_links.find (hl.url ()) != visited_links.end ()) + { + textColor = visited_color; + } + + const auto alignment = item_alignment (index); + + auto rect = option.rect; + + // animate the click + if (m_clicked_hyperlink) + { + rect.translate (QPoint (1, 2)); + } + + painter->save (); + + painter->setFont (font); + painter->setPen (QPen (textColor, painter->pen ().widthF ())); + + painter->drawText (option.rect, alignment, hl.title ()); + + painter->restore (); + } + +} diff --git a/libgui/src/item-hyperlink-delegate.h b/libgui/src/item-hyperlink-delegate.h new file mode 100644 --- /dev/null +++ b/libgui/src/item-hyperlink-delegate.h @@ -0,0 +1,150 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if !defined(octave_item_hyperlink_delegate_h) +#define octave_item_hyperlink_delegate_h 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QAbstractItemView; +class QMouseEvent; +class QObject; +class QPainter; + +namespace octave +{ + //! Describes an hyperlink, that is a url with a title + class hyperlink + { + public: + hyperlink (const QUrl &url, const QString &title) + : m_url{ url }, m_title{ title } + { + } + hyperlink (const QString &url, const QString &title) + : m_url{ url }, m_title{ title } + { + } + + hyperlink () = default; + ~hyperlink () = default; + hyperlink (const hyperlink &) = default; + hyperlink &operator= (const hyperlink &) = default; + + operator bool () const { return !isEmpty (); } + + bool isEmpty () const { return m_url.isEmpty () || m_title.isEmpty (); } + + QUrl url () const { return m_url; } + QString title () const { return m_title; } + + private: + QUrl m_url; + QString m_title; + }; + + //! This class allows to easily setup hyperlinks in item + //! views (such as QTreeView or QTableView). + //! + //! There is no additional widget instanciated, so it is much more efficient + //! than having a QLabel in each cell. + //! + //! The usage requirements are as follow: + //! 1. The view must forward its mouse events to this delegate + //! (and have mouse tracking set) + //! 2. For all indices with hyperlink, the model must return an hyperlink + //! object in data() when role is Qt::DisplayRole. + //! 3. For all indices with hyperlink, the item delegate must forward + //! sizeHint to this delegate + //! 4. For all indices with hyperlink, the item delegate must forward paint + //! to this delegate + class item_hyperlink_delegate : public QObject + { + Q_OBJECT + + public: + item_hyperlink_delegate (QObject *parent, QAbstractItemView *view); + virtual ~item_hyperlink_delegate (); + + //! The QFontMetrics used by this delegate to paint and compute + //! bounding rects + QFontMetrics font_metrics () const { return m_font_metrics; } + + //! Set the QFontMetrics used by this delegate. + //! Calling this method is entirely optional. + //! The delegate will use the style option item passed to paint + //! if this method is not called. + void set_font_metrics (const QFontMetrics &fm) + { + m_font_metrics = fm; + m_font_metrics_set = true; + } + + //! View must call this method to forward mouse move event + //! (mouse tracking must be enabled in the view) + void mouse_move_event (QMouseEvent *ev); + //! View must call this method to forward mouse press event + //! Returns whether the hyperlink was hit by the click. + //! Item view may want to accept the event to avoid further + //! process (like selecting the row) if the hyperlink is hit + bool mouse_press_event (QMouseEvent *ev); + //! View must call this method to forward mouse release event + void mouse_release_event (QMouseEvent *ev); + + //! View delegate must call this method to estimate size of the hyperlink. + QSize size_hint (const QStyleOptionViewItem &option, + const QModelIndex &index); + + //! View delegate must call this method to paint the hyperlink. + //! The hyperlink will be painted different color if link was visited. + void paint (QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index); + + private: + QAbstractItemView *m_view = nullptr; + QCursor m_old_cursor; + bool m_cursor_set = false; + QFontMetrics m_font_metrics; + bool m_font_metrics_set = false; + QPoint m_click_pos; + hyperlink m_clicked_hyperlink; + QModelIndex m_clicked_index; + QRect m_clicked_hl_rect; // to test dragging out easily + }; + +} + +Q_DECLARE_METATYPE (octave::hyperlink); + +#endif // octave_item_hyperlink_delegate_h diff --git a/libgui/src/main-window.cc b/libgui/src/main-window.cc --- a/libgui/src/main-window.cc +++ b/libgui/src/main-window.cc @@ -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), @@ -181,6 +181,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; @@ -258,6 +259,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; @@ -2094,6 +2096,9 @@ namespace octave m_variable_editor_window, SLOT (save_settings (void))); connect (qApp, SIGNAL (aboutToQuit (void)), + m_property_inspector_window, SLOT (save_settings (void))); + + connect (qApp, SIGNAL (aboutToQuit (void)), this, SLOT (prepare_to_exit (void))); connect (qApp, SIGNAL (focusChanged (QWidget*, QWidget*)), @@ -2161,6 +2166,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); @@ -2703,8 +2710,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 (); @@ -2729,6 +2741,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 (), @@ -2900,6 +2916,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); @@ -2907,6 +2924,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); @@ -2936,6 +2954,8 @@ namespace octave #endif list.append (static_cast (m_workspace_window)); list.append (static_cast (m_variable_editor_window)); + list.append ( + static_cast (m_property_inspector_window)); return list; } diff --git a/libgui/src/main-window.h b/libgui/src/main-window.h --- a/libgui/src/main-window.h +++ b/libgui/src/main-window.h @@ -56,6 +56,7 @@ #include "led-indicator.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" @@ -331,6 +332,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; @@ -383,6 +385,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; @@ -390,6 +393,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 --git a/libgui/src/module.mk b/libgui/src/module.mk --- a/libgui/src/module.mk +++ b/libgui/src/module.mk @@ -67,6 +67,8 @@ octave_gui_ICONS = \ %reldir%/icons/graphic_logo_ReleaseWidget.svg \ %reldir%/icons/graphic_logo_VariableEditor.svg \ %reldir%/icons/hand2.png \ + %reldir%/icons/hsplitter-handle.svg \ + %reldir%/icons/hsplitter-handle.png \ %reldir%/icons/icons_license \ %reldir%/icons/left_side.png \ %reldir%/icons/letter_logo_DocumentationDockWidget.png \ @@ -140,10 +142,14 @@ OCTAVE_GUI_SRC_MOC = \ %reldir%/moc-gui-settings.cc \ %reldir%/moc-history-dock-widget.cc \ %reldir%/moc-interpreter-qobject.cc \ + %reldir%/moc-item-hyperlink-delegate.cc \ %reldir%/moc-led-indicator.cc \ %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 \ @@ -174,6 +180,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)) @@ -200,6 +207,7 @@ noinst_HEADERS += \ %reldir%/gui-preferences-mw.h \ %reldir%/gui-preferences-nr.h \ %reldir%/gui-preferences-pd.h \ + %reldir%/gui-preferences-pi.h \ %reldir%/gui-preferences-sc.h \ %reldir%/gui-preferences-sd.h \ %reldir%/gui-preferences-ve.h \ @@ -211,6 +219,7 @@ noinst_HEADERS += \ %reldir%/graphics-init.h \ %reldir%/history-dock-widget.h \ %reldir%/interpreter-qobject.h \ + %reldir%/item-hyperlink-delegate.h \ %reldir%/led-indicator.h \ %reldir%/m-editor/file-editor-interface.h \ %reldir%/m-editor/file-editor-tab.h \ @@ -222,6 +231,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 \ @@ -253,6 +265,7 @@ noinst_HEADERS += \ %reldir%/gui-settings.cc \ %reldir%/history-dock-widget.cc \ %reldir%/interpreter-qobject.cc \ + %reldir%/item-hyperlink-delegate.cc \ %reldir%/led-indicator.cc \ %reldir%/m-editor/file-editor-tab.cc \ %reldir%/m-editor/file-editor.cc \ @@ -264,6 +277,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 --git a/libgui/src/property-inspector-color-editor.ui b/libgui/src/property-inspector-color-editor.ui new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector-color-editor.ui @@ -0,0 +1,312 @@ + + + color_editor_widget + + + + 0 + 0 + 277 + 179 + + + + Form + + + + 4 + + + 4 + + + 4 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + preset + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 80 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Pick + + + + + + + + + + + 0 + 0 + + + + RBG + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Matrix + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 3 + + + 1.000000000000000 + + + 0.100000000000000 + + + + + + + 3 + + + 1.000000000000000 + + + 0.100000000000000 + + + + + + + 3 + + + 1.000000000000000 + + + 0.100000000000000 + + + + + + + + + + + 0 + 0 + + + + SVG + + + + + + + + 100 + 0 + + + + true + + + + + + + Octave + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + diff --git a/libgui/src/property-inspector-table.cc b/libgui/src/property-inspector-table.cc new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector-table.cc @@ -0,0 +1,1725 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "caseless-str.h" + +#include "graphics.h" +#include "interpreter.h" + +#include "gui-preferences-pi.h" +#include "gui-settings.h" +#include "item-hyperlink-delegate.h" +#include "octave-qobject.h" +#include "property-inspector-table.h" +#include "qt-application.h" +#include "ui-property-inspector-color-editor.h" + +namespace octave +{ + + enum Column + { + // COL_NAME, + COL_VALUE, + COL_TYPE, + COL_DOCLINK, + + 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; + + go_properties_table_view::go_properties_table_view (QWidget *parent) + : QTableView (parent) + { + setMouseTracking (true); + } + + go_properties_table_view::~go_properties_table_view () = default; + + void go_properties_table_view::mouseMoveEvent (QMouseEvent *ev) + { + if (m_hl_dg) + { + m_hl_dg->mouse_move_event (ev); + } + QTableView::mouseMoveEvent (ev); + } + + void go_properties_table_view::mousePressEvent (QMouseEvent *ev) + { + if (m_hl_dg && m_hl_dg->mouse_press_event (ev)) + { + ev->accept (); + return; + } + QTableView::mousePressEvent (ev); + } + + void go_properties_table_view::mouseReleaseEvent (QMouseEvent *ev) + { + if (m_hl_dg) + { + m_hl_dg->mouse_release_event (ev); + } + QTableView::mouseReleaseEvent (ev); + } + + //! turn "figure" or "FIGURE" into "Figure" + inline QString titlecased (const QString &input) + { + if (input.size () <= 1) + return input; + return input[0].toUpper () + input.right (input.size () - 1).toLower (); + } + + inline QUrl go_property_doc_url (const QString &obj_type, + const QString &prop_name, + const QString &octave_version = "latest") + { + return QUrl ( + QString ("https://octave.org/doc/%1/%2-Properties.html#XREF%3%4") + .arg (octave_version, titlecased (obj_type), obj_type.toLower (), + prop_name.toLower ())); + } + + template + inline QString property_string_value (const base_property *prop) + { + const auto *p = static_cast (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 () == PROP_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 PROP_HANDLE: + { + const auto *p = static_cast (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 PROP_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 PROP_INVALID: + return "invalid"; + case PROP_ROW_VECTOR: + case PROP_ARRAY: + return QString ("%1 matrix").arg (dims_string (prop->get ().dims ())); + case PROP_BOOL: + return "boolean"; + case PROP_CALLBACK: + return "callback"; + case PROP_CHILDREN: + return "children"; + case PROP_COLOR: + return "color"; + case PROP_DOUBLE: + return "double"; + case PROP_DOUBLE_RADIO: + return "double radio"; + case PROP_HANDLE: + return "handle"; + case PROP_RADIO: + return "radio"; + case PROP_STRING: + return "string"; + case PROP_STRING_ARRAY: + return "string array"; + case PROP_TEXT_LABEL: + return "text label"; + case PROP_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 PROP_INVALID: + case PROP_STRING_ARRAY: + case PROP_DOUBLE_RADIO: + case PROP_CHILDREN: + case PROP_HANDLE: + case PROP_CALLBACK: + case PROP_ANY: + return false; + default: + break; + } + + if (prop->prop_type () == PROP_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; + + const auto prop = m_go.get_properties ().get_property (name); + + if (prop.is_hidden ()) + continue; + + prop_index pi; + pi.name = name; + pi.id = prop.get_id (); + m_props.push_back (pi); + } + std::sort (m_props.begin (), m_props.end (), + [] (const prop_index &a, const prop_index &b) -> bool { + return a.name < b.name; + }); + + auto events = m_oct_qobj.get_qt_interpreter_events (); + + QObject::connect (events.get (), &qt_interpreter_events::go_updated, this, + &go_properties_table_model::handle_go_update); + } + + 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 (m_props.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 (m_props.size ())) + return QVariant{}; + return QString::fromStdString (m_props[section].name); + } + 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"); + case COL_DOCLINK: + return tr ("Doc"); + default: + return QVariant{}; + } + case Qt::TextAlignmentRole: + switch (section) + { + case COL_DOCLINK: + return QVariant (Qt::AlignCenter); + // case COL_NAME: + case COL_VALUE: + case COL_TYPE: + default: + 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 (m_props.size ())) + return QModelIndex{}; + + graphics_object go = m_go; // remove constness + + const auto &prop_name = m_props[row].name; + 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 (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 (index.internalPointer ()); + if (!prop) + return QVariant{}; + + if (index.row () < 0 || index.row () >= static_cast (m_props.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); + case COL_DOCLINK: + return QVariant (Qt::AlignCenter); + default: + return QVariant{}; + } + } + + if (role == Qt::ToolTipRole && index.column () == COL_DOCLINK) + return go_property_doc_url (QString::fromStdString (m_go.type ()), + QString::fromStdString (prop->get_name ())); + + 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); + case COL_DOCLINK: + { + QVariant val; + val.setValue (hyperlink ( + go_property_doc_url (QString::fromStdString (m_go.type ()), + QString::fromStdString (prop->get_name ())), + "?")); + return val; + } + 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 (index.internalPointer ()); + if (!prop) + return false; + + bool res = false; + + const auto typ = static_cast (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; + } + + void go_properties_table_model::handle_go_update (const graphics_object &go, + int prop_id) + { + if (go.get_handle () != m_go.get_handle ()) + return; + + auto prop = std::find_if ( + m_props.begin (), m_props.end (), + [=] (const prop_index &pi) { return pi.id == prop_id; }); + + if (prop == m_props.end ()) + return; + + const auto row = static_cast (prop - m_props.begin ()); + + const auto topLeft = index (row, COL_VALUE, QModelIndex{}); + const auto bottomRight = index (row, COL_TYPE, QModelIndex{}); + + emit dataChanged (topLeft, bottomRight); + } + + go_properties_item_delegate::go_properties_item_delegate ( + QObject *p, base_qobject &oct_qobj, QTableView *view, + item_hyperlink_delegate *hl_dg, const gui_settings *settings) + : QStyledItemDelegate (p), + m_oct_qobj (oct_qobj), m_view{ view }, m_hl_dg{ hl_dg } + { + m_default_row_height = settings->value (pi_table_row_height).toInt (); + QObject::connect (this, &go_properties_item_delegate::layoutWillChange, + m_view, &QTableView::resizeRowToContents, + Qt::QueuedConnection); + } + + void + go_properties_item_delegate::notice_settings (const gui_settings *settings) + { + m_default_row_height = settings->value (pi_table_row_height).toInt (); + } + + inline int color_int_comp (const double comp) + { + return static_cast ( + 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_vpadding = 3; + constexpr int color_cell_hpadding = 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; + + painter->save (); + + int dy = (option.rect.height () - color_cell_icon_height) / 2; + + QRect iconRect + = option.rect.adjusted (color_cell_hpadding, 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 ( + color_cell_hpadding + color_cell_spacing + color_cell_icon_width, + dy, -color_cell_hpadding, -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); + } + } + + constexpr int hpadding = 3; + + 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 (index.internalPointer ()); + + if (prop->prop_type () == PROP_COLOR) + { + const auto *cp = static_cast (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 alignModel = index.data (Qt::TextAlignmentRole); + const auto align = alignModel.isValid () ? alignModel.toInt () + : Qt::AlignCenter; + + for (int r = 0; r < matrix.rows (); r++) + { + for (int c = 0; c < matrix.cols (); c++) + { + const auto tl = orig + QPoint (w * c + hpadding, h * r); + const auto rect = QRect (tl, QSize (w - 2 * hpadding, 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; + } + } + else if (index.column () == COL_DOCLINK) + { + return m_hl_dg->paint (painter, option, index); + } + return QStyledItemDelegate::paint (painter, option, index); + } + + QSize + go_properties_item_delegate::sizeHint (const QStyleOptionViewItem &option, + const QModelIndex &index) const + { + const auto minRowH = m_default_row_height; + + if (index.column () == COL_VALUE) + { + const auto *prop + = static_cast (index.internalPointer ()); + + if (prop->prop_type () == PROP_COLOR) + { + const auto *cp = static_cast (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_hpadding + color_cell_spacing + + color_cell_icon_width + textRect.width (), + 2 * color_cell_vpadding + 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 * hpadding; + + return QSize ( + maxW * matrix.cols (), + std::max (static_cast (maxH * matrix.rows ()), minRowH)); + } + } + else if (index.column () == COL_DOCLINK) + { + return m_hl_dg->size_hint (option, index); + } + + 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 (index.internalPointer ()); + + if (prop->prop_type () == PROP_COLOR) + { + // Color editor is quite big. it need to fit in at least three rows + + auto *editor = new color_editor_widget ( + parent, static_cast (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 (index.internalPointer ()); + + auto *combo = dynamic_cast (editor); + if (combo) + { + combo->setCurrentText ( + QString::fromStdString (prop->get ().string_value ())); + return; + } + + auto *cw = dynamic_cast (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 (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 (index.internalPointer ()); + + QComboBox *w = dynamic_cast (editor); + if (w) + { + model->setData (index, w->currentText (), Qt::EditRole); + return; + } + + auto *cw = dynamic_cast (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 (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 (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 (matrix.rows ()); ++r) + { + for (int c = 0; c < static_cast (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::quiet_NaN (); + } + C.replace (",", ""); + } + if (lastComma > lastDot) + { + // comma is decimal sep, dot (if any) are thousands sep + if (commas > 1) + { + return std::numeric_limits::quiet_NaN (); + } + C.replace (".", ""); + C.replace (",", "."); + } + + bool ok = false; + const auto val = C.toDouble (&ok); + return ok ? val : std::numeric_limits::quiet_NaN (); + } + + Matrix matrix_widget::get (bool *ok) const + { + Matrix matrix (m_layout->rowCount (), m_layout->columnCount ()); + + for (int r = 0; r < static_cast (matrix.rows ()); ++r) + { + for (int c = 0; c < static_cast (matrix.cols ()); ++c) + { + auto *item = m_layout->itemAtPosition (r, c); + auto *edit + = item ? dynamic_cast (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 (matrix.rows ()); ++r) + { + for (int c = 0; c < static_cast (matrix.cols ()); ++c) + { + auto *edit + = dynamic_cast (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 svg_color_names (); + + static const std::vector &svg_colors () + { + static std::vector 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 &octave_colors () + { + static std::vector result; + if (result.size () == 0) + { + const auto src = std::vector>{ + { "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::of (&QDoubleSpinBox::valueChanged), + this, &color_editor_widget::handle_rgb_mat_change); + QObject::connect (ui->gSpn, + QOverload::of (&QDoubleSpinBox::valueChanged), + this, &color_editor_widget::handle_rgb_mat_change); + QObject::connect (ui->bSpn, + QOverload::of (&QDoubleSpinBox::valueChanged), + this, &color_editor_widget::handle_rgb_mat_change); + + QObject::connect ( + ui->octaveBox, QOverload::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 (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 (svg_it - svg_cols.begin ())); + } + else + { + ui->svgBox->setCurrentText (color_web_code (color)); + } + } + + const auto sz = ui->rgbIcon->size () - QSize (8, 8); + 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 svg_color_names () + { + return std::vector{ "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 --git a/libgui/src/property-inspector-table.h b/libgui/src/property-inspector-table.h new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector-table.h @@ -0,0 +1,239 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if !defined(octave_property_inspector_table_h) +#define octave_property_inspector_table_h 1 + +#include + +#include +#include +#include +#include + +#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 gui_settings; + class item_hyperlink_delegate; + + class go_properties_table_view : public QTableView + { + Q_OBJECT + public: + go_properties_table_view (QWidget *parent); + virtual ~go_properties_table_view (); + + void set_hl_delegate (item_hyperlink_delegate *hl_dg) { m_hl_dg = hl_dg; } + + protected: + void mouseMoveEvent (QMouseEvent *ev) override; + void mousePressEvent (QMouseEvent *ev) override; + void mouseReleaseEvent (QMouseEvent *ev) override; + + private: + item_hyperlink_delegate *m_hl_dg = nullptr; + }; + + 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 slots: + + void handle_go_update (const graphics_object& go, int prop_id); + + private: + + struct prop_index { + std::string name; + int id; + }; + + base_qobject &m_oct_qobj; + graphics_object m_go; + + std::vector m_props; + }; + + class go_properties_item_delegate : public QStyledItemDelegate + { + Q_OBJECT + public: + go_properties_item_delegate (QObject *parent, base_qobject &oct_qobj, + QTableView *view, + item_hyperlink_delegate *hl_dg, + const gui_settings *settings); + ~go_properties_item_delegate () = default; + + void notice_settings (const gui_settings *settings); + + // 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; + item_hyperlink_delegate *m_hl_dg; + int m_default_row_height; + + 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 --git a/libgui/src/property-inspector-tree.cc b/libgui/src/property-inspector-tree.cc new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector-tree.cc @@ -0,0 +1,430 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#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 +{ + + enum Column { + COL_TYPE, + COL_DESC, + COL_VALUE, + COL_COUNT, + }; + + static QString go_description (graphics_object& go, gh_manager& gh_mgr) + { + const auto type = go.type (); + if (type == "uimenu") + { + return QString::fromStdString (go.get_properties () + .get_property ("label") + .get () + .string_value ()); + } + if (type == "uitoggletool" || type == "uipushtool") + { + return QString::fromStdString (go.get_properties () + .get_property ("tooltipstring") + .get () + .string_value ()); + } + if (type == "text") + { + return QString::fromStdString (go.get_properties () + .get_property ("string") + .get () + .string_value ()); + } + if (type == "axes") + { + auto titlehandle = go.get_properties () + .get_property ("title") + .get () + .scalar_value (); + auto title = gh_mgr.get_object (titlehandle); + return go_description (title, gh_mgr); + } + return ""; + } + + using node_ref = std::shared_ptr; + + // a node corresponding to a single graphics object + class pi_tree_node : public std::enable_shared_from_this + { + 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 ()); + m_description = go_description (obj, gh_mgr); + + 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 (m_children.size ()); + const auto c + = std::make_shared (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; } + + QString description () const { return m_description; } + + void update_description (gh_manager& gh_mgr) + { + auto obj = gh_mgr.get_object (m_gh); + m_description = go_description (obj, gh_mgr); + } + + const std::vector& 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 (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 (m_children.size ()); + ++r) + { + m_children[r]->m_row = r; + } + } + + private: + graphics_handle m_gh; + int m_row; + QString m_type; + QString m_description; + std::weak_ptr m_parent; + std::vector 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); + + QObject::connect (events.get (), &qt_interpreter_events::go_updated, this, + &go_tree_model::handle_go_update); + + m_root = std::make_shared (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 (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 COL_COUNT; } + + 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 (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 COL_TYPE: + return "Handle type"; + case COL_DESC: + return "Description"; + case COL_VALUE: + 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 COL_TYPE: + return pnode->type (); + case COL_DESC: + return pnode->description (); + case COL_VALUE: + 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 (pnode->children ().size ()); + + beginInsertRows (pindex, r, r); + + const auto node = std::make_shared (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 (); + } + + void go_tree_model::handle_go_update (const graphics_object& go, int prop_id) + { + Q_UNUSED(prop_id) + + const auto node = lookup (go); + + if (!node) + return; + + const auto olddesc = node->description (); + + { + auto gh_mgr = m_oct_qobj.interpreter_qobj ()->get_gh_manager (); + node->update_description (*gh_mgr); + } + + const auto newdesc = node->description (); + + if (olddesc == newdesc) + return; + + auto index = node_index(node, COL_DESC); + emit dataChanged(index, index); + } + + QModelIndex + go_tree_model::node_index (const std::shared_ptr& node, + int column) const + { + if (!node) + return QModelIndex{}; + + return createIndex (node->row (), column, + static_cast (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 (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 --git a/libgui/src/property-inspector-tree.h b/libgui/src/property-inspector-tree.h new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector-tree.h @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if !defined(octave_property_inspector_tree_h) +#define octave_property_inspector_tree_h 1 + +#include + +#include + +#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); + void handle_go_update (const graphics_object& go, int prop_id); + + private: + base_qobject& m_oct_qobj; + std::shared_ptr m_root; + + QModelIndex node_index (const std::shared_ptr& node, + int column = 0) const; + std::shared_ptr get_node (const QModelIndex& index) const; + std::shared_ptr lookup (const graphics_object& obj) const; + }; +} + +#endif diff --git a/libgui/src/property-inspector.cc b/libgui/src/property-inspector.cc new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector.cc @@ -0,0 +1,194 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "gui-preferences-pi.h" +#include "gui-settings.h" +#include "item-hyperlink-delegate.h" +#include "octave-qobject.h" +#include "property-inspector-table.h" +#include "property-inspector-tree.h" +#include "property-inspector.h" +#include "qt-application.h" +#include "resource-manager.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 go_properties_table_view (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); + + // setting widget layout + m_tree->setUniformRowHeights (true); + + m_splitter->addWidget (m_tree); + m_splitter->addWidget (m_table); + m_splitter->setChildrenCollapsible (true); + + // show handle of QSplitter + m_splitter->setStyleSheet ( + "QSplitter::handle:horizontal {" + " image: url(:/actions/icons/hsplitter-handle.png)" + "}"); + m_splitter->setHandleWidth(7); + + m_splitter->setSizes (QIntList{ 300, 600 }); + + setWidget (m_splitter); + + // read preferences + resource_manager &rmgr = m_octave_qobj.get_resource_manager (); + gui_settings *settings = rmgr.get_settings (); + + const auto table_row_height = settings->value (pi_table_row_height); + if (table_row_height.isValid ()) + { + // m_table->verticalHeader ()->setDefaultSectionSize ( + // table_row_height_pref.toInt ()); + } + else + { + settings->setValue ( + pi_table_row_height.key, + QVariant (m_table->verticalHeader ()->defaultSectionSize ())); + } + + m_tree->setAlternatingRowColors ( + settings->value (pi_tree_alternate_rows).toBool ()); + m_table->setAlternatingRowColors ( + settings->value (pi_table_alternate_rows).toBool ()); + + auto *table_hl_dg = new item_hyperlink_delegate (this, m_table); + + m_table_delegate = new go_properties_item_delegate ( + this, m_octave_qobj, m_table, table_hl_dg, settings); + + m_table->set_hl_delegate (table_hl_dg); + m_table->setItemDelegate (m_table_delegate); + + 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::notice_settings (const gui_settings *settings) + { + // m_table->verticalHeader ()->setDefaultSectionSize ( + // settings->value (pi_table_row_height).toInt ()); + m_tree->setAlternatingRowColors ( + settings->value (pi_tree_alternate_rows).toBool ()); + m_table->setAlternatingRowColors ( + settings->value (pi_table_alternate_rows).toBool ()); + + m_table_delegate->notice_settings (settings); + + octave_dock_widget::notice_settings (settings); + + m_table->resizeRowsToContents (); + } + + void property_inspector::save_settings () + { + // resource_manager &rmgr = m_octave_qobj.get_resource_manager (); + // gui_settings *settings = rmgr.get_settings (); + + // settings->setValue (pi_table_row_height.key, + // m_table->verticalHeader ()->defaultSectionSize ()); + // settings->setValue (pi_table_alternate_rows.key, + // m_table->alternatingRowColors ()); + // settings->setValue (pi_tree_alternate_rows.key, + // m_tree->alternatingRowColors ()); + + octave_dock_widget::save_settings (); + } + + 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 ¤t, + 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 ( + 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 --git a/libgui/src/property-inspector.h b/libgui/src/property-inspector.h new file mode 100644 --- /dev/null +++ b/libgui/src/property-inspector.h @@ -0,0 +1,86 @@ +//////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2013-2020 The Octave Project Developers +// +// See the file COPYRIGHT.md in the top-level directory of this +// distribution or . +// +// 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 +// . +// +//////////////////////////////////////////////////////////////////////// + +#if !defined(octave_property_inspector_h) +#define octave_property_inspector_h 1 + +#include + +#include "octave-dock-widget.h" + +class QSplitter; +class QTreeView; + +namespace octave +{ + class base_qobject; + + class go_tree_model; + class go_properties_table_model; + class go_properties_table_view; + class go_properties_item_delegate; + + 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; + + public slots: + + virtual void notice_settings (const gui_settings *) override; + + void save_settings (); + + private slots: + + // we need the interpreter ready, so we defer this to a slot. + void initialize_models (); + + void tree_current_changed (const QModelIndex ¤t, + const QModelIndex &previous); + + private: + QSplitter *m_splitter; + QTreeView *m_tree; + go_properties_table_view *m_table; + go_properties_item_delegate *m_table_delegate; + + go_tree_model *m_tree_model; + // the table is often switched, so it is easier to put it in unique_ptr + std::unique_ptr m_table_model; + }; +} + +#endif diff --git a/libgui/src/qt-interpreter-events.cc b/libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc +++ b/libgui/src/qt-interpreter-events.cc @@ -575,6 +575,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 --git a/libgui/src/qt-interpreter-events.h b/libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h +++ b/libgui/src/qt-interpreter-events.h @@ -35,15 +35,22 @@ #include #include +#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 as part of signal. typedef QList QIntList; class octave_value; +class graphics_object; + +namespace QtHandles +{ + class qt_graphics_toolkit; +} namespace octave { @@ -178,6 +185,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); } @@ -260,6 +273,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 --git a/libgui/src/resource.qrc b/libgui/src/resource.qrc --- a/libgui/src/resource.qrc +++ b/libgui/src/resource.qrc @@ -59,6 +59,7 @@ icons/graphic_logo_VariableEditor.png icons/fleur.png icons/hand2.png + icons/hsplitter-handle.png icons/left_side.png icons/letter_logo_FilesDockWidget.png icons/letter_logo_FileEditor.png diff --git a/libgui/src/settings-dialog.cc b/libgui/src/settings-dialog.cc --- a/libgui/src/settings-dialog.cc +++ b/libgui/src/settings-dialog.cc @@ -451,6 +451,11 @@ namespace octave // variable editor colors read_varedit_colors (settings); + // property inspector + pi_treeAlternateRows->setChecked(settings->value(pi_tree_alternate_rows).toBool()); + pi_tableRowHeight->setValue(settings->value(pi_table_row_height).toInt()); + pi_tableAlternateRows->setChecked(settings->value(pi_table_alternate_rows).toBool()); + // shortcuts shortcut_manager& scmgr = m_octave_qobj.get_shortcut_manager (); @@ -1069,6 +1074,11 @@ namespace octave settings->setValue (ve_font_size.key, varedit_fontSize->value ()); write_varedit_colors (settings); + // Property inspector + settings->setValue(pi_tree_alternate_rows.key, pi_treeAlternateRows->isChecked()); + settings->setValue(pi_table_alternate_rows.key, pi_tableAlternateRows->isChecked()); + settings->setValue(pi_table_row_height.key, pi_tableRowHeight->value()); + // shortcuts settings->setValue (sc_prevent_rl_conflicts.key, cb_prevent_readline_conflicts->isChecked ()); diff --git a/libgui/src/settings-dialog.ui b/libgui/src/settings-dialog.ui --- a/libgui/src/settings-dialog.ui +++ b/libgui/src/settings-dialog.ui @@ -22,8 +22,8 @@ Preferences - - + + @@ -32,7 +32,7 @@ - 7 + 0 @@ -843,7 +843,7 @@ 0 0 1021 - 1529 + 1528 @@ -2609,6 +2609,91 @@ + + + Property Inspector + + + + + + Graphics objects tree + + + + + + Alternate row colors + + + + + + + + + + Graphics properties table + + + + + + + 80 + 0 + + + + 32 + + + + + + + Alternate row colors + + + + + + + Default row height + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + Shortcuts @@ -2764,15 +2849,15 @@ false + + 64 + 120 false - - 64 - true @@ -2996,7 +3081,7 @@ - + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok diff --git a/libgui/src/shortcut-manager.cc b/libgui/src/shortcut-manager.cc --- a/libgui/src/shortcut-manager.cc +++ b/libgui/src/shortcut-manager.cc @@ -177,6 +177,7 @@ namespace octave init (tr ("Show Editor"), sc_main_window_show_editor); init (tr ("Show Documentation"), sc_main_window_show_doc); init (tr ("Show Variable Editor"), sc_main_window_show_variable_editor); + init (tr ("Show Property Inspector"), sc_main_window_show_property_inspector); init (tr ("Command Window"), sc_main_window_command); init (tr ("Command History"), sc_main_window_history); init (tr ("File Browser"), sc_main_window_file_browser); @@ -184,6 +185,7 @@ namespace octave init (tr ("Editor"), sc_main_window_editor); init (tr ("Documentation"), sc_main_window_doc); init (tr ("Variable Editor"), sc_main_window_variable_editor); + init (tr ("Property Inspector"), sc_main_window_property_inspector); init (tr ("Previous Widget"), sc_main_window_previous_dock); init (tr ("Reset Default Window Layout"), sc_main_window_reset); diff --git a/libinterp/corefcn/event-manager.h b/libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h +++ b/libinterp/corefcn/event-manager.h @@ -40,6 +40,7 @@ class octave_value; class string_vector; +class graphics_object; namespace octave { @@ -244,6 +245,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. @@ -623,6 +636,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 --git a/libinterp/corefcn/genprops.awk b/libinterp/corefcn/genprops.awk --- a/libinterp/corefcn/genprops.awk +++ b/libinterp/corefcn/genprops.awk @@ -300,7 +300,7 @@ function emit_declarations () if (class_name && ! base) emit_common_declarations(); - printf ("public:\n\n\n static std::set core_property_names (void);\n\n static std::set 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 all_property_names (void) const;\n\n"); + printf ("public:\n\n\n static std::set core_property_names (void);\n\n static std::set 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 all_property_names (void) const;\n\n"); if (! base) printf (" bool has_property (const caseless_str& pname) const;\n\n"); diff --git a/libinterp/corefcn/graphics.cc b/libinterp/corefcn/graphics.cc --- a/libinterp/corefcn/graphics.cc +++ b/libinterp/corefcn/graphics.cc @@ -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 ()); @@ -11892,6 +11909,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; } @@ -11909,6 +11929,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 --git a/libinterp/corefcn/graphics.in.h b/libinterp/corefcn/graphics.in.h --- a/libinterp/corefcn/graphics.in.h +++ b/libinterp/corefcn/graphics.in.h @@ -60,6 +60,25 @@ // --------------------------------------------------------------------- +enum property_type +{ + PROP_INVALID, + PROP_ARRAY, + PROP_BOOL, + PROP_CALLBACK, + PROP_CHILDREN, + PROP_COLOR, + PROP_DOUBLE, + PROP_DOUBLE_RADIO, + PROP_HANDLE, + PROP_RADIO, + PROP_ROW_VECTOR, + PROP_STRING, + PROP_STRING_ARRAY, + PROP_TEXT_LABEL, + PROP_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 PROP_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 PROP_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 PROP_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 PROP_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 PROP_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 PROP_COLOR; + } + octave_value get (void) const { if (current_type == color_t) @@ -1188,6 +1234,11 @@ public: minval (std::pair (octave_NaN, true)), maxval (std::pair (octave_NaN, true)) { } + virtual property_type prop_type () const override + { + return PROP_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 PROP_DOUBLE_RADIO; + } + octave_value get (void) const { if (current_type == double_t) @@ -1401,6 +1457,11 @@ public: maxval (std::pair (octave_NaN, true)) { } + virtual property_type prop_type () const override + { + return PROP_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 PROP_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 PROP_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 PROP_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 PROP_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 PROP_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 PROP_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 +inline prop_type& prop_cast(base_property *rep, const char *who) +{ + auto *p = dynamic_cast(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 (rep)); } - - const radio_property& as_radio_property (void) const - { return *(dynamic_cast (rep)); } + + const array_property& as_array_property (void) const + { + return detail::prop_cast ( + rep, "property::as_array_property"); + } + + array_property& as_array_property (void) + { + return detail::prop_cast (rep, + "property::as_array_property"); + } + + const bool_property& as_bool_property (void) const + { + return detail::prop_cast ( + rep, "property::as_bool_property"); + } + + bool_property& as_bool_property (void) + { + return detail::prop_cast (rep, + "property::as_bool_property"); + } + + const callback_property& as_callback_property (void) const + { + return detail::prop_cast ( + rep, "property::as_callback_property"); + } + + callback_property& as_callback_property (void) + { + return detail::prop_cast ( + rep, "property::as_callback_property"); + } + + const children_property& as_children_property (void) const + { + return detail::prop_cast ( + rep, "property::as_children_property"); + } + + children_property& as_children_property (void) + { + return detail::prop_cast ( + rep, "property::as_children_property"); + } const color_property& as_color_property (void) const - { return *(dynamic_cast (rep)); } + { + return detail::prop_cast ( + rep, "property::as_color_property"); + } + + color_property& as_color_property (void) + { + return detail::prop_cast (rep, + "property::as_color_property"); + } const double_property& as_double_property (void) const - { return *(dynamic_cast (rep)); } - - const bool_property& as_bool_property (void) const - { return *(dynamic_cast (rep)); } + { + return detail::prop_cast ( + rep, "property::as_double_property"); + } + + double_property& as_double_property (void) + { + return detail::prop_cast (rep, + "property::as_double_property"); + } + + const double_radio_property& as_double_radio_property (void) const + { + return detail::prop_cast ( + rep, "property::as_double_radio_property"); + } + + double_radio_property& as_double_radio_property (void) + { + return detail::prop_cast ( + rep, "property::as_double_radio_property"); + } const handle_property& as_handle_property (void) const - { return *(dynamic_cast (rep)); } -#endif + { + return detail::prop_cast ( + rep, "property::as_handle_property"); + } + + handle_property& as_handle_property (void) + { + return detail::prop_cast (rep, + "property::as_handle_property"); + } + + const radio_property& as_radio_property (void) const + { + return detail::prop_cast ( + rep, "property::as_radio_property"); + } + + radio_property& as_radio_property (void) + { + return detail::prop_cast (rep, + "property::as_radio_property"); + } + + const row_vector_property& as_row_vector_property (void) const + { + return detail::prop_cast ( + rep, "property::as_row_vector_property"); + } + + row_vector_property& as_row_vector_property (void) + { + return detail::prop_cast ( + rep, "property::as_row_vector_property"); + } + + const string_property& as_string_property (void) const + { + return detail::prop_cast ( + rep, "property::as_string_property"); + } + + string_property& as_string_property (void) + { + return detail::prop_cast (rep, + "property::as_string_property"); + } + + const string_array_property& as_string_array_property (void) const + { + return detail::prop_cast ( + rep, "property::as_string_array_property"); + } + + string_array_property& as_string_array_property (void) + { + return detail::prop_cast ( + rep, "property::as_string_array_property"); + } + + const text_label_property& as_text_label_property (void) const + { + return detail::prop_cast ( + rep, "property::as_text_label_property"); + } + + text_label_property& as_text_label_property (void) + { + return detail::prop_cast ( + rep, "property::as_text_label_property"); + } + + const any_property& as_any_property (void) const + { + return detail::prop_cast (rep, + "property::as_any_property"); + } + + any_property& as_any_property (void) + { + return detail::prop_cast (rep, "property::as_any_property"); + } + +#endif // 0 private: base_property *rep; diff --git a/liboctave/util/oct-mutex.h b/liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h +++ b/liboctave/util/oct-mutex.h @@ -26,6 +26,8 @@ #if ! defined (octave_oct_mutex_h) #define octave_oct_mutex_h 1 +#include + #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 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