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 + + + + + + + + + +