refactor: simplify double click selection (#4898)

This commit is contained in:
kornes 2023-10-17 11:38:38 +00:00 committed by GitHub
parent b975900043
commit 12808d3154
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 200 deletions

View file

@ -22,6 +22,7 @@
- Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873) - Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873)
- Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875) - Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875)
- Bugfix: Fixed the input completion popup from disappearing when clicking on it on Windows and macOS. (#4876) - Bugfix: Fixed the input completion popup from disappearing when clicking on it on Windows and macOS. (#4876)
- Bugfix: Fixed double-click text selection moving its position with each new message. (#4898)
- Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899) - Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767) - Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <algorithm>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <tuple> #include <tuple>
@ -72,6 +73,13 @@ struct Selection {
return !this->operator==(b); return !this->operator==(b);
} }
//union of both selections
Selection operator|(const Selection &b) const
{
return {std::min(this->selectionMin, b.selectionMin),
std::max(this->selectionMax, b.selectionMax)};
}
bool isEmpty() const bool isEmpty() const
{ {
return this->start == this->end; return this->start == this->end;
@ -127,15 +135,4 @@ struct Selection {
} }
} }
}; };
struct DoubleClickSelection {
uint32_t originalStart{0};
uint32_t originalEnd{0};
uint32_t origMessageIndex{0};
bool selectingLeft{false};
bool selectingRight{false};
SelectionItem origStartItem;
SelectionItem origEndItem;
};
} // namespace chatterino } // namespace chatterino

View file

