#include "log_frame.h" #include "qt_utils.h" #include "gui_settings.h" #include "rpcs3_version.h" #include "Utilities/mutex.h" #include "Utilities/lockless.h" #include "util/asm.hpp" #include #include #include #include #include #include #include #include #include extern fs::file g_tty; extern atomic_t g_tty_size; extern std::array, 16> g_tty_input; extern std::mutex g_tty_mutex; extern bool g_log_all_errors; constexpr auto qstr = QString::fromStdString; struct gui_listener : logs::listener { atomic_t enabled{logs::level{0xff}}; struct packet_t { logs::level sev{}; std::string msg; }; lf_queue_slice pending; lf_queue queue; atomic_t show_prefix{false}; gui_listener() : logs::listener() { // Self-registration logs::listener::add(this); } ~gui_listener() { } void log(u64 stamp, const logs::message& msg, const std::string& prefix, const std::string& text) override { Q_UNUSED(stamp) if (msg <= enabled) { packet_t p,* _new = &p; _new->sev = msg; if ((msg == logs::level::fatal || show_prefix) && !prefix.empty()) { _new->msg += "{"; _new->msg += prefix; _new->msg += "} "; } if (msg->name && '\0' != *msg->name) { _new->msg += msg->name; _new->msg += msg == logs::level::todo ? " TODO: " : ": "; } else if (msg == logs::level::todo) { _new->msg += "TODO: "; } _new->msg += text; queue.push(std::move(p)); } } void pop() { pending.pop_front(); } packet_t* get() { if (packet_t* _head = pending.get()) { return _head; } pending = queue.pop_all(); return pending.get(); } void clear() { pending = lf_queue_slice(); queue.pop_all(); } }; // GUI Listener instance static gui_listener s_gui_listener; log_frame::log_frame(std::shared_ptr _gui_settings, QWidget* parent) : custom_dock_widget(tr("Log"), parent), m_gui_settings(std::move(_gui_settings)) { const int max_block_count_log = m_gui_settings->GetValue(gui::l_limit).toInt(); const int max_block_count_tty = m_gui_settings->GetValue(gui::l_limit_tty).toInt(); m_tabWidget = new QTabWidget; m_tabWidget->setObjectName(QStringLiteral("tab_widget_log")); m_tabWidget->tabBar()->setObjectName(QStringLiteral("tab_bar_log")); m_log = new QPlainTextEdit(m_tabWidget); m_log->setObjectName(QStringLiteral("log_frame")); m_log->setReadOnly(true); m_log->setContextMenuPolicy(Qt::CustomContextMenu); m_log->document()->setMaximumBlockCount(max_block_count_log); m_log->installEventFilter(this); m_tty = new QPlainTextEdit(m_tabWidget); m_tty->setObjectName(QStringLiteral("tty_frame")); m_tty->setReadOnly(true); m_tty->setContextMenuPolicy(Qt::CustomContextMenu); m_tty->document()->setMaximumBlockCount(max_block_count_tty); m_tty->installEventFilter(this); m_tty_input = new QLineEdit(); if (m_tty_channel >= 0) { m_tty_input->setPlaceholderText(tr("Channel %0").arg(m_tty_channel)); } else { m_tty_input->setPlaceholderText(tr("All user channels")); } QVBoxLayout* tty_layout = new QVBoxLayout(); tty_layout->addWidget(m_tty); tty_layout->addWidget(m_tty_input); tty_layout->setContentsMargins(0, 0, 0, 0); m_tty_container = new QWidget(); m_tty_container->setLayout(tty_layout); m_tabWidget->addTab(m_log, tr("Log")); m_tabWidget->addTab(m_tty_container, tr("TTY")); setWidget(m_tabWidget); // Open or create TTY.log m_tty_file.open(fs::get_cache_dir() + "TTY.log", fs::read + fs::create); CreateAndConnectActions(); LoadSettings(); m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &log_frame::UpdateUI); } void log_frame::SetLogLevel(logs::level lev) const { switch (lev) { case logs::level::always: case logs::level::fatal: { m_fatal_act->trigger(); break; } case logs::level::error: { m_error_act->trigger(); break; } case logs::level::todo: { m_todo_act->trigger(); break; } case logs::level::success: { m_success_act->trigger(); break; } case logs::level::warning: { m_warning_act->trigger(); break; } case logs::level::notice: { m_notice_act->trigger(); break; } case logs::level::trace: { m_trace_act->trigger(); break; } } } void log_frame::SetTTYLogging(bool val) const { m_tty_act->setChecked(val); } void log_frame::CreateAndConnectActions() { // I, for one, welcome our lambda overlord // It's either this or a signal mapper // Then, probably making a list of these actions so that it's easier to iterate to generate the mapper. auto l_initAct = [this](QAction* act, logs::level logLevel) { act->setCheckable(true); // This sets the log level properly when the action is triggered. connect(act, &QAction::triggered, [this, logLevel]() { s_gui_listener.enabled = std::max(logLevel, logs::level::fatal); m_gui_settings->SetValue(gui::l_level, static_cast(logLevel)); }); }; m_clear_act = new QAction(tr("Clear"), this); connect(m_clear_act, &QAction::triggered, [this]() { m_old_log_text.clear(); m_log->clear(); s_gui_listener.clear(); }); m_clear_tty_act = new QAction(tr("Clear"), this); connect(m_clear_tty_act, &QAction::triggered, [this]() { m_old_tty_text.clear(); m_tty->clear(); }); m_perform_goto_on_debugger = new QAction(tr("Go-To On The Debugger"), this); connect(m_perform_goto_on_debugger, &QAction::triggered, [this]() { QPlainTextEdit* pte = (m_tabWidget->currentIndex() == 1 ? m_tty : m_log); Q_EMIT PerformGoToOnDebugger(pte->textCursor().selectedText()); }); m_stack_act_tty = new QAction(tr("Stack Mode (TTY)"), this); m_stack_act_tty->setCheckable(true); connect(m_stack_act_tty, &QAction::toggled, [this](bool checked) { m_gui_settings->SetValue(gui::l_stack_tty, checked); m_stack_tty = checked; }); m_ansi_act_tty = new QAction(tr("ANSI Code (TTY)"), this); m_ansi_act_tty->setCheckable(true); connect(m_ansi_act_tty, &QAction::toggled, [this](bool checked) { m_gui_settings->SetValue(gui::l_ansi_code, checked); m_ansi_tty = checked; }); m_tty_channel_acts = new QActionGroup(this); // Special Channel: All QAction* all_channels_act = new QAction(tr("All user channels"), m_tty_channel_acts); all_channels_act->setCheckable(true); all_channels_act->setChecked(m_tty_channel == -1); connect(all_channels_act, &QAction::triggered, [this]() { m_tty_channel = -1; m_tty_input->setPlaceholderText(tr("All user channels")); }); for (int i = 3; i < 16; i++) { QAction* act = new QAction(tr("Channel %0").arg(i), m_tty_channel_acts); act->setCheckable(true); act->setChecked(i == m_tty_channel); connect(act, &QAction::triggered, [this, i]() { m_tty_channel = i; m_tty_input->setPlaceholderText(tr("Channel %0").arg(m_tty_channel)); }); } // Action groups make these actions mutually exclusive. m_log_level_acts = new QActionGroup(this); m_nothing_act = new QAction(tr("Nothing"), m_log_level_acts); m_nothing_act->setVisible(false); m_fatal_act = new QAction(tr("Fatal"), m_log_level_acts); m_error_act = new QAction(tr("Error"), m_log_level_acts); m_todo_act = new QAction(tr("Todo"), m_log_level_acts); m_success_act = new QAction(tr("Success"), m_log_level_acts); m_warning_act = new QAction(tr("Warning"), m_log_level_acts); m_notice_act = new QAction(tr("Notice"), m_log_level_acts); m_trace_act = new QAction(tr("Trace"), m_log_level_acts); m_stack_act_log = new QAction(tr("Stack Mode (Log)"), this); m_stack_act_log->setCheckable(true); connect(m_stack_act_log, &QAction::toggled, [this](bool checked) { m_gui_settings->SetValue(gui::l_stack, checked); m_stack_log = checked; }); m_stack_act_err = new QAction(tr("Stack Cell Errors"), this); m_stack_act_err->setCheckable(true); connect(m_stack_act_err, &QAction::toggled, [this](bool checked) { m_gui_settings->SetValue(gui::l_stack_err, checked); g_log_all_errors = !checked; }); m_show_prefix_act = new QAction(tr("Show Thread Prefix"), this); m_show_prefix_act->setCheckable(true); connect(m_show_prefix_act, &QAction::toggled, [this](bool checked) { m_gui_settings->SetValue(gui::l_prefix, checked); s_gui_listener.show_prefix = checked; }); m_tty_act = new QAction(tr("Enable TTY"), this); m_tty_act->setCheckable(true); connect(m_tty_act, &QAction::triggered, [this](bool checked) { m_gui_settings->SetValue(gui::l_tty, checked); }); l_initAct(m_nothing_act, logs::level::fatal); l_initAct(m_fatal_act, logs::level::fatal); l_initAct(m_error_act, logs::level::error); l_initAct(m_todo_act, logs::level::todo); l_initAct(m_success_act, logs::level::success); l_initAct(m_warning_act, logs::level::warning); l_initAct(m_notice_act, logs::level::notice); l_initAct(m_trace_act, logs::level::trace); connect(m_log, &QWidget::customContextMenuRequested, [this](const QPoint& pos) { QMenu* menu = m_log->createStandardContextMenu(); menu->addAction(m_clear_act); menu->addAction(m_perform_goto_on_debugger); std::shared_ptr goto_signal_accepted = std::make_shared(false); Q_EMIT PerformGoToOnDebugger("", true, goto_signal_accepted); m_perform_goto_on_debugger->setEnabled(m_log->textCursor().hasSelection() && *goto_signal_accepted); m_perform_goto_on_debugger->setToolTip(tr("Jump to the selected hexadecimal address from the log text on the debugger.")); menu->addSeparator(); menu->addActions(m_log_level_acts->actions()); menu->addSeparator(); menu->addAction(m_stack_act_log); menu->addAction(m_stack_act_err); menu->addAction(m_show_prefix_act); menu->exec(m_log->viewport()->mapToGlobal(pos)); }); connect(m_tty, &QWidget::customContextMenuRequested, [this](const QPoint& pos) { QMenu* menu = m_tty->createStandardContextMenu(); menu->addAction(m_clear_tty_act); menu->addAction(m_perform_goto_on_debugger); std::shared_ptr goto_signal_accepted = std::make_shared(false); Q_EMIT PerformGoToOnDebugger("", true, goto_signal_accepted); m_perform_goto_on_debugger->setEnabled(m_tty->textCursor().hasSelection() && *goto_signal_accepted); m_perform_goto_on_debugger->setToolTip(tr("Jump to the selected hexadecimal address from the TTY text on the debugger.")); menu->addSeparator(); menu->addAction(m_tty_act); menu->addAction(m_stack_act_tty); menu->addAction(m_ansi_act_tty); menu->addSeparator(); menu->addActions(m_tty_channel_acts->actions()); menu->exec(m_tty->viewport()->mapToGlobal(pos)); }); connect(m_tabWidget, &QTabWidget::currentChanged, [this](int/* index*/) { if (m_find_dialog) m_find_dialog->close(); }); connect(m_tty_input, &QLineEdit::returnPressed, [this]() { std::string text = m_tty_input->text().toStdString(); { std::lock_guard lock(g_tty_mutex); if (m_tty_channel == -1) { for (int i = 3; i < 16; i++) { g_tty_input[i].push_back(text + "\n"); } } else { g_tty_input[m_tty_channel].push_back(text + "\n"); } } // Write to tty if (m_tty_channel == -1) { text = "All channels > " + text + "\n"; } else { text = fmt::format("Ch.%d > %s\n", m_tty_channel, text); } if (g_tty) { g_tty_size -= (1ll << 48); g_tty.write(text.c_str(), text.size()); g_tty_size += (1ll << 48) + text.size(); } m_tty_input->clear(); }); } void log_frame::LoadSettings() { SetLogLevel(m_gui_settings->GetLogLevel()); SetTTYLogging(m_gui_settings->GetValue(gui::l_tty).toBool()); m_stack_log = m_gui_settings->GetValue(gui::l_stack).toBool(); m_stack_tty = m_gui_settings->GetValue(gui::l_stack_tty).toBool(); m_ansi_tty = m_gui_settings->GetValue(gui::l_ansi_code).toBool(); g_log_all_errors = !m_gui_settings->GetValue(gui::l_stack_err).toBool(); m_stack_act_log->setChecked(m_stack_log); m_stack_act_tty->setChecked(m_stack_tty); m_ansi_act_tty->setChecked(m_ansi_tty); m_stack_act_err->setChecked(!g_log_all_errors); s_gui_listener.show_prefix = m_gui_settings->GetValue(gui::l_prefix).toBool(); m_show_prefix_act->setChecked(s_gui_listener.show_prefix); if (m_log) { m_log->document()->setMaximumBlockCount(m_gui_settings->GetValue(gui::l_limit).toInt()); } if (m_tty) { m_tty->document()->setMaximumBlockCount(m_gui_settings->GetValue(gui::l_limit_tty).toInt()); } // Note: There's an issue where the scrollbar value won't be set to max if we start the log frame too early, // so let's delay the timer until we load the settings from the main window for the first time. if (m_timer && !m_timer->isActive()) { // Check for updates every ~10 ms m_timer->start(10); } } void log_frame::RepaintTextColors() { // Backup old colors QList old_colors = m_color; QColor old_stack_color = m_color_stack; // Get text color. Do this once to prevent possible slowdown m_color.clear(); m_color.append(gui::utils::get_label_color("log_level_always")); m_color.append(gui::utils::get_label_color("log_level_fatal")); m_color.append(gui::utils::get_label_color("log_level_error")); m_color.append(gui::utils::get_label_color("log_level_todo")); m_color.append(gui::utils::get_label_color("log_level_success")); m_color.append(gui::utils::get_label_color("log_level_warning")); m_color.append(gui::utils::get_label_color("log_level_notice")); m_color.append(gui::utils::get_label_color("log_level_trace")); m_color_stack = gui::utils::get_label_color("log_stack"); // Use new colors if the old colors weren't set yet if (old_colors.empty()) { old_colors = m_color; } if (!old_stack_color.isValid()) { old_stack_color = m_color_stack; } // Repaint TTY with new colors QTextCursor tty_cursor = m_tty->textCursor(); QTextCharFormat text_format = tty_cursor.charFormat(); text_format.setForeground(gui::utils::get_label_color("tty_text")); tty_cursor.setCharFormat(text_format); m_tty->setTextCursor(tty_cursor); // Repaint log with new colors QString html = m_log->document()->toHtml(); const QHash log_chars { { static_cast(logs::level::always), '-' }, { static_cast(logs::level::fatal), 'F' }, { static_cast(logs::level::error), 'E' }, { static_cast(logs::level::todo), 'U' }, { static_cast(logs::level::success), 'S' }, { static_cast(logs::level::warning), 'W' }, { static_cast(logs::level::notice), '!' }, { static_cast(logs::level::trace), 'T' } }; const auto replace_color = [&](logs::level lvl) { const QString old_style = QStringLiteral("color:") + old_colors[static_cast(lvl)].name() + QStringLiteral(";\">") + log_chars[static_cast(lvl)]; const QString new_style = QStringLiteral("color:") + m_color[static_cast(lvl)].name() + QStringLiteral(";\">") + log_chars[static_cast(lvl)]; html.replace(old_style, new_style); }; replace_color(logs::level::always); replace_color(logs::level::fatal); replace_color(logs::level::error); replace_color(logs::level::todo); replace_color(logs::level::success); replace_color(logs::level::warning); replace_color(logs::level::notice); replace_color(logs::level::trace); // Special case: stack const QString old_style = QStringLiteral("color:") + old_stack_color.name() + QStringLiteral(";\"> x"); const QString new_style = QStringLiteral("color:") + m_color_stack.name() + QStringLiteral(";\"> x"); html.replace(old_style, new_style); m_log->document()->setHtml(html); } void log_frame::UpdateUI() { const std::chrono::time_point start = steady_clock::now(); const std::chrono::time_point tty_timeout = start + 4ms; const std::chrono::time_point log_timeout = start + 7ms; // Check TTY logs if (u64 size = std::max(0, m_tty_file ? (g_tty_size.load() - m_tty_file.pos()) : 0)) { if (m_tty_act->isChecked()) { m_tty_buf.resize(std::min(size, m_tty_limited_read ? m_tty_limited_read : usz{umax})); m_tty_buf.resize(m_tty_file.read(&m_tty_buf.front(), m_tty_buf.size())); m_tty_limited_read = 0; usz str_index = 0; std::string buf_line; while (str_index < m_tty_buf.size()) { buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index)); str_index += buf_line.size() + 1; // Ignore control characters and greater/equal to 0x80 buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) { return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); }), buf_line.end()); // save old scroll bar state QScrollBar* sb = m_tty->verticalScrollBar(); const int sb_pos = sb->value(); const bool is_max = sb_pos == sb->maximum(); // save old selection QTextCursor text_cursor{ m_tty->document() }; const int sel_pos = text_cursor.position(); int sel_start = text_cursor.selectionStart(); int sel_end = text_cursor.selectionEnd(); // clear selection or else it will get colorized as well text_cursor.clearSelection(); QString tty_text; if (m_ansi_tty) { tty_text = QString::fromStdString(buf_line); } else { // Strip ANSI color code static const QRegularExpression ansi_color_code("\\[\\d+;\\d+(;\\d+)?m|\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]"); tty_text = QString::fromStdString(buf_line).remove(ansi_color_code); } // create counter suffix and remove recurring line if needed if (m_stack_tty) { text_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); if (tty_text == m_old_tty_text) { text_cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); text_cursor.insertText(tty_text % QStringLiteral(" x") % QString::number(++m_tty_counter)); } else { m_tty_counter = 1; m_old_tty_text = tty_text; // write text to the end m_tty->setTextCursor(text_cursor); m_tty->appendPlainText(tty_text); } } else { // write text to the end m_tty->setTextCursor(text_cursor); m_tty->appendPlainText(tty_text); } // if we mark text from right to left we need to swap sides (start is always smaller than end) if (sel_pos < sel_end) { std::swap(sel_start, sel_end); } // reset old text cursor and selection text_cursor.setPosition(sel_start); text_cursor.setPosition(sel_end, QTextCursor::KeepAnchor); m_tty->setTextCursor(text_cursor); // set scrollbar to max means auto-scroll sb->setValue(is_max ? sb->maximum() : sb_pos); // Limit processing time if (steady_clock::now() >= tty_timeout) { const s64 back = ::narrow(str_index) - ::narrow(m_tty_buf.size()); ensure(back <= 1); if (back < 0) { // If more than two thirds of the buffer are unprocessed make the next fs::file::read read only that // This is because reading is also costly on performance, and if we already know half of that takes more time than our limit.. const usz third_size = utils::aligned_div(m_tty_buf.size(), 3); if (back <= -16384 && static_cast(0 - back) >= third_size * 2) { // This only really works if there is a newline somewhere const usz known_term = std::string_view(m_tty_buf).substr(str_index, str_index * 2).find_last_of('\n', str_index * 2 - 4096); if (known_term != umax) { m_tty_limited_read = known_term + 1 - str_index; } } // Revert unprocessed reads m_tty_file.seek(back, fs::seek_cur); } break; } } } else { // Advance in position without printing m_tty_file.seek(size, fs::seek_cur); m_tty_limited_read = 0; } } const auto font_start_tag = [](const QColor& color) -> const QString { return QStringLiteral(""); }; const QString font_start_tag_stack = ""; static const QString font_end_tag = QStringLiteral(""); static constexpr auto escaped = [](const QString& text) { return text.toHtmlEscaped().replace(QStringLiteral("\n"), QStringLiteral("
")); }; // Preserve capacity m_log_text.resize(0); // Handle a common case in which we may need to override the previous repetition count bool is_first_rep = m_stack_log; usz first_rep_counter = m_log_counter; // Batch output of multiple lines if possible (optimization) auto flush = [&]() { if (m_log_text.isEmpty() && !is_first_rep) { return; } // save old log state QScrollBar* sb = m_log->verticalScrollBar(); const bool is_max = sb->value() == sb->maximum(); const int sb_pos = sb->value(); QTextCursor text_cursor = m_log->textCursor(); const int sel_pos = text_cursor.position(); int sel_start = text_cursor.selectionStart(); int sel_end = text_cursor.selectionEnd(); // clear selection or else it will get colorized as well text_cursor.clearSelection(); m_log->setTextCursor(text_cursor); if (is_first_rep) { // Overwrite existing repetition counter in our text document. ensure(first_rep_counter > m_log_counter); // Anything else is a bug text_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); if (m_log_counter > 1) { constexpr int size_of_x = 2; // " x" const int size_of_number = static_cast(QString::number(m_log_counter).size()); text_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, size_of_x + size_of_number); } text_cursor.insertHtml(font_start_tag_stack % QStringLiteral(" x") % QString::number(first_rep_counter) % font_end_tag); } else { // Insert both messages and repetition prefix (append is more optimized than concatenation) m_log_text += font_end_tag; if (m_log_counter > 1) { m_log_text += font_start_tag_stack; m_log_text += QStringLiteral(" x"); m_log_text += QString::number(m_log_counter); m_log_text += font_end_tag; } m_log->appendHtml(m_log_text); } // if we mark text from right to left we need to swap sides (start is always smaller than end) if (sel_pos < sel_end) { std::swap(sel_start, sel_end); } // reset old text cursor and selection text_cursor.setPosition(sel_start); text_cursor.setPosition(sel_end, QTextCursor::KeepAnchor); m_log->setTextCursor(text_cursor); // set scrollbar to max means auto-scroll sb->setValue(is_max ? sb->maximum() : sb_pos); m_log_text.clear(); }; const auto patch_first_stacked_message = [&]() -> bool { if (is_first_rep) { const bool needs_update = first_rep_counter > m_log_counter; if (needs_update) { // Update the existing stack suffix. flush(); } is_first_rep = false; m_log_counter = first_rep_counter; return needs_update; } return false; }; // Check main logs while (auto* packet = s_gui_listener.get()) { // Confirm log level if (packet->sev <= s_gui_listener.enabled) { // Check if we can stack this log message. if (m_stack_log && m_old_log_level == packet->sev && packet->msg == m_old_log_text) { if (is_first_rep) { // Keep tracking the old stack suffix count as long as the last known message keeps repeating. first_rep_counter++; } else { m_log_counter++; } s_gui_listener.pop(); if (steady_clock::now() >= log_timeout) { // Must break eventually break; } continue; } // Add/update counter suffix if needed. Try not to hold too much data at a time so the frame content will be updated frequently. if (!patch_first_stacked_message() && (m_log_counter > 1 || m_log_text.size() > 0x1000)) { flush(); } if (m_log_text.isEmpty()) { m_old_log_level = packet->sev; m_log_text += font_start_tag(m_color[static_cast(m_old_log_level)]); } else { if (packet->sev != m_old_log_level) { flush(); m_old_log_level = packet->sev; m_log_text += font_start_tag(m_color[static_cast(m_old_log_level)]); } else { m_log_text += QStringLiteral("
"); } } switch (packet->sev) { case logs::level::always: m_log_text += QStringLiteral("- "); break; case logs::level::fatal: m_log_text += QStringLiteral("F "); break; case logs::level::error: m_log_text += QStringLiteral("E "); break; case logs::level::todo: m_log_text += QStringLiteral("U "); break; case logs::level::success: m_log_text += QStringLiteral("S "); break; case logs::level::warning: m_log_text += QStringLiteral("W "); break; case logs::level::notice: m_log_text += QStringLiteral("! "); break; case logs::level::trace: m_log_text += QStringLiteral("T "); break; } // Print UTF-8 text. m_log_text += escaped(qstr(packet->msg)); if (m_stack_log) { m_log_counter = 1; m_old_log_text = std::move(packet->msg); } } // Drop packet s_gui_listener.pop(); // Limit processing time if (steady_clock::now() >= log_timeout) break; } if (!patch_first_stacked_message()) { flush(); } } void log_frame::closeEvent(QCloseEvent *event) { QDockWidget::closeEvent(event); Q_EMIT LogFrameClosed(); } bool log_frame::eventFilter(QObject* object, QEvent* event) { if (object != m_log && object != m_tty) { return QDockWidget::eventFilter(object, event); } if (event->type() == QEvent::KeyPress) { QKeyEvent* e = static_cast(event); if (e && !e->isAutoRepeat() && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F) { if (m_find_dialog && m_find_dialog->isVisible()) m_find_dialog->close(); m_find_dialog.reset(new find_dialog(static_cast(object), this)); } } return QDockWidget::eventFilter(object, event); }