Skip to content

File progress_box.cpp

File List > gui > progress_box.cpp

Go to the documentation of this file

#include "progress_box.hpp"

#include <QException>
#include <QFutureWatcher>
#include <QProgressBar>
#include <QTimer>
#include <QtConcurrent>
#include <chrono>
#include <cmath>
#include <sstream>

#include "error_dialog.hpp"
#include "ui_progress_box.h"
#include "utilities/memory_tracker.hpp"
#include "utilities/progress_tracker.hpp"
#include "utilities/trace_recorder.hpp"

namespace {

class DecimalProgressBar : public QProgressBar {
 public:
  using QProgressBar::QProgressBar;
  QString text() const override {
    double pct = static_cast<double>(value()) / static_cast<double>(maximum()) * 100.0;
    if (std::abs(pct - std::round(pct)) < 0.05)
      return QString("%1%").arg(static_cast<int>(std::round(pct)));
    return QString("%1%").arg(pct, 0, 'f', 1);
  }
};

}  // namespace

void ProgressBox::abort() { done(1); }

ProgressBox::ProgressBox(QWidget* parent)
    : QDialog(parent), ui(new Ui::ProgressBox), m_start_time(std::chrono::steady_clock::now()) {
  ui->setupUi(this);
  setWindowFlag(Qt::WindowCloseButtonHint, false);

  // ETA label is defined in the .ui file
  m_eta_label = ui->etaLabel;
  m_eta_label->setStyleSheet("QLabel { color: #888; font-size: 11px; }");

  // Timer to refresh elapsed every second even when no progress events fire
  auto* elapsed_timer = new QTimer(this);
  connect(elapsed_timer, &QTimer::timeout, this, &ProgressBox::update_elapsed);
  elapsed_timer->start(1000);

  connect(this, &ProgressBox::send_progress_bars, this, &ProgressBox::receive_progress_bars);
  connect(this, &ProgressBox::send_status_text, this, &ProgressBox::receive_status_text);
}

static std::string format_duration(double seconds) {
  int total_secs = static_cast<int>(std::round(seconds));
  if (total_secs < 60) {
    return std::to_string(total_secs) + "s";
  }
  int mins = total_secs / 60;
  int secs = total_secs % 60;
  if (mins < 60) {
    return std::to_string(mins) + "m " + std::to_string(secs) + "s";
  }
  int hrs = mins / 60;
  mins = mins % 60;
  return std::to_string(hrs) + "h " + std::to_string(mins) + "m " + std::to_string(secs) + "s";
}

void ProgressBox::receive_progress_bars(std::vector<std::pair<double, bool>> bars) {
  for (size_t i = 0; i < bars.size(); i++) {
    if (i >= m_progress.size()) {
      auto* bar = new DecimalProgressBar(ui->barsContainer);
      auto* lbl = new QLabel(ui->barsContainer);
      bar->setMaximum(10000);
      lbl->setWordWrap(true);
      lbl->hide();
      m_progress.push_back({bar, lbl});
      ui->verticalLayout_3->addWidget(bar);
      ui->verticalLayout_3->addWidget(lbl);
    }
    bool vis = bars[i].second;
    m_progress[i].bar->setVisible(vis);
    m_progress[i].bar->setValue(static_cast<int>(bars[i].first * 10000.0));
  }
  for (size_t i = bars.size(); i < m_progress.size(); i++) {
    ui->verticalLayout_3->removeWidget(m_progress[i].bar);
    ui->verticalLayout_3->removeWidget(m_progress[i].label);
    delete m_progress[i].bar;
    delete m_progress[i].label;
  }
  m_progress.resize(bars.size());

  if (!bars.empty()) m_last_overall = bars[0].first;
  update_elapsed();
}

void ProgressBox::receive_status_text(std::string text, int depth) {
  int idx = depth - 1;
  if (idx >= 0 && static_cast<size_t>(idx) < m_progress.size()) {
    if (text.empty()) {
      m_progress[idx].label->hide();
    } else {
      m_progress[idx].label->setText(QString::fromStdString(text));
      m_progress[idx].label->show();
    }
  }
}

void ProgressBox::update_elapsed() {
  auto now = std::chrono::steady_clock::now();
  double elapsed = std::chrono::duration<double>(now - m_start_time).count();
  const std::string mem = blaze::memory_tracker::format_summary();

  if (m_last_overall > 0.999) {
    std::ostringstream ss;
    ss << "Total time: " << format_duration(elapsed) << "  |  " << mem;
    m_eta_label->setText(QString::fromStdString(ss.str()));
    m_eta_label->setStyleSheet("QLabel { color: #4a4; font-size: 12px; font-weight: bold; }");
  } else if (m_last_overall > 0.001) {
    double eta = elapsed * (1.0 - m_last_overall) / m_last_overall;
    std::ostringstream ss;
    ss << "Elapsed: " << format_duration(elapsed) << "  |  ETA: " << format_duration(eta)
       << " remaining"
       << "  |  " << mem;
    m_eta_label->setText(QString::fromStdString(ss.str()));
  } else if (elapsed > 1.0) {
    std::ostringstream ss;
    ss << "Elapsed: " << format_duration(elapsed) << "  |  Estimating…"
       << "  |  " << mem;
    m_eta_label->setText(QString::fromStdString(ss.str()));
  }
}

void ProgressBox::update_progress(double _) {
  (void)_;
  std::vector<std::pair<double, bool>> bars;
  ProgressTracker* child = this->child();
  while (child != nullptr) {
    bars.emplace_back(child->proportion(), child->is_visible());
    child = child->child();
  }
  emit send_progress_bars(std::move(bars));
}

void ProgressBox::text_update(const std::string& text, int depth) {
  emit send_status_text(text, depth);
}

class TaskException : public QException {
  std::string m_what;

 public:
  TaskException(const char* what) : QException(), m_what(what) {}
  void raise() const { throw *this; }
  QException* clone() const { return new TaskException(*this); }
  inline virtual const char* what() const noexcept { return m_what.c_str(); }
};

void ProgressBox::start_task(std::function<void()> task, std::function<void()> on_finish,
                             std::function<void(const QString&)> on_error) {
  m_start_time = std::chrono::steady_clock::now();
  QFutureWatcher<int>* watcher = new QFutureWatcher<int>(this);
  connect(watcher, &QFutureWatcher<int>::finished, this, [this, watcher, on_finish, on_error] {
    if (watcher->future().isCanceled()) {
      QString message = "Unknown error";
      try {
        watcher->future().result();
      } catch (TaskException& e) {
        message = e.what();
      }
      this->done(1);
      if (on_error) {
        on_error(message);
      } else {
        show_error_message(this, "Error running task", message);
      }
    } else {
      this->done(0);
      on_finish();
    }
  });
  watcher->setFuture(QtConcurrent::run([task] {
    try {
      task();
    } catch (std::exception& e) {
      throw TaskException(e.what());
    }
    return 0;
  }));
}