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

    class ChatFrames (QObject): group and switch between multiple chat frames
    class ChatFrame (QFrame): display ChatTableView together with input elements
    class ChatTableView (QTableView): display chat and console
    class DateDelegate (QStyledItemDelegate): paint date line and message time in chat frame

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

#include "chatframe.h"
#include "ui_chatframe.h"

#include "qtfix.h"
#include "chatwindow.h"
#include "contacts.h"
#include "transfers.h"

#include <QClipboard>
#include <QPainter>
#include <QScrollBar>
#include <QMenu>

#define SCROLL_TO_BOTTOM_SAFE_COUNTER 10

#ifdef __APPLE__
#define CtrlModifier MetaModifier
#else
#define CtrlModifier ControlModifier
#endif
#define AllModifiers (Qt::MetaModifier | Qt::ControlModifier | Qt::AltModifier)

void DateDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
  QStyleOptionViewItem fixOption = option;
  fixOption.features &= ~QStyleOptionViewItem::Alternate;

  if (index.column() == ((const BaseModel *)index.model())->getDataCol()) {
    if ((option.state & QStyle::State_Selected) && ((const BaseModel *)index.model())->doNotShowSelected(index.row())) {
      fixOption.state &= ~QStyle::State_Selected;
    }
  }

  Parent::paint(painter, fixOption, index);
  QColor background;
  QColor foreground;
  bool bold;
  char newDate = ((const BaseModel *)index.model())->isNewDate(&background, &foreground, &bold);

  if (index.column() == ((const BaseModel *)index.model())->getTimeCol()) {
    QPen pen(Qt::SolidLine);
    QColor color;
    char edited = ((const MessagesModel *)index.model())->isEdited(&color);
    if (option.state & QStyle::State_Selected) {
      QPalette::ColorGroup group = option.state & QStyle::State_Active ? QPalette::Normal : QPalette::Inactive;
      painter->setPen(pen = option.palette.color(group, QPalette::HighlightedText));
    } else if (color.isValid()) {
      painter->setPen(color);
    } else {
      painter->setPen(pen);
    }
    if (edited) {
      int width = QFontMetrics(option.font).width(" 00:00");
      QFont font = painter->font();
      QFont boldFont = font;
      if (color.isValid()) boldFont.setBold(true);
      painter->setFont(boldFont);
      painter->drawText(option.rect.x() + width, option.rect.y(), option.rect.width() - width, option.rect.height(),
                        Qt::AlignLeft | Qt::AlignTop, newDate ? "\n" + QString(edited) : QString(edited));
      painter->setFont(font);
    }
    painter->setPen(pen);
    painter->drawText(option.rect.x(), option.rect.y(), option.rect.width(), option.rect.height(),
                      Qt::AlignLeft | Qt::AlignTop,
                      ((const MessagesModel *)index.model())->getMessageTime(index.row()));
  }

  if (newDate) {
    int h = QFontMetrics(option.font).height();
    QBrush brush = QBrush(background);
    if (!background.isValid()) {
      brush = option.palette.shadow().color().value() ? option.palette.shadow() : option.palette.dark();
    }
    painter->fillRect(option.rect.x(), option.rect.y(), option.rect.width(), h, brush);
    if (index.column() == ((const BaseModel *)index.model())->getDataCol()) {
      QFont font;
      if (bold) {
        QFont boldFont = font = painter->font();
        boldFont.setBold(true);
        painter->setFont(boldFont);
      }
      if (!foreground.isValid()) {
        foreground = brush.color().value() ? option.palette.highlightedText().color() : QColor(Qt::white);
      }
      painter->setPen(foreground);
      const char * date = ((const BaseModel *)index.model())->getMsgDate(index.row());
      QScrollBar * scrollBar = m_parent->verticalScrollBar();
      int w = m_parent->width() - (scrollBar && scrollBar->isVisible() ? scrollBar->width() : 0);
      QRect rect = painter->boundingRect(0, option.rect.y(), w, h, Qt::AlignCenter | Qt::AlignTop, date);
      if (rect.x() >= option.rect.x() && rect.x() + rect.width() < option.rect.x() + option.rect.width()) {
        int x = (w - rect.width()) / 2;
        painter->drawText(x, option.rect.y(), rect.width(), h, Qt::AlignHCenter | Qt::AlignTop, date);
      } else {
        painter->drawText(option.rect.x(), option.rect.y(), option.rect.width(), h, Qt::AlignLeft | Qt::AlignTop, date);
      }
      if (bold) painter->setFont(font);
    }
  }
}

void ChatTableView::resizeEvent(QResizeEvent * event)
{
  bool scroll = false;
  if (m_scrollToBottomOnResize && event) {
    int last = event->oldSize().height() <= 0 ? -1 : rowAt(event->oldSize().height() - 2);
    scroll = (last == -1) || (last >= model()->rowCount() - 1);
    if (event->oldSize().height() < event->size().height()) {
      // when oldSize is smaller than new one, then we may need to recalculate
      resizeRowContentVisiblePart(true); // maybe no longer necessary ???
    }
  }

  if (event) Parent::resizeEvent(event);
  if (!m_timeColSize) m_timeColSize = fontMetrics().width(m_testUser ? " 00:00:00  " : " 00:00*  ");

  setColumnWidth(0, m_nickColSize);
  setColumnWidth(1, viewport()->width() - m_nickColSize - m_timeColSize);
  setColumnWidth(2, m_timeColSize);

  resizeRowContentVisiblePart();

  if (scroll) scrollToBottom(); //QTimer::singleShot(0, this, SLOT(scrollToBottomSafe()));
}

void ChatTableView::scrollContentsBy(int dx, int dy)
{
  Parent::scrollContentsBy(dx, dy);
  resizeRowContentVisiblePart();
}

bool ChatTableView::calcNickSize(const QString & nick)
{
  unsigned width = fontMetrics().width(nick) + fontMetrics().width(' ') * 3; //8;
  bool result = width > m_nickColSize;
  if (result) m_nickColSize = width;
  return result;
}

void ChatTableView::resizeRowContentVisiblePart(bool last256)
{
  QRect r = contentsRect();
  int first = rowAt(r.top());
  int last = rowAt(r.bottom() - 2);

  if (last256) {
    last = model()->rowCount() - 1;
    first = last - 256;
    if (last < 0) return;
    if (first < 0) first = 0;
  }

  if (first >= 0) {
    if (!last256 && (last - first) < 250) last = first + 250;
    if (last < 0 || last >= model()->rowCount()) last = model()->rowCount() - 1;
    for (int i = last; i >= first; --i) {
      resizeRowToContents(i);
    }
  }
}

void ChatTableView::scrollToBottomSafe()
{
  updateGeometries();
  scrollTo(model()->index(model()->rowCount() - 1, 1), QAbstractItemView::PositionAtBottom);
#if SCROLL_TO_BOTTOM_SAFE_COUNTER
  int val = verticalScrollBar()->value();
  if (val < verticalScrollBar()->maximum()) {
    if (m_scrollToBottomSafeCounter != SCROLL_TO_BOTTOM_SAFE_COUNTER) {
      log_debug_("ui", "scroll loop (count = %d)\n", SCROLL_TO_BOTTOM_SAFE_COUNTER - m_scrollToBottomSafeCounter);
    }
    if (m_scrollToBottomSafeCounter) {
      --m_scrollToBottomSafeCounter;
      QTimer::singleShot(0, this, SLOT(scrollToBottomSafe())); // call updateGeometries()
    }
  }
#endif
}

void ChatTableView::scrollToBottom()
{
#if SCROLL_TO_BOTTOM_SAFE_COUNTER
  m_scrollToBottomSafeCounter = SCROLL_TO_BOTTOM_SAFE_COUNTER; // prevent infinite loop
#endif
  scrollToBottomSafe();
}

void ChatTableView::updateGeometries()
{
  Parent::updateGeometries();

  int step = verticalScrollBar()->pageStep() - verticalScrollBar()->singleStep();
  if (step > 0) verticalScrollBar()->setPageStep(step);
}

QPixmap * ChatFrame::mc_connectionStatePixmaps[SimCore::state_NConnectionStates];

