1、概述
源码放在文章末尾

Qt 多列时间轴控件。

可与多段字符串格式自由转换,也可手动添加列表项。

专门用来以时间轴作为事件线发展顺序的故事大纲。

特点
时间背包功能:记录所有物品或属性发生的变化,随时回溯
时间可输入任意内容,不限于时间
每一时间段允许多列,即多个文字节点
全自动调整节点大小(宽高),尽量看起来舒服
行与行、列与列 之间任意拖拽更换顺序
可与文字自由转换,默认一段为一个文字节点
方便的多行操作
按需修改快捷键
所有编辑可撤销
美观的调整动画

项目demo演示如下所示:
《QT实用小工具·六十》Qt 多列时间轴控件-LMLPHP
项目部分代码如下所示:

#ifndef TIMELINEWIDGET_H
#define TIMELINEWIDGET_H

#include <QObject>
#include <QDebug>
#include <QInputDialog>
#include <QMenu>
#include <QAction>
#include <QMimeData>
#include <QDrag>
#include <QPropertyAnimation>
#include <QScrollArea>
#include <QScrollBar>
#include <QPlainTextEdit>
#include <QTimer>
#include "timelinebucket.h"
#include "labeleditor.h"

class TimelineWidget : public QScrollArea
{
    Q_OBJECT
public:
    TimelineWidget(QWidget *parent = nullptr);

    void addItem(QString time, QString text);
    void addItem(QString time, QStringList texts);
    TimelineBucket* insertItem(QString time, QStringList texts, int index = -1);
    void removeItem(int index);
    void clearAll();
    int count();
    int indexOf(TimelineBucket* bucket) const;
    TimelineBucket* at(int index) const;
    void moveBucket(int from_index, int to_index);

    void selectAll();
    void unselectAll();
    void selectItem(TimelineBucket* bucket);
    void selectItems(QList<int> rows, bool clearBefore = true);
    void unselectItem(TimelineBucket* bucket);
    void setCurrentItem(int row, bool multi = false);
    void setCurrentItem(TimelineBucket* bucket, bool multi = false);
    void scrollTo(int index = -1);
    QList<int> selectedIndexes(int delta = 0) const;

    void resetWidth();
    void adjustBucketsPositions(int start = -1);
    void adjustBucketsPositionsWithAnimation(int start = 0, int end = -1);

    void fromString(QString string, QString time_reg = "【(.*)】", QString node_split = "\n", QString nodes_split = "\n");
    QString toString(QString time_format = "【%1】", QString para_split = "\n", QString line_split = "\n\n");

protected:
    void keyPressEvent(QKeyEvent* event) override;

private:
    TimelineBucket *createItemWidget(QString time, QStringList texts);

signals:
    void manualSelected(); // 鼠标/键盘事件
    void targetItemsChanged(); // 选中项改变,或者选中的内容改变了

public slots:
    void updateUI();
    void slotBucketWidgetToSelect(TimelineBucket* bucket);
    void slotTimeWidgetClicked(TimelineTimeLabel* label);
    void slotTextWidgetClicked(TimelineTextLabel* label);
    void slotTimeWidgetDoubleClicked(TimelineTimeLabel* label);
    void slotTextWidgetDoubleClicked(TimelineTextLabel* label);
    void slotMenuShowed(const QPoint& pos);
    void slotDroppedAndMoved(TimelineBucket* from, TimelineBucket* to);
    void slotEditChanged();
    void slotEdit(int row, int col);
    void finishEditing();

    void actionAddText();
    void actionAddTextLeft();
    void actionAddTextRight();
    void actionEditTime();
    void actionEditText(int index);
    void actionAddLine();
    void actionInsertAbove();
    void actionInsertUnder();
    void actionDeleteLine();
    void actionCopyText();
    void actionPaste();

private:
    QWidget* center_widget;
    QList<TimelineBucket*> buckets;
    QList<TimelineBucket*> selected_buckets;
    int current_index;

    LabelEditor* edit;
    TimelineBucket* editing_bucket;
    QLabel* editing_label;
    
    bool _adusting_buckets_size; // 是否正在调整索引buckets大小(无视此时触发的sizeHintChanged信号)
    bool _width_need_adjust = false; // 下次动画是否强制调整宽度
};

#endif // TIMELINEWIDGET_H
#include "timelinewidget.h"

