Mercurial > octave
view libgui/src/command-widget.cc @ 31723:9ab31fe294c1 stable
don't emit signals for invalid objects in interpreter callbacks (bug #62863)
Some interpreter_event callbacks emit Qt signals. The callback is
queued by the Octave interpreter and may be executed at
some arbitrary time. It is possible that the object for which the
signal is intended to be executed may be deleted before the callback
executes. If so, then the callback should not emit the signal. In
some cases, execution of the entire callback function may be skipped.
In others, we still need to perform actions in the interpreter but
must skip the signals for Qt objects that no longer exist.
* command-widget.cc, file-editor-tab.cc, file-editor.cc,
octave-qscintilla.cc, main-window.cc, set-path-model.cc,
variable-editor-model.cc, variable-editor.cc: In all interpreter_event
callback functions that emit Qt signals, use a QPointer object to
protect the "this" pointer and check that it is still valid when the
callback executes.
author | John W. Eaton <jwe@octave.org> |
---|---|
date | Wed, 11 Jan 2023 12:16:25 -0500 |
parents | 597f3ee61a48 |
children | 340d016c2edf |
line wrap: on
line source
//////////////////////////////////////////////////////////////////////// // // Copyright (C) 2021-2023 The Octave Project Developers // // See the file COPYRIGHT.md in the top-level directory of this // distribution or <https://octave.org/copyright/>. // // This file is part of Octave. // // Octave is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Octave is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Octave; see the file COPYING. If not, see // <https://www.gnu.org/licenses/>. // //////////////////////////////////////////////////////////////////////// #if defined (HAVE_CONFIG_H) # include "config.h" #endif #if defined (HAVE_QSCINTILLA) #include <QGroupBox> #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> #include <QPushButton> #include <QTextEdit> #include <QTextBlock> #include <QVBoxLayout> #include "command-widget.h" #include "cmd-edit.h" #include "event-manager.h" #include "gui-preferences-cs.h" #include "gui-preferences-global.h" #include "gui-utils.h" #include "input.h" #include "interpreter.h" OCTAVE_BEGIN_NAMESPACE(octave) command_widget::command_widget (base_qobject& oct_qobj, QWidget *p) : QWidget (p), m_incomplete_parse (false), m_prompt (QString ()), m_console (new console (this, oct_qobj)) { QPushButton *pause_button = new QPushButton (tr("Pause"), this); QPushButton *stop_button = new QPushButton (tr("Stop"), this); QPushButton *resume_button = new QPushButton (tr("Continue"), this); QGroupBox *input_group_box = new QGroupBox (); QHBoxLayout *input_layout = new QHBoxLayout; input_layout->addWidget (pause_button); input_layout->addWidget (stop_button); input_layout->addWidget (resume_button); input_group_box->setLayout (input_layout); QVBoxLayout *main_layout = new QVBoxLayout (); main_layout->addWidget (m_console); main_layout->addWidget (input_group_box); setLayout (main_layout); setFocusProxy (m_console); connect (pause_button, &QPushButton::clicked, this, &command_widget::interpreter_pause); connect (resume_button, &QPushButton::clicked, this, &command_widget::interpreter_resume); connect (stop_button, &QPushButton::clicked, this, &command_widget::interpreter_stop); connect (this, &command_widget::new_command_line_signal, m_console, &console::new_command_line); insert_interpreter_output ("\n\n Welcome to Octave\n\n"); } void command_widget::init_command_prompt () { // The interpreter_event callback function below emits a signal. // Because we don't control when that happens, use a guarded pointer // so that the callback can abort if this object is no longer valid. QPointer<command_widget> this_cw (this); emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD // We can skip the entire callback function because it does not // make any changes to the interpreter state. if (this_cw.isNull ()) return; event_manager& evmgr = interp.get_event_manager (); input_system& input_sys = interp.get_input_system (); std::string prompt = input_sys.PS1 (); evmgr.update_prompt (command_editor::decode_prompt_string (prompt)); emit new_command_line_signal (); }); } void command_widget::update_prompt (const QString& prompt) { m_prompt = prompt; } QString command_widget::prompt () { return m_prompt; } void command_widget::insert_interpreter_output (const QString& msg) { m_console->append (msg); } void command_widget::process_input_line (const QString& input_line) { // The interpreter_event callback function below emits a signal. // Because we don't control when that happens, use a guarded pointer // so that the callback can abort if this object is no longer valid. QPointer<command_widget> this_cw (this); emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD // If THIS_CW is no longer valid, we still want to parse and // execute INPUT_LINE but we can't emit the signals associated // with THIS_CW. interp.parse_and_execute (input_line.toStdString (), m_incomplete_parse); if (this_cw.isNull ()) return; event_manager& evmgr = interp.get_event_manager (); input_system& input_sys = interp.get_input_system (); std::string prompt = m_incomplete_parse ? input_sys.PS2 () : input_sys.PS1 (); evmgr.update_prompt (command_editor::decode_prompt_string (prompt)); emit new_command_line_signal (); }); } void command_widget::notice_settings (const gui_settings *settings) { // Set terminal font: QFont term_font = QFont (); term_font.setStyleHint (QFont::TypeWriter); QString default_font = settings->value (global_mono_font).toString (); term_font.setFamily (settings->value (cs_font.key, default_font).toString ()); term_font.setPointSize (settings->value (cs_font_size).toInt ()); m_console->setFont (term_font); // Colors int mode = settings->value (cs_color_mode).toInt (); QColor fgc = settings->color_value (cs_colors[0], mode); QColor bgc = settings->color_value (cs_colors[1], mode); m_console->setStyleSheet (QString ("color: %1; background-color:%2;") .arg (fgc.name ()).arg (bgc.name ())); } // The console itself using QScintilla. // This implementation is partly based on the basic concept of // "qpconsole" as proposed by user "DerManu" in the Qt-forum thread // https://forum.qt.io/topic/28765/command-terminal-using-qtextedit console::console (command_widget *p, base_qobject&) : QsciScintilla (p), m_command_position (-1), m_cursor_position (0), m_text_changed (false), m_command_widget (p), m_last_key_string (QString ()) { setMargins (0); setWrapMode (QsciScintilla::WrapWord); connect (this, SIGNAL (cursorPositionChanged (int, int)), this, SLOT (cursor_position_changed (int, int))); connect (this, SIGNAL (textChanged (void)), this, SLOT (text_changed (void))); connect (this, SIGNAL (modificationAttempted (void)), this, SLOT (move_cursor_to_end (void))); } // Prepare a new command line with the current prompt void console::new_command_line (const QString& command) { if (! text (lines () -1).isEmpty ()) append ("\n"); append_string (m_command_widget->prompt ()); int line, index; getCursorPosition (&line,&index); m_command_position = positionFromLineIndex (line, index); append_string (command); } // Accept the current command line (or block) void console::accept_command_line () { QString input_line = text (lines () - 1); if (input_line.startsWith (m_command_widget->prompt ())) input_line.remove(0, m_command_widget->prompt ().length ()); input_line = input_line.trimmed (); append_string ("\n"); if (input_line.isEmpty ()) new_command_line (); else m_command_widget->process_input_line (input_line); } // Execute a command void console::execute_command (const QString& command) { if (command.trimmed ().isEmpty ()) return; new_command_line (command); accept_command_line (); } // Append a string and update the curdor püosition void console::append_string (const QString& string) { setReadOnly (false); append (string); int line, index; lineIndexFromPosition (text ().length (), &line, &index); setCursorPosition (line, index); } // Cursor position changed: Are we in the command line or not? void console::cursor_position_changed (int line, int col) { m_cursor_position = positionFromLineIndex (line, col); if (m_cursor_position < m_command_position) { // We are in the read only area if (m_text_changed && (m_cursor_position == m_command_position - 1)) { setReadOnly (false); insert (m_command_widget->prompt ().right (1)); // And here we have tried to remove the prompt by Backspace setCursorPosition (line+1, col); } setReadOnly (true); } else setReadOnly (false); // Writable area m_text_changed = false; } // User attempted to type on read only mode: move cursor at end and allow // editing void console::move_cursor_to_end (void) { if ((! m_last_key_string.isEmpty ()) && (m_last_key_string.at (0).isPrint ())) { append_string (m_last_key_string); setReadOnly (true); // Avoid that changing read only text is done afterwards } } // Text has changed: is cursor still in "writable" area? // This signal seems to be emitted before cursor position changed. void console::text_changed (void) { m_text_changed = true; } // Re-implement key event void console::keyPressEvent (QKeyEvent *e) { if (e->key () == Qt::Key_Return) // On "return", accept the current command line accept_command_line (); else { // Otherwise, store text process the expected event m_last_key_string = e->text (); QsciScintilla::keyPressEvent(e); } } OCTAVE_END_NAMESPACE(octave) #endif