/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014 John Preston, https://desktop.telegram.org */ #include "stdafx.h" #include "style.h" #include "lang.h" #include "boxes/confirmbox.h" #include "historywidget.h" #include "gui/filedialog.h" #include "boxes/photosendbox.h" #include "mainwidget.h" #include "window.h" #include "passcodewidget.h" #include "window.h" #include "fileuploader.h" #include "audio.h" #include "localstorage.h" // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html HistoryList::HistoryList(HistoryWidget *historyWidget, ScrollArea *scroll, History *history) : QWidget(0) , hist(history) , ySkip(0) , botInfo(history->peer->isUser() ? history->peer->asUser()->botInfo : 0) , botDescWidth(0), botDescHeight(0) , historyWidget(historyWidget) , scrollArea(scroll) , currentBlock(0) , currentItem(0) , _firstLoading(false) , _cursor(style::cur_default) , _dragAction(NoDrag) , _dragSelType(TextSelectLetters) , _dragItem(0) , _dragCursorState(HistoryDefaultCursorState) , _dragWasInactive(false) , _dragSelFrom(0) , _dragSelTo(0) , _dragSelecting(false) , _wasSelectedText(false) , _touchScroll(false) , _touchSelect(false) , _touchInProgress(false) , _touchScrollState(TouchScrollManual) , _touchPrevPosValid(false) , _touchWaitingAcceleration(false) , _touchSpeedTime(0) , _touchAccelerationTime(0) , _touchTime(0) , _menu(0) { linkTipTimer.setSingleShot(true); connect(&linkTipTimer, SIGNAL(timeout()), this, SLOT(showLinkTip())); _touchSelectTimer.setSingleShot(true); connect(&_touchSelectTimer, SIGNAL(timeout()), this, SLOT(onTouchSelect())); setAttribute(Qt::WA_AcceptTouchEvents); connect(&_touchScrollTimer, SIGNAL(timeout()), this, SLOT(onTouchScrollTimer())); _trippleClickTimer.setSingleShot(true); if (botInfo && !botInfo->inited) { App::api()->requestFullPeer(hist->peer); } setMouseTracking(true); } void HistoryList::messagesReceived(const QVector &messages) { hist->addToFront(messages); } void HistoryList::messagesReceivedDown(const QVector &messages) { hist->addToBack(messages); } void HistoryList::updateMsg(const HistoryItem *msg) { if (!msg || msg->detached() || !hist || hist != msg->history()) return; update(0, ySkip + msg->block()->y + msg->y, width(), msg->height()); } void HistoryList::paintEvent(QPaintEvent *e) { QRect r(e->rect()); bool trivial = (rect() == r); QPainter p(this); if (!trivial) { p.setClipRect(r); } if (!_firstLoading && botInfo && !botInfo->text.isEmpty() && botDescHeight > 0) { if (r.top() < botDescRect.y() + botDescRect.height() && r.bottom() > botDescRect.y()) { textstyleSet(&st::inTextStyle); App::roundRect(p, botDescRect, st::msgInBg, MessageInCorners, &st::msgInShadow); p.setFont(st::msgNameFont->f); p.setPen(st::black->p); p.drawText(botDescRect.left() + st::msgPadding.left(), botDescRect.top() + st::msgPadding.top() + st::msgNameFont->ascent, lang(lng_bot_description)); botInfo->text.draw(p, botDescRect.left() + st::msgPadding.left(), botDescRect.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip, botDescWidth); textstyleRestore(); } } else if (_firstLoading || hist->isEmpty()) { QPoint dogPos((width() - st::msgDogImg.pxWidth()) / 2, ((height() - st::msgDogImg.pxHeight()) * 4) / 9); p.drawPixmap(dogPos, *cChatDogImage()); } if (!_firstLoading && !hist->isEmpty()) { adjustCurrent(r.top()); HistoryBlock *block = (*hist)[currentBlock]; HistoryItem *item = (*block)[currentItem]; SelectedItems::const_iterator selEnd = _selected.cend(); bool hasSel = !_selected.isEmpty(); int32 drawToY = r.bottom() - ySkip; int32 selfromy = 0, seltoy = 0; if (_dragSelFrom && _dragSelTo) { selfromy = _dragSelFrom->y + _dragSelFrom->block()->y; seltoy = _dragSelTo->y + _dragSelTo->block()->y + _dragSelTo->height(); } int32 iBlock = currentBlock, iItem = currentItem, y = block->y + item->y; p.translate(0, ySkip + y); while (y < drawToY) { int32 h = item->height(); uint32 sel = 0; if (y >= selfromy && y < seltoy) { sel = (_dragSelecting && !item->serviceMsg() && item->id > 0) ? FullItemSel : 0; } else if (hasSel) { SelectedItems::const_iterator i = _selected.constFind(item); if (i != selEnd) { sel = i.value(); } } item->draw(p, sel); p.translate(0, h); ++iItem; if (iItem == block->size()) { iItem = 0; ++iBlock; if (iBlock == hist->size()) { break; } block = (*hist)[iBlock]; } item = (*block)[iItem]; y += h; } } } bool HistoryList::event(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { QTouchEvent *ev = static_cast(e); if (ev->device()->type() == QTouchDevice::TouchScreen) { touchEvent(ev); return true; } } return QWidget::event(e); } void HistoryList::onTouchScrollTimer() { uint64 nowTime = getms(); if (_touchScrollState == TouchScrollAcceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) { _touchScrollState = TouchScrollManual; touchResetSpeed(); } else if (_touchScrollState == TouchScrollAuto || _touchScrollState == TouchScrollAcceleration) { int32 elapsed = int32(nowTime - _touchTime); QPoint delta = _touchSpeed * elapsed / 1000; bool hasScrolled = historyWidget->touchScroll(delta); if (_touchSpeed.isNull() || !hasScrolled) { _touchScrollState = TouchScrollManual; _touchScroll = false; _touchScrollTimer.stop(); } else { _touchTime = nowTime; } touchDeaccelerate(elapsed); } } void HistoryList::touchUpdateSpeed() { const uint64 nowTime = getms(); if (_touchPrevPosValid) { const int elapsed = nowTime - _touchSpeedTime; if (elapsed) { const QPoint newPixelDiff = (_touchPos - _touchPrevPos); const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed); // fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because // of a small horizontal offset when scrolling vertically const int newSpeedY = (qAbs(pixelsPerSecond.y()) > FingerAccuracyThreshold) ? pixelsPerSecond.y() : 0; const int newSpeedX = (qAbs(pixelsPerSecond.x()) > FingerAccuracyThreshold) ? pixelsPerSecond.x() : 0; if (_touchScrollState == TouchScrollAuto) { const int oldSpeedY = _touchSpeed.y(); const int oldSpeedX = _touchSpeed.x(); if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0) && (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) { _touchSpeed.setY(snap((oldSpeedY + (newSpeedY / 4)), -MaxScrollAccelerated, +MaxScrollAccelerated)); _touchSpeed.setX(snap((oldSpeedX + (newSpeedX / 4)), -MaxScrollAccelerated, +MaxScrollAccelerated)); } else { _touchSpeed = QPoint(); } } else { // we average the speed to avoid strange effects with the last delta if (!_touchSpeed.isNull()) { _touchSpeed.setX(snap((_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), -MaxScrollFlick, +MaxScrollFlick)); _touchSpeed.setY(snap((_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), -MaxScrollFlick, +MaxScrollFlick)); } else { _touchSpeed = QPoint(newSpeedX, newSpeedY); } } } } else { _touchPrevPosValid = true; } _touchSpeedTime = nowTime; _touchPrevPos = _touchPos; } void HistoryList::touchResetSpeed() { _touchSpeed = QPoint(); _touchPrevPosValid = false; } void HistoryList::touchDeaccelerate(int32 elapsed) { int32 x = _touchSpeed.x(); int32 y = _touchSpeed.y(); _touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed)); _touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed)); } void HistoryList::touchEvent(QTouchEvent *e) { const Qt::TouchPointStates &states(e->touchPointStates()); if (e->type() == QEvent::TouchCancel) { // cancel if (!_touchInProgress) return; _touchInProgress = false; _touchSelectTimer.stop(); _touchScroll = _touchSelect = false; _touchScrollState = TouchScrollManual; dragActionCancel(); return; } if (!e->touchPoints().isEmpty()) { _touchPrevPos = _touchPos; _touchPos = e->touchPoints().cbegin()->screenPos().toPoint(); } switch (e->type()) { case QEvent::TouchBegin: if (_menu) { e->accept(); return; // ignore mouse press, that was hiding context menu } if (_touchInProgress) return; if (e->touchPoints().isEmpty()) return; _touchInProgress = true; if (_touchScrollState == TouchScrollAuto) { _touchScrollState = TouchScrollAcceleration; _touchWaitingAcceleration = true; _touchAccelerationTime = getms(); touchUpdateSpeed(); _touchStart = _touchPos; } else { _touchScroll = false; _touchSelectTimer.start(QApplication::startDragTime()); } _touchSelect = false; _touchStart = _touchPrevPos = _touchPos; break; case QEvent::TouchUpdate: if (!_touchInProgress) return; if (_touchSelect) { dragActionUpdate(_touchPos); } else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { _touchSelectTimer.stop(); _touchScroll = true; touchUpdateSpeed(); } if (_touchScroll) { if (_touchScrollState == TouchScrollManual) { touchScrollUpdated(_touchPos); } else if (_touchScrollState == TouchScrollAcceleration) { touchUpdateSpeed(); _touchAccelerationTime = getms(); if (_touchSpeed.isNull()) { _touchScrollState = TouchScrollManual; } } } break; case QEvent::TouchEnd: if (!_touchInProgress) return; _touchInProgress = false; if (_touchSelect) { dragActionFinish(_touchPos, Qt::RightButton); QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos); showContextMenu(&contextMenu, true); _touchScroll = false; } else if (_touchScroll) { if (_touchScrollState == TouchScrollManual) { _touchScrollState = TouchScrollAuto; _touchPrevPosValid = false; _touchScrollTimer.start(15); _touchTime = getms(); } else if (_touchScrollState == TouchScrollAuto) { _touchScrollState = TouchScrollManual; _touchScroll = false; touchResetSpeed(); } else if (_touchScrollState == TouchScrollAcceleration) { _touchScrollState = TouchScrollAuto; _touchWaitingAcceleration = false; _touchPrevPosValid = false; } } else { // one short tap -- like mouse click dragActionStart(_touchPos); dragActionFinish(_touchPos); } _touchSelectTimer.stop(); _touchSelect = false; break; } } void HistoryList::mouseMoveEvent(QMouseEvent *e) { if (!(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) && (textlnkDown() || _dragAction != NoDrag)) { mouseReleaseEvent(e); } dragActionUpdate(e->globalPos()); } void HistoryList::dragActionUpdate(const QPoint &screenPos) { _dragPos = screenPos; onUpdateSelected(); } void HistoryList::touchScrollUpdated(const QPoint &screenPos) { _touchPos = screenPos; historyWidget->touchScroll(_touchPos - _touchPrevPos); touchUpdateSpeed(); } QPoint HistoryList::mapMouseToItem(QPoint p, HistoryItem *item) { if (!item || item->detached()) return QPoint(0, 0); p.setY(p.y() - (height() - hist->height - st::historyPadding) - item->block()->y - item->y); return p; } void HistoryList::mousePressEvent(QMouseEvent *e) { if (_menu) { e->accept(); return; // ignore mouse press, that was hiding context menu } dragActionStart(e->globalPos(), e->button()); } void HistoryList::dragActionStart(const QPoint &screenPos, Qt::MouseButton button) { dragActionUpdate(screenPos); if (button != Qt::LeftButton) return; if (App::pressedItem() != App::hoveredItem()) { updateMsg(App::pressedItem()); App::pressedItem(App::hoveredItem()); updateMsg(App::pressedItem()); } if (textlnkDown() != textlnkOver()) { updateMsg(App::pressedLinkItem()); textlnkDown(textlnkOver()); App::pressedLinkItem(App::hoveredLinkItem()); updateMsg(App::pressedLinkItem()); updateMsg(App::pressedItem()); } _dragAction = NoDrag; _dragItem = App::mousedItem(); _dragStartPos = mapMouseToItem(mapFromGlobal(screenPos), _dragItem); _dragWasInactive = App::wnd()->inactivePress(); if (_dragWasInactive) App::wnd()->inactivePress(false); if (textlnkDown()) { _dragAction = PrepareDrag; } else if (!_selected.isEmpty()) { if (_selected.cbegin().value() == FullItemSel) { if (_selected.constFind(_dragItem) != _selected.cend() && App::hoveredItem()) { _dragAction = PrepareDrag; // start items drag } else if (!_dragWasInactive) { _dragAction = PrepareSelect; // start items select } } } if (_dragAction == NoDrag && _dragItem) { bool afterDragSymbol, uponSymbol; uint16 symbol; if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) { _dragItem->getSymbol(symbol, afterDragSymbol, uponSymbol, _dragStartPos.x(), _dragStartPos.y()); if (uponSymbol) { uint32 selStatus = (symbol << 16) | symbol; if (selStatus != FullItemSel && (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel)) { if (!_selected.isEmpty()) { updateMsg(_selected.cbegin().key()); _selected.clear(); } _selected.insert(_dragItem, selStatus); _dragSymbol = symbol; _dragAction = Selecting; _dragSelType = TextSelectParagraphs; dragActionUpdate(_dragPos); _trippleClickTimer.start(QApplication::doubleClickInterval()); } } } else if (App::pressedItem()) { _dragItem->getSymbol(symbol, afterDragSymbol, uponSymbol, _dragStartPos.x(), _dragStartPos.y()); } if (_dragSelType != TextSelectParagraphs) { if (App::pressedItem()) { _dragSymbol = symbol; bool uponSelected = uponSymbol; if (uponSelected) { if (_selected.isEmpty() || _selected.cbegin().value() == FullItemSel || _selected.cbegin().key() != _dragItem ) { uponSelected = false; } else { uint16 selFrom = (_selected.cbegin().value() >> 16) & 0xFFFF, selTo = _selected.cbegin().value() & 0xFFFF; if (_dragSymbol < selFrom || _dragSymbol >= selTo) { uponSelected = false; } } } if (uponSelected) { _dragAction = PrepareDrag; // start text drag } else if (!_dragWasInactive) { if (dynamic_cast(App::pressedItem()->getMedia()) || _dragCursorState == HistoryInDateCursorState) { _dragAction = PrepareDrag; // start sticker drag or by-date drag } else { if (afterDragSymbol) ++_dragSymbol; uint32 selStatus = (_dragSymbol << 16) | _dragSymbol; if (selStatus != FullItemSel && (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel)) { if (!_selected.isEmpty()) { updateMsg(_selected.cbegin().key()); _selected.clear(); } _selected.insert(_dragItem, selStatus); _dragAction = Selecting; updateMsg(_dragItem); } else { _dragAction = PrepareSelect; } } } } else if (!_dragWasInactive) { _dragAction = PrepareSelect; // start items select } } } if (!_dragItem) { _dragAction = NoDrag; } else if (_dragAction == NoDrag) { _dragItem = 0; } } void HistoryList::dragActionCancel() { _dragItem = 0; _dragAction = NoDrag; _dragStartPos = QPoint(0, 0); _dragSelFrom = _dragSelTo = 0; _wasSelectedText = false; historyWidget->noSelectingScroll(); } void HistoryList::onDragExec() { if (_dragAction != Dragging) return; bool uponSelected = false; if (_dragItem) { bool afterDragSymbol; uint16 symbol; if (!_selected.isEmpty() && _selected.cbegin().value() == FullItemSel) { uponSelected = _selected.contains(_dragItem); } else { _dragItem->getSymbol(symbol, afterDragSymbol, uponSelected, _dragStartPos.x(), _dragStartPos.y()); if (uponSelected) { if (_selected.isEmpty() || _selected.cbegin().value() == FullItemSel || _selected.cbegin().key() != _dragItem ) { uponSelected = false; } else { uint16 selFrom = (_selected.cbegin().value() >> 16) & 0xFFFF, selTo = _selected.cbegin().value() & 0xFFFF; if (symbol < selFrom || symbol >= selTo) { uponSelected = false; } } } } } QString sel; QList urls; if (uponSelected) { sel = getSelectedText(); } else if (textlnkDown()) { sel = textlnkDown()->encoded(); if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') { // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o } } if (!sel.isEmpty()) { updateDragSelection(0, 0, false); historyWidget->noSelectingScroll(); QDrag *drag = new QDrag(App::wnd()); QMimeData *mimeData = new QMimeData; mimeData->setText(sel); if (!urls.isEmpty()) mimeData->setUrls(urls); if (uponSelected && !_selected.isEmpty() && _selected.cbegin().value() == FullItemSel && cWideMode()) { mimeData->setData(qsl("application/x-td-forward-selected"), "1"); } drag->setMimeData(mimeData); drag->exec(Qt::CopyAction); if (App::main()) App::main()->updateAfterDrag(); return; } else { HistoryItem *pressedLnkItem = App::pressedLinkItem(), *pressedItem = App::pressedItem(); QLatin1String lnkType = (textlnkDown() && pressedLnkItem) ? textlnkDown()->type() : qstr(""); bool lnkPhoto = (lnkType == qstr("PhotoLink")), lnkVideo = (lnkType == qstr("VideoOpenLink")), lnkAudio = (lnkType == qstr("AudioOpenLink")), lnkDocument = (lnkType == qstr("DocumentOpenLink")), lnkContact = (lnkType == qstr("PeerLink") && dynamic_cast(pressedLnkItem->getMedia())), dragSticker = dynamic_cast(pressedItem ? pressedItem->getMedia() : 0), dragByDate = (_dragCursorState == HistoryInDateCursorState); if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact || dragSticker || dragByDate) { QDrag *drag = new QDrag(App::wnd()); QMimeData *mimeData = new QMimeData; if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact) { mimeData->setData(qsl("application/x-td-forward-pressed-link"), "1"); } else { mimeData->setData(qsl("application/x-td-forward-pressed"), "1"); } if (lnkDocument) { QString already = static_cast(textlnkDown().data())->document()->already(true); if (!already.isEmpty()) { QList urls; urls.push_back(QUrl::fromLocalFile(already)); mimeData->setUrls(urls); } } drag->setMimeData(mimeData); drag->exec(Qt::CopyAction); if (App::main()) App::main()->updateAfterDrag(); return; } } } void HistoryList::itemRemoved(HistoryItem *item) { SelectedItems::iterator i = _selected.find(item); if (i != _selected.cend()) { _selected.erase(i); historyWidget->updateTopBarSelection(); } if (_dragAction == NoDrag) return; if (_dragItem == item) { dragActionCancel(); } onUpdateSelected(); if (_dragSelFrom == item) _dragSelFrom = 0; if (_dragSelTo == item) _dragSelTo = 0; updateDragSelection(_dragSelFrom, _dragSelTo, _dragSelecting, true); } void HistoryList::itemReplaced(HistoryItem *oldItem, HistoryItem *newItem) { if (_dragItem == oldItem) _dragItem = newItem; SelectedItems::iterator i = _selected.find(oldItem); if (i != _selected.cend()) { uint32 v = i.value(); _selected.erase(i); _selected.insert(newItem, v); } if (_dragSelFrom == oldItem) _dragSelFrom = newItem; if (_dragSelTo == oldItem) _dragSelTo = newItem; } void HistoryList::dragActionFinish(const QPoint &screenPos, Qt::MouseButton button) { TextLinkPtr needClick; dragActionUpdate(screenPos); if (textlnkOver()) { if (textlnkDown() == textlnkOver() && _dragAction != Dragging) { needClick = textlnkDown(); QLatin1String lnkType = needClick->type(); bool lnkPhoto = (lnkType == qstr("PhotoLink")), lnkVideo = (lnkType == qstr("VideoOpenLink")), lnkAudio = (lnkType == qstr("AudioOpenLink")), lnkDocument = (lnkType == qstr("DocumentOpenLink")), lnkContact = (lnkType == qstr("PeerLink") && dynamic_cast(App::pressedLinkItem() ? App::pressedLinkItem()->getMedia() : 0)); if (_dragAction == PrepareDrag && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullItemSel && button != Qt::RightButton) { if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument || lnkContact) { needClick = TextLinkPtr(); } } } } if (textlnkDown()) { updateMsg(App::pressedLinkItem()); textlnkDown(TextLinkPtr()); App::pressedLinkItem(0); if (!textlnkOver() && _cursor != style::cur_default) { _cursor = style::cur_default; setCursor(_cursor); } } if (App::pressedItem()) { updateMsg(App::pressedItem()); App::pressedItem(0); } _wasSelectedText = false; if (needClick) { DEBUG_LOG(("Clicked link: %1 (%2) %3").arg(needClick->text()).arg(needClick->readable()).arg(needClick->encoded())); needClick->onClick(button); dragActionCancel(); return; } if (_dragAction == PrepareSelect && !_dragWasInactive && !_selected.isEmpty() && _selected.cbegin().value() == FullItemSel) { SelectedItems::iterator i = _selected.find(_dragItem); if (i == _selected.cend() && !_dragItem->serviceMsg() && _dragItem->id > 0) { if (_selected.size() < MaxSelectedItems) { if (!_selected.isEmpty() && _selected.cbegin().value() != FullItemSel) { _selected.clear(); } _selected.insert(_dragItem, FullItemSel); } } else { _selected.erase(i); } updateMsg(_dragItem); } else if (_dragAction == PrepareDrag && !_dragWasInactive && button != Qt::RightButton) { SelectedItems::iterator i = _selected.find(_dragItem); if (i != _selected.cend() && i.value() == FullItemSel) { _selected.erase(i); updateMsg(_dragItem); } else if (i == _selected.cend() && !_dragItem->serviceMsg() && _dragItem->id > 0 && !_selected.isEmpty() && _selected.cbegin().value() == FullItemSel) { if (_selected.size() < MaxSelectedItems) { _selected.insert(_dragItem, FullItemSel); updateMsg(_dragItem); } } else { _selected.clear(); parentWidget()->update(); } } else if (_dragAction == Selecting) { if (_dragSelFrom && _dragSelTo) { applyDragSelection(); _dragSelFrom = _dragSelTo = 0; } else if (!_selected.isEmpty() && !_dragWasInactive) { uint32 sel = _selected.cbegin().value(); if (sel != FullItemSel && (sel & 0xFFFF) == ((sel >> 16) & 0xFFFF)) { _selected.clear(); App::wnd()->setInnerFocus(); } } } _dragAction = NoDrag; _dragSelType = TextSelectLetters; historyWidget->noSelectingScroll(); historyWidget->updateTopBarSelection(); } void HistoryList::mouseReleaseEvent(QMouseEvent *e) { dragActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); } } void HistoryList::mouseDoubleClickEvent(QMouseEvent *e) { if (!hist) return; if (((_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullItemSel) || (_dragAction == NoDrag && (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel))) && _dragSelType == TextSelectLetters && _dragItem) { bool afterDragSymbol, uponSelected; uint16 symbol; _dragItem->getSymbol(symbol, afterDragSymbol, uponSelected, _dragStartPos.x(), _dragStartPos.y()); if (uponSelected) { _dragSymbol = symbol; _dragSelType = TextSelectWords; if (_dragAction == NoDrag) { _dragAction = Selecting; uint32 selStatus = (symbol << 16) | symbol; if (!_selected.isEmpty()) { updateMsg(_selected.cbegin().key()); _selected.clear(); } _selected.insert(_dragItem, selStatus); } mouseMoveEvent(e); _trippleClickPoint = e->globalPos(); _trippleClickTimer.start(QApplication::doubleClickInterval()); } } else { mousePressEvent(e); } } void HistoryList::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (_menu) { _menu->deleteLater(); _menu = 0; } if (e->reason() == QContextMenuEvent::Mouse) { dragActionUpdate(e->globalPos()); } // -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items int32 isUponSelected = 0, hasSelected = 0;; if (!_selected.isEmpty()) { isUponSelected = -1; if (_selected.cbegin().value() == FullItemSel) { hasSelected = 2; if (App::hoveredItem() && _selected.constFind(App::hoveredItem()) != _selected.cend()) { isUponSelected = 2; } else { isUponSelected = -2; } } else { uint16 symbol, selFrom = (_selected.cbegin().value() >> 16) & 0xFFFF, selTo = _selected.cbegin().value() & 0xFFFF; hasSelected = (selTo > selFrom) ? 1 : 0; if (_dragItem && _dragItem == App::hoveredItem()) { QPoint mousePos(mapMouseToItem(mapFromGlobal(_dragPos), _dragItem)); bool afterDragSymbol, uponSymbol; _dragItem->getSymbol(symbol, afterDragSymbol, uponSymbol, mousePos.x(), mousePos.y()); if (uponSymbol && symbol >= selFrom && symbol < selTo) { isUponSelected = 1; } } } } if (showFromTouch && hasSelected && isUponSelected < hasSelected) { isUponSelected = hasSelected; } _contextMenuLnk = textlnkOver(); HistoryItem *item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem(); PhotoLink *lnkPhoto = dynamic_cast(_contextMenuLnk.data()); VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument) { _menu = new ContextMenu(historyWidget); if (isUponSelected > 0) { _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); } if (item && item->id > 0 && isUponSelected != 2 && isUponSelected != -2 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { _menu->addAction(lang(lng_context_reply_msg), historyWidget, SLOT(onReplyToMessage())); } if (lnkPhoto) { _menu->addAction(lang(lng_context_open_image), this, SLOT(openContextUrl()))->setEnabled(true); _menu->addAction(lang(lng_context_save_image), this, SLOT(saveContextImage()))->setEnabled(true); _menu->addAction(lang(lng_context_copy_image), this, SLOT(copyContextImage()))->setEnabled(true); } else { if ((lnkVideo && lnkVideo->video()->loader) || (lnkAudio && lnkAudio->audio()->loader) || (lnkDocument && lnkDocument->document()->loader)) { _menu->addAction(lang(lng_context_cancel_download), this, SLOT(cancelContextDownload()))->setEnabled(true); } else { if ((lnkVideo && !lnkVideo->video()->already(true).isEmpty()) || (lnkAudio && !lnkAudio->audio()->already(true).isEmpty()) || (lnkDocument && !lnkDocument->document()->already(true).isEmpty())) { _menu->addAction(lang(cPlatform() == dbipMac ? lng_context_show_in_finder : lng_context_show_in_folder), this, SLOT(showContextInFolder()))->setEnabled(true); } _menu->addAction(lang(lnkVideo ? lng_context_open_video : (lnkAudio ? lng_context_open_audio : lng_context_open_file)), this, SLOT(openContextFile()))->setEnabled(true); _menu->addAction(lang(lnkVideo ? lng_context_save_video : (lnkAudio ? lng_context_save_audio : lng_context_save_file)), this, SLOT(saveContextFile()))->setEnabled(true); } } if (isUponSelected > 1) { _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); } else if (App::hoveredLinkItem()) { if (isUponSelected != -2 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { if (dynamic_cast(App::hoveredLinkItem()) && App::hoveredLinkItem()->id > 0) { _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); } _menu->addAction(lang(lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); } if (App::hoveredLinkItem()->id > 0 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); } App::contextItem(App::hoveredLinkItem()); } } else { // maybe cursor on some text history item? bool canDelete = (item && item->itemType() == HistoryItem::MsgType) && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned); bool canForward = canDelete && (item->id > 0) && !item->serviceMsg(); HistoryMessage *msg = dynamic_cast(item); HistoryServiceMsg *srv = dynamic_cast(item); if (isUponSelected > 0) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); if (item && item->id > 0 && isUponSelected != 2 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { _menu->addAction(lang(lng_context_reply_msg), historyWidget, SLOT(onReplyToMessage())); } } else { if (item && item->id > 0 && isUponSelected != -2 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(lng_context_reply_msg), historyWidget, SLOT(onReplyToMessage())); } if (item && !isUponSelected && !_contextMenuLnk) { if (HistorySticker *sticker = dynamic_cast(msg ? msg->getMedia() : 0)) { DocumentData *doc = sticker->document(); if (doc && doc->sticker() && doc->sticker()->set.type() != mtpc_inputStickerSetEmpty) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(doc->sticker()->setInstalled() ? lng_context_pack_info : lng_context_pack_add), historyWidget, SLOT(onStickerPackInfo())); } } QString contextMenuText = item->selectedText(FullItemSel); if (!contextMenuText.isEmpty() && (!msg || !msg->getMedia() || msg->getMedia()->type() != MediaTypeSticker)) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(lng_context_copy_text), this, SLOT(copyContextText()))->setEnabled(true); } } } if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { if (!_menu) _menu = new ContextMenu(historyWidget); _menu->addAction(lang(lng_context_open_link), this, SLOT(openContextUrl()))->setEnabled(true); _menu->addAction(lang(lng_context_copy_link), this, SLOT(copyContextUrl()))->setEnabled(true); } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { if (!_menu) _menu = new ContextMenu(historyWidget); _menu->addAction(lang(lng_context_open_email), this, SLOT(openContextUrl()))->setEnabled(true); _menu->addAction(lang(lng_context_copy_email), this, SLOT(copyContextUrl()))->setEnabled(true); } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { if (!_menu) _menu = new ContextMenu(historyWidget); _menu->addAction(lang(lng_context_open_mention), this, SLOT(openContextUrl()))->setEnabled(true); _menu->addAction(lang(lng_context_copy_mention), this, SLOT(copyContextUrl()))->setEnabled(true); } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { if (!_menu) _menu = new ContextMenu(historyWidget); _menu->addAction(lang(lng_context_open_hashtag), this, SLOT(openContextUrl()))->setEnabled(true); _menu->addAction(lang(lng_context_copy_hashtag), this, SLOT(copyContextUrl()))->setEnabled(true); } else { } if (isUponSelected > 1) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); } else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->id > 0)) { if (!_menu) _menu = new ContextMenu(this); if (isUponSelected != -2) { if (canForward) { _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); } if (canDelete) { _menu->addAction(lang((msg && msg->uploading()) ? lng_context_cancel_upload : lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); } } if (item->id > 0 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); } } else { if (App::mousedItem() && App::mousedItem()->itemType() == HistoryItem::MsgType && App::mousedItem()->id > 0 && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned)) { if (!_menu) _menu = new ContextMenu(this); _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); item = App::mousedItem(); } } App::contextItem(item); } if (_menu) { _menu->deleteOnHide(); connect(_menu, SIGNAL(destroyed(QObject*)), this, SLOT(onMenuDestroy(QObject*))); _menu->popup(e->globalPos()); e->accept(); } } void HistoryList::onMenuDestroy(QObject *obj) { if (_menu == obj) { _menu = 0; } } void HistoryList::copySelectedText() { QString sel = getSelectedText(); if (!sel.isEmpty()) { QApplication::clipboard()->setText(sel); } } void HistoryList::openContextUrl() { HistoryItem *was = App::hoveredLinkItem(); App::hoveredLinkItem(App::contextItem()); _contextMenuLnk->onClick(Qt::LeftButton); App::hoveredLinkItem(was); } void HistoryList::copyContextUrl() { QString enc = _contextMenuLnk->encoded(); if (!enc.isEmpty()) { QApplication::clipboard()->setText(enc); } } void HistoryList::saveContextImage() { PhotoLink *lnk = dynamic_cast(_contextMenuLnk.data()); if (!lnk) return; PhotoData *photo = lnk->photo(); if (!photo || !photo->date || !photo->full->loaded()) return; QString file; if (filedialogGetSaveFile(file, lang(lng_save_photo), qsl("JPEG Image (*.jpg);;All files (*.*)"), filedialogDefaultName(qsl("photo"), qsl(".jpg")))) { if (!file.isEmpty()) { photo->full->pix().toImage().save(file, "JPG"); } } } void HistoryList::copyContextImage() { PhotoLink *lnk = dynamic_cast(_contextMenuLnk.data()); if (!lnk) return; PhotoData *photo = lnk->photo(); if (!photo || !photo->date || !photo->full->loaded()) return; QApplication::clipboard()->setPixmap(photo->full->pix()); } void HistoryList::cancelContextDownload() { VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); mtpFileLoader *loader = lnkVideo ? lnkVideo->video()->loader : (lnkAudio ? lnkAudio->audio()->loader : (lnkDocument ? lnkDocument->document()->loader : 0)); if (loader) loader->cancel(); } void HistoryList::showContextInFolder() { VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); QString already = lnkVideo ? lnkVideo->video()->already(true) : (lnkAudio ? lnkAudio->audio()->already(true) : (lnkDocument ? lnkDocument->document()->already(true) : QString())); if (!already.isEmpty()) psShowInFolder(already); } void HistoryList::openContextFile() { VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); if (lnkVideo) VideoOpenLink(lnkVideo->video()).onClick(Qt::LeftButton); if (lnkAudio) AudioOpenLink(lnkAudio->audio()).onClick(Qt::LeftButton); if (lnkDocument) DocumentOpenLink(lnkDocument->document()).onClick(Qt::LeftButton); } void HistoryList::saveContextFile() { VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); if (lnkVideo) VideoSaveLink::doSave(lnkVideo->video(), true); if (lnkAudio) AudioSaveLink::doSave(lnkAudio->audio(), true); if (lnkDocument) DocumentSaveLink::doSave(lnkDocument->document(), true); } void HistoryList::copyContextText() { HistoryItem *item = App::contextItem(); if (item && item->itemType() != HistoryItem::MsgType) { item = 0; } if (!item) return; QString contextMenuText = item->selectedText(FullItemSel); if (!contextMenuText.isEmpty()) { QApplication::clipboard()->setText(contextMenuText); } } void HistoryList::resizeEvent(QResizeEvent *e) { onUpdateSelected(); } QString HistoryList::getSelectedText() const { SelectedItems sel = _selected; if (_dragAction == Selecting && _dragSelFrom && _dragSelTo) { applyDragSelection(&sel); } if (sel.isEmpty()) return QString(); if (sel.cbegin().value() != FullItemSel) { return sel.cbegin().key()->selectedText(sel.cbegin().value()); } int32 fullSize = 0; QString timeFormat(qsl(", [dd.MM.yy hh:mm]\n")); QMap texts; for (SelectedItems::const_iterator i = sel.cbegin(), e = sel.cend(); i != e; ++i) { HistoryItem *item = i.key(); if (item->detached()) continue; QString text, sel = item->selectedText(FullItemSel), time = item->date.toString(timeFormat); int32 size = item->from()->name.size() + time.size() + sel.size(); text.reserve(size); texts.insert(item->y + item->block()->y, text.append(item->from()->name).append(time).append(sel)); fullSize += size; } QString result, sep(qsl("\n\n")); result.reserve(fullSize + (texts.size() - 1) * 2); for (QMap::const_iterator i = texts.cbegin(), e = texts.cend(); i != e; ++i) { result.append(i.value()); if (i + 1 != e) { result.append(sep); } } return result; } void HistoryList::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { historyWidget->onListEscapePressed(); } else if (e == QKeySequence::Copy && !_selected.isEmpty()) { copySelectedText(); } else if (e == QKeySequence::Delete) { historyWidget->onDeleteSelected(); } else { e->ignore(); } } int32 HistoryList::recountHeight(HistoryItem *resizedItem) { int32 st = hist->lastScrollTop; int32 ph = scrollArea->height(), minadd = 0; int32 wasYSkip = ph - (hist->height + st::historyPadding); if (botInfo && !botInfo->text.isEmpty()) { minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + botDescHeight; } if (wasYSkip < minadd) wasYSkip = minadd; hist->geomResize(scrollArea->width(), &st, resizedItem); updateBotInfo(false); if (botInfo && !botInfo->text.isEmpty()) { int32 tw = scrollArea->width() - st::msgMargin.left() - st::msgMargin.right(); if (tw > st::msgMaxWidth) tw = st::msgMaxWidth; tw -= st::msgPadding.left() + st::msgPadding.right(); int32 mw = qMax(botInfo->text.maxWidth(), st::msgNameFont->m.width(lang(lng_bot_description))); if (tw > mw) tw = mw; botDescWidth = tw; botDescHeight = botInfo->text.countHeight(botDescWidth); int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom(); int32 descAtX = (scrollArea->width() - botDescWidth) / 2 - st::msgPadding.left(); int32 descAtY = qMin(ySkip - descH, qMax(0, (scrollArea->height() - descH) / 2)) + st::msgMargin.top(); botDescRect = QRect(descAtX, descAtY, botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom()); } else { botDescWidth = botDescHeight = 0; botDescRect = QRect(); } int32 newYSkip = ph - (hist->height + st::historyPadding); if (botInfo && !botInfo->text.isEmpty()) { minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + botDescHeight; } if (newYSkip < minadd) newYSkip = minadd; return st + (newYSkip - wasYSkip); } void HistoryList::updateBotInfo(bool recount) { int32 newh = 0; if (botInfo && !botInfo->description.isEmpty()) { if (botInfo->text.isEmpty()) { botInfo->text.setText(st::msgFont, botInfo->description, _historyBotOptions); if (recount) { int32 tw = scrollArea->width() - st::msgMargin.left() - st::msgMargin.right(); if (tw > st::msgMaxWidth) tw = st::msgMaxWidth; tw -= st::msgPadding.left() + st::msgPadding.right(); int32 mw = qMax(botInfo->text.maxWidth(), st::msgNameFont->m.width(lang(lng_bot_description))); if (tw > mw) tw = mw; botDescWidth = tw; newh = botInfo->text.countHeight(botDescWidth); } } else if (recount) { newh = botDescHeight; } } if (recount) { if (botDescHeight != newh) { botDescHeight = newh; updateSize(); } if (botDescHeight > 0) { int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom(); int32 descAtX = (scrollArea->width() - botDescWidth) / 2 - st::msgPadding.left(); int32 descAtY = qMin(ySkip - descH, (scrollArea->height() - descH) / 2) + st::msgMargin.top(); botDescRect = QRect(descAtX, descAtY, botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom()); } else { botDescWidth = 0; botDescRect = QRect(); } } } bool HistoryList::wasSelectedText() const { return _wasSelectedText; } void HistoryList::setFirstLoading(bool loading) { _firstLoading = loading; update(); } void HistoryList::updateSize() { int32 ph = scrollArea->height(), minadd = 0; int32 newYSkip = ph - (hist->height + st::historyPadding); if (botInfo && !botInfo->text.isEmpty()) { minadd = st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + botDescHeight; } if (newYSkip < minadd) newYSkip = minadd; if (botDescHeight > 0) { int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + botDescHeight + st::msgPadding.bottom() + st::msgMargin.bottom(); int32 descAtX = (scrollArea->width() - botDescWidth) / 2 - st::msgPadding.left(); int32 descAtY = qMin(newYSkip - descH, qMax(0, (scrollArea->height() - descH) / 2)) + st::msgMargin.top(); botDescRect = QRect(descAtX, descAtY, botDescWidth + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom()); } int32 yAdded = newYSkip - ySkip; ySkip = newYSkip; int32 nh = hist->height + st::historyPadding + ySkip; if (width() != scrollArea->width() || height() != nh) { resize(scrollArea->width(), nh); dragActionUpdate(QCursor::pos()); } else { update(); } } void HistoryList::enterEvent(QEvent *e) { return QWidget::enterEvent(e); } void HistoryList::leaveEvent(QEvent *e) { if (textlnkOver()) { updateMsg(App::hoveredItem()); updateMsg(App::hoveredLinkItem()); textlnkOver(TextLinkPtr()); App::hoveredLinkItem(0); App::hoveredItem(0); if (!textlnkDown() && _cursor != style::cur_default) { _cursor = style::cur_default; setCursor(_cursor); } } return QWidget::leaveEvent(e); } HistoryList::~HistoryList() { delete _menu; _dragAction = NoDrag; } void HistoryList::adjustCurrent(int32 y) { if (hist->isEmpty()) return; if (currentBlock >= hist->size()) { currentBlock = hist->size() - 1; currentItem = 0; } while ((*hist)[currentBlock]->y + ySkip > y && currentBlock > 0) { --currentBlock; currentItem = 0; } while ((*hist)[currentBlock]->y + (*hist)[currentBlock]->height + ySkip <= y && currentBlock + 1 < hist->size()) { ++currentBlock; currentItem = 0; } HistoryBlock *block = (*hist)[currentBlock]; if (currentItem >= block->size()) { currentItem = block->size() - 1; } int32 by = block->y; while ((*block)[currentItem]->y + by + ySkip > y && currentItem > 0) { --currentItem; } while ((*block)[currentItem]->y + (*block)[currentItem]->height() + by + ySkip <= y && currentItem + 1 < block->size()) { ++currentItem; } } HistoryItem *HistoryList::prevItem(HistoryItem *item) { if (!item) return 0; HistoryBlock *block = item->block(); int32 blockIndex = hist->indexOf(block), itemIndex = block->indexOf(item); if (blockIndex < 0 || itemIndex < 0) return 0; if (itemIndex > 0) { return (*block)[itemIndex - 1]; } if (blockIndex > 0) { return *((*hist)[blockIndex - 1]->cend() - 1); } return 0; } HistoryItem *HistoryList::nextItem(HistoryItem *item) { if (!item) return 0; HistoryBlock *block = item->block(); int32 blockIndex = hist->indexOf(block), itemIndex = block->indexOf(item); if (blockIndex < 0 || itemIndex < 0) return 0; if (itemIndex + 1 < block->size()) { return (*block)[itemIndex + 1]; } if (blockIndex + 1 < hist->size()) { return *(*hist)[blockIndex + 1]->cbegin(); } return 0; } bool HistoryList::canCopySelected() const { return !_selected.isEmpty(); } bool HistoryList::canDeleteSelected() const { return !_selected.isEmpty() && (_selected.cbegin().value() == FullItemSel) && (!hist->peer->isChannel() || hist->peer->asChannel()->adminned); } void HistoryList::getSelectionState(int32 &selectedForForward, int32 &selectedForDelete) const { selectedForForward = selectedForDelete = 0; for (SelectedItems::const_iterator i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) { if (i.key()->itemType() == HistoryItem::MsgType && i.value() == FullItemSel) { ++selectedForDelete; if (!i.key()->serviceMsg() && i.key()->id > 0) { ++selectedForForward; } } } if (!selectedForDelete && !selectedForForward && !_selected.isEmpty()) { // text selection selectedForForward = -1; } } void HistoryList::clearSelectedItems(bool onlyTextSelection) { if (!_selected.isEmpty() && (!onlyTextSelection || _selected.cbegin().value() != FullItemSel)) { _selected.clear(); historyWidget->updateTopBarSelection(); historyWidget->update(); } } void HistoryList::fillSelectedItems(SelectedItemSet &sel, bool forDelete) { if (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel) return; for (SelectedItems::const_iterator i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) { HistoryItem *item = i.key(); if (dynamic_cast(item) && item->id > 0) { sel.insert(item->id, item); } } } void HistoryList::selectItem(HistoryItem *item) { if (!_selected.isEmpty() && _selected.cbegin().value() != FullItemSel) { _selected.clear(); } else if (_selected.size() == MaxSelectedItems && _selected.constFind(item) == _selected.cend()) { return; } _selected.insert(item, FullItemSel); historyWidget->updateTopBarSelection(); historyWidget->update(); } void HistoryList::onTouchSelect() { _touchSelect = true; dragActionStart(_touchPos); } void HistoryList::onUpdateSelected() { if (!hist) return; QPoint mousePos(mapFromGlobal(_dragPos)); QPoint point(historyWidget->clampMousePosition(mousePos)); HistoryBlock *block = 0; HistoryItem *item = 0; QPoint m; if (!hist->isEmpty()) { adjustCurrent(point.y()); block = (*hist)[currentBlock]; item = (*block)[currentItem]; App::mousedItem(item); m = mapMouseToItem(point, item); if (item->hasPoint(m.x(), m.y())) { updateMsg(App::hoveredItem()); App::hoveredItem(item); updateMsg(App::hoveredItem()); } else if (App::hoveredItem()) { updateMsg(App::hoveredItem()); App::hoveredItem(0); } } if (_dragItem && _dragItem->detached()) { dragActionCancel(); } Qt::CursorShape cur = style::cur_default; HistoryCursorState cursorState = HistoryDefaultCursorState; bool lnkChanged = false, lnkInDesc = false; TextLinkPtr lnk; if (point.y() < ySkip) { if (botInfo && !botInfo->text.isEmpty() && botDescHeight > 0) { bool inText = false; botInfo->text.getState(lnk, inText, point.x() - botDescRect.left() - st::msgPadding.left(), point.y() - botDescRect.top() - st::msgPadding.top() - st::botDescSkip - st::msgNameFont->height, botDescWidth); cursorState = inText ? HistoryInTextCursorState : HistoryDefaultCursorState; lnkInDesc = true; } } else if (item) { item->getState(lnk, cursorState, m.x(), m.y()); } if (lnk != textlnkOver()) { lnkChanged = true; if (textlnkOver()) { if (App::hoveredLinkItem()) { updateMsg(App::hoveredLinkItem()); } else { update(botDescRect); } } textlnkOver(lnk); QToolTip::hideText(); App::hoveredLinkItem((lnk && !lnkInDesc) ? item : 0); if (textlnkOver()) { if (App::hoveredLinkItem()) { updateMsg(App::hoveredLinkItem()); } else { update(botDescRect); } } } if (lnk || cursorState == HistoryInDateCursorState) { linkTipTimer.start(1000); } if (_dragCursorState == HistoryInDateCursorState && cursorState != HistoryInDateCursorState) { QToolTip::hideText(); } if (_dragAction == NoDrag) { _dragCursorState = cursorState; if (lnk) { cur = style::cur_pointer; } else if (_dragCursorState == HistoryInTextCursorState && (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel)) { cur = style::cur_text; } else if (_dragCursorState == HistoryInDateCursorState) { // cur = style::cur_cross; } } else if (item) { if (item != _dragItem || (m - _dragStartPos).manhattanLength() >= QApplication::startDragDistance()) { if (_dragAction == PrepareDrag) { _dragAction = Dragging; QTimer::singleShot(1, this, SLOT(onDragExec())); } else if (_dragAction == PrepareSelect) { _dragAction = Selecting; } } cur = textlnkDown() ? style::cur_pointer : style::cur_default; if (_dragAction == Selecting) { bool canSelectMany = (hist != 0); if (item == _dragItem && item == App::hoveredItem() && !_selected.isEmpty() && _selected.cbegin().value() != FullItemSel) { bool afterSymbol, uponSymbol; uint16 second; _dragItem->getSymbol(second, afterSymbol, uponSymbol, m.x(), m.y()); if (afterSymbol && _dragSelType == TextSelectLetters) ++second; uint32 selState = _dragItem->adjustSelection(qMin(second, _dragSymbol), qMax(second, _dragSymbol), _dragSelType); _selected[_dragItem] = selState; if (!_wasSelectedText && (selState == FullItemSel || (selState & 0xFFFF) != ((selState >> 16) & 0xFFFF))) { _wasSelectedText = true; setFocus(); } updateDragSelection(0, 0, false); } else if (canSelectMany) { bool selectingDown = (_dragItem->block()->y < item->block()->y) || ((_dragItem->block() == item->block()) && (_dragItem->y < item->y || (_dragItem == item && _dragStartPos.y() < m.y()))); HistoryItem *dragSelFrom = _dragItem, *dragSelTo = item; if (!dragSelFrom->hasPoint(_dragStartPos.x(), _dragStartPos.y())) { // maybe exclude dragSelFrom if (selectingDown) { if (_dragStartPos.y() >= dragSelFrom->height() - st::msgMargin.bottom() || ((item == dragSelFrom) && (m.y() < _dragStartPos.y() + QApplication::startDragDistance()))) { dragSelFrom = (dragSelFrom == dragSelTo) ? 0 : nextItem(dragSelFrom); } } else { if (_dragStartPos.y() < st::msgMargin.top() || ((item == dragSelFrom) && (m.y() >= _dragStartPos.y() - QApplication::startDragDistance()))) { dragSelFrom = (dragSelFrom == dragSelTo) ? 0 : prevItem(dragSelFrom); } } } if (_dragItem != item) { // maybe exclude dragSelTo if (selectingDown) { if (m.y() < st::msgMargin.top()) { dragSelTo = (dragSelFrom == dragSelTo) ? 0 : prevItem(dragSelTo); } } else { if (m.y() >= dragSelTo->height() - st::msgMargin.bottom()) { dragSelTo = (dragSelFrom == dragSelTo) ? 0 : nextItem(dragSelTo); } } } bool dragSelecting = false; HistoryItem *dragFirstAffected = dragSelFrom; while (dragFirstAffected && (dragFirstAffected->id < 0 || dragFirstAffected->serviceMsg())) { dragFirstAffected = (dragFirstAffected == dragSelTo) ? 0 : (selectingDown ? nextItem(dragFirstAffected) : prevItem(dragFirstAffected)); } if (dragFirstAffected) { SelectedItems::const_iterator i = _selected.constFind(dragFirstAffected); dragSelecting = (i == _selected.cend() || i.value() != FullItemSel); } updateDragSelection(dragSelFrom, dragSelTo, dragSelecting); } } else if (_dragAction == Dragging) { } if (textlnkDown()) { cur = style::cur_pointer; } else if (_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullItemSel) { if (!_dragSelFrom || !_dragSelTo) { cur = style::cur_text; } } } if (_dragAction == Selecting) { historyWidget->checkSelectingScroll(mousePos); } else { updateDragSelection(0, 0, false); historyWidget->noSelectingScroll(); } if (lnkChanged || cur != _cursor) { setCursor(_cursor = cur); } } void HistoryList::updateDragSelection(HistoryItem *dragSelFrom, HistoryItem *dragSelTo, bool dragSelecting, bool force) { if (_dragSelFrom != dragSelFrom || _dragSelTo != dragSelTo || _dragSelecting != dragSelecting) { _dragSelFrom = dragSelFrom; _dragSelTo = dragSelTo; if (_dragSelFrom && _dragSelTo && _dragSelFrom->y + _dragSelFrom->block()->y > _dragSelTo->y + _dragSelTo->block()->y) { qSwap(_dragSelFrom, _dragSelTo); } _dragSelecting = dragSelecting; if (!_wasSelectedText && _dragSelFrom && _dragSelTo && _dragSelecting) { _wasSelectedText = true; setFocus(); } force = true; } if (!force) return; parentWidget()->update(); } void HistoryList::applyDragSelection() { applyDragSelection(&_selected); } void HistoryList::applyDragSelection(SelectedItems *toItems) const { if (!toItems->isEmpty() && toItems->cbegin().value() != FullItemSel) { toItems->clear(); } int32 fromy = _dragSelFrom->y + _dragSelFrom->block()->y, toy = _dragSelTo->y + _dragSelTo->block()->y + _dragSelTo->height(); if (_dragSelecting) { int32 fromblock = hist->indexOf(_dragSelFrom->block()), fromitem = _dragSelFrom->block()->indexOf(_dragSelFrom); int32 toblock = hist->indexOf(_dragSelTo->block()), toitem = _dragSelTo->block()->indexOf(_dragSelTo); if (fromblock >= 0 && fromitem >= 0 && toblock >= 0 && toitem >= 0) { for (; fromblock <= toblock; ++fromblock) { HistoryBlock *block = (*hist)[fromblock]; for (int32 cnt = (fromblock < toblock) ? block->size() : (toitem + 1); fromitem < cnt; ++fromitem) { HistoryItem *item = (*block)[fromitem]; SelectedItems::iterator i = toItems->find(item); if (item->id > 0 && !item->serviceMsg()) { if (i == toItems->cend()) { if (toItems->size() >= MaxSelectedItems) break; toItems->insert(item, FullItemSel); } else if (i.value() != FullItemSel) { *i = FullItemSel; } } else { if (i != toItems->cend()) { toItems->erase(i); } } } if (toItems->size() >= MaxSelectedItems) break; fromitem = 0; } } } else { for (SelectedItems::iterator i = toItems->begin(); i != toItems->cend();) { int32 iy = i.key()->y + i.key()->block()->y; if (iy >= fromy && iy < toy) { i = toItems->erase(i); } else { ++i; } } } } void HistoryList::showLinkTip() { TextLinkPtr lnk = textlnkOver(); int32 dd = QApplication::startDragDistance(); QPoint dp(mapFromGlobal(_dragPos)); QRect r(dp.x() - dd, dp.y() - dd, 2 * dd, 2 * dd); if (lnk && !lnk->fullDisplayed()) { QToolTip::showText(_dragPos, lnk->readable(), this, r); } else if (_dragCursorState == HistoryInDateCursorState && _dragAction == NoDrag) { if (App::hoveredItem()) { QToolTip::showText(_dragPos, App::hoveredItem()->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)), this, r); } } } void HistoryList::onParentGeometryChanged() { bool needToUpdate = (_dragAction != NoDrag || _touchScroll || rect().contains(mapFromGlobal(QCursor::pos()))); if (needToUpdate) { dragActionUpdate(QCursor::pos()); } } MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st, const QString &ph, const QString &val) : FlatTextarea(history, st, ph, val), history(history), _maxHeight(st::maxFieldHeight) { connect(this, SIGNAL(changed()), this, SLOT(onChange())); } void MessageField::setMaxHeight(int32 maxHeight) { _maxHeight = maxHeight; int newh = ceil(document()->size().height()) + 2 * fakeMargin(), minh = st::btnSend.height - 2 * st::sendPadding; if (newh > _maxHeight) { newh = _maxHeight; } else if (newh < minh) { newh = minh; } if (height() != newh) { resize(width(), newh); } } bool MessageField::hasSendText() const { const QString &text(getLastText()); for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { ushort code = ch->unicode(); if (code != ' ' && code != '\n' && code != '\r' && !replaceCharBySpace(code)) { return true; } } return false; } void MessageField::onChange() { int newh = ceil(document()->size().height()) + 2 * fakeMargin(), minh = st::btnSend.height - 2 * st::sendPadding; if (newh > _maxHeight) { newh = _maxHeight; } else if (newh < minh) { newh = minh; } if (height() != newh) { resize(width(), newh); emit resized(); } } void MessageField::onEmojiInsert(EmojiPtr emoji) { insertEmoji(emoji, textCursor()); } void MessageField::dropEvent(QDropEvent *e) { FlatTextarea::dropEvent(e); if (e->isAccepted()) { App::wnd()->activateWindow(); } } void MessageField::resizeEvent(QResizeEvent *e) { FlatTextarea::resizeEvent(e); onChange(); } bool MessageField::canInsertFromMimeData(const QMimeData *source) const { if (source->hasImage()) return true; return FlatTextarea::canInsertFromMimeData(source); } void MessageField::insertFromMimeData(const QMimeData *source) { if (source->hasImage()) { QImage img = qvariant_cast(source->imageData()); if (!img.isNull()) { history->uploadImage(img, false, source->text()); return; } } FlatTextarea::insertFromMimeData(source); } void MessageField::focusInEvent(QFocusEvent *e) { FlatTextarea::focusInEvent(e); emit focused(); } ReportSpamPanel::ReportSpamPanel(HistoryWidget *parent) : TWidget(parent), _report(this, lang(lng_report_spam), st::reportSpamHide), _hide(this, lang(lng_report_spam_hide), st::reportSpamHide), _clear(this, lang(lng_profile_delete_conversation)) { resize(parent->width(), _hide.height() + st::titleShadow); connect(&_report, SIGNAL(clicked()), this, SIGNAL(reportClicked())); connect(&_hide, SIGNAL(clicked()), this, SIGNAL(hideClicked())); connect(&_clear, SIGNAL(clicked()), this, SIGNAL(clearClicked())); _clear.hide(); } void ReportSpamPanel::resizeEvent(QResizeEvent *e) { _report.resize(width() - (_hide.width() + st::reportSpamSeparator) * 2, _report.height()); _report.moveToLeft(_hide.width() + st::reportSpamSeparator, 0, width()); _hide.moveToRight(0, 0, width()); _clear.move((width() - _clear.width()) / 2, height() - _clear.height() - ((height() - st::msgFont->height - _clear.height()) / 2)); } void ReportSpamPanel::paintEvent(QPaintEvent *e) { Painter p(this); p.fillRect(QRect(0, 0, width(), height() - st::titleShadow), st::reportSpamBg->b); if (cWideMode()) { p.fillRect(st::titleShadow, height() - st::titleShadow, width() - st::titleShadow, st::titleShadow, st::titleShadowColor->b); } else { p.fillRect(0, height() - st::titleShadow, width(), st::titleShadow, st::titleShadowColor->b); } if (!_clear.isHidden()) { p.setPen(st::black->p); p.setFont(st::msgFont->f); p.drawText(QRect(_report.x(), (_clear.y() - st::msgFont->height) / 2, _report.width(), st::msgFont->height), lang(lng_report_spam_thanks), style::al_top); } } void ReportSpamPanel::setReported(bool reported) { if (reported) { _report.hide(); _clear.show(); } else { _report.show(); _clear.hide(); } update(); } BotKeyboard::BotKeyboard() : _height(0), _maxOuterHeight(0), _maximizeSize(false), _singleUse(false), _forceReply(false), _sel(-1), _down(-1), _hoverAnim(animFunc(this, &BotKeyboard::hoverStep)), _st(&st::botKbButton) { setGeometry(0, 0, _st->margin, _st->margin); _height = _st->margin; setMouseTracking(true); _cmdTipTimer.setSingleShot(true); connect(&_cmdTipTimer, SIGNAL(timeout()), this, SLOT(showCommandTip())); } void BotKeyboard::paintEvent(QPaintEvent *e) { Painter p(this); QRect r(e->rect()); p.setClipRect(r); p.fillRect(r, st::white->b); p.setPen(st::botKbColor->p); p.setFont(st::botKbFont->f); for (int32 i = 0, l = _btns.size(); i != l; ++i) { int32 j = 0, s = _btns.at(i).size(); for (; j != s; ++j) { const Button &btn(_btns.at(i).at(j)); QRect rect(btn.rect); if (rect.top() >= r.bottom()) break; if (rect.bottom() < r.top()) continue; if (rtl()) rect.moveLeft(width() - rect.left() - rect.width()); int32 tx = rect.x(), tw = rect.width(); if (tw > st::botKbFont->elidew + _st->padding * 2) { tx += _st->padding; tw -= _st->padding * 2; } else if (tw > st::botKbFont->elidew) { tx += (tw - st::botKbFont->elidew) / 2; tw = st::botKbFont->elidew; } if (_down == i * MatrixRowShift + j) { App::roundRect(p, rect, st::botKbDownBg, BotKeyboardDownCorners); btn.text.drawElided(p, tx, rect.y() + _st->downTextTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top); } else { App::roundRect(p, rect, st::botKbBg, BotKeyboardCorners); float64 hover = btn.hover; if (hover > 0) { p.setOpacity(hover); App::roundRect(p, rect, st::botKbOverBg, BotKeyboardOverCorners); p.setOpacity(1); } btn.text.drawElided(p, tx, rect.y() + _st->textTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top); } } if (j < s) break; } } void BotKeyboard::resizeEvent(QResizeEvent *e) { updateStyle(); _height = (_btns.size() + 1) * _st->margin + _btns.size() * _st->height; if (_maximizeSize) _height = qMax(_height, _maxOuterHeight); if (height() != _height) { resize(width(), _height); return; } float64 y = _st->margin, btnh = _btns.isEmpty() ? _st->height : (float64(_height - _st->margin) / _btns.size()); for (int32 i = 0, l = _btns.size(); i != l; ++i) { int32 j = 0, s = _btns.at(i).size(); float64 widthForText = width() - (s * _st->margin + st::botKbScroll.width + s * 2 * _st->padding), widthOfText = 0.; for (; j != s; ++j) { Button &btn(_btns[i][j]); if (btn.text.isEmpty()) btn.text.setText(st::botKbFont, textOneLine(btn.cmd), _textPlainOptions); if (!btn.cwidth) btn.cwidth = btn.cmd.size(); if (!btn.cwidth) btn.cwidth = 1; widthOfText += qMax(btn.text.maxWidth(), 1); } float64 x = _st->margin, coef = widthForText / widthOfText; for (j = 0; j != s; ++j) { Button &btn(_btns[i][j]); float64 tw = widthForText / float64(s), w = 2 * _st->padding + tw; if (w < _st->padding) w = _st->padding; btn.rect = QRect(qRound(x), qRound(y), qRound(w), qRound(btnh - _st->margin)); x += w + _st->margin; btn.full = tw >= btn.text.maxWidth(); } y += btnh; } } void BotKeyboard::mousePressEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); _down = _sel; update(); } void BotKeyboard::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); } void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) { int32 down = _down; _down = -1; _lastMousePos = e->globalPos(); updateSelected(); if (_sel == down && down >= 0) { int row = (down / MatrixRowShift), col = down % MatrixRowShift; App::sendBotCommand(_btns.at(row).at(col).cmd, _wasForMsgId.msg); } } void BotKeyboard::leaveEvent(QEvent *e) { _lastMousePos = QPoint(-1, -1); updateSelected(); } bool BotKeyboard::updateMarkup(HistoryItem *to) { if (to && to->hasReplyMarkup()) { if (_wasForMsgId == FullMsgId(to->channelId(), to->id)) return false; _wasForMsgId = FullMsgId(to->channelId(), to->id); clearSelection(); _btns.clear(); const ReplyMarkup &markup(App::replyMarkup(to->channelId(), to->id)); _forceReply = markup.flags & MTPDreplyKeyboardMarkup_flag_FORCE_REPLY; _maximizeSize = !(markup.flags & MTPDreplyKeyboardMarkup_flag_resize); _singleUse = _forceReply || (markup.flags & MTPDreplyKeyboardMarkup_flag_single_use); const ReplyMarkup::Commands &commands(markup.commands); if (!commands.isEmpty()) { int32 i = 0, l = qMin(commands.size(), 512); _btns.reserve(l); for (; i != l; ++i) { const QList &row(commands.at(i)); QList