Skip to content

Commit 3c63e75

Browse files
author
Mike Nelson
authored
Qtfred event/campaign editor enhancements (scp-fs2open#7613)
* add move to top and move to bottom * enable or disable log states based on selection * add move to top and move to bottom to campaigns
1 parent e182280 commit 3c63e75

12 files changed

Lines changed: 463 additions & 19 deletions

qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1408,7 +1408,45 @@ void CampaignEditorDialogModel::moveBranchDown()
14081408
// Swap the selected branch with the one below it.
14091409
std::swap(mission.branches[m_current_branch_index], mission.branches[m_current_branch_index + 1]);
14101410
set_modified();
1411-
1411+
1412+
m_current_branch_index = -1; // set no branch selected
1413+
// Rebuild the visual tree from the model's authoritative state
1414+
m_tree_ops.rebuildBranchTree(mission.branches, mission.filename);
1415+
}
1416+
1417+
void CampaignEditorDialogModel::moveBranchToTop()
1418+
{
1419+
// Ensure a mission and a branch are currently selected.
1420+
if (!SCP_vector_inbounds(m_missions, m_current_mission_index)) {
1421+
return;
1422+
}
1423+
auto& mission = m_missions[m_current_mission_index];
1424+
if (!SCP_vector_inbounds(mission.branches, m_current_branch_index) || m_current_branch_index == 0) {
1425+
return;
1426+
}
1427+
// Rotate the selected branch to the front, preserving the order of the rest.
1428+
std::rotate(mission.branches.begin(), mission.branches.begin() + m_current_branch_index, mission.branches.begin() + m_current_branch_index + 1);
1429+
set_modified();
1430+
1431+
m_current_branch_index = -1; // set no branch selected
1432+
// Rebuild the visual tree from the model's authoritative state
1433+
m_tree_ops.rebuildBranchTree(mission.branches, mission.filename);
1434+
}
1435+
1436+
void CampaignEditorDialogModel::moveBranchToBottom()
1437+
{
1438+
// Ensure a mission and a branch are currently selected.
1439+
if (!SCP_vector_inbounds(m_missions, m_current_mission_index)) {
1440+
return;
1441+
}
1442+
auto& mission = m_missions[m_current_mission_index];
1443+
if (!SCP_vector_inbounds(mission.branches, m_current_branch_index) || m_current_branch_index == static_cast<int>(mission.branches.size()) - 1) {
1444+
return;
1445+
}
1446+
// Rotate the selected branch to the back, preserving the order of the rest.
1447+
std::rotate(mission.branches.begin() + m_current_branch_index, mission.branches.begin() + m_current_branch_index + 1, mission.branches.end());
1448+
set_modified();
1449+
14121450
m_current_branch_index = -1; // set no branch selected
14131451
// Rebuild the visual tree from the model's authoritative state
14141452
m_tree_ops.rebuildBranchTree(mission.branches, mission.filename);

qtfred/src/mission/dialogs/CampaignEditorDialogModel.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ class CampaignEditorDialogModel : public AbstractDialogModel {
159159
void removeBranch(int mission_index, int branch_index);
160160
void moveBranchUp();
161161
void moveBranchDown();
162+
void moveBranchToTop();
163+
void moveBranchToBottom();
162164
bool getCurrentBranchIsSpecial() const;
163165

164166
void setModified() { set_modified(); }

qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,29 @@ void MissionEventsDialogModel::moveMessageDown()
11251125
set_modified();
11261126
}
11271127

1128+
void MissionEventsDialogModel::moveMessageToTop()
1129+
{
1130+
if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg == 0)
1131+
return;
1132+
1133+
// rotate the selected message to the front, preserving the order of the rest
1134+
std::rotate(m_messages.begin(), m_messages.begin() + m_cur_msg, m_messages.begin() + m_cur_msg + 1);
1135+
setCurrentlySelectedMessage(0);
1136+
set_modified();
1137+
}
1138+
1139+
void MissionEventsDialogModel::moveMessageToBottom()
1140+
{
1141+
const int last = static_cast<int>(m_messages.size()) - 1;
1142+
if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg >= last)
1143+
return;
1144+
1145+
// rotate the selected message to the back, preserving the order of the rest
1146+
std::rotate(m_messages.begin() + m_cur_msg, m_messages.begin() + m_cur_msg + 1, m_messages.end());
1147+
setCurrentlySelectedMessage(last);
1148+
set_modified();
1149+
}
1150+
11281151

