vibefpmgui.window.configs
Build configurations (see vibefpmgui.build_configs): the Configuration
list at the top of Build options (its ⋯ menu opens another configuration
file), and File → Save Build Configuration…. (A project opened with File →
Open Project… or Recent Projects comes with its vibefpm.json: see
vibefpmgui.window.project.)
"New configuration" is the Build options as before: editable, kept in the app's settings. Choosing a saved configuration shows its options, read-only (the fpm executable stays editable: it isn't part of one); choosing New configuration again brings back what was there. Save… adds what's shown to the project's configuration file, as a configuration chosen from then on. Edit makes a saved configuration's options editable, and Save puts them back in the file under its name; leaving it (choosing another, opening another project or file, quitting) with changes not saved asks first.
1"""Build configurations (see `vibefpmgui.build_configs`): the Configuration 2list at the top of Build options (its ⋯ menu opens another configuration 3file), and File → Save Build Configuration…. (A project opened with File → 4Open Project… or Recent Projects comes with its vibefpm.json: see 5vibefpmgui.window.project.) 6 7"New configuration" is the Build options as before: editable, kept in the 8app's settings. Choosing a saved configuration shows its options, read-only 9(the fpm executable stays editable: it isn't part of one); choosing New 10configuration again brings back what was there. Save… adds what's shown to 11the project's configuration file, as a configuration chosen from then on. 12Edit makes a saved configuration's options editable, and Save puts them back 13in the file under its name; leaving it (choosing another, opening another 14project or file, quitting) with changes not saved asks first. 15""" 16 17from __future__ import annotations 18 19from pathlib import Path 20 21from qtpy.QtCore import QSize, Qt 22from qtpy.QtGui import QPalette 23from qtpy.QtWidgets import ( 24 QApplication, 25 QComboBox, 26 QFileDialog, 27 QHBoxLayout, 28 QInputDialog, 29 QLabel, 30 QMenu, 31 QMessageBox, 32 QToolButton, 33) 34 35from .. import build_configs as bc 36from ..icons import make_icon 37from .options import project_key 38 39NEW_CONFIGURATION = "New configuration" 40 41 42class ConfigsMixin: 43 """Build configurations (a part of `vibefpmgui.main_window.MainWindow`).""" 44 45 _free_options: bc.Configuration | None = None # New configuration's options, while a saved one is shown 46 _editing = "" # the saved configuration being edited (its options editable), if any 47 _listed_file: bc.ConfigFile | None = None # the file the list is of 48 49 # (The project's configuration file: config_file, see vibefpmgui.window.options.) 50 @property 51 def configurations(self) -> list[bc.Configuration]: 52 return self.config_file.configurations if self.config_file is not None else [] 53 54 @property 55 def _config_error(self) -> str: 56 """Why the configuration file can't be read ("" if it can).""" 57 return self.config_file.error if self.config_file is not None else "" 58 59 def _build_config_row(self, form): 60 """The Configuration row (the first of Build options) and the 61 read-only note under it.""" 62 self.config_combo = QComboBox() 63 self.config_combo.addItem(NEW_CONFIGURATION) 64 # (As narrow as the panel gets: not as wide as the longest name.) 65 self.config_combo.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) 66 self.config_combo.setMinimumContentsLength(6) 67 self.config_combo.currentIndexChanged.connect(self._config_chosen) 68 self.config_edit_button = QToolButton() 69 self.config_edit_button.setText("Edit") 70 self.config_edit_button.clicked.connect(self._edit_clicked) 71 self.config_save_button = QToolButton() 72 self.config_save_button.setText("Save…") 73 self.config_save_button.clicked.connect(self.save_configuration) 74 more = QToolButton() 75 more.setText("⋯") 76 more.setToolTip("More: save as, edit a copy, delete, open a configuration file") 77 menu = QMenu(more) 78 self.config_save_as_action = menu.addAction("Save As…", self.save_configuration_as) 79 self.config_save_as_action.setToolTip("Save the options shown as a configuration under another name") 80 self.config_edit_action = menu.addAction("Edit as New Configuration", self.edit_configuration_as_new) 81 self.config_edit_action.setToolTip("Copy this configuration's options to New configuration, to change " 82 "them (and Save…, under its name or another)") 83 self.config_delete_action = menu.addAction("Delete Configuration…", self.delete_configuration) 84 menu.addSeparator() 85 menu.addAction("Open Configuration File…", self.browse_configuration) 86 self.config_show_action = menu.addAction("Show Configuration File in Editor", self._show_config_file) 87 menu.setToolTipsVisible(True) 88 # (Opened by hand: a button's own menu adds an arrow beside the icon.) 89 more.clicked.connect(lambda: menu.popup(more.mapToGlobal(more.rect().bottomLeft()))) 90 self._config_more_menu = menu 91 self.config_more_button = more 92 # Icon buttons, as the Debug and Git panels' (the texts kept, for the tooltips' and 93 # screen readers' sake; the icons: see _draw_config_icons). 94 for button in (self.config_edit_button, self.config_save_button, more): 95 button.setToolButtonStyle(Qt.ToolButtonIconOnly) 96 button.setIconSize(QSize(18, 18)) 97 button.setAutoRaise(True) 98 row = QHBoxLayout() 99 row.addWidget(self.config_combo, 1) 100 row.addWidget(self.config_edit_button) 101 row.addWidget(self.config_save_button) 102 row.addWidget(more) 103 form.addRow(row) # (the panel's whole width: with a label, it made the panel wider than its fields) 104 self.config_note = QLabel() 105 self.config_note.setWordWrap(True) 106 self.config_note.hide() 107 form.addRow(self.config_note) 108 self._update_config_ui() 109 110 # ------------------------------------------------------------ options 111 def _option_fields(self) -> list: 112 """The fields a configuration sets (not the fpm executable).""" 113 return [self.profile_combo, self.compiler_edit, self.flag_edit, self.target_edit, self.extra_edit, 114 self.runner_args_edit, self.test_jobs_spin, self.coverage_check, self.verbose_check] 115 116 def _watch_option_fields(self): 117 """(After the fields are made:) a change shows whether there are unsaved ones.""" 118 for widget in self._option_fields(): 119 for signal in ("textChanged", "valueChanged", "toggled", "currentTextChanged"): 120 if hasattr(widget, signal): 121 getattr(widget, signal).connect(self._options_changed) 122 break 123 124 def _options_changed(self, *_): 125 if self._editing: 126 self._update_config_ui() 127 128 def config_shown(self) -> bool: 129 """A saved configuration is shown (read-only, or being edited).""" 130 return self.config_combo.currentIndex() > 0 131 132 def options_locked(self) -> bool: 133 """A saved configuration is shown, read-only (not being edited).""" 134 return self.config_shown() and not self._editing 135 136 def has_unsaved_configuration(self) -> bool: 137 """The configuration being edited has changes not in the file.""" 138 return bool(self._editing) and self.current_options(self._editing) != self._configuration(self._editing) 139 140 def current_options(self, name: str = "") -> bc.Configuration: 141 """The Build options shown, as a configuration named ``name``.""" 142 return bc.Configuration( 143 name, profile=self.profile_combo.currentText() or bc.DEFAULT_PROFILE, 144 features=list(self.selected_features), compiler=self.compiler_edit.text().strip(), 145 flags=self.flag_edit.text().strip(), target=self.target_edit.text().strip(), 146 extra_args=self.extra_edit.text().strip(), program_args=self.runner_args_edit.text().strip(), 147 parallel_tests=self.test_jobs_spin.value(), coverage=self.coverage_check.isChecked(), 148 verbose=self.verbose_check.isChecked()) 149 150 def free_options(self) -> bc.Configuration: 151 """New configuration's options (those to remember), even while a saved one is shown.""" 152 return self._free_options if self._free_options is not None else self.current_options() 153 154 def _show_options(self, config: bc.Configuration): 155 """Fill the fields from ``config``.""" 156 combo = self.profile_combo 157 if combo.findText(config.profile) < 0: # (a profile fpm.toml no longer has: shown as asked for) 158 combo.addItem(config.profile) 159 combo.setCurrentText(config.profile) 160 self.compiler_edit.setText(config.compiler) 161 self.flag_edit.setText(config.flags) 162 self.target_edit.setText(config.target) 163 self.extra_edit.setText(config.extra_args) 164 self.runner_args_edit.setText(config.program_args) 165 self.test_jobs_spin.setValue(config.parallel_tests) 166 self.coverage_check.setChecked(config.coverage) 167 self.verbose_check.setChecked(config.verbose) 168 self.selected_features = list(config.features) 169 self._update_features_ui() 170 171 def _set_options_locked(self, locked: bool): 172 """Greyed out (read-only fields looked the same as the others).""" 173 for widget in self._option_fields(): 174 if widget is not self.profile_combo: # (its state: see _update_features_ui) 175 widget.setEnabled(not locked) 176 self._update_features_ui() 177 178 # ------------------------------------------------------------ choosing 179 def _config_chosen(self, index: int): 180 if self._editing: 181 editing = self._editing 182 if not self.maybe_save_configuration(): 183 self.config_combo.blockSignals(True) # (Cancel: still on it, its changes kept) 184 self.config_combo.setCurrentIndex(self._index_of(editing)) 185 self.config_combo.blockSignals(False) 186 return 187 self._editing = "" 188 if index <= 0: 189 if self._free_options is not None: 190 free, self._free_options = self._free_options, None 191 self._show_options(free) 192 else: 193 config = self._configuration(self.config_combo.itemText(index)) 194 if config is None: 195 return 196 if self._free_options is None: 197 self._free_options = self.current_options() 198 self._show_options(config) 199 self._set_options_locked(index > 0) 200 if self.manifest_path is not None: 201 self.settings.setValue("build_configuration", self.config_combo.currentText() if index > 0 else "") 202 self._update_config_ui() 203 204 def _configuration(self, name: str) -> bc.Configuration | None: 205 return next((c for c in self.configurations if c.name == name), None) 206 207 def _index_of(self, name: str) -> int: 208 """The list's item of the saved configuration named ``name`` (0: New 209 configuration, if there's none; one may be named like it).""" 210 combo = self.config_combo 211 return next((i for i in range(1, combo.count()) if name and combo.itemText(i) == name), 0) 212 213 def select_configuration(self, name: str): 214 """Show the configuration named ``name`` ("": New configuration).""" 215 self.config_combo.setCurrentIndex(self._index_of(name)) 216 217 # ------------------------------------------------------------ the file 218 def _load_configurations(self, reread: bool = True): 219 """The list of the project's configurations (the project was opened 220 or reloaded): its configurations read again (not for a file just 221 opened, or ``reread`` False: just read), the one shown kept (for a 222 file just opened, the one shown last time); one being edited keeps 223 its changes.""" 224 file = self.config_file 225 if file is None or self.manifest_path is None: 226 return 227 first = file is not self._listed_file 228 if reread and not first: 229 file.reread_configurations() # (not its options: they're the window's until written) 230 self._listed_file = file 231 if self._editing and not first: 232 self._refill_items(self._editing) 233 return 234 if first: 235 self._editing = "" # (asked about before the project changed: see maybe_save_configuration) 236 wanted = self.settings.value("build_configuration", "") or "" 237 else: 238 wanted = self.config_combo.currentText() if self.options_locked() else "" 239 if self._config_error: 240 self.statusBar().showMessage(f"Build configurations: {self._config_error}", 8000) 241 self._fill_config_combo(wanted) 242 243 def reload_configurations(self): 244 """The configuration file was saved in the editor: its options, if 245 they were changed there, applied, and its configurations listed again 246 (the one shown kept: with its options as the file has them now, 247 unless it's being edited: its changes are kept).""" 248 if self.config_file is None or self.manifest_path is None: 249 return 250 self.reload_config_file() # (it reads it all) 251 self._load_configurations(reread=False) 252 if self._config_error: 253 return # (the list says why; it's empty until the file is fixed) 254 count = len(self.configurations) 255 self.statusBar().showMessage(f"Build configurations read again from {self.config_path.name}: {count} " 256 f"configuration{'s' * (count != 1)}", 5000) 257 258 def _fill_config_combo(self, wanted: str = ""): 259 """The list again, showing ``wanted`` (New configuration if it's gone): 260 back to New configuration first (its options restored), then to 261 ``wanted`` (its options read again: the file may have changed).""" 262 self.select_configuration("") 263 self._refill_items() 264 if wanted and self._configuration(wanted) is not None: 265 self.select_configuration(wanted) 266 self._update_config_ui() 267 268 def _refill_items(self, shown: str = ""): 269 """The list's names again, ``shown`` chosen (no options changed).""" 270 combo = self.config_combo 271 combo.blockSignals(True) 272 while combo.count() > 1: 273 combo.removeItem(1) 274 names = [config.name for config in self.configurations] 275 combo.addItems(names + ([shown] if shown and shown not in names else [])) # (being edited: kept) 276 combo.setCurrentIndex(self._index_of(shown)) 277 combo.blockSignals(False) 278 self._update_config_ui() 279 280 def _draw_config_icons(self): 281 """The Configuration row's icons in the theme's text color (again when 282 it changes: see window.view): Edit a pencil (Cancel, while editing, an 283 ×), Save a disk, ⋯ three lines.""" 284 color = QApplication.palette().color(QPalette.WindowText) 285 self.config_edit_button.setIcon(make_icon("close" if self._editing else "pencil", color)) 286 self.config_save_button.setIcon(make_icon("save", color)) 287 self.config_more_button.setIcon(make_icon("menu", color)) 288 289 def _update_config_ui(self): 290 locked, editing = self.options_locked(), bool(self._editing) 291 unsaved = self.has_unsaved_configuration() 292 has_project = getattr(self, "manifest_path", None) is not None 293 self.config_edit_button.setText("Cancel" if editing else "Edit") 294 self._draw_config_icons() 295 self.config_edit_button.setEnabled(locked or editing) 296 self.config_edit_button.setToolTip( 297 "Stop editing: the configuration's options as saved" if editing else 298 "Change this configuration's options (then Save them under its name)" if locked else 299 "Choose a saved configuration to edit it") 300 self.config_save_button.setText("Save" if editing else "Save…") 301 self.config_save_button.setEnabled(has_project and (unsaved if editing else not locked)) 302 self.config_save_button.setToolTip( 303 f"Save the changes to “{self._editing}”" if editing else 304 f"Save these options as a configuration in the project's {bc.FILE_NAME} (beside fpm.toml)") 305 self.config_save_as_action.setEnabled(has_project) 306 self.config_edit_action.setEnabled(self.config_shown()) 307 self.config_delete_action.setEnabled(self.config_shown()) 308 self.config_show_action.setEnabled(bool(self.config_path and self.config_path.is_file())) 309 where = self.config_path.name if self.config_path else bc.FILE_NAME 310 if self._config_error: 311 tip = f"⚠ {self._config_error}" 312 if self.options_in_use(): # (read when the project was opened: kept, not written over) 313 tip += "\nThe project's options aren't saved in it until it's fixed." 314 elif self.config_path and self.configurations: 315 tip = f"The configurations in {self.config_path}" 316 else: 317 tip = f"No saved configurations yet: Save… adds the options shown to {where}, beside fpm.toml" 318 self.config_combo.setToolTip(tip) 319 if locked: 320 self.config_note.setText(f"<span style='color:gray'>Read-only, from {where}.</span>") 321 self.config_note.setToolTip("Edit to change its options (then Save), or choose " 322 f"{NEW_CONFIGURATION}.") 323 elif editing: 324 self.config_note.setText(f"<span style='color:#e8590c'>Editing; changes not saved in {where}.</span>" 325 if unsaved else f"<span style='color:gray'>Editing (from {where}).</span>") 326 self.config_note.setToolTip("Save puts the changes in the file; Cancel goes back to it as saved.") 327 self.config_note.setVisible(locked or editing) 328 self.update_window_title() # (the configuration chosen is in it) 329 330 def _remember_config_file(self, manifest: Path, path: Path): 331 """The configuration file the project uses from now on.""" 332 self.settings.setValue(project_key(manifest, "build_config_file"), 333 "" if path == bc.default_path(manifest) else str(path)) 334 335 # ------------------------------------------------------------ commands 336 def save_configuration(self) -> bool: 337 """Build options → Save (File → Save Build Configuration…): the 338 configuration being edited, under its name; otherwise Save As….""" 339 if not self._editing: 340 return self.save_configuration_as() 341 name = self._editing 342 if not self._write_configuration(self.current_options(name)): 343 return False 344 self._editing = "" 345 self._fill_config_combo(name) 346 return True 347 348 def _config_file_for_saving(self) -> bc.ConfigFile | None: 349 """The file to save in (None: it can't be read, so isn't written over).""" 350 file = self.config_file 351 if file is None: 352 return None 353 if file.error and file.exists(): 354 QMessageBox.warning(self, "Save Build Configuration", 355 f"{file.error}\n\nFix the file (or move it away) to save configurations in it.") 356 return None 357 return file 358 359 def _write_configuration(self, config: bc.Configuration) -> bool: 360 file = self._config_file_for_saving() 361 if file is None: 362 return False 363 try: 364 file.save(config) 365 except (OSError, bc.ConfigError) as exc: 366 QMessageBox.warning(self, "Save Build Configuration", f"Couldn't save {file.path}:\n\n{exc}") 367 return False 368 self._remember_config_file(self.manifest_path, file.path) 369 self._config_file_saved() # (a file just made: the project's options go in it from now on) 370 self.statusBar().showMessage(f"Saved build configuration “{config.name}” in {file.path.name}", 5000) 371 return True 372 373 def save_configuration_as(self) -> bool: 374 """Save… / ⋯ → Save As…: what's shown, as a configuration in the 375 project's file under a name asked for, chosen from then on.""" 376 if self.manifest_path is None: 377 return False 378 file = self._config_file_for_saving() 379 if file is None: 380 return False 381 suggestion = self.config_combo.currentText() if self.config_shown() else "" 382 name, ok = QInputDialog.getText(self, "Save Build Configuration", 383 f"Save these Build options in {file.path.name} as:", text=suggestion) 384 name = name.strip() 385 if not ok or not name: 386 return False 387 if name == NEW_CONFIGURATION: 388 QMessageBox.warning(self, "Save Build Configuration", f"“{NEW_CONFIGURATION}” is taken: choose " 389 "another name.") 390 return False 391 if self._configuration(name) is not None and name != self._editing and QMessageBox.question( 392 self, "Save Build Configuration", 393 f"{file.path.name} already has a configuration named “{name}”. Replace it?") != QMessageBox.Yes: 394 return False 395 if not self._write_configuration(self.current_options(name)): 396 return False 397 self._editing = "" # (saved: under another name, the one edited is left as it was in the file) 398 self._fill_config_combo(name) 399 return True 400 401 def _edit_clicked(self): 402 if self._editing: 403 self.cancel_editing() 404 else: 405 self.edit_configuration() 406 407 def edit_configuration(self): 408 """Edit: the saved configuration's options, editable (Save puts them back).""" 409 if not self.options_locked(): 410 return 411 self._editing = self.config_combo.currentText() 412 self._set_options_locked(False) 413 self._update_config_ui() 414 415 def cancel_editing(self): 416 """Cancel: the configuration as saved, read-only again (changes dropped).""" 417 if not self._editing: 418 return 419 name, self._editing = self._editing, "" 420 config = self._configuration(name) 421 if config is None: # (gone from the file while it was edited) 422 self._fill_config_combo() 423 return 424 self._show_options(config) 425 self._set_options_locked(True) 426 self._update_config_ui() 427 428 def maybe_save_configuration(self) -> bool: 429 """Before leaving a configuration being edited with unsaved changes 430 (choosing another, opening another project or file, quitting): Save, 431 Discard (it's shown as saved again: what follows may not happen) or 432 Cancel. False: Cancel (or saving failed): stay.""" 433 if not self.has_unsaved_configuration(): 434 return True 435 answer = QMessageBox.question( 436 self, "Unsaved Build Configuration", 437 f"Save the changes to the build configuration “{self._editing}” in {self.config_path.name}?", 438 QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) 439 if answer == QMessageBox.Cancel: 440 return False 441 if answer == QMessageBox.Save: 442 name = self._editing 443 if not self._write_configuration(self.current_options(name)): 444 return False 445 else: 446 self.cancel_editing() 447 return True 448 449 def edit_configuration_as_new(self): 450 """⋯ → Edit as New Configuration: the configuration's options (as 451 shown, with any changes being made), to change, as New 452 configuration's (they replace what it had).""" 453 if not self.config_shown(): 454 return 455 self._editing = "" # (the changes go to New configuration: nothing to ask) 456 self._free_options = None # (what's shown stays, editable) 457 self.select_configuration("") 458 self._save_features() 459 460 def delete_configuration(self): 461 if not self.config_shown() or self.config_path is None: 462 return 463 name = self.config_combo.currentText() 464 if QMessageBox.question(self, "Delete Build Configuration", 465 f"Delete the configuration “{name}” from {self.config_path.name}?") \ 466 != QMessageBox.Yes: 467 return 468 try: 469 self.config_file.remove(name) 470 except (OSError, bc.ConfigError) as exc: 471 QMessageBox.warning(self, "Delete Build Configuration", f"Couldn't change {self.config_path}:\n\n{exc}") 472 return 473 self._editing = "" 474 self.select_configuration("") 475 self._fill_config_combo() 476 477 def browse_configuration(self): 478 start = str(self.manifest_path.parent) if self.manifest_path else str(Path.home()) 479 filename, _ = QFileDialog.getOpenFileName(self, "Open Build Configuration", start, 480 "VibeFPM configurations (*.json);;All files (*)") 481 if filename: 482 self.open_configuration(filename) 483 484 def open_configuration(self, filename: str | Path): 485 """Open a configuration file: its project (the fpm.toml beside it), 486 if it isn't the one open, and its first configuration.""" 487 path = Path(filename).resolve() 488 manifest = bc.manifest_for(path) 489 if manifest is None: 490 QMessageBox.warning(self, "Open Build Configuration", 491 f"There's no fpm.toml beside {path.name}: a configuration file goes in the " 492 "project's folder, next to its fpm.toml.") 493 return 494 opened = bc.ConfigFile(path).read() 495 if opened.error: 496 QMessageBox.warning(self, "Open Build Configuration", opened.error) 497 return 498 if not self.maybe_save_configuration(): 499 return 500 manifest = manifest.resolve() 501 key = project_key(manifest, "build_config_file") 502 before = self.settings.app.value(key, "") 503 self._remember_config_file(manifest, path) # (the file the project is opened with) 504 if manifest != self.manifest_path: 505 self.open_manifest(manifest) 506 if self.manifest_path != manifest: # (not opened: Cancel, or fpm running) 507 self.settings.app.setValue(key, before) 508 return 509 elif path != self.config_path: 510 self._switch_config_file(manifest, force=True) 511 self._load_configurations() 512 configs = self.configurations 513 self._fill_config_combo(configs[0].name if configs else "") 514 self.statusBar().showMessage(f"{path.name}: {len(configs)} configuration" 515 f"{'s' * (len(configs) != 1)}", 5000) 516 517 def _show_config_file(self): 518 if self.config_path and self.config_path.is_file(): 519 self.open_file(str(self.config_path), force_editor=True)
43class ConfigsMixin: 44 """Build configurations (a part of `vibefpmgui.main_window.MainWindow`).""" 45 46 _free_options: bc.Configuration | None = None # New configuration's options, while a saved one is shown 47 _editing = "" # the saved configuration being edited (its options editable), if any 48 _listed_file: bc.ConfigFile | None = None # the file the list is of 49 50 # (The project's configuration file: config_file, see vibefpmgui.window.options.) 51 @property 52 def configurations(self) -> list[bc.Configuration]: 53 return self.config_file.configurations if self.config_file is not None else [] 54 55 @property 56 def _config_error(self) -> str: 57 """Why the configuration file can't be read ("" if it can).""" 58 return self.config_file.error if self.config_file is not None else "" 59 60 def _build_config_row(self, form): 61 """The Configuration row (the first of Build options) and the 62 read-only note under it.""" 63 self.config_combo = QComboBox() 64 self.config_combo.addItem(NEW_CONFIGURATION) 65 # (As narrow as the panel gets: not as wide as the longest name.) 66 self.config_combo.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) 67 self.config_combo.setMinimumContentsLength(6) 68 self.config_combo.currentIndexChanged.connect(self._config_chosen) 69 self.config_edit_button = QToolButton() 70 self.config_edit_button.setText("Edit") 71 self.config_edit_button.clicked.connect(self._edit_clicked) 72 self.config_save_button = QToolButton() 73 self.config_save_button.setText("Save…") 74 self.config_save_button.clicked.connect(self.save_configuration) 75 more = QToolButton() 76 more.setText("⋯") 77 more.setToolTip("More: save as, edit a copy, delete, open a configuration file") 78 menu = QMenu(more) 79 self.config_save_as_action = menu.addAction("Save As…", self.save_configuration_as) 80 self.config_save_as_action.setToolTip("Save the options shown as a configuration under another name") 81 self.config_edit_action = menu.addAction("Edit as New Configuration", self.edit_configuration_as_new) 82 self.config_edit_action.setToolTip("Copy this configuration's options to New configuration, to change " 83 "them (and Save…, under its name or another)") 84 self.config_delete_action = menu.addAction("Delete Configuration…", self.delete_configuration) 85 menu.addSeparator() 86 menu.addAction("Open Configuration File…", self.browse_configuration) 87 self.config_show_action = menu.addAction("Show Configuration File in Editor", self._show_config_file) 88 menu.setToolTipsVisible(True) 89 # (Opened by hand: a button's own menu adds an arrow beside the icon.) 90 more.clicked.connect(lambda: menu.popup(more.mapToGlobal(more.rect().bottomLeft()))) 91 self._config_more_menu = menu 92 self.config_more_button = more 93 # Icon buttons, as the Debug and Git panels' (the texts kept, for the tooltips' and 94 # screen readers' sake; the icons: see _draw_config_icons). 95 for button in (self.config_edit_button, self.config_save_button, more): 96 button.setToolButtonStyle(Qt.ToolButtonIconOnly) 97 button.setIconSize(QSize(18, 18)) 98 button.setAutoRaise(True) 99 row = QHBoxLayout() 100 row.addWidget(self.config_combo, 1) 101 row.addWidget(self.config_edit_button) 102 row.addWidget(self.config_save_button) 103 row.addWidget(more) 104 form.addRow(row) # (the panel's whole width: with a label, it made the panel wider than its fields) 105 self.config_note = QLabel() 106 self.config_note.setWordWrap(True) 107 self.config_note.hide() 108 form.addRow(self.config_note) 109 self._update_config_ui() 110 111 # ------------------------------------------------------------ options 112 def _option_fields(self) -> list: 113 """The fields a configuration sets (not the fpm executable).""" 114 return [self.profile_combo, self.compiler_edit, self.flag_edit, self.target_edit, self.extra_edit, 115 self.runner_args_edit, self.test_jobs_spin, self.coverage_check, self.verbose_check] 116 117 def _watch_option_fields(self): 118 """(After the fields are made:) a change shows whether there are unsaved ones.""" 119 for widget in self._option_fields(): 120 for signal in ("textChanged", "valueChanged", "toggled", "currentTextChanged"): 121 if hasattr(widget, signal): 122 getattr(widget, signal).connect(self._options_changed) 123 break 124 125 def _options_changed(self, *_): 126 if self._editing: 127 self._update_config_ui() 128 129 def config_shown(self) -> bool: 130 """A saved configuration is shown (read-only, or being edited).""" 131 return self.config_combo.currentIndex() > 0 132 133 def options_locked(self) -> bool: 134 """A saved configuration is shown, read-only (not being edited).""" 135 return self.config_shown() and not self._editing 136 137 def has_unsaved_configuration(self) -> bool: 138 """The configuration being edited has changes not in the file.""" 139 return bool(self._editing) and self.current_options(self._editing) != self._configuration(self._editing) 140 141 def current_options(self, name: str = "") -> bc.Configuration: 142 """The Build options shown, as a configuration named ``name``.""" 143 return bc.Configuration( 144 name, profile=self.profile_combo.currentText() or bc.DEFAULT_PROFILE, 145 features=list(self.selected_features), compiler=self.compiler_edit.text().strip(), 146 flags=self.flag_edit.text().strip(), target=self.target_edit.text().strip(), 147 extra_args=self.extra_edit.text().strip(), program_args=self.runner_args_edit.text().strip(), 148 parallel_tests=self.test_jobs_spin.value(), coverage=self.coverage_check.isChecked(), 149 verbose=self.verbose_check.isChecked()) 150 151 def free_options(self) -> bc.Configuration: 152 """New configuration's options (those to remember), even while a saved one is shown.""" 153 return self._free_options if self._free_options is not None else self.current_options() 154 155 def _show_options(self, config: bc.Configuration): 156 """Fill the fields from ``config``.""" 157 combo = self.profile_combo 158 if combo.findText(config.profile) < 0: # (a profile fpm.toml no longer has: shown as asked for) 159 combo.addItem(config.profile) 160 combo.setCurrentText(config.profile) 161 self.compiler_edit.setText(config.compiler) 162 self.flag_edit.setText(config.flags) 163 self.target_edit.setText(config.target) 164 self.extra_edit.setText(config.extra_args) 165 self.runner_args_edit.setText(config.program_args) 166 self.test_jobs_spin.setValue(config.parallel_tests) 167 self.coverage_check.setChecked(config.coverage) 168 self.verbose_check.setChecked(config.verbose) 169 self.selected_features = list(config.features) 170 self._update_features_ui() 171 172 def _set_options_locked(self, locked: bool): 173 """Greyed out (read-only fields looked the same as the others).""" 174 for widget in self._option_fields(): 175 if widget is not self.profile_combo: # (its state: see _update_features_ui) 176 widget.setEnabled(not locked) 177 self._update_features_ui() 178 179 # ------------------------------------------------------------ choosing 180 def _config_chosen(self, index: int): 181 if self._editing: 182 editing = self._editing 183 if not self.maybe_save_configuration(): 184 self.config_combo.blockSignals(True) # (Cancel: still on it, its changes kept) 185 self.config_combo.setCurrentIndex(self._index_of(editing)) 186 self.config_combo.blockSignals(False) 187 return 188 self._editing = "" 189 if index <= 0: 190 if self._free_options is not None: 191 free, self._free_options = self._free_options, None 192 self._show_options(free) 193 else: 194 config = self._configuration(self.config_combo.itemText(index)) 195 if config is None: 196 return 197 if self._free_options is None: 198 self._free_options = self.current_options() 199 self._show_options(config) 200 self._set_options_locked(index > 0) 201 if self.manifest_path is not None: 202 self.settings.setValue("build_configuration", self.config_combo.currentText() if index > 0 else "") 203 self._update_config_ui() 204 205 def _configuration(self, name: str) -> bc.Configuration | None: 206 return next((c for c in self.configurations if c.name == name), None) 207 208 def _index_of(self, name: str) -> int: 209 """The list's item of the saved configuration named ``name`` (0: New 210 configuration, if there's none; one may be named like it).""" 211 combo = self.config_combo 212 return next((i for i in range(1, combo.count()) if name and combo.itemText(i) == name), 0) 213 214 def select_configuration(self, name: str): 215 """Show the configuration named ``name`` ("": New configuration).""" 216 self.config_combo.setCurrentIndex(self._index_of(name)) 217 218 # ------------------------------------------------------------ the file 219 def _load_configurations(self, reread: bool = True): 220 """The list of the project's configurations (the project was opened 221 or reloaded): its configurations read again (not for a file just 222 opened, or ``reread`` False: just read), the one shown kept (for a 223 file just opened, the one shown last time); one being edited keeps 224 its changes.""" 225 file = self.config_file 226 if file is None or self.manifest_path is None: 227 return 228 first = file is not self._listed_file 229 if reread and not first: 230 file.reread_configurations() # (not its options: they're the window's until written) 231 self._listed_file = file 232 if self._editing and not first: 233 self._refill_items(self._editing) 234 return 235 if first: 236 self._editing = "" # (asked about before the project changed: see maybe_save_configuration) 237 wanted = self.settings.value("build_configuration", "") or "" 238 else: 239 wanted = self.config_combo.currentText() if self.options_locked() else "" 240 if self._config_error: 241 self.statusBar().showMessage(f"Build configurations: {self._config_error}", 8000) 242 self._fill_config_combo(wanted) 243 244 def reload_configurations(self): 245 """The configuration file was saved in the editor: its options, if 246 they were changed there, applied, and its configurations listed again 247 (the one shown kept: with its options as the file has them now, 248 unless it's being edited: its changes are kept).""" 249 if self.config_file is None or self.manifest_path is None: 250 return 251 self.reload_config_file() # (it reads it all) 252 self._load_configurations(reread=False) 253 if self._config_error: 254 return # (the list says why; it's empty until the file is fixed) 255 count = len(self.configurations) 256 self.statusBar().showMessage(f"Build configurations read again from {self.config_path.name}: {count} " 257 f"configuration{'s' * (count != 1)}", 5000) 258 259 def _fill_config_combo(self, wanted: str = ""): 260 """The list again, showing ``wanted`` (New configuration if it's gone): 261 back to New configuration first (its options restored), then to 262 ``wanted`` (its options read again: the file may have changed).""" 263 self.select_configuration("") 264 self._refill_items() 265 if wanted and self._configuration(wanted) is not None: 266 self.select_configuration(wanted) 267 self._update_config_ui() 268 269 def _refill_items(self, shown: str = ""): 270 """The list's names again, ``shown`` chosen (no options changed).""" 271 combo = self.config_combo 272 combo.blockSignals(True) 273 while combo.count() > 1: 274 combo.removeItem(1) 275 names = [config.name for config in self.configurations] 276 combo.addItems(names + ([shown] if shown and shown not in names else [])) # (being edited: kept) 277 combo.setCurrentIndex(self._index_of(shown)) 278 combo.blockSignals(False) 279 self._update_config_ui() 280 281 def _draw_config_icons(self): 282 """The Configuration row's icons in the theme's text color (again when 283 it changes: see window.view): Edit a pencil (Cancel, while editing, an 284 ×), Save a disk, ⋯ three lines.""" 285 color = QApplication.palette().color(QPalette.WindowText) 286 self.config_edit_button.setIcon(make_icon("close" if self._editing else "pencil", color)) 287 self.config_save_button.setIcon(make_icon("save", color)) 288 self.config_more_button.setIcon(make_icon("menu", color)) 289 290 def _update_config_ui(self): 291 locked, editing = self.options_locked(), bool(self._editing) 292 unsaved = self.has_unsaved_configuration() 293 has_project = getattr(self, "manifest_path", None) is not None 294 self.config_edit_button.setText("Cancel" if editing else "Edit") 295 self._draw_config_icons() 296 self.config_edit_button.setEnabled(locked or editing) 297 self.config_edit_button.setToolTip( 298 "Stop editing: the configuration's options as saved" if editing else 299 "Change this configuration's options (then Save them under its name)" if locked else 300 "Choose a saved configuration to edit it") 301 self.config_save_button.setText("Save" if editing else "Save…") 302 self.config_save_button.setEnabled(has_project and (unsaved if editing else not locked)) 303 self.config_save_button.setToolTip( 304 f"Save the changes to “{self._editing}”" if editing else 305 f"Save these options as a configuration in the project's {bc.FILE_NAME} (beside fpm.toml)") 306 self.config_save_as_action.setEnabled(has_project) 307 self.config_edit_action.setEnabled(self.config_shown()) 308 self.config_delete_action.setEnabled(self.config_shown()) 309 self.config_show_action.setEnabled(bool(self.config_path and self.config_path.is_file())) 310 where = self.config_path.name if self.config_path else bc.FILE_NAME 311 if self._config_error: 312 tip = f"⚠ {self._config_error}" 313 if self.options_in_use(): # (read when the project was opened: kept, not written over) 314 tip += "\nThe project's options aren't saved in it until it's fixed." 315 elif self.config_path and self.configurations: 316 tip = f"The configurations in {self.config_path}" 317 else: 318 tip = f"No saved configurations yet: Save… adds the options shown to {where}, beside fpm.toml" 319 self.config_combo.setToolTip(tip) 320 if locked: 321 self.config_note.setText(f"<span style='color:gray'>Read-only, from {where}.</span>") 322 self.config_note.setToolTip("Edit to change its options (then Save), or choose " 323 f"{NEW_CONFIGURATION}.") 324 elif editing: 325 self.config_note.setText(f"<span style='color:#e8590c'>Editing; changes not saved in {where}.</span>" 326 if unsaved else f"<span style='color:gray'>Editing (from {where}).</span>") 327 self.config_note.setToolTip("Save puts the changes in the file; Cancel goes back to it as saved.") 328 self.config_note.setVisible(locked or editing) 329 self.update_window_title() # (the configuration chosen is in it) 330 331 def _remember_config_file(self, manifest: Path, path: Path): 332 """The configuration file the project uses from now on.""" 333 self.settings.setValue(project_key(manifest, "build_config_file"), 334 "" if path == bc.default_path(manifest) else str(path)) 335 336 # ------------------------------------------------------------ commands 337 def save_configuration(self) -> bool: 338 """Build options → Save (File → Save Build Configuration…): the 339 configuration being edited, under its name; otherwise Save As….""" 340 if not self._editing: 341 return self.save_configuration_as() 342 name = self._editing 343 if not self._write_configuration(self.current_options(name)): 344 return False 345 self._editing = "" 346 self._fill_config_combo(name) 347 return True 348 349 def _config_file_for_saving(self) -> bc.ConfigFile | None: 350 """The file to save in (None: it can't be read, so isn't written over).""" 351 file = self.config_file 352 if file is None: 353 return None 354 if file.error and file.exists(): 355 QMessageBox.warning(self, "Save Build Configuration", 356 f"{file.error}\n\nFix the file (or move it away) to save configurations in it.") 357 return None 358 return file 359 360 def _write_configuration(self, config: bc.Configuration) -> bool: 361 file = self._config_file_for_saving() 362 if file is None: 363 return False 364 try: 365 file.save(config) 366 except (OSError, bc.ConfigError) as exc: 367 QMessageBox.warning(self, "Save Build Configuration", f"Couldn't save {file.path}:\n\n{exc}") 368 return False 369 self._remember_config_file(self.manifest_path, file.path) 370 self._config_file_saved() # (a file just made: the project's options go in it from now on) 371 self.statusBar().showMessage(f"Saved build configuration “{config.name}” in {file.path.name}", 5000) 372 return True 373 374 def save_configuration_as(self) -> bool: 375 """Save… / ⋯ → Save As…: what's shown, as a configuration in the 376 project's file under a name asked for, chosen from then on.""" 377 if self.manifest_path is None: 378 return False 379 file = self._config_file_for_saving() 380 if file is None: 381 return False 382 suggestion = self.config_combo.currentText() if self.config_shown() else "" 383 name, ok = QInputDialog.getText(self, "Save Build Configuration", 384 f"Save these Build options in {file.path.name} as:", text=suggestion) 385 name = name.strip() 386 if not ok or not name: 387 return False 388 if name == NEW_CONFIGURATION: 389 QMessageBox.warning(self, "Save Build Configuration", f"“{NEW_CONFIGURATION}” is taken: choose " 390 "another name.") 391 return False 392 if self._configuration(name) is not None and name != self._editing and QMessageBox.question( 393 self, "Save Build Configuration", 394 f"{file.path.name} already has a configuration named “{name}”. Replace it?") != QMessageBox.Yes: 395 return False 396 if not self._write_configuration(self.current_options(name)): 397 return False 398 self._editing = "" # (saved: under another name, the one edited is left as it was in the file) 399 self._fill_config_combo(name) 400 return True 401 402 def _edit_clicked(self): 403 if self._editing: 404 self.cancel_editing() 405 else: 406 self.edit_configuration() 407 408 def edit_configuration(self): 409 """Edit: the saved configuration's options, editable (Save puts them back).""" 410 if not self.options_locked(): 411 return 412 self._editing = self.config_combo.currentText() 413 self._set_options_locked(False) 414 self._update_config_ui() 415 416 def cancel_editing(self): 417 """Cancel: the configuration as saved, read-only again (changes dropped).""" 418 if not self._editing: 419 return 420 name, self._editing = self._editing, "" 421 config = self._configuration(name) 422 if config is None: # (gone from the file while it was edited) 423 self._fill_config_combo() 424 return 425 self._show_options(config) 426 self._set_options_locked(True) 427 self._update_config_ui() 428 429 def maybe_save_configuration(self) -> bool: 430 """Before leaving a configuration being edited with unsaved changes 431 (choosing another, opening another project or file, quitting): Save, 432 Discard (it's shown as saved again: what follows may not happen) or 433 Cancel. False: Cancel (or saving failed): stay.""" 434 if not self.has_unsaved_configuration(): 435 return True 436 answer = QMessageBox.question( 437 self, "Unsaved Build Configuration", 438 f"Save the changes to the build configuration “{self._editing}” in {self.config_path.name}?", 439 QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) 440 if answer == QMessageBox.Cancel: 441 return False 442 if answer == QMessageBox.Save: 443 name = self._editing 444 if not self._write_configuration(self.current_options(name)): 445 return False 446 else: 447 self.cancel_editing() 448 return True 449 450 def edit_configuration_as_new(self): 451 """⋯ → Edit as New Configuration: the configuration's options (as 452 shown, with any changes being made), to change, as New 453 configuration's (they replace what it had).""" 454 if not self.config_shown(): 455 return 456 self._editing = "" # (the changes go to New configuration: nothing to ask) 457 self._free_options = None # (what's shown stays, editable) 458 self.select_configuration("") 459 self._save_features() 460 461 def delete_configuration(self): 462 if not self.config_shown() or self.config_path is None: 463 return 464 name = self.config_combo.currentText() 465 if QMessageBox.question(self, "Delete Build Configuration", 466 f"Delete the configuration “{name}” from {self.config_path.name}?") \ 467 != QMessageBox.Yes: 468 return 469 try: 470 self.config_file.remove(name) 471 except (OSError, bc.ConfigError) as exc: 472 QMessageBox.warning(self, "Delete Build Configuration", f"Couldn't change {self.config_path}:\n\n{exc}") 473 return 474 self._editing = "" 475 self.select_configuration("") 476 self._fill_config_combo() 477 478 def browse_configuration(self): 479 start = str(self.manifest_path.parent) if self.manifest_path else str(Path.home()) 480 filename, _ = QFileDialog.getOpenFileName(self, "Open Build Configuration", start, 481 "VibeFPM configurations (*.json);;All files (*)") 482 if filename: 483 self.open_configuration(filename) 484 485 def open_configuration(self, filename: str | Path): 486 """Open a configuration file: its project (the fpm.toml beside it), 487 if it isn't the one open, and its first configuration.""" 488 path = Path(filename).resolve() 489 manifest = bc.manifest_for(path) 490 if manifest is None: 491 QMessageBox.warning(self, "Open Build Configuration", 492 f"There's no fpm.toml beside {path.name}: a configuration file goes in the " 493 "project's folder, next to its fpm.toml.") 494 return 495 opened = bc.ConfigFile(path).read() 496 if opened.error: 497 QMessageBox.warning(self, "Open Build Configuration", opened.error) 498 return 499 if not self.maybe_save_configuration(): 500 return 501 manifest = manifest.resolve() 502 key = project_key(manifest, "build_config_file") 503 before = self.settings.app.value(key, "") 504 self._remember_config_file(manifest, path) # (the file the project is opened with) 505 if manifest != self.manifest_path: 506 self.open_manifest(manifest) 507 if self.manifest_path != manifest: # (not opened: Cancel, or fpm running) 508 self.settings.app.setValue(key, before) 509 return 510 elif path != self.config_path: 511 self._switch_config_file(manifest, force=True) 512 self._load_configurations() 513 configs = self.configurations 514 self._fill_config_combo(configs[0].name if configs else "") 515 self.statusBar().showMessage(f"{path.name}: {len(configs)} configuration" 516 f"{'s' * (len(configs) != 1)}", 5000) 517 518 def _show_config_file(self): 519 if self.config_path and self.config_path.is_file(): 520 self.open_file(str(self.config_path), force_editor=True)
Build configurations (a part of vibefpmgui.main_window.MainWindow).
129 def config_shown(self) -> bool: 130 """A saved configuration is shown (read-only, or being edited).""" 131 return self.config_combo.currentIndex() > 0
A saved configuration is shown (read-only, or being edited).
133 def options_locked(self) -> bool: 134 """A saved configuration is shown, read-only (not being edited).""" 135 return self.config_shown() and not self._editing
A saved configuration is shown, read-only (not being edited).
137 def has_unsaved_configuration(self) -> bool: 138 """The configuration being edited has changes not in the file.""" 139 return bool(self._editing) and self.current_options(self._editing) != self._configuration(self._editing)
The configuration being edited has changes not in the file.
141 def current_options(self, name: str = "") -> bc.Configuration: 142 """The Build options shown, as a configuration named ``name``.""" 143 return bc.Configuration( 144 name, profile=self.profile_combo.currentText() or bc.DEFAULT_PROFILE, 145 features=list(self.selected_features), compiler=self.compiler_edit.text().strip(), 146 flags=self.flag_edit.text().strip(), target=self.target_edit.text().strip(), 147 extra_args=self.extra_edit.text().strip(), program_args=self.runner_args_edit.text().strip(), 148 parallel_tests=self.test_jobs_spin.value(), coverage=self.coverage_check.isChecked(), 149 verbose=self.verbose_check.isChecked())
The Build options shown, as a configuration named name.
151 def free_options(self) -> bc.Configuration: 152 """New configuration's options (those to remember), even while a saved one is shown.""" 153 return self._free_options if self._free_options is not None else self.current_options()
New configuration's options (those to remember), even while a saved one is shown.
214 def select_configuration(self, name: str): 215 """Show the configuration named ``name`` ("": New configuration).""" 216 self.config_combo.setCurrentIndex(self._index_of(name))
Show the configuration named name ("": New configuration).
244 def reload_configurations(self): 245 """The configuration file was saved in the editor: its options, if 246 they were changed there, applied, and its configurations listed again 247 (the one shown kept: with its options as the file has them now, 248 unless it's being edited: its changes are kept).""" 249 if self.config_file is None or self.manifest_path is None: 250 return 251 self.reload_config_file() # (it reads it all) 252 self._load_configurations(reread=False) 253 if self._config_error: 254 return # (the list says why; it's empty until the file is fixed) 255 count = len(self.configurations) 256 self.statusBar().showMessage(f"Build configurations read again from {self.config_path.name}: {count} " 257 f"configuration{'s' * (count != 1)}", 5000)
The configuration file was saved in the editor: its options, if they were changed there, applied, and its configurations listed again (the one shown kept: with its options as the file has them now, unless it's being edited: its changes are kept).
337 def save_configuration(self) -> bool: 338 """Build options → Save (File → Save Build Configuration…): the 339 configuration being edited, under its name; otherwise Save As….""" 340 if not self._editing: 341 return self.save_configuration_as() 342 name = self._editing 343 if not self._write_configuration(self.current_options(name)): 344 return False 345 self._editing = "" 346 self._fill_config_combo(name) 347 return True
Build options → Save (File → Save Build Configuration…): the configuration being edited, under its name; otherwise Save As….
374 def save_configuration_as(self) -> bool: 375 """Save… / ⋯ → Save As…: what's shown, as a configuration in the 376 project's file under a name asked for, chosen from then on.""" 377 if self.manifest_path is None: 378 return False 379 file = self._config_file_for_saving() 380 if file is None: 381 return False 382 suggestion = self.config_combo.currentText() if self.config_shown() else "" 383 name, ok = QInputDialog.getText(self, "Save Build Configuration", 384 f"Save these Build options in {file.path.name} as:", text=suggestion) 385 name = name.strip() 386 if not ok or not name: 387 return False 388 if name == NEW_CONFIGURATION: 389 QMessageBox.warning(self, "Save Build Configuration", f"“{NEW_CONFIGURATION}” is taken: choose " 390 "another name.") 391 return False 392 if self._configuration(name) is not None and name != self._editing and QMessageBox.question( 393 self, "Save Build Configuration", 394 f"{file.path.name} already has a configuration named “{name}”. Replace it?") != QMessageBox.Yes: 395 return False 396 if not self._write_configuration(self.current_options(name)): 397 return False 398 self._editing = "" # (saved: under another name, the one edited is left as it was in the file) 399 self._fill_config_combo(name) 400 return True
Save… / ⋯ → Save As…: what's shown, as a configuration in the project's file under a name asked for, chosen from then on.
408 def edit_configuration(self): 409 """Edit: the saved configuration's options, editable (Save puts them back).""" 410 if not self.options_locked(): 411 return 412 self._editing = self.config_combo.currentText() 413 self._set_options_locked(False) 414 self._update_config_ui()
Edit: the saved configuration's options, editable (Save puts them back).
416 def cancel_editing(self): 417 """Cancel: the configuration as saved, read-only again (changes dropped).""" 418 if not self._editing: 419 return 420 name, self._editing = self._editing, "" 421 config = self._configuration(name) 422 if config is None: # (gone from the file while it was edited) 423 self._fill_config_combo() 424 return 425 self._show_options(config) 426 self._set_options_locked(True) 427 self._update_config_ui()
Cancel: the configuration as saved, read-only again (changes dropped).
429 def maybe_save_configuration(self) -> bool: 430 """Before leaving a configuration being edited with unsaved changes 431 (choosing another, opening another project or file, quitting): Save, 432 Discard (it's shown as saved again: what follows may not happen) or 433 Cancel. False: Cancel (or saving failed): stay.""" 434 if not self.has_unsaved_configuration(): 435 return True 436 answer = QMessageBox.question( 437 self, "Unsaved Build Configuration", 438 f"Save the changes to the build configuration “{self._editing}” in {self.config_path.name}?", 439 QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) 440 if answer == QMessageBox.Cancel: 441 return False 442 if answer == QMessageBox.Save: 443 name = self._editing 444 if not self._write_configuration(self.current_options(name)): 445 return False 446 else: 447 self.cancel_editing() 448 return True
Before leaving a configuration being edited with unsaved changes (choosing another, opening another project or file, quitting): Save, Discard (it's shown as saved again: what follows may not happen) or Cancel. False: Cancel (or saving failed): stay.
450 def edit_configuration_as_new(self): 451 """⋯ → Edit as New Configuration: the configuration's options (as 452 shown, with any changes being made), to change, as New 453 configuration's (they replace what it had).""" 454 if not self.config_shown(): 455 return 456 self._editing = "" # (the changes go to New configuration: nothing to ask) 457 self._free_options = None # (what's shown stays, editable) 458 self.select_configuration("") 459 self._save_features()
⋯ → Edit as New Configuration: the configuration's options (as shown, with any changes being made), to change, as New configuration's (they replace what it had).
461 def delete_configuration(self): 462 if not self.config_shown() or self.config_path is None: 463 return 464 name = self.config_combo.currentText() 465 if QMessageBox.question(self, "Delete Build Configuration", 466 f"Delete the configuration “{name}” from {self.config_path.name}?") \ 467 != QMessageBox.Yes: 468 return 469 try: 470 self.config_file.remove(name) 471 except (OSError, bc.ConfigError) as exc: 472 QMessageBox.warning(self, "Delete Build Configuration", f"Couldn't change {self.config_path}:\n\n{exc}") 473 return 474 self._editing = "" 475 self.select_configuration("") 476 self._fill_config_combo()
478 def browse_configuration(self): 479 start = str(self.manifest_path.parent) if self.manifest_path else str(Path.home()) 480 filename, _ = QFileDialog.getOpenFileName(self, "Open Build Configuration", start, 481 "VibeFPM configurations (*.json);;All files (*)") 482 if filename: 483 self.open_configuration(filename)
485 def open_configuration(self, filename: str | Path): 486 """Open a configuration file: its project (the fpm.toml beside it), 487 if it isn't the one open, and its first configuration.""" 488 path = Path(filename).resolve() 489 manifest = bc.manifest_for(path) 490 if manifest is None: 491 QMessageBox.warning(self, "Open Build Configuration", 492 f"There's no fpm.toml beside {path.name}: a configuration file goes in the " 493 "project's folder, next to its fpm.toml.") 494 return 495 opened = bc.ConfigFile(path).read() 496 if opened.error: 497 QMessageBox.warning(self, "Open Build Configuration", opened.error) 498 return 499 if not self.maybe_save_configuration(): 500 return 501 manifest = manifest.resolve() 502 key = project_key(manifest, "build_config_file") 503 before = self.settings.app.value(key, "") 504 self._remember_config_file(manifest, path) # (the file the project is opened with) 505 if manifest != self.manifest_path: 506 self.open_manifest(manifest) 507 if self.manifest_path != manifest: # (not opened: Cancel, or fpm running) 508 self.settings.app.setValue(key, before) 509 return 510 elif path != self.config_path: 511 self._switch_config_file(manifest, force=True) 512 self._load_configurations() 513 configs = self.configurations 514 self._fill_config_combo(configs[0].name if configs else "") 515 self.statusBar().showMessage(f"{path.name}: {len(configs)} configuration" 516 f"{'s' * (len(configs) != 1)}", 5000)
Open a configuration file: its project (the fpm.toml beside it), if it isn't the one open, and its first configuration.