diff --git a/Telegram/Resources/basic.style b/Telegram/Resources/basic.style index 60adbe02f..cf2a840a5 100644 --- a/Telegram/Resources/basic.style +++ b/Telegram/Resources/basic.style @@ -72,9 +72,10 @@ overBg: #edf2f5; labelDefFlat: flatLabel { font: font(fsize); - minWidth: 100px; width: 0px; + maxHeight: 0px; align: align(left); + textFg: windowTextFg; } boxBg: white; diff --git a/Telegram/Resources/basic_types.style b/Telegram/Resources/basic_types.style index 750a52cea..22e79f521 100644 --- a/Telegram/Resources/basic_types.style +++ b/Telegram/Resources/basic_types.style @@ -210,9 +210,11 @@ slider { flatLabel { font: font; - minWidth: pixels; + margin: margins; width: pixels; align: align; + textFg: color; + maxHeight: pixels; } switcher { diff --git a/Telegram/SourceFiles/boxes/aboutbox.h b/Telegram/SourceFiles/boxes/aboutbox.h index ceccef089..45af6b009 100644 --- a/Telegram/SourceFiles/boxes/aboutbox.h +++ b/Telegram/SourceFiles/boxes/aboutbox.h @@ -21,6 +21,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #pragma once #include "abstractbox.h" +#include "ui/flatlabel.h" class AboutBox : public AbstractBox { Q_OBJECT diff --git a/Telegram/SourceFiles/boxes/confirmbox.h b/Telegram/SourceFiles/boxes/confirmbox.h index 097e6f436..4af3c49af 100644 --- a/Telegram/SourceFiles/boxes/confirmbox.h +++ b/Telegram/SourceFiles/boxes/confirmbox.h @@ -21,6 +21,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #pragma once #include "abstractbox.h" +#include "ui/flatlabel.h" class InformBox; class ConfirmBox : public AbstractBox, public ClickHandlerHost { diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 1206f2d97..a81ccd818 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -654,7 +654,7 @@ void HistoryInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton butt _selected.insert(_dragItem, selStatus); _dragSymbol = dragState.symbol; _dragAction = Selecting; - _dragSelType = TextSelectParagraphs; + _dragSelType = TextSelectType::Paragraphs; dragActionUpdate(_dragPos); _trippleClickTimer.start(QApplication::doubleClickInterval()); } @@ -664,7 +664,7 @@ void HistoryInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton butt request.flags = Text::StateRequest::Flag::LookupSymbol; dragState = _dragItem->getState(_dragStartPos.x(), _dragStartPos.y(), request); } - if (_dragSelType != TextSelectParagraphs) { + if (_dragSelType != TextSelectType::Paragraphs) { if (App::pressedItem()) { _dragSymbol = dragState.symbol; bool uponSelected = (dragState.cursor == HistoryInTextCursorState); @@ -711,7 +711,7 @@ void HistoryInner::dragActionStart(const QPoint &screenPos, Qt::MouseButton butt if (!_dragItem) { _dragAction = NoDrag; } else if (_dragAction == NoDrag) { - _dragItem = 0; + _dragItem = nullptr; } } @@ -902,7 +902,7 @@ void HistoryInner::dragActionFinish(const QPoint &screenPos, Qt::MouseButton but } _dragAction = NoDrag; _dragItem = 0; - _dragSelType = TextSelectLetters; + _dragSelType = TextSelectType::Letters; _widget->noSelectingScroll(); _widget->updateTopBarSelection(); } @@ -918,13 +918,13 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) { if (!_history) return; dragActionStart(e->globalPos(), e->button()); - if (((_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) || (_dragAction == NoDrag && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection))) && _dragSelType == TextSelectLetters && _dragItem) { + if (((_dragAction == Selecting && !_selected.isEmpty() && _selected.cbegin().value() != FullSelection) || (_dragAction == NoDrag && (_selected.isEmpty() || _selected.cbegin().value() != FullSelection))) && _dragSelType == TextSelectType::Letters && _dragItem) { HistoryStateRequest request; request.flags |= Text::StateRequest::Flag::LookupSymbol; auto dragState = _dragItem->getState(_dragStartPos.x(), _dragStartPos.y(), request); if (dragState.cursor == HistoryInTextCursorState) { _dragSymbol = dragState.symbol; - _dragSelType = TextSelectWords; + _dragSelType = TextSelectType::Words; if (_dragAction == NoDrag) { _dragAction = Selecting; TextSelection selStatus = { dragState.symbol, dragState.symbol }; @@ -1815,7 +1815,7 @@ void HistoryInner::onUpdateSelected() { bool canSelectMany = (_history != nullptr); if (selectingText) { uint16 second = dragState.symbol; - if (dragState.afterSymbol && _dragSelType == TextSelectLetters) { + if (dragState.afterSymbol && _dragSelType == TextSelectType::Letters) { ++second; } auto selState = _dragItem->adjustSelection({ qMin(second, _dragSymbol), qMax(second, _dragSymbol) }, _dragSelType); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index f9e82b830..4dd4bc49c 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -204,7 +204,7 @@ private: Selecting = 0x04, }; DragAction _dragAction = NoDrag; - TextSelectType _dragSelType = TextSelectLetters; + TextSelectType _dragSelType = TextSelectType::Letters; QPoint _dragStartPos, _dragPos; HistoryItem *_dragItem = nullptr; HistoryCursorState _dragCursorState = HistoryDefaultCursorState; diff --git a/Telegram/SourceFiles/intro/introphone.h b/Telegram/SourceFiles/intro/introphone.h index 8e3bc5e96..8c53bc6b4 100644 --- a/Telegram/SourceFiles/intro/introphone.h +++ b/Telegram/SourceFiles/intro/introphone.h @@ -20,9 +20,9 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org */ #pragma once -#include #include "ui/flatbutton.h" #include "ui/countryinput.h" +#include "ui/flatlabel.h" #include "intro/introwidget.h" class IntroPhone final : public IntroStep { diff --git a/Telegram/SourceFiles/intro/introstart.h b/Telegram/SourceFiles/intro/introstart.h index 8bd86075d..c9665476e 100644 --- a/Telegram/SourceFiles/intro/introstart.h +++ b/Telegram/SourceFiles/intro/introstart.h @@ -21,6 +21,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #pragma once #include "intro/introwidget.h" +#include "ui/flatlabel.h" class IntroStart final : public IntroStep { public: diff --git a/Telegram/SourceFiles/profile/profile.style b/Telegram/SourceFiles/profile/profile.style index ad27e820b..756d40781 100644 --- a/Telegram/SourceFiles/profile/profile.style +++ b/Telegram/SourceFiles/profile/profile.style @@ -38,8 +38,14 @@ profilePhotoSize: 112px; profilePhotoLeft: 35px; profileNameLeft: 26px; profileNameTop: 9px; -profileNameFont: font(16px); -profileNameFg: windowTextFg; +profileNameLabel: flatLabel(labelDefFlat) { + margin: margins(10px, 5px, 10px, 5px); + font: font(16px); + width: 160px; + maxHeight: 24px; +} +profileNameTextStyle: textStyle(defaultTextStyle) { +} profileStatusLeft: 27px; profileStatusTop: 35px; profileStatusFont: normalFont; diff --git a/Telegram/SourceFiles/profile/profile_cover.cpp b/Telegram/SourceFiles/profile/profile_cover.cpp index e9bf3666d..d1bc03036 100644 --- a/Telegram/SourceFiles/profile/profile_cover.cpp +++ b/Telegram/SourceFiles/profile/profile_cover.cpp @@ -129,9 +129,13 @@ CoverWidget::CoverWidget(QWidget *parent, PeerData *peer) : TWidget(parent) , _peerChat(peer->asChat()) , _peerChannel(peer->asChannel()) , _peerMegagroup(peer->isMegagroup() ? _peerChannel : nullptr) -, _photoButton(this, peer) { +, _photoButton(this, peer) +, _name(this, QString(), st::profileNameLabel) { setAttribute(Qt::WA_OpaquePaintEvent); + _name.setSelectable(true); + _name.setContextCopyText(lang(lng_profile_copy_fullname)); + auto observeEvents = ButtonsUpdateFlags | Notify::PeerUpdateFlag::NameChanged; Notify::registerPeerObserver(observeEvents, this, &CoverWidget::notifyPeerUpdated); FileDialog::registerObserver(this, &CoverWidget::notifyFileQueryUpdated); @@ -161,7 +165,13 @@ void CoverWidget::resizeToWidth(int newWidth) { _photoButton->moveToLeft(st::profilePhotoLeft, newHeight); int infoLeft = _photoButton->x() + _photoButton->width(); - _namePosition = QPoint(infoLeft + st::profileNameLeft, _photoButton->y() + st::profileNameTop); + int nameLeft = infoLeft + st::profileNameLeft - st::profileNameLabel.margin.left(); + int nameTop = _photoButton->y() + st::profileNameTop - st::profileNameLabel.margin.top(); + _name.moveToLeft(nameLeft, nameTop); + int nameWidth = width() - infoLeft - st::profileNameLeft - st::profileButtonSkip; + nameWidth += st::profileNameLabel.margin.left() + st::profileNameLabel.margin.right(); + _name.resizeToWidth(nameWidth); + _statusPosition = QPoint(infoLeft + st::profileStatusLeft, _photoButton->y() + st::profileStatusTop); int buttonLeft = st::profilePhotoLeft + _photoButton->width() + st::profileButtonLeft; @@ -187,11 +197,6 @@ void CoverWidget::paintEvent(QPaintEvent *e) { p.fillRect(e->rect(), st::profileBg); - int availWidth = width() - _namePosition.x() - _photoButton->x(); - p.setFont(st::profileNameFont); - p.setPen(st::profileNameFg); - _nameText.drawLeftElided(p, _namePosition.x(), _namePosition.y(), availWidth, width()); - p.setFont(st::profileStatusFont); p.setPen(st::profileStatusFg); p.drawTextLeft(_statusPosition.x(), _statusPosition.y(), width(), _statusText); @@ -220,7 +225,7 @@ void CoverWidget::notifyPeerUpdated(const Notify::PeerUpdate &update) { } void CoverWidget::refreshNameText() { - _nameText.setText(st::profileNameFont, App::peerName(_peer)); + _name.setText(App::peerName(_peer)); update(); } diff --git a/Telegram/SourceFiles/profile/profile_cover.h b/Telegram/SourceFiles/profile/profile_cover.h index da3a5dcd5..d0392a451 100644 --- a/Telegram/SourceFiles/profile/profile_cover.h +++ b/Telegram/SourceFiles/profile/profile_cover.h @@ -22,6 +22,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "core/observer.h" #include "ui/filedialog.h" +#include "ui/flatlabel.h" namespace Ui { class RoundButton; @@ -87,8 +88,7 @@ private: // Cover content ChildWidget _photoButton; - QPoint _namePosition; - Text _nameText; + FlatLabel _name; QPoint _statusPosition; QString _statusText; diff --git a/Telegram/SourceFiles/stdafx.h b/Telegram/SourceFiles/stdafx.h index 1d9f12f90..d60e8f486 100644 --- a/Telegram/SourceFiles/stdafx.h +++ b/Telegram/SourceFiles/stdafx.h @@ -68,7 +68,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "ui/scrollarea.h" #include "ui/images.h" #include "ui/text/text.h" -#include "ui/flatlabel.h" #include "app.h" diff --git a/Telegram/SourceFiles/ui/flatlabel.cpp b/Telegram/SourceFiles/ui/flatlabel.cpp index d170ee92d..57f2e272d 100644 --- a/Telegram/SourceFiles/ui/flatlabel.cpp +++ b/Telegram/SourceFiles/ui/flatlabel.cpp @@ -21,6 +21,8 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "stdafx.h" #include "ui/flatlabel.h" +#include "mainwindow.h" +#include "lang.h" namespace { TextParseOptions _labelOptions = { @@ -34,30 +36,59 @@ namespace { FlatLabel::FlatLabel(QWidget *parent, const QString &text, const style::flatLabel &st, const style::textStyle &tst) : TWidget(parent), _text(st.width ? st.width : QFIXED_MAX), _st(st), _tst(tst), _opacity(1) { setRichText(text); + _trippleClickTimer.setSingleShot(true); + + _touchSelectTimer.setSingleShot(true); + connect(&_touchSelectTimer, SIGNAL(timeout()), this, SLOT(onTouchSelect())); } void FlatLabel::setText(const QString &text) { textstyleSet(&_tst); _text.setText(_st.font, text, _labelOptions); - int32 w = _st.width ? _st.width : _text.maxWidth(), h = _text.countHeight(w); + refreshSize(); textstyleRestore(); - resize(w, h); + setMouseTracking(_selectable || _text.hasLinks()); } void FlatLabel::setRichText(const QString &text) { textstyleSet(&_tst); _text.setRichText(_st.font, text, _labelOptions); - int32 w = _st.width ? _st.width : _text.maxWidth(), h = _text.countHeight(w); + refreshSize(); textstyleRestore(); - resize(w, h); - setMouseTracking(_text.hasLinks()); + setMouseTracking(_selectable || _text.hasLinks()); +} + +void FlatLabel::setSelectable(bool selectable) { + _selectable = selectable; + setMouseTracking(_selectable || _text.hasLinks()); +} + +void FlatLabel::setContextCopyText(const QString ©Text) { + _contextCopyText = copyText; } void FlatLabel::resizeToWidth(int32 width) { textstyleSet(&_tst); - int32 w = width, h = _text.countHeight(w); + _allowedWidth = width; + refreshSize(); textstyleRestore(); - resize(w, h); +} + +int FlatLabel::countTextWidth() const { + return _allowedWidth ? (_allowedWidth - _st.margin.left() - _st.margin.right()) : (_st.width ? _st.width : _text.maxWidth()); +} + +int FlatLabel::countTextHeight(int textWidth) { + _fullTextHeight = _text.countHeight(textWidth); + return _st.maxHeight ? qMin(_fullTextHeight, _st.maxHeight) : _fullTextHeight; +} + +void FlatLabel::refreshSize() { + int textWidth = countTextWidth(); + int textHeight = countTextHeight(textWidth); + int fullWidth = _st.margin.left() + textWidth + _st.margin.right(); + int fullHeight = _st.margin.top() + textHeight + _st.margin.bottom(); + resize(fullWidth, fullHeight); } void FlatLabel::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { @@ -66,34 +97,301 @@ void FlatLabel::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { void FlatLabel::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); - updateHover(); + dragActionUpdate(); } void FlatLabel::mousePressEvent(QMouseEvent *e) { - _lastMousePos = e->globalPos(); - updateHover(); + if (_contextMenu) { + e->accept(); + return; // ignore mouse press, that was hiding context menu + } + dragActionStart(e->globalPos(), e->button()); +} + +Text::StateResult FlatLabel::dragActionStart(const QPoint &p, Qt::MouseButton button) { + _lastMousePos = p; + auto state = dragActionUpdate(); + + if (button != Qt::LeftButton) return state; + ClickHandler::pressed(); + _dragAction = NoDrag; + _dragWasInactive = App::wnd()->inactivePress(); + if (_dragWasInactive) App::wnd()->inactivePress(false); + + if (ClickHandler::getPressed()) { + _dragAction = PrepareDrag; + } + if (!_selectable || _dragAction != NoDrag) { + return state; + } + + if (_trippleClickTimer.isActive() && (_lastMousePos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) { + if (state.uponSymbol) { + _selection = { state.symbol, state.symbol }; + _savedSelection = { 0, 0 }; + _dragSymbol = state.symbol; + _dragAction = Selecting; + _selectionType = TextSelectType::Paragraphs; + updateHover(state); + _trippleClickTimer.start(QApplication::doubleClickInterval()); + update(); + } + } + if (_selectionType != TextSelectType::Paragraphs) { + _dragSymbol = state.symbol; + bool uponSelected = state.uponSymbol; + if (uponSelected) { + if (_dragSymbol < _selection.from || _dragSymbol >= _selection.to) { + uponSelected = false; + } + } + if (uponSelected) { + _dragAction = PrepareDrag; // start text drag + } else if (!_dragWasInactive) { + if (state.afterSymbol) ++_dragSymbol; + _selection = { _dragSymbol, _dragSymbol }; + _savedSelection = { 0, 0 }; + _dragAction = Selecting; + update(); + } + } + return state; +} + +Text::StateResult FlatLabel::dragActionFinish(const QPoint &p, Qt::MouseButton button) { + _lastMousePos = p; + auto state = dragActionUpdate(); + + ClickHandlerPtr activated = ClickHandler::unpressed(); + if (_dragAction == Dragging) { + activated.clear(); + } else if (_dragAction == PrepareDrag) { + _selection = { 0, 0 }; + _savedSelection = { 0, 0 }; + update(); + } + _dragAction = NoDrag; + _selectionType = TextSelectType::Letters; + + if (activated) { + App::activateClickHandler(activated, button); + } + return state; } void FlatLabel::mouseReleaseEvent(QMouseEvent *e) { - _lastMousePos = e->globalPos(); - updateHover(); - if (ClickHandlerPtr activated = ClickHandler::unpressed()) { - App::activateClickHandler(activated, e->button()); + dragActionFinish(e->globalPos(), e->button()); + if (!rect().contains(e->pos())) { + leaveEvent(e); + } +} + +void FlatLabel::mouseDoubleClickEvent(QMouseEvent *e) { + auto state = dragActionStart(e->globalPos(), e->button()); + if (((_dragAction == Selecting) || (_dragAction == NoDrag)) && _selectionType == TextSelectType::Letters) { + if (state.uponSymbol) { + _dragSymbol = state.symbol; + _selectionType = TextSelectType::Words; + if (_dragAction == NoDrag) { + _dragAction = Selecting; + _selection = { state.symbol, state.symbol }; + _savedSelection = { 0, 0 }; + } + mouseMoveEvent(e); + + _trippleClickPoint = e->globalPos(); + _trippleClickTimer.start(QApplication::doubleClickInterval()); + } } } void FlatLabel::enterEvent(QEvent *e) { _lastMousePos = QCursor::pos(); - updateHover(); + dragActionUpdate(); } void FlatLabel::leaveEvent(QEvent *e) { ClickHandler::clearActive(this); } +void FlatLabel::focusOutEvent(QFocusEvent *e) { + if (!_selection.empty()) { + _savedSelection = _selection; + _selection = { 0, 0 }; + update(); + } +} + +void FlatLabel::focusInEvent(QFocusEvent *e) { + if (!_savedSelection.empty()) { + _selection = _savedSelection; + _savedSelection = { 0, 0 }; + update(); + } +} + +void FlatLabel::keyPressEvent(QKeyEvent *e) { + e->ignore(); + if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && e->modifiers().testFlag(Qt::ControlModifier))) { + if (!_selection.empty()) { + onCopySelectedText(); + e->accept(); + } + } +} + +void FlatLabel::contextMenuEvent(QContextMenuEvent *e) { + showContextMenu(e, ContextMenuReason::FromEvent); +} + +bool FlatLabel::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 FlatLabel::touchEvent(QTouchEvent *e) { + const Qt::TouchPointStates &states(e->touchPointStates()); + if (e->type() == QEvent::TouchCancel) { // cancel + if (!_touchInProgress) return; + _touchInProgress = false; + _touchSelectTimer.stop(); + _touchSelect = false; + _dragAction = NoDrag; + return; + } + + if (!e->touchPoints().isEmpty()) { + _touchPrevPos = _touchPos; + _touchPos = e->touchPoints().cbegin()->screenPos().toPoint(); + } + + switch (e->type()) { + case QEvent::TouchBegin: + if (_contextMenu) { + e->accept(); + return; // ignore mouse press, that was hiding context menu + } + if (_touchInProgress) return; + if (e->touchPoints().isEmpty()) return; + + _touchInProgress = true; + _touchSelectTimer.start(QApplication::startDragTime()); + _touchSelect = false; + _touchStart = _touchPrevPos = _touchPos; + break; + + case QEvent::TouchUpdate: + if (!_touchInProgress) return; + if (_touchSelect) { + _lastMousePos = _touchPos; + dragActionUpdate(); + } + break; + + case QEvent::TouchEnd: + if (!_touchInProgress) return; + _touchInProgress = false; + if (_touchSelect) { + dragActionFinish(_touchPos, Qt::RightButton); + QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos); + showContextMenu(&contextMenu, ContextMenuReason::FromTouch); + } else { // one short tap -- like mouse click + dragActionStart(_touchPos, Qt::LeftButton); + dragActionFinish(_touchPos, Qt::LeftButton); + } + _touchSelectTimer.stop(); + _touchSelect = false; + break; + } +} + +void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) { + if (_contextMenu) { + _contextMenu->deleteLater(); + _contextMenu = nullptr; + } + + if (e->reason() == QContextMenuEvent::Mouse) { + _lastMousePos = e->globalPos(); + } else { + _lastMousePos = QCursor::pos(); + } + auto state = dragActionUpdate(); + + bool hasSelection = !_selection.empty(); + bool uponSelection = state.uponSymbol && (state.symbol >= _selection.from) && (state.symbol < _selection.to); + bool fullSelection = _text.isFullSelection(_selection); + if (reason == ContextMenuReason::FromTouch && hasSelection && !uponSelection) { + uponSelection = hasSelection; + } + + _contextMenu = new PopupMenu(); + + _contextMenuClickHandler = ClickHandler::getActive(); + + if (fullSelection) { + _contextMenu->addAction(contextCopyText(), this, SLOT(onCopyContextText()))->setEnabled(true); + } else if (uponSelection) { + _contextMenu->addAction(lang(lng_context_copy_selected), this, SLOT(onCopySelectedText()))->setEnabled(true); + } else if (!hasSelection) { + _contextMenu->addAction(contextCopyText(), this, SLOT(onCopyContextText()))->setEnabled(true); + } + + QString linkCopyToClipboardText = _contextMenuClickHandler ? _contextMenuClickHandler->copyToClipboardContextItemText() : QString(); + if (!linkCopyToClipboardText.isEmpty()) { + _contextMenu->addAction(linkCopyToClipboardText, this, SLOT(onCopyContextUrl()))->setEnabled(true); + } + + if (_contextMenu->actions().isEmpty()) { + delete _contextMenu; + _contextMenu = nullptr; + } else { + connect(_contextMenu, SIGNAL(destroyed(QObject*)), this, SLOT(onContextMenuDestroy(QObject*))); + _contextMenu->popup(e->globalPos()); + e->accept(); + } +} + +QString FlatLabel::contextCopyText() const { + return _contextCopyText.isEmpty() ? lang(lng_context_copy_text) : _contextCopyText; +} + +void FlatLabel::onCopySelectedText() { + auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection; + if (!selection.empty()) { + QApplication::clipboard()->setText(_text.originalText(selection, ExpandLinksAll)); + } +} + +void FlatLabel::onCopyContextText() { + QApplication::clipboard()->setText(_text.originalText({ 0, 0xFFFF }, ExpandLinksAll)); +} + +void FlatLabel::onCopyContextUrl() { + if (_contextMenuClickHandler) { + _contextMenuClickHandler->copyToClipboard(); + } +} + +void FlatLabel::onTouchSelect() { + _touchSelect = true; + dragActionStart(_touchPos, Qt::LeftButton); +} + +void FlatLabel::onContextMenuDestroy(QObject *obj) { + if (obj == _contextMenu) { + _contextMenu = nullptr; + } +} + void FlatLabel::clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) { - setCursor(active ? style::cur_pointer : style::cur_default); update(); } @@ -101,21 +399,97 @@ void FlatLabel::clickHandlerPressedChanged(const ClickHandlerPtr &action, bool a update(); } -void FlatLabel::updateLink() { - _lastMousePos = QCursor::pos(); - updateHover(); +Text::StateResult FlatLabel::dragActionUpdate() { + QPoint m(mapFromGlobal(_lastMousePos)); + LOG(("DRAG ACTION UPDATE: %1 %2").arg(m.x()).arg(m.y())); + auto state = getTextState(m); + updateHover(state); + return state; } -void FlatLabel::updateHover() { - QPoint m(mapFromGlobal(_lastMousePos)); +void FlatLabel::updateHover(const Text::StateResult &state) { + bool lnkChanged = ClickHandler::setActive(state.link, this); + + if (!_selectable) { + refreshCursor(state.uponSymbol); + return; + } + + Qt::CursorShape cur = style::cur_default; + if (_dragAction == NoDrag) { + if (state.link) { + cur = style::cur_pointer; + } else if (state.uponSymbol) { + cur = style::cur_text; + } + } else { + if (_dragAction == Selecting) { + uint16 second = state.symbol; + if (state.afterSymbol && _selectionType == TextSelectType::Letters) { + ++second; + } + auto selection = _text.adjustSelection({ qMin(second, _dragSymbol), qMax(second, _dragSymbol) }, _selectionType); + if (_selection != selection) { + _selection = selection; + _savedSelection = { 0, 0 }; + setFocus(); + update(); + } + } else if (_dragAction == Dragging) { + } + + if (ClickHandler::getPressed()) { + cur = style::cur_pointer; + } else if (_dragAction == Selecting) { + cur = style::cur_text; + } + } + if (_dragAction == Selecting) { +// checkSelectingScroll(); + } else { +// noSelectingScroll(); + } + + if (_dragAction == NoDrag && (lnkChanged || cur != _cursor)) { + setCursor(_cursor = cur); + } +} + +void FlatLabel::refreshCursor(bool uponSymbol) { + if (_dragAction != NoDrag) { + return; + } + bool needTextCursor = _selectable && uponSymbol; + style::cursor newCursor = needTextCursor ? style::cur_text : style::cur_default; + if (ClickHandler::getActive()) { + newCursor = style::cur_pointer; + } + if (newCursor != _cursor) { + _cursor = newCursor; + setCursor(_cursor); + } +} + +Text::StateResult FlatLabel::getTextState(const QPoint &m) const { + Text::StateRequestElided request; + request.align = _st.align; + if (_selectable) { + request.flags |= Text::StateRequest::Flag::LookupSymbol; + } + int textWidth = width() - _st.margin.left() - _st.margin.right(); textstyleSet(&_tst); - Text::StateRequest request; - request.align = _st.align; - auto state = _text.getState(m.x(), m.y(), width(), request); + Text::StateResult state; + if (_st.maxHeight && _st.maxHeight < _fullTextHeight) { + auto lineHeight = qMax(_tst.lineHeight, _st.font->height); + request.lines = qMax(_st.maxHeight / lineHeight, 1); + state = _text.getStateElided(m.x() - _st.margin.left(), m.y() - _st.margin.top(), textWidth, request); + } else { + state = _text.getState(m.x() - _st.margin.left(), m.y() - _st.margin.top(), textWidth, request); + } textstyleRestore(); - ClickHandler::setActive(state.link, this); + return state; } void FlatLabel::setOpacity(float64 o) { @@ -124,9 +498,18 @@ void FlatLabel::setOpacity(float64 o) { } void FlatLabel::paintEvent(QPaintEvent *e) { - QPainter p(this); + Painter p(this); p.setOpacity(_opacity); + p.setPen(_st.textFg); textstyleSet(&_tst); - _text.draw(p, 0, 0, width(), _st.align, e->rect().y(), e->rect().bottom()); + int textWidth = width() - _st.margin.left() - _st.margin.right(); + auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection; + if (_st.maxHeight && _st.maxHeight < _fullTextHeight) { + auto lineHeight = qMax(_tst.lineHeight, _st.font->height); + auto lines = qMax(_st.maxHeight / lineHeight, 1); + _text.drawElided(p, _st.margin.left(), _st.margin.top(), textWidth, lines, _st.align, e->rect().y(), e->rect().bottom(), 0, false, selection); + } else { + _text.draw(p, _st.margin.left(), _st.margin.top(), textWidth, _st.align, e->rect().y(), e->rect().bottom(), selection); + } textstyleRestore(); } diff --git a/Telegram/SourceFiles/ui/flatlabel.h b/Telegram/SourceFiles/ui/flatlabel.h index a881bd2ac..159ddf6a0 100644 --- a/Telegram/SourceFiles/ui/flatlabel.h +++ b/Telegram/SourceFiles/ui/flatlabel.h @@ -24,20 +24,14 @@ class FlatLabel : public TWidget, public ClickHandlerHost { Q_OBJECT public: - FlatLabel(QWidget *parent, const QString &text, const style::flatLabel &st = st::labelDefFlat, const style::textStyle &tst = st::defaultTextStyle); - void paintEvent(QPaintEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void enterEvent(QEvent *e) override; - void leaveEvent(QEvent *e) override; - void updateLink(); void setOpacity(float64 o); void setText(const QString &text); void setRichText(const QString &text); + void setSelectable(bool selectable); + void setContextCopyText(const QString ©Text); void resizeToWidth(int32 width); @@ -47,15 +41,85 @@ public: void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) override; void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) override; -private: +protected: + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseDoubleClickEvent(QMouseEvent *e) override; + void enterEvent(QEvent *e) override; + void leaveEvent(QEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + bool event(QEvent *e) override; // calls touchEvent when necessary + void touchEvent(QTouchEvent *e); - void updateHover(); +private slots: + void onCopySelectedText(); + void onCopyContextText(); + void onCopyContextUrl(); + + void onTouchSelect(); + void onContextMenuDestroy(QObject *obj); + +private: + Text::StateResult dragActionUpdate(); + Text::StateResult dragActionStart(const QPoint &p, Qt::MouseButton button); + Text::StateResult dragActionFinish(const QPoint &p, Qt::MouseButton button); + void updateHover(const Text::StateResult &state); + Text::StateResult getTextState(const QPoint &m) const; + void refreshCursor(bool uponSymbol); + + int countTextWidth() const; + int countTextHeight(int textWidth); + void refreshSize(); + + enum class ContextMenuReason { + FromEvent, + FromTouch, + }; + void showContextMenu(QContextMenuEvent *e, ContextMenuReason reason); + QString contextCopyText() const; Text _text; style::flatLabel _st; style::textStyle _tst; float64 _opacity; + int _allowedWidth = 0; + int _fullTextHeight = 0; + + style::cursor _cursor = style::cur_default; + bool _selectable = false; + TextSelection _selection, _savedSelection; + TextSelectType _selectionType = TextSelectType::Letters; + + enum DragAction { + NoDrag = 0x00, + PrepareDrag = 0x01, + Dragging = 0x02, + PrepareSelect = 0x03, + Selecting = 0x04, + }; + DragAction _dragAction = NoDrag; + uint16 _dragSymbol = 0; + bool _dragWasInactive = false; + QPoint _lastMousePos; + QPoint _trippleClickPoint; + QTimer _trippleClickTimer; + + PopupMenu *_contextMenu = nullptr; + ClickHandlerPtr _contextMenuClickHandler; + QString _contextCopyText; + + // text selection and context menu by touch support (at least Windows Surface tablets) + bool _touchSelect = false; + bool _touchInProgress = false; + QPoint _touchStart, _touchPrevPos, _touchPos; + QTimer _touchSelectTimer; + }; diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index f828a2f28..18b42542c 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -1181,7 +1181,13 @@ public: bool drawLine(uint16 _lineEnd, const Text::TextBlocks::const_iterator &_endBlockIter, const Text::TextBlocks::const_iterator &_end) { _yDelta = (_lineHeight - _fontHeight) / 2; if (_yTo >= 0 && _y + _yDelta >= _yTo) return false; - if (_y + _yDelta + _fontHeight <= _yFrom) return true; + if (_y + _yDelta + _fontHeight <= _yFrom) { + if (_lookupSymbol) { + _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; + _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; + } + return true; + } uint16 trimmedLineEnd = _lineEnd; for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { @@ -2898,7 +2904,7 @@ TextSelection Text::adjustSelection(TextSelection selection, TextSelectType sele uint16 from = selection.from, to = selection.to; if (from < _text.size() && from <= to) { if (to > _text.size()) to = _text.size(); - if (selectType == TextSelectParagraphs) { + if (selectType == TextSelectType::Paragraphs) { if (!chIsParagraphSeparator(_text.at(from))) { while (from > 0 && !chIsParagraphSeparator(_text.at(from - 1))) { --from; @@ -2913,7 +2919,7 @@ TextSelection Text::adjustSelection(TextSelection selection, TextSelectType sele } } } - } else if (selectType == TextSelectWords) { + } else if (selectType == TextSelectType::Words) { if (!chIsWordSeparator(_text.at(from))) { while (from > 0 && !chIsWordSeparator(_text.at(from - 1))) { --from; diff --git a/Telegram/SourceFiles/ui/text/text.h b/Telegram/SourceFiles/ui/text/text.h index d9ea6e798..c26189a9e 100644 --- a/Telegram/SourceFiles/ui/text/text.h +++ b/Telegram/SourceFiles/ui/text/text.h @@ -53,10 +53,10 @@ struct TextParseOptions { }; extern const TextParseOptions _defaultOptions, _textPlainOptions; -enum TextSelectType { - TextSelectLetters = 0x01, - TextSelectWords = 0x02, - TextSelectParagraphs = 0x03, +enum class TextSelectType { + Letters = 0x01, + Words = 0x02, + Paragraphs = 0x03, }; struct TextSelection { @@ -168,6 +168,9 @@ public: } TextSelection adjustSelection(TextSelection selection, TextSelectType selectType) const; + bool isFullSelection(TextSelection selection) const { + return (selection.from == 0) && (selection.to >= _text.size()); + } bool isEmpty() const { return _text.isEmpty();