From 3d0bb8516bb458b88452a92eb88b7e8bf625377e Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 2 Sep 2023 00:36:17 +0200 Subject: [PATCH] Non-modal notifications for macro editors too --- src/lay/lay/layMacroEditorDialog.cc | 101 +++++++++++++----------- src/lay/lay/layMacroEditorDialog.h | 1 + src/lay/lay/layMacroEditorPage.cc | 116 +++++++++++++++++++++++++++- src/lay/lay/layMacroEditorPage.h | 115 ++++++++++++++++++++++++++- 4 files changed, 282 insertions(+), 51 deletions(-) diff --git a/src/lay/lay/layMacroEditorDialog.cc b/src/lay/lay/layMacroEditorDialog.cc index 07e54259a..6129038ee 100644 --- a/src/lay/lay/layMacroEditorDialog.cc +++ b/src/lay/lay/layMacroEditorDialog.cc @@ -2373,6 +2373,15 @@ BEGIN_PROTECTED END_PROTECTED } +void +MacroEditorDialog::close_requested () +{ + MacroEditorPage *page = dynamic_cast (sender ()); + if (! m_in_exec && page) { + tab_close_requested (tabWidget->indexOf (page)); + } +} + void MacroEditorDialog::tab_close_requested (int index) { @@ -2605,66 +2614,65 @@ BEGIN_PROTECTED return; } - std::set modified_files; - for (std::map ::const_iterator m = m_tab_widgets.begin (); m != m_tab_widgets.end (); ++m) { - if (m->first->is_modified ()) { - modified_files.insert (m->first->path ()); + std::map path_to_page; + for (int i = 0; i < tabWidget->count (); ++i) { + MacroEditorPage *page = dynamic_cast (tabWidget->widget (i)); + if (page) { + path_to_page.insert (std::make_pair (page->path (), page)); } } - std::vector conflicts; for (std::vector::const_iterator f = m_changed_files.begin (); f != m_changed_files.end (); ++f) { - if (modified_files.find (tl::to_string (*f)) != modified_files.end ()) { - conflicts.push_back (*f); + + std::string fn = tl::to_string (*f); + auto w = path_to_page.find (fn); + if (w == path_to_page.end ()) { + continue; + } + + if (w->second->macro () && w->second->macro ()->is_modified ()) { + + lay::MacroEditorNotification n ("reload", tl::to_string (tr ("Macro has changed on disk, but was modified")), tl::Variant (fn)); + n.add_action ("reload", tl::to_string (tr ("Reload and discard changes"))); + w->second->add_notification (n); + + } else { + + lay::MacroEditorNotification n ("reload", tl::to_string (tr ("Macro has changed on disk")), tl::Variant (fn)); + n.add_action ("reload", tl::to_string (tr ("Reload"))); + w->second->add_notification (n); + } } + for (std::vector::const_iterator f = m_removed_files.begin (); f != m_removed_files.end (); ++f) { - if (modified_files.find (tl::to_string (*f)) != modified_files.end ()) { - conflicts.push_back (*f); + + std::string fn = tl::to_string (*f); + auto w = path_to_page.find (fn); + if (w == path_to_page.end ()) { + continue; + } + + if (w->second->macro () && w->second->macro ()->is_modified ()) { + + lay::MacroEditorNotification n ("close", tl::to_string (tr ("Macro has been removed on disk, but was modified")), tl::Variant (fn)); + n.add_action ("close", tl::to_string (tr ("Close tab and discard changes"))); + w->second->add_notification (n); + + } else { + + lay::MacroEditorNotification n ("close", tl::to_string (tr ("Macro has been removed on disk")), tl::Variant (fn)); + n.add_action ("close", tl::to_string (tr ("Close tab"))); + w->second->add_notification (n); + } } - QString msg; - - if (m_changed_files.size () + m_removed_files.size () == 1) { - msg = QObject::tr ("The following file has been changed on disk:\n\n"); - for (std::vector::const_iterator f = m_changed_files.begin (); f != m_changed_files.end (); ++f) { - msg += QString::fromUtf8 (" %1 (modified)\n").arg (*f); - } - for (std::vector::const_iterator f = m_removed_files.begin (); f != m_removed_files.end (); ++f) { - msg += QString::fromUtf8 (" %1 (removed)\n").arg (*f); - } - if (!conflicts.empty ()) { - msg += tr ("\nThis file has been modified in the editor as well.\nRefresh this file and discard changes?"); - } else { - msg += tr ("\nRefresh this file?"); - } - } else { - msg = QObject::tr ("The following files have been changed on disk:\n\n"); - for (std::vector::const_iterator f = m_changed_files.begin (); f != m_changed_files.end (); ++f) { - msg += QString::fromUtf8 (" %1 (modified)\n").arg (*f); - } - for (std::vector::const_iterator f = m_removed_files.begin (); f != m_removed_files.end (); ++f) { - msg += QString::fromUtf8 (" %1 (removed)\n").arg (*f); - } - if (!conflicts.empty ()) { - msg += tr("\nSome of these files are modified on disk and in the editor:\n\n"); - for (std::vector::const_iterator f = conflicts.begin (); f != conflicts.end (); ++f) { - msg += QString::fromUtf8 (" %1 (conflict)\n").arg (*f); - } - msg += tr ("\nRefresh these and the other files and discard all changes?"); - } else { - msg += tr ("\nRefresh those files?"); - } - } + refresh_file_watcher (); m_changed_files.clear (); m_removed_files.clear (); - if (QMessageBox::question (this, tr ("Refresh Files"), msg, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - reload_macros (); - } - END_PROTECTED } @@ -3523,6 +3531,7 @@ MacroEditorDialog::create_page (lym::Macro *macro) editor->set_font (m_font_family, m_font_size); editor->exec_model ()->set_run_mode (m_in_exec); editor->connect_macro (macro); + connect (editor.get (), SIGNAL (close_requested ()), this, SLOT (close_requested ())); connect (editor.get (), SIGNAL (help_requested (const QString &)), this, SLOT (help_requested (const QString &))); connect (editor.get (), SIGNAL (search_requested (const QString &, bool)), this, SLOT (search_requested (const QString &, bool))); connect (editor.get (), SIGNAL (edit_trace (bool)), this, SLOT (add_edit_trace (bool))); diff --git a/src/lay/lay/layMacroEditorDialog.h b/src/lay/lay/layMacroEditorDialog.h index 24bd3eccf..71c78ca02 100644 --- a/src/lay/lay/layMacroEditorDialog.h +++ b/src/lay/lay/layMacroEditorDialog.h @@ -212,6 +212,7 @@ private slots: void search_editing (); void search_finished (); void tab_close_requested (int); + void close_requested (); void close_all (); void close_all_but_this (); void close_all_left (); diff --git a/src/lay/lay/layMacroEditorPage.cc b/src/lay/lay/layMacroEditorPage.cc index 3abaa328b..bcb8c302f 100644 --- a/src/lay/lay/layMacroEditorPage.cc +++ b/src/lay/lay/layMacroEditorPage.cc @@ -46,6 +46,8 @@ #include #include #include +#include +#include namespace lay { @@ -81,6 +83,59 @@ void MacroEditorTextWidget::paintEvent (QPaintEvent *event) return TextEditWidget::paintEvent (event); } +// ---------------------------------------------------------------------------------------------- +// MacroEditorNotificationWidget implementation + +MacroEditorNotificationWidget::MacroEditorNotificationWidget (MacroEditorPage *parent, const MacroEditorNotification *notification) + : QFrame (parent), mp_parent (parent), mp_notification (notification) +{ + setBackgroundRole (QPalette::ToolTipBase); + setAutoFillBackground (true); + + QHBoxLayout *layout = new QHBoxLayout (this); + layout->setContentsMargins (4, 4, 4, 4); + + QLabel *title_label = new QLabel (this); + layout->addWidget (title_label, 1); + title_label->setText (tl::to_qstring (notification->title ())); + title_label->setForegroundRole (QPalette::ToolTipText); + title_label->setWordWrap (true); + activate_help_links (title_label); + + for (auto a = notification->actions ().begin (); a != notification->actions ().end (); ++a) { + + QPushButton *pb = new QPushButton (this); + layout->addWidget (pb); + + pb->setText (tl::to_qstring (a->second)); + m_action_buttons.insert (std::make_pair (pb, a->first)); + connect (pb, SIGNAL (clicked ()), this, SLOT (action_triggered ())); + + } + + QToolButton *close_button = new QToolButton (); + close_button->setIcon (QIcon (":clear_edit_16px.png")); + close_button->setAutoRaise (true); + layout->addWidget (close_button); + + connect (close_button, SIGNAL (clicked ()), this, SLOT (close_triggered ())); +} + +void +MacroEditorNotificationWidget::action_triggered () +{ + auto a = m_action_buttons.find (sender ()); + if (a != m_action_buttons.end ()) { + mp_parent->notification_action (*mp_notification, a->second); + } +} + +void +MacroEditorNotificationWidget::close_triggered () +{ + mp_parent->remove_notification (*mp_notification); +} + // ---------------------------------------------------------------------------------------------- // MacroEditorHighlighters implementation @@ -491,15 +546,17 @@ void MacroEditorSidePanel::paintEvent (QPaintEvent *) MacroEditorPage::MacroEditorPage (QWidget * /*parent*/, MacroEditorHighlighters *highlighters) : mp_macro (0), mp_highlighters (highlighters), mp_highlighter (0), m_error_line (-1), m_ntab (8), m_nindent (2), m_ignore_cursor_changed_event (false) { - QVBoxLayout *layout = new QVBoxLayout (this); - + mp_layout = new QVBoxLayout (this); + mp_layout->setContentsMargins (0, 0, 0, 0); + mp_readonly_label = new QLabel (this); mp_readonly_label->setText (QObject::tr ("Macro is read-only and cannot be edited")); mp_readonly_label->hide (); - layout->addWidget (mp_readonly_label); + mp_layout->addWidget (mp_readonly_label); QHBoxLayout *hlayout = new QHBoxLayout (); - layout->addLayout (hlayout); + hlayout->setContentsMargins (4, 4, 4, 4); + mp_layout->addLayout (hlayout); mp_exec_model = new MacroEditorExecutionModel (this); mp_text = new MacroEditorTextWidget (this); @@ -1021,6 +1078,8 @@ void MacroEditorPage::connect_macro (lym::Macro *macro) if (mp_macro) { + m_path = mp_macro->path (); + connect (mp_macro, SIGNAL (changed ()), this, SLOT (update ())); lym::Macro::Interpreter lang = macro->interpreter (); @@ -1924,5 +1983,54 @@ MacroEditorPage::eventFilter (QObject *watched, QEvent *event) return false; } +void +MacroEditorPage::add_notification (const MacroEditorNotification ¬ification) +{ + if (m_notification_widgets.find (¬ification) == m_notification_widgets.end ()) { + m_notifications.push_back (notification); + QWidget *w = new MacroEditorNotificationWidget (this, &m_notifications.back ()); + m_notification_widgets.insert (std::make_pair (&m_notifications.back (), w)); + mp_layout->insertWidget (0, w); + } +} + +void +MacroEditorPage::remove_notification (const MacroEditorNotification ¬ification) +{ + auto nw = m_notification_widgets.find (¬ification); + if (nw != m_notification_widgets.end ()) { + + nw->second->deleteLater (); + m_notification_widgets.erase (nw); + + for (auto n = m_notifications.begin (); n != m_notifications.end (); ++n) { + if (*n == notification) { + m_notifications.erase (n); + break; + } + } + + } +} + +void +MacroEditorPage::notification_action (const MacroEditorNotification ¬ification, const std::string &action) +{ + if (action == "close") { + + remove_notification (notification); + + emit close_requested (); + + } else if (action == "reload") { + + remove_notification (notification); + + mp_macro->load (); + mp_macro->reset_modified (); + + } +} + } diff --git a/src/lay/lay/layMacroEditorPage.h b/src/lay/lay/layMacroEditorPage.h index c298a32b4..3e4e1fbf6 100644 --- a/src/lay/lay/layMacroEditorPage.h +++ b/src/lay/lay/layMacroEditorPage.h @@ -27,6 +27,7 @@ #include "lymMacro.h" #include "layGenericSyntaxHighlighter.h" +#include "tlVariant.h" #include #include @@ -47,10 +48,13 @@ class QSyntaxHighlighter; class QTimer; class QWindow; class QListWidget; +class QVBoxLayout; namespace lay { +class MacroEditorPage; + /** * @brief A collection of highlighters */ @@ -209,7 +213,92 @@ private: bool m_debugging_on; }; -class MacroEditorPage +/** + * @brief Descriptor for a notification inside the macro editor + * + * Notifications are popups added at the top of the view to indicate need for reloading for example. + * Notifications have a name, a title, optional actions (id, title) and a parameter (e.g. file path to reload). + * Actions are mapped to QPushButtons. + */ +class MacroEditorNotification +{ +public: + MacroEditorNotification (const std::string &name, const std::string &title, const tl::Variant ¶meter = tl::Variant ()) + : m_name (name), m_title (title), m_parameter (parameter) + { + // .. nothing yet .. + } + + void add_action (const std::string &name, const std::string &title) + { + m_actions.push_back (std::make_pair (name, title)); + } + + const std::vector > &actions () const + { + return m_actions; + } + + const std::string &name () const + { + return m_name; + } + + const std::string &title () const + { + return m_title; + } + + const tl::Variant ¶meter () const + { + return m_parameter; + } + + bool operator<(const MacroEditorNotification &other) const + { + if (m_name != other.name ()) { + return m_name < other.name (); + } + return m_parameter < other.parameter (); + } + + bool operator==(const MacroEditorNotification &other) const + { + if (m_name != other.name ()) { + return false; + } + return m_parameter == other.parameter (); + } + +private: + std::string m_name; + std::string m_title; + tl::Variant m_parameter; + std::vector > m_actions; +}; + +/** + * @brief A widget representing a notification + */ +class MacroEditorNotificationWidget + : public QFrame +{ +Q_OBJECT + +public: + MacroEditorNotificationWidget (MacroEditorPage *parent, const MacroEditorNotification *notification); + +private slots: + void action_triggered (); + void close_triggered (); + +private: + MacroEditorPage *mp_parent; + const MacroEditorNotification *mp_notification; + std::map m_action_buttons; +}; + +class MacroEditorPage : public QWidget { Q_OBJECT @@ -224,6 +313,11 @@ public: return mp_macro; } + const std::string path () const + { + return m_path; + } + bool is_modified () const { return m_is_modified; @@ -269,10 +363,14 @@ public: void set_editor_focus (); + void add_notification (const MacroEditorNotification ¬ificaton); + void remove_notification (const MacroEditorNotification ¬ificaton); + signals: void help_requested (const QString &s); void search_requested (const QString &s, bool backward); void edit_trace (bool); + void close_requested (); public slots: void commit (); @@ -288,10 +386,22 @@ protected slots: void hide_completer (); private: + friend class MacroEditorNotificationWidget; + + struct CompareNotificationPointers + { + bool operator() (const MacroEditorNotification *a, const MacroEditorNotification *b) const + { + return *a < *b; + } + }; + lym::Macro *mp_macro; + std::string m_path; MacroEditorExecutionModel *mp_exec_model; MacroEditorTextWidget *mp_text; MacroEditorSidePanel *mp_side_panel; + QVBoxLayout *mp_layout; QLabel *mp_readonly_label; bool m_is_modified; MacroEditorHighlighters *mp_highlighters; @@ -305,6 +415,8 @@ private: QTimer *mp_completer_timer; QWidget *mp_completer_popup; QListWidget *mp_completer_list; + std::list m_notifications; + std::map m_notification_widgets; void update_extra_selections (); bool return_pressed (); @@ -316,6 +428,7 @@ private: QTextCursor get_completer_cursor (int &pos0, int &pos); bool select_match_here (); void replace_in_selection (const QString &replace, bool first); + void notification_action (const MacroEditorNotification ¬ification, const std::string &action); bool eventFilter (QObject *watched, QEvent *event); };