ChatFrame::ChatFrame(Contact * contact, BaseModel * chatModel)
  : ui(new Ui::ChatFrame)
  , m_contact(contact)
  , m_chatModel(chatModel)
  , m_lastRowClicked(-1)
  , m_startFindFrom(-1)
  , m_editIndex(0)
  , m_shown(false)
  , m_verifyDialog(0)
  , m_commandIndex(0)
  , m_autoScroll(true)
  , m_disableBottomScroll(false)
  , m_focusFind(false)
  , m_someLoaded(false)
  , m_allLoaded(false)
  , m_noneLoaded(false)
  , m_typing(false)
  , m_showMinimized(false)
  , m_connected(false)
  , m_splitterInitialized(false)
  , m_timer(this)
{
  log_info_("ui", "create chat frame '%s'\n", contact ? contact->m_nick.toUtf8().data() : "?");
  ui->setupUi(this);
  ui->chatView->setModel(m_chatModel);
  qtfix::fixTableView(ui->chatView);

  setFont(qtfix::fixFontSize(font(), "QAbstractItemView"));
  qtfix::fixPushButtonIcon(ui->sendButton, font());
  qtfix::fixPushButtonIcon(ui->callButton, font());
  qtfix::fixPushButtonIcon(ui->hangupButton, font());
  qtfix::fixSplitterHandle(ui->splitter, font(), 'e');

  QFontMetrics fm(ui->textEdit->font());
  if (m_contact->isTest()) {
    ui->chatView->setTest(true);
    ui->chatView->calcNickSize("DEBUG"); // set minimal width of debug level column
    ui->textEdit->setCenterOnScroll(true);
    ui->textEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
    ui->sendButton->setVisible(false);
    m_callEnabled = sim_sound_start_(0, 0) == SIM_OK;
    m_splitterInitialized = true;
  }
  ui->textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  ui->textEdit->setMinimumHeight(fm.ascent() * 2);
  //ui->editFrame->setMinimumHeight(fm.ascent() * 2);

  QList<int> sizes;
  int val;
  QString ui_number = m_contact->getTextId();
  QString ui_key;
  if (SimParam::getNumber((ui_key = "ui.").append(ui_number).append(".top").toUtf8().data(), &val) && val > 0) {
    sizes.append(val);
    if (SimParam::getNumber((ui_key = "ui.").append(ui_number).append(".bottom").toUtf8().data(), &val) && val > 0) {
      sizes.append(val);
      ui->splitter->setSizes(sizes);
      m_splitterInitialized = true;
    }
  }

  connect(m_chatModel,
          SIGNAL(layoutChanged(const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint)),
          this, SLOT(onLayoutChanged(const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint)));
  connect(m_chatModel, SIGNAL(headerDataChanged(Qt::Orientation, int, int)),
          this, SLOT(onHeaderChanged(Qt::Orientation, int, int)));
  connect(m_chatModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
          this, SLOT(onRowsInserted(const QModelIndex &, int, int)));
  connect(ui->chatView->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onScrollChanged()));
  connect(ui->chatView->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), this, SLOT(onScrollChanged()));
  connect(ui->chatView, SIGNAL(doubleClicked(const QModelIndex &)),
          this, SLOT(onRowDoubleClicked(const QModelIndex &)));
  connect(ui->chatView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(onRowClicked(const QModelIndex &)));
  connect(SimCore::get(), SIGNAL(signalMessageReceived(unsigned, int, bool)),
          this, SLOT(onSignalMessageReceived(unsigned, int, bool)));
  connect(SimCore::get(), SIGNAL(signalMessageEdited(unsigned, int)), this, SLOT(onSignalMessageEdited(unsigned, int)));
  connect(SimCore::get(), SIGNAL(signalContactAudioChanged(unsigned, SimAudioState)),
          this, SLOT(onSignalContactAudioChanged(unsigned, SimAudioState)));
  connect(SimCore::get(), SIGNAL(signalContactChanged(unsigned)), this, SLOT(onSignalContactChanged(unsigned)));
  connect(SimCore::get(), SIGNAL(signalContactConnectionChanged(unsigned, int)),
          this, SLOT(onSignalContactConnectionChanged(unsigned, int)));
  connect(m_chatModel, SIGNAL(signalFoundUnseenNick(const QString &)),
          this, SLOT(onSignalFoundUnseenNick(const QString &)));
  connect(&m_timer, SIGNAL(timeout()), this, SLOT(onTypingTimeout()));

  if (!m_contact->isTest()) {
    m_timer.setSingleShot(true);
    int timeout = abs(SimParam::get("rights.type")) * 1000; // in milliseconds
    if (!timeout) timeout = 500;
    m_timer.setInterval(timeout);
  } else {
    connect(ui->textEdit, SIGNAL(textChanged()), this, SLOT(onTextChanged()));
  }

  QFont boldFont = ui->verifyButton->font();
  boldFont.setBold(true); //boldFont.setWeight(75);
  ui->verifyButton->setFont(boldFont);
  QPalette pal = ui->verifyButton->palette();
  pal.setColor(QPalette::Button, pal.color(QPalette::Active, QPalette::AlternateBase));
  ui->verifyButton->setPalette(pal);

  ui->chatView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  ui->chatView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
  ui->chatView->setItemDelegate(m_dateDelegate = new DateDelegate(ui->chatView));

  ui->findFrame->setVisible(false);
  ui->editLabel->setVisible(false);
  installFilter(this);

  QByteArray edit(SimParam::getString((ui_key = "ui.").append(ui_number).append(".utf").toUtf8().data()));
  if (!edit.isEmpty()) setEditText(edit);

  if (m_contact->isTest() && SimParam::get("ui.console.unload") < 0) {
    int simres = loadMessages(SimParam::get("file.log") ? SimParam::get("ui.console.load") : LOG_NO_LOAD);
    if (simres == SIM_FILE_START) {
      m_allLoaded = true;
    }
    m_someLoaded = true;
    Logs::newLog();
  }

  if (!mc_connectionStatePixmaps[SimCore::state_connected]) {
    for (int i = 0; i < SimCore::state_NConnectionStates; i++) {
      QPixmap * pixmap = mc_connectionStatePixmaps[i] = new QPixmap(":/connected" + QString::number(i));
      *mc_connectionStatePixmaps[i] = qtfix::fixPixmapSize(*pixmap, fontMetrics().lineSpacing(), false);
    }
  }

  connect(ui->chatView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(const QPoint &)));
  connect(ui->sendButton, SIGNAL(customContextMenuRequested(QPoint)),
          this, SLOT(onCustomContextMenuSend(const QPoint &)));
  connect(ui->infoLineLabel, SIGNAL(customContextMenuRequested(QPoint)),
          this, SLOT(onCustomContextMenuInfo(const QPoint &)));
  connect(ui->connectionLabel, SIGNAL(customContextMenuRequested(QPoint)),
          this, SLOT(onCustomContextMenuConnection(const QPoint &)));
}

ChatFrame::~ChatFrame()
{
  removeFilter(this);
  m_contact->m_chatFrame = 0;

  delete m_chatModel;
  delete m_dateDelegate;
  delete ui;
  log_info_("ui", "delete chat frame '%s'\n", m_contact->m_nick.toUtf8().data());
}

void ChatFrame::getSettings(simtype params) const
{
  if (!m_contact->isForgotten()) {
    QList<int> sizes = ui->splitter->sizes();
    QString text = ui->textEdit->toPlainText();

    if (!m_contact->isTest()) {
      int size;
      int splitLimit = SimParam::get("ui.chat.split");
      splitLimit = ((!splitLimit ? SimParam::getDefault("ui'chat.split", 0) : splitLimit) + 1) * 1000;

      text = text.left(splitLimit);
      while ((size = text.toUtf8().size() - splitLimit) > 0) text.chop(size);
    }

    QByteArray edit = text.toUtf8();
    QString ui_key;
    QString ui_number = m_contact->getTextId();
    bool deleted = m_contact->isDeleted() && !m_contact->isTest() && !m_contact->isSystem() && !m_contact->isMe();

    if (!deleted || edit.size()) {
      simtype val = sim_string_copy_length(edit.data(), edit.size());
      (ui_key = "ui.").append(ui_number).append(".utf");
      sim_table_add_key(params, sim_string_copy(ui_key.toUtf8().data()), val);
    }

    if (!deleted) {
      (ui_key = "ui.").append(ui_number).append(".top");
      sim_table_add_key_number(params, sim_string_copy(ui_key.toUtf8().data()), sizes[0]);
      (ui_key = "ui.").append(ui_number).append(".bottom");
      sim_table_add_key_number(params, sim_string_copy(ui_key.toUtf8().data()), sizes[1]);
    }
  }
}

void ChatFrame::readSettings()
{
  QString qs;
  if (m_contact->isTest()) {
    m_altArrow = false;
    m_ctrlEnter = -1;
    m_escape = SimParam::get("ui.console.escape");
    m_copyNewLine = SimParam::get("ui.console.copynewline");
    m_copyText = SimParam::get("ui.console.copytext") != 0;
    m_loadMessages = SimParam::get("ui.console.load");
    m_showConnection = SimParam::get("ui.console.connection") != 0;
    m_showAll = SimParam::get("ui.console.showall") != 0;
    ui->chatView->verticalScrollBar()->setTracking(SimParam::get("ui.console.tracking") != 0);
    ui->chatView->setShowGrid(SimParam::get("ui.console.grid") != 0);
    m_notfoundBrush = SimParam::getColor("ui.console.notfound");
    m_notfoundColor = SimParam::getColor("ui.console.notfoundtext");
    qs = SimParam::getString("ui.console.button");
  } else {
    m_altArrow = SimParam::get("ui.chat.altarrow") != 0;
    m_ctrlEnter = SimParam::get("ui.chat.ctrlenter") != 0;
    m_escape = SimParam::get("ui.chat.escape");
    m_copyNewLine = SimParam::get("ui.chat.copynewline");
    m_copyText = SimParam::get("ui.chat.copytext") != 0;
    m_loadMessages = SimParam::get("ui.chat.load");
    m_showConnection = SimParam::get("ui.chat.connection") != 0;
    m_showAll = SimParam::get("ui.chat.showall") != 0;
    ui->chatView->verticalScrollBar()->setTracking(SimParam::get("ui.chat.tracking") != 0);
    ui->chatView->setShowGrid(SimParam::get("ui.chat.grid") != 0);
    m_notfoundBrush = SimParam::getColor("ui.chat.notfound");
    m_notfoundColor = SimParam::getColor("ui.chat.notfoundtext");
    qs = SimParam::getString("ui.chat.button");
  }

  qs.sprintf("\nQPushButton { font-weight: bold; color: %s; }", qs.toUtf8().data());
  ui->scrollButton->setStyleSheet(qApp->styleSheet() + qs);
  ui->scrollButton->setVisible(!m_autoScroll);
  ui->connectionLabel->setVisible(m_showConnection);
  setAutoScrollToolTip();
  m_showInfoLine = m_contact->isTest() && m_contact->m_info.isEmpty() ? false : SimParam::get("ui.main.infoline") & 1;
  m_chatModel->readSettings();
  onSignalContactConnectionChanged(m_contact->m_id, 'i');
  onSignalContactChanged(m_contact->m_id);
}

void ChatFrame::installFilter(QObject * obj)
{
  ui->textEdit->installEventFilter(obj);
  ui->findEdit->installEventFilter(obj);
  installEventFilter(obj);
}

void ChatFrame::removeFilter(QObject * obj)
{
  ui->textEdit->removeEventFilter(obj);
  ui->findEdit->removeEventFilter(obj);
  removeEventFilter(obj);
}

bool ChatFrame::isFocused() const
{
  return Contacts::get()->isApplicationActive() && !isHidden() && !window()->isMinimized() &&
         (hasFocus() || ui->textEdit->hasFocus() || ui->findEdit->hasFocus()); // use isActiveWindow() instead?
}

void ChatFrame::setAutoScrollToolTip()
{
  switch (m_ctrlEnter) {
    case 0: ui->scrollButton->setToolTip(tr("Press Enter to show the last message.")); break;
    case 1: ui->scrollButton->setToolTip(tr("Press Ctrl+Enter to show the last message.")); break;
    default: ui->scrollButton->setToolTip(tr("Press Enter or Ctrl+Enter to show the last message."));
  }
}

void ChatFrame::setAutoScroll(bool checked)
{
  m_autoScroll = checked;
  //scroll();
  scroll();
  ui->chatView->m_scrollToBottomOnResize = hasAutoScroll();
  ui->scrollButton->setVisible(!checked);
}

void ChatFrame::scroll()
{
  if (m_autoScroll) ui->chatView->scrollToBottom();
}

void ChatFrame::enableChat(bool enabled)
{
  bool wasEnabled = ui->textEdit->isEnabled();
  ui->textEdit->setEnabled(m_contact->isTest() ? sim_console_exec__(0) != SIM_OK : enabled);
  if (ui->textEdit->isEnabled()) {
    if (!wasEnabled && !ui->textEdit->hasFocus() && (!m_focusFind || !ui->findFrame->isVisible())) {
      ui->textEdit->setFocus();
    }
  } else if (m_editIndex) {
    editFinish();
  }
}

void ChatFrame::enableCall(bool enabled)
{
  ui->callButton->setEnabled(m_contact->isTest() ? m_callEnabled : enabled);
}

void ChatFrame::setVerified(bool verified)
{
  if (verified && !m_contact->isVerified()) {
    m_contact->setVerified();
    SimCore::get()->emitContactChanged(m_contact->m_id);
  }
}

void ChatFrame::showAuthText()
{
  QString qs = m_contact->getAuthText();

  ui->authLabel->setVisible(!qs.isEmpty());
  if (!qs.isEmpty()) {
    qs = "<span style=\" color:#ff0000;\"><b> " + qs + " </b></span>";
    ui->authLabel->setText(qs);
  }
}

