From 1aa1b5a7ecc8146f1e79fda186b52a8946b4320d Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 14 Jan 2025 14:34:09 +1000 Subject: [PATCH] Qt: Make main toolbar editable --- src/common/string_util.h | 53 ++++++++++++- src/duckstation-qt/mainwindow.cpp | 120 +++++++++++++++++++++++++++++- src/duckstation-qt/mainwindow.h | 6 ++ src/duckstation-qt/mainwindow.ui | 24 ++---- 4 files changed, 180 insertions(+), 23 deletions(-) diff --git a/src/common/string_util.h b/src/common/string_util.h index 707632e21..578c85482 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -1,9 +1,11 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once #include "types.h" + +#include #include #include #include @@ -320,6 +322,45 @@ void StripWhitespace(std::string* str); [[nodiscard]] std::vector SplitNewString(const std::string_view str, char delimiter, bool skip_empty = true); +/// Returns true if the given string is found in the string list container. +template +static inline bool IsInStringList(const T& list, const std::string_view str) +{ + return std::any_of(std::begin(list), std::end(list), [&str](const auto& it) { return (str == it); }); +} + +/// Adds a string to a string list container. No append is performed if the string already exists. +template +static inline bool AddToStringList(T& list, const std::string_view str) +{ + if (IsInStringList(list, str)) + return false; + + list.emplace_back(str); + return true; +} + +/// Removes a string from a string list container. +template +static inline bool RemoveFromStringList(T& list, const std::string_view str) +{ + bool removed = false; + for (auto iter = std::begin(list); iter != std::end(list);) + { + if (str == *iter) + { + iter = list.erase(iter); + removed = true; + } + else + { + ++iter; + } + } + + return removed; +} + /// Joins a string together using the specified delimiter. template static inline std::string JoinString(const T& start, const T& end, char delimiter) @@ -334,6 +375,11 @@ static inline std::string JoinString(const T& start, const T& end, char delimite return ret; } template +static inline std::string JoinString(const T& list, char delimiter) +{ + return JoinString(std::begin(list), std::end(list), delimiter); +} +template static inline std::string JoinString(const T& start, const T& end, const std::string_view delimiter) { std::string ret; @@ -345,6 +391,11 @@ static inline std::string JoinString(const T& start, const T& end, const std::st } return ret; } +template +static inline std::string JoinString(const T& list, const std::string_view delimiter) +{ + return JoinString(std::begin(list), std::end(list), delimiter); +} /// Replaces all instances of search in subject with replacement. [[nodiscard]] std::string ReplaceAll(const std::string_view subject, const std::string_view search, diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index c8b0729e9..4c5ad5001 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -38,6 +38,7 @@ #include "common/error.h" #include "common/file_system.h" #include "common/log.h" +#include "common/string_util.h" #include #include @@ -63,6 +64,33 @@ LOG_CHANNEL(Host); +static constexpr std::pair s_toolbar_actions[] = { + {"StartFile", &Ui::MainWindow::actionStartFile}, + {"StartBIOS", &Ui::MainWindow::actionStartBios}, + {"StartDisc", &Ui::MainWindow::actionStartDisc}, + {"FullscreenUI", &Ui::MainWindow::actionStartFullscreenUI2}, + {nullptr, nullptr}, + {"PowerOff", &Ui::MainWindow::actionPowerOff}, + {"PowerOffWithoutSaving", &Ui::MainWindow::actionPowerOffWithoutSaving}, + {"Reset", &Ui::MainWindow::actionReset}, + {"Pause", &Ui::MainWindow::actionPause}, + {"ChangeDisc", &Ui::MainWindow::actionChangeDisc}, + {"Cheats", &Ui::MainWindow::actionCheatsToolbar}, + {"Screenshot", &Ui::MainWindow::actionScreenshot}, + {nullptr, nullptr}, + {"LoadState", &Ui::MainWindow::actionLoadState}, + {"SaveState", &Ui::MainWindow::actionSaveState}, + {nullptr, nullptr}, + {"Fullscreen", &Ui::MainWindow::actionFullscreen}, + {"Settings", &Ui::MainWindow::actionSettings2}, + {"ControllerSettings", &Ui::MainWindow::actionControllerSettings}, + {"ControllerPresets", &Ui::MainWindow::actionControllerProfiles}, +}; + +static constexpr const char* DEFAULT_TOOLBAR_ACTIONS = + "StartFile,StartBIOS,FullscreenUI,PowerOff,Reset,Pause,ChangeDisc,Cheats,Screenshot,LoadState,SaveState," + "Fullscreen,Settings,ControllerSettings"; + static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( "MainWindow", "All File Types (*.bin *.img *.iso *.cue *.chd *.cpe *.ecm *.mds *.pbp *.elf *.exe *.psexe *.ps-exe *.psx *.psf " @@ -150,6 +178,7 @@ void MainWindow::initialize() { m_ui.setupUi(this); setupAdditionalUi(); + updateToolbarActions(); connectSignals(); restoreStateFromConfig(); @@ -166,6 +195,11 @@ void MainWindow::initialize() #endif } +QMenu* MainWindow::createPopupMenu() +{ + return nullptr; +} + void MainWindow::reportError(const QString& title, const QString& message) { QMessageBox::critical(this, title, message, QMessageBox::Ok); @@ -1636,7 +1670,6 @@ void MainWindow::setupAdditionalUi() const bool toolbars_locked = Host::GetBaseBoolSettingValue("UI", "LockToolbar", false); m_ui.actionViewLockToolbar->setChecked(toolbars_locked); m_ui.toolBar->setMovable(!toolbars_locked); - m_ui.toolBar->setContextMenuPolicy(Qt::PreventContextMenu); m_game_list_widget = new GameListWidget(getContentParent()); m_game_list_widget->initialize(); @@ -1735,6 +1768,86 @@ void MainWindow::setupAdditionalUi() #endif } +void MainWindow::updateToolbarActions() +{ + const std::string active_buttons_str = + Host::GetBaseStringSettingValue("UI", "ToolbarButtons", DEFAULT_TOOLBAR_ACTIONS); + const std::vector active_buttons = StringUtil::SplitString(active_buttons_str, ','); + + m_ui.toolBar->clear(); + + bool any_items_before_separator = false; + for (const auto& [name, action_ptr] : s_toolbar_actions) + { + if (!name) + { + // separator, but don't insert empty space between them + if (any_items_before_separator) + { + any_items_before_separator = false; + m_ui.toolBar->addSeparator(); + } + + continue; + } + + // enabled? + if (!StringUtil::IsInStringList(active_buttons, name)) + continue; + + // only one of resume/poweroff should be present depending on system state + QAction* action = (m_ui.*action_ptr); + if (action == m_ui.actionPowerOff && !s_system_valid) + action = m_ui.actionResumeLastState; + + m_ui.toolBar->addAction(action); + any_items_before_separator = true; + } +} + +void MainWindow::onToolbarContextMenuRequested(const QPoint& pos) +{ + { + const std::string active_buttons_str = + Host::GetBaseStringSettingValue("UI", "ToolbarButtons", DEFAULT_TOOLBAR_ACTIONS); + std::vector active_buttons = StringUtil::SplitString(active_buttons_str, ','); + bool active_buttons_changed = false; + + QMenu menu; + + for (const auto& [name, action_ptr] : s_toolbar_actions) + { + if (!name) + { + menu.addSeparator(); + continue; + } + + QAction* action = (m_ui.*action_ptr); + QAction* menu_action = menu.addAction(action->text()); + menu_action->setCheckable(true); + menu_action->setChecked(StringUtil::IsInStringList(active_buttons, name)); + connect(menu_action, &QAction::toggled, this, [&active_buttons, &active_buttons_changed, name](bool checked) { + if (checked) + StringUtil::AddToStringList(active_buttons, name); + else + StringUtil::RemoveFromStringList(active_buttons, name); + active_buttons_changed = true; + }); + } + + menu.exec(m_ui.toolBar->mapToGlobal(pos)); + + if (!active_buttons_changed) + return; + + Host::SetBaseStringSettingValue("UI", "ToolbarButtons", StringUtil::JoinString(active_buttons, ',').c_str()); + Host::CommitBaseSettingChanges(); + } + + updateToolbarActions(); +} + void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) { const bool starting_or_running = (starting || running); @@ -1774,7 +1887,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo if (starting_or_running) { - if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff)) + if (m_ui.toolBar->widgetForAction(m_ui.actionResumeLastState)) { m_ui.toolBar->insertAction(m_ui.actionResumeLastState, m_ui.actionPowerOff); m_ui.toolBar->removeAction(m_ui.actionResumeLastState); @@ -1782,7 +1895,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo } else { - if (!m_ui.toolBar->actions().contains(m_ui.actionResumeLastState)) + if (m_ui.toolBar->widgetForAction(m_ui.actionPowerOff)) { m_ui.toolBar->insertAction(m_ui.actionPowerOff, m_ui.actionResumeLastState); m_ui.toolBar->removeAction(m_ui.actionPowerOff); @@ -1984,6 +2097,7 @@ void MainWindow::connectSignals() updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); + connect(m_ui.toolBar, &QToolBar::customContextMenuRequested, this, &MainWindow::onToolbarContextMenuRequested); connect(m_ui.actionStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered); connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index ba41bbb73..303b0f520 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -80,6 +80,9 @@ public: explicit MainWindow(); ~MainWindow(); + /// Disable createPopupMenu(), the menu is bogus. + QMenu* createPopupMenu() override; + /// Performs update check if enabled in settings. void startupUpdateCheck(); @@ -155,6 +158,8 @@ private Q_SLOTS: void onApplicationStateChanged(Qt::ApplicationState state); + void onToolbarContextMenuRequested(const QPoint& pos); + void onStartFileActionTriggered(); void onStartDiscActionTriggered(); void onStartBIOSActionTriggered(); @@ -226,6 +231,7 @@ private: void setupAdditionalUi(); void connectSignals(); + void updateToolbarActions(); void updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode); void updateShortcutActions(bool starting); void updateStatusBarWidgetVisibility(); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 96601e1dd..f116e55a8 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -248,8 +248,8 @@ - - toolBar + + Qt::ContextMenuPolicy::CustomContextMenu @@ -260,29 +260,15 @@ Qt::ToolButtonStyle::ToolButtonTextUnderIcon + + false + TopToolBarArea false - - - - - - - - - - - - - - - - -