Skip to content

Commit dc40f86

Browse files
feat(app): Ctrl+drag rubber band for thumbnail selection (#67)
Closes #67
1 parent fe18144 commit dc40f86

1 file changed

Lines changed: 148 additions & 6 deletions

File tree

src/app/ThumbnailSequence.cpp

Lines changed: 148 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <QGraphicsScene>
1212
#include <QGraphicsSceneMouseEvent>
1313
#include <QGraphicsView>
14+
#include <QMouseEvent>
15+
#include <QRubberBand>
1416
#include <QStyleOptionGraphicsItem>
1517
#include <QtWidgets/QCheckBox>
1618
#include <QtWidgets/QMessageBox>
@@ -143,7 +145,12 @@ class ThumbnailSequence::Impl {
143145

144146
void setSelectionModeEnabled(bool enabled);
145147

146-
private:
148+
void extendSelectionBySceneRect(const QRectF& sceneRect);
149+
150+
static bool isDescendantOfCompositeItem(QGraphicsItem* item);
151+
152+
class ViewportRubberBandFilter;
153+
147154
class ItemsByIdTag;
148155
class ItemsInOrderTag;
149156
class SelectedThenUnselectedTag;
@@ -233,6 +240,26 @@ class ThumbnailSequence::Impl {
233240
GraphicsScene m_graphicsScene;
234241
QRectF m_sceneRect;
235242
bool m_selectionMode;
243+
244+
std::unique_ptr<ViewportRubberBandFilter> m_rubberBandFilter;
245+
};
246+
247+
248+
class ThumbnailSequence::Impl::ViewportRubberBandFilter : public QObject {
249+
public:
250+
explicit ViewportRubberBandFilter(Impl* impl, QGraphicsView* view);
251+
252+
~ViewportRubberBandFilter() override;
253+
254+
protected:
255+
bool eventFilter(QObject* watched, QEvent* event) override;
256+
257+
private:
258+
Impl* m_impl;
259+
QGraphicsView* m_view;
260+
QRubberBand m_rubberBand;
261+
QPoint m_origin;
262+
bool m_active = false;
236263
};
237264

238265

@@ -283,14 +310,14 @@ class ThumbnailSequence::CompositeItem : public QGraphicsItemGroup {
283310

284311
void updateAppearence(bool selected, bool selectionLeader);
285312

286-
virtual QRectF boundingRect() const;
313+
QRectF boundingRect() const override;
287314

288-
virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget);
315+
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;
289316

290317
protected:
291-
virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent* event);
318+
void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override;
292319

293-
virtual void mousePressEvent(QGraphicsSceneMouseEvent* event);
320+
void mousePressEvent(QGraphicsSceneMouseEvent* event) override;
294321

295322
private:
296323
// We no longer use QGraphicsView's selection mechanism, so we
@@ -447,14 +474,18 @@ ThumbnailSequence::Impl::Impl(ThumbnailSequence& owner, const QSizeF& maxLogical
447474
[&](QGraphicsSceneContextMenuEvent* evt) { this->sceneContextMenuEvent(evt); });
448475
}
449476

450-
ThumbnailSequence::Impl::~Impl() {}
477+
ThumbnailSequence::Impl::~Impl() {
478+
m_rubberBandFilter.reset();
479+
}
451480

452481
void ThumbnailSequence::Impl::setThumbnailFactory(std::shared_ptr<ThumbnailFactory> factory) {
453482
m_factory = std::move(factory);
454483
}
455484

456485
void ThumbnailSequence::Impl::attachView(QGraphicsView* const view) {
486+
m_rubberBandFilter.reset();
457487
view->setScene(&m_graphicsScene);
488+
m_rubberBandFilter = std::make_unique<ViewportRubberBandFilter>(this, view);
458489
}
459490

460491
void ThumbnailSequence::Impl::reset(const PageSequence& pages,
@@ -1225,6 +1256,57 @@ void ThumbnailSequence::Impl::clearSelection() {
12251256
}
12261257
}
12271258