void ChatFrame::setEditText(const QString & text)
{
  ui->textEdit->setPlainText(text);
  QTextCursor cursor = ui->textEdit->textCursor();

  cursor.movePosition(QTextCursor::End);
  ui->textEdit->setTextCursor(cursor);
}

void ChatFrame::sendEditText()
{
  int simres = SIM_OK;
  QString qs = ui->textEdit->toPlainText();
  int n = qs.size();
  simunsigned ndx = 0;

  if (!n && !m_editIndex) return; // do not send empty string

  while (n && (qs[n - 1] == ' ' || qs[n - 1] == '\n' || qs[n - 1] == '\t')) --n; // remove trailing spaces and new lines
  qs.remove(n, qs.size());

  if (!m_contact->isTest()) {
    bool edit;

    if (!m_editIndex) {
      int splitLimit = SimParam::get("ui.chat.split");

      if (splitLimit) {
        simres = SIM_MSG_BAD_LENGTH;
        if (qs.toUtf8().size() <= splitLimit * 1000) {
          qtfix::setOverrideCursor(true);
          QStringList list = qs.split('\n');

          simres = SIM_OK;
          for (int i = 0; i < list.size(); ++i) {
            simres = sim_msg_send_utf_(m_contact->m_simId, sim_pointer_new(list[i].toUtf8().data()), 0);
            if (simres != SIM_OK) break;
          }

          if (simres == SIM_OK) {
            for (int i = 0; i < list.size(); ++i) {
              int simres2 = sim_msg_send_utf_(m_contact->m_simId, sim_string_copy(list[i].toUtf8().data()), &ndx);
              if (simres2 != SIM_OK) simres = simres2;
              if (!ndx) {
                if (i) ui->textEdit->setPlainText(QStringList(list.mid(i)).join('\n'));
                break;
              }
            }
          }
          log_debug_("ui", "elapsed MSG %lld ms\n", qtfix::setOverrideCursor(false));
        }
      } else {
        simres = sim_msg_send_utf_(m_contact->m_simId, sim_string_copy(qs.toUtf8().data()), &ndx);
      }
      edit = false;
    } else {
      edit = true;
      if (m_original != qs) {
        simtype msg = sim_string_copy(qs.toUtf8().data());
        simres = sim_msg_edit_utf_(m_contact->m_simId, msg, m_chatModel->getEditIndex() + 1);
        if (m_chatModel->getEditText(&m_editIndex, 0) == qs) ndx = 1;
      } else {
        simres = SIM_OK; // no change, so do not send
      }
      m_chatModel->clearCache();
      editFinish();
      ui->chatView->resizeRowContentVisiblePart();
      scroll();
    }
    m_chatModel->notifyRowsChanged();
    if (simres != SIM_OK) {
      QString error;
      error = edit ? tr("Editing chat message not successful (%1)") : tr("Sending chat message not successful (%1)");
      qtfix::execMessageBox(false, error.arg(simres), SimCore::getError(simres), this);
    }
    onTypingTimeout();
  } else if (!qs.isEmpty()) {
    if (!SimParam::get("ui.console.commands")) {
      QCheckBox cbox(tr("Do not ask me about this ever again."));
      if (!qtfix::execMessageBox(&cbox, tr("Simphone console"),
                                 tr("Typing commands to the Simphone console allows advanced users to enable"
                                    " functions not otherwise available with the GUI.<br/><br/><b>It also allows"
                                    " you to test, bearing the danger to crash, compromise security or permanently"
                                    " disable Simphone.</b><br/><br/>Are you <span style=\" color:#ff0000;\">"
                                    "absolutely</span> sure you want to proceed with that?<br/><br/>"),
                                 this,
                                 false)) {
        return;
      }
      SimParam::set("ui.console.commands", 1, !cbox.isChecked());
    }

    qtfix::setOverrideCursor(true);
    QStringList list(qs);

    if (SimParam::get("ui.console.split")) list = qs.split('\n');

    for (int i = 0; i < list.size(); ++i) {
      qs = list[i];

      if (qs.startsWith('*')) qs.remove(0, 1);
      log_xtra_(0, "> %s\n", qs.toUtf8().data());
      for (n = 0; n < qs.size() && qs[n] == ' '; n++) {}

      simres = SIM_CONSOLE_BAD_CMD;
      if (SIM_STRING_CHECK_DIFF_CONST(qs.toUtf8().data() + n, "init")) {
        if (SIM_STRING_CHECK_DIFF_CONST(qs.toUtf8().data() + n, "uninit")) {
          simres = sim_console_exec__(qs.toUtf8().data());
        }
      }

      if (simres == SIM_CONSOLE_BAD_CMD) {
        log_debug_(0, "%s: %s\n", sim_error_get(simres), qs.toUtf8().data());
      } else if (simres == SIM_CONSOLE_QUIT_CMD) {
        emit signalContactQuit();
        hide();
      }

      if (simres != SIM_OK) qs.insert(0, "*");
      m_lastCommands.append(qs.toUtf8().data());
      m_commandIndex = m_lastCommands.size();
      if (m_commandIndex > 1) {
        if (m_lastCommands.at(m_commandIndex - 1) == m_lastCommands.at(unsigned(m_commandIndex) - 2)) {
          m_lastCommands.removeAt(--m_commandIndex);
        }
      }
    }
    log_xtra_("ui", "elapsed CMD %lld ms\n", qtfix::setOverrideCursor(false));
    simres = SIM_OK;
  }
  if (simres == SIM_OK || ndx) ui->textEdit->clear();
}

bool ChatFrame::eventFilter(QObject * obj, QEvent * event)
{
  QEvent::Type type = event->type();
  if (type != QEvent::KeyPress) {
    if (type == QEvent::FocusIn) {
      if (obj == ui->findEdit) {
        m_focusFind = true;
        ui->scrollButton->setToolTip("");
      } else if (obj == ui->textEdit) {
        m_focusFind = false;
        setAutoScrollToolTip();
      }
    }

#ifdef DONOT_DEFINE
    if (type == QEvent::InputMethod) {
      QInputMethodEvent * input = static_cast<QInputMethodEvent *>(event);
      QByteArray bytes = input->commitString().toUtf8();
      simtype ptr = sim_pointer_new_length(bytes.data(), bytes.size());
      log_simtype_("ui", SIM_LOG_XTRA, ptr, LOG_BIT_BIN, "input ");
    }
#endif
    if (type == QEvent::FocusOut && obj == ui->textEdit) onTypingTimeout();
    return Parent::eventFilter(obj, event);
  }
  QKeyEvent * keyEvent = static_cast<QKeyEvent *>(event);
  Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
  int key = keyEvent->key();

  QString text;
  if (obj == ui->findEdit) {
    text = ui->findEdit->text();
  } else if (obj == ui->textEdit) {
    text = ui->textEdit->toPlainText();
  }

  if (key == Qt::Key_Home || key == Qt::Key_End) {
    if (modifiers & (Qt::AltModifier | Qt::MetaModifier) || text.isEmpty()) {
      if (key == Qt::Key_Home) {
        ui->chatView->verticalScrollBar()->setValue(ui->chatView->verticalScrollBar()->minimum());
      } else {
        ui->chatView->verticalScrollBar()->setValue(ui->chatView->verticalScrollBar()->maximum());
      }
      return true;
    }
  }

  if (key == Qt::Key_PageUp || key == Qt::Key_PageDown) {
    if (obj == ui->findEdit || modifiers & (Qt::AltModifier | Qt::MetaModifier) || text.isEmpty()) {
      if (key == Qt::Key_PageUp) {
        ui->chatView->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
      } else {
        ui->chatView->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
      }
      return true;
    }
  }

  if (qtfix::hasAltModifier(keyEvent, Qt::AltModifier) && modifiers & Qt::CtrlModifier) {
    switch (key) {
      case Qt::Key_V: on_verifyButton_clicked(); return true;
      case Qt::Key_C: on_callButton_clicked(); return true;
      case Qt::Key_H: on_hangupButton_clicked(); return true;
      case Qt::Key_S: on_sendButton_clicked(); return true;
    }
  }

  if (keyEvent->matches(QKeySequence::Copy) || (key == Qt::Key_C && qtfix::hasAltModifier(keyEvent, AllModifiers))) {
    if (modifiers & Qt::AltModifier || obj != ui->findEdit || !ui->findEdit->hasSelectedText()) {
      if (modifiers & Qt::AltModifier || obj != ui->textEdit || !ui->textEdit->textCursor().hasSelection()) {
        copySelection(key == Qt::Key_C && modifiers & Qt::ShiftModifier ? !m_copyText : m_copyText);
        return true;
      }
    }
  } else if (keyEvent->matches(QKeySequence::Find) ||
             (qtfix::hasAltModifier(keyEvent, AllModifiers) && (key == Qt::Key_F || key == Qt::Key_Question))) {
    if (ui->findFrame->isVisible()) {
      on_findCloseButton_clicked();
    } else {
      findOpen(-1);
      if (!m_editIndex) scroll();
    }
    return true;
  } else if (obj == ui->findEdit) {
    if (key == Qt::Key_Escape && m_escape) {
      if (m_escape == 1 || (m_escape == 3 && ui->findEdit->text().size())) {
        ui->findEdit->clear();
      } else {
        on_findCloseButton_clicked();
      }
      return true;
    }

    if (keyEvent->matches(QKeySequence::FindNext) ||
        ((key == Qt::Key_Return || key == Qt::Key_Enter) && modifiers & (AllModifiers | Qt::ShiftModifier))) {
      on_findNextButton_clicked();
      return true;
    }

    if (keyEvent->matches(QKeySequence::FindPrevious)) {
      on_findPreviousButton_clicked();
      return true;
    }

    if (modifiers & (Qt::AltModifier | Qt::MetaModifier)) {
      switch (key) {
        case Qt::Key_Up:
        case Qt::Key_Left:
          findMessage(-1);
          return true;

        case Qt::Key_Down:
        case Qt::Key_Right:
          findMessage(1);
          return true;
      }
    } else if (!m_altArrow) {
      switch (key) {
        case Qt::Key_Up:
          findMessage(-1);
          return true;

        case Qt::Key_Down:
          findMessage(1);
          return true;
      }
    }

    if (!(modifiers & AllModifiers) && key == Qt::Key_Backtab) {
      focusNextChild();
      return true;
    }
  } else if (obj == ui->textEdit && ui->textEdit->isEnabled()) {
    if (!(modifiers & AllModifiers) && key == Qt::Key_Tab) {
      focusPreviousChild();
      return true;
    }

    if (!(modifiers & (Qt::AltModifier | (m_ctrlEnter >= 0 ? Qt::ShiftModifier : Qt::AltModifier)))) {
      if ((m_ctrlEnter <= 0 || modifiers & Qt::CtrlModifier) && (key == Qt::Key_Return || key == Qt::Key_Enter)) {
        if (!m_ctrlEnter && modifiers & Qt::CtrlModifier) {
          ui->textEdit->insertPlainText("\n");
        } else if (!m_editIndex && !hasAutoScroll()) {
          setAutoScroll(true);
        } else {
          sendEditText();
        }
        return true;
      }
    }

    if (key == Qt::Key_Escape && (m_editIndex || m_escape)) {
      onTypingTimeout();
      if (m_editIndex || m_escape == 1 || (m_escape == 3 && ui->textEdit->toPlainText().size())) {
        QTextCursor cursor(ui->textEdit->document());

        cursor.select(QTextCursor::Document);
        cursor.insertText(""); // clear text on ESC but allow undo
        if (m_contact->isTest()) m_commandIndex = m_lastCommands.size();

        if (m_escape == 3 && !m_editIndex) return true;
      }

      if (m_editIndex) {
        editFinish();
        return true;
      }

      if (m_escape == 2 || m_escape == 3) {
        if (!Contacts::get()->isSplitMode()) {
          Contacts::get()->setWindowState(Qt::WindowMinimized);
        } else if (m_contact->m_chatWindow) {
          m_contact->m_chatWindow->setWindowState(Qt::WindowMinimized);
        }
      }
      return true;
    }

    // call typing notify
    if (key != Qt::Key_Alt && key != Qt::Key_Meta && key != Qt::Key_Control && key != Qt::Key_Shift) {
      if (key != Qt::Key_Tab && key != Qt::Key_Backtab && key != Qt::Key_PageUp && key != Qt::Key_PageDown) {
        if (key != Qt::Key_Enter && key != Qt::Key_Return && !m_contact->isTest()) {
          if (!m_typing) {
            m_typing = true;
            sim_contact_set_(m_contact->m_simId, CONTACT_KEY_FLAGS, sim_number_new(CONTACT_FLAG_TYPE_Y));
          }
          m_timer.start();
        }
      }
    }

    if (!(modifiers & (Qt::ControlModifier | Qt::ShiftModifier)) && // these arrows are used by the text edit
        (!ui->findFrame->isVisible() && (!m_altArrow || (modifiers & (Qt::AltModifier | Qt::MetaModifier))))) {
      switch (key) {
        case Qt::Key_Up:
          if (m_contact->isTest()) {
            if (modifiers & (Qt::AltModifier | Qt::MetaModifier)) {
              findCommand(-1);
            } else if (m_commandIndex) {
              setEditText(m_lastCommands.at(--m_commandIndex));
            }
          } else {
            if (!m_editIndex) { // still not started editing
              QString s = ui->textEdit->toPlainText();
              if (s.trimmed().size()) break; // do not allow edit if there is something inside chat
            } else if (!(modifiers & (Qt::AltModifier | Qt::MetaModifier))) {
              QTextCursor cursor = ui->textEdit->textCursor();
              int position = cursor.position();
              cursor.movePosition(QTextCursor::Up);
              // do not change edit message if not at the first visible line
              if (cursor.position() != position || ui->textEdit->document()->isModified()) break;
            }

            if (const char * rslt = m_chatModel->getEditText(&m_editIndex, 1)) editMessage(rslt);
          }
          return true;

        case Qt::Key_Down:
          if (m_contact->isTest()) {
            if (modifiers & (Qt::AltModifier | Qt::MetaModifier)) {
              findCommand(1);
            } else if (m_commandIndex + 1 < m_lastCommands.size()) {
              setEditText(m_lastCommands.at(++m_commandIndex));
            }
          } else {
            if (!m_editIndex) break; // do not change edit message if not already editing

            if (!(modifiers & (Qt::AltModifier | Qt::MetaModifier))) {
              QTextCursor cursor = ui->textEdit->textCursor();
              int position = cursor.position();
              cursor.movePosition(QTextCursor::Down);
              // do not change edit message if not at the last visible line
              if (cursor.position() != position || ui->textEdit->document()->isModified()) break;
            }

            if (const char * rslt = m_chatModel->getEditText(&m_editIndex, -1)) {
              editMessage(rslt);
            } else {
              ui->textEdit->clear();
              editFinish();
            }
          }
          return true;
      }
    }
  }

  return Parent::eventFilter(obj, event);
}

