gridview.cpp 18.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
\file gridview.cpp
\date 28/04/2021
\author Yann Boucher
\version 1
\brief GridView

Cette classe représente le widget utilisé pour l'affichage et l'interaction avec la grille actuelle.
        **/


#include "gridview.hpp"

#include <QEvent>
#include <QMouseEvent>
#include <QGraphicsSceneHoverEvent>
#include <QApplication>
#include <QGridLayout>
#include <QToolTip>
#include <QLabel>
21
#include <QMimeData>
22
#include <QStyleOptionGraphicsItem>
23
24
25

#include <QDebug>

26
#include <vector>
27
#include <cstdlib>
28

29
30
#include <qmath.h>

31
32
#include "structurereader.hpp"

33
34
35
    //! Permet de convertir un stateColor vers un QColor.
    //! Détail d'implémentation.
    static QColor stateColor_to_QColor(const stateColor& sc)
36
37
38
{
    return QColor::fromRgb(sc.getRed(), sc.getGreen(), sc.getBlue());
}
39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

namespace detail
{
Graphics_view_zoom::Graphics_view_zoom(QGraphicsView* view)
    : QObject(view), _view(view)
{
    _view->viewport()->installEventFilter(this);
    _view->setMouseTracking(true);
    _zoom_factor_base = 1.0015;
}

void Graphics_view_zoom::gentle_zoom(double factor) {
    _view->scale(factor, factor);
    _view->centerOn(target_scene_pos);
    QPointF delta_viewport_pos = target_viewport_pos - QPointF(_view->viewport()->width() / 2.0,
                                                               _view->viewport()->height() / 2.0);
    QPointF viewport_center = _view->mapFromScene(target_scene_pos) - delta_viewport_pos;
    _view->centerOn(_view->mapToScene(viewport_center.toPoint()));
    emit zoomed();
}

void Graphics_view_zoom::set_zoom_factor_base(double value) {
    _zoom_factor_base = value;
}

bool Graphics_view_zoom::eventFilter(QObject *object, QEvent *event) {
    if (event->type() == QEvent::MouseMove) {
        QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
        QPointF delta = target_viewport_pos - mouse_event->pos();
        if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) {
            target_viewport_pos = mouse_event->pos();
            target_scene_pos = _view->mapToScene(mouse_event->pos());
        }
    } else if (event->type() == QEvent::Wheel) {
        QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
        if (wheel_event->angleDelta().y() != 0) {
            double angle = wheel_event->angleDelta().y();
            double factor = qPow(_zoom_factor_base, angle);
            gentle_zoom(factor);
            return true;
        }
    }
    Q_UNUSED(object)
    return false;
}

}

88
class GridItem : public QGraphicsRectItem
89
90
{
public:
91
92
    GridItem(GridView& in_gridview, unsigned state = 0, Coord pos = Coord{0, 0}, QGraphicsItem *parent = nullptr)
        : QGraphicsRectItem(0, 0, GridItem::width(), GridItem::height(), parent), m_grid_view(in_gridview)
93
94
95
96
    {
        setAcceptHoverEvents(true);
        setToolTip(m_state_name);
        setFlag(QGraphicsItem::ItemIsSelectable);
97
        setAcceptDrops(true);
98
        //setCacheMode(QGraphicsItem::DeviceCoordinateCache);
99

100
101
102
        set_state(state);
        set_grid_pos(pos);
        setPen(QPen(brush(), 0));
103
104
    }

105
106
107
108
109
    static int width()
    { return 20; }
    static int height()
    { return 20; }

110
111
    unsigned state() const
    { return m_cell_state; }
112
    void set_state(unsigned s, bool update_history = false)
113
    {
114
115
116
        if (update_history && m_cell_state != s)
            m_grid_view.push_history();

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
        const Alphabet& alph = m_grid_view.alphabet();
        setBrush(QBrush(stateColor_to_QColor(alph.getState(s).getColor())));
        setToolTip(QString::fromStdString(alph.getState(s).getStateLabel()));
        m_cell_state = s;
        update();
    }

    Coord grid_pos() const
    { return m_cell_pos; }
    void set_grid_pos(Coord c)
    {
        setPos(QPointF(c.x*GridItem::width(), c.y*GridItem::height()));
        m_cell_pos = c;
    }

132
protected:
133
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
134
    {
135
        QRectF rect = QRectF(0, 0, GridItem::width(), GridItem::height());
136

137
        painter->setPen(Qt::NoPen);
138
139
140

        if (isSelected())
        {
141
142
            QBrush selection_brush = brush();
            selection_brush.setColor(brush().color().darker(175));
143
144
145
146
147
148
149
150
151
            painter->setBrush(selection_brush);
        }
        else
        {
            painter->setBrush(brush());
        }

        painter->drawRect(rect);
    }
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

    void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override
    {
        if (event->mimeData()->hasText())
        {
            event->acceptProposedAction();
            update();
        }
    }

    void dropEvent(QGraphicsSceneDragDropEvent *event) override
    {
        event->acceptProposedAction();

        QString file_path = event->mimeData()->text();
        Structure s;
        try
        {
            s = load_structure(file_path.toStdString());
        }
        catch (const StructureReaderException& e)
        {
            //qDebug() << "StructureReaderException : " << e.what() << "\n";
            return;
        }

178
        m_grid_view.paste_structure_at(m_cell_pos, s);
179
180
    }

181
private:
182
183
    Coord m_cell_pos;
    unsigned m_cell_state;
184
    QString m_state_name;
185
    GridView& m_grid_view;
186
187
};

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/**
 * \class GridGraphicsView
 *
 * Classe héritée de GraphicsView permettant les manipulations à la souris et de drag-and-drop.
 * Détail d'implémentation.
 */
