Log presets feature (#3509)

* Log presets feature
- Add “Load Presets…” button in Logger → Log Filter (settings_dialog.ui)
- Implement LogPresetsDialog (table: Comment, Filter)
- Support add (+), multi-remove (−), load via button or double-click
- Persist presets via QSettings at logger_presets/entries (qt_ui.ini)
- Wire button to open dialog and set logFilterLineEdit
- Update CMakeLists.txt to include new dialog sources

* CLang fix

* CLang fix again

* Log Presets: checkbox selection + tri-state header

- Added first column with per-row checkboxes
- Implemented header checkbox with tri-state; click toggles select-all/none
- Synced checkboxes and row selection in both directions
- Header toggle also updates row selection to match
- Remove operates on checked rows; falls back to selected rows if none checked
- Load uses first checked row; falls back to first selected row
- Double-click row triggers the same action as Load
- Load button visible only when exactly one row is selected
- Remove button visible only when at least one row is selected
- Dialog opens with no selected rows and no focused buttons; focus set to table
- New rows start editing in the Comment column
- Serialization updated for new column indices; checkbox state not persisted
- Cleanups: removed unused include and empty helper; pruned temporary comments

* Clang

* Fix: tri‑state header checkbox visible and aligned

- Overlay real QCheckBox in header section 0 (tri‑state)
- Align with row checkboxes using SE_ItemViewItemCheckIndicator
- Reposition on header resize and geometry changes
- Header click + checkbox click toggle select‑all/none
- Tri‑state reflects per‑row checkbox states

* CLang

---------

Co-authored-by: w1naenator <valdis.bogdans@hotmail.com>
This commit is contained in:
Valdis Bogdāns
2025-09-05 09:50:59 +03:00
committed by GitHub
parent b2269f628a
commit 5f0b13ac57
5 changed files with 486 additions and 0 deletions

View File

@@ -1098,6 +1098,8 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/kbm_help_dialog.h
src/qt_gui/main_window_themes.cpp
src/qt_gui/main_window_themes.h
src/qt_gui/log_presets_dialog.cpp
src/qt_gui/log_presets_dialog.h
src/qt_gui/settings_dialog.cpp
src/qt_gui/settings_dialog.h
src/qt_gui/settings_dialog.ui

View File

@@ -0,0 +1,414 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "log_presets_dialog.h"
#include <algorithm>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QItemSelection>
#include <QItemSelectionModel>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QStyleOptionViewItem>
#include <QTableWidget>
#include <QVBoxLayout>
namespace {
constexpr const char* kPresetsGroup = "logger_presets";
constexpr const char* kPresetsKey = "entries";
inline QString MakeEntry(const QString& comment, const QString& filter) {
return comment + "\t" + filter;
}
inline void SplitEntry(const QString& entry, QString& comment, QString& filter) {
const int idx = entry.indexOf('\t');
if (idx < 0) {
comment = entry;
filter = QString();
} else {
comment = entry.left(idx);
filter = entry.mid(idx + 1);
}
}
} // namespace
LogPresetsDialog::LogPresetsDialog(std::shared_ptr<gui_settings> gui_settings, QWidget* parent)
: QDialog(parent), m_gui_settings(std::move(gui_settings)) {
setWindowTitle(tr("Log Filter Presets"));
resize(640, 360);
auto* root = new QVBoxLayout(this);
m_table = new QTableWidget(this);
m_table->setColumnCount(3);
m_table->horizontalHeader()->setSectionsClickable(true);
m_table->horizontalHeader()->setHighlightSections(false);
QStringList headers;
headers << QString() << tr("Comment") << tr("Filter");
m_table->setHorizontalHeaderLabels(headers);
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeMode::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Interactive);
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::Stretch);
m_table->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
m_table->setEditTriggers(QAbstractItemView::EditTrigger::DoubleClicked |
QAbstractItemView::EditTrigger::SelectedClicked |
QAbstractItemView::EditTrigger::EditKeyPressed);
connect(m_table, &QTableWidget::cellDoubleClicked, this,
[this](int /*row*/, int /*col*/) { LoadSelected(); });
connect(m_table, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item) {
if (m_updating_checks)
return;
if (item && item->column() == 0) {
m_updating_checks = true;
const int row = item->row();
auto* sm = m_table->selectionModel();
const auto idx = m_table->model()->index(row, 0);
const bool check = (item->checkState() == Qt::Checked);
sm->select(idx, (check ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) |
QItemSelectionModel::Rows);
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
});
connect(m_table->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this](const QItemSelection& selected, const QItemSelection& deselected) {
if (m_updating_checks)
return;
m_updating_checks = true;
QSet<int> selRows;
for (const auto& idx : selected.indexes())
selRows.insert(idx.row());
QSet<int> deselRows;
for (const auto& idx : deselected.indexes())
deselRows.insert(idx.row());
for (int r : selRows) {
auto* it = m_table->item(r, 0);
if (!it) {
it = new QTableWidgetItem();
it->setFlags((QTableWidgetItem().flags() | Qt::ItemIsUserCheckable) &
~Qt::ItemIsEditable);
m_table->setItem(r, 0, it);
}
it->setCheckState(Qt::Checked);
}
for (int r : deselRows) {
auto* it = m_table->item(r, 0);
if (it)
it->setCheckState(Qt::Unchecked);
}
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
connect(m_table->horizontalHeader(), &QHeaderView::sectionClicked, this, [this](int section) {
if (section != 0)
return;
const auto next = (m_header_checkbox && m_header_checkbox->checkState() == Qt::Checked)
? Qt::Unchecked
: Qt::Checked;
if (m_header_checkbox)
m_header_checkbox->setCheckState(next);
SetAllCheckStates(next);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
m_header_checkbox = new QCheckBox(m_table->horizontalHeader());
m_header_checkbox->setTristate(true);
m_header_checkbox->setText(QString());
m_header_checkbox->setFocusPolicy(Qt::NoFocus);
PositionHeaderCheckbox();
QObject::connect(m_table->horizontalHeader(), &QHeaderView::geometriesChanged, this,
[this]() { PositionHeaderCheckbox(); });
QObject::connect(m_table->horizontalHeader(), &QHeaderView::sectionResized, this,
[this](int, int, int) { PositionHeaderCheckbox(); });
QObject::connect(m_header_checkbox, &QCheckBox::clicked, this, [this](bool checked) {
SetAllCheckStates(checked ? Qt::Checked : Qt::Unchecked);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
root->addWidget(m_table);
auto* buttons_layout = new QHBoxLayout();
m_add_btn = new QPushButton(tr("+"), this);
m_remove_btn = new QPushButton(tr("-"), this);
m_load_btn = new QPushButton(tr("Load"), this);
m_close_btn = new QPushButton(tr("Close"), this);
m_add_btn->setToolTip(tr("Add a new preset after the selected row"));
m_remove_btn->setToolTip(tr("Remove selected presets"));
m_load_btn->setToolTip(tr("Load the selected preset"));
buttons_layout->addWidget(m_add_btn);
buttons_layout->addWidget(m_remove_btn);
buttons_layout->addStretch();
buttons_layout->addWidget(m_load_btn);
buttons_layout->addWidget(m_close_btn);
root->addLayout(buttons_layout);
m_add_btn->setAutoDefault(false);
m_remove_btn->setAutoDefault(false);
m_load_btn->setAutoDefault(false);
m_close_btn->setAutoDefault(false);
m_add_btn->setDefault(false);
m_remove_btn->setDefault(false);
m_load_btn->setDefault(false);
m_close_btn->setDefault(false);
connect(m_add_btn, &QPushButton::clicked, this, [this]() { AddAfterSelection(); });
connect(m_remove_btn, &QPushButton::clicked, this, [this]() { RemoveSelected(); });
connect(m_load_btn, &QPushButton::clicked, this, [this]() { LoadSelected(); });
connect(m_close_btn, &QPushButton::clicked, this, [this]() { reject(); });
LoadFromSettings();
m_updating_checks = true;
m_table->clearSelection();
m_table->setCurrentItem(nullptr);
m_updating_checks = false;
m_table->setFocus(Qt::OtherFocusReason);
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::accept() {
SaveToSettings();
QDialog::accept();
}
void LogPresetsDialog::reject() {
SaveToSettings();
QDialog::reject();
}
void LogPresetsDialog::LoadFromSettings() {
if (!m_gui_settings)
return;
const auto var = m_gui_settings->GetValue(kPresetsGroup, kPresetsKey, QVariant());
QList<QString> list;
if (var.isValid()) {
list = m_gui_settings->Var2List(var);
}
PopulateFromList(list);
}
void LogPresetsDialog::SaveToSettings() {
if (!m_gui_settings)
return;
const auto list = SerializeTable();
m_gui_settings->SetValue(kPresetsGroup, kPresetsKey, m_gui_settings->List2Var(list));
}
QList<QString> LogPresetsDialog::SerializeTable() const {
QList<QString> list;
const int rows = m_table->rowCount();
for (int r = 0; r < rows; ++r) {
const auto* comment = m_table->item(r, 1);
const auto* filter = m_table->item(r, 2);
const QString c = comment ? comment->text() : QString();
const QString f = filter ? filter->text() : QString();
if (!c.isEmpty() || !f.isEmpty()) {
list.push_back(MakeEntry(c, f));
}
}
return list;
}
void LogPresetsDialog::PopulateFromList(const QList<QString>& list) {
m_table->setRowCount(0);
for (const auto& entry : list) {
QString c, f;
SplitEntry(entry, c, f);
const int row = m_table->rowCount();
m_table->insertRow(row);
auto* chk = new QTableWidgetItem();
chk->setFlags((chk->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable);
chk->setCheckState(Qt::Unchecked);
m_table->setItem(row, 0, chk);
m_table->setItem(row, 1, new QTableWidgetItem(c));
m_table->setItem(row, 2, new QTableWidgetItem(f));
}
m_updating_checks = true;
m_table->clearSelection();
m_table->setCurrentItem(nullptr);
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::AddAfterSelection() {
int insert_row = m_table->rowCount();
const auto selected = m_table->selectionModel()->selectedRows();
if (!selected.isEmpty()) {
// place after the last selected row
insert_row = selected.last().row() + 1;
}
m_table->insertRow(insert_row);
auto* chk = new QTableWidgetItem();
chk->setFlags((chk->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable);
chk->setCheckState(Qt::Unchecked);
m_table->setItem(insert_row, 0, chk);
m_table->setItem(insert_row, 1, new QTableWidgetItem(""));
m_table->setItem(insert_row, 2, new QTableWidgetItem(""));
UpdateHeaderCheckState();
m_table->setCurrentCell(insert_row, 1);
m_table->editItem(m_table->item(insert_row, 1));
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::RemoveSelected() {
// Prefer checked rows; fall back to selected rows if none checked
QList<int> to_remove = GetCheckedRows();
if (to_remove.isEmpty()) {
const auto selected = m_table->selectionModel()->selectedRows();
for (const auto& idx : selected)
to_remove.push_back(idx.row());
}
if (to_remove.isEmpty())
return;
std::sort(to_remove.begin(), to_remove.end(), [](int a, int b) { return a > b; });
for (int row : to_remove)
m_table->removeRow(row);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::LoadSelected() {
QList<int> rows = GetCheckedRows();
if (rows.isEmpty()) {
const auto selected = m_table->selectionModel()->selectedRows();
if (selected.isEmpty())
return;
rows.push_back(selected.first().row());
}
const int row = rows.first();
const auto* item = m_table->item(row, 2);
if (item) {
emit PresetChosen(item->text());
accept();
}
}
void LogPresetsDialog::UpdateHeaderCheckState() {
const int rows = m_table->rowCount();
if (rows == 0) {
if (m_header_checkbox)
m_header_checkbox->setCheckState(Qt::Unchecked);
return;
}
int checked = 0;
for (int r = 0; r < rows; ++r) {
auto* it = m_table->item(r, 0);
if (it && it->checkState() == Qt::Checked)
++checked;
}
const auto state = (checked == 0) ? Qt::Unchecked
: (checked == rows) ? Qt::Checked
: Qt::PartiallyChecked;
if (m_header_checkbox)
m_header_checkbox->setCheckState(state);
}
void LogPresetsDialog::SetAllCheckStates(Qt::CheckState state) {
const QSignalBlocker blocker(m_table);
m_updating_checks = true;
const int rows = m_table->rowCount();
auto* sm = m_table->selectionModel();
for (int r = 0; r < rows; ++r) {
auto* it = m_table->item(r, 0);
if (!it) {
it = new QTableWidgetItem();
it->setFlags((QTableWidgetItem().flags() | Qt::ItemIsUserCheckable) &
~Qt::ItemIsEditable);
m_table->setItem(r, 0, it);
}
it->setCheckState(state);
const auto idx = m_table->model()->index(r, 0);
sm->select(idx, ((state == Qt::Checked) ? QItemSelectionModel::Select
: QItemSelectionModel::Deselect) |
QItemSelectionModel::Rows);
}
m_updating_checks = false;
}
QList<int> LogPresetsDialog::GetCheckedRows() const {
QList<int> rows;
const int count = m_table->rowCount();
for (int r = 0; r < count; ++r) {
const auto* it = m_table->item(r, 0);
if (it && it->checkState() == Qt::Checked)
rows.push_back(r);
}
return rows;
}
void LogPresetsDialog::UpdateLoadButtonEnabled() {
if (!m_table || !m_table->selectionModel())
return;
const int count = m_table->selectionModel()->selectedRows().size();
if (m_load_btn) {
const bool showLoad = (count == 1);
m_load_btn->setVisible(showLoad);
m_load_btn->setEnabled(showLoad);
}
if (m_remove_btn) {
const bool showRemove = (count >= 1);
m_remove_btn->setVisible(showRemove);
m_remove_btn->setEnabled(showRemove);
}
}
void LogPresetsDialog::PositionHeaderCheckbox() {
if (!m_header_checkbox)
return;
auto* hdr = m_table->horizontalHeader();
const int section = 0;
const int x = hdr->sectionViewportPosition(section);
const int w = hdr->sectionSize(section);
const int h = hdr->height();
const QSize sz = m_header_checkbox->sizeHint();
// Try to align horizontally with cell checkbox indicator for this column
int left_in_section = 0;
if (m_table->model()->rowCount() > 0) {
QStyleOptionViewItem opt;
opt.initFrom(m_table);
opt.features = QStyleOptionViewItem::HasCheckIndicator;
// Use a representative cell rect width (section width) and a typical row height
const int cell_h = m_table->rowHeight(0) > 0 ? m_table->rowHeight(0) : sz.height();
opt.rect = QRect(0, 0, w, cell_h);
const QRect ind =
m_table->style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, m_table);
left_in_section = ind.left();
// Guard against styles that return empty rects
if (ind.isEmpty()) {
left_in_section =
m_table->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_table);
}
} else {
left_in_section =
m_table->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_table);
}
int cx = x + left_in_section;
// Center vertically in header
const int cy = (h - sz.height()) / 2;
// Ensure checkbox fully visible within the section bounds
if (cx + sz.width() > x + w)
cx = x + std::max(0, w - sz.width());
m_header_checkbox->setGeometry(QRect(QPoint(cx, cy), sz));
m_header_checkbox->show();
}

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <QPointer>
class QTableWidget;
class QPushButton;
class QTableWidgetItem;
class QCheckBox;
#include "gui_settings.h"
class LogPresetsDialog : public QDialog {
Q_OBJECT
public:
explicit LogPresetsDialog(std::shared_ptr<gui_settings> gui_settings,
QWidget* parent = nullptr);
~LogPresetsDialog() override = default;
signals:
void PresetChosen(const QString& filter);
protected:
void accept() override;
void reject() override;
private:
void LoadFromSettings();
void SaveToSettings();
void AddAfterSelection();
void RemoveSelected();
void LoadSelected();
void UpdateHeaderCheckState();
void SetAllCheckStates(Qt::CheckState state);
QList<int> GetCheckedRows() const;
void UpdateLoadButtonEnabled();
void PositionHeaderCheckbox();
QList<QString> SerializeTable() const;
void PopulateFromList(const QList<QString>& list);
private:
std::shared_ptr<gui_settings> m_gui_settings;
QTableWidget* m_table = nullptr;
QCheckBox* m_header_checkbox = nullptr;
QPushButton* m_add_btn = nullptr;
QPushButton* m_remove_btn = nullptr;
QPushButton* m_load_btn = nullptr;
QPushButton* m_close_btn = nullptr;
bool m_updating_checks = false;
};

View File

@@ -26,6 +26,7 @@
#include "background_music_player.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "log_presets_dialog.h"
#include "settings_dialog.h"
#include "ui_settings_dialog.h"
#include "video_core/renderer_vulkan/vk_instance.h"
@@ -386,6 +387,14 @@ SettingsDialog::SettingsDialog(std::shared_ptr<gui_settings> gui_settings,
Common::FS::GetUserPath(Common::FS::PathType::LogDir));
QDesktopServices::openUrl(QUrl::fromLocalFile(userPath));
});
// Log presets popup button
connect(ui->logPresetsButton, &QPushButton::clicked, this, [this]() {
auto dlg = new LogPresetsDialog(m_gui_settings, this);
connect(dlg, &LogPresetsDialog::PresetChosen, this,
[this](const QString& filter) { ui->logFilterLineEdit->setText(filter); });
dlg->exec();
});
}
// GRAPHICS TAB

View File

@@ -2217,6 +2217,13 @@
<item>
<widget class="QLineEdit" name="logFilterLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="logPresetsButton">
<property name="text">
<string>Load Presets...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>