TimelineWidget::TimelineWidget(QWidget *parent) : QScrollArea(parent)
{
    setAcceptDrops(true);
    setAttribute(Qt::WA_TransparentForMouseEvents, false);
    setContextMenuPolicy(Qt::CustomContextMenu);
    setFocusPolicy(Qt::StrongFocus);
    connect(this,SIGNAL(customContextMenuRequested (const QPoint&)),this,SLOT(slotMenuShowed(const QPoint&)));

    _adusting_buckets_size = false;
    _width_need_adjust = false;
    current_index = -1;
    center_widget = new QWidget(this);
    setWidget(center_widget);

    editing_bucket = nullptr;
    editing_label = nullptr;
    edit = new LabelEditor(center_widget);
    connect(edit, &LabelEditor::textChanged, this, [=] {
        if (editing_label == nullptr)
            return ;
        editing_label->setText(edit->toPlainText());
//        if (editing_label->objectName() == "TimelineTextLabel") {
//            static_cast<TimelineTextLabel>(editing_label).adjustSize(true, edit->toPlainText());
//        } else {
            editing_label->adjustSize();
//        }
        edit->move(editing_label->pos() + editing_label->parentWidget()->pos());
        edit->resize(editing_label->size());
        editing_bucket->adjustWidgetsPositions();
    });
    connect(edit, &LabelEditor::signalEditCanceled, this, [=](QString origin) {
        if (editing_label == nullptr)
            return ;
        edit->setPlainText(origin); // 设置回初始内容
        QTimer::singleShot(0, [=]{
            editing_label = nullptr;
            editing_bucket = nullptr;
        });
        this->setFocus();
    });
    connect(edit, &LabelEditor::signalEditFinished, this, [=](QString text) {
        if (editing_label == nullptr) // 快速按下两次时会触发这个信号槽,而第一次已经使 editing_label = nullptr
            return ;
        // 编辑结束,保存 undo
        QString orig = edit->getOriginText();
        if (text != orig) // 文本有变动
        {
            if (editing_bucket->indexOf(static_cast<TimelineTextLabel*>(editing_label)) >= 0)
            {
                timeline_undos->modifyCommand(editing_bucket, static_cast<TimelineTextLabel*>(editing_label), orig, text);
                emit targetItemsChanged();
            }
            else
            {
                timeline_undos->modifyCommand(editing_bucket, orig, text);
                emit targetItemsChanged();
            }
        }
        edit->hide();
        editing_label = nullptr;
        editing_bucket = nullptr;
        this->setFocus();
    });
    edit->hide();

    updateUI();
}

void TimelineWidget::addItem(QString time, QString text)
{
    addItem(time, QStringList{text});
}

void TimelineWidget::addItem(QString time, QStringList texts)
{
    insertItem(time, texts, -1);
}

TimelineBucket *TimelineWidget::insertItem(QString time, QStringList texts, int index)
{
    TimelineBucket* bucket = createItemWidget(time, texts);
    bucket->adjustWidgetsPositions();
    if (index < 0 || index >= count()) // 添加到末尾
    {
        if (count() >= 1)
            bucket->move(buckets.last()->pos());
        buckets.append(bucket);
        bucket->setVerticalIndex(count()-1); // 已经添加了,下标索引要-1
        if (count())
            bucket->move(buckets.last()->geometry().topLeft());
    }
    else // 插入到中间
    {
        buckets.insert(index, bucket);
        for (int i = index; i < count(); i++)
            buckets.at(i)->setVerticalIndex(i);
        if (index+1 < count())
            bucket->move(buckets.at(index+1)->geometry().topLeft());
        else if (index > 0)
            bucket->move(buckets.at(index-1)->geometry().topLeft());
    }

    bucket->show();

    // 设置item的尺寸
    connect(bucket, &TimelineBucket::signalSizeHintChanged, this, [=](QSize size){
        if (!_adusting_buckets_size)
            adjustBucketsPositions(buckets.indexOf(bucket));
    });

    // 连接事件信号
    connect(bucket, &TimelineBucket::signalBucketWidgetPressed, this, [=]{ slotBucketWidgetToSelect(bucket); });
    connect(bucket, SIGNAL(signalTimeWidgetClicked(TimelineTimeLabel*)), this, SLOT(slotTimeWidgetClicked(TimelineTimeLabel*)));
    connect(bucket, SIGNAL(signalTextWidgetClicked(TimelineTextLabel*)), this, SLOT(slotTextWidgetClicked(TimelineTextLabel*)));
    connect(bucket, SIGNAL(signalTimeWidgetDoubleClicked(TimelineTimeLabel*)), this, SLOT(slotTimeWidgetDoubleClicked(TimelineTimeLabel*)));
    connect(bucket, SIGNAL(signalTextWidgetDoubleClicked(TimelineTextLabel*)), this, SLOT(slotTextWidgetDoubleClicked(TimelineTextLabel*)));

    connect(bucket, &TimelineBucket::signalDroppedAndMoved, this, [=](TimelineBucket* from_bucket) {
        slotDroppedAndMoved(from_bucket, bucket);
    });

    return bucket;
}