class GridGraphicsView : public QGraphicsView
{
public:
    GridGraphicsView(GridView& gridview, QWidget* parent)
        : QGraphicsView(parent), m_gridview(gridview)
    {
        setAcceptDrops(true);
    }

protected:
    void mousePressEvent(QMouseEvent* event);
    void mouseMoveEvent(QMouseEvent* event);
206
    void drawForeground(QPainter * painter, const QRectF & rect);
207

208
209
210
private:
    void draw_bresenham(QPointF from, QPointF to);

211
212
private:
    GridView& m_gridview;
213
    QPoint m_last_mouse_pos;
214
215
216
};


217
218
void GridGraphicsView::mousePressEvent(QMouseEvent *event)
{
219
220
221
    QPointF item_pos = mapToScene(event->pos());
    Coord coord = Coord{(int)item_pos.x()/GridItem::width(), (int)item_pos.y()/GridItem::height()};
    GridItem* item = m_gridview.item_at(coord);
222

223
224
    m_last_mouse_pos = event->pos();

225
    if (QGuiApplication::keyboardModifiers() == Qt::NoModifier && event->button() == Qt::LeftButton)
226
    {
227
228
229
        if (item)
        {
            unsigned state = m_gridview.current_pen();
230
            item->set_state(state, true);
231
        }
232
    }
233
    else if (event->button() == Qt::RightButton && item)
234
235
236
237
238
239
240
241
    {
        QGraphicsView::mousePressEvent(event);
        item->setSelected(true);
    }
    else
        QGraphicsView::mousePressEvent(event);
}

242
243
void GridGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
244
245
246
    QPointF item_pos = mapToScene(event->pos());
    Coord coord = Coord{(int)item_pos.x()/GridItem::width(), (int)item_pos.y()/GridItem::height()};
    GridItem* item = m_gridview.item_at(coord);
247

248
    if (QGuiApplication::keyboardModifiers() == Qt::NoModifier && event->buttons() == Qt::LeftButton)
249
    {
250
        draw_bresenham(mapToScene(m_last_mouse_pos), mapToScene(event->pos()));
251
252
253
254
255
256
257
258
    }
    else if (event->buttons() == Qt::RightButton && item)
    {
        QGraphicsView::mouseMoveEvent(event);
        item->setSelected(true);
    }
    else
        QGraphicsView::mouseMoveEvent(event);
