feat(balloon): refactor balloon handling and introduce BalloonProcess class

This commit is contained in:
uncor3
2026-02-25 07:57:00 +00:00
parent bfe5d4f33b
commit 222b89f7e4
5 changed files with 376 additions and 598 deletions
+32 -4
View File
@@ -647,11 +647,13 @@ struct ExportItem {
QString sourcePathOnDevice;
QString suggestedFileName;
int itemIndex = -1;
std::string d_udid;
ExportItem() = default;
ExportItem(const QString &sourcePath, const QString &fileName, int index)
ExportItem(const QString &sourcePath, const QString &fileName,
std::string d_udid, int index)
: sourcePathOnDevice(sourcePath), suggestedFileName(fileName),
itemIndex(index)
d_udid(d_udid), itemIndex(index)
{
}
};
@@ -676,10 +678,36 @@ struct ExportJobSummary {
struct ExportJob {
QUuid jobId;
iDescriptorDevice *device = nullptr;
QList<ExportItem> items;
QString destinationPath;
std::optional<AfcClientHandle *> altAfc;
std::atomic<bool> cancelRequested{false};
QUuid statusBalloonProcessId;
};
// device udid
std::string d_udid;
};
inline QString formatFileSize(qint64 bytes)
{
const qint64 KB = 1024;
const qint64 MB = KB * 1024;
const qint64 GB = MB * 1024;
if (bytes >= GB) {
return QString("%1 GB").arg(
QString::number(bytes / double(GB), 'f', 2));
} else if (bytes >= MB) {
return QString("%1 MB").arg(
QString::number(bytes / double(MB), 'f', 1));
} else if (bytes >= KB) {
return QString("%1 KB").arg(
QString::number(bytes / double(KB), 'f', 0));
} else {
return QString("%1 B").arg(bytes);
}
}
inline QString formatTransferRate(qint64 bytesPerSecond)
{
return formatFileSize(bytesPerSecond) + "/s";
}
+60 -149
View File
@@ -15,49 +15,40 @@
#include <QTimerEvent>
#include <qpainterpath.h>
static QBalloonTip *theSolitaryBalloonTip = nullptr;
void QBalloonTip::showBalloon(const QIcon &icon, const QString &title,
const QString &message, QWidget *widget,
const QPoint &pos, int timeout, bool showArrow)
void QBalloonTip::toggleBaloon(const QPoint &pos, int timeout,
bool forceVisible)
{
hideBalloon();
if (message.isEmpty() && title.isEmpty())
if (m_visible && !forceVisible) {
hideBalloon();
return;
}
theSolitaryBalloonTip = new QBalloonTip(icon, title, message, widget);
if (timeout < 0)
timeout = 10000; // 10 s default
theSolitaryBalloonTip->balloon(pos, timeout, showArrow);
balloon(pos, timeout);
}
void QBalloonTip::hideBalloon()
{
if (!theSolitaryBalloonTip)
return;
theSolitaryBalloonTip->hide();
// delete theSolitaryBalloonTip;
theSolitaryBalloonTip = nullptr;
m_visible = false;
hide();
}
void QBalloonTip::updateBalloonPosition(const QPoint &pos)
{
if (!theSolitaryBalloonTip)
return;
theSolitaryBalloonTip->hide();
theSolitaryBalloonTip->balloon(pos, 0, theSolitaryBalloonTip->showArrow);
hideBalloon();
balloon(pos, 0);
}
bool QBalloonTip::isBalloonVisible() { return theSolitaryBalloonTip; }
bool QBalloonTip::isBalloonVisible() { return m_visible; }
QBalloonTip::QBalloonTip(const QIcon &icon, const QString &title,
const QString &message, QWidget *widget)
QBalloonTip::QBalloonTip(QWidget *widget)
: QWidget(widget ? widget->window() : QApplication::activeWindow(),
Qt::ToolTip),
widget(widget), showArrow(true)
widget(widget)
{
// setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_TranslucentBackground);
// setAttribute(Qt::WA_TranslucentBackground);
if (widget) {
connect(widget, &QWidget::destroyed, this, &QBalloonTip::close);
} else if (QApplication::activeWindow()) {
@@ -119,146 +110,64 @@ QBalloonTip::QBalloonTip(const QIcon &icon, const QString &title,
// setLayout(layout);
}
QBalloonTip::~QBalloonTip() { theSolitaryBalloonTip = nullptr; }
QBalloonTip::~QBalloonTip() {}
void QBalloonTip::paintEvent(QPaintEvent *ev)
{
QPainter painter(this);
painter.drawPixmap(rect(), pixmap);
// QPainter painter(this);
// painter.drawPixmap(rect(), pixmap);
QWidget::paintEvent(ev);
}
void QBalloonTip::resizeEvent(QResizeEvent *ev) { QWidget::resizeEvent(ev); }
void QBalloonTip::balloon(const QPoint &pos, int msecs, bool showArrow)
void QBalloonTip::balloon(const QPoint &pos, int msecs)
{
// this->showArrow = showArrow;
// QScreen *screen = QGuiApplication::screenAt(pos);
// if (!screen)
// screen = QGuiApplication::primaryScreen();
// QRect screenRect = screen->geometry();
// QSize sh = sizeHint();
// const int border = 1;
// const int ah = 18, aw = 18, rc = 7;
// bool arrowAtTop = (pos.y() + sh.height() + ah < screenRect.height());
// setContentsMargins(border + 3, border + (arrowAtTop ? ah : 0) + 2,
// border + 3, border + (arrowAtTop ? 0 : ah) + 2);
// updateGeometry();
// sh = sizeHint();
// // Center the balloon relative to the pos point (button center)
// int balloonX = pos.x() - sh.width() / 2;
// // Calculate arrow offset from left edge of balloon to center it
// int ao = sh.width() / 2 - aw / 2; // Center the arrow on the balloon
// int ml, mr, mt, mb;
// QSize sz = sizeHint();
// if (!arrowAtTop) {
// ml = mt = 0;
// mr = sz.width() - 1;
// mb = sz.height() - ah - 1;
// } else {
// ml = 0;
// mt = ah;
// mr = sz.width() - 1;
// mb = sz.height() - 1;
// }
// QPainterPath path;
// path.moveTo(ml + rc, mt);
// if (arrowAtTop) {
// if (showArrow) {
// path.lineTo(ml + ao, mt);
// path.lineTo(ml + ao + aw / 2, mt - ah);
// path.lineTo(ml + ao + aw, mt);
// }
// move(qBound(screenRect.left() + 2, balloonX,
// screenRect.right() - sh.width() - 2),
// pos.y());
// }
// path.lineTo(mr - rc, mt);
// path.arcTo(QRect(mr - rc * 2, mt, rc * 2, rc * 2), 90, -90);
// path.lineTo(mr, mb - rc);
// path.arcTo(QRect(mr - rc * 2, mb - rc * 2, rc * 2, rc * 2), 0, -90);
// if (!arrowAtTop) {
// if (showArrow) {
// path.lineTo(mr - ao - aw, mb);
// path.lineTo(mr - ao - aw / 2, mb + ah);
// path.lineTo(mr - ao, mb);
// }
// move(qBound(screenRect.left() + 2, balloonX,
// screenRect.right() - sh.width() - 2),
// pos.y() - sh.height());
// }
// path.lineTo(ml + rc, mb);
// path.arcTo(QRect(ml, mb - rc * 2, rc * 2, rc * 2), -90, -90);
// path.lineTo(ml, mt + rc);
// path.arcTo(QRect(ml, mt, rc * 2, rc * 2), 180, -90);
// // Set the mask
// QBitmap bitmap = QBitmap(sizeHint());
// bitmap.fill(Qt::color0);
// QPainter painter1(&bitmap);
// painter1.setPen(QPen(Qt::color1, border));
// painter1.setBrush(QBrush(Qt::color1));
// painter1.drawPath(path);
// setMask(bitmap);
// // Draw the border with background color
// pixmap = QPixmap(sz);
// pixmap.fill(Qt::transparent);
// QPainter painter2(&pixmap);
// painter2.setRenderHint(QPainter::Antialiasing);
// bool isDark = isDarkMode();
// QColor lightColor = qApp->palette().color(QPalette::Light);
// QColor darkColor = qApp->palette().color(QPalette::Dark);
// QColor bgColor = isDark ? lightColor : darkColor;
// painter2.setPen(QPen(bgColor.darker(160), border));
// painter2.setBrush(bgColor);
// painter2.drawPath(path);
if (msecs > 0)
timer.start(msecs, this);
// Install event filter to detect clicks outside
m_visible = true;
qApp->installEventFilter(this);
// // Set initial scale and opacity for animation
// setWindowOpacity(0.0);
QScreen *screen = QGuiApplication::screenAt(pos);
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
QRect scr = screen->availableGeometry();
const int border = 1;
const int ah = 18, ao = 18, aw = 18, rc = 7;
// bool arrowAtTop = (pos.y() + sh.height() + ah < scr.height());
// bool arrowAtLeft = (pos.x() + sh.width() - ao < scr.width());
// setContentsMargins(border + 3, border + (arrowAtTop ? ah : 0) + 2,
// border + 3, border + (arrowAtTop ? 0 : ah) + 2);
updateGeometry();
QSize sz = sizeHint();
QRect screenRect = screen->availableGeometry();
// Store the transform origin point (center of the widget)
QPoint center = rect().center();
setProperty("transformOriginPoint", center);
// Calculate the total required size for the balloon widget, including
// potential arrow space. Assuming the arrow takes up 'ah' height at either
// the top or bottom of the widget.
QSize sh_total = QSize(sz.width(), sz.height() + ah);
// Determine the desired X position: center the balloon horizontally on
// pos.x 'pos' is the global bottom-center of your button.
int targetX = pos.x() - sh_total.width() / 2;
// Clamp X position to screen bounds
targetX = qBound(screenRect.left(), targetX,
screenRect.right() - sh_total.width());
// Determine the desired Y position: Place the bottom of the balloon at
// pos.y() (button's bottom) This makes the balloon appear ABOVE the button.
int targetY = pos.y() - sh_total.height();
// Clamp Y position to screen bounds
targetY = qBound(screenRect.top(), targetY,
screenRect.bottom() - sh_total.height());
// Apply the calculated position
move(targetX, targetY);
// if (msecs > 0)
// timer = startTimer(msecs);
show();
// Create scale and opacity animations
QPropertyAnimation *scaleAnim = new QPropertyAnimation(this, "geometry");
scaleAnim->setDuration(200);
scaleAnim->setEasingCurve(QEasingCurve::OutBack);
// Calculate scaled geometry (start from 80% size)
QRect finalGeometry = geometry();
QRect startGeometry = finalGeometry;
int widthDiff = finalGeometry.width() * 0.2;
int heightDiff = finalGeometry.height() * 0.2;
startGeometry.adjust(widthDiff / 2, heightDiff / 2, -widthDiff / 2,
-heightDiff / 2);
scaleAnim->setStartValue(startGeometry);
scaleAnim->setEndValue(finalGeometry);
// QPropertyAnimation *opacityAnim =
// new QPropertyAnimation(this, "windowOpacity");
// opacityAnim->setDuration(200);
// opacityAnim->setStartValue(0.0);
// opacityAnim->setEndValue(1.0);
// opacityAnim->setEasingCurve(QEasingCurve::OutCubic);
// scaleAnim->start(QAbstractAnimation::DeleteWhenStopped);
// opacityAnim->start(QAbstractAnimation::DeleteWhenStopped);
raise();
activateWindow();
}
void QBalloonTip::mousePressEvent(QMouseEvent *e)
@@ -285,12 +194,14 @@ bool QBalloonTip::eventFilter(QObject *obj, QEvent *event)
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
// Check if click is outside the balloon
if (!geometry().contains(mouseEvent->globalPos())) {
m_visible = false;
close();
return false;
}
} else if (event->type() == QEvent::WindowDeactivate) {
// Close when window loses focus
if (obj == this) {
m_visible = false;
close();
return false;
}
+4 -8
View File
@@ -10,15 +10,12 @@ class QBalloonTip : public QWidget
{
Q_OBJECT
public:
explicit QBalloonTip(const QIcon &icon, const QString &title,
const QString &msg, QWidget *widget);
explicit QBalloonTip(QWidget *widget);
void hideBalloon();
bool isBalloonVisible();
void updateBalloonPosition(const QPoint &pos);
void showBalloon(const QIcon &icon, const QString &title,
const QString &msg, QWidget *widget, const QPoint &pos,
int timeout, bool showArrow = true);
void balloon(const QPoint &, int, bool);
void toggleBaloon(const QPoint &pos, int timeout, bool forceVisible);
void balloon(const QPoint &, int msecs);
signals:
void messageClicked();
@@ -34,8 +31,7 @@ protected:
private:
QWidget *widget;
QPixmap pixmap;
QBasicTimer timer;
bool showArrow;
bool m_visible = false;
};
#endif // QBALLOONTIP_H
+262 -421
View File
@@ -30,290 +30,18 @@
#include "platform/windows/win_common.h"
#endif
Process::Process(QWidget *parent) : QWidget(parent) {}
StatusBalloon *StatusBalloon::sharedInstance()
BalloonProcess::BalloonProcess(ProcessItem *item, QWidget *parent)
: QWidget(parent), m_item(item)
{
static StatusBalloon instance;
return &instance;
}
StatusBalloon::StatusBalloon(QWidget *parent)
: QBalloonTip(QIcon(), "", "", parent)
{
setMinimumHeight(300);
setMinimumWidth(300);
#ifdef WIN32
// FIXME: doesnt work the second time we call it
enableAcrylic((HWND)winId());
#endif
// Create main layout
m_mainLayout = new QVBoxLayout();
m_mainLayout->setSpacing(8);
m_mainLayout->setContentsMargins(12, 12, 12, 12);
// Header label
m_headerLabel = new QLabel("Processes");
QFont headerFont = m_headerLabel->font();
headerFont.setPointSize(headerFont.pointSize() + 2);
headerFont.setBold(true);
m_headerLabel->setFont(headerFont);
m_mainLayout->addWidget(m_headerLabel);
// Container for processes
m_processesContainer = new QWidget();
m_processesLayout = new QVBoxLayout(m_processesContainer);
m_processesLayout->setSpacing(12);
m_processesLayout->setContentsMargins(0, 0, 0, 0);
m_mainLayout->addWidget(m_processesContainer);
setLayout(m_mainLayout);
connect(m_button, &ZIconWidget::clicked, this, &StatusBalloon::showBalloon);
connectExportThreadSignals();
}
void StatusBalloon::connectExportThreadSignals()
{
ExportManager *exportManager = ExportManager::sharedInstance();
connect(exportManager->m_exportThread, &ExportManagerThread::exportFinished,
this, &StatusBalloon::onExportFinished);
connect(exportManager->m_exportThread, &ExportManagerThread::itemExported,
this, &StatusBalloon::onItemExported);
connect(exportManager->m_exportThread,
&ExportManagerThread::fileTransferProgress, this,
&StatusBalloon::onFileTransferProgress);
// QTimer::singleShot(0, this, [this]() {
// // test
// startExportProcess("Test Export Process", 10,
// "/path/to/destination");
// });
}
void StatusBalloon::onFileTransferProgress(const QUuid &processId,
int currentItem,
const QString &currentFile,
qint64 bytesTransferred,
qint64 totalBytes)
{
qDebug() << "StatusBalloon::updateProcessProgress entry:" << processId
<< currentItem << currentFile << bytesTransferred << totalBytes;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::updateProcessProgress: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
item->completedItems = currentItem;
item->currentFile = currentFile;
item->transferredBytes = bytesTransferred;
item->totalBytes = totalBytes;
if (!item->processWidget)
qDebug()
<< "StatusBalloon::updateProcessProgress: no widget for processId"
<< processId;
// Update status label
QString statusText;
if (item->status == ProcessStatus::Running) {
if (!item->currentFile.isEmpty()) {
statusText = item->currentFile;
} else {
statusText = "Processing...";
}
} else if (item->status == ProcessStatus::Completed) {
statusText = "Completed successfully";
} else if (item->status == ProcessStatus::Failed) {
statusText = "Failed";
} else if (item->status == ProcessStatus::Cancelled) {
statusText = "Cancelled";
}
item->statusLabel->setText(statusText);
// Update progress bar
// progess should be based on exported bytes vs total bytes of the current
// file
if (item->totalItems > 0) {
int progress = (item->transferredBytes * 100) / item->totalBytes;
item->progressBar->setValue(progress);
}
// Update stats
QString statsText = QString("%1 of %2 items")
.arg(item->completedItems)
.arg(item->totalItems);
if (item->failedItems > 0) {
statsText += QString(" • %1 failed").arg(item->failedItems);
}
if (item->status == ProcessStatus::Running && item->transferredBytes > 0) {
// Calculate transfer rate
QDateTime now = QDateTime::currentDateTime();
qint64 elapsed = m_lastUpdateTime[item->processId].msecsTo(now);
if (elapsed > 0) {
qint64 bytesDiff = item->transferredBytes -
m_lastBytesTransferred[item->processId];
qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed;
if (bytesPerSecond > 0) {
statsText += "" + formatTransferRate(bytesPerSecond);
}
m_lastBytesTransferred[item->processId] = item->transferredBytes;
m_lastUpdateTime[item->processId] = now;
}
}
item->statsLabel->setText(statsText);
// Update buttons
if (item->status == ProcessStatus::Running) {
item->cancelButton->setVisible(true);
item->actionButton->setVisible(false);
} else {
item->cancelButton->setVisible(false);
if (item->type == ProcessType::Export &&
item->status == ProcessStatus::Completed) {
item->actionButton->setVisible(true);
}
}
}
// todo fix these
// StatusBalloon::onItemExported entry:
// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}") Success: true
// StatusBalloon::onItemExported: unknown processId
// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}")
// StatusBalloon::onExportFinished entry:
// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}") WasCancelled: false
// StatusBalloon::onExportFinished: unknown processId
// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}")
void StatusBalloon::onExportFinished(const QUuid &processId,
const ExportJobSummary &summary)
{
qDebug() << "StatusBalloon::onExportFinished entry:" << processId
<< "WasCancelled:" << summary.wasCancelled;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::onExportFinished: unknown processId"
<< processId;
return;
}
// todo: handle failed ?
ProcessItem *item = m_processes[processId];
if (summary.wasCancelled) {
item->status = ProcessStatus::Cancelled;
} else {
item->status = ProcessStatus::Completed;
}
item->endTime = QDateTime::currentDateTime();
updateUI();
}
void StatusBalloon::onItemExported(const QUuid &processId,
const ExportResult &result)
{
qDebug() << "StatusBalloon::onItemExported entry:" << processId
<< "Success:" << result.success;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::onItemExported: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
if (result.success) {
item->completedItems += 1;
} else {
item->failedItems += 1;
}
updateUI();
}
QUuid StatusBalloon::startExportProcess(const QString &title, int totalItems,
const QString &destinationPath)
{
qDebug() << "StatusBalloon::startExportProcess entry:" << title
<< totalItems << destinationPath;
// allocate item first so it can be used after unlocking
auto *item = new ProcessItem();
item->processId = QUuid::createUuid();
item->type = ProcessType::Export;
item->status = ProcessStatus::Running;
item->title = title;
item->totalItems = totalItems;
item->completedItems = 0;
item->failedItems = 0;
item->totalBytes = 0;
item->transferredBytes = 0;
item->startTime = QDateTime::currentDateTime();
item->destinationPath = destinationPath;
{ // scope the lock only for shared-state mutation
QMutexLocker locker(&m_processesMutex);
m_processes[item->processId] = item;
m_currentProcessId = item->processId;
m_lastBytesTransferred[item->processId] = 0;
m_lastUpdateTime[item->processId] = QDateTime::currentDateTime();
} // mutex released here
// UI work must run without holding m_processesMutex to avoid re-locking
// deadlock
createProcessWidget(item);
updateUI();
return item->processId;
}
QUuid StatusBalloon::startUploadProcess(const QString &title, int totalItems)
{
// allocate item first
auto *item = new ProcessItem();
item->processId = QUuid::createUuid();
item->type = ProcessType::Upload;
item->status = ProcessStatus::Running;
item->title = title;
item->totalItems = totalItems;
item->completedItems = 0;
item->failedItems = 0;
item->totalBytes = 0;
item->transferredBytes = 0;
item->startTime = QDateTime::currentDateTime();
{ // scope the lock only for shared-state mutation
QMutexLocker locker(&m_processesMutex);
m_processes[item->processId] = item;
m_currentProcessId = item->processId;
m_lastBytesTransferred[item->processId] = 0;
m_lastUpdateTime[item->processId] = QDateTime::currentDateTime();
} // mutex released here
createProcessWidget(item);
updateUI();
return item->processId;
}
void StatusBalloon::createProcessWidget(ProcessItem *item)
{
item->processWidget = new QWidget();
auto *layout = new QVBoxLayout(item->processWidget);
auto *layout = new QVBoxLayout(this);
layout->setSpacing(6);
layout->setContentsMargins(0, 0, 0, 0);
m_lastBytesTransferred = 0;
m_lastUpdateTime = QDateTime::currentDateTime();
// Title
item->titleLabel = new QLabel(item->title);
item->titleLabel = new QLabel(m_item->title);
QFont titleFont = item->titleLabel->font();
titleFont.setBold(true);
item->titleLabel->setFont(titleFont);
@@ -347,7 +75,8 @@ void StatusBalloon::createProcessWidget(ProcessItem *item)
item->actionButton->setVisible(false);
if (item->type == ProcessType::Export) {
item->actionButton->setText("Open Folder");
connect(item->actionButton, &QPushButton::clicked, this,
connect(item->actionButton, &QPushButton::clicked,
StatusBalloon::sharedInstance(),
&StatusBalloon::onOpenFolderClicked);
}
buttonsLayout->addWidget(item->actionButton);
@@ -356,85 +85,263 @@ void StatusBalloon::createProcessWidget(ProcessItem *item)
// Cancel button
item->cancelButton = new QPushButton("Cancel");
connect(item->cancelButton, &QPushButton::clicked, this,
&StatusBalloon::onCancelClicked);
connect(item->cancelButton, &QPushButton::clicked,
StatusBalloon::sharedInstance(), &StatusBalloon::onCancelClicked);
buttonsLayout->addWidget(item->cancelButton);
layout->addLayout(buttonsLayout);
m_processesLayout->addWidget(item->processWidget);
}
void StatusBalloon::markProcessCompleted(const QUuid &processId)
void BalloonProcess::setProgress(int progress)
{
QMutexLocker locker(&m_processesMutex);
m_item->progressBar->setValue(progress);
}
if (!m_processes.contains(processId)) {
return;
void BalloonProcess::updateStats()
{
QString statsText = QString("%1 of %2 items")
.arg(m_item->completedItems)
.arg(m_item->totalItems);
if (m_item->failedItems > 0) {
statsText += QString(" • %1 failed").arg(m_item->failedItems);
}
ProcessItem *item = m_processes[processId];
item->status = ProcessStatus::Completed;
item->endTime = QDateTime::currentDateTime();
if (m_item->status == ProcessStatus::Running &&
m_item->transferredBytes > 0) {
// Calculate transfer rate
QDateTime now = QDateTime::currentDateTime();
qint64 elapsed = m_lastUpdateTime.msecsTo(now);
if (elapsed > 0) {
qint64 bytesDiff =
m_item->transferredBytes - m_lastBytesTransferred;
qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed;
if (bytesPerSecond > 0) {
statsText += "" + formatTransferRate(bytesPerSecond);
}
m_lastBytesTransferred = m_item->transferredBytes;
m_lastUpdateTime = now;
}
}
updateUI();
m_item->statsLabel->setText(statsText);
}
// Check if all processes are done
bool allDone = true;
for (auto *proc : m_processes) {
if (proc->status == ProcessStatus::Running) {
allDone = false;
break;
void BalloonProcess::updateButtons()
{
// Update buttons
if (m_item->status == ProcessStatus::Running) {
m_item->cancelButton->setVisible(true);
m_item->actionButton->setVisible(false);
} else {
m_item->cancelButton->setVisible(false);
if (m_item->type == ProcessType::Export &&
m_item->status == ProcessStatus::Completed) {
m_item->actionButton->setVisible(true);
}
}
}
void StatusBalloon::markProcessFailed(const QUuid &processId,
const QString &error)
StatusBalloon *StatusBalloon::sharedInstance()
{
QMutexLocker locker(&m_processesMutex);
static StatusBalloon instance;
return &instance;
}
StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent)
{
setMinimumHeight(300);
setMinimumWidth(300);
#ifdef WIN32
// FIXME: doesnt work the second time we call it
enableAcrylic((HWND)winId());
#endif
// Create main layout
m_mainLayout = new QVBoxLayout();
m_mainLayout->setSpacing(8);
m_mainLayout->setContentsMargins(0, 0, 0, 0);
// Header label
m_headerLabel = new QLabel("Processes");
QFont headerFont = m_headerLabel->font();
headerFont.setPointSize(headerFont.pointSize() + 2);
headerFont.setBold(true);
m_headerLabel->setFont(headerFont);
m_mainLayout->addWidget(m_headerLabel);
// Container for processes
m_processesContainer = new QWidget();
m_processesLayout = new QVBoxLayout(m_processesContainer);
QScrollArea *scrollArea = new QScrollArea();
scrollArea->setWidget(m_processesContainer);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setStyleSheet(
"QScrollArea { background: transparent; border: none; }");
scrollArea->viewport()->setStyleSheet("background: transparent;");
m_processesLayout->setSpacing(12);
m_processesLayout->setContentsMargins(5, 5, 5, 5);
m_mainLayout->addWidget(scrollArea);
setLayout(m_mainLayout);
connect(m_button, &ZIconWidget::clicked, this, &StatusBalloon::handleShow);
connectExportThreadSignals();
}
void StatusBalloon::connectExportThreadSignals()
{
ExportManager *exportManager = ExportManager::sharedInstance();
connect(exportManager->m_exportThread, &ExportManagerThread::exportFinished,
this, &StatusBalloon::onExportFinished);
connect(exportManager->m_exportThread, &ExportManagerThread::itemExported,
this, &StatusBalloon::onItemExported);
connect(exportManager->m_exportThread,
&ExportManagerThread::fileTransferProgress, this,
&StatusBalloon::onFileTransferProgress);
QTimer::singleShot(3000, this, [this]() {
// test
startExportProcess("Test Export Process", 10, "/path/to/destination");
});
}
void StatusBalloon::onFileTransferProgress(const QUuid &processId,
int currentItem,
const QString &currentFile,
qint64 bytesTransferred,
qint64 totalBytes)
{
qDebug() << "StatusBalloon::updateProcessProgress";
// QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::updateProcessProgress: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
item->status = ProcessStatus::Failed;
item->endTime = QDateTime::currentDateTime();
item->completedItems = currentItem;
item->currentFile = currentFile;
item->transferredBytes = bytesTransferred;
item->totalBytes = totalBytes;
updateUI();
if (!item->processWidget) {
qDebug()
<< "StatusBalloon::updateProcessProgress: no widget for processId"
<< processId;
return;
}
handleJobUpdate(item);
}
void StatusBalloon::markProcessCancelled(const QUuid &processId)
void StatusBalloon::onExportFinished(const QUuid &processId,
const ExportJobSummary &summary)
{
qDebug() << "StatusBalloon::onExportFinished entry:" << processId
<< "WasCancelled:" << summary.wasCancelled;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::onExportFinished: unknown processId"
<< processId;
return;
}
// todo: handle failed ?
ProcessItem *item = m_processes[processId];
if (summary.wasCancelled) {
item->status = ProcessStatus::Cancelled;
} else {
item->status = ProcessStatus::Completed;
}
item->endTime = QDateTime::currentDateTime();
updateHeader();
}
void StatusBalloon::onItemExported(const QUuid &processId,
const ExportResult &result)
{
qDebug() << "StatusBalloon::onItemExported entry:" << processId
<< "Success:" << result.success;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::onItemExported: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
item->status = ProcessStatus::Cancelled;
item->endTime = QDateTime::currentDateTime();
updateUI();
}
void StatusBalloon::incrementFailedItems(const QUuid &processId)
{
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
return;
if (result.success) {
item->completedItems += 1;
} else {
item->failedItems += 1;
}
m_processes[processId]->failedItems++;
updateUI();
if (item->completedItems + item->failedItems == item->totalItems) {
// meaning all items are processed, but we don't know if the overall
// status is
if (item->failedItems > 0) {
item->status = ProcessStatus::Failed;
} else {
item->status = ProcessStatus::Completed;
}
}
handleJobUpdate(item);
updateHeader();
}
void StatusBalloon::updateUI()
QUuid StatusBalloon::startExportProcess(const QString &title, int totalItems,
const QString &destinationPath)
{
QMutexLocker locker(&m_processesMutex);
qDebug() << "StatusBalloon::startExportProcess entry:" << title
<< totalItems << destinationPath;
handleShow(); // ensure balloon is visible when process starts
auto *item = new ProcessItem();
item->processId = QUuid::createUuid();
item->type = ProcessType::Export;
item->status = ProcessStatus::Running;
item->title = title;
item->totalItems = totalItems;
item->completedItems = 0;
item->failedItems = 0;
item->totalBytes = 0;
item->transferredBytes = 0;
item->startTime = QDateTime::currentDateTime();
item->destinationPath = destinationPath;
{ // scope the lock only for shared-state mutation
QMutexLocker locker(&m_processesMutex);
m_processes[item->processId] = item;
m_currentProcessId = item->processId;
} // mutex released here
// UI work must run without holding m_processesMutex to avoid re-locking
// deadlock
createProcessWidget(item);
updateHeader();
return item->processId;
}
void StatusBalloon::createProcessWidget(ProcessItem *item)
{
BalloonProcess *processWidget = new BalloonProcess(item);
item->processWidget = processWidget;
m_processesLayout->addWidget(item->processWidget);
}
void StatusBalloon::updateHeader()
{
// QMutexLocker locker(&m_processesMutex);
// Update header
int running = 0, completed = 0, failed = 0;
@@ -455,86 +362,14 @@ void StatusBalloon::updateUI()
}
}
m_headerLabel->setText(headerText);
// Update each process widget
for (auto *item : m_processes) {
if (!item->processWidget)
continue;
// Update status label
QString statusText;
if (item->status == ProcessStatus::Running) {
if (!item->currentFile.isEmpty()) {
statusText = item->currentFile;
} else {
statusText = "Processing...";
}
} else if (item->status == ProcessStatus::Completed) {
statusText = "Completed successfully";
} else if (item->status == ProcessStatus::Failed) {
statusText = "Failed";
} else if (item->status == ProcessStatus::Cancelled) {
statusText = "Cancelled";
}
item->statusLabel->setText(statusText);
// Update progress bar
if (item->totalItems > 0) {
int progress = (item->completedItems * 100) / item->totalItems;
item->progressBar->setValue(progress);
}
// Update stats
QString statsText = QString("%1 of %2 items")
.arg(item->completedItems)
.arg(item->totalItems);
if (item->failedItems > 0) {
statsText += QString(" • %1 failed").arg(item->failedItems);
}
if (item->status == ProcessStatus::Running &&
item->transferredBytes > 0) {
// Calculate transfer rate
QDateTime now = QDateTime::currentDateTime();
qint64 elapsed = m_lastUpdateTime[item->processId].msecsTo(now);
if (elapsed > 0) {
qint64 bytesDiff = item->transferredBytes -
m_lastBytesTransferred[item->processId];
qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed;
if (bytesPerSecond > 0) {
statsText += "" + formatTransferRate(bytesPerSecond);
}
m_lastBytesTransferred[item->processId] =
item->transferredBytes;
m_lastUpdateTime[item->processId] = now;
}
}
item->statsLabel->setText(statsText);
// Update buttons
if (item->status == ProcessStatus::Running) {
item->cancelButton->setVisible(true);
item->actionButton->setVisible(false);
} else {
item->cancelButton->setVisible(false);
if (item->type == ProcessType::Export &&
item->status == ProcessStatus::Completed) {
item->actionButton->setVisible(true);
}
}
}
showBalloon();
}
void StatusBalloon::showBalloon()
void StatusBalloon::handleShow(bool forceVisible)
{
qDebug() << "StatusBalloon::showBalloon" << sender();
QPoint pos = m_button->mapToGlobal(
QPoint(m_button->width() / 2, m_button->height()));
balloon(pos, -1, true);
toggleBaloon(pos, -1, forceVisible);
}
bool StatusBalloon::isProcessRunning(const QUuid &processId) const
@@ -602,31 +437,6 @@ void StatusBalloon::onOpenFolderClicked()
}
}
QString StatusBalloon::formatFileSize(qint64 bytes) const
{
const qint64 KB = 1024;
const qint64 MB = KB * 1024;
const qint64 GB = MB * 1024;
if (bytes >= GB) {
return QString("%1 GB").arg(
QString::number(bytes / double(GB), 'f', 2));
} else if (bytes >= MB) {
return QString("%1 MB").arg(
QString::number(bytes / double(MB), 'f', 1));
} else if (bytes >= KB) {
return QString("%1 KB").arg(
QString::number(bytes / double(KB), 'f', 0));
} else {
return QString("%1 B").arg(bytes);
}
}
QString StatusBalloon::formatTransferRate(qint64 bytesPerSecond) const
{
return formatFileSize(bytesPerSecond) + "/s";
}
void StatusBalloon::removeProcessWidget(const QUuid &processId)
{
QMutexLocker locker(&m_processesMutex);
@@ -649,4 +459,35 @@ void StatusBalloon::removeProcessWidget(const QUuid &processId)
}
}
ZIconWidget *StatusBalloon::getButton() { return m_button; }
void StatusBalloon::handleJobUpdate(ProcessItem *item)
{
// QMutexLocker locker(&m_processesMutex);
// Update status label
QString statusText;
if (item->status == ProcessStatus::Running) {
if (!item->currentFile.isEmpty()) {
statusText = item->currentFile;
} else {
statusText = "Processing...";
}
} else if (item->status == ProcessStatus::Completed) {
statusText = "Completed successfully";
} else if (item->status == ProcessStatus::Failed) {
statusText = "Failed";
} else if (item->status == ProcessStatus::Cancelled) {
statusText = "Cancelled";
}
item->statusLabel->setText(statusText);
// Update progress bar
// progess should be based on exported bytes vs total bytes of the current
// file
if (item->totalItems > 0) {
int progress = (item->transferredBytes * 100) / item->totalBytes;
item->processWidget->setProgress(progress);
}
item->processWidget->updateStats();
item->processWidget->updateButtons();
}
+18 -16
View File
@@ -16,6 +16,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <atomic>
class BalloonProcess;
enum class ProcessType { Export, Upload };
@@ -35,7 +36,7 @@ struct ProcessItem {
QDateTime startTime;
QDateTime endTime;
QString destinationPath; // For export
QWidget *processWidget;
BalloonProcess *processWidget;
QLabel *titleLabel;
QLabel *statusLabel;
QLabel *statsLabel;
@@ -45,11 +46,21 @@ struct ProcessItem {
std::atomic<bool> cancelRequested{false};
};
class Process : public QWidget
class BalloonProcess : public QWidget
{
Q_OBJECT
public:
explicit Process(QWidget *parent = nullptr);
explicit BalloonProcess(ProcessItem *item, QWidget *parent = nullptr);
void setProgress(int progress);
void updateStats();
void updateButtons();
void done();
private:
ProcessItem *m_item;
QDateTime m_lastUpdateTime;
qint64 m_lastBytesTransferred;
};
class StatusBalloon : public QBalloonTip
@@ -62,35 +73,28 @@ public:
// Process management
QUuid startExportProcess(const QString &title, int totalItems,
const QString &destinationPath);
QUuid startUploadProcess(const QString &title, int totalItems);
void onFileTransferProgress(const QUuid &processId, int currentItem,
const QString &currentFile,
qint64 bytesTransferred, qint64 totalBytes);
void markProcessCompleted(const QUuid &processId);
void markProcessFailed(const QUuid &processId, const QString &error);
void markProcessCancelled(const QUuid &processId);
void incrementFailedItems(const QUuid &processId);
bool isProcessRunning(const QUuid &processId) const;
bool hasActiveProcesses() const;
bool isCancelRequested(const QUuid &processId) const;
ZIconWidget *getButton();
private slots:
ZIconWidget *getButton() { return m_button; }
void onCancelClicked();
void onOpenFolderClicked();
private:
void updateUI();
void showBalloon();
void updateHeader();
void handleShow(bool forceVisible = false);
void createProcessWidget(ProcessItem *item);
QString formatFileSize(qint64 bytes) const;
QString formatTransferRate(qint64 bytesPerSecond) const;
void removeProcessWidget(const QUuid &processId);
void connectExportThreadSignals();
void onExportFinished(const QUuid &processId,
const ExportJobSummary &summary);
void onItemExported(const QUuid &processId, const ExportResult &result);
void handleJobUpdate(ProcessItem *item);
QVBoxLayout *m_mainLayout;
QLabel *m_headerLabel;
@@ -101,8 +105,6 @@ private:
QUuid m_currentProcessId;
mutable QMutex m_processesMutex;
QMap<QUuid, qint64> m_lastBytesTransferred;
QMap<QUuid, QDateTime> m_lastUpdateTime;
ZIconWidget *m_button =
new ZIconWidget(QIcon(":/resources/icons/UimProcess.png"), "Processes");
};