/** Copyright (c) 2020-2022 The Creators of Simphone

    class Transfers (QDialog): display file transfers
    class TransferItem (QTableWidgetItem): table cell
    class Transfer: store data about a file transfer
    class TransferProgress: state of a current transfer
    class SimError: simcore error code and error message

    See the file COPYING.LESSER.txt for copying permission.
**/

#include "transfers.h"
#include "ui_transfers.h"

#include "qtfix.h"
#include "contacts.h"

#include <QDesktopWidget>
#include <QMenu>
#include <QDirIterator>
#include <QDateTime>
#include <QSizeGrip>
#include <QScrollBar>
#include <QKeyEvent>
#include <QFileDialog>

#include <time.h>
#include <math.h>

#ifdef _WIN32 // GetOpenFileName
#include <shlobj.h>
#endif

#define NDX_BASE 0x2000000000000000LL // initial value of m_ndx
#define NDX_FILE 0x4000000000000000LL // initial value of m_ndx for files
#define NDX_MASK 0x8000000000000000LL // m_ndx base for received

enum {
  xfercol_nick,
  xfercol_time,
  xfercol_size,
  xfercol_rcvd,
  xfercol_sent,
  xfercol_name,
  xfercol_nCols
};

enum {
  xfer_current,
  xfer_waiting,
  xfer_refused,
  xfer_failed,
  xfer_failing,
  xfer_erasing,
  xfer_finished
};

class TransferProgress
{
public:
  TransferProgress()
    : m_xferId(-1), m_bytes(0), m_precision(0), m_update(false) {}

  void preparePaths(simnumber simId)
  {
    simtype info = sim_xfer_get_(simId, SIM_XFER_GET_INFO, SIM_XFER_BIT_EXTENDED);
    m_names[false] = sim_table_get_pointer(info, SIM_XFER_GET_INFO_RECEIVED);
    m_names[true] = sim_table_get_pointer(info, SIM_XFER_GET_INFO_SENT);
    sim_xfer_free_(info);
  }

  QString getPath(bool type, const QString & name = QString()) const
  {
    QString qs = m_names[type];
    if (!name.isEmpty() && !qs.isEmpty()) qs.append(QDir::separator()).append(name);
    return qs;
  }

  QString getProgress(simnumber xferId, simnumber bytes, simnumber maxBytes)
  {
    QString qs;
    double percent = maxBytes > 0 ? double(bytes) / double(maxBytes) * 100 : 100;
    double oldPercent = maxBytes > 0 ? double(m_bytes) / double(maxBytes) * 100 : 100;
    int precision = m_precision - 1;
    if (m_xferId == xferId) {
      do {
        qs = QString::number(percent, 'f', ++precision);
        if (!m_update) break;
      } while (m_bytes != bytes && precision < 16 && QString::number(oldPercent, 'f', precision) == qs);
      m_update = false;
    } else {
      qs = QString::number(floor(percent), 'f', precision = 0);
    }
    if (xferId) m_xferId = xferId, m_bytes = bytes, m_precision = precision;
    return qs.append('%');
  }

  void updateProgress() { m_update = true; }

private:
  QString m_names[2];
  simnumber m_xferId;
  simnumber m_bytes;
  int m_precision;
  bool m_update;
};

class Transfer
{
public:
  QString m_fileName;
  Contact * m_contact;
  simnumber m_ndx;
  simnumber m_xferId;
  simnumber m_fileSize;
  simnumber m_rcvdSize;
  simnumber m_sendTime;
  short m_simres;
  char m_type[5];
  bool m_perContact : 1;
  bool m_perTransfer : 1;
  bool m_shared : 1;

  Transfer(char dialog, const char * type, Contact * contact = 0, simnumber xferId = 0, simnumber ndx = 0)
    : m_contact(contact), m_xferId(xferId), m_perContact(dialog != 0), m_perTransfer(dialog > 1), m_shared(false)
  {
    memcpy(m_type, type, sizeof(m_type) - 1);
    m_type[sizeof(m_type) - 1] = 0;
    m_ndx = ndx;
    if (strcmp(type, SIM_XFER_TYPE_INIT) && strcmp(type, SIM_XFER_TYPE_WAIT) && strcmp(type, SIM_XFER_TYPE_HOLD)) {
      if (strcmp(type, "sent") && strcmp(type, SIM_XFER_TYPE_SEND) && strcmp(type, SIM_XFER_TYPE_DONE)) {
        m_ndx |= NDX_MASK;
      }
    }

    if (Transfers * parent = getDialog()) parent->m_count++;
  }

  ~Transfer()
  {
    if (Transfers * parent = getDialog()) parent->m_count--;
  }

  Transfers * getDialog() const
  {
    Contact * contact = m_perContact ? m_contact : SimCore::getContact(SimCore::get()->getTestContactId());
    return contact ? (Transfers *)contact->getDialog(m_perTransfer + 1) : 0;
  }

  QString getName() const;
  int setName(const char * name, unsigned * length, bool paths);
};

QString Transfer::getName() const
{
  QString qs = m_fileName;
  if (qs.endsWith('\n')) qs.chop(1);
  return qs.isEmpty() ? qs : qs.split('\n').last();
}

int Transfer::setName(const char * name, unsigned * length, bool paths)
{
  const char * s = 0;
  int len = --*length;
  int pos = 0;
  int last = paths ? len : -1;

  if (m_ndx >= 0 && (s = strrchr(name, QDir::separator().cell())) != 0) {
    *length -= (last = s - name) + 1;
    if (!paths) {
      name += last + 1;
      pos = 0;
      len = *length;
      last = -1;
    } else if (int(*length) < last) {
      *length = last;
    } else {
      pos = last + 1;
    }
  } else {
    len = *length + paths;
  }

  m_fileName = QString::fromUtf8(name, len);
  if (last >= 0) {
    m_fileName.replace(last, 1, "\n");
    if (!s && m_ndx >= 0 && paths) m_fileName.remove(last, 1).insert(0, '\n');
  }
  return pos;
}

class TransferItem : public QTableWidgetItem
{
public:
  TransferItem() {}
  QVariant data(int role) const Q_DECL_OVERRIDE;
};

QVariant TransferItem::data(int role) const
{
  char buffer[64];
  QString qs;
  Transfers * dialog = (Transfers *)tableWidget()->parentWidget()->parentWidget();
  Transfer * transfer = dialog->m_transfers[row()];

  switch (role) {
    case Qt::TextAlignmentRole: {
      int col = column();
      int align = col == xfercol_size || col == xfercol_rcvd ? Qt::AlignHCenter : Qt::AlignLeft;
      return align | (dialog->hasViewSentPaths() ? Qt::AlignTop : Qt::AlignVCenter);
    }

    case Qt::TextColorRole:
      return dialog->m_colors[Transfers::getTransferState(transfer->m_type, transfer->m_simres)];

    case Qt::ToolTipRole:
      if (transfer->m_ndx & NDX_FILE || dialog->m_update) {
        return "<p style='white-space:pre'>" + dialog->getToolTip(transfer);
      }
      transfer = dialog->newTransfer(transfer->m_contact, transfer->m_xferId);
      if (!transfer) break;
      qs = "<p style='white-space:pre'>" + dialog->getToolTip(transfer);
      delete transfer;
      return qs;

    case Qt::DisplayRole:
      switch (column()) {
        case xfercol_nick:
          return transfer->m_contact->m_nick;

        case xfercol_time:
          return dialog->getCurrent(transfer, 't');

        case xfercol_size:
          return transfer->m_fileSize >= 0 ? dialog->convertFileSize(transfer->m_fileSize) : QString();

        case xfercol_rcvd:
          return dialog->getCurrent(transfer, 'r');

        case xfercol_name:
          return transfer->m_fileName;

        case xfercol_sent:
          if (transfer->m_sendTime > 0) {
            sim_convert_time_to_string(transfer->m_sendTime / 1000000000, buffer);
            buffer[SIM_SIZE_TIME - 4] = buffer[SIM_SIZE_TIME - 3] = buffer[SIM_SIZE_TIME - 2] = ' ';
            return buffer;
          }
      }
  }

  return QVariant();
}

Transfers::Transfers(QWidget * parent, Contact * contact, int msgNdx)
  : QDialog(parent)
  , ui(new Ui::Transfers)
  , m_contact(contact)
  , m_minIndex(NDX_BASE)
  , m_maxIndex(NDX_BASE)
  , m_fileIndex(-1)
  , m_createTime(0)
  , m_updateTime(0)
  , m_lastTime(0)
  , m_addWidth(0)
  , m_frameRow(-1)
  , m_lastRow(-1)
  , m_count(0)
  , m_update(false)
  , m_dialog(contact->isTest() ? 0 : msgNdx ? 2 : 1)
{
  static const char * colors[] = {
    "ui.color.note", "ui.color.info", "ui.color.warn", "ui.color.warn", "ui.color.error", "ui.color.error"
  };

  log_info_("ui", "create transfers %s #%d\n", contact->m_nick.toUtf8().data(), msgNdx);
  for (unsigned i = 0; i < SIM_ARRAY_SIZE(colors); ++i) m_colors[i] = SimParam::getColor(colors[i]);
  QByteArray sortOrder(SimParam::getString(m_dialog ? "ui.xfer.orders" : "ui.xfer.order"));
  strncpy(m_sortOrder, sortOrder.data(), sizeof(m_sortOrder));
  m_sortOrder[sizeof(m_sortOrder) - 1] = 0;

  ui->setupUi(this);
  setFont(qtfix::fixFontSize(font(), "QAbstractItemView"));
  setAttribute(Qt::WA_DeleteOnClose);
  setWindowFlags(Qt::WindowMinimizeButtonHint | Qt::WindowSystemMenuHint |
                 Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint);
  ui->fileTable->setColumnCount(xfercol_nCols);

  QMenu * menu = new QMenu(this);
  const char * param = m_dialog ? "ui.xfer.files" : "ui.xfer.file";
  m_viewFinished = qtfix::addMenuItem(menu, tr("Finished Files"), m_dialog >= 2 ? true : SimParam::get(param) != 0);
  if (m_dialog >= 2) m_viewFinished->setEnabled(false);
  menu->addSeparator();

  param = m_dialog ? "ui.xfer.sents" : "ui.xfer.sent";
  m_viewSent = qtfix::addMenuItem(menu, tr("Sent Files"), m_dialog >= 2 ? true : SimParam::get(param) != 0);
  if (m_dialog >= 2) m_viewSent->setEnabled(false);
  param = m_dialog ? "ui.xfer.paths" : "ui.xfer.path";
  m_viewPaths = qtfix::addMenuItem(menu, tr("Full Names"), SimParam::get(param) != 0);

  int units = SimParam::get(m_dialog ? "ui.xfer.units" : "ui.xfer.unit");
  menu->addSeparator();
  m_viewTime[0] = qtfix::addMenuItem(menu, tr("One Time Unit"), units & 1);
  m_viewTime[1] = qtfix::addMenuItem(menu, tr("Two Time Units"), units >> 1);
  ui->viewButton->setMenu(menu);
  qtfix::fixIconSize(ui->viewButton, ui->viewButton->font());

  QByteArray state(SimParam::getString(m_dialog ? "ui.xfer.headers" : "ui.xfer.header"));
  QHeaderView * header = ui->fileTable->horizontalHeader();
  if (!state.isEmpty()) header->restoreState(state);
  initLabelsToolTips();
  ui->fileTable->horizontalHeaderItem(xfercol_nick)->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
  ui->fileTable->horizontalHeaderItem(xfercol_name)->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
  header->setContextMenuPolicy(Qt::CustomContextMenu);
  header->setSectionResizeMode(QHeaderView::Interactive);
  header->setSectionsMovable(true);
  header->setSectionsClickable(true);
  header->setSortIndicatorShown(true);
  header->setResizeContentsPrecision(-1);
  header->setDefaultSectionSize(header->minimumSectionSize());
  if (m_dialog) header->hideSection(xfercol_nick);

  memset(m_colWidth, 0, sizeof(m_colWidth));
  setTimeUnits(units);
  m_precision = SimParam::get(m_dialog ? "ui.xfer.digits" : "ui.xfer.digit");
  for (unsigned i = 0; i < 60; i += 10) setColWidth(&m_colWidth[xfercol_size], convertFileSize(qlonglong(511) << i));
  m_colWidth[xfercol_rcvd] = ui->fileTable->fontMetrics().width("0.0000%");
  char buffer[64];
  sim_convert_time_to_string(time(0), buffer);
  buffer[SIM_SIZE_TIME - 4] = buffer[SIM_SIZE_TIME - 3] = buffer[SIM_SIZE_TIME - 2] = ' ';
  m_colWidth[xfercol_sent] = ui->fileTable->fontMetrics().width(buffer);
  m_addWidth = ui->fileTable->fontMetrics().width(' ') * 3;
  for (int i = 0; i < xfercol_nCols; ++i) m_colWidth[i] = qMax(m_colWidth[i] + m_addWidth, header->sectionSizeHint(i));

  int posX;
  int posY;
  QDialog * window = contact->getDialog(m_dialog < 2 ? 1 : 2, &posX, &posY);
  onSignalSettingsChanged();
  contact->setDialog(m_dialog < 2 ? 1 : 2, this);
  setFilter(msgNdx);
  onViewMenuTriggered(m_viewPaths);

  QRect rect = QApplication::desktop()->availableGeometry();
  int hs = header->sizeHint().height() + 8;
  int vs = hs * 2 + ui->viewButton->sizeHint().height();
  int extra = vs;
  for (int i = 0; i < xfercol_nCols; ++i) hs += header->sectionSize(i) * !header->isSectionHidden(i);
  for (int i = 0; i < ui->fileTable->rowCount(); ++i) vs += ui->fileTable->verticalHeader()->sectionSize(i);
  resize(qMin(hs, rect.right() - rect.left()), qMin(vs, qMax(rect.bottom() - rect.top() - extra, extra)));
  if (posX != -1 || posY != -1) qtfix::setGeometry(this, QRect(posX, posY, width(), height()), window != 0);

  connect(Contacts::get(), SIGNAL(signalLogout()), this, SLOT(onSignalLogout()));
  connect(Contacts::get(), SIGNAL(signalSettingsChanged()), this, SLOT(onSignalSettingsChanged()));
  connect(SimCore::get(), SIGNAL(signalContactAdded(int)), this, SLOT(onSignalContactAdded(int)));
  connect(SimCore::get(), SIGNAL(signalContactChanged(unsigned)), this, SLOT(onSignalContactChanged(unsigned)));
  connect(SimCore::get(), SIGNAL(signalTransferState(unsigned, qlonglong, const char *)),
          this, SLOT(onSignalTransferState(unsigned, qlonglong, const char *)));
  connect(SimCore::get(), SIGNAL(signalTransfer(unsigned, qlonglong, const QString &, const QString &)),
          this, SLOT(onSignalTransfer(unsigned, qlonglong, const QString &, const QString &)));
  connect(menu, SIGNAL(triggered(QAction *)), this, SLOT(onViewMenuTriggered(QAction *)));
  connect(header, SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
          this, SLOT(onSortIndicatorChanged(int, Qt::SortOrder)));
  connect(header, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenuHeader(const QPoint &)));
  connect(ui->fileTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(const QPoint &)));
  connect(ui->fileTable, SIGNAL(clicked(const QModelIndex &)), this, SLOT(onRowClicked(const QModelIndex &)));
  connect(ui->fileTable, SIGNAL(doubleClicked(const QModelIndex &)),
          this, SLOT(onRowDoubleClicked(const QModelIndex &)));
  connect(&m_timer, SIGNAL(timeout()), this, SLOT(onTimerTimeout()));
  m_timer.start(1000);

  onSignalContactChanged(contact->m_id);
  installEventFilter(this);
}