void ChatFrame::findCommand(int direction)
{
  int ndx = m_commandIndex;
  QString qs = ui->textEdit->textCursor().selectedText();

  if (qs.isEmpty()) qs = ui->textEdit->toPlainText();
  qs = qs.trimmed();
  if (qs[0] == '*') qs = qs.mid(1);

  for (int count = m_lastCommands.size(); count; count--) {
    ndx += direction;
    if (ndx < 0) {
      ndx = m_lastCommands.size() - 1;
    } else if (ndx >= m_lastCommands.size()) {
      ndx = 0;
    }

    int pos = m_lastCommands.at(ndx).indexOf(qs, Qt::CaseInsensitive);

    if (pos >= 0) {
      m_commandIndex = ndx;
      if (!qs.size()) {
        pos = m_lastCommands.at(ndx).size();
        ui->textEdit->setPlainText(m_lastCommands.at(ndx) + (qs = " "));
      } else {
        ui->textEdit->setPlainText(m_lastCommands.at(ndx));
      }

      QTextCursor cursor = ui->textEdit->textCursor();

      cursor.setPosition(pos);
      cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, qs.size());
      ui->textEdit->setTextCursor(cursor);

      break;
    }
  }
}

void ChatFrame::findMessage(int direction)
{
  if (!m_chatModel->rowCount()) return;

  int msgSearchNdx = m_startFindFrom < 0 ? m_chatModel->getFoundIndex() : m_startFindFrom;
  m_startFindFrom = -1;
  QString str;
  QString qs = ui->findEdit->text();
  if (qs.isEmpty()) return;

  if (msgSearchNdx < 0) {
    msgSearchNdx = m_chatModel->rowCount() - 1;
  } else {
    msgSearchNdx += direction;
  }

  int startFrom = -1;
  qtfix::setOverrideCursor(true);
  for (;;) {
    if (msgSearchNdx < 0 || msgSearchNdx >= m_chatModel->rowCount()) {
      msgSearchNdx = direction > 0 ? 0 : m_chatModel->rowCount() - 1;
    }

    if (startFrom < 0) {
      startFrom = msgSearchNdx;
    } else if (startFrom == msgSearchNdx) {
      changeFindColor(true);
      break; // not found
    }

    const char * msg = m_chatModel->getSearchText(msgSearchNdx);
    if (msg) str = msg;
    if (msg && str.contains(qs, Qt::CaseInsensitive)) {
      m_chatModel->setFoundIndex(msgSearchNdx);
      ui->chatView->scrollTo(m_chatModel->index(msgSearchNdx, msgcol_msg), QAbstractItemView::PositionAtBottom);
      // do it again, because lines are resized and actual line goes out of visibility (sometimes)
      ui->chatView->scrollTo(m_chatModel->index(msgSearchNdx, msgcol_msg), QAbstractItemView::PositionAtCenter);
      changeFindColor(false);
      break; // not found
    }
    msgSearchNdx += direction;
  }
  log_debug_("ui", "elapsed FIND %lld ms\n", qtfix::setOverrideCursor(false));
}

void ChatFrame::findOpen(int startNdx)
{
  if (!m_editIndex) {
    m_startFindFrom = startNdx;
    ui->findFrame->setVisible(true);
    changeFindColor(false);
    ui->findEdit->setFocus();
    ui->findEdit->selectAll();
  }
}

void ChatFrame::changeFindColor(bool error)
{
  QPalette palette; // = ui->findEdit->parentWidget()->palette();
  if (error && m_notfoundBrush.isValid() && m_notfoundColor.isValid()) {
    palette.setColor(QPalette::Base, m_notfoundBrush);
    palette.setColor(QPalette::Text, m_notfoundColor);
  }
  ui->findEdit->setPalette(palette);
}

void ChatFrame::copySelection(bool textOnly)
{
  QString qs;
  qtfix::setOverrideCursor(true);
  QModelIndexList selected = ui->chatView->selectionModel()->selectedRows();
  int n = selected.size();
  log_debug_("ui", "copy %d messages\n", n);
  std::sort(selected.begin(), selected.end());

  if (textOnly && n == 1 && m_chatModel->getMsgType(selected.at(0).row()) == 'x') {
    QStringList list = Transfers::getMessage(m_contact->m_simId, selected.at(0).row() + 1).split(' ');
    QChar ch = list.size() >= 3 ? Transfers::convertFileName(qs = list.at(2)) : QChar();
    if (!ch.isNull() && !qs.isEmpty()) {
      simtype info = sim_xfer_get_(m_contact->m_simId, SIM_XFER_GET_INFO, SIM_XFER_BIT_EXTENDED);
      QString qs2 = sim_table_get_pointer(info, SIM_XFER_GET_INFO_RECEIVED);
      sim_xfer_free_(info);
      if (!qs2.isEmpty()) qs = qs2.append(ch).append(qs);
    }
    qs.append('\n');
  } else {
    for (int i = 0; i < n; ++i) qs.append(m_chatModel->getSelectedText(selected.at(i).row(), textOnly)).append('\n');
  }

  if (!m_copyNewLine || (m_copyNewLine < 0 && n == 1)) {
    if (!qs.isEmpty()) qs.resize(qs.size() - 1);
  }
  QApplication::clipboard()->setText(qs);
  log_debug_("ui", "elapsed COPY %lld ms\n", qtfix::setOverrideCursor(false));
}

void ChatFrame::editMessage(const char * text)
{
  m_original = text;
  setEditText(m_original); //ui->textEdit->setPlainText(m_original);
  m_chatModel->setEditIndex(m_chatModel->getTextIndex());
  ui->chatView->scrollTo(m_chatModel->index(m_chatModel->getEditIndex(), msgcol_msg));
  ui->editLabel->setVisible(true);
  ui->scrollButton->setToolTip("");
}

void ChatFrame::editFinish()
{
  m_editIndex = 0;
  m_chatModel->setEditIndex(-1);
  ui->editLabel->setVisible(false);
  setAutoScrollToolTip();
}

int ChatFrame::loadMessages(int nMsgs, bool ignoreErrors)
{
  int simres;
  qtfix::setOverrideCursor(true);
  if (!m_contact->isTest()) {
    int errors = ignoreErrors ? SimParam::get("msg.errors") : 0;
    if (errors) SimParam::set("msg.errors", 0, true);
    simres = sim_msg_load_(m_contact->m_simId, nMsgs);
    char * simerr = sim_error_get_text(simres);
    if (errors) SimParam::set("msg.errors", errors, true);
    sim_error_set_text(simerr, 0, 0, simres);
    sim_free_string(simerr);
  } else {
    simres = sim_console_load_(nMsgs);
  }
  log_debug_("ui", "elapsed LOAD %lld ms\n", qtfix::setOverrideCursor(false));
  return simres;
}

int ChatFrame::removeMessage(int row)
{
  int simres = sim_msg_remove_(m_contact->m_simId, row + 1);
  m_chatModel->clearCache();
  emit m_chatModel->dataChanged(m_chatModel->index(row, 0), m_chatModel->index(row, msgcol_nCols));
  return simres;
}