1259+
bool ThumbnailSequence::Impl::isDescendantOfCompositeItem(QGraphicsItem* item) {
1260+
for (; item; item = item->parentItem()) {
1261+
if (dynamic_cast<CompositeItem*>(item)) {
1262+
return true;
1263+
}
1264+
}
1265+
return false;
1266+
}
1267+
1268+
void ThumbnailSequence::Impl::extendSelectionBySceneRect(const QRectF& sceneRect) {
1269+
if (!sceneRect.isValid()) {
1270+
return;
1271+
}
1272+
1273+
const Item* firstHit = nullptr;
1274+
1275+
for (const Item& item : m_itemsInOrder) {
1276+
const QRectF br(item.composite->mapToScene(item.composite->boundingRect()).boundingRect());
1277+
if (!br.intersects(sceneRect)) {
1278+
continue;
1279+
}
1280+
if (!firstHit) {
1281+
firstHit = &item;
1282+
}
1283+
if (!item.isSelected()) {
1284+
item.setSelected(true);
1285+
moveToSelected(&item);
1286+
}
1287+
}
1288+
1289+
if (!firstHit) {
1290+
return;
1291+
}
1292+
1293+
const bool leaderStillValid
1294+
= m_selectionLeader && m_selectionLeader->isSelected()
1295+
&& m_selectionLeader->composite->mapToScene(m_selectionLeader->composite->boundingRect())
1296+
.boundingRect()
1297+
.intersects(sceneRect);
1298+
1299+
if (!leaderStillValid) {
1300+
if (m_selectionLeader) {
1301+
m_selectionLeader->setSelectionLeader(false);
1302+
}
1303+
const_cast<Item*>(firstHit)->setSelectionLeader(true);
1304+
m_selectionLeader = firstHit;
1305+
}
1306+
1307+
m_owner.emitNewSelectionLeader(m_selectionLeader->pageInfo, m_selectionLeader->composite, SELECTED_BY_USER);
1308+
}
1309+
12281310
ThumbnailSequence::Impl::ItemsInOrder::iterator ThumbnailSequence::Impl::itemInsertPosition(
12291311
const ItemsInOrder::iterator begin,
12301312
const ItemsInOrder::iterator end,
@@ -1568,3 +1650,63 @@ void ThumbnailSequence::CompositeItem::contextMenuEvent(QGraphicsSceneContextMen
15681650
event->accept(); // Prevent it from propagating further.
15691651
m_owner.contextMenuRequested(m_item->pageInfo, event->screenPos(), m_item->isSelected());
15701652
}
1653+
1654+
ThumbnailSequence::Impl::ViewportRubberBandFilter::ViewportRubberBandFilter(Impl* impl, QGraphicsView* view)
1655+
: QObject(view->viewport()), m_impl(impl), m_view(view), m_rubberBand(QRubberBand::Rectangle, view->viewport()) {
1656+
view->viewport()->installEventFilter(this);
1657+
}
1658+
1659+
ThumbnailSequence::Impl::ViewportRubberBandFilter::~ViewportRubberBandFilter() {
1660+
if (m_view) {
1661+
m_view->viewport()->removeEventFilter(this);
1662+
}
1663+
}
1664+
1665+
bool ThumbnailSequence::Impl::ViewportRubberBandFilter::eventFilter(QObject* watched, QEvent* event) {
1666+
if (!m_view || watched != m_view->viewport()) {
1667+
return QObject::eventFilter(watched, event);
1668+
}
1669+
1670+
switch (event->type()) {
1671+
case QEvent::MouseButtonPress: {
1672+
auto* const me = static_cast<QMouseEvent*>(event);
1673+
if (me->button() != Qt::LeftButton || !(me->modifiers() & Qt::ControlModifier)) {
1674+
return false;
1675+
}
1676+
QGraphicsItem* const top = m_view->itemAt(me->pos());
1677+
if (Impl::isDescendantOfCompositeItem(top)) {
1678+
return false;
1679+
}
1680+
m_origin = me->pos();
1681+
m_active = true;
1682+
m_rubberBand.setGeometry(QRect(m_origin, QSize()));
1683+
m_rubberBand.show();
1684+
return true;
1685+
}
1686+
case QEvent::MouseMove: {
1687+
if (!m_active) {
1688+
return false;
1689+
}
1690+
auto* const me = static_cast<QMouseEvent*>(event);
1691+
m_rubberBand.setGeometry(QRect(m_origin, me->pos()).normalized());
1692+
return true;
1693+
}
1694+
case QEvent::MouseButtonRelease: {
1695+
auto* const me = static_cast<QMouseEvent*>(event);
1696+
if (!m_active || me->button() != Qt::LeftButton) {
1697+
return false;
1698+
}
1699+
m_active = false;
1700+
m_rubberBand.hide();
1701+
const QRect r(m_rubberBand.geometry().normalized());
1702+
if (r.width() > 3 && r.height() > 3) {
1703+
const QRectF sceneRect(m_view->mapToScene(r).boundingRect());
1704+
m_impl->extendSelectionBySceneRect(sceneRect);
1705+
}
1706+
return true;
1707+
}
1708+
default:
1709+
break;
1710+
}
1711+
return false;
1712+
}

0 commit comments

Comments
 (0)