/* * This file is part of the PulseView project. * * Copyright (C) 2012 Joel Holdsworth * * 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, see . */ #ifdef ENABLE_DECODE #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mainwindow.hpp" #include "devicemanager.hpp" #include "devices/hardwaredevice.hpp" #include "dialogs/settings.hpp" #include "globalsettings.hpp" #include "toolbars/mainbar.hpp" #include "util.hpp" #include "views/trace/view.hpp" #include "views/trace/standardbar.hpp" #include using std::bind; using std::dynamic_pointer_cast; using std::make_shared; using std::placeholders::_1; using std::shared_ptr; using std::string; namespace pv { namespace view { class ViewItem; } using toolbars::MainBar; const QString MainWindow::WindowTitle = tr("PulseView"); MainWindow::MainWindow(DeviceManager &device_manager, QWidget *parent) : QMainWindow(parent), device_manager_(device_manager), session_selector_(this), session_state_mapper_(this), icon_red_(":/icons/status-red.svg"), icon_green_(":/icons/status-green.svg"), icon_grey_(":/icons/status-grey.svg") { qRegisterMetaType("util::Timestamp"); qRegisterMetaType("uint64_t"); GlobalSettings::add_change_handler(this); GlobalSettings settings; settings.set_defaults_where_needed(); setup_ui(); restore_ui_settings(); } MainWindow::~MainWindow() { GlobalSettings::remove_change_handler(this); while (!sessions_.empty()) remove_session(sessions_.front()); } shared_ptr MainWindow::get_active_view() const { // If there's only one view, use it... if (view_docks_.size() == 1) return view_docks_.begin()->second; // ...otherwise find the dock widget the widget with focus is contained in QObject *w = QApplication::focusWidget(); QDockWidget *dock = nullptr; while (w) { dock = qobject_cast(w); if (dock) break; w = w->parent(); } // Get the view contained in the dock widget for (auto entry : view_docks_) if (entry.first == dock) return entry.second; return nullptr; } shared_ptr MainWindow::add_view(const QString &title, views::ViewType type, Session &session) { GlobalSettings settings; shared_ptr v; QMainWindow *main_window = nullptr; for (auto entry : session_windows_) if (entry.first.get() == &session) main_window = entry.second; assert(main_window); shared_ptr main_bar = session.main_bar(); QDockWidget* dock = new QDockWidget(title, main_window); dock->setObjectName(title); main_window->addDockWidget(Qt::TopDockWidgetArea, dock); // Insert a QMainWindow into the dock widget to allow for a tool bar QMainWindow *dock_main = new QMainWindow(dock); dock_main->setWindowFlags(Qt::Widget); // Remove Qt::Window flag if (type == views::ViewTypeTrace) // This view will be the main view if there's no main bar yet v = make_shared(session, (main_bar ? false : true), dock_main); if (!v) return nullptr; view_docks_[dock] = v; session.register_view(v); dock_main->setCentralWidget(v.get()); dock->setWidget(dock_main); dock->setContextMenuPolicy(Qt::PreventContextMenu); dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); QAbstractButton *close_btn = dock->findChildren ("qt_dockwidget_closebutton").front(); connect(close_btn, SIGNAL(clicked(bool)), this, SLOT(on_view_close_clicked())); connect(&session, SIGNAL(trigger_event(int, util::Timestamp)), qobject_cast(v.get()), SLOT(trigger_event(int, util::Timestamp))); if (type == views::ViewTypeTrace) { views::trace::View *tv = qobject_cast(v.get()); tv->enable_coloured_bg(settings.value(GlobalSettings::Key_View_ColouredBG).toBool()); tv->enable_show_sampling_points(settings.value(GlobalSettings::Key_View_ShowSamplingPoints).toBool()); tv->enable_show_analog_minor_grid(settings.value(GlobalSettings::Key_View_ShowAnalogMinorGrid).toBool()); if (!main_bar) { /* Initial view, create the main bar */ main_bar = make_shared(session, this, tv); dock_main->addToolBar(main_bar.get()); session.set_main_bar(main_bar); connect(main_bar.get(), SIGNAL(new_view(Session*)), this, SLOT(on_new_view(Session*))); main_bar->action_view_show_cursors()->setChecked(tv->cursors_shown()); /* For the main view we need to prevent the dock widget from * closing itself when its close button is clicked. This is * so we can confirm with the user first. Regular views don't * need this */ close_btn->disconnect(SIGNAL(clicked()), dock, SLOT(close())); } else { /* Additional view, create a standard bar */ pv::views::trace::StandardBar *standard_bar = new pv::views::trace::StandardBar(session, this, tv); dock_main->addToolBar(standard_bar); standard_bar->action_view_show_cursors()->setChecked(tv->cursors_shown()); } } return v; } void MainWindow::remove_view(shared_ptr view) { for (shared_ptr session : sessions_) { if (!session->has_view(view)) continue; // Find the dock the view is contained in and remove it for (auto entry : view_docks_) if (entry.second == view) { // Remove the view from the session session->deregister_view(view); // Remove the view from its parent; otherwise, Qt will // call deleteLater() on it, which causes a double free // since the shared_ptr in view_docks_ doesn't know // that Qt keeps a pointer to the view around view->setParent(nullptr); // Delete the view's dock widget and all widgets inside it entry.first->deleteLater(); // Remove the dock widget from the list and stop iterating view_docks_.erase(entry.first); break; } } } shared_ptr MainWindow::add_session() { static int last_session_id = 1; QString name = tr("Session %1").arg(last_session_id++); shared_ptr session = make_shared(device_manager_, name); connect(session.get(), SIGNAL(add_view(const QString&, views::ViewType, Session*)), this, SLOT(on_add_view(const QString&, views::ViewType, Session*))); connect(session.get(), SIGNAL(name_changed()), this, SLOT(on_session_name_changed())); session_state_mapper_.setMapping(session.get(), session.get()); connect(session.get(), SIGNAL(capture_state_changed(int)), &session_state_mapper_, SLOT(map())); sessions_.push_back(session); QMainWindow *window = new QMainWindow(); window->setWindowFlags(Qt::Widget); // Remove Qt::Window flag session_windows_[session] = window; int index = session_selector_.addTab(window, name); session_selector_.setCurrentIndex(index); last_focused_session_ = session; window->setDockNestingEnabled(true); shared_ptr main_view = add_view(name, views::ViewTypeTrace, *session); return session; } void MainWindow::remove_session(shared_ptr session) { // Determine the height of the button before it collapses int h = new_session_button_->height(); // Stop capture while the session still exists so that the UI can be // updated in case we're currently running. If so, this will schedule a // call to our on_capture_state_changed() slot for the next run of the // event loop. We need to have this executed immediately or else it will // be dismissed since the session object will be deleted by the time we // leave this method and the event loop gets a chance to run again. session->stop_capture(); QApplication::processEvents(); for (shared_ptr view : session->views()) remove_view(view); QMainWindow *window = session_windows_.at(session); session_selector_.removeTab(session_selector_.indexOf(window)); session_windows_.erase(session); if (last_focused_session_ == session) last_focused_session_.reset(); // Remove the session from our list of sessions (which also destroys it) sessions_.remove_if([&](shared_ptr s) { return s == session; }); if (sessions_.empty()) { // When there are no more tabs, the height of the QTabWidget // drops to zero. We must prevent this to keep the static // widgets visible for (QWidget *w : static_tab_widget_->findChildren()) w->setMinimumHeight(h); int margin = static_tab_widget_->layout()->contentsMargins().bottom(); static_tab_widget_->setMinimumHeight(h + 2 * margin); session_selector_.setMinimumHeight(h + 2 * margin); // Update the window title if there is no view left to // generate focus change events setWindowTitle(WindowTitle); } } void MainWindow::add_session_with_file(string open_file_name, string open_file_format) { shared_ptr session = add_session(); session->load_init_file(open_file_name, open_file_format); } void MainWindow::add_default_session() { // Only add the default session if there would be no session otherwise if (sessions_.size() > 0) return; shared_ptr session = add_session(); // Check the list of available devices. Prefer the one that was // found with user supplied scan specs (if applicable). Then try // one of the auto detected devices that are not the demo device. // Pick demo in the absence of "genuine" hardware devices. shared_ptr user_device, other_device, demo_device; for (shared_ptr dev : device_manager_.devices()) { if (dev == device_manager_.user_spec_device()) { user_device = dev; } else if (dev->hardware_device()->driver()->name() == "demo") { demo_device = dev; } else { other_device = dev; } } if (user_device) session->select_device(user_device); else if (other_device) session->select_device(other_device); else session->select_device(demo_device); } void MainWindow::save_sessions() { QSettings settings; int id = 0; for (shared_ptr session : sessions_) { // Ignore sessions using the demo device or no device at all if (session->device()) { shared_ptr device = dynamic_pointer_cast< devices::HardwareDevice > (session->device()); if (device && device->hardware_device()->driver()->name() == "demo") continue; settings.beginGroup("Session" + QString::number(id++)); settings.remove(""); // Remove all keys in this group session->save_settings(settings); settings.endGroup(); } } settings.setValue("sessions", id); } void MainWindow::restore_sessions() { QSettings settings; int i, session_count; session_count = settings.value("sessions", 0).toInt(); for (i = 0; i < session_count; i++) { settings.beginGroup("Session" + QString::number(i)); shared_ptr session = add_session(); session->restore_settings(settings); settings.endGroup(); } } void MainWindow::on_setting_changed(const QString &key, const QVariant &value) { if (key == GlobalSettings::Key_View_ColouredBG) on_settingViewColouredBg_changed(value); if (key == GlobalSettings::Key_View_ShowSamplingPoints) on_settingViewShowSamplingPoints_changed(value); if (key == GlobalSettings::Key_View_ShowAnalogMinorGrid) on_settingViewShowAnalogMinorGrid_changed(value); } void MainWindow::setup_ui() { setObjectName(QString::fromUtf8("MainWindow")); setCentralWidget(&session_selector_); // Set the window icon QIcon icon; icon.addFile(QString(":/icons/pulseview.png")); setWindowIcon(icon); view_sticky_scrolling_shortcut_ = new QShortcut(QKeySequence(Qt::Key_S), this, SLOT(on_view_sticky_scrolling_shortcut())); view_sticky_scrolling_shortcut_->setAutoRepeat(false); view_show_sampling_points_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Period), this, SLOT(on_view_show_sampling_points_shortcut())); view_show_sampling_points_shortcut_->setAutoRepeat(false); view_show_analog_minor_grid_shortcut_ = new QShortcut(QKeySequence(Qt::Key_G), this, SLOT(on_view_show_analog_minor_grid_shortcut())); view_show_analog_minor_grid_shortcut_->setAutoRepeat(false); view_coloured_bg_shortcut_ = new QShortcut(QKeySequence(Qt::Key_B), this, SLOT(on_view_coloured_bg_shortcut())); view_coloured_bg_shortcut_->setAutoRepeat(false); // Set up the tab area new_session_button_ = new QToolButton(); new_session_button_->setIcon(QIcon::fromTheme("document-new", QIcon(":/icons/document-new.png"))); new_session_button_->setToolTip(tr("Create New Session")); new_session_button_->setAutoRaise(true); run_stop_button_ = new QToolButton(); run_stop_button_->setAutoRaise(true); run_stop_button_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); run_stop_button_->setToolTip(tr("Start/Stop Acquisition")); run_stop_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Space), run_stop_button_, SLOT(click())); run_stop_shortcut_->setAutoRepeat(false); settings_button_ = new QToolButton(); settings_button_->setIcon(QIcon::fromTheme("preferences-system", QIcon(":/icons/preferences-system.png"))); settings_button_->setToolTip(tr("Settings")); settings_button_->setAutoRaise(true); QFrame *separator1 = new QFrame(); separator1->setFrameStyle(QFrame::VLine | QFrame::Raised); QFrame *separator2 = new QFrame(); separator2->setFrameStyle(QFrame::VLine | QFrame::Raised); QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(2, 2, 2, 2); layout->addWidget(new_session_button_); layout->addWidget(separator1); layout->addWidget(run_stop_button_); layout->addWidget(separator2); layout->addWidget(settings_button_); static_tab_widget_ = new QWidget(); static_tab_widget_->setLayout(layout); session_selector_.setCornerWidget(static_tab_widget_, Qt::TopLeftCorner); session_selector_.setTabsClosable(true); close_application_shortcut_ = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this, SLOT(close())); close_application_shortcut_->setAutoRepeat(false); close_current_tab_shortcut_ = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this, SLOT(on_close_current_tab())); connect(new_session_button_, SIGNAL(clicked(bool)), this, SLOT(on_new_session_clicked())); connect(run_stop_button_, SIGNAL(clicked(bool)), this, SLOT(on_run_stop_clicked())); connect(&session_state_mapper_, SIGNAL(mapped(QObject*)), this, SLOT(on_capture_state_changed(QObject*))); connect(settings_button_, SIGNAL(clicked(bool)), this, SLOT(on_settings_clicked())); connect(&session_selector_, SIGNAL(tabCloseRequested(int)), this, SLOT(on_tab_close_requested(int))); connect(&session_selector_, SIGNAL(currentChanged(int)), this, SLOT(on_tab_changed(int))); connect(static_cast(QCoreApplication::instance()), SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(on_focus_changed())); } void MainWindow::save_ui_settings() { QSettings settings; settings.beginGroup("MainWindow"); settings.setValue("state", saveState()); settings.setValue("geometry", saveGeometry()); settings.endGroup(); } void MainWindow::restore_ui_settings() { QSettings settings; settings.beginGroup("MainWindow"); if (settings.contains("geometry")) { restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("state").toByteArray()); } else resize(1000, 720); settings.endGroup(); } shared_ptr MainWindow::get_tab_session(int index) const { // Find the session that belongs to the tab's main window for (auto entry : session_windows_) if (entry.second == session_selector_.widget(index)) return entry.first; return nullptr; } void MainWindow::closeEvent(QCloseEvent *event) { bool data_saved = true; for (auto entry : session_windows_) if (!entry.first->data_saved()) data_saved = false; if (!data_saved && (QMessageBox::question(this, tr("Confirmation"), tr("There is unsaved data. Close anyway?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No)) { event->ignore(); } else { save_ui_settings(); save_sessions(); event->accept(); } } QMenu* MainWindow::createPopupMenu() { return nullptr; } bool MainWindow::restoreState(const QByteArray &state, int version) { (void)state; (void)version; // Do nothing. We don't want Qt to handle this, or else it // will try to restore all the dock widgets and create havoc. return false; } void MainWindow::session_error(const QString text, const QString info_text) { QMetaObject::invokeMethod(this, "show_session_error", Qt::QueuedConnection, Q_ARG(QString, text), Q_ARG(QString, info_text)); } void MainWindow::show_session_error(const QString text, const QString info_text) { QMessageBox msg(this); msg.setText(text); msg.setInformativeText(info_text); msg.setStandardButtons(QMessageBox::Ok); msg.setIcon(QMessageBox::Warning); msg.exec(); } void MainWindow::on_add_view(const QString &title, views::ViewType type, Session *session) { // We get a pointer and need a reference for (shared_ptr s : sessions_) if (s.get() == session) add_view(title, type, *s); } void MainWindow::on_focus_changed() { shared_ptr view = get_active_view(); if (view) { for (shared_ptr session : sessions_) { if (session->has_view(view)) { if (session != last_focused_session_) { // Activate correct tab if necessary shared_ptr tab_session = get_tab_session( session_selector_.currentIndex()); if (tab_session != session) session_selector_.setCurrentWidget( session_windows_.at(session)); on_focused_session_changed(session); } break; } } } if (sessions_.empty()) setWindowTitle(WindowTitle); } void MainWindow::on_focused_session_changed(shared_ptr session) { last_focused_session_ = session; setWindowTitle(session->name() + " - " + WindowTitle); // Update the state of the run/stop button, too on_capture_state_changed(session.get()); } void MainWindow::on_new_session_clicked() { add_session(); } void MainWindow::on_run_stop_clicked() { shared_ptr session = last_focused_session_; if (!session) return; switch (session->get_capture_state()) { case Session::Stopped: session->start_capture([&](QString message) { session_error("Capture failed", message); }); break; case Session::AwaitingTrigger: case Session::Running: session->stop_capture(); break; } } void MainWindow::on_settings_clicked() { dialogs::Settings dlg(device_manager_); dlg.exec(); } void MainWindow::on_session_name_changed() { // Update the corresponding dock widget's name(s) Session *session = qobject_cast(QObject::sender()); assert(session); for (shared_ptr view : session->views()) { // Get the dock that contains the view for (auto entry : view_docks_) if (entry.second == view) { entry.first->setObjectName(session->name()); entry.first->setWindowTitle(session->name()); } } // Update the tab widget by finding the main window and the tab from that for (auto entry : session_windows_) if (entry.first.get() == session) { QMainWindow *window = entry.second; const int index = session_selector_.indexOf(window); session_selector_.setTabText(index, session->name()); } // Refresh window title if the affected session has focus if (session == last_focused_session_.get()) setWindowTitle(session->name() + " - " + WindowTitle); } void MainWindow::on_capture_state_changed(QObject *obj) { Session *caller = qobject_cast(obj); // Ignore if caller is not the currently focused session // unless there is only one session if ((sessions_.size() > 1) && (caller != last_focused_session_.get())) return; int state = caller->get_capture_state(); const QIcon *icons[] = {&icon_grey_, &icon_red_, &icon_green_}; run_stop_button_->setIcon(*icons[state]); run_stop_button_->setText((state == pv::Session::Stopped) ? tr("Run") : tr("Stop")); } void MainWindow::on_new_view(Session *session) { // We get a pointer and need a reference for (shared_ptr s : sessions_) if (s.get() == session) add_view(session->name(), views::ViewTypeTrace, *s); } void MainWindow::on_view_close_clicked() { // Find the dock widget that contains the close button that was clicked QObject *w = QObject::sender(); QDockWidget *dock = nullptr; while (w) { dock = qobject_cast(w); if (dock) break; w = w->parent(); } // Get the view contained in the dock widget shared_ptr view; for (auto entry : view_docks_) if (entry.first == dock) view = entry.second; // Deregister the view for (shared_ptr session : sessions_) { if (!session->has_view(view)) continue; // Also destroy the entire session if its main view is closing... if (view == session->main_view()) { // ...but only if data is saved or the user confirms closing if (session->data_saved() || (QMessageBox::question(this, tr("Confirmation"), tr("This session contains unsaved data. Close it anyway?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) remove_session(session); break; } else // All other views can be closed at any time as no data will be lost remove_view(view); } } void MainWindow::on_tab_changed(int index) { shared_ptr session = get_tab_session(index); if (session) on_focused_session_changed(session); } void MainWindow::on_tab_close_requested(int index) { shared_ptr session = get_tab_session(index); if (!session) return; if (session->data_saved() || (QMessageBox::question(this, tr("Confirmation"), tr("This session contains unsaved data. Close it anyway?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) remove_session(session); } void MainWindow::on_view_coloured_bg_shortcut() { GlobalSettings settings; bool state = settings.value(GlobalSettings::Key_View_ColouredBG).toBool(); settings.setValue(GlobalSettings::Key_View_ColouredBG, !state); } void MainWindow::on_view_sticky_scrolling_shortcut() { GlobalSettings settings; bool state = settings.value(GlobalSettings::Key_View_StickyScrolling).toBool(); settings.setValue(GlobalSettings::Key_View_StickyScrolling, !state); } void MainWindow::on_view_show_sampling_points_shortcut() { GlobalSettings settings; bool state = settings.value(GlobalSettings::Key_View_ShowSamplingPoints).toBool(); settings.setValue(GlobalSettings::Key_View_ShowSamplingPoints, !state); } void MainWindow::on_view_show_analog_minor_grid_shortcut() { GlobalSettings settings; bool state = settings.value(GlobalSettings::Key_View_ShowAnalogMinorGrid).toBool(); settings.setValue(GlobalSettings::Key_View_ShowAnalogMinorGrid, !state); } void MainWindow::on_settingViewColouredBg_changed(const QVariant new_value) { bool state = new_value.toBool(); for (auto entry : view_docks_) { shared_ptr viewbase = entry.second; // Only trace views have this setting views::trace::View* view = qobject_cast(viewbase.get()); if (view) view->enable_coloured_bg(state); } } void MainWindow::on_settingViewShowSamplingPoints_changed(const QVariant new_value) { bool state = new_value.toBool(); for (auto entry : view_docks_) { shared_ptr viewbase = entry.second; // Only trace views have this setting views::trace::View* view = qobject_cast(viewbase.get()); if (view) view->enable_show_sampling_points(state); } } void MainWindow::on_settingViewShowAnalogMinorGrid_changed(const QVariant new_value) { bool state = new_value.toBool(); for (auto entry : view_docks_) { shared_ptr viewbase = entry.second; // Only trace views have this setting views::trace::View* view = qobject_cast(viewbase.get()); if (view) view->enable_show_analog_minor_grid(state); } } void MainWindow::on_close_current_tab() { int tab = session_selector_.currentIndex(); on_tab_close_requested(tab); } } // namespace pv