Skip to content

Commit

Permalink
refactor: simplify double click selection (#4898)
Browse files Browse the repository at this point in the history
  • Loading branch information
kornes authored Oct 17, 2023
1 parent b975900 commit 12808d3
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 200 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- 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 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)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)
Expand Down
19 changes: 8 additions & 11 deletions src/messages/Selection.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <tuple>
Expand Down Expand Up @@ -72,6 +73,13 @@ struct Selection {
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
{
return this->start == this->end;
Expand Down Expand Up @@ -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
232 changes: 48 additions & 184 deletions src/widgets/helper/ChannelView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ void ChannelView::unpaused()
{
/// Move selection
this->selection_.shiftMessageIndex(this->pauseSelectionOffset_);
this->doubleClickSelection_.shiftMessageIndex(this->pauseSelectionOffset_);

this->pauseSelectionOffset_ = 0;
}
Expand Down Expand Up @@ -927,6 +928,7 @@ void ChannelView::messageAppended(MessagePtr &message,
this->scrollBar_->scrollToBottom(false);
}
this->selection_.shiftMessageIndex(1);
this->doubleClickSelection_.shiftMessageIndex(1);
}
}

Expand Down Expand Up @@ -1088,10 +1090,8 @@ void ChannelView::resizeEvent(QResizeEvent *)
this->update();
}

void ChannelView::setSelection(const SelectionItem &start,
const SelectionItem &end)
void ChannelView::setSelection(const Selection &newSelection)
{
auto newSelection = Selection(start, end);
if (this->selection_ != newSelection)
{
this->selection_ = newSelection;
Expand All @@ -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
{
auto app = getApp();
Expand Down Expand Up @@ -1512,15 +1518,31 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
this->currentMousePosition_ = event->screenPos();
}

// is selecting
// check for word underneath cursor
const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos);

// selecting single characters
if (this->isLeftMouseDown_)
{
auto index = layout->getSelectionIndex(relativePos);

this->setSelection(this->selection_.start,
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
if (layout->flags.has(MessageLayoutFlag::Collapsed))
{
Expand All @@ -1529,153 +1551,13 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
return;
}

// check if word underneath cursor
const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos);

if (hoverLayoutElement == nullptr)
{
this->setCursor(Qt::ArrowCursor);
tooltipWidget->hide();
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();
bool isLinkValid = hoverLayoutElement->getLink().isValid();
auto emoteElement = dynamic_cast<const EmoteElement *>(element);
Expand Down Expand Up @@ -1950,13 +1832,11 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
// check if mouse was pressed
if (event->button() == Qt::LeftButton)
{
this->doubleClickSelection_.selectingLeft =
this->doubleClickSelection_.selectingRight = false;
if (this->isDoubleClick_)
{
this->isDoubleClick_ = false;
// Was actually not a wanted triple-click
if (fabsf(distanceBetweenPoints(this->lastDClickPosition_,
if (fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_,
event->screenPos())) > 10.f)
{
this->clickTimer_->stop();
Expand All @@ -1975,7 +1855,7 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)

// Triple-clicking a message selects the whole message
if (foundElement && this->clickTimer_->isActive() &&
(fabsf(distanceBetweenPoints(this->lastDClickPosition_,
(fabsf(distanceBetweenPoints(this->lastDoubleClickPosition_,
event->screenPos())) < 10.f))
{
this->selectWholeMessage(layout.get(), messageIndex);
Expand Down Expand Up @@ -2597,6 +2477,10 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
return;
}

this->isDoubleClick_ = true;
this->lastDoubleClickPosition_ = event->screenPos();
this->clickTimer_->start();

// message under cursor is collapsed
if (layout->flags.has(MessageLayoutFlag::Collapsed))
{
Expand All @@ -2605,38 +2489,17 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)

const MessageLayoutElement *hoverLayoutElement =
layout->getElementAt(relativePos);
this->lastDClickPosition_ = event->screenPos();

if (hoverLayoutElement == nullptr)
{
// Possibility for triple click which doesn't have to be over an
// existing layout element
this->clickTimer_->start();
return;
}

if (!this->isLeftMouseDown_)
{
this->isDoubleClick_ = true;

int wordStart;
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);
}
auto [wordStart, wordEnd] =
this->getWordBounds(layout.get(), hoverLayoutElement, relativePos);
this->doubleClickSelection_ = {SelectionItem(messageIndex, wordStart),
SelectionItem(messageIndex, wordEnd)};
this->setSelection(this->doubleClickSelection_);

if (getSettings()->linksDoubleClickOnly)
{
Expand Down Expand Up @@ -2892,17 +2755,18 @@ void ChannelView::selectWholeMessage(MessageLayout *layout, int &messageIndex)
this->setSelection(msgStart, msgEnd);
}

void ChannelView::getWordBounds(MessageLayout *layout,
const MessageLayoutElement *element,
const QPoint &relativePos, int &wordStart,
int &wordEnd)
/// @returns [wordStart, wordEnd] position indexes for word hovered by mouse
std::pair<size_t, size_t> ChannelView::getWordBounds(
MessageLayout *layout, const MessageLayoutElement *element,
const QPoint &relativePos)
{
const int mouseInWordIndex = element->getMouseOverIndex(relativePos);
wordStart = layout->getSelectionIndex(relativePos) - mouseInWordIndex;
const int selectionLength = element->getSelectionIndexCount();
const int length =
const auto wordStart = layout->getSelectionIndex(relativePos) -
element->getMouseOverIndex(relativePos);
const auto selectionLength = element->getSelectionIndexCount();
const auto length =
element->hasTrailingSpace() ? selectionLength - 1 : selectionLength;
wordEnd = wordStart + length;

return {wordStart, wordStart + length};
}

void ChannelView::enableScrolling(const QPointF &scrollStart)
Expand Down
Loading

0 comments on commit 12808d3

Please sign in to comment.