/* KLayout Layout Viewer Copyright (C) 2006-2024 Matthias Koefferlein This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "layMacroEditorPage.h" #include "lymMacroInterpreter.h" #include "tlExceptions.h" #include "tlString.h" #include "layQtTools.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace lay { // ---------------------------------------------------------------------------------------------- // Utility wrapper around QTextBlock::firstLineNumber - emulation for Qt < 4.5 static int firstLineNumber (const QTextBlock &b) { #if QT_VERSION >= 0x040500 return b.firstLineNumber (); #else return QTextCursor (b).blockNumber (); #endif } // ---------------------------------------------------------------------------------------------- // MacroEditorTextWidget implementation MacroEditorTextWidget::MacroEditorTextWidget (QWidget *parent) : TextEditWidget (parent) { } void MacroEditorTextWidget::paintEvent (QPaintEvent *event) { // lacking any other good way to detect scrolling, we catch the paint events of the viewport // to detect a change in the geometry QRect r (0, -verticalScrollBar ()->value (), 1, height ()); if (r != m_r) { m_r = r; emit contentsChanged (); } 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 MacroEditorHighlighters::MacroEditorHighlighters (QObject *parent) : m_basic_attributes () { // TODO: more languages m_attributes.push_back (std::make_pair ("ruby", GenericSyntaxHighlighterAttributes (&m_basic_attributes))); m_attributes.push_back (std::make_pair ("python", GenericSyntaxHighlighterAttributes (&m_basic_attributes))); for (std::vector >::iterator a = m_attributes.begin (); a != m_attributes.end (); ++a) { // Note: this loads and initializes the attributes delete highlighter_for_scheme (parent, a->first, &a->second, true); } } QSyntaxHighlighter * MacroEditorHighlighters::highlighter_for (QObject *parent, lym::Macro::Interpreter lang, const std::string &dsl_name, bool initialize) { std::string scheme = scheme_for (lang, dsl_name); for (std::vector >::iterator a = m_attributes.begin (); a != m_attributes.end (); ++a) { if (a->first == scheme) { return highlighter_for_scheme (parent, a->first, &a->second, initialize); } } return 0; } lay::GenericSyntaxHighlighter * MacroEditorHighlighters::highlighter_for_scheme (QObject *parent, const std::string &scheme, GenericSyntaxHighlighterAttributes *attributes, bool initialize) { if (! scheme.empty ()) { QResource res (tl::to_qstring (":/syntax/" + scheme + ".xml")); QByteArray data; #if QT_VERSION >= 0x60000 if (res.compressionAlgorithm () == QResource::ZlibCompression) { #else if (res.isCompressed ()) { #endif data = qUncompress ((const unsigned char *)res.data (), (int)res.size ()); } else { data = QByteArray ((const char *)res.data (), (int)res.size ()); } QBuffer input (&data); input.open (QIODevice::ReadOnly); lay::GenericSyntaxHighlighter *hl = new GenericSyntaxHighlighter (parent, input, attributes, initialize); input.close (); return hl; } else { return 0; } } GenericSyntaxHighlighterAttributes * MacroEditorHighlighters::attributes_for (lym::Macro::Interpreter lang, const std::string &dsl_name) { std::string scheme = scheme_for (lang, dsl_name); for (std::vector >::iterator a = m_attributes.begin (); a != m_attributes.end (); ++a) { if (a->first == scheme) { return &a->second; } } return 0; } GenericSyntaxHighlighterAttributes * MacroEditorHighlighters::basic_attributes () { return &m_basic_attributes; } std::string MacroEditorHighlighters::to_string () const { std::string s = "basic:" + m_basic_attributes.to_string (); for (std::vector >::const_iterator a = m_attributes.begin (); a != m_attributes.end (); ++a) { s += tl::to_word_or_quoted_string (a->first) + ":" + a->second.to_string (); } return s; } void MacroEditorHighlighters::load (const std::string &s) { try { GenericSyntaxHighlighterAttributes def; tl::Extractor ex (s.c_str ()); while (! ex.at_end ()) { std::string t; ex.read_word_or_quoted (t); ex.test (":"); GenericSyntaxHighlighterAttributes *attributes = &def; if (t == "basic") { attributes = &m_basic_attributes; } else { for (std::vector >::iterator a = m_attributes.begin (); a != m_attributes.end (); ++a) { if (a->first == t) { attributes = &a->second; break; } } } attributes->read (ex); } } catch (...) { // ignore errors; } } std::string MacroEditorHighlighters::scheme_for (lym::Macro::Interpreter lang, const std::string &dsl_name) { if (lang == lym::Macro::Ruby) { return "ruby"; } else if (lang == lym::Macro::Python) { return "python"; } else if (lang == lym::Macro::DSLInterpreter) { return lym::MacroInterpreter::syntax_scheme (dsl_name); } else { return std::string (); } } // ---------------------------------------------------------------------------------------------- // MacroEditorExecutionModel implementation MacroEditorExecutionModel::MacroEditorExecutionModel (QObject *parent) : QObject (parent), m_current_line (-1), m_run_mode (false), m_interpreter (lym::Macro::None) { // .. nothing yet .. } void MacroEditorExecutionModel::set_interpreter (lym::Macro::Interpreter lang) { m_interpreter = lang; if (lang == lym::Macro::None) { set_breakpoints (std::set ()); } } void MacroEditorExecutionModel::set_breakpoints (const std::set &b) { if (m_interpreter == lym::Macro::None) { return; } if (m_breakpoints != b) { m_breakpoints = b; emit breakpoints_changed (); } } void MacroEditorExecutionModel::toggle_breakpoint (int line) { if (m_interpreter == lym::Macro::None) { return; } if (m_breakpoints.find (line) == m_breakpoints.end ()) { m_breakpoints.insert (line); } else { m_breakpoints.erase (line); } emit breakpoints_changed (); } void MacroEditorExecutionModel::set_breakpoint (int line) { if (m_interpreter == lym::Macro::None) { return; } if (m_breakpoints.find (line) == m_breakpoints.end ()) { m_breakpoints.insert (line); emit breakpoints_changed (); } } void MacroEditorExecutionModel::remove_breakpoint (int line) { if (m_interpreter == lym::Macro::None) { return; } if (m_breakpoints.find (line) != m_breakpoints.end ()) { m_breakpoints.erase (line); emit breakpoints_changed (); } } void MacroEditorExecutionModel::set_current_line (int line, bool force_event) { if (m_interpreter == lym::Macro::None) { return; } if (force_event || line != m_current_line) { m_current_line = line; emit current_line_changed (); } } void MacroEditorExecutionModel::set_run_mode (bool run_mode) { if (m_interpreter == lym::Macro::None) { return; } if (m_run_mode != run_mode) { m_run_mode = run_mode; emit run_mode_changed (); } } // ---------------------------------------------------------------------------------------------- // MacroEditorSidePanel implementation const int sidePanelMargin = 4; MacroEditorSidePanel::MacroEditorSidePanel (QWidget *parent, MacroEditorTextWidget *text, MacroEditorExecutionModel *exec_model) : QWidget (parent), mp_text (text), mp_exec_model (exec_model), m_breakpoint_pixmap (QString::fromUtf8 (":/breakpointmark_16px.png")), m_breakpoint_disabled_pixmap (QString::fromUtf8 (":/breakpointmarkdisabled_16px.png")), m_exec_point_pixmap (QString::fromUtf8 (":/execmark_16px.png")), m_debugging_on (true) { connect (text, SIGNAL (contentsChanged ()), this, SLOT (redraw ())); connect (text, SIGNAL (cursorPositionChanged ()), this, SLOT (redraw ())); connect (exec_model, SIGNAL (breakpoints_changed ()), this, SLOT (redraw ())); connect (exec_model, SIGNAL (current_line_changed ()), this, SLOT (redraw ())); connect (exec_model, SIGNAL (run_mode_changed ()), this, SLOT (redraw ())); } void MacroEditorSidePanel::set_debugging_on (bool on) { if (m_debugging_on != on) { m_debugging_on = on; update (); } } QSize MacroEditorSidePanel::sizeHint () const { int w; #if QT_VERSION >= 0x60000 w = QFontMetrics (mp_text->font ()).horizontalAdvance (QString::fromUtf8 ("12345")); #else w = QFontMetrics (mp_text->font ()).width (QString::fromUtf8 ("12345")); #endif return QSize (w + 3 * sidePanelMargin + m_breakpoint_pixmap.width (), 0); } void MacroEditorSidePanel::mousePressEvent (QMouseEvent *event) { if (event->button () == Qt::LeftButton) { // toggle a breakpoint if required QTextBlock b = mp_text->cursorForPosition (QPoint (0, -mp_text->viewport ()->rect ().top ())).block (); int line = -1; while (b.isValid ()) { QRect rc = mp_text->cursorRect (QTextCursor (b)); rc.translate (0, mp_text->frameWidth () + mp_text->viewport ()->rect ().top ()); QRect rt (0, rc.top (), width () - 1, rc.height ()); if (rt.contains (event->pos ())) { line = firstLineNumber (b) + 1; break; } b = b.next (); } if (line >= 0) { mp_exec_model->toggle_breakpoint (line); } } } void MacroEditorSidePanel::redraw () { update (); } void MacroEditorSidePanel::set_watermark (const QString &wm) { if (m_watermark != wm) { m_watermark = wm; update (); } } void MacroEditorSidePanel::paintEvent (QPaintEvent *) { QPainter p (this); QPen sepPen (palette ().color (QPalette::Dark)); QPen textPen (palette ().color (QPalette::Dark)); QPen hlTextPen (palette ().color (QPalette::Light)); QBrush hlBrush (palette ().color (QPalette::Dark)); QRect rsel = mp_text->cursorRect (mp_text->textCursor ()); // paint background for (QTextBlock b = mp_text->cursorForPosition (QPoint (0, -mp_text->viewport ()->rect ().top ())).block (); b.isValid (); b = b.next ()) { QRect rc = mp_text->cursorRect (QTextCursor (b)); rc.translate (0, mp_text->frameWidth () + mp_text->viewport ()->rect ().top ()); QRect rt (sidePanelMargin + m_breakpoint_pixmap.width (), rc.top (), width (), rc.height ()); int rsel_center = (rsel.bottom () + rsel.top ()) / 2; if (rc.top () < rsel_center && rc.bottom () > rsel_center) { p.fillRect (rt, hlBrush); } } // paint watermark text if (! m_watermark.isEmpty ()) { p.save (); p.rotate (-90.0); p.setPen (QPen (QColor (0, 0, 0, 10))); QFont ip_font (QString::fromUtf8 ("Helvetica")); ip_font.setWeight (QFont::Bold); ip_font.setPixelSize (width ()); p.setFont (ip_font); p.drawText (QRect (-height (), 0, height (), width ()), Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextDontClip | Qt::TextSingleLine, m_watermark); p.restore (); } // paint foreground for (QTextBlock b = mp_text->cursorForPosition (QPoint (0, -mp_text->viewport ()->rect ().top ())).block (); b.isValid (); b = b.next ()) { int l = firstLineNumber (b) + 1; QRect rc = mp_text->cursorRect (QTextCursor (b)); rc.translate (0, mp_text->frameWidth () + mp_text->viewport ()->rect ().top ()); QRect rt (sidePanelMargin + m_breakpoint_pixmap.width (), rc.top (), width (), rc.height ()); p.setFont (b.charFormat ().font ()); int rsel_center = (rsel.bottom () + rsel.top ()) / 2; if (rc.top () < rsel_center && rc.bottom () > rsel_center) { p.setPen (hlTextPen); } else { p.setPen (textPen); } p.drawText (rt.adjusted (sidePanelMargin, 0, 0, 0), Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip | Qt::TextSingleLine, QString::number (l)); p.setPen (sepPen); p.drawLine (0, rc.top (), width () - 1, rc.top ()); if (rc.top () >= rect ().bottom ()) { break; } if (mp_exec_model->breakpoints ().find (l) != mp_exec_model->breakpoints ().end ()) { int icon_size = std::min (m_breakpoint_pixmap.height (), rt.height ()); QRect rpt (0, rt.center ().y () - icon_size / 2 + 1, icon_size, icon_size); if (m_debugging_on) { p.drawPixmap (rpt, m_breakpoint_pixmap, m_breakpoint_pixmap.rect ()); } else { p.drawPixmap (rpt, m_breakpoint_disabled_pixmap, m_breakpoint_pixmap.rect ()); } } if (mp_exec_model->run_mode () && mp_exec_model->current_line () == l) { int icon_size = std::min (m_exec_point_pixmap.height (), rt.height ()); QRect rpt (sidePanelMargin, rt.center ().y () - icon_size / 2 + 1, icon_size, icon_size); p.drawPixmap (rpt, m_exec_point_pixmap, m_exec_point_pixmap.rect ()); } } } // ---------------------------------------------------------------------------------------------- // MacroEditorPage implementation 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) { mp_layout = new QVBoxLayout (this); mp_layout->setContentsMargins (0, 0, 0, 0); QVBoxLayout *vlayout = new QVBoxLayout (this); vlayout->setContentsMargins (4, 4, 4, 4); mp_layout->addLayout (vlayout); mp_readonly_label = new QLabel (this); mp_readonly_label->setText (QObject::tr ("Macro is read-only and cannot be edited")); mp_readonly_label->hide (); vlayout->addWidget (mp_readonly_label); QHBoxLayout *hlayout = new QHBoxLayout (); mp_layout->addLayout (hlayout); mp_exec_model = new MacroEditorExecutionModel (this); mp_text = new MacroEditorTextWidget (this); mp_side_panel = new MacroEditorSidePanel (this, mp_text, mp_exec_model); hlayout->addWidget (mp_side_panel); hlayout->addWidget (mp_text); mp_text->setWordWrapMode(QTextOption::NoWrap); #if QT_VERSION >= 0x60000 mp_text->setTabStopDistance (m_ntab * QFontMetrics (mp_text->font ()).horizontalAdvance (QString::fromUtf8 ("x"))); #else mp_text->setTabStopWidth (m_ntab * QFontMetrics (mp_text->font ()).width (QString::fromUtf8 ("x"))); #endif m_is_modified = false; connect (mp_text, SIGNAL (textChanged ()), this, SLOT (text_changed ())); connect (mp_text, SIGNAL (cursorPositionChanged ()), this, SLOT (cursor_position_changed ())); connect (mp_text->horizontalScrollBar (), SIGNAL (valueChanged (int)), this, SLOT (hide_completer ())); connect (mp_text->verticalScrollBar (), SIGNAL (valueChanged (int)), this, SLOT (hide_completer ())); connect (mp_exec_model, SIGNAL (breakpoints_changed ()), this, SLOT (breakpoints_changed ())); connect (mp_exec_model, SIGNAL (current_line_changed ()), this, SLOT (current_line_changed ())); connect (mp_exec_model, SIGNAL (run_mode_changed ()), this, SLOT (run_mode_changed ())); mp_text->installEventFilter (this); mp_completer_popup = new QWidget (window (), Qt::ToolTip); mp_completer_popup->setWindowModality (Qt::NonModal); QHBoxLayout *ly = new QHBoxLayout (mp_completer_popup); ly->setContentsMargins (0, 0, 0, 0); mp_completer_list = new QListWidget (mp_completer_popup); ly->addWidget (mp_completer_list); mp_completer_popup->hide (); mp_completer_timer = new QTimer (this); mp_completer_timer->setInterval (1000); mp_completer_timer->setSingleShot (true); connect (mp_completer_timer, SIGNAL (timeout ()), this, SLOT (completer_timer ())); } void MacroEditorPage::update () { if (mp_macro) { QString mt = tl::to_qstring (mp_macro->text ()); QString et = mp_text->toPlainText (); if (mt != et) { // Leave trailing lines as far as they are identical - that way we deal with the // "header changed" case gracefully and don't destroy breakpoints if we change macro properties int nm = mt.size (); int ne = et.size (); while (nm > 0 && ne > 0 && mt [nm - 1] == et [ne - 1]) { --nm; --ne; } QTextCursor c = mp_text->textCursor (); QTextCursor cursor (mp_text->document ()); cursor.beginEditBlock (); cursor.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor, int (ne)); cursor.removeSelectedText (); cursor.insertText (mt.left (int (nm))); cursor.endEditBlock (); mp_text->setTextCursor (c); } } } void MacroEditorPage::set_debugging_on (bool on) { mp_side_panel->set_debugging_on (on); } void MacroEditorPage::commit () { if (mp_macro) { mp_macro->set_text (tl::to_string (mp_text->toPlainText ())); } } void MacroEditorPage::current_line_changed () { if (mp_exec_model->current_line () >= 0) { goto_line (mp_exec_model->current_line ()); } emit edit_trace (false); update_extra_selections (); } void MacroEditorPage::run_mode_changed () { // this prevents recursion when the following lines trigger anything that routes through the interpreter bool bl = mp_exec_model->blockSignals (true); try { if (mp_exec_model->run_mode ()) { set_error_line (0); } mp_text->setReadOnly (! mp_macro || mp_macro->is_readonly () || mp_exec_model->run_mode ()); update_extra_selections (); } catch (...) { // .. ignore exceptions here .. } mp_exec_model->blockSignals (bl); } void MacroEditorPage::breakpoints_changed () { // update the breakpoint's block list m_breakpoints.clear (); if (! mp_exec_model->breakpoints ().empty ()) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { if (mp_exec_model->breakpoints ().find (firstLineNumber (b) + 1) != mp_exec_model->breakpoints ().end ()) { m_breakpoints.insert (b); // Right now, the user data is just used as a flag for a breakpoint b.setUserData (new QTextBlockUserData ()); } else { // Right now, the user data is just used as a flag for a breakpoint b.setUserData (0); } } } } static bool valid_element (const SyntaxHighlighterElement &e) { return e.basic_attribute_id != lay::dsComment && e.basic_attribute_id != lay::dsString; } QTextCursor MacroEditorPage::get_completer_cursor (int &pos0, int &pos) { QTextCursor c = mp_text->textCursor (); if (c.selectionStart () != c.selectionEnd ()) { return QTextCursor (); } pos = c.anchor (); c.select (QTextCursor::WordUnderCursor); pos0 = c.selectionStart (); if (pos0 >= pos) { // if there is no word before, move to left to catch one c = mp_text->textCursor (); c.movePosition (QTextCursor::WordLeft, QTextCursor::KeepAnchor); pos = c.anchor (); pos0 = c.selectionStart (); } if (pos0 >= pos) { return QTextCursor (); } else { return c; } } void MacroEditorPage::complete () { int pos0 = 0, pos = 0; QTextCursor c = get_completer_cursor (pos0, pos); if (c.isNull ()) { return; } if (mp_completer_list->currentItem ()) { QString s = mp_completer_list->currentItem ()->text (); c.insertText (s); } } void MacroEditorPage::fill_completer_list () { int pos0 = 0, pos = 0; QTextCursor c = get_completer_cursor (pos0, pos); if (c.isNull ()) { return; } QString ssel = c.selectedText (); QString s = ssel.mid (0, pos - pos0); if (s.length () == 0 || (! s[0].isLetter () && s[0].toLatin1 () != '_')) { return; // not a word } QString text = mp_text->toPlainText (); std::set words; int i = -1; while (true) { i = text.indexOf (s, i + 1); if (i < 0) { // no more occurance break; } if (i == pos0) { // same position than we are at currently continue; } if (i > 0 && (text [i - 1].isLetterOrNumber () || text [i - 1].toLatin1 () == '_')) { // not at the beginning of the word continue; } QString::iterator c = text.begin () + i; QString w; while (c->isLetterOrNumber () || c->toLatin1 () == '_') { w += *c; ++c; } if (w == ssel) { // the selected word is present already - assume it's the right one words.clear (); break; } else if (! w.isEmpty () && w != s) { words.insert (w); } } for (std::set::const_iterator w = words.begin (); w != words.end (); ++w) { mp_completer_list->addItem (*w); } } void MacroEditorPage::completer_timer () { if (! mp_text->hasFocus ()) { return; } mp_completer_list->clear (); fill_completer_list (); if (mp_completer_list->count () > 0) { mp_completer_list->setCurrentRow (0); QTextCursor c = mp_text->textCursor (); c.clearSelection (); QRect r = mp_text->cursorRect (c); QPoint pos = mp_text->mapToGlobal (r.bottomLeft ()); QSize sz = mp_completer_list->sizeHint (); QFontMetrics fm (mp_completer_list->font ()); mp_completer_popup->setGeometry (pos.x (), pos.y () + r.height () / 3, sz.width (), 4 + 4 * fm.height () /*sz.height ()*/); mp_completer_popup->show (); mp_text->setFocus (); } else { mp_completer_popup->hide (); } } void MacroEditorPage::hide_completer () { mp_completer_popup->hide (); } void MacroEditorPage::cursor_position_changed () { if (m_ignore_cursor_changed_event) { return; } mp_completer_popup->hide (); mp_completer_timer->stop (); mp_completer_timer->start (); QTextCursor cursor = mp_text->textCursor (); m_edit_cursor = cursor; // prepare a format for the bracket highlights QTextCharFormat fmt; fmt.setForeground (Qt::red); fmt.setBackground (QColor (224, 224, 224)); QFont f = fmt.font (); f.setBold (true); fmt.setFont (f); QTextBlock b = cursor.block (); SyntaxHighlighterUserData *user_data = dynamic_cast (b.userData()); if (user_data) { // Look for matching brackets and highlight the other one // NOTE: the whole scheme is somewhat more complex than it could be. It's // based on the syntax highlighter elements and we confine ourselves to // elements not being comment or string. So we need to iterate over elements // and over characters inside these elements. #if QT_VERSION < 0x40700 size_t pos = size_t (cursor.position() - cursor.block().position()); #else size_t pos = size_t (cursor.positionInBlock ()); #endif std::vector::const_iterator e; for (e = user_data->elements ().begin (); e != user_data->elements ().end (); ++e) { if (e->start_offset <= pos && e->start_offset + e->length > pos) { break; } } static const QString open_rbracket (QString::fromUtf8 ("(")); static const QString open_sqbracket (QString::fromUtf8 ("[")); static const QString open_cbracket (QString::fromUtf8 ("{")); static const QString close_rbracket (QString::fromUtf8 (")")); static const QString close_sqbracket (QString::fromUtf8 ("]")); static const QString close_cbracket (QString::fromUtf8 ("}")); bool forward = false, backward = false; if (e != user_data->elements ().end () && valid_element (*e)) { QString t = b.text ().mid (int (pos), 1); forward = (t == open_rbracket || t == open_sqbracket || t == open_cbracket); } if (e != user_data->elements ().begin () && e[-1].start_offset + e[-1].length >= pos && valid_element (e[-1])) { QString t = b.text ().mid (int (pos) - 1, 1); backward = (t == close_rbracket || t == close_sqbracket || t == close_cbracket); } if (forward) { backward = false; } else if (backward) { --e; } if (forward || backward) { std::vector bs; int found = -1; while (true) { QString t = b.text ().mid (int (e->start_offset), int (e->length)).trimmed (); if (valid_element (*e)) { if (forward) { for (int p = 0; p != t.size () && found < 0; ++p) { if (p + e->start_offset >= pos) { QString c = t.mid (p, 1); if (c == open_rbracket) { bs.push_back (close_rbracket); } else if (c == open_cbracket) { bs.push_back (close_cbracket); } else if (c == open_sqbracket) { bs.push_back (close_sqbracket); } else if (c == bs.back ()) { bs.pop_back (); if (bs.empty ()) { found = p + int (e->start_offset); } } } } } else if (backward) { for (int p = t.size (); p > 0 && found < 0; ) { --p; if (p + int (e->start_offset) < int (pos)) { QString c = t.mid (p, 1); if (c == close_rbracket) { bs.push_back (open_rbracket); } else if (c == close_cbracket) { bs.push_back (open_cbracket); } else if (c == close_sqbracket) { bs.push_back (open_sqbracket); } else if (c == bs.back ()) { bs.pop_back (); if (bs.empty ()) { found = p + int (e->start_offset); } } } } } } if (found >= 0) { break; } if (forward) { ++e; if (e == user_data->elements ().end ()) { break; } } else { if (e == user_data->elements ().begin ()) { break; } --e; } } if (found >= 0) { QList extra_selections = mp_text->extraSelections (); for (QList::iterator i = extra_selections.begin (); i != extra_selections.end (); ) { if (i->format == fmt) { i = extra_selections.erase (i); } else { ++i; } } QTextEdit::ExtraSelection es; es.format = fmt; es.cursor = QTextCursor (b); es.cursor.setPosition (b.position () + found); es.cursor.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); extra_selections.push_back (es); es.cursor = QTextCursor (b); es.cursor.setPosition (b.position () + found); es.cursor.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); extra_selections.push_back (es); mp_text->setExtraSelections (extra_selections); } } } } void MacroEditorPage::text_changed () { m_is_modified = true; // update the breakpoint's line numbers std::set bl; for (std::set::const_iterator b = m_breakpoints.begin (); b != m_breakpoints.end (); ++b) { if (b->isValid () && b->userData () != 0) { bl.insert (firstLineNumber (*b) + 1); } } mp_exec_model->set_breakpoints (bl); emit edit_trace (true); } void MacroEditorPage::set_ntab (int n) { if (n != m_ntab) { m_ntab = n; #if QT_VERSION >= 0x60000 mp_text->setTabStopDistance (m_ntab * QFontMetrics (mp_text->font ()).horizontalAdvance (QString::fromUtf8 ("x"))); #else mp_text->setTabStopWidth (m_ntab * QFontMetrics (mp_text->font ()).width (QString::fromUtf8 ("x"))); #endif } } void MacroEditorPage::set_nindent (int n) { m_nindent = n; } void MacroEditorPage::set_font (const std::string &family, int size) { QFont f = font (); if (! family.empty ()) { f.setFamily (tl::to_qstring (family)); } else { f.setFamily (lay::monospace_font ().family ()); } f.setFixedPitch (true); if (size > 0) { f.setPointSize (size); } mp_text->setFont (f); } void MacroEditorPage::apply_attributes () { if (mp_highlighter) { mp_highlighter->rehighlight (); } } void MacroEditorPage::connect_macro (lym::Macro *macro) { if (mp_macro != macro) { if (mp_highlighter) { delete mp_highlighter; mp_highlighter = 0; } if (mp_macro) { disconnect (mp_macro, SIGNAL (changed ()), this, SLOT (update ())); } mp_macro = macro; if (mp_macro) { m_path = mp_macro->path (); connect (mp_macro, SIGNAL (changed ()), this, SLOT (update ())); lym::Macro::Interpreter lang = macro->interpreter (); if (lang == lym::Macro::DSLInterpreter) { lang = lym::MacroInterpreter::debugger_scheme (macro->dsl_interpreter ()); } mp_exec_model->set_interpreter (lang); mp_text->blockSignals (true); mp_text->setPlainText (tl::to_qstring (mp_macro->text ())); mp_text->setReadOnly (macro->is_readonly ()); mp_readonly_label->setVisible (macro->is_readonly ()); mp_highlighter = mp_highlighters->highlighter_for (mp_text, mp_macro->interpreter (), mp_macro->dsl_interpreter (), false); if (mp_highlighter) { mp_highlighter->setDocument (mp_text->document ()); } mp_text->blockSignals (false); m_is_modified = false; } else { mp_exec_model->set_interpreter (lym::Macro::None); } mp_side_panel->set_watermark (mp_macro ? tl::to_qstring (mp_macro->interpreter_name ()) : QString ()); } } void MacroEditorPage::find_reset () { m_ignore_cursor_changed_event = true; mp_text->setTextCursor (m_edit_cursor); m_ignore_cursor_changed_event = false; } bool MacroEditorPage::find_prev () { update_extra_selections (); if (m_current_search == QRegExp ()) { return false; } QTextCursor c = mp_text->textCursor (); bool first = true; for (QTextBlock b = c.block (); true; ) { int o = (first ? c.position () - b.position () : -1); first = false; int i = -1; int p = 0; while (true) { int ii = m_current_search.indexIn (b.text (), p); if (ii >= 0 && (o < 0 || ii < o)) { i = ii; p = ii + 1; } else { break; } } if (i >= 0) { QTextCursor newc (b); newc.setPosition (i + b.position ()); m_ignore_cursor_changed_event = true; mp_text->setTextCursor (newc); m_ignore_cursor_changed_event = false; return true; } if (b == mp_text->document ()->begin()) { b = mp_text->document ()->end (); } b = b.previous (); if (b == c.block ()) { break; } } return false; } bool MacroEditorPage::find_next () { update_extra_selections (); if (m_current_search == QRegExp ()) { return false; } QTextCursor c = mp_text->textCursor (); if (c.isNull ()) { c = QTextCursor (mp_text->document ()); mp_text->setTextCursor (c); } bool first = true; for (QTextBlock b = c.block (); true; ) { int o = (first ? std::max (0, c.position () + 1 - b.position ()) : 0); first = false; int i = m_current_search.indexIn (b.text (), o); if (i >= 0) { QTextCursor newc (b); newc.setPosition (i + b.position ()); m_ignore_cursor_changed_event = true; mp_text->setTextCursor (newc); m_ignore_cursor_changed_event = false; emit edit_trace (false); return true; } b = b.next (); if (b == mp_text->document ()->end()) { b = mp_text->document ()->begin (); } if (b == c.block ()) { break; } } return false; } bool MacroEditorPage::select_match_here () { if (m_current_search == QRegExp ()) { return false; } QTextCursor c = mp_text->textCursor (); if (c.isNull ()) { return false; } if (c.hasSelection ()) { return true; } QTextBlock b = c.block (); int pos = c.position () - b.position (); int i = m_current_search.indexIn (b.text (), pos); if (i == pos) { QTextCursor newc (b); newc.setPosition (i + b.position () + m_current_search.matchedLength ()); newc.setPosition (i + b.position (), QTextCursor::KeepAnchor); m_ignore_cursor_changed_event = true; mp_text->setTextCursor (newc); m_ignore_cursor_changed_event = false; emit edit_trace (false); return true; } else { return false; } } void MacroEditorPage::set_editor_focus () { mp_text->setFocus (Qt::OtherFocusReason); } static QString interpolate_string (const QString &replace, const QRegExp &re) { if (re.patternSyntax () == QRegExp::FixedString) { return replace; } else { QString r = replace; // In some older Qt versions, capturedTexts is not const: QStringList ct = const_cast (re).capturedTexts (); // TODO: this is simple implementation for the \\ sequence which also implies that "\ " is an escape sequence r.replace (QString::fromUtf8 ("\\ "), QString::fromUtf8 (" ")); r.replace (QString::fromUtf8 ("\\\\"), QString::fromUtf8 ("\\ ")); for (int i = ct.size () - 1; i >= 0; --i) { r.replace (QString::fromUtf8 ("\\") + QString::number (i), ct [i]); } r.replace (QString::fromUtf8 ("\\ "), QString::fromUtf8 ("\\")); return r; } } void MacroEditorPage::replace_and_find_next (const QString &replace) { if (! mp_macro || mp_macro->is_readonly ()) { return; } if (select_match_here ()) { replace_in_selection (replace, true); } find_next (); } void MacroEditorPage::replace_all (const QString &replace) { if (! mp_macro || mp_macro->is_readonly ()) { return; } replace_in_selection (replace, false); } void MacroEditorPage::replace_in_selection (const QString &replace, bool first) { const QTextDocument *doc = mp_text->document (); QTextBlock bs = doc->begin (), be = doc->end (); int ps = 0; int pe = be.length (); QTextCursor c = mp_text->textCursor (); bool has_selection = c.hasSelection (); bool anchor_at_end = false; if (has_selection) { anchor_at_end = (c.selectionStart () == c.position ()); ps = c.selectionStart (); pe = c.selectionEnd (); bs = mp_text->document ()->findBlock (ps); be = mp_text->document ()->findBlock (pe); } else if (first) { // don't replace first entry without selection return; } ps -= bs.position (); pe -= be.position (); c.beginEditBlock (); bool done = false; for (QTextBlock b = bs; ; b = b.next()) { int o = 0; while (!done) { bool substitute = false; int i = m_current_search.indexIn (b.text (), o); if (i < 0) { break; } else if (m_current_search.matchedLength () == 0) { break; // avoid an infinite loop } else if (b == bs && i < ps) { // ignore } else if (b == be && i + m_current_search.matchedLength () > pe) { // ignore done = true; } else { substitute = true; } if (substitute) { QString r = interpolate_string (replace, m_current_search); c.setPosition (i + b.position () + m_current_search.matchedLength ()); c.setPosition (i + b.position (), QTextCursor::KeepAnchor); c.insertText (r); o = i + r.size (); if (first) { has_selection = false; done = true; } else if (b == be) { pe += int (r.size ()) - int (m_current_search.matchedLength ()); } } else { o = i + m_current_search.matchedLength (); } } if (b == be || done) { break; } } if (has_selection) { // restore selection which might have changed due to insert c.setPosition (anchor_at_end ? be.position () + pe : bs.position () + ps); c.setPosition (!anchor_at_end ? be.position () + pe : bs.position () + ps, QTextCursor::KeepAnchor); } mp_text->setTextCursor (c); c.endEditBlock (); } void MacroEditorPage::set_search (const QRegExp &text) { m_current_search = text; m_error_line = -1; update_extra_selections (); } void MacroEditorPage::set_error_line (int line) { m_error_line = line - 1; goto_line (line); update_extra_selections (); } void MacroEditorPage::goto_line (int line) { if (line > 0) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { if (firstLineNumber (b) + 1 == line) { mp_text->setTextCursor (QTextCursor (b)); mp_text->ensureCursorVisible (); break; } } } } void MacroEditorPage::goto_position (int line, int pos) { if (line > 0) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { if (firstLineNumber (b) + 1 == line) { QTextCursor cursor (b); cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, pos); mp_text->setTextCursor (cursor); mp_text->ensureCursorVisible (); break; } } } } void MacroEditorPage::update_extra_selections () { QList extra_selections; if (m_error_line >= 0) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { if (firstLineNumber (b) == m_error_line) { QTextEdit::ExtraSelection es; es.cursor = QTextCursor (b); es.cursor.select (QTextCursor::LineUnderCursor); es.format.setBackground (QColor (Qt::red).lighter ()); extra_selections.push_back (es); break; } } } else if (mp_exec_model->run_mode () && mp_exec_model->current_line () >= 0) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { if (firstLineNumber (b) == mp_exec_model->current_line () - 1) { QTextEdit::ExtraSelection es; es.cursor = QTextCursor (b); es.cursor.select (QTextCursor::LineUnderCursor); es.format.setBackground (QColor (Qt::lightGray)); extra_selections.push_back (es); break; } } } else if (m_current_search != QRegExp ()) { const QTextDocument *doc = mp_text->document (); for (QTextBlock b = doc->begin(); b != doc->end(); b = b.next()) { QString t = b.text (); int o = 0; int i = 0; while ((i = m_current_search.indexIn (t, o)) >= 0) { int l = m_current_search.matchedLength (); if (l > 0) { o = i + l; QTextEdit::ExtraSelection es; es.cursor = QTextCursor (b); es.cursor.setPosition (b.position () + i); es.cursor.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor, l); es.format.setBackground (Qt::yellow); extra_selections.push_back (es); } else { // avoid endless loop on empty search break; } } } } mp_text->setExtraSelections (extra_selections); } int MacroEditorPage::current_line () const { return firstLineNumber (mp_text->textCursor ().block ()) + 1; } int MacroEditorPage::current_pos () const { return mp_text->textCursor ().position () - mp_text->textCursor ().block ().position (); } bool MacroEditorPage::has_multi_block_selection () const { QTextCursor c = mp_text->textCursor (); if (c.selectionStart () != c.selectionEnd ()) { QTextBlock s = mp_text->document ()->findBlock (c.selectionStart ()); QTextBlock e = mp_text->document ()->findBlock (c.selectionEnd ()); return e != s; } else { return false; } } bool MacroEditorPage::tab_key_pressed () { if (mp_text->isReadOnly ()) { return false; } QTextBlock bs, be; bool adjust_end = false; bool indent = false; if (mp_text->textCursor ().hasSelection ()) { bs = mp_text->document ()->findBlock (mp_text->textCursor ().selectionStart ()); be = mp_text->document ()->findBlock (mp_text->textCursor ().selectionEnd ()); if (be != bs) { indent = true; QTextCursor se (mp_text->document ()); se.setPosition (mp_text->textCursor ().selectionEnd ()); if (se.atBlockStart ()) { be = be.previous (); adjust_end = true; } } } if (indent) { // tab out QTextCursor c (mp_text->document ()); c.setPosition (bs.position ()); c.beginEditBlock (); for (QTextBlock b = bs; ; b = b.next()) { c.setPosition (b.position ()); QString text = b.text (); bool has_tabs = false; int p = 0; int i = 0; for (; i < text.length (); ++i) { if (text [i] == QChar::fromLatin1 (' ')) { ++p; } else if (text [i] == QChar::fromLatin1 ('\t')) { p = (p - p % m_ntab) + m_ntab; has_tabs = true; } else { break; } } if (has_tabs) { for ( ; i > 0; --i) { c.deleteChar (); } c.insertText (QString (m_nindent + p, QChar::fromLatin1 (' '))); } else { c.insertText (QString (m_nindent, QChar::fromLatin1 (' '))); } if (b == be) { break; } } c.endEditBlock (); c.setPosition (bs.position ()); if (adjust_end) { c.setPosition (be.next ().position (), QTextCursor::KeepAnchor); } else { c.setPosition (be.position () + be.text ().length (), QTextCursor::KeepAnchor); } mp_text->setTextCursor (c); } else { QTextCursor c = mp_text->textCursor (); QString text = c.block ().text (); int col = c.position () - c.block ().position (); int p = 0; for (int i = 0; i < text.length () && i < col; ++i) { if (text [i] == QChar::fromLatin1 ('\t')) { p = (p - p % m_ntab) + m_ntab; } else { ++p; } } c.insertText (QString (m_nindent - p % m_nindent, QChar::fromLatin1 (' '))); mp_text->setTextCursor (c); } return true; } bool MacroEditorPage::back_tab_key_pressed () { if (!mp_text->textCursor ().hasSelection () || mp_text->isReadOnly ()) { return false; } // tab in QTextBlock bs = mp_text->document ()->findBlock (mp_text->textCursor ().selectionStart ()); QTextBlock be = mp_text->document ()->findBlock (mp_text->textCursor ().selectionEnd ()); bool adjust_end = false; if (be != bs) { QTextCursor se (mp_text->document ()); se.setPosition (mp_text->textCursor ().selectionEnd ()); if (se.atBlockStart ()) { be = be.previous (); adjust_end = true; } } QTextCursor c (mp_text->document ()); c.setPosition (bs.position ()); c.beginEditBlock (); for (QTextBlock b = bs; ; b = b.next()) { c.setPosition (b.position ()); QString text = b.text (); int n = m_nindent; int p = 0; for (int i = 0; i < text.length () && n > 0; ++i) { if (text [i] == QChar::fromLatin1 (' ')) { ++p; --n; c.deleteChar (); } else if (text [i] == QChar::fromLatin1 ('\t')) { c.deleteChar (); int pp = p; p = (p - p % m_ntab) + m_ntab; if (p - pp >= n) { if (p - pp > n) { c.insertText (QString (p - pp - n, QChar::fromLatin1 (' '))); } n = 0; } else { n -= p - pp; } } else { break; } } if (b == be) { break; } } c.endEditBlock (); c.setPosition (bs.position ()); if (adjust_end) { c.setPosition (be.next ().position (), QTextCursor::KeepAnchor); } else { c.setPosition (be.position () + be.text ().length (), QTextCursor::KeepAnchor); } mp_text->setTextCursor (c); return true; } bool MacroEditorPage::backspace_pressed () { if (mp_text->textCursor ().hasSelection () || mp_text->isReadOnly()) { return false; } QTextCursor c = mp_text->textCursor (); QString text = c.block ().text (); int col = c.position () - c.block ().position (); if (col > 0) { int p = 0; bool only_space_before = true; for (int i = 0; i < text.length () && i < col; ++i) { if (text [i] == QChar::fromLatin1 ('\t')) { p = (p - p % m_ntab) + m_ntab; } else if (text [i] == QChar::fromLatin1 (' ')) { ++p; } else { only_space_before = false; } } if (only_space_before) { for (int i = 0; i < col; ++i) { c.deletePreviousChar (); } c.insertText (QString (std::max (0, ((p - 1) / m_nindent) * m_nindent), QChar::fromLatin1 (' '))); mp_text->setTextCursor (c); return true; } } return false; } bool MacroEditorPage::return_pressed () { if (mp_text->isReadOnly ()) { return false; } // Implement auto-indent on return QTextCursor c = mp_text->textCursor (); QTextBlock b = c.block (); c.insertBlock (); QString l; if (b.isValid ()) { QString text = b.text (); for (int i = 0; i < text.length (); ++i) { if (text [i] == QChar::fromLatin1 ('\t') || text [i] == QChar::fromLatin1 (' ')) { l += text [i]; } else { break; } } } c.insertText (l); mp_text->setTextCursor (c); return true; } static bool is_tab_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Tab && (ke->modifiers () & Qt::ShiftModifier) == 0; } static bool is_backtab_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Backtab || (ke->key () == Qt::Key_Tab && (ke->modifiers () & Qt::ShiftModifier) != 0); } static bool is_backspace_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Backspace; } static bool is_escape_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Escape; } static bool is_return_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Return; } static bool is_help_key (QKeyEvent *ke) { return ke->key () == Qt::Key_F1; } static bool is_find_next_key (QKeyEvent *ke) { return ke->key () == Qt::Key_F3; } static bool is_find_key (QKeyEvent *ke) { return ke->key () == Qt::Key_F && (ke->modifiers () & Qt::ControlModifier) != 0; } static bool is_find_backwards_key (QKeyEvent *ke) { return ke->key () == Qt::Key_F && (ke->modifiers () & Qt::ControlModifier) != 0 && (ke->modifiers () & Qt::ShiftModifier) != 0; } static bool is_up_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Up; } static bool is_down_key (QKeyEvent *ke) { return ke->key () == Qt::Key_Down; } static bool is_any_known_key (QKeyEvent *ke) { return is_tab_key (ke) || is_backtab_key (ke) || is_backspace_key (ke) || is_escape_key (ke) || is_return_key (ke) || is_help_key (ke) || is_find_next_key (ke) || is_find_key (ke) || is_find_backwards_key (ke) || is_up_key (ke) || is_down_key (ke); } bool MacroEditorPage::eventFilter (QObject *watched, QEvent *event) { if (watched == mp_text) { if (event->type () == QEvent::ShortcutOverride) { // override shortcuts if the collide with keys we accept ourselves QKeyEvent *ke = dynamic_cast (event); if (! ke) { return false; // should not happen } if (is_any_known_key (ke)) { event->accept (); return true; } } else if (event->type () == QEvent::FocusOut) { hide_completer (); return true; } else if (event->type () == QEvent::KeyPress) { m_error_line = -1; mp_text->setExtraSelections (QList ()); QKeyEvent *ke = dynamic_cast (event); if (! ke) { return false; // should not happen } if (is_tab_key (ke)) { if (mp_completer_popup->isVisible ()) { complete (); return true; } else { return tab_key_pressed (); } } else if (is_backtab_key (ke)) { return back_tab_key_pressed (); } else if (is_backspace_key (ke)) { return backspace_pressed (); } else if (is_escape_key (ke)) { // Handle Esc to return to the before-find position and clear the selection or to hide popup if (mp_completer_popup->isVisible ()) { mp_completer_popup->hide (); } else { find_reset (); QTextCursor c = mp_text->textCursor (); c.clearSelection (); mp_text->setTextCursor (c); } return true; } else if (is_return_key (ke)) { if (mp_completer_popup->isVisible ()) { complete (); return true; } else { return return_pressed (); } } else if (is_help_key (ke)) { QTextCursor c = mp_text->textCursor (); if (c.selectionStart () == c.selectionEnd ()) { c.select (QTextCursor::WordUnderCursor); } emit help_requested (c.selectedText ()); return true; } else if (mp_completer_popup->isVisible () && (is_up_key (ke) || is_down_key (ke))) { QApplication::sendEvent (mp_completer_list, event); return true; } else if (is_find_key (ke) || is_find_backwards_key (ke)) { bool prev = is_find_backwards_key (ke); QTextCursor c = mp_text->textCursor (); if (c.selectionStart () != c.selectionEnd ()) { QTextBlock s = mp_text->document ()->findBlock (c.selectionStart ()); QTextBlock e = mp_text->document ()->findBlock (c.selectionEnd ()); if (e == s) { emit search_requested (c.selectedText (), prev); } else { emit search_requested (QString (), prev); } } else { emit search_requested (QString (), prev); } return true; } else if (is_find_next_key (ke)) { // Jump to the next occurrence of the search string if (m_current_search != QRegExp ()) { if ((ke->modifiers () & Qt::ShiftModifier) != 0) { find_prev (); } else { find_next (); } update_extra_selections (); } return true; } } } 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 (); } } }