Transfers::~Transfers()
{
  removeEventFilter(this);

  if (m_contact) {
    QHash<QString, Transfer *>::Iterator hit;
    for (hit = m_files.begin(); m_files.end() != hit; hit++) derefTransfer(hit.value());
    for (unsigned i = 0; i < m_transfers.size(); ++i) derefTransfer(m_transfers[i]);

    log_info_("ui", "delete %d transfers %s\n", m_count, m_contact->m_nick.toUtf8().data());

    Q_ASSERT(!m_count);
    m_contact->setDialog(m_dialog < 2 ? 1 : 2, 0, geometry().x(), geometry().y());
    sim_xfer_set_(m_dialog ? m_contact->m_simId : 0, 0, SIM_XFER_TYPE_FREE);
  }

  for (QHash<Contact *, TransferProgress *>::Iterator it = m_current.begin(); m_current.end() != it; it++) delete *it;
  delete ui;
}

bool Transfers::eventFilter(QObject * obj, QEvent * event)
{
  if (event->type() == QEvent::KeyPress) {
    switch (static_cast<QKeyEvent *>(event)->key()) {
      case Qt::Key_Home:
        ui->fileTable->verticalScrollBar()->setValue(ui->fileTable->verticalScrollBar()->minimum());
        return true;

      case Qt::Key_End:
        ui->fileTable->verticalScrollBar()->setValue(ui->fileTable->verticalScrollBar()->maximum());
        return true;

      case Qt::Key_PageUp:
        ui->fileTable->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
        return true;

      case Qt::Key_PageDown:
        ui->fileTable->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
        return true;

      case Qt::Key_F5:
        QTimer::singleShot(0, this, SLOT(on_buttonBox_clicked()));
        return true;

      case Qt::Key_ScrollLock:
        if (m_frameRow >= 0) {
          int index = 0;
          int column;
          do {
            column = ui->fileTable->horizontalHeader()->logicalIndex(index++);
          } while (ui->fileTable->isColumnHidden(column));
          ui->fileTable->scrollToItem(ui->fileTable->item(m_frameRow, column), QAbstractItemView::PositionAtCenter);
        }
        return true;
    }
  }
  return Parent::eventFilter(obj, event);
}

void Transfers::changeEvent(QEvent * event)
{
  Parent::changeEvent(event);
  if (event->type() == QEvent::LanguageChange) {
    ui->retranslateUi(this);
    onSignalContactChanged(m_contact->m_id);
    QTimer::singleShot(0, this, SLOT(initLabelsToolTips()));
  }
}

void Transfers::initLabelsToolTips()
{
  QStringList labels;
  labels << tr("Nick") << tr("Time") << tr("Size") << tr("Received") << tr("Sent") << tr("File Name");
  ui->fileTable->setHorizontalHeaderLabels(labels);

  int fontH = ui->fileTable->fontMetrics().ascent();
  m_pauseButton = ui->buttonBox->button(QDialogButtonBox::RestoreDefaults);
  m_pauseButton->setIconSize(QSize(fontH, fontH));
  m_pauseButton->setText(tr("Pause"));
  m_pauseButton->setToolTip(tr("Push this button to pause file transfers."));
  m_pauseButton->setIcon(qtfix::fixPixmapSize(QPixmap(":/filePause"), fontH, false));
  m_startButton = ui->buttonBox->button(QDialogButtonBox::Reset);
  m_startButton->setIconSize(QSize(fontH, fontH));
  m_startButton->setText(tr("Resume"));
  m_startButton->setToolTip(tr("Push this button to resume file transfers."));
  m_startButton->setIcon(qtfix::fixPixmapSize(QPixmap(":/fileStart"), fontH, false));
  m_updateButton = ui->buttonBox->button(QDialogButtonBox::Discard);
  m_updateButton->setIconSize(QSize(fontH, fontH));
  m_updateButton->setText(tr("Update"));
  m_updateButton->setToolTip(tr("Press F5 or push this button to refresh the list."));
  m_updateButton->setIcon(qtfix::fixPixmapSize(QPixmap(":/fileRefresh"), fontH, false));
  ui->horizontalSpacer->changeSize(fontH, 0, QSizePolicy::Preferred);
  QString qs = tr("Push the button to apply current view settings to this dialog permanently.");
  if (m_dialog < 2) ui->buttonBox->button(QDialogButtonBox::Ok)->setToolTip(qs);
  ui->horizontalSpacer_2->changeSize(QSizeGrip(this).sizeHint().width(), 0);

  for (int i = 0; i < xfercol_nCols; ++i) {
    static const char * tooltips[] = {
      QT_TR_NOOP("Click here to sort rows by nickname."), QT_TR_NOOP("Click here to sort rows by color."),
      QT_TR_NOOP("Click here to sort rows by file size."), QT_TR_NOOP("Click here to sort rows by received size."),
      QT_TR_NOOP("Click here to sort rows by send time."), QT_TR_NOOP("Click here to sort rows by file name.")
    };
    qs = tr(tooltips[i]).append('\n').append(tr("Right-click to reset order to default."));
    ui->fileTable->horizontalHeaderItem(i)->setToolTip(qs);
    qs = tr("Sent files are below this line.\nReceived files are above this line.");
    if (m_frameRow >= 0) ui->fileTable->cellWidget(m_frameRow, i)->setToolTip(qs);
  }
}

void Transfers::onSignalLogout()
{
  m_contact = 0;

  m_timer.stop();
  accept();
}

void Transfers::onSignalSettingsChanged()
{
  if (!m_contact) return;
  QString download = m_current[m_contact] ? m_current[m_contact]->getPath(false) : QString();
  QString upload = m_current[m_contact] ? m_current[m_contact]->getPath(true) : QString();

  if (m_dialog) {
    if (!m_current[m_contact]) m_current[m_contact] = new TransferProgress;
    m_current[m_contact]->preparePaths(m_contact->m_simId);
  } else {
    onSignalContactAdded(0);
  }

  if (!m_timer.isActive() || !m_viewFinished->isChecked()) return;
  if (!m_viewSent->isChecked() || m_current[m_contact]->getPath(false) == download) {
    if (m_current[m_contact]->getPath(true) == upload) return;
  }
  onTimerTimeout('v');
}

void Transfers::onSignalContactAdded(int)
{
  if (!m_dialog) {
    std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
    for (unsigned i = 0; i < contacts.size(); ++i) {
      if (!m_current[contacts[i]]) m_current[contacts[i]] = new TransferProgress;
      m_current[contacts[i]]->preparePaths(contacts[i]->isTest() ? 0 : contacts[i]->m_simId);
    }
  }
}

void Transfers::onSignalContactChanged(unsigned id)
{
  if (!m_contact) return;
  if (m_dialog) {
    if (m_contact->m_id == int(id)) {
      setWindowTitle((m_dialog < 2 ? tr("Transfers - %1") : tr("Transfer - %1")).arg(m_contact->m_nick));
    }
  } else if (m_contact->m_id == int(id)) {
    setWindowTitle(tr("Transfers"));
  } else {
    QRect r = contentsRect();
    int first = ui->fileTable->verticalHeader()->visualIndexAt(r.top());
    int last = ui->fileTable->verticalHeader()->visualIndexAt(r.bottom() - 2);
    if (last < 0 || last >= ui->fileTable->rowCount()) last = ui->fileTable->rowCount() - 1;
    for (int i = 0; i < int(m_transfers.size()); ++i) {
      if (m_transfers[i]->m_contact && m_transfers[i]->m_contact->m_id == int(id)) {
        if (i >= first && i <= last + 1) ui->fileTable->setItem(i, xfercol_nick, new TransferItem);
      }
    }
  }
}

void Transfers::onSignalTransferState(unsigned id, qlonglong xferId, const char * type)
{
  if (!m_contact || (unsigned(m_contact->m_id) != id && m_dialog)) return;
  Contact * contact = SimCore::getContact(id);
  if (!contact || (!m_dialog && !Contacts::get()->isContactShown(contact))) return;

  if (m_frameRow < 0 && type) {
    Transfer transfer(m_dialog, type);
    if (transfer.m_ndx >= 0) return;
  }

  bool prepend = ((xferId >> 62) + 1) >> 1;
  if (prepend) xferId ^= qlonglong(1) << 62;
  if (m_dialog >= 2 && !m_filters.contains(xferId < 0 ? -xferId : xferId)) return;
  if (setTransfer(contact, 0, type, xferId, prepend) && m_dialog >= 2 && type) onTimerTimeout('s');
}

