cabana: save settings to user-specific directory
diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore
index c3f5ef2b69fbda..362a51f5c9231d 100644
--- a/tools/cabana/.gitignore
+++ b/tools/cabana/.gitignore
@@ -2,6 +2,5 @@ moc_*
 *.moc
 
 cabana
-settings
 dbc/car_fingerprint_to_dbc.json
 tests/test_cabana
diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc
index 0ccef7d3aba845..33403a2bff9ccd 100644
--- a/tools/cabana/cabana.cc
+++ b/tools/cabana/cabana.cc
@@ -18,8 +18,6 @@ int main(int argc, char *argv[]) {
   app.setWindowIcon(QIcon(":cabana-icon.png"));
 
   UnixSignalHandler signalHandler;
-
-  settings.load();
   utils::setTheme(settings.theme);
 
   QCommandLineParser cmd_parser;
diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc
index 9ac88032d5ed30..4f27dcf5ed30fb 100644
--- a/tools/cabana/mainwin.cc
+++ b/tools/cabana/mainwin.cc
@@ -608,12 +608,6 @@ void MainWindow::closeEvent(QCloseEvent *event) {
   }
   settings.message_header_state = messages_widget->saveHeaderState();
 
-  auto status = settings.save();
-  if (status == QSettings::AccessError) {
-    QString error = tr("Failed to write settings to [%1]: access denied").arg(Settings::filePath());
-    qDebug() << error;
-    QMessageBox::warning(this, tr("Failed to write settings"), error);
-  }
   QWidget::closeEvent(event);
 }
 
diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc
index 6a94e013f68422..aea09c55dee152 100644
--- a/tools/cabana/messageswidget.cc
+++ b/tools/cabana/messageswidget.cc
@@ -2,6 +2,7 @@
 
 #include <limits>
 
+#include <QCheckBox>
 #include <QHBoxLayout>
 #include <QPainter>
 #include <QPushButton>
diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc
index c408179fddfd5b..ac8d45007d65a2 100644
--- a/tools/cabana/settings.cc
+++ b/tools/cabana/settings.cc
@@ -6,65 +6,51 @@
 #include <QFileDialog>
 #include <QFormLayout>
 #include <QPushButton>
+#include <QSettings>
 #include <QStandardPaths>
+#include <type_traits>
 
 #include "tools/cabana/util.h"
 
 Settings settings;
 
