From c96c0d538f7badcbcc7f38e05b1237192e119c86 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Fri, 6 Mar 2020 20:58:55 +0100 Subject: [PATCH] Implemented basic grid rendering of task strips --- .gitignore | 103 ++++++++++++++--------- SprintPlanningTool.pro | 21 +++++ flowlayout.cpp | 150 +++++++++++++++++++++++++++++++++ flowlayout.h | 34 ++++++++ main.cpp | 14 +++ mainwindow.cpp | 11 +++ mainwindow.h | 19 +++++ mainwindow.ui | 81 ++++++++++++++++++ stripsgrid.cpp | 187 +++++++++++++++++++++++++++++++++++++++++ stripsgrid.h | 27 ++++++ stripwidget.cpp | 40 +++++++++ stripwidget.h | 26 ++++++ stripwidget.ui | 128 ++++++++++++++++++++++++++++ 13 files changed, 800 insertions(+), 41 deletions(-) create mode 100644 SprintPlanningTool.pro create mode 100644 flowlayout.cpp create mode 100644 flowlayout.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 stripsgrid.cpp create mode 100644 stripsgrid.h create mode 100644 stripwidget.cpp create mode 100644 stripwidget.h create mode 100644 stripwidget.ui diff --git a/.gitignore b/.gitignore index f147edf..fab7372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,73 @@ -# C++ objects and libs -*.slo -*.lo -*.o +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave *.a -*.la -*.lai +*.core +*.moc +*.o +*.obj +*.orig +*.rej *.so *.so.* -*.dll -*.dylib - -# Qt-es -object_script.*.Release -object_script.*.Debug -*_plugin_import.cpp +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc /.qmake.cache /.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -*.qmlc -*.jsc -Makefile* -*build-* -*.qm -*.prl -# Qt unit tests -target_wrapper.* +# qtcreator generated files +*.pro.user* -# QtCreator -*.autosave +# xemacs temporary files +*.flc -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* +# Vim temporary files +.*.swp -# QtCreator CMake -CMakeLists.txt.user* +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* -# QtCreator 4.8< compilation database -compile_commands.json +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe -# QtCreator local machine specific files for imported projects -*creator.user* diff --git a/SprintPlanningTool.pro b/SprintPlanningTool.pro new file mode 100644 index 0000000..3fc51d8 --- /dev/null +++ b/SprintPlanningTool.pro @@ -0,0 +1,21 @@ +QT = core gui widgets + +CONFIG += c++17 + +DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +SOURCES += main.cpp \ + flowlayout.cpp \ + mainwindow.cpp \ + stripsgrid.cpp \ + stripwidget.cpp + +FORMS += \ + mainwindow.ui \ + stripwidget.ui + +HEADERS += \ + flowlayout.h \ + mainwindow.h \ + stripsgrid.h \ + stripwidget.h diff --git a/flowlayout.cpp b/flowlayout.cpp new file mode 100644 index 0000000..ee77562 --- /dev/null +++ b/flowlayout.cpp @@ -0,0 +1,150 @@ +#include "flowlayout.h" + +#include + +FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) + : m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::~FlowLayout() +{ + QLayoutItem *item; + while ((item = takeAt(0))) + delete item; +} + +void FlowLayout::addItem(QLayoutItem *item) +{ + itemList.append(item); +} + +int FlowLayout::horizontalSpacing() const +{ + if (m_hSpace >= 0) { + return m_hSpace; + } else { + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); + } +} + +int FlowLayout::verticalSpacing() const +{ + if (m_vSpace >= 0) { + return m_vSpace; + } else { + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); + } +} + +int FlowLayout::count() const +{ + return itemList.size(); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + return itemList.value(index); +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < itemList.size()) + return itemList.takeAt(index); + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return 0; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + int height = doLayout(QRect(0, 0, width, 0), true); + return height; +} + +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const QLayoutItem *item : qAsConst(itemList)) + size = size.expandedTo(item->minimumSize()); + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const +{ + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + for (QLayoutItem *item : qAsConst(itemList)) { + const QWidget *wid = item->widget(); + int spaceX = horizontalSpacing(); + if (spaceX == -1) + spaceX = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal); + int spaceY = verticalSpacing(); + if (spaceY == -1) + spaceY = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical); + + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + bottom; +} + +int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) { + return -1; + } else if (parent->isWidgetType()) { + QWidget *pw = static_cast(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(parent)->spacing(); + } +} diff --git a/flowlayout.h b/flowlayout.h new file mode 100644 index 0000000..1a682e2 --- /dev/null +++ b/flowlayout.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +class FlowLayout : public QLayout +{ +public: + explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1); + explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); + ~FlowLayout(); + + void addItem(QLayoutItem *item) override; + int horizontalSpacing() const; + int verticalSpacing() const; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int) const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +private: + int doLayout(const QRect &rect, bool testOnly) const; + int smartSpacing(QStyle::PixelMetric pm) const; + + QList itemList; + int m_hSpace; + int m_vSpace; +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..a7556c4 --- /dev/null +++ b/main.cpp @@ -0,0 +1,14 @@ +#include + +#include "mainwindow.h" +#include "stripwidget.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + MainWindow mainWindow; + mainWindow.show(); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..f32e756 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,11 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow{parent}, + m_ui{std::make_unique()} +{ + m_ui->setupUi(this); +} + +MainWindow::~MainWindow() = default; diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..c3c45fb --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include + +namespace Ui { class MainWindow; } + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private: + std::unique_ptr m_ui; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..3d17997 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,81 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + true + + + + + 0 + 0 + 798 + 558 + + + + + + + + 0 + 0 + 800 + 18 + + + + + &File + + + + + + + + + &Quit + + + + + + StripsGrid + QWidget +
stripsgrid.h
+
+
+ + + + action_Quit + triggered() + MainWindow + close() + + + -1 + -1 + + + 399 + 299 + + + + +
diff --git a/stripsgrid.cpp b/stripsgrid.cpp new file mode 100644 index 0000000..927de15 --- /dev/null +++ b/stripsgrid.cpp @@ -0,0 +1,187 @@ +#include "stripsgrid.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stripwidget.h" +#include "flowlayout.h" + +namespace { +QDebug &operator<<(QDebug &debug, const QMouseEvent &event) +{ + return debug << "QMouseEvent(" << event.pos() << event.source() << event.flags() << ")"; +} + +template +void call_n_times(int count, T func) +{ + for (int i = 0; i < count; i++) + func(); +} +} + +StripsGrid::StripsGrid(QWidget *parent) : + QWidget{parent} +{ + m_layout = new QGridLayout{this}; + + QRandomGenerator random((qint32)(QDateTime::currentMSecsSinceEpoch())); + + const auto stories = random.bounded(3, 10); + + int row{0}; + { + int column{0}; + for (const auto text : {"Story", "To Do:", "In Progress:", "In Review:", "Done:"}) + { + auto label = new QLabel{text}; + { + auto font = label->font(); + font.setPointSize(24); + label->setFont(font); + } + label->setFixedHeight(40); + m_layout->addWidget(label, row, column++); + + { + auto line = new QFrame; + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + m_layout->addWidget(line, row, column++, (stories*2)+1, 1); + } + } + } + + row++; + + call_n_times(stories, [&]() + { + { + auto line = new QFrame; + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + m_layout->addWidget(line, row, 0, 1, 9); + } + + row++; + + int storyPoints{0}; + + auto storyWidget = new StripWidget; + storyWidget->setTitle(QString("ATC-%0").arg(random.bounded(1000, 5000))); + storyWidget->setStyleSheet("StripWidget { background-color: #AFA; }"); + + for (int column = 0; column < 5; column++) + { + if (column == 0) + { + auto layout = new QVBoxLayout; + layout->addSpacing(0); + layout->addWidget(storyWidget); + layout->addStretch(1); + m_layout->addLayout(layout, row, column*2); + } + else + { + int count = random.bounded(0, 5); + if (!count) + continue; + + auto layout = new FlowLayout; + + call_n_times(count, [&]() + { + auto widget = new StripWidget; + widget->setTitle(QString("ATC-%0").arg(random.bounded(1000, 5000))); + { + const auto points = std::array{1,3,5,8,13}[random.bounded(5)]; + widget->setPoints(points); + storyPoints += points; + } + widget->setOwner(std::array{"DB", "KW", "BK", "MS", "AS"}[random.bounded(5)]); + widget->setStyleSheet("StripWidget { background-color: #FCC; }"); + layout->addWidget(widget); + }); + + m_layout->addLayout(layout, row, column*2); + } + } + + storyWidget->setPoints(storyPoints); + + row++; + }); + + m_layout->addItem(new QSpacerItem{0, 40, QSizePolicy::Minimum, QSizePolicy::Expanding}, ++row, 0, 1, 5); + + m_widget = new StripWidget{this}; + m_widget->setStyleSheet("StripWidget { background-color: #88FFFF; }"); + m_widget->move(100, 100); + +// auto widget = new QWidget; +// setCentralWidget(widget); + +// m_layout = new QGridLayout{widget}; +// const auto addColumn = [&](const QString &name) +// { +// auto label = new QLabel{name}; +// { +// auto font = label->font(); +// font.setPointSize(30); +// label->setFont(font); +// } +// m_layout->addWidget(label, 0, m_layout->columnCount()); +// }; +// addColumn("To Do"); +// addColumn("In Progress"); +// addColumn("In Review"); +// addColumn("Done"); + +// m_layout->addWidget(new QWidget, 1, 0); +} + +void StripsGrid::mousePressEvent(QMouseEvent *event) +{ + qDebug() << "mousePressEvent" << *event; + + if (event->button() == Qt::LeftButton && + !m_isDragging && + m_widget->startDragging(event->pos() - m_widget->pos())) + { + m_widget->setGraphicsEffect(new QGraphicsOpacityEffect); + m_isDragging = true; + m_dragOffset = event->pos() - m_widget->pos(); + } + + QWidget::mousePressEvent(event); +} + +void StripsGrid::mouseReleaseEvent(QMouseEvent *event) +{ + qDebug() << "mouseReleaseEvent" << *event; + + if (event->button() == Qt::LeftButton && + m_isDragging) + { + m_widget->setGraphicsEffect(nullptr); + m_isDragging = false; + } + + QWidget::mouseReleaseEvent(event); +} + +void StripsGrid::mouseMoveEvent(QMouseEvent *event) +{ + qDebug() << "mouseMoveEvent" << *event; + + if (m_isDragging) + m_widget->move(event->pos() - m_dragOffset); + + QWidget::mouseMoveEvent(event); +} diff --git a/stripsgrid.h b/stripsgrid.h new file mode 100644 index 0000000..db81001 --- /dev/null +++ b/stripsgrid.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class QGridLayout; + +class StripWidget; + +class StripsGrid : public QWidget +{ + Q_OBJECT + +public: + explicit StripsGrid(QWidget *parent = nullptr); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + +private: + QGridLayout *m_layout; + + bool m_isDragging{false}; + QPoint m_dragOffset; + StripWidget *m_widget; +}; diff --git a/stripwidget.cpp b/stripwidget.cpp new file mode 100644 index 0000000..726ef4c --- /dev/null +++ b/stripwidget.cpp @@ -0,0 +1,40 @@ +#include "stripwidget.h" +#include "ui_stripwidget.h" + +StripWidget::StripWidget(QWidget *parent) : + QFrame{parent}, + m_ui{std::make_unique()} +{ + m_ui->setupUi(this); + + setMinimumSize(size()); + setMaximumSize(size()); + setFixedSize(size()); +} + +StripWidget::~StripWidget() = default; + +bool StripWidget::startDragging(const QPoint &pos) const +{ + return m_ui->title->geometry().contains(pos); +} + +void StripWidget::setTitle(const QString &title) +{ + m_ui->title->setText(title); +} + +void StripWidget::setDescription(const QString &description) +{ + m_ui->description->setText(description); +} + +void StripWidget::setPoints(int points) +{ + m_ui->points->setText(QString::number(points)); +} + +void StripWidget::setOwner(const QString &owner) +{ + m_ui->owner->setText(owner); +} diff --git a/stripwidget.h b/stripwidget.h new file mode 100644 index 0000000..292fcf2 --- /dev/null +++ b/stripwidget.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace Ui { class StripWidget; } + +class StripWidget : public QFrame +{ + Q_OBJECT + +public: + explicit StripWidget(QWidget *parent = nullptr); + ~StripWidget() override; + + bool startDragging(const QPoint &pos) const; + + void setTitle(const QString &title); + void setDescription(const QString &description); + void setPoints(int points); + void setOwner(const QString &owner); + +private: + std::unique_ptr m_ui; +}; diff --git a/stripwidget.ui b/stripwidget.ui new file mode 100644 index 0000000..575f24e --- /dev/null +++ b/stripwidget.ui @@ -0,0 +1,128 @@ + + + StripWidget + + + + 0 + 0 + 195 + 105 + + + + Form + + + QFrame::Panel + + + QFrame::Raised + + + + + + + + + 15 + + + + QFrame::Panel + + + QFrame::Sunken + + + ATC-1234 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Refactor once again everything + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + 12 + + + + QFrame::Panel + + + QFrame::Sunken + + + TextLabel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 12 + + + + QFrame::Panel + + + QFrame::Sunken + + + TextLabel + + + + + + + + + +