void Transfers::onSignalTransfer(unsigned id, qlonglong xferId, const QString & oldName, const QString & newName)
{
  bool type = xferId == SIM_XFER_GET_SENT;
  if (!m_viewFinished->isChecked() || (type && m_frameRow < 0)) return;
  if (!m_contact || (unsigned(m_contact->m_id) != id && m_dialog)) return;
  Contact * contact = SimCore::getContact(id);
  if (!contact || (!m_dialog && !Contacts::get()->isContactShown(contact))) return;

  if ((!type && xferId != SIM_XFER_GET_RECEIVED) || m_dialog >= 2) { // from handleSIMeventHISTORY
    if (m_dialog >= 2 && m_filters.contains(xferId)) {               // update filter per name with the history message
      for (QHash<QString, Transfer *>::Iterator hit = m_files.begin(); m_files.end() != hit; hit++) {
        if (hit.value()->m_xferId == xferId) {
          if (Transfer * transfer = newFilter(hit.value()->m_ndx >= 0, xferId, newName, oldName.toLongLong())) {
            derefTransfer(hit.value());
            m_files.erase(hit);
            m_files.insertMulti(transfer->m_fileName, transfer);
          }
          break;
        }
      }
    }
    return;
  }

  Transfer * oldTransfer = 0;
  qlonglong fileNdx = ++m_fileIndex;

  if (!oldName.isEmpty()) { // file rename or delete: look up the old file name
    QHash<QString, Transfer *>::Iterator hit = m_files.find(oldName);

    while (m_files.end() != hit && hit.value()->m_contact != contact) {
      if (m_files.end() == ++hit || hit.key() != oldName || (hit.value()->m_ndx >= 0) != type) {
        hit = m_files.end();
        break;
      }
    }

    if (m_files.end() != hit) { // old file name existed: delete it
      oldTransfer = hit.value();
      fileNdx = oldTransfer->m_ndx & ~NDX_FILE;
      m_files.erase(hit);
      if (newName.isEmpty()) setTransfer(contact, oldTransfer, "", 0, false);
    }
  }

  if (!newName.isEmpty()) { // file rename or create: add new unless duplicate
    Transfer * newTransfer = 0;
    if (type || !isTransferFound(contact, newName, oldName == newName)) {
      QString qs = m_current[contact]->getPath(type, newName);
      QFileInfo info(qs);
      if (!qs.isEmpty()) newTransfer = newFile(contact, type, fileNdx, info);
      if (newTransfer) m_files.insertMulti(info.fileName(), newTransfer);
    }
    if (newTransfer || oldTransfer) {
      setTransfer(contact, newTransfer ? newTransfer : oldTransfer, newTransfer ? " " : "", 0, false);
    }
  }
  if (oldTransfer) derefTransfer(oldTransfer);
}

void Transfers::onViewMenuTriggered(QAction * action)
{
  if (action == m_viewSent || action == m_viewPaths) {
    int margin = ui->fileTable->style()->pixelMetric(QStyle::PM_FocusFrameVMargin, 0, this);
    int minSize = ui->fileTable->verticalHeader()->minimumSectionSize();
    int height = ui->fileTable->fontMetrics().height() * (hasViewSentPaths() + 1) + margin;
    QHeaderView::ResizeMode resizeMode = m_viewPaths->isChecked() ? QHeaderView::Interactive : QHeaderView::Stretch;
    ui->fileTable->horizontalHeader()->setSectionResizeMode(xfercol_name, resizeMode);
    ui->fileTable->verticalHeader()->setDefaultSectionSize(qMax(height, minSize));
    onTimerTimeout('v');
  } else if (action == m_viewFinished) {
    onTimerTimeout('v');
  } else if (action == m_viewTime[0] || action == m_viewTime[1]) {
    setTimeUnits(getTimeUnits());
    if (ui->fileTable->columnWidth(xfercol_time) < m_colWidth[xfercol_time]) {
      ui->fileTable->setColumnWidth(xfercol_time, m_colWidth[xfercol_time]);
    }
    onTimerTimeout('s');
  }
}

void Transfers::onSortIndicatorChanged(int index, Qt::SortOrder order)
{
  char * s = strchr(m_sortOrder, index + 'a');
  if (!s) s = strchr(m_sortOrder, index + 'A');
  if (s) memmove(s, s + 1, strlen(s) + 1);
  memmove(m_sortOrder + 1, m_sortOrder, strlen(m_sortOrder));
  m_sortOrder[sizeof(m_sortOrder) - 1] = 0;
  *m_sortOrder = char(index + (order == Qt::AscendingOrder ? 'a' : 'A'));
  onTimerTimeout('s');
}

void Transfers::onCustomContextMenuHeader(const QPoint & pos)
{
  int column = ui->fileTable->horizontalHeader()->logicalIndexAt(pos);
  QMenu menu(this);
  QAction * reset = m_dialog < 2 ? menu.addAction(tr("Reset")) : 0;
  if (column >= 0 && (column != xfercol_name || m_viewPaths->isChecked())) menu.addAction(tr("Resize"));

  if (QAction * action = menu.exec(ui->fileTable->horizontalHeader()->viewport()->mapToGlobal(pos))) {
    if (action == reset) {
      saveSettings(true);
    } else {
      ui->fileTable->resizeColumnToContents(column);
    }
  }
}

void Transfers::onCustomContextMenu(const QPoint & pos)
{
  QModelIndex cell = ui->fileTable->indexAt(pos);
  if (!cell.isValid()) return;

  int row = cell.row();
  QTableWidgetItem * item = ui->fileTable->item(row, cell.column());
  m_lastRow = item && item->isSelected() && getSelectedRows(ui->fileTable->selectionModel()) == 1 ? row : -1;
  if (unsigned(row) >= m_transfers.size()) return;

  Transfer * transfer = m_transfers[row];
  Contact * contact = transfer->m_contact;
  simnumber xferId = transfer->m_xferId;
  if (!contact) return;
  QItemSelectionModel * select = ui->fileTable->selectionModel();

  // create menu with items depending on clicked row and exit if no items can be added or if no action clicked
  QMenu menu(this);
  QAction * cancel = 0;
  int color = getTransferState(transfer->m_type, transfer->m_simres);
  if (color >= xfer_failed && (color != xfer_finished || transfer->m_ndx <= 0)) {
    if (m_dialog < 2 || !transfer->m_ndx) return;
  } else {
    if (color != xfer_current && color != xfer_finished) menu.addAction(tr("&Start"));
    cancel = color != xfer_finished ? menu.addAction(tr("Cancel")) : m_dialog < 2 ? menu.addAction(tr("Remove")) : 0;
  }

  QAction * hide = m_dialog >= 2 && m_filters.count() > 1 ? menu.addAction(tr("Hide")) : 0;
  QAction * action = menu.exec(ui->fileTable->viewport()->mapToGlobal(pos));
  if (!action) return;

  // check unclear click
  QModelIndexList selected;
  bool ok = row < int(m_transfers.size());
  ok = ok && m_transfers[row]->m_xferId == xferId && m_transfers[row]->m_contact == contact;
  if ((action == cancel || action == hide) && select->hasSelection()) {
    selected = select->selectedRows();
    ok = selected.count() != 1 || selected[0].row() == row;
    row = -1;
  }
  if (!ok) {
    QString qs = tr("You have selected a file different from the one you clicked.").append("\n\n");
    if (action == hide) {
      qs.append(tr("Please, click the file you want to hide and then try again."));
      QMessageBox::warning(this, tr("Hide file transfer"), qs);
    } else if (action != cancel) {
      qs.append(tr("Please, click the file you want to start transferring and then try again."));
      QMessageBox::warning(this, tr("Start file transfer"), qs);
    } else if (color != xfer_finished) {
      qs.append(tr("Please, click the file you want to cancel and then try again."));
      QMessageBox::warning(this, tr("Cancel file transfer"), qs);
    } else {
      qs.append(tr("Please, click the file you want to remove and then try again."));
      QMessageBox::warning(this, tr("Remove sent file"), qs);
    }
    return;
  }

  if (action == cancel && color != xfer_finished) { // cancelling simcore transfers: ask for confirmation
    QString qs;
    QString text;
    if (selected.count() > 1) {
      qlonglong sizes[2] = { 0, 0 };
      int files[2] = { 0, 0 };
      bool wipe = SimParam::get("xfer.wipe") != 0;
      for (int j = 0; j < selected.count(); ++j) {
        int i = selected[j].row();
        transfer = m_transfers[i];
        if (transfer->m_xferId && getTransferState(transfer->m_type, transfer->m_simres) < xfer_failing) {
          row = i;
          files[transfer->m_ndx >= 0]++;
          if (transfer->m_rcvdSize > 0) {
            bool wiped = wipe;
            if (transfer->m_ndx >= 0) {
              wiped = !(Contacts::getContactFlags(transfer->m_contact) & CONTACT_FLAG_XFER_CANCEL);
            }
            if (wiped) sizes[transfer->m_ndx >= 0] += transfer->m_rcvdSize;
          }
        }
      }

      if (files[0]) qs = tr("You have selected <b>%n received file(s)</b> to cancel.", 0, files[0]).append("<br/>");
      if (files[1]) qs.append(tr("You have selected <b>%n sent file(s)</b> to cancel.", 0, files[1])).append("<br/>");
      if (sizes[0] > 0) {
        text = tr("<span style=\" color:#ff0000;\">%1 worth of received data are going to be erased.</span>");
        qs.append("<br/>").append(text.arg(convertFileSize(sizes[0])));
      }
      if (sizes[1] > 0) {
        text = tr("<span style=\" color:#ff0000;\">%1 worth of sent data are probably going to be erased.</span>");
        qs.append("<br/>").append(text.arg(convertFileSize(sizes[1])));
      }
      qs.append(sizes[0] > 0 || sizes[1] > 0 ? "<br/><br/>" : "<br/>");
      qs.append(tr("If you proceed, the files would have to be re-sent manually, so they can be received."));
      qs.append("<br/><br/>").append(tr("Are you sure you want to <b>cancel</b> these file transfers?"));
      if (files[0] + files[1] <= 1) qs = QString();
      if (files[0] + files[1] != 1) row = -1;
    }

    if (row >= 0 || selected.count() == 1) {
      transfer = m_transfers[row < 0 ? selected[0].row() : row];
      qlonglong n = transfer->m_rcvdSize;
      if (transfer->m_ndx >= 0) {
        if (n > 0) {
          text = tr("<b>%1</b> has already received <b>%2</b> of this file from you.");
          qs = text.arg(transfer->m_contact->m_nick.toHtmlEscaped(), convertFileSize(n)).append(" ");

          if (!(Contacts::getContactFlags(transfer->m_contact) & CONTACT_FLAG_XFER_CANCEL)) {
            qs.append(tr("<span style=\" color:#ff0000;\">The sent data are probably going to be erased.</span>"));
          }
          qs.append("<br/><br/>");
        }
        text = tr("If you proceed, <b>%1</b> will not receive the file, unless you send it again.");
      } else {
        if (n > 0) {
          text = tr("You have already received <b>%1</b> of this file from <b>%2</b>.");
          qs = text.arg(convertFileSize(n), transfer->m_contact->m_nick.toHtmlEscaped()).append(" ");
          if (SimParam::get("xfer.wipe")) {
            qs.append(tr("<span style=\" color:#ff0000;\">The received data are going to be erased.</span>"));
          } else {
            qs.append(tr("The received data will be kept, but you will lose the file name."));
          }
          qs.append("<br/><br/>");
        }
        text = tr("If you proceed, <b>%1</b> will have to send you the file again.");
      }

      qs.append(text.arg(transfer->m_contact->m_nick.toHtmlEscaped())).append("<br/><br/");
      text = tr("Are you sure you want to <b>cancel</b> the file transfer of \"%1\"?");
      qs.append("<br/><br/>").append(text.arg(transfer->getName().toHtmlEscaped()));
    }

    if (!qs.isNull()) {
      const QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
      if (QMessageBox::question(this, tr("Cancel file transfer"), qs, buttons, QMessageBox::No) != QMessageBox::Yes) {
        return;
      }
    }

    qtfix::setOverrideCursor(true);
  }

  if (action == hide) { // hide transfers in the per-transfer dialog
    QSet<qlonglong> hash;
    for (int j = row < 0 ? selected.count() : 1; j--;) {
      transfer = m_transfers[row < 0 ? selected[j].row() : row];
      qlonglong id = transfer->m_xferId < 0 ? -transfer->m_xferId : transfer->m_xferId;
      if (m_filters.contains(id)) hash.insert(id);
    }
    if (m_filters.count() == hash.count()) {
      qlonglong id = m_transfers[cell.row()]->m_xferId;
      hash.remove(id < 0 ? -id : id);
    }

    for (QHash<QString, Transfer *>::Iterator hit = m_files.begin(); m_files.end() != hit;) {
      if (hash.contains(hit.value()->m_xferId)) {
        m_filters.remove(hit.value()->m_xferId);
        derefTransfer(hit.value());
        hit = m_files.erase(hit);
      } else {
        hit++;
      }
    }

    onTimerTimeout('v');
  } else if (color == xfer_finished) { // remove finished files
    QHash<Transfer *, QString> hash;
    for (int j = row < 0 ? selected.count() : 1; j--;) {
      transfer = m_transfers[row < 0 ? selected[j].row() : row];
      if (transfer->m_ndx > 0 && getTransferState(transfer->m_type, transfer->m_simres) == xfer_finished) {
        hash.insert(transfer, QString());
      }
    }

    if (selected.count() > 1) {
      const QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
      QString qs = tr("You have selected <b>%n sent file(s)</b> to remove from this list.", 0, hash.count());
      qs.append(" ").append(tr("Are you sure you want to do this?"));
      if (QMessageBox::question(this, tr("Remove sent files"), qs, buttons, QMessageBox::No) != QMessageBox::Yes) {
        return;
      }
      qtfix::setOverrideCursor(true);
    }

    int nok = 0;
    for (QHash<QString, Transfer *>::Iterator hit = m_files.begin(); m_files.end() != hit; hit++) {
      QHash<Transfer *, QString>::Iterator it = hash.find(transfer = hit.value());
      if (hash.end() != it) {
        QString qs = m_current[transfer->m_contact]->getPath(true, hit.key());
        nok += ok = QFileInfo(qs).isSymLink() ? !QFile::remove(qs) : QFileInfo(qs).exists();
        if (!ok) {
          it.value() = hit.key();
        } else {
          hash.erase(it);
        }
      }
    }

    if (selected.count() > 1) log_debug_("ui", "elapsed REMOVE %lld ms\n", qtfix::setOverrideCursor(false));
    m_lastRow = -1;
    ui->fileTable->clearSelection();
    for (QHash<Transfer *, QString>::Iterator hit = hash.begin(); hash.end() != hit; hit++) {
      SimCore::get()->emitTransferChanged(hit.key()->m_contact->m_id, SIM_XFER_GET_SENT, hit.value(), "");
    }

    QString error = "\n";
    if (nok == 1) {
      transfer = m_transfers[row < 0 ? selected[0].row() : row];
      error.append(tr("Sent file \"%1\" could not be removed.").arg(transfer->getName().toHtmlEscaped()));
    } else {
      error.append(tr("%1 out of %2 sent files could not be removed.").arg(nok).arg(selected.count()));
    }
    if (nok) QMessageBox::warning(this, tr("Simphone warning"), error);
  } else { // cancel simcore transfers or start a single one
    std::vector<SimError> errorList;
    for (int j = row < 0 ? selected.count() : 1; j--;) {
      const char * type = action == cancel ? SIM_XFER_TYPE_CANCEL : SIM_XFER_TYPE_PAUSE;
      transfer = m_transfers[row < 0 ? selected[j].row() : row];
      int simres = SIM_OK;
      if (transfer->m_xferId > 0) simres = sim_xfer_set_(transfer->m_contact->m_simId, transfer->m_xferId, type);
      if (simres != SIM_OK) errorList.push_back(SimError(transfer->getName(), SimCore::getError(simres), simres));
    }

    if (action == cancel) {
      log_debug_("ui", "elapsed CANCEL %lld ms\n", qtfix::setOverrideCursor(false));
      m_lastRow = -1;
      ui->fileTable->clearSelection();
    } else {
      onTimerTimeout('b');
    }

    QString error;
    if (errorList.size() == 1) {
      if (action != cancel) {
        error = tr("Starting file transfer not successful (%1)");
      } else {
        error = tr("Cancelling file transfer of \"%1\" not successful (%2)").arg(errorList[0].m_name);
      }
      SimCore::execMessageBox(false, error.arg(errorList[0].m_simres), errorList[0].m_simerr);
    } else if (!errorList.empty()) {
      error = tr("Cancelling %1 out of %2 files not successful");
      showErrors(errorList, error.arg(errorList.size()).arg(selected.count()));
    }
  }
}

