Files
leocad/common/lc_timelinewidget.cpp
T
Leonardo Zide 61be531444 Manage piece data ownership with scoped asset references
Replace manual piece-data retain/release bookkeeping with shared copyable
asset-reference handles. Pieces, models, selection, minifigs, thumbnails,
and stored insertion previews retain their dependencies explicitly, while
transient queries continue to borrow pointers.

Separate registered lookup and acquisition from asynchronous load requests
and explicit readiness waits. Keep loader-held references through work and
notification delivery, resetting shared request handles on the UI thread
and guarding callback-triggered library teardown.

Validated with the Qt 6.10 debug build and 26 offscreen lifecycle checks,
including cancellation, stale requests, copy/move operations, model/preview
teardown, undo, project merge/export, and loader shutdown.
2026-10-19 10:02:26 -07:00

626 lines
15 KiB
C++

#include "lc_global.h"
#include "lc_timelinewidget.h"
#include "lc_model.h"
#include "piece.h"
#include "lc_piecedata.h"
#include "lc_mainwindow.h"
lcTimelineWidget::lcTimelineWidget(QWidget* Parent)
: QTreeWidget(Parent)
{
mCurrentStepItem = nullptr;
mIgnoreUpdates = false;
setSelectionMode(QAbstractItemView::ExtendedSelection);
setDragEnabled(true);
setDragDropMode(QAbstractItemView::InternalMove);
setUniformRowHeights(true);
setHeaderHidden(true);
setContextMenuPolicy(Qt::CustomContextMenu);
invisibleRootItem()->setFlags(invisibleRootItem()->flags() & ~Qt::ItemIsDropEnabled);
connect(this, &QTreeWidget::currentItemChanged, this, &lcTimelineWidget::CurrentItemChanged);
connect(this, &QTreeWidget::itemSelectionChanged, this, &lcTimelineWidget::ItemSelectionChanged);
connect(this, &QWidget::customContextMenuRequested, this, &lcTimelineWidget::CustomMenuRequested);
}
lcTimelineWidget::~lcTimelineWidget()
{
}
void lcTimelineWidget::CustomMenuRequested(QPoint Pos)
{
QMenu* Menu = new QMenu(this);
lcObject* FocusObject = gMainWindow->GetActiveModel()->GetFocusObject();
if (FocusObject && FocusObject->IsPiece())
{
lcPiece* Piece = (lcPiece*)FocusObject;
if (Piece->mPieceData->IsModel())
{
Menu->addAction(gMainWindow->mActions[LC_PIECE_EDIT_SELECTED_SUBMODEL]);
Menu->addAction(gMainWindow->mActions[LC_PIECE_VIEW_SELECTED_MODEL]);
Menu->addAction(gMainWindow->mActions[LC_PIECE_INLINE_SELECTED_MODELS]);
Menu->addSeparator();
}
}
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_SET_CURRENT]);
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_INSERT_BEFORE]);
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_INSERT_AFTER]);
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_DELETE]);
Menu->addSeparator();
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_MOVE_SELECTION]);
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_MOVE_SELECTION_BEFORE]);
Menu->addAction(gMainWindow->mActions[LC_TIMELINE_MOVE_SELECTION_AFTER]);
Menu->addSeparator();
Menu->addAction(gMainWindow->mActions[LC_PIECE_HIDE_SELECTED]);
Menu->addAction(gMainWindow->mActions[LC_PIECE_HIDE_UNSELECTED]);
Menu->addAction(gMainWindow->mActions[LC_PIECE_UNHIDE_SELECTED]);
Menu->addAction(gMainWindow->mActions[LC_PIECE_UNHIDE_ALL]);
Menu->exec(viewport()->mapToGlobal(Pos));
delete Menu;
}
void lcTimelineWidget::Update(bool Clear, bool UpdateItems)
{
if (mIgnoreUpdates)
return;
lcModel* Model = gMainWindow->GetActiveModel();
bool Blocked = blockSignals(true);
if (!Model)
{
mCurrentStepItem = nullptr;
mItems.clear();
clear();
blockSignals(Blocked);
return;
}
if (Clear)
{
mCurrentStepItem = nullptr;
mItems.clear();
clear();
}
lcStep LastStep = Model->GetLastStep();
if (Model->HasPieces())
LastStep++;
LastStep = lcMax(LastStep, Model->GetCurrentStep());
for (int TopLevelItemIdx = LastStep; TopLevelItemIdx < topLevelItemCount(); )
{
QTreeWidgetItem* StepItem = topLevelItem(TopLevelItemIdx);
while (StepItem->childCount())
{
QTreeWidgetItem* PieceItem = StepItem->child(0);
lcPiece* Piece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
mItems.remove(Piece);
delete PieceItem;
}
if (mCurrentStepItem == StepItem)
mCurrentStepItem = nullptr;
delete StepItem;
}
for (unsigned int TopLevelItemIdx = topLevelItemCount(); TopLevelItemIdx < LastStep; TopLevelItemIdx++)
{
QTreeWidgetItem* StepItem = new QTreeWidgetItem(this, QStringList(tr("Step %1").arg(TopLevelItemIdx + 1)));
StepItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDropEnabled);
addTopLevelItem(StepItem);
StepItem->setExpanded(true);
}
const std::vector<std::unique_ptr<lcPiece>>& Pieces = Model->GetPieces();
QTreeWidgetItem* StepItem = nullptr;
int PieceItemIndex = 0;
lcStep Step = 0;
for (const std::unique_ptr<lcPiece>& Piece : Pieces)
{
while (Step != Piece->GetStepShow())
{
if (StepItem)
{
while (PieceItemIndex < StepItem->childCount())
{
QTreeWidgetItem* PieceItem = StepItem->child(PieceItemIndex);
lcPiece* RemovePiece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
auto RemoveIt = std::find_if(Pieces.begin(), Pieces.end(), [RemovePiece](const std::unique_ptr<lcPiece>& CheckPiece){ return CheckPiece.get() == RemovePiece; });
if (RemoveIt == Pieces.end())
{
mItems.remove(RemovePiece);
delete PieceItem;
}
else
{
PieceItem->parent()->removeChild(PieceItem);
topLevelItem(RemovePiece->GetStepShow() - 1)->addChild(PieceItem);
}
}
}
Step++;
StepItem = topLevelItem(Step - 1);
PieceItemIndex = 0;
}
QTreeWidgetItem* PieceItem = mItems.value(Piece.get());
bool UpdateItem = UpdateItems;
if (StepItem)
{
if (!PieceItem)
{
PieceItem = new QTreeWidgetItem();
PieceItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
PieceItem->setData(0, Qt::UserRole, QVariant::fromValue<uintptr_t>((uintptr_t)Piece.get()));
StepItem->insertChild(PieceItemIndex, PieceItem);
mItems[Piece.get()] = PieceItem;
UpdateItem = true;
}
else
{
if (PieceItemIndex >= StepItem->childCount() || PieceItem != StepItem->child(PieceItemIndex))
{
QTreeWidgetItem* PieceParent = PieceItem->parent();
if (PieceParent)
PieceParent->removeChild(PieceItem);
StepItem->insertChild(PieceItemIndex, PieceItem);
}
}
}
if (UpdateItem)
{
PieceItem->setText(0, Piece->mPieceData->m_strDescription);
int ColorIndex = Piece->GetColorIndex();
if (!mIcons.contains(ColorIndex))
{
int Size = rowHeight(indexFromItem(PieceItem));
QImage Image(Size, Size, QImage::Format_ARGB32);
Image.fill(0);
const lcVector4& Color = gColorList[ColorIndex].Value;
QPainter Painter(&Image);
Painter.setPen(Qt::darkGray);
Painter.setBrush(QColor::fromRgbF(Color[0], Color[1], Color[2]));
Painter.drawEllipse(0, 0, Size - 1, Size - 1);
mIcons[ColorIndex] = QIcon(QPixmap::fromImage(Image));
}
PieceItem->setIcon(0, mIcons[ColorIndex]);
QColor Color = palette().text().color();
if (Piece->mPieceData->mState == lcAssetState::Failed)
Color = QColor(208, 0, 0);
else if (Piece->IsHidden())
Color.setAlpha(128);
PieceItem->setForeground(0, Color);
}
PieceItem->setSelected(Piece->IsSelected());
PieceItemIndex++;
}
if (Step == 0)
{
Step = 1;
StepItem = topLevelItem(0);
}
while (Step <= LastStep && StepItem)
{
while (PieceItemIndex < StepItem->childCount())
{
QTreeWidgetItem* PieceItem = StepItem->child(PieceItemIndex);
lcPiece* RemovePiece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
mItems.remove(RemovePiece);
delete PieceItem;
}
Step++;
StepItem = topLevelItem(Step - 1);
PieceItemIndex = 0;
}
UpdateCurrentStepItem();
blockSignals(Blocked);
}
void lcTimelineWidget::UpdateCurrentStepItem()
{
lcModel* Model = gMainWindow->GetActiveModel();
lcStep CurrentStep = Model->GetCurrentStep();
QTreeWidgetItem* CurrentStepItem = topLevelItem(CurrentStep - 1);
if (CurrentStepItem != mCurrentStepItem)
{
if (mCurrentStepItem)
{
QFont Font = mCurrentStepItem->font(0);
Font.setBold(false);
mCurrentStepItem->setFont(0, Font);
}
if (CurrentStepItem)
{
QFont Font = CurrentStepItem->font(0);
Font.setBold(true);
CurrentStepItem->setFont(0, Font);
setCurrentItem(CurrentStepItem);
}
mCurrentStepItem = CurrentStepItem;
}
}
void lcTimelineWidget::UpdateSelection()
{
if (mIgnoreUpdates)
return;
QItemSelection ItemSelection;
for (int TopLevelItemIdx = 0; TopLevelItemIdx < topLevelItemCount(); TopLevelItemIdx++)
{
QTreeWidgetItem* StepItem = topLevelItem(TopLevelItemIdx);
for (int PieceItemIdx = 0; PieceItemIdx < StepItem->childCount(); PieceItemIdx++)
{
QTreeWidgetItem* PieceItem = StepItem->child(PieceItemIdx);
lcPiece* Piece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
if (Piece && Piece->IsSelected())
{
QModelIndex Index = indexFromItem(PieceItem);
ItemSelection.select(Index, Index);
}
}
}
bool Blocked = blockSignals(true);
selectionModel()->select(ItemSelection, QItemSelectionModel::ClearAndSelect);
blockSignals(Blocked);
}
void lcTimelineWidget::InsertStepBefore()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
gMainWindow->GetActiveModel()->InsertStepAction(Step + 1);
}
void lcTimelineWidget::InsertStepAfter()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
gMainWindow->GetActiveModel()->InsertStepAction(Step + 2);
}
void lcTimelineWidget::RemoveStep()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
gMainWindow->GetActiveModel()->RemoveStepAction(Step + 1);
}
void lcTimelineWidget::MoveSelection()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
Step++;
const QList<QTreeWidgetItem*> SelectedItems = selectedItems();
for (QTreeWidgetItem* PieceItem : SelectedItems)
{
QTreeWidgetItem* Parent = PieceItem->parent();
if (!Parent)
continue;
int ChildIndex = Parent->indexOfChild(PieceItem);
CurrentItem->addChild(Parent->takeChild(ChildIndex));
}
UpdateModel();
lcModel* Model = gMainWindow->GetActiveModel();
if (Step > static_cast<int>(Model->GetCurrentStep()))
Model->SetCurrentStep(Step);
}
void lcTimelineWidget::MoveSelectionBefore()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
Step++;
lcModel* Model = gMainWindow->GetActiveModel();
Model->InsertStepAndMoveSelectedPieces(Step);
if (Step > static_cast<int>(Model->GetCurrentStep()))
Model->SetCurrentStep(Step);
}
void lcTimelineWidget::MoveSelectionAfter()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
Step += 2;
lcModel* Model = gMainWindow->GetActiveModel();
Model->InsertStepAndMoveSelectedPieces(Step);
if (Step > static_cast<int>(Model->GetCurrentStep()))
Model->SetCurrentStep(Step);
}
void lcTimelineWidget::SetCurrentStep()
{
QTreeWidgetItem* CurrentItem = currentItem();
if (!CurrentItem)
return;
if (CurrentItem->parent())
CurrentItem = CurrentItem->parent();
int Step = indexOfTopLevelItem(CurrentItem);
if (Step == -1)
return;
gMainWindow->GetActiveModel()->SetCurrentStep(Step + 1);
}
void lcTimelineWidget::CurrentItemChanged(QTreeWidgetItem* Current, QTreeWidgetItem* Previous)
{
Q_UNUSED(Previous);
if (Current && !Current->parent())
SetCurrentStep();
}
void lcTimelineWidget::ItemSelectionChanged()
{
std::vector<lcObject*> Selection;
lcStep LastStep = 1;
const QList<QTreeWidgetItem*> SelectedItems = selectedItems();
for (QTreeWidgetItem* PieceItem : SelectedItems)
{
lcPiece* Piece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
if (Piece)
{
LastStep = lcMax(LastStep, Piece->GetStepShow());
Selection.emplace_back(Piece);
}
}
lcPiece* CurrentPiece = nullptr;
QTreeWidgetItem* CurrentItem = currentItem();
if (CurrentItem && CurrentItem->isSelected())
CurrentPiece = (lcPiece*)CurrentItem->data(0, Qt::UserRole).value<uintptr_t>();
bool Blocked = blockSignals(true);
mIgnoreUpdates = true;
lcModel* Model = gMainWindow->GetActiveModel();
if (LastStep > Model->GetCurrentStep())
{
Model->SetCurrentStep(LastStep);
UpdateCurrentStepItem();
}
Model->SetSelectionAndFocusAction(Selection, CurrentPiece, LC_PIECE_SECTION_POSITION, lcSelectionMode::Single);
mIgnoreUpdates = false;
blockSignals(Blocked);
UpdateSelection();
}
void lcTimelineWidget::dropEvent(QDropEvent* Event)
{
QTreeWidgetItem* DropItem = itemAt(Event->pos());
if (!DropItem)
return;
QList<QTreeWidgetItem*> SelectedItems = selectedItems();
clearSelection();
QTreeWidgetItem* ParentItem = DropItem->parent();
lcStep Step = indexOfTopLevelItem(ParentItem ? ParentItem : DropItem) + 1;
lcModel* Model = gMainWindow->GetActiveModel();
if (Step > Model->GetCurrentStep())
Model->SetCurrentStep(Step);
auto SortItems = [this](QTreeWidgetItem* Item1, QTreeWidgetItem* Item2)
{
QTreeWidgetItem* StepItem1 = Item1->parent();
QTreeWidgetItem* StepItem2 = Item2->parent();
if (StepItem1 == StepItem2)
return StepItem1->indexOfChild(Item1) < StepItem1->indexOfChild(Item2);
return indexOfTopLevelItem(StepItem1) < indexOfTopLevelItem(StepItem2);
};
std::sort(SelectedItems.begin(), SelectedItems.end(), SortItems);
for (QTreeWidgetItem* SelectedItem : std::as_const(SelectedItems))
SelectedItem->setSelected(true);
QTreeWidget::dropEvent(Event);
UpdateModel();
Update(false, false);
}
void lcTimelineWidget::mousePressEvent(QMouseEvent* Event)
{
if (Event->button() == Qt::RightButton)
{
QItemSelection Selection = selectionModel()->selection();
bool Blocked = blockSignals(true);
QTreeWidget::mousePressEvent(Event);
blockSignals(Blocked);
selectionModel()->select(Selection, QItemSelectionModel::ClearAndSelect);
}
else
QTreeWidget::mousePressEvent(Event);
}
void lcTimelineWidget::mouseDoubleClickEvent(QMouseEvent* MouseEvent)
{
if (MouseEvent->button() == Qt::LeftButton)
{
QTreeWidgetItem* CurrentItem = currentItem();
PreviewSelection(CurrentItem);
}
QTreeWidget::mouseDoubleClickEvent(MouseEvent);
}
void lcTimelineWidget::PreviewSelection(QTreeWidgetItem* CurrentItem)
{
if (!CurrentItem)
return;
lcPiece* Piece = (lcPiece*)CurrentItem->data(0, Qt::UserRole).value<uintptr_t>();
if (!Piece)
return;
lcPieceData* PieceData = Piece->mPieceData.Get();
if (!PieceData)
return;
gMainWindow->PreviewPiece(PieceData, Piece->GetColorCode(), false);
}
void lcTimelineWidget::UpdateModel()
{
std::vector<std::pair<lcPiece*, lcStep>> PieceSteps;
for (int TopLevelItemIdx = 0; TopLevelItemIdx < topLevelItemCount(); TopLevelItemIdx++)
{
QTreeWidgetItem* StepItem = topLevelItem(TopLevelItemIdx);
for (int PieceItemIdx = 0; PieceItemIdx < StepItem->childCount(); PieceItemIdx++)
{
QTreeWidgetItem* PieceItem = StepItem->child(PieceItemIdx);
lcPiece* Piece = (lcPiece*)PieceItem->data(0, Qt::UserRole).value<uintptr_t>();
PieceSteps.emplace_back(std::pair<lcPiece*, lcStep>(Piece, TopLevelItemIdx + 1));
}
}
mIgnoreUpdates = true;
gMainWindow->GetActiveModel()->SetPieceSteps(PieceSteps);
mIgnoreUpdates = false;
}