gridview.cpp 14 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
23
24

#include <QDebug>

25
26
#include <vector>

27
28
#include <qmath.h>

29
30
#include "structurereader.hpp"

31
    // FIXME : faire en sorte que le tooltip soit correctement mis à jour à chaque fois
32

33
    class GridItem : public QGraphicsRectItem
34
35
{
public:
36
37
    GridItem(GridView& in_gridview, QColor color, const QString& state_name, QGraphicsItem *parent = nullptr)
        : QGraphicsRectItem(0, 0, GridItem::width(), GridItem::height(), parent), grid_view(in_gridview), m_state_name(state_name)
38
39
40
41
42
43
    {
        setAcceptHoverEvents(true);
        setPen(QPen(QBrush(color), 0));
        setBrush(QBrush(color));
        setToolTip(m_state_name);
        setFlag(QGraphicsItem::ItemIsSelectable);
44
45
46
        setAcceptDrops(true);

        view = nullptr;
47
48
    }

49
50
51
52
53
    static int width()
    { return 20; }
    static int height()
    { return 20; }

54
protected:
55
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
56
    {
57
58
59
60
61
62
        if (!view)
        {
            assert(scene()->views().size() != 0);
            view = scene()->views().first();
        }

63
64
        QSizeF screen_size = view->transform().mapRect(sceneBoundingRect()).size();

65
        QRectF rect = QRectF(0, 0, width(), height());
66

67
68
69
70
        if (screen_size.width() < 7.5)
            painter->setPen(Qt::NoPen); // if the cell is too small, don't draw its outline
        else
            painter->setPen(QPen(Qt::black, 0)); // cosmetic 1-pixel pen
71
72
73
74
75
76
77
78
79
80
81
82
83

        if (isSelected())
        {
            QBrush selection_brush(Qt::cyan);
            painter->setBrush(selection_brush);
        }
        else
        {
            painter->setBrush(brush());
        }

        painter->drawRect(rect);
    }
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

    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;
        }

        grid_view.paste_structure_at(cell_pos, s);
    }


114
115
116
117
    // infos associées à la cellule affichée
public:
    unsigned cell_state;
    Coord cell_pos;
118
119
    QGraphicsView* view;
    GridView& grid_view;
120
121
122
123
124

private:
    QString m_state_name;
};

125
126
127
128
129
static QColor stateColor_to_QColor(const stateColor& sc)
{
    return QColor::fromRgb(sc.getRed(), sc.getGreen(), sc.getBlue());
}

130
131
132
133
134
void GridGraphicsView::mousePressEvent(QMouseEvent *event)
{
    QGraphicsItem* gitem_ptr = itemAt(event->pos());
    GridItem* item = dynamic_cast<GridItem*>(gitem_ptr);

135
    if (event->button() == Qt::LeftButton && item)
136
137
138
    {
        unsigned state = m_gridview.current_pen();
        item->setBrush(QBrush(stateColor_to_QColor(m_gridview.alphabet().getState(state).getColor())));
139
        item->cell_state = state;
140
141
        item->update();
    }
142
    else if (event->button() == Qt::RightButton && item)
143
144
145
146
147
148
149
150
    {
        QGraphicsView::mousePressEvent(event);
        item->setSelected(true);
    }
    else
        QGraphicsView::mousePressEvent(event);
}

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
void GridGraphicsView::mouseMoveEvent(QMouseEvent *event)
{

    QGraphicsItem* gitem_ptr = itemAt(event->pos());
    GridItem* item = dynamic_cast<GridItem*>(gitem_ptr);

    if (event->buttons() == Qt::LeftButton && item)
    {

        unsigned state = m_gridview.current_pen();
        item->setBrush(QBrush(stateColor_to_QColor(m_gridview.alphabet().getState(state).getColor())));
        item->cell_state = state;
        item->update();
    }
    else if (event->buttons() == Qt::RightButton && item)
    {
        QGraphicsView::mouseMoveEvent(event);
        item->setSelected(true);
    }
    else
        QGraphicsView::mouseMoveEvent(event);
}