void Transfers::onRowClicked(const QModelIndex & index)
{
  QItemSelectionModel * select = ui->fileTable->selectionModel();
  if (getSelectedRows(select) == 1 && select->selectedRows().value(0).row() == index.row()) {
    if (index.row() == m_lastRow) select->clearSelection();
    m_lastRow = index.row() == m_lastRow ? -1 : index.row();
  } else {
    m_lastRow = -1;
  }
}

void Transfers::onRowDoubleClicked(const QModelIndex & index)
{
  ui->fileTable->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
  m_lastRow = -1;
}

// 0 - periodic, b - buttons only, s - sort only, v - resize
void Transfers::onTimerTimeout(char update)
{
  if (!m_contact) return;

  // update current transfers if any, and check changes to the contact list
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  QBitArray bits(contacts.size());
  int pause = 0;
  for (unsigned j = m_dialog ? m_contact->m_id + 1 : contacts.size(); j--;) {
    Contact * contact = contacts[j];
    if (m_dialog) j = 0;
    if (!contact->isTest()) {
      simtype info = sim_xfer_get_(contact->m_simId, SIM_XFER_GET_INFO, SIM_XFER_BIT_DEFAULT);
      simnumber xferId = sim_table_get_number(info, SIM_XFER_GET_INFO_CURRENT);
      pause |= (sim_table_get_number(info, SIM_XFER_GET_INFO_PAUSE) == SIM_CMD_XFER_INIT_PAUSE_ALL) ? 2 : 1;
      sim_xfer_free_(info);
      if (!update && xferId) onSignalTransferState(contact->m_id, xferId, 0);
      if (!m_dialog && Contacts::get()->isContactShown(contact)) bits.setBit(contact->m_id);
    }
  }

  // update button visibility
  if (pause & 1) {
    m_pauseButton->setVisible(true);
    m_startButton->setVisible(pause >> 1);
  } else {
    QWidget::setTabOrder(m_pauseButton, m_startButton);
    m_startButton->setVisible(pause >> 1);
    m_pauseButton->setVisible(false);
    QWidget::setTabOrder(m_startButton, m_pauseButton);
  }
  m_updateButton->setVisible(m_update);

  // check whether updates are stopped and exit unless they are to be resumed
  int selectedRow;
  int count = getSelectedRows(ui->fileTable->selectionModel(), &selectedRow);
  simnumber selectedId = count == 1 ? m_transfers[selectedRow]->m_xferId : 0;
  Contact * selectedContact = count == 1 ? m_transfers[selectedRow]->m_contact : m_contact;
  if (update == 'b' || (!update && count > 1)) return;

  if (m_contacts != bits) {
    QBitArray oldBits(m_contacts);
    m_contacts = bits;
    bits.resize(oldBits.size());
    if (oldBits != bits) update = 'v';
  }

  if (!update) {
    if (m_updateTime <= 100000000) {
      m_updateTime = 0;
      return;
    }
    if (m_updateTime != m_lastTime) {
      m_lastTime = m_updateTime;
      return;
    }
    if (m_updateTime <= m_createTime) {
      m_updateTime = 0;
      return;
    }
  }
  m_updateButton->setVisible(m_update = false);
  m_lastTime = m_updateTime = 0;

  // set up for a full update
  qint64 msec = 0;
  qtfix::setOverrideCursor(true);
  for (unsigned i = 0; i < m_transfers.size(); ++i) derefTransfer(m_transfers[i]);
  m_transfers.resize(0);
  m_handles.clear();

  bool resize = update == 'v' && (m_viewPaths->isChecked() || !m_timer.isActive());
  bool viewPaths = hasViewSentPaths();
  int viewSent = (m_viewSent->isChecked() ? SIM_XFER_GET_SENT : 0) | SIM_XFER_GET_RECEIVED;
  if (viewSent != SIM_XFER_GET_RECEIVED) m_transfers.push_back(new Transfer(m_dialog, SIM_XFER_TYPE_INIT));

  unsigned len = 0;
  simnumber n = m_minIndex = NDX_BASE;
  if (m_dialog >= 2) {
    QString fileName;
    // per-transfer dialog: fetch transfers from simcore per filter id
    for (QSet<qlonglong>::Iterator it = m_filters.begin(); m_filters.end() != it; it++) {
      Transfer * transfer = newTransfer(m_contact, *it, resize ? &fileName : 0);
      if (!transfer) transfer = newTransfer(m_contact, -*it, resize ? &fileName : 0);
      if (transfer) {
        setColWidth(&m_colWidth[xfercol_name], fileName);
        m_transfers.push_back(transfer);
        transfer->m_ndx |= n++;
        m_handles.insertMulti(transfer->m_xferId, transfer);
      }
    }

    // per-transfer dialog: update filters per name with data from the filesystem
    for (QHash<QString, Transfer *>::Iterator hit = m_files.begin(); m_files.end() != hit; hit++) {
      Transfer * transfer = 0;
      int type = hit.value()->m_ndx < 0 ? 0 : hit.key().contains(QDir::separator()) ? -1 : 1;
      int simres = hit.value()->m_simres;

      int ok = type || simres == SIM_OK;
      for (int attempt = ok ? 1 : -1; attempt <= ok && !transfer; ++attempt) {
        fileName.sprintf("%019lld%s", hit.value()->m_xferId, attempt ? ".temp" : ".part");
        if (attempt && isTransferFound(hit.value()->m_contact, fileName)) break;
        if (attempt > 0) fileName = hit.key();
#ifdef _WIN32
        if (type < 0 && fileName.size() > 1 && fileName.at(0) == '\\' && fileName.at(1) != '\\') type = -2;
        if (type < 0 && fileName.left(2) == ":\\") type = -2;
#else
        if (type < 0 && fileName.left(2) == ":/") type = -2;
#endif
        QFileInfo info(type < 0 ? fileName : m_current[m_contact]->getPath(type, fileName));
        transfer = newFile(m_contact, type, hit.value()->m_ndx, info);
      }

      if (transfer) {
        transfer->m_xferId = hit.value()->m_xferId;
        if (type) transfer->m_sendTime = hit.value()->m_sendTime;
        transfer->m_simres = short(simres);
        if (transfer->m_simres != SIM_OK) transfer->m_rcvdSize = -1;
        derefTransfer(hit.value());
        hit.value() = transfer;
      }

      QByteArray pathName = fileName.toUtf8();
      simtype name = sim_pointer_new_length(pathName.data(), pathName.size() + 1);
      int pos = hit.value()->setName(sim_get_pointer(name), &sim_get_length(name), viewPaths);
      setColWidth(&m_colWidth[xfercol_name], hit.value()->m_fileName.mid(pos, sim_get_length(name)));
    }
  } else {
    // fetch transfers from simcore
    for (unsigned k = 0; k < contacts.size(); ++k) {
      Contact * contact = contacts[k];
      if (((!m_dialog && Contacts::get()->isContactShown(contact)) || contact == m_contact) && !contact->isTest()) {
        static const char * tables[] = {
          SIM_CMD_XFER_SEND_TYPE, SIM_CMD_XFER_SEND_HANDLE, SIM_CMD_XFER_SEND_NAME,
          SIM_CMD_XFER_SEND_SIZE, SIM_CMD_XFER_OFFSET, SIM_CMD_XFER_SEND_TIME, SIM_CMD_XFER_CLOSE_ERROR
        };
        simtype xfers = sim_xfer_get_(contact->m_simId, viewSent, SIM_XFER_BIT_DEFAULT);
        simtype arrays[7];
        for (unsigned i = 0; i < SIM_ARRAY_SIZE(tables); ++i) arrays[i] = sim_table_get(xfers, tables[i]);

        for (unsigned i = 1; i <= arrays->len; i++) {
          unsigned j = *m_sortOrder ? unsigned(qulonglong(i - 1) * unsigned(-5) % arrays->len + 1) : i;
          const char * type = sim_array_get_pointer(arrays[0], j);
          simnumber xferId = sim_array_get_number(arrays[1], j);
          Transfer * transfer = new Transfer(m_dialog, type, contact, xferId, n + j - 1);
          transfer->m_fileSize = sim_array_get_number(arrays[3], j);
          transfer->m_rcvdSize = sim_array_get_number(arrays[4], j);
          transfer->m_sendTime = sim_array_get_number(arrays[5], j);
          transfer->m_simres = short(sim_array_get_number(arrays[6], j));
          if (transfer->m_simres == SIM_XFER_NO_ERROR) transfer->m_simres = SIM_OK;

          simtype name = sim_array_get(arrays[2], j);
          int pos = transfer->setName(sim_get_pointer(name), &sim_get_length(name), viewPaths);
          if (resize && sim_get_length(name) > len) {
            setColWidth(&m_colWidth[xfercol_name], transfer->m_fileName.mid(pos, len = sim_get_length(name)));
          }
          m_transfers.push_back(transfer);
          m_handles.insertMulti(transfer->m_xferId, transfer);
        }
        n += arrays->len;
        sim_xfer_free_(xfers);
      }
    }

    if (update == 'v') {
      // fetch finished files from the filesystem subdirectories
      msec = qtfix::getOverrideCursor();
      QHash<QString, Transfer *>::Iterator hit;
      for (hit = m_files.begin(); m_files.end() != hit; hit++) derefTransfer(hit.value());
      m_files.clear();
      m_fileIndex = -1;

      if (m_viewFinished->isChecked()) {
        for (unsigned i = 0; i < contacts.size(); ++i) {
          Contact * contact = contacts[i];
          if (((!m_dialog && Contacts::get()->isContactShown(contact)) || contact == m_contact) && !contact->isTest()) {
            for (int type = 0; type <= (viewSent != SIM_XFER_GET_RECEIVED ? 1 : 0); type++) {
              QString name = m_current[contact]->getPath(type);
              if (!name.isEmpty()) {
                QDirIterator it(name, QDir::Files | QDir::System | QDir::Hidden);
                while (!it.next().isEmpty()) {
                  if (type || !isTransferFound(contact, it.fileName())) {
                    QString fileName;
                    QFileInfo info = it.fileInfo();
                    Transfer * transfer = newFile(contact, type, ++m_fileIndex, info, resize ? &fileName : 0);
                    if (transfer) m_files.insertMulti(info.fileName(), transfer);
                    setColWidth(&m_colWidth[xfercol_name], fileName);
                  }
                }
              }
            }
          }
        }
      }

      msec -= qtfix::getOverrideCursor();
      log_debug_("ui", "elapsed FILES %lld ms\n", -msec);
    }
  }
  m_maxIndex = n - 1;

  // merge filters per name or finished files into the list of transfers, skipping duplicates
  for (QHash<QString, Transfer *>::Iterator hit = m_files.begin(); m_files.end() != hit;) {
    Transfer * transfer = hit.value();
    QString fileName = hit.key();
    if (m_dialog >= 2) fileName.sprintf("%019lld.temp", transfer->m_xferId);
    if ((m_dialog < 2 && (transfer->m_ndx > 0 || update == 'v')) || !isTransferFound(transfer->m_contact, fileName)) {
      refTransfer(transfer);
      m_transfers.push_back(transfer);
      m_handles.insertMulti(transfer->m_xferId, transfer);
      hit++;
    } else if (m_dialog < 2) {
      derefTransfer(transfer);
      hit = m_files.erase(hit);
    } else {
      hit++;
    }
  }

  if (*m_sortOrder || viewSent != SIM_XFER_GET_RECEIVED || !m_files.isEmpty()) {
    std::sort(m_transfers.begin(), m_transfers.end(), compareTransfers);
  }

  // update table rows with items from the list of transfers, preserving selected row if any
  m_lastRow = -1;
  ui->fileTable->clearContents();
  ui->fileTable->setRowCount(m_transfers.size());
  if (update) ui->fileTable->setAutoScroll(true);

  QSet<QString> nicks;
  QHeaderView * header = ui->fileTable->horizontalHeader();
  int frameRow = m_frameRow;
  m_frameRow = -1;
  for (int j = 0; j < int(m_transfers.size()); ++j) {
    Transfer * transfer = m_transfers[j];
    Contact * contact = transfer->m_contact;

    if (transfer->m_ndx) {
      if (resize && !header->isSectionHidden(xfercol_nick) && !nicks.contains(contact->m_nick)) {
        setColWidth(&m_colWidth[xfercol_nick], contact->m_nick);
        nicks.insert(contact->m_nick);
      }

      if (!header->isSectionHidden(xfercol_nick)) ui->fileTable->setItem(j, xfercol_nick, new TransferItem);
      for (int i = xfercol_size; i < xfercol_nCols; ++i) ui->fileTable->setItem(j, i, new TransferItem);
      if (frameRow == j) {
        for (int i = 0; i < xfercol_nCols; ++i) ui->fileTable->removeCellWidget(j, i);
      }
    } else {
      m_frameRow = j;
      for (int i = 0; i < xfercol_nCols; ++i) {
        QTableWidgetItem * item = new QTableWidgetItem();
        item->setFlags(Qt::NoItemFlags);
        if (viewPaths) item->setText("\n");
        if (!header->isSectionHidden(i)) ui->fileTable->setItem(j, i, item);

        QFrame * frame = new QFrame(ui->fileTable);
        frame->setFrameShape(QFrame::HLine);
        frame->setLineWidth(SimParam::get(m_dialog ? "ui.xfer.lines" : "ui.xfer.line"));
        frame->setToolTip(tr("Sent files are below this line.\nReceived files are above this line."));
        ui->fileTable->setCellWidget(j, i, frame);
      }
    }

    if (contact == selectedContact && transfer->m_xferId == selectedId) ui->fileTable->selectRow(m_lastRow = j);
  }

  // resize table columns and finalize the update
  for (int i = 0; i < xfercol_name + resize; ++i) {
    if (ui->fileTable->columnWidth(i) < m_colWidth[i]) ui->fileTable->setColumnWidth(i, m_colWidth[i]);
  }

  if (update) ui->fileTable->setAutoScroll(false);
  msec += qtfix::setOverrideCursor(false);
  if (msec < 0) msec = 0;
  m_createTime = (msec + 1) * 500000;
  if (m_createTime < 100000000) m_createTime = 100000000;
  log_debug_("ui", "elapsed XFERS %lld ms\n", msec);
}

