|
1 /* |
|
2 * LDForge: LDraw parts authoring CAD |
|
3 * Copyright (C) 2013, 2014 Santeri 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 <QMessageBox> |
|
20 #include <QFileDialog> |
|
21 #include <QDir> |
|
22 #include <QApplication> |
|
23 #include "main.h" |
|
24 #include "configuration.h" |
|
25 #include "ldDocument.h" |
|
26 #include "miscallenous.h" |
|
27 #include "mainWindow.h" |
|
28 #include "editHistory.h" |
|
29 #include "dialogs.h" |
|
30 #include "glRenderer.h" |
|
31 #include "misc/invokeLater.h" |
|
32 |
|
33 cfg (String, io_ldpath, ""); |
|
34 cfg (List, io_recentfiles, {}); |
|
35 extern_cfg (String, net_downloadpath); |
|
36 extern_cfg (Bool, gl_logostuds); |
|
37 |
|
38 static bool g_loadingMainFile = false; |
|
39 static const int g_maxRecentFiles = 10; |
|
40 static bool g_aborted = false; |
|
41 static LDDocumentPointer g_logoedStud = null; |
|
42 static LDDocumentPointer g_logoedStud2 = null; |
|
43 |
|
44 LDDocument* LDDocument::m_curdoc = null; |
|
45 |
|
46 const QStringList g_specialSubdirectories ({ "s", "48", "8" }); |
|
47 |
|
48 // ============================================================================= |
|
49 // |
|
50 namespace LDPaths |
|
51 { |
|
52 static QString pathError; |
|
53 |
|
54 struct |
|
55 { |
|
56 QString LDConfigPath; |
|
57 QString partsPath, primsPath; |
|
58 } pathInfo; |
|
59 |
|
60 void initPaths() |
|
61 { |
|
62 if (!tryConfigure (io_ldpath)) |
|
63 { |
|
64 LDrawPathDialog dlg (false); |
|
65 |
|
66 if (!dlg.exec()) |
|
67 exit (0); |
|
68 |
|
69 io_ldpath = dlg.filename(); |
|
70 } |
|
71 } |
|
72 |
|
73 bool tryConfigure (QString path) |
|
74 { |
|
75 QDir dir; |
|
76 |
|
77 if (!dir.cd (path)) |
|
78 { |
|
79 pathError = "Directory does not exist."; |
|
80 return false; |
|
81 } |
|
82 |
|
83 QStringList mustHave = { "LDConfig.ldr", "parts", "p" }; |
|
84 QStringList contents = dir.entryList (mustHave); |
|
85 |
|
86 if (contents.size() != mustHave.size()) |
|
87 { |
|
88 pathError = "Not an LDraw directory! Must<br />have LDConfig.ldr, parts/ and p/."; |
|
89 return false; |
|
90 } |
|
91 |
|
92 pathInfo.partsPath = format ("%1" DIRSLASH "parts", path); |
|
93 pathInfo.LDConfigPath = format ("%1" DIRSLASH "LDConfig.ldr", path); |
|
94 pathInfo.primsPath = format ("%1" DIRSLASH "p", path); |
|
95 |
|
96 return true; |
|
97 } |
|
98 |
|
99 // Accessors |
|
100 QString getError() |
|
101 { |
|
102 return pathError; |
|
103 } |
|
104 |
|
105 QString ldconfig() |
|
106 { |
|
107 return pathInfo.LDConfigPath; |
|
108 } |
|
109 |
|
110 QString prims() |
|
111 { |
|
112 return pathInfo.primsPath; |
|
113 } |
|
114 |
|
115 QString parts() |
|
116 { |
|
117 return pathInfo.partsPath; |
|
118 } |
|
119 } |
|
120 |
|
121 // ============================================================================= |
|
122 // |
|
123 LDDocument::LDDocument() : |
|
124 m_gldata (new LDGLData) |
|
125 { |
|
126 setImplicit (true); |
|
127 setSavePosition (-1); |
|
128 setTabIndex (-1); |
|
129 setHistory (new History); |
|
130 history()->setDocument (this); |
|
131 } |
|
132 |
|
133 // ============================================================================= |
|
134 // |
|
135 LDDocument::~LDDocument() |
|
136 { |
|
137 // Remove this file from the list of files. This MUST be done FIRST, otherwise |
|
138 // a ton of other functions will think this file is still valid when it is not! |
|
139 g_loadedFiles.removeOne (this); |
|
140 |
|
141 m_history->setIgnoring (true); |
|
142 |
|
143 // Clear everything from the model |
|
144 for (LDObject* obj : objects()) |
|
145 obj->destroy(); |
|
146 |
|
147 // Clear the cache as well |
|
148 for (LDObject* obj : cache()) |
|
149 obj->destroy(); |
|
150 |
|
151 delete m_history; |
|
152 delete m_gldata; |
|
153 |
|
154 // If we just closed the current file, we need to set the current |
|
155 // file as something else. |
|
156 if (this == getCurrentDocument()) |
|
157 { |
|
158 bool found = false; |
|
159 |
|
160 // Try find an explicitly loaded file - if we can't find one, |
|
161 // we need to create a new file to switch to. |
|
162 for (LDDocument* file : g_loadedFiles) |
|
163 { |
|
164 if (!file->isImplicit()) |
|
165 { |
|
166 LDDocument::setCurrent (file); |
|
167 found = true; |
|
168 break; |
|
169 } |
|
170 } |
|
171 |
|
172 if (!found) |
|
173 newFile(); |
|
174 } |
|
175 |
|
176 if (this == g_logoedStud) |
|
177 g_logoedStud = null; |
|
178 elif (this == g_logoedStud2) |
|
179 g_logoedStud2 = null; |
|
180 |
|
181 g_win->updateDocumentList(); |
|
182 print ("Closed %1", name()); |
|
183 } |
|
184 |
|
185 // ============================================================================= |
|
186 // |
|
187 LDDocument* findDocument (QString name) |
|
188 { |
|
189 for (LDDocument * file : g_loadedFiles) |
|
190 if (!file->name().isEmpty() && file->name() == name) |
|
191 return file; |
|
192 |
|
193 return null; |
|
194 } |
|
195 |
|
196 // ============================================================================= |
|
197 // |
|
198 QString dirname (QString path) |
|
199 { |
|
200 long lastpos = path.lastIndexOf (DIRSLASH); |
|
201 |
|
202 if (lastpos > 0) |
|
203 return path.left (lastpos); |
|
204 |
|
205 #ifndef _WIN32 |
|
206 if (path[0] == DIRSLASH_CHAR) |
|
207 return DIRSLASH; |
|
208 #endif // _WIN32 |
|
209 |
|
210 return ""; |
|
211 } |
|
212 |
|
213 // ============================================================================= |
|
214 // |
|
215 QString basename (QString path) |
|
216 { |
|
217 long lastpos = path.lastIndexOf (DIRSLASH); |
|
218 |
|
219 if (lastpos != -1) |
|
220 return path.mid (lastpos + 1); |
|
221 |
|
222 return path; |
|
223 } |
|
224 |
|
225 // ============================================================================= |
|
226 // |
|
227 static QString findLDrawFilePath (QString relpath, bool subdirs) |
|
228 { |
|
229 QString fullPath; |
|
230 |
|
231 // LDraw models use Windows-style path separators. If we're not on Windows, |
|
232 // replace the path separator now before opening any files. Qt expects |
|
233 // forward-slashes as directory separators. |
|
234 #ifndef WIN32 |
|
235 relpath.replace ("\\", "/"); |
|
236 #endif // WIN32 |
|
237 |
|
238 // Try find it relative to other currently open documents. We want a file |
|
239 // in the immediate vicinity of a current model to override stock LDraw stuff. |
|
240 QString reltop = basename (dirname (relpath)); |
|
241 |
|
242 for (LDDocument* doc : g_loadedFiles) |
|
243 { |
|
244 if (doc->fullPath().isEmpty()) |
|
245 continue; |
|
246 |
|
247 QString partpath = format ("%1/%2", dirname (doc->fullPath()), relpath); |
|
248 QFile f (partpath); |
|
249 |
|
250 if (f.exists()) |
|
251 { |
|
252 // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48 |
|
253 QString proptop = basename (dirname (partpath)); |
|
254 |
|
255 bool bogus = false; |
|
256 |
|
257 for (QString s : g_specialSubdirectories) |
|
258 { |
|
259 if ((proptop == s && reltop != s) || (reltop == s && proptop != s)) |
|
260 { |
|
261 bogus = true; |
|
262 break; |
|
263 } |
|
264 } |
|
265 |
|
266 if (!bogus) |
|
267 return partpath; |
|
268 } |
|
269 } |
|
270 |
|
271 if (QFile::exists (relpath)) |
|
272 return relpath; |
|
273 |
|
274 // Try with just the LDraw path first |
|
275 fullPath = format ("%1" DIRSLASH "%2", io_ldpath, relpath); |
|
276 |
|
277 if (QFile::exists (fullPath)) |
|
278 return fullPath; |
|
279 |
|
280 if (subdirs) |
|
281 { |
|
282 // Look in sub-directories: parts and p. Also look in net_downloadpath, since that's |
|
283 // where we download parts from the PT to. |
|
284 for (const QString& topdir : QList<QString> ({ io_ldpath, net_downloadpath })) |
|
285 { |
|
286 for (const QString& subdir : QList<QString> ({ "parts", "p" })) |
|
287 { |
|
288 fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath); |
|
289 |
|
290 if (QFile::exists (fullPath)) |
|
291 return fullPath; |
|
292 } |
|
293 } |
|
294 } |
|
295 |
|
296 // Did not find the file. |
|
297 return ""; |
|
298 } |
|
299 |
|
300 QFile* openLDrawFile (QString relpath, bool subdirs, QString* pathpointer) |
|
301 { |
|
302 print ("Opening %1...\n", relpath); |
|
303 QString path = findLDrawFilePath (relpath, subdirs); |
|
304 |
|
305 if (pathpointer != null) |
|
306 *pathpointer = path; |
|
307 |
|
308 if (path.isEmpty()) |
|
309 return null; |
|
310 |
|
311 QFile* fp = new QFile (path); |
|
312 |
|
313 if (fp->open (QIODevice::ReadOnly)) |
|
314 return fp; |
|
315 |
|
316 fp->deleteLater(); |
|
317 return null; |
|
318 } |
|
319 |
|
320 // ============================================================================= |
|
321 // |
|
322 void LDFileLoader::start() |
|
323 { |
|
324 setDone (false); |
|
325 setProgress (0); |
|
326 setAborted (false); |
|
327 |
|
328 if (isOnForeground()) |
|
329 { |
|
330 g_aborted = false; |
|
331 |
|
332 // Show a progress dialog if we're loading the main ldDocument.here so we can |
|
333 // show progress updates and keep the WM posted that we're still here. |
|
334 // Of course we cannot exec() the dialog because then the dialog would |
|
335 // block. |
|
336 dlg = new OpenProgressDialog (g_win); |
|
337 dlg->setNumLines (lines().size()); |
|
338 dlg->setModal (true); |
|
339 dlg->show(); |
|
340 |
|
341 // Connect the loader in so we can show updates |
|
342 connect (this, SIGNAL (workDone()), dlg, SLOT (accept())); |
|
343 connect (dlg, SIGNAL (rejected()), this, SLOT (abort())); |
|
344 } |
|
345 else |
|
346 dlg = null; |
|
347 |
|
348 // Begin working |
|
349 work (0); |
|
350 } |
|
351 |
|
352 // ============================================================================= |
|
353 // |
|
354 void LDFileLoader::work (int i) |
|
355 { |
|
356 // User wishes to abort, so stop here now. |
|
357 if (isAborted()) |
|
358 { |
|
359 for (LDObject* obj : m_objects) |
|
360 obj->destroy(); |
|
361 |
|
362 m_objects.clear(); |
|
363 setDone (true); |
|
364 return; |
|
365 } |
|
366 |
|
367 // Parse up to 300 lines per iteration |
|
368 int max = i + 300; |
|
369 |
|
370 for (; i < max && i < (int) lines().size(); ++i) |
|
371 { |
|
372 QString line = lines()[i]; |
|
373 |
|
374 // Trim the trailing newline |
|
375 QChar c; |
|
376 |
|
377 while (line.endsWith ("\n") || line.endsWith ("\r")) |
|
378 line.chop (1); |
|
379 |
|
380 LDObject* obj = parseLine (line); |
|
381 |
|
382 // Check for parse errors and warn about tthem |
|
383 if (obj->type() == LDObject::EError) |
|
384 { |
|
385 print ("Couldn't parse line #%1: %2", progress() + 1, static_cast<LDError*> (obj)->reason()); |
|
386 |
|
387 if (warnings() != null) |
|
388 (*warnings())++; |
|
389 } |
|
390 |
|
391 m_objects << obj; |
|
392 setProgress (i); |
|
393 |
|
394 // If we have a dialog pointer, update the progress now |
|
395 if (isOnForeground()) |
|
396 dlg->updateProgress (i); |
|
397 } |
|
398 |
|
399 // If we're done now, tell the environment we're done and stop. |
|
400 if (i >= ((int) lines().size()) - 1) |
|
401 { |
|
402 emit workDone(); |
|
403 setDone (true); |
|
404 return; |
|
405 } |
|
406 |
|
407 // Otherwise, continue, by recursing back. |
|
408 if (!isDone()) |
|
409 { |
|
410 // If we have a dialog to show progress output to, we cannot just call |
|
411 // work() again immediately as the dialog needs some processor cycles as |
|
412 // well. Thus, take a detour through the event loop by using the |
|
413 // meta-object system. |
|
414 // |
|
415 // This terminates the loop here and control goes back to the function |
|
416 // which called the file loader. It will keep processing the event loop |
|
417 // until we're ready (see loadFileContents), thus the event loop will |
|
418 // eventually catch the invokation we throw here and send us back. Though |
|
419 // it's not technically recursion anymore, more like a for loop. :P |
|
420 if (isOnForeground()) |
|
421 QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i)); |
|
422 else |
|
423 work (i); |
|
424 } |
|
425 } |
|
426 |
|
427 // ============================================================================= |
|
428 // |
|
429 void LDFileLoader::abort() |
|
430 { |
|
431 setAborted (true); |
|
432 |
|
433 if (isOnForeground()) |
|
434 g_aborted = true; |
|
435 } |
|
436 |
|
437 // ============================================================================= |
|
438 // |
|
439 LDObjectList loadFileContents (QFile* fp, int* numWarnings, bool* ok) |
|
440 { |
|
441 QStringList lines; |
|
442 LDObjectList objs; |
|
443 |
|
444 if (numWarnings) |
|
445 *numWarnings = 0; |
|
446 |
|
447 // Read in the lines |
|
448 while (fp->atEnd() == false) |
|
449 lines << QString::fromUtf8 (fp->readLine()); |
|
450 |
|
451 LDFileLoader* loader = new LDFileLoader; |
|
452 loader->setWarnings (numWarnings); |
|
453 loader->setLines (lines); |
|
454 loader->setOnForeground (g_loadingMainFile); |
|
455 loader->start(); |
|
456 |
|
457 // After start() returns, if the loader isn't done yet, it's delaying |
|
458 // its next iteration through the event loop. We need to catch this here |
|
459 // by telling the event loop to tick, which will tick the file loader again. |
|
460 // We keep doing this until the file loader is ready. |
|
461 while (loader->isDone() == false) |
|
462 qApp->processEvents(); |
|
463 |
|
464 // If we wanted the success value, supply that now |
|
465 if (ok) |
|
466 *ok = !loader->isAborted(); |
|
467 |
|
468 objs = loader->objects(); |
|
469 return objs; |
|
470 } |
|
471 |
|
472 // ============================================================================= |
|
473 // |
|
474 LDDocument* openDocument (QString path, bool search) |
|
475 { |
|
476 // Convert the file name to lowercase since some parts contain uppercase |
|
477 // file names. I'll assume here that the library will always use lowercase |
|
478 // file names for the actual parts.. |
|
479 QFile* fp; |
|
480 QString fullpath; |
|
481 |
|
482 if (search) |
|
483 fp = openLDrawFile (path.toLower(), true, &fullpath); |
|
484 else |
|
485 { |
|
486 fp = new QFile (path); |
|
487 fullpath = path; |
|
488 |
|
489 if (!fp->open (QIODevice::ReadOnly)) |
|
490 { |
|
491 delete fp; |
|
492 return null; |
|
493 } |
|
494 } |
|
495 |
|
496 if (!fp) |
|
497 return null; |
|
498 |
|
499 LDDocument* load = new LDDocument; |
|
500 load->setFullPath (fullpath); |
|
501 load->setName (LDDocument::shortenName (load->fullPath())); |
|
502 dprint ("name: %1 (%2)", load->name(), load->fullPath()); |
|
503 g_loadedFiles << load; |
|
504 |
|
505 // Don't take the file loading as actual edits to the file |
|
506 load->history()->setIgnoring (true); |
|
507 |
|
508 int numWarnings; |
|
509 bool ok; |
|
510 LDObjectList objs = loadFileContents (fp, &numWarnings, &ok); |
|
511 fp->close(); |
|
512 fp->deleteLater(); |
|
513 |
|
514 if (!ok) |
|
515 { |
|
516 g_loadedFiles.removeOne (load); |
|
517 delete load; |
|
518 return null; |
|
519 } |
|
520 |
|
521 load->addObjects (objs); |
|
522 |
|
523 if (g_loadingMainFile) |
|
524 { |
|
525 LDDocument::setCurrent (load); |
|
526 g_win->R()->setDocument (load); |
|
527 print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); |
|
528 } |
|
529 |
|
530 load->history()->setIgnoring (false); |
|
531 return load; |
|
532 } |
|
533 |
|
534 // ============================================================================= |
|
535 // |
|
536 bool LDDocument::isSafeToClose() |
|
537 { |
|
538 typedef QMessageBox msgbox; |
|
539 setlocale (LC_ALL, "C"); |
|
540 |
|
541 // If we have unsaved changes, warn and give the option of saving. |
|
542 if (hasUnsavedChanges()) |
|
543 { |
|
544 QString message = format (tr ("There are unsaved changes to %1. Should it be saved?"), |
|
545 (name().length() > 0) ? name() : tr ("<anonymous>")); |
|
546 |
|
547 int button = msgbox::question (g_win, tr ("Unsaved Changes"), message, |
|
548 (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel); |
|
549 |
|
550 switch (button) |
|
551 { |
|
552 case msgbox::Yes: |
|
553 { |
|
554 // If we don't have a file path yet, we have to ask the user for one. |
|
555 if (name().length() == 0) |
|
556 { |
|
557 QString newpath = QFileDialog::getSaveFileName (g_win, tr ("Save As"), |
|
558 getCurrentDocument()->name(), tr ("LDraw files (*.dat *.ldr)")); |
|
559 |
|
560 if (newpath.length() == 0) |
|
561 return false; |
|
562 |
|
563 setName (newpath); |
|
564 } |
|
565 |
|
566 if (!save()) |
|
567 { |
|
568 message = format (tr ("Failed to save %1 (%2)\nDo you still want to close?"), |
|
569 name(), strerror (errno)); |
|
570 |
|
571 if (msgbox::critical (g_win, tr ("Save Failure"), message, |
|
572 (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No) |
|
573 { |
|
574 return false; |
|
575 } |
|
576 } |
|
577 } break; |
|
578 |
|
579 case msgbox::Cancel: |
|
580 return false; |
|
581 |
|
582 default: |
|
583 break; |
|
584 } |
|
585 } |
|
586 |
|
587 return true; |
|
588 } |
|
589 |
|
590 // ============================================================================= |
|
591 // |
|
592 void closeAll() |
|
593 { |
|
594 // Remove all loaded files and the objects they contain |
|
595 QList<LDDocument*> files = g_loadedFiles; |
|
596 |
|
597 for (LDDocument* file : files) |
|
598 delete file; |
|
599 } |
|
600 |
|
601 // ============================================================================= |
|
602 // |
|
603 void newFile() |
|
604 { |
|
605 // Create a new anonymous file and set it to our current |
|
606 LDDocument* f = new LDDocument; |
|
607 f->setName (""); |
|
608 f->setImplicit (false); |
|
609 g_loadedFiles << f; |
|
610 LDDocument::setCurrent (f); |
|
611 LDDocument::closeInitialFile(); |
|
612 g_win->R()->setDocument (f); |
|
613 g_win->doFullRefresh(); |
|
614 g_win->updateTitle(); |
|
615 g_win->updateActions(); |
|
616 } |
|
617 |
|
618 // ============================================================================= |
|
619 // |
|
620 void addRecentFile (QString path) |
|
621 { |
|
622 auto& rfiles = io_recentfiles; |
|
623 int idx = rfiles.indexOf (path); |
|
624 |
|
625 // If this file already is in the list, pop it out. |
|
626 if (idx != -1) |
|
627 { |
|
628 if (rfiles.size() == 1) |
|
629 return; // only recent file - abort and do nothing |
|
630 |
|
631 // Pop it out. |
|
632 rfiles.removeAt (idx); |
|
633 } |
|
634 |
|
635 // If there's too many recent files, drop one out. |
|
636 while (rfiles.size() > (g_maxRecentFiles - 1)) |
|
637 rfiles.removeAt (0); |
|
638 |
|
639 // Add the file |
|
640 rfiles << path; |
|
641 |
|
642 Config::save(); |
|
643 g_win->updateRecentFilesMenu(); |
|
644 } |
|
645 |
|
646 // ============================================================================= |
|
647 // Open an LDraw file and set it as the main model |
|
648 // ============================================================================= |
|
649 void openMainFile (QString path) |
|
650 { |
|
651 g_loadingMainFile = true; |
|
652 |
|
653 // If there's already a file with the same name, this file must replace it. |
|
654 LDDocument* documentToReplace = null; |
|
655 QString shortName = LDDocument::shortenName (path); |
|
656 |
|
657 for (LDDocument* doc : g_loadedFiles) |
|
658 { |
|
659 if (doc->name() == shortName) |
|
660 { |
|
661 documentToReplace = doc; |
|
662 break; |
|
663 } |
|
664 } |
|
665 |
|
666 // We cannot open this file if the document this would replace is not |
|
667 // safe to close. |
|
668 if (documentToReplace != null && documentToReplace->isSafeToClose() == false) |
|
669 { |
|
670 g_loadingMainFile = false; |
|
671 return; |
|
672 } |
|
673 |
|
674 LDDocument* file = openDocument (path, false); |
|
675 |
|
676 if (!file) |
|
677 { |
|
678 // Loading failed, thus drop down to a new file since we |
|
679 // closed everything prior. |
|
680 newFile(); |
|
681 |
|
682 if (!g_aborted) |
|
683 { |
|
684 // Tell the user loading failed. |
|
685 setlocale (LC_ALL, "C"); |
|
686 critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno))); |
|
687 } |
|
688 |
|
689 g_loadingMainFile = false; |
|
690 return; |
|
691 } |
|
692 |
|
693 file->setImplicit (false); |
|
694 |
|
695 // Replace references to the old file with the new file. |
|
696 if (documentToReplace != null) |
|
697 { |
|
698 for (LDDocumentPointer* ptr : documentToReplace->references()) |
|
699 { dprint ("ptr: %1 (%2)\n", |
|
700 ptr, ptr->pointer() ? ptr->pointer()->name() : "<null>"); |
|
701 |
|
702 *ptr = file; |
|
703 } |
|
704 |
|
705 assert (documentToReplace->references().isEmpty()); |
|
706 delete documentToReplace; |
|
707 } |
|
708 |
|
709 // If we have an anonymous, unchanged file open as the only open file |
|
710 // (aside of the one we just opened), close it now. |
|
711 LDDocument::closeInitialFile(); |
|
712 |
|
713 // Rebuild the object tree view now. |
|
714 LDDocument::setCurrent (file); |
|
715 g_win->doFullRefresh(); |
|
716 |
|
717 // Add it to the recent files list. |
|
718 addRecentFile (path); |
|
719 g_loadingMainFile = false; |
|
720 } |
|
721 |
|
722 // ============================================================================= |
|
723 // |
|
724 bool LDDocument::save (QString savepath) |
|
725 { |
|
726 if (!savepath.length()) |
|
727 savepath = fullPath(); |
|
728 |
|
729 QFile f (savepath); |
|
730 |
|
731 if (!f.open (QIODevice::WriteOnly)) |
|
732 return false; |
|
733 |
|
734 // If the second object in the list holds the file name, update that now. |
|
735 // Only do this if the file is explicitly open. |
|
736 LDObject* nameObject = getObject (1); |
|
737 |
|
738 if (!isImplicit() && nameObject != null && nameObject->type() == LDObject::EComment) |
|
739 { |
|
740 LDComment* nameComment = static_cast<LDComment*> (nameObject); |
|
741 |
|
742 if (nameComment->text().left (6) == "Name: ") |
|
743 { |
|
744 QString newname = shortenName (savepath); |
|
745 nameComment->setText (format ("Name: %1", newname)); |
|
746 g_win->buildObjList(); |
|
747 } |
|
748 } |
|
749 |
|
750 // File is open, now save the model to it. Note that LDraw requires files to |
|
751 // have DOS line endings, so we terminate the lines with \r\n. |
|
752 for (LDObject* obj : objects()) |
|
753 f.write ((obj->asText() + "\r\n").toUtf8()); |
|
754 |
|
755 // File is saved, now clean up. |
|
756 f.close(); |
|
757 |
|
758 // We have successfully saved, update the save position now. |
|
759 setSavePosition (history()->position()); |
|
760 setFullPath (savepath); |
|
761 setName (shortenName (savepath)); |
|
762 |
|
763 g_win->updateDocumentListItem (this); |
|
764 g_win->updateTitle(); |
|
765 return true; |
|
766 } |
|
767 |
|
768 // ============================================================================= |
|
769 // |
|
770 class LDParseError : public std::exception |
|
771 { |
|
772 PROPERTY (private, QString, error, setError, STOCK_WRITE) |
|
773 PROPERTY (private, QString, line, setLine, STOCK_WRITE) |
|
774 |
|
775 public: |
|
776 LDParseError (QString line, QString a) : |
|
777 m_error (a), |
|
778 m_line (line) {} |
|
779 |
|
780 const char* what() const throw() |
|
781 { |
|
782 return qPrintable (error()); |
|
783 } |
|
784 }; |
|
785 |
|
786 // ============================================================================= |
|
787 // |
|
788 void checkTokenCount (QString line, const QStringList& tokens, int num) |
|
789 { |
|
790 if (tokens.size() != num) |
|
791 throw LDParseError (line, format ("Bad amount of tokens, expected %1, got %2", num, tokens.size())); |
|
792 } |
|
793 |
|
794 // ============================================================================= |
|
795 // |
|
796 void checkTokenNumbers (QString line, const QStringList& tokens, int min, int max) |
|
797 { |
|
798 bool ok; |
|
799 |
|
800 // Check scientific notation, e.g. 7.99361e-15 |
|
801 QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+"); |
|
802 |
|
803 for (int i = min; i <= max; ++i) |
|
804 { |
|
805 tokens[i].toDouble (&ok); |
|
806 |
|
807 if (!ok && !scient.exactMatch (tokens[i])) |
|
808 throw LDParseError (line, format ("Token #%1 was `%2`, expected a number (matched length: %3)", (i + 1), tokens[i], scient.matchedLength())); |
|
809 } |
|
810 } |
|
811 |
|
812 // ============================================================================= |
|
813 // |
|
814 static Vertex parseVertex (QStringList& s, const int n) |
|
815 { |
|
816 Vertex v; |
|
817 |
|
818 for_axes (ax) |
|
819 v[ax] = s[n + ax].toDouble(); |
|
820 |
|
821 return v; |
|
822 } |
|
823 |
|
824 // ============================================================================= |
|
825 // This is the LDraw code parser function. It takes in a string containing LDraw |
|
826 // code and returns the object parsed from it. parseLine never returns null, |
|
827 // the object will be LDError if it could not be parsed properly. |
|
828 // ============================================================================= |
|
829 LDObject* parseLine (QString line) |
|
830 { |
|
831 try |
|
832 { |
|
833 QStringList tokens = line.split (" ", QString::SkipEmptyParts); |
|
834 |
|
835 if (tokens.size() <= 0) |
|
836 { |
|
837 // Line was empty, or only consisted of whitespace |
|
838 return new LDEmpty; |
|
839 } |
|
840 |
|
841 if (tokens[0].length() != 1 || tokens[0][0].isDigit() == false) |
|
842 throw LDParseError (line, "Illogical line code"); |
|
843 |
|
844 int num = tokens[0][0].digitValue(); |
|
845 |
|
846 switch (num) |
|
847 { |
|
848 case 0: |
|
849 { |
|
850 // Comment |
|
851 QString comm = line.mid (line.indexOf ("0") + 1).simplified(); |
|
852 |
|
853 // Handle BFC statements |
|
854 if (tokens.size() > 2 && tokens[1] == "BFC") |
|
855 { |
|
856 for (int i = 0; i < LDBFC::NumStatements; ++i) |
|
857 if (comm == format ("BFC %1", LDBFC::k_statementStrings [i])) |
|
858 return new LDBFC ( (LDBFC::Statement) i); |
|
859 |
|
860 // MLCAD is notorious for stuffing these statements in parts it |
|
861 // creates. The above block only handles valid statements, so we |
|
862 // need to handle MLCAD-style invertnext, clip and noclip separately. |
|
863 struct |
|
864 { |
|
865 QString a; |
|
866 LDBFC::Statement b; |
|
867 } BFCData[] = |
|
868 { |
|
869 { "INVERTNEXT", LDBFC::InvertNext }, |
|
870 { "NOCLIP", LDBFC::NoClip }, |
|
871 { "CLIP", LDBFC::Clip } |
|
872 }; |
|
873 |
|
874 for (const auto& i : BFCData) |
|
875 if (comm == "BFC CERTIFY " + i.a) |
|
876 return new LDBFC (i.b); |
|
877 } |
|
878 |
|
879 if (tokens.size() > 2 && tokens[1] == "!LDFORGE") |
|
880 { |
|
881 // Handle LDForge-specific types, they're embedded into comments too |
|
882 if (tokens[2] == "VERTEX") |
|
883 { |
|
884 // Vertex (0 !LDFORGE VERTEX) |
|
885 checkTokenCount (line, tokens, 7); |
|
886 checkTokenNumbers (line, tokens, 3, 6); |
|
887 |
|
888 LDVertex* obj = new LDVertex; |
|
889 obj->setColor (tokens[3].toLong()); |
|
890 |
|
891 for_axes (ax) |
|
892 obj->pos[ax] = tokens[4 + ax].toDouble(); // 4 - 6 |
|
893 |
|
894 return obj; |
|
895 } elif (tokens[2] == "OVERLAY") |
|
896 { |
|
897 checkTokenCount (line, tokens, 9);; |
|
898 checkTokenNumbers (line, tokens, 5, 8); |
|
899 |
|
900 LDOverlay* obj = new LDOverlay; |
|
901 obj->setFileName (tokens[3]); |
|
902 obj->setCamera (tokens[4].toLong()); |
|
903 obj->setX (tokens[5].toLong()); |
|
904 obj->setY (tokens[6].toLong()); |
|
905 obj->setWidth (tokens[7].toLong()); |
|
906 obj->setHeight (tokens[8].toLong()); |
|
907 return obj; |
|
908 } |
|
909 } |
|
910 |
|
911 // Just a regular comment: |
|
912 LDComment* obj = new LDComment; |
|
913 obj->setText (comm); |
|
914 return obj; |
|
915 } |
|
916 |
|
917 case 1: |
|
918 { |
|
919 // Subfile |
|
920 checkTokenCount (line, tokens, 15); |
|
921 checkTokenNumbers (line, tokens, 1, 13); |
|
922 |
|
923 // Try open the file. Disable g_loadingMainFile temporarily since we're |
|
924 // not loading the main file now, but the subfile in question. |
|
925 bool tmp = g_loadingMainFile; |
|
926 g_loadingMainFile = false; |
|
927 LDDocument* load = getDocument (tokens[14]); |
|
928 g_loadingMainFile = tmp; |
|
929 |
|
930 // If we cannot open the file, mark it an error. Note we cannot use LDParseError |
|
931 // here because the error object needs the document reference. |
|
932 if (!load) |
|
933 { |
|
934 LDError* obj = new LDError (line, format ("Could not open %1", tokens[14])); |
|
935 obj->setFileReferenced (tokens[14]); |
|
936 return obj; |
|
937 } |
|
938 |
|
939 LDSubfile* obj = new LDSubfile; |
|
940 obj->setColor (tokens[1].toLong()); |
|
941 obj->setPosition (parseVertex (tokens, 2)); // 2 - 4 |
|
942 |
|
943 Matrix transform; |
|
944 |
|
945 for (int i = 0; i < 9; ++i) |
|
946 transform[i] = tokens[i + 5].toDouble(); // 5 - 13 |
|
947 |
|
948 obj->setTransform (transform); |
|
949 obj->setFileInfo (load); |
|
950 return obj; |
|
951 } |
|
952 |
|
953 case 2: |
|
954 { |
|
955 checkTokenCount (line, tokens, 8); |
|
956 checkTokenNumbers (line, tokens, 1, 7); |
|
957 |
|
958 // Line |
|
959 LDLine* obj = new LDLine; |
|
960 obj->setColor (tokens[1].toLong()); |
|
961 |
|
962 for (int i = 0; i < 2; ++i) |
|
963 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 7 |
|
964 |
|
965 return obj; |
|
966 } |
|
967 |
|
968 case 3: |
|
969 { |
|
970 checkTokenCount (line, tokens, 11); |
|
971 checkTokenNumbers (line, tokens, 1, 10); |
|
972 |
|
973 // Triangle |
|
974 LDTriangle* obj = new LDTriangle; |
|
975 obj->setColor (tokens[1].toLong()); |
|
976 |
|
977 for (int i = 0; i < 3; ++i) |
|
978 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 10 |
|
979 |
|
980 return obj; |
|
981 } |
|
982 |
|
983 case 4: |
|
984 case 5: |
|
985 { |
|
986 checkTokenCount (line, tokens, 14); |
|
987 checkTokenNumbers (line, tokens, 1, 13); |
|
988 |
|
989 // Quadrilateral / Conditional line |
|
990 LDObject* obj; |
|
991 |
|
992 if (num == 4) |
|
993 obj = new LDQuad; |
|
994 else |
|
995 obj = new LDCondLine; |
|
996 |
|
997 obj->setColor (tokens[1].toLong()); |
|
998 |
|
999 for (int i = 0; i < 4; ++i) |
|
1000 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13 |
|
1001 |
|
1002 return obj; |
|
1003 } |
|
1004 |
|
1005 default: // Strange line we couldn't parse |
|
1006 throw LDError (line, "Unknown line code number"); |
|
1007 } |
|
1008 } |
|
1009 catch (LDParseError& e) |
|
1010 { |
|
1011 return new LDError (e.line(), e.error()); |
|
1012 } |
|
1013 } |
|
1014 |
|
1015 // ============================================================================= |
|
1016 // |
|
1017 LDDocument* getDocument (QString filename) |
|
1018 { |
|
1019 // Try find the file in the list of loaded files |
|
1020 LDDocument* doc = findDocument (filename); |
|
1021 |
|
1022 // If it's not loaded, try open it |
|
1023 if (!doc) |
|
1024 doc = openDocument (filename, true); |
|
1025 |
|
1026 return doc; |
|
1027 } |
|
1028 |
|
1029 // ============================================================================= |
|
1030 // |
|
1031 void reloadAllSubfiles() |
|
1032 { |
|
1033 if (!getCurrentDocument()) |
|
1034 return; |
|
1035 |
|
1036 g_loadedFiles.clear(); |
|
1037 g_loadedFiles << getCurrentDocument(); |
|
1038 |
|
1039 // Go through all objects in the current file and reload the subfiles |
|
1040 for (LDObject* obj : getCurrentDocument()->objects()) |
|
1041 { |
|
1042 if (obj->type() == LDObject::ESubfile) |
|
1043 { |
|
1044 LDSubfile* ref = static_cast<LDSubfile*> (obj); |
|
1045 LDDocument* fileInfo = getDocument (ref->fileInfo()->name()); |
|
1046 |
|
1047 if (fileInfo) |
|
1048 ref->setFileInfo (fileInfo); |
|
1049 else |
|
1050 ref->replace (new LDError (ref->asText(), format ("Could not open %1", ref->fileInfo()->name()))); |
|
1051 } |
|
1052 |
|
1053 // Reparse gibberish files. It could be that they are invalid because |
|
1054 // of loading errors. Circumstances may be different now. |
|
1055 if (obj->type() == LDObject::EError) |
|
1056 obj->replace (parseLine (static_cast<LDError*> (obj)->contents())); |
|
1057 } |
|
1058 } |
|
1059 |
|
1060 // ============================================================================= |
|
1061 // |
|
1062 int LDDocument::addObject (LDObject* obj) |
|
1063 { |
|
1064 history()->add (new AddHistory (objects().size(), obj)); |
|
1065 m_objects << obj; |
|
1066 |
|
1067 if (obj->type() == LDObject::EVertex) |
|
1068 m_vertices << obj; |
|
1069 |
|
1070 #ifdef DEBUG |
|
1071 if (!isImplicit()) |
|
1072 dprint ("Added object #%1 (%2)\n", obj->id(), obj->typeName()); |
|
1073 #endif |
|
1074 |
|
1075 obj->setDocument (this); |
|
1076 return getObjectCount() - 1; |
|
1077 } |
|
1078 |
|
1079 // ============================================================================= |
|
1080 // |
|
1081 void LDDocument::addObjects (const LDObjectList objs) |
|
1082 { |
|
1083 for (LDObject* obj : objs) |
|
1084 if (obj) |
|
1085 addObject (obj); |
|
1086 } |
|
1087 |
|
1088 // ============================================================================= |
|
1089 // |
|
1090 void LDDocument::insertObj (int pos, LDObject* obj) |
|
1091 { |
|
1092 history()->add (new AddHistory (pos, obj)); |
|
1093 m_objects.insert (pos, obj); |
|
1094 obj->setDocument (this); |
|
1095 |
|
1096 #ifdef DEBUG |
|
1097 if (!isImplicit()) |
|
1098 dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos); |
|
1099 #endif |
|
1100 } |
|
1101 |
|
1102 // ============================================================================= |
|
1103 // |
|
1104 void LDDocument::forgetObject (LDObject* obj) |
|
1105 { |
|
1106 int idx = obj->lineNumber(); |
|
1107 obj->unselect(); |
|
1108 assert (m_objects[idx] == obj); |
|
1109 |
|
1110 if (!history()->isIgnoring()) |
|
1111 history()->add (new DelHistory (idx, obj)); |
|
1112 |
|
1113 m_objects.removeAt (idx); |
|
1114 obj->setDocument (null); |
|
1115 } |
|
1116 |
|
1117 // ============================================================================= |
|
1118 // |
|
1119 bool safeToCloseAll() |
|
1120 { |
|
1121 for (LDDocument* f : g_loadedFiles) |
|
1122 if (!f->isSafeToClose()) |
|
1123 return false; |
|
1124 |
|
1125 return true; |
|
1126 } |
|
1127 |
|
1128 // ============================================================================= |
|
1129 // |
|
1130 void LDDocument::setObject (int idx, LDObject* obj) |
|
1131 { |
|
1132 assert (idx >= 0 && idx < m_objects.size()); |
|
1133 |
|
1134 // Mark this change to history |
|
1135 if (!m_history->isIgnoring()) |
|
1136 { |
|
1137 QString oldcode = getObject (idx)->asText(); |
|
1138 QString newcode = obj->asText(); |
|
1139 *m_history << new EditHistory (idx, oldcode, newcode); |
|
1140 } |
|
1141 |
|
1142 m_objects[idx]->unselect(); |
|
1143 m_objects[idx]->setDocument (null); |
|
1144 obj->setDocument (this); |
|
1145 m_objects[idx] = obj; |
|
1146 } |
|
1147 |
|
1148 // ============================================================================= |
|
1149 // |
|
1150 // Close all documents we don't need anymore |
|
1151 // |
|
1152 void LDDocument::closeUnused() |
|
1153 { |
|
1154 for (LDDocument* file : g_loadedFiles) |
|
1155 if (file->isImplicit() && file->references().isEmpty()) |
|
1156 delete file; |
|
1157 } |
|
1158 |
|
1159 // ============================================================================= |
|
1160 // |
|
1161 LDObject* LDDocument::getObject (int pos) const |
|
1162 { |
|
1163 if (m_objects.size() <= pos) |
|
1164 return null; |
|
1165 |
|
1166 return m_objects[pos]; |
|
1167 } |
|
1168 |
|
1169 // ============================================================================= |
|
1170 // |
|
1171 int LDDocument::getObjectCount() const |
|
1172 { |
|
1173 return objects().size(); |
|
1174 } |
|
1175 |
|
1176 // ============================================================================= |
|
1177 // |
|
1178 bool LDDocument::hasUnsavedChanges() const |
|
1179 { |
|
1180 return !isImplicit() && history()->position() != savePosition(); |
|
1181 } |
|
1182 |
|
1183 // ============================================================================= |
|
1184 // |
|
1185 QString LDDocument::getDisplayName() |
|
1186 { |
|
1187 if (!name().isEmpty()) |
|
1188 return name(); |
|
1189 |
|
1190 if (!defaultName().isEmpty()) |
|
1191 return "[" + defaultName() + "]"; |
|
1192 |
|
1193 return tr ("<anonymous>"); |
|
1194 } |
|
1195 |
|
1196 // ============================================================================= |
|
1197 // |
|
1198 LDObjectList LDDocument::inlineContents (LDSubfile::InlineFlags flags) |
|
1199 { |
|
1200 // Possibly substitute with logoed studs: |
|
1201 // stud.dat -> stud-logo.dat |
|
1202 // stud2.dat -> stud-logo2.dat |
|
1203 if (gl_logostuds && (flags & LDSubfile::RendererInline)) |
|
1204 { |
|
1205 // Ensure logoed studs are loaded first |
|
1206 loadLogoedStuds(); |
|
1207 |
|
1208 if (name() == "stud.dat" && g_logoedStud) |
|
1209 return g_logoedStud->inlineContents (flags); |
|
1210 elif (name() == "stud2.dat" && g_logoedStud2) |
|
1211 return g_logoedStud2->inlineContents (flags); |
|
1212 } |
|
1213 |
|
1214 LDObjectList objs, objcache; |
|
1215 |
|
1216 bool deep = flags & LDSubfile::DeepInline, |
|
1217 doCache = flags & LDSubfile::CacheInline; |
|
1218 |
|
1219 if (m_needsCache) |
|
1220 { |
|
1221 m_cache.clear(); |
|
1222 doCache = true; |
|
1223 } |
|
1224 |
|
1225 // If we have this cached, just create a copy of that |
|
1226 if (deep && cache().isEmpty() == false) |
|
1227 { |
|
1228 for (LDObject* obj : cache()) |
|
1229 objs << obj->createCopy(); |
|
1230 } |
|
1231 else |
|
1232 { |
|
1233 if (!deep) |
|
1234 doCache = false; |
|
1235 |
|
1236 for (LDObject* obj : objects()) |
|
1237 { |
|
1238 // Skip those without scemantic meaning |
|
1239 if (!obj->isScemantic()) |
|
1240 continue; |
|
1241 |
|
1242 // Got another sub-file reference, inline it if we're deep-inlining. If not, |
|
1243 // just add it into the objects normally. Also, we only cache immediate |
|
1244 // subfiles and this is not one. Yay, recursion! |
|
1245 if (deep && obj->type() == LDObject::ESubfile) |
|
1246 { |
|
1247 LDSubfile* ref = static_cast<LDSubfile*> (obj); |
|
1248 |
|
1249 // We only want to cache immediate subfiles, so shed the caching |
|
1250 // flag when recursing deeper in hierarchy. |
|
1251 LDObjectList otherobjs = ref->inlineContents (flags & ~ (LDSubfile::CacheInline)); |
|
1252 |
|
1253 for (LDObject* otherobj : otherobjs) |
|
1254 { |
|
1255 // Cache this object, if desired |
|
1256 if (doCache) |
|
1257 objcache << otherobj->createCopy(); |
|
1258 |
|
1259 objs << otherobj; |
|
1260 } |
|
1261 } |
|
1262 else |
|
1263 { |
|
1264 if (doCache) |
|
1265 objcache << obj->createCopy(); |
|
1266 |
|
1267 objs << obj->createCopy(); |
|
1268 } |
|
1269 } |
|
1270 |
|
1271 if (doCache) |
|
1272 setCache (objcache); |
|
1273 } |
|
1274 |
|
1275 return objs; |
|
1276 } |
|
1277 |
|
1278 // ============================================================================= |
|
1279 // |
|
1280 LDDocument* LDDocument::current() |
|
1281 { |
|
1282 return m_curdoc; |
|
1283 } |
|
1284 |
|
1285 // ============================================================================= |
|
1286 // Sets the given file as the current one on display. At some point in time this |
|
1287 // was an operation completely unheard of. ;) |
|
1288 // |
|
1289 // TODO: f can be temporarily null. This probably should not be the case. |
|
1290 // ============================================================================= |
|
1291 void LDDocument::setCurrent (LDDocument* f) |
|
1292 { |
|
1293 // Implicit files were loaded for caching purposes and must never be set |
|
1294 // current. |
|
1295 if (f && f->isImplicit()) |
|
1296 return; |
|
1297 |
|
1298 m_curdoc = f; |
|
1299 |
|
1300 if (g_win && f) |
|
1301 { |
|
1302 // A ton of stuff needs to be updated |
|
1303 g_win->updateDocumentListItem (f); |
|
1304 g_win->buildObjList(); |
|
1305 g_win->updateTitle(); |
|
1306 g_win->R()->setDocument (f); |
|
1307 g_win->R()->repaint(); |
|
1308 print ("Changed file to %1", f->getDisplayName()); |
|
1309 } |
|
1310 } |
|
1311 |
|
1312 // ============================================================================= |
|
1313 // |
|
1314 int LDDocument::countExplicitFiles() |
|
1315 { |
|
1316 int count = 0; |
|
1317 |
|
1318 for (LDDocument* f : g_loadedFiles) |
|
1319 if (f->isImplicit() == false) |
|
1320 count++; |
|
1321 |
|
1322 return count; |
|
1323 } |
|
1324 |
|
1325 // ============================================================================= |
|
1326 // This little beauty closes the initial file that was open at first when opening |
|
1327 // a new file over it. |
|
1328 // ============================================================================= |
|
1329 void LDDocument::closeInitialFile() |
|
1330 { |
|
1331 if ( |
|
1332 countExplicitFiles() == 2 && |
|
1333 g_loadedFiles[0]->name().isEmpty() && |
|
1334 g_loadedFiles[1]->name().isEmpty() == false && |
|
1335 !g_loadedFiles[0]->hasUnsavedChanges() |
|
1336 ) |
|
1337 delete g_loadedFiles[0]; |
|
1338 } |
|
1339 |
|
1340 // ============================================================================= |
|
1341 // |
|
1342 void loadLogoedStuds() |
|
1343 { |
|
1344 if (g_logoedStud && g_logoedStud2) |
|
1345 return; |
|
1346 |
|
1347 delete g_logoedStud; |
|
1348 delete g_logoedStud2; |
|
1349 |
|
1350 g_logoedStud = openDocument ("stud-logo.dat", true); |
|
1351 g_logoedStud2 = openDocument ("stud2-logo.dat", true); |
|
1352 |
|
1353 print (LDDocument::tr ("Logoed studs loaded.\n")); |
|
1354 } |
|
1355 |
|
1356 // ============================================================================= |
|
1357 // |
|
1358 void LDDocument::addToSelection (LDObject* obj) // [protected] |
|
1359 { |
|
1360 if (obj->isSelected()) |
|
1361 return; |
|
1362 |
|
1363 assert (obj->document() == this); |
|
1364 m_sel << obj; |
|
1365 obj->setSelected (true); |
|
1366 } |
|
1367 |
|
1368 // ============================================================================= |
|
1369 // |
|
1370 void LDDocument::removeFromSelection (LDObject* obj) // [protected] |
|
1371 { |
|
1372 if (!obj->isSelected()) |
|
1373 return; |
|
1374 |
|
1375 assert (obj->document() == this); |
|
1376 m_sel.removeOne (obj); |
|
1377 obj->setSelected (false); |
|
1378 } |
|
1379 |
|
1380 // ============================================================================= |
|
1381 // |
|
1382 void LDDocument::clearSelection() |
|
1383 { |
|
1384 for (LDObject* obj : m_sel) |
|
1385 removeFromSelection (obj); |
|
1386 |
|
1387 assert (m_sel.isEmpty()); |
|
1388 } |
|
1389 |
|
1390 // ============================================================================= |
|
1391 // |
|
1392 const LDObjectList& LDDocument::getSelection() const |
|
1393 { |
|
1394 return m_sel; |
|
1395 } |
|
1396 |
|
1397 // ============================================================================= |
|
1398 // |
|
1399 void LDDocument::swapObjects (LDObject* one, LDObject* other) |
|
1400 { |
|
1401 int a = m_objects.indexOf (one); |
|
1402 int b = m_objects.indexOf (other); |
|
1403 assert (a != b && a != -1 && b != -1); |
|
1404 m_objects[b] = one; |
|
1405 m_objects[a] = other; |
|
1406 addToHistory (new SwapHistory (one->id(), other->id())); |
|
1407 } |
|
1408 |
|
1409 // ============================================================================= |
|
1410 // |
|
1411 QString LDDocument::shortenName (QString a) // [static] |
|
1412 { |
|
1413 QString shortname = basename (a); |
|
1414 QString topdirname = basename (dirname (a)); |
|
1415 |
|
1416 if (g_specialSubdirectories.contains (topdirname)) |
|
1417 shortname.prepend (topdirname + "\\"); |
|
1418 |
|
1419 return shortname; |
|
1420 } |
|
1421 |
|
1422 // ============================================================================= |
|
1423 // |
|
1424 void LDDocument::addReference (LDDocumentPointer* ptr) |
|
1425 { |
|
1426 m_references << ptr; |
|
1427 } |
|
1428 |
|
1429 // ============================================================================= |
|
1430 // |
|
1431 void LDDocument::removeReference (LDDocumentPointer* ptr) |
|
1432 { |
|
1433 m_references.removeOne (ptr); |
|
1434 |
|
1435 if (references().isEmpty()) |
|
1436 invokeLater (closeUnused); |
|
1437 } |