void TimelineWidget::removeItem(int index)
{
    if (index < 0 || index >= count())
        return ;

    auto bucket = buckets.takeAt(index);
    selected_buckets.removeOne(bucket);
    bucket->deleteLater();

    adjustBucketsPositionsWithAnimation(index);
}

void TimelineWidget::clearAll()
{
    while (buckets.size())
    {
        buckets.takeFirst()->deleteLater();
    }
}

int TimelineWidget::count()
{
    return buckets.size();
}

int TimelineWidget::indexOf(TimelineBucket *bucket) const
{
    return buckets.indexOf(bucket);
}

TimelineBucket *TimelineWidget::at(int index) const
{
    if (index < 0 || index >= buckets.size())
        return nullptr;
    return buckets.at(index);
}

void TimelineWidget::moveBucket(int from_index, int to_index)
{
    if (from_index == to_index) // 很可能发生的自己和自己交换
        return ;
    if (from_index < 0 || to_index < 0)
        return ;
    finishEditing();

    // 交换 bucket
    TimelineBucket* bucket = buckets.at(from_index);
    buckets.removeAt(from_index);
    if (from_index < to_index) // 下移
    {
        buckets.insert(to_index, bucket);
        for (int i = from_index; i <= to_index; i++)
            buckets.at(i)->setVerticalIndex(i);
    }
    else // 上移
    {
        buckets.insert(to_index, bucket);
        for (int i = from_index; i >= to_index; i--)
            buckets.at(i)->setVerticalIndex(i);
    }

    adjustBucketsPositionsWithAnimation(qMin(from_index, to_index));
}

void TimelineWidget::selectAll()
{
    int left = horizontalScrollBar()->sliderPosition(),
            right = horizontalScrollBar()->sliderPosition() + horizontalScrollBar()->pageStep();
    bool odd = true;
    foreach (TimelineBucket* bucket, buckets) {
        if (odd)
            bucket->setPressPos(QPoint(left, bucket->height()/2));
        else
            bucket->setPressPos(QPoint(right, bucket->height()/2));
        odd = !odd;
        bucket->setSelected(true);
    }
    selected_buckets = buckets;
    emit targetItemsChanged();
}

void TimelineWidget::unselectAll()
{
    foreach (TimelineBucket* bucket, selected_buckets) {
        bucket->setSelected(false);
    }
    selected_buckets.clear();
    emit targetItemsChanged();
}

void TimelineWidget::selectItem(TimelineBucket *bucket)
{
    bucket->setSelected(true);
    if (!selected_buckets.contains(bucket))
        selected_buckets.append(bucket);
    emit targetItemsChanged();
}

void TimelineWidget::selectItems(QList<int> rows, bool clearBefore)
{
    if (clearBefore)
        unselectAll();
    foreach (auto row, rows)
    {
        auto bucket = buckets.at(row);
        bucket->setSelected(true);
        if (!selected_buckets.contains(bucket))
            selected_buckets.append(bucket);
    }
    emit targetItemsChanged();
}

void TimelineWidget::unselectItem(TimelineBucket *bucket)
{
    bucket->setSelected(false);
    selected_buckets.removeOne(bucket);
    emit targetItemsChanged();
}

void TimelineWidget::setCurrentItem(int row, bool multi)
{
    if (!multi)
        unselectAll();
    selectItem(buckets.at(row));
    current_index = row;
}

void TimelineWidget::setCurrentItem(TimelineBucket *bucket, bool multi)
{
    if (!multi)
        unselectAll();
    selectItem(bucket);
    current_index = buckets.indexOf(bucket);
}

/**
 * 确保某个bucket可视
 */