174
GridView::GridView(QWidget *parent)
175
    : QFrame(parent), m_width(10), m_height(10)
176
{
177
178
    setMouseTracking(true);

179
180
181
182
    setFrameStyle(Sunken | StyledPanel);

    m_scene = new QGraphicsScene(this);

183
    m_view = new GridGraphicsView(*this, this);
184
    m_view->setRenderHint(QPainter::Antialiasing, false);
185
    m_view->setDragMode(QGraphicsView::RubberBandDrag);
Yann Boucher's avatar
Yann Boucher committed
186
    m_view->setOptimizationFlags(QGraphicsView::DontSavePainterState|QGraphicsView::DontAdjustForAntialiasing);
187
188
189
190
    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
191
    m_view->setBackgroundBrush(QBrush(Qt::gray));
192
    m_view->setRubberBandSelectionMode(Qt::IntersectsItemBoundingRect); // provides better performance
193

194
    m_zoom = new detail::Graphics_view_zoom(m_view);
195
196
197

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(m_view);
198
199
    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));
200
201
    setLayout(layout);

202
    set_current_pen(0);
203
204
205
206
207
    // Test Alphabet
    Alphabet alph(state{stateColor{255, 255, 255}, "Dead"});
    alph.newEtat(state{stateColor{0, 0, 255}, "Alive"});
    set_alphabet(alph);

208
209
210
    // Initialisation la première fois
    m_height = 10;
    m_width = 10;
211

212
213
    Grid default_grid(m_height, m_width);
    copy_grid(default_grid);
214
215
216
217
218

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

221
222
223
224
225
void GridView::set_alphabet(const Alphabet &alph)
{
    m_alph = alph;
}

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
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;
}

241
242
243
244
245
void GridView::set_clipboard(const Structure &s)
{
    m_copy_paste_buffer = s;
}

246
247
248
249
250
251
252
253
254
255
256
unsigned GridView::cell_pixel_size() const
{
    if (m_view->items().empty())
        return 10; // a default size if there are no cells in the grid yet

    const GridItem* item = item_at({0, 0});
    assert(item);

    return m_view->transform().mapRect(item->sceneBoundingRect()).size().toSize().width();
}

257
258
259
void GridView::copy_grid(const Grid &grid)
{
    m_scene->clear();
260
261
    m_height = grid.get_rows();
    m_width = grid.get_col();
262
    // Populate scene;
263
    for (unsigned i = 0; i < m_width; ++i)
264
    {
265
        for (unsigned j = 0; j < m_height; ++j)
266
        {
267
268
            Coord pos = {static_cast<int>(i), static_cast<int>(j)};
            unsigned state = grid.get_state(pos);
269

270
            GridItem *item = new GridItem(*this, stateColor_to_QColor(m_alph.getState(state).getColor()), QString::fromStdString(m_alph.getState(state).getStateLabel()));
271
            //GridItem *item = new GridItem((i ^ j) % 2 ? Qt::blue : Qt::white, (i ^ j) % 2 ? "Alive" : "Dead");
272
273
274
            item->setPos(QPointF(i*GridItem::width(), j*GridItem::height()));
            item->cell_pos = Coord{(int)i, (int)j};
            item->cell_state = state;
275
276
277
            m_scene->addItem(item);
        }
    }
278
279
    m_width = grid.get_col();
    m_height = grid.get_rows();
280
281
282
283
}

Grid GridView::get_grid() const
{
284
285
286
287
288
289
290
    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);
291
292
293
294
            Coord pos;
            pos.x = i;
            pos.y = j;
            grid.set_cell(pos, item->cell_state);
295
296
297
298
        }
    }

    return grid;
299
300
301
302
303
304
305
306
307
}

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

Structure GridView::selected_cells() const
{
308
309
310
    if (m_scene->selectedItems().empty())
        return Structure{};

311
    std::vector<std::pair<Coord, unsigned>> cells;
312
313
314

    // 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();
315
    Q_FOREACH(const auto& item, m_scene->selectedItems())
316
317
    {
        GridItem* cell = static_cast<GridItem*>(item);
318
        cells.push_back(std::make_pair(cell->cell_pos - origin, cell->cell_state));
319
320
321
322
    }
    return Structure(cells.begin(), cells.end());
}

