Added check views to selection mode of messages.

This commit is contained in:
23rd 2024-10-11 23:04:56 +03:00 committed by John Preston
parent 9064f3ba4b
commit 7b4bd5696b
12 changed files with 242 additions and 39 deletions

View file

@ -662,8 +662,8 @@ bool InnerWidget::elementUnderCursor(
return (Element::Hovered() == view);
}
bool InnerWidget::elementInSelectionMode() {
return false;
HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode() {
return {};
}
bool InnerWidget::elementIntersectsRange(

View file

@ -94,7 +94,7 @@ public:
HistoryView::Context elementContext() override;
bool elementUnderCursor(
not_null<const HistoryView::Element*> view) override;
bool elementInSelectionMode() override;
HistoryView::SelectionModeResult elementInSelectionMode() override;
bool elementIntersectsRange(
not_null<const HistoryView::Element*> view,
int from,

View file

@ -43,8 +43,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/report_box_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/controls/delete_message_context_action.h"
#include "ui/painter.h"
#include "ui/inactive_press.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
@ -210,8 +211,10 @@ public:
not_null<const Element*> view) override {
return (Element::Moused() == view);
}
bool elementInSelectionMode() override {
return _widget ? _widget->inSelectionMode() : false;
HistoryView::SelectionModeResult elementInSelectionMode() override {
return _widget
? _widget->inSelectionMode()
: HistoryView::SelectionModeResult();
}
bool elementIntersectsRange(
not_null<const Element*> view,
@ -599,7 +602,7 @@ void HistoryInner::setupSwipeReply() {
}
}, [=, show = _controller->uiShow()](int cursorTop) {
auto result = HistoryView::SwipeHandlerFinishData();
if (inSelectionMode()) {
if (inSelectionMode().inSelectionMode) {
return result;
}
enumerateItems<EnumItemsDirection::BottomToTop>([&](
@ -1369,6 +1372,18 @@ bool HistoryInner::eventHook(QEvent *e) {
return RpWidget::eventHook(e);
}
int HistoryInner::SelectionViewOffset(
not_null<const HistoryInner*> inner,
not_null<const Element*> view) {
if (inner->_lastInSelectionMode) {
const auto translation
= Element::AdditionalSpaceForSelectionCheckbox(view);
const auto progress = inner->_inSelectionModeAnimation.value(1.);
return translation * progress;
}
return 0;
}
HistoryInner::VideoUserpic *HistoryInner::validateVideoUserpic(
not_null<PeerData*> peer) {
if (!peer->isPremium()
@ -1705,7 +1720,7 @@ void HistoryInner::mouseActionStart(const QPoint &screenPos, Qt::MouseButton but
&& (!Element::Hovered()
|| !Element::Hovered()->allowTextSelectionByHandler(pressed))) {
_mouseAction = MouseAction::PrepareDrag;
} else if (inSelectionMode()) {
} else if (inSelectionMode().inSelectionMode) {
if (_dragStateItem
&& _selected.find(_dragStateItem) != _selected.cend()
&& Element::Hovered()) {
@ -1993,7 +2008,7 @@ void HistoryInner::mouseActionFinish(
// toggle selection instead of activating the pressed link
if (_mouseAction == MouseAction::PrepareDrag
&& !_pressWasInactive
&& inSelectionMode()
&& inSelectionMode().inSelectionMode
&& button != Qt::RightButton) {
if (const auto view = viewByItem(_mouseActionItem)) {
if (view->toggleSelectionByHandlerClick(activated)) {
@ -2026,7 +2041,7 @@ void HistoryInner::mouseActionFinish(
}
if ((_mouseAction == MouseAction::PrepareSelect)
&& !_pressWasInactive
&& inSelectionMode()) {
&& inSelectionMode().inSelectionMode) {
changeSelectionAsGroup(
&_selected,
_mouseActionItem,
@ -2043,7 +2058,7 @@ void HistoryInner::mouseActionFinish(
} else if ((i == _selected.cend())
&& !_dragStateItem->isService()
&& _dragStateItem->isRegular()
&& inSelectionMode()) {
&& inSelectionMode().inSelectionMode) {
if (_selected.size() < MaxSelectedItems) {
_selected.emplace(_dragStateItem, FullSelection);
repaintItem(_mouseActionItem);
@ -2127,7 +2142,7 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
&& !ClickHandler::getPressed()
&& (_mouseCursorState == CursorState::None
|| _mouseCursorState == CursorState::Date)
&& !inSelectionMode()
&& !inSelectionMode().inSelectionMode
&& !_emptyPainter
&& e->button() == Qt::LeftButton) {
if (const auto view = Element::Moused()) {
@ -3638,17 +3653,46 @@ bool HistoryInner::canDeleteSelected() const {
&& (selectedState.count == selectedState.canDeleteCount);
}
bool HistoryInner::inSelectionMode() const {
if (hasSelectedItems()) {
return true;
} else if (_mouseAction == MouseAction::Selecting
&& _dragSelFrom
&& _dragSelTo) {
return true;
} else if (_chooseForReportReason.has_value()) {
return true;
HistoryView::SelectionModeResult HistoryInner::inSelectionMode() const {
const auto inSelectionMode = [&] {
if (hasSelectedItems()) {
return true;
} else if (_mouseAction == MouseAction::Selecting
&& _dragSelFrom
&& _dragSelTo) {
return true;
} else if (_chooseForReportReason.has_value()) {
return true;
}
return false;
}();
const auto now = inSelectionMode;
if (_lastInSelectionMode != now) {
_lastInSelectionMode = now;
if (_inSelectionModeAnimation.animating()) {
const auto progress = !now
? _inSelectionModeAnimation.value(0.)
: 1. - _inSelectionModeAnimation.value(0.);
_inSelectionModeAnimation.change(
now ? 1. : 0.,
st::universalDuration * (1. - progress));
} else {
_inSelectionModeAnimation.stop();
_inSelectionModeAnimation.start(
[this] {
const_cast<HistoryInner*>(this)->update(
QRect(
0,
_visibleAreaTop,
width(),
_visibleAreaBottom - _visibleAreaTop));
},
now ? 0. : 1.,
now ? 1. : 0.,
st::universalDuration);
}
}
return false;
return { now, _inSelectionModeAnimation.value(now ? 1. : 0.) };
}
bool HistoryInner::elementIntersectsRange(
@ -3835,7 +3879,7 @@ auto HistoryInner::reactionButtonParameters(
|| !view->data()->canReact()
|| _mouseAction == MouseAction::Dragging
|| _mouseAction == MouseAction::Selecting
|| inSelectionMode()) {
|| inSelectionMode().inSelectionMode) {
return {};
}
auto result = view->reactionButtonParameters(
@ -3856,7 +3900,7 @@ void HistoryInner::mouseActionUpdate() {
auto mousePos = mapFromGlobal(_mousePosition);
auto point = _widget->clampMousePosition(mousePos);
QPoint m;
auto m = QPoint();
adjustCurrent(point.y());
const auto reactionState = _reactionsManager->buttonTextState(point);
@ -3873,6 +3917,10 @@ void HistoryInner::mouseActionUpdate() {
? _curHistory->blocks[_curBlock]->messages[_curItem].get()
: nullptr;
const auto item = view ? view->data().get() : nullptr;
const auto selectionViewOffset = view
? QPoint(SelectionViewOffset(this, view), 0)
: QPoint(0, 0);
point -= selectionViewOffset;
if (view) {
const auto changed = (Element::Moused() != view);
if (changed) {
@ -3920,7 +3968,7 @@ void HistoryInner::mouseActionUpdate() {
dragState = reactionState;
lnkhost = reactionView;
} else if (item) {
if (item != _mouseActionItem || (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
if (item != _mouseActionItem || ((m + selectionViewOffset) - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
if (_mouseAction == MouseAction::PrepareDrag) {
_mouseAction = MouseAction::Dragging;
InvokeQueued(this, [=] { performDrag(); });

View file

@ -30,6 +30,7 @@ namespace HistoryView {
class ElementDelegate;
class EmojiInteractions;
struct TextState;
struct SelectionModeResult;
struct StateRequest;
enum class CursorState : char;
enum class PointState : char;
@ -135,7 +136,7 @@ public:
void clearSelected(bool onlyTextSelection = false);
[[nodiscard]] MessageIdsList getSelectedItems() const;
[[nodiscard]] bool hasSelectedItems() const;
[[nodiscard]] bool inSelectionMode() const;
[[nodiscard]] HistoryView::SelectionModeResult inSelectionMode() const;
[[nodiscard]] bool elementIntersectsRange(
not_null<const Element*> view,
int from,
@ -243,6 +244,10 @@ private:
void onTouchSelect();
void onTouchScrollTimer();
[[nodiscard]] static int SelectionViewOffset(
not_null<const HistoryInner*> inner,
not_null<const Element*> view);
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
using VideoUserpic = Dialogs::Ui::VideoUserpic;
using SelectedItems = std::map<HistoryItem*, TextSelection, std::less<>>;
@ -508,6 +513,9 @@ private:
bool _dragSelecting = false;
bool _wasSelectedText = false; // was some text selected in current drag action
mutable bool _lastInSelectionMode = false;
mutable Ui::Animations::Simple _inSelectionModeAnimation;
// scroll by touch support (at least Windows Surface tablets)
bool _touchScroll = false;
bool _touchSelect = false;

View file

@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/item_text_options.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "data/data_forum.h"
@ -110,8 +111,8 @@ bool DefaultElementDelegate::elementUnderCursor(
return false;
}
bool DefaultElementDelegate::elementInSelectionMode() {
return false;
SelectionModeResult DefaultElementDelegate::elementInSelectionMode() {
return {};
}
bool DefaultElementDelegate::elementIntersectsRange(
@ -740,6 +741,25 @@ not_null<PurchasedTag*> Element::enforcePurchasedTag() {
return Get<PurchasedTag>();
}
int Element::AdditionalSpaceForSelectionCheckbox(
not_null<const Element*> view,
QRect countedGeometry) {
if (!view->hasOutLayout() || view->delegate()->elementIsChatWide()) {
return 0;
}
if (countedGeometry.isEmpty()) {
countedGeometry = view->innerGeometry();
}
const auto diff = view->width()
- (countedGeometry.x() + countedGeometry.width())
- st::msgPadding.right()
- st::msgSelectionOffset
- view->rightActionSize().value_or(QSize()).width();
return (diff < 0)
? -(std::min(st::msgSelectionOffset, -diff))
: 0;
}
void Element::refreshMedia(Element *replacing) {
if (_flags & Flag::MediaOverriden) {
return;

View file

@ -70,12 +70,17 @@ enum class OnlyEmojiAndSpaces : char {
No,
};
struct SelectionModeResult {
bool inSelectionMode = false;
float64 progress = 0.0;
};
class Element;
class ElementDelegate {
public:
virtual Context elementContext() = 0;
virtual bool elementUnderCursor(not_null<const Element*> view) = 0;
virtual bool elementInSelectionMode() = 0;
virtual SelectionModeResult elementInSelectionMode() = 0;
virtual bool elementIntersectsRange(
not_null<const Element*> view,
int from,
@ -131,7 +136,7 @@ public:
class DefaultElementDelegate : public ElementDelegate {
public:
bool elementUnderCursor(not_null<const Element*> view) override;
bool elementInSelectionMode() override;
SelectionModeResult elementInSelectionMode() override;
bool elementIntersectsRange(
not_null<const Element*> view,
int from,
@ -571,6 +576,10 @@ public:
[[nodiscard]] not_null<PurchasedTag*> enforcePurchasedTag();
[[nodiscard]] static int AdditionalSpaceForSelectionCheckbox(
not_null<const Element*> view,
QRect countedGeometry = QRect());
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
return false;
}

View file

@ -1229,8 +1229,34 @@ bool ListWidget::hasSelectedItems() const {
return !_selected.empty();
}
bool ListWidget::inSelectionMode() const {
return hasSelectedItems() || !_dragSelected.empty();
SelectionModeResult ListWidget::inSelectionMode() const {
const auto now = hasSelectedItems() || !_dragSelected.empty();
if (_lastInSelectionMode != now) {
_lastInSelectionMode = now;
if (_inSelectionModeAnimation.animating()) {
const auto progress = !now
? _inSelectionModeAnimation.value(0.)
: 1. - _inSelectionModeAnimation.value(0.);
_inSelectionModeAnimation.change(
now ? 1. : 0.,
st::universalDuration * (1. - progress));
} else {
_inSelectionModeAnimation.stop();
_inSelectionModeAnimation.start(
[this] {
const_cast<ListWidget*>(this)->update(
QRect(
0,
_visibleTop,
width(),
_visibleBottom - _visibleTop));
},
now ? 0. : 1.,
now ? 1. : 0.,
st::universalDuration);
}
}
return { now, _inSelectionModeAnimation.value(now ? 1. : 0.) };
}
bool ListWidget::overSelectedItems() const {
@ -1751,7 +1777,7 @@ bool ListWidget::elementUnderCursor(
return (_overElement == view);
}
bool ListWidget::elementInSelectionMode() {
SelectionModeResult ListWidget::elementInSelectionMode() {
return inSelectionMode();
}
@ -3410,7 +3436,7 @@ Reactions::ButtonParameters ListWidget::reactionButtonParameters(
if (top < 0
|| !view->data()->canReact()
|| _mouseAction == MouseAction::Dragging
|| inSelectionMode()) {
|| inSelectionMode().inSelectionMode) {
return {};
}
auto result = view->reactionButtonParameters(
@ -3536,6 +3562,19 @@ ClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) {
};
}
int ListWidget::SelectionViewOffset(
not_null<const ListWidget*> inner,
not_null<const Element*> view) {
if (inner->_lastInSelectionMode) {
const auto translation
= Element::AdditionalSpaceForSelectionCheckbox(view);
const auto progress = inner->_inSelectionModeAnimation.value(1.);
return translation * progress;
}
return 0;
}
void ListWidget::mouseActionUpdate() {
auto mousePosition = mapFromGlobal(_mousePosition);
auto point = QPoint(
@ -3551,6 +3590,9 @@ void ListWidget::mouseActionUpdate() {
? reactionView
: strictFindItemByY(point.y());
const auto item = view ? view->data().get() : nullptr;
if (view) {
point -= QPoint(SelectionViewOffset(this, view), 0);
}
const auto itemPoint = mapPointToItem(point, view);
_overState = MouseState(
item ? item->fullId() : FullMsgId(),

View file

@ -389,7 +389,7 @@ public:
// ElementDelegate interface.
Context elementContext() override;
bool elementUnderCursor(not_null<const Element*> view) override;
bool elementInSelectionMode() override;
SelectionModeResult elementInSelectionMode() override;
bool elementIntersectsRange(
not_null<const Element*> view,
int from,
@ -459,6 +459,10 @@ protected:
int resizeGetHeight(int newWidth) override;
private:
[[nodiscard]] static int SelectionViewOffset(
not_null<const ListWidget*> inner,
not_null<const Element*> view);
using ScrollTopState = ListMemento::ScrollTopState;
using PointState = HistoryView::PointState;
using CursorState = HistoryView::CursorState;
@ -618,7 +622,7 @@ private:
const SelectedMap::const_iterator &i);
bool hasSelectedText() const;
bool hasSelectedItems() const;
bool inSelectionMode() const;
SelectionModeResult inSelectionMode() const;
bool overSelectedItems() const;
void clearTextSelection();
void clearSelected();
@ -830,6 +834,9 @@ private:
ElementHighlighter _highlighter;
mutable bool _lastInSelectionMode = false;
mutable Ui::Animations::Simple _inSelectionModeAnimation;
// scroll by touch support (at least Windows Surface tablets)
bool _touchScroll = false;
bool _touchSelect = false;

View file

@ -1100,6 +1100,14 @@ void Message::draw(Painter &p, const PaintContext &context) const {
if (hasGesture) {
p.translate(context.gestureHorizontal.translation, 0);
}
const auto selectionModeResult = delegate()->elementInSelectionMode();
const auto selectionTranslation = (selectionModeResult.progress > 0)
? (selectionModeResult.progress
* AdditionalSpaceForSelectionCheckbox(this, g))
: 0;
if (selectionTranslation) {
p.translate(selectionTranslation, 0);
}
if (item->hasUnrequestedFactcheck()) {
item->history()->session().factchecks().requestFor(item);
@ -1449,7 +1457,14 @@ void Message::draw(Painter &p, const PaintContext &context) const {
const auto fastShareTop = data()->isSponsored()
? g.top() + fastShareSkip
: g.top() + g.height() - fastShareSkip - size->height();
const auto o = p.opacity();
if (selectionModeResult.progress > 0) {
p.setOpacity(1. - selectionModeResult.progress);
}
drawRightAction(p, context, fastShareLeft, fastShareTop, width());
if (selectionModeResult.progress > 0) {
p.setOpacity(o);
}
}
if (media) {
@ -1570,6 +1585,48 @@ void Message::draw(Painter &p, const PaintContext &context) const {
}
p.restore();
}
if (selectionTranslation) {
p.translate(-selectionTranslation, 0);
}
if (selectionModeResult.progress) {
const auto progress = selectionModeResult.progress;
if (progress <= 1.) {
if (context.selected()) {
if (!_selectionRoundCheckbox) {
_selectionRoundCheckbox
= std::make_unique<Ui::RoundCheckbox>(
st::msgSelectionCheck,
[this] { repaint(); });
}
}
if (_selectionRoundCheckbox) {
_selectionRoundCheckbox->setChecked(
context.selected(),
anim::type::normal);
}
const auto o = ScopedPainterOpacity(p, progress);
const auto &st = st::msgSelectionCheck;
const auto pos = QPoint(
(width()
- (st::msgSelectionOffset * progress - st.size) / 2
- st::msgPadding.right() / 2
- st.size),
g.y() + (g.height() - st.size) / 2);
{
p.setPen(QPen(st.border, st.width));
p.setBrush(context.st->msgServiceBg());
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(QRect(pos, Size(st.size)));
}
if (_selectionRoundCheckbox) {
_selectionRoundCheckbox->paint(p, pos.x(), pos.y(), width());
}
} else {
_selectionRoundCheckbox = nullptr;
}
} else {
_selectionRoundCheckbox = nullptr;
}
}
void Message::paintCommentsButton(
@ -2959,7 +3016,9 @@ bool Message::getStateText(
void Message::updatePressed(QPoint point) {
const auto item = data();
const auto media = this->media();
if (!media) return;
if (!media) {
return;
}
auto g = countGeometry();
auto keyboard = item->inlineReplyKeyboard();
@ -3792,7 +3851,7 @@ bool Message::displayFastReply() const {
return hasFastReply()
&& data()->isRegular()
&& canSendAnything()
&& !delegate()->elementInSelectionMode();
&& !delegate()->elementInSelectionMode().inSelectionMode;
}
bool Message::displayRightActionComments() const {
@ -3956,6 +4015,9 @@ void Message::drawRightAction(
ClickHandlerPtr Message::rightActionLink(
std::optional<QPoint> pressPoint) const {
if (delegate()->elementInSelectionMode().progress > 0) {
return nullptr;
}
ensureRightAction();
if (!_rightAction->link) {
_rightAction->link = prepareRightActionLink();

View file

@ -22,6 +22,7 @@ struct ReactionId;
namespace Ui {
struct BubbleRounding;
class RoundCheckbox;
} // namespace Ui
namespace HistoryView {
@ -313,6 +314,7 @@ private:
mutable Ui::Text::String _fromName;
mutable std::unique_ptr<FromNameStatus> _fromNameStatus;
mutable std::unique_ptr<Ui::RoundCheckbox> _selectionRoundCheckbox;
Ui::Text::String _rightBadge;
mutable int _fromNameVersion = 0;
uint32 _bubbleWidthLimit : 28 = 0;

View file

@ -898,7 +898,7 @@ void RepliesWidget::setupSwipeReply() {
}
}, [=, show = controller()->uiShow()](int cursorTop) {
auto result = HistoryView::SwipeHandlerFinishData();
if (_inner->elementInSelectionMode()) {
if (_inner->elementInSelectionMode().inSelectionMode) {
return result;
}
const auto view = _inner->lookupItemByY(cursorTop);

View file

@ -26,6 +26,7 @@ msgPadding: margins(11px, 8px, 11px, 8px);
msgMargin: margins(16px, 6px, 56px, 2px);
msgMarginTopAttached: 0px;
msgShadow: 2px;
msgSelectionOffset: 30px;
historyReplyTop: 2px;
historyReplyBottom: 2px;
@ -1149,3 +1150,7 @@ factcheckField: InputField(defaultInputField) {
style: defaultTextStyle;
}
purchasedTagPadding: margins(3px, 2px, 6px, 2px);
msgSelectionCheck: RoundCheckbox(defaultPeerListCheck) {
bgActive: boxTextFgGood;
}