src/mainwindow.cpp

changeset 201
5d201ee4a9c3
parent 200
ca23936b455b
child 354
91053052bb28
equal deleted inserted replaced
200:ca23936b455b 201:5d201ee4a9c3
13 * GNU General Public License for more details. 13 * GNU General Public License for more details.
14 * 14 *
15 * You should have received a copy of the GNU General Public License 15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */ 17 */
18
19 #include <QLabel>
20 #include <QVBoxLayout>
21 #include <QCloseEvent>
22 #include <QFileDialog>
23 #include <QMessageBox>
24 #include "mainwindow.h"
25 #include "ui_mainwindow.h"
26 #include "settingseditor/settingseditor.h"
27 #include "version.h"
28 #include "document.h"
29 #include "uiutilities.h"
30 #include "widgets/colorselectdialog.h"
31
32 template<typename BaseType, typename MemberType, typename DataType>
33 struct MemberData
34 {
35 std::size_t member;
36 DataType payload;
37 constexpr MemberType memberInstance(BaseType* instance) const
38 {
39 return *reinterpret_cast<MemberType*>(reinterpret_cast<char*>(instance) + this->member);
40 }
41 };
42
43 static constexpr MemberData<Ui_MainWindow, QAction*, gl::RenderStyle> renderStyleButtons[] = {
44 { offsetof(Ui_MainWindow, actionRenderStyleNormal), gl::RenderStyle::Normal },
45 { offsetof(Ui_MainWindow, actionRenderStyleBfc), gl::RenderStyle::BfcRedGreen },
46 { offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors },
47 { offsetof(Ui_MainWindow, actionRenderStylePickScene), gl::RenderStyle::PickScene },
48 };
49
50 class A : public QSettings
51 {
52 using QSettings::QSettings;
53 };
54
55 MainWindow::MainWindow(QWidget *parent) :
56 QMainWindow{parent},
57 ui{std::make_unique<Ui_MainWindow>()},
58 documents{this},
59 settings{},
60 libraries{this}
61 {
62 this->ui->setupUi(this);
63 defaultKeyboardShortcuts = uiutilities::makeKeySequenceMap(uiutilities::collectActions(this));
64 connect(ui->actionNew, &QAction::triggered, this, &MainWindow::newModel);
65 connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::openModel);
66 connect(ui->actionQuit, &QAction::triggered, this, &QMainWindow::close);
67 connect(ui->actionSettingsEditor, &QAction::triggered, this, &MainWindow::runSettingsEditor);
68 connect(ui->actionAdjustGridToView, &QAction::triggered, [&]()
69 {
70 if (this->currentDocument() != nullptr)
71 {
72 adjustGridToView(this->currentDocument()->canvas);
73 }
74 });
75 connect(this->ui->actionSave, &QAction::triggered,
76 this, &MainWindow::actionSave);
77 connect(this->ui->actionSaveAs, &QAction::triggered,
78 this, &MainWindow::actionSaveAs);
79 connect(this->ui->actionClose, &QAction::triggered, this, &MainWindow::actionClose);
80 connect(this->ui->actionDelete, &QAction::triggered, this, &MainWindow::actionDelete);
81 connect(this->ui->actionInvert, &QAction::triggered, this, &MainWindow::actionInvert);
82 connect(this->ui->tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::handleTabCloseButton);
83 for (auto data : ::renderStyleButtons)
84 {
85 QAction* action = data.memberInstance(this->ui.get());
86 connect(action, &QAction::triggered, [this, data]()
87 {
88 this->setRenderStyle(data.payload);
89 });
90 }
91 connect(this->ui->actionDrawAxes, &QAction::triggered, this, &MainWindow::setDrawAxes);
92 this->updateTitle();
93 this->restoreStartupSettings();
94 this->restoreSettings();
95 this->updateRenderPreferences();
96 this->newModel();
97 }
98
99 // MainWindow needs a destructor even if it is empty because otherwise the destructor of the
100 // std::unique_ptr is resolved in the header file, where it will complain about Ui_MainWindow
101 // being incomplete.
102 MainWindow::~MainWindow()
103 {
104 }
105
106 void MainWindow::newModel()
107 {
108 this->openModelForEditing(documents.newModel());
109 }
110
111 void MainWindow::openModel()
112 {
113 const QString path = QFileDialog::getOpenFileName(
114 this,
115 tr("Open model"),
116 "",
117 tr("LDraw models (*.ldr *.dat)"));
118 if (not path.isEmpty())
119 {
120 this->openModelFromPath(path);
121 }
122 }
123
124 void MainWindow::openModelFromPath(const QString& path)
125 {
126 QString errorString;
127 QTextStream errorStream{&errorString};
128 std::optional<ModelId> modelIdOpt = this->documents.openModel(
129 path,
130 errorStream,
131 DocumentManager::OpenType::ManuallyOpened);
132 if (modelIdOpt.has_value())
133 {
134 const ModelId modelId = modelIdOpt.value();
135 this->documents.loadDependenciesForModel(modelId, path, this->libraries, errorStream);
136 if (not errorString.isEmpty())
137 {
138 QMessageBox::warning(
139 this,
140 tr("Problem loading references"),
141 errorString);
142 }
143 this->openModelForEditing(modelId);
144 this->addRecentlyOpenedFile(path);
145 }
146 else
147 {
148 QMessageBox::critical(
149 this,
150 tr("Problem opening file"),
151 utility::format(
152 tr("Could not open %1: %2"),
153 path,
154 errorString));
155 }
156 }
157
158 /**
159 * @brief Changes the application language to the specified language
160 * @param localeCode Code of the locale to translate to
161 */
162 void MainWindow::changeLanguage(QString localeCode)
163 {
164 if (not localeCode.isEmpty() and localeCode != this->currentLanguage)
165 {
166 this->currentLanguage = localeCode;
167 if (localeCode == "system")
168 {
169 localeCode = QLocale::system().name();
170 }
171 QLocale::setDefault({localeCode});
172 qApp->removeTranslator(&this->translator);
173 const bool loadSuccessful = this->translator.load(pathToTranslation(localeCode));
174 if (loadSuccessful)
175 {
176 qApp->installTranslator(&this->translator);
177 }
178 }
179 }
180
181 void MainWindow::addRecentlyOpenedFile(const QString& path)
182 {
183 this->recentlyOpenedFiles.removeAll(path);
184 this->recentlyOpenedFiles.insert(0, path);
185 while (this->recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
186 {
187 this->recentlyOpenedFiles.removeLast();
188 }
189 this->saveSettings();
190 this->updateRecentlyOpenedDocumentsMenu();
191 }
192
193 void MainWindow::openModelForEditing(const ModelId modelId)
194 {
195 EditorTabWidget* document = new EditorTabWidget{
196 this->documents.getModelById(modelId),
197 &this->documents,
198 this->colorTable,
199 };
200 document->canvas->setRenderPreferences(this->renderPreferences);
201 connect(document, &EditorTabWidget::newStatusText, [&](const QString& newStatusText)
202 {
203 this->statusBar()->showMessage(newStatusText);
204 });
205 const QFileInfo fileInfo{*this->documents.modelPath(modelId)};
206 QString tabName = fileInfo.baseName();
207 if (tabName.isEmpty())
208 {
209 tabName = tr("<unnamed>");
210 }
211 this->ui->tabs->addTab(document, tabName);
212 this->ui->tabs->setCurrentWidget(document);
213 document->restoreSplitterState(this->documentSplitterState);
214 }
215
216 void MainWindow::runSettingsEditor()
217 {
218 SettingsEditor settingsEditor{&this->settings, this->defaultKeyboardShortcuts, this};
219 const int result = settingsEditor.exec();
220 if (result == QDialog::Accepted)
221 {
222 this->restoreSettings();
223 }
224 }
225
226 EditorTabWidget* MainWindow::currentDocument()
227 {
228 return qobject_cast<EditorTabWidget*>(this->ui->tabs->currentWidget());
229 }
230
231 const EditorTabWidget* MainWindow::currentDocument() const
232 {
233 return qobject_cast<const EditorTabWidget*>(this->ui->tabs->currentWidget());
234 }
235
236 void MainWindow::handleDocumentSplitterChange()
237 {
238 EditorTabWidget* currentDocument = this->currentDocument();
239 if (currentDocument != nullptr)
240 {
241 this->documentSplitterState = currentDocument->saveSplitterState();
242 for (int i = 0; i < this->ui->tabs->count(); i += 1)
243 {
244 EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
245 if (document != nullptr and document != currentDocument)
246 {
247 document->restoreSplitterState(this->documentSplitterState);
248 }
249 }
250 this->settings.setMainSplitterState(this->documentSplitterState);
251 }
252 }
253
254 void MainWindow::updateRecentlyOpenedDocumentsMenu()
255 {
256 this->ui->menuRecentFiles->clear();
257 for (const QString& path : this->recentlyOpenedFiles)
258 {
259 QAction* action = new QAction{path, this};
260 action->setData(path);
261 this->ui->menuRecentFiles->addAction(action);
262 connect(action, &QAction::triggered, this, &MainWindow::openRecentFile);
263 }
264 }
265
266 void MainWindow::openRecentFile()
267 {
268 QAction* action = qobject_cast<QAction*>(this->sender());
269 if (action != nullptr)
270 {
271 const QString path = action->data().toString();
272 this->openModelFromPath(path);
273 }
274 }
275
276 void MainWindow::setRenderStyle(gl::RenderStyle renderStyle)
277 {
278 this->renderPreferences.style = renderStyle;
279 this->saveSettings();
280 this->updateRenderPreferences();
281 }
282
283 void MainWindow::setDrawAxes(bool drawAxes)
284 {
285 this->renderPreferences.drawAxes = drawAxes;
286 this->saveSettings();
287 this->updateRenderPreferences();
288 }
289
290 /**
291 * @brief Handles the "Save" (Ctrl+S) action
292 */
293 void MainWindow::actionSave()
294 {
295 if (this->currentDocument() != nullptr)
296 {
297 const std::optional<ModelId> modelId = this->findCurrentModelId();
298 if (modelId.has_value())
299 {
300 const QString* path = this->documents.modelPath(*modelId);
301 if (path == nullptr or path->isEmpty())
302 {
303 this->actionSaveAs();
304 }
305 else
306 {
307 QString error;
308 QTextStream errorStream{&error};
309 const bool succeeded = this->documents.saveModel(*modelId, errorStream);
310 if (not succeeded)
311 {
312 QMessageBox::critical(this, tr("Save error"), error);
313 }
314 else
315 {
316 this->addRecentlyOpenedFile(*path);
317 }
318 }
319 }
320 }
321 }
322
323 /**
324 * @brief Handles the "Save as…" (Ctrl+Shift+S) action
325 */
326 void MainWindow::actionSaveAs()
327 {
328 if (this->currentDocument() != nullptr)
329 {
330 const std::optional<ModelId> modelId = this->findCurrentModelId();
331 if (modelId.has_value())
332 {
333 const QString* pathPtr = this->documents.modelPath(*modelId);
334 QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
335 const QString newPath = QFileDialog::getSaveFileName(
336 this,
337 tr("Save as…"),
338 QFileInfo{defaultPath}.absoluteDir().path(),
339 tr("LDraw files (*.ldr *dat);;All files (*)")
340 );
341 if (not newPath.isEmpty())
342 {
343 QString error;
344 QTextStream errorStream{&error};
345 this->documents.setModelPath(*modelId, newPath, this->libraries, errorStream);
346 this->ui->tabs->setTabText(this->ui->tabs->currentIndex(), QFileInfo{newPath}.fileName());
347 this->actionSave();
348 }
349 }
350 }
351 }
352
353 /**
354 * @brief Handles the "Close" (Ctrl+W) action
355 */
356 void MainWindow::actionClose()
357 {
358 if (this->currentDocument() != nullptr)
359 {
360 this->closeDocument(this->currentDocument());
361 }
362 }
363
364 /**
365 * @brief Handles the "Delete" (Del) action
366 */
367 void MainWindow::actionDelete()
368 {
369 /*
370 EditorTabWidget* document = this->currentDocument();
371 if (document != nullptr)
372 {
373 std::unique_ptr<ModelEditor> modelEditor = document->editModel();
374 QSet<ldraw::id_t> ids = document->selectedObjects(); // copy
375 for (const ldraw::id_t id : ids)
376 {
377 const QModelIndex index = modelEditor->model().find(id);
378 if (index.isValid())
379 {
380 modelEditor->remove(index.row());
381 }
382 }
383 }
384 */
385 }
386
387 /**
388 * @brief Handles the "Invert" action
389 */
390 void MainWindow::actionInvert()
391 {
392 /*
393 EditorTabWidget* document = this->currentDocument();
394 if (document != nullptr)
395 {
396 // TODO: simplify
397 std::unique_ptr<ModelEditor> modelEditor = document->editModel();
398 const std::optional<ModelId> modelId = this->documents.findIdForModel(&modelEditor->model());
399 if (modelId.has_value())
400 {
401 ldraw::GetPolygonsContext context = {
402 .modelId = modelId.value(),
403 .documents = &this->documents,
404 };
405 for (const ldraw::id_t id : document->selectedObjects())
406 {
407 modelEditor->modifyObject(id, [&context](ldraw::Object* object)
408 {
409 object->invert(&context);
410 });
411 }
412 }
413 }
414 */
415 }
416
417 /**
418 * @brief Removes the document at the specified tab index
419 * @param index
420 */
421 void MainWindow::handleTabCloseButton(int tabIndex)
422 {
423 if (tabIndex >= 0 and tabIndex < this->ui->tabs->count())
424 {
425 EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(tabIndex));
426 if (document != nullptr)
427 {
428 this->closeDocument(document);
429 }
430 }
431 }
432
433 /**
434 * @brief Closes the specified document
435 * @param document
436 */
437 void MainWindow::closeDocument(EditorTabWidget *document)
438 {
439 std::optional<ModelId> modelId = this->documents.findIdForModel(document->model);
440 if (modelId.has_value())
441 {
442 this->documents.closeDocument(modelId.value());
443 delete document;
444 }
445 }
446
447 std::optional<ModelId> MainWindow::findCurrentModelId() const
448 {
449 const EditorTabWidget* document = this->currentDocument();
450 if (document != nullptr)
451 {
452 return this->documents.findIdForModel(document->model);
453 }
454 else
455 {
456 return {};
457 }
458 }
459
460 void MainWindow::changeEvent(QEvent* event)
461 {
462 if (event != nullptr)
463 {
464 switch (event->type())
465 {
466 case QEvent::LanguageChange:
467 this->ui->retranslateUi(this);
468 break;
469 default:
470 break;
471 }
472 }
473 QMainWindow::changeEvent(event);
474 }
475
476 /**
477 * @brief Handles closing the main window
478 * @param event Event information
479 */
480 void MainWindow::closeEvent(QCloseEvent* event)
481 {
482 saveSettings();
483 event->accept();
484 }
485
486 /**
487 * @brief Updates the title of the main window so to contain the app's name
488 * and version as well as the open document name.
489 */
490 void MainWindow::updateTitle()
491 {
492 QString title = ::appName;
493 title += " ";
494 title += fullVersionString();
495 setWindowTitle(title);
496 }
497
498 void MainWindow::updateRenderPreferences()
499 {
500 for (int i = 0; i < this->ui->tabs->count(); i += 1)
501 {
502 EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
503 if (document != nullptr)
504 {
505 document->canvas->setRenderPreferences(this->renderPreferences);
506 }
507 }
508 for (auto data : ::renderStyleButtons)
509 {
510 QAction* action = data.memberInstance(this->ui.get());
511 action->setChecked(this->renderPreferences.style == data.payload);
512 }
513 this->ui->actionDrawAxes->setChecked(this->renderPreferences.drawAxes);
514 }
515
516 /**
517 * @brief Stores the settings of the main window, storing geometry, etc
518 */
519 void MainWindow::saveSettings()
520 {
521 this->settings.setMainWindowGeometry(this->saveGeometry());
522 this->settings.setRecentFiles(this->recentlyOpenedFiles);
523 this->settings.setMainSplitterState(this->documentSplitterState);
524 this->settings.setRenderStyle(static_cast<int>(this->renderPreferences.style));
525 this->settings.setDrawAxes(this->renderPreferences.drawAxes);
526 this->libraries.storeToSettings(&this->settings);
527 }
528
529 void MainWindow::restoreStartupSettings()
530 {
531 this->restoreGeometry(this->settings.mainWindowGeometry());
532 }
533
534 /**
535 * @brief Restores saved settings relating to the main window
536 */
537 void MainWindow::restoreSettings()
538 {
539 this->recentlyOpenedFiles = this->settings.recentFiles();
540 this->documentSplitterState = this->settings.mainSplitterState();
541 this->renderPreferences.style = static_cast<gl::RenderStyle>(this->settings.renderStyle());
542 this->renderPreferences.mainColor = this->settings.mainColor();
543 this->renderPreferences.backgroundColor = this->settings.backgroundColor();
544 this->renderPreferences.lineThickness = this->settings.lineThickness();
545 this->renderPreferences.lineAntiAliasing = this->settings.lineAntiAliasing();
546 this->renderPreferences.selectedColor = this->settings.selectedColor();
547 this->renderPreferences.drawAxes = this->settings.drawAxes();
548 const QString systemLocale = QLocale::system().name();
549 const QVariant defaultLocale = this->settings.locale();
550 this->changeLanguage(defaultLocale.toString());
551 this->libraries.restoreFromSettings(&this->settings);
552 this->updateRecentlyOpenedDocumentsMenu();
553 this->loadColors();
554 this->updateRenderPreferences();
555 }
556
557 QString MainWindow::pathToTranslation(const QString& localeCode)
558 {
559 QDir dir {":/locale"};
560 return dir.filePath(localeCode + ".qm");
561 }
562
563 void MainWindow::loadColors()
564 {
565 QTextStream errors;
566 this->colorTable = this->libraries.loadColorTable(errors);
567 }
568
569 void MainWindow::keyReleaseEvent(QKeyEvent* /*event*/)
570 {
571 /*
572 Document* document = this->currentDocument();
573 if (document != nullptr)
574 {
575 document->handleKeyPress(event);
576 }
577 */
578 }

mercurial