-QSettings::Status Settings::save() {
-  QSettings s(filePath(), QSettings::IniFormat);
-  s.setValue("absolute_time", absolute_time);
-  s.setValue("fps", fps);
-  s.setValue("max_cached_minutes", max_cached_minutes);
-  s.setValue("chart_height", chart_height);
-  s.setValue("chart_range", chart_range);
-  s.setValue("chart_column_count", chart_column_count);
-  s.setValue("last_dir", last_dir);
-  s.setValue("last_route_dir", last_route_dir);
-  s.setValue("window_state", window_state);
-  s.setValue("geometry", geometry);
-  s.setValue("video_splitter_state", video_splitter_state);
-  s.setValue("recent_files", recent_files);
-  s.setValue("message_header_state_v3", message_header_state);
-  s.setValue("chart_series_type", chart_series_type);
-  s.setValue("theme", theme);
-  s.setValue("sparkline_range", sparkline_range);
-  s.setValue("multiple_lines_bytes", multiple_lines_bytes);
-  s.setValue("log_livestream", log_livestream);
-  s.setValue("log_path", log_path);
-  s.setValue("drag_direction", drag_direction);
-  s.setValue("suppress_defined_signals", suppress_defined_signals);
-  s.sync();
-  return s.status();
+template <class SettingOperation>
+void settings_op(SettingOperation op) {
+  QSettings s("cabana");
+  op(s, "absolute_time", settings.absolute_time);
+  op(s, "fps", settings.fps);
+  op(s, "max_cached_minutes", settings.max_cached_minutes);
+  op(s, "chart_height", settings.chart_height);
+  op(s, "chart_range", settings.chart_range);
+  op(s, "chart_column_count", settings.chart_column_count);
+  op(s, "last_dir", settings.last_dir);
+  op(s, "last_route_dir", settings.last_route_dir);
+  op(s, "window_state", settings.window_state);
+  op(s, "geometry", settings.geometry);
+  op(s, "video_splitter_state", settings.video_splitter_state);
+  op(s, "recent_files", settings.recent_files);
+  op(s, "message_header_state", settings.message_header_state);
+  op(s, "chart_series_type", settings.chart_series_type);
+  op(s, "theme", settings.theme);
+  op(s, "sparkline_range", settings.sparkline_range);
+  op(s, "multiple_lines_bytes", settings.multiple_lines_bytes);
+  op(s, "log_livestream", settings.log_livestream);
+  op(s, "log_path", settings.log_path);
+  op(s, "drag_direction", (int &)settings.drag_direction);
+  op(s, "suppress_defined_signals", settings.suppress_defined_signals);
 }
 
-void Settings::load() {
-  QSettings s(filePath(), QSettings::IniFormat);
-  absolute_time = s.value("absolute_time", false).toBool();
-  fps = s.value("fps", 10).toInt();
-  max_cached_minutes = s.value("max_cached_minutes", 30).toInt();
-  chart_height = s.value("chart_height", 200).toInt();
-  chart_range = s.value("chart_range", 3 * 60).toInt();
-  chart_column_count = s.value("chart_column_count", 1).toInt();
-  last_dir = s.value("last_dir", QDir::homePath()).toString();
-  last_route_dir = s.value("last_route_dir", QDir::homePath()).toString();
-  window_state = s.value("window_state").toByteArray();
-  geometry = s.value("geometry").toByteArray();
-  video_splitter_state = s.value("video_splitter_state").toByteArray();
-  recent_files = s.value("recent_files").toStringList();
-  message_header_state = s.value("message_header_state_v3").toByteArray();
-  chart_series_type = s.value("chart_series_type", 0).toInt();
-  theme = s.value("theme", 0).toInt();
-  sparkline_range = s.value("sparkline_range", 15).toInt();
-  multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool();
-  log_livestream = s.value("log_livestream", true).toBool();
-  log_path = s.value("log_path").toString();
-  drag_direction = (Settings::DragDirection)s.value("drag_direction", 0).toInt();
-  suppress_defined_signals = s.value("suppress_defined_signals", false).toBool();
-  if (log_path.isEmpty()) {
-    log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
-  }
+Settings::Settings() {
+  last_dir = last_route_dir = QDir::homePath();
+  log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
+  settings_op([](QSettings &s, const QString &key, auto &value) {
+    if (auto v = s.value(key); v.canConvert<std::decay_t<decltype(value)>>())
+      value = v.value<std::decay_t<decltype(value)>>();
+  });
+}
+
+Settings::~Settings() {
+  settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); });
 }
 
 // SettingsDlg
@@ -75,45 +61,39 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
   QGroupBox *groupbox = new QGroupBox("General");
   QFormLayout *form_layout = new QFormLayout(groupbox);
 
-  theme = new QComboBox(this);
+  form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this));
   theme->setToolTip(tr("You may need to restart cabana after changes theme"));
   theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")});
   theme->setCurrentIndex(settings.theme);
-  form_layout->addRow(tr("Color Theme"), theme);
 
-  fps = new QSpinBox(this);
+  form_layout->addRow("FPS", fps = new QSpinBox(this));
   fps->setRange(10, 100);
   fps->setSingleStep(10);
   fps->setValue(settings.fps);
-  form_layout->addRow("FPS", fps);
 
-  cached_minutes = new QSpinBox(this);
+  form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this));
   cached_minutes->setRange(5, 60);
   cached_minutes->setSingleStep(1);
   cached_minutes->setValue(settings.max_cached_minutes);
-  form_layout->addRow(tr("Max Cached Minutes"), cached_minutes);
   main_layout->addWidget(groupbox);
 
   groupbox = new QGroupBox("New Signal Settings");
   form_layout = new QFormLayout(groupbox);
-  drag_direction = new QComboBox(this);
+  form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this));
   drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")});
   drag_direction->setCurrentIndex(settings.drag_direction);
-  form_layout->addRow(tr("Drag Direction"), drag_direction);
   main_layout->addWidget(groupbox);
 
   groupbox = new QGroupBox("Chart");
   form_layout = new QFormLayout(groupbox);
-  chart_series_type = new QComboBox(this);
+  form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this));
   chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")});
   chart_series_type->setCurrentIndex(settings.chart_series_type);
