1 /* |
|
2 * LDForge: LDraw parts authoring CAD |
|
3 * Copyright (C) 2013 - 2020 Teemu Piippo |
|
4 * |
|
5 * This program is free software: you can redistribute it and/or modify |
|
6 * it under the terms of the GNU General Public License as published by |
|
7 * the Free Software Foundation, either version 3 of the License, or |
|
8 * (at your option) any later version. |
|
9 * |
|
10 * This program is distributed in the hope that it will be useful, |
|
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 * GNU General Public License for more details. |
|
14 * |
|
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/>. |
|
17 */ |
|
18 |
|
19 #include <QApplication> |
1 #include <QApplication> |
|
2 #include <QFileDialog> |
|
3 #include <QMessageBox> |
20 #include "mainwindow.h" |
4 #include "mainwindow.h" |
|
5 #include "ui_mainwindow.h" |
21 #include "version.h" |
6 #include "version.h" |
|
7 #include "document.h" |
|
8 #include "settingseditor/settingseditor.h" |
|
9 #include "widgets/colorselectdialog.h" |
|
10 |
|
11 static const QDir LOCALE_DIR {":/locale"}; |
22 |
12 |
23 static void doQtRegistrations() |
13 static void doQtRegistrations() |
24 { |
14 { |
25 QCoreApplication::setApplicationName(::appName); |
15 QCoreApplication::setApplicationName(::appName); |
26 QCoreApplication::setOrganizationName("hecknology.net"); |
16 QCoreApplication::setOrganizationName("hecknology.net"); |
27 QCoreApplication::setOrganizationDomain("hecknology.net"); |
17 QCoreApplication::setOrganizationDomain("hecknology.net"); |
28 qRegisterMetaTypeStreamOperators<Library>("Library"); |
18 qRegisterMetaTypeStreamOperators<Library>("Library"); |
29 qRegisterMetaTypeStreamOperators<Libraries>("Libraries"); |
19 qRegisterMetaTypeStreamOperators<Libraries>("Libraries"); |
30 } |
20 } |
31 |
21 |
|
22 template<typename BaseType, typename MemberType, typename DataType> |
|
23 struct MemberData |
|
24 { |
|
25 std::size_t member; |
|
26 DataType payload; |
|
27 constexpr MemberType memberInstance(BaseType* instance) const |
|
28 { |
|
29 return *reinterpret_cast<MemberType*>(reinterpret_cast<char*>(instance) + this->member); |
|
30 } |
|
31 }; |
|
32 |
|
33 static constexpr MemberData<Ui_MainWindow, QAction*, gl::RenderStyle> renderStyleButtons[] = { |
|
34 { offsetof(Ui_MainWindow, actionRenderStyleNormal), gl::RenderStyle::Normal }, |
|
35 { offsetof(Ui_MainWindow, actionRenderStyleBfc), gl::RenderStyle::BfcRedGreen }, |
|
36 { offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors }, |
|
37 { offsetof(Ui_MainWindow, actionRenderStylePickScene), gl::RenderStyle::PickScene }, |
|
38 }; |
|
39 |
|
40 static std::optional<ModelId> openModelFromPath( |
|
41 const QString& path, |
|
42 const LibraryManager* libraries, |
|
43 DocumentManager* documents, |
|
44 QWidget* parent) |
|
45 { |
|
46 QString errorString; |
|
47 QTextStream errorStream{&errorString}; |
|
48 const std::optional<ModelId> modelIdOpt = documents->openModel( |
|
49 path, |
|
50 errorStream, |
|
51 DocumentManager::OpenType::ManuallyOpened); |
|
52 if (modelIdOpt.has_value()) { |
|
53 documents->loadDependenciesForModel(modelIdOpt.value(), path, *libraries, errorStream); |
|
54 if (not errorString.isEmpty()) { |
|
55 QMessageBox::warning( |
|
56 parent, |
|
57 QObject::tr("Problem loading references"), |
|
58 errorString); |
|
59 } |
|
60 } |
|
61 else { |
|
62 QMessageBox::critical( |
|
63 parent, |
|
64 QObject::tr("Problem opening file"), |
|
65 format(QObject::tr("Could not open %1: %2"), path, errorString)); |
|
66 } |
|
67 return modelIdOpt; |
|
68 } |
|
69 |
|
70 static QString getOpenModelPath(QWidget* parent) |
|
71 { |
|
72 return QFileDialog::getOpenFileName( |
|
73 parent, |
|
74 QObject::tr("Open model"), |
|
75 "", |
|
76 QObject::tr("LDraw models (*.ldr *.dat)")); |
|
77 } |
|
78 |
|
79 static const QString localeCode(const QString& locale) |
|
80 { |
|
81 if (locale == "system") { |
|
82 return QLocale::system().name(); |
|
83 } |
|
84 else { |
|
85 return locale; |
|
86 } |
|
87 } |
|
88 |
|
89 /** |
|
90 * @brief Changes the application language to the specified language |
|
91 */ |
|
92 static void changeLanguage(const QString& locale, QTranslator* translator) |
|
93 { |
|
94 if (not locale.isEmpty()) { |
|
95 const QString localeCode = ::localeCode(locale); |
|
96 QLocale::setDefault({localeCode}); |
|
97 qApp->removeTranslator(translator); |
|
98 const QString path = LOCALE_DIR.filePath(localeCode + ".qm"); |
|
99 const bool loadSuccessful = translator->load(path); |
|
100 if (loadSuccessful) |
|
101 { |
|
102 qApp->installTranslator(translator); |
|
103 } |
|
104 } |
|
105 } |
|
106 |
|
107 /* |
|
108 void MainWindow::handleDocumentSplitterChange() |
|
109 { |
|
110 EditorTabWidget* currentDocument = this->currentDocument(); |
|
111 if (currentDocument != nullptr) |
|
112 { |
|
113 this->documentSplitterState = currentDocument->saveSplitterState(); |
|
114 for (int i = 0; i < this->ui->tabs->count(); i += 1) |
|
115 { |
|
116 EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i)); |
|
117 if (document != nullptr and document != currentDocument) |
|
118 { |
|
119 document->restoreSplitterState(this->documentSplitterState); |
|
120 } |
|
121 } |
|
122 this->settings.setMainSplitterState(this->documentSplitterState); |
|
123 } |
|
124 } |
|
125 */ |
|
126 |
|
127 static EditorTabWidget* currentTabWidget(Ui_MainWindow* ui) |
|
128 { |
|
129 return qobject_cast<EditorTabWidget*>(ui->tabs->currentWidget()); |
|
130 }; |
|
131 |
|
132 |
|
133 static void closeDocument(DocumentManager* documents, EditorTabWidget *document) |
|
134 { |
|
135 std::optional<ModelId> modelId = documents->findIdForModel(document->model); |
|
136 if (modelId.has_value()) { |
|
137 documents->closeDocument(modelId.value()); |
|
138 delete document; |
|
139 } |
|
140 } |
|
141 |
|
142 static void handleTabCloseButton(Ui_MainWindow* ui, DocumentManager* documents, int tabIndex) |
|
143 { |
|
144 if (tabIndex >= 0 and tabIndex < ui->tabs->count()) { |
|
145 EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(tabIndex)); |
|
146 if (tab != nullptr) { |
|
147 closeDocument(documents, tab); |
|
148 } |
|
149 } |
|
150 } |
|
151 |
|
152 static std::optional<ModelId> findCurrentModelId(Ui_MainWindow* ui, DocumentManager* documents) |
|
153 { |
|
154 const EditorTabWidget* tab = currentTabWidget(ui); |
|
155 if (tab != nullptr) { |
|
156 return documents->findIdForModel(tab->model); |
|
157 } |
|
158 else { |
|
159 return {}; |
|
160 } |
|
161 } |
|
162 |
|
163 /** |
|
164 * @brief Updates the title of the main window so to contain the app's name |
|
165 * and version as well as the open document name. |
|
166 */ |
|
167 static QString title() |
|
168 { |
|
169 QString title = ::appName; |
|
170 title += " "; |
|
171 title += fullVersionString(); |
|
172 return title; |
|
173 } |
|
174 |
|
175 static ldraw::ColorTable loadColors(const LibraryManager* libraries) |
|
176 { |
|
177 QTextStream errors; |
|
178 return libraries->loadColorTable(errors); |
|
179 } |
|
180 |
|
181 static QString tabName(const QFileInfo& fileInfo) |
|
182 { |
|
183 QString result = fileInfo.baseName(); |
|
184 if (result.isEmpty()) { |
|
185 result = QObject::tr("<unnamed>"); |
|
186 } |
|
187 return result; |
|
188 } |
|
189 |
|
190 void rebuildRecentFilesMenu(QMenu* menu, const QStringList& strings, QWidget* parent) |
|
191 { |
|
192 menu->clear(); |
|
193 for (const QString& path : strings) { |
|
194 QAction* action = new QAction{path, parent}; |
|
195 action->setData(path); |
|
196 menu->addAction(action); |
|
197 } |
|
198 } |
|
199 |
|
200 static void updateRenderPreferences( |
|
201 Ui_MainWindow* ui, |
|
202 const gl::RenderPreferences* renderPreferences) |
|
203 { |
|
204 for (int i = 0; i < ui->tabs->count(); i += 1) { |
|
205 EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(i)); |
|
206 if (tab != nullptr) { |
|
207 tab->canvas->setRenderPreferences(*renderPreferences); |
|
208 } |
|
209 } |
|
210 for (auto data : ::renderStyleButtons) { |
|
211 QAction* action = data.memberInstance(ui); |
|
212 action->setChecked(renderPreferences->style == data.payload); |
|
213 } |
|
214 ui->actionDrawAxes->setChecked(renderPreferences->drawAxes); |
|
215 }; |
|
216 |
|
217 static gl::RenderPreferences loadRenderPreferences(Configuration* settings) |
|
218 { |
|
219 return gl::RenderPreferences{ |
|
220 .style = static_cast<gl::RenderStyle>(settings->renderStyle()), |
|
221 .mainColor = settings->mainColor(), |
|
222 .backgroundColor = settings->backgroundColor(), |
|
223 .selectedColor = settings->selectedColor(), |
|
224 .lineThickness = settings->lineThickness(), |
|
225 .lineAntiAliasing = settings->lineAntiAliasing(), |
|
226 .drawAxes = settings->drawAxes(), |
|
227 }; |
|
228 } |
|
229 |
32 int main(int argc, char *argv[]) |
230 int main(int argc, char *argv[]) |
33 { |
231 { |
34 doQtRegistrations(); |
232 doQtRegistrations(); |
35 QApplication app{argc, argv}; |
233 QApplication app{argc, argv}; |
36 MainWindow mainwindow; |
234 QMainWindow mainWindow; |
37 mainwindow.show(); |
235 Ui_MainWindow ui; |
38 return app.exec(); |
236 DocumentManager documents{&mainWindow}; |
39 } |
237 QString currentLanguage = "en"; |
|
238 QTranslator translator{&mainWindow}; |
|
239 Configuration settings; |
|
240 LibraryManager libraries{&mainWindow}; |
|
241 QByteArray documentSplitterState; |
|
242 QStringList recentlyOpenedFiles; |
|
243 ldraw::ColorTable colorTable; |
|
244 gl::RenderPreferences renderPreferences; |
|
245 ui.setupUi(&mainWindow); |
|
246 const uiutilities::KeySequenceMap defaultKeyboardShortcuts = |
|
247 uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow)); |
|
248 const auto saveSettings = [&]{ |
|
249 settings.setMainWindowGeometry(mainWindow.saveGeometry()); |
|
250 settings.setRecentFiles(recentlyOpenedFiles); |
|
251 settings.setMainSplitterState(documentSplitterState); |
|
252 settings.setRenderStyle(static_cast<int>(renderPreferences.style)); |
|
253 settings.setDrawAxes(renderPreferences.drawAxes); |
|
254 libraries.storeToSettings(&settings); |
|
255 }; |
|
256 const auto updateRecentlyOpenedDocumentsMenu = [&]{ |
|
257 rebuildRecentFilesMenu(ui.menuRecentFiles, recentlyOpenedFiles, &mainWindow); |
|
258 for (QAction* action : ui.menuRecentFiles->findChildren<QAction*>()) { |
|
259 QString path = action->data().toString(); |
|
260 QObject::connect( |
|
261 action, |
|
262 &QAction::triggered, |
|
263 [path, &libraries, &documents, &mainWindow]() { |
|
264 openModelFromPath(path, &libraries, &documents, &mainWindow); |
|
265 } |
|
266 ); |
|
267 } |
|
268 }; |
|
269 const auto restoreSettings = [&]{ |
|
270 recentlyOpenedFiles = settings.recentFiles(); |
|
271 documentSplitterState = settings.mainSplitterState(); |
|
272 renderPreferences = loadRenderPreferences(&settings); |
|
273 changeLanguage(settings.locale(), &translator); |
|
274 libraries.restoreFromSettings(&settings); |
|
275 updateRecentlyOpenedDocumentsMenu(); |
|
276 colorTable = loadColors(&libraries); |
|
277 updateRenderPreferences(&ui, &renderPreferences); |
|
278 ui.retranslateUi(&mainWindow); |
|
279 }; |
|
280 const auto addRecentlyOpenedFile = [&](const QString& path){ |
|
281 constexpr int maxRecentlyOpenedFiles = 10; |
|
282 recentlyOpenedFiles.removeAll(path); |
|
283 recentlyOpenedFiles.insert(0, path); |
|
284 while (recentlyOpenedFiles.size() > maxRecentlyOpenedFiles) |
|
285 { |
|
286 recentlyOpenedFiles.removeLast(); |
|
287 } |
|
288 saveSettings(); |
|
289 updateRecentlyOpenedDocumentsMenu(); |
|
290 }; |
|
291 const auto openModelForEditing = [&](const ModelId modelId){ |
|
292 EditorTabWidget* document = new EditorTabWidget{ |
|
293 documents.getModelById(modelId), |
|
294 &documents, |
|
295 colorTable, |
|
296 }; |
|
297 document->canvas->setRenderPreferences(renderPreferences); |
|
298 QObject::connect( |
|
299 document, |
|
300 &EditorTabWidget::newStatusText, |
|
301 [&](const QString& newStatusText) { |
|
302 mainWindow.statusBar()->showMessage(newStatusText); |
|
303 }); |
|
304 const QFileInfo fileInfo{*documents.modelPath(modelId)}; |
|
305 ui.tabs->addTab(document, tabName(fileInfo)); |
|
306 ui.tabs->setCurrentWidget(document); |
|
307 document->restoreSplitterState(documentSplitterState); |
|
308 }; |
|
309 const auto newModel = [&openModelForEditing](DocumentManager* documents){ |
|
310 openModelForEditing(documents->newModel()); |
|
311 }; |
|
312 QObject::connect(ui.actionNew, &QAction::triggered, [&newModel, &documents]{ |
|
313 newModel(&documents); |
|
314 }); |
|
315 QObject::connect(ui.actionOpen, &QAction::triggered, [&]{ |
|
316 const QString path = getOpenModelPath(&mainWindow); |
|
317 if (not path.isEmpty()) |
|
318 { |
|
319 const std::optional<ModelId> id = openModelFromPath(path, &libraries, &documents, &mainWindow); |
|
320 if (id.has_value()) { |
|
321 openModelForEditing(id.value()); |
|
322 addRecentlyOpenedFile(path); |
|
323 } |
|
324 } |
|
325 }); |
|
326 QObject::connect(ui.actionSettingsEditor, &QAction::triggered, [&]{ |
|
327 SettingsEditor settingsEditor{&settings, defaultKeyboardShortcuts, &mainWindow}; |
|
328 const int result = settingsEditor.exec(); |
|
329 if (result == QDialog::Accepted) |
|
330 { |
|
331 restoreSettings(); |
|
332 } |
|
333 }); |
|
334 QObject::connect(ui.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close); |
|
335 QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&ui]{ |
|
336 EditorTabWidget* tab = currentTabWidget(&ui); |
|
337 if (tab != nullptr) |
|
338 { |
|
339 adjustGridToView(tab->canvas); |
|
340 } |
|
341 }); |
|
342 QObject::connect(ui.actionClose, &QAction::triggered, [&ui, &documents]{ |
|
343 EditorTabWidget* tab = currentTabWidget(&ui); |
|
344 if (tab != nullptr) |
|
345 { |
|
346 closeDocument(&documents, tab); |
|
347 } |
|
348 }); |
|
349 const auto save = [&](ModelId modelId){ |
|
350 QString error; |
|
351 QTextStream errorStream{&error}; |
|
352 const bool succeeded = documents.saveModel(modelId, errorStream); |
|
353 if (not succeeded) |
|
354 { |
|
355 QMessageBox::critical(&mainWindow, QObject::tr("Save error"), error); |
|
356 } |
|
357 else |
|
358 { |
|
359 const QString* pathPtr = documents.modelPath(modelId); |
|
360 if (pathPtr != nullptr) { |
|
361 addRecentlyOpenedFile(*pathPtr); |
|
362 } |
|
363 } |
|
364 }; |
|
365 const auto actionSaveAs = [&]{ |
|
366 const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents); |
|
367 if (modelId.has_value()) |
|
368 { |
|
369 const QString* pathPtr = documents.modelPath(*modelId); |
|
370 QString defaultPath = (pathPtr != nullptr) ? *pathPtr : ""; |
|
371 const QString newPath = QFileDialog::getSaveFileName( |
|
372 &mainWindow, |
|
373 QObject::tr("Save as…"), |
|
374 QFileInfo{defaultPath}.absoluteDir().path(), |
|
375 QObject::tr("LDraw files (*.ldr *dat);;All files (*)") |
|
376 ); |
|
377 if (not newPath.isEmpty()) { |
|
378 QString error; |
|
379 QTextStream errorStream{&error}; |
|
380 documents.setModelPath(*modelId, newPath, libraries, errorStream); |
|
381 ui.tabs->setTabText(ui.tabs->currentIndex(), QFileInfo{newPath}.fileName()); |
|
382 save(*modelId); |
|
383 } |
|
384 } |
|
385 }; |
|
386 QObject::connect(ui.actionSaveAs, &QAction::triggered, actionSaveAs); |
|
387 QObject::connect(ui.actionSave, &QAction::triggered, [&]{ |
|
388 if (currentTabWidget(&ui) != nullptr) { |
|
389 const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents); |
|
390 if (modelId.has_value()) { |
|
391 const QString* path = documents.modelPath(*modelId); |
|
392 if (path == nullptr or path->isEmpty()) { |
|
393 actionSaveAs(); |
|
394 } |
|
395 else { |
|
396 save(*modelId); |
|
397 } |
|
398 } |
|
399 } |
|
400 }); |
|
401 QObject::connect(ui.tabs, &QTabWidget::tabCloseRequested, [&](int index){ |
|
402 handleTabCloseButton(&ui, &documents, index); |
|
403 }); |
|
404 QObject::connect(ui.actionDrawAxes, &QAction::triggered, [&](bool drawAxes){ |
|
405 renderPreferences.drawAxes = drawAxes; |
|
406 saveSettings(); |
|
407 updateRenderPreferences(&ui, &renderPreferences); |
|
408 }); |
|
409 for (auto data : ::renderStyleButtons) { |
|
410 QAction* action = data.memberInstance(&ui); |
|
411 QObject::connect(action, &QAction::triggered, [&, data]{ |
|
412 renderPreferences.style = data.payload; |
|
413 saveSettings(); |
|
414 updateRenderPreferences(&ui, &renderPreferences); |
|
415 }); |
|
416 } |
|
417 mainWindow.setWindowTitle(title()); |
|
418 mainWindow.restoreGeometry(settings.mainWindowGeometry()); |
|
419 restoreSettings(); |
|
420 updateRenderPreferences(&ui, &renderPreferences); |
|
421 newModel(&documents); |
|
422 mainWindow.show(); |
|
423 const int result = app.exec(); |
|
424 saveSettings(); |
|
425 return result; |
|
426 } |