diff options
Diffstat (limited to 'opentrack/main-window.cpp')
-rw-r--r-- | opentrack/main-window.cpp | 920 |
1 files changed, 920 insertions, 0 deletions
diff --git a/opentrack/main-window.cpp b/opentrack/main-window.cpp new file mode 100644 index 00000000..b90aa3bd --- /dev/null +++ b/opentrack/main-window.cpp @@ -0,0 +1,920 @@ +/* Copyright (c) 2013-2016, Stanislaw Halik <sthalik@misaki.pl> + + * Permission to use, copy, modify, and/or distribute this + * software for any purpose with or without fee is hereby granted, + * provided that the above copyright notice and this permission + * notice appear in all copies. + */ + +#include "main-window.hpp" +#include "logic/pipeline.hpp" +#include "options/options.hpp" +#include "new_file_dialog.h" +#include "migration/migration.hpp" +#include "compat/check-visible.hpp" +#include "compat/sleep.hpp" +#include "compat/macros.hpp" +#include "compat/library-path.hpp" +#include "compat/math.hpp" +#include "compat/sysexits.hpp" + +#include <algorithm> +#include <utility> + +#include <QMessageBox> +#include <QDesktopServices> +#include <QDir> + +extern "C" const char* const opentrack_version; + +using namespace options::globals; +using namespace options; + +main_window::main_window() : State(OPENTRACK_BASE_PATH + OPENTRACK_LIBRARY_PATH) +{ + ui.setupUi(this); + +#if !defined _WIN32 && !defined __APPLE__ + annoy_if_root(); +#endif + + init_profiles(); + init_buttons(); + init_tray_menu(); + init_dylibs(); + init_shortcuts(); + + setWindowFlags(Qt::MSWindowsFixedSizeDialogHint | windowFlags()); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + adjustSize(); + + if (!start_in_tray()) + { + setVisible(true); + show(); + } + else + setVisible(false); + + connect(&pose_update_timer, &QTimer::timeout, + this, &main_window::show_pose, Qt::DirectConnection); + connect(&det_timer, &QTimer::timeout, + this, &main_window::maybe_start_profile_from_executable); + det_timer.start(1000); +} + +void main_window::init_shortcuts() +{ + register_shortcuts(); + + // ctrl+q exits + connect(&kbd_quit, &QShortcut::activated, this, [this]() { main_window::exit(EXIT_SUCCESS); }, Qt::DirectConnection); + kbd_quit.setEnabled(true); +} + +void main_window::init_dylibs() +{ + using dylib_ptr = Modules::dylib_ptr; + using dylib_list = Modules::dylib_list; + + modules.filters().insert(modules.filters().begin(), + std::make_shared<dylib>("", dylib_type::Filter)); + + for (dylib_ptr& x : modules.trackers()) + ui.iconcomboTrackerSource->addItem(x->icon, x->name, x->module_name); + + for (dylib_ptr& x : modules.protocols()) + ui.iconcomboProtocol->addItem(x->icon, x->name, x->module_name); + + for (dylib_ptr& x : modules.filters()) + ui.iconcomboFilter->addItem(x->icon, x->name, x->module_name); + + connect(ui.iconcomboTrackerSource, &QComboBox::currentTextChanged, + this, [&](const QString&) { pTrackerDialog = nullptr; }); + + connect(ui.iconcomboProtocol, &QComboBox::currentTextChanged, + this, [&](const QString&) { pProtocolDialog = nullptr; }); + + connect(ui.iconcomboFilter, &QComboBox::currentTextChanged, + this, [&](const QString&) { pFilterDialog = nullptr; }); + + connect(&m.tracker_dll, value_::value_changed<QString>(), + this, &main_window::save_modules, + Qt::DirectConnection); + + connect(&m.protocol_dll, value_::value_changed<QString>(), + this, &main_window::save_modules, + Qt::DirectConnection); + + connect(&m.filter_dll, value_::value_changed<QString>(), + this, &main_window::save_modules, + Qt::DirectConnection); + + { + struct list { + dylib_list& libs; + QComboBox* input; + value<QString>& place; + }; + + list types[] { + { modules.trackers(), ui.iconcomboTrackerSource, m.tracker_dll }, + { modules.protocols(), ui.iconcomboProtocol, m.protocol_dll }, + { modules.filters(), ui.iconcomboFilter, m.filter_dll }, + }; + + for (list& type : types) + { + list t = type; + tie_setting(t.place, t.input, + [t](const QString& name) { + auto [ptr, idx] = module_by_name(name, t.libs); + return idx; + }, + [t](int, const QVariant& userdata) { + auto [ptr, idx] = module_by_name(userdata.toString(), t.libs); + if (ptr) + return ptr->module_name; + else + return QString(); + }); + } + } +} + +void main_window::init_profiles() +{ + refresh_profile_list(); + // implicitly created by `ini_directory()' + if (ini_directory().isEmpty() || !QDir(ini_directory()).isReadable()) + die_on_profile_not_writable(); + + set_profile(ini_filename()); + + // profile menu + profile_menu.addAction(tr("Create new empty config"), this, &main_window::create_empty_profile); + profile_menu.addAction(tr("Create new copied config"), this, &main_window::create_copied_profile); + profile_menu.addAction(tr("Open configuration directory"), this, &main_window::open_profile_directory); + ui.profile_button->setMenu(&profile_menu); + + connect(&profile_list_timer, &QTimer::timeout, this, &main_window::refresh_profile_list); + profile_list_timer.start(1000 * 5); + + connect(ui.iconcomboProfile, &QComboBox::currentTextChanged, + this, [this](const QString& x) { main_window::set_profile(x); }); +} + +void main_window::init_tray_menu() +{ + tray_menu.clear(); + + QString display_name(opentrack_version); + if (display_name.startsWith("opentrack-")) + display_name = tr("opentrack") + " " + display_name.mid(sizeof("opentrack-") - 1); + if (display_name.endsWith("-DEBUG")) + display_name.replace(display_name.size() - int(sizeof("DEBUG")), display_name.size(), tr(" (debug)")); + + menu_action_header.setEnabled(false); + menu_action_header.setText(display_name); + menu_action_header.setIcon(QIcon(":/images/opentrack.png")); + tray_menu.addAction(&menu_action_header); + + menu_action_show.setIconVisibleInMenu(true); + menu_action_show.setText(isHidden() ? tr("Show the Octopus") : tr("Hide the Octopus")); + menu_action_show.setIcon(QIcon(":/images/opentrack.png")); + QObject::connect(&menu_action_show, &QAction::triggered, this, [&] { toggle_restore_from_tray(QSystemTrayIcon::Trigger); }); + tray_menu.addAction(&menu_action_show); + + tray_menu.addSeparator(); + + menu_action_tracker.setText(tr("Tracker settings")); + menu_action_tracker.setIcon(QIcon(":/images/tools.png")); + QObject::connect(&menu_action_tracker, &QAction::triggered, this, &main_window::show_tracker_settings); + tray_menu.addAction(&menu_action_tracker); + + menu_action_filter.setText(tr("Filter settings")); + menu_action_filter.setIcon(QIcon(":/images/filter-16.png")); + QObject::connect(&menu_action_filter, &QAction::triggered, this, &main_window::show_filter_settings); + tray_menu.addAction(&menu_action_filter); + + menu_action_proto.setText(tr("Protocol settings")); + menu_action_proto.setIcon(QIcon(":/images/settings16.png")); + QObject::connect(&menu_action_proto, &QAction::triggered, this, &main_window::show_proto_settings); + tray_menu.addAction(&menu_action_proto); + + tray_menu.addSeparator(); + + menu_action_mappings.setIcon(QIcon(":/images/curves.png")); + menu_action_mappings.setText(tr("Mappings")); + QObject::connect(&menu_action_mappings, &QAction::triggered, this, &main_window::show_mapping_window); + tray_menu.addAction(&menu_action_mappings); + + menu_action_options.setIcon(QIcon(":/images/tools.png")); + menu_action_options.setText(tr("Options")); + QObject::connect(&menu_action_options, &QAction::triggered, this, &main_window::show_options_dialog); + tray_menu.addAction(&menu_action_options); + + tray_menu.addSeparator(); + + menu_action_exit.setText(tr("Exit")); + QObject::connect(&menu_action_exit, &QAction::triggered, this, &main_window::exit); + tray_menu.addAction(&menu_action_exit); + + connect(&s.tray_enabled, value_::value_changed<bool>(), + this, &main_window::ensure_tray); + + ensure_tray(); +} + +void main_window::init_buttons() +{ + update_button_state(false, false); + connect(ui.btnEditCurves, &QPushButton::clicked, this, &main_window::show_mapping_window); + connect(ui.btnShortcuts, &QPushButton::clicked, this, &main_window::show_options_dialog); + connect(ui.btnShowEngineControls, &QPushButton::clicked, this, &main_window::show_tracker_settings); + connect(ui.btnShowServerControls, &QPushButton::clicked, this, &main_window::show_proto_settings); + connect(ui.btnShowFilterControls, &QPushButton::clicked, this, &main_window::show_filter_settings); + connect(ui.btnStartTracker, &QPushButton::clicked, this, &main_window::start_tracker_); + connect(ui.btnStopTracker, &QPushButton::clicked, this, &main_window::stop_tracker_); +} + +void main_window::register_shortcuts() +{ + global_shortcuts.reload({ + { s.key_start_tracking1, [this](bool) { start_tracker(); }, true }, + { s.key_start_tracking2, [this](bool) { start_tracker(); }, true }, + + { s.key_stop_tracking1, [this](bool) { stop_tracker(); }, true }, + { s.key_stop_tracking2, [this](bool) { stop_tracker(); }, true }, + + { s.key_toggle_tracking1, [this](bool) { toggle_tracker(); }, true }, + { s.key_toggle_tracking2, [this](bool) { toggle_tracker(); }, true }, + + { s.key_restart_tracking1, [this](bool) { restart_tracker(); }, true }, + { s.key_restart_tracking2, [this](bool) { restart_tracker(); }, true }, + }); + + if (work) + work->reload_shortcuts(); +} + +void main_window::die_on_profile_not_writable() +{ + stop_tracker_(); + + static const QString pad(16, QChar(' ')); + + QMessageBox::critical(this, + tr("The Octopus is sad"), + tr("Check permissions for your .ini directory:\n\n\"%1\"%2\n\nExiting now.").arg(ini_directory(), pad), + QMessageBox::Close, QMessageBox::NoButton); + + exit(EX_OSFILE); +} + +bool main_window::profile_name_from_dialog(QString& ret) +{ + new_file_dialog dlg; + dlg.exec(); + return dlg.is_ok(ret); +} + +main_window::~main_window() +{ + // stupid ps3 eye has LED issues + if (work && ui.video_frame->layout()) + { + hide(); + stop_tracker_(); + close(); + + constexpr int inc = 25, max = 1000; + + for (int k = 0; k < max; k += inc) + { + QEventLoop ev; + ev.processEvents(); + portable::sleep(inc); + } + } + + exit(); +} + +void main_window::save_modules() +{ + m.b->save(); +} + +void main_window::create_empty_profile() +{ + QString name; + if (profile_name_from_dialog(name)) + { + QFile(ini_combine(name)).open(QFile::ReadWrite); + refresh_profile_list(); + + if (profile_list.contains(name)) + { + QSignalBlocker q(ui.iconcomboProfile); + + set_profile(name, false); + mark_profile_as_not_needing_migration(); + } + } +} + +void main_window::create_copied_profile() +{ + const QString cur = ini_pathname(); + QString name; + if (!cur.isEmpty() && profile_name_from_dialog(name)) + { + const QString new_name = ini_combine(name); + (void) QFile::remove(new_name); + QFile::copy(cur, new_name); + + refresh_profile_list(); + + if (profile_list.contains(name)) + { + QSignalBlocker q(ui.iconcomboProfile); + + set_profile(name, false); + mark_profile_as_not_needing_migration(); + } + } + +} + +void main_window::open_profile_directory() +{ + QDesktopServices::openUrl("file:///" + QDir::toNativeSeparators(ini_directory())); +} + +void main_window::refresh_profile_list() +{ + if (work) + return; + + QStringList list = ini_list(); + QString current = ini_filename(); + + if (list == profile_list) + return; + + if (!list.contains(current)) + current = OPENTRACK_DEFAULT_PROFILE; + + profile_list = list; + + static const QIcon icon(":/images/settings16.png"); + + QSignalBlocker l(ui.iconcomboProfile); + + ui.iconcomboProfile->clear(); + ui.iconcomboProfile->addItems(list); + + for (int i = 0; i < list.size(); i++) + ui.iconcomboProfile->setItemIcon(i, icon); + + ui.iconcomboProfile->setCurrentText(current); +} + + + +void main_window::update_button_state(bool running, bool inertialp) +{ + bool not_running = !running; + ui.iconcomboProfile->setEnabled(not_running); + ui.btnStartTracker->setEnabled(not_running); + ui.btnStopTracker->setEnabled(running); + ui.iconcomboProtocol->setEnabled(not_running); + ui.iconcomboFilter->setEnabled(not_running); + ui.iconcomboTrackerSource->setEnabled(not_running); + ui.profile_button->setEnabled(not_running); + ui.video_frame_label->setVisible(not_running || inertialp); + if(not_running) + { + ui.video_frame_label->setPixmap(QPixmap(":/images/tracking-not-started.png")); + } + else { + ui.video_frame_label->setPixmap(QPixmap(":/images/no-feed.png")); + } +} + +void main_window::start_tracker_() +{ + if (work) + return; + + work = std::make_shared<Work>(pose, ev, ui.video_frame, current_tracker(), current_protocol(), current_filter()); + + if (!work->is_ok()) + { + work = nullptr; + return; + } + + { + double p[6] = {0,0,0, 0,0,0}; + show_pose_(p, p); + } + + if (pTrackerDialog) + pTrackerDialog->register_tracker(work->libs.pTracker.get()); + + if (pFilterDialog) + pFilterDialog->register_filter(work->libs.pFilter.get()); + + if (pProtocolDialog) + pProtocolDialog->register_protocol(work->libs.pProtocol.get()); + + pose_update_timer.start(50); + + // NB check valid since SelectedLibraries ctor called + // trackers take care of layout state updates + const bool is_inertial = ui.video_frame->layout() == nullptr; + update_button_state(true, is_inertial); + + ui.btnStopTracker->setFocus(); +} + +void main_window::stop_tracker_() +{ + if (!work) + return; + + force_is_visible(true); + with_tracker_teardown sentinel; + + pose_update_timer.stop(); + ui.pose_display->present(0,0,0, 0,0,0); + + if (pTrackerDialog) + pTrackerDialog->unregister_tracker(); + + if (pProtocolDialog) + pProtocolDialog->unregister_protocol(); + + if (pFilterDialog) + pFilterDialog->unregister_filter(); + + work = nullptr; + + { + double p[6] {}; + show_pose_(p, p); + } + + update_button_state(false, false); + set_title(); + ui.btnStartTracker->setFocus(); +} + +void main_window::show_pose_(const double* mapped, const double* raw) +{ + ui.pose_display->present(mapped[Yaw], mapped[Pitch], -mapped[Roll], + mapped[TX], mapped[TY], mapped[TZ]); + + QLCDNumber* raw_[] = { + ui.raw_x, ui.raw_y, ui.raw_z, + ui.raw_yaw, ui.raw_pitch, ui.raw_roll, + }; + + QLCDNumber* mapped_[] = { + ui.pose_x, ui.pose_y, ui.pose_z, + ui.pose_yaw, ui.pose_pitch, ui.pose_roll, + }; + + for (int k = 0; k < 6; k++) + { + raw_[k]->display(iround(raw[k])); + mapped_[k]->display(iround(mapped[k])); + } + + QString game_title; + if (work && work->libs.pProtocol) + game_title = work->libs.pProtocol->game_name(); + set_title(game_title); +} + +void main_window::set_title(const QString& game_title) +{ + static const QString version{opentrack_version}; + static const QString sep { tr(" :: ") }; + static const QString pat1{ version + sep + "%1" + sep + "%2" }; + static const QString pat2{ version + sep + "%1" }; + + const QString current = ini_filename(); + + if (game_title.isEmpty()) + setWindowTitle(pat2.arg(current)); + else + setWindowTitle(pat1.arg(current, game_title)); +} + +void main_window::show_pose() +{ + set_is_visible(*this); + + if (mapping_widget) + mapping_widget->refresh_tab(); + + if (!check_is_visible()) + return; + + double mapped[6], raw[6]; + + work->pipeline_.raw_and_mapped_pose(mapped, raw); + + show_pose_(mapped, raw); +} + +static void show_window(QWidget& d, bool fresh) +{ + if (fresh) + { + d.setWindowFlags(Qt::MSWindowsFixedSizeDialogHint | d.windowFlags()); + d.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + d.show(); + d.adjustSize(); + d.raise(); + } + else + { + d.show(); + d.raise(); + } +} + +template<typename t, typename F> +static bool mk_window_common(std::unique_ptr<t>& d, F&& fun) +{ + bool fresh = false; + + if (!d) + d = fun(), fresh = !!d; + + if (d) + show_window(*d, fresh); + + return fresh; +} + +template<typename t, typename... Args> +static bool mk_window(std::unique_ptr<t>& place, Args&&... params) +{ + return mk_window_common(place, [&] { + return std::make_unique<t>(params...); + }); +} + +template<typename t> +static bool mk_dialog(std::unique_ptr<t>& place, const std::shared_ptr<dylib>& lib) +{ + using u = std::unique_ptr<t>; + + return mk_window_common(place, [&] { + if (lib && lib->Dialog) + return u{ (t*)lib->Dialog() }; + else + return u{}; + }); +} + +void main_window::show_tracker_settings() +{ + if (mk_dialog(pTrackerDialog, current_tracker()) && work && work->libs.pTracker) + pTrackerDialog->register_tracker(work->libs.pTracker.get()); + if (pTrackerDialog) + QObject::connect(pTrackerDialog.get(), &ITrackerDialog::closing, + this, [this] { pTrackerDialog = nullptr; }); +} + +void main_window::show_proto_settings() +{ + if (mk_dialog(pProtocolDialog, current_protocol()) && work && work->libs.pProtocol) + pProtocolDialog->register_protocol(work->libs.pProtocol.get()); + if (pProtocolDialog) + QObject::connect(pProtocolDialog.get(), &IProtocolDialog::closing, + this, [this] { pProtocolDialog = nullptr; }); +} + +void main_window::show_filter_settings() +{ + if (mk_dialog(pFilterDialog, current_filter()) && work && work->libs.pFilter) + pFilterDialog->register_filter(work->libs.pFilter.get()); + if (pFilterDialog) + QObject::connect(pFilterDialog.get(), &IFilterDialog::closing, + this, [this] { pFilterDialog = nullptr; }); +} + +void main_window::show_options_dialog() +{ + if (mk_window(options_widget, [&](bool flag) { set_keys_enabled(!flag); })) + { + // XXX this should logically connect to a bundle + // also doesn't work when switching profiles with options dialog open + // move shortcuts to a separate bundle and add a migration -sh 20180218 + connect(options_widget.get(), &options_dialog::closing, + this, &main_window::register_shortcuts); + } +} + +void main_window::show_mapping_window() +{ + mk_window(mapping_widget, pose); +} + +void main_window::exit(int status) +{ + // don't use std::call_once here, leads to freeze in Microsoft's CRT + // this function never needs reentrancy anyway + + // this is probably harmless, but better safe than sorry + if (exiting_already) + return; + exiting_already = true; + + qDebug() << "opentrack: exiting"; + + if (tray) + tray->hide(); + tray = nullptr; + + //close(); + QApplication::setQuitOnLastWindowClosed(true); + QApplication::exit(status); +} + +void main_window::set_profile(const QString& new_name_, bool migrate) +{ + QSignalBlocker b(ui.iconcomboProfile); + + QString new_name = new_name_; + + if (!profile_list.contains(new_name)) + { + new_name = OPENTRACK_DEFAULT_PROFILE; + if (!profile_list.contains(new_name)) + migrate = false; + } + + const bool status = new_name != ini_filename(); + + if (status) + set_profile_in_registry(new_name); + + using bundler = options::detail::bundler; + + bundler::reload_no_notify(); + + if (migrate) + // migrations are for config layout changes and other user-visible + // incompatibilities in future versions + run_migrations(); + else + mark_profile_as_not_needing_migration(); + + bundler::notify(); + + set_title(); + + if (status) + ui.iconcomboProfile->setCurrentText(new_name); +} + +void main_window::ensure_tray() +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + { + QApplication::setQuitOnLastWindowClosed(true); + return; + } + + if (s.tray_enabled) + { + if (!tray) + { + tray = std::make_unique<QSystemTrayIcon>(this); + tray->setIcon(QIcon(":/images/opentrack.png")); + tray->setContextMenu(&tray_menu); + tray->show(); + + connect(tray.get(), + &QSystemTrayIcon::activated, + this, + &main_window::toggle_restore_from_tray); + } + + QApplication::setQuitOnLastWindowClosed(false); + } + else + { + if (!isVisible()) + { + show(); + setVisible(true); + + raise(); // for OSX + activateWindow(); // for Windows + } + + if (tray) + tray->hide(); + tray = nullptr; + + QApplication::setQuitOnLastWindowClosed(true); + } +} + +void main_window::toggle_restore_from_tray(QSystemTrayIcon::ActivationReason e) +{ + switch (e) + { + // if we enable double click also then it causes + // toggle back to the original state + //case QSystemTrayIcon::DoubleClick: + case QSystemTrayIcon::Trigger: // single click + break; + default: + return; + } + + ensure_tray(); + + const bool is_minimized = isHidden() || !tray_enabled(); + + menu_action_show.setText(!isHidden() ? tr("Show the Octopus") : tr("Hide the Octopus")); + + setVisible(is_minimized); + setHidden(!is_minimized); + + setWindowState(is_minimized ? windowState() & ~Qt::WindowMinimized : Qt::WindowNoState); + + if (is_minimized) + { + raise(); // for OSX + activateWindow(); // for Windows + } + else + { + lower(); + clearFocus(); + } +} + +bool main_window::maybe_hide_to_tray(QEvent* e) +{ + if (e->type() == QEvent::WindowStateChange && + (windowState() & Qt::WindowMinimized) && + tray_enabled()) + { + e->accept(); + ensure_tray(); + hide(); + + return true; + } + + return false; +} + +void main_window::closeEvent(QCloseEvent*) +{ + exit(); +} + +void main_window::maybe_start_profile_from_executable() +{ + if (!work) + { + QString profile; + if (det.profile_to_start(profile) && profile_list.contains(profile)) + { + set_profile(profile); + start_tracker_(); + } + } + else + { + if (det.should_stop()) + stop_tracker_(); + } +} + +void main_window::set_keys_enabled(bool flag) +{ + if (!flag) + { + if (work) + work->sc.reload({}); + global_shortcuts.reload({}); + } + else + register_shortcuts(); +} + +void main_window::changeEvent(QEvent* e) +{ + if (!maybe_hide_to_tray(e)) + e->ignore(); +} + +bool main_window::event(QEvent* event) +{ + using t = QEvent::Type; + + if (work) + { + switch (event->type()) + { + case t::Hide: + case t::WindowActivate: + case t::WindowDeactivate: + case t::WindowStateChange: + case t::FocusIn: + set_is_visible(*this, true); + break; + default: + break; + } + } + return QMainWindow::event(event); +} + +bool main_window::tray_enabled() +{ + return s.tray_enabled && QSystemTrayIcon::isSystemTrayAvailable(); +} + +bool main_window::start_in_tray() +{ + return tray_enabled() && s.tray_start; +} + +void main_window::set_profile_in_registry(const QString &profile) +{ + with_global_settings_object([&](QSettings& s) { + s.setValue(OPENTRACK_PROFILE_FILENAME_KEY, profile); + }); +} + +void main_window::restart_tracker_() +{ + qDebug() << "restart tracker"; + + stop_tracker_(); + start_tracker_(); +} + +void main_window::toggle_tracker_() +{ + qDebug() << "toggle tracker"; + + if (work) + stop_tracker_(); + else + start_tracker_(); +} + +#if !defined _WIN32 +# include <unistd.h> +void main_window::annoy_if_root() +{ + if (geteuid() == 0) + { + struct lst { + QString caption; + QString msg; + int sleep_ms; + }; + + const lst list[] = { + { + tr("Running as root is bad"), + tr("Do not run as root. Set correct device node permissions."), + 1000, + }, + { + tr("Running as root is bad, seriously"), + tr("Do not run as root. I'll keep whining at every startup."), + 3000, + }, + { + tr("Be annoyed, comprehensively."), + tr("Don't run as root to remove these annoying messages."), + 0 + } + }; + + for (const auto& x : list) + { + QMessageBox::critical(this, x.caption, x.msg, QMessageBox::Ok); + portable::sleep(x.sleep_ms); + } + } +} +#endif |