259
260
261
262

    m_last_mouse_pos = event->pos();
}

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
void GridGraphicsView::drawForeground(QPainter *painter, const QRectF &rect)
{
    qreal lod = QStyleOptionGraphicsItem::levelOfDetailFromTransform(painter->worldTransform());

    if (lod < 0.4)
        return; // trop zoomé, on ne dessine rien

    painter->setPen(QPen(Qt::black, 0)); // cosmetic 1-pixel pen

    QVector<QLine> lines;
    // horizontal lines
    for (int i = -1; i < m_gridview.grid_size().height(); ++i)
    {
        const unsigned y_offset = (i+1)*GridItem::height();
        lines.append(QLine(QPoint(0, y_offset), QPoint(GridItem::width()*m_gridview.grid_size().width(), y_offset)));
    }
    // vertical lines
    for (int j = -1; j < m_gridview.grid_size().width(); ++j)
    {
        const unsigned x_offset = (j+1)*GridItem::width();
        lines.append(QLine(QPoint(x_offset, 0), QPoint(x_offset, GridItem::height()*m_gridview.grid_size().height())));
    }

    painter->drawLines(lines);

    painter->setPen(Qt::NoPen);
}

291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
// Algorithme de Bresenham pour tracer une ligne entre les deux points et dessiner sur ces items
void GridGraphicsView::draw_bresenham(QPointF from, QPointF to)
{
    int x0 = from.x()/GridItem::width();
    int y0 = from.y()/GridItem::height();
    int x1 = to.x()/GridItem::width();
    int y1 = to.y()/GridItem::height();

    // cf. https://gist.github.com/bert/1085538
    int dx =  abs (x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = -abs (y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = dx + dy, e2; /* error value e_xy */

    for (;;){  /* loop */

        //printf("Trying %d %d\n", x, y);
        auto item = m_gridview.item_at(Coord{x0, y0});
        if (item)
            item->set_state(m_gridview.current_pen());
        if (x0 == x1 && y0 == y1) break;
        e2 = 2 * err;
        if (e2 >= dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
        if (e2 <= dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
    }
315
316
}

317
GridView::GridView(QWidget *parent)
318
    : QFrame(parent), m_undo_history(10), m_width(10), m_height(10)
319
{
320
    //setMouseTracking(true);
321

322
323
324
325
    setFrameStyle(Sunken | StyledPanel);

    m_scene = new QGraphicsScene(this);

326
    m_view = new GridGraphicsView(*this, this);
327
    m_view->setRenderHint(QPainter::Antialiasing, false);
328
    m_view->setDragMode(QGraphicsView::RubberBandDrag);
Yann Boucher's avatar
Yann Boucher committed
329
    m_view->setOptimizationFlags(QGraphicsView::DontSavePainterState|QGraphicsView::DontAdjustForAntialiasing);
330
331
332
333
    m_view->setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
    m_view->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
    m_view->setScene(m_scene);
    m_view->viewport()->setCursor(Qt::ArrowCursor);
Yann Boucher's avatar
Yann Boucher committed
334
    m_view->setBackgroundBrush(QBrush(Qt::gray));
335
    m_view->setRubberBandSelectionMode(Qt::IntersectsItemBoundingRect); // provides better performance
336
337
338
339
340
341
342
    /*
    QOpenGLWidget *gl = new QOpenGLWidget();
    QSurfaceFormat format;
    format.setSamples(4);
    gl->setFormat(format);
    m_view->setViewport(gl);
    */
343

344
    m_zoom = new detail::Graphics_view_zoom(m_view);
345
346
347

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(m_view);
348
349
    layout->addWidget(new QLabel("Clic gauche : éditer; Clic droit : sélectionner", this));
    layout->addWidget(new QLabel("Maintenir SHIFT pour déplacer la grille; Molette ou CTRL+/CTRL- pour zoomer", this));
350
351
    setLayout(layout);

352
    set_current_pen(0);
353
354
355
356
357
    // Test Alphabet
    Alphabet alph(state{stateColor{255, 255, 255}, "Dead"});
    alph.newEtat(state{stateColor{0, 0, 255}, "Alive"});
    set_alphabet(alph);

358
359
360
    // Initialisation la première fois
    m_height = 10;
    m_width = 10;
361

362
    Grid default_grid(m_height, m_width);
363
    load_grid(default_grid);
364
365
366
367
368

    connect(m_zoom, &detail::Graphics_view_zoom::zoomed, this, [this]
            {
                emit zoom_changed(cell_pixel_size());
            });
369
370
}

371
372
void GridView::set_alphabet(const Alphabet &alph)
{
373
374
375
    while (!m_undo_history.isEmpty())
        m_undo_history.popGrid();

376
377
378
    m_alph = alph;
}

379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
const Alphabet &GridView::alphabet() const
{
    return m_alph;
}

void GridView::set_current_pen(unsigned state)
{
    m_pen = state;
}

unsigned GridView::current_pen() const
{
    return m_pen;
}

394
395
396
397
398
void GridView::set_clipboard(const Structure &s)
{
    m_copy_paste_buffer = s;
}

399
400
unsigned GridView::cell_pixel_size() const
{
401
402
    return m_view->transform().mapRect(QRectF(0,0,GridItem::width(), GridItem::height())).size().toSize().width();
}
403

404
405
406
QSize GridView::grid_size() const
{
    return QSize(m_width, m_height);
407
408
}

409
410
void GridView::copy_grid(const Grid &grid)
{
411
412
    push_history();
    load_grid(grid);
413
414
415
416
}

Grid GridView::get_grid() const
{
417
418
419
420
421
422
423
    Grid grid = Grid(m_height, m_width);
    for (size_t i = 0; i < grid.get_col(); ++i)
    {
        for (size_t j = 0; j < grid.get_rows(); ++j)
        {
            const GridItem* item = item_at(Coord{(int)i, (int)j});
            assert(item);
424
425
426
            Coord pos;
            pos.x = i;
            pos.y = j;
427
            grid.set_cell(pos, item->state());
428
429
430
431
        }
    }

    return grid;
432
433
434
435
436
437
438
439
440
}

void GridView::clear_selection()
{
    m_scene->clearSelection();
}

Structure GridView::selected_cells() const
{
441
442
443
    if (m_scene->selectedItems().empty())
        return Structure{};

444
    std::vector<std::pair<Coord, unsigned>> cells;
445
446
447

    // on calcule la position du coin supérieur gauche de la sélection, afin de la considérer comme l'origine de de la structure
    Coord origin = top_left_of_selection();
448
    Q_FOREACH(const auto& item, m_scene->selectedItems())
449
450
    {
        GridItem* cell = static_cast<GridItem*>(item);
451
        cells.push_back(std::make_pair(cell->grid_pos() - origin, cell->state()));
452
453
454
455
    }
    return Structure(cells.begin(), cells.end());
}

456
457
458
459
460
461
462
463
464
465
466
467
468
void GridView::push_history()
{
    m_undo_history.pushGrid(get_grid());
}

void GridView::undo()
{
    if (!m_undo_history.isEmpty())
    {
        load_grid(m_undo_history.popGrid());
    }
}

469
470
GridItem *GridView::item_at(Coord pos)
{
471
472
    if (pos.x < 0 || pos.y < 0 || pos.x >= (int)m_width || pos.y >= (int)m_height)
        return nullptr;
473
474
475
    return m_item_cache[pos.y*m_width + pos.x];
    //QGraphicsItem* gitem_ptr = m_scene->itemAt(QPointF(pos.x*GridItem::width(), pos.y*GridItem::height()), QTransform());
    //return dynamic_cast<GridItem*>(gitem_ptr);
476
}
477
478
const GridItem *GridView::item_at(Coord pos) const
{
479
480
    if (pos.x < 0 || pos.y < 0 || pos.x >= (int)m_width || pos.y >= (int)m_height)
        return nullptr;
481
482
483
    return m_item_cache[pos.y*m_width + pos.x];
    //QGraphicsItem* gitem_ptr = m_scene->itemAt(QPointF(pos.x*GridItem::width(), pos.y*GridItem::height()), QTransform());
    //return dynamic_cast<GridItem*>(gitem_ptr);
484
}
485

486
487
488
489
void GridView::load_grid(const Grid &grid)
{
    m_height = grid.get_rows();
    m_width = grid.get_col();
490
    if (m_height*m_width != m_item_cache.size())
491
    {
492
493
494
495
        m_scene->clear();
        m_item_cache.resize(m_width*m_height);
        // Populate scene;
        for (unsigned i = 0; i < m_width; ++i)
496
        {
497
498
499
500
501
502
503
504
505
506
507
            for (unsigned j = 0; j < m_height; ++j)
            {
                Coord pos = {static_cast<int>(i), static_cast<int>(j)};
                unsigned state = grid.get_state(pos);

                GridItem *item = new GridItem(*this, state);
                item->set_grid_pos(pos);
                m_item_cache[pos.y*m_width + pos.x] = item;
                m_scene->addItem(item);
            }
        }
508

509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
        // On met à jour la taille de la scène
        QRectF rect = m_scene->itemsBoundingRect();
        m_scene->setSceneRect(rect);
    }
    // Pas la peine de recharger la grille si la taille est la même
    else
    {
        for (unsigned i = 0; i < m_width; ++i)
        {
            for (unsigned j = 0; j < m_height; ++j)
            {
                Coord pos = {static_cast<int>(i), static_cast<int>(j)};
                unsigned state = grid.get_state(pos);

                GridItem *item = item_at(pos);
                item->set_state(state);
            }
526
527
528
529
        }
    }
}

530
531
532
533
534
535
536
537
538
539
540
void GridView::copy_selection()
{
    m_copy_paste_buffer = selected_cells();
}

void GridView::paste_clipboard()
{
    // la destination est la cellule sélectionnée actuelle, on ne fait rien si on est pas dans ce cas
    if (m_scene->selectedItems().size() < 1)
        return;

541
542
    push_history();

543
544
    Coord origin = top_left_of_selection();

545
    paste_structure_at(origin, m_copy_paste_buffer);
546
547
}

548
void GridView::fill_selection(unsigned state)
549
{
550
551
    push_history();

552
553
554
    for (auto& abstract_item : m_scene->selectedItems())
    {
        GridItem* item = static_cast<GridItem*>(abstract_item);
555
        item->set_state(state);
556
    }
557
558
559
560
561
}

void GridView::delete_selection()
{
    fill_selection(0);
562
563
564
    clear_selection();
}

565
566
void GridView::paste_structure_at(Coord origin, const Structure &s)
{
567
568
    push_history();

569
570
571
572
573
574
575
    for (const auto& cell : s)
    {
        Coord corrected = wrap_coords(cell.first + origin, m_width, m_height);

        GridItem* item = item_at(corrected);
        assert(item != nullptr);

576
        item->set_state(cell.second);
577
578
579
    }
}

580
581
582
583
Coord GridView::top_left_of_selection() const
{
    // on calcule la position du coin supérieur gauche de la sélection
    Coord selection_top_left = Coord{std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
584
    Q_FOREACH(const auto& item, m_scene->selectedItems())
585
586
    {
        GridItem* cell = static_cast<GridItem*>(item);
587
588
589
590
        if (cell->grid_pos().x < selection_top_left.x)
            selection_top_left.x = cell->grid_pos().x;
        if (cell->grid_pos().y < selection_top_left.y)
            selection_top_left.y = cell->grid_pos().y;
591
592
593
594
595
596
    }

    return selection_top_left;
}


597
598
599
600
void GridView::keyPressEvent(QKeyEvent *event)
{
    if (event->modifiers() & Qt::ShiftModifier)
    {
601
        // prevent items from stealing the mouse event
602
        for (auto& item : m_item_cache)
603
            item->setAcceptedMouseButtons(Qt::NoButton);
604
605
        m_view->setDragMode(QGraphicsView::ScrollHandDrag);
    }
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622

    // copier-couper-coller
    if (event->modifiers() & Qt::ControlModifier)
    {
        if (event->key() == Qt::Key_C)
        {
            copy_selection();
        }
        else if (event->key() == Qt::Key_X)
        {
            copy_selection();
            delete_selection();
        }
        else if (event->key() == Qt::Key_V)
        {
            paste_clipboard();
        }
623
624
625
626
627
628
        else if (event->key() == Qt::Key_Z)
        {
            undo();
        }
        else if (event->key() == Qt::Key_A)
        {
629
            for (auto& item : m_item_cache)
630
631
                item->setSelected(true);
        }
632
633
634
635
636
637
638
639
        else if (event->key() == Qt::Key_Plus)
        {
            m_zoom->gentle_zoom(1.25);
        }
        else if (event->key() == Qt::Key_Minus)
        {
            m_zoom->gentle_zoom(0.75);
        }
640
641
642
643
644
645
646
647
648
649
    }


    // suppression de sélection
    if (event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete)
    {
        delete_selection();
    }

    return QFrame::keyPressEvent(event);
650
651
652
653
654
655
656
657
}

void GridView::keyReleaseEvent(QKeyEvent *event)
{
    (void)event;
    if (QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)
    {
        m_view->setDragMode(QGraphicsView::RubberBandDrag);
658
        // prevent item mouse event handling
659
        for (auto& item : m_item_cache)
660
            item->setAcceptedMouseButtons(Qt::AllButtons);
661
    }
662
663

    return QFrame::keyReleaseEvent(event);
664
665
}