void TimelineWidget::scrollTo(int index)
{
    if (index == -1)
        index = current_index;
    if (index == -1)
        return ;
    auto bucket = buckets.at(index);
    int h = bucket->height();
    int top = bucket->pos().y();
    int bottom = bucket->geometry().bottom();
    if (top - h < verticalScrollBar()->sliderPosition()) // 在上面
    {
        verticalScrollBar()->setSliderPosition(top - h);
    }
    else if (bottom + h > verticalScrollBar()->sliderPosition() + verticalScrollBar()->pageStep()) // 在下面
    {
        verticalScrollBar()->setSliderPosition(bottom + h - verticalScrollBar()->pageStep());
    }
}

QList<int> TimelineWidget::selectedIndexes(int delta) const
{
    int size = buckets.size();
    QList<int> indexes;
    for (int i = 0; i < size; i++)
        if (buckets.at(i)->isSelected())
            indexes << (i+delta);
    return indexes;
}

void TimelineWidget::resetWidth()
{
    _width_need_adjust = true;
}

/**
 * 调整某一个位置及后面的所有top
 */
void TimelineWidget::adjustBucketsPositions(int start)
{
    int end = count();
    int top = (start-1) >= 0 ? buckets.at(start-1)->geometry().bottom() : 0;
    int max_width = 0;
    int current_width = center_widget->width();
    if (start > 0)
        max_width = center_widget->width();
    for (int i = start; i < end; i++)
    {
        TimelineBucket* bucket = buckets.at(i);
        if (max_width < bucket->width())
            max_width = bucket->width();
        bucket->move(bucket->pos().x(), top);
        top += bucket->height();
    }

    _adusting_buckets_size = true;
    {
        if (max_width != current_width || _width_need_adjust)
        {
            foreach (auto bucket, buckets)
            {
                bucket->resize(max_width, bucket->height());
            }
        }

        int height = 0;
        if (buckets.size())
            height = top + buckets.last()->height();
        else
            height = 50;
        center_widget->resize(max_width, height);
    }
   _adusting_buckets_size = false;
}

/**
 * 调整某一范围内 buckets 的位置
 * 并且包含位置移动动画
 */
void TimelineWidget::adjustBucketsPositionsWithAnimation(int start, int end)
{
    if (end == -1)
        end = count();
    else
        end++;
    int top = (start-1) >= 0 ? buckets.at(start-1)->geometry().bottom() : 0;
    int current_width = center_widget->width();
    int max_width = 0;
    if (start > 0)
        max_width = center_widget->width();
    for (int i = start; i < end; i++)
    {
        TimelineBucket* bucket = buckets.at(i);
        if (max_width < bucket->width())
            max_width = bucket->width();
        if (top != bucket->pos().y())
        {
            QPropertyAnimation* ani = new QPropertyAnimation(bucket, "pos");
            ani->setStartValue(bucket->pos());
            ani->setEndValue(QPoint(bucket->pos().x(), top));
            ani->setDuration(300);
            ani->setEasingCurve(QEasingCurve::OutQuart);
            connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
            ani->start();
        }
        top += bucket->height();
    }

    // 这句会在启动时触发 signalSizeHintChanged,但是必须需要啊
    // _adusting_buckets_size = true;
    {
        if (max_width != current_width || _width_need_adjust)
        {
            foreach (auto bucket, buckets)
            {
                bucket->resize(max_width, bucket->height());
            }
        }

        int height = 0;
        if (buckets.size())
            height = top + buckets.last()->height();
        else
            height = 50;
        center_widget->resize(max_width, height);
    }
    // _adusting_buckets_size = false;
}

/**
 * 从字符串中读取
 * @param string       带格式的字符串
 * @param time_format  获取时间正则表达式,以第一个括号确定(不要带有 ^ $ 标记!)
 * @param node_split   时间节点内分段格式
 * @param nodes_split  时间节点之间分段格式
 */