void Transfers::on_buttonBox_clicked(QAbstractButton * button)
{
  if (!button || m_updateButton == button) {
    onTimerTimeout(button ? 's' : 'v');
    return;
  }

  bool pause = m_pauseButton == button;
  if (!pause && m_startButton != button) return;
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  bool ok = true;
  for (unsigned j = 0; j < contacts.size(); ++j) {
    Contact * contact = contacts[j];
    if ((!m_dialog || contact == m_contact) && !contact->isTest()) {
      simnumber xferId = pause ? SIM_CMD_XFER_INIT_PAUSE_ALL : SIM_CMD_XFER_INIT_PAUSE_NONE;
      int simres = sim_xfer_set_(contact->m_simId, xferId, SIM_XFER_TYPE_PAUSE);
      if (simres != SIM_OK) {
        QString error;
        if (pause) {
          error = tr("Pausing file transfers with %1 not successful (%2)");
        } else {
          error = tr("Unpausing file transfers with %1 not successful (%2)");
        }
        qtfix::execMessageBox(false, error.arg(contact->m_nick).arg(simres), SimCore::getError(simres), this);
        ok = false;
      }
    }
  }

  onTimerTimeout('b');

  if (ok) {
    QString qs;
    const char * param;
    if (pause) {
      if (m_dialog) {
        qs = tr("You have stopped any file transfers with <b>%1</b> temporarily.");
        qs = qs.arg(m_contact->m_nick.toHtmlEscaped());
        param = "ui.xfer.pauses";
      } else {
        qs = tr("You have stopped any file transfers with all your contacts temporarily.");
        param = "ui.xfer.pause";
      }
      qs.append("<br/><br/>").append(tr("Please, push the <b>Resume</b> button whenever you want to restart them."));
    } else {
      if (m_dialog) {
        qs = tr("You have restarted any stopped file transfers with <b>%1</b>.");
        qs = qs.arg(m_contact->m_nick.toHtmlEscaped());
        param = "ui.xfer.unpauses";
      } else {
        qs = tr("You have restarted any stopped file transfers with all your contacts.");
        param = "ui.xfer.unpause";
      }
    }
    if (SimParam::get(param)) {
      QString title = pause ? tr("Pause file transfers") : tr("Resume file transfers");
      qs.prepend("<br/>");
      if (qtfix::execMessageBox(QMessageBox::Information, title.toUtf8().data(), qs.append("<br/><br/>"), this,
                                tr("Do not show this information again."))) SimParam::set(param, 0, false);
    }
  }
}

void Transfers::on_buttonBox_accepted()
{
  saveSettings(false);
}

void Transfers::saveSettings(bool reset)
{
  if (m_dialog < 2) {
    QByteArray saved;
    simtype params = sim_table_new(3);
    const char * sortOrder = "";

    if (!reset) {
      sim_table_add_number(params, m_dialog ? "ui.xfer.files" : "ui.xfer.file", m_viewFinished->isChecked());
      sim_table_add_number(params, m_dialog ? "ui.xfer.sents" : "ui.xfer.sent", m_viewSent->isChecked());
      sim_table_add_number(params, m_dialog ? "ui.xfer.paths" : "ui.xfer.path", m_viewPaths->isChecked());
      sim_table_add_number(params, m_dialog ? "ui.xfer.units" : "ui.xfer.unit", getTimeUnits());
      saved = ui->fileTable->horizontalHeader()->saveState();
      sortOrder = m_sortOrder;
    }

    sim_table_add_pointer_length(params, m_dialog ? "ui.xfer.headers" : "ui.xfer.header", saved.data(), saved.size());
    sim_table_add_pointer(params, m_dialog ? "ui.xfer.orders" : "ui.xfer.order", sortOrder);
    SimParam::set(params);
    sim_table_free(params);
  }

  m_timer.stop();
  accept();
}

QString Transfers::convertFileSize(qlonglong n) const
{
  static const char * sizes[] = {
    QT_TR_NOOP("bytes"), QT_TR_NOOP("KB"), QT_TR_NOOP("MB"),
    QT_TR_NOOP("GB"), QT_TR_NOOP("TB"), QT_TR_NOOP("PB"), QT_TR_NOOP("EB")
  };

  int i;
  int k = 0;
  for (i = 0; n >> 9; i++) {
    k = int(n & 1023);
    n >>= 10;
  }

  int precision = !i ? 0 : m_precision >= 0 ? m_precision : n < 10 ? 3 : n < 100 ? 2 : 1;
  return QString::number((double(n << 10) + k) / 1024.0, 'f', precision).append(' ').append(tr(sizes[i]));
}

QChar Transfers::convertFileName(QString & fileName)
{
#ifdef _WIN32
  if (fileName.size() > 1 && fileName.at(0) == '/' && fileName.at(1) != '/') {
    for (int i = 0; i < fileName.size(); ++i) {
      if (fileName[i] == '/') {
        fileName[i] = '\\';
      } else if (fileName[i] == '\\') {
        fileName[i] = '/';
      }
    }
  } else {
    fileName.replace('/', '\\');
  }
#endif
  return fileName.contains(QDir::separator()) ? QChar() : QDir::separator();
}

bool Transfers::isTransferFound(Contact * contact, const QString & fileName, bool ignore)
{
  if (fileName.size() != 24) return false;
  QString qs = fileName.right(5);
  if (qs != ".temp" && qs != ".part") return false;
  qlonglong xferId = fileName.left(19).toULongLong();
  if (!xferId || findTransfer(contact, -xferId) != m_handles.end()) return xferId != 0;
  return !ignore && findTransfer(contact, xferId) != m_handles.end();
}

QHash<qlonglong, Transfer *>::Iterator Transfers::findTransfer(Contact * contact, qlonglong xferId)
{
  QHash<qlonglong, Transfer *>::Iterator hit = m_handles.find(xferId);
  while (m_handles.end() != hit && hit.value()->m_contact != contact) {
    if (m_handles.end() == ++hit || hit.key() != xferId) {
      hit = m_handles.end();
      break;
    }
  }
  return hit;
}

void Transfers::refTransfer(Transfer * transfer)
{
  Q_ASSERT(!transfer->m_shared);
  transfer->m_shared = true;
}

void Transfers::derefTransfer(Transfer * transfer)
{
  bool shared = transfer->m_shared;
  transfer->m_shared = false;
  if (!shared) delete transfer;
}