11291152
SCP_string MissionEventsDialogModel::getMessageName() const
11301153
{

qtfred/src/mission/dialogs/MissionEventsDialogModel.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class MissionEventsDialogModel : public AbstractDialogModel {
102102
void deleteMessage();
103103
void moveMessageUp();
104104
void moveMessageDown();
105+
void moveMessageToTop();
106+
void moveMessageToBottom();
105107
SCP_string getMessageName() const;
106108
void setMessageName(const SCP_string& name);
107109
SCP_string getMessageText() const;

qtfred/src/ui/Theme.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,115 @@ void bindStandardIcon(QAbstractButton* btn, QStyle::StandardPixmap sp)
454454
qApp->installEventFilter(filter);
455455
}
456456

457+
QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size)
458+
{
459+
QPixmap pm(size, size);
460+
pm.fill(Qt::transparent);
461+
QPainter p(&pm);
462+
p.setRenderHint(QPainter::Antialiasing);
463+
p.setPen(Qt::NoPen);
464+
p.setBrush(color);
465+
466+
const qreal m = size * 0.15;
467+
const QRectF full(m, m, size - 2 * m, size - 2 * m);
468+
469+
const qreal bar = full.width() * 0.16; // thickness of the "end" bar (full is square)
470+
const qreal gap = full.width() * 0.10; // space between the bar and the arrow
471+
472+
// Fill a vertical shaft+head arrow (single polygon, no seam) inside the sub-rect.
473+
auto drawVArrow = [&](const QRectF& r, bool up) {
474+
const qreal shaftW = full.width() * 0.38;
475+
const qreal shaftOff = (r.width() - shaftW) / 2.0;
476+
const qreal headLen = r.height() * 0.55;
477+
const qreal cx = r.center().x();
478+
QPainterPath path;
479+
if (up) {
480+
const qreal headY = r.top() + headLen;
481+
path.moveTo(cx, r.top()); // tip
482+
path.lineTo(r.right(), headY); // head right corner
483+
path.lineTo(r.left() + shaftOff + shaftW, headY); // shoulder right
484+
path.lineTo(r.left() + shaftOff + shaftW, r.bottom()); // shaft bottom right
485+
path.lineTo(r.left() + shaftOff, r.bottom()); // shaft bottom left
486+
path.lineTo(r.left() + shaftOff, headY); // shoulder left
487+
path.lineTo(r.left(), headY); // head left corner
488+
} else {
489+
const qreal headY = r.bottom() - headLen;
490+
path.moveTo(r.left() + shaftOff, r.top()); // shaft top left
491+
path.lineTo(r.left() + shaftOff + shaftW, r.top()); // shaft top right
492+
path.lineTo(r.left() + shaftOff + shaftW, headY); // shoulder right
493+
path.lineTo(r.right(), headY); // head right corner
494+
path.lineTo(cx, r.bottom()); // tip
495+
path.lineTo(r.left(), headY); // head left corner
496+
path.lineTo(r.left() + shaftOff, headY); // shoulder left
497+
}
498+
path.closeSubpath();
499+
p.drawPath(path);
500+
};
501+
502+
// Fill a horizontal shaft+head arrow (single polygon, no seam) inside the sub-rect.
503+
auto drawHArrow = [&](const QRectF& r, bool left) {
504+
const qreal shaftW = full.height() * 0.38;
505+
const qreal shaftOff = (r.height() - shaftW) / 2.0;
506+
const qreal headLen = r.width() * 0.55;
507+
const qreal cy = r.center().y();
508+
QPainterPath path;
509+
if (left) {
510+
const qreal headX = r.left() + headLen;
511+
path.moveTo(r.left(), cy); // tip
512+
path.lineTo(headX, r.top()); // head top corner
513+
path.lineTo(headX, r.top() + shaftOff); // shoulder top
514+
path.lineTo(r.right(), r.top() + shaftOff); // shaft right top
515+
path.lineTo(r.right(), r.top() + shaftOff + shaftW); // shaft right bottom
516+
path.lineTo(headX, r.top() + shaftOff + shaftW); // shoulder bottom
517+
path.lineTo(headX, r.bottom()); // head bottom corner
518+
} else {
519+
const qreal headX = r.right() - headLen;
520+
path.moveTo(r.left(), r.top() + shaftOff); // shaft left top
521+
path.lineTo(headX, r.top() + shaftOff); // shoulder top
522+
path.lineTo(headX, r.top()); // head top corner
523+
path.lineTo(r.right(), cy); // tip
524+
path.lineTo(headX, r.bottom()); // head bottom corner
525+
path.lineTo(headX, r.top() + shaftOff + shaftW); // shoulder bottom
526+
path.lineTo(r.left(), r.top() + shaftOff + shaftW); // shaft left bottom
527+
}
528+
path.closeSubpath();
529+
p.drawPath(path);
530+
};
531+
532+
switch (icon) {
533+
case CustomIcon::MoveToTop:
534+
p.drawRect(QRectF(full.left(), full.top(), full.width(), bar));
535+
drawVArrow(QRectF(full.left(), full.top() + bar + gap, full.width(), full.height() - bar - gap), true);
536+
break;
537+
case CustomIcon::MoveToBottom:
538+
p.drawRect(QRectF(full.left(), full.bottom() - bar, full.width(), bar));
539+
drawVArrow(QRectF(full.left(), full.top(), full.width(), full.height() - bar - gap), false);
540+
break;
541+
case CustomIcon::MoveToLeft:
542+
p.drawRect(QRectF(full.left(), full.top(), bar, full.height()));
543+
drawHArrow(QRectF(full.left() + bar + gap, full.top(), full.width() - bar - gap, full.height()), true);
544+
break;
545+
case CustomIcon::MoveToRight:
546+
p.drawRect(QRectF(full.right() - bar, full.top(), bar, full.height()));
547+
drawHArrow(QRectF(full.left(), full.top(), full.width() - bar - gap, full.height()), false);
548+
break;
549+
}
550+
551+
p.end();
552+
return {pm};
553+
}
554+
555+
void bindCustomIcon(QAbstractButton* btn, CustomIcon icon)
556+
{
557+
auto refresh = [btn, icon]() {
558+
const QColor color = qApp->palette().color(QPalette::ButtonText);
559+
btn->setIcon(makeThemedIcon(icon, color));
560+
};
561+
refresh();
562+
auto* filter = new PaletteChangeFilter(btn, refresh);
563+
qApp->installEventFilter(filter);
564+
}
565+
457566
void bindThemeIcon(QAction* action, const QString& baseName)
458567
{
459568
auto refresh = [action, baseName]() {

qtfred/src/ui/Theme.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ QIcon makeThemedIcon(QStyle::StandardPixmap sp, const QColor& color, int size =
2929
// Bind a palette-aware icon to a button and refresh it when the theme changes.
3030
void bindStandardIcon(QAbstractButton* btn, QStyle::StandardPixmap sp);
3131

32+
// Palette-aware icons drawn by QPainter that have no QStyle::StandardPixmap equivalent.
33+
// The MoveTo* values are "jump to end" arrows: an arrow with a bar across the end it
34+
// points toward (e.g. MoveToTop is an up arrow with a bar along the top).
35+
enum class CustomIcon {
36+
MoveToTop,
37+
MoveToBottom,
38+
MoveToLeft,
39+
MoveToRight,
40+
};
41+
42+
// Draw a palette-aware icon for a CustomIcon using QPainter.
43+
QIcon makeThemedIcon(CustomIcon icon, const QColor& color, int size = 16);
44+
45+
// Bind a palette-aware CustomIcon to a button and refresh it when the theme changes.
46+
void bindCustomIcon(QAbstractButton* btn, CustomIcon icon);
47+
3248
// Bind a theme-adaptive PNG icon to a toolbar action.
3349
// Loads :/images/toolbar/<baseName>-dark.png or <baseName>-light.png based on
3450
// the current palette, and refreshes automatically when the theme changes.

qtfred/src/ui/dialogs/CampaignEditorDialog.cpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,10 @@ void CampaignEditorDialog::initializeUi()
244244
{
245245
util::SignalBlockers blocker(this);
246246

247-
fso::fred::bindStandardIcon(ui->moveBranchUpButton, QStyle::SP_ArrowUp);
248-
fso::fred::bindStandardIcon(ui->moveBranchDownButton, QStyle::SP_ArrowDown);
247+
fso::fred::bindCustomIcon(ui->moveBranchTopButton, CustomIcon::MoveToTop);
248+
fso::fred::bindStandardIcon(ui->moveBranchUpButton, QStyle::SP_ArrowUp);
249+
fso::fred::bindStandardIcon(ui->moveBranchDownButton, QStyle::SP_ArrowDown);
250+
fso::fred::bindCustomIcon(ui->moveBranchBottomButton, CustomIcon::MoveToBottom);
249251
fso::fred::bindStandardIcon(ui->testVoiceButton, QStyle::SP_MediaPlay);
250252

251253
// setup the types combo box
@@ -395,8 +397,12 @@ void CampaignEditorDialog::enableDisableControls()
395397
ui->debriefingPersonaSpinBox->setEnabled(has_mission);
396398

397399
bool branch_selected = (_model->getCurrentBranchSelection() >= 0);
398-
ui->moveBranchUpButton->setEnabled(branch_selected && _model->getCurrentBranchSelection() > 0);
399-
ui->moveBranchDownButton->setEnabled(branch_selected && _model->getCurrentBranchSelection() < _model->getNumBranches() -1);
400+
bool can_move_up = branch_selected && _model->getCurrentBranchSelection() > 0;
401+
bool can_move_down = branch_selected && _model->getCurrentBranchSelection() < _model->getNumBranches() - 1;
402+
ui->moveBranchTopButton->setEnabled(can_move_up);
403+
ui->moveBranchUpButton->setEnabled(can_move_up);
404+
ui->moveBranchDownButton->setEnabled(can_move_down);
405+
ui->moveBranchBottomButton->setEnabled(can_move_down);
400406

401407
bool special_branch_selected = _model->getCurrentBranchIsSpecial();
402408
ui->loopDescriptionPlainTextEdit->setEnabled(special_branch_selected);
@@ -776,6 +782,16 @@ void CampaignEditorDialog::on_substituteMainhallComboBox_currentIndexChanged(con
776782
_model->setCurrentMissionSubstituteMainhall(arg1.toUtf8().constData());
777783
}
778784

785+
void CampaignEditorDialog::on_moveBranchTopButton_clicked()
786+
{
787+
int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it
788+
789+
_model->moveBranchToTop();
790+
ui->graphView->rebuildAll();
791+
792+
ui->graphView->setSelectedMission(mission_selection);
793+
}
794+
779795
void CampaignEditorDialog::on_moveBranchUpButton_clicked()
780796
{
781797
int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it
@@ -796,6 +812,16 @@ void CampaignEditorDialog::on_moveBranchDownButton_clicked()
796812
ui->graphView->setSelectedMission(mission_selection);
797813
}
798814

815+
void CampaignEditorDialog::on_moveBranchBottomButton_clicked()
816+
{
817+
int mission_selection = _model->getCurrentMissionSelection(); // save now because rebuild clears it
818+
819+
_model->moveBranchToBottom();
820+
ui->graphView->rebuildAll();
821+
822+
ui->graphView->setSelectedMission(mission_selection);
823+
}
824+
799825
void CampaignEditorDialog::on_loopDescriptionPlainTextEdit_textChanged()
800826
{
801827
_model->setCurrentBranchLoopDescription(ui->loopDescriptionPlainTextEdit->toPlainText().toUtf8().constData());

qtfred/src/ui/dialogs/CampaignEditorDialog.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ class CampaignEditorDialog : public QMainWindow, public SexpTreeEditorInterface
7474
void on_mainhallComboBox_currentIndexChanged(const QString& arg1);
7575
void on_substituteMainhallComboBox_currentIndexChanged(const QString& arg1);
7676

77+
void on_moveBranchTopButton_clicked();
7778
void on_moveBranchUpButton_clicked();
7879
void on_moveBranchDownButton_clicked();
80+
void on_moveBranchBottomButton_clicked();
7981

8082
void on_loopDescriptionPlainTextEdit_textChanged();
8183
void on_loopAnimLineEdit_textChanged(const QString& arg1);

0 commit comments

Comments
 (0)