void TimelineWidget::fromString(QString string, QString time_reg, QString node_split, QString nodes_split)
{
    QList<QString> times;
    QList<QStringList> textss;

    if (node_split == nodes_split) // 分段符一致,以每一段的时间标记为准
    {
        QString time, time_total;
        QStringList texts;

        QRegExp rx(time_reg);
        rx.setMinimal(true);
        
        QStringList lines = string.split(nodes_split, QString::SkipEmptyParts);
        for (int i = 0; i < lines.length(); i++)
        {
            QString& line = lines[i];
            int pos = rx.indexIn(line);
            if (pos != -1) // 找到时间标记
            {
                // 添加上一个时间轴
                if (time != nullptr || texts.length() > 0)
                {
                    times.append(time);
                    textss.append(texts);
                    time = "";
                    texts.clear();
                }
                
                time_total = rx.cap(0);
                time = rx.cap(1);

                // 删除行内标记
                QRegExp ex(time_total + "[\\s ]*");
                line.replace(ex, "");
                if (!line.trimmed().isEmpty()) // 这一段还有其他内容,继续便利
                    i--;
            }
            else
            {
                texts.append(line.trimmed());
            }
        }
        if (time != nullptr || texts.length() > 0)
        {
            times.append(time);
            textss.append(texts);
            time = "";
            texts.clear();
        }
    }
    else // 根据分割来
    {
        QStringList lines = string.split(nodes_split, QString::SkipEmptyParts);
        foreach (QString line, lines)
        {
            QString time_total, time; // 带格式的时间字符串、纯时间字符串
            QStringList texts;
            QRegExp rx(time_reg);
            rx.setMinimal(true);
            int pos = rx.indexIn(line);
            if (pos != -1)
            {
                time_total = rx.cap(0);
                time = rx.cap(1);

                // 删除时间标记
                QRegExp ex(time_total + "[\\s ]*");
                line.replace(ex, "");
            }
            texts = line.split(node_split, QString::SkipEmptyParts);
            for (int i = 0; i < texts.size(); i++)
            {
                texts[i] = texts[i].trimmed();
            }

            times.append(time);
            textss.append(texts);
        }
    }

    QList<int> indexs;
    int c = count();
    for (int i = 0; i < times.size(); i++)
        indexs.append(c);
    timeline_undos->addCommand(indexs, times, textss);
}

/**
 * 将时间轴转换成带分段格式的字符串
 * @param time_format 时间格式,以 %1 确定
 * @param para_split  同一时间节点内分段格式
 * @param line_split  时间节点之间的分段格式
 * @return 所有字符串
 */
QString TimelineWidget::toString(QString time_format, QString para_split, QString line_split)
{
    QString result;
    foreach (auto bucket, buckets)
    {
        if (!result.isEmpty())
            result += line_split;
        result += bucket->toString(time_format, para_split);
    }
    return result;
}

void TimelineWidget::keyPressEvent(QKeyEvent *event)
{
    auto modifiers = event->modifiers();
    auto key = event->key();
    switch (key)
    {
    case Qt::Key_Up:
        if (current_index > 0)
        {
            if (modifiers == Qt::NoModifier) // 上移选中项
            {
                auto bucket = buckets.at(current_index-1);
                bucket->setPressPos(QPoint(qMin(bucket->width(), horizontalScrollBar()->pageStep()), bucket->height()));
                setCurrentItem(current_index-1);
                scrollTo();
                emit manualSelected();
                return ;
            }
            else if (modifiers == Qt::ShiftModifier) // 上移并选中/取消你
            {
                auto bucket = buckets.at(current_index);
                auto bucket_up = buckets.at(current_index-1);
                if (bucket->isSelected() && bucket_up->isSelected())
                {
                    unselectItem(bucket);
                    current_index--;
                }
                else
                {
                    bucket_up->setPressPos(QPoint(qMin(bucket_up->width(), horizontalScrollBar()->pageStep()), bucket_up->height()));
                    setCurrentItem(current_index-1, true);
                }
                scrollTo();
                emit manualSelected();
                return ;
            }
        }
        break;
    case Qt::Key_Down:
        if (current_index > -1 && current_index < count()-1)
        {
            if (modifiers == Qt::NoModifier) // 下移选中项
            {
                auto bucket = buckets.at(current_index+1);
                bucket->setPressPos(QPoint(qMin(bucket->width(), horizontalScrollBar()->pageStep()), 0));
                setCurrentItem(current_index+1);
                scrollTo();
                emit manualSelected();
                return ;
            }
            else if (modifiers == Qt::ShiftModifier) // 下移并选中/取消
            {
                auto bucket = buckets.at(current_index);
                auto bucket_down = buckets.at(current_index+1);
                if (bucket->isSelected() && bucket_down->isSelected())
                {
                    unselectItem(bucket);
                    current_index++;
                }
                else
                {
                    bucket_down->setPressPos(QPoint(qMin(bucket_down->width(), horizontalScrollBar()->pageStep()), 0));
                    setCurrentItem(current_index+1, true);
                }
                scrollTo();
                emit manualSelected();
                return ;
            }
        }
        break;
    case Qt::Key_Home:
        if (current_index > 0 && modifiers == Qt::ShiftModifier)
        {
            int index = current_index;
            while (index >= 0)
            {
                setCurrentItem(index, true);
                index--;
            }
            scrollTo();
            emit manualSelected();
            return ;
        }
        break;
    case Qt::Key_End:
        if (current_index > -1 && modifiers == Qt::ShiftModifier)
        {
            int index = current_index;
            while (index < count())
            {
                setCurrentItem(index, true);
                index++;
            }
            scrollTo();
            emit manualSelected();
            return ;
        }
        break;
    case Qt::Key_Delete:
    {
        int index = current_index;
        actionDeleteLine();
        // 删除键删除的需要继续保持选中状态
        if (index > -1 && index < count()) // 聚焦原来的同一个索引
            setCurrentItem(index);
        else if (index > 0 && index == count()) // 聚焦最后一个
            setCurrentItem(index-1);
        return ;
    }
    case Qt::Key_Insert:
        actionInsertAbove();
        return ;
    case Qt::Key_Escape:
        if (current_index > -1)
        {
            if (selected_buckets.size() > 1)
                setCurrentItem(current_index);
            else if (selected_buckets.size())
                unselectItem(buckets.at(current_index));
            return ;
        }
        else
        {
            unselectAll();
        }
        break;
    case Qt::Key_A:
        if (modifiers == Qt::ControlModifier)
        {
            selectAll();
            return ;
        }
        break;
    case Qt::Key_Z:
        if (modifiers == Qt::ControlModifier)
        {
            timeline_undos->undoCommand();
            return ;
        }
        break;
    case Qt::Key_Y:
        if (modifiers == Qt::ControlModifier)
        {
            timeline_undos->redoCommand();
            return ;
        }
        break;
    case Qt::Key_C:
        actionCopyText();
        return ;
    case Qt::Key_Tab:
        /**
         * 注意:如果要监听到 Tab 键,要禁止 Tab 切换
         * QWidget::setFocus(Qt::NoFocus)
         */
        if (modifiers == Qt::NoModifier)
        {
            actionAddText();
        }
        return ;
    case Qt::Key_Enter:
    case Qt::Key_Return:
        if (modifiers == Qt::ShiftModifier)
            actionInsertAbove();
        else if (modifiers == Qt::ControlModifier)
            actionAddLine();
        else if (modifiers == Qt::NoModifier)
            actionInsertUnder();
        return ;
    case Qt::Key_Space:
        actionEditText(0);
        return ;
    case Qt::Key_1:
        actionEditText(0);
        return ;
    case Qt::Key_2:
        actionEditText(1);
        return ;
    case Qt::Key_3:
        actionEditText(2);
        return ;
    case Qt::Key_4:
        actionEditText(3);
        return ;
    case Qt::Key_5:
        actionEditText(4);
        return ;
    case Qt::Key_6:
        actionEditText(5);
        return ;
    case Qt::Key_7:
        actionEditText(6);
        return ;
    case Qt::Key_8:
        actionEditText(7);
        return ;
    case Qt::Key_9:
        actionEditText(8);
        return ;
    case Qt::Key_0:
    case Qt::Key_QuoteLeft: // 反撇号
        actionEditTime();
        return ;
    case Qt::Key_Apostrophe: // 这是单引号……
        break;
    }

    QScrollArea::keyPressEvent(event);
}

TimelineBucket *TimelineWidget::createItemWidget(QString time, QStringList texts)
{
    TimelineBucket* bucket = new TimelineBucket(center_widget);
    bucket->setTime(time);
    bucket->setText(texts);
    connect(bucket, SIGNAL(signalBucketContentsChanged()), this, SIGNAL(targetItemsChanged()));
    return bucket;
}

void TimelineWidget::updateUI()
{
    QString style = "#TimelineTimeLabel { background:white; border: 1px solid orange; border-radius: 5px; padding: 10px; }"
            "#TimelineTextLabel { background:white; border: 1px solid blue; border-radius: 5px; padding: 10px; }"
            "#TimelineEdit { background:white; border: 1px solid transparent; border-radius: 5px; padding: 5px; margin: 1px; margin-left: 4px;}";
    setStyleSheet(style);
}

void TimelineWidget::slotBucketWidgetToSelect(TimelineBucket *bucket)
{
    finishEditing();

    if (QApplication::keyboardModifiers() == Qt::NoModifier) // 没有修饰符,单选
    {
        setCurrentItem(bucket);
    }
    else if (QApplication::keyboardModifiers() == Qt::ControlModifier) // 按下 ctrl
    {
        if (!bucket->isSelected())
            setCurrentItem(bucket, true);
        else
            unselectItem(bucket);
    }
    else if (QApplication::keyboardModifiers() == Qt::ShiftModifier) // 按下 shift
    {
        int prev = current_index; // 上次按下的
        int curr = buckets.indexOf(bucket);
        if (prev != -1)
        {
            if (prev < curr)
            {
                // 判断是否已经全选
                bool has_unselect = false;
                for (int i = prev; i <= curr; i++)
                {
                    if (!buckets.at(i)->isSelected())
                    {
                        has_unselect = true;
                        break;
                    }
                }

                // 再次遍历,如果有没有选择的,则选择;否则取消选择
                for (int i = prev; i <= curr; i++)
                {
                    TimelineBucket* bucket = buckets[i];
                    if (bucket->isSelected() != has_unselect)
                    {
                        selected_buckets.append(bucket);
                        bucket->setSelected(has_unselect);
                    }
                }
            }
            else if (prev > curr)
            {
                bool has_unselect = false;
                for (int i = prev; i >= curr; i--)
                {
                    if (!buckets.at(i)->isSelected())
                    {
                        has_unselect = true;
                        break;
                    }
                }

                for (int i = prev; i >= curr; i--)
                {
                    TimelineBucket* bucket = buckets[i];
                    if (bucket->isSelected() != has_unselect)
                    {
                        selected_buckets.append(bucket);
                        bucket->setSelected(has_unselect);
                    }
                }
            }
        }
        current_index = curr;
    }
    emit manualSelected();
}

void TimelineWidget::slotTimeWidgetClicked(TimelineTimeLabel *label)
{

}

void TimelineWidget::slotTextWidgetClicked(TimelineTextLabel *label)
{

}

void TimelineWidget::slotTimeWidgetDoubleClicked(TimelineTimeLabel *label)
{
    QTimer::singleShot(0, [=]{
        editing_bucket = buckets.at(current_index);
        editing_label = label;
        edit->move(label->pos() + label->parentWidget()->pos());
        edit->setPlainText(label->text());
        edit->resize(label->size());
        edit->setOriginText(label->text());
        edit->show();
        edit->raise();
        edit->setFocus();
        edit->selectAll();
    });
}

void TimelineWidget::slotTextWidgetDoubleClicked(TimelineTextLabel *label)
{
    QTimer::singleShot(0, [=]{
        editing_bucket = buckets.at(current_index);
        editing_label = label;
        edit->move(label->pos() + label->parentWidget()->pos());
        edit->setPlainText(label->text());
        edit->resize(label->size());
        edit->setOriginText(label->text());
        edit->show();
        edit->raise();
        edit->setFocus();
        edit->selectAll();
    });
}

void TimelineWidget::slotMenuShowed(const QPoint &pos)
{
    QMenu* menu = new QMenu("菜单", this);
    QAction* add_text_action = new QAction("添加文字节点", this);
    QAction* add_line_action = new QAction("添加新行", this);
    QAction* insert_above_action = new QAction("上方插入行", this);
    QAction* insert_under_action = new QAction("下方插入行", this);
    QAction* delete_line_action = new QAction("删除行", this);
    QAction* copy_text_action = new QAction("复制文字", this);
    QAction* paste_action = new QAction("剪贴板导入", this);
    menu->addAction(add_text_action);
    menu->addAction(add_line_action);
    menu->addAction(insert_above_action);
    menu->addAction(insert_under_action);
    menu->addAction(delete_line_action);
    menu->addSeparator();
    menu->addAction(copy_text_action);
    menu->addAction(paste_action);

    if (current_index == -1)
    {
        add_text_action->setEnabled(false);
        insert_above_action->setEnabled(false);
        insert_under_action->setEnabled(false);
        delete_line_action->setEnabled(false);
        copy_text_action->setEnabled(false);
    }

    // 设置事件
    connect(add_text_action, SIGNAL(triggered()), this, SLOT(actionAddText()));
    connect(add_line_action, SIGNAL(triggered()), this, SLOT(actionAddLine()));
    connect(insert_above_action, SIGNAL(triggered()), this, SLOT(actionInsertAbove()));
    connect(insert_under_action, SIGNAL(triggered()), this, SLOT(actionInsertUnder()));
    connect(delete_line_action, SIGNAL(triggered()), this, SLOT(actionDeleteLine()));
    connect(copy_text_action, SIGNAL(triggered()), this, SLOT(actionCopyText()));
    connect(paste_action, SIGNAL(triggered()), this, SLOT(actionPaste()));

    menu->exec(QCursor::pos());
}

void TimelineWidget::slotDroppedAndMoved(TimelineBucket *from, TimelineBucket *to)
{
    int from_index = buckets.indexOf(from);
    int to_index = buckets.indexOf(to);
    timeline_undos->moveCommand(from_index, to_index);
}

void TimelineWidget::slotEditChanged()
{

}

/**
 * 编辑某一个节点
 * @param row 时间行
 * @param col 改行第几项。0为时间,1开始为文字
 */
void TimelineWidget::slotEdit(int row, int col)
{
    if (row < 0 || row >= buckets.size())
        return;
    auto bucket = buckets.at(row);
    bucket->edit(col);
}

/**
 * 准备进行其他操作时,如果正在编辑,则结束编辑
 */
void TimelineWidget::finishEditing()
{
    edit->finishIfEditing();
}

void TimelineWidget::actionAddText()
{
    QList<int> bucket_indexes = selectedIndexes();
    QList<QList<int>> texts_indexes;
    foreach (auto bucket_index, bucket_indexes)
    {
        texts_indexes << QList<int>{buckets.at(bucket_index)->count()};
    }

    timeline_undos->addCommand(bucket_indexes, texts_indexes);

    if (bucket_indexes.size() == 1)
    {
        // 等待动画结束,显示编辑框
        QTimer::singleShot(300, [=]{
            slotEdit(bucket_indexes.first(), buckets.at(bucket_indexes.first())->count());
        });
    }
}

void TimelineWidget::actionAddTextLeft()
{

}

void TimelineWidget::actionAddTextRight()
{

}

void TimelineWidget::actionEditTime()
{
    if (current_index == -1)
        return ;
    auto bucket = at(current_index);
    slotTimeWidgetDoubleClicked(bucket->timeLabel());
}

void TimelineWidget::actionEditText(int index)
{
    if (current_index == -1)
        return ;
    auto bucket = at(current_index);
    if (bucket->count() <= index)
        return ;
    slotTextWidgetDoubleClicked(bucket->at(index));
}

void TimelineWidget::actionAddLine()
{
    timeline_undos->addCommand(count());
    setCurrentItem(count()-1);
    scrollTo();
}

void TimelineWidget::actionInsertAbove()
{
    QList<int> indexes = selectedIndexes();

    timeline_undos->addCommand(indexes);

    unselectAll();
    int cumu = 0;
    for (int i = 0; i < indexes.count(); i++)
    {
        auto bucket = buckets.at(indexes.at(i)+cumu);
        bucket->setSelected(true);
        selected_buckets.append(bucket);
        cumu++;
    }
}

void TimelineWidget::actionInsertUnder()
{
    QList<int> indexes = selectedIndexes(1);

    timeline_undos->addCommand(indexes);

    unselectAll();
    int cumu = 0;
    for (int i = 0; i < indexes.count(); i++)
    {
        auto bucket = buckets.at(indexes.at(i)+cumu);
        bucket->setSelected(true);
        selected_buckets.append(bucket);
        cumu++;
    }
}

void TimelineWidget::actionDeleteLine()
{
    finishEditing();

    QList<int> indexes = selectedIndexes();

    timeline_undos->deleteCommand(indexes);
    selected_buckets.clear();
    current_index = -1;
}

void TimelineWidget::actionCopyText()
{
    QString result;
    foreach (auto bucket, buckets)
    {
        if (bucket->isSelected())
        {
            if (!result.isEmpty())
                result += "\n";
            result += bucket->toString();
        }
    }
    QApplication::clipboard()->setText(result);
}

void TimelineWidget::actionPaste()
{
    QString text = QApplication::clipboard()->text();
    if (text.isEmpty())
        return ;
    int c = this->count();
    fromString(text);
    int c2 = this->count();
    if (c == c2) // 没有变化
        return ;
    unselectAll();
    for (int i = c; i < c2; i++)
        selectItem(at(i));
    scrollTo(c2-1);
    scrollTo(c);
}

源码下载

05-08 00:57