Transfer * Transfers::newTransfer(Contact * contact, qlonglong xferId, QString * fileName) const
{
  simtype xfer = sim_xfer_get_(contact->m_simId, xferId, SIM_XFER_BIT_DEFAULT);
  if (!sim_get_pointer(xfer)) return 0;
  const char * type = sim_table_get_pointer(xfer, SIM_CMD_XFER_SEND_TYPE);
  Transfer * transfer = new Transfer(m_dialog, type, contact, xferId);
  simtype name = sim_table_get(xfer, SIM_CMD_XFER_SEND_NAME);
  int pos = transfer->setName(sim_get_pointer(name), &sim_get_length(name), hasViewSentPaths());
  if (fileName) *fileName = transfer->m_fileName.mid(pos, sim_get_length(name));
  transfer->m_fileSize = sim_table_get_number(xfer, SIM_CMD_XFER_SEND_SIZE);
  transfer->m_rcvdSize = sim_table_get_number(xfer, SIM_CMD_XFER_OFFSET);
  transfer->m_sendTime = sim_table_get_number(xfer, SIM_CMD_XFER_SEND_TIME);
  transfer->m_simres = short(sim_table_get_number(xfer, SIM_CMD_XFER_CLOSE_ERROR));
  if (transfer->m_simres == SIM_XFER_NO_ERROR) transfer->m_simres = SIM_OK;
  sim_xfer_free_(xfer);
  return transfer;
}

Transfer * Transfers::newFile(Contact * contact, int type, qlonglong ndx, const QFileInfo & info, QString * fileName)
{
  QString qs = type < -1 || info.isFile() ? info.fileName() : QString();
  if (type > 0 && info.isSymLink()) qs = info.symLinkTarget();
  if (qs.isNull()) return 0;

  simnumber xferId = qulonglong(5) << 61 | ndx;
  Transfer * transfer = new Transfer(m_dialog, type ? "sent" : "rcvd", contact, xferId, ndx | NDX_FILE);
  transfer->m_rcvdSize = transfer->m_fileSize = type >= -1 ? info.size() : 0;
  if (type < -1 || !info.exists()) transfer->m_fileSize = -1;
  transfer->m_sendTime = type >= -1 ? info.created().toMSecsSinceEpoch() * 1000000 : 0;
  transfer->m_simres = SIM_OK;

#ifdef _WIN32
  if (type > 0) qs.replace('/', QDir::separator());
#endif
  QByteArray pathName = qtfix::getString(type < 0 ? info.filePath() : qs, "[\\000-\\037]").toUtf8();
  simtype name = sim_pointer_new_length(pathName.data(), pathName.size() + 1);
  if (!type) {
    transfer->m_simres = short(sim_xfer_send_file_(CONTACT_ID_XFER_RECEIVED, name, 0));
    if (transfer->m_simres != SIM_OK) transfer->m_rcvdSize = -1;
  } else if (type > 0) {
    sim_xfer_send_file_(CONTACT_ID_XFER_SENT, sim_pointer_new(info.filePath().toUtf8().data()), &transfer->m_sendTime);
  }

  int pos = transfer->setName(sim_get_pointer(name), &sim_get_length(name), hasViewSentPaths());
  if (fileName) *fileName = transfer->m_fileName.mid(pos, sim_get_length(name));
  return transfer;
}

Transfer * Transfers::newFilter(int type, qlonglong xferId, const QString & xferTagName, qlonglong xferTs)
{
  Transfer * transfer = 0;
  QStringList list = xferTagName.split(' ');
  if (list.size() >= 3) {
    QString fileName = list.at(2);
    if (convertFileName(fileName).isNull()) type = -type;
    QString pathName = type < 0 || fileName.isEmpty() ? fileName : m_current[m_contact]->getPath(type, fileName);

    if (!pathName.isEmpty()) {
      int simres = list.at(1).toInt();
      transfer = new Transfer(m_dialog, type ? "sent" : "rcvd", m_contact, xferId, ++m_fileIndex | NDX_FILE);
      transfer->m_fileName = type < 0 ? pathName : fileName;
      transfer->m_fileSize = -1;
      transfer->m_rcvdSize = simres == SIM_OK ? 0 : -1;
      transfer->m_sendTime = xferTs * 1000000000;
      transfer->m_simres = short(simres);
    }
  }
  return transfer;
}

void Transfers::setFilter(int msgNdx)
{
  if (msgNdx <= 0 || (m_dialog != 2 && m_timer.isActive()) || !m_dialog) return;

  simnumber xferId = 0;
  simnumber xferTs = 0;
  QString xferTagName = getMessage(m_contact->m_simId, msgNdx, &xferId, &xferTs);

  int type = int(((xferId >> 62) + 1) >> 1);
  if (type) xferId ^= qlonglong(1) << 62;
  if (m_filters.contains(xferId)) return;
  m_filters.insert(xferId);

  Transfer * transfer = newFilter(type, xferId, xferTagName, xferTs);
  if (transfer) m_files.insertMulti(transfer->m_fileName, transfer);
  if (m_timer.isActive()) onTimerTimeout('v');
}

bool Transfers::setTransfer(Contact * contact, Transfer * transfer, const char * type, qlonglong xferId, bool prepend)
{
  // check for update stop and exit if so
  bool removed = false;
  int selectedRow = -1;
  QItemSelectionModel * select = ui->fileTable->selectionModel();
  if ((m_updateTime > m_createTime && m_createTime) || getSelectedRows(select, &selectedRow) > 1) {
    if (type) {
      m_updateTime = qMax(m_updateTime, m_createTime) + 1;
      m_lastTime = 0;
      m_updateButton->setVisible(m_update = true);
    }
    return false;
  }

  QElapsedTimer timer;
  timer.start();
  if (!type) m_current[contact]->updateProgress();

  // fetch the new transfer and find whether it already existed
  if (transfer) {
    xferId = transfer->m_xferId;
    if (!*type) {
      transfer = 0;
    } else {
      refTransfer(transfer);
    }
  } else {
    transfer = newTransfer(contact, xferId);
  }

  QHash<qlonglong, Transfer *>::Iterator hit = findTransfer(contact, xferId);
  std::vector<Transfer *>::iterator it;
  int row = -1;
  int oldRow = 0;
  int newRow = -1;

  if (m_handles.end() != hit) { // transfer existed: find it inside the sorted list of transfers
    Q_ASSERT(!m_transfers.empty());
    it = std::lower_bound(m_transfers.begin(), m_transfers.end(), hit.value(), compareTransfers);
    oldRow = row = it - m_transfers.begin();
    Q_ASSERT(m_transfers.end() != it && hit.value() == m_transfers[row]);

    if (transfer) { // transfer still exists: find its new position in the list
      if (!prepend) {
        transfer->m_ndx = m_transfers[row]->m_ndx;
      } else {
        transfer->m_ndx |= --m_minIndex & ~NDX_MASK;
      }
      if (*m_sortOrder || prepend) {
        it = std::lower_bound(m_transfers.begin(), m_transfers.end(), transfer, compareTransfers);
        row = it - m_transfers.begin();
      } else {
        it = m_transfers.begin();
        Q_ASSERT(std::lower_bound(it, m_transfers.end(), transfer, compareTransfers) == it + row);
      }

      bool nok = m_transfers.size() == unsigned(row);
      if (nok || m_transfers[row]->m_xferId != xferId || m_transfers[row]->m_contact != contact) {
        Transfer ** transfers = &m_transfers[0];
        int n = oldRow - row;
        int selectRow = -1;

        // move transfer to a new position so the sorted list remains sorted
        derefTransfer(transfers[oldRow]);
        if (n > 0) {
          if (selectedRow >= row && selectedRow < oldRow) selectRow = selectedRow + 1;
          memmove(&transfers[row + 1], &transfers[row], n * sizeof(*transfers));
        } else if ((n = --row - oldRow) > 0) {
          if (selectedRow > oldRow && selectedRow <= row) selectRow = selectedRow - 1;
          memmove(&transfers[oldRow], &transfers[oldRow + 1], n * sizeof(*transfers));
        }
        hit.value() = transfers[row] = transfer;
        if (selectRow >= 0 || selectedRow == oldRow) {
          select->clearSelection();
          m_lastRow = selectRow >= 0 ? selectRow : row;
          ui->fileTable->selectRow(m_lastRow);
        }

        if (oldRow > row) {
          newRow = oldRow;
          oldRow = row;
        } else {
          newRow = row;
        }
        Q_ASSERT(m_frameRow < oldRow || m_frameRow > newRow);
      } else if (!prepend) { // no change of position: simply replace transfer
        Q_ASSERT(row == oldRow);
        derefTransfer(m_transfers[row]);
        hit.value() = m_transfers[newRow = row] = transfer;
      } else {
        if (transfer) derefTransfer(transfer);
      }
    } else { // transfer no longer exists: delete it from the list
      if (m_transfers.end() != it) derefTransfer(hit.value());
      m_transfers.erase(it);
      m_handles.erase(hit);
      if (row < m_frameRow) m_frameRow--;
      if (row < m_lastRow) m_lastRow--;
      ui->fileTable->removeRow(row);
      removed = true;
    }
  } else if (type && transfer) { // transfer didn't exist: insert into the sorted list of transfers
    if (*type != ' ') transfer->m_ndx |= (!prepend ? ++m_maxIndex : --m_minIndex) & ~NDX_MASK;
    newRow = oldRow = row = ui->fileTable->rowCount();
    if (m_transfers.empty()) {
      m_transfers.push_back(transfer);
    } else if (!m_dialog || *m_sortOrder || m_frameRow >= 0 || !m_files.isEmpty() || prepend) {
      it = std::lower_bound(m_transfers.begin(), m_transfers.end(), transfer, compareTransfers);
      newRow = oldRow = row = it - m_transfers.begin();
      m_transfers.insert(it, transfer);
    } else {
      Q_ASSERT((it = m_transfers.end()) - m_transfers.begin() == row);
      Q_ASSERT(std::lower_bound(m_transfers.begin(), m_transfers.end(), transfer, compareTransfers) == it);
      m_transfers.push_back(transfer);
    }

    m_handles.insertMulti(xferId, transfer);
    if (row <= m_frameRow) m_frameRow++;
    if (row <= m_lastRow) m_lastRow++;
    ui->fileTable->insertRow(row);
  } else { // transfer didn't exist and still doesn't: ignore it
    if (transfer) derefTransfer(transfer);
  }

  // update table rows that got changed
  QHeaderView * header = ui->fileTable->horizontalHeader();
  for (int j = oldRow; j <= newRow; ++j) {
    Q_ASSERT(m_frameRow != j);
    if (!header->isSectionHidden(xfercol_nick)) ui->fileTable->setItem(j, xfercol_nick, new TransferItem);
    if (!header->isSectionHidden(xfercol_time)) {
      if (type || j != row) {
        delete ui->fileTable->takeItem(j, xfercol_time);
      } else {
        ui->fileTable->setItem(j, xfercol_time, new TransferItem);
      }
    }
    for (int i = xfercol_size; i < xfercol_nCols; ++i) ui->fileTable->setItem(j, i, new TransferItem);
  }

  if (type) m_updateTime += timer.nsecsElapsed() + 1;
  return removed;
}

void Transfers::setColWidth(int * width, const QString & fileName) const
{
  *width = qMax(*width, ui->fileTable->fontMetrics().width(fileName) + m_addWidth);
}

void Transfers::setTimeUnits(int units)
{
  m_colWidth[xfercol_time] = 0;
  if (!units) {
    ui->fileTable->horizontalHeader()->hideSection(xfercol_time);
    for (int i = 0; i < ui->fileTable->rowCount(); ++i) delete ui->fileTable->takeItem(i, xfercol_time);
  } else if (units < 3) {
    static const int maxWidths[2][5] = {
      { 59, 3599, 86399, 604799, 31449600 }, { 3599, 86399, 2419199, 31449599, 31449600 }
    };
    for (unsigned i = 0; i < SIM_ARRAY_SIZE(*maxWidths); ++i) {
      setColWidth(&m_colWidth[xfercol_time], Contact::convertTimeToText(maxWidths[units - 1][i], units));
    }
  } else {
    m_colWidth[xfercol_time] = ui->fileTable->fontMetrics().width("00:00:00");
  }
  if (units) ui->fileTable->horizontalHeader()->showSection(xfercol_time);
}

