Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: simplify double click selection #4898

Merged
merged 10 commits into from
Oct 17, 2023
Merged
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)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)
- Dev: Tests now run on Ubuntu 22.04 instead of 20.04 to loosen C++ restrictions in tests. (#4774)
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),
kornes marked this conversation as resolved.
Show resolved Hide resolved
std::max(this->selectionMax, b.selectionMax)};
kornes marked this conversation as resolved.
Show resolved Hide resolved
}

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_)
kornes marked this conversation as resolved.
Show resolved Hide resolved
{
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(
kornes marked this conversation as resolved.
Show resolved Hide resolved
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
Loading