aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CashFlo.pro8
-rw-r--r--TODO.md19
-rw-r--r--cashflow.cpp369
-rw-r--r--cashflow.h9
-rw-r--r--cashflow.ui89
-rw-r--r--multiselectcombobox.cpp175
-rw-r--r--multiselectcombobox.h41
7 files changed, 676 insertions, 34 deletions
diff --git a/CashFlo.pro b/CashFlo.pro
index 920badf..d739ab1 100644
--- a/CashFlo.pro
+++ b/CashFlo.pro
@@ -1,4 +1,4 @@
-QT += core gui sql
+QT += core gui sql charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
@@ -12,13 +12,15 @@ SOURCES += \
main.cpp \
cashflow.cpp \
database.cpp \
- settingsdialog.cpp
+ settingsdialog.cpp \
+ multiselectcombobox.cpp
HEADERS += \
cashflow.h \
database.h \
transaction.h \
- settingsdialog.h
+ settingsdialog.h \
+ multiselectcombobox.h
FORMS += \
cashflow.ui \
diff --git a/TODO.md b/TODO.md
index 3ef0700..65b2515 100644
--- a/TODO.md
+++ b/TODO.md
@@ -50,6 +50,19 @@ transactions table:
- [ ] Account types: Asset vs Liability
- [ ] Net worth dashboard (Assets - Liabilities)
+### Account Management
+- [ ] First-class Account table (id, name, account_type, is_active)
+- [ ] Account management UI in Preferences
+- [ ] Migration: auto-create accounts from existing transaction strings
+- [ ] Update combo boxes to use accounts table
+- [ ] Keep simple: no interest rates, no starting balances (use transactions)
+
+### Attachments / Document Storage
+- [ ] Store scanned receipts/documents as BLOBs in SQLite
+- [ ] Attachment viewer/manager per transaction
+- [ ] File upload dialog
+- [ ] Display attached file count in grid
+
### AI Integration
- [ ] MCP server for AI assistant integration
- [ ] HTTP REST API in Qt app (QHttpServer)
@@ -85,6 +98,12 @@ transactions table:
- ✅ Escape key to deselect
- ✅ Sortable recurring rules table with numeric sorting
- ✅ Delete preserves selection (moves to next row)
+- ✅ Charts tab with balance trend visualization
+- ✅ Multi-account line charts
+- ✅ Hover tooltips showing exact balance at date
+- ✅ Mouse wheel zoom on charts
+- ✅ Auto-fit chart axes to data range
+- ✅ Independent chart date range controls
### UI
diff --git a/cashflow.cpp b/cashflow.cpp
index 6c5ea7d..a8b6a5a 100644
--- a/cashflow.cpp
+++ b/cashflow.cpp
@@ -1,6 +1,7 @@
#include "cashflow.h"
#include "ui_cashflow.h"
#include "settingsdialog.h"
+#include "multiselectcombobox.h"
#include <QMessageBox>
#include <QDebug>
#include <QDir>
@@ -23,6 +24,8 @@ CashFlow::CashFlow(QWidget *parent)
, startingBalance(0.0)
, currentAmountFont("Courier New", 10)
, weekStartDay(1) // Default to Monday
+ , chartView(nullptr)
+ , chart(nullptr)
{
ui->setupUi(this);
@@ -36,6 +39,26 @@ CashFlow::CashFlow(QWidget *parent)
setupConnections();
+ // Initialize chart view
+ chart = new QChart();
+ chart->setTitle("Balance Over Time");
+ chart->setAnimationOptions(QChart::SeriesAnimations);
+
+ chartView = new QChartView(chart);
+ chartView->setRenderHint(QPainter::Antialiasing);
+ chartView->setRubberBand(QChartView::RectangleRubberBand); // Enable zoom with mouse drag
+ chartView->installEventFilter(this); // Install event filter for mouse wheel zoom
+
+ // Add chart view to the chart tab layout
+ QVBoxLayout *chartLayout = ui->chartsTab->findChild<QVBoxLayout*>("chartViewLayout");
+ if (chartLayout) {
+ chartLayout->addWidget(chartView);
+ }
+
+ // Initialize chart date range to match main date range
+ ui->chartDateFromEdit->setDate(QDate::currentDate().addMonths(-1));
+ ui->chartDateToEdit->setDate(QDate::currentDate().addMonths(3));
+
if (!openDatabase(defaultPath)) {
QMessageBox::critical(this, "Database Error", "Failed to open default database: " + database->lastError());
return;
@@ -132,6 +155,27 @@ void CashFlow::keyPressEvent(QKeyEvent *event)
QMainWindow::keyPressEvent(event);
}
+bool CashFlow::eventFilter(QObject *watched, QEvent *event)
+{
+ if (watched == chartView && event->type() == QEvent::Wheel) {
+ QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);
+
+ if (chart->axes().isEmpty()) {
+ return QMainWindow::eventFilter(watched, event);
+ }
+
+ // Zoom factor based on wheel delta
+ double factor = wheelEvent->angleDelta().y() > 0 ? 0.9 : 1.1;
+
+ // Zoom on both axes
+ chart->zoom(factor);
+
+ return true;
+ }
+
+ return QMainWindow::eventFilter(watched, event);
+}
+
void CashFlow::setupConnections() {
// File menu
connect(ui->actionNew, &QAction::triggered, this, &CashFlow::onNewFile);
@@ -147,7 +191,7 @@ void CashFlow::setupConnections() {
connect(ui->dateFromEdit, &QDateEdit::dateChanged, this, &CashFlow::onDateRangeChanged);
connect(ui->dateToEdit, &QDateEdit::dateChanged, this, &CashFlow::onDateRangeChanged);
connect(ui->periodCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onPeriodChanged);
- connect(ui->accountFilterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onDateRangeChanged);
+ connect(ui->accountFilterCombo, &MultiSelectComboBox::selectionChanged, this, &CashFlow::onDateRangeChanged);
connect(ui->recurringFilterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onDateRangeChanged);
connect(ui->showAccountBalancesCheck, &QCheckBox::stateChanged, this, &CashFlow::onDateRangeChanged);
connect(ui->searchEdit, &QLineEdit::textChanged, this, &CashFlow::onSearchTextChanged);
@@ -197,6 +241,10 @@ void CashFlow::setupConnections() {
// Set up Delete key shortcut for recurring table
ui->deleteRecurringBtn->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Delete));
+
+ // Charts tab
+ connect(ui->chartDateFromEdit, &QDateEdit::dateChanged, this, &CashFlow::refreshCharts);
+ connect(ui->chartDateToEdit, &QDateEdit::dateChanged, this, &CashFlow::refreshCharts);
}
void CashFlow::refreshView() {
@@ -204,9 +252,8 @@ void CashFlow::refreshView() {
// Block signals to prevent recursive refresh
ui->accountFilterCombo->blockSignals(true);
- QString currentFilter = ui->accountFilterCombo->currentText();
+ QStringList previouslySelected = ui->accountFilterCombo->getSelectedItems();
ui->accountFilterCombo->clear();
- ui->accountFilterCombo->addItem("All Accounts");
// Get unique account names and categories
QSet<QString> accounts;
@@ -224,7 +271,20 @@ void CashFlow::refreshView() {
sortedAccounts.sort();
ui->accountFilterCombo->addItems(sortedAccounts);
- // Populate entry form account combo
+ // Restore previous selection if possible, otherwise select all
+ if (!previouslySelected.isEmpty()) {
+ QStringList validSelection;
+ for (const QString &account : previouslySelected) {
+ if (sortedAccounts.contains(account)) {
+ validSelection.append(account);
+ }
+ }
+ if (!validSelection.isEmpty()) {
+ ui->accountFilterCombo->setSelectedItems(validSelection);
+ }
+ }
+
+ ui->accountFilterCombo->blockSignals(false);
ui->entryAccountCombo->blockSignals(true);
QString currentAccount = ui->entryAccountCombo->currentText();
ui->entryAccountCombo->clear();
@@ -256,16 +316,9 @@ void CashFlow::refreshView() {
ui->recurringCategoryCombo->addItems(sortedCategories);
ui->recurringCategoryCombo->blockSignals(false);
- // Restore previous selection if possible
- int index = ui->accountFilterCombo->findText(currentFilter);
- if (index >= 0) {
- ui->accountFilterCombo->setCurrentIndex(index);
- }
-
- ui->accountFilterCombo->blockSignals(false);
-
refreshTransactionTable();
refreshRecurringTable();
+ refreshCharts();
calculateAndDisplayBalance();
}
@@ -274,6 +327,34 @@ void CashFlow::refreshTransactionTable() {
ui->transactionTable->setRowCount(0);
+ // Calculate starting balance from all transactions before the date range
+ QDate startDate = ui->dateFromEdit->date();
+ QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems();
+ bool showAllAccounts = ui->accountFilterCombo->allSelected();
+
+ // Get transactions before the visible range
+ QList<Transaction> priorTransactions = database->getTransactions(QDate(1900, 1, 1), startDate.addDays(-1));
+
+ // Filter by selected accounts if applicable
+ if (!showAllAccounts && !selectedAccounts.isEmpty()) {
+ QList<Transaction> filtered;
+ for (const Transaction &t : priorTransactions) {
+ if (selectedAccounts.contains(t.account)) {
+ filtered.append(t);
+ }
+ }
+ priorTransactions = filtered;
+ }
+
+ // Calculate starting balance and per-account balances from prior transactions
+ double calculatedStartingBalance = 0.0;
+ QMap<QString, double> accountBalances; // Track per-account balances
+
+ for (const Transaction &t : priorTransactions) {
+ calculatedStartingBalance += t.amount;
+ accountBalances[t.account] += t.amount;
+ }
+
// Determine if we're showing per-account columns
bool showAccountColumns = ui->showAccountBalancesCheck->isChecked();
@@ -288,6 +369,13 @@ void CashFlow::refreshTransactionTable() {
}
accountList = uniqueAccounts.values();
std::sort(accountList.begin(), accountList.end());
+
+ // Ensure all accounts in accountList are initialized in accountBalances map
+ for (const QString &account : accountList) {
+ if (!accountBalances.contains(account)) {
+ accountBalances[account] = 0.0;
+ }
+ }
}
// Set up columns: Date | Amount | Balance | [Per-account columns] | Account | Category | Description | Type
@@ -307,8 +395,7 @@ void CashFlow::refreshTransactionTable() {
// Enable column reordering by dragging headers
ui->transactionTable->horizontalHeader()->setSectionsMovable(true);
- double runningBalance = startingBalance;
- QMap<QString, double> accountBalances; // Track per-account balances
+ double runningBalance = calculatedStartingBalance;
QDate currentPeriodEnd;
QString periodLabel;
int periodCount = 1;
@@ -769,15 +856,252 @@ void CashFlow::refreshRecurringTable() {
ui->recurringTable->resizeColumnsToContents();
}
+void CashFlow::refreshCharts() {
+ if (!chart || !chartView) return;
+
+ // Clear existing series and axes
+ chart->removeAllSeries();
+
+ // Remove all existing axes
+ for (QAbstractAxis *axis : chart->axes()) {
+ chart->removeAxis(axis);
+ delete axis;
+ }
+
+ // Get chart-specific date range
+ QDate chartStartDate = ui->chartDateFromEdit->date();
+ QDate chartEndDate = ui->chartDateToEdit->date();
+
+ // Get all transactions (not filtered by main date range)
+ QList<Transaction> allTransactions = database->getTransactions(chartStartDate, chartEndDate);
+
+ // Apply account filter from main view
+ QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems();
+ bool showAllAccounts = ui->accountFilterCombo->allSelected();
+
+ // Filter transactions by selected accounts
+ if (!showAllAccounts && !selectedAccounts.isEmpty()) {
+ QList<Transaction> filtered;
+ for (const Transaction &t : allTransactions) {
+ if (selectedAccounts.contains(t.account)) {
+ filtered.append(t);
+ }
+ }
+ allTransactions = filtered;
+ }
+
+ // Sort by date
+ std::sort(allTransactions.begin(), allTransactions.end(),
+ [](const Transaction &a, const Transaction &b) {
+ if (a.date != b.date) return a.date < b.date;
+ if (a.sortOrder != b.sortOrder) return a.sortOrder < b.sortOrder;
+ return a.amount > b.amount;
+ });
+
+ if (allTransactions.isEmpty()) {
+ chart->setTitle("Balance Over Time (No Data)");
+ return;
+ }
+
+ // Get unique accounts
+ QSet<QString> accountSet;
+ for (const Transaction &t : allTransactions) {
+ if (!t.account.isEmpty()) {
+ accountSet.insert(t.account);
+ }
+ }
+ QStringList accounts = accountSet.values();
+ std::sort(accounts.begin(), accounts.end());
+
+ // Filter to only selected accounts
+ if (!showAllAccounts && !selectedAccounts.isEmpty()) {
+ accounts = selectedAccounts;
+ }
+
+ // Create a series for each account
+ QMap<QString, QLineSeries*> seriesMap;
+ QMap<QString, double> accountBalances;
+
+ for (const QString &account : accounts) {
+ QLineSeries *series = new QLineSeries();
+ series->setName(account);
+ seriesMap[account] = series;
+ accountBalances[account] = 0.0;
+
+ // Add starting point
+ series->append(QDateTime(chartStartDate, QTime(0, 0)).toMSecsSinceEpoch(), 0.0);
+ }
+
+ // Also create total balance series if showing multiple accounts
+ QLineSeries *totalSeries = nullptr;
+ double totalBalance = startingBalance;
+ if (showAllAccounts && accounts.size() > 1) {
+ totalSeries = new QLineSeries();
+ totalSeries->setName("Total");
+ totalSeries->append(QDateTime(chartStartDate, QTime(0, 0)).toMSecsSinceEpoch(), totalBalance);
+ }
+
+ // Process transactions and update balances
+ for (const Transaction &t : allTransactions) {
+ if (t.type == TransactionType::Reconciliation) continue;
+
+ // Update account balance
+ if (seriesMap.contains(t.account)) {
+ accountBalances[t.account] += t.amount;
+ seriesMap[t.account]->append(QDateTime(t.date, QTime(0, 0)).toMSecsSinceEpoch(),
+ accountBalances[t.account]);
+ }
+
+ // Update total balance
+ if (totalSeries) {
+ totalBalance += t.amount;
+ totalSeries->append(QDateTime(t.date, QTime(0, 0)).toMSecsSinceEpoch(), totalBalance);
+ }
+ }
+
+ // Add all series to chart
+ for (QLineSeries *series : seriesMap.values()) {
+ chart->addSeries(series);
+ }
+ if (totalSeries) {
+ chart->addSeries(totalSeries);
+ QPen pen = totalSeries->pen();
+ pen.setWidth(3);
+ totalSeries->setPen(pen);
+ }
+
+ // Create axes
+ QDateTimeAxis *axisX = new QDateTimeAxis;
+ axisX->setFormat("MMM dd");
+ axisX->setTitleText("Date");
+ axisX->setTickCount(10);
+ axisX->setRange(QDateTime(chartStartDate, QTime(0, 0)), QDateTime(chartEndDate, QTime(23, 59)));
+ chart->addAxis(axisX, Qt::AlignBottom);
+
+ // Find min/max values across all series
+ double minBalance = 0.0;
+ double maxBalance = 0.0;
+ bool firstValue = true;
+
+ for (QLineSeries *series : seriesMap.values()) {
+ for (const QPointF &point : series->points()) {
+ if (firstValue) {
+ minBalance = maxBalance = point.y();
+ firstValue = false;
+ } else {
+ minBalance = qMin(minBalance, point.y());
+ maxBalance = qMax(maxBalance, point.y());
+ }
+ }
+ }
+ if (totalSeries) {
+ for (const QPointF &point : totalSeries->points()) {
+ if (firstValue) {
+ minBalance = maxBalance = point.y();
+ firstValue = false;
+ } else {
+ minBalance = qMin(minBalance, point.y());
+ maxBalance = qMax(maxBalance, point.y());
+ }
+ }
+ }
+
+ // Add 10% padding to the range
+ double range = maxBalance - minBalance;
+ double padding = range * 0.1;
+ if (range < 0.01) padding = 100; // Minimum padding if range is tiny
+
+ QValueAxis *axisY = new QValueAxis;
+ axisY->setTitleText("Balance ($)");
+ axisY->setLabelFormat("$%.2f");
+ axisY->setRange(minBalance - padding, maxBalance + padding);
+ chart->addAxis(axisY, Qt::AlignLeft);
+
+ // Attach axes to all series
+ for (QLineSeries *series : seriesMap.values()) {
+ series->attachAxis(axisX);
+ series->attachAxis(axisY);
+
+ // Enable hover events for tooltips
+ series->setPointsVisible(false);
+ connect(series, &QLineSeries::hovered, this, [this, series](const QPointF &point, bool state) {
+ if (state) {
+ QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(point.x());
+ QString tooltip = QString("%1\n%2\n$%3")
+ .arg(series->name())
+ .arg(dateTime.date().toString("MMM dd, yyyy"))
+ .arg(formatCurrency(point.y()));
+ chart->setToolTip(tooltip);
+ }
+ });
+ }
+ if (totalSeries) {
+ totalSeries->attachAxis(axisX);
+ totalSeries->attachAxis(axisY);
+
+ // Enable hover events for total series
+ totalSeries->setPointsVisible(false);
+ connect(totalSeries, &QLineSeries::hovered, this, [this, totalSeries](const QPointF &point, bool state) {
+ if (state) {
+ QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(point.x());
+ QString tooltip = QString("%1\n%2\n$%3")
+ .arg(totalSeries->name())
+ .arg(dateTime.date().toString("MMM dd, yyyy"))
+ .arg(formatCurrency(point.y()));
+ chart->setToolTip(tooltip);
+ }
+ });
+ }
+
+ // Set chart title
+ QString title;
+ if (showAllAccounts) {
+ title = "Balance Over Time (All Accounts)";
+ } else if (selectedAccounts.size() == 1) {
+ title = QString("Balance Over Time (%1)").arg(selectedAccounts.first());
+ } else {
+ title = QString("Balance Over Time (%1 Accounts)").arg(selectedAccounts.size());
+ }
+ chart->setTitle(title);
+
+ chart->legend()->setVisible(true);
+ chart->legend()->setAlignment(Qt::AlignBottom);
+}
+
void CashFlow::calculateAndDisplayBalance() {
QList<Transaction> allTransactions = getAllTransactionsInRange();
- double endBalance = startingBalance;
+ // Calculate starting balance from all transactions before the date range
+ QDate startDate = ui->dateFromEdit->date();
+ QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems();
+ bool showAllAccounts = ui->accountFilterCombo->allSelected();
+
+ // Get transactions before the visible range
+ QList<Transaction> priorTransactions = database->getTransactions(QDate(1900, 1, 1), startDate.addDays(-1));
+
+ // Filter by selected accounts if applicable
+ if (!showAllAccounts && !selectedAccounts.isEmpty()) {
+ QList<Transaction> filtered;
+ for (const Transaction &t : priorTransactions) {
+ if (selectedAccounts.contains(t.account)) {
+ filtered.append(t);
+ }
+ }
+ priorTransactions = filtered;
+ }
+
+ // Calculate starting balance from prior transactions
+ double calculatedStartingBalance = 0.0;
+ for (const Transaction &t : priorTransactions) {
+ calculatedStartingBalance += t.amount;
+ }
+
+ double endBalance = calculatedStartingBalance;
for (const Transaction &t : allTransactions) {
endBalance += t.amount;
}
- ui->startBalanceLabel->setText(QString("Starting Balance: $%1").arg(formatCurrency(startingBalance)));
+ ui->startBalanceLabel->setText(QString("Starting Balance: $%1").arg(formatCurrency(calculatedStartingBalance)));
ui->endBalanceLabel->setText(QString("Ending Balance: $%1").arg(formatCurrency(endBalance)));
// Color code the ending balance
@@ -793,23 +1117,18 @@ void CashFlow::calculateAndDisplayBalance() {
QList<Transaction> CashFlow::getAllTransactionsInRange() {
QDate startDate = ui->dateFromEdit->date();
QDate endDate = ui->dateToEdit->date();
- QString accountFilter = ui->accountFilterCombo->currentText();
+ QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems();
int recurringFilter = ui->recurringFilterCombo->currentData().toInt();
QString searchText = ui->searchEdit->text().trimmed().toLower();
- // Safety check
- if (accountFilter.isEmpty()) {
- accountFilter = "All Accounts";
- }
-
// Get all transactions from database (includes both actual and projected)
QList<Transaction> allTransactions = database->getTransactions(startDate, endDate);
- // Filter by account if not "All Accounts"
- if (accountFilter != "All Accounts") {
+ // Filter by selected accounts
+ if (!selectedAccounts.isEmpty() && !ui->accountFilterCombo->allSelected()) {
QList<Transaction> filtered;
for (const Transaction &t : allTransactions) {
- if (t.account == accountFilter) {
+ if (selectedAccounts.contains(t.account)) {
filtered.append(t);
}
}
diff --git a/cashflow.h b/cashflow.h
index 4bc0e7b..92be93d 100644
--- a/cashflow.h
+++ b/cashflow.h
@@ -4,6 +4,11 @@
#include <QMainWindow>
#include <QTableWidget>
#include <QDate>
+#include <QtCharts/QChartView>
+#include <QtCharts/QLineSeries>
+#include <QtCharts/QChart>
+#include <QtCharts/QDateTimeAxis>
+#include <QtCharts/QValueAxis>
#include "database.h"
#include "transaction.h"
@@ -21,6 +26,7 @@ public:
protected:
void keyPressEvent(QKeyEvent *event) override;
+ bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void onDateRangeChanged();
@@ -64,11 +70,14 @@ private:
int weekStartDay;
QString currentFilePath;
QMap<PeriodType, QSet<int>> collapsedPeriods; // Track which period end rows are collapsed per period type
+ QChartView *chartView;
+ QChart *chart;
void setupConnections();
void refreshView();
void refreshTransactionTable();
void refreshRecurringTable();
+ void refreshCharts();
void calculateAndDisplayBalance();
QList<Transaction> getAllTransactionsInRange();
void clearTransactionEntry();
diff --git a/cashflow.ui b/cashflow.ui
index 7f35845..14d4562 100644
--- a/cashflow.ui
+++ b/cashflow.ui
@@ -90,12 +90,7 @@
</widget>
</item>
<item>
- <widget class="QComboBox" name="accountFilterCombo">
- <item>
- <property name="text">
- <string>All Accounts</string>
- </property>
- </item>
+ <widget class="MultiSelectComboBox" name="accountFilterCombo">
</widget>
</item>
<item>
@@ -630,6 +625,81 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="chartsTab">
+ <attribute name="title">
+ <string>Charts</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_charts">
+ <item>
+ <layout class="QHBoxLayout" name="chartControlsLayout">
+ <item>
+ <widget class="QLabel" name="chartTypeLabel">
+ <property name="text">
+ <string>Chart Type:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="chartTypeCombo">
+ <item>
+ <property name="text">
+ <string>Balance Trend</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="chartDateFromLabel">
+ <property name="text">
+ <string>From:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDateEdit" name="chartDateFromEdit">
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="chartDateToLabel">
+ <property name="text">
+ <string>To:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDateEdit" name="chartDateToEdit">
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_chart">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="chartViewLayout">
+ <property name="objectName">
+ <string notr="true">chartViewLayout</string>
+ </property>
+ </layout>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
</layout>
@@ -714,6 +784,13 @@
</property>
</action>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>MultiSelectComboBox</class>
+ <extends>QComboBox</extends>
+ <header>multiselectcombobox.h</header>
+ </customwidget>
+ </customwidgets>
<resources/>
<connections/>
</ui>
diff --git a/multiselectcombobox.cpp b/multiselectcombobox.cpp
new file mode 100644
index 0000000..e1fe324
--- /dev/null
+++ b/multiselectcombobox.cpp
@@ -0,0 +1,175 @@
+#include "multiselectcombobox.h"
+#include <QListView>
+#include <QMouseEvent>
+#include <QCheckBox>
+
+MultiSelectComboBox::MultiSelectComboBox(QWidget *parent)
+ : QComboBox(parent)
+ , suppressUpdate(false)
+{
+ model = new QStandardItemModel(this);
+ setModel(model);
+
+ // Use list view for better checkbox display
+ QListView *listView = new QListView(this);
+ setView(listView);
+
+ // Create a line edit to display the text (read-only)
+ displayLabel = new QLineEdit(this);
+ displayLabel->setReadOnly(true);
+ setLineEdit(displayLabel);
+
+ // Install event filter to keep popup open on click
+ view()->viewport()->installEventFilter(this);
+
+ connect(view(), &QAbstractItemView::pressed, this, &MultiSelectComboBox::onItemPressed);
+}
+
+void MultiSelectComboBox::addItem(const QString &text)
+{
+ // First item is "Select All"
+ if (model->rowCount() == 0) {
+ QStandardItem *selectAllItem = new QStandardItem("Select All");
+ selectAllItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
+ selectAllItem->setData(Qt::Checked, Qt::CheckStateRole);
+ selectAllItem->setData(true, Qt::UserRole); // Mark as "Select All" item
+ model->appendRow(selectAllItem);
+ }
+
+ QStandardItem *item = new QStandardItem(text);
+ item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
+ item->setData(Qt::Checked, Qt::CheckStateRole);
+ item->setData(false, Qt::UserRole); // Regular item
+ model->appendRow(item);
+}
+
+void MultiSelectComboBox::addItems(const QStringList &texts)
+{
+ for (const QString &text : texts) {
+ addItem(text);
+ }
+ updateText();
+}
+
+QStringList MultiSelectComboBox::getSelectedItems() const
+{
+ QStringList selected;
+ for (int i = 1; i < model->rowCount(); ++i) { // Skip "Select All" at index 0
+ QStandardItem *item = model->item(i);
+ if (item && item->checkState() == Qt::Checked) {
+ selected.append(item->text());
+ }
+ }
+ return selected;
+}
+
+void MultiSelectComboBox::setSelectedItems(const QStringList &items)
+{
+ suppressUpdate = true;
+
+ for (int i = 1; i < model->rowCount(); ++i) { // Skip "Select All" at index 0
+ QStandardItem *item = model->item(i);
+ if (item) {
+ bool shouldCheck = items.contains(item->text());
+ item->setCheckState(shouldCheck ? Qt::Checked : Qt::Unchecked);
+ }
+ }
+
+ // Update "Select All" checkbox
+ if (model->rowCount() > 1) {
+ QStandardItem *selectAllItem = model->item(0);
+ if (selectAllItem) {
+ selectAllItem->setCheckState(allSelected() ? Qt::Checked : Qt::Unchecked);
+ }
+ }
+
+ suppressUpdate = false;
+ updateText();
+}
+
+bool MultiSelectComboBox::allSelected() const
+{
+ for (int i = 1; i < model->rowCount(); ++i) { // Skip "Select All" at index 0
+ QStandardItem *item = model->item(i);
+ if (item && item->checkState() == Qt::Unchecked) {
+ return false;
+ }
+ }
+ return model->rowCount() > 1;
+}
+
+void MultiSelectComboBox::clear()
+{
+ model->clear();
+ QComboBox::clear();
+}
+
+void MultiSelectComboBox::hidePopup()
+{
+ // Don't hide on item click - only when clicking outside
+ QComboBox::hidePopup();
+ updateText();
+ emit selectionChanged();
+}
+
+bool MultiSelectComboBox::eventFilter(QObject *watched, QEvent *event)
+{
+ if (watched == view()->viewport()) {
+ if (event->type() == QEvent::MouseButtonRelease) {
+ return true; // Prevent popup from closing
+ }
+ }
+ return QComboBox::eventFilter(watched, event);
+}
+
+void MultiSelectComboBox::onItemPressed(const QModelIndex &index)
+{
+ QStandardItem *item = model->itemFromIndex(index);
+ if (item) {
+ // Check if this is the "Select All" item
+ bool isSelectAll = item->data(Qt::UserRole).toBool();
+
+ if (isSelectAll) {
+ // Toggle all items
+ Qt::CheckState newState = (item->checkState() == Qt::Checked) ? Qt::Unchecked : Qt::Checked;
+ suppressUpdate = true;
+ for (int i = 0; i < model->rowCount(); ++i) {
+ QStandardItem *childItem = model->item(i);
+ if (childItem) {
+ childItem->setCheckState(newState);
+ }
+ }
+ suppressUpdate = false;
+ } else {
+ // Toggle individual item
+ Qt::CheckState newState = (item->checkState() == Qt::Checked) ? Qt::Unchecked : Qt::Checked;
+ item->setCheckState(newState);
+
+ // Update "Select All" state
+ if (model->rowCount() > 1) {
+ QStandardItem *selectAllItem = model->item(0);
+ if (selectAllItem) {
+ selectAllItem->setCheckState(allSelected() ? Qt::Checked : Qt::Unchecked);
+ }
+ }
+ }
+ updateText();
+ }
+}
+
+void MultiSelectComboBox::updateText()
+{
+ if (suppressUpdate) return;
+
+ QStringList selected = getSelectedItems();
+
+ if (selected.isEmpty()) {
+ displayLabel->setText("No Accounts");
+ } else if (allSelected()) {
+ displayLabel->setText("All Accounts");
+ } else if (selected.size() == 1) {
+ displayLabel->setText(selected.first());
+ } else {
+ displayLabel->setText(QString("%1 Accounts Selected").arg(selected.size()));
+ }
+}
diff --git a/multiselectcombobox.h b/multiselectcombobox.h
new file mode 100644
index 0000000..2478fae
--- /dev/null
+++ b/multiselectcombobox.h
@@ -0,0 +1,41 @@
+#ifndef MULTISELECTCOMBOBOX_H
+#define MULTISELECTCOMBOBOX_H
+
+#include <QComboBox>
+#include <QStandardItemModel>
+#include <QStringList>
+#include <QEvent>
+#include <QLineEdit>
+
+class MultiSelectComboBox : public QComboBox
+{
+ Q_OBJECT
+
+public:
+ explicit MultiSelectComboBox(QWidget *parent = nullptr);
+
+ void addItem(const QString &text);
+ void addItems(const QStringList &texts);
+ QStringList getSelectedItems() const;
+ void setSelectedItems(const QStringList &items);
+ bool allSelected() const;
+ void clear();
+
+signals:
+ void selectionChanged();
+
+protected:
+ void hidePopup() override;
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+private slots:
+ void onItemPressed(const QModelIndex &index);
+ void updateText();
+
+private:
+ QStandardItemModel *model;
+ bool suppressUpdate;
+ QLineEdit *displayLabel;
+};
+
+#endif // MULTISELECTCOMBOBOX_H