Allow to select and copy text in the events log.

Also better handle window resize in the events log.
This commit is contained in:
John Preston 2017-06-23 22:28:42 +03:00
parent 693c30d264
commit 624f33c5e2
13 changed files with 203 additions and 66 deletions

View File

@ -94,6 +94,23 @@ TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) {
return result;
}
std::unique_ptr<QMimeData> MimeDataFromTextWithEntities(const TextWithEntities &forClipboard) {
if (forClipboard.text.isEmpty()) {
return nullptr;
}
auto result = std::make_unique<QMimeData>();
result->setText(forClipboard.text);
auto tags = ConvertEntitiesToTextTags(forClipboard.entities);
if (!tags.isEmpty()) {
for (auto &tag : tags) {
tag.id = ConvertTagToMimeTag(tag.id);
}
result->setData(Ui::FlatTextarea::tagsMimeType(), Ui::FlatTextarea::serializeTagsList(tags));
}
return result;
}
MessageField::MessageField(QWidget *parent, gsl::not_null<Window::Controller*> controller, const style::FlatTextarea &st, base::lambda<QString()> placeholderFactory, const QString &val) : Ui::FlatTextarea(parent, st, std::move(placeholderFactory), val)
, _controller(controller) {
setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);

View File

@ -31,6 +31,7 @@ QString ConvertTagToMimeTag(const QString &tagId);
EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags);
TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities);
std::unique_ptr<QMimeData> MimeDataFromTextWithEntities(const TextWithEntities &forClipboard);
class MessageField final : public Ui::FlatTextarea {
Q_OBJECT

View File

@ -25,6 +25,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
#include "history/history_message.h"
#include "history/history_service_layout.h"
#include "history/history_admin_log_section.h"
#include "chat_helpers/message_field.h"
#include "mainwindow.h"
#include "window/window_controller.h"
#include "auth_session.h"
@ -234,16 +235,20 @@ void InnerWidget::setVisibleTopBottom(int visibleTop, int visibleBottom) {
}
void InnerWidget::updateVisibleTopItem() {
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {
return itemTop(elem) + elem->height() <= top;
});
if (from != end) {
_visibleTopItem = *from;
_visibleTopFromItem = _visibleTop - _visibleTopItem->y();
} else {
if (_visibleBottom == height()) {
_visibleTopItem = nullptr;
_visibleTopFromItem = _visibleTop;
} else {
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {
return itemTop(elem) + elem->height() <= top;
});
if (from != end) {
_visibleTopItem = *from;
_visibleTopFromItem = _visibleTop - _visibleTopItem->y();
} else {
_visibleTopItem = nullptr;
_visibleTopFromItem = _visibleTop;
}
}
}
@ -447,8 +452,7 @@ void InnerWidget::itemsAdded(Direction direction) {
void InnerWidget::updateSize() {
TWidget::resizeToWidth(width());
auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax;
_scrollTo(newVisibleTop);
restoreScrollPosition();
updateVisibleTopItem();
checkPreloadMore();
}
@ -466,6 +470,11 @@ int InnerWidget::resizeGetHeight(int newWidth) {
return _itemsTop + _itemsHeight + st::historyPaddingBottom;
}
void InnerWidget::restoreScrollPosition() {
auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax;
_scrollTo(newVisibleTop);
}
void InnerWidget::paintEvent(QPaintEvent *e) {
if (Ui::skipPaintEvent(this, e)) {
return;
@ -490,7 +499,8 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
auto top = itemTop(from->get());
p.translate(0, top);
for (auto i = from; i != to; ++i) {
(*i)->draw(p, clip.translated(0, -top), TextSelection(), ms);
auto selection = (*i == _selectedItem) ? _selectedText : TextSelection();
(*i)->draw(p, clip.translated(0, -top), selection, ms);
auto height = (*i)->height();
top += height;
p.translate(0, height);
@ -562,9 +572,37 @@ void InnerWidget::paintEmpty(Painter &p) {
//p.drawText(tr.left() + st::msgPadding.left(), tr.top() + st::msgServicePadding.top() + 1 + font->ascent, lang(lng_willbe_history));
}
TextWithEntities InnerWidget::getSelectedText() const {
return _selectedItem ? _selectedItem->selectedText(_selectedText) : TextWithEntities();
}
void InnerWidget::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape && _cancelledCallback) {
_cancelledCallback();
} else if (e == QKeySequence::Copy && _selectedItem != nullptr) {
copySelectedText();
#ifdef Q_OS_MAC
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
setToClipboard(getSelectedText(), QClipboard::FindBuffer);
#endif // Q_OS_MAC
} else {
e->ignore();
}
}
void InnerWidget::copySelectedText() {
setToClipboard(getSelectedText());
}
void InnerWidget::copyContextUrl() {
//if (_contextMenuLnk) {
// _contextMenuLnk->copyToClipboard();
//}
}
void InnerWidget::setToClipboard(const TextWithEntities &forClipboard, QClipboard::Mode mode) {
if (auto data = MimeDataFromTextWithEntities(forClipboard)) {
QApplication::clipboard()->setMimeData(data.release(), mode);
}
}
@ -826,7 +864,10 @@ void InnerWidget::updateSelected() {
if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
++second;
}
auto selection = _mouseActionItem->adjustSelection({ qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) }, _mouseSelectType);
auto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
if (_mouseSelectType != TextSelectType::Letters) {
_mouseActionItem->adjustSelection(selection, _mouseSelectType);
}
if (_selectedText != selection) {
_selectedText = selection;
repaintItem(_mouseActionItem);

View File

@ -68,6 +68,9 @@ public:
// Updates the area that is visible inside the scroll container.
void setVisibleTopBottom(int visibleTop, int visibleBottom) override;
// Set the correct scroll position after being resized.
void restoreScrollPosition();
void resizeToWidth(int newWidth, int minHeight) {
_minHeight = minHeight;
return TWidget::resizeToWidth(newWidth);
@ -141,6 +144,11 @@ private:
void scrollDateCheck();
void scrollDateHideByTimer();
TextWithEntities getSelectedText() const;
void copySelectedText();
void copyContextUrl();
void setToClipboard(const TextWithEntities &forClipboard, QClipboard::Mode mode = QClipboard::Clipboard);
// This function finds all history items that are displayed and calls template method
// for each found message (in given direction) in the passed history with passed top offset.
//

View File

@ -219,6 +219,7 @@ void Widget::resizeEvent(QResizeEvent *e) {
if (_scroll->size() != scrollSize) {
_scroll->resize(scrollSize);
_inner->resizeToWidth(scrollSize.width(), _scroll->height());
_inner->restoreScrollPosition();
}
if (!_scroll->isHidden()) {

View File

@ -78,23 +78,6 @@ int BinarySearchBlocksOrItems(const T &list, int edge) {
return start;
}
std::unique_ptr<QMimeData> MimeDataFromTextWithEntities(const TextWithEntities &forClipboard) {
if (forClipboard.text.isEmpty()) {
return nullptr;
}
auto result = std::make_unique<QMimeData>();
result->setText(forClipboard.text);
auto tags = ConvertEntitiesToTextTags(forClipboard.entities);
if (!tags.isEmpty()) {
for (auto &tag : tags) {
tag.id = ConvertTagToMimeTag(tag.id);
}
result->setData(Ui::FlatTextarea::tagsMimeType(), Ui::FlatTextarea::serializeTagsList(tags));
}
return result;
}
} // namespace
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
@ -2143,7 +2126,10 @@ void HistoryInner::onUpdateSelected() {
if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
++second;
}
auto selState = _mouseActionItem->adjustSelection({ qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) }, _mouseSelectType);
auto selState = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
if (_mouseSelectType != TextSelectType::Letters) {
_mouseActionItem->adjustSelection(selState, _mouseSelectType);
}
if (_selected[_mouseActionItem] != selState) {
_selected[_mouseActionItem] = selState;
repaintItem(_mouseActionItem);

View File

@ -584,18 +584,18 @@ HistoryMediaPtr::~HistoryMediaPtr() {
namespace internal {
TextSelection unshiftSelection(TextSelection selection, const Text &byText) {
TextSelection unshiftSelection(TextSelection selection, uint16 byLength) {
if (selection == FullSelection) {
return selection;
}
return ::unshiftSelection(selection, byText);
return ::unshiftSelection(selection, byLength);
}
TextSelection shiftSelection(TextSelection selection, const Text &byText) {
TextSelection shiftSelection(TextSelection selection, uint16 byLength) {
if (selection == FullSelection) {
return selection;
}
return ::shiftSelection(selection, byText);
return ::shiftSelection(selection, byLength);
}
} // namespace internal

View File

@ -465,8 +465,14 @@ private:
namespace internal {
TextSelection unshiftSelection(TextSelection selection, const Text &byText);
TextSelection shiftSelection(TextSelection selection, const Text &byText);
TextSelection unshiftSelection(TextSelection selection, uint16 byLength);
TextSelection shiftSelection(TextSelection selection, uint16 byLength);
inline TextSelection unshiftSelection(TextSelection selection, const Text &byText) {
return ::internal::unshiftSelection(selection, byText.length());
}
inline TextSelection shiftSelection(TextSelection selection, const Text &byText) {
return ::internal::shiftSelection(selection, byText.length());
}
} // namespace internal
@ -984,10 +990,10 @@ protected:
return nullptr;
}
TextSelection toMediaSelection(TextSelection selection) const {
TextSelection skipTextSelection(TextSelection selection) const {
return internal::unshiftSelection(selection, _text);
}
TextSelection fromMediaSelection(TextSelection selection) const {
TextSelection unskipTextSelection(TextSelection selection) const {
return internal::shiftSelection(selection, _text);
}

View File

@ -90,6 +90,15 @@ public:
virtual bool consumeMessageText(const TextWithEntities &textWithEntities) {
return false;
}
virtual uint16 fullSelectionLength() const {
return 0;
}
TextSelection skipSelection(TextSelection selection) const {
return internal::unshiftSelection(selection, fullSelectionLength());
}
TextSelection unskipSelection(TextSelection selection) const {
return internal::shiftSelection(selection, fullSelectionLength());
}
// if we press and drag this link should we drag the item
virtual bool dragItemByHandler(const ClickHandlerPtr &p) const = 0;

View File

@ -139,6 +139,9 @@ public:
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override {
return _caption.adjustSelection(selection, type);
}
uint16 fullSelectionLength() const override {
return _caption.length();
}
bool hasTextForCopy() const override {
return !_caption.isEmpty();
}
@ -221,6 +224,9 @@ public:
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override {
return _caption.adjustSelection(selection, type);
}
uint16 fullSelectionLength() const override {
return _caption.length();
}
bool hasTextForCopy() const override {
return !_caption.isEmpty();
}
@ -372,6 +378,12 @@ public:
}
return selection;
}
uint16 fullSelectionLength() const override {
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
return captioned->_caption.length();
}
return 0;
}
bool hasTextForCopy() const override {
return Has<HistoryDocumentCaptioned>();
}
@ -475,6 +487,9 @@ public:
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override {
return _caption.adjustSelection(selection, type);
}
uint16 fullSelectionLength() const override {
return _caption.length();
}
bool hasTextForCopy() const override {
return !_caption.isEmpty();
}
@ -769,6 +784,9 @@ public:
HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return false; // we do not add _title and _description in FullSelection text copy.
}
@ -869,6 +887,9 @@ public:
HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool isAboveMessage() const override {
return true;
}
@ -977,6 +998,9 @@ public:
HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return false; // we do not add _title and _description in FullSelection text copy.
}
@ -1060,6 +1084,9 @@ public:
HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
TextSelection adjustSelection(TextSelection selection, TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return !_title.isEmpty() || !_description.isEmpty();
}

View File

@ -1058,24 +1058,33 @@ void HistoryMessage::eraseFromOverview() {
}
TextWithEntities HistoryMessage::selectedText(TextSelection selection) const {
TextWithEntities result, textResult, mediaResult;
TextWithEntities textResult, mediaResult, logEntryOriginalResult;
if (selection == FullSelection) {
textResult = _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
} else {
textResult = _text.originalTextWithEntities(selection, ExpandLinksAll);
}
if (_media) {
mediaResult = _media->selectedText(toMediaSelection(selection));
auto skipped = skipTextSelection(selection);
auto mediaDisplayed = (_media && _media->isDisplayed());
if (mediaDisplayed) {
mediaResult = _media->selectedText(skipped);
}
if (textResult.text.isEmpty()) {
result = mediaResult;
} else if (mediaResult.text.isEmpty()) {
result = textResult;
} else {
result.text = textResult.text + qstr("\n\n");
result.entities = textResult.entities;
if (auto entry = Get<HistoryMessageLogEntryOriginal>()) {
logEntryOriginalResult = entry->_page->selectedText(mediaDisplayed ? _media->skipSelection(skipped) : skipped);
}
auto result = textResult;
if (result.text.isEmpty()) {
result = std::move(mediaResult);
} else if (!mediaResult.text.isEmpty()) {
result.text += qstr("\n\n");
appendTextWithEntities(result, std::move(mediaResult));
}
if (result.text.isEmpty()) {
result = std::move(logEntryOriginalResult);
} else if (!logEntryOriginalResult.text.isEmpty()) {
result.text += qstr("\n\n");
appendTextWithEntities(result, std::move(logEntryOriginalResult));
}
if (auto forwarded = Get<HistoryMessageForwarded>()) {
if (selection == FullSelection) {
auto fwdinfo = forwarded->_text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
@ -1138,7 +1147,8 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
if (mediaDisplayed && _media->consumeMessageText(textWithEntities)) {
setEmptyText();
} else {
if (_media && _media->isDisplayed() && !_media->isAboveMessage()) {
auto mediaOnBottom = (_media && _media->isDisplayed() && _media->isBubbleBottom()) || Has<HistoryMessageLogEntryOriginal>();
if (mediaOnBottom) {
_text.setMarkedText(st::messageTextStyle, textWithEntities, itemTextOptions(this));
} else {
_text.setMarkedText(st::messageTextStyle, { textWithEntities.text + skipBlock(), textWithEntities.entities }, itemTextOptions(this));
@ -1442,7 +1452,7 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM
paintText(p, trect, selection);
}
p.translate(mediaLeft, mediaTop);
_media->draw(p, clip.translated(-mediaLeft, -mediaTop), toMediaSelection(selection), ms);
_media->draw(p, clip.translated(-mediaLeft, -mediaTop), skipTextSelection(selection), ms);
p.translate(-mediaLeft, -mediaTop);
if (mediaAboveText) {
@ -1458,7 +1468,11 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM
auto entryLeft = g.left();
auto entryTop = trect.y() + trect.height();
p.translate(entryLeft, entryTop);
entry->_page->draw(p, clip.translated(-entryLeft, -entryTop), TextSelection(), ms);
auto entrySelection = skipTextSelection(selection);
if (mediaDisplayed) {
entrySelection = _media->skipSelection(entrySelection);
}
entry->_page->draw(p, clip.translated(-entryLeft, -entryTop), entrySelection, ms);
p.translate(-entryLeft, -entryTop);
}
if (needDrawInfo) {
@ -1466,7 +1480,7 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM
}
} else if (_media) {
p.translate(g.topLeft());
_media->draw(p, clip.translated(-g.topLeft()), toMediaSelection(selection), ms);
_media->draw(p, clip.translated(-g.topLeft()), skipTextSelection(selection), ms);
p.translate(-g.topLeft());
}
@ -1749,7 +1763,7 @@ HistoryTextState HistoryMessage::getState(QPoint point, HistoryStateRequest requ
auto entryTop = trect.y() + trect.height();
if (point.y() >= entryTop && point.y() < entryTop + entryHeight) {
result = entry->_page->getState(point - QPoint(entryLeft, entryTop), request);
result.symbol += _text.length();
result.symbol += _text.length() + (mediaDisplayed ? _media->fullSelectionLength() : 0);
}
}
@ -1926,15 +1940,38 @@ bool HistoryMessage::getStateText(QPoint point, QRect &trect, HistoryTextState *
}
TextSelection HistoryMessage::adjustSelection(TextSelection selection, TextSelectType type) const {
if (!_media || selection.to <= _text.length()) {
return _text.adjustSelection(selection, type);
auto result = _text.adjustSelection(selection, type);
auto beforeMediaLength = _text.length();
if (selection.to <= beforeMediaLength) {
return result;
}
auto mediaSelection = _media->adjustSelection(toMediaSelection(selection), type);
if (selection.from >= _text.length()) {
return fromMediaSelection(mediaSelection);
auto mediaDisplayed = _media && _media->isDisplayed();
if (mediaDisplayed) {
auto mediaSelection = unskipTextSelection(_media->adjustSelection(skipTextSelection(selection), type));
if (selection.from >= beforeMediaLength) {
result = mediaSelection;
} else {
result.to = mediaSelection.to;
}
}
auto textSelection = _text.adjustSelection(selection, type);
return { textSelection.from, fromMediaSelection(mediaSelection).to };
auto beforeEntryLength = beforeMediaLength + (mediaDisplayed ? _media->fullSelectionLength() : 0);
if (selection.to <= beforeEntryLength) {
return result;
}
if (auto entry = Get<HistoryMessageLogEntryOriginal>()) {
auto entrySelection = mediaDisplayed ? _media->skipSelection(skipTextSelection(selection)) : skipTextSelection(selection);
auto logEntryOriginalSelection = entry->_page->adjustSelection(entrySelection, type);
if (mediaDisplayed) {
logEntryOriginalSelection = _media->unskipSelection(logEntryOriginalSelection);
}
logEntryOriginalSelection = unskipTextSelection(logEntryOriginalSelection);
if (selection.from >= beforeEntryLength) {
result = logEntryOriginalSelection;
} else {
result.to = logEntryOriginalSelection.to;
}
}
return result;
}
void HistoryMessage::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {

View File

@ -218,7 +218,7 @@ void ServiceMessagePainter::paint(Painter &p, const HistoryService *message, con
height -= st::msgServiceMargin.top() + media->height();
auto left = st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, top = st::msgServiceMargin.top() + height + st::msgServiceMargin.top();
p.translate(left, top);
media->draw(p, context.clip.translated(-left, -top), message->toMediaSelection(context.selection), context.ms);
media->draw(p, context.clip.translated(-left, -top), message->skipTextSelection(context.selection), context.ms);
p.translate(-left, -top);
}

View File

@ -245,13 +245,17 @@ private:
inline TextSelection snapSelection(int from, int to) {
return { static_cast<uint16>(snap(from, 0, 0xFFFF)), static_cast<uint16>(snap(to, 0, 0xFFFF)) };
}
inline TextSelection shiftSelection(TextSelection selection, uint16 byLength) {
return snapSelection(int(selection.from) + byLength, int(selection.to) + byLength);
}
inline TextSelection unshiftSelection(TextSelection selection, uint16 byLength) {
return snapSelection(int(selection.from) - int(byLength), int(selection.to) - int(byLength));
}
inline TextSelection shiftSelection(TextSelection selection, const Text &byText) {
int len = byText.length();
return snapSelection(int(selection.from) + len, int(selection.to) + len);
return shiftSelection(selection, byText.length());
}
inline TextSelection unshiftSelection(TextSelection selection, const Text &byText) {
int len = byText.length();
return snapSelection(int(selection.from) - len, int(selection.to) - len);
return unshiftSelection(selection, byText.length());
}
void initLinkSets();