323
324
325
326
327
GridItem *GridView::item_at(Coord pos)
{
    QGraphicsItem* gitem_ptr = m_scene->itemAt(QPointF(pos.x*GridItem::width(), pos.y*GridItem::height()), QTransform());
    return dynamic_cast<GridItem*>(gitem_ptr);
}
328
329
330
331
332
const GridItem *GridView::item_at(Coord pos) const
{
    QGraphicsItem* gitem_ptr = m_scene->itemAt(QPointF(pos.x*GridItem::width(), pos.y*GridItem::height()), QTransform());
    return dynamic_cast<GridItem*>(gitem_ptr);
}
333
334
335
336
337
338
339
340
341
342
343
344
345
346

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;

    Coord origin = top_left_of_selection();

347
    paste_structure_at(origin, m_copy_paste_buffer);
348
349
}

350
void GridView::fill_selection(unsigned state)
351
352
353
354
{
    for (auto& abstract_item : m_scene->selectedItems())
    {
        GridItem* item = static_cast<GridItem*>(abstract_item);
355
356
        item->setBrush(QBrush(stateColor_to_QColor(m_alph.getState(state).getColor())));
        item->cell_state = state;
357
358
        item->update();
    }
359
360
361
362
363
}

void GridView::delete_selection()
{
    fill_selection(0);
364
365
366
    clear_selection();
}

367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
void GridView::paste_structure_at(Coord origin, const Structure &s)
{
    for (const auto& cell : s)
    {
        Coord corrected = wrap_coords(cell.first + origin, m_width, m_height);

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

        item->cell_state = cell.second;
        item->setBrush(QBrush(stateColor_to_QColor(m_alph.getState(cell.second).getColor())));
        item->update();
    }
}

382
383
384
385
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()};
386
    Q_FOREACH(const auto& item, m_scene->selectedItems())
387
388
389
390
391
392
393
394
395
396
397
398
    {
        GridItem* cell = static_cast<GridItem*>(item);
        if (cell->cell_pos.x < selection_top_left.x)
            selection_top_left.x = cell->cell_pos.x;
        if (cell->cell_pos.y < selection_top_left.y)
            selection_top_left.y = cell->cell_pos.y;
    }

    return selection_top_left;
}


399
400
401
402
void GridView::keyPressEvent(QKeyEvent *event)
{
    if (event->modifiers() & Qt::ShiftModifier)
    {
403
404
405
        // prevent items from stealing the mouse event
        for (auto& item : m_scene->items())
            item->setAcceptedMouseButtons(Qt::NoButton);
406
407
        m_view->setDragMode(QGraphicsView::ScrollHandDrag);
    }
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424

    // 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();
        }
425
426
427
428
429
430
431
432
        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);
        }
433
434
435
436
437
438
439
440
441
442
    }


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

    return QFrame::keyPressEvent(event);
443
444
445
446
447
448
449
450
}

void GridView::keyReleaseEvent(QKeyEvent *event)
{
    (void)event;
    if (QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)
    {
        m_view->setDragMode(QGraphicsView::RubberBandDrag);
451
452
453
        // prevent item mouse event handling
        for (auto& item : m_scene->items())
            item->setAcceptedMouseButtons(Qt::AllButtons);
454
    }
455
456

    return QFrame::keyReleaseEvent(event);
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
}

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 detail::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 detail::Graphics_view_zoom::set_zoom_factor_base(double value) {
    _zoom_factor_base = value;
}

bool detail::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);
491
492
493
494
495
496
        if (wheel_event->angleDelta().y() != 0) {
            double angle = wheel_event->angleDelta().y();
            double factor = qPow(_zoom_factor_base, angle);
            gentle_zoom(factor);
            return true;
        }
497
498
499
500
    }
    Q_UNUSED(object)
    return false;
}