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 } |
|