-  form_layout->addRow(tr("Chart Default Series Type"), chart_series_type);
 
-  chart_height = new QSpinBox(this);
+  form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this));
   chart_height->setRange(100, 500);
   chart_height->setSingleStep(10);
   chart_height->setValue(settings.chart_height);
-  form_layout->addRow(tr("Chart Height"), chart_height);
   main_layout->addWidget(groupbox);
 
   log_livestream = new QGroupBox(tr("Enable live stream logging"), this);
@@ -125,10 +105,9 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
   path_layout->addWidget(browse_btn);
   main_layout->addWidget(log_livestream);
 
-
-  auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply);
+  auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
   main_layout->addWidget(buttonBox);
-  main_layout->addStretch(1);
+  setFixedSize(400, sizeHint().height());
 
   QObject::connect(browse_btn, &QPushButton::clicked, [this]() {
     QString fn = QFileDialog::getExistingDirectory(
@@ -139,31 +118,22 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
       log_path->setText(fn);
     }
   });
-  QObject::connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) {
-    auto role = buttonBox->buttonRole(button);
-    if (role == QDialogButtonBox::AcceptRole) {
-      save();
-      accept();
-    } else if (role == QDialogButtonBox::ApplyRole) {
-      save();
-    } else if (role == QDialogButtonBox::RejectRole) {
-      reject();
-    }
-  });
+  QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+  QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save);
 }
 
 void SettingsDlg::save() {
-  settings.fps = fps->value();
   if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) {
     // set theme before emit changed
     utils::setTheme(settings.theme);
   }
+  settings.fps = fps->value();
   settings.max_cached_minutes = cached_minutes->value();
   settings.chart_series_type = chart_series_type->currentIndex();
   settings.chart_height = chart_height->value();
   settings.log_livestream = log_livestream->isChecked();
   settings.log_path = log_path->text();
   settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex();
-  settings.save();
   emit settings.changed();
+  QDialog::accept();
 }
diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h
index da7781e6e44e86..9f24a6fbd370f7 100644
--- a/tools/cabana/settings.h
+++ b/tools/cabana/settings.h
@@ -1,13 +1,10 @@
 #pragma once
 
-#include <QApplication>
 #include <QByteArray>
-#include <QCheckBox>
 #include <QComboBox>
 #include <QDialog>
 #include <QGroupBox>
 #include <QLineEdit>
-#include <QSettings>
 #include <QSpinBox>
 
 #define LIGHT_THEME 1
@@ -24,10 +21,8 @@ class Settings : public QObject {
     AlwaysBE,
   };
 
-  Settings() {}
-  QSettings::Status save();
-  void load();
-  inline static QString filePath() { return QApplication::applicationDirPath() + "/settings"; }
+  Settings();
+  ~Settings();
 
   bool absolute_time = false;
   int fps = 10;
@@ -49,15 +44,13 @@ class Settings : public QObject {
   QByteArray window_state;
   QStringList recent_files;
   QByteArray message_header_state;
-  DragDirection drag_direction;
+  DragDirection drag_direction = MsbFirst;
 
 signals:
   void changed();
 };
 
 class SettingsDlg : public QDialog {
-  Q_OBJECT
-
 public:
   SettingsDlg(QWidget *parent);
   void save();
diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc
index 4a6c588e5102eb..13d202d9cab320 100644
--- a/tools/cabana/streams/pandastream.cc
+++ b/tools/cabana/streams/pandastream.cc
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <QCheckBox>
 #include <QLabel>
 #include <QMessageBox>
 #include <QPushButton>
diff --git a/tools/cabana/tools/findsignal.h b/tools/cabana/tools/findsignal.h
index e9e5f9f1808895..5ef7461fee276b 100644
--- a/tools/cabana/tools/findsignal.h
+++ b/tools/cabana/tools/findsignal.h
@@ -4,6 +4,7 @@
 #include <limits>
 
 #include <QAbstractTableModel>
+#include <QCheckBox>
 #include <QLabel>
 #include <QPushButton>
 #include <QTableView>

1. save settings to QSettings::UserScope with QSettings::NativeFormat. On Unix system, it's `$HOME/.config/cabana.conf`. 
2. use template to simplify the read/write operation.