34 #include "dialogs/openprogressdialog.h" |
34 #include "dialogs/openprogressdialog.h" |
35 |
35 |
36 ConfigOption (QStringList RecentFiles) |
36 ConfigOption (QStringList RecentFiles) |
37 ConfigOption (bool TryDownloadMissingFiles = false) |
37 ConfigOption (bool TryDownloadMissingFiles = false) |
38 |
38 |
39 static bool g_loadingMainFile = false; |
39 LDDocument::LDDocument (DocumentManager* parent) : |
40 enum { MAX_RECENT_FILES = 10 }; |
|
41 static LDDocument* g_logoedStud; |
|
42 static LDDocument* g_logoedStud2; |
|
43 static bool g_loadingLogoedStuds = false; |
|
44 const QStringList g_specialSubdirectories ({ "s", "48", "8" }); |
|
45 |
|
46 LDDocument::LDDocument (QObject* parent) : |
|
47 QObject (parent), |
40 QObject (parent), |
48 HierarchyElement (parent), |
41 HierarchyElement (parent), |
49 m_history (new EditHistory (this)), |
42 m_history (new EditHistory (this)), |
50 m_isCache (true), |
43 m_isCache (true), |
51 m_verticesOutdated (true), |
44 m_verticesOutdated (true), |
52 m_needVertexMerge (true), |
45 m_needVertexMerge (true), |
53 m_beingDestroyed (false), |
46 m_beingDestroyed (false), |
54 m_gldata (new LDGLData) |
47 m_gldata (new LDGLData), |
|
48 m_manager (parent) |
55 { |
49 { |
56 setSavePosition (-1); |
50 setSavePosition (-1); |
57 setTabIndex (-1); |
51 setTabIndex (-1); |
58 m_needsReCache = true; |
52 m_needsReCache = true; |
59 } |
53 } |
60 |
54 |
61 LDDocument::~LDDocument() |
55 LDDocument::~LDDocument() |
62 { |
56 { |
|
57 for (int i = 0; i < m_objects.size(); ++i) |
|
58 delete m_objects[i]; |
|
59 |
63 m_beingDestroyed = true; |
60 m_beingDestroyed = true; |
64 delete m_history; |
61 delete m_history; |
65 delete m_gldata; |
62 delete m_gldata; |
66 } |
63 } |
67 |
64 |
189 } |
186 } |
190 |
187 |
191 LDGLData* LDDocument::glData() |
188 LDGLData* LDDocument::glData() |
192 { |
189 { |
193 return m_gldata; |
190 return m_gldata; |
194 } |
|
195 |
|
196 // ============================================================================= |
|
197 // |
|
198 LDDocument* FindDocument (QString name) |
|
199 { |
|
200 for (LDDocument* document : g_win->allDocuments()) |
|
201 { |
|
202 if (isOneOf (name, document->name(), document->defaultName())) |
|
203 return document; |
|
204 } |
|
205 |
|
206 return nullptr; |
|
207 } |
|
208 |
|
209 // ============================================================================= |
|
210 // |
|
211 QString Dirname (QString path) |
|
212 { |
|
213 long lastpos = path.lastIndexOf (DIRSLASH); |
|
214 |
|
215 if (lastpos > 0) |
|
216 return path.left (lastpos); |
|
217 |
|
218 #ifndef _WIN32 |
|
219 if (path[0] == DIRSLASH_CHAR) |
|
220 return DIRSLASH; |
|
221 #endif // _WIN32 |
|
222 |
|
223 return ""; |
|
224 } |
|
225 |
|
226 // ============================================================================= |
|
227 // |
|
228 QString Basename (QString path) |
|
229 { |
|
230 long lastpos = path.lastIndexOf (DIRSLASH); |
|
231 |
|
232 if (lastpos != -1) |
|
233 return path.mid (lastpos + 1); |
|
234 |
|
235 return path; |
|
236 } |
|
237 |
|
238 // ============================================================================= |
|
239 // |
|
240 static QString FindDocumentPath (QString relpath, bool subdirs) |
|
241 { |
|
242 QString fullPath; |
|
243 |
|
244 // LDraw models use backslashes as path separators. Replace those into forward slashes for Qt. |
|
245 relpath.replace ("\\", "/"); |
|
246 |
|
247 // Try find it relative to other currently open documents. We want a file in the immediate vicinity of a current |
|
248 // part model to override stock LDraw stuff. |
|
249 QString reltop = Basename (Dirname (relpath)); |
|
250 |
|
251 for (LDDocument* doc : g_win->allDocuments()) |
|
252 { |
|
253 QString partpath = format ("%1/%2", Dirname (doc->fullPath()), relpath); |
|
254 QFile f (partpath); |
|
255 |
|
256 if (f.exists()) |
|
257 { |
|
258 // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48 |
|
259 QString proptop = Basename (Dirname (partpath)); |
|
260 |
|
261 bool bogus = false; |
|
262 |
|
263 for (QString s : g_specialSubdirectories) |
|
264 { |
|
265 if ((proptop == s and reltop != s) or (reltop == s and proptop != s)) |
|
266 { |
|
267 bogus = true; |
|
268 break; |
|
269 } |
|
270 } |
|
271 |
|
272 if (not bogus) |
|
273 return partpath; |
|
274 } |
|
275 } |
|
276 |
|
277 if (QFile::exists (relpath)) |
|
278 return relpath; |
|
279 |
|
280 // Try with just the LDraw path first |
|
281 fullPath = format ("%1" DIRSLASH "%2", g_win->configBag()->lDrawPath(), relpath); |
|
282 |
|
283 if (QFile::exists (fullPath)) |
|
284 return fullPath; |
|
285 |
|
286 if (subdirs) |
|
287 { |
|
288 // Look in sub-directories: parts and p. Also look in the download path, since that's where we download parts |
|
289 // from the PT to. |
|
290 QStringList dirs = { g_win->configBag()->lDrawPath(), g_win->configBag()->downloadFilePath() }; |
|
291 for (const QString& topdir : dirs) |
|
292 { |
|
293 for (const QString& subdir : QStringList ({ "parts", "p" })) |
|
294 { |
|
295 fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath); |
|
296 |
|
297 if (QFile::exists (fullPath)) |
|
298 return fullPath; |
|
299 } |
|
300 } |
|
301 } |
|
302 |
|
303 // Did not find the file. |
|
304 return ""; |
|
305 } |
|
306 |
|
307 // ============================================================================= |
|
308 // |
|
309 QFile* OpenLDrawFile (QString relpath, bool subdirs, QString* pathpointer) |
|
310 { |
|
311 print ("Opening %1...\n", relpath); |
|
312 QString path = FindDocumentPath (relpath, subdirs); |
|
313 |
|
314 if (pathpointer) |
|
315 *pathpointer = path; |
|
316 |
|
317 if (path.isEmpty()) |
|
318 return nullptr; |
|
319 |
|
320 QFile* fp = new QFile (path); |
|
321 |
|
322 if (fp->open (QIODevice::ReadOnly)) |
|
323 return fp; |
|
324 |
|
325 fp->deleteLater(); |
|
326 return nullptr; |
|
327 } |
|
328 |
|
329 // ============================================================================= |
|
330 // |
|
331 LDObjectList LoadFileContents (QFile* fp, int* numWarnings, bool* ok) |
|
332 { |
|
333 LDObjectList objs; |
|
334 |
|
335 if (numWarnings) |
|
336 *numWarnings = 0; |
|
337 |
|
338 DocumentLoader* loader = new DocumentLoader (g_loadingMainFile); |
|
339 loader->read (fp); |
|
340 loader->start(); |
|
341 |
|
342 // After start() returns, if the loader isn't done yet, it's delaying |
|
343 // its next iteration through the event loop. We need to catch this here |
|
344 // by telling the event loop to tick, which will tick the file loader again. |
|
345 // We keep doing this until the file loader is ready. |
|
346 while (not loader->isDone()) |
|
347 qApp->processEvents(); |
|
348 |
|
349 // If we wanted the success value, supply that now |
|
350 if (ok) |
|
351 *ok = not loader->hasAborted(); |
|
352 |
|
353 objs = loader->objects(); |
|
354 delete loader; |
|
355 return objs; |
|
356 } |
|
357 |
|
358 // ============================================================================= |
|
359 // |
|
360 LDDocument* OpenDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride, bool* aborted) |
|
361 { |
|
362 // Convert the file name to lowercase when searching because some parts contain subfile |
|
363 // subfile references with uppercase file names. I'll assume here that the library will always |
|
364 // use lowercase file names for the part files. |
|
365 QFile* fp; |
|
366 QString fullpath; |
|
367 |
|
368 if (search) |
|
369 { |
|
370 fp = OpenLDrawFile (path.toLower(), true, &fullpath); |
|
371 } |
|
372 else |
|
373 { |
|
374 fp = new QFile (path); |
|
375 fullpath = path; |
|
376 |
|
377 if (not fp->open (QIODevice::ReadOnly)) |
|
378 { |
|
379 delete fp; |
|
380 return nullptr; |
|
381 } |
|
382 } |
|
383 |
|
384 if (not fp) |
|
385 return nullptr; |
|
386 |
|
387 LDDocument* load = (fileToOverride ? fileToOverride : g_win->newDocument (implicit)); |
|
388 load->setFullPath (fullpath); |
|
389 load->setName (LDDocument::shortenName (load->fullPath())); |
|
390 |
|
391 // Loading the file shouldn't count as actual edits to the document. |
|
392 load->history()->setIgnoring (true); |
|
393 |
|
394 int numWarnings; |
|
395 bool ok; |
|
396 LDObjectList objs = LoadFileContents (fp, &numWarnings, &ok); |
|
397 fp->close(); |
|
398 fp->deleteLater(); |
|
399 |
|
400 if (aborted) |
|
401 *aborted = ok == false; |
|
402 |
|
403 if (not ok) |
|
404 { |
|
405 load->close(); |
|
406 return nullptr; |
|
407 } |
|
408 |
|
409 load->addObjects (objs); |
|
410 |
|
411 if (g_loadingMainFile) |
|
412 { |
|
413 g_win->changeDocument (load); |
|
414 g_win->renderer()->setDocument (load); |
|
415 print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); |
|
416 } |
|
417 |
|
418 load->history()->setIgnoring (false); |
|
419 return load; |
|
420 } |
191 } |
421 |
192 |
422 // ============================================================================= |
193 // ============================================================================= |
423 // |
194 // |
424 // Performs safety checks. Do this before closing any files! |
195 // Performs safety checks. Do this before closing any files! |
473 break; |
244 break; |
474 } |
245 } |
475 } |
246 } |
476 |
247 |
477 return true; |
248 return true; |
478 } |
|
479 |
|
480 // ============================================================================= |
|
481 // |
|
482 void CloseAllDocuments() |
|
483 { |
|
484 for (LDDocument* file : g_win->allDocuments()) |
|
485 file->close(); |
|
486 } |
|
487 |
|
488 // ============================================================================= |
|
489 // |
|
490 void AddRecentFile (QString path) |
|
491 { |
|
492 QStringList recentFiles = g_win->configBag()->recentFiles(); |
|
493 int idx = recentFiles.indexOf (path); |
|
494 |
|
495 // If this file already is in the list, pop it out. |
|
496 if (idx != -1) |
|
497 { |
|
498 if (idx == recentFiles.size() - 1) |
|
499 return; // first recent file - abort and do nothing |
|
500 |
|
501 recentFiles.removeAt (idx); |
|
502 } |
|
503 |
|
504 // If there's too many recent files, drop one out. |
|
505 while (recentFiles.size() > (MAX_RECENT_FILES - 1)) |
|
506 recentFiles.removeAt (0); |
|
507 |
|
508 // Add the file |
|
509 recentFiles << path; |
|
510 g_win->configBag()->setRecentFiles (recentFiles); |
|
511 g_win->syncSettings(); |
|
512 g_win->updateRecentFilesMenu(); |
|
513 } |
|
514 |
|
515 // ============================================================================= |
|
516 // Open an LDraw file and set it as the main model |
|
517 // ============================================================================= |
|
518 void OpenMainModel (QString path) |
|
519 { |
|
520 // If there's already a file with the same name, this file must replace it. |
|
521 LDDocument* documentToReplace = nullptr; |
|
522 LDDocument* file = nullptr; |
|
523 QString shortName = LDDocument::shortenName (path); |
|
524 |
|
525 for (LDDocument* doc : g_win->allDocuments()) |
|
526 { |
|
527 if (doc->name() == shortName) |
|
528 { |
|
529 documentToReplace = doc; |
|
530 break; |
|
531 } |
|
532 } |
|
533 |
|
534 // We cannot open this file if the document this would replace is not |
|
535 // safe to close. |
|
536 if (documentToReplace and not documentToReplace->isSafeToClose()) |
|
537 return; |
|
538 |
|
539 g_loadingMainFile = true; |
|
540 |
|
541 // If we're replacing an existing document, clear the document and |
|
542 // make it ready for being loaded to. |
|
543 if (documentToReplace) |
|
544 { |
|
545 file = documentToReplace; |
|
546 file->clear(); |
|
547 } |
|
548 |
|
549 bool aborted; |
|
550 file = OpenDocument (path, false, false, file, &aborted); |
|
551 |
|
552 if (file == nullptr) |
|
553 { |
|
554 if (not aborted) |
|
555 { |
|
556 // Tell the user loading failed. |
|
557 setlocale (LC_ALL, "C"); |
|
558 Critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno))); |
|
559 } |
|
560 |
|
561 g_loadingMainFile = false; |
|
562 return; |
|
563 } |
|
564 |
|
565 file->openForEditing(); |
|
566 g_win->closeInitialDocument(); |
|
567 g_win->changeDocument (file); |
|
568 g_win->doFullRefresh(); |
|
569 AddRecentFile (path); |
|
570 g_loadingMainFile = false; |
|
571 |
|
572 // If there were problems loading subfile references, try see if we can find these |
|
573 // files on the parts tracker. |
|
574 QStringList unknowns; |
|
575 |
|
576 for (LDObject* obj : file->objects()) |
|
577 { |
|
578 if (obj->type() != OBJ_Error or static_cast<LDError*> (obj)->fileReferenced().isEmpty()) |
|
579 continue; |
|
580 |
|
581 unknowns << static_cast<LDError*> (obj)->fileReferenced(); |
|
582 } |
|
583 |
|
584 if (g_win->configBag()->tryDownloadMissingFiles() and not unknowns.isEmpty()) |
|
585 { |
|
586 PartDownloader dl (g_win); |
|
587 dl.setSourceType (PartDownloader::PartsTracker); |
|
588 dl.setPrimaryFile (file); |
|
589 |
|
590 for (QString const& unknown : unknowns) |
|
591 dl.downloadFromPartsTracker (unknown); |
|
592 |
|
593 dl.exec(); |
|
594 dl.checkIfFinished(); |
|
595 file->reloadAllSubfiles(); |
|
596 } |
|
597 } |
249 } |
598 |
250 |
599 // ============================================================================= |
251 // ============================================================================= |
600 // |
252 // |
601 bool LDDocument::save (QString path, int64* sizeptr) |
253 bool LDDocument::save (QString path, int64* sizeptr) |
1167 |
792 |
1168 // ============================================================================= |
793 // ============================================================================= |
1169 // ----------------------------------------------------------------------------- |
794 // ----------------------------------------------------------------------------- |
1170 LDObjectList LDDocument::inlineContents (bool deep, bool renderinline) |
795 LDObjectList LDDocument::inlineContents (bool deep, bool renderinline) |
1171 { |
796 { |
1172 // Possibly substitute with logoed studs: |
|
1173 // stud.dat -> stud-logo.dat |
|
1174 // stud2.dat -> stud-logo2.dat |
|
1175 if (m_config->useLogoStuds() and renderinline) |
|
1176 { |
|
1177 // Ensure logoed studs are loaded first |
|
1178 LoadLogoStuds(); |
|
1179 |
|
1180 if (name() == "stud.dat" and g_logoedStud) |
|
1181 return g_logoedStud->inlineContents (deep, renderinline); |
|
1182 else if (name() == "stud2.dat" and g_logoedStud2) |
|
1183 return g_logoedStud2->inlineContents (deep, renderinline); |
|
1184 } |
|
1185 |
|
1186 LDObjectList objs, objcache; |
797 LDObjectList objs, objcache; |
|
798 |
|
799 if (m_manager->preInline (this, objs)) |
|
800 return objs; // Manager dealt with this inline |
1187 |
801 |
1188 for (LDObject* obj : objects()) |
802 for (LDObject* obj : objects()) |
1189 { |
803 { |
1190 // Skip those without scemantic meaning |
804 // Skip those without scemantic meaning |
1191 if (not obj->isScemantic()) |
805 if (not obj->isScemantic()) |