int Transfers::getSelectedRows(const QItemSelectionModel * model, int * row)
{
  int count = qtfix::hasSelectedRows(model);
  if (count == 1 || count == 2) {
    QModelIndexList selected = model->selectedRows();
    count = selected.count();
    if (count == 1 && row) *row = selected[0].row();
  }
  return count;
}

QVariant Transfers::getCurrent(Transfer * transfer, char field)
{
  simnumber current = 0;
  simnumber msec = 0;
  simnumber bpms = 0;
  const char * type = transfer->m_type;

  if (!strcmp(type, SIM_XFER_TYPE_RECV) || !strcmp(type, SIM_XFER_TYPE_SEND) || !strcmp(type, SIM_XFER_TYPE_DONE)) {
    simtype info = sim_xfer_get_(transfer->m_contact->m_simId, SIM_XFER_GET_INFO, SIM_XFER_BIT_DEFAULT);
    current = transfer->m_xferId;
    if (sim_table_get_number(info, SIM_XFER_GET_INFO_CURRENT) == current) {
      msec = sim_table_get_number(info, SIM_XFER_GET_INFO_DURATION);
      if (msec >= 1000) bpms = sim_table_get_number(info, SIM_XFER_GET_INFO_TRANSFERRED) / msec;
    } else {
      msec = -2;
    }
    sim_xfer_free_(info);
  }

  if (field != 'r' || transfer->m_rcvdSize < 0) {
    if (field != 't' || transfer->m_rcvdSize < 0 || transfer->m_fileSize - transfer->m_rcvdSize <= 0 || bpms <= 0) {
      return field || !msec ? QString() : msec < -1 ? "" : " ";
    }
    simnumber sec = (transfer->m_fileSize - transfer->m_rcvdSize) / (bpms * 1000);
    int units = transfer->getDialog()->getTimeUnits();
    return units < 3 ? Contact::convertTimeToText(sec, units) : Contact::convertTimeToString(sec);
  }
  return m_current[transfer->m_contact]->getProgress(current, transfer->m_rcvdSize, transfer->m_fileSize);
}

