Skip to content

Commit 970912f

Browse files
authored
shield system dialog ux clarity (#7473)
1 parent 96a60eb commit 970912f

8 files changed

Lines changed: 157 additions & 116 deletions

File tree

qtfred/help-src/doc/dialogs/ShieldSystemDialog.html

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,28 @@
1010
<h1>Shield System</h1>
1111
<p>Opens via <strong>Editors &rsaquo; Shield System</strong>.</p>
1212
<p>A convenience tool for enabling or disabling shields on ships in bulk, by ship class
13-
or by IFF team. When applied, the dialog directly sets the shield flag on every
14-
existing ship in the mission; it is not a runtime override. If you add new ships
15-
after using this dialog, reopen it and apply again to include them.</p>
13+
or by IFF team. Each side of the dialog is an independent batch operation; pressing
14+
an <strong>Apply</strong> button writes the shield flag on every existing ship of the
15+
chosen class or team and also records the choice as the default for ships of that
16+
class or team placed afterwards. Close the dialog with the window's close button when
17+
you are done &mdash; there is no separate save step.</p>
1618

1719
<h2>By ship class (left panel)</h2>
18-
<p>Select a ship class from the dropdown and choose
19-
<strong>Has shield system</strong> or <strong>No shield system</strong>. Applying
20-
sets the flag on all ships of that class currently in the mission.</p>
20+
<p>Pick a ship class from the dropdown. The <em>Currently:</em> label shows the
21+
current state for that class &mdash; <em>Has shields</em>, <em>No shields</em>, or
22+
<em>Mixed</em> when ships of the class disagree. Choose
23+
<strong>Has shield system</strong> or <strong>No shield system</strong> and click
24+
<strong>Apply to current class</strong> to commit. When the state is <em>Mixed</em>,
25+
neither radio is preselected and the Apply button stays disabled until you pick a
26+
value, so you cannot flatten per-ship customisation by accident.</p>
2127

2228
<h2>By IFF team (right panel)</h2>
23-
<p>Select a team from the dropdown and choose
24-
<strong>Has shield system</strong> or <strong>No shield system</strong>. Applying
25-
sets the flag on all ships belonging to that team currently in the mission.</p>
29+
<p>Works the same way but operates on every ship of the selected team. Use
30+
<strong>Apply to current team</strong> to commit.</p>
2631

32+
<div class="note">Each Apply only touches the axis whose button you pressed. The
33+
other side's selection is not affected, so you can switch back and forth and apply
34+
class- and team-level changes independently in the same session.</div>
2735
<div class="note">When both a ship class setting and a team setting apply to the same
2836
ship, the ship class setting takes precedence.</div>
2937
<div class="note">The Shield System dialog, the <a href="ShipEditorDialog.html">Ship
@@ -32,8 +40,9 @@ <h2>By IFF team (right panel)</h2>
3240
per-ship shield flag. There is no fixed precedence between them; whichever is
3341
applied last takes effect. A typical workflow is to use this dialog to establish a
3442
baseline across the mission, then make individual adjustments per ship in the Ship
35-
Editor. If you later reopen this dialog and a team or class shows as
36-
<em>Mixed</em> (some ships differ), applying without changing that setting will
37-
leave those individual ship values intact.</div>
43+
Editor. Reopening this dialog re-reads the state from the ships themselves, so if
44+
individual ships have been edited since the last bulk apply, the class or team will
45+
show as <em>Mixed</em>; applying without changing the radio choice is not possible
46+
in that state, which protects per-ship edits from being silently overwritten.</div>
3847
</body>
3948
</html>

qtfred/src/mission/Editor.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,13 +1972,6 @@ SCP_vector<SCP_string> Editor::get_docking_list(int model_index) {
19721972
return out;
19731973
}
19741974

1975-
bool Editor::compareShieldSysData(const SCP_vector<GlobalShieldStatus>& teams, const SCP_vector<GlobalShieldStatus>& types) const {
1976-
Assertion(Shield_sys_teams.size() == teams.size(), "Mismatched shield data from global shield dialog!");
1977-
Assertion(Shield_sys_types.size() == types.size(), "Mismatched shield data from global shield dialog!");
1978-
1979-
return (Shield_sys_teams == teams) && (Shield_sys_types == types);
1980-
}
1981-
19821975
void Editor::exportShieldSysData(SCP_vector<GlobalShieldStatus>& teams, SCP_vector<GlobalShieldStatus>& types) const {
19831976
teams = Shield_sys_teams;
19841977
types = Shield_sys_types;

qtfred/src/mission/Editor.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ class Editor : public QObject {
261261

262262
static SCP_vector<SCP_string> get_docking_list(int model_index);
263263

264-
bool compareShieldSysData(const SCP_vector<GlobalShieldStatus>& teams, const SCP_vector<GlobalShieldStatus>& types) const;
265264
void exportShieldSysData(SCP_vector<GlobalShieldStatus>& teams, SCP_vector<GlobalShieldStatus>& types) const;
266265
void importShieldSysData(const SCP_vector<GlobalShieldStatus>& teams, const SCP_vector<GlobalShieldStatus>& types);
267266
void normalizeShieldSysData();

qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,32 @@ void ShieldSystemDialogModel::initializeData() {
1515
_currTeam = 0;
1616
_currType = 0;
1717

18-
_editor->normalizeShieldSysData();
19-
20-
_editor->exportShieldSysData(_teams, _types);
21-
2218
for (const auto& info : Ship_info) {
2319
_shipTypeOptions.emplace_back(info.name);
2420
}
2521

2622
for (const auto& iff : Iff_info) {
2723
_teamOptions.emplace_back(iff.iff_name);
2824
}
25+
26+
refresh();
27+
}
28+
29+
void ShieldSystemDialogModel::refresh() {
30+
_editor->normalizeShieldSysData();
31+
_editor->exportShieldSysData(_teams, _types);
2932
_modified = false;
3033
}
3134

3235
bool ShieldSystemDialogModel::apply() {
33-
if (query_modified()) {
34-
_editor->importShieldSysData(_teams, _types);
35-
}
36+
// Each Apply button commits immediately; nothing to do at dialog close.
3637
return true;
3738
}
39+
3840
void ShieldSystemDialogModel::reject() {
3941
// nothing to do
4042
}
4143

42-
bool ShieldSystemDialogModel::query_modified() const {
43-
return !_editor->compareShieldSysData(_teams, _types);
44-
}
45-
4644
int ShieldSystemDialogModel::getCurrentTeam() const
4745
{
4846
return _currTeam;
@@ -54,12 +52,12 @@ int ShieldSystemDialogModel::getCurrentShipType() const
5452
void ShieldSystemDialogModel::setCurrentTeam(int team)
5553
{
5654
Assertion(SCP_vector_inbounds(Iff_info, team), "Team index %d out of bounds (size: %d)", team, static_cast<int>(Iff_info.size()));
57-
modify(_currTeam, team);
55+
_currTeam = team;
5856
}
5957
void ShieldSystemDialogModel::setCurrentShipType(int type)
6058
{
6159
Assertion(type >= 0 && type < MAX_SHIP_CLASSES, "Ship class index %d is invalid!", type); // NOLINT(readability-simplify-boolean-expr)
62-
modify(_currType, type);
60+
_currType = type;
6361
}
6462

6563
GlobalShieldStatus ShieldSystemDialogModel::getCurrentTeamShieldSys() const
@@ -70,25 +68,23 @@ GlobalShieldStatus ShieldSystemDialogModel::getCurrentTypeShieldSys() const
7068
{
7169
return _types[_currType];
7270
}
73-
void ShieldSystemDialogModel::setCurrentTeamShieldSys(bool value)
74-
{
75-
// UI can only turn shields on or off, so just map to the appropriate enum value
7671

77-
if (value) {
78-
modify(_teams[_currTeam], GlobalShieldStatus::HasShields);
79-
} else {
80-
modify(_teams[_currTeam], GlobalShieldStatus::NoShields);
81-
}
82-
}
83-
void ShieldSystemDialogModel::setCurrentTypeShieldSys(bool value)
72+
void ShieldSystemDialogModel::applyTeam(bool hasShields)
8473
{
85-
// UI can only turn shields on or off, so just map to the appropriate enum value
74+
// Mutate only the currently selected team entry. Other entries already
75+
// reflect current per-ship state (refresh() ran on open / after last
76+
// apply), so importShieldSysData is a no-op for them. Mixed entries
77+
// stay Mixed, Has/No entries already match each ship's flag.
78+
_teams[_currTeam] = hasShields ? GlobalShieldStatus::HasShields : GlobalShieldStatus::NoShields;
79+
_editor->importShieldSysData(_teams, _types);
80+
refresh();
81+
}
8682

87-
if (value) {
88-
modify(_types[_currType], GlobalShieldStatus::HasShields);
89-
} else {
90-
modify(_types[_currType], GlobalShieldStatus::NoShields);
91-
}
83+
void ShieldSystemDialogModel::applyType(bool hasShields)
84+
{
85+
_types[_currType] = hasShields ? GlobalShieldStatus::HasShields : GlobalShieldStatus::NoShields;
86+
_editor->importShieldSysData(_teams, _types);
87+
refresh();
9288
}
9389

9490
const SCP_vector<SCP_string>& ShieldSystemDialogModel::getShipTypeOptions() const

qtfred/src/mission/dialogs/ShieldSystemDialogModel.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class ShieldSystemDialogModel: public AbstractDialogModel {
1212
ShieldSystemDialogModel(QObject* parent, EditorViewport* viewport);
1313
~ShieldSystemDialogModel() override = default;
1414

15+
// AbstractDialogModel requires these, but the dialog no longer uses an
16+
// OK/Cancel working-copy contract. Each Apply commits immediately.
1517
bool apply() override;
1618
void reject() override;
1719

@@ -22,16 +24,16 @@ class ShieldSystemDialogModel: public AbstractDialogModel {
2224

2325
GlobalShieldStatus getCurrentTeamShieldSys() const;
2426
GlobalShieldStatus getCurrentTypeShieldSys() const;
25-
void setCurrentTeamShieldSys(bool value);
26-
void setCurrentTypeShieldSys(bool value);
27+
28+
void applyTeam(bool hasShields);
29+
void applyType(bool hasShields);
2730

2831
const SCP_vector<SCP_string>& getShipTypeOptions() const;
2932
const SCP_vector<SCP_string>& getTeamOptions() const;
3033

31-
bool query_modified() const;
3234
private:
3335
void initializeData();
34-
36+
void refresh();
3537

3638
SCP_vector<SCP_string> _shipTypeOptions;
3739
SCP_vector<SCP_string> _teamOptions;
Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1-
#include <QCloseEvent>
2-
#include <QKeyEvent>
1+
#include <QRadioButton>
32
#include "ShieldSystemDialog.h"
43
#include "ui/util/SignalBlockers.h"
54
#include "ui_ShieldSystemDialog.h"
6-
#include "mission/util.h"
75

86
namespace fso::fred::dialogs {
97

8+
namespace {
9+
10+
QString statusLabelText(GlobalShieldStatus status)
11+
{
12+
switch (status) {
13+
case GlobalShieldStatus::HasShields:
14+
return QObject::tr("Currently: Has shields");
15+
case GlobalShieldStatus::NoShields:
16+
return QObject::tr("Currently: No shields");
17+
case GlobalShieldStatus::MixedShields:
18+
default:
19+
return QObject::tr("Currently: Mixed");
20+
}
21+
}
22+
23+
// Clear the selection in a Qt exclusive button group. Without toggling
24+
// autoExclusive off, neither radio in an exclusive pair can be unchecked
25+
// programmatically.
26+
void clearRadioPair(QRadioButton* a, QRadioButton* b)
27+
{
28+
a->setAutoExclusive(false);
29+
b->setAutoExclusive(false);
30+
a->setChecked(false);
31+
b->setChecked(false);
32+
a->setAutoExclusive(true);
33+
b->setAutoExclusive(true);
34+
}
35+
36+
} // namespace
37+
1038
ShieldSystemDialog::ShieldSystemDialog(FredView* parent, EditorViewport* viewport) :
1139
QDialog(parent),
1240
_viewport(viewport),
@@ -23,32 +51,6 @@ ShieldSystemDialog::ShieldSystemDialog(FredView* parent, EditorViewport* viewpor
2351

2452
ShieldSystemDialog::~ShieldSystemDialog() = default;
2553

26-
void ShieldSystemDialog::accept()
27-
{
28-
// If apply() returns true, close the dialog
29-
if (_model->apply()) {
30-
QDialog::accept();
31-
}
32-
// else: validation failed, don't close
33-
}
34-
35-
void ShieldSystemDialog::reject()
36-
{
37-
// Asks the user if they want to save changes, if any
38-
// If they do, it runs _model->apply() and returns the success value
39-
// If they don't, it runs _model->reject() and returns true
40-
if (rejectOrCloseHandler(this, _model.get(), _viewport)) {
41-
QDialog::reject(); // actually close
42-
}
43-
// else: do nothing, don't close
44-
}
45-
46-
void ShieldSystemDialog::closeEvent(QCloseEvent* e)
47-
{
48-
reject();
49-
e->ignore(); // Don't let the base class close the window
50-
}
51-
5254
void ShieldSystemDialog::initializeUi() {
5355
util::SignalBlockers blockers(this);
5456

@@ -73,58 +75,74 @@ void ShieldSystemDialog::updateUi() {
7375
util::SignalBlockers blockers(this);
7476

7577
auto typeShieldSys = _model->getCurrentTypeShieldSys();
76-
ui->typeHasShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::HasShields);
77-
ui->typeNoShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::NoShields);
78+
ui->typeCurrentStatusLabel->setText(statusLabelText(typeShieldSys));
79+
if (typeShieldSys == GlobalShieldStatus::MixedShields) {
80+
clearRadioPair(ui->typeHasShieldRadio, ui->typeNoShieldRadio);
81+
ui->applyTypeButton->setEnabled(false);
82+
} else {
83+
ui->typeHasShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::HasShields);
84+
ui->typeNoShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::NoShields);
85+
ui->applyTypeButton->setEnabled(true);
86+
}
7887

7988
auto teamShieldSys = _model->getCurrentTeamShieldSys();
80-
ui->teamHasShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::HasShields);
81-
ui->teamNoShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::NoShields);
82-
}
83-
84-
void ShieldSystemDialog::on_okAndCancelButtons_accepted() {
85-
accept();
86-
}
87-
88-
void ShieldSystemDialog::on_okAndCancelButtons_rejected() {
89-
reject();
89+
ui->teamCurrentStatusLabel->setText(statusLabelText(teamShieldSys));
90+
if (teamShieldSys == GlobalShieldStatus::MixedShields) {
91+
clearRadioPair(ui->teamHasShieldRadio, ui->teamNoShieldRadio);
92+
ui->applyTeamButton->setEnabled(false);
93+
} else {
94+
ui->teamHasShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::HasShields);
95+
ui->teamNoShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::NoShields);
96+
ui->applyTeamButton->setEnabled(true);
97+
}
9098
}
9199

92100
void ShieldSystemDialog::on_shipTypeCombo_currentIndexChanged(int index) {
93101
if (index >= 0) {
94-
_model->setCurrentTeam(index);
102+
_model->setCurrentShipType(index);
95103
}
96104
updateUi();
97105
}
98106

99107
void ShieldSystemDialog::on_shipTeamCombo_currentIndexChanged(int index) {
100108
if (index >= 0) {
101-
_model->setCurrentShipType(index);
109+
_model->setCurrentTeam(index);
102110
}
103111
updateUi();
104112
}
105113

106114
void ShieldSystemDialog::on_typeHasShieldRadio_toggled(bool checked) {
107115
if (checked) {
108-
_model->setCurrentTypeShieldSys(checked);
116+
ui->applyTypeButton->setEnabled(true);
109117
}
110118
}
111119

112120
void ShieldSystemDialog::on_typeNoShieldRadio_toggled(bool checked) {
113121
if (checked) {
114-
_model->setCurrentTypeShieldSys(!checked);
122+
ui->applyTypeButton->setEnabled(true);
115123
}
116124
}
117125

118126
void ShieldSystemDialog::on_teamHasShieldRadio_toggled(bool checked) {
119127
if (checked) {
120-
_model->setCurrentTeamShieldSys(checked);
128+
ui->applyTeamButton->setEnabled(true);
121129
}
122130
}
123131

124132
void ShieldSystemDialog::on_teamNoShieldRadio_toggled(bool checked) {
125133
if (checked) {
126-
_model->setCurrentTeamShieldSys(!checked);
134+
ui->applyTeamButton->setEnabled(true);
127135
}
128136
}
129137

138+
void ShieldSystemDialog::on_applyTypeButton_clicked() {
139+
_model->applyType(ui->typeHasShieldRadio->isChecked());
140+
updateUi();
141+
}
142+
143+
void ShieldSystemDialog::on_applyTeamButton_clicked() {
144+
_model->applyTeam(ui->teamHasShieldRadio->isChecked());
145+
updateUi();
146+
}
147+
130148
} // namespace fso::fred::dialogs

qtfred/src/ui/dialogs/ShieldSystemDialog.h

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,7 @@ class ShieldSystemDialog : public QDialog
1919
explicit ShieldSystemDialog(FredView* parent, EditorViewport* viewport);
2020
~ShieldSystemDialog() override;
2121

22-
void accept() override;
23-
void reject() override;
24-
25-
protected:
26-
void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject()
27-
2822
private slots:
29-
void on_okAndCancelButtons_accepted();
30-
void on_okAndCancelButtons_rejected();
31-
3223
void on_shipTypeCombo_currentIndexChanged(int index);
3324
void on_shipTeamCombo_currentIndexChanged(int index);
3425

@@ -37,6 +28,9 @@ private slots:
3728
void on_teamHasShieldRadio_toggled(bool checked);
3829
void on_teamNoShieldRadio_toggled(bool checked);
3930

31+
void on_applyTypeButton_clicked();
32+
void on_applyTeamButton_clicked();
33+
4034
private: // NOLINT(readability-redundant-access-specifiers)
4135
void initializeUi();
4236
void updateUi();

0 commit comments

Comments
 (0)