void ChatFrame::removeMessages(int row)
{
  int force = SimParam::get("msg.remove");
retry:
  int simres = removeMessage(row);
  ui->chatView->resizeRowContentVisiblePart();
  if (simres != SIM_OK) {
    QString checkBox;
    QString simerr = SimCore::getError(simres);
    if (simres == SIM_MSG_NO_EDIT) {
      checkBox = tr("Force delete of this message when I push \"%1\"").arg(qApp->translate("QMessageBox", "OK"));
    }
    if (!m_allLoaded) {
      simerr.append("\n\n");
      simerr.append(tr("Right-click your chat history and choose \"Show More\" to reload it and then try again."));
    }
    if (!m_allLoaded || SimParam::get("msg.remove")) checkBox = "";
    QString error = tr("Deleting chat message not successful (%1)").arg(simres);
    if (qtfix::execMessageBox(false, error, simerr, this, checkBox)) {
      SimParam::set("msg.remove", 1, true);
      goto retry;
    }
  }
  SimParam::set("msg.remove", force, true);
}

void ChatFrame::removeMessages()
{
  QItemSelectionModel * select = ui->chatView->selectionModel();
  if (select->hasSelection()) {
    int force = SimParam::get("msg.remove");
    QModelIndexList selected = select->selectedRows();
    int count = selected.count();
  retry:
    bool edit = false;
    int nok = 0;
    qtfix::setOverrideCursor(true);
    for (int i = 0; i < count; i++) {
      if (!m_chatModel->isRemoved(selected[i].row())) {
        int simres = removeMessage(selected[i].row());
        if (simres != SIM_OK) nok++;
        if (simres == SIM_MSG_NO_EDIT) edit = true;
      }
    }
    log_debug_("ui", "elapsed DELETE %lld ms\n", qtfix::setOverrideCursor(false));
    ui->chatView->resizeRowContentVisiblePart();
    if (nok) {
      QString checkBox;
      QString qs = tr("%1 out of %n message(s) could not be deleted.", 0, selected.count()).arg(nok).append("\n");
      if (edit) {
        checkBox = tr("Force delete of partially-accessible messages when I push \"%1\"");
        checkBox = checkBox.arg(qApp->translate("QMessageBox", "OK"));
      }
      if (!m_allLoaded) {
        qs.append("\n");
        qs.append(tr("Right-click your chat history and choose \"Show More\" to reload it and then try again."));
      }
      if (!m_allLoaded || SimParam::get("msg.remove")) checkBox = "";
      if (!checkBox.isEmpty()) qs.append("\n");
      if (qtfix::execMessageBox(QMessageBox::Warning, tr("Simphone warning").toUtf8().data(), qs, this, checkBox)) {
        SimParam::set("msg.remove", 1, true);
        goto retry;
      }
    }
    SimParam::set("msg.remove", force, true);
  }
}

void ChatFrame::closeEvent(QCloseEvent * event)
{
  log_note_("ui", "%s '%s'\n", __FUNCTION__, m_contact->m_nick.toUtf8().data());
  ChatFrames::get()->notifyChatStopped(m_contact->m_id);
  Parent::closeEvent(event);
}

void ChatFrame::changeEvent(QEvent * event)
{
  if (event->type() == QEvent::LanguageChange) {
    ui->retranslateUi(this);
    showAuthText();
    setAutoScrollToolTip();
    onSignalContactChanged(m_contact->m_id);
    onSignalContactConnectionChanged(m_contact->m_id, 'l');

    QString callText = tr("Call");
    QString hangText = tr("Hang Up");
    simnumber talkid = sim_audio_check_talking_();
    if (!talkid) {
      if (m_contact->isCallState(Contact::call_outgoing)) {
        hangText = tr("End Call");
      } else if (m_contact->isCallState(Contact::call_incoming)) {
        callText = tr("Answer");
        hangText = tr("Decline");
      }
    } else if (m_contact->m_simId != talkid && m_contact->isCallState(Contact::call_incoming)) {
      callText = tr("Answer");
      hangText = tr("Decline");
    }
    ui->callButton->setText(callText);
    ui->hangupButton->setText(hangText);
  }
  Parent::changeEvent(event);
}

void ChatFrame::showEvent(QShowEvent * event)
{
  bool minimized = event->spontaneous();
  log_xtra_("ui", "%s %s '%s'\n", __FUNCTION__, minimized ? "unminimize" : "unhide", m_contact->m_nick.toUtf8().data());
  Parent::showEvent(event);
#ifndef DONOT_DEFINE
  if (!m_splitterInitialized) {
    QFontMetrics fm(ui->textEdit->font());
    int addHeight = fm.ascent() * 5;
    QList<int> sizes = ui->splitter->sizes();
    if (sizes[0] - addHeight >= addHeight) {
      sizes[0] -= addHeight;
      sizes[1] += addHeight;
      ui->splitter->setSizes(sizes);
    }
    m_splitterInitialized = true;
  }
#endif
  if (!minimized) onVisibilityChanged(true);
}

void ChatFrame::hideEvent(QHideEvent * event)
{
  bool minimized = event->spontaneous();
  log_xtra_("ui", "%s %s '%s'\n", __FUNCTION__, minimized ? "minimize" : "hide", m_contact->m_nick.toUtf8().data());
  Parent::hideEvent(event);
  if (!minimized) onVisibilityChanged(false);
}

void ChatFrame::focusInEvent(QFocusEvent * event)
{
  log_xtra_("ui", "%s '%s'\n", __FUNCTION__, m_contact->m_nick.toUtf8().data());
  Parent::focusInEvent(event);
  if (m_showMinimized) {
    // was shown minimized, but does not load history
    m_showMinimized = false;
    onVisibilityChanged(true);
  }

  m_contact->clearNotifications(Contact::flag_Notifications);

  if (!ui->textEdit->hasFocus()) {
    if (!m_focusFind || !ui->findFrame->isVisible()) {
      ui->textEdit->setFocus();
    } else if (!ui->findEdit->hasFocus()) {
      ui->findEdit->setFocus();
    }
  }

  ChatFrames::get()->notifyChatUsed(m_contact->m_id);
}

void ChatFrame::on_sendButton_clicked()
{
  if (ui->sendButton->isEnabled()) Transfers::sendFiles(m_contact);
}

