Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(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);
Expand Down
2 changes: 2 additions & 0 deletions qtfred/src/mission/dialogs/CampaignEditorDialogModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }
Expand Down
23 changes: 23 additions & 0 deletions qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(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
{
Expand Down
2 changes: 2 additions & 0 deletions qtfred/src/mission/dialogs/MissionEventsDialogModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
109 changes: 109 additions & 0 deletions qtfred/src/ui/Theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,115 @@ 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 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 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;
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);
};

// 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(), 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() - 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;
}

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]() {
Expand Down
16 changes: 16 additions & 0 deletions qtfred/src/ui/Theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ 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.
// 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.
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/<baseName>-dark.png or <baseName>-light.png based on
// the current palette, and refreshes automatically when the theme changes.
Expand Down
34 changes: 30 additions & 4 deletions qtfred/src/ui/dialogs/CampaignEditorDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions qtfred/src/ui/dialogs/CampaignEditorDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading