mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
launcher: add resizable game grid/improve controller setup
This commit is contained in:
@@ -204,6 +204,72 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="grid_size_slider">
|
||||
<property name="maximumSize">
|
||||
<size><width>120</width><height>16777215</height></size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Game image size</string>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Game image size</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>320</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>16</number>
|
||||
</property>
|
||||
<property name="pageStep">
|
||||
<number>32</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>224</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="gridSizeSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size><width>8</width><height>0</height></size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="grid_view_button">
|
||||
<property name="minimumSize">
|
||||
<size><width>36</width><height>36</height></size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Grid view</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Toggle grid view</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size><width>32</width><height>32</height></size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_line_edit">
|
||||
<property name="sizePolicy">
|
||||
@@ -238,50 +304,53 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="GameListTreeWidget" name="cfgs_list">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game</string>
|
||||
<widget class="QStackedWidget" name="game_views">
|
||||
<widget class="GameListTreeWidget" name="cfgs_list">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Serial</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Firmware Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Size</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Status</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Comments</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Serial</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Firmware Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Size</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Status</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Comments</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QListView" name="cfgs_grid"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<g fill="none" stroke="#000000" stroke-width="2" stroke-linejoin="round">
|
||||
<rect x="5" y="5" width="8" height="8" rx="1"/>
|
||||
<rect x="19" y="5" width="8" height="8" rx="1"/>
|
||||
<rect x="5" y="19" width="8" height="8" rx="1"/>
|
||||
<rect x="19" y="19" width="8" height="8" rx="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -12,9 +12,9 @@
|
||||
|
||||
class ConfigurationItem;
|
||||
class CompatibilityDatabase;
|
||||
class ControllerPreview;
|
||||
class QEvent;
|
||||
class QTreeWidgetItem;
|
||||
class QPoint;
|
||||
|
||||
namespace Ui {
|
||||
class ConfigurationListWidget;
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
|
||||
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
|
||||
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
|
||||
void SetControllerPreview(ControllerPreview* preview) { m_controller_preview = preview; }
|
||||
|
||||
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
|
||||
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
|
||||
@@ -65,13 +66,13 @@ protected slots:
|
||||
void edit_global_settings();
|
||||
void edit_input_mapping();
|
||||
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
|
||||
void show_context_menu(const QPoint& pos);
|
||||
void open_game_folder();
|
||||
void remove_save_data();
|
||||
void filter_configurations(const QString& text);
|
||||
|
||||
private:
|
||||
void SelectItem(QTreeWidgetItem* witem);
|
||||
void ShowContextMenu(QTreeWidgetItem* witem);
|
||||
void ApplyCompatibility();
|
||||
void UpdateToolbarIcons();
|
||||
void ImportGameSettings(QWidget* parent);
|
||||
@@ -87,6 +88,7 @@ private:
|
||||
Configuration m_global_info;
|
||||
QMap<QString, Configuration*> m_custom_infos;
|
||||
CompatibilityDatabase* m_compatibility = nullptr;
|
||||
ControllerPreview* m_controller_preview = nullptr;
|
||||
};
|
||||
|
||||
#endif // CONFIGURATION_LIST_WIDGET_H
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#ifndef CONTROLLER_LIGHTBAR_H
|
||||
#define CONTROLLER_LIGHTBAR_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
class ControllerLightbar final: public QObject {
|
||||
public:
|
||||
explicit ControllerLightbar(QObject* parent = nullptr);
|
||||
~ControllerLightbar() override;
|
||||
|
||||
void SetColor(const QString& color);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void Refresh();
|
||||
void ApplyColor(SDL_Gamepad* gamepad) const;
|
||||
|
||||
QTimer m_timer;
|
||||
QHash<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_LIGHTBAR_H
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef CONTROLLER_PREVIEW_H
|
||||
#define CONTROLLER_PREVIEW_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QPointF>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
class ControllerPreview final: public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ControllerPreview(QObject* parent = nullptr);
|
||||
~ControllerPreview() override;
|
||||
|
||||
void SetColor(const QString& color);
|
||||
void SetPreviewEnabled(bool enabled);
|
||||
void Stop();
|
||||
void SetSuspended(bool suspended);
|
||||
|
||||
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
|
||||
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
|
||||
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
|
||||
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
|
||||
[[nodiscard]] const QString& Error() const { return m_error; }
|
||||
|
||||
signals:
|
||||
void StateChanged();
|
||||
|
||||
private:
|
||||
void UpdateRunning();
|
||||
void OpenGamepad(SDL_JoystickID id);
|
||||
void Refresh();
|
||||
void ApplyColor(SDL_Gamepad* gamepad) const;
|
||||
|
||||
QTimer m_timer;
|
||||
QMap<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
|
||||
QColor m_color;
|
||||
bool m_preview_enabled = false;
|
||||
bool m_suspended = false;
|
||||
QSet<QString> m_pressed;
|
||||
QPointF m_left_stick;
|
||||
QPointF m_right_stick;
|
||||
QString m_device_name;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_PREVIEW_H
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
|
||||
#define LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
|
||||
|
||||
#include <QPointF>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QTransform>
|
||||
#include <QWidget>
|
||||
|
||||
class DualSenseWidget final: public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DualSenseWidget(QWidget* parent = nullptr);
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
void SetPressed(const QSet<QString>& pressed);
|
||||
void SetSelected(const QString& selected);
|
||||
void SetSticks(const QPointF& left, const QPointF& right);
|
||||
|
||||
signals:
|
||||
void ControlClicked(const QString& id);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] QTransform ViewTransform() const;
|
||||
[[nodiscard]] QString HitTest(const QPointF& position) const;
|
||||
|
||||
QSet<QString> m_pressed;
|
||||
QString m_selected;
|
||||
QString m_hovered;
|
||||
QPointF m_left_stick;
|
||||
QPointF m_right_stick;
|
||||
};
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_ */
|
||||
@@ -2,32 +2,38 @@
|
||||
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QStringList>
|
||||
|
||||
class ControllerPreview;
|
||||
class DualSenseWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QDoubleSpinBox;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QWidget;
|
||||
|
||||
class InputMappingDialog final: public QDialog {
|
||||
public:
|
||||
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
|
||||
explicit InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
|
||||
QWidget* parent = nullptr);
|
||||
~InputMappingDialog() override;
|
||||
|
||||
[[nodiscard]] QStringList Mapping() const;
|
||||
|
||||
private:
|
||||
void ChangeBinding();
|
||||
void ClearBinding();
|
||||
void ChangeBinding(const QString& id);
|
||||
void RestoreDefaults();
|
||||
void SetBinding(QTreeWidgetItem* item, const QString& binding);
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
void UpdateHighlights();
|
||||
|
||||
QTreeWidget* m_bindings = nullptr;
|
||||
QPushButton* m_change_button = nullptr;
|
||||
QPushButton* m_clear_button = nullptr;
|
||||
QDoubleSpinBox* m_sensitivity = nullptr;
|
||||
bool m_custom_bindings = false;
|
||||
ControllerPreview* m_preview;
|
||||
DualSenseWidget* m_controller = nullptr;
|
||||
QHash<QString, QString> m_bindings;
|
||||
QHash<QString, QPushButton*> m_buttons;
|
||||
QString m_selected;
|
||||
QLabel* m_status = nullptr;
|
||||
QDoubleSpinBox* m_sensitivity = nullptr;
|
||||
bool m_custom_bindings = false;
|
||||
};
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<file alias="global-settings.svg">icons/global-settings.svg</file>
|
||||
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
|
||||
<file alias="refresh.svg">icons/refresh.svg</file>
|
||||
<file alias="grid-view.svg">icons/grid-view.svg</file>
|
||||
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
|
||||
<file alias="trophy.svg">icons/trophy.svg</file>
|
||||
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QJsonParseError>
|
||||
#include <QKeySequence>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
@@ -46,7 +47,9 @@
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSlider>
|
||||
#include <QStyle>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QUrl>
|
||||
@@ -141,6 +144,26 @@ static QStringList SettingsStringList(const QVariant& value) {
|
||||
return text.isEmpty() ? QStringList() : QStringList({text});
|
||||
}
|
||||
|
||||
class GameGridDelegate: public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex&) const override {
|
||||
return option.decorationSize + QSize(16, 24 + option.fontMetrics.lineSpacing() * 2);
|
||||
}
|
||||
|
||||
protected:
|
||||
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override {
|
||||
const auto image_size = option->decorationSize;
|
||||
QStyledItemDelegate::initStyleOption(option, index);
|
||||
option->decorationSize = image_size;
|
||||
option->decorationPosition = QStyleOptionViewItem::Top;
|
||||
option->decorationAlignment = Qt::AlignCenter;
|
||||
option->displayAlignment = Qt::AlignHCenter | Qt::AlignTop;
|
||||
option->features |= QStyleOptionViewItem::WrapText;
|
||||
}
|
||||
};
|
||||
|
||||
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->setAlternatingRowColors(false);
|
||||
ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
@@ -152,6 +175,21 @@ static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
ui->cfgs_list->header()->setHighlightSections(false);
|
||||
ui->cfgs_list->header()->setStretchLastSection(true);
|
||||
|
||||
// Both presentations use the same items, icons and current game.
|
||||
ui->cfgs_grid->setModel(ui->cfgs_list->model());
|
||||
ui->cfgs_grid->setSelectionModel(ui->cfgs_list->selectionModel());
|
||||
ui->cfgs_grid->setViewMode(QListView::IconMode);
|
||||
ui->cfgs_grid->setMovement(QListView::Static);
|
||||
ui->cfgs_grid->setResizeMode(QListView::Adjust);
|
||||
ui->cfgs_grid->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
ui->cfgs_grid->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
ui->cfgs_grid->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui->cfgs_grid->setSpacing(8);
|
||||
ui->cfgs_grid->setUniformItemSizes(true);
|
||||
ui->cfgs_grid->setWordWrap(true);
|
||||
ui->cfgs_grid->setItemDelegate(new GameGridDelegate(ui->cfgs_grid));
|
||||
ui->cfgs_grid->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
}
|
||||
|
||||
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
|
||||
@@ -252,7 +290,27 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
|
||||
&ConfigurationListWidget::list_itemDoubleClicked);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::customContextMenuRequested, this,
|
||||
&ConfigurationListWidget::show_context_menu);
|
||||
[this](const QPoint& pos) { ShowContextMenu(m_ui->cfgs_list->itemAt(pos)); });
|
||||
connect(m_ui->cfgs_grid, &QListView::doubleClicked, this, [this](const QModelIndex& index) {
|
||||
list_itemDoubleClicked(m_ui->cfgs_list->itemFromIndex(index), GAME_NAME_COLUMN);
|
||||
});
|
||||
connect(m_ui->cfgs_grid, &QListView::customContextMenuRequested, this,
|
||||
[this](const QPoint& pos) {
|
||||
ShowContextMenu(m_ui->cfgs_list->itemFromIndex(m_ui->cfgs_grid->indexAt(pos)));
|
||||
});
|
||||
connect(m_ui->grid_view_button, &QToolButton::toggled, this, [this](bool grid) {
|
||||
m_ui->grid_size_slider->setVisible(grid);
|
||||
auto* view = grid ? static_cast<QAbstractItemView*>(m_ui->cfgs_grid) : m_ui->cfgs_list;
|
||||
m_ui->game_views->setCurrentWidget(view);
|
||||
view->setCurrentIndex(view->currentIndex().siblingAtColumn(GAME_NAME_COLUMN));
|
||||
view->scrollTo(view->currentIndex());
|
||||
view->setFocus();
|
||||
});
|
||||
connect(m_ui->grid_view_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::WriteSettings);
|
||||
connect(m_ui->cfgs_list->model(), &QAbstractItemModel::layoutChanged, this, [this]() {
|
||||
filter_configurations(m_ui->search_line_edit->text());
|
||||
});
|
||||
connect(m_ui->search_line_edit, &QLineEdit::textChanged, this,
|
||||
&ConfigurationListWidget::filter_configurations);
|
||||
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
|
||||
@@ -261,6 +319,19 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
|
||||
|
||||
ReadSettings();
|
||||
m_ui->grid_size_slider->setVisible(m_ui->grid_view_button->isChecked());
|
||||
const auto resize_grid = [this](int width) {
|
||||
m_ui->cfgs_grid->setIconSize(QSize(width, width * 9 / 16));
|
||||
};
|
||||
resize_grid(m_ui->grid_size_slider->value());
|
||||
connect(m_ui->grid_size_slider, &QSlider::valueChanged, this, [this, resize_grid](int width) {
|
||||
resize_grid(width);
|
||||
if (!m_ui->grid_size_slider->isSliderDown()) {
|
||||
WriteSettings();
|
||||
}
|
||||
});
|
||||
connect(m_ui->grid_size_slider, &QSlider::sliderReleased, this,
|
||||
&ConfigurationListWidget::WriteSettings);
|
||||
if (m_compatibility->IsLocal()) {
|
||||
m_compatibility->Load();
|
||||
}
|
||||
@@ -300,6 +371,7 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
|
||||
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
|
||||
set_icon(m_ui->delete_button, QStringLiteral(":/icons/remove-configuration.svg"));
|
||||
set_icon(m_ui->trophy_overview_button, QStringLiteral(":/icons/trophy.svg"));
|
||||
set_icon(m_ui->grid_view_button, QStringLiteral(":/icons/grid-view.svg"));
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::WriteSettings() {
|
||||
@@ -323,6 +395,8 @@ void ConfigurationListWidget::WriteSettings() {
|
||||
s->beginGroup(CONF_LAUNCHER);
|
||||
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
|
||||
s->setValue(CONF_GAME_DIRS, m_game_dirs);
|
||||
s->setValue("grid_view", m_ui->grid_view_button->isChecked());
|
||||
s->setValue("grid_icon_width", m_ui->grid_size_slider->value());
|
||||
s->remove(CONF_GAME_DIR);
|
||||
s->endGroup();
|
||||
|
||||
@@ -363,6 +437,9 @@ void ConfigurationListWidget::ReadSettings() {
|
||||
ConfigurationEditDialog::ReadSettings(*s);
|
||||
|
||||
s->beginGroup(CONF_LAUNCHER);
|
||||
m_ui->grid_view_button->setChecked(s->value("grid_view", false).toBool());
|
||||
m_ui->grid_size_slider->setValue(
|
||||
s->value("grid_icon_width", m_ui->grid_size_slider->value()).toInt());
|
||||
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIRS)));
|
||||
if (m_game_dirs.isEmpty()) {
|
||||
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
|
||||
@@ -959,7 +1036,7 @@ void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_input_mapping() {
|
||||
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
|
||||
InputMappingDialog dialog(m_global_info.host_input_mapping, m_controller_preview, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
m_global_info.host_input_mapping = dialog.Mapping();
|
||||
WriteSettings();
|
||||
@@ -1097,6 +1174,7 @@ void ConfigurationListWidget::filter_configurations(const QString& text) {
|
||||
item->text(GAME_NAME_COLUMN).contains(query, Qt::CaseInsensitive) ||
|
||||
item->text(GAME_SERIAL_COLUMN).contains(query, Qt::CaseInsensitive);
|
||||
item->setHidden(!match);
|
||||
m_ui->cfgs_grid->setRowHidden(item_index, !match);
|
||||
|
||||
if (match && item == m_selected_item) {
|
||||
selection_is_shown = true;
|
||||
@@ -1137,8 +1215,8 @@ void ConfigurationListWidget::list_itemDoubleClicked(QTreeWidgetItem* witem, int
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->itemAt(pos));
|
||||
void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
|
||||
auto* item = static_cast<ConfigurationItem*>(witem);
|
||||
|
||||
if (item != nullptr) {
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#include "controllerLightbar.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
ControllerLightbar::ControllerLightbar(QObject* parent): QObject(parent) {
|
||||
m_timer.setInterval(100);
|
||||
connect(&m_timer, &QTimer::timeout, this, &ControllerLightbar::Refresh);
|
||||
}
|
||||
|
||||
ControllerLightbar::~ControllerLightbar() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void ControllerLightbar::SetColor(const QString& color) {
|
||||
const QColor parsed(color);
|
||||
if (!parsed.isValid()) {
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
if (m_color == parsed) {
|
||||
return;
|
||||
}
|
||||
if (!m_timer.isActive()) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
qWarning() << "Cannot initialize controller color preview:" << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
m_timer.start();
|
||||
}
|
||||
m_color = parsed;
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
ApplyColor(gamepad);
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ControllerLightbar::Stop() {
|
||||
if (!m_timer.isActive()) {
|
||||
return;
|
||||
}
|
||||
m_timer.stop();
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
m_gamepads.clear();
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
m_color = {};
|
||||
}
|
||||
|
||||
void ControllerLightbar::Refresh() {
|
||||
// Bluetooth DualSense may defer an accepted LED command until its connection
|
||||
// animation ends. Polling events also delivers those deferred commands.
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED && !m_gamepads.contains(event.gdevice.which)) {
|
||||
auto* gamepad = SDL_OpenGamepad(event.gdevice.which);
|
||||
if (gamepad == nullptr) {
|
||||
qWarning() << "Cannot open controller color preview:" << SDL_GetError();
|
||||
continue;
|
||||
}
|
||||
if (SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
|
||||
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) {
|
||||
m_gamepads.insert(event.gdevice.which, gamepad);
|
||||
ApplyColor(gamepad);
|
||||
} else {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerLightbar::ApplyColor(SDL_Gamepad* gamepad) const {
|
||||
if (!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
|
||||
static_cast<Uint8>(m_color.green()),
|
||||
static_cast<Uint8>(m_color.blue()))) {
|
||||
qWarning() << "Cannot set controller LED:" << SDL_GetError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "controllerPreview.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr struct {
|
||||
SDL_GamepadButton button;
|
||||
const char* control;
|
||||
} BUTTONS[] = {
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, "Cross"}, {SDL_GAMEPAD_BUTTON_EAST, "Circle"},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, "Square"}, {SDL_GAMEPAD_BUTTON_NORTH, "Triangle"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, "Up"}, {SDL_GAMEPAD_BUTTON_DPAD_DOWN, "Down"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "Left"}, {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "Right"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "L1"}, {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "R1"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "L3"}, {SDL_GAMEPAD_BUTTON_RIGHT_STICK, "R3"},
|
||||
{SDL_GAMEPAD_BUTTON_START, "Options"},
|
||||
};
|
||||
|
||||
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
|
||||
const int value = SDL_GetGamepadAxis(gamepad, axis);
|
||||
return value / (value < 0 ? 32768.0 : 32767.0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerPreview::ControllerPreview(QObject* parent): QObject(parent) {
|
||||
m_timer.setInterval(100);
|
||||
connect(&m_timer, &QTimer::timeout, this, &ControllerPreview::Refresh);
|
||||
}
|
||||
|
||||
ControllerPreview::~ControllerPreview() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void ControllerPreview::SetColor(const QString& color) {
|
||||
const QColor parsed(color);
|
||||
if (m_color == parsed && m_timer.isActive()) {
|
||||
return;
|
||||
}
|
||||
m_color = parsed;
|
||||
UpdateRunning();
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
ApplyColor(gamepad);
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerPreview::SetPreviewEnabled(bool enabled) {
|
||||
m_preview_enabled = enabled;
|
||||
m_timer.setInterval(enabled ? 16 : 100);
|
||||
UpdateRunning();
|
||||
}
|
||||
|
||||
void ControllerPreview::Stop() {
|
||||
m_preview_enabled = false;
|
||||
m_color = {};
|
||||
UpdateRunning();
|
||||
}
|
||||
|
||||
void ControllerPreview::SetSuspended(bool suspended) {
|
||||
if (m_suspended != suspended) {
|
||||
m_suspended = suspended;
|
||||
UpdateRunning();
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerPreview::UpdateRunning() {
|
||||
if (!m_suspended && (m_preview_enabled || m_color.isValid())) {
|
||||
if (m_timer.isActive()) {
|
||||
return;
|
||||
}
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
m_error = QString::fromUtf8(SDL_GetError());
|
||||
qWarning() << "Cannot initialize controller preview:" << m_error;
|
||||
emit StateChanged();
|
||||
return;
|
||||
}
|
||||
m_error.clear();
|
||||
int count = 0;
|
||||
auto* ids = SDL_GetGamepads(&count);
|
||||
for (int index = 0; index < count; ++index) {
|
||||
OpenGamepad(ids[index]);
|
||||
}
|
||||
SDL_free(ids);
|
||||
m_timer.start();
|
||||
Refresh();
|
||||
emit StateChanged();
|
||||
return;
|
||||
}
|
||||
if (m_timer.isActive()) {
|
||||
m_timer.stop();
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
m_gamepads.clear();
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
m_pressed.clear();
|
||||
m_left_stick = {};
|
||||
m_right_stick = {};
|
||||
m_device_name.clear();
|
||||
m_error = m_suspended ? tr("Preview is paused while a game is running.") : QString();
|
||||
emit StateChanged();
|
||||
}
|
||||
|
||||
void ControllerPreview::OpenGamepad(SDL_JoystickID id) {
|
||||
if (!m_gamepads.contains(id)) {
|
||||
if (auto* gamepad = SDL_OpenGamepad(id); gamepad != nullptr) {
|
||||
m_gamepads.insert(id, gamepad);
|
||||
ApplyColor(gamepad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerPreview::Refresh() {
|
||||
// One SDL event pump serves both the live diagram and lightbar preview.
|
||||
// It also delivers deferred Bluetooth DualSense LED commands.
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
OpenGamepad(event.gdevice.which);
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> pressed;
|
||||
QPointF left;
|
||||
QPointF right;
|
||||
QString name;
|
||||
if (!m_gamepads.isEmpty()) {
|
||||
auto* gamepad = m_gamepads.first();
|
||||
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
|
||||
for (const auto& binding: BUTTONS) {
|
||||
if (SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
pressed.insert(QString::fromLatin1(binding.control));
|
||||
}
|
||||
}
|
||||
const auto stick = [&](SDL_GamepadAxis x, SDL_GamepadAxis y, const char* prefix) {
|
||||
QPointF position(Axis(gamepad, x), Axis(gamepad, y));
|
||||
if (std::hypot(position.x(), position.y()) < 0.15) {
|
||||
position = {};
|
||||
}
|
||||
const auto id = QString::fromLatin1(prefix);
|
||||
if (position.x() < -0.25) pressed.insert(id + QStringLiteral("Left"));
|
||||
if (position.x() > 0.25) pressed.insert(id + QStringLiteral("Right"));
|
||||
if (position.y() < -0.25) pressed.insert(id + QStringLiteral("Up"));
|
||||
if (position.y() > 0.25) pressed.insert(id + QStringLiteral("Down"));
|
||||
return position;
|
||||
};
|
||||
left = stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, "LeftStick");
|
||||
right = stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, "RightStick");
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.05) {
|
||||
pressed.insert(QStringLiteral("L2"));
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.05) {
|
||||
pressed.insert(QStringLiteral("R2"));
|
||||
}
|
||||
bool touching = false;
|
||||
if (SDL_GetNumGamepadTouchpads(gamepad) > 0) {
|
||||
for (int finger = 0; finger < SDL_GetNumGamepadTouchpadFingers(gamepad, 0); ++finger) {
|
||||
bool down = false;
|
||||
float x = 0;
|
||||
if (SDL_GetGamepadTouchpadFinger(gamepad, 0, finger, &down, &x, nullptr, nullptr) &&
|
||||
down) {
|
||||
pressed.insert(x < 0.5f ? QStringLiteral("TouchPad")
|
||||
: QStringLiteral("TouchPadRight"));
|
||||
touching = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!touching && SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_TOUCHPAD)) {
|
||||
pressed.insert(QStringLiteral("TouchPad"));
|
||||
pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
}
|
||||
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
|
||||
name != m_device_name) {
|
||||
m_pressed = pressed;
|
||||
m_left_stick = left;
|
||||
m_right_stick = right;
|
||||
m_device_name = name;
|
||||
emit StateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerPreview::ApplyColor(SDL_Gamepad* gamepad) const {
|
||||
if (m_color.isValid() &&
|
||||
SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
|
||||
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false) &&
|
||||
!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
|
||||
static_cast<Uint8>(m_color.green()),
|
||||
static_cast<Uint8>(m_color.blue()))) {
|
||||
qWarning() << "Cannot set controller LED:" << SDL_GetError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "dualsenseWidget.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
enum class Shape { Button, Shoulder, Touchpad, Face, Dpad, Stick, Direction };
|
||||
|
||||
struct Control {
|
||||
const char* id;
|
||||
QRectF bounds;
|
||||
Shape shape = Shape::Button;
|
||||
int angle = 0;
|
||||
};
|
||||
|
||||
// Painting and hit testing share a canvas with room above the shell for the shoulders.
|
||||
const QRectF CANVAS(0, -20, 480, 330);
|
||||
|
||||
const Control CONTROLS[] = {
|
||||
{"L2", {90, -12, 58, 18}, Shape::Shoulder},
|
||||
{"R2", {332, -12, 58, 18}, Shape::Shoulder},
|
||||
{"L1", {84, 12, 64, 12}, Shape::Shoulder},
|
||||
{"R1", {332, 12, 64, 12}, Shape::Shoulder},
|
||||
{"TouchPad", {150, 25, 90, 100}, Shape::Touchpad},
|
||||
{"TouchPadRight", {240, 25, 90, 100}, Shape::Touchpad},
|
||||
{"Options", {330, 60, 9, 20}},
|
||||
{"Up", {105, 72, 22, 25}, Shape::Dpad, 0},
|
||||
{"Right", {124, 94, 25, 22}, Shape::Dpad, 90},
|
||||
{"Down", {105, 113, 22, 25}, Shape::Dpad, 180},
|
||||
{"Left", {83, 94, 25, 22}, Shape::Dpad, 270},
|
||||
{"Triangle", {353, 67, 26, 26}, Shape::Face},
|
||||
{"Circle", {382, 96, 26, 26}, Shape::Face},
|
||||
{"Cross", {353, 125, 26, 26}, Shape::Face},
|
||||
{"Square", {324, 96, 26, 26}, Shape::Face},
|
||||
{"L3", {143, 131, 66, 66}, Shape::Stick},
|
||||
{"R3", {271, 131, 66, 66}, Shape::Stick},
|
||||
{"LeftStickUp", {143, 131, 66, 66}, Shape::Direction, 0},
|
||||
{"LeftStickRight", {143, 131, 66, 66}, Shape::Direction, 90},
|
||||
{"LeftStickDown", {143, 131, 66, 66}, Shape::Direction, 180},
|
||||
{"LeftStickLeft", {143, 131, 66, 66}, Shape::Direction, 270},
|
||||
{"RightStickUp", {271, 131, 66, 66}, Shape::Direction, 0},
|
||||
{"RightStickRight", {271, 131, 66, 66}, Shape::Direction, 90},
|
||||
{"RightStickDown", {271, 131, 66, 66}, Shape::Direction, 180},
|
||||
{"RightStickLeft", {271, 131, 66, 66}, Shape::Direction, 270},
|
||||
};
|
||||
|
||||
QPainterPath TouchpadPath() {
|
||||
QPainterPath path;
|
||||
path.moveTo(158, 42);
|
||||
path.cubicTo(156, 33, 168, 32, 178, 31);
|
||||
path.cubicTo(219, 28, 261, 28, 302, 31);
|
||||
path.cubicTo(312, 32, 324, 33, 322, 42);
|
||||
path.cubicTo(318, 64, 315, 87, 309, 105);
|
||||
path.cubicTo(306, 117, 299, 122, 286, 122);
|
||||
path.lineTo(194, 122);
|
||||
path.cubicTo(181, 122, 174, 117, 171, 105);
|
||||
path.cubicTo(165, 87, 162, 64, 158, 42);
|
||||
path.closeSubpath();
|
||||
return path;
|
||||
}
|
||||
|
||||
QPainterPath ControlPath(const Control& control) {
|
||||
QPainterPath path;
|
||||
if (control.shape == Shape::Direction) {
|
||||
const auto inner = control.bounds.adjusted(7, 7, -7, -7);
|
||||
const int start = 49 - control.angle;
|
||||
path.arcMoveTo(control.bounds, start);
|
||||
path.arcTo(control.bounds, start, 82);
|
||||
path.arcTo(inner, start + 82, -82);
|
||||
path.closeSubpath();
|
||||
} else if (control.shape == Shape::Touchpad) {
|
||||
path.addRect(control.bounds);
|
||||
return TouchpadPath().intersected(path);
|
||||
} else if (control.shape == Shape::Face || control.shape == Shape::Stick) {
|
||||
path.addEllipse(control.shape == Shape::Stick ? control.bounds.adjusted(7, 7, -7, -7)
|
||||
: control.bounds);
|
||||
} else if (control.shape == Shape::Dpad) {
|
||||
path.moveTo(-8, -12);
|
||||
path.quadTo(-11, -12, -11, -8);
|
||||
path.lineTo(-10, 3);
|
||||
path.quadTo(-10, 6, -7, 8);
|
||||
path.lineTo(0, 12);
|
||||
path.lineTo(7, 8);
|
||||
path.quadTo(10, 6, 10, 3);
|
||||
path.lineTo(11, -8);
|
||||
path.quadTo(11, -12, 8, -12);
|
||||
path.closeSubpath();
|
||||
QTransform transform;
|
||||
transform.translate(control.bounds.center().x(), control.bounds.center().y());
|
||||
transform.rotate(control.angle);
|
||||
return transform.map(path);
|
||||
} else if (control.shape == Shape::Shoulder) {
|
||||
const auto& bounds = control.bounds;
|
||||
path.moveTo(bounds.bottomLeft());
|
||||
path.lineTo(bounds.left() + 2, bounds.top() + bounds.height() / 2);
|
||||
path.cubicTo(bounds.left() + 10, bounds.top(), bounds.right() - 10, bounds.top(),
|
||||
bounds.right() - 2, bounds.top() + bounds.height() / 2);
|
||||
path.lineTo(bounds.bottomRight());
|
||||
path.closeSubpath();
|
||||
} else {
|
||||
path.addRoundedRect(control.bounds, 4, 4);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
void DrawSymbol(QPainter& painter, const Control& control) {
|
||||
const auto id = QLatin1String(control.id);
|
||||
painter.save();
|
||||
painter.translate(control.bounds.center());
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
if (control.shape == Shape::Dpad) {
|
||||
painter.rotate(control.angle);
|
||||
painter.drawPolyline(QPolygonF({{-4, 2}, {0, -2}, {4, 2}}));
|
||||
} else if (id == QLatin1String("Triangle")) {
|
||||
painter.drawPolygon(QPolygonF({{0, -7}, {7, 5}, {-7, 5}}));
|
||||
} else if (id == QLatin1String("Circle")) {
|
||||
painter.drawEllipse(QPointF(), 7, 7);
|
||||
} else if (id == QLatin1String("Square")) {
|
||||
painter.drawRect(QRectF(-6, -6, 12, 12));
|
||||
} else if (id == QLatin1String("Cross")) {
|
||||
painter.drawLine(QPointF(-5, -5), QPointF(5, 5));
|
||||
painter.drawLine(QPointF(-5, 5), QPointF(5, -5));
|
||||
}
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DualSenseWidget::DualSenseWidget(QWidget* parent): QWidget(parent) {
|
||||
setMinimumSize(330, 230);
|
||||
setMouseTracking(true);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
setAccessibleName(tr("DualSense controller"));
|
||||
setAccessibleDescription(tr("Click a control to select its binding. Connected controller input "
|
||||
"lights up the controls."));
|
||||
}
|
||||
|
||||
QSize DualSenseWidget::sizeHint() const {
|
||||
return CANVAS.size().toSize();
|
||||
}
|
||||
|
||||
void DualSenseWidget::SetPressed(const QSet<QString>& pressed) {
|
||||
if (m_pressed != pressed) {
|
||||
m_pressed = pressed;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void DualSenseWidget::SetSelected(const QString& selected) {
|
||||
if (m_selected != selected) {
|
||||
m_selected = selected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void DualSenseWidget::SetSticks(const QPointF& left, const QPointF& right) {
|
||||
if (m_left_stick != left || m_right_stick != right) {
|
||||
m_left_stick = left;
|
||||
m_right_stick = right;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QTransform DualSenseWidget::ViewTransform() const {
|
||||
const qreal scale = std::min(width() / CANVAS.width(), height() / CANVAS.height());
|
||||
QTransform transform;
|
||||
transform.translate(width() / 2.0, height() / 2.0);
|
||||
transform.scale(scale, scale);
|
||||
transform.translate(-CANVAS.center().x(), -CANVAS.center().y());
|
||||
return transform;
|
||||
}
|
||||
|
||||
QString DualSenseWidget::HitTest(const QPointF& position) const {
|
||||
const auto point = ViewTransform().inverted().map(position);
|
||||
for (const auto& control: CONTROLS) {
|
||||
if (ControlPath(control).contains(point)) {
|
||||
return QString::fromLatin1(control.id);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void DualSenseWidget::paintEvent(QPaintEvent*) {
|
||||
const QColor black("#111111");
|
||||
const QColor white("#eeeeee");
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setTransform(ViewTransform());
|
||||
|
||||
QPainterPath shell;
|
||||
shell.moveTo(81, 48);
|
||||
shell.cubicTo(151, 21, 329, 21, 399, 48);
|
||||
shell.cubicTo(420, 94, 445, 166, 445, 215);
|
||||
shell.cubicTo(447, 253, 437, 291, 419, 296);
|
||||
shell.cubicTo(403, 302, 395, 297, 389, 283);
|
||||
shell.cubicTo(381, 268, 373, 236, 354, 213);
|
||||
shell.cubicTo(347, 205, 341, 205, 325, 205);
|
||||
shell.lineTo(155, 205);
|
||||
shell.cubicTo(139, 205, 133, 205, 126, 213);
|
||||
shell.cubicTo(107, 236, 99, 268, 91, 283);
|
||||
shell.cubicTo(85, 297, 77, 302, 61, 296);
|
||||
shell.cubicTo(43, 291, 33, 253, 35, 215);
|
||||
shell.cubicTo(35, 166, 60, 94, 81, 48);
|
||||
shell.closeSubpath();
|
||||
painter.setBrush(black);
|
||||
painter.setPen(QPen(white, 1.5));
|
||||
painter.drawPath(shell);
|
||||
|
||||
// Panel seams flow into the inner grips without a second outline across the belly.
|
||||
QPainterPath seams;
|
||||
seams.moveTo(153, 35);
|
||||
seams.cubicTo(153, 69, 176, 105, 158, 127);
|
||||
seams.cubicTo(123, 166, 108, 226, 76, 297);
|
||||
seams.moveTo(327, 35);
|
||||
seams.cubicTo(327, 69, 304, 105, 322, 127);
|
||||
seams.cubicTo(357, 166, 372, 226, 404, 297);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawPath(seams);
|
||||
painter.setBrush(black);
|
||||
painter.drawPath(TouchpadPath());
|
||||
painter.drawRoundedRect(QRectF(141, 60, 9, 20), 4, 4); // Create button, currently unmapped.
|
||||
painter.drawEllipse(QPointF(240, 166), 8, 8);
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(9);
|
||||
font.setBold(true);
|
||||
painter.setFont(font);
|
||||
|
||||
for (const auto& control: CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const bool pressed = m_pressed.contains(id);
|
||||
const bool selected = m_selected == id;
|
||||
const bool hovered = m_hovered == id;
|
||||
const bool active = pressed || selected || hovered;
|
||||
if ((control.shape == Shape::Direction || control.shape == Shape::Touchpad) && !active) {
|
||||
continue;
|
||||
}
|
||||
const QColor fill = pressed ? QColor("#2eb6df") : active ? QColor("#3a6179") : black;
|
||||
painter.setBrush(fill);
|
||||
painter.setPen(QPen(active ? QColor("#79e3ff") : white, selected ? 2 : 1.3));
|
||||
if (control.shape == Shape::Touchpad) {
|
||||
// Clip one shared surface so independent bindings never add a permanent center seam.
|
||||
painter.save();
|
||||
painter.setClipRect(control.bounds);
|
||||
painter.drawPath(TouchpadPath());
|
||||
painter.restore();
|
||||
} else if (control.shape == Shape::Stick) {
|
||||
painter.setBrush(black);
|
||||
painter.drawEllipse(control.bounds);
|
||||
const auto position = id == QLatin1String("L3") ? m_left_stick : m_right_stick;
|
||||
QPointF offset(std::clamp(position.x(), -1.0, 1.0),
|
||||
std::clamp(position.y(), -1.0, 1.0));
|
||||
offset *= 5 / std::max(1.0, std::hypot(offset.x(), offset.y()));
|
||||
painter.setBrush(fill);
|
||||
painter.drawEllipse(control.bounds.adjusted(12, 12, -12, -12).translated(offset));
|
||||
} else {
|
||||
painter.drawPath(ControlPath(control));
|
||||
painter.setPen(QPen(white, 1.3));
|
||||
if (control.shape == Shape::Face || control.shape == Shape::Dpad) {
|
||||
DrawSymbol(painter, control);
|
||||
} else if (control.shape == Shape::Shoulder) {
|
||||
painter.drawText(control.bounds.adjusted(0, 2, 0, 0), Qt::AlignCenter, id);
|
||||
} else if (id == QLatin1String("Options")) {
|
||||
painter.drawLine(control.bounds.center() - QPointF(0, 4),
|
||||
control.bounds.center() + QPointF(0, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DualSenseWidget::mouseMoveEvent(QMouseEvent* event) {
|
||||
const auto hovered = HitTest(event->position());
|
||||
if (hovered != m_hovered) {
|
||||
m_hovered = hovered;
|
||||
setCursor(hovered.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
|
||||
setToolTip(hovered);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void DualSenseWidget::mousePressEvent(QMouseEvent* event) {
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
const auto id = HitTest(event->position());
|
||||
if (!id.isEmpty()) {
|
||||
emit ControlClicked(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void DualSenseWidget::leaveEvent(QEvent*) {
|
||||
m_hovered.clear();
|
||||
unsetCursor();
|
||||
update();
|
||||
}
|
||||
@@ -1,61 +1,69 @@
|
||||
#include "inputMappingDialog.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include "controllerPreview.h"
|
||||
#include "dualsenseWidget.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QHeaderView>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QStringList>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int BINDING_COLUMN = 1;
|
||||
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
|
||||
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
|
||||
|
||||
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
|
||||
|
||||
struct PadControl {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* default_binding;
|
||||
Group group;
|
||||
int row;
|
||||
int column;
|
||||
};
|
||||
|
||||
constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Up", "D-pad Up", "Up"},
|
||||
{"Down", "D-pad Down", "Down"},
|
||||
{"Left", "D-pad Left", "Left"},
|
||||
{"Right", "D-pad Right", "Right"},
|
||||
{"LeftStickUp", "Left stick Up", "W"},
|
||||
{"LeftStickDown", "Left stick Down", "S"},
|
||||
{"LeftStickLeft", "Left stick Left", "A"},
|
||||
{"LeftStickRight", "Left stick Right", "D"},
|
||||
{"RightStickUp", "Right stick Up", "T"},
|
||||
{"RightStickDown", "Right stick Down", "G"},
|
||||
{"RightStickLeft", "Right stick Left", "F"},
|
||||
{"RightStickRight", "Right stick Right", "H"},
|
||||
{"Triangle", "Triangle", "I"},
|
||||
{"Circle", "Circle", "L"},
|
||||
{"Cross", "Cross", "J"},
|
||||
{"Square", "Square", "K"},
|
||||
{"L1", "L1", "Q"},
|
||||
{"R1", "R1", "E"},
|
||||
{"L2", "L2", "Z"},
|
||||
{"R2", "R2", "C"},
|
||||
{"L3", "L3", "Left Shift"},
|
||||
{"R3", "R3", "Left Ctrl"},
|
||||
{"Options", "Options", "Return"},
|
||||
{"TouchPad", "Touch pad left (SELECT)", "Backspace"},
|
||||
{"TouchPadRight", "Touch pad right (START)", "Tab"},
|
||||
{"SpeakerVolume", "Speaker volume (cycle)", "1"},
|
||||
{"VibrationIntensity", "Vibration intensity (cycle)", "2"},
|
||||
{"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"},
|
||||
{"Up", "Up", "Up", Dpad, 0, 1},
|
||||
{"Down", "Down", "Down", Dpad, 2, 1},
|
||||
{"Left", "Left", "Left", Dpad, 1, 0},
|
||||
{"Right", "Right", "Right", Dpad, 1, 2},
|
||||
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
|
||||
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
|
||||
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
|
||||
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
|
||||
{"RightStickUp", "Up", "T", RightStick, 0, 1},
|
||||
{"RightStickDown", "Down", "G", RightStick, 2, 1},
|
||||
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
|
||||
{"RightStickRight", "Right", "H", RightStick, 1, 2},
|
||||
{"Triangle", "Triangle", "I", Face, 0, 1},
|
||||
{"Circle", "Circle", "L", Face, 1, 2},
|
||||
{"Cross", "Cross", "J", Face, 2, 1},
|
||||
{"Square", "Square", "K", Face, 1, 0},
|
||||
{"L1", "L1", "Q", Shoulders, 0, 1},
|
||||
{"R1", "R1", "E", Shoulders, 0, 2},
|
||||
{"L2", "L2", "Z", Shoulders, 0, 0},
|
||||
{"R2", "R2", "C", Shoulders, 0, 3},
|
||||
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
|
||||
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
|
||||
{"Options", "Options", "Return", Center, 0, 1},
|
||||
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
|
||||
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
|
||||
};
|
||||
|
||||
QString KeypadName(int key) {
|
||||
@@ -183,6 +191,17 @@ private:
|
||||
QString m_binding;
|
||||
};
|
||||
|
||||
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
|
||||
for (auto item = bindings.begin(); item != bindings.end();) {
|
||||
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
|
||||
item = bindings.erase(item);
|
||||
} else {
|
||||
++item;
|
||||
}
|
||||
}
|
||||
bindings.insert(id, binding);
|
||||
}
|
||||
|
||||
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
QHash<QString, QString> result;
|
||||
for (const auto& entry: mapping) {
|
||||
@@ -191,15 +210,7 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
}
|
||||
const auto separator = entry.indexOf(QLatin1Char('='));
|
||||
if (separator > 0 && separator + 1 < entry.size()) {
|
||||
const auto binding = entry.mid(separator + 1);
|
||||
for (auto item = result.begin(); item != result.end();) {
|
||||
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
|
||||
item = result.erase(item);
|
||||
} else {
|
||||
++item;
|
||||
}
|
||||
}
|
||||
result.insert(entry.left(separator), binding);
|
||||
AssignBinding(result, entry.left(separator), entry.mid(separator + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -216,82 +227,146 @@ double ParseMouseSensitivity(const QStringList& mapping) {
|
||||
|
||||
} // namespace
|
||||
|
||||
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setWindowTitle(tr("Input Mapping"));
|
||||
resize(460, 650);
|
||||
InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_preview(preview) {
|
||||
setWindowTitle(tr("Controls"));
|
||||
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->addWidget(
|
||||
new QLabel(tr("Map keyboard or mouse buttons to DualSense controls.\n"
|
||||
"Press F7 in-game to toggle mouse movement on the right stick."),
|
||||
this));
|
||||
layout->setSpacing(10);
|
||||
auto* title = new QLabel(tr("Controller setup"), this);
|
||||
auto font = title->font();
|
||||
font.setPointSize(font.pointSize() + 7);
|
||||
font.setBold(true);
|
||||
title->setFont(font);
|
||||
layout->addWidget(title);
|
||||
auto* hint = new QLabel(
|
||||
tr("Select a button to remap. Press controller buttons to test."), this);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
m_status = new QLabel(this);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
|
||||
const auto parsed = ParseMapping(mapping);
|
||||
m_custom_bindings = !parsed.isEmpty();
|
||||
auto* scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* grid = new QGridLayout(content);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setSpacing(12);
|
||||
QGroupBox* groups[GroupCount];
|
||||
QGridLayout* cells[GroupCount];
|
||||
const char* titles[] = {"D-Pad",
|
||||
"Left stick",
|
||||
"Right stick",
|
||||
"Face buttons",
|
||||
"Shoulders and triggers",
|
||||
"Touch pad and options",
|
||||
"In-game shortcuts (cycle)"};
|
||||
for (int index = 0; index < GroupCount; ++index) {
|
||||
groups[index] = new QGroupBox(tr(titles[index]), content);
|
||||
cells[index] = new QGridLayout(groups[index]);
|
||||
cells[index]->setSpacing(5);
|
||||
}
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
|
||||
auto* cell_layout = new QVBoxLayout(cell);
|
||||
cell_layout->setContentsMargins(3, 5, 3, 3);
|
||||
auto* button = new QPushButton(cell);
|
||||
button->setObjectName(QStringLiteral("binding_") + id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(64);
|
||||
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
|
||||
tr(control.label));
|
||||
cell_layout->addWidget(button);
|
||||
cells[control.group]->addWidget(cell, control.row, control.column);
|
||||
m_buttons.insert(id, button);
|
||||
if (!m_custom_bindings) {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
|
||||
}
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
left->addWidget(groups[Dpad]);
|
||||
left->addWidget(groups[LeftStick]);
|
||||
left->addStretch();
|
||||
grid->addLayout(left, 0, 0);
|
||||
auto* center = new QVBoxLayout;
|
||||
center->addWidget(groups[Shoulders]);
|
||||
m_controller = new DualSenseWidget(content);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
center->addWidget(m_controller, 1);
|
||||
center->addWidget(groups[Center]);
|
||||
grid->addLayout(center, 0, 1);
|
||||
auto* right = new QVBoxLayout;
|
||||
right->addWidget(groups[Face]);
|
||||
right->addWidget(groups[RightStick]);
|
||||
right->addStretch();
|
||||
grid->addLayout(right, 0, 2);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
|
||||
scroll->setWidget(content);
|
||||
layout->addWidget(scroll, 1);
|
||||
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
|
||||
m_sensitivity = new QDoubleSpinBox(this);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("x"));
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
m_bindings = new QTreeWidget(this);
|
||||
m_bindings->setColumnCount(2);
|
||||
m_bindings->setHeaderLabels({tr("DualSense control"), tr("Host input")});
|
||||
m_bindings->setRootIsDecorated(false);
|
||||
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
layout->addWidget(m_bindings);
|
||||
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
auto* item = new QTreeWidgetItem(m_bindings);
|
||||
item->setText(0, tr(control.label));
|
||||
item->setData(0, Qt::UserRole, QString::fromLatin1(control.id));
|
||||
SetBinding(item, m_custom_bindings ? parsed.value(QString::fromLatin1(control.id))
|
||||
: QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
|
||||
|
||||
auto* controls = new QHBoxLayout;
|
||||
m_change_button = new QPushButton(tr("Change..."), this);
|
||||
m_clear_button = new QPushButton(tr("Clear"), this);
|
||||
auto* defaults = new QPushButton(tr("Defaults"), this);
|
||||
controls->addWidget(m_change_button);
|
||||
controls->addWidget(m_clear_button);
|
||||
controls->addWidget(defaults);
|
||||
controls->addStretch();
|
||||
layout->addLayout(controls);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
|
||||
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
|
||||
this);
|
||||
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
|
||||
defaults->setObjectName(QStringLiteral("defaults"));
|
||||
defaults->setAutoDefault(false);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
|
||||
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
|
||||
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
|
||||
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
|
||||
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
|
||||
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
&InputMappingDialog::ChangeBinding);
|
||||
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
if (m_preview != nullptr) {
|
||||
connect(m_preview, &ControllerPreview::StateChanged, this,
|
||||
&InputMappingDialog::UpdatePreview);
|
||||
m_preview->SetPreviewEnabled(true);
|
||||
}
|
||||
UpdateButtons();
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
InputMappingDialog::~InputMappingDialog() {
|
||||
if (m_preview != nullptr) {
|
||||
m_preview->SetPreviewEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList InputMappingDialog::Mapping() const {
|
||||
QStringList result;
|
||||
if (m_custom_bindings) {
|
||||
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
|
||||
const auto* item = m_bindings->topLevelItem(index);
|
||||
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto binding = m_bindings.value(id);
|
||||
if (!binding.isEmpty()) {
|
||||
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
|
||||
result.append(id + QLatin1Char('=') + binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,58 +377,73 @@ QStringList InputMappingDialog::Mapping() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
void InputMappingDialog::ChangeBinding() {
|
||||
auto* item = m_bindings->currentItem();
|
||||
if (item == nullptr) {
|
||||
void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
if (!m_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selected = id;
|
||||
UpdateHighlights();
|
||||
InputCaptureDialog dialog(this);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
dialog.setWindowTitle(tr("Set binding — %1").arg(m_buttons.value(id)->accessibleName()));
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
AssignBinding(m_bindings, id, dialog.Binding());
|
||||
m_custom_bindings = true;
|
||||
}
|
||||
|
||||
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
|
||||
auto* other = m_bindings->topLevelItem(index);
|
||||
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
|
||||
.toString()
|
||||
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
|
||||
SetBinding(other, {});
|
||||
}
|
||||
}
|
||||
SetBinding(item, dialog.Binding());
|
||||
m_custom_bindings = true;
|
||||
}
|
||||
|
||||
void InputMappingDialog::ClearBinding() {
|
||||
SetBinding(m_bindings->currentItem(), {});
|
||||
m_custom_bindings = true;
|
||||
m_selected.clear();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
|
||||
SetBinding(m_bindings->topLevelItem(index),
|
||||
QString::fromLatin1(PAD_CONTROLS[index].default_binding));
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
}
|
||||
|
||||
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
|
||||
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdateButtons() {
|
||||
if (m_change_button == nullptr) {
|
||||
return;
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
auto* button = item.value();
|
||||
const auto binding = m_bindings.value(item.key());
|
||||
const auto label = binding.isEmpty() ? tr("None") : binding;
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
|
||||
}
|
||||
UpdateHighlights();
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdatePreview() {
|
||||
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
|
||||
m_controller->SetPressed(pressed);
|
||||
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
|
||||
m_preview != nullptr ? m_preview->RightStick() : QPointF());
|
||||
UpdateHighlights();
|
||||
if (m_preview != nullptr && !m_preview->Error().isEmpty()) {
|
||||
m_status->setText(tr("Controller preview unavailable: %1").arg(m_preview->Error()));
|
||||
} else if (m_preview != nullptr && !m_preview->DeviceName().isEmpty()) {
|
||||
m_status->setText(tr("Connected: %1").arg(m_preview->DeviceName()));
|
||||
} else {
|
||||
m_status->setText(tr("No controller connected"));
|
||||
}
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdateHighlights() {
|
||||
m_controller->SetSelected(m_selected);
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
|
||||
auto colors = palette();
|
||||
if (down) {
|
||||
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
|
||||
colors.setColor(QPalette::ButtonText, colors.color(QPalette::HighlightedText));
|
||||
} else if (item.key() == m_selected) {
|
||||
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
|
||||
}
|
||||
auto label_font = font();
|
||||
label_font.setBold(down);
|
||||
item.value()->setPalette(colors);
|
||||
item.value()->setFont(label_font);
|
||||
}
|
||||
const auto* item = m_bindings->currentItem();
|
||||
m_change_button->setEnabled(item != nullptr);
|
||||
m_clear_button->setEnabled(item != nullptr &&
|
||||
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerLightbar.h"
|
||||
#include "controllerPreview.h"
|
||||
#include "gameContent.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
@@ -89,7 +89,7 @@ private:
|
||||
Ui::MainDialog* m_ui = {nullptr};
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
UpdateChecker* m_update_checker = nullptr;
|
||||
ControllerLightbar m_lightbar;
|
||||
ControllerPreview m_controller_preview;
|
||||
QString m_interpreter;
|
||||
|
||||
QProcess m_process;
|
||||
@@ -112,6 +112,7 @@ MainDialogPrivate::~MainDialogPrivate() {
|
||||
void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
m_ui = new Ui::MainDialog;
|
||||
m_ui->setupUi(main_dialog);
|
||||
m_ui->widget->SetControllerPreview(&m_controller_preview);
|
||||
|
||||
m_main_dialog = main_dialog;
|
||||
m_update_checker = new UpdateChecker(main_dialog);
|
||||
@@ -127,7 +128,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
|
||||
[this](const QString& color) {
|
||||
if (m_process.state() == QProcess::NotRunning) {
|
||||
m_lightbar.SetColor(color);
|
||||
m_controller_preview.SetColor(color);
|
||||
}
|
||||
});
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
|
||||
@@ -514,7 +515,7 @@ void MainDialogPrivate::Run() {
|
||||
}
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
m_lightbar.Stop();
|
||||
m_controller_preview.SetSuspended(true);
|
||||
|
||||
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
|
||||
m_main_dialog->RunInterpreter(&m_process, *info);
|
||||
@@ -535,10 +536,11 @@ void MainDialogPrivate::Update() {
|
||||
|
||||
m_ui->widget->SetRunEnabled(run_enabled);
|
||||
if (m_process.state() != QProcess::NotRunning) {
|
||||
m_lightbar.Stop();
|
||||
m_controller_preview.SetSuspended(true);
|
||||
return;
|
||||
}
|
||||
m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor());
|
||||
m_controller_preview.SetSuspended(false);
|
||||
m_controller_preview.SetColor(m_ui->widget->GetGlobalControllerColor());
|
||||
}
|
||||
|
||||
#include "mainDialog.moc"
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
QListView#cfgs_grid {
|
||||
background: palette(base);
|
||||
border: 1px solid palette(mid);
|
||||
color: palette(text);
|
||||
}
|
||||
|
||||
QListView#cfgs_grid::item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
GameListTreeWidget#cfgs_list {
|
||||
background: transparent;
|
||||
border: 1px solid palette(mid);
|
||||
@@ -39,6 +49,7 @@ QLineEdit#search_line_edit:focus {
|
||||
border-color: palette(highlight);
|
||||
}
|
||||
|
||||
QToolButton#grid_view_button,
|
||||
QToolButton#refresh_button,
|
||||
QToolButton#global_settings_button,
|
||||
QToolButton#input_mapping_button,
|
||||
@@ -51,6 +62,7 @@ QToolButton#delete_button {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
QToolButton#grid_view_button:hover,
|
||||
QToolButton#refresh_button:hover,
|
||||
QToolButton#global_settings_button:hover,
|
||||
QToolButton#input_mapping_button:hover,
|
||||
@@ -61,6 +73,8 @@ QToolButton#delete_button:hover {
|
||||
border-color: palette(mid);
|
||||
}
|
||||
|
||||
QToolButton#grid_view_button:checked,
|
||||
QToolButton#grid_view_button:pressed,
|
||||
QToolButton#refresh_button:pressed,
|
||||
QToolButton#global_settings_button:pressed,
|
||||
QToolButton#input_mapping_button:pressed,
|
||||
|
||||
Reference in New Issue
Block a user