QString Transfers::getToolTip(Transfer * transfer)
{
  if (transfer->m_simres != SIM_OK) {
    QString qs = tr("<b>FAIL%1:</b> This file transfer is failing.<br/><b>%2</b>");
    if (transfer->m_simres == SIM_XFER_CANCELLED) qs = tr("<b>FAIL-0:</b> This file transfer is being cancelled.");
    if (!strcmp(transfer->m_type, "rcvd")) {
      int simres = transfer->m_simres;
      qs = tr("<b>rcvd%1:</b> This file transfer seems to have failed.<br/><b>%2</b>");
      if (simres == SIM_XFER_CANCELLED) qs = tr("<b>rcvd%1:</b> This file transfer seems to have been cancelled.");
    } else if (!strcmp(transfer->m_type, "sent")) {
      qs = tr("<b>sent%1:</b> This file transfer has failed.<br/><b>%2</b>");
      if (transfer->m_simres == SIM_XFER_CANCELLED) qs = tr("<b>sent%1:</b> This file transfer has been cancelled.");
    } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_HOLD)) {
      qs = tr("<b>WAIT%1:</b> Your contact could not yet receive this file.<br/><b>%2</b>");
    }
    return qs.arg(transfer->m_simres).arg(SimCore::getErrorBuffer(transfer->m_simres));
  } else if (!strcmp(transfer->m_type, "rcvd")) {
    return tr("<b>rcvd-0:</b> You have received the complete content of this file.");
  } else if (!strcmp(transfer->m_type, "sent")) {
    return tr("<b>sent-0:</b> The complete content of this file has been received by your contact.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_WIPE)) {
    if (transfer->m_rcvdSize != -1) return tr("<b>WIPE-1:</b> This file is currently being erased.");
    return tr("<b>WIPE-0:</b> This file will be erased when you restart Simphone.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_RECV)) {
    QString qs = getCurrent(transfer).toString();
    if (qs.isNull()) return tr("<b>RECV-0:</b> Waiting for your contact to start sending the content of this file.");
    return tr("<b>RECV-1:</b> You are currently receiving the content of this file from your contact.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_HASH)) {
    return tr("<b>DATA-1:</b> The content of this file is now being checked, before it can be received.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_DATA)) {
    return tr("<b>RECV-2:</b> You have tried to receive this file, but the transfer is not currently running.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_INIT)) {
    return tr("<b>INIT-0:</b> This file will be sent to your contact later.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_SEND)) {
    QString qs = getCurrent(transfer).toString();
    if (qs.isNull()) return tr("<b>SEND-0:</b> The content of this file is now being checked, before it can be sent.");
    if (!qs.isEmpty()) return tr("<b>SEND-1:</b> You are currently sending the content of this file to your contact.");
    return tr("<b>INIT-1:</b> Your contact has accepted this file and is about to receive its content.");
  } else if (!strcmp(transfer->m_type, SIM_XFER_TYPE_DONE)) {
    return tr("<b>SEND-2:</b> The complete content of this file has already been sent to your contact.");
  }
  if (!strcmp(transfer->m_type, SIM_XFER_TYPE_EXEC)) return tr("<b>DATA-0:</b> You will receive this file later.");
  if (strcmp(transfer->m_type, SIM_XFER_TYPE_WAIT)) return QString();
  return tr("<b>WAIT-0:</b> Waiting for your contact to accept this file.");
}

QString Transfers::getMessage(qlonglong simId, int msgNdx, qlonglong * xferId, qlonglong * xferTs)
{
  QString xferTagName;
  simtype msg = sim_msg_get_(simId, msgNdx);
  if (sim_get_pointer(msg)) {
    if (const char * s = sim_table_get_pointer(msg, SIM_CMD_MSG_TEXT)) {
      int tag = 0;
      if (!SIM_STRING_CHECK_DIFF_CONST(s, "FILE SEND ")) {
        tag = 1;
      } else if (!SIM_STRING_CHECK_DIFF_CONST(s, "FILE RECV ")) {
        tag = -1;
      } else if (!SIM_STRING_CHECK_DIFF_CONST(s, "FILE REJECT ")) {
        tag = -2;
      } else if (!SIM_STRING_CHECK_DIFF_CONST(s, "FILE CANCEL ")) {
        tag = 2;
      }

      if (tag) {
        if (xferId) {
          *xferId = simnumber((sim_table_get_number(msg, SIM_CMD_MSG_STATUS) == SIM_MSG_INCOMING) == (tag < 0)) << 62;
          *xferId ^= sim_table_get_number(msg, SIM_CMD_MSG_HANDLE);
        }
        if (xferTs) *xferTs = sim_table_get_number(msg, SIM_CMD_MSG_RECEIVED);

        if (tag == 1) {
          QString qs;
          simtype xfer = sim_xfer_get_(simId, msgNdx, SIM_XFER_BIT_EXTENDED);
          int simres = SIM_MSG_BAD_INDEX;
          if (sim_get_pointer(xfer)) {
            qs = sim_table_get_pointer(xfer, SIM_CMD_XFER_SEND_NAME);
            simres = !qs.isEmpty() ? int(sim_table_get_number(xfer, SIM_CMD_XFER_CLOSE_ERROR)) : SIM_XFER_BAD_HANDLE;
            sim_xfer_free_(xfer);
          }
          if (qs.isEmpty()) qs = strchr(s + SIM_STRING_GET_LENGTH_CONST("FILE SEND "), ' ') + 1;
          xferTagName.sprintf("SEND %d ", simres).append(qs);
        } else {
          xferTagName = s += SIM_STRING_GET_LENGTH_CONST("FILE ");
          if (tag != -1) {
            if (const char * t = strchr(s, ' ')) {
              if (t[1] == '0' && t[2] == ' ') {
                xferTagName.sprintf("%s %d %s", tag > 0 ? "CANCEL" : "REJECT", SIM_XFER_CANCELLED, t + 3);
              }
            }
          }
        }
      }
    }
    sim_msg_free_(msg);
  }
  return xferTagName;
}

int Transfers::getTransferState(const char * type, int simres)
{
  if (!strcmp(type, "rcvd")) return simres == SIM_OK ? xfer_finished : xfer_failed;
  if (!strcmp(type, SIM_XFER_TYPE_HOLD)) return xfer_refused;
  if (simres != SIM_OK) return xfer_failing;
  if (!strcmp(type, "sent")) return xfer_finished;
  if (!strcmp(type, SIM_XFER_TYPE_RECV) || !strcmp(type, SIM_XFER_TYPE_SEND)) return xfer_current;
  if (!strcmp(type, SIM_XFER_TYPE_HASH) || !strcmp(type, SIM_XFER_TYPE_DONE)) return xfer_current;
  return strcmp(type, SIM_XFER_TYPE_WIPE) ? xfer_waiting : xfer_erasing;
}

bool Transfers::compareTransfers(const Transfer * left, const Transfer * right)
{
  if ((left->m_ndx > 0) == (right->m_ndx > 0) && (left->m_ndx < 0) == (right->m_ndx < 0) && left != right) {
    int ok = 0;

    for (const char * s = left->getDialog()->m_sortOrder; *s && !ok; ++s) {
      switch (*s) {
        case 'a':
        case 'A':
          ok = QString::localeAwareCompare(left->m_contact->m_nick, right->m_contact->m_nick);
          break;

        case 'b':
        case 'B': {
          int leftState = Transfers::getTransferState(left->m_type, left->m_simres);
          int rightState = Transfers::getTransferState(right->m_type, right->m_simres);
          ok = leftState < rightState ? -1 : leftState > rightState;
        } break;

        case 'c':
        case 'C':
          ok = left->m_fileSize < right->m_fileSize ? -1 : left->m_fileSize > right->m_fileSize;
          break;

        case 'd':
        case 'D':
          if (right->m_rcvdSize >= 0) {
            double leftP = left->m_fileSize > 0 ? double(left->m_rcvdSize) / double(left->m_fileSize) * 100 : 100;
            double rightP = right->m_fileSize > 0 ? double(right->m_rcvdSize) / double(right->m_fileSize) * 100 : 100;
            ok = left->m_rcvdSize < 0 || leftP < rightP ? -1 : leftP > rightP;
          } else {
            ok = left->m_rcvdSize < right->m_rcvdSize ? -1 : left->m_rcvdSize > right->m_rcvdSize;
          }
          break;

        case 'e':
        case 'E':
          ok = left->m_sendTime < right->m_sendTime ? -1 : left->m_sendTime > right->m_sendTime;
          break;

        case 'f':
        case 'F':
          ok = QString::localeAwareCompare(left->m_fileName, right->m_fileName);
      }

      if (*s >= 'A' && *s <= 'Z') ok = -ok;
    }

    if (ok) return ok < 0;
    if (left->m_ndx && right->m_ndx && left->m_contact->m_id != right->m_contact->m_id) {
      return left->m_contact->m_id < right->m_contact->m_id;
    }
  }

  return left->m_ndx < right->m_ndx;
}

void Transfers::countTransfers(qlonglong simId, int type, int * nok)
{
  simtype xfers = sim_xfer_get_(simId, type, SIM_XFER_BIT_DEFAULT);
  simtype types = sim_table_get_array_string(xfers, SIM_CMD_XFER_SEND_TYPE);
  simtype errors = sim_table_get_array_number(xfers, SIM_CMD_XFER_CLOSE_ERROR);

  for (unsigned i = 1; i <= sim_get_length(errors); i++) {
    if (sim_array_get_number(errors, i) == SIM_OK || sim_array_get_number(errors, i) == SIM_XFER_NO_ERROR) {
      Transfer transfer(0, sim_array_get_pointer(types, i));
      bool wipe = !strcmp(transfer.m_type, SIM_XFER_TYPE_WIPE);
      if (wipe || type == SIM_XFER_GET_RECEIVED || transfer.m_ndx >= 0) *nok += *nok < 0 ? -1 : 1;
      if (wipe) *nok = -abs(*nok);
    }
  }

  sim_xfer_free_(xfers);
}

#ifdef _WIN32
static QT_WIN_CALLBACK int changeDirectoryCallback(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
  if (uMsg == BFFM_SELCHANGED) {
    WCHAR buffer[MAX_PATH];
    SendMessage(hwnd, BFFM_ENABLEOK, 0, SHGetPathFromIDList(LPITEMIDLIST(lParam), buffer) && buffer[0]);
  } else if (uMsg == BFFM_INITIALIZED) {
    QString * directory = (QString *)lpData;
    SetWindowText(hwnd, (WCHAR *)Transfers::tr("Change download directory").utf16());
    SendMessage(hwnd, BFFM_SETOKTEXT, FALSE, LPARAM(Transfers::tr("Choose").utf16()));
    if (!directory->isEmpty()) SendMessage(hwnd, BFFM_SETSELECTION, TRUE, LPARAM(directory->utf16()));
  }
  return 0;
}
#endif

void Transfers::changeDirectory(bool init)
{
  QString name;
  simtype info = sim_xfer_get_(0, SIM_XFER_GET_INFO, SIM_XFER_BIT_EXTENDED);
  QString directory = sim_table_get_pointer(info, SIM_XFER_GET_INFO_RECEIVED);
  sim_xfer_free_(info);

  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  int nok = 0;
  for (unsigned j = 0; j < contacts.size(); ++j) {
    if (!contacts[j]->isTest()) countTransfers(contacts[j]->m_simId, SIM_XFER_GET_RECEIVED, &nok);
  }

  QString title = tr("Change download directory");
  const QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
  QString qs = tr("Changing your download directory will cause Simphone to forget all about any files you might"
                  " have received previously.\n\nAre you sure you want to choose a new download directory?");
  if (init) {
    qs = tr("<b>You have started Simphone with no user directory.</b><p>All functions are available in this mode, but "
            "in order to receive files, you will need to choose a download directory.</p>Would you like to do so now?");
  }
  if (nok) {
    if (nok < 0) {
      QString error = tr("\nPlease, restart Simphone before changing the download directory.");
      QMessageBox::warning(Contacts::get(), tr("Simphone warning"), error);
    } else {
      QString error = tr("You have %n incoming file transfer(s) that are currently active.", 0, nok);
      qs = tr("Please, wait for this transfer to finish or cancel it before changing the download directory.", 0, nok);
      SimCore::execMessageBox(false, error, qs);
    }
    return;
  }
  if (init || !directory.isEmpty()) {
    QMessageBox::StandardButton button = init ? QMessageBox::Yes : QMessageBox::No;
    if (QMessageBox::question(Contacts::get(), title, qs, buttons, button) != QMessageBox::Yes) {
      if (init) {
        QString error = tr("<p>Please, push the <b>Change Download Directory</b> button from"
                           " the \"File\" tab of \"Settings\" at any time later.</p>");
        error.append(tr("<span style=\" color:#ff0000;\">You cannot receive files,"
                        " unless you choose a download directory.</span>"));
        SimCore::execMessageBox(false, tr("The download directory was not changed."), error);
      }
      return;
    }
  }

#ifdef _WIN32
  BROWSEINFO bi;
  WCHAR buffer[MAX_PATH];
  title = SimParam::get("ui.main.menu") ? "\n" : "";
  memset(&bi, 0, sizeof(bi));
  if (GetWindowThreadProcessId(HWND(Contacts::get()->winId()), 0)) bi.hwndOwner = HWND(Contacts::get()->winId());
  bi.pszDisplayName = buffer;
  bi.ulFlags = (title.isEmpty() ? BIF_EDITBOX : BIF_USENEWUI) | BIF_RETURNONLYFSDIRS;
  bi.lpszTitle = (WCHAR *)title.append(tr("PLEASE SELECT A DIRECTORY TO STORE RECEIVED FILES INTO:")).utf16();
  bi.lpfn = changeDirectoryCallback;
  bi.lParam = LPARAM(&directory);

  if (LPITEMIDLIST item = SHBrowseForFolderW(&bi)) {
    buffer[0] = 0;
    if (SHGetPathFromIDListW(item, buffer) && buffer[0]) name = QString::fromWCharArray(buffer);

    IMalloc * alloc;
    if (SHGetMalloc(&alloc) == NOERROR) {
      alloc->Free(item);
      alloc->Release();
    }
  }
#else
  QFileDialog dialog(Contacts::get(), title, directory);
  if (!SimParam::get("ui.main.file")) dialog.setOption(QFileDialog::DontUseNativeDialog);
  dialog.setFilter(QDir::Dirs | QDir::Drives | QDir::Hidden | QDir::System);
  dialog.setFileMode(QFileDialog::DirectoryOnly);

  if (dialog.exec() == QDialog::Accepted) name = dialog.selectedFiles().value(0);
#endif

  if (!name.isEmpty()) {
    QString error = tr("Your download directory has been set to:\n%1").arg(name);
    if (name.right(1) != QDir::separator()) name.append(QDir::separator());
    int simres = SimParam::set("xfer.received", name.toUtf8().data(), false);
    if (simres == SIM_OK) {
      Contacts::get()->emitSettingsChanged();
      QMessageBox::information(Contacts::get(), tr("Download directory changed"), error);
    } else {
      error = tr("Changing download directory not successful (%1)");
      SimCore::execMessageBox(true, error.arg(simres), SimCore::getError(simres));
    }
  } else {
    QString error = tr("If you really want to change it, select a valid directory"
                       " and try again by pushing the <b>%1</b> button. ");
    if (init) {
      error.append(tr("<p>To do so, push the <b>Change Download Directory</b> button"
                      " from the \"File\" tab of \"Settings\".</p>"));
    }
    if (directory.isEmpty()) {
      error.append(tr("<span style=\" color:#ff0000;\">You cannot receive files,"
                      " unless you choose a download directory.</span>"));
    }
    SimCore::execMessageBox(false, tr("The download directory was not changed."), error.arg(tr("Choose")));
  }
}

void Transfers::sendFiles(Contact * contact)
{
  std::vector<SimError> errorList;
  QStringList list;
  QString error;
#ifdef _WIN32
  QString title = tr("Send files to %1").arg(contact->m_nick);
  OPENFILENAME ofn;
retry:
  int value = SimParam::get("ui.main.file");
  int size = (abs(value) + 2) * 1024;

  WCHAR * buffer = (WCHAR *)malloc(size);
  if (buffer) buffer[0] = 0;
  memset(&ofn, 0, sizeof(ofn));
  if (GetWindowThreadProcessId(HWND(Contacts::get()->winId()), 0)) ofn.hwndOwner = HWND(Contacts::get()->winId());
  ofn.lStructSize = sizeof(ofn);
  ofn.lpstrFile = buffer;
  ofn.nMaxFile = size / sizeof(*buffer);
  ofn.lpstrTitle = (WCHAR *)title.utf16();
  ofn.Flags = OFN_ALLOWMULTISELECT | OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_LONGNAMES |
              OFN_NOTESTFILECREATE | OFN_HIDEREADONLY | OFN_DONTADDTORECENT | OFN_FORCESHOWHIDDEN | OFN_NOCHANGEDIR;

  SetLastError(0);
  if (buffer && GetOpenFileNameW(&ofn)) {
    WCHAR * name = buffer;
    QString dir = QString::fromWCharArray(name);
    while (*name) {
      name += wcslen(name) + 1;
      if (*name) list << dir + "\\" + QString::fromWCharArray(name);
    }
    if (list.isEmpty()) list = QStringList(dir);
  } else {
    int simres = GetLastError();
    DWORD err = buffer ? CommDlgExtendedError() : CDERR_MEMALLOCFAILURE;
    if (err == FNERR_BUFFERTOOSMALL && value < 0 && value >= SimParam::getMinValue("ui.main.file")) {
      SimParam::set("ui.main.file", value * 2, false);
      free(buffer);
      error = tr("\nYou have chosen more than <b>%1 kilobytes</b> of file names. This is no problem, but you have to"
                 " select the same files and try again. The name buffer has now been enlarged. Depending on the number"
                 " of files, you may need to repeat the same operation a few times, until this message disappears.");
      QMessageBox::information(Contacts::get(), tr("No files to send could be chosen"), error.arg(size / 1024));
      goto retry;
    }
    error = tr("Sending of files not successful (%1)").arg(error.sprintf("0x%04lX", err));
    if (err) SimCore::execMessageBox(true, error, tr("No files to send could be chosen (error %1)").arg(simres));
  }

  free(buffer);
#else
  QFileDialog dialog(Contacts::get(), tr("Send files to %1").arg(contact->m_nick));

#ifndef DONOT_DEFINE
  QByteArray directory(SimParam::getString("ui.xfer.directory"));
  if (!directory.isEmpty()) dialog.setDirectory(directory);
#endif
  QByteArray geometry(SimParam::getString("ui.xfer.geometry"));
  if (!geometry.isEmpty()) dialog.restoreGeometry(geometry);
  QByteArray state(SimParam::getString("ui.xfer.state"));
  if (!state.isEmpty()) dialog.restoreState(state);

  if (!SimParam::get("ui.main.file")) dialog.setOption(QFileDialog::DontUseNativeDialog);
  dialog.setOption(QFileDialog::ReadOnly);
  dialog.setFileMode(QFileDialog::ExistingFiles);
  dialog.setFilter(QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDot);

  QDir prevDirectory(dialog.directory());
  if (dialog.exec() == QDialog::Accepted) {
    simtype params = sim_table_new(3);
    QByteArray savedGeometry = dialog.saveGeometry();
    QByteArray savedState = dialog.saveState();
#ifndef DONOT_DEFINE
    QByteArray savedDirectory = dialog.directory().canonicalPath().toUtf8();
    sim_table_add_pointer_length(params, "ui.xfer.directory", savedDirectory.data(), savedDirectory.size());
#endif
    sim_table_add_pointer_length(params, "ui.xfer.geometry", savedGeometry.data(), savedGeometry.size());
    sim_table_add_pointer_length(params, "ui.xfer.state", savedState.data(), savedState.size());
    SimParam::set(params);
    sim_table_free(params);
    list = dialog.selectedFiles();
  } else {
    dialog.setDirectory(prevDirectory);
  }
#endif

  qtfix::setOverrideCursor(true);
  for (int i = 0; i < list.size(); ++i) {
    simnumber xferId;
    int simres = sim_xfer_send_file_(contact->m_simId, sim_pointer_new(list[i].toUtf8().data()), &xferId);
    error = xferId ? QString() : qtfix::getString(QFileInfo(list[i]).fileName(), "[\\000-\\037]");
    if (simres != SIM_OK) errorList.push_back(SimError(error, SimCore::getError(simres), simres));
    if (xferId) SimCore::get()->emitTransferStateChanged(contact->m_id, xferId, SIM_XFER_TYPE_INIT);
  }
  log_debug_("ui", "elapsed FILE %lld ms\n", qtfix::setOverrideCursor(false));

  if (errorList.size() == 1) {
    if (!errorList[0].m_name.isEmpty()) {
      error = tr("Sending of file \"%1\" not successful (%2)").arg(errorList[0].m_name).arg(errorList[0].m_simres);
    } else {
      error = tr("Sending of file not successful (%1)").arg(errorList[0].m_simres);
    }
    SimCore::execMessageBox(false, error, errorList[0].m_simerr);
  } else if (!errorList.empty()) {
    showErrors(errorList, tr("Sending %1 out of %2 files not successful").arg(errorList.size()).arg(list.size()));
  }
}

void Transfers::showErrors(const std::vector<SimError> & errorList, QString error)
{
  int simres = errorList[0].m_simres;
  for (unsigned i = 1; i < errorList.size() && simres != SIM_OK; ++i) {
    if (errorList[i].m_simres != simres) simres = SIM_OK;
  }
  if (simres != SIM_OK) error.append(" (").append(QString::number(simres)).append(")");
  error = "<b>" + error.toHtmlEscaped().replace(" ", "&nbsp;") + "</b>";
  if (simres != SIM_OK) {
    error.append("<br/><br/>").append(errorList[0].m_simerr.toHtmlEscaped().replace(" ", "&nbsp;"));
  }

  QMessageBox mbox(QMessageBox::Warning, tr("Simphone warning"), error, QMessageBox::Ok, Contacts::get());
  error = QString();
  for (unsigned i = 0; i < errorList.size(); ++i) {
    if (!errorList[i].m_name.isEmpty()) {
      error.append(errorList[i].m_name);
      if (simres == SIM_OK) {
        error.append(" (").append(QString::number(errorList[i].m_simres)).append("): ").append(errorList[i].m_simerr);
      }
      error.append("\n");
    }
  }
  if (!error.isEmpty()) mbox.setDetailedText(error);
  mbox.exec();
}
