mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
feat(balloon): refactor balloon handling and introduce BalloonProcess class
This commit is contained in:
+32
-4
@@ -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
@@ -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
@@ -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
@@ -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 ¤tFile,
|
||||
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 ¤tFile,
|
||||
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
@@ -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 ¤tFile,
|
||||
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");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user