From 81792d7c4dad42ea9f0164edc69958d5572729c6 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 16 Jul 2026 09:49:02 -0500 Subject: [PATCH 1/3] add move to top and move to bottom --- .../dialogs/MissionEventsDialogModel.cpp | 23 ++++ .../dialogs/MissionEventsDialogModel.h | 2 + qtfred/src/ui/Theme.cpp | 71 ++++++++++ qtfred/src/ui/Theme.h | 13 ++ qtfred/src/ui/dialogs/MissionEventsDialog.cpp | 79 ++++++++++- qtfred/src/ui/dialogs/MissionEventsDialog.h | 4 + qtfred/ui/MissionEventsDialog.ui | 130 ++++++++++++++++-- 7 files changed, 308 insertions(+), 14 deletions(-) diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp index efd4c944533..676c3c91d4a 100644 --- a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -1125,6 +1125,29 @@ void MissionEventsDialogModel::moveMessageDown() set_modified(); } +void MissionEventsDialogModel::moveMessageToTop() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg == 0) + return; + + // rotate the selected message to the front, preserving the order of the rest + std::rotate(m_messages.begin(), m_messages.begin() + m_cur_msg, m_messages.begin() + m_cur_msg + 1); + setCurrentlySelectedMessage(0); + set_modified(); +} + +void MissionEventsDialogModel::moveMessageToBottom() +{ + const int last = static_cast(m_messages.size()) - 1; + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg >= last) + return; + + // rotate the selected message to the back, preserving the order of the rest + std::rotate(m_messages.begin() + m_cur_msg, m_messages.begin() + m_cur_msg + 1, m_messages.end()); + setCurrentlySelectedMessage(last); + set_modified(); +} + SCP_string MissionEventsDialogModel::getMessageName() const { diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.h b/qtfred/src/mission/dialogs/MissionEventsDialogModel.h index 00aa7510a2b..610781f8b7b 100644 --- a/qtfred/src/mission/dialogs/MissionEventsDialogModel.h +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.h @@ -102,6 +102,8 @@ class MissionEventsDialogModel : public AbstractDialogModel { void deleteMessage(); void moveMessageUp(); void moveMessageDown(); + void moveMessageToTop(); + void moveMessageToBottom(); SCP_string getMessageName() const; void setMessageName(const SCP_string& name); SCP_string getMessageText() const; diff --git a/qtfred/src/ui/Theme.cpp b/qtfred/src/ui/Theme.cpp index 459f7ec765f..1c99bb75852 100644 --- a/qtfred/src/ui/Theme.cpp +++ b/qtfred/src/ui/Theme.cpp @@ -454,6 +454,77 @@ void bindStandardIcon(QAbstractButton* btn, QStyle::StandardPixmap sp) qApp->installEventFilter(filter); } +QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size) +{ + QPixmap pm(size, size); + pm.fill(Qt::transparent); + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(color); + + const qreal m = size * 0.15; + const QRectF full(m, m, size - 2 * m, size - 2 * m); + + const qreal barH = full.height() * 0.16; // thickness of the top/bottom "end" bar + const qreal gap = full.height() * 0.10; // space between the bar and the arrow + + // Fill a shaft+head arrow (single polygon, no seam) inside the given sub-rect. + auto drawArrow = [&](const QRectF& r, bool up) { + const qreal shaftW = full.width() * 0.38; + const qreal shaftOff = (r.width() - shaftW) / 2.0; + const qreal headLen = r.height() * 0.55; + const qreal cx = r.center().x(); + QPainterPath path; + if (up) { + const qreal headY = r.top() + headLen; + path.moveTo(cx, r.top()); // tip + path.lineTo(r.right(), headY); // head right corner + path.lineTo(r.left() + shaftOff + shaftW, headY); // shoulder right + path.lineTo(r.left() + shaftOff + shaftW, r.bottom()); // shaft bottom right + path.lineTo(r.left() + shaftOff, r.bottom()); // shaft bottom left + path.lineTo(r.left() + shaftOff, headY); // shoulder left + path.lineTo(r.left(), headY); // head left corner + } else { + const qreal headY = r.bottom() - headLen; + path.moveTo(r.left() + shaftOff, r.top()); // shaft top left + path.lineTo(r.left() + shaftOff + shaftW, r.top()); // shaft top right + path.lineTo(r.left() + shaftOff + shaftW, headY); // shoulder right + path.lineTo(r.right(), headY); // head right corner + path.lineTo(cx, r.bottom()); // tip + path.lineTo(r.left(), headY); // head left corner + path.lineTo(r.left() + shaftOff, headY); // shoulder left + } + path.closeSubpath(); + p.drawPath(path); + }; + + switch (icon) { + case CustomIcon::MoveToTop: + p.drawRect(QRectF(full.left(), full.top(), full.width(), barH)); + drawArrow(QRectF(full.left(), full.top() + barH + gap, full.width(), full.height() - barH - gap), true); + break; + case CustomIcon::MoveToBottom: + p.drawRect(QRectF(full.left(), full.bottom() - barH, full.width(), barH)); + drawArrow(QRectF(full.left(), full.top(), full.width(), full.height() - barH - gap), false); + break; + } + + p.end(); + return {pm}; +} + +void bindCustomIcon(QAbstractButton* btn, CustomIcon icon) +{ + auto refresh = [btn, icon]() { + const QColor color = qApp->palette().color(QPalette::ButtonText); + btn->setIcon(makeThemedIcon(icon, color)); + }; + refresh(); + auto* filter = new PaletteChangeFilter(btn, refresh); + qApp->installEventFilter(filter); +} + void bindThemeIcon(QAction* action, const QString& baseName) { auto refresh = [action, baseName]() { diff --git a/qtfred/src/ui/Theme.h b/qtfred/src/ui/Theme.h index d3b4c6e86de..9aa512be589 100644 --- a/qtfred/src/ui/Theme.h +++ b/qtfred/src/ui/Theme.h @@ -29,6 +29,19 @@ QIcon makeThemedIcon(QStyle::StandardPixmap sp, const QColor& color, int size = // Bind a palette-aware icon to a button and refresh it when the theme changes. void bindStandardIcon(QAbstractButton* btn, QStyle::StandardPixmap sp); +// Palette-aware icons drawn by QPainter that have no QStyle::StandardPixmap equivalent. +// MoveToTop/MoveToBottom are the "jump to end" arrows (an up/down arrow with a bar). +enum class CustomIcon { + MoveToTop, + MoveToBottom, +}; + +// Draw a palette-aware icon for a CustomIcon using QPainter. +QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size = 16); + +// Bind a palette-aware CustomIcon to a button and refresh it when the theme changes. +void bindCustomIcon(QAbstractButton* btn, CustomIcon icon); + // Bind a theme-adaptive PNG icon to a toolbar action. // Loads :/images/toolbar/-dark.png or -light.png based on // the current palette, and refreshes automatically when the theme changes. diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp index ecd56b83099..14f5d1e1014 100644 --- a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp @@ -59,11 +59,15 @@ MissionEventsDialog::MissionEventsDialog(QWidget* parent, EditorViewport* viewpo ui->mainSplitter->setStretchFactor(1, 1); ui->mainSplitter->setSizes({600, 350}); - fso::fred::bindStandardIcon(ui->eventUpBtn, QStyle::SP_ArrowUp); - fso::fred::bindStandardIcon(ui->eventDownBtn, QStyle::SP_ArrowDown); - fso::fred::bindStandardIcon(ui->msgUpBtn, QStyle::SP_ArrowUp); - fso::fred::bindStandardIcon(ui->msgDownBtn, QStyle::SP_ArrowDown); - fso::fred::bindStandardIcon(ui->btnWavePlay, QStyle::SP_MediaPlay); + fso::fred::bindCustomIcon(ui->eventMoveTopBtn, CustomIcon::MoveToTop); + fso::fred::bindStandardIcon(ui->eventUpBtn, QStyle::SP_ArrowUp); + fso::fred::bindStandardIcon(ui->eventDownBtn, QStyle::SP_ArrowDown); + fso::fred::bindCustomIcon(ui->eventMoveBottomBtn, CustomIcon::MoveToBottom); + fso::fred::bindCustomIcon(ui->msgMoveTopBtn, CustomIcon::MoveToTop); + fso::fred::bindStandardIcon(ui->msgUpBtn, QStyle::SP_ArrowUp); + fso::fred::bindStandardIcon(ui->msgDownBtn, QStyle::SP_ArrowDown); + fso::fred::bindCustomIcon(ui->msgMoveBottomBtn, CustomIcon::MoveToBottom); + fso::fred::bindStandardIcon(ui->btnWavePlay, QStyle::SP_MediaPlay); ui->editDirectiveText->setMaxLength(NAME_LENGTH - 1); ui->editDirectiveKeypressText->setMaxLength(NAME_LENGTH - 1); @@ -457,8 +461,10 @@ void MissionEventsDialog::updateEventMoveButtons() canDown = (idx >= 0 && idx < count - 1); } + ui->eventMoveTopBtn->setEnabled(canUp); ui->eventUpBtn->setEnabled(canUp); ui->eventDownBtn->setEnabled(canDown); + ui->eventMoveBottomBtn->setEnabled(canDown); } void MissionEventsDialog::initHeadCombo() { @@ -565,8 +571,10 @@ void MissionEventsDialog::updateMessageMoveButtons() const bool canUp = hasSel && row > 0; const bool canDown = hasSel && row < count - 1; + ui->msgMoveTopBtn->setEnabled(canUp); ui->msgUpBtn->setEnabled(canUp); ui->msgDownBtn->setEnabled(canDown); + ui->msgMoveBottomBtn->setEnabled(canDown); } SCP_vector MissionEventsDialog::read_root_formula_order(sexp_tree_view* tree) @@ -641,6 +649,23 @@ void MissionEventsDialog::on_btnDeleteEvent_clicked() updateEventUi(); } +void MissionEventsDialog::on_eventMoveTopBtn_clicked() +{ + auto* cur = ui->eventTree->currentItem(); + if (!cur || cur->parent()) + return; // roots only + const int idx = ui->eventTree->indexOfTopLevelItem(cur); + if (idx <= 0) + return; // already at top + + QTreeWidgetItem* dest = ui->eventTree->topLevelItem(0); + ui->eventTree->move_root(cur, dest, /*insert_before=*/true); // visual move + modified() + + ui->eventTree->setCurrentItem(cur); + ui->eventTree->scrollToItem(cur); + updateEventMoveButtons(); +} + void MissionEventsDialog::on_eventUpBtn_clicked() { auto* cur = ui->eventTree->currentItem(); @@ -677,6 +702,24 @@ void MissionEventsDialog::on_eventDownBtn_clicked() updateEventMoveButtons(); } +void MissionEventsDialog::on_eventMoveBottomBtn_clicked() +{ + auto* cur = ui->eventTree->currentItem(); + if (!cur || cur->parent()) + return; // roots only + const int idx = ui->eventTree->indexOfTopLevelItem(cur); + const int last = ui->eventTree->topLevelItemCount() - 1; + if (idx < 0 || idx >= last) + return; // already at bottom + + QTreeWidgetItem* dest = ui->eventTree->topLevelItem(last); + ui->eventTree->move_root(cur, dest, /*insert_before=*/false); // visual move + modified() + + ui->eventTree->setCurrentItem(cur); + ui->eventTree->scrollToItem(cur); + updateEventMoveButtons(); +} + void MissionEventsDialog::on_repeatCountBox_valueChanged(int value) { _model->setRepeatCount(value); @@ -848,6 +891,19 @@ void MissionEventsDialog::on_btnDeleteMsg_clicked() updateMessageUi(); } +void MissionEventsDialog::on_msgMoveTopBtn_clicked() +{ + _model->moveMessageToTop(); + rebuildMessageList(); + const int sel = _model->getCurrentlySelectedMessage(); + if (auto* w = ui->messageList) { + w->setCurrentRow(sel); + if (auto* it = w->item(sel)) + w->scrollToItem(it); + } + updateMessageUi(); +} + void MissionEventsDialog::on_msgUpBtn_clicked() { _model->moveMessageUp(); @@ -874,6 +930,19 @@ void MissionEventsDialog::on_msgDownBtn_clicked() updateMessageUi(); } +void MissionEventsDialog::on_msgMoveBottomBtn_clicked() +{ + _model->moveMessageToBottom(); + rebuildMessageList(); + const int sel = _model->getCurrentlySelectedMessage(); + if (auto* w = ui->messageList) { + w->setCurrentRow(sel); + if (auto* it = w->item(sel)) + w->scrollToItem(it); + } + updateMessageUi(); +} + void MissionEventsDialog::on_messageName_textChanged(const QString& text) { SCP_string name = text.toUtf8().constData(); diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.h b/qtfred/src/ui/dialogs/MissionEventsDialog.h index 15cd5561c67..d8e8f77c9ec 100644 --- a/qtfred/src/ui/dialogs/MissionEventsDialog.h +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.h @@ -44,8 +44,10 @@ private slots: void on_btnNewEvent_clicked(); void on_btnInsertEvent_clicked(); void on_btnDeleteEvent_clicked(); + void on_eventMoveTopBtn_clicked(); void on_eventUpBtn_clicked(); void on_eventDownBtn_clicked(); + void on_eventMoveBottomBtn_clicked(); void on_repeatCountBox_valueChanged(int value); void on_triggerCountBox_valueChanged(int value); @@ -74,8 +76,10 @@ private slots: void on_btnNewMsg_clicked(); void on_btnInsertMsg_clicked(); void on_btnDeleteMsg_clicked(); + void on_msgMoveTopBtn_clicked(); void on_msgUpBtn_clicked(); void on_msgDownBtn_clicked(); + void on_msgMoveBottomBtn_clicked(); void on_messageName_textChanged(const QString& text); void on_messageContent_textChanged(); diff --git a/qtfred/ui/MissionEventsDialog.ui b/qtfred/ui/MissionEventsDialog.ui index f12789aba22..deae57c7511 100644 --- a/qtfred/ui/MissionEventsDialog.ui +++ b/qtfred/ui/MissionEventsDialog.ui @@ -223,6 +223,22 @@ + + + + + 0 + 0 + + + + Move to top + + + + + + @@ -231,6 +247,9 @@ 0 + + Move up + @@ -244,6 +263,25 @@ 0 + + Move down + + + + + + + + + + + 0 + 0 + + + + Move to bottom + @@ -596,20 +634,42 @@ in Milliseconds - - + + + + 4 + + + 4 + - - - Name + + + Qt::Horizontal - - messageName + + + 40 + 20 + - + - + + + + 0 + 0 + + + + Move to top + + + + + @@ -619,6 +679,9 @@ in Milliseconds 0 + + Move up + @@ -632,11 +695,60 @@ in Milliseconds 0 + + Move down + + + + + + 0 + 0 + + + + Move to bottom + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Name + + + messageName + + + + + + From 2e96a776cf9a653c02f361eadc0282a23c4198ce Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 16 Jul 2026 09:54:31 -0500 Subject: [PATCH 2/3] enable or disable log states based on selection --- qtfred/src/ui/dialogs/MissionEventsDialog.cpp | 14 ++++++++++++++ qtfred/src/ui/dialogs/MissionEventsDialog.h | 1 + 2 files changed, 15 insertions(+) diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp index 14f5d1e1014..de021ffb6f4 100644 --- a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp @@ -371,6 +371,18 @@ void MissionEventsDialog::rebuildMessageList() { } } +// The log-state checkboxes only apply to a selected event, so gray them out (and +// clear their stale state) when nothing is selected. +void MissionEventsDialog::setEventLogEnabled(bool enable) +{ + for (auto* cb : {ui->checkLogTrue, ui->checkLogFalse, ui->checkLogPrevious, ui->checkLogAlwaysFalse, + ui->checkLogFirstRepeat, ui->checkLogLastRepeat, ui->checkLogFirstTrigger, ui->checkLogLastTrigger}) { + cb->setEnabled(enable); + if (!enable) + cb->setChecked(false); + } +} + void MissionEventsDialog::updateEventUi() { util::SignalBlockers blockers(this); @@ -394,6 +406,7 @@ void MissionEventsDialog::updateEventUi() { ui->teamCombo->setEnabled(false); ui->editDirectiveText->setEnabled(false); ui->editDirectiveKeypressText->setEnabled(false); + setEventLogEnabled(false); return; } @@ -436,6 +449,7 @@ void MissionEventsDialog::updateEventUi() { ui->teamCombo->setEnabled(_model->getMissionIsMultiTeam()); // handle event log flags + setEventLogEnabled(true); ui->checkLogTrue->setChecked(_model->getLogTrue()); ui->checkLogFalse->setChecked(_model->getLogFalse()); ui->checkLogPrevious->setChecked(_model->getLogLogPrevious()); diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.h b/qtfred/src/ui/dialogs/MissionEventsDialog.h index d8e8f77c9ec..f138948d8ee 100644 --- a/qtfred/src/ui/dialogs/MissionEventsDialog.h +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.h @@ -106,6 +106,7 @@ private slots: void updateEventUi(); void updateEventMoveButtons(); + void setEventLogEnabled(bool enable); void updateMessageUi(); void updateMessageMoveButtons(); From 1fcb5bda1b9429f37293cd7d63ad70d350803f99 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 16 Jul 2026 10:10:43 -0500 Subject: [PATCH 3/3] add move to top and move to bottom to campaigns --- .../dialogs/CampaignEditorDialogModel.cpp | 40 +++++++++++++- .../dialogs/CampaignEditorDialogModel.h | 2 + qtfred/src/ui/Theme.cpp | 54 ++++++++++++++++--- qtfred/src/ui/Theme.h | 5 +- .../src/ui/dialogs/CampaignEditorDialog.cpp | 34 ++++++++++-- qtfred/src/ui/dialogs/CampaignEditorDialog.h | 2 + qtfred/ui/CampaignEditorDialog.ui | 26 +++++++++ 7 files changed, 149 insertions(+), 14 deletions(-) diff --git a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp index 5e4b38e7164..49c83dd0882 100644 --- a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp @@ -1408,7 +1408,45 @@ void CampaignEditorDialogModel::moveBranchDown() // Swap the selected branch with the one below it. std::swap(mission.branches[m_current_branch_index], mission.branches[m_current_branch_index + 1]); set_modified(); - + + m_current_branch_index = -1; // set no branch selected + // Rebuild the visual tree from the model's authoritative state + m_tree_ops.rebuildBranchTree(mission.branches, mission.filename); +} + +void CampaignEditorDialogModel::moveBranchToTop() +{ + // Ensure a mission and a branch are currently selected. + if (!SCP_vector_inbounds(m_missions, m_current_mission_index)) { + return; + } + auto& mission = m_missions[m_current_mission_index]; + if (!SCP_vector_inbounds(mission.branches, m_current_branch_index) || m_current_branch_index == 0) { + return; + } + // Rotate the selected branch to the front, preserving the order of the rest. + std::rotate(mission.branches.begin(), mission.branches.begin() + m_current_branch_index, mission.branches.begin() + m_current_branch_index + 1); + set_modified(); + + m_current_branch_index = -1; // set no branch selected + // Rebuild the visual tree from the model's authoritative state + m_tree_ops.rebuildBranchTree(mission.branches, mission.filename); +} + +void CampaignEditorDialogModel::moveBranchToBottom() +{ + // Ensure a mission and a branch are currently selected. + if (!SCP_vector_inbounds(m_missions, m_current_mission_index)) { + return; + } + auto& mission = m_missions[m_current_mission_index]; + if (!SCP_vector_inbounds(mission.branches, m_current_branch_index) || m_current_branch_index == static_cast(mission.branches.size()) - 1) { + return; + } + // Rotate the selected branch to the back, preserving the order of the rest. + std::rotate(mission.branches.begin() + m_current_branch_index, mission.branches.begin() + m_current_branch_index + 1, mission.branches.end()); + set_modified(); + m_current_branch_index = -1; // set no branch selected // Rebuild the visual tree from the model's authoritative state m_tree_ops.rebuildBranchTree(mission.branches, mission.filename); diff --git a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.h b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.h index f0ab4040a4a..c7616547c8e 100644 --- a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.h @@ -159,6 +159,8 @@ class CampaignEditorDialogModel : public AbstractDialogModel { void removeBranch(int mission_index, int branch_index); void moveBranchUp(); void moveBranchDown(); + void moveBranchToTop(); + void moveBranchToBottom(); bool getCurrentBranchIsSpecial() const; void setModified() { set_modified(); } diff --git a/qtfred/src/ui/Theme.cpp b/qtfred/src/ui/Theme.cpp index 1c99bb75852..8dc46e490cb 100644 --- a/qtfred/src/ui/Theme.cpp +++ b/qtfred/src/ui/Theme.cpp @@ -466,11 +466,11 @@ QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size) const qreal m = size * 0.15; const QRectF full(m, m, size - 2 * m, size - 2 * m); - const qreal barH = full.height() * 0.16; // thickness of the top/bottom "end" bar - const qreal gap = full.height() * 0.10; // space between the bar and the arrow + const qreal bar = full.width() * 0.16; // thickness of the "end" bar (full is square) + const qreal gap = full.width() * 0.10; // space between the bar and the arrow - // Fill a shaft+head arrow (single polygon, no seam) inside the given sub-rect. - auto drawArrow = [&](const QRectF& r, bool up) { + // Fill a vertical shaft+head arrow (single polygon, no seam) inside the sub-rect. + auto drawVArrow = [&](const QRectF& r, bool up) { const qreal shaftW = full.width() * 0.38; const qreal shaftOff = (r.width() - shaftW) / 2.0; const qreal headLen = r.height() * 0.55; @@ -499,14 +499,52 @@ QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size) p.drawPath(path); }; + // Fill a horizontal shaft+head arrow (single polygon, no seam) inside the sub-rect. + auto drawHArrow = [&](const QRectF& r, bool left) { + const qreal shaftW = full.height() * 0.38; + const qreal shaftOff = (r.height() - shaftW) / 2.0; + const qreal headLen = r.width() * 0.55; + const qreal cy = r.center().y(); + QPainterPath path; + if (left) { + const qreal headX = r.left() + headLen; + path.moveTo(r.left(), cy); // tip + path.lineTo(headX, r.top()); // head top corner + path.lineTo(headX, r.top() + shaftOff); // shoulder top + path.lineTo(r.right(), r.top() + shaftOff); // shaft right top + path.lineTo(r.right(), r.top() + shaftOff + shaftW); // shaft right bottom + path.lineTo(headX, r.top() + shaftOff + shaftW); // shoulder bottom + path.lineTo(headX, r.bottom()); // head bottom corner + } else { + const qreal headX = r.right() - headLen; + path.moveTo(r.left(), r.top() + shaftOff); // shaft left top + path.lineTo(headX, r.top() + shaftOff); // shoulder top + path.lineTo(headX, r.top()); // head top corner + path.lineTo(r.right(), cy); // tip + path.lineTo(headX, r.bottom()); // head bottom corner + path.lineTo(headX, r.top() + shaftOff + shaftW); // shoulder bottom + path.lineTo(r.left(), r.top() + shaftOff + shaftW); // shaft left bottom + } + path.closeSubpath(); + p.drawPath(path); + }; + switch (icon) { case CustomIcon::MoveToTop: - p.drawRect(QRectF(full.left(), full.top(), full.width(), barH)); - drawArrow(QRectF(full.left(), full.top() + barH + gap, full.width(), full.height() - barH - gap), true); + p.drawRect(QRectF(full.left(), full.top(), full.width(), bar)); + drawVArrow(QRectF(full.left(), full.top() + bar + gap, full.width(), full.height() - bar - gap), true); break; case CustomIcon::MoveToBottom: - p.drawRect(QRectF(full.left(), full.bottom() - barH, full.width(), barH)); - drawArrow(QRectF(full.left(), full.top(), full.width(), full.height() - barH - gap), false); + p.drawRect(QRectF(full.left(), full.bottom() - bar, full.width(), bar)); + drawVArrow(QRectF(full.left(), full.top(), full.width(), full.height() - bar - gap), false); + break; + case CustomIcon::MoveToLeft: + p.drawRect(QRectF(full.left(), full.top(), bar, full.height())); + drawHArrow(QRectF(full.left() + bar + gap, full.top(), full.width() - bar - gap, full.height()), true); + break; + case CustomIcon::MoveToRight: + p.drawRect(QRectF(full.right() - bar, full.top(), bar, full.height())); + drawHArrow(QRectF(full.left(), full.top(), full.width() - bar - gap, full.height()), false); break; } diff --git a/qtfred/src/ui/Theme.h b/qtfred/src/ui/Theme.h index 9aa512be589..52f5f5240bd 100644 --- a/qtfred/src/ui/Theme.h +++ b/qtfred/src/ui/Theme.h @@ -30,10 +30,13 @@ QIcon makeThemedIcon(QStyle::StandardPixmap sp, const QColor& color, int size = void bindStandardIcon(QAbstractButton* btn, QStyle::StandardPixmap sp); // Palette-aware icons drawn by QPainter that have no QStyle::StandardPixmap equivalent. -// MoveToTop/MoveToBottom are the "jump to end" arrows (an up/down arrow with a bar). +// The MoveTo* values are "jump to end" arrows: an arrow with a bar across the end it +// points toward (e.g. MoveToTop is an up arrow with a bar along the top). enum class CustomIcon { MoveToTop, MoveToBottom, + MoveToLeft, + MoveToRight, }; // Draw a palette-aware icon for a CustomIcon using QPainter. diff --git a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp index 34365832c79..b6699447985 100644 --- a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp @@ -244,8 +244,10 @@ void CampaignEditorDialog::initializeUi() { util::SignalBlockers blocker(this); - fso::fred::bindStandardIcon(ui->moveBranchUpButton, QStyle::SP_ArrowUp); - fso::fred::bindStandardIcon(ui->moveBranchDownButton, QStyle::SP_ArrowDown); + fso::fred::bindCustomIcon(ui->moveBranchTopButton, CustomIcon::MoveToTop); + fso::fred::bindStandardIcon(ui->moveBranchUpButton, QStyle::SP_ArrowUp); + fso::fred::bindStandardIcon(ui->moveBranchDownButton, QStyle::SP_ArrowDown); + fso::fred::bindCustomIcon(ui->moveBranchBottomButton, CustomIcon::MoveToBottom); fso::fred::bindStandardIcon(ui->testVoiceButton, QStyle::SP_MediaPlay); // setup the types combo box @@ -395,8 +397,12 @@ void CampaignEditorDialog::enableDisableControls() ui->debriefingPersonaSpinBox->setEnabled(has_mission); bool branch_selected = (_model->getCurrentBranchSelection() >= 0); - ui->moveBranchUpButton->setEnabled(branch_selected && _model->getCurrentBranchSelection() > 0); - ui->moveBranchDownButton->setEnabled(branch_selected && _model->getCurrentBranchSelection() < _model->getNumBranches() -1); + bool can_move_up = branch_selected && _model->getCurrentBranchSelection() > 0; + bool can_move_down = branch_selected && _model->getCurrentBranchSelection() < _model->getNumBranches() - 1; + ui->moveBranchTopButton->setEnabled(can_move_up); + ui->moveBranchUpButton->setEnabled(can_move_up); + ui->moveBranchDownButton->setEnabled(can_move_down); + ui->moveBranchBottomButton->setEnabled(can_move_down); bool special_branch_selected = _model->getCurrentBranchIsSpecial(); ui->loopDescriptionPlainTextEdit->setEnabled(special_branch_selected); @@ -776,6 +782,16 @@ void CampaignEditorDialog::on_substituteMainhallComboBox_currentIndexChanged(con _model->setCurrentMissionSubstituteMainhall(arg1.toUtf8().constData()); } +void CampaignEditorDialog::on_moveBranchTopButton_clicked() +{ + int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it + + _model->moveBranchToTop(); + ui->graphView->rebuildAll(); + + ui->graphView->setSelectedMission(mission_selection); +} + void CampaignEditorDialog::on_moveBranchUpButton_clicked() { int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it @@ -796,6 +812,16 @@ void CampaignEditorDialog::on_moveBranchDownButton_clicked() ui->graphView->setSelectedMission(mission_selection); } +void CampaignEditorDialog::on_moveBranchBottomButton_clicked() +{ + int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it + + _model->moveBranchToBottom(); + ui->graphView->rebuildAll(); + + ui->graphView->setSelectedMission(mission_selection); +} + void CampaignEditorDialog::on_loopDescriptionPlainTextEdit_textChanged() { _model->setCurrentBranchLoopDescription(ui->loopDescriptionPlainTextEdit->toPlainText().toUtf8().constData()); diff --git a/qtfred/src/ui/dialogs/CampaignEditorDialog.h b/qtfred/src/ui/dialogs/CampaignEditorDialog.h index 9cdd4d3ea2e..2990dcd0dde 100644 --- a/qtfred/src/ui/dialogs/CampaignEditorDialog.h +++ b/qtfred/src/ui/dialogs/CampaignEditorDialog.h @@ -74,8 +74,10 @@ class CampaignEditorDialog : public QMainWindow, public SexpTreeEditorInterface void on_mainhallComboBox_currentIndexChanged(const QString& arg1); void on_substituteMainhallComboBox_currentIndexChanged(const QString& arg1); + void on_moveBranchTopButton_clicked(); void on_moveBranchUpButton_clicked(); void on_moveBranchDownButton_clicked(); + void on_moveBranchBottomButton_clicked(); void on_loopDescriptionPlainTextEdit_textChanged(); void on_loopAnimLineEdit_textChanged(const QString& arg1); diff --git a/qtfred/ui/CampaignEditorDialog.ui b/qtfred/ui/CampaignEditorDialog.ui index 24f65768e7a..31e3992cd35 100644 --- a/qtfred/ui/CampaignEditorDialog.ui +++ b/qtfred/ui/CampaignEditorDialog.ui @@ -354,8 +354,21 @@ + + + + Move to top + + + + + + + + Move up + @@ -363,6 +376,19 @@ + + Move down + + + + + + + + + + Move to bottom +