void ChatFrame::on_callButton_clicked()
{
  int simres = SIM_OK;

  if (m_contact->isCallState(Contact::call_outgoing)) { // we are calling and we want to stop call
    simres = sim_audio_hangup_(m_contact->m_simId);     // call rejected
  } else if (ui->callButton->isEnabled()) {
    if (m_contact->isCallState(Contact::call_incoming)) { // the other party is calling, so switch to talking state
      m_contact->clearNotifications(Contact::flag_hasMissedCall);
    }
    simres = sim_audio_call_(m_contact->m_simId);
  }
  if (simres != SIM_OK) {
    QString error = tr("Calling %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
    qtfix::execMessageBox(false, error, SimCore::getError(simres), this);
  }
}

void ChatFrame::on_hangupButton_clicked()
{
  int simres = SIM_OK;

  if (m_contact->isCallState(Contact::call_incoming)) {
    m_contact->clearNotifications(Contact::flag_hasMissedCall);
    simres = sim_audio_hangup_(m_contact->m_simId);            // the other party is calling and we press Reject
  } else if (m_contact->isCallState(Contact::call_outgoing)) { // we are calling and we want to stop call
    simres = sim_audio_hangup_(m_contact->m_simId);            // call rejected
  } else if (sim_audio_check_talking_()) {
    // currently talking, so hangup
    simres = sim_audio_hangup_(sim_audio_check_talking_()); // hangup any call (unless we support conference calls)
  }
  if (simres != SIM_OK) {
    QString error = tr("Hanging up to %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
    qtfix::execMessageBox(false, error, SimCore::getError(simres), this);
  }
}

void ChatFrame::on_verifyButton_clicked()
{
  if (!ui->verifyButton->isVisible()) return;

  simnumber talkid = sim_audio_check_talking_();
  if (!talkid || m_contact->m_simId != talkid) {
    QString qs;
    qs = tr("Please, call <b>%1</b>.<br/><br/>The <b>Verify</b> button will work when <b>%1</b> answers your call.");
    QString nick = m_contact->m_nick.toHtmlEscaped();
    QMessageBox::information(this, tr("Verification of %1").arg(nick), qs.arg(nick));
    return;
  }

  if (!m_contact->m_badSystemName.isEmpty()) {
    QString qs =
      tr("<span style=\" color:#ff0000;\"><h1><b><center>ATTENTION ! ! !</center></b></h1></span><h3>Simphone has"
         " detected an OS on %1's computer, which runs or ran widely debated, hardly obvious operations.</h3>Namely"
         " the <span style=\" color:#ff0000;\"><b>%2</b></span> operating system, which <b>%1</b> uses, contained or"
         " contains routines, which may or may not forward keyboard entries, as well as audio cuts and other personal"
         " data via the internet to addresses unknown to you, without being able to easily switch these options off or"
         " just learn about which data were or would be transferred to whom.<br/><br/>Depending on the individual grade"
         " for security, both users should weigh and discuss the advantages and shortcomings of these security issues."
         "<br/><br/><b>Are you sure you want to proceed with verification?</b>");
    QString message = qs.arg(m_contact->m_nick.toHtmlEscaped(), m_contact->m_badSystemName);
    const QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
    if (QMessageBox::warning(this, tr("SIMPHONE SECURITY"), message, buttons, QMessageBox::No) != QMessageBox::Yes) {
      return;
    }
  }

  simtype contact = sim_contact_get_(m_contact->m_simId, CONTACT_BIT_VERIFY);
  QString vcode = sim_table_get_pointer(contact, CONTACT_KEY_VERIFY);
  QString addr = sim_table_get_pointer(contact, CONTACT_KEY_ADDRESS);
  sim_contact_free_(contact);

  if (vcode.startsWith('W')) vcode.remove(0, 1);
#ifdef _DEBUG
  QApplication::clipboard()->setText(vcode);
#endif

  // add spaces
  for (int i = vcode.size(); (i -= 4) > 1;) {
    vcode.insert(i, "&nbsp;");
  }

  if (m_contact->isVerified()) {
    QString qs = tr("Your verification code is: <b>%1</b><br/><br/>Please, ask"
                    " <span style=\" color:#ff0000;\"><b>%2</b></span> to push the <b>Verify</b> button"
                    " and read (not type) this code to <span style=\" color:#ff0000;\"><b>%2</b></span>");
    QMessageBox mbox(QMessageBox::NoIcon, tr("Verification of %1").arg(m_contact->m_nick),
                     qs.arg(vcode, m_contact->m_nick.toHtmlEscaped()), QMessageBox::Ok, this);
    m_verifyDialog = &mbox;
    mbox.exec();
    m_verifyDialog = 0;
    return;
  }

  QInputDialog input(this);
  if (addr.isEmpty()) addr = m_contact->getTextId();
  input.setWindowTitle(tr("Verification of %1 (%2)").arg(m_contact->m_nick, addr));
  addr = tr("Your verification code is: <b>%1</b><br/><br/>Please, ask <span style=\" color:#ff0000;\"><b>%2</b></span>"
            " to push the <b>Verify</b> button and read (not type) the verification code to you.<br/><br/>"
            "You can be sure that the connection is free of eavesdropping only after you have entered this code.<br/>"
            "You only need to do this once on this system.<br/><br/>Enter the verification code of"
            " <span style=\" color:#ff0000;\"><b>%2</b></span> here:");
  input.setLabelText(addr.arg(vcode, m_contact->m_nick.toHtmlEscaped()));
  input.setTextValue(m_verifyCode);
  m_verifyDialog = &input;

  if (qtfix::execInputDialog(&input) == QDialog::Accepted) {
    QString pass = m_verifyCode = input.textValue();
    m_verifyDialog = 0;
    pass.replace(' ', "");
    int simres = sim_contact_set_(m_contact->m_simId, CONTACT_KEY_VERIFY, sim_pointer_new(pass.toUtf8().data()));
    if (simres != SIM_OK) {
      simres = sim_contact_set_(m_contact->m_simId, CONTACT_KEY_VERIFY, sim_pointer_new(("W" + pass).toUtf8().data()));
    }
    if (simres != SIM_OK) {
      QString error = tr("Verification of %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
      qtfix::execMessageBox(true, error, SimCore::getError(simres), this);
    } else if (pass.size()) {
      m_verifyCode = QString();
      if (m_contact->isVerify()) ui->verifyButton->setVisible(false);
      ui->authLabel->setVisible(false);
      setVerified(true);
    }
  } else {
    m_verifyDialog = 0;
  }
}

void ChatFrame::on_scrollButton_clicked()
{
  setAutoScroll(true);
}

void ChatFrame::on_findEdit_returnPressed()
{
  findMessage(-1);
}

void ChatFrame::on_findCloseButton_clicked()
{
  ui->textEdit->setFocus();
  ui->findFrame->setVisible(false);
  ui->chatView->m_scrollToBottomOnResize = hasAutoScroll();
  m_chatModel->setFoundIndex(-1);
}

void ChatFrame::on_findNextButton_clicked()
{
  findMessage(1);
}

void ChatFrame::on_findPreviousButton_clicked()
{
  findMessage(-1);
}

void ChatFrame::onLayoutChanged(const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint)
{
  scroll();
}

void ChatFrame::onHeaderChanged(Qt::Orientation orientation, int, int)
{
  if (orientation == Qt::Vertical) scroll();
}

void ChatFrame::onRowsInserted(const QModelIndex &, int first, int last)
{
  m_noneLoaded = false;
  if (last < 0) last = first;
  if (first >= 0) {
    QRect r = ui->chatView->contentsRect();
    int firstVisible = ui->chatView->rowAt(r.top()) - 1;
    int lastVisible = ui->chatView->rowAt(r.bottom() - 2) + 1;
    if (first < firstVisible) first = firstVisible;
    if (lastVisible && last > lastVisible) last = lastVisible;
    for (int i = last; i >= first; --i) {
      ui->chatView->resizeRowToContents(i);
    }
  }
  if (!m_disableBottomScroll) scroll();
}

void ChatFrame::onScrollChanged(int, int)
{
  QScrollBar * scrollBar = ui->chatView->verticalScrollBar();
  setAutoScroll(scrollBar->value() == scrollBar->maximum());
}

void ChatFrame::onRowDoubleClicked(const QModelIndex & index)
{
  if (m_contact->isTest()) {
    if (SimParam::get("ui.console.doubleclick")) {
      QApplication::clipboard()->setText(m_chatModel->getSelectedText(index.row(), false));
    }
  } else if (SimParam::get("ui.chat.doubleclick")) {
    QString qs = m_chatModel->data(m_chatModel->index(index.row(), msgcol_msg)).toString();
    if (qs.size() && qs.at(0) == '\n') qs.remove(0, 1);
    QApplication::clipboard()->setText(qs);
  }
  m_lastRowClicked = -1;
  ui->chatView->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
}

void ChatFrame::onRowClicked(const QModelIndex & index)
{
  QItemSelectionModel * select = ui->chatView->selectionModel();
  QModelIndexList selected = select->selectedRows();
  if (qtfix::hasSelectedRows(select) == 1 && select->selectedRows().value(0).row() == index.row()) {
    if (index.row() == m_lastRowClicked) select->clearSelection();
    m_lastRowClicked = index.row() == m_lastRowClicked ? -1 : index.row();
  } else {
    m_lastRowClicked = -1;
  }
}

void ChatFrame::onVisibilityChanged(bool visible)
{
  if (visible) {
    ChatFrames::get()->notifyChatUsed(m_contact->m_id);
  } else if (!Contacts::get()->isCommitting()) {
    ChatFrames::get()->notifyChatStopped(m_contact->m_id);
  }

  int simres = SIM_OK;
  if (m_contact->isTest()) { // test user
    bool nofilelog = !SimParam::get("file.log") /* || SimCore::mc_startupUser.isNull()*/;
    if ((nofilelog && visible && !m_someLoaded) || SimParam::get("ui.console.unload") > 0) {
      simres = loadMessages(!visible ? LOG_NO_EVENT : nofilelog ? LOG_NO_LOAD : m_loadMessages);
      m_someLoaded = visible;
      if (visible) {
        Logs::newLog();
        if (m_shown && !hasAutoScroll()) {
          // if it is already shown and we have no scroll to bottom then console will be displayed without resizing
          // and without recalculating row sizes, so we have to recalculate them.
          ui->chatView->resizeRowContentVisiblePart();
        }
      } else {
        m_noneLoaded = m_allLoaded = false;
      }
    } else if (visible) {
      if (m_someLoaded || m_showMinimized) {
        m_chatModel->notifyRowsChanged();
      } else {
        m_someLoaded = true;
        simres = loadMessages(m_loadMessages);
        Logs::newLog();
      }
    }
  } else { // normal user
    if (!visible) {
      m_chatModel->setOldRowCount(m_chatModel->rowCount());
      m_chatModel->clearCache();
      onTypingTimeout();
      if (m_connected && (Contacts::get()->isSplitMode() || Contacts::get()->isVisible())) {
        sim_contact_disconnect_(m_contact->m_simId);
        m_connected = false;
      }
    } else {
      if (m_someLoaded || m_showMinimized) {
        m_chatModel->notifyRowsChanged();
      } else {
        simunsigned time = sim_get_tick() + 1000;
        int n = m_loadMessages > m_contact->m_newMessages ? m_loadMessages : m_contact->m_newMessages;
        m_someLoaded = true;
        do {
          simres = loadMessages(n);
          m_chatModel->notifyRowsChanged();
          n += m_loadMessages;
        } while (simres == SIM_OK && m_chatModel->rowCount() < (m_loadMessages + 1) / 2 && sim_get_tick() < time);
        n = m_chatModel->rowCount() - (simres == SIM_OK || simres == SIM_FILE_START ? m_contact->m_newMessages : 0);
        m_chatModel->setOldRowCount(n < 0 ? 0 : n);
        m_chatModel->clearCache();
      }
      if (!m_showMinimized) {
        if (!m_connected) sim_contact_connect_(m_contact->m_simId);
        m_connected = true;
      }
    }
  }

  if (simres == SIM_FILE_START) {
    m_allLoaded = true;
  } else if (simres != SIM_OK && simres >= ERROR_BASE_EXPAT) {
    bool test = m_contact->isTest();
    QString error;
    error = test ? tr("Loading console log not successful (%1)") : tr("Loading chat history not successful (%1)");
    qtfix::execMessageBox(false, error.arg(simres), SimCore::getError(simres), this);
  }
  if (visible && !m_shown && !m_showMinimized) {
    m_shown = true;
    ui->chatView->scrollToBottom();
  }

  onSignalContactAudioChanged(-1, audio_none);
}

void ChatFrame::onTextChanged()
{
  QTextCursor cursor = ui->textEdit->textCursor();
  int position;

  do { // make sure cursor stays at top line on paste
    position = cursor.position();
    cursor.movePosition(QTextCursor::Up);
  } while (cursor.position() != position);

  ui->textEdit->setTextCursor(cursor);
}

void ChatFrame::onTypingTimeout()
{
  if (m_typing) {
    m_typing = false;
    sim_contact_set_(m_contact->m_simId, CONTACT_KEY_FLAGS, sim_number_new(CONTACT_FLAG_TYPE_N));
  }
}

void ChatFrame::onSignalFoundUnseenNick(const QString & nick)
{
  if (ui->chatView->calcNickSize(nick)) {
    ui->chatView->resizeEvent(0);
    QTimer::singleShot(0, ui->chatView, SLOT(resizeRowContentVisiblePart()));
  }
}

void ChatFrame::onSignalMessageReceived(unsigned id, int, bool noNotify)
{
  if (!noNotify && unsigned(m_contact->m_id) == id && !isFocused()) {
    m_contact->setNotifications(Contact::flag_hasUnreadMsg);
  }
}

void ChatFrame::onSignalMessageEdited(unsigned id, int msgNdx)
{
  onSignalMessageReceived(id, msgNdx, false);
  if (unsigned(m_contact->m_id) == id) {
    ui->chatView->resizeRowContentVisiblePart();
    scroll();
  }
}

void ChatFrame::onSignalContactAudioChanged(unsigned id, SimAudioState newState)
{
  log_note_("ui", "%s '%s' %d\n", __FUNCTION__, m_contact->m_nick.toUtf8().data(), id);
  QString callText = tr("Call");
  QString hangText = tr("Hang Up");
  bool hasCall = true;
  bool mayCall = true;
  bool hasHang = true;
  bool allowCall = true;
  bool showVerify = !m_contact->isVerified();
  if (simnumber talkid = sim_audio_check_talking_()) {
    mayCall = false;
    hasHang = m_contact->m_simId == talkid;
    showVerify = showVerify || (hasHang && !m_contact->isVerify());
    if (!hasHang) {                                         // talking to somebody else
      if (m_contact->isCallState(Contact::call_incoming)) { // the other party is calling
        callText = tr("Answer");
        hangText = tr("Decline");
        mayCall = true;
        hasHang = true;
      }
    }
  } else {
    if (m_contact->isCallState(Contact::call_outgoing)) { // we are calling
      hangText = tr("End Call");
      hasCall = false;
    } else if (m_contact->isCallState(Contact::call_incoming)) { // the other party is calling
      callText = tr("Answer");
      hangText = tr("Decline");
    } else if (int(id) < 0 || int(id) == m_contact->m_id) {
      hasHang = false;
      mayCall = !SimCore::isCalling();
    } else {
      mayCall = !SimCore::isCalling() && newState != audio_ringing;
      hasHang = false;
    }

    allowCall = m_contact->m_rights & CONTACT_FLAG_AUDIO || m_contact->isTest();
  }
  ui->callButton->setVisible(hasCall);
  if (hasCall) {
    enableCall(mayCall && allowCall);
    ui->callButton->setText(callText);
    ui->callButton->setDefault(false);
    ui->callButton->setAutoDefault(false);
  }
  ui->hangupButton->setVisible(hasHang);
  if (hasHang) {
    ui->hangupButton->setText(hangText);
    ui->hangupButton->setEnabled(hasHang);
    ui->hangupButton->setDefault(false);
    ui->hangupButton->setAutoDefault(false);
  }

  if (m_contact->isBlocked() || m_contact->isDeleted() || m_contact->isForgotten()) showVerify = false;
  ui->verifyButton->setVisible(showVerify);
}

void ChatFrame::onSignalContactChanged(unsigned id)
{
  if (m_contact->m_id == int(id)) {
    enableChat((m_contact->m_rights & CONTACT_FLAG_UTF) != 0);
    enableCall((m_contact->m_rights & CONTACT_FLAG_AUDIO) != 0);
    ui->sendButton->setEnabled((m_contact->m_rights & CONTACT_FLAG_XFER) != 0);

    showAuthText();

    bool showVerify = !m_contact->isVerified();
    if (!showVerify) {
      simnumber talkid = sim_audio_check_talking_();
      showVerify = talkid && (m_contact->m_simId == talkid) && !m_contact->isVerify();
    }
    if (m_contact->isNewContact() || m_contact->isBlocked() || m_contact->isDeleted() || m_contact->isForgotten()) {
      showVerify = false;
    }
    ui->verifyButton->setVisible(showVerify);

    if ((!m_showInfoLine || m_contact->m_info.isEmpty()) && !m_contact->isMe()) {
      ui->infoLineLabel->setVisible(false);
    } else {
      QFont boldFont = ui->infoLineLabel->font();
      int i = 0;
      for (int j = 0; j < 5 && (i = m_contact->m_info.indexOf('\n', i)) != -1; ++j) ++i;
      boldFont.setBold(true);
      ui->infoLineLabel->setFont(boldFont);
      ui->infoLineLabel->setText(m_contact->m_info.left(i - 1));
      ui->infoLineLabel->setVisible(true);
      if (!m_contact->isTest()) {
        QString qs = tr("This text is visible to all of %1's contacts.").arg(m_contact->m_nick.toHtmlEscaped());
        ui->infoLineLabel->setToolTip(qs.prepend("<p style='white-space:pre'>"));
      }
    }
  }
}

void ChatFrame::onSignalContactConnectionChanged(unsigned id, int connectionState)
{
  if (m_contact->m_id == int(id)) {
    if (connectionState == 'd' && m_verifyDialog) emit m_verifyDialog->reject();
    if (m_showConnection) {
      static const char * tooltips[SimCore::state_NConnectionStates + 1] = {
        QT_TR_NOOP("Not connected to the DHT."), QT_TR_NOOP("Connecting to the DHT..."),
        QT_TR_NOOP("Connected to the DHT."), QT_TR_NOOP("Fully connected to the DHT."),
        QT_TR_NOOP("Connected directly (incoming)"), QT_TR_NOOP("Connected directly (outgoing)"),
        QT_TR_NOOP("Connected directly (incoming traversal)"), QT_TR_NOOP("Connected directly (outgoing traversal)"),
        QT_TR_NOOP("Connected directly (incoming reversal)"), QT_TR_NOOP("Connected directly (outgoing reversal)"),
        QT_TR_NOOP("Your connection is being relayed (by your proxy)"),
        QT_TR_NOOP("Your connection is being relayed (by other proxy)"),
        QT_TR_NOOP("Trying to connect directly (to bypass your proxy)"),
        QT_TR_NOOP("Trying to connect directly (to bypass other proxy)"),
        QT_TR_NOOP("UDP audio call (direct)"), QT_TR_NOOP("UDP audio call (traversed)"),
        QT_TR_NOOP("Not connected to contact.")
      };
      QString qs;
      SimCore::E_ConnectionState state = SimCore::getConnectionState(id);
      ui->connectionLabel->setPixmap(*mc_connectionStatePixmaps[state]);
      if (state == SimCore::state_disconnect && !m_contact->isTest()) state = SimCore::state_NConnectionStates;
      ui->connectionLabel->setToolTip(tr(tooltips[state]));
    }
  }
}

void ChatFrame::onCustomContextMenu(const QPoint & pos)
{
  QModelIndex cell = ui->chatView->indexAt(pos);
  bool none = m_chatModel->rowCount() / 2 <= m_loadMessages;
  char type = cell.isValid() ? m_chatModel->getMsgType(cell.row()) : char(0);

  QMenu menu(this);
  QAction * x = 0;
  if (type == 'x') {
    x = menu.addAction(tr("File Transfer"));
    menu.addSeparator();
  }
  QAction * lm = menu.addAction(tr("Show &More"));
  QAction * ll = menu.addAction(none ? tr("Show &None") : tr("Show &Less"));
  QAction * la = m_showAll ? menu.addAction(tr("Show All")) : 0;
  QAction * f = cell.isValid() ? menu.addAction(tr("&Find")) : 0;
  menu.addSeparator();
  QAction * c = menu.addAction(qApp->translate("QWidgetTextControl", "&Copy"));
  QAction * t = menu.addAction(tr("Copy &Text"));
  menu.addSeparator();
  QAction * s = menu.addAction(tr("Select &All"));
  QAction * e = 0;
  QAction * r = 0;
  bool editAllowed = m_editIndex ? false : m_chatModel->isEditAllowed(cell.row());
  QItemSelectionModel * select = ui->chatView->selectionModel();
  m_lastRowClicked = select->isSelected(cell) && qtfix::hasSelectedRows(select) == 1 ? cell.row() : -1;

  if (m_editIndex || m_allLoaded || SimCore::mc_startupUser.isNull()) {
    lm->setEnabled(false);
    if (la) la->setEnabled(false);
  }
  if (m_editIndex || m_noneLoaded || (SimCore::mc_startupUser.isNull() && !none)) ll->setEnabled(false);

  if (!m_contact->isTest()) {
    menu.addSeparator();
    if (type == 'o') e = menu.addAction(tr("&Edit Message"));
    if (qtfix::hasSelectedRows(select) > 1) {
      r = menu.addAction(tr("&Delete Messages"));
      if (m_editIndex) r->setEnabled(false);
    } else {
      r = menu.addAction(tr("&Delete Message"));
      if (m_editIndex || m_chatModel->isRemoved(cell.row())) r->setEnabled(false);
    }
    if (!editAllowed || ui->findFrame->isVisible() || !ui->textEdit->isEnabled() ||
        ui->textEdit->toPlainText().trimmed().size()) {
      if (e) e->setEnabled(false);
    }
    if (m_editIndex && f) f->setEnabled(false);
  }
  if (m_copyText) {
    t->setShortcut(Qt::CTRL + Qt::Key_C);
    c->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C);
  } else {
    t->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C);
    c->setShortcut(Qt::CTRL + Qt::Key_C);
  }
  if (f) f->setShortcut(Qt::CTRL + Qt::Key_F);

  QPoint point = ui->chatView->viewport()->mapToGlobal(pos);
  if (QAction * a = menu.exec(point)) {
    if (a == f) {
      findOpen(cell.row());
    } else if (a == c || a == t) {
      copySelection(a == t);
    } else if (a == x) {
      Contacts::get()->showDialog(m_contact, point, cell.row() + 1);
    } else if (a == s) {
      m_lastRowClicked = -1;
      ui->chatView->selectAll();
    } else if (a == e && !m_editIndex && !ui->findFrame->isVisible() && ui->textEdit->isEnabled() &&
               !ui->textEdit->toPlainText().trimmed().size()) {
      unsigned ndxBack;
      const char * rslt = 0;
      m_chatModel->setEditIndex(-1);
      qtfix::setOverrideCursor(true);
      for (ndxBack = 0; m_chatModel->getEditText(&ndxBack, 1);) {
        if (m_chatModel->rowCount() - cell.row() == int(ndxBack)) {
          m_editIndex = m_chatModel->rowCount() - cell.row();
          rslt = m_chatModel->getEditText(&m_editIndex, 0);
          ndxBack = unsigned(-1);
          break;
        }
      }
      log_debug_("ui", "elapsed EDIT %lld ms\n", qtfix::setOverrideCursor(false));
      m_chatModel->setEditIndex(-1);
      if (int(ndxBack) == -1) editMessage(rslt);
    } else if (a == r) {
      QString qs;

      if (select->hasSelection()) {
        QModelIndexList selected = select->selectedRows();
        std::sort(selected.begin(), selected.end());
        if (selected.count() > 1) {
          QString qs2 = m_chatModel->getSelectedText(selected[0].row(), false).left(SIM_SIZE_TIME - 4);
          qs = m_chatModel->getSelectedText(selected[selected.count() - 1].row(), false).left(SIM_SIZE_TIME - 4);

          QString text = tr("You have selected <span style=\" color:#ff0000;\"><b>%1 messages</b></span> to"
                            " delete from your local chat history.<br/><br/>The first message to delete is"
                            " dated <span style=\" color:#ff0000;\"><b>%2</b></span>.<br/>The last message"
                            " to delete is dated <span style=\" color:#ff0000;\"><b>%3</b></span>.<br/><br/>"
                            "<b>It will not be possible to undelete or modify these messages later."
                            "</b> If you only wanted to delete a single message, push <b>%4</b>."
                            "<br/><br/>Are you sure you want to <b>destroy</b> all these messages?");
          QString qs1 = qApp->translate("QShortcut", "No");
          text = text.arg(selected.count()).arg(qs2.replace(" ", "&nbsp;"), qs.replace(" ", "&nbsp;"), qs1);
          if (QMessageBox::question(this, tr("Delete messages"), text,
                                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
            if (qs2.left(SIM_SIZE_TIME - 10) != qs.left(SIM_SIZE_TIME - 10)) {
              text = tr("The <span style=\" color:#ff0000;\"><b>%1 messages</b></span> you have selected"
                        " for deletion span more than one date.<br/><br/>The first message to delete"
                        " is dated <span style=\" color:#ff0000;\"><b>%2</b></span>.<br/>"
                        "The last message to delete is dated <span style=\" color:#ff0000;\"><b>%3</b></span>."
                        "<br/><br/>Are you <b>absolutely</b> sure you want to destroy all these messages?");
              text = text.arg(selected.count()).arg(qs2.left(SIM_SIZE_TIME - 10), qs.left(SIM_SIZE_TIME - 10));
              if (QMessageBox::question(this, tr("Delete messages"), text,
                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) {
                return;
              }
            }
            removeMessages();
          }
          return;
        }
        if (cell.row() != selected[0].row()) {
          QMessageBox::warning(this, tr("Delete message"),
                               tr("You have selected a message different from the one you clicked."
                                  "\n\nPlease, click the message you want to delete and then try again."));
          return;
        }
      }

      char received = m_chatModel->isReceived(cell.row());
      unsigned ndxBack = m_chatModel->rowCount() - cell.row();
      const char * rslt = m_chatModel->getEditText(&ndxBack, 0);
      QColor color;

      if (rslt && (!received || (received != 'n' ? *rslt : ((MessagesModel *)m_chatModel)->isEdited(&color) == '*'))) {
        if (received == 'y') {
          qs.append("<b>").append(tr("This message was already received by your contact.")).append("</b>");
        } else {
          qs.append("<b>").append(tr("This message may already have been received by your contact.")).append("</b>");
        }
        qs.append("<br/><br/>").append(tr("Deleting it won't change that."));
        if (editAllowed) {
          QString qs2 = tr("To remove this message from your contact's chat history, you must first edit the message"
                           " erasing all text and leaving only an empty line. Push <span style=\" color:#ff0000;\">"
                           "<b>%1</b></span> to cancel this operation, or else editing the message would later be"
                           " impossible, once you have pushed <b>%2</b>. As soon as the empty line is received"
                           " by <b>%3</b>, you will be able to delete it here without seeing this warning.");
          QString nick = m_contact->m_nick.toHtmlEscaped();
          qs.append(" ").append(qs2.arg(qApp->translate("QShortcut", "No"), qApp->translate("QShortcut", "Yes"), nick));
        }
        qs.append("<br/>");
      }

      QString qs2 = tr("The message to delete is dated <span style=\" color:#ff0000;\"><b>%1</b></span>."
                       "<br/><br/>Are you sure you want to <b>destroy</b> the message"
                       " that you clicked and delete it from your local chat history?");
      qs2 = qs2.arg(m_chatModel->getSelectedText(cell.row(), false).left(SIM_SIZE_TIME - 1).replace(" ", "&nbsp;"));
      qs.append("<br/>").append(qs2).append("<br/>");

      const QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
      if (QMessageBox::question(this, tr("Delete message"), qs, buttons, QMessageBox::No) == QMessageBox::Yes) {
        removeMessages(cell.row());
      }
    } else if (a == ll || a == lm || a == la) {
      int simres;
      int nRows = m_chatModel->rowCount();
      int n = a == la ? 2000000000 : a == ll ? nRows / 2 : nRows * 2;

      if (n <= m_loadMessages) n = a == ll ? 0 : m_loadMessages ? m_loadMessages : 1;
    redo:
      if (a == ll) ui->chatView->clearColSizes();
      simres = loadMessages(n, m_contact->hasLoadError());
      if (m_contact->isTest()) Logs::getLogs()->reset();

      m_chatModel->reset();
      m_disableBottomScroll = true;
      m_chatModel->notifyRowsChanged();
      m_disableBottomScroll = false;
      if (a != ll) {
        if (simres == SIM_OK && m_chatModel->rowCount() <= nRows * 3 / 2 + 1 && n < 2000000000) {
          if (n < 500000000) {
            n *= 4;
          } else {
            n = 2000000000;
          }
          goto redo;
        }
        m_noneLoaded = false;
      } else {
        m_allLoaded = false;
        if (simres == SIM_OK) { // don't disable 'Show Less' on drain error
          m_noneLoaded = m_chatModel->rowCount() == nRows;
        } else if (n && m_chatModel->rowCount() == nRows) {
          n = 0;
          goto redo;
        }
      }
      m_chatModel->setOldRowCount(m_chatModel->rowCount());

      m_chatModel->clearCache();
      m_chatModel->setFoundIndex(-1);
      m_lastRowClicked = -1;
      if (select->hasSelection()) select->clearSelection();
      ui->chatView->resizeRowContentVisiblePart();
      ui->chatView->scrollToTop();

      if (simres == SIM_FILE_START) {
        m_allLoaded = true;
      } else if (simres != SIM_OK) {
        QString simerr = SimCore::getError(simres);
        if (simres >= ERROR_BASE_EXPAT || (!m_contact->hasLoadError() && SimParam::get("msg.errors"))) {
          bool test = m_contact->isTest();
          QString checkBox = tr("I have no idea how to fix a corrupted HTML file\n"
                                "and do not want to see this kind of error ever again.");
          QString error;
          error = test ? tr("Loading console log not successful (%1)") : tr("Loading chat history not successful (%1)");
          if (simres < ERROR_BASE_EXPAT) m_contact->setLoadError();
          if (qtfix::execMessageBox(true, error.arg(simres), simerr, this, simres < ERROR_BASE_EXPAT ? checkBox : "")) {
            SimParam::set("msg.errors", 0, false);
          }
        }
      }
    }
  }
}

void ChatFrame::onCustomContextMenuSend(const QPoint & pos)
{
  Contacts::get()->showDialog(m_contact, ui->sendButton->mapToGlobal(pos), 0);
}

void ChatFrame::onCustomContextMenuInfo(const QPoint & pos)
{
  QMenu menu(this);
  QAction * c = menu.addAction(qApp->translate("QWidgetTextControl", "&Copy"));
  QAction * s = menu.addAction(tr("&Set Info"));

  if (m_contact->isTest() || m_contact->m_info.isEmpty()) c->setEnabled(false);
  if (!m_contact->isMe()) s->setEnabled(false);
  if (QAction * a = menu.exec(ui->infoLineLabel->mapToGlobal(pos))) {
    if (a == c) {
      QString text = ui->infoLineLabel->selectedText();
      QApplication::clipboard()->setText(text.isEmpty() ? m_contact->m_info : text);
    } else if (a == s) {
      Contacts::get()->changeInfoContact(m_contact);
    }
  }
}

void ChatFrame::onCustomContextMenuConnection(const QPoint & pos)
{
  QMenu menu(this);
  QAction * c = 0;
  QAction * d = 0;
  QAction * p = 0;
  QAction * t = 0;
  QAction * i = 0;

  if (!m_contact->isTest()) {
    SimCore::E_ConnectionState state = SimCore::getConnectionState(m_contact->m_id);

    if (state == SimCore::state_disconnect) {
      c = menu.addAction(tr("&Connect"));
    } else if (state == SimCore::state_relayed_in || state == SimCore::state_relayed_out) {
      t = menu.addAction(tr("&Traverse"));
    }
    d = menu.addAction(tr("&Disconnect"));
  } else {
    p = menu.addAction(tr("&Change Proxy"));
  }
  i = menu.addAction(tr("&Info"));

  QPoint point = ui->connectionLabel->mapToGlobal(pos);
  if (QAction * a = menu.exec(point)) {
    int simres = SIM_OK;

    if (a == c) {
      sim_contact_connect_(m_contact->m_simId);
      sim_contact_disconnect_(m_contact->m_simId);
    } else if (a == d) {
      simres = sim_contact_set_(m_contact->m_simId, CONTACT_KEY_AUTH, sim_number_new(CONTACT_AUTH_DROP));
    } else if (a == p) {
      simres = sim_status_exec_(SIM_STATUS_EXEC_PROXY_DROP, 0);
    } else if (a == t) {
      simres = sim_contact_ping_(m_contact->m_simId, 0, SIM_PING_BIT_NAT);
    } else if (a == i) {
      Contacts::get()->showDialog(m_contact, point, -1);
    }

    if (simres != SIM_OK) {
      QString error;
      if (a == t) {
        error = tr("Traversal to %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
      } else {
        error = tr("Disconnecting %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
      }
      qtfix::execMessageBox(false, error, SimCore::getError(simres), this);
    }
  }
}

ChatFrames * ChatFrames::mc_chatFrames = 0;

ChatFrames::ChatFrames()
{
  mc_chatFrames = this;
  connect(SimCore::get(), SIGNAL(signalMessageReceived(unsigned, int, bool)),
          this, SLOT(onSignalMessageReceived(unsigned, int, bool)));
  connect(SimCore::get(), SIGNAL(signalMessageEdited(unsigned, int)), this, SLOT(onSignalMessageEdited(unsigned, int)));
}

ChatFrame * ChatFrames::getFrame(Contact * contact)
{
  if (!contact) return 0;

  if (!contact->m_chatFrame) {
    contact->m_chatFrame = new ChatFrame(contact, !contact->isTest() ? (BaseModel *)new MessagesModel(contact->m_id)
                                                                     : (BaseModel *)new LogsModel());
  }
  contact->m_chatFrame->readSettings();

  return contact->m_chatFrame;
}

int ChatFrames::getLastChatId()
{
  return !m_lastContactUsed.isEmpty() ? int(m_lastContactUsed[m_lastContactUsed.size() - 1]) : -1;
}

void ChatFrames::activateChat(Contact * contact, int popupNotify)
{
  if (!getFrame(contact)) return;

  if (!contact->m_chatWindow) contact->m_chatWindow = new ChatWindow;

  contact->m_chatWindow->attachFrame(contact->m_chatFrame);

  if (popupNotify != 1) {
    if (popupNotify != 3) {
      qtfix::showActivateWindow(contact->m_chatWindow, popupNotify > 0);
      ChatFrames::get()->notifyChatUsed(contact->m_id);
    } else {
      if (!(contact->m_chatWindow->windowState() & Qt::WindowMinimized)) contact->m_chatWindow->show();
    }
  } else if (!contact->m_chatWindow->isVisible()) {
    contact->m_chatFrame->setShowMinimized();
    qtfix::showMinimizedWindow(contact->m_chatWindow, SimParam::get("ui.main.sleep"));
  }
}

void ChatFrames::closeAll()
{
  log_info_("ui", "%s\n", __FUNCTION__);
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  for (unsigned i = contacts.size(); i--;) {
    if (Contact * c = contacts[i]) {
      if (c->m_chatWindow) {
        log_info_("ui", "%s '%s'\n", __FUNCTION__, c->m_nick.toUtf8().data());
        c->m_chatWindow->setQuitting();
        c->m_chatWindow->close();
        c->m_chatWindow = 0;
      }
    }
  }
}

void ChatFrames::detachAll()
{
  log_info_("ui", "%s\n", __FUNCTION__);
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  for (unsigned i = contacts.size(); i--;) {
    if (Contact * c = contacts[i]) {
      if (c->m_chatWindow) {
        log_info_("ui", "%s '%s'\n", __FUNCTION__, c->m_nick.toUtf8().data());
        c->m_chatWindow->detachFrame();
      }
    }
  }

  closeAll();
}

void ChatFrames::notifyChatUsed(unsigned id)
{
  if (!m_lastContactUsed.isEmpty() && m_lastContactUsed[m_lastContactUsed.size() - 1] == id) return;

  int i = m_lastContactUsed.indexOf(id);
  if (i >= 0) m_lastContactUsed.removeAt(i);
  m_lastContactUsed.push_back(id);
  //log_note_("ui", "%s id %d (size %d)\n", __FUNCTION__, id, m_lastContactUsed.size());
}

void ChatFrames::notifyChatStopped(unsigned id)
{
  int i = m_lastContactUsed.indexOf(id);
  if (i >= 0) m_lastContactUsed.removeAt(i);
  //log_note_("ui", "%s id %d (size %d)\n", __FUNCTION__, id, m_lastContactUsed.size());
}

void ChatFrames::onSignalMessageReceived(unsigned id, int, bool noNotify)
{
  Contact * contact = SimCore::getContact(id);
  if (!noNotify && contact && !contact->m_chatFrame) { // there is still no chat window created
    contact->setNotifications(Contact::flag_hasUnreadMsg);
  }
}

void ChatFrames::onSignalMessageEdited(unsigned id, int msgNdx)
{
  onSignalMessageReceived(id, msgNdx, false);
}