@ -385,6 +385,7 @@ void ChannelView::unpaused()
{ {
/// Move selection /// Move selection
this->selection_.shiftMessageIndex(this->pauseSelectionOffset_); this->selection_.shiftMessageIndex(this->pauseSelectionOffset_);
this->doubleClickSelection_.shiftMessageIndex(this->pauseSelectionOffset_);
this->pauseSelectionOffset_ = 0; this->pauseSelectionOffset_ = 0;
} }
@ -927,6 +928,7 @@ void ChannelView::messageAppended(MessagePtr &message,
this->scrollBar_->scrollToBottom(false); this->scrollBar_->scrollToBottom(false);
} }
this->selection_.shiftMessageIndex(1); this->selection_.shiftMessageIndex(1);
this->doubleClickSelection_.shiftMessageIndex(1);
} }
} }
@ -1088,10 +1090,8 @@ void ChannelView::resizeEvent(QResizeEvent *)
this->update(); this->update();
} }
void ChannelView::setSelection(const SelectionItem &start, void ChannelView::setSelection(const Selection &newSelection)
const SelectionItem &end)
{ {
auto newSelection = Selection(start, end);
if (this->selection_ != newSelection) if (this->selection_ != newSelection)
{ {
this->selection_ = newSelection; this->selection_ = newSelection;
@ -1100,6 +1100,12 @@ void ChannelView::setSelection(const SelectionItem &start,
} }
} }
void ChannelView::setSelection(const SelectionItem &start,
const SelectionItem &end)
{
this->setSelection({start, end});
}
MessageElementFlags ChannelView::getFlags() const MessageElementFlags ChannelView::getFlags() const
{ {
auto app = getApp(); auto app = getApp();
@ -1512,15 +1518,31 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
this->currentMousePosition_ = event->screenPos(); this->currentMousePosition_ = event->screenPos();
} }
// is selecting // check for word underneath cursor
const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos);
// selecting single characters
if (this->isLeftMouseDown_) if (this->isLeftMouseDown_)
{ {
auto index = layout->getSelectionIndex(relativePos); auto index = layout->getSelectionIndex(relativePos);
this->setSelection(this->selection_.start, this->setSelection(this->selection_.start,
SelectionItem(messageIndex, index)); SelectionItem(messageIndex, index));
} }
// selecting whole words
if (this->isDoubleClick_ && hoverLayoutElement)
{
auto [wordStart, wordEnd] =
this->getWordBounds(layout.get(), hoverLayoutElement, relativePos);
auto hoveredWord = Selection{SelectionItem(messageIndex, wordStart),
SelectionItem(messageIndex, wordEnd)};
// combined selection spanning from initially selected word to hoveredWord
auto selectUnion = this->doubleClickSelection_ | hoveredWord;
this->setSelection(selectUnion);
}
// message under cursor is collapsed // message under cursor is collapsed
if (layout->flags.has(MessageLayoutFlag::Collapsed)) if (layout->flags.has(MessageLayoutFlag::Collapsed))
{ {
@ -1529,10 +1551,6 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
return; return;
} }
// check if word underneath cursor
const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos);
if (hoverLayoutElement == nullptr) if (hoverLayoutElement == nullptr)
{ {
this->setCursor(Qt::ArrowCursor); this->setCursor(Qt::ArrowCursor);
@ -1540,142 +1558,6 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
return; return;
} }
if (this->isDoubleClick_)
{
int wordStart;
int wordEnd;
this->getWordBounds(layout.get(), hoverLayoutElement, relativePos,
wordStart, wordEnd);
SelectionItem newStart(messageIndex, wordStart);
SelectionItem newEnd(messageIndex, wordEnd);
// Selection changed in same message
if (messageIndex == this->doubleClickSelection_.origMessageIndex)
{
// Selecting to the left
if (wordStart < this->selection_.start.charIndex &&
!this->doubleClickSelection_.selectingRight)
{
this->doubleClickSelection_.selectingLeft = true;
// Ensure that the original word stays selected(Edge case)
if (wordStart > this->doubleClickSelection_.originalEnd)
{
this->setSelection(
this->doubleClickSelection_.origStartItem, newEnd);
}
else
{
this->setSelection(newStart, this->selection_.end);
}
// Selecting to the right
}
else if (wordEnd > this->selection_.end.charIndex &&
!this->doubleClickSelection_.selectingLeft)
{
this->doubleClickSelection_.selectingRight = true;
// Ensure that the original word stays selected(Edge case)
if (wordEnd < this->doubleClickSelection_.originalStart)
{
this->setSelection(newStart,
this->doubleClickSelection_.origEndItem);
}
else
{
this->setSelection(this->selection_.start, newEnd);
}
}
// Swapping from selecting left to selecting right
if (wordStart > this->selection_.start.charIndex &&
!this->doubleClickSelection_.selectingRight)
{
if (wordStart > this->doubleClickSelection_.originalEnd)
{
this->doubleClickSelection_.selectingLeft = false;
this->doubleClickSelection_.selectingRight = true;
this->setSelection(
this->doubleClickSelection_.origStartItem, newEnd);
}
else
{
this->setSelection(newStart, this->selection_.end);
}
// Swapping from selecting right to selecting left
}
else if (wordEnd < this->selection_.end.charIndex &&
!this->doubleClickSelection_.selectingLeft)
{
if (wordEnd < this->doubleClickSelection_.originalStart)
{
this->doubleClickSelection_.selectingLeft = true;
this->doubleClickSelection_.selectingRight = false;
this->setSelection(newStart,
this->doubleClickSelection_.origEndItem);
}
else
{
this->setSelection(this->selection_.start, newEnd);
}
}
// Selection changed in a different message
}
else
{
// Message over the original
if (messageIndex < this->selection_.start.messageIndex)
{
// Swapping from left to right selecting
if (!this->doubleClickSelection_.selectingLeft)
{
this->doubleClickSelection_.selectingLeft = true;
this->doubleClickSelection_.selectingRight = false;
}
if (wordStart < this->selection_.start.charIndex &&
!this->doubleClickSelection_.selectingRight)
{
this->doubleClickSelection_.selectingLeft = true;
}
this->setSelection(newStart,
this->doubleClickSelection_.origEndItem);
// Message under the original
}
else if (messageIndex > this->selection_.end.messageIndex)
{
// Swapping from right to left selecting
if (!this->doubleClickSelection_.selectingRight)
{
this->doubleClickSelection_.selectingLeft = false;
this->doubleClickSelection_.selectingRight = true;
}
if (wordEnd > this->selection_.end.charIndex &&
!this->doubleClickSelection_.selectingLeft)
{
this->doubleClickSelection_.selectingRight = true;
}
this->setSelection(this->doubleClickSelection_.origStartItem,
newEnd);
// Selection changed in non original message
}
else
{
if (this->doubleClickSelection_.selectingLeft)
{
this->setSelection(newStart, this->selection_.end);
}
else
{
this->setSelection(this->selection_.start, newEnd);
}
}
}
// Reset direction of selection
if (wordStart == this->doubleClickSelection_.originalStart &&
wordEnd == this->doubleClickSelection_.originalEnd)
{
this->doubleClickSelection_.selectingLeft =
this->doubleClickSelection_.selectingRight = false;
}
}
auto element = &hoverLayoutElement->getCreator(); auto element = &hoverLayoutElement->getCreator();
bool isLinkValid = hoverLayoutElement->getLink().isValid(); bool isLinkValid = hoverLayoutElement->getLink().isValid();
auto emoteElement = dynamic_cast<const EmoteElement *>(element); auto emoteElement = dynamic_cast<const EmoteElement *>(element);
@ -1950,13 +1832,11 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
// check if mouse was pressed // check if mouse was pressed
if (event->button() == Qt::LeftButton) if (event->button() == Qt::LeftButton)
{ {
this->doubleClickSelection_.selectingLeft =
this->doubleClickSelection_.selectingRight = false;
if (this->isDoubleClick_) if (this->isDoubleClick_)
{ {
this->isDoubleClick_ = false; this->isDoubleClick_ = false;
// Was actually not a wanted triple-click // Was actually not a wanted triple-click
if (fabsf(distanceBetweenPoints(this->lastDClickPosition_, if (fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_,
event->screenPos())) > 10.f) event->screenPos())) > 10.f)
{ {
this->clickTimer_->stop(); this->clickTimer_->stop();
@ -1975,7 +1855,7 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
// Triple-clicking a message selects the whole message // Triple-clicking a message selects the whole message
if (foundElement && this->clickTimer_->isActive() && if (foundElement && this->clickTimer_->isActive() &&
(fabsf(distanceBetweenPoints(this->lastDClickPosition_, (fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_,
event->screenPos())) < 10.f)) event->screenPos())) < 10.f))
{ {
this->selectWholeMessage(layout.get(), messageIndex); this->selectWholeMessage(layout.get(), messageIndex);
@ -2597,6 +2477,10 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
return; return;
} }
this->isDoubleClick_ = true;
this->lastDoubleClickPosition_ = event->screenPos();
this->clickTimer_->start();
// message under cursor is collapsed // message under cursor is collapsed
if (layout->flags.has(MessageLayoutFlag::Collapsed)) if (layout->flags.has(MessageLayoutFlag::Collapsed))
{ {
@ -2605,38 +2489,17 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
const MessageLayoutElement *hoverLayoutElement = const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos); layout->getElementAt(relativePos);
this->lastDClickPosition_ = event->screenPos();
if (hoverLayoutElement == nullptr) if (hoverLayoutElement == nullptr)
{ {
// Possibility for triple click which doesn't have to be over an
// existing layout element
this->clickTimer_->start();
return; return;
} }
if (!this->isLeftMouseDown_) auto [wordStart, wordEnd] =
{ this->getWordBounds(layout.get(), hoverLayoutElement, relativePos);
this->isDoubleClick_ = true; this->doubleClickSelection_ = {SelectionItem(messageIndex, wordStart),
SelectionItem(messageIndex, wordEnd)};
int wordStart; this->setSelection(this->doubleClickSelection_);
int wordEnd;
this->getWordBounds(layout.get(), hoverLayoutElement, relativePos,
wordStart, wordEnd);
this->clickTimer_->start();
SelectionItem wordMin(messageIndex, wordStart);
SelectionItem wordMax(messageIndex, wordEnd);
this->doubleClickSelection_.originalStart = wordStart;
this->doubleClickSelection_.originalEnd = wordEnd;
this->doubleClickSelection_.origMessageIndex = messageIndex;
this->doubleClickSelection_.origStartItem = wordMin;
this->doubleClickSelection_.origEndItem = wordMax;
this->setSelection(wordMin, wordMax);
}
if (getSettings()->linksDoubleClickOnly) if (getSettings()->linksDoubleClickOnly)
{ {
@ -2892,17 +2755,18 @@ void ChannelView::selectWholeMessage(MessageLayout *layout, int &messageIndex)
this->setSelection(msgStart, msgEnd); this->setSelection(msgStart, msgEnd);
} }
void ChannelView::getWordBounds(MessageLayout *layout, /// @returns [wordStart, wordEnd] position indexes for word hovered by mouse
const MessageLayoutElement *element, std::pair<size_t, size_t> ChannelView::getWordBounds(
const QPoint &relativePos, int &wordStart, MessageLayout *layout, const MessageLayoutElement *element,
int &wordEnd) const QPoint &relativePos)
{ {
const int mouseInWordIndex = element->getMouseOverIndex(relativePos); const auto wordStart = layout->getSelectionIndex(relativePos) -
wordStart = layout->getSelectionIndex(relativePos) - mouseInWordIndex; element->getMouseOverIndex(relativePos);
const int selectionLength = element->getSelectionIndexCount(); const auto selectionLength = element->getSelectionIndexCount();
const int length = const auto length =
element->hasTrailingSpace() ? selectionLength - 1 : selectionLength; element->hasTrailingSpace() ? selectionLength - 1 : selectionLength;
wordEnd = wordStart + length;
return {wordStart, wordStart + length};
} }
void ChannelView::enableScrolling(const QPointF &scrollStart) void ChannelView::enableScrolling(const QPointF &scrollStart)

View file

@ -210,10 +210,11 @@ private:
void drawMessages(QPainter &painter); void drawMessages(QPainter &painter);
void setSelection(const SelectionItem &start, const SelectionItem &end); void setSelection(const SelectionItem &start, const SelectionItem &end);
void setSelection(const Selection &newSelection);
void selectWholeMessage(MessageLayout *layout, int &messageIndex); void selectWholeMessage(MessageLayout *layout, int &messageIndex);
void getWordBounds(MessageLayout *layout, std::pair<size_t, size_t> getWordBounds(MessageLayout *layout,
const MessageLayoutElement *element, const MessageLayoutElement *element,
const QPoint &relativePos, int &wordStart, int &wordEnd); const QPoint &relativePos);
void handleMouseClick(QMouseEvent *event, void handleMouseClick(QMouseEvent *event,
const MessageLayoutElement *hoveredElement, const MessageLayoutElement *hoveredElement,
@ -307,10 +308,9 @@ private:
bool isLeftMouseDown_ = false; bool isLeftMouseDown_ = false;
bool isRightMouseDown_ = false; bool isRightMouseDown_ = false;
bool isDoubleClick_ = false; bool isDoubleClick_ = false;
DoubleClickSelection doubleClickSelection_;
QPointF lastLeftPressPosition_; QPointF lastLeftPressPosition_;
QPointF lastRightPressPosition_; QPointF lastRightPressPosition_;
QPointF lastDClickPosition_; QPointF lastDoubleClickPosition_;
QTimer *clickTimer_; QTimer *clickTimer_;
bool isScrolling_ = false; bool isScrolling_ = false;
@ -330,6 +330,7 @@ private:
} cursors_; } cursors_;
Selection selection_; Selection selection_;
Selection doubleClickSelection_;
const Context context_; const Context context_;